var required = [
"jquery",
"underscore",
"backbone",
"models/DataONEObject",
"models/metadata/eml220/EMLText",
];
if (MetacatUI.appModel.get("customEMLMethods").length) {
required.push("models/metadata/eml/EMLSpecializedText");
}
define(
required,
function ($, _, Backbone, DataONEObject, EMLText, EMLSpecializedText) {
/**
* @class EMLMethodStep
* @classdesc Represents the EML Method Steps. The methodStep field allows for repeated sets of
elements that document a series of procedures followed to produce a
data object. These include text descriptions of the procedures,
relevant literature, software, instrumentation, source data and any
quality control measures taken.
* @classcategory Models/Metadata/EML
* @extends Backbone.Model
* @since 2.19.0
*/
var EMLMethodStep = Backbone.Model.extend(
/** @lends EMLMethodStep.prototype */ {
/**
* Default attributes for EMLMethodSteps
* @returns {object}
* @property {string} objectXML The original XML snippet string from the EML XML
* @property {Element} objectDOM The original XML snippet as an Element
* @property {EMLText|EMLSpecializedText} description A textual description of this method step
* @property {string[]} instrumentation One or more instruments used for measurement and recording data
* @property {EMLMethodStep[]} subStep Nested additional method steps within this step. This is useful for hierarchical method descriptions. This is *not* fully supported in MetacatUI yet
* @property {string[]} customMethodID A unique identifier for this Custom Method Step type, which is defined in {@link AppConfig#customEMLMethods}
* @property {boolean} required If true, this method step is required in it's parent EML
*/
defaults: function () {
return {
objectXML: null,
objectDOM: null,
description: null,
instrumentation: [],
subStep: [],
customMethodID: "",
required: false,
};
},
initialize: function (attributes) {
attributes = attributes || {};
if (attributes.objectDOM) {
this.set(this.parse(attributes.objectDOM));
} else if (attributes.customMethodID) {
try {
let customMethodConfig = MetacatUI.appModel
.get("customEMLMethods")
.find((config) => config.id == attributes.customMethodID);
this.set(
"description",
new EMLSpecializedText({
type: "description",
title: customMethodConfig.titleOptions[0],
titleOptions: customMethodConfig.titleOptions,
}),
);
} catch (e) {
console.error(e);
}
} else {
this.set(
"description",
new EMLText({
type: "description",
}),
);
}
//Set the required attribute
if (typeof attributes.required == "boolean") {
this.set("required", attributes.required);
}
//specific attributes to listen to
this.on("change:instrumentation", 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()
* @returns {object}
*/
nodeNameMap: function () {
return {
alternateidentifier: "alternateIdentifier",
methodstep: "methodStep",
substep: "subStep",
datasource: "dataSource",
referencedentityid: "referencedEntityId",
qualitycontrol: "qualityControl",
shortname: "shortName",
};
},
parse: function (objectDOM) {
var modelJSON = {};
if (!objectDOM) var objectDOM = this.get("objectDOM");
let $objectDOM = $(objectDOM),
description = $objectDOM.children("description");
//Get the titles of all the custom method steps from the App Config
let customMethodOptions = MetacatUI.appModel.get("customEMLMethods"),
customMethodTitles = _.flatten(
_.pluck(customMethodOptions, "titleOptions"),
),
isCustom = false;
try {
//If there is at least one custom method configured, check if this description is one
if (customMethodOptions && customMethodOptions.length) {
let specializedTextAttr = EMLSpecializedText.prototype.parse(
description[0],
),
matchingCustomMethod = customMethodOptions.find((options) =>
options.titleOptions.includes(specializedTextAttr.title),
);
if (matchingCustomMethod) {
isCustom = true;
//Use the EMLSpecializedText model for custom methods
modelJSON.description = new EMLSpecializedText({
objectDOM: description[0],
type: "description",
titleOptions: matchingCustomMethod.titleOptions,
parentModel: this,
});
//Save the other configurations of this custom method to this EMLMethodStep
modelJSON.customMethodID = matchingCustomMethod.id;
modelJSON.required = matchingCustomMethod.required;
}
}
} catch (e) {
console.error(e);
}
//Create a regular EMLText description for non-custom methods
if (!isCustom) {
modelJSON.description = new EMLText({
objectDOM: description[0],
type: "description",
parentModel: this,
});
}
//Parse the instrumentation
modelJSON.instrumentation = [];
$objectDOM.children("instrumentation").each((i, el) => {
modelJSON.instrumentation.push(el.textContent);
});
/** @todo: Support parsing subSteps */
return modelJSON;
},
serialize: function () {
var objectDOM = this.updateDOM();
if (!objectDOM) return "";
var 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 () {
//Return nothing if this model has only the default values
if (this.isEmpty()) {
return;
}
try {
var objectDOM;
if (this.get("objectDOM")) {
objectDOM = this.get("objectDOM").cloneNode(true);
} else {
objectDOM = $(document.createElement("methodstep"));
}
let $objectDOM = $(objectDOM);
//Update the description
let description = this.get("description");
if (description) {
let updatedDescription = description.updateDOM();
//Descriptions are required for method steps, so if updating the DOM didn't work, don't serialize this method step.
if (!updatedDescription) {
return;
}
//Add the description to the method step
let existingDesc = $objectDOM.children("description");
if (existingDesc.length) {
existingDesc.replaceWith(updatedDescription);
} else {
$objectDOM.append(updatedDescription);
}
}
try {
//Update the instrumentation
let instrumentation = this.get("instrumentation");
$objectDOM.children("instrumentation").remove();
if (instrumentation && instrumentation.length) {
instrumentation.reverse().each((i) => {
let updatedI = document.createElement("instrumentation");
updatedI.textContent = i;
$objectDOM.children("description").after(updatedI);
});
}
} catch (e) {
console.error(
"Failed to serialize method step instrumentation, skipping. ",
e,
);
}
/** @todo: Update software and subSteps */
// Remove empty (zero-length or whitespace-only) nodes
$objectDOM
.find("*")
.filter(function () {
return $.trim(this.innerHTML) === "";
})
.remove();
return objectDOM;
} catch (e) {
console.error(
"Failed to update the EMLMethodStep. Won't serialize. ",
e,
);
return;
}
},
/**
* function isEmpty() - Will check if there are any values set on this model
* that are different than the default values and would be serialized to the EML.
*
* @return {boolean} - Returns true is this model is empty, false if not
*/
isEmpty: function () {
if (!this.get("description") || this.get("description").isEmpty()) {
return true;
}
},
/**
* Returns whether or not this Method Step is a custom one, which currently only applies to the description
* @returns {boolean}
*/
isCustom: function () {
return this.get("description")
? this.get("description").type == "EMLSpecializedText"
: false;
},
/**
* Overloads Backbone.Model.validate() to check if this model has valid values set on it
* @extends Backbone.Model.validate
* @returns {object}
*/
validate: function () {
try {
let validationErrors = {};
if (this.isCustom() && this.get("required")) {
let desc = this.get("description"),
isMissing = false;
//If there is a missing description, we need to show the required error
if (!desc) {
isMissing = true;
} else if (!desc.get("text")) {
isMissing = true;
} else if (!_.compact(desc.get("text")).length) {
isMissing = true;
}
if (isMissing) {
validationErrors.description = `${desc.get("title")} is required.`;
return validationErrors;
}
}
} catch (e) {
console.error("Error while validating the Methods: ", e);
return false;
}
},
trickleUpChange: function () {
MetacatUI.rootDataPackage.packageModel.set("changed", true);
},
formatXML: function (xmlString) {
return DataONEObject.prototype.formatXML.call(this, xmlString);
},
},
);
return EMLMethodStep;
},
);