@mhamz.01/easyflow-whiteboard 2.120.0 → 2.121.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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-node-overlay-layer.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node-overlay-layer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"custom-node-overlay-layer.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node-overlay-layer.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAU9C,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,UAAU,uBAAuB;IAC/B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,YAAY,CAAC,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;IACzE,qBAAqB,CAAC,EAAE,YAAY,EAAE,CAAC;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAkBD,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,KAAK,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,UAAc,EACd,cAA+B,EAC/B,YAAmB,EACnB,qBAA0B,EAC1B,YAAY,GACb,EAAE,uBAAuB,2CAimBzB"}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect, useRef } from "react";
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
4
|
import TaskNode from "./custom-node";
|
|
5
5
|
import DocumentNode from "./document-node";
|
|
6
|
+
import { memo } from "react";
|
|
7
|
+
// Wrap TaskNode and DocumentNode
|
|
8
|
+
const MemoTaskNode = memo(TaskNode);
|
|
9
|
+
const MemoDocumentNode = memo(DocumentNode);
|
|
6
10
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
7
11
|
export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, onDocumentsUpdate, canvasZoom = 1, canvasViewport = { x: 0, y: 0 }, selectionBox = null, selectedCanvasObjects = [], fabricCanvas, }) {
|
|
8
12
|
const [localTasks, setLocalTasks] = useState(tasks);
|
|
@@ -14,6 +18,13 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
14
18
|
tasks: [],
|
|
15
19
|
documents: [],
|
|
16
20
|
});
|
|
21
|
+
// In render, replace TaskNode/DocumentNode with memoized versions
|
|
22
|
+
{
|
|
23
|
+
localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(MemoTaskNode, { ...task, isSelected: selectedIds.has(task.id), onSelect: handleSelect, onDragStart: handleDragStart, onStatusChange: handleStatusChange, zoom: 1 })));
|
|
24
|
+
}
|
|
25
|
+
{
|
|
26
|
+
localDocuments.map((doc) => renderItem(doc.id, doc.x, doc.y, _jsx(MemoDocumentNode, { ...doc, isSelected: selectedIds.has(doc.id), onSelect: handleSelect, onDragStart: handleDragStart })));
|
|
27
|
+
}
|
|
17
28
|
const dragStateRef = useRef({
|
|
18
29
|
isDragging: false,
|
|
19
30
|
itemIds: [],
|
|
@@ -25,7 +36,16 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
25
36
|
const rafIdRef = useRef(null);
|
|
26
37
|
const overlayRef = useRef(null);
|
|
27
38
|
const selectedIdsRef = useRef(selectedIds);
|
|
39
|
+
// Add these refs
|
|
40
|
+
const localTasksRef = useRef(localTasks);
|
|
41
|
+
const localDocsRef = useRef(localDocuments);
|
|
42
|
+
const onTasksUpdateRef = useRef(onTasksUpdate);
|
|
43
|
+
const onDocsUpdateRef = useRef(onDocumentsUpdate);
|
|
28
44
|
selectedIdsRef.current = selectedIds;
|
|
45
|
+
localTasksRef.current = localTasks;
|
|
46
|
+
localDocsRef.current = localDocuments;
|
|
47
|
+
onTasksUpdateRef.current = onTasksUpdate;
|
|
48
|
+
onDocsUpdateRef.current = onDocumentsUpdate;
|
|
29
49
|
// ── Sync props → local state ────────────────────────────────────────────────
|
|
30
50
|
useEffect(() => { setLocalTasks(tasks); }, [tasks]);
|
|
31
51
|
useEffect(() => { setLocalDocuments(documents); }, [documents]);
|
|
@@ -106,7 +126,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
106
126
|
return () => {
|
|
107
127
|
overlayEl.removeEventListener("wheel", handleGlobalWheel);
|
|
108
128
|
};
|
|
109
|
-
}, [fabricCanvas,
|
|
129
|
+
}, [fabricCanvas, canvasReady]); // Re-bind when zoom changes to keep closure fresh
|
|
110
130
|
// ── Fabric → Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
|
|
111
131
|
useEffect(() => {
|
|
112
132
|
const canvas = fabricCanvas?.current;
|
|
@@ -165,7 +185,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
165
185
|
// Having selectedIds here caused the effect to re-register on every selection
|
|
166
186
|
// change, creating a new closure each time. The second drag captured a stale
|
|
167
187
|
// or empty selectedIds from the closure at re-registration time.
|
|
168
|
-
}, [
|
|
188
|
+
}, [fabricCanvas, canvasReady]);
|
|
169
189
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
170
190
|
const getItemPosition = (id) => {
|
|
171
191
|
const task = localTasks.find((t) => t.id === id);
|
|
@@ -219,72 +239,61 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
219
239
|
return e.touches[0];
|
|
220
240
|
return e;
|
|
221
241
|
};
|
|
222
|
-
const handleDragStart = (itemId, e) => {
|
|
223
|
-
// 1. Safety check for the Fabric instance
|
|
242
|
+
const handleDragStart = useCallback((itemId, e) => {
|
|
224
243
|
const canvas = fabricCanvas?.current;
|
|
225
244
|
if (!canvas)
|
|
226
245
|
return;
|
|
227
|
-
// 2. Normalize the event (Touch vs Mouse)
|
|
228
246
|
if (e.cancelable)
|
|
229
247
|
e.preventDefault();
|
|
230
248
|
const pointer = getPointerEvent(e);
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
let itemsToDrag
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
itemsToDrag = [itemId];
|
|
239
|
-
}
|
|
240
|
-
// 4. Capture current World Transform (Zoom & Pan)
|
|
241
|
-
// We read directly from the canvas to ensure zero-frame lag
|
|
249
|
+
// read from ref — always fresh
|
|
250
|
+
const currentSelectedIds = selectedIdsRef.current;
|
|
251
|
+
let itemsToDrag = currentSelectedIds.has(itemId)
|
|
252
|
+
? Array.from(currentSelectedIds)
|
|
253
|
+
: [itemId];
|
|
242
254
|
const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
|
|
243
255
|
const liveZoom = vpt[0];
|
|
244
256
|
const liveVpX = vpt[4];
|
|
245
257
|
const liveVpY = vpt[5];
|
|
246
|
-
// 5. Convert the Click Position from Screen Pixels to World Units
|
|
247
258
|
const clickWorldX = (pointer.clientX - liveVpX) / liveZoom;
|
|
248
259
|
const clickWorldY = (pointer.clientY - liveVpY) / liveZoom;
|
|
249
|
-
//
|
|
250
|
-
const
|
|
260
|
+
// read positions from refs
|
|
261
|
+
const getPos = (id) => {
|
|
262
|
+
const t = localTasksRef.current.find((t) => t.id === id);
|
|
263
|
+
if (t)
|
|
264
|
+
return { x: t.x, y: t.y };
|
|
265
|
+
const d = localDocsRef.current.find((d) => d.id === id);
|
|
266
|
+
if (d)
|
|
267
|
+
return { x: d.x, y: d.y };
|
|
268
|
+
};
|
|
269
|
+
const clickedPos = getPos(itemId);
|
|
251
270
|
if (!clickedPos)
|
|
252
271
|
return;
|
|
253
|
-
// 7. Calculate the Offset in WORLD UNITS
|
|
254
|
-
// This is the distance from the mouse to the node's top-left in the infinite grid.
|
|
255
|
-
// This value remains constant even if you zoom during the drag.
|
|
256
|
-
const worldOffsetX = clickWorldX - clickedPos.x;
|
|
257
|
-
const worldOffsetY = clickWorldY - clickedPos.y;
|
|
258
|
-
// 8. Snapshot starting positions for all selected HTML nodes
|
|
259
272
|
const startPositions = new Map();
|
|
260
273
|
itemsToDrag.forEach((id) => {
|
|
261
|
-
const pos =
|
|
274
|
+
const pos = getPos(id);
|
|
262
275
|
if (pos)
|
|
263
276
|
startPositions.set(id, pos);
|
|
264
277
|
});
|
|
265
|
-
// 9. Snapshot starting positions for all selected Fabric objects
|
|
266
278
|
const canvasObjectsStartPos = new Map();
|
|
267
279
|
selectedCanvasObjects.forEach((obj) => {
|
|
268
280
|
canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
|
|
269
281
|
});
|
|
270
|
-
// 10. Commit to the ref for the requestAnimationFrame loop
|
|
271
282
|
dragStateRef.current = {
|
|
272
283
|
isDragging: true,
|
|
273
284
|
itemIds: itemsToDrag,
|
|
274
285
|
startPositions,
|
|
275
286
|
canvasObjectsStartPos,
|
|
276
|
-
offsetX: clickWorldX,
|
|
277
|
-
offsetY: clickWorldY,
|
|
287
|
+
offsetX: clickWorldX,
|
|
288
|
+
offsetY: clickWorldY,
|
|
278
289
|
};
|
|
279
|
-
if (!
|
|
290
|
+
if (!currentSelectedIds.has(itemId))
|
|
280
291
|
setSelectedIds(new Set([itemId]));
|
|
281
|
-
}
|
|
282
|
-
// 11. Trigger UI states
|
|
283
292
|
setDragging({ itemIds: itemsToDrag });
|
|
284
293
|
document.body.style.cursor = "grabbing";
|
|
285
294
|
document.body.style.userSelect = "none";
|
|
286
295
|
document.body.style.touchAction = "none";
|
|
287
|
-
};
|
|
296
|
+
}, []); // now truly stable
|
|
288
297
|
// ── Drag move (HTML Node side) ───────────────────────────────────────────────
|
|
289
298
|
useEffect(() => {
|
|
290
299
|
if (!dragging)
|
|
@@ -363,8 +372,8 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
363
372
|
document.body.style.cursor = "";
|
|
364
373
|
document.body.style.userSelect = "";
|
|
365
374
|
document.body.style.touchAction = "";
|
|
366
|
-
|
|
367
|
-
|
|
375
|
+
onTasksUpdateRef.current?.(localTasksRef.current); // ref instead of closure
|
|
376
|
+
onDocsUpdateRef.current?.(localDocsRef.current);
|
|
368
377
|
};
|
|
369
378
|
window.addEventListener("mousemove", handleMove, { passive: false });
|
|
370
379
|
window.addEventListener("mouseup", handleEnd);
|
|
@@ -378,9 +387,9 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
378
387
|
window.removeEventListener("touchend", handleEnd);
|
|
379
388
|
window.removeEventListener("touchcancel", handleEnd);
|
|
380
389
|
};
|
|
381
|
-
}, [dragging,
|
|
390
|
+
}, [dragging, fabricCanvas]);
|
|
382
391
|
// ── Selection, Status, Keyboard Logic ────────────────────────────────────────
|
|
383
|
-
const handleSelect = (id, e) => {
|
|
392
|
+
const handleSelect = useCallback((id, e) => {
|
|
384
393
|
if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
|
|
385
394
|
setSelectedIds((prev) => {
|
|
386
395
|
const next = new Set(prev);
|
|
@@ -391,41 +400,38 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
391
400
|
else {
|
|
392
401
|
setSelectedIds(new Set([id]));
|
|
393
402
|
}
|
|
394
|
-
};
|
|
395
|
-
const handleStatusChange = (taskId, newStatus) => {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
403
|
+
}, []);
|
|
404
|
+
const handleStatusChange = useCallback((taskId, newStatus) => {
|
|
405
|
+
setLocalTasks((prev) => {
|
|
406
|
+
const updated = prev.map((t) => t.id === taskId ? { ...t, status: newStatus } : t);
|
|
407
|
+
onTasksUpdateRef.current?.(updated);
|
|
408
|
+
return updated;
|
|
409
|
+
});
|
|
410
|
+
}, []);
|
|
400
411
|
useEffect(() => {
|
|
401
412
|
const handleKeyDown = (e) => {
|
|
402
|
-
// Don't trigger if typing in input
|
|
403
413
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
|
|
404
414
|
return;
|
|
405
|
-
// Select All
|
|
406
415
|
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
|
407
416
|
e.preventDefault();
|
|
408
|
-
setSelectedIds(new Set([...
|
|
417
|
+
setSelectedIds(new Set([...localTasksRef.current.map((t) => t.id), ...localDocsRef.current.map((d) => d.id)]));
|
|
409
418
|
}
|
|
410
|
-
|
|
411
|
-
if (e.key === "Escape") {
|
|
419
|
+
if (e.key === "Escape")
|
|
412
420
|
setSelectedIds(new Set());
|
|
413
|
-
|
|
414
|
-
// ← ADD THIS: Delete selected nodes
|
|
415
|
-
if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
|
|
421
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selectedIdsRef.current.size > 0) {
|
|
416
422
|
e.preventDefault();
|
|
417
|
-
const updatedTasks =
|
|
418
|
-
const updatedDocs =
|
|
423
|
+
const updatedTasks = localTasksRef.current.filter((t) => !selectedIdsRef.current.has(t.id));
|
|
424
|
+
const updatedDocs = localDocsRef.current.filter((d) => !selectedIdsRef.current.has(d.id));
|
|
419
425
|
setLocalTasks(updatedTasks);
|
|
420
426
|
setLocalDocuments(updatedDocs);
|
|
421
427
|
setSelectedIds(new Set());
|
|
422
|
-
|
|
423
|
-
|
|
428
|
+
onTasksUpdateRef.current?.(updatedTasks);
|
|
429
|
+
onDocsUpdateRef.current?.(updatedDocs);
|
|
424
430
|
}
|
|
425
431
|
};
|
|
426
432
|
window.addEventListener("keydown", handleKeyDown);
|
|
427
433
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
428
|
-
}, [
|
|
434
|
+
}, []); // stable — everything via refs
|
|
429
435
|
// ── Render helper ────────────────────────────────────────────────────────────
|
|
430
436
|
const renderItem = (id, x, y, children) => {
|
|
431
437
|
const screenX = x * canvasZoom;
|
|
@@ -454,5 +460,5 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
454
460
|
}, children: _jsxs("div", { className: "absolute top-0 left-0 pointer-events-none", style: {
|
|
455
461
|
transform: `translate(${canvasViewport.x}px, ${canvasViewport.y}px)`,
|
|
456
462
|
transformOrigin: "top left",
|
|
457
|
-
}, children: [localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(
|
|
463
|
+
}, children: [localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(MemoTaskNode, { ...task, isSelected: selectedIds.has(task.id), onSelect: handleSelect, onDragStart: handleDragStart, onStatusChange: handleStatusChange, zoom: 1 }))), localDocuments.map((doc) => renderItem(doc.id, doc.x, doc.y, _jsx(MemoDocumentNode, { ...doc, isSelected: selectedIds.has(doc.id), onSelect: handleSelect, onDragStart: handleDragStart })))] }) }));
|
|
458
464
|
}
|