맥 os 전략편집기 지표 드래그 문제해결
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@goldenchart/desktop",
|
"name": "@goldenchart/desktop",
|
||||||
"version": "0.1.22",
|
"version": "0.1.43",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+1
-1
@@ -1444,7 +1444,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "goldenchart-desktop"
|
name = "goldenchart-desktop"
|
||||||
version = "0.1.0"
|
version = "0.1.43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "goldenchart-desktop"
|
name = "goldenchart-desktop"
|
||||||
version = "0.1.0"
|
version = "0.1.43"
|
||||||
description = "GoldenChart Desktop Client"
|
description = "GoldenChart Desktop Client"
|
||||||
authors = ["GoldenChart"]
|
authors = ["GoldenChart"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "GoldenChart",
|
"productName": "GoldenChart",
|
||||||
"version": "0.1.22",
|
"version": "0.1.43",
|
||||||
"identifier": "com.goldenchart.desktop",
|
"identifier": "com.goldenchart.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"label": "main",
|
"label": "main",
|
||||||
"title": "GoldenChart",
|
"title": "GoldenChart (v0.1.43)",
|
||||||
"width": 1440,
|
"width": 1440,
|
||||||
"height": 900,
|
"height": 900,
|
||||||
"minWidth": 960,
|
"minWidth": 960,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
handlePalettePointerDown,
|
handlePalettePointerDown,
|
||||||
handlePaletteTouchStart,
|
handlePaletteTouchStart,
|
||||||
endHtmlDragMouseTracking,
|
endHtmlDragMouseTracking,
|
||||||
markPaletteHtmlDragEnd,
|
|
||||||
needsPointerPaletteDrag,
|
needsPointerPaletteDrag,
|
||||||
shouldSuppressPaletteClick,
|
shouldSuppressPaletteClick,
|
||||||
startHtmlDragMouseTracking,
|
startHtmlDragMouseTracking,
|
||||||
@@ -52,8 +51,10 @@ export default function PaletteChip({
|
|||||||
longPeriod,
|
longPeriod,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pointerMode = needsPointerPaletteDrag();
|
||||||
|
|
||||||
const onDragStart = (e: React.DragEvent) => {
|
const onDragStart = (e: React.DragEvent) => {
|
||||||
if (needsPointerPaletteDrag()) {
|
if (pointerMode) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -67,14 +68,17 @@ export default function PaletteChip({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onPointerDown = (e: React.PointerEvent) => {
|
const onPointerDown = (e: React.PointerEvent) => {
|
||||||
|
if (!pointerMode) return;
|
||||||
handlePalettePointerDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
|
handlePalettePointerDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseDown = (e: React.MouseEvent) => {
|
const onMouseDown = (e: React.MouseEvent) => {
|
||||||
|
if (!pointerMode) return;
|
||||||
handlePaletteMouseDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
|
handlePaletteMouseDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTouchStart = (e: React.TouchEvent) => {
|
const onTouchStart = (e: React.TouchEvent) => {
|
||||||
|
if (!pointerMode) return;
|
||||||
handlePaletteTouchStart(e, buildPayload(), composite ? `${label} + ${label}` : label);
|
handlePaletteTouchStart(e, buildPayload(), composite ? `${label} + ${label}` : label);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,11 +106,11 @@ export default function PaletteChip({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
draggable={!needsPointerPaletteDrag()}
|
draggable={!pointerMode}
|
||||||
className={`se-palette-card ${color ? `se-palette-card--${color}` : 'se-palette-card--ind'}${composite ? ' se-palette-card--composite' : ''}${selected ? ' se-palette-card--selected' : ''}${needsPointerPaletteDrag() ? ' se-palette-card--pointer' : ''}`}
|
className={`se-palette-card ${color ? `se-palette-card--${color}` : 'se-palette-card--ind'}${composite ? ' se-palette-card--composite' : ''}${selected ? ' se-palette-card--selected' : ''}${pointerMode ? ' se-palette-card--pointer' : ''}`}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onPointerDown={onPointerDown}
|
onPointerDownCapture={onPointerDown}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onTouchStart={onTouchStart}
|
onTouchStart={onTouchStart}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -51,11 +51,6 @@ export default function PaletteDragOverlay() {
|
|||||||
bindPaletteDragOverlayElement(el);
|
bindPaletteDragOverlayElement(el);
|
||||||
const s = sessionRef.current;
|
const s = sessionRef.current;
|
||||||
if (el && s) {
|
if (el && s) {
|
||||||
try {
|
|
||||||
el.setPointerCapture(s.pointerId);
|
|
||||||
} catch {
|
|
||||||
/* window backup in paletteDragSession */
|
|
||||||
}
|
|
||||||
const label = labelRef.current;
|
const label = labelRef.current;
|
||||||
if (label) {
|
if (label) {
|
||||||
label.style.left = `${s.x}px`;
|
label.style.left = `${s.x}px`;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useMemo, useState, useCallback } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import type { DefType } from '../../utils/strategyEditorShared';
|
import type { DefType } from '../../utils/strategyEditorShared';
|
||||||
import {
|
import {
|
||||||
|
handlePaletteMouseDown,
|
||||||
handlePalettePointerDown,
|
handlePalettePointerDown,
|
||||||
handlePaletteTouchStart,
|
handlePaletteTouchStart,
|
||||||
endHtmlDragMouseTracking,
|
endHtmlDragMouseTracking,
|
||||||
@@ -99,6 +100,7 @@ function SidewaysFilterChip({
|
|||||||
|
|
||||||
const onPointerDown = (e: React.PointerEvent) => {
|
const onPointerDown = (e: React.PointerEvent) => {
|
||||||
hideHint();
|
hideHint();
|
||||||
|
if (!needsPointerPaletteDrag()) return;
|
||||||
const payload: PaletteDragPayload = {
|
const payload: PaletteDragPayload = {
|
||||||
type: 'sidewaysFilter',
|
type: 'sidewaysFilter',
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -107,6 +109,16 @@ function SidewaysFilterChip({
|
|||||||
handlePalettePointerDown(e, payload, item.label);
|
handlePalettePointerDown(e, payload, item.label);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onMouseDown = (e: React.MouseEvent) => {
|
||||||
|
hideHint();
|
||||||
|
const payload: PaletteDragPayload = {
|
||||||
|
type: 'sidewaysFilter',
|
||||||
|
value: item.id,
|
||||||
|
label: item.label,
|
||||||
|
};
|
||||||
|
handlePaletteMouseDown(e, payload, item.label);
|
||||||
|
};
|
||||||
|
|
||||||
const onTouchStart = (e: React.TouchEvent) => {
|
const onTouchStart = (e: React.TouchEvent) => {
|
||||||
hideHint();
|
hideHint();
|
||||||
const payload: PaletteDragPayload = {
|
const payload: PaletteDragPayload = {
|
||||||
@@ -136,7 +148,8 @@ function SidewaysFilterChip({
|
|||||||
className={`se-palette-card se-palette-card--range se-palette-card--range-${item.mode}${selected ? ' se-palette-card--selected' : ''}${needsPointerPaletteDrag() ? ' se-palette-card--pointer' : ''}`}
|
className={`se-palette-card se-palette-card--range se-palette-card--range-${item.mode}${selected ? ' se-palette-card--selected' : ''}${needsPointerPaletteDrag() ? ' se-palette-card--pointer' : ''}`}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onPointerDown={onPointerDown}
|
onPointerDownCapture={onPointerDown}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
onTouchStart={onTouchStart}
|
onTouchStart={onTouchStart}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
isPaletteHtmlDrag,
|
isPaletteHtmlDrag,
|
||||||
isPaletteHtmlDragActive,
|
isPaletteHtmlDragActive,
|
||||||
getLastHtmlPaletteDragClientXY,
|
getLastHtmlPaletteDragClientXY,
|
||||||
|
isPaletteDragSessionActive,
|
||||||
markPaletteHtmlDragEnd,
|
markPaletteHtmlDragEnd,
|
||||||
needsPointerPaletteDrag,
|
needsPointerPaletteDrag,
|
||||||
readPaletteHtmlDragData,
|
readPaletteHtmlDragData,
|
||||||
@@ -898,6 +899,7 @@ function StrategyEditorCanvasInner({
|
|||||||
|
|
||||||
// 트리 구조 변경 → 전체 재배치 / 조건 편집 → 라벨만 갱신(위치 유지)
|
// 트리 구조 변경 → 전체 재배치 / 조건 편집 → 라벨만 갱신(위치 유지)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isPaletteDragSessionActive()) return;
|
||||||
const { nodes: layoutNodes, edges: layoutEdges } = rebuildFlow();
|
const { nodes: layoutNodes, edges: layoutEdges } = rebuildFlow();
|
||||||
const mergedEdges = applyEdgeHandles(layoutEdges, positionsRef.current, edgeHandlesRef.current);
|
const mergedEdges = applyEdgeHandles(layoutEdges, positionsRef.current, edgeHandlesRef.current);
|
||||||
pruneEdgeHandles(edgeHandlesRef.current, mergedEdges);
|
pruneEdgeHandles(edgeHandlesRef.current, mergedEdges);
|
||||||
@@ -1319,7 +1321,7 @@ function StrategyEditorCanvasInner({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const xy = getLastHtmlPaletteDragClientXY();
|
const xy = getLastHtmlPaletteDragClientXY();
|
||||||
if (xy) {
|
if (xy && (xy.x !== 0 || xy.y !== 0)) {
|
||||||
const { flowPos, anchorId } = resolveDropFromClientRef.current(xy.x, xy.y);
|
const { flowPos, anchorId } = resolveDropFromClientRef.current(xy.x, xy.y);
|
||||||
if (anchorId) {
|
if (anchorId) {
|
||||||
updatePreviewRef.current(anchorId, flowPos);
|
updatePreviewRef.current(anchorId, flowPos);
|
||||||
@@ -1342,7 +1344,7 @@ function StrategyEditorCanvasInner({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (needsPointerPaletteDrag()) return;
|
if (needsPointerPaletteDrag()) return undefined;
|
||||||
const onDocDragOver = (e: DragEvent) => {
|
const onDocDragOver = (e: DragEvent) => {
|
||||||
if (!isPaletteHtmlDragActive()) return;
|
if (!isPaletteHtmlDragActive()) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1352,12 +1354,13 @@ function StrategyEditorCanvasInner({
|
|||||||
lastPreviewAnchorRef.current = anchorId;
|
lastPreviewAnchorRef.current = anchorId;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('dragover', onDocDragOver, { passive: false });
|
document.addEventListener('dragover', onDocDragOver, { passive: false, capture: true });
|
||||||
return () => document.removeEventListener('dragover', onDocDragOver);
|
return () => document.removeEventListener('dragover', onDocDragOver, { capture: true });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onDocDragEnd = () => {
|
const onDocDragEnd = () => {
|
||||||
|
if (!isPaletteHtmlDragActive()) return;
|
||||||
lastPreviewAnchorRef.current = null;
|
lastPreviewAnchorRef.current = null;
|
||||||
lastPreviewKeyRef.current = null;
|
lastPreviewKeyRef.current = null;
|
||||||
clearPreviewRef.current();
|
clearPreviewRef.current();
|
||||||
|
|||||||
@@ -2050,6 +2050,18 @@ body.se-palette-drag-active {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.se-palette-drag-armed .se-right-body,
|
||||||
|
body.se-palette-drag-armed .se-palette-section--scroll {
|
||||||
|
overflow: hidden !important;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.se-palette-card .se-palette-period-input[readonly] {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.se-palette-drag-overlay {
|
.se-palette-drag-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|||||||
@@ -4,18 +4,27 @@ import {
|
|||||||
getLastHtmlPaletteDragClientXY,
|
getLastHtmlPaletteDragClientXY,
|
||||||
isPaletteHtmlDragActive,
|
isPaletteHtmlDragActive,
|
||||||
} from './paletteDragSession';
|
} from './paletteDragSession';
|
||||||
|
import { getNodeDimensions } from './strategyFlowLayout';
|
||||||
|
|
||||||
type FlowProjector = Pick<ReactFlowInstance, 'screenToFlowPosition' | 'getViewport'>;
|
type FlowProjector = Pick<ReactFlowInstance, 'screenToFlowPosition' | 'getViewport'>;
|
||||||
|
|
||||||
const DRAG_LAYER_SELECTOR = '.se-palette-drag-overlay, .se-palette-drag-ghost';
|
const DRAG_LAYER_SELECTOR = '.se-palette-drag-overlay, .se-palette-drag-ghost';
|
||||||
|
|
||||||
|
function getFlowProjectionHost(containerEl: HTMLElement | null): HTMLElement | null {
|
||||||
|
if (!containerEl) return null;
|
||||||
|
return containerEl.querySelector('.react-flow__viewport') as HTMLElement | null
|
||||||
|
?? containerEl.querySelector('.react-flow__renderer') as HTMLElement | null
|
||||||
|
?? containerEl.querySelector('.react-flow') as HTMLElement | null
|
||||||
|
?? containerEl;
|
||||||
|
}
|
||||||
|
|
||||||
function manualProject(
|
function manualProject(
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
rf: FlowProjector,
|
rf: FlowProjector,
|
||||||
containerEl: HTMLElement | null,
|
containerEl: HTMLElement | null,
|
||||||
): { x: number; y: number } {
|
): { x: number; y: number } {
|
||||||
const host = (containerEl?.querySelector('.react-flow') as HTMLElement | null) ?? containerEl;
|
const host = getFlowProjectionHost(containerEl);
|
||||||
if (!host) return { x: 0, y: 0 };
|
if (!host) return { x: 0, y: 0 };
|
||||||
|
|
||||||
const rect = host.getBoundingClientRect();
|
const rect = host.getBoundingClientRect();
|
||||||
@@ -159,12 +168,73 @@ export interface PaletteDropResolution {
|
|||||||
anchorId: string | null;
|
anchorId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** WKWebView HTML5 drag — dragover client 좌표가 0,0 으로 오는 경우 보정 */
|
function projectFlowToScreen(
|
||||||
export function resolvePaletteDragClientPoint(clientX: number, clientY: number): { x: number; y: number } {
|
flowX: number,
|
||||||
if (clientX !== 0 || clientY !== 0) return { x: clientX, y: clientY };
|
flowY: number,
|
||||||
if (!isPaletteHtmlDragActive()) return { x: clientX, y: clientY };
|
rf: FlowProjector | null,
|
||||||
const last = getLastHtmlPaletteDragClientXY();
|
containerEl: HTMLElement | null,
|
||||||
return last ?? { x: clientX, y: clientY };
|
): { x: number; y: number } {
|
||||||
|
const host = getFlowProjectionHost(containerEl);
|
||||||
|
if (!host || !rf) return { x: flowX, y: flowY };
|
||||||
|
|
||||||
|
const rect = host.getBoundingClientRect();
|
||||||
|
const { x: vx, y: vy, zoom } = rf.getViewport();
|
||||||
|
const z = zoom || 1;
|
||||||
|
return {
|
||||||
|
x: rect.left + vx + flowX * z,
|
||||||
|
y: rect.top + vy + flowY * z,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WKWebView HTML5 drag — dragover client 0,0 보정 (Web 전용) */
|
||||||
|
export function resolvePaletteDragClientPoint(
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
): { x: number; y: number } {
|
||||||
|
if (clientX !== 0 || clientY !== 0) {
|
||||||
|
return { x: clientX, y: clientY };
|
||||||
|
}
|
||||||
|
if (!isPaletteHtmlDragActive() && !getActivePaletteDrag()) {
|
||||||
|
return { x: clientX, y: clientY };
|
||||||
|
}
|
||||||
|
const fromHtml = getLastHtmlPaletteDragClientXY();
|
||||||
|
if (fromHtml && (fromHtml.x !== 0 || fromHtml.y !== 0)) return fromHtml;
|
||||||
|
return { x: clientX, y: clientY };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** flow 노드 screen rect hit-test (DOM / elementsFromPoint 불필요 — WKWebView HTML5 drag 대응) */
|
||||||
|
function findConnectTargetAtClientPoint(
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
rf: FlowProjector | null,
|
||||||
|
containerEl: HTMLElement | null,
|
||||||
|
nodes: { id: string; position: { x: number; y: number } }[],
|
||||||
|
isConnectTarget: (nodeId: string) => boolean,
|
||||||
|
): string | null {
|
||||||
|
const pad = 12;
|
||||||
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||||
|
const n = nodes[i];
|
||||||
|
if (!isConnectTarget(n.id)) continue;
|
||||||
|
const dim = getNodeDimensions(n.id);
|
||||||
|
const tl = projectFlowToScreen(n.position.x, n.position.y, rf, containerEl);
|
||||||
|
const br = projectFlowToScreen(
|
||||||
|
n.position.x + dim.w,
|
||||||
|
n.position.y + dim.h,
|
||||||
|
rf,
|
||||||
|
containerEl,
|
||||||
|
);
|
||||||
|
const left = Math.min(tl.x, br.x) - pad;
|
||||||
|
const right = Math.max(tl.x, br.x) + pad;
|
||||||
|
const top = Math.min(tl.y, br.y) - pad;
|
||||||
|
const bottom = Math.max(tl.y, br.y) + pad;
|
||||||
|
if (
|
||||||
|
clientX >= left && clientX <= right
|
||||||
|
&& clientY >= top && clientY <= bottom
|
||||||
|
) {
|
||||||
|
return n.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** client 좌표 → flow 위치 + 연결 대상 노드 (DOM 우선, flow hit 보조) */
|
/** client 좌표 → flow 위치 + 연결 대상 노드 (DOM 우선, flow hit 보조) */
|
||||||
@@ -183,6 +253,32 @@ export function resolvePaletteDropClientPoint(
|
|||||||
const { x: px, y: py } = resolvePaletteDragClientPoint(clientX, clientY);
|
const { x: px, y: py } = resolvePaletteDragClientPoint(clientX, clientY);
|
||||||
const flowPos = projectScreenToFlowPosition(px, py, rf, containerEl);
|
const flowPos = projectScreenToFlowPosition(px, py, rf, containerEl);
|
||||||
|
|
||||||
|
// 1) DOM rect — pointer overlay 에서 가장 정확
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (!isConnectTarget(node.id)) continue;
|
||||||
|
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}"]`)
|
||||||
|
?? document.getElementById(`react-flow__node-${node.id}`);
|
||||||
|
if (!el) continue;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const pad = 12;
|
||||||
|
if (
|
||||||
|
px >= rect.left - pad && px <= rect.right + pad
|
||||||
|
&& py >= rect.top - pad && py <= rect.bottom + pad
|
||||||
|
) {
|
||||||
|
return { flowPos, anchorId: node.id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flowRectId = findConnectTargetAtClientPoint(
|
||||||
|
px, py, rf, containerEl, nodes, isConnectTarget,
|
||||||
|
);
|
||||||
|
if (flowRectId) {
|
||||||
|
return { flowPos, anchorId: flowRectId };
|
||||||
|
}
|
||||||
|
|
||||||
const domId = findFlowNodeIdAtClientPoint(px, py);
|
const domId = findFlowNodeIdAtClientPoint(px, py);
|
||||||
if (domId && nodes.some(n => n.id === domId) && isConnectTarget(domId)) {
|
if (domId && nodes.some(n => n.id === domId) && isConnectTarget(domId)) {
|
||||||
return { flowPos, anchorId: domId };
|
return { flowPos, anchorId: domId };
|
||||||
@@ -195,23 +291,6 @@ export function resolvePaletteDropClientPoint(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (!isConnectTarget(node.id)) continue;
|
|
||||||
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}"]`)
|
|
||||||
?? document.getElementById(`react-flow__node-${node.id}`);
|
|
||||||
if (!el) continue;
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
if (
|
|
||||||
px >= rect.left && px <= rect.right
|
|
||||||
&& py >= rect.top && py <= rect.bottom
|
|
||||||
) {
|
|
||||||
return { flowPos, anchorId: node.id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const flowHit = findAtFlow(flowPos, nodes);
|
const flowHit = findAtFlow(flowPos, nodes);
|
||||||
if (flowHit && isConnectTarget(flowHit.id)) {
|
if (flowHit && isConnectTarget(flowHit.id)) {
|
||||||
return { flowPos, anchorId: flowHit.id };
|
return { flowPos, anchorId: flowHit.id };
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Desktop(Tauri) 및 터치 — HTML5 DnD 대신 pointer/touch + overlay 드래그
|
* Desktop(Tauri) — document capture pointer 드래그 + overlay
|
||||||
* (useDraggablePanel 과 동일: pointerdown 1회 bindWindowDrag 로 전체 제스처 처리)
|
* Web — HTML5 DnD
|
||||||
|
*
|
||||||
|
* Desktop: pointerdown 즉시 overlay + document capture move/end
|
||||||
|
* → subscribePaletteDrag(move) → preview + drop (동일 좌표 파이프)
|
||||||
|
* Web: HTML5 drag + dragover 좌표 (브라우저 네이티브 지원)
|
||||||
*/
|
*/
|
||||||
import { elementsUnderDragPoint } from './flowScreenProjection';
|
import { elementsUnderDragPoint } from './flowScreenProjection';
|
||||||
import {
|
import {
|
||||||
|
bindDocumentPointerDrag,
|
||||||
bindWindowDrag,
|
bindWindowDrag,
|
||||||
isPrimaryPointerButton,
|
isPrimaryPointerButton,
|
||||||
} from './pointerDrag';
|
} from './pointerDrag';
|
||||||
import { isDesktop, isMacDesktop } from './platform';
|
import { isDesktop } from './platform';
|
||||||
|
|
||||||
export type PaletteDragPayload = Record<string, unknown> & {
|
export type PaletteDragPayload = Record<string, unknown> & {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -40,20 +45,21 @@ type OverlayController = {
|
|||||||
bindElement: (el: HTMLElement | null) => void;
|
bindElement: (el: HTMLElement | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let suppressClickUntil = 0;
|
/** 클릭과 드래그 구분 — 이보다 작으면 cancel (클릭 허용) */
|
||||||
const DRAG_THRESHOLD_PX = 6;
|
const CLICK_VS_DRAG_PX = 5;
|
||||||
|
|
||||||
|
let suppressClickUntil = 0;
|
||||||
let activePayload: PaletteDragPayload | null = null;
|
let activePayload: PaletteDragPayload | null = null;
|
||||||
let activePointerId: number | null = null;
|
let activePointerId: number | null = null;
|
||||||
let overlayController: OverlayController | null = null;
|
let overlayController: OverlayController | null = null;
|
||||||
let overlayElement: HTMLElement | null = null;
|
let overlayElement: HTMLElement | null = null;
|
||||||
let gestureCaptureEl: HTMLElement | null = null;
|
|
||||||
let listeners = new Set<PaletteDragListener>();
|
let listeners = new Set<PaletteDragListener>();
|
||||||
let dropHandler: PaletteDropHandler | null = null;
|
let dropHandler: PaletteDropHandler | null = null;
|
||||||
let moveRafId: number | null = null;
|
let moveRafId: number | null = null;
|
||||||
let pendingMove: { x: number; y: number } | null = null;
|
let pendingMove: { x: number; y: number } | null = null;
|
||||||
let unbindGesture: (() => void) | null = null;
|
let unbindGesture: (() => void) | null = null;
|
||||||
let lastDragClientXY: { x: number; y: number } | null = null;
|
let lastDragClientXY: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
let htmlDragMouseTrackUnbind: (() => void) | null = null;
|
let htmlDragMouseTrackUnbind: (() => void) | null = null;
|
||||||
let htmlPaletteDragActive = false;
|
let htmlPaletteDragActive = false;
|
||||||
let lastHtmlDragClientXY: { x: number; y: number } | null = null;
|
let lastHtmlDragClientXY: { x: number; y: number } | null = null;
|
||||||
@@ -66,58 +72,12 @@ type HtmlPaletteDropRouter = (
|
|||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
let htmlDropRouter: HtmlPaletteDropRouter | null = null;
|
let htmlDropRouter: HtmlPaletteDropRouter | null = null;
|
||||||
|
let htmlDropCommitted = false;
|
||||||
|
|
||||||
export function setHtmlPaletteDropRouter(router: HtmlPaletteDropRouter | null): void {
|
export function setHtmlPaletteDropRouter(router: HtmlPaletteDropRouter | null): void {
|
||||||
htmlDropRouter = router;
|
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 {
|
function hasCoarsePointer(): boolean {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
try {
|
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 {
|
export function needsPointerPaletteDrag(): boolean {
|
||||||
if (isMacDesktop()) return false;
|
|
||||||
return isDesktop() || hasCoarsePointer();
|
return isDesktop() || hasCoarsePointer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPaletteDragSessionActive(): boolean {
|
||||||
|
return activePayload != null || htmlPaletteDragActive;
|
||||||
|
}
|
||||||
|
|
||||||
export function subscribePaletteDrag(listener: PaletteDragListener): () => void {
|
export function subscribePaletteDrag(listener: PaletteDragListener): () => void {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
return () => listeners.delete(listener);
|
return () => listeners.delete(listener);
|
||||||
@@ -165,24 +128,8 @@ function emit(event: PaletteDragEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dispatchEnd(xy: { x: number; y: number }, payload: PaletteDragPayload) {
|
function dispatchEnd(xy: { x: number; y: number }, payload: PaletteDragPayload) {
|
||||||
const event: PaletteDragEvent = { phase: 'end', x: xy.x, y: xy.y, payload };
|
dropHandler?.({ phase: 'end', x: xy.x, y: xy.y, payload });
|
||||||
dropHandler?.(event);
|
emit({ phase: 'end', x: xy.x, y: xy.y, payload });
|
||||||
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 */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unbindGestureListeners() {
|
function unbindGestureListeners() {
|
||||||
@@ -190,10 +137,16 @@ function unbindGestureListeners() {
|
|||||||
unbindGesture = null;
|
unbindGesture = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function armPaletteDragScrollLock() {
|
||||||
|
document.body.classList.add('se-palette-drag-armed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function releasePaletteDragScrollLock() {
|
||||||
|
document.body.classList.remove('se-palette-drag-armed');
|
||||||
|
}
|
||||||
|
|
||||||
function cleanupDrag() {
|
function cleanupDrag() {
|
||||||
const pointerId = activePointerId;
|
|
||||||
unbindGestureListeners();
|
unbindGestureListeners();
|
||||||
releasePointerCapture(pointerId);
|
|
||||||
activePayload = null;
|
activePayload = null;
|
||||||
activePointerId = null;
|
activePointerId = null;
|
||||||
pendingMove = null;
|
pendingMove = null;
|
||||||
@@ -203,13 +156,17 @@ function cleanupDrag() {
|
|||||||
moveRafId = null;
|
moveRafId = null;
|
||||||
}
|
}
|
||||||
document.body.classList.remove('se-palette-drag-active');
|
document.body.classList.remove('se-palette-drag-active');
|
||||||
|
releasePaletteDragScrollLock();
|
||||||
overlayController?.unmount();
|
overlayController?.unmount();
|
||||||
overlayElement = null;
|
overlayElement = null;
|
||||||
document.querySelectorAll('.se-palette-drag-ghost').forEach(el => el.remove());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) {
|
function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) {
|
||||||
if (!activePayload) return;
|
if (!activePayload) {
|
||||||
|
unbindGestureListeners();
|
||||||
|
releasePaletteDragScrollLock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const payload = activePayload;
|
const payload = activePayload;
|
||||||
const dropXY = lastDragClientXY ?? xy;
|
const dropXY = lastDragClientXY ?? xy;
|
||||||
|
|
||||||
@@ -231,6 +188,7 @@ function mountDragOverlay(
|
|||||||
activePointerId = pointerId;
|
activePointerId = pointerId;
|
||||||
lastDragClientXY = { x: xy.x, y: xy.y };
|
lastDragClientXY = { x: xy.x, y: xy.y };
|
||||||
document.body.classList.add('se-palette-drag-active');
|
document.body.classList.add('se-palette-drag-active');
|
||||||
|
armPaletteDragScrollLock();
|
||||||
overlayController?.mount({ payload, pointerId, x: xy.x, y: xy.y });
|
overlayController?.mount({ payload, pointerId, x: xy.x, y: xy.y });
|
||||||
emit({ phase: 'start', x: xy.x, y: xy.y, payload });
|
emit({ phase: 'start', x: xy.x, y: xy.y, payload });
|
||||||
notifyPaletteDragMove(xy.x, xy.y);
|
notifyPaletteDragMove(xy.x, xy.y);
|
||||||
@@ -260,95 +218,40 @@ export function notifyPaletteDragCancel(clientX: number, clientY: number) {
|
|||||||
finishDrag('cancel', { x: clientX, y: clientY });
|
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) {
|
export function bindPaletteDragOverlayElement(el: HTMLElement | null) {
|
||||||
overlayElement = el;
|
overlayElement = el;
|
||||||
overlayController?.bindElement(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(
|
function startPaletteDragGesture(
|
||||||
startX: number,
|
startX: number,
|
||||||
startY: number,
|
startY: number,
|
||||||
pointerId: number,
|
pointerId: number,
|
||||||
payload: PaletteDragPayload,
|
payload: PaletteDragPayload,
|
||||||
): void {
|
): void {
|
||||||
if (activePayload) {
|
if (activePayload) cleanupDrag();
|
||||||
cleanupDrag();
|
|
||||||
}
|
|
||||||
unbindGestureListeners();
|
unbindGestureListeners();
|
||||||
|
|
||||||
let dragging = false;
|
const origin = { x: startX, y: startY };
|
||||||
|
mountDragOverlay(payload, origin, pointerId);
|
||||||
|
|
||||||
unbindGesture = bindWindowDrag(
|
unbindGesture = bindDocumentPointerDrag(
|
||||||
(xy) => {
|
pointerId,
|
||||||
if (!dragging) {
|
(xy) => notifyPaletteDragMove(xy.x, xy.y),
|
||||||
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);
|
|
||||||
},
|
|
||||||
(xy) => {
|
(xy) => {
|
||||||
unbindGestureListeners();
|
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);
|
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(
|
export function handlePalettePointerDown(
|
||||||
@@ -358,23 +261,16 @@ export function handlePalettePointerDown(
|
|||||||
): void {
|
): void {
|
||||||
if (!needsPointerPaletteDrag()) return;
|
if (!needsPointerPaletteDrag()) return;
|
||||||
if (!isPrimaryPointerButton(e)) 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.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);
|
startPaletteDragGesture(e.clientX, e.clientY, e.pointerId, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** macOS WebView 등 — pointer 대신 mouse 이벤트만 오는 경우 */
|
|
||||||
export function handlePaletteMouseDown(
|
export function handlePaletteMouseDown(
|
||||||
e: React.MouseEvent,
|
e: React.MouseEvent,
|
||||||
payload: PaletteDragPayload,
|
payload: PaletteDragPayload,
|
||||||
@@ -382,15 +278,16 @@ export function handlePaletteMouseDown(
|
|||||||
): void {
|
): void {
|
||||||
if (!needsPointerPaletteDrag()) return;
|
if (!needsPointerPaletteDrag()) return;
|
||||||
if (e.button !== 0) 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;
|
if (typeof window !== 'undefined' && 'PointerEvent' in window) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
|
||||||
startPaletteDragGesture(e.clientX, e.clientY, 1, payload);
|
startPaletteDragGesture(e.clientX, e.clientY, 1, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PointerEvent 미지원 — touchstart 전용 */
|
|
||||||
export function handlePaletteTouchStart(
|
export function handlePaletteTouchStart(
|
||||||
e: React.TouchEvent,
|
e: React.TouchEvent,
|
||||||
payload: PaletteDragPayload,
|
payload: PaletteDragPayload,
|
||||||
@@ -406,12 +303,77 @@ export function handlePaletteTouchStart(
|
|||||||
startPaletteDragGesture(touch.clientX, touch.clientY, touch.identifier, payload);
|
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 {
|
function stopHtmlDragMouseTracking(): void {
|
||||||
htmlDragMouseTrackUnbind?.();
|
htmlDragMouseTrackUnbind?.();
|
||||||
htmlDragMouseTrackUnbind = null;
|
htmlDragMouseTrackUnbind = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** HTML5 drag 중 mousemove 로 preview 좌표 추적 (macOS WKWebView — dragover 좌표/hit-test 불안정) */
|
|
||||||
export function startHtmlDragMouseTracking(
|
export function startHtmlDragMouseTracking(
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
@@ -427,9 +389,7 @@ export function startHtmlDragMouseTracking(
|
|||||||
lastHtmlDragClientXY = xy;
|
lastHtmlDragClientXY = xy;
|
||||||
emit({ phase: 'move', x: xy.x, y: xy.y, payload });
|
emit({ phase: 'move', x: xy.x, y: xy.y, payload });
|
||||||
},
|
},
|
||||||
() => {
|
() => { /* dragend 에서 정리 */ },
|
||||||
/* native HTML5 drag 중 mouseup 은 dragend 전에 올 수 있음 — dragend 에서 정리 */
|
|
||||||
},
|
|
||||||
{ preventTouchScroll: false },
|
{ preventTouchScroll: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -441,6 +401,7 @@ export function endHtmlDragMouseTracking(): void {
|
|||||||
|
|
||||||
export function markPaletteHtmlDragStart(): void {
|
export function markPaletteHtmlDragStart(): void {
|
||||||
htmlPaletteDragActive = true;
|
htmlPaletteDragActive = true;
|
||||||
|
htmlDropCommitted = false;
|
||||||
lastHtmlDragClientXY = null;
|
lastHtmlDragClientXY = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,39 @@ export interface BindWindowDragOptions {
|
|||||||
useCapture?: boolean;
|
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 리스너 등록.
|
* 드래그 시작 후 window 에 move/end 리스너 등록.
|
||||||
* pointer + mouse + touch 모두 수신 (구형 브라우저·iOS 대응).
|
* pointer + mouse + touch 모두 수신 (구형 브라우저·iOS 대응).
|
||||||
|
|||||||
Generated
+1
-1
@@ -45,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"desktop": {
|
"desktop": {
|
||||||
"name": "@goldenchart/desktop",
|
"name": "@goldenchart/desktop",
|
||||||
"version": "0.1.4",
|
"version": "0.1.43",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@goldenchart/shared": "*",
|
"@goldenchart/shared": "*",
|
||||||
"@stomp/stompjs": "^7.3.0",
|
"@stomp/stompjs": "^7.3.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user