eedf621b1bb9fc52351772ddf4b87a26211449f2
chmalee
  Fri Jun 21 10:07:16 2024 -0700
Fix tooltip mouseover event firing while mouse is still moving, refs #34007

diff --git src/hg/js/utils.js src/hg/js/utils.js
index 861a5da..d6ca08a 100644
--- src/hg/js/utils.js
+++ src/hg/js/utils.js
@@ -3859,32 +3859,37 @@
         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;
+            parentRect = parent.getBoundingClientRect();
+            currParentOffset = parentRect.y;
             yDiff = y1;
+            // and offset the x coordinate, because we are in a <center> element
+            refX += parentRect.x;
+            refLeft += parentRect.left;
+            refRight += parentRect.x;
         } 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;
@@ -3990,68 +3995,95 @@
 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");
+    if (origName === null) {origName = ele.getAttribute("mouseoverid");}
     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 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 a tooltip is not visible and we are not currently over the item that triggered
+    // the mouseover, then we want to stop the mouseover event and wait for a new mouseover
+    // event
+    if (!tooltipIsVisible()) {
+        if (!(mouseIsOverItem(e, lastMouseoverEle, 0))) {
+            // we have left the item boundaries, cancel any timers
+            mousemoveController.abort();
+            clearTimeout(mouseoverTimer);
+            clearTimeout(mousemoveTimer);
+        }
+        return;
+    }
+
+    // otherwise, a tooltip is visible or we are over the item that triggered the mouseover
+    // but we have just moved the mouse a little
     if (mousemoveTimer) {
         clearTimeout(mousemoveTimer);
     }
+    // for the currently shown tooltip if any:
+    let currTooltipItem = this.getAttribute("origItemMouseoverId");
+    let currTooltipDelayedSetting = this.getAttribute("isDelayedTooltip");
+    let currTooltipIsDelayed = currTooltipDelayedSetting === "delayed";
+
+    // the tooltip that just triggered a mouseover event (not mousemove!)
     let isDelayedTooltip = lastMouseoverEle.getAttribute("tooltipDelay");
-    if (isDelayedTooltip !== null && isDelayedTooltip === "delayed") {
-        mousemoveTimer = setTimeout(mousemoveTimerHelper, 1500, e, this);
-        mousedNewItem = true;
+
+    // if the currently shown tooltip is a delayed one, hide the tooltip because,
+    // the mouse has moved, regardless how much time has passed
+    if (currTooltipIsDelayed || !(mouseIsOverItem(e, this) || mouseIsOverPopup(e, this))) {
         mousemoveController.abort();
         hideMouseoverText(this);
+    }
+
+    // wait for the mouse to stop moving:
+    if (currTooltipIsDelayed) {
+        mousemoveTimer = setTimeout(mousemoveTimerHelper, 1500, e, this);
+        mousedNewItem = true;
     } else {
         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 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() &&
             // wiggle mouseovers have special code, don't use these tooltips for those:
             (typeof mouseOver === "undefined" || !mouseOver.visible)) {
@@ -4073,34 +4105,34 @@
 
         // some tooltips are special and have a long delay, make sure the container knows
         // that too
         let isDelayed = referenceElement.getAttribute("tooltipDelay");
         if (isDelayed !== null && isDelayed === "delayed") {
             mouseoverContainer.setAttribute("isDelayedTooltip", "delayed");
         } else {
             mouseoverContainer.setAttribute("isDelayedTooltip", "normal");
         }
         // 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) {
@@ -4132,37 +4164,44 @@
             // 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
+        // user is moving their mouse around and has 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) {
+        // set up the mousemove handlers to prevent showing a tooltip if we have
+        // already moved on from this item by the time the below delay passes:
+        mousemoveController = new AbortController();
+        let callback = mousemoveHelper.bind(mouseoverContainer);
+        document.addEventListener("mousemove", callback, {signal: mousemoveController.signal});
+        document.addEventListener("scroll", callback, {signal: mousemoveController.signal});
+
         // some tooltips are special and have a longer delay
         let isDelayedTooltip = ele1.getAttribute("tooltipDelay");
         if (isDelayedTooltip !== null && isDelayedTooltip === "delayed") {
             mouseoverTimer = setTimeout(showMouseoverText, 1500, e);
         } else {
             mouseoverTimer = setTimeout(showMouseoverText, 500, 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";