Source: src/js/views/portals/editor/PortEditorSectionView.js

define([
  "underscore",
  "jquery",
  "backbone",
  "text!templates/portals/editor/portEditorSection.html",
  "text!templates/portals/editor/portEditorSectionOption.html",
  "text!templates/portals/editor/portEditorSectionOptionImgs/freeform.svg",
  "text!templates/portals/editor/portEditorSectionOptionImgs/metrics.svg",
  "text!templates/portals/editor/portEditorSectionOptionImgs/cesiumViewer.svg",
], (
  _,
  $,
  Backbone,
  Template,
  SectionOptionTemplate,
  FreeformSVG,
  MetricsSVG,
  CesiumSVG,
) => {
  /**
   * @class PortEditorSectionView
   * @classdesc A view of a single section of the PortalEditorView. This default
   * section view displays a choice of which PortalSection to add to the Portal.
   * @classcategory Views/Portals/Editor
   * @augments Backbone.View
   */
  const PortEditorSectionView = Backbone.View.extend(
    /** @lends PortEditorSectionView.prototype */ {
      /**
       * The type of View this is
       * @type {string}
       */
      type: "PortEditorSection",

      /**
       * The unique label for this Section. It most likely matches the label on
       * the model, but may include a number after if more than one section has
       * the same name.
       * @type {string}
       */
      uniqueSectionLabel: "",

      /**
       * The HTML tag name for this view's element
       * @type {string}
       */
      tagName: "div",

      /**
       * The HTML classes to use for this view's element
       * @type {string}
       */
      className: "port-editor-section tab-pane",

      /**
       * The PortalSectionModel being displayed
       * @type {PortalSectionModel}
       */
      model: undefined,

      /**
       * A reference to the PortalEditorView
       * @type {PortalEditorView}
       */
      editorView: null,

      /**
       * References to templates for this view. HTML files are converted to
       * Underscore.js templates
       */
      template: _.template(Template),
      sectionOptionTemplate: _.template(SectionOptionTemplate),

      /**
       * A jQuery selector for the element that the section option buttons
       * should be inserted into
       * @type {string}
       */
      sectionsOptionsContainer: "#section-options-container",

      /**
       * @typedef {object} PortEditorSectionView#sectionOption - Information
       * about a section type that can be added to a portal
       * @property {string} title - The name of the section type to be displayed
       * to the user
       * @property {string} description - A brief description of the section
       * type, to be displayed to the user
       * @property {string|number} limiter - The limiter is used to determine
       * whether the user is allowed to add more of this section type. If
       * limiter is a number, then it's used as the maximum number of the given
       * sections allowed (currently this only applies to 'freeform'/markdown
       * sections). If limiter is a string, it should be the name of the 'hide'
       * option in the project model (e.g. hideMetrics for the metrics view). In
       * this case, it's assumed that only one of the given page type is
       * allowed.
       * @property {string} svg - SVG that illustrates the section type. SVG
       * elements should use classes to define fill colors that are not
       * greyscale, so that they may be greyed-out to indicate that a section
       * type is unavailable. SVG elements that use theme colors should use the
       * classes 'theme-primary-fill', 'theme-secondary-fill', and
       * 'theme-accent-fill'.
       */

      /**
       * Information about each of the section types available to a user. Note
       * that the key (e.g. "freeform") is used to ID the UI selection element.
       * @type {Object<string, PortEditorSectionView#sectionOption>}
       */
      sectionsOptions: {
        freeform: {
          title: "Freeform",
          description: "Add content and images styled using markdown",
          limiter: 30,
          svg: FreeformSVG,
        },
        metrics: {
          title: "Metrics",
          description: "Show visual summaries of your data collection",
          limiter: "hideMetrics",
          svg: MetricsSVG,
        },
        cesium: {
          title: "3D Map",
          description: "Show your data on a 3D map",
          limiter: 1,
          svg: CesiumSVG,
        },
      },

      /**
       * The events this view will listen to and the associated function to
       * call.
       * @type {object}
       */
      events: {
        "click .section-option": "addNewSection",
      },

      /**
       * Creates a new PortEditorSectionView
       * @param {object} options - A literal object with options to pass to the
       * view
       * @param {PortalSectionModel} options.model - The section rendered in this
       * view
       */
      initialize(options) {
        // Get all the options and apply them to this view
        if (typeof options === "object") {
          const optionKeys = Object.keys(options);
          _.each(
            optionKeys,
            function assignOption(key) {
              this[key] = options[key];
            },
            this,
          );
        }
      },

      /**
       * Renders this view.
       * @returns {PortEditorSectionView} This view
       */
      render() {
        try {
          // Insert the template into the view
          this.$el.html(this.template());

          // Add a section option element for each section type the user can
          // select from.
          _.each(
            this.sectionsOptions,
            function renderSectionOption(sectionData, sectionType) {
              if (
                sectionType === "cesium" &&
                !MetacatUI.appModel.get("enableCesium")
              ) {
                return;
              }

              this.$(this.sectionsOptionsContainer).append(
                this.sectionOptionTemplate({
                  id: `section-option-${sectionType}`,
                  title: sectionData.title,
                  description: sectionData.description,
                  img: sectionData.svg,
                  sectionType,
                }),
              );

              // Check whether the section option is available to user
              this.toggleDisableSectionOption(sectionType);

              // For metrics, data, and members sections, add a listener to
              // update the section availability when the associated model
              // option is changed.
              if (
                typeof sectionData.limiter === "string" ||
                sectionData.limiter instanceof String
              ) {
                this.stopListening(this.model, `change:${sectionData.limiter}`);
                this.listenTo(
                  this.model,
                  `change:${sectionData.limiter}`,
                  function updateSectionOption() {
                    try {
                      this.toggleDisableSectionOption(sectionType);
                    } catch (e) {
                      // eslint-disable-next-line no-console
                      console.log(
                        `Cannot toggle disabling of section types, error message: ${e}`,
                      );
                    }
                  },
                );
              }
            },
            this,
          );

          // Listen for changes to the sections attribute and update section
          // availability accordingly.
          this.stopListening(this.model, "change:sections");
          this.listenTo(this.model, "change:sections", () => {
            _.each(this.sectionsOptions, ({ limiter }, sectionType) => {
              if (typeof limiter === "number" || limiter instanceof Number) {
                this.toggleDisableSectionOption(sectionType);
              }
            });
          });

          // Save a reference to this view
          this.$el.data("view", this);
        } catch (e) {
          // eslint-disable-next-line no-console
          console.log(`Section view cannot be rendered, error message: ${e}`);
        }

        return this;
      },

      /**
       * Checks whether a section type is available to a user to add, then calls
       * functions that change content and styling to indicate the availability
       * to the user.
       * @param {string} sectionType - The section name. This is the same string
       * used as the key in sectionsOptions (e.g. "freeform").
       */
      toggleDisableSectionOption(sectionType) {
        try {
          const { limiter } = this.sectionsOptions[sectionType];

          // If limiter's a string, look up whether this section type is hidden
          if (typeof limiter === "string" || limiter instanceof String) {
            // If it's currently hidden
            if (this.model.get(limiter)) {
              // then allow user to 'unhide' it.
              this.enableSectionOption(sectionType);
              // If it's already displayed
            } else {
              // then user can't add more of this type of section.
              this.disableSectionOption(sectionType);
            }
            // If limiter's a number, compare it to the count of sections in the
            // model
          } else if (typeof limiter === "number" || limiter instanceof Number) {
            const sections = this.model.get("sections");
            const sectionCount =
              sectionType === "cesium"
                ? sections.filter(
                    (section) =>
                      section.get("visualizationType") === sectionType,
                  ).length
                : sections.length;

            if (sectionCount < limiter) {
              this.enableSectionOption(sectionType);
            } else {
              this.disableSectionOption(sectionType);
            }
            // If limiter is neither a string nor a number
          } else {
            // eslint-disable-next-line no-console
            console.log(
              "Error: In toggleDisableSectionOption(sectionType), the sectionType must be a string or a number.",
            );
          }
        } catch (e) {
          // eslint-disable-next-line no-console
          console.error(e);
        }
      },

      /**
       * Adds styling and content to a section option element to indicate that
       * the user already added the maximum allowable number of this section
       * type (i.e. it's disabled).
       * @param {string} sectionType - The section name. This is the same string
       * used as the key in sectionsOptions (e.g. "freeform").
       */
      disableSectionOption(sectionType) {
        try {
          if (
            !sectionType ||
            !(typeof sectionType === "string" || sectionType instanceof String)
          ) {
            // eslint-disable-next-line no-console
            console.error(
              "Error: In disableSectionOption(sectionType), a string that indicates the sectionType is required",
            );
            return;
          }

          const sectionOption = this.$(`#section-option-${sectionType}`);
          const description = sectionOption.find(".description");
          const { limiter } = this.sectionsOptions[sectionType];
          const title = this.sectionsOptions[sectionType].title.toLowerCase();
          const limit =
            typeof limiter === "number" || limiter instanceof Number
              ? limiter
              : 1;
          const singOrPlur = limit > 1 ? "s" : "";
          const descriptionMsg = `You've already added ${limit} ${title} page${singOrPlur}`;

          // Add the disabled class
          sectionOption.addClass("disabled");

          // Add the new description
          description.html(descriptionMsg);

          // Create a tooltip
          sectionOption.tooltip({
            placement: "top",
            delay: {
              show: 800,
              hide: 0,
            },
            title: descriptionMsg,
            trigger: "hover",
            container: sectionOption,
          });

          // Make sure disabled option isn't clickable
          sectionOption.off("click");
        } catch (e) {
          // eslint-disable-next-line no-console
          console.error(e);
        }
      },

      /**
       * Adds styling and content to a section option element to indicate that
       * the user is able to add more of this section type (i.e. it's not
       * disabled).
       * @param {string} sectionType - The section name. This is the same string
       * used as the key in sectionsOptions (e.g. "freeform").
       */
      enableSectionOption(sectionType) {
        try {
          if (
            !sectionType ||
            !(typeof sectionType === "string" || sectionType instanceof String)
          ) {
            // eslint-disable-next-line no-console
            console.log(
              "Error: In enableSectionOption(sectionType), a string that indicates the sectionType is required",
            );
            return;
          }

          const sectionOption = this.$(`#section-option-${sectionType}`);
          const descriptionEl = sectionOption.find(".description");
          const descriptionText = this.sectionsOptions[sectionType].description;

          // Remove the disabled class
          sectionOption.removeClass("disabled");

          // Update the description
          descriptionEl.html(descriptionText);

          // Remove the tooltip
          sectionOption.tooltip("destroy");
        } catch (e) {
          // eslint-disable-next-line no-console
          console.error(e);
        }
      },

      /**
       * Gets the section type to add, and triggers an event so the rest of the
       * app will add a new section
       * @param {Event} e - The element that was clicked that represents the
       * section option
       */
      addNewSection(e) {
        if (
          !e ||
          $(e.target).is(".disabled") ||
          $(e.target).parents(".section-option").first().is(".disabled")
        ) {
          return;
        }

        // Get the section type
        let sectionType = $(e.target).data("section-type");

        if (!sectionType) {
          sectionType = $(e.target)
            .parents("[data-section-type]")
            .first()
            .data("section-type");
          if (!sectionType) {
            return;
          }
        }

        this.trigger("addNewSection", sectionType);

        this.toggleDisableSectionOption(sectionType);
      },
    },
  );

  return PortEditorSectionView;
});