define(['underscore', 'jquery', 'backbone', 'views/metadata/ScienceMetadataView', 'views/metadata/EMLGeoCoverageView', 'views/metadata/EMLPartyView', 'views/metadata/EMLMethodsView', 'views/metadata/EMLTempCoverageView', 'models/metadata/eml211/EML211', 'models/metadata/eml211/EMLGeoCoverage', 'models/metadata/eml211/EMLKeywordSet', 'models/metadata/eml211/EMLParty', 'models/metadata/eml211/EMLProject', 'models/metadata/eml211/EMLText', 'models/metadata/eml211/EMLTaxonCoverage', 'models/metadata/eml211/EMLTemporalCoverage', 'models/metadata/eml211/EMLMethods', 'text!templates/metadata/eml.html', 'text!templates/metadata/eml-people.html', 'text!templates/metadata/EMLPartyCopyMenu.html', 'text!templates/metadata/metadataOverview.html', 'text!templates/metadata/dates.html', 'text!templates/metadata/locationsSection.html', 'text!templates/metadata/taxonomicCoverage.html', 'text!templates/metadata/taxonomicClassificationTable.html', 'text!templates/metadata/taxonomicClassificationRow.html'], function(_, $, Backbone, ScienceMetadataView, EMLGeoCoverageView, EMLPartyView, EMLMethodsView, EMLTempCoverageView, EML, EMLGeoCoverage, EMLKeywordSet, EMLParty, EMLProject, EMLText, EMLTaxonCoverage, EMLTemporalCoverage, EMLMethods, Template, PeopleTemplate, EMLPartyCopyMenuTemplate, OverviewTemplate, DatesTemplate, LocationsTemplate, TaxonomicCoverageTemplate, TaxonomicClassificationTable, TaxonomicClassificationRow){ /** * @class EMLView * @classdesc An EMLView renders an editable view of an EML 2.1.1 document * @classcategory Views/Metadata * @extends ScienceMetadataView */ var EMLView = ScienceMetadataView.extend( /** @lends EMLView */{ type: "EML211", el: '#metadata-container', events: { "change .text" : "updateText", "change .basic-text" : "updateBasicText", "keyup .basic-text.new" : "addBasicText", "mouseover .basic-text-row .remove" : "previewTextRemove", "mouseout .basic-text-row .remove" : "previewTextRemove", "change .pubDate input" : "updatePubDate", "focusout .pubDate input" : "showPubDateValidation", "keyup .eml-geocoverage.new" : "updateLocations", "change .taxonomic-coverage" : "updateTaxonCoverage", "keyup .taxonomic-coverage .new input" : "addNewTaxon", "keyup .taxonomic-coverage .new select" : "addNewTaxon", "focusout .taxonomic-coverage tr" : "showTaxonValidation", "click .taxonomic-coverage-row .remove" : "removeTaxonRank", "mouseover .taxonomic-coverage .remove" : "previewTaxonRemove", "mouseout .taxonomic-coverage .remove" : "previewTaxonRemove", "change .keywords" : "updateKeywords", "keyup .keyword-row.new input" : "addNewKeyword", "mouseover .keyword-row .remove" : "previewKeywordRemove", "mouseout .keyword-row .remove" : "previewKeywordRemove", "change .usage" : "updateRadioButtons", "change .funding" : "updateFunding", "keyup .funding.new" : "addFunding", "mouseover .funding-row .remove" : "previewFundingRemove", "mouseout .funding-row .remove" : "previewFundingRemove", "keyup .funding.error" : "handleFundingTyping", "click .side-nav-item" : "switchSection", "keyup .eml-party.new" : "handlePersonTyping", "change #new-party-menu" : "chooseNewPersonType", "click .eml-party .copy" : "showCopyPersonMenu", "click #copy-party-save" : "copyPerson", "click .eml-party .remove" : "removePerson", "click .eml-party .move-up" : "movePersonUp", "click .eml-party .move-down" : "movePersonDown", "click .remove" : "handleRemove" }, /* A list of the subviews */ subviews: [], /* The active section in the view - can only be the section name (e.g. overview, people) * The active section is highlighted in the table of contents and is scrolled to when the page loads */ activeSection: "overview", /* The visible section in the view - can either be the section name (e.g. overview, people) or "all" * The visible section is the ONLY section that is displayed. If set to all, all sections are displayed. */ visibleSection: "overview", /* Templates */ template: _.template(Template), overviewTemplate: _.template(OverviewTemplate), datesTemplate: _.template(DatesTemplate), locationsTemplate: _.template(LocationsTemplate), taxonomicCoverageTemplate: _.template(TaxonomicCoverageTemplate), taxonomicClassificationTableTemplate: _.template(TaxonomicClassificationTable), taxonomicClassificationRowTemplate: _.template(TaxonomicClassificationRow), copyPersonMenuTemplate: _.template(EMLPartyCopyMenuTemplate), peopleTemplate: _.template(PeopleTemplate), initialize: function(options) { //Set up all the options if(typeof options == "undefined") var options = {}; //The EML Model and ID this.model = options.model || new EML(); if(!this.model.get("id") && options.id) this.model.set("id", options.id); //Get the current mode this.edit = options.edit || false; return this; }, /* Render the view */ render: function() { MetacatUI.appModel.set('headerType', 'default'); //Render the basic structure of the page and table of contents this.$el.html(this.template({ activeSection: this.activeSection, visibleSection: this.visibleSection })); this.$container = this.$(".metadata-container"); //Render all the EML sections when the model is synced this.renderAllSections(); if(!this.model.get("synced")) this.listenToOnce(this.model, "sync", this.renderAllSections); //Listen to updates on the data package collections _.each(this.model.get("collections"), function(dataPackage){ if(dataPackage.type != "DataPackage") return; //When the data package has been saved, render the EML again this.listenTo(dataPackage, "successSaving", this.renderAllSections); }, this); return this; }, renderAllSections: function(){ this.renderOverview(); this.renderPeople(); this.renderDates(); this.renderLocations(); this.renderTaxa(); this.renderMethods(); this.renderProject(); this.renderSharing(); //Scroll to the active section if(this.activeSection != "overview"){ MetacatUI.appView.scrollTo(this.$(".section." + this.activeSection)); } //When scrolling through the metadata, highlight the side navigation var view = this; $(document).scroll(function(){ view.highlightTOC.call(view); }); }, /* * Renders the Overview section of the page */ renderOverview: function(){ //Get the overall view mode var edit = this.edit; var view = this; //Append the empty layout var overviewEl = this.$container.find(".overview"); $(overviewEl).html(this.overviewTemplate()); //Title this.renderTitle(); this.listenTo(this.model, "change:title", this.renderTitle); //Abstract _.each(this.model.get("abstract"), function(abs){ var abstractEl = this.createEMLText(abs, edit, "abstract"); //Add the abstract element to the view $(overviewEl).find(".abstract").append(abstractEl); }, this); if(!this.model.get("abstract").length){ var abstractEl = this.createEMLText(null, edit, "abstract"); //Add the abstract element to the view $(overviewEl).find(".abstract").append(abstractEl); } //Keywords //Iterate over each keyword and add a text input for the keyword value and a dropdown menu for the thesaurus _.each(this.model.get("keywordSets"), function(keywordSetModel){ _.each(keywordSetModel.get("keywords"), function(keyword){ this.addKeyword(keyword, keywordSetModel.get("thesaurus")); }, this); }, this); //Add a new keyword row this.addKeyword(); //Alternate Ids var altIdsEls = this.createBasicTextFields("alternateIdentifier", "Add a new alternate identifier"); $(overviewEl).find(".altids").append(altIdsEls); //Usage //Find the model value that matches a radio button and check it // Note the replace() call removing newlines and replacing them with a single space // character. This is a temporary hack to fix https://github.com/NCEAS/metacatui/issues/128 if(this.model.get("intellectualRights")) this.$(".checkbox .usage[value='" + this.model.get("intellectualRights").replace(/\r?\n|\r/g, ' ') + "']").prop("checked", true); //Funding this.renderFunding(); // pubDate // BDM: This isn't a createBasicText call because that helper // assumes multiple values for the category // TODO: Consider a re-factor of createBasicText var pubDateInput = $(overviewEl).find("input.pubDate").val(this.model.get("pubDate")); //Initialize all the tooltips this.$(".tooltip-this").tooltip(); }, renderTitle: function(){ var titleEl = this.createBasicTextFields("title", "Example: Greater Yellowstone Rivers from 1:126,700 U.S. Forest Service Visitor Maps (1961-1983)", false); this.$container.find(".overview").find(".title-container").html(titleEl); }, /* * Renders the People section of the page */ renderPeople: function(){ this.$(".section.people").empty().append("
Only one publisher can be specified.
', ''); _.each(this.model.get("publisher"), function(publisher){ this.renderPerson(publisher, "publisher") }, this); } else emptyTypes.push("publisher"); //User if(user.length){ this.$(".section.people").append("Only one publisher can be specified.
'); //Remove this type from the dropdown menu partyMenu.find("[value='" + partyType + "']").remove(); //Remove the menu from the page temporarily partyMenu.detach(); //Add the new party type form var newPartyContainer = $(document.createElement("div")) .attr("data-attribute", "new") .addClass("row-striped"); this.$(".section.people").append(newPartyContainer); this.renderPerson(null, "new"); $(newPartyContainer).before(partyMenu); //Update the model if( _.includes(partyModel.get("roleOptions"), partyType) ){ partyModel.get("roles").push(partyType); } else { partyModel.set("type", partyType); } if(partyModel.isValid()){ partyModel.mergeIntoParent(); //Add a new person of that type this.renderPerson(null, partyType); } else{ partyForm.find(".eml-party").data("view").showValidation(); } }, /* * addNewPersonType - Adds a header and container to the People section for the given party type/role, */ addNewPersonType: function(partyType){ if(!partyType) return; // Container element to hold all parties of this type var partyTypeContainer = $(document.createElement("div")).addClass("party-type-container"); // Add a new header for the party type var header = $(document.createElement("h4")).text(this.partyTypeMap[partyType]); $(partyTypeContainer).append(header); //Remove this type from the dropdown menu this.$("#new-party-menu").find("[value='" + partyType + "']").remove(); //Add the new party container var partyRow = $(document.createElement("div")) .attr("data-attribute", partyType) .addClass("row-striped"); partyTypeContainer.append(partyRow); // Add in the new party type container just before the dropdown this.$("#new-party-menu").before(partyTypeContainer); //Add a blank form to the new person type section this.renderPerson(null, partyType); }, /* * showCopyPersonMenu: Displays a modal window to the user with a list of roles that they can * copy this person to */ showCopyPersonMenu: function(e){ //Get the EMLParty to copy var partyToCopy = $(e.target).parents(".eml-party").data("model"), menu = this.$("#copy-person-menu"); //Check if the modal window menu has been created already if( !menu.length ){ //Create the modal window menu from the template menu = $(this.copyPersonMenuTemplate()); //Add to the DOM this.$el.append(menu); //Initialize the modal menu.modal(); } else{ //Reset all the checkboxes menu.find("input:checked").prop("checked", false); menu.find(".disabled") .prop("disabled", false) .removeClass("disabled") .parent(".checkbox") .attr("title", ""); } //Disable the roles this person is already in var currentRoles = partyToCopy.get("roles") || partyToCopy.get("type") || ""; // "type" is a string and "roles" is an array. // so that we can use _.each() on both, convert "type" to an array if(typeof currentRoles == "string"){ currentRoles = [currentRoles]; } _.each(currentRoles, function(currentRole){ menu.find("input[value='" + currentRole + "']") .prop("disabled", "disabled") .addClass("disabled") .parent(".checkbox") .attr("title", "This person is already in the " + this.partyTypeMap[currentRole] + " list."); }, this); //If there is already one publisher, disable that option if( this.model.get("publisher").length ){ var publisherName = this.model.get("publisher")[0].get("individualName") ? this.model.get("publisher")[0].get("individualName").givenName + " " + this.model.get("publisher")[0].get("individualName").surName : this.model.get("publisher")[0].get("organizationName") || this.model.get("publisher")[0].get("positionName") || "Someone"; menu.find("input[value='publisher']") .prop("disabled", "disabled") .addClass("disabled") .parent(".checkbox") .attr("title", publisherName + " is already listed as the publisher. (There can be only one)."); } //Attach the EMLParty to the menu DOMs menu.data({ EMLParty: partyToCopy }); //Show the modal window menu now menu.modal("show"); }, /* * copyPerson: Gets the selected checkboxes from the copy person menu and copies the EMLParty * to those new roles */ copyPerson: function(){ //Get all the checked boxes var checkedBoxes = this.$("#copy-person-menu input:checked"), //Get the EMLParty to copy partyToCopy = this.$("#copy-person-menu").data("EMLParty"); //For each selected role, _.each(checkedBoxes, function(checkedBox){ //Get the roles var role = $(checkedBox).val(), isAssocParty = _.contains(partyToCopy.get("roleOptions"), role); //If the new role is an associated party ... if( isAssocParty ){ //If there are no parties in this role yet, // then add this person type to the view if( !this.model.get("associatedParty").length ) this.addNewPersonType(role); else if( !_.find(this.model.get("associatedParty"), function(p){ return p.get("role") == role; }) ){ this.addNewPersonType(role); } //Create a new EMLParty model var newPerson = new EMLParty(); //Create all the attributes for the new person. We're only changing the role newPerson.set( partyToCopy.copyValues() ); newPerson.set("type", "associatedParty"); newPerson.set("roles", [role]); //Add this new EMLParty to the EML model this.model.addParty(newPerson); //Render this new person this.renderPerson(newPerson, role); } //If the new role is not an associated party... else{ //If there are no parties in this role yet, // then add this person type to the view if( !this.model.get(role).length ) this.addNewPersonType(role); //Create a new EMLParty model var newPerson = new EMLParty(); // Copy the attributes from the original person // and set it on the new person newPerson.set(partyToCopy.copyValues()); newPerson.set("type", role); newPerson.set("roles", newPerson.defaults().role); //Add this new EMLParty to the EML model this.model.addParty(newPerson); //Render this new person this.renderPerson(newPerson, role); } }, this); //If there was at least one copy created, then trigger the change event if(checkedBoxes.length){ this.model.trickleUpChange(); } }, removePerson: function(e){ e.preventDefault(); //Get the party view el, view, and model var partyEl = $(e.target).parents(".eml-party"), partyView = partyEl.data("view"), partyToRemove = partyEl.data("model"); //If there is no model found, we have nothing to do, so exit if(!partyToRemove) return false; //Call removeParty on the EML211 model to remove this EMLParty this.model.removeParty(partyToRemove); //Let the EMLPartyView remove itself partyView.remove(); }, /** * Attempt to move the current person (Party) one index backward (up). * * @param {EventHandler} e: The click event handler */ movePersonUp: function(e){ e.preventDefault(); // Get the party view el, view, and model var partyEl = $(e.target).parents(".eml-party"), model = partyEl.data("model"), next = $(partyEl).prev().not(".new"); if (next.length === 0) { return; } // Remove current view, create and insert a new one for the model $(partyEl).remove(); var newView = new EMLPartyView({ model: model, edit: this.edit }); $(next).before(newView.render().el); // Move the party down within the model too this.model.movePartyUp(model); this.model.trickleUpChange(); }, /** * Attempt to move the current person (Party) one index forward (down). * * @param {EventHandler} e: The click event handler */ movePersonDown: function(e){ e.preventDefault(); // Get the party view el, view, and model var partyEl = $(e.target).parents(".eml-party"), model = partyEl.data("model"), next = $(partyEl).next().not(".new"); if (next.length === 0) { return; } // Remove current view, create and insert a new one for the model $(partyEl).remove(); var newView = new EMLPartyView({ model: model, edit: this.edit }); $(next).after(newView.render().el); // Move the party down within the model too this.model.movePartyDown(model); this.model.trickleUpChange(); }, /* * Renders the Dates section of the page */ renderDates: function(){ //Add a header this.$(".section.dates").html( $(document.createElement("h2")).text("Dates") ); _.each(this.model.get('temporalCoverage'), function(model){ var tempCovView = new EMLTempCoverageView({ model: model, isNew: false, edit: this.edit }); tempCovView.render(); this.$(".section.dates").append(tempCovView.el); }, this); if( !this.model.get('temporalCoverage').length ){ var tempCovView = new EMLTempCoverageView({ isNew: true, edit: this.edit, model: new EMLTemporalCoverage({ parentModel: this.model }) }); tempCovView.render(); this.$(".section.dates").append(tempCovView.el); } }, /* * Renders the Locations section of the page */ renderLocations: function(){ var locationsSection = this.$(".section.locations"); //Add the Locations header locationsSection.html(this.locationsTemplate()); var locationsTable = locationsSection.find(".locations-table"); //Render an EMLGeoCoverage view for each EMLGeoCoverage model _.each(this.model.get("geoCoverage"), function(geo, i){ //Create an EMLGeoCoverageView var geoView = new EMLGeoCoverageView({ model: geo, edit: this.edit }); //Render the view geoView.render(); geoView.$el.find(".remove-container").append(this.createRemoveButton(null, "geoCoverage", ".eml-geocoverage", ".locations-table")); //Add the locations section to the page locationsTable.append(geoView.el); //Listen to validation events this.listenTo(geo, "valid", this.updateLocationsError); //Save it in our subviews array this.subviews.push(geoView); }, this); //Now add one empty row to enter a new geo coverage if(this.edit){ var newGeoModel = new EMLGeoCoverage({ parentModel: this.model, isNew: true}), newGeoView = new EMLGeoCoverageView({ edit: true, model: newGeoModel, isNew: true }); locationsTable.append(newGeoView.render().el); newGeoView.$el.find(".remove-container").append(this.createRemoveButton(null, "geoCoverage", ".eml-geocoverage", ".locations-table")); //Listen to validation events this.listenTo(newGeoModel, "valid", this.updateLocationsError); } }, /* * Renders the Taxa section of the page */ renderTaxa: function(){ this.$(".section.taxa").html($(document.createElement("h2")).text("Taxa")); var taxonomy = this.model.get('taxonCoverage'); // Render a set of tables for each taxonomicCoverage if (typeof taxonomy !== "undefined" && (Array.isArray(taxonomy) && taxonomy.length)) { for (var i = 0; i < taxonomy.length; i++) { this.$(".section.taxa").append(this.createTaxonomicCoverage(taxonomy[i])); } } else { // Create a new one var taxonCov = new EMLTaxonCoverage({ parentModel: this.model }); this.model.set('taxonCoverage', [taxonCov], {silent: true}); this.$(".section.taxa").append(this.createTaxonomicCoverage(taxonCov)); } // updating the indexes of taxa-tables before rendering the information on page(view). var taxaNums = this.$(".editor-header-index"); for (var i = 0; i < taxaNums.length; i++) { $(taxaNums[i]).text(i + 1); } }, /* * Renders the Methods section of the page */ renderMethods: function(){ var methodsModel = this.model.get("methods"); if (!methodsModel) { methodsModel = new EMLMethods({ edit: this.edit, parentModel: this.model }); } this.$(".section.methods").html(new EMLMethodsView({ model: methodsModel, edit: this.edit }).render().el); }, /* * Renders the Projcet section of the page */ renderProject: function(){ }, /* * Renders the Sharing section of the page */ renderSharing: function(){ }, /* * Renders the funding field of the EML */ renderFunding: function(){ //Funding var funding = this.model.get("project") ? this.model.get("project").get("funding") : []; //Clear the funding section $(".section.overview .funding").empty(); //Create the funding input elements _.each(funding, function(fundingItem, i){ this.addFunding(fundingItem); }, this); //Add a blank funding input this.addFunding(); }, /* * Adds a single funding input row. Can either be called directly or used as an event callback */ addFunding: function(argument){ if(this.edit){ if(typeof argument == "string") var value = argument; else if(!argument) var value = ""; //Don't add another new funding input if there already is one else if( !value && (typeof argument == "object") && !$(argument.target).is(".new") ) return; else if((typeof argument == "object") && argument.target) { var event = argument; // Don't add a new funding row if the current one is empty if ( $(event.target).val().trim() === "") return; } var fundingInput = $(document.createElement("input")) .attr("type", "text") .attr("data-category", "funding") .addClass("span12 funding hover-autocomplete-target") .attr("placeholder", "Search for NSF awards by keyword or enter custom funding information") .val(value), hiddenFundingInput = fundingInput.clone().attr("type", "hidden").val(value).attr("id", "").addClass("hidden"), loadingSpinner = $(document.createElement("i")).addClass("icon icon-spinner input-icon icon-spin subtle hidden"); //Append all the elements to a container var containerEl = $(document.createElement("div")) .addClass("ui-autocomplete-container funding-row") .append(fundingInput, loadingSpinner, hiddenFundingInput); if (!value){ $(fundingInput).addClass("new"); if(event) { $(event.target).parents("div.funding-row").append(this.createRemoveButton('project', 'funding', '.funding-row', 'div.funding-container')); $(event.target).removeClass("new"); } } else { // Add a remove button if this is a non-new funding element $(containerEl).append(this.createRemoveButton('project', 'funding', '.funding-row', 'div.funding-container')); } var view = this; //Setup the autocomplete widget for the funding input fundingInput.autocomplete({ source: function(request, response){ var beforeRequest = function(){ loadingSpinner.show(); } var afterRequest = function(){ loadingSpinner.hide(); } return MetacatUI.appLookupModel.getGrantAutocomplete(request, response, beforeRequest, afterRequest) }, select: function(e, ui) { e.preventDefault(); var value = "NSF Award " + ui.item.value + " (" + ui.item.label + ")"; hiddenFundingInput.val(value); fundingInput.val(value); $(".funding .ui-helper-hidden-accessible").hide(); loadingSpinner.css("top", "5px"); view.updateFunding(e); }, position: { my: "left top", at: "left bottom", of: fundingInput, collision: "fit" }, appendTo: containerEl, minLength: 3 }); this.$(".funding-container").append(containerEl); } }, previewFundingRemove: function(e){ $(e.target).parents(".funding-row").toggleClass("remove-preview"); }, handleFundingTyping: function(e){ var fundingInput = $(e.target); //If the funding value is at least one character if(fundingInput.val().length > 0){ //Get rid of the error styling in this row fundingInput.parent(".funding-row").children().removeClass("error"); //If this was the only funding input with an error, we can safely remove the error message if( !this.$("input.funding.error").length ) this.$("[data-category='funding'] .notification").removeClass("error").text(""); } }, addKeyword: function(keyword, thesaurus){ if(typeof keyword != "string" || !keyword){ var keyword = ""; //Only show one new keyword row at a time if((this.$(".keyword.new").length == 1) && !this.$(".keyword.new").val()) return; else if(this.$(".keyword.new").length > 1) return; } //Create the keyword row HTML var row = $(document.createElement("div")).addClass("row-fluid keyword-row"), keywordInput = $(document.createElement("input")).attr("type", "text").addClass("keyword span10").attr("placeholder", "Add one new keyword"), thesInput = $(document.createElement("select")).addClass("thesaurus span2"), thesOptionExists = false, removeButton; // Piece together the inputs row.append(keywordInput, thesInput); //Create the thesaurus options dropdown menu _.each(MetacatUI.appModel.get("emlKeywordThesauri"), function(option){ var optionEl = $(document.createElement("option")).val(option.thesaurus).text(option.label); thesInput.append( optionEl ); if( option.thesaurus == thesaurus ){ optionEl.prop("selected", true); thesOptionExists = true; } }); //Add a "None" option, which is always in the dropdown thesInput.prepend( $(document.createElement("option")).val("None").text("None") ); if( thesaurus == "None" || !thesaurus ){ thesInput.val("None"); } //If this keyword is from a custom thesaurus that is NOT configured in this App, AND // there is an option with the same label, then remove the option so it doesn't look like a duplicate. else if( !thesOptionExists && _.findWhere(MetacatUI.appModel.get("emlKeywordThesauri"), { label: thesaurus }) ){ var duplicateOptions = thesInput.find("option:contains(" + thesaurus + ")"); duplicateOptions.each(function(i, option){ if( $(option).text() == thesaurus && !$(option).prop("selected") ){ $(option).remove(); } }); } //If this keyword is from a custom thesaurus that is NOT configured in this App, then show it as a custom option else if( !thesOptionExists ){ thesInput.append( $(document.createElement("option")).val(thesaurus).text(thesaurus).prop("selected", true) ); } if(!keyword) row.addClass("new"); else{ //Set the keyword value on the text input keywordInput.val(keyword); // Add a remove button unless this is the .new keyword row.append(this.createRemoveButton(null, 'keywordSets', 'div.keyword-row', 'div.keywords')); } this.$(".keywords").append(row); }, addNewKeyword: function(e) { if ($(e.target).val().trim() === "") return; $(e.target).parents(".keyword-row").first().removeClass("new"); // Add in a remove button $(e.target).parents(".keyword-row").append(this.createRemoveButton(null, 'keywordSets', 'div.keyword-row', 'div.keywords')); var row = $(document.createElement("div")).addClass("row-fluid keyword-row new").data({ model: new EMLKeywordSet() }), keywordInput = $(document.createElement("input")).attr("type", "text").addClass("keyword span10"), thesInput = $(document.createElement("select")).addClass("thesaurus span2"); row.append(keywordInput, thesInput); //Create the thesaurus options dropdown menu _.each(MetacatUI.appModel.get("emlKeywordThesauri"), function(option){ thesInput.append( $(document.createElement("option")).val(option.thesaurus).text(option.label) ); }); //Add a "None" option, which is always in the dropdown thesInput.prepend( $(document.createElement("option")).val("None").text("None").prop("selected", true) ); this.$(".keywords").append(row); }, previewKeywordRemove: function(e){ var row = $(e.target).parents(".keyword-row").toggleClass("remove-preview"); }, /* * Update the funding info when the form is changed */ updateFunding: function(e){ if(!e) return; var row = $(e.target).parent(".funding-row").first(), rowNum = this.$(".funding-row").index(row), input = $(row).find("input"), isNew = $(row).is(".new"); var newValue = isNew? $(e.target).siblings("input.hidden").val() : $(e.target).val(); newValue = this.model.cleanXMLText(newValue); if( typeof newValue == "string" ){ newValue = newValue.trim(); } //If there is no project model if(!this.model.get("project")){ var model = new EMLProject({ parentModel: this.model }); this.model.set("project", model); } else var model = this.model.get("project"); var currentFundingValues = model.get("funding"); //If the new value is an empty string, then remove that index in the array if( typeof newValue == "string" && newValue.trim().length == 0 ){ currentFundingValues = currentFundingValues.splice(rowNum, 1); } else{ currentFundingValues[rowNum] = newValue; } if(isNew && newValue != ''){ $(row).removeClass("new"); // Add in a remove button $(e.target).parent().append(this.createRemoveButton('project', 'funding', '.funding-row', 'div.funding-container')); this.addFunding(); } this.model.trickleUpChange(); }, //TODO: Comma and semi-colon seperate keywords updateKeywords: function(e){ var keywordSets = this.model.get("keywordSets"), newKeywordSets = []; //Get all the keywords in the view _.each(this.$(".keyword-row"), function(thisRow){ var thesaurus = this.model.cleanXMLText( $(thisRow).find("select").val() ), keyword = this.model.cleanXMLText( $(thisRow).find("input").val() ); if(!keyword) return; var keywordSet = _.find(newKeywordSets, function(keywordSet){ return keywordSet.get("thesaurus") == thesaurus; }); if(typeof keywordSet != "undefined"){ keywordSet.get("keywords").push(keyword); } else{ newKeywordSets.push(new EMLKeywordSet({ parentModel: this.model, keywords: [keyword], thesaurus: thesaurus })); } }, this); //Update the EML model this.model.set("keywordSets", newKeywordSets); if(e){ var row = $(e.target).parent(".keyword-row"); //Add a new row when the user has added a new keyword just now if(row.is(".new")){ row.removeClass("new"); row.append(this.createRemoveButton(null, "keywordSets", "div.keyword-row", "div.keywords")); this.addKeyword(); } } }, /* * Update the EML Geo Coverage models and views when the user interacts with the locations section */ updateLocations: function(e){ if(!e) return; e.preventDefault(); var viewEl = $(e.target).parents(".eml-geocoverage"), geoCovModel = viewEl.data("model"); //If the EMLGeoCoverage is new if(viewEl.is(".new")){ if(this.$(".eml-geocoverage.new").length > 1) return; //Render the new geo coverage view var newGeo = new EMLGeoCoverageView({ edit: this.edit, model: new EMLGeoCoverage({ parentModel: this.model, isNew: true}), isNew: true }); this.$(".locations-table").append(newGeo.render().el); newGeo.$el.find(".remove-container").append(this.createRemoveButton(null, "geoCoverage", ".eml-geocoverage", ".locations-table")); //Unmark the view as new viewEl.data("view").notNew(); //Get the EMLGeoCoverage model attached to this EMlGeoCoverageView var geoModel = viewEl.data("model"), //Get the current EMLGeoCoverage models set on the parent EML model currentCoverages = this.model.get("geoCoverage"); //Add this new geo coverage model to the parent EML model if(Array.isArray(currentCoverages)){ if( !_.contains(currentCoverages, geoModel) ){ currentCoverages.push(geoModel); this.model.trigger("change:geoCoverage"); } } else{ currentCoverages = [currentCoverages, geoModel]; this.model.set("geoCoverage", currentCoverages); } } }, /* * If all the EMLGeoCoverage models are valid, remove the error messages for the Locations section */ updateLocationsError: function(){ var allValid = _.every(this.model.get("geoCoverage"), function(geoCoverageModel){ return geoCoverageModel.isValid(); }); if(allValid){ this.$(".side-nav-item.error[data-category='geoCoverage']") .removeClass("error") .find(".icon.error").hide(); this.$(".section[data-section='locations'] .notification.error") .removeClass("error") .text(""); } }, /* * Creates the text elements */ createEMLText: function(textModel, edit, category){ if(!textModel && edit){ return $(document.createElement("textarea")) .attr("data-category", category) .addClass("xlarge text"); } else if(!textModel && !edit){ return $(document.createElement("div")) .attr("data-category", category); } //Get the EMLText from the EML model var finishedEl; //Get the text attribute from the EMLText model var paragraphs = textModel.get("text"), paragraphsString = ""; //If the text should be editable, if(edit){ //Format the paragraphs with carriage returns between paragraphs paragraphsString = paragraphs.join(String.fromCharCode(13)); //Create the textarea element finishedEl = $(document.createElement("textarea")) .addClass("xlarge text") .attr("data-category", category) .html(paragraphsString); } else{ //Format the paragraphs with HTML _.each(paragraphs, function(p){ paragraphsString += "" + p + "
"; }); //Create a div finishedEl = $(document.createElement("div")) .attr("data-category", category) .append(paragraphsString); } $(finishedEl).data({ model: textModel }); //Return the finished DOM element return finishedEl; }, /* * Updates a basic text field in the EML after the user changes the value */ updateText: function(e){ if(!e) return false; var category = $(e.target).attr("data-category"), currentValue = this.model.get(category), textModel = $(e.target).data("model"), value = this.model.cleanXMLText($(e.target).val()); //We can't update anything without a category if(!category) return false; //Get the list of paragraphs - checking for carriage returns and line feeds var paragraphsCR = value.split(String.fromCharCode(13)); var paragraphsLF = value.split(String.fromCharCode(10)); //Use the paragraph list that has the most var paragraphs = (paragraphsCR > paragraphsLF)? paragraphsCR : paragraphsLF; //If this category isn't set yet, then create a new EMLText model if(!textModel){ //Get the current value for this category and create a new EMLText model var newTextModel = new EMLText({ text: paragraphs, parentModel: this.model }); // Save the new model onto the underlying DOM node $(e.target).data({ "model" : newTextModel }); //Set the new EMLText model on the EML model if(Array.isArray(currentValue)){ currentValue.push(newTextModel); this.model.trigger("change:" + category); this.model.trigger("change"); } else{ this.model.set(category, newTextModel); } } //Update the existing EMLText model else{ //If there are no paragraphs or all the paragraphs are empty... if( !paragraphs.length || _.every(paragraphs, function(p){ return p.trim() == "" }) ){ //Remove this text model from the array of text models since it is empty var newValue = _.without(currentValue, textModel); this.model.set(category, newValue); } else{ textModel.set("text", paragraphs); textModel.trigger("change:text"); //Is this text model set on the EML model? if( Array.isArray(currentValue) && !_.contains(currentValue, textModel) ){ //Push this text model into the array of EMLText models currentValue.push(textModel); this.model.trigger("change:" + category); this.model.trigger("change"); } } } }, /* * Creates and returns an array of basic text input field for editing */ createBasicTextFields: function(category, placeholder){ var textContainer = $(document.createElement("div")).addClass("text-container"), modelValues = this.model.get(category), textRow; // Holds the DOM for each field //Format as an array if(!Array.isArray(modelValues) && modelValues) modelValues = [modelValues]; //For each value in this category, create an HTML element with the value inserted _.each(modelValues, function(value, i, allModelValues){ if(this.edit){ var textRow = $(document.createElement("div")).addClass("basic-text-row"), input = $(document.createElement("input")) .attr("type", "text") .attr("data-category", category) .addClass("basic-text"); textRow.append(input.clone().val(value)); if(category != "title") textRow.append(this.createRemoveButton(null, category, 'div.basic-text-row', 'div.text-container')); textContainer.append(textRow); //At the end, append an empty input for the user to add a new one if(i+1 == allModelValues.length && category != "title") { var newRow = $($(document.createElement("div")).addClass("basic-text-row")); newRow.append(input.clone().addClass("new").attr("placeholder", placeholder || "Add a new " + category)); textContainer.append(newRow); } } else{ textContainer.append($(document.createElement("div")) .addClass("basic-text-row") .attr("data-category", category) .text(value)); } }, this); if((!modelValues || !modelValues.length) && this.edit){ var input = $(document.createElement("input")) .attr("type", "text") .attr("data-category", category) .addClass("basic-text new") .attr("placeholder", placeholder || "Add a new " + category); textContainer.append($(document.createElement("div")).addClass("basic-text-row").append(input)); } return textContainer; }, updateBasicText: function(e){ if(!e) return false; //Get the category, new value, and model var category = $(e.target).attr("data-category"), value = this.model.cleanXMLText($(e.target).val()), model = $(e.target).data("model") || this.model; //We can't update anything without a category if(!category) return false; //Get the current value var currentValue = model.get(category); //Insert the new value into the array if( Array.isArray(currentValue) ){ //Find the position this text input is in var position = $(e.target).parents("div.text-container").first().children("div").index($(e.target).parent()); //Set the value in that position in the array currentValue[position] = value; //Set the changed array on this model model.set(category, currentValue); model.trigger("change:" + category); } //Update the model if the current value is a string else if(typeof currentValue == "string"){ model.set(category, [value]); model.trigger("change:" + category); } else if(!currentValue) { model.set(category, [value]); model.trigger("change:" + category); } //Add another blank text input if($(e.target).is(".new") && value != '' && category != "title"){ $(e.target).removeClass("new"); this.addBasicText(e); } // Trigger a change on the entire package MetacatUI.rootDataPackage.packageModel.set("changed", true); }, /* One-off handler for updating pubDate on the model when the form input changes. Fairly similar but just a pared down version of updateBasicText. */ updatePubDate: function(e){ if(!e) return false; this.model.set('pubDate', $(e.target).val().trim()); this.model.trigger("change"); // Trigger a change on the entire package MetacatUI.rootDataPackage.packageModel.set("changed", true); }, /* * Adds a basic text input */ addBasicText: function(e){ var category = $(e.target).attr("data-category"), allBasicTexts = $(".basic-text.new[data-category='" + category + "']"); //Only show one new row at a time if((allBasicTexts.length == 1) && !allBasicTexts.val()) return; else if(allBasicTexts.length > 1) return; //We are only supporting one title right now else if(category == "title") return; //Add another blank text input var newRow = $(document.createElement("div")).addClass("basic-text-row"); newRow.append($(document.createElement("input")) .attr("type", "text") .attr("data-category", category) .attr("placeholder", $(e.target).attr("placeholder")) .addClass("new basic-text")); $(e.target).parent().after(newRow); $(e.target).after(this.createRemoveButton(null, category, '.basic-text-row', "div.text-container")); }, previewTextRemove: function(e){ $(e.target).parents(".basic-text-row").toggleClass("remove-preview"); }, // publication date validation. isDateFormatValid: function(dateString){ //Date strings that are four characters should be a full year. Make sure all characters are numbers if(dateString.length == 4){ var digits = dateString.match( /[0-9]/g ); return (digits.length == 4) } //Date strings that are 10 characters long should be a valid date else{ var dateParts = dateString.split("-"); if(dateParts.length != 3 || dateParts[0].length != 4 || dateParts[1].length != 2 || dateParts[2].length != 2) return false; dateYear = dateParts[0]; dateMonth = dateParts[1]; dateDay = dateParts[2]; // Validating the values for the date and month if in YYYY-MM-DD format. if (dateMonth < 1 || dateMonth > 12) return false; else if (dateDay < 1 || dateDay > 31) return false; else if ((dateMonth == 4 || dateMonth == 6 || dateMonth == 9 || dateMonth == 11) && dateDay == 31) return false; else if (dateMonth == 2) { // Validation for leap year dates. var isleap = (dateYear % 4 == 0 && (dateYear % 100 != 0 || dateYear % 400 == 0)); if ((dateDay > 29) || (dateDay == 29 && !isleap)) return false; } var digits = _.filter(dateParts, function(part){ return (part.match( /[0-9]/g ).length == part.length); }); return (digits.length == 3); } }, /* Event handler for showing validation messaging for the pubDate input which has to conform to the EML yearDate type (YYYY or YYYY-MM-DD) */ showPubDateValidation: function(e) { var container = $(e.target).parents(".pubDate").first(), input = $(e.target), messageEl = $(container).find('.notification'), value = input.val(), errors = []; // Remove existing error borders and notifications input.removeClass("error"); messageEl.text(""); messageEl.removeClass("error"); if (value != "" && value.length > 0) { if (!this.isDateFormatValid(value)) { errors.push("The value entered for publication date, '" + value + "' is not a valid value for this field. Enter either a year (e.g. 2017) or a date in the format YYYY-MM-DD."); input.addClass("error") } } if (errors.length > 0) { messageEl.text(errors[0]).addClass("error"); } }, // Creates a table to hold a single EMLTaxonCoverage element (table) for // each root-level taxonomicClassification createTaxonomicCoverage: function(coverage) { var finishedEls = $(this.taxonomicCoverageTemplate({ generalTaxonomicCoverage: coverage.get('generalTaxonomicCoverage') || "" })), coverageEl = finishedEls.filter(".taxonomic-coverage"); coverageEl.data({ model: coverage }); var classifications = coverage.get("taxonomicClassification"); // Makes a table... for the root level for (var i = 0; i < classifications.length; i++) { coverageEl.append(this.createTaxonomicClassifcationTable(classifications[i])); } // Create a new, blank table for another taxonomicClassification var newTableEl = this.createTaxonomicClassifcationTable(); coverageEl.append(newTableEl); return finishedEls; }, createTaxonomicClassifcationTable: function(classification) { // updating the taxonomic table indexes before adding a new table to the page. var taxaNums = this.$(".editor-header-index"); for (var i = 0; i < taxaNums.length; i++) { $(taxaNums[i]).text(i + 1); } // Adding the taxoSpeciesCounter to the table header for enhancement of the view var finishedEl = $(''); $(finishedEl).append('