2102d096150eb0ea18eb2b935dc1d837cf252184 chmalee Fri Jun 28 12:39:45 2024 -0700 Fix a few bugs in the tooltip implementation revealed when making some tooltips have longer wait timers: 1. Get item coordinates that was moused over correctly. 2. Set up the scroll and mouse move events and their signals on mouseover, and not in both mouseover and upon actually showing the text, the competing functions were causing events to not get removed and then other events could not fire appropriately, refs #34007 diff --git src/hg/js/utils.js src/hg/js/utils.js index d6ca08a..94aab6a 100644 --- src/hg/js/utils.js +++ src/hg/js/utils.js @@ -1,4416 +1,4410 @@ // 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 0 && inpType !== 'select' && ele.type !== inpType) continue; var identifier = ele.name; if (nameOrId.search(/id/i) !== -1) identifier = ele.id; var failed = false; if (prefix.length > 0) failed = (identifier.indexOf(prefix) !== 0); if (!failed && suffix.length > 0) failed = (identifier.lastIndexOf(suffix) !== (identifier.length - suffix.length)); if (!failed) { for (var aIx=4;aIx2) alert("arrayOfInputsThatMatch is unimplemented for this browser"); } return found; } function normed(thing) { // RETURNS undefined, the lone member of the set or the full set if more than one member. // Used for normalizing returns from jquery DOM selects (e.g. $('tr.track').children('td.data')) // jquery returns an "array like 'object'" with 0 or more entries. // May be used on non-jquery objects and will reduce single element arrays to the element. // Use this to treat 0 entries the same as undefined and 1 entry as the item itself if (typeof(thing) === 'undefined' || thing === null || (thing.length !== undefined && thing.length === 0) // Empty array (or 'array like object') || ($.isPlainObject(thing) && $.isEmptyObject(thing))) // Empty simple object return undefined; if (thing.length && thing.length === 1 && 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(""); } 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