diff --git a/src/js/views/MetadataView.js b/src/js/views/MetadataView.js index 19d42fa7b..599abd610 100644 --- a/src/js/views/MetadataView.js +++ b/src/js/views/MetadataView.js @@ -17,8 +17,6 @@ define([ "views/DownloadButtonView", "views/ProvChartView", "views/MetadataIndexView", - "views/ExpandCollapseListView", - "views/ProvStatementView", "views/CitationHeaderView", "views/citations/CitationModalView", "views/AnnotationView", @@ -34,9 +32,7 @@ define([ "text!templates/editMetadata.html", "text!templates/dataDisplay.html", "text!templates/map.html", - "text!templates/annotation.html", "text!templates/metaTagsHighwirePress.html", - "uuid", "views/MetricView", ], ( $, @@ -44,7 +40,7 @@ define([ _, Backbone, gmaps, - fancybox, + _fancybox, Clipboard, DataPackage, DataONEObject, @@ -57,8 +53,6 @@ define([ DownloadButtonView, ProvChart, MetadataIndex, - ExpandCollapseList, - ProvStatement, CitationHeaderView, CitationModalView, AnnotationView, @@ -74,9 +68,7 @@ define([ EditMetadataTemplate, DataDisplayTemplate, MapTemplate, - AnnotationTemplate, metaTagsHighwirePressTemplate, - uuid, MetricView, ) => { "use strict"; @@ -98,8 +90,8 @@ define([ saveProvPending: false, model: new SolrResult(), - packageModels: new Array(), - entities: new Array(), + packageModels: [], + entities: [], dataPackage: null, dataPackageSynced: false, el: "#Content", @@ -133,8 +125,8 @@ define([ objectIds: [], /** - * Text to display in the help tooltip for the alternative identifier field, - * if the field is present. + * Text to display in the help tooltip for the alternative identifier + * field, if the field is present. * @type {string} * @since 2.26.0 */ @@ -147,7 +139,7 @@ define([ history and evolution of the dataset. `, - // Delegated events for creating new items, and clearing completed ones. + /** @inheritdoc */ events: { "click #publish": "publish", "mouseover .highlight-node": "highlightNode", @@ -156,9 +148,15 @@ define([ "click #save-metadata-prov": "saveProv", }, - initialize(options) { - if (options === undefined || !options) var options = {}; - + /** + * Initialize the MetadataView + * @param {object} options Object containing the view's options + * @param {string} [options.pid] The identifier of the metadata object to + * render + * @param {string} [options.el] The jQuery selector for the element in + * which to render the view + */ + initialize(options = {}) { this.pid = options.pid || options.id || MetacatUI.appModel.get("pid") || null; @@ -167,7 +165,7 @@ define([ if (typeof options.el !== "undefined") this.setElement(options.el); }, - // Render the main metadata view + /** @inheritdoc */ render() { this.stopListening(); @@ -175,10 +173,10 @@ define([ // this.showLoading("Loading..."); // Reset various properties of this view first - this.classMap = new Array(); - this.subviews = new Array(); + this.classMap = []; + this.subviews = []; this.model.set(this.model.defaults); - this.packageModels = new Array(); + this.packageModels = []; // get the pid to render if (!this.pid) this.pid = MetacatUI.appModel.get("pid"); @@ -186,17 +184,18 @@ define([ this.listenTo(MetacatUI.appUserModel, "change:loggedIn", this.render); // Listen to when the metadata has been rendered - this.once("metadataLoaded", function () { + this.once("metadataLoaded", () => { this.createAnnotationViews(); this.insertMarkdownViews(); }); // Listen to when the package table has been rendered - this.once("dataPackageRendered", function () { + this.once("dataPackageRendered", () => { const packageTableContainer = this.$("#data-package-container"); $(packageTableContainer).children(".loading").remove(); - // Scroll to the element on the page that is in the hash fragment (if there is one) + // Scroll to the element on the page that is in the hash fragment (if + // there is one) this.scrollToFragment(); }); @@ -206,8 +205,8 @@ define([ }, /** - * Retrieve the resource map given its PID, and when it's fetched, - * check for write permissions, then check for private members in the package + * Retrieve the resource map given its PID, and when it's fetched, check + * for write permissions, then check for private members in the package * table view, if there is one. * @param {string} pid - The PID of the resource map */ @@ -224,15 +223,15 @@ define([ // If there is no resource map if (!pid) { - // mark the data package as synced, - // since there are no other models to fetch + // mark the data package as synced, since there are no other models to + // fetch this.dataPackageSynced = true; this.trigger("changed:dataPackageSynced"); this.checkWritePermissions(); return; } - this.listenToOnce(this.dataPackage, "complete", function () { + this.listenToOnce(this.dataPackage, "complete", () => { this.dataPackageSynced = true; this.trigger("changed:dataPackageSynced"); const dataPackageView = _.findWhere(this.subviews, { @@ -267,7 +266,7 @@ define([ ) { this.checkWritePermissions(); } else { - this.listenToOnce(this.dataPackage.packageModel, "sync", function () { + this.listenToOnce(this.dataPackage.packageModel, "sync", () => { this.checkWritePermissions(); }); } @@ -278,47 +277,49 @@ define([ }, /* - * 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 + * 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(pid) { - // Get the pid and sid - if (typeof pid === "undefined" || !pid) var { pid } = this; - if (typeof this.seriesId !== "undefined" && this.seriesId) - var sid = this.seriesId; + getModel(id) { + const pid = id || this.pid; + const sid = this.seriesId || null; // Get the package ID this.model.set({ id: pid, seriesId: sid }); const { model } = this; - this.listenToOnce(model, "sync", function () { + this.listenToOnce(model, "sync", () => { if ( - this.model.get("formatType") == "METADATA" || + this.model.get("formatType") === "METADATA" || !this.model.get("formatType") ) { this.model = model; this.renderMetadata(); - } else if (this.model.get("formatType") == "DATA") { + } else if (this.model.get("formatType") === "DATA") { // Get the metadata pids that document this data object const 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) { + // 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.navigateWithFragment(_.first(isDocBy), 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. + // 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. if (isDocBy && isDocBy.length > 1) { const view = this; + // eslint-disable-next-line import/no-dynamic-require require(["collections/Filters", "collections/SolrResults"], ( Filters, SolrResults, ) => { - // Create a search for the metadata docs that document this data object + // Create a search for the metadata docs that document this data + // object const searchFilters = new Filters([ { values: isDocBy, @@ -336,17 +337,21 @@ define([ }); // When the search results are returned, process those results - view.listenToOnce(searchResults, "sync", (searchResults) => { + view.listenToOnce(searchResults, "sync", () => { // Keep track of the latest version of the metadata doc(s) const latestVersions = []; - // Iterate over each search result and find the latest version of each metadata version chain + // Iterate over each search result and find the latest version + // of each metadata version chain searchResults.each((searchResult) => { - // If this metadata isn't obsoleted by another object, it is the latest version + // 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 + // 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")) ) { @@ -354,16 +359,21 @@ define([ } }, view); - // If at least one latest version was found (should always be the case), + // 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. + // 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.navigateWithFragment(latestVersions[0], view.pid); } - // 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). + // 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 { view.navigateWithFragment(_.last(isDocBy), view.pid); } @@ -376,11 +386,11 @@ define([ return; } this.noMetadata(this.model); - } else if (this.model.get("formatType") == "RESOURCE") { + } else if (this.model.get("formatType") === "RESOURCE") { const packageModel = new Package({ id: this.model.get("id") }); packageModel.on( "complete", - function () { + () => { const metadata = packageModel.getMetadata(); if (!metadata) { @@ -411,6 +421,12 @@ define([ model.getInfo(); }, + /** + * Render the main components of the metadata view. Insert HTML from the + * view service or fallback to rendering from the index. Insert + * breadcrumbs, citation, data source logo, metadata controls, and + * metadata metrics. + */ renderMetadata() { const pid = this.model.get("id"); @@ -435,8 +451,8 @@ define([ // 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 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(); @@ -448,31 +464,29 @@ define([ ); // 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") { + const viewServiceUrl = MetacatUI.appModel.get("viewServiceUrl"); + if (viewServiceUrl) { + const endpoint = viewServiceUrl + encodeURIComponent(pid); const viewRef = this; const loadSettings = { url: endpoint, - success(response, status, xhr) { + success(response, status, _xhr) { try { - // If the user has navigated away from the MetadataView, then don't render anything further - if (MetacatUI.appView.currentView != viewRef) return; + // 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 + // Our fallback is to show the metadata details from the Solr + // index if ( - status == "error" || + status === "error" || !response || typeof response !== "string" ) viewRef.renderMetadataFromIndex(); else { - // Check for a response that is a 200 OK status, but is an error msg + // 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 && @@ -481,8 +495,9 @@ define([ viewRef.renderMetadataFromIndex(); return; } - // Mark this as a metadata doc with no stylesheet, or one that is at least different than usual EML and FGDC - if (response.indexOf('id="Metadata"') == -1) { + // Mark this as a metadata doc with no stylesheet, or one that + // is at least different than usual EML and FGDC + if (response.indexOf('id="Metadata"') === -1) { viewRef.$el.addClass("container no-stylesheet"); if (viewRef.model.get("indexed")) { @@ -499,7 +514,8 @@ define([ viewRef.storeEntityPIDs(entityEl, entityId); }); - // If there is no info from the index and there is no metadata doc rendered either, then display a message + // 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") && @@ -518,19 +534,20 @@ define([ // Add a map of the spatial coverage if (gmaps) viewRef.insertSpatialCoverageMap(); - // Injects Clipboard objects into DOM elements returned from the View Service + // Injects Clipboard objects into DOM elements returned from + // the View Service viewRef.insertCopiables(); } } catch (e) { - console.log( - "Error rendering metadata from the view service", - e, + MetacatUI.analytics?.trackException( + `Error rendering metadata from the view service. Fellback to index, ${e}, Response: ${response}`, + pid, + false, ); - console.log("Response from the view service: ", response); viewRef.renderMetadataFromIndex(); } }, - error(xhr, textStatus, errorThrown) { + error(_xhr, _textStatus, _errorThrown) { viewRef.renderMetadataFromIndex(); }, }; @@ -549,7 +566,10 @@ define([ this.insertCitationMetaTags(); }, - /* If there is no view service available, then display the metadata fields from the index */ + /** + * If there is no view service available, then display the metadata fields + * from the index + */ renderMetadataFromIndex() { const metadataFromIndex = new MetadataIndex({ pid: this.pid, @@ -571,6 +591,7 @@ define([ }); }, + /** @deprecated */ removeCitation() { let citation = ""; let citationEl = null; @@ -583,13 +604,14 @@ define([ // 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 + // 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 const wells = this.$(".well"); - const viewRef = this; - // Find the div.well with the citation. If we never find it, we don't insert the list of contents + // Find the div.well with the citation. If we never find it, we don't + // insert the list of contents _.each(wells, (well) => { if ( (!citationEl && @@ -607,7 +629,8 @@ define([ } }); - // Remove the unnecessary classes that are used in older versions of Metacat (2.4.3 and older) + // Remove the unnecessary classes that are used in older versions of + // Metacat (2.4.3 and older) const citationText = $(citationEl).find(".span10"); $(citationText).removeClass("span10").addClass("span12"); } @@ -618,6 +641,9 @@ define([ citationEl.remove(); }, + /** + * Add breadcrumbs to the page to show the user where they are in the app + */ insertBreadcrumbs() { const breadcrumbs = $(document.createElement("ol")) .addClass("breadcrumb") @@ -641,7 +667,7 @@ define([ `${MetacatUI.root}/data${ MetacatUI.appModel.get("page") > 0 ? `/page/${ - parseInt(MetacatUI.appModel.get("page")) + 1 + parseInt(MetacatUI.appModel.get("page"), 10) + 1 }` : "" }`, @@ -662,14 +688,14 @@ define([ ), ); - if (MetacatUI.uiRouter.lastRoute() == "data") { + 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 + ? parseInt(MetacatUI.appModel.get("page"), 10) + 1 : "" }`, ) @@ -686,7 +712,7 @@ define([ this.$(this.breadcrumbContainer).html(breadcrumbs); }, - /* + /** * When the metadata object doesn't exist, display a message to the user */ showNotFound() { @@ -695,21 +721,17 @@ define([ return; } - try { - // Check if a query string was in the URL and if so, try removing it in the identifier - if (this.model.get("id").match(/\?\S+\=\S+/g) && !this.findTries) { - const newID = this.model.get("id").replace(/\?\S+\=\S+/g, ""); - this.onClose(); - this.model.set("id", newID); - this.pid = newID; - this.findTries = 1; - this.render(); - return; - } - } catch (e) { - console.warn("Caught error while determining query string", e); + // Check if a query string was in the URL and if so, try removing it in + // the identifier + if (this.model.get("id").match(/\?\S+=\S+/g) && !this.findTries) { + const newID = this.model.get("id").replace(/\?\S+=\S+/g, ""); + this.onClose(); + this.model.set("id", newID); + this.pid = newID; + this.findTries = 1; + this.render(); + return; } - // Construct a message that shows this object doesn't exist const msg = `

Nothing was found.

` + @@ -727,7 +749,8 @@ define([ // 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 + // 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( @@ -736,12 +759,11 @@ define([ ); }, - /* - * When the metadata object is private, display a message to the user - */ + /** When the metadata object is private, display a message to the user */ showIsPrivate() { - // 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 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, @@ -751,9 +773,12 @@ define([ return; } - // If the user is logged in, the message will display that this dataset is private. + let msg = ""; + + // If the user is logged in, the message will display that this dataset + // is private. if (MetacatUI.appUserModel.get("loggedIn")) { - var msg = + msg = '' + @@ -763,7 +788,7 @@ define([ } // If the user isn't logged in, display a log in link. else { - var msg = + msg = `` + @@ -780,7 +805,14 @@ define([ this.showError(msg); }, + /** + * Retrieves and processes the details of the specified data packages. + * @param {string[]} packageIDs - An array of package IDs to retrieve + * details for. If the array is empty or not provided, it processes the + * current metadata document as a standalone package. + */ getPackageDetails(packageIDs) { + const view = this; let completePackages = 0; // This isn't a package, but just a lonely metadata doc... @@ -794,7 +826,7 @@ define([ } else { _.each( packageIDs, - function (thisPackageID, i) { + (thisPackageID, _i) => { // Create a model representing the data package const thisPackage = new Package({ id: thisPackageID }); @@ -806,28 +838,24 @@ define([ ); // When the package info is fully retrieved - this.listenToOnce( - thisPackage, - "complete", - function (thisPackage) { - // When all packages are fully retrieved - completePackages++; - if (completePackages >= packageIDs.length) { - const latestPackages = _.filter( - this.packageModels, - (m) => !_.contains(packageIDs, m.get("obsoletedBy")), - ); + view.listenToOnce(thisPackage, "complete", () => { + // When all packages are fully retrieved + completePackages += 1; + if (completePackages >= packageIDs.length) { + const latestPackages = _.filter( + view.packageModels, + (m) => !_.contains(packageIDs, m.get("obsoletedBy")), + ); - // Set those packages as the most recent package - this.packageModels = latestPackages; + // Set those packages as the most recent package + view.packageModels = latestPackages; - this.insertPackageDetails(latestPackages); - } - }, - ); + view.insertPackageDetails(latestPackages); + } + }); // Save the package in the view - this.packageModels.push(thisPackage); + view.packageModels.push(thisPackage); // Make sure we get archived content, too thisPackage.set("getArchivedMembers", true); @@ -840,8 +868,10 @@ define([ } }, + /** Make minor modifications to the HTML returned from the view service */ alterMarkup() { - // Find the taxonomic range and give it a class for styling - for older versions of Metacat only (v2.4.3 and older) + // 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() @@ -860,7 +890,8 @@ define([ $(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 + // 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"); // Add explanation text to the alternate identifier @@ -871,73 +902,71 @@ define([ * Inserts an info icon next to the alternate identifier field, if it * exists. The icon will display a tooltip with the help text for the * field. - * @returns {jQuery} The jQuery object for the icon element. + * @returns {jQuery|null} The jQuery object for the icon element. * @since 2.26.0 */ renderAltIdentifierHelpText() { - try { - // Find the HTML element that contains the alternate identifier. - const altIdentifierLabel = this.$( - ".control-label:contains('Alternate Identifier')", - ); + // Find the HTML element that contains the alternate identifier. + const altIdentifierLabel = this.$( + ".control-label:contains('Alternate Identifier')", + ); - // It may not exist for all datasets. - if (!altIdentifierLabel.length) return; + // It may not exist for all datasets. + if (!altIdentifierLabel.length) return null; - const text = this.alternativeIdentifierHelpText; + const text = this.alternativeIdentifierHelpText; - if (!text) return; + if (!text) return null; - // Create the tooltip - const icon = $(document.createElement("i")) - .addClass("tooltip-this icon icon-info-sign") - .css("margin-left", "4px"); + // Create the tooltip + const icon = $(document.createElement("i")) + .addClass("tooltip-this icon icon-info-sign") + .css("margin-left", "4px"); - // Activate the jQuery tooltip plugin - icon.tooltip({ - title: text, - placement: "top", - container: "body", - }); + // Activate the jQuery tooltip plugin + icon.tooltip({ + title: text, + placement: "top", + container: "body", + }); - // Add the icon to the label. - altIdentifierLabel.append(icon); + // Add the icon to the label. + altIdentifierLabel.append(icon); - return icon; - } catch (e) { - console.log("Error adding help text to alternate identifier", e); - } + return icon; }, - /* - * Inserts a table with all the data package member information and sends the call to display annotations + /** + * Inserts a table with all the data package member information and sends + * the call to display annotations + * @param {Array} packageModels - An array of Package models + * @param {object} options - An object with options for rendering the + * package table + * @returns {MetadataView|null} Returns this view object */ - insertPackageDetails(packages, options) { - if (typeof options === "undefined") { - var options = {}; - } + insertPackageDetails(packageModels, options = {}) { // Don't insert the package details twice const view = this; const tableEls = this.$(view.tableContainer).children().not(".loading"); - if (tableEls.length > 0) return; + if (tableEls.length > 0) return view; // wait for the metadata to load const metadataEls = this.$(view.metadataContainer).children(); if (!metadataEls.length || metadataEls.first().is(".loading")) { - this.once("metadataLoaded", function () { + this.once("metadataLoaded", () => { view.insertPackageDetails(this.packageModels, options); }); - return; + return view; } - if (!packages) var packages = this.packageModels; + const packages = packageModels || this.packageModels; // Get the entity names from this page/metadata this.getEntityNames(packages); _.each( packages, - function (packageModel) { + function showDetails(packageModel) { // If the package model is not complete, don't do anything if (!packageModel.complete) return; @@ -959,51 +988,57 @@ define([ if ( !( !this.model.get("archived") && - packageModel.get("archived") == true - ) - ) { - var title = packageModel.get("id") - ? `Package: ${packageModel.get( - "id", - )}` - : ""; - options.title = `Files in this dataset ${title}`; - options.nested = true; - this.insertPackageTable(packageModel, options); - } - } else { - // If this metadata is not archived, then don't display archived packages - if ( - !( - !this.model.get("archived") && - packageModel.get("archived") == true + packageModel.get("archived") === true ) ) { - var title = packageModel.get("id") + const title = packageModel.get("id") ? `Package: ${packageModel.get( "id", )}` : ""; - options.title = `Files in this dataset ${title}`; - this.insertPackageTable(packageModel, options); + const tableOptions = { ...options, nested: true, title }; + this.insertPackageTable(packageModel, tableOptions); } + } else if ( + // If this metadata is not archived, then don't display archived + // packages + !( + !this.model.get("archived") && + packageModel.get("archived") === true + ) + ) { + const title = packageModel.get("id") + ? `Package: ${packageModel.get( + "id", + )}` + : ""; + const tableOptions = { + ...options, + title: `Files in this dataset ${title}`, + }; + this.insertPackageTable(packageModel, tableOptions); } - // Remove the extra download button returned from the XSLT since the package table will have all the download links + // Remove the extra download button returned from the XSLT since the + // package table will have all the download links $("#downloadPackage").remove(); }, this, ); - // If this metadata doc is not in a package, but is just a lonely metadata doc... + // If this metadata doc is not in a package, but is just a lonely + // metadata doc... if (!packages.length) { const packageModel = new Package({ members: [this.model], }); packageModel.complete = true; - options.title = "Files in this dataset"; - options.disablePackageDownloads = true; - this.insertPackageTable(packageModel, options); + const tableOptions = { + ...options, + title: "Files in this dataset", + disablePackageDownloads: true, + }; + this.insertPackageTable(packageModel, tableOptions); } // Insert the data details sections @@ -1026,21 +1061,25 @@ define([ (p) => !p.get("obsoletedBy"), ); - // If all of the packages are obsoleted, then use the last package in the array, - // which is most likely the most recent. - /** @todo Use the DataONE version API to find the most recent package in the version chain */ + // If all of the packages are obsoleted, then use the last package + // in the array, which is most likely the most recent. + + // @todo Use the DataONE version API to find the most recent + // package in the version chain if (!mostRecentPackage) { mostRecentPackage = packages[packages.length - 1]; } - // Get the data package only if it is not the same as the previously fetched package - if (mostRecentPackage.get("id") != packages[0].get("id")) + // Get the data package only if it is not the same as the previously + // fetched package + if (mostRecentPackage.get("id") !== packages[0].get("id")) this.getDataPackage(mostRecentPackage.get("id")); } } catch (e) { - console.error( - "Could not get the data package (prov will not be displayed, possibly other info as well).", - e, + MetacatUI.analytics?.trackException( + "Could not get the data package and could not display provenance graphs.", + view.pid, + false, ); } @@ -1050,9 +1089,18 @@ define([ return this; }, + /** + * Inserts a package table into the view. + * @param {object} packageModel - The package model. + * @param {object} options - The options for the package table. + * @param {string} options.title - The title of the package table. + * @param {boolean} options.disablePackageDownloads - Whether to disable + * package downloads. + * @param {boolean} options.nested - Whether the package table is nested. + */ insertPackageTable(packageModel, options) { const view = this; - if (this.dataPackage == null || !this.dataPackageSynced) { + if (!this.dataPackage || !this.dataPackageSynced) { this.listenToOnce(this, "changed:dataPackageSynced", () => { view.insertPackageTable(packageModel, options); }); @@ -1067,16 +1115,16 @@ define([ this.dataPackage.mergeModels(packageModel.get("members")); } + let title = ""; + let disablePackageDownloads = false; + let nested = false; + if (options) { - var title = options.title || ""; - var disablePackageDownloads = - options.disablePackageDownloads || false; - var nested = + title = options.title || ""; + disablePackageDownloads = options.disablePackageDownloads || false; + nested = typeof options.nested === "undefined" ? false : options.nested; - } else - var title = "", - nested = false, - disablePackageDownloads = false; + } //* * Draw the package table **// const tableView = new DataPackageView({ @@ -1099,23 +1147,25 @@ define([ const numTables = $(tablesContainer).find( "table.download-contents", ).length; - if (numTables == 1) { - var tableContainer = $(document.createElement("div")).attr( + + let tableContainer = tablesContainer; + if (numTables === 1) { + 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; + } else if (numTables > 1) { + tableContainer = this.$(`#additional-tables-for-${this.cid}`); + } // Insert the package table HTML $(tableContainer).empty(); $(tableContainer).append(tableView.render().el); - // Add Package Download - // create an instance of DownloadButtonView to handle package downloads + // Add Package Download create an instance of DownloadButtonView to + // handle package downloads this.downloadButtonView = new DownloadButtonView({ model: packageModel, view: "actionsView", @@ -1135,15 +1185,21 @@ define([ this.subviews.push(tableView); - // Trigger a custom event in this view that indicates the package table has been rendered + // Trigger a custom event in this view that indicates the package table + // has been rendered this.trigger("dataPackageRendered"); }, + /** + * Inserts parent package links into the view for nested packages. + * @param {PackageModel} packageModel - The package model containing the + * parent package metadata. + */ insertParentLink(packageModel) { const parentPackageMetadata = packageModel.get("parentPackageMetadata"); const view = this; - _.each(parentPackageMetadata, (m, i) => { + _.each(parentPackageMetadata, (m, _i) => { const title = m.get("title"); const icon = $(document.createElement("i")).addClass( "icon icon-on-left icon-level-up", @@ -1161,45 +1217,51 @@ define([ }); }, + /** + * Shows a map with the bounding coordinates of the spatial coverage of + * the dataset. + * @param {Array} [customCoordinates] - An array of custom coordinates to + * use for the map in the order of [north, south, east, west]. + * @returns {boolean} Returns false if the map could not be inserted. + */ insertSpatialCoverageMap(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) { + let geoCoverEls = this.$(".geographicCoverage"); + let parseText = false; + let directions = ["north", "south", "east", "west"]; + let [n, s, e, w] = 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 (!geoCoverEls.length) { // For EML let title = this.$('h4:contains("Geographic Region")'); // For FGDC - if (title.length == 0) { + 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"); + geoCoverEls = $(title).parent(); + parseText = true; + directions = ["North", "South", "East", "West"]; } - for (let i = 0; i < georegionEls.length; i++) { - var georegion = georegionEls[i]; + for (let i = 0; i < geoCoverEls.length; i += 1) { + const georegion = geoCoverEls[i]; - if (typeof customCoordinates !== "undefined") { - // Extract the coordinates - var n = customCoordinates[0]; - var s = customCoordinates[1]; - var e = customCoordinates[2]; - var w = customCoordinates[3]; - } else { - var coordinates = new Array(); + if (!customCoordinates?.length) { + const coordinates = []; _.each(directions, (direction) => { // Parse text for older versions of Metacat (v2.4.3 and earlier) + let coordinate = ""; if (parseText) { const labelEl = $(georegion).find( `label:contains("${direction}")`, ); if (labelEl.length) { - var coordinate = $(labelEl).next().html(); + coordinate = $(labelEl).next().html(); if ( typeof coordinate !== "undefined" && coordinate.indexOf(" ") > -1 @@ -1210,7 +1272,7 @@ define([ ); } } else { - var coordinate = $(georegion) + coordinate = $(georegion) .find(`.${direction}BoundingCoordinate`) .attr("data-value"); } @@ -1220,10 +1282,7 @@ define([ }); // Extract the coordinates - var n = coordinates[0]; - var s = coordinates[1]; - var e = coordinates[2]; - var w = coordinates[3]; + [n, s, e, w] = coordinates; } // Create Google Map LatLng objects out of our coordinates @@ -1238,7 +1297,7 @@ define([ // If there isn't a center point found, don't draw the map. if (typeof latLngCEN === "undefined") { - return; + return false; } // Get the map path color @@ -1267,15 +1326,15 @@ define([ `&key=${MetacatUI.mapKey}'/>`; // Find the spot in the DOM to insert our map image - if (parseText) - var insertAfter = $(georegion) + let insertAfter = georegion; + if (parseText) { + 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; + } // Get the URL to the interactive Google Maps instance const url = this.getGoogleMapsUrl(latLngCEN, bounds); @@ -1323,8 +1382,8 @@ define([ }, /** - * Returns the zoom level that will display the given bounding box at - * the given dimensions. + * Returns the zoom level that will display the given bounding box at the + * given dimensions. * @param {LatLngBounds} bounds - The bounding box to display. * @param {object} mapDim - The dimensions of the map. * @param {number} mapDim.height - The height of the map. @@ -1339,8 +1398,9 @@ define([ // useful /** - * - * @param lat + * Converts a latitude to radians. + * @param {number} lat - The latitude to convert. + * @returns {number} The latitude in radians. */ function latRad(lat) { const sin = Math.sin((lat * Math.PI) / 180); @@ -1349,10 +1409,12 @@ define([ } /** - * - * @param mapPx - * @param worldPx - * @param fraction + * Returns the zoom level that will display the given bounding box at + * the given dimensions. + * @param {number} mapPx - The dimensions of the map. + * @param {number} worldPx - The dimensions of the world. + * @param {number} fraction - The fraction of the world to display. + * @returns {number} The zoom level. */ function zoom(mapPx, worldPx, fraction) { return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2); @@ -1372,13 +1434,15 @@ define([ return Math.min(latZoom, lngZoom, ZOOM_MAX); }, + /** Insert the citation header into the view */ insertCitation() { - if (!this.model) return false; + if (!this.model) return; // Create a citation header element from the model attributes const header = new CitationHeaderView({ model: this.model }); this.$(this.citationContainer).html(header.render().el); }, + /** Show a logo for the repository that hosts this metadata */ insertDataSource() { if ( !this.model || @@ -1403,7 +1467,7 @@ define([ // Construct a URL to the profile of this repository const profileURL = - dataSource.identifier == MetacatUI.appModel.get("nodeId") + dataSource.identifier === MetacatUI.appModel.get("nodeId") ? `${MetacatUI.root}/profile` : `${MetacatUI.appModel.get("dataoneSearchUrl")}/portals/${ dataSource.shortIdentifier @@ -1448,18 +1512,18 @@ define([ }, animation: false, }) - .on("mouseenter", function () { - const _this = this; + .on("mouseenter", () => { + const el = this; $(this).popover("show"); $(".popover").on("mouseleave", () => { - $(_this).popover("hide"); + $(el).popover("hide"); }); }) - .on("mouseleave", function () { - const _this = this; + .on("mouseleave", () => { + const el = this; setTimeout(() => { if (!$(".popover:hover").length) { - $(_this).popover("hide"); + $(el).popover("hide"); } }, 300); }); @@ -1467,9 +1531,9 @@ define([ }, /** - * Check whether the user has write permissions on the resource map and the EML. - * Once the permission checks have finished, continue with the functions that - * depend on them. + * Check whether the user has write permissions on the resource map and + * the EML. Once the permission checks have finished, continue with the + * functions that depend on them. */ checkWritePermissions() { const view = this; @@ -1480,20 +1544,20 @@ define([ const modelsToCheck = [this.model, resourceMap]; modelsToCheck.forEach((model, index) => { - // If there is no resource map or no EML, - // then the user does not need permission to edit it. - if (!model || model.get("notFound") == true) { + // If there is no resource map or no EML, then the user does not need + // permission to edit it. + if (!model || model.get("notFound") === true) { authorization[index] = true; - // If we already checked, and the user is authorized, - // record that information in the authorzation array. + // If we already checked, and the user is authorized, record that + // information in the authorzation array. } else if (model.get("isAuthorized_write") === true) { authorization[index] = true; - // If we already checked, and the user is not authorized, - // record that information in the authorzation array. + // If we already checked, and the user is not authorized, record + // that information in the authorzation array. } else if (model.get("isAuthorized_write") === false) { authorization[index] = false; - // If we haven't checked for authorization yet, do that now. - // Return to this function once we've finished checking. + // If we haven't checked for authorization yet, do that now. Return + // to this function once we've finished checking. } else { view.stopListening(model, "change:isAuthorized_write"); view.listenToOnce(model, "change:isAuthorized_write", () => { @@ -1509,11 +1573,13 @@ define([ // Check that all the models were tested for authorization - // Every value in the auth array must be true for the user to have full permissions + // Every value in the auth array must be true for the user to have full + // permissions const allTrue = _.every(authorization, (test) => test); - // When we have completed checking each of the models that we need to check for - // permissions, every value in the authorization array should be "true" or "false", - // and the array should have the same length as the modelsToCheck array. + // When we have completed checking each of the models that we need to + // check for permissions, every value in the authorization array should + // be "true" or "false", and the array should have the same length as + // the modelsToCheck array. const allBoolean = _.every( authorization, (test) => typeof test === "boolean", @@ -1521,22 +1587,25 @@ define([ const allChecked = allBoolean && authorization.length === modelsToCheck.length; - // Check for and render prov diagrams now that we know whether or not the user has editor permissions - // (There is a different version of the chart for users who can edit the resource map and users who cannot) + // Check for and render prov diagrams now that we know whether or not + // the user has editor permissions (There is a different version of the + // chart for users who can edit the resource map and users who cannot) if (allChecked) { this.checkForProv(); } else { return; } - // Only render the editor controls if we have completed the checks AND the user has full editor permissions + // Only render the editor controls if we have completed the checks AND + // the user has full editor permissions if (allTrue) { this.insertEditorControls(); } }, /* - * Inserts control elements onto the page for the user to interact with the dataset - edit, publish, etc. - * Editor permissions should already have been checked before running this function. + * Inserts control elements onto the page for the user to interact with + * the dataset - edit, publish, etc. Editor permissions should already + * have been checked before running this function. */ insertEditorControls() { const view = this; @@ -1545,14 +1614,16 @@ define([ : null; const modelsToCheck = [this.model, resourceMap]; const authorized = _.every(modelsToCheck, (model) => - // If there is no EML or no resource map, the user doesn't need permission to edit it. - !model || model.get("notFound") == true + // If there is no EML or no resource map, the user doesn't need + // permission to edit it. + !model || model.get("notFound") === true ? true : model.get("isAuthorized_write") === true, ); // Only run this function when the user has full editor permissions - // (i.e. write permission on the EML, and write permission on the resource map if there is one.) + // (i.e. write permission on the EML, and write permission on the + // resource map if there is one.) if (!authorized) { return; } @@ -1562,7 +1633,7 @@ define([ this.model.get("obsoletedBy").length > 0) || this.model.get("archived") ) { - return false; + return; } // Save the element that will contain the owner control buttons @@ -1590,7 +1661,8 @@ define([ }), ); } - // If this format is not editable, insert an unspported Edit Metadata template + // If this format is not editable, insert an unspported Edit Metadata + // template else { container.append( this.editMetadataTemplate({ @@ -1600,47 +1672,45 @@ define([ } } - 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 - let canBePublished = - MetacatUI.appModel.get("enablePublishDOI") && !view.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 - const 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. + // Determine if this metadata can be published. The Publish feature has + // to be enabled in the app. The model cannot already have a DOI + let canBePublished = + MetacatUI.appModel.get("enablePublishDOI") && !view.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 + const 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 ( - Array.isArray(authorizedPublishers) && - authorizedPublishers.length + MetacatUI.appUserModel.hasIdentityOverlap(authorizedPublishers) ) { - if ( - MetacatUI.appUserModel.hasIdentityOverlap(authorizedPublishers) - ) { - canBePublished = true; - } else { - canBePublished = false; - } + 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( - view.doiTemplate({ - isAuthorized: true, - identifier: pid, - }), - ); - } - } catch (e) { - console.error("Cannot display the publish button: ", e); + // If this metadata can be published, then insert the Publish button + // template + if (canBePublished) { + // Insert a Publish button template + container.append( + view.doiTemplate({ + isAuthorized: true, + identifier: pid, + }), + ); } }, @@ -1650,12 +1720,11 @@ define([ * 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 add more copiable buttons (or other elements) to a View Service + * XSLT, you should be able to just add something like: * * + * Copy * * to your XSLT and this should pick it up automatically. */ @@ -1675,8 +1744,8 @@ define([ ); // 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 + // it didn't look flexible enough to allow me update innerHTML in a + // chain setTimeout(() => { $(el).html("Copy"); }, 500); @@ -1689,21 +1758,18 @@ define([ * - A "Copy Citation" button to copy the citation text */ insertControls() { - // 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, "\\\\$&"); - }; + // Convert the support mdq formatId list to a version that JS regex + // likes (with special characters double + RegExp.escape = (s) => s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\\\$&"); const 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. + // 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. const thisFormatId = this.model.get("formatId"); - const mdqFormatSupported = false; let formatFound = false; if (mdqFormatIds !== null) { - for (let ifmt = 0; ifmt < mdqFormatIds.length; ++ifmt) { + for (let ifmt = 0; ifmt < mdqFormatIds.length; ifmt += 1) { const currentFormatId = RegExp.escape(mdqFormatIds[ifmt]); const re = new RegExp(currentFormatId); formatFound = re.test(thisFormatId); @@ -1760,7 +1826,8 @@ define([ }, /** - *Creates a button which the user can click to launch the package in Whole Tale + *Creates a button which the user can click to launch the package in Whole + *Tale */ createWholeTaleButton() { const self = this; @@ -1783,24 +1850,24 @@ define([ }); }, - // Inserting the Metric Stats + /** Insert the Metric Stats */ insertMetricsControls() { // Exit if metrics shouldn't be shown for this dataset if (this.model.hideMetrics()) { return; } - const pid_list = []; - pid_list.push(this.pid); + const pidList = []; + pidList.push(this.pid); const metricsModel = new MetricsModel({ - pid_list, + pid_list: pidList, type: "dataset", }); metricsModel.fetch(); this.metricsModel = metricsModel; - // Retreive the model from the server for the given PID - // TODO: Create a Metric Request Object + // Retreive the model from the server for the given PID TODO: Create a + // Metric Request Object if (MetacatUI.appModel.get("displayDatasetMetrics")) { const buttonToolbar = this.$(".metrics-container"); @@ -1824,26 +1891,22 @@ define([ buttonToolbar.append(citationsMetricView.render().el); this.subviews.push(citationsMetricView); - try { - // Check if the registerCitation=true query string is set - if (window.location.search) { - if ( - window.location.search.indexOf("registerCitation=true") > -1 - ) { - // Open the modal for the citations - citationsMetricView.showMetricModal(); - - // Show the register citation form - if (citationsMetricView.modalView) { - citationsMetricView.modalView.on( - "renderComplete", - citationsMetricView.modalView.showCitationForm, - ); - } + // Check if the registerCitation=true query string is set + if (window.location.search) { + if ( + window.location.search.indexOf("registerCitation=true") > -1 + ) { + // Open the modal for the citations + citationsMetricView.showMetricModal(); + + // Show the register citation form + if (citationsMetricView.modalView) { + citationsMetricView.modalView.on( + "renderComplete", + citationsMetricView.modalView.showCitationForm, + ); } } - } catch (e) { - console.warn("Not able to show the register citation form ", e); } } @@ -1861,22 +1924,22 @@ define([ /** * Check if the DataPackage provenance parsing has completed. If it has, - * draw provenance charts. If it hasn't start the parseProv function. - * The view must have the DataPackage collection set as view.dataPackage - * for this function to run. + * draw provenance charts. If it hasn't start the parseProv function. The + * view must have the DataPackage collection set as view.dataPackage for + * this function to run. */ checkForProv() { if (!this.dataPackage) { return; } - // Render the provenance trace using the redrawProvCharts function instead of the drawProvCharts function - // just in case the prov charts have already been inserted. Redraw will make sure they are removed + // Render the provenance trace using the redrawProvCharts function + // instead of the drawProvCharts function just in case the prov charts + // have already been inserted. Redraw will make sure they are removed // before being re-inserted. - const { model } = this; - if (this.dataPackage.provenanceFlag == "complete") { + if (this.dataPackage.provenanceFlag === "complete") { this.redrawProvCharts(this.dataPackage); } else { - this.listenToOnce(this.dataPackage, "queryComplete", function () { + this.listenToOnce(this.dataPackage, "queryComplete", () => { this.redrawProvCharts(this.dataPackage); }); // parseProv triggers "queryComplete" @@ -1884,21 +1947,28 @@ define([ } }, - /* - * 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 + /** + * 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 + * @param {DataPackage} collection - The DataPackage collection to render + * provenance for */ - drawProvCharts(dataPackage) { + drawProvCharts(collection) { + const dataPackage = collection || this.dataPackage; + const view = this; // Set a listener to re-draw the prov charts when needed - this.stopListening(this.dataPackage, "redrawProvCharts"); + this.stopListening(dataPackage, "redrawProvCharts"); this.listenToOnce( - this.dataPackage, + dataPackage, "redrawProvCharts", this.redrawProvCharts, ); - // Provenance has to be retrieved from the Package Model (getProvTrace()) before the charts can be drawn - if (dataPackage.provenanceFlag != "complete") return false; + // Provenance has to be retrieved from the Package Model + // (getProvTrace()) before the charts can be drawn + if (dataPackage.provenanceFlag !== "complete") return; // If the user is authorized to edit the provenance for this package // then turn on editing, so that edit icons are displayed. @@ -1911,11 +1981,12 @@ define([ } // 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 + // we should fetch the DataPackage since it likely has only had a + // shallow fetch so far const 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 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) { let modelsToMerge = []; @@ -1924,7 +1995,7 @@ define([ // Get the PackageModel for this DataPackage const packageModel = _.find( this.packageModels, - (packageModel) => packageModel.get("id") == dataPackage.id, + (model) => model.get("id") === dataPackage.id, ); // Merge the SolrResult models into the DataONEObject models @@ -1933,18 +2004,21 @@ define([ } } - // If there is at least one model to merge into this data package, do so + // 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 () { + // Listen to the DataPackage fetch to complete and re-execute this + // function + this.listenToOnce(dataPackage, "complete", () => { this.drawProvCharts(dataPackage); }); - // Create a query that searches for all the members of this DataPackage in Solr + // 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; @@ -1956,13 +2030,13 @@ define([ // 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 + // 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 + // Draw two flow charts to represent the sources and derivations at a + // package level const packageSources = dataPackage.sourcePackages; const packageDerivations = dataPackage.derivationPackages; @@ -1994,12 +2068,13 @@ define([ dataPackage.derivations.length || editModeOn ) { - // Draw the provenance charts for each member of this package at an object level - _.each(dataPackage.toArray(), (member, i) => { + // Draw the provenance charts for each member of this package at an + // object level + _.each(dataPackage.toArray(), (member, _i) => { // Don't draw prov charts for metadata objects. if ( - member.get("type").toLowerCase() == "metadata" || - member.get("formatType").toLowerCase() == "metadata" + member.get("type").toLowerCase() === "metadata" || + member.get("formatType").toLowerCase() === "metadata" ) { return; } @@ -2011,13 +2086,13 @@ define([ } // Retrieve the sources and derivations for this member - const memberSources = member.get("provSources") || new Array(); - const memberDerivations = - member.get("provDerivations") || new Array(); + const memberSources = member.get("provSources") || []; + const memberDerivations = member.get("provDerivations") || []; - // 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. + // 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) { const memberSourcesProvChart = new ProvChart({ sources: memberSources, @@ -2035,9 +2110,10 @@ define([ 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. + // 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) { const memberDerivationsProvChart = new ProvChart({ derivations: memberDerivations, @@ -2061,10 +2137,9 @@ define([ if (this.$(".prov-chart").length > 10000) { const allNodes = this.$(".prov-chart .node"); let ids = []; - var view = this; let i = 1; - $(allNodes).each(function () { + $(allNodes).each(() => { ids.push($(this).attr("data-id")); }); ids = _.uniq(ids); @@ -2075,12 +2150,13 @@ define([ .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 + // Don't use the unique class on images since they will look a lot + // different anyway by their image if (!$(matchingNodes).first().hasClass("image")) { const className = `uniqueNode${i}`; // Add the unique class and up the iterator - if (matchingNodes.prop("tagName") != "polygon") + if (matchingNodes.prop("tagName") !== "polygon") $(matchingNodes).addClass(className); else $(matchingNodes).attr( @@ -2096,40 +2172,41 @@ define([ id, className, }); - i++; + i += 1; } }); } }, - /* Step through all prov charts and re-render each one that has been - marked for re-rendering. - */ + /** + * Step through all prov charts and re-render each one that has been + * marked for re-rendering. + */ redrawProvCharts() { const 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. + // 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(), (item) => { - item.selectedInEditor == false; + this.dataPackage.toArray().forEach((item) => { + // eslint-disable-next-line no-param-reassign + item.selectedInEditor = false; }); } - _.each(this.subviews, (thisView, i) => { + _.each(this.subviews, (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 + // Check if this ProvChartView is marked for re-rendering Erase the + // current ProvChartView thisView.onClose(); } }); @@ -2138,18 +2215,19 @@ define([ this.subviews = _.filter( this.subviews, (item) => - item.className && item.className.indexOf("prov-chart") == -1, + item.className && item.className.indexOf("prov-chart") === -1, ); view.drawProvCharts(this.dataPackage); }, - /* + /** * When the data package collection saves successfully, tell the user + * @param {DataPackage} savedObject - The object that was saved */ saveSuccess(savedObject) { // We only want to perform these actions after the package saves - if (savedObject.type != "DataPackage") return; + if (savedObject.type !== "DataPackage") return; // Change the URL to the new id MetacatUI.uiRouter.navigate( @@ -2170,7 +2248,8 @@ define([ // Reset the state to clean this.dataPackage.packageModel.set("changed", false); - // If provenance relationships were updated, then reset the edit list now. + // If provenance relationships were updated, then reset the edit list + // now. if (this.dataPackage.provEdits.length) this.dataPackage.provEdits = []; this.saveProvPending = false; @@ -2180,19 +2259,21 @@ define([ // Turn off "save" footer this.hideEditorControls(); - // Update the metadata table header with the new resource map id. - // First find the DataPackageView for the top level package, and - // then re-render it with the update resmap id. + // Update the metadata table header with the new resource map id. First + // find the DataPackageView for the top level package, and then + // re-render it with the update resmap id. const view = this; const metadataId = this.packageModels[0].getMetadata().get("id"); - _.each(this.subviews, (thisView, i) => { + _.each(this.subviews, (thisView, _i) => { // Check if this is a ProvChartView if (thisView.type && thisView.type.indexOf("DataPackage") !== -1) { - if (thisView.currentlyViewing == metadataId) { + if (thisView.currentlyViewing === metadataId) { const packageId = view.dataPackage.packageModel.get("id"); const title = packageId ? `Package: ${packageId}` : ""; + + // eslint-disable-next-line no-param-reassign thisView.title = `Files in this dataset ${title}`; thisView.render(); } @@ -2200,8 +2281,9 @@ define([ }); }, - /* + /** * When the data package collection fails to save, tell the user + * @param {string} errorMsg - The error message to display */ saveError(errorMsg) { const errorId = `error${Math.round(Math.random() * 100)}`; @@ -2234,14 +2316,15 @@ define([ 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. - */ + /** + * 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() { // Only call this function once per save operation. if (this.saveProvPending) return; - const view = this; if (this.dataPackage.provEditsPending()) { this.saveProvPending = true; // If the Data Package failed saving, display an error message @@ -2255,12 +2338,13 @@ define([ this.showSaving(); this.dataPackage.saveProv(); } else { - // TODO: should a dialog be displayed saying that no prov edits were made? + // TODO: should a dialog be displayed saying that no prov edits were + // made? } }, + /** Inactivate the save button during the save process */ showSaving() { - // Change the style of the save button this.$("#save-metadata-prov") .html(' Saving...') .addClass("btn-disabled"); @@ -2268,6 +2352,7 @@ define([ this.$("input, textarea, select, button").prop("disabled", true); }, + /** Activate the save button after the save process */ hideSaving() { this.$("input, textarea, select, button").prop("disabled", false); @@ -2275,35 +2360,43 @@ define([ this.$("#save-metadata-prov").html("Save").removeClass("btn-disabled"); }, + /** Show the editor controls */ showEditorControls() { this.$("#editor-footer").slideDown(); }, + /** Hide the editor controls */ hideEditorControls() { this.$("#editor-footer").slideUp(); }, + /** + * Get the names of the entities in this package + * @param {Array} packageModels - An array of models in this package + */ getEntityNames(packageModels) { const viewRef = this; _.each(packageModels, (packageModel) => { - // Don't get entity names for larger packages - users must put the names in the system metadata + // 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 + // If this package has a different metadata doc than the one we are + // currently viewing const metadataModel = packageModel.getMetadata(); if (!metadataModel) return; - if (metadataModel.get("id") != viewRef.pid) { + if (metadataModel.get("id") !== viewRef.pid) { const requestSettings = { url: MetacatUI.appModel.get("viewServiceUrl") + encodeURIComponent(metadataModel.get("id")), - success(parsedMetadata, response, xhr) { - _.each(packageModel.get("members"), (solrResult, i) => { + success(parsedMetadata, _response, _xhr) { + _.each(packageModel.get("members"), (solrResult, _i) => { let entityName = ""; - if (solrResult.get("formatType") == "METADATA") + if (solrResult.get("formatType") === "METADATA") entityName = solrResult.get("title"); const container = viewRef.findEntityDetailsContainer( @@ -2338,14 +2431,14 @@ define([ return; } - _.each(packageModel.get("members"), (solrResult, i) => { + _.each(packageModel.get("members"), (solrResult, _i) => { let entityName = ""; if (solrResult.get("fileName")) entityName = solrResult.get("fileName"); - else if (solrResult.get("formatType") == "METADATA") + else if (solrResult.get("formatType") === "METADATA") entityName = solrResult.get("title"); - else if (solrResult.get("formatType") == "RESOURCE") return; + else if (solrResult.get("formatType") === "RESOURCE") return; else { const container = viewRef.findEntityDetailsContainer(solrResult); @@ -2360,6 +2453,12 @@ define([ }); }, + /** + * Get the name of the entity in the given container element + * @param {Element} containerEl - The DOM element that contains the entity + * name + * @returns {string} - The name of the entity + */ getEntityName(containerEl) { if (!containerEl) return false; @@ -2377,19 +2476,26 @@ define([ return entityName; }, - // Checks if the metadata has entity details sections + /** + * Checks if the metadata has entity details sections + * @returns {boolean} - True if the metadata has entity details sections + */ hasEntityDetails() { 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. - * @returns {Element} - The DOM element that describbbes the given data entity. + * 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} [containerEl] - The DOM element to exclusivly search + * inside. + * @returns {Element|null} - The DOM element that describes the given data + * entity or null if it cannot be found. */ - findEntityDetailsContainer(model, el) { - if (!el) var { el } = this; + findEntityDetailsContainer(model, containerEl) { + const el = containerEl || this.el; // Get the id and file name for this data object let id = ""; @@ -2398,8 +2504,7 @@ define([ // If a model is given, get the id and file name from the object if ( model && - (DataONEObject.prototype.isPrototypeOf(model) || - SolrResult.prototype.isPrototypeOf(model)) + (model instanceof DataONEObject || model instanceof SolrResult) ) { id = model.get("id"); fileName = model.get("fileName"); @@ -2410,7 +2515,7 @@ define([ } // Otherwise, there isn't enough info to find the element, so exit else { - return; + return null; } // If we already found it earlier, return it now @@ -2427,8 +2532,9 @@ define([ return container; } - // Are we looking for the main object that this MetadataView is displaying? - if (id == this.pid) { + // Are we looking for the main object that this MetadataView is + // displaying? + if (id === this.pid) { if (this.$("#Metadata").length > 0) return this.$("#Metadata"); return this.el; } @@ -2436,7 +2542,8 @@ define([ // Metacat 2.4.2 and up will have the Online Distribution Link marked let link = this.$(`.entitydetails a[data-pid='${id}']`); - // Otherwise, try looking for an anchor with the id matching this object's 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, "\\$&")}`); @@ -2460,12 +2567,14 @@ define([ container = $(link).parents(".entitydetails"); if (container.length < 1) { - // backup - find the parent of this link that is a direct child of the form element + // backup - find the parent of this link that is a direct child of + // the form element const 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 + // 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"); @@ -2487,12 +2596,15 @@ define([ // ----Find by file name rather than id----- if (!fileName) { // Get the name of the object first - for (var i = 0; i < this.packageModels.length; i++) { - var model = _.findWhere(this.packageModels[i].get("members"), { - id, - }); - if (model) { - fileName = model.get("fileName"); + for (let i = 0; i < this.packageModels.length; i += 1) { + const packageModel = _.findWhere( + this.packageModels[i].get("members"), + { + id, + }, + ); + if (packageModel) { + fileName = packageModel.get("fileName"); break; } } @@ -2505,13 +2617,14 @@ define([ `.entitydetails .control-label:contains('Entity Name') + .controls-well:contains('${fileName}')`, ]; - // Search through each possible location in the DOM where the file name might be - for (var i = 0; i < possibleLocations.length; i++) { + // Search through each possible location in the DOM where the file + // name might be + for (let i = 0; i < possibleLocations.length; i += 1) { // Get the elements in this view that match the possible location const matches = this.$(possibleLocations[i]); // If exactly one match is found - if (matches.length == 1) { + if (matches.length === 1) { // Get the entity details parent element container = $(matches).parents(".entitydetails").first(); // Set the object ID on the element for easier locating later @@ -2528,15 +2641,15 @@ define([ } } - // --- The last option:---- - // If this package has only one item, we can assume the only entity details are about that item + // --- The last option:---- If this package has only one item, we can + // assume the only entity details are about that item const members = this.packageModels[0].get("members"); const dataMembers = _.filter( members, - (m) => m.get("formatType") == "DATA", + (m) => m.get("formatType") === "DATA", ); - if (dataMembers.length == 1) { - if (this.$(".entitydetails").length == 1) { + if (dataMembers.length === 1) { + if (this.$(".entitydetails").length === 1) { this.$(".entitydetails").attr("data-id", id); // Store the PID on this element for moreInfo icons this.storeEntityPIDs(this.$(".entitydetails"), id); @@ -2549,7 +2662,8 @@ define([ }, /* - * Inserts new image elements into the DOM via the image template. Use for displaying images that are part of this metadata's resource map. + * Inserts new image elements into the DOM via the image template. Use for + * displaying images that are part of this metadata's resource map. */ insertDataDetails() { // If there is a metadataIndex subview, render from there. @@ -2565,32 +2679,32 @@ define([ const viewRef = this; - _.each(this.packageModels, (packageModel) => { - const dataDisplay = ""; + this.packageModels.forEach((packageModel) => { const images = []; - const other = []; const packageMembers = packageModel.get("members"); // Don't do this for large packages if (packageMembers.length > 150) return; - //= === Loop over each visual object and create a dataDisplay template for it to attach to the DOM ==== - _.each(packageMembers, (solrResult, i) => { + //= === Loop over each visual object and create a dataDisplay template + // for it to attach to the DOM ==== + _.each(packageMembers, (solrResult, _i) => { // Don't display any info about nested packages - if (solrResult.type == "Package") return; + if (solrResult.type === "Package") return; const objID = solrResult.get("id"); - if (objID == viewRef.pid) return; + if (objID === viewRef.pid) return; // Is this a visual object (image)? const type = - solrResult.type == "SolrResult" + solrResult.type === "SolrResult" ? solrResult.getType() : "Data set"; - if (type == "image") images.push(solrResult); + if (type === "image") images.push(solrResult); - // Find the part of the HTML Metadata view that describes this data object + // Find the part of the HTML Metadata view that describes this data + // object const anchor = $(document.createElement("a")).attr( "id", objID.replace(/[^A-Za-z0-9]/g, "-"), @@ -2602,10 +2716,11 @@ define([ }); downloadButton.render(); - // Insert the data display HTML and the anchor tag to mark this spot on the page + // 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") { + if (type === "image") { // Create the data display HTML const dataDisplay = $.parseHTML( viewRef @@ -2622,13 +2737,14 @@ define([ $(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 this image is private, we need to load it via an XHR + // request if (!solrResult.get("isPublic")) { // Create an XHR const xhr = new XMLHttpRequest(); xhr.withCredentials = true; - xhr.onload = function () { + xhr.onload = () => { if (xhr.response) $(dataDisplay) .find("img") @@ -2657,8 +2773,9 @@ define([ } }); - //= === 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. + //= === 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. const numImages = images.length; // The shared lightbox options for both images const lightboxOptions = { @@ -2669,7 +2786,8 @@ define([ aspectRatio: true, closeClick: true, afterLoad() { - // Create a custom HTML caption based on data stored in the DOM element + // Create a custom HTML caption based on data stored in the DOM + // element viewRef.title = `${viewRef.title} Download `; }, helpers: { @@ -2689,17 +2807,18 @@ define([ imgLightboxOptions.type = "image"; imgLightboxOptions.perload = 1; - const initializeImgLightboxes = function () { - numImgChecks++; + const initializeImgLightboxes = () => { + numImgChecks += 1; // Initialize what images have loaded so far after 5 seconds - if (numImgChecks == 10) { + if (numImgChecks === 10) { $(lightboxImgSelector).fancybox(imgLightboxOptions); } - // When 15 seconds have passed, stop checking so we don't blow up the browser + // 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); + window.clearInterval(viewRef.imgIntervalID); return; } @@ -2710,10 +2829,10 @@ define([ $(lightboxImgSelector).fancybox(imgLightboxOptions); // We're done - clear the interval - window.clearInterval(imgIntervalID); + window.clearInterval(viewRef.imgIntervalID); }; - var imgIntervalID = window.setInterval( + viewRef.imgIntervalID = window.setInterval( initializeImgLightboxes, 500, ); @@ -2721,9 +2840,8 @@ define([ }); }, + /** Remove ecogrid links and replace them with workable links */ replaceEcoGridLinks() { - const viewRef = this; - // Find the element in the DOM housing the ecogrid link $("a:contains('ecogrid://')").each((i, thisLink) => { // Get the link text @@ -2744,17 +2862,22 @@ define([ }); }, + /** + * Publish the data package with a DOI + * @param {Event} event - The click event + */ publish(event) { // target may not actually prevent click events, so double check const disabled = $(event.target).closest("a").attr("disabled"); if (disabled) { - return false; + return; } const publishServiceUrl = MetacatUI.appModel.get("publishServiceUrl"); const pid = $(event.target).closest("a").attr("pid"); + // eslint-disable-next-line no-restricted-globals, no-alert const ret = confirm( `Are you sure you want to publish ${pid} with a DOI?`, - ); + ); // TODO: We should use a custom modal here instead of the browser's confirm dialog if (ret) { // show the loading icon @@ -2769,7 +2892,7 @@ define([ xhrFields: { withCredentials: true, }, - success(data, textStatus, xhr) { + success(data, _textStatus, _xhr) { // the response should have new identifier in it identifier = $(data).find("d1\\:identifier, identifier").text(); @@ -2798,7 +2921,7 @@ define([ }, 3000); } }, - error(xhr, textStatus, errorThrown) { + error(xhr, _textStatus, _errorThrown) { // show the error message, but stay on the same page const msg = `Publish failed: ${$(xhr.responseText) .find("description") @@ -2818,7 +2941,12 @@ define([ } }, - // When the given ID from the URL is a resource map that has no metadata, do the following... + /** + * When the given ID from the URL is a resource map that has no metadata, + * render metadata from the index instead. + * @param {SolrResult} solrResultModel - The SolrResult model for the + * resource map + */ noMetadata(solrResultModel) { this.hideLoading(); this.$el.html(this.template()); @@ -2844,9 +2972,10 @@ define([ ); }, - // this will lookup the latest version of the PID + /** Lokup the latest version of the PID and display a link to it */ showLatestVersion() { - // If this metadata doc is not obsoleted by a new version, then exit the function + // If this metadata doc is not obsoleted by a new version, then exit the + // function if (!this.model.get("obsoletedBy")) { return; } @@ -2856,7 +2985,7 @@ define([ // When the latest version is found, this.listenTo(this.model, "change:newestVersion", () => { // Make sure it has a newer version, and if so, - if (view.model.get("newestVersion") != view.model.get("id")) { + 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({ @@ -2879,6 +3008,10 @@ define([ this.model.findLatestVersion(); }, + /** + * Indicate that the metadata is being loaded + * @param {string} message - The message to display while loading + */ showLoading(message) { this.hideLoading(); @@ -2893,11 +3026,16 @@ define([ this.$el.html(loading); }, + /** Hide the loading message */ hideLoading() { if (this.$loading) this.$loading.remove(); if (this.$detached) this.$el.html(this.$detached); }, + /** + * Show an error message to the user + * @param {string} msg - The error message to display + */ showError(msg) { // Remove any existing error messages this.$el.children(".alert-container").remove(); @@ -2913,10 +3051,13 @@ define([ }, /** - * 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) - * @param e + * 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) + * @param {Event} e - The click event + * @returns {boolean} - Returns false if the click event should not be + * followed */ previewData(e) { // Don't go anywhere yet... @@ -2926,10 +3067,12 @@ define([ let 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 (!link?.length) return false; + + const id = $(link).attr("data-id"); + + // This will make the app defualt to the child view previewData function + if (!id) return false; // If we are on the Metadata view, update the URL and scroll to the // anchor @@ -2968,27 +3111,32 @@ define([ /** * Navigate to a new /view URL with a fragment * - * Used in getModel() when the pid originally passed into MetadataView - * is not a metadata PID but is, instead, a data PID. getModel() does - * the work of finding an appropriate metadata PID for the data PID and - * this method handles re-routing to the correct URL. - * @param {string} metadata_pid - The new metadata PID - * @param {string} data_pid - Optional. A data PID that's part of the - * package metadata_pid exists within. + * Used in getModel() when the pid originally passed into MetadataView is + * not a metadata PID but is, instead, a data PID. getModel() does the + * work of finding an appropriate metadata PID for the data PID and this + * method handles re-routing to the correct URL. + * @param {string} metadataPid - The new metadata PID + * @param {string} dataPid - Optional. A data PID that's part of the + * package metadataPid exists within. */ - navigateWithFragment(metadata_pid, data_pid) { - let next_route = `view/${encodeURIComponent(metadata_pid)}`; + navigateWithFragment(metadataPid, dataPid) { + let nextRoute = `view/${encodeURIComponent(metadataPid)}`; - if (typeof data_pid === "string" && data_pid.length > 0) { - next_route += `#${encodeURIComponent(data_pid)}`; + if (typeof dataPid === "string" && dataPid.length > 0) { + nextRoute += `#${encodeURIComponent(dataPid)}`; } - MetacatUI.uiRouter.navigate(next_route, { trigger: true }); + MetacatUI.uiRouter.navigate(nextRoute, { trigger: true }); }, + /** + * Close any active popovers when the user clicks outside of them + * @param {Event} e - The click event + */ closePopovers(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 + // 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 const svg = $(e.target).parents("svg"); if ( @@ -3004,6 +3152,11 @@ define([ this.$(".popover-this.active").popover("hide"); }, + /** + * When the user clicks on a node in the provenance chart, highlight the + * node and its metadata section + * @param {Event} e - The click event + */ highlightNode(e) { // Find the id let id = $(e.target).attr("data-id"); @@ -3012,13 +3165,13 @@ define([ id = $(e.target).parents("[data-id]").attr("data-id"); // If there is no id, return - if (typeof id === "undefined") return false; + if (typeof id === "undefined") return; // Highlight its node $(`.prov-chart .node[data-id='${id}']`).toggleClass("active"); // Highlight its metadata section - if (MetacatUI.appModel.get("pid") == id) + if (MetacatUI.appModel.get("pid") === id) this.$("#Metadata").toggleClass("active"); else { const entityDetails = this.findEntityDetailsContainer(id); @@ -3026,16 +3179,15 @@ define([ } }, + /** Actions to perform when the view is closed */ onClose() { - const viewRef = this; - this.stopListening(); _.each(this.subviews, (subview) => { if (subview.onClose) subview.onClose(); }); - this.packageModels = new Array(); + this.packageModels = []; this.model.set(this.model.defaults); this.pid = null; this.dataPackage = null; @@ -3056,6 +3208,7 @@ define([ * 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. + * @returns {string} - A string of author names, formatted for citation */ getAuthorText() { const authors = this.model.get("origin"); @@ -3063,9 +3216,9 @@ define([ let authorText = ""; _.each(authors, (author) => { - count++; + count += 1; - if (count == 6) { + if (count === 6) { authorText += ", et al. "; return; } @@ -3078,7 +3231,7 @@ define([ authorText += ","; } - if (count == authors.length) { + if (count === authors.length) { authorText += " and"; } @@ -3097,6 +3250,8 @@ define([ * 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. + * @returns {string} - A string of the publisher name, formatted for + * citation */ getPublisherText() { const datasource = this.model.get("datasource"); @@ -3111,10 +3266,12 @@ define([ /** * Generate a string appropriate to be used as the publication date in a * dataset citation. + * @returns {string} - A string of the publication date, formatted for + * citation */ getDatePublishedText() { - // Dataset/datePublished - // Prefer pubDate, fall back to dateUploaded so we have something to show + // Dataset/datePublished Prefer pubDate, fall back to dateUploaded so we + // have something to show if (this.model.get("pubDate") !== "") { return this.model.get("pubDate"); } @@ -3122,25 +3279,19 @@ define([ }, /** - * Generate Schema.org-compliant JSONLD for the model bound to the view into - * the head tag of the page by `insertJSONLD`. + * 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. + * Note: `insertJSONLD` should be called to do the actual inserting into + * the DOM. + * @returns {object} - JSON-LD object for the model bound to the view */ generateJSONLD() { const { model } = this; - // Determine the path (either #view or view, depending on router - // configuration) for use in the 'url' property - const { href } = document.location; - const 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. + // know will come back from Solr (System Metadata fields). Add the rest + // in conditional on whether they are present. const elJSON = { "@context": { "@vocab": "https://schema.org/", @@ -3244,10 +3395,10 @@ define([ if (model.get("abstract")) { elJSON.description = model.get("abstract"); } else { - const datasets_url = `https://dataone.org/datasets/${encodeURIComponent( + const datasetsUrl = `https://dataone.org/datasets/${encodeURIComponent( model.get("id"), )}`; - elJSON.description = `No description is available. Visit ${datasets_url} for complete metadata about this dataset.`; + elJSON.description = `No description is available. Visit ${datasetsUrl} for complete metadata about this dataset.`; } // Dataset/keywords @@ -3285,9 +3436,10 @@ define([ /** * Generate a Schema.org/identifier from the model's id * - * Tries to use the PropertyValue pattern when the identifier is a DOI - * and falls back to a Text value otherwise + * Tries to use the PropertyValue pattern when the identifier is a DOI and + * falls back to a Text value otherwise * @param {string} identifier - The raw identifier + * @returns {object} - A Schema.org/PropertyValue object or a string */ generateSchemaOrgIdentifier(identifier) { if (!this.model.isDOI()) { @@ -3313,10 +3465,11 @@ define([ * * Either generates a GeoCoordinates (when the north and east coords are * the same) or a GeoShape otherwise. - * @param north - * @param east - * @param south - * @param west + * @param {number} north - North bounding coordinate + * @param {number} east - East bounding coordinate + * @param {number} south - South bounding coordinate + * @param {number} west - West bounding coordinate + * @returns {object} - A Schema.org/Place/geo object */ generateSchemaOrgGeo(north, east, south, west) { if (north === south) { @@ -3340,8 +3493,8 @@ define([ * 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 + * 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. * @@ -3351,6 +3504,7 @@ define([ * @param {number} east - East bounding coordinate * @param {number} south - South bounding coordinate * @param {number} west - West bounding coordinate + * @returns {string} - A stringified GeoJSON object */ generateGeoJSONString(north, east, south, west) { if (north === south) { @@ -3363,8 +3517,8 @@ define([ * Generate a GeoJSON Point object * @param {number} north - North bounding coordinate * @param {number} east - East bounding coordinate - * - * Example: + * @returns {string} - A stringified GeoJSON Point object + * @example * { * "type": "Point", * "coordinates": [ @@ -3386,10 +3540,8 @@ define([ * @param {number} east - East bounding coordinate * @param {number} south - South bounding coordinate * @param {number} west - West bounding coordinate - * - * - * Example: - * + * @returns {string} - A stringified GeoJSON Polygon object + * @example * { * "type": "Polygon", * "coordinates": [[ @@ -3405,14 +3557,12 @@ define([ '{"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; - } + const fixedEast = east < west ? 360 - east : east; const inner = `[${west},${south}],` + - `[${east},${south}],` + - `[${east},${north}],` + + `[${fixedEast},${south}],` + + `[${fixedEast},${north}],` + `[${west},${north}],` + `[${west},${south}]`; @@ -3423,16 +3573,13 @@ define([ /** * Create a canonical IRI for a DOI given a random DataONE identifier. - * @param {string} identifier: The identifier to (possibly) create the IRI - * for. - * @param identifier - * @returns {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. + * @param {string} identifier The identifier to (possibly) create the IRI + * for. + * @returns {string|null} Returns null when matching the identifier to a + * DOI regex fails or a string when the match is successful */ getCanonicalDOIIRI(identifier) { return MetacatUI.appModel.DOItoURL(identifier) || null; @@ -3489,21 +3636,18 @@ define([ ); }, + /** Insert the interactive annoation views */ createAnnotationViews() { - try { - const viewRef = this; - - _.each($(".annotation"), (annoEl) => { - const newView = new AnnotationView({ - el: annoEl, - }); - viewRef.subviews.push(newView); + const viewRef = this; + _.each($(".annotation"), (annoEl) => { + const newView = new AnnotationView({ + el: annoEl, }); - } catch (e) { - console.error(e); - } + viewRef.subviews.push(newView); + }); }, + /** Insert the markdown views */ insertMarkdownViews() { const viewRef = this; @@ -3522,19 +3666,27 @@ define([ }); }, + /** + * Store PIDs for each entity as an array on the view (this.entities) + * @param {HTMLElement} entityEl - An element that contains the entity PID + * as a data-id attribute. Used if the entity PID is not provided. + * @param {string} entityId - The identifier for the entity. + */ storeEntityPIDs(entityEl, entityId) { let entityPID = entityId; // Get the entity ID if it is null or undefined - if (entityPID == null) entityPID = $(entityEl).data("id"); + if (entityPID === null) entityPID = $(entityEl).data("id"); // Perform clean up with the entity ID if (entityPID && typeof entityPID === "string") { - // Check and replace urn-uuid- with urn:uuid: if the string starts with urn-uuid- + // Check and replace urn-uuid- with urn:uuid: if the string starts + // with urn-uuid- if (entityPID.startsWith("urn-uuid-")) { entityPID = entityPID.replace("urn-uuid-", "urn:uuid:"); } - // Check and replace doi-10. with doi:10. if the string starts with doi-10. + // Check and replace doi-10. with doi:10. if the string starts with + // doi-10. if (entityPID.startsWith("doi-10.")) { entityPID = entityPID.replace("doi-10.", "doi:10."); } diff --git a/src/js/views/TableEditorView.js b/src/js/views/TableEditorView.js index 71f6dbde2..98fc449dd 100644 --- a/src/js/views/TableEditorView.js +++ b/src/js/views/TableEditorView.js @@ -5,22 +5,20 @@ define([ "markdownTableFromJson", "markdownTableToJson", "text!templates/tableEditor.html", -], function ( - _, - $, - Backbone, - markdownTableFromJson, - markdownTableToJson, - Template, -) { +], (_, $, Backbone, markdownTableFromJson, markdownTableToJson, Template) => { + // a utility function to check if a value is empty for sorting + const valIsEmpty = (x) => + x === "" || x === undefined || x === null || Number.isNaN(x); + /** * @class TableEditorView - * @classdesc A view of an HTML textarea with markdown editor UI and preview tab + * @classdesc A view of an HTML textarea with markdown editor UI and preview + * tab * @classcategory Views - * @extends Backbone.View - * @constructor + * @augments Backbone.View + * @class */ - var TableEditorView = Backbone.View.extend( + const TableEditorView = Backbone.View.extend( /** @lends TableEditorView.prototype */ { /** @@ -48,14 +46,14 @@ define([ * header row * @type {number} */ - rowCount: 0, // No of rows + rowCount: 0, /** - * The current number of columns displayed in the spreadsheet, including the - * row number column + * The current number of columns displayed in the spreadsheet, including + * the row number column * @type {number} */ - colCount: 0, // No of cols + colCount: 0, /** * The same data shown in the table as a stringified JSON object. @@ -70,8 +68,9 @@ define([ sortingHistory: new Map(), /** - * The events this view will listen to and the associated function to call. - * @type {Object} + * The events this view will listen to and the associated function to + * call. + * @type {object} */ events: { "click #reset": "resetData", @@ -93,269 +92,204 @@ define([ /** * Initialize is executed when a new tableEditor is created. * @constructs TableEditorView - * @param {Object} options - A literal object with options to pass to the view - */ - initialize: function (options) { - try { - options = _.extend(this.defaults, options); - - // Get all the options and apply them to this view - if (options) { - var optionKeys = Object.keys(options); - _.each( - optionKeys, - function (key, i) { - this[key] = options[key]; - }, - this, - ); - } - } catch (e) { - console.log( - "Failed to initialize the table editor view, error message: " + e, - ); - } + * @param {object} options - A literal object with options to pass to the + * view + * @param {string} options.markdown - A markdown table to edit. + * @param {string} options.tableData - The table data as a stringified + * JSON in the form of an array of arrays. Only used if markdown is not + * provided. + */ + initialize(options = {}) { + const mergedOptions = { ...this.defaults, ...options }; + + Object.keys(mergedOptions).forEach((key) => { + this[key] = mergedOptions[key]; + }); }, /** - * render - Renders the tableEditor - add UI for creating and editing tables - */ - render: function () { - try { - // Insert the template into the view - this.$el - .html( - this.template({ - cid: this.cid, - }), - ) - .data("view", this); - - // If initalized with markdown, convert to JSON and use as table data - // Parse the table string into a javascript object so that we can pass it - // into the table editor view to be edited by the user. - if (this.markdown && this.markdown.length > 0) { - var tableArray = this.getJSONfromMarkdown(this.markdown); - if (tableArray && Array.isArray(tableArray) && tableArray.length) { - this.saveData(tableArray); - this.createSpreadsheet(); - // Add the column that we use for row numbers in the editor - this.addColumn(0, "left"); - } - } else { + * Renders the tableEditor - add UI for creating and editing tables + */ + render() { + // Insert the template into the view + this.$el + .html( + this.template({ + cid: this.cid, + }), + ) + .data("view", this); + + // If initalized with markdown, convert to JSON and use as table data + // Parse the table string into a javascript object so that we can pass + // it into the table editor view to be edited by the user. + if (this.markdown && this.markdown.length > 0) { + const tableArray = this.getJSONfromMarkdown(this.markdown); + if (tableArray && Array.isArray(tableArray) && tableArray.length) { + this.saveData(tableArray); this.createSpreadsheet(); + // Add the column that we use for row numbers in the editor + this.addColumn(0, "left"); } - } catch (e) { - console.log( - "Failed to render the table editor view, error message: " + e, - ); + } else { + this.createSpreadsheet(); } }, /** - * createSpreadsheet - Creates or re-creates the table & headers with data, - * if there is any. + * Creates or re-creates the table & headers with data, if there is any. */ - createSpreadsheet: function () { - try { - const spreadsheetData = this.getData(); + createSpreadsheet() { + const spreadsheetData = this.getData(); - this.rowCount = spreadsheetData.length - 1 || this.initialRowCount; - this.colCount = spreadsheetData[0].length - 1 || this.initialColCount; + this.rowCount = spreadsheetData.length - 1 || this.initialRowCount; + this.colCount = spreadsheetData[0].length - 1 || this.initialColCount; - const tableHeaderElement = this.$el.find(".table-headers")[0]; - const tableBodyElement = this.$el.find(".table-body")[0]; + const tableHeaderElement = this.$el.find(".table-headers")[0]; + const tableBodyElement = this.$el.find(".table-body")[0]; - const tableBody = tableBodyElement.cloneNode(true); - tableBodyElement.parentNode.replaceChild(tableBody, tableBodyElement); - const tableHeaders = tableHeaderElement.cloneNode(true); - tableHeaderElement.parentNode.replaceChild( - tableHeaders, - tableHeaderElement, - ); + const tableBody = tableBodyElement.cloneNode(true); + tableBodyElement.parentNode.replaceChild(tableBody, tableBodyElement); + const tableHeaders = tableHeaderElement.cloneNode(true); + tableHeaderElement.parentNode.replaceChild( + tableHeaders, + tableHeaderElement, + ); - tableHeaders.innerHTML = ""; - tableBody.innerHTML = ""; + tableHeaders.innerHTML = ""; + tableBody.innerHTML = ""; - tableHeaders.appendChild(this.createHeaderRow(this.colCount)); - this.createTableBody(tableBody, this.rowCount, this.colCount); + tableHeaders.appendChild(this.createHeaderRow(this.colCount)); + this.createTableBody(tableBody, this.rowCount, this.colCount); - this.populateTable(); - } catch (e) { - console.log( - "Failed to create a spreadsheet in the table editor view, error message: " + - e, - ); - } + this.populateTable(); }, /** - * populateTable - Fill data in created table from saved data + * Fill data in created table from saved data */ - populateTable: function () { - try { - const data = this.getData(); - if (data === undefined || data === null) return; - - for (let i = 0; i < data.length; i++) { - for (let j = 1; j < data[i].length; j++) { - const cell = this.$el.find(`#r-${i}-${j}`)[0]; - let value = data[i][j]; - if (i > 0) { - cell.innerHTML = data[i][j]; - } else { - // table headers - if (!value) { - value = "Col " + j; - } - $(cell).find(".column-header-span")[0].innerHTML = value; + populateTable() { + const data = this.getData(); + if (data === undefined || data === null) return; + + for (let i = 0; i < data.length; i += 1) { + for (let j = 1; j < data[i].length; j += 1) { + const cell = this.$el.find(`#r-${i}-${j}`)[0]; + let value = data[i][j]; + if (i > 0) { + cell.innerHTML = data[i][j]; + } else { + // table headers + if (!value) { + value = `Col ${j}`; } + // TODO: test this + $(cell).find(".column-header-span").text(value); } } - } catch (e) { - console.log( - "Failed to populate the table in the table editor view, error message: " + - e, - ); } }, /** - * getData - Get the saved data and parse it. If there's no saved data, - * create it. + * Get the saved data and parse it. If there's no saved data, create it. + * @returns {Array} The table data as an array of arrays */ - getData: function () { - try { - let data = this.tableData; - if (data === undefined || data === null || data.length == 0) { - return this.initializeData(); - } - return JSON.parse(data); - } catch (e) { - console.log( - "Failed to get and parse data in the Table Editor View, error message: " + - e, - ); + getData() { + const data = this.tableData; + if (!data) { + return this.initializeData(); } + return JSON.parse(data); }, /** - * initializeData - Create some empty arrays to hold data + * Create some empty arrays to hold data + * @returns {Array} An array of arrays, each of which is an empty array */ - initializeData: function () { - try { - const data = []; - for (let i = 0; i <= this.rowCount; i++) { - const child = []; - for (let j = 0; j <= this.colCount; j++) { - child.push(""); - } - data.push(child); + initializeData() { + const data = []; + for (let i = 0; i <= this.rowCount; i += 1) { + const child = []; + for (let j = 0; j <= this.colCount; j += 1) { + child.push(""); } - return data; - } catch (e) { - console.log( - "Failed to create new data in the Table Editor View, error message: " + - e, - ); + data.push(child); } + return data; }, /** - * updateData - When the user focuses out, presume they've changed the data, - * and updated the saved data. - * + * When the user focuses out, presume they've changed the data, and + * updated the saved data. * @param {event} e The focus out event that triggered this function */ - updateData: function (e) { - try { - if (e.target) { - let item; - let newValue; - if (e.target.nodeName === "TD") { - item = e.target; - newValue = item.textContent; - } else if (e.target.classList.contains("column-header-span")) { - item = e.target.parentNode; - newValue = e.target.textContent; - } - if (item) { - const indices = item.id.split("-"); - let spreadsheetData = this.getData(); - spreadsheetData[indices[1]][indices[2]] = newValue; - this.saveData(spreadsheetData); - } + updateData(e) { + if (e.target) { + let item; + let newValue; + if (e.target.nodeName === "TD") { + item = e.target; + newValue = item.textContent; + } else if (e.target.classList.contains("column-header-span")) { + item = e.target.parentNode; + newValue = e.target.textContent; + } + if (item) { + const indices = item.id.split("-"); + const spreadsheetData = this.getData(); + spreadsheetData[indices[1]][indices[2]] = newValue; + this.saveData(spreadsheetData); } - } catch (e) { - console.log( - "Failed to update data in the Table Editor View, error message: " + - e, - ); } }, /** - * saveData - Save the data as a string. - * - * @param {type} data description - * @return {type} description + * Save the data as a string on the tableData property of the view + * @param {Array} data The table data as an array of arrays */ - saveData: function (data) { - try { - this.tableData = JSON.stringify(data); - } catch (e) { - console.log( - "Failed to save data in the Table Editor View, error message: " + e, - ); - } + saveData(data) { + this.tableData = JSON.stringify(data); }, /** - * resetData - Clear the saved data and reset the table to the default - * number of rows & columns - * - * @param {event} e - the event that triggered this function + * Clear the saved data and reset the table to the default number of rows + * & columns + * @param {event} _e - the event that triggered this function */ - resetData: function (e) { - try { - confirmation = confirm( - "This will erase all data and reset the table. Are you sure?", - ); - if (confirmation == true) { - this.tableData = ""; - this.rowCount = this.initialRowCount; - this.colCount = this.initialColCount; - this.createSpreadsheet(); - } else { - return; - } - } catch (e) { - console.log( - "Failed to reset data in the Table Editor View, error message: " + - e, - ); + resetData(_e) { + // eslint-disable-next-line no-restricted-globals, no-alert + const confirmation = confirm( + "This will erase all data and reset the table. Are you sure?", + ); + if (confirmation === true) { + this.tableData = ""; + this.rowCount = this.initialRowCount; + this.colCount = this.initialColCount; + this.createSpreadsheet(); + } else { + // TODO? } }, /** - * createHeaderRow - Create a header row for the table - */ - createHeaderRow: function () { - try { - const tr = document.createElement("tr"); - tr.setAttribute("id", "r-0"); - for (let i = 0; i <= this.colCount; i++) { - const th = document.createElement("th"); - th.setAttribute("id", `r-0-${i}`); - th.setAttribute("class", `${i === 0 ? "" : "column-header"}`); - if (i !== 0) { - const span = document.createElement("span"); - span.innerHTML = `Col ${i}`; - span.setAttribute("class", "column-header-span"); - span.setAttribute("contentEditable", "true"); - const dropDownDiv = document.createElement("div"); - dropDownDiv.setAttribute("class", "dropdown"); - dropDownDiv.innerHTML = ` + * Create a header row for the table + * @returns {HTMLElement} The header row element + */ + createHeaderRow() { + const tr = document.createElement("tr"); + tr.setAttribute("id", "r-0"); + for (let i = 0; i <= this.colCount; i += 1) { + const th = document.createElement("th"); + th.setAttribute("id", `r-0-${i}`); + th.setAttribute("class", `${i === 0 ? "" : "column-header"}`); + if (i !== 0) { + const span = document.createElement("span"); + span.innerHTML = `Col ${i}`; + span.setAttribute("class", "column-header-span"); + span.setAttribute("contentEditable", "true"); + const dropDownDiv = document.createElement("div"); + dropDownDiv.setAttribute("class", "dropdown"); + dropDownDiv.innerHTML = ` @@ -366,39 +300,33 @@ define([ `; - th.appendChild(span); - th.appendChild(dropDownDiv); - } - tr.appendChild(th); + th.appendChild(span); + th.appendChild(dropDownDiv); } - return tr; - } catch (e) { - console.log( - "Failed to create header row in the Table Editor View, error message: " + - e, - ); + tr.appendChild(th); } + return tr; }, /** - * createTableBodyRow - Create a row for the table - * - * @param {number} rowNum The table row number to add to the table, where 0 is the header row - */ - createTableBodyRow: function (rowNum) { - try { - const tr = document.createElement("tr"); - tr.setAttribute("id", `r-${rowNum}`); - for (let i = 0; i <= this.colCount; i++) { - const cell = document.createElement(`${i === 0 ? "th" : "td"}`); - // header - if (i === 0) { - cell.contentEditable = false; - const span = document.createElement("span"); - const dropDownDiv = document.createElement("div"); - span.innerHTML = rowNum; - dropDownDiv.setAttribute("class", "dropdown"); - dropDownDiv.innerHTML = ` + * Create a row for the table + * @param {number} rowNum The table row number to add to the table, where + * 0 is the header row + * @returns {HTMLElement} The row element + */ + createTableBodyRow(rowNum) { + const tr = document.createElement("tr"); + tr.setAttribute("id", `r-${rowNum}`); + for (let i = 0; i <= this.colCount; i += 1) { + const cell = document.createElement(`${i === 0 ? "th" : "td"}`); + // header + if (i === 0) { + cell.contentEditable = false; + const span = document.createElement("span"); + const dropDownDiv = document.createElement("div"); + span.innerHTML = rowNum; + dropDownDiv.setAttribute("class", "dropdown"); + dropDownDiv.innerHTML = ` @@ -408,441 +336,333 @@ define([ `; - cell.appendChild(span); - cell.appendChild(dropDownDiv); - cell.setAttribute("class", "row-header"); - } else { - cell.contentEditable = true; - } - cell.setAttribute("id", `r-${rowNum}-${i}`); - tr.appendChild(cell); + cell.appendChild(span); + cell.appendChild(dropDownDiv); + cell.setAttribute("class", "row-header"); + } else { + cell.contentEditable = true; } - return tr; - } catch (e) { - console.log( - "Failed to create table row in the Table Editor View, error message: " + - e, - ); + cell.setAttribute("id", `r-${rowNum}-${i}`); + tr.appendChild(cell); } + return tr; }, /** - * createTableBody - Given a table element, add table rows - * + * Given a table element, add table rows * @param {HTMLElement} tableBody A table HTML Element */ - createTableBody: function (tableBody) { - try { - for (let rowNum = 1; rowNum <= this.rowCount; rowNum++) { - tableBody.appendChild(this.createTableBodyRow(rowNum)); - } - } catch (e) { - console.log( - "Failed to create table body in the Table Editor View, error message: " + - e, - ); + createTableBody(tableBody) { + for (let rowNum = 1; rowNum <= this.rowCount; rowNum += 1) { + tableBody.appendChild(this.createTableBodyRow(rowNum)); } }, /** - * addRow - Utility function to add row - * + * Utility function to add row * @param {number} currentRow The row number at which to add a new row - * @param {string} direction Can be "top" or "bottom", indicating whether to new row should be above or below the current row - */ - addRow: function (currentRow, direction) { - try { - let data = this.getData(); - const colCount = data[0].length; - const newRow = new Array(colCount).fill(""); - if (direction === "top") { - data.splice(currentRow, 0, newRow); - } else if (direction === "bottom") { - data.splice(currentRow + 1, 0, newRow); - } - this.rowCount++; - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log( - "Failed to add row in the Table Editor View, error message: " + e, - ); + * @param {string} direction Can be "top" or "bottom", indicating + * whether to new row should be above or below the current row + */ + addRow(currentRow, direction) { + const data = this.getData(); + const colCount = data[0].length; + const newRow = new Array(colCount).fill(""); + if (direction === "top") { + data.splice(currentRow, 0, newRow); + } else if (direction === "bottom") { + data.splice(currentRow + 1, 0, newRow); } + this.rowCount += 1; + this.saveData(data); + this.createSpreadsheet(); }, /** - * deleteRow - Utility function to delete row - * + * Utility function to delete row * @param {number} currentRow The row number to delete */ - deleteRow: function (currentRow) { - try { - let data = this.getData(); - // Don't allow deletion of the last row - if (data.length <= 2) { - this.resetData(); - return; - } - data.splice(currentRow, 1); - this.rowCount--; - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log( - "Failed to delete row in the Table Editor View, error message: " + - e, - ); + deleteRow(currentRow) { + const data = this.getData(); + // Don't allow deletion of the last row + if (data.length <= 2) { + this.resetData(); + return; } + data.splice(currentRow, 1); + this.rowCount -= 1; + this.saveData(data); + this.createSpreadsheet(); }, /** - * addColumn - Utility function to add columns - * - * @param {number} currentCol The column number at which to add a new column - * @param {string} direction Can be "left" or "right", indicating whether to new column should be to the left or right of the current column - */ - addColumn: function (currentCol, direction) { - try { - let data = this.getData(); - for (let i = 0; i <= this.rowCount; i++) { - if (direction === "left") { - data[i].splice(currentCol, 0, ""); - } else if (direction === "right") { - data[i].splice(currentCol + 1, 0, ""); - } + * Utility function to add columns + * @param {number} currentCol The column number at which to add a new + * column + * @param {string} direction Can be "left" or "right", indicating + * whether to new column should be to the left or right of the current + * column + */ + addColumn(currentCol, direction) { + const data = this.getData(); + for (let i = 0; i <= this.rowCount; i += 1) { + if (direction === "left") { + data[i].splice(currentCol, 0, ""); + } else if (direction === "right") { + data[i].splice(currentCol + 1, 0, ""); } - this.colCount++; - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log( - "Failed to add column in the Table Editor View, error message: " + - e, - ); } + this.colCount += 1; + this.saveData(data); + this.createSpreadsheet(); }, /** - * deleteColumn - Utility function to delete column - * + * Utility function to delete column * @param {number} currentCol The number of the column to delete */ - deleteColumn: function (currentCol) { - try { - let data = this.getData(); - // Don't allow deletion of the last column - if (data[0].length <= 2) { - this.resetData(); - return; - } - for (let i = 0; i <= this.rowCount; i++) { - data[i].splice(currentCol, 1); - } - this.colCount--; - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log( - "Failed to delete column in the Table Editor View, error message: " + - e, - ); + deleteColumn(currentCol) { + const data = this.getData(); + // Don't allow deletion of the last column + if (data[0].length <= 2) { + this.resetData(); + return; + } + for (let i = 0; i <= this.rowCount; i += 1) { + data[i].splice(currentCol, 1); } + this.colCount -= 1; + this.saveData(data); + this.createSpreadsheet(); }, /** - * sortColumn - Utility function to sort columns - * + * Utility function to sort columns * @param {number} currentCol The column number of the column to delete */ - sortColumn: function (currentCol) { - try { - let spreadSheetData = this.getData(); - let data = spreadSheetData.slice(1); - let headers = spreadSheetData.slice(0, 1)[0]; - if (!data.some((a) => a[currentCol] !== "")) return; - if (this.sortingHistory.has(currentCol)) { - const sortOrder = this.sortingHistory.get(currentCol); - switch (sortOrder) { - case "desc": - data.sort(this.ascSort.bind(this, currentCol)); - this.sortingHistory.set(currentCol, "asc"); - break; - case "asc": - data.sort(this.dscSort.bind(this, currentCol)); - this.sortingHistory.set(currentCol, "desc"); - break; - } - } else { + sortColumn(currentCol) { + const spreadSheetData = this.getData(); + const data = spreadSheetData.slice(1); + const headers = spreadSheetData.slice(0, 1)[0]; + if (!data.some((a) => a[currentCol] !== "")) return; + if (this.sortingHistory.has(currentCol)) { + const sortOrder = this.sortingHistory.get(currentCol); + if (sortOrder === "desc") { data.sort(this.ascSort.bind(this, currentCol)); this.sortingHistory.set(currentCol, "asc"); + } else { + data.sort(this.dscSort.bind(this, currentCol)); + this.sortingHistory.set(currentCol, "desc"); } - data.splice(0, 0, headers); - this.saveData(data); - this.createSpreadsheet(); - } catch (e) { - console.log( - "Failed to sort column in the Table Editor View, error message: " + - e, - ); + } else { + data.sort(this.ascSort.bind(this, currentCol)); + this.sortingHistory.set(currentCol, "asc"); } + data.splice(0, 0, headers); + this.saveData(data); + this.createSpreadsheet(); }, /** - * ascSort - Compare Functions for sorting - ascending - * - * @param {number} currentCol The number of the column to sort - * @param {*} a One of two items to compare - * @param {*} b The second of two items to compare - * @return {number} A number indicating the order to place a vs b in the list. It it returns less than zero, then a will be placed before b in the list. + * Compare Functions for sorting - ascending + * @param {number} currentCol The number of the column to sort + * @param {*} a One of two items to compare + * @param {*} b The second of two items to compare + * @returns {number} A number indicating the order to place a vs b in the + * list. It it returns less than zero, then a will be placed before b in + * the list. */ - ascSort: function (currentCol, a, b) { + ascSort(currentCol, a, b) { try { - let _a = a[currentCol]; - let _b = b[currentCol]; - if (_a === "") return 1; - if (_b === "") return -1; + let valA = a[currentCol]; + let valB = b[currentCol]; + + if (valIsEmpty(valA)) return 1; + if (valIsEmpty(valB)) return -1; // Check for strings and numbers - if (isNaN(_a) || isNaN(_b)) { - _a = _a.toUpperCase(); - _b = _b.toUpperCase(); - if (_a < _b) return -1; - if (_a > _b) return 1; - return 0; + if (typeof valA === "number" && typeof valB === "number") { + return valA - valB; } - return _a - _b; + valA = String(valA).toUpperCase(); + valB = String(valB).toUpperCase(); + if (valA < valB) return -1; + if (valA > valB) return 1; + return 0; } catch (e) { - console.log( - "The ascending compare function in Table Editor View failed, error message: " + - e, - ); return 0; } }, /** - * dscSort - Descending compare function - * - * @param {number} currentCol The number of the column to sort - * @param {*} a One of two items to compare - * @param {*} b The second of two items to compare - * @return {number} A number indicating the order to place a vs b in the list. It it returns less than zero, then a will be placed before b in the list. + * Descending compare function + * @param {number} currentCol The number of the column to sort + * @param {*} a One of two items to compare + * @param {*} b The second of two items to compare + * @returns {number} A number indicating the order to place a vs + * b in the list. It it returns less than zero, then a will be placed + * before b in the list. */ - dscSort: function (currentCol, a, b) { + dscSort(currentCol, a, b) { try { - let _a = a[currentCol]; - let _b = b[currentCol]; - if (_a === "") return 1; - if (_b === "") return -1; + let valA = a[currentCol]; + let valB = b[currentCol]; + if (valIsEmpty(valA)) return -1; + if (valIsEmpty(valB)) return 1; // Check for strings and numbers - if (isNaN(_a) || isNaN(_b)) { - _a = _a.toUpperCase(); - _b = _b.toUpperCase(); - if (_a < _b) return 1; - if (_a > _b) return -1; - return 0; + if (typeof valA === "number" && typeof valB === "number") { + return valB - valA; } - return _b - _a; + valA = String(valA).toUpperCase(); + valB = String(valB).toUpperCase(); + if (valB < valA) return -1; + if (valB > valA) return 1; + return 0; } catch (e) { - console.log( - "The descending compare function in Table Editor View failed, error message: " + - e, - ); return 0; } }, /** - * convertToMarkdown - Returns the table data as markdown - * - * @return {string} The markdownified table as string + * Returns the table data as markdown + * @returns {string} The markdownified table as string */ - getMarkdown: function () { - try { - // Ensure there are at least two dashes below the table header, - // i.e. use | -- | not | - | - // Showdown requries this to avoid ambiguous markdown. - const minStringLength = function (s) { - l = s.length <= 1 ? 2 : s.length; - return l; - }; - // Get the current table data - var tableData = this.getData(); - // Remove the empty column that we use for row numbers first - if (this.hasEmptyCol1(tableData)) { - for (let i = 0; i <= tableData.length - 1; i++) { - tableData[i].splice(0, 1); - } + getMarkdown() { + // Ensure there are at least two dashes below the table header, i.e. use + // | -- | not | - | Showdown requries this to avoid ambiguous markdown. + const minStringLength = (s) => (s.length <= 1 ? 2 : s.length); + // Get the current table data + const tableData = this.getData(); + // Remove the empty column that we use for row numbers first + if (this.hasEmptyCol1(tableData)) { + for (let i = 0; i <= tableData.length - 1; i += 1) { + tableData[i].splice(0, 1); } - // Convert json data to markdown, for options see https://github.com/wooorm/markdown-table - // TODO: Add alignment information that we will store in view as an array - // Include in markdownTableFromJson() options like this - align: ['l', 'c', 'r'] - var markdown = markdownTableFromJson(tableData, { - stringLength: minStringLength, - }); - // Add a new line to the end - return markdown + "\n"; - } catch (e) { - console.log( - "Failed to convert json to markdown in the Table Editor View, error message: " + - e, - ); - return ""; } + // Convert json data to markdown, for options see + // https://github.com/wooorm/markdown-table TODO: Add alignment + // information that we will store in view as an array Include in + // markdownTableFromJson() options like this - align: ['l', 'c', 'r'] + const markdown = markdownTableFromJson(tableData, { + stringLength: minStringLength, + }); + // Add a new line to the end + return `${markdown}\n`; }, /** - * getJSONfromMarkdown - Converts a given markdown table string to JSON. - * + * Converts a given markdown table string to JSON. * @param {string} markdown description - * @return {Array} The markdown table as an array of arrays, where the header is the first array and each row is an array that follows. - */ - getJSONfromMarkdown: function (markdown) { - try { - parsedMarkdown = markdownTableToJson(markdown); - if (!parsedMarkdown) return; - // TODO: Add alignment information to the view, returned as parsedMarkdown.align - return parsedMarkdown.table; - } catch (e) { - console.log( - "Failed to parse markdown in the Table Editor View, error message: " + - e, - ); - return []; - } + * @returns {Array} The markdown table as an array of arrays, + * where the header is the first array and each row is an array that + * follows. + */ + getJSONfromMarkdown(markdown) { + const parsedMarkdown = markdownTableToJson(markdown); + if (!parsedMarkdown) return null; + // TODO: Add alignment information to the view, returned as + // parsedMarkdown.align + return parsedMarkdown.table; }, /** - * hasEmptyCol1 - Checks whether the first column is empty. - * - * @param {Object} data The table data in the form of an array of arrays - * @return {boolean} returns true if the first column is empty, false if at least one cell in the first column contains a value + * Checks whether the first column is empty. + * @param {object} data The table data in the form of an array of arrays + * @returns {boolean} returns true if the first column is empty, false + * if at least one cell in the first column contains a value */ - hasEmptyCol1: function (data) { - try { - var firstColEmpty = true; - // Check if the first item in each row is blank - for (let i = 0; i <= data.length - 1; i++) { - if (data[i][0] != "") { - firstColEmpty = false; - break; - } + hasEmptyCol1(data) { + let firstColEmpty = true; + // Check if the first item in each row is blank + for (let i = 0; i <= data.length - 1; i += 1) { + if (data[i][0] !== "" && data[i][0] !== undefined) { + firstColEmpty = false; + break; } - return firstColEmpty; - } catch (e) { - console.log( - "Failed to detect if there's an empty first column in the Table Editor View. Assuming the first column has data, but this could cause some issues. Error message: " + - e, - ); - return false; } + return firstColEmpty; }, /** - * closeDropdown - Close the dropdown menu if the user clicks outside of it - * + * Close the dropdown menu if the user clicks outside of it * @param {type} e The event that triggered this function */ - closeDropdown: function (e) { - try { - if (!e.target.matches(".dropbtn") || !e) { - var dropdowns = document.getElementsByClassName("dropdown-content"); - var i; - for (i = 0; i < dropdowns.length; i++) { - var openDropdown = dropdowns[i]; - if (openDropdown.classList.contains("show")) { - openDropdown.classList.remove("show"); - } + closeDropdown(e) { + if (!e.target.matches(".dropbtn") || !e) { + const dropdowns = document.getElementsByClassName("dropdown-content"); + let i; + for (i = 0; i < dropdowns.length; i += 1) { + const openDropdown = dropdowns[i]; + if (openDropdown.classList.contains("show")) { + openDropdown.classList.remove("show"); } } - } catch (e) { - console.log( - "Failed to close a dropdown menu in the Table Editor View, error message: " + - e, - ); } }, /** - * handleHeadersClick - Called when the table header is clicked. Depending - * on what is clicked, shows or hides the dropdown menus in the header, - * or calls one of the functions listed in the menu (e.g. delete column). - * + * Called when the table header is clicked. Depending on what is clicked, + * shows or hides the dropdown menus in the header, or calls one of the + * functions listed in the menu (e.g. delete column). * @param {event} e The event that triggered this function */ - handleHeadersClick: function (e) { - try { - var view = this; - if (e.target) { - var classes = e.target.classList; - - if (classes.contains("column-header-span")) { - // If the header element is clicked... - } else if (classes.contains("dropbtn")) { - const idArr = e.target.id.split("-"); - document - .getElementById(`col-dropdown-${idArr[2]}`) - .classList.toggle("show"); - } else if (classes.contains("col-dropdown-option")) { - const index = e.target.parentNode.id.split("-")[2]; - - if (classes.contains("col-insert-left")) { - view.addColumn(index, "left"); - } else if (classes.contains("col-insert-right")) { - view.addColumn(index, "right"); - } else if (classes.contains("col-sort")) { - view.sortColumn(index); - } else if (classes.contains("col-delete")) { - view.deleteColumn(index); - } + handleHeadersClick(e) { + const view = this; + if (e.target) { + const classes = e.target.classList; + + if (classes.contains("column-header-span")) { + // If the header element is clicked... + } else if (classes.contains("dropbtn")) { + const idArr = e.target.id.split("-"); + document + .getElementById(`col-dropdown-${idArr[2]}`) + .classList.toggle("show"); + } else if (classes.contains("col-dropdown-option")) { + const index = e.target.parentNode.id.split("-")[2]; + + if (classes.contains("col-insert-left")) { + view.addColumn(index, "left"); + } else if (classes.contains("col-insert-right")) { + view.addColumn(index, "right"); + } else if (classes.contains("col-sort")) { + view.sortColumn(index); + } else if (classes.contains("col-delete")) { + view.deleteColumn(index); } } - } catch (e) { - console.log( - "Failed to handle a click in the table header in the Table Editor View, error message: " + - e, - ); } }, /** - * handleHeadersClick - Called when the table body is clicked. Depending - * on what is clicked, shows or hides the dropdown menus in the body, - * or calls one of the functions listed in the menu (e.g. delete row). - * - * @param {type} e description - * @return {type} description + * Called when the table body is clicked. Depending on what is clicked, + * shows or hides the dropdown menus in the body, or calls one of the + * functions listed in the menu (e.g. delete row). + * @param {type} e The event that triggered this function */ - handleBodyClick: function (e) { - try { - var view = this; - if (e.target) { - var classes = e.target.classList; - - if (classes.contains("dropbtn")) { - const idArr = e.target.id.split("-"); - view.$el - .find(`#row-dropdown-${idArr[2]}`)[0] - .classList.toggle("show"); - } else if (classes.contains("row-dropdown-option")) { - const index = parseInt(e.target.parentNode.id.split("-"))[2]; - if (classes.contains("row-insert-top")) { - view.addRow(index, "top"); - } - if (classes.contains("row-insert-bottom")) { - view.addRow(index, "bottom"); - } - if (classes.contains("row-delete")) { - view.deleteRow(index); - } + handleBodyClick(e) { + const view = this; + if (e.target) { + const classes = e.target.classList; + + if (classes.contains("dropbtn")) { + const idArr = e.target.id.split("-"); + view.$el + .find(`#row-dropdown-${idArr[2]}`)[0] + .classList.toggle("show"); + } else if (classes.contains("row-dropdown-option")) { + const index = parseInt(e.target.parentNode.id.split("-"), 10)[2]; + if (classes.contains("row-insert-top")) { + view.addRow(index, "top"); + } + if (classes.contains("row-insert-bottom")) { + view.addRow(index, "bottom"); + } + if (classes.contains("row-delete")) { + view.deleteRow(index); } } - } catch (e) { - console.log( - "Failed to handle a click in the table body in the Table Editor View, error message: " + - e, - ); } }, },