@jsenv/dom 0.17.44 → 0.17.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/jsenv_dom.js +1602 -1320
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -4524,15 +4524,31 @@ const waitForPressHeld = (
4524
4524
  }
4525
4525
  cancelPress(pointerEndEvent);
4526
4526
  };
4527
+ // Somebody else settled what this press is. Taking the pointer is how a gesture
4528
+ // says it — and it says it about the same finger this wait is counting on, so
4529
+ // whatever the press turned out to be, it is not a hold. Two waits on one press
4530
+ // is the ordinary case rather than an odd one: an element that can be picked up
4531
+ // AND held answers a finger with two delays, the shorter one wins, and without
4532
+ // this the longer one would answer a hundred milliseconds into the carry.
4533
+ // The listener goes with the countdown, so the gesture THIS wait starts (which
4534
+ // captures the pointer from inside onPressHeld) never reaches it.
4535
+ const onGotPointerCapture = (captureEvent) => {
4536
+ if (captureEvent.pointerId !== pointerId) {
4537
+ return;
4538
+ }
4539
+ cancelPress(captureEvent);
4540
+ };
4527
4541
  // On window rather than on the element: the finger can leave it, and the
4528
4542
  // element itself can be taken out of the document while the press is waiting.
4529
4543
  window.addEventListener("pointermove", onPointerMove);
4530
4544
  window.addEventListener("pointerup", onPointerEnd);
4531
4545
  window.addEventListener("pointercancel", onPointerEnd);
4546
+ window.addEventListener("gotpointercapture", onGotPointerCapture, true);
4532
4547
  countdownCleanupCallbacks.push(() => {
4533
4548
  window.removeEventListener("pointermove", onPointerMove);
4534
4549
  window.removeEventListener("pointerup", onPointerEnd);
4535
4550
  window.removeEventListener("pointercancel", onPointerEnd);
4551
+ window.removeEventListener("gotpointercapture", onGotPointerCapture, true);
4536
4552
  });
4537
4553
 
4538
4554
  onPressStart?.(pressEvent);
@@ -7515,7 +7531,8 @@ const getScrollbarState = (
7515
7531
  * scrollIntoViewScoped avoids this by targeting one container explicitly.
7516
7532
  *
7517
7533
  * Uses scrollTo() so CSS scroll-behavior:smooth on the container is respected.
7518
- * Respects scroll-margin-* on the element.
7534
+ * Respects scroll-margin-* on the element and scroll-padding-* on the
7535
+ * container.
7519
7536
  *
7520
7537
  * @param {Element} el - The element to scroll into view.
7521
7538
  * @param {object} options
@@ -7621,6 +7638,31 @@ const getScrollIntoViewScopedOffsets = (
7621
7638
  containerWidth = containerRect.width;
7622
7639
  }
7623
7640
 
7641
+ // The band the container keeps free at each of its edges for whatever is
7642
+ // scrolled to — what a sticky header, a footer or a fixed bar reserves so an
7643
+ // element does not land underneath it (see navi's safe_area.js). The
7644
+ // scrollport shrinks by it on both axes, and every alignment below is against
7645
+ // that smaller rectangle.
7646
+ const containerStyle = getComputedStyle(container);
7647
+ const scrollPaddingTop = resolveScrollPadding(
7648
+ containerStyle.scrollPaddingTop,
7649
+ containerHeight,
7650
+ );
7651
+ const scrollPaddingBottom = resolveScrollPadding(
7652
+ containerStyle.scrollPaddingBottom,
7653
+ containerHeight,
7654
+ );
7655
+ const scrollPaddingLeft = resolveScrollPadding(
7656
+ containerStyle.scrollPaddingLeft,
7657
+ containerWidth,
7658
+ );
7659
+ const scrollPaddingRight = resolveScrollPadding(
7660
+ containerStyle.scrollPaddingRight,
7661
+ containerWidth,
7662
+ );
7663
+ const viewHeight = containerHeight - scrollPaddingTop - scrollPaddingBottom;
7664
+ const viewWidth = containerWidth - scrollPaddingLeft - scrollPaddingRight;
7665
+
7624
7666
  // Element position relative to the container's scroll origin.
7625
7667
  const elTop = elRect.top - containerTop + currentScrollTop - scrollMarginTop;
7626
7668
  const elBottom = elTop + elRect.height + scrollMarginTop + scrollMarginBottom;
@@ -7628,55 +7670,67 @@ const getScrollIntoViewScopedOffsets = (
7628
7670
  elRect.left - containerLeft + currentScrollLeft - scrollMarginLeft;
7629
7671
  const elRight = elLeft + elRect.width + scrollMarginLeft + scrollMarginRight;
7630
7672
 
7673
+ // The two scroll positions that put the element against one edge of the view
7674
+ // or the other — every alignment below is one of them, or a point between.
7675
+ const scrollTopPuttingElAtViewStart = elTop - scrollPaddingTop;
7676
+ const scrollTopPuttingElAtViewEnd =
7677
+ elBottom - containerHeight + scrollPaddingBottom;
7678
+ const viewStart = currentScrollTop + scrollPaddingTop;
7679
+ const viewEnd = currentScrollTop + containerHeight - scrollPaddingBottom;
7680
+
7631
7681
  let newScrollTop = currentScrollTop;
7632
7682
  if (block === "start") {
7633
- newScrollTop = elTop;
7683
+ newScrollTop = scrollTopPuttingElAtViewStart;
7634
7684
  } else if (block === "end") {
7635
- newScrollTop = elBottom - containerHeight;
7685
+ newScrollTop = scrollTopPuttingElAtViewEnd;
7636
7686
  } else if (block === "center") {
7637
- newScrollTop = elTop + (elRect.height - containerHeight) / 2;
7687
+ newScrollTop = elTop + (elRect.height - viewHeight) / 2 - scrollPaddingTop;
7638
7688
  } else {
7639
7689
  // nearest: scroll only if partially or fully out of view.
7640
- // When the element is taller than the container, only scroll if it is
7690
+ // When the element is taller than the view, only scroll if it is
7641
7691
  // completely out of view — otherwise it is already as visible as possible.
7642
- const scrollBottom = currentScrollTop + containerHeight;
7643
7692
  const elHeight = elBottom - elTop;
7644
- if (elHeight <= containerHeight) {
7645
- if (elTop < currentScrollTop) {
7646
- newScrollTop = elTop;
7647
- } else if (elBottom > scrollBottom) {
7648
- newScrollTop = elBottom - containerHeight;
7693
+ if (elHeight <= viewHeight) {
7694
+ if (elTop < viewStart) {
7695
+ newScrollTop = scrollTopPuttingElAtViewStart;
7696
+ } else if (elBottom > viewEnd) {
7697
+ newScrollTop = scrollTopPuttingElAtViewEnd;
7649
7698
  }
7650
- } else if (elBottom < currentScrollTop) {
7651
- newScrollTop = elBottom - containerHeight;
7652
- } else if (elTop > scrollBottom) {
7653
- newScrollTop = elTop;
7699
+ } else if (elBottom < viewStart) {
7700
+ newScrollTop = scrollTopPuttingElAtViewEnd;
7701
+ } else if (elTop > viewEnd) {
7702
+ newScrollTop = scrollTopPuttingElAtViewStart;
7654
7703
  }
7655
7704
  }
7656
7705
 
7706
+ const scrollLeftPuttingElAtViewStart = elLeft - scrollPaddingLeft;
7707
+ const scrollLeftPuttingElAtViewEnd =
7708
+ elRight - containerWidth + scrollPaddingRight;
7709
+ const viewLeftEdge = currentScrollLeft + scrollPaddingLeft;
7710
+ const viewRightEdge = currentScrollLeft + containerWidth - scrollPaddingRight;
7711
+
7657
7712
  let newScrollLeft = currentScrollLeft;
7658
7713
  if (inline === "start") {
7659
- newScrollLeft = elLeft;
7714
+ newScrollLeft = scrollLeftPuttingElAtViewStart;
7660
7715
  } else if (inline === "end") {
7661
- newScrollLeft = elRight - containerWidth;
7716
+ newScrollLeft = scrollLeftPuttingElAtViewEnd;
7662
7717
  } else if (inline === "center") {
7663
- newScrollLeft = elLeft + (elRect.width - containerWidth) / 2;
7718
+ newScrollLeft = elLeft + (elRect.width - viewWidth) / 2 - scrollPaddingLeft;
7664
7719
  } else {
7665
7720
  // nearest: scroll only if partially or fully out of view.
7666
- // When the element is wider than the container, only scroll if it is
7721
+ // When the element is wider than the view, only scroll if it is
7667
7722
  // completely out of view — otherwise it is already as visible as possible.
7668
- const scrollRight = currentScrollLeft + containerWidth;
7669
7723
  const elWidth = elRight - elLeft;
7670
- if (elWidth <= containerWidth) {
7671
- if (elLeft < currentScrollLeft) {
7672
- newScrollLeft = elLeft;
7673
- } else if (elRight > scrollRight) {
7674
- newScrollLeft = elRight - containerWidth;
7724
+ if (elWidth <= viewWidth) {
7725
+ if (elLeft < viewLeftEdge) {
7726
+ newScrollLeft = scrollLeftPuttingElAtViewStart;
7727
+ } else if (elRight > viewRightEdge) {
7728
+ newScrollLeft = scrollLeftPuttingElAtViewEnd;
7675
7729
  }
7676
- } else if (elRight < currentScrollLeft) {
7677
- newScrollLeft = elRight - containerWidth;
7678
- } else if (elLeft > scrollRight) {
7679
- newScrollLeft = elLeft;
7730
+ } else if (elRight < viewLeftEdge) {
7731
+ newScrollLeft = scrollLeftPuttingElAtViewEnd;
7732
+ } else if (elLeft > viewRightEdge) {
7733
+ newScrollLeft = scrollLeftPuttingElAtViewStart;
7680
7734
  }
7681
7735
  }
7682
7736
 
@@ -7686,6 +7740,20 @@ const getScrollIntoViewScopedOffsets = (
7686
7740
  };
7687
7741
  };
7688
7742
 
7743
+ // "auto" is the one keyword scroll-padding takes, and it means "the browser
7744
+ // picks" — which is 0 everywhere in practice. Percentages resolve against the
7745
+ // scrollport, and getComputedStyle hands them back as written.
7746
+ const resolveScrollPadding = (value, scrollportSize) => {
7747
+ const number = parseFloat(value);
7748
+ if (Number.isNaN(number)) {
7749
+ return 0;
7750
+ }
7751
+ if (value.endsWith("%")) {
7752
+ return (number / 100) * scrollportSize;
7753
+ }
7754
+ return number;
7755
+ };
7756
+
7689
7757
  /**
7690
7758
  * DON'T USE THIS, use scroll-padding-top/bottom in CSS instead
7691
7759
  * better in every aspect
@@ -8487,17 +8555,9 @@ const isolateInteractions = (elements) => {
8487
8555
  };
8488
8556
  };
8489
8557
 
8490
- installImportMetaCssBuild(import.meta);/**
8491
- * Drag Gesture System
8492
- *
8493
- * TODO: rename moveX/moveY en juste x/y
8494
- * puisque move c'est perturbant sachant que c'est drag + scroll
8495
- * et que drag c'est juste la partie mouvement de la souris
8496
- *
8497
- * donc juste x/y ca seras surement mieux
8498
- *
8499
- */
8500
- const css$5 = /* css */`.navi_drag_gesture_backdrop {
8558
+ installImportMetaCssBuild(import.meta);
8559
+
8560
+ const css$5 = /* css */ `.navi_drag_gesture_backdrop {
8501
8561
  touch-action: none;
8502
8562
  user-select: none;
8503
8563
  position: fixed;
@@ -8521,6 +8581,51 @@ import.meta.css = [css$5, "@jsenv/dom/src/interaction/drag/drag_gesture.js"];
8521
8581
  * apart: `lostpointercapture` says the same thing either way.
8522
8582
  */
8523
8583
  const captureHolderByPointerId = new Map();
8584
+
8585
+ /**
8586
+ * The drag loop: it takes the pointer, follows it, and says when it is let go of.
8587
+ * What the movement MEANS is the caller's — this one carries nothing, moves
8588
+ * nothing and commits nothing on its own, which is what makes it the primitive
8589
+ * for a gesture whose product is a value (an angle, a scale, an offset) rather
8590
+ * than an element that ends up somewhere.
8591
+ *
8592
+ * While it runs it owns the document: everything but the dragged element (and the
8593
+ * `[data-droppable]` places) goes inert, a backdrop unifies the cursor and takes
8594
+ * the pointer events, the focus moves to what is held and comes back at the
8595
+ * release, and the selection is refused for the length of the gesture.
8596
+ *
8597
+ * Whether the element may be grabbed AT ALL is decided before the finger lands
8598
+ * and elsewhere: see `markDragSource` in drag_after_intent.js for the
8599
+ * touch-action half, and `dragAfterIntent` for when a press becomes a drag.
8600
+ *
8601
+ * @param {object} [options]
8602
+ * @param {string} [options.name] Carried on `gestureInfo`, for whoever reads a gesture they did not start.
8603
+ * @param {(gestureInfo: object) => void} [options.onGrab]
8604
+ * The pointer went down: there is a gesture, which may still turn out to be a press.
8605
+ * @param {(gestureInfo: object) => void} [options.onDragStart] The threshold was crossed — this one IS a drag.
8606
+ * @param {(gestureInfo: object, someLayoutChange: boolean) => void} [options.onDrag]
8607
+ * Every move, and every scroll of the container: `someLayoutChange` is false when
8608
+ * nothing about the element changed, which still matters to whatever sticks to the scroll.
8609
+ * @param {(gestureInfo: object) => void} [options.onRelease]
8610
+ * The hand let go — or did not: `gestureInfo.cancelled` says the gesture was taken away
8611
+ * (the browser took the touch to scroll with, another gesture took the pointer), and a
8612
+ * release nobody asked for says nothing about where things belong, so nothing read from
8613
+ * it may be committed.
8614
+ * @param {number} [options.threshold=5] How far (px) the pointer travels before the drag starts. `0` starts it at the grab.
8615
+ * @param {{x: boolean, y: boolean}} [options.direction] Which axes the gesture reads.
8616
+ * @param {"auto"|"manual"} [options.documentInteractions="auto"]
8617
+ * `"manual"` leaves the document alone — no inertness, no backdrop, no focus taken. For a
8618
+ * gesture that only measures and hands over (see `dragAfterIntent`).
8619
+ * @param {boolean} [options.backdrop=true]
8620
+ * @param {number} [options.backdropZIndex=999999]
8621
+ * @returns {{grab: function, grabViaPointer: function}}
8622
+ * `grabViaPointer(pointerdownEvent, { element, direction, cursor, scrollContainer })` for a
8623
+ * gesture a hand starts, `grab({ element, grabX, grabY })` for one nothing pressed. Both give
8624
+ * back the gesture — `gestureInfo` (its `layout` with `xDelta`/`yDelta`, its velocity, the way
8625
+ * it is going), `drag`, `release`, and `addDragCallback`/`addReleaseCallback` for whoever joins
8626
+ * after it started. `grabViaPointer` gives back `null` when the press is not one it can read: a
8627
+ * secondary button, a text node.
8628
+ */
8524
8629
  const createDragGestureController = (options = {}) => {
8525
8630
  const {
8526
8631
  name,
@@ -8529,18 +8634,17 @@ const createDragGestureController = (options = {}) => {
8529
8634
  onDrag,
8530
8635
  onRelease,
8531
8636
  threshold = 5,
8532
- direction: defaultDirection = {
8533
- x: true,
8534
- y: true
8535
- },
8637
+ direction: defaultDirection = { x: true, y: true },
8536
8638
  documentInteractions = "auto",
8537
8639
  backdrop = true,
8538
- backdropZIndex = 999999
8640
+ backdropZIndex = 999999,
8539
8641
  } = options;
8642
+
8540
8643
  const dragGestureController = {
8541
8644
  grab: null,
8542
- gravViaPointer: null
8645
+ gravViaPointer: null,
8543
8646
  };
8647
+
8544
8648
  const grab = ({
8545
8649
  element,
8546
8650
  direction = defaultDirection,
@@ -8550,7 +8654,7 @@ const createDragGestureController = (options = {}) => {
8550
8654
  cursor = "grabbing",
8551
8655
  scrollContainer = document.documentElement,
8552
8656
  layoutScrollableLeft: scrollableLeftAtGrab = 0,
8553
- layoutScrollableTop: scrollableTopAtGrab = 0
8657
+ layoutScrollableTop: scrollableTopAtGrab = 0,
8554
8658
  } = {}) => {
8555
8659
  if (!element) {
8556
8660
  throw new Error("element is required");
@@ -8558,6 +8662,7 @@ const createDragGestureController = (options = {}) => {
8558
8662
  if (!direction.x && !direction.y) {
8559
8663
  return null;
8560
8664
  }
8665
+
8561
8666
  const [publishBeforeDrag, addBeforeDragCallback] = createPubSub();
8562
8667
  const [publishDrag, addDragCallback] = createPubSub();
8563
8668
  const [publishRelease, addReleaseCallback] = createPubSub();
@@ -8567,15 +8672,13 @@ const createDragGestureController = (options = {}) => {
8567
8672
  if (onRelease) {
8568
8673
  addReleaseCallback(onRelease);
8569
8674
  }
8675
+
8570
8676
  const scrollLeftAtGrab = scrollContainer.scrollLeft;
8571
8677
  const scrollTopAtGrab = scrollContainer.scrollTop;
8572
8678
  const leftAtGrab = scrollLeftAtGrab + scrollableLeftAtGrab;
8573
8679
  const topAtGrab = scrollTopAtGrab + scrollableTopAtGrab;
8574
8680
  const createLayout = (x, y) => {
8575
- const {
8576
- scrollLeft,
8577
- scrollTop
8578
- } = scrollContainer;
8681
+ const { scrollLeft, scrollTop } = scrollContainer;
8579
8682
  const left = scrollableLeftAtGrab + x;
8580
8683
  const top = scrollableTopAtGrab + y;
8581
8684
  const scrollableLeft = left - scrollLeft;
@@ -8595,30 +8698,33 @@ const createDragGestureController = (options = {}) => {
8595
8698
  top,
8596
8699
  // Delta since grab (number representing how much we dragged)
8597
8700
  xDelta: left - leftAtGrab,
8598
- yDelta: top - topAtGrab
8701
+ yDelta: top - topAtGrab,
8599
8702
  };
8600
8703
  return layoutProps;
8601
8704
  };
8602
- const grabLayout = createLayout(grabX + scrollContainer.scrollLeft, grabY + scrollContainer.scrollTop);
8705
+
8706
+ const grabLayout = createLayout(
8707
+ grabX + scrollContainer.scrollLeft,
8708
+ grabY + scrollContainer.scrollTop,
8709
+ );
8603
8710
  const gestureInfo = {
8604
8711
  name,
8605
8712
  direction,
8606
8713
  started: !threshold,
8607
8714
  status: "grabbed",
8715
+
8608
8716
  element,
8609
8717
  scrollContainer,
8610
- grabX,
8611
- // x grab coordinate (excluding scroll)
8612
- grabY,
8613
- // y grab coordinate (excluding scroll)
8718
+ grabX, // x grab coordinate (excluding scroll)
8719
+ grabY, // y grab coordinate (excluding scroll)
8614
8720
  grabLayout,
8615
8721
  leftAtGrab,
8616
8722
  topAtGrab,
8617
- dragX: grabX,
8618
- // coordinate of the last drag (excluding scroll of the scrollContainer)
8619
- dragY: grabY,
8620
- // coordinate of the last drag (excluding scroll of the scrollContainer)
8723
+
8724
+ dragX: grabX, // coordinate of the last drag (excluding scroll of the scrollContainer)
8725
+ dragY: grabY, // coordinate of the last drag (excluding scroll of the scrollContainer)
8621
8726
  layout: grabLayout,
8727
+
8622
8728
  isGoingUp: undefined,
8623
8729
  isGoingDown: undefined,
8624
8730
  isGoingLeft: undefined,
@@ -8627,11 +8733,13 @@ const createDragGestureController = (options = {}) => {
8627
8733
  intentGoingDown: false,
8628
8734
  intentGoingLeft: false,
8629
8735
  intentGoingRight: false,
8736
+
8630
8737
  // How fast the pointer is going, in px/ms, signed per axis
8631
8738
  // (see measureVelocity)
8632
8739
  velocityX: 0,
8633
8740
  velocityY: 0,
8634
8741
  velocity: 0,
8742
+
8635
8743
  // metadata about interaction sources
8636
8744
  grabEvent: event,
8637
8745
  dragEvent: null,
@@ -8640,7 +8748,7 @@ const createDragGestureController = (options = {}) => {
8640
8748
  // the touch to scroll with, another gesture took the pointer. Whoever
8641
8749
  // reads it must not commit anything — a release that was not asked for
8642
8750
  // says nothing about where things belong.
8643
- cancelled: false
8751
+ cancelled: false,
8644
8752
  };
8645
8753
  definePropertyAsReadOnly(gestureInfo, "name");
8646
8754
  definePropertyAsReadOnly(gestureInfo, "direction");
@@ -8656,6 +8764,7 @@ const createDragGestureController = (options = {}) => {
8656
8764
  // matter of speed, and the gesture is the only place that sees the timing of
8657
8765
  // the events it receives.
8658
8766
  const measureVelocity = createVelocityMeter(grabX, grabY);
8767
+
8659
8768
  document_interactions: {
8660
8769
  if (documentInteractions === "manual") {
8661
8770
  break document_interactions;
@@ -8668,18 +8777,23 @@ const createDragGestureController = (options = {}) => {
8668
8777
  2. Break the visual feedback (inconsistent cursors, hover states)
8669
8778
  3. Cause unwanted scrolling (keyboard shortcuts, wheel events in restricted directions)
8670
8779
  4. Create accessibility issues (focus jumping, screen reader confusion)
8671
- STRATEGY: Create a controlled interaction environment by:
8780
+
8781
+ STRATEGY: Create a controlled interaction environment by:
8672
8782
  1. VISUAL CONTROL: Use a backdrop to unify cursor appearance and block pointer events
8673
8783
  2. INTERACTION ISOLATION: Make non-dragged elements inert to prevent interference
8674
8784
  3. FOCUS MANAGEMENT: Control focus location and prevent focus changes during drag
8675
8785
  4. SELECTIVE SCROLLING: Allow scrolling only in directions supported by the drag gesture
8676
- IMPLEMENTATION:
8786
+
8787
+ IMPLEMENTATION:
8677
8788
  */
8678
8789
 
8679
8790
  // 1. INTERACTION ISOLATION: Make everything except the dragged element inert
8680
8791
  // This prevents keyboard events, pointer interactions, and screen reader navigation
8681
8792
  // on non-relevant elements during the drag operation
8682
- const cleanupInert = isolateInteractions([element, ...Array.from(document.querySelectorAll("[data-droppable]"))]);
8793
+ const cleanupInert = isolateInteractions([
8794
+ element,
8795
+ ...Array.from(document.querySelectorAll("[data-droppable]")),
8796
+ ]);
8683
8797
  addReleaseCallback(() => {
8684
8798
  cleanupInert();
8685
8799
  });
@@ -8696,14 +8810,14 @@ const createDragGestureController = (options = {}) => {
8696
8810
  // Handle wheel events on backdrop for directionally-constrained drag gestures
8697
8811
  // (e.g., table column resize should only allow horizontal scrolling)
8698
8812
  if (!direction.x || !direction.y) {
8699
- backdropElement.onwheel = e => {
8813
+ backdropElement.onwheel = (e) => {
8700
8814
  e.preventDefault();
8701
8815
  const scrollX = direction.x ? e.deltaX : 0;
8702
8816
  const scrollY = direction.y ? e.deltaY : 0;
8703
8817
  scrollContainer.scrollBy({
8704
8818
  left: scrollX,
8705
8819
  top: scrollY,
8706
- behavior: "auto"
8820
+ behavior: "auto",
8707
8821
  });
8708
8822
  };
8709
8823
  }
@@ -8714,9 +8828,7 @@ const createDragGestureController = (options = {}) => {
8714
8828
  }
8715
8829
 
8716
8830
  // 3. FOCUS MANAGEMENT: Control and stabilize focus during drag
8717
- const {
8718
- activeElement
8719
- } = document;
8831
+ const { activeElement } = document;
8720
8832
  const focusableElement = findFocusable(element);
8721
8833
  // Focus the dragged element (or document.body as fallback) to establish clear focus context
8722
8834
  // This also ensure any keydown event listened by the currently focused element
@@ -8724,17 +8836,17 @@ const createDragGestureController = (options = {}) => {
8724
8836
  const elementToFocus = focusableElement || document.body;
8725
8837
  elementToFocus.setAttribute("data-drag-focus", "");
8726
8838
  elementToFocus.focus({
8727
- preventScroll: true
8839
+ preventScroll: true,
8728
8840
  });
8729
8841
  addReleaseCallback(() => {
8730
8842
  elementToFocus.removeAttribute("data-drag-focus");
8731
8843
  // Restore original focus on release
8732
8844
  activeElement.focus({
8733
- preventScroll: true
8845
+ preventScroll: true,
8734
8846
  });
8735
8847
  });
8736
8848
  // Prevent Tab navigation entirely (focus should stay stable)
8737
- const onkeydown = e => {
8849
+ const onkeydown = (e) => {
8738
8850
  if (e.key === "Tab") {
8739
8851
  e.preventDefault();
8740
8852
  return;
@@ -8747,16 +8859,27 @@ const createDragGestureController = (options = {}) => {
8747
8859
 
8748
8860
  // 4. SELECTIVE SCROLLING: Allow keyboard scrolling only in supported directions
8749
8861
  {
8750
- const onDocumentKeydown = keyboardEvent => {
8862
+ const onDocumentKeydown = (keyboardEvent) => {
8751
8863
  // Vertical scrolling keys - prevent if vertical movement not supported
8752
- if (keyboardEvent.key === "ArrowUp" || keyboardEvent.key === "ArrowDown" || keyboardEvent.key === " " || keyboardEvent.key === "PageUp" || keyboardEvent.key === "PageDown" || keyboardEvent.key === "Home" || keyboardEvent.key === "End") {
8864
+ if (
8865
+ keyboardEvent.key === "ArrowUp" ||
8866
+ keyboardEvent.key === "ArrowDown" ||
8867
+ keyboardEvent.key === " " ||
8868
+ keyboardEvent.key === "PageUp" ||
8869
+ keyboardEvent.key === "PageDown" ||
8870
+ keyboardEvent.key === "Home" ||
8871
+ keyboardEvent.key === "End"
8872
+ ) {
8753
8873
  if (!direction.y) {
8754
8874
  keyboardEvent.preventDefault();
8755
8875
  }
8756
8876
  return;
8757
8877
  }
8758
8878
  // Horizontal scrolling keys - prevent if horizontal movement not supported
8759
- if (keyboardEvent.key === "ArrowLeft" || keyboardEvent.key === "ArrowRight") {
8879
+ if (
8880
+ keyboardEvent.key === "ArrowLeft" ||
8881
+ keyboardEvent.key === "ArrowRight"
8882
+ ) {
8760
8883
  if (!direction.x) {
8761
8884
  keyboardEvent.preventDefault();
8762
8885
  }
@@ -8773,38 +8896,36 @@ const createDragGestureController = (options = {}) => {
8773
8896
  // Set up scroll event handling to adjust drag position when scrolling occurs
8774
8897
  {
8775
8898
  let isHandlingScroll = false;
8776
- const handleScroll = scrollEvent => {
8899
+ const handleScroll = (scrollEvent) => {
8777
8900
  if (isHandlingScroll) {
8778
8901
  return;
8779
8902
  }
8780
8903
  isHandlingScroll = true;
8781
- drag(gestureInfo.dragX, gestureInfo.dragY, {
8782
- event: scrollEvent
8783
- });
8904
+ drag(gestureInfo.dragX, gestureInfo.dragY, { event: scrollEvent });
8784
8905
  isHandlingScroll = false;
8785
8906
  };
8786
- const scrollEventReceiver = scrollContainer === document.documentElement ? document : scrollContainer;
8907
+ const scrollEventReceiver =
8908
+ scrollContainer === document.documentElement
8909
+ ? document
8910
+ : scrollContainer;
8787
8911
  scrollEventReceiver.addEventListener("scroll", handleScroll, {
8788
- passive: true
8912
+ passive: true,
8789
8913
  });
8790
8914
  addReleaseCallback(() => {
8791
8915
  scrollEventReceiver.removeEventListener("scroll", handleScroll, {
8792
- passive: true
8916
+ passive: true,
8793
8917
  });
8794
8918
  });
8795
8919
  }
8920
+
8796
8921
  const determineDragData = ({
8797
8922
  dragX,
8798
8923
  dragY,
8799
8924
  dragEvent,
8800
- isRelease = false
8925
+ isRelease = false,
8801
8926
  }) => {
8802
8927
  // === ÉTAT INITIAL (au moment du grab) ===
8803
- const {
8804
- grabX,
8805
- grabY,
8806
- grabLayout
8807
- } = gestureInfo;
8928
+ const { grabX, grabY, grabLayout } = gestureInfo;
8808
8929
  // === CE QUI EST DEMANDÉ (où on veut aller) ===
8809
8930
  // Calcul de la direction basé sur le mouvement précédent
8810
8931
  // (ne tient pas compte du mouvement final une fois les contraintes appliquées)
@@ -8816,38 +8937,56 @@ const createDragGestureController = (options = {}) => {
8816
8937
  const isGoingRight = dragX > currentDragX;
8817
8938
  const isGoingUp = dragY < currentDragY;
8818
8939
  const isGoingDown = dragY > currentDragY;
8819
- const layoutXRequested = direction.x ? scrollContainer.scrollLeft + (dragX - grabX) : grabLayout.scrollLeft;
8820
- const layoutYRequested = direction.y ? scrollContainer.scrollTop + (dragY - grabY) : grabLayout.scrollTop;
8940
+
8941
+ const layoutXRequested = direction.x
8942
+ ? scrollContainer.scrollLeft + (dragX - grabX)
8943
+ : grabLayout.scrollLeft;
8944
+ const layoutYRequested = direction.y
8945
+ ? scrollContainer.scrollTop + (dragY - grabY)
8946
+ : grabLayout.scrollTop;
8821
8947
  const layoutRequested = createLayout(layoutXRequested, layoutYRequested);
8822
8948
  const currentLayout = gestureInfo.layout;
8823
8949
  let layout;
8824
- if (layoutRequested.x === currentLayout.x && layoutRequested.y === currentLayout.y) {
8950
+ if (
8951
+ layoutRequested.x === currentLayout.x &&
8952
+ layoutRequested.y === currentLayout.y
8953
+ ) {
8825
8954
  layout = currentLayout;
8826
8955
  } else {
8827
8956
  // === APPLICATION DES CONTRAINTES ===
8828
8957
  let layoutConstrained = layoutRequested;
8829
8958
  const limitLayout = (left, top) => {
8830
- layoutConstrained = createLayout(left === undefined ? layoutConstrained.x : left - scrollableLeftAtGrab, top === undefined ? layoutConstrained.y : top - scrollableTopAtGrab);
8959
+ layoutConstrained = createLayout(
8960
+ left === undefined
8961
+ ? layoutConstrained.x
8962
+ : left - scrollableLeftAtGrab,
8963
+ top === undefined ? layoutConstrained.y : top - scrollableTopAtGrab,
8964
+ );
8831
8965
  };
8966
+
8832
8967
  publishBeforeDrag(layoutRequested, currentLayout, limitLayout, {
8833
8968
  dragEvent,
8834
- isRelease
8969
+ isRelease,
8835
8970
  });
8836
8971
  // === ÉTAT FINAL ===
8837
8972
  layout = layoutConstrained;
8838
8973
  }
8974
+
8839
8975
  const dragData = {
8840
8976
  dragX,
8841
8977
  dragY,
8842
8978
  layout,
8979
+
8843
8980
  isGoingLeft,
8844
8981
  isGoingRight,
8845
8982
  isGoingUp,
8846
8983
  isGoingDown,
8984
+
8847
8985
  status: isRelease ? "released" : "dragging",
8848
8986
  dragEvent: isRelease ? gestureInfo.dragEvent : dragEvent,
8849
- releaseEvent: isRelease ? dragEvent : null
8987
+ releaseEvent: isRelease ? dragEvent : null,
8850
8988
  };
8989
+
8851
8990
  if (isRelease) {
8852
8991
  return dragData;
8853
8992
  }
@@ -8872,6 +9011,7 @@ const createDragGestureController = (options = {}) => {
8872
9011
  }
8873
9012
  return dragData;
8874
9013
  };
9014
+
8875
9015
  const markAsStarted = () => {
8876
9016
  const clickSuppressionIsOver = suppressClickAfterGesture();
8877
9017
  addReleaseCallback(clickSuppressionIsOver);
@@ -8884,7 +9024,7 @@ const createDragGestureController = (options = {}) => {
8884
9024
  // would then outlive it: what one sees is a click swallowed long after
8885
9025
  // the drag it belonged to.
8886
9026
  dispatchPublicCustomEvent(element, "navi_drag_start", {
8887
- gestureInfo
9027
+ gestureInfo,
8888
9028
  });
8889
9029
  onDragStart?.(gestureInfo);
8890
9030
  };
@@ -8899,19 +9039,18 @@ const createDragGestureController = (options = {}) => {
8899
9039
  gestureInfo.started = true;
8900
9040
  markAsStarted();
8901
9041
  };
8902
- const drag = (dragX = gestureInfo.dragX,
8903
- // Scroll container relative X coordinate
8904
- dragY = gestureInfo.dragY,
8905
- // Scroll container relative Y coordinate
8906
- {
8907
- event = new CustomEvent("programmatic"),
8908
- isRelease = false
8909
- } = {}) => {
9042
+
9043
+ const drag = (
9044
+ dragX = gestureInfo.dragX, // Scroll container relative X coordinate
9045
+ dragY = gestureInfo.dragY, // Scroll container relative Y coordinate
9046
+ { event = new CustomEvent("programmatic"), isRelease = false } = {},
9047
+ ) => {
9048
+
8910
9049
  const dragData = determineDragData({
8911
9050
  dragX,
8912
9051
  dragY,
8913
9052
  dragEvent: event,
8914
- isRelease
9053
+ isRelease,
8915
9054
  });
8916
9055
  const [velocityX, velocityY] = measureVelocity(dragX, dragY);
8917
9056
  const startedPrevious = gestureInfo.started;
@@ -8941,36 +9080,33 @@ const createDragGestureController = (options = {}) => {
8941
9080
  const someLayoutChange = gestureInfo.layout !== layoutPrevious;
8942
9081
  dispatchPublicCustomEvent(element, "navi_drag", {
8943
9082
  gestureInfo,
8944
- someLayoutChange
9083
+ someLayoutChange,
8945
9084
  });
8946
- publishDrag(gestureInfo,
8947
- // we still publish drag event even when unchanged
8948
- // because UI might need to adjust when document scrolls
8949
- // even if nothing truly changes visually the element
8950
- // can decide to stick to the scroll for example
8951
- someLayoutChange);
9085
+ publishDrag(
9086
+ gestureInfo,
9087
+ // we still publish drag event even when unchanged
9088
+ // because UI might need to adjust when document scrolls
9089
+ // even if nothing truly changes visually the element
9090
+ // can decide to stick to the scroll for example
9091
+ someLayoutChange,
9092
+ );
8952
9093
  };
9094
+
8953
9095
  const release = ({
8954
9096
  event = new CustomEvent("programmatic"),
8955
9097
  cancelled = event.type === "pointercancel",
8956
9098
  releaseX = gestureInfo.dragX,
8957
- releaseY = gestureInfo.dragY
9099
+ releaseY = gestureInfo.dragY,
8958
9100
  } = {}) => {
8959
9101
  // Written before the last drag is reported, so the callbacks reading the
8960
9102
  // end of the gesture all see the same one.
8961
9103
  gestureInfo.cancelled = cancelled;
8962
- drag(releaseX, releaseY, {
8963
- event,
8964
- isRelease: true
8965
- });
8966
- dispatchPublicCustomEvent(element, "navi_drag_release", {
8967
- gestureInfo
8968
- });
9104
+ drag(releaseX, releaseY, { event, isRelease: true });
9105
+ dispatchPublicCustomEvent(element, "navi_drag_release", { gestureInfo });
8969
9106
  publishRelease(gestureInfo);
8970
9107
  };
8971
- dispatchPublicCustomEvent(element, "navi_drag_grab", {
8972
- gestureInfo
8973
- });
9108
+
9109
+ dispatchPublicCustomEvent(element, "navi_drag_grab", { gestureInfo });
8974
9110
  onGrab?.(gestureInfo);
8975
9111
  const dragGesture = {
8976
9112
  gestureInfo,
@@ -8979,11 +9115,12 @@ const createDragGestureController = (options = {}) => {
8979
9115
  addReleaseCallback,
8980
9116
  start,
8981
9117
  drag,
8982
- release
9118
+ release,
8983
9119
  };
8984
9120
  return dragGesture;
8985
9121
  };
8986
9122
  dragGestureController.grab = grab;
9123
+
8987
9124
  const initDragByPointer = (grabEvent, dragOptions, initializer) => {
8988
9125
  if (!isPrimaryButtonEvent(grabEvent)) {
8989
9126
  return null;
@@ -8993,11 +9130,8 @@ const createDragGestureController = (options = {}) => {
8993
9130
  // target is a text node
8994
9131
  return null;
8995
9132
  }
8996
- const mouseEventCoords = mouseEvent => {
8997
- const {
8998
- clientX,
8999
- clientY
9000
- } = mouseEvent;
9133
+ const mouseEventCoords = (mouseEvent) => {
9134
+ const { clientX, clientY } = mouseEvent;
9001
9135
  return [clientX, clientY];
9002
9136
  };
9003
9137
  const [grabX, grabY] = mouseEventCoords(grabEvent);
@@ -9005,23 +9139,21 @@ const createDragGestureController = (options = {}) => {
9005
9139
  grabX,
9006
9140
  grabY,
9007
9141
  event: grabEvent,
9008
- ...dragOptions
9142
+ ...dragOptions,
9009
9143
  });
9010
- const dragViaPointer = dragEvent => {
9144
+ const dragViaPointer = (dragEvent) => {
9011
9145
  const [mouseDragX, mouseDragY] = mouseEventCoords(dragEvent);
9012
9146
  dragGesture.drag(mouseDragX, mouseDragY, {
9013
- event: dragEvent
9147
+ event: dragEvent,
9014
9148
  });
9015
9149
  };
9016
- const releaseViaPointer = (pointerEvent, {
9017
- cancelled
9018
- } = {}) => {
9150
+ const releaseViaPointer = (pointerEvent, { cancelled } = {}) => {
9019
9151
  const [mouseReleaseX, mouseReleaseY] = mouseEventCoords(pointerEvent);
9020
9152
  dragGesture.release({
9021
9153
  event: pointerEvent,
9022
9154
  cancelled,
9023
9155
  releaseX: mouseReleaseX,
9024
- releaseY: mouseReleaseY
9156
+ releaseY: mouseReleaseY,
9025
9157
  });
9026
9158
  };
9027
9159
  dragGesture.dragViaPointer = dragViaPointer;
@@ -9040,7 +9172,7 @@ const createDragGestureController = (options = {}) => {
9040
9172
  * everybody: the element would stop being selectable even when nobody is
9041
9173
  * dragging it. Here it is refused for the length of one gesture.
9042
9174
  */
9043
- const preventSelectStart = selectStartEvent => {
9175
+ const preventSelectStart = (selectStartEvent) => {
9044
9176
  selectStartEvent.preventDefault();
9045
9177
  };
9046
9178
  document.addEventListener("selectstart", preventSelectStart);
@@ -9075,7 +9207,7 @@ const createDragGestureController = (options = {}) => {
9075
9207
  * waiting for a click that never comes costs nothing until then.
9076
9208
  */
9077
9209
  removePendingMultiClickListener();
9078
- const onClick = clickEvent => {
9210
+ const onClick = (clickEvent) => {
9079
9211
  const clickCount = clickEvent.detail;
9080
9212
  if (clickCount < 2) {
9081
9213
  return;
@@ -9091,7 +9223,10 @@ const createDragGestureController = (options = {}) => {
9091
9223
  // is doing its own selecting — a second opinion there would only fight
9092
9224
  // it.
9093
9225
  const clickTarget = clickEvent.target;
9094
- if (clickTarget !== grabEvent.target && !clickTarget.contains(grabEvent.target)) {
9226
+ if (
9227
+ clickTarget !== grabEvent.target &&
9228
+ !clickTarget.contains(grabEvent.target)
9229
+ ) {
9095
9230
  return;
9096
9231
  }
9097
9232
  // Text the page says is not selectable stays not selectable: a
@@ -9117,204 +9252,204 @@ const createDragGestureController = (options = {}) => {
9117
9252
  onMove: dragViaPointer,
9118
9253
  onRelease: releaseViaPointer,
9119
9254
  gestureInfo: dragGesture.gestureInfo,
9120
- dragGesture
9255
+ dragGesture,
9121
9256
  });
9122
9257
  dragGesture.addReleaseCallback(() => {
9123
9258
  cleanup();
9124
9259
  });
9125
9260
  return dragGesture;
9126
9261
  };
9262
+
9127
9263
  const grabViaPointer = (grabEvent, options) => {
9128
9264
  if (grabEvent.type === "pointerdown") {
9129
- return initDragByPointer(grabEvent, options, ({
9130
- onMove,
9131
- onRelease,
9132
- gestureInfo,
9133
- dragGesture
9134
- }) => {
9135
- // Captured on something that will still be there at the end of the
9136
- // gesture: the browser releases the capture when its element leaves the
9137
- // document, and a gesture whose own effect replaces the DOM under the
9138
- // finger would lose the pointer at its first move. Callers whose target
9139
- // is stable have nothing to say and keep it.
9140
- const target = options?.pointerCaptureElement || grabEvent.target;
9141
- // The capture is ONE per pointer for the whole document: taking it is
9142
- // taking it away from whoever had it, who is then told the exact same
9143
- // thing it is told when its own gesture ends. So it is taken when this
9144
- // gesture is established and not a moment earlier — for most callers
9145
- // that is the grab itself (the intent was settled before, by a handle
9146
- // or a long press), and for a caller that is still deciding what the
9147
- // press means, it is whenever it says so (see capturePointer).
9148
- let captured = false;
9149
- dragGesture.capturePointer = () => {
9150
- captured = true;
9151
- // Written down before it is taken: this is the only place a capture
9152
- // is ever taken from, so what this map says is who asked for it
9153
- // last — which is what tells a hand-over from a capture the browser
9154
- // dropped on its own (see onCaptureLost).
9155
- captureHolderByPointerId.set(grabEvent.pointerId, dragGesture);
9156
- target.setPointerCapture(grabEvent.pointerId);
9157
- };
9158
- if (!options?.pointerCaptureDeferred) {
9159
- dragGesture.capturePointer();
9160
- }
9161
- /*
9162
- * A touchmove left alone is the browser deciding the touch belongs to
9163
- * it: it takes it to scroll with, and a touch it has taken is a pointer
9164
- * stream it CANCELS — the gesture dies mid-move, the finger is still
9165
- * down, and nothing reads it anymore.
9166
- *
9167
- * Refused only once the gesture is established (a `touch-action: none`
9168
- * would take the touch from everyone who merely brushes past the
9169
- * element), but LISTENED FOR from the grab: whether a touchmove can be
9170
- * refused at all is decided when the touch begins, from the listeners
9171
- * present at that moment. Registered later, the listener is handed
9172
- * events that are already `cancelable: false` — refusing them does
9173
- * nothing, and the reason is invisible in the code that refuses.
9174
- *
9175
- * On the window in capture AND on the grabbed element: a touch keeps
9176
- * being dispatched at the node it started on, and a gesture may take
9177
- * that node out of the document (a page that travels navigates) — from
9178
- * then on the event never passes through the window on its way
9179
- * anywhere.
9180
- */
9181
- const preventTouchScroll = touchMoveEvent => {
9182
- if (gestureInfo.started && touchMoveEvent.cancelable) {
9183
- touchMoveEvent.preventDefault();
9184
- }
9185
- };
9186
- const grabTarget = grabEvent.target;
9187
- window.addEventListener("touchmove", preventTouchScroll, {
9188
- passive: false,
9189
- capture: true
9190
- });
9191
- grabTarget.addEventListener("touchmove", preventTouchScroll, {
9192
- passive: false
9193
- });
9194
- // Only OUR capture ending is this gesture's business:
9195
- // lostpointercapture bubbles, so a descendant giving up its own capture
9196
- // walks straight into this listener. That is not a rare shape — it is
9197
- // exactly what happens when a gesture hands over to another one (a
9198
- // press that becomes a drag releases its intermediate gesture, held on
9199
- // the pressed element, while the real one is being held on a container
9200
- // above it), and taken as our own it kills the new gesture one
9201
- // millisecond after it started.
9202
- //
9203
- // And when it IS ours, it is a loss and never an end: the ends a
9204
- // gesture has are the pointer going up and the pointer being
9205
- // cancelled, both listened for below. What a loss MEANS is the
9206
- // question, and the event does not answer it — two very different
9207
- // things arrive as the same one:
9208
- //
9209
- // - it was HANDED OVER: another gesture took the pointer, or the
9210
- // element it was held on left the document. There is nothing to go
9211
- // on with, and what was being carried must go back rather than land
9212
- // wherever the hand happened to be.
9213
- // - it was simply LET GO OF by the browser, with the hand still down
9214
- // and still moving. It happens, and not rarely: the capture is a
9215
- // guarantee that events keep coming to one element, and the browser
9216
- // drops it for reasons of its own that no code here can see. Killing
9217
- // the gesture for that is dropping an object mid-air — the copy
9218
- // vanishes, the place the hint had lit up is thrown away, and the
9219
- // hand is left having done nothing.
9220
- //
9221
- // They are told apart by who asked (see captureHolderByPointerId): a
9222
- // capture nobody here took, on an element still in the document, was
9223
- // let go of. The gesture does not need it — every move and the release
9224
- // are read at the WINDOW, not at the element — so it goes on.
9225
- const onCaptureLost = pointerEvent => {
9226
- if (!captured || pointerEvent.target !== target) {
9227
- return;
9228
- }
9229
- const handedOver = captureHolderByPointerId.get(grabEvent.pointerId) !== dragGesture;
9230
- if (!handedOver && target.isConnected) {
9231
- // Nobody took it and the element it was held on is still there:
9232
- // the browser let the capture go by itself, which it does — a
9233
- // node moved by a re-render and put straight back, a decision of
9234
- // its own we are not told the reason for. The hand has not let go
9235
- // of anything, so neither does the gesture: it is a guarantee that
9236
- // was lost, not the gesture. Every move and the release are read
9237
- // at the window (see below), so it goes on without it rather than
9238
- // dropping what is still being carried — and the drop the hand was
9239
- // aiming at, which the hint had already lit up, still happens.
9240
- captured = false;
9241
- return;
9265
+ return initDragByPointer(
9266
+ grabEvent,
9267
+ options,
9268
+ ({ onMove, onRelease, gestureInfo, dragGesture }) => {
9269
+ // Captured on something that will still be there at the end of the
9270
+ // gesture: the browser releases the capture when its element leaves the
9271
+ // document, and a gesture whose own effect replaces the DOM under the
9272
+ // finger would lose the pointer at its first move. Callers whose target
9273
+ // is stable have nothing to say and keep it.
9274
+ const target = options?.pointerCaptureElement || grabEvent.target;
9275
+ // The capture is ONE per pointer for the whole document: taking it is
9276
+ // taking it away from whoever had it, who is then told the exact same
9277
+ // thing it is told when its own gesture ends. So it is taken when this
9278
+ // gesture is established and not a moment earlier — for most callers
9279
+ // that is the grab itself (the intent was settled before, by a handle
9280
+ // or a long press), and for a caller that is still deciding what the
9281
+ // press means, it is whenever it says so (see capturePointer).
9282
+ let captured = false;
9283
+ dragGesture.capturePointer = () => {
9284
+ captured = true;
9285
+ // Written down before it is taken: this is the only place a capture
9286
+ // is ever taken from, so what this map says is who asked for it
9287
+ // last — which is what tells a hand-over from a capture the browser
9288
+ // dropped on its own (see onCaptureLost).
9289
+ captureHolderByPointerId.set(grabEvent.pointerId, dragGesture);
9290
+ target.setPointerCapture(grabEvent.pointerId);
9291
+ };
9292
+ if (!options?.pointerCaptureDeferred) {
9293
+ dragGesture.capturePointer();
9242
9294
  }
9243
- onRelease(pointerEvent, {
9244
- cancelled: true
9295
+ /*
9296
+ * A touchmove left alone is the browser deciding the touch belongs to
9297
+ * it: it takes it to scroll with, and a touch it has taken is a pointer
9298
+ * stream it CANCELS — the gesture dies mid-move, the finger is still
9299
+ * down, and nothing reads it anymore.
9300
+ *
9301
+ * Refused only once the gesture is established (a `touch-action: none`
9302
+ * would take the touch from everyone who merely brushes past the
9303
+ * element), but LISTENED FOR from the grab: whether a touchmove can be
9304
+ * refused at all is decided when the touch begins, from the listeners
9305
+ * present at that moment. Registered later, the listener is handed
9306
+ * events that are already `cancelable: false` — refusing them does
9307
+ * nothing, and the reason is invisible in the code that refuses.
9308
+ *
9309
+ * On the window in capture AND on the grabbed element: a touch keeps
9310
+ * being dispatched at the node it started on, and a gesture may take
9311
+ * that node out of the document (a page that travels navigates) — from
9312
+ * then on the event never passes through the window on its way
9313
+ * anywhere.
9314
+ */
9315
+ const preventTouchScroll = (touchMoveEvent) => {
9316
+ if (gestureInfo.started && touchMoveEvent.cancelable) {
9317
+ touchMoveEvent.preventDefault();
9318
+ }
9319
+ };
9320
+ const grabTarget = grabEvent.target;
9321
+ window.addEventListener("touchmove", preventTouchScroll, {
9322
+ passive: false,
9323
+ capture: true,
9245
9324
  });
9246
- };
9247
- target.addEventListener("lostpointercapture", onCaptureLost);
9248
- target.addEventListener("pointercancel", onRelease);
9249
- target.addEventListener("pointerup", onRelease);
9250
- // Read from the window rather than from the element while the pointer
9251
- // is not this gesture's: without a capture a move is delivered to
9252
- // whatever is under the pointer, and a hand that has left the element
9253
- // is exactly the hand this gesture is trying to make sense of. The
9254
- // capture phase reaches the window before anything else, captured or
9255
- // not, so there is one place to read whichever way the gesture ends up.
9256
- const onPointerMove = pointerEvent => {
9257
- if (pointerEvent.pointerId !== grabEvent.pointerId) {
9258
- return;
9259
- }
9260
- onMove(pointerEvent);
9261
- };
9262
- window.addEventListener("pointermove", onPointerMove, true);
9263
- // The end of the pointer is also listened for on the window, because
9264
- // the end is the one event a gesture cannot afford to miss and the
9265
- // element it is captured on is not always on its way: a pointer can
9266
- // be delivered somewhere else entirely (a browser view transition
9267
- // sends presses to the document root), and a cancel dispatched there
9268
- // never passes through this element. Missed, the gesture never ends —
9269
- // whatever it was holding stays held.
9270
- let released = false;
9271
- const onPointerEnd = pointerEvent => {
9272
- if (pointerEvent.pointerId !== grabEvent.pointerId || released) {
9273
- return;
9274
- }
9275
- released = true;
9276
- onRelease(pointerEvent);
9277
- };
9278
- window.addEventListener("pointerup", onPointerEnd, true);
9279
- window.addEventListener("pointercancel", onPointerEnd, true);
9280
- return () => {
9281
- // Listeners first, capture last: giving the pointer back is the
9282
- // one thing here that can throw, and a gesture that fails to clean
9283
- // up half way is worse than one that never cleaned up at all — its
9284
- // listeners stay on the element and answer the NEXT gesture, from
9285
- // a gesture whose pointer is long gone.
9286
- window.removeEventListener("touchmove", preventTouchScroll, {
9287
- capture: true
9325
+ grabTarget.addEventListener("touchmove", preventTouchScroll, {
9326
+ passive: false,
9288
9327
  });
9289
- grabTarget.removeEventListener("touchmove", preventTouchScroll);
9290
- target.removeEventListener("lostpointercapture", onCaptureLost);
9291
- target.removeEventListener("pointercancel", onRelease);
9292
- window.removeEventListener("pointermove", onPointerMove, true);
9293
- target.removeEventListener("pointerup", onRelease);
9294
- window.removeEventListener("pointerup", onPointerEnd, true);
9295
- window.removeEventListener("pointercancel", onPointerEnd, true);
9296
- // Only what this gesture took, and only while there is something to
9297
- // give back: a capture on that element may be someone else's (two
9298
- // gestures reading the same press hold the same node), and a pointer
9299
- // that is up no longer exists — the browser has already dropped the
9300
- // capture with it, and asking again throws ("No active pointer with
9301
- // the given id is found") on the most ordinary release there is.
9302
- if (captureHolderByPointerId.get(grabEvent.pointerId) === dragGesture) {
9303
- captureHolderByPointerId.delete(grabEvent.pointerId);
9304
- }
9305
- if (captured && target.hasPointerCapture(grabEvent.pointerId)) {
9306
- target.releasePointerCapture(grabEvent.pointerId);
9307
- }
9308
- };
9309
- });
9328
+ // Only OUR capture ending is this gesture's business:
9329
+ // lostpointercapture bubbles, so a descendant giving up its own capture
9330
+ // walks straight into this listener. That is not a rare shape — it is
9331
+ // exactly what happens when a gesture hands over to another one (a
9332
+ // press that becomes a drag releases its intermediate gesture, held on
9333
+ // the pressed element, while the real one is being held on a container
9334
+ // above it), and taken as our own it kills the new gesture one
9335
+ // millisecond after it started.
9336
+ //
9337
+ // And when it IS ours, it is a loss and never an end: the ends a
9338
+ // gesture has are the pointer going up and the pointer being
9339
+ // cancelled, both listened for below. What a loss MEANS is the
9340
+ // question, and the event does not answer it — two very different
9341
+ // things arrive as the same one:
9342
+ //
9343
+ // - it was HANDED OVER: another gesture took the pointer, or the
9344
+ // element it was held on left the document. There is nothing to go
9345
+ // on with, and what was being carried must go back rather than land
9346
+ // wherever the hand happened to be.
9347
+ // - it was simply LET GO OF by the browser, with the hand still down
9348
+ // and still moving. It happens, and not rarely: the capture is a
9349
+ // guarantee that events keep coming to one element, and the browser
9350
+ // drops it for reasons of its own that no code here can see. Killing
9351
+ // the gesture for that is dropping an object mid-air — the copy
9352
+ // vanishes, the place the hint had lit up is thrown away, and the
9353
+ // hand is left having done nothing.
9354
+ //
9355
+ // They are told apart by who asked (see captureHolderByPointerId): a
9356
+ // capture nobody here took, on an element still in the document, was
9357
+ // let go of. The gesture does not need it — every move and the release
9358
+ // are read at the WINDOW, not at the element — so it goes on.
9359
+ const onCaptureLost = (pointerEvent) => {
9360
+ if (!captured || pointerEvent.target !== target) {
9361
+ return;
9362
+ }
9363
+ const handedOver =
9364
+ captureHolderByPointerId.get(grabEvent.pointerId) !== dragGesture;
9365
+ if (!handedOver && target.isConnected) {
9366
+ // Nobody took it and the element it was held on is still there:
9367
+ // the browser let the capture go by itself, which it does — a
9368
+ // node moved by a re-render and put straight back, a decision of
9369
+ // its own we are not told the reason for. The hand has not let go
9370
+ // of anything, so neither does the gesture: it is a guarantee that
9371
+ // was lost, not the gesture. Every move and the release are read
9372
+ // at the window (see below), so it goes on without it rather than
9373
+ // dropping what is still being carried — and the drop the hand was
9374
+ // aiming at, which the hint had already lit up, still happens.
9375
+ captured = false;
9376
+ return;
9377
+ }
9378
+ onRelease(pointerEvent, { cancelled: true });
9379
+ };
9380
+ target.addEventListener("lostpointercapture", onCaptureLost);
9381
+ target.addEventListener("pointercancel", onRelease);
9382
+ target.addEventListener("pointerup", onRelease);
9383
+ // Read from the window rather than from the element while the pointer
9384
+ // is not this gesture's: without a capture a move is delivered to
9385
+ // whatever is under the pointer, and a hand that has left the element
9386
+ // is exactly the hand this gesture is trying to make sense of. The
9387
+ // capture phase reaches the window before anything else, captured or
9388
+ // not, so there is one place to read whichever way the gesture ends up.
9389
+ const onPointerMove = (pointerEvent) => {
9390
+ if (pointerEvent.pointerId !== grabEvent.pointerId) {
9391
+ return;
9392
+ }
9393
+ onMove(pointerEvent);
9394
+ };
9395
+ window.addEventListener("pointermove", onPointerMove, true);
9396
+ // The end of the pointer is also listened for on the window, because
9397
+ // the end is the one event a gesture cannot afford to miss and the
9398
+ // element it is captured on is not always on its way: a pointer can
9399
+ // be delivered somewhere else entirely (a browser view transition
9400
+ // sends presses to the document root), and a cancel dispatched there
9401
+ // never passes through this element. Missed, the gesture never ends —
9402
+ // whatever it was holding stays held.
9403
+ let released = false;
9404
+ const onPointerEnd = (pointerEvent) => {
9405
+ if (pointerEvent.pointerId !== grabEvent.pointerId || released) {
9406
+ return;
9407
+ }
9408
+ released = true;
9409
+ onRelease(pointerEvent);
9410
+ };
9411
+ window.addEventListener("pointerup", onPointerEnd, true);
9412
+ window.addEventListener("pointercancel", onPointerEnd, true);
9413
+ return () => {
9414
+ // Listeners first, capture last: giving the pointer back is the
9415
+ // one thing here that can throw, and a gesture that fails to clean
9416
+ // up half way is worse than one that never cleaned up at all — its
9417
+ // listeners stay on the element and answer the NEXT gesture, from
9418
+ // a gesture whose pointer is long gone.
9419
+ window.removeEventListener("touchmove", preventTouchScroll, {
9420
+ capture: true,
9421
+ });
9422
+ grabTarget.removeEventListener("touchmove", preventTouchScroll);
9423
+ target.removeEventListener("lostpointercapture", onCaptureLost);
9424
+ target.removeEventListener("pointercancel", onRelease);
9425
+ window.removeEventListener("pointermove", onPointerMove, true);
9426
+ target.removeEventListener("pointerup", onRelease);
9427
+ window.removeEventListener("pointerup", onPointerEnd, true);
9428
+ window.removeEventListener("pointercancel", onPointerEnd, true);
9429
+ // Only what this gesture took, and only while there is something to
9430
+ // give back: a capture on that element may be someone else's (two
9431
+ // gestures reading the same press hold the same node), and a pointer
9432
+ // that is up no longer exists — the browser has already dropped the
9433
+ // capture with it, and asking again throws ("No active pointer with
9434
+ // the given id is found") on the most ordinary release there is.
9435
+ if (
9436
+ captureHolderByPointerId.get(grabEvent.pointerId) === dragGesture
9437
+ ) {
9438
+ captureHolderByPointerId.delete(grabEvent.pointerId);
9439
+ }
9440
+ if (captured && target.hasPointerCapture(grabEvent.pointerId)) {
9441
+ target.releasePointerCapture(grabEvent.pointerId);
9442
+ }
9443
+ };
9444
+ },
9445
+ );
9310
9446
  }
9311
9447
  if (grabEvent.type === "mousedown") {
9312
- console.warn(`Received "mousedown" event, "pointerdown" events are recommended to perform drag gestures.`);
9313
- return initDragByPointer(grabEvent, options, ({
9314
- onMove,
9315
- onRelease
9316
- }) => {
9317
- const onPointerUp = pointerEvent => {
9448
+ console.warn(
9449
+ `Received "mousedown" event, "pointerdown" events are recommended to perform drag gestures.`,
9450
+ );
9451
+ return initDragByPointer(grabEvent, options, ({ onMove, onRelease }) => {
9452
+ const onPointerUp = (pointerEvent) => {
9318
9453
  // <button disabled> for example does not emit mouseup if we release mouse over it
9319
9454
  // -> we add "pointerup" to catch mouseup occuring on disabled element
9320
9455
  if (pointerEvent.pointerType === "mouse") {
@@ -9331,15 +9466,19 @@ const createDragGestureController = (options = {}) => {
9331
9466
  };
9332
9467
  });
9333
9468
  }
9334
- throw new Error(`Unsupported "${grabEvent.type}" evenet passed to grabViaPointer. "pointerdown" was expected.`);
9469
+ throw new Error(
9470
+ `Unsupported "${grabEvent.type}" evenet passed to grabViaPointer. "pointerdown" was expected.`,
9471
+ );
9335
9472
  };
9336
9473
  dragGestureController.grabViaPointer = grabViaPointer;
9474
+
9337
9475
  return dragGestureController;
9338
9476
  };
9339
9477
 
9340
9478
  // Only the primary button drags: a right click (or any secondary button) opens
9341
9479
  // a context menu, it never grabs anything.
9342
- const isPrimaryButtonEvent = event => event.button === undefined || event.button === 0;
9480
+ const isPrimaryButtonEvent = (event) =>
9481
+ event.button === undefined || event.button === 0;
9343
9482
 
9344
9483
  /*
9345
9484
  * Speed over the last VELOCITY_WINDOW_MS rather than between the last two
@@ -9352,18 +9491,11 @@ const isPrimaryButtonEvent = event => event.button === undefined || event.button
9352
9491
  */
9353
9492
  const VELOCITY_WINDOW_MS = 100;
9354
9493
  const createVelocityMeter = (grabX, grabY) => {
9355
- const samples = [{
9356
- time: performance.now(),
9357
- x: grabX,
9358
- y: grabY
9359
- }];
9494
+ const samples = [{ time: performance.now(), x: grabX, y: grabY }];
9495
+
9360
9496
  const measureVelocity = (x, y) => {
9361
9497
  const time = performance.now();
9362
- samples.push({
9363
- time,
9364
- x,
9365
- y
9366
- });
9498
+ samples.push({ time, x, y });
9367
9499
  while (samples.length > 2 && time - samples[1].time > VELOCITY_WINDOW_MS) {
9368
9500
  samples.shift();
9369
9501
  }
@@ -9374,14 +9506,17 @@ const createVelocityMeter = (grabX, grabY) => {
9374
9506
  }
9375
9507
  return [(x - oldestSample.x) / elapsed, (y - oldestSample.y) / elapsed];
9376
9508
  };
9509
+
9377
9510
  return measureVelocity;
9378
9511
  };
9512
+
9379
9513
  const definePropertyAsReadOnly = (object, propertyName) => {
9380
9514
  Object.defineProperty(object, propertyName, {
9381
9515
  writable: false,
9382
- value: object[propertyName]
9516
+ value: object[propertyName],
9383
9517
  });
9384
9518
  };
9519
+
9385
9520
  const NOOP = () => {};
9386
9521
  let removePendingMultiClickListener = NOOP;
9387
9522
 
@@ -9427,12 +9562,11 @@ const selectParagraphAtPoint = (x, y) => {
9427
9562
  paragraphRange.selectNodeContents(block);
9428
9563
  selection.addRange(paragraphRange);
9429
9564
  };
9430
- const getClosestBlock = node => {
9565
+
9566
+ const getClosestBlock = (node) => {
9431
9567
  let element = node.nodeType === 1 ? node : node.parentElement;
9432
9568
  while (element) {
9433
- const {
9434
- display
9435
- } = window.getComputedStyle(element);
9569
+ const { display } = window.getComputedStyle(element);
9436
9570
  // Everything that is not laid out as a line inside another line: the first
9437
9571
  // ancestor that breaks the flow is the one whose text reads as a paragraph
9438
9572
  // of its own.
@@ -9443,6 +9577,7 @@ const getClosestBlock = node => {
9443
9577
  }
9444
9578
  return null;
9445
9579
  };
9580
+
9446
9581
  const createCaretRange = (x, y) => {
9447
9582
  if (document.caretPositionFromPoint) {
9448
9583
  const caretPosition = document.caretPositionFromPoint(x, y);
@@ -9459,7 +9594,8 @@ const createCaretRange = (x, y) => {
9459
9594
  }
9460
9595
  return null;
9461
9596
  };
9462
- const isSelectable = element => {
9597
+
9598
+ const isSelectable = (element) => {
9463
9599
  if (!element || element.nodeType !== 1) {
9464
9600
  return true;
9465
9601
  }
@@ -9467,6 +9603,7 @@ const isSelectable = element => {
9467
9603
  const userSelect = computedStyle.userSelect || computedStyle.webkitUserSelect;
9468
9604
  return userSelect !== "none";
9469
9605
  };
9606
+
9470
9607
  const collapseSelection = () => {
9471
9608
  const selection = window.getSelection();
9472
9609
  if (selection && !selection.isCollapsed) {
@@ -9474,53 +9611,7 @@ const collapseSelection = () => {
9474
9611
  }
9475
9612
  };
9476
9613
 
9477
- installImportMetaCssBuild(import.meta);/**
9478
- * When a press becomes a drag.
9479
- *
9480
- * A pointer going down on a draggable element is ambiguous — it may be a click,
9481
- * a text selection, a scroll, or a drag — and starting the gesture right away
9482
- * would steal all the others. This module picks which signal resolves the
9483
- * ambiguity for the pointer at hand, and only then hands over to the real
9484
- * gesture.
9485
- *
9486
- * There is one gesture, with a trigger per pointer:
9487
- * - a dedicated handle ([data-drag-handle]) says it outright: drag on contact
9488
- * - a mouse resolves it by distance — a mouse scrolls with its wheel, so travel
9489
- * can only mean drag
9490
- * - a finger resolves it by time — travel is exactly what a scroll looks like,
9491
- * so the only unambiguous signal left is a finger that does NOT move
9492
- *
9493
- * Whichever trigger fired, it has established the intent: the gesture then
9494
- * starts at the first pixel, without a second threshold to cross.
9495
- *
9496
- * WHEN A FINGER MAY TRAVEL TOO: [data-drag-on-contact].
9497
- *
9498
- * The wait a finger is asked for is not a rule about fingers, it is the answer to
9499
- * an ambiguity — travel means scroll as much as it means drag, so the two have to
9500
- * be told apart. Where nothing scrolls the ambiguity does not exist, and the wait
9501
- * is asking the hand to prove something nothing else could have meant: inside a
9502
- * dialog that holds the page still, a finger travelling on a piece can only be
9503
- * carrying it.
9504
- *
9505
- * So the attribute says that place, not that element — put on the dialog, every
9506
- * source inside it reads by distance like a mouse does, at the same few pixels.
9507
- * A tap is left alone by that: a press that goes nowhere is still a press, which
9508
- * is what a piece that is also a link or a card needs.
9509
- *
9510
- * A HOLD is not, and nothing in such a place is text to select. Taking the wait
9511
- * away takes away the only thing that answered a finger held still, so the
9512
- * browser answers it alone: its own long press selects the word under the thumb,
9513
- * and the card the hand meant to carry is left blue and handled. Said to every
9514
- * pointer — a mouse there drags from the first few pixels, so a selection begun
9515
- * there is one that could never be finished either. What says its press is its
9516
- * own keeps its text (see the stylesheet below).
9517
- *
9518
- * It is opt-in and cannot be anything else. Nothing here can see whether the
9519
- * surroundings scroll — a page scrolls by default, an overflow is one CSS
9520
- * property away, and getting it wrong the wrong way means the list runs away
9521
- * under the finger that meant to reorder it. Only the application knows it has
9522
- * taken the scroll away.
9523
- */
9614
+ installImportMetaCssBuild(import.meta);
9524
9615
 
9525
9616
  /* At module scope, and on the markers rather than on the pressed element: both
9526
9617
  rules below have to be true BEFORE the finger lands — a stylesheet, never a
@@ -9541,7 +9632,7 @@ installImportMetaCssBuild(import.meta);/**
9541
9632
  lets the page scroll and still makes the refusal effective — provided the
9542
9633
  listener that will refuse is already known too, which is markDragSource's
9543
9634
  half of the same rule. */
9544
- const css$4 = /* css */`[data-drag-handle], [data-drag-source] {
9635
+ const css$4 = /* css */ `[data-drag-handle], [data-drag-source] {
9545
9636
  -webkit-touch-callout: none;
9546
9637
  }
9547
9638
 
@@ -9632,9 +9723,7 @@ const keepTouchRefusable = () => {
9632
9723
  */
9633
9724
  const markDragSource = (element, axes = "xy") => {
9634
9725
  element.setAttribute("data-drag-source", axes);
9635
- element.addEventListener("touchmove", keepTouchRefusable, {
9636
- passive: false
9637
- });
9726
+ element.addEventListener("touchmove", keepTouchRefusable, { passive: false });
9638
9727
  return () => {
9639
9728
  element.removeAttribute("data-drag-source");
9640
9729
  element.removeEventListener("touchmove", keepTouchRefusable);
@@ -9671,40 +9760,49 @@ const markDragSource = (element, axes = "xy") => {
9671
9760
  * @param {function} [options.onPress]
9672
9761
  * The wait completed and the object is now held (haptics, scale…).
9673
9762
  */
9674
- const dragAfterIntent = (grabEvent, dragGestureInitializer, {
9675
- threshold = 5,
9676
- longPress = "if-touch",
9677
- longPressDelay = 400,
9678
- longPressSlop = 8,
9679
- onPressStart,
9680
- onPressCancel,
9681
- onPress
9682
- } = {}) => {
9763
+ const dragAfterIntent = (
9764
+ grabEvent,
9765
+ dragGestureInitializer,
9766
+ {
9767
+ threshold = 5,
9768
+ longPress = "if-touch",
9769
+ longPressDelay = 400,
9770
+ longPressSlop = 8,
9771
+ onPressStart,
9772
+ onPressCancel,
9773
+ onPress,
9774
+ } = {},
9775
+ ) => {
9683
9776
  if (!isPrimaryButtonEvent(grabEvent)) {
9684
9777
  return;
9685
9778
  }
9686
9779
  const target = grabEvent.target;
9687
- const isDedicatedHandle = target.closest && target.closest("[data-drag-handle]");
9780
+ const isDedicatedHandle =
9781
+ target.closest && target.closest("[data-drag-handle]");
9688
9782
  if (isDedicatedHandle) {
9689
9783
  startDragGesture(dragGestureInitializer);
9690
9784
  return;
9691
9785
  }
9692
- const startsOnLongPress = longPress === true || longPress === "if-touch" && grabEvent.pointerType === "touch" &&
9693
- // The wait tells a scroll from a drag, and here there is no scroll to tell
9694
- // it from — see [data-drag-on-contact] at the top of this file.
9695
- !(target.closest && target.closest("[data-drag-on-contact]"));
9786
+ const startsOnLongPress =
9787
+ longPress === true ||
9788
+ (longPress === "if-touch" &&
9789
+ grabEvent.pointerType === "touch" &&
9790
+ // The wait tells a scroll from a drag, and here there is no scroll to tell
9791
+ // it from — see [data-drag-on-contact] at the top of this file.
9792
+ !(target.closest && target.closest("[data-drag-on-contact]")));
9696
9793
  if (startsOnLongPress) {
9697
9794
  dragAfterLongPress(grabEvent, dragGestureInitializer, {
9698
9795
  longPressDelay,
9699
9796
  longPressSlop,
9700
9797
  onPressStart,
9701
9798
  onPressCancel,
9702
- onPress
9799
+ onPress,
9703
9800
  });
9704
9801
  return;
9705
9802
  }
9706
9803
  dragAfterDistance(grabEvent, dragGestureInitializer, threshold);
9707
9804
  };
9805
+
9708
9806
  const startDragGesture = (dragGestureInitializer, catchUpEvent) => {
9709
9807
  const dragGesture = dragGestureInitializer();
9710
9808
  if (!dragGesture) {
@@ -9718,6 +9816,7 @@ const startDragGesture = (dragGestureInitializer, catchUpEvent) => {
9718
9816
  }
9719
9817
  return dragGesture;
9720
9818
  };
9819
+
9721
9820
  const dragAfterDistance = (grabEvent, dragGestureInitializer, threshold) => {
9722
9821
  const significantDragGestureController = createDragGestureController({
9723
9822
  threshold,
@@ -9725,28 +9824,28 @@ const dragAfterDistance = (grabEvent, dragGestureInitializer, threshold) => {
9725
9824
  // user should still be able to scroll or interact with the document
9726
9825
  // only once the gesture is significant we take control
9727
9826
  documentInteractions: "manual",
9728
- onDragStart: gestureInfo => {
9827
+ onDragStart: (gestureInfo) => {
9729
9828
  significantDragGesture.release(); // kill that gesture
9730
9829
  startDragGesture(dragGestureInitializer, gestureInfo.dragEvent);
9731
- }
9732
- });
9733
- const significantDragGesture = significantDragGestureController.grabViaPointer(grabEvent, {
9734
- element: grabEvent.target,
9735
- // This one owns nothing: it measures a distance to find out whether there
9736
- // is a gesture at all, and it is over the moment there is. Taking the
9737
- // pointer for that would take it from whoever is already holding this same
9738
- // element for a gesture of their own — and giving it back at the threshold
9739
- // would tell them theirs is over.
9740
- pointerCaptureDeferred: true
9830
+ },
9741
9831
  });
9832
+ const significantDragGesture =
9833
+ significantDragGestureController.grabViaPointer(grabEvent, {
9834
+ element: grabEvent.target,
9835
+ // This one owns nothing: it measures a distance to find out whether there
9836
+ // is a gesture at all, and it is over the moment there is. Taking the
9837
+ // pointer for that would take it from whoever is already holding this same
9838
+ // element for a gesture of their own — and giving it back at the threshold
9839
+ // would tell them theirs is over.
9840
+ pointerCaptureDeferred: true,
9841
+ });
9742
9842
  };
9743
- const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
9744
- longPressDelay,
9745
- longPressSlop,
9746
- onPressStart,
9747
- onPressCancel,
9748
- onPress
9749
- }) => {
9843
+
9844
+ const dragAfterLongPress = (
9845
+ grabEvent,
9846
+ dragGestureInitializer,
9847
+ { longPressDelay, longPressSlop, onPressStart, onPressCancel, onPress },
9848
+ ) => {
9750
9849
  /*
9751
9850
  * Nothing is done here to keep the touch refusable: whether it can be refused
9752
9851
  * at all was settled when the finger landed, from what the element already
@@ -9760,9 +9859,7 @@ const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
9760
9859
  slop: longPressSlop,
9761
9860
  onPressStart,
9762
9861
  onPressCancel,
9763
- onPressHeld: (pressEvent, {
9764
- endPress
9765
- }) => {
9862
+ onPressHeld: (pressEvent, { endPress }) => {
9766
9863
  onPress?.(pressEvent);
9767
9864
  const dragGesture = startDragGesture(dragGestureInitializer);
9768
9865
  if (!dragGesture) {
@@ -9772,7 +9869,7 @@ const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
9772
9869
  dragGesture.addReleaseCallback(() => {
9773
9870
  endPress();
9774
9871
  });
9775
- }
9872
+ },
9776
9873
  });
9777
9874
  };
9778
9875
 
@@ -9949,6 +10046,13 @@ const createDragElementPositioner = (
9949
10046
  // returns null (document.documentElement instead — see its own doc).
9950
10047
  const positionedParent = getPositionedParent(elementToMove || element);
9951
10048
  const scrollContainer = getScrollContainer(element);
10049
+ // Whether what MOVES is pinned to the viewport — a copy carried above the page
10050
+ // in the top layer, a fixed panel dragged by its handle. Such a thing does not
10051
+ // travel with any scroll, so the scroll that happens while it is carried is
10052
+ // none of its business (see getScrollOffsets).
10053
+ const elementMovedIsPinnedToViewport = Boolean(
10054
+ findSelfOrAncestorFixedPosition(elementToMove || element),
10055
+ );
9952
10056
  const [getPositionOffsets, getScrollOffsets] = createGetOffsets({
9953
10057
  positionedParent,
9954
10058
  referencePositionedParent: referenceElement
@@ -9958,6 +10062,7 @@ const createDragElementPositioner = (
9958
10062
  referenceScrollContainer: referenceElement
9959
10063
  ? getScrollContainer(referenceElement)
9960
10064
  : scrollContainer,
10065
+ elementMovedIsPinnedToViewport,
9961
10066
  });
9962
10067
 
9963
10068
  {
@@ -10010,6 +10115,7 @@ const createGetOffsets = ({
10010
10115
  referencePositionedParent,
10011
10116
  scrollContainer,
10012
10117
  referenceScrollContainer,
10118
+ elementMovedIsPinnedToViewport,
10013
10119
  }) => {
10014
10120
  const samePositionedParent = positionedParent === referencePositionedParent;
10015
10121
  const getScrollOffsets = createGetScrollOffsets(
@@ -10017,6 +10123,7 @@ const createGetOffsets = ({
10017
10123
  referenceScrollContainer,
10018
10124
  positionedParent,
10019
10125
  samePositionedParent,
10126
+ elementMovedIsPinnedToViewport,
10020
10127
  );
10021
10128
 
10022
10129
  if (samePositionedParent) {
@@ -10221,6 +10328,7 @@ const createGetScrollOffsets = (
10221
10328
  referenceScrollContainer,
10222
10329
  positionedParent,
10223
10330
  samePositionedParent,
10331
+ elementMovedIsPinnedToViewport,
10224
10332
  ) => {
10225
10333
  const getGetScrollOffsetsSameContainer = () => {
10226
10334
  const scrollContainerIsDocument = scrollContainer === documentElement;
@@ -10242,9 +10350,14 @@ const createGetScrollOffsets = (
10242
10350
  return getScrollOffsetsFixed;
10243
10351
  }
10244
10352
  }
10353
+ // A thing pinned to the viewport is inside nothing that scrolls: the page
10354
+ // can run under it and it stays where it is. So it takes the frozen offset
10355
+ // below whatever containment says — and the document, as a scroll container,
10356
+ // answers "yes, it is inside me" to everything.
10245
10357
  const positionedParentIsInsideScrollContainer =
10246
- referenceScrollContainer === documentElement ||
10247
- referenceScrollContainer.contains(positionedParent);
10358
+ !elementMovedIsPinnedToViewport &&
10359
+ (referenceScrollContainer === documentElement ||
10360
+ referenceScrollContainer.contains(positionedParent));
10248
10361
  if (!positionedParentIsInsideScrollContainer) {
10249
10362
  // positionedParent is outside the scroll container (e.g. clone in document.body
10250
10363
  // while tracking an element inside a custom scroll container).
@@ -10292,7 +10405,9 @@ const getDragCoordinates = (
10292
10405
  return [leftRelativeToScrollContainer, topRelativeToScrollContainer];
10293
10406
  };
10294
10407
 
10295
- installImportMetaCssBuild(import.meta);const css$3 = /* css */`.navi_constraint_feedback_line {
10408
+ installImportMetaCssBuild(import.meta);
10409
+
10410
+ const css$3 = /* css */ `.navi_constraint_feedback_line {
10296
10411
  z-index: 9998;
10297
10412
  visibility: hidden;
10298
10413
  transform-origin: 0;
@@ -10306,6 +10421,7 @@ installImportMetaCssBuild(import.meta);const css$3 = /* css */`.navi_constraint_
10306
10421
  visibility: visible;
10307
10422
  }
10308
10423
  `;
10424
+
10309
10425
  const setupConstraintFeedbackLine = () => {
10310
10426
  import.meta.css = [css$3, "@jsenv/dom/src/interaction/drag/constraint_feedback_line.js"];
10311
10427
  const constraintFeedbackLine = createConstraintFeedbackLine();
@@ -10315,17 +10431,18 @@ const setupConstraintFeedbackLine = () => {
10315
10431
  let lastMouseY = null;
10316
10432
 
10317
10433
  // Internal function to update constraint feedback line
10318
- const onDrag = gestureInfo => {
10319
- const {
10320
- grabEvent,
10321
- dragEvent
10322
- } = gestureInfo;
10323
- if (grabEvent.type === "programmatic" ||
10324
- // dragEvent can be null when only mousedown without yet any mousemove
10325
- !dragEvent || dragEvent.type === "programmatic") {
10434
+ const onDrag = (gestureInfo) => {
10435
+ const { grabEvent, dragEvent } = gestureInfo;
10436
+ if (
10437
+ grabEvent.type === "programmatic" ||
10438
+ // dragEvent can be null when only mousedown without yet any mousemove
10439
+ !dragEvent ||
10440
+ dragEvent.type === "programmatic"
10441
+ ) {
10326
10442
  // programmatic drag
10327
10443
  return;
10328
10444
  }
10445
+
10329
10446
  const mouseX = dragEvent.clientX;
10330
10447
  const mouseY = dragEvent.clientY;
10331
10448
  // Use last known position if current position not available (e.g., during scroll)
@@ -10338,6 +10455,7 @@ const setupConstraintFeedbackLine = () => {
10338
10455
  // Store current mouse position for potential use during scroll
10339
10456
  lastMouseX = mouseX;
10340
10457
  lastMouseY = mouseY;
10458
+
10341
10459
  const grabClientX = grabEvent.clientX;
10342
10460
  const grabClientY = grabEvent.clientY;
10343
10461
 
@@ -10366,28 +10484,33 @@ const setupConstraintFeedbackLine = () => {
10366
10484
  constraintFeedbackLine.style.opacity = `${maxOpacity * opacityFactor}`;
10367
10485
  constraintFeedbackLine.setAttribute("data-visible", "");
10368
10486
  };
10487
+
10369
10488
  return {
10370
10489
  onDrag,
10371
10490
  onRelease: () => {
10372
10491
  constraintFeedbackLine.remove();
10373
- }
10492
+ },
10374
10493
  };
10375
10494
  };
10495
+
10376
10496
  const createConstraintFeedbackLine = () => {
10377
10497
  const line = document.createElement("div");
10378
10498
  line.className = "navi_constraint_feedback_line";
10379
- line.title = "Constraint feedback - shows distance between mouse and moving grab point";
10499
+ line.title =
10500
+ "Constraint feedback - shows distance between mouse and moving grab point";
10380
10501
  document.body.appendChild(line);
10381
10502
  return line;
10382
10503
  };
10383
10504
 
10384
- installImportMetaCssBuild(import.meta);// Keep visual markers (debug markers, obstacle markers, constraint feedback line) in DOM after drag ends
10505
+ installImportMetaCssBuild(import.meta);
10385
10506
  const MARKER_SIZE = 12;
10507
+
10386
10508
  let currentDebugMarkers = [];
10387
10509
  let currentConstraintMarkers = [];
10388
10510
  let currentReferenceElementMarker = null;
10389
10511
  let currentElementMarker = null;
10390
- const css$2 = /* css */`.navi_debug_markers_container {
10512
+
10513
+ const css$2 = /* css */ `.navi_debug_markers_container {
10391
10514
  z-index: 999998;
10392
10515
  pointer-events: none;
10393
10516
  --marker-size: ${MARKER_SIZE}px;
@@ -10554,9 +10677,8 @@ const css$2 = /* css */`.navi_debug_markers_container {
10554
10677
  left: 0;
10555
10678
  }
10556
10679
  `;
10557
- const setupDragDebugMarkers = (dragGesture, {
10558
- referenceElement
10559
- }) => {
10680
+
10681
+ const setupDragDebugMarkers = (dragGesture, { referenceElement }) => {
10560
10682
  import.meta.css = [css$2, "@jsenv/dom/src/interaction/drag/drag_debug_markers.js"];
10561
10683
 
10562
10684
  // Clean up any existing persistent markers from previous drag gestures
@@ -10567,27 +10689,29 @@ const setupDragDebugMarkers = (dragGesture, {
10567
10689
  container.innerHTML = ""; // Clear all markers efficiently
10568
10690
  }
10569
10691
  }
10570
- const {
10571
- direction,
10572
- scrollContainer
10573
- } = dragGesture.gestureInfo;
10692
+
10693
+ const { direction, scrollContainer } = dragGesture.gestureInfo;
10694
+
10574
10695
  return {
10575
- onConstraints: (constraints, {
10576
- left,
10577
- top,
10578
- right,
10579
- bottom,
10580
- autoScrollArea
10581
- }) => {
10696
+ onConstraints: (
10697
+ constraints,
10698
+ { left, top, right, bottom, autoScrollArea },
10699
+ ) => {
10582
10700
  // Schedule removal of previous markers if they exist
10583
10701
  const previousDebugMarkers = [...currentDebugMarkers];
10584
10702
  const previousConstraintMarkers = [...currentConstraintMarkers];
10585
10703
  const previousReferenceElementMarker = currentReferenceElementMarker;
10586
10704
  const previousElementMarker = currentElementMarker;
10587
- if (previousDebugMarkers.length > 0 || previousConstraintMarkers.length > 0 || previousReferenceElementMarker || previousElementMarker) {
10705
+
10706
+ if (
10707
+ previousDebugMarkers.length > 0 ||
10708
+ previousConstraintMarkers.length > 0 ||
10709
+ previousReferenceElementMarker ||
10710
+ previousElementMarker
10711
+ ) {
10588
10712
  setTimeout(() => {
10589
- previousDebugMarkers.forEach(marker => marker.remove());
10590
- previousConstraintMarkers.forEach(marker => marker.remove());
10713
+ previousDebugMarkers.forEach((marker) => marker.remove());
10714
+ previousConstraintMarkers.forEach((marker) => marker.remove());
10591
10715
  if (previousReferenceElementMarker) {
10592
10716
  previousReferenceElementMarker.remove();
10593
10717
  }
@@ -10616,7 +10740,7 @@ const setupDragDebugMarkers = (dragGesture, {
10616
10740
  bottom,
10617
10741
  scrollContainer,
10618
10742
  label: elementLabel,
10619
- color: elementColor
10743
+ color: elementColor,
10620
10744
  });
10621
10745
 
10622
10746
  // Create reference element marker if reference element exists
@@ -10626,47 +10750,52 @@ const setupDragDebugMarkers = (dragGesture, {
10626
10750
  top,
10627
10751
  right,
10628
10752
  bottom,
10629
- scrollContainer
10753
+ scrollContainer,
10630
10754
  });
10631
10755
  }
10632
10756
 
10633
10757
  // Collect all markers to be created, then merge duplicates
10634
10758
  const markersToCreate = [];
10759
+
10635
10760
  {
10636
10761
  if (direction.x) {
10637
10762
  markersToCreate.push({
10638
- name: autoScrollArea.paddingLeft ? `autoscroll.left + padding(${autoScrollArea.paddingLeft})` : "autoscroll.left",
10763
+ name: autoScrollArea.paddingLeft
10764
+ ? `autoscroll.left + padding(${autoScrollArea.paddingLeft})`
10765
+ : "autoscroll.left",
10639
10766
  x: autoScrollArea.left,
10640
10767
  y: 0,
10641
- color: "0 128 0",
10642
- // green
10643
- side: "left"
10768
+ color: "0 128 0", // green
10769
+ side: "left",
10644
10770
  });
10645
10771
  markersToCreate.push({
10646
- name: autoScrollArea.paddingRight ? `autoscroll.right + padding(${autoScrollArea.paddingRight})` : "autoscroll.right",
10772
+ name: autoScrollArea.paddingRight
10773
+ ? `autoscroll.right + padding(${autoScrollArea.paddingRight})`
10774
+ : "autoscroll.right",
10647
10775
  x: autoScrollArea.right,
10648
10776
  y: 0,
10649
- color: "0 128 0",
10650
- // green
10651
- side: "right"
10777
+ color: "0 128 0", // green
10778
+ side: "right",
10652
10779
  });
10653
10780
  }
10654
10781
  if (direction.y) {
10655
10782
  markersToCreate.push({
10656
- name: autoScrollArea.paddingTop ? `autoscroll.top + padding(${autoScrollArea.paddingTop})` : "autoscroll.top",
10783
+ name: autoScrollArea.paddingTop
10784
+ ? `autoscroll.top + padding(${autoScrollArea.paddingTop})`
10785
+ : "autoscroll.top",
10657
10786
  x: 0,
10658
10787
  y: autoScrollArea.top,
10659
- color: "255 0 0",
10660
- // red
10661
- side: "top"
10788
+ color: "255 0 0", // red
10789
+ side: "top",
10662
10790
  });
10663
10791
  markersToCreate.push({
10664
- name: autoScrollArea.paddingBottom ? `autoscroll.bottom + padding(${autoScrollArea.paddingBottom})` : "autoscroll.bottom",
10792
+ name: autoScrollArea.paddingBottom
10793
+ ? `autoscroll.bottom + padding(${autoScrollArea.paddingBottom})`
10794
+ : "autoscroll.bottom",
10665
10795
  x: 0,
10666
10796
  y: autoScrollArea.bottom,
10667
- color: "255 165 0",
10668
- // orange
10669
- side: "bottom"
10797
+ color: "255 165 0", // orange
10798
+ side: "bottom",
10670
10799
  });
10671
10800
  }
10672
10801
  }
@@ -10674,9 +10803,7 @@ const setupDragDebugMarkers = (dragGesture, {
10674
10803
  // Process each constraint individually to preserve names
10675
10804
  for (const constraint of constraints) {
10676
10805
  if (constraint.type === "bounds") {
10677
- const {
10678
- bounds
10679
- } = constraint;
10806
+ const { bounds } = constraint;
10680
10807
 
10681
10808
  // Create individual markers for each bound with constraint name
10682
10809
  if (direction.x) {
@@ -10685,9 +10812,8 @@ const setupDragDebugMarkers = (dragGesture, {
10685
10812
  name: `${constraint.name}.left`,
10686
10813
  x: bounds.left,
10687
10814
  y: 0,
10688
- color: "128 0 128",
10689
- // purple
10690
- side: "left"
10815
+ color: "128 0 128", // purple
10816
+ side: "left",
10691
10817
  });
10692
10818
  }
10693
10819
  if (bounds.right !== undefined) {
@@ -10697,9 +10823,8 @@ const setupDragDebugMarkers = (dragGesture, {
10697
10823
  name: `${constraint.name}.right`,
10698
10824
  x: bounds.right,
10699
10825
  y: 0,
10700
- color: "128 0 128",
10701
- // purple
10702
- side: "right"
10826
+ color: "128 0 128", // purple
10827
+ side: "right",
10703
10828
  });
10704
10829
  }
10705
10830
  }
@@ -10709,9 +10834,8 @@ const setupDragDebugMarkers = (dragGesture, {
10709
10834
  name: `${constraint.name}.top`,
10710
10835
  x: 0,
10711
10836
  y: bounds.top,
10712
- color: "128 0 128",
10713
- // purple
10714
- side: "top"
10837
+ color: "128 0 128", // purple
10838
+ side: "top",
10715
10839
  });
10716
10840
  }
10717
10841
  if (bounds.bottom !== undefined) {
@@ -10721,28 +10845,37 @@ const setupDragDebugMarkers = (dragGesture, {
10721
10845
  name: `${constraint.name}.bottom`,
10722
10846
  x: 0,
10723
10847
  y: bounds.bottom,
10724
- color: "128 0 128",
10725
- // purple
10726
- side: "bottom"
10848
+ color: "128 0 128", // purple
10849
+ side: "bottom",
10727
10850
  });
10728
10851
  }
10729
10852
  }
10730
10853
  } else if (constraint.type === "obstacle") {
10731
- const obstacleMarker = createObstacleMarker(constraint, scrollContainer);
10854
+ const obstacleMarker = createObstacleMarker(
10855
+ constraint,
10856
+ scrollContainer,
10857
+ );
10732
10858
  currentConstraintMarkers.push(obstacleMarker);
10733
10859
  }
10734
10860
  }
10735
10861
 
10736
10862
  // Create markers with merging for overlapping positions
10737
- const createdMarkers = createMergedMarkers(markersToCreate, scrollContainer);
10738
- currentDebugMarkers.push(...createdMarkers.filter(m => m.type !== "constraint"));
10739
- currentConstraintMarkers.push(...createdMarkers.filter(m => m.type === "constraint"));
10863
+ const createdMarkers = createMergedMarkers(
10864
+ markersToCreate,
10865
+ scrollContainer,
10866
+ );
10867
+ currentDebugMarkers.push(
10868
+ ...createdMarkers.filter((m) => m.type !== "constraint"),
10869
+ );
10870
+ currentConstraintMarkers.push(
10871
+ ...createdMarkers.filter((m) => m.type === "constraint"),
10872
+ );
10740
10873
  },
10741
10874
  onRelease: () => {
10742
10875
  {
10743
10876
  return;
10744
10877
  }
10745
- }
10878
+ },
10746
10879
  };
10747
10880
  };
10748
10881
 
@@ -10761,9 +10894,8 @@ const getMarkersContainer = () => {
10761
10894
  // Convert document-relative coordinates to viewport coordinates for marker positioning
10762
10895
  // Takes the scroll container into account for proper positioning relative to the container
10763
10896
  const getDebugMarkerPos = (x, y, scrollContainer, side = null) => {
10764
- const {
10765
- documentElement
10766
- } = document;
10897
+ const { documentElement } = document;
10898
+
10767
10899
  const leftWithoutScroll = x - scrollContainer.scrollLeft;
10768
10900
  const topWithoutScroll = y - scrollContainer.scrollTop;
10769
10901
  let baseX;
@@ -10795,6 +10927,7 @@ const getDebugMarkerPos = (x, y, scrollContainer, side = null) => {
10795
10927
  // For obstacles and other markers: use converted coordinates directly
10796
10928
  return [baseX, baseY];
10797
10929
  };
10930
+
10798
10931
  const createMergedMarkers = (markersToCreate, scrollContainer) => {
10799
10932
  const mergedMarkers = [];
10800
10933
  const positionMap = new Map();
@@ -10802,6 +10935,7 @@ const createMergedMarkers = (markersToCreate, scrollContainer) => {
10802
10935
  // Group markers by position and side
10803
10936
  for (const marker of markersToCreate) {
10804
10937
  const key = `${marker.x},${marker.y},${marker.side}`;
10938
+
10805
10939
  if (!positionMap.has(key)) {
10806
10940
  positionMap.set(key, []);
10807
10941
  }
@@ -10819,36 +10953,43 @@ const createMergedMarkers = (markersToCreate, scrollContainer) => {
10819
10953
  } else {
10820
10954
  // Multiple markers at same position - merge labels
10821
10955
  const firstMarker = markers[0];
10822
- const combinedName = markers.map(m => m.name).join(" + ");
10956
+ const combinedName = markers.map((m) => m.name).join(" + ");
10823
10957
 
10824
10958
  // Use the first marker's color, or mix colors if needed
10825
- const domMarker = createDebugMarker({
10826
- ...firstMarker,
10827
- name: combinedName
10828
- }, scrollContainer);
10829
- domMarker.type = markers.some(m => m.name.includes("Bound")) ? "constraint" : "visible";
10959
+ const domMarker = createDebugMarker(
10960
+ {
10961
+ ...firstMarker,
10962
+ name: combinedName,
10963
+ },
10964
+ scrollContainer,
10965
+ );
10966
+ domMarker.type = markers.some((m) => m.name.includes("Bound"))
10967
+ ? "constraint"
10968
+ : "visible";
10830
10969
  mergedMarkers.push(domMarker);
10831
10970
  }
10832
10971
  }
10972
+
10833
10973
  return mergedMarkers;
10834
10974
  };
10835
- const createDebugMarker = ({
10836
- name,
10837
- x,
10838
- y,
10839
- color = "255 0 0",
10840
- side
10841
- }, scrollContainer) => {
10975
+
10976
+ const createDebugMarker = (
10977
+ { name, x, y, color = "255 0 0", side },
10978
+ scrollContainer,
10979
+ ) => {
10842
10980
  // Convert coordinates from document-relative to viewport
10843
10981
  const [viewportX, viewportY] = getDebugMarkerPos(x, y, scrollContainer, side);
10982
+
10844
10983
  const marker = document.createElement("div");
10845
10984
  marker.className = `navi_debug_marker`;
10846
10985
  marker.setAttribute(`data-${side}`, "");
10847
10986
  // Set the color as a CSS custom property
10848
10987
  marker.style.setProperty("--marker-color", `rgb(${color})`);
10849
10988
  // Position markers exactly at the boundary coordinates
10850
- marker.style.left = side === "right" ? `${viewportX - MARKER_SIZE}px` : `${viewportX}px`;
10851
- marker.style.top = side === "bottom" ? `${viewportY - MARKER_SIZE}px` : `${viewportY}px`;
10989
+ marker.style.left =
10990
+ side === "right" ? `${viewportX - MARKER_SIZE}px` : `${viewportX}px`;
10991
+ marker.style.top =
10992
+ side === "bottom" ? `${viewportY - MARKER_SIZE}px` : `${viewportY}px`;
10852
10993
  marker.title = name;
10853
10994
 
10854
10995
  // Add label
@@ -10856,6 +10997,7 @@ const createDebugMarker = ({
10856
10997
  label.className = `navi_debug_marker_label`;
10857
10998
  label.textContent = name;
10858
10999
  marker.appendChild(label);
11000
+
10859
11001
  const container = getMarkersContainer();
10860
11002
  container.appendChild(marker);
10861
11003
  return marker;
@@ -10865,7 +11007,13 @@ const createObstacleMarker = (obstacleObj, scrollContainer) => {
10865
11007
  const height = obstacleObj.bounds.bottom - obstacleObj.bounds.top;
10866
11008
 
10867
11009
  // Convert document-relative coordinates to viewport coordinates
10868
- const [x, y] = getDebugMarkerPos(obstacleObj.bounds.left, obstacleObj.bounds.top, scrollContainer, "obstacle");
11010
+ const [x, y] = getDebugMarkerPos(
11011
+ obstacleObj.bounds.left,
11012
+ obstacleObj.bounds.top,
11013
+ scrollContainer,
11014
+ "obstacle",
11015
+ );
11016
+
10869
11017
  const marker = document.createElement("div");
10870
11018
  marker.className = "navi_obstacle_marker";
10871
11019
  marker.style.left = `${x}px`;
@@ -10879,10 +11027,12 @@ const createObstacleMarker = (obstacleObj, scrollContainer) => {
10879
11027
  label.className = "navi_obstacle_marker_label";
10880
11028
  label.textContent = obstacleObj.name;
10881
11029
  marker.appendChild(label);
11030
+
10882
11031
  const container = getMarkersContainer();
10883
11032
  container.appendChild(marker);
10884
11033
  return marker;
10885
11034
  };
11035
+
10886
11036
  const createElementMarker = ({
10887
11037
  left,
10888
11038
  top,
@@ -10890,12 +11040,13 @@ const createElementMarker = ({
10890
11040
  bottom,
10891
11041
  scrollContainer,
10892
11042
  label = "Element",
10893
- color = "0, 200, 0" // Default green color
11043
+ color = "0, 200, 0", // Default green color
10894
11044
  }) => {
10895
11045
  const width = right - left;
10896
11046
  const height = bottom - top;
10897
11047
  // Convert document-relative coordinates to viewport coordinates
10898
11048
  const [x, y] = getDebugMarkerPos(left, top, scrollContainer, "element");
11049
+
10899
11050
  const marker = document.createElement("div");
10900
11051
  marker.className = "navi_element_marker";
10901
11052
  marker.style.left = `${x}px`;
@@ -10913,21 +11064,24 @@ const createElementMarker = ({
10913
11064
  labelEl.className = "navi_element_marker_label";
10914
11065
  labelEl.textContent = label;
10915
11066
  marker.appendChild(labelEl);
11067
+
10916
11068
  const container = getMarkersContainer();
10917
11069
  container.appendChild(marker);
10918
11070
  return marker;
10919
11071
  };
11072
+
10920
11073
  const createReferenceElementMarker = ({
10921
11074
  left,
10922
11075
  top,
10923
11076
  right,
10924
11077
  bottom,
10925
- scrollContainer
11078
+ scrollContainer,
10926
11079
  }) => {
10927
11080
  const width = right - left;
10928
11081
  const height = bottom - top;
10929
11082
  // Convert document-relative coordinates to viewport coordinates
10930
11083
  const [x, y] = getDebugMarkerPos(left, top, scrollContainer, "reference");
11084
+
10931
11085
  const marker = document.createElement("div");
10932
11086
  marker.className = "navi_reference_element_marker";
10933
11087
  marker.style.left = `${x}px`;
@@ -10941,6 +11095,7 @@ const createReferenceElementMarker = ({
10941
11095
  label.className = "navi_reference_element_marker_label";
10942
11096
  label.textContent = "Reference Element";
10943
11097
  marker.appendChild(label);
11098
+
10944
11099
  const container = getMarkersContainer();
10945
11100
  container.appendChild(marker);
10946
11101
  return marker;
@@ -11847,39 +12002,8 @@ const createStickyFrontierOnAxis = (
11847
12002
  return matchingStickyFrontiers;
11848
12003
  };
11849
12004
 
11850
- installImportMetaCssBuild(import.meta);/**
11851
- * A drag, and what it is FOR.
11852
- *
11853
- * What a hand does is always the same — pick the thing up, carry it, let go — so
11854
- * the gesture is not what distinguishes these. What distinguishes them is the
11855
- * outcome the caller asked for, and that is what `startDragTo` takes:
11856
- *
11857
- * - **move**: it stays where it was put. The element ITSELF travels and keeps the
11858
- * place the hand gave it.
11859
- * - **reorder**: it takes a place in a list. A COPY travels while the original
11860
- * keeps its place in the layout, which is what makes the gesture possible at
11861
- * all — nothing else moves while the hand looks for a place, so there is a
11862
- * stable row of items to look between.
11863
- * - **toss**: it is gotten rid of. The same copy, for the opposite reason: the
11864
- * original stays until the answer says it is really gone.
11865
- * - **land**: it comes down ON something. Also a copy, and the closest to
11866
- * `reorder` — the difference is what a target IS: a row of a list is a place
11867
- * BETWEEN two others, whereas a square of a board is a place of its own, which
11868
- * may already be taken. So nothing is inserted and nothing is a no-op: the
11869
- * answer is "this one came down on that one", and what that means (take the
11870
- * place, swap the two, refuse) is the caller's.
11871
- *
11872
- * The caller lists which outcomes ITS element can answer, and only the machinery
11873
- * those need runs: no copy for a move, no drop hint for something that can only be
11874
- * thrown away, no landing looked for where nothing lands. `reorder` and `toss`
11875
- * combine (dropped on a row, or thrown off the screen); `move` and `reorder` cannot
11876
- * both be true of one release, and the caller is the one who must not ask for both.
11877
- *
11878
- * `createDragToMoveGestureController` below is the layer under all of that — the
11879
- * translation, the auto-scroll, the constraints — and stays usable on its own for
11880
- * anything that is none of the three (a table column being dragged, a sticky
11881
- * frontier being moved).
11882
- */
12005
+ installImportMetaCssBuild(import.meta);
12006
+
11883
12007
  const dragStyleController = createStyleController("drag_to_move");
11884
12008
 
11885
12009
  // How long the copy takes to leave the screen, and to come back. Written into the
@@ -11888,7 +12012,8 @@ const dragStyleController = createStyleController("drag_to_move");
11888
12012
  const TOSS_DURATION_MS = 320;
11889
12013
  // Far enough to be off any screen, in the direction the hand was going.
11890
12014
  const TOSS_DISTANCE = 900;
11891
- const css$1 = /* css */`.navi_drop_hint {
12015
+
12016
+ const css$1 = /* css */ `.navi_drop_hint {
11892
12017
  inset: auto;
11893
12018
  top: var(--drop-hint-y);
11894
12019
  left: calc(var(--drop-target-left) + var(--drop-hint-margin-x, 0px));
@@ -12025,6 +12150,7 @@ const css$1 = /* css */`.navi_drop_hint {
12025
12150
  transform: scale(var(--drag-clone-scale, 1.03));
12026
12151
  transform-origin: var(--drag-origin);
12027
12152
  transition: transform .15s cubic-bezier(.34, 1.56, .64, 1), box-shadow .15s;
12153
+ translate: none !important;
12028
12154
  }
12029
12155
 
12030
12156
  @starting-style {
@@ -12052,99 +12178,16 @@ import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_to.js"];
12052
12178
  // stays a descendant of whatever it is anchored in (a callout next to a button,
12053
12179
  // in the card that carries both), and the press bubbles through the surface as
12054
12180
  // if it had landed on it.
12055
- const DRAG_IGNORED_SELECTOR = '[data-drag-ignore],[data-self-interactions~="drag"],[data-self-interactions~="*"],[popover],dialog';
12181
+ const DRAG_IGNORED_SELECTOR =
12182
+ '[data-drag-ignore],[data-self-interactions~="drag"],[data-self-interactions~="*"],[popover],dialog';
12056
12183
 
12057
12184
  /**
12058
- * Starts a drag-to-reorder interaction on a list item.
12059
- *
12060
- * Handles the full reorder UX:
12061
- * - Activates only once the intent is established — a short movement with a mouse, a long
12062
- * press with a finger (see `dragAfterIntent`), so that neither a click nor a scroll
12063
- * reorders anything by accident.
12064
- * - Clones the grabbed element and moves the clone while the original stays hidden in place
12065
- * (keeps the layout intact so other items don't shift during the drag).
12066
- * - The clone and the drop-hint live in the dragged element's own parent, so the CSS vars
12067
- * that dress them (`--drag-clone-shadow`, `--drop-hint-size`, …) reach them by plain
12068
- * inheritance, and so do the rules the list writes for its items.
12069
- * - Shows a drop-hint line indicating where the item will land.
12070
- * - Drop-target detection is intersection-based: the clone's bounding rect is compared
12071
- * against every item that matches `itemSelector` in the scroll container.
12072
- * - No-ops are filtered: releasing on the grabbed element itself, or in a position that
12073
- * would leave it at exactly the same index, never triggers `onReorder`.
12074
- * - On a valid drop, the clone animates to the drop position via the View Transitions API,
12075
- * `onReorder` is called inside the transition callback so the DOM update and the animation
12076
- * are captured together, then the clone is removed.
12077
- * - On a cancelled drop (pointer released with no valid target), the clone is removed
12078
- * immediately without calling `onReorder`.
12079
- *
12080
- * IDs are used as the bridge between DOM elements and JS state because:
12081
- * - Not all DOM elements matching `itemSelector` may be valid drop targets
12082
- * (holes in the structure), so DOM indices don't reliably map to state indices.
12083
- * - Virtual lists render fewer DOM nodes than the total item count, so
12084
- * DOM-index-based counting would be wrong.
12185
+ * Creates a gesture controller that moves elements via drag.
12085
12186
  *
12086
- * Any option not listed below is forwarded to `createDragToMoveGestureController`
12087
- * (`areaConstraint`, `autoScrollAreaPadding`, `stickyFrontiers`…), except
12088
- * `releasePositionEffect`, always `"manual"` here: what moves is the clone, and it
12089
- * is removed on release, so there is no position to commit or cancel.
12090
- *
12091
- * @param {PointerEvent} event
12092
- * The `pointerdown` event that may become a reorder.
12093
- * @param {object} options
12094
- * @param {Element} [options.draggedElement=event.currentTarget]
12095
- * The list item to drag.
12096
- * @param {Element} [options.containerElement=draggedElement.parentElement]
12097
- * Element searched with `itemSelector` to find the items to drop between.
12098
- * @param {string} [options.itemSelector]
12099
- * CSS selector that matches all list items inside `containerElement`.
12100
- * Used for drop-target detection and no-op filtering. Left out, nothing is a
12101
- * drop target: no hint is drawn and no reorder can be answered — which is what
12102
- * a drag that only ever throws the thing away asks for.
12103
- * @param {function} options.getItemId
12104
- * Returns the stable ID for a given DOM element.
12105
- * Signature: `getItemId(element) → id`.
12106
- * @param {function} options.onReorder
12107
- * Called when the user drops the item in a new position.
12108
- * Signature: `onReorder(fromId, toId, syncCloneWithDropTarget)`.
12109
- * - `fromId`: stable ID of the dragged item.
12110
- * - `toId`: stable ID of the item to insert before, or `null` to append at the end.
12111
- * - `syncCloneWithDropTarget`: call it synchronously inside a
12112
- * `document.startViewTransition` callback, next to the DOM mutation, so the
12113
- * clone is captured at its landing position.
12114
- * @param {(detail: {gestureInfo: object, dropTarget: Element|null}) => "reorder"|"toss"|"cancel"} [options.resolveDrop]
12115
- * What THIS release means, when the answer is not simply "a target was found or
12116
- * not": the same grab can be meant to reorder or to get rid of the thing, and
12117
- * only the caller knows which — far and fast is a throw, over a row is a move.
12118
- * Left out, a drop target reorders and anything else is cancelled.
12119
- * @param {(detail: {gestureInfo: object}) => Promise|void} [options.onToss]
12120
- * The release was a throw. The clone leaves the screen the way it was thrown
12121
- * while this runs; it comes back if the promise rejects, because the thing still
12122
- * exists and the screen has to say so.
12123
- * @param {object} [options.direction={ x: false, y: true }]
12124
- * Axes along which dragging is allowed. Passed to `createDragToMoveGestureController`.
12125
- * @param {number} [options.threshold=5]
12126
- * Distance (px) a mouse must travel before the press becomes a drag.
12127
- * @param {boolean|"if-touch"} [options.longPress="if-touch"]
12128
- * Which pointers start the drag by holding still instead of by travelling.
12129
- * @param {number} [options.longPressDelay=400]
12130
- * How long (ms) such a pointer must stay down.
12131
- * @param {number} [options.longPressSlop=8]
12132
- * How far (px) it may drift during that wait before the press is abandoned.
12133
- * @param {function} [options.onPressStart]
12134
- * The pointer went down and the wait began (a cue that the press counts).
12135
- * @param {function} [options.onPressCancel]
12136
- * The pointer moved or lifted before the wait was over.
12137
- * @param {function} [options.onPress]
12138
- * The wait completed and the item is now held (haptics, scale…).
12139
- */
12140
-
12141
- /**
12142
- * Creates a gesture controller that moves elements via drag.
12143
- *
12144
- * Wraps `createDragGestureController` and adds:
12145
- * - Element translation via CSS transform (translate only; other existing transforms are preserved)
12146
- * - Auto-scroll while dragging near scroll-container edges
12147
- * - Constraints (area boundaries, obstacle elements)
12187
+ * Wraps `createDragGestureController` and adds:
12188
+ * - Element translation via CSS transform (translate only; other existing transforms are preserved)
12189
+ * - Auto-scroll while dragging near scroll-container edges
12190
+ * - Constraints (area boundaries, obstacle elements)
12148
12191
  *
12149
12192
  * The returned controller exposes a `grab(options)` / `grabViaPointer(event, options)` method.
12150
12193
  * Key grab options:
@@ -12213,13 +12256,12 @@ const createDragToMoveGestureController = ({
12213
12256
  cancelAnimationEasing = "ease-out",
12214
12257
  ...options
12215
12258
  } = {}) => {
12216
- const initGrabToMoveElement = (dragGesture, {
12217
- element,
12218
- referenceElement,
12219
- elementToMove,
12220
- convertScrollablePosition
12221
- }) => {
12259
+ const initGrabToMoveElement = (
12260
+ dragGesture,
12261
+ { element, referenceElement, elementToMove, convertScrollablePosition },
12262
+ ) => {
12222
12263
  const scrollContainer = dragGesture.gestureInfo.scrollContainer;
12264
+
12223
12265
  const direction = dragGesture.gestureInfo.direction;
12224
12266
  // elementImpacted is either an externally provided elementToMove (e.g. a drag clone)
12225
12267
  const elementImpacted = elementToMove || element;
@@ -12227,9 +12269,13 @@ const createDragToMoveGestureController = ({
12227
12269
  // (e.g. a drag clone passed by the caller) or the element itself.
12228
12270
  // Capture any pre-existing translate so we can accumulate on top of it
12229
12271
  // rather than resetting it to zero on the first drag event.
12230
- const transformAtGrab = dragStyleController.getUnderlyingValue(elementImpacted, "transform");
12272
+ const transformAtGrab = dragStyleController.getUnderlyingValue(
12273
+ elementImpacted,
12274
+ "transform",
12275
+ );
12231
12276
  const translateXAtGrab = transformAtGrab.translateX;
12232
12277
  const translateYAtGrab = transformAtGrab.translateY;
12278
+
12233
12279
  const cancelPosition = () => {
12234
12280
  dragStyleController.clear(elementImpacted);
12235
12281
  };
@@ -12239,7 +12285,7 @@ const createDragToMoveGestureController = ({
12239
12285
  // animation has only to bridge the two.
12240
12286
  const cancelPositionAnimated = ({
12241
12287
  duration = cancelAnimationDuration,
12242
- easing = cancelAnimationEasing
12288
+ easing = cancelAnimationEasing,
12243
12289
  } = {}) => {
12244
12290
  const transformWhileHeld = getComputedStyle(elementImpacted).transform;
12245
12291
  cancelPosition();
@@ -12249,14 +12295,10 @@ const createDragToMoveGestureController = ({
12249
12295
  }
12250
12296
  // No fill: the element already sits at its resting transform, the
12251
12297
  // animation only replays the way back to it.
12252
- return elementImpacted.animate([{
12253
- transform: transformWhileHeld
12254
- }, {
12255
- transform: transformAtRest
12256
- }], {
12257
- duration,
12258
- easing
12259
- });
12298
+ return elementImpacted.animate(
12299
+ [{ transform: transformWhileHeld }, { transform: transformAtRest }],
12300
+ { duration, easing },
12301
+ );
12260
12302
  };
12261
12303
  const commitPosition = () => {
12262
12304
  dragStyleController.commit(elementImpacted);
@@ -12264,6 +12306,7 @@ const createDragToMoveGestureController = ({
12264
12306
  dragGesture.gestureInfo.cancelPosition = cancelPosition;
12265
12307
  dragGesture.gestureInfo.cancelPositionAnimated = cancelPositionAnimated;
12266
12308
  dragGesture.gestureInfo.commitPosition = commitPosition;
12309
+
12267
12310
  dragGesture.addReleaseCallback(() => {
12268
12311
  if (releasePositionEffect === "cancel") {
12269
12312
  cancelPosition();
@@ -12274,6 +12317,7 @@ const createDragToMoveGestureController = ({
12274
12317
  }
12275
12318
  // "manual": caller handles cleanup, do nothing.
12276
12319
  });
12320
+
12277
12321
  let elementWidth;
12278
12322
  let elementHeight;
12279
12323
  {
@@ -12285,6 +12329,7 @@ const createDragToMoveGestureController = ({
12285
12329
  updateElementDimension();
12286
12330
  dragGesture.addBeforeDragCallback(updateElementDimension);
12287
12331
  }
12332
+
12288
12333
  let scrollArea;
12289
12334
  {
12290
12335
  // Snapshot at grab time so that DOM mutations during dragging
@@ -12293,9 +12338,10 @@ const createDragToMoveGestureController = ({
12293
12338
  left: 0,
12294
12339
  top: 0,
12295
12340
  right: scrollContainer.scrollWidth,
12296
- bottom: scrollContainer.scrollHeight
12341
+ bottom: scrollContainer.scrollHeight,
12297
12342
  };
12298
12343
  }
12344
+
12299
12345
  let scrollport;
12300
12346
  let autoScrollArea;
12301
12347
  {
@@ -12306,11 +12352,14 @@ const createDragToMoveGestureController = ({
12306
12352
  scrollport = getScrollport(scrollBox, scrollContainer);
12307
12353
  autoScrollArea = scrollport;
12308
12354
  if (stickyFrontiers) {
12309
- autoScrollArea = applyStickyFrontiersToAutoScrollArea(autoScrollArea, {
12310
- scrollContainer,
12311
- direction
12312
- // dragGestureName,
12313
- });
12355
+ autoScrollArea = applyStickyFrontiersToAutoScrollArea(
12356
+ autoScrollArea,
12357
+ {
12358
+ scrollContainer,
12359
+ direction,
12360
+ // dragGestureName,
12361
+ },
12362
+ );
12314
12363
  }
12315
12364
  if (autoScrollAreaPadding > 0) {
12316
12365
  autoScrollArea = {
@@ -12321,7 +12370,7 @@ const createDragToMoveGestureController = ({
12321
12370
  left: autoScrollArea.left + autoScrollAreaPadding,
12322
12371
  top: autoScrollArea.top + autoScrollAreaPadding,
12323
12372
  right: autoScrollArea.right - autoScrollAreaPadding,
12324
- bottom: autoScrollArea.bottom - autoScrollAreaPadding
12373
+ bottom: autoScrollArea.bottom - autoScrollAreaPadding,
12325
12374
  };
12326
12375
  }
12327
12376
  };
@@ -12344,77 +12393,97 @@ const createDragToMoveGestureController = ({
12344
12393
  obstacleAttributeName,
12345
12394
  showConstraintFeedbackLine,
12346
12395
  showDebugMarkers,
12347
- referenceElement
12348
- });
12349
- dragGesture.addBeforeDragCallback((layoutRequested, currentLayout, limitLayout, {
12350
- dragEvent
12351
- }) => {
12352
- dragConstraints.applyConstraints(layoutRequested, currentLayout, limitLayout, {
12353
- elementWidth,
12354
- elementHeight,
12355
- scrollArea,
12356
- scrollport,
12357
- hasCrossedScrollportLeftOnce,
12358
- hasCrossedScrollportTopOnce,
12359
- autoScrollArea,
12360
- dragEvent
12361
- });
12396
+ referenceElement,
12362
12397
  });
12363
- const dragToMove = gestureInfo => {
12364
- const {
12365
- isGoingDown,
12366
- isGoingUp,
12367
- isGoingLeft,
12368
- isGoingRight,
12369
- layout
12370
- } = gestureInfo;
12398
+ dragGesture.addBeforeDragCallback(
12399
+ (layoutRequested, currentLayout, limitLayout, { dragEvent }) => {
12400
+ dragConstraints.applyConstraints(
12401
+ layoutRequested,
12402
+ currentLayout,
12403
+ limitLayout,
12404
+ {
12405
+ elementWidth,
12406
+ elementHeight,
12407
+ scrollArea,
12408
+ scrollport,
12409
+ hasCrossedScrollportLeftOnce,
12410
+ hasCrossedScrollportTopOnce,
12411
+ autoScrollArea,
12412
+ dragEvent,
12413
+ },
12414
+ );
12415
+ },
12416
+ );
12417
+
12418
+ const dragToMove = (gestureInfo) => {
12419
+ const { isGoingDown, isGoingUp, isGoingLeft, isGoingRight, layout } =
12420
+ gestureInfo;
12371
12421
  const left = layout.left;
12372
12422
  const top = layout.top;
12373
12423
  const right = left + elementWidth;
12374
12424
  const bottom = top + elementHeight;
12425
+
12375
12426
  {
12376
- hasCrossedScrollportLeftOnce = hasCrossedScrollportLeftOnce || left < scrollport.left;
12377
- hasCrossedScrollportTopOnce = hasCrossedScrollportTopOnce || top < scrollport.top;
12378
- const getScrollMove = axis => {
12427
+ hasCrossedScrollportLeftOnce =
12428
+ hasCrossedScrollportLeftOnce || left < scrollport.left;
12429
+ hasCrossedScrollportTopOnce =
12430
+ hasCrossedScrollportTopOnce || top < scrollport.top;
12431
+
12432
+ const getScrollMove = (axis) => {
12379
12433
  const isGoingPositive = axis === "x" ? isGoingRight : isGoingDown;
12380
12434
  if (isGoingPositive) {
12381
12435
  const elementEnd = axis === "x" ? right : bottom;
12382
- const autoScrollAreaEnd = axis === "x" ? autoScrollArea.right : autoScrollArea.bottom;
12436
+ const autoScrollAreaEnd =
12437
+ axis === "x" ? autoScrollArea.right : autoScrollArea.bottom;
12438
+
12383
12439
  if (elementEnd <= autoScrollAreaEnd) {
12384
12440
  return 0;
12385
12441
  }
12386
12442
  const scrollAmountNeeded = elementEnd - autoScrollAreaEnd;
12387
12443
  return scrollAmountNeeded;
12388
12444
  }
12445
+
12389
12446
  const isGoingNegative = axis === "x" ? isGoingLeft : isGoingUp;
12390
12447
  if (!isGoingNegative) {
12391
12448
  return 0;
12392
12449
  }
12450
+
12393
12451
  const referenceOrEl = referenceElement || element;
12394
- const canAutoScrollNegative = axis === "x" ? !referenceOrEl.hasAttribute("data-sticky-left") || hasCrossedScrollportLeftOnce : !referenceOrEl.hasAttribute("data-sticky-top") || hasCrossedScrollportTopOnce;
12452
+ const canAutoScrollNegative =
12453
+ axis === "x"
12454
+ ? !referenceOrEl.hasAttribute("data-sticky-left") ||
12455
+ hasCrossedScrollportLeftOnce
12456
+ : !referenceOrEl.hasAttribute("data-sticky-top") ||
12457
+ hasCrossedScrollportTopOnce;
12395
12458
  if (!canAutoScrollNegative) {
12396
12459
  return 0;
12397
12460
  }
12461
+
12398
12462
  const elementStart = axis === "x" ? left : top;
12399
- const autoScrollAreaStart = axis === "x" ? autoScrollArea.left : autoScrollArea.top;
12463
+ const autoScrollAreaStart =
12464
+ axis === "x" ? autoScrollArea.left : autoScrollArea.top;
12400
12465
  if (elementStart >= autoScrollAreaStart) {
12401
12466
  return 0;
12402
12467
  }
12468
+
12403
12469
  const scrollAmountNeeded = autoScrollAreaStart - elementStart;
12404
12470
  return -scrollAmountNeeded;
12405
12471
  };
12472
+
12406
12473
  let scrollLeftTarget;
12407
12474
  let scrollTopTarget;
12408
12475
  if (direction.x) {
12409
12476
  const containerScrollLeftMove = getScrollMove("x");
12410
12477
  if (containerScrollLeftMove) {
12411
- scrollLeftTarget = scrollContainer.scrollLeft + containerScrollLeftMove;
12478
+ scrollLeftTarget =
12479
+ scrollContainer.scrollLeft + containerScrollLeftMove;
12412
12480
  }
12413
12481
  }
12414
12482
  if (direction.y) {
12415
12483
  const containerScrollTopMove = getScrollMove("y");
12416
12484
  if (containerScrollTopMove) {
12417
- scrollTopTarget = scrollContainer.scrollTop + containerScrollTopMove;
12485
+ scrollTopTarget =
12486
+ scrollContainer.scrollTop + containerScrollTopMove;
12418
12487
  }
12419
12488
  }
12420
12489
  // now we know what to do, do it
@@ -12425,12 +12494,13 @@ const createDragToMoveGestureController = ({
12425
12494
  scrollContainer.scrollTop = scrollTopTarget;
12426
12495
  }
12427
12496
  }
12497
+
12428
12498
  {
12429
- const {
12499
+ const { scrollableLeft, scrollableTop } = layout;
12500
+ const [positionedLeft, positionedTop] = convertScrollablePosition(
12430
12501
  scrollableLeft,
12431
- scrollableTop
12432
- } = layout;
12433
- const [positionedLeft, positionedTop] = convertScrollablePosition(scrollableLeft, scrollableTop);
12502
+ scrollableTop,
12503
+ );
12434
12504
  // Build the transform to apply, preserving any transforms that were
12435
12505
  // already on the element before the grab (e.g. rotate from another
12436
12506
  // controller), and accumulating from the pre-grab translate baseline.
@@ -12442,32 +12512,33 @@ const createDragToMoveGestureController = ({
12442
12512
  // the distance covered. Dragging moves things on screen, so its translate
12443
12513
  // has to come first, whatever else the element carries. The spread still
12444
12514
  // wins on the value when the element already had a translate of its own.
12445
- const transform = {
12446
- translateX: 0,
12447
- translateY: 0,
12448
- ...transformAtGrab
12449
- };
12515
+ const transform = { translateX: 0, translateY: 0, ...transformAtGrab };
12450
12516
  if (direction.x) {
12451
12517
  const leftTarget = positionedLeft;
12452
12518
  const leftAtGrab = dragGesture.gestureInfo.leftAtGrab;
12453
12519
  const leftDelta = leftTarget - leftAtGrab;
12454
- const translateX = translateXAtGrab ? translateXAtGrab + leftDelta : leftDelta;
12520
+ const translateX = translateXAtGrab
12521
+ ? translateXAtGrab + leftDelta
12522
+ : leftDelta;
12455
12523
  transform.translateX = translateX;
12456
12524
  }
12457
12525
  if (direction.y) {
12458
12526
  const topTarget = positionedTop;
12459
12527
  const topAtGrab = dragGesture.gestureInfo.topAtGrab;
12460
12528
  const topDelta = topTarget - topAtGrab;
12461
- const translateY = translateYAtGrab ? translateYAtGrab + topDelta : topDelta;
12529
+ const translateY = translateYAtGrab
12530
+ ? translateYAtGrab + topDelta
12531
+ : topDelta;
12462
12532
  transform.translateY = translateY;
12463
12533
  }
12464
12534
  dragStyleController.set(elementImpacted, {
12465
- transform
12535
+ transform,
12466
12536
  });
12467
12537
  }
12468
12538
  };
12469
12539
  dragGesture.addDragCallback(dragToMove);
12470
12540
  };
12541
+
12471
12542
  const dragGestureController = createDragGestureController(options);
12472
12543
  const grab = dragGestureController.grab;
12473
12544
  dragGestureController.grab = ({
@@ -12478,23 +12549,28 @@ const createDragToMoveGestureController = ({
12478
12549
  ...rest
12479
12550
  } = {}) => {
12480
12551
  const scrollContainer = getScrollContainer(referenceElement || element);
12481
- const [elementScrollableLeft, elementScrollableTop, convertScrollablePosition] = createDragElementPositioner(element, referenceElement, elementToMove);
12552
+ const [
12553
+ elementScrollableLeft,
12554
+ elementScrollableTop,
12555
+ convertScrollablePosition,
12556
+ ] = createDragElementPositioner(element, referenceElement, elementToMove);
12482
12557
  const dragGesture = grab({
12483
12558
  element,
12484
12559
  scrollContainer,
12485
12560
  layoutScrollableLeft: elementScrollableLeft,
12486
12561
  layoutScrollableTop: elementScrollableTop,
12487
12562
  event,
12488
- ...rest
12563
+ ...rest,
12489
12564
  });
12490
12565
  initGrabToMoveElement(dragGesture, {
12491
12566
  element,
12492
12567
  referenceElement,
12493
12568
  elementToMove,
12494
- convertScrollablePosition
12569
+ convertScrollablePosition,
12495
12570
  });
12496
12571
  return dragGesture;
12497
12572
  };
12573
+
12498
12574
  return dragGestureController;
12499
12575
  };
12500
12576
 
@@ -12513,25 +12589,43 @@ const createDragToMoveGestureController = ({
12513
12589
  * It was put somewhere. The position is already committed when this runs — the
12514
12590
  * hand let go of it there — and travels back if the promise rejects.
12515
12591
  * @param {Element} [options.containerElement=draggedElement.parentElement]
12516
- * Searched with `itemSelector` for the items to drop between.
12592
+ * Where the places are looked for with `itemSelector`, and where the drop hint
12593
+ * is drawn. The parent covers a list and a board, whose items are siblings of
12594
+ * what is carried; anything else has to be said — a palette beside the surface
12595
+ * it fills, a piece drawn INSIDE the place it can be put back on.
12517
12596
  * @param {string} [options.itemSelector] What matches the items of the list.
12518
- * @param {function} [options.getItemId] `getItemId(element) → id`.
12597
+ * Left out, nothing is a place: no hint is drawn and no landing can be
12598
+ * answered, which is what a drag that only ever throws the thing away asks for.
12599
+ * @param {function} [options.getItemId] `getItemId(element) → id`. An id rather
12600
+ * than a DOM index, because the two do not match: a list draws fewer rows than
12601
+ * it has, a search reorders them, and the structure may have holes.
12519
12602
  * @param {function} [options.onReorder]
12520
12603
  * `onReorder(fromId, toId, syncCloneWithDropTarget)` — see its own note below.
12521
12604
  * @param {(detail: {gestureInfo: object}) => Promise|void} [options.onToss]
12522
12605
  * It was thrown away. The copy leaves the screen while this runs and comes back
12523
12606
  * if the promise rejects, because the thing still exists and the screen has to
12524
12607
  * say so.
12525
- * @param {function} [options.onLand]
12526
- * `onLand(fromId, toId, syncCloneWithDropTarget)` — it came down on `toId`, which
12527
- * is an element and never null: nothing under the copy is a cancelled release.
12528
- * The copy is held until what comes back settles, exactly like `onReorder`.
12529
- * `syncCloneWithDropTarget` takes an element when the place is not the shape of
12530
- * what stands on it: the copy then takes THAT box instead of the target's.
12608
+ * @param {(detail: {fromId: string, toId: string, x: number, y: number, width: number, height: number, syncCloneWithDropTarget: Function}) => Promise|void} [options.onLand]
12609
+ * It came down on `toId`, which is an element and never null: nothing under the
12610
+ * copy is a cancelled release. `x`/`y`/`width`/`height` say WHERE on it, which
12611
+ * is the whole answer when the place is a surface — a plan, a map — with no
12612
+ * element under the copy to name. The copy is held until what comes back
12613
+ * settles, exactly like `onReorder`. `syncCloneWithDropTarget` takes an element
12614
+ * when the place is not the shape of what stands on it: the copy then takes
12615
+ * THAT box instead of the target's.
12531
12616
  * @param {number} [options.tossDistance=110] How far a throw goes, in px.
12532
12617
  * @param {number} [options.tossSpeed=0.45] And how fast, in px/ms. BOTH are asked
12533
12618
  * for: one without the other is moving the thing while hesitating, and nothing is
12534
12619
  * thrown away on a hesitation.
12620
+ * @param {("throw"|"release-outside")[]} [options.tossBy=["throw"]] How a toss
12621
+ * is made here. A THROW is far and fast — the flick that gets rid of something,
12622
+ * a list's gesture, judged before any landing. A RELEASE OUTSIDE has no place
12623
+ * under it — dragging something off the surface it belongs to and letting go,
12624
+ * deliberate and never a flick, a surface's gesture. Each is asked for on its
12625
+ * own: a surface wants only the second (a fast drag across it that ends ON it
12626
+ * has not asked for the thing to go), a list only the first (a row let go of
12627
+ * beside its list is a row put back). With no place to be outside of (no
12628
+ * `itemSelector`), every release is outside.
12535
12629
  *
12536
12630
  * Everything else is forwarded to `createDragToMoveGestureController`
12537
12631
  * (`areaConstraint`, `autoScrollAreaPadding`, `direction`…) and to `dragAfterIntent`
@@ -12547,10 +12641,11 @@ const createDragToMoveGestureController = ({
12547
12641
  * The gesture holds its copy until what `onReorder` returns settles, so returning
12548
12642
  * the transition is what makes the landing continuous.
12549
12643
  */
12550
- const startDragTo = (event, effects, {
12551
- draggedElement = event.currentTarget,
12552
- ...options
12553
- } = {}) => {
12644
+ const startDragTo = (
12645
+ event,
12646
+ effects,
12647
+ { draggedElement = event.currentTarget, ...options } = {},
12648
+ ) => {
12554
12649
  // An area that opted out of dragging (a text one wants to select, a control that
12555
12650
  // owns the gesture, a callout): the press there is none of our business.
12556
12651
  if (isPressIgnored(event.target, draggedElement)) {
@@ -12569,13 +12664,10 @@ const startDragTo = (event, effects, {
12569
12664
  canReorder,
12570
12665
  canToss,
12571
12666
  canLand,
12572
- ...options
12667
+ ...options,
12573
12668
  });
12574
12669
  }
12575
- return startDragToMoveElement(event, {
12576
- draggedElement,
12577
- ...options
12578
- });
12670
+ return startDragToMoveElement(event, { draggedElement, ...options });
12579
12671
  };
12580
12672
 
12581
12673
  /**
@@ -12584,70 +12676,73 @@ const startDragTo = (event, effects, {
12584
12676
  * No copy, unlike the two others: what is being moved is the thing and not a
12585
12677
  * stand-in for it, so there is nothing to put back and nothing to reveal.
12586
12678
  */
12587
- const startDragToMoveElement = (event, {
12588
- draggedElement,
12589
- onMove,
12590
- threshold,
12591
- longPress,
12592
- longPressDelay,
12593
- longPressSlop,
12594
- onPressStart,
12595
- onPressCancel,
12596
- onPress,
12597
- ...options
12598
- }) => {
12599
- event.preventDefault();
12600
- return dragAfterIntent(event, () => {
12601
- const gestureController = createDragToMoveGestureController({
12602
- releasePositionEffect: "manual",
12603
- ...options
12604
- });
12605
- const dragGesture = gestureController.grabViaPointer(event, {
12606
- element: draggedElement
12607
- });
12608
- if (!dragGesture) {
12609
- return null;
12610
- }
12611
- dragGesture.addReleaseCallback(async gestureInfo => {
12612
- const {
12613
- xDelta,
12614
- yDelta
12615
- } = gestureInfo.layout;
12616
- if (!xDelta && !yDelta) {
12617
- // Picked up and put back down: nothing moved, so nobody is told.
12618
- gestureInfo.cancelPosition();
12619
- return;
12620
- }
12621
- // Committed before the answer rather than after: the hand let go of it
12622
- // there, and a thing that snaps home while a request is in flight says the
12623
- // gesture was not understood.
12624
- gestureInfo.commitPosition();
12625
- try {
12626
- await onMove?.({
12627
- gestureInfo,
12628
- x: xDelta,
12629
- y: yDelta
12630
- });
12631
- } catch {
12632
- gestureInfo.cancelPositionAnimated();
12633
- }
12634
- });
12635
- return dragGesture;
12636
- }, {
12679
+ const startDragToMoveElement = (
12680
+ event,
12681
+ {
12682
+ draggedElement,
12683
+ onMove,
12637
12684
  threshold,
12638
12685
  longPress,
12639
12686
  longPressDelay,
12640
12687
  longPressSlop,
12641
12688
  onPressStart,
12642
12689
  onPressCancel,
12643
- onPress
12644
- });
12690
+ onPress,
12691
+ ...options
12692
+ },
12693
+ ) => {
12694
+ event.preventDefault();
12695
+ return dragAfterIntent(
12696
+ event,
12697
+ () => {
12698
+ const gestureController = createDragToMoveGestureController({
12699
+ releasePositionEffect: "manual",
12700
+ ...options,
12701
+ });
12702
+ const dragGesture = gestureController.grabViaPointer(event, {
12703
+ element: draggedElement,
12704
+ });
12705
+ if (!dragGesture) {
12706
+ return null;
12707
+ }
12708
+ dragGesture.addReleaseCallback(async (gestureInfo) => {
12709
+ const { xDelta, yDelta } = gestureInfo.layout;
12710
+ if (!xDelta && !yDelta) {
12711
+ // Picked up and put back down: nothing moved, so nobody is told.
12712
+ gestureInfo.cancelPosition();
12713
+ return;
12714
+ }
12715
+ // Committed before the answer rather than after: the hand let go of it
12716
+ // there, and a thing that snaps home while a request is in flight says the
12717
+ // gesture was not understood.
12718
+ gestureInfo.commitPosition();
12719
+ try {
12720
+ await onMove?.({ gestureInfo, x: xDelta, y: yDelta });
12721
+ } catch {
12722
+ gestureInfo.cancelPositionAnimated();
12723
+ }
12724
+ });
12725
+ return dragGesture;
12726
+ },
12727
+ {
12728
+ threshold,
12729
+ longPress,
12730
+ longPressDelay,
12731
+ longPressSlop,
12732
+ onPressStart,
12733
+ onPressCancel,
12734
+ onPress,
12735
+ },
12736
+ );
12645
12737
  };
12646
12738
 
12647
12739
  // Far and fast, both at once: one without the other is moving the thing while
12648
12740
  // hesitating, and nothing is thrown away on a hesitation — it comes back.
12649
12741
  const TOSS_DISTANCE_TO_COMMIT = 110;
12650
12742
  const TOSS_SPEED_TO_COMMIT = 0.45;
12743
+ // The flick is how a list gets rid of a row; a surface says otherwise.
12744
+ const TOSS_BY_DEFAULT = ["throw"];
12745
+
12651
12746
  const resolveDropMeaning = ({
12652
12747
  gestureInfo,
12653
12748
  hasDropTarget,
@@ -12655,7 +12750,8 @@ const resolveDropMeaning = ({
12655
12750
  canToss,
12656
12751
  canLand,
12657
12752
  tossDistance = TOSS_DISTANCE_TO_COMMIT,
12658
- tossSpeed = TOSS_SPEED_TO_COMMIT
12753
+ tossSpeed = TOSS_SPEED_TO_COMMIT,
12754
+ tossBy = TOSS_BY_DEFAULT,
12659
12755
  }) => {
12660
12756
  if (gestureInfo.cancelled) {
12661
12757
  // Nobody let go of anything: the gesture was taken away mid-air (the
@@ -12663,11 +12759,8 @@ const resolveDropMeaning = ({
12663
12759
  // to be at that moment is not a place it was put.
12664
12760
  return "cancel";
12665
12761
  }
12666
- if (canToss) {
12667
- const {
12668
- xDelta,
12669
- yDelta
12670
- } = gestureInfo.layout;
12762
+ if (canToss && tossBy.includes("throw")) {
12763
+ const { xDelta, yDelta } = gestureInfo.layout;
12671
12764
  const distance = Math.hypot(xDelta, yDelta);
12672
12765
  if (distance > tossDistance && gestureInfo.velocity > tossSpeed) {
12673
12766
  return "toss";
@@ -12681,6 +12774,21 @@ const resolveDropMeaning = ({
12681
12774
  return "reorder";
12682
12775
  }
12683
12776
  }
12777
+ if (canToss && tossBy.includes("release-outside")) {
12778
+ // Let go of away from every place: the other way of meaning "get rid of
12779
+ // this", and the one a surface asks for — off the plan and down is
12780
+ // deliberate, never a flick.
12781
+ //
12782
+ // "Away from every place" is the last frame's answer, not the one above: a
12783
+ // row let go of where it already was has no place to INSERT it at and still
12784
+ // has a place under it. And it has to have gone somewhere to be away from
12785
+ // anything — picked up and put straight back down is a hand that changed its
12786
+ // mind, not a thing dropped over nothing.
12787
+ const { xDelta, yDelta } = gestureInfo.layout;
12788
+ if ((xDelta || yDelta) && !gestureInfo.dropTargetInfo) {
12789
+ return "toss";
12790
+ }
12791
+ }
12684
12792
  return "cancel";
12685
12793
  };
12686
12794
 
@@ -12692,47 +12800,45 @@ const resolveDropMeaning = ({
12692
12800
  * for the opposite reason: the original stays until the answer says it is really
12693
12801
  * gone.
12694
12802
  */
12695
- const startDragToCarryCopy = (event, {
12696
- draggedElement,
12697
- canReorder,
12698
- canToss,
12699
- canLand,
12700
- // Something that can be thrown away has to be able to LEAVE. The default of
12701
- // the layer below keeps what is dragged inside its scroll area, which is right
12702
- // for a reorder (a row belongs to its list) and makes a throw impossible — the
12703
- // copy hits the edge of the list and no distance is ever covered, so no throw
12704
- // ever happens and no sideways movement is even visible.
12705
- // Destructured with the default here rather than written at the call below: a
12706
- // caller passing `areaConstraint: undefined` (which is what saying nothing
12707
- // through an options object looks like) would otherwise put the layer below
12708
- // back on its own default and undo this.
12709
- areaConstraint = canToss ? "none" : undefined,
12710
- containerElement = draggedElement.parentElement,
12711
- itemSelector,
12712
- getItemId,
12713
- onReorder,
12714
- onLand,
12715
- onToss,
12716
- tossDistance,
12717
- tossSpeed,
12718
- // A list runs one way and reordering walks it; a board has places all around,
12719
- // so something landing on one of them goes wherever the hand takes it.
12720
- direction = canLand ? {
12721
- x: true,
12722
- y: true
12723
- } : {
12724
- x: false,
12725
- y: true
12803
+ const startDragToCarryCopy = (
12804
+ event,
12805
+ {
12806
+ draggedElement,
12807
+ canReorder,
12808
+ canToss,
12809
+ canLand,
12810
+ // Something that can be thrown away has to be able to LEAVE. The default of
12811
+ // the layer below keeps what is dragged inside its scroll area, which is right
12812
+ // for a reorder (a row belongs to its list) and makes a throw impossible — the
12813
+ // copy hits the edge of the list and no distance is ever covered, so no throw
12814
+ // ever happens and no sideways movement is even visible.
12815
+ // Destructured with the default here rather than written at the call below: a
12816
+ // caller passing `areaConstraint: undefined` (which is what saying nothing
12817
+ // through an options object looks like) would otherwise put the layer below
12818
+ // back on its own default and undo this.
12819
+ areaConstraint = canToss ? "none" : undefined,
12820
+ containerElement = draggedElement.parentElement,
12821
+ itemSelector,
12822
+ getItemId,
12823
+ onReorder,
12824
+ onLand,
12825
+ onToss,
12826
+ tossDistance,
12827
+ tossSpeed,
12828
+ tossBy,
12829
+ // A list runs one way and reordering walks it; a board has places all around,
12830
+ // so something landing on one of them goes wherever the hand takes it.
12831
+ direction = canLand ? { x: true, y: true } : { x: false, y: true },
12832
+ threshold,
12833
+ longPress,
12834
+ longPressDelay,
12835
+ longPressSlop,
12836
+ onPressStart,
12837
+ onPressCancel,
12838
+ onPress,
12839
+ ...options
12726
12840
  },
12727
- threshold,
12728
- longPress,
12729
- longPressDelay,
12730
- longPressSlop,
12731
- onPressStart,
12732
- onPressCancel,
12733
- onPress,
12734
- ...options
12735
- }) => {
12841
+ ) => {
12736
12842
  // An area that opted out of dragging (a text one wants to select, a control
12737
12843
  // that owns the gesture, a callout): the press there is none of our business.
12738
12844
  if (isPressIgnored(event.target, draggedElement)) {
@@ -12748,243 +12854,318 @@ const startDragToCarryCopy = (event, {
12748
12854
  // had got to.
12749
12855
  const startCarry = (pointerEvent, cloneWrapperCaught, onCarryStart) => {
12750
12856
  pointerEvent.preventDefault();
12751
- return dragAfterIntent(pointerEvent, () => {
12752
- // Here and nowhere else is where a press has turned into a carry — the
12753
- // one moment both ways in (a finger held still, a mouse travelled)
12754
- // agree on.
12755
- onCarryStart?.();
12756
- const cloneWrapper = cloneWrapperCaught || createDragClone(draggedElement, pointerEvent);
12757
- if (cloneWrapperCaught) {
12758
- liftDragClone(cloneWrapperCaught, pointerEvent);
12759
- }
12760
- draggedElement.setAttribute("navi-drag-clone-source", "");
12761
- const gestureController = createDragToMoveGestureController({
12762
- direction,
12763
- releasePositionEffect: "manual",
12764
- areaConstraint,
12765
- ...options
12766
- });
12767
- const dragGesture = gestureController.grabViaPointer(pointerEvent, {
12768
- element: draggedElement,
12769
- elementToMove: cloneWrapper
12770
- });
12771
- // getDropTargetInfo uses gestureInfo.elementImpacted to compute the dragged rect.
12772
- // Point it at the clone so drop detection tracks the clone's current position.
12773
- dragGesture.gestureInfo.elementImpacted = cloneWrapper;
12774
-
12775
- // No place to land, no hint: an element that can only be thrown away has
12776
- // nowhere to be put. What the hint LOOKS like follows what a place is here —
12777
- // a line in the gap between two items, or the place itself lit up.
12778
- const dropHintEl = canLand ? createDropSurface() : canReorder ? createDropHint() : null;
12779
- if (dropHintEl) {
12780
- // In the container it draws into, which is where its own vars are set: the
12781
- // shape of a drop hint is a property of the list or board it belongs to,
12782
- // and reading it from there is inheritance rather than a hand-off.
12783
- draggedElement.parentElement.appendChild(dropHintEl);
12784
- }
12785
- // The hint first, the clone second: that order is what stacks them in the
12786
- // top layer. A copy taken back in hand is already up there, and the hint
12787
- // just went above it — shown again it returns to the top, where what the
12788
- // hand carries belongs.
12789
- dropHintEl?.showPopover();
12790
- if (cloneWrapperCaught) {
12791
- cloneWrapper.hidePopover();
12792
- }
12793
- cloneWrapper.showPopover();
12794
-
12795
- // currentBeforeElement: element before which the grabbed item will be inserted (null = end)
12796
- // currentReleaseElement: the actual hovered drop target — used to snap the clone on release
12797
- let currentBeforeElement;
12798
- let currentReleaseElement;
12799
- const clearDropHintDOM = () => {
12800
- if (!dropHintEl) {
12801
- return;
12857
+ return dragAfterIntent(
12858
+ pointerEvent,
12859
+ () => {
12860
+ // Here and nowhere else is where a press has turned into a carry — the
12861
+ // one moment both ways in (a finger held still, a mouse travelled)
12862
+ // agree on.
12863
+ onCarryStart?.();
12864
+ const cloneWrapper =
12865
+ cloneWrapperCaught || createDragClone(draggedElement, pointerEvent);
12866
+ if (cloneWrapperCaught) {
12867
+ liftDragClone(cloneWrapperCaught, pointerEvent);
12802
12868
  }
12803
- dropHintEl.removeAttribute("data-drop-edge");
12804
- dropHintEl.removeAttribute("data-drop-over");
12805
- dropHintEl.style.removeProperty("--drop-target-top");
12806
- dropHintEl.style.removeProperty("--drop-target-bottom");
12807
- dropHintEl.style.removeProperty("--drop-target-left");
12808
- dropHintEl.style.removeProperty("--drop-target-width");
12809
- dropHintEl.style.removeProperty("--drop-target-height");
12810
- };
12811
- const clearDropHint = () => {
12812
- currentBeforeElement = undefined;
12813
- currentReleaseElement = undefined;
12814
- clearDropHintDOM();
12815
- };
12816
- dragGesture.addDragCallback(gestureInfo => {
12817
- if (!dropHintEl) {
12818
- return;
12869
+ draggedElement.setAttribute("navi-drag-clone-source", "");
12870
+
12871
+ const gestureController = createDragToMoveGestureController({
12872
+ direction,
12873
+ releasePositionEffect: "manual",
12874
+ areaConstraint,
12875
+ ...options,
12876
+ });
12877
+ const dragGesture = gestureController.grabViaPointer(pointerEvent, {
12878
+ element: draggedElement,
12879
+ elementToMove: cloneWrapper,
12880
+ });
12881
+ // getDropTargetInfo uses gestureInfo.elementImpacted to compute the dragged rect.
12882
+ // Point it at the clone so drop detection tracks the clone's current position.
12883
+ dragGesture.gestureInfo.elementImpacted = cloneWrapper;
12884
+
12885
+ // No place to land, no hint: an element that can only be thrown away has
12886
+ // nowhere to be put. What the hint LOOKS like follows what a place is here —
12887
+ // a line in the gap between two items, or the place itself lit up.
12888
+ const dropHintEl = canLand
12889
+ ? createDropSurface()
12890
+ : canReorder
12891
+ ? createDropHint()
12892
+ : null;
12893
+ if (dropHintEl) {
12894
+ // Among the places it lights up, which is where its own vars are set: the
12895
+ // shape of a drop hint is a property of the list or board it belongs to,
12896
+ // and reading it from there is inheritance rather than a hand-off. The
12897
+ // copy goes the other way (see createDragClone) — it is dressed by where
12898
+ // the thing it copies stands, which may be a palette far from here.
12899
+ containerElement.appendChild(dropHintEl);
12819
12900
  }
12820
- const allItems = [];
12821
- const items = [];
12822
- for (const el of containerElement.querySelectorAll(itemSelector)) {
12823
- // The copy in hand is a clone of an item, attributes included, and it
12824
- // lives in this same parent: it matches the selector without being
12825
- // an item. Excluded here rather than by stripping attributes off the
12826
- // clone, because a custom itemSelector (a class, a tag) would still
12827
- // match it.
12828
- if (cloneWrapper.contains(el)) {
12829
- continue;
12901
+ // The hint first, the clone second: that order is what stacks them in the
12902
+ // top layer. A copy taken back in hand is already up there, and the hint
12903
+ // just went above it — shown again it returns to the top, where what the
12904
+ // hand carries belongs.
12905
+ dropHintEl?.showPopover();
12906
+ if (cloneWrapperCaught) {
12907
+ cloneWrapper.hidePopover();
12908
+ }
12909
+ cloneWrapper.showPopover();
12910
+
12911
+ // currentBeforeElement: element before which the grabbed item will be inserted (null = end)
12912
+ // currentReleaseElement: the actual hovered drop target — used to snap the clone on release
12913
+ let currentBeforeElement;
12914
+ let currentReleaseElement;
12915
+
12916
+ const clearDropHintDOM = () => {
12917
+ if (!dropHintEl) {
12918
+ return;
12830
12919
  }
12831
- allItems.push(el);
12832
- if (el !== draggedElement) {
12833
- items.push(el);
12920
+ dropHintEl.removeAttribute("data-drop-edge");
12921
+ dropHintEl.removeAttribute("data-drop-over");
12922
+ dropHintEl.style.removeProperty("--drop-target-top");
12923
+ dropHintEl.style.removeProperty("--drop-target-bottom");
12924
+ dropHintEl.style.removeProperty("--drop-target-left");
12925
+ dropHintEl.style.removeProperty("--drop-target-width");
12926
+ dropHintEl.style.removeProperty("--drop-target-height");
12927
+ };
12928
+
12929
+ const clearDropHint = () => {
12930
+ currentBeforeElement = undefined;
12931
+ currentReleaseElement = undefined;
12932
+ clearDropHintDOM();
12933
+ };
12934
+
12935
+ dragGesture.addDragCallback((gestureInfo) => {
12936
+ if (!dropHintEl) {
12937
+ return;
12834
12938
  }
12835
- }
12836
- const dropTargetInfo = getDropTargetInfo(gestureInfo, items, {
12837
- // The edges of a LIST: above the first row means the top of it, below
12838
- // the last one means the end of it. A board has no such reading — away
12839
- // from every place is away from every place.
12840
- fallbackToEdge: !canLand
12841
- });
12842
- gestureInfo.dropTargetInfo = dropTargetInfo || null;
12843
- if (!dropTargetInfo) {
12844
- clearDropHint();
12845
- return;
12846
- }
12847
- if (canLand) {
12848
- // The whole element is the target, so which of its edges the copy came
12849
- // in by says nothing: there is no gap to be on one side of.
12850
- const dropElement = dropTargetInfo.element;
12851
- if (dropElement === currentReleaseElement) {
12939
+ const allItems = [];
12940
+ const items = [];
12941
+ for (const el of containerElement.querySelectorAll(itemSelector)) {
12942
+ // The copy in hand is a clone of an item, attributes included, and it
12943
+ // lives in this same parent: it matches the selector without being
12944
+ // an item. Excluded here rather than by stripping attributes off the
12945
+ // clone, because a custom itemSelector (a class, a tag) would still
12946
+ // match it.
12947
+ if (cloneWrapper.contains(el)) {
12948
+ continue;
12949
+ }
12950
+ allItems.push(el);
12951
+ if (el !== draggedElement) {
12952
+ items.push(el);
12953
+ }
12954
+ }
12955
+
12956
+ const dropTargetInfo = getDropTargetInfo(gestureInfo, items, {
12957
+ // The edges of a LIST: above the first row means the top of it, below
12958
+ // the last one means the end of it. A board has no such reading — away
12959
+ // from every place is away from every place.
12960
+ fallbackToEdge: !canLand,
12961
+ });
12962
+ gestureInfo.dropTargetInfo = dropTargetInfo || null;
12963
+ if (!dropTargetInfo) {
12964
+ clearDropHint();
12852
12965
  return;
12853
12966
  }
12854
- currentReleaseElement = dropElement;
12855
- const dropRect = dropElement.getBoundingClientRect();
12856
- dropHintEl.setAttribute("data-drop-over", "");
12857
- dropHintEl.style.setProperty("--drop-target-top", `${dropRect.top}px`);
12858
- dropHintEl.style.setProperty("--drop-target-left", `${dropRect.left}px`);
12859
- dropHintEl.style.setProperty("--drop-target-width", `${dropRect.width}px`);
12860
- dropHintEl.style.setProperty("--drop-target-height", `${dropRect.height}px`);
12861
- return;
12862
- }
12863
- // Convert {element, edge} to a beforeElement using the items array
12864
- // (not nextElementSibling, which breaks if non-item elements exist between items).
12865
- // edge "start" → insert before the hovered element
12866
- // edge "end" → insert before the next item (null = append at end)
12867
- const edge = dropTargetInfo.elementSide.y;
12868
- const hoveredIndex = items.indexOf(dropTargetInfo.element);
12869
- const beforeElement = edge === "start" ? dropTargetInfo.element : items[hoveredIndex + 1] ?? null;
12870
- // Detect no-op: result would leave the grabbed element in the same position.
12871
- const elementIndex = allItems.indexOf(draggedElement);
12872
- const elementNextItem = allItems[elementIndex + 1] ?? null;
12873
- const isNoop = beforeElement === elementNextItem;
12874
- if (isNoop) {
12875
- clearDropHint();
12876
- return;
12877
- }
12878
- // Early return if nothing changed.
12879
- const releaseElement = dropTargetInfo.element;
12880
- if (beforeElement === currentBeforeElement && releaseElement === currentReleaseElement) {
12881
- return;
12882
- }
12883
- currentBeforeElement = beforeElement;
12884
- currentReleaseElement = releaseElement;
12885
- // Update drop hint CSS vars.
12886
- // beforeElement = null → insert at end (hint after last item)
12887
- // beforeElement = X → insert before X (hint at top edge of X)
12888
- const anchorEl = beforeElement || items[items.length - 1];
12889
- const anchorEdge = beforeElement !== null ? "top" : "bottom";
12890
- // Viewport coordinates, straight from the anchor row: the hint is fixed
12891
- // in the page (see its CSS), so there is no container box to be relative
12892
- // to and no scroll offset to add back.
12893
- const anchorRect = anchorEl.getBoundingClientRect();
12894
- dropHintEl.setAttribute("data-drop-edge", anchorEdge);
12895
- dropHintEl.style.setProperty("--drop-target-top", `${anchorRect.top}px`);
12896
- dropHintEl.style.setProperty("--drop-target-bottom", `${anchorRect.bottom}px`);
12897
- dropHintEl.style.setProperty("--drop-target-left", `${anchorRect.left}px`);
12898
- dropHintEl.style.setProperty("--drop-target-width", `${anchorRect.width}px`);
12899
- });
12900
- dragGesture.addReleaseCallback(async gestureInfo => {
12901
- clearDropHintDOM();
12902
- dropHintEl?.remove();
12903
-
12904
- // What THIS release means, from what the element said it can answer. A
12905
- // throw is asked about first: it is the more insistent of the two, and a
12906
- // hand that sent the thing across the screen has not asked for it to swap
12907
- // places with whatever it happened to fly over.
12908
- const hasDropTarget = canLand ? currentReleaseElement !== undefined : currentBeforeElement !== undefined;
12909
- const dropMeans = resolveDropMeaning({
12910
- gestureInfo,
12911
- hasDropTarget,
12912
- canReorder,
12913
- canToss,
12914
- canLand,
12915
- tossDistance,
12916
- tossSpeed
12967
+ if (canLand) {
12968
+ // The whole element is the target, so which of its edges the copy came
12969
+ // in by says nothing: there is no gap to be on one side of.
12970
+ const dropElement = dropTargetInfo.element;
12971
+ if (dropElement === currentReleaseElement) {
12972
+ return;
12973
+ }
12974
+ currentReleaseElement = dropElement;
12975
+ const dropRect = dropElement.getBoundingClientRect();
12976
+ dropHintEl.setAttribute("data-drop-over", "");
12977
+ dropHintEl.style.setProperty(
12978
+ "--drop-target-top",
12979
+ `${dropRect.top}px`,
12980
+ );
12981
+ dropHintEl.style.setProperty(
12982
+ "--drop-target-left",
12983
+ `${dropRect.left}px`,
12984
+ );
12985
+ dropHintEl.style.setProperty(
12986
+ "--drop-target-width",
12987
+ `${dropRect.width}px`,
12988
+ );
12989
+ dropHintEl.style.setProperty(
12990
+ "--drop-target-height",
12991
+ `${dropRect.height}px`,
12992
+ );
12993
+ return;
12994
+ }
12995
+ // Convert {element, edge} to a beforeElement using the items array
12996
+ // (not nextElementSibling, which breaks if non-item elements exist between items).
12997
+ // edge "start" → insert before the hovered element
12998
+ // edge "end" → insert before the next item (null = append at end)
12999
+ const edge = dropTargetInfo.elementSide.y;
13000
+ const hoveredIndex = items.indexOf(dropTargetInfo.element);
13001
+ const beforeElement =
13002
+ edge === "start"
13003
+ ? dropTargetInfo.element
13004
+ : (items[hoveredIndex + 1] ?? null);
13005
+ // Detect no-op: result would leave the grabbed element in the same position.
13006
+ const elementIndex = allItems.indexOf(draggedElement);
13007
+ const elementNextItem = allItems[elementIndex + 1] ?? null;
13008
+ const isNoop = beforeElement === elementNextItem;
13009
+ if (isNoop) {
13010
+ clearDropHint();
13011
+ return;
13012
+ }
13013
+ // Early return if nothing changed.
13014
+ const releaseElement = dropTargetInfo.element;
13015
+ if (
13016
+ beforeElement === currentBeforeElement &&
13017
+ releaseElement === currentReleaseElement
13018
+ ) {
13019
+ return;
13020
+ }
13021
+ currentBeforeElement = beforeElement;
13022
+ currentReleaseElement = releaseElement;
13023
+ // Update drop hint CSS vars.
13024
+ // beforeElement = null → insert at end (hint after last item)
13025
+ // beforeElement = X → insert before X (hint at top edge of X)
13026
+ const anchorEl = beforeElement || items[items.length - 1];
13027
+ const anchorEdge = beforeElement !== null ? "top" : "bottom";
13028
+ // Viewport coordinates, straight from the anchor row: the hint is fixed
13029
+ // in the page (see its CSS), so there is no container box to be relative
13030
+ // to and no scroll offset to add back.
13031
+ const anchorRect = anchorEl.getBoundingClientRect();
13032
+ dropHintEl.setAttribute("data-drop-edge", anchorEdge);
13033
+ dropHintEl.style.setProperty(
13034
+ "--drop-target-top",
13035
+ `${anchorRect.top}px`,
13036
+ );
13037
+ dropHintEl.style.setProperty(
13038
+ "--drop-target-bottom",
13039
+ `${anchorRect.bottom}px`,
13040
+ );
13041
+ dropHintEl.style.setProperty(
13042
+ "--drop-target-left",
13043
+ `${anchorRect.left}px`,
13044
+ );
13045
+ dropHintEl.style.setProperty(
13046
+ "--drop-target-width",
13047
+ `${anchorRect.width}px`,
13048
+ );
12917
13049
  });
12918
13050
 
12919
- // Let go of and still on the screen: from here until it is taken away
12920
- // the copy can be taken back in hand (see letCopyBeCaught).
12921
- const copyLetGoOf = letCopyBeCaught(cloneWrapper, (pointerDownEvent, whenCarried) => startCarry(pointerDownEvent, cloneWrapper, whenCarried));
12922
-
12923
- // The copy stops where the hand left it, and the answer is given a way to
12924
- // take it the rest of the way — synchronously, inside a view transition, so
12925
- // it is captured where it lands rather than where it was let go of.
12926
- const landCopyOn = async (targetElement, answer) => {
12927
- const clone = cloneWrapper.firstElementChild;
12928
- // Bake the current visual position (transform included) into the CSS vars
12929
- // so the copy stays where the user released it when the transform goes.
12930
- setCloneViewportRect(cloneWrapper, cloneWrapper);
12931
- gestureInfo.cancelPosition();
12932
- // Where the copy comes down is not always the thing it came down ON: a
12933
- // place of a board can be larger than what stands on it, and the copy
12934
- // has to keep its own size and land where the item will be. Said with
12935
- // an element, because the caller has one — the piece already standing
12936
- // there, the empty slot waiting.
12937
- const syncCloneWithDropTarget = (landingElement = targetElement) => {
12938
- setCloneViewportRect(cloneWrapper, landingElement);
12939
- // Removing this attr drops the CSS scale, so the browser captures the
12940
- // copy at scale 1 as the "new" state.
12941
- clone.removeAttribute("navi-drag-clone");
13051
+ dragGesture.addReleaseCallback(async (gestureInfo) => {
13052
+ clearDropHintDOM();
13053
+ dropHintEl?.remove();
13054
+
13055
+ // What THIS release means, from what the element said it can answer. A
13056
+ // throw is asked about first: it is the more insistent of the two, and a
13057
+ // hand that sent the thing across the screen has not asked for it to swap
13058
+ // places with whatever it happened to fly over.
13059
+ const hasDropTarget = canLand
13060
+ ? currentReleaseElement !== undefined
13061
+ : currentBeforeElement !== undefined;
13062
+ const dropMeans = resolveDropMeaning({
13063
+ gestureInfo,
13064
+ hasDropTarget,
13065
+ canReorder,
13066
+ canToss,
13067
+ canLand,
13068
+ tossDistance,
13069
+ tossSpeed,
13070
+ tossBy,
13071
+ });
13072
+
13073
+ // Let go of and still on the screen: from here until it is taken away
13074
+ // the copy can be taken back in hand (see letCopyBeCaught).
13075
+ const copyLetGoOf = letCopyBeCaught(
13076
+ cloneWrapper,
13077
+ (pointerDownEvent, whenCarried) =>
13078
+ startCarry(pointerDownEvent, cloneWrapper, whenCarried),
13079
+ );
13080
+
13081
+ // The copy stops where the hand left it, and the answer is given a way to
13082
+ // take it the rest of the way — synchronously, inside a view transition, so
13083
+ // it is captured where it lands rather than where it was let go of.
13084
+ const landCopyOn = async (targetElement, answer) => {
13085
+ const clone = cloneWrapper.firstElementChild;
13086
+ // Bake the current visual position (transform included) into the CSS vars
13087
+ // so the copy stays where the user released it when the transform goes.
13088
+ setCloneViewportRect(cloneWrapper, cloneWrapper);
13089
+ gestureInfo.cancelPosition();
13090
+ // Where the copy comes down is not always the thing it came down ON: a
13091
+ // place of a board can be larger than what stands on it, and the copy
13092
+ // has to keep its own size and land where the item will be. Said with
13093
+ // an element, because the caller has one — the piece already standing
13094
+ // there, the empty slot waiting.
13095
+ const syncCloneWithDropTarget = (
13096
+ landingElement = targetElement,
13097
+ ) => {
13098
+ setCloneViewportRect(cloneWrapper, landingElement);
13099
+ // Removing this attr drops the CSS scale, so the browser captures the
13100
+ // copy at scale 1 as the "new" state.
13101
+ clone.removeAttribute("navi-drag-clone");
13102
+ };
13103
+ await answer(syncCloneWithDropTarget);
12942
13104
  };
12943
- await answer(syncCloneWithDropTarget);
12944
- };
12945
- if (dropMeans === "toss") {
12946
- // Bake the position the hand left it at, so the flight starts from
12947
- // there rather than from where the clone was declared.
12948
- setCloneViewportRect(cloneWrapper, cloneWrapper);
12949
- gestureInfo.cancelPosition();
12950
- const gone = await tossCloneAway(cloneWrapper, gestureInfo, onToss);
12951
- if (!gone) {
12952
- // It still exists, so the screen has to say so: the copy comes back
12953
- // over the original, and taking it away then reveals the row in
12954
- // place.
12955
- await settleCloneBack(cloneWrapper, draggedElement);
13105
+
13106
+ if (dropMeans === "toss") {
13107
+ // Bake the position the hand left it at, so the flight starts from
13108
+ // there rather than from where the clone was declared.
13109
+ setCloneViewportRect(cloneWrapper, cloneWrapper);
13110
+ gestureInfo.cancelPosition();
13111
+ const gone = await tossCloneAway(cloneWrapper, gestureInfo, onToss);
13112
+ if (!gone) {
13113
+ // It still exists, so the screen has to say so: the copy comes back
13114
+ // over the original, and taking it away then reveals the row in
13115
+ // place.
13116
+ await settleCloneBack(cloneWrapper, draggedElement);
13117
+ }
13118
+ } else if (dropMeans === "land") {
13119
+ // Read before the copy is put down: landing bakes its position and
13120
+ // drops the transform, and where it came down is where the hand let
13121
+ // go of it.
13122
+ const landedAt = getRectInside(cloneWrapper, currentReleaseElement);
13123
+ await landCopyOn(currentReleaseElement, (syncCloneWithDropTarget) =>
13124
+ onLand({
13125
+ fromId: getItemId(draggedElement),
13126
+ toId: getItemId(currentReleaseElement),
13127
+ ...landedAt,
13128
+ syncCloneWithDropTarget,
13129
+ }),
13130
+ );
13131
+ } else if (dropMeans === "reorder") {
13132
+ await landCopyOn(currentReleaseElement, (syncCloneWithDropTarget) =>
13133
+ onReorder(
13134
+ getItemId(draggedElement),
13135
+ currentBeforeElement ? getItemId(currentBeforeElement) : null,
13136
+ syncCloneWithDropTarget,
13137
+ ),
13138
+ );
12956
13139
  }
12957
- } else if (dropMeans === "land") {
12958
- await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onLand(getItemId(draggedElement), getItemId(currentReleaseElement), syncCloneWithDropTarget));
12959
- } else if (dropMeans === "reorder") {
12960
- await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onReorder(getItemId(draggedElement), currentBeforeElement ? getItemId(currentBeforeElement) : null, syncCloneWithDropTarget));
12961
- }
12962
- if (await copyLetGoOf.settled()) {
12963
- // In a hand again: the copy, and the place kept for it, are the new
12964
- // gesture's — this one has nothing left to take away.
12965
- return;
12966
- }
12967
- draggedElement.removeAttribute("navi-drag-clone-source");
12968
- cloneWrapper.remove();
12969
- });
12970
- return dragGesture;
12971
- }, {
12972
- threshold,
12973
- // A copy caught on its way home is not an ambiguous press: the hand
12974
- // reached for something moving, and the press was already matched
12975
- // against the copy's own box before it got here. The wait a finger is
12976
- // asked for elsewhere tells a scroll from a drag, and there is no scroll
12977
- // to tell it from — the copy covers that spot from the top layer. Asked
12978
- // for anyway it cannot even be answered: the wait is about as long as the
12979
- // journey, so the thing is home before the proof is done, while a mouse
12980
- // takes it in five pixels.
12981
- longPress: cloneWrapperCaught ? false : longPress,
12982
- longPressDelay,
12983
- longPressSlop,
12984
- onPressStart,
12985
- onPressCancel,
12986
- onPress
12987
- });
13140
+ if (await copyLetGoOf.settled()) {
13141
+ // In a hand again: the copy, and the place kept for it, are the new
13142
+ // gesture's — this one has nothing left to take away.
13143
+ return;
13144
+ }
13145
+ draggedElement.removeAttribute("navi-drag-clone-source");
13146
+ cloneWrapper.remove();
13147
+ });
13148
+
13149
+ return dragGesture;
13150
+ },
13151
+ {
13152
+ threshold,
13153
+ // A copy caught on its way home is not an ambiguous press: the hand
13154
+ // reached for something moving, and the press was already matched
13155
+ // against the copy's own box before it got here. The wait a finger is
13156
+ // asked for elsewhere tells a scroll from a drag, and there is no scroll
13157
+ // to tell it from — the copy covers that spot from the top layer. Asked
13158
+ // for anyway it cannot even be answered: the wait is about as long as the
13159
+ // journey, so the thing is home before the proof is done, while a mouse
13160
+ // takes it in five pixels.
13161
+ longPress: cloneWrapperCaught ? false : longPress,
13162
+ longPressDelay,
13163
+ longPressSlop,
13164
+ onPressStart,
13165
+ onPressCancel,
13166
+ onPress,
13167
+ },
13168
+ );
12988
13169
  };
12989
13170
  return startCarry(event);
12990
13171
  };
@@ -13000,6 +13181,28 @@ const setCloneViewportRect = (cloneWrapper, el) => {
13000
13181
  cloneWrapper.style.setProperty("--clone-height", `${rect.height}px`);
13001
13182
  };
13002
13183
 
13184
+ // Where one box sits inside another, in that other's own content: its border and
13185
+ // its scroll are taken out, so the numbers say where IN the thing rather than
13186
+ // where in what is visible of it at that moment.
13187
+ const getRectInside = (element, containerElement) => {
13188
+ const rect = element.getBoundingClientRect();
13189
+ const containerRect = containerElement.getBoundingClientRect();
13190
+ return {
13191
+ x:
13192
+ rect.left -
13193
+ containerRect.left -
13194
+ containerElement.clientLeft +
13195
+ containerElement.scrollLeft,
13196
+ y:
13197
+ rect.top -
13198
+ containerRect.top -
13199
+ containerElement.clientTop +
13200
+ containerElement.scrollTop,
13201
+ width: rect.width,
13202
+ height: rect.height,
13203
+ };
13204
+ };
13205
+
13003
13206
  // Creates the two-layer clone structure used for drag-to-reorder.
13004
13207
  //
13005
13208
  // Layer 1 — wrapper (navi-drag-clone-wrapper):
@@ -13017,7 +13220,7 @@ const setCloneViewportRect = (cloneWrapper, el) => {
13017
13220
 
13018
13221
  // The chevron is the one the table's column drop preview uses, rotated by the
13019
13222
  // CSS above so each cap points into the line.
13020
- const dropHintTemplate = /* html */`
13223
+ const dropHintTemplate = /* html */ `
13021
13224
  <div
13022
13225
  class="navi_drop_hint"
13023
13226
  popover="manual"
@@ -13043,6 +13246,7 @@ const createDropHint = () => {
13043
13246
  div.innerHTML = dropHintTemplate.trim();
13044
13247
  return div.firstElementChild;
13045
13248
  };
13249
+
13046
13250
  const createDropSurface = () => {
13047
13251
  const div = document.createElement("div");
13048
13252
  div.className = "navi_drop_surface";
@@ -13061,17 +13265,14 @@ const createDropSurface = () => {
13061
13265
  * that means "get rid of this" — nobody waits to watch it go.
13062
13266
  */
13063
13267
  const tossCloneAway = async (cloneWrapper, gestureInfo, onToss) => {
13064
- const {
13065
- xDelta,
13066
- yDelta
13067
- } = gestureInfo.layout;
13268
+ const { xDelta, yDelta } = gestureInfo.layout;
13068
13269
  const distance = Math.hypot(xDelta, yDelta) || 1;
13069
13270
  cloneWrapper.dataset.tossed = "away";
13070
- cloneWrapper.style.translate = `${xDelta / distance * TOSS_DISTANCE}px ${yDelta / distance * TOSS_DISTANCE}px`;
13271
+ cloneWrapper.style.translate = `${(xDelta / distance) * TOSS_DISTANCE}px ${
13272
+ (yDelta / distance) * TOSS_DISTANCE
13273
+ }px`;
13071
13274
  try {
13072
- await onToss?.({
13073
- gestureInfo
13074
- });
13275
+ await onToss?.({ gestureInfo });
13075
13276
  return true;
13076
13277
  } catch {
13077
13278
  return false;
@@ -13088,11 +13289,17 @@ const tossCloneAway = async (cloneWrapper, gestureInfo, onToss) => {
13088
13289
  */
13089
13290
  const settleCloneBack = (cloneWrapper, sourceElement) => {
13090
13291
  const sourceRect = sourceElement.getBoundingClientRect();
13091
- const releaseLeft = parseFloat(cloneWrapper.style.getPropertyValue("--clone-left"));
13092
- const releaseTop = parseFloat(cloneWrapper.style.getPropertyValue("--clone-top"));
13292
+ const releaseLeft = parseFloat(
13293
+ cloneWrapper.style.getPropertyValue("--clone-left"),
13294
+ );
13295
+ const releaseTop = parseFloat(
13296
+ cloneWrapper.style.getPropertyValue("--clone-top"),
13297
+ );
13093
13298
  cloneWrapper.dataset.tossed = "back";
13094
- cloneWrapper.style.translate = `${sourceRect.left - releaseLeft}px ${sourceRect.top - releaseTop}px`;
13095
- return new Promise(resolve => {
13299
+ cloneWrapper.style.translate = `${sourceRect.left - releaseLeft}px ${
13300
+ sourceRect.top - releaseTop
13301
+ }px`;
13302
+ return new Promise((resolve) => {
13096
13303
  setTimeout(resolve, TOSS_DURATION_MS);
13097
13304
  });
13098
13305
  };
@@ -13132,23 +13339,20 @@ const settleCloneBack = (cloneWrapper, sourceElement) => {
13132
13339
  const letCopyBeCaught = (cloneWrapper, carryAgain) => {
13133
13340
  let caught = false;
13134
13341
  let pressIsOver = Promise.resolve();
13135
- const onPointerDown = pointerDownEvent => {
13136
- const {
13137
- left,
13138
- right,
13139
- top,
13140
- bottom
13141
- } = cloneWrapper.getBoundingClientRect();
13142
- const {
13143
- clientX,
13144
- clientY
13145
- } = pointerDownEvent;
13146
- if (clientX < left || clientX > right || clientY < top || clientY > bottom) {
13342
+ const onPointerDown = (pointerDownEvent) => {
13343
+ const { left, right, top, bottom } = cloneWrapper.getBoundingClientRect();
13344
+ const { clientX, clientY } = pointerDownEvent;
13345
+ if (
13346
+ clientX < left ||
13347
+ clientX > right ||
13348
+ clientY < top ||
13349
+ clientY > bottom
13350
+ ) {
13147
13351
  // Somewhere else on the page: this press is not about the copy.
13148
13352
  return;
13149
13353
  }
13150
13354
  let pressIsOverResolve;
13151
- pressIsOver = new Promise(resolve => {
13355
+ pressIsOver = new Promise((resolve) => {
13152
13356
  pressIsOverResolve = resolve;
13153
13357
  });
13154
13358
  const onPointerEnd = () => {
@@ -13176,9 +13380,8 @@ const letCopyBeCaught = (cloneWrapper, carryAgain) => {
13176
13380
  // caught with a finger and can with a mouse.
13177
13381
  const root = document.documentElement;
13178
13382
  root.setAttribute("data-drag-catchable", "");
13179
- root.addEventListener("touchmove", keepTouchRefusable, {
13180
- passive: false
13181
- });
13383
+ root.addEventListener("touchmove", keepTouchRefusable, { passive: false });
13384
+
13182
13385
  return {
13183
13386
  settled: async () => {
13184
13387
  // A hand that lets go and presses again while the copy is still there is
@@ -13193,7 +13396,7 @@ const letCopyBeCaught = (cloneWrapper, carryAgain) => {
13193
13396
  root.removeAttribute("data-drag-catchable");
13194
13397
  root.removeEventListener("touchmove", keepTouchRefusable);
13195
13398
  return caught;
13196
- }
13399
+ },
13197
13400
  };
13198
13401
  };
13199
13402
 
@@ -13203,11 +13406,16 @@ const letCopyBeCaught = (cloneWrapper, carryAgain) => {
13203
13406
  // again is somewhere else on it — so taking one back hands it both again.
13204
13407
  const liftDragClone = (cloneWrapper, pointerEvent) => {
13205
13408
  const rect = cloneWrapper.getBoundingClientRect();
13206
- cloneWrapper.style.setProperty("--drag-origin", `${pointerEvent.clientX - rect.left}px ${pointerEvent.clientY - rect.top}px`);
13409
+ cloneWrapper.style.setProperty(
13410
+ "--drag-origin",
13411
+ `${pointerEvent.clientX - rect.left}px ${pointerEvent.clientY - rect.top}px`,
13412
+ );
13207
13413
  cloneWrapper.firstElementChild.setAttribute("navi-drag-clone", "");
13208
13414
  };
13415
+
13209
13416
  const createDragClone = (element, pointerEvent) => {
13210
13417
  const rect = element.getBoundingClientRect();
13418
+
13211
13419
  const wrapper = document.createElement("div");
13212
13420
  wrapper.setAttribute("navi-drag-clone-wrapper", "");
13213
13421
  // A copy can be caught on its way home (see settleCloneBack), which is a press
@@ -13223,7 +13431,10 @@ const createDragClone = (element, pointerEvent) => {
13223
13431
  // Grab point within the element — used as transform-origin so the
13224
13432
  // scale(1.15) expands from where the user clicked, not the element center.
13225
13433
  // These offsets are element-relative so viewport coords are correct here.
13226
- wrapper.style.setProperty("--drag-origin", `${pointerEvent.clientX - rect.left}px ${pointerEvent.clientY - rect.top}px`);
13434
+ wrapper.style.setProperty(
13435
+ "--drag-origin",
13436
+ `${pointerEvent.clientX - rect.left}px ${pointerEvent.clientY - rect.top}px`,
13437
+ );
13227
13438
  const elementClone = element.cloneNode(true);
13228
13439
  // A deep copy copies the ids too, and two elements answering to one id is a
13229
13440
  // document that lies: getElementById picks whichever comes first, an anchor
@@ -13240,6 +13451,18 @@ const createDragClone = (element, pointerEvent) => {
13240
13451
  // (The original wears it too, but it is hidden — see navi-drag-clone-source.)
13241
13452
  elementClone.setAttribute("data-grabbed", "");
13242
13453
  elementClone.style.viewTransitionName = "navi-drag-clone";
13454
+ // The copy takes the wrapper's box, and nothing the page said about where the
13455
+ // ORIGINAL stands may place it: a piece drawn at "left: 40px; top: 130px" on
13456
+ // its board, a marker centred by a translate, a card pushed by a margin — the
13457
+ // deep copy carries all of that, inline styles included, and would sit that far
13458
+ // from the wrapper's corner, so from the hand. Written inline on the copy
13459
+ // because that is the one place that outranks both the inline values it copied
13460
+ // and any rule of the page's, without a stylesheet having to shout !important.
13461
+ elementClone.style.position = "absolute";
13462
+ elementClone.style.inset = "0";
13463
+ elementClone.style.margin = "0";
13464
+ elementClone.style.translate = "none";
13465
+
13243
13466
  wrapper.appendChild(elementClone);
13244
13467
  // Beside the thing it copies, so it stands where that thing stands: every
13245
13468
  // inherited value and every custom property the original reads, the copy reads
@@ -13248,6 +13471,7 @@ const createDragClone = (element, pointerEvent) => {
13248
13471
  // whatever its depth in the tree, and being fixed keeps it out of the
13249
13472
  // scrollable overflow of the list it sits in.
13250
13473
  element.parentElement.appendChild(wrapper);
13474
+
13251
13475
  return wrapper;
13252
13476
  };
13253
13477
 
@@ -13329,44 +13553,7 @@ const getResizeDirection = (element) => {
13329
13553
  return { x, y };
13330
13554
  };
13331
13555
 
13332
- installImportMetaCssBuild(import.meta);/**
13333
- * What a drag means when it TRAVELS: a whole screen pushed aside to bring in the
13334
- * next one — slides inside one box, pages that are URLs.
13335
- *
13336
- * The pointer itself is not read here: reading a press, waiting for it to become
13337
- * a gesture, capturing it, measuring how fast it goes, swallowing the click it
13338
- * would have made is one gesture system for the whole codebase
13339
- * (@jsenv/dom's drag_gesture + drag_after_intent), and this asks it for the
13340
- * plain version — nothing carried, so no backdrop over the page, nothing made
13341
- * inert, no focus taken: a screen slides and the page keeps its scrolling and
13342
- * its keyboard.
13343
- *
13344
- * What IS here is everything that makes a travel a travel rather than a
13345
- * carry — and it is policy, not plumbing:
13346
- * - the axis is LOCKED by the first movement, instead of being constrained
13347
- * ahead of time;
13348
- * - a press becomes a gesture by distance for every pointer, finger included:
13349
- * a swipe is a travel, and asking a finger to hold still first (the rule for
13350
- * picking an object up) would mean waiting before being allowed to swipe;
13351
- * - what travels walks ONE BOX, resists past its ends, and is measured from
13352
- * where the finger is once it gets there — unless the caller has another box
13353
- * to offer at that edge, and then the gesture walks on into it;
13354
- * - letting go is a question with an answer: a third of a box, or a flick.
13355
- *
13356
- * What is NOT here is geometry: how big a box is, what lies one step that way,
13357
- * what to paint while the finger moves. The caller knows those and nothing else
13358
- * does — this reads the gesture and calls back.
13359
- *
13360
- * Who owns a gesture is decided in three places, and all three are read here:
13361
- * - what says so itself, with [data-no-drag-travel] or by being a field — a
13362
- * component that reads the pointer marks itself, because the container it
13363
- * ends up in cannot know what it is;
13364
- * - a scroller between the pointer and the box with room left that way, which
13365
- * keeps the gesture until it has none;
13366
- * - another box that travels, between the pointer and this one: the innermost
13367
- * one walks the axis it walks, and leaves the others whatever axis it does
13368
- * not (see axesLeftBy).
13369
- */
13556
+ installImportMetaCssBuild(import.meta);
13370
13557
 
13371
13558
  // While a pointer is on something that travels: said on the document, because
13372
13559
  // what has to be told is the document.
@@ -13375,7 +13562,8 @@ const GESTURE_ATTRIBUTE = "data-drag-travel-gesture";
13375
13562
  // …and while one is actually travelling something, which is a later moment and
13376
13563
  // takes more away (see the CSS).
13377
13564
  const WALKING_ATTRIBUTE = "data-drag-travel-walking";
13378
- import.meta.css = [/* css */`
13565
+
13566
+ import.meta.css = /* css */ [`
13379
13567
  :root[${GESTURE_ATTRIBUTE}] {
13380
13568
  /* The bounce the browser plays when a gesture reaches the end of a page —
13381
13569
  and the swipe that goes back in history with it. Both are the browser
@@ -13535,24 +13723,42 @@ const DRAG_RESISTANCE = 0.3;
13535
13723
  // press, and nothing hands that back mid-gesture — the travel would drag a
13536
13724
  // selection along with it. With a finger the hold is the selection UI itself, so
13537
13725
  // there is nothing to take there either.
13538
- const PRESSED_INPUT_TYPES = ['[type="checkbox"]', '[type="radio"]', '[type="button"]', '[type="submit"]', '[type="reset"]', '[type="image"]', '[type="color"]', '[type="file"]'].join(",");
13539
- const NATIVE_POINTER_GESTURES = [{
13540
- // The value follows the pointer, pixel by pixel: a thumb along its rail, a
13541
- // spinner clicked up and down. There is no moment in that which could be
13542
- // given away — the gesture IS the pointer.
13543
- selector: 'input[type="range"],input[type="number"]'
13544
- }, {
13545
- // The text answers the pointer, and differently per hand: a mouse drags a
13546
- // caret through it (a selection), a finger drags the field's own content
13547
- // sideways when it overflows, and a finger held still opens the selection
13548
- // UI. Three things a field is for, against one travel that has a whole box
13549
- // to start from.
13550
- selector: [`input:not(${PRESSED_INPUT_TYPES},[type="range"],[type="number"])`, "textarea", '[contenteditable=""]', '[contenteditable="true"]'].join(",")
13551
- }, {
13552
- // A menu opens under the pointer and the browser keeps the rest of the
13553
- // gesture: dragged over, it picks an option.
13554
- selector: "select"
13555
- }];
13726
+ const PRESSED_INPUT_TYPES = [
13727
+ '[type="checkbox"]',
13728
+ '[type="radio"]',
13729
+ '[type="button"]',
13730
+ '[type="submit"]',
13731
+ '[type="reset"]',
13732
+ '[type="image"]',
13733
+ '[type="color"]',
13734
+ '[type="file"]',
13735
+ ].join(",");
13736
+ const NATIVE_POINTER_GESTURES = [
13737
+ {
13738
+ // The value follows the pointer, pixel by pixel: a thumb along its rail, a
13739
+ // spinner clicked up and down. There is no moment in that which could be
13740
+ // given away — the gesture IS the pointer.
13741
+ selector: 'input[type="range"],input[type="number"]',
13742
+ },
13743
+ {
13744
+ // The text answers the pointer, and differently per hand: a mouse drags a
13745
+ // caret through it (a selection), a finger drags the field's own content
13746
+ // sideways when it overflows, and a finger held still opens the selection
13747
+ // UI. Three things a field is for, against one travel that has a whole box
13748
+ // to start from.
13749
+ selector: [
13750
+ `input:not(${PRESSED_INPUT_TYPES},[type="range"],[type="number"])`,
13751
+ "textarea",
13752
+ '[contenteditable=""]',
13753
+ '[contenteditable="true"]',
13754
+ ].join(","),
13755
+ },
13756
+ {
13757
+ // A menu opens under the pointer and the browser keeps the rest of the
13758
+ // gesture: dragged over, it picks an option.
13759
+ selector: "select",
13760
+ },
13761
+ ];
13556
13762
 
13557
13763
  // …and what an element says against its own tag. Every line above is about what
13558
13764
  // an element DOES, and a tag only says what it IS: a picker's façade is an
@@ -13569,9 +13775,9 @@ const NATIVE_POINTER_GESTURES = [{
13569
13775
  // that can really be typed in) has not made a travel possible, it has taken the
13570
13776
  // gesture the user was making away from them.
13571
13777
  const PRESS_ONLY_ATTRIBUTE = "data-press-only";
13572
- const NATIVE_POINTER_GESTURE_SELECTOR = NATIVE_POINTER_GESTURES.map(({
13573
- selector
13574
- }) => `:is(${selector}):not([${PRESS_ONLY_ATTRIBUTE}])`).join(",");
13778
+ const NATIVE_POINTER_GESTURE_SELECTOR = NATIVE_POINTER_GESTURES.map(
13779
+ ({ selector }) => `:is(${selector}):not([${PRESS_ONLY_ATTRIBUTE}])`,
13780
+ ).join(",");
13575
13781
 
13576
13782
  // What a drag must not start on: what answers the pointer natively (above),
13577
13783
  // plus what has said the gesture is its own. A drag SOURCE is not in the list:
@@ -13583,7 +13789,15 @@ const NATIVE_POINTER_GESTURE_SELECTOR = NATIVE_POINTER_GESTURES.map(({
13583
13789
  // through (see DRAG_IGNORED_SELECTOR in drag_to.js). And so is a popover or a
13584
13790
  // dialog: a layer OVER the box, whose press only bubbles through the box because
13585
13791
  // the layer is anchored in it.
13586
- const DRAG_EXCLUDED_SELECTOR = [NATIVE_POINTER_GESTURE_SELECTOR, "[data-drag-handle]", "[data-no-drag-travel]", '[data-self-interactions~="drag"]', '[data-self-interactions~="*"]', "[popover]", "dialog"].join(",");
13792
+ const DRAG_EXCLUDED_SELECTOR = [
13793
+ NATIVE_POINTER_GESTURE_SELECTOR,
13794
+ "[data-drag-handle]",
13795
+ "[data-no-drag-travel]",
13796
+ '[data-self-interactions~="drag"]',
13797
+ '[data-self-interactions~="*"]',
13798
+ "[popover]",
13799
+ "dialog",
13800
+ ].join(",");
13587
13801
 
13588
13802
  // Which axes a box travels on, one attribute per gesture, said in the DOM by
13589
13803
  // whoever owns the box: it is what a box ABOVE another reads to know the
@@ -13603,8 +13817,12 @@ const DRAG_SOURCE_AXES_ATTRIBUTE = "data-drag-source";
13603
13817
  // covers everything. So a gesture that happened on it is not the gesture of any
13604
13818
  // box it merely sits on top of, and every walk up the tree from the pointer
13605
13819
  // ends here: the boxes above are behind, and behind is not under the finger.
13606
- const TOP_LAYER_SELECTOR = [":popover-open", "dialog:modal", ":fullscreen"].join(",");
13607
- const isTopLayer = element => {
13820
+ const TOP_LAYER_SELECTOR = [
13821
+ ":popover-open",
13822
+ "dialog:modal",
13823
+ ":fullscreen",
13824
+ ].join(",");
13825
+ const isTopLayer = (element) => {
13608
13826
  return element.matches(TOP_LAYER_SELECTOR);
13609
13827
  };
13610
13828
 
@@ -13675,12 +13893,10 @@ const scrollRoomTowards = (fromElement, stopElement, axis, sign) => {
13675
13893
  return false;
13676
13894
  }
13677
13895
  const size = axis === "x" ? element.clientWidth : element.clientHeight;
13678
- const scrollSize = axis === "x" ? element.scrollWidth : element.scrollHeight;
13896
+ const scrollSize =
13897
+ axis === "x" ? element.scrollWidth : element.scrollHeight;
13679
13898
  if (scrollSize > size + 1) {
13680
- const {
13681
- overflowX,
13682
- overflowY
13683
- } = getComputedStyle(element);
13899
+ const { overflowX, overflowY } = getComputedStyle(element);
13684
13900
  const overflow = axis === "x" ? overflowX : overflowY;
13685
13901
  if (overflow === "auto" || overflow === "scroll") {
13686
13902
  const position = axis === "x" ? element.scrollLeft : element.scrollTop;
@@ -13706,7 +13922,7 @@ const travelsAfter = ({
13706
13922
  size,
13707
13923
  velocity,
13708
13924
  towardsSomething,
13709
- commitRatio
13925
+ commitRatio,
13710
13926
  }) => {
13711
13927
  if (!towardsSomething) {
13712
13928
  return false;
@@ -13727,11 +13943,15 @@ const travelsAfter = ({
13727
13943
  // other way. Without this the picture alone decides, and a screen caught at
13728
13944
  // two thirds and thrown back still arrives: the gesture was read as the
13729
13945
  // place it was let go of rather than as a movement.
13730
- if (Math.abs(velocity) > DRAG_THROW_BACK_VELOCITY && Math.sign(velocity) !== sign) {
13946
+ if (
13947
+ Math.abs(velocity) > DRAG_THROW_BACK_VELOCITY &&
13948
+ Math.sign(velocity) !== sign
13949
+ ) {
13731
13950
  return false;
13732
13951
  }
13733
13952
  // …and going towards it travels, however slowly: see DRAG_DRIFT_VELOCITY.
13734
- const drifting = Math.abs(velocity) > DRAG_DRIFT_VELOCITY && Math.sign(velocity) === sign;
13953
+ const drifting =
13954
+ Math.abs(velocity) > DRAG_DRIFT_VELOCITY && Math.sign(velocity) === sign;
13735
13955
  if (drifting && Math.abs(pulled) > DRAG_DRIFT_DISTANCE) {
13736
13956
  return true;
13737
13957
  }
@@ -13799,17 +14019,20 @@ const travelsAfter = ({
13799
14019
  * refused it. Nothing was painted and nothing has to be put back — this is
13800
14020
  * only so the caller can forget the gesture it is holding.
13801
14021
  */
13802
- const startDragToTravel = (pointerDownEvent, {
13803
- element,
13804
- axes = "xy",
13805
- immediate = false,
13806
- commitRatio = DRAG_COMMIT_RATIO,
13807
- onStart,
13808
- onPull,
13809
- onEnd,
13810
- onEdge = () => false,
13811
- onGiveUp = () => {}
13812
- }) => {
14022
+ const startDragToTravel = (
14023
+ pointerDownEvent,
14024
+ {
14025
+ element,
14026
+ axes = "xy",
14027
+ immediate = false,
14028
+ commitRatio = DRAG_COMMIT_RATIO,
14029
+ onStart,
14030
+ onPull,
14031
+ onEnd,
14032
+ onEdge = () => false,
14033
+ onGiveUp = () => {},
14034
+ },
14035
+ ) => {
13813
14036
  const target = pointerDownEvent.target;
13814
14037
  if (!target.closest || isPressExcluded(target, element)) {
13815
14038
  return null;
@@ -13818,8 +14041,15 @@ const startDragToTravel = (pointerDownEvent, {
13818
14041
  // anything between them that is picked up and carried the same way: the
13819
14042
  // gesture is theirs, and this one is left with the axes none of them walks —
13820
14043
  // none at all, most of the time, and then there is no gesture here to read.
13821
- const axesLeftByTravels = axesLeftBy(axes, target, element, DRAG_AXES_ATTRIBUTE);
13822
- const axesLeft = axesLeftByTravels && axesLeftBy(axesLeftByTravels, target, element, DRAG_SOURCE_AXES_ATTRIBUTE);
14044
+ const axesLeftByTravels = axesLeftBy(
14045
+ axes,
14046
+ target,
14047
+ element,
14048
+ DRAG_AXES_ATTRIBUTE,
14049
+ );
14050
+ const axesLeft =
14051
+ axesLeftByTravels &&
14052
+ axesLeftBy(axesLeftByTravels, target, element, DRAG_SOURCE_AXES_ATTRIBUTE);
13823
14053
  if (!axesLeft) {
13824
14054
  return null;
13825
14055
  }
@@ -13835,6 +14065,7 @@ const startDragToTravel = (pointerDownEvent, {
13835
14065
  let travel = null;
13836
14066
  let dragGesture = null;
13837
14067
  let over = false;
14068
+
13838
14069
  const finish = () => {
13839
14070
  if (over) {
13840
14071
  return;
@@ -13848,7 +14079,7 @@ const startDragToTravel = (pointerDownEvent, {
13848
14079
  // A press that never became a travel: the intent never resolved, or the axis
13849
14080
  // it leaned on is not one this box walks. Nothing was painted and nothing has
13850
14081
  // to be put back — the caller is only told so it can forget the gesture.
13851
- const onPressOver = pointerEvent => {
14082
+ const onPressOver = (pointerEvent) => {
13852
14083
  if (pointerEvent.pointerId !== pointerDownEvent.pointerId || travel) {
13853
14084
  return;
13854
14085
  }
@@ -13869,8 +14100,11 @@ const startDragToTravel = (pointerDownEvent, {
13869
14100
  // layout the gesture computes for something being carried: nothing is being
13870
14101
  // carried here, and a scroll happening meanwhile must not read as a finger
13871
14102
  // that moved.
13872
- const coveredOn = (axis, gestureInfo) => axis === "x" ? gestureInfo.dragX - gestureInfo.grabX : gestureInfo.dragY - gestureInfo.grabY;
13873
- const pullOf = gestureInfo => {
14103
+ const coveredOn = (axis, gestureInfo) =>
14104
+ axis === "x"
14105
+ ? gestureInfo.dragX - gestureInfo.grabX
14106
+ : gestureInfo.dragY - gestureInfo.grabY;
14107
+ const pullOf = (gestureInfo) => {
13874
14108
  const covered = coveredOn(travel.axis, gestureInfo);
13875
14109
  return travel.slack + (covered - travel.origin);
13876
14110
  };
@@ -13884,7 +14118,7 @@ const startDragToTravel = (pointerDownEvent, {
13884
14118
  const next = onEdge({
13885
14119
  axis: travel.axis,
13886
14120
  sign,
13887
- event: gestureInfo.dragEvent
14121
+ event: gestureInfo.dragEvent,
13888
14122
  });
13889
14123
  if (!next || !next.size) {
13890
14124
  return null;
@@ -13902,6 +14136,7 @@ const startDragToTravel = (pointerDownEvent, {
13902
14136
  travel.origin = coveredOn(travel.axis, gestureInfo) - pulled;
13903
14137
  return pulled;
13904
14138
  };
14139
+
13905
14140
  const controller = createDragGestureController({
13906
14141
  // The threshold is left at its default and never crossed: what says this
13907
14142
  // press has become a gesture is the intent module below, which calls
@@ -13916,7 +14151,7 @@ const startDragToTravel = (pointerDownEvent, {
13916
14151
  onDragStart: () => {
13917
14152
  document.documentElement.setAttribute(GESTURE_ATTRIBUTE, "");
13918
14153
  },
13919
- onDrag: gestureInfo => {
14154
+ onDrag: (gestureInfo) => {
13920
14155
  // Releasing a gesture reports one last move, so giving one up would come
13921
14156
  // back through here and give it up again, forever.
13922
14157
  if (over) {
@@ -13982,7 +14217,7 @@ const startDragToTravel = (pointerDownEvent, {
13982
14217
  axis,
13983
14218
  sign,
13984
14219
  target,
13985
- event: gestureInfo.dragEvent
14220
+ event: gestureInfo.dragEvent,
13986
14221
  });
13987
14222
  if (!started || !started.size) {
13988
14223
  giveUp();
@@ -14003,17 +14238,21 @@ const startDragToTravel = (pointerDownEvent, {
14003
14238
  // established before the press (see immediate): there was no
14004
14239
  // threshold to cross, so every pixel since the grab is the hand's
14005
14240
  // and is owed to it.
14006
- origin: immediate ? 0 : covered > DRAG_START_THRESHOLD ? DRAG_START_THRESHOLD : covered < -DRAG_START_THRESHOLD ? -DRAG_START_THRESHOLD : covered,
14007
- pulled: started.slack || 0
14241
+ origin: immediate
14242
+ ? 0
14243
+ : covered > DRAG_START_THRESHOLD
14244
+ ? DRAG_START_THRESHOLD
14245
+ : covered < -DRAG_START_THRESHOLD
14246
+ ? -DRAG_START_THRESHOLD
14247
+ : covered,
14248
+ pulled: started.slack || 0,
14008
14249
  };
14009
14250
  document.documentElement.setAttribute(WALKING_ATTRIBUTE, axis);
14010
14251
  // The travel exists: from here the pointer is this box's, and it is
14011
14252
  // followed wherever it goes.
14012
14253
  dragGesture.capturePointer();
14013
14254
  }
14014
- const {
14015
- axis
14016
- } = travel;
14255
+ const { axis } = travel;
14017
14256
  let pulled = pullOf(gestureInfo);
14018
14257
  // Which side is being pulled in: dragging to the right brings in what is
14019
14258
  // on the left, which is what comes BEFORE.
@@ -14051,7 +14290,8 @@ const startDragToTravel = (pointerDownEvent, {
14051
14290
  // from where the finger IS once it has reached the end, so turning
14052
14291
  // around moves the picture at once instead of first walking back
14053
14292
  // over the distance the hand went too far.
14054
- travel.origin = coveredOn(axis, gestureInfo) - (pulled - travel.slack);
14293
+ travel.origin =
14294
+ coveredOn(axis, gestureInfo) - (pulled - travel.slack);
14055
14295
  } else {
14056
14296
  pulled = relayed;
14057
14297
  size = travel.size;
@@ -14064,46 +14304,42 @@ const startDragToTravel = (pointerDownEvent, {
14064
14304
  pulled,
14065
14305
  size,
14066
14306
  progress: pulled / size,
14067
- event: gestureInfo.dragEvent
14307
+ event: gestureInfo.dragEvent,
14068
14308
  });
14069
14309
  },
14070
- onRelease: gestureInfo => {
14310
+ onRelease: (gestureInfo) => {
14071
14311
  if (over || !travel) {
14072
14312
  return;
14073
14313
  }
14074
14314
  finish();
14075
- const {
14076
- axis,
14077
- size,
14078
- pulled,
14079
- slack
14080
- } = travel;
14315
+ const { axis, size, pulled, slack } = travel;
14081
14316
  const towardsSomething = pulled > 0 ? travel.travelBack : travel.travelOn;
14082
- const velocity = axis === "x" ? gestureInfo.velocityX : gestureInfo.velocityY;
14317
+ const velocity =
14318
+ axis === "x" ? gestureInfo.velocityX : gestureInfo.velocityY;
14083
14319
  // A gesture taken away rather than let go of (the browser scrolling
14084
14320
  // something else, a call coming in, another gesture taking the pointer)
14085
14321
  // said nothing: things go back.
14086
14322
  const releaseEvent = gestureInfo.releaseEvent || gestureInfo.dragEvent;
14087
- const {
14088
- cancelled
14089
- } = gestureInfo;
14323
+ const { cancelled } = gestureInfo;
14090
14324
  onEnd({
14091
14325
  axis,
14092
14326
  pulled,
14093
14327
  size,
14094
14328
  sign: pulled > 0 ? 1 : -1,
14095
- travels: !cancelled && travelsAfter({
14096
- pulled,
14097
- slack,
14098
- size,
14099
- velocity,
14100
- towardsSomething,
14101
- commitRatio
14102
- }),
14329
+ travels:
14330
+ !cancelled &&
14331
+ travelsAfter({
14332
+ pulled,
14333
+ slack,
14334
+ size,
14335
+ velocity,
14336
+ towardsSomething,
14337
+ commitRatio,
14338
+ }),
14103
14339
  cancelled,
14104
- event: releaseEvent
14340
+ event: releaseEvent,
14105
14341
  });
14106
- }
14342
+ },
14107
14343
  });
14108
14344
 
14109
14345
  // When a press becomes a gesture, and by which rule. A travel is a swipe, so
@@ -14125,7 +14361,7 @@ const startDragToTravel = (pointerDownEvent, {
14125
14361
  // would be taken away from whoever else is reading the same press for
14126
14362
  // gestures that give themselves up one event later. It is claimed once
14127
14363
  // the travel exists, in onDrag below.
14128
- pointerCaptureDeferred: true
14364
+ pointerCaptureDeferred: true,
14129
14365
  });
14130
14366
  return dragGesture;
14131
14367
  };
@@ -14138,16 +14374,17 @@ const startDragToTravel = (pointerDownEvent, {
14138
14374
  } else {
14139
14375
  dragAfterIntent(pointerDownEvent, grab, {
14140
14376
  longPress: false,
14141
- threshold: DRAG_START_THRESHOLD
14377
+ threshold: DRAG_START_THRESHOLD,
14142
14378
  });
14143
14379
  }
14144
14380
  window.addEventListener("pointerup", onPressOver);
14145
14381
  window.addEventListener("pointercancel", onPressOver);
14382
+
14146
14383
  return {
14147
14384
  stop: () => {
14148
14385
  finish();
14149
14386
  dragGesture?.release();
14150
- }
14387
+ },
14151
14388
  };
14152
14389
  };
14153
14390
 
@@ -14219,11 +14456,9 @@ const WHEEL_REGROW_RUN = 2;
14219
14456
  * scrolls, and pushing content to the right reveals its left.
14220
14457
  * @returns {() => void} stop listening.
14221
14458
  */
14222
- const watchWheelTravel = (element, {
14223
- axes = "xy",
14224
- onStep
14225
- }) => {
14459
+ const watchWheelTravel = (element, { axes = "xy", onStep }) => {
14226
14460
  let gesture = null;
14461
+
14227
14462
  const forgetGesture = () => {
14228
14463
  gesture = null;
14229
14464
  document.documentElement.removeAttribute(GESTURE_ATTRIBUTE);
@@ -14236,10 +14471,8 @@ const watchWheelTravel = (element, {
14236
14471
  // pseudo-elements are told about pointer-events. Heard on the box alone, a
14237
14472
  // gesture that sets a travel off loses every event after the first — and the
14238
14473
  // page scrolls behind the travel with everything that was not taken.
14239
- const isOverElement = wheelEvent => {
14240
- const {
14241
- target
14242
- } = wheelEvent;
14474
+ const isOverElement = (wheelEvent) => {
14475
+ const { target } = wheelEvent;
14243
14476
  if (element.contains(target)) {
14244
14477
  return true;
14245
14478
  }
@@ -14253,26 +14486,22 @@ const watchWheelTravel = (element, {
14253
14486
  if (!target.contains(element)) {
14254
14487
  return false;
14255
14488
  }
14256
- const {
14257
- left,
14258
- right,
14259
- top,
14260
- bottom
14261
- } = element.getBoundingClientRect();
14262
- const {
14263
- clientX,
14264
- clientY
14265
- } = wheelEvent;
14266
- return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom;
14489
+ const { left, right, top, bottom } = element.getBoundingClientRect();
14490
+ const { clientX, clientY } = wheelEvent;
14491
+ return (
14492
+ clientX >= left && clientX <= right && clientY >= top && clientY <= bottom
14493
+ );
14267
14494
  };
14268
- const onWheel = wheelEvent => {
14495
+
14496
+ const onWheel = (wheelEvent) => {
14269
14497
  // The burst is already somebody else's — the box inside this one, a wheel
14270
14498
  // picker, whoever answered its first event. It is theirs to the end of it,
14271
14499
  // wherever the pointer has drifted since (see wheel_gesture.js).
14272
14500
  if (wheelGestureIsTakenFrom(element)) {
14273
14501
  return;
14274
14502
  }
14275
- const axis = Math.abs(wheelEvent.deltaX) > Math.abs(wheelEvent.deltaY) ? "x" : "y";
14503
+ const axis =
14504
+ Math.abs(wheelEvent.deltaX) > Math.abs(wheelEvent.deltaY) ? "x" : "y";
14276
14505
  const delta = axis === "x" ? wheelEvent.deltaX : wheelEvent.deltaY;
14277
14506
  if (!delta) {
14278
14507
  return;
@@ -14297,15 +14526,16 @@ const watchWheelTravel = (element, {
14297
14526
  // it — the same claims a press is read against (see the top of this
14298
14527
  // file), and all of them are answered by giving the gesture up whole:
14299
14528
  // nothing is prevented and the browser scrolls as it would have.
14300
- const {
14301
- target
14302
- } = wheelEvent;
14303
- if (target.closest && isPressExcluded(target, element) || scrollRoomTowards(target, element, axis, sign) ||
14304
- // …plus the third: a box below this one that travels on this axis. Its
14305
- // watcher hears the same wheel event this one does — they all listen at
14306
- // the document — so without this both step, and one push moves two
14307
- // things.
14308
- !axesLeftBy(axis, target, element, WHEEL_AXES_ATTRIBUTE)) {
14529
+ const { target } = wheelEvent;
14530
+ if (
14531
+ (target.closest && isPressExcluded(target, element)) ||
14532
+ scrollRoomTowards(target, element, axis, sign) ||
14533
+ // …plus the third: a box below this one that travels on this axis. Its
14534
+ // watcher hears the same wheel event this one does — they all listen at
14535
+ // the document — so without this both step, and one push moves two
14536
+ // things.
14537
+ !axesLeftBy(axis, target, element, WHEEL_AXES_ATTRIBUTE)
14538
+ ) {
14309
14539
  return;
14310
14540
  }
14311
14541
  gesture = {
@@ -14316,7 +14546,7 @@ const watchWheelTravel = (element, {
14316
14546
  fadeRun: 0,
14317
14547
  growRun: 0,
14318
14548
  faded: false,
14319
- stepped: false
14549
+ stepped: false,
14320
14550
  };
14321
14551
  document.documentElement.setAttribute(GESTURE_ATTRIBUTE, "");
14322
14552
  document.documentElement.setAttribute(WALKING_ATTRIBUTE, axis);
@@ -14345,9 +14575,7 @@ const watchWheelTravel = (element, {
14345
14575
  }
14346
14576
  // …and said on every event on our axis, because a claim nobody renews is a
14347
14577
  // gesture that is over.
14348
- claimWheelGesture(element, {
14349
- onEnd: forgetGesture
14350
- });
14578
+ claimWheelGesture(element, { onEnd: forgetGesture });
14351
14579
  if (sign !== gesture.sign) {
14352
14580
  // Turned around: what was adding up was going the other way.
14353
14581
  gesture.sign = sign;
@@ -14364,11 +14592,7 @@ const watchWheelTravel = (element, {
14364
14592
  // happen pushes harder rather than waiting. Everything a threshold could
14365
14593
  // buy here is bought by what a screen AFTER this one costs.
14366
14594
  gesture.stepped = true;
14367
- onStep({
14368
- axis: gesture.axis,
14369
- sign: gesture.sign,
14370
- event: wheelEvent
14371
- });
14595
+ onStep({ axis: gesture.axis, sign: gesture.sign, event: wheelEvent });
14372
14596
  return;
14373
14597
  }
14374
14598
  const magnitude = Math.abs(delta);
@@ -14393,11 +14617,7 @@ const watchWheelTravel = (element, {
14393
14617
  gesture.faded = false;
14394
14618
  gesture.growRun = 0;
14395
14619
  gesture.pushed = 0;
14396
- onStep({
14397
- axis: gesture.axis,
14398
- sign: gesture.sign,
14399
- event: wheelEvent
14400
- });
14620
+ onStep({ axis: gesture.axis, sign: gesture.sign, event: wheelEvent });
14401
14621
  return;
14402
14622
  }
14403
14623
  if (gesture.fadeRun >= WHEEL_FADE_RUN) {
@@ -14408,20 +14628,15 @@ const watchWheelTravel = (element, {
14408
14628
  return;
14409
14629
  }
14410
14630
  gesture.pushed = 0;
14411
- onStep({
14412
- axis: gesture.axis,
14413
- sign: gesture.sign,
14414
- event: wheelEvent
14415
- });
14631
+ onStep({ axis: gesture.axis, sign: gesture.sign, event: wheelEvent });
14416
14632
  };
14633
+
14417
14634
  document.addEventListener("wheel", onWheel, {
14418
14635
  passive: false,
14419
- capture: true
14636
+ capture: true,
14420
14637
  });
14421
14638
  return () => {
14422
- document.removeEventListener("wheel", onWheel, {
14423
- capture: true
14424
- });
14639
+ document.removeEventListener("wheel", onWheel, { capture: true });
14425
14640
  // Handed back rather than left to lapse: a box that is gone must not hold a
14426
14641
  // gesture the boxes still there are asking about.
14427
14642
  releaseWheelGesture(element);
@@ -14462,11 +14677,13 @@ const isPressExcluded = (target, element) => {
14462
14677
  * @param {Element} element Where the press landed.
14463
14678
  * @returns {boolean}
14464
14679
  */
14465
- const isPressDisputedByDrag = element => {
14680
+ const isPressDisputedByDrag = (element) => {
14466
14681
  if (!element || typeof element.closest !== "function") {
14467
14682
  return false;
14468
14683
  }
14469
- const dragged = element.closest(`[${DRAG_AXES_ATTRIBUTE}],[${DRAG_SOURCE_AXES_ATTRIBUTE}]`);
14684
+ const dragged = element.closest(
14685
+ `[${DRAG_AXES_ATTRIBUTE}],[${DRAG_SOURCE_AXES_ATTRIBUTE}]`,
14686
+ );
14470
14687
  if (!dragged) {
14471
14688
  return false;
14472
14689
  }
@@ -14534,6 +14751,12 @@ const isAncestorOpen = (ancestor) => {
14534
14751
  * own closed-state CSS ([navi-hidden], :not([popover])) for the custom
14535
14752
  * renderers — so nothing inside one answers true here, while everything a
14536
14753
  * trigger keeps on screen does.
14754
+ *
14755
+ * Ask it about an ancestor that is closed AND settled — on mount, or when
14756
+ * setting up a long-lived observer. A surface animating its way out still has a
14757
+ * box for the length of the animation (transition-behavior: allow-discrete on
14758
+ * display/overlay), so asked at the instant one closes this says "on screen"
14759
+ * about something on its way off it.
14537
14760
  */
14538
14761
  const isDisplayedDespiteClosedAncestor = (element) => {
14539
14762
  if (typeof element.checkVisibility !== "function") {
@@ -14681,36 +14904,17 @@ const getWidth = (element) => {
14681
14904
  return width;
14682
14905
  };
14683
14906
 
14684
- installImportMetaCssBuild(import.meta);/**
14685
- * Position Sticky Polyfill
14686
- *
14687
- * This module provides a workaround for position:sticky limitations when used with
14688
- * overflow:auto/hidden parent elements (see https://github.com/w3c/csswg-drafts/issues/865).
14689
- *
14690
- * How it works:
14691
- * 1. Creates a placeholder clone of the sticky element to maintain document flow
14692
- * 2. Positions the real element using fixed positioning relative to viewport
14693
- * 3. Adjusts position on scroll to emulate position:sticky behavior
14694
- * 4. Handles parent boundary detection to keep element within its container
14695
- * 5. Updates dimensions on resize and DOM changes
14696
- *
14697
- * Usage:
14698
- * ```
14699
- * const cleanup = initPositionSticky(element);
14700
- * // Later when no longer needed
14701
- * cleanup();
14702
- * ```
14703
- *
14704
- * The element should have a CSS "top" value specified (e.g., top: 10px).
14705
- */
14706
- const css = /* css */`[data-position-sticky-placeholder] {
14907
+ installImportMetaCssBuild(import.meta);
14908
+
14909
+ const css = /* css */ `[data-position-sticky-placeholder] {
14707
14910
  opacity: 0 !important;
14708
14911
  width: auto !important;
14709
14912
  height: auto !important;
14710
14913
  position: static !important;
14711
14914
  }
14712
14915
  `;
14713
- const initPositionSticky = element => {
14916
+
14917
+ const initPositionSticky = (element) => {
14714
14918
  import.meta.css = [css, "@jsenv/dom/src/position/position_sticky.js"];
14715
14919
  const computedStyle = getComputedStyle(element);
14716
14920
  const topCssValue = computedStyle.top;
@@ -14735,10 +14939,20 @@ const initPositionSticky = element => {
14735
14939
  break;
14736
14940
  }
14737
14941
  const style = getComputedStyle(scrollContainer);
14738
- if (xScrollContainer === null && (style.overflowX === "auto" || style.overflowX === "hidden" || style.overflowX === "scroll")) {
14942
+ if (
14943
+ xScrollContainer === null &&
14944
+ (style.overflowX === "auto" ||
14945
+ style.overflowX === "hidden" ||
14946
+ style.overflowX === "scroll")
14947
+ ) {
14739
14948
  xScrollContainer = scrollContainer;
14740
14949
  }
14741
- if (yScrollContainer === null && (style.overflowY === "auto" || style.overflowY === "hidden" || style.overflowY === "scroll")) {
14950
+ if (
14951
+ yScrollContainer === null &&
14952
+ (style.overflowY === "auto" ||
14953
+ style.overflowY === "hidden" ||
14954
+ style.overflowY === "scroll")
14955
+ ) {
14742
14956
  yScrollContainer = scrollContainer;
14743
14957
  }
14744
14958
  }
@@ -14747,6 +14961,7 @@ const initPositionSticky = element => {
14747
14961
  if (!needsPolyfillX && !needsPolyfillY) {
14748
14962
  return () => {}; // Native sticky will work fine on both axes
14749
14963
  }
14964
+
14750
14965
  const cleanupCallbackSet = new Set();
14751
14966
  const cleanup = () => {
14752
14967
  for (const cleanupCallback of cleanupCallbackSet) {
@@ -14754,6 +14969,7 @@ const initPositionSticky = element => {
14754
14969
  }
14755
14970
  cleanupCallbackSet.clear();
14756
14971
  };
14972
+
14757
14973
  const parentElement = element.parentElement;
14758
14974
  const createPlaceholderClone = () => {
14759
14975
  const clone = element.cloneNode(true);
@@ -14761,13 +14977,16 @@ const initPositionSticky = element => {
14761
14977
  clone.removeAttribute("data-sticky");
14762
14978
  return clone;
14763
14979
  };
14980
+
14764
14981
  let placeholder = createPlaceholderClone();
14765
14982
  parentElement.insertBefore(placeholder, element);
14766
14983
  cleanupCallbackSet.add(() => {
14767
14984
  placeholder.remove();
14768
14985
  });
14986
+
14769
14987
  let width = getWidth(element);
14770
14988
  let height = getHeight(element);
14989
+
14771
14990
  const updateSize = () => {
14772
14991
  const newPlaceholder = createPlaceholderClone();
14773
14992
  parentElement.replaceChild(newPlaceholder, placeholder);
@@ -14776,12 +14995,14 @@ const initPositionSticky = element => {
14776
14995
  height = getHeight(placeholder);
14777
14996
  updatePosition();
14778
14997
  };
14998
+
14779
14999
  const updatePosition = () => {
14780
15000
  // Ensure placeholder dimensions match element
14781
15001
  setStyles(placeholder, {
14782
15002
  width: `${width}px`,
14783
- height: `${height}px`
15003
+ height: `${height}px`,
14784
15004
  });
15005
+
14785
15006
  const placeholderRect = placeholder.getBoundingClientRect();
14786
15007
  const parentRect = parentElement.getBoundingClientRect();
14787
15008
 
@@ -14795,12 +15016,12 @@ const initPositionSticky = element => {
14795
15016
  // -420 <= 250 → stuck → element.style.left = 250px (main's left edge). ✓
14796
15017
  //
14797
15018
  // If no intermediate scroll container exists, use 0 (document/viewport edge).
14798
- const yContainerRect = yScrollContainer ? yScrollContainer.getBoundingClientRect() : {
14799
- top: 0
14800
- };
14801
- const xContainerRect = xScrollContainer ? xScrollContainer.getBoundingClientRect() : {
14802
- left: 0
14803
- };
15019
+ const yContainerRect = yScrollContainer
15020
+ ? yScrollContainer.getBoundingClientRect()
15021
+ : { top: 0 };
15022
+ const xContainerRect = xScrollContainer
15023
+ ? xScrollContainer.getBoundingClientRect()
15024
+ : { left: 0 };
14804
15025
  const topThreshold = yContainerRect.top + top;
14805
15026
  const leftThreshold = xContainerRect.left + left;
14806
15027
 
@@ -14843,6 +15064,7 @@ const initPositionSticky = element => {
14843
15064
  } else {
14844
15065
  leftPosition = placeholderRect.left;
14845
15066
  }
15067
+
14846
15068
  element.style.top = `${topPosition}px`;
14847
15069
  element.style.left = `${Math.round(leftPosition)}px`;
14848
15070
  element.style.width = `${width}px`;
@@ -14855,15 +15077,18 @@ const initPositionSticky = element => {
14855
15077
  element.removeAttribute("data-sticky");
14856
15078
  }
14857
15079
  };
15080
+
14858
15081
  {
14859
15082
  const restorePositionStyle = forceStyles(element, {
14860
15083
  "position": "fixed",
14861
15084
  "z-index": 1,
14862
- "will-change": "transform" // Hint for hardware acceleration
15085
+ "will-change": "transform", // Hint for hardware acceleration
14863
15086
  });
14864
15087
  cleanupCallbackSet.add(restorePositionStyle);
14865
15088
  }
15089
+
14866
15090
  updatePosition();
15091
+
14867
15092
  {
14868
15093
  const handleScroll = () => {
14869
15094
  updatePosition();
@@ -14874,16 +15099,15 @@ const initPositionSticky = element => {
14874
15099
  const listenTargets = new Set(scrollContainerSet);
14875
15100
  listenTargets.add(document.documentElement);
14876
15101
  for (const scrollTarget of listenTargets) {
14877
- scrollTarget.addEventListener("scroll", handleScroll, {
14878
- passive: true
14879
- });
15102
+ scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
14880
15103
  cleanupCallbackSet.add(() => {
14881
15104
  scrollTarget.removeEventListener("scroll", handleScroll, {
14882
- passive: true
15105
+ passive: true,
14883
15106
  });
14884
15107
  });
14885
15108
  }
14886
15109
  }
15110
+
14887
15111
  {
14888
15112
  let animationFrame = null;
14889
15113
  const resizeObserver = new ResizeObserver(() => {
@@ -14902,6 +15126,7 @@ const initPositionSticky = element => {
14902
15126
  animationFrame = null;
14903
15127
  });
14904
15128
  }
15129
+
14905
15130
  {
14906
15131
  const mutationObserver = new MutationObserver(() => {
14907
15132
  updateSize();
@@ -14909,12 +15134,13 @@ const initPositionSticky = element => {
14909
15134
  mutationObserver.observe(element, {
14910
15135
  childList: true,
14911
15136
  subtree: true,
14912
- characterData: true
15137
+ characterData: true,
14913
15138
  });
14914
15139
  cleanupCallbackSet.add(() => {
14915
15140
  mutationObserver.disconnect();
14916
15141
  });
14917
15142
  }
15143
+
14918
15144
  return cleanup;
14919
15145
  };
14920
15146
 
@@ -15191,6 +15417,20 @@ window.addEventListener("resize", (event) => {
15191
15417
  const getVisibleViewportRect = () => {
15192
15418
  const visualViewport = window.visualViewport;
15193
15419
  const documentElement = document.documentElement;
15420
+ // Read FIRST, before the visual viewport just below — and used again in the
15421
+ // branch further down, so it is a read this function needs either way.
15422
+ //
15423
+ // The visual viewport's own numbers are refreshed with the frame's geometry,
15424
+ // and reading the layout viewport is what brings that geometry up to date.
15425
+ // Read them the other way round and the answer is the state as it was before
15426
+ // the caller's own measuring — every caller here measures an element moments
15427
+ // later, with getBoundingClientRect/offsetHeight, which forces exactly that
15428
+ // update. The box would then be measured in one layout state and placed in
15429
+ // another; on a phone opening its keyboard the two are a screen apart, the
15430
+ // popup lands where the pre-keyboard viewport put it, and nothing after
15431
+ // corrects it. Costs no extra layout: it moves the one the caller triggers
15432
+ // anyway rather than adding a second.
15433
+ const layoutViewportHeight = window.innerHeight;
15194
15434
  if (!visualViewport) {
15195
15435
  return {
15196
15436
  left: 0,
@@ -15217,8 +15457,8 @@ const getVisibleViewportRect = () => {
15217
15457
  ) {
15218
15458
  offsetLeft = 0;
15219
15459
  offsetTop = 0;
15220
- if (window.innerHeight > height) {
15221
- height = window.innerHeight;
15460
+ if (layoutViewportHeight > height) {
15461
+ height = layoutViewportHeight;
15222
15462
  }
15223
15463
  }
15224
15464
  return {
@@ -15830,7 +16070,23 @@ const visibleRectEffect = (
15830
16070
  let currentOpenableAncestor = selfOrClosestOpenableAncestor(element);
15831
16071
  while (currentOpenableAncestor) {
15832
16072
  const openableAncestor = currentOpenableAncestor;
15833
- if (!isAncestorOpen(openableAncestor)) {
16073
+ const openableAncestorIsOpen = isAncestorOpen(openableAncestor);
16074
+ // A closed openable that keeps `element` on screen is the *trigger* of
16075
+ // what is closed rather than the thing itself — a picker's root, an
16076
+ // expandable's header, a <summary> all carry aria-expanded/open for a
16077
+ // surface they are not part of, and `element` here belongs to the
16078
+ // façade they keep showing the whole time. Its closed state hides
16079
+ // nothing of this element, so it must not count as a state that does.
16080
+ //
16081
+ // Read once, here, and kept for the effect's lifetime: the layout only
16082
+ // answers this question honestly about an ancestor that is already
16083
+ // closed and settled. Asked at the instant a surface closes, it still
16084
+ // says "on screen" for as long as a close animation keeps a box for it,
16085
+ // which is the opposite answer — so a surface closing later goes on
16086
+ // counting, unasked.
16087
+ const elementIsInAncestorFacade =
16088
+ !openableAncestorIsOpen && isDisplayedDespiteClosedAncestor(element);
16089
+ if (!openableAncestorIsOpen && !elementIsInAncestorFacade) {
15834
16090
  ancestorClosedCount++;
15835
16091
  pauseResizeWatching();
15836
16092
  }
@@ -15839,6 +16095,9 @@ const visibleRectEffect = (
15839
16095
  // eslint-disable-next-line no-loop-func
15840
16096
  ({ isOpen, toggleEvent }) => {
15841
16097
  if (!isOpen) {
16098
+ if (elementIsInAncestorFacade) {
16099
+ return;
16100
+ }
15842
16101
  ancestorClosedCount++;
15843
16102
  pauseResizeWatching();
15844
16103
  // Invalidates check()'s own "did anything actually change"
@@ -15867,7 +16126,7 @@ const visibleRectEffect = (
15867
16126
  );
15868
16127
  return;
15869
16128
  }
15870
- if (ancestorClosedCount > 0) {
16129
+ if (!elementIsInAncestorFacade && ancestorClosedCount > 0) {
15871
16130
  ancestorClosedCount--;
15872
16131
  }
15873
16132
  if (ancestorClosedCount === 0) {
@@ -16889,21 +17148,17 @@ const parseTransitionDurationMs = (element, cssVarName, fallbackMs) => {
16889
17148
  * A second reposition landing mid-animation cancels the pending one and
16890
17149
  * flushes its own registered callbacks immediately (same spirit as a real
16891
17150
  * `transitioncancel`), so nothing is left waiting on a superseded `onEnd`.
16892
- *
16893
- * `commitStyles()` below isn't what makes the final position correct —
16894
- * `applyNewPosition` already sets the specified `left`/`top` before this
16895
- * animation starts, so it takes back over once the active duration elapses
16896
- * regardless. It just makes that explicit instead of relying on `fill:
16897
- * "none"` timing, and drops the finished Animation instead of leaving it.
17151
+ * Every second reposition, animated or not — see
17152
+ * cancelPendingPositionTransition, which applyNewPosition calls on the path
17153
+ * that does not come through here.
17154
+ *
17155
+ * `commitStyles()` below is what the specified `translate` reads once the
17156
+ * active duration elapses, so it must only ever run while this animation is
17157
+ * still the current placement: it writes THIS animation's target, which a
17158
+ * placement landing after it has already superseded.
16898
17159
  */
16899
17160
  const notifyPositionTransition = (element, animation) => {
16900
- const pending = pendingPositionTransitions.get(element);
16901
- if (pending) {
16902
- pending.animation.cancel();
16903
- for (const callback of pending.endCallbacks) {
16904
- callback();
16905
- }
16906
- }
17161
+ cancelPendingPositionTransition(element);
16907
17162
  const endCallbacks = [];
16908
17163
  dispatchCustomEvent(element, "navi_position_transition", {
16909
17164
  onEnd: (callback) => {
@@ -16933,6 +17188,31 @@ const notifyPositionTransition = (element, animation) => {
16933
17188
  });
16934
17189
  };
16935
17190
 
17191
+ /**
17192
+ * Ends the placement animation in flight on `element`, if any, and flushes what
17193
+ * was waiting on it — what a new placement owes the old one, whether or not it
17194
+ * animates itself.
17195
+ *
17196
+ * The non-animated case is the one worth naming: a placement that just writes
17197
+ * `translate` leaves an animation started a moment earlier running, and that
17198
+ * animation commits ITS target when it ends, on top of the value written since.
17199
+ * The box then lands where a viewport that no longer exists put it, and nothing
17200
+ * comes after to correct it — a phone opening its keyboard fires exactly this
17201
+ * sequence, an animated "resize" followed by focus/size deliveries that do not
17202
+ * animate.
17203
+ */
17204
+ const cancelPendingPositionTransition = (element) => {
17205
+ const pending = pendingPositionTransitions.get(element);
17206
+ if (!pending) {
17207
+ return;
17208
+ }
17209
+ pendingPositionTransitions.delete(element);
17210
+ pending.animation.cancel();
17211
+ for (const callback of pending.endCallbacks) {
17212
+ callback();
17213
+ }
17214
+ };
17215
+
16936
17216
  /**
16937
17217
  * Applies a `pickPositionRelativeTo` result to `element`, as a `translate`.
16938
17218
  *
@@ -17055,6 +17335,8 @@ const applyNewPosition = (
17055
17335
  },
17056
17336
  );
17057
17337
  notifyPositionTransition(element, animation);
17338
+ } else {
17339
+ cancelPendingPositionTransition(element);
17058
17340
  }
17059
17341
  // The specified translate is set to its final target right away,
17060
17342
  // regardless of `shouldTransition` — the animation above only plays the