ef0895aab6133bd856473607aa3716f13ccb9860 chmalee Thu Sep 28 18:09:29 2023 -0700 Fix new mouseover code to better handle the case of moving between close items and moving to the popup. I was using a second timer correctly but not setting the flags correctly for the control of showing/hiding the popup. Also did some refactoring to make the flag setting possible, refs Jonathan email, refs #31365 diff --git src/hg/js/utils.js src/hg/js/utils.js index 38454c2..830b712 100644 --- src/hg/js/utils.js +++ src/hg/js/utils.js @@ -3758,37 +3758,44 @@ this.onclick = posting.mapClk; }); } }; function trackHubSkipHubName(name) { // Just like hg/lib/trackHub.c's... var matches; if (name && (matches = name.match(/^hub_[0-9]+_(.*)/)) !== null) { return matches[1]; } else { return name; } } -function positionMouseover(ev, refEl, popUpEl, mouseX, mouseY) { - /* The actual mouseover positioning function. - * refEl is an already existing element with coords that we use to - * position popUpEl. popUpEl will try to be as close to the right/above the refEl, except when: - * it would extend past the screen in which case it would go left/below appropriately. - * the refEl takes up the whole screen, in which case we can cover the refEl - * with no consequence */ +function replaceReserved(txt) { + /* This should somehow be made more general so we can stop worrying about + * user made tracks with whatever characters in it */ + if (!txt) { + throw new Error("trying to replace null txt"); + } + return txt.replace(/[^A-Za-z0-9_]/g, "_"); +} + +function boundingRect(refEl) { + /* For regular HTML elements, this function wraps getBoundingClientRect(). For area + * elements like on hgTracks, getBoundingClientRect() won't work and we have to figure + * everything out from the .coords attribute along with taking into account dragReorder, + * page scroll, etc */ if (! (refEl instanceof Element)) { // not a map/area element, maybe from some other part of the UI console.log("trying to place a mouseover element next to an element that has not been created yet"); throw new Error(); } // 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 element, the refEl width and height // are for the whole image and not for just the area, so @@ -3829,166 +3836,217 @@ // 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; 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. + * refEl is an already existing element with coords that we use to position popUpEl. + * popUpEl will try to be as close to the right/above the refEl, except when: + * it would extend past the screen in which case it would go left/below appropriately. + * the refEl takes up the whole screen, in which case we can cover the refEl + * with no consequence */ + rect = boundingRect(refEl); + refX = rect.x; refY = rect.y; + refWidth = rect.width; refHeight = rect.height; + refRight = rect.right; refLeft = rect.left; + refTop = rect.top; refBottom = rect.bottom; + let windowWidth = window.innerWidth; + let windowHeight = window.innerHeight; // figure out how large the mouseover will be // use firstChild because popUpEl should be a div with a sole child in it let popUpRect = popUpEl.firstChild.getBoundingClientRect(); // position the popUp to the right and above the cursor by default let topOffset = refTop - popUpRect.height; let leftOffset = mouseX + 15; // add 15 for large cursor sizes // first case, refEl takes the whole width of the image, so not a big deal to cover some of it // this is most common for the track labels if (mouseX + popUpRect.width >= (windowWidth - 25)) { // move to the left leftOffset = mouseX - popUpRect.width; } // or the mouse is on the right third of the screen if (mouseX > (windowWidth* 0.66)) { // move to the left leftOffset = mouseX - popUpRect.width; } if (leftOffset < 0) { throw new Error("trying to position off of screen to left"); } popUpEl.style.left = leftOffset + "px"; popUpEl.style.top = topOffset + "px"; } -function replaceReserved(txt) { - /* This should somehow be made more general so we can stop worrying about - * user made tracks with whatever characters in it */ - if (!txt) { - throw new Error("trying to replace null txt"); - } - return txt.replace(/[^A-Za-z0-9_]/g, "_"); -} - // the current mouseover timer, for showing the mouseover after a delay var 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 var mousemoveTimer; -var showDiffMouseover = false; +// 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 +var mousedNewItem = false; +var 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 -var mousemoveSignal; +let mousemoveController; // The div that moves around the users screen with the visible mouseover text var mouseoverContainer; 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=45) { + /* 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=45) { + /* 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(e) { - showDiffMouseover = true; + /* user has moved the mouse and then stopped moving for long enough. */ clearTimeout(mousemoveTimer); + canShowNewMouseover = true; mousemoveTimer = undefined; } function mousemoveHelper(e) { /* Helper function for deciding whether to keep a tooltip visible upon a mousemove event */ if (mousemoveTimer === undefined) { // if we are over another mouseable element we want to show that one instead // use this timer to do so - showDiffMouseover = false; - mousemoveTimer = setTimeout(mousemoveTimerHelper, 100, e); - } - let targetBox = this.getBoundingClientRect(); - let mouseX = e.clientX; - let mouseY = e.clientY; - // currently allow the mouse to move in a 45 pixel box around the element - if ( mouseX >= (targetBox.left - 45) && mouseX <= (targetBox.right + 45) && - mouseY >= (targetBox.top - 45) && mouseY <= (targetBox.bottom + 45) - && !showDiffMouseover) { + let callback = mousemoveTimerHelper.bind(mouseoverContainer); + mousemoveTimer = setTimeout(callback, 250, e); + } + + if (mousedNewItem && canShowNewMouseover && !mouseIsOverPopup(e, this, 0)) { + // the !mouseIsOverPopup() check catches the corner case where the mouse + // was moved slowly enough to the popup to set off the mousemoveTimer, but + // is now over the pop up itself + hideMouseoverText(this); + mousemoveController.abort(); + } else { + if (mouseIsOverPopup(e, this) || mouseIsOverItem(e, this)) { + // the mouse is in the general area of the popup/item + // the mouse needs to stop moving and then the flag will + // get set by the mousemoveTimer + canShowNewMouseover = false; return; } else { - // now that we are going to hide the pop up we can remove the event listener - // for whether we wanted to keep the pop up or not - if ($(".tooltip.isShown").length > 0) { - console.log("hiding mouseover in mousemove helper:"); - console.log(this); hideMouseoverText(this); + mousemoveController.abort(); + canShowNewMouseover = true; } - mousemoveSignal.abort(); } } function showMouseoverText(e) { // actually show the mouseover text e.preventDefault(); + // the mouseover event will fire all the time, let the mousemove code + // set the flags for whether its time to show the new mouseover or not + if (mousedNewItem && canShowNewMouseover) { referenceElement = e.target; 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]; mouseoverContainer.replaceChildren(); let divCpy = tooltipDiv.cloneNode(true); divCpy.childNodes.forEach(function(n) { mouseoverContainer.appendChild(n); }); positionMouseover(e, referenceElement, mouseoverContainer, e.pageX, e.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 new AbortController interface to let the + // 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 - mousemoveSignal = new AbortController(); + mousemoveController = new AbortController(); let callback = mousemoveHelper.bind(mouseoverContainer); - // allow the user to mouse over the mouse over, (eg. clicking a link or selecting text) - document.addEventListener("mousemove", callback, {signal: mousemoveSignal.signal}); - document.addEventListener("scroll", callback, {signal: mousemoveSignal.signal}); + mousedNewItem = false; + canShowNewMouseover = 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(); - // if a tooltip is currently visible, we need to wait for its mouseout + // 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; if (mouseoverTimer) { // user is moving their mouse around, make sure where they stop is what we show clearTimeout(mouseoverTimer); } mouseoverTimer = setTimeout(showMouseoverText, 300, 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"; mouseoverContainer.style.position = "fixed"; mouseoverContainer.style.display = "inline-block"; mouseoverContainer.style.visibility = "hidden";