vgapp 0.9.9 → 1.0.0

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.
@@ -0,0 +1,1686 @@
1
+ import BaseModule from "../../base-module";
2
+ import VGCollapse from "../../vgcollapse/js/vgcollapse";
3
+ import { mergeDeepObject, normalizeData } from "../../../utils/js/functions";
4
+ import EventHandler from "../../../utils/js/dom/event";
5
+ import Selectors from "../../../utils/js/dom/selectors";
6
+ import {getSVG} from "../../module-fn";
7
+ import Ajax from "../../../utils/js/components/ajax";
8
+ import Sanitize from "../../../utils/js/components/sanitize";
9
+
10
+ const NAME = "nestable";
11
+ const NAME_KEY = "vg.nestable";
12
+
13
+ const SELECTOR_DATA_TOGGLE = '[data-vg-toggle="nestable"]';
14
+
15
+ const CLASS_ITEM_DRAGGING = "is-dragging";
16
+ const CLASS_ITEM_GHOST = "is-drag-ghost";
17
+ const CLASS_PLACEHOLDER = "vg-nestable-placeholder";
18
+ const CLASS_PLACEHOLDER_HIDDEN = "vg-nestable-placeholder-hidden";
19
+ const CLASS_DRAG_ELEMENT = "vg-nestable-drag-element";
20
+ const CLASS_DRAG_LAYER = "vg-nestable-drag-layer";
21
+ const CLASS_INNER = "vg-nestable-inner";
22
+ const CLASS_HANDLE = "vg-nestable-handle";
23
+ const CLASS_HANDLE_ICON = "vg-nestable-handle-icon";
24
+ const CLASS_COLLAPSE_TOGGLE = "vg-nestable-collapse-toggle";
25
+ const CLASS_DROP_TARGET = "is-drop-target";
26
+ const CLASS_DROP_DENIED = "is-drop-denied";
27
+ const CLASS_LIVE_REGION = "vg-nestable-live";
28
+
29
+ const EVENT_KEY_START = `${NAME_KEY}.start`;
30
+ const EVENT_KEY_CHANGE = `${NAME_KEY}.change`;
31
+ const EVENT_KEY_END = `${NAME_KEY}.end`;
32
+ const EVENT_KEY_SAVE = `${NAME_KEY}.save`;
33
+ const EVENT_KEY_INIT = `${NAME_KEY}.init`;
34
+ const EVENT_KEY_REFRESH = `${NAME_KEY}.refresh`;
35
+ const EVENT_KEY_POINTER_DOWN = `${NAME_KEY}.pointerdown`;
36
+ const EVENT_KEY_MOVE = `${NAME_KEY}.move`;
37
+ const EVENT_KEY_DROP = `${NAME_KEY}.drop`;
38
+ const EVENT_KEY_TRANSFER = `${NAME_KEY}.transfer`;
39
+ const EVENT_KEY_PLACEHOLDER_MOVE = `${NAME_KEY}.placeholdermove`;
40
+ const EVENT_KEY_DESTROY = `${NAME_KEY}.destroy`;
41
+
42
+ class VGNestable extends BaseModule {
43
+ static _groups = new Map();
44
+
45
+ constructor(element, params = {}) {
46
+ super(element, params);
47
+
48
+ this._params = this._getParams(element, mergeDeepObject({
49
+ listselector: ".vg-nestable-list", // Root/nested list for dnd.
50
+ itemselector: ".vg-nestable-item", // Sortable item selector.
51
+ handleselector: ".vg-nestable-handle", // Drag handle selector.
52
+ idattribute: "data-id", // Item id attribute used by serialize().
53
+ childlistclass: "vg-nestable-list", // Class for auto-created child list.
54
+ handleicon: '', // иконка для хендлера
55
+ indent: 50, // X offset (px), after which item moves into child level.
56
+ maxdepth: 6, // Maximum nesting depth.
57
+ moveaxis: "default", // Drag direction: default | vertical | horizontal.
58
+ hoverthreshold: 0, // Vertical threshold (0..0.5) around hovered item center.
59
+ neighborchangethreshold: 0, // Percentage (0..49) of entering adjacent item before position changes.
60
+ showplaceholder: true, // Show placeholder during drag.
61
+ group: "",
62
+ connect: false,
63
+ accept: null,
64
+ collapse: {
65
+ enabled: true,
66
+ open: true,
67
+ showtext: getSVG("plus"),
68
+ hidetext: getSVG("minus")
69
+ },
70
+ callbacks: {
71
+ init: null,
72
+ refresh: null,
73
+ pointerdown: null,
74
+ start: null,
75
+ move: null,
76
+ placeholdermove: null,
77
+ drop: null,
78
+ transfer: null,
79
+ change: null,
80
+ end: null,
81
+ save: null,
82
+ destroy: null
83
+ },
84
+ ajax: {
85
+ route: "",
86
+ method: "post",
87
+ field: "items",
88
+ data: {},
89
+ loader: false,
90
+ once: false,
91
+ output: false,
92
+ timeout: 0тзь
93
+ }
94
+ }, params));
95
+
96
+ this._rootList = this._resolveRootList();
97
+ this._draggedItem = null;
98
+ this._placeholder = null;
99
+ this._dragElement = null;
100
+ this._dragLayer = null;
101
+ this._isDragging = false;
102
+ this._startSnapshot = "";
103
+ this._sourceInstance = this;
104
+ this._sourceRoot = null;
105
+ this._currentTargetInstance = this;
106
+ this._lastDropInstance = this;
107
+ this._activeDropList = null;
108
+
109
+ this._mouse = {
110
+ startX: 0,
111
+ startY: 0,
112
+ startItemX: 0,
113
+ grabOffsetX: 0,
114
+ grabOffsetY: 0,
115
+ x: 0,
116
+ y: 0
117
+ };
118
+
119
+ this._previousBodyCursor = "";
120
+
121
+ this._boundOnMouseDown = this._onMouseDown.bind(this);
122
+ this._boundOnMouseMove = this._onMouseMove.bind(this);
123
+ this._boundOnMouseUp = this._onMouseUp.bind(this);
124
+ this._boundOnPointerDown = this._onPointerDown.bind(this);
125
+ this._boundOnPointerMove = this._onPointerMove.bind(this);
126
+ this._boundOnPointerUp = this._onPointerUp.bind(this);
127
+ this._boundOnPointerCancel = this._onPointerCancel.bind(this);
128
+ this._boundOnTouchStart = this._onTouchStart.bind(this);
129
+ this._boundOnTouchMove = this._onTouchMove.bind(this);
130
+ this._boundOnTouchEnd = this._onTouchEnd.bind(this);
131
+ this._boundOnTouchCancel = this._onTouchCancel.bind(this);
132
+ this._boundOnKeyDown = this._onKeyDown.bind(this);
133
+ this._supportsPointerEvents = typeof window !== "undefined" && "PointerEvent" in window;
134
+ this._activePointerId = null;
135
+ this._activeTouchId = null;
136
+ this._keyboardDraggedItem = null;
137
+ this._keyboardStartSnapshot = "";
138
+ this._liveRegion = null;
139
+
140
+ if (!this._rootList) {
141
+ return;
142
+ }
143
+
144
+ this._registerGroup();
145
+ this.refresh();
146
+ this._ensureLiveRegion();
147
+ this._bindEvents();
148
+ this._emit("init", {
149
+ payload: this.serialize()
150
+ });
151
+ }
152
+
153
+ static get NAME() {
154
+ return NAME;
155
+ }
156
+
157
+ static get NAME_KEY() {
158
+ return NAME_KEY;
159
+ }
160
+
161
+ _getEventKey(action) {
162
+ switch (action) {
163
+ case "init":
164
+ return EVENT_KEY_INIT;
165
+ case "refresh":
166
+ return EVENT_KEY_REFRESH;
167
+ case "pointerdown":
168
+ return EVENT_KEY_POINTER_DOWN;
169
+ case "start":
170
+ return EVENT_KEY_START;
171
+ case "move":
172
+ return EVENT_KEY_MOVE;
173
+ case "placeholdermove":
174
+ return EVENT_KEY_PLACEHOLDER_MOVE;
175
+ case "drop":
176
+ return EVENT_KEY_DROP;
177
+ case "transfer":
178
+ return EVENT_KEY_TRANSFER;
179
+ case "change":
180
+ return EVENT_KEY_CHANGE;
181
+ case "end":
182
+ return EVENT_KEY_END;
183
+ case "save":
184
+ return EVENT_KEY_SAVE;
185
+ case "destroy":
186
+ return EVENT_KEY_DESTROY;
187
+ default:
188
+ return `${NAME_KEY}.${action}`;
189
+ }
190
+ }
191
+
192
+ _emit(action, detail = {}) {
193
+ const payload = {
194
+ action,
195
+ instance: this,
196
+ ...detail
197
+ };
198
+
199
+ EventHandler.trigger(this._element, this._getEventKey(action), payload);
200
+
201
+ const callback = this._params?.callbacks?.[action];
202
+ if (typeof callback === "function") {
203
+ try {
204
+ callback(payload);
205
+ } catch (error) {
206
+ // Keep drag lifecycle stable even if external callback throws.
207
+ console.error(`[${NAME}] callback "${action}" failed`, error);
208
+ }
209
+ }
210
+
211
+ return payload;
212
+ }
213
+
214
+ _getGroupName() {
215
+ const name = this._params?.group;
216
+ if (name === null || name === undefined) return "";
217
+ return String(name).trim();
218
+ }
219
+
220
+ _registerGroup() {
221
+ const group = this._getGroupName();
222
+ if (!group) return;
223
+
224
+ if (!VGNestable._groups.has(group)) {
225
+ VGNestable._groups.set(group, new Set());
226
+ }
227
+
228
+ VGNestable._groups.get(group).add(this);
229
+ }
230
+
231
+ _unregisterGroup() {
232
+ const group = this._getGroupName();
233
+ if (!group || !VGNestable._groups.has(group)) return;
234
+
235
+ const set = VGNestable._groups.get(group);
236
+ set.delete(this);
237
+ if (!set.size) {
238
+ VGNestable._groups.delete(group);
239
+ }
240
+ }
241
+
242
+ _getConnectedInstances() {
243
+ if (!this._params.connect) return [this];
244
+
245
+ const group = this._getGroupName();
246
+ if (!group || !VGNestable._groups.has(group)) return [this];
247
+
248
+ return Array.from(VGNestable._groups.get(group)).filter(
249
+ (instance) => instance && instance._rootList
250
+ );
251
+ }
252
+
253
+ _findOwnerInstanceByElement(element) {
254
+ if (!element) return null;
255
+ return this._getConnectedInstances().find(
256
+ (instance) => instance._rootList && instance._rootList.contains(element)
257
+ ) || null;
258
+ }
259
+
260
+ _canAcceptItem(item, sourceInstance) {
261
+ const accept = this._params?.accept;
262
+ if (typeof accept !== "function") return true;
263
+ try {
264
+ return !!accept(item, sourceInstance, this);
265
+ } catch (error) {
266
+ console.error(`[${NAME}] accept() failed`, error);
267
+ return false;
268
+ }
269
+ }
270
+
271
+ refresh() {
272
+ this._getItems().forEach((item) => {
273
+ const handle = this._ensureItemLayout(item);
274
+
275
+ if (handle) {
276
+ handle.style.cursor = "grab";
277
+ handle.style.userSelect = "none";
278
+ handle.style.touchAction = "none";
279
+
280
+ if (!["BUTTON", "A", "INPUT"].includes(handle.tagName)) {
281
+ handle.setAttribute("role", "button");
282
+ }
283
+ if (!handle.hasAttribute("tabindex")) {
284
+ handle.setAttribute("tabindex", "0");
285
+ }
286
+ if (!handle.hasAttribute("aria-label")) {
287
+ handle.setAttribute("aria-label", "Drag item");
288
+ }
289
+ }
290
+
291
+ this._syncItemCollapse(item);
292
+ });
293
+
294
+ this._emit("refresh", {
295
+ items: this._getItems().length
296
+ });
297
+ }
298
+
299
+ _syncItemCollapse(item) {
300
+ if (!item || !this._params.collapse?.enabled) {
301
+ return;
302
+ }
303
+
304
+ const inner = this._getDirectChildBySelector(item, `.${CLASS_INNER}`);
305
+ const handle = inner ? this._getDirectChildBySelector(inner, `.${CLASS_HANDLE}`) : null;
306
+ const childList = this._getDirectChildBySelector(item, this._params.listselector);
307
+ let toggle = inner ? this._getDirectChildBySelector(inner, `.${CLASS_COLLAPSE_TOGGLE}`) : null;
308
+
309
+ if (!inner || !childList) {
310
+ if (toggle) {
311
+ toggle.remove();
312
+ }
313
+ return;
314
+ }
315
+
316
+ if (!childList.id) {
317
+ childList.id = `vg-nestable-collapse-${Math.random().toString(36).slice(2, 10)}`;
318
+ }
319
+
320
+ childList.classList.add("vg-collapse");
321
+ if (this._params.collapse.open) {
322
+ childList.classList.add("show");
323
+ } else {
324
+ childList.classList.remove("show");
325
+ }
326
+
327
+ if (!toggle) {
328
+ toggle = document.createElement("button");
329
+ toggle.type = "button";
330
+ toggle.className = CLASS_COLLAPSE_TOGGLE;
331
+ if (handle) {
332
+ inner.insertBefore(toggle, handle.nextSibling);
333
+ } else {
334
+ inner.insertBefore(toggle, inner.firstChild);
335
+ }
336
+ } else if (handle && toggle.previousElementSibling !== handle) {
337
+ inner.insertBefore(toggle, handle.nextSibling);
338
+ }
339
+
340
+ toggle.setAttribute("data-vg-toggle", "collapse");
341
+ toggle.setAttribute("data-vg-target", `#${childList.id}`);
342
+ toggle.setAttribute("aria-controls", childList.id);
343
+ const safeShowText = Sanitize.toSafeHtmlString(this._params.collapse.showtext || getSVG("chevron"));
344
+ const safeHideText = Sanitize.toSafeHtmlString(this._params.collapse.hidetext || getSVG("chevron"));
345
+ toggle.setAttribute("data-show-text", safeShowText);
346
+ toggle.setAttribute("data-hide-text", safeHideText);
347
+
348
+ const isOpen = childList.classList.contains("show");
349
+ toggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
350
+ toggle.innerHTML = isOpen ? safeHideText : safeShowText;
351
+
352
+ VGCollapse.getOrCreateInstance(childList, { toggle: false });
353
+ }
354
+
355
+ _ensureItemLayout(item) {
356
+ if (!item || !this._params.handleselector) {
357
+ return null;
358
+ }
359
+
360
+ const inner = this._ensureInnerLayout(item);
361
+ let handle = this._getDirectChildBySelector(inner, this._params.handleselector);
362
+
363
+ if (!handle) {
364
+ const ownHandle = Array.from(item.querySelectorAll(this._params.handleselector)).find(
365
+ (node) => node.closest(this._params.itemselector) === item
366
+ );
367
+
368
+ if (ownHandle) {
369
+ handle = ownHandle;
370
+ }
371
+ }
372
+
373
+ if (!handle) {
374
+ if (this._params.handleselector !== `.${CLASS_HANDLE}`) {
375
+ return null;
376
+ }
377
+
378
+ handle = document.createElement("div");
379
+ handle.classList.add(CLASS_HANDLE);
380
+ }
381
+
382
+ if (handle.parentElement !== inner || handle !== inner.firstElementChild) {
383
+ inner.insertBefore(handle, inner.firstChild);
384
+ }
385
+
386
+ if (!handle.querySelector(`:scope > .${CLASS_HANDLE_ICON}`)) {
387
+ const icon = document.createElement("span");
388
+ icon.className = CLASS_HANDLE_ICON;
389
+ icon.setAttribute("aria-hidden", "true");
390
+ icon.innerHTML = Sanitize.toSafeHtmlString(this._params.handleicon || getSVG("dots-six-vertical") || ":::");
391
+ handle.prepend(icon);
392
+ }
393
+
394
+ return handle;
395
+ }
396
+
397
+ _ensureInnerLayout(item) {
398
+ let inner = this._getDirectChildBySelector(item, `.${CLASS_INNER}`);
399
+ const childList = this._getDirectChildBySelector(item, this._params.listselector);
400
+
401
+ if (!inner) {
402
+ inner = document.createElement("div");
403
+ inner.className = CLASS_INNER;
404
+ item.insertBefore(inner, childList || item.firstChild);
405
+ }
406
+
407
+ const nodesToMove = Array.from(item.childNodes).filter((node) => {
408
+ if (node === inner) {
409
+ return false;
410
+ }
411
+ return !(node.nodeType === Node.ELEMENT_NODE && node.matches(this._params.listselector));
412
+ });
413
+
414
+ nodesToMove.forEach((node) => inner.append(node));
415
+
416
+ if (inner.parentElement !== item) {
417
+ item.insertBefore(inner, childList || item.firstChild);
418
+ }
419
+
420
+ return inner;
421
+ }
422
+
423
+ _getDirectChildBySelector(parent, selector) {
424
+ if (!parent || !selector) {
425
+ return null;
426
+ }
427
+
428
+ return Array.from(parent.children).find((child) => child.matches(selector)) || null;
429
+ }
430
+
431
+ serialize() {
432
+ if (!this._rootList) {
433
+ return [];
434
+ }
435
+
436
+ return this._serializeList(this._rootList);
437
+ }
438
+
439
+ save() {
440
+ const payload = this.serialize();
441
+ const route = this._params.ajax?.route;
442
+
443
+ if (!route) {
444
+ this._emit("save", { payload, status: "skipped" });
445
+ return Promise.resolve(payload);
446
+ }
447
+
448
+ this._emit("save", { payload, status: "start" });
449
+
450
+ return new Promise((resolve, reject) => {
451
+ const ajaxParams = this._params.ajax || {};
452
+ const field = ajaxParams.field || "items";
453
+ const method = String(ajaxParams.method || "post").toLowerCase();
454
+ const timeout = Number(ajaxParams.timeout || 0);
455
+ const data = mergeDeepObject(ajaxParams.data || {}, {
456
+ [field]: payload
457
+ });
458
+ const ajax = new Ajax();
459
+ const callbacks = {
460
+ onSuccess: (response) => {
461
+ this._emit("save", {
462
+ payload,
463
+ status: "success",
464
+ response
465
+ });
466
+ resolve(response);
467
+ },
468
+ onError: (error) => {
469
+ this._emit("save", {
470
+ payload,
471
+ status: "error",
472
+ error
473
+ });
474
+ reject(error);
475
+ }
476
+ };
477
+
478
+ setTimeout(() => {
479
+ switch (method) {
480
+ case "get":
481
+ ajax.get(route, callbacks);
482
+ break;
483
+ case "delete":
484
+ ajax.delete(route, callbacks);
485
+ break;
486
+ default:
487
+ ajax.post(route, data, callbacks);
488
+ break;
489
+ }
490
+ }, timeout);
491
+ });
492
+ }
493
+
494
+ dispose() {
495
+ if (this._rootList) {
496
+ if (this._supportsPointerEvents) {
497
+ this._rootList.removeEventListener("pointerdown", this._boundOnPointerDown);
498
+ } else {
499
+ this._rootList.removeEventListener("mousedown", this._boundOnMouseDown);
500
+ this._rootList.removeEventListener("touchstart", this._boundOnTouchStart);
501
+ }
502
+ this._rootList.removeEventListener("keydown", this._boundOnKeyDown);
503
+ }
504
+
505
+ document.removeEventListener("mousemove", this._boundOnMouseMove);
506
+ document.removeEventListener("mouseup", this._boundOnMouseUp);
507
+ document.removeEventListener("pointermove", this._boundOnPointerMove);
508
+ document.removeEventListener("pointerup", this._boundOnPointerUp);
509
+ document.removeEventListener("pointercancel", this._boundOnPointerCancel);
510
+ document.removeEventListener("touchmove", this._boundOnTouchMove);
511
+ document.removeEventListener("touchend", this._boundOnTouchEnd);
512
+ document.removeEventListener("touchcancel", this._boundOnTouchCancel);
513
+
514
+ this._clearDragState();
515
+ this._clearKeyboardDragState();
516
+ if (this._liveRegion?.parentElement) {
517
+ this._liveRegion.remove();
518
+ }
519
+ this._liveRegion = null;
520
+ this._unregisterGroup();
521
+ this._emit("destroy");
522
+ super.dispose();
523
+ }
524
+
525
+ _bindEvents() {
526
+ this._rootList.addEventListener("keydown", this._boundOnKeyDown);
527
+
528
+ if (this._supportsPointerEvents) {
529
+ this._rootList.addEventListener("pointerdown", this._boundOnPointerDown);
530
+ return;
531
+ }
532
+
533
+ this._rootList.addEventListener("mousedown", this._boundOnMouseDown);
534
+ this._rootList.addEventListener("touchstart", this._boundOnTouchStart, { passive: false });
535
+ }
536
+
537
+ _resolveRootList() {
538
+ if (!this._element) {
539
+ return null;
540
+ }
541
+
542
+ if (this._element.matches(this._params.listselector)) {
543
+ return this._element;
544
+ }
545
+
546
+ return this._element.querySelector(this._params.listselector);
547
+ }
548
+
549
+ _getItems(scope = this._rootList) {
550
+ if (!scope) {
551
+ return [];
552
+ }
553
+
554
+ return Array.from(scope.querySelectorAll(this._params.itemselector));
555
+ }
556
+
557
+ _onMouseDown(event) {
558
+ if (event.button !== 0) {
559
+ return;
560
+ }
561
+
562
+ this._startInteraction(event.target, event.clientX, event.clientY, event);
563
+ if (!this._draggedItem) {
564
+ return;
565
+ }
566
+
567
+ document.addEventListener("mousemove", this._boundOnMouseMove);
568
+ document.addEventListener("mouseup", this._boundOnMouseUp);
569
+ }
570
+
571
+ _onMouseMove(event) {
572
+ this._processDragMove(event.clientX, event.clientY, event);
573
+ }
574
+
575
+ _onMouseUp() {
576
+ document.removeEventListener("mousemove", this._boundOnMouseMove);
577
+ document.removeEventListener("mouseup", this._boundOnMouseUp);
578
+ this._finishDrag();
579
+ }
580
+
581
+ _onPointerDown(event) {
582
+ if (event.pointerType === "mouse" && event.button !== 0) {
583
+ return;
584
+ }
585
+
586
+ this._startInteraction(event.target, event.clientX, event.clientY, event);
587
+ if (!this._draggedItem) {
588
+ return;
589
+ }
590
+
591
+ this._activePointerId = event.pointerId;
592
+ document.addEventListener("pointermove", this._boundOnPointerMove);
593
+ document.addEventListener("pointerup", this._boundOnPointerUp);
594
+ document.addEventListener("pointercancel", this._boundOnPointerCancel);
595
+ }
596
+
597
+ _onPointerMove(event) {
598
+ if (!this._draggedItem || event.pointerId !== this._activePointerId) {
599
+ return;
600
+ }
601
+
602
+ this._processDragMove(event.clientX, event.clientY, event);
603
+ }
604
+
605
+ _onPointerUp(event) {
606
+ if (event.pointerId !== this._activePointerId) {
607
+ return;
608
+ }
609
+
610
+ this._activePointerId = null;
611
+ document.removeEventListener("pointermove", this._boundOnPointerMove);
612
+ document.removeEventListener("pointerup", this._boundOnPointerUp);
613
+ document.removeEventListener("pointercancel", this._boundOnPointerCancel);
614
+ this._finishDrag();
615
+ }
616
+
617
+ _onPointerCancel(event) {
618
+ if (event.pointerId !== this._activePointerId) {
619
+ return;
620
+ }
621
+
622
+ this._activePointerId = null;
623
+ document.removeEventListener("pointermove", this._boundOnPointerMove);
624
+ document.removeEventListener("pointerup", this._boundOnPointerUp);
625
+ document.removeEventListener("pointercancel", this._boundOnPointerCancel);
626
+ this._cancelDrag();
627
+ }
628
+
629
+ _onTouchStart(event) {
630
+ if (this._activeTouchId !== null || !event.changedTouches?.length) {
631
+ return;
632
+ }
633
+
634
+ const touch = event.changedTouches[0];
635
+ this._startInteraction(touch.target, touch.clientX, touch.clientY, event);
636
+ if (!this._draggedItem) {
637
+ return;
638
+ }
639
+
640
+ this._activeTouchId = touch.identifier;
641
+ document.addEventListener("touchmove", this._boundOnTouchMove, { passive: false });
642
+ document.addEventListener("touchend", this._boundOnTouchEnd);
643
+ document.addEventListener("touchcancel", this._boundOnTouchCancel);
644
+ }
645
+
646
+ _onTouchMove(event) {
647
+ if (!this._draggedItem || this._activeTouchId === null) {
648
+ return;
649
+ }
650
+
651
+ const touch = this._findTouchById(event.changedTouches, this._activeTouchId)
652
+ || this._findTouchById(event.touches, this._activeTouchId);
653
+ if (!touch) {
654
+ return;
655
+ }
656
+
657
+ this._processDragMove(touch.clientX, touch.clientY, event);
658
+ }
659
+
660
+ _onTouchEnd(event) {
661
+ if (this._activeTouchId === null) {
662
+ return;
663
+ }
664
+
665
+ const touch = this._findTouchById(event.changedTouches, this._activeTouchId);
666
+ if (!touch) {
667
+ return;
668
+ }
669
+
670
+ this._activeTouchId = null;
671
+ document.removeEventListener("touchmove", this._boundOnTouchMove);
672
+ document.removeEventListener("touchend", this._boundOnTouchEnd);
673
+ document.removeEventListener("touchcancel", this._boundOnTouchCancel);
674
+ this._finishDrag();
675
+ }
676
+
677
+ _onTouchCancel() {
678
+ if (this._activeTouchId === null) {
679
+ return;
680
+ }
681
+
682
+ this._activeTouchId = null;
683
+ document.removeEventListener("touchmove", this._boundOnTouchMove);
684
+ document.removeEventListener("touchend", this._boundOnTouchEnd);
685
+ document.removeEventListener("touchcancel", this._boundOnTouchCancel);
686
+ this._cancelDrag();
687
+ }
688
+
689
+ _onKeyDown(event) {
690
+ if (this._isDragging || this._draggedItem) {
691
+ return;
692
+ }
693
+
694
+ const item = event.target?.closest?.(this._params.itemselector);
695
+ if (!item || !this._rootList.contains(item)) {
696
+ return;
697
+ }
698
+
699
+ const key = event.key;
700
+ const isToggleKey = key === " " || key === "Enter";
701
+
702
+ if (isToggleKey && !this._keyboardDraggedItem) {
703
+ if (!this._isKeyboardDragHandle(event.target, item)) {
704
+ return;
705
+ }
706
+
707
+ event.preventDefault();
708
+ this._startKeyboardDrag(item);
709
+ return;
710
+ }
711
+
712
+ if (!this._keyboardDraggedItem) {
713
+ return;
714
+ }
715
+
716
+ if (item !== this._keyboardDraggedItem && !this._keyboardDraggedItem.contains(item)) {
717
+ return;
718
+ }
719
+
720
+ if (isToggleKey) {
721
+ event.preventDefault();
722
+ this._finishKeyboardDrag();
723
+ return;
724
+ }
725
+
726
+ if (key === "Escape") {
727
+ event.preventDefault();
728
+ this._finishKeyboardDrag({ cancelled: true });
729
+ return;
730
+ }
731
+
732
+ let moved = false;
733
+ let moveAnnouncement = "";
734
+ const moveAxis = this._getMoveAxis();
735
+ const allowVertical = moveAxis !== "horizontal";
736
+ const allowHorizontal = moveAxis !== "vertical";
737
+
738
+ if (key === "ArrowUp") {
739
+ if (!allowVertical) {
740
+ return;
741
+ }
742
+ event.preventDefault();
743
+ moved = this._moveKeyboardUp(this._keyboardDraggedItem);
744
+ moveAnnouncement = "Moved up.";
745
+ } else if (key === "ArrowDown") {
746
+ if (!allowVertical) {
747
+ return;
748
+ }
749
+ event.preventDefault();
750
+ moved = this._moveKeyboardDown(this._keyboardDraggedItem);
751
+ moveAnnouncement = "Moved down.";
752
+ } else if (key === "ArrowRight") {
753
+ if (!allowHorizontal) {
754
+ return;
755
+ }
756
+ event.preventDefault();
757
+ moved = this._indentKeyboard(this._keyboardDraggedItem);
758
+ moveAnnouncement = "Nested into previous item.";
759
+ } else if (key === "ArrowLeft") {
760
+ if (!allowHorizontal) {
761
+ return;
762
+ }
763
+ event.preventDefault();
764
+ moved = this._outdentKeyboard(this._keyboardDraggedItem);
765
+ moveAnnouncement = "Moved out one level.";
766
+ }
767
+
768
+ if (!moved) {
769
+ return;
770
+ }
771
+
772
+ this._cleanupEmptyLists();
773
+ this.refresh();
774
+ this._keyboardDraggedItem.focus?.();
775
+ this._emit("move", {
776
+ item: this._keyboardDraggedItem,
777
+ targetInstance: this,
778
+ keyboard: true,
779
+ mouse: null
780
+ });
781
+ if (moveAnnouncement) {
782
+ this._announce(moveAnnouncement);
783
+ }
784
+ }
785
+
786
+ _isKeyboardDragHandle(target, item) {
787
+ if (!target || !item) {
788
+ return false;
789
+ }
790
+
791
+ const handleSelector = this._params.handleselector;
792
+ if (!handleSelector) {
793
+ return true;
794
+ }
795
+
796
+ const handle = target.closest(handleSelector);
797
+ return !!(handle && item.contains(handle));
798
+ }
799
+
800
+ _startKeyboardDrag(item) {
801
+ if (!item || this._keyboardDraggedItem) {
802
+ return;
803
+ }
804
+
805
+ this._keyboardDraggedItem = item;
806
+ this._keyboardStartSnapshot = JSON.stringify(this.serialize());
807
+ item.classList.add(CLASS_ITEM_DRAGGING);
808
+ item.setAttribute("aria-grabbed", "true");
809
+
810
+ this._emit("start", {
811
+ item,
812
+ payload: this.serialize(),
813
+ keyboard: true
814
+ });
815
+ this._announce(this._getKeyboardDragInstructions());
816
+ }
817
+
818
+ _finishKeyboardDrag(options = {}) {
819
+ if (!this._keyboardDraggedItem) {
820
+ return;
821
+ }
822
+
823
+ const draggedItem = this._keyboardDraggedItem;
824
+ const previousPayload = this._keyboardStartSnapshot ? JSON.parse(this._keyboardStartSnapshot) : [];
825
+ const payload = this.serialize();
826
+ const changed = this._keyboardStartSnapshot !== JSON.stringify(payload);
827
+
828
+ this._emit("drop", {
829
+ item: draggedItem,
830
+ payload,
831
+ previousPayload,
832
+ changed,
833
+ targetInstance: this,
834
+ keyboard: true,
835
+ cancelled: !!options.cancelled
836
+ });
837
+
838
+ if (changed) {
839
+ this._emit("change", { payload, previousPayload, keyboard: true });
840
+ }
841
+
842
+ this._emit("end", {
843
+ payload,
844
+ changed,
845
+ keyboard: true,
846
+ cancelled: !!options.cancelled
847
+ });
848
+ if (options.cancelled) {
849
+ this._announce("Move cancelled.");
850
+ } else if (changed) {
851
+ this._announce("Item dropped.");
852
+ } else {
853
+ this._announce("Item position unchanged.");
854
+ }
855
+
856
+ if (changed && this._params.ajax?.route) {
857
+ this.save();
858
+ }
859
+
860
+ this._clearKeyboardDragState();
861
+ }
862
+
863
+ _clearKeyboardDragState() {
864
+ if (this._keyboardDraggedItem) {
865
+ this._keyboardDraggedItem.classList.remove(CLASS_ITEM_DRAGGING);
866
+ this._keyboardDraggedItem.removeAttribute("aria-grabbed");
867
+ }
868
+
869
+ this._keyboardDraggedItem = null;
870
+ this._keyboardStartSnapshot = "";
871
+ }
872
+
873
+ _moveKeyboardUp(item) {
874
+ const prev = this._getPreviousItem(item);
875
+ if (!prev) {
876
+ this._announce("Cannot move up.");
877
+ return false;
878
+ }
879
+
880
+ const list = item.parentElement;
881
+ list.insertBefore(item, prev);
882
+ return true;
883
+ }
884
+
885
+ _moveKeyboardDown(item) {
886
+ const next = this._getNextItem(item);
887
+ if (!next) {
888
+ this._announce("Cannot move down.");
889
+ return false;
890
+ }
891
+
892
+ const list = item.parentElement;
893
+ list.insertBefore(item, next.nextSibling);
894
+ return true;
895
+ }
896
+
897
+ _indentKeyboard(item) {
898
+ const parentCandidate = this._getPreviousItem(item);
899
+ if (!parentCandidate || parentCandidate === item || item.contains(parentCandidate)) {
900
+ this._announce("Cannot nest here.");
901
+ return false;
902
+ }
903
+
904
+ const childList = this._getOrCreateChildList(parentCandidate);
905
+ if (this._isDepthExceeded(childList, item)) {
906
+ this._announce("Maximum nesting depth reached.");
907
+ return false;
908
+ }
909
+
910
+ childList.append(item);
911
+ return true;
912
+ }
913
+
914
+ _outdentKeyboard(item) {
915
+ const currentList = item.parentElement;
916
+ const parentItem = currentList?.closest(this._params.itemselector);
917
+ if (!parentItem) {
918
+ this._announce("Cannot move left.");
919
+ return false;
920
+ }
921
+
922
+ const targetList = parentItem.parentElement;
923
+ if (!targetList) {
924
+ this._announce("Cannot move left.");
925
+ return false;
926
+ }
927
+
928
+ targetList.insertBefore(item, parentItem.nextSibling);
929
+ return true;
930
+ }
931
+
932
+ _getMoveAxis(params = this._params) {
933
+ const rawAxis = String(params?.moveaxis || "default").trim().toLowerCase();
934
+ if (rawAxis === "vertical" || rawAxis === "horizontal") {
935
+ return rawAxis;
936
+ }
937
+ return "default";
938
+ }
939
+
940
+ _getKeyboardDragInstructions() {
941
+ const moveAxis = this._getMoveAxis();
942
+ if (moveAxis === "vertical") {
943
+ return "Picked up item. Use Up/Down arrows to move, Enter or Space to drop, Escape to cancel.";
944
+ }
945
+ if (moveAxis === "horizontal") {
946
+ return "Picked up item. Use Left/Right arrows to move, Enter or Space to drop, Escape to cancel.";
947
+ }
948
+ return "Picked up item. Use arrow keys to move, Enter or Space to drop, Escape to cancel.";
949
+ }
950
+
951
+ _startInteraction(target, clientX, clientY, event) {
952
+ if (this._keyboardDraggedItem) {
953
+ this._clearKeyboardDragState();
954
+ }
955
+
956
+ const item = target?.closest?.(this._params.itemselector);
957
+ if (!item || !this._rootList.contains(item)) {
958
+ return;
959
+ }
960
+
961
+ const handleSelector = this._params.handleselector;
962
+ if (handleSelector) {
963
+ const handle = target.closest(handleSelector);
964
+ if (!handle || !item.contains(handle)) {
965
+ return;
966
+ }
967
+ }
968
+
969
+ event.preventDefault();
970
+
971
+ const itemRect = item.getBoundingClientRect();
972
+ this._draggedItem = item;
973
+ this._mouse.startX = clientX;
974
+ this._mouse.startY = clientY;
975
+ this._mouse.startItemX = itemRect.left;
976
+ this._mouse.grabOffsetX = clientX - itemRect.left;
977
+ this._mouse.grabOffsetY = clientY - itemRect.top;
978
+ this._mouse.x = clientX;
979
+ this._mouse.y = clientY;
980
+ this._isDragging = false;
981
+ this._startSnapshot = "";
982
+ this._emit("pointerdown", {
983
+ item: this._draggedItem,
984
+ mouse: {
985
+ x: this._mouse.x,
986
+ y: this._mouse.y
987
+ }
988
+ });
989
+ }
990
+
991
+ _processDragMove(clientX, clientY, event) {
992
+ if (!this._draggedItem) {
993
+ return;
994
+ }
995
+
996
+ this._mouse.x = clientX;
997
+ this._mouse.y = clientY;
998
+
999
+ if (!this._isDragging) {
1000
+ const deltaX = Math.abs(this._mouse.x - this._mouse.startX);
1001
+ const deltaY = Math.abs(this._mouse.y - this._mouse.startY);
1002
+ if (deltaX < 3 && deltaY < 3) {
1003
+ return;
1004
+ }
1005
+ this._startDrag();
1006
+ }
1007
+
1008
+ event.preventDefault();
1009
+
1010
+ this._moveDragElement(this._mouse.x, this._mouse.y);
1011
+
1012
+ const pointerTarget = this._getPointerTarget(this._mouse.x, this._mouse.y);
1013
+ if (!pointerTarget) {
1014
+ this._setDropListState(null);
1015
+ return;
1016
+ }
1017
+
1018
+ if (pointerTarget.closest(`.${CLASS_PLACEHOLDER}`)) {
1019
+ return;
1020
+ }
1021
+
1022
+ const targetInstance = this._findOwnerInstanceByElement(pointerTarget) || this;
1023
+ const hoveredItem = pointerTarget.closest(targetInstance._params.itemselector);
1024
+ const list = this._resolveDropList(pointerTarget, hoveredItem, targetInstance);
1025
+ if (!list || !targetInstance._rootList.contains(list)) {
1026
+ this._setDropListState(null);
1027
+ return;
1028
+ }
1029
+
1030
+ if (!targetInstance._canAcceptItem(this._draggedItem, this._sourceInstance)) {
1031
+ this._setDropListState(list, true);
1032
+ return;
1033
+ }
1034
+
1035
+ const mode = this._resolveMode(this._mouse.x, this._mouse.y, hoveredItem, targetInstance._params);
1036
+ this._emit("move", {
1037
+ item: this._draggedItem,
1038
+ hoveredItem,
1039
+ mode,
1040
+ targetInstance,
1041
+ mouse: {
1042
+ x: this._mouse.x,
1043
+ y: this._mouse.y
1044
+ }
1045
+ });
1046
+ if (mode === "keep") {
1047
+ this._setDropListState(list);
1048
+ return;
1049
+ }
1050
+
1051
+ if (mode === "child") {
1052
+ // Indent: make dragged item a child of the hovered item (when available).
1053
+ // This lets the user "shift right" without needing an extra vertical nudge first.
1054
+ let parentCandidate = null;
1055
+
1056
+ if (hoveredItem) {
1057
+ if (hoveredItem === this._draggedItem || this._draggedItem.contains(hoveredItem)) {
1058
+ this._setDropListState(null);
1059
+ return;
1060
+ }
1061
+ parentCandidate = hoveredItem;
1062
+ } else {
1063
+ this._placePlaceholderWhenExplicitlyNeeded(list, this._mouse.y, targetInstance);
1064
+ const placeholderParent = this._placeholder?.parentElement || null;
1065
+ if (placeholderParent) {
1066
+ parentCandidate = targetInstance._getPreviousItem(this._placeholder);
1067
+ while (parentCandidate && (parentCandidate === this._draggedItem || this._draggedItem.contains(parentCandidate))) {
1068
+ parentCandidate = targetInstance._getPreviousItem(parentCandidate);
1069
+ }
1070
+ }
1071
+ }
1072
+
1073
+ if (!parentCandidate) {
1074
+ this._setDropListState(null);
1075
+ return;
1076
+ }
1077
+
1078
+ const childList = targetInstance._getOrCreateChildList(parentCandidate);
1079
+ if (targetInstance._isDepthExceeded(childList, this._draggedItem)) {
1080
+ this._setDropListState(childList, true);
1081
+ return;
1082
+ }
1083
+
1084
+ this._placePlaceholder(childList, null);
1085
+ this._setDropListState(childList);
1086
+ this._currentTargetInstance = targetInstance;
1087
+ this._lastDropInstance = targetInstance;
1088
+ return;
1089
+ }
1090
+
1091
+ if (hoveredItem) {
1092
+ if (hoveredItem === this._draggedItem || this._draggedItem.contains(hoveredItem)) {
1093
+ this._setDropListState(null);
1094
+ return;
1095
+ }
1096
+
1097
+ const targetList = hoveredItem.parentElement;
1098
+ if (targetInstance._isDepthExceeded(targetList, this._draggedItem)) {
1099
+ this._setDropListState(targetList, true);
1100
+ return;
1101
+ }
1102
+
1103
+ if (mode === "before") {
1104
+ this._placePlaceholder(targetList, hoveredItem);
1105
+ } else {
1106
+ this._placePlaceholder(targetList, hoveredItem.nextSibling);
1107
+ }
1108
+ this._setDropListState(targetList);
1109
+ this._currentTargetInstance = targetInstance;
1110
+ this._lastDropInstance = targetInstance;
1111
+ return;
1112
+ }
1113
+
1114
+ this._placePlaceholderWhenExplicitlyNeeded(list, this._mouse.y, targetInstance);
1115
+ this._setDropListState(list);
1116
+ this._currentTargetInstance = targetInstance;
1117
+ this._lastDropInstance = targetInstance;
1118
+ }
1119
+
1120
+ _finishDrag() {
1121
+ if (!this._draggedItem) {
1122
+ return;
1123
+ }
1124
+
1125
+ const draggedItem = this._draggedItem;
1126
+ const hadDrag = this._isDragging;
1127
+ const targetInstance = this._lastDropInstance || this;
1128
+ const previousPayload = this._startSnapshot ? JSON.parse(this._startSnapshot) : [];
1129
+ let sourcePayload = this.serialize();
1130
+ let targetPayload = targetInstance.serialize();
1131
+ let changed = false;
1132
+
1133
+ if (hadDrag) {
1134
+ const placeholderParent = this._placeholder?.parentElement || null;
1135
+ if (placeholderParent) {
1136
+ placeholderParent.insertBefore(draggedItem, this._placeholder);
1137
+ }
1138
+
1139
+ draggedItem.style.display = "";
1140
+ this._cleanupEmptyLists();
1141
+ if (targetInstance !== this) {
1142
+ targetInstance._cleanupEmptyLists();
1143
+ }
1144
+ this.refresh();
1145
+ if (targetInstance !== this) {
1146
+ targetInstance.refresh();
1147
+ }
1148
+
1149
+ sourcePayload = this.serialize();
1150
+ targetPayload = targetInstance.serialize();
1151
+ changed = this._startSnapshot !== JSON.stringify(sourcePayload) || targetInstance !== this;
1152
+ }
1153
+
1154
+ const payload = sourcePayload;
1155
+ this._emit("drop", {
1156
+ item: draggedItem,
1157
+ payload,
1158
+ previousPayload,
1159
+ changed,
1160
+ targetInstance
1161
+ });
1162
+
1163
+ if (changed && targetInstance !== this) {
1164
+ this._emit("transfer", {
1165
+ item: draggedItem,
1166
+ from: this,
1167
+ to: targetInstance,
1168
+ sourcePayload,
1169
+ targetPayload
1170
+ });
1171
+ targetInstance._emit("transfer", {
1172
+ item: draggedItem,
1173
+ from: this,
1174
+ to: targetInstance,
1175
+ sourcePayload,
1176
+ targetPayload
1177
+ });
1178
+ targetInstance._emit("change", { payload: targetPayload, previousPayload: [] });
1179
+ }
1180
+
1181
+ this._clearDragState();
1182
+
1183
+ if (changed) {
1184
+ this._emit("change", { payload, previousPayload });
1185
+ }
1186
+
1187
+ if (hadDrag) {
1188
+ this._emit("end", {
1189
+ payload,
1190
+ changed
1191
+ });
1192
+ }
1193
+
1194
+ if (changed) {
1195
+ if (this._params.ajax?.route) {
1196
+ this.save();
1197
+ }
1198
+ if (targetInstance !== this && targetInstance._params.ajax?.route) {
1199
+ targetInstance.save();
1200
+ }
1201
+ }
1202
+ }
1203
+
1204
+ _cancelDrag() {
1205
+ if (!this._draggedItem) {
1206
+ return;
1207
+ }
1208
+
1209
+ const hadDrag = this._isDragging;
1210
+ this._clearDragState();
1211
+
1212
+ if (hadDrag) {
1213
+ this._emit("end", {
1214
+ payload: this.serialize(),
1215
+ changed: false,
1216
+ cancelled: true
1217
+ });
1218
+ }
1219
+ }
1220
+
1221
+ _findTouchById(touchList, id) {
1222
+ if (!touchList || id === null || id === undefined) {
1223
+ return null;
1224
+ }
1225
+
1226
+ for (let index = 0; index < touchList.length; index += 1) {
1227
+ if (touchList[index].identifier === id) {
1228
+ return touchList[index];
1229
+ }
1230
+ }
1231
+
1232
+ return null;
1233
+ }
1234
+
1235
+ _startDrag() {
1236
+ if (!this._draggedItem || this._isDragging) {
1237
+ return;
1238
+ }
1239
+
1240
+ this._isDragging = true;
1241
+ this._startSnapshot = JSON.stringify(this.serialize());
1242
+ this._sourceInstance = this;
1243
+ this._sourceRoot = this._rootList;
1244
+ this._currentTargetInstance = this;
1245
+ this._lastDropInstance = this;
1246
+
1247
+ this._placeholder = this._createPlaceholder(this._draggedItem);
1248
+ if (!this._params.showplaceholder) {
1249
+ this._placeholder.classList.add(CLASS_PLACEHOLDER_HIDDEN);
1250
+ }
1251
+
1252
+ this._draggedItem.parentElement.insertBefore(this._placeholder, this._draggedItem.nextSibling);
1253
+
1254
+ this._dragElement = this._createDragElement(this._draggedItem);
1255
+ this._dragLayer = document.createElement("div");
1256
+ this._dragLayer.classList.add(CLASS_DRAG_LAYER);
1257
+ // Keep scoped styles working for the drag preview (e.g. ".vg-nestable .vg-nestable-item ...").
1258
+ if (this._element?.classList) {
1259
+ this._element.classList.forEach((className) => this._dragLayer.classList.add(className));
1260
+ }
1261
+ this._dragLayer.style.position = "fixed";
1262
+ this._dragLayer.style.left = "0";
1263
+ this._dragLayer.style.top = "0";
1264
+ this._dragLayer.style.width = "0";
1265
+ this._dragLayer.style.height = "0";
1266
+ this._dragLayer.style.pointerEvents = "none";
1267
+ this._dragLayer.style.zIndex = "99999";
1268
+ document.body.append(this._dragLayer);
1269
+ this._dragLayer.append(this._dragElement);
1270
+
1271
+ if (typeof document !== "undefined" && document.body) {
1272
+ this._previousBodyCursor = document.body.style.cursor || "";
1273
+ document.body.style.cursor = "grabbing";
1274
+ }
1275
+
1276
+ this._moveDragElement(this._mouse.x, this._mouse.y);
1277
+
1278
+ this._draggedItem.classList.add(CLASS_ITEM_DRAGGING);
1279
+ this._draggedItem.style.display = "none";
1280
+ this._draggedItem.setAttribute("aria-grabbed", "true");
1281
+
1282
+ this._emit("start", {
1283
+ item: this._draggedItem,
1284
+ payload: this.serialize()
1285
+ });
1286
+ }
1287
+
1288
+ _createDragElement(item) {
1289
+ const rect = item.getBoundingClientRect();
1290
+ const dragEl = item.cloneNode(true);
1291
+
1292
+ dragEl.classList.add(CLASS_ITEM_GHOST, CLASS_DRAG_ELEMENT);
1293
+ dragEl.style.position = "fixed";
1294
+ dragEl.style.left = `${rect.left}px`;
1295
+ dragEl.style.top = `${rect.top}px`;
1296
+ dragEl.style.width = `${rect.width}px`;
1297
+ dragEl.style.zIndex = "99999";
1298
+ dragEl.style.pointerEvents = "none";
1299
+ dragEl.style.margin = "0";
1300
+
1301
+ return dragEl;
1302
+ }
1303
+
1304
+ _moveDragElement(x, y) {
1305
+ if (!this._dragElement) {
1306
+ return;
1307
+ }
1308
+
1309
+ // Keep the cursor at the same relative point where the user grabbed the item.
1310
+ const left = x - (Number(this._mouse.grabOffsetX) || 0);
1311
+ const top = y - (Number(this._mouse.grabOffsetY) || 0);
1312
+ this._dragElement.style.left = `${left}px`;
1313
+ this._dragElement.style.top = `${top}px`;
1314
+ }
1315
+
1316
+ _getPointerTarget(x, y) {
1317
+ const el = document.elementFromPoint(x, y);
1318
+ if (!el) {
1319
+ return null;
1320
+ }
1321
+
1322
+ return this._findOwnerInstanceByElement(el) ? el : null;
1323
+ }
1324
+
1325
+ _resolveDropList(pointerTarget, hoveredItem, targetInstance = this) {
1326
+ if (hoveredItem) {
1327
+ return hoveredItem.parentElement;
1328
+ }
1329
+
1330
+ const list = pointerTarget.closest(targetInstance._params.listselector);
1331
+ if (list && targetInstance._rootList.contains(list)) {
1332
+ return list;
1333
+ }
1334
+
1335
+ return targetInstance._rootList;
1336
+ }
1337
+
1338
+ _resolveMode(pointerX, pointerY, hoveredItem, params = this._params) {
1339
+ if (!hoveredItem) {
1340
+ return "append";
1341
+ }
1342
+
1343
+ const rect = hoveredItem.getBoundingClientRect();
1344
+ const offsetY = pointerY - rect.top;
1345
+ // Use the dragged item's original left edge as baseline, not the pointer-down X.
1346
+ // This makes "shift right to nest" work reliably regardless of where inside the handle the user grabbed.
1347
+ const baseX = Number.isFinite(this._mouse.startItemX) ? this._mouse.startItemX : this._mouse.startX;
1348
+ const offsetX = pointerX - baseX;
1349
+ const indentValue = parseFloat(params.indent);
1350
+ const indent = Number.isFinite(indentValue) ? indentValue : 0;
1351
+ const moveAxis = this._getMoveAxis(params);
1352
+ const allowVertical = moveAxis !== "horizontal";
1353
+ const allowHorizontal = moveAxis !== "vertical";
1354
+ const neighborThresholdPercent = Math.max(
1355
+ 0,
1356
+ Math.min(49, Number(params.neighborchangethreshold || 0))
1357
+ );
1358
+
1359
+ if (neighborThresholdPercent > 0) {
1360
+ const edgeRatio = neighborThresholdPercent / 100;
1361
+ const topBorder = rect.height * edgeRatio;
1362
+ const bottomBorder = rect.height * (1 - edgeRatio);
1363
+
1364
+ if (allowHorizontal && offsetX > indent) {
1365
+ return "child";
1366
+ }
1367
+
1368
+ if (allowVertical && offsetY <= topBorder) {
1369
+ return "before";
1370
+ }
1371
+
1372
+ if (allowVertical && offsetY >= bottomBorder) {
1373
+ return "after";
1374
+ }
1375
+
1376
+ return "keep";
1377
+ }
1378
+
1379
+ const threshold = Math.min(
1380
+ 0.45,
1381
+ Math.max(0.05, Number(params.hoverthreshold || 0.18))
1382
+ );
1383
+
1384
+ if (allowHorizontal && offsetX > indent) {
1385
+ return "child";
1386
+ }
1387
+
1388
+ if (allowVertical && offsetY < rect.height * (0.5 - threshold)) {
1389
+ return "before";
1390
+ }
1391
+
1392
+ if (allowVertical && offsetY > rect.height * (0.5 + threshold)) {
1393
+ return "after";
1394
+ }
1395
+
1396
+ return "keep";
1397
+ }
1398
+
1399
+ _getPreviousItem(item) {
1400
+ let sibling = item?.previousElementSibling || null;
1401
+ while (sibling && !sibling.matches(this._params.itemselector)) {
1402
+ sibling = sibling.previousElementSibling;
1403
+ }
1404
+ return sibling;
1405
+ }
1406
+
1407
+ _getNextItem(item) {
1408
+ let sibling = item?.nextElementSibling || null;
1409
+ while (sibling && !sibling.matches(this._params.itemselector)) {
1410
+ sibling = sibling.nextElementSibling;
1411
+ }
1412
+ return sibling;
1413
+ }
1414
+
1415
+ _getOrCreateChildList(item) {
1416
+ let list = item.querySelector(`:scope > ${this._params.listselector}`);
1417
+ if (list) {
1418
+ return list;
1419
+ }
1420
+
1421
+ list = document.createElement(this._rootList.tagName || "ol");
1422
+
1423
+ if (this._params.childlistclass) {
1424
+ this._params.childlistclass
1425
+ .split(" ")
1426
+ .filter(Boolean)
1427
+ .forEach((className) => list.classList.add(className));
1428
+ }
1429
+
1430
+ item.append(list);
1431
+ return list;
1432
+ }
1433
+
1434
+ _isDepthExceeded(list, draggedItem = this._draggedItem) {
1435
+ const listDepth = this._getListDepth(list);
1436
+ const maxDepth = Number(this._params.maxdepth || 1);
1437
+ const subtreeDepth = this._getItemSubtreeDepth(draggedItem);
1438
+ return (listDepth + subtreeDepth - 1) > maxDepth;
1439
+ }
1440
+
1441
+ _getListDepth(list) {
1442
+ if (!list) {
1443
+ return 1;
1444
+ }
1445
+
1446
+ let depth = 1;
1447
+ let currentList = list;
1448
+
1449
+ while (currentList && currentList !== this._rootList) {
1450
+ const parentItem = currentList.closest(this._params.itemselector);
1451
+ if (!parentItem) {
1452
+ break;
1453
+ }
1454
+ depth += 1;
1455
+ currentList = parentItem.parentElement?.closest(this._params.listselector);
1456
+ }
1457
+
1458
+ return depth;
1459
+ }
1460
+
1461
+ _getItemSubtreeDepth(item) {
1462
+ if (!item) {
1463
+ return 1;
1464
+ }
1465
+
1466
+ const childList = this._getDirectChildBySelector(item, this._params.listselector);
1467
+ if (!childList) {
1468
+ return 1;
1469
+ }
1470
+
1471
+ const childItems = Array.from(childList.children).filter((child) => child.matches(this._params.itemselector));
1472
+ if (!childItems.length) {
1473
+ return 1;
1474
+ }
1475
+
1476
+ const maxChildDepth = childItems.reduce(
1477
+ (depth, childItem) => Math.max(depth, this._getItemSubtreeDepth(childItem)),
1478
+ 1
1479
+ );
1480
+
1481
+ return maxChildDepth + 1;
1482
+ }
1483
+
1484
+ _cleanupEmptyLists() {
1485
+ if (!this._rootList) {
1486
+ return;
1487
+ }
1488
+
1489
+ const lists = Array.from(this._rootList.querySelectorAll(this._params.listselector));
1490
+ lists.forEach((list) => {
1491
+ if (list === this._rootList) {
1492
+ return;
1493
+ }
1494
+
1495
+ const hasItems = Array.from(list.children).some((child) => child.matches(this._params.itemselector));
1496
+ if (!hasItems) {
1497
+ list.remove();
1498
+ }
1499
+ });
1500
+ }
1501
+
1502
+ _createPlaceholder(item) {
1503
+ const tag = item.tagName && item.tagName.toLowerCase() === "li" ? "li" : "div";
1504
+ const placeholder = document.createElement(tag);
1505
+ placeholder.className = CLASS_PLACEHOLDER;
1506
+ placeholder.style.height = `${Math.max(item.getBoundingClientRect().height, 24)}px`;
1507
+ placeholder.setAttribute("aria-hidden", "true");
1508
+ return placeholder;
1509
+ }
1510
+
1511
+ _placePlaceholder(parent, beforeNode = null) {
1512
+ if (!parent || !this._placeholder) {
1513
+ return;
1514
+ }
1515
+
1516
+ if (beforeNode === this._placeholder) {
1517
+ return;
1518
+ }
1519
+
1520
+ const currentParent = this._placeholder.parentElement;
1521
+ const currentNext = this._placeholder.nextSibling;
1522
+
1523
+ if (currentParent === parent && currentNext === beforeNode) {
1524
+ return;
1525
+ }
1526
+
1527
+ parent.insertBefore(this._placeholder, beforeNode);
1528
+ this._emit("placeholdermove", {
1529
+ parent,
1530
+ beforeNode,
1531
+ item: this._draggedItem
1532
+ });
1533
+ }
1534
+
1535
+ _placePlaceholderWhenExplicitlyNeeded(list, pointerY, targetInstance = this) {
1536
+ if (!list || targetInstance._isDepthExceeded(list, this._draggedItem)) {
1537
+ return;
1538
+ }
1539
+
1540
+ const items = Array.from(list.children).filter((child) => child.matches(this._params.itemselector));
1541
+ if (!items.length) {
1542
+ this._placePlaceholder(list, null);
1543
+ return;
1544
+ }
1545
+
1546
+ const edgeThreshold = 8;
1547
+ const firstItem = items[0];
1548
+ const lastItem = items[items.length - 1];
1549
+ const firstRect = firstItem.getBoundingClientRect();
1550
+ const lastRect = lastItem.getBoundingClientRect();
1551
+
1552
+ if (pointerY <= firstRect.top + edgeThreshold) {
1553
+ this._placePlaceholder(list, firstItem);
1554
+ return;
1555
+ }
1556
+
1557
+ if (pointerY >= lastRect.bottom - edgeThreshold) {
1558
+ this._placePlaceholder(list, null);
1559
+ }
1560
+ }
1561
+
1562
+ _setDropListState(list, denied = false) {
1563
+ if (this._activeDropList && this._activeDropList !== list) {
1564
+ this._activeDropList.classList.remove(CLASS_DROP_TARGET, CLASS_DROP_DENIED);
1565
+ }
1566
+
1567
+ if (!list) {
1568
+ this._activeDropList = null;
1569
+ return;
1570
+ }
1571
+
1572
+ list.classList.add(denied ? CLASS_DROP_DENIED : CLASS_DROP_TARGET);
1573
+ list.classList.remove(denied ? CLASS_DROP_TARGET : CLASS_DROP_DENIED);
1574
+ this._activeDropList = list;
1575
+ }
1576
+
1577
+ _clearDragState() {
1578
+ this._setDropListState(null);
1579
+
1580
+ if (this._draggedItem) {
1581
+ this._draggedItem.classList.remove(CLASS_ITEM_DRAGGING);
1582
+ this._draggedItem.style.display = "";
1583
+ this._draggedItem.removeAttribute("aria-grabbed");
1584
+ }
1585
+
1586
+ if (this._dragElement && this._dragElement.parentElement) {
1587
+ this._dragElement.remove();
1588
+ }
1589
+
1590
+ if (this._dragLayer && this._dragLayer.parentElement) {
1591
+ this._dragLayer.remove();
1592
+ }
1593
+
1594
+ if (typeof document !== "undefined" && document.body) {
1595
+ document.body.style.cursor = this._previousBodyCursor || "";
1596
+ }
1597
+
1598
+ if (this._placeholder && this._placeholder.parentElement) {
1599
+ this._placeholder.remove();
1600
+ }
1601
+
1602
+ this._draggedItem = null;
1603
+ this._placeholder = null;
1604
+ this._dragElement = null;
1605
+ this._dragLayer = null;
1606
+ this._previousBodyCursor = "";
1607
+ this._isDragging = false;
1608
+ this._currentTargetInstance = this;
1609
+ this._lastDropInstance = this;
1610
+ this._activePointerId = null;
1611
+ this._activeTouchId = null;
1612
+ }
1613
+
1614
+ _ensureLiveRegion() {
1615
+ if (!this._element || this._liveRegion) {
1616
+ return;
1617
+ }
1618
+
1619
+ const live = document.createElement("div");
1620
+ live.className = CLASS_LIVE_REGION;
1621
+ live.setAttribute("aria-live", "polite");
1622
+ live.setAttribute("aria-atomic", "true");
1623
+ live.style.position = "absolute";
1624
+ live.style.width = "1px";
1625
+ live.style.height = "1px";
1626
+ live.style.margin = "-1px";
1627
+ live.style.border = "0";
1628
+ live.style.padding = "0";
1629
+ live.style.clip = "rect(0 0 0 0)";
1630
+ live.style.clipPath = "inset(50%)";
1631
+ live.style.overflow = "hidden";
1632
+ live.style.whiteSpace = "nowrap";
1633
+ this._element.append(live);
1634
+ this._liveRegion = live;
1635
+ }
1636
+
1637
+ _announce(message) {
1638
+ if (!message) {
1639
+ return;
1640
+ }
1641
+
1642
+ this._ensureLiveRegion();
1643
+ if (!this._liveRegion) {
1644
+ return;
1645
+ }
1646
+
1647
+ this._liveRegion.textContent = "";
1648
+ requestAnimationFrame(() => {
1649
+ if (this._liveRegion) {
1650
+ this._liveRegion.textContent = message;
1651
+ }
1652
+ });
1653
+ }
1654
+
1655
+ _serializeList(list) {
1656
+ const items = Array.from(list.children).filter((child) => child.matches(this._params.itemselector));
1657
+
1658
+ return items.map((item) => {
1659
+ const id = normalizeData(item.getAttribute(this._params.idattribute));
1660
+ const childList = item.querySelector(`:scope > ${this._params.listselector}`);
1661
+
1662
+ const data = { id: id ?? null };
1663
+
1664
+ if (childList) {
1665
+ const children = this._serializeList(childList);
1666
+ if (children.length) {
1667
+ data.children = children;
1668
+ }
1669
+ }
1670
+
1671
+ return data;
1672
+ });
1673
+ }
1674
+ }
1675
+
1676
+ EventHandler.on(document, `DOMContentLoaded.${NAME_KEY}.data.api`, () => {
1677
+ Selectors.findAll(SELECTOR_DATA_TOGGLE).forEach((el) => VGNestable.getOrCreateInstance(el));
1678
+ });
1679
+
1680
+ export default VGNestable;
1681
+
1682
+
1683
+
1684
+
1685
+
1686
+