@mhamz.01/easyflow-whiteboard 2.0.1 → 2.2.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;AAM9C,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;CAC/C;AAaD,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,2CAudzB"}
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;AAM9C,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;CAC/C;AAaD,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,2CA2dzB"}
@@ -142,21 +142,26 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
142
142
  // ── Selection box detection ──────────────────────────────────────────────────
143
143
  useEffect(() => {
144
144
  if (!selectionBox)
145
- return; // Don't clear on null — let keyboard/click handlers do that
145
+ return;
146
+ // ── O(n) single pass — no sort, no join, no extra allocations ──
146
147
  const newSelected = new Set();
147
- localTasks.forEach((task) => {
148
+ for (const task of localTasks) {
148
149
  if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
149
150
  newSelected.add(task.id);
150
- });
151
- localDocuments.forEach((doc) => {
152
- if (isItemInSelectionBox(doc.x, doc.y, 300, 160, selectionBox))
151
+ }
152
+ for (const doc of localDocuments) {
153
+ if (isItemInSelectionBox(doc.x, doc.y, 320, 160, selectionBox))
153
154
  newSelected.add(doc.id);
154
- });
155
- // Only update if something actually changed (avoids re-render churn)
155
+ }
156
+ // ── O(n) equality check: size first (fast path), then membership ──
156
157
  setSelectedIds((prev) => {
157
- const prevArr = Array.from(prev).sort().join(",");
158
- const nextArr = Array.from(newSelected).sort().join(",");
159
- return prevArr === nextArr ? prev : newSelected;
158
+ if (prev.size !== newSelected.size)
159
+ return newSelected;
160
+ for (const id of newSelected) {
161
+ if (!prev.has(id))
162
+ return newSelected; // found a difference, swap
163
+ }
164
+ return prev; // identical — return same reference, no re-render
160
165
  });
161
166
  }, [selectionBox, localTasks, localDocuments]);
162
167
  // ── Drag start (HTML Node side) ──────────────────────────────────────────────
@@ -6,7 +6,7 @@ import { useWhiteboardStore } from "../../store/whiteboard-store";
6
6
  const AVAILABLE_DOCUMENTS = [
7
7
  {
8
8
  id: "doc-1",
9
- title: "Product Requirements Document",
9
+ title: "Product Not Requirements Document",
10
10
  project: "Website Redesign",
11
11
  breadcrumb: ["Design", "Specs"],
12
12
  preview: "This document outlines the core requirements for the new landing page including user flows, component specs, and acceptance criteria.",
@@ -16,6 +16,6 @@ interface UseSelectionProps {
16
16
  setSelectedCanvasObjects: (objects: FabricObject[]) => void;
17
17
  isDrawingRef: React.MutableRefObject<boolean>;
18
18
  }
19
- export declare const useSelection: ({ fabricCanvas, activeTool, canvasZoom, canvasViewport, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }: UseSelectionProps) => void;
19
+ export declare const useSelection: ({ fabricCanvas, activeTool, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }: UseSelectionProps) => void;
20
20
  export {};
21
21
  //# sourceMappingURL=useSelection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useSelection.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAQ,YAAY,EAAe,MAAM,QAAQ,CAAC;AAMjE,UAAU,iBAAiB;IACzB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,eAAe,EAAE,CAAC,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1F,wBAAwB,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IAC5D,YAAY,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,eAAO,MAAM,YAAY,GAAI,oHAQ1B,iBAAiB,SAgLnB,CAAC"}
1
+ {"version":3,"file":"useSelection.d.ts","sourceRoot":"","sources":["../../src/hooks/useSelection.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAQ,YAAY,EAAe,MAAM,QAAQ,CAAC;AAMjE,UAAU,iBAAiB;IACzB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,eAAe,EAAE,CAAC,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1F,wBAAwB,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IAC5D,YAAY,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,eAAO,MAAM,YAAY,GAAI,wFAM1B,iBAAiB,SAwJnB,CAAC"}
@@ -5,17 +5,11 @@ import { Frame } from "../lib/fabric-frame";
5
5
  import { Arrow } from "../lib/fabric-arrow";
6
6
  import { BidirectionalArrow } from "../lib/fabric-bidirectional-arrow";
7
7
  import { useWhiteboardStore } from "../store/whiteboard-store";
8
- export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewport, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }) => {
9
- const setSelectedObjectType = useWhiteboardStore((state) => state.setSelectedObjectType);
10
- const setActiveTool = useWhiteboardStore((state) => state.setActiveTool);
11
- // ── KEY FIX 1: Store zoom/viewport in refs so the effect never re-registers ──
12
- // This prevents listener teardown/re-attach during pan/zoom gestures
13
- const zoomRef = useRef(canvasZoom);
14
- const viewportRef = useRef(canvasViewport);
8
+ export const useSelection = ({ fabricCanvas, activeTool, setSelectionBox, setSelectedCanvasObjects, isDrawingRef, }) => {
9
+ const setSelectedObjectType = useWhiteboardStore((s) => s.setSelectedObjectType);
10
+ const setActiveTool = useWhiteboardStore((s) => s.setActiveTool);
11
+ // activeTool ref so effect never re-registers on tool change
15
12
  const activeToolRef = useRef(activeTool);
16
- // Keep refs in sync with latest props on every render
17
- useEffect(() => { zoomRef.current = canvasZoom; }, [canvasZoom]);
18
- useEffect(() => { viewportRef.current = canvasViewport; }, [canvasViewport]);
19
13
  useEffect(() => { activeToolRef.current = activeTool; }, [activeTool]);
20
14
  useEffect(() => {
21
15
  const canvas = fabricCanvas.current;
@@ -26,11 +20,11 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
26
20
  let selRect = null;
27
21
  let rafId = null;
28
22
  const onDown = (e) => {
29
- // ── KEY FIX 2: Use ref instead of closure-captured activeTool ──
30
23
  if (activeToolRef.current !== "select" || e.target)
31
24
  return;
32
25
  isSelecting = true;
33
- // getScenePoint returns world coordinates (zoom + pan already factored in by Fabric)
26
+ // Hide Fabric's native rubber-band rect we draw our own
27
+ canvas.selection = false;
34
28
  const p = canvas.getScenePoint(e.e);
35
29
  selStart = { x: p.x, y: p.y };
36
30
  selRect = new Rect({
@@ -40,11 +34,9 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
40
34
  height: 0,
41
35
  fill: "rgba(2, 154, 255, 0.08)",
42
36
  stroke: "#029AFF",
43
- strokeWidth: 1 / zoomRef.current, // stays 1px visually at any zoom
37
+ strokeWidth: 1 / canvas.getZoom(),
44
38
  selectable: false,
45
39
  evented: false,
46
- // ── KEY FIX 3: Make it visible so user sees the selection rect ──
47
- // was `visible: false` before — invisible but still blocking
48
40
  excludeFromExport: true,
49
41
  });
50
42
  canvas.add(selRect);
@@ -64,20 +56,20 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
64
56
  top: h < 0 ? p.y : selStart.y,
65
57
  width: Math.abs(w),
66
58
  height: Math.abs(h),
67
- strokeWidth: 1 / zoomRef.current, // keep stroke sharp at any zoom
59
+ strokeWidth: 1 / canvas.getZoom(),
68
60
  });
69
61
  selRect.setCoords();
70
62
  canvas.renderAll();
71
- // ── KEY FIX 4: Read from refs — always fresh, no stale closure ──
72
- const zoom = zoomRef.current;
73
- const vp = viewportRef.current;
74
- // World Screen conversion:
75
- // screenX = worldX * zoom + panX (matches exactly how nodes are rendered)
63
+ // ── Read directly from Fabric VPT — always frame-perfect, zero React lag ──
64
+ const vpt = canvas.viewportTransform;
65
+ const zoom = vpt[0]; // scaleX
66
+ const vpX = vpt[4]; // panX
67
+ const vpY = vpt[5]; // panY
76
68
  setSelectionBox({
77
- x1: Math.min(selStart.x, p.x) * zoom + vp.x,
78
- y1: Math.min(selStart.y, p.y) * zoom + vp.y,
79
- x2: Math.max(selStart.x, p.x) * zoom + vp.x,
80
- y2: Math.max(selStart.y, p.y) * zoom + vp.y,
69
+ x1: Math.min(selStart.x, p.x) * zoom + vpX,
70
+ y1: Math.min(selStart.y, p.y) * zoom + vpY,
71
+ x2: Math.max(selStart.x, p.x) * zoom + vpX,
72
+ y2: Math.max(selStart.y, p.y) * zoom + vpY,
81
73
  });
82
74
  });
83
75
  };
@@ -93,11 +85,9 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
93
85
  canvas.renderAll();
94
86
  selRect = null;
95
87
  }
88
+ // Restore Fabric's native selection for object clicking
89
+ canvas.selection = true;
96
90
  isSelecting = false;
97
- // ── KEY FIX 5: Delay clear long enough for overlay useEffect to fire ──
98
- // onDeselected (selection:cleared) was calling setSelectionBox(null) immediately,
99
- // racing with the overlay's useEffect. 150ms ensures the React render cycle
100
- // processes the final box position before it's cleared.
101
91
  setTimeout(() => setSelectionBox(null), 150);
102
92
  };
103
93
  const onSelected = () => {
@@ -106,34 +96,24 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
106
96
  return;
107
97
  setSelectedCanvasObjects(sel.type === "activeSelection" ? sel.getObjects() : [sel]);
108
98
  const typeMap = {
109
- rect: "rectangle",
110
- circle: "circle",
111
- line: "line",
112
- arrow: "arrow",
113
- "bidirectional-arrow": "arrow",
114
- "i-text": "text",
115
- text: "text",
116
- path: "pen",
117
- image: "image",
99
+ rect: "rectangle", circle: "circle", line: "line",
100
+ arrow: "arrow", "bidirectional-arrow": "arrow",
101
+ "i-text": "text", text: "text", path: "pen", image: "image",
118
102
  };
119
- const t = sel instanceof Frame
120
- ? "frame"
121
- : sel instanceof FabricImage
122
- ? "image"
123
- : sel instanceof Arrow
124
- ? "arrow"
125
- : sel instanceof BidirectionalArrow
126
- ? "arrow"
103
+ const t = sel instanceof Frame ? "frame"
104
+ : sel instanceof FabricImage ? "image"
105
+ : sel instanceof Arrow ? "arrow"
106
+ : sel instanceof BidirectionalArrow ? "arrow"
127
107
  : typeMap[sel.type] ?? null;
128
108
  setSelectedObjectType(t);
129
109
  };
130
110
  const onDeselected = () => {
131
111
  setSelectedObjectType(null);
132
- // ── KEY FIX 6: Do NOT clear selectionBox here ──
133
- // This was racing with onUp's setTimeout and clearing before overlay processed it.
134
- // onUp owns the selectionBox lifecycle. Only clear canvas objects here.
112
+ // ── Do NOT touch selectionBox here — onUp owns its lifecycle ──
135
113
  setSelectedCanvasObjects([]);
136
- if (!isDrawingRef.current && activeToolRef.current !== "select" && activeToolRef.current !== "pan") {
114
+ if (!isDrawingRef.current &&
115
+ activeToolRef.current !== "select" &&
116
+ activeToolRef.current !== "pan") {
137
117
  setActiveTool("select");
138
118
  }
139
119
  };
@@ -152,10 +132,11 @@ export const useSelection = ({ fabricCanvas, activeTool, canvasZoom, canvasViewp
152
132
  canvas.off("mouse:up", onUp);
153
133
  if (rafId !== null)
154
134
  cancelAnimationFrame(rafId);
155
- if (selRect)
135
+ if (selRect) {
156
136
  canvas.remove(selRect);
137
+ }
138
+ canvas.selection = true; // always restore on cleanup
157
139
  };
158
- // ── KEY FIX 7: Remove canvasZoom/canvasViewport/activeTool from deps ──
159
- // They are now read via refs — effect only registers once per canvas mount.
140
+ // ── Stable deps only effect registers once per canvas mount ──
160
141
  }, [fabricCanvas, setSelectionBox, setSelectedCanvasObjects, setSelectedObjectType, setActiveTool, isDrawingRef]);
161
142
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "A feature-rich whiteboard component built with Fabric.js and React",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",