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