터치로 전략컨트롤 동작
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
/**
|
||||
* Desktop(Tauri) — HTML5 DnD 대신 pointer + 전체화면 overlay 드래그
|
||||
* Desktop(Tauri) 및 터치 기기 — HTML5 DnD 대신 pointer/touch + 전체화면 overlay 드래그
|
||||
*/
|
||||
import { elementsUnderDragPoint } from './flowScreenProjection';
|
||||
import {
|
||||
bindWindowDrag,
|
||||
isPrimaryPointerButton,
|
||||
} from './pointerDrag';
|
||||
import { isDesktop } from './platform';
|
||||
|
||||
export type PaletteDragPayload = Record<string, unknown> & {
|
||||
@@ -40,15 +44,28 @@ const DRAG_THRESHOLD_PX = 6;
|
||||
|
||||
let activePayload: PaletteDragPayload | null = null;
|
||||
let activePointerId: number | null = null;
|
||||
let activeTouchId: number | null = null;
|
||||
let overlayController: OverlayController | null = null;
|
||||
let overlayElement: HTMLElement | null = null;
|
||||
let listeners = new Set<PaletteDragListener>();
|
||||
let dropHandler: PaletteDropHandler | null = null;
|
||||
let moveRafId: number | null = null;
|
||||
let pendingMove: { x: number; y: number } | null = null;
|
||||
let unbindActiveDrag: (() => void) | null = null;
|
||||
let pendingPointerListeners: (() => void) | null = null;
|
||||
|
||||
function hasCoarsePointer(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
try {
|
||||
return window.matchMedia('(pointer: coarse)').matches;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** HTML5 DnD 대신 pointer/touch overlay 드래그 사용 */
|
||||
export function needsPointerPaletteDrag(): boolean {
|
||||
return isDesktop();
|
||||
return isDesktop() || hasCoarsePointer();
|
||||
}
|
||||
|
||||
export function subscribePaletteDrag(listener: PaletteDragListener): () => void {
|
||||
@@ -88,9 +105,36 @@ function dispatchEnd(xy: { x: number; y: number }, payload: PaletteDragPayload)
|
||||
emit(event);
|
||||
}
|
||||
|
||||
function releasePointerCapture(pointerId: number | null) {
|
||||
if (pointerId == null) return;
|
||||
try {
|
||||
overlayElement?.releasePointerCapture(pointerId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
if (document.body.hasPointerCapture(pointerId)) {
|
||||
document.body.releasePointerCapture(pointerId);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function clearPendingPointerListeners() {
|
||||
pendingPointerListeners?.();
|
||||
pendingPointerListeners = null;
|
||||
}
|
||||
|
||||
function cleanupDrag() {
|
||||
const pointerId = activePointerId;
|
||||
unbindActiveDrag?.();
|
||||
unbindActiveDrag = null;
|
||||
clearPendingPointerListeners();
|
||||
releasePointerCapture(pointerId);
|
||||
activePayload = null;
|
||||
activePointerId = null;
|
||||
activeTouchId = null;
|
||||
pendingMove = null;
|
||||
if (moveRafId != null) {
|
||||
cancelAnimationFrame(moveRafId);
|
||||
@@ -98,6 +142,7 @@ function cleanupDrag() {
|
||||
}
|
||||
document.body.classList.remove('se-palette-drag-active');
|
||||
overlayController?.unmount();
|
||||
overlayElement = null;
|
||||
document.querySelectorAll('.se-palette-drag-ghost').forEach(el => el.remove());
|
||||
}
|
||||
|
||||
@@ -140,6 +185,37 @@ export function completePaletteDragAt(clientX: number, clientY: number): boolean
|
||||
return true;
|
||||
}
|
||||
|
||||
function eventMatchesActiveInput(ev: Event): boolean {
|
||||
if (!activePayload || activePointerId == null) return false;
|
||||
if (ev instanceof PointerEvent) return ev.pointerId === activePointerId;
|
||||
if (ev instanceof TouchEvent) {
|
||||
if (activeTouchId != null) {
|
||||
const touches = ev.touches.length > 0 ? ev.touches : ev.changedTouches;
|
||||
return Array.from(touches).some(t => t.identifier === activeTouchId);
|
||||
}
|
||||
return ev.touches.length <= 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function bindActiveDragWindowListeners(pointerId: number) {
|
||||
unbindActiveDrag?.();
|
||||
|
||||
unbindActiveDrag = bindWindowDrag(
|
||||
(xy, ev) => {
|
||||
if (!activePayload || activePointerId !== pointerId) return;
|
||||
if (!eventMatchesActiveInput(ev)) return;
|
||||
notifyPaletteDragMove(xy.x, xy.y);
|
||||
},
|
||||
(xy, ev) => {
|
||||
if (!activePayload || activePointerId !== pointerId) return;
|
||||
if (!eventMatchesActiveInput(ev)) return;
|
||||
finishDrag('end', xy);
|
||||
},
|
||||
{ preventTouchScroll: true },
|
||||
);
|
||||
}
|
||||
|
||||
function captureOnOverlay(pointerId: number) {
|
||||
try {
|
||||
overlayElement?.setPointerCapture(pointerId);
|
||||
@@ -159,12 +235,20 @@ function beginDrag(
|
||||
payload: PaletteDragPayload,
|
||||
xy: { x: number; y: number },
|
||||
pointerId: number,
|
||||
touchId: number | null = null,
|
||||
) {
|
||||
if (activePayload) {
|
||||
cleanupDrag();
|
||||
}
|
||||
|
||||
activePayload = payload;
|
||||
activePointerId = pointerId;
|
||||
activeTouchId = touchId;
|
||||
document.body.classList.add('se-palette-drag-active');
|
||||
|
||||
bindActiveDragWindowListeners(pointerId);
|
||||
overlayController?.mount({ payload, pointerId, x: xy.x, y: xy.y });
|
||||
|
||||
requestAnimationFrame(() => captureOnOverlay(pointerId));
|
||||
|
||||
emit({ phase: 'start', x: xy.x, y: xy.y, payload });
|
||||
@@ -178,47 +262,83 @@ export function bindPaletteDragOverlayElement(el: HTMLElement | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function startPaletteDragGesture(
|
||||
startX: number,
|
||||
startY: number,
|
||||
pointerId: number,
|
||||
touchId: number | null,
|
||||
payload: PaletteDragPayload,
|
||||
): void {
|
||||
if (activePayload) {
|
||||
cleanupDrag();
|
||||
}
|
||||
|
||||
clearPendingPointerListeners();
|
||||
|
||||
let dragStarted = false;
|
||||
|
||||
const unbind = bindWindowDrag(
|
||||
(xy, ev) => {
|
||||
if (dragStarted) return;
|
||||
const dx = xy.x - startX;
|
||||
const dy = xy.y - startY;
|
||||
if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
|
||||
dragStarted = true;
|
||||
clearPendingPointerListeners();
|
||||
if (ev.cancelable) ev.preventDefault();
|
||||
beginDrag(payload, xy, pointerId, touchId);
|
||||
},
|
||||
() => {
|
||||
if (dragStarted) return;
|
||||
clearPendingPointerListeners();
|
||||
},
|
||||
{ preventTouchScroll: true },
|
||||
);
|
||||
|
||||
pendingPointerListeners = unbind;
|
||||
}
|
||||
|
||||
export function handlePalettePointerDown(
|
||||
e: React.PointerEvent,
|
||||
payload: PaletteDragPayload,
|
||||
_label: string,
|
||||
): void {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
if (e.button !== 0) return;
|
||||
if (!isPrimaryPointerButton(e)) return;
|
||||
if ((e.target as HTMLElement).closest('input, textarea, select, button')) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const startX = e.clientX;
|
||||
const startY = e.clientY;
|
||||
const pointerId = e.pointerId;
|
||||
startPaletteDragGesture(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
e.pointerId,
|
||||
null,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
const onPointerMove = (ev: PointerEvent) => {
|
||||
if (ev.pointerId !== pointerId) return;
|
||||
const dx = ev.clientX - startX;
|
||||
const dy = ev.clientY - startY;
|
||||
if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
|
||||
/** PointerEvent 미지원 환경 — touchstart 전용 진입 */
|
||||
export function handlePaletteTouchStart(
|
||||
e: React.TouchEvent,
|
||||
payload: PaletteDragPayload,
|
||||
_label: string,
|
||||
): void {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
if (typeof window !== 'undefined' && 'PointerEvent' in window) return;
|
||||
if (e.touches.length !== 1) return;
|
||||
if ((e.target as HTMLElement).closest('input, textarea, select, button')) return;
|
||||
|
||||
window.removeEventListener('pointermove', onPointerMove);
|
||||
window.removeEventListener('pointerup', onPointerUp);
|
||||
window.removeEventListener('pointercancel', onPointerCancel);
|
||||
e.preventDefault();
|
||||
|
||||
ev.preventDefault();
|
||||
beginDrag(payload, { x: ev.clientX, y: ev.clientY }, pointerId);
|
||||
};
|
||||
|
||||
const onPointerUp = (ev: PointerEvent) => {
|
||||
if (ev.pointerId !== pointerId) return;
|
||||
window.removeEventListener('pointermove', onPointerMove);
|
||||
window.removeEventListener('pointerup', onPointerUp);
|
||||
window.removeEventListener('pointercancel', onPointerCancel);
|
||||
};
|
||||
|
||||
const onPointerCancel = onPointerUp;
|
||||
|
||||
window.addEventListener('pointermove', onPointerMove, { passive: false });
|
||||
window.addEventListener('pointerup', onPointerUp);
|
||||
window.addEventListener('pointercancel', onPointerCancel);
|
||||
const touch = e.touches[0];
|
||||
startPaletteDragGesture(
|
||||
touch.clientX,
|
||||
touch.clientY,
|
||||
touch.identifier,
|
||||
touch.identifier,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDragPayload): void {
|
||||
|
||||
Reference in New Issue
Block a user