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

define([
  "underscore",
  "jquery",
  "backbone",
  "sortable",
  "models/portals/PortalModel",
  "models/portals/PortalSectionModel",
  "views/portals/editor/PortEditorSectionView",
  "views/portals/editor/PortEditorSettingsView",
  "views/portals/editor/PortEditorDataView",
  "views/portals/editor/PortEditorMdSectionView",
  "text!templates/portals/editor/portEditorSections.html",
  "text!templates/portals/editor/portEditorMetrics.html",
  "text!templates/portals/editor/portEditorSectionLink.html",
  "text!templates/portals/editor/portEditorSectionOptionImgs/metrics.svg",
], function (
  _,
  $,
  Backbone,
  Sortable,
  Portal,
  PortalSection,
  PortEditorSectionView,
  PortEditorSettingsView,
  PortEditorDataView,
  PortEditorMdSectionView,
  Template,
  MetricsSectionTemplate,
  SectionLinkTemplate,
  MetricsSVG,
) {
  /**
   * @class PortEditorSectionsView
   * @classdesc A view of one or more Portal Editor sections
   * @classcategory Views/Portals/Editor
   * @extends Backbone.View
   * @constructor
   */
  var PortEditorSectionsView = Backbone.View.extend(
    /** @lends PortEditorSectionsView.prototype */ {
      /**
       * The type of View this is
       * @type {string}
       */
      type: "PortEditorSections",

      /**
       * 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-sections",

      /**
       * The PortalModel that is being edited
       * @type {Portal}
       */
      model: undefined,

      /**
       * A reference to the currently active editor section. e.g. Data, Metrics, Settings, etc.
       * @type {PortEditorSectionView}
       */
      activeSection: undefined,

      /**
       * The name of the active section when the view is first loaded. This is retrieved from the router/URL
       * @type {string}
       */
      activeSectionLabel: undefined,

      /**
       * The unique labels for each section in this Portal
       * @type {string[]}
       */
      sectionLabels: [],

      /**
       * The subviews contained within this view to be removed with onClose
       * @type {Array}
       */
      subviews: new Array(),

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

      /**
       * References to templates for this view. HTML files are converted to Underscore.js templates
       */
      /**
    @type {Underscore.Template}
    */
      template: _.template(Template),
      /**
    @type {Underscore.Template}
    */
      sectionLinkTemplate: _.template(SectionLinkTemplate),
      /**
    @type {Underscore.Template}
    */
      metricsSectionTemplate: _.template(MetricsSectionTemplate),

      /**
       * A jQuery selector for the elements that are links to the individual sections
       * @type {string}
       */
      sectionLinks: ".portal-section-link",
      /**
       * A jQuery selector for the element that the section links should be inserted into
       * @type {string}
       */
      sectionLinksContainer: ".section-links-container",
      /**
       * A jQuery selector for the element that a single section link will be inserted into
       * @type {string}
       */
      sectionLinkContainer: ".section-link-container",
      /**
       * A jQuery selector for the element that the editor sections should be inserted into
       * @type {string}
       */
      sectionsContainer: ".sections-container",

      /**
       * A jQuery selector for the section elements
       * @type {string}
       */
      sectionEls: ".port-editor-section",

      /**
       * A selector for link or tab elements that the user is allowed to re-order,
       * starting from the sectionLinksContainer
       * @type {string}
       */
      sortableLinksSelector: ">li:not(.unsortable)",

      /**
       * A class name for the handles on tabs that the user can drag to re-order
       * @type {string}
       */
      handleClass: "handle",

      /**
       * A label for the section used to add a new page
       * @type {string}
       */
      addPageLabel: "AddPage",

      /**
       * Flag to add section name to URL. Enabled by default.
       * @type {boolean}
       */
      displaySectionInUrl: true,

      /**
       * @borrows PortalEditorView.newPortalTempName as newPortalTempName
       */
      newPortalTempName: "",

      /**
       * The events this view will listen to and the associated function to call.
       * @type {Object}
       */
      events: {
        "click .rename-section": "renameSection",
        "dblclick .portal-section-link": "renameSection",
        "click .show-section": "showSection",
        "click .portal-section-link": "handleSwitchSection",
        "focusout .portal-section-link[contenteditable=true]": "updateName",
        "click .cancelled-section-removal": "closePopovers",
        "click .confirmed-section-removal": "removeSection",
        // both keyup and keydown events are needed for limitLabelLength function
        "keyup .portal-section-link[contenteditable=true]": "limitLabelInput",
        "keydown .portal-section-link[contenteditable=true]": "limitLabelInput",
        "click #link-to-data": "navigateToData",
      },

      /**
       * Creates a new PortEditorSectionsView
       * @constructs PortEditorSectionsView
       * @param {Object} options - A literal object with options to pass to the view
       */
      initialize: function (options) {
        //Reset arrays and objects set on this View, otherwise they will be shared across intances, causing errors
        this.subviews = new Array();
        this.editorView = null;

        // Get all the options and apply them to this view
        if (options) {
          var optionKeys = Object.keys(options);
          _.each(
            optionKeys,
            function (key, i) {
              this[key] = options[key];
            },
            this,
          );
        }
      },

      /**
       * Renders the PortEditorSectionsView
       */
      render: function () {
        //Insert the template into the view
        this.$el.html(this.template());

        //Render the Data section
        this.renderDataSection();

        //Render the Metrics section
        this.renderMetricsSection();

        //Render the Add Section tab
        this.renderAddSection();

        //Render the Settings
        this.renderSettings();

        //Render a Section View for each content section in the Portal
        this.renderContentSections();

        // Disable the delete/hide section option if there is only one section
        this.toggleRemoveSectionOption();

        var view = this,
          linksContainer = view.el.querySelector(view.sectionLinksContainer),
          sortableLinksSelector = view.sortableLinksSelector,
          sortableLinks = view.el.querySelectorAll(
            view.sectionLinksContainer + view.sortableLinksSelector,
          ),
          sortableLinksArray = Array.prototype.slice.call(sortableLinks, 0),
          pageOrder = this.model.get("pageOrder");

        // Arrange tabs in the order the user has pre-selected
        try {
          if (pageOrder && pageOrder.length) {
            // sort the links according the pageOrder
            sortableLinksArray.sort(function (a, b) {
              var aName = $(a).data("section-name");
              var bName = $(b).data("section-name");
              var aIndex = pageOrder.indexOf(aName);
              var bIndex = pageOrder.indexOf(bName);
              // If the label can't be found in the list of labels, place it at the end
              if (bIndex === -1) {
                return +1;
              }
              if (aIndex === -1) {
                return -1;
              }
              // Sort backwards because we use prepend
              return bIndex - aIndex;
            });
            // Rearrange the links in the DOM
            for (i = 0; i < sortableLinksArray.length; ++i) {
              // Use preprend so that Settings and AddPage tabs remain last in list
              linksContainer.prepend(sortableLinksArray[i]);
            }
          }
        } catch (error) {
          console.log(
            "Error re-arranging tabs according to the pageOrder option. Error message: " +
              error,
          );
        }

        // Initialize user-controlled tab re-ordering
        var sortable = Sortable.create(linksContainer, {
          direction: "horizontal",
          easing: "cubic-bezier(1, 0, 0, 1)",
          animation: 200,
          // Only tabs that have an element with this class will be draggable
          handle: "." + view.handleClass,
          draggable: sortableLinksSelector,
          // When the tab order is changed, update the portal model option with new order
          onUpdate: function (evt) {
            view.updatePageOrder();
          },
        });

        // Switch to the active section, if there is one
        if (this.activeSectionLabel) {
          this.activeSection = this.getSectionByLabel(this.activeSectionLabel);

          //Switch to the active section
          this.switchSection(this.activeSection);

          //Reset the active section label, since it is only used during the initial rendering
          this.activeSectionLabel = undefined;
        } else {
          //Switch to the default section
          this.switchSection();
        }
      },

      /**
       * Render a section for adding a new section
       */
      renderAddSection: function () {
        //Create a unique label for this section and save it
        this.updateSectionLabelsList(this.addPageLabel);

        // Add a "Add section" button/tab
        var addSectionView = new PortEditorSectionView({
          model: this.model,
          uniqueSectionLabel: this.addPageLabel,
          sectionType: "addpage",
          editorView: this.editorView,
        });

        addSectionView.$el
          .addClass("tab-pane")
          .addClass("port-editor-add-section-container")
          .attr("id", this.addPageLabel);

        //Add the section element to this view
        this.$(this.sectionsContainer).append(addSectionView.$el);

        //Render the section view
        addSectionView.render();

        // Add the tab to the tab navigation
        this.addSectionLink(addSectionView);

        // Replace the name "AddSection" with fontawsome "+" icon
        this.$el
          .find(
            this.sectionLinkContainer +
              "[data-section-name='" +
              this.addPageLabel +
              "'] a",
          )
          .html("<i class='icon icon-plus'></i>")
          .attr("title", "Add a new page");

        // When a sectionOption is clicked in the addSectionView subview,
        // the "addNewSection" event is triggered.
        this.listenTo(addSectionView, "addNewSection", this.addSection);

        //Add the view to the subviews array
        this.subviews.push(addSectionView);
      },

      /**
       * Render all sections in the editor for each content section in the Portal
       */
      renderContentSections: function () {
        // Get the sections from the Portal
        var sections = this.model.get("sections");

        // Render each markdown (aka "freeform") section already in the PortalModel
        _.each(
          sections,
          function (section) {
            try {
              if (section) {
                //Render the content section
                this.renderContentSection(section);
              }
            } catch (e) {
              console.error(e);
            }
          },
          this,
        );
      },

      /**
       * Render a single markdown section in the editor (sectionView + link)
       * @param {PortalSectionModel} section - The section to render
       * @param {boolean} isNew - If true, this section will be rendered as a section that was just added by the user
       */
      renderContentSection: function (section, isNew) {
        try {
          if (typeof isNew == "undefined" || isNew == null) {
            var isNew = false;
          }

          if (section) {
            // Create and render and markdown section view
            var sectionView = new PortEditorMdSectionView({
              model: section,
            });

            // Pass the portal editor view onto the section
            sectionView.editorView = this.editorView;

            //Create a unique label for this section and save it
            var uniqueLabel = this.getUniqueSectionLabel(section);

            //Set the unique section label for this view
            sectionView.uniqueSectionLabel = uniqueLabel;

            this.updateSectionLabelsList(uniqueLabel);

            //Attach the editor view to this view
            sectionView.editorView = this.editorView;

            sectionView.$el.attr("id", uniqueLabel);

            //Insert the PortEditorMdSectionView element into this view
            this.$(this.sectionsContainer).append(sectionView.$el);

            //Render the PortEditorMdSectionView
            sectionView.render();

            // Add the tab to the tab navigation
            this.addSectionLink(sectionView, ["Rename", "Delete"], isNew);

            // Add the sections to the list of subviews
            this.subviews.push(sectionView);
          }
        } catch (e) {
          console.error(e);
        }
      },

      /**
       * Renders a Data section in this view
       */
      renderDataSection: function () {
        try {
          this.updateSectionLabelsList("Data");

          // Render a PortEditorDataView and corresponding tab
          var dataView = new PortEditorDataView({
            model: this.model,
            uniqueSectionLabel: "Data",
            editorView: this.editorView,
          });

          //Save a reference to this view
          this.dataView = dataView;

          //Add the view to the page
          this.$(this.sectionsContainer).append(dataView.el);

          //Render the PortEditorDataView
          dataView.render();

          //Create the menu options for the Data section link
          var menuOptions = [];
          if (this.model.get("hideData") === true) {
            menuOptions.push("Show");
          } else {
            menuOptions.push("Hide");
          }

          // Add the tab to the tab navigation
          this.addSectionLink(dataView, menuOptions);

          //When the Data section has been hidden or shown, update the section link
          this.stopListening(this.model, "change:hideData");
          this.listenTo(this.model, "change:hideData", function () {
            //Create the menu options for the Data section link
            var menuOptions = [];
            if (this.model.get("hideData") === true) {
              menuOptions.push("Show");
            } else {
              menuOptions.push("Hide");
            }

            this.updateSectionLink(dataView, menuOptions);
          });

          // Add the data section to the list of subviews
          this.subviews.push(dataView);
        } catch (e) {
          console.error(e);
        }
      },

      /**
       * Renders the Metrics section of the editor
       */
      renderMetricsSection: function () {
        // Render a PortEditorSectionView for the Metrics section if metrics is set
        // to show, and the view hasn't already been rendered.
        if (this.model.get("hideMetrics") !== true && !this.metricsView) {
          //Create a unique label for this section and save it
          this.updateSectionLabelsList("Metrics");

          //Create a section view
          this.metricsView = new PortEditorSectionView({
            model: this.model,
            uniqueSectionLabel: "Metrics",
            template: this.metricsSectionTemplate,
            sectionType: "metrics",
            editorView: this.editorView,
          });

          this.metricsView.$el.attr("id", "Metrics");
          this.$(this.sectionsContainer).append(this.metricsView.el);

          //Render the view
          this.metricsView.render();

          // Insert the metrics illustration
          $(this.metricsView.el)
            .find(".metrics-figure-container")
            .html(MetricsSVG);

          // Add the data section to the list of subviews
          this.subviews.push(this.metricsView);
        }
        //If the metrics aren't hidden AND the metrics view was created already, then show it
        else if (this.model.get("hideMetrics") !== true && this.metricsView) {
          this.$(this.sectionsContainer).append(this.metricsView.$el);
          this.metricsView.$el.data({
            view: this.metricsView,
            model: this.model,
          });
        }

        //When the metrics section has been toggled, remove or add the link
        this.toggleMetricsLink();
      },

      /**
       * navigateToData - Navigate to the data tab.
       */
      navigateToData: function () {
        if (this.dataView) {
          this.switchSection(this.dataView);
        }
      },

      /**
       * Adds or removes the metrics link depending on the 'hideMetrics' option in
       * the model.
       */
      toggleMetricsLink: function () {
        try {
          // Need a metrics view to exist already if metrics is set to show
          /*  if(!this.metricsView && !this.model.get("hideMetrics") === true){
          this.renderMetricsSection();
        }*/
          //If hideMetrics has been set to true, remove the link
          if (this.model.get("hideMetrics") === true) {
            this.removeSectionLink(this.metricsView);
            // Otherwise add it
          } else {
            this.addSectionLink(this.metricsView, ["Delete"]);
          }
        } catch (e) {
          console.error(e);
        }
      },

      /**
       * Renders the Settings section of the editor
       */
      renderSettings: function () {
        //Create a unique label for this section and save it
        this.updateSectionLabelsList("Settings");

        //Create a PortEditorSettingsView
        var settingsView = new PortEditorSettingsView({
          model: this.model,
          uniqueSectionLabel: "Settings",
        });

        settingsView.editorView = this.editorView;

        //Add the Settings view to the page
        this.$(this.sectionsContainer).append(settingsView.$el);

        //Render the PortEditorSettingsView
        settingsView.render();

        //Create and add a section link
        this.addSectionLink(settingsView);

        // Add the data section to the list of subviews
        this.subviews.push(settingsView);
      },

      /**
       * Update the window location path with the active section name
       * @param {boolean} [showSectionLabel] - If true, the editor section label will be added to the path
       */
      updatePath: function (showSectionLabel) {
        //Get the current portal label
        var label = this.model.get("label") || "",
          //Get the last-saved portal label
          originalLabel = this.model.get("originalLabel") || "",
          //Get the current path from the window location
          pathName = decodeURIComponent(window.location.pathname)
            .substring(MetacatUI.root.length)
            // remove trailing forward slash if one exists in path
            .replace(/\/$/, "");

        // Add or replace the label part of the path with the new label.
        // pathRE matches "/label/section", where the "/section" part is optional
        var pathRE = new RegExp(
          "\\/(" + label + "|" + originalLabel + ")(\\/[^\\/]*)?$",
          "i",
        );
        newPathName = pathName.replace(pathRE, "");

        //If there is a label, add it to the new path.
        // (there will always be a label unless this is a new portal)
        if (label) {
          newPathName += "/" + label;
        }

        //If there is an active section, and the showSectionLabel parameter is true, add the section label to the path.
        if (showSectionLabel && this.activeSection) {
          newPathName += "/" + this.activeSection.uniqueSectionLabel;
        }

        //If the path has changed, navigate to the new path, which creates a record in the browser history
        if (pathName != newPathName) {
          // Update the window location
          MetacatUI.uiRouter.navigate(newPathName, {
            trigger: false,
          });
        }
      },

      /**
       * Returns the section view that has a label matching the one given.
       * @param {string} label - The label for the section
       * @return {PortEditorSectionView|false} - Returns false if a matching section view isn't found
       */
      getSectionByLabel: function (label) {
        //If no label is given, exit
        if (!label) {
          return;
        }

        //Find the section view whose unique label matches the given label. Case-insensitive matching.
        return _.find(this.subviews, function (view) {
          if (typeof view.uniqueSectionLabel == "string") {
            return view.uniqueSectionLabel.toLowerCase() == label.toLowerCase();
          } else {
            return false;
          }
        });
      },

      /**
       * Returns the section view that has a label matching the one given.
       * @param {PortalSectionModel} section - The section model
       * @return {PortEditorSectionView|false} - Returns false if a matching section view isn't found
       */
      getSectionByModel: function (section) {
        //If no section is given, exit
        if (!section) {
          return;
        }

        //Find the section view whose unique label matches the given label. Case-insensitive matching.
        return _.findWhere(this.subviews, { model: section });
      },

      /**
       * Creates and returns a unique label for the given section. This label is just used in the view,
       * because portal sections can have duplicate labels. But unique labels need to be used for navigation in the view.
       * @param {PortEditorSection} sectionModel - The section for which to create a unique label
       * @return {string} The unique label string
       */
      getUniqueSectionLabel: function (sectionModel) {
        //Get the label for this section
        var sectionLabel = sectionModel
            .get("label")
            .replace(/[^a-zA-Z0-9 ]/g, "")
            .replace(/ /g, "-"),
          unalteredLabel = sectionLabel,
          sectionLabels = this.sectionLabels || [],
          i = 2;

        //Concatenate a number to the label if this one already exists
        while (sectionLabels.includes(sectionLabel)) {
          sectionLabel = unalteredLabel + i;
          i++;
        }

        return sectionLabel;
      },

      /**
       * Manually switch to a section subview by making the tab and tab panel active.
       * Navigation between sections is usually handled automatically by the Bootstrap
       * library, but a manual switch may be necessary sometimes
       * @param {PortEditorSectionView} [sectionView] - The section view to switch to. If not given, defaults to the activeSection set on the view.
       */
      switchSection: function (sectionView) {
        //Create a flag for whether the section label should be shown in the URL
        var showSectionLabelInURL = true;

        // If no section view is given, use the active section in the view.
        if (!sectionView) {
          //Use the sectionView set already
          if (this.activeSection) {
            var sectionView = this.activeSection;
          }
          //Or find the section view by name, which may have been passed through the URL
          else if (this.activeSectionLabel) {
            var sectionView = this.getSectionByLabel(this.activeSectionLabel);
          }
        }

        //If no section view was indicated, just default to the first visible one
        if (!sectionView) {
          var sectionView = this.$(
            this.sectionLinkContainer + ":not(.removing)",
          )
            .first()
            .data("view");

          //If we are defaulting to the first section, don't show the section label in the URL
          showSectionLabelInURL = false;

          //If there are no section views on the page at all, exit now
          if (!sectionView) {
            return;
          }
        }

        // Update the activeSection set on the view
        this.activeSection = sectionView;

        // Activate the section content
        this.$(this.sectionEls).each(function (i, contentEl) {
          if ($(contentEl).data("view") == sectionView) {
            $(contentEl).addClass("active");
            sectionView.trigger("active");
          } else {
            // make sure no other sections are active
            $(contentEl).removeClass("active");
          }
        });

        // Activate the link to the content
        this.$(this.sectionLinkContainer).each(function (i, linkEl) {
          if ($(linkEl).data("view") == sectionView) {
            $(linkEl).addClass("active");
          } else {
            // make sure no other sections are active
            $(linkEl).removeClass("active");
          }
        });

        //Never show the section label in the URL, since it messes with the back/forward browser navigation. See #1364
        showSectionLabelInURL = false;
        //Update the location path
        this.updatePath(showSectionLabelInURL);
      },

      /**
       * When a section link has been clicked, switch to that section
       * @param {Event} e - The click event on the section link
       */
      handleSwitchSection: function (e) {
        e.preventDefault();

        // Make sure any markdown editor toolbar modals are closed
        // (otherwise they persist in new tab)
        $("body").find(".wk-prompt").remove();

        // Make sure any markdown editor toolbar modals are closed
        // (otherwise they persist in new tab)
        $("body").find(".wk-prompt").remove();

        var sectionView = $(e.target)
          .parents(this.sectionLinkContainer)
          .first()
          .data("view");

        if (sectionView) {
          this.switchSection(sectionView);
          // If the user clicks a link and is not near the top of the page
          // (i.e. on mobile), scroll to the top of the section content.
          // Otherwise it might look like the page hasn't changed
          if (window.pageYOffset > $("#editor-body").offset().top) {
            MetacatUI.appView.scrollTo($("#editor-body"));
          }
        }
      },

      /**
       * Add a link to the given editor section
       * @param {PortEditorSectionView} sectionView - The view to add a link to
       * @param {string[]} menuOptions - An array of menu options for this section. e.g. Rename, Delete
       * @param {boolean} isFocused - A boolean flag to enable focus on new section link
       */
      addSectionLink: function (sectionView, menuOptions, isFocused) {
        try {
          if (typeof isFocused != "boolean") {
            var isFocused = false;
          }

          var view = this;

          var newLink = this.createSectionLink(sectionView, menuOptions);
          var isMarkdownSection =
            $(newLink).data("view").type == "PortEditorMdSection";

          // Make the tab hidden to start
          $(newLink)
            .find(this.sectionLinks)
            .css("max-width", "0px")
            .css("opacity", "0.2")
            .css("white-space", "nowrap");

          $(newLink)
            .find(".section-menu-link")
            .css("opacity", "0.5")
            .css("transition", "opacity 0.1s");

          // Find the "+" link to help determine the order in which we should add links
          var addSectionEl = this.$(this.sectionLinksContainer).find(
            this.sectionLinkContainer +
              "[data-section-name='" +
              this.addPageLabel +
              "']",
          )[0];

          // If the new link is for a markdown section and there's no user-defined page
          // order, then insert the markdown sections before the data and metrics section
          // (this is the default order when there is no page ordering).
          if (
            isMarkdownSection &&
            (!view.model.get("pageOrder") ||
              !view.model.get("pageOrder").length)
          ) {
            // Find the last markdown section in the list of links
            var currentLinks = this.$(this.sectionLinksContainer).find(
              this.sectionLinkContainer,
            );
            var i = _.map(currentLinks, function (li) {
              return $(li).data("view") ? $(li).data("view").type : "";
            }).lastIndexOf("PortEditorMdSection");
            var lastMdSection = currentLinks[i];
            // Append the new link after the last markdown section, or add it first.
            if (lastMdSection) {
              $(lastMdSection).after(newLink);
            } else {
              this.$(this.sectionLinksContainer).prepend(newLink);
            }
            // If there is already some user-defined page ordering, or if not a markdown
            // section and not the Settings section, and if there is already a "+" link, add
            // new link before the "+" link
          } else if (
            addSectionEl &&
            sectionView.uniqueSectionLabel != "Settings"
          ) {
            $(addSectionEl).before(newLink);
            // If the new link is "Settings", or there's no "+" link yet, insert new link last.
          } else {
            this.$(this.sectionLinksContainer).append(newLink);
          }

          // If this is a newly added markdown section, highlight the section name and make
          // it content editable. Currently only markdown sections labels are editable.
          if (isFocused && isMarkdownSection) {
            var newSectionLink = $(newLink).children(".portal-section-link");
            newSectionLink.attr("contenteditable", true);
            newSectionLink.focus();

            //Select the text of the link
            if (window.getSelection && window.document.createRange) {
              var selection = window.getSelection();
              var range = window.document.createRange();
              range.selectNodeContents(newSectionLink[0]);
              selection.removeAllRanges();
              selection.addRange(range);
            } else if (window.document.body.createTextRange) {
              range = window.document.body.createTextRange();
              range.moveToElementText(newSectionLink[0]);
              range.select();
            }
          }

          // Animate the link to full width / opacity
          $(newLink)
            .find(this.sectionLinks)
            .animate(
              {
                "max-width": "500px",
                overflow: "hidden",
                opacity: 1,
              },
              {
                duration: 300,
                complete: function () {
                  $(newLink).find(".section-menu-link").css("opacity", "1");
                },
              },
            );
        } catch (e) {
          console.error(
            "Could not add a new section link. Error message: " + e,
          );
        }
      },

      /**
       * toggleRemoveSectionOption - Disables the hide and remove option from
       * section link if it's the only section left. Re-enables the remove/hide
       * link when a new section is added. Called on initial render and each time
       * a section is added, removed, shown, or hidden.
       */
      toggleRemoveSectionOption: function () {
        try {
          // Determine the number of pages (sections + metrics + data)
          var totalPages =
              this.model.get("sections").length +
              !this.model.get("hideMetrics") +
              !this.model.get("hideData"),
            removeSectionLinks = this.$(this.sectionLinkContainer).find(
              ".remove-section",
            );

          // If there's just one section, hide the delete and hide option on last
          // remaining section link
          if (totalPages == 1) {
            removeSectionLinks.addClass("disabled");

            if (!removeSectionLinks.closest("li").find(".tooltip").length) {
              removeSectionLinks.closest("li").tooltip({
                placement: "bottom",
                trigger: "hover",
                title:
                  "At least one displayed page is required. To remove this page, first add or show another page.",
              });
            }

            // If there are 2 sections, re-show the delete or hide options.
          } else if (totalPages == 2) {
            removeSectionLinks.removeClass("disabled");
            removeSectionLinks.closest("li").tooltip("destroy");

            // If there are three or more pages, nothing needs to be changed.
          } else {
            return;
          }
        } catch (e) {
          console.log(
            "Failure to show/hide the remove section option. Error message: " +
              e,
          );
        }
      },

      /**
       * Add a link to the given editor section
       * @param {PortEditorSectionView} sectionView - The view to add a link to
       * @param {string[]} menuOptions - An array of menu options for this section. e.g. Rename, Delete
       * @return {Element}
       */
      createSectionLink: function (sectionView, menuOptions) {
        var classes = "";
        if (Array.isArray(menuOptions) && menuOptions.includes("Show")) {
          classes = "hidden-section";
        }

        // Do not allow dragging/sorting of the Settings or Add Page sections
        var sortable = true;
        if (["addpage", "settings"].includes(sectionView.sectionType)) {
          sortable = false;
        }

        //Create a section link
        var sectionLink = $(
          this.sectionLinkTemplate({
            menuOptions: menuOptions || [],
            uniqueLabel: sectionView.uniqueSectionLabel,
            sectionLabel: PortalSection.prototype.isPrototypeOf(
              sectionView.model,
            )
              ? sectionView.model.get("label")
              : sectionView.uniqueSectionLabel,
            sectionURL:
              this.model.get("label") + "/" + sectionView.uniqueSectionLabel,
            sectionType: sectionView.sectionType,
            classes: classes,
            handleClass: this.handleClass,
            sortable: sortable,
          }),
        );

        //Attach the section model to the link
        sectionLink.data({
          model: sectionView.model,
          view: sectionView,
        });

        if (
          sectionView.sectionType == "freeform" &&
          menuOptions.includes("Delete")
        ) {
          var content = $(document.createElement("div")).append(
            $(document.createElement("div")).append(
              $(document.createElement("p")).text(
                "Deleting this page will premanently remove it from this " +
                  MetacatUI.appModel.get("portalTermSingular") +
                  ".",
              ),
            ),
            $(document.createElement("div"))
              .addClass("inline-buttons")
              .append(
                $(document.createElement("button"))
                  .addClass("btn cancelled-section-removal")
                  .text("No, keep page"),
                $(document.createElement("button"))
                  .addClass("btn btn-danger confirmed-section-removal")
                  .text("Yes, delete page"),
              ),
          );

          // Create a popover with the confirmation buttons
          sectionLink.find(".remove-section").addClass("popover-this").popover({
            html: true,
            placement: "right",
            title: "Delete this page?",
            content: content,
            container: sectionLink,
            trigger: "click",
          });
        }

        return sectionLink[0];
      },

      /**
       * Add a link to the given editor section
       * @param {PortEditorSectionView} sectionView - The view to add a link to
       * @param {string[]} menuOptions - An array of menu options for this section. e.g. Rename, Delete
       */
      updateSectionLink: function (sectionView, menuOptions) {
        //Create a new link to the section
        var sectionLink = this.createSectionLink(sectionView, menuOptions);

        //Replace the existing link
        this.$(this.sectionLinksContainer)
          .children()
          .each(function (i, link) {
            if ($(link).data("view") == sectionView) {
              $(link).replaceWith(sectionLink);
            }
          });
      },

      /**
       * Remove the link to the given section view
       * @param {View} sectionView - The view to remove the link to
       */
      removeSectionLink: function (sectionView) {
        // Switch to the default section the user is deleting the active section
        if (sectionView == this.activeSection) {
          this.switchSection();
        }

        try {
          var view = this;
          //Find the section link associated with this section view
          this.$(this.sectionLinksContainer)
            .children()
            .each(function (i, link) {
              if ($(link).data("view") == sectionView) {
                //Remove the menu link
                $(link)
                  .addClass("removing")
                  .find(".section-menu-link")
                  .remove();

                //Destroy any popovers
                $(link).popover("destroy");
                $(link).find(".popover-this").popover("destroy");

                //Hide the section name link with an animation
                $(link).animate(
                  { width: "0px", overflow: "hidden" },
                  {
                    duration: 300,
                    complete: function () {
                      this.remove();
                      // If there's a page order option set on the model, update it
                      var pageOrder = view.model.get("pageOrder");
                      if (pageOrder && pageOrder.length) {
                        view.updatePageOrder();
                      }
                    },
                  },
                );
              }
            });
        } catch (e) {
          console.error(e);
        }
      },

      /**
       * Adds a section and tab to this view and the PortalModel
       * @param {string} sectionType - The type of section to add
       */
      addSection: function (sectionType) {
        try {
          //Create a new section to the Portal model
          this.model.addSection(sectionType);

          if (typeof sectionType == "string") {
            switch (sectionType.toLowerCase()) {
              case "data":
                this.switchSection(this.dataView);
                break;
              case "metrics":
                this.renderMetricsSection();
                this.switchSection(this.metricsView);
                break;
              case "freeform":
                // Set up page ordering if it isn't already. This allows us to add a new
                // freeform page at the end of the list of tabs, instead of before Data and
                // Metrics (the default before page ordering was enabled).
                var pageOrder = this.model.get("pageOrder");
                if (!pageOrder || !pageOrder.length) {
                  this.updatePageOrder();
                }
                // Get the section model that was just added
                var newestSection =
                  this.model.get("sections")[
                    this.model.get("sections").length - 1
                  ];
                //Render the content section view for it
                this.renderContentSection(newestSection, true);
                //Switch to that new view
                this.switchSection(this.getSectionByModel(newestSection));
                break;
              case "members":
                // TODO
                // this.switchSection(this.getSectionByLabel("Members"));
                break;
            }

            // If the section we just added is now one of two sections, re-enable
            // the hide/delete button on the other section link.
            this.toggleRemoveSectionOption();

            this.editorView.showControls();

            // Add the item to the the pageOrder option on the model, if it exists
            var pageOrder = this.model.get("pageOrder");
            if (pageOrder && pageOrder.length) {
              this.updatePageOrder();
            }
          } else {
            return;
          }
        } catch (e) {
          console.error(e);
        }
      },

      /**
       * Removes a section and its tab from this view and the PortalModel.
       * At least one of the parameters is required, but not both
       * @param {Event} [e] - (optional) The click event on the Remove button
       * @param {Element|jQuery} [sectionLink] - The link element of the section to be removed.
       */
      removeSection: function (e, sectionLink) {
        try {
          if (!sectionLink || !sectionLink.length) {
            var clickedEl = $(e.target);

            //Get the PortalSection model for this remove button
            var sectionLink = clickedEl
              .parents(this.sectionLinkContainer)
              .first();

            //Exit if no section link was found
            if (!sectionLink || !sectionLink.length) {
              return;
            }
          }

          //Get the section model and view
          var sectionModel = sectionLink.data("model"),
            sectionView = sectionLink.data("view"),
            sectionType = sectionLink.data("section-type"),
            uniqueSectionLabel = sectionView.uniqueSectionLabel,
            sectionLabelIndex = this.sectionLabels.indexOf(uniqueSectionLabel);

          if (PortalSection.prototype.isPrototypeOf(sectionModel)) {
            // Remove this section from the Portal
            this.model.removeSection(sectionModel);
            // Remove the section label from the list of unique section labels
            if (sectionLabelIndex > -1) {
              this.sectionLabels.splice(sectionLabelIndex, 1);
            }
          } else {
            //Remove this section type from the model
            this.model.removeSection(sectionType);
          }

          try {
            //If no section view was found, exit now
            if (!sectionView) {
              return;
            }

            //If this is not the Data section, remove the view, since Data sections can only be hidden
            if (sectionType.toLowerCase() != "data") {
              // remove the sectionView
              this.removeSectionLink(sectionView);

              // remove the section view from the subviews array
              this.subviews.splice($.inArray(sectionView, this.subviews), 1);

              //Remove the view from the page
              sectionView.$el.remove();

              //Reset the active section, if the one that was removed is currently active
              if (this.activeSection == sectionView) {
                this.activeSection = undefined;

                //Switch to the default section
                this.switchSection();
              }
            }
          } catch (error) {
            console.error(error);
          }

          // If the section just removed was the second-to-last section, disable
          // the hide/delete button on the last section link.
          this.toggleRemoveSectionOption();

          this.editorView.showControls();
        } catch (e) {
          console.error(e);
          MetacatUI.appView.showAlert(
            "The section could not be deleted. (" + e.message + ")",
            "alert-error",
          );
        }
      },

      /**
       * Shows a previously-hidden section
       * @param {Event} [e] - (optional) The click event on the Show button
       */
      showSection: function (e) {
        try {
          //Get the PortalSection model for this show button
          var sectionLink = $(e.target).parents(this.sectionLinkContainer),
            section = sectionLink.data("model");

          //If this section is not a PortalSection model, get the section type
          if (!PortalSection.prototype.isPrototypeOf(section)) {
            section = sectionLink.data("section-type");
          }

          //If no section was found, exit now
          if (!section) {
            return;
          }

          //Mark this section as shown
          this.model.addSection(section);

          // If the section we're now showing is now one of two sections, re-enable
          // the hide/delete button on the other section link.
          this.toggleRemoveSectionOption();
        } catch (e) {
          console.error(e);
          MetacatUI.appView.showAlert(
            "The section could not be shown. (" + e.message + ")",
            "alert-error",
          );
        }
      },

      /**
       * Renames a section in the tab in this view and in the PortalSectionModel
       * @param {Event} [e] - (optional) The click event on the Rename button
       */
      renameSection: function (e) {
        try {
          //Get the PortalSection model for this rename button
          var sectionLink = $(e.target).parents(this.sectionLinkContainer),
            targetLink = sectionLink.children(this.sectionLinks),
            section = sectionLink.data("model");

          // double-click events
          if (e.type === "dblclick") {
            // Continue editing tab-name on double click only for markdown sections
            if ($(sectionLink).data("view").type != "PortEditorMdSection") {
              return;
            }
          }

          // make the text editable
          targetLink.attr("contenteditable", true);

          // add focus to the text
          targetLink.focus();

          //Select the text of the link
          if (window.getSelection && window.document.createRange) {
            var selection = window.getSelection();
            var range = window.document.createRange();
            range.selectNodeContents(targetLink[0]);
            selection.removeAllRanges();
            selection.addRange(range);
          } else if (window.document.body.createTextRange) {
            range = window.document.body.createTextRange();
            range.moveToElementText(targetLink[0]);
            range.select();
          }
        } catch (error) {
          console.error(error);
        }
      },

      /**
       * Stops user from entering more than 50 characters, and shows a message
       * if user tries to exceed the limit. Also stops a user from entering
       * RETURN or TAB characters, and instead re-directs to updateName().
       * In the case of the TAB key, the focus moves to the title field.
       * @param {Event} e - The keyup or keydown event when the user types in the portal-section-link field
       */
      limitLabelInput: function (e) {
        try {
          // Character limit for the labels
          var limit = 50;
          var currentLabel = $(e.target).text();

          // If the RETURN key is pressed
          if (e.which == 13) {
            // Don't allow character to be entered
            e.preventDefault();
            e.stopPropagation();
            // Update name and exit function
            this.updateName(e);
            return;
          }

          // If the TAB key is pressed
          if (e.which == 9) {
            // Don't allow character to be entered
            e.preventDefault();
            e.stopPropagation();
            // Update name, change focus to title, and exit function
            this.updateName(e);
            $("textarea.title").focus();
            return;
          }

          // Keys that a user can use as normal, even if character limit is met
          var allowedKeys = [
            8, // DELETE
            35, // END
            36, // HOME
            37, // LEFT
            38, // UP
            39, // RIGHT
            40, // DOWN
            46, // DEL
            17, // CTRL
          ];

          // Stop addition of more characters and show message
          if (
            // If at or greater than limit and
            currentLabel.length >= limit &&
            // key isn't a special key and
            !allowedKeys.includes(e.which) &&
            // cmd key isn't held down and
            !e.metaKey &&
            // user doesn't have some of the text selected
            !window.getSelection().toString().length
          ) {
            // Don't allow character to be entered
            e.preventDefault();
            e.stopPropagation();
            // Add a tooltip if one doesn't exist yet
            if (!$(e.delegateTarget).find(".tooltip").length) {
              $(e.target).tooltip({
                placement: "top",
                trigger: "manual",
                title: "Limit of " + limit + " characters or fewer",
              });
            }
            // Show the tooltip
            $(e.target).tooltip("show");
            // If under the character limit, proceed as normal.
          } else {
            // Make sure there's no tooltip showing.
            $(e.delegateTarget).find(".tooltip").remove();
          }
        } catch (error) {
          "Error limiting user input in label field, error message: " + error;
        }
      },

      /**
       * Update the section label
       *
       * @param e The event triggering this method
       */
      updateName: function (e) {
        // Remove tooltip incase one was set by limitLabelInput function
        $(e.delegateTarget).find(".tooltip").remove();

        try {
          //Get the PortalSection model for this rename button
          var sectionLink = $(e.target).parents(this.sectionLinkContainer),
            targetLink = sectionLink.find(this.sectionLinks),
            sectionModel = sectionLink.data("model"),
            sectionView = sectionLink.data("view"),
            // Clean up the typed in name so it's valid for XML
            oldLabel = sectionModel.get("label"),
            newLabel = this.model.cleanXMLText(targetLink.text().trim()),
            pageOrder = this.model.get("pageOrder");

          // Remove the content editable attribute
          targetLink.attr("contenteditable", false);

          // If this section is an object of PortalSection model, update the label.
          if (
            sectionModel &&
            PortalSection.prototype.isPrototypeOf(sectionModel)
          ) {
            // update the label on the model
            sectionModel.set("label", newLabel);
          } else {
            // TODO: handle the case for non-markdown sections
          }

          // Update the name set on the link element, since it's used for setting the pageOrder option
          sectionLink.data("section-name", newLabel);

          // Update the name in the pageOrder option, if it exists
          if (pageOrder && pageOrder.length) {
            this.updatePageOrder();
          }

          // Update the array of unique section labels

          // Get the position of the unique label in the list
          var indexIDs = this.sectionLabels.indexOf(
              sectionView.uniqueSectionLabel,
            ),
            newUniqueLabel = "";

          // Remove the old unique label so when we create a new unique label,
          // we don't consider the label that we're replacing in determining uniqueness
          if (indexIDs > -1) {
            this.sectionLabels.splice(indexIDs, 1);
            newUniqueLabel = this.getUniqueSectionLabel(sectionModel);
            this.sectionLabels.splice(indexIDs, 0, newUniqueLabel);
          } else {
            newUniqueLabel = this.getUniqueSectionLabel(sectionModel);
            this.sectionLabels.push(newUniqueLabel);
          }

          // Update the label set on the view
          sectionView.uniqueSectionLabel = newUniqueLabel;
        } catch (error) {
          console.error(error);
        }
      },

      /**
       * Using the "section-name" data attribute set on each section link,
       * and the order that the links are displayed in the DOM, update the
       * pageOrder option in the portal model.
       */
      updatePageOrder: function () {
        try {
          var view = this;
          // Get the links as they are ordered in the UI
          var links = view.el.querySelectorAll(
              view.sectionLinksContainer + view.sortableLinksSelector,
            ),
            pageOrder = [];
          _.each(links, function (link) {
            // Use the value set on section-name to re-order pages
            var label = $(link).data("section-name");
            if (label) {
              pageOrder.push(label);
            }
          });
          view.model.set("pageOrder", pageOrder);
          view.editorView.showControls();
        } catch (error) {
          console.log(
            "Error updating the portal page order, message: " + error,
          );
        }
      },

      /**
       * Add a new unique label to the list of unique section labels
       * (used the ensure that tab links and anchors are unique,
       * otherwise, tab switching does not work)
       */
      updateSectionLabelsList: function (newLabel) {
        try {
          if (!this.sectionLabels) {
            this.sectionLabels = [];
          }
          this.sectionLabels.push(newLabel);
        } catch (error) {
          console.log(
            "Error updating the list of unique section labels. Error message: " +
              error,
          );
        }
      },

      /**
       * Shows a validation error message and adds error styling to the given elements
       * @param {jQuery} elements - The elements to add error styling and messaging to
       */
      showValidation: function (elements) {
        try {
          //Get the parent elements that have ids set
          var sectionEls = elements.parents(this.sectionEls);

          //See if there is a matching section link
          for (var i = 0; i < sectionEls.length; i++) {
            //Get the section view attached to the section element
            var sectionView = sectionEls.data("view");

            //If a section view was found,
            if (sectionView) {
              //Find the section link that links to this section view
              var matchingLink = _.find(
                $(this.sectionLinkContainer),
                function (link) {
                  return $(link).data("view") == sectionView;
                },
              );

              //Add the error class and display the error icon
              if (matchingLink) {
                $(matchingLink).addClass("error").find(".icon.error").show();
                //Exit the loop
                i = sectionEls.length + 1;
              }
            }
          }
        } catch (e) {
          console.error("Error showing validation message: ", e);
        }
      },

      /**
       * Closes all the popovers in this view
       */
      closePopovers: function () {
        this.$(".popover-this").popover("hide");
      },

      /**
       * This function is called when the app navigates away from this view.
       * Any clean-up or housekeeping happens at this time.
       */
      onClose: function () {
        //Remove each subview from the DOM and remove listeners
        _.invoke(this.subviews, "remove");

        this.subviews = new Array();

        //Remove the reference to the EditorView
        this.editorView = null;
      },
    },
  );

  return PortEditorSectionsView;
});