ngx-virtual-dnd 3.2.0-alpha.0 → 3.2.0-alpha.2
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 +362 -76
- package/fesm2022/ngx-virtual-dnd.mjs.map +1 -1
- package/package.json +2 -2
- package/types/ngx-virtual-dnd.d.ts +35 -6
|
@@ -456,12 +456,17 @@ const VDND_VIRTUAL_VIEWPORT = new InjectionToken('VDND_VIRTUAL_VIEWPORT');
|
|
|
456
456
|
const DEFAULT_SHIFT_DURATION = 200;
|
|
457
457
|
/** Default shift animation easing used when the config omits `shiftEasing`. */
|
|
458
458
|
const DEFAULT_SHIFT_EASING = 'cubic-bezier(0.2, 0, 0, 1)';
|
|
459
|
+
/** Default drop animation duration (ms) used when the config omits `dropDuration`. */
|
|
460
|
+
const DEFAULT_DROP_DURATION = 200;
|
|
461
|
+
/** Default drop animation easing used when the config omits `dropEasing`. */
|
|
462
|
+
const DEFAULT_DROP_EASING = 'cubic-bezier(0.2, 0, 0, 1)';
|
|
459
463
|
/**
|
|
460
464
|
* Opt-in animation configuration. When provided, items displaced by the placeholder
|
|
461
|
-
* slide into their new position instead of jumping
|
|
465
|
+
* slide into their new position instead of jumping, and on drop the drag preview glides
|
|
466
|
+
* into the item's final position. Resolved through the element
|
|
462
467
|
* injector, so it can be provided app-wide or per component subtree.
|
|
463
468
|
*
|
|
464
|
-
*
|
|
469
|
+
* Animations are skipped when the user prefers reduced motion. Values are read each
|
|
465
470
|
* time an animation starts, so getters can toggle the animation at runtime.
|
|
466
471
|
*
|
|
467
472
|
* @example
|
|
@@ -1235,9 +1240,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
1235
1240
|
* AutoScrollService registers here to perform edge-scroll + immediate
|
|
1236
1241
|
* placeholder recalculation in the same synchronous frame — preserving
|
|
1237
1242
|
* the Safari synchronous-callback constraint.
|
|
1238
|
-
* 2. Main compute phase (COMPUTE+WRITE): onTick is called with the
|
|
1239
|
-
* cursor
|
|
1240
|
-
* autoscroll-only frames (cursor stationary) incur
|
|
1243
|
+
* 2. Main compute phase (COMPUTE+WRITE): onTick is called every frame with the
|
|
1244
|
+
* latest cursor and a `cursorDirty` flag. Callers skip their hit-test when
|
|
1245
|
+
* the flag is false, so autoscroll-only frames (cursor stationary) incur
|
|
1246
|
+
* zero hit-test work.
|
|
1241
1247
|
*
|
|
1242
1248
|
* PointerDragHandler calls `queueCursorUpdate()` instead of managing its own
|
|
1243
1249
|
* RAF per pointermove. This coalesces all moves within a frame into one update
|
|
@@ -1882,6 +1888,8 @@ class KeyboardDragService {
|
|
|
1882
1888
|
#dragState = inject(DragStateService);
|
|
1883
1889
|
/** Total item count for the current droppable (set by droppable on keyboard drag start) */
|
|
1884
1890
|
#totalItemCount = signal(0, ...(ngDevMode ? [{ debugName: "#totalItemCount" }] : []));
|
|
1891
|
+
/** Per-droppable callbacks that scroll the placeholder into view (see `registerRevealer`) */
|
|
1892
|
+
#revealers = new Map();
|
|
1885
1893
|
/** Whether a keyboard drag is currently active */
|
|
1886
1894
|
isActive = computed(() => this.#dragState.isKeyboardDrag() && this.#dragState.isDragging(), ...(ngDevMode ? [{ debugName: "isActive" }] : []));
|
|
1887
1895
|
/** Current target index during keyboard navigation */
|
|
@@ -1917,9 +1925,10 @@ class KeyboardDragService {
|
|
|
1917
1925
|
if (!this.isActive()) {
|
|
1918
1926
|
return targetIndex;
|
|
1919
1927
|
}
|
|
1920
|
-
const
|
|
1921
|
-
const clampedIndex = Math.max(0, Math.min(targetIndex,
|
|
1928
|
+
const maxIndex = this.#maxTargetIndex(this.activeDroppableId());
|
|
1929
|
+
const clampedIndex = Math.max(0, Math.min(targetIndex, maxIndex));
|
|
1922
1930
|
this.#dragState.setKeyboardTargetIndex(clampedIndex);
|
|
1931
|
+
this.#revealActivePlaceholder();
|
|
1923
1932
|
return clampedIndex;
|
|
1924
1933
|
}
|
|
1925
1934
|
/**
|
|
@@ -1947,8 +1956,26 @@ class KeyboardDragService {
|
|
|
1947
1956
|
return;
|
|
1948
1957
|
}
|
|
1949
1958
|
this.#totalItemCount.set(totalItemCount);
|
|
1950
|
-
const clampedIndex = Math.max(0, Math.min(targetIndex,
|
|
1959
|
+
const clampedIndex = Math.max(0, Math.min(targetIndex, this.#maxTargetIndex(droppableId)));
|
|
1951
1960
|
this.#dragState.setKeyboardActiveDroppable(droppableId, clampedIndex);
|
|
1961
|
+
this.#revealActivePlaceholder();
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Register the callback that scrolls a droppable's placeholder into view. It runs
|
|
1965
|
+
* synchronously on every keyboard move into or within that droppable, so a drop that
|
|
1966
|
+
* follows before the next render still lands inside the rendered range.
|
|
1967
|
+
*/
|
|
1968
|
+
registerRevealer(droppableId, reveal) {
|
|
1969
|
+
this.#revealers.set(droppableId, reveal);
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Unregister a droppable's reveal callback. Ignored if another callback has since been
|
|
1973
|
+
* registered under the same ID.
|
|
1974
|
+
*/
|
|
1975
|
+
unregisterRevealer(droppableId, reveal) {
|
|
1976
|
+
if (this.#revealers.get(droppableId) === reveal) {
|
|
1977
|
+
this.#revealers.delete(droppableId);
|
|
1978
|
+
}
|
|
1952
1979
|
}
|
|
1953
1980
|
/**
|
|
1954
1981
|
* Complete the keyboard drag (Space or Enter to drop).
|
|
@@ -1974,6 +2001,22 @@ class KeyboardDragService {
|
|
|
1974
2001
|
setTotalItemCount(count) {
|
|
1975
2002
|
this.#totalItemCount.set(count);
|
|
1976
2003
|
}
|
|
2004
|
+
#revealActivePlaceholder() {
|
|
2005
|
+
const droppableId = this.activeDroppableId();
|
|
2006
|
+
if (droppableId !== null) {
|
|
2007
|
+
this.#revealers.get(droppableId)?.();
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Last valid target index in the given droppable. Targets use the drop convention (the item's
|
|
2012
|
+
* final index after it leaves its source), so the source list ends at `totalItemCount - 1`,
|
|
2013
|
+
* while any other list can be appended to at `totalItemCount`.
|
|
2014
|
+
*/
|
|
2015
|
+
#maxTargetIndex(droppableId) {
|
|
2016
|
+
const totalItems = this.#totalItemCount();
|
|
2017
|
+
const isSourceList = droppableId !== null && droppableId === this.#dragState.sourceDroppableId();
|
|
2018
|
+
return isSourceList ? Math.max(0, totalItems - 1) : totalItems;
|
|
2019
|
+
}
|
|
1977
2020
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: KeyboardDragService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1978
2021
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: KeyboardDragService, providedIn: 'root' });
|
|
1979
2022
|
}
|
|
@@ -2451,7 +2494,7 @@ const MIN_SHIFT_PX = 0.5;
|
|
|
2451
2494
|
* FLIP animator for items displaced by the drag placeholder.
|
|
2452
2495
|
*
|
|
2453
2496
|
* Call `beforeUpdate()` before the DOM reflects a new drag/placeholder state. When the
|
|
2454
|
-
* placeholder moved, it snapshots every rendered element's visual position, then after
|
|
2497
|
+
* placeholder moved (or the drag ended), it snapshots every rendered element's visual position, then after
|
|
2455
2498
|
* the render measures again and plays a compositor-only `transform` animation from the
|
|
2456
2499
|
* old position to the new one.
|
|
2457
2500
|
*
|
|
@@ -2488,11 +2531,19 @@ class ShiftAnimator {
|
|
|
2488
2531
|
this.#wasDragging = isDragging;
|
|
2489
2532
|
this.#lastPlaceholderIndex = placeholderIndex;
|
|
2490
2533
|
if (!isDragging) {
|
|
2491
|
-
// Drop/cancel:
|
|
2492
|
-
|
|
2493
|
-
|
|
2534
|
+
// Drop/cancel: one last pass slides rows from their drag layout into the committed
|
|
2535
|
+
// order. After a drop the rows usually already sit in their final slots, so in-flight
|
|
2536
|
+
// slides simply finish; after a cancel the displaced rows slide back.
|
|
2537
|
+
// A pending snapshot already holds this tick's "before".
|
|
2538
|
+
if (!wasDragging || this.#snapshot) {
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
if (!this.#isEnabled()) {
|
|
2494
2542
|
this.cancelAll();
|
|
2543
|
+
return;
|
|
2495
2544
|
}
|
|
2545
|
+
this.#snapshot = this.#measure();
|
|
2546
|
+
afterNextRender(() => this.#play(), { injector: this.#injector });
|
|
2496
2547
|
return;
|
|
2497
2548
|
}
|
|
2498
2549
|
// Drag start renders the placeholder in the hidden item's slot — nothing moves.
|
|
@@ -2562,7 +2613,7 @@ class ShiftAnimator {
|
|
|
2562
2613
|
#play() {
|
|
2563
2614
|
const first = this.#snapshot;
|
|
2564
2615
|
this.#snapshot = null;
|
|
2565
|
-
if (!first
|
|
2616
|
+
if (!first)
|
|
2566
2617
|
return;
|
|
2567
2618
|
const last = this.#measure();
|
|
2568
2619
|
const stale = [];
|
|
@@ -2662,6 +2713,7 @@ class VirtualScrollContainerComponent {
|
|
|
2662
2713
|
#elementRef = inject((ElementRef));
|
|
2663
2714
|
#autoScrollService = inject(AutoScrollService);
|
|
2664
2715
|
#dragIndexCalculator = inject(DragIndexCalculatorService);
|
|
2716
|
+
#keyboardDrag = inject(KeyboardDragService);
|
|
2665
2717
|
#ngZone = inject(NgZone);
|
|
2666
2718
|
#injector = inject(Injector);
|
|
2667
2719
|
/** Cleanup function for scroll listener */
|
|
@@ -2979,43 +3031,19 @@ class VirtualScrollContainerComponent {
|
|
|
2979
3031
|
untracked(() => animator.beforeUpdate(isDragging, placeholderIndex));
|
|
2980
3032
|
});
|
|
2981
3033
|
}
|
|
2982
|
-
// Keyboard drag autoscroll:
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
const
|
|
2988
|
-
if (
|
|
2989
|
-
return;
|
|
2990
|
-
const targetIndex = this.#dragState.keyboardTargetIndex();
|
|
2991
|
-
if (targetIndex === null)
|
|
2992
|
-
return;
|
|
2993
|
-
const strategy = this.#strategy();
|
|
2994
|
-
strategy.version();
|
|
2995
|
-
const height = this.effectiveHeight();
|
|
2996
|
-
if (height <= 0)
|
|
3034
|
+
// Keyboard drag autoscroll: keep the placeholder visible. Arrow keys call
|
|
3035
|
+
// #revealPlaceholder synchronously (through KeyboardDragService) so a drop right after a
|
|
3036
|
+
// move never lands outside the rendered range; this effect re-runs it when item
|
|
3037
|
+
// measurements or the viewport height change during the drag.
|
|
3038
|
+
effect((onCleanup) => {
|
|
3039
|
+
const droppableId = this.droppableId();
|
|
3040
|
+
if (!droppableId)
|
|
2997
3041
|
return;
|
|
2998
|
-
const
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
const targetTop = strategy.getOffsetForIndex(targetIndex);
|
|
3002
|
-
const targetBottom = targetTop + strategy.getItemHeight(targetIndex);
|
|
3003
|
-
// Calculate visible range
|
|
3004
|
-
const viewportTop = currentScrollTop;
|
|
3005
|
-
const viewportBottom = currentScrollTop + height;
|
|
3006
|
-
// Check if target is fully visible
|
|
3007
|
-
if (targetTop < viewportTop) {
|
|
3008
|
-
// Target is above viewport - scroll up
|
|
3009
|
-
element.scrollTop = targetTop;
|
|
3010
|
-
this.#scrollTop.set(targetTop);
|
|
3011
|
-
}
|
|
3012
|
-
else if (targetBottom > viewportBottom) {
|
|
3013
|
-
// Target is below viewport - scroll down
|
|
3014
|
-
const newScrollTop = targetBottom - height;
|
|
3015
|
-
element.scrollTop = newScrollTop;
|
|
3016
|
-
this.#scrollTop.set(newScrollTop);
|
|
3017
|
-
}
|
|
3042
|
+
const reveal = () => untracked(() => this.#revealPlaceholder());
|
|
3043
|
+
this.#keyboardDrag.registerRevealer(droppableId, reveal);
|
|
3044
|
+
onCleanup(() => this.#keyboardDrag.unregisterRevealer(droppableId, reveal));
|
|
3018
3045
|
});
|
|
3046
|
+
effect(() => this.#revealPlaceholder());
|
|
3019
3047
|
// Preserve scroll position when drag ends at bottom of list.
|
|
3020
3048
|
// Safety net: totalHeight no longer shrinks during drag (getTotalHeight
|
|
3021
3049
|
// ignores exclusion), but this logic remains as harmless protection
|
|
@@ -3096,6 +3124,50 @@ class VirtualScrollContainerComponent {
|
|
|
3096
3124
|
this.#observeRenderedItems();
|
|
3097
3125
|
}
|
|
3098
3126
|
}
|
|
3127
|
+
/**
|
|
3128
|
+
* Scroll so the placeholder is fully visible during a keyboard drag into this list.
|
|
3129
|
+
* Reads only signals and strategy offsets, so it works before the placeholder renders.
|
|
3130
|
+
*/
|
|
3131
|
+
#revealPlaceholder() {
|
|
3132
|
+
// Only apply when this droppable is active during keyboard drag
|
|
3133
|
+
if (!this.#dragState.isKeyboardDrag())
|
|
3134
|
+
return;
|
|
3135
|
+
const activeDroppable = this.#dragState.activeDroppableId();
|
|
3136
|
+
if (activeDroppable !== this.droppableId())
|
|
3137
|
+
return;
|
|
3138
|
+
// The placeholder renders before the item at placeholderIndex. Below the source in the
|
|
3139
|
+
// same list that is keyboardTargetIndex + 1: the dragged item's slot is excluded.
|
|
3140
|
+
if (this.#dragState.keyboardTargetIndex() === null)
|
|
3141
|
+
return;
|
|
3142
|
+
const placeholderIndex = this.placeholderIndex();
|
|
3143
|
+
if (placeholderIndex < 0)
|
|
3144
|
+
return;
|
|
3145
|
+
const strategy = this.#strategy();
|
|
3146
|
+
strategy.version();
|
|
3147
|
+
const height = this.effectiveHeight();
|
|
3148
|
+
if (height <= 0)
|
|
3149
|
+
return;
|
|
3150
|
+
const element = this.#elementRef.nativeElement;
|
|
3151
|
+
const currentScrollTop = element.scrollTop;
|
|
3152
|
+
// Calculate placeholder position using strategy
|
|
3153
|
+
const targetTop = strategy.getOffsetForIndex(placeholderIndex);
|
|
3154
|
+
const targetBottom = targetTop + this.placeholderHeight();
|
|
3155
|
+
// Calculate visible range
|
|
3156
|
+
const viewportTop = currentScrollTop;
|
|
3157
|
+
const viewportBottom = currentScrollTop + height;
|
|
3158
|
+
// Check if target is fully visible
|
|
3159
|
+
if (targetTop < viewportTop) {
|
|
3160
|
+
// Target is above viewport - scroll up
|
|
3161
|
+
element.scrollTop = targetTop;
|
|
3162
|
+
this.#scrollTop.set(targetTop);
|
|
3163
|
+
}
|
|
3164
|
+
else if (targetBottom > viewportBottom) {
|
|
3165
|
+
// Target is below viewport - scroll down
|
|
3166
|
+
const newScrollTop = targetBottom - height;
|
|
3167
|
+
element.scrollTop = newScrollTop;
|
|
3168
|
+
this.#scrollTop.set(newScrollTop);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3099
3171
|
ngOnDestroy() {
|
|
3100
3172
|
this.#scrollCleanup?.();
|
|
3101
3173
|
this.#resizeCleanup?.();
|
|
@@ -3294,6 +3366,124 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
3294
3366
|
`, styles: [":host{display:block;overflow:auto;position:relative;overflow-anchor:none}.vdnd-virtual-scroll-spacer{position:absolute;top:0;left:0;width:1px;visibility:hidden;pointer-events:none}.vdnd-virtual-scroll-content-wrapper{position:absolute;top:0;left:0;right:0;will-change:transform}\n"] }]
|
|
3295
3367
|
}], ctorParameters: () => [], propDecorators: { itemTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemTemplate", required: true }] }], scrollContainerId: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollContainerId", required: false }] }], autoScrollEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScrollEnabled", required: false }] }], autoScrollConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScrollConfig", required: false }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: true }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: true }] }], dynamicItemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "dynamicItemHeight", required: false }] }], containerHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "containerHeight", required: false }] }], overscan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscan", required: false }] }], stickyItemIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyItemIds", required: false }] }], itemIdFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemIdFn", required: true }] }], trackByFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackByFn", required: false }] }], droppableId: [{ type: i0.Input, args: [{ isSignal: true, alias: "droppableId", required: false }] }], autoStickyDraggedItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoStickyDraggedItem", required: false }] }] } });
|
|
3296
3368
|
|
|
3369
|
+
/**
|
|
3370
|
+
* Plays the drop ("settle") animation: the drag preview glides from where it was released
|
|
3371
|
+
* into the dropped item's final position, while that item stays invisible underneath.
|
|
3372
|
+
*
|
|
3373
|
+
* Purely visual. The drag state and the drop events are already final when it starts, so it
|
|
3374
|
+
* never delays or changes a drop. Without a target (the item is not rendered, or scrolled out
|
|
3375
|
+
* of view) the ghost fades out where it was released.
|
|
3376
|
+
* @internal
|
|
3377
|
+
*/
|
|
3378
|
+
class DropAnimator {
|
|
3379
|
+
#config;
|
|
3380
|
+
#ghostAnimation = null;
|
|
3381
|
+
#hideAnimation = null;
|
|
3382
|
+
constructor(config) {
|
|
3383
|
+
this.#config = config;
|
|
3384
|
+
}
|
|
3385
|
+
/** Whether a drop animation would play right now (duration, WAAPI, reduced motion). */
|
|
3386
|
+
isEnabled() {
|
|
3387
|
+
if (this.#duration() <= 0)
|
|
3388
|
+
return false;
|
|
3389
|
+
if (typeof Element === 'undefined' || typeof Element.prototype.animate !== 'function') {
|
|
3390
|
+
return false;
|
|
3391
|
+
}
|
|
3392
|
+
if (typeof matchMedia !== 'function')
|
|
3393
|
+
return true;
|
|
3394
|
+
return !matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
3395
|
+
}
|
|
3396
|
+
/**
|
|
3397
|
+
* Animate `ghost` onto `target` (or fade it out without one), then call `onDone`.
|
|
3398
|
+
* `onDone` is not called when the animation is cancelled.
|
|
3399
|
+
*/
|
|
3400
|
+
play(ghost, target, onDone) {
|
|
3401
|
+
this.cancel();
|
|
3402
|
+
const timing = {
|
|
3403
|
+
duration: this.#duration(),
|
|
3404
|
+
easing: this.#config.dropEasing ?? DEFAULT_DROP_EASING,
|
|
3405
|
+
fill: 'forwards',
|
|
3406
|
+
};
|
|
3407
|
+
let ghostAnimation;
|
|
3408
|
+
if (target) {
|
|
3409
|
+
const from = ghost.getBoundingClientRect();
|
|
3410
|
+
const dx = target.rect.left - from.left;
|
|
3411
|
+
const dy = target.rect.top - from.top;
|
|
3412
|
+
// Travel on top of the preview's own position transform (width/height must not be
|
|
3413
|
+
// composited, so the base transform is repeated instead of using composite: 'add').
|
|
3414
|
+
const base = ghost.style.transform;
|
|
3415
|
+
ghostAnimation = ghost.animate([
|
|
3416
|
+
{
|
|
3417
|
+
transform: `${base} translate(0px, 0px)`,
|
|
3418
|
+
width: `${from.width}px`,
|
|
3419
|
+
height: `${from.height}px`,
|
|
3420
|
+
},
|
|
3421
|
+
{
|
|
3422
|
+
transform: `${base} translate(${dx}px, ${dy}px)`,
|
|
3423
|
+
width: `${target.rect.width}px`,
|
|
3424
|
+
height: `${target.rect.height}px`,
|
|
3425
|
+
},
|
|
3426
|
+
], timing);
|
|
3427
|
+
// Keep the real item invisible until the ghost lands on it, so it is never seen twice.
|
|
3428
|
+
this.#hideAnimation = target.element.animate([{ opacity: 0 }, { opacity: 0 }], {
|
|
3429
|
+
duration: timing.duration,
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
else {
|
|
3433
|
+
ghostAnimation = ghost.animate([{ opacity: 1 }, { opacity: 0 }], timing);
|
|
3434
|
+
}
|
|
3435
|
+
this.#ghostAnimation = ghostAnimation;
|
|
3436
|
+
ghostAnimation.onfinish = () => {
|
|
3437
|
+
if (this.#ghostAnimation !== ghostAnimation)
|
|
3438
|
+
return;
|
|
3439
|
+
this.cancel();
|
|
3440
|
+
onDone();
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
/** Stop the running animation immediately and reveal the real item. */
|
|
3444
|
+
cancel() {
|
|
3445
|
+
const ghostAnimation = this.#ghostAnimation;
|
|
3446
|
+
const hideAnimation = this.#hideAnimation;
|
|
3447
|
+
this.#ghostAnimation = null;
|
|
3448
|
+
this.#hideAnimation = null;
|
|
3449
|
+
ghostAnimation?.cancel();
|
|
3450
|
+
hideAnimation?.cancel();
|
|
3451
|
+
}
|
|
3452
|
+
#duration() {
|
|
3453
|
+
return this.#config.dropDuration ?? DEFAULT_DROP_DURATION;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Find the rendered element of a dropped item: first in the droppable it was dropped on, then
|
|
3458
|
+
* in the one it came from (a drop the consumer rejected or has not committed yet leaves it
|
|
3459
|
+
* there). Returns null when it is not rendered, or rendered but scrolled out of its list's view.
|
|
3460
|
+
*/
|
|
3461
|
+
function findDropTarget(draggableId, droppableIds) {
|
|
3462
|
+
if (typeof document === 'undefined')
|
|
3463
|
+
return null;
|
|
3464
|
+
for (const droppableId of droppableIds) {
|
|
3465
|
+
if (!droppableId)
|
|
3466
|
+
continue;
|
|
3467
|
+
const droppable = queryByAttribute(document, 'data-droppable-id', droppableId);
|
|
3468
|
+
const element = droppable
|
|
3469
|
+
? queryByAttribute(droppable, 'data-draggable-id', draggableId)
|
|
3470
|
+
: null;
|
|
3471
|
+
if (!droppable || !element)
|
|
3472
|
+
continue;
|
|
3473
|
+
const rect = element.getBoundingClientRect();
|
|
3474
|
+
// Hidden, or virtualized/scrolled out of the list's visible box: nowhere to land.
|
|
3475
|
+
if (rect.width === 0 && rect.height === 0)
|
|
3476
|
+
return null;
|
|
3477
|
+
const bounds = droppable.getBoundingClientRect();
|
|
3478
|
+
const visible = rect.bottom > bounds.top &&
|
|
3479
|
+
rect.top < bounds.bottom &&
|
|
3480
|
+
rect.right > bounds.left &&
|
|
3481
|
+
rect.left < bounds.right;
|
|
3482
|
+
return visible ? { element, rect } : null;
|
|
3483
|
+
}
|
|
3484
|
+
return null;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3297
3487
|
/**
|
|
3298
3488
|
* Renders a preview of the dragged item that follows the cursor.
|
|
3299
3489
|
*
|
|
@@ -3301,6 +3491,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
3301
3491
|
* so it works correctly even inside ancestors with CSS `transform` (e.g. Ionic pages).
|
|
3302
3492
|
* It can be placed anywhere in the component tree.
|
|
3303
3493
|
*
|
|
3494
|
+
* When `VDND_ANIMATION_CONFIG` is provided, the preview stays up briefly after a drop or
|
|
3495
|
+
* cancel and glides into the item's final position.
|
|
3496
|
+
*
|
|
3304
3497
|
* @example
|
|
3305
3498
|
* ```html
|
|
3306
3499
|
* <vdnd-drag-preview>
|
|
@@ -3314,12 +3507,24 @@ class DragPreviewComponent {
|
|
|
3314
3507
|
dragState = inject(DragStateService);
|
|
3315
3508
|
#overlayContainer = inject(OverlayContainerService);
|
|
3316
3509
|
#elementRef = inject((ElementRef));
|
|
3510
|
+
#injector = inject(Injector);
|
|
3511
|
+
#dropAnimator = this.#createDropAnimator();
|
|
3317
3512
|
/** Optional custom template for the preview */
|
|
3318
3513
|
previewTemplate = input(...(ngDevMode ? [undefined, { debugName: "previewTemplate" }] : []));
|
|
3319
3514
|
/** Offset from cursor to preview (to avoid cursor being on top of preview) */
|
|
3320
3515
|
cursorOffset = input({ x: 8, y: 8 }, ...(ngDevMode ? [{ debugName: "cursorOffset" }] : []));
|
|
3321
3516
|
/** Reference to the clone container element (cannot use ES private with viewChild) */
|
|
3322
3517
|
cloneContainer = viewChild('cloneContainer', ...(ngDevMode ? [{ debugName: "cloneContainer" }] : []));
|
|
3518
|
+
/** The rendered preview box (cannot use ES private with viewChild) */
|
|
3519
|
+
previewElement = viewChild('preview', ...(ngDevMode ? [{ debugName: "previewElement" }] : []));
|
|
3520
|
+
/** Set while the preview animates into the drop position after the drag ended */
|
|
3521
|
+
#settling = signal(null, ...(ngDevMode ? [{ debugName: "#settling" }] : []));
|
|
3522
|
+
/** Whether the drag was still active on the last state change (detects drag end) */
|
|
3523
|
+
#wasDragging = false;
|
|
3524
|
+
/** Whether the preview is playing the drop animation */
|
|
3525
|
+
isSettling = computed(() => this.#settling() !== null, ...(ngDevMode ? [{ debugName: "isSettling" }] : []));
|
|
3526
|
+
/** The item shown: the live dragged item, or the dropped one while it settles */
|
|
3527
|
+
displayedItem = computed(() => this.dragState.draggedItem() ?? this.#settling()?.item ?? null, ...(ngDevMode ? [{ debugName: "displayedItem" }] : []));
|
|
3323
3528
|
/** Whether this preview's template registration is currently counted (kept balanced). */
|
|
3324
3529
|
#templateRegistered = false;
|
|
3325
3530
|
/** The cloned element from drag state (used when no custom template is provided) */
|
|
@@ -3327,7 +3532,7 @@ class DragPreviewComponent {
|
|
|
3327
3532
|
if (this.previewTemplate()) {
|
|
3328
3533
|
return null; // Custom template takes precedence
|
|
3329
3534
|
}
|
|
3330
|
-
return this.
|
|
3535
|
+
return this.displayedItem()?.clonedElement ?? null;
|
|
3331
3536
|
}, ...(ngDevMode ? [{ debugName: "clonedElement" }] : []));
|
|
3332
3537
|
constructor() {
|
|
3333
3538
|
// Teleport host element into the body-level overlay container after first render.
|
|
@@ -3365,8 +3570,24 @@ class DragPreviewComponent {
|
|
|
3365
3570
|
container.appendChild(clone);
|
|
3366
3571
|
}
|
|
3367
3572
|
});
|
|
3573
|
+
// Drop animation: when a drag ends, keep the preview up and glide it into place.
|
|
3574
|
+
// A new drag cuts any settle short.
|
|
3575
|
+
effect(() => {
|
|
3576
|
+
const isDragging = this.dragState.isDragging();
|
|
3577
|
+
untracked(() => {
|
|
3578
|
+
const wasDragging = this.#wasDragging;
|
|
3579
|
+
this.#wasDragging = isDragging;
|
|
3580
|
+
if (isDragging) {
|
|
3581
|
+
this.#stopSettling();
|
|
3582
|
+
}
|
|
3583
|
+
else if (wasDragging) {
|
|
3584
|
+
this.#startSettling();
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3587
|
+
});
|
|
3368
3588
|
}
|
|
3369
3589
|
ngOnDestroy() {
|
|
3590
|
+
this.#dropAnimator?.cancel();
|
|
3370
3591
|
if (this.#templateRegistered) {
|
|
3371
3592
|
this.#overlayContainer.setTemplatePreviewActive(false);
|
|
3372
3593
|
this.#templateRegistered = false;
|
|
@@ -3375,20 +3596,30 @@ class DragPreviewComponent {
|
|
|
3375
3596
|
}
|
|
3376
3597
|
/** Whether the preview is visible */
|
|
3377
3598
|
isVisible = computed(() => {
|
|
3378
|
-
return this.dragState.isDragging() && this.dragState.cursorPosition() !== null;
|
|
3599
|
+
return ((this.dragState.isDragging() && this.dragState.cursorPosition() !== null) || this.isSettling());
|
|
3379
3600
|
}, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
|
|
3380
3601
|
/** Position of the preview */
|
|
3381
3602
|
position = computed(() => {
|
|
3382
|
-
const
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3603
|
+
const settling = this.#settling();
|
|
3604
|
+
if (settling && !this.dragState.isDragging()) {
|
|
3605
|
+
return settling.position;
|
|
3606
|
+
}
|
|
3607
|
+
return this.#positionFor({
|
|
3608
|
+
cursorPosition: this.dragState.cursorPosition(),
|
|
3609
|
+
grabOffset: this.dragState.grabOffset(),
|
|
3610
|
+
initialPosition: this.dragState.initialPosition(),
|
|
3611
|
+
lockAxis: this.dragState.lockAxis(),
|
|
3612
|
+
});
|
|
3613
|
+
}, ...(ngDevMode ? [{ debugName: "position" }] : []));
|
|
3614
|
+
#positionFor(state) {
|
|
3615
|
+
const cursor = state.cursorPosition;
|
|
3616
|
+
const initialPosition = state.initialPosition;
|
|
3617
|
+
const lockAxis = state.lockAxis;
|
|
3387
3618
|
if (!cursor) {
|
|
3388
3619
|
return { x: 0, y: 0 };
|
|
3389
3620
|
}
|
|
3390
3621
|
// Use grab offset if available (preserves grab position), otherwise fall back to cursorOffset input
|
|
3391
|
-
const offset = grabOffset ??
|
|
3622
|
+
const offset = state.grabOffset ?? this.cursorOffset();
|
|
3392
3623
|
let x = cursor.x - offset.x;
|
|
3393
3624
|
let y = cursor.y - offset.y;
|
|
3394
3625
|
// Apply axis locking if configured
|
|
@@ -3403,7 +3634,7 @@ class DragPreviewComponent {
|
|
|
3403
3634
|
}
|
|
3404
3635
|
}
|
|
3405
3636
|
return { x, y };
|
|
3406
|
-
}
|
|
3637
|
+
}
|
|
3407
3638
|
/** Transform-based positioning for better performance (avoid layout from left/top). */
|
|
3408
3639
|
transform = computed(() => {
|
|
3409
3640
|
const { x, y } = this.position();
|
|
@@ -3411,7 +3642,7 @@ class DragPreviewComponent {
|
|
|
3411
3642
|
}, ...(ngDevMode ? [{ debugName: "transform" }] : []));
|
|
3412
3643
|
/** Dimensions of the preview */
|
|
3413
3644
|
dimensions = computed(() => {
|
|
3414
|
-
const item = this.
|
|
3645
|
+
const item = this.displayedItem();
|
|
3415
3646
|
if (!item) {
|
|
3416
3647
|
return { width: 100, height: 50 };
|
|
3417
3648
|
}
|
|
@@ -3422,19 +3653,65 @@ class DragPreviewComponent {
|
|
|
3422
3653
|
}, ...(ngDevMode ? [{ debugName: "dimensions" }] : []));
|
|
3423
3654
|
/** Template context */
|
|
3424
3655
|
templateContext = computed(() => {
|
|
3425
|
-
const item = this.
|
|
3656
|
+
const item = this.displayedItem();
|
|
3426
3657
|
return {
|
|
3427
3658
|
$implicit: (item?.data ?? null),
|
|
3428
3659
|
draggableId: item?.draggableId ?? '',
|
|
3429
3660
|
droppableId: item?.droppableId ?? '',
|
|
3430
3661
|
};
|
|
3431
3662
|
}, ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
|
|
3663
|
+
#createDropAnimator() {
|
|
3664
|
+
const config = inject(VDND_ANIMATION_CONFIG, { optional: true });
|
|
3665
|
+
return config ? new DropAnimator(config) : null;
|
|
3666
|
+
}
|
|
3667
|
+
/**
|
|
3668
|
+
* Keep the ended drag's preview on screen, then (once the consumer's drop handler has
|
|
3669
|
+
* re-rendered the lists) animate it onto the item's final position.
|
|
3670
|
+
*/
|
|
3671
|
+
#startSettling() {
|
|
3672
|
+
const ended = this.dragState.endedDragState();
|
|
3673
|
+
if (!this.#dropAnimator?.isEnabled() || !ended?.draggedItem || !ended.cursorPosition) {
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
const settling = {
|
|
3677
|
+
item: ended.draggedItem,
|
|
3678
|
+
position: this.#positionFor(ended),
|
|
3679
|
+
};
|
|
3680
|
+
this.#settling.set(settling);
|
|
3681
|
+
// A normal drop lands in the target list; a cancel, a rejected drop or one the consumer
|
|
3682
|
+
// has not committed yet leaves the item in its source list.
|
|
3683
|
+
const droppableIds = this.dragState.wasCancelled()
|
|
3684
|
+
? [ended.sourceDroppableId]
|
|
3685
|
+
: [ended.activeDroppableId, ended.sourceDroppableId];
|
|
3686
|
+
afterNextRender(() => {
|
|
3687
|
+
if (this.#settling() !== settling) {
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
const ghost = this.previewElement()?.nativeElement;
|
|
3691
|
+
if (!ghost || !this.#dropAnimator) {
|
|
3692
|
+
this.#settling.set(null);
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
const target = findDropTarget(settling.item.draggableId, droppableIds);
|
|
3696
|
+
this.#dropAnimator.play(ghost, target, () => {
|
|
3697
|
+
if (this.#settling() === settling) {
|
|
3698
|
+
this.#settling.set(null);
|
|
3699
|
+
}
|
|
3700
|
+
});
|
|
3701
|
+
}, { injector: this.#injector });
|
|
3702
|
+
}
|
|
3703
|
+
#stopSettling() {
|
|
3704
|
+
this.#dropAnimator?.cancel();
|
|
3705
|
+
this.#settling.set(null);
|
|
3706
|
+
}
|
|
3432
3707
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DragPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3433
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: DragPreviewComponent, isStandalone: true, selector: "vdnd-drag-preview", inputs: { previewTemplate: { classPropertyName: "previewTemplate", publicName: "previewTemplate", isSignal: true, isRequired: false, transformFunction: null }, cursorOffset: { classPropertyName: "cursorOffset", publicName: "cursorOffset", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "cloneContainer", first: true, predicate: ["cloneContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
3708
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: DragPreviewComponent, isStandalone: true, selector: "vdnd-drag-preview", inputs: { previewTemplate: { classPropertyName: "previewTemplate", publicName: "previewTemplate", isSignal: true, isRequired: false, transformFunction: null }, cursorOffset: { classPropertyName: "cursorOffset", publicName: "cursorOffset", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "cloneContainer", first: true, predicate: ["cloneContainer"], descendants: true, isSignal: true }, { propertyName: "previewElement", first: true, predicate: ["preview"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
3434
3709
|
@if (isVisible()) {
|
|
3435
3710
|
<div
|
|
3711
|
+
#preview
|
|
3436
3712
|
class="vdnd-drag-preview"
|
|
3437
|
-
|
|
3713
|
+
[class.vdnd-drag-preview-dropping]="isSettling()"
|
|
3714
|
+
[attr.data-testid]="isSettling() ? 'vdnd-drag-preview-dropping' : 'vdnd-drag-preview'"
|
|
3438
3715
|
[style.transform]="transform()"
|
|
3439
3716
|
[style.width.px]="dimensions().width"
|
|
3440
3717
|
[style.height.px]="dimensions().height"
|
|
@@ -3446,7 +3723,7 @@ class DragPreviewComponent {
|
|
|
3446
3723
|
<div class="vdnd-drag-preview-clone" #cloneContainer></div>
|
|
3447
3724
|
} @else {
|
|
3448
3725
|
<div class="vdnd-drag-preview-default">
|
|
3449
|
-
{{
|
|
3726
|
+
{{ displayedItem()?.draggableId }}
|
|
3450
3727
|
</div>
|
|
3451
3728
|
}
|
|
3452
3729
|
</div>
|
|
@@ -3458,8 +3735,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
3458
3735
|
args: [{ selector: 'vdnd-drag-preview', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], template: `
|
|
3459
3736
|
@if (isVisible()) {
|
|
3460
3737
|
<div
|
|
3738
|
+
#preview
|
|
3461
3739
|
class="vdnd-drag-preview"
|
|
3462
|
-
|
|
3740
|
+
[class.vdnd-drag-preview-dropping]="isSettling()"
|
|
3741
|
+
[attr.data-testid]="isSettling() ? 'vdnd-drag-preview-dropping' : 'vdnd-drag-preview'"
|
|
3463
3742
|
[style.transform]="transform()"
|
|
3464
3743
|
[style.width.px]="dimensions().width"
|
|
3465
3744
|
[style.height.px]="dimensions().height"
|
|
@@ -3471,13 +3750,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
3471
3750
|
<div class="vdnd-drag-preview-clone" #cloneContainer></div>
|
|
3472
3751
|
} @else {
|
|
3473
3752
|
<div class="vdnd-drag-preview-default">
|
|
3474
|
-
{{
|
|
3753
|
+
{{ displayedItem()?.draggableId }}
|
|
3475
3754
|
</div>
|
|
3476
3755
|
}
|
|
3477
3756
|
</div>
|
|
3478
3757
|
}
|
|
3479
3758
|
`, styles: [".vdnd-drag-preview{box-sizing:border-box;position:fixed;left:0;top:0;will-change:transform;pointer-events:none;z-index:1000}.vdnd-drag-preview-clone{width:100%;height:100%;overflow:hidden}\n"] }]
|
|
3480
|
-
}], ctorParameters: () => [], propDecorators: { previewTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewTemplate", required: false }] }], cursorOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "cursorOffset", required: false }] }], cloneContainer: [{ type: i0.ViewChild, args: ['cloneContainer', { isSignal: true }] }] } });
|
|
3759
|
+
}], ctorParameters: () => [], propDecorators: { previewTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewTemplate", required: false }] }], cursorOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "cursorOffset", required: false }] }], cloneContainer: [{ type: i0.ViewChild, args: ['cloneContainer', { isSignal: true }] }], previewElement: [{ type: i0.ViewChild, args: ['preview', { isSignal: true }] }] } });
|
|
3481
3760
|
|
|
3482
3761
|
/**
|
|
3483
3762
|
* A placeholder component that indicates where a dropped item will be inserted.
|
|
@@ -4754,9 +5033,13 @@ class KeyboardDragHandler {
|
|
|
4754
5033
|
sourceIndex,
|
|
4755
5034
|
destinationIndex,
|
|
4756
5035
|
});
|
|
5036
|
+
// The item stays in its source list when there is no valid target
|
|
5037
|
+
const fallbackDroppableId = hasValidTarget
|
|
5038
|
+
? activeDroppableId
|
|
5039
|
+
: this.#deps.dragState.sourceDroppableId();
|
|
4757
5040
|
this.#deps.keyboardDrag.completeKeyboardDrag();
|
|
4758
5041
|
// Restore focus to the moved element after state updates
|
|
4759
|
-
this.#restoreFocus(ctx.draggableId);
|
|
5042
|
+
this.#restoreFocus(ctx.draggableId, fallbackDroppableId);
|
|
4760
5043
|
}
|
|
4761
5044
|
/**
|
|
4762
5045
|
* Cancel a keyboard drag operation.
|
|
@@ -4764,6 +5047,8 @@ class KeyboardDragHandler {
|
|
|
4764
5047
|
cancel() {
|
|
4765
5048
|
const ctx = this.#deps.getContext();
|
|
4766
5049
|
const sourceIndex = this.#deps.dragState.sourceIndex() ?? 0;
|
|
5050
|
+
// A cancelled item goes back to its source list
|
|
5051
|
+
const sourceDroppableId = this.#deps.dragState.sourceDroppableId();
|
|
4767
5052
|
// Remove document listener
|
|
4768
5053
|
this.#cleanupDocumentListener();
|
|
4769
5054
|
// Clear droppable metadata cache from this drag session
|
|
@@ -4779,7 +5064,7 @@ class KeyboardDragHandler {
|
|
|
4779
5064
|
});
|
|
4780
5065
|
this.#deps.keyboardDrag.cancelKeyboardDrag();
|
|
4781
5066
|
// Restore focus to the original element after state updates
|
|
4782
|
-
this.#restoreFocus(ctx.draggableId);
|
|
5067
|
+
this.#restoreFocus(ctx.draggableId, sourceDroppableId);
|
|
4783
5068
|
}
|
|
4784
5069
|
/**
|
|
4785
5070
|
* Teardown — remove any active document listener.
|
|
@@ -4794,20 +5079,21 @@ class KeyboardDragHandler {
|
|
|
4794
5079
|
*
|
|
4795
5080
|
* Uses EnvironmentInjector to ensure callback runs even if the directive
|
|
4796
5081
|
* is destroyed during cross-list moves.
|
|
5082
|
+
*
|
|
5083
|
+
* `fallbackDroppableId` is the list whose first draggable gets focus when the element is not
|
|
5084
|
+
* rendered (for example scrolled out of a virtual list). Callers read it before ending the
|
|
5085
|
+
* drag, which clears the drag state.
|
|
4797
5086
|
*/
|
|
4798
|
-
#restoreFocus(draggableId) {
|
|
4799
|
-
// Capture the destination droppable BEFORE scheduling afterNextRender
|
|
4800
|
-
// (the directive may be destroyed during cross-list moves)
|
|
4801
|
-
const destinationDroppableId = this.#deps.dragState.activeDroppableId();
|
|
5087
|
+
#restoreFocus(draggableId, fallbackDroppableId) {
|
|
4802
5088
|
afterNextRender(() => {
|
|
4803
5089
|
const element = queryByAttribute(document, 'data-draggable-id', draggableId);
|
|
4804
5090
|
if (element) {
|
|
4805
5091
|
element.focus();
|
|
4806
5092
|
}
|
|
4807
|
-
else if (
|
|
4808
|
-
// Fallback: focus the first draggable in the
|
|
4809
|
-
const
|
|
4810
|
-
const firstDraggable =
|
|
5093
|
+
else if (fallbackDroppableId) {
|
|
5094
|
+
// Fallback: focus the first draggable in the list the item ended up in
|
|
5095
|
+
const container = queryByAttribute(document, 'data-droppable-id', fallbackDroppableId);
|
|
5096
|
+
const firstDraggable = container?.querySelector('[data-draggable-id]');
|
|
4811
5097
|
firstDraggable?.focus();
|
|
4812
5098
|
}
|
|
4813
5099
|
}, { injector: this.#deps.envInjector });
|
|
@@ -4866,7 +5152,7 @@ var _a;
|
|
|
4866
5152
|
* - Pointer down / move / up lifecycle
|
|
4867
5153
|
* - Drag threshold detection
|
|
4868
5154
|
* - Drag delay timer
|
|
4869
|
-
* -
|
|
5155
|
+
* - Forwarding moves to the directive (DragSchedulerService coalesces them per frame)
|
|
4870
5156
|
* - Document-level listener management
|
|
4871
5157
|
* - Escape key cancellation during pointer drag
|
|
4872
5158
|
*/
|