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

/* global define */
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;
});