@mhamz.01/easyflow-whiteboard 2.167.0 → 2.170.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/node/custom-node-overlay-layer.d.ts +2 -43
- package/dist/components/node/custom-node-overlay-layer.d.ts.map +1 -1
- package/dist/components/node/custom-node-overlay-layer.js +89 -568
- package/dist/components/node/custom-node.d.ts +2 -5
- package/dist/components/node/custom-node.d.ts.map +1 -1
- package/dist/components/node/custom-node.js +11 -22
- package/dist/components/node/document-node.d.ts +2 -5
- package/dist/components/node/document-node.d.ts.map +1 -1
- package/dist/components/node/document-node.js +25 -42
- package/dist/components/node/hooks/useFabricSync.d.ts +30 -0
- package/dist/components/node/hooks/useFabricSync.d.ts.map +1 -0
- package/dist/components/node/hooks/useFabricSync.js +89 -0
- package/dist/components/node/hooks/useKeyboardShortcuts.d.ts +22 -8
- package/dist/components/node/hooks/useKeyboardShortcuts.d.ts.map +1 -1
- package/dist/components/node/hooks/useKeyboardShortcuts.js +30 -21
- package/dist/components/node/hooks/useNodeDrag.d.ts +31 -18
- package/dist/components/node/hooks/useNodeDrag.d.ts.map +1 -1
- package/dist/components/node/hooks/useNodeDrag.js +128 -78
- package/dist/components/node/hooks/useNodeSelection.d.ts +28 -0
- package/dist/components/node/hooks/useNodeSelection.d.ts.map +1 -0
- package/dist/components/node/hooks/useNodeSelection.js +55 -0
- package/dist/components/node/hooks/useNodeState.d.ts +15 -0
- package/dist/components/node/hooks/useNodeState.d.ts.map +1 -0
- package/dist/components/node/hooks/useNodeState.js +24 -0
- package/dist/components/node/hooks/useSelectionBox.d.ts +14 -3
- package/dist/components/node/hooks/useSelectionBox.d.ts.map +1 -1
- package/dist/components/node/hooks/useSelectionBox.js +39 -18
- package/dist/components/node/hooks/useWheelZoom.d.ts +16 -6
- package/dist/components/node/hooks/useWheelZoom.d.ts.map +1 -1
- package/dist/components/node/hooks/useWheelZoom.js +41 -44
- package/dist/components/node/types/overlay-types.d.ts +11 -8
- package/dist/components/node/types/overlay-types.d.ts.map +1 -1
- package/dist/styles.css +0 -3
- package/package.json +1 -1
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
import { useRef, useCallback } from "react";
|
|
2
|
-
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
|
2
|
+
function readPointer(e) {
|
|
3
|
+
if ("touches" in e && e.touches.length > 0) {
|
|
4
|
+
return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY };
|
|
5
|
+
}
|
|
6
|
+
return {
|
|
7
|
+
clientX: e.clientX,
|
|
8
|
+
clientY: e.clientY,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* SRP: owns the full HTML-node drag lifecycle — start, move (rAF-throttled),
|
|
13
|
+
* and end — while keeping Fabric canvas objects in sync imperatively.
|
|
14
|
+
*
|
|
15
|
+
* Key design decisions:
|
|
16
|
+
* - All world-space math reads live viewport transform from Fabric so zoom/pan
|
|
17
|
+
* during a drag stays correct without extra state dependencies.
|
|
18
|
+
* - startPositions + anchor-offset approach prevents the "jump on drag start"
|
|
19
|
+
* bug that occurs when using raw mouse coords.
|
|
20
|
+
* - activeSelection group positions are resolved via calcTransformMatrix so
|
|
21
|
+
* multi-selected Fabric objects stay in sync when dragged from HTML nodes.
|
|
22
|
+
*/
|
|
23
|
+
export function useNodeDrag({ selectedIdsRef, dragSelectedIdsRef, localTasksRef, localDocumentsRef, fabricCanvas, setLocalTasks, setLocalDocuments, onTasksUpdate, onDocumentsUpdate, }) {
|
|
24
|
+
const [dragging, setDragging] = useState(null);
|
|
3
25
|
const dragStateRef = useRef({
|
|
4
26
|
isDragging: false,
|
|
5
27
|
itemIds: [],
|
|
@@ -9,111 +31,139 @@ export function useNodeDrag({ selectedIds, fabricCanvas, selectedCanvasObjects,
|
|
|
9
31
|
offsetY: 0,
|
|
10
32
|
});
|
|
11
33
|
const rafIdRef = useRef(null);
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
const getViewportTransform = (canvas) => {
|
|
25
|
-
const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
|
|
26
|
-
return {
|
|
27
|
-
zoom: vpt[0],
|
|
28
|
-
vpX: vpt[4],
|
|
29
|
-
vpY: vpt[5],
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
const handleDragStart = useCallback((itemId, getItemPosition, e) => {
|
|
34
|
+
const getItemPosition = useCallback((id) => {
|
|
35
|
+
const task = localTasksRef.current.find((t) => t.id === id);
|
|
36
|
+
if (task)
|
|
37
|
+
return { x: task.x, y: task.y };
|
|
38
|
+
const doc = localDocumentsRef.current.find((d) => d.id === id);
|
|
39
|
+
if (doc)
|
|
40
|
+
return { x: doc.x, y: doc.y };
|
|
41
|
+
return undefined;
|
|
42
|
+
}, [localTasksRef, localDocumentsRef]);
|
|
43
|
+
const handleDragStart = useCallback((itemId, e) => {
|
|
33
44
|
const canvas = fabricCanvas?.current;
|
|
34
45
|
if (!canvas)
|
|
35
46
|
return;
|
|
36
47
|
if (e.cancelable)
|
|
37
48
|
e.preventDefault();
|
|
38
|
-
const pointer =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const { zoom: liveZoom, vpX: liveVpX, vpY: liveVpY } = getViewportTransform(canvas);
|
|
49
|
+
const pointer = readPointer(e);
|
|
50
|
+
const itemsToDrag = selectedIdsRef.current.has(itemId)
|
|
51
|
+
? Array.from(selectedIdsRef.current)
|
|
52
|
+
: [itemId];
|
|
53
|
+
const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
|
|
54
|
+
const liveZoom = vpt[0];
|
|
55
|
+
const liveVpX = vpt[4];
|
|
56
|
+
const liveVpY = vpt[5];
|
|
47
57
|
const clickWorldX = (pointer.clientX - liveVpX) / liveZoom;
|
|
48
58
|
const clickWorldY = (pointer.clientY - liveVpY) / liveZoom;
|
|
49
59
|
const clickedPos = getItemPosition(itemId);
|
|
50
60
|
if (!clickedPos)
|
|
51
61
|
return;
|
|
62
|
+
// Offset in world units: distance from mouse to node's top-left.
|
|
63
|
+
// Stays constant even when zooming during a drag.
|
|
64
|
+
const worldOffsetX = clickWorldX - clickedPos.x;
|
|
65
|
+
const worldOffsetY = clickWorldY - clickedPos.y;
|
|
52
66
|
const startPositions = new Map();
|
|
53
67
|
itemsToDrag.forEach((id) => {
|
|
54
68
|
const pos = getItemPosition(id);
|
|
55
69
|
if (pos)
|
|
56
70
|
startPositions.set(id, pos);
|
|
57
71
|
});
|
|
72
|
+
// Resolve Fabric object start positions. For activeSelection groups,
|
|
73
|
+
// calcTransformMatrix gives the true world position regardless of the
|
|
74
|
+
// group's internal coordinate space.
|
|
75
|
+
const liveActiveObjects = canvas.getActiveObjects();
|
|
76
|
+
const activeSelection = canvas.getActiveObject();
|
|
58
77
|
const canvasObjectsStartPos = new Map();
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
liveActiveObjects.forEach((obj) => {
|
|
79
|
+
if (activeSelection?.type === "activeSelection") {
|
|
80
|
+
const matrix = obj.calcTransformMatrix();
|
|
81
|
+
canvasObjectsStartPos.set(obj, { left: matrix[4], top: matrix[5] });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
|
|
85
|
+
}
|
|
61
86
|
});
|
|
62
87
|
dragStateRef.current = {
|
|
63
88
|
isDragging: true,
|
|
64
89
|
itemIds: itemsToDrag,
|
|
65
90
|
startPositions,
|
|
66
91
|
canvasObjectsStartPos,
|
|
67
|
-
offsetX:
|
|
68
|
-
offsetY:
|
|
92
|
+
offsetX: worldOffsetX,
|
|
93
|
+
offsetY: worldOffsetY,
|
|
69
94
|
};
|
|
70
|
-
|
|
95
|
+
dragSelectedIdsRef.current = new Set(itemsToDrag);
|
|
96
|
+
setDragging({ itemIds: itemsToDrag });
|
|
71
97
|
document.body.style.cursor = "grabbing";
|
|
72
98
|
document.body.style.userSelect = "none";
|
|
73
99
|
document.body.style.touchAction = "none";
|
|
74
|
-
}, [
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
}, [fabricCanvas, selectedIdsRef, dragSelectedIdsRef, getItemPosition]);
|
|
101
|
+
// Attach move/end listeners only while a drag is active (SRP for side effects).
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!dragging)
|
|
77
104
|
return;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
rafIdRef.current = requestAnimationFrame(() => {
|
|
81
|
-
const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY } = dragStateRef.current;
|
|
82
|
-
const canvas = fabricCanvas?.current;
|
|
83
|
-
if (!canvas)
|
|
105
|
+
const handleMove = (e) => {
|
|
106
|
+
if (!dragStateRef.current.isDragging)
|
|
84
107
|
return;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
if (e.cancelable)
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
const pointer = readPointer(e);
|
|
111
|
+
if (rafIdRef.current !== null)
|
|
112
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
113
|
+
rafIdRef.current = requestAnimationFrame(() => {
|
|
114
|
+
const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY } = dragStateRef.current;
|
|
115
|
+
const canvas = fabricCanvas?.current;
|
|
116
|
+
if (!canvas)
|
|
117
|
+
return;
|
|
118
|
+
const vpt = canvas.viewportTransform;
|
|
119
|
+
const currentWorldX = (pointer.clientX - vpt[4]) / vpt[0];
|
|
120
|
+
const currentWorldY = (pointer.clientY - vpt[5]) / vpt[0];
|
|
121
|
+
const firstStart = startPositions.get(itemIds[0]);
|
|
122
|
+
if (!firstStart)
|
|
123
|
+
return;
|
|
124
|
+
const deltaX = currentWorldX - offsetX - firstStart.x;
|
|
125
|
+
const deltaY = currentWorldY - offsetY - firstStart.y;
|
|
126
|
+
setLocalTasks((prev) => prev.map((t) => itemIds.includes(t.id)
|
|
127
|
+
? { ...t, x: (startPositions.get(t.id)?.x ?? t.x) + deltaX, y: (startPositions.get(t.id)?.y ?? t.y) + deltaY }
|
|
128
|
+
: t));
|
|
129
|
+
setLocalDocuments((prev) => prev.map((d) => itemIds.includes(d.id)
|
|
130
|
+
? { ...d, x: (startPositions.get(d.id)?.x ?? d.x) + deltaX, y: (startPositions.get(d.id)?.y ?? d.y) + deltaY }
|
|
131
|
+
: d));
|
|
132
|
+
canvasObjectsStartPos.forEach((startPos, obj) => {
|
|
133
|
+
obj.set({ left: startPos.left + deltaX, top: startPos.top + deltaY });
|
|
134
|
+
obj.setCoords();
|
|
95
135
|
});
|
|
96
|
-
|
|
136
|
+
const activeObj = canvas.getActiveObject();
|
|
137
|
+
if (activeObj?.type === "activeSelection") {
|
|
138
|
+
activeObj.setCoords();
|
|
139
|
+
}
|
|
140
|
+
canvas.requestRenderAll();
|
|
97
141
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
};
|
|
143
|
+
const handleEnd = () => {
|
|
144
|
+
if (rafIdRef.current !== null)
|
|
145
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
146
|
+
dragStateRef.current.isDragging = false;
|
|
147
|
+
dragSelectedIdsRef.current = new Set();
|
|
148
|
+
setDragging(null);
|
|
149
|
+
document.body.style.cursor = "";
|
|
150
|
+
document.body.style.userSelect = "";
|
|
151
|
+
document.body.style.touchAction = "";
|
|
152
|
+
onTasksUpdate?.(localTasksRef.current);
|
|
153
|
+
onDocumentsUpdate?.(localDocumentsRef.current);
|
|
154
|
+
};
|
|
155
|
+
window.addEventListener("mousemove", handleMove, { passive: false });
|
|
156
|
+
window.addEventListener("mouseup", handleEnd);
|
|
157
|
+
window.addEventListener("touchmove", handleMove, { passive: false });
|
|
158
|
+
window.addEventListener("touchend", handleEnd);
|
|
159
|
+
window.addEventListener("touchcancel", handleEnd);
|
|
160
|
+
return () => {
|
|
161
|
+
window.removeEventListener("mousemove", handleMove);
|
|
162
|
+
window.removeEventListener("mouseup", handleEnd);
|
|
163
|
+
window.removeEventListener("touchmove", handleMove);
|
|
164
|
+
window.removeEventListener("touchend", handleEnd);
|
|
165
|
+
window.removeEventListener("touchcancel", handleEnd);
|
|
166
|
+
};
|
|
167
|
+
}, [dragging, fabricCanvas]);
|
|
168
|
+
return { dragging, getItemPosition, handleDragStart };
|
|
119
169
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RefObject, MutableRefObject, MouseEvent as ReactMouseEvent } from "react";
|
|
2
|
+
import type { Canvas } from "fabric";
|
|
3
|
+
interface UseNodeSelectionProps {
|
|
4
|
+
fabricCanvas?: RefObject<Canvas | null>;
|
|
5
|
+
clearHtmlSelectionRef?: MutableRefObject<() => void>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* SRP: owns the selectedIds set and all coordination guard refs that
|
|
9
|
+
* prevent feedback loops between HTML-node selection and Fabric selection.
|
|
10
|
+
*
|
|
11
|
+
* Guard refs:
|
|
12
|
+
* isHtmlSelectingRef — HTML node is the initiator; Fabric listeners should skip
|
|
13
|
+
* isSelectionBoxActiveRef — rubber-band box is live; Fabric mouse:down should skip
|
|
14
|
+
* htmlNodesSelectedByBoxRef — box just selected HTML nodes; Fabric selection:created should skip
|
|
15
|
+
* dragSelectedIdsRef — snapshot of selection at the moment a drag begins
|
|
16
|
+
*/
|
|
17
|
+
export declare function useNodeSelection({ fabricCanvas, clearHtmlSelectionRef, }: UseNodeSelectionProps): {
|
|
18
|
+
selectedIds: Set<string>;
|
|
19
|
+
setSelectedIds: import("react").Dispatch<import("react").SetStateAction<Set<string>>>;
|
|
20
|
+
selectedIdsRef: RefObject<Set<string>>;
|
|
21
|
+
isHtmlSelectingRef: RefObject<boolean>;
|
|
22
|
+
isSelectionBoxActiveRef: RefObject<boolean>;
|
|
23
|
+
htmlNodesSelectedByBoxRef: RefObject<boolean>;
|
|
24
|
+
dragSelectedIdsRef: RefObject<Set<string>>;
|
|
25
|
+
handleSelect: (id: string, e?: ReactMouseEvent) => void;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=useNodeSelection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNodeSelection.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useNodeSelection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAChB,UAAU,IAAI,eAAe,EAC9B,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,UAAU,qBAAqB;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,qBAAqB,CAAC,EAAE,gBAAgB,CAAC,MAAM,IAAI,CAAC,CAAC;CACtD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,YAAY,EACZ,qBAAqB,GACtB,EAAE,qBAAqB;;;;;;;;uBAiBf,MAAM,MAAM,eAAe;EAkCnC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* SRP: owns the selectedIds set and all coordination guard refs that
|
|
4
|
+
* prevent feedback loops between HTML-node selection and Fabric selection.
|
|
5
|
+
*
|
|
6
|
+
* Guard refs:
|
|
7
|
+
* isHtmlSelectingRef — HTML node is the initiator; Fabric listeners should skip
|
|
8
|
+
* isSelectionBoxActiveRef — rubber-band box is live; Fabric mouse:down should skip
|
|
9
|
+
* htmlNodesSelectedByBoxRef — box just selected HTML nodes; Fabric selection:created should skip
|
|
10
|
+
* dragSelectedIdsRef — snapshot of selection at the moment a drag begins
|
|
11
|
+
*/
|
|
12
|
+
export function useNodeSelection({ fabricCanvas, clearHtmlSelectionRef, }) {
|
|
13
|
+
const [selectedIds, setSelectedIds] = useState(new Set());
|
|
14
|
+
const selectedIdsRef = useRef(selectedIds);
|
|
15
|
+
const isHtmlSelectingRef = useRef(false);
|
|
16
|
+
const isSelectionBoxActiveRef = useRef(false);
|
|
17
|
+
const htmlNodesSelectedByBoxRef = useRef(false);
|
|
18
|
+
const dragSelectedIdsRef = useRef(new Set());
|
|
19
|
+
selectedIdsRef.current = selectedIds;
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!clearHtmlSelectionRef)
|
|
22
|
+
return;
|
|
23
|
+
clearHtmlSelectionRef.current = () => setSelectedIds(new Set());
|
|
24
|
+
}, [clearHtmlSelectionRef]);
|
|
25
|
+
const handleSelect = useCallback((id, e) => {
|
|
26
|
+
const canvas = fabricCanvas?.current;
|
|
27
|
+
if (canvas) {
|
|
28
|
+
isHtmlSelectingRef.current = true;
|
|
29
|
+
canvas.discardActiveObject();
|
|
30
|
+
canvas.requestRenderAll();
|
|
31
|
+
isHtmlSelectingRef.current = false;
|
|
32
|
+
}
|
|
33
|
+
htmlNodesSelectedByBoxRef.current = false;
|
|
34
|
+
if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
|
|
35
|
+
setSelectedIds((prev) => {
|
|
36
|
+
const next = new Set(prev);
|
|
37
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
38
|
+
return next;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
setSelectedIds(new Set([id]));
|
|
43
|
+
}
|
|
44
|
+
}, [fabricCanvas]);
|
|
45
|
+
return {
|
|
46
|
+
selectedIds,
|
|
47
|
+
setSelectedIds,
|
|
48
|
+
selectedIdsRef,
|
|
49
|
+
isHtmlSelectingRef,
|
|
50
|
+
isSelectionBoxActiveRef,
|
|
51
|
+
htmlNodesSelectedByBoxRef,
|
|
52
|
+
dragSelectedIdsRef,
|
|
53
|
+
handleSelect,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Task, Document } from "../types/overlay-types";
|
|
2
|
+
/**
|
|
3
|
+
* SRP: owns local task/document state and keeps it in sync with parent props.
|
|
4
|
+
* Exposes stable refs so other hooks can read the latest values without
|
|
5
|
+
* subscribing to re-renders.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useNodeState(tasks: Task[], documents: Document[]): {
|
|
8
|
+
localTasks: Task[];
|
|
9
|
+
setLocalTasks: import("react").Dispatch<import("react").SetStateAction<Task[]>>;
|
|
10
|
+
localDocuments: Document[];
|
|
11
|
+
setLocalDocuments: import("react").Dispatch<import("react").SetStateAction<Document[]>>;
|
|
12
|
+
localTasksRef: import("react").RefObject<Task[]>;
|
|
13
|
+
localDocumentsRef: import("react").RefObject<Document[]>;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=useNodeState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNodeState.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useNodeState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE;;;;;;;EAoBhE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* SRP: owns local task/document state and keeps it in sync with parent props.
|
|
4
|
+
* Exposes stable refs so other hooks can read the latest values without
|
|
5
|
+
* subscribing to re-renders.
|
|
6
|
+
*/
|
|
7
|
+
export function useNodeState(tasks, documents) {
|
|
8
|
+
const [localTasks, setLocalTasks] = useState(tasks);
|
|
9
|
+
const [localDocuments, setLocalDocuments] = useState(documents);
|
|
10
|
+
const localTasksRef = useRef(localTasks);
|
|
11
|
+
const localDocumentsRef = useRef(localDocuments);
|
|
12
|
+
localTasksRef.current = localTasks;
|
|
13
|
+
localDocumentsRef.current = localDocuments;
|
|
14
|
+
useEffect(() => { setLocalTasks(tasks); }, [tasks]);
|
|
15
|
+
useEffect(() => { setLocalDocuments(documents); }, [documents]);
|
|
16
|
+
return {
|
|
17
|
+
localTasks,
|
|
18
|
+
setLocalTasks,
|
|
19
|
+
localDocuments,
|
|
20
|
+
setLocalDocuments,
|
|
21
|
+
localTasksRef,
|
|
22
|
+
localDocumentsRef,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MutableRefObject, Dispatch, SetStateAction } from "react";
|
|
2
|
+
import type { Task, Document } from "../types/overlay-types";
|
|
2
3
|
interface UseSelectionBoxProps {
|
|
3
4
|
selectionBox: {
|
|
4
5
|
x1: number;
|
|
@@ -13,8 +14,18 @@ interface UseSelectionBoxProps {
|
|
|
13
14
|
x: number;
|
|
14
15
|
y: number;
|
|
15
16
|
};
|
|
16
|
-
|
|
17
|
+
isSelectionBoxActiveRef: MutableRefObject<boolean>;
|
|
18
|
+
htmlNodesSelectedByBoxRef: MutableRefObject<boolean>;
|
|
19
|
+
setSelectedIds: Dispatch<SetStateAction<Set<string>>>;
|
|
17
20
|
}
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* SRP: detects which HTML nodes fall inside the rubber-band selection box and
|
|
23
|
+
* updates selectedIds. Also maintains the guard refs so Fabric event handlers
|
|
24
|
+
* know not to clobber the box selection while it is active.
|
|
25
|
+
*
|
|
26
|
+
* O(n) single pass with an early-exit equality check avoids unnecessary
|
|
27
|
+
* re-renders when the selection set hasn't actually changed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useSelectionBox({ selectionBox, localTasks, localDocuments, canvasZoom, canvasViewport, isSelectionBoxActiveRef, htmlNodesSelectedByBoxRef, setSelectedIds, }: UseSelectionBoxProps): void;
|
|
19
30
|
export {};
|
|
20
31
|
//# sourceMappingURL=useSelectionBox.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSelectionBox.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useSelectionBox.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"useSelectionBox.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useSelectionBox.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AACxE,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAE7D,UAAU,oBAAoB;IAC5B,YAAY,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACxE,UAAU,EAAE,IAAI,EAAE,CAAC;IACnB,cAAc,EAAE,QAAQ,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,uBAAuB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACnD,yBAAyB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACrD,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;CACvD;AAwBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC9B,YAAY,EACZ,UAAU,EACV,cAAc,EACd,UAAU,EACV,cAAc,EACd,uBAAuB,EACvB,yBAAyB,EACzB,cAAc,GACf,EAAE,oBAAoB,QAiCtB"}
|
|
@@ -1,30 +1,51 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
function hitTest(x, y, width, height, box, zoom, viewport) {
|
|
3
|
+
const itemX1 = x * zoom + viewport.x;
|
|
4
|
+
const itemY1 = y * zoom + viewport.y;
|
|
5
|
+
const itemX2 = itemX1 + width * zoom;
|
|
6
|
+
const itemY2 = itemY1 + height * zoom;
|
|
7
|
+
const bx1 = Math.min(box.x1, box.x2);
|
|
8
|
+
const by1 = Math.min(box.y1, box.y2);
|
|
9
|
+
const bx2 = Math.max(box.x1, box.x2);
|
|
10
|
+
const by2 = Math.max(box.y1, box.y2);
|
|
11
|
+
return !(bx2 < itemX1 || bx1 > itemX2 || by2 < itemY1 || by1 > itemY2);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* SRP: detects which HTML nodes fall inside the rubber-band selection box and
|
|
15
|
+
* updates selectedIds. Also maintains the guard refs so Fabric event handlers
|
|
16
|
+
* know not to clobber the box selection while it is active.
|
|
17
|
+
*
|
|
18
|
+
* O(n) single pass with an early-exit equality check avoids unnecessary
|
|
19
|
+
* re-renders when the selection set hasn't actually changed.
|
|
20
|
+
*/
|
|
21
|
+
export function useSelectionBox({ selectionBox, localTasks, localDocuments, canvasZoom, canvasViewport, isSelectionBoxActiveRef, htmlNodesSelectedByBoxRef, setSelectedIds, }) {
|
|
14
22
|
useEffect(() => {
|
|
15
|
-
if (!selectionBox)
|
|
23
|
+
if (!selectionBox) {
|
|
24
|
+
isSelectionBoxActiveRef.current = false;
|
|
16
25
|
return;
|
|
26
|
+
}
|
|
27
|
+
isSelectionBoxActiveRef.current = true;
|
|
17
28
|
const newSelected = new Set();
|
|
18
29
|
for (const task of localTasks) {
|
|
19
|
-
if (
|
|
30
|
+
if (hitTest(task.x, task.y, 300, 140, selectionBox, canvasZoom, canvasViewport))
|
|
20
31
|
newSelected.add(task.id);
|
|
21
|
-
}
|
|
22
32
|
}
|
|
23
33
|
for (const doc of localDocuments) {
|
|
24
|
-
if (
|
|
34
|
+
if (hitTest(doc.x, doc.y, 320, 160, selectionBox, canvasZoom, canvasViewport))
|
|
25
35
|
newSelected.add(doc.id);
|
|
26
|
-
}
|
|
27
36
|
}
|
|
28
|
-
|
|
37
|
+
setSelectedIds((prev) => {
|
|
38
|
+
if (prev.size !== newSelected.size) {
|
|
39
|
+
htmlNodesSelectedByBoxRef.current = newSelected.size > 0;
|
|
40
|
+
return newSelected;
|
|
41
|
+
}
|
|
42
|
+
for (const id of newSelected) {
|
|
43
|
+
if (!prev.has(id)) {
|
|
44
|
+
htmlNodesSelectedByBoxRef.current = newSelected.size > 0;
|
|
45
|
+
return newSelected;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return prev;
|
|
49
|
+
});
|
|
29
50
|
}, [selectionBox, localTasks, localDocuments, canvasZoom, canvasViewport]);
|
|
30
51
|
}
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RefObject, WheelEvent as ReactWheelEvent } from "react";
|
|
2
|
+
import type { Canvas } from "fabric";
|
|
2
3
|
interface UseWheelZoomProps {
|
|
3
|
-
overlayRef:
|
|
4
|
-
fabricCanvas?:
|
|
5
|
-
canvasZoom?: number;
|
|
4
|
+
overlayRef: RefObject<HTMLDivElement | null>;
|
|
5
|
+
fabricCanvas?: RefObject<Canvas | null>;
|
|
6
6
|
canvasReady?: boolean;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* SRP: forwards wheel events that land on HTML nodes into Fabric's event bus
|
|
10
|
+
* so the canvas zoom/pan handlers fire regardless of which layer the user
|
|
11
|
+
* is hovering over.
|
|
12
|
+
*
|
|
13
|
+
* Two mechanisms:
|
|
14
|
+
* handleOverlayWheel — React synthetic handler attached to the overlay div
|
|
15
|
+
* native listener — passive:false DOM listener so e.preventDefault() works
|
|
16
|
+
* (React synthetic events cannot cancel passive listeners)
|
|
17
|
+
*/
|
|
18
|
+
export declare function useWheelZoom({ overlayRef, fabricCanvas, canvasReady, }: UseWheelZoomProps): {
|
|
19
|
+
handleOverlayWheel: (e: ReactWheelEvent<HTMLDivElement>) => void;
|
|
10
20
|
};
|
|
11
21
|
export {};
|
|
12
22
|
//# sourceMappingURL=useWheelZoom.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWheelZoom.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useWheelZoom.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"useWheelZoom.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useWheelZoom.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,IAAI,eAAe,EAAE,MAAM,OAAO,CAAC;AACtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,UAAU,iBAAiB;IACzB,UAAU,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC7C,YAAY,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,YAAY,EACZ,WAAmB,GACpB,EAAE,iBAAiB;4BACa,eAAe,CAAC,cAAc,CAAC;EA6C/D"}
|
|
@@ -1,56 +1,53 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* SRP: forwards wheel events that land on HTML nodes into Fabric's event bus
|
|
4
|
+
* so the canvas zoom/pan handlers fire regardless of which layer the user
|
|
5
|
+
* is hovering over.
|
|
6
|
+
*
|
|
7
|
+
* Two mechanisms:
|
|
8
|
+
* handleOverlayWheel — React synthetic handler attached to the overlay div
|
|
9
|
+
* native listener — passive:false DOM listener so e.preventDefault() works
|
|
10
|
+
* (React synthetic events cannot cancel passive listeners)
|
|
11
|
+
*/
|
|
12
|
+
export function useWheelZoom({ overlayRef, fabricCanvas, canvasReady = false, }) {
|
|
4
13
|
const handleOverlayWheel = (e) => {
|
|
5
|
-
if (e.ctrlKey || e.metaKey || e.shiftKey)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
e.preventDefault();
|
|
22
|
-
e.stopPropagation();
|
|
23
|
-
}
|
|
14
|
+
if (!(e.ctrlKey || e.metaKey || e.shiftKey))
|
|
15
|
+
return;
|
|
16
|
+
const canvas = fabricCanvas?.current;
|
|
17
|
+
if (!canvas)
|
|
18
|
+
return;
|
|
19
|
+
const nativeEvent = e.nativeEvent;
|
|
20
|
+
const scenePoint = canvas.getScenePoint(nativeEvent);
|
|
21
|
+
const rect = canvas.getElement().getBoundingClientRect();
|
|
22
|
+
const viewportPoint = {
|
|
23
|
+
x: nativeEvent.clientX - rect.left,
|
|
24
|
+
y: nativeEvent.clientY - rect.top,
|
|
25
|
+
};
|
|
26
|
+
canvas.fire("mouse:wheel", { e: nativeEvent, scenePoint, viewportPoint });
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
e.stopPropagation();
|
|
24
29
|
};
|
|
25
|
-
// Global wheel event for hovering over nodes
|
|
26
30
|
useEffect(() => {
|
|
27
31
|
const overlayEl = overlayRef.current;
|
|
28
32
|
const canvas = fabricCanvas?.current;
|
|
29
|
-
if (!overlayEl || !canvas
|
|
33
|
+
if (!overlayEl || !canvas)
|
|
30
34
|
return;
|
|
31
35
|
const handleGlobalWheel = (e) => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
e: e,
|
|
45
|
-
scenePoint,
|
|
46
|
-
viewportPoint,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
36
|
+
const isOverNode = e.target !== overlayEl;
|
|
37
|
+
if (!(e.ctrlKey || e.metaKey) || !isOverNode)
|
|
38
|
+
return;
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
e.stopPropagation();
|
|
41
|
+
const scenePoint = canvas.getScenePoint(e);
|
|
42
|
+
const rect = canvas.getElement().getBoundingClientRect();
|
|
43
|
+
const viewportPoint = {
|
|
44
|
+
x: e.clientX - rect.left,
|
|
45
|
+
y: e.clientY - rect.top,
|
|
46
|
+
};
|
|
47
|
+
canvas.fire("mouse:wheel", { e, scenePoint, viewportPoint });
|
|
49
48
|
};
|
|
50
49
|
overlayEl.addEventListener("wheel", handleGlobalWheel, { passive: false });
|
|
51
|
-
return () =>
|
|
52
|
-
|
|
53
|
-
};
|
|
54
|
-
}, [fabricCanvas, canvasZoom, canvasReady, overlayRef]);
|
|
50
|
+
return () => overlayEl.removeEventListener("wheel", handleGlobalWheel);
|
|
51
|
+
}, [fabricCanvas, canvasReady, overlayRef]);
|
|
55
52
|
return { handleOverlayWheel };
|
|
56
53
|
}
|