/* global define */
define(['underscore', 'jquery', 'backbone',
'models/DataONEObject',
'models/metadata/eml211/EMLMeasurementScale',
'text!templates/metadata/eml-measurement-scale.html',
'text!templates/metadata/codelist-row.html',
'text!templates/metadata/nonNumericDomain.html',
'text!templates/metadata/textDomain.html'],
function(_, $, Backbone, DataONEObject, EMLMeasurementScale,
EMLMeasurementScaleTemplate, CodeListRowTemplate, NonNumericDomainTemplate, TextDomainTemplate){
/**
* @class EMLMeasurementScaleView
* @classdesc An EMLMeasurementScaleView displays the info about one the measurement scale or category of an eml attribute
* @classcategory Views/Metadata
* @extends Backbone.View
*/
var EMLMeasurementScaleView = Backbone.View.extend(
/** @lends EMLMeasurementScaleView.prototype */{
tagName: "div",
className: "eml-measurement-scale",
id: null,
/* The HTML template for a measurement scale */
template: _.template(EMLMeasurementScaleTemplate),
codeListRowTemplate: _.template(CodeListRowTemplate),
nonNumericDomainTemplate: _.template(NonNumericDomainTemplate),
textDomainTemplate: _.template(TextDomainTemplate),
/* Events this view listens to */
events: {
"click .category" : "switchCategory",
"change .datetime-string" : "toggleCustomDateTimeFormat",
"change .possible-text" : "toggleNonNumericDomain",
"keyup .new .codelist" : "addNewCodeRow",
"click .code-row .remove" : "removeCodeRow",
"mouseover .code-row .remove" : "previewCodeRemove",
"mouseout .code-row .remove" : "previewCodeRemove",
"change .units" : "updateModel",
"change .datetime" : "updateModel",
"change .codelist" : "updateModel",
"change .textDomain" : "updateModel",
"focusout .code-row" : "showValidation",
"focusout .units.input" : "showValidation"
},
initialize: function(options){
if(!options)
var options = {};
this.isNew = (options.isNew === true) ? true : this.model? false : true;
this.model = options.model || EMLMeasurementScale.getInstance();
this.parentView = options.parentView || null;
},
render: function(){
//Render the template
var viewHTML = this.template(this.model.toJSON());
if(this.isNew)
this.$el.addClass("new");
//Insert the template HTML
this.$el.html(viewHTML);
//Render any nonNumericDomain models
this.$(".non-numeric-domain").append( this.nonNumericDomainTemplate(this.model.get("nonNumericDomain")) );
//Render the text domain choices and details
this.$(".text-domain").html( this.textDomainTemplate() );
//If this attribute is already defined as nonNumericDomain, then fill in the metadata
_.each(this.model.get("nonNumericDomain"), function(domain){
var nominalTextDomain = this.$(".nominal-options .text-domain"),
ordinalTextDomain = this.$(".ordinal-options .text-domain");
if(domain.textDomain){
if(this.model.get("measurementScale") == "nominal"){
nominalTextDomain.html( this.textDomainTemplate(domain.textDomain) );
}
else{
ordinalTextDomain.html( this.textDomainTemplate(domain.textDomain) );
}
}
else if(domain.enumeratedDomain){
this.renderCodeList(domain.enumeratedDomain);
}
}, this);
//Add the new code rows in the code list table
this.addNewCodeRow("nominal");
this.addNewCodeRow("ordinal");
},
postRender: function(){
//Determine which category to select
//Interval measurement scales will be displayed as ratio
var selectedCategory = this.model.get("measurementScale") == "interval" ? "ratio" : this.model.get("measurementScale");
//Set the category
this.$(".category[value='" + selectedCategory + "']").prop("checked", true);
this.switchCategory();
this.renderUnitDropdown();
this.chooseDateTimeFormat();
this.chooseNonNumericDomain();
},
/*
* Render the table of code definitions from the enumeratedDomain node of the EML
*/
renderCodeList: function(codeList){
var scaleType = this.model.get("measurementScale"),
$container = this.$("." + scaleType + "-options .enumeratedDomain.non-numeric-domain-type .table");
_.each(codeList.codeDefinition, function(definition){
var row = this.codeListRowTemplate(definition);
//Add the row to the table
$container.append(row);
}, this);
},
showValidation: function(e){
//Reset the error messages and styling
this.$(".error").removeClass("error");
this.$(".notification").text("");
//If the measurement scale model is NOT valid
if( !this.$(".category:checked").length ){
this.$(".category-container")
.addClass("error")
.find(".notification")
.text("Choose a category")
.addClass("error");
//Trigger the invalid event on the attribute model
this.model.get("parentModel").trigger("invalid", this.model.get("parentModel"));
}
else if( !this.model.isValid() ){
//Get the errors
var errors = this.model.validationError,
modelType = this.model.get("measurementScale");
//Display error messages for each type of error
_.each(Object.keys(errors), function(attr){
//If this is an enumeratedDomain error
if(attr == "enumeratedDomain"){
var view = this;
//Give the user a few milliseconds to focus on a new element
setTimeout(function(){
//Highlight the inputs in code rows that are empty
var emptyInputs = view.$("." + modelType + "-options .codelist.input")
.not(document.activeElement)
.filter(function(){
if( $(this).val() ) return false;
else return true;
});
emptyInputs.addClass("error");
if(emptyInputs.length)
view.$("." + modelType + "-options [data-category='enumeratedDomain'] .notification").text(errors[attr]).addClass("error");
}, 200);
}
//For all other attributes, just display the errors the same way
else{
this.$("." + modelType + "-options [data-category='" + attr + "'] .notification").text(errors[attr]).addClass("error");
this.$("." + modelType + "-options .input[data-category='" + attr + "']").addClass("error");
}
//Highlight the border of the non numeric domain container
if(attr == "nonNumericDomain"){
this.$("." + modelType + "-options.non-numeric-domain").addClass("error");
}
}, this);
//Trigger the invalid event on the attribute model
// this.model.get("parentModel").trigger("invalid", this.model.get("parentModel"));
}
else{
//Trigger the valid event on the attribute model
// this.model.get("parentModel").trigger("valid", this.model.get("parentModel"));
}
},
switchCategory: function(){
//Switch the category in the view
var chosenCategory = this.$("input[name='measurementScale']:checked").val();
//Show the new category options
this.$(".options").hide();
this.$("." + chosenCategory + "-options.options").show();
//Get the current category
var modelCategory = this.model.get("measurementScale");
//Get the parent attribute model
var parentEMLAttrModel = this.model.get("parentModel");
//Switch the model type, if needed
if(chosenCategory && (modelCategory != chosenCategory) && !(modelCategory == "interval" && chosenCategory == "ratio")){
var newModel;
if(typeof this.modelCache != "object"){
this.modelCache = {};
}
//Get the model type from this view's cache
if(this.modelCache[chosenCategory])
newModel = this.modelCache[chosenCategory];
else if( chosenCategory == "ratio" && this.modelCache["interval"] )
newModel = this.modelCache["interval"];
//Get a new model instance based on the type
else
newModel = EMLMeasurementScale.getInstance(chosenCategory);
//Save this model for later in case the user switches back
if(modelCategory)
this.modelCache[modelCategory] = this.model;
//save the new model
this.model = newModel;
//Set references to and from this model and the parent attribute model
this.model.set("parentModel", parentEMLAttrModel);
parentEMLAttrModel.set("measurementScale", this.model);
//Update the codelist values, if needed
if(chosenCategory == "nominal" || chosenCategory == "ordinal" &&
this.model.get("nonNumericDomain").length &&
this.model.get("nonNumericDomain")[0].enumeratedDomain){
this.updateCodeList();
}
}
},
renderUnitDropdown: function(){
if(this.$("select.units").length) return;
//Create a dropdown menu
var select = $(document.createElement("select"))
.addClass("units full-width input")
.attr("data-category", "unit");
var eml = this.model.getParentEML();
//Get the units collection or wait until it has been fetched
if(!eml.units.length){
this.listenTo(eml.units, "sync", this.renderUnitDropdown);
return;
}
//Create a default option
var defaultOption = $(document.createElement("option"))
.text("Choose a standard unit");
select.append(defaultOption);
//Create an "Other" option to show at the top
var otherOption = $(document.createElement("option"))
.text("Other / None")
.attr("value", "dimensionless");
select.append(otherOption);
//Create each unit option in the unit dropdown
eml.units.each(function(unit){
var option = $(document.createElement("option"))
.val(unit.get("_name"))
.text(unit.get("_name").charAt(0).toUpperCase() +
unit.get("_name").slice(1) +
" (" + unit.get("description") + ")")
.data({ model: unit });
select.append(option);
}, this);
//Add the dropdown to the page
this.$(".units-container").append(select);
//Select the unit from the EML, if there is one
var currentUnit = this.model.get("unit");
if(currentUnit && currentUnit.standardUnit){
//Get the dropdown for this measurement scale
// (We default interval to ratio in the editor)
var currentDropdown = this.$(".ratio-options select");
//Select the unit from the EML
currentDropdown.val(currentUnit.standardUnit);
}
//If this unit is a custom unit
else if( currentUnit && currentUnit.customUnit ){
//Create an <option> for this custom unit
var customUnitOption = $(document.createElement("option"))
.val( currentUnit.customUnit )
.text( currentUnit.customUnit )
.addClass("custom");
//Add it to the <select> and select it as the active option
select.append(customUnitOption)
.val(currentUnit.customUnit);
}
},
/*
* Chooses the date-time format from the dropdown menu
*/
chooseDateTimeFormat: function(){
if(this.model.type == "EMLDateTimeDomain"){
var formatString = this.model.get("formatString");
//Go back to the default option when the model isn't set yet
if(!formatString){
var options = this.$("select.datetime-string option");
this.$("select.datetime-string").val(options.first().val());
return;
}
var matchingOption = this.$("select.datetime-string [value='" + formatString + "']");
if(matchingOption.length){
this.$("select.datetime-string").val(formatString);
this.$(".datetime-string-custom-container").hide();
}
else{
this.$("select.datetime-string").val("custom");
this.$(".datetime-string-custom").val(formatString);
this.$(".datetime-string-custom-container").show();
}
}
},
toggleCustomDateTimeFormat: function(e){
var choice = this.$("select.datetime-string").val();
if(choice == "custom"){
this.$(".datetime-string-custom-container").show();
}
else{
this.$(".datetime-string-custom-container").hide();
}
},
chooseNonNumericDomain: function(){
if(this.model.get("nonNumericDomain") && this.model.get("nonNumericDomain").length){
//Hide all the details first
this.$(".non-numeric-domain-type").hide();
//Get the domain from the model
var domain = this.model.get("nonNumericDomain")[0];
//If the domain type is text, select it and show it
if( domain.textDomain ){
//If the pattern is just a wildcard, then check the "anything" radio button
if(domain.textDomain.pattern && domain.textDomain.pattern.length && domain.textDomain.pattern[0] == "*")
this.$("." + this.model.get("measurementScale") + "-options .possible-text[value='anything']").prop("checked", true);
//Otherwise, check the pattern radio button
else{
this.$("." + this.model.get("measurementScale") + "-options .possible-text[value='pattern']").prop("checked", true);
this.$("." + this.model.get("measurementScale") + "-options .non-numeric-domain-type.pattern").show();
}
}
//If the domain type is a code list, select it and show it
else if( domain.enumeratedDomain ){
this.$("." + this.model.get("measurementScale") + "-options .possible-text[value='enumeratedDomain']").prop("checked", true);
this.$(".non-numeric-domain-type.enumeratedDomain").show();
}
}
},
toggleNonNumericDomain: function(e){
//Hide the domain type details
this.$(".non-numeric-domain-type").hide();
//Get the new value selected
var value = this.$(".non-numeric-domain .possible-text:checked").val();
var activeScale = this.$(".nominal-options").is(":visible")? "nominal" : "ordinal";
//Show the form elements for that non numeric type
this.$("." + activeScale + "-options .non-numeric-domain-type." + value).show();
this.updateModel(e);
},
addNewCodeRow: function(e){
if(typeof e == "object"){
var $row = $(e.target).parents(".code-row"),
code = $row.find(".code").val(),
definition = $row.find(".definition").val();
//Only add a row when there is a value for the code and code definition
if(!code || !definition) return false;
$row.removeClass("new");
var newRow = this.addCodeRow();
}
else if(typeof e == "string"){
var newRow = this.addCodeRow(e);
}
newRow.addClass("new");
},
addCodeRow: function(scaleType){
if(!scaleType)
var scaleType = this.model.get("measurementScale");
var $container = this.$("." + scaleType + "-options .enumeratedDomain.non-numeric-domain-type .table");
//Create a code list row from the template
var row = $(this.codeListRowTemplate({ code: "", definition: ""}));
$container.append(row);
return row;
},
removeCodeRow: function(e){
var codeRow = $($(e.target).parents(".code-row")),
allRows = codeRow.parents(".enumerated-domain").find(".code-row"),
index = allRows.index(codeRow);
this.model.removeCode(index);
codeRow.remove();
this.showValidation();
this.parentView.showValidation();
},
/*
* When the user changes the value of the form, update the model
*/
updateModel: function(e){
var updatedInput = $(e.target);
var emlModel = this.model.getParentEML();
//Update the standard unit
if(updatedInput.is(".units")){
var chosenUnit = updatedInput.val(),
chosenOption = updatedInput.children("[value='" + chosenUnit + "']");
if( chosenOption.is(".custom") ){
this.model.set("unit", {customUnit: chosenUnit});
}
else{
this.model.set("unit", {standardUnit: chosenUnit});
}
// Hard-code the numberType for now
this.model.set("numericDomain", {numberType: "real"});
//Trickle up the change to the most parent-level metadata model
this.model.trickleUpChange();
}
//Update the datetime format
else if(updatedInput.is(".datetime")){
var format = emlModel? emlModel.cleanXMLText( updatedInput.val() ) : updatedInput.val();
if(format == "custom"){
format = emlModel? emlModel.cleanXMLText( this.$(".datetime-string-custom").val() ) : this.$(".datetime-string-custom").val();
}
//If no format string was provided, then set the default value
if( typeof format == "string" && !format.trim().length )
this.model.set("formatString", this.model.defaults().formatString);
else
this.model.set("formatString", format);
}
else if(updatedInput.is(".possible-text")){
var possibleText = emlModel? emlModel.cleanXMLText( updatedInput.val() ) : updatedInput.val();
if(possibleText == "enumeratedDomain"){
//Update the code list
this.updateCodeList();
}
else if(possibleText == "pattern"){
if(!this.model.get("nonNumericDomain").length || !this.model.get("nonNumericDomain")[0].textDomain){
var textDomain = {
definition: null,
pattern: [],
source: null
}
this.model.set("nonNumericDomain", [{ textDomain: textDomain }]);
}
else{
//Get the value of the text input fields for the definition and pattern
var definition = this.$("." + this.model.get("measurementScale") + "-options .textDomain[data-category='definition']").val(),
pattern = this.$("." + this.model.get("measurementScale") + "-options .textDomain[data-category='pattern']").val();
definition = emlModel? emlModel.cleanXMLText( definition ) : definition;
pattern = emlModel? emlModel.cleanXMLText( pattern ) : pattern;
// If the pattern is an empty string, then set an empty array on the model
if( typeof pattern == "string" && !pattern.trim().length ){
pattern = new Array();
}
// For all other values, put it in an array
else {
pattern = [pattern];
}
// If the definition is a string of space characters, then set it to an empty string
if( typeof definition == "string" && !definition.trim().length ){
definition = "";
}
var textDomain = {
definition: definition,
pattern: pattern,
source: null
}
this.model.set("nonNumericDomain", [{ textDomain: textDomain }]);
}
}
else if(possibleText == "anything"){
var textDomain = {
definition: "Any text",
pattern: ["*"],
source: null
}
this.model.set("nonNumericDomain", [{ textDomain: textDomain }]);
}
}
else if(updatedInput.is(".textDomain")){
// If there is no nonNumericDomain object set on the model, create a new empty one
if(typeof this.model.get("nonNumericDomain")[0] != "object"){
this.model.get("nonNumericDomain")[0] = { textDomain: { definition: null, pattern: [], source: null } };
}
//Get the textDomain object
var textDomain = this.model.get("nonNumericDomain")[0].textDomain;
//If the text definition was updated...
if(updatedInput.attr("data-category") == "definition"){
//Get the value that was input by the user
var definition = emlModel? emlModel.cleanXMLText( updatedInput.val() ) : updatedInput.val();
// If the definition is a string of space characters, then set it to an empty string
if( typeof definition == "string" && !definition.trim().length ){
definition = "";
}
//Update the textDomain object
textDomain.definition = definition;
}
//If the text pattern was updated...
else if(updatedInput.attr("data-category") == "pattern"){
//Get the value that was input by the user
var pattern = emlModel? emlModel.cleanXMLText( updatedInput.val() ) : updatedInput.val();
// If the pattern is a string of space characters, then set it to an empty string
if( typeof pattern == "string" && !pattern.trim().length ){
textDomain.pattern = [];
}
//Put the value inside a new array and update the textDomain object
else{
textDomain.pattern = [pattern];
}
}
//Manually trigger a change on the nonNumericDomain attribute
this.model.trigger("change:nonNumericDomain");
}
else if(updatedInput.is(".codelist")){
var row = updatedInput.parents(".code-row"),
index = this.$("." + this.model.get("measurementScale") + "-options .code-row").index(row);
this.updateCodeList(index);
}
//Add this EMLMeasurementScale model to the EMLAttribute model when it is updated in the view
var attributeModel = this.model.get("parentModel");
if( attributeModel )
attributeModel.set("measurementScale", this.model);
},
updateCodeList: function(rowNum){
//If the model is not set as an enumerated domain yet
if(!this.model.get("nonNumericDomain").length ||
!this.model.get("nonNumericDomain")[0] ||
!this.model.get("nonNumericDomain")[0].enumeratedDomain){
var isEmpty = false;
var emlModel = this.model.getParentEML();
//Go through each code row in this view and grab the values
_.each(this.$("." + this.model.get("measurementScale") + "-options .code-row"), function(row, i, rows){
var $row = $(row),
code = $row.find(".code").val(),
def = $row.find(".definition").val();
code = emlModel? emlModel.cleanXMLText( code ) : code;
def = emlModel? emlModel.cleanXMLText( def ) : def;
//Update the enumerated domain with this code
if(code || def){
this.model.updateEnumeratedDomain(code, def, i);
}
//If there is only one row and it has no code or definition,
//then this is an empty code list
else if( rows.length == 1 && i == 0){
isEmpty = true;
}
}, this);
//If there are no codes in the list, update the enumerated domain with blank values
if( isEmpty ){
this.model.updateEnumeratedDomain(null, null, rowNum);
}
}
else if(rowNum > -1){
var $row = $(this.$("." + this.model.get("measurementScale") + "-options .code-row")[rowNum]),
code = $row.find(".code").val(),
def = $row.find(".definition").val();
code = emlModel? emlModel.cleanXMLText( code ) : code;
def = emlModel? emlModel.cleanXMLText( def ) : def;
if(code || def){
this.model.updateEnumeratedDomain(code, def, rowNum);
}
}
},
previewCodeRemove: function(e){
$(e.target).parents(".code-row").toggleClass("remove-preview");
}
});
return EMLMeasurementScaleView;
});