import { onMounted, onUnmounted, watch } from 'vue'; import type { Ref } from 'vue'; export type SnapType = 'left' | 'right' | 'top' | null; interface Constraints { top: number; right: number; bottom: number; left: number; } interface UseDraggableOptions { onDrag: (x: number, y: number) => void; onDragStart?: () => void; onDragEnd?: (snapType: SnapType) => void; onSnap?: (snapType: SnapType) => void; position: Ref<{ x: number; y: number; }>; constraints?: Ref; targetSize: Ref<{ width: number; height: number; }>; enabled: Ref; } export function useDraggable(target: Ref, options: UseDraggableOptions) { const { onDrag, onDragStart, onDragEnd, onSnap, position, constraints, targetSize, enabled } = options; let startX = 0; let startY = 0; let initialMouseX = 0; let initialMouseY = 0; let currentSnapType: SnapType = null; const handleMouseDown = (event: MouseEvent) => { if (!target.value || !enabled.value) return; if (onDragStart) onDragStart(); startX = position.value.x; startY = position.value.y; initialMouseX = event.clientX; initialMouseY = event.clientY; currentSnapType = null; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; const handleMouseMove = (event: MouseEvent) => { const dx = event.clientX - initialMouseX; const dy = event.clientY - initialMouseY; let newX = startX + dx; let newY = startY + dy; let snapType: SnapType = null; if (event.clientX <= 1) { snapType = 'left'; } else if (event.clientX >= window.innerWidth - 1) { snapType = 'right'; } else if (event.clientY <= 1) { snapType = 'top'; } if (snapType !== currentSnapType) { currentSnapType = snapType; if (onSnap) onSnap(currentSnapType); } if (!currentSnapType && constraints?.value) { const { top, right, bottom, left } = constraints.value; const { width, height } = targetSize.value; newX = Math.max(left, Math.min(newX, right - width)); newY = Math.max(top, Math.min(newY, bottom - height)); } onDrag(newX, newY); }; const handleMouseUp = () => { if (onDragEnd) onDragEnd(currentSnapType); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; const addListener = () => { if (target.value) { target.value.addEventListener('mousedown', handleMouseDown); target.value.style.cursor = 'move'; } }; const removeListener = () => { if (target.value) { target.value.removeEventListener('mousedown', handleMouseDown); target.value.style.cursor = 'default'; } }; // Watch both the target and the enabled flag watch([target, enabled], ([newTarget, isEnabled]) => { if (newTarget) { if (isEnabled) { addListener(); } else { removeListener(); } } }, { immediate: true }); onUnmounted(() => { removeListener(); document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }); }