ngx-virtual-dnd 3.2.0-alpha.1 → 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.
|
@@ -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
|
|
@@ -2488,7 +2494,7 @@ const MIN_SHIFT_PX = 0.5;
|
|
|
2488
2494
|
* FLIP animator for items displaced by the drag placeholder.
|
|
2489
2495
|
*
|
|
2490
2496
|
* Call `beforeUpdate()` before the DOM reflects a new drag/placeholder state. When the
|
|
2491
|
-
* 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
|
|
2492
2498
|
* the render measures again and plays a compositor-only `transform` animation from the
|
|
2493
2499
|
* old position to the new one.
|
|
2494
2500
|
*
|
|
@@ -2525,11 +2531,19 @@ class ShiftAnimator {
|
|
|
2525
2531
|
this.#wasDragging = isDragging;
|
|
2526
2532
|
this.#lastPlaceholderIndex = placeholderIndex;
|
|
2527
2533
|
if (!isDragging) {
|
|
2528
|
-
// Drop/cancel:
|
|
2529
|
-
|
|
2530
|
-
|
|
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()) {
|
|
2531
2542
|
this.cancelAll();
|
|
2543
|
+
return;
|
|
2532
2544
|
}
|
|
2545
|
+
this.#snapshot = this.#measure();
|
|
2546
|
+
afterNextRender(() => this.#play(), { injector: this.#injector });
|
|
2533
2547
|
return;
|
|
2534
2548
|
}
|
|
2535
2549
|
// Drag start renders the placeholder in the hidden item's slot — nothing moves.
|
|
@@ -2599,7 +2613,7 @@ class ShiftAnimator {
|
|
|
2599
2613
|
#play() {
|
|
2600
2614
|
const first = this.#snapshot;
|
|
2601
2615
|
this.#snapshot = null;
|
|
2602
|
-
if (!first
|
|
2616
|
+
if (!first)
|
|
2603
2617
|
return;
|
|
2604
2618
|
const last = this.#measure();
|
|
2605
2619
|
const stale = [];
|
|
@@ -3352,6 +3366,124 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
3352
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"] }]
|
|
3353
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 }] }] } });
|
|
3354
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
|
+
|
|
3355
3487
|
/**
|
|
3356
3488
|
* Renders a preview of the dragged item that follows the cursor.
|
|
3357
3489
|
*
|
|
@@ -3359,6 +3491,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
3359
3491
|
* so it works correctly even inside ancestors with CSS `transform` (e.g. Ionic pages).
|
|
3360
3492
|
* It can be placed anywhere in the component tree.
|
|
3361
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
|
+
*
|
|
3362
3497
|
* @example
|
|
3363
3498
|
* ```html
|
|
3364
3499
|
* <vdnd-drag-preview>
|
|
@@ -3372,12 +3507,24 @@ class DragPreviewComponent {
|
|
|
3372
3507
|
dragState = inject(DragStateService);
|
|
3373
3508
|
#overlayContainer = inject(OverlayContainerService);
|
|
3374
3509
|
#elementRef = inject((ElementRef));
|
|
3510
|
+
#injector = inject(Injector);
|
|
3511
|
+
#dropAnimator = this.#createDropAnimator();
|
|
3375
3512
|
/** Optional custom template for the preview */
|
|
3376
3513
|
previewTemplate = input(...(ngDevMode ? [undefined, { debugName: "previewTemplate" }] : []));
|
|
3377
3514
|
/** Offset from cursor to preview (to avoid cursor being on top of preview) */
|
|
3378
3515
|
cursorOffset = input({ x: 8, y: 8 }, ...(ngDevMode ? [{ debugName: "cursorOffset" }] : []));
|
|
3379
3516
|
/** Reference to the clone container element (cannot use ES private with viewChild) */
|
|
3380
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" }] : []));
|
|
3381
3528
|
/** Whether this preview's template registration is currently counted (kept balanced). */
|
|
3382
3529
|
#templateRegistered = false;
|
|
3383
3530
|
/** The cloned element from drag state (used when no custom template is provided) */
|
|
@@ -3385,7 +3532,7 @@ class DragPreviewComponent {
|
|
|
3385
3532
|
if (this.previewTemplate()) {
|
|
3386
3533
|
return null; // Custom template takes precedence
|
|
3387
3534
|
}
|
|
3388
|
-
return this.
|
|
3535
|
+
return this.displayedItem()?.clonedElement ?? null;
|
|
3389
3536
|
}, ...(ngDevMode ? [{ debugName: "clonedElement" }] : []));
|
|
3390
3537
|
constructor() {
|
|
3391
3538
|
// Teleport host element into the body-level overlay container after first render.
|
|
@@ -3423,8 +3570,24 @@ class DragPreviewComponent {
|
|
|
3423
3570
|
container.appendChild(clone);
|
|
3424
3571
|
}
|
|
3425
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
|
+
});
|
|
3426
3588
|
}
|
|
3427
3589
|
ngOnDestroy() {
|
|
3590
|
+
this.#dropAnimator?.cancel();
|
|
3428
3591
|
if (this.#templateRegistered) {
|
|
3429
3592
|
this.#overlayContainer.setTemplatePreviewActive(false);
|
|
3430
3593
|
this.#templateRegistered = false;
|
|
@@ -3433,20 +3596,30 @@ class DragPreviewComponent {
|
|
|
3433
3596
|
}
|
|
3434
3597
|
/** Whether the preview is visible */
|
|
3435
3598
|
isVisible = computed(() => {
|
|
3436
|
-
return this.dragState.isDragging() && this.dragState.cursorPosition() !== null;
|
|
3599
|
+
return ((this.dragState.isDragging() && this.dragState.cursorPosition() !== null) || this.isSettling());
|
|
3437
3600
|
}, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
|
|
3438
3601
|
/** Position of the preview */
|
|
3439
3602
|
position = computed(() => {
|
|
3440
|
-
const
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
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;
|
|
3445
3618
|
if (!cursor) {
|
|
3446
3619
|
return { x: 0, y: 0 };
|
|
3447
3620
|
}
|
|
3448
3621
|
// Use grab offset if available (preserves grab position), otherwise fall back to cursorOffset input
|
|
3449
|
-
const offset = grabOffset ??
|
|
3622
|
+
const offset = state.grabOffset ?? this.cursorOffset();
|
|
3450
3623
|
let x = cursor.x - offset.x;
|
|
3451
3624
|
let y = cursor.y - offset.y;
|
|
3452
3625
|
// Apply axis locking if configured
|
|
@@ -3461,7 +3634,7 @@ class DragPreviewComponent {
|
|
|
3461
3634
|
}
|
|
3462
3635
|
}
|
|
3463
3636
|
return { x, y };
|
|
3464
|
-
}
|
|
3637
|
+
}
|
|
3465
3638
|
/** Transform-based positioning for better performance (avoid layout from left/top). */
|
|
3466
3639
|
transform = computed(() => {
|
|
3467
3640
|
const { x, y } = this.position();
|
|
@@ -3469,7 +3642,7 @@ class DragPreviewComponent {
|
|
|
3469
3642
|
}, ...(ngDevMode ? [{ debugName: "transform" }] : []));
|
|
3470
3643
|
/** Dimensions of the preview */
|
|
3471
3644
|
dimensions = computed(() => {
|
|
3472
|
-
const item = this.
|
|
3645
|
+
const item = this.displayedItem();
|
|
3473
3646
|
if (!item) {
|
|
3474
3647
|
return { width: 100, height: 50 };
|
|
3475
3648
|
}
|
|
@@ -3480,19 +3653,65 @@ class DragPreviewComponent {
|
|
|
3480
3653
|
}, ...(ngDevMode ? [{ debugName: "dimensions" }] : []));
|
|
3481
3654
|
/** Template context */
|
|
3482
3655
|
templateContext = computed(() => {
|
|
3483
|
-
const item = this.
|
|
3656
|
+
const item = this.displayedItem();
|
|
3484
3657
|
return {
|
|
3485
3658
|
$implicit: (item?.data ?? null),
|
|
3486
3659
|
draggableId: item?.draggableId ?? '',
|
|
3487
3660
|
droppableId: item?.droppableId ?? '',
|
|
3488
3661
|
};
|
|
3489
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
|
+
}
|
|
3490
3707
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DragPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3491
|
-
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: `
|
|
3492
3709
|
@if (isVisible()) {
|
|
3493
3710
|
<div
|
|
3711
|
+
#preview
|
|
3494
3712
|
class="vdnd-drag-preview"
|
|
3495
|
-
|
|
3713
|
+
[class.vdnd-drag-preview-dropping]="isSettling()"
|
|
3714
|
+
[attr.data-testid]="isSettling() ? 'vdnd-drag-preview-dropping' : 'vdnd-drag-preview'"
|
|
3496
3715
|
[style.transform]="transform()"
|
|
3497
3716
|
[style.width.px]="dimensions().width"
|
|
3498
3717
|
[style.height.px]="dimensions().height"
|
|
@@ -3504,7 +3723,7 @@ class DragPreviewComponent {
|
|
|
3504
3723
|
<div class="vdnd-drag-preview-clone" #cloneContainer></div>
|
|
3505
3724
|
} @else {
|
|
3506
3725
|
<div class="vdnd-drag-preview-default">
|
|
3507
|
-
{{
|
|
3726
|
+
{{ displayedItem()?.draggableId }}
|
|
3508
3727
|
</div>
|
|
3509
3728
|
}
|
|
3510
3729
|
</div>
|
|
@@ -3516,8 +3735,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
3516
3735
|
args: [{ selector: 'vdnd-drag-preview', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], template: `
|
|
3517
3736
|
@if (isVisible()) {
|
|
3518
3737
|
<div
|
|
3738
|
+
#preview
|
|
3519
3739
|
class="vdnd-drag-preview"
|
|
3520
|
-
|
|
3740
|
+
[class.vdnd-drag-preview-dropping]="isSettling()"
|
|
3741
|
+
[attr.data-testid]="isSettling() ? 'vdnd-drag-preview-dropping' : 'vdnd-drag-preview'"
|
|
3521
3742
|
[style.transform]="transform()"
|
|
3522
3743
|
[style.width.px]="dimensions().width"
|
|
3523
3744
|
[style.height.px]="dimensions().height"
|
|
@@ -3529,13 +3750,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
3529
3750
|
<div class="vdnd-drag-preview-clone" #cloneContainer></div>
|
|
3530
3751
|
} @else {
|
|
3531
3752
|
<div class="vdnd-drag-preview-default">
|
|
3532
|
-
{{
|
|
3753
|
+
{{ displayedItem()?.draggableId }}
|
|
3533
3754
|
</div>
|
|
3534
3755
|
}
|
|
3535
3756
|
</div>
|
|
3536
3757
|
}
|
|
3537
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"] }]
|
|
3538
|
-
}], 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 }] }] } });
|
|
3539
3760
|
|
|
3540
3761
|
/**
|
|
3541
3762
|
* A placeholder component that indicates where a dropped item will be inserted.
|
|
@@ -4931,7 +5152,7 @@ var _a;
|
|
|
4931
5152
|
* - Pointer down / move / up lifecycle
|
|
4932
5153
|
* - Drag threshold detection
|
|
4933
5154
|
* - Drag delay timer
|
|
4934
|
-
* -
|
|
5155
|
+
* - Forwarding moves to the directive (DragSchedulerService coalesces them per frame)
|
|
4935
5156
|
* - Document-level listener management
|
|
4936
5157
|
* - Escape key cancellation during pointer drag
|
|
4937
5158
|
*/
|