ngx-virtual-dnd 3.1.2 → 3.2.0-alpha.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 +87 -32
- package/fesm2022/ngx-virtual-dnd.mjs +826 -161
- package/fesm2022/ngx-virtual-dnd.mjs.map +1 -1
- package/package.json +4 -4
- package/types/ngx-virtual-dnd.d.ts +114 -10
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Inspired by [react-virtualized-dnd](https://github.com/forecast-it/react-virtual
|
|
|
15
15
|
- **Container Constraints** - Constrain drag preview to container boundaries
|
|
16
16
|
- **Axis Locking** - Lock dragging to horizontal or vertical axis
|
|
17
17
|
- **Custom Previews** - Template-based drag preview and placeholder customization
|
|
18
|
+
- **Shift Animations** - Opt-in sliding of displaced items, plus a per-step event for haptics
|
|
18
19
|
- **Keyboard Accessible** - Space to grab, arrows to move, Escape to cancel
|
|
19
20
|
- **Touch Support** - Works with mouse and touch, with configurable delay/threshold
|
|
20
21
|
- **Angular 21+** - Signals, standalone components, modern patterns
|
|
@@ -45,15 +46,15 @@ import {
|
|
|
45
46
|
DragPreviewComponent,
|
|
46
47
|
],
|
|
47
48
|
template: `
|
|
48
|
-
<!-- Item template -->
|
|
49
|
-
<ng-template #itemTpl let-item>
|
|
50
|
-
<div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
|
|
51
|
-
{{ item.name }}
|
|
52
|
-
</div>
|
|
53
|
-
</ng-template>
|
|
54
|
-
|
|
55
49
|
<!-- Lists wrapped in a group -->
|
|
56
50
|
<div vdndGroup="my-group">
|
|
51
|
+
<!-- Item template: declare it inside vdndGroup so the draggables inherit the group -->
|
|
52
|
+
<ng-template #itemTpl let-item>
|
|
53
|
+
<div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
|
|
54
|
+
{{ item.name }}
|
|
55
|
+
</div>
|
|
56
|
+
</ng-template>
|
|
57
|
+
|
|
57
58
|
<vdnd-sortable-list
|
|
58
59
|
droppableId="list-1"
|
|
59
60
|
group="my-group"
|
|
@@ -98,6 +99,8 @@ export class MyComponent {
|
|
|
98
99
|
|
|
99
100
|
**That's it!** `VirtualSortableListComponent` handles placeholder positioning, sticky items during drag, and virtual scroll integration automatically.
|
|
100
101
|
|
|
102
|
+
> Every draggable and droppable needs a group (from a `vdndGroup` ancestor or `vdndDraggableGroup` / `vdndDroppableGroup`), even for a single list — without one, drag is disabled. Templates resolve the group from where they are **declared**, so keep the `<ng-template>` inside the `vdndGroup` element.
|
|
103
|
+
|
|
101
104
|
## API Overview
|
|
102
105
|
|
|
103
106
|
The library exports these main pieces (use IDE completion for full details):
|
|
@@ -241,10 +244,13 @@ The preview cannot leave the droppable area, and the placeholder snaps to the ed
|
|
|
241
244
|
Lock dragging to a single axis:
|
|
242
245
|
|
|
243
246
|
```html
|
|
247
|
+
<!-- Lock the Y axis → item moves horizontally only -->
|
|
244
248
|
<div [vdndDraggable]="item.id" lockAxis="y">{{ item.name }}</div>
|
|
245
249
|
```
|
|
246
250
|
|
|
247
|
-
Values: `'x'` (
|
|
251
|
+
Values: `'x'` (X axis locked → **vertical-only** movement), `'y'` (Y axis locked → **horizontal-only** movement), or omit for free movement. The value names the axis that is _frozen_.
|
|
252
|
+
|
|
253
|
+
> **Note:** This is the opposite of Angular CDK's `cdkDragLockAxis`, where `'x'` means _movement is constrained to_ the X axis (horizontal only). Here `'x'` freezes the X axis. Keep this in mind when migrating from CDK.
|
|
248
254
|
|
|
249
255
|
### Drag Threshold & Delay
|
|
250
256
|
|
|
@@ -289,14 +295,48 @@ Configure auto-scroll behavior when dragging near container edges:
|
|
|
289
295
|
/>
|
|
290
296
|
```
|
|
291
297
|
|
|
292
|
-
| Option | Default | Description
|
|
293
|
-
| ------------ | ------- |
|
|
294
|
-
| `threshold` | `50` | Distance from edge (px) to trigger
|
|
295
|
-
| `maxSpeed` | `15` | Maximum scroll speed
|
|
296
|
-
| `accelerate` | `true` | Speed up based on distance from edge
|
|
298
|
+
| Option | Default | Description |
|
|
299
|
+
| ------------ | ------- | ---------------------------------------------- |
|
|
300
|
+
| `threshold` | `50` | Distance from edge (px) to trigger |
|
|
301
|
+
| `maxSpeed` | `15` | Maximum scroll speed in pixels per 60fps frame |
|
|
302
|
+
| `accelerate` | `true` | Speed up based on distance from edge |
|
|
297
303
|
|
|
298
304
|
Set `[autoScrollEnabled]="false"` to disable auto-scroll entirely. These options are available on `VirtualSortableListComponent`, `DroppableDirective`, and `ScrollableDirective`.
|
|
299
305
|
|
|
306
|
+
### Shift Animations & Haptics
|
|
307
|
+
|
|
308
|
+
Items displaced by the placeholder jump into place by default. Provide `VDND_ANIMATION_CONFIG` to make them slide instead (a compositor-only `transform` animation, works with virtual scrolling and dynamic heights):
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { VDND_ANIMATION_CONFIG } from 'ngx-virtual-dnd';
|
|
312
|
+
|
|
313
|
+
// app.config.ts (app-wide) or any component's `providers` (that subtree only)
|
|
314
|
+
providers: [{ provide: VDND_ANIMATION_CONFIG, useValue: { shiftDuration: 200 } }];
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
| Option | Default | Description |
|
|
318
|
+
| --------------- | ---------------------------- | ----------------------------------- |
|
|
319
|
+
| `shiftDuration` | `200` | Slide duration in ms (`0` disables) |
|
|
320
|
+
| `shiftEasing` | `cubic-bezier(0.2, 0, 0, 1)` | CSS easing function |
|
|
321
|
+
|
|
322
|
+
The animation is skipped when the user prefers reduced motion. If the placeholder moves again mid-slide, items continue from where they currently are. Values are read each time an animation starts, so a getter can toggle it at runtime.
|
|
323
|
+
|
|
324
|
+
For haptic feedback on every step, listen to `(placeholderMove)` on `vdndDroppable` or `vdnd-sortable-list`. It fires each time the placeholder moves within that list (every item displacement), including when it enters the list — not for the initial pick-up or when it leaves:
|
|
325
|
+
|
|
326
|
+
```html
|
|
327
|
+
<vdnd-sortable-list ... (placeholderMove)="onPlaceholderMove($event)" />
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { Haptics } from '@capacitor/haptics';
|
|
332
|
+
|
|
333
|
+
onPlaceholderMove(event: PlaceholderMoveEvent): void {
|
|
334
|
+
Haptics.selectionChanged(); // or navigator.vibrate?.(10) on the web
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
`PlaceholderMoveEvent` carries `draggableId`, `sourceDroppableId`, `droppableId`, `previousIndex` (`null` when the placeholder just entered the list) and `currentIndex`, using the same index convention as `DropEvent.destination.index`.
|
|
339
|
+
|
|
300
340
|
### Disabling Drag & Drop
|
|
301
341
|
|
|
302
342
|
Use the `disabled` input to conditionally disable draggables, droppables, or entire lists:
|
|
@@ -320,6 +360,8 @@ Use the `disabled` input to conditionally disable draggables, droppables, or ent
|
|
|
320
360
|
|
|
321
361
|
Disabled draggables get the `vdnd-draggable-disabled` CSS class. Disabled droppables get `vdnd-droppable-disabled`.
|
|
322
362
|
|
|
363
|
+
A disabled droppable is removed from all drag-time candidate sets: pointer hit-testing skips it (the cursor falls through to whatever enabled droppable sits underneath, or none) and keyboard `ArrowLeft`/`ArrowRight` navigation steps over it. Releasing a drag over a disabled droppable fires **no `drop` event**; the `(dragEnd)` event still fires with `cancelled: false` and `destinationIndex: null`, so consumers that pair `drop`/`dragEnd` should treat a `null` `destinationIndex` as "no valid drop target".
|
|
364
|
+
|
|
323
365
|
### Low-Level API
|
|
324
366
|
|
|
325
367
|
For maximum control, use individual components instead of `VirtualSortableListComponent`:
|
|
@@ -334,13 +376,13 @@ For maximum control, use individual components instead of `VirtualSortableListCo
|
|
|
334
376
|
DragPreviewComponent,
|
|
335
377
|
],
|
|
336
378
|
template: `
|
|
337
|
-
<ng-template #itemTpl let-item>
|
|
338
|
-
<div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
|
|
339
|
-
{{ item.name }}
|
|
340
|
-
</div>
|
|
341
|
-
</ng-template>
|
|
342
|
-
|
|
343
379
|
<div vdndGroup="demo">
|
|
380
|
+
<ng-template #itemTpl let-item>
|
|
381
|
+
<div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
|
|
382
|
+
{{ item.name }}
|
|
383
|
+
</div>
|
|
384
|
+
</ng-template>
|
|
385
|
+
|
|
344
386
|
<div vdndDroppable="list-1" (drop)="onDrop($event)">
|
|
345
387
|
<vdnd-virtual-scroll
|
|
346
388
|
droppableId="list-1"
|
|
@@ -428,6 +470,7 @@ The library emits events with position data. Implement announcements in your app
|
|
|
428
470
|
```typescript
|
|
429
471
|
@Component({
|
|
430
472
|
template: `
|
|
473
|
+
<!-- Inside a vdndGroup, like any draggable -->
|
|
431
474
|
<div
|
|
432
475
|
vdndDraggable="item-1"
|
|
433
476
|
(dragStart)="announce('Grabbed ' + item.name)"
|
|
@@ -446,11 +489,13 @@ export class MyComponent {
|
|
|
446
489
|
}
|
|
447
490
|
|
|
448
491
|
announceEnd(e: DragEndEvent) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
492
|
+
// destinationIndex is null when there is no valid drop target: an Escape cancel,
|
|
493
|
+
// a release outside every droppable, or a release over a disabled droppable.
|
|
494
|
+
if (e.destinationIndex === null) {
|
|
495
|
+
this.announce(`Returned to position ${e.sourceIndex + 1}`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
this.announce(`Dropped at position ${e.destinationIndex + 1}`);
|
|
454
499
|
}
|
|
455
500
|
}
|
|
456
501
|
```
|
|
@@ -483,19 +528,29 @@ ARIA attributes (`aria-grabbed`, `aria-dropeffect`, `tabindex`) are managed auto
|
|
|
483
528
|
|
|
484
529
|
All event types are importable from `ngx-virtual-dnd`.
|
|
485
530
|
|
|
486
|
-
| Output
|
|
487
|
-
|
|
|
488
|
-
| `(dragStart)`
|
|
489
|
-
| `(dragEnd)`
|
|
490
|
-
| `(drop)`
|
|
531
|
+
| Output | Event Type | Emitted By |
|
|
532
|
+
| ------------------- | ---------------------- | ---------------------------------------------------- |
|
|
533
|
+
| `(dragStart)` | `DragStartEvent` | `DraggableDirective` |
|
|
534
|
+
| `(dragEnd)` | `DragEndEvent` | `DraggableDirective` |
|
|
535
|
+
| `(drop)` | `DropEvent` | `DroppableDirective`, `VirtualSortableListComponent` |
|
|
536
|
+
| `(placeholderMove)` | `PlaceholderMoveEvent` | `DroppableDirective`, `VirtualSortableListComponent` |
|
|
491
537
|
|
|
492
|
-
`DragEndEvent`
|
|
538
|
+
`DragEndEvent.destinationIndex` is `null` when no drop occurred — an Escape cancel, a release outside every droppable, or a release over a disabled droppable — so branch on `destinationIndex === null` to detect that. The `cancelled` boolean is `true` only for an active Escape cancel.
|
|
493
539
|
|
|
494
540
|
## How It Works
|
|
495
541
|
|
|
496
542
|
Traditional drag-and-drop libraries query sibling DOM elements via `getBoundingClientRect()`. This fails with virtual scrolling because items outside the viewport aren't rendered.
|
|
497
543
|
|
|
498
|
-
This library uses **
|
|
544
|
+
This library uses **geometric hit-testing against a cached rect snapshot**: at drag start it snapshots the candidate droppables of the active group and their bounding rects, then per-pointermove it hit-tests the cursor against those rects (last match in document order wins) and calculates the placeholder position mathematically. Caching the rects avoids the forced layout flush that `document.elementFromPoint()` imposes on the hot drag loop. The snapshot self-heals during the drag: rects are re-read on scroll/resize and when a candidate resizes (via `ResizeObserver`), the candidate **list** is refreshed when a droppable mounts or unmounts mid-drag, and each rect is clipped to its nearest `vdndScrollable` ancestor so a droppable scrolled out of a clipping container is not falsely hit.
|
|
545
|
+
|
|
546
|
+
### Known Limitations
|
|
547
|
+
|
|
548
|
+
Because hit-testing is geometry against cached rects (not live `elementFromPoint`), a few browser-native behaviors are **not** reproduced:
|
|
549
|
+
|
|
550
|
+
- **Occluding overlays** — a modal, toolbar, or other element painted over a droppable does not block a geometric hit; the droppable underneath still resolves as the target.
|
|
551
|
+
- **Stacking contexts / explicit `z-index`** — the "last in document order" tie-break approximates paint order but is not true painter order once `z-index` and stacking contexts are involved. An earlier-in-DOM droppable raised visually on top via `z-index` loses the tie-break to a later sibling.
|
|
552
|
+
|
|
553
|
+
If your layout depends on these, keep overlapping droppables out of the same drag group, or avoid painting interactive overlays across active drop targets during a drag.
|
|
499
554
|
|
|
500
555
|
## Browser Support
|
|
501
556
|
|
|
@@ -517,7 +572,7 @@ npm run e2e # E2E tests
|
|
|
517
572
|
Install a skill to teach AI coding assistants how to integrate this library:
|
|
518
573
|
|
|
519
574
|
```bash
|
|
520
|
-
npx skills add gultyayev/
|
|
575
|
+
npx skills add gultyayev/ngx-virtual-dnd
|
|
521
576
|
```
|
|
522
577
|
|
|
523
578
|
Works with Claude Code, Cursor, Windsurf, GitHub Copilot, and [40+ other agents](https://skills.sh).
|