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