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

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
   * @extends Backbone.Model
   */
  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;
});