ngx-virtual-dnd 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/LICENSE +21 -0
- package/README.md +605 -0
- package/fesm2022/ngx-virtual-dnd.mjs +3154 -0
- package/fesm2022/ngx-virtual-dnd.mjs.map +1 -0
- package/package.json +47 -0
- package/types/ngx-virtual-dnd.d.ts +1250 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sergey Gultyayev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
# ngx-virtual-dnd
|
|
2
|
+
|
|
3
|
+
A performant drag-and-drop library for Angular that works seamlessly with virtual scrolling. Built for large lists where traditional drag-and-drop solutions fail due to DOM virtualization.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Drag-and-drop with virtual scroll support
|
|
8
|
+
- Automatic scrolling when dragging near container edges
|
|
9
|
+
- Multiple droppable containers with group-based restrictions
|
|
10
|
+
- Mouse and touch support
|
|
11
|
+
- Accessible with ARIA attributes and keyboard support
|
|
12
|
+
- Signal-based state management
|
|
13
|
+
- Simplified high-level API with `VirtualSortableListComponent`
|
|
14
|
+
- Group inheritance with `DroppableGroupDirective`
|
|
15
|
+
- Utility functions for drop handling (`moveItem`, `reorderItems`)
|
|
16
|
+
- **External scroll container support** via `vdndScrollable` directive
|
|
17
|
+
- No external dependencies (except Angular)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install ngx-virtual-dnd
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Angular 21+
|
|
28
|
+
|
|
29
|
+
## Quick Start (Simplified API)
|
|
30
|
+
|
|
31
|
+
The easiest way to get started is with the high-level `VirtualSortableListComponent`:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import {
|
|
35
|
+
DragPreviewComponent,
|
|
36
|
+
DraggableDirective,
|
|
37
|
+
VirtualSortableListComponent,
|
|
38
|
+
DroppableGroupDirective,
|
|
39
|
+
PlaceholderComponent,
|
|
40
|
+
DropEvent,
|
|
41
|
+
moveItem,
|
|
42
|
+
} from 'ngx-virtual-dnd';
|
|
43
|
+
|
|
44
|
+
@Component({
|
|
45
|
+
imports: [
|
|
46
|
+
DragPreviewComponent,
|
|
47
|
+
DraggableDirective,
|
|
48
|
+
VirtualSortableListComponent,
|
|
49
|
+
DroppableGroupDirective,
|
|
50
|
+
PlaceholderComponent,
|
|
51
|
+
],
|
|
52
|
+
template: `
|
|
53
|
+
<!-- Item template -->
|
|
54
|
+
<ng-template #itemTpl let-item let-isPlaceholder="isPlaceholder">
|
|
55
|
+
@if (isPlaceholder) {
|
|
56
|
+
<vdnd-placeholder [height]="50"></vdnd-placeholder>
|
|
57
|
+
} @else {
|
|
58
|
+
<div class="item" vdndDraggable="{{ item.id }}" [vdndDraggableData]="item">
|
|
59
|
+
{{ item.name }}
|
|
60
|
+
</div>
|
|
61
|
+
}
|
|
62
|
+
</ng-template>
|
|
63
|
+
|
|
64
|
+
<!-- Use vdndGroup to set group for all children -->
|
|
65
|
+
<div vdndGroup="my-group">
|
|
66
|
+
<!-- Sortable list - handles placeholder and sticky items automatically! -->
|
|
67
|
+
<vdnd-sortable-list
|
|
68
|
+
droppableId="list-1"
|
|
69
|
+
group="my-group"
|
|
70
|
+
[items]="list1()"
|
|
71
|
+
[itemHeight]="50"
|
|
72
|
+
[containerHeight]="400"
|
|
73
|
+
[itemIdFn]="getItemId"
|
|
74
|
+
[itemTemplate]="itemTpl"
|
|
75
|
+
(drop)="onDrop($event)"
|
|
76
|
+
>
|
|
77
|
+
</vdnd-sortable-list>
|
|
78
|
+
|
|
79
|
+
<vdnd-sortable-list
|
|
80
|
+
droppableId="list-2"
|
|
81
|
+
group="my-group"
|
|
82
|
+
[items]="list2()"
|
|
83
|
+
[itemHeight]="50"
|
|
84
|
+
[containerHeight]="400"
|
|
85
|
+
[itemIdFn]="getItemId"
|
|
86
|
+
[itemTemplate]="itemTpl"
|
|
87
|
+
(drop)="onDrop($event)"
|
|
88
|
+
>
|
|
89
|
+
</vdnd-sortable-list>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<vdnd-drag-preview></vdnd-drag-preview>
|
|
93
|
+
`,
|
|
94
|
+
})
|
|
95
|
+
export class MyComponent {
|
|
96
|
+
list1 = signal<Item[]>([]);
|
|
97
|
+
list2 = signal<Item[]>([]);
|
|
98
|
+
|
|
99
|
+
getItemId = (item: Item) => item.id;
|
|
100
|
+
|
|
101
|
+
// One-liner drop handler using moveItem utility!
|
|
102
|
+
onDrop(event: DropEvent): void {
|
|
103
|
+
moveItem(event, {
|
|
104
|
+
'list-1': this.list1,
|
|
105
|
+
'list-2': this.list2,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Quick Start (Low-Level API)
|
|
112
|
+
|
|
113
|
+
For more control, use the individual directives and components:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import {
|
|
117
|
+
DragPreviewComponent,
|
|
118
|
+
DraggableDirective,
|
|
119
|
+
DroppableDirective,
|
|
120
|
+
VirtualScrollContainerComponent,
|
|
121
|
+
PlaceholderComponent,
|
|
122
|
+
DropEvent,
|
|
123
|
+
DragStateService,
|
|
124
|
+
} from 'ngx-virtual-dnd';
|
|
125
|
+
|
|
126
|
+
@Component({
|
|
127
|
+
imports: [
|
|
128
|
+
DragPreviewComponent,
|
|
129
|
+
DraggableDirective,
|
|
130
|
+
DroppableDirective,
|
|
131
|
+
VirtualScrollContainerComponent,
|
|
132
|
+
PlaceholderComponent,
|
|
133
|
+
],
|
|
134
|
+
template: `
|
|
135
|
+
<!-- Item template with manual placeholder handling -->
|
|
136
|
+
<ng-template #itemTpl let-item>
|
|
137
|
+
@if (item.isPlaceholder) {
|
|
138
|
+
<vdnd-placeholder [height]="50"></vdnd-placeholder>
|
|
139
|
+
} @else {
|
|
140
|
+
<div
|
|
141
|
+
class="item"
|
|
142
|
+
vdndDraggable="{{ item.id }}"
|
|
143
|
+
vdndDraggableGroup="my-group"
|
|
144
|
+
[vdndDraggableData]="item"
|
|
145
|
+
>
|
|
146
|
+
{{ item.name }}
|
|
147
|
+
</div>
|
|
148
|
+
}
|
|
149
|
+
</ng-template>
|
|
150
|
+
|
|
151
|
+
<!-- Droppable container with virtual scroll -->
|
|
152
|
+
<div vdndDroppable="list-1" vdndDroppableGroup="my-group" (drop)="onDrop($event)">
|
|
153
|
+
<vdnd-virtual-scroll
|
|
154
|
+
[items]="itemsWithPlaceholder()"
|
|
155
|
+
[itemHeight]="50"
|
|
156
|
+
[containerHeight]="400"
|
|
157
|
+
[stickyItemIds]="stickyIds()"
|
|
158
|
+
[itemIdFn]="getItemId"
|
|
159
|
+
[trackByFn]="trackById"
|
|
160
|
+
[itemTemplate]="itemTpl"
|
|
161
|
+
>
|
|
162
|
+
</vdnd-virtual-scroll>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<vdnd-drag-preview></vdnd-drag-preview>
|
|
166
|
+
`,
|
|
167
|
+
})
|
|
168
|
+
export class MyComponent {
|
|
169
|
+
private dragState = inject(DragStateService);
|
|
170
|
+
items = signal<Item[]>([]);
|
|
171
|
+
|
|
172
|
+
// Must manually compute sticky IDs
|
|
173
|
+
stickyIds = computed(() => {
|
|
174
|
+
const draggedItem = this.dragState.draggedItem();
|
|
175
|
+
return draggedItem ? [draggedItem.draggableId] : [];
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Must manually insert placeholder
|
|
179
|
+
itemsWithPlaceholder = computed(() => {
|
|
180
|
+
// ... placeholder insertion logic
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
getItemId = (item: Item) => item.id;
|
|
184
|
+
trackById = (index: number, item: Item) => item.id;
|
|
185
|
+
|
|
186
|
+
onDrop(event: DropEvent): void {
|
|
187
|
+
// Manual drop handling logic
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## API Reference
|
|
193
|
+
|
|
194
|
+
### DraggableDirective
|
|
195
|
+
|
|
196
|
+
Makes an element draggable.
|
|
197
|
+
|
|
198
|
+
```html
|
|
199
|
+
<div
|
|
200
|
+
vdndDraggable="unique-id"
|
|
201
|
+
vdndDraggableGroup="group-name"
|
|
202
|
+
[vdndDraggableData]="data"
|
|
203
|
+
[disabled]="false"
|
|
204
|
+
[dragHandle]=".handle"
|
|
205
|
+
[dragThreshold]="5"
|
|
206
|
+
(dragStart)="onDragStart($event)"
|
|
207
|
+
(dragMove)="onDragMove($event)"
|
|
208
|
+
(dragEnd)="onDragEnd($event)"
|
|
209
|
+
></div>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
| Input | Type | Description |
|
|
213
|
+
| -------------------- | --------- | ------------------------------------------------ |
|
|
214
|
+
| `vdndDraggable` | `string` | Unique identifier for the draggable |
|
|
215
|
+
| `vdndDraggableGroup` | `string` | Group name for restricting drop targets |
|
|
216
|
+
| `vdndDraggableData` | `unknown` | Optional data attached to the draggable |
|
|
217
|
+
| `disabled` | `boolean` | Whether dragging is disabled |
|
|
218
|
+
| `dragHandle` | `string` | CSS selector for drag handle element |
|
|
219
|
+
| `dragThreshold` | `number` | Minimum distance before drag starts (default: 5) |
|
|
220
|
+
|
|
221
|
+
| Output | Type | Description |
|
|
222
|
+
| ----------- | ---------------- | ---------------------------- |
|
|
223
|
+
| `dragStart` | `DragStartEvent` | Emitted when drag starts |
|
|
224
|
+
| `dragMove` | `DragMoveEvent` | Emitted during drag movement |
|
|
225
|
+
| `dragEnd` | `DragEndEvent` | Emitted when drag ends |
|
|
226
|
+
|
|
227
|
+
### DroppableDirective
|
|
228
|
+
|
|
229
|
+
Marks an element as a valid drop target.
|
|
230
|
+
|
|
231
|
+
```html
|
|
232
|
+
<div
|
|
233
|
+
vdndDroppable="list-id"
|
|
234
|
+
vdndDroppableGroup="group-name"
|
|
235
|
+
[vdndDroppableData]="data"
|
|
236
|
+
[disabled]="false"
|
|
237
|
+
[autoScrollEnabled]="true"
|
|
238
|
+
[autoScrollConfig]="{ threshold: 50, maxSpeed: 15 }"
|
|
239
|
+
(dragEnter)="onDragEnter($event)"
|
|
240
|
+
(dragLeave)="onDragLeave($event)"
|
|
241
|
+
(dragOver)="onDragOver($event)"
|
|
242
|
+
(drop)="onDrop($event)"
|
|
243
|
+
></div>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
| Input | Type | Description |
|
|
247
|
+
| -------------------- | ------------------ | --------------------------------------------- |
|
|
248
|
+
| `vdndDroppable` | `string` | Unique identifier for the droppable |
|
|
249
|
+
| `vdndDroppableGroup` | `string` | Group name (must match draggable group) |
|
|
250
|
+
| `vdndDroppableData` | `unknown` | Optional data attached to the droppable |
|
|
251
|
+
| `disabled` | `boolean` | Whether dropping is disabled |
|
|
252
|
+
| `autoScrollEnabled` | `boolean` | Enable auto-scroll near edges (default: true) |
|
|
253
|
+
| `autoScrollConfig` | `AutoScrollConfig` | Auto-scroll configuration |
|
|
254
|
+
|
|
255
|
+
| Output | Type | Description |
|
|
256
|
+
| ----------- | ---------------- | ----------------------------------------------- |
|
|
257
|
+
| `dragEnter` | `DragEnterEvent` | Emitted when a draggable enters |
|
|
258
|
+
| `dragLeave` | `DragLeaveEvent` | Emitted when a draggable leaves |
|
|
259
|
+
| `dragOver` | `DragOverEvent` | Emitted while hovering with placeholder updates |
|
|
260
|
+
| `drop` | `DropEvent` | Emitted when an item is dropped |
|
|
261
|
+
|
|
262
|
+
### VirtualScrollContainerComponent
|
|
263
|
+
|
|
264
|
+
A virtual scroll container that only renders visible items.
|
|
265
|
+
|
|
266
|
+
```html
|
|
267
|
+
<vdnd-virtual-scroll
|
|
268
|
+
[items]="items()"
|
|
269
|
+
[itemHeight]="50"
|
|
270
|
+
[containerHeight]="400"
|
|
271
|
+
[overscan]="3"
|
|
272
|
+
[stickyItemIds]="stickyIds()"
|
|
273
|
+
[itemIdFn]="getItemId"
|
|
274
|
+
[trackByFn]="trackById"
|
|
275
|
+
[itemTemplate]="itemTpl"
|
|
276
|
+
(visibleRangeChange)="onRangeChange($event)"
|
|
277
|
+
(scrollPositionChange)="onScroll($event)"
|
|
278
|
+
>
|
|
279
|
+
</vdnd-virtual-scroll>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
| Input | Type | Description |
|
|
283
|
+
| ----------------- | ------------------------------------ | ------------------------------------------------- |
|
|
284
|
+
| `items` | `T[]` | Array of items to render |
|
|
285
|
+
| `itemHeight` | `number` | Height of each item in pixels |
|
|
286
|
+
| `containerHeight` | `number` | Height of the container in pixels |
|
|
287
|
+
| `overscan` | `number` | Items to render above/below viewport (default: 3) |
|
|
288
|
+
| `stickyItemIds` | `string[]` | IDs of items that should always be rendered |
|
|
289
|
+
| `itemIdFn` | `(item: T) => string` | Function to get unique ID from item |
|
|
290
|
+
| `trackByFn` | `(index: number, item: T) => string` | Track-by function for the loop |
|
|
291
|
+
| `itemTemplate` | `TemplateRef` | Template for rendering each item |
|
|
292
|
+
|
|
293
|
+
### VirtualForDirective with Custom Scroll Containers
|
|
294
|
+
|
|
295
|
+
For advanced use cases where you need virtual scrolling inside an external scroll container (e.g., a custom scrollable div, a framework-provided scroll host), use the `vdndScrollable` directive with `*vdndVirtualFor`:
|
|
296
|
+
|
|
297
|
+
```html
|
|
298
|
+
<div vdndScrollable style="overflow: auto; height: 400px;">
|
|
299
|
+
<ng-container
|
|
300
|
+
*vdndVirtualFor="
|
|
301
|
+
let item of items();
|
|
302
|
+
itemHeight: 50;
|
|
303
|
+
trackBy: trackById;
|
|
304
|
+
droppableId: 'list-1';
|
|
305
|
+
let isPlaceholder = isPlaceholder
|
|
306
|
+
"
|
|
307
|
+
>
|
|
308
|
+
@if (isPlaceholder) {
|
|
309
|
+
<vdnd-placeholder [height]="50"></vdnd-placeholder>
|
|
310
|
+
} @else {
|
|
311
|
+
<div vdndDraggable="{{ item.id }}" vdndDraggableGroup="my-group">{{ item.name }}</div>
|
|
312
|
+
}
|
|
313
|
+
</ng-container>
|
|
314
|
+
</div>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The `vdndScrollable` directive marks the element as the scroll container and provides it to `*vdndVirtualFor` via dependency injection.
|
|
318
|
+
|
|
319
|
+
#### ScrollableDirective
|
|
320
|
+
|
|
321
|
+
Marks an element as a scroll container for virtual scrolling.
|
|
322
|
+
|
|
323
|
+
```html
|
|
324
|
+
<div
|
|
325
|
+
vdndScrollable
|
|
326
|
+
[scrollContainerId]="'my-scroll-container'"
|
|
327
|
+
[autoScrollEnabled]="true"
|
|
328
|
+
[autoScrollConfig]="{ threshold: 50, maxSpeed: 15 }"
|
|
329
|
+
style="overflow: auto; height: 400px;"
|
|
330
|
+
>
|
|
331
|
+
<!-- content with *vdndVirtualFor -->
|
|
332
|
+
</div>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
| Input | Type | Description |
|
|
336
|
+
| ------------------- | ------------------ | --------------------------------------------- |
|
|
337
|
+
| `scrollContainerId` | `string` | Optional ID for auto-scroll registration |
|
|
338
|
+
| `autoScrollEnabled` | `boolean` | Enable auto-scroll near edges (default: true) |
|
|
339
|
+
| `autoScrollConfig` | `AutoScrollConfig` | Auto-scroll configuration |
|
|
340
|
+
|
|
341
|
+
#### VirtualForDirective
|
|
342
|
+
|
|
343
|
+
A structural directive for virtual scrolling within custom scroll containers.
|
|
344
|
+
|
|
345
|
+
```html
|
|
346
|
+
<ng-container
|
|
347
|
+
*vdndVirtualFor="
|
|
348
|
+
let item of items();
|
|
349
|
+
itemHeight: 50;
|
|
350
|
+
trackBy: trackById;
|
|
351
|
+
overscan: 3;
|
|
352
|
+
droppableId: 'list-1';
|
|
353
|
+
autoPlaceholder: true;
|
|
354
|
+
let index = index;
|
|
355
|
+
let isPlaceholder = isPlaceholder
|
|
356
|
+
"
|
|
357
|
+
>
|
|
358
|
+
<!-- item template -->
|
|
359
|
+
</ng-container>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
| Input | Type | Description |
|
|
363
|
+
| ----------------- | ------------------------------------- | --------------------------------------------- |
|
|
364
|
+
| `of` | `T[]` | Array of items to iterate over |
|
|
365
|
+
| `itemHeight` | `number` | Height of each item in pixels |
|
|
366
|
+
| `trackBy` | `(index: number, item: T) => unknown` | Track-by function for efficient updates |
|
|
367
|
+
| `overscan` | `number` | Items to render outside viewport (default: 3) |
|
|
368
|
+
| `droppableId` | `string` | Droppable ID for auto-placeholder support |
|
|
369
|
+
| `autoPlaceholder` | `boolean` | Auto-insert placeholder (default: true) |
|
|
370
|
+
|
|
371
|
+
| Context Variable | Type | Description |
|
|
372
|
+
| ---------------- | --------- | -------------------------------------------- |
|
|
373
|
+
| `$implicit` | `T` | The item data |
|
|
374
|
+
| `index` | `number` | Item index (-1 for placeholders) |
|
|
375
|
+
| `first` | `boolean` | Whether this is the first visible item |
|
|
376
|
+
| `last` | `boolean` | Whether this is the last visible item |
|
|
377
|
+
| `count` | `number` | Total item count |
|
|
378
|
+
| `isPlaceholder` | `boolean` | Whether this is an auto-inserted placeholder |
|
|
379
|
+
|
|
380
|
+
### DragPreviewComponent
|
|
381
|
+
|
|
382
|
+
Renders a preview that follows the cursor during drag.
|
|
383
|
+
|
|
384
|
+
```html
|
|
385
|
+
<vdnd-drag-preview [cursorOffset]="{ x: 8, y: 8 }">
|
|
386
|
+
<ng-template let-data let-id="draggableId" let-droppableId="droppableId">
|
|
387
|
+
<div class="preview">{{ data?.name }}</div>
|
|
388
|
+
</ng-template>
|
|
389
|
+
</vdnd-drag-preview>
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### PlaceholderComponent
|
|
393
|
+
|
|
394
|
+
A visual placeholder indicating where the item will be inserted.
|
|
395
|
+
|
|
396
|
+
```html
|
|
397
|
+
<vdnd-placeholder [height]="50"></vdnd-placeholder>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### VirtualSortableListComponent
|
|
401
|
+
|
|
402
|
+
A high-level component that combines droppable, virtual scroll, and placeholder functionality. **Recommended for most use cases.**
|
|
403
|
+
|
|
404
|
+
```html
|
|
405
|
+
<vdnd-sortable-list
|
|
406
|
+
droppableId="list-1"
|
|
407
|
+
group="my-group"
|
|
408
|
+
[items]="items()"
|
|
409
|
+
[itemHeight]="50"
|
|
410
|
+
[itemIdFn]="getItemId"
|
|
411
|
+
[itemTemplate]="itemTpl"
|
|
412
|
+
[placeholderTemplate]="placeholderTpl"
|
|
413
|
+
(drop)="onDrop($event)"
|
|
414
|
+
>
|
|
415
|
+
</vdnd-sortable-list>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
| Input | Type | Description |
|
|
419
|
+
| --------------------- | --------------------- | ---------------------------------------- |
|
|
420
|
+
| `droppableId` | `string` | Unique identifier for this list |
|
|
421
|
+
| `group` | `string` | Drag-and-drop group name |
|
|
422
|
+
| `items` | `T[]` | Array of items to render |
|
|
423
|
+
| `itemHeight` | `number` | Height of each item in pixels |
|
|
424
|
+
| `itemIdFn` | `(item: T) => string` | Function to get unique ID from item |
|
|
425
|
+
| `itemTemplate` | `TemplateRef` | Template for rendering each item |
|
|
426
|
+
| `trackByFn` | `Function` | Optional track-by (defaults to itemIdFn) |
|
|
427
|
+
| `placeholderTemplate` | `TemplateRef` | Optional custom placeholder template |
|
|
428
|
+
| `containerHeight` | `number` | Optional explicit container height |
|
|
429
|
+
| `disabled` | `boolean` | Whether this list is disabled |
|
|
430
|
+
|
|
431
|
+
| Output | Type | Description |
|
|
432
|
+
| -------------------- | -------------------- | ---------------------------------- |
|
|
433
|
+
| `drop` | `DropEvent` | Emitted when an item is dropped |
|
|
434
|
+
| `dragEnter` | `DragEnterEvent` | Emitted when a draggable enters |
|
|
435
|
+
| `dragLeave` | `DragLeaveEvent` | Emitted when a draggable leaves |
|
|
436
|
+
| `visibleRangeChange` | `VisibleRangeChange` | Emitted when visible range changes |
|
|
437
|
+
|
|
438
|
+
### DroppableGroupDirective
|
|
439
|
+
|
|
440
|
+
Provides group context to child draggables and droppables, eliminating repetitive `vdndDraggableGroup` and `vdndDroppableGroup` attributes.
|
|
441
|
+
|
|
442
|
+
```html
|
|
443
|
+
<!-- Without group directive (verbose) -->
|
|
444
|
+
<div vdndDroppable="list-1" vdndDroppableGroup="my-group">
|
|
445
|
+
<div vdndDraggable="item-1" vdndDraggableGroup="my-group">Item 1</div>
|
|
446
|
+
<div vdndDraggable="item-2" vdndDraggableGroup="my-group">Item 2</div>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<!-- With group directive (concise) -->
|
|
450
|
+
<div vdndGroup="my-group">
|
|
451
|
+
<div vdndDroppable="list-1">
|
|
452
|
+
<div vdndDraggable="item-1">Item 1</div>
|
|
453
|
+
<div vdndDraggable="item-2">Item 2</div>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Drop Utilities
|
|
459
|
+
|
|
460
|
+
Utility functions for common drop handling patterns:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
import { moveItem, reorderItems, applyMove, isNoOpDrop } from 'ngx-virtual-dnd';
|
|
464
|
+
|
|
465
|
+
// Move items between signal-based lists
|
|
466
|
+
moveItem(event, {
|
|
467
|
+
'list-1': this.list1,
|
|
468
|
+
'list-2': this.list2,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Reorder within a single list
|
|
472
|
+
reorderItems(event, this.items);
|
|
473
|
+
|
|
474
|
+
// Immutable version (returns new arrays)
|
|
475
|
+
const updated = applyMove(event, {
|
|
476
|
+
'list-1': this.list1(),
|
|
477
|
+
'list-2': this.list2(),
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Check if drop is a no-op (same position)
|
|
481
|
+
if (isNoOpDrop(event)) return;
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### DragStateService
|
|
485
|
+
|
|
486
|
+
Central service for accessing drag state. Inject to build custom integrations.
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
@Injectable({ providedIn: 'root' })
|
|
490
|
+
export class DragStateService {
|
|
491
|
+
readonly isDragging: Signal<boolean>;
|
|
492
|
+
readonly draggedItem: Signal<DraggedItem | null>;
|
|
493
|
+
readonly sourceDroppableId: Signal<string | null>;
|
|
494
|
+
readonly activeDroppableId: Signal<string | null>;
|
|
495
|
+
readonly placeholderId: Signal<string | null>;
|
|
496
|
+
readonly cursorPosition: Signal<CursorPosition | null>;
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Custom Scroll Container (Advanced)
|
|
501
|
+
|
|
502
|
+
For advanced use cases, you can implement your own scroll container by providing the `VDND_SCROLL_CONTAINER` token:
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
import { VDND_SCROLL_CONTAINER, VdndScrollContainer } from 'ngx-virtual-dnd';
|
|
506
|
+
|
|
507
|
+
@Directive({
|
|
508
|
+
selector: '[myCustomScrollable]',
|
|
509
|
+
providers: [{ provide: VDND_SCROLL_CONTAINER, useExisting: MyCustomScrollableDirective }],
|
|
510
|
+
})
|
|
511
|
+
export class MyCustomScrollableDirective implements VdndScrollContainer {
|
|
512
|
+
readonly #elementRef = inject(ElementRef<HTMLElement>);
|
|
513
|
+
readonly #scrollTop = signal(0);
|
|
514
|
+
readonly #containerHeight = signal(0);
|
|
515
|
+
|
|
516
|
+
get nativeElement(): HTMLElement {
|
|
517
|
+
return this.#elementRef.nativeElement;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
scrollTop(): number {
|
|
521
|
+
return this.#scrollTop();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
containerHeight(): number {
|
|
525
|
+
return this.#containerHeight();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
scrollTo(options: ScrollToOptions): void {
|
|
529
|
+
this.nativeElement.scrollTo(options);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Set up scroll listeners and resize observers to update signals...
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The `VdndScrollContainer` interface requires:
|
|
537
|
+
|
|
538
|
+
| Method/Property | Type | Description |
|
|
539
|
+
| ------------------- | ------------------------------------ | ------------------------------------------ |
|
|
540
|
+
| `nativeElement` | `HTMLElement` | The scrollable DOM element |
|
|
541
|
+
| `scrollTop()` | `number` | Current scroll position (must be reactive) |
|
|
542
|
+
| `containerHeight()` | `number` | Container height (must be reactive) |
|
|
543
|
+
| `scrollTo()` | `(options: ScrollToOptions) => void` | Scroll to a position |
|
|
544
|
+
|
|
545
|
+
**Important:** The `scrollTop()` and `containerHeight()` methods must be backed by signals so that changes trigger re-computation in `*vdndVirtualFor`.
|
|
546
|
+
|
|
547
|
+
## Event Types
|
|
548
|
+
|
|
549
|
+
### DropEvent
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
interface DropEvent {
|
|
553
|
+
source: {
|
|
554
|
+
draggableId: string;
|
|
555
|
+
droppableId: string;
|
|
556
|
+
index: number;
|
|
557
|
+
data?: unknown;
|
|
558
|
+
};
|
|
559
|
+
destination: {
|
|
560
|
+
droppableId: string;
|
|
561
|
+
placeholderId: string;
|
|
562
|
+
index: number;
|
|
563
|
+
data?: unknown;
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### AutoScrollConfig
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
interface AutoScrollConfig {
|
|
572
|
+
threshold: number; // Distance from edge to start scrolling (default: 50)
|
|
573
|
+
maxSpeed: number; // Maximum scroll speed in px/frame (default: 15)
|
|
574
|
+
accelerate: boolean; // Accelerate based on distance from edge (default: true)
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## CSS Classes
|
|
579
|
+
|
|
580
|
+
The library adds CSS classes for styling drag states:
|
|
581
|
+
|
|
582
|
+
| Class | Element | Applied When |
|
|
583
|
+
| ------------------------- | --------- | --------------------------- |
|
|
584
|
+
| `vdnd-draggable` | Draggable | Always |
|
|
585
|
+
| `vdnd-draggable-dragging` | Draggable | While being dragged |
|
|
586
|
+
| `vdnd-draggable-disabled` | Draggable | When disabled |
|
|
587
|
+
| `vdnd-droppable` | Droppable | Always |
|
|
588
|
+
| `vdnd-droppable-active` | Droppable | When a draggable is over it |
|
|
589
|
+
| `vdnd-droppable-disabled` | Droppable | When disabled |
|
|
590
|
+
|
|
591
|
+
## How It Works
|
|
592
|
+
|
|
593
|
+
1. **Virtual Scrolling**: Only items in the visible viewport (plus overscan) are rendered. This allows lists with thousands of items to perform well.
|
|
594
|
+
|
|
595
|
+
2. **Sticky Items**: During drag, the dragged item is marked as "sticky" so it remains rendered even when scrolled out of view.
|
|
596
|
+
|
|
597
|
+
3. **Position Detection**: Uses `document.elementFromPoint()` with temporarily hidden drag preview to detect what's under the cursor.
|
|
598
|
+
|
|
599
|
+
4. **Auto-scroll**: When dragging near container edges, the container automatically scrolls to reveal more items.
|
|
600
|
+
|
|
601
|
+
5. **Group-based Restrictions**: Draggables can only be dropped on droppables with the same group name.
|
|
602
|
+
|
|
603
|
+
## License
|
|
604
|
+
|
|
605
|
+
MIT
|