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

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;
});