2102d096150eb0ea18eb2b935dc1d837cf252184 chmalee Fri Jun 28 12:39:45 2024 -0700 Fix a few bugs in the tooltip implementation revealed when making some tooltips have longer wait timers: 1. Get item coordinates that was moused over correctly. 2. Set up the scroll and mouse move events and their signals on mouseover, and not in both mouseover and upon actually showing the text, the competing functions were causing events to not get removed and then other events could not fire appropriately, refs #34007 diff --git src/hg/js/utils.js src/hg/js/utils.js index d6ca08a..94aab6a 100644 --- src/hg/js/utils.js +++ src/hg/js/utils.js @@ -3835,73 +3835,52 @@ } // obtain coordinates for placing the mouseover let refWidth, refHeight, refX, refY, y1; let refRight, refLeft, refTop, refBottom; let rect; let windowWidth = window.innerWidth; let windowHeight = window.innerHeight; if (refEl.coords !== undefined && refEl.coords.length > 0 && refEl.coords.split(",").length == 4) { // if we are dealing with an <area> element, the refEl width and height // are for the whole image and not for just the area, so // getBoundingClientRect() will return nothing, sad! let refImg = $("[usemap=#" + refEl.parentNode.name + "]")[0]; let refImgRect = refImg.getBoundingClientRect(); let refImgWidth = refImgRect.width; - let refImgHeight = refImgRect.height; let label = $("[id^=td_side]")[0]; let btn = $("[id^=td_btn]")[0]; let labelWidth = 0, btnWidth = 0; if (label && btn) { labelWidth = label.getBoundingClientRect().width; btnWidth = label.getBoundingClientRect().width; } let imgWidth = refImgWidth; if (refEl.parentNode.name !== "ideoMap") { imgWidth -= labelWidth - btnWidth; } - let refImgOffset = refImgRect.y + window.scrollY; // distance from start of image to top of viewport; + let refImgOffsetY = refImgRect.y; // distance from start of image to top of viewport, includes any scroll; [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"); - 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; + refX = x1 + refImgRect.x; + refY = y1 + refImgRect.y; + refRight = x2 + refImgRect.left; + refLeft = x1 + refImgRect.left; + refTop = y1 + refImgOffsetY; + refBottom = y2 + refImgOffsetY; + refWidth = x2 - x1; + refHeight = y2 - y1; + } else { rect = refEl.getBoundingClientRect(); refX = rect.x; refY = rect.y; refWidth = rect.width; refHeight = rect.height; refRight = rect.right; refLeft = rect.left; refTop = rect.top; refBottom = rect.bottom; } return {bottom: refBottom, height: refHeight, left: refLeft, right: refRight, top: refTop, width: refWidth, x: refX, y: refY}; } function positionMouseover(ev, refEl, popUpEl, mouseX, mouseY) { /* The actual mouseover positioning function. @@ -4026,116 +4005,121 @@ 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(); + rect = boundingRect(lastMouseoverEle); clearTimeout(mouseoverTimer); clearTimeout(mousemoveTimer); + // we can safely stop listening for mousemove now, because we need a new mouseover event + // to start a new mousemove event: + mousemoveController.abort(); } 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 + // but we have just moved the mouse a little, reset the timer 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 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); + return; } // wait for the mouse to stop moving: if (currTooltipIsDelayed) { mousemoveTimer = setTimeout(mousemoveTimerHelper, 1500, e, this); mousedNewItem = true; } else { mousemoveTimer = setTimeout(mousemoveTimerHelper, 500, e, this); if (!(mouseIsOverPopup(e, this) || mouseIsOverItem(e, this))) { return; } } } +// Add some info text at the bottom of each tooltip: +const infoText = document.createElement("p"); +infoText.style = "margin-bottom: 0"; +infoText.textContent = "Wiggle mouse or press ESC to close this tooltip"; + 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)) { 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); }); + mouseoverContainer.appendChild(infoText); 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")); // 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) { if (text.length > 0) { let newEl = document.createElement("span"); newEl.style = "max-width: 400px"; // max width of the mouseover text @@ -4173,34 +4157,44 @@ 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 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: + if (mousemoveController) { + // a previous mouseover event has fired and it waiting, clear it before + // setting up this one + mousemoveController.abort(); + } mousemoveController = new AbortController(); let callback = mousemoveHelper.bind(mouseoverContainer); document.addEventListener("mousemove", callback, {signal: mousemoveController.signal}); - document.addEventListener("scroll", callback, {signal: mousemoveController.signal}); + document.addEventListener("scroll", function(e) { + hideMouseoverText(mouseoverContainer); + clearTimeout(mousemoveTimer); + mousemoveController.abort(); + canShowNewMouseover = true; + }); // 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");