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 CHANGED
@@ -1,24 +1,18 @@
1
1
  # ngx-virtual-dnd
2
2
 
3
- Angular drag-and-drop library optimized for virtual scrolling. Handles thousands of items efficiently by only rendering visible elements.
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
- Inspired by [react-virtualized-dnd](https://github.com/forecast-it/react-virtualized-dnd/).
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
- - **Virtual Scrolling** - Renders only visible items plus overscan buffer
10
- - **Dynamic Item Heights** - Auto-measured via ResizeObserver with O(log N) lookups
11
- - **Smooth Drag & Drop** - 60fps with RAF throttling
12
- - **Cross-List Support** - Drag between multiple lists with group filtering
13
- - **Auto-Scroll** - Configurable edge scrolling with speed/threshold control
14
- - **Drag Handles** - Restrict drag initiation to specific elements
15
- - **Container Constraints** - Constrain drag preview to container boundaries
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
- ## Quick Start
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
- moveItem,
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
- <!-- Lists wrapped in a group -->
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>
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="list-1"
60
- group="my-group"
61
- [items]="list1()"
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]="getItemId"
76
- [itemTemplate]="itemTpl"
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 MyComponent {
86
- list1 = signal<Item[]>([...]);
87
- list2 = signal<Item[]>([...]);
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
- getItemId = (item: Item) => item.id;
75
+ readonly taskId = (task: Task): string => task.id;
90
76
 
91
77
  onDrop(event: DropEvent): void {
92
- moveItem(event, {
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
- **That's it!** `VirtualSortableListComponent` handles placeholder positioning, sticky items during drag, and virtual scroll integration automatically.
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
- 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.
85
+ ## Documentation
323
86
 
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:
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
- ```html
327
- <vdnd-sortable-list ... (placeholderMove)="onPlaceholderMove($event)" />
328
- ```
329
-
330
- ```typescript
331
- import { Haptics } from '@capacitor/haptics';
91
+ ## AI agent skills
332
92
 
333
- onPlaceholderMove(event: PlaceholderMoveEvent): void {
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
- `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
-
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
- ## Keyboard Navigation
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
- ```bash
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
- npx skills add gultyayev/ngx-virtual-dnd
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