windows/composables/useResizable.ts

143 lines
3.8 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';
interface Constraints {
top: number;
right: number;
bottom: number;
left: number;
}
interface ResizeOptions {
onResize: (data: { x: number; y: number; width: number; height: number; }) => void;
onResizeStart?: () => void;
onResizeEnd?: () => void;
initialSize: { width: number; height: number; };
initialPosition: { x: number; y: number; };
constraints?: Ref<Constraints | undefined>;
2025-09-25 06:38:14 +00:00
maxSize?: { width?: number; height?: number; };
minSize?: { width?: number; height?: number; };
2025-09-23 16:43:57 +00:00
enabled: Ref<boolean>;
}
export function useResizable(
target: Ref<HTMLElement | null>,
options: ResizeOptions
) {
2025-09-25 06:38:14 +00:00
const { onResize, onResizeStart, onResizeEnd, initialSize, initialPosition, constraints, maxSize, minSize, enabled } = options;
2025-09-23 16:43:57 +00:00
let activeHandle: string | null = null;
let initialMouseX = 0;
let initialMouseY = 0;
let initialRect = { x: 0, y: 0, width: 0, height: 0 };
const handleMouseDown = (event: MouseEvent) => {
if (!enabled.value) return;
const handle = (event.target as HTMLElement).dataset.direction;
if (!handle) return;
event.preventDefault();
event.stopPropagation();
if (onResizeStart) onResizeStart();
activeHandle = handle;
initialMouseX = event.clientX;
initialMouseY = event.clientY;
initialRect = {
...initialSize,
...initialPosition,
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
const handleMouseMove = (event: MouseEvent) => {
if (!activeHandle) return;
const dx = event.clientX - initialMouseX;
const dy = event.clientY - initialMouseY;
let { x, y, width, height } = initialRect;
if (activeHandle.includes('e')) {
width = initialRect.width + dx;
}
if (activeHandle.includes('w')) {
width = initialRect.width - dx;
x = initialRect.x + dx;
}
if (activeHandle.includes('s')) {
height = initialRect.height + dy;
}
if (activeHandle.includes('n')) {
height = initialRect.height - dy;
y = initialRect.y + dy;
}
if (constraints?.value) {
const { top, right, bottom, left } = constraints.value;
if (x < left) { width -= (left - x); x = left; }
if (y < top) { height -= (top - y); y = top; }
if (x + width > right) { width = right - x; }
if (y + height > bottom) { height = bottom - y; }
}
2025-09-25 06:38:14 +00:00
// Apply minimum size constraints
const minWidth = minSize?.width || 200;
const minHeight = minSize?.height || 150;
width = Math.max(minWidth, width);
height = Math.max(minHeight, height);
// Apply maximum size constraints
if (maxSize?.width) {
width = Math.min(maxSize.width, width);
}
if (maxSize?.height) {
height = Math.min(maxSize.height, height);
}
2025-09-23 16:43:57 +00:00
onResize({ x, y, width, height });
};
const handleMouseUp = () => {
if (onResizeEnd) onResizeEnd();
activeHandle = null;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
const addListener = () => {
if (target.value) {
target.value.addEventListener('mousedown', handleMouseDown);
}
};
const removeListener = () => {
if (target.value) {
target.value.removeEventListener('mousedown', handleMouseDown);
}
};
// 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);
});
}