Files
goldenChart/frontend/src/utils/pointerDrag.ts
T

144 lines
4.7 KiB
TypeScript

/**
* 마우스·터치·펜 입력을 통합한 드래그/포인터 좌표 유틸
*/
export interface ClientXY {
x: number;
y: number;
}
type ReactPointerLike =
| React.MouseEvent
| React.TouchEvent
| React.PointerEvent;
/** React 합성 이벤트에서 client 좌표 추출 */
export function clientXYFromReact(e: ReactPointerLike): ClientXY {
if ('touches' in e && e.touches.length > 0) {
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
if ('changedTouches' in e && e.changedTouches.length > 0) {
return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY };
}
const pe = e as React.MouseEvent | React.PointerEvent;
return { x: pe.clientX, y: pe.clientY };
}
/** window/document 네이티브 이벤트에서 client 좌표 추출 */
export function clientXYFromNative(e: Event): ClientXY | null {
if (e instanceof TouchEvent) {
const t = e.touches[0] ?? e.changedTouches[0];
if (!t) return null;
return { x: t.clientX, y: t.clientY };
}
if (e instanceof MouseEvent || e instanceof PointerEvent) {
return { x: e.clientX, y: e.clientY };
}
return null;
}
export interface BindWindowDragOptions {
/** 터치 드래그 중 페이지 스크롤 방지 (기본 true) */
preventTouchScroll?: boolean;
/** capture 단계에서 수신 — overlay stopPropagation 우회 */
useCapture?: boolean;
}
/**
* document capture 단계 pointer 추적 (스크롤 패널 내부 드래그 — WKWebView 대응)
*/
export function bindDocumentPointerDrag(
pointerId: number,
onMove: (xy: ClientXY) => void,
onEnd: (xy: ClientXY) => void,
): () => void {
const cap: AddEventListenerOptions = { capture: true, passive: false };
const move = (ev: Event) => {
if (!(ev instanceof PointerEvent) || ev.pointerId !== pointerId) return;
if (ev.cancelable) ev.preventDefault();
onMove({ x: ev.clientX, y: ev.clientY });
};
const end = (ev: Event) => {
if (!(ev instanceof PointerEvent) || ev.pointerId !== pointerId) return;
if (ev.cancelable) ev.preventDefault();
onEnd({ x: ev.clientX, y: ev.clientY });
};
document.addEventListener('pointermove', move, cap);
document.addEventListener('pointerup', end, cap);
document.addEventListener('pointercancel', end, cap);
return () => {
document.removeEventListener('pointermove', move, cap);
document.removeEventListener('pointerup', end, cap);
document.removeEventListener('pointercancel', end, cap);
};
}
/**
* 드래그 시작 후 window 에 move/end 리스너 등록.
* pointer + mouse + touch 모두 수신 (구형 브라우저·iOS 대응).
*/
export function bindWindowDrag(
onMove: (xy: ClientXY, ev: Event) => void,
onEnd: (xy: ClientXY, ev: Event) => void,
options: BindWindowDragOptions = {},
): () => void {
const { preventTouchScroll = true, useCapture = false } = options;
let lastXY: ClientXY = { x: 0, y: 0 };
const moveOpts: AddEventListenerOptions | boolean = useCapture
? { capture: true, passive: !preventTouchScroll }
: { passive: !preventTouchScroll };
const endOpts: AddEventListenerOptions | boolean = useCapture
? { capture: true }
: false;
const move = (ev: Event) => {
const xy = clientXYFromNative(ev);
if (!xy) return;
lastXY = xy;
if (preventTouchScroll && ev.cancelable && (ev instanceof TouchEvent || ev.type === 'touchmove')) {
ev.preventDefault();
}
onMove(xy, ev);
};
const end = (ev: Event) => {
const xy = clientXYFromNative(ev) ?? lastXY;
onEnd(xy, ev);
};
window.addEventListener('pointermove', move, moveOpts);
window.addEventListener('pointerup', end, endOpts);
window.addEventListener('pointercancel', end, endOpts);
window.addEventListener('mousemove', move, moveOpts);
window.addEventListener('mouseup', end, endOpts);
window.addEventListener('touchmove', move, moveOpts);
window.addEventListener('touchend', end, endOpts);
window.addEventListener('touchcancel', end, endOpts);
return () => {
window.removeEventListener('pointermove', move, moveOpts);
window.removeEventListener('pointerup', end, endOpts);
window.removeEventListener('pointercancel', end, endOpts);
window.removeEventListener('mousemove', move, moveOpts);
window.removeEventListener('mouseup', end, endOpts);
window.removeEventListener('touchmove', move, moveOpts);
window.removeEventListener('touchend', end, endOpts);
window.removeEventListener('touchcancel', end, endOpts);
};
}
/** 드래그 핸들(타이틀바 등)에 붙일 공통 스타일 */
export const DRAG_HANDLE_TOUCH_STYLE: React.CSSProperties = {
touchAction: 'none',
};
/** primary 버튼(마우스 왼쪽 / 터치)만 허용 */
export function isPrimaryPointerButton(e: { button: number; pointerType: string }): boolean {
return e.button === 0;
}