define([
"jquery",
"underscore",
"backbone",
"models/filters/ChoiceFilter",
"views/filters/FilterView",
"text!templates/filters/choiceFilter.html",
], function ($, _, Backbone, ChoiceFilter, FilterView, Template) {
"use strict";
/**
* @class ChoiceFilterView
* @classdesc Render a view of a single ChoiceFilter model
* @classcategory Views/Filters
* @extends FilterView
*/
var ChoiceFilterView = FilterView.extend(
/** @lends ChoiceFilterView.prototype */ {
/**
* A ChoiceFilter model to be rendered in this view
* @type {ChoiceFilter} */
model: null,
/**
* @inheritdoc
*/
modelClass: ChoiceFilter,
className: "filter choice",
template: _.template(Template),
/**
* When this view is in "uiBuilder" mode, the class name for the handles on each choice
* row that the user can click and drag to re-order
* @type {string}
*/
choiceHandleClass: "handle",
/**
* The class to add to the element that a user should click to remove a choice
* value and label when this view is in "uiEditor" mode
* @since 2.17.0
* @type {string}
*/
removeChoiceClass: "remove-choice",
/**
* A function that creates and returns the Backbone events object.
* @return {Object} Returns a Backbone events object
*/
events: function () {
try {
var events = FilterView.prototype.events.call(this);
events["change select"] = "handleChange";
var removeClass = "." + this.removeChoiceClass;
events["click " + removeClass] = "removeChoice";
events["mouseover " + removeClass] = "previewRemoveChoice";
events["mouseout " + removeClass] = "previewRemoveChoice";
return events;
} catch (error) {
console.log(
"There was an error creating the events object for a ChoiceFilterView" +
" Error details: " +
error,
);
}
},
render: function () {
var view = this;
// Renders the template and inserts the FilterEditorView if the mode is uiBuilder
FilterView.prototype.render.call(this);
var placeHolderText = this.model.get("placeholder");
var select = this.$("select");
if (this.mode === "uiBuilder") {
// If this is the filter view where the user can edit the filter UI options,
// like the label, placeholder text, and choices, then render the inputs
// for these options.
// The ignore-changes class prevents the editor footer from showing on keypress
var placeholderInput = $(
'<input placeholder="placeholder" class="' +
this.uiInputClass +
' placeholder ignore-changes" data-category="placeholder" value="' +
(placeHolderText ? placeHolderText : "") +
'" />',
);
// Replace the select element with the placeholder text element
placeholderInput.insertAfter(select);
// Create the interface for a user to edit the value-label choice options
var choicesEditor = this.createChoicesEditor();
view.$el.append(choicesEditor);
} else {
// For regular search filter views, or the edit filter view, render the dropdown
// interface
//Create the placeholder text for the dropdown menu
//If placeholder text is already provided in the model, use it
//If not, create placeholder text using the model label
if (!placeHolderText) {
if (this.model.get("label")) {
//If the label starts with a vowel, use "an"
var vowels = ["a", "e", "i", "o", "u"];
if (
_.contains(
vowels,
this.model.get("label").toLowerCase().charAt(0),
)
) {
placeHolderText = "Choose an " + this.model.get("label");
}
//Otherwise use "a"
else {
placeHolderText = "Choose a " + this.model.get("label");
}
}
}
//Create the default option
var defaultOption = $(document.createElement("option"))
.attr("value", "")
.text(placeHolderText);
select.append(defaultOption);
//Create an option element for each choice listen in the model
_.each(
this.model.get("choices"),
function (choice) {
select.append(
$(document.createElement("option"))
.attr("value", choice.value)
.text(choice.label),
);
},
this,
);
//When the ChoiceFilter is changed, update the choice list in the UI
this.listenTo(this.model, "change:values", this.updateChoices);
this.listenTo(this.model, "remove", this.updateChoices);
}
},
/**
* Create the set of inputs where a use can select label-value pairs for the regular
* choice filter view
* @since 2.17.0
*/
createChoicesEditor: function () {
try {
var view = this;
this.choicesEditor = $("<div class='choices-editor'></div>");
var choicesEditorText = $(
"<p class='modal-instructions'>Allow people to select from the following search terms</p>",
);
var choiceEditorError = $(
"<p class='notification error' data-category='choices' style='display: none'></p>",
);
var labelContainer = $(
"<div class='choice-editor unsortable'></div>",
);
this.choicesEditor.append(
choicesEditorText,
choiceEditorError,
labelContainer,
);
labelContainer.append(
"<p class='ui-builder-container-text choice-label subtle'>Enter the text to display</p>",
);
labelContainer.append(
"<p class='ui-builder-container-text choice-value subtle'>Enter the text to search for</p>",
);
_.each(
this.model.get("choices"),
function (choice) {
var choiceEditorEl = this.createChoiceEditor(choice);
this.choicesEditor.append(choiceEditorEl);
},
this,
);
// Create a blank choice at the end
this.addEmptyChoiceEditor();
// Initialize choice drag and drop to re-order functionality
require(["sortable"], function (Sortable) {
Sortable.create(view.choicesEditor[0], {
direction: "vertical",
easing: "cubic-bezier(1, 0, 0, 1)",
animation: 200,
handle: "." + view.choiceHandleClass,
draggable: ".choice-editor:not(.unsortable)",
onUpdate: function (evt) {
// When the choice order is changed, update the filter model
view.updateModelChoices();
},
});
});
return this.choicesEditor;
} catch (error) {
console.log(
"There was an error creating choices editor in a ChoiceFilterView" +
" Error details: " +
error,
);
}
},
/**
* Create a row where a user can input a value and label for a single choice.
* @since 2.17.0
*/
createChoiceEditor: function (choice) {
try {
if (!choice) {
return;
}
var view = this;
// Create the choice container
var choiceContainer = $("<div class='choice-editor'></div>");
// Create the click and drag handle
var handle = $(
'<span class="' +
view.choiceHandleClass +
'">' +
'<i class= "icon icon-ellipsis-vertical" ></i>' +
'<i class="icon icon-ellipsis-vertical"></i>' +
"</span >",
);
choiceContainer.append(handle);
// Create inputs for "value" and "label", insert them in the container
for (const [attrName, attrValue] of Object.entries(choice)) {
var inputEl = $("<input>").attr({
// The ignore-changes class prevents the editor footer from showing on keypress
class: "ignore-changes choice-input choice-" + attrName,
value: attrValue,
"data-category": attrName,
});
// Update the values in the model when the user focuses out of an input
inputEl.on("blur", function () {
view.updateModelChoices.call(view);
});
choiceContainer.append(inputEl);
}
// Create the remove "X" button. Save references to the parent choice container so
// that we can remove it from the view when the button is clicked
var removeButton = $(
"<i class='icon icon-remove " +
this.removeChoiceClass +
"' title='Remove this choice'></i>",
).data({
choiceEl: choiceContainer,
});
// Insert the remove button into the choice container
choiceContainer.append(removeButton);
return choiceContainer;
} catch (error) {
console.log(
"There was an error ChoiceFilterView" + " Error details: " + error,
);
}
},
/**
* Create an empty choice editor row
* @since 2.17.0
*/
addEmptyChoiceEditor: function () {
try {
var view = this;
var choice = {
label: "",
value: "",
};
var choiceEditorEl = this.createChoiceEditor(choice);
this.choicesEditor.append(choiceEditorEl);
// Don't let users remove or sort the new choice entry fields until some text has
// been entered
var removeButton = choiceEditorEl.find("." + this.removeChoiceClass);
var handle = choiceEditorEl.find("." + this.choiceHandleClass);
removeButton.hide();
handle.hide();
choiceEditorEl.addClass("unsortable");
// The inputs for value and label
var inputs = choiceEditorEl.find("input");
var onInputChange = function () {
choiceEditorEl.removeClass("unsortable");
removeButton.show();
handle.show();
view.addEmptyChoiceEditor();
inputs.off("input", onInputChange);
};
inputs.on("input", onInputChange);
} catch (error) {
console.log(
"There was an error creating a choice editor in a ChoiceFilterView" +
" Error details: " +
error,
);
}
},
/**
* Indicate to the user that the choice value and label inputs will be removed when
* they hover over the remove button.
* @since 2.17.0
*/
previewRemoveChoice: function (e) {
try {
var normalOpacity = 1.0,
previewOpacity = 0.2,
speed = 120;
var removeEl = $(e.target);
var subElements = removeEl.data("choiceEl").children().not(removeEl);
if (e.type === "mouseover") {
subElements.fadeTo(speed, previewOpacity);
$(removeEl).fadeTo(speed, normalOpacity);
}
if (e.type === "mouseout") {
subElements.fadeTo(speed, normalOpacity);
$(removeEl).fadeTo(speed, previewOpacity);
}
} catch (error) {
console.log(
"Error showing a preview of the removal of a Choice editor in a " +
"Choice Filter View, details: " +
error,
);
}
},
/**
* Remove a choice editor row and the corresponding label-value pair from the choice
* Filter Model (TODO)
* @since 2.17.0
* @param {Object} e The click event object
*/
removeChoice: function (e) {
try {
var choiceEl = $(e.target).data("choiceEl");
// See how many choice elements there are (subtract one because the label elements
// are within a choice-editor element)
var numChoices = this.$el.find(".choice-editor").length - 1;
// Don't allow removing the last choice element. Empty the last element and hide the
// remove button instead.
if (numChoices <= 1) {
choiceEl.find("input").val("");
} else {
// Remove the choice editor element from the view, plus any listeners
choiceEl.off();
choiceEl.remove();
}
// Update the choices in the model
this.updateModelChoices();
} catch (error) {
console.log(
"There was an error removing a choice editor in the ChoiceFilterView" +
" Error details: " +
error,
);
}
},
/**
* Update the choices attribute in the choiceFilter model based on the values in the
* choices editor
* @since 2.17.0
*/
updateModelChoices: function () {
try {
// The array of label-value pairs that will be set on the choiceFilter model.
var newChoices = [];
// Find each choice editor container, and find the values from the two inputs
// within.
this.$el.find(".choice-editor").each(function () {
var choiceEditor = $(this);
var valueEl = choiceEditor.find("[data-category='value']");
var labelEl = choiceEditor.find("[data-category='label']");
if (valueEl.length && labelEl.length) {
var newValue = valueEl[0].value;
var newLabel = labelEl[0].value;
// TODO: validate the label/value here and show error if choice is not
// complete.
if (!newValue && !newLabel) {
// Don't add empty choices to the model
return;
} else {
newChoices.push({
label: newLabel,
value: newValue,
});
}
}
});
// Replace the choices in the model with the new array with new values
this.model.set("choices", newChoices);
} catch (error) {
console.log(
"There was an error updating the choices in a ChoiceFilterView" +
" Error details: " +
error,
);
}
},
/**
* Updates the value set on the ChoiceFilter Model associated with this view.
* The filter value is grabbed from the select element in this view.
*
*/
updateModel: function () {
//Get the new value from the text input
var newValue = this.$("select").val();
//Get the current values array from the model
var currentValue = this.model.get("values");
//If the ChoiceFilter allows multiple values to be added,
// add the new choice to the values array
if (this.model.get("chooseMultiple")) {
//Duplicate the current values array
var newValuesArray = currentValue.slice(0);
//Add the new value to the array
newValuesArray.push(newValue);
//Set the new values array on the model
this.model.set("values", newValuesArray);
}
//If multiple choices are not allowed,
else {
//Replace the first index of the array with the new value
var newValuesArray = currentValue.slice(0);
newValuesArray[0] = newValue;
//Set the new values array on the model
this.model.set("values", newValuesArray);
}
},
/**
* Update the choices in the select dropdown menu based on which choices are
* currently selected
*/
updateChoices: function () {
//Enable all the choices
this.$("option").prop("disabled", false);
//Get the currently-selected choices
var selectedChoices = this.model.get("values");
_.each(
selectedChoices,
function (choice) {
//Find each choice in the dropdown menu and disable it
this.$("option[value='" + choice + "']").prop("disabled", true);
},
this,
);
},
/**
* Show validation errors. This is used for filters that are in "UIBuilder" mode.
* @param {Object} errors The error messages associated with each attribute that has
* an error, passed from the Filter model validation function.
*/
showValidationErrors: function (errors) {
try {
var view = this;
// Select the messages container for the choice error (added in the template)
var messageContainer = view.el.querySelector(
".notification[data-category='choices']",
);
// Show errors for label, placeholder, etc (elements common to all FilterViews)
FilterView.prototype.showValidationErrors.call(this, errors);
// Show errors in the choices editor
var inputs = this.choicesEditor.find("input");
// Add error styling to all the choices inputs. Remove error styling (and input
// listeners) from all inputs when there is text in at least one of them
var handleInput = function () {
inputs.each(function (i, input) {
view.hideInputError(input, messageContainer);
input.removeEventListener("input", handleInput);
});
};
if (inputs.length) {
inputs.each(function (i, input) {
view.showInputError(input);
input.addEventListener("input", handleInput);
});
}
} catch (error) {
console.log(
"There was an error showing validation errors in a FilterView" +
". Error details: " +
error,
);
}
},
},
);
return ChoiceFilterView;
});