/*global define */ define(['jquery', 'underscore', 'backbone', 'text!templates/bioportalAnnotationTemplate.html',], function($, _, Backbone, AnnotationPopoverTemplate) { 'use strict'; /** * @class AnnotationView * @classdesc A view of a single semantic annotation for a metadata field. It is usually displayed on the {@link MetadataView}. * @classcategory Views * @extends Backbone.View * @screenshot views/AnnotationView.png * @constructor */ var AnnotationView = Backbone.View.extend( /** @lends AnnotationView.prototype */{ className: 'annotation-view', annotationPopoverTemplate: _.template(AnnotationPopoverTemplate), el: null, events: { "click" : "handleClick", "click .annotation-popover-findmore" : "findMore", }, /** * Context string is a human-readable bit of text that comes out of the * Metacat view service and describes the context of the annotation * i.e., what entity, or which attribute within which entity the * annotation is on */ context: null, // State. See initialize(), we store a bunch of info in these property: null, value: null, initialize: function () { // Detect legacy pill DOM structure with the old arrow, // ┌───────────┬───────┬───┐ // │ property │ value │ ↗ │ // └───────────┴───────┴───┘ // clean up, and disable ourselves. This can be removed at some // point in the future if (this.$el.find(".annotation-findmore").length > 0) { this.$el.find(".annotation-findmore").remove(); this.$el.find(".annotation-value").attr("style", "color: white"); return; } this.property = { type: "property", el: null, popover: null, label: null, uri: null, definition: null, ontology: null, ontologyName: null, resolved: false }; this.value = { type: "value", el: null, popover: null, label: null, uri: null, definition: null, ontology: null, ontologyName: null, resolved: false }; this.property.el = this.$el.children(".annotation-property"); this.value.el = this.$el.children(".annotation-value"); // Bail now if things aren't set up right if (!this.property.el || !this.value.el) { return; } this.context = this.$el.data("context"); this.property.label = this.property.el.data("label"); this.property.uri = this.property.el.data("uri"); this.value.label = this.value.el.data("label"); this.value.uri = this.value.el.data("uri"); // Decode HTML tags in the context string, which is passed in as // an HTML attribute from the XSLT so it needs encoding of some sort // Note: Only supports < and > at this point if (this.context) { this.context = this.context.replace("<", "<").replace(">", ">"); } }, /** * Click handler for when the user clicks either the property or the * value portion of the pill. * * If the popover hasn't yet been created for either, we create the * popover and query BioPortal for more information. Otherwise, we do * nothing and Bootstrap's default popover handling is triggered, * showing the popover. * * @param {Event} e - Click event */ handleClick: function (e) { if (!this.property || !this.value) { return; } if (e.target.className === "annotation-property") { if (this.property.popover) { return; } this.createPopover(this.property); this.property.popover.popover("show"); this.queryAndUpdate(this.property); } else if (e.target.className === "annotation-value" || e.target.className === "annotation-value-text") { if (this.value.popover) { return; } this.createPopover(this.value); this.value.popover.popover("show"); this.queryAndUpdate(this.value); } }, /** * Update the value popover with the current state * * @param {Object} which - Which popover to create. Either this.property * or this.value. */ createPopover: function (which) { var new_content = this.annotationPopoverTemplate({ context: this.context, label: which.label, uri: which.uri, definition: which.definition, ontology: which.ontology, ontologyName: which.ontologyName, resolved: which.resolved, propertyURI: this.property.uri, propertyLabel: this.property.label, valueURI: this.value.uri, valueLabel: this.value.label }); which.el.data("content", new_content); which.popover = which.el.popover({ container: which.el, delay: 500, trigger: "click" }); }, /** * Find a definition for the value URI either from cache or from * Bioportal. Updates the popover if necessary. * * @param {Object} which - Which popover to create. Either this.property * or this.value. */ queryAndUpdate: function (which) { if (which.resolved) { return; } var viewRef = this, cache = MetacatUI.appModel.get("bioportalLookupCache"), token = MetacatUI.appModel.get("bioportalAPIKey"); // Attempt to grab from cache first if (cache && cache[which.uri]) { which.definition = cache[which.uri].definition; which.ontology = cache[which.uri].links.ontology; // Try to get a simpler name for the ontology, rather than just // using the ontology URI, which is all Bioportal gives back which.ontologyName = this.getFriendlyOntologyName(cache[which.uri].links.ontology); which.resolved = true; viewRef.updatePopover(which); return; } // Verify token before moving on if (typeof token !== "string" || token.length === 0) { which.resolved = true; return; } // Query the API and handle the response // TODO: Looks like we should proxy this so the token doesn't leak var url = MetacatUI.appModel.get("bioportalSearchUrl") + "?q=" + encodeURIComponent(which.uri) + "&apikey=" + token; $.get(url, function (data) { var match = null; // Verify response structure before trusting it if (!data.collection || !data.collection.length || !data.collection.length > 0) { return; } // Find the first match by URI match = _.find(data.collection, function(result) { return result["@id"] && result["@id"] === which.uri; }); // Verify structure of response looks right and bail out if it // doesn't if (!match || !match.definition || !match.definition.length || !match.definition.length > 0) { which.resolved = true; return; } which.definition = match.definition[0]; which.ontology = match.links.ontology; // Try to get a simpler name for the ontology, rather than just // using the ontology URI, which is all Bioportal gives back which.ontologyName = viewRef.getFriendlyOntologyName(match.links.ontology); which.resolved = true; viewRef.updateCache(which.uri, match); viewRef.updatePopover(which); }); }, /** * Update the popover data and raw HTML. This is necessary because * we want to create the popover before we fetch the data to populate * it from BioPortal and Bootstrap Popovers are designed to be static. * * The main trick I had to figure out here was that I could access * the underlying content member of the popover with * popover_data.options.content which wasn't documented in the API. * * @param {Object} which - Which popover to create. Either this.property * or this.value. */ updatePopover: function(which) { var popover_content = $(which.popover).find(".popover-content") var new_content = this.annotationPopoverTemplate({ context: this.context, label: which.label, uri: which.uri, definition: which.definition, ontology: which.ontology, ontologyName: which.ontologyName, resolved: which.resolved, propertyURI: which.uri, propertyLabel: which.label, valueURI: this.value.uri, valueLabel: this.value.label }); // Update both the existing DOM and the underlying data // attribute in order to persist the updated content between // displays of the popover // Update the Popover first // // This is a hack to work around the fact that we're updating the // content of the popover after it is created. I read the source // for Bootstrap's Popover and it showed the popover is generated // from the data-popover attribute's content which has an // options.content member we can modify directly var popover_data = $(which.el).data('popover'); if (popover_data && popover_data.options && popover_data.options) { popover_data.options.content = new_content; } $(which.el).data('popover', popover_data); // Then update the DOM on the open popover $(popover_content).html(new_content); }, /** * Update the cache for a given term. * * @param {string} term - The term URI * @param {Object} match - The BioPortal match object for the term */ updateCache: function(term, match) { var cache = MetacatUI.appModel.get("bioportalLookupCache"); if (cache && typeof term === "string" && typeof match === "string") { cache[term] = match; } }, /** * Send the user to a pre-canned search for a term. * * @param {Event} e - Click event */ findMore: function(e) { e.preventDefault(); // Find the URI we need to filter on. Try the value first var parent = $(e.target).parents(".annotation-value"); // Fall back to finding the URI from the property if (parent.length <= 0) { parent = $(e.target).parents(".annotation-property"); } // Bail if we found neither if (parent.length <= 0) { return; } // Now grab the label and URI and filter var label = $(parent).data("label"), uri = $(parent).data("uri"); if (!label || !uri) { return; } // Direct the user towards a search for the annotation MetacatUI.appSearchModel.clear(); MetacatUI.appSearchModel.set('annotation', [{ label: label, value: uri }]); MetacatUI.uiRouter.navigate('data', {trigger: true}); }, /** * Get a friendly name (ie ECSO) from a long BioPortal URI * * @param {string} uri - A URI returned from the BioPortal API * @return {string} */ getFriendlyOntologyName: function(uri) { if ((typeof uri === "string")) { return uri; } return uri.replace("http://data.bioontology.org/ontologies/", ""); } }); return AnnotationView; });