@dnd-block-tree/vanilla 2.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1034 @@
1
+ 'use strict';
2
+
3
+ var core = require('@dnd-block-tree/core');
4
+
5
+ // src/index.ts
6
+
7
+ // src/collision-bridge.ts
8
+ function measureDropZoneRects(zones) {
9
+ const rects = /* @__PURE__ */ new Map();
10
+ for (const [id, el] of zones) {
11
+ const domRect = el.getBoundingClientRect();
12
+ rects.set(id, {
13
+ top: domRect.top,
14
+ left: domRect.left,
15
+ width: domRect.width,
16
+ height: domRect.height,
17
+ right: domRect.right,
18
+ bottom: domRect.bottom
19
+ });
20
+ }
21
+ return rects;
22
+ }
23
+ function buildCandidates(snapshotRects) {
24
+ const candidates = [];
25
+ for (const [id, rect] of snapshotRects) {
26
+ candidates.push({ id, rect });
27
+ }
28
+ return candidates;
29
+ }
30
+ function pointerToRect(x, y) {
31
+ return {
32
+ top: y,
33
+ left: x,
34
+ width: 1,
35
+ height: 1,
36
+ right: x + 1,
37
+ bottom: y + 1
38
+ };
39
+ }
40
+ function detectCollision(detector, snapshotRects, pointerX, pointerY) {
41
+ const candidates = buildCandidates(snapshotRects);
42
+ const pointerRect = pointerToRect(pointerX, pointerY);
43
+ const results = detector(candidates, pointerRect);
44
+ return results.length > 0 ? results[0].id : null;
45
+ }
46
+
47
+ // src/drag-overlay.ts
48
+ var DragOverlay = class {
49
+ constructor(options = {}) {
50
+ this.overlay = null;
51
+ this.options = options;
52
+ }
53
+ show(block, sourceEl, x, y) {
54
+ this.hide();
55
+ const overlay = document.createElement("div");
56
+ overlay.setAttribute("data-drag-overlay", "true");
57
+ overlay.style.position = "fixed";
58
+ overlay.style.zIndex = "9999";
59
+ overlay.style.pointerEvents = "none";
60
+ overlay.style.opacity = "0.7";
61
+ overlay.style.backdropFilter = "blur(4px)";
62
+ overlay.style.willChange = "transform";
63
+ if (this.options.renderOverlay) {
64
+ overlay.appendChild(this.options.renderOverlay(block));
65
+ } else {
66
+ const rect = sourceEl.getBoundingClientRect();
67
+ overlay.style.width = `${rect.width}px`;
68
+ const clone = sourceEl.cloneNode(true);
69
+ clone.style.margin = "0";
70
+ overlay.appendChild(clone);
71
+ }
72
+ this.setPosition(overlay, x, y);
73
+ document.body.appendChild(overlay);
74
+ this.overlay = overlay;
75
+ }
76
+ move(x, y) {
77
+ if (this.overlay) {
78
+ this.setPosition(this.overlay, x, y);
79
+ }
80
+ }
81
+ hide() {
82
+ if (this.overlay) {
83
+ this.overlay.remove();
84
+ this.overlay = null;
85
+ }
86
+ }
87
+ setPosition(el, x, y) {
88
+ el.style.transform = `translate(${x}px, ${y}px)`;
89
+ }
90
+ };
91
+
92
+ // src/utils/dom.ts
93
+ function createElement(tag, attrs, children) {
94
+ const el = document.createElement(tag);
95
+ if (attrs) {
96
+ for (const [key, value] of Object.entries(attrs)) {
97
+ el.setAttribute(key, value);
98
+ }
99
+ }
100
+ if (children) {
101
+ for (const child of children) {
102
+ if (typeof child === "string") {
103
+ el.appendChild(document.createTextNode(child));
104
+ } else {
105
+ el.appendChild(child);
106
+ }
107
+ }
108
+ }
109
+ return el;
110
+ }
111
+ function setDataAttributes(el, data) {
112
+ for (const [key, value] of Object.entries(data)) {
113
+ if (value === false) {
114
+ el.removeAttribute(`data-${key}`);
115
+ } else {
116
+ el.setAttribute(`data-${key}`, String(value));
117
+ }
118
+ }
119
+ }
120
+ function closestWithData(el, dataAttr) {
121
+ return el.closest(`[data-${dataAttr}]`);
122
+ }
123
+
124
+ // src/sensors/pointer-sensor.ts
125
+ var PointerSensor = class {
126
+ constructor(callbacks, options = {}) {
127
+ this.container = null;
128
+ this.isDragging = false;
129
+ this.startX = 0;
130
+ this.startY = 0;
131
+ this.blockId = null;
132
+ this.callbacks = callbacks;
133
+ this.activationDistance = options.activationDistance ?? 8;
134
+ this.boundPointerDown = this.onPointerDown.bind(this);
135
+ this.boundPointerMove = this.onPointerMove.bind(this);
136
+ this.boundPointerUp = this.onPointerUp.bind(this);
137
+ }
138
+ attach(container) {
139
+ this.container = container;
140
+ container.addEventListener("pointerdown", this.boundPointerDown);
141
+ }
142
+ detach() {
143
+ this.cleanup();
144
+ this.container?.removeEventListener("pointerdown", this.boundPointerDown);
145
+ this.container = null;
146
+ }
147
+ onPointerDown(e) {
148
+ if (e.button !== 0) return;
149
+ const draggable = closestWithData(e.target, "draggable-id");
150
+ if (!draggable) return;
151
+ const id = draggable.getAttribute("data-draggable-id");
152
+ if (!id) return;
153
+ this.blockId = id;
154
+ this.startX = e.clientX;
155
+ this.startY = e.clientY;
156
+ this.isDragging = false;
157
+ document.addEventListener("pointermove", this.boundPointerMove);
158
+ document.addEventListener("pointerup", this.boundPointerUp);
159
+ }
160
+ onPointerMove(e) {
161
+ const dx = e.clientX - this.startX;
162
+ const dy = e.clientY - this.startY;
163
+ const distance = Math.sqrt(dx * dx + dy * dy);
164
+ if (!this.isDragging) {
165
+ if (distance >= this.activationDistance) {
166
+ this.isDragging = true;
167
+ this.callbacks.onDragStart(this.blockId, this.startX, this.startY);
168
+ }
169
+ return;
170
+ }
171
+ this.callbacks.onDragMove(e.clientX, e.clientY);
172
+ }
173
+ onPointerUp(e) {
174
+ if (this.isDragging) {
175
+ this.callbacks.onDragEnd(e.clientX, e.clientY);
176
+ }
177
+ this.cleanup();
178
+ }
179
+ cleanup() {
180
+ this.isDragging = false;
181
+ this.blockId = null;
182
+ document.removeEventListener("pointermove", this.boundPointerMove);
183
+ document.removeEventListener("pointerup", this.boundPointerUp);
184
+ }
185
+ };
186
+
187
+ // src/utils/haptic.ts
188
+ function triggerHaptic(durationMs = 10) {
189
+ if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
190
+ navigator.vibrate(durationMs);
191
+ }
192
+ }
193
+
194
+ // src/sensors/touch-sensor.ts
195
+ var TouchSensor = class {
196
+ constructor(callbacks, options = {}) {
197
+ this.container = null;
198
+ this.isDragging = false;
199
+ this.pressTimer = null;
200
+ this.blockId = null;
201
+ this.callbacks = callbacks;
202
+ this.longPressDelay = options.longPressDelay ?? 200;
203
+ this.hapticFeedback = options.hapticFeedback ?? true;
204
+ this.boundTouchStart = this.onTouchStart.bind(this);
205
+ this.boundTouchMove = this.onTouchMove.bind(this);
206
+ this.boundTouchEnd = this.onTouchEnd.bind(this);
207
+ }
208
+ attach(container) {
209
+ this.container = container;
210
+ container.addEventListener("touchstart", this.boundTouchStart, { passive: false });
211
+ }
212
+ detach() {
213
+ this.cleanup();
214
+ this.container?.removeEventListener("touchstart", this.boundTouchStart);
215
+ this.container = null;
216
+ }
217
+ onTouchStart(e) {
218
+ if (e.touches.length !== 1) return;
219
+ const draggable = closestWithData(e.target, "draggable-id");
220
+ if (!draggable) return;
221
+ const id = draggable.getAttribute("data-draggable-id");
222
+ if (!id) return;
223
+ this.blockId = id;
224
+ const touch = e.touches[0];
225
+ this.pressTimer = setTimeout(() => {
226
+ this.isDragging = true;
227
+ if (this.hapticFeedback) triggerHaptic();
228
+ this.callbacks.onDragStart(this.blockId, touch.clientX, touch.clientY);
229
+ }, this.longPressDelay);
230
+ document.addEventListener("touchmove", this.boundTouchMove, { passive: false });
231
+ document.addEventListener("touchend", this.boundTouchEnd);
232
+ document.addEventListener("touchcancel", this.boundTouchEnd);
233
+ }
234
+ onTouchMove(e) {
235
+ const touch = e.touches[0];
236
+ if (!this.isDragging) {
237
+ if (this.pressTimer) {
238
+ clearTimeout(this.pressTimer);
239
+ this.pressTimer = null;
240
+ this.cleanup();
241
+ }
242
+ return;
243
+ }
244
+ e.preventDefault();
245
+ this.callbacks.onDragMove(touch.clientX, touch.clientY);
246
+ }
247
+ onTouchEnd(e) {
248
+ if (this.isDragging) {
249
+ const touch = e.changedTouches[0];
250
+ this.callbacks.onDragEnd(touch.clientX, touch.clientY);
251
+ }
252
+ this.cleanup();
253
+ }
254
+ cleanup() {
255
+ if (this.pressTimer) {
256
+ clearTimeout(this.pressTimer);
257
+ this.pressTimer = null;
258
+ }
259
+ this.isDragging = false;
260
+ this.blockId = null;
261
+ document.removeEventListener("touchmove", this.boundTouchMove);
262
+ document.removeEventListener("touchend", this.boundTouchEnd);
263
+ document.removeEventListener("touchcancel", this.boundTouchEnd);
264
+ }
265
+ };
266
+
267
+ // src/sensors/keyboard-sensor.ts
268
+ var KeyboardSensor = class {
269
+ constructor(callbacks) {
270
+ this.container = null;
271
+ this.callbacks = callbacks;
272
+ this.boundKeyDown = this.onKeyDown.bind(this);
273
+ }
274
+ attach(container) {
275
+ this.container = container;
276
+ container.addEventListener("keydown", this.boundKeyDown);
277
+ }
278
+ detach() {
279
+ this.container?.removeEventListener("keydown", this.boundKeyDown);
280
+ this.container = null;
281
+ }
282
+ onKeyDown(e) {
283
+ switch (e.key) {
284
+ case "ArrowUp":
285
+ e.preventDefault();
286
+ this.callbacks.onFocusPrev();
287
+ break;
288
+ case "ArrowDown":
289
+ e.preventDefault();
290
+ this.callbacks.onFocusNext();
291
+ break;
292
+ case "ArrowRight":
293
+ e.preventDefault();
294
+ this.callbacks.onExpand();
295
+ break;
296
+ case "ArrowLeft":
297
+ e.preventDefault();
298
+ this.callbacks.onCollapse();
299
+ break;
300
+ case "Home":
301
+ e.preventDefault();
302
+ this.callbacks.onFocusFirst();
303
+ break;
304
+ case "End":
305
+ e.preventDefault();
306
+ this.callbacks.onFocusLast();
307
+ break;
308
+ case "Enter":
309
+ case " ":
310
+ e.preventDefault();
311
+ this.callbacks.onToggleExpand();
312
+ break;
313
+ case "Escape":
314
+ this.callbacks.onDragCancel();
315
+ break;
316
+ }
317
+ }
318
+ };
319
+ function createBlockHistory(initialBlocks, options = {}) {
320
+ const { maxSteps = 50 } = options;
321
+ let state = {
322
+ past: [],
323
+ present: initialBlocks,
324
+ future: []
325
+ };
326
+ return {
327
+ push(blocks) {
328
+ state = core.historyReducer(state, { type: "SET", payload: blocks, maxSteps });
329
+ },
330
+ undo() {
331
+ if (state.past.length === 0) return null;
332
+ state = core.historyReducer(state, { type: "UNDO" });
333
+ return state.present;
334
+ },
335
+ redo() {
336
+ if (state.future.length === 0) return null;
337
+ state = core.historyReducer(state, { type: "REDO" });
338
+ return state.present;
339
+ },
340
+ canUndo: () => state.past.length > 0,
341
+ canRedo: () => state.future.length > 0,
342
+ getPresent: () => state.present,
343
+ clear(blocks) {
344
+ state = { past: [], present: blocks, future: [] };
345
+ }
346
+ };
347
+ }
348
+
349
+ // src/controller.ts
350
+ function createBlockTreeController(options = {}) {
351
+ const {
352
+ initialBlocks = [],
353
+ containerTypes = [],
354
+ orderingStrategy,
355
+ maxDepth,
356
+ previewDebounce,
357
+ canDrag,
358
+ canDrop,
359
+ idGenerator,
360
+ initialExpanded,
361
+ sensors: sensorConfig,
362
+ onChange,
363
+ callbacks
364
+ } = options;
365
+ const stickyCollision = core.createStickyCollision(15);
366
+ const tree = core.createBlockTree({
367
+ initialBlocks,
368
+ containerTypes,
369
+ orderingStrategy,
370
+ maxDepth,
371
+ previewDebounce,
372
+ canDrag,
373
+ canDrop,
374
+ idGenerator,
375
+ initialExpanded,
376
+ collisionDetection: stickyCollision
377
+ });
378
+ const emitter = new core.EventEmitter();
379
+ let container = null;
380
+ const draggableElements = /* @__PURE__ */ new Map();
381
+ const dropZoneElements = /* @__PURE__ */ new Map();
382
+ let snapshotRects = null;
383
+ const selectedIds = /* @__PURE__ */ new Set();
384
+ let lastSelectedId = null;
385
+ const activeSensors = [];
386
+ const overlay = new DragOverlay();
387
+ let overlayRenderer = null;
388
+ let history = null;
389
+ tree.on("blocks:change", (blocks) => {
390
+ onChange?.(blocks);
391
+ if (history) {
392
+ history.push(blocks);
393
+ }
394
+ emitter.emit("render", blocks, tree.getExpandedMap());
395
+ });
396
+ tree.on("expand:change", () => {
397
+ emitter.emit("render", tree.getBlocks(), tree.getExpandedMap());
398
+ });
399
+ if (callbacks?.onDragStart) {
400
+ tree.on("drag:start", (e) => callbacks.onDragStart(e));
401
+ }
402
+ if (callbacks?.onDragEnd) {
403
+ tree.on("drag:end", (e) => callbacks.onDragEnd(e));
404
+ }
405
+ if (callbacks?.onBlockAdd) {
406
+ tree.on("block:add", (e) => callbacks.onBlockAdd(e));
407
+ }
408
+ if (callbacks?.onBlockDelete) {
409
+ tree.on("block:delete", (e) => callbacks.onBlockDelete(e));
410
+ }
411
+ if (callbacks?.onExpandChange) {
412
+ tree.on("expand:change", (e) => callbacks.onExpandChange(e));
413
+ }
414
+ if (callbacks?.onHoverChange) {
415
+ tree.on("hover:change", (e) => callbacks.onHoverChange(e));
416
+ }
417
+ const sensorCallbacks = {
418
+ onDragStart(blockId, x, y) {
419
+ const draggedIds = selectedIds.has(blockId) && selectedIds.size > 1 ? [...selectedIds] : [blockId];
420
+ const started = tree.startDrag(blockId, draggedIds);
421
+ if (!started) return;
422
+ stickyCollision.reset();
423
+ snapshotRects = measureDropZoneRects(dropZoneElements);
424
+ const block = tree.getBlock(blockId);
425
+ const el = draggableElements.get(blockId);
426
+ if (block && el) {
427
+ if (overlayRenderer) {
428
+ overlay.show(block, el, x, y);
429
+ } else {
430
+ overlay.show(block, el, x, y);
431
+ }
432
+ }
433
+ emitter.emit("drag:statechange", getDragState());
434
+ },
435
+ onDragMove(x, y) {
436
+ if (!snapshotRects) return;
437
+ overlay.move(x, y);
438
+ const detector = tree.getCollisionDetection();
439
+ if (!detector) return;
440
+ const targetZone = detectCollision(detector, snapshotRects, x, y);
441
+ if (targetZone) {
442
+ tree.updateDrag(targetZone);
443
+ }
444
+ },
445
+ onDragEnd(_x, _y) {
446
+ const result = tree.endDrag();
447
+ overlay.hide();
448
+ snapshotRects = null;
449
+ if (result && callbacks?.onBlockMove) {
450
+ callbacks.onBlockMove({
451
+ block: tree.getBlock(tree.getBlocks()[0]?.id),
452
+ from: { parentId: null, index: 0 },
453
+ to: { parentId: null, index: 0 },
454
+ blocks: result.blocks,
455
+ movedIds: []
456
+ });
457
+ }
458
+ emitter.emit("drag:statechange", getDragState());
459
+ emitter.emit("render", tree.getBlocks(), tree.getExpandedMap());
460
+ },
461
+ onDragCancel() {
462
+ tree.cancelDrag();
463
+ overlay.hide();
464
+ snapshotRects = null;
465
+ emitter.emit("drag:statechange", getDragState());
466
+ emitter.emit("render", tree.getBlocks(), tree.getExpandedMap());
467
+ }
468
+ };
469
+ function getDragState() {
470
+ return {
471
+ isDragging: tree.getActiveId() !== null,
472
+ activeId: tree.getActiveId(),
473
+ hoverZone: tree.getHoverZone()
474
+ };
475
+ }
476
+ function setupSensors() {
477
+ const config = sensorConfig ?? {};
478
+ if (!container) return;
479
+ if (config.pointer !== false) {
480
+ const pointer = new PointerSensor(sensorCallbacks, {
481
+ activationDistance: config.activationDistance
482
+ });
483
+ pointer.attach(container);
484
+ activeSensors.push(pointer);
485
+ }
486
+ if (config.touch !== false) {
487
+ const touch = new TouchSensor(sensorCallbacks, {
488
+ longPressDelay: config.longPressDelay,
489
+ hapticFeedback: config.hapticFeedback
490
+ });
491
+ touch.attach(container);
492
+ activeSensors.push(touch);
493
+ }
494
+ if (config.keyboard) {
495
+ const visibleBlocks = () => tree.getEffectiveBlocks();
496
+ let focusedIndex = -1;
497
+ const keyboard = new KeyboardSensor({
498
+ ...sensorCallbacks,
499
+ onFocusPrev() {
500
+ const blocks = visibleBlocks();
501
+ focusedIndex = Math.max(0, focusedIndex - 1);
502
+ const block = blocks[focusedIndex];
503
+ if (block) focusDraggable(block.id);
504
+ },
505
+ onFocusNext() {
506
+ const blocks = visibleBlocks();
507
+ focusedIndex = Math.min(blocks.length - 1, focusedIndex + 1);
508
+ const block = blocks[focusedIndex];
509
+ if (block) focusDraggable(block.id);
510
+ },
511
+ onExpand() {
512
+ const blocks = visibleBlocks();
513
+ const block = blocks[focusedIndex];
514
+ if (block && containerTypes.includes(block.type) && !tree.isExpanded(block.id)) {
515
+ tree.toggleExpand(block.id);
516
+ }
517
+ },
518
+ onCollapse() {
519
+ const blocks = visibleBlocks();
520
+ const block = blocks[focusedIndex];
521
+ if (block && containerTypes.includes(block.type) && tree.isExpanded(block.id)) {
522
+ tree.toggleExpand(block.id);
523
+ }
524
+ },
525
+ onFocusFirst() {
526
+ focusedIndex = 0;
527
+ const blocks = visibleBlocks();
528
+ if (blocks[0]) focusDraggable(blocks[0].id);
529
+ },
530
+ onFocusLast() {
531
+ const blocks = visibleBlocks();
532
+ focusedIndex = blocks.length - 1;
533
+ if (blocks[focusedIndex]) focusDraggable(blocks[focusedIndex].id);
534
+ },
535
+ onToggleExpand() {
536
+ const blocks = visibleBlocks();
537
+ const block = blocks[focusedIndex];
538
+ if (block && containerTypes.includes(block.type)) {
539
+ tree.toggleExpand(block.id);
540
+ }
541
+ },
542
+ onSelect() {
543
+ const blocks = visibleBlocks();
544
+ const block = blocks[focusedIndex];
545
+ if (block) {
546
+ controller.select(block.id, "toggle");
547
+ }
548
+ }
549
+ });
550
+ keyboard.attach(container);
551
+ activeSensors.push(keyboard);
552
+ }
553
+ }
554
+ function focusDraggable(id) {
555
+ const el = draggableElements.get(id);
556
+ if (el) el.focus();
557
+ }
558
+ function teardownSensors() {
559
+ for (const sensor of activeSensors) {
560
+ sensor.detach();
561
+ }
562
+ activeSensors.length = 0;
563
+ }
564
+ const controller = {
565
+ mount(el) {
566
+ container = el;
567
+ setupSensors();
568
+ emitter.emit("render", tree.getBlocks(), tree.getExpandedMap());
569
+ },
570
+ unmount() {
571
+ teardownSensors();
572
+ overlay.hide();
573
+ container = null;
574
+ },
575
+ registerDraggable(id, element) {
576
+ element.setAttribute("data-draggable-id", id);
577
+ element.setAttribute("data-block-id", id);
578
+ draggableElements.set(id, element);
579
+ return () => {
580
+ draggableElements.delete(id);
581
+ };
582
+ },
583
+ registerDropZone(id, element) {
584
+ element.setAttribute("data-zone-id", id);
585
+ dropZoneElements.set(id, element);
586
+ return () => {
587
+ dropZoneElements.delete(id);
588
+ };
589
+ },
590
+ getDragState,
591
+ getBlocks: () => tree.getBlocks(),
592
+ getEffectiveBlocks: () => tree.getEffectiveBlocks(),
593
+ getExpandedMap: () => tree.getExpandedMap(),
594
+ getBlock: (id) => tree.getBlock(id),
595
+ toggleExpand(id) {
596
+ tree.toggleExpand(id);
597
+ },
598
+ setExpandAll(expanded) {
599
+ tree.setExpandAll(expanded);
600
+ },
601
+ addBlock(type, parentId = null) {
602
+ return tree.addBlock(type, parentId);
603
+ },
604
+ deleteBlock(id) {
605
+ tree.deleteBlock(id);
606
+ },
607
+ setBlocks(blocks) {
608
+ tree.setBlocks(blocks);
609
+ },
610
+ select(id, mode) {
611
+ if (mode === "single") {
612
+ selectedIds.clear();
613
+ selectedIds.add(id);
614
+ } else if (mode === "toggle") {
615
+ if (selectedIds.has(id)) {
616
+ selectedIds.delete(id);
617
+ } else {
618
+ selectedIds.add(id);
619
+ }
620
+ } else if (mode === "range" && lastSelectedId) {
621
+ const blocks = tree.getBlocks();
622
+ const startIdx = blocks.findIndex((b) => b.id === lastSelectedId);
623
+ const endIdx = blocks.findIndex((b) => b.id === id);
624
+ if (startIdx !== -1 && endIdx !== -1) {
625
+ const [from, to] = startIdx < endIdx ? [startIdx, endIdx] : [endIdx, startIdx];
626
+ for (let i = from; i <= to; i++) {
627
+ selectedIds.add(blocks[i].id);
628
+ }
629
+ }
630
+ }
631
+ lastSelectedId = id;
632
+ emitter.emit("selection:change", new Set(selectedIds));
633
+ },
634
+ clearSelection() {
635
+ selectedIds.clear();
636
+ lastSelectedId = null;
637
+ emitter.emit("selection:change", /* @__PURE__ */ new Set());
638
+ },
639
+ getSelectedIds: () => new Set(selectedIds),
640
+ enableHistory(maxSteps = 50) {
641
+ history = createBlockHistory(tree.getBlocks(), { maxSteps });
642
+ },
643
+ undo() {
644
+ if (!history) return null;
645
+ const blocks = history.undo();
646
+ if (blocks) tree.setBlocks(blocks);
647
+ return blocks;
648
+ },
649
+ redo() {
650
+ if (!history) return null;
651
+ const blocks = history.redo();
652
+ if (blocks) tree.setBlocks(blocks);
653
+ return blocks;
654
+ },
655
+ canUndo: () => history?.canUndo() ?? false,
656
+ canRedo: () => history?.canRedo() ?? false,
657
+ on(event, handler) {
658
+ return emitter.on(event, handler);
659
+ },
660
+ setOverlayRenderer(render) {
661
+ overlayRenderer = render;
662
+ },
663
+ getTree: () => tree,
664
+ destroy() {
665
+ teardownSensors();
666
+ overlay.hide();
667
+ tree.destroy();
668
+ emitter.removeAllListeners();
669
+ draggableElements.clear();
670
+ dropZoneElements.clear();
671
+ selectedIds.clear();
672
+ }
673
+ };
674
+ return controller;
675
+ }
676
+
677
+ // src/layout-animation.ts
678
+ var LayoutAnimation = class {
679
+ constructor(options = {}) {
680
+ this.snapshots = /* @__PURE__ */ new Map();
681
+ this.duration = options.duration ?? 200;
682
+ this.easing = options.easing ?? "ease";
683
+ }
684
+ /** Record current positions of all block elements in a container */
685
+ snapshot(container) {
686
+ this.snapshots.clear();
687
+ const blocks = container.querySelectorAll("[data-block-id]");
688
+ for (const el of blocks) {
689
+ const id = el.getAttribute("data-block-id");
690
+ if (id) {
691
+ this.snapshots.set(id, el.getBoundingClientRect());
692
+ }
693
+ }
694
+ }
695
+ /** After DOM update, animate elements from old to new positions */
696
+ animate(container) {
697
+ if (this.snapshots.size === 0) return;
698
+ const blocks = container.querySelectorAll("[data-block-id]");
699
+ for (const el of blocks) {
700
+ const id = el.getAttribute("data-block-id");
701
+ if (!id) continue;
702
+ const oldRect = this.snapshots.get(id);
703
+ if (!oldRect) continue;
704
+ const newRect = el.getBoundingClientRect();
705
+ const deltaX = oldRect.left - newRect.left;
706
+ const deltaY = oldRect.top - newRect.top;
707
+ if (Math.abs(deltaX) < 1 && Math.abs(deltaY) < 1) continue;
708
+ el.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
709
+ el.style.transition = "none";
710
+ requestAnimationFrame(() => {
711
+ el.style.transition = `transform ${this.duration}ms ${this.easing}`;
712
+ el.style.transform = "";
713
+ const onEnd = () => {
714
+ el.style.transition = "";
715
+ el.removeEventListener("transitionend", onEnd);
716
+ };
717
+ el.addEventListener("transitionend", onEnd, { once: true });
718
+ });
719
+ }
720
+ this.snapshots.clear();
721
+ }
722
+ };
723
+
724
+ // src/virtual-scroller.ts
725
+ var VirtualScroller = class {
726
+ constructor(options) {
727
+ this.itemHeight = options.itemHeight;
728
+ this.overscan = options.overscan ?? 5;
729
+ }
730
+ /** Calculate the visible range for a given scroll state */
731
+ calculate(scrollTop, viewportHeight, totalItems, blockIds) {
732
+ const startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.overscan);
733
+ const visibleCount = Math.ceil(viewportHeight / this.itemHeight) + 2 * this.overscan;
734
+ const endIndex = Math.min(totalItems, startIndex + visibleCount);
735
+ const visibleIds = /* @__PURE__ */ new Set();
736
+ for (let i = startIndex; i < endIndex && i < blockIds.length; i++) {
737
+ visibleIds.add(blockIds[i]);
738
+ }
739
+ return {
740
+ startIndex,
741
+ endIndex,
742
+ offsetY: startIndex * this.itemHeight,
743
+ totalHeight: totalItems * this.itemHeight,
744
+ visibleIds
745
+ };
746
+ }
747
+ };
748
+
749
+ // src/renderer/drop-zone.ts
750
+ function createDropZoneElement(options) {
751
+ const { id, height = 4 } = options;
752
+ const el = createElement("div");
753
+ setDataAttributes(el, {
754
+ "zone-id": id
755
+ });
756
+ el.style.height = `${height}px`;
757
+ el.style.minHeight = `${height}px`;
758
+ el.style.transition = "background-color 150ms ease";
759
+ return el;
760
+ }
761
+ function setDropZoneActive(el, active) {
762
+ setDataAttributes(el, { "zone-active": active });
763
+ }
764
+
765
+ // src/renderer/tree-renderer.ts
766
+ function renderTree(blocks, expandedMap, controller, options, parentId = null, depth = 0) {
767
+ const { renderBlock, containerTypes, dropZoneHeight } = options;
768
+ const container = createElement("div", {
769
+ role: parentId === null ? "tree" : "group"
770
+ });
771
+ if (parentId === null) {
772
+ container.setAttribute("data-dnd-tree-root", "true");
773
+ }
774
+ const children = blocks.filter((b) => b.parentId === parentId);
775
+ const activeId = controller.getDragState().activeId;
776
+ const selectedIds = controller.getSelectedIds();
777
+ const index = controller.getTree().getBlockIndex();
778
+ if (parentId !== null) {
779
+ const startZone = createDropZoneElement({ id: `into-${parentId}`, height: dropZoneHeight });
780
+ controller.registerDropZone(`into-${parentId}`, startZone);
781
+ container.appendChild(startZone);
782
+ } else {
783
+ const rootStart = createDropZoneElement({ id: "root-start", height: dropZoneHeight });
784
+ controller.registerDropZone("root-start", rootStart);
785
+ container.appendChild(rootStart);
786
+ }
787
+ for (const block of children) {
788
+ if (block.id === activeId) continue;
789
+ const beforeZone = createDropZoneElement({ id: `before-${block.id}`, height: dropZoneHeight });
790
+ controller.registerDropZone(`before-${block.id}`, beforeZone);
791
+ container.appendChild(beforeZone);
792
+ const isContainer = containerTypes.includes(block.type);
793
+ const isExpanded = expandedMap[block.id] !== false;
794
+ const blockDepth = core.getBlockDepth(index, block.id);
795
+ let childrenEl = null;
796
+ if (isContainer && isExpanded) {
797
+ childrenEl = renderTree(blocks, expandedMap, controller, options, block.id, depth + 1);
798
+ }
799
+ const ctx = {
800
+ children: childrenEl,
801
+ depth: blockDepth,
802
+ isExpanded,
803
+ isDragging: block.id === activeId,
804
+ isSelected: selectedIds.has(block.id),
805
+ onToggleExpand: isContainer ? () => controller.toggleExpand(block.id) : null
806
+ };
807
+ const blockEl = renderBlock(block, ctx);
808
+ setDataAttributes(blockEl, {
809
+ "block-id": block.id,
810
+ "block-type": block.type,
811
+ depth: String(blockDepth),
812
+ dragging: block.id === activeId,
813
+ selected: selectedIds.has(block.id)
814
+ });
815
+ controller.registerDraggable(block.id, blockEl);
816
+ blockEl.setAttribute("role", "treeitem");
817
+ blockEl.setAttribute("aria-level", String(blockDepth + 1));
818
+ if (isContainer) {
819
+ blockEl.setAttribute("aria-expanded", String(isExpanded));
820
+ }
821
+ blockEl.setAttribute("tabindex", "-1");
822
+ container.appendChild(blockEl);
823
+ }
824
+ if (parentId !== null) {
825
+ const endZone = createDropZoneElement({ id: `end-${parentId}`, height: dropZoneHeight });
826
+ controller.registerDropZone(`end-${parentId}`, endZone);
827
+ container.appendChild(endZone);
828
+ } else {
829
+ const rootEnd = createDropZoneElement({ id: "root-end", height: dropZoneHeight });
830
+ controller.registerDropZone("root-end", rootEnd);
831
+ container.appendChild(rootEnd);
832
+ }
833
+ return container;
834
+ }
835
+
836
+ // src/renderer/default-renderer.ts
837
+ function createDefaultRenderer(controller, options) {
838
+ const { container, renderBlock, dropZoneHeight, animateExpand } = options;
839
+ const containerTypes = controller.getTree().containerTypes ?? [];
840
+ function render(blocks, expandedMap) {
841
+ container.innerHTML = "";
842
+ const tree = renderTree(blocks, expandedMap, controller, {
843
+ renderBlock,
844
+ containerTypes,
845
+ dropZoneHeight
846
+ });
847
+ container.appendChild(tree);
848
+ }
849
+ const unsubRender = controller.on("render", render);
850
+ render(controller.getBlocks(), controller.getExpandedMap());
851
+ const renderer = () => {
852
+ unsubRender();
853
+ };
854
+ renderer.refresh = () => {
855
+ render(controller.getBlocks(), controller.getExpandedMap());
856
+ };
857
+ return renderer;
858
+ }
859
+
860
+ // src/renderer/ghost-preview.ts
861
+ function createGhostPreview(sourceEl) {
862
+ const ghost = sourceEl.cloneNode(true);
863
+ setDataAttributes(ghost, { "dnd-ghost": "true" });
864
+ ghost.style.opacity = "0.3";
865
+ ghost.style.pointerEvents = "none";
866
+ ghost.removeAttribute("data-draggable-id");
867
+ ghost.removeAttribute("data-block-id");
868
+ return ghost;
869
+ }
870
+
871
+ // src/utils/disposable.ts
872
+ function createDisposable() {
873
+ const cleanups = [];
874
+ return {
875
+ add(fn) {
876
+ cleanups.push(fn);
877
+ },
878
+ dispose() {
879
+ for (const fn of cleanups.reverse()) {
880
+ fn();
881
+ }
882
+ cleanups.length = 0;
883
+ }
884
+ };
885
+ }
886
+
887
+ Object.defineProperty(exports, "EventEmitter", {
888
+ enumerable: true,
889
+ get: function () { return core.EventEmitter; }
890
+ });
891
+ Object.defineProperty(exports, "blockReducer", {
892
+ enumerable: true,
893
+ get: function () { return core.blockReducer; }
894
+ });
895
+ Object.defineProperty(exports, "buildOrderedBlocks", {
896
+ enumerable: true,
897
+ get: function () { return core.buildOrderedBlocks; }
898
+ });
899
+ Object.defineProperty(exports, "cloneMap", {
900
+ enumerable: true,
901
+ get: function () { return core.cloneMap; }
902
+ });
903
+ Object.defineProperty(exports, "cloneParentMap", {
904
+ enumerable: true,
905
+ get: function () { return core.cloneParentMap; }
906
+ });
907
+ Object.defineProperty(exports, "closestCenterCollision", {
908
+ enumerable: true,
909
+ get: function () { return core.closestCenterCollision; }
910
+ });
911
+ Object.defineProperty(exports, "compareFractionalKeys", {
912
+ enumerable: true,
913
+ get: function () { return core.compareFractionalKeys; }
914
+ });
915
+ Object.defineProperty(exports, "computeNormalizedIndex", {
916
+ enumerable: true,
917
+ get: function () { return core.computeNormalizedIndex; }
918
+ });
919
+ Object.defineProperty(exports, "createBlockTree", {
920
+ enumerable: true,
921
+ get: function () { return core.createBlockTree; }
922
+ });
923
+ Object.defineProperty(exports, "createStickyCollision", {
924
+ enumerable: true,
925
+ get: function () { return core.createStickyCollision; }
926
+ });
927
+ Object.defineProperty(exports, "debounce", {
928
+ enumerable: true,
929
+ get: function () { return core.debounce; }
930
+ });
931
+ Object.defineProperty(exports, "deleteBlockAndDescendants", {
932
+ enumerable: true,
933
+ get: function () { return core.deleteBlockAndDescendants; }
934
+ });
935
+ Object.defineProperty(exports, "expandReducer", {
936
+ enumerable: true,
937
+ get: function () { return core.expandReducer; }
938
+ });
939
+ Object.defineProperty(exports, "extractBlockId", {
940
+ enumerable: true,
941
+ get: function () { return core.extractBlockId; }
942
+ });
943
+ Object.defineProperty(exports, "extractUUID", {
944
+ enumerable: true,
945
+ get: function () { return core.extractUUID; }
946
+ });
947
+ Object.defineProperty(exports, "flatToNested", {
948
+ enumerable: true,
949
+ get: function () { return core.flatToNested; }
950
+ });
951
+ Object.defineProperty(exports, "generateId", {
952
+ enumerable: true,
953
+ get: function () { return core.generateId; }
954
+ });
955
+ Object.defineProperty(exports, "generateInitialKeys", {
956
+ enumerable: true,
957
+ get: function () { return core.generateInitialKeys; }
958
+ });
959
+ Object.defineProperty(exports, "generateKeyBetween", {
960
+ enumerable: true,
961
+ get: function () { return core.generateKeyBetween; }
962
+ });
963
+ Object.defineProperty(exports, "generateNKeysBetween", {
964
+ enumerable: true,
965
+ get: function () { return core.generateNKeysBetween; }
966
+ });
967
+ Object.defineProperty(exports, "getBlockDepth", {
968
+ enumerable: true,
969
+ get: function () { return core.getBlockDepth; }
970
+ });
971
+ Object.defineProperty(exports, "getDescendantIds", {
972
+ enumerable: true,
973
+ get: function () { return core.getDescendantIds; }
974
+ });
975
+ Object.defineProperty(exports, "getDropZoneType", {
976
+ enumerable: true,
977
+ get: function () { return core.getDropZoneType; }
978
+ });
979
+ Object.defineProperty(exports, "getSubtreeDepth", {
980
+ enumerable: true,
981
+ get: function () { return core.getSubtreeDepth; }
982
+ });
983
+ Object.defineProperty(exports, "historyReducer", {
984
+ enumerable: true,
985
+ get: function () { return core.historyReducer; }
986
+ });
987
+ Object.defineProperty(exports, "initFractionalOrder", {
988
+ enumerable: true,
989
+ get: function () { return core.initFractionalOrder; }
990
+ });
991
+ Object.defineProperty(exports, "nestedToFlat", {
992
+ enumerable: true,
993
+ get: function () { return core.nestedToFlat; }
994
+ });
995
+ Object.defineProperty(exports, "reparentBlockIndex", {
996
+ enumerable: true,
997
+ get: function () { return core.reparentBlockIndex; }
998
+ });
999
+ Object.defineProperty(exports, "reparentMultipleBlocks", {
1000
+ enumerable: true,
1001
+ get: function () { return core.reparentMultipleBlocks; }
1002
+ });
1003
+ Object.defineProperty(exports, "validateBlockTree", {
1004
+ enumerable: true,
1005
+ get: function () { return core.validateBlockTree; }
1006
+ });
1007
+ Object.defineProperty(exports, "weightedVerticalCollision", {
1008
+ enumerable: true,
1009
+ get: function () { return core.weightedVerticalCollision; }
1010
+ });
1011
+ exports.DragOverlay = DragOverlay;
1012
+ exports.KeyboardSensor = KeyboardSensor;
1013
+ exports.LayoutAnimation = LayoutAnimation;
1014
+ exports.PointerSensor = PointerSensor;
1015
+ exports.TouchSensor = TouchSensor;
1016
+ exports.VirtualScroller = VirtualScroller;
1017
+ exports.buildCandidates = buildCandidates;
1018
+ exports.closestWithData = closestWithData;
1019
+ exports.createBlockHistory = createBlockHistory;
1020
+ exports.createBlockTreeController = createBlockTreeController;
1021
+ exports.createDefaultRenderer = createDefaultRenderer;
1022
+ exports.createDisposable = createDisposable;
1023
+ exports.createDropZoneElement = createDropZoneElement;
1024
+ exports.createElement = createElement;
1025
+ exports.createGhostPreview = createGhostPreview;
1026
+ exports.detectCollision = detectCollision;
1027
+ exports.measureDropZoneRects = measureDropZoneRects;
1028
+ exports.pointerToRect = pointerToRect;
1029
+ exports.renderTree = renderTree;
1030
+ exports.setDataAttributes = setDataAttributes;
1031
+ exports.setDropZoneActive = setDropZoneActive;
1032
+ exports.triggerHaptic = triggerHaptic;
1033
+ //# sourceMappingURL=index.js.map
1034
+ //# sourceMappingURL=index.js.map