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"