@mhamz.01/easyflow-whiteboard 2.38.0 → 2.40.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":"usePan.d.ts","sourceRoot":"","sources":["../../src/hooks/usePan.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,WAAW;IACnB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,iBAAiB,EAAE,CAAC,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACjE;AAED,eAAO,MAAM,MAAM,GAAI,8DAKpB,WAAW,SAkGb,CAAC"}
1
+ {"version":3,"file":"usePan.d.ts","sourceRoot":"","sources":["../../src/hooks/usePan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,WAAW;IACnB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,iBAAiB,EAAE,CAAC,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACjE;AAED,eAAO,MAAM,MAAM,GAAI,8DAKpB,WAAW,SA8Gb,CAAC"}
@@ -1,5 +1,14 @@
1
- import { useEffect } from "react";
1
+ import { useEffect, useRef } from "react";
2
2
  export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport, }) => {
3
+ // Refs so the effect registers ONCE but always reads latest values.
4
+ // Avoids tearing down/reattaching mouse:down/move/up on every tool change.
5
+ const activeToolRef = useRef(activeTool);
6
+ const handleZoomRef = useRef(handleZoom);
7
+ const setCanvasViewportRef = useRef(setCanvasViewport);
8
+ // Assigned synchronously in render — always fresh before any event fires
9
+ activeToolRef.current = activeTool;
10
+ handleZoomRef.current = handleZoom;
11
+ setCanvasViewportRef.current = setCanvasViewport;
3
12
  useEffect(() => {
4
13
  const canvas = fabricCanvas.current;
5
14
  if (!canvas)
@@ -11,16 +20,14 @@ export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport
11
20
  let lastY = 0;
12
21
  let lastTouchDistance = 0;
13
22
  const onDown = (opt) => {
14
- if (activeTool !== "pan")
23
+ if (activeToolRef.current !== "pan")
15
24
  return;
16
25
  const e = opt.e;
17
- // Pinch initialization
18
26
  if (e.touches && e.touches.length === 2) {
19
27
  isPanning = false;
20
28
  lastTouchDistance = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
21
29
  return;
22
30
  }
23
- // Pan initialization
24
31
  const pointer = e.touches ? e.touches[0] : e;
25
32
  isPanning = true;
26
33
  lastX = pointer.clientX;
@@ -28,45 +35,50 @@ export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport
28
35
  canvas.setCursor("grabbing");
29
36
  };
30
37
  const onMove = (opt) => {
31
- if (activeTool !== "pan")
38
+ if (activeToolRef.current !== "pan")
32
39
  return;
33
40
  const e = opt.e;
34
- // Handle pinch zoom (two fingers)
35
41
  if (e.touches && e.touches.length === 2) {
36
42
  const currentDistance = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
37
43
  if (lastTouchDistance > 0) {
38
44
  const zoom = canvas.getZoom();
39
45
  const delta = (currentDistance - lastTouchDistance) * 0.01;
40
- const newZoom = zoom + delta;
41
46
  const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
42
47
  const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
43
48
  const rect = canvasEl.getBoundingClientRect();
44
- handleZoom(newZoom, { x: midX - rect.left, y: midY - rect.top });
49
+ handleZoomRef.current(zoom + delta, {
50
+ x: midX - rect.left,
51
+ y: midY - rect.top,
52
+ });
45
53
  }
46
54
  lastTouchDistance = currentDistance;
47
55
  return;
48
56
  }
49
- // Handle panning (one finger or mouse)
50
- if (isPanning) {
51
- const pointer = e.touches ? e.touches[0] : e;
52
- const vpt = canvas.viewportTransform;
53
- if (vpt) {
54
- vpt[4] += pointer.clientX - lastX;
55
- vpt[5] += pointer.clientY - lastY;
56
- canvas.requestRenderAll();
57
- }
58
- lastX = pointer.clientX;
59
- lastY = pointer.clientY;
60
- }
57
+ if (!isPanning)
58
+ return; // early exit — avoids any work on non-pan moves
59
+ const pointer = e.touches ? e.touches[0] : e;
60
+ const vpt = canvas.viewportTransform;
61
+ if (!vpt)
62
+ return;
63
+ // VPT mutation is synchronous — NO rAF here intentionally.
64
+ // rAF delays the mutation by one frame which makes pan feel like 20fps.
65
+ vpt[4] += pointer.clientX - lastX;
66
+ vpt[5] += pointer.clientY - lastY;
67
+ canvas.requestRenderAll();
68
+ lastX = pointer.clientX;
69
+ lastY = pointer.clientY;
61
70
  };
62
71
  const onUp = () => {
72
+ if (!isPanning && lastTouchDistance === 0)
73
+ return; // nothing to clean up
63
74
  const vpt = canvas.viewportTransform;
64
75
  if (vpt) {
65
- setCanvasViewport({ x: vpt[4], y: vpt[5] });
76
+ // React state update only on mouse:up not per-frame during pan
77
+ setCanvasViewportRef.current({ x: vpt[4], y: vpt[5] });
66
78
  }
67
79
  isPanning = false;
68
80
  lastTouchDistance = 0;
69
- canvas.setCursor(activeTool === "pan" ? "grab" : "default");
81
+ canvas.setCursor(activeToolRef.current === "pan" ? "grab" : "default");
70
82
  };
71
83
  canvas.on("mouse:down", onDown);
72
84
  canvas.on("mouse:move", onMove);
@@ -76,5 +88,5 @@ export const usePan = ({ fabricCanvas, activeTool, handleZoom, setCanvasViewport
76
88
  canvas.off("mouse:move", onMove);
77
89
  canvas.off("mouse:up", onUp);
78
90
  };
79
- }, [activeTool, handleZoom, setCanvasViewport, fabricCanvas]);
91
+ }, [fabricCanvas]); // registered once per canvas mount
80
92
  };
@@ -1 +1 @@
1
- {"version":3,"file":"usePersistance.d.ts","sourceRoot":"","sources":["../../src/hooks/usePersistance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAgC,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,mBAAmB;IAC3B,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,GAAG,EAAE,CAAC;IACb,SAAS,EAAE,GAAG,EAAE,CAAC;IACjB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACzC,kBAAkB,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;CAC9C;AAKD,eAAO,MAAM,cAAc,GAAI,sFAO5B,mBAAmB,SAgFrB,CAAC"}
1
+ {"version":3,"file":"usePersistance.d.ts","sourceRoot":"","sources":["../../src/hooks/usePersistance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAgC,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,UAAU,mBAAmB;IAC3B,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,GAAG,EAAE,CAAC;IACb,SAAS,EAAE,GAAG,EAAE,CAAC;IACjB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACzC,kBAAkB,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;CAC9C;AAKD,eAAO,MAAM,cAAc,GAAI,sFAO5B,mBAAmB,SAkFrB,CAAC"}
@@ -12,6 +12,7 @@ export const usePersistence = ({ fabricCanvas, tasks, documents, pushHistory, is
12
12
  const json = JSON.stringify(canvas.toJSON());
13
13
  localStorage.setItem(CANVAS_KEY, json);
14
14
  pushHistory(json);
15
+ console.log("💾 Canvas content saving to storage is: " + json);
15
16
  console.log("💾 Canvas saved via requestIdleCallback");
16
17
  }
17
18
  catch (err) {
@@ -34,6 +35,7 @@ export const usePersistence = ({ fabricCanvas, tasks, documents, pushHistory, is
34
35
  try {
35
36
  const nodesData = { tasks, documents };
36
37
  localStorage.setItem(NODES_KEY, JSON.stringify(nodesData));
38
+ console.log("💾 Nodes saving to storage is: " + JSON.stringify(nodesData));
37
39
  console.log("💾 Custom nodes saved");
38
40
  }
39
41
  catch (err) {
@@ -62,38 +62,22 @@ interface WhiteboardState {
62
62
  addCanvasObject: (obj: FabricObject) => void;
63
63
  removeCanvasObject: (obj: FabricObject) => void;
64
64
  clearCanvasObjects: () => void;
65
+ selectedObjects: FabricObject[];
66
+ setSelectedObjects: (objects: FabricObject[]) => void;
65
67
  toolOptions: ToolOptions;
66
68
  setToolOption: <T extends keyof ToolOptions>(tool: T, option: keyof ToolOptions[T], value: any) => void;
67
69
  history: string[];
68
70
  historyIndex: number;
69
71
  canUndo: boolean;
70
72
  canRedo: boolean;
71
- pushHistory: (state: string) => void;
73
+ pushHistory: (snapshot: string) => void;
72
74
  undo: () => string | null;
73
75
  redo: () => string | null;
74
- setCanUndo: (canUndo: boolean) => void;
75
- setCanRedo: (canRedo: boolean) => void;
76
+ setCanUndo: (v: boolean) => void;
77
+ setCanRedo: (v: boolean) => void;
76
78
  zoom: number;
77
79
  setZoom: (zoom: number) => void;
78
- selectedObjects: FabricObject[];
79
- setSelectedObjects: (objects: FabricObject[]) => void;
80
80
  }
81
- export declare const useWhiteboardStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<WhiteboardState>, "setState" | "devtools"> & {
82
- setState(partial: WhiteboardState | Partial<WhiteboardState> | ((state: WhiteboardState) => WhiteboardState | Partial<WhiteboardState>), replace?: false | undefined, action?: (string | {
83
- [x: string]: unknown;
84
- [x: number]: unknown;
85
- [x: symbol]: unknown;
86
- type: string;
87
- }) | undefined): void;
88
- setState(state: WhiteboardState | ((state: WhiteboardState) => WhiteboardState), replace: true, action?: (string | {
89
- [x: string]: unknown;
90
- [x: number]: unknown;
91
- [x: symbol]: unknown;
92
- type: string;
93
- }) | undefined): void;
94
- devtools: {
95
- cleanup: () => void;
96
- };
97
- }>;
81
+ export declare const useWhiteboardStore: import("zustand").UseBoundStore<import("zustand").StoreApi<WhiteboardState>>;
98
82
  export {};
99
83
  //# sourceMappingURL=whiteboard-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"whiteboard-store.d.ts","sourceRoot":"","sources":["../../src/store/whiteboard-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"whiteboard-store.d.ts","sourceRoot":"","sources":["../../src/store/whiteboard-store.ts"],"names":[],"mappings":"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"}
@@ -1,137 +1,84 @@
1
1
  import { create } from "zustand";
2
- import { devtools } from "zustand/middleware";
2
+ // ─── Constants ────────────────────────────────────────────────────────────────
3
+ // Each history entry is a full JSON canvas snapshot (can be 100KB+).
4
+ // Without a cap: 200 actions × 100KB = 20MB of undo history in memory.
5
+ const MAX_HISTORY = 50;
3
6
  const defaultToolOptions = {
4
- pen: {
5
- color: "#ffffff",
6
- strokeWidth: 2,
7
- opacity: 1,
8
- strokeDashArray: null,
9
- },
10
- rectangle: {
11
- fillColor: "transparent",
12
- strokeColor: "#ffffff",
13
- strokeWidth: 2,
14
- strokeDashArray: null,
15
- },
16
- circle: {
17
- fillColor: "transparent",
18
- strokeColor: "#ffffff",
19
- strokeWidth: 2,
20
- strokeDashArray: null,
21
- },
22
- frame: {
23
- fillColor: "#FFFFFF",
24
- strokeColor: "#E5E7EB",
25
- strokeWidth: 1,
26
- strokeDashArray: null,
27
- },
28
- text: {
29
- fontSize: 24,
30
- fontFamily: "cursive",
31
- color: "#ffffff",
32
- fontWeight: "400",
33
- textAlign: "left",
34
- },
35
- image: {
36
- opacity: 1,
37
- filters: [],
38
- },
39
- eraser: {
40
- size: 20,
41
- },
42
- line: {
43
- strokeColor: "#ffffff",
44
- strokeWidth: 2,
45
- strokeDashArray: null,
46
- }, // ← ADD THIS
47
- arrow: {
48
- strokeColor: "#ffffff",
49
- strokeWidth: 2,
50
- strokeDashArray: null,
51
- }, // ← ADD THIS
7
+ pen: { color: "#ffffff", strokeWidth: 2, opacity: 1, strokeDashArray: null },
8
+ rectangle: { fillColor: "transparent", strokeColor: "#ffffff", strokeWidth: 2, strokeDashArray: null },
9
+ circle: { fillColor: "transparent", strokeColor: "#ffffff", strokeWidth: 2, strokeDashArray: null },
10
+ frame: { fillColor: "#FFFFFF", strokeColor: "#E5E7EB", strokeWidth: 1, strokeDashArray: null },
11
+ text: { fontSize: 24, fontFamily: "cursive", color: "#ffffff", fontWeight: "400", textAlign: "left" },
12
+ image: { opacity: 1, filters: [] },
13
+ eraser: { size: 20 },
14
+ line: { strokeColor: "#ffffff", strokeWidth: 2, strokeDashArray: null },
15
+ arrow: { strokeColor: "#ffffff", strokeWidth: 2, strokeDashArray: null },
52
16
  };
53
- export const useWhiteboardStore = create()(devtools((set, get) => ({
54
- // Tool state
17
+ // ─── Store ────────────────────────────────────────────────────────────────────
18
+ export const useWhiteboardStore = create()((set, get) => ({
19
+ // ── Tool ──────────────────────────────────────────────────────────────────
55
20
  activeTool: "select",
56
21
  setActiveTool: (tool) => set({ activeTool: tool }),
57
- // Selected object type (for editing)
22
+ // ── Dropdown ──────────────────────────────────────────────────────────────
23
+ activeDropdown: null,
24
+ setActiveDropdown: (id) => set({ activeDropdown: id }),
25
+ // ── Selected object type ──────────────────────────────────────────────────
58
26
  selectedObjectType: null,
59
27
  setSelectedObjectType: (type) => set({ selectedObjectType: type }),
60
- // Canvas objects
28
+ // ── Canvas objects ────────────────────────────────────────────────────────
29
+ // NOTE: Fabric instances have circular refs and DOM refs — they cannot be
30
+ // serialized. If you add devtools back, exclude this slice or it will throw.
31
+ // Using concat() instead of spread avoids the intermediate spread allocation.
61
32
  canvasObjects: [],
62
- addCanvasObject: (obj) => set((state) => ({
63
- canvasObjects: [...state.canvasObjects, obj],
64
- })),
65
- removeCanvasObject: (obj) => set((state) => ({
66
- canvasObjects: state.canvasObjects.filter((o) => o !== obj),
67
- })),
33
+ addCanvasObject: (obj) => set((s) => ({ canvasObjects: s.canvasObjects.concat(obj) })),
34
+ removeCanvasObject: (obj) => set((s) => ({ canvasObjects: s.canvasObjects.filter((o) => o !== obj) })),
68
35
  clearCanvasObjects: () => set({ canvasObjects: [] }),
69
- // Tool options
36
+ // ── Selected objects ──────────────────────────────────────────────────────
37
+ selectedObjects: [],
38
+ setSelectedObjects: (objects) => set({ selectedObjects: objects }),
39
+ // ── Tool options ──────────────────────────────────────────────────────────
70
40
  toolOptions: defaultToolOptions,
71
- setToolOption: (tool, option, value) => set((state) => ({
41
+ setToolOption: (tool, option, value) => set((s) => ({
72
42
  toolOptions: {
73
- ...state.toolOptions,
74
- [tool]: {
75
- ...state.toolOptions[tool],
76
- [option]: value,
77
- },
43
+ ...s.toolOptions,
44
+ [tool]: { ...s.toolOptions[tool], [option]: value },
78
45
  },
79
46
  })),
80
- // History
47
+ // ── History ───────────────────────────────────────────────────────────────
81
48
  history: [],
82
49
  historyIndex: -1,
83
50
  canUndo: false,
84
51
  canRedo: false,
85
- pushHistory: (state) => {
86
- const { history, historyIndex } = get();
87
- // Slice off any redo states ahead of current index
88
- const newHistory = history.slice(0, historyIndex + 1);
89
- newHistory.push(state);
90
- set({
91
- history: newHistory,
92
- historyIndex: newHistory.length - 1,
93
- // Can undo if there is at least 1 previous state (index > 0)
94
- canUndo: newHistory.length > 1,
95
- canRedo: false,
96
- });
97
- },
98
- // Sets the selected dropdown (task/document) - used to auto-switch back to select tool when closing
99
- activeDropdown: null,
100
- setActiveDropdown: (id) => set({
101
- activeDropdown: id,
52
+ // Uses set() updater (single atomic write) instead of get() + set() which
53
+ // could interleave in React concurrent mode.
54
+ pushHistory: (snapshot) => set((s) => {
55
+ const trimmed = s.history.slice(0, s.historyIndex + 1);
56
+ trimmed.push(snapshot);
57
+ const capped = trimmed.length > MAX_HISTORY ? trimmed.slice(-MAX_HISTORY) : trimmed;
58
+ const newIndex = capped.length - 1;
59
+ return { history: capped, historyIndex: newIndex, canUndo: newIndex > 0, canRedo: false };
102
60
  }),
103
61
  undo: () => {
104
62
  const { history, historyIndex } = get();
105
- // Must have a previous state to go back to
106
63
  if (historyIndex > 0) {
107
- const newIndex = historyIndex - 1;
108
- set({
109
- historyIndex: newIndex,
110
- canUndo: newIndex > 0,
111
- canRedo: true,
112
- });
113
- return history[newIndex];
64
+ const i = historyIndex - 1;
65
+ set({ historyIndex: i, canUndo: i > 0, canRedo: true });
66
+ return history[i];
114
67
  }
115
68
  return null;
116
69
  },
117
70
  redo: () => {
118
71
  const { history, historyIndex } = get();
119
72
  if (historyIndex < history.length - 1) {
120
- const newIndex = historyIndex + 1;
121
- set({
122
- historyIndex: newIndex,
123
- canUndo: true,
124
- canRedo: newIndex < history.length - 1,
125
- });
126
- return history[newIndex];
73
+ const i = historyIndex + 1;
74
+ set({ historyIndex: i, canUndo: true, canRedo: i < history.length - 1 });
75
+ return history[i];
127
76
  }
128
77
  return null;
129
78
  },
130
79
  setCanUndo: (v) => set({ canUndo: v }),
131
80
  setCanRedo: (v) => set({ canRedo: v }),
132
- // Zoom
81
+ // ── Zoom ──────────────────────────────────────────────────────────────────
133
82
  zoom: 1,
134
83
  setZoom: (zoom) => set({ zoom }),
135
- selectedObjects: [],
136
- setSelectedObjects: (objects) => set({ selectedObjects: objects }),
137
- }), { name: "fabric-whiteboard-store" }));
84
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.38.0",
3
+ "version": "2.40.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",