Files
goldenChart/frontend/src/hooks/useDraggablePanel.ts
T
2026-05-23 15:11:48 +09:00

112 lines
3.1 KiB
TypeScript

import { useState, useRef, useCallback, useEffect, type RefObject } from 'react';
import { bindWindowDrag, clientXYFromReact, isPrimaryPointerButton } from '../utils/pointerDrag';
export interface DraggablePosition {
x: number;
y: number;
}
export interface UseDraggablePanelOptions {
/** 마운트 후 패널 크기 기준 화면 중앙 배치 */
centerOnMount?: boolean;
initialPosition?: DraggablePosition;
panelRef?: RefObject<HTMLElement | null>;
}
const DEFAULT_POS: DraggablePosition = { x: 80, y: 72 };
/**
* 팝업 패널 타이틀바 드래그 이동 (마우스·터치·펜)
*/
export function useDraggablePanel(options: UseDraggablePanelOptions = {}) {
const {
centerOnMount = false,
initialPosition = DEFAULT_POS,
panelRef: externalRef,
} = options;
const internalRef = useRef<HTMLDivElement>(null);
const panelRef = (externalRef ?? internalRef) as RefObject<HTMLDivElement | null>;
const [pos, setPos] = useState<DraggablePosition>(initialPosition);
const [dragging, setDragging] = useState(false);
const dragOrigin = useRef({ mx: 0, my: 0, px: 0, py: 0 });
const centeredOnce = useRef(false);
const unbindDragRef = useRef<(() => void) | null>(null);
useEffect(() => {
if (!centerOnMount || centeredOnce.current) return;
const el = panelRef.current;
if (!el) return;
const placeCenter = () => {
const w = el.offsetWidth || 400;
const h = el.offsetHeight || 320;
setPos({
x: Math.max(8, (window.innerWidth - w) / 2),
y: Math.max(8, (window.innerHeight - h) / 2),
});
centeredOnce.current = true;
};
placeCenter();
const id = requestAnimationFrame(placeCenter);
return () => cancelAnimationFrame(id);
}, [centerOnMount, panelRef]);
const endDrag = useCallback(() => {
unbindDragRef.current?.();
unbindDragRef.current = null;
setDragging(false);
}, []);
const onHeaderPointerDown = useCallback((e: React.PointerEvent) => {
if (!isPrimaryPointerButton(e)) return;
if ((e.target as HTMLElement).closest('button')) return;
e.preventDefault();
e.stopPropagation();
const el = e.currentTarget as HTMLElement;
try {
el.setPointerCapture(e.pointerId);
} catch {
/* ignore */
}
const { x, y } = clientXYFromReact(e);
setDragging(true);
dragOrigin.current = { mx: x, my: y, px: pos.x, py: pos.y };
unbindDragRef.current?.();
unbindDragRef.current = bindWindowDrag(
({ x: cx, y: cy }) => {
const o = dragOrigin.current;
setPos({
x: o.px + (cx - o.mx),
y: o.py + (cy - o.my),
});
},
endDrag,
);
}, [pos, endDrag]);
useEffect(() => () => {
unbindDragRef.current?.();
}, []);
const panelStyle: React.CSSProperties = {
position: 'fixed',
left: pos.x,
top: pos.y,
};
return {
panelRef: internalRef,
pos,
dragging,
/** @deprecated onHeaderPointerDown 사용 */
onHeaderMouseDown: onHeaderPointerDown,
onHeaderPointerDown,
panelStyle,
headerCursor: dragging ? 'grabbing' : 'grab',
headerTouchStyle: { touchAction: 'none' as const },
};
}