christian
86ba372227
All checks were successful
Deploy to Cloudflare Pages / deploy (push) Successful in 32s
131 lines
3.5 KiB
TypeScript
131 lines
3.5 KiB
TypeScript
import { useState, useEffect, MouseEvent, useRef, Dispatch, SetStateAction } from 'react';
|
|
|
|
type Coordinates = {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
interface DraggableBoxProps {
|
|
position: Coordinates;
|
|
className?: string;
|
|
children?: React.ReactNode;
|
|
coordSetter: Dispatch<SetStateAction<Coordinates>>;
|
|
mode?: 'handle' | 'box';
|
|
width?: number;
|
|
height?: number;
|
|
onDrag?: (delta: Coordinates) => void;
|
|
}
|
|
|
|
const DraggableBox = ({
|
|
position,
|
|
className = "",
|
|
children = "",
|
|
coordSetter,
|
|
mode = 'handle',
|
|
width = 0,
|
|
height = 0,
|
|
onDrag
|
|
}: DraggableBoxProps) => {
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
|
const dragRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isDragging) return;
|
|
|
|
const handleMouseMove = (e: globalThis.MouseEvent) => {
|
|
if (!dragRef.current?.parentElement) return;
|
|
|
|
const parent = dragRef.current.parentElement;
|
|
const parentRect = parent.getBoundingClientRect();
|
|
|
|
if (mode === 'handle') {
|
|
const x = ((e.clientX - parentRect.left - dragOffset.x) / parentRect.width) * 100;
|
|
const y = ((e.clientY - parentRect.top - dragOffset.y) / parentRect.height) * 100;
|
|
|
|
const newX = Math.min(Math.max(0, x), 100);
|
|
const newY = Math.min(Math.max(0, y), 100);
|
|
|
|
coordSetter({
|
|
x: Number(newX.toFixed(1)),
|
|
y: Number(newY.toFixed(1))
|
|
});
|
|
} else {
|
|
const x = ((e.clientX - parentRect.left - dragOffset.x) / parentRect.width) * 100;
|
|
const y = ((e.clientY - parentRect.top - dragOffset.y) / parentRect.height) * 100;
|
|
|
|
const maxX = 100 - width;
|
|
const maxY = 100 - height;
|
|
|
|
const newX = Math.min(Math.max(0, x), maxX);
|
|
const newY = Math.min(Math.max(0, y), maxY);
|
|
|
|
coordSetter({
|
|
x: Number(newX.toFixed(1)),
|
|
y: Number(newY.toFixed(1))
|
|
});
|
|
|
|
onDrag?.({ x: newX, y: newY });
|
|
}
|
|
};
|
|
|
|
const handleMouseUp = () => setIsDragging(false);
|
|
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
window.addEventListener('mouseup', handleMouseUp);
|
|
|
|
return () => {
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
window.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
}, [isDragging, dragOffset, mode, width, height, onDrag]);
|
|
|
|
const handleMouseDown = (e: MouseEvent) => {
|
|
if (!dragRef.current?.parentElement) return;
|
|
|
|
const parentRect = dragRef.current.parentElement.getBoundingClientRect();
|
|
const elementRect = dragRef.current.getBoundingClientRect();
|
|
|
|
if (mode === 'handle') {
|
|
setDragOffset({
|
|
x: e.clientX - elementRect.left,
|
|
y: e.clientY - elementRect.top
|
|
});
|
|
} else {
|
|
setDragOffset({
|
|
x: e.clientX - parentRect.left - (position.x * parentRect.width / 100),
|
|
y: e.clientY - parentRect.top - (position.y * parentRect.height / 100)
|
|
});
|
|
}
|
|
|
|
setIsDragging(true);
|
|
};
|
|
|
|
const style = mode === 'handle' ? {
|
|
left: `${position.x}%`,
|
|
top: `${position.y}%`,
|
|
transform: 'translate(-50%, -50%)', // Center the handle
|
|
} : {
|
|
left: `${position.x}%`,
|
|
top: `${position.y}%`,
|
|
width: `${width}%`,
|
|
height: `${height}%`,
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={dragRef}
|
|
className={`absolute cursor-pointer transform-gpu ${mode === 'box' ? 'cursor-move' : ''} ${className}`}
|
|
style={{
|
|
...style,
|
|
userSelect: 'none',
|
|
}}
|
|
onMouseDown={handleMouseDown}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DraggableBox;
|