dnd-block-tree 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +482 -14
- package/dist/index.d.mts +111 -4
- package/dist/index.d.ts +111 -4
- package/dist/index.js +311 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,9 +13,10 @@ A headless React library for building hierarchical drag-and-drop interfaces. Bri
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
- **Stable Drop Zones** - Zones render based on original block positions, not preview state, ensuring consistent drop targets during drag
|
|
16
|
-
- **Ghost Preview** -
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
16
|
+
- **Ghost Preview** - In-flow semi-transparent preview shows where blocks will land with accurate layout
|
|
17
|
+
- **Snapshotted Collision** - Zone rects are frozen on drag start and re-measured after each ghost commit, preventing layout-shift feedback loops
|
|
18
|
+
- **Depth-Aware Collision** - Smart algorithm prefers nested zones when cursor is at indented levels, with cross-depth-aware hysteresis
|
|
19
|
+
- **Mobile & Touch Support** - Separate touch/pointer activation constraints with configurable `longPressDelay` and optional `hapticFeedback`
|
|
19
20
|
- **Snapshot-Based Computation** - State captured at drag start. All preview computations use snapshot, ensuring consistent behavior
|
|
20
21
|
- **Debounced Preview** - 150ms debounced virtual state for smooth drag previews without jitter
|
|
21
22
|
- **Customizable Drag Rules** - `canDrag` and `canDrop` filters for fine-grained control over drag behavior
|
|
@@ -25,6 +26,10 @@ A headless React library for building hierarchical drag-and-drop interfaces. Bri
|
|
|
25
26
|
- **Undo/Redo** - Composable `useBlockHistory` hook with past/future stacks for state history management
|
|
26
27
|
- **Lifecycle Callbacks** - `onBlockAdd`, `onBlockDelete`, `onBlockMove`, `onBeforeMove` middleware, and more
|
|
27
28
|
- **Fractional Indexing** - Opt-in CRDT-compatible ordering via `orderingStrategy: 'fractional'`
|
|
29
|
+
- **Serialization** - `flatToNested` / `nestedToFlat` converters for flat array ↔ nested tree transforms
|
|
30
|
+
- **SSR Compatible** - `BlockTreeSSR` wrapper for hydration-safe rendering in Next.js and other SSR environments
|
|
31
|
+
- **Animation Support** - CSS expand/collapse transitions via `AnimationConfig` and FLIP reorder animations via `useLayoutAnimation`
|
|
32
|
+
- **Virtual Scrolling** - Windowed rendering for large trees (1000+ blocks) via `virtualize` prop
|
|
28
33
|
|
|
29
34
|
## Installation
|
|
30
35
|
|
|
@@ -83,12 +88,17 @@ function App() {
|
|
|
83
88
|
| `showDropPreview` | `boolean` | `true` | Show live preview of block at drop position |
|
|
84
89
|
| `canDrag` | `(block: T) => boolean` | - | Filter which blocks can be dragged |
|
|
85
90
|
| `canDrop` | `(block, zone, target) => boolean` | - | Filter valid drop targets |
|
|
91
|
+
| `collisionDetection` | `CollisionDetection` | sticky | Custom collision detection algorithm (from `@dnd-kit/core`) |
|
|
92
|
+
| `sensors` | `SensorConfig` | - | Sensor configuration (see [Sensor Config](#sensor-config)) |
|
|
93
|
+
| `animation` | `AnimationConfig` | - | Animation configuration (see [Animation](#animation--transitions)) |
|
|
86
94
|
| `maxDepth` | `number` | - | Maximum nesting depth (1 = flat, 2 = one level, etc.) |
|
|
87
95
|
| `keyboardNavigation` | `boolean` | `false` | Enable keyboard navigation with arrow keys |
|
|
88
96
|
| `multiSelect` | `boolean` | `false` | Enable multi-select with Cmd/Ctrl+Click and Shift+Click |
|
|
89
97
|
| `selectedIds` | `Set<string>` | - | Externally-controlled selected IDs (for multi-select) |
|
|
90
98
|
| `onSelectionChange` | `(ids: Set<string>) => void` | - | Called when selection changes |
|
|
91
99
|
| `orderingStrategy` | `'integer' \| 'fractional'` | `'integer'` | Sibling ordering strategy |
|
|
100
|
+
| `initialExpanded` | `string[] \| 'all' \| 'none'` | `'all'` | Initially expanded container IDs |
|
|
101
|
+
| `virtualize` | `{ itemHeight: number; overscan?: number }` | - | Enable virtual scrolling (see [Virtual Scrolling](#virtual-scrolling)) |
|
|
92
102
|
| `className` | `string` | - | Root container class |
|
|
93
103
|
| `dropZoneClassName` | `string` | - | Drop zone class |
|
|
94
104
|
| `dropZoneActiveClassName` | `string` | - | Active drop zone class |
|
|
@@ -109,6 +119,145 @@ function App() {
|
|
|
109
119
|
| `onExpandChange` | `(event: ExpandChangeEvent<T>) => void` | Called when expand/collapse changes |
|
|
110
120
|
| `onHoverChange` | `(event: HoverChangeEvent<T>) => void` | Called when hover zone changes |
|
|
111
121
|
|
|
122
|
+
### Sensor Config
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface SensorConfig {
|
|
126
|
+
/** Distance in pixels before drag starts (default: 8) */
|
|
127
|
+
activationDistance?: number
|
|
128
|
+
/** Delay in ms before drag starts (overrides distance) */
|
|
129
|
+
activationDelay?: number
|
|
130
|
+
/** Tolerance in px for delay-based activation (default: 5) */
|
|
131
|
+
tolerance?: number
|
|
132
|
+
/** Override the default long-press delay for touch sensors (default: 200ms) */
|
|
133
|
+
longPressDelay?: number
|
|
134
|
+
/** Trigger haptic feedback (vibration) on drag start for touch devices */
|
|
135
|
+
hapticFeedback?: boolean
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<BlockTree
|
|
141
|
+
sensors={{
|
|
142
|
+
longPressDelay: 300, // 300ms long-press for touch
|
|
143
|
+
hapticFeedback: true, // Vibrate on drag start
|
|
144
|
+
}}
|
|
145
|
+
...
|
|
146
|
+
/>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Animation Config
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
interface AnimationConfig {
|
|
153
|
+
/** Duration for expand/collapse animations in ms */
|
|
154
|
+
expandDuration?: number
|
|
155
|
+
/** Duration for drag overlay animation in ms */
|
|
156
|
+
dragOverlayDuration?: number
|
|
157
|
+
/** Easing function (CSS timing function, default: 'ease') */
|
|
158
|
+
easing?: string
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Event Types
|
|
163
|
+
|
|
164
|
+
All event types are exported and generic over your block type `T`.
|
|
165
|
+
|
|
166
|
+
#### DragStartEvent
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
interface DragStartEvent<T> {
|
|
170
|
+
block: T
|
|
171
|
+
blockId: string
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### DragMoveEvent
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
interface DragMoveEvent<T> {
|
|
179
|
+
block: T
|
|
180
|
+
blockId: string
|
|
181
|
+
overZone: string | null // Current hover zone ID
|
|
182
|
+
coordinates: { x: number; y: number }
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### DragEndEvent
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
interface DragEndEvent<T> {
|
|
190
|
+
block: T
|
|
191
|
+
blockId: string
|
|
192
|
+
targetZone: string | null // Zone where block was dropped
|
|
193
|
+
cancelled: boolean
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### BlockMoveEvent
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
interface BlockMoveEvent<T> {
|
|
201
|
+
block: T
|
|
202
|
+
from: BlockPosition // { parentId, index }
|
|
203
|
+
to: BlockPosition
|
|
204
|
+
blocks: T[] // Full block array after the move
|
|
205
|
+
movedIds: string[] // All moved block IDs (for multi-select)
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### MoveOperation
|
|
210
|
+
|
|
211
|
+
Passed to the `onBeforeMove` middleware:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
interface MoveOperation<T> {
|
|
215
|
+
block: T
|
|
216
|
+
from: BlockPosition
|
|
217
|
+
targetZone: string // Drop zone ID (e.g. "after-uuid", "into-uuid")
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### BlockAddEvent
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
interface BlockAddEvent<T> {
|
|
225
|
+
block: T
|
|
226
|
+
parentId: string | null
|
|
227
|
+
index: number
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### BlockDeleteEvent
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
interface BlockDeleteEvent<T> {
|
|
235
|
+
block: T
|
|
236
|
+
deletedIds: string[] // Block + all descendant IDs
|
|
237
|
+
parentId: string | null
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### ExpandChangeEvent
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
interface ExpandChangeEvent<T> {
|
|
245
|
+
block: T
|
|
246
|
+
blockId: string
|
|
247
|
+
expanded: boolean
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### HoverChangeEvent
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
interface HoverChangeEvent<T> {
|
|
255
|
+
zoneId: string | null
|
|
256
|
+
zoneType: 'before' | 'after' | 'into' | null
|
|
257
|
+
targetBlock: T | null
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
112
261
|
### Types
|
|
113
262
|
|
|
114
263
|
#### BaseBlock
|
|
@@ -150,6 +299,182 @@ interface ContainerRendererProps<T extends BaseBlock> extends BlockRendererProps
|
|
|
150
299
|
}
|
|
151
300
|
```
|
|
152
301
|
|
|
302
|
+
#### NestedBlock
|
|
303
|
+
|
|
304
|
+
Tree representation used by serialization helpers:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
type NestedBlock<T extends BaseBlock> = Omit<T, 'parentId' | 'order'> & {
|
|
308
|
+
children: NestedBlock<T>[]
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Serialization Helpers
|
|
313
|
+
|
|
314
|
+
Convert between flat block arrays and nested tree structures:
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
import { flatToNested, nestedToFlat, type NestedBlock } from 'dnd-block-tree'
|
|
318
|
+
|
|
319
|
+
// Flat array -> nested tree (e.g. for JSON export)
|
|
320
|
+
const nested: NestedBlock<MyBlock>[] = flatToNested(blocks)
|
|
321
|
+
|
|
322
|
+
// Nested tree -> flat array (e.g. for JSON import)
|
|
323
|
+
const flat: MyBlock[] = nestedToFlat(nested)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`flatToNested` groups by `parentId`, sorts siblings by `order`, and recursively builds `children` arrays. `parentId` and `order` are omitted from the output since they're structural.
|
|
327
|
+
|
|
328
|
+
`nestedToFlat` performs a DFS walk, assigning `parentId` and integer `order` on the way down.
|
|
329
|
+
|
|
330
|
+
## SSR Compatibility
|
|
331
|
+
|
|
332
|
+
For Next.js App Router or other SSR environments, use `BlockTreeSSR` to avoid hydration mismatches:
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
import { BlockTreeSSR } from 'dnd-block-tree'
|
|
336
|
+
|
|
337
|
+
function Page() {
|
|
338
|
+
return (
|
|
339
|
+
<BlockTreeSSR
|
|
340
|
+
blocks={blocks}
|
|
341
|
+
renderers={renderers}
|
|
342
|
+
containerTypes={CONTAINER_TYPES}
|
|
343
|
+
onChange={setBlocks}
|
|
344
|
+
fallback={<div>Loading tree...</div>}
|
|
345
|
+
/>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`BlockTreeSSR` renders the `fallback` (default: `null`) on the server, then mounts the full `BlockTree` after hydration via `useEffect`. All `BlockTree` props are passed through.
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
interface BlockTreeSSRProps<T, C> extends BlockTreeProps<T, C> {
|
|
354
|
+
fallback?: ReactNode
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Animation & Transitions
|
|
359
|
+
|
|
360
|
+
### Expand/Collapse Transitions
|
|
361
|
+
|
|
362
|
+
Pass an `AnimationConfig` to enable CSS transitions on container expand/collapse:
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
<BlockTree
|
|
366
|
+
animation={{
|
|
367
|
+
expandDuration: 200, // ms
|
|
368
|
+
easing: 'ease-in-out', // CSS timing function
|
|
369
|
+
}}
|
|
370
|
+
...
|
|
371
|
+
/>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### FLIP Reorder Animation
|
|
375
|
+
|
|
376
|
+
The `useLayoutAnimation` hook provides FLIP-based (First-Last-Invert-Play) animations for block reorder transitions. It's a standalone composable — not built into BlockTree:
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
import { useLayoutAnimation } from 'dnd-block-tree'
|
|
380
|
+
|
|
381
|
+
function MyTree() {
|
|
382
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
383
|
+
|
|
384
|
+
useLayoutAnimation(containerRef, {
|
|
385
|
+
duration: 200, // Transition duration in ms (default: 200)
|
|
386
|
+
easing: 'ease', // CSS easing (default: 'ease')
|
|
387
|
+
selector: '[data-block-id]', // Selector for animated children (default)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<div ref={containerRef}>
|
|
392
|
+
<BlockTree ... />
|
|
393
|
+
</div>
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
interface UseLayoutAnimationOptions {
|
|
400
|
+
duration?: number // default: 200
|
|
401
|
+
easing?: string // default: 'ease'
|
|
402
|
+
selector?: string // default: '[data-block-id]'
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Virtual Scrolling
|
|
407
|
+
|
|
408
|
+
Enable windowed rendering for large trees (1000+ blocks) with the `virtualize` prop:
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
<BlockTree
|
|
412
|
+
virtualize={{
|
|
413
|
+
itemHeight: 40, // Fixed height of each item in pixels
|
|
414
|
+
overscan: 5, // Extra items rendered outside viewport (default: 5)
|
|
415
|
+
}}
|
|
416
|
+
blocks={blocks}
|
|
417
|
+
...
|
|
418
|
+
/>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
When enabled, only visible blocks (plus overscan) are rendered. The tree is wrapped in a scrollable container with a spacer div maintaining correct total height.
|
|
422
|
+
|
|
423
|
+
**Limitations:** Fixed item height only. Variable height items are not supported.
|
|
424
|
+
|
|
425
|
+
### useVirtualTree Hook
|
|
426
|
+
|
|
427
|
+
For custom virtual scrolling implementations outside of BlockTree:
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
import { useVirtualTree } from 'dnd-block-tree'
|
|
431
|
+
|
|
432
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
433
|
+
const { visibleRange, totalHeight, offsetY } = useVirtualTree({
|
|
434
|
+
containerRef,
|
|
435
|
+
itemCount: 1000,
|
|
436
|
+
itemHeight: 40,
|
|
437
|
+
overscan: 5,
|
|
438
|
+
})
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
interface UseVirtualTreeOptions {
|
|
443
|
+
containerRef: React.RefObject<HTMLElement | null>
|
|
444
|
+
itemCount: number
|
|
445
|
+
itemHeight: number
|
|
446
|
+
overscan?: number // default: 5
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
interface UseVirtualTreeResult {
|
|
450
|
+
visibleRange: { start: number; end: number }
|
|
451
|
+
totalHeight: number
|
|
452
|
+
offsetY: number
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## Touch & Mobile
|
|
457
|
+
|
|
458
|
+
Touch support is built-in with sensible defaults (200ms long-press, 5px tolerance). For fine-tuning:
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
<BlockTree
|
|
462
|
+
sensors={{
|
|
463
|
+
longPressDelay: 300, // Override default 200ms touch activation
|
|
464
|
+
hapticFeedback: true, // Vibrate on drag start (uses navigator.vibrate)
|
|
465
|
+
}}
|
|
466
|
+
...
|
|
467
|
+
/>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
The `triggerHaptic` utility is also exported for use in custom components:
|
|
471
|
+
|
|
472
|
+
```tsx
|
|
473
|
+
import { triggerHaptic } from 'dnd-block-tree'
|
|
474
|
+
|
|
475
|
+
triggerHaptic(10) // Vibrate for 10ms (default)
|
|
476
|
+
```
|
|
477
|
+
|
|
153
478
|
## Undo/Redo
|
|
154
479
|
|
|
155
480
|
The `useBlockHistory` hook provides undo/redo support as a composable layer on top of `BlockTree`:
|
|
@@ -222,6 +547,112 @@ Limit nesting depth to prevent deeply nested trees:
|
|
|
222
547
|
- `maxDepth={2}` - blocks can nest one level inside containers
|
|
223
548
|
- When a move would exceed the limit, the drop zone is rejected and the move is a no-op
|
|
224
549
|
|
|
550
|
+
## Move Middleware (`onBeforeMove`)
|
|
551
|
+
|
|
552
|
+
The `onBeforeMove` callback intercepts moves before they are committed. You can use it to validate, transform, or cancel moves:
|
|
553
|
+
|
|
554
|
+
```tsx
|
|
555
|
+
<BlockTree
|
|
556
|
+
blocks={blocks}
|
|
557
|
+
onChange={setBlocks}
|
|
558
|
+
onBeforeMove={(operation) => {
|
|
559
|
+
// Cancel: return false to prevent the move
|
|
560
|
+
if (operation.block.type === 'locked') {
|
|
561
|
+
return false
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Transform: change the target zone
|
|
565
|
+
if (operation.targetZone.startsWith('into-') && someCondition) {
|
|
566
|
+
return { ...operation, targetZone: `after-${extractBlockId(operation.targetZone)}` }
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Allow: return void/undefined to proceed as-is
|
|
570
|
+
}}
|
|
571
|
+
/>
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
The middleware receives a `MoveOperation` with the block, its original position (`from`), and the target drop zone. Returning:
|
|
575
|
+
- `false` cancels the move entirely
|
|
576
|
+
- A modified `MoveOperation` transforms the move (e.g. redirect to a different zone)
|
|
577
|
+
- `void` / `undefined` allows the move as-is
|
|
578
|
+
|
|
579
|
+
## Fractional Indexing
|
|
580
|
+
|
|
581
|
+
By default, siblings are reindexed `0, 1, 2, ...` on every move. For collaborative or CRDT-compatible scenarios, use fractional indexing:
|
|
582
|
+
|
|
583
|
+
```tsx
|
|
584
|
+
import { BlockTree, initFractionalOrder } from 'dnd-block-tree'
|
|
585
|
+
|
|
586
|
+
// Convert existing blocks to fractional ordering
|
|
587
|
+
const [blocks, setBlocks] = useState(() => initFractionalOrder(initialBlocks))
|
|
588
|
+
|
|
589
|
+
<BlockTree
|
|
590
|
+
blocks={blocks}
|
|
591
|
+
onChange={setBlocks}
|
|
592
|
+
orderingStrategy="fractional"
|
|
593
|
+
/>
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
With fractional ordering, only the moved block receives a new `order` value (a lexicographically sortable string key). Siblings are never reindexed, making it safe for concurrent edits.
|
|
597
|
+
|
|
598
|
+
Related utilities:
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
import {
|
|
602
|
+
generateKeyBetween, // Generate a key between two existing keys
|
|
603
|
+
generateNKeysBetween, // Generate N keys between two existing keys
|
|
604
|
+
generateInitialKeys, // Generate N evenly-spaced initial keys
|
|
605
|
+
initFractionalOrder, // Convert integer-ordered blocks to fractional
|
|
606
|
+
compareFractionalKeys, // Compare two fractional keys
|
|
607
|
+
} from 'dnd-block-tree'
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
## Collision Detection
|
|
611
|
+
|
|
612
|
+
The library ships with three collision detection strategies:
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
import {
|
|
616
|
+
weightedVerticalCollision, // Default: edge-distance based, depth-aware
|
|
617
|
+
closestCenterCollision, // Simple closest-center algorithm
|
|
618
|
+
createStickyCollision, // Wraps any strategy with hysteresis to prevent flickering
|
|
619
|
+
} from 'dnd-block-tree'
|
|
620
|
+
|
|
621
|
+
// Use a custom collision strategy
|
|
622
|
+
<BlockTree collisionDetection={closestCenterCollision} ... />
|
|
623
|
+
|
|
624
|
+
// Or use the sticky wrapper with a custom threshold (px)
|
|
625
|
+
const collision = createStickyCollision(20)
|
|
626
|
+
<BlockTree collisionDetection={collision} ... />
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
You can also pass any `CollisionDetection` function from `@dnd-kit/core`.
|
|
630
|
+
|
|
631
|
+
### Snapshotted Zone Rects
|
|
632
|
+
|
|
633
|
+
`createStickyCollision` accepts an optional `SnapshotRectsRef` — a ref to a `Map<string, DOMRect>` of frozen zone positions. When provided, collision detection uses these snapshots instead of live DOM measurements, preventing feedback loops caused by the in-flow ghost preview shifting zone positions.
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
import { createStickyCollision, type SnapshotRectsRef } from 'dnd-block-tree'
|
|
637
|
+
|
|
638
|
+
const snapshotRef: SnapshotRectsRef = { current: null }
|
|
639
|
+
const collision = createStickyCollision(20, snapshotRef)
|
|
640
|
+
|
|
641
|
+
// Snapshot all zone rects after drag starts:
|
|
642
|
+
snapshotRef.current = new Map(
|
|
643
|
+
[...document.querySelectorAll('[data-zone-id]')].map(el => [
|
|
644
|
+
el.getAttribute('data-zone-id')!,
|
|
645
|
+
el.getBoundingClientRect(),
|
|
646
|
+
])
|
|
647
|
+
)
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
`BlockTree` handles this lifecycle automatically — zones are snapshotted on drag start and re-measured via `requestAnimationFrame` after each ghost position commit.
|
|
651
|
+
|
|
652
|
+
### Cross-Depth Hysteresis
|
|
653
|
+
|
|
654
|
+
The sticky collision uses a reduced threshold (25% of normal) when switching between zones at different indentation levels. This makes it easy to drag blocks in and out of containers while still preventing flickering between same-depth adjacent zones.
|
|
655
|
+
|
|
225
656
|
## Type Safety
|
|
226
657
|
|
|
227
658
|
The library provides automatic type inference for container vs non-container renderers:
|
|
@@ -245,20 +676,57 @@ const renderers: BlockRenderers<MyBlock, typeof CONTAINER_TYPES> = {
|
|
|
245
676
|
|
|
246
677
|
## Utilities
|
|
247
678
|
|
|
248
|
-
The library exports
|
|
679
|
+
The library exports utility functions for tree manipulation, ID generation, and zone parsing:
|
|
249
680
|
|
|
250
681
|
```typescript
|
|
251
682
|
import {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
683
|
+
// Tree operations
|
|
684
|
+
computeNormalizedIndex, // Convert flat array to { byId, byParent } index
|
|
685
|
+
buildOrderedBlocks, // Convert index back to ordered flat array
|
|
686
|
+
reparentBlockIndex, // Move a single block to a new position
|
|
687
|
+
reparentMultipleBlocks, // Move multiple blocks preserving relative order
|
|
688
|
+
getDescendantIds, // Get all descendant IDs of a block (Set)
|
|
689
|
+
deleteBlockAndDescendants, // Remove a block and all its descendants from index
|
|
690
|
+
getBlockDepth, // Compute depth of a block (root = 1)
|
|
691
|
+
getSubtreeDepth, // Max depth of a subtree (leaf = 1)
|
|
692
|
+
|
|
693
|
+
// Serialization
|
|
694
|
+
flatToNested, // Convert flat block array to nested tree
|
|
695
|
+
nestedToFlat, // Convert nested tree to flat block array
|
|
696
|
+
|
|
697
|
+
// ID / zone helpers
|
|
698
|
+
generateId, // Generate unique block IDs
|
|
699
|
+
extractUUID, // Extract block ID from zone ID string
|
|
700
|
+
getDropZoneType, // Parse zone type: 'before' | 'after' | 'into'
|
|
701
|
+
extractBlockId, // Extract block ID from zone ID (alias)
|
|
702
|
+
|
|
703
|
+
// Touch
|
|
704
|
+
triggerHaptic, // Trigger haptic feedback (navigator.vibrate)
|
|
705
|
+
|
|
706
|
+
// Fractional indexing
|
|
707
|
+
generateKeyBetween, // Generate a fractional key between two keys
|
|
708
|
+
generateNKeysBetween, // Generate N keys between two existing keys
|
|
709
|
+
generateInitialKeys, // Generate N evenly-spaced initial keys
|
|
710
|
+
initFractionalOrder, // Convert integer-ordered blocks to fractional
|
|
711
|
+
compareFractionalKeys, // Compare two fractional keys
|
|
712
|
+
|
|
713
|
+
// Collision detection
|
|
714
|
+
weightedVerticalCollision, // Edge-distance collision, depth-aware
|
|
715
|
+
closestCenterCollision, // Simple closest-center collision
|
|
716
|
+
createStickyCollision, // Hysteresis wrapper with snapshot support
|
|
717
|
+
type SnapshotRectsRef, // Ref type for frozen zone rects
|
|
718
|
+
|
|
719
|
+
// Hooks
|
|
720
|
+
useBlockHistory, // Undo/redo state management
|
|
721
|
+
useLayoutAnimation, // FLIP-based reorder animations
|
|
722
|
+
useVirtualTree, // Virtual scrolling primitives
|
|
723
|
+
|
|
724
|
+
// Components
|
|
725
|
+
BlockTree, // Main drag-and-drop tree component
|
|
726
|
+
BlockTreeSSR, // SSR-safe wrapper
|
|
727
|
+
TreeRenderer, // Recursive tree renderer
|
|
728
|
+
DropZone, // Individual drop zone
|
|
729
|
+
DragOverlay, // Drag overlay wrapper
|
|
262
730
|
} from 'dnd-block-tree'
|
|
263
731
|
```
|
|
264
732
|
|