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