수정
This commit is contained in:
@@ -141,6 +141,7 @@ import {
|
|||||||
strategyDtoToListExportItem,
|
strategyDtoToListExportItem,
|
||||||
} from '../utils/strategyImportExport';
|
} from '../utils/strategyImportExport';
|
||||||
import PaletteChip from './strategyEditor/PaletteChip';
|
import PaletteChip from './strategyEditor/PaletteChip';
|
||||||
|
import PaletteDragOverlay from './strategyEditor/PaletteDragOverlay';
|
||||||
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
|
||||||
import {
|
import {
|
||||||
clampPanelSize,
|
clampPanelSize,
|
||||||
@@ -1616,6 +1617,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`se-page se-page--${theme}`}>
|
<div className={`se-page se-page--${theme}`}>
|
||||||
|
<PaletteDragOverlay />
|
||||||
{saveToast && (
|
{saveToast && (
|
||||||
<div className="se-save-toast">전략이 DB에 성공적으로 저장되었습니다.</div>
|
<div className="se-save-toast">전략이 DB에 성공적으로 저장되었습니다.</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import {
|
||||||
|
bindPaletteDragOverlayElement,
|
||||||
|
notifyPaletteDragCancel,
|
||||||
|
notifyPaletteDragEnd,
|
||||||
|
notifyPaletteDragMove,
|
||||||
|
registerPaletteDragOverlay,
|
||||||
|
type PaletteDragOverlaySession,
|
||||||
|
} from '../../utils/paletteDragSession';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tauri WebView — 드래그 중 전체 화면 overlay 가 pointer 이벤트·drop 을 수신
|
||||||
|
*/
|
||||||
|
export default function PaletteDragOverlay() {
|
||||||
|
const [session, setSession] = useState<PaletteDragOverlaySession | null>(null);
|
||||||
|
const [cursor, setCursor] = useState({ x: 0, y: 0 });
|
||||||
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
registerPaletteDragOverlay({
|
||||||
|
mount: next => {
|
||||||
|
setCursor({ x: next.x, y: next.y });
|
||||||
|
setSession(next);
|
||||||
|
},
|
||||||
|
unmount: () => setSession(null),
|
||||||
|
bindElement: el => {
|
||||||
|
overlayRef.current = el as HTMLDivElement | null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => registerPaletteDragOverlay(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
bindPaletteDragOverlayElement(overlayRef.current);
|
||||||
|
return () => bindPaletteDragOverlayElement(null);
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!session || !overlayRef.current) return;
|
||||||
|
try {
|
||||||
|
overlayRef.current.setPointerCapture(session.pointerId);
|
||||||
|
} catch {
|
||||||
|
/* body capture fallback */
|
||||||
|
}
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
const onPointerMove = useCallback((e: React.PointerEvent) => {
|
||||||
|
if (!session || e.pointerId !== session.pointerId) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setCursor({ x: e.clientX, y: e.clientY });
|
||||||
|
notifyPaletteDragMove(e.clientX, e.clientY);
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
const onPointerUp = useCallback((e: React.PointerEvent) => {
|
||||||
|
if (!session || e.pointerId !== session.pointerId) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
notifyPaletteDragEnd(e.clientX, e.clientY);
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
const onPointerCancel = useCallback((e: React.PointerEvent) => {
|
||||||
|
if (!session || e.pointerId !== session.pointerId) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
notifyPaletteDragCancel(e.clientX, e.clientY);
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
if (!session) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
ref={overlayRef}
|
||||||
|
className="se-palette-drag-overlay"
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerCancel={onPointerCancel}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="se-palette-drag-overlay-label"
|
||||||
|
style={{ left: cursor.x, top: cursor.y }}
|
||||||
|
>
|
||||||
|
{session.payload.label}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1233,9 +1233,9 @@ function StrategyEditorCanvasInner({
|
|||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
) => {
|
) => {
|
||||||
if (!isOverStrategyBuilder(clientX, clientY)) return;
|
|
||||||
|
|
||||||
const { flowPos, anchorId } = resolveDropFromClient(clientX, clientY);
|
const { flowPos, anchorId } = resolveDropFromClient(clientX, clientY);
|
||||||
|
const overCanvas = isOverStrategyBuilder(clientX, clientY);
|
||||||
|
if (!overCanvas && !anchorId) return;
|
||||||
if (data.type === 'start') {
|
if (data.type === 'start') {
|
||||||
addStartAt(flowPos);
|
addStartAt(flowPos);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2027,6 +2027,12 @@
|
|||||||
background: var(--se-palette-card-bg);
|
background: var(--se-palette-card-bg);
|
||||||
transition: transform 0.12s, box-shadow 0.12s, border-color 0.12s, background 0.12s;
|
transition: transform 0.12s, box-shadow 0.12s, border-color 0.12s, background 0.12s;
|
||||||
}
|
}
|
||||||
|
.desktop-client .se-palette-card {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.desktop-client .se-palette-card:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
.se-palette-card:hover {
|
.se-palette-card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
@@ -2039,6 +2045,34 @@ body.se-palette-drag-active {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.se-palette-drag-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 20040;
|
||||||
|
touch-action: none;
|
||||||
|
cursor: grabbing;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.se-palette-drag-overlay-label {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20041;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--se-text, #e2e8f0);
|
||||||
|
background: color-mix(in srgb, var(--se-accent, #4dabf7) 22%, var(--se-panel-card-bg, #1e293b));
|
||||||
|
border: 1px solid color-mix(in srgb, var(--se-accent, #4dabf7) 55%, transparent);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: min(240px, 90vw);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.se-palette-drag-ghost {
|
.se-palette-drag-ghost {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 20050;
|
z-index: 20050;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import type { ReactFlowInstance } from '@xyflow/react';
|
|||||||
|
|
||||||
type FlowProjector = Pick<ReactFlowInstance, 'screenToFlowPosition' | 'getViewport'>;
|
type FlowProjector = Pick<ReactFlowInstance, 'screenToFlowPosition' | 'getViewport'>;
|
||||||
|
|
||||||
|
const DRAG_LAYER_SELECTOR = '.se-palette-drag-overlay, .se-palette-drag-ghost';
|
||||||
|
|
||||||
function manualProject(
|
function manualProject(
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
@@ -20,6 +22,27 @@ function manualProject(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 드래그 overlay 아래 DOM 탐색 (overlay pointer-events 잠시 비활성) */
|
||||||
|
export function elementsUnderDragPoint(clientX: number, clientY: number): Element[] {
|
||||||
|
const overlays = document.querySelectorAll<HTMLElement>(DRAG_LAYER_SELECTOR);
|
||||||
|
const prev: string[] = [];
|
||||||
|
overlays.forEach(el => {
|
||||||
|
prev.push(el.style.pointerEvents);
|
||||||
|
el.style.pointerEvents = 'none';
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
if (typeof document.elementsFromPoint === 'function') {
|
||||||
|
return document.elementsFromPoint(clientX, clientY);
|
||||||
|
}
|
||||||
|
const hit = document.elementFromPoint(clientX, clientY);
|
||||||
|
return hit ? [hit] : [];
|
||||||
|
} finally {
|
||||||
|
overlays.forEach((el, i) => {
|
||||||
|
el.style.pointerEvents = prev[i] ?? '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** screen(client) → React Flow 좌표 (Tauri WebView screenToFlowPosition 보정) */
|
/** screen(client) → React Flow 좌표 (Tauri WebView screenToFlowPosition 보정) */
|
||||||
export function projectScreenToFlowPosition(
|
export function projectScreenToFlowPosition(
|
||||||
clientX: number,
|
clientX: number,
|
||||||
@@ -31,7 +54,6 @@ export function projectScreenToFlowPosition(
|
|||||||
try {
|
try {
|
||||||
const pos = rf.screenToFlowPosition({ x: clientX, y: clientY });
|
const pos = rf.screenToFlowPosition({ x: clientX, y: clientY });
|
||||||
if (Number.isFinite(pos.x) && Number.isFinite(pos.y)) {
|
if (Number.isFinite(pos.x) && Number.isFinite(pos.y)) {
|
||||||
// WebView 버그: 화면 픽셀을 그대로 반환하는 경우
|
|
||||||
const looksLikeScreenPx =
|
const looksLikeScreenPx =
|
||||||
Math.abs(pos.x - clientX) < 2 && Math.abs(pos.y - clientY) < 2;
|
Math.abs(pos.x - clientX) < 2 && Math.abs(pos.y - clientY) < 2;
|
||||||
if (!looksLikeScreenPx) return pos;
|
if (!looksLikeScreenPx) return pos;
|
||||||
@@ -50,26 +72,18 @@ export function projectScreenToFlowPosition(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 팔레트 드롭 — 전략 빌더 캔버스 위인지 (우측 팔레트 위 release 무시) */
|
/** 팔레트 드롭 — 전략 빌더 캔버스 위인지 */
|
||||||
export function isOverStrategyBuilder(clientX: number, clientY: number): boolean {
|
export function isOverStrategyBuilder(clientX: number, clientY: number): boolean {
|
||||||
const elements = typeof document.elementsFromPoint === 'function'
|
for (const el of elementsUnderDragPoint(clientX, clientY)) {
|
||||||
? document.elementsFromPoint(clientX, clientY)
|
if (el.closest('.se-palette-panel, .se-side-wrap--right')) continue;
|
||||||
: [document.elementFromPoint(clientX, clientY)].filter(Boolean) as Element[];
|
if (el.closest('.se-canvas-wrap, .react-flow__pane, .react-flow__renderer')) return true;
|
||||||
|
|
||||||
for (const el of elements) {
|
|
||||||
if (el.closest('.se-palette-panel, .se-side-wrap--right, .se-palette-drag-ghost')) continue;
|
|
||||||
if (el.closest('.se-canvas-wrap')) return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DOM hit-test — elementsFromPoint 로 ghost·overlay 아래 노드까지 탐색 */
|
/** DOM hit-test — overlay 아래 react-flow 노드 탐색 */
|
||||||
export function findFlowNodeIdAtClientPoint(clientX: number, clientY: number): string | null {
|
export function findFlowNodeIdAtClientPoint(clientX: number, clientY: number): string | null {
|
||||||
const elements = typeof document.elementsFromPoint === 'function'
|
for (const el of elementsUnderDragPoint(clientX, clientY)) {
|
||||||
? document.elementsFromPoint(clientX, clientY)
|
|
||||||
: [document.elementFromPoint(clientX, clientY)].filter(Boolean) as Element[];
|
|
||||||
|
|
||||||
for (const el of elements) {
|
|
||||||
const nodeEl = el.closest('.react-flow__node') as HTMLElement | null;
|
const nodeEl = el.closest('.react-flow__node') as HTMLElement | null;
|
||||||
if (nodeEl) {
|
if (nodeEl) {
|
||||||
const id = nodeEl.getAttribute('data-id');
|
const id = nodeEl.getAttribute('data-id');
|
||||||
@@ -109,10 +123,12 @@ export function resolvePaletteDropClientPoint(
|
|||||||
return { flowPos, anchorId: domId };
|
return { flowPos, anchorId: domId };
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOM hit-test 실패 시 START 노드 bounding rect 로 보조
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (!isConnectTarget(node.id)) continue;
|
if (!isConnectTarget(node.id)) continue;
|
||||||
const el = document.querySelector(`.react-flow__node[data-id="${CSS.escape(node.id)}"]`);
|
const escaped = typeof CSS !== 'undefined' && CSS.escape
|
||||||
|
? CSS.escape(node.id)
|
||||||
|
: node.id.replace(/"/g, '\\"');
|
||||||
|
const el = document.querySelector(`.react-flow__node[data-id="${escaped}"]`);
|
||||||
if (!el) continue;
|
if (!el) continue;
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Tauri WebView — HTML5 DnD 대신 pointer capture 기반 팔레트 드래그
|
* Desktop(Tauri) — HTML5 DnD 대신 pointer + 전체화면 overlay 드래그
|
||||||
*/
|
*/
|
||||||
import { clientXYFromNative } from './pointerDrag';
|
|
||||||
import { isDesktop } from './platform';
|
import { isDesktop } from './platform';
|
||||||
|
|
||||||
export type PaletteDragPayload = Record<string, unknown> & {
|
export type PaletteDragPayload = Record<string, unknown> & {
|
||||||
@@ -19,16 +18,29 @@ export interface PaletteDragEvent {
|
|||||||
payload: PaletteDragPayload;
|
payload: PaletteDragPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaletteDragOverlaySession {
|
||||||
|
payload: PaletteDragPayload;
|
||||||
|
pointerId: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
type PaletteDragListener = (event: PaletteDragEvent) => void;
|
type PaletteDragListener = (event: PaletteDragEvent) => void;
|
||||||
type PaletteDropHandler = (event: PaletteDragEvent) => void;
|
type PaletteDropHandler = (event: PaletteDragEvent) => void;
|
||||||
|
|
||||||
|
type OverlayController = {
|
||||||
|
mount: (session: PaletteDragOverlaySession) => void;
|
||||||
|
unmount: () => void;
|
||||||
|
bindElement: (el: HTMLElement | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
let suppressClickUntil = 0;
|
let suppressClickUntil = 0;
|
||||||
const DRAG_THRESHOLD_PX = 6;
|
const DRAG_THRESHOLD_PX = 6;
|
||||||
|
|
||||||
let activePayload: PaletteDragPayload | null = null;
|
let activePayload: PaletteDragPayload | null = null;
|
||||||
let activePointerId: number | null = null;
|
let activePointerId: number | null = null;
|
||||||
let unbindMove: (() => void) | null = null;
|
let overlayController: OverlayController | null = null;
|
||||||
let pendingPointerId: number | null = null;
|
let overlayElement: HTMLElement | null = null;
|
||||||
let listeners = new Set<PaletteDragListener>();
|
let listeners = new Set<PaletteDragListener>();
|
||||||
let dropHandler: PaletteDropHandler | null = null;
|
let dropHandler: PaletteDropHandler | null = null;
|
||||||
|
|
||||||
@@ -41,11 +53,14 @@ export function subscribePaletteDrag(listener: PaletteDragListener): () => void
|
|||||||
return () => listeners.delete(listener);
|
return () => listeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Canvas 가 mount 직후 drop 콜백 등록 (subscribe 누락 방지) */
|
|
||||||
export function setPaletteDragDropHandler(handler: PaletteDropHandler | null): void {
|
export function setPaletteDragDropHandler(handler: PaletteDropHandler | null): void {
|
||||||
dropHandler = handler;
|
dropHandler = handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerPaletteDragOverlay(controller: OverlayController | null): void {
|
||||||
|
overlayController = controller;
|
||||||
|
}
|
||||||
|
|
||||||
export function getActivePaletteDrag(): PaletteDragPayload | null {
|
export function getActivePaletteDrag(): PaletteDragPayload | null {
|
||||||
return activePayload;
|
return activePayload;
|
||||||
}
|
}
|
||||||
@@ -71,12 +86,10 @@ function dispatchEnd(xy: { x: number; y: number }, payload: PaletteDragPayload)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanupDrag() {
|
function cleanupDrag() {
|
||||||
unbindMove?.();
|
|
||||||
unbindMove = null;
|
|
||||||
activePayload = null;
|
activePayload = null;
|
||||||
activePointerId = null;
|
activePointerId = null;
|
||||||
pendingPointerId = null;
|
|
||||||
document.body.classList.remove('se-palette-drag-active');
|
document.body.classList.remove('se-palette-drag-active');
|
||||||
|
overlayController?.unmount();
|
||||||
document.querySelectorAll('.se-palette-drag-ghost').forEach(el => el.remove());
|
document.querySelectorAll('.se-palette-drag-ghost').forEach(el => el.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,13 +105,40 @@ function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 캔버스 pointerup 백업 — WebView 에서 window 이벤트 누락 시 */
|
export function notifyPaletteDragMove(clientX: number, clientY: number) {
|
||||||
|
if (!activePayload) return;
|
||||||
|
emit({ phase: 'move', x: clientX, y: clientY, payload: activePayload });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifyPaletteDragEnd(clientX: number, clientY: number) {
|
||||||
|
finishDrag('end', { x: clientX, y: clientY });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifyPaletteDragCancel(clientX: number, clientY: number) {
|
||||||
|
finishDrag('cancel', { x: clientX, y: clientY });
|
||||||
|
}
|
||||||
|
|
||||||
export function completePaletteDragAt(clientX: number, clientY: number): boolean {
|
export function completePaletteDragAt(clientX: number, clientY: number): boolean {
|
||||||
if (!activePayload) return false;
|
if (!activePayload) return false;
|
||||||
finishDrag('end', { x: clientX, y: clientY });
|
finishDrag('end', { x: clientX, y: clientY });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function captureOnOverlay(pointerId: number) {
|
||||||
|
try {
|
||||||
|
overlayElement?.setPointerCapture(pointerId);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!overlayElement?.hasPointerCapture(pointerId)) {
|
||||||
|
document.body.setPointerCapture(pointerId);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function beginDrag(
|
function beginDrag(
|
||||||
payload: PaletteDragPayload,
|
payload: PaletteDragPayload,
|
||||||
xy: { x: number; y: number },
|
xy: { x: number; y: number },
|
||||||
@@ -108,48 +148,18 @@ function beginDrag(
|
|||||||
activePointerId = pointerId;
|
activePointerId = pointerId;
|
||||||
document.body.classList.add('se-palette-drag-active');
|
document.body.classList.add('se-palette-drag-active');
|
||||||
|
|
||||||
try {
|
overlayController?.mount({ payload, pointerId, x: xy.x, y: xy.y });
|
||||||
document.body.setPointerCapture(pointerId);
|
requestAnimationFrame(() => captureOnOverlay(pointerId));
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
|
|
||||||
emit({ phase: 'start', x: xy.x, y: xy.y, payload });
|
emit({ phase: 'start', x: xy.x, y: xy.y, payload });
|
||||||
|
}
|
||||||
|
|
||||||
const onMove = (ev: Event) => {
|
export function bindPaletteDragOverlayElement(el: HTMLElement | null) {
|
||||||
if (!(ev instanceof PointerEvent) || ev.pointerId !== pointerId || !activePayload) return;
|
overlayElement = el;
|
||||||
if (ev.cancelable) ev.preventDefault();
|
overlayController?.bindElement(el);
|
||||||
emit({ phase: 'move', x: ev.clientX, y: ev.clientY, payload: activePayload });
|
if (el && activePointerId != null) {
|
||||||
};
|
captureOnOverlay(activePointerId);
|
||||||
|
|
||||||
const onUp = (ev: Event) => {
|
|
||||||
if (!(ev instanceof PointerEvent) || ev.pointerId !== pointerId) return;
|
|
||||||
const xy = clientXYFromNative(ev) ?? { x: ev.clientX, y: ev.clientY };
|
|
||||||
finishDrag('end', xy);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('pointermove', onMove, { passive: false });
|
|
||||||
window.addEventListener('pointerup', onUp, true);
|
|
||||||
window.addEventListener('pointercancel', onUp, true);
|
|
||||||
window.addEventListener('mouseup', onUp, true);
|
|
||||||
document.addEventListener('pointerup', onUp, true);
|
|
||||||
document.addEventListener('lostpointercapture', onUp, true);
|
|
||||||
|
|
||||||
unbindMove = () => {
|
|
||||||
window.removeEventListener('pointermove', onMove);
|
|
||||||
window.removeEventListener('pointerup', onUp, true);
|
|
||||||
window.removeEventListener('pointercancel', onUp, true);
|
|
||||||
window.removeEventListener('mouseup', onUp, true);
|
|
||||||
document.removeEventListener('pointerup', onUp, true);
|
|
||||||
document.removeEventListener('lostpointercapture', onUp, true);
|
|
||||||
try {
|
|
||||||
if (document.body.hasPointerCapture(pointerId)) {
|
|
||||||
document.body.releasePointerCapture(pointerId);
|
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handlePalettePointerDown(
|
export function handlePalettePointerDown(
|
||||||
@@ -161,10 +171,11 @@ export function handlePalettePointerDown(
|
|||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
if ((e.target as HTMLElement).closest('input, textarea, select, button')) return;
|
if ((e.target as HTMLElement).closest('input, textarea, select, button')) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
const startX = e.clientX;
|
const startX = e.clientX;
|
||||||
const startY = e.clientY;
|
const startY = e.clientY;
|
||||||
const pointerId = e.pointerId;
|
const pointerId = e.pointerId;
|
||||||
pendingPointerId = pointerId;
|
|
||||||
|
|
||||||
const onPointerMove = (ev: PointerEvent) => {
|
const onPointerMove = (ev: PointerEvent) => {
|
||||||
if (ev.pointerId !== pointerId) return;
|
if (ev.pointerId !== pointerId) return;
|
||||||
@@ -185,12 +196,11 @@ export function handlePalettePointerDown(
|
|||||||
window.removeEventListener('pointermove', onPointerMove);
|
window.removeEventListener('pointermove', onPointerMove);
|
||||||
window.removeEventListener('pointerup', onPointerUp);
|
window.removeEventListener('pointerup', onPointerUp);
|
||||||
window.removeEventListener('pointercancel', onPointerCancel);
|
window.removeEventListener('pointercancel', onPointerCancel);
|
||||||
pendingPointerId = null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerCancel = onPointerUp;
|
const onPointerCancel = onPointerUp;
|
||||||
|
|
||||||
window.addEventListener('pointermove', onPointerMove);
|
window.addEventListener('pointermove', onPointerMove, { passive: false });
|
||||||
window.addEventListener('pointerup', onPointerUp);
|
window.addEventListener('pointerup', onPointerUp);
|
||||||
window.addEventListener('pointercancel', onPointerCancel);
|
window.addEventListener('pointercancel', onPointerCancel);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user