@mhamz.01/easyflow-whiteboard 2.56.0 → 2.58.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":"useCopyPaste.d.ts","sourceRoot":"","sources":["../../src/hooks/useCopyPaste.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAE9C,UAAU,iBAAiB;IACzB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACxC;AAED,eAAO,MAAM,YAAY,GAAI,kBAAkB,iBAAiB,SAmE/D,CAAC"}
1
+ {"version":3,"file":"useCopyPaste.d.ts","sourceRoot":"","sources":["../../src/hooks/useCopyPaste.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,MAAM,EAAiC,MAAM,QAAQ,CAAC;AAE/D,UAAU,iBAAiB;IACzB,YAAY,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACxC;AAED,eAAO,MAAM,YAAY,GAAI,kBAAkB,iBAAiB,SAmG/D,CAAC"}
@@ -1,61 +1,85 @@
1
1
  import { useEffect, useRef } from "react";
2
+ import { ActiveSelection } from "fabric";
2
3
  export const useCopyPaste = ({ fabricCanvas }) => {
3
- // Clipboard lives in a ref no React state needed, never causes re-renders
4
- const clipboardRef = useRef(null);
4
+ // Store cloned individual objects — never a canvas-attached ActiveSelection
5
+ const clipboardRef = useRef([]);
5
6
  useEffect(() => {
6
7
  const handleKeyDown = async (e) => {
7
- // Don't intercept when typing in inputs
8
8
  if (e.target instanceof HTMLInputElement ||
9
9
  e.target instanceof HTMLTextAreaElement)
10
10
  return;
11
11
  const canvas = fabricCanvas.current;
12
12
  if (!canvas)
13
13
  return;
14
- // ── Copy ──────────────────────────────────────────────────────────────
14
+ // ── Copy ────────────────────────────────────────────────────────────
15
15
  if ((e.ctrlKey || e.metaKey) && e.key === "c") {
16
16
  const active = canvas.getActiveObject();
17
17
  if (!active)
18
18
  return;
19
- // clone() returns a deep copy with no canvas reference —
20
- // safe to store and paste multiple times
21
- const cloned = await active.clone();
22
- clipboardRef.current = cloned;
19
+ if (active.type === "activeSelection" && "getObjects" in active) {
20
+ // Clone each child independently detached from canvas
21
+ // Store as flat array of individual objects, never as a group
22
+ const clones = await Promise.all(active.getObjects().map((obj) => obj.clone()));
23
+ // Snapshot each child's ABSOLUTE canvas position at copy time
24
+ // getObjects() inside activeSelection returns group-relative coords,
25
+ // so we must add the group's position to get true canvas coords
26
+ const groupLeft = active.left ?? 0;
27
+ const groupTop = active.top ?? 0;
28
+ const groupAngle = active.angle ?? 0;
29
+ clones.forEach((clone, i) => {
30
+ const src = active.getObjects()[i];
31
+ clone.set({
32
+ left: (src.left ?? 0) + groupLeft,
33
+ top: (src.top ?? 0) + groupTop,
34
+ angle: (src.angle ?? 0) + groupAngle,
35
+ });
36
+ clone.setCoords();
37
+ });
38
+ clipboardRef.current = clones;
39
+ }
40
+ else {
41
+ // Single object — clone detached
42
+ const clone = await active.clone();
43
+ clipboardRef.current = [clone];
44
+ }
23
45
  return;
24
46
  }
25
- // ── Paste ─────────────────────────────────────────────────────────────
47
+ // ── Paste ────────────────────────────────────────────────────────────
26
48
  if ((e.ctrlKey || e.metaKey) && e.key === "v") {
27
- const copied = clipboardRef.current;
28
- if (!copied)
49
+ if (clipboardRef.current.length === 0)
29
50
  return;
30
- // Clone again from clipboard so paste can be repeated (Ctrl+V × N)
31
- const cloned = await copied.clone();
32
- // Offset so paste doesn't land exactly on top of the original
33
- cloned.set({
34
- left: (cloned.left ?? 0) + 20,
35
- top: (cloned.top ?? 0) + 20,
36
- // Clear any active selection state from the cloned object
37
- evented: true,
38
- });
39
- // If it's a group/active selection, ungroup into individual objects
40
- if (cloned.type === "activeSelection" && "getObjects" in cloned) {
41
- cloned.canvas = canvas;
42
- cloned.getObjects().forEach((obj) => {
43
- canvas.add(obj);
51
+ canvas.discardActiveObject();
52
+ const offset = 20;
53
+ const addedObjects = [];
54
+ // Clone fresh from our stored detached objects each paste
55
+ // This guarantees no canvas reference contamination
56
+ const freshClones = await Promise.all(clipboardRef.current.map((obj) => obj.clone()));
57
+ for (const clone of freshClones) {
58
+ clone.set({
59
+ left: (clone.left ?? 0) + offset,
60
+ top: (clone.top ?? 0) + offset,
61
+ evented: true,
62
+ selectable: true,
44
63
  });
45
- cloned.setCoords();
64
+ clone.setCoords();
65
+ canvas.add(clone);
66
+ addedObjects.push(clone);
67
+ }
68
+ if (addedObjects.length === 1) {
69
+ canvas.setActiveObject(addedObjects[0]);
46
70
  }
47
71
  else {
48
- canvas.add(cloned);
72
+ const newSelection = new ActiveSelection(addedObjects, { canvas });
73
+ canvas.setActiveObject(newSelection);
49
74
  }
50
- // Select the newly pasted object(s)
51
- canvas.setActiveObject(cloned);
52
75
  canvas.requestRenderAll();
53
- // Update clipboard to the new clone so next paste offsets again
54
- clipboardRef.current = cloned;
55
- return;
76
+ // Update clipboard with the newly pasted clones (detached copies)
77
+ // so next Ctrl+V offsets from current paste position
78
+ // CRITICAL: clone them immediately so they're detached from canvas
79
+ clipboardRef.current = await Promise.all(addedObjects.map((obj) => obj.clone()));
56
80
  }
57
81
  };
58
82
  window.addEventListener("keydown", handleKeyDown);
59
83
  return () => window.removeEventListener("keydown", handleKeyDown);
60
- }, [fabricCanvas]); // registered once — canvas read via ref
84
+ }, [fabricCanvas]);
61
85
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.56.0",
3
+ "version": "2.58.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",