@jsenv/dom 0.17.9 → 0.17.10

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 +1379 -968
  2. package/package.json +2 -2
package/dist/jsenv_dom.js CHANGED
@@ -4257,6 +4257,187 @@ const createPreviousNodeIterator = (fromNode, rootNode, skipRoot = null) => {
4257
4257
  };
4258
4258
  };
4259
4259
 
4260
+ /**
4261
+ * The click a gesture leaves behind.
4262
+ *
4263
+ * A press that turned into something else — an object carried, a screen swiped,
4264
+ * a menu opened by holding still — still ends with a `pointerup`, and the
4265
+ * browser follows that with a `click` on whatever the pointer was over. On a
4266
+ * link or a button that click means "follow me", which is not what the hand
4267
+ * asked for: the press was already answered, by the gesture.
4268
+ *
4269
+ * So it is swallowed, once, in capture on the document — before any handler an
4270
+ * element may have, and without anyone having to know which element that is.
4271
+ */
4272
+
4273
+ /**
4274
+ * Swallows the next click, for a gesture that has just answered the press.
4275
+ *
4276
+ * @returns {() => void} the gesture is over. The suppressor cannot be taken
4277
+ * down with it — the click is dispatched AFTER the pointerup that ends the
4278
+ * gesture, so it would be gone one event too early, and the drag would end on
4279
+ * the link it started from being followed. It goes once it has swallowed a
4280
+ * click, or at the next press if the gesture produced none: a click is always
4281
+ * preceded by a press, so a suppressor that outlives one press can never
4282
+ * reach the click of another.
4283
+ */
4284
+ const suppressClickAfterGesture = () => {
4285
+ const suppressClick = (clickEvent) => {
4286
+ clickEvent.stopPropagation();
4287
+ clickEvent.preventDefault();
4288
+ stopSuppressing();
4289
+ };
4290
+ const stopSuppressing = () => {
4291
+ document.removeEventListener("click", suppressClick, { capture: true });
4292
+ document.removeEventListener("pointerdown", stopSuppressing, {
4293
+ capture: true,
4294
+ });
4295
+ };
4296
+ document.addEventListener("click", suppressClick, { capture: true });
4297
+ return () => {
4298
+ document.addEventListener("pointerdown", stopSuppressing, {
4299
+ capture: true,
4300
+ });
4301
+ };
4302
+ };
4303
+
4304
+ /**
4305
+ * A press that says something by NOT moving.
4306
+ *
4307
+ * A finger landing on an element is ambiguous — it may be a tap, a scroll, a
4308
+ * swipe — and the one unambiguous signal a finger can give is staying still:
4309
+ * travel is exactly what a scroll looks like, so it cannot be the sign. This
4310
+ * owns that wait, and only that: what the press then means (an object picked
4311
+ * up, a menu opened) belongs to whoever asked for it.
4312
+ *
4313
+ * The wait also has to hold off the system's own answer to the same gesture: a
4314
+ * FINGER held long enough IS the context-menu gesture, and Android's menu (around
4315
+ * 500ms) or iOS's callout lands a tenth of a second after the press was answered
4316
+ * here. The half of that which is an event is refused below; the half that is not
4317
+ * (iOS selecting the word under the finger) is a stylesheet the caller writes on
4318
+ * its own elements — `-webkit-touch-callout: none` has to be true before the
4319
+ * finger lands, so it cannot be set from here.
4320
+ *
4321
+ * A mouse is a different matter and is left alone: its context menu comes from
4322
+ * the other button, not from this press, and refusing it would take the browser's
4323
+ * menu away from an element for no reason.
4324
+ */
4325
+
4326
+ /**
4327
+ * Waits for a press to be held still, then tells the caller.
4328
+ *
4329
+ * @param {PointerEvent} pressEvent The `pointerdown` that may become a hold.
4330
+ * @param {object} options
4331
+ * @param {number} [options.delay=400] How long (ms) the pointer must stay down.
4332
+ * Kept under the system context-menu delay so the press is answered before
4333
+ * the menu would have opened.
4334
+ * @param {number} [options.slop=8] How far (px) the pointer may drift during
4335
+ * the wait — beyond it the finger is going somewhere, and a press answered in
4336
+ * passing is a press nobody made.
4337
+ * @param {function} [options.onPressStart] The wait began (a cue that the press
4338
+ * counts).
4339
+ * @param {function} [options.onPressCancel] The pointer moved or lifted before
4340
+ * the wait was over.
4341
+ * @param {(pressEvent: PointerEvent, handle: {endPress: () => void}) => void} options.onPressHeld
4342
+ * The wait completed. Whatever the press now means outlives this call — an
4343
+ * object is being carried, a menu is open under the finger — so the caller
4344
+ * owns the end of it and says when with `endPress`, which is what gives the
4345
+ * context menu back.
4346
+ * @returns {{ cancel: () => void }}
4347
+ */
4348
+ const waitForPressHeld = (
4349
+ pressEvent,
4350
+ { delay = 400, slop = 8, onPressStart, onPressCancel, onPressHeld },
4351
+ ) => {
4352
+ const { pointerId, clientX, clientY } = pressEvent;
4353
+
4354
+ const pressCleanupCallbacks = [];
4355
+ const endPress = () => {
4356
+ for (const pressCleanupCallback of pressCleanupCallbacks) {
4357
+ pressCleanupCallback();
4358
+ }
4359
+ pressCleanupCallbacks.length = 0;
4360
+ };
4361
+
4362
+ /* A FINGER held down is the system's own context-menu gesture, and the menu it
4363
+ raises lands on top of the answer this press was already given. A MOUSE is
4364
+ not: its context menu comes from the other button, has nothing to do with
4365
+ this press, and is the user asking for the browser's menu — so it is left
4366
+ alone, and only a touch press refuses it.
4367
+ The listener goes on window, in capture: what answers the press may cover the
4368
+ page (a drag backdrop, a popup), and the contextmenu event is then aimed at
4369
+ that instead of at the element pressed. */
4370
+ if (pressEvent.pointerType === "touch") {
4371
+ const preventContextMenu = (contextMenuEvent) => {
4372
+ contextMenuEvent.preventDefault();
4373
+ };
4374
+ window.addEventListener("contextmenu", preventContextMenu, true);
4375
+ pressCleanupCallbacks.push(() => {
4376
+ window.removeEventListener("contextmenu", preventContextMenu, true);
4377
+ });
4378
+ }
4379
+
4380
+ const countdownCleanupCallbacks = [];
4381
+ const endCountdown = () => {
4382
+ for (const countdownCleanupCallback of countdownCleanupCallbacks) {
4383
+ countdownCleanupCallback();
4384
+ }
4385
+ countdownCleanupCallbacks.length = 0;
4386
+ };
4387
+
4388
+ const timeout = setTimeout(() => {
4389
+ endCountdown();
4390
+ onPressHeld(pressEvent, { endPress });
4391
+ }, delay);
4392
+ countdownCleanupCallbacks.push(() => {
4393
+ clearTimeout(timeout);
4394
+ });
4395
+
4396
+ const cancelPress = (pointerEvent) => {
4397
+ endCountdown();
4398
+ endPress();
4399
+ onPressCancel?.(pointerEvent);
4400
+ };
4401
+ const onPointerMove = (pointerMoveEvent) => {
4402
+ if (pointerMoveEvent.pointerId !== pointerId) {
4403
+ return;
4404
+ }
4405
+ const xDrift = Math.abs(pointerMoveEvent.clientX - clientX);
4406
+ const yDrift = Math.abs(pointerMoveEvent.clientY - clientY);
4407
+ if (xDrift < slop && yDrift < slop) {
4408
+ return;
4409
+ }
4410
+ // The finger is going somewhere: it is scrolling the page, or running down
4411
+ // the list. Letting the countdown survive would answer a press in passing.
4412
+ cancelPress(pointerMoveEvent);
4413
+ };
4414
+ const onPointerEnd = (pointerEndEvent) => {
4415
+ if (pointerEndEvent.pointerId !== pointerId) {
4416
+ return;
4417
+ }
4418
+ cancelPress(pointerEndEvent);
4419
+ };
4420
+ // On window rather than on the element: the finger can leave it, and the
4421
+ // element itself can be taken out of the document while the press is waiting.
4422
+ window.addEventListener("pointermove", onPointerMove);
4423
+ window.addEventListener("pointerup", onPointerEnd);
4424
+ window.addEventListener("pointercancel", onPointerEnd);
4425
+ countdownCleanupCallbacks.push(() => {
4426
+ window.removeEventListener("pointermove", onPointerMove);
4427
+ window.removeEventListener("pointerup", onPointerEnd);
4428
+ window.removeEventListener("pointercancel", onPointerEnd);
4429
+ });
4430
+
4431
+ onPressStart?.(pressEvent);
4432
+
4433
+ return {
4434
+ cancel: () => {
4435
+ endCountdown();
4436
+ endPress();
4437
+ },
4438
+ };
4439
+ };
4440
+
4260
4441
  const activeElementSignal = signal(
4261
4442
  typeof document === "object" ? document.activeElement : undefined,
4262
4443
  );
@@ -8296,36 +8477,8 @@ const createDragGestureController = (options = {}) => {
8296
8477
  return dragData;
8297
8478
  };
8298
8479
  const markAsStarted = () => {
8299
- // Suppress the click that the browser fires after pointerup following a real drag.
8300
- // The capture phase runs before any element onClick handler.
8301
- const suppressClick = clickEvent => {
8302
- clickEvent.stopPropagation();
8303
- clickEvent.preventDefault();
8304
- stopSuppressingClick();
8305
- };
8306
- // That click is dispatched AFTER the pointerup that ends the drag, so
8307
- // this cannot be taken down with the gesture — it would be gone one event
8308
- // too early, and the drag would end on the link it started from being
8309
- // followed. It goes once it has swallowed the click, or at the next press
8310
- // if the drag produced none: a click is always preceded by a press, so a
8311
- // suppressor that outlives one press can never reach the click of
8312
- // another.
8313
- const stopSuppressingClick = () => {
8314
- document.removeEventListener("click", suppressClick, {
8315
- capture: true
8316
- });
8317
- document.removeEventListener("pointerdown", stopSuppressingClick, {
8318
- capture: true
8319
- });
8320
- };
8321
- document.addEventListener("click", suppressClick, {
8322
- capture: true
8323
- });
8324
- addReleaseCallback(() => {
8325
- document.addEventListener("pointerdown", stopSuppressingClick, {
8326
- capture: true
8327
- });
8328
- });
8480
+ const clickSuppressionIsOver = suppressClickAfterGesture();
8481
+ addReleaseCallback(clickSuppressionIsOver);
8329
8482
  // Everything this gesture puts on the document is in place, and undoable,
8330
8483
  // BEFORE anybody is told it started: a listener may end the gesture from
8331
8484
  // inside this very notification — that is how a press becomes a drag (see
@@ -8668,8 +8821,9 @@ installImportMetaCssBuild(import.meta);/**
8668
8821
  *
8669
8822
  * A pointer going down on a draggable element is ambiguous — it may be a click,
8670
8823
  * a text selection, a scroll, or a drag — and starting the gesture right away
8671
- * would steal all the others. This module owns the wait that resolves the
8672
- * ambiguity, and only then hands over to the real gesture.
8824
+ * would steal all the others. This module picks which signal resolves the
8825
+ * ambiguity for the pointer at hand, and only then hands over to the real
8826
+ * gesture.
8673
8827
  *
8674
8828
  * There is one gesture, with a trigger per pointer:
8675
8829
  * - a dedicated handle ([data-drag-handle]) says it outright: drag on contact
@@ -8822,94 +8976,26 @@ const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
8822
8976
  onPressCancel,
8823
8977
  onPress
8824
8978
  }) => {
8825
- const {
8826
- pointerId,
8827
- clientX,
8828
- clientY
8829
- } = grabEvent;
8830
- const pressCleanupCallbacks = [];
8831
- const endPress = () => {
8832
- for (const pressCleanupCallback of pressCleanupCallbacks) {
8833
- pressCleanupCallback();
8834
- }
8835
- pressCleanupCallbacks.length = 0;
8836
- };
8837
-
8838
- /*
8839
- * A press held long enough IS the system's context-menu gesture: Android opens
8840
- * its menu around 500ms, iOS its callout — both a tenth of a second after the
8841
- * object has been picked up, landing on top of something the finger is already
8842
- * carrying.
8843
- * The listener goes on window, in capture: once the gesture runs, the drag
8844
- * backdrop covers the page, so the contextmenu event is aimed at the backdrop
8845
- * and never reaches the element being dragged.
8846
- * It is removed on release — a right click with a mouse remains a right click.
8847
- */
8848
- const preventContextMenu = contextMenuEvent => {
8849
- contextMenuEvent.preventDefault();
8850
- };
8851
- window.addEventListener("contextmenu", preventContextMenu, true);
8852
- pressCleanupCallbacks.push(() => {
8853
- window.removeEventListener("contextmenu", preventContextMenu, true);
8854
- });
8855
- const countdownCleanupCallbacks = [];
8856
- const endCountdown = () => {
8857
- for (const countdownCleanupCallback of countdownCleanupCallbacks) {
8858
- countdownCleanupCallback();
8859
- }
8860
- countdownCleanupCallbacks.length = 0;
8861
- };
8862
- const timeout = setTimeout(() => {
8863
- endCountdown();
8864
- onPress?.(grabEvent);
8865
- // Scrolling is taken away by the gesture itself, from the moment it starts
8866
- // (see markAsStarted in drag_gesture.js) — one place refuses the touchmove,
8867
- // for every way a drag can begin.
8868
- const dragGesture = startDragGesture(dragGestureInitializer);
8869
- if (!dragGesture) {
8870
- endPress();
8871
- return;
8872
- }
8873
- dragGesture.addReleaseCallback(endPress);
8874
- }, longPressDelay);
8875
- countdownCleanupCallbacks.push(() => {
8876
- clearTimeout(timeout);
8877
- });
8878
- const cancelPress = pointerEvent => {
8879
- endCountdown();
8880
- endPress();
8881
- onPressCancel?.(pointerEvent);
8882
- };
8883
- const onPointerMove = pointerMoveEvent => {
8884
- if (pointerMoveEvent.pointerId !== pointerId) {
8885
- return;
8886
- }
8887
- const xDrift = Math.abs(pointerMoveEvent.clientX - clientX);
8888
- const yDrift = Math.abs(pointerMoveEvent.clientY - clientY);
8889
- if (xDrift < longPressSlop && yDrift < longPressSlop) {
8890
- return;
8891
- }
8892
- // The finger is going somewhere: it is scrolling the page, or running down
8893
- // the list. Letting the countdown survive would unhook an object in passing.
8894
- cancelPress(pointerMoveEvent);
8895
- };
8896
- const onPointerEnd = pointerEndEvent => {
8897
- if (pointerEndEvent.pointerId !== pointerId) {
8898
- return;
8979
+ waitForPressHeld(grabEvent, {
8980
+ delay: longPressDelay,
8981
+ slop: longPressSlop,
8982
+ onPressStart,
8983
+ onPressCancel,
8984
+ onPressHeld: (pressEvent, {
8985
+ endPress
8986
+ }) => {
8987
+ onPress?.(pressEvent);
8988
+ // Scrolling is taken away by the gesture itself, from the moment it starts
8989
+ // (see markAsStarted in drag_gesture.js) one place refuses the touchmove,
8990
+ // for every way a drag can begin.
8991
+ const dragGesture = startDragGesture(dragGestureInitializer);
8992
+ if (!dragGesture) {
8993
+ endPress();
8994
+ return;
8995
+ }
8996
+ dragGesture.addReleaseCallback(endPress);
8899
8997
  }
8900
- cancelPress(pointerEndEvent);
8901
- };
8902
- // On window rather than on the element: the finger can leave it, and the
8903
- // element itself can be taken out of the document while the press is waiting.
8904
- window.addEventListener("pointermove", onPointerMove);
8905
- window.addEventListener("pointerup", onPointerEnd);
8906
- window.addEventListener("pointercancel", onPointerEnd);
8907
- countdownCleanupCallbacks.push(() => {
8908
- window.removeEventListener("pointermove", onPointerMove);
8909
- window.removeEventListener("pointerup", onPointerEnd);
8910
- window.removeEventListener("pointercancel", onPointerEnd);
8911
8998
  });
8912
- onPressStart?.(grabEvent);
8913
8999
  };
8914
9000
 
8915
9001
  /**
@@ -10609,773 +10695,413 @@ const roundForConstraints = (value) => {
10609
10695
  return Math.round(value * 100) / 100;
10610
10696
  };
10611
10697
 
10612
- const applyStickyFrontiersToAutoScrollArea = (
10613
- autoScrollArea,
10614
- { direction, scrollContainer, dragName },
10698
+ /**
10699
+ * Detects the drop target based on what element is actually under the mouse cursor.
10700
+ * Uses document.elementsFromPoint() to respect visual stacking order naturally.
10701
+ *
10702
+ * @param {Object} gestureInfo - Gesture information
10703
+ * @param {Element[]} targetElements - Array of potential drop target elements
10704
+ * @param {object} [options]
10705
+ * @param {Element} [options.dragElement] - The element being dragged. When provided and
10706
+ * `fallbackToEdge` is true, used to compute the fallback rect.
10707
+ * @param {boolean} [options.fallbackToEdge=false] - When true and the drag element does
10708
+ * not intersect any target, falls back to the first item (if above all items) or the
10709
+ * last item (if below all items) so there is always a valid drop target at list edges.
10710
+ * @returns {Object|null} Drop target info with elementSide or null if no valid target found
10711
+ */
10712
+ const getDropTargetInfo = (
10713
+ gestureInfo,
10714
+ targetElements,
10715
+ { fallbackToEdge = false } = {},
10615
10716
  ) => {
10616
- let { left, right, top, bottom } = autoScrollArea;
10617
-
10618
- if (direction.x) {
10619
- const horizontalStickyFrontiers = createStickyFrontierOnAxis(
10620
- scrollContainer,
10621
- {
10622
- name: dragName,
10623
- scrollContainer,
10624
- primarySide: "left",
10625
- oppositeSide: "right",
10626
- },
10627
- );
10628
- for (const horizontalStickyFrontier of horizontalStickyFrontiers) {
10629
- const { side, bounds, element } = horizontalStickyFrontier;
10630
- if (side === "left") {
10631
- if (bounds.right <= left) {
10632
- continue;
10633
- }
10634
- left = bounds.right;
10635
- continue;
10636
- }
10637
- // right
10638
- if (bounds.left >= right) {
10639
- continue;
10640
- }
10641
- right = bounds.left;
10717
+ const dragElement = gestureInfo.elementImpacted || gestureInfo.element;
10718
+ const dragElementRect = dragElement.getBoundingClientRect();
10719
+ const intersectingTargets = [];
10720
+ let someTargetIsCol;
10721
+ let someTargetIsTr;
10722
+ for (const targetElement of targetElements) {
10723
+ const targetRect = targetElement.getBoundingClientRect();
10724
+ if (!rectangleAreIntersecting(dragElementRect, targetRect)) {
10642
10725
  continue;
10643
10726
  }
10644
- }
10645
-
10646
- if (direction.y) {
10647
- const verticalStickyFrontiers = createStickyFrontierOnAxis(
10648
- scrollContainer,
10649
- {
10650
- name: dragName,
10651
- scrollContainer,
10652
- primarySide: "top",
10653
- oppositeSide: "bottom",
10654
- },
10655
- );
10656
- for (const verticalStickyFrontier of verticalStickyFrontiers) {
10657
- const { side, bounds, element } = verticalStickyFrontier;
10658
-
10659
- // Frontier acts as a top barrier - constrains from the bottom edge of the frontier
10660
- if (side === "top") {
10661
- if (bounds.bottom <= top) {
10662
- continue;
10663
- }
10664
- top = bounds.bottom;
10665
- continue;
10666
- }
10667
-
10668
- // Frontier acts as a bottom barrier - constrains from the top edge of the frontier
10669
- if (bounds.top >= bottom) {
10670
- continue;
10671
- }
10672
- bottom = bounds.top;
10673
- continue;
10727
+ if (!someTargetIsCol && targetElement.tagName === "COL") {
10728
+ someTargetIsCol = true;
10729
+ }
10730
+ if (!someTargetIsTr && targetElement.tagName === "TR") {
10731
+ someTargetIsTr = true;
10674
10732
  }
10733
+ intersectingTargets.push(targetElement);
10675
10734
  }
10676
10735
 
10677
- return { left, right, top, bottom };
10678
- };
10736
+ if (intersectingTargets.length === 0) {
10737
+ if (fallbackToEdge) {
10738
+ const dragElement = gestureInfo.elementImpacted || gestureInfo.element;
10739
+ const dragElementRect = dragElement.getBoundingClientRect();
10740
+ const firstItem = targetElements[0];
10741
+ const lastItem = targetElements[targetElements.length - 1];
10742
+ if (
10743
+ firstItem &&
10744
+ dragElementRect.bottom < firstItem.getBoundingClientRect().top
10745
+ ) {
10746
+ // Drag element is above all items → treat as hovering the first item from the top.
10747
+ return {
10748
+ element: firstItem,
10749
+ elementSide: { x: "start", y: "start" },
10750
+ index: 0,
10751
+ intersectingIndex: 0,
10752
+ intersecting: [firstItem],
10753
+ };
10754
+ }
10755
+ if (
10756
+ lastItem &&
10757
+ dragElementRect.top > lastItem.getBoundingClientRect().bottom
10758
+ ) {
10759
+ // Drag element is below all items → treat as hovering the last item from the bottom.
10760
+ return {
10761
+ element: lastItem,
10762
+ elementSide: { x: "start", y: "end" },
10763
+ index: targetElements.length - 1,
10764
+ intersectingIndex: 0,
10765
+ intersecting: [lastItem],
10766
+ };
10767
+ }
10768
+ }
10769
+ return null;
10770
+ }
10679
10771
 
10680
- const createStickyFrontierOnAxis = (
10681
- element,
10682
- { name, scrollContainer, primarySide, oppositeSide },
10683
- ) => {
10684
- const primaryAttrName = `data-drag-sticky-${primarySide}-frontier`;
10685
- const oppositeAttrName = `data-drag-sticky-${oppositeSide}-frontier`;
10686
- const frontiers = element.querySelectorAll(
10687
- `[${primaryAttrName}], [${oppositeAttrName}]`,
10688
- );
10689
- const matchingStickyFrontiers = [];
10690
- for (const frontier of frontiers) {
10691
- if (frontier.closest("[data-drag-ignore]")) {
10692
- continue;
10772
+ const dragElementCenterX = dragElementRect.left + dragElementRect.width / 2;
10773
+ const dragElementCenterY = dragElementRect.top + dragElementRect.height / 2;
10774
+ // Clamp coordinates to viewport to avoid issues with elementsFromPoint
10775
+ const viewportWidth = document.documentElement.clientWidth;
10776
+ const viewportHeight = document.documentElement.clientHeight;
10777
+ const clientX =
10778
+ dragElementCenterX < 0
10779
+ ? 0
10780
+ : dragElementCenterX > viewportWidth
10781
+ ? viewportWidth - 1
10782
+ : dragElementCenterX;
10783
+ const clientY =
10784
+ dragElementCenterY < 0
10785
+ ? 0
10786
+ : dragElementCenterY > viewportHeight
10787
+ ? viewportHeight - 1
10788
+ : dragElementCenterY;
10789
+
10790
+ // Find the first target element in the stack (topmost visible target)
10791
+ const elementsUnderDragElement = document.elementsFromPoint(clientX, clientY);
10792
+ let targetElement = null;
10793
+ let targetIndex = -1;
10794
+ let intersectingIndex = -1;
10795
+ for (const element of elementsUnderDragElement) {
10796
+ // First, check if the element itself is a target
10797
+ const directIndex = intersectingTargets.indexOf(element);
10798
+ if (directIndex !== -1) {
10799
+ targetElement = element;
10800
+ intersectingIndex = directIndex;
10801
+ break;
10693
10802
  }
10694
- const hasPrimary = frontier.hasAttribute(primaryAttrName);
10695
- const hasOpposite = frontier.hasAttribute(oppositeAttrName);
10696
- // Check if element has both sides (invalid)
10697
- if (hasPrimary && hasOpposite) {
10698
- const elementSignature = getElementSignature(frontier);
10699
- console.warn(
10700
- `Sticky frontier element (${elementSignature}) has both ${primarySide} and ${oppositeSide} attributes.
10701
- A sticky frontier should only have one side attribute.`,
10702
- );
10803
+ // Special case: if element is <td> or <th> and not in targets,
10804
+ // try to find its corresponding <col> element
10805
+ if (!isTableCell(element)) {
10703
10806
  continue;
10704
10807
  }
10705
- const attrName = hasPrimary ? primaryAttrName : oppositeAttrName;
10706
- const attributeValue = frontier.getAttribute(attrName);
10707
- if (attributeValue && name) {
10708
- const frontierNames = attributeValue.split(",");
10709
- const isMatching = frontierNames.some(
10710
- (frontierName) =>
10711
- frontierName.trim().toLowerCase() === name.toLowerCase(),
10712
- );
10713
- if (!isMatching) {
10714
- continue;
10808
+ try_col: {
10809
+ if (!someTargetIsCol) {
10810
+ break try_col;
10811
+ }
10812
+ const tableCellCol = findTableCellCol(element);
10813
+ if (!tableCellCol) {
10814
+ break try_col;
10815
+ }
10816
+ const colIndex = intersectingTargets.indexOf(tableCellCol);
10817
+ if (colIndex === -1) {
10818
+ break try_col;
10715
10819
  }
10820
+ targetElement = tableCellCol;
10821
+ intersectingIndex = colIndex;
10822
+ break;
10823
+ }
10824
+ try_tr: {
10825
+ if (!someTargetIsTr) {
10826
+ break try_tr;
10827
+ }
10828
+ const tableRow = element.closest("tr");
10829
+ const rowIndex = targetElements.indexOf(tableRow);
10830
+ if (rowIndex === -1) {
10831
+ break try_tr;
10832
+ }
10833
+ targetElement = tableRow;
10834
+ intersectingIndex = intersectingTargets.indexOf(tableRow);
10835
+ break;
10716
10836
  }
10717
- const frontierBounds = getScrollRelativeRect(frontier, scrollContainer);
10718
- const stickyFrontierObject = {
10719
- type: "sticky-frontier",
10720
- element: frontier,
10721
- side: hasPrimary ? primarySide : oppositeSide,
10722
- bounds: frontierBounds,
10723
- name: `sticky_frontier_${hasPrimary ? primarySide : oppositeSide} (${getElementSignature(frontier)})`,
10724
- };
10725
- matchingStickyFrontiers.push(stickyFrontierObject);
10726
10837
  }
10727
- return matchingStickyFrontiers;
10838
+ if (!targetElement) {
10839
+ targetElement = intersectingTargets[0];
10840
+ intersectingIndex = 0;
10841
+ }
10842
+ targetIndex = targetElements.indexOf(targetElement);
10843
+
10844
+ // Determine position within the target for both axes.
10845
+ //
10846
+ // Use the leading edge of the dragged element (in the direction of movement)
10847
+ // compared against the target's center:
10848
+ // - Dragging down: "after" as soon as the bottom crosses the target center.
10849
+ // - Dragging up: "before" as soon as the top crosses the target center.
10850
+ // - Not moving: center-vs-center fallback.
10851
+ //
10852
+ // This gives consistent, predictable thresholds regardless of element size.
10853
+ const targetRect = targetElement.getBoundingClientRect();
10854
+ const targetCenterX = targetRect.left + targetRect.width / 2;
10855
+ const targetCenterY = targetRect.top + targetRect.height / 2;
10856
+ const { intentGoingDown, intentGoingUp, intentGoingRight, intentGoingLeft } =
10857
+ gestureInfo;
10858
+ let sideY;
10859
+ if (intentGoingDown) {
10860
+ sideY = dragElementRect.bottom > targetCenterY ? "end" : "start";
10861
+ } else if (intentGoingUp) {
10862
+ sideY = dragElementRect.top < targetCenterY ? "start" : "end";
10863
+ } else {
10864
+ sideY = dragElementCenterY < targetCenterY ? "start" : "end";
10865
+ }
10866
+ let sideX;
10867
+ if (intentGoingRight) {
10868
+ sideX = dragElementRect.right > targetCenterX ? "end" : "start";
10869
+ } else if (intentGoingLeft) {
10870
+ sideX = dragElementRect.left < targetCenterX ? "start" : "end";
10871
+ } else {
10872
+ sideX = dragElementCenterX < targetCenterX ? "start" : "end";
10873
+ }
10874
+ const result = {
10875
+ // NOTE: avoid relying on `index` in application code. The targetElements
10876
+ // array may be dynamically filtered (e.g. excluding the grabbed element),
10877
+ // making this index inconsistent with the full list. Use `element` instead
10878
+ // and look up its position yourself from your own data source.
10879
+ index: targetIndex,
10880
+ element: targetElement,
10881
+ elementSide: {
10882
+ x: sideX,
10883
+ y: sideY,
10884
+ },
10885
+ // Index within the intersecting subset — could be useful to know how many
10886
+ // elements were overlapping, but rarely needed in practice
10887
+ intersectingIndex,
10888
+ intersecting: intersectingTargets,
10889
+ };
10890
+ return result;
10728
10891
  };
10729
10892
 
10730
- const dragStyleController = createStyleController("drag_to_move");
10893
+ const rectangleAreIntersecting = (r1, r2) => {
10894
+ return !(
10895
+ r2.left > r1.right ||
10896
+ r2.right < r1.left ||
10897
+ r2.top > r1.bottom ||
10898
+ r2.bottom < r1.top
10899
+ );
10900
+ };
10901
+
10902
+ const isTableCell = (el) => {
10903
+ return el.tagName === "TD" || el.tagName === "TH";
10904
+ };
10731
10905
 
10732
10906
  /**
10733
- * Creates a gesture controller that moves elements via drag.
10734
- *
10735
- * Wraps `createDragGestureController` and adds:
10736
- * - Element translation via CSS transform (translate only; other existing transforms are preserved)
10737
- * - Auto-scroll while dragging near scroll-container edges
10738
- * - Constraints (area boundaries, obstacle elements)
10739
- *
10740
- * The returned controller exposes a `grab(options)` / `grabViaPointer(event, options)` method.
10741
- * Key grab options:
10742
- * - `element`: the element whose position drives layout calculations (scroll-container detection,
10743
- * constraints, auto-scroll). Sets `data-grabbed` during the drag.
10744
- * - `referenceElement`: optional sticky-frontier / obstacle reference, defaults to `element`.
10745
- * - `elementToMove`: optional different element to actually translate (e.g. a drag clone).
10746
- * If omitted, `element` is translated. The translate is read from `dragStyleController`
10747
- * at grab time so any pre-existing translate is accumulated rather than reset.
10748
- *
10749
- * A `transform` already on the moved element (rotate, scale…) is preserved and does
10750
- * not disturb the movement. `rotate` and `scale` set as individual CSS properties do:
10751
- * they apply outside `transform`, where nothing the gesture writes can reach them —
10752
- * put those on a child element instead (a warning says so in dev).
10753
- *
10754
- * @param {object} [options]
10755
- * @param {boolean} [options.stickyFrontiers=true]
10756
- * Shrinks the auto-scroll area at sticky boundaries (elements with `data-sticky-left` /
10757
- * `data-sticky-top`).
10758
- * @param {number} [options.autoScrollAreaPadding=0]
10759
- * Extra padding (px) subtracted from each edge of the auto-scroll trigger area.
10760
- * @param {string|object|function} [options.areaConstraint="scroll"]
10761
- * Constrains where the element can be dragged.
10762
- * `"scroll"` — bounded by the full scroll area.
10763
- * `"scrollport"` — bounded by the visible viewport of the scroll container.
10764
- * `"none"` — no area constraint.
10765
- * `{left, top, right, bottom}` — fixed bounds (values may be functions receiving context).
10766
- * `function` — called each drag frame, must return a `{left,top,right,bottom}` object.
10767
- * @param {Element} [options.obstaclesContainer]
10768
- * Container to look for obstacle elements in. Defaults to the scroll container.
10769
- * @param {string} [options.obstacleAttributeName="data-drag-obstacle"]
10770
- * Attribute that marks obstacle elements.
10771
- * @param {boolean} [options.showConstraintFeedbackLine=false]
10772
- * Renders a visual line when the pointer deviates from the element due to constraints.
10773
- * @param {boolean} [options.showDebugMarkers=false]
10774
- * Renders debug markers for constraint regions.
10775
- * @param {"commit"|"cancel"|"cancel-animated"|"manual"} [options.releasePositionEffect="commit"]
10776
- * Controls what happens to the translated position on release.
10777
- * - `"commit"`: bakes the translate into inline styles so the element stays put (default).
10778
- * - `"cancel"`: discards the translate so the element snaps back to its original position.
10779
- * - `"cancel-animated"`: same, travelling back to it over `cancelAnimationDuration`.
10780
- * - `"manual"`: does nothing — the caller is responsible for clearing or committing
10781
- * the transform via `dragStyleController`.
10782
- * @param {number} [options.cancelAnimationDuration=200]
10783
- * Duration (ms) of the way back for `"cancel-animated"`.
10784
- * @param {string} [options.cancelAnimationEasing="ease-out"]
10785
- * Easing of the way back for `"cancel-animated"`.
10786
- * @returns {object} Drag gesture controller with augmented `grab()` / `grabViaPointer()` methods.
10787
- *
10788
- * `gestureInfo` gains `cancelPosition()`, `commitPosition()` and
10789
- * `cancelPositionAnimated({duration, easing})` — the last returns the `Animation`
10790
- * playing the way back (`null` when the element was already home), so a caller
10791
- * on `"manual"` can decide between thrown and put back, and still await the
10792
- * landing.
10907
+ * Find the corresponding <col> element for a given <td> or <th> cell
10908
+ * @param {Element} cellElement - The <td> or <th> element
10909
+ * @param {Element[]} targetColElements - Array of <col> elements to search in
10910
+ * @returns {Element|null} The corresponding <col> element or null if not found
10793
10911
  */
10794
- const createDragToMoveGestureController = ({
10795
- stickyFrontiers = true,
10796
- autoScrollAreaPadding = 0,
10797
- areaConstraint = "scroll",
10798
- obstaclesContainer,
10799
- obstacleAttributeName = "data-drag-obstacle",
10800
- showConstraintFeedbackLine = false,
10801
- showDebugMarkers = false,
10802
- releasePositionEffect = "commit",
10803
- cancelAnimationDuration = 200,
10804
- cancelAnimationEasing = "ease-out",
10805
- ...options
10806
- } = {}) => {
10807
- const initGrabToMoveElement = (
10808
- dragGesture,
10809
- { element, referenceElement, elementToMove, convertScrollablePosition },
10810
- ) => {
10811
- const scrollContainer = dragGesture.gestureInfo.scrollContainer;
10812
-
10813
- const direction = dragGesture.gestureInfo.direction;
10814
- // elementImpacted is either an externally provided elementToMove (e.g. a drag clone)
10815
- const elementImpacted = elementToMove || element;
10816
- // elementImpacted is either an externally provided elementToMove
10817
- // (e.g. a drag clone passed by the caller) or the element itself.
10818
- // Capture any pre-existing translate so we can accumulate on top of it
10819
- // rather than resetting it to zero on the first drag event.
10820
- const transformAtGrab = dragStyleController.getUnderlyingValue(
10821
- elementImpacted,
10822
- "transform",
10823
- );
10824
- const translateXAtGrab = transformAtGrab.translateX;
10825
- const translateYAtGrab = transformAtGrab.translateY;
10826
-
10827
- const cancelPosition = () => {
10828
- dragStyleController.clear(elementImpacted);
10829
- };
10830
- // Reading the transform on either side of the clear is what lets this work
10831
- // without knowing anything about the element: how it looked while held and
10832
- // how it looks once let go are both just computed transforms, and the
10833
- // animation has only to bridge the two.
10834
- const cancelPositionAnimated = ({
10835
- duration = cancelAnimationDuration,
10836
- easing = cancelAnimationEasing,
10837
- } = {}) => {
10838
- const transformWhileHeld = getComputedStyle(elementImpacted).transform;
10839
- cancelPosition();
10840
- const transformAtRest = getComputedStyle(elementImpacted).transform;
10841
- if (transformWhileHeld === transformAtRest) {
10842
- return null;
10843
- }
10844
- // No fill: the element already sits at its resting transform, the
10845
- // animation only replays the way back to it.
10846
- return elementImpacted.animate(
10847
- [{ transform: transformWhileHeld }, { transform: transformAtRest }],
10848
- { duration, easing },
10849
- );
10850
- };
10851
- const commitPosition = () => {
10852
- dragStyleController.commit(elementImpacted);
10853
- };
10854
- dragGesture.gestureInfo.cancelPosition = cancelPosition;
10855
- dragGesture.gestureInfo.cancelPositionAnimated = cancelPositionAnimated;
10856
- dragGesture.gestureInfo.commitPosition = commitPosition;
10857
-
10858
- dragGesture.addReleaseCallback(() => {
10859
- if (releasePositionEffect === "cancel") {
10860
- cancelPosition();
10861
- } else if (releasePositionEffect === "cancel-animated") {
10862
- cancelPositionAnimated();
10863
- } else if (releasePositionEffect === "commit") {
10864
- commitPosition();
10865
- }
10866
- // "manual": caller handles cleanup, do nothing.
10867
- });
10868
-
10869
- let elementWidth;
10870
- let elementHeight;
10871
- {
10872
- const updateElementDimension = () => {
10873
- const elementRect = element.getBoundingClientRect();
10874
- elementWidth = elementRect.width;
10875
- elementHeight = elementRect.height;
10876
- };
10877
- updateElementDimension();
10878
- dragGesture.addBeforeDragCallback(updateElementDimension);
10879
- }
10912
+ const findTableCellCol = (cellElement) => {
10913
+ const table = cellElement.closest("table");
10914
+ const colgroup = table.querySelector("colgroup");
10915
+ if (!colgroup) {
10916
+ return null;
10917
+ }
10918
+ const cols = colgroup.querySelectorAll("col");
10919
+ const columnIndex = cellElement.cellIndex;
10920
+ const correspondingCol = cols[columnIndex];
10921
+ return correspondingCol;
10922
+ };
10880
10923
 
10881
- let scrollArea;
10882
- {
10883
- // Snapshot at grab time so that DOM mutations during dragging
10884
- // (e.g. items shifting) don't change the scrollable boundary mid-drag.
10885
- scrollArea = {
10886
- left: 0,
10887
- top: 0,
10888
- right: scrollContainer.scrollWidth,
10889
- bottom: scrollContainer.scrollHeight,
10890
- };
10924
+ // Temporarily attach to the element so inherited CSS vars resolve correctly,
10925
+ // then snapshot all drop-hint custom properties onto the scroll container
10926
+ // so they survive once the element moves to the scroll container.
10927
+ const moveCSSVars = (vars, fromEl, toEl) => {
10928
+ const fromComputedStyle = getComputedStyle(fromEl);
10929
+ const savedVars = {};
10930
+ for (const varName of vars) {
10931
+ const value = fromComputedStyle.getPropertyValue(varName).trim();
10932
+ if (value) {
10933
+ savedVars[varName] = toEl.style.getPropertyValue(varName);
10934
+ toEl.style.setProperty(varName, value);
10891
10935
  }
10936
+ }
10892
10937
 
10893
- let scrollport;
10894
- let autoScrollArea;
10895
- {
10896
- // scrollBox is the fixed bounding rect of the scroll container viewport.
10897
- // scrollport is recomputed before each drag event to account for scrolling.
10898
- const scrollBox = getScrollBox(scrollContainer);
10899
- const updateScrollportAndAutoScrollArea = () => {
10900
- scrollport = getScrollport(scrollBox, scrollContainer);
10901
- autoScrollArea = scrollport;
10902
- if (stickyFrontiers) {
10903
- autoScrollArea = applyStickyFrontiersToAutoScrollArea(
10904
- autoScrollArea,
10905
- {
10906
- scrollContainer,
10907
- direction,
10908
- // dragGestureName,
10909
- },
10910
- );
10911
- }
10912
- if (autoScrollAreaPadding > 0) {
10913
- autoScrollArea = {
10914
- paddingLeft: autoScrollAreaPadding,
10915
- paddingTop: autoScrollAreaPadding,
10916
- paddingRight: autoScrollAreaPadding,
10917
- paddingBottom: autoScrollAreaPadding,
10918
- left: autoScrollArea.left + autoScrollAreaPadding,
10919
- top: autoScrollArea.top + autoScrollAreaPadding,
10920
- right: autoScrollArea.right - autoScrollAreaPadding,
10921
- bottom: autoScrollArea.bottom - autoScrollAreaPadding,
10922
- };
10938
+ return () => {
10939
+ for (const varName of vars) {
10940
+ if (varName in savedVars) {
10941
+ if (savedVars[varName]) {
10942
+ toEl.style.setProperty(varName, savedVars[varName]);
10943
+ } else {
10944
+ toEl.style.removeProperty(varName);
10923
10945
  }
10924
- };
10925
- updateScrollportAndAutoScrollArea();
10926
- dragGesture.addBeforeDragCallback(updateScrollportAndAutoScrollArea);
10946
+ }
10927
10947
  }
10948
+ };
10949
+ };
10928
10950
 
10929
- // Set up dragging attribute
10930
- element.setAttribute("data-grabbed", "");
10931
- dragGesture.addReleaseCallback(() => {
10932
- element.removeAttribute("data-grabbed");
10933
- });
10951
+ const applyStickyFrontiersToAutoScrollArea = (
10952
+ autoScrollArea,
10953
+ { direction, scrollContainer, dragName },
10954
+ ) => {
10955
+ let { left, right, top, bottom } = autoScrollArea;
10934
10956
 
10935
- // Will be used for dynamic constraints on sticky elements
10936
- let hasCrossedScrollportLeftOnce = false;
10937
- let hasCrossedScrollportTopOnce = false;
10938
- const dragConstraints = initDragConstraints(dragGesture, {
10939
- areaConstraint,
10940
- obstaclesContainer: obstaclesContainer || scrollContainer,
10941
- obstacleAttributeName,
10942
- showConstraintFeedbackLine,
10943
- showDebugMarkers,
10944
- referenceElement,
10945
- });
10946
- dragGesture.addBeforeDragCallback(
10947
- (layoutRequested, currentLayout, limitLayout, { dragEvent }) => {
10948
- dragConstraints.applyConstraints(
10949
- layoutRequested,
10950
- currentLayout,
10951
- limitLayout,
10952
- {
10953
- elementWidth,
10954
- elementHeight,
10955
- scrollArea,
10956
- scrollport,
10957
- hasCrossedScrollportLeftOnce,
10958
- hasCrossedScrollportTopOnce,
10959
- autoScrollArea,
10960
- dragEvent,
10961
- },
10962
- );
10957
+ if (direction.x) {
10958
+ const horizontalStickyFrontiers = createStickyFrontierOnAxis(
10959
+ scrollContainer,
10960
+ {
10961
+ name: dragName,
10962
+ scrollContainer,
10963
+ primarySide: "left",
10964
+ oppositeSide: "right",
10963
10965
  },
10964
10966
  );
10967
+ for (const horizontalStickyFrontier of horizontalStickyFrontiers) {
10968
+ const { side, bounds, element } = horizontalStickyFrontier;
10969
+ if (side === "left") {
10970
+ if (bounds.right <= left) {
10971
+ continue;
10972
+ }
10973
+ left = bounds.right;
10974
+ continue;
10975
+ }
10976
+ // right
10977
+ if (bounds.left >= right) {
10978
+ continue;
10979
+ }
10980
+ right = bounds.left;
10981
+ continue;
10982
+ }
10983
+ }
10965
10984
 
10966
- const dragToMove = (gestureInfo) => {
10967
- const { isGoingDown, isGoingUp, isGoingLeft, isGoingRight, layout } =
10968
- gestureInfo;
10969
- const left = layout.left;
10970
- const top = layout.top;
10971
- const right = left + elementWidth;
10972
- const bottom = top + elementHeight;
10973
-
10985
+ if (direction.y) {
10986
+ const verticalStickyFrontiers = createStickyFrontierOnAxis(
10987
+ scrollContainer,
10974
10988
  {
10975
- hasCrossedScrollportLeftOnce =
10976
- hasCrossedScrollportLeftOnce || left < scrollport.left;
10977
- hasCrossedScrollportTopOnce =
10978
- hasCrossedScrollportTopOnce || top < scrollport.top;
10989
+ name: dragName,
10990
+ scrollContainer,
10991
+ primarySide: "top",
10992
+ oppositeSide: "bottom",
10993
+ },
10994
+ );
10995
+ for (const verticalStickyFrontier of verticalStickyFrontiers) {
10996
+ const { side, bounds, element } = verticalStickyFrontier;
10979
10997
 
10980
- const getScrollMove = (axis) => {
10981
- const isGoingPositive = axis === "x" ? isGoingRight : isGoingDown;
10982
- if (isGoingPositive) {
10983
- const elementEnd = axis === "x" ? right : bottom;
10984
- const autoScrollAreaEnd =
10985
- axis === "x" ? autoScrollArea.right : autoScrollArea.bottom;
10998
+ // Frontier acts as a top barrier - constrains from the bottom edge of the frontier
10999
+ if (side === "top") {
11000
+ if (bounds.bottom <= top) {
11001
+ continue;
11002
+ }
11003
+ top = bounds.bottom;
11004
+ continue;
11005
+ }
10986
11006
 
10987
- if (elementEnd <= autoScrollAreaEnd) {
10988
- return 0;
10989
- }
10990
- const scrollAmountNeeded = elementEnd - autoScrollAreaEnd;
10991
- return scrollAmountNeeded;
10992
- }
11007
+ // Frontier acts as a bottom barrier - constrains from the top edge of the frontier
11008
+ if (bounds.top >= bottom) {
11009
+ continue;
11010
+ }
11011
+ bottom = bounds.top;
11012
+ continue;
11013
+ }
11014
+ }
10993
11015
 
10994
- const isGoingNegative = axis === "x" ? isGoingLeft : isGoingUp;
10995
- if (!isGoingNegative) {
10996
- return 0;
10997
- }
11016
+ return { left, right, top, bottom };
11017
+ };
10998
11018
 
10999
- const referenceOrEl = referenceElement || element;
11000
- const canAutoScrollNegative =
11001
- axis === "x"
11002
- ? !referenceOrEl.hasAttribute("data-sticky-left") ||
11003
- hasCrossedScrollportLeftOnce
11004
- : !referenceOrEl.hasAttribute("data-sticky-top") ||
11005
- hasCrossedScrollportTopOnce;
11006
- if (!canAutoScrollNegative) {
11007
- return 0;
11008
- }
11009
-
11010
- const elementStart = axis === "x" ? left : top;
11011
- const autoScrollAreaStart =
11012
- axis === "x" ? autoScrollArea.left : autoScrollArea.top;
11013
- if (elementStart >= autoScrollAreaStart) {
11014
- return 0;
11015
- }
11016
-
11017
- const scrollAmountNeeded = autoScrollAreaStart - elementStart;
11018
- return -scrollAmountNeeded;
11019
- };
11020
-
11021
- let scrollLeftTarget;
11022
- let scrollTopTarget;
11023
- if (direction.x) {
11024
- const containerScrollLeftMove = getScrollMove("x");
11025
- if (containerScrollLeftMove) {
11026
- scrollLeftTarget =
11027
- scrollContainer.scrollLeft + containerScrollLeftMove;
11028
- }
11029
- }
11030
- if (direction.y) {
11031
- const containerScrollTopMove = getScrollMove("y");
11032
- if (containerScrollTopMove) {
11033
- scrollTopTarget =
11034
- scrollContainer.scrollTop + containerScrollTopMove;
11035
- }
11036
- }
11037
- // now we know what to do, do it
11038
- if (scrollLeftTarget !== undefined) {
11039
- scrollContainer.scrollLeft = scrollLeftTarget;
11040
- }
11041
- if (scrollTopTarget !== undefined) {
11042
- scrollContainer.scrollTop = scrollTopTarget;
11043
- }
11044
- }
11045
-
11046
- {
11047
- const { scrollableLeft, scrollableTop } = layout;
11048
- const [positionedLeft, positionedTop] = convertScrollablePosition(
11049
- scrollableLeft,
11050
- scrollableTop,
11051
- );
11052
- // Build the transform to apply, preserving any transforms that were
11053
- // already on the element before the grab (e.g. rotate from another
11054
- // controller), and accumulating from the pre-grab translate baseline.
11055
- // The translate keys are seeded HERE, before the spread, and not merely
11056
- // assigned below: a transform object is serialized in key order, and in a
11057
- // transform list every function transforms the frame of the ones after it.
11058
- // A translate written after a rotate or a scale therefore travels rotated
11059
- // and scaled — the element drifts away from the pointer, proportionally to
11060
- // the distance covered. Dragging moves things on screen, so its translate
11061
- // has to come first, whatever else the element carries. The spread still
11062
- // wins on the value when the element already had a translate of its own.
11063
- const transform = { translateX: 0, translateY: 0, ...transformAtGrab };
11064
- if (direction.x) {
11065
- const leftTarget = positionedLeft;
11066
- const leftAtGrab = dragGesture.gestureInfo.leftAtGrab;
11067
- const leftDelta = leftTarget - leftAtGrab;
11068
- const translateX = translateXAtGrab
11069
- ? translateXAtGrab + leftDelta
11070
- : leftDelta;
11071
- transform.translateX = translateX;
11072
- }
11073
- if (direction.y) {
11074
- const topTarget = positionedTop;
11075
- const topAtGrab = dragGesture.gestureInfo.topAtGrab;
11076
- const topDelta = topTarget - topAtGrab;
11077
- const translateY = translateYAtGrab
11078
- ? translateYAtGrab + topDelta
11079
- : topDelta;
11080
- transform.translateY = translateY;
11081
- }
11082
- dragStyleController.set(elementImpacted, {
11083
- transform,
11084
- });
11085
- }
11086
- };
11087
- dragGesture.addDragCallback(dragToMove);
11088
- };
11089
-
11090
- const dragGestureController = createDragGestureController(options);
11091
- const grab = dragGestureController.grab;
11092
- dragGestureController.grab = ({
11093
- element,
11094
- referenceElement,
11095
- elementToMove,
11096
- event,
11097
- ...rest
11098
- } = {}) => {
11099
- const scrollContainer = getScrollContainer(referenceElement || element);
11100
- const [
11101
- elementScrollableLeft,
11102
- elementScrollableTop,
11103
- convertScrollablePosition,
11104
- ] = createDragElementPositioner(element, referenceElement, elementToMove);
11105
- const dragGesture = grab({
11106
- element,
11107
- scrollContainer,
11108
- layoutScrollableLeft: elementScrollableLeft,
11109
- layoutScrollableTop: elementScrollableTop,
11110
- event,
11111
- ...rest,
11112
- });
11113
- initGrabToMoveElement(dragGesture, {
11114
- element,
11115
- referenceElement,
11116
- elementToMove,
11117
- convertScrollablePosition,
11118
- });
11119
- return dragGesture;
11120
- };
11121
-
11122
- return dragGestureController;
11123
- };
11124
-
11125
- /**
11126
- * Detects the drop target based on what element is actually under the mouse cursor.
11127
- * Uses document.elementsFromPoint() to respect visual stacking order naturally.
11128
- *
11129
- * @param {Object} gestureInfo - Gesture information
11130
- * @param {Element[]} targetElements - Array of potential drop target elements
11131
- * @param {object} [options]
11132
- * @param {Element} [options.dragElement] - The element being dragged. When provided and
11133
- * `fallbackToEdge` is true, used to compute the fallback rect.
11134
- * @param {boolean} [options.fallbackToEdge=false] - When true and the drag element does
11135
- * not intersect any target, falls back to the first item (if above all items) or the
11136
- * last item (if below all items) so there is always a valid drop target at list edges.
11137
- * @returns {Object|null} Drop target info with elementSide or null if no valid target found
11138
- */
11139
- const getDropTargetInfo = (
11140
- gestureInfo,
11141
- targetElements,
11142
- { fallbackToEdge = false } = {},
11019
+ const createStickyFrontierOnAxis = (
11020
+ element,
11021
+ { name, scrollContainer, primarySide, oppositeSide },
11143
11022
  ) => {
11144
- const dragElement = gestureInfo.elementImpacted || gestureInfo.element;
11145
- const dragElementRect = dragElement.getBoundingClientRect();
11146
- const intersectingTargets = [];
11147
- let someTargetIsCol;
11148
- let someTargetIsTr;
11149
- for (const targetElement of targetElements) {
11150
- const targetRect = targetElement.getBoundingClientRect();
11151
- if (!rectangleAreIntersecting(dragElementRect, targetRect)) {
11023
+ const primaryAttrName = `data-drag-sticky-${primarySide}-frontier`;
11024
+ const oppositeAttrName = `data-drag-sticky-${oppositeSide}-frontier`;
11025
+ const frontiers = element.querySelectorAll(
11026
+ `[${primaryAttrName}], [${oppositeAttrName}]`,
11027
+ );
11028
+ const matchingStickyFrontiers = [];
11029
+ for (const frontier of frontiers) {
11030
+ if (frontier.closest("[data-drag-ignore]")) {
11152
11031
  continue;
11153
11032
  }
11154
- if (!someTargetIsCol && targetElement.tagName === "COL") {
11155
- someTargetIsCol = true;
11156
- }
11157
- if (!someTargetIsTr && targetElement.tagName === "TR") {
11158
- someTargetIsTr = true;
11033
+ const hasPrimary = frontier.hasAttribute(primaryAttrName);
11034
+ const hasOpposite = frontier.hasAttribute(oppositeAttrName);
11035
+ // Check if element has both sides (invalid)
11036
+ if (hasPrimary && hasOpposite) {
11037
+ const elementSignature = getElementSignature(frontier);
11038
+ console.warn(
11039
+ `Sticky frontier element (${elementSignature}) has both ${primarySide} and ${oppositeSide} attributes.
11040
+ A sticky frontier should only have one side attribute.`,
11041
+ );
11042
+ continue;
11159
11043
  }
11160
- intersectingTargets.push(targetElement);
11161
- }
11162
-
11163
- if (intersectingTargets.length === 0) {
11164
- if (fallbackToEdge) {
11165
- const dragElement = gestureInfo.elementImpacted || gestureInfo.element;
11166
- const dragElementRect = dragElement.getBoundingClientRect();
11167
- const firstItem = targetElements[0];
11168
- const lastItem = targetElements[targetElements.length - 1];
11169
- if (
11170
- firstItem &&
11171
- dragElementRect.bottom < firstItem.getBoundingClientRect().top
11172
- ) {
11173
- // Drag element is above all items → treat as hovering the first item from the top.
11174
- return {
11175
- element: firstItem,
11176
- elementSide: { x: "start", y: "start" },
11177
- index: 0,
11178
- intersectingIndex: 0,
11179
- intersecting: [firstItem],
11180
- };
11181
- }
11182
- if (
11183
- lastItem &&
11184
- dragElementRect.top > lastItem.getBoundingClientRect().bottom
11185
- ) {
11186
- // Drag element is below all items → treat as hovering the last item from the bottom.
11187
- return {
11188
- element: lastItem,
11189
- elementSide: { x: "start", y: "end" },
11190
- index: targetElements.length - 1,
11191
- intersectingIndex: 0,
11192
- intersecting: [lastItem],
11193
- };
11044
+ const attrName = hasPrimary ? primaryAttrName : oppositeAttrName;
11045
+ const attributeValue = frontier.getAttribute(attrName);
11046
+ if (attributeValue && name) {
11047
+ const frontierNames = attributeValue.split(",");
11048
+ const isMatching = frontierNames.some(
11049
+ (frontierName) =>
11050
+ frontierName.trim().toLowerCase() === name.toLowerCase(),
11051
+ );
11052
+ if (!isMatching) {
11053
+ continue;
11194
11054
  }
11195
11055
  }
11196
- return null;
11056
+ const frontierBounds = getScrollRelativeRect(frontier, scrollContainer);
11057
+ const stickyFrontierObject = {
11058
+ type: "sticky-frontier",
11059
+ element: frontier,
11060
+ side: hasPrimary ? primarySide : oppositeSide,
11061
+ bounds: frontierBounds,
11062
+ name: `sticky_frontier_${hasPrimary ? primarySide : oppositeSide} (${getElementSignature(frontier)})`,
11063
+ };
11064
+ matchingStickyFrontiers.push(stickyFrontierObject);
11197
11065
  }
11066
+ return matchingStickyFrontiers;
11067
+ };
11198
11068
 
11199
- const dragElementCenterX = dragElementRect.left + dragElementRect.width / 2;
11200
- const dragElementCenterY = dragElementRect.top + dragElementRect.height / 2;
11201
- // Clamp coordinates to viewport to avoid issues with elementsFromPoint
11202
- const viewportWidth = document.documentElement.clientWidth;
11203
- const viewportHeight = document.documentElement.clientHeight;
11204
- const clientX =
11205
- dragElementCenterX < 0
11206
- ? 0
11207
- : dragElementCenterX > viewportWidth
11208
- ? viewportWidth - 1
11209
- : dragElementCenterX;
11210
- const clientY =
11211
- dragElementCenterY < 0
11212
- ? 0
11213
- : dragElementCenterY > viewportHeight
11214
- ? viewportHeight - 1
11215
- : dragElementCenterY;
11069
+ installImportMetaCssBuild(import.meta);/**
11070
+ * A drag, and what it is FOR.
11071
+ *
11072
+ * What a hand does is always the same — pick the thing up, carry it, let go — so
11073
+ * the gesture is not what distinguishes these. What distinguishes them is the
11074
+ * outcome the caller asked for, and that is what `startDragTo` takes:
11075
+ *
11076
+ * - **move**: it stays where it was put. The element ITSELF travels and keeps the
11077
+ * place the hand gave it.
11078
+ * - **reorder**: it takes a place in a list. A COPY travels while the original
11079
+ * keeps its place in the layout, which is what makes the gesture possible at
11080
+ * all — nothing else moves while the hand looks for a place, so there is a
11081
+ * stable row of items to look between.
11082
+ * - **toss**: it is gotten rid of. The same copy, for the opposite reason: the
11083
+ * original stays until the answer says it is really gone.
11084
+ *
11085
+ * The caller lists which outcomes ITS element can answer, and only the machinery
11086
+ * those need runs: no copy for a move, no drop hint for something that can only be
11087
+ * thrown away, no landing looked for where nothing lands. `reorder` and `toss`
11088
+ * combine (dropped on a row, or thrown off the screen); `move` and `reorder` cannot
11089
+ * both be true of one release, and the caller is the one who must not ask for both.
11090
+ *
11091
+ * `createDragToMoveGestureController` below is the layer under all of that — the
11092
+ * translation, the auto-scroll, the constraints — and stays usable on its own for
11093
+ * anything that is none of the three (a table column being dragged, a sticky
11094
+ * frontier being moved).
11095
+ */
11096
+ const dragStyleController = createStyleController("drag_to_move");
11216
11097
 
11217
- // Find the first target element in the stack (topmost visible target)
11218
- const elementsUnderDragElement = document.elementsFromPoint(clientX, clientY);
11219
- let targetElement = null;
11220
- let targetIndex = -1;
11221
- let intersectingIndex = -1;
11222
- for (const element of elementsUnderDragElement) {
11223
- // First, check if the element itself is a target
11224
- const directIndex = intersectingTargets.indexOf(element);
11225
- if (directIndex !== -1) {
11226
- targetElement = element;
11227
- intersectingIndex = directIndex;
11228
- break;
11229
- }
11230
- // Special case: if element is <td> or <th> and not in targets,
11231
- // try to find its corresponding <col> element
11232
- if (!isTableCell(element)) {
11233
- continue;
11234
- }
11235
- try_col: {
11236
- if (!someTargetIsCol) {
11237
- break try_col;
11238
- }
11239
- const tableCellCol = findTableCellCol(element);
11240
- if (!tableCellCol) {
11241
- break try_col;
11242
- }
11243
- const colIndex = intersectingTargets.indexOf(tableCellCol);
11244
- if (colIndex === -1) {
11245
- break try_col;
11246
- }
11247
- targetElement = tableCellCol;
11248
- intersectingIndex = colIndex;
11249
- break;
11250
- }
11251
- try_tr: {
11252
- if (!someTargetIsTr) {
11253
- break try_tr;
11254
- }
11255
- const tableRow = element.closest("tr");
11256
- const rowIndex = targetElements.indexOf(tableRow);
11257
- if (rowIndex === -1) {
11258
- break try_tr;
11259
- }
11260
- targetElement = tableRow;
11261
- intersectingIndex = intersectingTargets.indexOf(tableRow);
11262
- break;
11263
- }
11264
- }
11265
- if (!targetElement) {
11266
- targetElement = intersectingTargets[0];
11267
- intersectingIndex = 0;
11268
- }
11269
- targetIndex = targetElements.indexOf(targetElement);
11270
-
11271
- // Determine position within the target for both axes.
11272
- //
11273
- // Use the leading edge of the dragged element (in the direction of movement)
11274
- // compared against the target's center:
11275
- // - Dragging down: "after" as soon as the bottom crosses the target center.
11276
- // - Dragging up: "before" as soon as the top crosses the target center.
11277
- // - Not moving: center-vs-center fallback.
11278
- //
11279
- // This gives consistent, predictable thresholds regardless of element size.
11280
- const targetRect = targetElement.getBoundingClientRect();
11281
- const targetCenterX = targetRect.left + targetRect.width / 2;
11282
- const targetCenterY = targetRect.top + targetRect.height / 2;
11283
- const { intentGoingDown, intentGoingUp, intentGoingRight, intentGoingLeft } =
11284
- gestureInfo;
11285
- let sideY;
11286
- if (intentGoingDown) {
11287
- sideY = dragElementRect.bottom > targetCenterY ? "end" : "start";
11288
- } else if (intentGoingUp) {
11289
- sideY = dragElementRect.top < targetCenterY ? "start" : "end";
11290
- } else {
11291
- sideY = dragElementCenterY < targetCenterY ? "start" : "end";
11292
- }
11293
- let sideX;
11294
- if (intentGoingRight) {
11295
- sideX = dragElementRect.right > targetCenterX ? "end" : "start";
11296
- } else if (intentGoingLeft) {
11297
- sideX = dragElementRect.left < targetCenterX ? "start" : "end";
11298
- } else {
11299
- sideX = dragElementCenterX < targetCenterX ? "start" : "end";
11300
- }
11301
- const result = {
11302
- // NOTE: avoid relying on `index` in application code. The targetElements
11303
- // array may be dynamically filtered (e.g. excluding the grabbed element),
11304
- // making this index inconsistent with the full list. Use `element` instead
11305
- // and look up its position yourself from your own data source.
11306
- index: targetIndex,
11307
- element: targetElement,
11308
- elementSide: {
11309
- x: sideX,
11310
- y: sideY,
11311
- },
11312
- // Index within the intersecting subset — could be useful to know how many
11313
- // elements were overlapping, but rarely needed in practice
11314
- intersectingIndex,
11315
- intersecting: intersectingTargets,
11316
- };
11317
- return result;
11318
- };
11319
-
11320
- const rectangleAreIntersecting = (r1, r2) => {
11321
- return !(
11322
- r2.left > r1.right ||
11323
- r2.right < r1.left ||
11324
- r2.top > r1.bottom ||
11325
- r2.bottom < r1.top
11326
- );
11327
- };
11328
-
11329
- const isTableCell = (el) => {
11330
- return el.tagName === "TD" || el.tagName === "TH";
11331
- };
11332
-
11333
- /**
11334
- * Find the corresponding <col> element for a given <td> or <th> cell
11335
- * @param {Element} cellElement - The <td> or <th> element
11336
- * @param {Element[]} targetColElements - Array of <col> elements to search in
11337
- * @returns {Element|null} The corresponding <col> element or null if not found
11338
- */
11339
- const findTableCellCol = (cellElement) => {
11340
- const table = cellElement.closest("table");
11341
- const colgroup = table.querySelector("colgroup");
11342
- if (!colgroup) {
11343
- return null;
11344
- }
11345
- const cols = colgroup.querySelectorAll("col");
11346
- const columnIndex = cellElement.cellIndex;
11347
- const correspondingCol = cols[columnIndex];
11348
- return correspondingCol;
11349
- };
11350
-
11351
- // Temporarily attach to the element so inherited CSS vars resolve correctly,
11352
- // then snapshot all drop-hint custom properties onto the scroll container
11353
- // so they survive once the element moves to the scroll container.
11354
- const moveCSSVars = (vars, fromEl, toEl) => {
11355
- const fromComputedStyle = getComputedStyle(fromEl);
11356
- const savedVars = {};
11357
- for (const varName of vars) {
11358
- const value = fromComputedStyle.getPropertyValue(varName).trim();
11359
- if (value) {
11360
- savedVars[varName] = toEl.style.getPropertyValue(varName);
11361
- toEl.style.setProperty(varName, value);
11362
- }
11363
- }
11364
-
11365
- return () => {
11366
- for (const varName of vars) {
11367
- if (varName in savedVars) {
11368
- if (savedVars[varName]) {
11369
- toEl.style.setProperty(varName, savedVars[varName]);
11370
- } else {
11371
- toEl.style.removeProperty(varName);
11372
- }
11373
- }
11374
- }
11375
- };
11376
- };
11377
-
11378
- installImportMetaCssBuild(import.meta);const css$1 = /* css */`
11098
+ // How long the copy takes to leave the screen, and to come back. Written into the
11099
+ // CSS below from here: the flight has to be waited for, and a duration living only
11100
+ // in a stylesheet is a timing JS cannot read reliably.
11101
+ const TOSS_DURATION_MS = 320;
11102
+ // Far enough to be off any screen, in the direction the hand was going.
11103
+ const TOSS_DISTANCE = 900;
11104
+ const css$1 = /* css */`
11379
11105
  /* IN THE PAGE, NOT IN THE LIST: the hint lands on the edge of a row, which
11380
11106
  for the last one is the very bottom of the scroll area — drawn inside it,
11381
11107
  the line would push the scrollable area a few pixels further and make a
@@ -11453,7 +11179,7 @@ installImportMetaCssBuild(import.meta);const css$1 = /* css */`
11453
11179
  the pointer), so an I-beam over it would promise something that does not
11454
11180
  happen: it reads as a plain surface instead. An opted-out area keeps both
11455
11181
  its cursor and its selection, and never starts a drag (see the check in
11456
- startDragToReorder).
11182
+ startDragTo).
11457
11183
  Controls inside a source keep their own cursor: cursor is inherited, and
11458
11184
  anything setting its own (a button's pointer) wins on itself.
11459
11185
  Only the resting cursor is set here: what it becomes once a drag is under
@@ -11471,133 +11197,713 @@ installImportMetaCssBuild(import.meta);const css$1 = /* css */`
11471
11197
  user-select: auto;
11472
11198
  }
11473
11199
 
11474
- [navi-drag-clone-source] {
11475
- visibility: hidden;
11200
+ [navi-drag-clone-source] {
11201
+ visibility: hidden;
11202
+ }
11203
+
11204
+ [navi-drag-clone-wrapper] {
11205
+ /* Also a popover (see .navi_drop_hint): in the top layer it is over the
11206
+ page whatever the page's own stacking is, and the coordinates it is
11207
+ given are viewport ones — which is what the pointer carrying it works
11208
+ in. Same UA-style reset as the hint. */
11209
+ position: fixed;
11210
+ inset: auto;
11211
+ top: var(--clone-top);
11212
+ left: var(--clone-left);
11213
+ box-sizing: border-box;
11214
+ width: var(--clone-width);
11215
+ height: var(--clone-height);
11216
+ margin: 0;
11217
+ padding: 0;
11218
+ color: inherit;
11219
+ background: transparent;
11220
+ border: none;
11221
+ /* A var, and read from the dragged element (see dragCSSVars): what being
11222
+ carried LOOKS like belongs to whoever owns the thing — a row lifted off a
11223
+ list wants this shadow, a sheet of paper leaving a board wants none, and its
11224
+ shade is a theme's business either way. */
11225
+ box-shadow: var(--drag-clone-shadow, 0 12px 28px rgba(0, 0, 0, 0.22));
11226
+ opacity: 0.95;
11227
+ transition: box-shadow 0.15s ease;
11228
+ pointer-events: none;
11229
+ /* Nothing in a copy being carried by a pointer is text to select: the
11230
+ selection belongs to the original, which is still in the page. This is the
11231
+ one place the rule is unconditional — an element that can be dragged is
11232
+ usually selectable too (a link is both), and forcing it there would take
11233
+ away a selection made from outside the element. */
11234
+ user-select: none;
11235
+ overflow: visible;
11236
+ }
11237
+
11238
+ /* Ce qui a été lancé: il continue dans la direction du geste jusqu'à sortir de
11239
+ l'écran, et revient par le même chemin si la réponse refuse. */
11240
+ [navi-drag-clone-wrapper][data-tossed] {
11241
+ transition:
11242
+ translate ${TOSS_DURATION_MS}ms ease-out,
11243
+ opacity ${TOSS_DURATION_MS}ms ease-out;
11244
+ }
11245
+ [navi-drag-clone-wrapper][data-tossed="away"] {
11246
+ opacity: 0;
11247
+ }
11248
+
11249
+ [navi-drag-clone] {
11250
+ transform: scale(var(--drag-clone-scale, 1.03));
11251
+ transform-origin: var(--drag-origin);
11252
+ transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
11253
+ }
11254
+
11255
+ @starting-style {
11256
+ [navi-drag-clone-wrapper] {
11257
+ box-shadow: none;
11258
+ }
11259
+
11260
+ [navi-drag-clone] {
11261
+ transform: scale(1);
11262
+ }
11263
+ }
11264
+ `;
11265
+ // At module scope, not inside startDragTo: the cursor rules above say who
11266
+ // can start a drag, and they have to be true BEFORE anyone drags anything.
11267
+ import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_to.js"];
11268
+ const dragCSSVars = ["--drop-hint-size", "--drop-hint-background-color", "--drop-hint-border-radius", "--drop-hint-margin-x", "--drop-hint-margin-y", "--drop-hint-arrow-size", "--drag-clone-scale", "--drag-clone-shadow"];
11269
+
11270
+ /**
11271
+ * Starts a drag-to-reorder interaction on a list item.
11272
+ *
11273
+ * Handles the full reorder UX:
11274
+ * - Activates only once the intent is established — a short movement with a mouse, a long
11275
+ * press with a finger (see `dragAfterIntent`), so that neither a click nor a scroll
11276
+ * reorders anything by accident.
11277
+ * - Clones the grabbed element and moves the clone while the original stays hidden in place
11278
+ * (keeps the layout intact so other items don't shift during the drag).
11279
+ * - CSS vars (`--drop-hint-size`, `--drop-hint-background-color`, etc.) are read from the
11280
+ * dragged element and moved to `document.documentElement` for the duration of the drag so
11281
+ * the drop-hint and clone — both in `document.body` — can inherit them.
11282
+ * - Shows a drop-hint line indicating where the item will land.
11283
+ * - Drop-target detection is intersection-based: the clone's bounding rect is compared
11284
+ * against every item that matches `itemSelector` in the scroll container.
11285
+ * - No-ops are filtered: releasing on the grabbed element itself, or in a position that
11286
+ * would leave it at exactly the same index, never triggers `onReorder`.
11287
+ * - On a valid drop, the clone animates to the drop position via the View Transitions API,
11288
+ * `onReorder` is called inside the transition callback so the DOM update and the animation
11289
+ * are captured together, then the clone is removed.
11290
+ * - On a cancelled drop (pointer released with no valid target), the clone is removed
11291
+ * immediately without calling `onReorder`.
11292
+ *
11293
+ * IDs are used as the bridge between DOM elements and JS state because:
11294
+ * - Not all DOM elements matching `itemSelector` may be valid drop targets
11295
+ * (holes in the structure), so DOM indices don't reliably map to state indices.
11296
+ * - Virtual lists render fewer DOM nodes than the total item count, so
11297
+ * DOM-index-based counting would be wrong.
11298
+ *
11299
+ * Any option not listed below is forwarded to `createDragToMoveGestureController`
11300
+ * (`areaConstraint`, `autoScrollAreaPadding`, `stickyFrontiers`…), except
11301
+ * `releasePositionEffect`, always `"manual"` here: what moves is the clone, and it
11302
+ * is removed on release, so there is no position to commit or cancel.
11303
+ *
11304
+ * @param {PointerEvent} event
11305
+ * The `pointerdown` event that may become a reorder.
11306
+ * @param {object} options
11307
+ * @param {Element} [options.draggedElement=event.currentTarget]
11308
+ * The list item to drag.
11309
+ * @param {Element} [options.containerElement=draggedElement.parentElement]
11310
+ * Element searched with `itemSelector` to find the items to drop between.
11311
+ * @param {string} [options.itemSelector]
11312
+ * CSS selector that matches all list items inside `containerElement`.
11313
+ * Used for drop-target detection and no-op filtering. Left out, nothing is a
11314
+ * drop target: no hint is drawn and no reorder can be answered — which is what
11315
+ * a drag that only ever throws the thing away asks for.
11316
+ * @param {function} options.getItemId
11317
+ * Returns the stable ID for a given DOM element.
11318
+ * Signature: `getItemId(element) → id`.
11319
+ * @param {function} options.onReorder
11320
+ * Called when the user drops the item in a new position.
11321
+ * Signature: `onReorder(fromId, toId, syncCloneWithDropTarget)`.
11322
+ * - `fromId`: stable ID of the dragged item.
11323
+ * - `toId`: stable ID of the item to insert before, or `null` to append at the end.
11324
+ * - `syncCloneWithDropTarget`: call it synchronously inside a
11325
+ * `document.startViewTransition` callback, next to the DOM mutation, so the
11326
+ * clone is captured at its landing position.
11327
+ * @param {(detail: {gestureInfo: object, dropTarget: Element|null}) => "reorder"|"toss"|"cancel"} [options.resolveDrop]
11328
+ * What THIS release means, when the answer is not simply "a target was found or
11329
+ * not": the same grab can be meant to reorder or to get rid of the thing, and
11330
+ * only the caller knows which — far and fast is a throw, over a row is a move.
11331
+ * Left out, a drop target reorders and anything else is cancelled.
11332
+ * @param {(detail: {gestureInfo: object}) => Promise|void} [options.onToss]
11333
+ * The release was a throw. The clone leaves the screen the way it was thrown
11334
+ * while this runs; it comes back if the promise rejects, because the thing still
11335
+ * exists and the screen has to say so.
11336
+ * @param {object} [options.direction={ x: false, y: true }]
11337
+ * Axes along which dragging is allowed. Passed to `createDragToMoveGestureController`.
11338
+ * @param {number} [options.threshold=5]
11339
+ * Distance (px) a mouse must travel before the press becomes a drag.
11340
+ * @param {boolean|"if-touch"} [options.longPress="if-touch"]
11341
+ * Which pointers start the drag by holding still instead of by travelling.
11342
+ * @param {number} [options.longPressDelay=400]
11343
+ * How long (ms) such a pointer must stay down.
11344
+ * @param {number} [options.longPressSlop=8]
11345
+ * How far (px) it may drift during that wait before the press is abandoned.
11346
+ * @param {function} [options.onPressStart]
11347
+ * The pointer went down and the wait began (a cue that the press counts).
11348
+ * @param {function} [options.onPressCancel]
11349
+ * The pointer moved or lifted before the wait was over.
11350
+ * @param {function} [options.onPress]
11351
+ * The wait completed and the item is now held (haptics, scale…).
11352
+ */
11353
+
11354
+ /**
11355
+ * Creates a gesture controller that moves elements via drag.
11356
+ *
11357
+ * Wraps `createDragGestureController` and adds:
11358
+ * - Element translation via CSS transform (translate only; other existing transforms are preserved)
11359
+ * - Auto-scroll while dragging near scroll-container edges
11360
+ * - Constraints (area boundaries, obstacle elements)
11361
+ *
11362
+ * The returned controller exposes a `grab(options)` / `grabViaPointer(event, options)` method.
11363
+ * Key grab options:
11364
+ * - `element`: the element whose position drives layout calculations (scroll-container detection,
11365
+ * constraints, auto-scroll). Sets `data-grabbed` during the drag.
11366
+ * - `referenceElement`: optional sticky-frontier / obstacle reference, defaults to `element`.
11367
+ * - `elementToMove`: optional different element to actually translate (e.g. a drag clone).
11368
+ * If omitted, `element` is translated. The translate is read from `dragStyleController`
11369
+ * at grab time so any pre-existing translate is accumulated rather than reset.
11370
+ *
11371
+ * A `transform` already on the moved element (rotate, scale…) is preserved and does
11372
+ * not disturb the movement. `rotate` and `scale` set as individual CSS properties do:
11373
+ * they apply outside `transform`, where nothing the gesture writes can reach them —
11374
+ * put those on a child element instead (a warning says so in dev).
11375
+ *
11376
+ * @param {object} [options]
11377
+ * @param {boolean} [options.stickyFrontiers=true]
11378
+ * Shrinks the auto-scroll area at sticky boundaries (elements with `data-sticky-left` /
11379
+ * `data-sticky-top`).
11380
+ * @param {number} [options.autoScrollAreaPadding=0]
11381
+ * Extra padding (px) subtracted from each edge of the auto-scroll trigger area.
11382
+ * @param {string|object|function} [options.areaConstraint="scroll"]
11383
+ * Constrains where the element can be dragged.
11384
+ * `"scroll"` — bounded by the full scroll area.
11385
+ * `"scrollport"` — bounded by the visible viewport of the scroll container.
11386
+ * `"none"` — no area constraint.
11387
+ * `{left, top, right, bottom}` — fixed bounds (values may be functions receiving context).
11388
+ * `function` — called each drag frame, must return a `{left,top,right,bottom}` object.
11389
+ * @param {Element} [options.obstaclesContainer]
11390
+ * Container to look for obstacle elements in. Defaults to the scroll container.
11391
+ * @param {string} [options.obstacleAttributeName="data-drag-obstacle"]
11392
+ * Attribute that marks obstacle elements.
11393
+ * @param {boolean} [options.showConstraintFeedbackLine=false]
11394
+ * Renders a visual line when the pointer deviates from the element due to constraints.
11395
+ * @param {boolean} [options.showDebugMarkers=false]
11396
+ * Renders debug markers for constraint regions.
11397
+ * @param {"commit"|"cancel"|"cancel-animated"|"manual"} [options.releasePositionEffect="commit"]
11398
+ * Controls what happens to the translated position on release.
11399
+ * - `"commit"`: bakes the translate into inline styles so the element stays put (default).
11400
+ * - `"cancel"`: discards the translate so the element snaps back to its original position.
11401
+ * - `"cancel-animated"`: same, travelling back to it over `cancelAnimationDuration`.
11402
+ * - `"manual"`: does nothing — the caller is responsible for clearing or committing
11403
+ * the transform via `dragStyleController`.
11404
+ * @param {number} [options.cancelAnimationDuration=200]
11405
+ * Duration (ms) of the way back for `"cancel-animated"`.
11406
+ * @param {string} [options.cancelAnimationEasing="ease-out"]
11407
+ * Easing of the way back for `"cancel-animated"`.
11408
+ * @returns {object} Drag gesture controller with augmented `grab()` / `grabViaPointer()` methods.
11409
+ *
11410
+ * `gestureInfo` gains `cancelPosition()`, `commitPosition()` and
11411
+ * `cancelPositionAnimated({duration, easing})` — the last returns the `Animation`
11412
+ * playing the way back (`null` when the element was already home), so a caller
11413
+ * on `"manual"` can decide between thrown and put back, and still await the
11414
+ * landing.
11415
+ */
11416
+ const createDragToMoveGestureController = ({
11417
+ stickyFrontiers = true,
11418
+ autoScrollAreaPadding = 0,
11419
+ areaConstraint = "scroll",
11420
+ obstaclesContainer,
11421
+ obstacleAttributeName = "data-drag-obstacle",
11422
+ showConstraintFeedbackLine = false,
11423
+ showDebugMarkers = false,
11424
+ releasePositionEffect = "commit",
11425
+ cancelAnimationDuration = 200,
11426
+ cancelAnimationEasing = "ease-out",
11427
+ ...options
11428
+ } = {}) => {
11429
+ const initGrabToMoveElement = (dragGesture, {
11430
+ element,
11431
+ referenceElement,
11432
+ elementToMove,
11433
+ convertScrollablePosition
11434
+ }) => {
11435
+ const scrollContainer = dragGesture.gestureInfo.scrollContainer;
11436
+ const direction = dragGesture.gestureInfo.direction;
11437
+ // elementImpacted is either an externally provided elementToMove (e.g. a drag clone)
11438
+ const elementImpacted = elementToMove || element;
11439
+ // elementImpacted is either an externally provided elementToMove
11440
+ // (e.g. a drag clone passed by the caller) or the element itself.
11441
+ // Capture any pre-existing translate so we can accumulate on top of it
11442
+ // rather than resetting it to zero on the first drag event.
11443
+ const transformAtGrab = dragStyleController.getUnderlyingValue(elementImpacted, "transform");
11444
+ const translateXAtGrab = transformAtGrab.translateX;
11445
+ const translateYAtGrab = transformAtGrab.translateY;
11446
+ const cancelPosition = () => {
11447
+ dragStyleController.clear(elementImpacted);
11448
+ };
11449
+ // Reading the transform on either side of the clear is what lets this work
11450
+ // without knowing anything about the element: how it looked while held and
11451
+ // how it looks once let go are both just computed transforms, and the
11452
+ // animation has only to bridge the two.
11453
+ const cancelPositionAnimated = ({
11454
+ duration = cancelAnimationDuration,
11455
+ easing = cancelAnimationEasing
11456
+ } = {}) => {
11457
+ const transformWhileHeld = getComputedStyle(elementImpacted).transform;
11458
+ cancelPosition();
11459
+ const transformAtRest = getComputedStyle(elementImpacted).transform;
11460
+ if (transformWhileHeld === transformAtRest) {
11461
+ return null;
11462
+ }
11463
+ // No fill: the element already sits at its resting transform, the
11464
+ // animation only replays the way back to it.
11465
+ return elementImpacted.animate([{
11466
+ transform: transformWhileHeld
11467
+ }, {
11468
+ transform: transformAtRest
11469
+ }], {
11470
+ duration,
11471
+ easing
11472
+ });
11473
+ };
11474
+ const commitPosition = () => {
11475
+ dragStyleController.commit(elementImpacted);
11476
+ };
11477
+ dragGesture.gestureInfo.cancelPosition = cancelPosition;
11478
+ dragGesture.gestureInfo.cancelPositionAnimated = cancelPositionAnimated;
11479
+ dragGesture.gestureInfo.commitPosition = commitPosition;
11480
+ dragGesture.addReleaseCallback(() => {
11481
+ if (releasePositionEffect === "cancel") {
11482
+ cancelPosition();
11483
+ } else if (releasePositionEffect === "cancel-animated") {
11484
+ cancelPositionAnimated();
11485
+ } else if (releasePositionEffect === "commit") {
11486
+ commitPosition();
11487
+ }
11488
+ // "manual": caller handles cleanup, do nothing.
11489
+ });
11490
+ let elementWidth;
11491
+ let elementHeight;
11492
+ {
11493
+ const updateElementDimension = () => {
11494
+ const elementRect = element.getBoundingClientRect();
11495
+ elementWidth = elementRect.width;
11496
+ elementHeight = elementRect.height;
11497
+ };
11498
+ updateElementDimension();
11499
+ dragGesture.addBeforeDragCallback(updateElementDimension);
11500
+ }
11501
+ let scrollArea;
11502
+ {
11503
+ // Snapshot at grab time so that DOM mutations during dragging
11504
+ // (e.g. items shifting) don't change the scrollable boundary mid-drag.
11505
+ scrollArea = {
11506
+ left: 0,
11507
+ top: 0,
11508
+ right: scrollContainer.scrollWidth,
11509
+ bottom: scrollContainer.scrollHeight
11510
+ };
11511
+ }
11512
+ let scrollport;
11513
+ let autoScrollArea;
11514
+ {
11515
+ // scrollBox is the fixed bounding rect of the scroll container viewport.
11516
+ // scrollport is recomputed before each drag event to account for scrolling.
11517
+ const scrollBox = getScrollBox(scrollContainer);
11518
+ const updateScrollportAndAutoScrollArea = () => {
11519
+ scrollport = getScrollport(scrollBox, scrollContainer);
11520
+ autoScrollArea = scrollport;
11521
+ if (stickyFrontiers) {
11522
+ autoScrollArea = applyStickyFrontiersToAutoScrollArea(autoScrollArea, {
11523
+ scrollContainer,
11524
+ direction
11525
+ // dragGestureName,
11526
+ });
11527
+ }
11528
+ if (autoScrollAreaPadding > 0) {
11529
+ autoScrollArea = {
11530
+ paddingLeft: autoScrollAreaPadding,
11531
+ paddingTop: autoScrollAreaPadding,
11532
+ paddingRight: autoScrollAreaPadding,
11533
+ paddingBottom: autoScrollAreaPadding,
11534
+ left: autoScrollArea.left + autoScrollAreaPadding,
11535
+ top: autoScrollArea.top + autoScrollAreaPadding,
11536
+ right: autoScrollArea.right - autoScrollAreaPadding,
11537
+ bottom: autoScrollArea.bottom - autoScrollAreaPadding
11538
+ };
11539
+ }
11540
+ };
11541
+ updateScrollportAndAutoScrollArea();
11542
+ dragGesture.addBeforeDragCallback(updateScrollportAndAutoScrollArea);
11543
+ }
11544
+
11545
+ // Set up dragging attribute
11546
+ element.setAttribute("data-grabbed", "");
11547
+ dragGesture.addReleaseCallback(() => {
11548
+ element.removeAttribute("data-grabbed");
11549
+ });
11550
+
11551
+ // Will be used for dynamic constraints on sticky elements
11552
+ let hasCrossedScrollportLeftOnce = false;
11553
+ let hasCrossedScrollportTopOnce = false;
11554
+ const dragConstraints = initDragConstraints(dragGesture, {
11555
+ areaConstraint,
11556
+ obstaclesContainer: obstaclesContainer || scrollContainer,
11557
+ obstacleAttributeName,
11558
+ showConstraintFeedbackLine,
11559
+ showDebugMarkers,
11560
+ referenceElement
11561
+ });
11562
+ dragGesture.addBeforeDragCallback((layoutRequested, currentLayout, limitLayout, {
11563
+ dragEvent
11564
+ }) => {
11565
+ dragConstraints.applyConstraints(layoutRequested, currentLayout, limitLayout, {
11566
+ elementWidth,
11567
+ elementHeight,
11568
+ scrollArea,
11569
+ scrollport,
11570
+ hasCrossedScrollportLeftOnce,
11571
+ hasCrossedScrollportTopOnce,
11572
+ autoScrollArea,
11573
+ dragEvent
11574
+ });
11575
+ });
11576
+ const dragToMove = gestureInfo => {
11577
+ const {
11578
+ isGoingDown,
11579
+ isGoingUp,
11580
+ isGoingLeft,
11581
+ isGoingRight,
11582
+ layout
11583
+ } = gestureInfo;
11584
+ const left = layout.left;
11585
+ const top = layout.top;
11586
+ const right = left + elementWidth;
11587
+ const bottom = top + elementHeight;
11588
+ {
11589
+ hasCrossedScrollportLeftOnce = hasCrossedScrollportLeftOnce || left < scrollport.left;
11590
+ hasCrossedScrollportTopOnce = hasCrossedScrollportTopOnce || top < scrollport.top;
11591
+ const getScrollMove = axis => {
11592
+ const isGoingPositive = axis === "x" ? isGoingRight : isGoingDown;
11593
+ if (isGoingPositive) {
11594
+ const elementEnd = axis === "x" ? right : bottom;
11595
+ const autoScrollAreaEnd = axis === "x" ? autoScrollArea.right : autoScrollArea.bottom;
11596
+ if (elementEnd <= autoScrollAreaEnd) {
11597
+ return 0;
11598
+ }
11599
+ const scrollAmountNeeded = elementEnd - autoScrollAreaEnd;
11600
+ return scrollAmountNeeded;
11601
+ }
11602
+ const isGoingNegative = axis === "x" ? isGoingLeft : isGoingUp;
11603
+ if (!isGoingNegative) {
11604
+ return 0;
11605
+ }
11606
+ const referenceOrEl = referenceElement || element;
11607
+ const canAutoScrollNegative = axis === "x" ? !referenceOrEl.hasAttribute("data-sticky-left") || hasCrossedScrollportLeftOnce : !referenceOrEl.hasAttribute("data-sticky-top") || hasCrossedScrollportTopOnce;
11608
+ if (!canAutoScrollNegative) {
11609
+ return 0;
11610
+ }
11611
+ const elementStart = axis === "x" ? left : top;
11612
+ const autoScrollAreaStart = axis === "x" ? autoScrollArea.left : autoScrollArea.top;
11613
+ if (elementStart >= autoScrollAreaStart) {
11614
+ return 0;
11615
+ }
11616
+ const scrollAmountNeeded = autoScrollAreaStart - elementStart;
11617
+ return -scrollAmountNeeded;
11618
+ };
11619
+ let scrollLeftTarget;
11620
+ let scrollTopTarget;
11621
+ if (direction.x) {
11622
+ const containerScrollLeftMove = getScrollMove("x");
11623
+ if (containerScrollLeftMove) {
11624
+ scrollLeftTarget = scrollContainer.scrollLeft + containerScrollLeftMove;
11625
+ }
11626
+ }
11627
+ if (direction.y) {
11628
+ const containerScrollTopMove = getScrollMove("y");
11629
+ if (containerScrollTopMove) {
11630
+ scrollTopTarget = scrollContainer.scrollTop + containerScrollTopMove;
11631
+ }
11632
+ }
11633
+ // now we know what to do, do it
11634
+ if (scrollLeftTarget !== undefined) {
11635
+ scrollContainer.scrollLeft = scrollLeftTarget;
11636
+ }
11637
+ if (scrollTopTarget !== undefined) {
11638
+ scrollContainer.scrollTop = scrollTopTarget;
11639
+ }
11640
+ }
11641
+ {
11642
+ const {
11643
+ scrollableLeft,
11644
+ scrollableTop
11645
+ } = layout;
11646
+ const [positionedLeft, positionedTop] = convertScrollablePosition(scrollableLeft, scrollableTop);
11647
+ // Build the transform to apply, preserving any transforms that were
11648
+ // already on the element before the grab (e.g. rotate from another
11649
+ // controller), and accumulating from the pre-grab translate baseline.
11650
+ // The translate keys are seeded HERE, before the spread, and not merely
11651
+ // assigned below: a transform object is serialized in key order, and in a
11652
+ // transform list every function transforms the frame of the ones after it.
11653
+ // A translate written after a rotate or a scale therefore travels rotated
11654
+ // and scaled — the element drifts away from the pointer, proportionally to
11655
+ // the distance covered. Dragging moves things on screen, so its translate
11656
+ // has to come first, whatever else the element carries. The spread still
11657
+ // wins on the value when the element already had a translate of its own.
11658
+ const transform = {
11659
+ translateX: 0,
11660
+ translateY: 0,
11661
+ ...transformAtGrab
11662
+ };
11663
+ if (direction.x) {
11664
+ const leftTarget = positionedLeft;
11665
+ const leftAtGrab = dragGesture.gestureInfo.leftAtGrab;
11666
+ const leftDelta = leftTarget - leftAtGrab;
11667
+ const translateX = translateXAtGrab ? translateXAtGrab + leftDelta : leftDelta;
11668
+ transform.translateX = translateX;
11669
+ }
11670
+ if (direction.y) {
11671
+ const topTarget = positionedTop;
11672
+ const topAtGrab = dragGesture.gestureInfo.topAtGrab;
11673
+ const topDelta = topTarget - topAtGrab;
11674
+ const translateY = translateYAtGrab ? translateYAtGrab + topDelta : topDelta;
11675
+ transform.translateY = translateY;
11676
+ }
11677
+ dragStyleController.set(elementImpacted, {
11678
+ transform
11679
+ });
11680
+ }
11681
+ };
11682
+ dragGesture.addDragCallback(dragToMove);
11683
+ };
11684
+ const dragGestureController = createDragGestureController(options);
11685
+ const grab = dragGestureController.grab;
11686
+ dragGestureController.grab = ({
11687
+ element,
11688
+ referenceElement,
11689
+ elementToMove,
11690
+ event,
11691
+ ...rest
11692
+ } = {}) => {
11693
+ const scrollContainer = getScrollContainer(referenceElement || element);
11694
+ const [elementScrollableLeft, elementScrollableTop, convertScrollablePosition] = createDragElementPositioner(element, referenceElement, elementToMove);
11695
+ const dragGesture = grab({
11696
+ element,
11697
+ scrollContainer,
11698
+ layoutScrollableLeft: elementScrollableLeft,
11699
+ layoutScrollableTop: elementScrollableTop,
11700
+ event,
11701
+ ...rest
11702
+ });
11703
+ initGrabToMoveElement(dragGesture, {
11704
+ element,
11705
+ referenceElement,
11706
+ elementToMove,
11707
+ convertScrollablePosition
11708
+ });
11709
+ return dragGesture;
11710
+ };
11711
+ return dragGestureController;
11712
+ };
11713
+
11714
+ /**
11715
+ * Starts a drag, for one or more of the outcomes listed.
11716
+ *
11717
+ * @param {PointerEvent} event The `pointerdown` that may become a drag.
11718
+ * @param {("move"|"reorder"|"toss")[]} effects
11719
+ * What letting go of this element can mean. `reorder` and `toss` carry a copy;
11720
+ * `move` carries the element itself. Asking for `move` and `reorder` together is
11721
+ * asking one release to mean two things.
11722
+ * @param {object} [options]
11723
+ * @param {Element} [options.draggedElement=event.currentTarget]
11724
+ * @param {(detail: {gestureInfo: object, x: number, y: number}) => Promise|void} [options.onMove]
11725
+ * It was put somewhere. The position is already committed when this runs — the
11726
+ * hand let go of it there — and travels back if the promise rejects.
11727
+ * @param {Element} [options.containerElement=draggedElement.parentElement]
11728
+ * Searched with `itemSelector` for the items to drop between.
11729
+ * @param {string} [options.itemSelector] What matches the items of the list.
11730
+ * @param {function} [options.getItemId] `getItemId(element) → id`.
11731
+ * @param {function} [options.onReorder]
11732
+ * `onReorder(fromId, toId, syncCloneWithDropTarget)` — see its own note below.
11733
+ * @param {(detail: {gestureInfo: object}) => Promise|void} [options.onToss]
11734
+ * It was thrown away. The copy leaves the screen while this runs and comes back
11735
+ * if the promise rejects, because the thing still exists and the screen has to
11736
+ * say so.
11737
+ * @param {number} [options.tossDistance=110] How far a throw goes, in px.
11738
+ * @param {number} [options.tossSpeed=0.45] And how fast, in px/ms. BOTH are asked
11739
+ * for: one without the other is moving the thing while hesitating, and nothing is
11740
+ * thrown away on a hesitation.
11741
+ *
11742
+ * Everything else is forwarded to `createDragToMoveGestureController`
11743
+ * (`areaConstraint`, `autoScrollAreaPadding`, `direction`…) and to `dragAfterIntent`
11744
+ * (`threshold`, `longPress`, `longPressDelay`, `longPressSlop`, `onPressStart`,
11745
+ * `onPressCancel`, `onPress`).
11746
+ *
11747
+ * About `onReorder`:
11748
+ * - `fromId`: id of the item that moved.
11749
+ * - `toId`: id of the item to insert before, or `null` to append at the end.
11750
+ * - `syncCloneWithDropTarget`: call it synchronously inside a
11751
+ * `document.startViewTransition` callback, next to the DOM mutation, so the copy
11752
+ * is captured at its landing position.
11753
+ * The gesture holds its copy until what `onReorder` returns settles, so returning
11754
+ * the transition is what makes the landing continuous.
11755
+ */
11756
+ const startDragTo = (event, effects, {
11757
+ draggedElement = event.currentTarget,
11758
+ ...options
11759
+ } = {}) => {
11760
+ // An area that opted out of dragging (a text one wants to select, a control that
11761
+ // owns the gesture): the press there is none of our business.
11762
+ if (event.target.closest && event.target.closest("[data-drag-ignore]")) {
11763
+ return undefined;
11476
11764
  }
11477
-
11478
- [navi-drag-clone-wrapper] {
11479
- /* Also a popover (see .navi_drop_hint): in the top layer it is over the
11480
- page whatever the page's own stacking is, and the coordinates it is
11481
- given are viewport ones — which is what the pointer carrying it works
11482
- in. Same UA-style reset as the hint. */
11483
- position: fixed;
11484
- inset: auto;
11485
- top: var(--clone-top);
11486
- left: var(--clone-left);
11487
- box-sizing: border-box;
11488
- width: var(--clone-width);
11489
- height: var(--clone-height);
11490
- margin: 0;
11491
- padding: 0;
11492
- color: inherit;
11493
- background: transparent;
11494
- border: none;
11495
- box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
11496
- opacity: 0.95;
11497
- transition: box-shadow 0.15s ease;
11498
- pointer-events: none;
11499
- overflow: visible;
11765
+ // A secondary button (right click and friends) is a context menu, not a grab.
11766
+ if (!isPrimaryButtonEvent(event)) {
11767
+ return undefined;
11500
11768
  }
11501
-
11502
- [navi-drag-clone] {
11503
- transform: scale(var(--drag-clone-scale, 1.03));
11504
- transform-origin: var(--drag-origin);
11505
- transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
11769
+ const canReorder = effects.includes("reorder");
11770
+ const canToss = effects.includes("toss");
11771
+ if (canReorder || canToss) {
11772
+ return startDragToCarryCopy(event, {
11773
+ draggedElement,
11774
+ canReorder,
11775
+ canToss,
11776
+ ...options
11777
+ });
11506
11778
  }
11779
+ return startDragToMoveElement(event, {
11780
+ draggedElement,
11781
+ ...options
11782
+ });
11783
+ };
11507
11784
 
11508
- @starting-style {
11509
- [navi-drag-clone-wrapper] {
11510
- box-shadow: none;
11785
+ /**
11786
+ * The element ITSELF is carried, and keeps the place the hand gave it.
11787
+ *
11788
+ * No copy, unlike the two others: what is being moved is the thing and not a
11789
+ * stand-in for it, so there is nothing to put back and nothing to reveal.
11790
+ */
11791
+ const startDragToMoveElement = (event, {
11792
+ draggedElement,
11793
+ onMove,
11794
+ threshold,
11795
+ longPress,
11796
+ longPressDelay,
11797
+ longPressSlop,
11798
+ onPressStart,
11799
+ onPressCancel,
11800
+ onPress,
11801
+ ...options
11802
+ }) => {
11803
+ event.preventDefault();
11804
+ return dragAfterIntent(event, () => {
11805
+ const gestureController = createDragToMoveGestureController({
11806
+ releasePositionEffect: "manual",
11807
+ ...options
11808
+ });
11809
+ const dragGesture = gestureController.grabViaPointer(event, {
11810
+ element: draggedElement
11811
+ });
11812
+ if (!dragGesture) {
11813
+ return null;
11511
11814
  }
11815
+ dragGesture.addReleaseCallback(async gestureInfo => {
11816
+ const {
11817
+ xDelta,
11818
+ yDelta
11819
+ } = gestureInfo.layout;
11820
+ if (!xDelta && !yDelta) {
11821
+ // Picked up and put back down: nothing moved, so nobody is told.
11822
+ gestureInfo.cancelPosition();
11823
+ return;
11824
+ }
11825
+ // Committed before the answer rather than after: the hand let go of it
11826
+ // there, and a thing that snaps home while a request is in flight says the
11827
+ // gesture was not understood.
11828
+ gestureInfo.commitPosition();
11829
+ try {
11830
+ await onMove?.({
11831
+ gestureInfo,
11832
+ x: xDelta,
11833
+ y: yDelta
11834
+ });
11835
+ } catch {
11836
+ gestureInfo.cancelPositionAnimated();
11837
+ }
11838
+ });
11839
+ return dragGesture;
11840
+ }, {
11841
+ threshold,
11842
+ longPress,
11843
+ longPressDelay,
11844
+ longPressSlop,
11845
+ onPressStart,
11846
+ onPressCancel,
11847
+ onPress
11848
+ });
11849
+ };
11512
11850
 
11513
- [navi-drag-clone] {
11514
- transform: scale(1);
11851
+ // Far and fast, both at once: one without the other is moving the thing while
11852
+ // hesitating, and nothing is thrown away on a hesitation — it comes back.
11853
+ const TOSS_DISTANCE_TO_COMMIT = 110;
11854
+ const TOSS_SPEED_TO_COMMIT = 0.45;
11855
+ const resolveDropMeaning = ({
11856
+ gestureInfo,
11857
+ hasDropTarget,
11858
+ canReorder,
11859
+ canToss,
11860
+ tossDistance = TOSS_DISTANCE_TO_COMMIT,
11861
+ tossSpeed = TOSS_SPEED_TO_COMMIT
11862
+ }) => {
11863
+ if (canToss) {
11864
+ const {
11865
+ xDelta,
11866
+ yDelta
11867
+ } = gestureInfo.layout;
11868
+ const distance = Math.hypot(xDelta, yDelta);
11869
+ if (distance > tossDistance && gestureInfo.velocity > tossSpeed) {
11870
+ return "toss";
11515
11871
  }
11516
11872
  }
11517
- `;
11518
- // At module scope, not inside startDragToReorder: the cursor rules above say who
11519
- // can start a drag, and they have to be true BEFORE anyone drags anything.
11520
- import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_to_reorder.js"];
11521
- const dragCSSVars = ["--drop-hint-size", "--drop-hint-background-color", "--drop-hint-border-radius", "--drop-hint-margin-x", "--drop-hint-margin-y", "--drop-hint-arrow-size", "--drag-clone-scale"];
11873
+ if (canReorder && hasDropTarget) {
11874
+ return "reorder";
11875
+ }
11876
+ return "cancel";
11877
+ };
11522
11878
 
11523
11879
  /**
11524
- * Starts a drag-to-reorder interaction on a list item.
11525
- *
11526
- * Handles the full reorder UX:
11527
- * - Activates only once the intent is established a short movement with a mouse, a long
11528
- * press with a finger (see `dragAfterIntent`), so that neither a click nor a scroll
11529
- * reorders anything by accident.
11530
- * - Clones the grabbed element and moves the clone while the original stays hidden in place
11531
- * (keeps the layout intact so other items don't shift during the drag).
11532
- * - CSS vars (`--drop-hint-size`, `--drop-hint-background-color`, etc.) are read from the
11533
- * dragged element and moved to `document.documentElement` for the duration of the drag so
11534
- * the drop-hint and clone — both in `document.body` — can inherit them.
11535
- * - Shows a drop-hint line indicating where the item will land.
11536
- * - Drop-target detection is intersection-based: the clone's bounding rect is compared
11537
- * against every item that matches `itemSelector` in the scroll container.
11538
- * - No-ops are filtered: releasing on the grabbed element itself, or in a position that
11539
- * would leave it at exactly the same index, never triggers `onReorder`.
11540
- * - On a valid drop, the clone animates to the drop position via the View Transitions API,
11541
- * `onReorder` is called inside the transition callback so the DOM update and the animation
11542
- * are captured together, then the clone is removed.
11543
- * - On a cancelled drop (pointer released with no valid target), the clone is removed
11544
- * immediately without calling `onReorder`.
11545
- *
11546
- * IDs are used as the bridge between DOM elements and JS state because:
11547
- * - Not all DOM elements matching `itemSelector` may be valid drop targets
11548
- * (holes in the structure), so DOM indices don't reliably map to state indices.
11549
- * - Virtual lists render fewer DOM nodes than the total item count, so
11550
- * DOM-index-based counting would be wrong.
11551
- *
11552
- * Any option not listed below is forwarded to `createDragToMoveGestureController`
11553
- * (`areaConstraint`, `autoScrollAreaPadding`, `stickyFrontiers`…), except
11554
- * `releasePositionEffect`, always `"manual"` here: what moves is the clone, and it
11555
- * is removed on release, so there is no position to commit or cancel.
11556
- *
11557
- * @param {PointerEvent} event
11558
- * The `pointerdown` event that may become a reorder.
11559
- * @param {object} options
11560
- * @param {Element} [options.draggedElement=event.currentTarget]
11561
- * The list item to drag.
11562
- * @param {Element} [options.containerElement=draggedElement.parentElement]
11563
- * Element searched with `itemSelector` to find the items to drop between.
11564
- * @param {string} options.itemSelector
11565
- * CSS selector that matches all list items inside `containerElement`.
11566
- * Used for drop-target detection and no-op filtering.
11567
- * @param {function} options.getItemId
11568
- * Returns the stable ID for a given DOM element.
11569
- * Signature: `getItemId(element) → id`.
11570
- * @param {function} options.onReorder
11571
- * Called when the user drops the item in a new position.
11572
- * Signature: `onReorder(fromId, toId, syncCloneWithDropTarget)`.
11573
- * - `fromId`: stable ID of the dragged item.
11574
- * - `toId`: stable ID of the item to insert before, or `null` to append at the end.
11575
- * - `syncCloneWithDropTarget`: call it synchronously inside a
11576
- * `document.startViewTransition` callback, next to the DOM mutation, so the
11577
- * clone is captured at its landing position.
11578
- * @param {object} [options.direction={ x: false, y: true }]
11579
- * Axes along which dragging is allowed. Passed to `createDragToMoveGestureController`.
11580
- * @param {number} [options.threshold=5]
11581
- * Distance (px) a mouse must travel before the press becomes a drag.
11582
- * @param {boolean|"if-touch"} [options.longPress="if-touch"]
11583
- * Which pointers start the drag by holding still instead of by travelling.
11584
- * @param {number} [options.longPressDelay=400]
11585
- * How long (ms) such a pointer must stay down.
11586
- * @param {number} [options.longPressSlop=8]
11587
- * How far (px) it may drift during that wait before the press is abandoned.
11588
- * @param {function} [options.onPressStart]
11589
- * The pointer went down and the wait began (a cue that the press counts).
11590
- * @param {function} [options.onPressCancel]
11591
- * The pointer moved or lifted before the wait was over.
11592
- * @param {function} [options.onPress]
11593
- * The wait completed and the item is now held (haptics, scale…).
11880
+ * A COPY of the element is carried, and the original keeps its place in the
11881
+ * layout — which is what makes a reorder possible at all: nothing else moves
11882
+ * while the hand looks for a place, so there is a stable row of items to look
11883
+ * between. A throw uses the same copy for the opposite reason: the original stays
11884
+ * until the answer says it is really gone.
11594
11885
  */
11595
- const startDragToReorder = (event, {
11596
- draggedElement = event.currentTarget,
11886
+ const startDragToCarryCopy = (event, {
11887
+ draggedElement,
11888
+ canReorder,
11889
+ canToss,
11890
+ // Something that can be thrown away has to be able to LEAVE. The default of
11891
+ // the layer below keeps what is dragged inside its scroll area, which is right
11892
+ // for a reorder (a row belongs to its list) and makes a throw impossible — the
11893
+ // copy hits the edge of the list and no distance is ever covered, so no throw
11894
+ // ever happens and no sideways movement is even visible.
11895
+ // Destructured with the default here rather than written at the call below: a
11896
+ // caller passing `areaConstraint: undefined` (which is what saying nothing
11897
+ // through an options object looks like) would otherwise put the layer below
11898
+ // back on its own default and undo this.
11899
+ areaConstraint = canToss ? "none" : undefined,
11597
11900
  containerElement = draggedElement.parentElement,
11598
11901
  itemSelector,
11599
11902
  getItemId,
11600
11903
  onReorder,
11904
+ onToss,
11905
+ tossDistance,
11906
+ tossSpeed,
11601
11907
  direction = {
11602
11908
  x: false,
11603
11909
  y: true
@@ -11630,6 +11936,7 @@ const startDragToReorder = (event, {
11630
11936
  const gestureController = createDragToMoveGestureController({
11631
11937
  direction,
11632
11938
  releasePositionEffect: "manual",
11939
+ areaConstraint,
11633
11940
  ...options
11634
11941
  });
11635
11942
  const dragGesture = gestureController.grabViaPointer(event, {
@@ -11639,11 +11946,16 @@ const startDragToReorder = (event, {
11639
11946
  // getDropTargetInfo uses gestureInfo.elementImpacted to compute the dragged rect.
11640
11947
  // Point it at the clone so drop detection tracks the clone's current position.
11641
11948
  dragGesture.gestureInfo.elementImpacted = cloneWrapper;
11642
- const dropHintEl = createDropHint();
11643
- document.body.appendChild(dropHintEl);
11949
+
11950
+ // No place to land, no hint: an element that can only be thrown away has
11951
+ // nowhere to be put.
11952
+ const dropHintEl = canReorder ? createDropHint() : null;
11953
+ if (dropHintEl) {
11954
+ document.body.appendChild(dropHintEl);
11955
+ }
11644
11956
  // The hint first, the clone second: that order is what stacks them in the
11645
11957
  // top layer.
11646
- dropHintEl.showPopover();
11958
+ dropHintEl?.showPopover();
11647
11959
  cloneWrapper.showPopover();
11648
11960
 
11649
11961
  // currentBeforeElement: element before which the grabbed item will be inserted (null = end)
@@ -11651,6 +11963,9 @@ const startDragToReorder = (event, {
11651
11963
  let currentBeforeElement;
11652
11964
  let currentReleaseElement;
11653
11965
  const clearDropHintDOM = () => {
11966
+ if (!dropHintEl) {
11967
+ return;
11968
+ }
11654
11969
  dropHintEl.removeAttribute("data-drop-edge");
11655
11970
  dropHintEl.style.removeProperty("--drop-target-top");
11656
11971
  dropHintEl.style.removeProperty("--drop-target-bottom");
@@ -11663,6 +11978,9 @@ const startDragToReorder = (event, {
11663
11978
  clearDropHintDOM();
11664
11979
  };
11665
11980
  dragGesture.addDragCallback(gestureInfo => {
11981
+ if (!dropHintEl) {
11982
+ return;
11983
+ }
11666
11984
  const allItems = [];
11667
11985
  const items = [];
11668
11986
  for (const el of containerElement.querySelectorAll(itemSelector)) {
@@ -11718,9 +12036,35 @@ const startDragToReorder = (event, {
11718
12036
  });
11719
12037
  dragGesture.addReleaseCallback(async gestureInfo => {
11720
12038
  clearDropHintDOM();
11721
- dropHintEl.remove();
12039
+ dropHintEl?.remove();
11722
12040
  restoreCSSVars();
11723
- if (currentBeforeElement !== undefined) {
12041
+
12042
+ // What THIS release means, from what the element said it can answer. A
12043
+ // throw is asked about first: it is the more insistent of the two, and a
12044
+ // hand that sent the thing across the screen has not asked for it to swap
12045
+ // places with whatever it happened to fly over.
12046
+ const hasDropTarget = currentBeforeElement !== undefined;
12047
+ const dropMeans = resolveDropMeaning({
12048
+ gestureInfo,
12049
+ hasDropTarget,
12050
+ canReorder,
12051
+ canToss,
12052
+ tossDistance,
12053
+ tossSpeed
12054
+ });
12055
+ if (dropMeans === "toss") {
12056
+ // Bake the position the hand left it at, so the flight starts from
12057
+ // there rather than from where the clone was declared.
12058
+ setCloneViewportRect(cloneWrapper, cloneWrapper);
12059
+ gestureInfo.cancelPosition();
12060
+ const gone = await tossCloneAway(cloneWrapper, gestureInfo, onToss);
12061
+ if (!gone) {
12062
+ // It still exists, so the screen has to say so: the copy comes back
12063
+ // over the original, and taking it away then reveals the row in
12064
+ // place.
12065
+ await settleCloneBack(cloneWrapper, draggedElement);
12066
+ }
12067
+ } else if (dropMeans === "reorder" && hasDropTarget) {
11724
12068
  const clone = cloneWrapper.firstElementChild;
11725
12069
  // Bake the current visual position (transform included) into the CSS vars
11726
12070
  // so the clone stays where the user released it when we clear the transform.
@@ -11780,6 +12124,7 @@ const setCloneViewportRect = (cloneWrapper, el) => {
11780
12124
  // so the element expands naturally from where the user clicked.
11781
12125
  // On release, the `navi-drag-clone` attribute is removed inside
11782
12126
  // startViewTransition to drop the scale back to 1 as the "new" state.
12127
+
11783
12128
  // The chevron is the one the table's column drop preview uses, rotated by the
11784
12129
  // CSS above so each cap points into the line.
11785
12130
  const dropHintTemplate = /* html */`
@@ -11808,6 +12153,51 @@ const createDropHint = () => {
11808
12153
  div.innerHTML = dropHintTemplate.trim();
11809
12154
  return div.firstElementChild;
11810
12155
  };
12156
+
12157
+ /**
12158
+ * The copy leaves the screen the way it was thrown, and the caller says what that
12159
+ * meant. Resolves true when it is really gone.
12160
+ *
12161
+ * The answer is asked for WHILE it flies rather than after: the thing is already
12162
+ * far away by the time the request lands, which is the whole point of a gesture
12163
+ * that means "get rid of this" — nobody waits to watch it go.
12164
+ */
12165
+ const tossCloneAway = async (cloneWrapper, gestureInfo, onToss) => {
12166
+ const {
12167
+ xDelta,
12168
+ yDelta
12169
+ } = gestureInfo.layout;
12170
+ const distance = Math.hypot(xDelta, yDelta) || 1;
12171
+ cloneWrapper.dataset.tossed = "away";
12172
+ cloneWrapper.style.translate = `${xDelta / distance * TOSS_DISTANCE}px ${yDelta / distance * TOSS_DISTANCE}px`;
12173
+ try {
12174
+ await onToss?.({
12175
+ gestureInfo
12176
+ });
12177
+ return true;
12178
+ } catch {
12179
+ return false;
12180
+ }
12181
+ };
12182
+
12183
+ /**
12184
+ * It comes back where it came from, and only then is taken away — which is what
12185
+ * makes the original reappear in place rather than blink back into it.
12186
+ *
12187
+ * Flown home on `translate` rather than by rewriting the position vars: the vars
12188
+ * hold where the hand let go, the transition is on translate, and moving the vars
12189
+ * would put the copy there instantly instead of taking it there.
12190
+ */
12191
+ const settleCloneBack = (cloneWrapper, sourceElement) => {
12192
+ const sourceRect = sourceElement.getBoundingClientRect();
12193
+ const releaseLeft = parseFloat(cloneWrapper.style.getPropertyValue("--clone-left"));
12194
+ const releaseTop = parseFloat(cloneWrapper.style.getPropertyValue("--clone-top"));
12195
+ cloneWrapper.dataset.tossed = "back";
12196
+ cloneWrapper.style.translate = `${sourceRect.left - releaseLeft}px ${sourceRect.top - releaseTop}px`;
12197
+ return new Promise(resolve => {
12198
+ setTimeout(resolve, TOSS_DURATION_MS);
12199
+ });
12200
+ };
11811
12201
  const createDragClone = (element, pointerEvent) => {
11812
12202
  const rect = element.getBoundingClientRect();
11813
12203
  const wrapper = document.createElement("div");
@@ -11829,7 +12219,20 @@ const createDragClone = (element, pointerEvent) => {
11829
12219
  wrapper.style.setProperty(property, computedStyle.getPropertyValue(property));
11830
12220
  }
11831
12221
  const elementClone = element.cloneNode(true);
12222
+ // A deep copy copies the ids too, and two elements answering to one id is a
12223
+ // document that lies: getElementById picks whichever comes first, an anchor
12224
+ // resolves to the wrong one, a view-transition-name is claimed twice and the
12225
+ // transition is dropped. The copy is a picture of the thing, not another one of
12226
+ // it — so it answers to no name at all.
12227
+ elementClone.removeAttribute("id");
12228
+ for (const descendantWithId of elementClone.querySelectorAll("[id]")) {
12229
+ descendantWithId.removeAttribute("id");
12230
+ }
11832
12231
  elementClone.setAttribute("navi-drag-clone", "");
12232
+ // What is held is the copy, so it is the copy that must LOOK held: the caller
12233
+ // dresses `[data-grabbed]` on its own element once, and the copy is that element.
12234
+ // (The original wears it too, but it is hidden — see navi-drag-clone-source.)
12235
+ elementClone.setAttribute("data-grabbed", "");
11833
12236
  elementClone.style.viewTransitionName = "navi-drag-clone";
11834
12237
  wrapper.appendChild(elementClone);
11835
12238
  document.body.appendChild(wrapper);
@@ -12004,9 +12407,9 @@ import.meta.css = [/* css */`
12004
12407
  // a press that wandered a pixel is still a press, and nothing budges.
12005
12408
  const DRAG_START_THRESHOLD = 10;
12006
12409
  // How much of a box has to be pulled for letting go to carry on rather than put
12007
- // things back. Under half, because a gesture that has clearly begun is an
12008
- // intention: asking for the box to be dragged all the way across turns a travel
12009
- // into work.
12410
+ // things back, when the caller does not say. Under half, because a gesture that
12411
+ // has clearly begun is an intention: asking for the box to be dragged all the
12412
+ // way across turns a travel into work.
12010
12413
  const DRAG_COMMIT_RATIO = 0.3;
12011
12414
  // A flick travels whatever the distance: the hand said "away" quickly, which is
12012
12415
  // the whole gesture — px/ms of pointer, and a few pixels to tell it from a tap
@@ -12136,7 +12539,8 @@ const travelsAfter = ({
12136
12539
  slack,
12137
12540
  size,
12138
12541
  velocity,
12139
- towardsSomething
12542
+ towardsSomething,
12543
+ commitRatio
12140
12544
  }) => {
12141
12545
  if (!towardsSomething) {
12142
12546
  return false;
@@ -12164,7 +12568,7 @@ const travelsAfter = ({
12164
12568
  // …and going towards it travels whatever the distance: the hand said "away"
12165
12569
  // quickly, which is the whole gesture.
12166
12570
  const flicked = goingFast && Math.abs(pulled) > DRAG_FLICK_DISTANCE;
12167
- return flicked || Math.abs(pulled) > size * DRAG_COMMIT_RATIO;
12571
+ return flicked || Math.abs(pulled) > size * commitRatio;
12168
12572
  };
12169
12573
 
12170
12574
  /**
@@ -12196,6 +12600,11 @@ const travelsAfter = ({
12196
12600
  * pixel since the grab is owed to the hand. The axis comes from the caller
12197
12601
  * rather than from the movement, because there is nothing to decide — what
12198
12602
  * was caught is travelling on one already.
12603
+ * @param {number} [options.commitRatio=0.3] - what fraction of the box has to
12604
+ * be pulled for letting go to carry on rather than put things back. A
12605
+ * fraction and never a distance, so the same gesture asks for the same thing
12606
+ * on a phone and on a wide screen. Speed still answers on its own (see
12607
+ * travelsAfter), whatever this says.
12199
12608
  * @param {(detail: {axis: string, sign: number, target: Element, event: PointerEvent}) => false|{size: number, slack?: number, travelBack?: boolean, travelOn?: boolean}} options.onStart
12200
12609
  * - the finger has picked its axis. Answer `false` to give the gesture up, or
12201
12610
  * with the geometry it walks: `size` (one box along that axis), `slack` (how
@@ -12226,6 +12635,7 @@ const startDragToTravel = (pointerDownEvent, {
12226
12635
  element,
12227
12636
  axes = "xy",
12228
12637
  immediate = false,
12638
+ commitRatio = DRAG_COMMIT_RATIO,
12229
12639
  onStart,
12230
12640
  onPull,
12231
12641
  onEnd,
@@ -12497,7 +12907,8 @@ const startDragToTravel = (pointerDownEvent, {
12497
12907
  slack,
12498
12908
  size,
12499
12909
  velocity,
12500
- towardsSomething
12910
+ towardsSomething,
12911
+ commitRatio
12501
12912
  }),
12502
12913
  cancelled,
12503
12914
  event: releaseEvent
@@ -18312,4 +18723,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
18312
18723
  };
18313
18724
  };
18314
18725
 
18315
- export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, claimWheelGesture, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterIntent, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isPrimaryButtonEvent, isSameColor, isScrollable, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, observeAncestorOpenState, onAncestorReopen, parsePositionArea, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, releaseWheelGesture, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, scrollRoomTowards, setAttribute, setAttributes, setStyles, snapToPixel, startDragToReorder, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, watchWheelTravel, wheelGestureIsTakenFrom };
18726
+ export { EASING, ELEMENT_SIZE_CHANGE, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, applyNewPosition, canScroll, captureScrollState, chainEvent, claimWheelGesture, closestOpenableAncestor, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterIntent, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, findSelfOrAncestorFixedPosition, formatEventSideEffect, getAncestorOpenType, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPositioningScrollOffset, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isAncestorOpen, isPrimaryButtonEvent, isSameColor, isScrollable, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, observeAncestorOpenState, onAncestorReopen, parsePositionArea, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, releaseWheelGesture, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, scrollRoomTowards, setAttribute, setAttributes, setStyles, snapToPixel, startDragTo, startDragToResizeGesture, startDragToTravel, stickyAsRelativeCoords, stringifyStyle, subscribeVisualViewportResizeSettled, subscribeWindowResizeSettled, suppressClickAfterGesture, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect, waitForPressHeld, watchWheelTravel, wheelGestureIsTakenFrom };