@celestia-island/hikari 0.43.8 → 0.43.9
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/package.json
CHANGED
|
@@ -1547,7 +1547,7 @@ describe("HkTagInput panel geometry and drag reordering", () => {
|
|
|
1547
1547
|
expect(tagTexts(container)).toEqual(["Technology", "Germany", "News"]);
|
|
1548
1548
|
});
|
|
1549
1549
|
|
|
1550
|
-
it("
|
|
1550
|
+
it("follows the row it grabbed when the host reorders the list under it", async () => {
|
|
1551
1551
|
const { events, container, model } = mountTagInput({ modelValue: ["news", "tech", "de"] });
|
|
1552
1552
|
await openPanel(container);
|
|
1553
1553
|
const selected = reorderableRows();
|
|
@@ -1555,8 +1555,9 @@ describe("HkTagInput panel geometry and drag reordering", () => {
|
|
|
1555
1555
|
|
|
1556
1556
|
holdPointer({ x: 10, y: 10 }, { x: 10, y: 130 }, grip(selected[0]));
|
|
1557
1557
|
await settle();
|
|
1558
|
-
// The host rewrites its own order out of band while the drag is held:
|
|
1559
|
-
//
|
|
1558
|
+
// The host rewrites its own order out of band while the drag is held: the
|
|
1559
|
+
// index the drag landed at names a different row now, but the row the
|
|
1560
|
+
// pointer is holding is still the one being moved.
|
|
1560
1561
|
model.value = ["de", "news", "tech"];
|
|
1561
1562
|
await settle();
|
|
1562
1563
|
expect(rowLabels().slice(0, 3)).toEqual(["Germany", "News", "Technology"]);
|
|
@@ -1564,10 +1565,35 @@ describe("HkTagInput panel geometry and drag reordering", () => {
|
|
|
1564
1565
|
releasePointer({ x: 10, y: 130 });
|
|
1565
1566
|
await settle();
|
|
1566
1567
|
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1569
|
-
|
|
1570
|
-
expect(
|
|
1568
|
+
// "News" — the row the pointer grabbed — moved to the slot it was dragged
|
|
1569
|
+
// to, in the strip's own new order: the released gesture is never
|
|
1570
|
+
// silently replaced by a move of whichever row slid into that index.
|
|
1571
|
+
expect(events.modelValue.at(-1)).toEqual(["de", "tech", "news"]);
|
|
1572
|
+
expect(tagTexts(container)).toEqual(["Germany", "Technology", "News"]);
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
it("keeps a dragged chip on the chip it grabbed when a × removes another one mid-drag", async () => {
|
|
1576
|
+
const { events, container } = mountTagInput({ modelValue: ["news", "tech", "de"] });
|
|
1577
|
+
const strip = tags(container);
|
|
1578
|
+
pinStrip(strip, 60, "x");
|
|
1579
|
+
|
|
1580
|
+
// A second pair of eyes on the same list: the × of the FIRST chip is
|
|
1581
|
+
// clicked while the SECOND is being dragged. The field re-lays out under
|
|
1582
|
+
// the held pointer and the dragged chip slides one index left — so the
|
|
1583
|
+
// index the press landed at names the wrong chip by the time the pointer
|
|
1584
|
+
// comes up, and the drop has to stay on the chip the pointer is holding.
|
|
1585
|
+
holdPointer({ x: 70, y: 10 }, { x: 200, y: 10 }, strip[1]);
|
|
1586
|
+
await settle();
|
|
1587
|
+
closeButtons(container)[0].click();
|
|
1588
|
+
await settle();
|
|
1589
|
+
expect(tagTexts(container), "the × landed").toEqual(["Technology", "Germany"]);
|
|
1590
|
+
|
|
1591
|
+
releasePointer({ x: 200, y: 10 });
|
|
1592
|
+
await settle();
|
|
1593
|
+
|
|
1594
|
+
// "Technology" — the grabbed chip — lands last, in the field's own order.
|
|
1595
|
+
expect(events.modelValue.at(-1)).toEqual(["de", "tech"]);
|
|
1596
|
+
expect(tagTexts(container)).toEqual(["Germany", "Technology"]);
|
|
1571
1597
|
});
|
|
1572
1598
|
|
|
1573
1599
|
it("clamps a row drag to the selected group", async () => {
|
|
@@ -460,14 +460,15 @@ export const HkTagInput = defineComponent({
|
|
|
460
460
|
const stop = activeStop.value;
|
|
461
461
|
if (props.disabled || !stop || stop.kind !== "option") return false;
|
|
462
462
|
// A live press OWNS the order — either list's, and from the moment
|
|
463
|
-
// the press lands rather than from the moment it turns into a drag
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
// the
|
|
463
|
+
// the press lands rather than from the moment it turns into a drag.
|
|
464
|
+
// The drag would survive a reorder (it moves the ELEMENT the press
|
|
465
|
+
// grabbed, re-reading its index as it goes), but the item's geometry
|
|
466
|
+
// is the one the press read: reordering the strip under a held pointer
|
|
467
|
+
// leaves the drag resolving against a place its item no longer
|
|
468
|
+
// occupies, which is how a gesture ends up quietly doing nothing. So
|
|
469
|
+
// the chord is inert while a pointer owns the list. It is still
|
|
470
|
+
// consumed by the caller either way (Alt+Arrow is not a plain arrow,
|
|
471
|
+
// and the browser must not act on it).
|
|
471
472
|
if (chipDrag.pressed.value || rowDrag.pressed.value) return false;
|
|
472
473
|
const group = selectedRows.value;
|
|
473
474
|
const from = group.findIndex((option) => option.key === stop.option.key);
|
|
@@ -218,6 +218,56 @@ function scrollStrip(
|
|
|
218
218
|
return { container, items };
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/** A strip whose frame IS the scroller it sits in — the frame's box, its
|
|
222
|
+
* LAID-OUT size (`offsetWidth`/`offsetHeight`, which no transform touches)
|
|
223
|
+
* and its scroll offsets all answer as a browser would, and `scene` lets a
|
|
224
|
+
* test scale and scroll the whole thing mid-gesture. */
|
|
225
|
+
function scrollerStrip(
|
|
226
|
+
count: number,
|
|
227
|
+
size: number,
|
|
228
|
+
height: number,
|
|
229
|
+
): {
|
|
230
|
+
frame: HTMLElement;
|
|
231
|
+
items: HTMLElement[];
|
|
232
|
+
scene: { scale: number; scroll: number; laid: number };
|
|
233
|
+
} {
|
|
234
|
+
const frame = document.createElement("div");
|
|
235
|
+
document.body.appendChild(frame);
|
|
236
|
+
const scene = { scale: 1, scroll: 0, laid: count * size };
|
|
237
|
+
Object.defineProperty(frame, "offsetWidth", { configurable: true, get: () => scene.laid });
|
|
238
|
+
Object.defineProperty(frame, "offsetHeight", { configurable: true, get: () => height });
|
|
239
|
+
Object.defineProperty(frame, "scrollLeft", { configurable: true, get: () => scene.scroll });
|
|
240
|
+
Object.defineProperty(frame, "scrollTop", { configurable: true, get: () => 0 });
|
|
241
|
+
Object.defineProperty(frame, "getBoundingClientRect", {
|
|
242
|
+
configurable: true,
|
|
243
|
+
value: () =>
|
|
244
|
+
asRect({
|
|
245
|
+
left: 0,
|
|
246
|
+
right: scene.laid * scene.scale,
|
|
247
|
+
top: 0,
|
|
248
|
+
bottom: height * scene.scale,
|
|
249
|
+
}),
|
|
250
|
+
});
|
|
251
|
+
const items = Array.from({ length: count }, (_, i) => {
|
|
252
|
+
const el = document.createElement("div");
|
|
253
|
+
Object.defineProperty(el, "getBoundingClientRect", {
|
|
254
|
+
configurable: true,
|
|
255
|
+
value: () => {
|
|
256
|
+
const start = (i * size - scene.scroll) * scene.scale;
|
|
257
|
+
return asRect({
|
|
258
|
+
left: start,
|
|
259
|
+
right: start + size * scene.scale,
|
|
260
|
+
top: 0,
|
|
261
|
+
bottom: height * scene.scale,
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
frame.appendChild(el);
|
|
266
|
+
return el;
|
|
267
|
+
});
|
|
268
|
+
return { frame, items, scene };
|
|
269
|
+
}
|
|
270
|
+
|
|
221
271
|
const scopes: EffectScope[] = [];
|
|
222
272
|
|
|
223
273
|
/** Mount the composable inside a real effect scope (the component case). */
|
|
@@ -874,7 +924,7 @@ describe("usePointerReorder", () => {
|
|
|
874
924
|
|
|
875
925
|
press(handle, items, 0, { x: 10, y: 20 });
|
|
876
926
|
move(150, 20);
|
|
877
|
-
items.
|
|
927
|
+
items.shift(); // the strip drops the very chip the pointer is holding
|
|
878
928
|
move(260, 20);
|
|
879
929
|
up(260, 20);
|
|
880
930
|
expect(drops).toEqual([]);
|
|
@@ -886,15 +936,15 @@ describe("usePointerReorder", () => {
|
|
|
886
936
|
expect(clicks).toEqual(["row"]);
|
|
887
937
|
});
|
|
888
938
|
|
|
889
|
-
it("
|
|
890
|
-
// The caller hands its items over in DISPLAY order,
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
939
|
+
it("follows the element it grabbed when the strip reorders under it", () => {
|
|
940
|
+
// The caller hands its items over in DISPLAY order, and the index a drag
|
|
941
|
+
// landed at is only the way the resolution names the element. A strip
|
|
942
|
+
// that reorders mid-gesture — a host edit from outside, a row toggled by
|
|
943
|
+
// a second pointer, a chip's ×, an Enter, a query that filters rows out —
|
|
944
|
+
// moves the elements under those indices, and the drag has to follow the
|
|
945
|
+
// ELEMENT: resolving the press-time index would move whichever item slid
|
|
946
|
+
// into it, and giving way would throw away a gesture the user is still
|
|
947
|
+
// holding.
|
|
898
948
|
const items = boxes([
|
|
899
949
|
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
900
950
|
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
@@ -907,16 +957,160 @@ describe("usePointerReorder", () => {
|
|
|
907
957
|
expect(handle.dragging.value).toBe(true);
|
|
908
958
|
expect(handle.dragOver.value, "150 is past the first chip's midpoint").toBe(1);
|
|
909
959
|
|
|
910
|
-
// The
|
|
911
|
-
|
|
960
|
+
// The host moves the LAST chip to the front: the strip is now
|
|
961
|
+
// [C, A, B], so the chip the pointer grabbed sits at index 1.
|
|
962
|
+
const [grabbed, middle, last] = items;
|
|
963
|
+
const moved = [last, grabbed, middle];
|
|
964
|
+
moved.forEach((el, i) => reshape(el, { left: i * 100, right: i * 100 + 100, top: 0, bottom: 40 }));
|
|
965
|
+
items.splice(0, items.length, ...moved);
|
|
912
966
|
move(260, 20);
|
|
913
|
-
expect(handle.dragging.value, "the drag
|
|
967
|
+
expect(handle.dragging.value, "the drag is still the one the pointer started").toBe(true);
|
|
968
|
+
expect(handle.dragFrom.value, "the lift follows the chip it grabbed").toBe(1);
|
|
969
|
+
expect(handle.dragOver.value, "260 is past the second chip's midpoint of 250").toBe(2);
|
|
970
|
+
up(260, 20);
|
|
971
|
+
expect(drops, "the drop is reported in the indices the strip has now").toEqual([[1, 2]]);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it("ends the gesture the moment the strip empties", () => {
|
|
975
|
+
// Nothing left to resolve against is the same answer as the element being
|
|
976
|
+
// gone: the gesture is over then, not at the release — a drag left
|
|
977
|
+
// "running" against an empty strip would keep publishing a slot it can no
|
|
978
|
+
// longer read, and would not swallow the click it owes.
|
|
979
|
+
const items = boxes([
|
|
980
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
981
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
982
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
983
|
+
]);
|
|
984
|
+
const { handle, drops } = harness("x", items);
|
|
985
|
+
const inner = document.createElement("span");
|
|
986
|
+
items[0].appendChild(inner);
|
|
987
|
+
const clicks: string[] = [];
|
|
988
|
+
inner.addEventListener("click", () => clicks.push("row"));
|
|
989
|
+
|
|
990
|
+
press(handle, items, 0, { x: 10, y: 20 });
|
|
991
|
+
move(150, 20);
|
|
992
|
+
expect(handle.dragging.value).toBe(true);
|
|
993
|
+
|
|
994
|
+
items.splice(0, items.length); // every item goes at once
|
|
995
|
+
move(260, 20);
|
|
996
|
+
expect(handle.dragging.value, "the gesture ended with the strip").toBe(false);
|
|
997
|
+
expect(handle.dragFrom.value).toBe(-1);
|
|
998
|
+
expect(handle.dragOver.value).toBe(-1);
|
|
999
|
+
up(260, 20);
|
|
1000
|
+
expect(drops).toEqual([]);
|
|
1001
|
+
inner.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
1002
|
+
expect(clicks, "…and its trailing click was swallowed").toEqual([]);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it("abandons a drag whose pressed item leaves the strip", () => {
|
|
1006
|
+
// The other side of the same rule: a strip that no longer carries the
|
|
1007
|
+
// element at all — it was removed, or a drag of it can no longer land
|
|
1008
|
+
// anywhere — has nothing for the gesture to move, so it ends without a
|
|
1009
|
+
// reorder.
|
|
1010
|
+
const items = boxes([
|
|
1011
|
+
{ left: 0, right: 100, top: 0, bottom: 40 },
|
|
1012
|
+
{ left: 100, right: 200, top: 0, bottom: 40 },
|
|
1013
|
+
{ left: 200, right: 300, top: 0, bottom: 40 },
|
|
1014
|
+
]);
|
|
1015
|
+
const { handle, drops } = harness("x", items);
|
|
1016
|
+
|
|
1017
|
+
press(handle, items, 0, { x: 10, y: 20 });
|
|
1018
|
+
move(150, 20);
|
|
1019
|
+
expect(handle.dragOver.value).toBe(1);
|
|
1020
|
+
|
|
1021
|
+
items.shift(); // the chip the pointer is holding is gone
|
|
1022
|
+
move(260, 20);
|
|
1023
|
+
expect(handle.dragging.value, "the drag ended with the strip").toBe(false);
|
|
914
1024
|
expect(handle.dragFrom.value).toBe(-1);
|
|
915
1025
|
expect(handle.dragOver.value).toBe(-1);
|
|
916
1026
|
up(260, 20);
|
|
917
1027
|
expect(drops, "nothing was dropped").toEqual([]);
|
|
918
1028
|
});
|
|
919
1029
|
|
|
1030
|
+
it("carries the snapshot exactly through a frame that scrolls AND is scaled", () => {
|
|
1031
|
+
// The frame here is the scroller the strip SITS IN — the shape this
|
|
1032
|
+
// composable documents — and the host zooms it mid-drag while it is
|
|
1033
|
+
// scrolled. The frame's own scroll slides the content inside a box that
|
|
1034
|
+
// does not move, and it does so in the frame's LAYOUT units: taken off
|
|
1035
|
+
// the drawn box as a raw offset it is measured at the wrong magnitude
|
|
1036
|
+
// the moment the frame is scaled (by `(scale - 1) x scroll`, here 300px
|
|
1037
|
+
// on a 2x zoom), and the snapshot drifts off the item it belongs to. The
|
|
1038
|
+
// pointer here is past the third chip and before the fourth, so the
|
|
1039
|
+
// drifted snapshot answers "stay where you are" where the strip answers
|
|
1040
|
+
// "land after the fourth".
|
|
1041
|
+
const { items, scene } = scrollerStrip(5, 100, 40);
|
|
1042
|
+
const { handle, drops } = harness("x", items);
|
|
1043
|
+
|
|
1044
|
+
press(handle, items, 2, { x: 250, y: 20 });
|
|
1045
|
+
scene.scale = 2; // the host zooms the strip…
|
|
1046
|
+
scene.scroll = 300; // …while it is scrolled under the live drag
|
|
1047
|
+
move(100, 20);
|
|
1048
|
+
expect(handle.dragging.value).toBe(true);
|
|
1049
|
+
expect(handle.dragOver.value, "past the third chip, before the fourth").toBe(3);
|
|
1050
|
+
up(100, 20);
|
|
1051
|
+
expect(drops).toEqual([[2, 3]]);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it("reads a LAYOUT change as a layout change, not as a scale", () => {
|
|
1055
|
+
// The frame's own box growing is not the frame being drawn bigger: the
|
|
1056
|
+
// chips keep their place and their size, and the frame merely got wider
|
|
1057
|
+
// around them. A carrier that reads the drawn box alone cannot tell the
|
|
1058
|
+
// two apart, and would stretch the snapshot by the ratio of the two
|
|
1059
|
+
// sizes — here it would answer "stay where you are" for a pointer the
|
|
1060
|
+
// strip answers "land after the third chip".
|
|
1061
|
+
const { items, scene } = scrollerStrip(5, 100, 40);
|
|
1062
|
+
const { handle, drops } = harness("x", items);
|
|
1063
|
+
|
|
1064
|
+
press(handle, items, 1, { x: 150, y: 20 });
|
|
1065
|
+
scene.laid = 1000; // the frame is re-laid out around the same chips
|
|
1066
|
+
move(260, 20);
|
|
1067
|
+
expect(handle.dragging.value).toBe(true);
|
|
1068
|
+
expect(handle.dragOver.value, "260 is past the third chip's midpoint of 250").toBe(2);
|
|
1069
|
+
up(260, 20);
|
|
1070
|
+
expect(drops).toEqual([[1, 2]]);
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
it("carries the snapshot in the nearest frame that has a box", () => {
|
|
1074
|
+
// A strip wrapped in an element that generates no box of its own
|
|
1075
|
+
// (`display: contents`) is laid out by the next box up, and that is the
|
|
1076
|
+
// box its content moves with: reading the boxless wrapper leaves the
|
|
1077
|
+
// snapshot behind the moment the page scrolls the real frame.
|
|
1078
|
+
const frame = document.createElement("div");
|
|
1079
|
+
const wrapper = document.createElement("div");
|
|
1080
|
+
frame.appendChild(wrapper);
|
|
1081
|
+
document.body.appendChild(frame);
|
|
1082
|
+
const scene = { shift: 0 };
|
|
1083
|
+
reshape(frame, { left: 0, right: 300, top: 0, bottom: 40 });
|
|
1084
|
+
Object.defineProperty(frame, "getBoundingClientRect", {
|
|
1085
|
+
configurable: true,
|
|
1086
|
+
value: () => {
|
|
1087
|
+
const box = { left: 0, right: 300, top: scene.shift, bottom: scene.shift + 40 };
|
|
1088
|
+
return asRect(box);
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
// The wrapper itself: no box at all, exactly as `display: contents` reports.
|
|
1092
|
+
reshape(wrapper, { left: 0, right: 0, top: 0, bottom: 0 });
|
|
1093
|
+
const items = [0, 100, 200].map((start) => {
|
|
1094
|
+
const el = document.createElement("div");
|
|
1095
|
+
Object.defineProperty(el, "getBoundingClientRect", {
|
|
1096
|
+
configurable: true,
|
|
1097
|
+
value: () =>
|
|
1098
|
+
asRect({ left: start, right: start + 100, top: scene.shift, bottom: scene.shift + 40 }),
|
|
1099
|
+
});
|
|
1100
|
+
wrapper.appendChild(el);
|
|
1101
|
+
return el;
|
|
1102
|
+
});
|
|
1103
|
+
const { handle, drops } = harness("x", items);
|
|
1104
|
+
|
|
1105
|
+
press(handle, items, 0, { x: 10, y: 20 });
|
|
1106
|
+
scene.shift = 40; // the page scrolls the strip a line up under the drag
|
|
1107
|
+
move(150, 20);
|
|
1108
|
+
expect(handle.dragging.value).toBe(true);
|
|
1109
|
+
expect(handle.dragOver.value, "the chips kept their places inside the frame").toBe(1);
|
|
1110
|
+
up(150, 20);
|
|
1111
|
+
expect(drops).toEqual([[0, 1]]);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
920
1114
|
it("keeps the pressed item's geometry on the content when the frame is SCALED under it", () => {
|
|
921
1115
|
// A `scale()` or a zoom painted on the frame — or on an ancestor — during
|
|
922
1116
|
// a live drag is not a movement the frame's DISPLACEMENT can describe:
|
|
@@ -75,8 +75,10 @@ export interface PointerReorder {
|
|
|
75
75
|
* crossed the threshold into a drag. The strip is this gesture's until
|
|
76
76
|
* it ends, so a caller that can change the order by other means (the
|
|
77
77
|
* panel's own keyboard reorder, a host edit) can hold off while this is
|
|
78
|
-
* `true`: the drag
|
|
79
|
-
* is
|
|
78
|
+
* `true`: the drag moves the element the press grabbed and the snapshot
|
|
79
|
+
* it resolves by is the one the press read, so an order that reorders the
|
|
80
|
+
* strip under the gesture is an order resolved against geometry its item
|
|
81
|
+
* no longer occupies. */
|
|
80
82
|
pressed: Ref<boolean>;
|
|
81
83
|
/** Watch a press on item `index` (item 0 of `items()` is index 0).
|
|
82
84
|
* Nothing is reported until the pointer travels past the threshold,
|
|
@@ -191,7 +193,13 @@ export function indexAt(
|
|
|
191
193
|
* before the lift is painted on it (`start`) and carried along with the
|
|
192
194
|
* frame it was read in, however that frame moves under the drag — so no
|
|
193
195
|
* transform an item picks up mid-drag can move the line it is dragged along
|
|
194
|
-
* or the midpoint it is compared against.
|
|
196
|
+
* or the midpoint it is compared against. The drag is moved BY ELEMENT: the
|
|
197
|
+
* press records the element it landed on and every resolution re-reads that
|
|
198
|
+
* element's index from the live strip, so a list that reorders under the
|
|
199
|
+
* gesture (a host edit, a second pointer, a removal elsewhere) keeps the
|
|
200
|
+
* drop on what the pointer is holding instead of on whatever slid into the
|
|
201
|
+
* index it started at — and a gesture whose element is gone from the strip
|
|
202
|
+
* ends without a reorder.
|
|
195
203
|
*
|
|
196
204
|
* The threshold is what keeps the gesture honest on a control that is also
|
|
197
205
|
* clickable: a press under ~6px is not a drag at all — no state is
|
|
@@ -236,15 +244,19 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
236
244
|
let lastMain = 0;
|
|
237
245
|
let lastCross = 0;
|
|
238
246
|
/** The pressed item's LAYOUT geometry (`null` until a press reads it), the
|
|
239
|
-
* FRAME it was laid out in (its parent element) and
|
|
240
|
-
* when the geometry was
|
|
241
|
-
* instead of the item's live rect, which carries the lift.
|
|
247
|
+
* FRAME it was laid out in (its parent element) and that frame's reading
|
|
248
|
+
* when the geometry was taken — the whole drag resolves against this
|
|
249
|
+
* instead of the item's live rect, which carries the lift. The geometry
|
|
250
|
+
* stays the press's even if the ELEMENT is re-anchored to another index
|
|
251
|
+
* mid-gesture (`slotAt`): the pointer grabbed it where it was. */
|
|
242
252
|
let pressedBand: { lo: number; hi: number } | null = null;
|
|
243
253
|
let pressedMid = 0;
|
|
244
254
|
let pressedFrame: HTMLElement | null = null;
|
|
245
|
-
let pressedFrameBox:
|
|
246
|
-
/** The very ELEMENT the press landed on
|
|
247
|
-
* is
|
|
255
|
+
let pressedFrameBox: FrameBox | null = null;
|
|
256
|
+
/** The very ELEMENT the press landed on. The drag is moved BY element:
|
|
257
|
+
* the index is re-read from it on every resolution, so a strip that
|
|
258
|
+
* reorders under the gesture keeps the drop on what the pointer grabbed
|
|
259
|
+
* (`slotAt`). */
|
|
248
260
|
let pressedItem: HTMLElement | null = null;
|
|
249
261
|
/** Auto-scroll: px per frame (signed: negative scrolls back), 0 = off. */
|
|
250
262
|
let scrollVelocity = 0;
|
|
@@ -262,87 +274,145 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
262
274
|
return options.items().filter((el): el is HTMLElement => el != null);
|
|
263
275
|
}
|
|
264
276
|
|
|
265
|
-
/** The slot the live strip resolves for a pointer at (`main`, `cross`)
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* first: that the
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
|
|
277
|
+
/** The slot the live strip resolves for a pointer at (`main`, `cross`).
|
|
278
|
+
* The press is on an ELEMENT, and the index it landed at is only the way
|
|
279
|
+
* the resolution names it — so the index is re-read from the element
|
|
280
|
+
* first: anything that reorders the strip under the gesture (a host edit,
|
|
281
|
+
* a row toggled by a second pointer, a chip's ×, an Enter, a query that
|
|
282
|
+
* filters rows out) moves the elements under those indices, while the
|
|
283
|
+
* element the pointer grabbed is still the one being dragged. The
|
|
284
|
+
* resolution then follows IT, and the caller's `onDrop` gets the indices
|
|
285
|
+
* the strip has now. Only a strip that no longer carries the element at
|
|
286
|
+
* all — it was removed, or a drag of it can no longer land anywhere —
|
|
287
|
+
* ends the gesture, and it ends without a reorder. */
|
|
288
|
+
function slotAt(main: number, cross: number): number {
|
|
275
289
|
const items = liveItems();
|
|
276
|
-
if (items.length === 0)
|
|
277
|
-
if (pressedItem && items[pressedIndex] !== pressedItem) {
|
|
290
|
+
if (items.length === 0) {
|
|
278
291
|
abandonDrag();
|
|
279
292
|
return -1;
|
|
280
293
|
}
|
|
294
|
+
if (pressedItem) {
|
|
295
|
+
const index = items.indexOf(pressedItem);
|
|
296
|
+
if (index < 0) {
|
|
297
|
+
abandonDrag();
|
|
298
|
+
return -1;
|
|
299
|
+
}
|
|
300
|
+
if (index !== pressedIndex) {
|
|
301
|
+
pressedIndex = index;
|
|
302
|
+
if (dragging.value) dragFrom.value = index;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
281
305
|
return indexAt(
|
|
282
306
|
items.map((item) => item.getBoundingClientRect()),
|
|
283
307
|
axis,
|
|
284
308
|
main,
|
|
285
309
|
cross,
|
|
286
|
-
|
|
310
|
+
pressedIndex,
|
|
287
311
|
layoutOrigin(),
|
|
288
312
|
);
|
|
289
313
|
}
|
|
290
314
|
|
|
291
|
-
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* the
|
|
296
|
-
*
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Everything the frame says about where it puts a coordinate: the box the
|
|
317
|
+
* item is laid out in — where it is drawn (`x`/`y`), how big it is drawn
|
|
318
|
+
* (`w`/`h`) and how big it is LAID OUT (`laidW`/`laidH`, which no
|
|
319
|
+
* transform touches, so the two together are the scale it is drawn at) —
|
|
320
|
+
* and the scroll that slides its children inside that box.
|
|
321
|
+
*/
|
|
322
|
+
interface FrameBox {
|
|
323
|
+
x: number;
|
|
324
|
+
y: number;
|
|
325
|
+
w: number;
|
|
326
|
+
h: number;
|
|
327
|
+
laidW: number;
|
|
328
|
+
laidH: number;
|
|
329
|
+
scrollX: number;
|
|
330
|
+
scrollY: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The nearest ancestor of `item` that is actually LAID OUT. A snapshot
|
|
334
|
+
* can only be carried in a frame that has a box to move it: an element
|
|
335
|
+
* with `display: contents` generates none, and its children are laid out
|
|
336
|
+
* by the next box up — which is the frame whose movement they follow. A
|
|
337
|
+
* tree with no boxes anywhere (no layout engine at all, a test
|
|
338
|
+
* environment) walks out of ancestors and keeps the parent it started
|
|
339
|
+
* with. */
|
|
340
|
+
function layoutFrameOf(item: HTMLElement): HTMLElement | null {
|
|
341
|
+
for (let el = item.parentElement; el; el = el.parentElement) {
|
|
342
|
+
const rect = el.getBoundingClientRect();
|
|
343
|
+
if (rect.width > 0 || rect.height > 0 || el.offsetWidth > 0 || el.offsetHeight > 0) return el;
|
|
344
|
+
}
|
|
345
|
+
return item.parentElement;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Read the frame as it is right now (see `FrameBox`). */
|
|
349
|
+
function frameBox(frame: HTMLElement): FrameBox {
|
|
307
350
|
const rect = frame.getBoundingClientRect();
|
|
308
351
|
return {
|
|
309
|
-
x: rect.left
|
|
310
|
-
y: rect.top
|
|
352
|
+
x: rect.left,
|
|
353
|
+
y: rect.top,
|
|
311
354
|
w: rect.width,
|
|
312
355
|
h: rect.height,
|
|
356
|
+
laidW: frame.offsetWidth,
|
|
357
|
+
laidH: frame.offsetHeight,
|
|
358
|
+
scrollX: frame.scrollLeft,
|
|
359
|
+
scrollY: frame.scrollTop,
|
|
313
360
|
};
|
|
314
361
|
}
|
|
315
362
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
* box AS IT IS NOW. The snapshot is in VIEWPORT coordinates, so anything
|
|
363
|
+
/** The pressed item's layout geometry for `indexAt`, placed where the
|
|
364
|
+
* frame puts it NOW. The snapshot is in VIEWPORT coordinates, so anything
|
|
319
365
|
* that moves the frame under it — the auto-scroll this composable drives,
|
|
320
366
|
* a host scrolling the surface or any scroller above it, a page scroll, a
|
|
321
|
-
* re-parent, a transform on an ancestor — has to be
|
|
322
|
-
* the line the item was laid out on would be compared
|
|
323
|
-
* that have since moved away from it. `null` before any
|
|
324
|
-
* press whose item was not in the strip.
|
|
367
|
+
* re-parent, a transform on the frame or on an ancestor — has to be
|
|
368
|
+
* carried with it, or the line the item was laid out on would be compared
|
|
369
|
+
* against siblings that have since moved away from it. `null` before any
|
|
370
|
+
* press, and for a press whose item was not in the strip.
|
|
371
|
+
*
|
|
372
|
+
* The carrier is the frame's own reading of a coordinate: the snapshot
|
|
373
|
+
* travels to wherever the frame puts that same place in its layout now,
|
|
374
|
+
* which is exact for the three things that can move it —
|
|
325
375
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
376
|
+
* - a DISPLACEMENT of the frame (a page scroll, a re-parent, a
|
|
377
|
+
* translation) moves the snapshot by the same amount;
|
|
378
|
+
* - the frame's own SCROLL slides its children inside a box that does not
|
|
379
|
+
* move, so it is taken off inside the frame's own units rather than in
|
|
380
|
+
* drawn pixels (`sample`);
|
|
381
|
+
* - a `scale()` or a zoom — on the frame or on an ancestor — keeps the
|
|
382
|
+
* snapshot at the same place INSIDE the frame, which is what the frame
|
|
383
|
+
* does to everything it holds, so the placement scales with it.
|
|
384
|
+
*
|
|
385
|
+
* A frame with no box to read any of this from falls back to the plain
|
|
333
386
|
* displacement. */
|
|
334
387
|
function layoutOrigin(): PointerReorderOrigin | null {
|
|
335
388
|
if (!pressedBand || !pressedFrameBox) return null;
|
|
336
389
|
const at = pressedFrameBox;
|
|
337
390
|
const now = pressedFrame && pressedFrame.isConnected ? frameBox(pressedFrame) : at;
|
|
391
|
+
/** Where the frame draws a coordinate it drew at `from` when the press
|
|
392
|
+
* was read: how far it sat from the frame's content origin, taken
|
|
393
|
+
* against the scroll the frame has moved since, drawn again at the
|
|
394
|
+
* scale the frame carries now. A frame whose LAID-OUT size cannot be
|
|
395
|
+
* read (no layout engine, `display: contents`) answers with the ratio
|
|
396
|
+
* of its drawn sizes, and takes its own scroll off raw — the reading
|
|
397
|
+
* every frame got before the scale was measurable at all. A frame with
|
|
398
|
+
* no box and no scroll of its own — one that cannot be read at all —
|
|
399
|
+
* carries nothing, so the snapshot stays where the press put it. */
|
|
338
400
|
const carry = (value: number, along: "x" | "y"): number => {
|
|
339
401
|
const from = along === "x" ? at.x : at.y;
|
|
340
|
-
const fromSpan = along === "x" ? at.w : at.h;
|
|
341
402
|
const to = along === "x" ? now.x : now.y;
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
403
|
+
const drawnFrom = along === "x" ? at.w : at.h;
|
|
404
|
+
const drawnTo = along === "x" ? now.w : now.h;
|
|
405
|
+
const laidFrom = along === "x" ? at.laidW : at.laidH;
|
|
406
|
+
const laidTo = along === "x" ? now.laidW : now.laidH;
|
|
407
|
+
const scrollFrom = along === "x" ? at.scrollX : at.scrollY;
|
|
408
|
+
const scrollTo = along === "x" ? now.scrollX : now.scrollY;
|
|
409
|
+
if (laidFrom > 0 && laidTo > 0) {
|
|
410
|
+
const scaleFrom = drawnFrom / laidFrom;
|
|
411
|
+
const scaleTo = drawnTo / laidTo;
|
|
412
|
+
return to + (scaleTo / scaleFrom) * (value - from) + scaleTo * (scrollFrom - scrollTo);
|
|
413
|
+
}
|
|
414
|
+
const ratio = drawnFrom > 0 && drawnTo > 0 ? drawnTo / drawnFrom : 1;
|
|
415
|
+
return to + ratio * (value - from) + (scrollFrom - scrollTo);
|
|
346
416
|
};
|
|
347
417
|
const down = axis === "x" ? "y" : "x";
|
|
348
418
|
const lo = carry(pressedBand.lo, down);
|
|
@@ -403,7 +473,7 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
403
473
|
} else {
|
|
404
474
|
container.scrollTop = next;
|
|
405
475
|
}
|
|
406
|
-
const over = slotAt(lastMain, lastCross
|
|
476
|
+
const over = slotAt(lastMain, lastCross);
|
|
407
477
|
if (over >= 0) dragOver.value = over;
|
|
408
478
|
scrollFrameId = requestAnimationFrame(scrollTick);
|
|
409
479
|
}
|
|
@@ -527,16 +597,18 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
527
597
|
const at = pointerAt(event);
|
|
528
598
|
lastMain = at.main;
|
|
529
599
|
lastCross = at.cross;
|
|
530
|
-
const over = slotAt(lastMain, lastCross
|
|
600
|
+
const over = slotAt(lastMain, lastCross);
|
|
531
601
|
if (over >= 0) dragOver.value = over;
|
|
532
602
|
syncAutoScroll();
|
|
533
603
|
}
|
|
534
604
|
|
|
535
605
|
function onPointerUp(event: PointerEvent): void {
|
|
536
606
|
if (!pressed.value || event.pointerId !== pointerId) return;
|
|
537
|
-
const from = pressedIndex;
|
|
538
607
|
const at = pointerAt(event);
|
|
539
|
-
|
|
608
|
+
// The slot is resolved first: it re-reads the pressed ELEMENT's index
|
|
609
|
+
// from the live strip, and that is the index the drop is reported with.
|
|
610
|
+
const to = moved ? slotAt(at.main, at.cross) : pressedIndex;
|
|
611
|
+
const from = pressedIndex;
|
|
540
612
|
const dropped = moved && to >= 0 && to !== from;
|
|
541
613
|
// The drag is over either way — the trap only guards the click the
|
|
542
614
|
// browser is about to deliver.
|
|
@@ -545,11 +617,12 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
545
617
|
if (dropped) options.onDrop(from, to);
|
|
546
618
|
}
|
|
547
619
|
|
|
548
|
-
/** End a drag that must not resolve — the strip
|
|
549
|
-
*
|
|
550
|
-
* exactly as a drop's is: this WAS a drag
|
|
551
|
-
* threshold), and the click the browser is
|
|
552
|
-
* the finger came to rest is one the user
|
|
620
|
+
/** End a drag that must not resolve — the strip no longer carries the
|
|
621
|
+
* element the press grabbed, so there is nothing for the gesture to move.
|
|
622
|
+
* The trailing click is swallowed exactly as a drop's is: this WAS a drag
|
|
623
|
+
* (the pointer travelled past the threshold), and the click the browser is
|
|
624
|
+
* about to deliver at wherever the finger came to rest is one the user
|
|
625
|
+
* never made. */
|
|
553
626
|
function abandonDrag(): void {
|
|
554
627
|
if (moved) trapClick();
|
|
555
628
|
releaseDrag();
|
|
@@ -594,7 +667,7 @@ export function usePointerReorder(options: PointerReorderOptions): PointerReorde
|
|
|
594
667
|
? rect.left + rect.width / 2
|
|
595
668
|
: rect.top + rect.height / 2
|
|
596
669
|
: 0;
|
|
597
|
-
pressedFrame = item
|
|
670
|
+
pressedFrame = item ? layoutFrameOf(item) : null;
|
|
598
671
|
pressedFrameBox = pressedFrame ? frameBox(pressedFrame) : null;
|
|
599
672
|
pressedItem = item ?? null;
|
|
600
673
|
originX = event.clientX;
|