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

define([
  "underscore",
  "jquery",
  "backbone",
  "models/portals/PortalModel",
  "models/portals/PortalImage",
  "collections/Filters",
  "views/EditorView",
  "views/SignInView",
  "views/portals/editor/PortEditorSectionsView",
  "views/portals/editor/PortEditorImageView",
  "text!templates/loading.html",
  "text!templates/portals/editor/portalEditor.html",
  "text!templates/portals/editor/portalEditorSubmitMessage.html",
  "text!templates/portals/editor/portalLoginPage.html",
], function (
  _,
  $,
  Backbone,
  Portal,
  PortalImage,
  Filters,
  EditorView,
  SignInView,
  PortEditorSectionsView,
  ImageEdit,
  LoadingTemplate,
  Template,
  portalEditorSubmitMessageTemplate,
  LoginTemplate,
) {
  /**
   * @class PortalEditorView
   * @classdesc A view of a form for creating and editing DataONE Portal documents
   * @classcategory Views/Portals/Editor
   * @name PortalEditorView
   * @extends EditorView
   * @constructs
   */
  var PortalEditorView = EditorView.extend(
    /** @lends PortalEditorView.prototype */ {
      /**
       * The type of View this is
       * @type {string}
       */
      type: "PortalEditor",

      /**
       * The short name OR pid for the portal
       * @type {string}
       */
      portalIdentifier: "",

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

      /**
       * The currently active editor section. e.g. Data, Metrics, Settings, etc.
       * @type {string}
       */
      activeSectionLabel: "",

      /**
       * When a new portal is being created, this is the label of the section that will be active when the editor first renders
       * @type {string}
       */
      newPortalActiveSectionLabel:
        (MetacatUI.appModel.get("portalDefaults")
          ? MetacatUI.appModel.get("portalDefaults").newPortalActiveSectionLabel
          : "") || "Settings",

      /**
       * References to templates for this view. HTML files are converted to Underscore.js templates
       */
      template: _.template(Template),
      loadingTemplate: _.template(LoadingTemplate),
      loginTemplate: _.template(LoginTemplate),
      // Over-ride the default editor submit message template (which is currently
      // used by the metadata editor) with the portal editor version
      editorSubmitMessageTemplate: _.template(
        portalEditorSubmitMessageTemplate,
      ),

      /**
       * An array of Backbone Views that are contained in this view.
       * @type {Backbone.View[]}
       */
      subviews: [],

      /**
       * A reference to the PortEditorSectionsView for this instance of the PortEditorView
       * @type {PortEditorSectionsView}
       */
      sectionsView: null,

      /**
       * The text to use in the editor submit button
       * @type {string}
       */
      submitButtonText: "Save",

      /**
       * A jQuery selector for the element that the PortEditorSectionsView should be inserted into
       * @type {string}
       */
      portEditSectionsContainer: ".port-editor-sections-container",

      /**
       * A jQuery selector for the element that the portal logo image uploader
       * should be inserted into
       * @type {string}
       */
      portEditLogoContainer: ".logo-editor-container",

      /**
       * A jQuery selector for links to view this portal
       * @type {string}
       */
      viewPortalLinks: ".view-portal-link",

      /**
       * A temporary name to use for portals when they are first created but don't have a label yet.
       * This name should only be used in views, and never set on the model so it doesn't risk getting
       * serialized and saved.
       * @type {string}
       */
      newPortalTempName: "new",

      /**
       * The events this view will listen to and the associated function to call.
       * This view will inherit events from the parent class, EditorView.
       * @type {Object}
       */
      events: _.extend(EditorView.prototype.events, {
        "focusout .basic-text": "updateBasicText",
        "click .section-links-toggle-container": "toggleSectionLinks",
      }),

      /**
       * Is executed when a new PortalEditorView is created
       * @param {Object} options - A literal object with options to pass to the view
       */
      initialize: function (options) {
        EditorView.prototype.initialize.call(this, options);

        //Reset arrays and objects set on this View, otherwise they will be shared across intances, causing errors
        this.subviews = new Array();
        this.sectionsView = null;

        if (typeof options == "object") {
          // initializing the PortalEditorView properties
          this.portalIdentifier = options.portalIdentifier
            ? options.portalIdentifier
            : undefined;
          this.activeSectionLabel = options.activeSectionLabel || "";
        }
      },

      /**
       * Renders the PortalEditorView
       */
      render: function () {
        //Execute the superclass render() function, which will add some basic Editor functionality
        EditorView.prototype.render.call(this);

        $("body").addClass("Portal");

        // Display a spinner to indicate loading until model is created.
        this.$el.html(
          this.loadingTemplate({
            msg: "Retrieving portal details...",
          }),
        );

        //Create the model
        this.createModel();

        // An existing portal should have a portalIdentifier already set
        // from the router, that does not equal the newPortalTempName ("new"),
        // plus a seriesId or label set during createModel()
        if (
          (this.model.get("seriesId") || this.model.get("label")) &&
          this.portalIdentifier &&
          this.portalIdentifier != this.newPortalTempName
        ) {
          var view = this;

          this.listenToOnce(this.model, "change:isAuthorized", function () {
            if (this.model.get("isAuthorized")) {
              // When an existing model has been synced render the results
              view.stopListening(view.model, "sync", view.renderPortalEditor);
              view.listenToOnce(view.model, "sync", view.renderPortalEditor);
              view.listenToOnce(view.model, "error", (_model, response) => {
                view.stopListening(view.model, "sync", view.renderPortalEditor);
                view.hideLoading();
                const message =
                  typeof response === "string"
                    ? response
                    : "The portal could not be loaded. Please try again.";
                MetacatUI.appView.showAlert({
                  message: $("<p></p>").text(message)[0],
                  classes: "alert-error non-fixed",
                  container: view.el,
                });
              });

              // If the portal model already exists - fetch it.
              view.model.fetch();

              // Listens to the focus event on the window to detect when a user
              // switches back to this browser tab from somewhere else
              // When a user checks back, we want to check for log-in status
              MetacatUI.appView.listenForActivity();

              // Determine the length of time until the user's current token expires
              // Asks to sign in in case of time out
              MetacatUI.appView.listenForTimeout();
            } else {
              // generate error message
              var msg = MetacatUI.appModel.get("portalEditNotAuthEditMessage");

              //Show the not authorized error message
              MetacatUI.appView.showAlert(
                msg,
                "alert-error non-fixed",
                this.$el,
              );
            }
          });

          // Check if the user is Authorized to edit the portal
          this.authorizeUser();
        }
        //If there is no portal identifier given, this is a new portal.
        else {
          // if the user is not signed in, display the sign in view
          if (
            MetacatUI.appUserModel.get("tokenChecked") &&
            !MetacatUI.appUserModel.get("loggedIn")
          ) {
            this.showSignIn();
          } else {
            // Check whether the user can create a portal
            this.listenToOnce(
              MetacatUI.appUserModel,
              "change:isAuthorizedCreatePortal",
              function () {
                if (MetacatUI.appUserModel.get("isAuthorizedCreatePortal")) {
                  // Start new portals on the settings tab
                  this.activeSectionLabel = this.newPortalActiveSectionLabel;

                  // Render the default model if the portal is new
                  this.renderPortalEditor();
                } else {
                  const errorMessage = MetacatUI.appModel.get(
                    "portalEditNotAuthCreateMessage",
                  );

                  //Hide the loading icon
                  this.hideLoading();

                  //Show the error message
                  MetacatUI.appView.showAlert(
                    errorMessage,
                    "alert-error non-fixed",
                    this.$el,
                  );
                }

                //Reset the isAuthorizedCreatePortal attribute
                MetacatUI.appUserModel.set("isAuthorizedCreatePortal", null);
              },
            );

            //If the user authentication hasn't been checked yet, then wait for it
            if (!MetacatUI.appUserModel.get("tokenChecked")) {
              this.listenTo(
                MetacatUI.appUserModel,
                "change:tokenChecked",
                function () {
                  if (MetacatUI.appUserModel.get("loggedIn")) {
                    //Check if this user is authorized to create a new portal
                    MetacatUI.appUserModel.isAuthorizedCreatePortal();
                  }
                  //If the user is not logged in, show the sign in buttons
                  else if (!MetacatUI.appUserModel.get("loggedIn")) {
                    this.showSignIn();
                  }
                },
              );
              return;
            }
            //If the user is logged in,
            else if (MetacatUI.appUserModel.get("loggedIn")) {
              //Check if this user is authorized to create a new portal
              MetacatUI.appUserModel.isAuthorizedCreatePortal();
            }
            //If the user is not logged in, show the sign in buttons
            else if (!MetacatUI.appUserModel.get("loggedIn")) {
              this.showSignIn();
            }
          }
        }

        return this;
      },

      /**
       * Renders the portal editor view once the portal view is created
       */
      renderPortalEditor: function () {
        var view = this;
        this.stopListening(this.model, "error");

        // Add the template to the view and give the body the "Editor" class
        this.$el.html(
          this.template({
            name: this.model.get("name"),
            submitButtonText: this.submitButtonText,
            primaryColor: this.model.get("primaryColor"),
            secondaryColor: this.model.get("secondaryColor"),
            accentColor: this.model.get("accentColor"),
            primaryColorTransparent: this.model.get("primaryColorTransparent"),
            secondaryColorTransparent: this.model.get(
              "secondaryColorTransparent",
            ),
            accentColorTransparent: this.model.get("accentColorTransparent"),
          }),
        );

        //Render the editor controls
        this.renderEditorControls();

        //Hide the Save controls
        this.hideControls();

        //Remove the rendering class from the body element
        $("body").removeClass("rendering");

        // On mobile where the section-links-toggle-container is set to fixed,
        // hide the portal navigation element when user scrolls down,
        // show again when the user scrolls up.
        MetacatUI.appView.prevScrollpos = window.pageYOffset;
        $(window).off("scroll");
        $(window).scroll(_.throttle(view.handleScroll.bind(view), 60));

        // Functions to perform when the window is resized
        var onResize = function () {
          view.updateAlertOffset();
          // Auto-resize the portal title
          $("textarea.portal-title").trigger("windowResize");
          // Ensure that the menu is always shown when switching from mobile to full width
          view.toggleSectionLinks();
        };
        $(window).off("resize");
        $(window).resize(_.throttle(onResize, 400));

        // Auto-resize the height of the portal title field on user-input and on
        // window resize events.
        this.$("textarea.portal-title")
          .each(function () {
            this.style.height = "0px"; // note: textfield MUST have a min-height set
            this.style.height = this.scrollHeight + "px";
          })
          .on("input windowResize", function () {
            this.style.height = "0px"; // note: textfield MUST have a min-height set
            this.style.height = this.scrollHeight + "px";
          });

        // Get the portal identifier
        // or set it to a default value in the case that it's a new portal
        var portalIdentifier = this.portalIdentifier;
        if (!portalIdentifier) {
          portalIdentifier = this.newPortalTempName;
        }

        //Create a view for the editor sections
        this.sectionsView = new PortEditorSectionsView({
          model: this.model,
          activeSectionLabel: this.activeSectionLabel,
          newPortalTempName: this.newPortalTempName,
        });

        //Save the PortEditorSectionsView as a subview
        this.subviews.push(this.sectionsView);

        //Attach a reference to this view
        this.sectionsView.editorView = this;

        //Add the view element to this view
        this.$(this.portEditSectionsContainer).html(this.sectionsView.el);

        //Render the sections view
        this.sectionsView.render();

        //Show the required fields for this editor
        this.renderRequiredIcons(
          MetacatUI.appModel.get("portalEditorRequiredFields"),
        );

        // Insert the logo editor
        this.renderLogoEditor();

        // When the collection definition is changed, show the Save button
        var definition = this.model.get("definition"),
          definitionEvents = "update change";
        this.stopListening(definition, definitionEvents);
        this.listenTo(definition, definitionEvents, function (model, record) {
          // Don't show the controls for the addition of an empty filter model, or the
          // controls will show right away when we add a new blank query rule
          if (
            record &&
            record.changes &&
            record.changes.added &&
            record.changes.added.length
          ) {
            if (
              record.changes.added[0].isEmpty &&
              record.changes.added[0].isEmpty()
            ) {
              return;
            }
          }
          this.showControls();
        });

        // On mobile, hide section tabs a moment after page loads so
        // users notice where they are
        setTimeout(function () {
          view.toggleSectionLinks();
        }, 700);

        //Show a link to view the portal, if it is not a new portal
        if (!this.model.isNew()) {
          var viewURL =
            MetacatUI.root +
            "/" +
            MetacatUI.appModel.get("portalTermPlural") +
            "/" +
            portalIdentifier;
          //Update the view URL in any other portal view links
          this.$(this.viewPortalLinks).attr("href", viewURL).show();
        } else {
          //Remove the href attribute and hide the link
          this.$(this.viewPortalLinks).attr("href", "").hide();
        }

        onResize();
      },

      /**
       * Create a PortalModel object
       */
      createModel: function () {
        // Look up the portal document seriesId by its registered name if given
        if (
          this.portalIdentifier &&
          this.portalIdentifier != this.newPortalTempName
        ) {
          // Create a new portal model with the identifier
          this.model = new Portal({
            label: this.portalIdentifier,
            edit: true,
          });

          // Save the original label in case a user changes it. During URL
          // validation, the original label will always be shown as available.
          // TODO: if user navigates to portal using a SID or PID, we will need
          // to get the matching label and then save it to the model
          this.model.set("originalLabel", this.portalIdentifier);

          // Otherwise, create a new portal
        } else {
          // Create a new, default portal model
          this.model = new Portal({
            //Set the isNew attribute so the model will execute certain functions when a Portal is new
            isNew: true,
            rightsHolder: MetacatUI.appUserModel.get("username"),
            isAuthorized_read: true,
            isAuthorized_write: true,
            isAuthorized_changePermission: true,
            edit: true,
          });
        }

        // set listeners on the new model
        this.setListeners();
      },

      /**
       * The authorizeUser function checks if the current user is authorized
       * to edit the given PortalModel. If not, a message is displayed and
       * the view doesn't render anything else.
       *
       * If the user isn't logged in at all, don't check for authorization and
       * display a message and login button.
       */
      authorizeUser: function () {
        //If the user authentication hasn't been checked yet, wait for it to finish.
        if (!MetacatUI.appUserModel.get("tokenChecked")) {
          this.listenToOnce(
            MetacatUI.appUserModel,
            "change:tokenChecked",
            this.authorizeUser,
          );
          return;
        }
        //If the user authentication has been checked and they are not logged in, then display the Sign In buttons
        else if (
          MetacatUI.appUserModel.get("tokenChecked") &&
          !MetacatUI.appUserModel.get("loggedIn")
        ) {
          //Remove the loading message
          this.hideLoading();

          // show the sign in view
          this.showSignIn();

          return;
        } else {
          //If the seriesId hasn't been found yet, but we have the label
          if (
            !this.model.get("seriesId") &&
            !this.model.get("latestVersion") &&
            this.model.get("label")
          ) {
            //When the seriesId or latest pid is found, come back to this function
            this.listenToOnce(
              this.model,
              "change:seriesId",
              this.authorizeUser,
            );
            this.listenToOnce(
              this.model,
              "latestVersionFound",
              this.authorizeUser,
            );

            //If the portal isn't found, display a 404 message
            this.listenToOnce(this.model, "notFound", this.showNotFound);

            //Get the seriesId or latest pid
            this.model.getSeriesIdByLabel();
            return;
          } else {
            //Remove the listeners for the seriesId and latest pid
            this.stopListening(
              this.model,
              "change:seriesId",
              this.authorizeUser,
            );
            this.stopListening(
              this.model,
              "latestVersionFound",
              this.authorizeUser,
            );
          }

          // checking for the write Permission
          this.model.checkAuthority("write");
        }
      },

      /**
       * Hides the loading
       */
      hideLoading: function () {
        // Find the loading object and remove it.
        if (this.$el.find(".loading")) {
          this.$el.find(".loading").remove();
        }
      },

      /**
       * toggleSectionLinks - show or hide the section links. Used for the
       * mobile/small screen view of the portal.
       */
      toggleSectionLinks: function (e) {
        try {
          // Don't close the menu if the user clicked the dropdown for the rename/delete menu,
          // or something within that menu. Also do not close when the user clicked to update
          // the tab name in a content editable element.
          if (e && e.target) {
            if (
              $(e.target).closest(".section-menu-link").length ||
              $(e.target).closest(".dropdown-menu").length ||
              $(e.target).attr("contentEditable") == "true"
            ) {
              return;
            }
          }
          var tabs = this.$("#portal-section-tabs");
          if (!tabs) {
            return;
          }
          // Only toggle the section links on mobile. On mobile, the
          // ".show-sections-toggle" is visible.
          if (this.$(".show-sections-toggle").is(":visible")) {
            tabs.slideToggle();
            // If not on mobile, the section tabs should always be visible
          } else {
            tabs.show();
          }
        } catch (e) {
          console.error("Failed to toggle section links, error message: " + e);
        }
      },

      /**
       * renderLogoEditor - Creates a new PortalImage model for the portal logo if
       *  one doesn't exist already, then inserts an ImageEdit view into the
       *  portEditLogoContainer.
       */
      renderLogoEditor: function () {
        try {
          // If the portal has no logo, add the default model for one
          if (!this.model.get("logo")) {
            this.model.set(
              "logo",
              new PortalImage({
                label: "logo",
                nodeName: "logo",
              }),
            );
          }
          // Add the image view (incl. uploader) for the portal logo
          this.logoEdit = new ImageEdit({
            model: this.model.get("logo"),
            imageUploadInstructions: "Upload logo",
            imageWidth: 70,
            imageHeight: 70,
            minWidth: 64,
            minHeight: 64,
            maxHeight: 300,
            maxWidth: 300,
            nameLabel: false,
            urlLabel: false,
            imageTagName: "img",
            removeButton: false,
          });
          this.$(this.portEditLogoContainer).append(this.logoEdit.el);
          this.logoEdit.render();
          this.logoEdit.editorView = this;

          this.listenTo(this.model.get("logo"), "change", this.showControls);
        } catch (e) {
          console.error(
            "Logo editor view could not be rendered. Error message: " + e,
          );
        }
      },

      /**
       * When a simple text input field loses focus, the corresponding model
       * attribute is updated with the value from the input field
       *
       *  @param {Event} [e] - The focusout event
       */
      updateBasicText: function (e) {
        if (!e) return false;

        //Get the category, new value, and model
        var category = $(e.target).attr("data-category"),
          value = $(e.target).val(),
          model = $(e.target).data("model") || this.model;

        //We can't update anything without a category
        if (!category) return false;

        //Clean up the value string so it's valid for XML
        value = this.model.cleanXMLText(value);

        //If the value is an empty string,
        if (typeof value == "string" && !value.length) {
          //Remove the value from the input
          $(e.target).val("");
        }
        //If the value is only spaces,
        else if (typeof value == "string" && !value.trim().length) {
          //Remove the value from the input
          $(e.target).val("");
          //Update the model as if this is an empty string
          value = "";
        }

        //Get the current value
        var currentValue = model.get(category);

        //Insert the new value into the array
        if (Array.isArray(currentValue)) {
          //Find the position this text input is in
          var position = $(e.target)
            .parents("div.text-container")
            .first()
            .children("div")
            .index($(e.target).parent());

          //Set the value in that position in the array
          currentValue[position] = value;

          //Set the changed array on this model
          model.set(category, currentValue);
          model.trigger("change:" + category);
        }
        //Update the model if the current value is a string
        else if (typeof currentValue == "string" || !currentValue) {
          model.set(category, value);
          model.trigger("change:" + category);
        }

        //TODO: Add another blank text input (write addBasicText function)
        // if($(e.target).is(".new") && value != '' && category != "title"){
        //   $(e.target).removeClass("new");
        //   this.addBasicText(e);
        // }
      },

      /**
       * When the object is saved successfully, tell the user.
       * @param {object} savedObject - the object that was successfully saved
       */
      saveSuccess: function (savedObject) {
        var identifier =
            this.model.get("label") ||
            this.model.get("seriesId") ||
            this.model.get("id"),
          viewURL =
            MetacatUI.root +
            "/" +
            MetacatUI.appModel.get("portalTermPlural") +
            "/" +
            identifier;

        var message = this.editorSubmitMessageTemplate({
          messageText: "Your changes have been submitted.",
          viewURL: viewURL,
          buttonText:
            "View your " + MetacatUI.appModel.get("portalTermSingular"),
        });

        MetacatUI.appView.showAlert(message, "alert-success", this.$el, null, {
          remove: true,
        });

        //Update the view URL in any other portal view links
        this.$(this.viewPortalLinks).attr("href", viewURL).show();

        this.hideSaving();

        this.removeValidation();

        // Update the path in case the user selected a new portal label
        this.sectionsView.updatePath();

        // Reset the original label (note: this MUST occur AFTER updatePath())
        this.model.set("originalLabel", this.model.get("label"));
      },

      /**
       * When the Portal model has been flagged as invalid, show the validation error messages
       */
      showValidation: function () {
        //First clear all the error messaging
        this.removeValidation();

        var errors = this.model.validationError;

        _.each(
          errors,
          function (errorMsg, category) {
            var categoryEls = this.$("[data-category='" + category + "']");

            //The label category is unique, because it is duplicated in the PortalImage, which can cause bugs
            if (category == "label") {
              categoryEls = this.$(
                ".change-label-container [data-category='label']",
              );
              var settingsView = _.findWhere(this.sectionsView.subviews, {
                type: "PortEditorSettings",
              });
              //Show the "change label" elements so the validation will appear
              settingsView.changeLabel();
            }

            //Get the elements that have views attached to them
            var elsWithViews = _.filter(categoryEls, function (el) {
              return (
                $(el).data("view") &&
                $(el).data("view").showValidation &&
                !$(el).data("view").isNew
              );
            });

            //If at least one element of this category has a view,
            if (elsWithViews.length) {
              //Use the view's showValidation function, if it exists.
              _.each(elsWithViews, function (el) {
                var view = $(el).data("view");

                if (view && view.showValidation) {
                  view.showValidation();
                }
              });
            } else {
              //Show the validation message
              this.showValidationMessage(categoryEls, errorMsg);
            }
          },
          this,
        );

        if (errors) {
          MetacatUI.appView.showAlert(
            "Provide the content flagged below before submitting.",
            "alert-error",
            this.$el,
            null,
            {
              remove: true,
            },
          );

          //Hide the saving styling
          this.hideSaving();
        }
      },

      /**
       * 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
       * @param {string} errorMsg - The error message to display
       */
      showValidationMessage: function (elements, errorMsg) {
        //Show the error message
        elements.filter(".notification").addClass("error").text(errorMsg);

        //Add the error class to inputs
        var inputs = elements.filter("textarea, input").addClass("error");

        //Show the validation message in the portal sections
        if (this.sectionsView) {
          this.sectionsView.showValidation(elements);
        }
      },

      /**
       * Removes all the validation error styling and messaging from this view
       */
      removeValidation: function () {
        EditorView.prototype.removeValidation.call(this);
        this.$(
          ".section-link-container.error, input.error, textarea.error",
        ).removeClass("error");
      },

      /**
       * Show Sign In buttons
       */
      showSignIn: function () {
        // Messsage if the user is trying to edit an existing portal
        var title = "Sign in with your ORCID to edit this portal";
        // Message to create a portal if the portal is new
        if (this.model.get("isNew")) {
          title =
            "<strong>You're one step away from the portal builder</strong><br>Start by signing in with your ORCID";
        }

        this.$el.html(
          this.loginTemplate({
            title: title,
            portalInfoLink: MetacatUI.appModel.get("portalInfoURL"),
            portalImageSrc:
              MetacatUI.root + "/img/portals/portal-data-page-example.png",
            altText:
              "Screen shot of a portal data page for a climate research lab. The page shows a search bar, customized filters, and a map of the the geographic area the data covers.",
          }),
        );
      },

      /**
       * @inheritdoc
       */
      isAccessPolicyEditEnabled: function () {
        if (!MetacatUI.appModel.get("allowAccessPolicyChanges")) {
          return false;
        }

        if (!MetacatUI.appModel.get("allowAccessPolicyChangesPortals")) {
          return false;
        }

        let limitedTo = MetacatUI.appModel.get(
          "allowAccessPolicyChangesPortalsForSubjects",
        );
        if (Array.isArray(limitedTo) && limitedTo.length) {
          return (
            _.intersection(
              limitedTo,
              MetacatUI.appUserModel.get("allIdentitiesAndGroups"),
            ).length > 0
          );
        } else {
          return true;
        }
      },

      /**
       * If the given portal doesn't exist, display a Not Found message.
       */
      showNotFound: function () {
        this.hideLoading();

        var notFoundMessage = $(document.createElement("p")).text(
          "The " + MetacatUI.appModel.get("portalTermSingular") + " ",
        );
        notFoundMessage
          .append(
            $(document.createElement("span")).text(
              this.model.get("label") || this.portalIdentifier,
            ),
          )
          .append($(document.createElement("span")).text(" doesn't exist."));

        MetacatUI.appView.showAlert(
          notFoundMessage,
          "alert-error non-fixed",
          this.$el,
          undefined,
          { remove: true },
        );
      },

      /** Keep fixed alerts below the visible part of the theme navigation. */
      updateAlertOffset() {
        const navbar = document.getElementById("Navbar");
        this.el.style.setProperty(
          "--portal-editor-nav-height",
          `${Math.max(0, navbar?.getBoundingClientRect().bottom || 0)}px`,
        );
      },

      /**
       * This function is called whenever the window is scrolled.
       */
      handleScroll: function () {
        this.updateAlertOffset();
        try {
          var menu = $(".section-links-toggle-container")[0],
            editorFooter = this.$("#editor-footer")[0],
            editorFooterHeight = editorFooter ? editorFooter.offsetHeight : 0,
            menuHeight = menu ? menu.offsetHeight : 0,
            hiddenHeight = menuHeight * -1 + editorFooterHeight,
            currentScrollPos = window.pageYOffset;

          if (!menu) {
            return;
          }
          if (MetacatUI.appView.prevScrollpos >= currentScrollPos) {
            // when scrolling upward
            menu.style.bottom = editorFooterHeight + "px";
          } else {
            // when scrolling downward
            menu.style.bottom = hiddenHeight + "px";
          }
          MetacatUI.appView.prevScrollpos = currentScrollPos;
        } catch (error) {
          console.log(
            "There was an error adjusting menu position on scroll. Error details: " +
              error,
          );
        }
      },

      /**
       * @inheritdoc
       */
      onClose: function () {
        this.el.style.removeProperty("--portal-editor-nav-height");

        //Call the superclass onClose() function
        EditorView.prototype.onClose.call(this);

        //Remove the Portal class from the body element
        $("body").removeClass("Portal");

        //Remove the scroll and resize listener
        $(window).off("scroll");
        $(window).off("resize");

        //Close and remove all of the subviews
        _.invoke(this.subviews, "onClose");
        _.invoke(this.subviews, "remove");
        //Reset the subviews array
        this.subviews = new Array();

        //Reset the sectionsView reference
        this.sectionsView = null;
      },
    },
  );

  return PortalEditorView;
});