Source: src/js/models/metadata/eml211/EMLText.js

/* global define */
define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'],
    function($, _, Backbone, DataONEObject) {

  /**
  * @class EMLText211
  * @classdesc A model that represents the EML 2.1.1 Text module
  * @classcategory Models/Metadata/EML211
  */
  var EMLText = Backbone.Model.extend(
    /** @lends EMLText211.prototype */{

    type: "EMLText",

    defaults: function(){
      return {
        objectXML: null,
        objectDOM: null,
        parentModel: null,
        originalText: [],
        text: [] //The text content
      }
    },

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

      if(attributes.objectDOM)
        this.set(this.parse(attributes.objectDOM));

      if(attributes.text) {
        if (_.isArray(attributes.text)) {
          this.text = attributes.text
        } else {
          this.text = [attributes.text]
        }
      }

      this.on("change:text", 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()
         */
    nodeNameMap: function(){
      return{

      }
    },

    /**
    * function setText
    *
    * @param text {string} - The text, usually taken directly from an HTML textarea
    * value, to parse and set on this model
    */
    setText: function(text){

      if( typeof text !== "string" )
        return "";

      let model = this;

      require(["models/metadata/eml211/EML211"], function(EMLModel){
        //Get the EML model and use the cleanXMLText function to clean up the text
        text = EMLModel.prototype.cleanXMLText(text);

        //Get the list of paragraphs - checking for carriage returns and line feeds
        var paragraphsCR = text.split(String.fromCharCode(13));
        var paragraphsLF = text.split(String.fromCharCode(10));

        //Use the paragraph list that has the most
        var paragraphs = (paragraphsCR > paragraphsLF)? paragraphsCR : paragraphsLF;

        paragraphs = _.map(paragraphs, function(p){ return p.trim() });

        model.set("text", paragraphs);
      });

    },

    parse: function(objectDOM){
      if(!objectDOM)
        var objectDOM = this.get("objectDOM").cloneNode(true);

      //Start a list of paragraphs
      var paragraphs = [];

      //Get all the child nodes of this text element
      var $objectDOM = $(objectDOM);

      // Save all the contained nodes as paragraphs
      // ignore any nested formatting elements for now
      //TODO: Support more detailed text formatting
      if( $objectDOM.children().length ){

        paragraphs = this.parseNestedElements($objectDOM);

      }
      else if( objectDOM.textContent ){
        paragraphs[0] = objectDOM.textContent;
      }

      return {
        text: paragraphs,
        originalText: paragraphs.slice(0) //The slice function will effectively clone the array
      }
    },

    parseNestedElements: function(nodeEl){

      let children = $(nodeEl).children(),
          paragraphs = [];

      children.each((i, childNode) => {
        if( $(childNode).children().length ){
          paragraphs = paragraphs.concat(this.parseNestedElements(childNode));
        }
        else{
          paragraphs = paragraphs.concat(this.parseParagraphs(childNode));
        }
      })

      return paragraphs;
    },

    parseParagraphs: function(nodeEl){
      if( nodeEl.textContent ){

        //Get the list of paragraphs - checking for carriage returns and line feeds
        var paragraphsCR = nodeEl.textContent.split(String.fromCharCode(13));
        var paragraphsLF = nodeEl.textContent.split(String.fromCharCode(10));

        //Use the paragraph list that has the most
        var paragraphs = (paragraphsCR > paragraphsLF)? paragraphsCR : paragraphsLF;

        //Trim extra whitespace off each paragraph to get rid of the line break characters
        paragraphs = _.map(paragraphs, function(text){
          if(typeof text == "string")
            return text.trim();
          else
            return text;
        });

        //Remove all falsey values - primarily empty strings
        paragraphs = _.compact(paragraphs);

        return paragraphs;

      }
    },

    serialize: function(){
      var objectDOM = this.updateDOM(),
        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(){
      var type = this.get("type") || this.get("parentAttribute") || 'text',
          objectDOM = this.get("objectDOM") ? this.get("objectDOM").cloneNode(true) : document.createElement(type);

      //FIrst check if any of the text in this model has changed since it was originally parsed
      if( _.intersection(this.get("text"), this.get("originalText")).length == this.get("text").length
       && this.get("objectDOM")){
        return objectDOM;
      }

      //If there is no text, return an empty string
      if( this.isEmpty() ){
        return "";
      }

      //Empty the DOM
      $(objectDOM).empty();

      //Format the text
      var paragraphs = this.get("text");
      _.each(paragraphs, function(p){

        //If this paragraph text is a string, add a <para> node with that text
        if( typeof p == "string" && p.trim().length )
          $(objectDOM).append("<para>" + p + "</para>");

      });

      return objectDOM;
    },

    /**
    * Climbs up the model heirarchy until it finds the EML model
    *
    * @return {EML211|false} - Returns the EML 211 Model or false if not found
    */
    getParentEML: function(){
      var emlModel = this.get("parentModel"),
          tries = 0;

      while (emlModel.type !== "EML" && tries < 6){
        emlModel = emlModel.get("parentModel");
        tries++;
      }

      if( emlModel && emlModel.type == "EML")
        return emlModel;
      else
        return false;

    },

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

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

    isEmpty: function() {

      //If the text is an empty array, this is empty
      if( Array.isArray(this.get("text")) && this.get("text").length == 0 ){
        return true;
      }
      //If the text is a falsey value, it is empty
      else if( !this.get("text") ){
        return true;
      }

      //Iterate over each paragraph in the text array and check if it's an empty string
      for (var i = 0; i < this.get('text').length; i++) {
        if (this.get('text')[i].trim().length > 0)
          return false;
      }

      return true;
    },

    /**
    * Returns the EML Text paragraphs as a string, with each paragraph on a new line.
    * @returns {string}
    */
    toString: function() {
      var value = [];

      if (_.isArray(this.get('text'))) {
        value = this.get('text');
      }

      return value.join('\n\n');
    }
  });

  return EMLText;
});