define([
"underscore",
"jquery",
"backbone",
"models/metadata/eml211/EMLMethods",
"models/metadata/eml/EMLMethodStep",
"models/metadata/eml211/EMLText",
"models/metadata/eml/EMLSpecializedText",
"text!templates/metadata/EMLMethods.html",
], function (
_,
$,
Backbone,
EMLMethods,
EMLMethodStep,
EMLText,
EMLSpecializedText,
EMLMethodsTemplate,
) {
/**
* @class EMLMethodsView
* @classdesc The EMLMethods renders the content of an EMLMethods model
* @classcategory Views/Metadata
* @extends Backbone.View
*/
var EMLMethodsView = Backbone.View.extend(
/** @lends EMLMethodsView.prototype */ {
type: "EMLMethodsView",
tagName: "div",
className: "row-fluid eml-methods",
stepsContainerSelector: "#eml-method-steps-container",
editTemplate: _.template(EMLMethodsTemplate),
/**
* A small template to display each EMLMethodStep.
* If you are going to extend this template for a theme, note that:
* This template must keep the ".step-container" wrapper class.
* This template must keep the textarea with the default data attributes.
* The remove button must have a "remove" class
* @type {UnderscoreTemplate}
*/
stepTemplate: _.template(
'<div class="step-container">\
<h5>Step <span class="step-num"><%=num%></span></h5>\
<p class="notification" data-attribute="methodStepDescription"></p>\
<textarea data-attribute="methodStepDescription"\
data-step-attribute="description"\
rows="7" class="method-step"><%=text%></textarea>\
<i class="remove icon-remove"></i>\
</div>',
),
/**
* A reference to the EML211View that contains this EMLMethodsView.
* @type {EML211View}
*/
parentEMLView: null,
/**
* jQuery selector for the element that contains the Custom Methods
* @type {string}
*/
customMethodsSelector: ".custom-methods-container",
initialize: function (options) {
options = options || {};
this.isNew = options.isNew || (options.model ? false : true);
this.model = options.model || new EMLMethods();
this.edit = options.edit || false;
this.parentEMLView = options.parentEMLView || null;
this.$el.data({ model: this.model });
},
events: {
change: "updateModel",
"keyup .method-step.new": "renderNewMethodStep",
"click .remove": "removeMethodStep",
"mouseover .remove": "previewRemove",
"mouseout .remove": "previewRemove",
},
render: function () {
//Save the view and model on the element
this.$el
.data({
model: this.model,
view: this,
})
.attr("data-category", "methods");
if (this.edit) {
this.$el.html(
this.editTemplate({
studyExtentDescription: this.model.get("studyExtentDescription"),
samplingDescription: this.model.get("samplingDescription"),
}),
);
//Render each EMLMethodStep
let regularMethodSteps = this.model.getNonCustomSteps();
regularMethodSteps.forEach((step) => {
this.renderMethodStep(step);
});
//Create a blank step for the user to make a new one
this.renderMethodStep();
//Populate all the step numbers
this.updateMethodStepNums();
//Render the custom methods differently
this.renderCustomMethods();
}
return this;
},
/**
* Renders a single EMLMethodStep model
* @param {EMLMethodStep} [step]
* @since 2.19.0
*/
renderMethodStep: function (step) {
try {
let stepEl;
if (step) {
//Render the step HTML
stepEl = $(
this.stepTemplate({
text: step.get("description").toString(),
num: "",
}),
);
//Attach the model to the elements that will be interacted with
stepEl
.find("textarea[data-attribute='methodStepDescription'], .remove")
.data({ methodStepModel: step });
} else {
//Only one new method step should be displayed at the same time
if (this.$(".method-step.new").length) {
return;
}
//Render the step HTML
stepEl = $(
this.stepTemplate({
text: "",
num: "",
}),
);
stepEl
.find("textarea[data-attribute='methodStepDescription']")
.addClass("new");
}
//Add the step to the page
this.$(this.stepsContainerSelector).append(stepEl);
} catch (e) {
console.error("Failed to render a method step: ", e);
}
},
/**
* Renders the inputs for the custom EML Methods that are configured in the {@link AppConfig}
* If none are configured, nothing will be shown.
* @since 2.19.0
*/
renderCustomMethods: function () {
//Get the custom EML Methods that are configured in the AppConfig
let customMethodsOptions = MetacatUI.appModel.get("customEMLMethods");
//If there is at least one custom Method configured, proceed with rendering it
if (
Array.isArray(customMethodsOptions) &&
customMethodsOptions.length
) {
let view = this;
//Get the custom Methods template
require([
"text!templates/metadata/eml-custom-methods.html",
], function (CustomMethodsTemplate) {
try {
//Get the Methods from the EMLMethods model
let allMethodSteps = view.model.get("methodSteps"),
//Find the custom methods set on the model
allCustomMethods = allMethodSteps.filter((step) => {
return step.isCustom();
}),
//Start a literal object to send to the custom methods template
templateInfo = {};
//Add each custom method model to the template info
allCustomMethods.forEach((step) => {
templateInfo[step.get("customMethodID")] = step;
});
//Insert the custom methods template into the page
let customMethodsTemplate = _.template(CustomMethodsTemplate);
view
.$(view.customMethodsSelector)
.html(customMethodsTemplate(templateInfo));
//Attach each custom method model to it's textarea or input
allCustomMethods.forEach((step) => {
view
.$(view.customMethodsSelector)
.find(
"[data-custom-method-id='" +
step.get("customMethodID") +
"']",
)
.data({ methodStepModel: step });
});
//If this is inside a parent EML View (most likely), trigger the event
//that lets the parent view know that new editor components have been added to the page.
if (view.parentEMLView) {
view.parentEMLView.trigger("editorInputsAdded");
}
} catch (e) {
console.error("Couldn't show the custom EML Methods: ", e);
return;
}
});
}
},
updateModel: function (e) {
if (!e) return false;
var updatedInput = $(e.target);
//Get the attribute that was changed
var changedAttr = updatedInput.attr("data-attribute");
if (!changedAttr) return false;
// Method Step Descriptions are ordered arrays, so update them with special rules
if (changedAttr == "methodStepDescription") {
// Get the EMLMethodStep model
var methodStep = updatedInput.data("methodStepModel");
//If there is already an EMLMethodStep model created, then update it
if (methodStep) {
let desc = methodStep.get("description");
desc.setText(updatedInput.val());
} else {
//Create a new EMLMethodStep model
var newMethodStep = this.model.addMethodStep();
//Attach the model to the elements that will be interacted with
updatedInput
.parents(".step-container")
.find("textarea[data-attribute='methodStepDescription'], .remove")
.data({ methodStepModel: newMethodStep });
//Update the model with the textarea value
newMethodStep.get("description").setText(updatedInput.val());
}
// Trigger the change event manually because, without this, the change event
// never fires.
this.model.trigger("change:methodSteps");
}
//All other attributes on this model are updated differently
else {
//Get the EMLText model to update
var textModelToUpdate = this.model.get(changedAttr);
//Double-check that this is an EMLText model, then update it
if (
textModelToUpdate &&
typeof textModelToUpdate == "object" &&
textModelToUpdate.type == "EMLText"
) {
textModelToUpdate.setText(updatedInput.val());
}
//If there's no value set on this attribute yet, create a new EMLText model
else if (!textModelToUpdate) {
let textType;
switch (changedAttr) {
case "studyExtentDescription":
textType = "description";
break;
case "samplingDescription":
textType = "samplingdescription";
break;
}
if (!textType) return;
//Create a new EMLText model
var newTextModel = new EMLText({
type: textType,
parentModel: this.model,
});
//Update the model with the textarea value
newTextModel.setText(updatedInput.val());
//Set the EMLText model on the EMLMethods model
this.model.set(changedAttr, newTextModel);
}
}
//Show the remove button
$(e.target).parents(".step-container").find(".remove").show();
},
/**
* Renders a new empty method step input. Does not update the model at all.
*/
renderNewMethodStep: function () {
// Add new textareas as needed
this.$(".method-step.new").removeClass("new");
this.renderMethodStep();
this.updateMethodStepNums();
},
/**
* Remove this method step
* @param {Event} e
*/
removeMethodStep: function (e) {
try {
//Get the EMLMethodStep
var step = $(e.target).data("methodStepModel");
//Exit if there is no EMLMethodStep
if (!step) {
return;
}
//Remove this step from the model
this.model.removeMethodStep(step);
//Remove the step elements from the page
let view = this;
$(e.target)
.parent(".step-container")
.slideUp("fast", function () {
this.remove();
//Bump down all the step numbers
view.updateMethodStepNums();
});
} catch (e) {
console.error("Failed to remove the EML Method Step: ", e);
}
},
/**
* Updates the step number in the view for each step
* @since 2.19.0
*/
updateMethodStepNums: function () {
//Update all the step numbers
this.$(".step-num").each((i, numEl) => {
numEl.textContent = i + 1;
});
},
/**
* Shows validation errors that need to be fixed by the user
*/
showValidation: function () {
try {
if (Object.keys(this.model.validationError).length) {
if (this.model.validationError.methodSteps) {
//A general error about all method steps will just be a string.
//Apply the error styling to all the elements for the method steps
if (typeof this.model.validationError.methodSteps == "string") {
this.$('.notification[data-attribute="methodStepDescription"]')
.text(this.model.validationError.methodSteps)
.addClass("error");
this.$(
'[data-attribute="methodStepDescription"]:not([data-custom-method-id])',
).addClass("error");
}
//Validation errors that aren't strings are errors about specific
// Custom EML Method Steps.
else {
_.mapObject(
this.model.validationError.methodSteps,
(errors, customMethodID) => {
this.$(`.notification[data-category="${customMethodID}"]`)
.text(errors.description)
.addClass("error");
this.$(
`[data-custom-method-id="${customMethodID}"]`,
).addClass("error");
},
);
}
}
}
} catch (e) {
console.warn("Failed to show Methods validation: ", e);
}
},
previewRemove: function (e) {
$(e.target).parents(".step-container").toggleClass("remove-preview");
},
},
);
return EMLMethodsView;
});