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

define([
  "jquery",
  "underscore",
  "backbone",
  "models/filters/ChoiceFilter",
  "views/filters/FilterView",
  "text!templates/filters/choiceFilter.html",
], function ($, _, Backbone, ChoiceFilter, FilterView, Template) {
  "use strict";

  /**
   * @class ChoiceFilterView
   * @classdesc Render a view of a single ChoiceFilter model
   * @classcategory Views/Filters
   * @extends FilterView
   */
  var ChoiceFilterView = FilterView.extend(
    /** @lends ChoiceFilterView.prototype */ {
      /**
       * A ChoiceFilter model to be rendered in this view
       * @type {ChoiceFilter} */
      model: null,

      /**
       * @inheritdoc
       */
      modelClass: ChoiceFilter,

      className: "filter choice",

      template: _.template(Template),

      /**
       * When this view is in "uiBuilder" mode, the class name for the handles on each choice
       * row that the user can click and drag to re-order
       * @type {string}
       */
      choiceHandleClass: "handle",

      /**
       * The class to add to the element that a user should click to remove a choice
       * value and label when this view is in "uiEditor" mode
       * @since 2.17.0
       * @type {string}
       */
      removeChoiceClass: "remove-choice",

      /**
       * A function that creates and returns the Backbone events object.
       * @return {Object} Returns a Backbone events object
       */
      events: function () {
        try {
          var events = FilterView.prototype.events.call(this);
          events["change select"] = "handleChange";
          var removeClass = "." + this.removeChoiceClass;
          events["click " + removeClass] = "removeChoice";
          events["mouseover " + removeClass] = "previewRemoveChoice";
          events["mouseout " + removeClass] = "previewRemoveChoice";
          return events;
        } catch (error) {
          console.log(
            "There was an error creating the events object for a ChoiceFilterView" +
              " Error details: " +
              error,
          );
        }
      },

      render: function () {
        var view = this;

        // Renders the template and inserts the FilterEditorView if the mode is uiBuilder
        FilterView.prototype.render.call(this);

        var placeHolderText = this.model.get("placeholder");
        var select = this.$("select");

        if (this.mode === "uiBuilder") {
          // If this is the filter view where the user can edit the filter UI options,
          // like the label, placeholder text, and choices, then render the inputs
          // for these options.

          // The ignore-changes class prevents the editor footer from showing on keypress
          var placeholderInput = $(
            '<input placeholder="placeholder" class="' +
              this.uiInputClass +
              ' placeholder ignore-changes" data-category="placeholder" value="' +
              (placeHolderText ? placeHolderText : "") +
              '" />',
          );
          // Replace the select element with the placeholder text element
          placeholderInput.insertAfter(select);

          // Create the interface for a user to edit the value-label choice options
          var choicesEditor = this.createChoicesEditor();
          view.$el.append(choicesEditor);
        } else {
          // For regular search filter views, or the edit filter view, render the dropdown
          // interface

          //Create the placeholder text for the dropdown menu

          //If placeholder text is already provided in the model, use it
          //If not, create placeholder text using the model label
          if (!placeHolderText) {
            if (this.model.get("label")) {
              //If the label starts with a vowel, use "an"
              var vowels = ["a", "e", "i", "o", "u"];
              if (
                _.contains(
                  vowels,
                  this.model.get("label").toLowerCase().charAt(0),
                )
              ) {
                placeHolderText = "Choose an " + this.model.get("label");
              }
              //Otherwise use "a"
              else {
                placeHolderText = "Choose a " + this.model.get("label");
              }
            }
          }

          //Create the default option
          var defaultOption = $(document.createElement("option"))
            .attr("value", "")
            .text(placeHolderText);
          select.append(defaultOption);

          //Create an option element for each choice listen in the model
          _.each(
            this.model.get("choices"),
            function (choice) {
              select.append(
                $(document.createElement("option"))
                  .attr("value", choice.value)
                  .text(choice.label),
              );
            },
            this,
          );

          //When the ChoiceFilter is changed, update the choice list in the UI
          this.listenTo(this.model, "change:values", this.updateChoices);
          this.listenTo(this.model, "remove", this.updateChoices);
        }
      },

      /**
       * Create the set of inputs where a use can select label-value pairs for the regular
       * choice filter view
       * @since 2.17.0
       */
      createChoicesEditor: function () {
        try {
          var view = this;
          this.choicesEditor = $("<div class='choices-editor'></div>");
          var choicesEditorText = $(
            "<p class='modal-instructions'>Allow people to select from the following search terms</p>",
          );
          var choiceEditorError = $(
            "<p class='notification error' data-category='choices' style='display: none'></p>",
          );
          var labelContainer = $(
            "<div class='choice-editor unsortable'></div>",
          );

          this.choicesEditor.append(
            choicesEditorText,
            choiceEditorError,
            labelContainer,
          );

          labelContainer.append(
            "<p class='ui-builder-container-text choice-label subtle'>Enter the text to display</p>",
          );
          labelContainer.append(
            "<p class='ui-builder-container-text choice-value subtle'>Enter the text to search for</p>",
          );

          _.each(
            this.model.get("choices"),
            function (choice) {
              var choiceEditorEl = this.createChoiceEditor(choice);
              this.choicesEditor.append(choiceEditorEl);
            },
            this,
          );

          // Create a blank choice at the end
          this.addEmptyChoiceEditor();

          // Initialize choice drag and drop to re-order functionality
          require(["sortable"], function (Sortable) {
            Sortable.create(view.choicesEditor[0], {
              direction: "vertical",
              easing: "cubic-bezier(1, 0, 0, 1)",
              animation: 200,
              handle: "." + view.choiceHandleClass,
              draggable: ".choice-editor:not(.unsortable)",
              onUpdate: function (evt) {
                // When the choice order is changed, update the filter model
                view.updateModelChoices();
              },
            });
          });

          return this.choicesEditor;
        } catch (error) {
          console.log(
            "There was an error creating choices editor in a ChoiceFilterView" +
              " Error details: " +
              error,
          );
        }
      },

      /**
       * Create a row where a user can input a value and label for a single choice.
       * @since 2.17.0
       */
      createChoiceEditor: function (choice) {
        try {
          if (!choice) {
            return;
          }

          var view = this;

          // Create the choice container
          var choiceContainer = $("<div class='choice-editor'></div>");

          // Create the click and drag handle
          var handle = $(
            '<span class="' +
              view.choiceHandleClass +
              '">' +
              '<i class= "icon icon-ellipsis-vertical" ></i>' +
              '<i class="icon icon-ellipsis-vertical"></i>' +
              "</span >",
          );
          choiceContainer.append(handle);

          // Create inputs for "value" and "label", insert them in the container
          for (const [attrName, attrValue] of Object.entries(choice)) {
            var inputEl = $("<input>").attr({
              // The ignore-changes class prevents the editor footer from showing on keypress
              class: "ignore-changes choice-input choice-" + attrName,
              value: attrValue,
              "data-category": attrName,
            });
            // Update the values in the model when the user focuses out of an input
            inputEl.on("blur", function () {
              view.updateModelChoices.call(view);
            });
            choiceContainer.append(inputEl);
          }

          // Create the remove "X" button. Save references to the parent choice container so
          // that we can remove it from the view when the button is clicked
          var removeButton = $(
            "<i class='icon icon-remove " +
              this.removeChoiceClass +
              "' title='Remove this choice'></i>",
          ).data({
            choiceEl: choiceContainer,
          });

          // Insert the remove button into the choice container
          choiceContainer.append(removeButton);
          return choiceContainer;
        } catch (error) {
          console.log(
            "There was an error  ChoiceFilterView" + " Error details: " + error,
          );
        }
      },

      /**
       * Create an empty choice editor row
       * @since 2.17.0
       */
      addEmptyChoiceEditor: function () {
        try {
          var view = this;
          var choice = {
            label: "",
            value: "",
          };
          var choiceEditorEl = this.createChoiceEditor(choice);
          this.choicesEditor.append(choiceEditorEl);

          // Don't let users remove or sort the new choice entry fields until some text has
          // been entered
          var removeButton = choiceEditorEl.find("." + this.removeChoiceClass);
          var handle = choiceEditorEl.find("." + this.choiceHandleClass);
          removeButton.hide();
          handle.hide();
          choiceEditorEl.addClass("unsortable");
          // The inputs for value and label
          var inputs = choiceEditorEl.find("input");

          var onInputChange = function () {
            choiceEditorEl.removeClass("unsortable");
            removeButton.show();
            handle.show();
            view.addEmptyChoiceEditor();
            inputs.off("input", onInputChange);
          };
          inputs.on("input", onInputChange);
        } catch (error) {
          console.log(
            "There was an error creating a choice editor in a ChoiceFilterView" +
              " Error details: " +
              error,
          );
        }
      },

      /**
       * Indicate to the user that the choice value and label inputs will be removed when
       * they hover over the remove button.
       * @since 2.17.0
       */
      previewRemoveChoice: function (e) {
        try {
          var normalOpacity = 1.0,
            previewOpacity = 0.2,
            speed = 120;

          var removeEl = $(e.target);
          var subElements = removeEl.data("choiceEl").children().not(removeEl);

          if (e.type === "mouseover") {
            subElements.fadeTo(speed, previewOpacity);
            $(removeEl).fadeTo(speed, normalOpacity);
          }
          if (e.type === "mouseout") {
            subElements.fadeTo(speed, normalOpacity);
            $(removeEl).fadeTo(speed, previewOpacity);
          }
        } catch (error) {
          console.log(
            "Error showing a preview of the removal of a Choice editor in a " +
              "Choice Filter View, details: " +
              error,
          );
        }
      },

      /**
       * Remove a choice editor row and the corresponding label-value pair from the choice
       * Filter Model (TODO)
       * @since 2.17.0
       * @param {Object} e The click event object
       */
      removeChoice: function (e) {
        try {
          var choiceEl = $(e.target).data("choiceEl");

          // See how many choice elements there are (subtract one because the label elements
          // are within a choice-editor element)
          var numChoices = this.$el.find(".choice-editor").length - 1;

          // Don't allow removing the last choice element. Empty the last element and hide the
          // remove button instead.
          if (numChoices <= 1) {
            choiceEl.find("input").val("");
          } else {
            // Remove the choice editor element from the view, plus any listeners
            choiceEl.off();
            choiceEl.remove();
          }
          // Update the choices in the model
          this.updateModelChoices();
        } catch (error) {
          console.log(
            "There was an error removing a choice editor in the ChoiceFilterView" +
              " Error details: " +
              error,
          );
        }
      },

      /**
       * Update the choices attribute in the choiceFilter model based on the values in the
       * choices editor
       * @since 2.17.0
       */
      updateModelChoices: function () {
        try {
          // The array of label-value pairs that will be set on the choiceFilter model.
          var newChoices = [];

          // Find each choice editor container, and find the values from the two inputs
          // within.
          this.$el.find(".choice-editor").each(function () {
            var choiceEditor = $(this);
            var valueEl = choiceEditor.find("[data-category='value']");
            var labelEl = choiceEditor.find("[data-category='label']");
            if (valueEl.length && labelEl.length) {
              var newValue = valueEl[0].value;
              var newLabel = labelEl[0].value;
              // TODO: validate the label/value here and show error if choice is not
              // complete.
              if (!newValue && !newLabel) {
                // Don't add empty choices to the model
                return;
              } else {
                newChoices.push({
                  label: newLabel,
                  value: newValue,
                });
              }
            }
          });
          // Replace the choices in the model with the new array with new values
          this.model.set("choices", newChoices);
        } catch (error) {
          console.log(
            "There was an error updating the choices in a ChoiceFilterView" +
              " Error details: " +
              error,
          );
        }
      },

      /**
       * Updates the value set on the ChoiceFilter Model associated with this view.
       * The filter value is grabbed from the select element in this view.
       *
       */
      updateModel: function () {
        //Get the new value from the text input
        var newValue = this.$("select").val();

        //Get the current values array from the model
        var currentValue = this.model.get("values");

        //If the ChoiceFilter allows multiple values to be added,
        // add the new choice to the values array
        if (this.model.get("chooseMultiple")) {
          //Duplicate the current values array
          var newValuesArray = currentValue.slice(0);

          //Add the new value to the array
          newValuesArray.push(newValue);

          //Set the new values array on the model
          this.model.set("values", newValuesArray);
        }
        //If multiple choices are not allowed,
        else {
          //Replace the first index of the array with the new value
          var newValuesArray = currentValue.slice(0);
          newValuesArray[0] = newValue;

          //Set the new values array on the model
          this.model.set("values", newValuesArray);
        }
      },

      /**
       * Update the choices in the select dropdown menu based on which choices are
       * currently selected
       */
      updateChoices: function () {
        //Enable all the choices
        this.$("option").prop("disabled", false);

        //Get the currently-selected choices
        var selectedChoices = this.model.get("values");

        _.each(
          selectedChoices,
          function (choice) {
            //Find each choice in the dropdown menu and disable it
            this.$("option[value='" + choice + "']").prop("disabled", true);
          },
          this,
        );
      },

      /**
       * Show validation errors. This is used for filters that are in "UIBuilder" mode.
       * @param {Object} errors The error messages associated with each attribute that has
       * an error, passed from the Filter model validation function.
       */
      showValidationErrors: function (errors) {
        try {
          var view = this;
          // Select the messages container for the choice error (added in the template)
          var messageContainer = view.el.querySelector(
            ".notification[data-category='choices']",
          );

          // Show errors for label, placeholder, etc (elements common to all FilterViews)
          FilterView.prototype.showValidationErrors.call(this, errors);

          // Show errors in the choices editor
          var inputs = this.choicesEditor.find("input");

          // Add error styling to all the choices inputs. Remove error styling (and input
          // listeners) from all inputs when there is text in at least one of them
          var handleInput = function () {
            inputs.each(function (i, input) {
              view.hideInputError(input, messageContainer);
              input.removeEventListener("input", handleInput);
            });
          };
          if (inputs.length) {
            inputs.each(function (i, input) {
              view.showInputError(input);
              input.addEventListener("input", handleInput);
            });
          }
        } catch (error) {
          console.log(
            "There was an error showing validation errors in a FilterView" +
              ". Error details: " +
              error,
          );
        }
      },
    },
  );
  return ChoiceFilterView;
});