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

define([
  "underscore",
  "jquery",
  "backbone",
  "models/metadata/eml211/EMLMethods",
  "models/metadata/eml/EMLMethodStep",
  "models/metadata/eml211/EMLText",
  "models/metadata/eml/EMLSpecializedText",
  "text!templates/metadata/EMLMethods.html",
], function (
  _,
  $,
  Backbone,
  EMLMethods,
  EMLMethodStep,
  EMLText,
  EMLSpecializedText,
  EMLMethodsTemplate,
) {
  /**
   * @class EMLMethodsView
   * @classdesc The EMLMethods renders the content of an EMLMethods model
   * @classcategory Views/Metadata
   * @extends Backbone.View
   */
  var EMLMethodsView = Backbone.View.extend(
    /** @lends EMLMethodsView.prototype */ {
      type: "EMLMethodsView",

      tagName: "div",

      className: "row-fluid eml-methods",

      stepsContainerSelector: "#eml-method-steps-container",

      editTemplate: _.template(EMLMethodsTemplate),

      /**
       * A small template to display each EMLMethodStep.
       * If you are going to extend this template for a theme, note that:
       * This template must keep the ".step-container" wrapper class.
       * This template must keep the textarea with the default data attributes.
       * The remove button must have a "remove" class
       * @type {UnderscoreTemplate}
       */
      stepTemplate: _.template(
        '<div class="step-container">\
                <h5>Step <span class="step-num"><%=num%></span></h5>\
                <p class="notification" data-attribute="methodStepDescription"></p>\
                <textarea data-attribute="methodStepDescription"\
                      data-step-attribute="description"\
                      rows="7" class="method-step"><%=text%></textarea>\
                <i class="remove icon-remove"></i>\
              </div>',
      ),

      /**
       * A reference to the EML211View that contains this EMLMethodsView.
       * @type {EML211View}
       */
      parentEMLView: null,

      /**
       * jQuery selector for the element that contains the Custom Methods
       * @type {string}
       */
      customMethodsSelector: ".custom-methods-container",

      initialize: function (options) {
        options = options || {};

        this.isNew = options.isNew || (options.model ? false : true);
        this.model = options.model || new EMLMethods();
        this.edit = options.edit || false;
        this.parentEMLView = options.parentEMLView || null;

        this.$el.data({ model: this.model });
      },

      events: {
        change: "updateModel",
        "keyup .method-step.new": "renderNewMethodStep",
        "click .remove": "removeMethodStep",
        "mouseover .remove": "previewRemove",
        "mouseout .remove": "previewRemove",
      },

      render: function () {
        //Save the view and model on the element
        this.$el
          .data({
            model: this.model,
            view: this,
          })
          .attr("data-category", "methods");

        if (this.edit) {
          this.$el.html(
            this.editTemplate({
              studyExtentDescription: this.model.get("studyExtentDescription"),
              samplingDescription: this.model.get("samplingDescription"),
            }),
          );

          //Render each EMLMethodStep
          let regularMethodSteps = this.model.getNonCustomSteps();
          regularMethodSteps.forEach((step) => {
            this.renderMethodStep(step);
          });

          //Create a blank step for the user to make a new one
          this.renderMethodStep();

          //Populate all the step numbers
          this.updateMethodStepNums();

          //Render the custom methods differently
          this.renderCustomMethods();
        }

        return this;
      },

      /**
       * Renders a single EMLMethodStep model
       * @param {EMLMethodStep} [step]
       * @since 2.19.0
       */
      renderMethodStep: function (step) {
        try {
          let stepEl;

          if (step) {
            //Render the step HTML
            stepEl = $(
              this.stepTemplate({
                text: step.get("description").toString(),
                num: "",
              }),
            );
            //Attach the model to the elements that will be interacted with
            stepEl
              .find("textarea[data-attribute='methodStepDescription'], .remove")
              .data({ methodStepModel: step });
          } else {
            //Only one new method step should be displayed at the same time
            if (this.$(".method-step.new").length) {
              return;
            }

            //Render the step HTML
            stepEl = $(
              this.stepTemplate({
                text: "",
                num: "",
              }),
            );

            stepEl
              .find("textarea[data-attribute='methodStepDescription']")
              .addClass("new");
          }

          //Add the step to the page
          this.$(this.stepsContainerSelector).append(stepEl);
        } catch (e) {
          console.error("Failed to render a method step: ", e);
        }
      },

      /**
       * Renders the inputs for the custom EML Methods that are configured in the {@link AppConfig}
       * If none are configured, nothing will be shown.
       * @since 2.19.0
       */
      renderCustomMethods: function () {
        //Get the custom EML Methods that are configured in the AppConfig
        let customMethodsOptions = MetacatUI.appModel.get("customEMLMethods");

        //If there is at least one custom Method configured, proceed with rendering it
        if (
          Array.isArray(customMethodsOptions) &&
          customMethodsOptions.length
        ) {
          let view = this;

          //Get the custom Methods template
          require([
            "text!templates/metadata/eml-custom-methods.html",
          ], function (CustomMethodsTemplate) {
            try {
              //Get the Methods from the EMLMethods model
              let allMethodSteps = view.model.get("methodSteps"),
                //Find the custom methods set on the model
                allCustomMethods = allMethodSteps.filter((step) => {
                  return step.isCustom();
                }),
                //Start a literal object to send to the custom methods template
                templateInfo = {};

              //Add each custom method model to the template info
              allCustomMethods.forEach((step) => {
                templateInfo[step.get("customMethodID")] = step;
              });

              //Insert the custom methods template into the page
              let customMethodsTemplate = _.template(CustomMethodsTemplate);
              view
                .$(view.customMethodsSelector)
                .html(customMethodsTemplate(templateInfo));

              //Attach each custom method model to it's textarea or input
              allCustomMethods.forEach((step) => {
                view
                  .$(view.customMethodsSelector)
                  .find(
                    "[data-custom-method-id='" +
                      step.get("customMethodID") +
                      "']",
                  )
                  .data({ methodStepModel: step });
              });

              //If this is inside a parent EML View (most likely), trigger the event
              //that lets the parent view know that new editor components have been added to the page.
              if (view.parentEMLView) {
                view.parentEMLView.trigger("editorInputsAdded");
              }
            } catch (e) {
              console.error("Couldn't show the custom EML Methods: ", e);
              return;
            }
          });
        }
      },

      updateModel: function (e) {
        if (!e) return false;

        var updatedInput = $(e.target);

        //Get the attribute that was changed
        var changedAttr = updatedInput.attr("data-attribute");
        if (!changedAttr) return false;

        // Method Step Descriptions are ordered arrays, so update them with special rules
        if (changedAttr == "methodStepDescription") {
          // Get the EMLMethodStep model
          var methodStep = updatedInput.data("methodStepModel");

          //If there is already an EMLMethodStep model created, then update it
          if (methodStep) {
            let desc = methodStep.get("description");
            desc.setText(updatedInput.val());
          } else {
            //Create a new EMLMethodStep model
            var newMethodStep = this.model.addMethodStep();

            //Attach the model to the elements that will be interacted with
            updatedInput
              .parents(".step-container")
              .find("textarea[data-attribute='methodStepDescription'], .remove")
              .data({ methodStepModel: newMethodStep });

            //Update the model with the textarea value
            newMethodStep.get("description").setText(updatedInput.val());
          }

          // Trigger the change event manually because, without this, the change event
          // never fires.
          this.model.trigger("change:methodSteps");
        }
        //All other attributes on this model are updated differently
        else {
          //Get the EMLText model to update
          var textModelToUpdate = this.model.get(changedAttr);

          //Double-check that this is an EMLText model, then update it
          if (
            textModelToUpdate &&
            typeof textModelToUpdate == "object" &&
            textModelToUpdate.type == "EMLText"
          ) {
            textModelToUpdate.setText(updatedInput.val());
          }
          //If there's no value set on this attribute yet, create a new EMLText model
          else if (!textModelToUpdate) {
            let textType;
            switch (changedAttr) {
              case "studyExtentDescription":
                textType = "description";
                break;
              case "samplingDescription":
                textType = "samplingdescription";
                break;
            }

            if (!textType) return;

            //Create a new EMLText model
            var newTextModel = new EMLText({
              type: textType,
              parentModel: this.model,
            });

            //Update the model with the textarea value
            newTextModel.setText(updatedInput.val());

            //Set the EMLText model on the EMLMethods model
            this.model.set(changedAttr, newTextModel);
          }
        }

        //Show the remove button
        $(e.target).parents(".step-container").find(".remove").show();
      },

      /**
       * Renders a new empty method step input. Does not update the model at all.
       */
      renderNewMethodStep: function () {
        // Add new textareas as needed
        this.$(".method-step.new").removeClass("new");

        this.renderMethodStep();

        this.updateMethodStepNums();
      },

      /**
       * Remove this method step
       * @param {Event} e
       */
      removeMethodStep: function (e) {
        try {
          //Get the EMLMethodStep
          var step = $(e.target).data("methodStepModel");

          //Exit if there is no EMLMethodStep
          if (!step) {
            return;
          }

          //Remove this step from the model
          this.model.removeMethodStep(step);

          //Remove the step elements from the page
          let view = this;
          $(e.target)
            .parent(".step-container")
            .slideUp("fast", function () {
              this.remove();

              //Bump down all the step numbers
              view.updateMethodStepNums();
            });
        } catch (e) {
          console.error("Failed to remove the EML Method Step: ", e);
        }
      },

      /**
       * Updates the step number in the view for each step
       * @since 2.19.0
       */
      updateMethodStepNums: function () {
        //Update all the step numbers
        this.$(".step-num").each((i, numEl) => {
          numEl.textContent = i + 1;
        });
      },

      /**
       * Shows validation errors that need to be fixed by the user
       */
      showValidation: function () {
        try {
          if (Object.keys(this.model.validationError).length) {
            if (this.model.validationError.methodSteps) {
              //A general error about all method steps will just be a string.
              //Apply the error styling to all the elements for the method steps
              if (typeof this.model.validationError.methodSteps == "string") {
                this.$('.notification[data-attribute="methodStepDescription"]')
                  .text(this.model.validationError.methodSteps)
                  .addClass("error");
                this.$(
                  '[data-attribute="methodStepDescription"]:not([data-custom-method-id])',
                ).addClass("error");
              }
              //Validation errors that aren't strings are errors about specific
              // Custom EML Method Steps.
              else {
                _.mapObject(
                  this.model.validationError.methodSteps,
                  (errors, customMethodID) => {
                    this.$(`.notification[data-category="${customMethodID}"]`)
                      .text(errors.description)
                      .addClass("error");
                    this.$(
                      `[data-custom-method-id="${customMethodID}"]`,
                    ).addClass("error");
                  },
                );
              }
            }
          }
        } catch (e) {
          console.warn("Failed to show Methods validation: ", e);
        }
      },

      previewRemove: function (e) {
        $(e.target).parents(".step-container").toggleClass("remove-preview");
      },
    },
  );

  return EMLMethodsView;
});