Source: src/js/models/portals/PortalSectionModel.js

define([
  "jquery",
  "underscore",
  "backbone",
  "models/portals/PortalImage",
  "models/metadata/eml220/EMLText",
], function ($, _, Backbone, PortalImage, EMLText) {
  /**
   * @class PortalSectionModel
   * @classdesc A Portal Section model represents the ContentSectionType from the portal schema
   * @classcategory Models/Portals
   * @extends Backbone.Model
   */
  var PortalSectionModel = Backbone.Model.extend(
    /** @lends PortalSectionModel.prototype */ {
      defaults: function () {
        return {
          label: "Untitled",
          image: "",
          title: "",
          introduction: "",
          content: new EMLText({
            type: "content",
            parentModel: this,
          }),
          literatureCited: null,
          objectDOM: null,
          sectionType: "",
          portalModel: null,
        };
      },

      /**
       * Parses a <section> element from a portal document
       *
       *  @param {XMLElement} objectDOM - A ContentSectionType XML element from a portal document
       *  @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
       */
      parse: function (objectDOM) {
        if (!objectDOM) {
          return {};
        }

        var $objectDOM = $(objectDOM),
          modelJSON = {};

        //Parse all the simple string elements
        modelJSON.label = $objectDOM.children("label").text();
        modelJSON.title = $objectDOM.children("title").text();
        modelJSON.introduction = $objectDOM.children("introduction").text();

        //Parse the image URL or identifier
        var image = $objectDOM.children("image");
        if (image.length) {
          var portImageModel = new PortalImage({
            objectDOM: image[0],
            portalModel: this.get("portalModel"),
          });
          portImageModel.set(portImageModel.parse());
          modelJSON.image = portImageModel;
        }

        //Create an EMLText model for the section content
        modelJSON.content = new EMLText({
          objectDOM: $objectDOM.children("content")[0],
        });
        modelJSON.content.set(
          modelJSON.content.parse($objectDOM.children("content")),
        );

        return modelJSON;
      },

      /**
       *  Makes a copy of the original XML DOM and updates it with the new values from the model.
       *
       *  @return {XMLElement} An updated ContentSectionType XML element from a portal document
       */
      updateDOM: function () {
        var objectDOM = this.get("objectDOM");

        if (objectDOM) {
          objectDOM = objectDOM.cloneNode(true);
          $(objectDOM).empty();
        } else {
          // create an XML section element from scratch
          var xmlText = "<section></section>",
            objectDOM = new DOMParser().parseFromString(xmlText, "text/xml"),
            objectDOM = $(objectDOM).children()[0];
        }

        // Get and update the simple text strings (everything but content)
        var sectionTextData = {
          label: this.get("label"),
          title: this.get("title"),
          introduction: this.get("introduction"),
        };

        _.map(
          sectionTextData,
          function (value, nodeName) {
            // Don't serialize default values, except for default label strings, since labels are required
            if (
              value &&
              (value != this.defaults()[nodeName] ||
                (nodeName == "label" && typeof value == "string"))
            ) {
              // Make new sub-node
              var sectionSubnodeSerialized =
                objectDOM.ownerDocument.createElement(nodeName);
              $(sectionSubnodeSerialized).text(value);

              this.addUpdatedXMLNode(objectDOM, sectionSubnodeSerialized);
            }
            //If the value was removed from the model, then remove the element from the XML
            else {
              $(objectDOM).children(nodeName).remove();
            }
          },
          this,
        );

        //Update the image element
        if (
          this.get("image") &&
          typeof this.get("image").updateDOM == "function"
        ) {
          var imageSerialized = this.get("image").updateDOM();

          this.addUpdatedXMLNode(objectDOM, imageSerialized);
        } else {
          $(objectDOM).children("image").remove();
        }

        // Get markdown which is a child of content
        var content = this.get("content");

        if (content) {
          var contentSerialized = content.updateDOM("content");

          this.addUpdatedXMLNode(objectDOM, contentSerialized);
        } else {
          $(objectDOM).children("content").remove();
        }

        //If nothing was serialized, return an empty string
        if (!$(objectDOM).children().length) {
          return "";
        }

        return objectDOM;
      },

      /**
       * Takes the updated XML node and inserts it into the given object DOM in
       * the correct position.
       * @param {Element} objectDOM - The full object DOM for this model
       * @param {Element} newElement - The updated element to insert into the object DOM
       */
      addUpdatedXMLNode: function (objectDOM, newElement) {
        //If a parameter is missing, don't do anything
        if (!objectDOM || !newElement) {
          return;
        }

        try {
          //Get the node name of the new element
          var nodeName = $(newElement)[0].nodeName;

          if (nodeName) {
            //Only insert the new element if there is content in it
            if (
              $(newElement).children().length ||
              $(newElement).text().length
            ) {
              //Add the new element to the owner Document
              objectDOM.ownerDocument.adoptNode(newElement);

              //Get the existing node
              var existingNodes = $(objectDOM).children(nodeName);

              //Get the position that the image should be
              var insertAfter = this.getXMLPosition(objectDOM, nodeName);

              if (insertAfter) {
                //Insert it into that position
                $(insertAfter).after(newElement);
              } else {
                objectDOM.appendChild(newElement);
              }

              existingNodes.remove();
            }
          }
        } catch (e) {
          console.log(e);
          return;
        }
      },

      /**
       * Finds the node in the given portal XML document afterwhich the
       * given node type should be inserted
       *
       * @param {Element} parentNode - The parent XML element
       * @param {string} nodeName - The name of the node to be inserted
       *                             into xml
       * @return {(jQuery|boolean)} A jQuery object indicating a position,
       *                            or false when nodeName is not in the
       *                            portal schema
       */
      getXMLPosition: function (parentNode, nodeName) {
        var nodeOrder = [
          "label",
          "title",
          "introduction",
          "image",
          "content",
          "option",
        ];

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

        // First check that nodeName is in the list of nodes
        if (position == -1) {
          return false;
        }

        // If there's already an occurence of nodeName...
        if ($(parentNode).children(nodeName).length > 0) {
          // ...insert it after the last occurence
          return $(parentNode).children(nodeName).last();
        } else {
          // Go through each node in the node list and find the position
          // after which this node will be inserted
          for (var i = position - 1; i >= 0; i--) {
            if ($(parentNode).children(nodeOrder[i]).length) {
              return $(parentNode).children(nodeOrder[i]).last();
            }
          }
        }

        return false;
      },

      /**
       * Overrides the default Backbone.Model.validate.function() to
       * check if this PortalSection model has all the required values necessary
       * to save to the server.
       *
       * @return {Object} If there are errors, an object comprising error
       *                   messages. If no errors, returns nothing.
       */
      validate: function () {
        try {
          var errors = {},
            requiredFields = MetacatUI.appModel.get(
              "portalEditorRequiredFields",
            );

          //--Validate the label--
          //Labels are always required
          if (!this.get("label")) {
            errors.label = "Please provide a page name.";
          }

          //---Validate the title---
          //If section titles are required and there isn't one, set an error message
          if (
            requiredFields.sectionTitle &&
            typeof this.get("title") == "string" &&
            !this.get("title").length
          ) {
            errors.title = "Please provide a title for this page.";
          }

          //---Validate the introduction---
          //If section introductions are required and there isn't one, set an error message
          if (
            requiredFields.sectionIntroduction &&
            typeof this.get("introduction") == "string" &&
            !this.get("introduction").length
          ) {
            errors.introduction =
              "Please provide some a sub-title or some introductory text for this page.";
          }

          //---Validate the section content---
          //Content is always required
          if (!this.get("content")) {
            errors.markdown = "Please provide content for this page.";
          }
          //Check if there is either markdown or an array of strings in the text attribute
          else if (
            !this.get("content").get("markdown") &&
            !this.get("content").get("text").length
          ) {
            errors.markdown = "Please provide content for this page.";
          }
          //Check if the markdown hasn't been changed from the example markdown
          else if (
            this.get("content").get("markdown") ==
            this.get("content").get("markdownExample")
          ) {
            errors.markdown = "Please provide content for this page.";
          }

          //---Validate the section image---

          if (
            requiredFields.sectionImage &&
            (!this.get("image") || this.get("image").isEmpty())
          ) {
            errors.sectionImage = "A section image is required.";
          }

          //Return the errors object
          if (Object.keys(errors).length) return errors;
          else {
            return;
          }
        } catch (e) {
          console.error(e);
          return;
        }
      },

      /**
       * Handler function for the a portal section change. Can be overridden by
       * derived classes.
       * @param {boolean} isActive Whether the active portal section model is
       * this portal section model.
       */
      reportSectionChange(isActive) {
        // Do nothing.
      },
    },
  );

  return PortalSectionModel;
});