125 lines
3.0 KiB
TypeScript
125 lines
3.0 KiB
TypeScript
import { useCallback, useEffect, useRef, type RefObject } from 'react';
|
|
import {
|
|
bindWindowDrag,
|
|
isPrimaryPointerButton,
|
|
} from '../utils/pointerDrag';
|
|
|
|
export interface Size2d {
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface Position2d {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export interface FloatingWidgetResizeResult {
|
|
width: number;
|
|
height: number;
|
|
x?: number;
|
|
}
|
|
|
|
export type FloatingWidgetResizeCorner = 'se' | 'sw';
|
|
|
|
function clampSize(
|
|
width: number,
|
|
height: number,
|
|
minSize: Size2d,
|
|
maxW: number,
|
|
maxH: number,
|
|
): Size2d {
|
|
return {
|
|
width: Math.round(Math.max(minSize.width, Math.min(maxW, width))),
|
|
height: Math.round(Math.max(minSize.height, Math.min(maxH, height))),
|
|
};
|
|
}
|
|
|
|
export function useFloatingWidgetResize(
|
|
panelRef: RefObject<HTMLElement | null>,
|
|
size: Size2d,
|
|
minSize: Size2d,
|
|
onResize: (result: FloatingWidgetResizeResult) => void,
|
|
) {
|
|
const unbindDragRef = useRef<(() => void) | null>(null);
|
|
|
|
const endResize = useCallback(() => {
|
|
unbindDragRef.current?.();
|
|
unbindDragRef.current = null;
|
|
}, []);
|
|
|
|
useEffect(() => () => {
|
|
unbindDragRef.current?.();
|
|
}, []);
|
|
|
|
const startResize = useCallback((
|
|
e: React.PointerEvent,
|
|
corner: FloatingWidgetResizeCorner,
|
|
) => {
|
|
if (!isPrimaryPointerButton(e)) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const handle = e.currentTarget as HTMLElement;
|
|
try {
|
|
handle.setPointerCapture(e.pointerId);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
const el = panelRef.current;
|
|
if (!el) return;
|
|
|
|
const rect = el.getBoundingClientRect();
|
|
const startX = e.clientX;
|
|
const startY = e.clientY;
|
|
const startW = size.width;
|
|
const startH = size.height;
|
|
const startRight = rect.right;
|
|
const maxH = window.innerHeight - rect.top - 8;
|
|
|
|
unbindDragRef.current?.();
|
|
unbindDragRef.current = bindWindowDrag(
|
|
(xy) => {
|
|
if (corner === 'se') {
|
|
const maxW = window.innerWidth - rect.left - 8;
|
|
const next = clampSize(
|
|
startW + xy.x - startX,
|
|
startH + xy.y - startY,
|
|
minSize,
|
|
maxW,
|
|
maxH,
|
|
);
|
|
onResize(next);
|
|
return;
|
|
}
|
|
|
|
/* sw — 우측 가장자리 고정, 좌측·하단 조절 */
|
|
let width = startRight - xy.x;
|
|
let x = xy.x;
|
|
if (x < 8) {
|
|
x = 8;
|
|
width = startRight - 8;
|
|
}
|
|
const maxW = startRight - 8;
|
|
const next = clampSize(width, startH + xy.y - startY, minSize, maxW, maxH);
|
|
onResize({ ...next, x: Math.round(startRight - next.width) });
|
|
},
|
|
endResize,
|
|
{ preventTouchScroll: true },
|
|
);
|
|
}, [panelRef, size.width, size.height, minSize, onResize, endResize]);
|
|
|
|
const onResizeSePointerDown = useCallback(
|
|
(e: React.PointerEvent) => startResize(e, 'se'),
|
|
[startResize],
|
|
);
|
|
|
|
const onResizeSwPointerDown = useCallback(
|
|
(e: React.PointerEvent) => startResize(e, 'sw'),
|
|
[startResize],
|
|
);
|
|
|
|
return { onResizeSePointerDown, onResizeSwPointerDown };
|
|
}
|