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