« get me outta code hell

client: hoverable-tooltip: avoid elementFromPoint for hit detection - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2026-04-27 13:06:56 -0300
committer(quasar) nebula <qznebula@protonmail.com>2026-04-27 13:06:56 -0300
commitb74fa37385c147de772f689b003bd3d46eab912e (patch)
tree33450fb78250436d387996407132df528dd666c4 /src
parentb4dce2cbdfc295402d29badfcc6b7f0db0e91493 (diff)
client: hoverable-tooltip: avoid elementFromPoint for hit detection
Diffstat (limited to 'src')
-rw-r--r--src/static/js/client-util.js8
-rw-r--r--src/static/js/client/hoverable-tooltip.js48
-rw-r--r--src/static/js/rectangles.js16
3 files changed, 39 insertions, 33 deletions
diff --git a/src/static/js/client-util.js b/src/static/js/client-util.js
index 0c113758..a6846714 100644
--- a/src/static/js/client-util.js
+++ b/src/static/js/client-util.js
@@ -73,14 +73,6 @@ export function decodeEntities(string) {
   return textarea.value;
 }
 
-// Curry-style, so multiple points can more conveniently be tested at once.
-export function pointIsOverAnyOf(elements) {
-  return (clientX, clientY) => {
-    const element = document.elementFromPoint(clientX, clientY);
-    return elements.some(el => el.contains(element));
-  };
-}
-
 export function getVisuallyContainingElement(child) {
   let parent = child.parentElement;
 
diff --git a/src/static/js/client/hoverable-tooltip.js b/src/static/js/client/hoverable-tooltip.js
index 2d1ac552..863f4d64 100644
--- a/src/static/js/client/hoverable-tooltip.js
+++ b/src/static/js/client/hoverable-tooltip.js
@@ -6,7 +6,6 @@ import {
   cssProp,
   dispatchInternalEvent,
   getVisuallyContainingElement,
-  pointIsOverAnyOf,
 } from '../client-util.js';
 
 import {info as stickyHeadingInfo} from './sticky-heading.js';
@@ -387,11 +386,14 @@ function handleTooltipHoverableTouchEnded(hoverable, domEvent) {
   // Don't proceed if none of the (just-ended) touches ended over the
   // hoverable.
 
-  const pointIsOverThisHoverable = pointIsOverAnyOf([hoverable]);
-
-  const anyTouchEndedOverHoverable =
-    touches.some(({clientX, clientY}) =>
-      pointIsOverThisHoverable(clientX, clientY));
+  let anyTouchEndedOverHoverable = false;
+  for (const touch of touches) {
+    const point = WikiRect.fromPoint(touch.clientX, touch.clientY);
+    if (WikiRect.fromElementContaining(hoverable, point)) {
+      anyTouchEndedOverHoverable = true;
+      break;
+    }
+  }
 
   if (!anyTouchEndedOverHoverable) {
     return;
@@ -738,8 +740,12 @@ export function getTooltipFromHoverablePlacementOpportunityAreas(hoverable) {
   const baselineRects =
     getTooltipBaselineOpportunityAreas(tooltip);
 
+  const basicHoverableRect =
+    WikiRect.fromElementUnderMouse(hoverable) ??
+    WikiRect.fromRect(hoverable.getClientRects()[0]);
+
   const hoverableRect =
-    WikiRect.fromElementUnderMouse(hoverable).toExtended(5, 10);
+    basicHoverableRect.toExtended(5, 10);
 
   const tooltipRect =
     peekTooltipClientRect(tooltip);
@@ -1066,12 +1072,17 @@ export function addPageListeners() {
 
     if (empty(touches)) return;
 
-    const pointIsOverHoverableOrTooltip =
-      pointIsOverAnyOf(getHoverablesAndTooltips());
+    let anyTouchOverAnyHoverableOrTooltip = false;
+    for (const touch of touches) outer: {
+      const point = WikiRect.fromPoint(touch.clientX, touch.clientY);
 
-    const anyTouchOverAnyHoverableOrTooltip =
-      touches.some(({clientX, clientY}) =>
-        pointIsOverHoverableOrTooltip(clientX, clientY));
+      for (const element of getHoverablesAndTooltips()) {
+        if (WikiRect.fromElementContaining(element, point)) {
+          anyTouchOverAnyHoverableOrTooltip = true;
+          break outer;
+        }
+      }
+    }
 
     if (!anyTouchOverAnyHoverableOrTooltip) {
       hideCurrentlyShownTooltip();
@@ -1079,12 +1090,17 @@ export function addPageListeners() {
   });
 
   document.body.addEventListener('click', domEvent => {
-    const {clientX, clientY} = domEvent;
+    const point = WikiRect.fromPoint(domEvent.clientX, domEvent.clientY);
 
-    const pointIsOverHoverableOrTooltip =
-      pointIsOverAnyOf(getHoverablesAndTooltips());
+    let pointIsOverHoverableOrTooltip = false;
+    for (const element of getHoverablesAndTooltips()) {
+      if (WikiRect.fromElementContaining(element, point)) {
+        pointIsOverHoverableOrTooltip = true;
+        break;
+      }
+    }
 
-    if (!pointIsOverHoverableOrTooltip(clientX, clientY)) {
+    if (!pointIsOverHoverableOrTooltip) {
       // Hide with "intent to replace" - we aren't actually going to replace
       // the tooltip with a new one, but this intent indicates that it should
       // be hidden right away, instead of showing. What we're really replacing,
diff --git a/src/static/js/rectangles.js b/src/static/js/rectangles.js
index 24382ef8..428eebba 100644
--- a/src/static/js/rectangles.js
+++ b/src/static/js/rectangles.js
@@ -16,7 +16,10 @@ export class WikiRect extends DOMRect {
 
   static fromMouse() {
     const {clientX, clientY} = liveMousePositionInfo.state;
+    return WikiRect.fromPoint(clientX, clientY);
+  }
 
+  static fromPoint(clientX, clientY) {
     return WikiRect.fromRect({
       x: clientX,
       y: clientY,
@@ -26,20 +29,15 @@ export class WikiRect extends DOMRect {
   }
 
   static fromElementUnderMouse(element) {
-    const mouseRect = WikiRect.fromMouse();
+    return WikiRect.fromElementContaining(element, WikiRect.fromMouse());
+  }
 
+  static fromElementContaining(element, innerRect) {
     const rects =
       Array.from(element.getClientRects())
         .map(rect => WikiRect.fromRect(rect));
 
-    const rectUnderMouse =
-      rects.find(rect => rect.contains(mouseRect));
-
-    if (rectUnderMouse) {
-      return rectUnderMouse;
-    } else {
-      return rects[0];
-    }
+    return rects.find(rect => rect.contains(innerRect));
   }
 
   static leftOf(origin, offset = 0) {