19638d540a2552c4eb732adb5a032d97f45b375c
chmalee
  Fri Feb 9 12:05:13 2024 -0800
Mouseover performance optimization, do not make each mouseover element on page load, instead wait for an actual mouseover event to trigger before adding the mouseover nodes, refs #32997

diff --git src/hg/js/utils.js src/hg/js/utils.js
index 04aabb9..628ee07 100644
--- src/hg/js/utils.js
+++ src/hg/js/utils.js
@@ -3941,30 +3941,32 @@
 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) {
@@ -3980,113 +3982,138 @@
 }
 
 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) {
+function mousemoveTimerHelper(triggeringMouseMoveEv, currTooltip, originalEl) {
     /* Called after 100ms of the mouse being stationary, show a new tooltip
-     * if we are over a new mouseover element */
+     * if we are over a new mouseover-able element */
     e = triggeringMouseMoveEv;
     if (mousedNewItem && !(mouseIsOverPopup(e, currTooltip, 0))) {
         mousemoveController.abort();
         hideMouseoverText(currTooltip);
-        showMouseoverText(triggeringMouseMoveEv);
+        showMouseoverText(triggeringMouseMoveEv, originalEl);
     }
 }
 
 function mousemoveHelper(e) {
     /* Helper function for deciding whether to keep a tooltip visible upon a mousemove event */
     if (mousemoveTimer) {
         clearTimeout(mousemoveTimer);
     }
-    mousemoveTimer = setTimeout(mousemoveTimerHelper, 100, e, this);
+    mousemoveTimer = setTimeout(mousemoveTimerHelper, 100, e, this, lastMouseoverEle);
     // 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(e) {
-    /* 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();
-    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
+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
+     * after hiding the tooltip that is shown */
+    ev.preventDefault();
+    let referenceElement = lastMouseoverEle;
+
     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);
+        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 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, 100, e);
@@ -4100,58 +4127,32 @@
         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);
     }
     // create a mouseover element out of text, or, if text is null, use already
     // created ele2 and just show it
     newEl = ele2;
-    if (text !== null) {
-        newEl = document.createElement("span");
-        newEl.style = "max-width: 400px"; // max width of the mouseover text
-        newEl.innerHTML = text;
-    } else {
-        text = ele2.innerHTML;
-        // if newEl was already created (as in on the server side), then
-        // it may have had it's visibility hidden by default for page load purposes
-        newEl.style.display = "inline-block";
-    }
+    ele1.setAttribute("mouseoverText", text);
     if (ele1) {
-        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);
         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);