@mhamz.01/easyflow-whiteboard 2.47.0 → 2.50.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;AAS9C,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,2CA6hBzB"}
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;AAS9C,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,2CAuiBzB"}
@@ -115,20 +115,29 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
115
115
  setSelectedIds(new Set());
116
116
  return;
117
117
  }
118
- // ── Read from ref not stale closure ──
119
- const activeObject = canvas.getActiveObject(); // the group/selection box
120
- const activeObjects = canvas.getActiveObjects(); // individual objects inside it
121
- const isTargetAlreadySelected = activeObjects.includes(target) || // clicked an individual selected object
122
- activeObject === target; // clicked the selection box itself
123
- if (!isTargetAlreadySelected) {
118
+ // At zoom=1 with identity VPT, getActiveObject() can return null before
119
+ // Fabric updates _activeObject. Use e.transform as the primary check —
120
+ // it is populated by Fabric's hit-test regardless of zoom level.
121
+ const transformTarget = e.transform?.target;
122
+ const activeObject = canvas.getActiveObject();
123
+ const activeObjects = canvas.getActiveObjects();
124
+ const isPartOfActiveSelection = transformTarget === target || // most reliable — direct from event
125
+ activeObject === target || // selection box group
126
+ activeObjects.includes(target); // individual object in multi-select
127
+ if (!isPartOfActiveSelection) {
124
128
  setSelectedIds(new Set());
125
129
  }
126
130
  };
131
+ const handleSelectionCleared = () => {
132
+ setSelectedIds(new Set());
133
+ };
127
134
  canvas.on("object:moving", handleObjectMoving);
128
135
  canvas.on("mouse:down", handleMouseDown);
136
+ canvas.on("selection:cleared", handleSelectionCleared);
129
137
  return () => {
130
138
  canvas.off("object:moving", handleObjectMoving);
131
139
  canvas.off("mouse:down", handleMouseDown);
140
+ canvas.off("selection:cleared", handleSelectionCleared);
132
141
  };
133
142
  // ── selectedIds REMOVED from deps — read via selectedIdsRef instead ──────
134
143
  // Having selectedIds here caused the effect to re-register on every selection
@@ -1 +1 @@
1
- {"version":3,"file":"useCanvasInit.d.ts","sourceRoot":"","sources":["../../src/hooks/useCanvasInit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKhC,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAK,MAAM,CAAC;IACnB,KAAK,CAAC,EAAM,GAAG,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC;CACnB;AAED,UAAU,kBAAkB;IAC1B,SAAS,EAAW,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACxD,eAAe,EAAK,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAU,MAAM,CAAC;IAC3B,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,cAAc,EAAM,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,WAAW,EAAS,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,QAAQ,EAAY,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAC3C,YAAY,EAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAG1C,WAAW,CAAC,EAAQ,qBAAqB,CAAC;CAC3C;AAID,eAAO,MAAM,aAAa,GAAI,mIAU3B,kBAAkB,SA+FpB,CAAC"}
1
+ {"version":3,"file":"useCanvasInit.d.ts","sourceRoot":"","sources":["../../src/hooks/useCanvasInit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKhC,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAK,MAAM,CAAC;IACnB,KAAK,CAAC,EAAM,GAAG,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC;CACnB;AAED,UAAU,kBAAkB;IAC1B,SAAS,EAAW,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACxD,eAAe,EAAK,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,UAAU,EAAU,MAAM,CAAC;IAC3B,kBAAkB,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,cAAc,EAAM,SAAS,CAAC,OAAO,CAAC,CAAC;IACvC,WAAW,EAAS,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,QAAQ,EAAY,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAC3C,YAAY,EAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAC1C,WAAW,CAAC,EAAQ,qBAAqB,CAAC;CAC3C;AAID,eAAO,MAAM,aAAa,GAAI,mIAU3B,kBAAkB,SAuHpB,CAAC"}
@@ -4,7 +4,6 @@ import { initializeFabricCanvas, addWelcomeContent } from "../lib/fabric-utils";
4
4
  // ─── Hook ─────────────────────────────────────────────────────────────────────
5
5
  export const useCanvasInit = ({ canvasRef, fabricCanvasRef, activeTool, suppressHistoryRef, isRestoringRef, pushHistory, setTasks, setDocuments, initialData, }) => {
6
6
  useEffect(() => {
7
- // Guard: prevent re-initialization if canvas already exists
8
7
  if (!canvasRef.current || fabricCanvasRef.current)
9
8
  return;
10
9
  const canvas = new Canvas(canvasRef.current, {
@@ -20,31 +19,44 @@ export const useCanvasInit = ({ canvasRef, fabricCanvasRef, activeTool, suppress
20
19
  initializeFabricCanvas(canvas);
21
20
  const canvasElement = canvas.getElement();
22
21
  canvasElement.style.touchAction = "none";
23
- // ── Hydrate from initialData (replaces localStorage.getItem) ─────────────
24
- // Shape is identical to what usePersistence sends via onSave —
25
- // consumer reads from DB and passes straight back in as initialData.
22
+ // Tracks whether cleanup has run prevents hydrateCanvas from
23
+ // running after canvas.dispose() if rAFs fire post-unmount
24
+ let disposed = false;
25
+ let raf1;
26
+ let raf2;
26
27
  const hydrateCanvas = async () => {
28
+ // Guard 1: unmounted before rAF fired — canvas already disposed
29
+ if (disposed)
30
+ return;
31
+ // Guard 2: check Fabric's internal ctx directly.
32
+ // canvas.getContext() is NOT a valid Fabric v6 API — always undefined.
33
+ // contextContainer is the actual 2D ctx Fabric uses internally.
34
+ // If undefined, Fabric hasn't finished attaching to the DOM yet.
35
+ const fabricCtx = canvas.contextContainer;
36
+ if (!fabricCtx) {
37
+ console.warn("[useCanvasInit] Fabric ctx not ready, skipping hydration");
38
+ return;
39
+ }
27
40
  const hasCanvas = !!initialData?.canvas;
28
41
  const hasNodes = !!(initialData?.tasks || initialData?.documents);
29
42
  if (hasCanvas || hasNodes) {
30
43
  isRestoringRef.current = true;
31
44
  if (hasCanvas) {
32
45
  try {
33
- // Guard: ensure the canvas context is available before loading
34
- if (!canvas.getContext()) {
35
- console.warn("[useCanvasInit] Canvas context not ready, skipping load");
36
- return;
37
- }
38
46
  await canvas.loadFromJSON(JSON.parse(initialData.canvas));
47
+ if (disposed)
48
+ return; // re-check after async gap — dispose may have run during await
39
49
  canvas.renderAll();
40
50
  pushHistory(JSON.stringify(canvas.toJSON()));
41
51
  }
42
52
  catch (err) {
43
53
  console.error("[useCanvasInit] Canvas load failed:", err);
44
- addWelcomeContent(canvas, "https://res.cloudinary.com/dqyjffc9q/image/upload/v1773420161/easyflow-logo_nbpfd7.png");
54
+ if (!disposed) {
55
+ addWelcomeContent(canvas, "https://res.cloudinary.com/dqyjffc9q/image/upload/v1773420161/easyflow-logo_nbpfd7.png");
56
+ }
45
57
  }
46
58
  }
47
- if (hasNodes) {
59
+ if (hasNodes && !disposed) {
48
60
  try {
49
61
  if (initialData?.tasks)
50
62
  setTasks(initialData.tasks);
@@ -58,13 +70,17 @@ export const useCanvasInit = ({ canvasRef, fabricCanvasRef, activeTool, suppress
58
70
  isRestoringRef.current = false;
59
71
  }
60
72
  else {
61
- addWelcomeContent(canvas, "https://res.cloudinary.com/dqyjffc9q/image/upload/v1773420161/easyflow-logo_nbpfd7.png");
62
- pushHistory(JSON.stringify(canvas.toJSON()));
73
+ if (!disposed) {
74
+ addWelcomeContent(canvas, "https://res.cloudinary.com/dqyjffc9q/image/upload/v1773420161/easyflow-logo_nbpfd7.png");
75
+ pushHistory(JSON.stringify(canvas.toJSON()));
76
+ }
63
77
  }
64
78
  };
65
- // Defer by one tick so Fabric's internal canvas context is fully ready
66
- requestAnimationFrame(() => {
67
- requestAnimationFrame(() => {
79
+ // Double rAF ensures Fabric's internal ctx is fully attached:
80
+ // - raf1: fires when browser is about to paint (Fabric constructor done)
81
+ // - raf2: fires after first paint (contextContainer guaranteed attached)
82
+ raf1 = requestAnimationFrame(() => {
83
+ raf2 = requestAnimationFrame(() => {
68
84
  hydrateCanvas();
69
85
  });
70
86
  });
@@ -83,11 +99,17 @@ export const useCanvasInit = ({ canvasRef, fabricCanvasRef, activeTool, suppress
83
99
  canvasElement.addEventListener("touchmove", preventDefaults, { passive: false });
84
100
  // ── Cleanup ───────────────────────────────────────────────────────────────
85
101
  return () => {
102
+ // Set disposed FIRST and cancel rAFs BEFORE canvas.dispose().
103
+ // This ensures hydrateCanvas cannot run on an already-disposed canvas
104
+ // which is what causes the ctx/clearRect errors.
105
+ disposed = true;
106
+ cancelAnimationFrame(raf1);
107
+ cancelAnimationFrame(raf2);
86
108
  window.removeEventListener("resize", handleResize);
87
109
  canvasElement.removeEventListener("touchstart", preventDefaults);
88
110
  canvasElement.removeEventListener("touchmove", preventDefaults);
89
111
  canvas.dispose();
90
112
  fabricCanvasRef.current = null;
91
113
  };
92
- }, []); // Empty — must never re-run, initialData read once on mount
114
+ }, []);
93
115
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.47.0",
3
+ "version": "2.50.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",