Skip to content

Commit cdd822a

Browse files
committed
Fix advanced collision demo scrolling
1 parent f1f8b0b commit cdd822a

7 files changed

Lines changed: 252 additions & 156 deletions

File tree

packages/dragdoll-docs/docs/examples.md

Lines changed: 122 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
32543253
const 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.
32573260
const scrollContainers = [...document.querySelectorAll('.scroll-list')] as HTMLElement[];
32583261
const draggableElements = [...document.querySelectorAll('.draggable')] as HTMLElement[];
32593262
const 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.
32623326
const dndObserver = new DndObserver<AdvancedCollisionData>({
32633327
collisionDetector: (ctx) => new AdvancedCollisionDetector(ctx),
@@ -3273,28 +3337,23 @@ for (const droppableElement of droppableElements) {
32733337
// Create draggables.
32743338
const draggables: AnyDraggable[] = [];
32753339
for (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.
33193378
dndObserver.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.
33583400
dndObserver.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
}

packages/dragdoll-docs/docs/react/examples.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3994,7 +3994,9 @@ function createDragPreviewElement(element: HTMLElement, draggableId: string): HT
39943994
clone.style.top = `${rect.top}px`;
39953995
clone.style.transform = '';
39963996
clone.classList.add('drag-preview', 'dragging');
3997+
clone.setAttribute('aria-hidden', 'true');
39973998
clone.setAttribute('data-id', draggableId);
3999+
clone.tabIndex = -1;
39984000
dragContainer.appendChild(clone);
39994001
return clone;
40004002
}

packages/dragdoll-docs/docs/solid/examples.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3495,7 +3495,9 @@ function createDragPreviewElement(element: HTMLElement, draggableId: string): HT
34953495
clone.style.top = `${rect.top}px`;
34963496
clone.style.transform = '';
34973497
clone.classList.add('drag-preview', 'dragging');
3498+
clone.setAttribute('aria-hidden', 'true');
34983499
clone.setAttribute('data-id', draggableId);
3500+
clone.tabIndex = -1;
34993501
dragContainer.appendChild(clone);
35003502
return clone;
35013503
}

packages/dragdoll-docs/examples/core/015-dnd-advanced-collision-detector/index.css

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,18 @@ body {
7070
width: calc(100% - 20px);
7171
height: calc(100% - 20px);
7272
z-index: 100;
73+
}
74+
75+
.card.draggable.hidden {
76+
opacity: 0;
77+
pointer-events: none;
78+
}
79+
80+
.drag-preview {
81+
z-index: 1000;
82+
pointer-events: none;
7383

74-
&.animate {
84+
&.animating {
7585
transition: transform 0.3s cubic-bezier(0.33, 0.975, 0, 1.65);
7686
}
7787
}

0 commit comments

Comments
 (0)