/*global define */ define(['jquery', 'jqueryui', 'underscore', 'backbone', 'gmaps', 'fancybox', 'clipboard', 'collections/DataPackage', 'models/DataONEObject', 'models/PackageModel', 'models/SolrResult', 'models/metadata/ScienceMetadata', 'models/MetricsModel', 'models/Utilities', 'views/DownloadButtonView', 'views/ProvChartView', 'views/MetadataIndexView', 'views/ExpandCollapseListView', 'views/ProvStatementView', 'views/PackageTableView', 'views/CitationView', 'views/AnnotationView', 'views/MarkdownView', 'text!templates/metadata/metadata.html', 'text!templates/dataSource.html', 'text!templates/publishDOI.html', 'text!templates/newerVersion.html', 'text!templates/loading.html', 'text!templates/metadataControls.html', 'text!templates/metadataInfoIcons.html', 'text!templates/alert.html', 'text!templates/editMetadata.html', 'text!templates/dataDisplay.html', 'text!templates/map.html', 'text!templates/annotation.html', 'text!templates/metaTagsHighwirePress.html', 'uuid', 'views/MetricView', ], function($, $ui, _, Backbone, gmaps, fancybox, Clipboard, DataPackage, DataONEObject, Package, SolrResult, ScienceMetadata, MetricsModel, Utilities, DownloadButtonView, ProvChart, MetadataIndex, ExpandCollapseList, ProvStatement, PackageTable, CitationView, AnnotationView, MarkdownView, MetadataTemplate, DataSourceTemplate, PublishDoiTemplate, VersionTemplate, LoadingTemplate, ControlsTemplate, MetadataInfoIconsTemplate, AlertTemplate, EditMetadataTemplate, DataDisplayTemplate, MapTemplate, AnnotationTemplate, metaTagsHighwirePressTemplate, uuid, MetricView) { 'use strict'; /** * @class MetadataView * @classdesc A human-readable view of a science metadata file * @classcategory Views * @extends Backbone.View * @constructor */ var MetadataView = Backbone.View.extend( /** @lends MetadataView.prototype */{ subviews: [], pid: null, seriesId: null, saveProvPending: false, model: new SolrResult(), packageModels: new Array(), dataPackage: null, el: '#Content', metadataContainer: "#metadata-container", citationContainer: "#citation-container", tableContainer: "#table-container", controlsContainer: "#metadata-controls-container", metricsContainer: "#metrics-controls-container", ownerControlsContainer: "#owner-controls-container", breadcrumbContainer: "#breadcrumb-container", parentLinkContainer: "#parent-link-container", dataSourceContainer: "#data-source-container", articleContainer: "#article-container", type: "Metadata", //Templates template: _.template(MetadataTemplate), alertTemplate: _.template(AlertTemplate), doiTemplate: _.template(PublishDoiTemplate), versionTemplate: _.template(VersionTemplate), loadingTemplate: _.template(LoadingTemplate), controlsTemplate: _.template(ControlsTemplate), infoIconsTemplate: _.template(MetadataInfoIconsTemplate), dataSourceTemplate: _.template(DataSourceTemplate), editMetadataTemplate: _.template(EditMetadataTemplate), dataDisplayTemplate: _.template(DataDisplayTemplate), mapTemplate: _.template(MapTemplate), metaTagsHighwirePressTemplate: _.template(metaTagsHighwirePressTemplate), objectIds: [], // Delegated events for creating new items, and clearing completed ones. events: { "click #publish" : "publish", "mouseover .highlight-node" : "highlightNode", "mouseout .highlight-node" : "highlightNode", "click .preview" : "previewData", "click #save-metadata-prov" : "saveProv" }, initialize: function (options) { if((options === undefined) || (!options)) var options = {}; this.pid = options.pid || options.id || MetacatUI.appModel.get("pid") || null; if(typeof options.el !== "undefined") this.setElement(options.el); }, // Render the main metadata view render: function () { this.stopListening(); MetacatUI.appModel.set('headerType', 'default'); // this.showLoading("Loading..."); //Reset various properties of this view first this.classMap = new Array(); this.subviews = new Array(); this.model.set(this.model.defaults); this.packageModels = new Array(); // get the pid to render if(!this.pid) this.pid = MetacatUI.appModel.get("pid"); this.listenTo(MetacatUI.appUserModel, "change:loggedIn", this.render); //Listen to when the metadata has been rendered this.once("metadataLoaded", function(){ this.createAnnotationViews(); this.insertMarkdownViews(); }); //Listen to when the package table has been rendered this.once("packageTableRendered", function(){ //Scroll to the element on the page that is in the hash fragment (if there is one) this.scrollToFragment(); }); this.getModel(); return this; }, getDataPackage: function(pid) { // Get the metadata model that is associated with this DataPackage collection //var metadataModel = new ScienceMetadata({ id: this.pid }); // Once the ScienceMetadata is populated, populate the associated package //this.metadataModel = metadataModel; //Create a DataONEObject model to use in the DataPackage collection. var dataOneObject = new ScienceMetadata({ id: this.model.get("id") }); // Create a new data package with this id this.dataPackage = new DataPackage([dataOneObject], {id: pid}); this.dataPackage.mergeModels([ this.model ]); //Fetch the data package. DataPackage.parse() triggers 'complete' this.dataPackage.fetch({ fetchModels: false }); this.listenToOnce(this.dataPackage, "complete", function() { // parseProv triggers "queryComplete" this.dataPackage.parseProv(); this.checkForProv(); var packageTableView = _.findWhere(this.subviews, { type: "PackageTable"}); if( packageTableView ){ packageTableView.dataPackageCollection = this.dataPackage; packageTableView.checkForPrivateMembers(); } }); }, /* * Retrieves information from the index about this object, given the id (passed from the URL) * When the object info is retrieved from the index, we set up models depending on the type of object this is */ getModel: function(pid){ //Get the pid and sid if((typeof pid === "undefined") || !pid) var pid = this.pid; if((typeof this.seriesId !== "undefined") && this.seriesId) var sid = this.seriesId; //Get the package ID this.model.set({ id: pid, seriesId: sid }); var model = this.model; this.listenToOnce(model, "sync", function(){ if(this.model.get("formatType") == "METADATA" || !this.model.get("formatType")){ this.model = model; this.renderMetadata(); } else if(this.model.get("formatType") == "DATA"){ //Get the metadata pids that document this data object var isDocBy = this.model.get("isDocumentedBy"); //If there is only one metadata pid that documents this data object, then // get that metadata model for this view. if(isDocBy && isDocBy.length == 1){ this.pid = _.first(isDocBy); this.getModel(this.pid); return; } //If more than one metadata doc documents this data object, it is most likely // multiple versions of the same metadata. So we need to find the latest version. else if( isDocBy && isDocBy.length > 1 ){ var view = this; require(["collections/Filters", "collections/SolrResults"], function(Filters, SolrResults){ //Create a search for the metadata docs that document this data object var searchFilters = new Filters([{ values: isDocBy, fields: ["id", "seriesId"], operator: "OR", matchSubstring: false }]), //Create a list of search results searchResults = new SolrResults([], { rows: isDocBy.length, query: searchFilters.getQuery(), fields: "obsoletes,obsoletedBy,id" }); //When the search results are returned, process those results view.listenToOnce(searchResults, "sync", function(searchResults){ //Keep track of the latest version of the metadata doc(s) var latestVersions = []; //Iterate over each search result and find the latest version of each metadata version chain searchResults.each( function(searchResult){ //If this metadata isn't obsoleted by another object, it is the latest version if( !searchResult.get("obsoletedBy") ){ latestVersions.push( searchResult.get("id") ); } //If it is obsoleted by another object but that newer object does not document this data, then this is the latest version else if( !_.contains(isDocBy, searchResult.get("obsoletedBy")) ){ latestVersions.push( searchResult.get("id") ); } }, view); //If at least one latest version was found (should always be the case), if( latestVersions.length ){ //Set that metadata pid as this view's pid and get that metadata model. // TODO: Support navigation to multiple metadata docs. This should be a rare occurence, but // it is possible that more than one metadata version chain documents a data object, and we need // to show the user that the data is involved in multiple datasets. view.pid = latestVersions[0]; view.getModel(latestVersions[0]); } //If a latest version wasn't found, which should never happen, but just in case, default to the // last metadata pid in the isDocumentedBy field (most liekly to be the most recent since it was indexed last). else{ var fallbackPid = _.last(isDocBy); view.pid = fallbackPid; view.getModel(fallbackPid); } }); //Send the query to the Solr search service searchResults.query(); }); return; } else{ this.noMetadata(this.model); } } else if(this.model.get("formatType") == "RESOURCE"){ var packageModel = new Package({ id: this.model.get("id") }); packageModel.on("complete", function(){ var metadata = packageModel.getMetadata(); if(!metadata){ this.noMetadata(packageModel); } else{ this.model = metadata; this.pid = this.model.get("id"); this.renderMetadata(); if(this.model.get("resourceMap")) this.getPackageDetails(this.model.get("resourceMap")); } }, this); packageModel.getMembers(); return; } //Get the package information this.getPackageDetails(model.get("resourceMap")); }); //Listen to 404 and 401 errors when we get the metadata object this.listenToOnce(model, "404", this.showNotFound); this.listenToOnce(model, "401", this.showIsPrivate); //Fetch the model model.getInfo(); }, renderMetadata: function(){ var pid = this.model.get("id"); this.hideLoading(); //Load the template which holds the basic structure of the view this.$el.html(this.template()); this.$(this.tableContainer).html(this.loadingTemplate({ msg: "Retrieving data set details..." })); //Insert the breadcrumbs this.insertBreadcrumbs(); //Insert the citation this.insertCitation(); //Insert the data source logo this.insertDataSource(); // is this the latest version? (includes DOI link when needed) this.showLatestVersion(); // Insert various metadata controls in the page this.insertControls(); // If we're displaying the metrics well then display copy citation and edit button // inside the well if (MetacatUI.appModel.get("displayDatasetMetrics")) { //Insert Metrics Stats into the dataset landing pages this.insertMetricsControls(); } // Edit button and the publish button this.insertOwnerControls(); //Show loading icon in metadata section this.$(this.metadataContainer).html(this.loadingTemplate({ msg: "Retrieving metadata ..." })); // Check for a view service in this MetacatUI.appModel if((MetacatUI.appModel.get('viewServiceUrl') !== undefined) && (MetacatUI.appModel.get('viewServiceUrl'))) var endpoint = MetacatUI.appModel.get('viewServiceUrl') + encodeURIComponent(pid); if(endpoint && (typeof endpoint !== "undefined")){ var viewRef = this; var loadSettings = { url: endpoint, success: function(response, status, xhr) { //If the user has navigated away from the MetadataView, then don't render anything further if(MetacatUI.appView.currentView != viewRef) return; //Our fallback is to show the metadata details from the Solr index if (status=="error") viewRef.renderMetadataFromIndex(); else{ //Check for a response that is a 200 OK status, but is an error msg if((response.length < 250) && (response.indexOf("Error transforming document") > -1) && viewRef.model.get("indexed")){ viewRef.renderMetadataFromIndex(); return; } //Mark this as a metadata doc with no stylesheet, or one that is at least different than usual EML and FGDC else if((response.indexOf('id="Metadata"') == -1)){ viewRef.$el.addClass("container no-stylesheet"); if(viewRef.model.get("indexed")){ viewRef.renderMetadataFromIndex(); return; } } //Now show the response from the view service viewRef.$(viewRef.metadataContainer).html(response); //If there is no info from the index and there is no metadata doc rendered either, then display a message if(viewRef.$el.is(".no-stylesheet") && viewRef.model.get("archived") && !viewRef.model.get("indexed")) viewRef.$(viewRef.metadataContainer).prepend(viewRef.alertTemplate({ msg: "There is limited metadata about this dataset since it has been archived." })); viewRef.alterMarkup(); viewRef.trigger("metadataLoaded"); //Add a map of the spatial coverage if(gmaps) viewRef.insertSpatialCoverageMap(); // Injects Clipboard objects into DOM elements returned from the View Service viewRef.insertCopiables(); viewRef.setUpAnnotator(); } }, error: function(xhr, textStatus, errorThrown){ viewRef.renderMetadataFromIndex(); } } $.ajax(_.extend(loadSettings, MetacatUI.appUserModel.createAjaxSettings())); } else this.renderMetadataFromIndex(); // Insert the Linked Data into the header of the page. if (MetacatUI.appModel.get("isJSONLDEnabled")) { var json = this.generateJSONLD(); this.insertJSONLD(json); } this.insertCitationMetaTags(); }, /* If there is no view service available, then display the metadata fields from the index */ renderMetadataFromIndex: function(){ var metadataFromIndex = new MetadataIndex({ pid: this.pid, parentView: this }); this.subviews.push(metadataFromIndex); //Add the metadata HTML this.$(this.metadataContainer).html(metadataFromIndex.render().el); var view = this; this.listenTo(metadataFromIndex, "complete", function(){ //Add the package contents view.insertPackageDetails(); //Add a map of the spatial coverage if(gmaps) view.insertSpatialCoverageMap(); // render annotator from index content, too view.setUpAnnotator(); }); }, removeCitation: function(){ var citation = "", citationEl = null; //Find the citation element if(this.$(".citation").length > 0){ //Get the text for the citation citation = this.$(".citation").text(); //Save this element in the view citationEl = this.$(".citation"); } //Older versions of Metacat (v2.4.3 and older) will not have the citation class in the XSLT. Find the citation another way else{ //Find the DOM element with the citation var wells = this.$('.well'), viewRef = this; //Find the div.well with the citation. If we never find it, we don't insert the list of contents _.each(wells, function(well){ if(!citationEl && ($(well).find('#viewMetadataCitationLink').length > 0) || ($(well).children(".row-fluid > .span10 > a"))){ //Save this element in the view citationEl = well; //Mark this in the DOM for CSS styling $(well).addClass('citation'); //Save the text of the citation citation = $(well).text(); } }); //Remove the unnecessary classes that are used in older versions of Metacat (2.4.3 and older) var citationText = $(citationEl).find(".span10"); $(citationText).removeClass("span10").addClass("span12"); } //Set the document title to the citation MetacatUI.appModel.set("title", citation); citationEl.remove(); }, insertBreadcrumbs: function(){ var breadcrumbs = $(document.createElement("ol")) .addClass("breadcrumb") .append($(document.createElement("li")) .addClass("home") .append($(document.createElement("a")) .attr("href", MetacatUI.root || "/") .addClass("home") .text("Home"))) .append($(document.createElement("li")) .addClass("search") .append($(document.createElement("a")) .attr("href", MetacatUI.root + "/data" + ((MetacatUI.appModel.get("page") > 0)? ("/page/" + (parseInt(MetacatUI.appModel.get("page"))+1)) : "")) .addClass("search") .text("Search"))) .append($(document.createElement("li")) .append($(document.createElement("a")) .attr("href", MetacatUI.root + "/view/" + encodeURIComponent(this.pid)) .addClass("inactive") .text("Metadata"))); if(MetacatUI.uiRouter.lastRoute() == "data"){ $(breadcrumbs).prepend($(document.createElement("a")) .attr("href", MetacatUI.root + "/data/page/" + ((MetacatUI.appModel.get("page") > 0)? (parseInt(MetacatUI.appModel.get("page"))+1) : "")) .attr("title", "Back") .addClass("back") .text(" Back to search") .prepend($(document.createElement("i")) .addClass("icon-angle-left"))); $(breadcrumbs).find("a.search").addClass("inactive"); } this.$(this.breadcrumbContainer).html(breadcrumbs); }, /* * When the metadata object doesn't exist, display a message to the user */ showNotFound: function(){ //If the model was found, exit this function if(!this.model.get("notFound")){ return; } //Construct a message that shows this object doesn't exist var msg = "

Nothing was found.

" + "

The dataset identifier '" + Utilities.encodeHTML(this.model.get("id")) + "' " + "does not exist or it may have been removed. Search for " + "datasets that mention " + Utilities.encodeHTML(this.model.get("id")) + "

"; //Remove the loading message this.hideLoading(); //Show the not found error message this.showError(msg); //Add the pid to the link href. Add via JS so it is Attribute-encoded to prevent XSS attacks this.$("#metadata-view-not-found-message a").attr("href", MetacatUI.root + "/data/query=" + encodeURIComponent(this.model.get("id"))); }, /* * When the metadata object is private, display a message to the user */ showIsPrivate: function(){ //If we haven't checked the logged-in status of the user yet, wait a bit //until we show a 401 msg, in case this content is their private content if(!MetacatUI.appUserModel.get("checked")){ this.listenToOnce(MetacatUI.appUserModel, "change:checked", this.showIsPrivate); return; } //If the user is logged in, the message will display that this dataset is private. if( MetacatUI.appUserModel.get("loggedIn") ){ var msg = '' + '' + '' + ' This is a private dataset.'; } //If the user isn't logged in, display a log in link. else{ var msg = '' + '' + '' + ' This is a private dataset. If you believe you have permission ' + 'to access this dataset, then sign in.'; } //Remove the loading message this.hideLoading(); //Show the not found error message this.showError(msg); }, getPackageDetails: function(packageIDs){ var completePackages = 0; //This isn't a package, but just a lonely metadata doc... if(!packageIDs || !packageIDs.length){ var thisPackage = new Package({ id: null, members: [this.model] }); thisPackage.flagComplete(); this.packageModels = [thisPackage]; this.insertPackageDetails(thisPackage); } else{ _.each(packageIDs, function(thisPackageID, i){ //Create a model representing the data package var thisPackage = new Package({ id: thisPackageID }); //Listen for any parent packages this.listenToOnce(thisPackage, "change:parentPackageMetadata", this.insertParentLink); //When the package info is fully retrieved this.listenToOnce(thisPackage, 'complete', function(thisPackage){ //When all packages are fully retrieved completePackages++; if(completePackages >= packageIDs.length){ var latestPackages = _.filter(this.packageModels, function(m){ return !_.contains(packageIDs, m.get("obsoletedBy")); }); this.packageModels = latestPackages; this.insertPackageDetails(latestPackages); } }); //Save the package in the view this.packageModels.push(thisPackage); //Make sure we get archived content, too thisPackage.set("getArchivedMembers", true); //Get the members thisPackage.getMembers({getParentMetadata: true }); }, this); } }, alterMarkup: function(){ //Find the taxonomic range and give it a class for styling - for older versions of Metacat only (v2.4.3 and older) if(!this.$(".taxonomicCoverage").length) this.$('h4:contains("Taxonomic Range")').parent().addClass('taxonomicCoverage'); //Remove ecogrid links and replace them with workable links this.replaceEcoGridLinks(); //Find the tab links for attribute names this.$(".attributeListTable tr a").on('shown', function(e){ //When the attribute link is clicked on, highlight the tab as active $(e.target).parents(".attributeListTable").find(".active").removeClass("active"); $(e.target).parents("tr").first().addClass("active"); }); //Mark the first row in each attribute list table as active since the first attribute is displayed at first this.$(".attributeListTable tr:first-child()").addClass("active"); }, /* * Inserts a table with all the data package member information and sends the call to display annotations */ insertPackageDetails: function(packages){ //Don't insert the package details twice var tableEls = this.$(this.tableContainer).children().not(".loading"); if(tableEls.length > 0) return; //wait for the metadata to load var metadataEls = this.$(this.metadataContainer).children(); if(!metadataEls.length || metadataEls.first().is(".loading")){ this.once("metadataLoaded", this.insertPackageDetails); return; } if(!packages) var packages = this.packageModels; //Get the entity names from this page/metadata this.getEntityNames(packages); _.each(packages, function(packageModel){ //If the package model is not complete, don't do anything if(!packageModel.complete) return; //Insert a package table for each package in viewRef dataset var nestedPckgs = packageModel.getNestedPackages(), nestedPckgsToDisplay = []; //If this metadata is not archived, filter out archived packages if( !this.model.get("archived") ){ nestedPckgsToDisplay = _.reject(nestedPckgs, function(pkg){ return (pkg.get("archived")) }); } else{ //Display all packages is this metadata is archived nestedPckgsToDisplay = nestedPckgs; } if(nestedPckgsToDisplay.length > 0){ if( !(!this.model.get("archived") && packageModel.get("archived") == true) ){ var title = 'Current Data Set (1 of ' + (nestedPckgsToDisplay.length + 1) + ') Package: ' + packageModel.get("id") + ''; this.insertPackageTable(packageModel, { title: title }); } _.each(nestedPckgsToDisplay, function(nestedPackage, i, list){ if( !(!this.model.get("archived") && nestedPackage.get("archived") == true) ){ var title = 'Nested Data Set (' + (i+2) + ' of ' + (list.length+1) + ') Package: ' + nestedPackage.get("id") + ' (View ) '; this.insertPackageTable(nestedPackage, { title: title, nested: true }); } }, this); } else{ //If this metadata is not archived, then don't display archived packages if( !(!this.model.get("archived") && packageModel.get("archived") == true) ){ var title = packageModel.get("id") ? 'Package: ' + packageModel.get("id") + '' : ""; title = "Files in this dataset " + title; this.insertPackageTable(packageModel, {title: title}); } } //Remove the extra download button returned from the XSLT since the package table will have all the download links $("#downloadPackage").remove(); }, this); //Collapse the table list after the first table var additionalTables = $(this.$("#additional-tables-for-" + this.cid)), numTables = additionalTables.children(".download-contents").length, item = (numTables == 1)? "dataset" : "datasets"; if(numTables > 0){ var expandIcon = $(document.createElement("i")).addClass("icon icon-level-down"), expandLink = $(document.createElement("a")) .attr("href", "#") .addClass("toggle-slide toggle-display-on-slide") .attr("data-slide-el", "additional-tables-for-" + this.cid) .text("Show " + numTables + " nested " + item) .prepend(expandIcon), collapseLink = $(document.createElement("a")) .attr("href", "#") .addClass("toggle-slide toggle-display-on-slide") .attr("data-slide-el", "additional-tables-for-" + this.cid) .text("Hide nested " + item) .hide(), expandControl = $(document.createElement("div")).addClass("expand-collapse-control").append(expandLink, collapseLink); additionalTables.before(expandControl); } //If this metadata doc is not in a package, but is just a lonely metadata doc... if(!packages.length){ var packageModel = new Package({ members: [this.model], }); packageModel.complete = true; this.insertPackageTable(packageModel); } //Insert the data details sections this.insertDataDetails(); // Get DataPackge info in order to render prov extraced from the resmap. if(packages.length) this.getDataPackage(packages[0].get("id")); //Initialize tooltips in the package table(s) this.$(".tooltip-this").tooltip(); return this; }, insertPackageTable: function(packageModel, options){ var viewRef = this; if(options){ var title = options.title || ""; var nested = (typeof options.nested === "undefined")? false : options.nested; } else var title = "", nested = false; if(typeof packageModel === "undefined") return; //** Draw the package table **// var tableView = new PackageTable({ model: packageModel, currentlyViewing: this.pid, parentView: this, title: title, nested: nested, metricsModel: this.metricsModel }); //Get the package table container var tablesContainer = this.$(this.tableContainer); //After the first table, start collapsing them var numTables = $(tablesContainer).find("table.download-contents").length; if(numTables == 1){ var tableContainer = $(document.createElement("div")).attr("id", "additional-tables-for-" + this.cid); tableContainer.hide(); $(tablesContainer).append(tableContainer); } else if(numTables > 1) var tableContainer = this.$("#additional-tables-for-" + this.cid); else var tableContainer = tablesContainer; //Insert the package table HTML $(tableContainer).append(tableView.render().el); $(this.tableContainer).children(".loading").remove(); $(tableContainer).find(".tooltip-this").tooltip(); this.subviews.push(tableView); //Trigger a custom event in this view that indicates the package table has been rendered this.trigger("packageTableRendered"); }, insertParentLink: function(packageModel){ var parentPackageMetadata = packageModel.get("parentPackageMetadata"), view = this; _.each(parentPackageMetadata, function(m, i){ var title = m.get("title"), icon = $(document.createElement("i")).addClass("icon icon-on-left icon-level-up"), link = $(document.createElement("a")).attr("href", MetacatUI.root + "/view/" + encodeURIComponent(m.get("id"))) .addClass("parent-link") .text("Parent dataset: " + title) .prepend(icon); view.$(view.parentLinkContainer).append(link); }); }, insertSpatialCoverageMap: function(customCoordinates){ //Find the geographic region container. Older versions of Metacat (v2.4.3 and less) will not have it classified so look for the header text if(!this.$(".geographicCoverage").length){ //For EML var title = this.$('h4:contains("Geographic Region")'); //For FGDC if(title.length == 0){ title = this.$('label:contains("Bounding Coordinates")'); } var georegionEls = $(title).parent(); var parseText = true; var directions = new Array('North', 'South', 'East', 'West'); } else{ var georegionEls = this.$(".geographicCoverage"); var directions = new Array('north', 'south', 'east', 'west'); } for(var i=0; i -1) coordinate = coordinate.substring(0, coordinate.indexOf(" ")); } } else{ var coordinate = $(georegion).find("." + direction + "BoundingCoordinate").attr("data-value"); } //Save our coordinate value coordinates.push(coordinate); }); //Extract the coordinates var n = coordinates[0]; var s = coordinates[1]; var e = coordinates[2]; var w = coordinates[3]; } //Create Google Map LatLng objects out of our coordinates var latLngSW = new gmaps.LatLng(s, w); var latLngNE = new gmaps.LatLng(n, e); var latLngNW = new gmaps.LatLng(n, w); var latLngSE = new gmaps.LatLng(s, e); //Get the centertroid location of this data item var bounds = new gmaps.LatLngBounds(latLngSW, latLngNE); var latLngCEN = bounds.getCenter(); //If there isn't a center point found, don't draw the map. if( typeof latLngCEN == "undefined" ){ return; } var url = "https://maps.google.com/?ll=" + latLngCEN.lat() + "," + latLngCEN.lng() + "&spn=0.003833,0.010568" + "&t=m" + "&z=5"; //Get the map path color var pathColor = MetacatUI.appModel.get("datasetMapPathColor"); if( pathColor ){ pathColor = "color:" + pathColor + "|"; } else{ pathColor = ""; } //Get the map path fill color var fillColor = MetacatUI.appModel.get("datasetMapFillColor"); if( fillColor ){ fillColor = "fillcolor:" + fillColor + "|"; } else{ fillColor = ""; } //Create a google map image var mapHTML = ""; //Find the spot in the DOM to insert our map image if(parseText) var insertAfter = ($(georegion).find('label:contains("West")').parent().parent().length) ? $(georegion).find('label:contains("West")').parent().parent() : georegion; //The last coordinate listed else var insertAfter = georegion; $(insertAfter).append(this.mapTemplate({ map: mapHTML, url: url })); $('.fancybox-media').fancybox({ openEffect : 'elastic', closeEffect : 'elastic', helpers: { media: {} } }) } return true; }, insertCitation: function(){ if(!this.model) return false; //Create a citation element from the model attributes var citation = new CitationView({ model: this.model, createLink: false }).render().el; this.$(this.citationContainer).html(citation); }, insertDataSource: function(){ if(!this.model || !MetacatUI.nodeModel || !MetacatUI.nodeModel.get("members").length || !this.$(this.dataSourceContainer).length) return; var dataSource = MetacatUI.nodeModel.getMember(this.model), replicaMNs = MetacatUI.nodeModel.getMembers(this.model.get("replicaMN")); //Filter out the data source from the replica nodes if(Array.isArray(replicaMNs) && replicaMNs.length){ replicaMNs = _.without(replicaMNs, dataSource); } if(dataSource && dataSource.logo){ this.$("img.data-source").remove(); //Construct a URL to the profile of this repository var profileURL = (dataSource.identifier == MetacatUI.appModel.get("nodeId"))? MetacatUI.root + "/profile" : MetacatUI.appModel.get("dataoneSearchUrl") + "/portals/" + dataSource.shortIdentifier; //Insert the data source template this.$(this.dataSourceContainer).html(this.dataSourceTemplate({ node : dataSource, profileURL: profileURL })).addClass("has-data-source"); this.$(this.citationContainer).addClass("has-data-source"); this.$(".tooltip-this").tooltip(); $(".popover-this.data-source.logo").popover({ trigger: "manual", html: true, title: "From the " + dataSource.name + " repository", content: function(){ var content = "

" + dataSource.description + "

"; if(replicaMNs.length){ content += '
Exact copies hosted by ' + replicaMNs.length + ' repositories:
"; } return content; }, animation:false }) .on("mouseenter", function () { var _this = this; $(this).popover("show"); $(".popover").on("mouseleave", function () { $(_this).popover('hide'); }); }).on("mouseleave", function () { var _this = this; setTimeout(function () { if (!$(".popover:hover").length) { $(_this).popover("hide"); } }, 300); }); } }, /* * Checks the authority for the logged in user for this dataset * and inserts control elements onto the page for the user to interact with the dataset - edit, publish, etc. */ insertOwnerControls: function(){ //Do not show user controls for older versions of data sets if(this.model.get("obsoletedBy") && (this.model.get("obsoletedBy").length > 0)) return false; var container = this.$(this.ownerControlsContainer); //Save some references var pid = this.model.get("id") || this.pid, model = this.model, viewRef = this; this.listenToOnce(this.model, "change:isAuthorized", function(){ if(!model.get("isAuthorized") || model.get("archived")) return false; //Insert an Edit button if the Edit button is enabled if( MetacatUI.appModel.get("displayDatasetEditButton") ){ //Check that this is an editable metadata format if( _.contains(MetacatUI.appModel.get("editableFormats"), this.model.get("formatId")) ){ //Insert the Edit Metadata template container.append( this.editMetadataTemplate({ identifier: pid, supported: true })); } //If this format is not editable, insert an unspported Edit Metadata template else{ container.append(this.editMetadataTemplate({ supported: false })); } } try{ //Determine if this metadata can be published. // The Publish feature has to be enabled in the app. // The model cannot already have a DOI var canBePublished = MetacatUI.appModel.get("enablePublishDOI") && !model.isDOI(); //If publishing is enabled, check if only certain users and groups can publish metadata if( canBePublished ){ //Get the list of authorized publishers from the AppModel var authorizedPublishers = MetacatUI.appModel.get("enablePublishDOIForSubjects"); //If the logged-in user is one of the subjects in the list or is in a group that is // in the list, then this metadata can be published. Otherwise, it cannot. if( Array.isArray(authorizedPublishers) && authorizedPublishers.length ){ if( MetacatUI.appUserModel.hasIdentityOverlap(authorizedPublishers) ){ canBePublished = true; } else{ canBePublished = false; } } } //If this metadata can be published, then insert the Publish button template if( canBePublished ){ //Insert a Publish button template container.append( viewRef.doiTemplate({ isAuthorized: true, identifier: pid })); } } catch(e){ console.error("Cannot display the publish button: ", e); } //Check the authority on the package models //If there is no package, then exit now if(!viewRef.packageModels || !viewRef.packageModels.length) return; //Check for authorization on the resource map var packageModel = this.packageModels[0]; //if there is no package, then exit now if(!packageModel.get("id")) return; //Now get the RDF XML and check for the user's authority on this resource map packageModel.fetch(); packageModel.checkAuthority(); }); //Check if the current user has authority to `changePermission` on this metadata this.model.checkAuthority(); }, /* * Injects Clipboard objects onto DOM elements returned from the Metacat * View Service. This code depends on the implementation of the Metacat * View Service in that it depends on elements with the class "copy" being * contained in the HTML returned from the View Service. * * To add more copiable buttons (or other elements) to a View Service XSLT, * you should be able to just add something like: * * * * to your XSLT and this should pick it up automatically. */ insertCopiables: function(){ var copiables = $("#Metadata .copy"); _.each(copiables, function(copiable) { var clipboard = new Clipboard(copiable); clipboard.on("success", function(e) { var el = $(e.trigger); $(el).html( $(document.createElement("span")).addClass("icon icon-ok success") ); // Use setTimeout instead of jQuery's built-in Events system because // it didn't look flexible enough to allow me update innerHTML in // a chain setTimeout(function() { $(el).html("Copy"); }, 500) }); }); }, /* * Inserts elements users can use to interact with this dataset: * - A "Copy Citation" button to copy the citation text */ insertControls: function(){ // Convert the support mdq formatId list to a version // that JS regex likes (with special characters double RegExp.escape = function(s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\\\$&'); }; var mdqFormatIds = MetacatUI.appModel.get("mdqFormatIds"); // Check of the current formatId is supported by the current // metadata quality suite. If not, the 'Assessment Report' button // will not be displacyed in the metadata controls panel. var thisFormatId = this.model.get("formatId"); var mdqFormatSupported = false; var formatFound = false; if(mdqFormatIds !== null) { for (var ifmt = 0; ifmt < mdqFormatIds.length; ++ifmt) { var currentFormatId = RegExp.escape(mdqFormatIds[ifmt]); var re = new RegExp(currentFormatId); formatFound = re.test(thisFormatId); if(formatFound) { break; } } } //Get template var controlsContainer = this.controlsTemplate({ citationTarget: this.citationContainer, url: window.location, displayQualtyReport: MetacatUI.appModel.get("mdqBaseUrl") && formatFound && MetacatUI.appModel.get("displayDatasetQualityMetric"), showWholetale: MetacatUI.appModel.get("showWholeTaleFeatures"), model: this.model.toJSON() }); $(this.controlsContainer).html(controlsContainer); var view = this; //Insert the info icons var metricsWell = this.$(".metrics-container"); metricsWell.append( this.infoIconsTemplate({ model: this.model.toJSON() }) ); if(MetacatUI.appModel.get("showWholeTaleFeatures")) { this.createWholeTaleButton (); } //Create clickable "Copy" buttons to copy text (e.g. citation) to the user's clipboard var copyBtns = $(this.controlsContainer).find(".copy"); _.each(copyBtns, function(btn){ //Create a copy citation button var clipboard = new Clipboard(btn); clipboard.on("success", function(e){ var originalWidth = $(e.trigger).width(); $(e.trigger).html( $(document.createElement("span")).addClass("icon icon-ok success") ) .append(" Copied") .addClass("success") .css("width", originalWidth + "px"); setTimeout(function() { $(e.trigger).html(" Copy Citation") .removeClass("success") .css("width", "auto"); }, 500) }); clipboard.on("error", function(e){ if(!$(e.trigger).prev("input.copy").length){ var textarea = $(document.createElement("input")).val($(e.trigger).attr("data-clipboard-text")).addClass("copy").css("width", "0"); textarea.tooltip({ title: "Press Ctrl+c to copy", placement: "top" }); $(e.trigger).before(textarea); } else{ var textarea = $(e.trigger).prev("input.copy"); } textarea.animate({ width: "100px" }, { duration: "slow", complete: function(){ textarea.trigger("focus"); textarea.tooltip("show"); } }); textarea.focusout(function(){ textarea.animate({ width: "0px" }, function(){ textarea.remove(); }) }); }); }); this.$(".tooltip-this").tooltip(); }, /** *Creates a button which the user can click to launch the package in Whole Tale */ createWholeTaleButton: function() { let self=this; MetacatUI.appModel.get('taleEnvironments').forEach(function(environment){ var queryParams= '?uri='+ window.location.href+ '&title='+encodeURIComponent(self.model.get("title"))+ '&environment='+environment+ '&api='+MetacatUI.appModel.get("d1CNBaseUrl")+MetacatUI.appModel.get("d1CNService"); var composeUrl = MetacatUI.appModel.get('dashboardUrl')+queryParams; var anchor = $(''); anchor.attr('href',composeUrl).append( $('').attr('class', 'tab').append(environment)); anchor.attr('target', '_blank'); $('.analyze.dropdown-menu').append($('
  • ').append(anchor)); }); }, // Inserting the Metric Stats insertMetricsControls: function() { //Exit if metrics shouldn't be shown for this dataset if( this.model.hideMetrics() ){ return; } var pid_list = []; pid_list.push(this.pid); var metricsModel = new MetricsModel({pid_list: pid_list, type: "dataset"}); metricsModel.fetch(); this.metricsModel = metricsModel; // Retreive the model from the server for the given PID // TODO: Create a Metric Request Object if (MetacatUI.appModel.get("displayDatasetMetrics")) { var buttonToolbar = this.$(".metrics-container"); if (MetacatUI.appModel.get("displayDatasetCitationMetric")) { var citationsMetricView = new MetricView({metricName: 'Citations', model: metricsModel, pid: this.pid}); buttonToolbar.append(citationsMetricView.render().el); this.subviews.push(citationsMetricView); } if (MetacatUI.appModel.get("displayDatasetDownloadMetric")) { var downloadsMetricView = new MetricView({metricName: 'Downloads', model: metricsModel, pid: this.pid}); buttonToolbar.append(downloadsMetricView.render().el); this.subviews.push(downloadsMetricView); } if (MetacatUI.appModel.get("displayDatasetViewMetric")) { var viewsMetricView = new MetricView({metricName: 'Views', model: metricsModel, pid: this.pid}); buttonToolbar.append(viewsMetricView.render().el); this.subviews.push(viewsMetricView); } } /* if(MetacatUI.appModel.get("displayDatasetControls")) { var controlsToolbar = $(document.createElement("div")).addClass("edit-toolbar btn-toolbar"); var copyCitationToolbar = this.$(this.controlsContainer); //Insert controls this.insertControls(); controlsToolbar.append(copyCitationToolbar) if(MetacatUI.appModel.get("displayDatasetEditButton")) { var editToolbar = this.$(this.ownerControlsContainer); // Insert Owner Controls this.insertOwnerControls(); controlsToolbar.append(editToolbar) } metrics.append(controlsToolbar); } */ }, // Check if the DataPackage provenance parsing has completed. checkForProv: function() { // Show the provenance trace for this package var model = this.model; if(this.dataPackage.provenanceFlag == "complete") { this.drawProvCharts(this.dataPackage); // Check each prov chart to see if it has been marked for re-rendering and // redraw it if it has been. this.listenToOnce(this.dataPackage, "redrawProvCharts", this.redrawProvCharts); this.model.once("change:isAuthorized", this.redrawProvCharts, this); } else { this.listenToOnce(this.dataPackage, "queryComplete", function() { this.drawProvCharts(this.dataPackage); // Check each prov chart to see if it has been marked for re-rendering and // redraw it if it has been. this.listenToOnce(this.dataPackage, "redrawProvCharts", this.redrawProvCharts); model.once("change:isAuthorized", this.redrawProvCharts, this); }); } }, /* * Renders ProvChartViews on the page to display provenance on a package level and on an individual object level. * This function looks at four sources for the provenance - the package sources, the package derivations, member sources, and member derivations */ drawProvCharts: function(dataPackage){ //Provenance has to be retrieved from the Package Model (getProvTrace()) before the charts can be drawn if(dataPackage.provenanceFlag != "complete") return false; // If the user is authorized to edit the provenance for this package // then turn on editing, so that // edit icons are displayed. //var isAuthorized = true; var editModeOn = false; //If the user is authorized to edit this metadata doc, then turn edit mode on this.model.get("isAuthorized") ? editModeOn = true : editModeOn = false; //If this content is archived, then turn edit mode off if( this.model.get("archived") ){ editModeOn = false; } //If none of the models in this package have the formatId attributes, // we should fetch the DataPackage since it likely has only had a shallow fetch so far var formats = _.compact(dataPackage.pluck("formatId")); //If the number of formatIds is less than the number of models in this collection, // then we need to get them. if( formats.length < dataPackage.length ){ var modelsToMerge = []; //Get the PackageModel associated with this view if( this.packageModels.length ){ //Get the PackageModel for this DataPackage var packageModel = _.find(this.packageModels, function(packageModel){ return packageModel.get("id") == dataPackage.id }); //Merge the SolrResult models into the DataONEObject models if(packageModel && packageModel.get("members").length){ modelsToMerge = packageModel.get("members"); } } //If there is at least one model to merge into this data package, do so if( modelsToMerge.length ){ dataPackage.mergeModels(modelsToMerge); } //If there are no models to merge in, get them from the index else{ //Listen to the DataPackage fetch to complete and re-execute this function this.listenToOnce(dataPackage, "complete", function(){ this.drawProvCharts(dataPackage); }); //Create a query that searches for all the members of this DataPackage in Solr dataPackage.solrResults.currentquery = dataPackage.filterModel.getQuery() + "%20AND%20-formatType:METADATA"; dataPackage.solrResults.fields = "id,seriesId,formatId,fileName"; dataPackage.solrResults.rows = dataPackage.length; dataPackage.solrResults.sort = null; dataPackage.solrResults.start = 0; dataPackage.solrResults.facet = []; dataPackage.solrResults.stats = null; //Fetch the data package with the "fromIndex" option dataPackage.fetch({ fromIndex: true }); //Exit this function since it will be executed again when the fetch is complete return; } } var view = this; //Draw two flow charts to represent the sources and derivations at a package level var packageSources = dataPackage.sourcePackages; var packageDerivations = dataPackage.derivationPackages; if(Object.keys(packageSources).length){ var sourceProvChart = new ProvChart({ sources : packageSources, context : dataPackage, contextEl : this.$(this.articleContainer), dataPackage : dataPackage, parentView : view }); this.subviews.push(sourceProvChart); this.$(this.articleContainer).before(sourceProvChart.render().el); } if(Object.keys(packageDerivations).length){ var derivationProvChart = new ProvChart({ derivations : packageDerivations, context : dataPackage, contextEl : this.$(this.articleContainer), dataPackage : dataPackage, parentView : view }); this.subviews.push(derivationProvChart); this.$(this.articleContainer).after(derivationProvChart.render().el); } if(dataPackage.sources.length || dataPackage.derivations.length || editModeOn) { //Draw the provenance charts for each member of this package at an object level _.each(dataPackage.toArray(), function(member, i){ // Don't draw prov charts for metadata objects. if(member.get("type").toLowerCase() == "metadata" || member.get("formatType").toLowerCase() == "metadata"){ return; } var entityDetailsSection = view.findEntityDetailsContainer(member); if( !entityDetailsSection ){ return; } //Retrieve the sources and derivations for this member var memberSources = member.get("provSources") || new Array(), memberDerivations = member.get("provDerivations") || new Array(); //Make the source chart for this member. // If edit is on, then either a 'blank' sources ProvChart will be displayed if there // are no sources for this member, or edit icons will be displayed with prov icons. if(memberSources.length || editModeOn){ var memberSourcesProvChart = new ProvChart({ sources : memberSources, context : member, contextEl : entityDetailsSection, dataPackage : dataPackage, parentView : view, editModeOn : editModeOn, editorType : "sources" }); view.subviews.push(memberSourcesProvChart); $(entityDetailsSection).before(memberSourcesProvChart.render().el); view.$(view.articleContainer).addClass("gutters"); } //Make the derivation chart for this member // If edit is on, then either a 'blank' derivations ProvChart will be displayed if there, // are no derivations for this member or edit icons will be displayed with prov icons. if(memberDerivations.length || editModeOn){ var memberDerivationsProvChart = new ProvChart({ derivations : memberDerivations, context : member, contextEl : entityDetailsSection, dataPackage : dataPackage, parentView : view, editModeOn : editModeOn, editorType : "derivations" }); view.subviews.push(memberDerivationsProvChart); $(entityDetailsSection).after(memberDerivationsProvChart.render().el); view.$(view.articleContainer).addClass("gutters"); } }); } //Make all of the prov chart nodes look different based on id if(this.$(".prov-chart").length > 10000){ var allNodes = this.$(".prov-chart .node"), ids = [], view = this, i = 1; $(allNodes).each(function(){ ids.push($(this).attr("data-id"))}); ids = _.uniq(ids); _.each(ids, function(id){ var matchingNodes = view.$(".prov-chart .node[data-id='" + id + "']").not(".editorNode"); //var matchingEntityDetails = view.findEntityDetailsContainer(id); //Don't use the unique class on images since they will look a lot different anyway by their image if(!$(matchingNodes).first().hasClass("image")){ var className = "uniqueNode" + i; //Add the unique class and up the iterator if(matchingNodes.prop("tagName") != "polygon") $(matchingNodes).addClass(className); else $(matchingNodes).attr("class", $(matchingNodes).attr("class") + " " + className); /* if(matchingEntityDetails) $(matchingEntityDetails).addClass(className);*/ //Save this id->class mapping in this view view.classMap.push({ id : id, className : className }); i++; } }); } }, /* Step through all prov charts and re-render each one that has been marked for re-rendering. */ redrawProvCharts: function() { var view = this; // Check if prov edits are active and turn on the prov save bar if so. // Alternatively, turn off save bar if there are no prov edits, which // could occur if a user undoes a previous which could result in // an empty edit list. if(this.dataPackage.provEditsPending()) { this.showEditorControls(); } else { this.hideEditorControls(); // Reset the edited flag for each package member _.each(this.dataPackage.toArray(), function(item) { item.selectedInEditor == false; }); } _.each(this.subviews, function(thisView, i) { // Check if this is a ProvChartView if(thisView.className && thisView.className.indexOf("prov-chart") !== -1) { // Check if this ProvChartView is marked for re-rendering // Erase the current ProvChartView thisView.onClose(); } }); // Remove prov charts from the array of subviews. this.subviews = _.filter(this.subviews, function(item) { return (item.className && (item.className.indexOf("prov-chart") == -1)); }); view.drawProvCharts(this.dataPackage); view.listenToOnce(this.dataPackage, "redrawProvCharts", view.redrawProvCharts); }, /* * When the data package collection saves successfully, tell the user */ saveSuccess: function(savedObject){ //We only want to perform these actions after the package saves if(savedObject.type != "DataPackage") return; //Change the URL to the new id MetacatUI.uiRouter.navigate("view/" + this.dataPackage.packageModel.get("id"), { trigger: false, replace: true }); var message = $(document.createElement("div")).append($(document.createElement("span")).text("Your changes have been saved. ")); MetacatUI.appView.showAlert(message, "alert-success", "body", 4000, {remove: false}); // Reset the state to clean this.dataPackage.packageModel.set("changed", false); // If provenance relationships were updated, then reset the edit list now. if(this.dataPackage.provEdits.length) this.dataPackage.provEdits = []; this.saveProvPending = false; this.hideSaving(); this.stopListening(this.dataPackage, "errorSaving", this.saveError); // Turn off "save" footer this.hideEditorControls(); // Update the metadata table header with the new resource map id. // First find the PackageTableView for the top level package, and // then re-render it with the update resmap id. var view = this; var metadataId = this.packageModels[0].getMetadata().get("id") _.each(this.subviews, function(thisView, i) { // Check if this is a ProvChartView if(thisView.type.indexOf("PackageTable") !== -1) { if(thisView.currentlyViewing == metadataId) { var packageId = view.dataPackage.packageModel.get("id"); var title = packageId ? 'Package: ' + packageId + '' : ""; thisView.title = "Files in this dataset " + title; thisView.render(); } } }); }, /* * When the data package collection fails to save, tell the user */ saveError: function(errorMsg){ console.log("saveError called"); var errorId = "error" + Math.round(Math.random()*100), message = $(document.createElement("div")).append("

    Your changes could not be saved.

    "); message.append($(document.createElement("a")) .text("See details") .attr("data-toggle", "collapse") .attr("data-target", "#" + errorId) .addClass("pointer"), $(document.createElement("div")) .addClass("collapse") .attr("id", errorId) .append($(document.createElement("pre")).text(errorMsg))); MetacatUI.appView.showAlert(message, "alert-error", "body", null, { emailBody: "Error message: Data Package save error: " + errorMsg, remove: true }); this.saveProvPending = false; this.hideSaving(); this.stopListening(this.dataPackage, "successSaving", this.saveSuccess); // Turn off "save" footer this.hideEditorControls(); }, /* If provenance relationships have been modified by the provenance editor (in ProvChartView), then update the ORE Resource Map and save it to the server. */ saveProv: function() { // Only call this function once per save operation. if(this.saveProvPending) return; var view = this; if(this.dataPackage.provEditsPending()) { this.saveProvPending = true; // If the Data Package failed saving, display an error message this.listenToOnce(this.dataPackage, "errorSaving", this.saveError); // Listen for when the package has been successfully saved this.listenToOnce(this.dataPackage, "successSaving", this.saveSuccess); this.showSaving(); this.dataPackage.saveProv(); } else { //TODO: should a dialog be displayed saying that no prov edits were made? } }, showSaving: function(){ //Change the style of the save button this.$("#save-metadata-prov") .html(' Saving...') .addClass("btn-disabled"); this.$("input, textarea, select, button").prop("disabled", true); }, hideSaving: function(){ this.$("input, textarea, select, button").prop("disabled", false); //When prov is saved, revert the Save button back to normal this.$("#save-metadata-prov").html("Save").removeClass("btn-disabled"); }, showEditorControls: function(){ this.$("#editor-footer").slideDown(); }, hideEditorControls: function(){ this.$("#editor-footer").slideUp(); }, getEntityNames: function(packageModels){ var viewRef = this; _.each(packageModels, function(packageModel){ //Don't get entity names for larger packages - users must put the names in the system metadata if(packageModel.get("members").length > 100) return; //If this package has a different metadata doc than the one we are currently viewing var metadataModel = packageModel.getMetadata(); if(!metadataModel) return; if(metadataModel.get("id") != viewRef.pid){ var requestSettings = { url: MetacatUI.appModel.get("viewServiceUrl") + encodeURIComponent(metadataModel.get("id")), success: function(parsedMetadata, response, xhr){ _.each(packageModel.get("members"), function(solrResult, i){ var entityName = ""; if(solrResult.get("formatType") == "METADATA") entityName = solrResult.get("title"); var container = viewRef.findEntityDetailsContainer(solrResult, parsedMetadata); if(container) entityName = viewRef.getEntityName(container); //Set the entity name if(entityName){ solrResult.set("fileName", entityName); //Update the UI with the new name viewRef.$(".entity-name-placeholder[data-id='" + solrResult.get("id") + "']").text(entityName); } }); } } $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); return; } _.each(packageModel.get("members"), function(solrResult, i){ var entityName = ""; if(solrResult.get("fileName")) entityName = solrResult.get("fileName"); else if(solrResult.get("formatType") == "METADATA") entityName = solrResult.get("title"); else if(solrResult.get("formatType") == "RESOURCE") return; else{ var container = viewRef.findEntityDetailsContainer(solrResult); if(container && container.length > 0) entityName = viewRef.getEntityName(container); else entityName = null; } //Set the entityName, even if it's null solrResult.set("fileName", entityName); }); }); }, getEntityName: function(containerEl){ if(!containerEl) return false; var entityName = $(containerEl).find(".entityName").attr("data-entity-name"); if((typeof entityName === "undefined") || (!entityName)){ entityName = $(containerEl).find(".control-label:contains('Entity Name') + .controls-well").text(); if((typeof entityName === "undefined") || (!entityName)) entityName = null; } return entityName; }, //Checks if the metadata has entity details sections hasEntityDetails: function(){ return (this.$(".entitydetails").length > 0); }, /** * Finds the element in the rendered metadata that describes the given data entity. * * @param {(DataONEObject|SolrResult|string)} model - Either a model that represents the data object or the identifier of the data object * @param {Element} [el] - The DOM element to exclusivly search inside. * @return {Element} - The DOM element that describbbes the given data entity. */ findEntityDetailsContainer: function(model, el){ if(!el) var el = this.el; //Get the id and file name for this data object var id = "", fileName = ""; //If a model is given, get the id and file name from the object if( model && (DataONEObject.prototype.isPrototypeOf(model) || SolrResult.prototype.isPrototypeOf(model)) ) { id = model.get("id"); fileName = model.get("fileName"); } //If a string is given instead, it must be the id of the data object else if( typeof model == "string" ){ id = model; } //Otherwise, there isn't enough info to find the element, so exit else{ return; } //If we already found it earlier, return it now var container = this.$(".entitydetails[data-id='" + id + "'], " + ".entitydetails[data-id='" + DataONEObject.prototype.getXMLSafeID(id) + "']"); if(container.length) return container; //Are we looking for the main object that this MetadataView is displaying? if(id == this.pid){ if(this.$("#Metadata").length > 0) return this.$("#Metadata"); else return this.el; } //Metacat 2.4.2 and up will have the Online Distribution Link marked var link = this.$(".entitydetails a[data-pid='" + id + "']"); //Otherwise, try looking for an anchor with the id matching this object's id if(!link.length) link = $(el).find("a#" + id.replace(/[^A-Za-z0-9]/g, "\\$&")); //Get metadata index view var metadataFromIndex = _.findWhere(this.subviews, {type: "MetadataIndex"}); if(typeof metadataFromIndex === "undefined") metadataFromIndex = null; //Otherwise, find the Online Distribution Link the hard way if((link.length < 1) && (!metadataFromIndex)) link = $(el).find(".control-label:contains('Online Distribution Info') + .controls-well > a[href*='" + id.replace(/[^A-Za-z0-9]/g, "\\$&") + "']"); if(link.length > 0){ //Get the container element container = $(link).parents(".entitydetails"); if(container.length < 1){ //backup - find the parent of this link that is a direct child of the form element var firstLevelContainer = _.intersection($(link).parents("form").children(), $(link).parents()); //Find the controls-well inside of that first level container, which is the well that contains info about this data object if(firstLevelContainer.length > 0) container = $(firstLevelContainer).children(".controls-well"); if((container.length < 1) && (firstLevelContainer.length > 0)) container = firstLevelContainer; $(container).addClass("entitydetails"); } //Add the id so we can easily find it later container.attr("data-id", id); return container; } //----Find by file name rather than id----- if( !fileName ){ //Get the name of the object first for(var i=0; i 150) return; //==== Loop over each visual object and create a dataDisplay template for it to attach to the DOM ==== _.each(packageMembers, function(solrResult, i){ //Don't display any info about nested packages if(solrResult.type == "Package") return; var objID = solrResult.get("id"); if(objID == viewRef.pid) return; //Is this a visual object (image)? var type = solrResult.type == "SolrResult" ? solrResult.getType() : "Data set"; if(type == "image") images.push(solrResult); //Find the part of the HTML Metadata view that describes this data object var anchor = $(document.createElement("a")).attr("id", objID.replace(/[^A-Za-z0-9]/g, "-")), container = viewRef.findEntityDetailsContainer(objID); var downloadButton = new DownloadButtonView({ model: solrResult }); downloadButton.render(); //Insert the data display HTML and the anchor tag to mark this spot on the page if(container){ //Only show data displays for images hosted on the same origin if(type == "image" && solrResult.get("url").indexOf(window.location.host) > -1){ //Create the data display HTML var dataDisplay = $.parseHTML(viewRef.dataDisplayTemplate({ type : type, src : solrResult.get("url"), objID : objID }).trim()); //Insert into the page if($(container).children("label").length > 0) $(container).children("label").first().after(dataDisplay); else $(container).prepend(dataDisplay); //If this image is private, we need to load it via an XHR request if( !solrResult.get("isPublic") ){ //Create an XHR var xhr = new XMLHttpRequest(); xhr.withCredentials = true; xhr.onload = function(){ if(xhr.response) $(dataDisplay).find("img").attr("src", window.URL.createObjectURL(xhr.response)); } //Open and send the request with the user's auth token xhr.open('GET', solrResult.get("url")); xhr.responseType = "blob"; xhr.setRequestHeader("Authorization", "Bearer " + MetacatUI.appUserModel.get("token")); xhr.send(); } } $(container).prepend(anchor); var nameLabel = $(container).find("label:contains('Entity Name')"); if(nameLabel.length){ $(nameLabel).parent().after(downloadButton.el); } } }); //==== Initialize the fancybox images ===== // We will be checking every half-second if all the HTML has been loaded into the DOM - once they are all loaded, we can initialize the lightbox functionality. var numImages = images.length, //The shared lightbox options for both images lightboxOptions = { prevEffect : 'elastic', nextEffect : 'elastic', closeEffect : 'elastic', openEffect : 'elastic', aspectRatio : true, closeClick : true, afterLoad : function(){ //Create a custom HTML caption based on data stored in the DOM element viewRef.title = viewRef.title + "
    Download "; }, helpers : { title : { type : 'outside' } } }; if(numImages > 0){ var numImgChecks = 0, //Keep track of how many interval checks we have so we don't wait forever for images to load lightboxImgSelector = "a[class^='fancybox'][data-fancybox-type='image']"; //Add additional options for images var imgLightboxOptions = lightboxOptions; imgLightboxOptions.type = "image"; imgLightboxOptions.perload = 1; var initializeImgLightboxes = function(){ numImgChecks++; //Initialize what images have loaded so far after 5 seconds if(numImgChecks == 10){ $(lightboxImgSelector).fancybox(imgLightboxOptions); } //When 15 seconds have passed, stop checking so we don't blow up the browser else if(numImgChecks > 30){ $(lightboxImgSelector).fancybox(imgLightboxOptions); window.clearInterval(imgIntervalID); return; } //Are all of our images loaded yet? if(viewRef.$(lightboxImgSelector).length < numImages) return; else{ //Initialize our lightboxes $(lightboxImgSelector).fancybox(imgLightboxOptions); //We're done - clear the interval window.clearInterval(imgIntervalID); } } var imgIntervalID = window.setInterval(initializeImgLightboxes, 500); } }); }, replaceEcoGridLinks: function(){ var viewRef = this; //Find the element in the DOM housing the ecogrid link $("a:contains('ecogrid://')").each(function(i, thisLink){ //Get the link text var linkText = $(thisLink).text(); //Clean up the link text var withoutPrefix = linkText.substring(linkText.indexOf("ecogrid://") + 10), pid = withoutPrefix.substring(withoutPrefix.indexOf("/")+1), baseUrl = MetacatUI.appModel.get('resolveServiceUrl') || MetacatUI.appModel.get('objectServiceUrl'); $(thisLink).attr('href', baseUrl + encodeURIComponent(pid)).text(pid); }); }, publish: function(event) { // target may not actually prevent click events, so double check var disabled = $(event.target).closest("a").attr("disabled"); if (disabled) { return false; } var publishServiceUrl = MetacatUI.appModel.get('publishServiceUrl'); var pid = $(event.target).closest("a").attr("pid"); var ret = confirm("Are you sure you want to publish " + pid + " with a DOI?"); if (ret) { // show the loading icon var message = "Publishing package...this may take a few moments"; this.showLoading(message); var identifier = null; var viewRef = this; var requestSettings = { url: publishServiceUrl + pid, type: "PUT", xhrFields: { withCredentials: true }, success: function(data, textStatus, xhr) { // the response should have new identifier in it identifier = $(data).find("d1\\:identifier, identifier").text(); if (identifier) { viewRef.hideLoading(); var msg = "Published data package '" + identifier + "'. If you are not redirected soon, you can view your published data package here"; viewRef.$el.find('.container').prepend( viewRef.alertTemplate({ msg: msg, classes: 'alert-success' }) ); // navigate to the new view after a few seconds setTimeout( function() { // avoid a double fade out/in viewRef.$el.html(''); viewRef.showLoading(); MetacatUI.uiRouter.navigate("view/" + identifier, {trigger: true}) }, 3000); } }, error: function(xhr, textStatus, errorThrown) { // show the error message, but stay on the same page var msg = "Publish failed: " + $(xhr.responseText).find("description").text(); viewRef.hideLoading(); viewRef.showError(msg); } } $.ajax(_.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings())); } }, //When the given ID from the URL is a resource map that has no metadata, do the following... noMetadata: function(solrResultModel){ this.hideLoading(); this.$el.html(this.template()); this.pid = solrResultModel.get("resourceMap") || solrResultModel.get("id"); //Insert breadcrumbs this.insertBreadcrumbs(); this.insertDataSource(); //Insert a table of contents this.insertPackageTable(solrResultModel); this.renderMetadataFromIndex(); //Insert a message that this data is not described by metadata MetacatUI.appView.showAlert("Additional information about this data is limited since metadata was not provided by the creator.", "alert-warning", this.$(this.metadataContainer)); }, // this will lookup the latest version of the PID showLatestVersion: function() { //If this metadata doc is not obsoleted by a new version, then exit the function if( !this.model.get("obsoletedBy") ){ return; } var view = this; //When the latest version is found, this.listenTo(this.model, "change:newestVersion", function(){ //Make sure it has a newer version, and if so, if(view.model.get("newestVersion") != view.model.get("id")){ //Put a link to the newest version in the content view.$(".newer-version").replaceWith(view.versionTemplate({ pid: view.model.get("newestVersion") })); } else{ view.$(".newer-version").remove(); } }); //Insert the newest version template with a loading message this.$el.prepend( this.versionTemplate({ loading: true }) ); //Find the latest version of this metadata object this.model.findLatestVersion(); }, showLoading: function(message) { this.hideLoading(); MetacatUI.appView.scrollToTop(); var loading = this.loadingTemplate({ msg: message }); if(!loading) return; this.$loading = $($.parseHTML(loading)); this.$detached = this.$el.children().detach(); this.$el.html(loading); }, hideLoading: function() { if(this.$loading) this.$loading.remove(); if(this.$detached) this.$el.html(this.$detached); }, showError: function(msg){ //Remove any existing error messages this.$el.children(".alert-container").remove(); this.$el.prepend( this.alertTemplate({ msg: msg, classes: 'alert-error', containerClasses: "page", includeEmail: true })); }, /** * If the annotator feature is enabled, this function renders an AnnotatorView. * The Annotator feature is experiemental and unsupported, so this should only be enabled by advanced users * @deprecated */ setUpAnnotator: function() { if(!MetacatUI.appModel.get("annotatorUrl")) return; var thisView = this; require(["views/AnnotatorView"], function(AnnotatorView){ var annotator = new AnnotatorView({ parentView: thisView }); thisView.subviews.push(annotator); annotator.render(); }); }, /** * When the "Metadata" button in the table is clicked while we are on the Metadata view, * we want to scroll to the anchor tag of this data object within the page instead of navigating * to the metadata page again, which refreshes the page and re-renders (more loading time) **/ previewData: function(e){ //Don't go anywhere yet... e.preventDefault(); //Get the target and id of the click var link = $(e.target); if(!$(link).hasClass("preview")) link = $(link).parents("a.preview"); if(link){ var id = $(link).attr("data-id"); if((typeof id === "undefined") || !id) return false; //This will make the app defualt to the child view previewData function } else return false; // If we are on the Metadata view, update the URL and scroll to the // anchor window.location.hash = encodeURIComponent(id); MetacatUI.appView.scrollTo( this.findEntityDetailsContainer(id) ); return true; }, /** * Try to scroll to the section on a page describing the identifier in the * fragment/hash portion of the current page. * * This function depends on there being an `id` dataset attribute on an * element on the page set to an XML-safe version of the value in the * fragment/hash. Used to provide direct links to sub-resources on a page. */ scrollToFragment: function() { var hash = window.location.hash; if (!hash || hash.length <= 1) { return; } //Get the id from the URL hash and decode it var idFragment = decodeURIComponent(hash.substring(1)); //Find the corresponding entity details section for this id var entityDetailsEl = this.findEntityDetailsContainer(idFragment); if( entityDetailsEl || entityDetailsEl.length ){ MetacatUI.appView.scrollTo(entityDetailsEl); } }, closePopovers: function(e){ //If this is a popover element or an element that has a popover, don't close anything. //Check with the .classList attribute to account for SVG elements var svg = $(e.target).parents("svg"); if(_.contains(e.target.classList, "popover-this") || ($(e.target).parents(".popover-this").length > 0) || ($(e.target).parents(".popover").length > 0) || _.contains(e.target.classList, "popover") || (svg.length && _.contains(svg[0].classList, "popover-this"))) return; //Close all active popovers this.$(".popover-this.active").popover("hide"); }, highlightNode: function(e){ //Find the id var id = $(e.target).attr("data-id"); if((typeof id === "undefined") || (!id)) id = $(e.target).parents("[data-id]").attr("data-id"); //If there is no id, return if(typeof id === "undefined") return false; //Highlight its node $(".prov-chart .node[data-id='" + id + "']").toggleClass("active"); //Highlight its metadata section if(MetacatUI.appModel.get("pid") == id) this.$("#Metadata").toggleClass("active"); else{ var entityDetails = this.findEntityDetailsContainer(id); if(entityDetails) entityDetails.toggleClass("active"); } }, onClose: function () { var viewRef = this; this.stopListening(); _.each(this.subviews, function(subview) { if(subview.onClose) subview.onClose(); }); this.packageModels = new Array(); this.model.set(this.model.defaults); this.pid = null; this.seriesId = null; this.$detached = null; this.$loading = null; //Put the document title back to the default MetacatUI.appModel.set("title", MetacatUI.appModel.defaults.title); //Remove view-specific classes this.$el.removeClass("container no-stylesheet"); this.$el.empty(); }, /** * Generate a string appropriate to go into the author/creator portion of * a dataset citation from the value stored in the underlying model's * origin field. */ getAuthorText: function() { var authors = this.model.get("origin"), count = 0, authorText = ""; _.each(authors, function (author) { count++; if (count == 6) { authorText += ", et al. "; return; } else if (count > 6) { return; } if (count > 1) { if (authors.length > 2) { authorText += ","; } if (count == authors.length) { authorText += " and"; } if (authors.length > 1) { authorText += " "; } } authorText += author; }); return authorText; }, /** * Generate a string appropriate to be used in the publisher portion of a * dataset citation. This method falls back to the node ID when the proper * node name cannot be fetched from the app's NodeModel instance. */ getPublisherText: function() { var datasource = this.model.get("datasource"), memberNode = MetacatUI.nodeModel.getMember(datasource); if (memberNode) { return memberNode.name; } else { return datasource; } }, /** * Generate a string appropriate to be used as the publication date in a * dataset citation. */ getDatePublishedText: function() { // Dataset/datePublished // Prefer pubDate, fall back to dateUploaded so we have something to show if (this.model.get("pubDate") !== "") { return this.model.get("pubDate") } else { return this.model.get("dateUploaded") } }, /** * Generate Schema.org-compliant JSONLD for the model bound to the view into * the head tag of the page by `insertJSONLD`. * * Note: `insertJSONLD` should be called to do the actual inserting into the * DOM. */ generateJSONLD: function () { var model = this.model; // Determine the path (either #view or view, depending on router // configuration) for use in the 'url' property var href = document.location.href, route = href.replace(document.location.origin + "/", "") .split("/")[0]; // First: Create a minimal Schema.org Dataset with just the fields we // know will come back from Solr (System Metadata fields). // Add the rest in conditional on whether they are present. var elJSON = { "@context": { "@vocab": "https://schema.org/", }, "@type": "Dataset", "@id": "https://dataone.org/datasets/" + encodeURIComponent(model.get("id")), "datePublished" : this.getDatePublishedText(), "publisher": { "@type": "Organization", "name": this.getPublisherText() }, "identifier": model.get("id"), "version": model.get("version"), "url": "https://dataone.org/datasets/" + encodeURIComponent(model.get("id")), "schemaVersion": model.get("formatId"), "isAccessibleForFree": true }; // Attempt to add in a sameAs property of we have high confidence the // identifier is a DOI if (this.model.isDOI(model.get("id"))) { var doi = this.getCanonicalDOIIRI(model.get("id")); if (doi) { elJSON["sameAs"] = doi; } } // Second: Add in optional fields // Name if (model.get("title")) { elJSON["name"] = model.get("title") } // Creator if (model.get("origin")) { elJSON["creator"] = model.get("origin").map(function (creator) { return { "@type": "Person", "name": creator }; }); } // Dataset/spatialCoverage if (model.get("northBoundCoord") && model.get("eastBoundCoord") && model.get("southBoundCoord") && model.get("westBoundCoord")) { var spatialCoverage = { "@type": "Place", "additionalProperty": [ { "@type": "PropertyValue", "additionalType": "http://dbpedia.org/resource/Coordinate_reference_system", "name": "Coordinate Reference System", "value": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" } ], "geo": this.generateSchemaOrgGeo(model.get("northBoundCoord"), model.get("eastBoundCoord"), model.get("southBoundCoord"), model.get("westBoundCoord")), "subjectOf": { "@type": "CreativeWork", "fileFormat": "application/vnd.geo+json", "text": this.generateGeoJSONString(model.get("northBoundCoord"), model.get("eastBoundCoord"), model.get("southBoundCoord"), model.get("westBoundCoord")) } }; elJSON.spatialCoverage = spatialCoverage; } // Dataset/temporalCoverage if (model.get("beginDate") && !model.get("endDate")) { elJSON.temporalCoverage = model.get("beginDate"); } else if (model.get("beginDate") && model.get("endDate")) { elJSON.temporalCoverage = model.get("beginDate") + "/" + model.get("endDate"); } // Dataset/variableMeasured if (model.get("attributeName")) { elJSON.variableMeasured = model.get("attributeName"); } // Dataset/description if (model.get("abstract")) { elJSON.description = model.get("abstract"); } // Dataset/keywords if (model.get("keywords")) { elJSON.keywords = model.get("keywords").join(", "); } return elJSON; }, /** * Insert Schema.org-compliant JSONLD for the model bound to the view into * the head tag of the page (at the end). * * @param {object} json - JSON-LD to insert into the page * * Some notes: * * - Checks if the JSONLD already exists from the previous data view * - If not create a new script tag and append otherwise replace the text * for the script */ insertJSONLD: function(json) { if (!document.getElementById('jsonld')) { var el = document.createElement('script'); el.type = 'application/ld+json'; el.id = 'jsonld'; el.text = JSON.stringify(json); document.querySelector('head').appendChild(el); } else { var script = document.getElementById('jsonld'); script.text = JSON.stringify(json); } }, /** * Generate a Schema.org/Place/geo from bounding coordinates * * Either generates a GeoCoordinates (when the north and east coords are * the same) or a GeoShape otherwise. */ generateSchemaOrgGeo: function(north, east, south, west) { if (north === south) { return { "@type": "GeoCoordinates", "latitude" : north, "longitude" : west } } else { return { "@type": "GeoShape", "box": west + ", " + south + " " + east + ", " + north } } }, /** * Creates a (hopefully) valid geoJSON string from the a set of bounding * coordinates from the Solr index (north, east, south, west). * * This function produces either a GeoJSON Point or Polygon depending on * whether the north and south bounding coordinates are the same. * * Part of the reason for factoring this out, in addition to code * organization issues, is that the GeoJSON spec requires us to modify * the raw result from Solr when the coverage crosses -180W which is common * for datasets that cross the Pacific Ocean. In this case, We need to * convert the east bounding coordinate from degrees west to degrees east. * * e.g., if the east bounding coordinate is 120 W and west bounding * coordinate is 140 E, geoJSON requires we specify 140 E as 220 * * @param {number} north - North bounding coordinate * @param {number} east - East bounding coordinate * @param {number} south - South bounding coordinate * @param {number} west - West bounding coordinate */ generateGeoJSONString: function(north, east, south, west) { if (north === south) { return this.generateGeoJSONPoint(north, east); } else { return this.generateGeoJSONPolygon(north, east, south, west); } }, /** * Generate a GeoJSON Point object * * @param {number} north - North bounding coordinate * @param {number} east - East bounding coordinate * * Example: * { * "type": "Point", * "coordinates": [ * -105.01621, * 39.57422 * ]} */ generateGeoJSONPoint: function(north, east) { var preamble = "{\"type\":\"Point\",\"coordinates\":", inner = "[" + east + "," + north + "]", postamble = "}"; return preamble + inner + postamble; }, /** * Generate a GeoJSON Polygon object from * * @param {number} north - North bounding coordinate * @param {number} east - East bounding coordinate * @param {number} south - South bounding coordinate * @param {number} west - West bounding coordinate * * * Example: * * { * "type": "Polygon", * "coordinates": [[ * [ 100, 0 ], * [ 101, 0 ], * [ 101, 1 ], * [ 100, 1 ], * [ 100, 0 ] * ]} * */ generateGeoJSONPolygon: function(north, east, south, west) { var preamble = "{\"type\":\"Feature\",\"properties\":{},\"geometry\":{\"type\"\:\"Polygon\",\"coordinates\":[["; // Handle the case when the polygon wraps across the 180W/180E boundary if (east < west) { east = 360 - east } var inner = "[" + west + "," + south + "]," + "[" + east + "," + south + "]," + "[" + east + "," + north + "]," + "[" + west + "," + north + "]," + "[" + west + "," + south + "]"; var postamble = "]]}}"; return preamble + inner + postamble; }, /** * Create a canonical IRI for a DOI given a random DataONE identifier. * * @param {string} identifier: The identifier to (possibly) create the IRI * for. * @return {string|null} Returns null when matching the identifier to a DOI * regex fails or a string when the match is successful * * Useful for describing resources identified by DOIs in linked open data * contexts or possibly also useful for comparing two DOIs for equality. * * Note: Really could be generalized to more identifier schemes. */ getCanonicalDOIIRI: function(identifier) { var pattern = /(10\.\d{4,9}\/[-\._;()\/:A-Z0-9]+)$/, match = identifier.match(pattern); if (match === null || match.length !== 2 || match[1].length <= 0) { return null; } return "https://doi.org/" + match[1]; }, /** * Insert citation information as meta tags into the head of the page * * Currently supports Highwire Press style tags (citation_) which is * supposedly what Google (Scholar), Mendeley, and Zotero support. */ insertCitationMetaTags: function() { // Generate template data to use for all templates var title = this.model.get("title"), authors = this.getAuthorText(), publisher = this.getPublisherText(), date = new Date(this.getDatePublishedText()).getUTCFullYear().toString(); // Generate HTML strings from each template var hwpt = this.metaTagsHighwirePressTemplate({ title: title, authors: authors, publisher: publisher, date: date }); // Clear any that are already in the document. $("meta[name='citation_title']").remove(); $("meta[name='citation_authors']").remove(); $("meta[name='citation_publisher']").remove(); $("meta[name='citation_date']").remove(); // Insert document.head.insertAdjacentHTML("beforeend", hwpt); }, createAnnotationViews: function(){ try{ var viewRef = this; _.each($(".annotation"), function (annoEl) { var newView = new AnnotationView({ el: annoEl }); viewRef.subviews.push(newView); newView.render(); }); } catch(e){ console.error(e); } }, insertMarkdownViews: function() { var viewRef = this; _.each($(".markdown"), function (markdownEl) { var newView = new MarkdownView({ markdown: $(markdownEl).text().trim(), el: $(markdownEl).parent() }); viewRef.subviews.push(newView); // Clear out old content before rendering $(markdownEl).remove(); newView.render(); }); } }); return MetadataView; });