windows/composables/useDraggable.ts

117 lines
3.2 KiB
TypeScript
Raw Normal View History

2025-09-23 16:43:57 +00:00
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<Constraints | undefined>;
targetSize: Ref<{ width: number; height: number; }>;
enabled: Ref<boolean>;
}
export function useDraggable(target: Ref<HTMLElement | null>, 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);
});
}