Source: src/js/views/metadata/EML211EditorView.js

define([
  "underscore",
  "jquery",
  "backbone",
  "localforage",
  "collections/DataPackage",
  "models/metadata/eml211/EML211",
  "models/metadata/eml211/EMLOtherEntity",
  "models/metadata/ScienceMetadata",
  "views/EditorView",
  "views/CitationView",
  "views/DataPackageView",
  "views/metadata/EML211View",
  "views/metadata/EMLEntityView",
  "views/SignInView",
  "text!templates/editor.html",
  "collections/ObjectFormats",
  "text!templates/editorSubmitMessage.html",
], function (
  _,
  $,
  Backbone,
  LocalForage,
  DataPackage,
  EML,
  EMLOtherEntity,
  ScienceMetadata,
  EditorView,
  CitationView,
  DataPackageView,
  EMLView,
  EMLEntityView,
  SignInView,
  EditorTemplate,
  ObjectFormats,
  EditorSubmitMessageTemplate,
) {
  /**
   * @class EML211EditorView
   * @classdesc A view of a form for creating and editing EML 2.1.1 documents
   * @classcategory Views/Metadata
   * @name EML211EditorView
   * @extends EditorView
   * @constructs
   */
  var EML211EditorView = EditorView.extend(
    /** @lends EML211EditorView.prototype */ {
      type: "EML211Editor",

      /* The initial editor layout */
      template: _.template(EditorTemplate),
      editorSubmitMessageTemplate: _.template(EditorSubmitMessageTemplate),

      /**
       * The text to use in the editor submit button
       * @type {string}
       */
      submitButtonText: MetacatUI.appModel.get("editorSaveButtonText"),

      /**
       * 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, {
        change: "saveDraft",
        "click .data-package-item .edit": "showEntity",
      }),

      /**
        The identifier of the root package EML being rendered
        * @type {string}
        */
      pid: null,

      /**
       * A list of the subviews of the editor
       * @type {Backbone.Views[]}
       */
      subviews: [],

      /**
       * The data package view
       * @type {DataPackageView}
       */
      dataPackageView: null,

      /**
       * Initialize a new EML211EditorView - called post constructor
       */
      initialize: function (options) {
        // Ensure the object formats are cached for the editor's use
        if (typeof MetacatUI.objectFormats === "undefined") {
          MetacatUI.objectFormats = new ObjectFormats();
          MetacatUI.objectFormats.fetch();
        }
        return this;
      },

      /**
       * Create a new EML model for this view
       */
      createModel: function () {
        //If no pid is given, create a new EML model
        if (!this.pid) var model = new EML({ synced: true });
        //Otherwise create a generic metadata model until we find out the formatId
        else var model = new ScienceMetadata({ id: this.pid });

        // Once the ScienceMetadata is populated, populate the associated package
        this.model = model;

        //Listen for the replace event on this model
        var view = this;
        this.listenTo(this.model, "replace", function (newModel) {
          if (view.model.get("id") == newModel.get("id")) {
            view.model = newModel;
            view.setListeners();
          }
        });

        this.setListeners();
      },

      /**
       * Render the view
       */
      render: function () {
        var view = this;

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

        MetacatUI.appModel.set("headerType", "default");

        //Empty the view element first
        this.$el.empty();

        //Inert the basic template on the page
        this.$el.html(
          this.template({
            loading: MetacatUI.appView.loadingTemplate({
              msg: "Starting the editor...",
            }),
            submitButtonText: this.submitButtonText,
          }),
        );

        //If we don't have a model at this point, create one
        if (!this.model) this.createModel();

        // Before rendering the editor, we must:
        // 1. Make sure the user is signed in
        // 2. Fetch the metadata
        // 3. Use the metadata to identify and then fetch the resource map
        // 4. Make sure the user has write permission on the metadata
        // 5. Make sure the user has write permission on the resource map

        // As soon as we have all of the metadata information (STEP 2 complete)...
        this.listenToOnce(this.model, "sync", function () {
          // Skip the remaining steps the metadata doesn't exist.
          if (this.model.get("notFound") == true) {
            this.showNotFound();
            return;
          }

          // STEP 3
          // Listen for a trigger from the getDataPackage function that indicates
          // The data package (resource map) has been retrieved.
          this.listenToOnce(this, "dataPackageFound", function () {
            var resourceMap = MetacatUI.rootDataPackage.packageModel;

            // STEP 5
            // Once we have the resource map, then check that the user is authorized to edit this package.
            this.listenToOnce(
              resourceMap,
              "change:isAuthorized_write",
              function (model, authorization) {
                // Render if authorized (will show not authorized if not)
                this.renderEditorComponents();
              },
            );
            // No need to check authorization for a new resource map
            if (resourceMap.isNew()) {
              resourceMap.set("isAuthorized_write", true);
            } else {
              resourceMap.checkAuthority("write");
              this.updateLoadingText("Loading metadata...");
            }
          });

          this.getDataPackage();

          // STEP 4
          // Check the authority of this user to edit the metadata
          this.listenToOnce(
            this.model,
            "change:isAuthorized_write",
            function (model, authorization) {
              // Render if authorized (will show not authorized if not)
              this.renderEditorComponents();
            },
          );
          // If the model is new, no need to check for authorization.
          if (this.model.isNew()) {
            this.model.set("isAuthorized_write", true);
          } else {
            this.model.checkAuthority("write");
            this.updateLoadingText("Checking authorization...");
          }
        });

        // STEP 1
        // Check that the user is signed in
        var afterAccountChecked = function () {
          if (MetacatUI.appUserModel.get("loggedIn") == false) {
            // If they are not signed in, then show the sign-in view
            view.showSignIn();
          } else {
            // STEP 2
            // If signed in, then fetch model
            view.fetchModel();
          }
        };
        // If we've already checked the user account
        if (MetacatUI.appUserModel.get("checked")) {
          afterAccountChecked();
        }
        // If we haven't checked for authentication yet,
        // wait until the user info is loaded before we request the Metadata
        else {
          this.listenToOnce(
            MetacatUI.appUserModel,
            "change:checked",
            function () {
              afterAccountChecked();
            },
          );
        }

        // When the user mistakenly drops a file into an area in the window
        // that isn't a proper drop-target, prevent navigating away from the
        // page. Without this, the user will lose their progress in the
        // editor.
        window.addEventListener(
          "dragover",
          function (e) {
            e = e || event;
            e.preventDefault();
          },
          false,
        );

        window.addEventListener(
          "drop",
          function (e) {
            e = e || event;
            e.preventDefault();
          },
          false,
        );

        return this;
      },

      /**
       * Render the editor components (data package view and metadata view),
       * or, if not authorized, render the not authorized message.
       */
      renderEditorComponents: function () {
        if (!MetacatUI.rootDataPackage.packageModel) {
          return;
        }
        var resMapPermission =
            MetacatUI.rootDataPackage.packageModel.get("isAuthorized_write"),
          metadataPermission = this.model.get("isAuthorized_write");

        if (resMapPermission === true && metadataPermission === true) {
          var view = this;
          // Render the Data Package table.
          // This function will also render metadata.
          view.renderDataPackage();
        } else if (resMapPermission === false || metadataPermission === false) {
          this.notAuthorized();
        }
      },

      /**
       * Fetch the metadata model
       */
      fetchModel: function () {
        //If the user hasn't provided an id, then don't check the authority and mark as synced already
        if (!this.pid) {
          this.model.trigger("sync");
        } else {
          //Fetch the model
          this.model.fetch();
        }
      },

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

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

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

      /**
       * Update the text that is shown below the spinner while the editor is loading
       *
       * @param {string} message - The message to display
       */
      updateLoadingText: function (message) {
        try {
          if (!message || typeof message != "string") {
            console.log(
              "Was not able to update the loading message, left it as-is. A message must be provided to the updateLoadingText function",
            );
            return;
          }
          var loadingPara = this.$el.find(".loading > p");
          if (loadingPara) {
            loadingPara.text(message);
          }
        } catch (error) {
          console.log(
            "Was not able to update the loading message, left it as-is. Error details: " +
              error,
          );
        }
      },

      /**
       * Get the data package (resource map) associated with the EML. Save it to MetacatUI.rootDataPackage.
       * The metadata model must already be synced, and the user must be authorized to edit the EML before this function
       * can run.
       * @param {Model} scimetaModel - The science metadata model for which to find the associated data package
       */
      getDataPackage: function (scimetaModel) {
        if (!scimetaModel) var scimetaModel = this.model;

        // Check if this package is obsoleted
        if (this.model.get("obsoletedBy")) {
          this.showLatestVersion();
          return;
        }

        var resourceMapIds = scimetaModel.get("resourceMap");

        // Case 1: No resource map PID found in the metadata
        if (
          typeof resourceMapIds === "undefined" ||
          resourceMapIds === null ||
          resourceMapIds.length <= 0
        ) {
          // 1A: Check if the rootDataPackage contains the metadata document the user is trying to edit.
          // Ensure the resource map is not new. If it's a previously unsaved map, then getLatestVersion
          // will result in a 404.
          if (
            MetacatUI.rootDataPackage &&
            MetacatUI.rootDataPackage.pluck &&
            !MetacatUI.rootDataPackage.packageModel.isNew() &&
            _.contains(
              MetacatUI.rootDataPackage.pluck("id"),
              this.model.get("id"),
            )
          ) {
            // Remove the cached system metadata XML so we retrieve it again
            MetacatUI.rootDataPackage.packageModel.set("sysMetaXML", null);
            this.getLatestResourceMap();
          }

          // 1B. If the root data package does not contain the metadata the user is trying to edit,
          // then create a new data package.
          else {
            console.log(
              "Resource map ids could not be found for " +
                scimetaModel.id +
                ", creating a new resource map.",
            );

            // Create a new DataPackage collection for this view
            this.createDataPackage();
            this.trigger("dataPackageFound");
            // Set the listeners
            this.setListeners();
          }

          // Case 2: A resource map PID was found in the metadata
        } else {
          // Create a new data package with this id
          this.createRootDataPackage([this.model], { id: resourceMapIds[0] });

          //Handle the add of the metadata model
          MetacatUI.rootDataPackage.saveReference(this.model);

          // 2A. If there is more than one resource map, we need to make sure we fetch the most recent one
          if (resourceMapIds.length > 1) {
            this.getLatestResourceMap();

            // 2B. Just one resource map found
          } else {
            this.listenToOnce(MetacatUI.rootDataPackage, "sync", function () {
              this.trigger("dataPackageFound");
            });
            // Fetch the data package
            MetacatUI.rootDataPackage.fetch();
          }
        }
      },

      /**
       * Get the latest version of the resource map model stored in MetacatUI.rootDataPackage.packageModel.
       * When the newest resource map is synced, the "dataPackageFound" event will be triggered.
       */
      getLatestResourceMap: function () {
        try {
          if (
            !MetacatUI.rootDataPackage ||
            !MetacatUI.rootDataPackage.packageModel
          ) {
            console.log(
              "Could not get the latest verion of the resource map because no resource map is saved.",
            );
            return;
          }
          // Make sure we have the latest version of the resource map before we allow editing
          this.listenToOnce(
            MetacatUI.rootDataPackage.packageModel,
            "latestVersionFound",
            function (model) {
              //Create a new data package for the latest version package
              this.createRootDataPackage([this.model], {
                id: model.get("latestVersion"),
              });
              //Handle the add of the metadata model
              MetacatUI.rootDataPackage.saveReference(this.model);
              this.listenToOnce(MetacatUI.rootDataPackage, "sync", function () {
                this.trigger("dataPackageFound");
              });
              // Fetch the data package
              MetacatUI.rootDataPackage.fetch();
            },
          );

          //Find the latest version of the resource map
          MetacatUI.rootDataPackage.packageModel.findLatestVersion();
        } catch (error) {
          console.log(
            "Error attempting to find the latest version of the resource map. Error details: " +
              error,
          );
        }
      },

      /**
       * Creates a DataPackage collection for this EML211EditorView and sets it on the MetacatUI
       * global object (as `rootDataPackage`)
       */
      createDataPackage: function () {
        // Create a new Data packages
        this.createRootDataPackage([this.model], {
          packageModelAttrs: { synced: true },
        });

        try {
          //Inherit the access policy of the metadata document, if the metadata document is not `new`
          if (!this.model.isNew()) {
            let metadataAccPolicy = this.model.get("accessPolicy");
            let accPolicy =
              MetacatUI.rootDataPackage.packageModel.get("accessPolicy");

            //If there is no access policy, it hasn't been fetched yet, so wait
            if (!metadataAccPolicy.length) {
              //If the model is of ScienceMetadata class, we need to wait for the "replace" function,
              // which happens when the model is fetched and an EML211 model is created to replace it.
              if (this.model.type == "ScienceMetadata") {
                this.listenTo(this.model, "replace", function () {
                  this.listenToOnce(this.model, "sysMetaUpdated", function () {
                    accPolicy.copyAccessPolicy(this.model.get("accessPolicy"));
                    MetacatUI.rootDataPackage.packageModel.set(
                      "rightsHolder",
                      this.model.get("rightsHolder"),
                    );
                  });
                });
              }
            } else {
              accPolicy.copyAccessPolicy(this.model.get("accessPolicy"));
            }
          }
        } catch (e) {
          console.error(
            "Could not copy the access policy from the metadata to the resource map: ",
            e,
          );
        }

        //Handle the add of the metadata model
        MetacatUI.rootDataPackage.handleAdd(this.model);

        // Associate the science metadata with the resource map
        if (this.model.get && Array.isArray(this.model.get("resourceMap"))) {
          this.model
            .get("resourceMap")
            .push(MetacatUI.rootDataPackage.packageModel.id);
        } else {
          this.model.set(
            "resourceMap",
            MetacatUI.rootDataPackage.packageModel.id,
          );
        }

        // Set the sysMetaXML for the packageModel
        MetacatUI.rootDataPackage.packageModel.set(
          "sysMetaXML",
          MetacatUI.rootDataPackage.packageModel.serializeSysMeta(),
        );
      },

      /**
       * Creates a {@link DataPackage} collection for this Editor view, and saves it as the Root Data Package of the app.
       * This centralizes the DataPackage creation so listeners and other functionality is always performed
       * @param {(DataONEObject[]|ScienceMetadata[]|EML211[])} models - An array of models to add to the collection
       * @param {object} [attributes] A literal object of attributes to pass to the DataPackage.initialize() function
       * @since 2.17.1
       */
      createRootDataPackage: function (models, attributes) {
        MetacatUI.rootDataPackage = new DataPackage(models, attributes);

        this.listenTo(
          MetacatUI.rootDataPackage.packageModel,
          "change:numLoadingFiles",
          this.toggleEnableControls,
        );
      },

      renderChildren: function (model, options) {},

      /**
       * Render the Data Package View and insert it into this view
       */
      renderDataPackage: function () {
        var view = this;

        if (MetacatUI.rootDataPackage.packageModel.isNew()) {
          view.renderMember(this.model);
        }

        // As the root collection is updated with models, render the UI
        this.listenTo(MetacatUI.rootDataPackage, "add", function (model) {
          if (!model.get("synced") && model.get("id"))
            this.listenTo(model, "sync", view.renderMember);
          else if (model.get("synced")) view.renderMember(model);

          //Listen for changes on this member
          model.on("change:fileName", model.addToUploadQueue);
        });

        //Render the Data Package view
        this.dataPackageView = new DataPackageView({
          edit: true,
          dataPackage: MetacatUI.rootDataPackage,
          parentEditorView: this,
        });

        //Render the view
        var $packageTableContainer = this.$("#data-package-container");
        $packageTableContainer.html(this.dataPackageView.render().el);

        //Make the view resizable on the bottom
        var handle = $(document.createElement("div"))
          .addClass("ui-resizable-handle ui-resizable-s")
          .attr("title", "Drag to resize")
          .append(
            $(document.createElement("i")).addClass("icon icon-caret-down"),
          );
        $packageTableContainer.after(handle);
        $packageTableContainer.resizable({
          handles: { s: handle },
          minHeight: 100,
          maxHeight: 900,
          resize: function () {
            view.emlView.resizeTOC();
          },
        });

        var tableHeight = ($(window).height() - $("#Navbar").height()) * 0.4;
        $packageTableContainer.css("height", tableHeight + "px");

        var table = this.dataPackageView.$el;
        this.listenTo(this.dataPackageView, "addOne", function () {
          if (
            table.outerHeight() > $packageTableContainer.outerHeight() &&
            table.outerHeight() < 220
          ) {
            $packageTableContainer.css(
              "height",
              table.outerHeight() + handle.outerHeight(),
            );
            if (this.emlView) this.emlView.resizeTOC();
          }
        });

        if (this.emlView) this.emlView.resizeTOC();

        //Save the view as a subview
        this.subviews.push(this.dataPackageView);

        this.listenTo(
          MetacatUI.rootDataPackage.packageModel,
          "change:childPackages",
          this.renderChildren,
        );
      },

      /**
       * Calls the appropriate render method depending on the model type
       */
      renderMember: function (model, collection, options) {
        // Render metadata or package information, based on the type
        if (typeof model.attributes === "undefined") {
          return;
        } else {
          switch (model.get("type")) {
            case "DataPackage":
              // Do recursive rendering here for sub packages
              break;

            case "Metadata":
              // this.renderDataPackageItem(model, collection, options);
              this.renderMetadata(model, collection, options);
              break;

            case "Data":
              //this.renderDataPackageItem(model, collection, options);
              break;

            default:
              console.log("model.type is not set correctly");
          }
        }
      },

      /**
       * Renders the metadata section of the EML211EditorView
       */
      renderMetadata: function (model, collection, options) {
        if (!model && this.model) var model = this.model;
        if (!model) return;

        var emlView, dataPackageView;

        // render metadata as the collection is updated, but only EML passed from the event
        if (
          typeof model.get === "undefined" ||
          !(
            model.get("formatId") === "eml://ecoinformatics.org/eml-2.1.1" ||
            model.get("formatId") === "https://eml.ecoinformatics.org/eml-2.2.0"
          )
        ) {
          console.log("Not EML. TODO: Render generic ScienceMetadata.");
          return;
        }

        //Create an EML model
        if (model.type != "EML") {
          //Create a new EML model from the ScienceMetadata model
          var EMLmodel = new EML(model.toJSON());
          //Replace the old ScienceMetadata model in the collection
          MetacatUI.rootDataPackage.remove(model);
          MetacatUI.rootDataPackage.add(EMLmodel, { silent: true });
          MetacatUI.rootDataPackage.handleAdd(EMLmodel);
          model.trigger("replace", EMLmodel);

          //Fetch the EML and render it
          this.listenToOnce(EMLmodel, "sync", this.renderMetadata);
          EMLmodel.fetch();

          return;
        }

        //Create an EML211 View and render it
        emlView = new EMLView({
          model: model,
          edit: true,
        });
        this.subviews.push(emlView);
        this.emlView = emlView;
        emlView.render();

        //Show the required fields for this editor
        this.renderRequiredIcons(this.getRequiredFields());
        this.listenTo(emlView, "editorInputsAdded", function () {
          this.trigger("editorInputsAdded");
        });

        // Create a citation view and render it
        var citationView = new CitationView({
          model: model,
          defaultTitle: "Untitled dataset",
          createLink: false,
          createTitleLink: !model.isNew(),
        });

        this.subviews.push(citationView);
        $("#citation-container").html(citationView.render().$el);

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

        // Focus the folder name field once loaded but only if this is a new
        // document
        if (!this.pid) {
          $("#data-package-table-body td.name").focus();
        }
      },

      /**
       * Renders the data package section of the EML211EditorView
       */
      renderDataPackageItem: function (model, collection, options) {
        var hasPackageSubView = _.find(
          this.subviews,
          function (subview) {
            return subview.id === "data-package-table";
          },
          model,
        );

        // Only create the package table if it hasn't been created
        if (!hasPackageSubView) {
          this.dataPackageView = new DataPackageView({
            dataPackage: MetacatUI.rootDataPackage,
            edit: true,
            parentEditorView: this,
          });
          this.subviews.push(this.dataPackageView);
          dataPackageView.render();
        }
      },

      /**
       * Set listeners on the view's model for various reasons.
       * This function centralizes all the listeners so that when/if the view's model is replaced, the listeners would be reset.
       */
      setListeners: function () {
        this.listenTo(this.model, "change:uploadStatus", this.showControls);

        // Register a listener for any attribute change
        this.model.on("change", this.model.handleChange, this.model);

        // Register a listener to save drafts on change
        this.model.on("change", this.model.saveDraft, this.model);

        // If any attributes have changed (including nested objects), show the controls
        if (typeof MetacatUI.rootDataPackage.packageModel !== "undefined") {
          this.stopListening(
            MetacatUI.rootDataPackage.packageModel,
            "change:changed",
          );
          this.listenTo(
            MetacatUI.rootDataPackage.packageModel,
            "change:changed",
            this.toggleControls,
          );
          this.listenTo(
            MetacatUI.rootDataPackage.packageModel,
            "change:changed",
            function (event) {
              if (MetacatUI.rootDataPackage.packageModel.get("changed")) {
                // Put this metadata model in the queue when the package has been changed
                // Don't put it in the queue if it's in the process of saving already
                if (this.model.get("uploadStatus") != "p")
                  this.model.set("uploadStatus", "q");
              }
            },
          );
        }

        if (
          MetacatUI.rootDataPackage &&
          DataPackage.prototype.isPrototypeOf(MetacatUI.rootDataPackage)
        ) {
          // If the Data Package failed saving, display an error message
          this.listenTo(
            MetacatUI.rootDataPackage,
            "errorSaving",
            this.saveError,
          );

          // Listen for when the package has been successfully saved
          this.listenTo(
            MetacatUI.rootDataPackage,
            "successSaving",
            this.saveSuccess,
          );

          //When the Data Package cancels saving, hide the saving styling
          this.listenTo(
            MetacatUI.rootDataPackage,
            "cancelSave",
            this.hideSaving,
          );
          this.listenTo(
            MetacatUI.rootDataPackage,
            "cancelSave",
            this.handleSaveCancel,
          );
        }

        //When the model is invalid, show the required fields
        this.listenTo(this.model, "invalid", this.showValidation);
        this.listenTo(this.model, "valid", this.showValidation);

        // When a data package member fails to load, remove it and warn the user
        this.listenTo(
          MetacatUI.eventDispatcher,
          "fileLoadError",
          this.handleFileLoadError,
        );

        // When a data package member fails to be read, remove it and warn the user
        this.listenTo(
          MetacatUI.eventDispatcher,
          "fileReadError",
          this.handleFileReadError,
        );

        //Set a beforeunload event only if there isn't one already
        if (!this.beforeunloadCallback) {
          var view = this;
          //When the Window is about to be closed, show a confirmation message
          this.beforeunloadCallback = function (e) {
            if (!view.canClose()) {
              //Browsers don't support custom confirmation messages anymore,
              // so preventDefault() needs to be called or the return value has to be set
              e.preventDefault();
              e.returnValue = "";
            }
            return;
          };
          window.addEventListener("beforeunload", this.beforeunloadCallback);
        }
      },

      /**
       * Saves all edits in the collection
       * @param {Event} e - The DOM Event that triggerd this function
       */
      save: function (e) {
        var btn = e && e.target ? $(e.target) : this.$("#save-editor");

        //If the save button is disabled, then we don't want to save right now
        if (btn.is(".btn-disabled")) return;

        this.showSaving();

        //Save the package!
        MetacatUI.rootDataPackage.save();
      },

      /**
       * When the data package collection saves successfully, tell the user
       * @param {DataPackage|DataONEObject} savedObject - The model or collection that was just saved
       */
      saveSuccess: function (savedObject) {
        //We only want to perform these actions after the package saves
        if (savedObject.type != "DataPackage") return;

        //Change the URL to the new id
        MetacatUI.uiRouter.navigate(
          "submit/" + encodeURIComponent(this.model.get("id")),
          { trigger: false, replace: true },
        );

        this.toggleControls();

        // Construct the save message
        var message = this.editorSubmitMessageTemplate({
          messageText: "Your changes have been submitted.",
          viewURL:
            MetacatUI.root +
            "/view/" +
            encodeURIComponent(this.model.get("id")),
          buttonText: "View your dataset",
        });

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

        //Rerender the CitationView
        var citationView = _.where(this.subviews, { type: "Citation" });
        if (citationView.length) {
          citationView[0].createTitleLink = true;
          citationView[0].render();
        }

        // Reset the state to clean
        MetacatUI.rootDataPackage.packageModel.set("changed", false);
        this.model.set("hasContentChanges", false);

        this.setListeners();
      },

      /**
       * When the data package collection fails to save, tell the user
       * @param {string} errorMsg - The error message from the failed save() function
       */
      saveError: function (errorMsg) {
        var errorId = "error" + Math.round(Math.random() * 100),
          messageContainer = $(document.createElement("div")).append(
            document.createElement("p"),
          ),
          messageParagraph = messageContainer.find("p"),
          messageClasses = "alert-error";

        //Get all the models that have an error
        var failedModels = MetacatUI.rootDataPackage.where({
          uploadStatus: "e",
        });

        //If every failed model is a DataONEObject data file that failed
        // because of a slow network, construct a specific error message that
        // is more informative than the usual message
        if (
          failedModels.length &&
          _.every(failedModels, function (m) {
            return (
              m.get("type") == "Data" &&
              m.get("errorMessage").indexOf("network issue") > -1
            );
          })
        ) {
          //Create a list of file names for the files that failed to upload
          var failedFileList = $(document.createElement("ul"));

          _.each(
            failedModels,
            function (failedModel) {
              failedFileList.append(
                $(document.createElement("li")).text(
                  failedModel.get("fileName"),
                ),
              );
            },
            this,
          );

          //Make the error message
          messageParagraph.text(
            "The following files could not be uploaded due to a network issue. Make sure you are connected to a reliable internet connection. ",
          );
          messageParagraph.after(failedFileList);
        }
        //If one of the failed models is this package's metadata model or the
        // resource map model and it failed to upload due to a network issue,
        // show a more specific error message
        else if (
          _.find(
            failedModels,
            function (m) {
              var errorMsg = m.get("errorMessage") || "";
              return m == this.model && errorMsg.indexOf("network issue") > -1;
            },
            this,
          ) ||
          (MetacatUI.rootDataPackage.packageModel.get("uploadStatus") == "e" &&
            MetacatUI.rootDataPackage.packageModel
              .get("errorMessage")
              .indexOf("network issue") > -1)
        ) {
          messageParagraph.text(
            "Your changes could not be submitted due to a network issue. Make sure you are connected to a reliable internet connection. ",
          );
        } else {
          if (
            this.model.get("draftSaved") &&
            MetacatUI.appModel.get("editorSaveErrorMsgWithDraft")
          ) {
            messageParagraph.text(
              MetacatUI.appModel.get("editorSaveErrorMsgWithDraft"),
            );
            messageClasses = "alert-warning";
          } else if (MetacatUI.appModel.get("editorSaveErrorMsg")) {
            messageParagraph.text(MetacatUI.appModel.get("editorSaveErrorMsg"));
            messageClasses = "alert-error";
          } else {
            messageParagraph.text(
              "Not all of your changes could be submitted.",
            );
            messageClasses = "alert-error";
          }

          messageParagraph.after(
            $(document.createElement("p")).append(
              $(document.createElement("a"))
                .text("See technical details")
                .attr("data-toggle", "collapse")
                .attr("data-target", "#" + errorId)
                .addClass("pointer"),
            ),
            $(document.createElement("div"))
              .addClass("collapse")
              .attr("id", errorId)
              .append($(document.createElement("pre")).text(errorMsg)),
          );
        }

        MetacatUI.appView.showAlert(
          messageContainer,
          messageClasses,
          this.$el,
          null,
          {
            emailBody: "Error message: Data Package save error: " + errorMsg,
            remove: true,
          },
        );

        //Reset the Saving styling
        this.hideSaving();
      },

      /**
       * Find the most recently updated version of the metadata
       */
      showLatestVersion: function () {
        var view = this;

        //When the latest version is found,
        this.listenToOnce(this.model, "change:latestVersion", function () {
          //Make sure it has a newer version, and if so,
          if (view.model.get("latestVersion") != view.model.get("id")) {
            //Get the obsoleted id
            var oldID = view.model.get("id");

            //Reset the current model
            view.pid = view.model.get("latestVersion");
            view.model = null;

            //Update the URL
            MetacatUI.uiRouter.navigate(
              "submit/" + encodeURIComponent(view.pid),
              { trigger: false, replace: true },
            );

            //Render the new model
            view.render();

            //Show a warning that the user was trying to edit old content
            MetacatUI.appView.showAlert(
              "You've been forwarded to the newest version of your dataset for editing.",
              "alert-warning",
              this.$el,
              12000,
              { remove: true },
            );
          } else {
            view.getDataPackage();
          }
        });

        //Find the latest version of this metadata object
        this.model.findLatestVersion();
      },

      /**
       * Show the entity editor
       * @param {Event} e - The DOM Event that triggerd this function
       */
      showEntity: function (e) {
        if (!e || !e.target) return;

        //For EML metadata docs
        if (this.model.type == "EML") {
          //Get the Entity View
          var row = $(e.target).parents(".data-package-item"),
            entityView = row.data("entityView"),
            dataONEObject = row.data("model");

          if (
            dataONEObject.get("uploadStatus") == "p" ||
            dataONEObject.get("uploadStatus") == "l" ||
            dataONEObject.get("uploadStatus") == "e"
          )
            return;

          //If there isn't a view yet, create one
          if (!entityView) {
            //Get the entity model for this data package item
            var entityModel = this.model.getEntity(row.data("model"));

            //Create a new EMLOtherEntity if it doesn't exist
            if (!entityModel) {
              entityModel = new EMLOtherEntity({
                entityName: dataONEObject.get("fileName"),
                entityType:
                  dataONEObject.get("formatId") ||
                  dataONEObject.get("mediaType"),
                parentModel: this.model,
                xmlID: dataONEObject.getXMLSafeID(),
              });

              if (!dataONEObject.get("fileName")) {
                //Listen to changes to required fields on the otherEntity models
                this.listenTo(entityModel, "change:entityName", function () {
                  if (!entityModel.isValid()) return;

                  //Get the position this entity will be in
                  var position = $(".data-package-item.data").index(row);

                  this.model.addEntity(entityModel, position);
                });
              } else {
                //Get the position this entity will be in
                var position = $(".data-package-item.data").index(row);

                this.model.addEntity(entityModel, position);
              }
            } else {
              entityView = new EMLEntityView({
                model: entityModel,
                DataONEObject: dataONEObject,
                edit: true,
              });
            }

            //Attach the view to the edit button so we can access it again
            row.data("entityView", entityView);

            //Render the view
            entityView.render();
          }

          //Show the modal window editor for this entity
          if (entityView) entityView.show();
        }
      },

      /**
       * Shows a message if the user is not authorized to edit this package
       */
      notAuthorized: function () {
        // Don't show the not authorized message if the user is authorized to edit the EML and the resource map
        if (
          MetacatUI.rootDataPackage &&
          MetacatUI.rootDataPackage.packageModel
        ) {
          if (
            MetacatUI.rootDataPackage.packageModel.get(
              "isAuthorized_changePermission",
            ) &&
            this.model.get("isAuthorized")
          ) {
            return;
          }
        } else {
          if (this.model.get("isAuthorized")) {
            return;
          }
        }

        this.$("#editor-body").empty();
        MetacatUI.appView.showAlert(
          "You are not authorized to edit this data set.",
          "alert-error",
          "#editor-body",
        );

        //Stop listening to any further events
        this.stopListening();
        this.model.off();
      },

      /**
       * Toggle the editor footer controls (Save bar)
       */
      toggleControls: function () {
        if (
          MetacatUI.rootDataPackage &&
          MetacatUI.rootDataPackage.packageModel &&
          MetacatUI.rootDataPackage.packageModel.get("changed")
        ) {
          this.showControls();
        } else {
          this.hideControls();
        }
      },

      /**
       * Toggles whether the Save controls for the Editor are enabled or disabled based on various attributes of the DataPackage and its models.
       * @since 2.17.1
       */
      toggleEnableControls: function () {
        if (MetacatUI.rootDataPackage.packageModel.get("isLoadingFiles")) {
          let noun =
            MetacatUI.rootDataPackage.packageModel.get("numLoadingFiles") > 1
              ? " files"
              : " file";
          this.disableControls(
            "Waiting for " +
              MetacatUI.rootDataPackage.packageModel.get("numLoadingFiles") +
              noun +
              " to upload...",
          );
        } else {
          this.enableControls();
        }
      },

      /**
       * Show any errors that occured when trying to save changes
       */
      showValidation: function () {
        //First clear all the error messaging
        this.$(".notification.error").empty();
        this.$(".side-nav-item .icon").hide();
        this.$("#metadata-container .error").removeClass("error");
        $(".alert-container:not(:has(.temporary-message))").remove();

        var errors = this.model.validationError;

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

            //If this field is in a DataItemView, then delegate to that view
            if (dataItemRow.length && dataItemRow.data("view")) {
              dataItemRow.data("view").showValidation(category, errorMsg);
              return;
            } else {
              var elsWithViews = _.filter(categoryEls, function (el) {
                return (
                  $(el).data("view") &&
                  $(el).data("view").showValidation &&
                  !$(el).data("view").isNew
                );
              });

              if (elsWithViews.length) {
                _.each(elsWithViews, function (el) {
                  $(el).data("view").showValidation();
                });
              } else if (categoryEls.length) {
                //Show the error message
                categoryEls
                  .filter(".notification")
                  .addClass("error")
                  .text(errorMsg);

                //Add the error message to inputs
                categoryEls.filter("textarea, input").addClass("error");
              }
            }

            //Get the link in the table of contents navigation
            var navigationLink = this.$(
              ".side-nav-item[data-category='" + category + "']",
            );

            if (!navigationLink.length) {
              var section = categoryEls.parents("[data-section]");
              navigationLink = this.$(
                ".side-nav-item." + $(section).attr("data-section"),
              );
            }

            //Show the error icon in the table of contents
            navigationLink
              .addClass("error")
              .find(".icon")
              .addClass("error")
              .show();

            this.model.off("change:" + category, this.model.checkValidity);
            this.model.once("change:" + category, this.model.checkValidity);
          },
          this,
        );

        if (errors) {
          //Create a list of errors to display in the error message shown to the user
          let errorList = "<ul>" + this.getErrorListItem(errors) + "</ul>";

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

      /**
       * @inheritdoc
       */
      hasUnsavedChanges: function () {
        //If the form hasn't been edited, we can close this view without confirmation
        if (
          typeof MetacatUI.rootDataPackage.getQueue != "function" ||
          MetacatUI.rootDataPackage.getQueue().length
        )
          return true;
        else return false;
      },

      /**
       *  @inheritdoc
       */
      onClose: function () {
        //Execute the parent class onClose() function
        //EditorView.prototype.onClose.call(this);

        //Remove the listener on the Window
        if (this.beforeunloadCallback) {
          window.removeEventListener("beforeunload", this.beforeunloadCallback);
          delete this.beforeunloadCallback;
        }

        //Stop listening to the "add" event so that new package members aren't rendered.
        //Check first if the DataPackage has been intialized. An easy check is to see is
        // the 'models' attribute is undefined. If the DataPackage collection has been intialized,
        // then it would be an empty array.
        if (typeof MetacatUI.rootDataPackage.models !== "undefined") {
          this.stopListening(MetacatUI.rootDataPackage, "add");
        }

        //Remove all the other events
        this.off(); // remove callbacks, prevent zombies
        this.model.off();

        $(".Editor").removeClass("Editor");
        this.$el.empty();

        this.model = null;

        // Close each subview
        _.each(this.subviews, function (subview) {
          if (subview.onClose) subview.onClose();
        });

        this.subviews = [];

        this.undelegateEvents();
      },

      /**
       *  Handle "fileLoadError" events by alerting the user
       * and removing the row from the data package table.
       * @param  {DataONEObject} item The model item passed by the fileLoadError event
       */
      handleFileLoadError: function (item) {
        var message;
        var fileName;
        /* Remove the data package table row */
        this.dataPackageView.removeOne(item);
        /* Then inform the user */
        if (
          item &&
          item.get &&
          (item.get("fileName") !== "undefined" ||
            item.get("fileName") !== null)
        ) {
          fileName = item.get("fileName");
          message =
            "The file " +
            fileName +
            " is already included in this dataset. The duplicate file has not been added.";
        } else {
          message =
            "The chosen file is already included in this dataset. " +
            "The duplicate file has not been added.";
        }
        MetacatUI.appView.showAlert(message, "alert-info", this.el, 10000, {
          remove: true,
        });
      },

      /**
       * Handle "fileReadError" events by alerting the user
       * and removing the row from the data package table.
       * @param  {DataONEObject} item The model item passed by the fileReadError event
       */
      handleFileReadError: function (item) {
        var message;
        var fileName;
        /* Remove the data package table row */
        this.dataPackageView.removeOne(item);
        /* Then inform the user */
        if (
          item &&
          item.get &&
          (item.get("fileName") !== "undefined" ||
            item.get("fileName") !== null)
        ) {
          fileName = item.get("fileName");
          message =
            "The file " +
            fileName +
            " could not be read. You may not have permission to read the file," +
            " or the file was too large for your browser to upload. " +
            "The file has not been added.";
        } else {
          message =
            "The chosen file " +
            " could not be read. You may not have permission to read the file," +
            " or the file was too large for your browser to upload. " +
            "The file has not been added.";
        }
        MetacatUI.appView.showAlert(message, "alert-info", this.el, 10000, {
          remove: true,
        });
      },

      /**
       * Save a draft of the parent EML model
       */
      saveDraft: function () {
        var view = this;

        try {
          var title = this.model.get("title") || "No title";
          // Create a clone of the model that we will use for serialization.
          // Don't serialize the model that is currently being edited,
          // since serialize may make changes to the model that should not
          // happen until the user is ready to save
          // (e.g. - create a contact if there is not one)
          var draftModel = this.model.clone();

          LocalForage.setItem(this.model.get("id"), {
            id: this.model.get("id"),
            datetime: new Date().toISOString(),
            title: Array.isArray(title) ? title[0] : title,
            draft: draftModel.serialize(),
          }).then(function () {
            view.clearOldDrafts();
          });
        } catch (ex) {
          console.log("Error saving draft:", ex);
        }
      },

      /**
       * Clear older drafts by iterating over the sorted list of drafts
       * stored by LocalForage and removing any beyond a hardcoded limit.
       */
      clearOldDrafts: function () {
        var drafts = [];

        try {
          LocalForage.iterate(function (value, key, iterationNumber) {
            // Extract each draft
            drafts.push({
              key: key,
              value: value,
            });
          })
            .then(function () {
              // Sort by datetime
              drafts = _.sortBy(drafts, function (draft) {
                return draft.value.datetime.toString();
              }).reverse();
            })
            .then(function () {
              _.each(drafts, function (draft, i) {
                var age = new Date() - new Date(draft.value.datetime);
                var isOld = age / 2678400000 > 1; // ~31days

                // Delete this draft is not in the most recent 100 or
                // if older than 31 days
                var shouldDelete = i > 100 || isOld;

                if (!shouldDelete) {
                  return;
                }

                LocalForage.removeItem(draft.key).then(function () {
                  // Item should be removed
                });
              });
            });
        } catch (ex) {
          console.log("Failed to clear old drafts: ", ex);
        }
      },

      /**
       * Show the AccessPolicy view in a modal dialog
       *
       * This method calls the superclass method, feeding it the identifier
       * associated with the row in the package table that was clicked. The
       * reason for this is so the AccessPolicyView can be used for single
       * objects (like in the Portal editor) or an entire Collection of
       * objects, like in the EML editor: The superclass impelements the
       * generic behavior and the subclass tweaks it.
       *
       * @param {EventHandler} e: The click event
       */
      showAccessPolicyModal: function (e) {
        var id = null;

        try {
          id = $(e.target).parents("tr").data("id");
        } catch (e) {
          console.log(
            "Error determining the identifier to show an AccessPolicyView for:",
            e,
          );
        }

        var model = MetacatUI.rootDataPackage.find(function (model) {
          return model.get("id") === id;
        });

        EditorView.prototype.showAccessPolicyModal.call(this, e, model);
      },

      /**
       * Gets the EML required fields, as configured in the {@link AppConfig#emlEditorRequiredFields}, and adds
       * possible other special fields that may be configured elsewhere. (e.g. the {@link AppConfig#customEMLMethods})
       * @extends EditorView.getRequiredFields
       */
      getRequiredFields: function () {
        let requiredFields = _.clone(
          MetacatUI.appModel.get("emlEditorRequiredFields"),
        );

        //Add required fields for Custom Methods, which are configured in a different property of the AppConfig
        let customMethodOptions = MetacatUI.appModel.get("customEMLMethods");
        if (customMethodOptions) {
          customMethodOptions.forEach((options) => {
            if (options.required && !requiredFields[options.id]) {
              requiredFields[options.id] = true;
            }
          });
        }

        return requiredFields;
      },
    },
  );
  return EML211EditorView;
});