/*global define */
define(['jquery', 'underscore', 'backbone',
'models/filters/Filter',
'text!templates/filters/filter.html',
'text!templates/filters/filterLabel.html'],
function($, _, Backbone, Filter, Template, LabelTemplate) {
'use strict';
/**
* @class FilterView
* @classdesc Render a view of a single FilterModel
* @classcategory Views/Filters
* @extends Backbone.View
*/
var FilterView = Backbone.View.extend(
/** @lends FilterView.prototype */{
/**
* A Filter model to be rendered in this view
* @type {Filter} */
model: null,
/**
* The Filter model that this View renders. This is used to create a new
* instance of the model if one is not provided to the view.
* @type {Backbone.Model}
* @since 2.17.0
*/
modelClass: Filter,
tagName: "div",
className: "filter",
/**
* Reference to template for this view. HTML files are converted to Underscore.js
* templates
* @type {Underscore.Template}
*/
template: _.template(Template),
/**
* The template that renders the icon and label of a filter
* @type {Underscore.Template}
* @since 2.17.0
*/
labelTemplate: _.template(LabelTemplate),
/**
* One of "normal", "edit", or "uiBuilder". "normal" renders a regular filter used to
* update a search model in a DataCatalogViewWithFilters. "edit" creates a filter that
* cannot update a search model, but which has an "EDIT" button that opens a modal
* with an interface for editing the filter model's properties (e.g. fields, model
* type, etc.). "uiBuilder" is the view of the filter within this editing modal; it
* has inputs that are overlaid above the filter elements where a user can edit the
* placeholder text, label, etc. in a WYSIWYG fashion.
* @type {string}
* @since 2.17.0
*/
mode: "normal",
/**
* The class to add to the filter when it is in "uiBuilder" mode
* @type {string}
* @since 2.17.0
*/
uiBuilderClass: "ui-build",
/**
* Whether the filter is collapsible. If true, the filter will have a button that
* toggles the collapsed state.
* @type {boolean}
* @since 2.25.0
*/
collapsible: false,
/**
* The class to add to the filter when it is collapsed.
* @type {string}
* @since 2.25.0
* @default "collapsed"
*/
collapsedClass: "collapsed",
/**
* The class used for the button that toggles the collapsed state of the filter.
* @type {string}
* @since 2.25.0
* @default "collapse-toggle"
*/
collapseToggleClass: "collapse-toggle",
/**
* The current state of the filter, if it is {@link FilterView#collapsible}.
* Whatever this value is set to at initialization, will be how the filter is
* initially rendered.
* @type {boolean}
* @since 2.25.0
* @default true
*/
collapsed: true,
/**
* The class used for input elements where the user can change UI attributes when this
* view is in "uiBuilder" mode. For example, the input for the placeholder text should
* have this class. Elements with this class also need to have a data-category
* attribute with the name of the model attribute they correspond to.
* @type {string}
* @since 2.17.0
*/
uiInputClass: "ui-build-input",
/**
* A function that creates and returns the Backbone events object.
* @return {Object} Returns a Backbone events object
*/
events: function(){
try {
var events = {
"click .btn": "handleChange",
"keydown input": "handleTyping"
}
events["change ." + this.uiInputClass] = "updateUIAttribute";
events[`click .${this.collapseToggleClass}`] = "toggleCollapse";
return events
}
catch (error) {
console.log( 'There was an error setting the events object in a FilterView' +
' Error details: ' + error );
}
},
/**
* Function executed whenever a new FilterView is created.
* @param {Object} [options] - A literal object of options to set on this View
*/
initialize: function (options) {
try {
if (!options || typeof options != "object") {
var options = {};
}
this.editorView = options.editorView || null;
if (options.mode && ["edit", "uiBuilder", "normal"].includes(options.mode)) {
this.mode = options.mode
}
// When this view is being rendered in an editable mode (e.g. in the custom search
// filter editor), then overwrite the functions that update the search model. This
// way the user can interact with the filter without causing the
// dataCatalogViewWithFilters to update the search results. For simplicity, and
// because extended Filter Views call this function, update functions from other
// types of Filter views are included here.
if (["edit", "uiBuilder"].includes(this.mode)) {
var functionsToOverwrite = [
"updateModel", "handleChange", "handleTyping",
"updateChoices", "updateToggle", "updateYearRange"
]
functionsToOverwrite.forEach(function (fnName) {
if (typeof this[fnName] === "function") {
this[fnName] = function () { return }
}
}, this)
}
this.model = options.model || new this.modelClass();
if (options.collapsible && typeof options.collapsible === "boolean") {
this.collapsible = options.collapsible;
}
}
catch (error) {
console.log( 'There was an error initializing a FilterView' +
' Error details: ' + error );
}
},
/**
* Render an instance of a Filter View. All of the extended Filter Views also call
* this render function.
* @param {Object} templateVars - The variables to use in the HTML template. If not
* provided, defaults to the model in JSON
*/
render: function (templateVars) {
try {
var view = this;
if(!templateVars){
var templateVars = this.model.toJSON()
}
// Pass the mode (e.g. "edit", "uiBuilder") to the template, as well
// as the variables related to collapsibility.
const viewVars = {
mode: this.mode,
collapsible: this.collapsible,
collapseToggleClass: this.collapseToggleClass
}
templateVars = _.extend(templateVars, viewVars)
// Render the filter HTML (without label or icon)
this.$el.html( this.template( templateVars ) );
// Add the filter label & icon (common between most filters)
this.$el.prepend( this.labelTemplate( templateVars ) );
// a FilterEditorView adds an "EDIT" button, which opens a modal allowing the user
// to change the UI options of the filter - e.g., label, icon, placeholder text,
// etc.
if(this.mode === "edit"){
require(['views/filters/FilterEditorView'], function(FilterEditor){
var filterEditor = new FilterEditor({
model: view.model,
editorView: view.editorView
});
filterEditor.render();
view.$el.prepend(filterEditor.el);
});
}
if(this.mode === "uiBuilder"){
this.$el.addClass(this.uiBuilderClass);
}
// Don't show the editor footer with save button when a user types text into
// a filter in edit or build mode.
if(["edit", "uiBuilder"].includes(this.mode)){
this.$el.find("input").addClass("ignore-changes")
}
// If the filter is collapsible, set the initial collapsed state
if(this.collapsible && typeof this.collapsed === "boolean"){
this.toggleCollapse(this.collapsed)
}
}
catch (error) {
console.log( 'There was an error rendering a FilterView' +
' Error details: ' + error );
}
},
/**
* When the user presses Enter in the input element, update the view and model
*
* @param {Event} - The DOM Event that occurred on the filter view input element
*/
handleTyping: function (e) {
if (["edit", "uiBuilder"].includes(this.mode)) {
return
}
if (e.key == "Enter"){
this.handleChange();
return;
}
else{
/** @todo Get search suggestions when the user is typing. See {@link DataCatalogView#getAutoCompletes }*/
}
},
/**
* Updates the view when the filter input is updated
*
* @param {Event} - The DOM Event that occurred on the filter view input element
*/
handleChange: function () {
if (["edit", "uiBuilder"].includes(this.mode)) {
return
}
this.updateModel();
//Clear the value of the text input
this.$("input").val("");
},
/**
* Updates the value set on the Filter Model associated with this view.
* The filter value is grabbed from the input element in this view.
*/
updateModel: function () {
if (["edit", "uiBuilder"].includes(this.mode)) {
return
}
//Get the new value from the text input
var newValue = this.$("input").val();
if( newValue == "" )
return;
//Get the current values array from the model
var currentValue = this.model.get("values");
//Create a copy of the array
var newValuesArray = _.flatten(new Array(currentValue, newValue));
//Trigger the change event manually since it is an array
this.model.set("values", newValuesArray);
},
/**
* Updates the corresponding model attribute when an input for one of the UI options
* changes (in "uiBuilder" mode).
* @param {Object} e The change event
* @since 2.17.0
*/
updateUIAttribute: function(e){
try {
if (this.mode != "uiBuilder") {
return
}
var inputEl = e.target;
if (!inputEl) {
return
}
if (!inputEl.dataset || !inputEl.dataset.category) {
return
}
var modelAttribute = inputEl.dataset.category,
newValue = inputEl.value;
if (inputEl.type === "number") {
newValue = parseInt(newValue)
}
this.model.set(modelAttribute, newValue)
}
catch (error) {
console.log( 'There was an error updating a UI attribute in a FilterView' +
' Error details: ' + error );
}
},
/**
* 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;
var uiInputClass = this.uiInputClass;
for (const [category, message] of Object.entries(errors)) {
const input = view.el.querySelector("." + uiInputClass + "[data-category='" + category + "']");
const messageContainer = view.el.querySelector(".notification[data-category='" + category + "']");
view.showInputError(input, messageContainer, message);
if (input) {
input.addEventListener('input', function () {
view.hideInputError(input, messageContainer)
}, { once: true })
}
}
}
catch (error) {
console.log(
'There was an error showing validation errors in a FilterView' +
'. Error details: ' + error
);
}
},
/**
* This function indicates that there is an error with an input in this filter. It
* displays an error message and adds the error CSS class to the problematic input.
* @param {HTMLElement} input The input that has an error associated with its value
* @param {HTMLElement} messageContainer The element in which to insert the error
* message
* @param {string} message The error message to show
*/
showInputError: function (input, messageContainer, message) {
try {
if (messageContainer && message) {
messageContainer.innerText = message;
messageContainer.style.display = "block";
}
if (input) {
input.classList.add("error");
}
}
catch (error) {
console.log(
'Failed to show an error message for an input in a FilterView' +
'. Error details: ' + error
);
}
},
/**
* This function hides the error message and error class added to inputs with the
* FilterView#showInputError function.
* @param {HTMLElement} input The input that had an error associated with its value
* @param {HTMLElement} messageContainer The element in which the error message was
* inserted
*/
hideInputError: function (input, messageContainer) {
try {
if (messageContainer) {
messageContainer.innerText = "";
messageContainer.style.display = "none";
}
if (input) {
input.classList.remove("error");
}
}
catch (error) {
console.log(
'Failed to hide the error message for an input in a FilterView' +
'. Error details: ' + error
);
}
},
/**
* Toggle the collapsed state of the filter. If collapse is a boolean, then set the
* collapsed state to that value. Otherwise, set it to the opposite of whichever
* state is currently set.
* @param {boolean} [collapse] Whether to collapse the filter. If not provided, the
* filter will be collapsed if it is currently expanded, and vice versa.
* @since 2.25.0
*/
toggleCollapse: function (collapse) {
try {
// If collapse is a boolean, then set the collapsed state to that value.
// Otherwise, set it to the opposite of whichever state is currently set.
if (typeof collapse !== "boolean") {
collapse = !this.collapsed
}
if (collapse) {
this.el.classList.add(this.collapsedClass)
this.collapsed = true
} else {
this.el.classList.remove(this.collapsedClass)
this.collapsed = false
}
}
catch (e) {
console.log("Could not un/collapse filter.", e);
}
},
});
return FilterView;
});