4c756ae28c7c8a654ba4a8877224f03513999b64
chmalee
Tue May 19 14:16:27 2026 -0700
More fixes from code review. Hopefully have cleared the last remaining vestiges of the first version of the tooltip implementation, refs #37528
diff --git src/hg/js/utils.js src/hg/js/utils.js
index 4c07c1d2ede..aba6171b47e 100644
--- src/hg/js/utils.js
+++ src/hg/js/utils.js
@@ -1,5044 +1,5043 @@
// Utility JavaScript
// "use strict";
// Don't complain about line break before '||' etc:
/* jshint -W014 */
/* jshint -W087 */
/* jshint esnext: true */
var debug = false;
/* Support these formats for range specifiers. Note the ()'s around chrom,
* start and end portions for substring retrieval: */
// \s = whitespace, \w = azAZ0-9_,
var canonicalRangeExp = /^[\s]*([\w._#-]+)[\s]*:[\s]*([-0-9,]+)[\s]*[-_][\s]*([0-9,]+)[\s]*$/;
var gbrowserRangeExp = /^[\s]*([\w._#-]+)[\s]*:[\s]*([0-9,]+)[\s]*\.\.[\s]*([0-9,]+)[\s]*$/;
var lengthRangeExp = /^[\s]*([\w._#-]+)[\s]*:[\s]*([0-9,]+)[\s]*\+[\s]*([0-9,]+)[\s]*$/;
var bedRangeExp = /^[\s]*([\w._#-]+)[\s]+([0-9,]+)[\s]+([0-9,]+)[\s]*$/;
var sqlRangeExp = /^[\s]*([\w._#-]+)[\s]*\|[\s]*([0-9,]+)[\s]*\|[\s]*([0-9,]+)[\s]*$/;
var singleBaseExp = /^[\s]*([\w._#-]+)[\s]*:[\s]*([0-9,]+)[\s]*$/;
// also allow gnomad variants, ex: 12-1234-A-C
var gnomadVarExp = "^(([0-9]+)|(X|Y|M|MT))-([0-9]+)-([A-Za-z]+)-([A-Za-z]+)$";
// allow gnomad ranges, ex: 12-1234-11223344
var gnomadRangeExp = "^(([0-9]+)|(X|Y|M|MT))-([0-9]+)-([0-9]+)$";
function createInfoIcon(text) {
/* Create an info icon (i in circle) with tooltip text.
* Returns a span element containing the SVG icon.
* Uses addMouseover() for consistent tooltip behavior. */
var span = document.createElement("span");
span.style.marginLeft = "4px";
span.innerHTML = "";
addMouseover(span, text);
return span;
}
function copyToClipboard(ev) {
/* copy a piece of text to clipboard. event.target is some DIV or SVG that is an icon.
* The attribute data-target of this element is the ID of the element that contains the text to copy.
* The text is either in the attribute data-copy or the innerText.
* see C function printCopyToClipboardButton(iconId, targetId);
* */
ev.preventDefault();
var buttonEl = ev.target.closest("button"); // user can click SVG or BUTTON element
var targetId = buttonEl.getAttribute("data-target");
if (targetId===null)
targetId = ev.target.parentNode.getAttribute("data-target");
var textEl = document.getElementById(targetId);
var text = textEl.getAttribute("data-copy");
if (text===null)
text = textEl.innerText;
var textArea = document.createElement("textarea");
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
buttonEl.innerHTML = 'Copied';
ev.preventDefault();
}
function cfgPageOnVisChange(ev) {
/* configuration page event listener when user changes visibility in dropdown */
if (ev.target.value === 'hide')
ev.target.classList.replace("normalText", "hiddenText");
else
ev.target.classList.replace("hiddenText", "normalText");
}
function cfgPageAddListeners() {
/* add event listener to dropdowns */
var els = document.querySelectorAll(".trackVis");
for (var i=0; i < els.length; i++) {
var el = els[i];
el.addEventListener("change", cfgPageOnVisChange );
}
}
// Google Analytics helper functions to send events, see src/hg/lib/googleAnalytics.c
function gaOnButtonClick(ev) {
/* user clicked a button: send event to GA, then execute the old handler */
var button = ev.currentTarget;
var buttonName = button.name;
if (buttonName==="")
buttonName = button.id;
if (buttonName==="")
buttonName = button.value;
// add the original label, makes logs a lot easier to read
buttonName = button.value + " / "+buttonName;
ga('send', 'event', 'buttonClick', buttonName);
if (button.oldOnClick) // most buttons did not have an onclick function at all (the default click is a listener)
{
button.oldOnClick(ev);
}
}
function gaTrackButtons() {
/* replace the click handler on all buttons with one the sends a GA event first, then handles the click */
if (!window.ga || ga.loaded) // When using an Adblocker, the ga object does not exist
return;
var buttons = document.querySelectorAll('input[type=submit],input[type=button]');
var isFF = theClient.isFirefox();
for (var i = 0; i < buttons.length; i++) {
var b = buttons[i];
// some old Firefox versions <= 78 do not allow submit buttons to also send AJAX requests
// so Zoom/Move buttons are skipped in FF (even though newer versions allow it again, certainly FF >= 90)
if (isFF && b.name.match(/\.out|\.in|\.left|\.right/))
continue;
b.oldOnClick = b.onclick;
b.onclick = gaOnButtonClick; // addEventHandler would not work here, the default click stops propagation.
}
}
// end Google Analytics helper functions
function clickIt(obj,state,force)
{
// calls click() for an object, and click();click() if force
if (obj.checked !== state) {
obj.click();
} else if (force) {
obj.click();
obj.click(); //force onclick event
}
}
function setCheckBoxesWithPrefix(obj, prefix, state)
{
// Set all checkboxes with given prefix to state boolean
var list = inputArrayThatMatches("checkbox","id",prefix,"");
for (var i=0;i 0 && inpType !== 'select' && ele.type !== inpType)
continue;
var identifier = ele.name;
if (nameOrId.search(/id/i) !== -1)
identifier = ele.id;
var failed = false;
if (prefix.length > 0)
failed = (identifier.indexOf(prefix) !== 0);
if (!failed && suffix.length > 0)
failed = (identifier.lastIndexOf(suffix) !== (identifier.length - suffix.length));
if (!failed) {
for (var aIx=4;aIx2)
alert("arrayOfInputsThatMatch is unimplemented for this browser");
}
return found;
}
function normed(thing)
{ // RETURNS undefined, the lone member of the set or the full set if more than one member.
// Used for normalizing returns from jquery DOM selects (e.g. $('tr.track').children('td.data'))
// jquery returns an "array like 'object'" with 0 or more entries.
// May be used on non-jquery objects and will reduce single element arrays to the element.
// Use this to treat 0 entries the same as undefined and 1 entry as the item itself
if (typeof(thing) === 'undefined' || thing === null
|| (thing.length !== undefined && thing.length === 0) // Empty array (or 'array like object')
|| ($.isPlainObject(thing) && $.isEmptyObject(thing))) // Empty simple object
return undefined;
if (thing.length && thing.length === 1 && typeof thing !== 'string') // string is overkill
return thing[0]; // Container of one item should return the item itself.
return thing;
}
var theClient = (function() {
// Object that detects client browser if requested
// - - - - - Private variables and/or methods - - - - -
var ieVersion = null;
var browserNamed = null;
// - - - - - Public methods - - - - -
return { // returns an object with public methods
getIeVersion: function ()
{ // Adapted from the web: stackOverflow.com answer by Joachim Isaksson
if (ieVersion === null) {
ieVersion = -1.0;
var re = null;
if (navigator.appName === 'Microsoft Internet Explorer') {
browserNamed = 'MSIE';
re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");
if (re.exec(navigator.userAgent) !== null)
ieVersion = parseFloat( RegExp.$1 );
} else if (navigator.appName === 'Netscape') {
re = new RegExp("Trident/.*rv:([0-9]{1,}[.0-9]{0,})");
if (re.exec(navigator.userAgent) !== null) {
browserNamed = 'MSIE';
ieVersion = parseFloat( RegExp.$1 );
}
}
}
return ieVersion;
},
isIePre11: function ()
{
var ieVersion = theClient.getIeVersion();
if ( ieVersion !== -1.0 && ieVersion < 11.0 )
return true;
return false;
},
isIePost11: function ()
{
if (theClient.getIeVersion() >= 11.0 )
return true;
return false;
},
isIe: function ()
{
if (theClient.getIeVersion() === -1.0 )
return false;
return true;
},
isChrome: function ()
{
if (browserNamed !== null)
return (browserNamed === 'Chrome');
if (navigator.userAgent.indexOf("Chrome") !== -1) {
browserNamed = 'Chrome';
return true;
}
return false;
},
isNetscape: function ()
{ // IE sometimes mimics netscape
if (browserNamed !== null)
return (browserNamed === 'Netscape');
if (navigator.appName === 'Netscape'
&& navigator.userAgent.indexOf("Trident") === -1) {
browserNamed = 'Netscape';
return true;
}
return false;
},
isFirefox: function ()
{
if (browserNamed !== null)
return (browserNamed === 'FF');
if (navigator.userAgent.indexOf("Firefox") !== -1) {
browserNamed = 'FF';
return true;
}
return false;
},
isSafari: function ()
{ // Chrome sometimes mimics Safari
if (browserNamed !== null)
return (browserNamed === 'Safari');
if (navigator.userAgent.indexOf("Safari") !== -1
&& navigator.userAgent.indexOf('Chrome') === -1) {
browserNamed = 'Safari';
return true;
}
return false;
},
isOpera: function ()
{
if (browserNamed !== null)
return (browserNamed === 'Opera');
if (navigator.userAgent.indexOf("Presto") !== -1) {
browserNamed = 'Opera';
return true;
}
return false;
},
nameIs: function ()
{ // simple enough, this needs no comment!
if (browserNamed === null
&& theClient.isChrome() === false // Looking in the order of popularity
&& theClient.isFirefox() === false
&& theClient.isIe() === false
&& theClient.isSafari() === false
&& theClient.isOpera() === false
&& theClient.isNetscape() === false)
browserNamed = navigator.appName; // Don't know what else to look for.
return browserNamed;
}
};
}());
function waitCursor(obj) // DEAD CODE?
{
//document.body.style.cursor="wait"
obj.style.cursor="wait";
}
function endWaitCursor(obj) // DEAD CODE?
{
obj.style.cursor="";
}
function getURLParam()
{
// Retrieve variable value from an url.
// Can be called either:
// getURLParam(url, name)
// or:
// getURLParam(name)
// Second interface will default to using window.location.href
var strHref, strParamName;
var strReturn = "";
if (arguments.length === 1) {
strHref = window.location.href;
strParamName = arguments[0];
} else {
strHref = arguments[0];
strParamName = arguments[1];
}
if ( strHref.indexOf("?") > -1) {
var strQueryString = strHref.substr(strHref.indexOf("?")).toLowerCase();
var aQueryString = strQueryString.split("&");
for (var iParam = 0; iParam < aQueryString.length; iParam++) {
if (aQueryString[iParam].indexOf(strParamName.toLowerCase() + "=") > -1) {
var aParam = aQueryString[iParam].split("=");
strReturn = aParam[1];
break;
}
}
}
return unescape(strReturn);
}
function makeHiddenInput(theForm,aName,aValue)
{ // Create a hidden input to hold a value
$(theForm).find("input:last").after("");
}
function updateOrMakeNamedVariable(theForm,aName,aValue)
{ // Store a value to a named input. Will make the input if necessary
var inp = normed($(theForm).find("input[name='"+aName+"']:last"));
if (inp) {
$(inp).val(aValue);
inp.disabled = false;
} else
makeHiddenInput(theForm,aName,aValue);
}
function disableNamedVariable(theForm,aName)
{ // Store a value to a named input. Will make the input if necessary
var inp = normed($(theForm).find("input[name='"+aName+"']:last"));
if (inp)
inp.disabled = true;
}
function parseUrlAndUpdateVars(theForm,href) // DEAD CODE?
{ // Parses the URL and converts GET vals to POST vals
var url = href;
var extraIx = url.indexOf("?");
if (extraIx > 0) {
var extra = url.substring(extraIx+1);
url = url.substring(0,extraIx);
// now extra must be repeatedly broken into name=var
extraIx = extra.indexOf("=");
for (; extraIx > 0;extraIx = extra.indexOf("=")) {
var aValue;
var aName = extra.substring(0,extraIx);
var endIx = extra.indexOf("&");
if (endIx>0) {
aValue = extra.substring(extraIx+1,endIx);
extra = extra.substring(endIx+1);
} else {
aValue = extra.substring(extraIx+1);
extra = "";
}
if (aName.length > 0 && aValue.length > 0)
updateOrMakeNamedVariable(theForm,aName,aValue);
}
}
return url;
}
function postTheForm(formName,href)
{ // posts the form with a passed in href
var goodForm = normed($("form[name='"+formName+"']"));
if (goodForm) {
if (href && href.length > 0) {
$(goodForm).attr('action',href); // just attach the straight href
}
$(goodForm).attr('method','POST');
$(goodForm).submit();
}
return false; // Meaning do not continue with anything else
}
function setVarAndPostForm(aName,aValue,formName)
{ // Sets a specific variable then posts
var goodForm = normed($("form[name='"+formName+"']"));
if (goodForm) {
updateOrMakeNamedVariable(goodForm,aName,aValue);
}
return postTheForm(formName,window.location.href);
}
// json help routines
function tdbGetJsonRecord(trackName) { return hgTracks.trackDb[trackName]; }
// NOTE: These must jive with tdbKindOfParent() and tdbKindOfChild() in trackDb.h
function tdbIsFolder(tdb) { return (tdb.kindOfParent === 1); }
function tdbIsComposite(tdb) { return (tdb.kindOfParent === 2); }
function tdbIsMultiTrack(tdb) { return (tdb.kindOfParent === 3); }
function tdbIsView(tdb) { return (tdb.kindOfParent === 4); } // Don't expect to use
function tdbIsContainer(tdb) { return (tdb.kindOfParent === 2 || tdb.kindOfParent === 3); }
function tdbIsLeaf(tdb) { return (tdb.kindOfParent === 0); }
function tdbIsFolderContent(tdb) { return (tdb.kindOfChild === 1); }
function tdbIsCompositeSubtrack(tdb) { return (tdb.kindOfChild === 2); }
function tdbIsMultiTrackSubtrack(tdb) { return (tdb.kindOfChild === 3); }
function tdbIsSubtrack(tdb) { return (tdb.kindOfChild === 2 || tdb.kindOfChild === 3); }
function tdbHasParent(tdb) { return (tdb.kindOfChild !== 0 && tdb.parentTrack); }
function cartHideAnyTrack (id, cartVars, cartVals) {
/* set the right cart variables to hide a track, changes cartVars and cartVals */
var rec = hgTracks.trackDb[id];
if (tdbIsSubtrack(rec)) {
cartVars.push(id);
cartVals.push('[]');
cartVars.push(id+"_sel");
cartVals.push(0);
} else if (tdbIsFolderContent(rec)) {
// supertrack children need to have _sel set to trigger superttrack reshaping
cartVars.push(id);
cartVals.push('hide');
cartVars.push(id+"_sel");
cartVals.push(0);
} else {
// normal, top-level track
cartVars.push(id);
cartVals.push('hide');
}
}
function tdbCountChildren(trackDb, parentType) {
/* return dicts with count of children, uses either the .parentTrack or .topParent trackDb attributes */
var familySize = {};
var families = {};
// sort trackDb into object topParent -> count of children
for (var trackName of Object.keys(hgTracks.trackDb)) {
var rec = hgTracks.trackDb[trackName];
// when looking at superTracks, only look at those children that are in superTracks
if (rec[parentType]===undefined) {
continue; // ignore tracks without parents
}
var parentTrack = rec[parentType];
if (!familySize.hasOwnProperty(parentTrack)) {
familySize[parentTrack] = 0;
families[parentTrack] = [];
}
familySize[parentTrack]++;
families[parentTrack].push(trackName);
}
var ret = {};
ret.familySize = familySize;
ret.families = families;
return ret;
}
function tdbFindChildless(trackDb, delTracks) {
/* Find parents that have no children left anymore in hgTracks.trackDb if you remove delTracks.
* return obj with o.loneParents as array of [parent, array of children] , and o.others as an array of all other tracks
* The caller needs the children names, as we want to hide the children with Javascript right away */
// This functions uses a somewhat weird strategy: it counts the children of the composites, also counts the children of superTracks,
// and compares both at the end. There may be a better strategy but our data structures are so strange that I didn't know what
// else to do.
var parentType = "parentTrack";
others = [];
var compLinks = tdbCountChildren(trackDb, "parentTrack"); // look only at direct parents: superTracks and composites
var topLinks = tdbCountChildren(trackDb, "topParent"); // look at top parents, so only superTracks
// decrease the parent's count for each track to delete
for (var delTrack of delTracks) {
var tdbRec = hgTracks.trackDb[delTrack];
let parentName = tdbRec.parentTrack;
if (parentName)
compLinks.familySize[parentName]--;
parentName = tdbRec.topParent;
if (parentName)
topLinks.familySize[parentName]--;
}
// for the parents of deleted tracks with a count of 0, create an array of [parentName, children]
var loneParents = [];
var doneParents = [];
for (delTrack of delTracks) {
var parentName = hgTracks.trackDb[delTrack].parentTrack;
var topParentName = hgTracks.trackDb[delTrack].topParent;
if (parentName) {
// hide a superTrack parent only if that superTrack does not have any other tracks open
// This addresses the case where you hide the last child of a composite under a superTrack
// E.g. All Gencode / Gencode V46 or FANTOM5/Max count
if (compLinks.familySize[parentName]===0 && topLinks.familySize[topParentName]===0) {
if (!doneParents.includes(parentName)) {
loneParents.push([topParentName, compLinks.families[parentName]]);
doneParents.push(topParentName);
}
} else
for (var child of compLinks.families[parentName])
// do not hide tracks of lone parents that are not in delTracks
if (delTracks.includes(child))
others.push(child);
} else {
others.push(delTrack);
}
}
o = {};
o.loneParents = loneParents;
o.others = Array.from(new Set(others)); // remove duplicates (rare bug in the above)
return o;
}
function aryFind(ary,val)
{// returns the index of a value on the array or -1;
for (var ix=0; ix < ary.length; ix++) {
if (ary[ix] === val) {
return ix;
}
}
return -1;
}
function aryRemove(ary,vals)
{ // removes one or more variables that are found in the array
for (var vIx=0; vIx < vals.length; vIx++) {
var ix = aryFind(ary,vals[vIx]);
if (ix !== -1)
ary.splice(ix,1);
}
return ary;
}
function arysToObj(names,values)
{ // Make hash type obj with two parallel arrays.
var obj = {};
for (var ix=0; ix < names.length; ix++) {
obj[names[ix]] = values[ix];
}
return obj;
}
function objNotEmpty(obj)
{ // returns true on non empty object. Obj should pass $.isPlainObject()
if ($.isPlainObject(obj) === false)
warn("Only use plain js objects in objNotEmpty()");
return ($.isEmptyObject(obj) === false);
}
function objKeyCount(obj)
{ // returns number of keys in object.
if (!Object.keys) {
var count = 0;
for (var key in obj) {
count++;
}
return count;
} else
return Object.keys(obj).length;
}
function isInteger(s)
{
return (!isNaN(parseInt(s)) && isFinite(s) && s.toString().indexOf('.') < 0);
}
function isFloat(s)
{
return (!isNaN(parseFloat(s)) && isFinite(s));
}
function validateInt(obj,min,max)
{ // validates an integer which may be restricted to a range (if min and/or max are numbers)
var title = obj.title;
var rangeMin=parseInt(min);
var rangeMax=parseInt(max);
if (title.length === 0)
title = "Value";
var popup=( theClient.isIePre11() === false );
for (;;) {
if ((obj.value === undefined || obj.value === null || obj.value === "")
&& isInteger(obj.defaultValue))
obj.value = obj.defaultValue;
if (!isInteger(obj.value)) {
if (popup) {
obj.value = prompt(title +" is invalid.\nMust be an integer.",obj.value);
continue;
} else {
alert(title +" of '"+obj.value +"' is invalid.\nMust be an integer.");
obj.value = obj.defaultValue;
return false;
}
}
var val = parseInt(obj.value);
if (isInteger(min) && isInteger(max)) {
if (val < rangeMin || val > rangeMax) {
if (popup) {
obj.value = prompt(title +" is invalid.\nMust be between "+rangeMin+
" and "+rangeMax+".",obj.value);
continue;
} else {
alert(title +" of '"+obj.value +"' is invalid.\nMust be between "+
rangeMin+" and "+rangeMax+".");
obj.value = obj.defaultValue;
return false;
}
}
} else if (isInteger(min)) {
if (val < rangeMin) {
if (popup) {
obj.value = prompt(title +" is invalid.\nMust be no less than "+
rangeMin+".",obj.value);
continue;
} else {
alert(title +" of '"+obj.value +"' is invalid.\nMust be no less than "+
rangeMin+".");
obj.value = obj.defaultValue;
return false;
}
}
} else if (isInteger(max)) {
if (val > rangeMax) {
if (popup) {
obj.value = prompt(title +" is invalid.\nMust be no greater than "+
rangeMax+".",obj.value);
continue;
} else {
alert(title +" of '"+obj.value +"' is invalid.\nMust be no greater than "+
rangeMax+".");
obj.value = obj.defaultValue;
return false;
}
}
}
return true;
}
}
function validateFloat(obj,min,max)
{ // validates an float which may be restricted to a range (if min and/or max are numbers)
var title = obj.title;
var rangeMin=parseFloat(min);
var rangeMax=parseFloat(max);
if (title.length === 0)
title = "Value";
var popup=( theClient.isIePre11() === false );
for (;;) {
if ((obj.value === undefined || obj.value === null || obj.value === "")
&& isFloat(obj.defaultValue))
obj.value = obj.defaultValue;
if (!isFloat(obj.value)) {
if (popup) {
obj.value = prompt(title +" is invalid.\nMust be a number.",obj.value);
continue;
} else {
alert(title +" of '"+obj.value +"' is invalid.\nMust be a number."); // try a prompt box!
obj.value = obj.defaultValue;
return false;
}
}
var val = parseFloat(obj.value);
if (isFloat(min) && isFloat(max)) {
if (val < rangeMin || val > rangeMax) {
if (popup) {
obj.value = prompt(title +" is invalid.\nMust be between "+rangeMin+" and "+
rangeMax+".",obj.value);
continue;
} else {
alert(title +" of '"+obj.value +"' is invalid.\nMust be between "+rangeMin+
" and "+rangeMax+".");
obj.value = obj.defaultValue;
return false;
}
}
} else if (isFloat(min)) {
if (val < rangeMin) {
if (popup) {
obj.value = prompt(title +" is invalid.\nMust be no less than "+rangeMin+
".",obj.value);
continue;
} else {
alert(title +" of '"+obj.value +"' is invalid.\nMust be no less than "+
rangeMin+".");
obj.value = obj.defaultValue;
return false;
}
}
} else if (isFloat(max)) {
if (val > rangeMax) {
if (popup) {
obj.value = prompt(title +" is invalid.\nMust be no greater than "+
rangeMax+".",obj.value);
continue;
} else {
alert(title +" of '"+obj.value +"' is invalid.\nMust be no greater than "+
rangeMax+".");
obj.value = obj.defaultValue;
return false;
}
}
}
return true;
}
}
function validateLabel(label)
{ // returns true if label is valid in trackDb as short or long label
var regexp = /^[a-z][ a-z0-9/'!\$()*,\-.:;<=>?@\[\]^_`{|}~]*$/i;
if (regexp.test(label)) {
return true;
} else {
alert(label + " is an invalid label. The first character must be alphabetical and the rest of the string be alphanumeric or the following puncuation ~`!@$/^*.()_-=[{]}?|;:'<,>");
return false;
}
}
function validateUrl(url)
{ // returns true if url is a valid url, otherwise returns false and shows an alert
// I got this regexp from http://stackoverflow.com/questions/1303872/url-validation-using-javascript
var regexp = /^(https?|ftp|gs|s3|drs):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
if (regexp.test(url)) {
return true;
} else {
alert(url + " is an invalid url");
return false;
}
}
function metadataIsVisible(trackName)
{
var divit = normed($("#div_"+trackName+"_meta"));
if (!divit)
return false;
return ($(divit).css('display') !== 'none');
}
function metadataShowHide(trackName,showLonglabel,showShortLabel)
{
// Will show subtrack specific configuration controls
// Config controls not matching name will be hidden
var divit = normed($("#div_"+trackName+"_meta"));
if (!divit)
return false;
var img = normed($(divit).prev('a').find("img"));
if (img) {
if ($(divit).css('display') === 'none')
$(img).attr('src','../images/upBlue.png');
else
$(img).attr('src','../images/downBlue.png');
}
if ($(divit).css('display') === 'none') {
if (typeof(subCfg) === "object") {// subCfg.js file included?
var cfg = normed($("#div_cfg_"+trackName));
if (cfg) // Hide any configuration when opening metadata
$(cfg).hide();
}
}
var tr = $(divit).parents('tr');
if (tr.length > 0) {
tr = tr[0];
var bgClass = null;
var classes = $( tr ).attr("class").split(" ");
for (var ix=0;ix 5)
newVal = arguments[5] ? 0 : 1;
var oldSrc = $(button).attr("src");
if (oldSrc && oldSrc.length > 0) {
// Old img version of the toggleButton
if (newVal === -1)
newVal = oldSrc.indexOf("/remove") > 0 ? 1 : 0;
if (newVal === 1)
$(button).attr("src", oldSrc.replace("/remove", "/add") );
else
$(button).attr("src", oldSrc.replace("/add", "/remove") );
} else {
// new BUTTONS_BY_CSS
if (newVal === -1) {
oldSrc = $(button).text();
if (oldSrc && oldSrc.length === 1)
newVal = (oldSrc === '+') ? 0 : 1;
else {
warn("Uninterpretable toggleButton!");
newVal = 0;
}
}
if (newVal === 1)
$(button).text('+');
else
$(button).text('-');
}
var contents = $("tr[id^='"+prefix+"-']");
if(newVal === 1) {
$(button).attr('title', 'Expand this '+titleDesc);
contents.hide();
} else {
$(button).attr('title', 'Collapse this '+titleDesc);
contents.show().trigger('show');
}
$(hidden).val(newVal);
if (doAjax) {
setCartVar(hiddenPrefix+"_"+prefix+"_close", newVal);
}
retval = false;
}
return retval;
}
function getNonce(debug)
{ // Gets nonce value from page meta header
var content = $("meta[http-equiv='Content-Security-Policy']").attr("content");
if (!content)
return "";
// parse nonce like 'nonce-JDPiW8odQkiav4UCeXsa34ElFm7o'
var sectionBegin = "'nonce-";
var sectionEnd = "'";
var ix = content.indexOf(sectionBegin);
if (ix < 0)
return "";
content = content.substring(ix+sectionBegin.length);
ix = content.indexOf(sectionEnd);
if (ix < 0)
return "";
content = content.substring(0,ix);
if (debug)
alert('page nonce='+content);
return content;
}
function notifBoxShow(cgiName, keyName) {
/* move the notification bar div under '#TrackHeaderForm' */
let lsKey = cgiName + "_" + keyName;
if (localStorage.getItem(lsKey))
return;
var notifEl = document.getElementById(lsKey + "notifBox");
if (!notifEl) {
// missing call to setup function (ie generated server side like a udcTimeout message)
notifBoxSetup(cgiName, keyName);
}
// TODO: make a generic element for positioning this
var parentEl = document.getElementById('TrackHeaderForm');
if (parentEl) {
parentEl.appendChild(notifEl);
notifEl.style.display = 'block';
}
}
function notifBoxSetup(cgiName, keyName, msg, skipCloseButton) {
/* Create a notification box if one hasn't been created, and
* add msg to the list of shown notifications.
* cgiName.keyName will be saved to localStorage in order to show
* or hide this notification.
* Must call notifBoxShow() in order to display the notification */
lsKey = cgiName + "_" + keyName;
if (localStorage.getItem(lsKey))
return;
let alreadyPresent = false;
let notifBox = document.getElementById(lsKey+"notifBox");
if (notifBox) {
alreadyPreset = true;
if (msg) {
notifBox.innerHTML += " " + msg;
}
} else {
notifBox = document.createElement("div");
notifBox.className = "notifBox";
notifBox.style.display = "none";
notifBox.style.width = "90%";
notifBox.style.marginLeft = "100px";
notifBox.id = lsKey+"notifBox";
if (msg) {
notifBox.innerHTML = msg;
}
}
var closeHtml = " ";
var buttonStyles = "text-align: center";
// XX code review: The close button does not sense at all, why not just always remove it?
if (skipCloseButton===true) {
closeHtml = "";
buttonStyles += "; display: inline; margin-left: 20px";
}
notifBox.innerHTML += "
"+
closeHtml +
""+
"
";
if (!alreadyPresent) {
document.body.appendChild(notifBox);
}
$("#"+lsKey+"notifyHide").click({"id":lsKey}, function() {
let key = arguments[0].data.id;
$("#"+key+"notifBox").remove();
});
$("#"+lsKey+"notifyHideForever").click({"id": lsKey}, function() {
let key = arguments[0].data.id;
$("#"+key+"notifBox").remove();
localStorage.setItem(key, "1");
});
}
function warnBoxJsSetup()
{ // Sets up warnBox if not already established. This is duplicated from htmshell.c
var html = "";
html += "
";
html += "
";
html += "
";
html += "
";
html += "
";
html += "
";
// GALT TODO either add nonce or move the showWarnBox and hideWarnBox to some universal javascript
// file that is always included. Or consider if we can just dynamically define the functions
// right here inside this function? maybe prepend function names with "window." to (re)define the global functions.
// maybe something like window.showWarnBox = function(){stuff here};
html += "";
$('body').prepend(html);
document.getElementById('warnOK').onclick = function() {hideWarnBox();return false;};
}
function warn(msg)
{ // adds warnings to the warnBox
var warnList = normed($('#warnList')); // warnBox contains warnList
if (!warnList) {
warnBoxJsSetup();
warnList = normed($('#warnList'));
}
if (!warnList)
alert(msg);
else {
// don't add warnings that already exist:
var oldMsgs = [];
$('#warnList li').each(function(i, elem) {
oldMsgs.push(elem.innerHTML);
});
// make the would-be new message into an
element so the case and quotes
// match any pre-existing ones
var newNode = document.createElement('li');
newNode.innerHTML = msg;
if (oldMsgs.indexOf(newNode.innerHTML) === -1) {
$( warnList ).append(newNode);
}
if ($.isFunction(showWarnBox))
showWarnBox();
else
alert(msg);
}
}
var gWarnSinceMSecs = 0;
function warnSince(msg) // DEAD CODE?
{ // Warn messages with msecs since last warnSince msg
// This is necessary because IE Developer tools are hanging
var now = new Date();
var msecs = now.getTime();
var since = 0;
if (gWarnSinceMSecs > 0)
since = msecs - gWarnSinceMSecs;
gWarnSinceMSecs = msecs;
warn('['+since+'] '+msg);
}
function cgiBooleanShadowPrefix()
// Prefix for shadow variable set with boolean variables.
// Exact copy of code in cheapcgi.c
{
return "boolshad.";
}
function getAllVars(obj,subtrackName)
{
// Returns a hash for all inputs and selects in an obj.
// If obj is undefined then obj is document!
var urlData = {};
if (!obj)
obj = $('document');
var inp = $(obj).find('input');
var sel = $(obj).find('select');
//warn("obj:"+$(obj).attr('id') + " inputs:"+$(inp).length+ " selects:"+$(sel).length);
$(inp).filter(':not([name^="boolshad"]):enabled').each(function (i) {
var name = $(this).attr('name');
var val = $(this).val();
if ($(this).attr('type') === 'checkbox' || $(this).attr('type') === "CHECKBOX") {
name = cgiBooleanShadowPrefix() + name;
val = $(this).prop('checked') ? 1 : 0;
} else if ($(this).attr('type') === 'radio') {
if (!$(this).prop('checked')) {
name = undefined;
}
}
if (name && name !== "Submit" && val !== undefined && val !== null) {
urlData[name] = val;
}
});
// special case the vcfSampleOrder variable because it is a hidden input type that
// changes based on click-drag
$(inp).filter('[name$="vcfSampleOrder"]').each(function (i) {
var name = $(this).attr('name');
var val = $(this).val();
if (name && name !== "Submit" && val !== undefined && val !== null) {
urlData[name] = val;
}
});
// special case the highlight/color picker inputs
$(inp).filter('[id^=colorPicker],[id^=colorInput]').each(function(i) {
// remove the prefix that lets us recognize this setting, what remains
// is specific to the track or for the back end
var name = $(this)[0].id.replace("colorPicker.", "").replace("colorInput.", "");
var val = $(this).val();
urlData[name] = val;
});
$(sel).filter('[name]:enabled').each(function (i) {
var name = $(this).attr('name');
var val = $(this).val();
if (name && val !== undefined && val !== null) {
if (subtrackName && name === subtrackName) {
if (val === 'hide') {
urlData[name+"_sel"] = 0; // Can't delete "_sel" because default takes over
urlData[name] = "[]"; // Can delete vis because
} else { // subtrack vis should be inherited.
urlData[name+"_sel"] = 1;
urlData[name] = val;
}
} else {
if (Array.isArray( val) && val.length > 1) {
urlData[name] = "[" + val.toString() + "]";
} else
urlData[name] = val;
}
}
});
return urlData;
}
function debugDumpFormCollection(collectionName,vars)
{ // dumps form vars collection in an alert
var debugStr = "";
for (var thisVar in vars) {
debugStr += thisVar + "==" + vars[thisVar]+"\n";
}
alert("DEBUG "+ collectionName + ":\n"+debugStr);
}
function varHashChanges(newVars,oldVars)
{ // Returns a hash of all vars that are changed between old and new hash.
// New vars not found in old are changed.
var changedVars = {};
for (var newVar in newVars) {
if (oldVars[newVar] === null || oldVars[newVar] !== newVars[newVar])
changedVars[newVar] = newVars[newVar];
}
return changedVars;
}
function varHashToQueryString(varHash)
{
// return a CGI QUERY_STRING for name/vals in given object
var retVal = "";
var count = 0;
for (var aVar in varHash) {
if (count++ > 0) {
retVal += "&";
}
var val = varHash[aVar];
if (typeof(val) === 'string'
&& val.length >= 2
&& val.indexOf('[') === 0
&& val.lastIndexOf(']') === (val.length - 1)) {
var vals = val.substr(1,val.length - 2).split(',');
/* jshint loopfunc: true */// function inside loop works and replacement is awkward.
$(vals).each(function (ix) {
if (ix > 0)
retVal += "&";
retVal += aVar + "=" + encodeURIComponent(this);
});
} else if (typeof(val) === 'string') {
// sometimes val is already encoded or partially encoded
// the CGI cannot know if user input is double encoded
// so test for already encoded characters here and only
// encode what we need to
retVal += aVar + "=" + val.replace(/(%[0-9A-Fa-f]{2})+|[^%]+/g, (match) => {
if (/%[0-9A-Fa-f]{2}/.test(match)) {
// Already percent-encoded, leave as-is
return match;
}
// Encode unencoded parts
return encodeURIComponent(match);
});
} else {
retVal += aVar + "=" + encodeURIComponent(val);
}
}
return retVal;
}
function getAllVarsAsUrlData(obj) // DEAD CODE?
{
// Returns a string in the form of var1=val1&var2=val2... for all inputs and selects in an obj
// If obj is undefined then obj is document!
return varHashToQueryString(getAllVars(obj));
}
/*
function popupBox(popit, content, popTitle)
{
// Kicks off a Modal Dialog for the provided content.
// Requires jquery-ui.js
// NEEDS SOME WORK
warn(content);
// Set up the popit div if necessary
if (!popit) {
popit = $('#popit');
if (!popit ) {
$('body').prepend("");
popit = $('#popit');
}
}
// Set up the modal dialog
$(popit).html("
" + content + "
");
$(popit).dialog({
ajaxOptions: {
// This doesn't work
cache: true
},
resizable: true,
bgiframe: true,
height: 'auto',
width: 'auto',
minHeight: 200,
minWidth: 400,
modal: true,
closeOnEscape: true,
autoOpen: false,
close: function() { // clear out html after close to prevent
$(popDiv).empty(); // problems caused by duplicate html elements
}
});
// Apparently the options above to dialog take only once, so we set title explicitly.
if (popTitle && popTitle.length > 0)
$(popit).dialog('option' , 'title' , popTitle );
else
$(popit).dialog('option' , 'title' , "Please Respond");
jQuery('body').css('cursor', '');
$(popit).dialog('open');
}
*/
function embedBoxOpen(boxit, content, reenterable) // DEAD CODE?
{ // embeds a box for the provided content.
// This box has 1 button (close) by default and 2 buttons if the name of an applyFunc
// is provided (apply, cancel) If there is no apply function, the box may be reentrent,
// meaning subsequent calls do not need to provide content
// NOTE: 4 extra STRING Params: boxWidth, boxTitle, applyFunc, applyName
// Define extra params now
var boxWidth = "80%";
var boxTitle = "";
var applyFunc = "";
var applyName = "Apply";
if (arguments.length > 3 && arguments[3].length > 0) // FIXME: could check type
boxWidth = arguments[3];
if (arguments.length > 4 && arguments[4].length > 0)
boxTitle = arguments[4];
if (arguments.length > 5 && arguments[5].length > 0)
applyFunc = arguments[5];
if (arguments.length > 6 && arguments[6].length > 0)
applyName = arguments[6];
// Set up the popit div if necessary
if (!boxit) {
boxit = $('div#boxit');
if (!boxit) {
$('body').prepend("");
//$('body').prepend("");
boxit = $('div#boxit');
}
}
if (!reenterable || (content.length > 0)) { // Can reenter without changing content!
var buildHtml = "
";
if (boxTitle.length > 0)
buildHtml += "
" + boxTitle + "
";
buildHtml += "
" + content + "
";
// Set up closing code
var closeButton = "Close";
var closeHtml = "embedBoxClose($(\"#"+ $(boxit).attr('id') + "\"),";
if (reenterable && applyFunc.length === 0)
closeHtml += "true);";
else
closeHtml += "false);";
// Buttons
buildHtml += "
";
if (applyFunc.length > 0) { // "Apply" button and "Cancel" button. Apply also closes!
buildHtml += " ";
closeButton = "Cancel"; // If apply button then close is cancel
}
buildHtml += "
";
$(boxit).html("
" + buildHtml + "
"); // Make it boxed
}
var boxedHtml = $(boxit).html();
if (!boxedHtml || boxedHtml.length === 0)
warn("embedHtmlBox() called without content");
else
$(boxit).show();
}
function embedBoxClose(boxit, reenterable) // DEAD CODE?
{ // Close an embedded box
// NOTE 4 extra STRING Params: boxWidth, boxTitle, applyFunc, applyName
if (boxit) {
$(boxit).hide();
if (!reenterable)
$(boxit).empty();
}
}
function startTiming() // DEAD CODE?
{
var now = new Date();
return now.getTime();
}
function showTiming(start,whatTookSoLong) // DEAD CODE?
{
var now = new Date();
var end = (now.getTime() - start);
warn(whatTookSoLong+" took "+end+" msecs.");
return end;
}
function getHgsid()
{// return current session id
// .first() because hgTracks turned up 3 of these!
var hgsid = normed($("input[name='hgsid']").first());
if (hgsid)
return hgsid.value;
hgsid = getURLParam(window.location.href, "hgsid");
if (hgsid.length > 0)
return hgsid;
// This may be moved to 1st position as the most likely source
if (typeof(common) !== 'undefined' && common.hgsid !== undefined && common.hgsid !== null)
return common.hgsid;
hgsid = normed($("input#hgsid").first());
if (hgsid)
return hgsid.value;
return "";
}
function undecoratedDb(db)
// return the db name with any hub_id_ stripped
{
var retDb = db;
if (db.startsWith("hub_")) {
retDb = db.split('_').slice(2).join('_');
}
return retDb;
}
function getDb()
{
var db = normed($("input[name='db']").first());
if (db)
return db.value;
db = getURLParam(window.location.href, "db");
if (db.length > 0)
return db;
// This may be moved to 1st position as the most likely source
if (typeof(common) !== 'undefined' && common.db)
return common.db;
db = normed($("input#db").first());
if (db)
return db.value;
if (typeof uiState !== "undefined" && uiState.db)
return uiState.db;
db = document.getElementById("selectAssembly");
if (db)
return db.selectedOptions[0].value;
return "";
}
function undecoratedTrack(track)
// return the track name with any hub_id_ stripped
{
var retTrack = track;
if (track.startsWith("hub_")) {
retTrack = track.split('_').slice(2).join("_");
}
return retTrack;
}
function getTrack()
{
var track = normed($("input#g").first());
if (track)
return track.value;
track = normed($("input[name='g']").first());
if (track)
return track.value;
track = getURLParam(window.location.href, "g");
if (track.length > 0)
return track;
// This may be moved to 1st position as the most likely source
if (typeof(common) !== 'undefined' && common.track)
return common.track;
return "";
}
function Rectangle() // DEAD CODE?
{
// Rectangle object constructor:
// calling syntax:
//
// new Rectangle(startX, endX, startY, endY)
// new Rectangle(coords) <-- coordinate string from an area item
if (arguments.length === 4) {
this.startX = arguments[0];
this.endX = arguments[1];
this.startY = arguments[2];
this.endY = arguments[3];
} else if (arguments.length > 0) {
var coords = arguments[0].split(",");
this.startX = coords[0];
this.endX = coords[2];
this.startY = coords[1];
this.endY = coords[3];
} else { // what else to do?
this.startX = 0;
this.endX = 100;
this.startY = 0;
this.endY = 100;
}
}
Rectangle.prototype.contains = function(x, y) // DEAD CODE?
{
// returns true if given points are in the rectangle
var retval = x >= this.startX && x <= this.endX && y >= this.startY && y <= this.endY;
return retval;
};
function commify (str) {
if (typeof(str) === "number")
str = str + "";
var n = str.length;
if (n <= 3) {
return str;
} else {
var pre = str.substring(0, n-3);
var post = str.substring(n-3);
pre = commify(pre);
return pre + "," + post;
}
}
function parsePosition(position)
// Parse chr:start-end string into a chrom, start, end object
{
if (position && position.length > 0) {
position = position.replace(/,/g, "");
var a = /(\S+):(\d+)-(\d+)/.exec(position);
if (a && a.length === 4) {
var o = {};
o.chrom = a[1];
o.start = parseInt(a[2]);
o.end = parseInt(a[3]);
return o;
}
}
return null;
}
function makeHighlightString(db, chrom, start, end, color) {
/* given db and a range on it and a color (color must be prefixed by #),
* return the highlight string in the cart for it. See parsePositionWithDb for the history
* of the various accepted highlight strings */
return db+"#"+chrom+"#"+start+"#"+end+color;
}
function parsePositionWithDb(position)
// returns an object with chrom, start, end and optionally color attributes
// position is a string and can be in one of five different formats:
// 0) chr:start-end
// 1) db.chr:start-end
// 2) db.chr:start-end#color
// 3) db#chr#start#end#color
// Formats 0-2 are only supported for backwards compatibility with old carts
{
var out = {};
var parts = null;
if (position.split("#").length !==5 ) {
// formats of old carts: 0-2
parts = position.split(".");
// handle the db part
if (parts.length === 2) {
out.db = parts[0];
position = parts[1];
} else {
out.db = getDb(); // default the db
}
// position now contains chr:start-end#color
parts = position.split("#"); // Highlight Region may carry its color
if (parts.length === 2) {
position = parts[0];
out.color = '#' + parts[1];
}
var pos = parsePosition(position);
if (pos) {
out.chrom = pos.chrom;
out.start = pos.start;
out.end = pos.end;
}
} else {
// new format
parts = position.split("#");
out.db = parts[0];
out.chrom = parts[1];
out.start = parseInt(parts[2]);
out.end = parseInt(parts[3]);
out.color = "#" + parts[4];
}
return out;
}
function getHighlight(highlightStr, index)
/* Parse out highlight at index and return as a position object (see parsePositionWithDb) */
{
var hlStrings = highlightStr.split("|");
var myHlStr = hlStrings[index];
var posObj = parsePositionWithDb(myHlStr);
return posObj;
}
function getSizeFromCoordinates(position)
{
// Parse size out of a chr:start-end string
var o = parsePosition(position);
if (o) {
return o.end - o.start + 1;
}
return null;
}
// This code is intended to allow setting up a wait cursor while waiting on the function
var gWaitFuncArgs = [];
var gWaitFunc;
function waitMaskClear()
{ // Clears the waitMask
var waitMask = normed($('#waitMask'));
if (waitMask)
$(waitMask).hide();
}
function waitMaskSetup(timeOutInMs)
{ // Sets up the waitMask to block page manipulation until cleared
// Find or create the waitMask (which masks the whole page)
var waitMask = normed($('#waitMask'));
if (!waitMask) {
// create the waitMask
$("body").append("");
waitMask = normed($('#waitMask'));
}
$(waitMask).css({opacity:0.0,display:'block',top: '0px',
height: $(document).height().toString() + 'px' });
// Special for IE, since it takes so long, make mask obvious
//if (theClient.isIePre11())
// $(waitMask).css({opacity:0.4,backgroundColor:'gray'});
// Things could fail, so always have a timeout.
if (!timeOutInMs) // works for undefined, null and 0
timeOutInMs = 30000; // IE can take forever!
if (timeOutInMs > 0)
setTimeout(waitMaskClear,timeOutInMs); // Just in case
return waitMask; // The caller could add css if they wanted.
}
function _launchWaitOnFunction()
{ // should ONLY be called by waitOnFunction()
// Launches the saved function
var func = gWaitFunc;
gWaitFunc = null;
var funcArgs = gWaitFuncArgs;
gWaitFuncArgs = [];
if (!func || !jQuery.isFunction(func))
warn("_launchWaitOnFunction called without a function");
else
func.apply(this, funcArgs);
// Special if the first var is a button that can visually be inset
if (funcArgs.length > 0 && funcArgs[0].type) {
if (funcArgs[0].type === 'button' && $(funcArgs[0]).hasClass('inOutButton')) {
$(funcArgs[0]).css('borderStyle',"outset");
}
}
// Now we can get rid of the wait cursor
waitMaskClear();
}
function waitOnFunction(func)
{ // sets the waitMask (wait cursor and no clicking),
// then launches the function with up to 5 arguments
if (!jQuery.isFunction(func)) {
warn("waitOnFunction called without a function");
return false;
}
if (gWaitFunc) {
if (gWaitFunc === func) // already called (sometimes hapens when onchange event is triggered
return true; // by js (rather than direct user action). Happens in IE8
warn("waitOnFunction called but already waiting on a function");
return false;
}
waitMaskSetup(0); // Find or create waitMask (which masks whole page) but gives up after 5sec
// Special if the first var is a button that can visually be inset
if (arguments.length > 1 && arguments[1].type) {
if (arguments[1].type === 'button' && $(arguments[1]).hasClass('inOutButton')) {
$(arguments[1]).css( 'borderStyle',"inset");
}
}
// Build up the aruments array
for (var aIx=1; aIx < arguments.length; aIx++) {
gWaitFuncArgs.push(arguments[aIx]);
}
gWaitFunc = func;
setTimeout(_launchWaitOnFunction,10);
}
// --- yielding iterator ---
function _yieldingIteratorObject(yieldingFunc) // DEAD CODE?
{ // This is the "recusive object" or ro which is instantiated in waitOnIteratingFunction
// yieldingFunc is passed in from waitOnIteratingFunction
// and will recurse which recursively calls an iterator
this.step = function(msecs,args) {
setTimeout(function() { yieldingFunc(args); }, msecs); // recursive timeouts
return;
};
}
function yieldingIterator(iteratingFunc,continuingFunc,args) // DEAD CODE?
{ // Will run iteratingFunc function with "yields", then run continuingFunc
// Based upon design by Guido Tapia, PicNet
// iteratingFunc must return number of msecs to pause before next interation.
// return 0 ends iteration with call to continuingFunc
// return < 0 ends iteration with no call to continuingFunc
// Both iteratingFunc and continuingFunc will receive the single "args" param.
// Hint. for multiple args, create a single struct object
var ro = new _yieldingIteratorObject(function() {
var msecs = iteratingFunc(args);
if (msecs > 0)
ro.step(msecs,args); // recursion
else if (msecs === 0)
continuingFunc(args); // completion
// else (msec < 0) // abandon
});
ro.step(1,args); // kick-off
}
function showLoadingImage(id)
// Show a loading image above the given id; return's id of div added (allowing later removal).
{
var loadingId = id + "LoadingOverlay";
var overlay = $("");
var ele = $(document.getElementById(id));
overlay.appendTo("body");
var divLeft = ele.position().left + 2;
var width = ele.width() - 1;
var height = ele.height();
overlay.width(width);
overlay.height(height);
overlay.css({top: (ele.position().top + 1) + 'px', left: divLeft + 'px'});
return loadingId;
}
function hideLoadingImage(id)
{
$(document.getElementById(id)).remove();
}
function codonColoringChanged(name)
{ // Updated disabled state of codonNumbering checkbox based on current value
// of track coloring select.
var val = $("select[name='" + name + ".baseColorDrawOpt'] option:selected").text();
$("input[name='" + name + ".codonNumbering']").attr('disabled', val === "OFF");
$("#" + name + "CodonNumberingLabel").toggleClass("disabled", val === "OFF" ? true : false);
}
function gtexTransformChanged(name)
{ // Disable view limits settings if log transform enabled
// NOTE: selector strings are a bit complex due to dots GB vars/attributes (track.var)
// so can't use more concise jQuery syntax
// check log transform
var logCheckbox = $("input[name='" + name + ".logTransform']");
var isLogChecked = logCheckbox.attr('checked');
// enable/disable view limits
var maxTextbox = $("input[name='" + name + ".maxViewLimit']");
maxTextbox.attr('disabled', isLogChecked);
var maxTextLabel = $("." + name + "ViewLimitsMaxLabel");
maxTextLabel.toggleClass("disabled", isLogChecked ? true : false);
}
function barChartUiTransformChanged(name) {
// Disable view limits settings if log transform enabled
gtexTransformChanged(name);
}
function gtexSamplesChanged(name)
{ // Disable and comparison controls if all samples selected
// check sample select
var sampleSelect = $("input[name='" + name + ".samples']:checked");
var isAllSamples = (sampleSelect.val() === 'all');
// enable/disable comparison options
// limiting to radio buttons as there is a problem with tissue checkbox naming on popup
var comparisonButtons = $("input[type='radio' name='" + name + ".comparison']");
comparisonButtons.attr('disabled', isAllSamples);
var comparisonLabel = $("." + name + "ComparisonLabel");
comparisonLabel.toggleClass("disabled", isAllSamples ? true : false);
}
var bindings = {
// This object is for finding a subtring using tokens as bounds
// The tokens can be literal strings or regular expressions.
// If regular expressions are used, then only the first expression found will count
// If not using regexp, then you can pass in limits to the original string
_raw: function (begToken,endToken,someString,ixBeg,ixEnd)
{ // primitive not meant to be called directly but by bindings.inside and bindings.outside
if (someString.length <= 0)
return '';
if (ixBeg === undefined || ixBeg === null)
ixBeg = 0;
if (ixEnd === undefined || ixEnd === null)
ixEnd = someString.length;
var insideBeg = ixBeg;
var insideEnd = ixEnd;
if (begToken.constructor.name === "RegExp")
insideBeg = someString.search(begToken);
else if (begToken.length > 0)
insideBeg = someString.indexOf(begToken,ixBeg);
if (endToken.constructor.name === "RegExp")
insideEnd = someString.search(endToken);
else if (endToken.length > 0)
insideEnd = someString.indexOf(endToken,ixBeg);
if (ixBeg <= insideBeg && insideBeg <= insideEnd && insideEnd <= ixEnd)
return {start : insideBeg, stop : insideEnd};
return {start : -1, stop : -1};
},
inside: function (begToken,endToken,someString,ixBeg,ixEnd)
{ // returns the inside bounds of 2 tokens within a string
// Note ixBeg and ixEnd are optional bounds already established within string
// Pattern match can be used instead of literal token if a regexp is passed in for the tokens
var bounds = bindings._raw(begToken,endToken,someString,ixBeg,ixEnd);
if (bounds.start > -1) {
if (begToken.constructor.name === "RegExp")
bounds.start += someString.match(begToken)[0].length;
else
bounds.start += begToken.length;
}
return bounds;
},
outside: function (begToken,endToken,someString,ixBeg,ixEnd)
{ // returns the outside bounds of 2 tokens within a string
// Note ixBeg and ixEnd are optional bounds already established within string
// Pattern match can be used instead of literal token if a regexp is passed in for the tokens
var bounds = bindings._raw(begToken,endToken,someString,ixBeg,ixEnd);
if (bounds.start > -1) {
if (endToken.constructor.name === "RegExp")
bounds.stop += someString.match(endToken)[0].length;
else
bounds.stop += endToken.length;
}
return bounds;
},
insideOut: function (begToken,endToken,someString,ixBeg,ixEnd)
{ // returns what falls between begToken and endToken as found in the string provided
// Note ixBeg and ixEnd are optional bounds already established within string
var bounds = bindings.inside(begToken,endToken,someString,ixBeg,ixEnd);
if (bounds.start < bounds.stop)
return someString.slice(bounds.start,bounds.stop);
return '';
}
};
function stripHgErrors(returnedHtml, whatWeDid)
{ // strips HGERROR style 'early errors' and shows them in the warnBox
// If whatWeDid !== null, we use it to return info about what we stripped out and
// processed (current just warnMsg).
var cleanHtml = returnedHtml;
var begToken = '';
var endToken = '';
while (cleanHtml.length > 0) {
var bounds = bindings.outside(begToken,endToken,cleanHtml);
if (bounds.start === -1)
break;
// OLD WAY var warnMsg = bindings.insideOut('
','
',cleanHtml,bounds.start,bounds.stop);
var warnMsg = cleanHtml.slice(bounds.start+begToken.length,bounds.stop-endToken.length);
if (warnMsg.length > 0) {
warn(warnMsg);
if (whatWeDid)
whatWeDid.warnMsg = warnMsg;
}
cleanHtml = cleanHtml.slice(0,bounds.start) + cleanHtml.slice(bounds.stop);
}
return cleanHtml;
}
function stripJsFiles(returnedHtml, debug, whatWeDid)
{ // strips javascript files from html returned by ajax
var cleanHtml = returnedHtml;
// Match script tags with src attribute - handles single/double quotes, with/without type attr
var shlurpPattern=/";
var lastIx = 0;
while (1) {
var ix = html.indexOf(sectionBegin, lastIx);
if (ix < 0)
break;
var ix2 = ix + sectionBegin.length;
var ex = html.indexOf(sectionEnd, ix2);
if (ex < 0)
break;
content += html.substring(lastIx,ix);
var jsNonce = html.substring(ix2,ex);
if (debug)
alert("jsNonce:"+jsNonce);
results.push(jsNonce);
lastIx = ex + sectionEnd.length;
}
// grab the last piece.
content += html.substring(lastIx);
//return results;
if (whatWeDid)
whatWeDid.js = results;
return content;
}
function charsAreHex(s)
// are all the chars found hex?
{
var hexChars = "01234566789abcdefABCDEF";
var d = false;
var i = 0;
if (s) {
d = true;
while (i < s.length) {
if (hexChars.indexOf(s.charAt(i++)) < 0)
d = false;
}
}
return d;
}
function nonAlphaNumericHexDecodeText(s, prefix, postfix)
// For html tag attributes, it decodes non-alphanumeric characters
// with HH hex codes.
// Decoding happens in-place, changing the input string s.
// prefix must not be empty string or null, but postfix can be empty string.
// Because the decoded string is always equal to or shorter than the input string,
// the decoding is just done in-place modifying the input string.
// Accepts upper and lower case values in entities.
//
{
var d = "";
var pfxLen = prefix.length;
var postLen = postfix.length;
var i = 0;
if (s) {
while (i < s.length) {
var matched = false;
if (i+pfxLen+postLen+2 <= s.length) {
var pre = s.substr(i, pfxLen).toLowerCase();
if (pre === prefix) {
var post = s.substr(i+pfxLen+2, postLen).toLowerCase();
if (post === postfix) {
var hex = s.substr(i+pfxLen, 2);
if (charsAreHex(hex)) {
d = d + String.fromCharCode(parseInt(hex,16));
i += pfxLen + 2 + postLen;
matched = true;
}
}
}
}
if (!matched)
d = d + s.charAt(i++);
}
}
return d;
}
function jsDecode(s)
// For JS string values decode "\xHH"
{
return nonAlphaNumericHexDecodeText(s, "\\x", "");
}
function stripCSPAndNonceJs(content, debug, whatWeDid)
// Strip CSP Header and script blocks with the ajax nonce.
{
var pageNonce = getNonce(debug);
var csp = {};
content = stripCspHeader(content, debug, csp);
var ajaxNonce = parseNonce(csp.csp, debug);
var jsBlocks = {};
content = stripJsNonce(content, ajaxNonce, debug, jsBlocks);
if (whatWeDid) {
whatWeDid.pageNonce = pageNonce;
whatWeDid.ajaxNonce = ajaxNonce; // Not in use yet.
whatWeDid.js = jsBlocks.js;
}
return stripHgErrors(content, whatWeDid); // Certain early errors are not called via warnBox
}
function appendNonceJsToPage(jsNonce)
// Append ajax js blocks with nonce.
// Create jsNonce by calling stripCSPAndNonceJs.
// Call this after ajax html content has been added to the page/DOM.
{
var i;
for (i=0; i 0) {
var begPattern = //i;
var endPattern = /<\/script\>/i;
var bounds = bindings.outside(begPattern,endPattern,cleanHtml);
if (bounds.start === -1)
break;
var jsEmbeded = cleanHtml.slice(bounds.start,bounds.stop);
if (-1 === jsEmbeded.indexOf("showWarnBox")) {
if (debug)
alert("jsEmbedded:'"+jsEmbeded+"'\n---------------\n"+cleanHtml);
} else {
var warnMsg = bindings.insideOut('
','
',cleanHtml,bounds.start,bounds.stop);
if (warnMsg.length > 0) {
warnMsg = jsDecode(warnMsg);
warn(warnMsg);
if (whatWeDid)
whatWeDid.warnMsg = warnMsg;
}
}
cleanHtml = cleanHtml.slice(0,bounds.start) + cleanHtml.slice(bounds.stop);
}
return stripHgErrors(cleanHtml, whatWeDid); // Certain early errors are not called via warnBox
}
function stripMainMenu(returnedHtml, debug, whatWeDid)
{ // strips main menu div from html returned by ajax
// NOTE: any warnBox style errors will be put into the warnBox
// If whatWeDid !== null, we use it to return info about
// what we stripped out and processed (current just warnMsg).
var cleanHtml = returnedHtml;
// embedded javascript?
while (cleanHtml.length > 0) {
var begPattern = '
';
var endPattern = '
';
var bounds = bindings.outside(begPattern,endPattern,cleanHtml);
if (bounds.start === -1)
break;
var mainMenu = cleanHtml.slice(bounds.start,bounds.stop);
if (-1 === mainMenu.indexOf("showWarnBox")) {
if (debug)
alert("mainMenu:'"+mainMenu+"'\n---------------\n"+cleanHtml);
} else {
var warnMsg = bindings.insideOut('
','
',cleanHtml,bounds.start,bounds.stop);
if (warnMsg.length > 0) {
warn(warnMsg);
if (whatWeDid)
whatWeDid.warnMsg = warnMsg;
}
}
cleanHtml = cleanHtml.slice(0,bounds.start) + cleanHtml.slice(bounds.stop);
}
return stripHgErrors(cleanHtml, whatWeDid); // Certain early errors are not called via warnBox
}
function visTriggersHiddenSelect(obj)
{ // SuperTrack child changing vis should trigger superTrack reshaping.
// This is done by setting hidden input "_sel"
var trackName_Sel = $(obj).attr('name') + "_sel";
var theForm = $(obj).closest("form");
var visible = (obj.selectedIndex !== 0);
if (visible) {
updateOrMakeNamedVariable(theForm,trackName_Sel,"1");
} else
disableNamedVariable(theForm,trackName_Sel);
return true;
}
function setCheckboxList(list, value)
{ // set value of all checkboxes in semicolon delimited list
var names = list.split(";");
for (var i=0; i < names.length; i++) {
$("input[name='" + names[i] + "']").attr('checked', value);
}
}
function calculateHgTracksWidth()
{
// return appropriate width for hgTracks image given users current window width
return $(window).width() - 20;
}
function addPixAndReloadPage()
/* users who do not come in from hgGateway have no pix variable in the URL nor the cart.
* This is a rare case, and the solution is brute force: if it happens, set pix, then reload the entire page.
* This will only happen once to these users, as afterwards the cookie is set. */
{
var winWidth = calculateHgTracksWidth();
var myUrl = window.location.href;
var sep = '?';
if (myUrl.indexOf('?')!==-1)
sep = '&';
var newUrl = myUrl+sep+"pix="+winWidth;
window.location.href = newUrl;
}
function hgTracksSetWidth()
{
var winWidth = calculateHgTracksWidth();
if ($("#imgTbl").length === 0) {
// XXXX what's this code for?
$("#TrackForm").append('');
//$("#TrackForm").submit();
} else {
$("input[name=pix]").val(winWidth);
}
}
function filterByMaxHeight(multiSel)
{ // Setting a max height to scroll dropdownchecklists but
// multiSel is hidden when this is done, so it's position and height must be estimated.
var pos = $(multiSel).closest(':visible').offset().top + 30;
if (pos <= 0)
pos = 260;
// Special mess since the filterBy's on non-current tabs will calculate pos badly.
var tabbed = normed($('input#currentTab'));
if (tabbed) {
var tabDiv = $(multiSel).parents('div#'+ $(tabbed).attr('value'));
if (!tabDiv || $(tabDiv).length === 0) {
pos = 360;
}
}
var maxHeight = $(window).height() - pos;
var selHeight = ($(multiSel).children().length + 1) * 22;
if (maxHeight > selHeight)
maxHeight = null;
return maxHeight;
}
//////////// Drag and Drop ////////////
function tableDragAndDropRegister(thisTable)
{// Initialize a table with tableWithDragAndDrop
if ($(thisTable).hasClass("tableWithDragAndDrop") === false)
return;
$(thisTable).tableDnD({
onDragClass: "trDrag",
dragHandle: "dragHandle",
onDrop: function(table, row, dragStartIndex) {
if (row.rowIndex !== dragStartIndex) {
if (sortTable.savePositions) {
sortTable.savePositions(table);
}
}
}
});
$(thisTable).find("td.dragHandle").hover(
function(){ $(this).closest('tr').addClass('trDrag'); },
function(){ $(this).closest('tr').removeClass('trDrag'); }
);
}
///////////////////////////////
////////// Sort Table /////////
///////////////////////////////
var sortTable = {
// The sortTable object handles sorting HTML tables on columns.
// Just add the 'sortable' class to your table and in ready() call
// sortTable.initialize($('table.sortable')).
//
// Details you don't need to know until you want to do something fancy.
// A sortable table requires:
// TABLE.sortable: TABLE class='sortable' containing a THEAD header and sortable TBODY filled
// with the rows to sort.
// THEAD.sortable: (NOTE: created if not found) THEAD can contain multiple rows must contain:
// TR.sortable: exactly 1 header TH (tr) class='sortable' which will declare the sort columns:
// TH.sortable: 1 or more TH (column headers) with class='sortable sort1 [sortRev]'
// (or sort2, sort3) declaring sort order and whether reversed. e.g.:
//
...
// (this means that factor is currently the third sort column and reverse sorted)
// NOTE: If no TH.sortable is found, then every th in the TR.sortable will be converted
// for you and will be in sort1,2,3 order.
// ONCLICK: Each TH.sortable must call sortTable.sortOnButtonPress(this) directly
// or indirectly in the onclick event e.g.:
//
// NOTE: onclick function will automatically be added if not found.
// SUP: Each TH.sortable *may* contain a which will be filled with an
// up or down arrow and the column's sort order: e.g. ↓2
// NOTE: The sup can be added via the addSuperscript option in sortTable.initialize().
// TBODY.sortable: (NOTE: created if not found) The TBODY class='sortable' contains the
// table rows that get sorted:
// TBODY->TR & ->TD: Each row contains a TD for each sortable column.
// The innerHTML (entire contents) of the cell will be used for sorting.
// TRICK: You can use the 'abbr' field to subtly alter the sortable contents.
// Otherwise sorts on td contents ($(td).text()). Use the abbr field to make
// case-insensitive sorts or force exceptions to alpha-text order
// (such as. ZCTRL vs Control forcing controls to bottom) e.g.:
//
Control
// This is also the method to ensure a numeric sort e.g.:
//
3.2 GB
// IMPORTANT: You must add abbr='use' to the TH.sortable definitions.
// Finally if you want the tableSort to alternate the table row colors (using #FFFEE8 and #FFF9D2)
// then TBODY.sortable should also have class 'altColors'
// NOTE: This class can be added by using the altColors option to sortTable.initialize().
//
// PRESERVING TO CART: To send the sort column on a form 'submit', the header tr (TR.sortable)
// needs a named hidden input of class='sortOrder' as:
//
// AND each sortable column header (TH.sortable) must have id='{name}' which is the name of
// the sortable field (e.g. 'factor', 'shortLabel'). The value preserves the column sort order
// and direction based upon the id={name} of each sort column. In the example, while 'cell' may
// be the first column, the table is currently reverse ordered by 'factor', then by cell and view.
// And to send the sorted row orders on form 'submit', each TBODY->TR will need a named hidden
// input field of class='trPos'. e.g.:
//
// A reason to preserve the order in the cart is if the order will affect other cgis.
// For instance: sort subtracks and see that order in the hgTracks image.
// Sorting a table by columns relies upon the columns obj, whose C equivalent would look like:
//struct column
// {
// char * tags[]; // a list of field names in sort order (e.g. 'cell', 'shortLabel')
// boolean reverse[]; // the sort direction for each sort field
// int cellIxs[]; // The indexes of the columns in the table to be sorted
// boolean useAbbr[]; // Compare on Abbr or on text()?
// };
// These 2 globals are used during setTimeout, so that rows can be hidden while sorting
// and javascript timeout on slow (IE) browsers is less likely
columns: null,
tbody: null,
loadingId: null,
caseSensitive: false, // sorts are case INSENSITIVE by default
sortCaseSensitive: function (sensitive)
{ // set case senstivity, which can be added to each sortable columnn's onclick event.
// or set for the whole table right after initialize()
sortTable.caseSensitive = sensitive;
},
row: function (tr,sortColumns)
{
this.fields = [];
this.reverse = [];
this.row = tr;
for (var ix=0; ix < sortColumns.cellIxs.length; ix++)
{
var cell = tr.cells[sortColumns.cellIxs[ix]];
this.fields[ix] = (sortColumns.useAbbr[ix] ? cell.abbr : $(cell).text());
if (!sortTable.caseSensitive)
this.fields[ix] = this.fields[ix].toLowerCase(); // case insensitive sorts
this.reverse[ix] = sortColumns.reverse[ix];
}
},
rowCmp: function (a,b)
{
for (var ix=0; ix < a.fields.length; ix++) {
if (a.fields[ix] > b.fields[ix])
return (a.reverse[ix] ? -1:1);
else if (a.fields[ix] < b.fields[ix])
return (a.reverse[ix] ? 1:-1);
}
return 0;
},
field: function (value,reverse,row)
{
if (sortTable.caseSensitive || typeof(value) !== 'string')
this.value = value;
else
this.value = value.toLowerCase(); // case insensitive sorts
this.reverse = reverse;
this.row = row;
},
fieldCmp: function (a,b)
{
if (a.value > b.value)
return (a.reverse ? -1:1);
else if (a.value < b.value)
return (a.reverse ? 1:-1);
return 0;
},
sort: function (tbody,sortColumns)
{// Sorts table based upon rules passed in by function reference
// Expects tbody to not sort thead, but could take table
// The sort routine available is javascript array.sort(), which cannot sort rows directly
// Until we have jQuery >=v1.4, we cannot easily convert tbody.rows[] inot a javascript array
// So we will make our own array, sort, then then walk through the table and reorder
// FIXME: Until better methods are developed, only sortOrder based sorts are supported
// and fnCompare is obsolete
// Create an array of rows to sort
var rows = [];
var trs = tbody.rows;
$(trs).each(function(ix) {
rows.push(new sortTable.row(this, sortColumns));
});
// Sort the array
rows.sort(sortTable.rowCmp);
// most efficient reload of sorted rows I have found
var sortedRows = jQuery.map(rows, function(row, i) {
return row.row;
});
$(tbody).append(sortedRows);
sortTable.tbody=tbody;
sortTable.columns=sortColumns;
// Avoid js timeout
setTimeout(function() {
sortTable.sortFinish(sortTable.tbody,sortTable.columns);
},5);
},
sortFinish: function (tbody,sortColumns)
{ // Additional sort cleanup.
// This is in a separate function to allow calling with setTimeout() which will
// prevent javascript timeouts (I hope)
sortTable.savePositions(tbody);
if ($(tbody).hasClass('altColors'))
sortTable.alternateColors(tbody,sortColumns);
$(tbody).parents("table.tableWithDragAndDrop").each(function (ix) {
tableDragAndDropRegister(this);
});
if (sortTable.loadingId)
hideLoadingImage(sortTable.loadingId);
},
sortByColumns: function (tbody,sortColumns)
{ // Will sort the table based on the abbr values on a set of
// (this means that factor is currently the third sort column and reverse sorted)
// NOTE: If no TH.sortable is found, then every th in the TR.sortable will be
// converted for you and will be in sort1,2,3 order.)
// ONCLICK: Each TH.sortable must call sortTable.sortOnButtonPress(this) directly or
// indirectly in the onclick event. e.g.:
//
// NOTE: onclick function will automatically be added if not found.
// SUP: Each TH.sortable *may* contain a which will be filled with an up or down
// arrow and the column's sort order: e.g. ↓2
// NOTE: If no sup is found but addSuperscript is requested, then they will be added.
// TBODY.sortable: (NOTE: created if not found) The TBODY class='sortable' contains the
// table rows that get sorted:
// TBODY->TR & ->TD: Each row contains a TD for each sortable column.
// The innerHTML (entire contents) of the cell will be used for sorting.
// TRICK: You can use the 'abbr' field to subtly alter the sortable contents.
// Otherwise sorts on td contents ($(td).text()). Use the abbr field to make
// case-insensitive sorts or force exceptions to alpha-text order
// (such as ZCTRL vs Control forcing controls to bottom). e.g.:
//
'); // Display the content
w.document.close(); // Close the document to finish rendering
})
.catch(error => console.error('Error fetching BED file:', error));
}
function processFindGenome(result, term) {
// process the hubApi/findGenome?q= result set into somthing
// jquery-ui autocomplete can use
let data = [];
let apiSkipList = new Set(["downloadTime", "downloadTimeStamp", "availableAssemblies", "browser", "elapsedTimeMs", "itemCount", "q", "totalMatchCount", "liftable"]);
Object.keys(result).forEach((key) => {
if (!(apiSkipList.has(key))) {
let val = result[key];
let d = {
"genome": key,
"label": `${val.commonName} (${key})`,
};
Object.keys(val).forEach((vkey) => {
d[vkey] = val[vkey];
});
// Set db to the key (database name or accession) so the autocomplete
// select handler can save it to recent genomes
d.db = key;
if (val.hubUrl !== null) {
d.category = "UCSC GenArk - bulk annotated assemblies from NCBI GenBank / Refseq";
} else {
d.category = "UCSC Genome Browser assemblies - annotation tracks curated by UCSC";
}
data.push(d);
}
});
return data;
}
function initSpeciesAutoCompleteDropdown(inputId, selectFunction, baseUrl = null,
watermark = null, onServerReply = null, onError = null, onFilterDropdown = null) {
/* Generic function for turning an element into a species search bar with an autocomplete
* list separating results by category.
* Required arguments:
* inputId: id of the input element itself, not created here
* selectFunction: the function to call when the user actually clicks on a result
* Optional arguments:
* baseUrl: where we send requests to which will return json we can parse into a list
* of results, defaults to 'hubApi/findGenome?browser=mustExist&q='
* watermark: placeholder text in the input
* onServerReply: function to run after querying baseUrl, by default use processFindGenome()
* to standardize on hubApi, but can be something else
* onError: function to call when the server returns an error (e.g. HTTP 400)
* signature: onError(jqXHR, textStatus, errorThrown, searchTerm) => results array or null
*/
let defaultSearchUrl = "hubApi/findGenome?browser=mustExist&q=";
$.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)");
$(ul).css("z-index", "99999999");
// Check if all items are from recents (have displayCategory === "Recent")
// If so, skip category headers since they're all recent selections
var allRecent = items.length > 0 && items.every(function(item) {
return item.displayCategory === "Recent";
});
$.each(items, function(index, item) {
// Add a heading each time we see a new category (but not for recents)
if (!allRecent && 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
// Hits to assembly hub top level (not individial db names) have no item label,
// so use the value instead
var searchTerm = this.term;
// remove special characters - the \W means remove anything
// that is not: [A-Za-z0-9_] which are 'word' == \w characters
// then eliminate runs of white space characters and trim any
// white space at the beginning or end of the string
var cleanTerm = searchTerm.replace(/\W/g, ' ')
.replace(/\s+/g, ' ')
.trim();
var label = item.label !== null ? item.label : item.value;
// Highlight matching search terms with bold tags
if (cleanTerm && cleanTerm.length > 0) {
// Split search term into individual words (by whitespace)
var words = cleanTerm.split(/\s+/).filter(function(word) {
return word.length > 0; // Filter out empty strings
});
// Apply bolding for each word separately
words.forEach(function(word) {
// Escape special regex characters in each word
var escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Create case-insensitive regex to find all occurrences
var regex = new RegExp('(' + escapedWord + ')', 'gi');
// Replace matches with bolded version (preserves original case)
label = label.replace(regex, '$1');
});
}
let $li = $("")
.data("ui-autocomplete-item", item)
.append($("").html(label))
.appendTo(ul);
if (item.disabled) {
$li.attr('title', item.disabledReason || 'Not available');
$li.css({'opacity': '0.5'});
$li.find('a').css({'pointer-events': 'none'});
}
return $li;
}
}
);
autocompleteCat.init($("[id='"+inputId+"']"), {
baseUrl: baseUrl !== null ? baseUrl : defaultSearchUrl,
watermark: watermark,
onSelect: selectFunction,
onServerReply: onServerReply !== null ? onServerReply : processFindGenome,
onError: onError,
onFilterDropdown: onFilterDropdown,
showRecentGenomes: true,
enterSelectsIdentical: false
});
}
function setupGenomeSearchBar(config) {
/* Higher-level wrapper for setting up a genome search bar with standard boilerplate.
* This handles the common pattern used across CGIs: error handling, DOMContentLoaded,
* element binding, search button handler, item validation, and label update.
*
* config object properties:
* inputId (required): ID of the search input element
* labelElementId: ID of the element to update with selected genome label (default: 'genomeLabel')
* onSelect: Callback function(item, labelElement) when genome is selected.
* Called AFTER standard validation and label update.
* item has: {genome, label, commonName, disabled, ...}
* apiUrl: Custom API URL (default: null uses standard hubApi/findGenome)
* onServerReply: Custom function to process API results (default: null uses processFindGenome)
*/
function onSearchError(jqXHR, textStatus, errorThrown, term) {
return [{label: 'No genomes found', value: '', genome: '', disabled: true}];
}
function wrappedSelect(labelElement, item) {
// Standard validation - all CGIs check this
if (item.disabled || !item.genome) return;
// Standard label update - all CGIs do this
if (labelElement)
labelElement.innerHTML = item.label;
// Call user's custom callback for CGI-specific logic
if (typeof config.onSelect === 'function') {
config.onSelect(item, labelElement);
}
}
document.addEventListener("DOMContentLoaded", () => {
let labelElementId = config.labelElementId || 'genomeLabel';
let labelElement = document.getElementById(labelElementId);
let boundSelect = wrappedSelect.bind(null, labelElement);
initSpeciesAutoCompleteDropdown(config.inputId, boundSelect,
config.apiUrl || null, null, config.onServerReply || null, onSearchError);
// Standard search button handler
let btn = document.getElementById(config.inputId + "Button");
if (btn) {
btn.addEventListener("click", () => {
let val = document.getElementById(config.inputId).value;
$("[id='" + config.inputId + "']").autocompleteCat("search", val);
});
}
// Dropdown toggle button: opens/closes the autocomplete with recent+popular
let toggle = document.getElementById(config.inputId + "Toggle");
if (toggle) {
let wasOpen = false;
toggle.addEventListener("mousedown", () => {
let $input = $("[id='" + config.inputId + "']");
wasOpen = $input.autocompleteCat("widget").is(":visible");
});
toggle.addEventListener("click", () => {
let $input = $("[id='" + config.inputId + "']");
if (wasOpen) {
$input.autocompleteCat("close");
} else {
$input.val("");
$input.autocompleteCat("search", "");
$input.focus();
}
});
}
});
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
// check whether an autocomplete item is for a genark assembly
function isGenarkItem(item) {
return (typeof item.hubUrl !== "undefined" && item.hubUrl) &&
(typeof item.genome !== "undefined" && item.genome) &&
(item.genome.startsWith('GCA_') || item.genome.startsWith('GCF_'));
}