define([
"jquery",
"underscore",
"backbone",
"models/filters/Filter",
"models/filters/ChoiceFilter",
"models/filters/DateFilter",
"models/filters/ToggleFilter",
"collections/queryFields/QueryFields",
"views/searchSelect/QueryFieldSelectView",
"views/filters/FilterView",
"views/filters/ChoiceFilterView",
"views/filters/DateFilterView",
"views/filters/ToggleFilterView",
"text!templates/filters/filterEditor.html",
], function (
$,
_,
Backbone,
Filter,
ChoiceFilter,
DateFilter,
ToggleFilter,
QueryFields,
QueryFieldSelect,
FilterView,
ChoiceFilterView,
DateFilterView,
ToggleFilterView,
Template,
) {
"use strict";
/**
* @class FilterEditorView
* @classdesc Creates a view of an editor for a custom search filter
* @classcategory Views/Filters
* @screenshot views/filters/FilterEditorView.png
* @since 2.17.0
* @name FilterEditorView
* @extends Backbone.View
* @constructor
*/
var FilterEditorView = Backbone.View.extend(
/** @lends FilterEditorView.prototype */ {
/**
* A Filter model to be rendered and edited in this view. The Filter model must be
* part of a Filters collection.
// TODO: Add support for boolean and number filters
* @type {Filter|ChoiceFilter|DateFilter|ToggleFilter}
*/
model: null,
/**
* If rendering an editor for a brand new Filter model, provide the Filters
* collection instead of the Filter model. A new model will be created and, if the
* user clicks save, it will be added to this Filters collection.
* @type {Filters}
*/
collection: null,
/**
* A reference to the PortalEditorView
* @type {PortalEditorView}
*/
editorView: undefined,
/**
* Set to true if rendering an editor for a brand new Filter model that is not yet
* part of a Filters collection. If isNew is set to true, then the view requires a
* Filters model set to the view's collection property. A model will be created.
*/
isNew: false,
/**
* The HTML classes to use for this view's element
* @type {string}
*/
className: "filter-editor",
/**
* References to the template for this view. HTML files are converted to
* Underscore.js templates
* @type {Underscore.Template}
*/
template: _.template(Template),
/**
* The classes to use for various elements in this view
* @type {Object}
* @property {string} fieldsContainer - the element in the template that
* will contain the input where a user can select metadata fields for the custom
* search filter.
* @property {string} editButton - The button a user clicks to start
* editing a search filter
* @property {string} cancelButton - the element in the template that a
* user clicks to undo any changes made to the filter and close the editing modal.
* @property {string} saveButton - the element in the template that a user
* clicks to add their filter changes to the parent Filters collection and close
* the editing modal.
* @property {string} deleteButton - the element in the template that a
* user clicks to remove the Filter model from the Filters collection
* @property {string} uiBuilderChoicesContainer - The container for the
* uiBuilderChoices and the associated instruction text
* @property {string} uiBuilderChoices - The container for each "button" a
* user can click to switch the filter type
* @property {string} uiBuilderChoice - The element that acts like a
* button that switches the filter type
* @property {string} uiBuilderChoiceActive - The class to add to a
* uiBuilderChoice buttons when that option is active/selected
* @property {string} uiBuilderLabel - The label that goes along with the
* uiBuilderChoice element
* @property {string} uiBuilderContainer - The element that will be turned
* into a carousel that switches between each UI Builder view when a user switches
* the filter type
* @property {string} modalInstructions - The class to add to the
* instruction text in the editing modal window
*/
classes: {
fieldsContainer: "fields-container",
editButton: "edit-button",
cancelButton: "cancel-button",
saveButton: "save-button",
deleteButton: "delete-button",
uiBuilderChoicesContainer: "ui-builder-choices-container",
uiBuilderChoices: "ui-builder-choices",
uiBuilderChoice: "ui-builder-choice",
uiBuilderChoiceActive: "selected",
uiBuilderLabel: "ui-builder-choice-label",
uiBuilderContainer: "ui-builder-container",
modalInstructions: "modal-instructions",
},
/**
* Strings to use to display various messages to the user in this view
* @property {string} editButton - The text to show in the button a user clicks to
* open the editing modal window.
* @property {string} addFilterButton - The text to show in the button a user
* clicks to add a new search filter and open an editing modal window.
* @property {string} step1 - The instructions placed just before the fields input
* @property {string} step2 - The instructions placed after the fields input and
* before the uiBuilder select
* @property {string} filterNotAllowed - The message to show when a filter type
* doesn't work with the selected metadata fields
* @property {string} saveButton - Text for the button at the bottom of the
* editing modal that adds the filter model changes to the parent Filters
* collection and closes the modal
* @property {string} cancelButton - Text for the button at the bottom of the
* editing modal that closes the modal window without making any changes.
* @property {string} deleteButton - Text for the button at the bottom of the
* editing modal that removes the Filter model from the Filters collection.
* @property {string} validationError - The message to show at the top of the
* modal when there is at least one validation error.
* @property {string} noFilterOption - The message to show when there is no UI
* available for the selected field or combination of fields.
*/
text: {
editButton: "EDIT",
addFilterButton: "Add a search filter",
step1: "Let people filter your data by",
step2: "...using the following interface",
filterNotAllowed:
"This interface doesn't work with the metadata fields you" +
" selected. Change the 'filter data by' option to use this interface.",
saveButton: "Use these filter settings",
cancelButton: "Cancel",
deleteButton: "Remove filter",
validationError:
"Please provide the content flagged below before saving this " +
"search filter.",
noFilterOption:
"There are currently no filter options available to support " +
"this field, or this combination of fields. Change the 'filter data by' " +
"option to select an interface.",
},
/**
* A function that returns a Backbone events object
* @return {object} A Backbone events object - an object with the events this view
* will listen to and the associated function to call.
*/
events: function () {
var events = {};
events["click ." + this.classes.uiBuilderChoice] =
"handleFilterIconClick";
return events;
},
/**
* A list of query fields names to exclude from the list of options in the
* QueryFieldSelectView
* @type {string[]}
*/
excludeFields: MetacatUI.appModel.get("collectionQueryExcludeFields"),
/**
* An additional field object contains the properties for an additional query
* field to add to the QueryFieldSelectView that are required to render it
* correctly. An additional query field is one that does not actually exist in the
* query service index.
*
* @typedef {Object} AdditionalField
*
* @property {string} name - A unique ID to represent this field. It must not
* match the name of any other query fields.
* @property {string[]} fields - The list of real query fields that this
* abstracted field will represent. It must exactly match the names of the query
* fields that actually exist.
* @property {string} label - A user-facing label to display.
* @property {string} description - A description for this field.
* @property {string} category - The name of the category under which to place
* this field. It must match one of the category names for an existing query
* field.
*/
/**
* A list of additional fields which are not retrieved from the query service
* index, but which should be added to the list of options in the
* QueryFieldSelectView. This can be used to add abstracted fields which are a
* combination of multiple query fields, or to add a duplicate field that has a
* different label.
*
* @type {AdditionalField[]}
*/
specialFields: [],
/**
* The path to the directory that contains the SVG files which are used like an
* icon to represent each UI type
* @type {string}
*/
iconDir: "templates/filters/filterIcons/",
/**
* A single type of custom search filter that a user can select. An option
* represents a specific Filter model type and uses that associated Filter View.
* @typedef {Object} UIBuilderOption
* @property {string} label - The user-facing label to show for this option
* @property {string} modelType - The name of the filter model type that that this
* UI builder should create. Only one is allowed. The model must be one of the six
* filters that are allowed in a Portal "UIFilterGroupType". See
* {@link https://github.com/DataONEorg/collections-portals-schemas/blob/master/schemas/portals.xsd}.
* @property {string} iconFileName - The file name, including extension, of the SVG
* icon used to represent this option
* @property {string} description - A very brief, user-facing description of how
* this filter works
* @property {string[]} filterTypes - An array of one or more filter types that are
* allowed for this interface. If none are provided then any filter type is
* allowed. Filter types are one of the four keys defined in
* @property {string[]} blockedFields - An array of one or more search
* fields for which this interface should be blocked
* {@link QueryField#filterTypesMap}, and correspond to one of the four filter
* types that are allowed in a Collection definition. See
* {@link https://github.com/DataONEorg/collections-portals-schemas/blob/master/schemas/collections.xsd}.
* This property is used to help users match custom search filter UIs to
* appropriate query fields.
* @property {function} modelFunction - A function that takes an optional object
* with model properties and returns an instance of a model to use for this UI
* builder
* @property {function} uiFunction - A function that takes the model as an argument
* and returns the filter UI builder view for this option
*/
/**
* The list of UI types that a user can select from. They will appear in the
* carousel in the order they are listed here.
* @type {UIBuilderOption[]}
*/
uiBuilderOptions: [
{
label: "Free text",
modelType: "Filter",
iconFileName: "filter.svg",
description: "Allow people to search using any text they enter",
filterTypes: ["filter"],
blockedFields: [],
modelFunction: function (attrs) {
return new Filter(attrs);
},
uiFunction: function (model) {
return new FilterView({
model: model,
mode: "uiBuilder",
});
},
},
{
label: "Dropdown",
modelType: "ChoiceFilter",
iconFileName: "choice.svg",
description:
"Allow people to select a search term from a list of options",
filterTypes: ["filter"],
blockedFields: [...MetacatUI.appModel.get("querySemanticFields")],
modelFunction: function (attrs) {
return new ChoiceFilter(attrs);
},
uiFunction: function (model) {
return new ChoiceFilterView({
model: model,
mode: "uiBuilder",
});
},
},
{
label: "Year slider",
modelType: "DateFilter",
iconFileName: "number.svg",
description: "Let people search for a range of years",
filterTypes: ["dateFilter"],
blockedFields: [...MetacatUI.appModel.get("querySemanticFields")],
modelFunction: function (attrs) {
return new DateFilter(attrs);
},
uiFunction: function (model) {
return new DateFilterView({
model: model,
mode: "uiBuilder",
});
},
},
{
label: "Toggle",
modelType: "ToggleFilter",
iconFileName: "toggle.svg",
description:
"Let people add or remove a single, specific search term",
filterTypes: ["filter"],
blockedFields: [...MetacatUI.appModel.get("querySemanticFields")],
modelFunction: function (attrs) {
return new ToggleFilter(attrs);
},
uiFunction: function (model) {
return new ToggleFilterView({
model: model,
mode: "uiBuilder",
});
},
},
],
/**
* Executed when this view is created
* @param {object} options - A literal object of options to pass to this view
* @property {Filter|ChoiceFilter|DateFilter|ToggleFilter} options.model - The
* filter model to render an editor for. It must be part of a Filters collection.
*/
initialize: function (options) {
try {
// Ensure the query fields are cached for limitUITypes()
if (typeof MetacatUI.queryFields === "undefined") {
MetacatUI.queryFields = new QueryFields();
MetacatUI.queryFields.fetch();
}
if (!options || typeof options != "object") {
var options = {};
}
this.editorView = options.editorView || null;
if (!options.isNew) {
// If this view is an editor for an existing Filter model, check that the model
// and the Filters collection is provided.
if (!options.model) {
console.log(
"A Filter model is required to render a Filter Editor View",
);
return;
}
if (!options.model.collection) {
console.log(
"The Filter model for a FilterEditorView must be part of a" +
" Filters collection",
);
return;
}
// Set the model and collection on the view
this.model = options.model;
this.collection = options.model.collection;
} else {
// If this is an editor for a new Filter model, create a default model and
// make sure there is a Filters collection to add it to
if (!options.collection) {
console.log(
"A Filters collection is required to render a " +
"FilterEditorView for a new Filters model.",
);
return;
}
this.model = new Filter();
this.collection = options.collection;
this.isNew = true;
}
} catch (error) {
console.log(
"Error creating an FilterEditorView. Error details: " + error,
);
}
},
/**
* Render the view
*/
render: function () {
try {
// Save a reference to this view
var view = this;
// Create and insert an "edit" or a "add filter" button for the filter.
var buttonText = this.text.editButton,
buttonClasses = this.classes.editButton,
buttonIcon = "pencil";
// Text & styling is different for the "add a new filter" button
if (this.isNew) {
buttonText = this.text.addFilterButton;
buttonIcon = "plus";
buttonClasses = buttonClasses + " btn";
this.$el.addClass("new");
}
var editButton = $(
"<a class='" +
buttonClasses +
"'>" +
"<i class='icon icon-" +
buttonIcon +
" icon-on-left'></i> " +
buttonText +
"</a>",
);
this.$el.prepend(editButton);
// Render the editor modal on-the-fly to make the application load faster.
// No need to create editing modals for filters that a user doesn't edit.
editButton.on("click", function () {
view.renderEditorModal.call(view);
});
// Save a reference to this view
this.$el.data("view", this);
return this;
} catch (error) {
console.log(
"Error rendering an FilterEditorView. Error details: " + error,
);
}
},
/**
* Render and show the modal window that has all the components for editing a
* filter. This is created on-the-fly because creating these modals all at once in
* a FilterGroupsView in edit mode takes too much time.
*/
renderEditorModal: function () {
try {
// Save a reference to this view
var view = this;
// The list of UI Filter Editor options needs to be mutable. We will save the
// draft filter models, and the associated editor views to this list. Rewrite
// this.uiBuilders every time the editor modal is re-rendered.
this.uiBuilders = [];
this.uiBuilderOptions.forEach(function (opt) {
this.uiBuilders.push(_.clone(opt));
}, this);
// Create and insert the modal window that will contain the editing interface
var modalHTML = this.template({
classes: view.classes,
text: view.text,
});
this.modalEl = $(modalHTML);
this.$el.append(this.modalEl);
// Start rendering the metadata field input only after the modal is shown.
// Otherwise this step slows the rendering down, leaves too much of a delay
// before the modal appears.
this.modalEl.off();
this.modalEl.on("shown", function (event) {
view.modalEl.off("shown");
view.renderFieldInput();
});
this.modalEl.modal("show");
// Add listeners to the modal buttons save or cancel changes
this.activateModalButtons();
// Create and insert the "buttons" to switch filter type, and the elements
// that will contain the UI building interfaces for each filter type.
this.renderUIBuilders();
// Select and render the UI Filter Editor for the filter model set on this
// view.
this.switchFilterType();
// Disable any filter types that do not match the currently selected fields
this.handleFieldChange(_.clone(view.model.get("fields")));
} catch (error) {
console.log(
"There was an error rendering the modal in a FilterEditorView" +
" Error details: " +
error,
);
}
},
/**
* Hide and destroy the filter editor modal window
*/
hideModal: function () {
try {
var view = this;
view.modalEl.off("hidden");
view.modalEl.on("hidden", function () {
view.modalEl.off();
view.modalEl.remove();
});
view.modalEl.modal("hide");
} catch (error) {
console.log(
"There was an error hiding the editing modal in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Find the save and cancel buttons in the editing modal window, and add listeners
* that close the modal and update the Filters collection on save
*/
activateModalButtons: function () {
try {
var view = this;
// The buttons at the bottom of the modal
var saveButton = this.modalEl.find("." + this.classes.saveButton),
cancelButton = this.modalEl.find("." + this.classes.cancelButton),
deleteButton = this.modalEl.find("." + this.classes.deleteButton);
// Add listeners to the modal's "delete", "save", and "cancel" buttons.
// SAVE
saveButton.on("click", function (event) {
// Don't allow user to save a filter with a field that doesn't have a
// matching UI type supported yet.
if (view.noFilterOptions) {
view.handleErrors();
// Switch the message from "warning" to "error" so that it's clear this is
// the reason the user cannot save the filter
view.showNoFilterOptionMessage(false, "error");
return;
}
var results = view.createModel();
if (results.success === false) {
view.handleErrors(results.errors);
return;
}
saveButton.off("click");
view.hideModal();
// Only update the collection after the modal has closed because adding a
// new model triggers a re-render of the FilterGroupsView which interferes
// with removing the modal.
var oldModel = view.model;
// Update the filter model in the parent Filters collection
view.model = view.collection.replaceModel(oldModel, results.model);
if (view.editorView) {
view.editorView.showControls();
}
});
// CANCEL
cancelButton.on("click", function (event) {
cancelButton.off("click");
view.currentUIBuilder = null;
view.hideModal();
});
// DELETE
deleteButton.on("click", function (event) {
deleteButton.off("click");
view.hideModal();
if (!view.isNew) {
view.collection.remove(view.model);
}
if (view.editorView) {
view.editorView.showControls();
}
});
} catch (error) {
console.log(
"There was an error activating the modal buttons in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Create and insert the "buttons" to switch filter type and the elements
* that will contain the UI building interfaces for each filter type.
*/
renderUIBuilders: function () {
try {
var view = this;
// The container for the list of filter icons that allows users to switch
// between filter types, plus the associated instruction paragraph
var uiBuilderChoicesContainer = this.modalEl.find(
"." + this.classes.uiBuilderChoicesContainer,
);
// The container for just the icons/buttons
var uiBuilderChoices = $("<div></div>").addClass(
this.classes.uiBuilderChoices,
);
uiBuilderChoicesContainer.append(uiBuilderChoices);
// uiBuilderCarousel will contain all of the UIBuilder views as slides
this.uiBuilderCarousel = this.modalEl.find(
"." + this.classes.uiBuilderContainer,
);
// The bootstrap carousel plugin requires the carousel slide times to be
// contained within an inner div with the class 'carousel-inner'
var carouselInner = $('<div class="carousel-inner"></div>');
this.uiBuilderCarousel.append(carouselInner);
// Create a container and button for each uiBuilder option
this.uiBuilders.forEach(function (uiBuilder) {
// Create a label button that allows the user to select the given UI
// Create the button label
var labelEl = $("<h5>" + uiBuilder.label + "</h5>").addClass(
view.classes.uiBuilderLabel,
);
// Create the button
var button = $("<div></div>")
.addClass(view.classes.uiBuilderChoice)
.attr("data-filter-type", uiBuilder.modelType)
.append(labelEl);
// Insert the uiBuilder icon SVG into the button
var svgPath = "text!" + this.iconDir + uiBuilder.iconFileName;
require([svgPath], function (svgString) {
button.append(svgString);
});
// Add a tooltip with description to the button
button.tooltip({
title: uiBuilder.description,
delay: {
show: 900,
hide: 50,
},
});
// Insert the button into the list of uiBuilder choices
uiBuilderChoices.append(button);
// Create and insert the container / carousel slide. The carousel plugin
// requires slides to have the class 'item'. Save the container to the
// list of uiBuilder options.
var uiBuilderContainer = $('<div class="item"></div>');
carouselInner.append(uiBuilderContainer);
// Add the button and container to the list of uiBuilders to make it
// easy to switch between filter types
uiBuilder.container = uiBuilderContainer;
uiBuilder.button = button;
}, this);
// Initialize the carousel
this.uiBuilderCarousel.addClass("slide");
this.uiBuilderCarousel.addClass("carousel");
this.uiBuilderCarousel.carousel({
interval: false,
});
// Need active class on at least one item for carousel to work properly
this.uiBuilderCarousel.find(".item").first().addClass("active");
} catch (error) {
console.log(
"There was an error rendering the UI filter builders in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Create and insert the component that is used to edit the fields attribute of a
* Filter Model. Save it to the view so that the selected fields can be accessed
* on save.
*/
renderFieldInput: function () {
try {
var view = this;
var selectedFields = _.clone(view.model.get("fields"));
view.fieldInput = new QueryFieldSelect({
selected: selectedFields,
inputLabel: "Select one or more metadata fields",
excludeFields: view.excludeFields,
addFields: view.specialFields,
separatorText: view.model.get("fieldsOperator"),
});
view.modalEl
.find("." + view.classes.fieldsContainer)
.append(view.fieldInput.el);
view.fieldInput.render();
// When the field input is changed, limit UI options to options that match
// this field type
view.fieldInput.off("changeSelection");
view.fieldInput.on("changeSelection", function (selectedFields) {
view.handleFieldChange.call(view, selectedFields);
});
} catch (error) {
console.log(
"There was an error rendering a fields input in a FilterEditorView" +
" Error details: " +
error,
);
}
},
/**
* Run whenever the user selects or removes fields from the Query Field input.
* This function checks which filter UIs support the type of Query Field selected,
* and then blocks or enables the UIs in the editor. This is done to help prevent
* users from building mis-matched search filters, e.g. "Year Slider" filters with
* text query fields.
* @param {string[]} selectedFields The Query Field names (i.e. Solr field names)
* of the newly selected fields
*/
handleFieldChange: function (selectedFields) {
try {
var view = this;
// Enable all UI types if no field is selected yet
if (
!selectedFields ||
!selectedFields.length ||
selectedFields[0] === ""
) {
this.uiBuilders.forEach(function (uiBuilder) {
view.allowUI(uiBuilder);
});
return;
}
// If at least one field is selected, then limit the available UI types to
// those that match the type of Query Field.
var type =
MetacatUI.queryFields.getRequiredFilterType(selectedFields);
this.uiBuilders.forEach(function (uiBuilder) {
if (
uiBuilder.filterTypes.includes(type) &&
view.isBuilderAllowedForFields(uiBuilder, selectedFields)
) {
view.allowUI(uiBuilder);
} else {
view.blockUI(uiBuilder);
}
});
} catch (error) {
console.log(
"There was an error handling a field change in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Marks a UI builder is blocked (so that it can't be selected) and updates the
* tooltip with text explaining that this UI can't be used with the selected
* fields. If the UI to block is the currently selected UI, then switches to the
* next allowed UI. If there are no UIs that are allowed, then shows a message and
* hides all UI builders.
* @param {UIBuilderOption} uiBuilder - The UI builder Object to block
*/
blockUI: function (uiBuilder) {
try {
var view = this;
uiBuilder.allowed = false;
uiBuilder.button.addClass("disabled");
uiBuilder.button.tooltip("destroy");
uiBuilder.button.tooltip({
title: view.text.filterNotAllowed,
delay: {
show: 400,
hide: 50,
},
});
// If the current UI is a blocked one...
if (this.currentUIBuilder === uiBuilder) {
// ... switch to the next unblocked one.
var allowedUIBuilder = _.findWhere(this.uiBuilders, {
allowed: true,
});
if (allowedUIBuilder) {
view.switchFilterType(allowedUIBuilder.modelType);
} else {
// If there is no UI available, then show a message
this.showNoFilterOptionMessage();
}
}
} catch (error) {
console.log(
"There was an error blocking a filter UI builder in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Marks a UI builder is allowed (so that it can be selected) and updates the
* tooltip text with the description of this UI. If it's displayed, this function
* hides the message that indicates that there are no allowed UIs that match the
* selected query fields.
* @param {UIBuilderOption} uiBuilder - The UI builder Object to block
*/
allowUI: function (uiBuilder) {
try {
// If at least one UI is allowed, then make sure the "no filter message" is
// hidden.
if (this.noFilterOptions) {
this.hideNoFilterOptionMessage();
}
uiBuilder.allowed = true;
uiBuilder.button.removeClass("disabled");
uiBuilder.button.tooltip("destroy");
uiBuilder.button.tooltip({
title: uiBuilder.description,
delay: {
show: 900,
hide: 50,
},
});
} catch (error) {
console.log(
"There was an error unblocking a filter UI builder in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Hides all filter builder UIs and displays a warning message indicating that
* there are currently no UI options that support the selected fields.
* @param {string} message A message to show. If not set, then the string set in
* the view's text.noFilterOption attribute is used.
* @param {string} [type="warning"] The type of message to display (warning,
* error, or info)
*/
showNoFilterOptionMessage: function (message, type = "warning") {
try {
this.noFilterOptions = true;
if (!message) {
message = this.text.noFilterOption;
}
if (this.noFilterOptionMessageEl) {
this.noFilterOptionMessageEl.remove();
}
this.noFilterOptionMessageEl = $(
'<div class="alert alert-' + type + '">' + message + "</div>",
);
this.uiBuilderCarousel.hide();
this.uiBuilderCarousel.after(this.noFilterOptionMessageEl);
} catch (error) {
console.log(
"There was an error showing a message to indicate that no filter builder " +
"UI options are allowed in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Removes the message displayed by the
* {@link FilterEditorView#showNoFilterOptionMessage} function and un-hides all
* the filter builder UIs.
*/
hideNoFilterOptionMessage: function () {
try {
this.noFilterOptions = false;
if (this.noFilterOptionMessageEl) {
this.noFilterOptionMessageEl.remove();
console.log(this.noFilterOptionMessageEl);
}
if (this.uiBuilderCarousel.is(":hidden")) {
this.uiBuilderCarousel.show();
}
} catch (error) {
console.log(
"There was an error hiding a message in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Functions to run when a user clicks the "save" button in the editing modal
* window. Creates a new Filter model with all of the new attributes that the user
* has selected. Checks if the model is valid. If it is, then returns the model.
* If it is not, then returns the errors.
* @param {Object} event The click event
* @return {Object} Returns an object with a success property set to either true
* (if there were no errors), or false (if there were errors). If there were
* errors, then the object also has an errors property with the errors return from
* the Filter validate function. If there were no errors, then the object contains
* a model property with the new Filter to be saved to the Filters collection.
*/
createModel: function (event) {
try {
var selectedUI = this.currentUIBuilder,
newModelAttrs = selectedUI.draftModel.toJSON();
// Set the new fields
newModelAttrs.fields = _.clone(this.fieldInput.selected);
// set the new fieldsOperator
newModelAttrs.fieldsOperator = this.fieldInput.separatorText;
delete newModelAttrs.objectDOM;
delete newModelAttrs.cid;
// The collection's model function identifies the type of model to create
// based on the filterType attribute. Create a model before we add it to the
// collection, so that we can make sure it's valid first, while still allowing
// a user to press the UNDO button and not add any changes to the Filters
// collection.
var newModel = this.collection.model(newModelAttrs);
// Check if the filter is valid.
var newModelErrors = newModel.validate();
if (newModelErrors) {
return {
success: false,
errors: newModelErrors,
};
} else {
return {
success: true,
model: newModel,
};
}
} catch (error) {
console.log(
"There was an error updating a Filter model in a FilterEditorView" +
" Error details: " +
error,
);
}
},
/**
* Shows errors in the filter editor modal window.
* @param {object} errors An object where keys represent the Filter model
* attribute that has an error, and the corresponding value explains the error in
* text.
*/
handleErrors: function (errors) {
try {
var view = this;
// Show a general error message in the modal. (Don't add it twice.)
if (view.validationErrorEl) {
view.validationErrorEl.remove();
}
view.validationErrorEl = $(
'<p class="alert alert-error">' +
view.text.validationError +
"</p>",
);
this.$el.find(".modal-body").prepend(view.validationErrorEl);
if (errors) {
// Show an error for the "fields" attribute (common to all Filters)
if (errors.fields) {
view.fieldInput.showMessage(errors.fields, "error", true);
}
// Show errors for the attributes specific to each Filter type
view.currentUIBuilder.view.showValidationErrors(errors);
}
} catch (error) {
console.log(
"There was an error in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Function that takes the event when a user clicks on one of the filter type
* options, gets the name of the desired filter type, and passes it to the switch
* filter function.
* @param {object} event The click event
*/
handleFilterIconClick: function (event) {
try {
// Get the new Filter Type from the click event. The name of the new Filter
// Type is stored as a data attribute in the clicked Filter icon.
// var filterTypeIcon =
var newFilterType = event.currentTarget.dataset.filterType;
// Pass the Filter Type to the switch filter function
this.switchFilterType(newFilterType);
} catch (error) {
console.log(
"There was an error handling a click event in a FilterEditorView" +
" Error details: " +
error,
);
}
},
/**
* Switches the current draft Filter model to a different Filter model type.
* Carries over any common attributes from the previously selected filter type.
* If no filter type is provided, defaults to type of the view's model
* @param {string} newFilterType The name of the filter type to switch to
*/
switchFilterType: function (newFilterType) {
try {
var view = this;
// Use the filter type of the model if none is provided.
if (!newFilterType) {
newFilterType = this.model.type;
}
// Get the properties of the Filter UI Editor for the new filter type.
var uiBuilder = _.findWhere(this.uiBuilders, {
modelType: newFilterType,
});
// Don't allow user to build a mis-matched filter (e.g. text filter with date
// field)
if (uiBuilder.allowed === false) {
return;
}
// (Index is used for the carousel)
var index = this.uiBuilders.indexOf(uiBuilder);
// Treat the first Filter in the list of filter UI editor options as the
// default
if (!uiBuilder) {
uiBuilder = this.uiBuilders[0];
filterType = uiBuilder.modelType;
}
// Create an object with the properties to pass on to the new draft model
var newModelAttrs = {};
// If there is a currently selected UI editor, then find the common model
// attributes that we should pass on to the new UI editor type
if (this.currentUIBuilder) {
newModelAttrs = this.getCommonAttributes(
this.currentUIBuilder.draftModel,
newFilterType,
);
}
// All search filter models are UI Filter Type
newModelAttrs.isUIFilterType = true;
// If a UI editor has already been created for this Filter Type, then just
// update the pre-existing draft model. This way, if a user has already
// selected content that is specific to a filter type (e.g. choices for a
// choiceFilter), that content will still be there when they switch back to
// it. Otherwise, use a clone of the model set on this view. We will update
// the actual model in the Filters collection only when the user clicks save.
if (!uiBuilder.draftModel) {
if (this.model.type == newFilterType) {
uiBuilder.draftModel = this.model.clone();
} else {
uiBuilder.draftModel = uiBuilder.modelFunction({
isUIFilterType: true,
});
}
}
if (Object.keys(newModelAttrs).length) {
uiBuilder.draftModel.set(newModelAttrs);
}
// Save the new selection to the view
this.currentUIBuilder = uiBuilder;
// Find the container for this filter type
var uiBuilderContainer = uiBuilder.container;
// Create or update view
this.currentUIBuilder.view = this.currentUIBuilder.uiFunction(
uiBuilder.draftModel,
);
uiBuilderContainer.html(this.currentUIBuilder.view.el);
this.currentUIBuilder.view.render();
// Add the selected/active class to the clicked FilterTypeIcon, remove it from
// the other icons.
this.uiBuilders.forEach(function (uiBuilder) {
uiBuilder.button.removeClass(view.classes.uiBuilderChoiceActive);
});
this.currentUIBuilder.button.addClass(
view.classes.uiBuilderChoiceActive,
);
// Have the carousel slide to the selected uiBuilder container.
this.uiBuilderCarousel.carousel(index);
} catch (error) {
console.log(
"There was an error switching filter types in a FilterEditorView." +
" Error details: " +
error,
);
}
},
/**
* Checks for attribute keys that are the same between a given Filter model, and a
* new Filter model type. Returns an object of model attributes that are relevant
* to the new Filter model type. The values for this object will be pulled from
* the given model. objectDOM, cid, and nodeName attributes are always excluded.
*
* @param {Filter} filterModel A filter model
* @param {string} newFilterType The name of the new filter model type
*
* @returns {Object} returns the model attributes from the given filterModel that
* are also relevant to the new Filter model type.
*/
getCommonAttributes: function (filterModel, newFilterType) {
try {
// The filter model attributes that are common to both the current Filter Model
// and the new Filter Type that we want to create.
var commonAttributes = {};
// Given the newFilterType string, get the default attribute names for a new
// model of that type.
var uiBuilder = _.findWhere(this.uiBuilders, {
modelType: newFilterType,
});
var defaultAttrs = uiBuilder.modelFunction().defaults();
var defaultAttrNames = Object.keys(defaultAttrs);
// Check if any of those attribute types exist in the current filter model.
// If they do, include them in the common attributes object.
var currentAttrs = filterModel.toJSON();
defaultAttrNames.forEach(function (attrName) {
var valueInDraftModel = currentAttrs[attrName];
if (
valueInDraftModel ||
(valueInDraftModel === 0) | (valueInDraftModel === false)
) {
commonAttributes[attrName] = valueInDraftModel;
}
}, this);
// Exclude attributes that shouldn't be passed to a new model, like the
// objectDOM and the model ID.
delete commonAttributes.objectDOM;
delete commonAttributes.cid;
delete commonAttributes.nodeName;
// Return the common attributes
return commonAttributes;
} catch (error) {
console.log(
"There was an error getting common model attributes in a FilterEditorView" +
". Error details: " +
error,
);
}
},
/**
* Determine whether a particular UIBuilder is allowed for a set of
* search field names. For use in handleFieldChange to enable or disable
* certain UI builders using allowUI/blockUI when the user selects
* different search fields.
*
* @param {UIBuilderOption} uiBuilder The UIBuilderOption object to
* check
* @param {string[]} selectedFields An array of search field names to
* look for restrictions
*
* @return {boolean} Whether or not the uiBuilder is allowed for all
* of selectedFields. Returns true only if all selectedFields are
* allowed, not just one or more.
*/
isBuilderAllowedForFields: function (uiBuilder, selectedFields) {
// Return true early if this uiBuilder has no blockedFields
if (!uiBuilder.blockedFields || uiBuilder.blockedFields.length == 0) {
return true;
}
// Check each blockedField for presence in selectedFields
var isAllowed = uiBuilder.blockedFields.map(function (blockedField) {
return !selectedFields.includes(blockedField);
});
return isAllowed.every(function (e) {
return e;
});
},
},
);
return FilterEditorView;
});