97 lines
3.0 KiB
TypeScript
97 lines
3.0 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;
|
|
}
|
|
|
|
/**
|
|
* 드래그 시작 후 window 에 move/end 리스너 등록.
|
|
* pointer + mouse + touch 모두 수신 (구형 브라우저·iOS 대응).
|
|
*/
|
|
export function bindWindowDrag(
|
|
onMove: (xy: ClientXY, ev: Event) => void,
|
|
onEnd: () => void,
|
|
options: BindWindowDragOptions = {},
|
|
): () => void {
|
|
const { preventTouchScroll = true } = options;
|
|
|
|
const move = (ev: Event) => {
|
|
const xy = clientXYFromNative(ev);
|
|
if (!xy) return;
|
|
if (preventTouchScroll && ev.cancelable && (ev instanceof TouchEvent || ev.type === 'touchmove')) {
|
|
ev.preventDefault();
|
|
}
|
|
onMove(xy, ev);
|
|
};
|
|
|
|
const end = () => onEnd();
|
|
|
|
window.addEventListener('pointermove', move);
|
|
window.addEventListener('pointerup', end);
|
|
window.addEventListener('pointercancel', end);
|
|
window.addEventListener('mousemove', move);
|
|
window.addEventListener('mouseup', end);
|
|
window.addEventListener('touchmove', move, { passive: !preventTouchScroll });
|
|
window.addEventListener('touchend', end);
|
|
window.addEventListener('touchcancel', end);
|
|
|
|
return () => {
|
|
window.removeEventListener('pointermove', move);
|
|
window.removeEventListener('pointerup', end);
|
|
window.removeEventListener('pointercancel', end);
|
|
window.removeEventListener('mousemove', move);
|
|
window.removeEventListener('mouseup', end);
|
|
window.removeEventListener('touchmove', move);
|
|
window.removeEventListener('touchend', end);
|
|
window.removeEventListener('touchcancel', end);
|
|
};
|
|
}
|
|
|
|
/** 드래그 핸들(타이틀바 등)에 붙일 공통 스타일 */
|
|
export const DRAG_HANDLE_TOUCH_STYLE: React.CSSProperties = {
|
|
touchAction: 'none',
|
|
};
|
|
|
|
/** primary 버튼(마우스 왼쪽 / 터치)만 허용 */
|
|
export function isPrimaryPointerButton(e: { button: number; pointerType: string }): boolean {
|
|
return e.button === 0;
|
|
}
|