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;
});