@mhamz.01/easyflow-whiteboard 2.58.0 → 2.60.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;IAC9C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;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,2CA2jBzB"}
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;IAC9C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;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,2CA4nBzB"}
@@ -10,6 +10,10 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
10
10
  const [selectedIds, setSelectedIds] = useState(new Set());
11
11
  const [dragging, setDragging] = useState(null);
12
12
  const [canvasReady, setCanvasReady] = useState(false);
13
+ const nodeClipboardRef = useRef({
14
+ tasks: [],
15
+ documents: [],
16
+ });
13
17
  const dragStateRef = useRef({
14
18
  isDragging: false,
15
19
  itemIds: [],
@@ -395,6 +399,59 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
395
399
  e.preventDefault();
396
400
  setSelectedIds(new Set([...localTasks.map((t) => t.id), ...localDocuments.map((d) => d.id)]));
397
401
  }
402
+ // ── Copy HTML nodes ──────────────────────────────────────────────────────
403
+ if ((e.ctrlKey || e.metaKey) && e.key === "c") {
404
+ const ids = selectedIdsRef.current;
405
+ if (ids.size === 0)
406
+ return;
407
+ // Snapshot selected nodes into clipboard — plain data, no refs
408
+ nodeClipboardRef.current = {
409
+ tasks: localTasks.filter((t) => ids.has(t.id)),
410
+ documents: localDocuments.filter((d) => ids.has(d.id)),
411
+ };
412
+ return;
413
+ }
414
+ // ── Paste HTML nodes ─────────────────────────────────────────────────────
415
+ if ((e.ctrlKey || e.metaKey) && e.key === "v") {
416
+ const { tasks: copiedTasks, documents: copiedDocs } = nodeClipboardRef.current;
417
+ if (copiedTasks.length === 0 && copiedDocs.length === 0)
418
+ return;
419
+ const now = Date.now();
420
+ const pastedTasks = copiedTasks.map((t, i) => ({
421
+ ...t,
422
+ id: `${t.id}-copy-${now}-${i}`,
423
+ x: t.x + 20,
424
+ y: t.y + 20,
425
+ }));
426
+ const pastedDocs = copiedDocs.map((d, i) => ({
427
+ ...d,
428
+ id: `${d.id}-copy-${now}-${i}`,
429
+ x: d.x + 20,
430
+ y: d.y + 20,
431
+ }));
432
+ setLocalTasks((prev) => {
433
+ const updated = [...prev, ...pastedTasks];
434
+ onTasksUpdate?.(updated);
435
+ return updated;
436
+ });
437
+ setLocalDocuments((prev) => {
438
+ const updated = [...prev, ...pastedDocs];
439
+ onDocumentsUpdate?.(updated);
440
+ return updated;
441
+ });
442
+ // Select the newly pasted nodes
443
+ setSelectedIds(new Set([
444
+ ...pastedTasks.map((t) => t.id),
445
+ ...pastedDocs.map((d) => d.id),
446
+ ]));
447
+ // Mirror official Fabric pattern — offset clipboard itself for cascading paste
448
+ // so each Ctrl+V lands 20px further than the last
449
+ nodeClipboardRef.current = {
450
+ tasks: copiedTasks.map((t) => ({ ...t, x: t.x + 20, y: t.y + 20 })),
451
+ documents: copiedDocs.map((d) => ({ ...d, x: d.x + 20, y: d.y + 20 })),
452
+ };
453
+ return;
454
+ }
398
455
  // Clear selection
399
456
  if (e.key === "Escape") {
400
457
  setSelectedIds(new Set());
@@ -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,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
+ {"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,SAmE/D,CAAC"}
@@ -1,8 +1,7 @@
1
1
  import { useEffect, useRef } from "react";
2
2
  import { ActiveSelection } from "fabric";
3
3
  export const useCopyPaste = ({ fabricCanvas }) => {
4
- // Store cloned individual objects — never a canvas-attached ActiveSelection
5
- const clipboardRef = useRef([]);
4
+ const clipboardRef = useRef(null);
6
5
  useEffect(() => {
7
6
  const handleKeyDown = async (e) => {
8
7
  if (e.target instanceof HTMLInputElement ||
@@ -16,67 +15,45 @@ export const useCopyPaste = ({ fabricCanvas }) => {
16
15
  const active = canvas.getActiveObject();
17
16
  if (!active)
18
17
  return;
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
- }
18
+ // Clone once at copy time store as-is, exactly like official docs
19
+ const cloned = await active.clone();
20
+ clipboardRef.current = cloned;
45
21
  return;
46
22
  }
47
23
  // ── Paste ────────────────────────────────────────────────────────────
48
24
  if ((e.ctrlKey || e.metaKey) && e.key === "v") {
49
- if (clipboardRef.current.length === 0)
25
+ const copied = clipboardRef.current;
26
+ if (!copied)
50
27
  return;
28
+ // Clone again so multiple pastes work independently
29
+ const clonedObj = await copied.clone();
51
30
  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,
31
+ clonedObj.set({
32
+ left: clonedObj.left + 10,
33
+ top: clonedObj.top + 10,
34
+ evented: true,
35
+ });
36
+ if (clonedObj instanceof ActiveSelection) {
37
+ // Official docs pattern exactly:
38
+ // set canvas reference on the ActiveSelection FIRST,
39
+ // then use forEachObject to add children — not getObjects()
40
+ // forEachObject is the correct API for iterating ActiveSelection
41
+ clonedObj.canvas = canvas;
42
+ clonedObj.forEachObject((obj) => {
43
+ canvas.add(obj);
63
44
  });
64
- clone.setCoords();
65
- canvas.add(clone);
66
- addedObjects.push(clone);
67
- }
68
- if (addedObjects.length === 1) {
69
- canvas.setActiveObject(addedObjects[0]);
45
+ // setCoords on the selection itself — not on individual children
46
+ clonedObj.setCoords();
70
47
  }
71
48
  else {
72
- const newSelection = new ActiveSelection(addedObjects, { canvas });
73
- canvas.setActiveObject(newSelection);
49
+ canvas.add(clonedObj);
74
50
  }
51
+ // Official docs pattern: offset clipboard itself for cascading paste
52
+ // This is simpler and safer than storing new clones as clipboard
53
+ copied.top += 10;
54
+ copied.left += 10;
55
+ canvas.setActiveObject(clonedObj);
75
56
  canvas.requestRenderAll();
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()));
80
57
  }
81
58
  };
82
59
  window.addEventListener("keydown", handleKeyDown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.58.0",
3
+ "version": "2.60.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",