|
@ -12,8 +12,8 @@ interface Vec2 { |
|
|
|
|
|
|
|
|
export interface DraggableOpts { |
|
|
export interface DraggableOpts { |
|
|
initialRotation?: number; |
|
|
initialRotation?: number; |
|
|
onViewportExit?: () => void; |
|
|
onPageExit?: () => void; |
|
|
onViewportEnter?: () => void; |
|
|
onPageEnter?: () => void; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { |
|
|
export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { |
|
@ -21,8 +21,8 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { |
|
|
const calculateInitialCenter = (): Vec2 => { |
|
|
const calculateInitialCenter = (): Vec2 => { |
|
|
const rect = card.getBoundingClientRect(); |
|
|
const rect = card.getBoundingClientRect(); |
|
|
return { |
|
|
return { |
|
|
x: rect.left + rect.width / 2, |
|
|
x: rect.left + rect.width / 2 + window.scrollX, |
|
|
y: rect.top + rect.height / 2, |
|
|
y: rect.top + rect.height / 2 + window.scrollY, |
|
|
}; |
|
|
}; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
@ -55,25 +55,29 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { |
|
|
let lastMousePosition: Vec2 = { x: 0, y: 0 }; |
|
|
let lastMousePosition: Vec2 = { x: 0, y: 0 }; |
|
|
let activePointerId: number | null = null; |
|
|
let activePointerId: number | null = null; |
|
|
let animationFrameId: number | null = null; |
|
|
let animationFrameId: number | null = null; |
|
|
let isOutsideViewport = false; |
|
|
let isOutsideBounds = false; |
|
|
|
|
|
|
|
|
// --- Helpers ---
|
|
|
// --- Helpers ---
|
|
|
|
|
|
|
|
|
const checkViewportExit = throttle(() => { |
|
|
const checkPageBounds = throttle(() => { |
|
|
// Don't check if we're dragging, user may still be able to move the card back into view
|
|
|
|
|
|
if (state.dragging) return; |
|
|
if (state.dragging) return; |
|
|
|
|
|
|
|
|
const rect = card.getBoundingClientRect(); |
|
|
const rect = card.getBoundingClientRect(); |
|
|
|
|
|
const pageLeft = rect.left + window.scrollX; |
|
|
|
|
|
const pageTop = rect.top + window.scrollY; |
|
|
|
|
|
const pageRight = rect.right + window.scrollX; |
|
|
|
|
|
const pageBottom = rect.bottom + window.scrollY; |
|
|
|
|
|
|
|
|
const outside = |
|
|
const outside = |
|
|
rect.right < 0 || |
|
|
pageRight < 0 || |
|
|
rect.bottom < 0 || |
|
|
pageBottom < 0 || |
|
|
rect.left > window.innerWidth || |
|
|
pageLeft > document.documentElement.scrollWidth || |
|
|
rect.top > window.innerHeight; |
|
|
pageTop > document.documentElement.scrollHeight; |
|
|
|
|
|
|
|
|
if (outside !== isOutsideViewport) { |
|
|
if (outside !== isOutsideBounds) { |
|
|
isOutsideViewport = outside; |
|
|
isOutsideBounds = outside; |
|
|
if (isOutsideViewport) opts.onViewportExit?.(); |
|
|
if (isOutsideBounds) opts.onPageExit?.(); |
|
|
else opts.onViewportEnter?.(); |
|
|
else opts.onPageEnter?.(); |
|
|
} |
|
|
} |
|
|
}, VIEWPORT_CHECK_INTERVAL_MS); |
|
|
}, VIEWPORT_CHECK_INTERVAL_MS); |
|
|
|
|
|
|
|
@ -158,10 +162,8 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { |
|
|
// 1. Store current visual state relative to the *old* initialCenter
|
|
|
// 1. Store current visual state relative to the *old* initialCenter
|
|
|
const currentDeltaX = center.x - initialCenter.x; |
|
|
const currentDeltaX = center.x - initialCenter.x; |
|
|
const currentDeltaY = center.y - initialCenter.y; |
|
|
const currentDeltaY = center.y - initialCenter.y; |
|
|
const currentRotation = rotation; // Rotation doesn't depend on initialCenter
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Temporarily remove the transform
|
|
|
// 2. Temporarily remove the transform
|
|
|
card.style.transition = "none"; // Disable transitions during adjustment
|
|
|
|
|
|
card.style.transform = "none"; |
|
|
card.style.transform = "none"; |
|
|
|
|
|
|
|
|
// 3. Force browser reflow to get the *untouched* layout position
|
|
|
// 3. Force browser reflow to get the *untouched* layout position
|
|
@ -180,10 +182,7 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { |
|
|
|
|
|
|
|
|
// 6. Reapply the transform immediately before the next paint
|
|
|
// 6. Reapply the transform immediately before the next paint
|
|
|
// Use the *stored* delta and rotation to put it back visually where it was
|
|
|
// Use the *stored* delta and rotation to put it back visually where it was
|
|
|
card.style.transform = `translate(${currentDeltaX}px, ${currentDeltaY}px) rotate(${currentRotation}rad)`; |
|
|
card.style.transform = `translate(${currentDeltaX}px, ${currentDeltaY}px) rotate(${rotation}rad)`; |
|
|
|
|
|
|
|
|
// 7. Re-enable transitions if they were used
|
|
|
|
|
|
card.style.transition = ""; // Or restore previous transition style if needed
|
|
|
|
|
|
|
|
|
|
|
|
// The render loop will continue from this adjusted state.
|
|
|
// The render loop will continue from this adjusted state.
|
|
|
}, RESIZE_DEBOUNCE_MS); // Apply debouncing
|
|
|
}, RESIZE_DEBOUNCE_MS); // Apply debouncing
|
|
@ -231,7 +230,7 @@ export function makeDraggable(card: HTMLElement, opts: DraggableOpts = {}) { |
|
|
rotate(${rotation}rad) |
|
|
rotate(${rotation}rad) |
|
|
`;
|
|
|
`;
|
|
|
|
|
|
|
|
|
checkViewportExit(); |
|
|
checkPageBounds(); |
|
|
animationFrameId = requestAnimationFrame(render); |
|
|
animationFrameId = requestAnimationFrame(render); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|