83ffc65b0574e8b3bfd7d191548ccde8651670b2 chmalee Tue Apr 2 11:28:22 2024 -0700 Fix tooltip bug, when the user is moving their mouse and triggers a new mouseover event, we need to clear the mousemove event that was checking if they were heading to the tooltip. By clearing this event we prevent the mousemoveTimerHelper function from running, which in this case is mistakenly re-triggering a mouseover event but on the wrong event coordinates, leading to tooltips that are a ways away from where they are supposed to be, refs Max email diff --git src/hg/js/utils.js src/hg/js/utils.js index 20875d2..6fc2364 100644 --- src/hg/js/utils.js +++ src/hg/js/utils.js @@ -1,4334 +1,4331 @@ // 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: */ 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]*$/; 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<list.length;i++) { var ele = list[i]; if (ele.checked !== state) ele.click(); // Forces onclick() javascript to run } } function setCheckBoxesThatContain(nameOrId, state, force, sub1) { // Set all checkboxes which contain 1 or more given substrings in NAME or ID to state boolean // First substring: must begin with it; 2 subs: beg and end; 3: begin, middle and end. // This can force the 'onclick() js of the checkbox, even if it is already in the state if (debug) alert("setCheckBoxesContains is about to set the checkBoxes to "+state); var list; if (arguments.length === 4) list = inputArrayThatMatches("checkbox",nameOrId,sub1,""); else if (arguments.length === 5) list = inputArrayThatMatches("checkbox",nameOrId,sub1,arguments[4]); else if (arguments.length === 6) list = inputArrayThatMatches("checkbox",nameOrId,sub1,arguments[4],arguments[5]); for (var ix=0;ix<list.length;ix++) { clickIt(list[ix],state,force); } return true; } function inputArrayThatMatches(inpType,nameOrId,prefix,suffix) { // returns an array of input controls that match the criteria var found = []; var fIx = 0; if (document.getElementsByTagName) { var list; if (inpType === 'select') list = document.getElementsByTagName('select'); else list = document.getElementsByTagName('input'); for (var ix=0;ix<list.length;ix++) { var ele = list[ix]; if (inpType.length > 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;aIx<arguments.length;aIx++) { if (identifier.indexOf(arguments[aIx]) === -1) { failed = true; break; } } } if (!failed) { found[fIx] = ele; fIx++; } } } else { // NS 4.x - I gave up trying to get this to work. if (debugLevel>2) 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 && jQuery.type(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("<input type=hidden name='"+aName+"' value='"+aValue+"'>"); } 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 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<classes.length;ix++) { if (classes[ix].substring(0,'bgLevel'.length) === 'bgLevel') bgClass = classes[ix]; } if (bgClass) { $(divit).children('table').removeClass('bgLevel1 bgLevel2 bgLevel3 bgLevel4'); $(divit).children('table').addClass(bgClass); } } $(divit).toggle(); // jQuery hide/show return false; } function setTableRowVisibility(button, prefix, hiddenPrefix, titleDesc, doAjax) { // Show or hide one or more table rows whose id's begin with prefix followed by "-". // This code also modifies the corresponding hidden field (cart variable) and the // src of the +/- img button. var retval = true; var hidden = normed($("input[name='"+hiddenPrefix+"_"+prefix+"_close']")); if (button && hidden) { var newVal = -1; if (arguments.length > 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) { /* 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 += "<br>" + 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; } } notifBox.innerHTML += "<div style='text-align:center'>"+ "<button id='" + lsKey + "notifyHide'>Close</button> "+ "<button id='" + lsKey + "notifyHideForever'>Don't show again</button>"+ "</div>"; 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 += "<center>"; html += "<div id='warnBox' style='display:none;'>"; html += "<CENTER><B id='warnHead'></B></CENTER>"; html += "<UL id='warnList'></UL>"; html += "<CENTER><button id='warnOK'></button></CENTER>"; html += "</div></center>"; // 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 += "<script type='text/javascript'>"; html += "function showWarnBox() {"; html += "document.getElementById('warnOK').innerHTML=' OK ';"; html += "var warnBox=document.getElementById('warnBox');"; html += "warnBox.style.display=''; warnBox.style.width='65%%';"; html += "document.getElementById('warnHead').innerHTML='Warning/Error(s):';"; html += "window.scrollTo(0, 0);"; html += "}"; html += "function hideWarnBox() {"; html += "var warnBox=document.getElementById('warnBox');"; html += "warnBox.style.display='none';"; html += "var warnList=document.getElementById('warnList');"; html += "warnList.innerHTML='';"; html += "var endOfPage = document.body.innerHTML.substr(document.body.innerHTML.length-20);"; html += "if(endOfPage.lastIndexOf('-- ERROR --') > 0) { history.back(); }"; html += "}"; html += "</script>"; $('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 <li> 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('[name]:enabled').each(function (i) { var name = $(this).attr('name'); var val = $(this).val(); if ($(this).attr('type') === 'checkbox') { name = cgiBooleanShadowPrefix() + name; val = $(this).attr('checked') ? 1 : 0; } else if ($(this).attr('type') === 'radio') { if (!$(this).attr('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; } }); $(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 ($.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 { 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("<div id='popit' style='display: none'></div>"); popit = $('#popit'); } } // Set up the modal dialog $(popit).html("<div style='font-size:80%'>" + content + "</div>"); $(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("<div id='boxit'></div>"); //$('body').prepend("<div id='boxit' style='display: none'></div>"); boxit = $('div#boxit'); } } if (!reenterable || (content.length > 0)) { // Can reenter without changing content! var buildHtml = "<center>"; if (boxTitle.length > 0) buildHtml += "<div style='background-color:#D9E4F8;'><B>" + boxTitle + "</B></div>"; buildHtml += "<div>" + content + "</div>"; // 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 += "<div>"; if (applyFunc.length > 0) { // "Apply" button and "Cancel" button. Apply also closes! buildHtml += " <INPUT TYPE='button' value='" + applyName + "' onClick='"+ applyFunc + "(" + $(boxit).attr('id') + "); " + closeHtml + "'> "; closeButton = "Cancel"; // If apply button then close is cancel } buildHtml += " <INPUT TYPE='button' value='" + closeButton + "' onClick='" + closeHtml; buildHtml += "'> </div>"; $(boxit).html("<div class='blueBox' style='width:" + boxWidth + "; background-color:#FFF9D2;'>" + buildHtml + "</div>"); // 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('_', 3)[2]; } 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('_', 3)[2]; } 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("<div id='waitMask' class='waitMask'></div>"); 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 = $("<div id='"+loadingId+"' class='loading'></div>"); 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 (jQuery.type(begToken) === "regexp") insideBeg = someString.search(begToken); else if (begToken.length > 0) insideBeg = someString.indexOf(begToken,ixBeg); if (jQuery.type(endToken) === "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 (jQuery.type(begToken) === "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 (jQuery.type(endToken) === "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 = '<!-- HGERROR-START -->'; var endToken = '<!-- HGERROR-END -->'; while (cleanHtml.length > 0) { var bounds = bindings.outside(begToken,endToken,cleanHtml); if (bounds.start === -1) break; // OLD WAY var warnMsg = bindings.insideOut('<P>','</P>',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; var shlurpPattern=/<script type=\'text\/javascript\' SRC\=\'.*\'\><\/script\>/gi; if (debug || whatWeDid) { var jsFiles = cleanHtml.match(shlurpPattern); if (jsFiles && jsFiles.length > 0) { if (debug) alert("jsFiles:'"+jsFiles+"'\n---------------\n"+cleanHtml); // warn() interprets html if (whatWeDid) whatWeDid.jsFiles = jsFiles; } } cleanHtml = cleanHtml.replace(shlurpPattern,""); return cleanHtml; } function stripCspHeader(html, debug, whatWeDid) { // strips CSP Header from html returned by ajax var shlurpPattern=/<meta http-equiv=\'Content-Security-Policy\' content=".*"\>/i; if (debug || whatWeDid) { var csp = html.match(shlurpPattern); if (csp && csp.length > 0) { if (debug) alert("csp:'"+csp+"'\n---------------\n"+html); // warn() interprets html if (whatWeDid) whatWeDid.csp = csp[0]; } } return html.replace(shlurpPattern,""); // Clean CSP meta tag. } function parseNonce(content, debug) { // parse nonce from returned ajax page csp header 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('ajax nonce='+content); return content; } function stripCssFiles(returnedHtml,debug) { // strips csst files from html returned by ajax var cleanHtml = returnedHtml; var shlurpPattern=/<LINK rel=\'STYLESHEET\' href\=\'.*\' TYPE=\'text\/css\' \/\>/gi; if (debug) { var cssFiles = cleanHtml.match(shlurpPattern); if (cssFiles && cssFiles.length > 0) alert("cssFiles:'"+cssFiles+"'\n---------------\n"+cleanHtml); } cleanHtml = cleanHtml.replace(shlurpPattern,""); return cleanHtml; } function stripJsNonce(html, nonce, debug, whatWeDid) { // Strips and returns embedded javascript from html returned by ajax with nonce var results=[]; var content = ""; var sectionBegin = "<script type='text/javascript' nonce='"+nonce+"'>"; var sectionEnd = "</script>"; 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 <prefix>HH<postfix> 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<jsNonce.js.length; ++i) { var sTag = document.createElement("script"); sTag.type = "text/javascript"; sTag.text = jsNonce.js[i]; sTag.setAttribute('nonce', jsNonce.pageNonce); // CSP2 Requires document.head.appendChild(sTag); } } function stripJsEmbedded(returnedHtml, debug, whatWeDid) { // GALT NOTE: this may have been mostly obsoleted by CSP2 changes. // There were 3 or 4 places in the code that even in production // had called this function stripJsEmbedded with debug=true, which means that // if any script tag blocks are present, they would be seen and shown // to the user. This probably was because if these blocks were found // simply adding them to the div html from the ajax callback would result in // their being ignored by the browser. It seems to be a security feature of browsers. // Meanwhile however inline event handlers in the html worked and were allowed. // So this was just a way to warn developers that their script blocks would have been ignored // and have no effect. I think this concern no longer applies after my CSP2 changes // because it is able to pull in all the js, whether from event handlers or what would // have been individual script blocks in the old days, and adds it to // the page with a nonce and appendChild. // // strips embedded javascript 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 = /<script.*\>/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('<li>','</li>',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 = '<div id="main-menu-whole">'; var endPattern = '</div><!-- end main-menu-whole -->'; 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('<li>','</li>',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('<input type="hidden" name="pix" value="' + winWidth + '"/>'); //$("#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.: // <TH id='factor' class='sortable sortRev sort3' nowrap>...</TH> // (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.: // <TH id='factor' class='sortable sortRev sort3' nowrap title='Sort on this column' // onclick="return sortTable.sortOnButtonPress(this);"> // NOTE: onclick function will automatically be added if not found. // SUP: Each TH.sortable *may* contain a <sup> which will be filled with an // up or down arrow and the column's sort order: e.g. <sup>↓2</sup> // 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.: // <TD id='wgEncodeBroadHist...' nowrap abbr='ZCTRL' align='left'>Control</TD> // This is also the method to ensure a numeric sort e.g.: // <td align="right" abbr="000003416800354">3.2 GB</td> // 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: // <INPUT TYPE=HIDDEN NAME='wgEncodeBroadHistone.sortOrder' // class='sortOrder' VALUE="factor=- cell=+ view=+"> // 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.: // <INPUT TYPE=HIDDEN NAME='wgEncodeHaibTfbsA549ControlPcr2xDexaRawRep1.priority' // class='trPos' VALUE="2"> // 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 <TH> colIds // Expects tbody to not sort thead, but could take table // Used to use 'sorting' class, but showLoadingImage results in much less screen redrawing // For IE especially this was difference between dead/timedout scripts and working sorts! var id = $(tbody).attr('id'); if (!id || id.length === 0) { $(tbody).attr('id',"tbodySort"); // Must have some id! id = $(tbody).attr('id'); } if ($(tbody).css('display') === 'none') { // suppress loading image if element is hidden (and consequently has no position) sortTable.loadingId = null; } else { sortTable.loadingId = showLoadingImage(id); } sortTable.tbody=tbody; sortTable.columns=sortColumns; // This allows hiding the rows while sorting! setTimeout(function() { sortTable.sort(sortTable.tbody,sortTable.columns); },50); }, trAlternateColors: function (tbody,rowGroup,cellIx) { // Will alternate colors for visible table rows. // If cellIx(s) provided then color changes when the column(s) abbr or els innerHtml changes // If no cellIx is provided then alternates on rowGroup (5= change color 5,10,15,...) // Expects tbody to not color thead, but could take table var darker = false; // === false will trigger first row to be change color = darker if (arguments.length<3) { // No columns to check so alternate on rowGroup if (!rowGroup || rowGroup === 0) rowGroup = 1; var curCount = 0; // Always start with a change $(tbody).children('tr:visible').each( function(i) { if (curCount === 0) { curCount = rowGroup; darker = (!darker); } if (darker) { $(this).removeClass("bgLevel1"); $(this).addClass( "bgLevel2"); } else { $(this).removeClass("bgLevel2"); $(this).addClass( "bgLevel1"); } curCount--; }); } else { var lastContent = "startWithChange"; var cIxs = []; for (var aIx=2; aIx < arguments.length; aIx++) { // multiple columns cIxs[aIx-2] = arguments[aIx]; } $(tbody).children('tr:visible').each( function(i) { curContent = ""; for (var ix=0; ix < cIxs.length; ix++) { if (this.cells[cIxs[ix]]) { curContent += (this.cells[cIxs[ix]].abbr !== "" ? this.cells[cIxs[ix]].abbr : this.cells[cIxs[ix]].innerHTML ); } } if (lastContent !== curContent) { lastContent = curContent; darker = (!darker); } if (darker) { $(this).removeClass("bgLevel1"); $(this).addClass( "bgLevel2"); } else { $(this).removeClass("bgLevel2"); $(this).addClass( "bgLevel1"); } }); } }, alternateColors: function (tbody) { // Will alternate colors based upon sort columns (which may be passed in as second arg, // or discovered). Expects tbody to not color thead, but could take table var sortColumns; if (arguments.length > 1) sortColumns = arguments[1]; else { var table = tbody; if ($(table).is('tbody')) table = $(tbody).parent(); sortColumns = new sortTable.columnsFromTable(table); } if (sortColumns) { if (sortColumns.cellIxs.length === 1) sortTable.trAlternateColors(tbody,0,sortColumns.cellIxs[0]); else if (sortColumns.cellIxs.length === 2) sortTable.trAlternateColors(tbody,0,sortColumns.cellIxs[0],sortColumns.cellIxs[1]); else // Three columns is plenty sortTable.trAlternateColors(tbody,0,sortColumns.cellIxs[0],sortColumns.cellIxs[1], sortColumns.cellIxs[2]); } else { sortTable.trAlternateColors(tbody,5); // alternates every 5th row } }, orderFromColumns: function (sortColumns) {// Creates the trackDB setting entry sortOrder subGroup1=+ ... from a sortColumns structure fields = []; for (var ix=0; ix < sortColumns.cellIxs.length; ix++) { if (sortColumns.tags[ix] && sortColumns.tags[ix].length > 0) fields[ix] = sortColumns.tags[ix] + "=" + (sortColumns.reverse[ix] ? "-":"+"); else fields[ix] = sortColumns.cellIxs[ix] + "=" + (sortColumns.reverse[ix] ? "-":"+"); } var sortOrder = fields.join(' '); return sortOrder; }, orderUpdate: function (table,sortColumns,addSuperscript) {// Updates the sortOrder in a sortable table if (addSuperscript === undefined || addSuperscript === null) addSuperscript = false; if ($(table).is('tbody')) table = $(table).parent(); var tr = $(table).find('tr.sortable')[0]; if (tr) { for (cIx=0; cIx < sortColumns.cellIxs.length; cIx++) { var th = tr.cells[sortColumns.cellIxs[cIx]]; /* jshint loopfunc: true */// function inside loop works and replacement is awkward. $(th).each(function(i) { // First remove old sort classes var classList = $( this ).attr("class").split(" "); if (classList.length < 2) // assertable return; classList = aryRemove(classList,["sortable"]); while (classList.length > 0) { var aClass = classList.pop(); if (aClass.indexOf("sort") === 0) $(this).removeClass(aClass); } // Now add current sort classes $(this).addClass("sort"+(cIx+1)); if (sortColumns.reverse[cIx]) $(this).addClass("sortRev"); // update any superscript sup = $(this).find('sup')[0]; if (sup || addSuperscript) { var content = (sortColumns.reverse[cIx] === false ? "↓":"↑"); if (sortColumns.cellIxs.length>1) { // Number only if more than one if (cIx < 5) // Show numbering and direction only for the first 5 content += (cIx+1); else content = ""; } if (sup) sup.innerHTML = content; else $(th).append("<sup>"+content+"</sup>"); } }); } // There may be a hidden input that gets updated to the cart var inp = $(tr).find('input.sortOrder')[0]; if (inp) { $(inp).val(sortTable.orderFromColumns(sortColumns)); if (!addSuperscript && typeof(subCfg) === "object") // subCfg.js file included? subCfg.markChange(null,inp); // use instead of change() because type=hidden! } } }, orderFromTr: function (tr) { // Looks up the sortOrder input value from a *.sortable header row of a sortable table var inp = $(tr).find('input.sortOrder')[0]; if (inp) return $(inp).val(); else { // create something like "cellType=+ rep=+ protocol=+ treatment=+ factor=+ view=+" var fields = []; var cells = $(tr).find('th.sortable'); $(cells).each(function (i) { var classList = $( this ).attr("class").split(" "); if (classList.length < 2) // assertable return; classList = aryRemove(classList,["sortable"]); var reverse = false; var sortIx = -1; while (classList.length > 0) { var aClass = classList.pop(); if (aClass.indexOf("sort") === 0) { if (aClass === "sortRev") reverse = true; else { aClass = aClass.substring(4); // clip off the "sort" portion var ix = parseInt(aClass); if (!isNaN(ix)) { sortIx = ix; } } } } if (sortIx >= 0) { if (this.id && this.id.length > 0) fields[sortIx] = this.id + "=" + (reverse ? "-":"+"); else fields[sortIx] = this.cellIndex + "=" + (reverse ? "-":"+"); } }); if (fields.length > 0) { if (!fields[0]) fields.shift(); // 1 based sort ix and 0 based fields ix return fields.join(' '); } } return ""; }, columnsFromSortOrder: function (sortOrder) { // Creates sortColumns struct (without cellIxs[]) from a trackDB.sortOrder setting string this.tags = []; this.reverse = []; var fields = sortOrder.split(" "); // sortOrder looks like: "cell=+ factor=+ view=+" while (fields.length > 0) { var pair = fields.shift().split("="); // Take first and split into if (pair.length === 2) { this.tags.push(pair[0]); this.reverse.push(pair[1] !== '+'); } } }, columnsFromTr: function (tr,silent) { // Creates a sortColumns struct from entries in the 'tr.sortable' heading row of the table this.inheritFrom = sortTable.columnsFromSortOrder; var sortOrder = sortTable.orderFromTr(tr); if (sortOrder.length === 0 && !silent) { // developer needs to know something is wrong warn("Unable to obtain sortOrder from sortable table."); return; } this.inheritFrom(sortOrder); // Add two additional arrays this.cellIxs = []; this.useAbbr = []; var ths = $(tr).find('th.sortable'); for (var tIx=0; tIx < this.tags.length; tIx++) { for (ix=0; ix < ths.length; ix++) { if ((ths[ix].id && ths[ix].id === this.tags[tIx]) || (ths[ix].cellIndex === parseInt(this.tags[tIx]))) { this.cellIxs[tIx] = ths[ix].cellIndex; this.useAbbr[tIx] = (ths[ix].abbr.length > 0); } } } if (this.cellIxs.length === 0 && !silent) { warn("Unable to find any sortOrder.cells for sortable table. ths.length:"+ths.length + " tags.length:"+this.tags.length + " sortOrder:["+sortOrder+"]"); return; } }, columnsFromTable: function (table) {// Creates a sortColumns struct from the contents of a 'table.sortable' this.inheritNow = sortTable.columnsFromTr; var tr = $(table).find('tr.sortable')[0]; this.inheritNow(tr); }, _sortOnButtonPress: function (anchor) { // Updates the sortColumns struct and sorts the table when a column header has been pressed // If the current primary sort column is pressed, its direction is toggled then the table // is sorted. If a secondary sort column is pressed, it is moved to the primary spot and // sorted in fwd direction var th=$(anchor).closest('th')[0]; // Note that anchor is <a href> within th, not th var tr=$(th).parent(); var theOrder = new sortTable.columnsFromTr(tr); var oIx = th.cellIndex; for (oIx=0; oIx < theOrder.cellIxs.length; oIx++) { if (theOrder.cellIxs[oIx] === th.cellIndex) break; } if (oIx === theOrder.cellIxs.length) { // Developer must be warned that something is wrong with sortable table setup warn("Failure to find '"+th.id+"' in sort columns."); return; } if (oIx > 0) { // Need to reorder var newOrder = new sortTable.columnsFromTr(tr); var nIx=0; // button pushed puts this 'tagId' column first in new order newOrder.tags[nIx] = theOrder.tags[oIx]; newOrder.reverse[nIx] = false; // When moving to the first position sort forward newOrder.cellIxs[nIx] = theOrder.cellIxs[oIx]; newOrder.useAbbr[nIx] = theOrder.useAbbr[oIx]; for (var ix=0; ix < theOrder.cellIxs.length; ix++) { if (ix !== oIx) { nIx++; newOrder.tags[nIx] = theOrder.tags[ix]; newOrder.reverse[nIx] = theOrder.reverse[ix]; newOrder.cellIxs[nIx] = theOrder.cellIxs[ix]; newOrder.useAbbr[nIx] = theOrder.useAbbr[ix]; } } theOrder = newOrder; } else { // if (oIx === 0) { // need to reverse directions theOrder.reverse[oIx] = (theOrder.reverse[oIx] === false); } var table=$(tr).closest("table.sortable")[0]; if (table) { // assertable sortTable.orderUpdate(table,theOrder); // Must update sortOrder first! var tbody = $(table).find("tbody.sortable")[0]; sortTable.sortByColumns(tbody,theOrder); } return; }, sortOnButtonPress: function (anchor,tagId) { // wrapper for the real worker: _sortOnButtonPress() var table = $( anchor ).closest("table.sortable")[0]; if (table) { waitOnFunction( sortTable._sortOnButtonPress, anchor, tagId); } return false; // called by link so return false means don't try to go anywhere }, sortUsingColumns: function (table) // NOT USED { // Sorts a table body based upon the marked columns var columns = new sortTable.columnsFromTable(table); tbody = $(table).find("tbody.sortable")[0]; if (tbody) sortTable.sortByColumns(tbody,columns); }, savePositions: function (table) { // Sets the value for the input.trPos of a table row. Typically this is a "priority" for // a track. This gets called by sort or dragAndDrop in order to allow the new order to // affect hgTracks display var inputs = $(table).find("input.trPos"); $( inputs ).each( function(i) { var tr = $( this ).closest('tr')[0]; var trIx = $( tr ).attr('rowIndex').toString(); if ($( this ).val() != trIx) { $( this ).val( trIx ); if (typeof(subCfg) === "object") // NOTE: couldn't get $(this).change() to work. subCfg.markChange(null,this); // probably because this is input type=hidden! } }); }, ///// Following functions are for Sorting by priority trPriorityFind: function (tr) { // returns the position (*.priority) of a sortable table row var inp = $(tr).find('input.trPos')[0]; if (inp) return $(inp).val(); return 999999; }, trPriorityCmp: function (tr1,tr2) // UNUSED FUNCTION { // Compare routine for sorting by *.priority var priority1 = sortTable.trPriorityFind(tr1); var priority2 = sortTable.trPriorityFind(tr2); return priority2 - priority1; }, tablesSortAtStartup: function () // DEAD CODE? { // Called at startup if you want javascript to initialize and sort all your // class='sortable' tables // IMPORTANT: This function WILL ONLY sort by first column. // If there are multiple sort columns, please presort the list for accurtacy!!! var tables = $("table.sortable"); $(tables).each(function(i) { sortTable.initialize(this,true); // Will initialize superscripts sortTable.sortUsingColumns(this); }); }, initialize: function (table,addSuperscript,altColors) {// Called if you want javascript to initialize your class='sortable' table. // 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 (table row) class='sortable' which will declare // the sort columnns: // TH.sortable: 1 or more TH (table column headers) with class='sortable sort1 [sortRev]' // (or sort2, sort3) declaring sort order and whether reversed. e.g.: // <TH id='factor' class='sortable sortRev sort3' nowrap>...</TH> // (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.: // <TH id='factor' class='sortable sortRev sort3' nowrap title='Sort on column' // onclick="return sortTable.sortOnButtonPress(this);"> // NOTE: onclick function will automatically be added if not found. // SUP: Each TH.sortable *may* contain a <sup> which will be filled with an up or down // arrow and the column's sort order: e.g. <sup>↓2</sup> // 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.: // <TD id='wgEncodeBroadHist...' nowrap abbr='ZCTRL' align='left'>Control</TD> // 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 this function // To override, specify <TBODY class='sortable noAltColors'> // NOTE: Add class 'initBySortOrder' to have an sort by column performed at document initialization time, using // the saved sortOrder cart variable if (altColors === undefined || altColors === null) // explicitly default this boolean altColors = false; if ($(table).hasClass('sortable') === false) { warn('Table is not sortable'); return; } var tr = $(table).find('tr.sortable')[0]; if (!tr) { tr = $(table).find('tr')[0]; if (!tr) { warn('Sortable table has no rows'); return; } $(tr).addClass('sortable'); //warn('Made first row tr.sortable'); } if ($(table).find('tr.sortable').length !== 1) { warn('sortable table contains more than 1 header row declaring sort columns.'); return; } // If not TBODY is found, then create, wrapping all but those already in a thead tbody = $(table).find('tbody')[0]; if (!tbody) { trs = $(table).find('tr').not('thead tr'); $(trs).wrapAll("<TBODY class='sortable' />"); tbody = $(table).find('tbody')[0]; //warn('Wrapped all trs not in thead.sortable in tbody.sortable'); } if ($(tbody).hasClass('sortable') === false) { $(tbody).addClass('sortable'); //warn('Added sortable class to tbody'); } if (altColors && $(tbody).hasClass('altColors') === false && $(tbody).hasClass('noAltColors') === false) { $(tbody).addClass('altColors'); //warn('Added altColors class to tbody.sortable'); } $(tbody).hide(); // If not THEAD is found, then create, wrapping first row. thead = $(table).find('thead')[0]; if (!thead) { $(tr).wrapAll("<THEAD class='sortable' />"); thead = $(table).find('thead')[0]; $(thead).insertBefore(tbody); //warn('Wrapped tr.sortable with thead.sortable'); } if ($(thead).hasClass('sortable') === false) { $(thead).addClass('sortable'); //warn('Added sortable class to thead'); } var sortColumns = new sortTable.columnsFromTr(tr,"silent"); if (!sortColumns || sortColumns.cellIxs.length === 0) { // could mark all columns as sortable! $(tr).find('th').each(function (ix) { $(this).addClass('sortable'); $(this).addClass('sort'+(ix+1)); //warn("Added class='sortable sort"+(ix+1)+"' to th:"+this.innerHTML); }); sortColumns = new sortTable.columnsFromTr(tr,"silent"); if (!sortColumns || sortColumns.cellIxs.length === 0) { warn("sortable table's header row contains no sort columns."); return; } } // Can wrap all columnn headers with link $(tr).find("th.sortable").each(function (ix) { if ( ! $(this).attr('onclick') ) { $(this).click( function () { sortTable.sortOnButtonPress(this);} ); } if (theClient.isIePre11()) { // Special case for IE since CSS :hover doesn't work $(this).hover( function () { $(this).css( { backgroundColor: '#CCFFCC', cursor: 'hand' } ); }, function () { $(this).css( { backgroundColor: '#FCECC0', cursor: '' } ); } ); } if ( $(this).attr('title').length === 0) { var title = $(this).text().replace(/[^a-z0-9 ]/ig,''); if (title.length > 0 && $(this).find('sup')) title = title.replace(/[0-9]$/g,''); if (title.length > 0) $(this).attr('title',"Sort list on '" + title + "'." ); else $(this).attr('title',"Sort list on column." ); } }); // Now update all of those cells sortTable.orderUpdate(table,sortColumns,addSuperscript); if ($(tbody).hasClass('initBySortOrder')) { sortTable.sortByColumns(tbody,sortColumns); } // Alternate colors if requested if (altColors) { sortTable.alternateColors(tbody); } // Highlight rows? But on subtrack list, this will mess up "metadata dropdown" coloring. // So just exclude tables with drag and drop if ($(table).hasClass('tableWithDragAndDrop') === false) { $('tbody.sortable').find('tr').hover( function(){ $(this).addClass('bgLevel3'); $(this).find('table').addClass('bgLevel3'); }, function(){ $(this).removeClass('bgLevel3'); $(this).find('table').removeClass('bgLevel3'); } ); } // Finally, make visible $(tbody).removeClass('sorting'); $(tbody).show(); } }; function sortTableInitialize(table,addSuperscript,altColors) { // legacy in case some static pages still initialize the table the old way sortTable.initialize(table,addSuperscript,altColors); } ////////////////////////////// //// findTracks functions //// ///////////////////////////// var findTracks = { updateMdbHelp: function (index) { // update the metadata help links based on currently selected values. // If index === 0 we update all help items, otherwise we only update the one === index. var db = getDb(); var disabled = { // blackList 'accession': 1, 'dataVersion': 1, 'dccAccession': 1, 'expId': 1, 'geoSampleAccession': 1, 'grant': 1, 'lab': 1, 'labExpId': 1, 'labVersion': 1, 'origAssembly': 1, 'obtainedBy': 1, 'region': 1, 'replicate': 1, 'setType': 1, 'softwareVersion': 1, 'subId': 1, 'tableName': 1, 'tissueSourceType': 1, 'view': 1 }; var expected = $('tr.mdbSelect').length; var ix=1; if (index !== 0) { ix=index; expected=index; } for (; ix <= expected; ix++) { var helpLink = $("span#helpLink" + ix); if (helpLink.length > 0) { // NOTE must match METADATA_NAME_PREFIX in hg/hgTracks/searchTracks.c var val = $("select[name='hgt_mdbVar" + ix + "']").val(); var text = $("select[name='hgt_mdbVar" + ix + "'] option:selected").text(); helpLink.html(" "); // Do not want this with length === 0 later! if ( ! disabled[val] ) { var str; if (val === 'cell') { if (db.substr(0, 2) === "mm") { str = "../ENCODE/cellTypesMouse.html"; } else { str = "../ENCODE/cellTypes.html"; } } else if (val.toLowerCase() === 'antibody') { str = "../ENCODE/antibodies.html"; } else { str = "../ENCODE/otherTerms.html#" + val; } helpLink.html(" <a target='_blank' " + "title='detailed descriptions of terms'" + " href='" + str + "'>" + text + "</a>"); } } } }, mdbVarChanged: function (obj) { // Ajax call to repopulate a metadata vals select when mdb var changes // This handles the currnet case when 2 vars have the same name (e.g. advanced, files tabs) findTracks.clearFound(); // Changing values so abandon what has been found var newVar = $(obj).val(); // NOTE must match METADATA_NAME_PREFIX in hg/hgTracks/searchTracks.c var a = /hgt_mdbVar(\d+)/.exec(obj.name); if (newVar !== undefined && newVar !== null && a && a[1]) { var num = a[1]; if ($('#advancedTab').length === 1 && $('#filesTab').length === 1) { $("select.mdbVar[name='hgt_mdbVar"+num+"'][value!='"+newVar+"']").val(newVar); } var cgiVars = "db=" + getDb() + "&cmd=hgt_mdbVal" + num + "&var=" + newVar; if (document.URL.search('hgFileSearch') !== -1) cgiVars += "&fileSearch=1"; else cgiVars += "&fileSearch=0"; $.ajax({ type: "GET", url: "../cgi-bin/hgApi", data: cgiVars, dataType: 'html', trueSuccess: findTracks.handleNewMdbVals, success: catchErrorOrDispatch, error: errorHandler, cache: true, cmd: "hgt_mdbVal" + num, // NOTE must match METADATA_VALUE_PREFIX num: num // in hg/hgTracks/searchTracks.c }); } // NOTE: with newJquery, the response is getting a new error (missing ; before statement) // There were also several XML parsing errors. // This error is fixed with the addition of "dataType: 'html'," above. }, handleNewMdbVals: function (response, status) { // Handle ajax response (repopulate a metadata val select) // This handles the currnet case when 2 vars have the same name (e.g. advanced, files tabs) var td = normed($('td#' + this.cmd)); if (td) { $(td).empty(); $(td).append(response); var inp = normed($(td).find('.mdbVal')); var tdIsLike = normed($('td#isLike'+this.num)); if (inp && tdIsLike) { if ($(inp).hasClass('freeText')) { $(tdIsLike).text('contains'); } else if ($(inp).hasClass('wildList') || $(inp).hasClass('filterBy')) { $(tdIsLike).text('is among'); } else { $(tdIsLike).text('is'); } } // Do this by 'each' to set noneIsAll individually $(td).find('.filterBy').each( function(i) { ddcl.setup(this,'noneIsAll'); }); } findTracks.updateMdbHelp(this.num); }, mdbValChanged: function (obj) { // Keep all tabs with same selects in sync // TODO: Change from name to id based identification and only have one set of inputs in form // This handles the currnet case when 2 vars have the same name (e.g. advanced, files tabs) findTracks.clearFound(); // Changing values so abandon what has been found if ($('#advancedTab').length === 1 && $('#filesTab').length === 1) { var newVal = $(obj).val(); // NOTE must match METADATA_NAME_PREFIX in hg/hgTracks/searchTracks.c var a = /hgt_mdbVal(\d+)/.exec(obj.name); if (newVal !== undefined && newVar !== null && a && a[1]) { var num = a[1]; $("input.mdbVal[name='hgt_mdbVal"+num+"'][value!='"+newVal+"']").val(newVal); $("select.mdbVal[name='hgt_mdbVal"+num+"'][value!='"+newVal+"']").each( function (i) { $(this).val(newVal); if ($(this).hasClass('filterBy')) { $(this).dropdownchecklist("destroy"); ddcl.setup(this,'noneIsAll'); } }); } } //findTracks.searchButtonsEnable(true); }, changeVis: function (seenVis) { // called by onchange of vis var visName = $(seenVis).attr('id'); var trackName = visName.substring(0,visName.length - "_id".length); var hiddenVis = $("input[name='"+trackName+"']"); var tdb = tdbGetJsonRecord(trackName); if ($(seenVis).val() !== "hide") $(hiddenVis).val($(seenVis).val()); else { var selCb = $("input#"+trackName+"_sel_id"); $(selCb).attr('checked',false); // Can't set these to [] because that means default $(seenVis).attr('disabled',true); // setting is used. However, we're explicitly hiding! var needSel = (tdb.parentTrack !== undefined && tdb.parentTrack !== null); if (needSel) { var hiddenSel = $("input[name='"+trackName+"_sel']"); $(hiddenSel).val('0'); // Can't set it to [] because it means default setting used. $(hiddenSel).attr('disabled',false); } if (tdbIsSubtrack(tdb)) $(hiddenVis).val("[]"); else $(hiddenVis).val("hide"); } $(hiddenVis).attr('disabled',false); $('input.viewBtn').val('View in Browser'); }, clickedOne: function (selCb,justClicked) { // called by on click of CB and findTracks.checkAll() var selName = $(selCb).attr('id'); var trackName = selName.substring(0,selName.length - "_sel_id".length); var hiddenSel = $("input[name='"+trackName+"_sel']"); var seenVis = $('select#' + trackName + "_id"); var hiddenVis = $("input[name='"+trackName+"']"); var tr = $(selCb).parents('tr.found'); var tdb = tdbGetJsonRecord(trackName); var isHub = trackName.slice(0,4) === "hub_"; var hubUrl = isHub ? tdb.hubUrl : ""; var needSel = (typeof(tdb.parentTrack) === 'string' && tdb.parentTrack !== ''); var shouldPack = tdb.canPack && tdb.kindOfParent === 0; // If parent then not pack but full if (shouldPack && tdb.shouldPack !== undefined && tdb.shouldPack !== null && !tdb.shouldPack) shouldPack = false; var checked = $(selCb).attr('checked'); // First deal with seenVis control if (checked) { $(seenVis).attr('disabled', false); if ($(seenVis).attr('selectedIndex') === 0) { if (shouldPack) $(seenVis).attr('selectedIndex',3); // packed else $(seenVis).attr('selectedIndex',$(seenVis).attr('length') - 1); } } else { $(seenVis).attr('selectedIndex',0); // hide $(seenVis).attr('disabled', true ); } // Deal with hiddenSel and hiddenVis so that submit does the right thing // Setting these requires justClicked OR seen vs. hidden to be different var setHiddenInputs = justClicked; if (!justClicked) { if (needSel) setHiddenInputs = (checked !== (parseInt($(hiddenSel).val()) === 1)); else if (checked) setHiddenInputs = ($(seenVis).val() !== $(hiddenVis).val()); else setHiddenInputs = ($(hiddenVis).val() !== "hide" && $(hiddenVis).val() !== "[]"); } if (setHiddenInputs) { if (checked) $(hiddenVis).val($(seenVis).val()); else if (tdbIsSubtrack(tdb)) $(hiddenVis).val("[]"); else $(hiddenVis).val("hide"); $(hiddenVis).attr('disabled',false); if (needSel) { if (checked) $(hiddenSel).val('1'); else $(hiddenSel).val('0'); // Can't set it to [] because it means default is used. $(hiddenSel).attr('disabled',false); } } // if we selected a track in a public hub that is unconnected, we need to get the // hubUrl into the form so the genome browser knows to load that hub. If the hub // was already a connected hub, then we don't need to specify anything because it // will already be in the cart and we handle the visibility settings like normal. // The hubUrl field present in the json indicates this is an unconnected hub if (justClicked && hubUrl !== undefined && hubUrl.length > 0) { var form = $("form[id='searchResults'"); var newHubInput = document.createElement("input"); // if we are a subtrack we need to explicitly hide the parent // track so ALL subtracks of the parent don't show up unexpectedly if (needSel) { var parentTrack = tdb.parentTrack; var parentTrackInput = document.createElement("input"); parentTrackInput.setAttribute("type", "hidden"); parentTrackInput.setAttribute("name", parentTrack); parentTrackInput.setAttribute("value", "hide"); form.append(parentTrackInput); } newHubInput.setAttribute("type", "hidden"); newHubInput.setAttribute("name", "hubUrl"); newHubInput.setAttribute("value", hubUrl); form.append(newHubInput); } // The "view in browser" button should be enabled/disabled if (justClicked) { $('input.viewBtn').val('View in Browser'); findTracks.counts(); } }, normalize: function () { // Normalize the page based upon current state of all found tracks $('div#found').show(); var selCbs = $('input.selCb'); // All should have their vis enabled/disabled appropriately (false means don't update cart) $(selCbs).each( function(i) { findTracks.clickedOne(this,false); }); findTracks.counts(); }, normalizeWaitOn: function () // UNUSED ? { // Put up wait mask then Normalize the page based upon current state of all found tracks waitOnFunction( findTracks.normalize ); }, _checkAll: function (check) { // Checks/unchecks all found tracks. var selCbs = $('input.selCb'); $(selCbs).attr('checked',check); // All should have their vis enabled/disabled appropriately (false means don't update cart) $(selCbs).each( function(i) { findTracks.clickedOne(this,false); }); $('input.viewBtn').val('View in Browser'); findTracks.counts(); return false; // Pressing button does nothing more }, checkAllWithWait: function (check) { waitOnFunction( findTracks._checkAll, check); }, searchButtonsEnable: function (enable) { // Displays visible and checked track count // NOTE: must match TRACK_SEARCH in hg/inc/searchTracks.h var searchButton = $('input[name="hgt_tSearch"]'); var clearButton = $('input.clear'); if (enable) { $(searchButton).attr('disabled',false); $(clearButton).attr('disabled',false); } else { $(searchButton).attr('disabled',true); $(clearButton).attr('disabled',true); } }, counts: function () { // Displays visible and checked track count var counter = normed($('.selCbCount')); if (counter) { var selCbs = $("input.selCb"); if (selCbs && selCbs.length > 0) $(counter).text("("+$(selCbs).filter(":enabled:checked").length + " of " + $(selCbs).length+ " selected)"); } }, clearFound: function () { // Clear found tracks and all input controls var found = normed($('div#found')); if (found) $(found).remove(); found = normed($('div#filesFound')); if (found) $(found).remove(); return false; }, clear: function () { // Clear found tracks and all input controls findTracks.clearFound(); $('input[type="text"]').val(''); // This will always be found $('select.mdbVal').attr('selectedIndex',0); // Should be 'Any' $('select.filterBy').each( function(i) { // Do this by 'each' to set noneIsAll individually //$(this).dropdownchecklist("refresh"); // requires v1.1 $(this).dropdownchecklist("destroy"); $(this).show(); ddcl.setup(this,'noneIsAll'); }); $('select.groupSearch').attr('selectedIndex',0); $('select.typeSearch').attr('selectedIndex',0); //findTracks.searchButtonsEnable(false); return false; }, sortNow: function (obj) { // Called by radio button to sort tracks if ($('#sortIt').length === 0) $('form#trackSearch').append("<input TYPE=HIDDEN id='sortIt' name='"+ $(obj).attr('name')+"' value='"+$(obj).val()+"'>"); else $('#sortIt').val($(obj).val()); // How to hold onto selected tracks? There are 2 separate forms. // Scrape named inputs from searchResults form and dup them on trackSearch? var inp = $('form#searchResults').find('input:hidden').not(':disabled').not( "[name='hgsid']"); if ($(inp).length > 0) { $(inp).appendTo('form#trackSearch'); // Must be post to avoid url too long NOTE: probably needs to be post anyway $('form#trackSearch').attr('method','POST'); } $('#searchSubmit').click(); return true; }, page: function (pageVar,startAt) { // Called by radio button to sort tracks var pager = $("input[name='"+pageVar+"']"); if ($(pager).length === 1) $(pager).val(startAt); // How to hold onto selected tracks? There are 2 separate forms. // Scrape named inputs from searchResults form and dup them on trackSearch? var inp = $('form#searchResults').find('input:hidden').not(':disabled').not( "[name='hgsid']"); if ($(inp).length > 0) { $(inp).appendTo('form#trackSearch'); // Must be post to avoid url too long NOTE: probably needs to be post anyway $('form#trackSearch').attr('method','POST'); } $('#searchSubmit').click(); return false; }, configSet: function (name) { // Called when configuring a composite or superTrack var thisForm = $('form#searchResults'); $(thisForm).attr('action',"../cgi-bin/hgTrackUi?hgt_tSearch=Search&g="+name); $(thisForm).find('input.viewBtn').click(); }, mdbSelectPlusMinus: function (obj) { // Now [+][-] mdb var rows with javascript rather than cgi roundtrip // Will remove row or clone new one. Complication is that 'advanced' and 'files' // tab duplicate the tables! var objId = $(obj).attr('id'); var rowNum = objId.substring(objId.length - 1); var val = $(obj).text(); if (!val || val.length === 0) val = $(obj).val(); // Remove this when non-CSS buttons go away var buttons; if (val === '+') { buttons = $("#plusButton"+rowNum); // Two tabs may have the exact same buttons! if (buttons.length > 0) { var table = null; $(buttons).each(function (i) { var tr = $(this).parents('tr.mdbSelect')[0]; if (tr) { table = $(tr).parents('table')[0]; var newTr = $(tr).clone(); var element = $(newTr).find("td[id^='hgt_mdbVal']")[0]; if (element) $(element).empty(); element = $(newTr).find("td[id^='isLike']")[0]; if (element) $(element).empty(); $(tr).after( newTr ); element = $(newTr).find("select.mdbVar")[0]; if (element) $(element).attr('selectedIndex',-1); // chrome needs this after 'after' } }); if (table) findTracks.mdbSelectRowsNormalize(table); // magic is in this function return false; } } else { // === '-' buttons = $("#minusButton"+rowNum); // Two tabs may have the exact same buttons! if (buttons.length > 0) { var remaining = 0; $(buttons).each(function (i) { var tr = $(this).parents('tr')[0]; var table = $(tr).parents('table')[0]; if (tr) $(tr).remove(); // Must renormalize since 2nd of 3 rows may have been removed remaining = findTracks.mdbSelectRowsNormalize(table); }); // Got to remove the cart vars, though it doesn't matter which as // count must not be too many. if (remaining > 0) { removeNum = remaining + 1; setCartVars( ["hgt_mdbVar"+removeNum,"hgt_mdbVal"+removeNum ], [ "[]","[]" ] ); } findTracks.clearFound(); // Changing values so abandon what has been found return false; } } return true; }, mdbSelectRowsNormalize: function (table) { // Called when [-][+] buttons changed the number of mdbSelects in findTracks\ // Will walk through each row and get the numberings of addressable elements correct. if (table) { var mdbSelectRows = $(table).find('tr.mdbSelect'); var needMinus = (mdbSelectRows.length > 2); $(table).find('tr.mdbSelect').each( function (ix) { var rowNum = ix + 1; // Each [-][+] and mdb var=val pair of selects must be numbered // First the [-][+] buttons var plusButton = $(this).find("[id^='plusButton']")[0]; if (plusButton) { // should always be a plus button var oldNum = Number(plusButton.id.substring(plusButton.id.length - 1)); if (oldNum === rowNum) return; // that is continue with the next row $(plusButton).attr('id',"plusButton"+rowNum); var minusButton = $(this).find("[id^='minusButton']")[0]; if (needMinus) { if (!minusButton) { if ($(plusButton).hasClass('pmButton')) $(plusButton).before("<span class='pmButton' id='minusButton"+ rowNum+"' title='delete this row' "+ "onclick='findTracks.mdbSelectPlusMinus(this);'>-</span>"); else // Remove this else when non-CSS buttons go away $(plusButton).before("<input type='button' id='minusButton"+rowNum+ "' value='-' style='font-size:.7em;' title='delete this row'" + " onclick='return findTracks.mdbSelectPlusMinus(this);'>"); } else $(minusButton).attr('id',"minusButton"+rowNum); } else if (minusButton) $(minusButton).remove(); } // Now the mdb var=val pair of selects var element = $(this).find(".mdbVar")[0]; // select var if (element) $(element).attr('name','hgt_mdbVar' + rowNum); element = $(this).find(".mdbVal")[0]; // select val if (element) { // not there if new row $(element).attr('name','hgt_mdbVal' + rowNum); if ($(element).hasClass('filterBy')) { $(element).attr('id',''); // removing id ensures renumbering id ddcl.reinit([ element ],true); } } // A couple more things element = $(this).find("td[id^='isLike']")[0]; if (element) $(element).attr('id','isLike' + rowNum); element = $(this).find("td[id^='hgt_mdbVal']")[0]; if (element) $(element).attr('id','hgt_mdbVal' + rowNum); }); return mdbSelectRows.length; } return 0; }, switchTabs: function (ui) { // switching tabs on findTracks page if (ui.panel.id === 'simpleTab' && $('div#found').length < 1) { // delay necessary, since select event not afterSelect event setTimeout(function() { $('input#simpleSearch').focus(); },20); } else if (ui.panel.id === 'advancedTab') { // Advanced tab has DDCL wigets which were sized badly because the hidden width // was unknown delay necessary, since select event not afterSelect event setTimeout(function() { ddcl.reinit($('div#advancedTab').find('select.filterBy'),false); },20); } if ($('div#filesFound').length === 1) { if (ui.panel.id === 'filesTab') $('div#filesFound').show(); else $('div#filesFound').hide(); } if ($('div#found').length === 1) { if (ui.panel.id !== 'filesTab') $('div#found').show(); else $('div#found').hide(); } } }; function escapeJQuerySelectorChars(str) { // replace characters which are reserved in jQuery selectors // (surprisingly jQuery does not have a built in function to do this). return str.replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~"])/g,'\\$1'); } var preloadImages = []; var preloadImageCount = 0; function preloadImg(url) // DEAD CODE? { // force an image to be loaded (e.g. for images in menus or dialogs). preloadImages[preloadImageCount] = new Image(); preloadImages[preloadImageCount].src = url; preloadImageCount++; } /////////////////// ///// mouse ///// /////////////////// var mouse = { savedOffset: {x:0, y:0}, saveOffset: function (ev) { // Save the mouse offset associated with this event mouse.savedOffset = {x: ev.clientX, y: ev.clientY}; }, hasMoved: function (ev) { // return true if mouse has moved a significant amount var minPixels = 10; var movedX = ev.clientX - mouse.savedOffset.x; var movedY = ev.clientY - mouse.savedOffset.y; if (arguments.length === 2) { var num = Number(arguments[1]); if (isNaN(num)) { if ( arguments[1].toLowerCase() === "x" ) return (movedX > minPixels || movedX < (minPixels * -1)); if ( arguments[1].toLowerCase() === "y" ) return (movedY > minPixels || movedY < (minPixels * -1)); } else minPixels = num; } return ( movedX > minPixels || movedX < (minPixels * -1) || movedY > minPixels || movedY < (minPixels * -1)); } }; /////////////////////////// //// Drag Reorder Code //// /////////////////////////// var dragReorder = { originalHeights: {}, // trackName: startHeight setOrder: function (table) { // Sets the 'order' value for the image table after a drag reorder var varsToUpdate = {}; $("tr.imgOrd").each(function (i) { if ($(this).attr('abbr') !== $(this).attr('rowIndex').toString()) { $(this).attr('abbr',$(this).attr('rowIndex').toString()); var name = this.id.substring('tr_'.length) + '_imgOrd'; varsToUpdate[name] = $(this).attr('abbr'); } }); if (objNotEmpty(varsToUpdate)) { cart.setVarsObj(varsToUpdate); imageV2.markAsDirtyPage(); } }, sort: function (table) { // Sets the table row order to match the order of the abbr attribute. // This is needed for back-button, and for visBox changes combined with refresh. var tbody = $(table).find('tbody')[0]; if (!tbody) tbody = table; // Do we need to sort? var trs = tbody.rows; var needToSort = false; $(trs).each(function(ix) { if ($(this).attr('abbr') !== $(this).attr('rowIndex').toString()) { needToSort = true; return false; // break for each() loops } }); if (!needToSort) return false; // Create array of tr holders to sort var ary = []; $(trs).each(function(ix) { // using sortTable found in utils.js ary.push(new sortTable.field(parseInt($(this).attr('abbr')),false,this)); }); // Sort the array ary.sort(sortTable.fieldCmp); // most efficient reload of sorted rows I have found var sortedRows = jQuery.map(ary, function(ary, i) { return ary.row; }); $(tbody).append( sortedRows ); // removes tr from current position and adds to end. return true; }, showCenterLabel: function (tr, show) { // Will show or hide centerlabel as requested // adjust button, sideLabel height, sideLabelOffset and centerlabel display if (!$(tr).hasClass('clOpt')) return; var center = normed($(tr).find(".sliceDiv.cntrLab")); if (!center) return; var seen = ($(center).css('display') !== 'none'); if (show === seen) return; var centerHeight = $(center).height(); var btn = normed($(tr).find("p.btn")); var side = normed($(tr).find(".sliceDiv.sideLab")); if (btn && side) { var sideImg = normed($(side).find("img")); if (sideImg) { var top = parseInt($(sideImg).css('top')); if (show) { $(btn).css('height',$(btn).height() + centerHeight); $(side).css('height',$(side).height() + centerHeight); top += centerHeight; // top is a negative number $(sideImg).css( {'top': top.toString() + "px" }); $( center ).show(); } else if (!show) { $(btn).css('height',$(btn).height() - centerHeight); $(side).css('height',$(side).height() - centerHeight); top -= centerHeight; // top is a negative number $(sideImg).css( {'top': top.toString() + "px" }); $( center ).hide(); } } } }, getContiguousRowSet: function (row) { // Returns the set of rows that are of the same class and contiguous if (!row) return null; var btn = $( row ).find("p.btn"); if (btn.length === 0) return null; var classList = $( btn ).attr("class").split(" "); var matchClass = classList[0]; var table = $(row).parents('table#imgTbl')[0]; var rows = $(table).find('tr'); // Find start index var startIndex = $(row).attr('rowIndex'); var endIndex = startIndex; for (var ix=startIndex-1; ix >= 0; ix--) { btn = $( rows[ix] ).find("p.btn"); if (btn.length === 0) break; classList = $( btn ).attr("class").split(" "); if (classList[0] !== matchClass) break; startIndex = ix; } // Find end index for (var rIx=endIndex; rIx<rows.length; rIx++) { btn = $( rows[rIx] ).find("p.btn"); if (btn.length === 0) break; classList = $( btn ).attr("class").split(" "); if (classList[0] !== matchClass) break; endIndex = rIx; } return rows.slice(startIndex,endIndex+1); // endIndex is 1 based! }, getCompositeSet: function (row) { // Returns the set of rows that are of the same class and contiguous if (!row) return null; var rowId = $(row).attr('id').substring('tr_'.length); var rec = hgTracks.trackDb[rowId]; if (tdbIsSubtrack(rec) === false) return null; var rows = $('tr.trDraggable:has(p.' + rec.parentTrack+')'); return rows; }, zipButtons: function (table) { // Goes through the image and binds composite track buttons when adjacent var rows = $(table).find('tr'); var lastClass=""; var lastBtn = null; var lastSide = null; var lastMatchesLast=false; var lastBlue=true; var altColors=false; var count=0; var countN=0; for (var ix=0; ix<rows.length; ix++) { // Need to have buttons in order var btn = $( rows[ix] ).find("p.btn"); var side = $( rows[ix] ).find(".sliceDiv.sideLab"); // added by GALT if (btn.length === 0) continue; var classList = $( btn ).attr("class").split(" "); var curMatchesLast=(classList[0] === lastClass); // centerLabels may be conditionally seen if ($( rows[ix] ).hasClass('clOpt')) { // if same composite and previous also centerLabel optional then hide center label if (curMatchesLast && $( rows[ix - 1] ).hasClass('clOpt')) dragReorder.showCenterLabel(rows[ix],false); else dragReorder.showCenterLabel(rows[ix],true); } // On with buttons if (lastBtn) { $( lastBtn ).removeClass('btnN btnU btnL btnD'); if (curMatchesLast && lastMatchesLast) { $( lastBtn ).addClass('btnL'); $( lastBtn ).css('height', $( lastSide ).height() - 0); // added by GALT } else if (lastMatchesLast) { $( lastBtn ).addClass('btnU'); $( lastBtn ).css('height', $( lastSide ).height() - 1); // added by GALT } else if (curMatchesLast) { $( lastBtn ).addClass('btnD'); $( lastBtn ).css('height', $( lastSide ).height() - 2); // added by GALT } else { $( lastBtn ).addClass('btnN'); $( lastBtn ).css('height', $( lastSide ).height() - 3); // added by GALT countN++; } count++; if (altColors) { // lastMatch and lastBlue or not lastMatch and notLastBlue lastBlue = (lastMatchesLast === lastBlue); if (lastBlue) // Too smart by 1/3rd $( lastBtn ).addClass( 'btnBlue' ); else $( lastBtn ).removeClass( 'btnBlue' ); } } lastMatchesLast = curMatchesLast; lastClass = classList[0]; lastBtn = btn; lastSide = side; } if (lastBtn) { $( lastBtn ).removeClass('btnN btnU btnL btnD'); if (lastMatchesLast) { $( lastBtn ).addClass('btnU'); $( lastBtn ).css('height', $( lastSide ).height() - 1); // added by GALT } else { $( lastBtn ).addClass('btnN'); $( lastBtn ).css('height', $( lastSide ).height() - 3); // added by GALT countN++; } if (altColors) { // lastMatch and lastBlue or not lastMatch and notLastBlue lastBlue = (lastMatchesLast === lastBlue); if (lastBlue) // Too smart by 1/3rd $( lastBtn ).addClass( 'btnBlue' ); else $( lastBtn ).removeClass( 'btnBlue' ); } count++; } //warn("Zipped "+count+" buttons "+countN+" are independent."); }, dragHandleMouseOver: function () { // Highlights a single row when mouse over a dragHandle column (sideLabel and buttons) if ( ! jQuery.tableDnD ) { //var handle = $("td.dragHandle"); //$(handle) // .unbind('mouseenter')//, jQuery.tableDnD.mousemove); // .unbind('mouseleave');//, jQuery.tableDnD.mouseup); return; } if ( ! jQuery.tableDnD.dragObject ) { $( this ).parents("tr.trDraggable").addClass("trDrag"); } }, dragHandleMouseOut: function () { // Ends row highlighting by mouse over $( this ).parents("tr.trDraggable").removeClass("trDrag"); }, buttonMouseOver: function () { // Highlights a composite set of buttons, regarless of whether tracks are adjacent if ( ! jQuery.tableDnD || ! jQuery.tableDnD.dragObject ) { var classList = $( this ).attr("class").split(" "); var btns = $( "p." + classList[0] ); $( btns ).removeClass('btnGrey'); $( btns ).addClass('btnBlue'); if (jQuery.tableDnD) { var rows = dragReorder.getContiguousRowSet($(this).parents('tr.trDraggable')[0]); if (rows) $( rows ).addClass("trDrag"); } } }, buttonMouseOut: function () { // Ends composite highlighting by mouse over var classList = $( this ).attr("class").split(" "); var btns = $( "p." + classList[0] ); $( btns ).removeClass('btnBlue'); $( btns ).addClass('btnGrey'); if (jQuery.tableDnD) { var rows = dragReorder.getContiguousRowSet($(this).parents('tr.trDraggable')[0]); if (rows) $( rows ).removeClass("trDrag"); } }, trMouseOver: function (e) { // Trying to make sure there is always a imageV2.lastTrack so that we know where we are var id = ''; var a = /tr_(.*)/.exec($(this).attr('id')); // voodoo if (a && a[1]) { id = a[1]; } if (id.length > 0) { if ( ! imageV2.lastTrack || imageV2.lastTrack.id !== id) imageV2.lastTrack = rightClick.makeMapItem(id); // currentMapItem gets set by mapItemMapOver. This is just backup } }, mapItemMouseOver: function () { // Record data for current map area item var id = this.id; if (!id || id.length === 0) { id = ''; var tr = $( this ).parents('tr.imgOrd'); if ( $(tr).length === 1 ) { var a = /tr_(.*)/.exec($(tr).attr('id')); // voodoo if (a && a[1]) { id = a[1]; } } } if (id.length > 0) { rightClick.currentMapItem = rightClick.makeMapItem(id); if (rightClick.currentMapItem) { rightClick.currentMapItem.href = this.href; rightClick.currentMapItem.title = this.title; // if the custom mouseover code has removed this title, check the attr // for the original title if (this.title.length === 0) { rightClick.currentMapItem.title = this.getAttribute("originalTitle"); } // Handle linked features with separate clickmaps for each exon/intron if ((this.title.indexOf('Exon ') === 0) || (this.title.indexOf('Intron ') === 0)) { // if the title is Exon ... or Intron ... // then search for the sibling with the same href // that has the real title item label var elem = this.parentNode.firstChild; while (elem) { if ((elem.href === this.href) && !((elem.title.indexOf('Exon ') === 0) || (elem.title.indexOf('Intron ') === 0))) { rightClick.currentMapItem.title = elem.title; break; } elem = elem.nextSibling; } } } } }, mapItemMouseOut: function () { imageV2.lastTrack = rightClick.currentMapItem; // Just a backup rightClick.currentMapItem = null; }, init: function () { // Make side buttons visible (must also be called when updating rows in the imgTbl). var btns = $("p.btn"); if (btns.length > 0) { dragReorder.zipButtons($('#imgTbl')); $(btns).mouseenter( dragReorder.buttonMouseOver ); $(btns).mouseleave( dragReorder.buttonMouseOut ); $(btns).show(); } var handle = $("td.dragHandle"); if (handle.length > 0) { $(handle).mouseenter( dragReorder.dragHandleMouseOver ); $(handle).mouseleave( dragReorder.dragHandleMouseOut ); } // setup mouse callbacks for the area tags $("#imgTbl").find("tr").mouseover( dragReorder.trMouseOver ); $("#imgTbl").find("tr").each( function (i, row) { // save the original y positions of each row //if (row.id in dragReorder.originalHeights === false) { dragReorder.originalHeights[row.id] = row.getBoundingClientRect().y + window.scrollY; //} }); $(".area").each( function(t) { this.onmouseover = dragReorder.mapItemMouseOver; this.onmouseout = dragReorder.mapItemMouseOut; this.onclick = posting.mapClk; }); } }; function trackHubSkipHubName(name) { // Just like hg/lib/trackHub.c's... var matches; if (name && (matches = name.match(/^hub_[0-9]+_(.*)/)) !== null) { return matches[1]; } else { return name; } } function replaceReserved(txt) { /* This should somehow be made more general so we can stop worrying about * user made tracks with whatever characters in it */ if (!txt) { throw new Error("trying to replace null txt"); } return txt.replace(/[^A-Za-z0-9_]/g, "_"); } function boundingRect(refEl) { /* For regular HTML elements, this function wraps getBoundingClientRect(). For area * elements like on hgTracks, getBoundingClientRect() won't work and we have to figure * everything out from the .coords attribute along with taking into account dragReorder, * page scroll, etc */ if (! (refEl instanceof Element)) { // not a map/area element, maybe from some other part of the UI console.log("trying to place a mouseover element next to an element that has not been created yet"); throw new Error(); } // obtain coordinates for placing the mouseover let refWidth, refHeight, refX, refY, y1; let refRight, refLeft, refTop, refBottom; let rect; let windowWidth = window.innerWidth; let windowHeight = window.innerHeight; if (refEl.coords !== undefined && refEl.coords.length > 0 && refEl.coords.split(",").length == 4) { // if we are dealing with an <area> element, the refEl width and height // are for the whole image and not for just the area, so // getBoundingClientRect() will return nothing, sad! let refImg = $("[usemap=#" + refEl.parentNode.name + "]")[0]; let refImgRect = refImg.getBoundingClientRect(); let refImgWidth = refImgRect.width; let refImgHeight = refImgRect.height; let label = $("[id^=td_side]")[0]; let btn = $("[id^=td_btn]")[0]; let labelWidth = 0, btnWidth = 0; if (label && btn) { labelWidth = label.getBoundingClientRect().width; btnWidth = label.getBoundingClientRect().width; } let imgWidth = refImgWidth; if (refEl.parentNode.name !== "ideoMap") { imgWidth -= labelWidth - btnWidth; } let refImgOffset = refImgRect.y + window.scrollY; // distance from start of image to top of viewport; [x1,y1,x2,y2] = refEl.coords.split(",").map(x => parseInt(x)); refX = x1; refY = y1; refWidth = x2 - x1; refHeight = y2 - y1; refRight = x2; refLeft = x1; refTop = y1; refBottom = y2; // now we need to offset our coordinates to the track tr, to account for dragReorder let parent = refEl.closest(".trDraggable"); let currParentOffset = 0, yDiff = 0; if (refEl.parentNode.name === "ideoMap") { parent = refImg.closest("tr"); currParentOffset = parent.getBoundingClientRect().y; yDiff = y1; } else if (parent) { // how far in y direction we are from the tr start in the original image from the server: currParentOffset = parent.getBoundingClientRect().y; yDiff = y1 - hgTracks.trackDb[parent.id.slice(3)].imgOffsetY; // if track labels are on, then the imgOffsetY will be off by the track label amount if (typeof hgTracks.centerLabelHeight !== 'undefined') { yDiff += hgTracks.centerLabelHeight; } } // account for dragReorder and track labels refTop = currParentOffset + yDiff; refBottom = currParentOffset + yDiff + refHeight; } else { rect = refEl.getBoundingClientRect(); refX = rect.x; refY = rect.y; refWidth = rect.width; refHeight = rect.height; refRight = rect.right; refLeft = rect.left; refTop = rect.top; refBottom = rect.bottom; } return {bottom: refBottom, height: refHeight, left: refLeft, right: refRight, top: refTop, width: refWidth, x: refX, y: refY}; } function positionMouseover(ev, refEl, popUpEl, mouseX, mouseY) { /* The actual mouseover positioning function. * refEl is an already existing element with coords that we use to position popUpEl. * popUpEl will try to be as close to the right/above the refEl, except when: * it would extend past the screen in which case it would go left/below appropriately. * the refEl takes up the whole screen, in which case we can cover the refEl * with no consequence */ rect = boundingRect(refEl); refX = rect.x; refY = rect.y; refWidth = rect.width; refHeight = rect.height; refRight = rect.right; refLeft = rect.left; refTop = rect.top; refBottom = rect.bottom; let windowWidth = window.innerWidth; let windowHeight = window.innerHeight; // figure out how large the mouseover will be let popUpRect = popUpEl.getBoundingClientRect(); // position the popUp to the right and above the cursor by default // tricky: when the mouse enters the element from the top, we want the tooltip // relatively close to the element itself, because the mouse will already be // on top of it, leaving it clickable or interactable. But if we are entering // the element from the bottom, if we position the tooltip close to the mouse, // we obscure the element itself, so we need to leave a bit of extra room let topOffset; if (Math.abs(mouseY - refBottom) < Math.abs(mouseY - refTop)) { // just use the mouseY position for placement, the -15 accounts for enough room topOffset = mouseY - window.scrollY - popUpRect.height - 15; } else { // just use the mouseY position for placement, the -5 accounts for cursor size topOffset = mouseY - window.scrollY - popUpRect.height - 5; } let leftOffset = mouseX; // add 15 for large cursor sizes // first case, refEl takes the whole width of the image, so not a big deal to cover some of it // this is most common for the track labels if (mouseX + popUpRect.width >= (windowWidth - 25)) { // move to the left leftOffset = mouseX - popUpRect.width; } // or the mouse is on the right third of the screen if (mouseX > (windowWidth* 0.66)) { // move to the left leftOffset = mouseX - popUpRect.width; } // the page is scrolled or otherwise off the screen if (topOffset <= 0) { topOffset = mouseY - window.scrollY; } if (leftOffset < 0) { throw new Error("trying to position off of screen to left"); } popUpEl.style.left = leftOffset + "px"; popUpEl.style.top = topOffset + "px"; } // the current mouseover timer, for showing the mouseover after a delay let mouseoverTimer; // the timer for when a user is moving the mouse after already bringing up // a pop up, there may be many items close together and we want the user // to bring up those mouseovers let mousemoveTimer; // flags to help figure out what state the users mouse is in: // hovered an item, moving to new item, moving to popup, moving away from popup/item let mousedNewItem = false; let canShowNewMouseover = true; // signal handler for when mousemove has gone far enough away from the pop up // we can't use removeEventListener because the function call is hard to keep // track of because of a bounded this keyword let mousemoveController; // The div that moves around the users screen with the visible mouseover text let mouseoverContainer; // the last element that triggered a mouseover event let lastMouseoverEle; function tooltipIsVisible() { /* Is the tooltip visible on the screen right now? */ return mouseoverContainer.style.visibility !== "hidden"; } function hideMouseoverText(ele) { /* Actually hides the tooltip text */ let tooltipTarget = ele; tooltipTarget.classList.remove("isShown"); tooltipTarget.style.opacity = "0"; tooltipTarget.style.visibility = "hidden"; } function mouseIsOverPopup(ev, ele, fudgeFactor=25) { /* Is the mouse positioned over the popup? */ let targetBox = ele.getBoundingClientRect(); let mouseX = ev.clientX; let mouseY = ev.clientY; if ( (mouseX >= (targetBox.left - fudgeFactor) && mouseX <= (targetBox.right + fudgeFactor) && mouseY >= (targetBox.top - fudgeFactor) && mouseY <= (targetBox.bottom + fudgeFactor)) ) { return true; } return false; } function mouseIsOverItem(ev, ele, fudgeFactor=25) { /* Is the mouse positioned over the item that triggered the popup? */ let origName = ele.getAttribute("origItemMouseoverId"); let origTargetBox = boundingRect($("[mouseoverid='"+origName+"']")[0]); let mouseX = ev.clientX; let mouseY = ev.clientY; if ( (mouseX >= (origTargetBox.left - fudgeFactor) && mouseX <= (origTargetBox.right + fudgeFactor) && mouseY >= (origTargetBox.top - fudgeFactor) && mouseY <= (origTargetBox.bottom + fudgeFactor)) ) { return true; } return false; } function mousemoveTimerHelper(triggeringMouseMoveEv, currTooltip) { - /* Called after 100ms of the mouse being stationary, show a new tooltip + /* Called after 500ms of the mouse being stationary, show a new tooltip * if we are over a new mouseover-able element */ e = triggeringMouseMoveEv; if (mousedNewItem && !(mouseIsOverPopup(e, currTooltip, 0))) { mousemoveController.abort(); hideMouseoverText(currTooltip); showMouseoverText(triggeringMouseMoveEv); } } function mousemoveHelper(e) { /* Helper function for deciding whether to keep a tooltip visible upon a mousemove event */ if (mousemoveTimer) { clearTimeout(mousemoveTimer); } mousemoveTimer = setTimeout(mousemoveTimerHelper, 500, e, this); // we are moving the mouse away, hide the tooltip regardless how much time has passed if (!(mouseIsOverPopup(e, this) || mouseIsOverItem(e, this))) { mousemoveController.abort(); hideMouseoverText(this); return; } } function showMouseoverText(ev) { /* If a tooltip is not visible, show the tooltip text right away. If a tooltip - * is viisble, do nothing as the mousemove event helper will re-call us + * is visble, do nothing as the mousemove event helper will re-call us * after hiding the tooltip that is shown */ ev.preventDefault(); let referenceElement = lastMouseoverEle; if (!tooltipIsVisible()) { let tooltipDivId = "#" + referenceElement.getAttribute("mouseoverid"); let tooltipDiv = $(tooltipDivId)[0]; if (!tooltipDiv) { return; } mouseoverContainer.replaceChildren(); let divCpy = tooltipDiv.cloneNode(true); divCpy.childNodes.forEach(function(n) { mouseoverContainer.appendChild(n); }); positionMouseover(ev, referenceElement, mouseoverContainer, ev.pageX, ev.pageY); mouseoverContainer.classList.add("isShown"); mouseoverContainer.style.opacity = "1"; mouseoverContainer.style.visibility = "visible"; mouseoverContainer.setAttribute("origItemMouseoverId", referenceElement.getAttribute("mouseoverid")); // Events all get their own unique id but they are tough to keep track of if we // want to remove one. We can use the AbortController interface to let the // web browser automatically raise a signal when the event is fired and remove // appropriate event mousemoveController = new AbortController(); let callback = mousemoveHelper.bind(mouseoverContainer); mousedNewItem = false; clearTimeout(mouseoverTimer); mouseoverTimer = undefined; // allow the user to mouse over the mouse over, (eg. clicking a link or selecting text) document.addEventListener("mousemove", callback, {signal: mousemoveController.signal}); document.addEventListener("scroll", callback, {signal: mousemoveController.signal}); } } function showMouseover(e) { /* Helper function for showing a mouseover. Uses a timeout function to allow * user to not immediately see all available tooltips. */ e.preventDefault(); // make the mouseover div: let ele1 = e.currentTarget; let text = ele1.getAttribute("mouseoverText"); if (ele1.getAttribute("mouseoverId") === null) { if (text.length > 0) { let newEl = document.createElement("span"); newEl.style = "max-width: 400px"; // max width of the mouseover text newEl.innerHTML = text; let newDiv = document.createElement("div"); newDiv.className = "tooltip"; newDiv.style.position = "fixed"; newDiv.style.display = "inline-block"; if (ele1.title) { newDiv.id = replaceReserved(ele1.title); ele1.setAttribute("originalTitle", ele1.title); ele1.title = ""; } else { newDiv.id = replaceReserved(text); } if (ele1.coords) { newDiv.id += "_" + ele1.coords.replaceAll(",","_"); } ele1.setAttribute("mouseoverid", newDiv.id); newDiv.append(newEl); ele1.parentNode.append(newDiv); } else { // shouldn't show a mouseover for something that doesn't have a mouseoverText attr, // meaning we got here without calling addMouseover(), this should not happen // but catch it to be safe return; } } // if a tooltip is currently visible, we need to wait for its mousemove // event to clear it before we can show this one, ie a user "hovers" // this element on their way to mousing over the shown mouseover mousedNewItem = true; lastMouseoverEle = ele1; if (mouseoverTimer) { // user is moving their mouse around, make sure where they stop is what we show clearTimeout(mouseoverTimer); } + if (mousemoveTimer) { + // user is moving their mouse around and has triggered a potentially triggered + // a new pop up, clear the move timeout + clearTimeout(mousemoveTimer); + } // If there is no tooltip present, we want a small but noticeable delay // before showing a tooltip if (canShowNewMouseover) { - if (!tooltipIsVisible()) { mouseoverTimer = setTimeout(showMouseoverText, 500, e); - } else { - // the user has a tooltip visible already, so our timer for showing - // a new one can be shorter because the user expects another one to - // pop up, but we still need to be conscious that they may be moving - // the mouse to the tooltip itself. The mousemoveHelper() deals with that - mouseoverTimer = setTimeout(showMouseoverText, 300, e); - } } } function addMouseover(ele1, text = null, ele2 = null) { /* Adds wrapper elements to control various mouseover events */ if (!mouseoverContainer) { mouseoverContainer = document.createElement("div"); mouseoverContainer.className = "tooltip"; mouseoverContainer.style.position = "fixed"; mouseoverContainer.style.display = "inline-block"; mouseoverContainer.style.visibility = "hidden"; mouseoverContainer.style.opacity = "0"; mouseoverContainer.id = "mouseoverContainer"; let tooltipTextSize = localStorage.getItem("tooltipTextSize"); if (tooltipTextSize === null) {tooltipTextSize = window.browserTextSize;} mouseoverContainer.style.fontSize = tooltipTextSize + "px"; document.body.append(mouseoverContainer); } if (ele1) { ele1.setAttribute("mouseoverText", text); ele1.addEventListener("mouseover", showMouseover, {capture: true}); } } function titleTagToMouseover(mapEl) { /* for a given area tag, extract the title text into a div that can be positioned * like a standard tooltip mouseover next to the item */ addMouseover(mapEl, mapEl.title); } function convertTitleTagsToMouseovers() { /* make all the title tags in the document have mouseovers */ $("[title]").each(function(i, a) { if (a.title !== undefined && a.title.length > 0) { titleTagToMouseover(a); } }); /* Mouseover should clear if you leave the document window altogether */ document.body.addEventListener("mouseleave", (ev) => { clearTimeout(mouseoverTimer); if (mousemoveController) { mousemoveController.abort(); } hideMouseoverText(mouseoverContainer); canShowNewMouseover = false; // let mouseovers show up again upon moving back in to the window // but only need the event once // use capture: true to force this event to happen // before the regular mouseover event document.body.addEventListener("mouseover", (evt) => { canShowNewMouseover = true; }, {capture: true, once: true}); }); /* make the mouseovers go away if we are in an input */ const inps = document.getElementsByTagName("input"); for (let inp of inps) { if (!(inp.type == "hidden" || inp.type == "HIDDEN")) { if (inp.type !== "submit") { inp.addEventListener("focus", (ev) => { if (mousemoveController) {mousemoveController.abort();} clearTimeout(mouseoverTimer); clearTimeout(mousemoveTimer); hideMouseoverText(mouseoverContainer); canShowNewMouseover = false; inp.addEventListener("blur", (evt) => { canShowNewMouseover = true; }, {once: true}); }); } else { // the buttons are inputs that don't blur right away (or ever? I can't tell), so // be sure to restore the tooltips when they are clicked inp.addEventListener("click", (ev) => { if (mousemoveController) {mousemoveController.abort();} clearTimeout(mouseoverTimer); clearTimeout(mousemoveTimer); hideMouseoverText(mouseoverContainer); canShowNewMouseover = true; }); } } } /* on a select, we can hide the tooltip on focus, but don't disable them * altogether, because it's easy to click out of a select without actually * losing focus, and we can't detect that because the web browser handles * that click separately */ const sels = document.getElementsByTagName("select"); for (let sel of sels) { sel.addEventListener("focus", (ev) => { if (mousemoveController) {mousemoveController.abort();} clearTimeout(mouseoverTimer); hideMouseoverText(mouseoverContainer); canShowNewMouseover = true; }); for (let opt of sel.options) { opt.addEventListener("click", (evt) => { if (mousemoveController) {mousemoveController.abort();} clearTimeout(mouseoverTimer); hideMouseoverText(mouseoverContainer); canShowNewMouseover = true; }); } } /* Make the ESC key hide tooltips */ document.body.addEventListener("keyup", (ev) => { if (ev.keyCode === 27) { clearTimeout(mouseoverTimer); hideMouseoverText(mouseoverContainer); canShowNewMouseover = true; } }); } function parseUrl(url) { // turn a url into some of it's components like server, query-string, etc let protocol, serverName, pathInfo, queryString; let temp; temp = url.split("?"); if (temp.length > 1) queryString = temp.slice(1).join("?"); temp = temp[0].split("/"); protocol = temp[0]; // "https:" serverName = temp[2]; // "genome-test.gi.ucsc.edu" pathInfo = temp.slice(3).join("/"); // "cgi-bin/hgTracks" return {protocol: protocol, serverName: serverName, pathInfo: pathInfo, queryString: queryString}; } function dumpCart(seconds, skipNotification) { // dump current cart let currUrl = parseUrl(window.location.href); logUrl = currUrl.protocol + "//" + currUrl.serverName + "/" + currUrl.pathInfo + "?hgsid=" + getHgsid() + "&_dumpCart=" + encodeURIComponent(seconds) + "&skipNotif=" + skipNotification; let xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", logUrl, true); xmlhttp.send(); // sends request and exits this function } function writeToApacheLog(msg) { // send msg to web servers error_log // first need to figure out what server and CGI we are requesting: let currUrl = parseUrl(window.location.href); logUrl = currUrl.protocol + "//" + currUrl.serverName + "/" + currUrl.pathInfo + "?_dumpToLog=" + encodeURIComponent(msg); let xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", logUrl, true); xmlhttp.send(); // sends request and exits this function } function addRecentSearch(db, searchTerm, extra={}) { // Push a searchTerm onto a stack in localStorage to show users their most recent // search terms. If an optional extra argument is supplied (ex: the response from hgSuggest), // save that as well // The searchStack object (note: saved as a string via JSON.stringify in localStorage) keeps // a per database stack of the 5 most recently searched terms, as well as their "result", // which can be an autocomplete object from hgSuggest, something from hgSearch, or just nothing // Example: // var searchStack = { // hg38: { // "stack": ["foxp", "flag", "fla"], // "results: { // "foxp": { // "value": "FOXP1 (Homo sap...", // "id": "chr3:70954708-71583728", // ... // }, // "flag": {}, // NOTE: empty object // "fla": { // "value": ..., // "id": ..., // }, // } // }, // mm10: { // "stack": [...], // "results": {}, // } let searchStack = window.localStorage.getItem("searchStack"); let searchObj = {}; if (searchStack === null) { searchObj[db] = {"stack": [searchTerm], "results": {}}; searchObj[db].results[searchTerm] = extra; window.localStorage.setItem("searchStack", JSON.stringify(searchObj)); } else { searchObj = JSON.parse(searchStack); if (db in searchObj) { let searchList = searchObj[db].stack; if (searchList.includes(searchTerm)) { // remove it from wherever it is cause it's going to the front searchList.splice(searchList.indexOf(searchTerm), 1); } else { searchObj[db].results[searchTerm] = extra; if (searchList.length >= 5) { let toDelete = searchList.pop(); delete searchObj[db].results[toDelete]; } } searchList.unshift(searchTerm); searchObj.stack = searchList; } else { searchObj[db] = {"stack": [searchTerm], "results": {}}; searchObj[db].results[searchTerm] = extra; } window.localStorage.setItem("searchStack", JSON.stringify(searchObj)); } }