@mhamz.01/easyflow-whiteboard 2.7.0 → 2.9.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;AAM9C,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,2CAmfzB"}
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;AAM9C,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,2CAkezB"}
@@ -19,8 +19,6 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
19
19
  });
20
20
  const rafIdRef = useRef(null);
21
21
  const overlayRef = useRef(null);
22
- // Add this ref at top of CanvasOverlayLayer:
23
- const isHtmlDraggingRef = useRef(false);
24
22
  // ── Sync props → local state ────────────────────────────────────────────────
25
23
  useEffect(() => { setLocalTasks(tasks); }, [tasks]);
26
24
  useEffect(() => { setLocalDocuments(documents); }, [documents]);
@@ -91,20 +89,20 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
91
89
  if (!canvas)
92
90
  return;
93
91
  const handleObjectMoving = (e) => {
94
- // ── Skip if user is dragging via HTML node — avoid position conflict ──
95
- if (isHtmlDraggingRef.current)
96
- return;
97
92
  const target = e.transform?.target || e.target;
98
93
  if (!target)
99
94
  return;
95
+ // 1. Calculate delta in raw Scene Coordinates
96
+ // We do NOT divide by zoom here because target.left/top are world units.
100
97
  const deltaX = target.left - (target._prevLeft ?? target.left);
101
98
  const deltaY = target.top - (target._prevTop ?? target.top);
102
99
  target._prevLeft = target.left;
103
100
  target._prevTop = target.top;
104
101
  if (deltaX === 0 && deltaY === 0)
105
102
  return;
106
- setLocalTasks((prev) => prev.map((t) => selectedIds.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t));
107
- setLocalDocuments((prev) => prev.map((d) => selectedIds.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d));
103
+ // 2. Apply the raw delta to HTML items
104
+ setLocalTasks((prev) => prev.map((t) => (selectedIds.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t)));
105
+ setLocalDocuments((prev) => prev.map((d) => (selectedIds.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d)));
108
106
  };
109
107
  const handleMouseDown = (e) => {
110
108
  const target = e.target;
@@ -174,116 +172,108 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
174
172
  return e;
175
173
  };
176
174
  const handleDragStart = (itemId, e) => {
177
- if (e.cancelable)
178
- e.preventDefault();
179
- const pointer = getPointerEvent(e);
180
- // ── Snapshot VPT immediately before anything else ──
181
- const vpt = fabricCanvas?.current?.viewportTransform;
182
- const liveZoom = vpt ? vpt[0] : 1;
183
- const liveVpX = vpt ? vpt[4] : 0;
184
- const liveVpY = vpt ? vpt[5] : 0;
185
- // ── Snapshot ALL positions immediately from ref — before any state update ──
186
- const allItems = [
187
- ...localTasks.map(t => ({ id: t.id, x: t.x, y: t.y })),
188
- ...localDocuments.map(d => ({ id: d.id, x: d.x, y: d.y })),
189
- ];
190
- let itemsToDrag;
191
- if (selectedIds.has(itemId)) {
192
- itemsToDrag = Array.from(selectedIds);
193
- }
194
- else {
195
- itemsToDrag = [itemId];
196
- // setSelectedIds AFTER snapshot doesn't affect already-captured positions
175
+ const canvas = fabricCanvas?.current;
176
+ if (!canvas || !e)
177
+ return;
178
+ // Use the raw native event for the most accurate pointer position
179
+ const nativeEvent = 'nativeEvent' in e ? e.nativeEvent : e;
180
+ const pointer = getPointerEvent(nativeEvent);
181
+ // 1. Get the ABSOLUTE current transform from the canvas engine
182
+ const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
183
+ const zoom = vpt[0];
184
+ const vpX = vpt[4];
185
+ const vpY = vpt[5];
186
+ // 2. Calculate World Position of the mouse click immediately
187
+ const clickWorldX = (pointer.clientX - vpX) / zoom;
188
+ const clickWorldY = (pointer.clientY - vpY) / zoom;
189
+ // 3. Get the item's current world position
190
+ const clickedPos = getItemPosition(itemId);
191
+ if (!clickedPos)
192
+ return;
193
+ // 4. Calculate the WORLD OFFSET
194
+ // This is the distance from the mouse to the node's top-left in World Units.
195
+ const worldOffsetX = clickWorldX - clickedPos.x;
196
+ const worldOffsetY = clickWorldY - clickedPos.y;
197
+ // 5. Setup Drag State
198
+ let itemsToDrag = selectedIds.has(itemId) ? Array.from(selectedIds) : [itemId];
199
+ if (!selectedIds.has(itemId))
197
200
  setSelectedIds(new Set([itemId]));
198
- }
199
201
  const startPositions = new Map();
200
- allItems.forEach(({ id, x, y }) => {
201
- if (itemsToDrag.includes(id))
202
- startPositions.set(id, { x, y });
202
+ itemsToDrag.forEach(id => {
203
+ const pos = getItemPosition(id);
204
+ if (pos)
205
+ startPositions.set(id, pos);
203
206
  });
204
207
  const canvasObjectsStartPos = new Map();
205
- selectedCanvasObjects.forEach((obj) => {
208
+ selectedCanvasObjects.forEach(obj => {
206
209
  canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
207
210
  });
208
- const clickedPos = startPositions.get(itemId);
209
- if (!clickedPos)
210
- return;
211
- // ── World-space offset ──
212
- const pointerWorldX = (pointer.clientX - liveVpX) / liveZoom;
213
- const pointerWorldY = (pointer.clientY - liveVpY) / liveZoom;
214
- const offsetWorldX = pointerWorldX - clickedPos.x;
215
- const offsetWorldY = pointerWorldY - clickedPos.y;
216
211
  dragStateRef.current = {
217
212
  isDragging: true,
218
213
  itemIds: itemsToDrag,
219
214
  startPositions,
220
215
  canvasObjectsStartPos,
221
- offsetX: offsetWorldX,
222
- offsetY: offsetWorldY,
216
+ offsetX: worldOffsetX,
217
+ offsetY: worldOffsetY,
223
218
  };
224
- // ── Block object:moving sync for the duration of this HTML drag ──
225
- isHtmlDraggingRef.current = true;
226
219
  setDragging({ itemIds: itemsToDrag });
227
- document.body.style.cursor = "grabbing";
228
- document.body.style.userSelect = "none";
229
- document.body.style.touchAction = "none";
230
220
  };
231
221
  // ── Drag move (HTML Node side) ───────────────────────────────────────────────
232
222
  useEffect(() => {
233
223
  if (!dragging)
234
224
  return;
225
+ // Inside the useEffect that watches [dragging, localTasks, localDocuments, fabricCanvas]
235
226
  const handleMove = (e) => {
236
227
  if (!dragStateRef.current.isDragging)
237
228
  return;
238
229
  if (e.cancelable)
239
230
  e.preventDefault();
240
231
  const pointer = getPointerEvent(e);
232
+ // 1. Throttle updates using requestAnimationFrame for 120Hz/144Hz screen support
241
233
  if (rafIdRef.current !== null)
242
234
  cancelAnimationFrame(rafIdRef.current);
235
+ // Inside handleMove rAF
243
236
  rafIdRef.current = requestAnimationFrame(() => {
244
237
  const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY } = dragStateRef.current;
238
+ const canvas = fabricCanvas?.current;
239
+ if (!canvas)
240
+ return;
241
+ const vpt = canvas.viewportTransform;
242
+ const zoom = vpt[0];
243
+ const vpX = vpt[4];
244
+ const vpY = vpt[5];
245
+ // Current World position of the mouse
246
+ const currentWorldX = (pointer.clientX - vpX) / zoom;
247
+ const currentWorldY = (pointer.clientY - vpY) / zoom;
248
+ // The new world position of the node we are holding
249
+ const newX = currentWorldX - offsetX;
250
+ const newY = currentWorldY - offsetY;
245
251
  const firstId = itemIds[0];
246
252
  const firstStart = startPositions.get(firstId);
247
253
  if (!firstStart)
248
254
  return;
249
- // ── FIX: Read VPT live inside rAF — perfectly in sync with Fabric ──
250
- const vpt = fabricCanvas?.current?.viewportTransform;
251
- const liveZoom = vpt ? vpt[0] : 1;
252
- const liveVpX = vpt ? vpt[4] : 0;
253
- const liveVpY = vpt ? vpt[5] : 0;
254
- // Convert screen → world using live VPT values
255
- const newX = (pointer.clientX - offsetX - liveVpX) / liveZoom;
256
- const newY = (pointer.clientY - offsetY - liveVpY) / liveZoom;
257
255
  const deltaX = newX - firstStart.x;
258
256
  const deltaY = newY - firstStart.y;
259
- setLocalTasks((prev) => prev.map((t) => itemIds.includes(t.id) ? {
257
+ // UPDATE: Only update if the delta has actually changed to prevent jitter
258
+ if (Math.abs(deltaX) < 0.01 && Math.abs(deltaY) < 0.01)
259
+ return;
260
+ setLocalTasks(prev => prev.map(t => itemIds.includes(t.id) ? {
260
261
  ...t,
261
262
  x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
262
263
  y: (startPositions.get(t.id)?.y ?? t.y) + deltaY,
263
264
  } : t));
264
- setLocalDocuments((prev) => prev.map((d) => itemIds.includes(d.id) ? {
265
- ...d,
266
- x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
267
- y: (startPositions.get(d.id)?.y ?? d.y) + deltaY,
268
- } : d));
269
- if (fabricCanvas?.current) {
270
- canvasObjectsStartPos.forEach((startPos, obj) => {
271
- obj.set({
272
- left: startPos.left + deltaX,
273
- top: startPos.top + deltaY,
274
- });
275
- obj.setCoords();
276
- });
277
- fabricCanvas.current.requestRenderAll();
278
- }
265
+ // Sync Fabric objects directly
266
+ canvasObjectsStartPos.forEach((startPos, obj) => {
267
+ obj.set({ left: startPos.left + deltaX, top: startPos.top + deltaY });
268
+ obj.setCoords();
269
+ });
270
+ canvas.requestRenderAll();
279
271
  });
280
272
  };
281
273
  const handleEnd = () => {
282
274
  if (rafIdRef.current !== null)
283
275
  cancelAnimationFrame(rafIdRef.current);
284
276
  dragStateRef.current.isDragging = false;
285
- // ── Re-enable object:moving sync ──
286
- isHtmlDraggingRef.current = false;
287
277
  setDragging(null);
288
278
  document.body.style.cursor = "";
289
279
  document.body.style.userSelect = "";
@@ -367,7 +357,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
367
357
  transformOrigin: "top left",
368
358
  // 3. THE FIX: Remove transition entirely during any viewport change
369
359
  // Any 'ease' during zoom causes the "shaking" behavior.
370
- transition: "none",
360
+ transition: isDragging ? 'none' : 'transform 0.1s ease-out',
371
361
  // 4. Optimization
372
362
  willChange: "transform",
373
363
  zIndex: isDragging ? 1000 : 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhamz.01/easyflow-whiteboard",
3
- "version": "2.7.0",
3
+ "version": "2.9.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",