126 lines
4.2 KiB
TypeScript
126 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import React from 'react';
|
|
|
|
type SetPos = React.Dispatch<React.SetStateAction<{ x: number; y: number }>>;
|
|
|
|
export interface DraggableOptions {
|
|
constrainToViewport?: boolean;
|
|
disableIframeDuringDrag?: boolean;
|
|
}
|
|
|
|
export function useDraggableModal(
|
|
modalRef: React.RefObject<HTMLDivElement | null>,
|
|
setModalPosition: SetPos,
|
|
options: DraggableOptions = { constrainToViewport: true, disableIframeDuringDrag: true }
|
|
) {
|
|
const dragState = React.useRef<{
|
|
dragging: boolean;
|
|
startX: number;
|
|
startY: number;
|
|
origLeft: number;
|
|
origTop: number;
|
|
dx: number;
|
|
dy: number;
|
|
width: number;
|
|
height: number;
|
|
raf: number | null;
|
|
iframe: HTMLIFrameElement | null;
|
|
}>({ dragging: false, startX: 0, startY: 0, origLeft: 0, origTop: 0, dx: 0, dy: 0, width: 0, height: 0, raf: null, iframe: null });
|
|
|
|
const pointerDown = (e: React.PointerEvent) => {
|
|
if (!modalRef.current) return;
|
|
const rect = modalRef.current.getBoundingClientRect();
|
|
|
|
dragState.current.dragging = true;
|
|
dragState.current.startX = e.clientX;
|
|
dragState.current.startY = e.clientY;
|
|
dragState.current.origLeft = rect.left;
|
|
dragState.current.origTop = rect.top;
|
|
dragState.current.dx = 0;
|
|
dragState.current.dy = 0;
|
|
dragState.current.width = rect.width;
|
|
dragState.current.height = rect.height;
|
|
|
|
// Improve paint performance
|
|
modalRef.current.style.willChange = 'transform';
|
|
// Ensure no conflicting transform animation
|
|
modalRef.current.style.animation = 'none';
|
|
|
|
// Optionally disable iframe interactions during drag
|
|
if (options.disableIframeDuringDrag) {
|
|
const iframe = modalRef.current.querySelector('iframe');
|
|
if (iframe) {
|
|
dragState.current.iframe = iframe as HTMLIFrameElement;
|
|
dragState.current.iframe.style.pointerEvents = 'none';
|
|
} else {
|
|
dragState.current.iframe = null;
|
|
}
|
|
}
|
|
|
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
};
|
|
|
|
const pointerMove = (e: React.PointerEvent) => {
|
|
if (!dragState.current.dragging || !modalRef.current) return;
|
|
|
|
// Raw delta
|
|
let dx = e.clientX - dragState.current.startX;
|
|
let dy = e.clientY - dragState.current.startY;
|
|
|
|
if (options.constrainToViewport) {
|
|
const maxLeft = Math.max(0, window.innerWidth - dragState.current.width);
|
|
const maxTop = Math.max(0, window.innerHeight - dragState.current.height);
|
|
const wantedLeft = dragState.current.origLeft + dx;
|
|
const wantedTop = dragState.current.origTop + dy;
|
|
const clampedLeft = Math.min(Math.max(0, wantedLeft), maxLeft);
|
|
const clampedTop = Math.min(Math.max(0, wantedTop), maxTop);
|
|
dx = clampedLeft - dragState.current.origLeft;
|
|
dy = clampedTop - dragState.current.origTop;
|
|
}
|
|
|
|
dragState.current.dx = dx;
|
|
dragState.current.dy = dy;
|
|
|
|
if (dragState.current.raf) cancelAnimationFrame(dragState.current.raf);
|
|
dragState.current.raf = requestAnimationFrame(() => {
|
|
if (!modalRef.current) return;
|
|
modalRef.current.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
|
|
});
|
|
};
|
|
|
|
const pointerUp = (e: React.PointerEvent) => {
|
|
if (!dragState.current.dragging) return;
|
|
dragState.current.dragging = false;
|
|
|
|
// Snap to the exact visual rect before clearing transform
|
|
if (modalRef.current) {
|
|
const rectNow = modalRef.current.getBoundingClientRect();
|
|
modalRef.current.style.left = `${rectNow.left}px`;
|
|
modalRef.current.style.top = `${rectNow.top}px`;
|
|
modalRef.current.style.right = 'auto';
|
|
modalRef.current.style.bottom = 'auto';
|
|
setModalPosition({ x: rectNow.left, y: rectNow.top });
|
|
}
|
|
|
|
if (dragState.current.raf) {
|
|
cancelAnimationFrame(dragState.current.raf);
|
|
dragState.current.raf = null;
|
|
}
|
|
if (modalRef.current) {
|
|
modalRef.current.style.transform = '';
|
|
modalRef.current.style.willChange = '';
|
|
}
|
|
if (dragState.current.iframe) {
|
|
dragState.current.iframe.style.pointerEvents = '';
|
|
dragState.current.iframe = null;
|
|
}
|
|
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
|
};
|
|
|
|
return {
|
|
onPointerDown: pointerDown,
|
|
onPointerMove: pointerMove,
|
|
onPointerUp: pointerUp,
|
|
} as const;
|
|
}
|