dnd-block-tree 0.4.0 → 0.5.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 +453 -12
- package/dist/index.d.mts +104 -3
- package/dist/index.d.ts +104 -3
- package/dist/index.js +250 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +246 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ A headless React library for building hierarchical drag-and-drop interfaces. Bri
|
|
|
15
15
|
- **Stable Drop Zones** - Zones render based on original block positions, not preview state, ensuring consistent drop targets during drag
|
|
16
16
|
- **Ghost Preview** - Semi-transparent preview shows where blocks will land without affecting zone positions
|
|
17
17
|
- **Depth-Aware Collision** - Smart algorithm prefers nested zones when cursor is at indented levels, with hysteresis to prevent flickering
|
|
18
|
-
- **Mobile & Touch Support** - Separate touch/pointer activation constraints
|
|
18
|
+
- **Mobile & Touch Support** - Separate touch/pointer activation constraints with configurable `longPressDelay` and optional `hapticFeedback`
|
|
19
19
|
- **Snapshot-Based Computation** - State captured at drag start. All preview computations use snapshot, ensuring consistent behavior
|
|
20
20
|
- **Debounced Preview** - 150ms debounced virtual state for smooth drag previews without jitter
|
|
21
21
|
- **Customizable Drag Rules** - `canDrag` and `canDrop` filters for fine-grained control over drag behavior
|
|
@@ -25,6 +25,10 @@ A headless React library for building hierarchical drag-and-drop interfaces. Bri
|
|
|
25
25
|
- **Undo/Redo** - Composable `useBlockHistory` hook with past/future stacks for state history management
|
|
26
26
|
- **Lifecycle Callbacks** - `onBlockAdd`, `onBlockDelete`, `onBlockMove`, `onBeforeMove` middleware, and more
|
|
27
27
|
- **Fractional Indexing** - Opt-in CRDT-compatible ordering via `orderingStrategy: 'fractional'`
|
|
28
|
+
- **Serialization** - `flatToNested` / `nestedToFlat` converters for flat array ↔ nested tree transforms
|
|
29
|
+
- **SSR Compatible** - `BlockTreeSSR` wrapper for hydration-safe rendering in Next.js and other SSR environments
|
|
30
|
+
- **Animation Support** - CSS expand/collapse transitions via `AnimationConfig` and FLIP reorder animations via `useLayoutAnimation`
|
|
31
|
+
- **Virtual Scrolling** - Windowed rendering for large trees (1000+ blocks) via `virtualize` prop
|
|
28
32
|
|
|
29
33
|
## Installation
|
|
30
34
|
|
|
@@ -83,12 +87,17 @@ function App() {
|
|
|
83
87
|
| `showDropPreview` | `boolean` | `true` | Show live preview of block at drop position |
|
|
84
88
|
| `canDrag` | `(block: T) => boolean` | - | Filter which blocks can be dragged |
|
|
85
89
|
| `canDrop` | `(block, zone, target) => boolean` | - | Filter valid drop targets |
|
|
90
|
+
| `collisionDetection` | `CollisionDetection` | sticky | Custom collision detection algorithm (from `@dnd-kit/core`) |
|
|
91
|
+
| `sensors` | `SensorConfig` | - | Sensor configuration (see [Sensor Config](#sensor-config)) |
|
|
92
|
+
| `animation` | `AnimationConfig` | - | Animation configuration (see [Animation](#animation--transitions)) |
|
|
86
93
|
| `maxDepth` | `number` | - | Maximum nesting depth (1 = flat, 2 = one level, etc.) |
|
|
87
94
|
| `keyboardNavigation` | `boolean` | `false` | Enable keyboard navigation with arrow keys |
|
|
88
95
|
| `multiSelect` | `boolean` | `false` | Enable multi-select with Cmd/Ctrl+Click and Shift+Click |
|
|
89
96
|
| `selectedIds` | `Set<string>` | - | Externally-controlled selected IDs (for multi-select) |
|
|
90
97
|
| `onSelectionChange` | `(ids: Set<string>) => void` | - | Called when selection changes |
|
|
91
98
|
| `orderingStrategy` | `'integer' \| 'fractional'` | `'integer'` | Sibling ordering strategy |
|
|
99
|
+
| `initialExpanded` | `string[] \| 'all' \| 'none'` | `'all'` | Initially expanded container IDs |
|
|
100
|
+
| `virtualize` | `{ itemHeight: number; overscan?: number }` | - | Enable virtual scrolling (see [Virtual Scrolling](#virtual-scrolling)) |
|
|
92
101
|
| `className` | `string` | - | Root container class |
|
|
93
102
|
| `dropZoneClassName` | `string` | - | Drop zone class |
|
|
94
103
|
| `dropZoneActiveClassName` | `string` | - | Active drop zone class |
|
|
@@ -109,6 +118,145 @@ function App() {
|
|
|
109
118
|
| `onExpandChange` | `(event: ExpandChangeEvent<T>) => void` | Called when expand/collapse changes |
|
|
110
119
|
| `onHoverChange` | `(event: HoverChangeEvent<T>) => void` | Called when hover zone changes |
|
|
111
120
|
|
|
121
|
+
### Sensor Config
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
interface SensorConfig {
|
|
125
|
+
/** Distance in pixels before drag starts (default: 8) */
|
|
126
|
+
activationDistance?: number
|
|
127
|
+
/** Delay in ms before drag starts (overrides distance) */
|
|
128
|
+
activationDelay?: number
|
|
129
|
+
/** Tolerance in px for delay-based activation (default: 5) */
|
|
130
|
+
tolerance?: number
|
|
131
|
+
/** Override the default long-press delay for touch sensors (default: 200ms) */
|
|
132
|
+
longPressDelay?: number
|
|
133
|
+
/** Trigger haptic feedback (vibration) on drag start for touch devices */
|
|
134
|
+
hapticFeedback?: boolean
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
<BlockTree
|
|
140
|
+
sensors={{
|
|
141
|
+
longPressDelay: 300, // 300ms long-press for touch
|
|
142
|
+
hapticFeedback: true, // Vibrate on drag start
|
|
143
|
+
}}
|
|
144
|
+
...
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Animation Config
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
interface AnimationConfig {
|
|
152
|
+
/** Duration for expand/collapse animations in ms */
|
|
153
|
+
expandDuration?: number
|
|
154
|
+
/** Duration for drag overlay animation in ms */
|
|
155
|
+
dragOverlayDuration?: number
|
|
156
|
+
/** Easing function (CSS timing function, default: 'ease') */
|
|
157
|
+
easing?: string
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Event Types
|
|
162
|
+
|
|
163
|
+
All event types are exported and generic over your block type `T`.
|
|
164
|
+
|
|
165
|
+
#### DragStartEvent
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
interface DragStartEvent<T> {
|
|
169
|
+
block: T
|
|
170
|
+
blockId: string
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### DragMoveEvent
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
interface DragMoveEvent<T> {
|
|
178
|
+
block: T
|
|
179
|
+
blockId: string
|
|
180
|
+
overZone: string | null // Current hover zone ID
|
|
181
|
+
coordinates: { x: number; y: number }
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### DragEndEvent
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
interface DragEndEvent<T> {
|
|
189
|
+
block: T
|
|
190
|
+
blockId: string
|
|
191
|
+
targetZone: string | null // Zone where block was dropped
|
|
192
|
+
cancelled: boolean
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### BlockMoveEvent
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
interface BlockMoveEvent<T> {
|
|
200
|
+
block: T
|
|
201
|
+
from: BlockPosition // { parentId, index }
|
|
202
|
+
to: BlockPosition
|
|
203
|
+
blocks: T[] // Full block array after the move
|
|
204
|
+
movedIds: string[] // All moved block IDs (for multi-select)
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### MoveOperation
|
|
209
|
+
|
|
210
|
+
Passed to the `onBeforeMove` middleware:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
interface MoveOperation<T> {
|
|
214
|
+
block: T
|
|
215
|
+
from: BlockPosition
|
|
216
|
+
targetZone: string // Drop zone ID (e.g. "after-uuid", "into-uuid")
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### BlockAddEvent
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
interface BlockAddEvent<T> {
|
|
224
|
+
block: T
|
|
225
|
+
parentId: string | null
|
|
226
|
+
index: number
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### BlockDeleteEvent
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
interface BlockDeleteEvent<T> {
|
|
234
|
+
block: T
|
|
235
|
+
deletedIds: string[] // Block + all descendant IDs
|
|
236
|
+
parentId: string | null
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### ExpandChangeEvent
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
interface ExpandChangeEvent<T> {
|
|
244
|
+
block: T
|
|
245
|
+
blockId: string
|
|
246
|
+
expanded: boolean
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### HoverChangeEvent
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
interface HoverChangeEvent<T> {
|
|
254
|
+
zoneId: string | null
|
|
255
|
+
zoneType: 'before' | 'after' | 'into' | null
|
|
256
|
+
targetBlock: T | null
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
112
260
|
### Types
|
|
113
261
|
|
|
114
262
|
#### BaseBlock
|
|
@@ -150,6 +298,182 @@ interface ContainerRendererProps<T extends BaseBlock> extends BlockRendererProps
|
|
|
150
298
|
}
|
|
151
299
|
```
|
|
152
300
|
|
|
301
|
+
#### NestedBlock
|
|
302
|
+
|
|
303
|
+
Tree representation used by serialization helpers:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
type NestedBlock<T extends BaseBlock> = Omit<T, 'parentId' | 'order'> & {
|
|
307
|
+
children: NestedBlock<T>[]
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Serialization Helpers
|
|
312
|
+
|
|
313
|
+
Convert between flat block arrays and nested tree structures:
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
import { flatToNested, nestedToFlat, type NestedBlock } from 'dnd-block-tree'
|
|
317
|
+
|
|
318
|
+
// Flat array -> nested tree (e.g. for JSON export)
|
|
319
|
+
const nested: NestedBlock<MyBlock>[] = flatToNested(blocks)
|
|
320
|
+
|
|
321
|
+
// Nested tree -> flat array (e.g. for JSON import)
|
|
322
|
+
const flat: MyBlock[] = nestedToFlat(nested)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`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.
|
|
326
|
+
|
|
327
|
+
`nestedToFlat` performs a DFS walk, assigning `parentId` and integer `order` on the way down.
|
|
328
|
+
|
|
329
|
+
## SSR Compatibility
|
|
330
|
+
|
|
331
|
+
For Next.js App Router or other SSR environments, use `BlockTreeSSR` to avoid hydration mismatches:
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
import { BlockTreeSSR } from 'dnd-block-tree'
|
|
335
|
+
|
|
336
|
+
function Page() {
|
|
337
|
+
return (
|
|
338
|
+
<BlockTreeSSR
|
|
339
|
+
blocks={blocks}
|
|
340
|
+
renderers={renderers}
|
|
341
|
+
containerTypes={CONTAINER_TYPES}
|
|
342
|
+
onChange={setBlocks}
|
|
343
|
+
fallback={<div>Loading tree...</div>}
|
|
344
|
+
/>
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`BlockTreeSSR` renders the `fallback` (default: `null`) on the server, then mounts the full `BlockTree` after hydration via `useEffect`. All `BlockTree` props are passed through.
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
interface BlockTreeSSRProps<T, C> extends BlockTreeProps<T, C> {
|
|
353
|
+
fallback?: ReactNode
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Animation & Transitions
|
|
358
|
+
|
|
359
|
+
### Expand/Collapse Transitions
|
|
360
|
+
|
|
361
|
+
Pass an `AnimationConfig` to enable CSS transitions on container expand/collapse:
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
<BlockTree
|
|
365
|
+
animation={{
|
|
366
|
+
expandDuration: 200, // ms
|
|
367
|
+
easing: 'ease-in-out', // CSS timing function
|
|
368
|
+
}}
|
|
369
|
+
...
|
|
370
|
+
/>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### FLIP Reorder Animation
|
|
374
|
+
|
|
375
|
+
The `useLayoutAnimation` hook provides FLIP-based (First-Last-Invert-Play) animations for block reorder transitions. It's a standalone composable — not built into BlockTree:
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
import { useLayoutAnimation } from 'dnd-block-tree'
|
|
379
|
+
|
|
380
|
+
function MyTree() {
|
|
381
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
382
|
+
|
|
383
|
+
useLayoutAnimation(containerRef, {
|
|
384
|
+
duration: 200, // Transition duration in ms (default: 200)
|
|
385
|
+
easing: 'ease', // CSS easing (default: 'ease')
|
|
386
|
+
selector: '[data-block-id]', // Selector for animated children (default)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<div ref={containerRef}>
|
|
391
|
+
<BlockTree ... />
|
|
392
|
+
</div>
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
interface UseLayoutAnimationOptions {
|
|
399
|
+
duration?: number // default: 200
|
|
400
|
+
easing?: string // default: 'ease'
|
|
401
|
+
selector?: string // default: '[data-block-id]'
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Virtual Scrolling
|
|
406
|
+
|
|
407
|
+
Enable windowed rendering for large trees (1000+ blocks) with the `virtualize` prop:
|
|
408
|
+
|
|
409
|
+
```tsx
|
|
410
|
+
<BlockTree
|
|
411
|
+
virtualize={{
|
|
412
|
+
itemHeight: 40, // Fixed height of each item in pixels
|
|
413
|
+
overscan: 5, // Extra items rendered outside viewport (default: 5)
|
|
414
|
+
}}
|
|
415
|
+
blocks={blocks}
|
|
416
|
+
...
|
|
417
|
+
/>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
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.
|
|
421
|
+
|
|
422
|
+
**Limitations:** Fixed item height only. Variable height items are not supported.
|
|
423
|
+
|
|
424
|
+
### useVirtualTree Hook
|
|
425
|
+
|
|
426
|
+
For custom virtual scrolling implementations outside of BlockTree:
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
import { useVirtualTree } from 'dnd-block-tree'
|
|
430
|
+
|
|
431
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
432
|
+
const { visibleRange, totalHeight, offsetY } = useVirtualTree({
|
|
433
|
+
containerRef,
|
|
434
|
+
itemCount: 1000,
|
|
435
|
+
itemHeight: 40,
|
|
436
|
+
overscan: 5,
|
|
437
|
+
})
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
interface UseVirtualTreeOptions {
|
|
442
|
+
containerRef: React.RefObject<HTMLElement | null>
|
|
443
|
+
itemCount: number
|
|
444
|
+
itemHeight: number
|
|
445
|
+
overscan?: number // default: 5
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
interface UseVirtualTreeResult {
|
|
449
|
+
visibleRange: { start: number; end: number }
|
|
450
|
+
totalHeight: number
|
|
451
|
+
offsetY: number
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Touch & Mobile
|
|
456
|
+
|
|
457
|
+
Touch support is built-in with sensible defaults (200ms long-press, 5px tolerance). For fine-tuning:
|
|
458
|
+
|
|
459
|
+
```tsx
|
|
460
|
+
<BlockTree
|
|
461
|
+
sensors={{
|
|
462
|
+
longPressDelay: 300, // Override default 200ms touch activation
|
|
463
|
+
hapticFeedback: true, // Vibrate on drag start (uses navigator.vibrate)
|
|
464
|
+
}}
|
|
465
|
+
...
|
|
466
|
+
/>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
The `triggerHaptic` utility is also exported for use in custom components:
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
import { triggerHaptic } from 'dnd-block-tree'
|
|
473
|
+
|
|
474
|
+
triggerHaptic(10) // Vibrate for 10ms (default)
|
|
475
|
+
```
|
|
476
|
+
|
|
153
477
|
## Undo/Redo
|
|
154
478
|
|
|
155
479
|
The `useBlockHistory` hook provides undo/redo support as a composable layer on top of `BlockTree`:
|
|
@@ -222,6 +546,87 @@ Limit nesting depth to prevent deeply nested trees:
|
|
|
222
546
|
- `maxDepth={2}` - blocks can nest one level inside containers
|
|
223
547
|
- When a move would exceed the limit, the drop zone is rejected and the move is a no-op
|
|
224
548
|
|
|
549
|
+
## Move Middleware (`onBeforeMove`)
|
|
550
|
+
|
|
551
|
+
The `onBeforeMove` callback intercepts moves before they are committed. You can use it to validate, transform, or cancel moves:
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
<BlockTree
|
|
555
|
+
blocks={blocks}
|
|
556
|
+
onChange={setBlocks}
|
|
557
|
+
onBeforeMove={(operation) => {
|
|
558
|
+
// Cancel: return false to prevent the move
|
|
559
|
+
if (operation.block.type === 'locked') {
|
|
560
|
+
return false
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Transform: change the target zone
|
|
564
|
+
if (operation.targetZone.startsWith('into-') && someCondition) {
|
|
565
|
+
return { ...operation, targetZone: `after-${extractBlockId(operation.targetZone)}` }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Allow: return void/undefined to proceed as-is
|
|
569
|
+
}}
|
|
570
|
+
/>
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
The middleware receives a `MoveOperation` with the block, its original position (`from`), and the target drop zone. Returning:
|
|
574
|
+
- `false` cancels the move entirely
|
|
575
|
+
- A modified `MoveOperation` transforms the move (e.g. redirect to a different zone)
|
|
576
|
+
- `void` / `undefined` allows the move as-is
|
|
577
|
+
|
|
578
|
+
## Fractional Indexing
|
|
579
|
+
|
|
580
|
+
By default, siblings are reindexed `0, 1, 2, ...` on every move. For collaborative or CRDT-compatible scenarios, use fractional indexing:
|
|
581
|
+
|
|
582
|
+
```tsx
|
|
583
|
+
import { BlockTree, initFractionalOrder } from 'dnd-block-tree'
|
|
584
|
+
|
|
585
|
+
// Convert existing blocks to fractional ordering
|
|
586
|
+
const [blocks, setBlocks] = useState(() => initFractionalOrder(initialBlocks))
|
|
587
|
+
|
|
588
|
+
<BlockTree
|
|
589
|
+
blocks={blocks}
|
|
590
|
+
onChange={setBlocks}
|
|
591
|
+
orderingStrategy="fractional"
|
|
592
|
+
/>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
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.
|
|
596
|
+
|
|
597
|
+
Related utilities:
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
import {
|
|
601
|
+
generateKeyBetween, // Generate a key between two existing keys
|
|
602
|
+
generateNKeysBetween, // Generate N keys between two existing keys
|
|
603
|
+
generateInitialKeys, // Generate N evenly-spaced initial keys
|
|
604
|
+
initFractionalOrder, // Convert integer-ordered blocks to fractional
|
|
605
|
+
compareFractionalKeys, // Compare two fractional keys
|
|
606
|
+
} from 'dnd-block-tree'
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## Collision Detection
|
|
610
|
+
|
|
611
|
+
The library ships with three collision detection strategies:
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
import {
|
|
615
|
+
weightedVerticalCollision, // Default: edge-distance based, depth-aware
|
|
616
|
+
closestCenterCollision, // Simple closest-center algorithm
|
|
617
|
+
createStickyCollision, // Wraps any strategy with hysteresis to prevent flickering
|
|
618
|
+
} from 'dnd-block-tree'
|
|
619
|
+
|
|
620
|
+
// Use a custom collision strategy
|
|
621
|
+
<BlockTree collisionDetection={closestCenterCollision} ... />
|
|
622
|
+
|
|
623
|
+
// Or use the sticky wrapper with a custom threshold (px)
|
|
624
|
+
const collision = createStickyCollision(20)
|
|
625
|
+
<BlockTree collisionDetection={collision} ... />
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
You can also pass any `CollisionDetection` function from `@dnd-kit/core`.
|
|
629
|
+
|
|
225
630
|
## Type Safety
|
|
226
631
|
|
|
227
632
|
The library provides automatic type inference for container vs non-container renderers:
|
|
@@ -245,20 +650,56 @@ const renderers: BlockRenderers<MyBlock, typeof CONTAINER_TYPES> = {
|
|
|
245
650
|
|
|
246
651
|
## Utilities
|
|
247
652
|
|
|
248
|
-
The library exports
|
|
653
|
+
The library exports utility functions for tree manipulation, ID generation, and zone parsing:
|
|
249
654
|
|
|
250
655
|
```typescript
|
|
251
656
|
import {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
657
|
+
// Tree operations
|
|
658
|
+
computeNormalizedIndex, // Convert flat array to { byId, byParent } index
|
|
659
|
+
buildOrderedBlocks, // Convert index back to ordered flat array
|
|
660
|
+
reparentBlockIndex, // Move a single block to a new position
|
|
661
|
+
reparentMultipleBlocks, // Move multiple blocks preserving relative order
|
|
662
|
+
getDescendantIds, // Get all descendant IDs of a block (Set)
|
|
663
|
+
deleteBlockAndDescendants, // Remove a block and all its descendants from index
|
|
664
|
+
getBlockDepth, // Compute depth of a block (root = 1)
|
|
665
|
+
getSubtreeDepth, // Max depth of a subtree (leaf = 1)
|
|
666
|
+
|
|
667
|
+
// Serialization
|
|
668
|
+
flatToNested, // Convert flat block array to nested tree
|
|
669
|
+
nestedToFlat, // Convert nested tree to flat block array
|
|
670
|
+
|
|
671
|
+
// ID / zone helpers
|
|
672
|
+
generateId, // Generate unique block IDs
|
|
673
|
+
extractUUID, // Extract block ID from zone ID string
|
|
674
|
+
getDropZoneType, // Parse zone type: 'before' | 'after' | 'into'
|
|
675
|
+
extractBlockId, // Extract block ID from zone ID (alias)
|
|
676
|
+
|
|
677
|
+
// Touch
|
|
678
|
+
triggerHaptic, // Trigger haptic feedback (navigator.vibrate)
|
|
679
|
+
|
|
680
|
+
// Fractional indexing
|
|
681
|
+
generateKeyBetween, // Generate a fractional key between two keys
|
|
682
|
+
generateNKeysBetween, // Generate N keys between two existing keys
|
|
683
|
+
generateInitialKeys, // Generate N evenly-spaced initial keys
|
|
684
|
+
initFractionalOrder, // Convert integer-ordered blocks to fractional
|
|
685
|
+
compareFractionalKeys, // Compare two fractional keys
|
|
686
|
+
|
|
687
|
+
// Collision detection
|
|
688
|
+
weightedVerticalCollision, // Edge-distance collision, depth-aware
|
|
689
|
+
closestCenterCollision, // Simple closest-center collision
|
|
690
|
+
createStickyCollision, // Hysteresis wrapper to prevent flickering
|
|
691
|
+
|
|
692
|
+
// Hooks
|
|
693
|
+
useBlockHistory, // Undo/redo state management
|
|
694
|
+
useLayoutAnimation, // FLIP-based reorder animations
|
|
695
|
+
useVirtualTree, // Virtual scrolling primitives
|
|
696
|
+
|
|
697
|
+
// Components
|
|
698
|
+
BlockTree, // Main drag-and-drop tree component
|
|
699
|
+
BlockTreeSSR, // SSR-safe wrapper
|
|
700
|
+
TreeRenderer, // Recursive tree renderer
|
|
701
|
+
DropZone, // Individual drop zone
|
|
702
|
+
DragOverlay, // Drag overlay wrapper
|
|
262
703
|
} from 'dnd-block-tree'
|
|
263
704
|
```
|
|
264
705
|
|
package/dist/index.d.mts
CHANGED
|
@@ -213,6 +213,10 @@ interface SensorConfig {
|
|
|
213
213
|
activationDistance?: number;
|
|
214
214
|
activationDelay?: number;
|
|
215
215
|
tolerance?: number;
|
|
216
|
+
/** Override the default long-press delay (200ms) for touch sensors */
|
|
217
|
+
longPressDelay?: number;
|
|
218
|
+
/** Trigger haptic feedback (vibration) on drag start for touch devices */
|
|
219
|
+
hapticFeedback?: boolean;
|
|
216
220
|
}
|
|
217
221
|
/**
|
|
218
222
|
* Drop zone configuration
|
|
@@ -466,12 +470,19 @@ interface BlockTreeProps<T extends BaseBlock, C extends readonly T['type'][] = r
|
|
|
466
470
|
selectedIds?: Set<string>;
|
|
467
471
|
/** Called when selection changes (for multi-select) */
|
|
468
472
|
onSelectionChange?: (selectedIds: Set<string>) => void;
|
|
473
|
+
/** Enable virtual scrolling for large trees (fixed item height only) */
|
|
474
|
+
virtualize?: {
|
|
475
|
+
/** Fixed height of each item in pixels */
|
|
476
|
+
itemHeight: number;
|
|
477
|
+
/** Number of extra items to render outside the visible range (default: 5) */
|
|
478
|
+
overscan?: number;
|
|
479
|
+
};
|
|
469
480
|
}
|
|
470
481
|
/**
|
|
471
482
|
* Main BlockTree component
|
|
472
483
|
* Provides drag-and-drop functionality for hierarchical block structures
|
|
473
484
|
*/
|
|
474
|
-
declare function BlockTree<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]>({ blocks, renderers, containerTypes, onChange, dragOverlay, activationDistance, previewDebounce, className, dropZoneClassName, dropZoneActiveClassName, indentClassName, showDropPreview, onDragStart, onDragMove, onDragEnd, onDragCancel, onBeforeMove, onBlockMove, onExpandChange, onHoverChange, canDrag, canDrop, collisionDetection, sensors: sensorConfig, initialExpanded, orderingStrategy, maxDepth, keyboardNavigation, multiSelect, selectedIds: externalSelectedIds, onSelectionChange, }: BlockTreeProps<T, C>): react_jsx_runtime.JSX.Element;
|
|
485
|
+
declare function BlockTree<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]>({ blocks, renderers, containerTypes, onChange, dragOverlay, activationDistance, previewDebounce, className, dropZoneClassName, dropZoneActiveClassName, indentClassName, showDropPreview, onDragStart, onDragMove, onDragEnd, onDragCancel, onBeforeMove, onBlockMove, onExpandChange, onHoverChange, canDrag, canDrop, collisionDetection, sensors: sensorConfig, animation, initialExpanded, orderingStrategy, maxDepth, keyboardNavigation, multiSelect, selectedIds: externalSelectedIds, onSelectionChange, virtualize, }: BlockTreeProps<T, C>): react_jsx_runtime.JSX.Element;
|
|
475
486
|
|
|
476
487
|
interface TreeRendererProps<T extends BaseBlock> {
|
|
477
488
|
blocks: T[];
|
|
@@ -502,11 +513,15 @@ interface TreeRendererProps<T extends BaseBlock> {
|
|
|
502
513
|
selectedIds?: Set<string>;
|
|
503
514
|
/** Click handler for multi-select */
|
|
504
515
|
onBlockClick?: (blockId: string, event: React.MouseEvent) => void;
|
|
516
|
+
/** Animation configuration */
|
|
517
|
+
animation?: AnimationConfig;
|
|
518
|
+
/** When virtual scrolling is active, only render blocks in this set */
|
|
519
|
+
virtualVisibleIds?: Set<string> | null;
|
|
505
520
|
}
|
|
506
521
|
/**
|
|
507
522
|
* Recursive tree renderer with smart drop zones
|
|
508
523
|
*/
|
|
509
|
-
declare function TreeRenderer<T extends BaseBlock>({ blocks, blocksByParent, parentId, activeId, expandedMap, renderers, containerTypes, onHover, onToggleExpand, depth, dropZoneClassName, dropZoneActiveClassName, indentClassName, rootClassName, canDrag, previewPosition, draggedBlock, focusedId, selectedIds, onBlockClick, }: TreeRendererProps<T>): react_jsx_runtime.JSX.Element;
|
|
524
|
+
declare function TreeRenderer<T extends BaseBlock>({ blocks, blocksByParent, parentId, activeId, expandedMap, renderers, containerTypes, onHover, onToggleExpand, depth, dropZoneClassName, dropZoneActiveClassName, indentClassName, rootClassName, canDrag, previewPosition, draggedBlock, focusedId, selectedIds, onBlockClick, animation, virtualVisibleIds, }: TreeRendererProps<T>): react_jsx_runtime.JSX.Element;
|
|
510
525
|
|
|
511
526
|
interface DropZoneProps {
|
|
512
527
|
id: string;
|
|
@@ -536,6 +551,17 @@ interface DragOverlayProps<T extends BaseBlock> {
|
|
|
536
551
|
*/
|
|
537
552
|
declare function DragOverlay<T extends BaseBlock>({ activeBlock, children, selectedCount, }: DragOverlayProps<T>): react_jsx_runtime.JSX.Element;
|
|
538
553
|
|
|
554
|
+
interface BlockTreeSSRProps<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]> extends BlockTreeProps<T, C> {
|
|
555
|
+
/** Content to render before hydration completes (default: null) */
|
|
556
|
+
fallback?: ReactNode;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Hydration-safe wrapper for BlockTree in SSR environments.
|
|
560
|
+
* Renders the fallback (or nothing) on the server and during initial hydration,
|
|
561
|
+
* then mounts the full BlockTree after the client has hydrated.
|
|
562
|
+
*/
|
|
563
|
+
declare function BlockTreeSSR<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]>({ fallback, ...props }: BlockTreeSSRProps<T, C>): react_jsx_runtime.JSX.Element;
|
|
564
|
+
|
|
539
565
|
/**
|
|
540
566
|
* Create block state context and hooks
|
|
541
567
|
*/
|
|
@@ -588,6 +614,58 @@ interface UseBlockHistoryResult<T extends BaseBlock> {
|
|
|
588
614
|
*/
|
|
589
615
|
declare function useBlockHistory<T extends BaseBlock>(initialBlocks: T[], options?: UseBlockHistoryOptions): UseBlockHistoryResult<T>;
|
|
590
616
|
|
|
617
|
+
interface UseLayoutAnimationOptions {
|
|
618
|
+
/** Duration of the transition in ms (default: 200) */
|
|
619
|
+
duration?: number;
|
|
620
|
+
/** CSS easing function (default: 'ease') */
|
|
621
|
+
easing?: string;
|
|
622
|
+
/** CSS selector for animated children (default: '[data-block-id]') */
|
|
623
|
+
selector?: string;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* FLIP-based layout animation hook for reorder transitions.
|
|
627
|
+
*
|
|
628
|
+
* Captures block positions before render (layout effect cleanup),
|
|
629
|
+
* computes the delta after render, and applies a CSS transform transition.
|
|
630
|
+
*
|
|
631
|
+
* Usage:
|
|
632
|
+
* ```tsx
|
|
633
|
+
* const containerRef = useRef<HTMLDivElement>(null)
|
|
634
|
+
* useLayoutAnimation(containerRef, { duration: 200 })
|
|
635
|
+
* return <div ref={containerRef}>...</div>
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
638
|
+
declare function useLayoutAnimation(containerRef: React.RefObject<HTMLElement | null>, options?: UseLayoutAnimationOptions): void;
|
|
639
|
+
|
|
640
|
+
interface UseVirtualTreeOptions {
|
|
641
|
+
/** Ref to the scrollable container element */
|
|
642
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
643
|
+
/** Total number of items in the tree */
|
|
644
|
+
itemCount: number;
|
|
645
|
+
/** Fixed height of each item in pixels */
|
|
646
|
+
itemHeight: number;
|
|
647
|
+
/** Number of extra items to render outside the visible range (default: 5) */
|
|
648
|
+
overscan?: number;
|
|
649
|
+
}
|
|
650
|
+
interface UseVirtualTreeResult {
|
|
651
|
+
/** Start and end indices of the visible range (inclusive) */
|
|
652
|
+
visibleRange: {
|
|
653
|
+
start: number;
|
|
654
|
+
end: number;
|
|
655
|
+
};
|
|
656
|
+
/** Total height of all items for the spacer div */
|
|
657
|
+
totalHeight: number;
|
|
658
|
+
/** Offset from top for the first rendered item */
|
|
659
|
+
offsetY: number;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Lightweight fixed-height virtual scrolling hook for tree lists.
|
|
663
|
+
*
|
|
664
|
+
* Tracks scroll position on the container and computes which items
|
|
665
|
+
* should be rendered based on the viewport and overscan.
|
|
666
|
+
*/
|
|
667
|
+
declare function useVirtualTree({ containerRef, itemCount, itemHeight, overscan, }: UseVirtualTreeOptions): UseVirtualTreeResult;
|
|
668
|
+
|
|
591
669
|
/**
|
|
592
670
|
* Clone a Map
|
|
593
671
|
*/
|
|
@@ -659,6 +737,29 @@ declare function debounce<Args extends unknown[]>(fn: (...args: Args) => void, d
|
|
|
659
737
|
* Generate a unique ID (simple implementation)
|
|
660
738
|
*/
|
|
661
739
|
declare function generateId(): string;
|
|
740
|
+
/**
|
|
741
|
+
* Trigger haptic feedback (vibration) on supported devices.
|
|
742
|
+
* Safe to call in any environment — no-ops when `navigator.vibrate` is unavailable.
|
|
743
|
+
*/
|
|
744
|
+
declare function triggerHaptic(durationMs?: number): void;
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* A nested/tree representation of a block.
|
|
748
|
+
* Omits `parentId` and `order` since they are reconstructed during flattening.
|
|
749
|
+
*/
|
|
750
|
+
type NestedBlock<T extends BaseBlock> = Omit<T, 'parentId' | 'order'> & {
|
|
751
|
+
children: NestedBlock<T>[];
|
|
752
|
+
};
|
|
753
|
+
/**
|
|
754
|
+
* Convert a flat block array into a nested tree structure.
|
|
755
|
+
* Groups blocks by parentId, sorts siblings by order, and recursively builds children arrays.
|
|
756
|
+
*/
|
|
757
|
+
declare function flatToNested<T extends BaseBlock>(blocks: T[]): NestedBlock<T>[];
|
|
758
|
+
/**
|
|
759
|
+
* Convert a nested tree structure back to a flat block array.
|
|
760
|
+
* DFS walk assigning parentId and integer order on the way down.
|
|
761
|
+
*/
|
|
762
|
+
declare function nestedToFlat<T extends BaseBlock>(nested: NestedBlock<T>[]): T[];
|
|
662
763
|
|
|
663
764
|
/**
|
|
664
765
|
* Fractional indexing utilities
|
|
@@ -710,4 +811,4 @@ declare function initFractionalOrder<T extends {
|
|
|
710
811
|
order: number | string;
|
|
711
812
|
}>(blocks: T[]): T[];
|
|
712
813
|
|
|
713
|
-
export { type AnimationConfig, type AutoExpandConfig, type BaseBlock, type BlockAction, type BlockAddEvent, type BlockDeleteEvent, type BlockIndex, type BlockMoveEvent, type BlockPosition, type BlockRendererProps, type BlockRenderers, type BlockStateContextValue, type BlockStateProviderProps, BlockTree, type BlockTreeCallbacks, type BlockTreeConfig, type BlockTreeCustomization, type BlockTreeProps, type CanDragFn, type CanDropFn, type ContainerRendererProps, type DragEndEvent, type DragMoveEvent, DragOverlay, type DragOverlayProps$1 as DragOverlayProps, type DragStartEvent, DropZone, type DropZoneConfig, type DropZoneProps, type DropZoneType, type ExpandChangeEvent, type HoverChangeEvent, type IdGeneratorFn, type InternalRenderers, type MoveOperation, type OrderingStrategy, type RendererPropsFor, type SensorConfig, TreeRenderer, type TreeRendererProps, type TreeStateContextValue, type TreeStateProviderProps, type UseBlockHistoryOptions, type UseBlockHistoryResult, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, reparentBlockIndex, reparentMultipleBlocks, useBlockHistory, useConfiguredSensors, weightedVerticalCollision };
|
|
814
|
+
export { type AnimationConfig, type AutoExpandConfig, type BaseBlock, type BlockAction, type BlockAddEvent, type BlockDeleteEvent, type BlockIndex, type BlockMoveEvent, type BlockPosition, type BlockRendererProps, type BlockRenderers, type BlockStateContextValue, type BlockStateProviderProps, BlockTree, type BlockTreeCallbacks, type BlockTreeConfig, type BlockTreeCustomization, type BlockTreeProps, BlockTreeSSR, type BlockTreeSSRProps, type CanDragFn, type CanDropFn, type ContainerRendererProps, type DragEndEvent, type DragMoveEvent, DragOverlay, type DragOverlayProps$1 as DragOverlayProps, type DragStartEvent, DropZone, type DropZoneConfig, type DropZoneProps, type DropZoneType, type ExpandChangeEvent, type HoverChangeEvent, type IdGeneratorFn, type InternalRenderers, type MoveOperation, type NestedBlock, type OrderingStrategy, type RendererPropsFor, type SensorConfig, TreeRenderer, type TreeRendererProps, type TreeStateContextValue, type TreeStateProviderProps, type UseBlockHistoryOptions, type UseBlockHistoryResult, type UseLayoutAnimationOptions, type UseVirtualTreeOptions, type UseVirtualTreeResult, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, flatToNested, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, nestedToFlat, reparentBlockIndex, reparentMultipleBlocks, triggerHaptic, useBlockHistory, useConfiguredSensors, useLayoutAnimation, useVirtualTree, weightedVerticalCollision };
|