diff --git a/src/js/views/filters/SemanticFilterView.js b/src/js/views/filters/SemanticFilterView.js index 594693b65..63b4a57bd 100644 --- a/src/js/views/filters/SemanticFilterView.js +++ b/src/js/views/filters/SemanticFilterView.js @@ -1,143 +1,147 @@ /*global define */ -define(['jquery', - 'underscore', - 'backbone', - 'models/filters/Filter', - 'views/filters/FilterView', - 'views/searchSelect/AnnotationFilterView'], - function ($, _, Backbone, Filter, FilterView, AnnotationFilterView) { - 'use strict'; - - /** - * @class SemanticFilterView - * @classdesc Render a specialized view of a single Filter model using the - * AnnotationFilterView. - * @classcategory Views/Filters - * @extends FilterView - * @screenshot views/filters/SemanticFilterView.png - * @since 2.22.0 - */ - var SemanticFilterView = FilterView.extend( - /** @lends SemanticFilterView.prototype */{ - - /** - * @inheritdoc - */ - model: null, - - /** - * @inheritdoc - */ - modelClass: Filter, - - className: "filter semantic", - - // Template is an empty function because this view delegates to the - // AnnotationFilterView. See render() method. - template: function () { }, - - /** - * Render an instance of a Semantic Filter View. - * - * Note that this View doesn't have a template and instead delegates to - * the AnnotationFilterView which renders a SearchableSelectView which - * renders an NCBOTree. - * @since 2.22.0 - */ - render: function () { - - try { - var templateVars = this.model.toJSON(); - templateVars.id = this.model.cid; - - // Renders the template and inserts the FilterEditorView if the mode is uiBuilder - FilterView.prototype.render.call(this, templateVars); - - var viewOpts = { - "useSearchableSelect": true, - "placeholderText": templateVars.placeholder, - "inputLabel": null, // Hides label and uses FilterView label - "ontology": this.model.get("ontology"), - "startingRoot": this.model.get("startingRoot") - }; - - var subView = new AnnotationFilterView(viewOpts); - - this.$el.append(subView.el); - subView.render(); - - var view = this; - subView.on("annotationSelected", function (event, item) { - // Get the value of the associated input - var term = (!item || !item.value) ? input.val() : item.value; - var label = (!item || !item.filterLabel) ? null : item.filterLabel; - - // Set up a label mapping for the term so we can display a - // human-readable label for it in the UI - view.setLabelMapping(term, label); - - // Set the value, supports multiple values - var currentValue = view.model.get("values"); - var newValuesArray = _.flatten(new Array(currentValue, term)); - view.model.set("values", newValuesArray); - - view.defocus() - }); - } - catch (error) { - console.log('There was an error rendering a SemanticFilterView.' + - ' Error details: ' + error); - } - }, - - /** - * Helper function which defocuses the dropdown portion of the - * SearchableSelectView used by this View's AnnotationFilterView. When the - * user clicks an item in the NCBOTree widget, we want the - * SearchableSelectView's dropdown to go away and I couldn't find any API - * to do that so we have this code. See the render() method to see how it's - * called. - * - * Note: This isn't really a stable API and is really something we might - * remove in the future if we refactor the NCBOTree widget. - * @since 2.22.0 - */ - defocus: function () { - this.$el.find("div.menu").removeClass("visible").addClass("hidden") - this.$el.find("div.fluid.ui.dropdown").removeClass("active").removeClass("visible") - this.$el.find("input").blur() - }, - - /** - * Set the human-readable label for a term URI. - * - * For most uses of the Filter model, the value(s) set on the model can - * be shown directly in the UI. But for Semantic searches, we need to - * be able to display a human-readable label for the value because the - * value is likely an opaque URI. - * - * Rather than fetch and/or store all the possible labels for all - * possible URIs, we store a label for whichever terms the user chooses - * and keep that around until we need it in the UI. - * - * @param {string} term The term URI to set a label for - * @param {string} label The label to set - * @since 2.22.0 - */ - setLabelMapping: function (term, label) { - var newMappings; - - if (this.model.get("valueLabels")) { - newMappings = _.clone(this.model.get("valueLabels")); - } - else { - newMappings = new Object(); - } - - newMappings[term] = label; - this.model.set("valueLabels", newMappings); +define([ + "jquery", + "underscore", + "backbone", + "models/filters/Filter", + "views/filters/FilterView", + "views/searchSelect/AnnotationFilterView", +], function ($, _, Backbone, Filter, FilterView, AnnotationFilterView) { + "use strict"; + + /** + * @class SemanticFilterView + * @classdesc Render a specialized view of a single Filter model using the + * AnnotationFilterView. + * @classcategory Views/Filters + * @extends FilterView + * @screenshot views/filters/SemanticFilterView.png + * @since 2.22.0 + */ + var SemanticFilterView = FilterView.extend( + /** @lends SemanticFilterView.prototype */ { + /** + * @inheritdoc + */ + model: null, + + /** + * @inheritdoc + */ + modelClass: Filter, + + className: "filter semantic", + + // Template is an empty function because this view delegates to the + // AnnotationFilterView. See render() method. + template: function () {}, + + /** + * Render an instance of a Semantic Filter View. + * + * Note that this View doesn't have a template and instead delegates to + * the AnnotationFilterView which renders a SearchableSelectView which + * renders an NCBOTree. + * @since 2.22.0 + */ + render: function () { + try { + var templateVars = this.model.toJSON(); + templateVars.id = this.model.cid; + + // Renders the template and inserts the FilterEditorView if the mode is uiBuilder + FilterView.prototype.render.call(this, templateVars); + + var viewOpts = { + useSearchableSelect: true, + placeholderText: templateVars.placeholder, + inputLabel: null, // Hides label and uses FilterView label + ontology: this.model.get("ontology"), + startingRoot: this.model.get("startingRoot"), + }; + + var subView = new AnnotationFilterView(viewOpts); + + this.$el.append(subView.el); + subView.render(); + + var view = this; + subView.on("annotationSelected", function (event, item) { + // Get the value of the associated input + var term = !item || !item.value ? input.val() : item.value; + var label = !item || !item.filterLabel ? null : item.filterLabel; + + // Set up a label mapping for the term so we can display a + // human-readable label for it in the UI + view.setLabelMapping(term, label); + + // Set the value, supports multiple values + var currentValue = view.model.get("values"); + var newValuesArray = _.flatten(new Array(currentValue, term)); + view.model.set("values", newValuesArray); + + view.defocus(); + }); + } catch (error) { + console.log( + "There was an error rendering a SemanticFilterView." + + " Error details: " + + error + ); } - }); + }, + + /** + * Helper function which defocuses the dropdown portion of the + * SearchableSelectView used by this View's AnnotationFilterView. When the + * user clicks an item in the NCBOTree widget, we want the + * SearchableSelectView's dropdown to go away and I couldn't find any API + * to do that so we have this code. See the render() method to see how it's + * called. + * + * Note: This isn't really a stable API and is really something we might + * remove in the future if we refactor the NCBOTree widget. + * @since 2.22.0 + */ + defocus: function () { + this.$el.find("div.menu").removeClass("visible").addClass("hidden"); + this.$el + .find("div.fluid.ui.dropdown") + .removeClass("active") + .removeClass("visible"); + this.$el.find("input").blur(); + }, + + /** + * Set the human-readable label for a term URI. + * + * For most uses of the Filter model, the value(s) set on the model can + * be shown directly in the UI. But for Semantic searches, we need to + * be able to display a human-readable label for the value because the + * value is likely an opaque URI. + * + * Rather than fetch and/or store all the possible labels for all + * possible URIs, we store a label for whichever terms the user chooses + * and keep that around until we need it in the UI. + * + * @param {string} term The term URI to set a label for + * @param {string} label The label to set + * @since 2.22.0 + */ + setLabelMapping: function (term, label) { + var newMappings; + + if (this.model.get("valueLabels")) { + newMappings = _.clone(this.model.get("valueLabels")); + } else { + newMappings = new Object(); + } + + newMappings[term] = label; + this.model.set("valueLabels", newMappings); + }, + } + ); - return SemanticFilterView; - }); + return SemanticFilterView; +}); diff --git a/src/js/views/searchSelect/AnnotationFilterView.js b/src/js/views/searchSelect/AnnotationFilterView.js index 89114af07..826090425 100644 --- a/src/js/views/searchSelect/AnnotationFilterView.js +++ b/src/js/views/searchSelect/AnnotationFilterView.js @@ -1,99 +1,96 @@ -define( - [ - "jquery", - "underscore", - "backbone", - "bioportal", - ], - function( - $, _, Backbone, Bioportal, - ) { - - /** - * @class AnnotationFilter - * @classdesc A view that renders an annotation filter interface, which uses - * the bioportal tree search to select ontology terms. - * @classcategory Views/SearchSelect - * @extends Backbone.View - * @constructor - * @since 2.14.0 - * @screenshot views/searchSelect/AnnotationFilterView.png - */ - return Backbone.View.extend( - /** @lends AnnotationFilterView.prototype */ - { - /** - * The type of View this is - * @type {string} - */ - type: "AnnotationFilter", - - /** - * The HTML class names for this view element - * @type {string} - */ - className: "filter annotation-filter", - - /** - * The selector for the element that will show/hide the annotation - * popover interface when clicked. Searches within body. - * @type {string} - */ - popoverTriggerSelector: "", - - /** - * If set to true, instead of showing the annotation tree interface in - * a popover, show it in a multi-select input interface, which allows - * the user to select multiple annotations. - * @type {boolean} - */ - multiselect: false, - - /** - * If true, this filter will be added to the query but will - * act in the "background", like a default filter - * @type {boolean} - * @since 2.22.0 - */ - isInvisible: true, - - /** - * If set to true, instead of showing the annotation tree interface in - * a popover, show it on the custom search filter interface, which allows - * the user to filter search based on the annotations. - * @type {boolean} - * @since 2.22.0 - */ - useSearchableSelect: false, - - /** - * The acronym of the ontology or ontologies to render a tree from. - * - * Must be an ontology that's present on BioPortal. - * - * TODO: Test out comma-separated lists. How does that render? - * @type {string} - * @since 2.22.0 - */ - defaultOntology: "ECSO", - - /** - * The URL that indicates the concept where the tree should start - * @type {string} - */ - defaultStartingRoot: "http://ecoinformatics.org/oboe/oboe.1.2/oboe-core.owl#MeasurementType", - - /** - * Creates a new AnnotationFilterView - * @param {Object} options - A literal object with options to pass to the view - */ - initialize: function(options) { - try { - - // Get all the options and apply them to this view - if (typeof options == "object") { - var optionKeys = Object.keys(options); - _.each(optionKeys, function(key, i) { +define(["jquery", "underscore", "backbone", "bioportal"], function ( + $, + _, + Backbone, + Bioportal +) { + /** + * @class AnnotationFilter + * @classdesc A view that renders an annotation filter interface, which uses + * the bioportal tree search to select ontology terms. + * @classcategory Views/SearchSelect + * @extends Backbone.View + * @constructor + * @since 2.14.0 + * @screenshot views/searchSelect/AnnotationFilterView.png + */ + return Backbone.View.extend( + /** @lends AnnotationFilterView.prototype */ + { + /** + * The type of View this is + * @type {string} + */ + type: "AnnotationFilter", + + /** + * The HTML class names for this view element + * @type {string} + */ + className: "filter annotation-filter", + + /** + * The selector for the element that will show/hide the annotation + * popover interface when clicked. Searches within body. + * @type {string} + */ + popoverTriggerSelector: "", + + /** + * If set to true, instead of showing the annotation tree interface in + * a popover, show it in a multi-select input interface, which allows + * the user to select multiple annotations. + * @type {boolean} + */ + multiselect: false, + + /** + * If true, this filter will be added to the query but will + * act in the "background", like a default filter + * @type {boolean} + * @since 2.22.0 + */ + isInvisible: true, + + /** + * If set to true, instead of showing the annotation tree interface in + * a popover, show it on the custom search filter interface, which allows + * the user to filter search based on the annotations. + * @type {boolean} + * @since 2.22.0 + */ + useSearchableSelect: false, + + /** + * The acronym of the ontology or ontologies to render a tree from. + * + * Must be an ontology that's present on BioPortal. + * + * TODO: Test out comma-separated lists. How does that render? + * @type {string} + * @since 2.22.0 + */ + defaultOntology: "ECSO", + + /** + * The URL that indicates the concept where the tree should start + * @type {string} + */ + defaultStartingRoot: + "http://ecoinformatics.org/oboe/oboe.1.2/oboe-core.owl#MeasurementType", + + /** + * Creates a new AnnotationFilterView + * @param {Object} options - A literal object with options to pass to the view + */ + initialize: function (options) { + try { + // Get all the options and apply them to this view + if (typeof options == "object") { + var optionKeys = Object.keys(options); + _.each( + optionKeys, + function (key, i) { // Only override non-null values so we can pass in nulls and // still trigger default behavior if (typeof options[key] === "undefined") { @@ -101,280 +98,313 @@ define( } this[key] = options[key]; - }, this); - } - - // Mix in defaults if needed - if (!this.ontology) { - this.ontology = this.defaultOntology; - this.startingRoot = this.defaultStartingRoot; - } - - } catch (e) { - console.log("Failed to initialize an Annotation Filter View, error message:", e); + }, + this + ); } - }, - - /** - * render - Render the view - * - * @return {AnnotationFilter} Returns the view - */ - render: function() { - try { - - if(!MetacatUI.appModel.get("bioportalAPIKey")){ - console.log("A bioportal key is required for the Annotation Filter View. Please set a key in the MetacatUI config. The view will not render."); - return - } - - var view = this; - if(view.multiselect || view.useSearchableSelect){ - view.createMultiselect() - } else { - view.setUpTree() - view.createPopoverHTML() - view.setListeners() - } - - return this - - } catch (e) { - console.log("Failed to render an Annotation Filter View, error message: " + e); + // Mix in defaults if needed + if (!this.ontology) { + this.ontology = this.defaultOntology; + this.startingRoot = this.defaultStartingRoot; } - }, - - /** - * setUpTree - Create the HTML for the annotation tree - */ - setUpTree: function() { - - try { - - var view = this; - - view.treeEl = $('
').NCBOTree({ - apikey: MetacatUI.appModel.get("bioportalAPIKey"), - ontology: view.ontology, - width: "400", - startingRoot: view.startingRoot - }); - - // Make an element that contains the tree and reset/jumpUp buttons - var buttonProps = "data-trigger='hover' data-placement='top' data-container='body' style='margin-right: 3px'" - view.treeContent = $("
"); - view.buttonContainer = $('
'); - view.jumpUpButton = $(""); - view.resetButton = $(""); - $(view.buttonContainer).append(view.jumpUpButton); - $(view.buttonContainer).append(view.resetButton); - $(view.treeContent).append(view.buttonContainer); - $(view.treeContent).append(view.treeEl); - - } catch (e) { - console.log("Failed to set up an annotation tree, error message: " + e); + } catch (e) { + console.log( + "Failed to initialize an Annotation Filter View, error message:", + e + ); + } + }, + + /** + * render - Render the view + * + * @return {AnnotationFilter} Returns the view + */ + render: function () { + try { + if (!MetacatUI.appModel.get("bioportalAPIKey")) { + console.log( + "A bioportal key is required for the Annotation Filter View. Please set a key in the MetacatUI config. The view will not render." + ); + return; } - }, - - /** - * createMultiselect - Create a searchable multi-select interface - * that includes an annotation filter tree. - */ - createMultiselect: function(){ - - try { - var view = this; - - require(["views/searchSelect/SearchableSelectView"], function(SearchableSelect){ - - view.multiSelectView = new SearchableSelect({ - placeholderText: view.placeholderText ? view.placeholderText : "Search for or select a value", - icon: view.icon, - separatorText: view.separatorText, - inputLabel: view.inputLabel - }) - view.$el.append(view.multiSelectView.el); - view.multiSelectView.render(); - // If there are pre-selected values, get the user-facing labels - // and then update the multiselect - if(view.selected && view.selected.length){ - view.getClassLabels.call(view, view.updateMultiselect); - } else { - // Otherwise, update the multi-select right away with tree element - view.updateMultiselect.call(view) - } - - //Forward the separatorChanged event from the SearchableSelectView to this AnnotationFilterView - //(perhaps this view should have been a subclass?) - view.multiSelectView.on("separatorChanged", (separatorText) => { - view.trigger("separatorChanged", separatorText) - }) - }) - } catch (e) { - console.log("Failed to create the multi-select interface for an Annotation Filter View, error message: " + e); - } - }, - - /** - * updateMultiselect - Functions to run once a SearchableSelect view has - * been rendered and inserted into this view, and the labels for any - * pre-selected annotation values have been fetched. Updates the - * hidden menu of items and the selected items. - */ - updateMultiselect: function(){ - - try { - var view = this; - - // Check if this is the first time we are updating this multiselect. - // If it is, then don't trigger the event that updates the model, - // because nothing has changed. - if(view.updateMultiselectTimes === undefined){ - view.updateMultiselectTimes = 0 - } else { - view.updateMultiselectTimes++ - } + var view = this; - // Re-init the tree + if (view.multiselect || view.useSearchableSelect) { + view.createMultiselect(); + } else { view.setUpTree(); - - // Re-render the multiselect menu with the new options. These options - // will be hidden from view, but they must be present in the DOM for - // the multi-select interface to function correctly. - // Add an empty item to the list of selected values, so that - // the dropdown menu is always expandable. - if(view.options === undefined){ - view.options = [] - } - view.options.push({value:""}); - view.multiSelectView.options = view.options; - view.multiSelectView.updateMenu(); - // Make sure the new menu is attached before updating list of selected - // annotations - setTimeout(function () { - var silent = view.updateMultiselectTimes === 0; - var newValues = _.reject(view.selected, function(val){ return val === "" }); - view.multiSelectView.changeSelection(newValues, silent); - }, 25); - - // Add the annotation tree to the menu content - view.multiSelectView.$el.find(".menu").append(view.treeContent); - view.searchInput = view.multiSelectView.$selectUI.find("input"); - - // Simulate a search in the annotation tree when the user - // searches in the multiSelect interface - view.searchInput.on("keyup", function(e){ - var treeInput = view.treeContent.find("input.ncboAutocomplete"); - treeInput.val(e.target.value).keydown(); - }); - + view.createPopoverHTML(); view.setListeners(); - - } catch (e) { - console.log("Failed to update an annotation filter with selected values, error message: " + e); } - }, - - /** - * getClassLabels - Given an array of bioontology IDs set in - * view.selected, query the bioontology API to find the user-friendly - * labels (prefLabels) - * - * @param {function} callback A function to call once the labels have - * been found (or not). The function will be called with the formatted - * response: an array with an object for each ID with the properties - * value (the original ID) and label (the user-friendly label, or the - * value again if no label was found) - */ - getClassLabels: function(callback){ - - try { - var view = this; - - if(!view.selected || !view.selected.length){ - return + return this; + } catch (e) { + console.log( + "Failed to render an Annotation Filter View, error message: " + e + ); + } + }, + + /** + * setUpTree - Create the HTML for the annotation tree + */ + setUpTree: function () { + try { + var view = this; + + view.treeEl = $('
').NCBOTree({ + apikey: MetacatUI.appModel.get("bioportalAPIKey"), + ontology: view.ontology, + width: "400", + startingRoot: view.startingRoot, + }); + + // Make an element that contains the tree and reset/jumpUp buttons + var buttonProps = + "data-trigger='hover' data-placement='top' data-container='body' style='margin-right: 3px'"; + view.treeContent = $("
"); + view.buttonContainer = $( + '
' + ); + view.jumpUpButton = $( + "" + ); + view.resetButton = $( + "" + ); + $(view.buttonContainer).append(view.jumpUpButton); + $(view.buttonContainer).append(view.resetButton); + $(view.treeContent).append(view.buttonContainer); + $(view.treeContent).append(view.treeEl); + } catch (e) { + console.log( + "Failed to set up an annotation tree, error message: " + e + ); + } + }, + + /** + * createMultiselect - Create a searchable multi-select interface + * that includes an annotation filter tree. + */ + createMultiselect: function () { + try { + var view = this; + + require(["views/searchSelect/SearchableSelectView"], function ( + SearchableSelect + ) { + view.multiSelectView = new SearchableSelect({ + placeholderText: view.placeholderText + ? view.placeholderText + : "Search for or select a value", + icon: view.icon, + separatorText: view.separatorText, + inputLabel: view.inputLabel, + }); + view.$el.append(view.multiSelectView.el); + view.multiSelectView.render(); + // If there are pre-selected values, get the user-facing labels + // and then update the multiselect + if (view.selected && view.selected.length) { + view.getClassLabels.call(view, view.updateMultiselect); + } else { + // Otherwise, update the multi-select right away with tree element + view.updateMultiselect.call(view); } - const ontologyCollection = _.map(view.selected, function(id){ - return { - "class" : id, - "ontology": "http://data.bioontology.org/ontologies/" + view.ontology - } + //Forward the separatorChanged event from the SearchableSelectView to this AnnotationFilterView + //(perhaps this view should have been a subclass?) + view.multiSelectView.on("separatorChanged", (separatorText) => { + view.trigger("separatorChanged", separatorText); }); + }); + } catch (e) { + console.log( + "Failed to create the multi-select interface for an Annotation Filter View, error message: " + + e + ); + } + }, + + /** + * updateMultiselect - Functions to run once a SearchableSelect view has + * been rendered and inserted into this view, and the labels for any + * pre-selected annotation values have been fetched. Updates the + * hidden menu of items and the selected items. + */ + updateMultiselect: function () { + try { + var view = this; + + // Check if this is the first time we are updating this multiselect. + // If it is, then don't trigger the event that updates the model, + // because nothing has changed. + if (view.updateMultiselectTimes === undefined) { + view.updateMultiselectTimes = 0; + } else { + view.updateMultiselectTimes++; + } - const bioData = JSON.stringify({ - "http://www.w3.org/2002/07/owl#Class": { - "collection": ontologyCollection, - "display": "prefLabel" - } + // Re-init the tree + view.setUpTree(); + + // Re-render the multiselect menu with the new options. These options + // will be hidden from view, but they must be present in the DOM for + // the multi-select interface to function correctly. + // Add an empty item to the list of selected values, so that + // the dropdown menu is always expandable. + if (view.options === undefined) { + view.options = []; + } + view.options.push({ value: "" }); + view.multiSelectView.options = view.options; + view.multiSelectView.updateMenu(); + // Make sure the new menu is attached before updating list of selected + // annotations + setTimeout(function () { + var silent = view.updateMultiselectTimes === 0; + var newValues = _.reject(view.selected, function (val) { + return val === ""; }); + view.multiSelectView.changeSelection(newValues, silent); + }, 25); + + // Add the annotation tree to the menu content + view.multiSelectView.$el.find(".menu").append(view.treeContent); + view.searchInput = view.multiSelectView.$selectUI.find("input"); + + // Simulate a search in the annotation tree when the user + // searches in the multiSelect interface + view.searchInput.on("keyup", function (e) { + var treeInput = view.treeContent.find("input.ncboAutocomplete"); + treeInput.val(e.target.value).keydown(); + }); + + view.setListeners(); + } catch (e) { + console.log( + "Failed to update an annotation filter with selected values, error message: " + + e + ); + } + }, + + /** + * getClassLabels - Given an array of bioontology IDs set in + * view.selected, query the bioontology API to find the user-friendly + * labels (prefLabels) + * + * @param {function} callback A function to call once the labels have + * been found (or not). The function will be called with the formatted + * response: an array with an object for each ID with the properties + * value (the original ID) and label (the user-friendly label, or the + * value again if no label was found) + */ + getClassLabels: function (callback) { + try { + var view = this; + + if (!view.selected || !view.selected.length) { + return; + } - const formatResponse = function(response, success){ - if(view.options === undefined){ - view.options = [] - } - view.selected.forEach(function(item,index){ - if(success){ - var match = _.findWhere(response[Object.keys(response)[0]], { "@id": item }); - } else { - var match = null; - } - view.options[index] = { - value: item, - label: match ? match.prefLabel : item - } - }) + const ontologyCollection = _.map(view.selected, function (id) { + return { + class: id, + ontology: + "http://data.bioontology.org/ontologies/" + view.ontology, + }; + }); + + const bioData = JSON.stringify({ + "http://www.w3.org/2002/07/owl#Class": { + collection: ontologyCollection, + display: "prefLabel", + }, + }); + + const formatResponse = function (response, success) { + if (view.options === undefined) { + view.options = []; } - - // Get the pre-selected values - $.ajax({ - type: "POST", - url: "http://data.bioontology.org/batch?display_context=false", - headers: { - "Authorization" : "apikey token=" + MetacatUI.appModel.get("bioportalAPIKey"), - "Accept" : "application/json", - "Content-Type" : "application/json" - }, - processData: false, - data: bioData, - crossDomain: true, - timeout: 5000, - success: function(response) { - formatResponse(response, true) - callback.call(view) - }, - error: function(response) { - console.log("Error finding class labels for the Annotation Filter, error response:", response); - formatResponse(response, false) - callback.call(view) + view.selected.forEach(function (item, index) { + if (success) { + var match = _.findWhere(response[Object.keys(response)[0]], { + "@id": item, + }); + } else { + var match = null; } + view.options[index] = { + value: item, + label: match ? match.prefLabel : item, + }; }); - } catch (e) { - console.log("Failed to fetch labels for bioontology IDs, error message: " + e); - } - - }, - - /** - * createPopoverHTML - Create the HTML for annotation filters that are - * displayed as a popup (e.g. in the search catalog) - * - * @return {type} description - */ - createPopoverHTML: function(){ - try { - var view = this; - $("body").append($('
')); - $(view.popoverTriggerSelector).popover({ + }; + + // Get the pre-selected values + $.ajax({ + type: "POST", + url: "http://data.bioontology.org/batch?display_context=false", + headers: { + Authorization: + "apikey token=" + MetacatUI.appModel.get("bioportalAPIKey"), + Accept: "application/json", + "Content-Type": "application/json", + }, + processData: false, + data: bioData, + crossDomain: true, + timeout: 5000, + success: function (response) { + formatResponse(response, true); + callback.call(view); + }, + error: function (response) { + console.log( + "Error finding class labels for the Annotation Filter, error response:", + response + ); + formatResponse(response, false); + callback.call(view); + }, + }); + } catch (e) { + console.log( + "Failed to fetch labels for bioontology IDs, error message: " + e + ); + } + }, + + /** + * createPopoverHTML - Create the HTML for annotation filters that are + * displayed as a popup (e.g. in the search catalog) + * + * @return {type} description + */ + createPopoverHTML: function () { + try { + var view = this; + $("body").append( + $('
') + ); + $(view.popoverTriggerSelector) + .popover({ html: true, placement: "bottom", trigger: "manual", content: view.treeContent, - container: "#bioportal-popover" - }).on("click", function() { + container: "#bioportal-popover", + }) + .on("click", function () { if ($($(this).data().popover.options.content).is(":visible")) { // Detach the tree from the popover so it doesn't get removed by Bootstrap $(this).data().popover.options.content.detach(); @@ -382,11 +412,12 @@ define( $(this).popover("hide"); } else { // Get the popover content - var content = $(this).data().popoverContent || + var content = + $(this).data().popoverContent || $(this).data().popover.options.content.detach(); // Cache it $(this).data({ - popoverContent: content + popoverContent: content, }); // Show the popover $(this).popover("show"); @@ -397,228 +428,249 @@ define( $(".tooltip-this").tooltip(); } }); - } catch (e) { - console.log("Failed to create popover HTML for an annotation filter, error message: " + e); - } - }, - - /** - * setListeners - Sets listeners on the tree elements. Must be run - * after the tree HTML is created. - */ - setListeners: function(){ - try { - var view = this; - view.treeEl.off(); - view.jumpUpButton.off(); - view.resetButton.off(); - view.treeEl.on("afterSelect", function(event, classId, prefLabel, selectedNode) { - view.selectConcept.call(view, event, classId, prefLabel, selectedNode) - }); - view.treeEl.on("afterJumpToClass", function(event, classId) { - view.afterJumpToClass.call(view, event, classId); - }); - view.treeEl.on("afterExpand", function() { - view.afterExpand.call(view) - }); - view.jumpUpButton.on("click", function(){ - view.jumpUp.call(view); - }); - view.resetButton.on("click", function(){ - view.resetTree.call(view); + } catch (e) { + console.log( + "Failed to create popover HTML for an annotation filter, error message: " + + e + ); + } + }, + + /** + * setListeners - Sets listeners on the tree elements. Must be run + * after the tree HTML is created. + */ + setListeners: function () { + try { + var view = this; + view.treeEl.off(); + view.jumpUpButton.off(); + view.resetButton.off(); + view.treeEl.on( + "afterSelect", + function (event, classId, prefLabel, selectedNode) { + view.selectConcept.call( + view, + event, + classId, + prefLabel, + selectedNode + ); + } + ); + view.treeEl.on("afterJumpToClass", function (event, classId) { + view.afterJumpToClass.call(view, event, classId); + }); + view.treeEl.on("afterExpand", function () { + view.afterExpand.call(view); + }); + view.jumpUpButton.on("click", function () { + view.jumpUp.call(view); + }); + view.resetButton.on("click", function () { + view.resetTree.call(view); + }); + if (view.multiselect) { + view.treeEl.off("searchItemSelected"); + view.treeEl.on("searchItemSelected", function () { + view.searchInput.val(""); }); - if(view.multiselect){ - view.treeEl.off("searchItemSelected"); - view.treeEl.on("searchItemSelected", function(){ - view.searchInput.val("") - }); - view.stopListening(view.multiSelectView, "changeSelection"); - view.listenTo(view.multiSelectView, "changeSelection", function(newValues){ + view.stopListening(view.multiSelectView, "changeSelection"); + view.listenTo( + view.multiSelectView, + "changeSelection", + function (newValues) { // When values are removed, update the interface - if(newValues != view.selected){ + if (newValues != view.selected) { view.selected = newValues; // So that the function doesn't trigger an endless loop delete view.updateMultiselectTimes; - view.updateMultiselect() + view.updateMultiselect(); } view.trigger("changeSelection", newValues); - }) - } - } catch (e) { - console.log("Failed to set listeners in an Annotation Filter View, error message: " + e); + } + ); } - }, - - /** - * selectConcept - Actions that are performed after the user selects - * a concept from the annotation tree interface. Triggers an event for - * any parent views, hides and resets the annotation popup. - * - * @param {object} event The "afterSelect" event - * @param {string} classId The ID for the selected concept (a URL) - * @param {string} prefLabel The label for the selected concept - * @param {jQuery} selectedNode The element that was clicked - */ - selectConcept: function(event, classId, prefLabel, selectedNode) { - - try { - - var view = this; - - // Get the concept info - var item = { - value: classId, - label: prefLabel, - filterLabel: prefLabel, - desc: "" - } - - // Trigger an event so that the parent view can update filters, etc. - view.trigger("annotationSelected", event, item); - - // Hide the popover - if(!view.multiselect){ - var annotationFilterEl = $(view.popoverTriggerSelector); - annotationFilterEl.trigger("click"); - $(selectedNode).trigger("mouseout"); - view.resetTree(); + } catch (e) { + console.log( + "Failed to set listeners in an Annotation Filter View, error message: " + + e + ); + } + }, + + /** + * selectConcept - Actions that are performed after the user selects + * a concept from the annotation tree interface. Triggers an event for + * any parent views, hides and resets the annotation popup. + * + * @param {object} event The "afterSelect" event + * @param {string} classId The ID for the selected concept (a URL) + * @param {string} prefLabel The label for the selected concept + * @param {jQuery} selectedNode The element that was clicked + */ + selectConcept: function (event, classId, prefLabel, selectedNode) { + try { + var view = this; + + // Get the concept info + var item = { + value: classId, + label: prefLabel, + filterLabel: prefLabel, + desc: "", + }; + + // Trigger an event so that the parent view can update filters, etc. + view.trigger("annotationSelected", event, item); + + // Hide the popover + if (!view.multiselect) { + var annotationFilterEl = $(view.popoverTriggerSelector); + annotationFilterEl.trigger("click"); + $(selectedNode).trigger("mouseout"); + view.resetTree(); // Update the multi-select with the new options - } else { - view.options.push(item); - view.selected.push(item.value); - view.updateMultiselect(); - } - - // Ensure tooltips are removed - $("body > .tooltip").remove(); - - // Prevent default action - return false; - - } catch (e) { - console.log("Failed to select an annotation concept, error message: " + e); - } - - }, - - /** - * afterExpand - Actions to perform when the user expands a concept in - * the tree - */ - afterExpand: function() { - try { - // Ensure tooltips are activated - $(".tooltip-this").tooltip(); - } catch (e) { - console.log("Failed to initialize tooltips in the annotation filter, error message: " + e); - } - }, - - /** - * afterJumpToClass - Called when a user searches for and selects a - * concept from the search results - * - * @param {type} event The jump to class event - * @param {type} classId The ID for the selected concept (a URL) - */ - afterJumpToClass: function(event, classId) { - - try { - var view = this; - // Re-root the tree at this concept - var tree = view.treeEl.data("NCBOTree"); - var options = tree.options(); - $.extend(options, { - startingRoot: classId - }); - - // Force a re-render - tree.init(); - - // Ensure the tooltips are activated - $(".tooltip-this").tooltip(); - - } catch (e) { - console.log("Failed to re-render the annotation filter after jump to class, error message: " + e); + } else { + view.options.push(item); + view.selected.push(item.value); + view.updateMultiselect(); } - }, - - /** - * jumpUp - Jumps up to the parent concept in the UI - * - * @return {boolean} Returns false - */ - jumpUp: function() { - - try { - // Re-root the tree at the parent concept of the root - var view = this, - tree = view.treeEl.data("NCBOTree"), - options = tree.options(), - startingRoot = options.startingRoot; - - if (startingRoot == view.startingRoot) { - return false; - } - - var parentId = $("a[data-id='" + encodeURIComponent(startingRoot) + "'").attr("data-subclassof"); - - // Re-root - $.extend(options, { - startingRoot: parentId - }); - - // Force a re-render - tree.init(); - - // Ensure the tooltips are activated - $(".tooltip-this").tooltip(); - - return false; - - } catch (e) { - console.log("Failed to jump to parent concept in the annotation filter, error message: " + e); - } - - }, - - /** - * resetTree - Collapse all expanded concepts - * - * @return {boolean} Returns false - */ - resetTree: function() { - - try { - - var view = this; - - // Re-root the tree at the original concept - var tree = view.treeEl.data("NCBOTree"); - - var options = tree.options(); - - // Re-root - $.extend(options, { - startingRoot: view.startingRoot - }); - - tree.changeOntology(view.ontology); - - // Force a re-render - tree.init(); - - // Ensure the tooltips are activated - $(".tooltip-this").tooltip(); - + // Ensure tooltips are removed + $("body > .tooltip").remove(); + + // Prevent default action + return false; + } catch (e) { + console.log( + "Failed to select an annotation concept, error message: " + e + ); + } + }, + + /** + * afterExpand - Actions to perform when the user expands a concept in + * the tree + */ + afterExpand: function () { + try { + // Ensure tooltips are activated + $(".tooltip-this").tooltip(); + } catch (e) { + console.log( + "Failed to initialize tooltips in the annotation filter, error message: " + + e + ); + } + }, + + /** + * afterJumpToClass - Called when a user searches for and selects a + * concept from the search results + * + * @param {type} event The jump to class event + * @param {type} classId The ID for the selected concept (a URL) + */ + afterJumpToClass: function (event, classId) { + try { + var view = this; + // Re-root the tree at this concept + var tree = view.treeEl.data("NCBOTree"); + var options = tree.options(); + $.extend(options, { + startingRoot: classId, + }); + + // Force a re-render + tree.init(); + + // Ensure the tooltips are activated + $(".tooltip-this").tooltip(); + } catch (e) { + console.log( + "Failed to re-render the annotation filter after jump to class, error message: " + + e + ); + } + }, + + /** + * jumpUp - Jumps up to the parent concept in the UI + * + * @return {boolean} Returns false + */ + jumpUp: function () { + try { + // Re-root the tree at the parent concept of the root + var view = this, + tree = view.treeEl.data("NCBOTree"), + options = tree.options(), + startingRoot = options.startingRoot; + + if (startingRoot == view.startingRoot) { return false; - } catch (e) { - console.log("Failed to reset the annotation filter tree, error message: " + e); } - }, - - }); - }); + var parentId = $( + "a[data-id='" + encodeURIComponent(startingRoot) + "'" + ).attr("data-subclassof"); + + // Re-root + $.extend(options, { + startingRoot: parentId, + }); + + // Force a re-render + tree.init(); + + // Ensure the tooltips are activated + $(".tooltip-this").tooltip(); + + return false; + } catch (e) { + console.log( + "Failed to jump to parent concept in the annotation filter, error message: " + + e + ); + } + }, + + /** + * resetTree - Collapse all expanded concepts + * + * @return {boolean} Returns false + */ + resetTree: function () { + try { + var view = this; + + // Re-root the tree at the original concept + var tree = view.treeEl.data("NCBOTree"); + + var options = tree.options(); + + // Re-root + $.extend(options, { + startingRoot: view.startingRoot, + }); + + tree.changeOntology(view.ontology); + + // Force a re-render + tree.init(); + + // Ensure the tooltips are activated + $(".tooltip-this").tooltip(); + + return false; + } catch (e) { + console.log( + "Failed to reset the annotation filter tree, error message: " + e + ); + } + }, + } + ); +});