Source: js/views/metadata/EMLEntityView.js

/* global define */
define(['underscore', 'jquery', 'backbone', 'localforage',
        'models/DataONEObject', 'models/metadata/eml211/EMLAttribute', 'models/metadata/eml211/EMLEntity',
        'views/DataPreviewView',
        'views/metadata/EMLAttributeView',
        'text!templates/metadata/eml-entity.html',
        'text!templates/metadata/eml-attribute-menu-item.html'],
    function(_, $, Backbone, LocalForage, DataONEObject, EMLAttribute, EMLEntity,
    		DataPreviewView,
    		EMLAttributeView,
    		EMLEntityTemplate,
    		EMLAttributeMenuItemTemplate){

        /**
        * @class EMLEntityView
        * @classdesc An EMLEntityView shows the basic attributes of a DataONEObject, as described by EML
        */
        var EMLEntityView = Backbone.View.extend(
          /** @lends EMLEntityView.prototype */{

            tagName: "div",

            className: "eml-entity modal hide fade",

            id: null,

            /* The HTML template for an entity */
            template: _.template(EMLEntityTemplate),
            attributeMenuItemTemplate: _.template(EMLAttributeMenuItemTemplate),

            /* Events this view listens to */
            events: {
            	"change" : "saveDraft",
            	"change input" : "updateModel",
            	"change textarea" : "updateModel",
            	"click .nav-tabs a" : "showTab",
            	"click .attribute-menu-item" : "showAttribute",
            	"mouseover .attribute-menu-item .remove" : "previewAttrRemove",
            	"mouseout .attribute-menu-item .remove"  : "previewAttrRemove",
            	"click .attribute-menu-item .remove" : "removeAttribute"
            },

            initialize: function(options){
            	if(!options)
            		var options = {};

            	this.model = options.model || new EMLEntity();
            	this.DataONEObject = options.DataONEObject;
            },

            render: function(){

            	this.renderEntityTemplate();

            	this.renderPreview();

            	this.renderAttributes();

              this.listenTo(this.model, "invalid", this.showValidation);
              this.listenTo(this.model, "valid", this.showValidation);

            },

            renderEntityTemplate: function(){
            	var modelAttr = this.model.toJSON();

            	if(!modelAttr.entityName)
            		modelAttr.title = "this data";
            	else
            		modelAttr.title = modelAttr.entityName;

            	modelAttr.uniqueId = this.model.cid;

            	this.$el.html(this.template( modelAttr ));

            	//Initialize the modal window
            	this.$el.modal();


       		  	//Set the menu height
            	var view = this;
       		  	this.$el.on("shown", function(){
       		  		view.adjustHeight();
       		  		view.setMenuWidth();

       		  		window.addEventListener('resize', function(event){
	       		  		view.adjustHeight();
	       		  		view.setMenuWidth();
	       		  	});
       		  	});

              this.$el.on("hidden", function(){
                view.showValidation();
              });

            },

            renderPreview: function(){
            	//Get the DataONEObject model
            	if(this.DataONEObject){
            		var dataPreview = new DataPreviewView({
            			model: this.DataONEObject
            		});
            		dataPreview.render();
            		this.$(".preview-container").html(dataPreview.el);

            		if(dataPreview.$el.children().length){
            			this.$(".description").css("width", "calc(100% - 310px)");
            		}
            		else
            			dataPreview.$el.remove();
            	}
            },

            renderAttributes: function(){
            	//Render the attributes
            	var attributes      = this.model.get("attributeList"),
            		attributeListEl = this.$(".attribute-list"),
            		attributeMenuEl = this.$(".attribute-menu");

            	_.each(attributes, function(attr){

            		//Create an EMLAttributeView
            		var view = new EMLAttributeView({
            			model: attr
            		});

            		//Create a link in the attribute menu
            		var menuItem = $(this.attributeMenuItemTemplate({
	            			attrId: attr.cid,
	            			attributeName: attr.get("attributeName"),
	            			classes: ""
	            		})).data({
							model: attr,
							attributeView: view
							});
            		attributeMenuEl.append(menuItem);
            		menuItem.find(".tooltip-this").tooltip();

            		this.listenTo(attr, "change:attributeName", function(attr){
            			menuItem.find(".name").text(attr.get("attributeName"));
            		});

            		view.render();

            		attributeListEl.append(view.el);

            		view.$el.hide();

            		this.listenTo(attr, "change",  this.addAttribute);
            		this.listenTo(attr, "invalid", this.showAttributeValidation);
            		this.listenTo(attr, "valid",   this.hideAttributeValidation);

            	}, this);

            	//Add a new blank attribute view at the end
            	this.addNewAttribute();

            	//If there are no attributes in this EML model yet,
            	//then make sure we show a new add attribute button when the user starts typing
            	if(attributes.length == 0){
            		var onlyAttrView = this.$(".attribute-menu-item").first().data("attributeView"),
            			view = this,
            			keyUpCallback = function(){
            				//This attribute is no longer new
            				view.$(".attribute-menu-item").first().removeClass("new");

            				//Add a new attribute link and view
            				view.addNewAttribute();

            				//Don't listen to keyup anymore
            				onlyAttrView.$el.off("keyup", keyUpCallback);
            			};

            		onlyAttrView.$el.on("keyup", keyUpCallback);
            	}

        		//Activate the first navigation item
        		var firstAttr = this.$(".side-nav-item").first();
        		firstAttr.addClass("active");

        		//Show the first attribute view
        		firstAttr.data("attributeView").$el.show();

        		firstAttr.data("attributeView").postRender();

            },

            updateModel: function(e){
            	var changedAttr = $(e.target).attr("data-category");

            	if(!changedAttr) return;

              var emlModel = this.model.getParentEML(),
                  newValue = emlModel? emlModel.cleanXMLText($(e.target).val()) : $(e.target).val();

            	this.model.set(changedAttr, newValue);

              this.model.trickleUpChange();

            },

            addNewAttribute: function(){
            	var newAttrModel = new EMLAttribute({
            			parentModel: this.model,
                        xmlID: DataONEObject.generateId()
            		}),
            		newAttrView  = new EMLAttributeView({
            			isNew: true,
            			model: newAttrModel
            		});

            	newAttrView.render();
            	this.$(".attribute-list").append(newAttrView.el);
            	newAttrView.$el.hide();

            	//Change the last menu item if it still says "Add attribute"
            	if(this.$(".attribute-menu-item").length == 1){
            		var firstAttrMenuItem = this.$(".attribute-menu-item").first();

            		if( firstAttrMenuItem.find(".name").text() == "Add attribute" ){
            			firstAttrMenuItem.find(".name").text("New attribute");
            			firstAttrMenuItem.find(".add").hide();
            		}
            	}

            	//Create the new menu item
            	var menuItem = $(this.attributeMenuItemTemplate({
	            		attrId: newAttrModel.cid,
	            		attributeName: "Add attribute",
	            		classes: "new"
	            	})).data({
						model: newAttrModel,
						attributeView: newAttrView
					});
            	menuItem.find(".add").show();
            	this.$(".attribute-menu").append(menuItem);
            	menuItem.find(".tooltip-this").tooltip();

            	//When the attribute name is changed, update the navigation
        		this.listenTo(newAttrModel, "change:attributeName", function(attr){
        			menuItem.find(".name").text(attr.get("attributeName"));
        			menuItem.find(".add").hide();
        		});

            	this.listenTo(newAttrModel, "change",  this.addAttribute);
            	this.listenTo(newAttrModel, "invalid", this.showAttributeValidation);
            	this.listenTo(newAttrModel, "valid",   this.hideAttributeValidation);
            },

            addAttribute: function(emlAttribute){
            	//Add the attribute to the attribute list in the EMLEntity model
            	if( !_.contains(this.model.get("attributeList"), emlAttribute) )
            		this.model.addAttribute(emlAttribute);
            },

            removeAttribute: function(e){
            	var removeBtn = $(e.target);

            	var menuItem  = removeBtn.parents(".attribute-menu-item"),
            		attrModel = menuItem.data("model");

            	if(attrModel){
            		//Remove the attribute from the model
            		this.model.removeAttribute(attrModel);

            		//If this menu item is active, then make the next attribute active instead
            		if(menuItem.is(".active")){
            			var nextMenuItem = menuItem.next();

            			if(!nextMenuItem.length || nextMenuItem.is(".new")){
            				nextMenuItem = menuItem.prev();
            			}

            			if(nextMenuItem.length){
	            			nextMenuItem.addClass("active");

	            			this.showAttribute(nextMenuItem.data("model"));
            			}
            		}

            		//Remove the elements for this attribute from the page
            		menuItem.remove();
	            	this.$(".eml-attribute[data-attribute-id='" + attrModel.cid + "']").remove();
	            	$(".tooltip").remove();

	            	this.model.trickleUpChange();
            	}
            },

            adjustHeight: function(e){
            	var contentAreaHeight = this.$(".modal-body").height() - this.$(".nav-tabs").height();

            	this.$(".attribute-menu, .attribute-list").css("height", contentAreaHeight + "px");
            },

            setMenuWidth: function(){

            	this.$(".entity-container .nav").width( this.$el.width() );

            },

            /*
             * Shows the attribute in the attribute editor
             * Param e - JS event or attribute model
             */
            showAttribute: function(e){

            	if(e.target){
                   	var clickedEl = $(e.target),
                   		  menuItem = clickedEl.is(".attribute-menu-item") || clickedEl.parents(".attribute-menu-item");

                	if(clickedEl.is(".remove"))
                		return;
            	}
            	else{
            		var menuItem = this.$(".attribute-menu-item[data-attribute-id='" + e.cid + "']");
            	}

            	if(!menuItem)
            		return;

              //Validate the previously edited attribute
              //Get the current active attribute
              var activeAttrTab = this.$(".attribute-menu-item.active");

              //If there is a currently-active attribute tab,
              if( activeAttrTab.length ){
                //Get the attribute list from this view's model
                var emlAttributes = this.model.get("attributeList");

                //If there is an EMLAttribute list,
                if( emlAttributes && emlAttributes.length ){

                  //Get the active EMLAttribute
                  var activeEMLAttribute = _.findWhere(emlAttributes, { cid: activeAttrTab.attr("data-attribute-id") });

                  //If there is an active EMLAttribute model, validate it
                  if( activeEMLAttribute ){
                    activeEMLAttribute.isValid();
                  }

                }

              }

            	//If the user clicked on the add attribute link
            	if( menuItem.is(".new") && this.$(".new.attribute-menu-item").length < 2 ){

            		//Change the attribute menu item
            		menuItem.removeClass("new").find(".name").text("New attribute");
            		this.$(".eml-attribute.new").removeClass("new");
            		menuItem.find(".add").hide();

            		//Add a new attribute view and menu item
            		this.addNewAttribute();

                //Scroll the attribute menu to the bottom so that the "Add New" button is always visible
                var attrMenuHeight = this.$(".attribute-menu").scrollTop() + this.$(".attribute-menu").height();
                this.$(".attribute-menu").scrollTop( attrMenuHeight );
            	}

            	//Get the attribute view
            	var attrView = menuItem.data("attributeView");

            	//Change the active attribute in the menu
            	this.$(".attribute-menu-item.active").removeClass("active");
            	menuItem.addClass("active");

            	//Hide the old attribute view
            	this.$(".eml-attribute").hide();
            	//Show the new attribute view
            	attrView.$el.show();

              //Scroll to the top of the attribute view
              this.$(".attribute-list").scrollTop(0);

            	attrView.postRender();
            },

            /*
             * Show the attribute validation errors in the attribute navigation menu
             */
            showAttributeValidation: function(attr){

            	var attrLink = this.$(".attribute-menu-item[data-attribute-id='" + attr.cid + "']").find("a");

            	//If the validation is already displayed, then exit
            	if(attrLink.is(".error")) return;

            	var errorIcon = $(document.createElement("i")).addClass("icon icon-exclamation-sign error icon-on-left");

            	attrLink.addClass("error").prepend(errorIcon);
            },

            /*
             * Hide the attribute validation errors from the attribute navigation menu
             */
            hideAttributeValidation: function(attr){
            	this.$(".attribute-menu-item[data-attribute-id='" + attr.cid + "']")
            		.find("a").removeClass("error").find(".icon.error").remove();
            },

            /*
             * Show the user what will be removed when this remove button is clicked
             */
            previewAttrRemove: function(e){
            	var removeBtn = $(e.target);

            	removeBtn.parents(".attribute-menu-item").toggleClass("remove-preview");
            },

            /*
            * function showValidation
            *
            * Will display validation styling and messaging. Should be called after
            * this view's model has been validated and there are error messages to display
            */
            showValidation: function(){

              //Reset the error messages and styling
              //Only change elements inside the overview-container which contains only the
              // EMLEntity metadata. The Attributes will be changed by the EMLAttributeView.
    					this.$(".overview-container .notification").text("");
              this.$(".overview-tab .icon.error, .attributes-tab .icon.error").remove();
              this.$(".overview-container, .overview-tab a, .attributes-tab a, .overview-container .error").removeClass("error");

              var overviewTabErrorIcon  = false,
                  attributeTabErrorIcon = false;

              _.each( this.model.validationError, function(errorMsg, category){

                if( category == "attributeList" ){

                  //Create an error icon for the Attributes tab
                  if( !attributeTabErrorIcon ){
                    var errorIcon = $(document.createElement("i"))
                                      .addClass("icon icon-on-left icon-exclamation-sign error")
                                      .attr("title", "There is missing information in this tab");

                    //Add the icon to the Overview tab
                    this.$(".attributes-tab a").prepend(errorIcon).addClass("error");
                  }

                  return;
                }

                //Get all the elements for this category and add the error class
                this.$(".overview-container [data-category='" + category + "']").addClass("error");
                //Get the notification element for this category and add the error message
                this.$(".overview-container .notification[data-category='" + category + "']").text(errorMsg);

                //Create an error icon for the Overview tab
                if( !overviewTabErrorIcon ){
                  var errorIcon = $(document.createElement("i"))
                                    .addClass("icon icon-on-left icon-exclamation-sign error")
                                    .attr("title", "There is missing information in this tab");

                  //Add the icon to the Overview tab
                  this.$(".overview-tab a").prepend(errorIcon).addClass("error");

                  overviewTabErrorIcon = true;
                }

              }, this);

            },

            /*
             * Show the entity overview or attributes tab
             * depending on the click target
             */
            showTab: function(e){
            	e.preventDefault();

            	//Get the clicked link
       		  	var link = $(e.target);

       		  	//Remove the active class from all links and add it to the new active link
       		  	this.$(".nav-tabs li").removeClass("active");
       		  	link.parent("li").addClass("active");

       		  	//Hide all the panes and show the correct one
       		  	this.$(".tab-pane").hide();
       		  	this.$(link.attr("href")).show();

            },

            /*
             * Show the entity in a modal dialog
             */
            show: function(){

            	this.$el.modal('show');

            },

            /*
             * Hide the entity modal dialog
             */
            hide: function(){
            	this.$el.modal('hide');
            },

            /**
             * Save a draft of the parent EML model
             */
            saveDraft: function() {
            	var view = this;

            	try {
            		var model = this.model.getParentEML();
            		var title = model.get("title") || "No title";

            		LocalForage.setItem(model.get("id"),
            		{
            			id: model.get("id"),
            			datetime: (new Date()).toISOString(),
            			title: Array.isArray(title) ? title[0] : title,
            			draft: model.serialize()
            		}).then(function() {
            			view.clearOldDrafts();
            		});
            	} catch (ex) {
            		console.log("Error saving draft:", ex);
            	}
            },

            /**
           	* Clear older drafts by iterating over the sorted list of drafts
           	* stored by LocalForage and removing any beyond a hardcoded limit.
           	*/
           	clearOldDrafts: function() {
           		var drafts = [];

          		try {
          			LocalForage.iterate(function(value, key, iterationNumber) {
          			// Extract each draft
          			drafts.push({
          					key: key,
          					value: value
          				});
          			}).then(function(){
          				// Sort by datetime
          				drafts = _.sortBy(drafts, function(draft) {
          					return draft.value.datetime.toString();
          				}).reverse();
          			}).then(function() {
          				_.each(drafts, function(draft, i) {
          					var age = (new Date()) - new Date(draft.value.datetime);
          					var isOld = (age / 2678400000) > 1; // ~31days
          					// Delete this draft is not in the most recent 100 or
          					// if older than 31 days
          					var shouldDelete = i > 100 || isOld;
											if (!shouldDelete) {
												return;
											}

											LocalForage.removeItem(draft.key).then(function() {
												// Item should be removed
											});
										})
									});
							}
							catch (ex) {
								console.log("Failed to clear old drafts: ", ex);
							}
						}
	        });

        return EMLEntityView;
});