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

              // 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 the user's quota to create a new 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 {
                  //If the user doesn't have quota left, display this message
                  if (MetacatUI.appUserModel.get("portalQuota") == 0) {
                    var errorMessage = MetacatUI.appModel.get(
                      "portalEditNoQuotaMessage",
                    );
                  }
                  //Otherwise, display a more generic error message
                  else {
                    var 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;

        //Check if this is a plus portal
        if (MetacatUI.appModel.get("dataonePlusPreviewMode")) {
          var sourceMN = this.model.get("datasource");

          //Check if the portal source node is from the active alt repo OR is
          // configured as a Plus portal.
          if (
            !this.model.isNew() &&
            (typeof sourceMN != "string" ||
              (sourceMN !=
                MetacatUI.appModel.get("defaultAlternateRepositoryId") &&
                !_.findWhere(
                  MetacatUI.appModel.get("dataonePlusPreviewPortals"),
                  {
                    datasource: sourceMN,
                    seriesId: this.model.get("seriesId"),
                  },
                )))
          ) {
            //Get the name of the source member node
            var sourceMNName = "original data repository",
              mnURL = "";
            if (typeof sourceMN == "string") {
              var sourceMNObject = MetacatUI.nodeModel.getMember(sourceMN);
              if (sourceMNObject) {
                sourceMNName = sourceMNObject.name;

                //If there is a baseURL string
                if (sourceMNObject.baseURL) {
                  //Parse out the origin of the baseURL string. We want to crop out the /metacat/d1/mn parts.
                  mnURL =
                    sourceMNObject.baseURL.substring(
                      0,
                      sourceMNObject.baseURL.lastIndexOf("."),
                    ) +
                    sourceMNObject.baseURL.substring(
                      sourceMNObject.baseURL.lastIndexOf("."),
                      sourceMNObject.baseURL.indexOf(
                        "/",
                        sourceMNObject.baseURL.lastIndexOf("."),
                      ),
                    );
                }
              }
            }

            //Show a message that the portal can be found on the repository website.
            var message = $(document.createElement("h3")).addClass(
              "center stripe",
            );
            message.text(
              "The " +
                this.model.get("name") +
                " " +
                MetacatUI.appModel.get("portalTermSingular") +
                " can be edited in the ",
            );

            if (mnURL) {
              message.append(
                $(document.createElement("a"))
                  .attr("href", mnURL)
                  .attr("target", "_blank")
                  .text(sourceMNName),
              );
            } else {
              message.append(sourceMNName);
            }

            this.$el.html(message);

            return;
          }
        }

        // 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, 400));

        // Functions to perform when the window is resized
        var onResize = function () {
          // 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();

        //If this portal is a free trial DataONE Plus portal, then display some messaging
        this.renderSubscriptionInfo();

        //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();
        }
      },

      /**
       * 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: "Drag & drop a logo or click to upload",
            imageWidth: 100,
            imageHeight: 100,
            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.",
          }),
        );
      },

      /**
       * The DataONE Plus Subscription if fetched from Bookkeeper and the status of the
       * Subscription is rendered on the page.
       * Subviews in this view should have their own renderSubscriptionInfo() function
       * that inserts subscription details into the subview.
       */
      renderSubscriptionInfo: function () {
        if (MetacatUI.appModel.get("enableBookkeeperServices")) {
          if (
            MetacatUI.appUserModel.get("loggedIn") &&
            MetacatUI.appUserModel.get("dataoneSubscription")
          ) {
            //Show the free trial message for this portal, if the subscription is in a free trial
            var subscription = MetacatUI.appUserModel.get(
                "dataoneSubscription",
              ),
              isFreeTrial = false;

            //If the Subscription is in free trial mode
            if (subscription && subscription.isTrialing()) {
              if (MetacatUI.appModel.get("dataonePlusPreviewMode")) {
                //If this portal is not in the configured list of Plus portals
                var trialExceptions = MetacatUI.appModel.get(
                  "dataonePlusPreviewPortals",
                );
                isFreeTrial = !_.findWhere(trialExceptions, {
                  seriesId: this.model.get("seriesId"),
                });
              } else {
                isFreeTrial = true;
              }

              if (isFreeTrial) {
                //Show a free trial message in the editor footer
                var freeTrialMessage =
                  "This " +
                  MetacatUI.appModel.get("portalTermSingular") +
                  " is a free preview of " +
                  MetacatUI.appModel.get("dataonePlusName");
                var messageEl = $(document.createElement("span"))
                  .addClass("free-trial-message")
                  .text(freeTrialMessage)
                  .prepend(
                    $(document.createElement("i")).addClass(
                      "dataone-plus-icon-container",
                    ),
                  );
                this.$("#editor-footer").prepend(messageEl);

                // Update the label element to randomly generated label
                // And disable the input
                var labelEL = $(".label-input-text");
                labelEL.val(this.model.get("label"));

                //When the Portal Model label is changed, update the input
                this.listenTo(this.model, "change:label", function () {
                  $(".label-input-text").val(this.model.get("label"));
                });

                labelEL.attr("disabled", "disabled");
                //Remove the "Change URL" button that toggles the label input
                this.$(".btn.change-label").remove();

                // Show edit label message if the edit button is disabled
                var editLabelMessage =
                  "Create a custom " +
                  MetacatUI.appModel.get("portalTermSingular") +
                  " name for the URL when your free preview of " +
                  "<i class='dataone-plus-icon-container'></i>" +
                  MetacatUI.appModel.get("dataonePlusName") +
                  " ends.";
                var messageContainer = this.$(".label-container .notification")
                  .html(editLabelMessage)
                  .addClass("free-trial");

                if (!messageContainer.is(":visible")) {
                  messageContainer
                    .detach()
                    .appendTo(this.$(".change-label-container"));
                }

                //Insert the DataONE Plus icon
                var viewRef = this;
                require(["text!templates/dataonePlusIcon.html"], function (
                  iconTemplate,
                ) {
                  viewRef.$(".dataone-plus-icon-container").html(iconTemplate);
                });
              }
            }
          } else {
            this.listenTo(
              MetacatUI.appUserModel,
              "change:dataoneSubscription",
              this.renderSubscriptionInfo,
            );
          }
        }
      },

      /**
       * @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 },
        );
      },

      /**
       * This function is called whenever the window is scrolled.
       */
      handleScroll: function () {
        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 () {
        //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;
});