« get me outta code hell

client: make dynamic positioning work Well Enough For Now - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/static
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2024-03-09 19:49:55 -0400
committer(quasar) nebula <qznebula@protonmail.com>2024-03-09 19:49:55 -0400
commit84de0a88adf9f780c4ab76df8fd3ce91d215e3e8 (patch)
treeb9d198cafd40c9690a68995d36fc8078885c811e /src/static
parent02cb2cd0f4262c09aeebbba8c31cd851c1ee9017 (diff)
client: make dynamic positioning work Well Enough For Now
Diffstat (limited to 'src/static')
-rw-r--r--src/static/client3.js169
1 files changed, 154 insertions, 15 deletions
diff --git a/src/static/client3.js b/src/static/client3.js
index 0081ecb0..599a9348 100644
--- a/src/static/client3.js
+++ b/src/static/client3.js
@@ -625,6 +625,26 @@ class WikiRect extends DOMRect {
     return !!this.intersectionWith(rect)?.equals(this);
   }
 
+  fits(rect) {
+    const rectNormalized = WikiRect.fromRect(rect).toNormalized();
+    const thisNormalized = this.toNormalized();
+
+    return (
+      (!isFinite(this.width) || rectNormalized.width <= thisNormalized.width) &&
+      (!isFinite(this.height) || rectNormalized.height <= thisNormalized.height)
+    );
+  }
+
+  fitsWithin(rect) {
+    const rectNormalized = WikiRect.fromRect(rect).toNormalized();
+    const thisNormalized = this.toNormalized();
+
+    return (
+      (!isFinite(rect.width) || thisNormalized.width <= rectNormalized.width) &&
+      (!isFinite(rect.height) || thisNormalized.height <= rectNormalized.height)
+    );
+  }
+
   // Interfacing utilities
 
   static fromRect(rect) {
@@ -1486,7 +1506,7 @@ function peekTooltipClientRect(tooltip) {
     tooltip.querySelector('.tooltip-content');
 
   try {
-    return content.getBoundingClientRect();
+    return WikiRect.fromElement(content);
   } finally {
     cssProp(tooltip, 'display', oldDisplayStyle);
   }
@@ -1511,15 +1531,30 @@ function positionTooltipFromHoverableWithBrains(hoverable) {
   // prefer to keep it positioned naturally, adjusted by CSS
   // instead of JavaScript.
 
-  const {baseline: baselineRect} = opportunities;
+  const {numBaselineRects, idealBaseline: baselineRect} = opportunities;
 
   if (baselineRect.contains(tooltipRect)) {
     return;
   }
 
-  // STUB: Will select an opportunity to apply here.
+  let selectedRect = null;
+  for (let i = 0; i < numBaselineRects; i++) {
+    selectedRect = opportunities.right.down[i];
+    if (selectedRect) break;
+
+    selectedRect = opportunities.left.down[i];
+    if (selectedRect) break;
+
+    selectedRect = opportunities.right.up[i];
+    if (selectedRect) break;
+
+    selectedRect = opportunities.left.up[i];
+    if (selectedRect) break;
+  }
 
-  positionTooltip(tooltip, baselineRect.x, baselineRect.y);
+  selectedRect ??= baselineRect;
+
+  positionTooltip(tooltip, selectedRect.x, selectedRect.y);
 }
 
 function positionTooltip(tooltip, x, y) {
@@ -1552,28 +1587,120 @@ function getTooltipFromHoverablePlacementOpportunityAreas(hoverable) {
   const {state} = hoverableTooltipInfo;
   const {tooltip} = state.registeredHoverables.get(hoverable);
 
-  const baselineRect =
-    getTooltipBaselineOpportunityArea(tooltip);
+  const baselineRects =
+    getTooltipBaselineOpportunityAreas(tooltip);
 
   const hoverableRect =
-    hoverable.getBoundingClientRect();
+    WikiRect.fromElement(hoverable).toExtended(5, 10);
+
+  const tooltipRect =
+    peekTooltipClientRect(tooltip);
+
+  // Get placements relative to the hoverable. Make these available by key,
+  // allowing the caller to choose by preferred orientation. Each value is
+  // an array which corresponds to the baseline areas - placement closer to
+  // front of the array indicates stronger preference. Since not all relative
+  // placements cooperate with all baseline areas, any of these arrays may
+  // include (or be entirely made of) null.
 
-  // STUB: Will compute more opportunities here.
+  const keepIfFits = (rect) =>
+    (rect?.fits(tooltipRect)
+      ? rect
+      : null);
+
+  const prepareRegionRects = (relationalRect) =>
+    baselineRects
+      .map(rect => rect.intersectionWith(relationalRect))
+      .map(keepIfFits);
+
+  const regionRects = {
+    left: prepareRegionRects(WikiRect.leftOf(hoverableRect)),
+    right: prepareRegionRects(WikiRect.rightOf(hoverableRect)),
+    top: prepareRegionRects(WikiRect.above(hoverableRect)),
+    bottom: prepareRegionRects(WikiRect.beneath(hoverableRect)),
+  };
+
+  const neededVerticalOverlap = 30;
+  const neededHorizontalOverlap = 30;
+
+  const prepareVerticalOrientationRects = (regionRects) => {
+    const orientations = {};
+
+    const upTopDown =
+      WikiRect.beneath(
+        hoverableRect.top + neededVerticalOverlap - tooltipRect.height);
+
+    const downBottomUp =
+      WikiRect.above(
+        hoverableRect.bottom - neededVerticalOverlap + tooltipRect.height);
+
+    orientations.up =
+      regionRects
+        .map(rect => rect?.intersectionWith(upTopDown))
+        .map(keepIfFits);
+
+    orientations.down =
+      regionRects
+        .map(rect => rect?.intersectionWith(downBottomUp))
+        .map(rect =>
+          (rect
+            ? rect.intersectionWith(WikiRect.fromRect({
+                x: rect.x,
+                y: rect.bottom - tooltipRect.height,
+                width: rect.width,
+                height: tooltipRect.height,
+              }))
+            : null))
+        .map(keepIfFits);
+
+    const centerRect =
+      WikiRect.fromRect({
+        x: -Infinity, width: Infinity,
+        y: hoverableRect.top
+         + hoverableRect.height / 2
+         - tooltipRect.height / 2,
+        height: tooltipRect.height,
+      });
+
+    orientations.center =
+      regionRects
+        .map(rect => rect?.intersectionWith(centerRect))
+        .map(keepIfFits);
+
+    return orientations;
+  };
+
+  const orientationRects = {
+    left: prepareVerticalOrientationRects(regionRects.left),
+    right: prepareVerticalOrientationRects(regionRects.right),
+  };
 
   return {
-    baseline: baselineRect,
+    numBaselineRects: baselineRects.length,
+    idealBaseline: baselineRects[0],
+    ...orientationRects,
   };
 }
 
-function getTooltipBaselineOpportunityArea(tooltip) {
+function getTooltipBaselineOpportunityAreas(tooltip) {
+  // Returns multiple basic areas in order of preference, with front of the
+  // array representing greater preference.
+
   const {stickyContainers} = stickyHeadingInfo;
+  const results = [];
 
   const windowRect =
-    WikiRect.fromWindow();
+    WikiRect.fromWindow().toInset(10);
 
-  const baselineRect =
+  const workingRect =
     WikiRect.fromRect(windowRect);
 
+  const tooltipRect =
+    peekTooltipClientRect(tooltip);
+
+  // As a baseline, always treat the window rect as fitting the tooltip.
+  results.unshift(WikiRect.fromRect(workingRect));
+
   const containingParent =
     getVisuallyContainingElement(tooltip);
 
@@ -1595,7 +1722,13 @@ function getTooltipBaselineOpportunityArea(tooltip) {
         bottom: padding('bottom'),
       });
 
-    baselineRect.chopExtendingOutside(insetContainingRect);
+    workingRect.chopExtendingOutside(insetContainingRect);
+
+    if (!workingRect.fits(tooltipRect)) {
+      return results;
+    }
+
+    results.unshift(WikiRect.fromRect(workingRect));
   }
 
   // This currently assumes a maximum of one sticky container
@@ -1614,10 +1747,16 @@ function getTooltipBaselineOpportunityArea(tooltip) {
     const beneathStickyContainer =
       WikiRect.beneath(stickyRect, 10);
 
-    baselineRect.chopExtendingOutside(beneathStickyContainer);
+    workingRect.chopExtendingOutside(beneathStickyContainer);
+
+    if (!workingRect.fits(tooltipRect)) {
+      return results;
+    }
+
+    results.unshift(WikiRect.fromRect(workingRect));
   }
 
-  return baselineRect;
+  return results;
 }
 
 function addHoverableTooltipPageListeners() {