@dnd-block-tree/svelte 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/README.md +62 -0
- package/dist/bridge.d.ts +13 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +55 -0
- package/dist/components/BlockTree.svelte +368 -0
- package/dist/components/BlockTree.svelte.d.ts +32 -0
- package/dist/components/BlockTree.svelte.d.ts.map +1 -0
- package/dist/components/BlockTreeDevTools.svelte +54 -0
- package/dist/components/BlockTreeDevTools.svelte.d.ts +12 -0
- package/dist/components/BlockTreeDevTools.svelte.d.ts.map +1 -0
- package/dist/components/BlockTreeSSR.svelte +22 -0
- package/dist/components/BlockTreeSSR.svelte.d.ts +9 -0
- package/dist/components/BlockTreeSSR.svelte.d.ts.map +1 -0
- package/dist/components/DragOverlay.svelte +48 -0
- package/dist/components/DragOverlay.svelte.d.ts +11 -0
- package/dist/components/DragOverlay.svelte.d.ts.map +1 -0
- package/dist/components/DraggableBlock.svelte +43 -0
- package/dist/components/DraggableBlock.svelte.d.ts +17 -0
- package/dist/components/DraggableBlock.svelte.d.ts.map +1 -0
- package/dist/components/DropZone.svelte +50 -0
- package/dist/components/DropZone.svelte.d.ts +13 -0
- package/dist/components/DropZone.svelte.d.ts.map +1 -0
- package/dist/components/GhostPreview.svelte +13 -0
- package/dist/components/GhostPreview.svelte.d.ts +8 -0
- package/dist/components/GhostPreview.svelte.d.ts.map +1 -0
- package/dist/components/TreeRenderer.svelte +197 -0
- package/dist/components/TreeRenderer.svelte.d.ts +35 -0
- package/dist/components/TreeRenderer.svelte.d.ts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/state/block-history.svelte.d.ts +18 -0
- package/dist/state/block-history.svelte.d.ts.map +1 -0
- package/dist/state/block-history.svelte.js +30 -0
- package/dist/state/block-state.svelte.d.ts +27 -0
- package/dist/state/block-state.svelte.d.ts.map +1 -0
- package/dist/state/block-state.svelte.js +91 -0
- package/dist/state/tree-state.svelte.d.ts +39 -0
- package/dist/state/tree-state.svelte.d.ts.map +1 -0
- package/dist/state/tree-state.svelte.js +118 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/haptic.d.ts +6 -0
- package/dist/utils/haptic.d.ts.map +1 -0
- package/dist/utils/haptic.js +9 -0
- package/dist/utils/sensors.d.ts +11 -0
- package/dist/utils/sensors.d.ts.map +1 -0
- package/dist/utils/sensors.js +10 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @dnd-block-tree/svelte
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@dnd-block-tree/svelte)
|
|
4
|
+
|
|
5
|
+
Svelte 5 adapter for [dnd-block-tree](https://github.com/thesandybridge/dnd-block-tree) — components, state factories, and [@dnd-kit/svelte](https://dndkit.com/) integration for building hierarchical drag-and-drop interfaces.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @dnd-block-tree/svelte @dnd-kit/svelte @dnd-kit/dom svelte
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires **Svelte 5.29+**, **@dnd-kit/svelte 0.3+**, and **@dnd-kit/dom 0.3+**.
|
|
14
|
+
|
|
15
|
+
## What's Included
|
|
16
|
+
|
|
17
|
+
- **Components** — `BlockTree`, `BlockTreeSSR`, `TreeRenderer`, `DropZone`, `DraggableBlock`, `DragOverlay`, `GhostPreview`, `BlockTreeDevTools`
|
|
18
|
+
- **State Factories** — `createBlockState`, `createTreeState`, `createBlockHistory` (runes-based)
|
|
19
|
+
- **Collision Bridge** — `adaptCollisionDetection` to bridge core's `CoreCollisionDetection` to @dnd-kit/svelte
|
|
20
|
+
- **Re-exports** — all types and utilities from `@dnd-block-tree/core`
|
|
21
|
+
|
|
22
|
+
## Quick Example
|
|
23
|
+
|
|
24
|
+
```svelte
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import { BlockTree, type BaseBlock } from '@dnd-block-tree/svelte'
|
|
27
|
+
|
|
28
|
+
interface MyBlock extends BaseBlock {
|
|
29
|
+
type: 'section' | 'task'
|
|
30
|
+
title: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let blocks = $state<MyBlock[]>([
|
|
34
|
+
{ id: '1', type: 'section', title: 'Tasks', parentId: null, order: 0 },
|
|
35
|
+
{ id: '2', type: 'task', title: 'Do something', parentId: '1', order: 0 },
|
|
36
|
+
])
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<BlockTree
|
|
40
|
+
{blocks}
|
|
41
|
+
containerTypes={['section']}
|
|
42
|
+
onChange={(b) => blocks = b}
|
|
43
|
+
>
|
|
44
|
+
{#snippet renderBlock({ block, children, isExpanded, onToggleExpand })}
|
|
45
|
+
<div>
|
|
46
|
+
{#if onToggleExpand}
|
|
47
|
+
<button onclick={onToggleExpand}>{isExpanded ? '▼' : '▶'}</button>
|
|
48
|
+
{/if}
|
|
49
|
+
{block.title}
|
|
50
|
+
{#if children}{@render children()}{/if}
|
|
51
|
+
</div>
|
|
52
|
+
{/snippet}
|
|
53
|
+
</BlockTree>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
Full docs at **[blocktree.sandybridge.io](https://blocktree.sandybridge.io/docs)**.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CoreCollisionDetection } from '@dnd-block-tree/core';
|
|
2
|
+
/**
|
|
3
|
+
* Adapt a framework-agnostic CoreCollisionDetection into a @dnd-kit/dom CollisionDetection.
|
|
4
|
+
*
|
|
5
|
+
* @dnd-kit/dom collision detection receives a DragOperation and returns collisions.
|
|
6
|
+
* We bridge by extracting droppable rects and the pointer rect, running the core
|
|
7
|
+
* detector, and mapping results back.
|
|
8
|
+
*/
|
|
9
|
+
export declare function adaptCollisionDetection(coreDetector: CoreCollisionDetection): (args: {
|
|
10
|
+
droppables: any[];
|
|
11
|
+
dragOperation: any;
|
|
12
|
+
}) => any[];
|
|
13
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EAIvB,MAAM,sBAAsB,CAAA;AAE7B;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,sBAAsB,GACnC,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,GAAG,EAAE,CAAC;IAAC,aAAa,EAAE,GAAG,CAAA;CAAE,KAAK,GAAG,EAAE,CAgD5D"}
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapt a framework-agnostic CoreCollisionDetection into a @dnd-kit/dom CollisionDetection.
|
|
3
|
+
*
|
|
4
|
+
* @dnd-kit/dom collision detection receives a DragOperation and returns collisions.
|
|
5
|
+
* We bridge by extracting droppable rects and the pointer rect, running the core
|
|
6
|
+
* detector, and mapping results back.
|
|
7
|
+
*/
|
|
8
|
+
export function adaptCollisionDetection(coreDetector) {
|
|
9
|
+
return ({ droppables, dragOperation }) => {
|
|
10
|
+
const pointerPosition = dragOperation?.position?.current;
|
|
11
|
+
if (!pointerPosition)
|
|
12
|
+
return [];
|
|
13
|
+
// Build collision candidates from droppable elements
|
|
14
|
+
const candidates = [];
|
|
15
|
+
for (const droppable of droppables) {
|
|
16
|
+
const el = droppable.element;
|
|
17
|
+
if (!el)
|
|
18
|
+
continue;
|
|
19
|
+
const domRect = el.getBoundingClientRect();
|
|
20
|
+
candidates.push({
|
|
21
|
+
id: String(droppable.id),
|
|
22
|
+
rect: {
|
|
23
|
+
top: domRect.top,
|
|
24
|
+
left: domRect.left,
|
|
25
|
+
width: domRect.width,
|
|
26
|
+
height: domRect.height,
|
|
27
|
+
right: domRect.right,
|
|
28
|
+
bottom: domRect.bottom,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Build pointer rect
|
|
33
|
+
const pointerRect = {
|
|
34
|
+
top: pointerPosition.y,
|
|
35
|
+
left: pointerPosition.x,
|
|
36
|
+
width: 1,
|
|
37
|
+
height: 1,
|
|
38
|
+
right: pointerPosition.x + 1,
|
|
39
|
+
bottom: pointerPosition.y + 1,
|
|
40
|
+
};
|
|
41
|
+
// Call core detector
|
|
42
|
+
const results = coreDetector(candidates, pointerRect);
|
|
43
|
+
// Map results back to droppable references
|
|
44
|
+
return results.map(result => {
|
|
45
|
+
const droppable = droppables.find((d) => String(d.id) === result.id);
|
|
46
|
+
if (!droppable)
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
id: result.id,
|
|
50
|
+
value: result.value,
|
|
51
|
+
droppable,
|
|
52
|
+
};
|
|
53
|
+
}).filter(Boolean);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { DragDropProvider } from '@dnd-kit/svelte'
|
|
3
|
+
import type {
|
|
4
|
+
BaseBlock,
|
|
5
|
+
BlockTreeCallbacks,
|
|
6
|
+
BlockPosition,
|
|
7
|
+
DragStartEvent,
|
|
8
|
+
DragMoveEvent,
|
|
9
|
+
DragEndEvent,
|
|
10
|
+
BlockMoveEvent,
|
|
11
|
+
MoveOperation,
|
|
12
|
+
ExpandChangeEvent,
|
|
13
|
+
HoverChangeEvent,
|
|
14
|
+
DropZoneType,
|
|
15
|
+
Rect,
|
|
16
|
+
SnapshotRectsRef,
|
|
17
|
+
} from '@dnd-block-tree/core'
|
|
18
|
+
import {
|
|
19
|
+
getDropZoneType,
|
|
20
|
+
extractBlockId,
|
|
21
|
+
createStickyCollision,
|
|
22
|
+
computeNormalizedIndex,
|
|
23
|
+
reparentBlockIndex,
|
|
24
|
+
reparentMultipleBlocks,
|
|
25
|
+
buildOrderedBlocks,
|
|
26
|
+
debounce,
|
|
27
|
+
} from '@dnd-block-tree/core'
|
|
28
|
+
import type { BlockTreeCustomization } from '../types'
|
|
29
|
+
import { adaptCollisionDetection } from '../bridge'
|
|
30
|
+
import { triggerHaptic } from '../utils/haptic'
|
|
31
|
+
import TreeRenderer from './TreeRenderer.svelte'
|
|
32
|
+
import DragOverlay from './DragOverlay.svelte'
|
|
33
|
+
import type { Snippet } from 'svelte'
|
|
34
|
+
|
|
35
|
+
interface Props extends BlockTreeCallbacks<BaseBlock>, BlockTreeCustomization<BaseBlock> {
|
|
36
|
+
blocks: BaseBlock[]
|
|
37
|
+
containerTypes?: readonly string[]
|
|
38
|
+
onChange?: (blocks: BaseBlock[]) => void
|
|
39
|
+
renderBlock: Snippet<[{
|
|
40
|
+
block: BaseBlock
|
|
41
|
+
isDragging: boolean
|
|
42
|
+
depth: number
|
|
43
|
+
isExpanded: boolean
|
|
44
|
+
onToggleExpand: (() => void) | null
|
|
45
|
+
children: Snippet | null
|
|
46
|
+
}]>
|
|
47
|
+
dragOverlay?: Snippet<[BaseBlock]>
|
|
48
|
+
activationDistance?: number
|
|
49
|
+
previewDebounce?: number
|
|
50
|
+
showDropPreview?: boolean
|
|
51
|
+
multiSelect?: boolean
|
|
52
|
+
selectedIds?: Set<string>
|
|
53
|
+
onSelectionChange?: (selectedIds: Set<string>) => void
|
|
54
|
+
dropZoneClass?: string
|
|
55
|
+
dropZoneActiveClass?: string
|
|
56
|
+
class?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let {
|
|
60
|
+
blocks,
|
|
61
|
+
containerTypes = [],
|
|
62
|
+
onChange,
|
|
63
|
+
renderBlock,
|
|
64
|
+
dragOverlay,
|
|
65
|
+
activationDistance = 8,
|
|
66
|
+
previewDebounce = 150,
|
|
67
|
+
showDropPreview = true,
|
|
68
|
+
class: className = '',
|
|
69
|
+
dropZoneClass = '',
|
|
70
|
+
dropZoneActiveClass = '',
|
|
71
|
+
// Callbacks
|
|
72
|
+
onDragStart,
|
|
73
|
+
onDragMove,
|
|
74
|
+
onDragEnd,
|
|
75
|
+
onDragCancel,
|
|
76
|
+
onBeforeMove,
|
|
77
|
+
onBlockMove,
|
|
78
|
+
onExpandChange,
|
|
79
|
+
onHoverChange,
|
|
80
|
+
// Customization
|
|
81
|
+
canDrag,
|
|
82
|
+
canDrop,
|
|
83
|
+
collisionDetection: userCollisionDetection,
|
|
84
|
+
sensors: sensorConfig,
|
|
85
|
+
animation,
|
|
86
|
+
initialExpanded,
|
|
87
|
+
orderingStrategy = 'integer',
|
|
88
|
+
maxDepth,
|
|
89
|
+
multiSelect = false,
|
|
90
|
+
selectedIds: externalSelectedIds,
|
|
91
|
+
onSelectionChange,
|
|
92
|
+
}: Props = $props()
|
|
93
|
+
|
|
94
|
+
// Sticky collision detection bridged from core
|
|
95
|
+
const coreStickyDetector = createStickyCollision(20)
|
|
96
|
+
const adaptedCollision = adaptCollisionDetection(
|
|
97
|
+
userCollisionDetection ?? coreStickyDetector
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Internal state
|
|
101
|
+
let activeId = $state<string | null>(null)
|
|
102
|
+
let hoverZone = $state<string | null>(null)
|
|
103
|
+
let expandedMap = $state<Record<string, boolean>>(
|
|
104
|
+
computeInitialExpanded(blocks, containerTypes, initialExpanded)
|
|
105
|
+
)
|
|
106
|
+
let virtualState = $state<ReturnType<typeof computeNormalizedIndex> | null>(null)
|
|
107
|
+
let isDragging = $state(false)
|
|
108
|
+
|
|
109
|
+
// Non-reactive refs
|
|
110
|
+
let initialBlocksRef: BaseBlock[] = []
|
|
111
|
+
let cachedReorderRef: { targetId: string; reorderedBlocks: BaseBlock[] } | null = null
|
|
112
|
+
let fromPositionRef: BlockPosition | null = null
|
|
113
|
+
let draggedIdsRef: string[] = []
|
|
114
|
+
|
|
115
|
+
// Selection
|
|
116
|
+
let internalSelectedIds = $state(new Set<string>())
|
|
117
|
+
const selectedIds = $derived(externalSelectedIds ?? internalSelectedIds)
|
|
118
|
+
|
|
119
|
+
function setSelectedIds(ids: Set<string>) {
|
|
120
|
+
if (onSelectionChange) {
|
|
121
|
+
onSelectionChange(ids)
|
|
122
|
+
} else {
|
|
123
|
+
internalSelectedIds = ids
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Computed
|
|
128
|
+
const originalIndex = $derived(computeNormalizedIndex(blocks, orderingStrategy))
|
|
129
|
+
|
|
130
|
+
const blocksByParent = $derived.by(() => {
|
|
131
|
+
const effectiveIdx = virtualState ?? originalIndex
|
|
132
|
+
const map = new Map<string | null, BaseBlock[]>()
|
|
133
|
+
for (const [parentId, ids] of effectiveIdx.byParent.entries()) {
|
|
134
|
+
map.set(parentId, ids.map(id => effectiveIdx.byId.get(id)!).filter(Boolean))
|
|
135
|
+
}
|
|
136
|
+
return map
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const activeBlock = $derived(
|
|
140
|
+
activeId ? originalIndex.byId.get(activeId) ?? null : null
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const previewPosition = $derived.by(() => {
|
|
144
|
+
if (!showDropPreview || !virtualState || !activeId) return null
|
|
145
|
+
const block = virtualState.byId.get(activeId)
|
|
146
|
+
if (!block) return null
|
|
147
|
+
const parentId = block.parentId ?? null
|
|
148
|
+
const siblings = virtualState.byParent.get(parentId) ?? []
|
|
149
|
+
const index = siblings.indexOf(activeId)
|
|
150
|
+
return { parentId, index }
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const debouncedSetVirtual = debounce((newBlocks: BaseBlock[] | null) => {
|
|
154
|
+
if (newBlocks) {
|
|
155
|
+
virtualState = computeNormalizedIndex(newBlocks)
|
|
156
|
+
} else {
|
|
157
|
+
virtualState = null
|
|
158
|
+
}
|
|
159
|
+
}, previewDebounce)
|
|
160
|
+
|
|
161
|
+
// Event handlers
|
|
162
|
+
function handleDragStart(event: any) {
|
|
163
|
+
const id = String(event.operation?.source?.id ?? event.active?.id)
|
|
164
|
+
const block = blocks.find(b => b.id === id)
|
|
165
|
+
if (!block) return
|
|
166
|
+
if (canDrag && !canDrag(block)) return
|
|
167
|
+
|
|
168
|
+
const dragEvent: DragStartEvent<BaseBlock> = { block, blockId: id }
|
|
169
|
+
const result = onDragStart?.(dragEvent)
|
|
170
|
+
if (result === false) return
|
|
171
|
+
|
|
172
|
+
coreStickyDetector.reset()
|
|
173
|
+
fromPositionRef = getBlockPosition(blocks, id)
|
|
174
|
+
|
|
175
|
+
if (multiSelect && selectedIds.has(id)) {
|
|
176
|
+
draggedIdsRef = blocks.filter(b => selectedIds.has(b.id)).map(b => b.id)
|
|
177
|
+
} else {
|
|
178
|
+
draggedIdsRef = [id]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (sensorConfig?.hapticFeedback) triggerHaptic()
|
|
182
|
+
|
|
183
|
+
activeId = id
|
|
184
|
+
isDragging = true
|
|
185
|
+
initialBlocksRef = [...blocks]
|
|
186
|
+
cachedReorderRef = null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function handleDragOver(event: any) {
|
|
190
|
+
const targetId = String(event.operation?.target?.id ?? event.over?.id ?? '')
|
|
191
|
+
if (!targetId || !activeId) return
|
|
192
|
+
processHover(targetId)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function processHover(targetZone: string) {
|
|
196
|
+
const block = blocks.find(b => b.id === activeId)
|
|
197
|
+
const targetBlockId = extractBlockId(targetZone)
|
|
198
|
+
const targetBlock = blocks.find(b => b.id === targetBlockId) ?? null
|
|
199
|
+
|
|
200
|
+
if (canDrop && block && !canDrop(block, targetZone, targetBlock)) return
|
|
201
|
+
|
|
202
|
+
if (hoverZone !== targetZone) {
|
|
203
|
+
const zoneType: DropZoneType = getDropZoneType(targetZone)
|
|
204
|
+
onHoverChange?.({ zoneId: targetZone, zoneType, targetBlock })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
hoverZone = targetZone
|
|
208
|
+
|
|
209
|
+
const baseIndex = computeNormalizedIndex(initialBlocksRef, orderingStrategy)
|
|
210
|
+
const ids = draggedIdsRef
|
|
211
|
+
const updatedIndex = ids.length > 1
|
|
212
|
+
? reparentMultipleBlocks(baseIndex, ids, targetZone, containerTypes, orderingStrategy, maxDepth)
|
|
213
|
+
: reparentBlockIndex(baseIndex, activeId!, targetZone, containerTypes, orderingStrategy, maxDepth)
|
|
214
|
+
const orderedBlocks = buildOrderedBlocks(updatedIndex, containerTypes, orderingStrategy)
|
|
215
|
+
|
|
216
|
+
cachedReorderRef = { targetId: targetZone, reorderedBlocks: orderedBlocks }
|
|
217
|
+
if (showDropPreview) debouncedSetVirtual(orderedBlocks)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function handleDragEnd(event: any) {
|
|
221
|
+
debouncedSetVirtual.cancel()
|
|
222
|
+
|
|
223
|
+
let cached = cachedReorderRef
|
|
224
|
+
const dragId = activeId
|
|
225
|
+
const block = dragId ? blocks.find(b => b.id === dragId) : null
|
|
226
|
+
const cancelled = event?.canceled ?? false
|
|
227
|
+
|
|
228
|
+
if (cancelled) {
|
|
229
|
+
if (block && dragId) {
|
|
230
|
+
const cancelEvent: DragEndEvent<BaseBlock> = { block, blockId: dragId, targetZone: null, cancelled: true }
|
|
231
|
+
onDragCancel?.(cancelEvent)
|
|
232
|
+
onDragEnd?.(cancelEvent)
|
|
233
|
+
}
|
|
234
|
+
resetDragState()
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// onBeforeMove middleware
|
|
239
|
+
if (cached && block && fromPositionRef && onBeforeMove) {
|
|
240
|
+
const operation: MoveOperation<BaseBlock> = {
|
|
241
|
+
block,
|
|
242
|
+
from: fromPositionRef,
|
|
243
|
+
targetZone: cached.targetId,
|
|
244
|
+
}
|
|
245
|
+
const result = onBeforeMove(operation)
|
|
246
|
+
if (result === false) {
|
|
247
|
+
resetDragState()
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
if (result && result.targetZone !== cached.targetId) {
|
|
251
|
+
const baseIndex = computeNormalizedIndex(initialBlocksRef, orderingStrategy)
|
|
252
|
+
const ids = draggedIdsRef
|
|
253
|
+
const updatedIndex = ids.length > 1
|
|
254
|
+
? reparentMultipleBlocks(baseIndex, ids, result.targetZone, containerTypes, orderingStrategy, maxDepth)
|
|
255
|
+
: reparentBlockIndex(baseIndex, dragId!, result.targetZone, containerTypes, orderingStrategy, maxDepth)
|
|
256
|
+
cached = { targetId: result.targetZone, reorderedBlocks: buildOrderedBlocks(updatedIndex, containerTypes, orderingStrategy) }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (block && dragId) {
|
|
261
|
+
onDragEnd?.({ block, blockId: dragId, targetZone: cached?.targetId ?? null, cancelled: false })
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (cached && block && fromPositionRef) {
|
|
265
|
+
const toPosition = getBlockPosition(cached.reorderedBlocks, block.id)
|
|
266
|
+
onBlockMove?.({ block, from: fromPositionRef, to: toPosition, blocks: cached.reorderedBlocks, movedIds: [...draggedIdsRef] })
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (cached && onChange) {
|
|
270
|
+
onChange(cached.reorderedBlocks)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
resetDragState()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resetDragState() {
|
|
277
|
+
activeId = null
|
|
278
|
+
hoverZone = null
|
|
279
|
+
virtualState = null
|
|
280
|
+
isDragging = false
|
|
281
|
+
cachedReorderRef = null
|
|
282
|
+
initialBlocksRef = []
|
|
283
|
+
fromPositionRef = null
|
|
284
|
+
draggedIdsRef = []
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function handleToggleExpand(id: string) {
|
|
288
|
+
const newExpanded = expandedMap[id] === false
|
|
289
|
+
expandedMap = { ...expandedMap, [id]: newExpanded }
|
|
290
|
+
|
|
291
|
+
const block = blocks.find(b => b.id === id)
|
|
292
|
+
if (block && onExpandChange) {
|
|
293
|
+
onExpandChange({ block, blockId: id, expanded: newExpanded })
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function handleHover(zoneId: string, _parentId: string | null) {
|
|
298
|
+
if (!activeId) return
|
|
299
|
+
processHover(zoneId)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getBlockPosition(blocks: BaseBlock[], blockId: string): BlockPosition {
|
|
303
|
+
const block = blocks.find(b => b.id === blockId)
|
|
304
|
+
if (!block) return { parentId: null, index: 0 }
|
|
305
|
+
const siblings = blocks.filter(b => b.parentId === block.parentId)
|
|
306
|
+
const index = siblings.findIndex(b => b.id === blockId)
|
|
307
|
+
return { parentId: block.parentId, index }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function computeInitialExpanded(
|
|
311
|
+
blocks: BaseBlock[],
|
|
312
|
+
containerTypes: readonly string[],
|
|
313
|
+
initialExpanded: string[] | 'all' | 'none' | undefined
|
|
314
|
+
): Record<string, boolean> {
|
|
315
|
+
if (initialExpanded === 'none') {
|
|
316
|
+
const map: Record<string, boolean> = {}
|
|
317
|
+
for (const b of blocks) {
|
|
318
|
+
if (containerTypes.includes(b.type)) map[b.id] = false
|
|
319
|
+
}
|
|
320
|
+
return map
|
|
321
|
+
}
|
|
322
|
+
const map: Record<string, boolean> = {}
|
|
323
|
+
if (initialExpanded === 'all' || initialExpanded === undefined) {
|
|
324
|
+
for (const b of blocks) {
|
|
325
|
+
if (containerTypes.includes(b.type)) map[b.id] = true
|
|
326
|
+
}
|
|
327
|
+
} else if (Array.isArray(initialExpanded)) {
|
|
328
|
+
for (const id of initialExpanded) {
|
|
329
|
+
map[id] = true
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return map
|
|
333
|
+
}
|
|
334
|
+
</script>
|
|
335
|
+
|
|
336
|
+
<DragDropProvider
|
|
337
|
+
onDragStart={handleDragStart}
|
|
338
|
+
onDragOver={handleDragOver}
|
|
339
|
+
onDragEnd={handleDragEnd}
|
|
340
|
+
>
|
|
341
|
+
<div class={className} style:min-width="0">
|
|
342
|
+
<TreeRenderer
|
|
343
|
+
{blocks}
|
|
344
|
+
{blocksByParent}
|
|
345
|
+
parentId={null}
|
|
346
|
+
{activeId}
|
|
347
|
+
{expandedMap}
|
|
348
|
+
{containerTypes}
|
|
349
|
+
onHover={handleHover}
|
|
350
|
+
onToggleExpand={handleToggleExpand}
|
|
351
|
+
{renderBlock}
|
|
352
|
+
{dropZoneClass}
|
|
353
|
+
{dropZoneActiveClass}
|
|
354
|
+
{canDrag}
|
|
355
|
+
{previewPosition}
|
|
356
|
+
draggedBlock={activeBlock}
|
|
357
|
+
{selectedIds}
|
|
358
|
+
{animation}
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
<DragOverlay {activeBlock} selectedCount={multiSelect ? selectedIds.size : 0}>
|
|
362
|
+
{#snippet children(block)}
|
|
363
|
+
{#if dragOverlay}
|
|
364
|
+
{@render dragOverlay(block)}
|
|
365
|
+
{/if}
|
|
366
|
+
{/snippet}
|
|
367
|
+
</DragOverlay>
|
|
368
|
+
</DragDropProvider>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BaseBlock, BlockTreeCallbacks } from '@dnd-block-tree/core';
|
|
2
|
+
import type { BlockTreeCustomization } from '../types';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
interface Props extends BlockTreeCallbacks<BaseBlock>, BlockTreeCustomization<BaseBlock> {
|
|
5
|
+
blocks: BaseBlock[];
|
|
6
|
+
containerTypes?: readonly string[];
|
|
7
|
+
onChange?: (blocks: BaseBlock[]) => void;
|
|
8
|
+
renderBlock: Snippet<[
|
|
9
|
+
{
|
|
10
|
+
block: BaseBlock;
|
|
11
|
+
isDragging: boolean;
|
|
12
|
+
depth: number;
|
|
13
|
+
isExpanded: boolean;
|
|
14
|
+
onToggleExpand: (() => void) | null;
|
|
15
|
+
children: Snippet | null;
|
|
16
|
+
}
|
|
17
|
+
]>;
|
|
18
|
+
dragOverlay?: Snippet<[BaseBlock]>;
|
|
19
|
+
activationDistance?: number;
|
|
20
|
+
previewDebounce?: number;
|
|
21
|
+
showDropPreview?: boolean;
|
|
22
|
+
multiSelect?: boolean;
|
|
23
|
+
selectedIds?: Set<string>;
|
|
24
|
+
onSelectionChange?: (selectedIds: Set<string>) => void;
|
|
25
|
+
dropZoneClass?: string;
|
|
26
|
+
dropZoneActiveClass?: string;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
declare const BlockTree: import("svelte").Component<Props, {}, "">;
|
|
30
|
+
type BlockTree = ReturnType<typeof BlockTree>;
|
|
31
|
+
export default BlockTree;
|
|
32
|
+
//# sourceMappingURL=BlockTree.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BlockTree.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BlockTree.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,SAAS,EACT,kBAAkB,EAYnB,MAAM,sBAAsB,CAAA;AAW/B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAKtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGpC,UAAU,KAAM,SAAQ,kBAAkB,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC,SAAS,CAAC;IACtF,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IACxC,WAAW,EAAE,OAAO,CAAC;QAAC;YACpB,KAAK,EAAE,SAAS,CAAA;YAChB,UAAU,EAAE,OAAO,CAAA;YACnB,KAAK,EAAE,MAAM,CAAA;YACb,UAAU,EAAE,OAAO,CAAA;YACnB,cAAc,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;YACnC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAA;SACzB;KAAC,CAAC,CAAA;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACzB,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,CAAA;IACtD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAiTH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BaseBlock } from '@dnd-block-tree/core'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
blocks: BaseBlock[]
|
|
6
|
+
expandedMap: Record<string, boolean>
|
|
7
|
+
activeId?: string | null
|
|
8
|
+
hoverZone?: string | null
|
|
9
|
+
open?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
blocks,
|
|
14
|
+
expandedMap,
|
|
15
|
+
activeId = null,
|
|
16
|
+
hoverZone = null,
|
|
17
|
+
open = false,
|
|
18
|
+
}: Props = $props()
|
|
19
|
+
|
|
20
|
+
let isOpen = $state(open)
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
{#if isOpen}
|
|
24
|
+
<div
|
|
25
|
+
style="position: fixed; bottom: 10px; right: 10px; width: 360px; max-height: 400px; overflow: auto; background: #1f2937; color: #e5e7eb; font-family: monospace; font-size: 12px; padding: 12px; border-radius: 8px; z-index: 9999; box-shadow: 0 4px 16px rgba(0,0,0,0.3);"
|
|
26
|
+
>
|
|
27
|
+
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
28
|
+
<strong>BlockTree DevTools</strong>
|
|
29
|
+
<button onclick={() => isOpen = false} style="background: none; border: none; color: #9ca3af; cursor: pointer;">x</button>
|
|
30
|
+
</div>
|
|
31
|
+
<div style="margin-bottom: 8px;">
|
|
32
|
+
<span style="color: #9ca3af;">Blocks:</span> {blocks.length}
|
|
33
|
+
{#if activeId}
|
|
34
|
+
<span style="color: #fbbf24; margin-left: 8px;">Dragging: {activeId.slice(0, 8)}</span>
|
|
35
|
+
{/if}
|
|
36
|
+
{#if hoverZone}
|
|
37
|
+
<span style="color: #34d399; margin-left: 8px;">Hover: {hoverZone}</span>
|
|
38
|
+
{/if}
|
|
39
|
+
</div>
|
|
40
|
+
<pre style="margin: 0; white-space: pre-wrap; font-size: 11px;">{JSON.stringify(blocks.map(b => ({
|
|
41
|
+
id: b.id.slice(0, 8),
|
|
42
|
+
type: b.type,
|
|
43
|
+
parentId: b.parentId?.slice(0, 8) ?? null,
|
|
44
|
+
order: b.order,
|
|
45
|
+
})), null, 2)}</pre>
|
|
46
|
+
</div>
|
|
47
|
+
{:else}
|
|
48
|
+
<button
|
|
49
|
+
onclick={() => isOpen = true}
|
|
50
|
+
style="position: fixed; bottom: 10px; right: 10px; background: #1f2937; color: #e5e7eb; border: none; border-radius: 8px; padding: 8px 12px; font-size: 12px; cursor: pointer; z-index: 9999; box-shadow: 0 2px 8px rgba(0,0,0,0.2);"
|
|
51
|
+
>
|
|
52
|
+
DevTools
|
|
53
|
+
</button>
|
|
54
|
+
{/if}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BaseBlock } from '@dnd-block-tree/core';
|
|
2
|
+
interface Props {
|
|
3
|
+
blocks: BaseBlock[];
|
|
4
|
+
expandedMap: Record<string, boolean>;
|
|
5
|
+
activeId?: string | null;
|
|
6
|
+
hoverZone?: string | null;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
}
|
|
9
|
+
declare const BlockTreeDevTools: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type BlockTreeDevTools = ReturnType<typeof BlockTreeDevTools>;
|
|
11
|
+
export default BlockTreeDevTools;
|
|
12
|
+
//# sourceMappingURL=BlockTreeDevTools.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BlockTreeDevTools.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BlockTreeDevTools.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGpD,UAAU,KAAK;IACb,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AA8CH,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
children: Snippet
|
|
7
|
+
fallback?: Snippet
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { children, fallback }: Props = $props()
|
|
11
|
+
let mounted = $state(false)
|
|
12
|
+
|
|
13
|
+
onMount(() => {
|
|
14
|
+
mounted = true
|
|
15
|
+
})
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#if mounted}
|
|
19
|
+
{@render children()}
|
|
20
|
+
{:else if fallback}
|
|
21
|
+
{@render fallback()}
|
|
22
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
children: Snippet;
|
|
4
|
+
fallback?: Snippet;
|
|
5
|
+
}
|
|
6
|
+
declare const BlockTreeSSR: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type BlockTreeSSR = ReturnType<typeof BlockTreeSSR>;
|
|
8
|
+
export default BlockTreeSSR;
|
|
9
|
+
//# sourceMappingURL=BlockTreeSSR.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BlockTreeSSR.svelte.d.ts","sourceRoot":"","sources":["../../src/components/BlockTreeSSR.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGpC,UAAU,KAAK;IACb,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAsBH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|