@suphark/ui 0.38.0 → 0.40.1
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/CHANGELOG.md +51 -1
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/components/design-system/sections/showcase-forms.tsx +1 -1
- package/src/components/shared/date/date-picker.tsx +377 -0
- package/src/components/shared/form/form-field-date.tsx +1 -1
- package/src/components/shared/ui-text.tsx +43 -0
- package/src/components/sortable/sortable-list.tsx +254 -71
- package/src/components/ui/date-picker.tsx +8 -306
- package/src/index.ts +6 -1
- package/src/lib/sortable-order.ts +170 -33
- package/src/sortable.ts +4 -0
|
@@ -38,8 +38,10 @@ import {
|
|
|
38
38
|
gatherItems,
|
|
39
39
|
groupOfItem,
|
|
40
40
|
moveItemToGroup,
|
|
41
|
-
|
|
41
|
+
moveItemToRoot,
|
|
42
|
+
moveRootEntry,
|
|
42
43
|
reorderItems,
|
|
44
|
+
rootEntries,
|
|
43
45
|
type SortableOrder,
|
|
44
46
|
sameOrder,
|
|
45
47
|
UNGROUPED,
|
|
@@ -110,6 +112,11 @@ function createShiftStore() {
|
|
|
110
112
|
else shifts.delete(id);
|
|
111
113
|
for (const listener of listeners) listener();
|
|
112
114
|
},
|
|
115
|
+
clear() {
|
|
116
|
+
if (shifts.size === 0) return;
|
|
117
|
+
shifts.clear();
|
|
118
|
+
for (const listener of listeners) listener();
|
|
119
|
+
},
|
|
113
120
|
subscribe(listener: () => void) {
|
|
114
121
|
listeners.add(listener);
|
|
115
122
|
return () => {
|
|
@@ -129,6 +136,9 @@ interface SortableState {
|
|
|
129
136
|
overlay: boolean;
|
|
130
137
|
/** กลุ่มที่กำลังถูกลาก (แถวรายการของกลุ่มนี้จางตามหัวกลุ่ม) */
|
|
131
138
|
activeGroup: string | null;
|
|
139
|
+
/** กำลังลากของในชั้นบนสุด (กลุ่ม หรือรายการไม่มีกลุ่มเมื่อ `freeLayout`) — ระยะเลื่อนมาจาก provider */
|
|
140
|
+
rootDrag: boolean;
|
|
141
|
+
freeLayout: boolean;
|
|
132
142
|
/** รายการอื่นที่ลากไปพร้อมกันแบบ bulk (ไม่รวมตัวที่จับ) — แสดงจาง ๆ ระหว่างลาก */
|
|
133
143
|
carried: ReadonlySet<string>;
|
|
134
144
|
multiSelect: boolean;
|
|
@@ -146,6 +156,8 @@ const SortableStateContext = createContext<SortableState>({
|
|
|
146
156
|
handleLabel: "ลากเพื่อจัดลำดับ",
|
|
147
157
|
overlay: false,
|
|
148
158
|
activeGroup: null,
|
|
159
|
+
rootDrag: false,
|
|
160
|
+
freeLayout: false,
|
|
149
161
|
carried: EMPTY,
|
|
150
162
|
multiSelect: false,
|
|
151
163
|
selected: EMPTY,
|
|
@@ -240,6 +252,13 @@ export interface SortableProviderProps {
|
|
|
240
252
|
* ลากตัวที่เลือกอยู่ = ลากทุกตัวที่เลือกไปรวมที่จุดวาง เรียงตามลำดับเดิม
|
|
241
253
|
*/
|
|
242
254
|
multiSelect?: boolean;
|
|
255
|
+
/**
|
|
256
|
+
* ชั้นบนสุดจัดได้อิสระ: กลุ่มกับรายการที่ไม่มีกลุ่มสลับที่กันได้ทุกแบบ (กลุ่มขึ้นไปเหนือรายการ /
|
|
257
|
+
* รายการไปอยู่ระหว่างกลุ่ม / เอารายการออกจากกลุ่มไปวางก่อนกลุ่มไหนก็ได้) — แอปต้องเรนเดอร์ตาม
|
|
258
|
+
* `rootEntries(order)` และบันทึก `sortableOrderToPayload(order).root` · ชี้ครึ่งบนของหัวกลุ่ม = วางก่อนกลุ่ม,
|
|
259
|
+
* ครึ่งล่าง = เข้ากลุ่มเป็นตัวแรก · ไม่เปิด = รายการไม่มีกลุ่มอยู่บนสุดเสมอ (แบบเดิม)
|
|
260
|
+
*/
|
|
261
|
+
freeLayout?: boolean;
|
|
243
262
|
/** รายการที่เลือก (ควบคุมจากภายนอก) — ไม่ส่ง = provider เก็บเอง */
|
|
244
263
|
selected?: string[];
|
|
245
264
|
onSelectedChange?: (ids: string[]) => void;
|
|
@@ -267,6 +286,7 @@ export function SortableProvider({
|
|
|
267
286
|
id,
|
|
268
287
|
renderOverlay,
|
|
269
288
|
multiSelect = false,
|
|
289
|
+
freeLayout = false,
|
|
270
290
|
selected: selectedProp,
|
|
271
291
|
onSelectedChange,
|
|
272
292
|
children,
|
|
@@ -395,40 +415,122 @@ export function SortableProvider({
|
|
|
395
415
|
[blockRect],
|
|
396
416
|
);
|
|
397
417
|
|
|
398
|
-
// ----------
|
|
399
|
-
const
|
|
400
|
-
|
|
418
|
+
// ---------- เลื่อนทั้งก้อนในชั้นบนสุด (แบบรายการ: ตัวอื่นหลบ ช่องว่างของตัวที่ลากเลื่อนไปจุดวาง) ----------
|
|
419
|
+
const freeRef = useRef(freeLayout);
|
|
420
|
+
freeRef.current = freeLayout;
|
|
401
421
|
const overlayOn = Boolean(renderOverlay);
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* ลำดับที่ตัวที่ลากเรียงอยู่ด้วย (เป็น dnd id) — `freeLayout` = ชั้นบนสุดทั้งหมด,
|
|
425
|
+
* ไม่เปิด = เฉพาะกลุ่ม (รายการไม่มีกลุ่มเรียงกันเองด้วย dnd-kit) · null = ไม่ใช่การลากในชั้นบนสุด
|
|
426
|
+
*/
|
|
427
|
+
const entryKeys = useCallback((current: SortableOrder, activeKey: string): string[] | null => {
|
|
428
|
+
if (freeRef.current) {
|
|
429
|
+
const keys = rootEntries(current).map((e) => dndId(e.kind, e.id));
|
|
430
|
+
return keys.includes(activeKey) ? keys : null;
|
|
431
|
+
}
|
|
432
|
+
return kindOf(activeKey) === "g" ? current.groups.map((g) => dndId("group", g)) : null;
|
|
433
|
+
}, []);
|
|
434
|
+
|
|
435
|
+
/** กรอบของสิ่งหนึ่งในชั้นบนสุด: กลุ่ม = ทั้งก้อน, รายการ = แถวเดียว */
|
|
436
|
+
const measureKey = useCallback(
|
|
437
|
+
(key: string) => {
|
|
438
|
+
const raw = rawOf(key);
|
|
439
|
+
if (kindOf(key) === "g") {
|
|
440
|
+
const header = nodes.current.groups.get(raw);
|
|
441
|
+
return header?.isConnected ? blockRect(raw, header) : null;
|
|
442
|
+
}
|
|
443
|
+
const node = nodes.current.items.get(raw);
|
|
444
|
+
return node?.isConnected ? getClientRect(node, { ignoreTransform: true }) : null;
|
|
445
|
+
},
|
|
446
|
+
[blockRect],
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
/** จะวาง "ก่อน" อะไร (dnd id; null = ท้ายสุดของลำดับ) — null ทั้ง object = ไม่ใช่เป้าหมายในชั้นบนสุด */
|
|
450
|
+
const [target, setTargetState] = useState<{ before: string | null } | null>(null);
|
|
451
|
+
const targetRef = useRef(target);
|
|
452
|
+
const setTarget = useCallback((next: { before: string | null } | null) => {
|
|
453
|
+
const prev = targetRef.current;
|
|
454
|
+
if (prev === next || (prev && next && prev.before === next.before)) return;
|
|
455
|
+
targetRef.current = next;
|
|
456
|
+
setTargetState(next);
|
|
457
|
+
}, []);
|
|
458
|
+
|
|
402
459
|
useEffect(() => {
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
460
|
+
if (!activeDrag) return;
|
|
461
|
+
const activeKey = dndId(activeDrag.kind, activeDrag.id);
|
|
462
|
+
const keys = entryKeys(order, activeKey);
|
|
463
|
+
if (!keys) {
|
|
464
|
+
// รายการที่ลากเข้าไปอยู่ในกลุ่มแล้ว → dnd-kit ของกลุ่มนั้นคุมต่อ
|
|
465
|
+
shifts.clear();
|
|
407
466
|
return;
|
|
408
467
|
}
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
468
|
+
const rest = keys.filter((k) => k !== activeKey);
|
|
469
|
+
const at = target
|
|
470
|
+
? target.before === null
|
|
471
|
+
? rest.length
|
|
472
|
+
: rest.indexOf(target.before)
|
|
473
|
+
: keys.indexOf(activeKey);
|
|
474
|
+
const final = [...rest.slice(0, Math.max(0, at)), activeKey, ...rest.slice(Math.max(0, at))];
|
|
475
|
+
const rects = new Map(keys.map((k) => [k, measureKey(k)] as const));
|
|
476
|
+
const first = rects.get(keys[0] ?? "");
|
|
477
|
+
const second = rects.get(keys[1] ?? "");
|
|
478
|
+
if (!first) return;
|
|
479
|
+
// ช่องไฟระหว่างก้อน (ตาราง = 0, การ์ดมีระยะห่าง) — ใช้ระยะจริงระหว่างสองก้อนแรก
|
|
480
|
+
const gap = second ? Math.max(0, second.top - first.bottom) : 0;
|
|
416
481
|
const transition = `transform ${SORT_TRANSITION.duration}ms ${EASING}`;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
482
|
+
let cursor = first.top;
|
|
483
|
+
for (const key of final) {
|
|
484
|
+
const rect = rects.get(key);
|
|
485
|
+
if (!rect) continue;
|
|
486
|
+
const dy = cursor - rect.top;
|
|
487
|
+
cursor += rect.height + gap;
|
|
488
|
+
// ไม่มี overlay = ตัวที่ลากลอยตามเมาส์เอง (กลุ่ม: onDragMove, รายการ: dnd-kit)
|
|
489
|
+
if (key === activeKey && !overlayOn) continue;
|
|
490
|
+
shifts.set(key, { transform: `translate3d(0px, ${dy}px, 0)`, transition });
|
|
491
|
+
}
|
|
492
|
+
}, [activeDrag, target, order, overlayOn, shifts, entryKeys, measureKey]);
|
|
493
|
+
|
|
494
|
+
/** ตั้งเป้าหมายในชั้นบนสุดจากสิ่งที่ชี้อยู่ + ตัวที่ลากเลยครึ่งของมันหรือยัง */
|
|
495
|
+
function updateTarget(
|
|
496
|
+
active: DragMoveEvent["active"],
|
|
497
|
+
over: DragMoveEvent["over"],
|
|
498
|
+
keyboard: boolean,
|
|
499
|
+
) {
|
|
500
|
+
const activeKey = String(active.id);
|
|
501
|
+
const keys = entryKeys(latest.current, activeKey);
|
|
502
|
+
if (!keys) {
|
|
503
|
+
setTarget(null);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (!over) return;
|
|
507
|
+
const overKey = String(over.id);
|
|
508
|
+
const nextAfter = (index: number) => keys.slice(index + 1).find((k) => k !== activeKey) ?? null;
|
|
509
|
+
if (kindOf(overKey) === "z" && rawOf(overKey) === UNGROUPED) {
|
|
510
|
+
setTarget({ before: null });
|
|
511
|
+
} else if (overKey === activeKey) {
|
|
512
|
+
setTarget({ before: nextAfter(keys.indexOf(activeKey)) });
|
|
513
|
+
} else if (keys.includes(overKey)) {
|
|
514
|
+
if (kindOf(activeKey) === "i" && kindOf(overKey) === "g") {
|
|
515
|
+
// รายการชี้ครึ่งบนของหัวกลุ่ม = วางก่อนกลุ่มนั้น
|
|
516
|
+
setTarget({ before: overKey });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const a = keys.indexOf(activeKey);
|
|
520
|
+
const o = keys.indexOf(overKey);
|
|
521
|
+
const rect = measureKey(overKey);
|
|
522
|
+
const dragged = active.rect.current.translated;
|
|
523
|
+
// คีย์บอร์ดขยับทีละตำแหน่งไปทับกลางเป้าหมายพอดี → ใช้ทิศทางแทน
|
|
524
|
+
const after =
|
|
525
|
+
keyboard || !rect || !dragged
|
|
526
|
+
? o > a
|
|
527
|
+
: dragged.top + dragged.height / 2 > (rect.top + rect.bottom) / 2;
|
|
528
|
+
setTarget({ before: after ? nextAfter(o) : overKey });
|
|
529
|
+
} else {
|
|
530
|
+
setTarget(null);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
432
534
|
const measuring = useMemo(
|
|
433
535
|
() => ({ droppable: { strategy: MeasuringStrategy.Always, measure: measureDroppable } }),
|
|
434
536
|
[measureDroppable],
|
|
@@ -452,10 +554,16 @@ export function SortableProvider({
|
|
|
452
554
|
*/
|
|
453
555
|
const collisionDetection: CollisionDetection = useCallback((args) => {
|
|
454
556
|
if (kindOf(args.active.id) === "g") {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
557
|
+
// freeLayout: กลุ่มเรียงปนกับรายการไม่มีกลุ่ม → เทียบกับทั้งสองอย่าง (สิ่งที่อยู่ใต้เมาส์ก่อน)
|
|
558
|
+
const loose = new Set(latest.current.items[UNGROUPED] ?? []);
|
|
559
|
+
const rootLevel = args.droppableContainers.filter(
|
|
560
|
+
(c) =>
|
|
561
|
+
kindOf(c.id) === "g" ||
|
|
562
|
+
(freeRef.current && kindOf(c.id) === "i" && loose.has(rawOf(c.id))),
|
|
563
|
+
);
|
|
564
|
+
const scoped = { ...args, droppableContainers: rootLevel };
|
|
565
|
+
const under = freeRef.current ? pointerWithin(scoped) : [];
|
|
566
|
+
return under.length > 0 ? under.slice(0, 1) : closestCenter(scoped);
|
|
459
567
|
}
|
|
460
568
|
const candidates = args.droppableContainers.filter((c) => kindOf(c.id) !== "g");
|
|
461
569
|
const scoped = { ...args, droppableContainers: candidates };
|
|
@@ -463,14 +571,27 @@ export function SortableProvider({
|
|
|
463
571
|
const hits = underPointer.length > 0 ? underPointer : rectIntersection(scoped);
|
|
464
572
|
let overId = getFirstCollision(hits, "id");
|
|
465
573
|
if (overId != null) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
574
|
+
const header = kindOf(overId) === "z" ? args.droppableRects.get(overId) : undefined;
|
|
575
|
+
const pointer = args.pointerCoordinates;
|
|
576
|
+
if (
|
|
577
|
+
freeRef.current &&
|
|
578
|
+
header &&
|
|
579
|
+
pointer &&
|
|
580
|
+
rawOf(overId) !== UNGROUPED &&
|
|
581
|
+
pointer.y < header.top + header.height / 2
|
|
582
|
+
) {
|
|
583
|
+
// freeLayout: ครึ่งบนของหัวกลุ่ม = วาง "ก่อนกลุ่ม" ในชั้นบนสุด (ครึ่งล่าง = เข้ากลุ่มเป็นตัวแรก)
|
|
584
|
+
overId = dndId("group", rawOf(overId));
|
|
585
|
+
} else if (kindOf(overId) === "z" && rawOf(overId) !== UNGROUPED) {
|
|
586
|
+
// (โซนของ UNGROUPED = ที่วาง "ท้ายสุด" ไม่ใช่หัวกลุ่ม — ห้ามเปลี่ยนเป็นรายการแรก)
|
|
587
|
+
// ชี้หัวกลุ่ม = "บนสุดของกลุ่ม" → เป้าหมายคือรายการแรกที่แสดงอยู่ "รวมตัวที่ลากเอง"
|
|
588
|
+
// (เดิมเลือกตัวที่ใกล้ที่สุดที่ไม่ใช่ตัวที่ลาก: ลากเข้ากลุ่มแล้วขึ้นเป็นตัวแรกถูก แต่พอปล่อย
|
|
589
|
+
// onDragEnd ย้ายไปแทนที่ตัวแรกเดิม → กลายเป็นตัวที่สอง วางเป็นรายการแรกไม่ได้)
|
|
590
|
+
const rendered = new Set(
|
|
591
|
+
candidates.filter((c) => kindOf(c.id) === "i").map((c) => rawOf(c.id)),
|
|
470
592
|
);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
593
|
+
const first = (latest.current.items[rawOf(overId)] ?? []).find((x) => rendered.has(x));
|
|
594
|
+
if (first !== undefined) overId = dndId("item", first);
|
|
474
595
|
}
|
|
475
596
|
lastOverId.current = overId;
|
|
476
597
|
return [{ id: overId }];
|
|
@@ -503,21 +624,16 @@ export function SortableProvider({
|
|
|
503
624
|
select("", "clear");
|
|
504
625
|
}
|
|
505
626
|
draggingRef.current = kind;
|
|
506
|
-
|
|
507
|
-
blocks.current.clear();
|
|
508
|
-
for (const [groupId, header] of nodes.current.groups) {
|
|
509
|
-
if (header.isConnected) blocks.current.set(groupId, blockRect(groupId, header));
|
|
510
|
-
}
|
|
511
|
-
setOverGroup(raw);
|
|
512
|
-
}
|
|
627
|
+
setTarget(null);
|
|
513
628
|
setDragging(kind);
|
|
514
629
|
setActiveDrag({ kind, id: raw, ids: bulk.current ?? [raw] });
|
|
515
630
|
}
|
|
516
631
|
|
|
517
|
-
|
|
518
|
-
|
|
632
|
+
function onDragMove({ active, over, delta, activatorEvent }: DragMoveEvent) {
|
|
633
|
+
updateTarget(active, over, activatorEvent instanceof KeyboardEvent);
|
|
634
|
+
// ไม่มี overlay: ก้อนของกลุ่มที่ลากลอยตามเมาส์ทั้งก้อน (หัวกลุ่ม + แถวรายการ)
|
|
519
635
|
if (overlayOn || kindOf(active.id) !== "g") return;
|
|
520
|
-
shifts.set(
|
|
636
|
+
shifts.set(String(active.id), {
|
|
521
637
|
transform: `translate3d(${delta.x}px, ${delta.y}px, 0)`,
|
|
522
638
|
transition: undefined,
|
|
523
639
|
});
|
|
@@ -526,23 +642,54 @@ export function SortableProvider({
|
|
|
526
642
|
function endDrag() {
|
|
527
643
|
draggingRef.current = null;
|
|
528
644
|
bulk.current = null;
|
|
529
|
-
|
|
645
|
+
setTarget(null);
|
|
530
646
|
// ล้างระยะเลื่อนพร้อมกับที่ลำดับใหม่ render (รอ effect = มีเฟรมที่แถวใหม่ยังติด transform เก่า → กระพริบ)
|
|
531
|
-
|
|
647
|
+
shifts.clear();
|
|
532
648
|
setDragging(null);
|
|
533
649
|
setActiveDrag(null);
|
|
534
650
|
}
|
|
535
651
|
|
|
536
|
-
function onDragOver({ active, over }: DragOverEvent) {
|
|
537
|
-
|
|
538
|
-
|
|
652
|
+
function onDragOver({ active, over, activatorEvent }: DragOverEvent) {
|
|
653
|
+
const keyboard = activatorEvent instanceof KeyboardEvent;
|
|
654
|
+
if (!over || kindOf(active.id) !== "i") {
|
|
655
|
+
updateTarget(active, over, keyboard);
|
|
539
656
|
return;
|
|
540
657
|
}
|
|
541
|
-
if (!over || kindOf(active.id) !== "i") return;
|
|
542
658
|
const current = latest.current;
|
|
659
|
+
const raw = rawOf(active.id);
|
|
660
|
+
const apply = (next: SortableOrder) => {
|
|
661
|
+
if (next === current) return;
|
|
662
|
+
movedAcross.current = true;
|
|
663
|
+
latest.current = next;
|
|
664
|
+
onOrderChange(next);
|
|
665
|
+
};
|
|
666
|
+
// ครึ่งบนของหัวกลุ่ม (freeLayout) = ออกมาวางก่อนกลุ่มนั้นในชั้นบนสุด
|
|
667
|
+
if (kindOf(over.id) === "g") {
|
|
668
|
+
if (groupOfItem(current, raw) !== UNGROUPED) {
|
|
669
|
+
apply(moveItemToRoot(current, raw, { kind: "group", id: rawOf(over.id) }));
|
|
670
|
+
}
|
|
671
|
+
updateTarget(active, over, keyboard);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
543
674
|
const from = containerOf(active.id, current);
|
|
544
675
|
const to = containerOf(over.id, current);
|
|
545
|
-
if (!from || !to || from === to)
|
|
676
|
+
if (!from || !to || from === to) {
|
|
677
|
+
updateTarget(active, over, keyboard);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (to === UNGROUPED) {
|
|
681
|
+
// ออกจากกลุ่มมาชั้นบนสุด: ก่อน/หลังรายการที่ชี้ตามครึ่งแถว (ถัดไปอาจเป็นกลุ่ม) หรือท้ายสุด
|
|
682
|
+
const root = rootEntries(current);
|
|
683
|
+
let before: (typeof root)[number] | null = null;
|
|
684
|
+
if (kindOf(over.id) === "i") {
|
|
685
|
+
const at = root.findIndex((e) => e.kind === "item" && e.id === rawOf(over.id));
|
|
686
|
+
const dragged = active.rect.current.translated;
|
|
687
|
+
const below = dragged != null && dragged.top > over.rect.top + over.rect.height / 2;
|
|
688
|
+
before = root[at + (below ? 1 : 0)] ?? null;
|
|
689
|
+
}
|
|
690
|
+
apply(moveItemToRoot(current, raw, before));
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
546
693
|
// วางก่อนหรือหลังแถวเป้าหมาย ตามว่าตัวที่ลากเลยครึ่งแถวนั้นลงมาหรือยัง
|
|
547
694
|
// (เดิมแทรก "ก่อน" เสมอ — ลากเข้าจากด้านล่างแล้วรายการกระโดดข้ามขึ้นไป)
|
|
548
695
|
let before: string | null = null;
|
|
@@ -552,17 +699,12 @@ export function SortableProvider({
|
|
|
552
699
|
const dragged = active.rect.current.translated;
|
|
553
700
|
const below = dragged != null && dragged.top > over.rect.top + over.rect.height / 2;
|
|
554
701
|
before = list[at + (below ? 1 : 0)] ?? null;
|
|
555
|
-
} else
|
|
702
|
+
} else {
|
|
556
703
|
// วางบนหัวโฟลเดอร์ = ขึ้นบนสุดของโฟลเดอร์ (ตรงกับตำแหน่งที่เห็น และไม่กระโดดตอนโฟลเดอร์ที่ย่อไว้เปิดออก)
|
|
557
704
|
// ส่วนโซน "ไม่มีกลุ่ม" อยู่ท้ายตาราง จึงต่อท้าย
|
|
558
705
|
before = current.items[to]?.[0] ?? null;
|
|
559
706
|
}
|
|
560
|
-
|
|
561
|
-
if (next !== current) {
|
|
562
|
-
movedAcross.current = true;
|
|
563
|
-
latest.current = next;
|
|
564
|
-
onOrderChange(next);
|
|
565
|
-
}
|
|
707
|
+
apply(moveItemToGroup(current, raw, to, before));
|
|
566
708
|
}
|
|
567
709
|
|
|
568
710
|
/** ล้มเหลว = คืนลำดับเดิมเสมอ แล้วส่ง error ต่อให้แอป (ไม่มี `onCommitError` = ขึ้น toast ให้) */
|
|
@@ -589,12 +731,33 @@ export function SortableProvider({
|
|
|
589
731
|
function onDragEnd({ active, over }: DragEndEvent) {
|
|
590
732
|
const snapshot = beforeDrag.current;
|
|
591
733
|
const carried = bulk.current;
|
|
734
|
+
// อ่านเป้าหมายก่อน endDrag (endDrag ล้างทิ้ง)
|
|
735
|
+
const finalTarget = targetRef.current;
|
|
592
736
|
beforeDrag.current = null;
|
|
593
737
|
endDrag();
|
|
594
738
|
const current = latest.current;
|
|
595
739
|
let next = current;
|
|
596
|
-
|
|
597
|
-
|
|
740
|
+
const activeKey = String(active.id);
|
|
741
|
+
const keys = entryKeys(current, activeKey);
|
|
742
|
+
if (keys && finalTarget) {
|
|
743
|
+
// ชั้นบนสุด: แปลง "ก่อน X / ท้ายลำดับ" เป็นตำแหน่งใน root (ลำดับที่ไม่รวมตัวที่ลาก)
|
|
744
|
+
const rootRest = rootEntries(current)
|
|
745
|
+
.map((e) => dndId(e.kind, e.id))
|
|
746
|
+
.filter((k) => k !== activeKey);
|
|
747
|
+
const last = keys.filter((k) => k !== activeKey).at(-1);
|
|
748
|
+
const index =
|
|
749
|
+
finalTarget.before !== null
|
|
750
|
+
? rootRest.indexOf(finalTarget.before)
|
|
751
|
+
: last
|
|
752
|
+
? rootRest.indexOf(last) + 1
|
|
753
|
+
: rootRest.length;
|
|
754
|
+
if (index >= 0) {
|
|
755
|
+
next = moveRootEntry(
|
|
756
|
+
current,
|
|
757
|
+
{ kind: kindOf(activeKey) === "g" ? "group" : "item", id: rawOf(activeKey) },
|
|
758
|
+
index,
|
|
759
|
+
);
|
|
760
|
+
}
|
|
598
761
|
} else if (over && kindOf(active.id) === "i" && kindOf(over.id) === "i") {
|
|
599
762
|
next = reorderItems(current, rawOf(active.id), rawOf(over.id));
|
|
600
763
|
}
|
|
@@ -742,6 +905,10 @@ export function SortableProvider({
|
|
|
742
905
|
const groupIds = useMemo(() => order.groups.map((id) => dndId("group", id)), [order.groups]);
|
|
743
906
|
|
|
744
907
|
const groupOf = useCallback((itemId: string) => groupOfItem(order, itemId), [order]);
|
|
908
|
+
const rootDrag =
|
|
909
|
+
activeDrag !== null &&
|
|
910
|
+
(activeDrag.kind === "group" ||
|
|
911
|
+
(freeLayout && groupOfItem(order, activeDrag.id) === UNGROUPED));
|
|
745
912
|
const carried = useMemo<ReadonlySet<string>>(
|
|
746
913
|
() =>
|
|
747
914
|
activeDrag && activeDrag.ids.length > 1
|
|
@@ -757,6 +924,8 @@ export function SortableProvider({
|
|
|
757
924
|
handleLabel: handleLabel ?? text.sortable?.handle ?? "ลากเพื่อจัดลำดับ",
|
|
758
925
|
overlay: Boolean(renderOverlay),
|
|
759
926
|
activeGroup: activeDrag?.kind === "group" ? activeDrag.id : null,
|
|
927
|
+
rootDrag,
|
|
928
|
+
freeLayout,
|
|
760
929
|
carried,
|
|
761
930
|
multiSelect,
|
|
762
931
|
selected,
|
|
@@ -772,6 +941,8 @@ export function SortableProvider({
|
|
|
772
941
|
text.sortable?.handle,
|
|
773
942
|
renderOverlay,
|
|
774
943
|
activeDrag,
|
|
944
|
+
rootDrag,
|
|
945
|
+
freeLayout,
|
|
775
946
|
carried,
|
|
776
947
|
multiSelect,
|
|
777
948
|
selected,
|
|
@@ -887,18 +1058,30 @@ export function useSortableRow({
|
|
|
887
1058
|
);
|
|
888
1059
|
|
|
889
1060
|
const transform = CSS.Translate.toString(sortable.transform);
|
|
890
|
-
const groupDrag = state.dragging === "group";
|
|
891
1061
|
// ระหว่างลากกลุ่ม provider คำนวณระยะเลื่อนของแต่ละกลุ่มเอง — หัวกลุ่มและแถวรายการของกลุ่มอ่านค่าเดียวกัน
|
|
892
1062
|
// (หัวกลุ่มมักอยู่ใน `SortableGroup` ซึ่งเป็น context ของรายการ dnd-kit จึงคำนวณให้หัวกลุ่มไม่ได้)
|
|
893
1063
|
const groupId = type === "item" ? state.groupOf(id) : id;
|
|
1064
|
+
const loose = type === "item" && groupId === UNGROUPED;
|
|
1065
|
+
// ระยะเลื่อนในชั้นบนสุด: กลุ่ม/รายการไม่มีกลุ่ม อ่านของตัวเอง, รายการในกลุ่มอ่านของกลุ่ม
|
|
1066
|
+
const shiftKey =
|
|
1067
|
+
type === "group" || loose ? dndId(type, id) : groupId ? dndId("group", groupId) : undefined;
|
|
894
1068
|
const shift = useSyncExternalStore(
|
|
895
1069
|
shifts.subscribe,
|
|
896
|
-
() => (
|
|
1070
|
+
() => (shiftKey ? shifts.get(shiftKey) : undefined),
|
|
897
1071
|
() => undefined,
|
|
898
1072
|
);
|
|
899
1073
|
|
|
900
1074
|
let style: CSSProperties = { transform, transition: sortable.transition };
|
|
901
|
-
if (
|
|
1075
|
+
if (loose && state.freeLayout && state.rootDrag) {
|
|
1076
|
+
style =
|
|
1077
|
+
sortable.isDragging && !state.overlay
|
|
1078
|
+
? { ...style, position: "relative", zIndex: 1, opacity: 0.85 }
|
|
1079
|
+
: {
|
|
1080
|
+
transform: shift?.transform,
|
|
1081
|
+
transition: shift?.transition,
|
|
1082
|
+
...(sortable.isDragging || state.carried.has(id) ? { opacity: 0.4 } : {}),
|
|
1083
|
+
};
|
|
1084
|
+
} else if (type === "group" && state.rootDrag) {
|
|
902
1085
|
const own = state.activeGroup === id;
|
|
903
1086
|
style = {
|
|
904
1087
|
transform: shift?.transform,
|
|
@@ -914,7 +1097,7 @@ export function useSortableRow({
|
|
|
914
1097
|
? // มี overlay ลอยตามเมาส์แล้ว แถวต้นทางเหลือเป็นช่องจาง ๆ บอกตำแหน่งที่จะวาง
|
|
915
1098
|
{ ...style, opacity: 0.4 }
|
|
916
1099
|
: { ...style, position: "relative", zIndex: 1, opacity: 0.85 };
|
|
917
|
-
} else if (type === "item" &&
|
|
1100
|
+
} else if (type === "item" && state.rootDrag && shift) {
|
|
918
1101
|
const own = state.activeGroup !== null && state.activeGroup === groupId;
|
|
919
1102
|
style = {
|
|
920
1103
|
transform: shift.transform,
|