a53b9958fa734f73aeffb9ddfe2fbad1ca65f90c
galt
Mon Jan 30 16:18:41 2017 -0800
Check-in of CSP2 Content-Security-Policy work. All C-language CGIs should now support CSP2 in browser to stop major forms of XSS javascript injection. Javascript on pages is gathered together, and then emitted in a single script block at the end with a nonce that tells the browser, this is js that we generated instead of being injected by a hacker. Both inline script from script blocks and inline js event handlers had to be pulled out and separated. You will not see js sprinkled through-out the page now. Older browsers that support CSP1 or that do not understand CSP at all will still work, just without protection. External js libraries loaded at runtime need to be added to the CSP policy header in src/lib/htmshell.c.
diff --git src/hg/js/hgGateway.js src/hg/js/hgGateway.js
index 59e18c0..8c3aa9b 100644
--- src/hg/js/hgGateway.js
+++ src/hg/js/hgGateway.js
@@ -1,1690 +1,1692 @@
// hgGateway - autocomplete + graphical interface to select species, assembly & position.
// Copyright (C) 2016 The Regents of the University of California
// Several modules are defined in this file -- if some other code needs them someday,
// they can be moved out to lib files.
// function svgCreateEl: convenience function for creating new SVG elements, used by
// speciesTree and rainbow modules.
// speciesTree: module that exports draw() function for drawing a phylogenetic tree
// (and list of hubs above the tree, if any) in a pre-existing SVG element -- see
// hg/hgGateway/hgGateway.html.
// rainbow: module that exports draw() function and colors. draw() adds stripes using
// a spectrum of colors that are associated to species groups. The hgGateway view code
// uses coordinates of stripes within the tree image to create a corresponding "rainbow"
// slider bar to the left of the phylogenetic tree container.
// autocompleteCat: customized JQuery autocomplete plugin that includes watermark and
// can display results broken down by category (for example, genomes from various
// assembly hubs and native genomes).
// hgGateway: module of mostly view/controller code (model state comes directly from server).
// Globals (pragma for jshint):
/* globals dbDbTree, activeGenomes, surveyLink, surveyLabel, surveyLabelImage, cart */
/* globals calculateHgTracksWidth */ // function is defined in utils.js
+window.hgsid = '';
+window.activeGenomes = {};
+window.surveyLink=null;
+window.surveyLabel=null;
+window.surveyLabelImage=null;
+
function svgCreateEl(type, config) {
// Helper function for creating a new SVG element and initializing its
// properties and attributes. Type is something like 'rect', 'text', 'g', etc;
// config is an object like { id: 'newThingie', x: 0, y: 10, title: 'blah blah' }.
var svgns = 'http://www.w3.org/2000/svg';
var xlinkns = 'http://www.w3.org/1999/xlink';
var el = document.createElementNS(svgns, type);
var title, titleEl;
if (el) {
_.forEach(config, function(value, setting) {
if (setting === 'textContent') {
// Text content (the text in a text element or title element) is a property:
el.textContent = value;
} else if (setting === 'href') {
// href comes from a different namespace so must use setAttributeNS:
el.setAttributeNS(xlinkns, 'href', value);
} else if (setting === 'title') {
title = value;
} else if (setting === 'className') {
el.setAttribute('class', value);
} else {
// Most of the time we're just setting an attribute:
el.setAttribute(setting, value);
}
});
}
// Mouseover title actually requires creating a child element.
// Strangely, if I did this in the above loop, the child element was lost if
// props/attributes were set afterwards!! So save title for last.
if (title) {
titleEl = document.createElementNS(svgns, 'title');
titleEl.textContent = title;
el.appendChild(titleEl);
}
return el;
}
///////////////////////////// Module: speciesTree /////////////////////////////
var speciesTree = (function() {
// SVG phylogenetic tree of species in the browser (with connected assembly hubs above tree)
// Layout parameters/configuration (object passed into draw can override these defaults):
var cfg = { labelRightX: 230,
labelStartY: 18,
speciesLineOffsetX: 8,
branchLength: 5,
halfTextHeight: 4,
hubLineOffset: 100,
labelLineHeight: 18,
paddingRight: 5,
paddingBottom: 5,
branchPadding: 2,
- onClickSpeciesName: 'console.log',
- onClickHubName: 'console.log',
trackHubsUrl: '',
containerWidth: 370
};
function checkTree(node) {
// Return true if node and its descendants are of the form
// Array[ label:String, taxId:Number, sciName:String, Array[ [node]...] ]
// e.g. ['root', taxId0, sciName0,
// [ ['leaf1', taxId1, sciName1, []],
// ['leaf2', taxId2, sciName2, []] ]
// ]
if (! _.isString(node[0])) {
console.log('label is not a string', node);
return false;
}
if (! _.isNumber(node[1])) {
console.log('taxId is not a number', node);
return false;
}
if (! (node[2] === null || _.isString(node[2]))) {
console.log('sciName is not null or string', node);
return false;
}
return _.every(node[3], checkTree);
}
function addDepth(node) {
// Each node is of the form [ label, taxId, sciName, node[] ]
// e.g. ['root', taxId0, sciName0,
// [ ['leaf1', taxId1, sciName1, []],
// ['leaf2', taxId2, sciName2, []] ]
// ]
// Add a fifth property to node: depth, i.e. the maximum number of
// branching nodes along any path from node to a leaf.
// Returns depth.
var kids = node[3];
var depth, deepestKid;
if (!kids || kids.length === 0) {
// Leaf: depth is 0
depth = 0;
} else if (kids.length === 1) {
// Node with one child: pass-through, depth is child's depth
depth = addDepth(kids[0]);
} else {
// Node with multiple children: depth is 1 + max child depth
deepestKid = _.max(kids, addDepth);
depth = 1 + deepestKid[4];
}
node[4] = depth;
return depth;
}
function addSpeciesLabel(svg, label, taxId, sciName, y) {
// Add a species label to svg at y offset
- var onClickString = cfg.onClickSpeciesName + '(' + taxId + ')';
var text = svgCreateEl('text', { x: cfg.labelRightX, y: y,
+ id: 'textEl_' + taxId,
name: 'textEl_' + taxId,
title: sciName,
- textContent: label,
- onclick: onClickString });
+ textContent: label
+ });
+ // CSP2 will not allow setAttribute with events like onclick,
+ // no matter whether the value is string or function.
+ text.onclick = function(){hgGateway.onClickSpeciesLabel(taxId);};
+
svg.appendChild(text);
}
function addLine(svg, x1, y1, x2, y2, mouseover) {
// Add optionally with mouseover title
var config = { x1: x1, y1: y1,
x2: x2, y2: y2,
title: mouseover };
var line;
if (x1 === x2) {
// vertical lines get special styling
config.className = 'vert';
}
line = svgCreateEl('line', config);
svg.appendChild(line);
}
function calcBranchX(kidRetList, depth, parentDepth, parentNodeDepth) {
// Return the x offset used for drawing lines from children and a vertical line
// connecting them.
// If the branch from parent is longer than the minimum length, we can scale myX
// to reflect how many nodes are skipped in branches to the right vs. to the left,
// ensuring the minimum branch length on either side. Without this adjustment,
// we sometimes get very long branches that skip only one node to the right of short
// branches that skip many nodes.
var myX;
var kidMaxX = _.max(kidRetList, 'x').x;
var branchSteps = parentDepth - depth;
var kidMinNodeDepth, ratioSkipped, stepsX;
if (branchSteps > 1) {
kidMinNodeDepth = _.min(kidRetList, 'nodeDepth').nodeDepth;
ratioSkipped = kidMinNodeDepth / (kidMinNodeDepth + parentNodeDepth);
stepsX = (branchSteps+1) * ratioSkipped;
// Make sure there is at least one step (min branch width) before and after
// stepsX.
if (stepsX < 1) {
stepsX = 1;
} else if (stepsX > branchSteps) {
stepsX = branchSteps;
}
myX = kidMaxX + cfg.branchLength * stepsX;
} else {
// Only one change in depth level, so short branch:
myX = kidMaxX + cfg.branchLength;
}
return myX;
}
function drawNode(svg, node, leafY, leafTops, parentDepth, parentNodeDepth) {
// Each node is of the form [ label, taxId, sciName, node[], depth ]
// e.g. ['root', taxId0, sciName0,
// [ ['leaf1', taxId1, sciName1, [], 0],
// ['leaf2', taxId2, sciName2, [], 0] ],
// 1,
// ]
// depth is the max number of branching nodes under this node.
// Recursively draw the tree by generating new SVG elements and adding them to svg.
// Returns {x:, y:, leafY:} where x and y are the endpoint for the parent's branch
// to this node and leafY is the Y offset for the next leaf label.
// For leaf nodes, draw labels, store y in leafTops, and return label coordinates.
// Nodes with a single child don't draw anything, they simply return the
// child's info.
// Nodes with multiple children draw a horizontal line to each child and
// a vertical line connecting the horizontal lines. They return the {X,Y} of
// the midpoint of the vertical line.
// optional arg parentDepth is the depth of the parent node as defined above.
// optional arg parentNodeDepth is 1 plus the number of single-child nodes that were
// skipped on the way from the last branching ancestor.
// For internal use, the return object also includes nodeDepth: 1 + number of
// nodes skipped between this node and the next branching descendant or leaf
// and mouseover: horizontal line labels showing child label plus any skipped
// nodes' labels.
var label = node[0], taxId = node[1], sciName = node[2], kids = node[3], depth = node[4];
var myX, myY;
var kidRet, kidRetList, kidMinY, kidMaxY, extraSpace;
parentDepth = parentDepth || 1;
parentNodeDepth = parentNodeDepth || 1;
if (!kids || kids.length === 0) {
// leaf node: draw species label, store myY in leafTops
addSpeciesLabel(svg, label, taxId, sciName, leafY);
myX = cfg.labelRightX + cfg.speciesLineOffsetX;
myY = leafY - cfg.halfTextHeight;
leafTops[label] = leafY - cfg.labelLineHeight;
return { x: myX, y: myY, leafY: leafY + cfg.labelLineHeight, nodeDepth: 1 };
} else if (kids.length === 1) {
// Single child: don't draw anything (keep the rendering compact),
// but make a note that we skipped a node so we can use it for mouseover text
kidRet = drawNode(svg, kids[0], leafY, leafTops, parentDepth, parentNodeDepth + 1);
kidRet.nodeDepth++;
if (kidRet.mouseover) {
kidRet.mouseover += ' - ' + label;
} else {
kidRet.mouseover = label;
}
return kidRet;
} else {
// Multiple children. First pass to draw kids and gather their coords,
// second pass to draw lines connecting kids.
extraSpace = (depth - 1) * cfg.branchPadding;
kidRetList = _.map(kids, function(kid) {
var kidRet = drawNode(svg, kid, leafY, leafTops, depth, 1);
if (! kidRet.mouseover) {
// If nothing was skipped, use kid's sciName / label (same as kid's vert line)
var kidLabel = kid[0], kidSciName = kid[2];
kidRet.mouseover = kidSciName ? kidSciName : kidLabel;
}
leafY = kidRet.leafY + extraSpace;
return kidRet;
});
myX = calcBranchX(kidRetList, depth, parentDepth, parentNodeDepth);
// Draw horizontal lines from kids
_.forEach(kidRetList, function(kidRet) {
addLine(svg, kidRet.x, kidRet.y, myX, kidRet.y, kidRet.mouseover);
});
// Draw vertical line connecting kids
kidMinY = kidRetList[0].y;
kidMaxY = kidRetList[kids.length-1].y;
addLine(svg, myX, kidMinY, myX, kidMaxY, label);
myY = (kidMinY + kidMaxY) / 2;
return { x: myX, y: myY, leafY: leafY, nodeDepth: 1, mouseover: label };
}
}
function addTrackHubsLink(svg, x, y) {
// Add a label with link to hgHubConnect at the given position.
var a = svgCreateEl('a', { 'href': cfg.hgHubConnectUrl });
var text = svgCreateEl('text', { x: x, y: y,
textContent: 'Hub Genomes',
className: 'trackHubsLink' });
a.appendChild(text);
svg.appendChild(a);
}
function doubleQuote(string) {
return '"' + string + '"';
}
function addHubLabel(svg, hub, y) {
// Add a track hub label to svg at y offset
var label = hub.shortLabel + ' (' + hub.assemblyCount + ')';
// There are a bunch of hub properties to pass to the onClick handler;
// too bad we can't pass a bound function but instead must build a string:
- var onClickString = cfg.onClickHubName + '(' +
- doubleQuote(hub.hubUrl) + ', ' +
- hub.taxId + ', ' +
- doubleQuote(hub.defaultDb) + ', ' +
- doubleQuote(hub.name) +
- ')';
var text = svgCreateEl('text', { x: cfg.labelRightX, y: y,
textContent: label,
+ id: 'textEl_' + hub.name,
name: 'textEl_' + hub.name,
- onclick: onClickString,
- title: hub.longLabel });
+ title: hub.longLabel
+ });
+ text.onclick = function() {hgGateway.onClickHubName(hub.hubUrl, hub.taxId, hub.defaultDb, hub.name);};
svg.appendChild(text);
}
function drawHubs(svg, hubList, yIn) {
// Add a label for each hub in hubList, with a separator line below and
// "Hub Genomes" link instead of a tree.
var y = yIn;
var hub, i, textX, textY, lineX1, lineY, lineX2;
if (hubList && hubList.length) {
for (i = 0; i < hubList.length; i++) {
hub = hubList[i];
addHubLabel(svg, hub, y);
y += cfg.labelLineHeight;
}
textX = cfg.labelRightX + cfg.speciesLineOffsetX;
textY = (yIn + y - cfg.labelLineHeight) / 2;
addTrackHubsLink(svg, textX, textY);
lineX1 = cfg.hubLineOffset;
lineY = y - cfg.halfTextHeight;
lineX2 = cfg.containerWidth - cfg.speciesLineOffsetX;
addLine(svg, lineX1, lineY, lineX2, lineY);
y += cfg.labelLineHeight;
}
return y;
}
function draw(svg, dbDbTree, hubList, cfgOverrides) {
// dbDbTree is the root node of a phylogenetic tree that we render in svg.
// cfgOverrides should be used to provide (names of) meaningful onclick functions
// and track hub URL.
// Return the width and height of the tree, the top offset of the tree (below hubs
// if any), and an object with the top coords of each label/leaf.
// Instead of tacking a bunch of children directly onto svg, make a (group)
// and append that to svg when done.
// First see if there's already something there that we will replace:
var oldG = svg.getElementById('hubsAndTree');
var newG = svgCreateEl('g', { id: 'hubsAndTree' });
// y offsets of tops of species labels (leaves of dbDbTree), filled in by drawNode.
var leafTops = {};
var hubBottomY, treeInfo, width, height;
if (! checkTree(dbDbTree)) {
console.error('dbDbTree in wrong format', dbDbTree);
return;
}
_.assign(cfg, cfgOverrides);
hubBottomY = drawHubs(newG, hubList, cfg.labelStartY);
addDepth(dbDbTree);
treeInfo = drawNode(newG, dbDbTree, hubBottomY, leafTops);
width = treeInfo.x + cfg.paddingRight;
if (width < cfg.containerWidth) {
width = cfg.containerWidth;
}
height = treeInfo.leafY - cfg.labelLineHeight + cfg.paddingBottom;
if (oldG) {
svg.removeChild(oldG);
}
svg.appendChild(newG);
return { width: width, height: height,
yTree: hubBottomY - cfg.labelLineHeight,
leafTops: leafTops };
}
return { draw: draw };
}()); // speciesTree
///////////////////////////// Module: rainbow /////////////////////////////
var rainbow = (function() {
// Add rainbow stripes with cute species icons on the left of an SVG element that
// already has a phylogenetic tree drawn by speciesTree.
// Layout parameters/configuration (object passed into draw can override these defaults):
var cfg = { stripeWidth: 69,
iconX: 2,
iconYOffset: -14,
iconWidth: 65,
iconHeight: 65,
iconSpriteUrl: '../images/jWestIconsAlpha65px.png',
iconSpriteWidth: 325,
iconSpriteHeight: 325
};
// Color spectrum for slider and tree display:
var stripeColors = [ '#7E1F16',
'#A12321',
'#C15026',
'#DF6933',
'#EB8734',
'#B57E2A',
'#CD9C2A',
'#CFB32B',
'#959E38',
'#3A8349',
'#216D6D',
'#4C749B',
'#31469A',
'#7E4475',
'#231F1F' ];
// Hubs go above the species rainbow -- give them their own color of stripe:
var hubColor = '#60180C';
// Taxonomy IDs for assigning rainbow stripes to species groups:
var stripeTaxIds = [ 9443, // Primates
314146, // Euarchontoglires
9362, // Insectivora
91561, // Cetartiodactyla
314145, // Laurasiatheria
9397, // Chiroptera
9347, // Eutheria
40674, // Mammalia
32523, // Tetrapoda
7742, // Vertebrata
33511, // Deuterostomia
33392, // Endopterygota
6072, // Eumetazoa
2759, // Eukaryota
1 ]; // root
// Cute species icons placed along the stripe also help to orient.
// This maps leaf labels to icon names:
var iconSpeciesToName = { Human: 'Human',
Mouse: 'Mouse',
'D. melanogaster': 'Fly',
'C. elegans': 'Worm',
'S. cerevisiae': 'Yeast',
'Rhesus': 'Monkey',
Hedgehog: 'Hedgehog',
// Pig: 'Pig',
Cow: 'Cow',
'Killer whale': 'Orca',
Horse: 'Horse',
Dog: 'Dog',
'Pacific walrus': 'Walrus',
'Megabat': 'Bat',
Elephant: 'Elephant',
Manatee: 'Manatee',
Armadillo: 'Armadillo',
'Wallaby': 'Kangaroo',
'Zebra finch': 'Bird',
Lizard: 'Lizard',
'X. tropicalis': 'Frog',
'Fugu': 'Fish',
'Zaire ebolavirus': 'Ebola', // on hgwdev April 2016
'Ebola virus': 'Ebola' // on RR April 2016
};
// The icon sprite image has 5 rows and 5 columns:
var spriteRowCol = { Human: [0,0],
Mouse: [0,1],
Fly: [0,3],
Worm: [0,4],
Yeast: [1,0],
Monkey: [1,1],
Hedgehog: [1,2],
Pig: [1,3],
Cow: [1,4],
Orca: [2,0],
Horse: [2,1],
Dog: [2,2],
Walrus: [2,3],
Bat: [2,4],
Elephant: [3,0],
Manatee: [3,1],
Armadillo: [3,2],
Kangaroo: [3,3],
Bird: [3,4],
Lizard: [4,0],
Frog: [4,1],
Fish: [4,2],
Ebola: [4,3]
};
// Some icon drawings are shorter than others, and some need to be moved up to make space
// for close neighbors.
var iconFudgeY = { Human: 20,
Mouse: 0,
Fly: 0,
Worm: 0,
Monkey: 0,
Hedgehog: -18,
Pig: -5,
Cow: 0,
Orca: 0,
Horse: -8,
Dog: 15,
Walrus: 40,
Bat: 0,
Elephant: -20,
Manatee: 20,
Armadillo: 0,
Kangaroo: 0,
Bird: 0,
Lizard: -20,
Frog: -10,
Fish: 0,
Yeast: -15,
Ebola: 0
};
function findStripeTops(node, parentStripeIx, leafTops, stripeTops, yPrev) {
// Each node is of the form [ label, taxId, sciName, node[], ... ]
// Recursively find the top coordinate of each stripe in stripeTaxIds,
// using node taxId. Modifies stripeTops. Returns the y of the top of the
// last leaf visited.
var label = node[0], taxId = node[1], kids = node[3];
var stripeIx = stripeTaxIds.indexOf(taxId);
var i;
// Inherit parent stripe unless this node is found in stripeTaxIds:
if (stripeIx < 0) {
stripeIx = parentStripeIx;
}
if (!kids || kids.length === 0) {
// leaf node: if this stripe's top coord has not yet been assigned,
// assign it.
if (stripeTops[stripeIx] === undefined) {
stripeTops[stripeIx] = (leafTops[label] + yPrev) / 2;
}
yPrev = leafTops[label];
} else {
// descend to children
for (i = 0; i < kids.length; i++) {
yPrev = findStripeTops(kids[i], stripeIx, leafTops, stripeTops, yPrev);
}
}
return yPrev;
}
function addRectFill(svg, x, y, width, height, color) {
// Add filled rectangle to svg
var rect = svgCreateEl('rect', { x: x, y: y,
width: width, height: height,
style: 'fill:' + color + '; stroke:' + color });
svg.appendChild(rect);
}
function drawStripes(svg, dbDbTree, yTop, height, leafTops) {
var stripeCount = stripeColors.length;
var lastStripeIx = stripeCount - 1;
var stripeTops = [];
var i, stripeHeight;
findStripeTops(dbDbTree, lastStripeIx, leafTops, stripeTops, yTop);
// Add an extra "stripe" coord so we have the coord for the bottom of the last stripe:
stripeTops[stripeCount] = height;
// Initialize missing stripes to 0-height (top = next stripe's top), if any:
for (i = stripeCount - 1; i >= 0; i--) {
if (stripeTops[i] === undefined) {
stripeTops[i] = stripeTops[i+1];
}
}
for (i = 0; i < stripeCount; i++) {
stripeHeight = stripeTops[i+1] - stripeTops[i];
addRectFill(svg, 0, stripeTops[i], cfg.stripeWidth, stripeHeight, stripeColors[i]);
}
// Add stripe for hubs, if any:
if (yTop > 0) {
addRectFill(svg, 0, 0, cfg.stripeWidth, yTop, hubColor);
}
return stripeTops;
}
function drawOneIcon(svg, name, y) {
// Create an image, offset so that the icon is positioned where we need it,
// and use a clip-path to limit display to just that icon, not the whole sprite image.
var iconY = y + cfg.iconYOffset + iconFudgeY[name];
var rowCol = spriteRowCol[name];
var row = rowCol[0], column = rowCol[1];
var clipPathId = 'clip' + name;
var img = svgCreateEl('image', { x: cfg.iconX - (column * cfg.iconWidth),
y: iconY - (row * cfg.iconHeight),
width: cfg.iconSpriteWidth,
height: cfg.iconSpriteHeight,
style: 'clip-path: url(#' + clipPathId + ')',
href: cfg.iconSpriteUrl });
// Set the y of the pre-existing clip path:
var rect = $('#' + clipPathId + ' rect')[0];
rect.setAttribute('y', iconY);
svg.appendChild(img);
}
function drawIcons(svg, leafTops) {
// For each icon listed in iconSpeciesToName, look up the species' y offset in
// the tree and add the icon to svg.
_.forEach(iconSpeciesToName, function (name, species) {
var y = leafTops[species];
if (y >= 0) {
drawOneIcon(svg, name, y);
}
});
}
function draw(svg, dbDbTree, yTree, height, leafTops) {
// Draw stripes with colors corresponding to species groups and cute-species icons.
// Return y offsets of stripes so that a slider widget can be drawn accordingly.
// Instead of tacking a bunch of children directly onto svg, make a (group)
// and append that to svg when done.
// First see if there's already something there that we will replace:
var oldG = svg.getElementById('stripesAndIcons');
var newG = svgCreateEl('g', { id: 'stripesAndIcons' });
var stripeTops = drawStripes(newG, dbDbTree, yTree, height, leafTops);
drawIcons(newG, leafTops);
if (oldG) {
svg.removeChild(oldG);
}
svg.appendChild(newG);
return stripeTops;
}
return { draw: draw,
colors: stripeColors,
hubColor: hubColor
};
}()); // rainbow
///////////////////////////// Module: autocompleteCat /////////////////////////////
var autocompleteCat = (function() {
// Customize jQuery UI autocomplete to show item categories and support html markup in labels.
// Adapted from https://jqueryui.com/autocomplete/#categories and
// http://forum.jquery.com/topic/using-html-in-autocomplete
// Also adds watermark to input.
$.widget("custom.autocompleteCat",
$.ui.autocomplete,
{
_renderMenu: function(ul, items) {
var that = this;
var currentCategory = "";
// There's no this._super as shown in the doc, so I can't override
// _create as shown in the doc -- just do this every time we render...
this.widget().menu("option", "items", "> :not(.ui-autocomplete-category)");
$.each(items,
function(index, item) {
// Add a heading each time we see a new category:
if (item.category && item.category !== currentCategory) {
ul.append("
" +
item.category + "
" );
currentCategory = item.category;
}
that._renderItem( ul, item );
});
},
_renderItem: function(ul, item) {
// In order to use HTML markup in the autocomplete, one has to overwrite
// autocomplete's _renderItem method using .html instead of .text.
// http://forum.jquery.com/topic/using-html-in-autocomplete
return $("")
.data("item.autocomplete", item)
.append($("").html(item.label))
.appendTo(ul);
}
});
function init($input, options) {
// Set up an autocomplete and watermark for $input, with a callback options.onSelect
// for when the user chooses a result.
// If options.baseUrl is null, the autocomplete will not do anything, but we (re)initialize
// it anyway in case the same input had a previous db's autocomplete in effect.
// options.onServerReply (if given) is a function (Array, term) -> Array that
// post-processes the list of items returned by the server before the list is
// passed back to autocomplete for rendering.
// The following two options apply only when using our locally modified jquery-ui:
// If options.enterSelectsIdentical is true, then if the user hits Enter in the text input
// and their term has an exact match in the autocomplete results, that result is selected.
// options.onEnterTerm (if provided) is a callback function (jqEvent, jqUi) invoked
// when the user hits Enter, after handling enterSelectsIdentical.
// The function closure allows us to keep a private cache of past searches.
var cache = {};
var doSearch = function(term, acCallback) {
// Look up term in searchObj and by sending an ajax request
var timestamp = new Date().getTime();
var url = options.baseUrl + encodeURIComponent(term) + '&_=' + timestamp;
$.getJSON(url)
.done(function(results) {
if (_.isFunction(options.onServerReply)) {
results = options.onServerReply(results, term);
}
cache[term] = results;
acCallback(results);
});
// ignore errors to avoid spamming people on flaky network connections
// with tons of error messages (#8816).
};
var autoCompleteSource = function(request, acCallback) {
// This is a callback for jqueryui.autocomplete: when the user types
// a character, this is called with the input value as request.term and an acCallback
// for this to return the result to autocomplete.
// See http://api.jqueryui.com/autocomplete/#option-source
var results = cache[request.term];
if (results) {
acCallback(results);
} else if (options.baseUrl) {
doSearch(request.term, acCallback);
}
};
var autoCompleteSelect = function(event, ui) {
// This is a callback for autocomplete to let us know that the user selected
// a term from the list. See http://api.jqueryui.com/autocomplete/#event-select
options.onSelect(ui.item);
$input.blur();
};
// Provide default values where necessary:
options.onSelect = options.onSelect || console.log;
options.enterSelectsIdentical = options.enterSelectsIdentical || false;
$input.autocompleteCat({
delay: 500,
minLength: 2,
source: autoCompleteSource,
select: autoCompleteSelect,
enterSelectsIdentical: options.enterSelectsIdentical,
enterTerm: options.onEnterTerm
});
if (options.watermark) {
$input.css('color', 'black');
$input.Watermark(options.watermark, '#686868');
}
}
return { init: init };
}()); // autocompleteCat
///////////////////////////// Module: hgGateway /////////////////////////////
var hgGateway = (function() {
// Interactive parts of the new gateway page: species autocomplete,
// graphical species-picker, db select, and position autocomplete.
// Constants
var speciesWatermark = 'Enter species or common name';
var positionWatermark = 'Enter position, gene symbol or search terms';
// Shortcuts to popular species:
var favIconTaxId = [ ['Human', 9606],
['Mouse', 10090],
['Rat', 10116],
['Fly', 7227],
['Worm', 6239],
['Yeast', 559292] ];
// Aliases for species autocomplete:
var commonToSciNames = { bats: 'Chiroptera',
bees: 'Apoidea',
birds: 'Aves',
fish: 'Actinopterygii',
fly: 'Diptera',
flies: 'Diptera',
frogs: 'Anura',
fruitfly: 'Drosophila',
'fruit fly': 'Drosophila',
honeybees: 'Apinae',
'honey bees': 'Apinae',
monkeys: 'Simiiformes',
mosquitos: 'Culicidae',
worms: 'Nematoda',
yeast: 'Ascomycota' };
var getBetterBrowserMessage = '
' +
'Our website has detected that you are using ' +
'an outdated browser that will prevent you from ' +
'accessing certain features. An update is not ' +
'required, but it is strongly recommended to ' +
'improve your browsing experience. ' +
'Please use the following links to upgrade your ' +
'existing browser to ' +
'' +
' FireFox or ' +
'' +
'Chrome.' +
'
';
// Globals (within this function scope)
// Set this to true to see server requests and responses in the console.
var debugCartJson = false;
// This is a global (within wrapper function scope) so event handlers can use it
// without needing to bind functions.
var scrollbarWidth = 0;
// This holds everything we need to know to draw the page: taxId, db, hubs, description etc.
var uiState = {};
// This is used to check whether a taxId is found in activeGenomes:
- var activeTaxIds = _.invert(activeGenomes);
+ var activeTaxIds = []; // gets set in init() now;
// This is dbDbTree after pruning -- null if dbDbTree has no children left
var prunedDbDbTree = null;
// This keeps track of which gene the user has selected most recently from autocomplete.
var selectedGene = null;
function setupFavIcons() {
// Set up onclick handlers for shortcut buttons and labels
var haveIcon = false;
var i, name, taxId, onClick;
for (i = 0; i < favIconTaxId.length; i++) {
name = favIconTaxId[i][0];
taxId = favIconTaxId[i][1];
if (activeTaxIds[taxId]) {
// When user clicks on icon, set the taxId (default database);
// scroll the image to that species and clear the species autocomplete input.
onClick = setTaxId.bind(null, taxId, null, true, true);
// Onclick for both the icon and its sibling label:
$('.jwIconSprite' + name).parent().children().click(onClick);
haveIcon = true;
} else {
// Inactive on this site -- hide it
$('.jwIconSprite' + name).parent().hide();
}
}
if (! haveIcon) {
$('#popSpeciesTitle').text('Species Search');
}
}
function addCategory(cat, item) {
// Clone item, add category: cat to it and return it (helper function, see below).
var clone = {};
_.assign(clone, item, { category: cat });
return clone;
}
function autocompleteFromNode(node) {
// Traverse dbDbTree to make autocomplete result lists for all non-leaf node labels.
// Returns an object mapping each label of node and descendants to a list of
// result objects with the same structure that we'd get from a server request.
if (! node) {
return;
}
var searchObj = {};
var myResults = [];
var label = node[0], taxId = node[1], kids = node[3];
var addMyLabel;
if (!kids || kids.length === 0) {
// leaf node: return autocomplete result for species
myResults = [ { genome: label,
label: label,
taxId: taxId } ];
} else {
// Accumulate the list of all children's result lists;
// keep each's child searchObj mappings unless the child is a leaf
// (which would be redundant with server autocomplete results).
addMyLabel = addCategory.bind(null, label);
myResults = _.flatten(
_.map(kids, function(kid) {
var kidLabel = kid[0], kidKids = kid[3];
var kidObj = autocompleteFromNode(kid);
// Clone kid's result list and add own label as category:
var kidResults = _.map(kidObj[kidLabel], addMyLabel);
// Add kid's mappings to searchObj only if kid is not a leaf.
if (kidKids && kidKids.length > 0) {
_.assign(searchObj, kidObj);
}
return kidResults;
})
);
}
// Exclude some overly broad categories:
if (label !== 'root' && label !== 'cellular organisms') {
searchObj[label] = myResults;
}
return searchObj;
}
function autocompleteFromTree(node, searchObj) {
// Traverse dbDbTree to make autocomplete result lists for all non-leaf node labels.
// searchObj is extended to map each label of node and descendants to a list of
// result objects with the same structure that we'd get from a server request.
_.assign(searchObj, autocompleteFromNode(node));
// Add aliases for some common names that map to scientific names in the tree.
_.forEach(commonToSciNames, function(sciName, commonName) {
var label, addMyLabel;
if (searchObj[sciName]) {
label = sciName + ' (' + commonName + ')';
addMyLabel = addCategory.bind(null, label);
searchObj[commonName] = _.map(searchObj[sciName], addMyLabel);
}
});
}
function makeStripe(id, color, stripeHeight, scrollTop, onClickStripe) {
// Return an empty div with specified background color and height
var $stripe = $('