맥 os 전략편집기 지표 드래그 문제해결
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
/**
|
||||
* Desktop(Tauri) 및 터치 — HTML5 DnD 대신 pointer/touch + overlay 드래그
|
||||
* (useDraggablePanel 과 동일: pointerdown 1회 bindWindowDrag 로 전체 제스처 처리)
|
||||
* Desktop(Tauri) — document capture pointer 드래그 + overlay
|
||||
* Web — HTML5 DnD
|
||||
*
|
||||
* Desktop: pointerdown 즉시 overlay + document capture move/end
|
||||
* → subscribePaletteDrag(move) → preview + drop (동일 좌표 파이프)
|
||||
* Web: HTML5 drag + dragover 좌표 (브라우저 네이티브 지원)
|
||||
*/
|
||||
import { elementsUnderDragPoint } from './flowScreenProjection';
|
||||
import {
|
||||
bindDocumentPointerDrag,
|
||||
bindWindowDrag,
|
||||
isPrimaryPointerButton,
|
||||
} from './pointerDrag';
|
||||
import { isDesktop, isMacDesktop } from './platform';
|
||||
import { isDesktop } from './platform';
|
||||
|
||||
export type PaletteDragPayload = Record<string, unknown> & {
|
||||
type: string;
|
||||
@@ -40,20 +45,21 @@ type OverlayController = {
|
||||
bindElement: (el: HTMLElement | null) => void;
|
||||
};
|
||||
|
||||
let suppressClickUntil = 0;
|
||||
const DRAG_THRESHOLD_PX = 6;
|
||||
/** 클릭과 드래그 구분 — 이보다 작으면 cancel (클릭 허용) */
|
||||
const CLICK_VS_DRAG_PX = 5;
|
||||
|
||||
let suppressClickUntil = 0;
|
||||
let activePayload: PaletteDragPayload | null = null;
|
||||
let activePointerId: number | null = null;
|
||||
let overlayController: OverlayController | null = null;
|
||||
let overlayElement: HTMLElement | null = null;
|
||||
let gestureCaptureEl: 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 unbindGesture: (() => void) | null = null;
|
||||
let lastDragClientXY: { x: number; y: number } | null = null;
|
||||
|
||||
let htmlDragMouseTrackUnbind: (() => void) | null = null;
|
||||
let htmlPaletteDragActive = false;
|
||||
let lastHtmlDragClientXY: { x: number; y: number } | null = null;
|
||||
@@ -66,58 +72,12 @@ type HtmlPaletteDropRouter = (
|
||||
) => void;
|
||||
|
||||
let htmlDropRouter: HtmlPaletteDropRouter | null = null;
|
||||
let htmlDropCommitted = false;
|
||||
|
||||
export function setHtmlPaletteDropRouter(router: HtmlPaletteDropRouter | null): void {
|
||||
htmlDropRouter = router;
|
||||
}
|
||||
|
||||
export function getActiveHtmlDragPayload(): PaletteDragPayload | null {
|
||||
return activeHtmlDragPayload;
|
||||
}
|
||||
|
||||
function installHtmlPaletteDragGlobals() {
|
||||
if (typeof window === 'undefined') return;
|
||||
const w = window as Window & { __gcPaletteHtmlDragGlobals?: boolean };
|
||||
if (w.__gcPaletteHtmlDragGlobals) return;
|
||||
w.__gcPaletteHtmlDragGlobals = true;
|
||||
|
||||
window.addEventListener('dragover', (e) => {
|
||||
if (!htmlPaletteDragActive) return;
|
||||
if (e.clientX !== 0 || e.clientY !== 0) {
|
||||
lastHtmlDragClientXY = { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
||||
}, { passive: false });
|
||||
|
||||
window.addEventListener('drag', (e) => {
|
||||
if (!htmlPaletteDragActive) return;
|
||||
if (e.clientX !== 0 || e.clientY !== 0) {
|
||||
lastHtmlDragClientXY = { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('dragend', () => {
|
||||
stopHtmlDragMouseTracking();
|
||||
htmlPaletteDragActive = false;
|
||||
lastHtmlDragClientXY = null;
|
||||
activeHtmlDragPayload = null;
|
||||
});
|
||||
|
||||
document.addEventListener('drop', (e) => {
|
||||
if (!htmlPaletteDragActive) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const payload = readPaletteHtmlDragDataFromEvent(e) ?? activeHtmlDragPayload;
|
||||
if (!payload || !htmlDropRouter) return;
|
||||
const x = e.clientX !== 0 || e.clientY !== 0 ? e.clientX : (lastHtmlDragClientXY?.x ?? 0);
|
||||
const y = e.clientX !== 0 || e.clientY !== 0 ? e.clientY : (lastHtmlDragClientXY?.y ?? 0);
|
||||
htmlDropRouter(x, y, payload);
|
||||
}, true);
|
||||
}
|
||||
|
||||
installHtmlPaletteDragGlobals();
|
||||
|
||||
function hasCoarsePointer(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
try {
|
||||
@@ -127,12 +87,15 @@ function hasCoarsePointer(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** HTML5 DnD 대신 pointer overlay (Windows Desktop 등). macOS WKWebView는 HTML5 + mousemove preview */
|
||||
/** Desktop(Tauri) + coarse touch — pointer overlay. Web browser — HTML5 */
|
||||
export function needsPointerPaletteDrag(): boolean {
|
||||
if (isMacDesktop()) return false;
|
||||
return isDesktop() || hasCoarsePointer();
|
||||
}
|
||||
|
||||
export function isPaletteDragSessionActive(): boolean {
|
||||
return activePayload != null || htmlPaletteDragActive;
|
||||
}
|
||||
|
||||
export function subscribePaletteDrag(listener: PaletteDragListener): () => void {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
@@ -165,24 +128,8 @@ function emit(event: PaletteDragEvent) {
|
||||
}
|
||||
|
||||
function dispatchEnd(xy: { x: number; y: number }, payload: PaletteDragPayload) {
|
||||
const event: PaletteDragEvent = { phase: 'end', x: xy.x, y: xy.y, payload };
|
||||
dropHandler?.(event);
|
||||
emit(event);
|
||||
}
|
||||
|
||||
function releasePointerCapture(pointerId: number | null) {
|
||||
if (pointerId == null) return;
|
||||
try {
|
||||
gestureCaptureEl?.releasePointerCapture(pointerId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
gestureCaptureEl = null;
|
||||
try {
|
||||
overlayElement?.releasePointerCapture(pointerId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
dropHandler?.({ phase: 'end', x: xy.x, y: xy.y, payload });
|
||||
emit({ phase: 'end', x: xy.x, y: xy.y, payload });
|
||||
}
|
||||
|
||||
function unbindGestureListeners() {
|
||||
@@ -190,10 +137,16 @@ function unbindGestureListeners() {
|
||||
unbindGesture = null;
|
||||
}
|
||||
|
||||
function armPaletteDragScrollLock() {
|
||||
document.body.classList.add('se-palette-drag-armed');
|
||||
}
|
||||
|
||||
function releasePaletteDragScrollLock() {
|
||||
document.body.classList.remove('se-palette-drag-armed');
|
||||
}
|
||||
|
||||
function cleanupDrag() {
|
||||
const pointerId = activePointerId;
|
||||
unbindGestureListeners();
|
||||
releasePointerCapture(pointerId);
|
||||
activePayload = null;
|
||||
activePointerId = null;
|
||||
pendingMove = null;
|
||||
@@ -203,13 +156,17 @@ function cleanupDrag() {
|
||||
moveRafId = null;
|
||||
}
|
||||
document.body.classList.remove('se-palette-drag-active');
|
||||
releasePaletteDragScrollLock();
|
||||
overlayController?.unmount();
|
||||
overlayElement = null;
|
||||
document.querySelectorAll('.se-palette-drag-ghost').forEach(el => el.remove());
|
||||
}
|
||||
|
||||
function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) {
|
||||
if (!activePayload) return;
|
||||
if (!activePayload) {
|
||||
unbindGestureListeners();
|
||||
releasePaletteDragScrollLock();
|
||||
return;
|
||||
}
|
||||
const payload = activePayload;
|
||||
const dropXY = lastDragClientXY ?? xy;
|
||||
|
||||
@@ -231,6 +188,7 @@ function mountDragOverlay(
|
||||
activePointerId = pointerId;
|
||||
lastDragClientXY = { x: xy.x, y: xy.y };
|
||||
document.body.classList.add('se-palette-drag-active');
|
||||
armPaletteDragScrollLock();
|
||||
overlayController?.mount({ payload, pointerId, x: xy.x, y: xy.y });
|
||||
emit({ phase: 'start', x: xy.x, y: xy.y, payload });
|
||||
notifyPaletteDragMove(xy.x, xy.y);
|
||||
@@ -260,95 +218,40 @@ export function notifyPaletteDragCancel(clientX: number, clientY: number) {
|
||||
finishDrag('cancel', { x: clientX, y: clientY });
|
||||
}
|
||||
|
||||
export function completePaletteDragAt(clientX: number, clientY: number): boolean {
|
||||
if (!activePayload) return false;
|
||||
finishDrag('end', { x: clientX, y: clientY });
|
||||
return true;
|
||||
}
|
||||
|
||||
export function bindPaletteDragOverlayElement(el: HTMLElement | null) {
|
||||
overlayElement = el;
|
||||
overlayController?.bindElement(el);
|
||||
if (el && activePointerId != null) {
|
||||
try {
|
||||
el.setPointerCapture(activePointerId);
|
||||
} catch {
|
||||
/* window gesture listener 가 move/end 처리 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop pointer 드래그 — pointerdown 즉시 overlay, document capture 로 move/end
|
||||
* (스크롤 패널·setPointerCapture 충돌 없음 — drag + preview + drop 동일 좌표)
|
||||
*/
|
||||
function startPaletteDragGesture(
|
||||
startX: number,
|
||||
startY: number,
|
||||
pointerId: number,
|
||||
payload: PaletteDragPayload,
|
||||
): void {
|
||||
if (activePayload) {
|
||||
cleanupDrag();
|
||||
}
|
||||
if (activePayload) cleanupDrag();
|
||||
unbindGestureListeners();
|
||||
|
||||
let dragging = false;
|
||||
const origin = { x: startX, y: startY };
|
||||
mountDragOverlay(payload, origin, pointerId);
|
||||
|
||||
unbindGesture = bindWindowDrag(
|
||||
(xy) => {
|
||||
if (!dragging) {
|
||||
const dx = xy.x - startX;
|
||||
const dy = xy.y - startY;
|
||||
if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
|
||||
dragging = true;
|
||||
mountDragOverlay(payload, xy, pointerId);
|
||||
return;
|
||||
}
|
||||
notifyPaletteDragMove(xy.x, xy.y);
|
||||
},
|
||||
unbindGesture = bindDocumentPointerDrag(
|
||||
pointerId,
|
||||
(xy) => notifyPaletteDragMove(xy.x, xy.y),
|
||||
(xy) => {
|
||||
unbindGestureListeners();
|
||||
if (dragging && activePayload) {
|
||||
const dist = Math.hypot(xy.x - origin.x, xy.y - origin.y);
|
||||
if (dist < CLICK_VS_DRAG_PX) {
|
||||
finishDrag('cancel', xy);
|
||||
} else {
|
||||
finishDrag('end', xy);
|
||||
}
|
||||
},
|
||||
{ preventTouchScroll: true },
|
||||
);
|
||||
|
||||
if (gestureCaptureEl) {
|
||||
const el = gestureCaptureEl;
|
||||
const pid = pointerId;
|
||||
const onElMove = (ev: PointerEvent) => {
|
||||
if (ev.pointerId !== pid) return;
|
||||
const xy = { x: ev.clientX, y: ev.clientY };
|
||||
if (!dragging) {
|
||||
const dx = xy.x - startX;
|
||||
const dy = xy.y - startY;
|
||||
if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
|
||||
dragging = true;
|
||||
mountDragOverlay(payload, xy, pointerId);
|
||||
return;
|
||||
}
|
||||
notifyPaletteDragMove(xy.x, xy.y);
|
||||
};
|
||||
const onElEnd = (ev: PointerEvent) => {
|
||||
if (ev.pointerId !== pid) return;
|
||||
el.removeEventListener('pointermove', onElMove);
|
||||
el.removeEventListener('pointerup', onElEnd);
|
||||
el.removeEventListener('pointercancel', onElEnd);
|
||||
unbindGestureListeners();
|
||||
if (dragging && activePayload) {
|
||||
finishDrag('end', { x: ev.clientX, y: ev.clientY });
|
||||
}
|
||||
};
|
||||
el.addEventListener('pointermove', onElMove);
|
||||
el.addEventListener('pointerup', onElEnd);
|
||||
el.addEventListener('pointercancel', onElEnd);
|
||||
const windowUnbind = unbindGesture;
|
||||
unbindGesture = () => {
|
||||
el.removeEventListener('pointermove', onElMove);
|
||||
el.removeEventListener('pointerup', onElEnd);
|
||||
el.removeEventListener('pointercancel', onElEnd);
|
||||
windowUnbind?.();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function handlePalettePointerDown(
|
||||
@@ -358,23 +261,16 @@ export function handlePalettePointerDown(
|
||||
): void {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
if (!isPrimaryPointerButton(e)) return;
|
||||
if ((e.target as HTMLElement).closest('input, textarea, select, button')) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('button')) return;
|
||||
const input = target.closest('input, textarea, select') as HTMLInputElement | null;
|
||||
if (input && !input.readOnly && !input.disabled) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const sourceEl = e.currentTarget as HTMLElement;
|
||||
try {
|
||||
sourceEl.setPointerCapture(e.pointerId);
|
||||
gestureCaptureEl = sourceEl;
|
||||
} catch {
|
||||
/* bindWindowDrag 가 move/end 처리 */
|
||||
}
|
||||
|
||||
startPaletteDragGesture(e.clientX, e.clientY, e.pointerId, payload);
|
||||
}
|
||||
|
||||
/** macOS WebView 등 — pointer 대신 mouse 이벤트만 오는 경우 */
|
||||
export function handlePaletteMouseDown(
|
||||
e: React.MouseEvent,
|
||||
payload: PaletteDragPayload,
|
||||
@@ -382,15 +278,16 @@ export function handlePaletteMouseDown(
|
||||
): void {
|
||||
if (!needsPointerPaletteDrag()) return;
|
||||
if (e.button !== 0) return;
|
||||
if ((e.target as HTMLElement).closest('input, textarea, select, button')) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('button')) return;
|
||||
const input = target.closest('input, textarea, select') as HTMLInputElement | null;
|
||||
if (input && !input.readOnly && !input.disabled) return;
|
||||
if (typeof window !== 'undefined' && 'PointerEvent' in window) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
startPaletteDragGesture(e.clientX, e.clientY, 1, payload);
|
||||
}
|
||||
|
||||
/** PointerEvent 미지원 — touchstart 전용 */
|
||||
export function handlePaletteTouchStart(
|
||||
e: React.TouchEvent,
|
||||
payload: PaletteDragPayload,
|
||||
@@ -406,12 +303,77 @@ export function handlePaletteTouchStart(
|
||||
startPaletteDragGesture(touch.clientX, touch.clientY, touch.identifier, payload);
|
||||
}
|
||||
|
||||
/* ── Web HTML5 ── */
|
||||
|
||||
function installHtmlPaletteDragGlobals() {
|
||||
if (typeof window === 'undefined') return;
|
||||
const w = window as Window & { __gcPaletteHtmlDragGlobals?: boolean };
|
||||
if (w.__gcPaletteHtmlDragGlobals) return;
|
||||
w.__gcPaletteHtmlDragGlobals = true;
|
||||
|
||||
window.addEventListener('dragover', (e) => {
|
||||
if (!htmlPaletteDragActive) return;
|
||||
if (e.clientX !== 0 || e.clientY !== 0) {
|
||||
lastHtmlDragClientXY = { x: e.clientX, y: e.clientY };
|
||||
if (activeHtmlDragPayload) {
|
||||
emit({
|
||||
phase: 'move',
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
payload: activeHtmlDragPayload,
|
||||
});
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
||||
}, { passive: false });
|
||||
|
||||
window.addEventListener('drag', (e) => {
|
||||
if (!htmlPaletteDragActive || !activeHtmlDragPayload) return;
|
||||
if (e.clientX !== 0 || e.clientY !== 0) {
|
||||
lastHtmlDragClientXY = { x: e.clientX, y: e.clientY };
|
||||
emit({
|
||||
phase: 'move',
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
payload: activeHtmlDragPayload,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('dragend', () => {
|
||||
const payload = activeHtmlDragPayload;
|
||||
const xy = lastHtmlDragClientXY;
|
||||
if (htmlPaletteDragActive && !htmlDropCommitted && payload && xy && htmlDropRouter) {
|
||||
htmlDropRouter(xy.x, xy.y, payload);
|
||||
}
|
||||
htmlDropCommitted = false;
|
||||
stopHtmlDragMouseTracking();
|
||||
htmlPaletteDragActive = false;
|
||||
lastHtmlDragClientXY = null;
|
||||
activeHtmlDragPayload = null;
|
||||
});
|
||||
|
||||
document.addEventListener('drop', (e) => {
|
||||
if (!htmlPaletteDragActive) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const payload = readPaletteHtmlDragDataFromEvent(e) ?? activeHtmlDragPayload;
|
||||
if (!payload || !htmlDropRouter || htmlDropCommitted) return;
|
||||
htmlDropCommitted = true;
|
||||
const x = e.clientX !== 0 || e.clientY !== 0 ? e.clientX : (lastHtmlDragClientXY?.x ?? 0);
|
||||
const y = e.clientX !== 0 || e.clientY !== 0 ? e.clientY : (lastHtmlDragClientXY?.y ?? 0);
|
||||
htmlDropRouter(x, y, payload);
|
||||
}, true);
|
||||
}
|
||||
|
||||
installHtmlPaletteDragGlobals();
|
||||
|
||||
function stopHtmlDragMouseTracking(): void {
|
||||
htmlDragMouseTrackUnbind?.();
|
||||
htmlDragMouseTrackUnbind = null;
|
||||
}
|
||||
|
||||
/** HTML5 drag 중 mousemove 로 preview 좌표 추적 (macOS WKWebView — dragover 좌표/hit-test 불안정) */
|
||||
export function startHtmlDragMouseTracking(
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
@@ -427,9 +389,7 @@ export function startHtmlDragMouseTracking(
|
||||
lastHtmlDragClientXY = xy;
|
||||
emit({ phase: 'move', x: xy.x, y: xy.y, payload });
|
||||
},
|
||||
() => {
|
||||
/* native HTML5 drag 중 mouseup 은 dragend 전에 올 수 있음 — dragend 에서 정리 */
|
||||
},
|
||||
() => { /* dragend 에서 정리 */ },
|
||||
{ preventTouchScroll: false },
|
||||
);
|
||||
}
|
||||
@@ -441,6 +401,7 @@ export function endHtmlDragMouseTracking(): void {
|
||||
|
||||
export function markPaletteHtmlDragStart(): void {
|
||||
htmlPaletteDragActive = true;
|
||||
htmlDropCommitted = false;
|
||||
lastHtmlDragClientXY = null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user