4bb9e8caea515342ba98d3871da76cd4ec69916f
chmalee
Fri May 1 14:10:00 2026 -0700
Initial myVariants implementation: a form on hgTracks where users can enter item details in one of three ways: hgvs/item search, simple bed form, advanced bed form where additional non-bed fields can dynamically created. Allows changing the color of items, writing descriptions, and editing the items after creation. Show overlaps with hardcoded tracks when hgc page is open (not in the hgc dialog). Next commit has implementation of sharing these tracks with other users
diff --git src/hg/js/utils.js src/hg/js/utils.js
index 291449bd6d5..01dd56c17f4 100644
--- src/hg/js/utils.js
+++ src/hg/js/utils.js
@@ -1,5046 +1,5066 @@
// 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_'));
}