dcceb7569cea2c0e776a24e5892199a5e709855d chmalee Tue Dec 19 16:06:53 2023 -0800 Fix a major tooltip bug, after the timeout ended for the mouse to stop moving, need to re-trigger the most recent mouseover event so a new tooltip can show up, refs #32697 diff --git src/hg/js/utils.js src/hg/js/utils.js index 7243f04..df46939 100644 --- src/hg/js/utils.js +++ src/hg/js/utils.js @@ -3878,72 +3878,81 @@ 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 + 15; // add 15 for large cursor sizes + 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 -var mouseoverTimer; +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 -var mousemoveTimer; +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 -var mousedNewItem = false; -var canShowNewMouseover = true; +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 -var mouseoverContainer; +let mouseoverContainer; +// save the last moused over ele to show when the mouse has stopped moving +let newTooltipElement; +// the last time the mouse moved +//let lastTime = 0; + +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) && @@ -3954,127 +3963,131 @@ } 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(e) { - /* user has moved the mouse and then stopped moving for long enough. */ - clearTimeout(mousemoveTimer); - canShowNewMouseover = true; - mousemoveTimer = undefined; +function mousemoveTimerHelper(triggeringMouseMoveEv, currTooltip) { + /* Called after 100ms of the mouse being stationary, show a new tooltip + * if we are over a new mouseover element */ + e = triggeringMouseMoveEv; + if (mousedNewItem && !(mouseIsOverPopup(e, currTooltip, 0) || mouseIsOverItem(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 === undefined && canShowNewMouseover) { - // if we are over another mouseable element we want to show that one instead - // use this timer to do so - let callback = mousemoveTimerHelper.bind(mouseoverContainer); - mousemoveTimer = setTimeout(callback, 300, e); + if (mousemoveTimer) { + clearTimeout(mousemoveTimer); } - - if (mousedNewItem && canShowNewMouseover && !mouseIsOverPopup(e, this, 0)) { - // the !mouseIsOverPopup() check catches the corner case where the mouse - // was moved slowly enough to the popup to set off the mousemoveTimer, but - // is now over the pop up itself - hideMouseoverText(this); + mousemoveTimer = setTimeout(mousemoveTimerHelper, 100, 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(); - } else { - if (mouseIsOverPopup(e, this) || mouseIsOverItem(e, this)) { - // the mouse is in the general area of the popup/item - // the mouse needs to stop moving and then the flag will - // get set by the mousemoveTimer - canShowNewMouseover = false; - return; - } else { hideMouseoverText(this); - mousemoveController.abort(); - canShowNewMouseover = true; - } + return; } } function showMouseoverText(e) { - // actually show the mouseover text + /* If a tooltip is not visible, show the tooltip text right away. + * If a tooltip is visible, see how long it has been since the mouse + * stopped moving, and if the mouse has stopped, show a new tootlip + * if necessary */ e.preventDefault(); - // the mouseover event will fire all the time, let the mousemove code - // set the flags for whether its time to show the new mouseover or not - if (mousedNewItem && canShowNewMouseover ) { - referenceElement = e.target; + let referenceElement = e.target; + // if a tooltip is not visible, then we can show a new tooltip + // if a tooltip is visible, the mousemove event listener code will call + // this function + if (!tooltipIsVisible()) { if (!e.target.getAttribute("mouseoverid")) { // corner case: the side slice and grey control bar slice are weird, the td // container has the title tag, while the img or a element receives the mouseover // event, so we need to go back up the tree to find the mouseoverid for those elems while (referenceElement.parentElement && !referenceElement.getAttribute("mouseoverid")) { referenceElement = referenceElement.parentElement; } } 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(e, referenceElement, mouseoverContainer, e.pageX, e.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; - canShowNewMouseover = 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}); + } else { + newTooltipElement = referenceElement; } } 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(); // 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; if (mouseoverTimer) { // user is moving their mouse around, make sure where they stop is what we show clearTimeout(mouseoverTimer); } - // prevent attaching a timer if something has focus + // If there is no tooltip present, we want a small but noticeable delay + // before showing a tooltip if (canShowNewMouseover) { - mouseoverTimer = setTimeout(showMouseoverText, 300, e); + 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, 100, 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;} @@ -4100,43 +4113,43 @@ 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); - ele1.addEventListener("mouseover", showMouseover); + 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) { + $("[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 @@ -4182,40 +4195,37 @@ 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; }); } } -} -function newTooltips() { - /* For server side printed tooltip texts, make them work as pop ups. - * Please note that the Tooltiptext node can have any arbitrary html in it, like - * line breaks or links*/ - $("[title]").each(function(i, n) { - tooltiptext = n.getAttribute("title"); - if (tooltiptext !== null) { - addMouseover(n, tooltiptext); + /* 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"