Source: src/js/models/metadata/eml/EMLMethodStep.js

/* global define */
var required = ['jquery',
    'underscore',
    'backbone',
    'models/DataONEObject',
    'models/metadata/eml220/EMLText']

if( MetacatUI.appModel.get("customEMLMethods").length ){
  required.push("models/metadata/eml/EMLSpecializedText");
}

define(required,
    function($, _, Backbone, DataONEObject, EMLText, EMLSpecializedText) {

  /**
  * @class EMLMethodStep
  * @classdesc Represents the EML Method Steps. The methodStep field allows for repeated sets of
            elements that document a series of procedures followed to produce a
            data object. These include text descriptions of the procedures,
            relevant literature, software, instrumentation, source data and any
            quality control measures taken.
  * @classcategory Models/Metadata/EML
  * @extends Backbone.Model
  * @since 2.19.0
  */
  var EMLMethodStep = Backbone.Model.extend(
    /** @lends EMLMethodStep.prototype */{

    /**
    * Default attributes for EMLMethodSteps
    * @returns {object}
    * @property {string} objectXML The original XML snippet string from the EML XML
    * @property {Element} objectDOM The original XML snippet as an Element
    * @property {EMLText|EMLSpecializedText} description A textual description of this method step
    * @property {string[]} instrumentation One or more instruments used for measurement and recording data
    * @property {EMLMethodStep[]} subStep Nested additional method steps within this step.  This is useful for hierarchical method descriptions. This is *not* fully supported in MetacatUI yet
    * @property {string[]} customMethodID A unique identifier for this Custom Method Step type, which is defined in {@link AppConfig#customEMLMethods}
    * @property {boolean} required If true, this method step is required in it's parent EML
    */
    defaults: function(){
      return {
        objectXML: null,
        objectDOM: null,
        description: null,
        instrumentation: [],
        subStep: [],
        customMethodID: "",
        required: false
      }
    },

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

      if(attributes.objectDOM){
        this.set(this.parse(attributes.objectDOM));
      }
      else if(attributes.customMethodID){

        try{
          let customMethodConfig = MetacatUI.appModel.get("customEMLMethods").find(config => config.id == attributes.customMethodID);

          this.set("description", new EMLSpecializedText({
            type: "description",
            title: customMethodConfig.titleOptions[0],
            titleOptions: customMethodConfig.titleOptions
          }));
        }
        catch(e){
          console.error(e);
        }
      }
      else{
        this.set("description", new EMLText({
          type: "description"
        }))
      }

      //Set the required attribute
      if( typeof attributes.required == "boolean" ){
        this.set("required", attributes.required);
      }

      //specific attributes to listen to
      this.on("change:instrumentation", this.trickleUpChange);

    },

    /**
     * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
     * Used during parse() and serialize()
     * @returns {object}
     */
    nodeNameMap: function(){
      return {
        "alternateidentifier" : "alternateIdentifier",
        "methodstep" : "methodStep",
        "substep" : "subStep",
        "datasource" : "dataSource",
        "referencedentityid" : "referencedEntityId",
        "qualitycontrol" : "qualityControl",
        "shortname" : "shortName"
        }
    },

    parse: function(objectDOM) {
      var modelJSON = {};

      if (!objectDOM) var objectDOM = this.get("objectDOM");

      let $objectDOM = $(objectDOM),
          description = $objectDOM.children("description");

      //Get the titles of all the custom method steps from the App Config
      let customMethodOptions = MetacatUI.appModel.get("customEMLMethods"),
          customMethodTitles  = _.flatten(_.pluck(customMethodOptions, "titleOptions")),
          isCustom = false;

      try{
        //If there is at least one custom method configured, check if this description is one
        if( customMethodOptions && customMethodOptions.length ){
          let specializedTextAttr  = EMLSpecializedText.prototype.parse(description[0]),
              matchingCustomMethod = customMethodOptions.find(options => options.titleOptions.includes(specializedTextAttr.title));

          if( matchingCustomMethod ){
            isCustom = true;

            //Use the EMLSpecializedText model for custom methods
            modelJSON.description = new EMLSpecializedText({
              objectDOM: description[0],
              type: "description",
              titleOptions: matchingCustomMethod.titleOptions,
              parentModel: this
             });
            //Save the other configurations of this custom method to this EMLMethodStep
            modelJSON.customMethodID = matchingCustomMethod.id;
            modelJSON.required = matchingCustomMethod.required;
          }
        }
      }
      catch(e){
        console.error(e);
      }

      //Create a regular EMLText description for non-custom methods
      if( !isCustom ){
        modelJSON.description = new EMLText({ objectDOM: description[0], type: "description", parentModel: this });
      }

      //Parse the instrumentation
      modelJSON.instrumentation = [];
      $objectDOM.children("instrumentation").each((i, el) => {
        modelJSON.instrumentation.push(el.textContent);
      });

      /** @todo: Support parsing subSteps */

      return modelJSON;
    },

    serialize: function(){
      var objectDOM = this.updateDOM();

      if(!objectDOM)
        return "";

      var xmlString = objectDOM.outerHTML;

      //Camel-case the XML
      xmlString = this.formatXML(xmlString);

      return xmlString;
    },

    /**
     * Makes a copy of the original XML DOM and updates it with the new values from the model.
     */
    updateDOM: function(){

      //Return nothing if this model has only the default values
      if( this.isEmpty() ){
        return;
      }

      try{
        var objectDOM;

        if (this.get("objectDOM")) {
          objectDOM = this.get("objectDOM").cloneNode(true);
        } else {
          objectDOM = $(document.createElement("methodstep"));
        }

        let $objectDOM = $(objectDOM);

        //Update the description
        let description = this.get("description");
        if(description){
          let updatedDescription = description.updateDOM();

          //Descriptions are required for method steps, so if updating the DOM didn't work, don't serialize this method step.
          if( !updatedDescription ){
            return;
          }

          //Add the description to the method step
          let existingDesc = $objectDOM.children("description");
          if( existingDesc.length ){
            existingDesc.replaceWith(updatedDescription);
          }
          else{
            $objectDOM.append(updatedDescription);
          }
        }

        try{
          //Update the instrumentation
          let instrumentation = this.get("instrumentation");
          $objectDOM.children("instrumentation").remove();

          if(instrumentation && instrumentation.length){
            instrumentation.reverse().each(i => {
              let updatedI = document.createElement("instrumentation");
              updatedI.textContent = i;
              $objectDOM.children("description").after(updatedI);
            })
          }
        }
        catch(e){
          console.error("Failed to serialize method step instrumentation, skipping. ", e);
        }

        /** @todo: Update software and subSteps */

        // Remove empty (zero-length or whitespace-only) nodes
        $objectDOM.find("*").filter(function() { return $.trim(this.innerHTML) === ""; } ).remove();

         return objectDOM;
       }
       catch(e){
         console.error("Failed to update the EMLMethodStep. Won't serialize. ", e);
         return;
       }
    },

    /**
    *  function isEmpty() - Will check if there are any values set on this model
    * that are different than the default values and would be serialized to the EML.
    *
    * @return {boolean} - Returns true is this model is empty, false if not
    */
    isEmpty: function(){

      if( !this.get("description") || this.get("description").isEmpty() ){
        return true;
      }

    },

    /**
    * Returns whether or not this Method Step is a custom one, which currently only applies to the description
    * @returns {boolean}
    */
    isCustom: function(){
      return this.get("description")? this.get("description").type == "EMLSpecializedText" : false;
    },

    /**
    * Overloads Backbone.Model.validate() to check if this model has valid values set on it
    * @extends Backbone.Model.validate
    * @returns {object}
    */
    validate: function(){

      try{

        let validationErrors = {}

        if( this.isCustom() && this.get("required") ){
          let desc = this.get("description"),
              isMissing = false;

          //If there is a missing description, we need to show the required error
          if( !desc ){
            isMissing = true;
          }
          else if( !desc.get("text") ){
            isMissing = true;
          }
          else if( !_.compact(desc.get("text")).length ){
            isMissing = true;
          }

          if( isMissing ){
            validationErrors.description = `${desc.get("title")} is required.`
            return validationErrors;
          }
        }

      }
      catch(e){
        console.error("Error while validating the Methods: ", e);
        return false;
      }

    },

    trickleUpChange: function(){
      MetacatUI.rootDataPackage.packageModel.set("changed", true);
    },

    formatXML: function(xmlString){
      return DataONEObject.prototype.formatXML.call(this, xmlString);
    }
  });

  return EMLMethodStep;
});