# Source: src/js/views/filters/FilterEditorView.js

    /*global define */
    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
      });