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

define(["jquery", "underscore", "backbone", "models/DataONEObject"], function (
  $,
  _,
  Backbone,
  DataONEObject,
) {
  /**
   * @class EMLNonNumericDomain
   * @classdesc EMLNonNumericDomain represents the measurement scale of a nominal
   * or ordinal measurement scale attribute, and is an extension of
   * EMLMeasurementScale.
   * @classcategory Models/Metadata/EML211
   * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_AttributeType_measurementScale_AttributeType_AttributeType_measurementScale_nominal_nonNumericDomain
   * @extends Backbone.Model
   * @constructor
   */
  var EMLNonNumericDomain = Backbone.Model.extend(
    /** @lends EMLNonNumericDomain.prototype */ {
      type: "EMLNonNumericDomain",

      /* Attributes of an EMLNonNumericDomain object */
      defaults: function () {
        return {
          /* Attributes from EML, extends attributes from EMLMeasurementScale */
          measurementScale: null, // the name of this measurement scale
          nonNumericDomain: [], // One or more of enumeratedDomain, textDomain, references
        };
      },

      /**
       * The map of lower case to camel case node names
       * needed to deal with parsing issues with $.parseHTML().
       * Use this until we can figure out issues with $.parseXML().
       * @type {object}
       */
      nodeNameMap: {
        nonnumericdomain: "nonNumericDomain",
        enumerateddomain: "enumeratedDomain",
        textdomain: "textDomain",
        externalcodeset: "externalCodeSet",
        codesetname: "codesetName",
        codeseturl: "codesetURL",
        entityCodeList: "entityCodeList",
        entityreference: "entityReference",
        valueattributereference: "valueAttributeReference",
        definitionattributereference: "definitionAttributeReference",
        orderattributereference: "orderAttributeReference",
        sourced: "source",
      },

      /* Initialize an EMLNonNumericDomain object */
      initialize: function (attributes, options) {
        this.on("change:nonNumericDomain", this.trickleUpChange);
      },

      /**
       * Parse the incoming measurementScale's XML elements
       */
      parse: function (attributes, options) {
        var $objectDOM;
        var nonNumericDomainNodeList;
        var domainNodeList; // the list of domain elements
        var domain; // the text or enumerated domain to parse
        var domainObject; // The parsed domain object to be added to attributes.nonNumericDomain
        var rootNodeName; // Name of the fragment root elements

        if (attributes.objectDOM) {
          rootNodeName = $(attributes.objectDOM)[0].localName;
          $objectDOM = $(attributes.objectDOM);
        } else if (attributes.objectXML) {
          rootNodeName = $(attributes.objectXML)[0].localName;
          $objectDOM = $($(attributes.objectXML)[0]);
        } else {
          return {};
        }

        // do we have an appropriate measurementScale tree?
        var index = _.indexOf(
          ["measurementscale", "nominal", "ordinal"],
          rootNodeName,
        );
        if (index == -1) {
          throw new Error(
            "The measurement scale XML does not have a root " +
              "node of 'measurementScale', 'nominal', or 'ordinal'.",
          );
        }

        // If measurementScale is present, add it
        if (rootNodeName == "measurementscale") {
          attributes.measurementScale = $objectDOM
            .children()
            .first()[0].localName;
          $objectDOM = $objectDOM.children().first();
        } else {
          attributes.measurementScale = $objectDOM.localName;
        }

        nonNumericDomainNodeList = $objectDOM.find("nonnumericdomain");

        if (nonNumericDomainNodeList && nonNumericDomainNodeList.length > 0) {
          domainNodeList = nonNumericDomainNodeList[0].children;
        } else {
          // No content is available, return
          return attributes;
        }

        // Initialize an array of nonNumericDomain objects
        attributes.nonNumericDomain = [];

        // Set each domain if we have it
        if (domainNodeList && domainNodeList.length > 0) {
          _.each(
            domainNodeList,
            function (domain) {
              if (domain) {
                // match the camelCase name since DOMParser() is XML-aware
                switch (domain.localName) {
                  case "textdomain":
                    domainObject = this.parseTextDomain(domain);
                    break;
                  case "enumerateddomain":
                    domainObject = this.parseEnumeratedDomain(domain);
                    break;
                  case "references":
                    // TODO: Support references
                    console.log(
                      "In EMLNonNumericDomain.parse()" +
                        "We don't support references yet ",
                    );
                  default:
                    console.log(
                      "Unrecognized nonNumericDomain: " + domain.nodeName,
                    );
                }
              }
              attributes.nonNumericDomain.push(domainObject);
            },
            this,
          );
        }

        // Add in the textDomain content if present
        // TODO

        attributes.objectDOM = $objectDOM[0];

        return attributes;
      },

      /* Parse the nonNumericDomain/textDomain fragment
       * returning an object with a textDomain attribute, like:
       * {
       *     textDomain: {
       *         definition: "Some definition",
       *         pattern: ["*", "\w", "[0-9]"],
       *         source: "Some source reference"
       *     }
       * }
       */
      parseTextDomain: function (domain) {
        var domainObject = {};
        domainObject.textDomain = {};
        var xmlID;
        var definition;
        let patterns = [];
        var source;

        // Add the XML id attribute
        if ($(domain).attr("id")) {
          xmlID = $(domain).attr("id");
        } else {
          // Generate an id if it's not found
          xmlID = DataONEObject.generateId();
        }
        domainObject.textDomain.xmlID = xmlID;

        // Add the definition
        definition = $(domain).children("definition").text();
        domainObject.textDomain.definition = definition;

        // Add the pattern
        _.each($(domain).children("pattern"), function (pattern) {
          patterns.push(pattern.textContent);
        });
        domainObject.textDomain.pattern = patterns;

        // Add the source
        source = $(domain).children("sourced").text();
        domainObject.textDomain.source = source;

        return domainObject;
      },

      /* Parse the nonNumericDomain/enumeratedDomain fragment
       * returning an object with an enumeratedDomain attribute, like:
       * var emlCitation = {};
       * var nonNumericDomain = [
       *     {
       *         enumeratedDomain: {
       *             codeDefinition: [
       *                 {
       *                     code: "Some code", // required
       *                     definition: "Some definition", // required
       *                     source: "Some source"
       *                 } // repeatable
       *             ]
       *         }
       *     }, // or
       *     {
       *         enumeratedDomain: {
       *             externalCodeSet: [
       *                 {
       *                     codesetName: "Some code", // required
       *                     citation: [emlCitation], // one of citation or codesetURL
       *                     codesetURL: ["Some URL"] // is required, both repeatable
       *                 } // repeatable
       *             ]
       *         }
       *     }, // or
       *     {
       *         enumeratedDomain: {
       *             entityCodeList: {
       *                 entityReference: "Some reference", // required
       *                 valueAttributeReference: "Some attr reference", // required
       *                 definitionAttributeReference: "Some definition attr reference", // required
       *                 orderAttributeReference: "Some order attr reference"
       *             }
       *         }
       *     }
       * ]
       */
      parseEnumeratedDomain: function (domain) {
        var domainObject = {};
        domainObject.enumeratedDomain = {};
        var codeDefinition = {};
        var externalCodeSet = {};
        var entityCodeList = {};
        var xmlID;

        // Add the XML id attribute
        if ($(domain).attr("id")) {
          xmlID = $(domain).attr("id");
        } else {
          // Generate an id if it's not found
          xmlID = DataONEObject.generateId();
        }
        domainObject.enumeratedDomain.xmlID = xmlID;

        // Add the codeDefinitions if present
        var codeDefinitions = $(domain).children("codedefinition");

        if (codeDefinitions.length) {
          domainObject.enumeratedDomain.codeDefinition = [];
          _.each(codeDefinitions, function (codeDef) {
            var code = $(codeDef).children("code").text();
            var definition = $(codeDef).children("definition").text();
            var source = $(codeDef).children("sourced").text() || undefined;
            domainObject.enumeratedDomain.codeDefinition.push({
              code: code,
              definition: definition,
              source: source,
            });
          });
        }
        return domainObject;
      },

      /* Serialize the model to XML */
      serialize: function () {
        var objectDOM = this.updateDOM();
        var xmlString = objectDOM.outerHTML;

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

        return xmlString;
      },

      /* Copy the original XML DOM and update it with new values from the model */
      updateDOM: function (objectDOM) {
        var objectDOM;
        var xmlID; // The id of the textDomain or enumeratedDomain fragment
        var nonNumericDomainNode;
        var domainType; // Either textDomain or enumeratedDomain
        var $domainInDOM; // The jQuery object of the text or enumerated domain from the DOM
        var nodeToInsertAfter;
        var domainNode; // Either a textDomain or enumeratedDomain node
        var definitionNode;
        var patternNode;
        var sourceNode;
        var enumeratedDomainNode;
        var codeDefinitions;
        var codeDefinitionNode;
        var codeNode;

        var type = this.get("measurementScale");
        if (typeof type === "undefined") {
          console.warn("Defaulting to an nominal measurementScale.");
          type = "nominal";
        }
        if (!objectDOM) {
          objectDOM = this.get("objectDOM");
        }
        var objectXML = this.get("objectXML");

        // If present, use the cached DOM
        if (objectDOM) {
          objectDOM = objectDOM.cloneNode(true);

          // otherwise, use the cached XML
        } else if (objectXML) {
          objectDOM = $(objectXML)[0].cloneNode(true);

          // This is new, create it
        } else {
          objectDOM = document.createElement(type);
        }

        if (this.get("nonNumericDomain").length) {
          // Update each nonNumericDomain in the DOM
          _.each(
            this.get("nonNumericDomain"),
            function (domain, i) {
              // Is this a textDomain or enumeratedDomain?
              if (typeof domain.textDomain === "object") {
                domainType = "textDomain";
                xmlID = domain.textDomain.xmlID;
              } else if (typeof domain.enumeratedDomain === "object") {
                domainType = "enumeratedDomain";
                xmlID = domain.enumeratedDomain.xmlID;
              } else {
                console.log("Unrecognized NonNumericDomain type. Skipping.");
                // TODO: Handle references here
              }

              // Update the existing DOM node by id
              if (xmlID && $(objectDOM).find("#" + xmlID).length) {
                if (domainType === "textDomain") {
                  let originalTextDomain = $(objectDOM)
                    .find("#" + xmlID)
                    .find("textdomain");

                  //If there are existing textDomain nodes in the DOM, update them
                  if (originalTextDomain.length) {
                    let updatedTextDomain = this.updateTextDomain(
                      domain.textDomain,
                      originalTextDomain,
                    );
                    originalTextDomain.replaceWith(updatedTextDomain);
                  }
                  //If there are no textDomain nodes in the DOM, create new ones
                  else {
                    //Create new textDomain nodes
                    let newTextDomain = this.createTextDomain(
                      domain.textDomain,
                    );

                    //Insert the new textDomain nodes into the nonNumericDomain node
                    $($(objectDOM).children("nonnumericdomain")[i]).html(
                      newTextDomain,
                    );
                  }
                } else if (domainType === "enumeratedDomain") {
                  this.updateEnumeratedDomainDOM(
                    domain.enumeratedDomain,
                    $domainInDOM,
                  );
                }

                //If there is no XML ID but there are the same number of nonNumericDomains in the model and DOM
              } else if (
                this.get("nonNumericDomain").length ==
                  $(objectDOM).children("nonnumericdomain").length &&
                $(objectDOM).children("nonnumericdomain").length >= i
              ) {
                //If this is a text domain,
                if (typeof domain.textDomain === "object") {
                  let originalTextDomain = $(
                    $(objectDOM).children("nonnumericdomain")[i],
                  ).find("textdomain");

                  //If there are existing textDomain nodes in the DOM, update them
                  if (originalTextDomain.length) {
                    let updatedTextDomain = this.updateTextDomain(
                      domain.textDomain,
                      originalTextDomain,
                    );
                    originalTextDomain.replaceWith(updatedTextDomain);
                  }
                  //If there are no textDomain nodes in the DOM, create new ones
                  else {
                    //Create new textDomain nodes
                    var newTextDomain = this.createTextDomain(
                      domain.textDomain,
                    );

                    //Insert the new textDomain nodes into the nonNumericDomain node
                    $($(objectDOM).children("nonnumericdomain")[i]).html(
                      newTextDomain,
                    );
                  }
                } else if (typeof domain.enumeratedDomain === "object") {
                  //Get the nonNumericDomain node from the DOM
                  var nonNumericDomainNode =
                      $(objectDOM).children("nonnumericdomain")[i],
                    enumeratedDomain =
                      $(nonNumericDomainNode).children("enumerateddomain");

                  if (enumeratedDomain.length) {
                    this.updateEnumeratedDomainDOM(
                      domain.enumeratedDomain,
                      enumeratedDomain,
                    );
                  } else {
                    //Remove the textDomain node and replace it with an enumeratedDomain node
                    var textDomainToReplace = $(objectDOM).find("textdomain");

                    if (textDomainToReplace.length > 0) {
                      $(textDomainToReplace[i]).replaceWith(
                        this.createEnumeratedDomainDOM(domain.enumeratedDomain),
                      );
                    } else {
                      nonNumericDomainNode.html(
                        this.createEnumeratedDomainDOM(
                          domain.enumeratedDomain,
                          document.createElement("enumerateddomain"),
                        ),
                      );
                    }
                  }
                }

                // Otherwise append to the DOM
              } else {
                // Add the nonNumericDomain element
                nonNumericDomainNode =
                  document.createElement("nonnumericdomain");

                if (domainType === "textDomain") {
                  // Add the definiton element
                  domainNode = document.createElement("textdomain");
                  if (domain.textDomain.definition) {
                    definitionNode = document.createElement("definition");
                    $(definitionNode).text(domain.textDomain.definition);
                    $(domainNode).append(definitionNode);
                  }

                  // Add the pattern element(s)
                  if (domain.textDomain.pattern.length) {
                    _.each(
                      domain.textDomain.pattern,
                      function (pattern) {
                        patternNode = document.createElement("pattern");
                        $(patternNode).text(pattern);
                        $(domainNode).append(patternNode);
                      },
                      this,
                    );
                  }

                  // Add the source element
                  if (domain.textDomain.source) {
                    sourceNode = document.createElement("sourced"); // Accommodate parseHTML() with "d"
                    $(sourceNode).text(domain.textDomain.source);
                    $(domainNode).append(sourceNode);
                  }
                } else if (domainType === "enumeratedDomain") {
                  nonNumericDomainNode.append(
                    this.createEnumeratedDomainDOM(domain.enumeratedDomain),
                  );
                } else {
                  console.log(
                    "The domainType: " + domainType + " is not recognized.",
                  );
                }
                $(nonNumericDomainNode).append(domainNode);
                $(objectDOM).append(nonNumericDomainNode);
              }
            },
            this,
          );
        } else {
          // We have no content, so can't create a valid domain
          console.log(
            "In EMLNonNumericDomain.updateDOM(),\n" +
              "references are not handled yet. Returning undefined.",
          );
          // TODO: handle references here
          return undefined;
        }
        return objectDOM;
      },

      /*
       * Update the codeDefinitionList in the  first enumeratedDomain
       * found in the nonNumericDomain array.
       * TODO: Refactor this to support externalCodeSet and entityCodeList
       * TODO: Support the source field
       * TODO: Support repeatable enumeratedDomains
       * var nonNumericDomain = [
       *     {
       *         enumeratedDomain: {
       *             codeDefinition: [
       *                 {
       *                     code: "Some code", // required
       *                     definition: "Some definition", // required
       *                     source: "Some source"
       *                 } // repeatable
       *             ]
       *         }
       *     }
       * ]
       */
      updateEnumeratedDomain: function (code, definition, index) {
        var nonNumericDomain = this.get("nonNumericDomain");
        var enumeratedDomain = {};
        var codeDefinitions;

        if (typeof code == "string" && !code.trim().length) {
          code = "";
        }

        if (typeof definition == "string" && !definition.trim().length) {
          definition = "";
        }

        // Create from scratch
        if (
          !nonNumericDomain.length ||
          !nonNumericDomain[0] ||
          !nonNumericDomain[0].enumeratedDomain
        ) {
          nonNumericDomain[0] = {
            enumeratedDomain: {
              codeDefinition: [
                {
                  code: code,
                  definition: definition,
                },
              ],
            },
          };
        }
        // Update existing
        else {
          enumeratedDomain = this.get("nonNumericDomain")[0].enumeratedDomain;

          if (typeof enumeratedDomain !== "undefined") {
            //If there is no code or definition, then remove it from the code list
            if (!code && code !== 0 && !definition && definition !== 0) {
              this.removeCode(index);
            } else if (enumeratedDomain.codeDefinition.length >= index) {
              //Create a new code object and insert it into the array
              enumeratedDomain.codeDefinition[index] = {
                code: code,
                definition: definition,
              };
            } else {
              //Create a new code object and append it to the end of the array
              enumeratedDomain.codeDefinition.push({
                code: code,
                definition: definition,
              });
            }
          }
        }

        //Manually trigger the change event since we're updating an array on the model
        this.trigger("change:nonNumericDomain");
      },

      /*
       * Given a `codeDefinition` HTML node and an enumeratedDomain list,
       *   this function will update the HTML node code definitions with
       *   all the code definitions listed in the enumeratedDomain
       *
       * @param {object} enumeratedDomain - A literal object with an array of codeDefinitions
       * @param {DOM Element or jQuery Object} - A DOM Element or jQuery selection that represents the <enumeratedDomain> node
       */
      updateEnumeratedDomainDOM: function (
        enumeratedDomain,
        enumeratedDomainNode,
      ) {
        if (enumeratedDomain.codeDefinition.length) {
          // Update each codeDefinition
          _.each(
            enumeratedDomain.codeDefinition,
            function (codeDef, i) {
              var codeDefNode = $(
                $(enumeratedDomainNode).children("codedefinition")[i],
              );

              if (!codeDefNode.length) {
                codeDefNode = $(document.createElement("codedefinition"));
                $(enumeratedDomainNode).append(codeDefNode);
              }

              // Update the required code element
              if (codeDef.code) {
                var codeNode = codeDefNode.children("code");

                //If there is no <code> XML node, make one
                if (!codeNode.length) {
                  codeNode = $(document.createElement("code"));
                  codeDefNode.append(codeNode);
                }

                //Add the code text to the <code> node
                codeNode.text(codeDef.code);
              } else {
                codeDefNode.children("code").remove();
              }

              // Update the required definition element
              if (codeDef.definition) {
                var defNode = codeDefNode.children("definition");

                //If there is no <definition> XML node, make one
                if (!defNode.length) {
                  defNode = $(document.createElement("definition"));
                  codeDefNode.append(defNode);
                }

                //Add the definition text to the <definition> node
                defNode.text(codeDef.definition);
              } else {
                codeDefNode.children("definition").remove();
              }

              // Update the optional source element
              if (codeDef.source) {
                // Accommodate parseHTML() with source"d"
                var sourceNode = codeDefNode.children("sourced");

                //If there is no <source> XML node, make one
                if (!sourceNode.length) {
                  sourceNode = $(document.createElement("sourced"));
                  codeDefNode.append(sourceNode);
                }

                sourceNode.text(codeDef.source);
              } else {
                codeDefNode.children("sourced").remove();
              }
            },
            this,
          );

          // If there are more codeDefinition nodes than there are codeDefinitions
          // in the model, then we need to remove the extraneous nodes
          var numNodes =
              $(enumeratedDomainNode).children("codedefinition").length,
            numCodes = enumeratedDomain.codeDefinition.length;

          if (numNodes > numCodes) {
            //Get the extraneous nodes by selecting the last X child elements
            var nodesToRemove = $(enumeratedDomainNode)
              .children("codedefinition")
              .slice((numNodes - numCodes) * -1);
            //Remove them from the DOM
            nodesToRemove.remove();
          }
        } else if (domain.enumeratedDomain.externalCodeSet) {
          // TODO Handle externalCodeSet
        } else if (domain.enumeratedDomain.entityCodeList) {
          // TODO Handle entityCodeList
        }

        return enumeratedDomainNode;
      },

      /*
       * Given an enumeratedDomain list, this function will create an
       *   <enumeratedDomain> HTML element with all the code definitions
       *   listed in the enumeratedDomain object
       *
       * @param {object} enumeratedDomain - A literal object with an array of codeDefinitions
       * @return {DOM Element} - An <enumerateddomain> DOM element tree with code definitions
       */
      createEnumeratedDomainDOM: function (enumeratedDomain) {
        var enumeratedDomainNode = document.createElement("enumerateddomain");

        if (enumeratedDomain.codeDefinition.length) {
          // Add each codeDefinition
          _.each(
            enumeratedDomain.codeDefinition,
            function (codeDef) {
              var codeDefinitionNode = document.createElement("codedefinition");

              // Add the required code element
              if (codeDef.code) {
                var codeNode = document.createElement("code");
                $(codeNode).text(codeDef.code);
                $(codeDefinitionNode).append(codeNode);
              }

              // Add the required definition element
              if (codeDef.definition) {
                var definitionNode = document.createElement("definition");
                $(definitionNode).text(codeDef.definition);
                $(codeDefinitionNode).append(definitionNode);
              }

              // Add the optional source element
              if (codeDef.source) {
                var sourceNode = document.createElement("sourced"); // Accommodate parseHTML() with "d"
                $(sourceNode).text(codeDef.source);
                $(codeDefinitionNode).append(sourceNode);
              }
              $(enumeratedDomainNode).append(codeDefinitionNode);
            },
            this,
          );
        } else if (domain.enumeratedDomain.externalCodeSet) {
          // TODO Handle externalCodeSet
        } else if (domain.enumeratedDomain.entityCodeList) {
          // TODO Handle entityCodeList
        }

        return enumeratedDomainNode;
      },

      /*
       * Given a textDomain object, and textDomain DOM object, this function
       *  will update all the DOM elements with the textDomain object values
       *
       * @param {object} textDomain - A literal object representing an EML text domain
       * @param {DOM Element} textDomainEl - The <textDomain> DOM Element to update
       * @return {DOM Element} - An <textdomain> DOM element tree to update
       */
      updateTextDomain: function (textDomain, textDomainEl) {
        if (
          typeof textDomainEl === "undefined" ||
          (typeof textDomainEl == "object" && textDomainEl.length == 0)
        )
          var textDomainEl = document.createElement("textdomain");

        //Create a shortcut to the jQuery object of the text domain element
        var $textDomainEl = $(textDomainEl);

        var definitionEl = $textDomainEl.find("definition");

        //Update the definition element text
        if (definitionEl.length > 0) definitionEl.text(textDomain.definition);
        else {
          $textDomainEl.prepend(
            $(document.createElement("definition")).text(textDomain.definition),
          );
        }

        // Remove existing patterns
        $textDomainEl.find("pattern").remove();

        // Add any new patterns
        if (textDomain.pattern && textDomain.pattern.length) {
          let patterns = Array.from(textDomain.pattern).reverse();

          _.each(patterns, function (pattern) {
            //Don't serialize strings with only empty characters
            if (typeof pattern == "string" && !pattern.trim().length) return;

            var patternNode = document.createElement("pattern");

            $(patternNode).text(pattern);

            // Prepend before the sourced element if present
            if ($textDomainEl.find("sourced").length) {
              $textDomainEl.find("sourced").before(patternNode);
            } else {
              $textDomainEl.append(patternNode);
            }
          });
        }

        // Update any new source
        if (textDomain.source) {
          if ($textDomainEl.find("sourced").length) {
            $textDomainEl.find("sourced").text(textDomain.source);
          } else {
            //
            var src = document.createElement("sourced");
            src.textContent = textDomain.source;
            $textDomainEl.find("textDomain").append(src);
          }
        } else {
          // Remove the source in the DOM not present in the textDomain
          // TODO: Uncomment this when we support "source" in the UI
          // $domainInDOM.children("source").remove();
        }

        return textDomainEl;
      },

      /*
       * Creates a textDomain DOM object with the textDomain object values
       *
       * @param {object} textDomain - A literal object representing an EML text domain
       * @return {DOM Element} - An <textdomain> DOM element tree to update
       */
      createTextDomain: function (textDomain) {
        var textDomainEl = document.createElement("textdomain");

        this.updateTextDomain(textDomain, textDomainEl);

        return textDomainEl;
      },

      /*
       * Get the DOM node preceding the given nodeName
       * to find what position in the EML document
       * the named node should be appended
       */
      getEMLPosition: function (objectDOM, nodeName) {
        // TODO: set the node order
        var nodeOrder = ["enumerateddomain", "textdomain"];

        var position = _.indexOf(nodeOrder, nodeName);

        // Append to the bottom if not found
        if (position == -1) {
          return $(objectDOM).children().last()[0];
        }

        // Otherwise, go through each node in the node list and find the
        // position where this node will be inserted after
        for (var i = position - 1; i >= 0; i--) {
          if ($(objectDOM).find(nodeOrder[i]).length) {
            return $(objectDOM).find(nodeOrder[i]).last()[0];
          }
        }
      },

      /* Let the top level package know of attribute changes from this object */
      trickleUpChange: function () {
        MetacatUI.rootDataPackage.packageModel.set("changed", true);
      },

      validate: function () {
        var errors = {};

        if (!this.get("nonNumericDomain").length)
          errors.nonNumericDomain = "Choose a possible value type.";
        else {
          var domain = this.get("nonNumericDomain")[0];

          _.each(
            Object.keys(domain),
            function (key) {
              //For enumerated domain types
              if (key == "enumeratedDomain" && domain[key].codeDefinition) {
                var isEmpty =
                  domain[key].codeDefinition.length == 0 ? true : false;

                //Validate the list of codes
                for (var i = 0; i < domain[key].codeDefinition.length; i++) {
                  var codeDef = domain[key].codeDefinition[i];

                  //If either the code or definition is missing in at least one codeDefinition set,
                  //then this model is invalid
                  if (
                    (codeDef.code && !codeDef.definition) ||
                    (!codeDef.code && codeDef.definition)
                  ) {
                    errors.enumeratedDomain =
                      "Provide both a code and definition in each row.";
                    i = domain[key].codeDefinition.length;
                  } else if (
                    domain[key].codeDefinition.length == 1 &&
                    !codeDef.code &&
                    !codeDef.definition
                  )
                    isEmpty = true;
                }

                if (isEmpty)
                  errors.enumeratedDomain =
                    "Define at least one code and definition.";
              } else if (
                key == "textDomain" &&
                (typeof domain[key] != "object" || !domain[key].definition)
              ) {
                errors.definition =
                  "Provide a description of the kind of text allowed.";
              }
            },
            this,
          );
        }

        if (Object.keys(errors).length) return errors;
        else {
          this.trigger("valid");

          return;
        }
      },

      /*
       * Climbs up the model heirarchy until it finds the EML model
       *
       * @return {EML211 or 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;
      },

      removeCode: function (index) {
        var codeToRemove =
          this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition[
            index
          ];

        var newCodeList = _.without(
          this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition,
          codeToRemove,
        );

        this.get("nonNumericDomain")[0].enumeratedDomain.codeDefinition =
          newCodeList;

        this.trigger("change:nonNumericDomain");
      },
    },
  );

  return EMLNonNumericDomain;
});