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

/* global define */
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;
          }

        }


      });

      return PortalSectionModel;
});