espace-paie-odentas/hooks/useDraggableModal.ts

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;
}