dnd-block-tree 0.1.0 → 0.2.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/README.md +167 -0
- package/dist/index.d.mts +198 -34
- package/dist/index.d.ts +198 -34
- package/dist/index.js +412 -118
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +413 -120
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -3
package/dist/index.js
CHANGED
|
@@ -15,11 +15,8 @@ function extractBlockId(zoneId) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// src/core/collision.ts
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
collisionRect
|
|
21
|
-
}) => {
|
|
22
|
-
if (!collisionRect) return [];
|
|
18
|
+
function computeCollisionScores(droppableContainers, collisionRect) {
|
|
19
|
+
const pointerX = collisionRect.left + collisionRect.width / 2;
|
|
23
20
|
const pointerY = collisionRect.top + collisionRect.height / 2;
|
|
24
21
|
const candidates = droppableContainers.map((container) => {
|
|
25
22
|
const rect = container.rect.current;
|
|
@@ -29,11 +26,19 @@ var weightedVerticalCollision = ({
|
|
|
29
26
|
const edgeDistance = Math.min(distanceToTop, distanceToBottom);
|
|
30
27
|
const isBelowCenter = pointerY > rect.top + rect.height / 2;
|
|
31
28
|
const bias = isBelowCenter ? -5 : 0;
|
|
29
|
+
const isWithinX = pointerX >= rect.left && pointerX <= rect.right;
|
|
30
|
+
let horizontalScore = 0;
|
|
31
|
+
if (isWithinX) {
|
|
32
|
+
horizontalScore = -rect.left * 0.1;
|
|
33
|
+
} else {
|
|
34
|
+
const distanceToZone = pointerX < rect.left ? rect.left - pointerX : pointerX - rect.right;
|
|
35
|
+
horizontalScore = Math.min(distanceToZone, 50);
|
|
36
|
+
}
|
|
32
37
|
return {
|
|
33
38
|
id: container.id,
|
|
34
39
|
data: {
|
|
35
40
|
droppableContainer: container,
|
|
36
|
-
value: edgeDistance + bias
|
|
41
|
+
value: edgeDistance + bias + horizontalScore
|
|
37
42
|
}
|
|
38
43
|
};
|
|
39
44
|
}).filter((c) => c !== null);
|
|
@@ -42,8 +47,44 @@ var weightedVerticalCollision = ({
|
|
|
42
47
|
const bValue = b.data.value;
|
|
43
48
|
return aValue - bValue;
|
|
44
49
|
});
|
|
50
|
+
return candidates;
|
|
51
|
+
}
|
|
52
|
+
var weightedVerticalCollision = ({
|
|
53
|
+
droppableContainers,
|
|
54
|
+
collisionRect
|
|
55
|
+
}) => {
|
|
56
|
+
if (!collisionRect) return [];
|
|
57
|
+
const candidates = computeCollisionScores(droppableContainers, collisionRect);
|
|
45
58
|
return candidates.slice(0, 1);
|
|
46
59
|
};
|
|
60
|
+
function createStickyCollision(threshold = 15) {
|
|
61
|
+
let currentZoneId = null;
|
|
62
|
+
const detector = ({
|
|
63
|
+
droppableContainers,
|
|
64
|
+
collisionRect
|
|
65
|
+
}) => {
|
|
66
|
+
if (!collisionRect) return [];
|
|
67
|
+
const candidates = computeCollisionScores(droppableContainers, collisionRect);
|
|
68
|
+
if (candidates.length === 0) return [];
|
|
69
|
+
const bestCandidate = candidates[0];
|
|
70
|
+
const bestScore = bestCandidate.data.value;
|
|
71
|
+
if (currentZoneId !== null) {
|
|
72
|
+
const currentCandidate = candidates.find((c) => c.id === currentZoneId);
|
|
73
|
+
if (currentCandidate) {
|
|
74
|
+
const currentScore = currentCandidate.data.value;
|
|
75
|
+
if (currentScore - bestScore < threshold) {
|
|
76
|
+
return [currentCandidate];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
currentZoneId = bestCandidate.id;
|
|
81
|
+
return [bestCandidate];
|
|
82
|
+
};
|
|
83
|
+
detector.reset = () => {
|
|
84
|
+
currentZoneId = null;
|
|
85
|
+
};
|
|
86
|
+
return detector;
|
|
87
|
+
}
|
|
47
88
|
var closestCenterCollision = ({
|
|
48
89
|
droppableContainers,
|
|
49
90
|
collisionRect
|
|
@@ -76,38 +117,57 @@ var closestCenterCollision = ({
|
|
|
76
117
|
};
|
|
77
118
|
var DEFAULT_ACTIVATION_DISTANCE = 8;
|
|
78
119
|
function useConfiguredSensors(config = {}) {
|
|
79
|
-
const {
|
|
120
|
+
const {
|
|
121
|
+
activationDistance = DEFAULT_ACTIVATION_DISTANCE,
|
|
122
|
+
activationDelay,
|
|
123
|
+
tolerance
|
|
124
|
+
} = config;
|
|
125
|
+
let activationConstraint;
|
|
126
|
+
if (activationDelay !== void 0) {
|
|
127
|
+
activationConstraint = {
|
|
128
|
+
delay: activationDelay,
|
|
129
|
+
tolerance: tolerance ?? 5
|
|
130
|
+
};
|
|
131
|
+
} else {
|
|
132
|
+
activationConstraint = {
|
|
133
|
+
distance: activationDistance
|
|
134
|
+
};
|
|
135
|
+
}
|
|
80
136
|
return core.useSensors(
|
|
81
137
|
core.useSensor(core.PointerSensor, {
|
|
82
|
-
activationConstraint
|
|
83
|
-
distance: activationDistance
|
|
84
|
-
}
|
|
138
|
+
activationConstraint
|
|
85
139
|
}),
|
|
86
140
|
core.useSensor(core.TouchSensor, {
|
|
87
|
-
activationConstraint
|
|
88
|
-
distance: activationDistance
|
|
89
|
-
}
|
|
141
|
+
activationConstraint
|
|
90
142
|
}),
|
|
91
143
|
core.useSensor(core.KeyboardSensor)
|
|
92
144
|
);
|
|
93
145
|
}
|
|
94
|
-
function getSensorConfig(
|
|
146
|
+
function getSensorConfig(config = {}) {
|
|
147
|
+
const {
|
|
148
|
+
activationDistance = DEFAULT_ACTIVATION_DISTANCE,
|
|
149
|
+
activationDelay,
|
|
150
|
+
tolerance
|
|
151
|
+
} = config;
|
|
152
|
+
let activationConstraint;
|
|
153
|
+
if (activationDelay !== void 0) {
|
|
154
|
+
activationConstraint = {
|
|
155
|
+
delay: activationDelay,
|
|
156
|
+
tolerance: tolerance ?? 5
|
|
157
|
+
};
|
|
158
|
+
} else {
|
|
159
|
+
activationConstraint = {
|
|
160
|
+
distance: activationDistance
|
|
161
|
+
};
|
|
162
|
+
}
|
|
95
163
|
return {
|
|
96
|
-
pointer: {
|
|
97
|
-
|
|
98
|
-
distance: activationDistance
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
touch: {
|
|
102
|
-
activationConstraint: {
|
|
103
|
-
distance: activationDistance
|
|
104
|
-
}
|
|
105
|
-
}
|
|
164
|
+
pointer: { activationConstraint },
|
|
165
|
+
touch: { activationConstraint }
|
|
106
166
|
};
|
|
107
167
|
}
|
|
108
168
|
|
|
109
169
|
// src/utils/helper.ts
|
|
110
|
-
function extractUUID(id, pattern = "^(before|after|into)-") {
|
|
170
|
+
function extractUUID(id, pattern = "^(before|after|into|end)-") {
|
|
111
171
|
const regex = new RegExp(pattern);
|
|
112
172
|
return id.replace(regex, "");
|
|
113
173
|
}
|
|
@@ -147,8 +207,10 @@ function DropZoneComponent({
|
|
|
147
207
|
react.useEffect(() => {
|
|
148
208
|
if (isOver) handleInternalHover();
|
|
149
209
|
}, [isOver, handleInternalHover]);
|
|
150
|
-
|
|
151
|
-
|
|
210
|
+
const zoneBlockId = extractUUID(id);
|
|
211
|
+
const isIntoZone = id.startsWith("into-");
|
|
212
|
+
if (isIntoZone && active?.id && zoneBlockId === String(active.id)) return null;
|
|
213
|
+
if (isIntoZone && activeId && zoneBlockId === activeId) return null;
|
|
152
214
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
153
215
|
"div",
|
|
154
216
|
{
|
|
@@ -163,10 +225,12 @@ function DropZoneComponent({
|
|
|
163
225
|
var DropZone = react.memo(DropZoneComponent);
|
|
164
226
|
function DraggableBlock({
|
|
165
227
|
block,
|
|
166
|
-
children
|
|
228
|
+
children,
|
|
229
|
+
disabled
|
|
167
230
|
}) {
|
|
168
231
|
const { attributes, listeners, setNodeRef, isDragging } = core.useDraggable({
|
|
169
|
-
id: block.id
|
|
232
|
+
id: block.id,
|
|
233
|
+
disabled
|
|
170
234
|
});
|
|
171
235
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: setNodeRef, ...attributes, ...listeners, children: children({ isDragging }) });
|
|
172
236
|
}
|
|
@@ -184,47 +248,49 @@ function TreeRenderer({
|
|
|
184
248
|
dropZoneClassName,
|
|
185
249
|
dropZoneActiveClassName,
|
|
186
250
|
indentClassName = "ml-6 border-l border-gray-200 pl-4",
|
|
187
|
-
rootClassName = "flex flex-col gap-1"
|
|
251
|
+
rootClassName = "flex flex-col gap-1",
|
|
252
|
+
canDrag,
|
|
253
|
+
previewPosition,
|
|
254
|
+
draggedBlock
|
|
188
255
|
}) {
|
|
189
256
|
const items = blocksByParent.get(parentId) ?? [];
|
|
190
257
|
const filteredBlocks = items.filter((block) => block.id !== activeId);
|
|
191
|
-
const
|
|
258
|
+
const showGhostHere = previewPosition?.parentId === parentId && draggedBlock;
|
|
192
259
|
const containerClass = depth === 0 ? rootClassName : indentClassName;
|
|
193
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
260
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, children: [
|
|
261
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
262
|
+
DropZone,
|
|
263
|
+
{
|
|
264
|
+
id: parentId ? `into-${parentId}` : "root-start",
|
|
265
|
+
parentId,
|
|
266
|
+
onHover,
|
|
267
|
+
activeId,
|
|
268
|
+
className: dropZoneClassName,
|
|
269
|
+
activeClassName: dropZoneActiveClassName
|
|
270
|
+
}
|
|
271
|
+
),
|
|
272
|
+
filteredBlocks.map((block, index) => {
|
|
273
|
+
const isContainer = containerTypes.includes(block.type);
|
|
274
|
+
const isExpanded = expandedMap[block.id] !== false;
|
|
275
|
+
const Renderer = renderers[block.type];
|
|
276
|
+
const isDragDisabled = canDrag ? !canDrag(block) : false;
|
|
277
|
+
const ghostBeforeThis = showGhostHere && previewPosition.index === index;
|
|
278
|
+
const originalIndex = items.findIndex((b) => b.id === block.id);
|
|
279
|
+
const isLastInOriginal = originalIndex === items.length - 1;
|
|
280
|
+
if (!Renderer) {
|
|
281
|
+
console.warn(`No renderer found for block type: ${block.type}`);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const GhostRenderer = draggedBlock ? renderers[draggedBlock.type] : null;
|
|
285
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
286
|
+
ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "opacity-50 pointer-events-none", children: GhostRenderer({
|
|
287
|
+
block: draggedBlock,
|
|
288
|
+
isDragging: true,
|
|
289
|
+
depth
|
|
290
|
+
}) }),
|
|
291
|
+
/* @__PURE__ */ jsxRuntime.jsx(DraggableBlock, { block, disabled: isDragDisabled, children: ({ isDragging }) => {
|
|
292
|
+
if (isContainer) {
|
|
293
|
+
const childContent = isExpanded ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
228
294
|
TreeRenderer,
|
|
229
295
|
{
|
|
230
296
|
blocks,
|
|
@@ -240,38 +306,60 @@ function TreeRenderer({
|
|
|
240
306
|
dropZoneClassName,
|
|
241
307
|
dropZoneActiveClassName,
|
|
242
308
|
indentClassName,
|
|
243
|
-
rootClassName
|
|
309
|
+
rootClassName,
|
|
310
|
+
canDrag,
|
|
311
|
+
previewPosition,
|
|
312
|
+
draggedBlock
|
|
244
313
|
}
|
|
245
|
-
)
|
|
246
|
-
|
|
314
|
+
) }) : null;
|
|
315
|
+
return Renderer({
|
|
316
|
+
block,
|
|
317
|
+
children: childContent,
|
|
318
|
+
isDragging,
|
|
319
|
+
depth,
|
|
320
|
+
isExpanded,
|
|
321
|
+
onToggleExpand: () => onToggleExpand(block.id)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
247
324
|
return Renderer({
|
|
248
325
|
block,
|
|
249
|
-
children: childContent,
|
|
250
326
|
isDragging,
|
|
251
|
-
depth
|
|
252
|
-
isExpanded,
|
|
253
|
-
onToggleExpand: () => onToggleExpand(block.id)
|
|
327
|
+
depth
|
|
254
328
|
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
329
|
+
} }),
|
|
330
|
+
!isLastInOriginal && /* @__PURE__ */ jsxRuntime.jsx(
|
|
331
|
+
DropZone,
|
|
332
|
+
{
|
|
333
|
+
id: `after-${block.id}`,
|
|
334
|
+
parentId: block.parentId,
|
|
335
|
+
onHover,
|
|
336
|
+
activeId,
|
|
337
|
+
className: dropZoneClassName,
|
|
338
|
+
activeClassName: dropZoneActiveClassName
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
] }, block.id);
|
|
342
|
+
}),
|
|
343
|
+
showGhostHere && previewPosition.index >= filteredBlocks.length && draggedBlock && (() => {
|
|
344
|
+
const GhostRenderer = renderers[draggedBlock.type];
|
|
345
|
+
return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "opacity-50 pointer-events-none", children: GhostRenderer({
|
|
346
|
+
block: draggedBlock,
|
|
347
|
+
isDragging: true,
|
|
348
|
+
depth
|
|
349
|
+
}) }) : null;
|
|
350
|
+
})(),
|
|
351
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
352
|
+
DropZone,
|
|
353
|
+
{
|
|
354
|
+
id: parentId ? `end-${parentId}` : "root-end",
|
|
355
|
+
parentId,
|
|
356
|
+
onHover,
|
|
357
|
+
activeId,
|
|
358
|
+
className: dropZoneClassName,
|
|
359
|
+
activeClassName: dropZoneActiveClassName
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
] });
|
|
275
363
|
}
|
|
276
364
|
function DragOverlay({
|
|
277
365
|
activeBlock,
|
|
@@ -331,12 +419,15 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = []) {
|
|
|
331
419
|
const byParent = cloneParentMap(state.byParent);
|
|
332
420
|
const dragged = byId.get(String(activeId));
|
|
333
421
|
if (!dragged) return state;
|
|
422
|
+
const isRootStart = targetZone === "root-start";
|
|
423
|
+
const isRootEnd = targetZone === "root-end";
|
|
424
|
+
const isEnd = targetZone.startsWith("end-") || isRootEnd;
|
|
334
425
|
const zoneTargetId = extractUUID(targetZone);
|
|
335
426
|
const isAfter = targetZone.startsWith("after-");
|
|
336
|
-
const isInto = targetZone.startsWith("into-");
|
|
427
|
+
const isInto = targetZone.startsWith("into-") || isRootStart;
|
|
337
428
|
const target = byId.get(zoneTargetId);
|
|
338
429
|
const oldParentId = dragged.parentId ?? null;
|
|
339
|
-
const newParentId = isInto ? zoneTargetId : target?.parentId ?? null;
|
|
430
|
+
const newParentId = isRootStart || isRootEnd ? null : isInto || isEnd ? zoneTargetId : target?.parentId ?? null;
|
|
340
431
|
if (containerTypes.includes(dragged.type) && newParentId !== null) {
|
|
341
432
|
const newParent = byId.get(newParentId);
|
|
342
433
|
if (newParent && !containerTypes.includes(newParent.type)) {
|
|
@@ -348,8 +439,12 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = []) {
|
|
|
348
439
|
const filtered = oldList.filter((id) => id !== dragged.id);
|
|
349
440
|
byParent.set(oldParentId, filtered);
|
|
350
441
|
const newList = [...byParent.get(newParentId) ?? []];
|
|
351
|
-
let insertIndex
|
|
352
|
-
if (
|
|
442
|
+
let insertIndex;
|
|
443
|
+
if (isInto) {
|
|
444
|
+
insertIndex = 0;
|
|
445
|
+
} else if (isEnd) {
|
|
446
|
+
insertIndex = newList.length;
|
|
447
|
+
} else {
|
|
353
448
|
const idx = newList.indexOf(zoneTargetId);
|
|
354
449
|
insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
|
|
355
450
|
}
|
|
@@ -389,6 +484,30 @@ function deleteBlockAndDescendants(state, id) {
|
|
|
389
484
|
}
|
|
390
485
|
return { byId, byParent };
|
|
391
486
|
}
|
|
487
|
+
function getBlockPosition(blocks, blockId) {
|
|
488
|
+
const block = blocks.find((b) => b.id === blockId);
|
|
489
|
+
if (!block) return { parentId: null, index: 0 };
|
|
490
|
+
const siblings = blocks.filter((b) => b.parentId === block.parentId);
|
|
491
|
+
const index = siblings.findIndex((b) => b.id === blockId);
|
|
492
|
+
return { parentId: block.parentId, index };
|
|
493
|
+
}
|
|
494
|
+
function computeInitialExpanded(blocks, containerTypes, initialExpanded) {
|
|
495
|
+
if (initialExpanded === "none") {
|
|
496
|
+
return {};
|
|
497
|
+
}
|
|
498
|
+
const expandedMap = {};
|
|
499
|
+
const containers = blocks.filter((b) => containerTypes.includes(b.type));
|
|
500
|
+
if (initialExpanded === "all" || initialExpanded === void 0) {
|
|
501
|
+
for (const container of containers) {
|
|
502
|
+
expandedMap[container.id] = true;
|
|
503
|
+
}
|
|
504
|
+
} else if (Array.isArray(initialExpanded)) {
|
|
505
|
+
for (const id of initialExpanded) {
|
|
506
|
+
expandedMap[id] = true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return expandedMap;
|
|
510
|
+
}
|
|
392
511
|
function BlockTree({
|
|
393
512
|
blocks,
|
|
394
513
|
renderers,
|
|
@@ -400,17 +519,45 @@ function BlockTree({
|
|
|
400
519
|
className = "flex flex-col gap-1",
|
|
401
520
|
dropZoneClassName,
|
|
402
521
|
dropZoneActiveClassName,
|
|
403
|
-
indentClassName
|
|
522
|
+
indentClassName,
|
|
523
|
+
showDropPreview = true,
|
|
524
|
+
// Callbacks
|
|
525
|
+
onDragStart,
|
|
526
|
+
onDragMove,
|
|
527
|
+
onDragEnd,
|
|
528
|
+
onDragCancel,
|
|
529
|
+
onBlockMove,
|
|
530
|
+
onExpandChange,
|
|
531
|
+
onHoverChange,
|
|
532
|
+
// Customization
|
|
533
|
+
canDrag,
|
|
534
|
+
canDrop,
|
|
535
|
+
collisionDetection,
|
|
536
|
+
sensors: sensorConfig,
|
|
537
|
+
initialExpanded
|
|
404
538
|
}) {
|
|
405
|
-
const sensors = useConfiguredSensors({
|
|
539
|
+
const sensors = useConfiguredSensors({
|
|
540
|
+
activationDistance: sensorConfig?.activationDistance ?? activationDistance,
|
|
541
|
+
activationDelay: sensorConfig?.activationDelay,
|
|
542
|
+
tolerance: sensorConfig?.tolerance
|
|
543
|
+
});
|
|
544
|
+
const initialExpandedMap = react.useMemo(
|
|
545
|
+
() => computeInitialExpanded(blocks, containerTypes, initialExpanded),
|
|
546
|
+
// Only compute on mount
|
|
547
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
548
|
+
[]
|
|
549
|
+
);
|
|
406
550
|
const stateRef = react.useRef({
|
|
407
551
|
activeId: null,
|
|
408
552
|
hoverZone: null,
|
|
409
|
-
expandedMap:
|
|
410
|
-
virtualState: null
|
|
553
|
+
expandedMap: initialExpandedMap,
|
|
554
|
+
virtualState: null,
|
|
555
|
+
isDragging: false
|
|
411
556
|
});
|
|
412
557
|
const initialBlocksRef = react.useRef([]);
|
|
413
558
|
const cachedReorderRef = react.useRef(null);
|
|
559
|
+
const fromPositionRef = react.useRef(null);
|
|
560
|
+
const stickyCollisionRef = react.useRef(createStickyCollision(20));
|
|
414
561
|
const [, forceRender] = react.useReducer((x) => x + 1, 0);
|
|
415
562
|
const debouncedSetVirtual = react.useRef(
|
|
416
563
|
debounce((newBlocks) => {
|
|
@@ -422,72 +569,218 @@ function BlockTree({
|
|
|
422
569
|
forceRender();
|
|
423
570
|
}, previewDebounce)
|
|
424
571
|
).current;
|
|
425
|
-
const
|
|
572
|
+
const debouncedDragMove = react.useRef(
|
|
573
|
+
debounce((event) => {
|
|
574
|
+
onDragMove?.(event);
|
|
575
|
+
}, 50)
|
|
576
|
+
).current;
|
|
577
|
+
const originalIndex = computeNormalizedIndex(blocks);
|
|
426
578
|
const blocksByParent = /* @__PURE__ */ new Map();
|
|
427
|
-
for (const [parentId, ids] of
|
|
579
|
+
for (const [parentId, ids] of originalIndex.byParent.entries()) {
|
|
428
580
|
blocksByParent.set(
|
|
429
581
|
parentId,
|
|
430
|
-
ids.map((id) =>
|
|
582
|
+
ids.map((id) => originalIndex.byId.get(id)).filter(Boolean)
|
|
431
583
|
);
|
|
432
584
|
}
|
|
433
|
-
const
|
|
585
|
+
const previewPosition = react.useMemo(() => {
|
|
586
|
+
if (!showDropPreview || !stateRef.current.virtualState || !stateRef.current.activeId) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
const virtualIndex = stateRef.current.virtualState;
|
|
590
|
+
const activeId = stateRef.current.activeId;
|
|
591
|
+
const block = virtualIndex.byId.get(activeId);
|
|
592
|
+
if (!block) return null;
|
|
593
|
+
const parentId = block.parentId ?? null;
|
|
594
|
+
const siblings = virtualIndex.byParent.get(parentId) ?? [];
|
|
595
|
+
const index = siblings.indexOf(activeId);
|
|
596
|
+
return { parentId, index };
|
|
597
|
+
}, [showDropPreview, stateRef.current.virtualState, stateRef.current.activeId]);
|
|
598
|
+
const activeBlock = stateRef.current.activeId ? originalIndex.byId.get(stateRef.current.activeId) ?? null : null;
|
|
599
|
+
const draggedBlock = activeBlock;
|
|
434
600
|
const handleDragStart = react.useCallback((event) => {
|
|
435
601
|
const id = String(event.active.id);
|
|
602
|
+
const block = blocks.find((b) => b.id === id);
|
|
603
|
+
if (!block) return;
|
|
604
|
+
if (canDrag && !canDrag(block)) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const dragEvent = {
|
|
608
|
+
block,
|
|
609
|
+
blockId: id
|
|
610
|
+
};
|
|
611
|
+
const result = onDragStart?.(dragEvent);
|
|
612
|
+
if (result === false) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
fromPositionRef.current = getBlockPosition(blocks, id);
|
|
616
|
+
stickyCollisionRef.current.reset();
|
|
436
617
|
stateRef.current.activeId = id;
|
|
618
|
+
stateRef.current.isDragging = true;
|
|
437
619
|
initialBlocksRef.current = [...blocks];
|
|
438
620
|
cachedReorderRef.current = null;
|
|
439
621
|
forceRender();
|
|
440
|
-
}, [blocks]);
|
|
622
|
+
}, [blocks, canDrag, onDragStart]);
|
|
623
|
+
const handleDragMove = react.useCallback((event) => {
|
|
624
|
+
if (!onDragMove) return;
|
|
625
|
+
const id = stateRef.current.activeId;
|
|
626
|
+
if (!id) return;
|
|
627
|
+
const block = blocks.find((b) => b.id === id);
|
|
628
|
+
if (!block) return;
|
|
629
|
+
const moveEvent = {
|
|
630
|
+
block,
|
|
631
|
+
blockId: id,
|
|
632
|
+
overZone: stateRef.current.hoverZone,
|
|
633
|
+
coordinates: {
|
|
634
|
+
x: event.delta.x,
|
|
635
|
+
y: event.delta.y
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
debouncedDragMove(moveEvent);
|
|
639
|
+
}, [blocks, onDragMove, debouncedDragMove]);
|
|
441
640
|
const handleDragOver = react.useCallback((event) => {
|
|
442
641
|
if (!event.over) return;
|
|
443
642
|
const targetZone = String(event.over.id);
|
|
444
643
|
const activeId = stateRef.current.activeId;
|
|
445
644
|
if (!activeId) return;
|
|
645
|
+
const activeBlock2 = blocks.find((b) => b.id === activeId);
|
|
646
|
+
const targetBlockId = extractBlockId(targetZone);
|
|
647
|
+
const targetBlock = blocks.find((b) => b.id === targetBlockId) ?? null;
|
|
648
|
+
if (canDrop && activeBlock2 && !canDrop(activeBlock2, targetZone, targetBlock)) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (stateRef.current.hoverZone !== targetZone) {
|
|
652
|
+
const zoneType = getDropZoneType(targetZone);
|
|
653
|
+
const hoverEvent = {
|
|
654
|
+
zoneId: targetZone,
|
|
655
|
+
zoneType,
|
|
656
|
+
targetBlock
|
|
657
|
+
};
|
|
658
|
+
onHoverChange?.(hoverEvent);
|
|
659
|
+
}
|
|
446
660
|
stateRef.current.hoverZone = targetZone;
|
|
447
661
|
const baseIndex = computeNormalizedIndex(initialBlocksRef.current);
|
|
448
662
|
const updatedIndex = reparentBlockIndex(baseIndex, activeId, targetZone, containerTypes);
|
|
449
663
|
const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes);
|
|
450
664
|
cachedReorderRef.current = { targetId: targetZone, reorderedBlocks: orderedBlocks };
|
|
451
|
-
|
|
452
|
-
|
|
665
|
+
if (showDropPreview) {
|
|
666
|
+
debouncedSetVirtual(orderedBlocks);
|
|
667
|
+
}
|
|
668
|
+
}, [blocks, containerTypes, debouncedSetVirtual, canDrop, onHoverChange, showDropPreview]);
|
|
453
669
|
const handleDragEnd = react.useCallback((_event) => {
|
|
454
670
|
debouncedSetVirtual.cancel();
|
|
671
|
+
debouncedDragMove.cancel();
|
|
455
672
|
const cached = cachedReorderRef.current;
|
|
673
|
+
const activeId = stateRef.current.activeId;
|
|
674
|
+
const activeBlockData = activeId ? blocks.find((b) => b.id === activeId) : null;
|
|
675
|
+
if (activeBlockData) {
|
|
676
|
+
const endEvent = {
|
|
677
|
+
block: activeBlockData,
|
|
678
|
+
blockId: activeId,
|
|
679
|
+
targetZone: cached?.targetId ?? null,
|
|
680
|
+
cancelled: false
|
|
681
|
+
};
|
|
682
|
+
onDragEnd?.(endEvent);
|
|
683
|
+
}
|
|
684
|
+
if (cached && activeBlockData && fromPositionRef.current) {
|
|
685
|
+
const toPosition = getBlockPosition(cached.reorderedBlocks, activeBlockData.id);
|
|
686
|
+
const moveEvent = {
|
|
687
|
+
block: activeBlockData,
|
|
688
|
+
from: fromPositionRef.current,
|
|
689
|
+
to: toPosition,
|
|
690
|
+
blocks: cached.reorderedBlocks
|
|
691
|
+
};
|
|
692
|
+
onBlockMove?.(moveEvent);
|
|
693
|
+
}
|
|
456
694
|
stateRef.current.activeId = null;
|
|
457
695
|
stateRef.current.hoverZone = null;
|
|
458
696
|
stateRef.current.virtualState = null;
|
|
697
|
+
stateRef.current.isDragging = false;
|
|
459
698
|
cachedReorderRef.current = null;
|
|
460
699
|
initialBlocksRef.current = [];
|
|
700
|
+
fromPositionRef.current = null;
|
|
461
701
|
if (cached && onChange) {
|
|
462
702
|
onChange(cached.reorderedBlocks);
|
|
463
703
|
}
|
|
464
704
|
forceRender();
|
|
465
|
-
}, [debouncedSetVirtual, onChange]);
|
|
705
|
+
}, [blocks, debouncedSetVirtual, debouncedDragMove, onChange, onDragEnd, onBlockMove]);
|
|
706
|
+
const handleDragCancel = react.useCallback((_event) => {
|
|
707
|
+
debouncedSetVirtual.cancel();
|
|
708
|
+
debouncedDragMove.cancel();
|
|
709
|
+
const activeId = stateRef.current.activeId;
|
|
710
|
+
const activeBlockData = activeId ? blocks.find((b) => b.id === activeId) : null;
|
|
711
|
+
if (activeBlockData) {
|
|
712
|
+
const cancelEvent = {
|
|
713
|
+
block: activeBlockData,
|
|
714
|
+
blockId: activeId,
|
|
715
|
+
targetZone: null,
|
|
716
|
+
cancelled: true
|
|
717
|
+
};
|
|
718
|
+
onDragCancel?.(cancelEvent);
|
|
719
|
+
onDragEnd?.(cancelEvent);
|
|
720
|
+
}
|
|
721
|
+
stateRef.current.activeId = null;
|
|
722
|
+
stateRef.current.hoverZone = null;
|
|
723
|
+
stateRef.current.virtualState = null;
|
|
724
|
+
stateRef.current.isDragging = false;
|
|
725
|
+
cachedReorderRef.current = null;
|
|
726
|
+
initialBlocksRef.current = [];
|
|
727
|
+
fromPositionRef.current = null;
|
|
728
|
+
forceRender();
|
|
729
|
+
}, [blocks, debouncedSetVirtual, debouncedDragMove, onDragCancel, onDragEnd]);
|
|
466
730
|
const handleHover = react.useCallback((zoneId, _parentId) => {
|
|
467
731
|
const activeId = stateRef.current.activeId;
|
|
468
732
|
if (!activeId) return;
|
|
733
|
+
const activeBlockData = blocks.find((b) => b.id === activeId);
|
|
734
|
+
const targetBlockId = extractBlockId(zoneId);
|
|
735
|
+
const targetBlock = blocks.find((b) => b.id === targetBlockId) ?? null;
|
|
736
|
+
if (canDrop && activeBlockData && !canDrop(activeBlockData, zoneId, targetBlock)) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (stateRef.current.hoverZone !== zoneId) {
|
|
740
|
+
const zoneType = getDropZoneType(zoneId);
|
|
741
|
+
const hoverEvent = {
|
|
742
|
+
zoneId,
|
|
743
|
+
zoneType,
|
|
744
|
+
targetBlock
|
|
745
|
+
};
|
|
746
|
+
onHoverChange?.(hoverEvent);
|
|
747
|
+
}
|
|
469
748
|
stateRef.current.hoverZone = zoneId;
|
|
470
749
|
const baseIndex = computeNormalizedIndex(initialBlocksRef.current);
|
|
471
750
|
const updatedIndex = reparentBlockIndex(baseIndex, activeId, zoneId, containerTypes);
|
|
472
751
|
const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes);
|
|
473
752
|
cachedReorderRef.current = { targetId: zoneId, reorderedBlocks: orderedBlocks };
|
|
474
|
-
|
|
475
|
-
|
|
753
|
+
if (showDropPreview) {
|
|
754
|
+
debouncedSetVirtual(orderedBlocks);
|
|
755
|
+
}
|
|
756
|
+
}, [blocks, containerTypes, debouncedSetVirtual, canDrop, onHoverChange, showDropPreview]);
|
|
476
757
|
const handleToggleExpand = react.useCallback((id) => {
|
|
758
|
+
const newExpanded = stateRef.current.expandedMap[id] === false;
|
|
477
759
|
stateRef.current.expandedMap = {
|
|
478
760
|
...stateRef.current.expandedMap,
|
|
479
|
-
[id]:
|
|
761
|
+
[id]: newExpanded
|
|
480
762
|
};
|
|
763
|
+
const block = blocks.find((b) => b.id === id);
|
|
764
|
+
if (block && onExpandChange) {
|
|
765
|
+
const expandEvent = {
|
|
766
|
+
block,
|
|
767
|
+
blockId: id,
|
|
768
|
+
expanded: newExpanded
|
|
769
|
+
};
|
|
770
|
+
onExpandChange(expandEvent);
|
|
771
|
+
}
|
|
481
772
|
forceRender();
|
|
482
|
-
}, []);
|
|
773
|
+
}, [blocks, onExpandChange]);
|
|
483
774
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
484
775
|
core.DndContext,
|
|
485
776
|
{
|
|
486
777
|
sensors,
|
|
487
|
-
collisionDetection:
|
|
778
|
+
collisionDetection: collisionDetection ?? stickyCollisionRef.current,
|
|
488
779
|
onDragStart: handleDragStart,
|
|
780
|
+
onDragMove: handleDragMove,
|
|
489
781
|
onDragOver: handleDragOver,
|
|
490
782
|
onDragEnd: handleDragEnd,
|
|
783
|
+
onDragCancel: handleDragCancel,
|
|
491
784
|
children: [
|
|
492
785
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
493
786
|
TreeRenderer,
|
|
@@ -504,7 +797,10 @@ function BlockTree({
|
|
|
504
797
|
dropZoneClassName,
|
|
505
798
|
dropZoneActiveClassName,
|
|
506
799
|
indentClassName,
|
|
507
|
-
rootClassName: className
|
|
800
|
+
rootClassName: className,
|
|
801
|
+
canDrag,
|
|
802
|
+
previewPosition,
|
|
803
|
+
draggedBlock
|
|
508
804
|
}
|
|
509
805
|
) }),
|
|
510
806
|
/* @__PURE__ */ jsxRuntime.jsx(DragOverlay, { activeBlock, children: dragOverlay })
|
|
@@ -577,7 +873,6 @@ function createBlockState() {
|
|
|
577
873
|
reducerWithContainerTypes,
|
|
578
874
|
computeNormalizedIndex(initialBlocks)
|
|
579
875
|
);
|
|
580
|
-
const [lastCreatedItem, setLastCreatedItem] = react.useState(null);
|
|
581
876
|
const blocks = react.useMemo(() => {
|
|
582
877
|
const result = [];
|
|
583
878
|
const walk = (parentId) => {
|
|
@@ -626,7 +921,6 @@ function createBlockState() {
|
|
|
626
921
|
order: 0
|
|
627
922
|
};
|
|
628
923
|
dispatch({ type: "ADD_ITEM", payload: newItem });
|
|
629
|
-
setLastCreatedItem(newItem);
|
|
630
924
|
return newItem;
|
|
631
925
|
},
|
|
632
926
|
[]
|
|
@@ -649,7 +943,6 @@ function createBlockState() {
|
|
|
649
943
|
type: "INSERT_ITEM",
|
|
650
944
|
payload: { item: newItem, parentId, index: insertIndex }
|
|
651
945
|
});
|
|
652
|
-
setLastCreatedItem(newItem);
|
|
653
946
|
return newItem;
|
|
654
947
|
},
|
|
655
948
|
[state]
|
|
@@ -855,6 +1148,7 @@ exports.cloneParentMap = cloneParentMap;
|
|
|
855
1148
|
exports.closestCenterCollision = closestCenterCollision;
|
|
856
1149
|
exports.computeNormalizedIndex = computeNormalizedIndex;
|
|
857
1150
|
exports.createBlockState = createBlockState;
|
|
1151
|
+
exports.createStickyCollision = createStickyCollision;
|
|
858
1152
|
exports.createTreeState = createTreeState;
|
|
859
1153
|
exports.debounce = debounce;
|
|
860
1154
|
exports.deleteBlockAndDescendants = deleteBlockAndDescendants;
|