@mhamz.01/easyflow-whiteboard 2.119.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.
- package/dist/components/node/custom-node-overlay-layer.d.ts.map +1 -1
- package/dist/components/node/custom-node-overlay-layer.js +65 -59
- package/dist/components/whiteboard/whiteboard-test.d.ts.map +1 -1
- package/dist/components/whiteboard/whiteboard-test.js +17 -22
- package/dist/hooks/useEraser.d.ts.map +1 -1
- package/dist/hooks/useEraser.js +36 -24
- package/dist/styles.css +0 -3
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whiteboard-test.d.ts","sourceRoot":"","sources":["../../../src/components/whiteboard/whiteboard-test.tsx"],"names":[],"mappings":"AAQC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAkB3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACvE,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAIxE,UAAU,IAAI;IACZ,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,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,UAAU,qBAAqB;IAC7B,WAAW,CAAC,EAAE;QACZ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;QACf,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;KACxB,CAAC;IACF,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAClC,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,GAAG,EAAE,CAAC;QACb,SAAS,EAAE,GAAG,EAAE,CAAC;KAClB,KAAK,IAAI,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,WAAW,EACX,MAAM,EACN,cAAc,EACd,cAAmB,EACnB,kBAAuB,EACvB,aAAqB,GACtB,EAAE,qBAAqB,
|
|
1
|
+
{"version":3,"file":"whiteboard-test.d.ts","sourceRoot":"","sources":["../../../src/components/whiteboard/whiteboard-test.tsx"],"names":[],"mappings":"AAQC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAkB3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACvE,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAIxE,UAAU,IAAI;IACZ,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,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAID,UAAU,qBAAqB;IAC7B,WAAW,CAAC,EAAE;QACZ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;QACf,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;KACxB,CAAC;IACF,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAClC,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,GAAG,EAAE,CAAC;QACb,SAAS,EAAE,GAAG,EAAE,CAAC;KAClB,KAAK,IAAI,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,WAAW,EACX,MAAM,EACN,cAAc,EACd,cAAmB,EACnB,kBAAuB,EACvB,aAAqB,GACtB,EAAE,qBAAqB,2CA4PvB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { classRegistry } from "fabric";
|
|
5
5
|
import { useWhiteboardStore } from "../../store/whiteboard-store";
|
|
6
6
|
import WhiteboardToolbar from "../toolbar/whiteboard-toolbar";
|
|
@@ -161,12 +161,8 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
|
|
|
161
161
|
drawingHandlers,
|
|
162
162
|
eraserHandlers,
|
|
163
163
|
});
|
|
164
|
-
//
|
|
165
|
-
const
|
|
166
|
-
const handleZoomOut = () => handleZoom(canvasZoom - ZOOM_STEP);
|
|
167
|
-
const handleResetZoom = () => handleZoom(1);
|
|
168
|
-
// Dropdown handlers
|
|
169
|
-
const handleAddTaskFromDropdown = (taskTemplate) => {
|
|
164
|
+
// 1. Memoize static handlers
|
|
165
|
+
const handleAddTaskFromDropdown = useCallback((taskTemplate) => {
|
|
170
166
|
const canvas = fabricCanvasRef.current;
|
|
171
167
|
if (!canvas)
|
|
172
168
|
return;
|
|
@@ -176,12 +172,9 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
|
|
|
176
172
|
const liveZoom = vpt[0];
|
|
177
173
|
const cx = (canvas.getWidth() / 2 - vpt[4]) / liveZoom;
|
|
178
174
|
const cy = (canvas.getHeight() / 2 - vpt[5]) / liveZoom;
|
|
179
|
-
setTasks((prev) => [
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
]);
|
|
183
|
-
};
|
|
184
|
-
const handleAddDocumentFromDropdown = (docTemplate) => {
|
|
175
|
+
setTasks((prev) => [...prev, { ...taskTemplate, id: `${taskTemplate.id}-${Date.now()}`, x: cx - 150, y: cy - 60 }]);
|
|
176
|
+
}, []);
|
|
177
|
+
const handleAddDocumentFromDropdown = useCallback((docTemplate) => {
|
|
185
178
|
const canvas = fabricCanvasRef.current;
|
|
186
179
|
if (!canvas)
|
|
187
180
|
return;
|
|
@@ -191,16 +184,18 @@ export default function FabricWhiteboard({ initialData, onSave, saveDebounceMs,
|
|
|
191
184
|
const liveZoom = vpt[0];
|
|
192
185
|
const cx = (canvas.getWidth() / 2 - vpt[4]) / liveZoom;
|
|
193
186
|
const cy = (canvas.getHeight() / 2 - vpt[5]) / liveZoom;
|
|
194
|
-
setDocuments((prev) => [
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
187
|
+
setDocuments((prev) => [...prev, { ...docTemplate, id: `${docTemplate.id}-${Date.now()}`, x: cx - 160, y: cy - 80 }]);
|
|
188
|
+
}, []);
|
|
189
|
+
const handleZoomIn = useCallback(() => handleZoom(canvasZoom + ZOOM_STEP), [canvasZoom, handleZoom]);
|
|
190
|
+
const handleZoomOut = useCallback(() => handleZoom(canvasZoom - ZOOM_STEP), [canvasZoom, handleZoom]);
|
|
191
|
+
const handleResetZoom = useCallback(() => handleZoom(1), [handleZoom]);
|
|
192
|
+
const MemoOverlay = useMemo(() => (_jsx(CanvasOverlayLayer, { tasks: tasks, documents: documents, onTasksUpdate: setTasks, onDocumentsUpdate: setDocuments, canvasZoom: canvasZoom, canvasViewport: canvasViewport, selectionBox: selectionBox, selectedCanvasObjects: selectedCanvasObjects, fabricCanvas: fabricCanvasRef, canvasReady: canvasReady })), [tasks, documents, canvasZoom, canvasViewport, selectionBox, selectedCanvasObjects, canvasReady]);
|
|
193
|
+
const MemoToolbar = useMemo(() => (canvasReady
|
|
194
|
+
? _jsx(WhiteboardToolbar, { fabricCanvas: fabricCanvasRef, isRestoringRef: isRestoringRef, onAddTask: handleAddTaskFromDropdown, onAddDocument: handleAddDocumentFromDropdown, availableDocuments: availableDocuments, availableTasks: availableTasks, isLoadingData: isLoadingData })
|
|
195
|
+
: _jsx(ToolbarSkeleton, {})), [canvasReady, availableDocuments, availableTasks, isLoadingData, handleAddTaskFromDropdown, handleAddDocumentFromDropdown]);
|
|
196
|
+
return (_jsx("div", { className: "easyflow-whiteboard w-full h-full", children: _jsxs("div", { ref: containerRef, className: "relative w-full h-full overflow-hidden bg-[#0b0b0b]", style: { touchAction: "none", overscrollBehavior: "none" }, children: [_jsx("div", { className: "absolute inset-0 pointer-events-none", style: {
|
|
200
197
|
backgroundImage: `radial-gradient(circle, rgba(255,255,255,0.2) 1.2px, transparent 1.2px)`,
|
|
201
198
|
backgroundSize: "40px 40px",
|
|
202
199
|
zIndex: 0,
|
|
203
|
-
} }), _jsx("canvas", { ref: canvasRef, className: "absolute inset-0", style: { zIndex: 1 } }),
|
|
204
|
-
_jsx(WhiteboardToolbar, { fabricCanvas: fabricCanvasRef, isRestoringRef: isRestoringRef, onAddTask: handleAddTaskFromDropdown, onAddDocument: handleAddDocumentFromDropdown, availableDocuments: availableDocuments, availableTasks: availableTasks, isLoadingData: isLoadingData })
|
|
205
|
-
: _jsx(ToolbarSkeleton, {}) }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ToolOptionsPanel, { fabricCanvas: fabricCanvasRef }) }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ZoomControls, { zoom: canvasZoom, onZoomIn: handleZoomIn, onZoomOut: handleZoomOut, onResetZoom: handleResetZoom }) })] })] }) }));
|
|
200
|
+
} }), _jsx("canvas", { ref: canvasRef, className: "absolute inset-0", style: { zIndex: 1 } }), MemoOverlay, _jsxs("div", { className: "absolute inset-0 pointer-events-none", style: { zIndex: 100 }, children: [_jsx("div", { className: "pointer-events-auto", children: MemoToolbar }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ToolOptionsPanel, { fabricCanvas: fabricCanvasRef }) }), _jsx("div", { className: "pointer-events-auto", children: _jsx(ZoomControls, { zoom: canvasZoom, onZoomIn: handleZoomIn, onZoomOut: handleZoomOut, onResetZoom: handleResetZoom }) })] })] }) }));
|
|
206
201
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEraser.d.ts","sourceRoot":"","sources":["../../src/hooks/useEraser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,UAAU,cAAc;IACtB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC;IACjB,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACpC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,eAAO,MAAM,SAAS,GAAI,oKAWvB,cAAc;;;;gCAsBoB;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;;
|
|
1
|
+
{"version":3,"file":"useEraser.d.ts","sourceRoot":"","sources":["../../src/hooks/useEraser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtD,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,UAAU,cAAc;IACtB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC;IACjB,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/C,eAAe,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACpC,aAAa,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,eAAO,MAAM,SAAS,GAAI,oKAWvB,cAAc;;;;gCAsBoB;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;;CA0I5D,CAAC"}
|
package/dist/hooks/useEraser.js
CHANGED
|
@@ -24,7 +24,7 @@ export const useEraser = ({ fabricCanvas, activeTool, toolOptions, eraserTraceRe
|
|
|
24
24
|
const canvas = fabricCanvas.current;
|
|
25
25
|
if (!canvas || activeTool !== "eraser")
|
|
26
26
|
return;
|
|
27
|
-
//
|
|
27
|
+
// Create or update trace circle
|
|
28
28
|
if (!eraserTraceRef.current) {
|
|
29
29
|
eraserTraceRef.current = new Circle({
|
|
30
30
|
radius: toolOptions.eraser.size / 1.5,
|
|
@@ -45,54 +45,66 @@ export const useEraser = ({ fabricCanvas, activeTool, toolOptions, eraserTraceRe
|
|
|
45
45
|
eraserTraceRef.current.set({ left: pointer.x, top: pointer.y });
|
|
46
46
|
eraserTraceRef.current.setCoords();
|
|
47
47
|
canvas.bringObjectToFront(eraserTraceRef.current);
|
|
48
|
+
// Smooth continuous trail
|
|
48
49
|
if (eraserActiveRef.current) {
|
|
49
50
|
suppressHistoryRef.current = true;
|
|
50
51
|
if (eraserPathPointsRef.current.length === 0) {
|
|
51
52
|
eraserPathPointsRef.current = [`M ${pointer.x} ${pointer.y}`];
|
|
52
53
|
const path = new fabric.Path(eraserPathPointsRef.current.join(" "), {
|
|
53
|
-
stroke: "rgba(
|
|
54
|
+
stroke: "rgba(220, 220, 220, 0.7)",
|
|
54
55
|
strokeWidth: toolOptions.eraser.size,
|
|
55
56
|
fill: null,
|
|
56
57
|
selectable: false,
|
|
57
58
|
evented: false,
|
|
58
59
|
strokeLineCap: "round",
|
|
59
60
|
strokeLineJoin: "round",
|
|
61
|
+
opacity: 1,
|
|
60
62
|
excludeFromExport: true,
|
|
61
63
|
});
|
|
62
64
|
canvas.add(path);
|
|
63
65
|
eraserPathRef.current = path;
|
|
64
66
|
}
|
|
65
67
|
else {
|
|
66
|
-
// ── FIX 1: Mutate path in place — no remove/add, no events fired ──
|
|
67
68
|
eraserPathPointsRef.current.push(`L ${pointer.x} ${pointer.y}`);
|
|
68
69
|
const pathString = eraserPathPointsRef.current.join(" ");
|
|
69
70
|
if (eraserPathRef.current) {
|
|
70
|
-
|
|
71
|
-
eraserPathRef.current.set({ path: parsed.path });
|
|
72
|
-
eraserPathRef.current.setCoords();
|
|
71
|
+
canvas.remove(eraserPathRef.current);
|
|
73
72
|
}
|
|
73
|
+
const path = new fabric.Path(pathString, {
|
|
74
|
+
stroke: "rgba(53, 53, 53, 0.7)",
|
|
75
|
+
strokeWidth: toolOptions.eraser.size,
|
|
76
|
+
fill: null,
|
|
77
|
+
selectable: false,
|
|
78
|
+
evented: false,
|
|
79
|
+
strokeLineCap: "round",
|
|
80
|
+
strokeLineJoin: "round",
|
|
81
|
+
opacity: 1,
|
|
82
|
+
excludeFromExport: true,
|
|
83
|
+
});
|
|
84
|
+
canvas.add(path);
|
|
85
|
+
eraserPathRef.current = path;
|
|
74
86
|
}
|
|
75
87
|
canvas.bringObjectToFront(eraserTraceRef.current);
|
|
76
88
|
}
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
else if (previousTargets.has(obj)) {
|
|
90
|
-
obj.set({ opacity: 1 });
|
|
89
|
+
// Find objects under eraser
|
|
90
|
+
const previousTargets = new Set(eraserTargetsRef.current);
|
|
91
|
+
eraserTargetsRef.current.clear();
|
|
92
|
+
canvas.forEachObject((obj) => {
|
|
93
|
+
if (obj === eraserTraceRef.current)
|
|
94
|
+
return;
|
|
95
|
+
if (obj === eraserPathRef.current)
|
|
96
|
+
return;
|
|
97
|
+
if (obj.intersectsWithObject(eraserTraceRef.current)) {
|
|
98
|
+
eraserTargetsRef.current.add(obj);
|
|
99
|
+
if (eraserActiveRef.current && obj.opacity !== 0.3) {
|
|
100
|
+
obj.set({ opacity: 0.3 });
|
|
91
101
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
}
|
|
103
|
+
else if (previousTargets.has(obj)) {
|
|
104
|
+
obj.set({ opacity: 1 });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
canvas.renderAll();
|
|
96
108
|
};
|
|
97
109
|
const handleEraserUp = () => {
|
|
98
110
|
const canvas = fabricCanvas.current;
|
package/dist/styles.css
CHANGED