ngx-virtual-dnd 3.2.0-alpha.0 → 3.2.0-alpha.1
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 +53 -519
- package/fesm2022/ngx-virtual-dnd.mjs +113 -48
- package/fesm2022/ngx-virtual-dnd.mjs.map +1 -1
- package/package.json +2 -2
- package/types/ngx-virtual-dnd.d.ts +11 -0
package/README.md
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
# ngx-virtual-dnd
|
|
2
2
|
|
|
3
|
-
Angular drag
|
|
3
|
+
Angular drag and drop for virtual scrolling. Sortable lists and boards that stay fast with thousands of items: only the visible rows are rendered, and drag and drop still works across all of them.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**[Documentation](https://gultyayev.github.io/ngx-virtual-dnd/)** · **[Live demo](https://gultyayev.github.io/ngx-virtual-dnd/demo/)** · **[Changelog](https://gultyayev.github.io/ngx-virtual-dnd/changelog.html)**
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- **Axis Locking** - Lock dragging to horizontal or vertical axis
|
|
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
|
|
19
|
-
- **Keyboard Accessible** - Space to grab, arrows to move, Escape to cancel
|
|
20
|
-
- **Touch Support** - Works with mouse and touch, with configurable delay/threshold
|
|
21
|
-
- **Angular 21+** - Signals, standalone components, modern patterns
|
|
9
|
+
- Virtual scrolling with fixed or dynamic (auto-measured) item heights
|
|
10
|
+
- Drag between multiple lists, with edge auto-scroll
|
|
11
|
+
- Page-level scrolling, including Ionic `ion-content`
|
|
12
|
+
- Drag handles, delays, axis locking, container constraints, custom previews
|
|
13
|
+
- Opt-in shift animations, plus a per-step event for haptics
|
|
14
|
+
- Keyboard dragging and touch support
|
|
15
|
+
- Angular 21+: standalone components and signals
|
|
22
16
|
|
|
23
17
|
## Installation
|
|
24
18
|
|
|
@@ -26,19 +20,27 @@ Inspired by [react-virtualized-dnd](https://github.com/forecast-it/react-virtual
|
|
|
26
20
|
npm install ngx-virtual-dnd
|
|
27
21
|
```
|
|
28
22
|
|
|
29
|
-
##
|
|
23
|
+
## Example
|
|
30
24
|
|
|
31
25
|
```typescript
|
|
26
|
+
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
|
32
27
|
import {
|
|
33
|
-
VirtualSortableListComponent,
|
|
34
|
-
DroppableGroupDirective,
|
|
35
|
-
DraggableDirective,
|
|
36
28
|
DragPreviewComponent,
|
|
29
|
+
DraggableDirective,
|
|
37
30
|
DropEvent,
|
|
38
|
-
|
|
31
|
+
DroppableGroupDirective,
|
|
32
|
+
reorderItems,
|
|
33
|
+
VirtualSortableListComponent,
|
|
39
34
|
} from 'ngx-virtual-dnd';
|
|
40
35
|
|
|
36
|
+
interface Task {
|
|
37
|
+
id: string;
|
|
38
|
+
title: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
41
|
@Component({
|
|
42
|
+
selector: 'app-tasks',
|
|
43
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
42
44
|
imports: [
|
|
43
45
|
VirtualSortableListComponent,
|
|
44
46
|
DroppableGroupDirective,
|
|
@@ -46,537 +48,69 @@ import {
|
|
|
46
48
|
DragPreviewComponent,
|
|
47
49
|
],
|
|
48
50
|
template: `
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<ng-template #itemTpl let-item>
|
|
53
|
-
<div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
|
|
54
|
-
{{ item.name }}
|
|
55
|
-
</div>
|
|
51
|
+
<div vdndGroup="tasks">
|
|
52
|
+
<ng-template #taskTpl let-task>
|
|
53
|
+
<div class="row" [vdndDraggable]="task.id">{{ task.title }}</div>
|
|
56
54
|
</ng-template>
|
|
57
55
|
|
|
58
56
|
<vdnd-sortable-list
|
|
59
|
-
droppableId="
|
|
60
|
-
|
|
61
|
-
[
|
|
62
|
-
[itemHeight]="50"
|
|
63
|
-
[containerHeight]="400"
|
|
64
|
-
[itemIdFn]="getItemId"
|
|
65
|
-
[itemTemplate]="itemTpl"
|
|
66
|
-
(drop)="onDrop($event)"
|
|
67
|
-
/>
|
|
68
|
-
|
|
69
|
-
<vdnd-sortable-list
|
|
70
|
-
droppableId="list-2"
|
|
71
|
-
group="my-group"
|
|
72
|
-
[items]="list2()"
|
|
73
|
-
[itemHeight]="50"
|
|
57
|
+
droppableId="backlog"
|
|
58
|
+
[items]="tasks()"
|
|
59
|
+
[itemHeight]="48"
|
|
74
60
|
[containerHeight]="400"
|
|
75
|
-
[itemIdFn]="
|
|
76
|
-
[itemTemplate]="
|
|
61
|
+
[itemIdFn]="taskId"
|
|
62
|
+
[itemTemplate]="taskTpl"
|
|
77
63
|
(drop)="onDrop($event)"
|
|
78
64
|
/>
|
|
79
65
|
</div>
|
|
80
66
|
|
|
81
|
-
<!-- Required: renders the dragged item preview -->
|
|
82
67
|
<vdnd-drag-preview />
|
|
83
68
|
`,
|
|
84
69
|
})
|
|
85
|
-
export class
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
export class TasksComponent {
|
|
71
|
+
readonly tasks = signal<Task[]>(
|
|
72
|
+
Array.from({ length: 1000 }, (_, i) => ({ id: `task-${i + 1}`, title: `Task ${i + 1}` })),
|
|
73
|
+
);
|
|
88
74
|
|
|
89
|
-
|
|
75
|
+
readonly taskId = (task: Task): string => task.id;
|
|
90
76
|
|
|
91
77
|
onDrop(event: DropEvent): void {
|
|
92
|
-
|
|
93
|
-
'list-1': this.list1,
|
|
94
|
-
'list-2': this.list2,
|
|
95
|
-
});
|
|
78
|
+
reorderItems(event, this.tasks);
|
|
96
79
|
}
|
|
97
80
|
}
|
|
98
81
|
```
|
|
99
82
|
|
|
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
|
-
|
|
104
|
-
## API Overview
|
|
105
|
-
|
|
106
|
-
The library exports these main pieces (use IDE completion for full details):
|
|
107
|
-
|
|
108
|
-
**Components:**
|
|
109
|
-
|
|
110
|
-
- `VirtualSortableListComponent` - High-level component combining droppable, virtual scroll, and placeholder
|
|
111
|
-
- `VirtualScrollContainerComponent` - Low-level virtual scroll container
|
|
112
|
-
- `VirtualViewportComponent` - Self-contained virtual scroll viewport
|
|
113
|
-
- `VirtualContentComponent` - Virtual content for external scroll containers (page-level scroll)
|
|
114
|
-
- `DragPreviewComponent` - Renders the dragged item preview (required, supports custom templates)
|
|
115
|
-
- `PlaceholderComponent` - Drop position indicator
|
|
116
|
-
|
|
117
|
-
**Directives:**
|
|
118
|
-
|
|
119
|
-
- `DraggableDirective` (`vdndDraggable`) - Makes an element draggable (supports drag handles, axis locking, threshold/delay)
|
|
120
|
-
- `DroppableDirective` (`vdndDroppable`) - Marks a drop target (supports container constraints, auto-scroll config)
|
|
121
|
-
- `DroppableGroupDirective` (`vdndGroup`) - Provides group context to children
|
|
122
|
-
- `ScrollableDirective` (`vdndScrollable`) - Marks external scroll container
|
|
123
|
-
- `VirtualForDirective` (`*vdndVirtualFor`) - Structural directive for virtual lists
|
|
124
|
-
- `ContentHeaderDirective` (`vdndContentHeader`) - Marks a projected header inside `VirtualContentComponent` (auto-measured via ResizeObserver)
|
|
125
|
-
|
|
126
|
-
**Services:**
|
|
127
|
-
|
|
128
|
-
- `DragStateService` - Access drag state (isDragging, draggedItem, placeholderIndex, etc.)
|
|
129
|
-
- `AutoScrollService` - Controls edge auto-scrolling
|
|
130
|
-
- `PositionCalculatorService` - Calculates placeholder positions
|
|
131
|
-
|
|
132
|
-
**Utilities:**
|
|
133
|
-
|
|
134
|
-
- `moveItem()` - Move between signal-based lists
|
|
135
|
-
- `reorderItems()` - Reorder within a single list
|
|
136
|
-
- `applyMove()` - Immutable version (returns new arrays)
|
|
137
|
-
- `isNoOpDrop()` - Check if drop would be a no-op
|
|
138
|
-
- `insertAt()` / `removeAt()` - Low-level array helpers
|
|
139
|
-
|
|
140
|
-
**Strategies:**
|
|
141
|
-
|
|
142
|
-
- `VirtualScrollStrategy` - Interface for custom virtual scroll strategies
|
|
143
|
-
- `FixedHeightStrategy` - Fixed `index * itemHeight` math (zero overhead)
|
|
144
|
-
- `DynamicHeightStrategy` - Variable heights with auto-measurement and binary search
|
|
145
|
-
|
|
146
|
-
## Advanced Usage
|
|
147
|
-
|
|
148
|
-
### Dynamic Item Heights
|
|
149
|
-
|
|
150
|
-
When items have variable heights, enable `dynamicItemHeight`. Items are auto-measured via ResizeObserver — no manual height tracking needed. The `itemHeight` value serves as the initial estimate for unmeasured items.
|
|
151
|
-
|
|
152
|
-
**With `VirtualSortableListComponent`:**
|
|
153
|
-
|
|
154
|
-
```html
|
|
155
|
-
<vdnd-sortable-list
|
|
156
|
-
droppableId="list-1"
|
|
157
|
-
group="my-group"
|
|
158
|
-
[items]="list()"
|
|
159
|
-
[itemHeight]="80"
|
|
160
|
-
[dynamicItemHeight]="true"
|
|
161
|
-
[itemIdFn]="getItemId"
|
|
162
|
-
[itemTemplate]="itemTpl"
|
|
163
|
-
(drop)="onDrop($event)"
|
|
164
|
-
/>
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
**With `VirtualScrollContainerComponent`:**
|
|
168
|
-
|
|
169
|
-
```html
|
|
170
|
-
<vdnd-virtual-scroll
|
|
171
|
-
[items]="items()"
|
|
172
|
-
[itemHeight]="80"
|
|
173
|
-
[dynamicItemHeight]="true"
|
|
174
|
-
[itemIdFn]="getItemId"
|
|
175
|
-
[trackByFn]="trackById"
|
|
176
|
-
[itemTemplate]="itemTpl"
|
|
177
|
-
/>
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
**With `VirtualForDirective`:**
|
|
181
|
-
|
|
182
|
-
```html
|
|
183
|
-
<ng-container
|
|
184
|
-
*vdndVirtualFor="
|
|
185
|
-
let item of items();
|
|
186
|
-
itemHeight: 80;
|
|
187
|
-
dynamicItemHeight: true;
|
|
188
|
-
trackBy: trackById;
|
|
189
|
-
droppableId: 'list-1'
|
|
190
|
-
"
|
|
191
|
-
>
|
|
192
|
-
<div class="item">{{ item.description }}</div>
|
|
193
|
-
</ng-container>
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Notes:
|
|
197
|
-
|
|
198
|
-
- `FixedHeightStrategy` is used by default when `dynamicItemHeight` is not set
|
|
199
|
-
- Setting `dynamicItemHeight` switches to `DynamicHeightStrategy` with automatic height measurement
|
|
200
|
-
- Heights are tracked by `trackBy` key, so they survive reordering
|
|
201
|
-
- The `itemHeight` value is used as the initial estimate for items not yet measured
|
|
202
|
-
- Inside a viewport component (`vdnd-virtual-viewport` or `vdnd-virtual-content`), `itemHeight`, `dynamicItemHeight`, and `droppableId` are inherited automatically — only `trackBy` is needed on the directive
|
|
203
|
-
|
|
204
|
-
### Drag Handles
|
|
205
|
-
|
|
206
|
-
Use `dragHandle` with a CSS selector to restrict where users can initiate a drag:
|
|
207
|
-
|
|
208
|
-
```html
|
|
209
|
-
<div [vdndDraggable]="item.id" dragHandle=".handle">
|
|
210
|
-
<span class="handle">⠿</span>
|
|
211
|
-
<span>{{ item.name }}</span>
|
|
212
|
-
</div>
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Only clicks on elements matching the selector will start a drag. The rest of the element remains interactive.
|
|
216
|
-
|
|
217
|
-
### Container Constraints
|
|
218
|
-
|
|
219
|
-
Constrain the drag preview and placeholder to stay within the droppable container:
|
|
220
|
-
|
|
221
|
-
```html
|
|
222
|
-
<vdnd-sortable-list
|
|
223
|
-
droppableId="list-1"
|
|
224
|
-
group="my-group"
|
|
225
|
-
[items]="items()"
|
|
226
|
-
[itemHeight]="50"
|
|
227
|
-
[constrainToContainer]="true"
|
|
228
|
-
[itemIdFn]="getItemId"
|
|
229
|
-
[itemTemplate]="itemTpl"
|
|
230
|
-
(drop)="onDrop($event)"
|
|
231
|
-
/>
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Or on the directive directly:
|
|
235
|
-
|
|
236
|
-
```html
|
|
237
|
-
<div vdndDroppable="list-1" [constrainToContainer]="true">...</div>
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
The preview cannot leave the droppable area, and the placeholder snaps to the edges.
|
|
241
|
-
|
|
242
|
-
### Axis Locking
|
|
243
|
-
|
|
244
|
-
Lock dragging to a single axis:
|
|
245
|
-
|
|
246
|
-
```html
|
|
247
|
-
<!-- Lock the Y axis → item moves horizontally only -->
|
|
248
|
-
<div [vdndDraggable]="item.id" lockAxis="y">{{ item.name }}</div>
|
|
249
|
-
```
|
|
250
|
-
|
|
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.
|
|
254
|
-
|
|
255
|
-
### Drag Threshold & Delay
|
|
256
|
-
|
|
257
|
-
```html
|
|
258
|
-
<div [vdndDraggable]="item.id" [dragThreshold]="10" [dragDelay]="200">{{ item.name }}</div>
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
- `dragThreshold` — minimum distance (px) before drag starts (default: `5`). Prevents accidental drags on click.
|
|
262
|
-
- `dragDelay` — delay (ms) after pointer down before drag activates (default: `0`). Useful on touch devices to distinguish scrolling from dragging. Use the `vdnd-drag-pending` CSS class to show visual feedback when the delay passes.
|
|
263
|
-
|
|
264
|
-
### Custom Drag Preview
|
|
265
|
-
|
|
266
|
-
Provide a custom template for the drag preview:
|
|
267
|
-
|
|
268
|
-
```html
|
|
269
|
-
<ng-template #preview let-data let-draggableId="draggableId">
|
|
270
|
-
<div class="custom-preview">Dragging: {{ data.name }}</div>
|
|
271
|
-
</ng-template>
|
|
272
|
-
|
|
273
|
-
<vdnd-drag-preview [previewTemplate]="preview" [cursorOffset]="{ x: 16, y: 16 }" />
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
- `previewTemplate` — custom template for the preview. Context provides `$implicit` (the draggable's data), `draggableId`, and `droppableId`.
|
|
277
|
-
- `cursorOffset` — offset from cursor in pixels (default: `{ x: 8, y: 8 }`).
|
|
278
|
-
|
|
279
|
-
Without a custom template, the library clones the dragged element as the preview.
|
|
280
|
-
|
|
281
|
-
### Auto-Scroll Configuration
|
|
282
|
-
|
|
283
|
-
Configure auto-scroll behavior when dragging near container edges:
|
|
284
|
-
|
|
285
|
-
```html
|
|
286
|
-
<vdnd-sortable-list
|
|
287
|
-
droppableId="list-1"
|
|
288
|
-
group="my-group"
|
|
289
|
-
[items]="items()"
|
|
290
|
-
[itemHeight]="50"
|
|
291
|
-
[autoScrollConfig]="{ threshold: 80, maxSpeed: 20 }"
|
|
292
|
-
[itemIdFn]="getItemId"
|
|
293
|
-
[itemTemplate]="itemTpl"
|
|
294
|
-
(drop)="onDrop($event)"
|
|
295
|
-
/>
|
|
296
|
-
```
|
|
297
|
-
|
|
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 |
|
|
303
|
-
|
|
304
|
-
Set `[autoScrollEnabled]="false"` to disable auto-scroll entirely. These options are available on `VirtualSortableListComponent`, `DroppableDirective`, and `ScrollableDirective`.
|
|
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 |
|
|
83
|
+
Every draggable and droppable needs a group (here from `vdndGroup`), even for a single list, and the item template must be declared inside it. The [Quick start](https://gultyayev.github.io/ngx-virtual-dnd/guide/start/quick-start.html) explains each part, and [Core concepts](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/core-concepts.html) lists the rules that keep a setup working.
|
|
321
84
|
|
|
322
|
-
|
|
85
|
+
## Documentation
|
|
323
86
|
|
|
324
|
-
|
|
87
|
+
- [Getting started](https://gultyayev.github.io/ngx-virtual-dnd/guide/start/introduction.html): introduction, installation, quick start
|
|
88
|
+
- [Guides](https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/core-concepts.html): multiple lists, dynamic heights, page scroll, drag behavior, styling, accessibility
|
|
89
|
+
- [API reference](https://gultyayev.github.io/ngx-virtual-dnd/api/components.html): components, directives, events, utilities, services, types
|
|
325
90
|
|
|
326
|
-
|
|
327
|
-
<vdnd-sortable-list ... (placeholderMove)="onPlaceholderMove($event)" />
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
import { Haptics } from '@capacitor/haptics';
|
|
91
|
+
## AI agent skills
|
|
332
92
|
|
|
333
|
-
|
|
334
|
-
Haptics.selectionChanged(); // or navigator.vibrate?.(10) on the web
|
|
335
|
-
}
|
|
336
|
-
```
|
|
93
|
+
Install a skill that teaches AI coding assistants how to integrate this library:
|
|
337
94
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
### Disabling Drag & Drop
|
|
341
|
-
|
|
342
|
-
Use the `disabled` input to conditionally disable draggables, droppables, or entire lists:
|
|
343
|
-
|
|
344
|
-
```html
|
|
345
|
-
<!-- Disable a single item -->
|
|
346
|
-
<div [vdndDraggable]="item.id" [disabled]="!item.canDrag">{{ item.name }}</div>
|
|
347
|
-
|
|
348
|
-
<!-- Disable an entire list -->
|
|
349
|
-
<vdnd-sortable-list
|
|
350
|
-
droppableId="list-1"
|
|
351
|
-
group="my-group"
|
|
352
|
-
[items]="items()"
|
|
353
|
-
[itemHeight]="50"
|
|
354
|
-
[disabled]="isReadOnly()"
|
|
355
|
-
[itemIdFn]="getItemId"
|
|
356
|
-
[itemTemplate]="itemTpl"
|
|
357
|
-
(drop)="onDrop($event)"
|
|
358
|
-
/>
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
Disabled draggables get the `vdnd-draggable-disabled` CSS class. Disabled droppables get `vdnd-droppable-disabled`.
|
|
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
|
-
|
|
365
|
-
### Low-Level API
|
|
366
|
-
|
|
367
|
-
For maximum control, use individual components instead of `VirtualSortableListComponent`:
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
@Component({
|
|
371
|
-
imports: [
|
|
372
|
-
VirtualScrollContainerComponent,
|
|
373
|
-
DroppableGroupDirective,
|
|
374
|
-
DroppableDirective,
|
|
375
|
-
DraggableDirective,
|
|
376
|
-
DragPreviewComponent,
|
|
377
|
-
],
|
|
378
|
-
template: `
|
|
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
|
-
|
|
386
|
-
<div vdndDroppable="list-1" (drop)="onDrop($event)">
|
|
387
|
-
<vdnd-virtual-scroll
|
|
388
|
-
droppableId="list-1"
|
|
389
|
-
[items]="items()"
|
|
390
|
-
[itemHeight]="50"
|
|
391
|
-
[itemIdFn]="getItemId"
|
|
392
|
-
[trackByFn]="trackById"
|
|
393
|
-
[itemTemplate]="itemTpl"
|
|
394
|
-
/>
|
|
395
|
-
</div>
|
|
396
|
-
</div>
|
|
397
|
-
<vdnd-drag-preview />
|
|
398
|
-
`,
|
|
399
|
-
})
|
|
400
|
-
export class ListComponent {
|
|
401
|
-
items = signal<Item[]>([]);
|
|
402
|
-
}
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
### Page-Level Scroll
|
|
406
|
-
|
|
407
|
-
Use `VirtualContentComponent` with `vdndScrollable` for page-level scrolling with headers/footers:
|
|
408
|
-
|
|
409
|
-
```typescript
|
|
410
|
-
@Component({
|
|
411
|
-
imports: [
|
|
412
|
-
ScrollableDirective,
|
|
413
|
-
VirtualContentComponent,
|
|
414
|
-
VirtualForDirective,
|
|
415
|
-
DraggableDirective,
|
|
416
|
-
DroppableDirective,
|
|
417
|
-
DroppableGroupDirective,
|
|
418
|
-
DragPreviewComponent,
|
|
419
|
-
ContentHeaderDirective,
|
|
420
|
-
],
|
|
421
|
-
template: `
|
|
422
|
-
<ion-content [scrollY]="false">
|
|
423
|
-
<div class="scroll-container ion-content-scroll-host" vdndScrollable>
|
|
424
|
-
<div vdndGroup="tasks">
|
|
425
|
-
<vdnd-virtual-content
|
|
426
|
-
[itemHeight]="72"
|
|
427
|
-
vdndDroppable="list-1"
|
|
428
|
-
(drop)="onDrop($event)"
|
|
429
|
-
>
|
|
430
|
-
<!-- Header — auto-measured via ResizeObserver, scrolls with content -->
|
|
431
|
-
<div class="header" vdndContentHeader>Welcome!</div>
|
|
432
|
-
|
|
433
|
-
<ng-container
|
|
434
|
-
*vdndVirtualFor="
|
|
435
|
-
let item of items();
|
|
436
|
-
trackBy: trackById
|
|
437
|
-
"
|
|
438
|
-
>
|
|
439
|
-
<div class="item" [vdndDraggable]="item.id">{{ item.name }}</div>
|
|
440
|
-
</ng-container>
|
|
441
|
-
|
|
442
|
-
</vdnd-virtual-content>
|
|
443
|
-
</div>
|
|
444
|
-
|
|
445
|
-
<!-- Footer — normal sibling in document flow -->
|
|
446
|
-
<div class="footer">Load more</div>
|
|
447
|
-
</div>
|
|
448
|
-
</ion-content>
|
|
449
|
-
|
|
450
|
-
<vdnd-drag-preview />
|
|
451
|
-
`,
|
|
452
|
-
})
|
|
453
|
-
export class PageComponent {
|
|
454
|
-
items = signal<Item[]>([...]);
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
Key points:
|
|
459
|
-
|
|
460
|
-
- `vdndScrollable` marks the scroll container
|
|
461
|
-
- `VirtualContentComponent` provides wrapper-based positioning and derives total items from the child `*vdndVirtualFor` automatically
|
|
462
|
-
- `vdndContentHeader` marks a projected header — its height is auto-measured via ResizeObserver and used as the content offset (no manual measurement needed)
|
|
463
|
-
- `contentOffset` input is available as an escape hatch when the header lives outside the component
|
|
464
|
-
- `*vdndVirtualFor` inherits `itemHeight`, `dynamicItemHeight`, and `droppableId` from the parent viewport/droppable — only `trackBy` is required
|
|
465
|
-
|
|
466
|
-
### Screen Reader Announcements
|
|
467
|
-
|
|
468
|
-
The library emits events with position data. Implement announcements in your app:
|
|
469
|
-
|
|
470
|
-
```typescript
|
|
471
|
-
@Component({
|
|
472
|
-
template: `
|
|
473
|
-
<!-- Inside a vdndGroup, like any draggable -->
|
|
474
|
-
<div
|
|
475
|
-
vdndDraggable="item-1"
|
|
476
|
-
(dragStart)="announce('Grabbed ' + item.name)"
|
|
477
|
-
(dragEnd)="announceEnd($event)"
|
|
478
|
-
>
|
|
479
|
-
{{ item.name }}
|
|
480
|
-
</div>
|
|
481
|
-
<div aria-live="assertive" class="sr-only">{{ announcement() }}</div>
|
|
482
|
-
`,
|
|
483
|
-
})
|
|
484
|
-
export class MyComponent {
|
|
485
|
-
announcement = signal('');
|
|
486
|
-
|
|
487
|
-
announce(msg: string) {
|
|
488
|
-
this.announcement.set(msg);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
announceEnd(e: DragEndEvent) {
|
|
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}`);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
95
|
+
```bash
|
|
96
|
+
npx skills add gultyayev/ngx-virtual-dnd
|
|
501
97
|
```
|
|
502
98
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
| Key | Action |
|
|
506
|
-
| ----------- | --------------------------- |
|
|
507
|
-
| `Tab` | Navigate to draggable items |
|
|
508
|
-
| `Space` | Start/end drag |
|
|
509
|
-
| `Arrow ↑/↓` | Move item up/down |
|
|
510
|
-
| `Arrow ←/→` | Move to adjacent list |
|
|
511
|
-
| `Escape` | Cancel drag |
|
|
512
|
-
|
|
513
|
-
ARIA attributes (`aria-grabbed`, `aria-dropeffect`, `tabindex`) are managed automatically.
|
|
514
|
-
|
|
515
|
-
## CSS Classes
|
|
516
|
-
|
|
517
|
-
| Class | Applied When |
|
|
518
|
-
| ------------------------- | --------------------------- |
|
|
519
|
-
| `vdnd-draggable` | Always on draggable |
|
|
520
|
-
| `vdnd-draggable-dragging` | While being dragged |
|
|
521
|
-
| `vdnd-draggable-disabled` | When disabled |
|
|
522
|
-
| `vdnd-drag-pending` | After delay, ready to drag |
|
|
523
|
-
| `vdnd-droppable` | Always on droppable |
|
|
524
|
-
| `vdnd-droppable-active` | When a draggable is over it |
|
|
525
|
-
| `vdnd-droppable-disabled` | When disabled |
|
|
526
|
-
|
|
527
|
-
## Events
|
|
528
|
-
|
|
529
|
-
All event types are importable from `ngx-virtual-dnd`.
|
|
530
|
-
|
|
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` |
|
|
537
|
-
|
|
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.
|
|
539
|
-
|
|
540
|
-
## How It Works
|
|
541
|
-
|
|
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.
|
|
543
|
-
|
|
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.
|
|
554
|
-
|
|
555
|
-
## Browser Support
|
|
556
|
-
|
|
557
|
-
- Chrome/Edge (latest)
|
|
558
|
-
- Firefox (latest)
|
|
559
|
-
- Safari (latest)
|
|
99
|
+
It works with Claude Code, Cursor, Windsurf, GitHub Copilot and [40+ other agents](https://skills.sh). The docs are also published as [llms.txt](https://gultyayev.github.io/ngx-virtual-dnd/llms.txt).
|
|
560
100
|
|
|
561
101
|
## Development
|
|
562
102
|
|
|
563
|
-
|
|
564
|
-
npm start # Dev server (localhost:4200)
|
|
565
|
-
ng build ngx-virtual-dnd # Build library (required after lib edits)
|
|
566
|
-
npm test # Unit tests
|
|
567
|
-
npm run e2e # E2E tests
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
## AI Agent Skills
|
|
571
|
-
|
|
572
|
-
Install a skill to teach AI coding assistants how to integrate this library:
|
|
103
|
+
Requires npm 12 (the version pinned in `packageManager`): `npm install -g npm@12`.
|
|
573
104
|
|
|
574
105
|
```bash
|
|
575
|
-
|
|
106
|
+
npm start # Demo app (localhost:4200)
|
|
107
|
+
npm run build:lib # Build the library (required after library edits)
|
|
108
|
+
npm run docs:dev # Docs site (localhost:3000); live examples load from the demo on :4200
|
|
109
|
+
npm test # Unit tests
|
|
110
|
+
npm run e2e # E2E tests
|
|
111
|
+
npm run site:build # Full GitHub Pages build: docs at /, demo at /demo/
|
|
576
112
|
```
|
|
577
113
|
|
|
578
|
-
Works with Claude Code, Cursor, Windsurf, GitHub Copilot, and [40+ other agents](https://skills.sh).
|
|
579
|
-
|
|
580
114
|
## License
|
|
581
115
|
|
|
582
116
|
MIT
|