@mhamz.01/easyflow-whiteboard 2.67.0 → 2.68.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,2CAujBzB"}
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,2CAgmBzB"}
@@ -22,10 +22,15 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
22
22
  offsetX: 0,
23
23
  offsetY: 0,
24
24
  });
25
+ const finalPositionsRef = useRef(null);
25
26
  const rafIdRef = useRef(null);
26
27
  const overlayRef = useRef(null);
28
+ const localTasksRef = useRef(localTasks);
29
+ const localDocumentsRef = useRef(localDocuments);
27
30
  const selectedIdsRef = useRef(selectedIds);
28
31
  selectedIdsRef.current = selectedIds;
32
+ localTasksRef.current = localTasks;
33
+ localDocumentsRef.current = localDocuments;
29
34
  // ── Sync props → local state ────────────────────────────────────────────────
30
35
  useEffect(() => { setLocalTasks(tasks); }, [tasks]);
31
36
  useEffect(() => { setLocalDocuments(documents); }, [documents]);
@@ -220,53 +225,70 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
220
225
  return e;
221
226
  };
222
227
  const handleDragStart = (itemId, e) => {
228
+ // 1. Safety check for the Fabric instance
223
229
  const canvas = fabricCanvas?.current;
224
230
  if (!canvas)
225
231
  return;
232
+ // 2. Normalize the event (Touch vs Mouse)
226
233
  if (e.cancelable)
227
234
  e.preventDefault();
228
235
  const pointer = getPointerEvent(e);
236
+ // 3. Determine which items are being dragged
237
+ // selection update DOES NOT trigger before drag snapshot
229
238
  let itemsToDrag;
230
- if (selectedIds.has(itemId)) {
231
- itemsToDrag = Array.from(selectedIds);
239
+ if (selectedIdsRef.current.has(itemId)) {
240
+ itemsToDrag = Array.from(selectedIdsRef.current);
232
241
  }
233
242
  else {
234
243
  itemsToDrag = [itemId];
235
- setSelectedIds(new Set([itemId]));
236
244
  }
245
+ // 4. Capture current World Transform (Zoom & Pan)
246
+ // We read directly from the canvas to ensure zero-frame lag
237
247
  const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
238
248
  const liveZoom = vpt[0];
239
249
  const liveVpX = vpt[4];
240
250
  const liveVpY = vpt[5];
251
+ // 5. Convert the Click Position from Screen Pixels to World Units
241
252
  const clickWorldX = (pointer.clientX - liveVpX) / liveZoom;
242
253
  const clickWorldY = (pointer.clientY - liveVpY) / liveZoom;
254
+ // 6. Get the clicked item's current World Position
243
255
  const clickedPos = getItemPosition(itemId);
244
256
  if (!clickedPos)
245
257
  return;
258
+ // 7. Calculate the Offset in WORLD UNITS
259
+ // This is the distance from the mouse to the node's top-left in the infinite grid.
260
+ // This value remains constant even if you zoom during the drag.
246
261
  const worldOffsetX = clickWorldX - clickedPos.x;
247
262
  const worldOffsetY = clickWorldY - clickedPos.y;
263
+ // 8. Snapshot starting positions for all selected HTML nodes
248
264
  const startPositions = new Map();
249
265
  itemsToDrag.forEach((id) => {
250
266
  const pos = getItemPosition(id);
251
267
  if (pos)
252
268
  startPositions.set(id, pos);
253
269
  });
270
+ // 9. Snapshot starting positions for all selected Fabric objects
254
271
  const canvasObjectsStartPos = new Map();
255
272
  selectedCanvasObjects.forEach((obj) => {
256
273
  canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
257
274
  });
275
+ // 10. Commit to the ref for the requestAnimationFrame loop
258
276
  dragStateRef.current = {
259
277
  isDragging: true,
260
278
  itemIds: itemsToDrag,
261
279
  startPositions,
262
280
  canvasObjectsStartPos,
263
- offsetX: worldOffsetX,
264
- offsetY: worldOffsetY,
281
+ offsetX: worldOffsetX, // Now stored as World Units
282
+ offsetY: worldOffsetY, // Now stored as World Units
265
283
  };
284
+ if (!selectedIdsRef.current.has(itemId) && dragStateRef.current.itemIds.length === 0) {
285
+ setSelectedIds(new Set([itemId]));
286
+ }
287
+ // 11. Trigger UI states
266
288
  setDragging({ itemIds: itemsToDrag });
267
- // ── ATTACH LISTENERS DIRECTLY ──
268
289
  document.body.style.cursor = "grabbing";
269
290
  document.body.style.userSelect = "none";
291
+ document.body.style.touchAction = "none";
270
292
  };
271
293
  // ── Drag move (HTML Node side) ───────────────────────────────────────────────
272
294
  useEffect(() => {
@@ -308,16 +330,22 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
308
330
  const deltaX = newWorldX - firstStart.x;
309
331
  const deltaY = newWorldY - firstStart.y;
310
332
  // 6. Update HTML Nodes (Batching these into one state update)
311
- setLocalTasks((prev) => prev.map((t) => itemIds.includes(t.id) ? {
312
- ...t,
313
- x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
314
- y: (startPositions.get(t.id)?.y ?? t.y) + deltaY,
315
- } : t));
316
- setLocalDocuments((prev) => prev.map((d) => itemIds.includes(d.id) ? {
317
- ...d,
318
- x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
319
- y: (startPositions.get(d.id)?.y ?? d.y) + deltaY,
320
- } : d));
333
+ setLocalTasks((prev) => {
334
+ const next = prev.map((t) => itemIds.includes(t.id)
335
+ ? { ...t, x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
336
+ y: (startPositions.get(t.id)?.y ?? t.y) + deltaY }
337
+ : t);
338
+ localTasksRef.current = next; // write-through: ref always has latest
339
+ return next;
340
+ });
341
+ setLocalDocuments((prev) => {
342
+ const next = prev.map((d) => itemIds.includes(d.id)
343
+ ? { ...d, x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
344
+ y: (startPositions.get(d.id)?.y ?? d.y) + deltaY }
345
+ : d);
346
+ localDocumentsRef.current = next; // ← write-through
347
+ return next;
348
+ });
321
349
  // 7. Sync Fabric Objects (Imperative update for performance)
322
350
  canvasObjectsStartPos.forEach((startPos, obj) => {
323
351
  obj.set({
@@ -326,25 +354,29 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
326
354
  });
327
355
  obj.setCoords(); // Required for selection/intersection accuracy
328
356
  });
357
+ finalPositionsRef.current = {
358
+ tasks: localTasksRef.current, // will be updated by React after setState flushes
359
+ documents: localDocumentsRef.current,
360
+ };
329
361
  // 8. Single render call for all Fabric changes
330
362
  canvas.requestRenderAll();
331
363
  });
332
364
  };
333
365
  const handleEnd = () => {
334
- if (rafIdRef.current !== null)
366
+ if (rafIdRef.current !== null) {
335
367
  cancelAnimationFrame(rafIdRef.current);
336
- // 1. Kill the flag first
368
+ rafIdRef.current = null;
369
+ }
337
370
  dragStateRef.current.isDragging = false;
338
371
  setDragging(null);
339
- // 2. Remove the listeners immediately
340
- window.removeEventListener("mousemove", handleMove);
341
- window.removeEventListener("mouseup", handleEnd);
342
- window.removeEventListener("touchmove", handleMove);
343
- window.removeEventListener("touchend", handleEnd);
344
372
  document.body.style.cursor = "";
345
373
  document.body.style.userSelect = "";
346
- onTasksUpdate?.(localTasks);
347
- onDocumentsUpdate?.(localDocuments);
374
+ document.body.style.touchAction = "";
375
+ // ✅ FIX 1+3: Read from live refs — never from the stale closure.
376
+ // localTasksRef is kept in sync on every render, so this is always
377
+ // the position after the last committed setState, not the t=0 snapshot.
378
+ onTasksUpdate?.(localTasksRef.current);
379
+ onDocumentsUpdate?.(localDocumentsRef.current);
348
380
  };
349
381
  window.addEventListener("mousemove", handleMove, { passive: false });
350
382
  window.addEventListener("mouseup", handleEnd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.67.0",
3
+ "version": "2.68.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",