/*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;
});