Source: src/js/views/metadata/EMLMeasurementScaleView.js

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