@@ -3239,7 +3239,6 @@ import {
32393239 DndObserverEventType ,
32403240 Draggable ,
32413241 Droppable ,
3242- getLocalOffset ,
32433242 KeyboardMotionSensor ,
32443243 PointerSensor ,
32453244} from ' dragdoll' ;
@@ -3252,12 +3251,77 @@ const defaultPredicate = createDefaultStartPredicate<PointerSensor | KeyboardMot
32523251
32533252// Keep track of the best match droppable.
32543253const bestMatchMap: Map <AnyDraggable , Droppable > = new Map ();
3254+ const originalElementMap: Map <string , HTMLElement > = new Map ();
3255+
3256+ const dragContainer = document .getElementById (' drag-container' ) as HTMLElement ;
3257+ const ANIMATION_EPSILON = 0.5 ;
32553258
32563259// Get elements.
32573260const scrollContainers = [... document .querySelectorAll (' .scroll-list' )] as HTMLElement [];
32583261const draggableElements = [... document .querySelectorAll (' .draggable' )] as HTMLElement [];
32593262const droppableElements = [... document .querySelectorAll (' .droppable' )] as HTMLElement [];
32603263
3264+ function createDragPreviewElement(element : HTMLElement , draggableId : string ): HTMLElement {
3265+ const rect = element .getBoundingClientRect ();
3266+ const clone = element .cloneNode (true ) as HTMLElement ;
3267+ clone .style .position = ' fixed' ;
3268+ clone .style .width = ` ${rect .width }px ` ;
3269+ clone .style .height = ` ${rect .height }px ` ;
3270+ clone .style .left = ` ${rect .left }px ` ;
3271+ clone .style .top = ` ${rect .top }px ` ;
3272+ clone .style .transform = ' ' ;
3273+ clone .classList .add (' drag-preview' , ' dragging' );
3274+ clone .setAttribute (' aria-hidden' , ' true' );
3275+ clone .setAttribute (' data-id' , draggableId );
3276+ clone .tabIndex = - 1 ;
3277+ dragContainer .appendChild (clone );
3278+ return clone ;
3279+ }
3280+
3281+ function cleanupDrag(
3282+ previewElement : HTMLElement | null ,
3283+ originalElement : HTMLElement | null ,
3284+ bestMatch : Droppable | null ,
3285+ draggable : AnyDraggable ,
3286+ ) {
3287+ previewElement ?.remove ();
3288+ originalElement ?.classList .remove (' dragging' , ' hidden' );
3289+ bestMatch ?.element ?.removeAttribute (' data-draggable-over' );
3290+ bestMatchMap .delete (draggable );
3291+ }
3292+
3293+ function findBestMatch(contacts : ReadonlySet <Droppable >, draggableId : string ): Droppable | null {
3294+ for (const droppable of contacts ) {
3295+ // Skip if the droppable contains a different draggable.
3296+ const containedDraggableId = droppable .element ?.getAttribute (' data-draggable-contained' ) || ' ' ;
3297+ if (containedDraggableId && containedDraggableId !== draggableId ) {
3298+ continue ;
3299+ }
3300+
3301+ // Skip if a different draggable is over the droppable.
3302+ const overDraggableId = droppable .element ?.getAttribute (' data-draggable-over' ) || ' ' ;
3303+ if (overDraggableId && overDraggableId !== draggableId ) {
3304+ continue ;
3305+ }
3306+
3307+ return droppable ;
3308+ }
3309+
3310+ return null ;
3311+ }
3312+
3313+ function getTargetPosition(container : HTMLElement ) {
3314+ const rect = container .getBoundingClientRect ();
3315+ const style = getComputedStyle (container );
3316+ const borderLeft = parseFloat (style .borderLeftWidth || ' 0' ) || 0 ;
3317+ const borderTop = parseFloat (style .borderTopWidth || ' 0' ) || 0 ;
3318+
3319+ return {
3320+ left: rect .left + borderLeft + 10 ,
3321+ top: rect .top + borderTop + 10 ,
3322+ };
3323+ }
3324+
32613325// Initialize DndObserver.
32623326const dndObserver = new DndObserver <AdvancedCollisionData >({
32633327 collisionDetector : (ctx ) => new AdvancedCollisionDetector (ctx ),
@@ -3273,28 +3337,23 @@ for (const droppableElement of droppableElements) {
32733337// Create draggables.
32743338const draggables: AnyDraggable [] = [];
32753339for (const draggableElement of draggableElements ) {
3340+ const draggableId = draggableElement .getAttribute (' data-id' ) || ' ' ;
3341+ if (draggableId === ' ' ) continue ;
3342+
3343+ originalElementMap .set (draggableId , draggableElement );
3344+
32763345 const draggable = new Draggable (
32773346 [new PointerSensor (draggableElement ), new KeyboardMotionSensor (draggableElement )],
32783347 {
3279- // Only move the draggable element.
3280- elements : () => [draggableElement ],
3281- // Use the drag container (has contain: layout).
3282- container: document .getElementById (' drag-container' ) as HTMLElement ,
3283- // Freeze the width and height of the dragged element since we are using
3284- // a custom container and the element has percentage based values for
3285- // some of its properties.
3348+ // Move a fixed-position clone, not the focused original element.
3349+ elements : () => [createDragPreviewElement (draggableElement , draggableId )],
32863350 frozenStyles : () => [' width' , ' height' ],
3287- // Allow the drag to start only if the element is not animating ; otherwise
3351+ // Allow the drag to start only if the element is not hidden ; otherwise
32883352 // defer to the default predicate (touch long-press, mouse threshold).
32893353 startPredicate : (data ) =>
3290- draggableElement .classList .contains (' animate' ) ? false : defaultPredicate (data ),
3291- // Toggle the dragging class on the draggable element when the drag starts
3292- // and ends.
3354+ draggableElement .classList .contains (' hidden' ) ? false : defaultPredicate (data ),
32933355 onStart : () => {
3294- draggableElement .classList .add (' dragging' );
3295- },
3296- onEnd : () => {
3297- draggableElement .classList .remove (' dragging' );
3356+ draggableElement .classList .add (' dragging' , ' hidden' );
32983357 },
32993358 },
33003359 ).use (
@@ -3317,33 +3376,16 @@ dndObserver.addDraggables(draggables);
33173376
33183377// On draggable collision with droppables.
33193378dndObserver .on (DndObserverEventType .Collide , ({ draggable , contacts }) => {
3320- // Get the draggable element.
3321- const draggableElement = draggable .drag ?.items [0 ].element as HTMLElement | null ;
3322- if (! draggableElement ) return ;
3379+ // Get the drag preview element.
3380+ const previewElement = draggable .drag ?.items [0 ].element as HTMLElement | null ;
3381+ if (! previewElement ) return ;
33233382
33243383 // Get the draggable id.
3325- const draggableId = draggableElement .getAttribute (' data-id' ) || ' ' ;
3384+ const draggableId = previewElement .getAttribute (' data-id' ) || ' ' ;
33263385 if (draggableId === ' ' ) return ;
33273386
33283387 // Get the next best match droppable.
3329- let nextBestMatch: Droppable | null = null ;
3330- for (const droppable of contacts ) {
3331- // Skip if the droppable contains a different draggable.
3332- const containedDraggableId = droppable .element ?.getAttribute (' data-draggable-contained' ) || ' ' ;
3333- if (containedDraggableId && containedDraggableId !== draggableId ) {
3334- continue ;
3335- }
3336-
3337- // Skip if a different draggable is over the droppable.
3338- const overDraggableId = droppable .element ?.getAttribute (' data-draggable-over' ) || ' ' ;
3339- if (overDraggableId && overDraggableId !== draggableId ) {
3340- continue ;
3341- }
3342-
3343- // We found the next best match.
3344- nextBestMatch = droppable ;
3345- break ;
3346- }
3388+ const nextBestMatch = findBestMatch (contacts , draggableId );
33473389
33483390 // Update the best match droppable if it's changed.
33493391 const bestMatch = bestMatchMap .get (draggable );
@@ -3356,71 +3398,63 @@ dndObserver.on(DndObserverEventType.Collide, ({ draggable, contacts }) => {
33563398
33573399// On drag end.
33583400dndObserver .on (DndObserverEventType .End , ({ draggable , canceled }) => {
3359- const draggableElement = draggable .drag ?.items [0 ].element as HTMLElement | null ;
3360- if (! draggableElement ) return ;
3401+ const previewElement = draggable .drag ?.items [0 ].element as HTMLElement | null ;
3402+ if (! previewElement ) return ;
3403+
3404+ const draggableId = previewElement .getAttribute (' data-id' ) || ' ' ;
3405+ if (draggableId === ' ' ) return ;
3406+
3407+ const originalElement = originalElementMap .get (draggableId ) || null ;
33613408
33623409 // Find out the original container and the target container based on the best
33633410 // match droppable.
33643411 const bestMatch = bestMatchMap .get (draggable );
3365- const originalContainer = draggableElement .parentElement ! ;
3412+ const originalContainer = originalElement ? .parentElement as HTMLElement | null ;
33663413 const targetContainer =
33673414 ! canceled && bestMatch ? (bestMatch .element as HTMLElement ) : originalContainer ;
33683415
3369- // Record the element's current viewport position before any DOM changes.
3370- const rect = draggableElement .getBoundingClientRect ();
3416+ if (! originalElement || ! originalContainer || ! targetContainer ) {
3417+ cleanupDrag (previewElement , originalElement , bestMatch || null , draggable );
3418+ return ;
3419+ }
33713420
3372- // Clear the drag-applied transform so the element returns to its natural
3373- // CSS position within whichever container it ends up in.
3374- draggableElement .style .transform = ' ' ;
3421+ const baseLeft = parseFloat (previewElement .style .left || ' 0' );
3422+ const baseTop = parseFloat (previewElement .style .top || ' 0' );
3423+ const targetPos = getTargetPosition (targetContainer );
3424+ const currentPos = previewElement .getBoundingClientRect ();
3425+ const deltaX = targetPos .left - currentPos .left ;
3426+ const deltaY = targetPos .top - currentPos .top ;
33753427
3376- // If draggable moved into a different container.
3428+ // If draggable moved into a different container, commit the original hidden
3429+ // element to the new slot.
33773430 if (originalContainer !== targetContainer ) {
3378- targetContainer .appendChild (draggableElement );
3431+ targetContainer .appendChild (originalElement );
33793432
33803433 // Move the data-draggable-contained attribute to the target container.
33813434 originalContainer .removeAttribute (' data-draggable-contained' );
3382- targetContainer .setAttribute (
3383- ' data-draggable-contained' ,
3384- draggableElement .getAttribute (' data-id' )! ,
3385- );
3435+ targetContainer .setAttribute (' data-draggable-contained' , draggableId );
33863436 }
33873437
3388- // Compute the CSS offset delta that maintains the element's viewport
3389- // position. getLocalOffset accounts for any ancestor transforms (scale,
3390- // rotation, skew) on the container.
3391- const delta = getLocalOffset (draggableElement , rect .x , rect .y );
3392-
33933438 // Skip animation if the element is already at its target position.
3394- if (Math .abs (delta .x ) < 0.5 && Math .abs (delta .y ) < 0.5 ) {
3395- bestMatch ?.element ?.removeAttribute (' data-draggable-over' );
3396- bestMatchMap .delete (draggable );
3439+ if (Math .abs (deltaX ) < ANIMATION_EPSILON && Math .abs (deltaY ) < ANIMATION_EPSILON ) {
3440+ cleanupDrag (previewElement , originalElement , bestMatch || null , draggable );
33973441 return ;
33983442 }
33993443
3400- // Apply transform to hold the element at its drag-end viewport position.
3401- draggableElement . style . transform = ` translate(${ delta . x }px, ${ delta . y }px) ` ;
3444+ const finalTranslateX = targetPos . left - baseLeft ;
3445+ const finalTranslateY = targetPos . top - baseTop ;
34023446
3403- // Force a reflow to commit the starting transform BEFORE adding the
3404- // transition. Without this the browser's last committed state for transform
3405- // is "none" (from the getLocalOffset reflow), and the transition has no
3406- // starting point to animate from.
3407- draggableElement .clientHeight ;
3447+ previewElement .classList .add (' animating' );
3448+ previewElement .clientHeight ;
3449+ previewElement .style .transform = ` translate(${finalTranslateX }px, ${finalTranslateY }px) ` ;
34083450
3409- // Now enable the transition and animate to identity.
3410- draggableElement .classList .add (' animate' );
34113451 const onTransitionEnd = (e : TransitionEvent ) => {
3412- if (e .target === draggableElement && e .propertyName === ' transform' ) {
3413- draggableElement .classList .remove (' animate' );
3414- draggableElement .style .transform = ' ' ;
3452+ if (e .target === previewElement && e .propertyName === ' transform' ) {
3453+ cleanupDrag (previewElement , originalElement , bestMatch || null , draggable );
34153454 document .body .removeEventListener (' transitionend' , onTransitionEnd );
34163455 }
34173456 };
34183457 document .body .addEventListener (' transitionend' , onTransitionEnd );
3419- draggableElement .style .transform = ' matrix(1, 0, 0, 1, 0, 0)' ;
3420-
3421- // Reset the best match droppable.
3422- bestMatch ?.element ?.removeAttribute (' data-draggable-over' );
3423- bestMatchMap .delete (draggable );
34243458});
34253459```
34263460
@@ -3576,8 +3610,18 @@ body {
35763610 width : calc (100% - 20px );
35773611 height : calc (100% - 20px );
35783612 z-index : 100 ;
3613+ }
3614+
3615+ .card.draggable.hidden {
3616+ opacity : 0 ;
3617+ pointer-events : none ;
3618+ }
3619+
3620+ .drag-preview {
3621+ z-index : 1000 ;
3622+ pointer-events : none ;
35793623
3580- &.animate {
3624+ &.animating {
35813625 transition : transform 0.3s cubic-bezier (0.33 , 0.975 , 0 , 1.65 );
35823626 }
35833627}
0 commit comments