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