diff --git a/src/js/collections/AccessPolicy.js b/src/js/collections/AccessPolicy.js index 93222e1b4..bddd8e9b2 100644 --- a/src/js/collections/AccessPolicy.js +++ b/src/js/collections/AccessPolicy.js @@ -8,6 +8,7 @@ define(["jquery", "underscore", "backbone", "models/AccessRule"], * @classdesc An AccessPolicy collection is a collection of AccessRules that specify * the permissions set on a DataONEObject * @classcategory Collections + * @extends Backbone.Collection */ var AccessPolicy = Backbone.Collection.extend( /** @lends AccessPolicy.prototype */ diff --git a/src/js/collections/QualityReport.js b/src/js/collections/QualityReport.js index e11edc8bd..3d8fd779a 100644 --- a/src/js/collections/QualityReport.js +++ b/src/js/collections/QualityReport.js @@ -13,7 +13,8 @@ define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", @extends Backbone.Collection @constructor */ - var QualityReport = Backbone.Collection.extend({ + var QualityReport = Backbone.Collection.extend( + /** @lends QualityReport.prototype */{ //The name of this type of collection type: "QualityReport", diff --git a/src/js/collections/Units.js b/src/js/collections/Units.js index 8bd6be434..9cd959622 100644 --- a/src/js/collections/Units.js +++ b/src/js/collections/Units.js @@ -7,6 +7,7 @@ define(["jquery", "underscore", "backbone", "x2js", "models/metadata/eml211/EMLU * @class Units * @classdesc Units represents the Ecological Metadata Language units list * @classcategory Collections + * @extends Backbone.Collection */ var Units = Backbone.Collection.extend( /** @lends Units.prototype */{ diff --git a/src/js/collections/UserGroup.js b/src/js/collections/UserGroup.js index f5acbf1c8..61ad4087f 100644 --- a/src/js/collections/UserGroup.js +++ b/src/js/collections/UserGroup.js @@ -6,7 +6,8 @@ define(['jquery', 'underscore', 'backbone', 'models/UserModel'], /** * @class UserGroup * @classdesc The collection of Users that represent a DataONE group - * @classcategory Collections + * @classcategory Collections + * @extends Backbone.Collection */ var UserGroup = Backbone.Collection.extend( /** @lends UserGroup.prototype */{ diff --git a/src/js/collections/bookkeeper/Quotas.js b/src/js/collections/bookkeeper/Quotas.js index 43bee5e4c..1a51d3985 100644 --- a/src/js/collections/bookkeeper/Quotas.js +++ b/src/js/collections/bookkeeper/Quotas.js @@ -10,6 +10,7 @@ define(["jquery", "underscore", "backbone", "models/bookkeeper/Quota"], * per unit to help with communicating limit warnings. * @classcategory Collections/Bookkeeper * @since 2.14.0 + * @extends Backbone.Collection */ var Quotas = Backbone.Collection.extend( /** @lends Quotas.prototype */ { diff --git a/src/js/collections/bookkeeper/Usages.js b/src/js/collections/bookkeeper/Usages.js index c03f24e65..05c36e53d 100644 --- a/src/js/collections/bookkeeper/Usages.js +++ b/src/js/collections/bookkeeper/Usages.js @@ -11,6 +11,7 @@ define(["jquery", "underscore", "backbone", "models/bookkeeper/Usage", "models/b * This collection also stores a reference to the Quota model associated with these Usages. * @classcategory Collections/Bookkeeper * @since 2.14.0 + * @extends Backbone.Collection */ var Usages = Backbone.Collection.extend( /** @lends Usages.prototype */ { diff --git a/src/js/collections/metadata/eml/EMLAnnotations.js b/src/js/collections/metadata/eml/EMLAnnotations.js index dd55d2872..758eb531d 100644 --- a/src/js/collections/metadata/eml/EMLAnnotations.js +++ b/src/js/collections/metadata/eml/EMLAnnotations.js @@ -7,6 +7,7 @@ function($, _, Backbone, EMLAnnotation){ * @classdesc A collection of EMLAnnotations. * @classcategory Collections/Metadata/EML * @since 2.19.0 + * @extends Backbone.Collection */ var EMLAnnotations = Backbone.Collection.extend( /** @lends EMLAnnotations.prototype */ diff --git a/src/js/collections/metadata/eml/EMLMissingValueCodes.js b/src/js/collections/metadata/eml/EMLMissingValueCodes.js index 714a0388f..80d637954 100644 --- a/src/js/collections/metadata/eml/EMLMissingValueCodes.js +++ b/src/js/collections/metadata/eml/EMLMissingValueCodes.js @@ -9,6 +9,7 @@ define(["backbone", "models/metadata/eml211/EMLMissingValueCode"], function ( * @classdesc A collection of EMLMissingValueCodes. * @classcategory Collections/Metadata/EML * @since 2.26.0 + * @extends Backbone.Collection */ var EMLMissingValueCodes = Backbone.Collection.extend( /** @lends EMLMissingValueCodes.prototype */ diff --git a/src/js/models/AccessRule.js b/src/js/models/AccessRule.js index f443b008d..823222b3b 100644 --- a/src/js/models/AccessRule.js +++ b/src/js/models/AccessRule.js @@ -7,6 +7,7 @@ define(['jquery', 'underscore', 'backbone'], * @class AccessRule * @classdesc A model that specifies a single permission set on a DataONEObject * @classcategory Models + * @extends Backbone.Model */ var AccessRule = Backbone.Model.extend( /** @lends AccessRule */ diff --git a/src/js/models/AppModel.js b/src/js/models/AppModel.js index 2e68b04d2..d241d6251 100644 --- a/src/js/models/AppModel.js +++ b/src/js/models/AppModel.js @@ -881,33 +881,33 @@ define(['jquery', 'underscore', 'backbone'], /** * The URL for the DataONE metadata assessment service * @type {string} - * @default "https://docker-ucsb-4.dataone.org:30443/quality" + * @default "https://api.dataone.org/quality" */ - mdqBaseUrl: "https://docker-ucsb-4.dataone.org:30443/quality", + mdqBaseUrl: "https://api.dataone.org/quality", /** * Metadata Assessment Suite IDs for the dataset assessment reports. * @type {string[]} - * @default ["FAIR-suite-0.3.1"] + * @default ["FAIR-suite-0.4.0"] */ - mdqSuiteIds: ["FAIR-suite-0.3.1"], + mdqSuiteIds: ["FAIR-suite-0.4.0"], /** * Metadata Assessment Suite labels for the dataset assessment reports * @type {string[]} - * @default ["FAIR Suite v0.3.1"] + * @default ["FAIR Suite v0.4.0"] */ - mdqSuiteLabels: ["FAIR Suite v0.3.1"], + mdqSuiteLabels: ["FAIR Suite v0.4.0"], /** * Metadata Assessment Suite IDs for the aggregated assessment charts * @type {string[]} - * @default ["FAIR-suite-0.3.1"] + * @default ["FAIR-suite-0.4.0"] */ - mdqAggregatedSuiteIds: ["FAIR-suite-0.3.1"], + mdqAggregatedSuiteIds: ["FAIR-suite-0.4.0"], /** * Metadata Assessment Suite labels for the aggregated assessment charts * @type {string[]} - * @default ["FAIR Suite v0.3.1"] + * @default ["FAIR Suite v0.4.0"] */ - mdqAggregatedSuiteLabels: ["FAIR Suite v0.3.1"], + mdqAggregatedSuiteLabels: ["FAIR Suite v0.4.0"], /** * The metadata formats for which to display metadata assessment reports * @type {string[]} diff --git a/src/js/models/LogsSearch.js b/src/js/models/LogsSearch.js index ed0d06364..7798f0ada 100644 --- a/src/js/models/LogsSearch.js +++ b/src/js/models/LogsSearch.js @@ -10,7 +10,8 @@ define(['jquery', 'underscore', 'backbone', 'models/Search'], * @deprecated * @classcategory Deprecated */ - var LogsSearch = SearchModel.extend({ + var LogsSearch = SearchModel.extend( + /** @lends LogsSearch.prototype */{ // This model contains all of the search/filter terms /* * Search filters can be either plain text or a filter object with the following options: diff --git a/src/js/models/LookupModel.js b/src/js/models/LookupModel.js index 2edc30aa8..805203a67 100644 --- a/src/js/models/LookupModel.js +++ b/src/js/models/LookupModel.js @@ -12,6 +12,7 @@ define(["jquery", "jqueryui", "underscore", "backbone"], function ( * @classdesc A utility model that contains functions for looking up values * from various services * @classcategory Models + * @extends Backbone.Model */ var LookupModel = Backbone.Model.extend( /** @lends LookupModel.prototype */ { diff --git a/src/js/models/Map.js b/src/js/models/Map.js index 748c96586..78aacc691 100644 --- a/src/js/models/Map.js +++ b/src/js/models/Map.js @@ -7,6 +7,7 @@ define(['jquery', 'underscore', 'backbone', 'gmaps'], * @class Map * @classdesc The Map Model represents all of the settings and options for a Google Map. * @classcategory Models + * @extends Backbone.Model */ var Map = Backbone.Model.extend( /** @lends Map.prototype */{ diff --git a/src/js/models/QualityCheckModel.js b/src/js/models/QualityCheckModel.js index 703ab4d2c..5a58988d1 100644 --- a/src/js/models/QualityCheckModel.js +++ b/src/js/models/QualityCheckModel.js @@ -1,7 +1,7 @@ /* global define */ "use strict"; -define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) { +define(['jquery', 'underscore', 'backbone'], function ($, _, Backbone) { /** * @class QualityCheck @@ -11,28 +11,30 @@ define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) { * future it may be used to request/fetch quality result for a single * quality check (and not an entire suite). * @classcategory Models + * @extends Backbone.Model */ - var QualityCheck = Backbone.Model.extend({ + var QualityCheck = Backbone.Model.extend( + /** @lends QualityCheck.prototype */{ - /* The default object format fields */ - defaults: function() { - return { - check: null, - output: null, - status: null, - timestamp: null - }; - }, + /* The default object format fields */ + defaults: function () { + return { + check: null, + output: null, + status: null, + timestamp: null + }; + }, - /* Constructs a new instance */ - initialize: function(attrs, options) { - }, + /* Constructs a new instance */ + initialize: function (attrs, options) { + }, - /* No op - Formats are read only */ - save: function() { - return false; - } - }); + /* No op - Formats are read only */ + save: function () { + return false; + } + }); return QualityCheck; }); diff --git a/src/js/models/SolrResult.js b/src/js/models/SolrResult.js index 9dfa962c8..3cd2168da 100644 --- a/src/js/models/SolrResult.js +++ b/src/js/models/SolrResult.js @@ -6,6 +6,7 @@ define(['jquery', 'underscore', 'backbone'], * @class SolrResult * @classdesc A single result from the Solr search service * @classcategory Models + * @extends Backbone.Model */ var SolrResult = Backbone.Model.extend( /** @lends SolrResult.prototype */{ diff --git a/src/js/models/bookkeeper/Quota.js b/src/js/models/bookkeeper/Quota.js index 0f034daaa..2f6dd8ae6 100644 --- a/src/js/models/bookkeeper/Quota.js +++ b/src/js/models/bookkeeper/Quota.js @@ -15,6 +15,7 @@ define(["jquery", * @name Quota * @since 2.14.0 * @constructor + * @extends Backbone.Model */ var Quota = Backbone.Model.extend( /** @lends Quota.prototype */ { diff --git a/src/js/models/bookkeeper/Subscription.js b/src/js/models/bookkeeper/Subscription.js index a3420e9a8..ea8679a8d 100644 --- a/src/js/models/bookkeeper/Subscription.js +++ b/src/js/models/bookkeeper/Subscription.js @@ -15,6 +15,7 @@ define(["jquery", * @name Subscription * @since 2.14.0 * @constructor + * @extends Backbone.Model */ var Subscription = Backbone.Model.extend( /** @lends Subscription.prototype */ { diff --git a/src/js/models/bookkeeper/Usage.js b/src/js/models/bookkeeper/Usage.js index 4d75b88a5..99f4eb23b 100644 --- a/src/js/models/bookkeeper/Usage.js +++ b/src/js/models/bookkeeper/Usage.js @@ -13,6 +13,7 @@ define(["jquery", * @class Usage * @name Usage * @since 2.14.0 + * @extends Backbone.Model * @constructor */ var Usage = Backbone.Model.extend( diff --git a/src/js/models/formats/ObjectFormat.js b/src/js/models/formats/ObjectFormat.js index 9e2d4d62b..e0da89125 100644 --- a/src/js/models/formats/ObjectFormat.js +++ b/src/js/models/formats/ObjectFormat.js @@ -8,6 +8,7 @@ define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) { * @classdesc An ObjectFormat represents a V2 DataONE object format * See https://purl.dataone.org/architecture/apis/Types2.html#v2_0.Types.ObjectFormat * @classcategory Models/Formats + * @extends Backbone.Model */ var ObjectFormat = Backbone.Model.extend( /** @lends ObjectFormat.prototype */{ diff --git a/src/js/models/geocoder/GeocodedLocation.js b/src/js/models/geocoder/GeocodedLocation.js index 50ef4d766..b3a0c8b58 100644 --- a/src/js/models/geocoder/GeocodedLocation.js +++ b/src/js/models/geocoder/GeocodedLocation.js @@ -10,36 +10,38 @@ define( * navigating to on a map. * @classcategory Models/Geocoder * @since 2.28.0 + * @extends Backbone.Model */ - const GeocodedLocation = Backbone.Model.extend({ - /** - * Overrides the default Backbone.Model.defaults() function to specify - * default attributes. - * @name GeocodedLocation#defaults - * @type {Object} - * @property {GeoBoundingBox} box Bounding box representing this location - * on a map. - * @property {string} displayName A name that can be displayed to the user - * representing this location. - */ - defaults() { - return { - box: new GeoBoundingBox, - displayName: '', - }; - }, + const GeocodedLocation = Backbone.Model.extend( + /** @lends GeocodedLocation.prototype */{ + /** + * Overrides the default Backbone.Model.defaults() function to specify + * default attributes. + * @name GeocodedLocation#defaults + * @type {Object} + * @property {GeoBoundingBox} box Bounding box representing this location + * on a map. + * @property {string} displayName A name that can be displayed to the user + * representing this location. + */ + defaults() { + return { + box: new GeoBoundingBox, + displayName: '', + }; + }, - /** - * @typedef {Object} GeocodedLocationOptions - * @property {Object} box An object representing a boundary around a - * location on a map. - * @property {string} displayName A display name for the location. - */ - initialize({ box: { north, south, east, west } = {}, displayName = '' } = {}) { - this.set('box', new GeoBoundingBox({ north, south, east, west })); - this.set('displayName', displayName); - }, - }); + /** + * @typedef {Object} GeocodedLocationOptions + * @property {Object} box An object representing a boundary around a + * location on a map. + * @property {string} displayName A display name for the location. + */ + initialize({ box: { north, south, east, west } = {}, displayName = '' } = {}) { + this.set('box', new GeoBoundingBox({ north, south, east, west })); + this.set('displayName', displayName); + }, + }); return GeocodedLocation; }); diff --git a/src/js/models/geocoder/Prediction.js b/src/js/models/geocoder/Prediction.js index e79a2cc7f..40a46bdbc 100644 --- a/src/js/models/geocoder/Prediction.js +++ b/src/js/models/geocoder/Prediction.js @@ -7,34 +7,36 @@ define(['backbone'], (Backbone) => { * autocompletion search. * @classcategory Models/Geocoder * @since 2.28.0 + * @extends Backbone.Model */ - const Prediction = Backbone.Model.extend({ - /** - * Overrides the default Backbone.Model.defaults() function to specify - * default attributes for the Map - * @name Prediction#defaults - * @type {Object} - * @property {string} description A user-friendly description of a Google - * Maps Place. - * @property {string} googleMapsPlaceId Unique identifier that can be - * geocoded by the Google Maps Geocoder API. - */ - defaults() { - return { description: '', googleMapsPlaceId: '' }; - }, + const Prediction = Backbone.Model.extend( + /** @lends Prediction.prototype */{ + /** + * Overrides the default Backbone.Model.defaults() function to specify + * default attributes for the Map + * @name Prediction#defaults + * @type {Object} + * @property {string} description A user-friendly description of a Google + * Maps Place. + * @property {string} googleMapsPlaceId Unique identifier that can be + * geocoded by the Google Maps Geocoder API. + */ + defaults() { + return { description: '', googleMapsPlaceId: '' }; + }, - /** - * @typedef {Object} PredictionOptions - * @property {string} description A string describing the location - * represented by the Prediction. - * @property {string} googleMapsPlaceId The place ID that is used to - * uniquely identify a place in Google Maps API. - */ - initialize({ description, googleMapsPlaceId, } = {}) { - this.set('description', description); - this.set('googleMapsPlaceId', googleMapsPlaceId); - }, - }); + /** + * @typedef {Object} PredictionOptions + * @property {string} description A string describing the location + * represented by the Prediction. + * @property {string} googleMapsPlaceId The place ID that is used to + * uniquely identify a place in Google Maps API. + */ + initialize({ description, googleMapsPlaceId, } = {}) { + this.set('description', description); + this.set('googleMapsPlaceId', googleMapsPlaceId); + }, + }); return Prediction; }); diff --git a/src/js/models/maps/viewfinder/ViewfinderModel.js b/src/js/models/maps/viewfinder/ViewfinderModel.js index 78ee68e3b..a64b4826e 100644 --- a/src/js/models/maps/viewfinder/ViewfinderModel.js +++ b/src/js/models/maps/viewfinder/ViewfinderModel.js @@ -17,224 +17,226 @@ define( /** * @class ViewfinderModel - * @classdes ViewfinderModel maintains state for the ViewfinderView and + * @classdesc ViewfinderModel maintains state for the ViewfinderView and * interfaces with location searching services. * @classcategory Models/Maps * @since 2.28.0 + * @extends Backbone.Model */ - const ViewfinderModel = Backbone.Model.extend({ - /** - * @name ViewfinderModel#defaults - * @type {Object} - * @property {string} error is the current error string to be displayed - * in the UI. - * @property {number} focusIndex is the index of the element - * in the list of predictions that shoudl be highlighted as focus. - * @property {Prediction[]} predictions a list of Predictions models that - * correspond to the user's search query. - * @property {string} query the user's search query. - * @since 2.28.0 - */ - defaults() { - return { - error: '', - focusIndex: -1, - predictions: [], - query: '', - zoomPresets: [], - } - }, - - /** - * @param {Map} mapModel is the Map model that the ViewfinderModel is - * managing for the corresponding ViewfinderView. - */ - initialize({ mapModel }) { - this.geocoderSearch = new GeocoderSearch(); - this.mapModel = mapModel; - this.allLayers = this.mapModel.getAllLayers(); - - this.set('zoomPresets', mapModel.get('zoomPresetsCollection')?.models || []); - }, - - /** - * Get autocompletion predictions from the GeocoderSearch model. - * @param {string} rawQuery is the user's search query with spaces. - */ - async autocompleteSearch(rawQuery) { - const query = rawQuery.trim(); - if (this.get('query') === query) { - return; - } else if (!query) { - this.set({ error: '', predictions: [], query: '', focusIndex: -1, }); - return; - } else if (GeoPoint.couldBeLatLong(query)) { - this.set({ predictions: [], query: '', focusIndex: -1, }); - return; - } - - // Unset error so the error will fire a change event even if it is the - // same error as already exists. - this.unset('error', { silent: true }); - - try { - // User is looking for autocompletions. - const predictions = await this.geocoderSearch.autocomplete(query); - const error = predictions.length === 0 ? NO_RESULTS_MESSAGE : ''; - this.set({ error, focusIndex: -1, predictions, query, }); - } catch (e) { - if (e.code === 'REQUEST_DENIED' && e.endpoint === 'PLACES_AUTOCOMPLETE') { - this.set({ - error: PLACES_API_ERROR, - focusIndex: -1, - predictions: [], - query, - }); - } else { - this.set({ - error: NO_RESULTS_MESSAGE, - focusIndex: -1, - predictions: [], - query, - }); + const ViewfinderModel = Backbone.Model.extend( + /** @lends ViewfinderModel.prototype */{ + /** + * @name ViewfinderModel#defaults + * @type {Object} + * @property {string} error is the current error string to be displayed + * in the UI. + * @property {number} focusIndex is the index of the element + * in the list of predictions that shoudl be highlighted as focus. + * @property {Prediction[]} predictions a list of Predictions models that + * correspond to the user's search query. + * @property {string} query the user's search query. + * @since 2.28.0 + */ + defaults() { + return { + error: '', + focusIndex: -1, + predictions: [], + query: '', + zoomPresets: [], } - } - }, - - /** - * Decrement the focused index with a minimum value of 0. This corresponds - * to an ArrowUp key down event. - * Note: An ArrowUp key press while the current index is -1 will - * result in highlighting the first element in the list. - */ - decrementFocusIndex() { - const currentIndex = this.get('focusIndex'); - this.set('focusIndex', Math.max(0, currentIndex - 1)); - }, - - /** - * Increment the focused index with a maximum value of the last value in - * the list. This corresponds to an ArrowDown key down event. - */ - incrementFocusIndex() { - const currentIndex = this.get('focusIndex'); - this.set( - 'focusIndex', - Math.min(currentIndex + 1, this.get('predictions').length - 1) - ); - }, - - /** - * Reset the focused index back to the initial value so that no element - * in the UI is highlighted. - */ - resetFocusIndex() { - this.set('focusIndex', -1); - }, - - /** - * Navigate to the GeocodedLocation. - * @param {GeocodedLocation} geocoding is the location that corresponds - * to the the selected prediction. - */ - goToLocation(geocoding) { - if (!geocoding) return; - - const coords = geocoding.get('box').getCoords(); - this.mapModel.zoomTo({ - destination: Cesium.Rectangle.fromDegrees( - coords.west, - coords.south, - coords.east, - coords.north, - ) - }); - }, - - /** - * Select a ZoomPresetModel from the list of presets and navigate there. - * This function hides all layers that are not to be visible according to - * the ZoomPresetModel configuration. - * @param {ZoomPresetModel} preset A user selected preset for which to - * enable layers and navigate. - */ - selectZoomPreset(preset) { - const enabledLayerIds = preset.get('enabledLayerIds'); - for (const layer of this.allLayers) { - const isVisible = enabledLayerIds.includes(layer.get('layerId')); - // Show or hide the layer according to the preset. - layer.set('visible', isVisible); - } - - this.mapModel.zoomTo(preset.get('geoPoint')); - }, - - /** - * Select a prediction from the list of predictions and navigate there. - * @param {Prediction} prediction is the user-selected Prediction that - * needs to be geocoded and navigated to. - */ - async selectPrediction(prediction) { - if (!prediction) return; - - try { - const geocodings = await this.geocoderSearch.geocode(prediction); - - if (geocodings.length === 0) { - this.set('error', NO_RESULTS_MESSAGE) + }, + + /** + * @param {Map} mapModel is the Map model that the ViewfinderModel is + * managing for the corresponding ViewfinderView. + */ + initialize({ mapModel }) { + this.geocoderSearch = new GeocoderSearch(); + this.mapModel = mapModel; + this.allLayers = this.mapModel.getAllLayers(); + + this.set('zoomPresets', mapModel.get('zoomPresetsCollection')?.models || []); + }, + + /** + * Get autocompletion predictions from the GeocoderSearch model. + * @param {string} rawQuery is the user's search query with spaces. + */ + async autocompleteSearch(rawQuery) { + const query = rawQuery.trim(); + if (this.get('query') === query) { return; + } else if (!query) { + this.set({ error: '', predictions: [], query: '', focusIndex: -1, }); + return; + } else if (GeoPoint.couldBeLatLong(query)) { + this.set({ predictions: [], query: '', focusIndex: -1, }); + return; + } + + // Unset error so the error will fire a change event even if it is the + // same error as already exists. + this.unset('error', { silent: true }); + + try { + // User is looking for autocompletions. + const predictions = await this.geocoderSearch.autocomplete(query); + const error = predictions.length === 0 ? NO_RESULTS_MESSAGE : ''; + this.set({ error, focusIndex: -1, predictions, query, }); + } catch (e) { + if (e.code === 'REQUEST_DENIED' && e.endpoint === 'PLACES_AUTOCOMPLETE') { + this.set({ + error: PLACES_API_ERROR, + focusIndex: -1, + predictions: [], + query, + }); + } else { + this.set({ + error: NO_RESULTS_MESSAGE, + focusIndex: -1, + predictions: [], + query, + }); + } + } + }, + + /** + * Decrement the focused index with a minimum value of 0. This corresponds + * to an ArrowUp key down event. + * Note: An ArrowUp key press while the current index is -1 will + * result in highlighting the first element in the list. + */ + decrementFocusIndex() { + const currentIndex = this.get('focusIndex'); + this.set('focusIndex', Math.max(0, currentIndex - 1)); + }, + + /** + * Increment the focused index with a maximum value of the last value in + * the list. This corresponds to an ArrowDown key down event. + */ + incrementFocusIndex() { + const currentIndex = this.get('focusIndex'); + this.set( + 'focusIndex', + Math.min(currentIndex + 1, this.get('predictions').length - 1) + ); + }, + + /** + * Reset the focused index back to the initial value so that no element + * in the UI is highlighted. + */ + resetFocusIndex() { + this.set('focusIndex', -1); + }, + + /** + * Navigate to the GeocodedLocation. + * @param {GeocodedLocation} geocoding is the location that corresponds + * to the the selected prediction. + */ + goToLocation(geocoding) { + if (!geocoding) return; + + const coords = geocoding.get('box').getCoords(); + this.mapModel.zoomTo({ + destination: Cesium.Rectangle.fromDegrees( + coords.west, + coords.south, + coords.east, + coords.north, + ) + }); + }, + + /** + * Select a ZoomPresetModel from the list of presets and navigate there. + * This function hides all layers that are not to be visible according to + * the ZoomPresetModel configuration. + * @param {ZoomPresetModel} preset A user selected preset for which to + * enable layers and navigate. + */ + selectZoomPreset(preset) { + const enabledLayerIds = preset.get('enabledLayerIds'); + for (const layer of this.allLayers) { + const isVisible = enabledLayerIds.includes(layer.get('layerId')); + // Show or hide the layer according to the preset. + layer.set('visible', isVisible); } - this.trigger('selection-made', prediction.get('description')); - this.goToLocation(geocodings[0]); - } catch (e) { - if (e.code === 'REQUEST_DENIED' && e.endpoint === 'GEOCODER_GEOCODE') { - this.set({ error: GEOCODING_API_ERROR, focusIndex: -1, predictions: [] }); - } else { - this.set('error', NO_RESULTS_MESSAGE) + this.mapModel.zoomTo(preset.get('geoPoint')); + }, + + /** + * Select a prediction from the list of predictions and navigate there. + * @param {Prediction} prediction is the user-selected Prediction that + * needs to be geocoded and navigated to. + */ + async selectPrediction(prediction) { + if (!prediction) return; + + try { + const geocodings = await this.geocoderSearch.geocode(prediction); + + if (geocodings.length === 0) { + this.set('error', NO_RESULTS_MESSAGE) + return; + } + + this.trigger('selection-made', prediction.get('description')); + this.goToLocation(geocodings[0]); + } catch (e) { + if (e.code === 'REQUEST_DENIED' && e.endpoint === 'GEOCODER_GEOCODE') { + this.set({ error: GEOCODING_API_ERROR, focusIndex: -1, predictions: [] }); + } else { + this.set('error', NO_RESULTS_MESSAGE) + } } - } - }, - - /** - * Event handler for Backbone.View configuration that is called whenever - * the user clicks the search button or hits the Enter key. - * @param {string} value is the query string. - */ - async search(value) { - if (!value) return; - - // This is not a lat,long value, so geocode the prediction instead. - if (!GeoPoint.couldBeLatLong(value)) { - const focusedIndex = Math.max(0, this.get("focusIndex")); - this.selectPrediction(this.get('predictions')[focusedIndex]); - return; - } - - // Unset error so the error will fire a change event even if it is the - // same error as already exists. - this.unset('error', { silent: true }); - - try { - const geoPoint = new GeoPoint(value, { parse: true }); - geoPoint.set("height", 10000 /* meters */); - if (geoPoint.isValid()) { - this.set('error', ''); - this.mapModel.zoomTo(geoPoint); + }, + + /** + * Event handler for Backbone.View configuration that is called whenever + * the user clicks the search button or hits the Enter key. + * @param {string} value is the query string. + */ + async search(value) { + if (!value) return; + + // This is not a lat,long value, so geocode the prediction instead. + if (!GeoPoint.couldBeLatLong(value)) { + const focusedIndex = Math.max(0, this.get("focusIndex")); + this.selectPrediction(this.get('predictions')[focusedIndex]); return; } - const errors = geoPoint.validationError; - if (errors.latitude) { - this.set('error', errors.latitude); - } else if (errors.longitude) { - this.set('error', errors.longitude); + // Unset error so the error will fire a change event even if it is the + // same error as already exists. + this.unset('error', { silent: true }); + + try { + const geoPoint = new GeoPoint(value, { parse: true }); + geoPoint.set("height", 10000 /* meters */); + if (geoPoint.isValid()) { + this.set('error', ''); + this.mapModel.zoomTo(geoPoint); + return; + } + + const errors = geoPoint.validationError; + if (errors.latitude) { + this.set('error', errors.latitude); + } else if (errors.longitude) { + this.set('error', errors.longitude); + } + } catch (e) { + this.set('error', e.message); } - } catch (e) { - this.set('error', e.message); - } - }, - }); + }, + }); return ViewfinderModel; }); \ No newline at end of file diff --git a/src/js/models/metadata/eml/EMLSpecializedText.js b/src/js/models/metadata/eml/EMLSpecializedText.js index 67604e3c3..0c46a92b5 100644 --- a/src/js/models/metadata/eml/EMLSpecializedText.js +++ b/src/js/models/metadata/eml/EMLSpecializedText.js @@ -11,6 +11,7 @@ define(['jquery', 'underscore', 'backbone', 'models/metadata/eml220/EMLText'], * would be serialized in the EML XML as a section title or markdown header. * @classcategory Models/Metadata/EML * @since 2.19.0 + * @extends EMLText */ var EMLSpecializedText = EMLText.extend( /** @lends EMLSpecializedText.prototype */{ diff --git a/src/js/models/metadata/eml211/EMLAnnotation.js b/src/js/models/metadata/eml211/EMLAnnotation.js index ac0427aa5..78e7560c9 100644 --- a/src/js/models/metadata/eml211/EMLAnnotation.js +++ b/src/js/models/metadata/eml211/EMLAnnotation.js @@ -6,6 +6,7 @@ define(["jquery", "underscore", "backbone"], * @classdesc Stores EML SemanticAnnotation elements. * @classcategory Models/Metadata/EML211 * @see https://eml.ecoinformatics.org/eml-2.2.0/eml-semantics.xsd + * @extends Backbone.Model */ var EMLAnnotation = Backbone.Model.extend( /** @lends EMLAnnotation.prototype */{ diff --git a/src/js/models/metadata/eml211/EMLDataTable.js b/src/js/models/metadata/eml211/EMLDataTable.js index 190c1c28f..a240d4be1 100644 --- a/src/js/models/metadata/eml211/EMLDataTable.js +++ b/src/js/models/metadata/eml211/EMLDataTable.js @@ -7,6 +7,7 @@ define(["jquery", "underscore", "backbone", "models/metadata/eml211/EMLEntity"], * with the EML dataTable module. * @classcategory Models/Metadata/EML211 * @see https://eml.ecoinformatics.org/schema/eml-datatable_xsd + * @extends EMLEntity */ var EMLDataTable = EMLEntity.extend( /** @lends EMLDataTable.prototype */{ diff --git a/src/js/models/metadata/eml211/EMLDateTimeDomain.js b/src/js/models/metadata/eml211/EMLDateTimeDomain.js index 858d409d6..e503a285f 100644 --- a/src/js/models/metadata/eml211/EMLDateTimeDomain.js +++ b/src/js/models/metadata/eml211/EMLDateTimeDomain.js @@ -7,6 +7,7 @@ define(["jquery", "underscore", "backbone", * attribute. * @classcategory Models/Metadata/EML211 * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_AttributeType_measurementScale_dateTime + * @extends Backbone.Model */ var EMLDateTimeDomain = Backbone.Model.extend( /** @lends EMLDateTimeDomain.prototype */{ diff --git a/src/js/models/metadata/eml211/EMLDistribution.js b/src/js/models/metadata/eml211/EMLDistribution.js index fe0e17e28..29ccf2753 100644 --- a/src/js/models/metadata/eml211/EMLDistribution.js +++ b/src/js/models/metadata/eml211/EMLDistribution.js @@ -15,315 +15,316 @@ define(["jquery", "underscore", "backbone", "models/DataONEObject"], function ( * @extends Backbone.Model * @constructor */ - var EMLDistribution = Backbone.Model.extend({ - /** - * Default values for an EML 211 Distribution model. This is essentially a - * flattened version of the EML 2.1.1 DistributionType, including nodes and - * node attributes. Not all nodes are supported by this model yet. - * @type {Object} - * @property {string} type - The name of the top-level XML element that this - * model represents (distribution) - * @property {string} objectXML - The XML string representation of the - * distribution - * @property {Element} objectDOM - The DOM representation of the - * distribution - * @property {string} mediumName - The name of the medium on which the - * offline distribution is stored - * @property {string} mediumVolume - The volume number of the medium on - * which the offline distribution is stored - * @property {string} mediumFormat - The format of the medium on which the - * offline distribution is stored - * @property {string} mediumNote - A note about the medium on which the - * offline distribution is stored - * @property {string} url - The URL of the online distribution - * @property {string} urlFunction - The purpose of the URL. May be either - * "information" or "download". - * @property {string} onlineDescription - A description of the online - * distribution - * @property {EML211} parentModel - The parent model of this distribution - * model - */ - defaults: { - type: "distribution", - objectXML: null, - objectDOM: null, - mediumName: null, - mediumVolume: null, - mediumFormat: null, - mediumNote: null, - url: null, - urlFunction: null, - onlineDescription: null, - parentModel: null, - }, - - /** - * The direct children of the node that can have values, and - * that are supported by this model. "inline" is not supported yet. A - * distribution may have ONE of these nodes. - * @type {string[]} - * @since 2.26.0 - */ - distLocations: ["offline", "online"], - - /** - * lower-case EML node names that belong within the node. These - * must be in the correct order. - * @type {string[]} - * @since 2.26.0 - */ - offlineNodes: ["mediumname", "mediumvolume", "mediumformat", "mediumnote"], - - /** - * lower-case EML node names that belong within the node. These - * must be in the correct order. - * @type {string[]} - * @since 2.26.0 - */ - onlineNodes: ["url"], - - /** - * the allowed values for the urlFunction attribute - * @type {string[]} - * @since 2.26.0 - */ - urlFunctionTypes: ["information", "download"], - - /** - * Initializes this EMLDistribution object - * @param {Object} options - A literal object with options to pass to the - * model - */ - initialize: function (attributes, options) { - const nodeAttr = Object.values(this.nodeNameMap()); - this.listenTo( - this, - "change:" + nodeAttr.join(" change:"), - this.trickleUpChange - ); - }, - - /* - * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased - * EML node names (valid in EML). Used during parse() and serialize() - */ - nodeNameMap: function () { - return { - authsystem: "authSystem", - connectiondefinition: "connectionDefinition", - mediumdensity: "mediumDensity", - mediumdensityunits: "mediumDensityUnits", - mediumformat: "mediumFormat", - mediumname: "mediumName", - mediumnote: "mediumNote", - mediumvolume: "mediumVolume", - url: "url", - }; - }, - - /** - * Parses the given XML node or object and sets the model's attributes - * @param {Object} attributes - the attributes passed in when the model is - * instantiated. Should include objectDOM or objectXML to be parsed. - */ - parse: function (attributes) { - if (!attributes) attributes = {}; - const objectDOM = attributes.objectDOM || attributes.objectXML; - if (!objectDOM) return attributes; - const $objectDOM = $(objectDOM); - - const nodeNameMap = this.nodeNameMap(); - this.distLocations.forEach((distLocation) => { - const location = $objectDOM.find(distLocation); - if (location.length) { - this[`${distLocation}Nodes`].forEach((nodeName) => { - const value = location.children(nodeName)?.text()?.trim(); - if (value.length) { - attributes[nodeNameMap[nodeName]] = value; - } - }); + var EMLDistribution = Backbone.Model.extend( + /** @lends EMLDistribution.prototype */{ + /** + * Default values for an EML 211 Distribution model. This is essentially a + * flattened version of the EML 2.1.1 DistributionType, including nodes and + * node attributes. Not all nodes are supported by this model yet. + * @type {Object} + * @property {string} type - The name of the top-level XML element that this + * model represents (distribution) + * @property {string} objectXML - The XML string representation of the + * distribution + * @property {Element} objectDOM - The DOM representation of the + * distribution + * @property {string} mediumName - The name of the medium on which the + * offline distribution is stored + * @property {string} mediumVolume - The volume number of the medium on + * which the offline distribution is stored + * @property {string} mediumFormat - The format of the medium on which the + * offline distribution is stored + * @property {string} mediumNote - A note about the medium on which the + * offline distribution is stored + * @property {string} url - The URL of the online distribution + * @property {string} urlFunction - The purpose of the URL. May be either + * "information" or "download". + * @property {string} onlineDescription - A description of the online + * distribution + * @property {EML211} parentModel - The parent model of this distribution + * model + */ + defaults: { + type: "distribution", + objectXML: null, + objectDOM: null, + mediumName: null, + mediumVolume: null, + mediumFormat: null, + mediumNote: null, + url: null, + urlFunction: null, + onlineDescription: null, + parentModel: null, + }, + + /** + * The direct children of the node that can have values, and + * that are supported by this model. "inline" is not supported yet. A + * distribution may have ONE of these nodes. + * @type {string[]} + * @since 2.26.0 + */ + distLocations: ["offline", "online"], + + /** + * lower-case EML node names that belong within the node. These + * must be in the correct order. + * @type {string[]} + * @since 2.26.0 + */ + offlineNodes: ["mediumname", "mediumvolume", "mediumformat", "mediumnote"], + + /** + * lower-case EML node names that belong within the node. These + * must be in the correct order. + * @type {string[]} + * @since 2.26.0 + */ + onlineNodes: ["url"], + + /** + * the allowed values for the urlFunction attribute + * @type {string[]} + * @since 2.26.0 + */ + urlFunctionTypes: ["information", "download"], + + /** + * Initializes this EMLDistribution object + * @param {Object} options - A literal object with options to pass to the + * model + */ + initialize: function (attributes, options) { + const nodeAttr = Object.values(this.nodeNameMap()); + this.listenTo( + this, + "change:" + nodeAttr.join(" change:"), + this.trickleUpChange + ); + }, + + /* + * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased + * EML node names (valid in EML). Used during parse() and serialize() + */ + nodeNameMap: function () { + return { + authsystem: "authSystem", + connectiondefinition: "connectionDefinition", + mediumdensity: "mediumDensity", + mediumdensityunits: "mediumDensityUnits", + mediumformat: "mediumFormat", + mediumname: "mediumName", + mediumnote: "mediumNote", + mediumvolume: "mediumVolume", + url: "url", + }; + }, + + /** + * Parses the given XML node or object and sets the model's attributes + * @param {Object} attributes - the attributes passed in when the model is + * instantiated. Should include objectDOM or objectXML to be parsed. + */ + parse: function (attributes) { + if (!attributes) attributes = {}; + const objectDOM = attributes.objectDOM || attributes.objectXML; + if (!objectDOM) return attributes; + const $objectDOM = $(objectDOM); + + const nodeNameMap = this.nodeNameMap(); + this.distLocations.forEach((distLocation) => { + const location = $objectDOM.find(distLocation); + if (location.length) { + this[`${distLocation}Nodes`].forEach((nodeName) => { + const value = location.children(nodeName)?.text()?.trim(); + if (value.length) { + attributes[nodeNameMap[nodeName]] = value; + } + }); + } + }); + + // Check for a urlFunction attribute if there is a url node + const url = $objectDOM.find("url"); + if (url.length) { + attributes.urlFunction = url.attr("function") || null; } - }); - - // Check for a urlFunction attribute if there is a url node - const url = $objectDOM.find("url"); - if (url.length) { - attributes.urlFunction = url.attr("function") || null; - } - - return attributes; - }, - - /** - * Returns the XML string representation of this model - * @returns {string} - */ - serialize: function () { - const objectDOM = this.updateDOM(); - const xmlString = objectDOM.outerHTML; - - // Camel-case the XML - xmlString = this.formatXML(xmlString); - - return xmlString; - }, - - /** - * Check if the model has values for the given distribution location. - * @param {string} location - one of the names of the direct children of the - * node, i.e. any of the values in this.distLocations. - * @returns {boolean} - true if the model has values for the given location, - * false otherwise. - * @since 2.26.0 - */ - hasValuesForDistributionLocation(location) { - const nodeNameMap = this.nodeNameMap(); - return this[`${location}Nodes`].some((nodeName) => { - return this.get(nodeNameMap[nodeName]); - }); - }, - - /* - * Makes a copy of the original XML DOM and updates it with the new values - * from the model. - */ - updateDOM: function () { - const objectDOM = - this.get("objectDOM") || document.createElement(this.get("type")); - const $objectDOM = $(objectDOM); - - // Remove empty (zero-length or whitespace-only) nodes - $objectDOM - .find("*") - .filter(function () { - return !$.trim($(this).text()); - }) - .remove(); - - const nodeNameMap = this.nodeNameMap(); - - // Determine if this is an online, offline, or inline distribution - const distLocation = this.distLocations.find((location) => { - return this.hasValuesForDistributionLocation(location); - }); - - // Remove all other distribution locations - this.distLocations.forEach((location) => { - if (location !== distLocation) { - $objectDOM.find(location).remove(); + + return attributes; + }, + + /** + * Returns the XML string representation of this model + * @returns {string} + */ + serialize: function () { + const objectDOM = this.updateDOM(); + const xmlString = objectDOM.outerHTML; + + // Camel-case the XML + xmlString = this.formatXML(xmlString); + + return xmlString; + }, + + /** + * Check if the model has values for the given distribution location. + * @param {string} location - one of the names of the direct children of the + * node, i.e. any of the values in this.distLocations. + * @returns {boolean} - true if the model has values for the given location, + * false otherwise. + * @since 2.26.0 + */ + hasValuesForDistributionLocation(location) { + const nodeNameMap = this.nodeNameMap(); + return this[`${location}Nodes`].some((nodeName) => { + return this.get(nodeNameMap[nodeName]); + }); + }, + + /* + * Makes a copy of the original XML DOM and updates it with the new values + * from the model. + */ + updateDOM: function () { + const objectDOM = + this.get("objectDOM") || document.createElement(this.get("type")); + const $objectDOM = $(objectDOM); + + // Remove empty (zero-length or whitespace-only) nodes + $objectDOM + .find("*") + .filter(function () { + return !$.trim($(this).text()); + }) + .remove(); + + const nodeNameMap = this.nodeNameMap(); + + // Determine if this is an online, offline, or inline distribution + const distLocation = this.distLocations.find((location) => { + return this.hasValuesForDistributionLocation(location); + }); + + // Remove all other distribution locations + this.distLocations.forEach((location) => { + if (location !== distLocation) { + $objectDOM.find(location).remove(); + } + }); + + // If there is no distribution location, return the DOM + if (!distLocation) return objectDOM; + + // Add the distribution location if it doesn't exist + if (!$objectDOM.find(distLocation).length) { + $objectDOM.append(`<${distLocation}>`); } - }); - - // If there is no distribution location, return the DOM - if (!distLocation) return objectDOM; - - // Add the distribution location if it doesn't exist - if (!$objectDOM.find(distLocation).length) { - $objectDOM.append(`<${distLocation}>`); - } - - // For each node in the distribution location, add the value from the - // model. If the model value is empty, remove the node. Make sure that we - // don't replace any existing nodes, since not all nodes are supported by - // this model yet. We also need to ensure that the nodes are in the - // correct order. - this[`${distLocation}Nodes`].forEach((nodeName) => { - const nodeValue = this.get(nodeNameMap[nodeName]); - if (nodeValue) { - const node = $objectDOM.find(`${distLocation} > ${nodeName}`); - if (node.length) { - node.text(nodeValue); - } else { - const newNode = $(`<${nodeName}>${nodeValue}`); - const position = this.getEMLPosition(objectDOM, nodeName); - if (position) { - newNode.insertAfter(position); + + // For each node in the distribution location, add the value from the + // model. If the model value is empty, remove the node. Make sure that we + // don't replace any existing nodes, since not all nodes are supported by + // this model yet. We also need to ensure that the nodes are in the + // correct order. + this[`${distLocation}Nodes`].forEach((nodeName) => { + const nodeValue = this.get(nodeNameMap[nodeName]); + if (nodeValue) { + const node = $objectDOM.find(`${distLocation} > ${nodeName}`); + if (node.length) { + node.text(nodeValue); } else { - $objectDOM.children(distLocation).append(newNode); + const newNode = $(`<${nodeName}>${nodeValue}`); + const position = this.getEMLPosition(objectDOM, nodeName); + if (position) { + newNode.insertAfter(position); + } else { + $objectDOM.children(distLocation).append(newNode); + } } + } else { + $objectDOM.find(`${distLocation} > ${nodeName}`).remove(); + } + }); + + // Add the urlFunction attribute if one is set in the model. Remove it if + // it's not set. + const url = $objectDOM.find("url") + if (url) { + const urlFunction = this.get("urlFunction"); + if (urlFunction) { + url.attr("function", urlFunction); + } else { + url.removeAttr("function"); } - } else { - $objectDOM.find(`${distLocation} > ${nodeName}`).remove(); - } - }); - - // Add the urlFunction attribute if one is set in the model. Remove it if - // it's not set. - const url = $objectDOM.find("url") - if (url) { - const urlFunction = this.get("urlFunction"); - if (urlFunction) { - url.attr("function", urlFunction); - } else { - url.removeAttr("function"); } - } - - - return objectDOM; - }, - - /* - * Returns the node in the object DOM that the given node type should be - * inserted after. @param {string} nodeName - The name of the node to find - * the position for @return {jQuery} - The jQuery object of the node that - * the given node should be inserted after, or false if the node is not - * supported by this model. @since 2.26.0 - */ - getEMLPosition: function (objectDOM, nodeName) { - // If this is a top level node, return false since it should be inserted - // within the node, and there must only be one. - if (this.distLocations.includes(nodeName)) return false; - - // Handle according to whether it's an online or offline node - const nodeNameMap = this.nodeNameMap(); - this.distLocations.forEach((distLocation) => { - const nodeOrder = this[`${distLocation}Nodes`]; - const siblingNodes = $(objectDOM).find(distLocation).children(); - let position = nodeOrder.indexOf(nodeName); - if (position > -1) { - // Go through each node in the node list and find the position where - // this node will be inserted after - for (var i = position - 1; i >= 0; i--) { - const checkNode = siblingNodes.filter(nodeOrder[i]); - if (checkNode.length) { - return checkNode.last(); + + + return objectDOM; + }, + + /* + * Returns the node in the object DOM that the given node type should be + * inserted after. @param {string} nodeName - The name of the node to find + * the position for @return {jQuery} - The jQuery object of the node that + * the given node should be inserted after, or false if the node is not + * supported by this model. @since 2.26.0 + */ + getEMLPosition: function (objectDOM, nodeName) { + // If this is a top level node, return false since it should be inserted + // within the node, and there must only be one. + if (this.distLocations.includes(nodeName)) return false; + + // Handle according to whether it's an online or offline node + const nodeNameMap = this.nodeNameMap(); + this.distLocations.forEach((distLocation) => { + const nodeOrder = this[`${distLocation}Nodes`]; + const siblingNodes = $(objectDOM).find(distLocation).children(); + let position = nodeOrder.indexOf(nodeName); + if (position > -1) { + // Go through each node in the node list and find the position where + // this node will be inserted after + for (var i = position - 1; i >= 0; i--) { + const checkNode = siblingNodes.filter(nodeOrder[i]); + if (checkNode.length) { + return checkNode.last(); + } } } + }); + + // If we get here, the node is not supported by this model + return false; + }, + + /* + * Climbs up the model hierarchy until it finds the EML model + * + * @return {EML211 or false} - Returns the EML 211 Model or false if not + * found + */ + getParentEML: function () { + var emlModel = this.get("parentModel"), + tries = 0; + + while (emlModel.type !== "EML" && tries < 6) { + emlModel = emlModel.get("parentModel"); + tries++; } - }); - - // If we get here, the node is not supported by this model - return false; - }, - - /* - * Climbs up the model hierarchy until it finds the EML model - * - * @return {EML211 or false} - Returns the EML 211 Model or false if not - * found - */ - getParentEML: function () { - var emlModel = this.get("parentModel"), - tries = 0; - - while (emlModel.type !== "EML" && tries < 6) { - emlModel = emlModel.get("parentModel"); - tries++; - } - - if (emlModel && emlModel.type == "EML") return emlModel; - else return false; - }, - - trickleUpChange: function () { - MetacatUI.rootDataPackage?.packageModel?.set("changed", true); - }, - - formatXML: function (xmlString) { - return DataONEObject.prototype.formatXML.call(this, xmlString); - }, - }); + + if (emlModel && emlModel.type == "EML") return emlModel; + else return false; + }, + + trickleUpChange: function () { + MetacatUI.rootDataPackage?.packageModel?.set("changed", true); + }, + + formatXML: function (xmlString) { + return DataONEObject.prototype.formatXML.call(this, xmlString); + }, + }); return EMLDistribution; }); diff --git a/src/js/models/metadata/eml211/EMLEntity.js b/src/js/models/metadata/eml211/EMLEntity.js index cdceba481..2d1182b30 100644 --- a/src/js/models/metadata/eml211/EMLEntity.js +++ b/src/js/models/metadata/eml211/EMLEntity.js @@ -10,6 +10,7 @@ define(["jquery", "underscore", "backbone", "uuid", "models/DataONEObject", * spatialRaster, and storedProcedure * @classcategory Models/Metadata/EML211 * @see https://eml.ecoinformatics.org/schema/eml-entity_xsd + * @extends Backbone.Model */ var EMLEntity = Backbone.Model.extend( /** @lends EMLEntity.prototype */{ diff --git a/src/js/models/metadata/eml211/EMLMeasurementScale.js b/src/js/models/metadata/eml211/EMLMeasurementScale.js index 3d523f6dd..a77db84fa 100644 --- a/src/js/models/metadata/eml211/EMLMeasurementScale.js +++ b/src/js/models/metadata/eml211/EMLMeasurementScale.js @@ -11,6 +11,7 @@ define(["jquery", "underscore", "backbone", * EMLNumericDomain, or EMLDateTimeDomain, depending on the * domain name found in the given measurementScaleXML * @classcategory Models/Metadata/EML211 + * @extends Backbone.Model */ var EMLMeasurementScale = Backbone.Model.extend({}, /** @lends EMLMeasurementScale.prototype */ diff --git a/src/js/models/metadata/eml211/EMLMissingValueCode.js b/src/js/models/metadata/eml211/EMLMissingValueCode.js index 301a8dccf..9ecfae121 100644 --- a/src/js/models/metadata/eml211/EMLMissingValueCode.js +++ b/src/js/models/metadata/eml211/EMLMissingValueCode.js @@ -6,6 +6,7 @@ define(["backbone"], function (Backbone) { * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html * @classcategory Models/Metadata/EML211 * @since 2.26.0 + * @extends Backbone.Model */ var EMLMissingValueCode = Backbone.Model.extend( /** @lends EMLMissingValueCode.prototype */ { diff --git a/src/js/models/metadata/eml211/EMLNumericDomain.js b/src/js/models/metadata/eml211/EMLNumericDomain.js index 6b4c9638e..93c7e9c6b 100644 --- a/src/js/models/metadata/eml211/EMLNumericDomain.js +++ b/src/js/models/metadata/eml211/EMLNumericDomain.js @@ -9,6 +9,7 @@ define(["jquery", "underscore", "backbone", * EMLMeasurementScale. * @classcategory Models/Metadata/EML211 * @see https://eml.ecoinformatics.org/schema/eml-attribute_xsd.html#AttributeType_measurementScale + * @extends Backbone.Model */ var EMLNumericDomain = Backbone.Model.extend( /** @lends EMLNumericDomain.prototype */{ diff --git a/src/js/models/metadata/eml211/EMLTaxonCoverage.js b/src/js/models/metadata/eml211/EMLTaxonCoverage.js index 0bd449cbd..efbe29cfd 100644 --- a/src/js/models/metadata/eml211/EMLTaxonCoverage.js +++ b/src/js/models/metadata/eml211/EMLTaxonCoverage.js @@ -24,430 +24,440 @@ define(["jquery", "underscore", "backbone", "models/DataONEObject"], function ( * classification, since taxonomy is represented as a hierarchy in EML. */ - var EMLTaxonCoverage = Backbone.Model.extend({ - /** - * Returns the default properties for this model. Defined here. - * @type {Object} - * @property {string} objectXML - The XML string for this model - * @property {Element} objectDOM - The XML DOM for this model - * @property {EML211} parentModel - The parent EML211 model - * @property {taxonomicClassification[]} taxonomicClassification - An array - * of taxonomic classifications, defining the taxonomic coverage of the - * dataset - * @property {string} generalTaxonomicCoverage - A general description of the - * taxonomic coverage of the dataset - */ - defaults: { - objectXML: null, - objectDOM: null, - parentModel: null, - taxonomicClassification: [], - generalTaxonomicCoverage: null, - }, - - initialize: function (attributes) { - if (attributes.objectDOM) this.set(this.parse(attributes.objectDOM)); - - this.on("change:taxonomicClassification", this.trickleUpChange); - this.on("change:taxonomicClassification", this.updateDOM); - }, - - /* - * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased - * EML node names (valid in EML). Used during parse() and serialize() - */ - nodeNameMap: function () { - return { - generaltaxonomiccoverage: "generalTaxonomicCoverage", - taxonomicclassification: "taxonomicClassification", - taxonrankname: "taxonRankName", - taxonrankvalue: "taxonRankValue", - taxonomiccoverage: "taxonomicCoverage", - taxonomicsystem: "taxonomicSystem", - classificationsystem: "classificationSystem", - classificationsystemcitation: "classificationSystemCitation", - classificationsystemmodifications: "classificationSystemModifications", - identificationreference: "identificationReference", - identifiername: "identifierName", - taxonomicprocedures: "taxonomicProcedures", - taxonomiccompleteness: "taxonomicCompleteness", - taxonid: "taxonId", - commonname: "commonName", - }; - }, - - parse: function (objectDOM) { - if (!objectDOM) var xml = this.get("objectDOM"); - - var model = this, - taxonomicClassifications = $(objectDOM).children( - "taxonomicclassification" - ), - modelJSON = { - taxonomicClassification: _.map( - taxonomicClassifications, - function (tc) { - return model.parseTaxonomicClassification(tc); - } - ), - generalTaxonomicCoverage: $(objectDOM) - .children("generaltaxonomiccoverage") - .first() - .text(), + /** + * @class EMLTaxonCoverage + * @classdesc The EMLTaxonCoverage model represents the taxonomic coverage of + * a dataset. It includes a general description of the taxonomic coverage, as + * well as a list of taxonomic classifications. + * @classcategory Models/Metadata/EML + * @extends Backbone.Model + * @constructor + */ + var EMLTaxonCoverage = Backbone.Model.extend( + /** @lends EMLTaxonCoverage.prototype */{ + /** + * Returns the default properties for this model. Defined here. + * @type {Object} + * @property {string} objectXML - The XML string for this model + * @property {Element} objectDOM - The XML DOM for this model + * @property {EML211} parentModel - The parent EML211 model + * @property {taxonomicClassification[]} taxonomicClassification - An array + * of taxonomic classifications, defining the taxonomic coverage of the + * dataset + * @property {string} generalTaxonomicCoverage - A general description of the + * taxonomic coverage of the dataset + */ + defaults: { + objectXML: null, + objectDOM: null, + parentModel: null, + taxonomicClassification: [], + generalTaxonomicCoverage: null, + }, + + initialize: function (attributes) { + if (attributes.objectDOM) this.set(this.parse(attributes.objectDOM)); + + this.on("change:taxonomicClassification", this.trickleUpChange); + this.on("change:taxonomicClassification", this.updateDOM); + }, + + /* + * Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased + * EML node names (valid in EML). Used during parse() and serialize() + */ + nodeNameMap: function () { + return { + generaltaxonomiccoverage: "generalTaxonomicCoverage", + taxonomicclassification: "taxonomicClassification", + taxonrankname: "taxonRankName", + taxonrankvalue: "taxonRankValue", + taxonomiccoverage: "taxonomicCoverage", + taxonomicsystem: "taxonomicSystem", + classificationsystem: "classificationSystem", + classificationsystemcitation: "classificationSystemCitation", + classificationsystemmodifications: "classificationSystemModifications", + identificationreference: "identificationReference", + identifiername: "identifierName", + taxonomicprocedures: "taxonomicProcedures", + taxonomiccompleteness: "taxonomicCompleteness", + taxonid: "taxonId", + commonname: "commonName", }; + }, - return modelJSON; - }, - - parseTaxonomicClassification: function (classification) { - var id = $(classification).attr("id"); - var rankName = $(classification).children("taxonrankname"); - var rankValue = $(classification).children("taxonrankvalue"); - var commonName = $(classification).children("commonname"); - var taxonId = $(classification).children("taxonId"); - var taxonomicClassification = $(classification).children( - "taxonomicclassification" - ); - - var model = this, - modelJSON = { - id: id, - taxonRankName: $(rankName).text().trim(), - taxonRankValue: $(rankValue).text().trim(), - commonName: _.map(commonName, function (cn) { - return $(cn).text().trim(); - }), - taxonId: _.map(taxonId, function (tid) { - return { - provider: $(tid).attr("provider").trim(), - value: $(tid).text().trim(), - }; - }), - taxonomicClassification: _.map( - taxonomicClassification, - function (tc) { - return model.parseTaxonomicClassification(tc); - } - ), - }; + parse: function (objectDOM) { + if (!objectDOM) var xml = this.get("objectDOM"); - if ( - Array.isArray(modelJSON.taxonomicClassification) && - !modelJSON.taxonomicClassification.length - ) - modelJSON.taxonomicClassification = {}; - - return modelJSON; - }, - - serialize: function () { - var objectDOM = this.updateDOM(), - xmlString = objectDOM.outerHTML; - - //Camel-case the XML - xmlString = this.formatXML(xmlString); - - return xmlString; - }, - - /* - * Makes a copy of the original XML DOM and updates it with the new values - * from the model. - */ - updateDOM: function () { - var objectDOM = this.get("objectDOM") - ? this.get("objectDOM").cloneNode(true) - : document.createElement("taxonomiccoverage"); - - $(objectDOM).empty(); - - // generalTaxonomicCoverage - var generalCoverage = this.get("generalTaxonomicCoverage"); - if (_.isString(generalCoverage) && generalCoverage.length > 0) { - $(objectDOM).append( - $(document.createElement("generaltaxonomiccoverage")).text( - this.get("generalTaxonomicCoverage") - ) + var model = this, + taxonomicClassifications = $(objectDOM).children( + "taxonomicclassification" + ), + modelJSON = { + taxonomicClassification: _.map( + taxonomicClassifications, + function (tc) { + return model.parseTaxonomicClassification(tc); + } + ), + generalTaxonomicCoverage: $(objectDOM) + .children("generaltaxonomiccoverage") + .first() + .text(), + }; + + return modelJSON; + }, + + parseTaxonomicClassification: function (classification) { + var id = $(classification).attr("id"); + var rankName = $(classification).children("taxonrankname"); + var rankValue = $(classification).children("taxonrankvalue"); + var commonName = $(classification).children("commonname"); + var taxonId = $(classification).children("taxonId"); + var taxonomicClassification = $(classification).children( + "taxonomicclassification" ); - } - // taxonomicClassification(s) - var classifications = this.get("taxonomicClassification"); + var model = this, + modelJSON = { + id: id, + taxonRankName: $(rankName).text().trim(), + taxonRankValue: $(rankValue).text().trim(), + commonName: _.map(commonName, function (cn) { + return $(cn).text().trim(); + }), + taxonId: _.map(taxonId, function (tid) { + return { + provider: $(tid).attr("provider").trim(), + value: $(tid).text().trim(), + }; + }), + taxonomicClassification: _.map( + taxonomicClassification, + function (tc) { + return model.parseTaxonomicClassification(tc); + } + ), + }; + + if ( + Array.isArray(modelJSON.taxonomicClassification) && + !modelJSON.taxonomicClassification.length + ) + modelJSON.taxonomicClassification = {}; + + return modelJSON; + }, + + serialize: function () { + var objectDOM = this.updateDOM(), + xmlString = objectDOM.outerHTML; + + //Camel-case the XML + xmlString = this.formatXML(xmlString); + + return xmlString; + }, + + /* + * Makes a copy of the original XML DOM and updates it with the new values + * from the model. + */ + updateDOM: function () { + var objectDOM = this.get("objectDOM") + ? this.get("objectDOM").cloneNode(true) + : document.createElement("taxonomiccoverage"); + + $(objectDOM).empty(); + + // generalTaxonomicCoverage + var generalCoverage = this.get("generalTaxonomicCoverage"); + if (_.isString(generalCoverage) && generalCoverage.length > 0) { + $(objectDOM).append( + $(document.createElement("generaltaxonomiccoverage")).text( + this.get("generalTaxonomicCoverage") + ) + ); + } + + // taxonomicClassification(s) + var classifications = this.get("taxonomicClassification"); + + if ( + typeof classifications === "undefined" || + classifications.length === 0 + ) { + return objectDOM; + } + + for (var i = 0; i < classifications.length; i++) { + $(objectDOM).append( + this.createTaxonomicClassificationDOM(classifications[i]) + ); + } + + // Remove empty (zero-length or whitespace-only) nodes + $(objectDOM) + .find("*") + .filter(function () { + return $.trim(this.innerHTML) === ""; + }) + .remove(); - if ( - typeof classifications === "undefined" || - classifications.length === 0 - ) { return objectDOM; - } + }, + + /* + * Create the DOM for a single EML taxonomicClassification. + * This function is currently recursive! + */ + createTaxonomicClassificationDOM: function (classification) { + var id = classification.id, + taxonRankName = classification.taxonRankName || "", + taxonRankValue = classification.taxonRankValue || "", + commonName = classification.commonName || "", + taxonId = classification.taxonId, + finishedEl; + + if (!taxonRankName || !taxonRankValue) return ""; + + finishedEl = $(document.createElement("taxonomicclassification")); + + if (typeof id === "string" && id.length > 0) { + $(finishedEl).attr("id", id); + } - for (var i = 0; i < classifications.length; i++) { - $(objectDOM).append( - this.createTaxonomicClassificationDOM(classifications[i]) - ); - } - - // Remove empty (zero-length or whitespace-only) nodes - $(objectDOM) - .find("*") - .filter(function () { - return $.trim(this.innerHTML) === ""; - }) - .remove(); - - return objectDOM; - }, - - /* - * Create the DOM for a single EML taxonomicClassification. - * This function is currently recursive! - */ - createTaxonomicClassificationDOM: function (classification) { - var id = classification.id, - taxonRankName = classification.taxonRankName || "", - taxonRankValue = classification.taxonRankValue || "", - commonName = classification.commonName || "", - taxonId = classification.taxonId, - finishedEl; - - if (!taxonRankName || !taxonRankValue) return ""; - - finishedEl = $(document.createElement("taxonomicclassification")); - - if (typeof id === "string" && id.length > 0) { - $(finishedEl).attr("id", id); - } - - if (taxonRankName && taxonRankName.length > 0) { - $(finishedEl).append( - $(document.createElement("taxonrankname")).text(taxonRankName) - ); - } + if (taxonRankName && taxonRankName.length > 0) { + $(finishedEl).append( + $(document.createElement("taxonrankname")).text(taxonRankName) + ); + } - if (taxonRankValue && taxonRankValue.length > 0) { - $(finishedEl).append( - $(document.createElement("taxonrankvalue")).text(taxonRankValue) - ); - } + if (taxonRankValue && taxonRankValue.length > 0) { + $(finishedEl).append( + $(document.createElement("taxonrankvalue")).text(taxonRankValue) + ); + } - if (commonName && commonName.length > 0) { - $(finishedEl).append( - $(document.createElement("commonname")).text(commonName) - ); - } + if (commonName && commonName.length > 0) { + $(finishedEl).append( + $(document.createElement("commonname")).text(commonName) + ); + } - if (taxonId) { - if (!Array.isArray(taxonId)) taxonId = [taxonId]; - _.each(taxonId, function (el) { - var taxonIdEl = $(document.createElement("taxonId")).text(el.value); + if (taxonId) { + if (!Array.isArray(taxonId)) taxonId = [taxonId]; + _.each(taxonId, function (el) { + var taxonIdEl = $(document.createElement("taxonId")).text(el.value); - if (el.provider) { - $(taxonIdEl).attr("provider", el.provider); - } + if (el.provider) { + $(taxonIdEl).attr("provider", el.provider); + } - $(finishedEl).append(taxonIdEl); - }); - } - - if (classification.taxonomicClassification) { - _.each( - classification.taxonomicClassification, - function (tc) { - $(finishedEl).append(this.createTaxonomicClassificationDOM(tc)); - }, - this - ); - } - - return finishedEl; - }, - - /* Validate this model */ - validate: function () { - var errors = {}; - - if ( - !this.get("generalTaxonomicCoverage") && - MetacatUI.appModel.get("emlEditorRequiredFields") - .generalTaxonomicCoverage - ) - errors.generalTaxonomicCoverage = - "Provide a description of the taxonomic coverage."; - - //If there are no taxonomic classifications and it is either required in - // the AppModel OR a general coverage was given, then require it - if ( - !this.get("taxonomicClassification").length && - (MetacatUI.appModel.get("emlEditorRequiredFields").taxonCoverage || - this.get("generalTaxonomicCoverage")) - ) { - errors.taxonomicClassification = - "Provide at least one complete taxonomic classification."; - } else { - //Every taxonomic classification should be valid - if ( - !_.every( - this.get("taxonomicClassification"), - this.isClassificationValid, + $(finishedEl).append(taxonIdEl); + }); + } + + if (classification.taxonomicClassification) { + _.each( + classification.taxonomicClassification, + function (tc) { + $(finishedEl).append(this.createTaxonomicClassificationDOM(tc)); + }, this - ) - ) - errors.taxonomicClassification = - "Every classification row should have a rank and value."; - } + ); + } - // Check for and remove duplicate classifications - this.removeDuplicateClassifications(); + return finishedEl; + }, - if (Object.keys(errors).length) return errors; - }, + /* Validate this model */ + validate: function () { + var errors = {}; - isEmpty: function () { - return ( - !this.get("generalTaxonomicCoverage") && - !this.get("taxonomicClassification").length - ); - }, + if ( + !this.get("generalTaxonomicCoverage") && + MetacatUI.appModel.get("emlEditorRequiredFields") + .generalTaxonomicCoverage + ) + errors.generalTaxonomicCoverage = + "Provide a description of the taxonomic coverage."; - isClassificationValid: function (taxonomicClassification) { - if (!Object.keys(taxonomicClassification).length) return true; - if (Array.isArray(taxonomicClassification)) { + //If there are no taxonomic classifications and it is either required in + // the AppModel OR a general coverage was given, then require it if ( - !taxonomicClassification[0].taxonRankName || - !taxonomicClassification[0].taxonRankValue + !this.get("taxonomicClassification").length && + (MetacatUI.appModel.get("emlEditorRequiredFields").taxonCoverage || + this.get("generalTaxonomicCoverage")) ) { - return false; + errors.taxonomicClassification = + "Provide at least one complete taxonomic classification."; + } else { + //Every taxonomic classification should be valid + if ( + !_.every( + this.get("taxonomicClassification"), + this.isClassificationValid, + this + ) + ) + errors.taxonomicClassification = + "Every classification row should have a rank and value."; } - } else if ( - !taxonomicClassification.taxonRankName || - !taxonomicClassification.taxonRankValue - ) { - return false; - } - if (taxonomicClassification.taxonomicClassification) - return this.isClassificationValid( - taxonomicClassification.taxonomicClassification + // Check for and remove duplicate classifications + this.removeDuplicateClassifications(); + + if (Object.keys(errors).length) return errors; + }, + + isEmpty: function () { + return ( + !this.get("generalTaxonomicCoverage") && + !this.get("taxonomicClassification").length ); - else return true; - }, - - /** - * Check if two classifications are equal. Two classifications are equal if - * they have the same rankName, rankValue, commonName, and taxonId, as well - * as the same nested classifications. This function is recursive. - * @param {taxonomicClassification} c1 - * @param {taxonomicClassification} c2 - * @returns {boolean} - True if the two classifications are equal - * @since 2.24.0 - */ - classificationsAreEqual: function (c1, c2) { - if (!c1 && !c2) return true; - if (!c1 && c2) return false; - if (c1 && !c2) return false; - - // stringify the two classifications for - const stringKeys = ["taxonRankName", "taxonRankValue", "commonName"]; - - // Recursively stringify the nested classifications for comparison - stringifyClassification = function (c) { - const stringified = {}; - for (let key of stringKeys) { - if (c[key]) stringified[key] = c[key]; + }, + + isClassificationValid: function (taxonomicClassification) { + if (!Object.keys(taxonomicClassification).length) return true; + if (Array.isArray(taxonomicClassification)) { + if ( + !taxonomicClassification[0].taxonRankName || + !taxonomicClassification[0].taxonRankValue + ) { + return false; + } + } else if ( + !taxonomicClassification.taxonRankName || + !taxonomicClassification.taxonRankValue + ) { + return false; } - if (c.taxonId) stringified.taxonId = c.taxonId; - if (c.taxonomicClassification) { - stringified.taxonomicClassification = stringifyClassification( - c.taxonomicClassification + + if (taxonomicClassification.taxonomicClassification) + return this.isClassificationValid( + taxonomicClassification.taxonomicClassification ); + else return true; + }, + + /** + * Check if two classifications are equal. Two classifications are equal if + * they have the same rankName, rankValue, commonName, and taxonId, as well + * as the same nested classifications. This function is recursive. + * @param {taxonomicClassification} c1 + * @param {taxonomicClassification} c2 + * @returns {boolean} - True if the two classifications are equal + * @since 2.24.0 + */ + classificationsAreEqual: function (c1, c2) { + if (!c1 && !c2) return true; + if (!c1 && c2) return false; + if (c1 && !c2) return false; + + // stringify the two classifications for + const stringKeys = ["taxonRankName", "taxonRankValue", "commonName"]; + + // Recursively stringify the nested classifications for comparison + stringifyClassification = function (c) { + const stringified = {}; + for (let key of stringKeys) { + if (c[key]) stringified[key] = c[key]; + } + if (c.taxonId) stringified.taxonId = c.taxonId; + if (c.taxonomicClassification) { + stringified.taxonomicClassification = stringifyClassification( + c.taxonomicClassification + ); + } + const st = JSON.stringify(stringified); + // convert all to uppercase for comparison + return st.toUpperCase(); + }; + + return stringifyClassification(c1) === stringifyClassification(c2); + }, + + /** + * Returns true if the given classification is a duplicate of another + * classification in this model. Duplicates are considered those that have + * all values identical, including rankName, rankValue, commonName, and + * taxonId. If there are any nested classifications, then they too must + * be identical for the classification to be considered a duplicate, this + * this function is recursive. Only checks one classification at a time. + * @param {taxonomicClassification} classification + * @param {number} indexToSkip - The index of the classification to skip + * when checking for duplicates. This is useful when checking if a + * classification is a duplicate of another classification in the same + * model, but not itself. + * @returns {boolean} - True if the given classification is a duplicate + * @since 2.24.0 + */ + isDuplicate: function (classification, indexToSkip) { + const classifications = this.get("taxonomicClassification"); + for (let i = 0; i < classifications.length; i++) { + if (typeof indexToSkip === "number" && i === indexToSkip) continue; + if (this.classificationsAreEqual(classifications[i], classification)) { + return true; + } } - const st = JSON.stringify(stringified); - // convert all to uppercase for comparison - return st.toUpperCase(); - }; - - return stringifyClassification(c1) === stringifyClassification(c2); - }, - - /** - * Returns true if the given classification is a duplicate of another - * classification in this model. Duplicates are considered those that have - * all values identical, including rankName, rankValue, commonName, and - * taxonId. If there are any nested classifications, then they too must - * be identical for the classification to be considered a duplicate, this - * this function is recursive. Only checks one classification at a time. - * @param {taxonomicClassification} classification - * @param {number} indexToSkip - The index of the classification to skip - * when checking for duplicates. This is useful when checking if a - * classification is a duplicate of another classification in the same - * model, but not itself. - * @returns {boolean} - True if the given classification is a duplicate - * @since 2.24.0 - */ - isDuplicate: function (classification, indexToSkip) { - const classifications = this.get("taxonomicClassification"); - for (let i = 0; i < classifications.length; i++) { - if (typeof indexToSkip === "number" && i === indexToSkip) continue; - if (this.classificationsAreEqual(classifications[i], classification)) { - return true; + return false; + }, + + /** + * Remove any duplicated classifications from this model. See + * {@link isDuplicate} for more information on what is considered a + * duplicate. If any classifications are removed, then a + * "duplicateClassificationsRemoved" event is triggered, passing the + * removed classifications as an argument. + * @fires duplicateClassificationsRemoved + * @since 2.24.0 + */ + removeDuplicateClassifications: function () { + const classifications = this.get("taxonomicClassification"); + const removed = []; + for (let i = 0; i < classifications.length; i++) { + const classification = classifications[i]; + if (this.isDuplicate(classification, i)) { + classifications.splice(i, 1); + this.set("taxonomicClassification", classifications); + removed.push(classification); + i--; + } } - } - return false; - }, - - /** - * Remove any duplicated classifications from this model. See - * {@link isDuplicate} for more information on what is considered a - * duplicate. If any classifications are removed, then a - * "duplicateClassificationsRemoved" event is triggered, passing the - * removed classifications as an argument. - * @fires duplicateClassificationsRemoved - * @since 2.24.0 - */ - removeDuplicateClassifications: function () { - const classifications = this.get("taxonomicClassification"); - const removed = []; - for (let i = 0; i < classifications.length; i++) { - const classification = classifications[i]; - if (this.isDuplicate(classification, i)) { - classifications.splice(i, 1); - this.set("taxonomicClassification", classifications); - removed.push(classification); - i--; + if (removed.length) { + this.trigger("duplicateClassificationsRemoved", removed); } - } - if (removed.length) { - this.trigger("duplicateClassificationsRemoved", removed); - } - }, - - /* - * Climbs up the model hierarchy until it finds the EML model - * - * @return {EML211 or false} - Returns the EML 211 Model or false if not - * found - */ - getParentEML: function () { - var emlModel = this.get("parentModel"), - tries = 0; - - while (emlModel.type !== "EML" && tries < 6) { - emlModel = emlModel.get("parentModel"); - tries++; - } - - if (emlModel && emlModel.type == "EML") return emlModel; - else return false; - }, - - trickleUpChange: function () { - MetacatUI.rootDataPackage.packageModel.set("changed", true); - }, - - formatXML: function (xmlString) { - return DataONEObject.prototype.formatXML.call(this, xmlString); - }, - }); + }, + + /* + * Climbs up the model hierarchy until it finds the EML model + * + * @return {EML211 or false} - Returns the EML 211 Model or false if not + * found + */ + getParentEML: function () { + var emlModel = this.get("parentModel"), + tries = 0; + + while (emlModel.type !== "EML" && tries < 6) { + emlModel = emlModel.get("parentModel"); + tries++; + } + + if (emlModel && emlModel.type == "EML") return emlModel; + else return false; + }, + + trickleUpChange: function () { + MetacatUI.rootDataPackage.packageModel.set("changed", true); + }, + + formatXML: function (xmlString) { + return DataONEObject.prototype.formatXML.call(this, xmlString); + }, + }); return EMLTaxonCoverage; }); diff --git a/src/js/models/metadata/eml211/EMLTemporalCoverage.js b/src/js/models/metadata/eml211/EMLTemporalCoverage.js index c660865c1..697be18c8 100644 --- a/src/js/models/metadata/eml211/EMLTemporalCoverage.js +++ b/src/js/models/metadata/eml211/EMLTemporalCoverage.js @@ -5,6 +5,7 @@ define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'], /** * @class EMLTemporalCoverage * @classcategory Models/Metadata/EML211 + * @extends Backbone.Model */ var EMLTemporalCoverage = Backbone.Model.extend( /** @lends EMLTemporalCoverage.prototype */{ diff --git a/src/js/models/metadata/eml211/EMLText.js b/src/js/models/metadata/eml211/EMLText.js index 653042ad3..37b702a56 100644 --- a/src/js/models/metadata/eml211/EMLText.js +++ b/src/js/models/metadata/eml211/EMLText.js @@ -6,6 +6,7 @@ define(['jquery', 'underscore', 'backbone', 'models/DataONEObject'], * @class EMLText211 * @classdesc A model that represents the EML 2.1.1 Text module * @classcategory Models/Metadata/EML211 + * @extends Backbone.Model */ var EMLText = Backbone.Model.extend( /** @lends EMLText211.prototype */{ diff --git a/src/js/models/metadata/eml211/EMLUnit.js b/src/js/models/metadata/eml211/EMLUnit.js index f08422de5..f22bf04c4 100644 --- a/src/js/models/metadata/eml211/EMLUnit.js +++ b/src/js/models/metadata/eml211/EMLUnit.js @@ -6,6 +6,7 @@ define(["jquery", "underscore", "backbone"], function($, _, Backbone) { * @class EMLUnit * @classdesc An EMLUnit represents a single unit defined in the EML Unit Dictionary * @classcategory Models/Metadata/EML211 + * @extends Backbone.Model */ var EMLUnit = Backbone.Model.extend( /** @lends EMLUnit.prototype */{ diff --git a/src/js/models/metadata/eml220/EMLText.js b/src/js/models/metadata/eml220/EMLText.js index cf263eede..bf3f7f963 100644 --- a/src/js/models/metadata/eml220/EMLText.js +++ b/src/js/models/metadata/eml220/EMLText.js @@ -7,6 +7,7 @@ define(['jquery', 'underscore', 'backbone', 'models/metadata/eml211/EMLText', * @class EMLText * @classdesc A model that represents the EML 2.2.0 Text module * @classcategory Models/Metadata/EML220 + * @extends EMLText211 */ var EMLText = EMLText211.extend( /** @lends EMLText.prototype */{ diff --git a/src/js/models/projects/Project.js b/src/js/models/projects/Project.js index 39a5b6fcd..dde1de37e 100644 --- a/src/js/models/projects/Project.js +++ b/src/js/models/projects/Project.js @@ -9,6 +9,7 @@ define(['jquery', 'backbone'], * metacat. * @classcategory Models/Projects * @since 2.22.0 + * @extends Backbone.Model */ var Project = Backbone.Model.extend(/** @lends Project.prototype */{ diff --git a/src/js/themes/arctic/config.js b/src/js/themes/arctic/config.js index 3384c9e32..82ec5e912 100644 --- a/src/js/themes/arctic/config.js +++ b/src/js/themes/arctic/config.js @@ -14,10 +14,10 @@ MetacatUI.AppConfig = Object.assign( nodeId: "urn:node:ARCTIC", //Metadata quality - mdqSuiteIds: ["arctic.data.center.suite.1", "FAIR-suite-0.3.1"], + mdqSuiteIds: ["arctic.data.center.suite-1.2.0", "FAIR-suite-0.4.0"], mdqSuiteLabels: [ - "Arctic Data Center Conformance Suite v1.0", - "FAIR Suite v0.3.1", + "Arctic Data Center Conformance Suite v1.2", + "FAIR Suite v0.4.0", ], mdqFormatIds: ["eml*", "https://eml*"], displayDatasetQualityMetric: true, diff --git a/src/js/themes/knb/config.js b/src/js/themes/knb/config.js index 9f81191fe..c14c3ba52 100644 --- a/src/js/themes/knb/config.js +++ b/src/js/themes/knb/config.js @@ -62,8 +62,8 @@ MetacatUI.AppConfig = Object.assign({ //Metadata assessments displayDatasetQualityMetric: true, - mdqSuiteIds: ["knb.suite.1"], - mdqSuiteLabels: ["KNB Metadata Completeness Suite v1.0"], + mdqSuiteIds: ["FAIR-suite-0.4.0"], + mdqSuiteLabels: ["FAIR Suite v0.4.0"], mdqFormatIds:["eml*", "https://eml*"], //Portals diff --git a/src/js/themes/knb/views/TextView.js b/src/js/themes/knb/views/TextView.js index b7ad6feed..3767b5a8a 100644 --- a/src/js/themes/knb/views/TextView.js +++ b/src/js/themes/knb/views/TextView.js @@ -5,6 +5,7 @@ define(["jquery", "underscore", "backbone", "views/BaseTextView", "text!template /* * Extend the TextView to provide new templates + * @extends BaseTextView */ var TextView = BaseTextView.extend({ // Add the preservation page template diff --git a/src/js/views/AccessRuleView.js b/src/js/views/AccessRuleView.js index 2a4267ac7..92de11843 100644 --- a/src/js/views/AccessRuleView.js +++ b/src/js/views/AccessRuleView.js @@ -9,6 +9,7 @@ function(_, $, Backbone, AccessRule){ * @classdesc Renders a single access rule from an object's access policy * @classcategory Views * @screenshot views/AccessRuleView.png + * @extends Backbone.View */ var AccessRuleView = Backbone.View.extend( /** @lends AccessRuleView.prototype */{ diff --git a/src/js/views/AppView.js b/src/js/views/AppView.js index 02aee0a0e..32b67ba65 100644 --- a/src/js/views/AppView.js +++ b/src/js/views/AppView.js @@ -34,6 +34,7 @@ define([ * @class AppView * @classdesc The top-level view of the UI that contains and coordinates all other views of the UI * @classcategory Views + * @extends Backbone.View */ var AppView = Backbone.View.extend( /** @lends AppView.prototype */ { diff --git a/src/js/views/DataPackageView.js b/src/js/views/DataPackageView.js index 6f480d1f2..4b708dd2a 100644 --- a/src/js/views/DataPackageView.js +++ b/src/js/views/DataPackageView.js @@ -25,6 +25,7 @@ define([ * a file/folder browser * @classcategory Views * @screenshot views/DataPackageView.png + * @extends Backbone.View */ var DataPackageView = Backbone.View.extend( /** @lends DataPackageView.prototype */{ diff --git a/src/js/views/DraftsView.js b/src/js/views/DraftsView.js index 1c002d1ad..04e8a8568 100644 --- a/src/js/views/DraftsView.js +++ b/src/js/views/DraftsView.js @@ -4,6 +4,7 @@ define(["jquery", "underscore", "backbone", "localforage", "clipboard", "text!te * @class DraftsView * @classdesc A view that lists the local submission drafts for this user * @classcategory Views + * @extends Backbone.View */ var view = Backbone.View.extend( /** @lends DraftsView.prototype */{ diff --git a/src/js/views/GroupListView.js b/src/js/views/GroupListView.js index e00272012..9e8d8e018 100644 --- a/src/js/views/GroupListView.js +++ b/src/js/views/GroupListView.js @@ -6,8 +6,9 @@ define(['jquery', 'underscore', 'backbone', 'collections/UserGroup', 'models/Use /** * @class GroupListView * @classdesc Displays a list of UserModels of a UserGroup collection and allows owners to add/remove members from the group - * @classcategory Views - * @screenshot views/GroupListView.png + * @classcategory Views + * @screenshot views/GroupListView.png + * @extends Backbone.View */ var GroupListView = Backbone.View.extend( /** @lends GroupListView.prototype */{ diff --git a/src/js/views/ImageUploaderView.js b/src/js/views/ImageUploaderView.js index 0106d1518..4b31e5c3f 100644 --- a/src/js/views/ImageUploaderView.js +++ b/src/js/views/ImageUploaderView.js @@ -12,6 +12,7 @@ function(_, $, Backbone, DataONEObject, ObjectFormats, Dropzone, Template, corej * @class ImageUploaderView * @classdesc A view that allows a person to upload an image to the repository * @classcategory Views + * @extends Backbone.View */ var ImageUploaderView = Backbone.View.extend( /** @lends ImageUploaderView.prototype */{ diff --git a/src/js/views/MapsView.js b/src/js/views/MapsView.js index 607738d2d..90b714165 100644 --- a/src/js/views/MapsView.js +++ b/src/js/views/MapsView.js @@ -11,6 +11,7 @@ define(["jquery", * @classdesc The mapsView is the area where the the geographic coverage of the datasets that comprise the portal * are displayed. The mapsView will update to match the search results when they are filtered. * @classcategory Views + * @extends Backbone.View */ var mapsView = Backbone.View.extend({ diff --git a/src/js/views/MetricsChartView.js b/src/js/views/MetricsChartView.js index 33dd6e16a..56b2aa62d 100644 --- a/src/js/views/MetricsChartView.js +++ b/src/js/views/MetricsChartView.js @@ -8,6 +8,7 @@ define(['jquery', 'underscore', 'backbone', 'd3'], * @classdesc The MetricsChartView will render an SVG times-series chart using D3 that shows the number of metrics over time. * @screenshot views/MetricsChartView.png * @classcategory Views + * @extends Backbone.View */ var MetricsChartView = Backbone.View.extend( /** @lends MetricsChartView.prototype */{ diff --git a/src/js/views/ProvStatementView.js b/src/js/views/ProvStatementView.js index 14833c60c..14969b764 100644 --- a/src/js/views/ProvStatementView.js +++ b/src/js/views/ProvStatementView.js @@ -6,8 +6,10 @@ define(['jquery', 'underscore', 'backbone', 'views/ExpandCollapseListView', 'tex * Constructs a list of provenance statements based on the indexed prov fields of Solr documents. * Renders a list of paragraph tags with sentences and links to the objects in the sentence. * The Prov Statement template can be used to display other UI elements along with the textual prov statements. + * @extends Backbone.View */ - var ProvStatementView = Backbone.View.extend({ + var ProvStatementView = Backbone.View.extend( + /** @lends ProvStatementView */{ /* * OPTIONS diff --git a/src/js/views/TOCView.js b/src/js/views/TOCView.js index 764cd7d18..3399b7f3e 100644 --- a/src/js/views/TOCView.js +++ b/src/js/views/TOCView.js @@ -17,6 +17,7 @@ define(["jquery", there are 'h2' tags within the 'topLevelItem' containers, these will be listed under the 'topLevelItem'. * @classcategory Views + * @extends Backbone.View */ var TOCView = Backbone.View.extend( /** @lends TOCView.prototype */{ diff --git a/src/js/views/UserGroupView.js b/src/js/views/UserGroupView.js index ffcb4505f..90a6fc3aa 100644 --- a/src/js/views/UserGroupView.js +++ b/src/js/views/UserGroupView.js @@ -9,6 +9,7 @@ define(['jquery', 'underscore', 'backbone', 'collections/UserGroup', 'views/Grou * of members to groups * @classcategory Views * @screenshot views/UserGroupView.png + * @extends Backbone.View */ var UserGroupView = Backbone.View.extend( /** @lends UserGroupView.prototype */ { diff --git a/src/js/views/UserView.js b/src/js/views/UserView.js index 73f48abd4..036e3029d 100644 --- a/src/js/views/UserView.js +++ b/src/js/views/UserView.js @@ -19,8 +19,9 @@ define(['jquery', 'underscore', 'backbone', 'clipboard', * @class UserView * @classdesc A major view that displays a public profile for the user and a settings page for the logged-in user * to manage their account info, groups, identities, and API tokens. - * @classcategory Views - * @screenshot views/UserView.png + * @classcategory Views + * @screenshot views/UserView.png + * @extends Backbone.View */ var UserView = Backbone.View.extend( /** @lends UserView.prototype */{ diff --git a/src/js/views/filters/BooleanFilterView.js b/src/js/views/filters/BooleanFilterView.js index 205b19b89..173d2c953 100644 --- a/src/js/views/filters/BooleanFilterView.js +++ b/src/js/views/filters/BooleanFilterView.js @@ -13,6 +13,7 @@ define([ * @class BooleanFilterView * @classdesc Render a view of a single BooleanFilter model * @classcategory Views/Filters + * @extends FilterView */ var BooleanFilterView = FilterView.extend( /** @lends BooleanFilterView.prototype */ { diff --git a/src/js/views/maps/LayersPanelView.js b/src/js/views/maps/LayersPanelView.js index 081197fc9..2ac3ce1f1 100644 --- a/src/js/views/maps/LayersPanelView.js +++ b/src/js/views/maps/LayersPanelView.js @@ -24,86 +24,87 @@ define([ * @since 2.28.0 * @constructs LayersPanelView */ - const LayersPanelView = Backbone.View.extend({ - /** - * The type of View this is - * @type {string} - */ - type: "LayersPanelView", + const LayersPanelView = Backbone.View.extend( + /** @lends LayersPanelView.prototype */{ + /** + * The type of View this is + * @type {string} + */ + type: "LayersPanelView", - /** - * The HTML classes to use for this view's element - * @type {string} - */ - className: "layers-panel", + /** + * The HTML classes to use for this view's element + * @type {string} + */ + className: "layers-panel", - /** - * The HTML classes to use for this view's HTML elements. - * @type {Object} - */ - classNames: { - search: "layers-panel__search", - layers: "layers-panel__layers", - }, + /** + * The HTML classes to use for this view's HTML elements. + * @type {Object} + */ + classNames: { + search: "layers-panel__search", + layers: "layers-panel__layers", + }, - /** - * @typedef {Object} LayersPanelViewOptions - * @property {Map} The Map model that contains layers information. - */ - initialize(options) { - this.map = options.model; - }, + /** + * @typedef {Object} LayersPanelViewOptions + * @property {Map} The Map model that contains layers information. + */ + initialize(options) { + this.map = options.model; + }, - /** - * Render the view by updating the HTML of the element. - * The new HTML is computed from an HTML template that - * is passed an object with relevant view state. - * */ - render() { - this.el.innerHTML = _.template(Template)({ classNames: this.classNames }); + /** + * Render the view by updating the HTML of the element. + * The new HTML is computed from an HTML template that + * is passed an object with relevant view state. + * */ + render() { + this.el.innerHTML = _.template(Template)({ classNames: this.classNames }); - if (this.map.get('layerCategories')?.length > 0) { - this.layersView = new LayerCategoryListView({ collection: this.map.get("layerCategories") }); - } else { - this.layersView = new LayerListView({ - collection: this.map.get("layers"), - isCategorized: false, - }); - } - this.layersView.render(); - this.$(`.${this.classNames.layers}`).append(this.layersView.el); + if (this.map.get('layerCategories')?.length > 0) { + this.layersView = new LayerCategoryListView({ collection: this.map.get("layerCategories") }); + } else { + this.layersView = new LayerListView({ + collection: this.map.get("layers"), + isCategorized: false, + }); + } + this.layersView.render(); + this.$(`.${this.classNames.layers}`).append(this.layersView.el); - this.searchInput = new SearchInputView({ - placeholder: "Search all data layers", - search: text => this.search(text), - noMatchCallback: () => this.layersView.search(""), - }); - this.searchInput.render(); - this.$(`.${this.classNames.search}`).append(this.searchInput.el); - }, + this.searchInput = new SearchInputView({ + placeholder: "Search all data layers", + search: text => this.search(text), + noMatchCallback: () => this.layersView.search(""), + }); + this.searchInput.render(); + this.$(`.${this.classNames.search}`).append(this.searchInput.el); + }, - /** - * Search function for the SearchInputView. - * @param {string} [text] - The search text from user input. - * @returns {boolean} - True if there is a layer match. - */ - search(text) { - this.dismissLayerDetails(); - const matched = this.layersView.search(text); - if (!matched) { - this.searchInput.setError("No layers match your search"); - } - return matched; - }, + /** + * Search function for the SearchInputView. + * @param {string} [text] - The search text from user input. + * @returns {boolean} - True if there is a layer match. + */ + search(text) { + this.dismissLayerDetails(); + const matched = this.layersView.search(text); + if (!matched) { + this.searchInput.setError("No layers match your search"); + } + return matched; + }, - dismissLayerDetails() { - this.map.getLayerGroups().forEach(mapAssets => { - mapAssets.forEach(layerModel => { - layerModel.set("selected", false); + dismissLayerDetails() { + this.map.getLayerGroups().forEach(mapAssets => { + mapAssets.forEach(layerModel => { + layerModel.set("selected", false); + }); }); - }); - }, - }); + }, + }); return LayersPanelView; }); diff --git a/src/js/views/maps/SearchInputView.js b/src/js/views/maps/SearchInputView.js index 35af8a57b..14bfa004d 100644 --- a/src/js/views/maps/SearchInputView.js +++ b/src/js/views/maps/SearchInputView.js @@ -29,275 +29,276 @@ define([ * @since 2.28.0 * @constructs SearchInputView */ - const SearchInputView = Backbone.View.extend({ - /** - * The type of View this is - * @type {string} - */ - type: "SearchInputView", - - /** - * The HTML classes to use for this view's element - * @type {string} - */ - className: BASE_CLASS, - - /** - * Values meant to be used by the rendered HTML template. - */ - templateVars: { - errorText: "", - placeholder: "", - classNames: CLASS_NAMES, - }, - - /** - * The events this view will listen to and the associated function to call. - * @type {Object} - */ - events() { - return { - [`click .${CLASS_NAMES.cancelButton}`]: "onCancel", - [`blur .${CLASS_NAMES.input}`]: 'onBlur', - [`change .${CLASS_NAMES.input}`]: 'onKeyup', - [`focus .${CLASS_NAMES.input}`]: 'onFocus', - [`keydown .${CLASS_NAMES.input}`]: 'onKeydown', - [`keyup .${CLASS_NAMES.input}`]: 'onKeyup', - [`click .${CLASS_NAMES.searchButton}`]: "onSearch", - }; - }, - - /** - * @typedef {Object} SearchInputViewOptions - * @property {Function} search A function that takes in a text input and returns - * a boolean for whether there is a match. - * @property {Function} keydownCallback A function that receives a key event - * on keydown. - * @property {Function} keyupCallback A function that receives a key event - * on keyup stroke. - * @property {Function} blurCallback A function that receives an event on - * blur of the input. - * @property {Function} focusCallback A function that receives an event on - * focus of the input. - * @property {Function} noMatchCallback A callback function to handle a no match - * situation. - * @property {String} placeholder The placeholder text for the input box. - */ - initialize(options) { - if (typeof (options.search) !== "function") { - throw new Error("Initializing SearchInputView without a search function."); - } - this.search = options.search; - this.keyupCallback = options.keyupCallback || noop; - this.keydownCallback = options.keydownCallback || noop; - this.blurCallback = options.blurCallback || noop; - this.focusCallback = options.focusCallback || noop; - this.noMatchCallback = options.noMatchCallback || noop; - this.templateVars.placeholder = options.placeholder; - }, - - /** - * Render the view by updating the HTML of the element. - * The new HTML is computed from an HTML template that - * is passed an object with relevant view state. - * */ - render() { - this.el.innerHTML = _.template(Template)(this.templateVars); - }, - - /** - * Event handler for Backbone.View configuration that is called whenever - * the user types a key. - */ - onKeyup(event) { - if (event.key === "Enter") { - this.onSearch(); - return; - } - - if (this.getInputValue().toLowerCase() !== "") { - this.showCancelAndSearch(); - } else { - this.hideCancelAndDimSearch(); - } - - this.keyupCallback(event); - }, - - /** - * Manage state change for the search button and cancel button when user has - * entered some input. - */ - showCancelAndSearch() { - this.getCancelButtonContainer().show(); - this.getSearchButton().addClass(CLASS_NAMES.searchButtonActive); - }, - - /** - * Manage state change for the search button and cancel button when user has - * cleared the input. - */ - hideCancelAndDimSearch() { - this.getCancelButtonContainer().hide(); - this.getSearchButton().removeClass(CLASS_NAMES.searchButtonActive); - }, - - /** - * Event handler for Backbone.View configuration that is called whenever - * the user types a key. - */ - onKeydown(event) { - this.keydownCallback(event); - - if (this.getInputValue() === "") { - this.clearError(); - } - }, - - /** - * Event handler for Backbone.View configuration that is called whenever - * the user focuses the input. - */ - onFocus(event) { - this.focusCallback(event); - }, - - /** - * Event handler for Backbone.View configuration that is called whenever - * the user blurs the input. - */ - onBlur(event) { - this.blurCallback(event); - }, - - /** - * Event handler for Backbone.View configuration that is called whenever - * the user clicks the search button or hits the Enter key. - */ - onSearch() { - this.getError().hide(); - - const inputField = this.getInputField(); - const inputValue = this.getInputValue().toLowerCase(); - const matched = this.search(inputValue); - if (matched) { - this.clearError(); - } else if (typeof (this.noMatchCallback) === "function") { - this.noMatchCallback(); - } - }, - - /** - * API for the view that conducts the search to toggle on the error message. - * @param {string} errorText - */ - setError(errorText) { - if (errorText) { - this.getInputField().addClass(CLASS_NAMES.errorInput); + const SearchInputView = Backbone.View.extend( + /** @lends SearchInputView.prototype */{ + /** + * The type of View this is + * @type {string} + */ + type: "SearchInputView", + + /** + * The HTML classes to use for this view's element + * @type {string} + */ + className: BASE_CLASS, + + /** + * Values meant to be used by the rendered HTML template. + */ + templateVars: { + errorText: "", + placeholder: "", + classNames: CLASS_NAMES, + }, + + /** + * The events this view will listen to and the associated function to call. + * @type {Object} + */ + events() { + return { + [`click .${CLASS_NAMES.cancelButton}`]: "onCancel", + [`blur .${CLASS_NAMES.input}`]: 'onBlur', + [`change .${CLASS_NAMES.input}`]: 'onKeyup', + [`focus .${CLASS_NAMES.input}`]: 'onFocus', + [`keydown .${CLASS_NAMES.input}`]: 'onKeydown', + [`keyup .${CLASS_NAMES.input}`]: 'onKeyup', + [`click .${CLASS_NAMES.searchButton}`]: "onSearch", + }; + }, + + /** + * @typedef {Object} SearchInputViewOptions + * @property {Function} search A function that takes in a text input and returns + * a boolean for whether there is a match. + * @property {Function} keydownCallback A function that receives a key event + * on keydown. + * @property {Function} keyupCallback A function that receives a key event + * on keyup stroke. + * @property {Function} blurCallback A function that receives an event on + * blur of the input. + * @property {Function} focusCallback A function that receives an event on + * focus of the input. + * @property {Function} noMatchCallback A callback function to handle a no match + * situation. + * @property {String} placeholder The placeholder text for the input box. + */ + initialize(options) { + if (typeof (options.search) !== "function") { + throw new Error("Initializing SearchInputView without a search function."); + } + this.search = options.search; + this.keyupCallback = options.keyupCallback || noop; + this.keydownCallback = options.keydownCallback || noop; + this.blurCallback = options.blurCallback || noop; + this.focusCallback = options.focusCallback || noop; + this.noMatchCallback = options.noMatchCallback || noop; + this.templateVars.placeholder = options.placeholder; + }, + + /** + * Render the view by updating the HTML of the element. + * The new HTML is computed from an HTML template that + * is passed an object with relevant view state. + * */ + render() { + this.el.innerHTML = _.template(Template)(this.templateVars); + }, + + /** + * Event handler for Backbone.View configuration that is called whenever + * the user types a key. + */ + onKeyup(event) { + if (event.key === "Enter") { + this.onSearch(); + return; + } + + if (this.getInputValue().toLowerCase() !== "") { + this.showCancelAndSearch(); + } else { + this.hideCancelAndDimSearch(); + } + + this.keyupCallback(event); + }, + + /** + * Manage state change for the search button and cancel button when user has + * entered some input. + */ + showCancelAndSearch() { + this.getCancelButtonContainer().show(); + this.getSearchButton().addClass(CLASS_NAMES.searchButtonActive); + }, + + /** + * Manage state change for the search button and cancel button when user has + * cleared the input. + */ + hideCancelAndDimSearch() { + this.getCancelButtonContainer().hide(); + this.getSearchButton().removeClass(CLASS_NAMES.searchButtonActive); + }, + + /** + * Event handler for Backbone.View configuration that is called whenever + * the user types a key. + */ + onKeydown(event) { + this.keydownCallback(event); + + if (this.getInputValue() === "") { + this.clearError(); + } + }, + + /** + * Event handler for Backbone.View configuration that is called whenever + * the user focuses the input. + */ + onFocus(event) { + this.focusCallback(event); + }, + + /** + * Event handler for Backbone.View configuration that is called whenever + * the user blurs the input. + */ + onBlur(event) { + this.blurCallback(event); + }, + + /** + * Event handler for Backbone.View configuration that is called whenever + * the user clicks the search button or hits the Enter key. + */ + onSearch() { + this.getError().hide(); + + const inputField = this.getInputField(); + const inputValue = this.getInputValue().toLowerCase(); + const matched = this.search(inputValue); + if (matched) { + this.clearError(); + } else if (typeof (this.noMatchCallback) === "function") { + this.noMatchCallback(); + } + }, + + /** + * API for the view that conducts the search to toggle on the error message. + * @param {string} errorText + */ + setError(errorText) { + if (errorText) { + this.getInputField().addClass(CLASS_NAMES.errorInput); + const errorTextEl = this.getError(); + errorTextEl.html(errorText); + errorTextEl.show(); + } else { + this.clearError(); + } + }, + + /** + * Clear error text, remove error styling and hide the error element. + */ + clearError() { + this.getInputField().removeClass(CLASS_NAMES.errorInput); const errorTextEl = this.getError(); - errorTextEl.html(errorText); - errorTextEl.show(); - } else { + errorTextEl.hide(); + errorTextEl.html(''); + }, + + /** + * Handler function for the cancel icon button action. + */ + onCancel() { + this.hideCancelAndDimSearch(); + this.getInput().val(""); + this.onSearch(); + this.focus(); this.clearError(); - } - }, - - /** - * Clear error text, remove error styling and hide the error element. - */ - clearError() { - this.getInputField().removeClass(CLASS_NAMES.errorInput); - const errorTextEl = this.getError(); - errorTextEl.hide(); - errorTextEl.html(''); - }, - - /** - * Handler function for the cancel icon button action. - */ - onCancel() { - this.hideCancelAndDimSearch(); - this.getInput().val(""); - this.onSearch(); - this.focus(); - this.clearError(); - }, - - /** - * Focus the input field in this View. - */ - focus() { - this.getInput().trigger("focus"); - }, - - /** - * Blur the input field in this View. - */ - blur() { - this.getInput().trigger("blur"); - }, - - /** - * Get the search icon button. - * @return jQuery element representing the search icon button. Or an empty - * jQuery selector if the button is not found. - */ - getSearchButton() { - return this.$(`.${CLASS_NAMES.searchButton}`); - }, - - /** - * Get the cancel icon button container. - * @return jQuery element representing the cancel icon button container. Or - * an empty jQuery selector if the button is not found. - */ - getCancelButtonContainer() { - return this.$(`.${CLASS_NAMES.cancelButtonContainer}`); - }, - - /** - * Get the container element for the input. - * @return jQuery element representing the input container. Or an empty - * jQuery selector if the button is not found. - */ - getInputField() { - return this.$(`.${CLASS_NAMES.inputField}`); - }, - - /** - * Get the input. - * @return jQuery element representing the input. Or an empty - * jQuery selector if the button is not found. - */ - getInput() { - return this.$(`.${CLASS_NAMES.input}`); - }, - - /** - * Get the error text element. - * @return jQuery element representing the error text. Or an empty - * jQuery selector if the button is not found. - */ - getError() { - return this.$(`.${CLASS_NAMES.errorText}`); - }, - - /** - * Get the current value of the input field. - * @return The current value of the input field or empty string if the - * input field is not found. - */ - getInputValue() { - return this.getInput().val() || ''; - }, - - /** - * Set the current value of the input field. - */ - setInputValue(value) { - this.getInput().val(value); - }, - }); + }, + + /** + * Focus the input field in this View. + */ + focus() { + this.getInput().trigger("focus"); + }, + + /** + * Blur the input field in this View. + */ + blur() { + this.getInput().trigger("blur"); + }, + + /** + * Get the search icon button. + * @return jQuery element representing the search icon button. Or an empty + * jQuery selector if the button is not found. + */ + getSearchButton() { + return this.$(`.${CLASS_NAMES.searchButton}`); + }, + + /** + * Get the cancel icon button container. + * @return jQuery element representing the cancel icon button container. Or + * an empty jQuery selector if the button is not found. + */ + getCancelButtonContainer() { + return this.$(`.${CLASS_NAMES.cancelButtonContainer}`); + }, + + /** + * Get the container element for the input. + * @return jQuery element representing the input container. Or an empty + * jQuery selector if the button is not found. + */ + getInputField() { + return this.$(`.${CLASS_NAMES.inputField}`); + }, + + /** + * Get the input. + * @return jQuery element representing the input. Or an empty + * jQuery selector if the button is not found. + */ + getInput() { + return this.$(`.${CLASS_NAMES.input}`); + }, + + /** + * Get the error text element. + * @return jQuery element representing the error text. Or an empty + * jQuery selector if the button is not found. + */ + getError() { + return this.$(`.${CLASS_NAMES.errorText}`); + }, + + /** + * Get the current value of the input field. + * @return The current value of the input field or empty string if the + * input field is not found. + */ + getInputValue() { + return this.getInput().val() || ''; + }, + + /** + * Set the current value of the input field. + */ + setInputValue(value) { + this.getInput().val(value); + }, + }); // A function that does nothing. Can be safely called as a default callback. const noop = () => { }; diff --git a/src/js/views/maps/viewfinder/PredictionView.js b/src/js/views/maps/viewfinder/PredictionView.js index 6b014d5ee..9cbb3b3bf 100644 --- a/src/js/views/maps/viewfinder/PredictionView.js +++ b/src/js/views/maps/viewfinder/PredictionView.js @@ -16,7 +16,8 @@ define( * @since 2.28.0 * @constructs PredictionView */ - const PredictionView = Backbone.View.extend({ + const PredictionView = Backbone.View.extend( + /** @lends PredictionView.prototype */{ /** * The type of View this is * @type {string} diff --git a/src/js/views/maps/viewfinder/PredictionsListView.js b/src/js/views/maps/viewfinder/PredictionsListView.js index aa305b4b4..7bd4c4cf3 100644 --- a/src/js/views/maps/viewfinder/PredictionsListView.js +++ b/src/js/views/maps/viewfinder/PredictionsListView.js @@ -20,7 +20,8 @@ define( * @since 2.28.0 * @constructs PredictionsListView */ - var PredictionsListView = Backbone.View.extend({ + var PredictionsListView = Backbone.View.extend( + /** @lends PredictionsListView.prototype */{ /** * The type of View this is * @type {string} diff --git a/src/js/views/maps/viewfinder/ViewfinderView.js b/src/js/views/maps/viewfinder/ViewfinderView.js index 32d8f4c23..5bb25df78 100644 --- a/src/js/views/maps/viewfinder/ViewfinderView.js +++ b/src/js/views/maps/viewfinder/ViewfinderView.js @@ -42,35 +42,36 @@ define( * @since 2.28.0 * @constructs ViewfinderView */ - var ViewfinderView = Backbone.View.extend({ - /** - * The type of View this is - * @type {string} - */ - type: 'ViewfinderView', - - /** - * The HTML class to use for this view's outermost element. - * @type {string} - */ - className: BASE_CLASS, - - /** - * Values meant to be used by the rendered HTML template. - */ - templateVars: { - classNames: CLASS_NAMES, - }, - - /** - * @typedef {Object} ViewfinderViewOptions - * @property {Map} The Map model associated with this view allowing control - * of panning to different locations on the map. - */ - initialize({ model: mapModel }) { - this.viewfinderModel = new ViewfinderModel({ mapModel }); - this.panelsModel = new ExpansionPanelsModel({ isMulti: true }); - }, + var ViewfinderView = Backbone.View.extend( + /** @lends ViewfinderView.prototype */{ + /** + * The type of View this is + * @type {string} + */ + type: 'ViewfinderView', + + /** + * The HTML class to use for this view's outermost element. + * @type {string} + */ + className: BASE_CLASS, + + /** + * Values meant to be used by the rendered HTML template. + */ + templateVars: { + classNames: CLASS_NAMES, + }, + + /** + * @typedef {Object} ViewfinderViewOptions + * @property {Map} The Map model associated with this view allowing control + * of panning to different locations on the map. + */ + initialize({ model: mapModel }) { + this.viewfinderModel = new ViewfinderModel({ mapModel }); + this.panelsModel = new ExpansionPanelsModel({ isMulti: true }); + }, /** * Get the ZoomPresetsView element. @@ -81,13 +82,13 @@ define( return this.$el.find(`.${CLASS_NAMES.zoomPresetsView}`); }, - /** - * Get the SearchView element. - * @returns {JQuery} The SearchView element. - */ - getSearch() { - return this.$el.find(`.${CLASS_NAMES.searchView}`); - }, + /** + * Get the SearchView element. + * @returns {JQuery} The SearchView element. + */ + getSearch() { + return this.$el.find(`.${CLASS_NAMES.searchView}`); + }, /** * Helper function to focus input on the search query input and ensure @@ -119,33 +120,33 @@ define( }); expansionPanel.render(); - this.getZoomPresets().append(expansionPanel.el); - }, - - /** Render child SearchView and append to DOM. */ - renderSearchView() { - this.searchView = new SearchView({ - viewfinderModel: this.viewfinderModel, - }); - this.searchView.render(); - - this.getSearch().append(this.searchView.el); - }, - - /** - * Render the view by updating the HTML of the element. - * The new HTML is computed from an HTML template that - * is passed an object with relevant view state. - * */ - render() { - this.el.innerHTML = _.template(Template)(this.templateVars); - - this.renderSearchView(); - if (this.viewfinderModel.get('zoomPresets').length) { - this.renderZoomPresetsView(); - } - }, - }); + this.getZoomPresets().append(expansionPanel.el); + }, + + /** Render child SearchView and append to DOM. */ + renderSearchView() { + this.searchView = new SearchView({ + viewfinderModel: this.viewfinderModel, + }); + this.searchView.render(); + + this.getSearch().append(this.searchView.el); + }, + + /** + * Render the view by updating the HTML of the element. + * The new HTML is computed from an HTML template that + * is passed an object with relevant view state. + * */ + render() { + this.el.innerHTML = _.template(Template)(this.templateVars); + + this.renderSearchView(); + if (this.viewfinderModel.get('zoomPresets').length) { + this.renderZoomPresetsView(); + } + }, + }); return ViewfinderView; }); \ No newline at end of file diff --git a/src/js/views/metadata/EMLTempCoverageView.js b/src/js/views/metadata/EMLTempCoverageView.js index 1bae9778d..728048006 100644 --- a/src/js/views/metadata/EMLTempCoverageView.js +++ b/src/js/views/metadata/EMLTempCoverageView.js @@ -8,6 +8,7 @@ define(['underscore', 'jquery', 'backbone', * @class EMLTempCoverageView * @classdesc The EMLTempCoverage renders the content of an EMLTemporalCoverage model * @classcategory Views/Metadata + * @extends Backbone.View */ var EMLTempCoverageView = Backbone.View.extend( /** @lends EMLTempCoverageView.prototype */{ diff --git a/src/js/views/portals/PortalHeaderView.js b/src/js/views/portals/PortalHeaderView.js index 4029e0c67..c15c40294 100644 --- a/src/js/views/portals/PortalHeaderView.js +++ b/src/js/views/portals/PortalHeaderView.js @@ -8,6 +8,7 @@ define(["jquery", * @classdesc The PortalHeaderView is the view at the top of portal pages * that shows the portal's title, synopsis, and logo * @classcategory Views/Portals + * @extends Backbone.View */ var PortalHeaderView = Backbone.View.extend( /** @lends PortalHeaderView.prototype */{ diff --git a/src/js/views/portals/PortalVisualizationsView.js b/src/js/views/portals/PortalVisualizationsView.js index 89fe2e19b..39a0fb338 100644 --- a/src/js/views/portals/PortalVisualizationsView.js +++ b/src/js/views/portals/PortalVisualizationsView.js @@ -10,6 +10,7 @@ define(["jquery", * @classdesc The PortalVisualizationsView is a view to render the * portal visualizations tab (within PortalSectionView) * @classcategory Views/Portals + * @extends PortalSectionView */ var PortalVisualizationsView = PortalSectionView.extend( /** @lends PortalVisualizationsView.prototype */{ diff --git a/src/js/views/portals/editor/PortEditorDataView.js b/src/js/views/portals/editor/PortEditorDataView.js index b7b6a1dfc..85b879239 100644 --- a/src/js/views/portals/editor/PortEditorDataView.js +++ b/src/js/views/portals/editor/PortEditorDataView.js @@ -13,6 +13,7 @@ function( _, $, Backbone, FilterGroup, PortEditorSectionView, EditCollectionView /** * @class PortEditorDataView * @classcategory Views/Portals/Editor + * @extends PortEditorSectionView */ var PortEditorDataView = PortEditorSectionView.extend( /** @lends PortEditorDataView.prototype */{ diff --git a/src/js/views/portals/editor/PortEditorImageView.js b/src/js/views/portals/editor/PortEditorImageView.js index 1d0ad5667..9e00e1555 100644 --- a/src/js/views/portals/editor/PortEditorImageView.js +++ b/src/js/views/portals/editor/PortEditorImageView.js @@ -10,6 +10,7 @@ function(_, $, Backbone, PortalImage, ImageUploaderView, Template){ * @class PortEditorImageView * @classdesc A view that allows the user to upload an image as a DataONEObject * @classcategory Views/Portals/Editor + * @extends Backbone.View */ var PortEditorImageView = Backbone.View.extend( /** @lends PortEditorImageView.prototype */{ diff --git a/src/js/views/portals/editor/PortEditorLogosView.js b/src/js/views/portals/editor/PortEditorLogosView.js index 043d4d99e..6c7334835 100644 --- a/src/js/views/portals/editor/PortEditorLogosView.js +++ b/src/js/views/portals/editor/PortEditorLogosView.js @@ -8,6 +8,7 @@ function(_, $, Backbone, PortalImage, ImageEdit){ /** * @class PortEditorLogosView * @classcategory Views/Portals/Editor + * @extends Backbone.View */ var PortEditorLogosView = Backbone.View.extend( /** @lends PortEditorLogosView.prototype */{ diff --git a/src/js/views/portals/editor/PortEditorSettingsView.js b/src/js/views/portals/editor/PortEditorSettingsView.js index 43ad20291..b8fb182b1 100644 --- a/src/js/views/portals/editor/PortEditorSettingsView.js +++ b/src/js/views/portals/editor/PortEditorSettingsView.js @@ -11,6 +11,7 @@ function(_, $, Backbone, PortalSection, PortEditorSectionView, PortEditorLogosVi /** * @class PortEditorSettingsView * @classcategory Views/Portals/Editor + * @extends PortEditorSectionView */ var PortEditorSettingsView = PortEditorSectionView.extend( /** @lends PortEditorSettingsView.prototype */{