/*global define */ define(['jquery', 'underscore', 'backbone', 'text!templates/bioportalAnnotationTemplate.html',], function($, _, Backbone, AnnotationPopoverTemplate) { 'use strict'; var AnnotationView = Backbone.View.extend( /** @lends AnnotationView.prototype */{ className: 'annotation-view', annotationPopoverTemplate: _.template(AnnotationPopoverTemplate), el: null, context: null, propertyLabel: null, propertyURI: null, valueLabel: null, valueURI: null, valueDefinition: null, valueOntology: null, valueOntologyName: null, // Stores a reference to the child .annotation el which is handy but // I'm no sure it's needed // TODO: Rename to popoverReference or something or remove entirley popoverSource: null, // Helps track visibility of the popover so we know when it's safe to // destroy it and update it with new content visible: null, // Stores whether we successfully looked and did or did not find the // definition of the annotation value, either from cache or from // Bioportal valueResolved: null, events: { "click" : "onClick", "click .annotation-popover-findmore" : "findMore", "click .annotation-findmore" : "findMore" }, initialize: function () { this.context = this.$el.data('context'); this.propertyLabel = this.$el.data('propertyLabel'); this.propertyURI = this.$el.data('propertyUri'); this.valueLabel = this.$el.data('valueLabel'); this.valueURI = this.$el.data('valueUri'); // 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(">", ">"); } this.valueResolved = false; }, render: function () { //If there is no value URI, then there is probably no annotation // metadata to render, so exit the function. if( typeof this.valueURI == "undefined" ){ return this; } this.createPopover(); return this; }, // Helps us fetch data from the API on first interaction onClick: function () { this.queryAndUpdateValue(); }, /** * Find a definition for the value URI either from cache or from * Bioportal. Updates the popover if necessary. */ queryAndUpdateValue: function () { if (this.valueResolved) { return; } var viewRef = this, cache = MetacatUI.appModel.get("bioportalLookupCache"), token = MetacatUI.appModel.get("bioportalAPIKey"); // Attempt to grab from cache first if (cache && cache[this.valueURI]) { this.valueDefinition = cache[this.valueURI].definition; this.valueOntology = cache[this.valueURI].links.ontology; // Try to get a simpler name for the ontology, rather than just // using the ontology URI, which is all Bioportal gives back this.valueOntologyName = this.getFriendlyOntologyName(cache[this.valueURI].links.ontology); this.updatePopover(); this.valueResolved = true; return; } // Verify token before moving on if (typeof token !== "string") { this.valueResolved = 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(this.valueURI) + "&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"] === viewRef.valueURI; }); // Verify structure of response looks right and bail out if it // doesn't if (!match || !match.definition || !match.definition.length || !match.definition.length > 0) { viewRef.valueResolved = true; viewRef.updatePopover(); return; } viewRef.valueDefinition = match.definition[0]; viewRef.valueOntology = 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 viewRef.valueOntologyName = viewRef.getFriendlyOntologyName(match.links.ontology); viewRef.valueResolved = true; viewRef.updatePopover(); viewRef.updateCache(viewRef.valueURI, match); }); }, /** * Create the Popover for the annotation * * Note: Has a side-effect of updating this.popoverSource; */ createPopover: function () { var new_content = this.annotationPopoverTemplate({ context: this.context, propertyLabel: this.propertyLabel, propertyURI: this.propertyURI, valueLabel: this.valueLabel, valueURI: this.valueURI, valueDefinition: this.valueDefinition, valueOntology: this.valueOntology, valueOntologyName: this.valueOntologyName, valueResolved: this.valueResolved }); this.$el.data("content", new_content); this.popoverSource = this.$el.popover({ container: this.$el, delay: 500, trigger: "click" }); }, /** * 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. */ updatePopover: function() { var popover_content = $(this.$el).find(".popover-content").first(); var new_content = this.annotationPopoverTemplate({ context: this.context, propertyLabel: this.propertyLabel, propertyURI: this.propertyURI, valueLabel: this.valueLabel, valueURI: this.valueURI, valueDefinition: this.valueDefinition, valueOntology: this.valueOntology, valueOntologyName: this.valueOntologyName, valueResolved: this.valueResolved }); // 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 = $(this.$el).data('popover'); if (popover_data && popover_data.options && popover_data.options) { popover_data.options.content = new_content; } $(this.$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 term: (string) The URI * @param match: (object) 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. * * This gets called either from the popover or from clicking on the pill * itself. */ findMore: function(e) { e.preventDefault(); var valueURI, valueLabel, pill = $(e.target).parents(".annotation"); // Decide whether we clicked from the pill first if (pill.length == 1) { valueURI = $(pill).data("value-uri"); valueLabel = $(pill).data("value-label"); } else { valueURI = $(e.target).data("uri"); valueLabel = $(e.target).text(); } // Bail out if we didn't get a valueURI to search if (typeof valueURI === "undefined") { return; } // Direct the user towards a search for the annotation MetacatUI.appSearchModel.clear(); MetacatUI.appSearchModel.set('annotation', [{ label: valueLabel, value: valueURI }]); MetacatUI.uiRouter.navigate('data', {trigger: true}); }, getFriendlyOntologyName: function(uri) { if ((typeof uri === "string")) { return uri; } return uri.replace("http://data.bioontology.org/ontologies/", ""); } }); return AnnotationView; });