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);