@jsenv/dom 0.17.9 → 0.17.11

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 +1391 -834
  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
@@ -8470,6 +8623,72 @@ const createDragGestureController = (options = {}) => {
8470
8623
  };
8471
8624
  dragGesture.dragViaPointer = dragViaPointer;
8472
8625
  dragGesture.releaseViaPointer = releaseViaPointer;
8626
+ /*
8627
+ * A press that starts a drag is not a press that starts a selection: the
8628
+ * browser sees a pointer going down on text and moving, and that is its
8629
+ * own gesture — the words under the finger turn blue while the element
8630
+ * travels, and the selection outlives the release.
8631
+ *
8632
+ * Refused from the grab, before any threshold: whether the press becomes a
8633
+ * drag is decided a few pixels later, but the selection is decided at the
8634
+ * FIRST move, and by then it is too late to say no.
8635
+ *
8636
+ * `user-select: none` would say it in CSS, but it would say it to
8637
+ * everybody: the element would stop being selectable even when nobody is
8638
+ * dragging it. Here it is refused for the length of one gesture.
8639
+ */
8640
+ const preventSelectStart = selectStartEvent => {
8641
+ selectStartEvent.preventDefault();
8642
+ };
8643
+ document.addEventListener("selectstart", preventSelectStart);
8644
+ // A press also puts an end to the selection the page was already holding,
8645
+ // the way the browser's own press does: refusing selectstart keeps a new
8646
+ // selection from being made, it says nothing about the one painted before
8647
+ // — which would otherwise sit there through a gesture that has nothing to
8648
+ // do with it.
8649
+ collapseSelection();
8650
+ dragGesture.addReleaseCallback(() => {
8651
+ document.removeEventListener("selectstart", preventSelectStart);
8652
+ });
8653
+ /*
8654
+ * Refusing every selection also refuses the one a press is entitled to
8655
+ * make: a double click selects the word under it, and that selection is
8656
+ * over before the pointer has gone anywhere. It is made here instead,
8657
+ * spelled out (see selectWordAtPoint) rather than left to a browser
8658
+ * heuristic that cannot tell a drag from a click.
8659
+ *
8660
+ * On the document and outliving the gesture, because the gesture is
8661
+ * already over when the second click completes: dblclick comes after
8662
+ * mouseup, the gesture ends at pointerup. It is dropped when it fires, and
8663
+ * otherwise when the next press installs its own — a listener waiting for
8664
+ * a double click that never comes costs nothing until then.
8665
+ */
8666
+ removePendingDoubleClickListener();
8667
+ const onDoubleClick = dblclickEvent => {
8668
+ removePendingDoubleClickListener = NOOP;
8669
+ // A drag that happened is a gesture, not a click: the second press of a
8670
+ // double click can be the one that drags, and what it drags must not end
8671
+ // up selected too.
8672
+ if (dragGesture.gestureInfo.started) {
8673
+ return;
8674
+ }
8675
+ // Text the page says is not selectable stays not selectable: a
8676
+ // programmatic selection goes through `user-select: none` in every
8677
+ // engine — it is a rule about what the USER may start, and the browser
8678
+ // does not read it back when asked directly. Read here so that doing the
8679
+ // browser's work does not also undo what the page asked of it.
8680
+ if (!isSelectable(dblclickEvent.target)) {
8681
+ return;
8682
+ }
8683
+ selectWordAtPoint(dblclickEvent.clientX, dblclickEvent.clientY);
8684
+ };
8685
+ document.addEventListener("dblclick", onDoubleClick, {
8686
+ once: true
8687
+ });
8688
+ removePendingDoubleClickListener = () => {
8689
+ removePendingDoubleClickListener = NOOP;
8690
+ document.removeEventListener("dblclick", onDoubleClick);
8691
+ };
8473
8692
  const cleanup = initializer({
8474
8693
  onMove: dragViaPointer,
8475
8694
  onRelease: releaseViaPointer,
@@ -8662,14 +8881,66 @@ const definePropertyAsReadOnly = (object, propertyName) => {
8662
8881
  value: object[propertyName]
8663
8882
  });
8664
8883
  };
8884
+ const NOOP = () => {};
8885
+ let removePendingDoubleClickListener = NOOP;
8886
+
8887
+ // What a double click selects when the browser is allowed to do it itself: the
8888
+ // word around the caret the click lands on.
8889
+ const selectWordAtPoint = (x, y) => {
8890
+ const caretRange = createCaretRange(x, y);
8891
+ if (!caretRange) {
8892
+ return;
8893
+ }
8894
+ const selection = window.getSelection();
8895
+ selection.removeAllRanges();
8896
+ selection.addRange(caretRange);
8897
+ // "word" is not a position a Range can be built from — it is a movement the
8898
+ // selection knows how to make, and the caret walking to both of its edges is
8899
+ // what draws the word.
8900
+ if (selection.modify) {
8901
+ selection.modify("move", "backward", "word");
8902
+ selection.modify("extend", "forward", "word");
8903
+ }
8904
+ };
8905
+ const createCaretRange = (x, y) => {
8906
+ if (document.caretPositionFromPoint) {
8907
+ const caretPosition = document.caretPositionFromPoint(x, y);
8908
+ if (!caretPosition) {
8909
+ return null;
8910
+ }
8911
+ const range = document.createRange();
8912
+ range.setStart(caretPosition.offsetNode, caretPosition.offset);
8913
+ range.collapse(true);
8914
+ return range;
8915
+ }
8916
+ if (document.caretRangeFromPoint) {
8917
+ return document.caretRangeFromPoint(x, y);
8918
+ }
8919
+ return null;
8920
+ };
8921
+ const isSelectable = element => {
8922
+ if (!element || element.nodeType !== 1) {
8923
+ return true;
8924
+ }
8925
+ const computedStyle = window.getComputedStyle(element);
8926
+ const userSelect = computedStyle.userSelect || computedStyle.webkitUserSelect;
8927
+ return userSelect !== "none";
8928
+ };
8929
+ const collapseSelection = () => {
8930
+ const selection = window.getSelection();
8931
+ if (selection && !selection.isCollapsed) {
8932
+ selection.removeAllRanges();
8933
+ }
8934
+ };
8665
8935
 
8666
8936
  installImportMetaCssBuild(import.meta);/**
8667
8937
  * When a press becomes a drag.
8668
8938
  *
8669
8939
  * A pointer going down on a draggable element is ambiguous — it may be a click,
8670
8940
  * 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.
8941
+ * would steal all the others. This module picks which signal resolves the
8942
+ * ambiguity for the pointer at hand, and only then hands over to the real
8943
+ * gesture.
8673
8944
  *
8674
8945
  * There is one gesture, with a trigger per pointer:
8675
8946
  * - a dedicated handle ([data-drag-handle]) says it outright: drag on contact
@@ -8822,94 +9093,26 @@ const dragAfterLongPress = (grabEvent, dragGestureInitializer, {
8822
9093
  onPressCancel,
8823
9094
  onPress
8824
9095
  }) => {
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;
9096
+ waitForPressHeld(grabEvent, {
9097
+ delay: longPressDelay,
9098
+ slop: longPressSlop,
9099
+ onPressStart,
9100
+ onPressCancel,
9101
+ onPressHeld: (pressEvent, {
9102
+ endPress
9103
+ }) => {
9104
+ onPress?.(pressEvent);
9105
+ // Scrolling is taken away by the gesture itself, from the moment it starts
9106
+ // (see markAsStarted in drag_gesture.js) one place refuses the touchmove,
9107
+ // for every way a drag can begin.
9108
+ const dragGesture = startDragGesture(dragGestureInitializer);
9109
+ if (!dragGesture) {
9110
+ endPress();
9111
+ return;
9112
+ }
9113
+ dragGesture.addReleaseCallback(endPress);
8899
9114
  }
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
9115
  });
8912
- onPressStart?.(grabEvent);
8913
9116
  };
8914
9117
 
8915
9118
  /**
@@ -10609,40 +10812,266 @@ const roundForConstraints = (value) => {
10609
10812
  return Math.round(value * 100) / 100;
10610
10813
  };
10611
10814
 
10612
- const applyStickyFrontiersToAutoScrollArea = (
10613
- autoScrollArea,
10614
- { direction, scrollContainer, dragName },
10815
+ /**
10816
+ * Detects the drop target based on what element is actually under the mouse cursor.
10817
+ * Uses document.elementsFromPoint() to respect visual stacking order naturally.
10818
+ *
10819
+ * @param {Object} gestureInfo - Gesture information
10820
+ * @param {Element[]} targetElements - Array of potential drop target elements
10821
+ * @param {object} [options]
10822
+ * @param {Element} [options.dragElement] - The element being dragged. When provided and
10823
+ * `fallbackToEdge` is true, used to compute the fallback rect.
10824
+ * @param {boolean} [options.fallbackToEdge=false] - When true and the drag element does
10825
+ * not intersect any target, falls back to the first item (if above all items) or the
10826
+ * last item (if below all items) so there is always a valid drop target at list edges.
10827
+ * @returns {Object|null} Drop target info with elementSide or null if no valid target found
10828
+ */
10829
+ const getDropTargetInfo = (
10830
+ gestureInfo,
10831
+ targetElements,
10832
+ { fallbackToEdge = false } = {},
10615
10833
  ) => {
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;
10834
+ const dragElement = gestureInfo.elementImpacted || gestureInfo.element;
10835
+ const dragElementRect = dragElement.getBoundingClientRect();
10836
+ const intersectingTargets = [];
10837
+ let someTargetIsCol;
10838
+ let someTargetIsTr;
10839
+ for (const targetElement of targetElements) {
10840
+ const targetRect = targetElement.getBoundingClientRect();
10841
+ if (!rectangleAreIntersecting(dragElementRect, targetRect)) {
10642
10842
  continue;
10643
10843
  }
10644
- }
10645
-
10844
+ if (!someTargetIsCol && targetElement.tagName === "COL") {
10845
+ someTargetIsCol = true;
10846
+ }
10847
+ if (!someTargetIsTr && targetElement.tagName === "TR") {
10848
+ someTargetIsTr = true;
10849
+ }
10850
+ intersectingTargets.push(targetElement);
10851
+ }
10852
+
10853
+ if (intersectingTargets.length === 0) {
10854
+ if (fallbackToEdge) {
10855
+ const dragElement = gestureInfo.elementImpacted || gestureInfo.element;
10856
+ const dragElementRect = dragElement.getBoundingClientRect();
10857
+ const firstItem = targetElements[0];
10858
+ const lastItem = targetElements[targetElements.length - 1];
10859
+ if (
10860
+ firstItem &&
10861
+ dragElementRect.bottom < firstItem.getBoundingClientRect().top
10862
+ ) {
10863
+ // Drag element is above all items → treat as hovering the first item from the top.
10864
+ return {
10865
+ element: firstItem,
10866
+ elementSide: { x: "start", y: "start" },
10867
+ index: 0,
10868
+ intersectingIndex: 0,
10869
+ intersecting: [firstItem],
10870
+ };
10871
+ }
10872
+ if (
10873
+ lastItem &&
10874
+ dragElementRect.top > lastItem.getBoundingClientRect().bottom
10875
+ ) {
10876
+ // Drag element is below all items → treat as hovering the last item from the bottom.
10877
+ return {
10878
+ element: lastItem,
10879
+ elementSide: { x: "start", y: "end" },
10880
+ index: targetElements.length - 1,
10881
+ intersectingIndex: 0,
10882
+ intersecting: [lastItem],
10883
+ };
10884
+ }
10885
+ }
10886
+ return null;
10887
+ }
10888
+
10889
+ const dragElementCenterX = dragElementRect.left + dragElementRect.width / 2;
10890
+ const dragElementCenterY = dragElementRect.top + dragElementRect.height / 2;
10891
+ // Clamp coordinates to viewport to avoid issues with elementsFromPoint
10892
+ const viewportWidth = document.documentElement.clientWidth;
10893
+ const viewportHeight = document.documentElement.clientHeight;
10894
+ const clientX =
10895
+ dragElementCenterX < 0
10896
+ ? 0
10897
+ : dragElementCenterX > viewportWidth
10898
+ ? viewportWidth - 1
10899
+ : dragElementCenterX;
10900
+ const clientY =
10901
+ dragElementCenterY < 0
10902
+ ? 0
10903
+ : dragElementCenterY > viewportHeight
10904
+ ? viewportHeight - 1
10905
+ : dragElementCenterY;
10906
+
10907
+ // Find the first target element in the stack (topmost visible target)
10908
+ const elementsUnderDragElement = document.elementsFromPoint(clientX, clientY);
10909
+ let targetElement = null;
10910
+ let targetIndex = -1;
10911
+ let intersectingIndex = -1;
10912
+ for (const element of elementsUnderDragElement) {
10913
+ // First, check if the element itself is a target
10914
+ const directIndex = intersectingTargets.indexOf(element);
10915
+ if (directIndex !== -1) {
10916
+ targetElement = element;
10917
+ intersectingIndex = directIndex;
10918
+ break;
10919
+ }
10920
+ // Special case: if element is <td> or <th> and not in targets,
10921
+ // try to find its corresponding <col> element
10922
+ if (!isTableCell(element)) {
10923
+ continue;
10924
+ }
10925
+ try_col: {
10926
+ if (!someTargetIsCol) {
10927
+ break try_col;
10928
+ }
10929
+ const tableCellCol = findTableCellCol(element);
10930
+ if (!tableCellCol) {
10931
+ break try_col;
10932
+ }
10933
+ const colIndex = intersectingTargets.indexOf(tableCellCol);
10934
+ if (colIndex === -1) {
10935
+ break try_col;
10936
+ }
10937
+ targetElement = tableCellCol;
10938
+ intersectingIndex = colIndex;
10939
+ break;
10940
+ }
10941
+ try_tr: {
10942
+ if (!someTargetIsTr) {
10943
+ break try_tr;
10944
+ }
10945
+ const tableRow = element.closest("tr");
10946
+ const rowIndex = targetElements.indexOf(tableRow);
10947
+ if (rowIndex === -1) {
10948
+ break try_tr;
10949
+ }
10950
+ targetElement = tableRow;
10951
+ intersectingIndex = intersectingTargets.indexOf(tableRow);
10952
+ break;
10953
+ }
10954
+ }
10955
+ if (!targetElement) {
10956
+ targetElement = intersectingTargets[0];
10957
+ intersectingIndex = 0;
10958
+ }
10959
+ targetIndex = targetElements.indexOf(targetElement);
10960
+
10961
+ // Determine position within the target for both axes.
10962
+ //
10963
+ // Use the leading edge of the dragged element (in the direction of movement)
10964
+ // compared against the target's center:
10965
+ // - Dragging down: "after" as soon as the bottom crosses the target center.
10966
+ // - Dragging up: "before" as soon as the top crosses the target center.
10967
+ // - Not moving: center-vs-center fallback.
10968
+ //
10969
+ // This gives consistent, predictable thresholds regardless of element size.
10970
+ const targetRect = targetElement.getBoundingClientRect();
10971
+ const targetCenterX = targetRect.left + targetRect.width / 2;
10972
+ const targetCenterY = targetRect.top + targetRect.height / 2;
10973
+ const { intentGoingDown, intentGoingUp, intentGoingRight, intentGoingLeft } =
10974
+ gestureInfo;
10975
+ let sideY;
10976
+ if (intentGoingDown) {
10977
+ sideY = dragElementRect.bottom > targetCenterY ? "end" : "start";
10978
+ } else if (intentGoingUp) {
10979
+ sideY = dragElementRect.top < targetCenterY ? "start" : "end";
10980
+ } else {
10981
+ sideY = dragElementCenterY < targetCenterY ? "start" : "end";
10982
+ }
10983
+ let sideX;
10984
+ if (intentGoingRight) {
10985
+ sideX = dragElementRect.right > targetCenterX ? "end" : "start";
10986
+ } else if (intentGoingLeft) {
10987
+ sideX = dragElementRect.left < targetCenterX ? "start" : "end";
10988
+ } else {
10989
+ sideX = dragElementCenterX < targetCenterX ? "start" : "end";
10990
+ }
10991
+ const result = {
10992
+ // NOTE: avoid relying on `index` in application code. The targetElements
10993
+ // array may be dynamically filtered (e.g. excluding the grabbed element),
10994
+ // making this index inconsistent with the full list. Use `element` instead
10995
+ // and look up its position yourself from your own data source.
10996
+ index: targetIndex,
10997
+ element: targetElement,
10998
+ elementSide: {
10999
+ x: sideX,
11000
+ y: sideY,
11001
+ },
11002
+ // Index within the intersecting subset — could be useful to know how many
11003
+ // elements were overlapping, but rarely needed in practice
11004
+ intersectingIndex,
11005
+ intersecting: intersectingTargets,
11006
+ };
11007
+ return result;
11008
+ };
11009
+
11010
+ const rectangleAreIntersecting = (r1, r2) => {
11011
+ return !(
11012
+ r2.left > r1.right ||
11013
+ r2.right < r1.left ||
11014
+ r2.top > r1.bottom ||
11015
+ r2.bottom < r1.top
11016
+ );
11017
+ };
11018
+
11019
+ const isTableCell = (el) => {
11020
+ return el.tagName === "TD" || el.tagName === "TH";
11021
+ };
11022
+
11023
+ /**
11024
+ * Find the corresponding <col> element for a given <td> or <th> cell
11025
+ * @param {Element} cellElement - The <td> or <th> element
11026
+ * @param {Element[]} targetColElements - Array of <col> elements to search in
11027
+ * @returns {Element|null} The corresponding <col> element or null if not found
11028
+ */
11029
+ const findTableCellCol = (cellElement) => {
11030
+ const table = cellElement.closest("table");
11031
+ const colgroup = table.querySelector("colgroup");
11032
+ if (!colgroup) {
11033
+ return null;
11034
+ }
11035
+ const cols = colgroup.querySelectorAll("col");
11036
+ const columnIndex = cellElement.cellIndex;
11037
+ const correspondingCol = cols[columnIndex];
11038
+ return correspondingCol;
11039
+ };
11040
+
11041
+ const applyStickyFrontiersToAutoScrollArea = (
11042
+ autoScrollArea,
11043
+ { direction, scrollContainer, dragName },
11044
+ ) => {
11045
+ let { left, right, top, bottom } = autoScrollArea;
11046
+
11047
+ if (direction.x) {
11048
+ const horizontalStickyFrontiers = createStickyFrontierOnAxis(
11049
+ scrollContainer,
11050
+ {
11051
+ name: dragName,
11052
+ scrollContainer,
11053
+ primarySide: "left",
11054
+ oppositeSide: "right",
11055
+ },
11056
+ );
11057
+ for (const horizontalStickyFrontier of horizontalStickyFrontiers) {
11058
+ const { side, bounds, element } = horizontalStickyFrontier;
11059
+ if (side === "left") {
11060
+ if (bounds.right <= left) {
11061
+ continue;
11062
+ }
11063
+ left = bounds.right;
11064
+ continue;
11065
+ }
11066
+ // right
11067
+ if (bounds.left >= right) {
11068
+ continue;
11069
+ }
11070
+ right = bounds.left;
11071
+ continue;
11072
+ }
11073
+ }
11074
+
10646
11075
  if (direction.y) {
10647
11076
  const verticalStickyFrontiers = createStickyFrontierOnAxis(
10648
11077
  scrollContainer,
@@ -10727,34 +11156,355 @@ const createStickyFrontierOnAxis = (
10727
11156
  return matchingStickyFrontiers;
10728
11157
  };
10729
11158
 
10730
- const dragStyleController = createStyleController("drag_to_move");
10731
-
10732
- /**
10733
- * Creates a gesture controller that moves elements via drag.
11159
+ installImportMetaCssBuild(import.meta);/**
11160
+ * A drag, and what it is FOR.
10734
11161
  *
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)
11162
+ * What a hand does is always the same — pick the thing up, carry it, let go — so
11163
+ * the gesture is not what distinguishes these. What distinguishes them is the
11164
+ * outcome the caller asked for, and that is what `startDragTo` takes:
10739
11165
  *
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.
11166
+ * - **move**: it stays where it was put. The element ITSELF travels and keeps the
11167
+ * place the hand gave it.
11168
+ * - **reorder**: it takes a place in a list. A COPY travels while the original
11169
+ * keeps its place in the layout, which is what makes the gesture possible at
11170
+ * all nothing else moves while the hand looks for a place, so there is a
11171
+ * stable row of items to look between.
11172
+ * - **toss**: it is gotten rid of. The same copy, for the opposite reason: the
11173
+ * original stays until the answer says it is really gone.
11174
+ * - **land**: it comes down ON something. Also a copy, and the closest to
11175
+ * `reorder` — the difference is what a target IS: a row of a list is a place
11176
+ * BETWEEN two others, whereas a square of a board is a place of its own, which
11177
+ * may already be taken. So nothing is inserted and nothing is a no-op: the
11178
+ * answer is "this one came down on that one", and what that means (take the
11179
+ * place, swap the two, refuse) is the caller's.
10748
11180
  *
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).
11181
+ * The caller lists which outcomes ITS element can answer, and only the machinery
11182
+ * those need runs: no copy for a move, no drop hint for something that can only be
11183
+ * thrown away, no landing looked for where nothing lands. `reorder` and `toss`
11184
+ * combine (dropped on a row, or thrown off the screen); `move` and `reorder` cannot
11185
+ * both be true of one release, and the caller is the one who must not ask for both.
10753
11186
  *
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`).
11187
+ * `createDragToMoveGestureController` below is the layer under all of that — the
11188
+ * translation, the auto-scroll, the constraints — and stays usable on its own for
11189
+ * anything that is none of the three (a table column being dragged, a sticky
11190
+ * frontier being moved).
11191
+ */
11192
+ const dragStyleController = createStyleController("drag_to_move");
11193
+
11194
+ // How long the copy takes to leave the screen, and to come back. Written into the
11195
+ // CSS below from here: the flight has to be waited for, and a duration living only
11196
+ // in a stylesheet is a timing JS cannot read reliably.
11197
+ const TOSS_DURATION_MS = 320;
11198
+ // Far enough to be off any screen, in the direction the hand was going.
11199
+ const TOSS_DISTANCE = 900;
11200
+ const css$1 = /* css */`
11201
+ /* IT COSTS THE LIST NOTHING: the hint lands on the edge of a row, which for
11202
+ the last one is the very bottom of the scroll area — a line taking up room
11203
+ there would push the scrollable area a few pixels further and make a
11204
+ scrollbar appear (or hide the hint under it) exactly when one is trying to
11205
+ drop at the end. Being fixed is what avoids it: a fixed box has the
11206
+ viewport as containing block, so it is left out of the scrollable overflow
11207
+ of every ancestor and can overhang the list freely. Same for the clone it
11208
+ accompanies. */
11209
+ .navi_drop_hint {
11210
+ /* A popover, so it lands in the top layer: no z-index to bid against the
11211
+ page, and nothing it can be hidden behind. Shown BEFORE the clone, which
11212
+ is what puts the clone above it — the top layer stacks in the order
11213
+ things are shown, and the item being carried should pass over the line
11214
+ rather than under it. The UA styles for [popover] have to be undone:
11215
+ inset:0, margin:auto, a border and a background of its own. */
11216
+ position: fixed;
11217
+ inset: auto;
11218
+ top: var(--drop-hint-y);
11219
+ left: calc(var(--drop-target-left) + var(--drop-hint-margin-x, 0px));
11220
+ display: none;
11221
+ box-sizing: border-box;
11222
+ width: calc(var(--drop-target-width) - 2 * var(--drop-hint-margin-x, 0px));
11223
+ height: var(--drop-hint-size, 3px);
11224
+ margin: 0;
11225
+ padding: 0;
11226
+ color: inherit;
11227
+ background: var(--drop-hint-background-color, #4476ff);
11228
+ border: none;
11229
+ border-radius: var(--drop-hint-border-radius, 2px);
11230
+ transform: translateY(-50%);
11231
+ pointer-events: none;
11232
+ overflow: visible;
11233
+ }
11234
+ .navi_drop_hint[data-drop-edge]:popover-open {
11235
+ display: block;
11236
+ }
11237
+ .navi_drop_hint[data-drop-edge="top"] {
11238
+ --drop-hint-y: calc(
11239
+ var(--drop-target-top) - var(--drop-hint-margin-y, 0px)
11240
+ );
11241
+ }
11242
+ .navi_drop_hint[data-drop-edge="bottom"] {
11243
+ --drop-hint-y: calc(
11244
+ var(--drop-target-bottom) + var(--drop-hint-margin-y, 0px)
11245
+ );
11246
+ }
11247
+ /* A chevron at each end, pointing in: the line alone is easy to lose against
11248
+ a list of borders and separators, two arrows read as "here" at a glance
11249
+ (same idea as the table's column drop preview). They overhang the line,
11250
+ which costs nothing to a box left out of the scrollable area — and the more
11251
+ they stick out, the easier they are to spot. */
11252
+ .navi_drop_hint_cap {
11253
+ position: absolute;
11254
+ top: 50%;
11255
+ display: flex;
11256
+ color: var(--drop-hint-background-color, #4476ff);
11257
+ translate: 0 -50%;
11258
+ }
11259
+ .navi_drop_hint_cap svg {
11260
+ width: var(--drop-hint-arrow-size, 11px);
11261
+ height: var(--drop-hint-arrow-size, 11px);
11262
+ }
11263
+ .navi_drop_hint_cap[data-side="start"] {
11264
+ left: calc(-1 * var(--drop-hint-arrow-size, 11px));
11265
+ rotate: -90deg;
11266
+ }
11267
+ .navi_drop_hint_cap[data-side="end"] {
11268
+ right: calc(-1 * var(--drop-hint-arrow-size, 11px));
11269
+ rotate: 90deg;
11270
+ }
11271
+
11272
+ /* WHERE IT LANDS, when landing is ON a thing rather than between two: the
11273
+ place itself is lit up, because there is no gap to draw a line in. Fixed
11274
+ and in the top layer for the same reasons as the line above. */
11275
+ .navi_drop_surface {
11276
+ position: fixed;
11277
+ inset: auto;
11278
+ top: var(--drop-target-top);
11279
+ left: var(--drop-target-left);
11280
+ display: none;
11281
+ box-sizing: border-box;
11282
+ width: var(--drop-target-width);
11283
+ height: var(--drop-target-height);
11284
+ margin: 0;
11285
+ padding: 0;
11286
+ color: inherit;
11287
+ background: var(--drop-surface-background-color, rgba(68, 118, 255, 0.16));
11288
+ border: var(--drop-surface-border-width, 2px) solid
11289
+ var(--drop-surface-border-color, #4476ff);
11290
+ border-radius: var(--drop-surface-border-radius, 6px);
11291
+ pointer-events: none;
11292
+ overflow: visible;
11293
+ }
11294
+ .navi_drop_surface[data-drop-over]:popover-open {
11295
+ display: block;
11296
+ }
11297
+
11298
+ /* WHO CAN START A DRAG, said in the cursor.
11299
+ A handle exists only to drag, so it shows the hand. A source does not, and
11300
+ the gesture must not claim its cursor: it drags only once the intent shows
11301
+ (a few pixels of travel, or a long press), a plain click on it stays a
11302
+ click, and it is usually something else FIRST — a link, a card one opens.
11303
+ The cursor says what the element is, and a hand insisting on the one thing
11304
+ it can also be would talk over that. So it is left alone — default, and not
11305
+ an I-beam, because dragging across the text does not select it (the gesture
11306
+ takes the pointer; see the selectstart refused in drag_gesture.js) — and
11307
+ whoever puts the drag there asks for the hand when a grab really is the
11308
+ first thing the element offers.
11309
+ An opted-out area keeps both its cursor and its selection, and never starts
11310
+ a drag (see the check in startDragTo).
11311
+ Controls inside a source keep their own cursor: cursor is inherited, and
11312
+ anything setting its own (a button's pointer) wins on itself.
11313
+ Only the resting cursor is set here: what it becomes once a drag is under
11314
+ way belongs to the gesture (see the backdrop in drag_gesture.js), the only
11315
+ thing that knows a drag actually started. */
11316
+ [data-drag-handle] {
11317
+ cursor: grab;
11318
+ }
11319
+ [data-drag-source] {
11320
+ cursor: default;
11321
+ }
11322
+ [data-drag-ignore] {
11323
+ cursor: auto;
11324
+ }
11325
+
11326
+ [navi-drag-clone-source] {
11327
+ visibility: hidden;
11328
+ }
11329
+
11330
+ [navi-drag-clone-wrapper] {
11331
+ /* Also a popover (see .navi_drop_hint): in the top layer it is over the
11332
+ page whatever the page's own stacking is, and the coordinates it is
11333
+ given are viewport ones — which is what the pointer carrying it works
11334
+ in. Same UA-style reset as the hint. */
11335
+ position: fixed;
11336
+ inset: auto;
11337
+ top: var(--clone-top);
11338
+ left: var(--clone-left);
11339
+ box-sizing: border-box;
11340
+ width: var(--clone-width);
11341
+ height: var(--clone-height);
11342
+ margin: 0;
11343
+ padding: 0;
11344
+ color: inherit;
11345
+ background: transparent;
11346
+ border: none;
11347
+ /* Carries the chain down to the copy, for an item whose own radius is an
11348
+ "inherit" from the list around it. */
11349
+ border-radius: inherit;
11350
+ opacity: 0.95;
11351
+ pointer-events: none;
11352
+ /* Nothing in a copy being carried by a pointer is text to select: the
11353
+ selection belongs to the original, which is still in the page. This is the
11354
+ one place the rule is unconditional — an element that can be dragged is
11355
+ usually selectable too (a link is both), and forcing it there would take
11356
+ away a selection made from outside the element. */
11357
+ user-select: none;
11358
+ overflow: visible;
11359
+ }
11360
+
11361
+ /* Ce qui a été lancé: il continue dans la direction du geste jusqu'à sortir de
11362
+ l'écran, et revient par le même chemin si la réponse refuse. */
11363
+ [navi-drag-clone-wrapper][data-tossed] {
11364
+ transition:
11365
+ translate ${TOSS_DURATION_MS}ms ease-out,
11366
+ opacity ${TOSS_DURATION_MS}ms ease-out;
11367
+ }
11368
+ [navi-drag-clone-wrapper][data-tossed="away"] {
11369
+ opacity: 0;
11370
+ }
11371
+
11372
+ [navi-drag-clone] {
11373
+ /* Cast by the copy itself rather than by the box around it, so it takes the
11374
+ shape of the thing — a rounded row throws a rounded shadow. Its value is a
11375
+ var read on the copy, which IS the dragged element: what being carried
11376
+ looks like belongs to whoever owns the thing — a row lifted off a list
11377
+ wants this shadow, a sheet of paper leaving a board wants none, and its
11378
+ shade is a theme's business either way. */
11379
+ box-shadow: var(--drag-clone-shadow, 0 12px 28px rgba(0, 0, 0, 0.22));
11380
+ transform: scale(var(--drag-clone-scale, 1.03));
11381
+ transform-origin: var(--drag-origin);
11382
+ transition:
11383
+ transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1),
11384
+ box-shadow 0.15s ease;
11385
+ }
11386
+
11387
+ @starting-style {
11388
+ [navi-drag-clone] {
11389
+ box-shadow: none;
11390
+ transform: scale(1);
11391
+ }
11392
+ }
11393
+ `;
11394
+ // At module scope, not inside startDragTo: the cursor rules above say who
11395
+ // can start a drag, and they have to be true BEFORE anyone drags anything.
11396
+ import.meta.css = [css$1, "@jsenv/dom/src/interaction/drag/drag_to.js"];
11397
+
11398
+ /**
11399
+ * Starts a drag-to-reorder interaction on a list item.
11400
+ *
11401
+ * Handles the full reorder UX:
11402
+ * - Activates only once the intent is established — a short movement with a mouse, a long
11403
+ * press with a finger (see `dragAfterIntent`), so that neither a click nor a scroll
11404
+ * reorders anything by accident.
11405
+ * - Clones the grabbed element and moves the clone while the original stays hidden in place
11406
+ * (keeps the layout intact so other items don't shift during the drag).
11407
+ * - The clone and the drop-hint live in the dragged element's own parent, so the CSS vars
11408
+ * that dress them (`--drag-clone-shadow`, `--drop-hint-size`, …) reach them by plain
11409
+ * inheritance, and so do the rules the list writes for its items.
11410
+ * - Shows a drop-hint line indicating where the item will land.
11411
+ * - Drop-target detection is intersection-based: the clone's bounding rect is compared
11412
+ * against every item that matches `itemSelector` in the scroll container.
11413
+ * - No-ops are filtered: releasing on the grabbed element itself, or in a position that
11414
+ * would leave it at exactly the same index, never triggers `onReorder`.
11415
+ * - On a valid drop, the clone animates to the drop position via the View Transitions API,
11416
+ * `onReorder` is called inside the transition callback so the DOM update and the animation
11417
+ * are captured together, then the clone is removed.
11418
+ * - On a cancelled drop (pointer released with no valid target), the clone is removed
11419
+ * immediately without calling `onReorder`.
11420
+ *
11421
+ * IDs are used as the bridge between DOM elements and JS state because:
11422
+ * - Not all DOM elements matching `itemSelector` may be valid drop targets
11423
+ * (holes in the structure), so DOM indices don't reliably map to state indices.
11424
+ * - Virtual lists render fewer DOM nodes than the total item count, so
11425
+ * DOM-index-based counting would be wrong.
11426
+ *
11427
+ * Any option not listed below is forwarded to `createDragToMoveGestureController`
11428
+ * (`areaConstraint`, `autoScrollAreaPadding`, `stickyFrontiers`…), except
11429
+ * `releasePositionEffect`, always `"manual"` here: what moves is the clone, and it
11430
+ * is removed on release, so there is no position to commit or cancel.
11431
+ *
11432
+ * @param {PointerEvent} event
11433
+ * The `pointerdown` event that may become a reorder.
11434
+ * @param {object} options
11435
+ * @param {Element} [options.draggedElement=event.currentTarget]
11436
+ * The list item to drag.
11437
+ * @param {Element} [options.containerElement=draggedElement.parentElement]
11438
+ * Element searched with `itemSelector` to find the items to drop between.
11439
+ * @param {string} [options.itemSelector]
11440
+ * CSS selector that matches all list items inside `containerElement`.
11441
+ * Used for drop-target detection and no-op filtering. Left out, nothing is a
11442
+ * drop target: no hint is drawn and no reorder can be answered — which is what
11443
+ * a drag that only ever throws the thing away asks for.
11444
+ * @param {function} options.getItemId
11445
+ * Returns the stable ID for a given DOM element.
11446
+ * Signature: `getItemId(element) → id`.
11447
+ * @param {function} options.onReorder
11448
+ * Called when the user drops the item in a new position.
11449
+ * Signature: `onReorder(fromId, toId, syncCloneWithDropTarget)`.
11450
+ * - `fromId`: stable ID of the dragged item.
11451
+ * - `toId`: stable ID of the item to insert before, or `null` to append at the end.
11452
+ * - `syncCloneWithDropTarget`: call it synchronously inside a
11453
+ * `document.startViewTransition` callback, next to the DOM mutation, so the
11454
+ * clone is captured at its landing position.
11455
+ * @param {(detail: {gestureInfo: object, dropTarget: Element|null}) => "reorder"|"toss"|"cancel"} [options.resolveDrop]
11456
+ * What THIS release means, when the answer is not simply "a target was found or
11457
+ * not": the same grab can be meant to reorder or to get rid of the thing, and
11458
+ * only the caller knows which — far and fast is a throw, over a row is a move.
11459
+ * Left out, a drop target reorders and anything else is cancelled.
11460
+ * @param {(detail: {gestureInfo: object}) => Promise|void} [options.onToss]
11461
+ * The release was a throw. The clone leaves the screen the way it was thrown
11462
+ * while this runs; it comes back if the promise rejects, because the thing still
11463
+ * exists and the screen has to say so.
11464
+ * @param {object} [options.direction={ x: false, y: true }]
11465
+ * Axes along which dragging is allowed. Passed to `createDragToMoveGestureController`.
11466
+ * @param {number} [options.threshold=5]
11467
+ * Distance (px) a mouse must travel before the press becomes a drag.
11468
+ * @param {boolean|"if-touch"} [options.longPress="if-touch"]
11469
+ * Which pointers start the drag by holding still instead of by travelling.
11470
+ * @param {number} [options.longPressDelay=400]
11471
+ * How long (ms) such a pointer must stay down.
11472
+ * @param {number} [options.longPressSlop=8]
11473
+ * How far (px) it may drift during that wait before the press is abandoned.
11474
+ * @param {function} [options.onPressStart]
11475
+ * The pointer went down and the wait began (a cue that the press counts).
11476
+ * @param {function} [options.onPressCancel]
11477
+ * The pointer moved or lifted before the wait was over.
11478
+ * @param {function} [options.onPress]
11479
+ * The wait completed and the item is now held (haptics, scale…).
11480
+ */
11481
+
11482
+ /**
11483
+ * Creates a gesture controller that moves elements via drag.
11484
+ *
11485
+ * Wraps `createDragGestureController` and adds:
11486
+ * - Element translation via CSS transform (translate only; other existing transforms are preserved)
11487
+ * - Auto-scroll while dragging near scroll-container edges
11488
+ * - Constraints (area boundaries, obstacle elements)
11489
+ *
11490
+ * The returned controller exposes a `grab(options)` / `grabViaPointer(event, options)` method.
11491
+ * Key grab options:
11492
+ * - `element`: the element whose position drives layout calculations (scroll-container detection,
11493
+ * constraints, auto-scroll). Sets `data-grabbed` during the drag.
11494
+ * - `referenceElement`: optional sticky-frontier / obstacle reference, defaults to `element`.
11495
+ * - `elementToMove`: optional different element to actually translate (e.g. a drag clone).
11496
+ * If omitted, `element` is translated. The translate is read from `dragStyleController`
11497
+ * at grab time so any pre-existing translate is accumulated rather than reset.
11498
+ *
11499
+ * A `transform` already on the moved element (rotate, scale…) is preserved and does
11500
+ * not disturb the movement. `rotate` and `scale` set as individual CSS properties do:
11501
+ * they apply outside `transform`, where nothing the gesture writes can reach them —
11502
+ * put those on a child element instead (a warning says so in dev).
11503
+ *
11504
+ * @param {object} [options]
11505
+ * @param {boolean} [options.stickyFrontiers=true]
11506
+ * Shrinks the auto-scroll area at sticky boundaries (elements with `data-sticky-left` /
11507
+ * `data-sticky-top`).
10758
11508
  * @param {number} [options.autoScrollAreaPadding=0]
10759
11509
  * Extra padding (px) subtracted from each edge of the auto-scroll trigger area.
10760
11510
  * @param {string|object|function} [options.areaConstraint="scroll"]
@@ -10804,12 +11554,13 @@ const createDragToMoveGestureController = ({
10804
11554
  cancelAnimationEasing = "ease-out",
10805
11555
  ...options
10806
11556
  } = {}) => {
10807
- const initGrabToMoveElement = (
10808
- dragGesture,
10809
- { element, referenceElement, elementToMove, convertScrollablePosition },
10810
- ) => {
11557
+ const initGrabToMoveElement = (dragGesture, {
11558
+ element,
11559
+ referenceElement,
11560
+ elementToMove,
11561
+ convertScrollablePosition
11562
+ }) => {
10811
11563
  const scrollContainer = dragGesture.gestureInfo.scrollContainer;
10812
-
10813
11564
  const direction = dragGesture.gestureInfo.direction;
10814
11565
  // elementImpacted is either an externally provided elementToMove (e.g. a drag clone)
10815
11566
  const elementImpacted = elementToMove || element;
@@ -10817,13 +11568,9 @@ const createDragToMoveGestureController = ({
10817
11568
  // (e.g. a drag clone passed by the caller) or the element itself.
10818
11569
  // Capture any pre-existing translate so we can accumulate on top of it
10819
11570
  // rather than resetting it to zero on the first drag event.
10820
- const transformAtGrab = dragStyleController.getUnderlyingValue(
10821
- elementImpacted,
10822
- "transform",
10823
- );
11571
+ const transformAtGrab = dragStyleController.getUnderlyingValue(elementImpacted, "transform");
10824
11572
  const translateXAtGrab = transformAtGrab.translateX;
10825
11573
  const translateYAtGrab = transformAtGrab.translateY;
10826
-
10827
11574
  const cancelPosition = () => {
10828
11575
  dragStyleController.clear(elementImpacted);
10829
11576
  };
@@ -10833,7 +11580,7 @@ const createDragToMoveGestureController = ({
10833
11580
  // animation has only to bridge the two.
10834
11581
  const cancelPositionAnimated = ({
10835
11582
  duration = cancelAnimationDuration,
10836
- easing = cancelAnimationEasing,
11583
+ easing = cancelAnimationEasing
10837
11584
  } = {}) => {
10838
11585
  const transformWhileHeld = getComputedStyle(elementImpacted).transform;
10839
11586
  cancelPosition();
@@ -10843,10 +11590,14 @@ const createDragToMoveGestureController = ({
10843
11590
  }
10844
11591
  // No fill: the element already sits at its resting transform, the
10845
11592
  // animation only replays the way back to it.
10846
- return elementImpacted.animate(
10847
- [{ transform: transformWhileHeld }, { transform: transformAtRest }],
10848
- { duration, easing },
10849
- );
11593
+ return elementImpacted.animate([{
11594
+ transform: transformWhileHeld
11595
+ }, {
11596
+ transform: transformAtRest
11597
+ }], {
11598
+ duration,
11599
+ easing
11600
+ });
10850
11601
  };
10851
11602
  const commitPosition = () => {
10852
11603
  dragStyleController.commit(elementImpacted);
@@ -10854,7 +11605,6 @@ const createDragToMoveGestureController = ({
10854
11605
  dragGesture.gestureInfo.cancelPosition = cancelPosition;
10855
11606
  dragGesture.gestureInfo.cancelPositionAnimated = cancelPositionAnimated;
10856
11607
  dragGesture.gestureInfo.commitPosition = commitPosition;
10857
-
10858
11608
  dragGesture.addReleaseCallback(() => {
10859
11609
  if (releasePositionEffect === "cancel") {
10860
11610
  cancelPosition();
@@ -10865,7 +11615,6 @@ const createDragToMoveGestureController = ({
10865
11615
  }
10866
11616
  // "manual": caller handles cleanup, do nothing.
10867
11617
  });
10868
-
10869
11618
  let elementWidth;
10870
11619
  let elementHeight;
10871
11620
  {
@@ -10877,7 +11626,6 @@ const createDragToMoveGestureController = ({
10877
11626
  updateElementDimension();
10878
11627
  dragGesture.addBeforeDragCallback(updateElementDimension);
10879
11628
  }
10880
-
10881
11629
  let scrollArea;
10882
11630
  {
10883
11631
  // Snapshot at grab time so that DOM mutations during dragging
@@ -10886,10 +11634,9 @@ const createDragToMoveGestureController = ({
10886
11634
  left: 0,
10887
11635
  top: 0,
10888
11636
  right: scrollContainer.scrollWidth,
10889
- bottom: scrollContainer.scrollHeight,
11637
+ bottom: scrollContainer.scrollHeight
10890
11638
  };
10891
11639
  }
10892
-
10893
11640
  let scrollport;
10894
11641
  let autoScrollArea;
10895
11642
  {
@@ -10900,14 +11647,11 @@ const createDragToMoveGestureController = ({
10900
11647
  scrollport = getScrollport(scrollBox, scrollContainer);
10901
11648
  autoScrollArea = scrollport;
10902
11649
  if (stickyFrontiers) {
10903
- autoScrollArea = applyStickyFrontiersToAutoScrollArea(
10904
- autoScrollArea,
10905
- {
10906
- scrollContainer,
10907
- direction,
10908
- // dragGestureName,
10909
- },
10910
- );
11650
+ autoScrollArea = applyStickyFrontiersToAutoScrollArea(autoScrollArea, {
11651
+ scrollContainer,
11652
+ direction
11653
+ // dragGestureName,
11654
+ });
10911
11655
  }
10912
11656
  if (autoScrollAreaPadding > 0) {
10913
11657
  autoScrollArea = {
@@ -10918,7 +11662,7 @@ const createDragToMoveGestureController = ({
10918
11662
  left: autoScrollArea.left + autoScrollAreaPadding,
10919
11663
  top: autoScrollArea.top + autoScrollAreaPadding,
10920
11664
  right: autoScrollArea.right - autoScrollAreaPadding,
10921
- bottom: autoScrollArea.bottom - autoScrollAreaPadding,
11665
+ bottom: autoScrollArea.bottom - autoScrollAreaPadding
10922
11666
  };
10923
11667
  }
10924
11668
  };
@@ -10941,97 +11685,77 @@ const createDragToMoveGestureController = ({
10941
11685
  obstacleAttributeName,
10942
11686
  showConstraintFeedbackLine,
10943
11687
  showDebugMarkers,
10944
- referenceElement,
11688
+ referenceElement
10945
11689
  });
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
- );
10963
- },
10964
- );
10965
-
10966
- const dragToMove = (gestureInfo) => {
10967
- const { isGoingDown, isGoingUp, isGoingLeft, isGoingRight, layout } =
10968
- gestureInfo;
11690
+ dragGesture.addBeforeDragCallback((layoutRequested, currentLayout, limitLayout, {
11691
+ dragEvent
11692
+ }) => {
11693
+ dragConstraints.applyConstraints(layoutRequested, currentLayout, limitLayout, {
11694
+ elementWidth,
11695
+ elementHeight,
11696
+ scrollArea,
11697
+ scrollport,
11698
+ hasCrossedScrollportLeftOnce,
11699
+ hasCrossedScrollportTopOnce,
11700
+ autoScrollArea,
11701
+ dragEvent
11702
+ });
11703
+ });
11704
+ const dragToMove = gestureInfo => {
11705
+ const {
11706
+ isGoingDown,
11707
+ isGoingUp,
11708
+ isGoingLeft,
11709
+ isGoingRight,
11710
+ layout
11711
+ } = gestureInfo;
10969
11712
  const left = layout.left;
10970
11713
  const top = layout.top;
10971
11714
  const right = left + elementWidth;
10972
11715
  const bottom = top + elementHeight;
10973
-
10974
11716
  {
10975
- hasCrossedScrollportLeftOnce =
10976
- hasCrossedScrollportLeftOnce || left < scrollport.left;
10977
- hasCrossedScrollportTopOnce =
10978
- hasCrossedScrollportTopOnce || top < scrollport.top;
10979
-
10980
- const getScrollMove = (axis) => {
11717
+ hasCrossedScrollportLeftOnce = hasCrossedScrollportLeftOnce || left < scrollport.left;
11718
+ hasCrossedScrollportTopOnce = hasCrossedScrollportTopOnce || top < scrollport.top;
11719
+ const getScrollMove = axis => {
10981
11720
  const isGoingPositive = axis === "x" ? isGoingRight : isGoingDown;
10982
11721
  if (isGoingPositive) {
10983
11722
  const elementEnd = axis === "x" ? right : bottom;
10984
- const autoScrollAreaEnd =
10985
- axis === "x" ? autoScrollArea.right : autoScrollArea.bottom;
10986
-
11723
+ const autoScrollAreaEnd = axis === "x" ? autoScrollArea.right : autoScrollArea.bottom;
10987
11724
  if (elementEnd <= autoScrollAreaEnd) {
10988
11725
  return 0;
10989
11726
  }
10990
11727
  const scrollAmountNeeded = elementEnd - autoScrollAreaEnd;
10991
11728
  return scrollAmountNeeded;
10992
11729
  }
10993
-
10994
11730
  const isGoingNegative = axis === "x" ? isGoingLeft : isGoingUp;
10995
11731
  if (!isGoingNegative) {
10996
11732
  return 0;
10997
11733
  }
10998
-
10999
11734
  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;
11735
+ const canAutoScrollNegative = axis === "x" ? !referenceOrEl.hasAttribute("data-sticky-left") || hasCrossedScrollportLeftOnce : !referenceOrEl.hasAttribute("data-sticky-top") || hasCrossedScrollportTopOnce;
11006
11736
  if (!canAutoScrollNegative) {
11007
11737
  return 0;
11008
11738
  }
11009
-
11010
11739
  const elementStart = axis === "x" ? left : top;
11011
- const autoScrollAreaStart =
11012
- axis === "x" ? autoScrollArea.left : autoScrollArea.top;
11740
+ const autoScrollAreaStart = axis === "x" ? autoScrollArea.left : autoScrollArea.top;
11013
11741
  if (elementStart >= autoScrollAreaStart) {
11014
11742
  return 0;
11015
11743
  }
11016
-
11017
11744
  const scrollAmountNeeded = autoScrollAreaStart - elementStart;
11018
11745
  return -scrollAmountNeeded;
11019
11746
  };
11020
-
11021
11747
  let scrollLeftTarget;
11022
11748
  let scrollTopTarget;
11023
11749
  if (direction.x) {
11024
11750
  const containerScrollLeftMove = getScrollMove("x");
11025
11751
  if (containerScrollLeftMove) {
11026
- scrollLeftTarget =
11027
- scrollContainer.scrollLeft + containerScrollLeftMove;
11752
+ scrollLeftTarget = scrollContainer.scrollLeft + containerScrollLeftMove;
11028
11753
  }
11029
11754
  }
11030
11755
  if (direction.y) {
11031
11756
  const containerScrollTopMove = getScrollMove("y");
11032
11757
  if (containerScrollTopMove) {
11033
- scrollTopTarget =
11034
- scrollContainer.scrollTop + containerScrollTopMove;
11758
+ scrollTopTarget = scrollContainer.scrollTop + containerScrollTopMove;
11035
11759
  }
11036
11760
  }
11037
11761
  // now we know what to do, do it
@@ -11042,13 +11766,12 @@ const createDragToMoveGestureController = ({
11042
11766
  scrollContainer.scrollTop = scrollTopTarget;
11043
11767
  }
11044
11768
  }
11045
-
11046
11769
  {
11047
- const { scrollableLeft, scrollableTop } = layout;
11048
- const [positionedLeft, positionedTop] = convertScrollablePosition(
11770
+ const {
11049
11771
  scrollableLeft,
11050
- scrollableTop,
11051
- );
11772
+ scrollableTop
11773
+ } = layout;
11774
+ const [positionedLeft, positionedTop] = convertScrollablePosition(scrollableLeft, scrollableTop);
11052
11775
  // Build the transform to apply, preserving any transforms that were
11053
11776
  // already on the element before the grab (e.g. rotate from another
11054
11777
  // controller), and accumulating from the pre-grab translate baseline.
@@ -11060,33 +11783,32 @@ const createDragToMoveGestureController = ({
11060
11783
  // the distance covered. Dragging moves things on screen, so its translate
11061
11784
  // has to come first, whatever else the element carries. The spread still
11062
11785
  // wins on the value when the element already had a translate of its own.
11063
- const transform = { translateX: 0, translateY: 0, ...transformAtGrab };
11786
+ const transform = {
11787
+ translateX: 0,
11788
+ translateY: 0,
11789
+ ...transformAtGrab
11790
+ };
11064
11791
  if (direction.x) {
11065
11792
  const leftTarget = positionedLeft;
11066
11793
  const leftAtGrab = dragGesture.gestureInfo.leftAtGrab;
11067
11794
  const leftDelta = leftTarget - leftAtGrab;
11068
- const translateX = translateXAtGrab
11069
- ? translateXAtGrab + leftDelta
11070
- : leftDelta;
11795
+ const translateX = translateXAtGrab ? translateXAtGrab + leftDelta : leftDelta;
11071
11796
  transform.translateX = translateX;
11072
11797
  }
11073
11798
  if (direction.y) {
11074
11799
  const topTarget = positionedTop;
11075
11800
  const topAtGrab = dragGesture.gestureInfo.topAtGrab;
11076
11801
  const topDelta = topTarget - topAtGrab;
11077
- const translateY = translateYAtGrab
11078
- ? translateYAtGrab + topDelta
11079
- : topDelta;
11802
+ const translateY = translateYAtGrab ? translateYAtGrab + topDelta : topDelta;
11080
11803
  transform.translateY = translateY;
11081
11804
  }
11082
11805
  dragStyleController.set(elementImpacted, {
11083
- transform,
11806
+ transform
11084
11807
  });
11085
11808
  }
11086
11809
  };
11087
11810
  dragGesture.addDragCallback(dragToMove);
11088
11811
  };
11089
-
11090
11812
  const dragGestureController = createDragGestureController(options);
11091
11813
  const grab = dragGestureController.grab;
11092
11814
  dragGestureController.grab = ({
@@ -11094,511 +11816,246 @@ const createDragToMoveGestureController = ({
11094
11816
  referenceElement,
11095
11817
  elementToMove,
11096
11818
  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 } = {},
11143
- ) => {
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)) {
11152
- continue;
11153
- }
11154
- if (!someTargetIsCol && targetElement.tagName === "COL") {
11155
- someTargetIsCol = true;
11156
- }
11157
- if (!someTargetIsTr && targetElement.tagName === "TR") {
11158
- someTargetIsTr = true;
11159
- }
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
- };
11194
- }
11195
- }
11196
- return null;
11197
- }
11198
-
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;
11216
-
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 */`
11379
- /* IN THE PAGE, NOT IN THE LIST: the hint lands on the edge of a row, which
11380
- for the last one is the very bottom of the scroll area — drawn inside it,
11381
- the line would push the scrollable area a few pixels further and make a
11382
- scrollbar appear (or hide the hint under it) exactly when one is trying to
11383
- drop at the end. Placed in the body and positioned in viewport
11384
- coordinates, it can sit anywhere, overhang the list, and cost nothing to
11385
- the layout. Fixed, like the clone it accompanies. */
11386
- .navi_drop_hint {
11387
- /* A popover, so it lands in the top layer: no z-index to bid against the
11388
- page, and nothing it can be hidden behind. Shown BEFORE the clone, which
11389
- is what puts the clone above it — the top layer stacks in the order
11390
- things are shown, and the item being carried should pass over the line
11391
- rather than under it. The UA styles for [popover] have to be undone:
11392
- inset:0, margin:auto, a border and a background of its own. */
11393
- position: fixed;
11394
- inset: auto;
11395
- top: var(--drop-hint-y);
11396
- left: calc(var(--drop-target-left) + var(--drop-hint-margin-x, 0px));
11397
- display: none;
11398
- box-sizing: border-box;
11399
- width: calc(var(--drop-target-width) - 2 * var(--drop-hint-margin-x, 0px));
11400
- height: var(--drop-hint-size, 3px);
11401
- margin: 0;
11402
- padding: 0;
11403
- color: inherit;
11404
- background: var(--drop-hint-background-color, #4476ff);
11405
- border: none;
11406
- border-radius: var(--drop-hint-border-radius, 2px);
11407
- transform: translateY(-50%);
11408
- pointer-events: none;
11409
- overflow: visible;
11410
- }
11411
- .navi_drop_hint[data-drop-edge]:popover-open {
11412
- display: block;
11413
- }
11414
- .navi_drop_hint[data-drop-edge="top"] {
11415
- --drop-hint-y: calc(
11416
- var(--drop-target-top) - var(--drop-hint-margin-y, 0px)
11417
- );
11418
- }
11419
- .navi_drop_hint[data-drop-edge="bottom"] {
11420
- --drop-hint-y: calc(
11421
- var(--drop-target-bottom) + var(--drop-hint-margin-y, 0px)
11422
- );
11423
- }
11424
- /* A chevron at each end, pointing in: the line alone is easy to lose against
11425
- a list of borders and separators, two arrows read as "here" at a glance
11426
- (same idea as the table's column drop preview). They overhang the line,
11427
- which costs nothing now that the hint is out of the scrollable area — and
11428
- the more they stick out, the easier they are to spot. */
11429
- .navi_drop_hint_cap {
11430
- position: absolute;
11431
- top: 50%;
11432
- display: flex;
11433
- color: var(--drop-hint-background-color, #4476ff);
11434
- translate: 0 -50%;
11435
- }
11436
- .navi_drop_hint_cap svg {
11437
- width: var(--drop-hint-arrow-size, 11px);
11438
- height: var(--drop-hint-arrow-size, 11px);
11439
- }
11440
- .navi_drop_hint_cap[data-side="start"] {
11441
- left: calc(-1 * var(--drop-hint-arrow-size, 11px));
11442
- rotate: -90deg;
11443
- }
11444
- .navi_drop_hint_cap[data-side="end"] {
11445
- right: calc(-1 * var(--drop-hint-arrow-size, 11px));
11446
- rotate: 90deg;
11447
- }
11819
+ ...rest
11820
+ } = {}) => {
11821
+ const scrollContainer = getScrollContainer(referenceElement || element);
11822
+ const [elementScrollableLeft, elementScrollableTop, convertScrollablePosition] = createDragElementPositioner(element, referenceElement, elementToMove);
11823
+ const dragGesture = grab({
11824
+ element,
11825
+ scrollContainer,
11826
+ layoutScrollableLeft: elementScrollableLeft,
11827
+ layoutScrollableTop: elementScrollableTop,
11828
+ event,
11829
+ ...rest
11830
+ });
11831
+ initGrabToMoveElement(dragGesture, {
11832
+ element,
11833
+ referenceElement,
11834
+ elementToMove,
11835
+ convertScrollablePosition
11836
+ });
11837
+ return dragGesture;
11838
+ };
11839
+ return dragGestureController;
11840
+ };
11448
11841
 
11449
- /* WHO CAN START A DRAG, said in the cursor.
11450
- A handle drags on the spot, so it shows the hand. A source only drags once
11451
- the intent shows (a few pixels of travel, or a long press) — a plain click
11452
- stays a click but the text inside it cannot be selected (the gesture takes
11453
- the pointer), so an I-beam over it would promise something that does not
11454
- happen: it reads as a plain surface instead. An opted-out area keeps both
11455
- its cursor and its selection, and never starts a drag (see the check in
11456
- startDragToReorder).
11457
- Controls inside a source keep their own cursor: cursor is inherited, and
11458
- anything setting its own (a button's pointer) wins on itself.
11459
- Only the resting cursor is set here: what it becomes once a drag is under
11460
- way belongs to the gesture (see the backdrop in drag_gesture.js), the only
11461
- thing that knows a drag actually started. */
11462
- [data-drag-handle] {
11463
- cursor: grab;
11464
- }
11465
- [data-drag-source] {
11466
- cursor: default;
11467
- user-select: none;
11842
+ /**
11843
+ * Starts a drag, for one or more of the outcomes listed.
11844
+ *
11845
+ * @param {PointerEvent} event The `pointerdown` that may become a drag.
11846
+ * @param {("move"|"reorder"|"toss"|"land")[]} effects
11847
+ * What letting go of this element can mean. `reorder`, `toss` and `land` carry a
11848
+ * copy; `move` carries the element itself. Asking for `move` and `reorder`
11849
+ * together is asking one release to mean two things, and so is asking for
11850
+ * `reorder` and `land`.
11851
+ * @param {object} [options]
11852
+ * @param {Element} [options.draggedElement=event.currentTarget]
11853
+ * @param {(detail: {gestureInfo: object, x: number, y: number}) => Promise|void} [options.onMove]
11854
+ * It was put somewhere. The position is already committed when this runs — the
11855
+ * hand let go of it there — and travels back if the promise rejects.
11856
+ * @param {Element} [options.containerElement=draggedElement.parentElement]
11857
+ * Searched with `itemSelector` for the items to drop between.
11858
+ * @param {string} [options.itemSelector] What matches the items of the list.
11859
+ * @param {function} [options.getItemId] `getItemId(element) → id`.
11860
+ * @param {function} [options.onReorder]
11861
+ * `onReorder(fromId, toId, syncCloneWithDropTarget)` — see its own note below.
11862
+ * @param {(detail: {gestureInfo: object}) => Promise|void} [options.onToss]
11863
+ * It was thrown away. The copy leaves the screen while this runs and comes back
11864
+ * if the promise rejects, because the thing still exists and the screen has to
11865
+ * say so.
11866
+ * @param {function} [options.onLand]
11867
+ * `onLand(fromId, toId, syncCloneWithDropTarget)` — it came down on `toId`, which
11868
+ * is an element and never null: nothing under the copy is a cancelled release.
11869
+ * The copy is held until what comes back settles, exactly like `onReorder`.
11870
+ * `syncCloneWithDropTarget` takes an element when the place is not the shape of
11871
+ * what stands on it: the copy then takes THAT box instead of the target's.
11872
+ * @param {number} [options.tossDistance=110] How far a throw goes, in px.
11873
+ * @param {number} [options.tossSpeed=0.45] And how fast, in px/ms. BOTH are asked
11874
+ * for: one without the other is moving the thing while hesitating, and nothing is
11875
+ * thrown away on a hesitation.
11876
+ *
11877
+ * Everything else is forwarded to `createDragToMoveGestureController`
11878
+ * (`areaConstraint`, `autoScrollAreaPadding`, `direction`…) and to `dragAfterIntent`
11879
+ * (`threshold`, `longPress`, `longPressDelay`, `longPressSlop`, `onPressStart`,
11880
+ * `onPressCancel`, `onPress`).
11881
+ *
11882
+ * About `onReorder`:
11883
+ * - `fromId`: id of the item that moved.
11884
+ * - `toId`: id of the item to insert before, or `null` to append at the end.
11885
+ * - `syncCloneWithDropTarget`: call it synchronously inside a
11886
+ * `document.startViewTransition` callback, next to the DOM mutation, so the copy
11887
+ * is captured at its landing position.
11888
+ * The gesture holds its copy until what `onReorder` returns settles, so returning
11889
+ * the transition is what makes the landing continuous.
11890
+ */
11891
+ const startDragTo = (event, effects, {
11892
+ draggedElement = event.currentTarget,
11893
+ ...options
11894
+ } = {}) => {
11895
+ // An area that opted out of dragging (a text one wants to select, a control that
11896
+ // owns the gesture): the press there is none of our business.
11897
+ if (event.target.closest && event.target.closest("[data-drag-ignore]")) {
11898
+ return undefined;
11468
11899
  }
11469
- [data-drag-ignore] {
11470
- cursor: auto;
11471
- user-select: auto;
11900
+ // A secondary button (right click and friends) is a context menu, not a grab.
11901
+ if (!isPrimaryButtonEvent(event)) {
11902
+ return undefined;
11472
11903
  }
11473
-
11474
- [navi-drag-clone-source] {
11475
- visibility: hidden;
11904
+ const canReorder = effects.includes("reorder");
11905
+ const canToss = effects.includes("toss");
11906
+ const canLand = effects.includes("land");
11907
+ if (canReorder || canToss || canLand) {
11908
+ return startDragToCarryCopy(event, {
11909
+ draggedElement,
11910
+ canReorder,
11911
+ canToss,
11912
+ canLand,
11913
+ ...options
11914
+ });
11476
11915
  }
11916
+ return startDragToMoveElement(event, {
11917
+ draggedElement,
11918
+ ...options
11919
+ });
11920
+ };
11477
11921
 
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;
11500
- }
11922
+ /**
11923
+ * The element ITSELF is carried, and keeps the place the hand gave it.
11924
+ *
11925
+ * No copy, unlike the two others: what is being moved is the thing and not a
11926
+ * stand-in for it, so there is nothing to put back and nothing to reveal.
11927
+ */
11928
+ const startDragToMoveElement = (event, {
11929
+ draggedElement,
11930
+ onMove,
11931
+ threshold,
11932
+ longPress,
11933
+ longPressDelay,
11934
+ longPressSlop,
11935
+ onPressStart,
11936
+ onPressCancel,
11937
+ onPress,
11938
+ ...options
11939
+ }) => {
11940
+ event.preventDefault();
11941
+ return dragAfterIntent(event, () => {
11942
+ const gestureController = createDragToMoveGestureController({
11943
+ releasePositionEffect: "manual",
11944
+ ...options
11945
+ });
11946
+ const dragGesture = gestureController.grabViaPointer(event, {
11947
+ element: draggedElement
11948
+ });
11949
+ if (!dragGesture) {
11950
+ return null;
11951
+ }
11952
+ dragGesture.addReleaseCallback(async gestureInfo => {
11953
+ const {
11954
+ xDelta,
11955
+ yDelta
11956
+ } = gestureInfo.layout;
11957
+ if (!xDelta && !yDelta) {
11958
+ // Picked up and put back down: nothing moved, so nobody is told.
11959
+ gestureInfo.cancelPosition();
11960
+ return;
11961
+ }
11962
+ // Committed before the answer rather than after: the hand let go of it
11963
+ // there, and a thing that snaps home while a request is in flight says the
11964
+ // gesture was not understood.
11965
+ gestureInfo.commitPosition();
11966
+ try {
11967
+ await onMove?.({
11968
+ gestureInfo,
11969
+ x: xDelta,
11970
+ y: yDelta
11971
+ });
11972
+ } catch {
11973
+ gestureInfo.cancelPositionAnimated();
11974
+ }
11975
+ });
11976
+ return dragGesture;
11977
+ }, {
11978
+ threshold,
11979
+ longPress,
11980
+ longPressDelay,
11981
+ longPressSlop,
11982
+ onPressStart,
11983
+ onPressCancel,
11984
+ onPress
11985
+ });
11986
+ };
11501
11987
 
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);
11988
+ // Far and fast, both at once: one without the other is moving the thing while
11989
+ // hesitating, and nothing is thrown away on a hesitation — it comes back.
11990
+ const TOSS_DISTANCE_TO_COMMIT = 110;
11991
+ const TOSS_SPEED_TO_COMMIT = 0.45;
11992
+ const resolveDropMeaning = ({
11993
+ gestureInfo,
11994
+ hasDropTarget,
11995
+ canReorder,
11996
+ canToss,
11997
+ canLand,
11998
+ tossDistance = TOSS_DISTANCE_TO_COMMIT,
11999
+ tossSpeed = TOSS_SPEED_TO_COMMIT
12000
+ }) => {
12001
+ if (canToss) {
12002
+ const {
12003
+ xDelta,
12004
+ yDelta
12005
+ } = gestureInfo.layout;
12006
+ const distance = Math.hypot(xDelta, yDelta);
12007
+ if (distance > tossDistance && gestureInfo.velocity > tossSpeed) {
12008
+ return "toss";
12009
+ }
11506
12010
  }
11507
-
11508
- @starting-style {
11509
- [navi-drag-clone-wrapper] {
11510
- box-shadow: none;
12011
+ if (hasDropTarget) {
12012
+ if (canLand) {
12013
+ return "land";
11511
12014
  }
11512
-
11513
- [navi-drag-clone] {
11514
- transform: scale(1);
12015
+ if (canReorder) {
12016
+ return "reorder";
11515
12017
  }
11516
12018
  }
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"];
12019
+ return "cancel";
12020
+ };
11522
12021
 
11523
12022
  /**
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…).
12023
+ * A COPY of the element is carried, and the original keeps its place in the
12024
+ * layout — which is what makes a reorder possible at all: nothing else moves
12025
+ * while the hand looks for a place, so there is a stable row of items to look
12026
+ * between. A landing on a place of a board is the same, and a throw uses that copy
12027
+ * for the opposite reason: the original stays until the answer says it is really
12028
+ * gone.
11594
12029
  */
11595
- const startDragToReorder = (event, {
11596
- draggedElement = event.currentTarget,
12030
+ const startDragToCarryCopy = (event, {
12031
+ draggedElement,
12032
+ canReorder,
12033
+ canToss,
12034
+ canLand,
12035
+ // Something that can be thrown away has to be able to LEAVE. The default of
12036
+ // the layer below keeps what is dragged inside its scroll area, which is right
12037
+ // for a reorder (a row belongs to its list) and makes a throw impossible — the
12038
+ // copy hits the edge of the list and no distance is ever covered, so no throw
12039
+ // ever happens and no sideways movement is even visible.
12040
+ // Destructured with the default here rather than written at the call below: a
12041
+ // caller passing `areaConstraint: undefined` (which is what saying nothing
12042
+ // through an options object looks like) would otherwise put the layer below
12043
+ // back on its own default and undo this.
12044
+ areaConstraint = canToss ? "none" : undefined,
11597
12045
  containerElement = draggedElement.parentElement,
11598
12046
  itemSelector,
11599
12047
  getItemId,
11600
12048
  onReorder,
11601
- direction = {
12049
+ onLand,
12050
+ onToss,
12051
+ tossDistance,
12052
+ tossSpeed,
12053
+ // A list runs one way and reordering walks it; a board has places all around,
12054
+ // so something landing on one of them goes wherever the hand takes it.
12055
+ direction = canLand ? {
12056
+ x: true,
12057
+ y: true
12058
+ } : {
11602
12059
  x: false,
11603
12060
  y: true
11604
12061
  },
@@ -11624,12 +12081,10 @@ const startDragToReorder = (event, {
11624
12081
  return dragAfterIntent(event, () => {
11625
12082
  const cloneWrapper = createDragClone(draggedElement, event);
11626
12083
  draggedElement.setAttribute("navi-drag-clone-source", "");
11627
- // Move drag related CSS vars from the element to the document
11628
- // so they're accessible to .navi_drop_hint and the clone (which are both in document.body)
11629
- const restoreCSSVars = moveCSSVars(dragCSSVars, draggedElement, document.documentElement);
11630
12084
  const gestureController = createDragToMoveGestureController({
11631
12085
  direction,
11632
12086
  releasePositionEffect: "manual",
12087
+ areaConstraint,
11633
12088
  ...options
11634
12089
  });
11635
12090
  const dragGesture = gestureController.grabViaPointer(event, {
@@ -11639,11 +12094,20 @@ const startDragToReorder = (event, {
11639
12094
  // getDropTargetInfo uses gestureInfo.elementImpacted to compute the dragged rect.
11640
12095
  // Point it at the clone so drop detection tracks the clone's current position.
11641
12096
  dragGesture.gestureInfo.elementImpacted = cloneWrapper;
11642
- const dropHintEl = createDropHint();
11643
- document.body.appendChild(dropHintEl);
12097
+
12098
+ // No place to land, no hint: an element that can only be thrown away has
12099
+ // nowhere to be put. What the hint LOOKS like follows what a place is here —
12100
+ // a line in the gap between two items, or the place itself lit up.
12101
+ const dropHintEl = canLand ? createDropSurface() : canReorder ? createDropHint() : null;
12102
+ if (dropHintEl) {
12103
+ // In the container it draws into, which is where its own vars are set: the
12104
+ // shape of a drop hint is a property of the list or board it belongs to,
12105
+ // and reading it from there is inheritance rather than a hand-off.
12106
+ draggedElement.parentElement.appendChild(dropHintEl);
12107
+ }
11644
12108
  // The hint first, the clone second: that order is what stacks them in the
11645
12109
  // top layer.
11646
- dropHintEl.showPopover();
12110
+ dropHintEl?.showPopover();
11647
12111
  cloneWrapper.showPopover();
11648
12112
 
11649
12113
  // currentBeforeElement: element before which the grabbed item will be inserted (null = end)
@@ -11651,11 +12115,16 @@ const startDragToReorder = (event, {
11651
12115
  let currentBeforeElement;
11652
12116
  let currentReleaseElement;
11653
12117
  const clearDropHintDOM = () => {
12118
+ if (!dropHintEl) {
12119
+ return;
12120
+ }
11654
12121
  dropHintEl.removeAttribute("data-drop-edge");
12122
+ dropHintEl.removeAttribute("data-drop-over");
11655
12123
  dropHintEl.style.removeProperty("--drop-target-top");
11656
12124
  dropHintEl.style.removeProperty("--drop-target-bottom");
11657
12125
  dropHintEl.style.removeProperty("--drop-target-left");
11658
12126
  dropHintEl.style.removeProperty("--drop-target-width");
12127
+ dropHintEl.style.removeProperty("--drop-target-height");
11659
12128
  };
11660
12129
  const clearDropHint = () => {
11661
12130
  currentBeforeElement = undefined;
@@ -11663,6 +12132,9 @@ const startDragToReorder = (event, {
11663
12132
  clearDropHintDOM();
11664
12133
  };
11665
12134
  dragGesture.addDragCallback(gestureInfo => {
12135
+ if (!dropHintEl) {
12136
+ return;
12137
+ }
11666
12138
  const allItems = [];
11667
12139
  const items = [];
11668
12140
  for (const el of containerElement.querySelectorAll(itemSelector)) {
@@ -11672,13 +12144,32 @@ const startDragToReorder = (event, {
11672
12144
  }
11673
12145
  }
11674
12146
  const dropTargetInfo = getDropTargetInfo(gestureInfo, items, {
11675
- fallbackToEdge: true
12147
+ // The edges of a LIST: above the first row means the top of it, below
12148
+ // the last one means the end of it. A board has no such reading — away
12149
+ // from every place is away from every place.
12150
+ fallbackToEdge: !canLand
11676
12151
  });
11677
12152
  gestureInfo.dropTargetInfo = dropTargetInfo || null;
11678
12153
  if (!dropTargetInfo) {
11679
12154
  clearDropHint();
11680
12155
  return;
11681
12156
  }
12157
+ if (canLand) {
12158
+ // The whole element is the target, so which of its edges the copy came
12159
+ // in by says nothing: there is no gap to be on one side of.
12160
+ const dropElement = dropTargetInfo.element;
12161
+ if (dropElement === currentReleaseElement) {
12162
+ return;
12163
+ }
12164
+ currentReleaseElement = dropElement;
12165
+ const dropRect = dropElement.getBoundingClientRect();
12166
+ dropHintEl.setAttribute("data-drop-over", "");
12167
+ dropHintEl.style.setProperty("--drop-target-top", `${dropRect.top}px`);
12168
+ dropHintEl.style.setProperty("--drop-target-left", `${dropRect.left}px`);
12169
+ dropHintEl.style.setProperty("--drop-target-width", `${dropRect.width}px`);
12170
+ dropHintEl.style.setProperty("--drop-target-height", `${dropRect.height}px`);
12171
+ return;
12172
+ }
11682
12173
  // Convert {element, edge} to a beforeElement using the items array
11683
12174
  // (not nextElementSibling, which breaks if non-item elements exist between items).
11684
12175
  // edge "start" → insert before the hovered element
@@ -11718,27 +12209,61 @@ const startDragToReorder = (event, {
11718
12209
  });
11719
12210
  dragGesture.addReleaseCallback(async gestureInfo => {
11720
12211
  clearDropHintDOM();
11721
- dropHintEl.remove();
11722
- restoreCSSVars();
11723
- if (currentBeforeElement !== undefined) {
12212
+ dropHintEl?.remove();
12213
+
12214
+ // What THIS release means, from what the element said it can answer. A
12215
+ // throw is asked about first: it is the more insistent of the two, and a
12216
+ // hand that sent the thing across the screen has not asked for it to swap
12217
+ // places with whatever it happened to fly over.
12218
+ const hasDropTarget = canLand ? currentReleaseElement !== undefined : currentBeforeElement !== undefined;
12219
+ const dropMeans = resolveDropMeaning({
12220
+ gestureInfo,
12221
+ hasDropTarget,
12222
+ canReorder,
12223
+ canToss,
12224
+ canLand,
12225
+ tossDistance,
12226
+ tossSpeed
12227
+ });
12228
+
12229
+ // The copy stops where the hand left it, and the answer is given a way to
12230
+ // take it the rest of the way — synchronously, inside a view transition, so
12231
+ // it is captured where it lands rather than where it was let go of.
12232
+ const landCopyOn = async (targetElement, answer) => {
11724
12233
  const clone = cloneWrapper.firstElementChild;
11725
12234
  // Bake the current visual position (transform included) into the CSS vars
11726
- // so the clone stays where the user released it when we clear the transform.
12235
+ // so the copy stays where the user released it when the transform goes.
11727
12236
  setCloneViewportRect(cloneWrapper, cloneWrapper);
11728
12237
  gestureInfo.cancelPosition();
11729
- const fromId = getItemId(draggedElement);
11730
- const toId = currentBeforeElement ? getItemId(currentBeforeElement) : null;
11731
- // provide onReorder a way to synchronously move the clone to the drop target
11732
- // (meant to be used inside a startViewTransition callback)
11733
- const syncCloneWithDropTarget = () => {
11734
- // Snap the CSS-var position to the drop target rect so the browser
11735
- // captures the "new" state at the landing position.
11736
- setCloneViewportRect(cloneWrapper, currentReleaseElement);
11737
- // Removing this attr drops the CSS scale(1.15), so the browser
11738
- // captures the clone at scale 1 as the "new" state.
12238
+ // Where the copy comes down is not always the thing it came down ON: a
12239
+ // place of a board can be larger than what stands on it, and the copy
12240
+ // has to keep its own size and land where the item will be. Said with
12241
+ // an element, because the caller has one — the piece already standing
12242
+ // there, the empty slot waiting.
12243
+ const syncCloneWithDropTarget = (landingElement = targetElement) => {
12244
+ setCloneViewportRect(cloneWrapper, landingElement);
12245
+ // Removing this attr drops the CSS scale, so the browser captures the
12246
+ // copy at scale 1 as the "new" state.
11739
12247
  clone.removeAttribute("navi-drag-clone");
11740
12248
  };
11741
- await onReorder(fromId, toId, syncCloneWithDropTarget);
12249
+ await answer(syncCloneWithDropTarget);
12250
+ };
12251
+ if (dropMeans === "toss") {
12252
+ // Bake the position the hand left it at, so the flight starts from
12253
+ // there rather than from where the clone was declared.
12254
+ setCloneViewportRect(cloneWrapper, cloneWrapper);
12255
+ gestureInfo.cancelPosition();
12256
+ const gone = await tossCloneAway(cloneWrapper, gestureInfo, onToss);
12257
+ if (!gone) {
12258
+ // It still exists, so the screen has to say so: the copy comes back
12259
+ // over the original, and taking it away then reveals the row in
12260
+ // place.
12261
+ await settleCloneBack(cloneWrapper, draggedElement);
12262
+ }
12263
+ } else if (dropMeans === "land") {
12264
+ await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onLand(getItemId(draggedElement), getItemId(currentReleaseElement), syncCloneWithDropTarget));
12265
+ } else if (dropMeans === "reorder") {
12266
+ await landCopyOn(currentReleaseElement, syncCloneWithDropTarget => onReorder(getItemId(draggedElement), currentBeforeElement ? getItemId(currentBeforeElement) : null, syncCloneWithDropTarget));
11742
12267
  }
11743
12268
  draggedElement.removeAttribute("navi-drag-clone-source");
11744
12269
  cloneWrapper.remove();
@@ -11780,6 +12305,7 @@ const setCloneViewportRect = (cloneWrapper, el) => {
11780
12305
  // so the element expands naturally from where the user clicked.
11781
12306
  // On release, the `navi-drag-clone` attribute is removed inside
11782
12307
  // startViewTransition to drop the scale back to 1 as the "new" state.
12308
+
11783
12309
  // The chevron is the one the table's column drop preview uses, rotated by the
11784
12310
  // CSS above so each cap points into the line.
11785
12311
  const dropHintTemplate = /* html */`
@@ -11808,6 +12334,59 @@ const createDropHint = () => {
11808
12334
  div.innerHTML = dropHintTemplate.trim();
11809
12335
  return div.firstElementChild;
11810
12336
  };
12337
+ const createDropSurface = () => {
12338
+ const div = document.createElement("div");
12339
+ div.className = "navi_drop_surface";
12340
+ // Manual, like the copy it accompanies: it is opened and closed with the drag
12341
+ // and must survive an Escape or a click elsewhere.
12342
+ div.setAttribute("popover", "manual");
12343
+ return div;
12344
+ };
12345
+
12346
+ /**
12347
+ * The copy leaves the screen the way it was thrown, and the caller says what that
12348
+ * meant. Resolves true when it is really gone.
12349
+ *
12350
+ * The answer is asked for WHILE it flies rather than after: the thing is already
12351
+ * far away by the time the request lands, which is the whole point of a gesture
12352
+ * that means "get rid of this" — nobody waits to watch it go.
12353
+ */
12354
+ const tossCloneAway = async (cloneWrapper, gestureInfo, onToss) => {
12355
+ const {
12356
+ xDelta,
12357
+ yDelta
12358
+ } = gestureInfo.layout;
12359
+ const distance = Math.hypot(xDelta, yDelta) || 1;
12360
+ cloneWrapper.dataset.tossed = "away";
12361
+ cloneWrapper.style.translate = `${xDelta / distance * TOSS_DISTANCE}px ${yDelta / distance * TOSS_DISTANCE}px`;
12362
+ try {
12363
+ await onToss?.({
12364
+ gestureInfo
12365
+ });
12366
+ return true;
12367
+ } catch {
12368
+ return false;
12369
+ }
12370
+ };
12371
+
12372
+ /**
12373
+ * It comes back where it came from, and only then is taken away — which is what
12374
+ * makes the original reappear in place rather than blink back into it.
12375
+ *
12376
+ * Flown home on `translate` rather than by rewriting the position vars: the vars
12377
+ * hold where the hand let go, the transition is on translate, and moving the vars
12378
+ * would put the copy there instantly instead of taking it there.
12379
+ */
12380
+ const settleCloneBack = (cloneWrapper, sourceElement) => {
12381
+ const sourceRect = sourceElement.getBoundingClientRect();
12382
+ const releaseLeft = parseFloat(cloneWrapper.style.getPropertyValue("--clone-left"));
12383
+ const releaseTop = parseFloat(cloneWrapper.style.getPropertyValue("--clone-top"));
12384
+ cloneWrapper.dataset.tossed = "back";
12385
+ cloneWrapper.style.translate = `${sourceRect.left - releaseLeft}px ${sourceRect.top - releaseTop}px`;
12386
+ return new Promise(resolve => {
12387
+ setTimeout(resolve, TOSS_DURATION_MS);
12388
+ });
12389
+ };
11811
12390
  const createDragClone = (element, pointerEvent) => {
11812
12391
  const rect = element.getBoundingClientRect();
11813
12392
  const wrapper = document.createElement("div");
@@ -11821,23 +12400,32 @@ const createDragClone = (element, pointerEvent) => {
11821
12400
  // scale(1.15) expands from where the user clicked, not the element center.
11822
12401
  // These offsets are element-relative so viewport coords are correct here.
11823
12402
  wrapper.style.setProperty("--drag-origin", `${pointerEvent.clientX - rect.left}px ${pointerEvent.clientY - rect.top}px`);
11824
- // The clone is appended to document.body, so it loses inherited styles
11825
- // from the original parent. Copy the computed inherited properties that
11826
- // are most likely to affect visual appearance.
11827
- const computedStyle = getComputedStyle(element.parentElement);
11828
- for (const property of INHERITED_PROPERTIES_TO_COPY_SET) {
11829
- wrapper.style.setProperty(property, computedStyle.getPropertyValue(property));
11830
- }
11831
12403
  const elementClone = element.cloneNode(true);
12404
+ // A deep copy copies the ids too, and two elements answering to one id is a
12405
+ // document that lies: getElementById picks whichever comes first, an anchor
12406
+ // resolves to the wrong one, a view-transition-name is claimed twice and the
12407
+ // transition is dropped. The copy is a picture of the thing, not another one of
12408
+ // it — so it answers to no name at all.
12409
+ elementClone.removeAttribute("id");
12410
+ for (const descendantWithId of elementClone.querySelectorAll("[id]")) {
12411
+ descendantWithId.removeAttribute("id");
12412
+ }
11832
12413
  elementClone.setAttribute("navi-drag-clone", "");
12414
+ // What is held is the copy, so it is the copy that must LOOK held: the caller
12415
+ // dresses `[data-grabbed]` on its own element once, and the copy is that element.
12416
+ // (The original wears it too, but it is hidden — see navi-drag-clone-source.)
12417
+ elementClone.setAttribute("data-grabbed", "");
11833
12418
  elementClone.style.viewTransitionName = "navi-drag-clone";
11834
12419
  wrapper.appendChild(elementClone);
11835
- document.body.appendChild(wrapper);
12420
+ // Beside the thing it copies, so it stands where that thing stands: every
12421
+ // inherited value and every custom property the original reads, the copy reads
12422
+ // too, and a rule written for an item in this list finds the copy as well. The
12423
+ // top layer is what lets it stay there — a popover is painted above the page
12424
+ // whatever its depth in the tree, and being fixed keeps it out of the
12425
+ // scrollable overflow of the list it sits in.
12426
+ element.parentElement.appendChild(wrapper);
11836
12427
  return wrapper;
11837
12428
  };
11838
- const INHERITED_PROPERTIES_TO_COPY_SET = new Set(["color", "font-family", "font-size", "font-weight", "font-style", "line-height", "letter-spacing",
11839
- // in case the item has border-radius: inherit. The clone can inherit too
11840
- "border-radius"]);
11841
12429
 
11842
12430
  const startDragToResizeGesture = (
11843
12431
  pointerdownEvent,
@@ -11989,24 +12577,15 @@ import.meta.css = [/* css */`
11989
12577
  [data-drag-travel*="y"] * {
11990
12578
  overscroll-behavior-y: contain !important;
11991
12579
  }
11992
- :root[${WALKING_ATTRIBUTE}] {
11993
- /* A drag over text selects it on the way, and the blue trail says the
11994
- gesture was understood as something else. Not from the press: a press on
11995
- text IS how one selects it, and only a press that has become a travel has
11996
- said it was about something else — which is also why this cannot be the
11997
- whole answer, and why the selection made meanwhile is dropped by hand
11998
- (see dropSelection). */
11999
- user-select: none;
12000
- }
12001
12580
  `, "@jsenv/dom/src/interaction/drag/drag_to_travel.js"];
12002
12581
 
12003
12582
  // How far a pointer goes before it is a travel rather than a click: below this
12004
12583
  // a press that wandered a pixel is still a press, and nothing budges.
12005
12584
  const DRAG_START_THRESHOLD = 10;
12006
12585
  // 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.
12586
+ // things back, when the caller does not say. Under half, because a gesture that
12587
+ // has clearly begun is an intention: asking for the box to be dragged all the
12588
+ // way across turns a travel into work.
12010
12589
  const DRAG_COMMIT_RATIO = 0.3;
12011
12590
  // A flick travels whatever the distance: the hand said "away" quickly, which is
12012
12591
  // the whole gesture — px/ms of pointer, and a few pixels to tell it from a tap
@@ -12073,29 +12652,6 @@ const axesLeftBy = (axes, fromElement, stopElement, attribute) => {
12073
12652
  return left;
12074
12653
  };
12075
12654
 
12076
- /**
12077
- * What the browser painted blue while it was still allowed to think this press
12078
- * was about text.
12079
- *
12080
- * A mouse dragged across a page selects what it crosses, and it starts doing so
12081
- * from the first pixel — while this is still spending ten of them deciding
12082
- * whether the press is a travel at all. By the time it is one, a trail is
12083
- * already there. `user-select: none` (see the CSS) stops it GROWING, it does not
12084
- * take back what was made, and a selection already under way goes on being
12085
- * extended by some browsers whatever the property says.
12086
- *
12087
- * So it is dropped, and dropped again as it comes back. The cause is outside —
12088
- * one gesture, two things answering it, and the browser answers first — and
12089
- * cannot be removed from here; what can be removed is its trace, on every frame
12090
- * of a travel that is walking.
12091
- */
12092
- const dropSelection = () => {
12093
- const selection = window.getSelection();
12094
- if (selection && !selection.isCollapsed) {
12095
- selection.removeAllRanges();
12096
- }
12097
- };
12098
-
12099
12655
  /**
12100
12656
  * A scroller between the pointer and the box it is in, with room left the way
12101
12657
  * the gesture goes: it gets the gesture, and nothing travels — dragging a row
@@ -12136,7 +12692,8 @@ const travelsAfter = ({
12136
12692
  slack,
12137
12693
  size,
12138
12694
  velocity,
12139
- towardsSomething
12695
+ towardsSomething,
12696
+ commitRatio
12140
12697
  }) => {
12141
12698
  if (!towardsSomething) {
12142
12699
  return false;
@@ -12164,7 +12721,7 @@ const travelsAfter = ({
12164
12721
  // …and going towards it travels whatever the distance: the hand said "away"
12165
12722
  // quickly, which is the whole gesture.
12166
12723
  const flicked = goingFast && Math.abs(pulled) > DRAG_FLICK_DISTANCE;
12167
- return flicked || Math.abs(pulled) > size * DRAG_COMMIT_RATIO;
12724
+ return flicked || Math.abs(pulled) > size * commitRatio;
12168
12725
  };
12169
12726
 
12170
12727
  /**
@@ -12196,6 +12753,11 @@ const travelsAfter = ({
12196
12753
  * pixel since the grab is owed to the hand. The axis comes from the caller
12197
12754
  * rather than from the movement, because there is nothing to decide — what
12198
12755
  * was caught is travelling on one already.
12756
+ * @param {number} [options.commitRatio=0.3] - what fraction of the box has to
12757
+ * be pulled for letting go to carry on rather than put things back. A
12758
+ * fraction and never a distance, so the same gesture asks for the same thing
12759
+ * on a phone and on a wide screen. Speed still answers on its own (see
12760
+ * travelsAfter), whatever this says.
12199
12761
  * @param {(detail: {axis: string, sign: number, target: Element, event: PointerEvent}) => false|{size: number, slack?: number, travelBack?: boolean, travelOn?: boolean}} options.onStart
12200
12762
  * - the finger has picked its axis. Answer `false` to give the gesture up, or
12201
12763
  * with the geometry it walks: `size` (one box along that axis), `slack` (how
@@ -12226,6 +12788,7 @@ const startDragToTravel = (pointerDownEvent, {
12226
12788
  element,
12227
12789
  axes = "xy",
12228
12790
  immediate = false,
12791
+ commitRatio = DRAG_COMMIT_RATIO,
12229
12792
  onStart,
12230
12793
  onPull,
12231
12794
  onEnd,
@@ -12407,9 +12970,6 @@ const startDragToTravel = (pointerDownEvent, {
12407
12970
  };
12408
12971
  document.documentElement.setAttribute(WALKING_ATTRIBUTE, axis);
12409
12972
  }
12410
- // Whatever the press was taken for until now, it was taken for something
12411
- // else (see dropSelection).
12412
- dropSelection();
12413
12973
  const {
12414
12974
  axis
12415
12975
  } = travel;
@@ -12471,10 +13031,6 @@ const startDragToTravel = (pointerDownEvent, {
12471
13031
  return;
12472
13032
  }
12473
13033
  finish();
12474
- // Last chance: the pointer moves once more as it goes up, and by then the
12475
- // attribute above is off — so a trail made on that last move would be the
12476
- // one that stays (see dropSelection).
12477
- dropSelection();
12478
13034
  const {
12479
13035
  axis,
12480
13036
  size,
@@ -12497,7 +13053,8 @@ const startDragToTravel = (pointerDownEvent, {
12497
13053
  slack,
12498
13054
  size,
12499
13055
  velocity,
12500
- towardsSomething
13056
+ towardsSomething,
13057
+ commitRatio
12501
13058
  }),
12502
13059
  cancelled,
12503
13060
  event: releaseEvent
@@ -18312,4 +18869,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
18312
18869
  };
18313
18870
  };
18314
18871
 
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 };
18872
+ 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 };