@mhamz.01/easyflow-whiteboard 2.121.0 → 2.122.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.
- package/dist/components/node/custom-node-overlay-fix.js +545 -0
- package/dist/components/node/custom-node-overlay-layer.d.ts.map +1 -1
- package/dist/components/node/custom-node-overlay-layer.js +59 -65
- package/dist/hooks/usePersistance.d.ts.map +1 -1
- package/dist/hooks/usePersistance.js +0 -2
- package/package.json +1 -1
|
@@ -281,3 +281,548 @@
|
|
|
281
281
|
// );
|
|
282
282
|
// }
|
|
283
283
|
// // export { Task, Document, CanvasOverlayLayerProps } from "./types/overlay-types";
|
|
284
|
+
// Before change 26 april sunday
|
|
285
|
+
// "use client";
|
|
286
|
+
// import { useState, useEffect, useRef } from "react";
|
|
287
|
+
// import { FabricObject, Canvas } from "fabric";
|
|
288
|
+
// import TaskNode from "./custom-node";
|
|
289
|
+
// import DocumentNode from "./document-node";
|
|
290
|
+
// // ─── Interfaces ───────────────────────────────────────────────────────────────
|
|
291
|
+
// // THIS LAYER HAS ISSUE RELATED TO JUMP OF HTML NODES DURING DRAG/ZOOM
|
|
292
|
+
// // THIS NEED TO BE FIXED BEFORE ANY OTHER FEATURES ARE ADDED TO THIS COMPONENT
|
|
293
|
+
// export interface Task {
|
|
294
|
+
// id: string;
|
|
295
|
+
// title: string;
|
|
296
|
+
// status: "todo" | "in-progress" | "done";
|
|
297
|
+
// x: number;
|
|
298
|
+
// y: number;
|
|
299
|
+
// assignee?: string;
|
|
300
|
+
// project?: string;
|
|
301
|
+
// priority?: "low" | "medium" | "high";
|
|
302
|
+
// dueDate?: string;
|
|
303
|
+
// }
|
|
304
|
+
// export interface Document {
|
|
305
|
+
// id: string;
|
|
306
|
+
// title: string;
|
|
307
|
+
// project: string;
|
|
308
|
+
// breadcrumb?: string[];
|
|
309
|
+
// preview: string;
|
|
310
|
+
// updatedAt?: string;
|
|
311
|
+
// x: number;
|
|
312
|
+
// y: number;
|
|
313
|
+
// }
|
|
314
|
+
// interface CanvasOverlayLayerProps {
|
|
315
|
+
// tasks: Task[];
|
|
316
|
+
// documents: Document[];
|
|
317
|
+
// onTasksUpdate?: (tasks: Task[]) => void;
|
|
318
|
+
// onDocumentsUpdate?: (documents: Document[]) => void;
|
|
319
|
+
// canvasZoom?: number;
|
|
320
|
+
// canvasViewport?: { x: number; y: number };
|
|
321
|
+
// selectionBox?: { x1: number; y1: number; x2: number; y2: number } | null;
|
|
322
|
+
// selectedCanvasObjects?: FabricObject[];
|
|
323
|
+
// fabricCanvas?: React.RefObject<Canvas | null>;
|
|
324
|
+
// canvasReady?: boolean;
|
|
325
|
+
// }
|
|
326
|
+
// interface DragState {
|
|
327
|
+
// isDragging: boolean;
|
|
328
|
+
// itemIds: string[];
|
|
329
|
+
// startPositions: Map<string, { x: number; y: number }>;
|
|
330
|
+
// canvasObjectsStartPos: Map<FabricObject, { left: number; top: number }>;
|
|
331
|
+
// offsetX: number;
|
|
332
|
+
// offsetY: number;
|
|
333
|
+
// }
|
|
334
|
+
// // ─── Component ────────────────────────────────────────────────────────────────
|
|
335
|
+
// export default function CanvasOverlayLayer({
|
|
336
|
+
// tasks,
|
|
337
|
+
// documents,
|
|
338
|
+
// onTasksUpdate,
|
|
339
|
+
// onDocumentsUpdate,
|
|
340
|
+
// canvasZoom = 1,
|
|
341
|
+
// canvasViewport = { x: 0, y: 0 },
|
|
342
|
+
// selectionBox = null,
|
|
343
|
+
// selectedCanvasObjects = [],
|
|
344
|
+
// fabricCanvas,
|
|
345
|
+
// }: CanvasOverlayLayerProps) {
|
|
346
|
+
// const [localTasks, setLocalTasks] = useState<Task[]>(tasks);
|
|
347
|
+
// const [localDocuments, setLocalDocuments] = useState<Document[]>(documents);
|
|
348
|
+
// const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
349
|
+
// const [dragging, setDragging] = useState<{ itemIds: string[] } | null>(null);
|
|
350
|
+
// const [canvasReady, setCanvasReady] = useState(false);
|
|
351
|
+
// const nodeClipboardRef = useRef<{ tasks: Task[]; documents: Document[] }>({
|
|
352
|
+
// tasks: [],
|
|
353
|
+
// documents: [],
|
|
354
|
+
// });
|
|
355
|
+
// const dragStateRef = useRef<DragState>({
|
|
356
|
+
// isDragging: false,
|
|
357
|
+
// itemIds: [],
|
|
358
|
+
// startPositions: new Map(),
|
|
359
|
+
// canvasObjectsStartPos: new Map(),
|
|
360
|
+
// offsetX: 0,
|
|
361
|
+
// offsetY: 0,
|
|
362
|
+
// });
|
|
363
|
+
// const rafIdRef = useRef<number | null>(null);
|
|
364
|
+
// const overlayRef = useRef<HTMLDivElement>(null);
|
|
365
|
+
// const selectedIdsRef = useRef<Set<string>>(selectedIds);
|
|
366
|
+
// selectedIdsRef.current = selectedIds;
|
|
367
|
+
// // ── Sync props → local state ────────────────────────────────────────────────
|
|
368
|
+
// useEffect(() => { setLocalTasks(tasks); }, [tasks]);
|
|
369
|
+
// useEffect(() => { setLocalDocuments(documents); }, [documents]);
|
|
370
|
+
// // effect — polls until fabricCanvas.current is available:
|
|
371
|
+
// useEffect(() => {
|
|
372
|
+
// if (canvasReady) return;
|
|
373
|
+
// if (fabricCanvas?.current) {
|
|
374
|
+
// setCanvasReady(true);
|
|
375
|
+
// return;
|
|
376
|
+
// }
|
|
377
|
+
// // Poll every 50ms until canvas is ready (only needed on first load)
|
|
378
|
+
// const interval = setInterval(() => {
|
|
379
|
+
// if (fabricCanvas?.current) {
|
|
380
|
+
// setCanvasReady(true);
|
|
381
|
+
// clearInterval(interval);
|
|
382
|
+
// }
|
|
383
|
+
// }, 50);
|
|
384
|
+
// return () => clearInterval(interval);
|
|
385
|
+
// }, [fabricCanvas, canvasReady]);
|
|
386
|
+
// // ── Event Forwarding (Fixes Zooming on Nodes) ───────────────────────────────
|
|
387
|
+
// const handleOverlayWheel = (e: React.WheelEvent) => {
|
|
388
|
+
// if (e.ctrlKey || e.metaKey || e.shiftKey) {
|
|
389
|
+
// const canvas = fabricCanvas?.current;
|
|
390
|
+
// if (!canvas) return;
|
|
391
|
+
// const nativeEvent = e.nativeEvent;
|
|
392
|
+
// // getScenePoint handles the transformation from screen to canvas space
|
|
393
|
+
// const scenePoint = canvas.getScenePoint(nativeEvent);
|
|
394
|
+
// // Viewport point is simply the mouse position relative to the canvas element
|
|
395
|
+
// const rect = canvas.getElement().getBoundingClientRect();
|
|
396
|
+
// const viewportPoint = {
|
|
397
|
+
// x: nativeEvent.clientX - rect.left,
|
|
398
|
+
// y: nativeEvent.clientY - rect.top,
|
|
399
|
+
// };
|
|
400
|
+
// // We cast to 'any' here because we are manually triggering an internal
|
|
401
|
+
// // event bus, and Fabric's internal types for .fire() can be overly strict.
|
|
402
|
+
// canvas.fire("mouse:wheel", {
|
|
403
|
+
// e: nativeEvent,
|
|
404
|
+
// scenePoint,
|
|
405
|
+
// viewportPoint,
|
|
406
|
+
// } as any);
|
|
407
|
+
// e.preventDefault();
|
|
408
|
+
// e.stopPropagation();
|
|
409
|
+
// }
|
|
410
|
+
// };
|
|
411
|
+
// useEffect(() => {
|
|
412
|
+
// const overlayEl = overlayRef.current;
|
|
413
|
+
// const canvas = fabricCanvas?.current;
|
|
414
|
+
// if (!overlayEl || !canvas) return;
|
|
415
|
+
// const handleGlobalWheel = (e: WheelEvent) => {
|
|
416
|
+
// // Check if the user is hovering over an element that has pointer-events: auto
|
|
417
|
+
// // (meaning they are hovering over a Task or Document)
|
|
418
|
+
// const target = e.target as HTMLElement;
|
|
419
|
+
// const isOverNode = target !== overlayEl;
|
|
420
|
+
// if ((e.ctrlKey || e.metaKey) && isOverNode) {
|
|
421
|
+
// // 1. Prevent Browser Zoom immediately
|
|
422
|
+
// e.preventDefault();
|
|
423
|
+
// e.stopPropagation();
|
|
424
|
+
// // 2. Calculate coordinates for Fabric
|
|
425
|
+
// const scenePoint = canvas.getScenePoint(e);
|
|
426
|
+
// const rect = canvas.getElement().getBoundingClientRect();
|
|
427
|
+
// const viewportPoint = {
|
|
428
|
+
// x: e.clientX - rect.left,
|
|
429
|
+
// y: e.clientY - rect.top,
|
|
430
|
+
// };
|
|
431
|
+
// // 3. Manually fire the event into Fabric
|
|
432
|
+
// canvas.fire("mouse:wheel", {
|
|
433
|
+
// e: e,
|
|
434
|
+
// scenePoint,
|
|
435
|
+
// viewportPoint,
|
|
436
|
+
// } as any);
|
|
437
|
+
// }
|
|
438
|
+
// };
|
|
439
|
+
// // CRITICAL: { passive: false } allows us to cancel the browser's zoom
|
|
440
|
+
// overlayEl.addEventListener("wheel", handleGlobalWheel, { passive: false });
|
|
441
|
+
// return () => {
|
|
442
|
+
// overlayEl.removeEventListener("wheel", handleGlobalWheel);
|
|
443
|
+
// };
|
|
444
|
+
// }, [fabricCanvas, canvasZoom ,canvasReady]); // Re-bind when zoom changes to keep closure fresh
|
|
445
|
+
// // ── Fabric → Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
|
|
446
|
+
// useEffect(() => {
|
|
447
|
+
// const canvas = fabricCanvas?.current;
|
|
448
|
+
// if (!canvas) return;
|
|
449
|
+
// const handleObjectMoving = (e: any) => {
|
|
450
|
+
// const target = e.transform?.target || e.target;
|
|
451
|
+
// if (!target) return;
|
|
452
|
+
// const deltaX = target.left - (target._prevLeft ?? target.left);
|
|
453
|
+
// const deltaY = target.top - (target._prevTop ?? target.top);
|
|
454
|
+
// target._prevLeft = target.left;
|
|
455
|
+
// target._prevTop = target.top;
|
|
456
|
+
// if (deltaX === 0 && deltaY === 0) return;
|
|
457
|
+
// // ── Read from ref — always fresh, never stale ──
|
|
458
|
+
// const sel = selectedIdsRef.current;
|
|
459
|
+
// setLocalTasks((prev) =>
|
|
460
|
+
// prev.map((t) => sel.has(t.id) ? { ...t, x: t.x + deltaX, y: t.y + deltaY } : t)
|
|
461
|
+
// );
|
|
462
|
+
// setLocalDocuments((prev) =>
|
|
463
|
+
// prev.map((d) => sel.has(d.id) ? { ...d, x: d.x + deltaX, y: d.y + deltaY } : d)
|
|
464
|
+
// );
|
|
465
|
+
// };
|
|
466
|
+
// const handleMouseDown = (e: any) => {
|
|
467
|
+
// const target = e.target;
|
|
468
|
+
// if (target) {
|
|
469
|
+
// target._prevLeft = target.left;
|
|
470
|
+
// target._prevTop = target.top;
|
|
471
|
+
// }
|
|
472
|
+
// if (!target) {
|
|
473
|
+
// setSelectedIds(new Set());
|
|
474
|
+
// return;
|
|
475
|
+
// }
|
|
476
|
+
// // At zoom=1 with identity VPT, getActiveObject() can return null before
|
|
477
|
+
// // Fabric updates _activeObject. Use e.transform as the primary check —
|
|
478
|
+
// // it is populated by Fabric's hit-test regardless of zoom level.
|
|
479
|
+
// const transformTarget = e.transform?.target;
|
|
480
|
+
// const activeObject = canvas.getActiveObject();
|
|
481
|
+
// const activeObjects = canvas.getActiveObjects();
|
|
482
|
+
// const isPartOfActiveSelection =
|
|
483
|
+
// transformTarget === target || // most reliable — direct from event
|
|
484
|
+
// activeObject === target || // selection box group
|
|
485
|
+
// activeObjects.includes(target); // individual object in multi-select
|
|
486
|
+
// if (!isPartOfActiveSelection) {
|
|
487
|
+
// setSelectedIds(new Set());
|
|
488
|
+
// }
|
|
489
|
+
// };
|
|
490
|
+
// const handleSelectionCleared = () => {
|
|
491
|
+
// setSelectedIds(new Set());
|
|
492
|
+
// };
|
|
493
|
+
// canvas.on("object:moving", handleObjectMoving);
|
|
494
|
+
// canvas.on("mouse:down", handleMouseDown);
|
|
495
|
+
// canvas.on("selection:cleared", handleSelectionCleared);
|
|
496
|
+
// return () => {
|
|
497
|
+
// canvas.off("object:moving", handleObjectMoving);
|
|
498
|
+
// canvas.off("mouse:down", handleMouseDown);
|
|
499
|
+
// canvas.off("selection:cleared", handleSelectionCleared);
|
|
500
|
+
// };
|
|
501
|
+
// // ── selectedIds REMOVED from deps — read via selectedIdsRef instead ──────
|
|
502
|
+
// // Having selectedIds here caused the effect to re-register on every selection
|
|
503
|
+
// // change, creating a new closure each time. The second drag captured a stale
|
|
504
|
+
// // or empty selectedIds from the closure at re-registration time.
|
|
505
|
+
// }, [canvasZoom, fabricCanvas ,canvasReady]);
|
|
506
|
+
// // ── Helpers ─────────────────────────────────────────────────────────────────
|
|
507
|
+
// const getItemPosition = (id: string): { x: number; y: number } | undefined => {
|
|
508
|
+
// const task = localTasks.find((t) => t.id === id);
|
|
509
|
+
// if (task) return { x: task.x, y: task.y };
|
|
510
|
+
// const doc = localDocuments.find((d) => d.id === id);
|
|
511
|
+
// if (doc) return { x: doc.x, y: doc.y };
|
|
512
|
+
// return undefined;
|
|
513
|
+
// };
|
|
514
|
+
// const isItemInSelectionBox = (
|
|
515
|
+
// x: number,
|
|
516
|
+
// y: number,
|
|
517
|
+
// width: number,
|
|
518
|
+
// height: number,
|
|
519
|
+
// box: { x1: number; y1: number; x2: number; y2: number }
|
|
520
|
+
// ) => {
|
|
521
|
+
// const itemX1 = x * canvasZoom + canvasViewport.x;
|
|
522
|
+
// const itemY1 = y * canvasZoom + canvasViewport.y;
|
|
523
|
+
// const itemX2 = itemX1 + width * canvasZoom;
|
|
524
|
+
// const itemY2 = itemY1 + height * canvasZoom;
|
|
525
|
+
// const boxX1 = Math.min(box.x1, box.x2);
|
|
526
|
+
// const boxY1 = Math.min(box.y1, box.y2);
|
|
527
|
+
// const boxX2 = Math.max(box.x1, box.x2);
|
|
528
|
+
// const boxY2 = Math.max(box.y1, box.y2);
|
|
529
|
+
// return !(boxX2 < itemX1 || boxX1 > itemX2 || boxY2 < itemY1 || boxY1 > itemY2);
|
|
530
|
+
// };
|
|
531
|
+
// // ── Selection box detection ──────────────────────────────────────────────────
|
|
532
|
+
// useEffect(() => {
|
|
533
|
+
// if (!selectionBox) return;
|
|
534
|
+
// // ── O(n) single pass — no sort, no join, no extra allocations ──
|
|
535
|
+
// const newSelected = new Set<string>();
|
|
536
|
+
// for (const task of localTasks) {
|
|
537
|
+
// if (isItemInSelectionBox(task.x, task.y, 300, 140, selectionBox))
|
|
538
|
+
// newSelected.add(task.id);
|
|
539
|
+
// }
|
|
540
|
+
// for (const doc of localDocuments) {
|
|
541
|
+
// if (isItemInSelectionBox(doc.x, doc.y, 320, 160, selectionBox))
|
|
542
|
+
// newSelected.add(doc.id);
|
|
543
|
+
// }
|
|
544
|
+
// // ── O(n) equality check: size first (fast path), then membership ──
|
|
545
|
+
// setSelectedIds((prev) => {
|
|
546
|
+
// if (prev.size !== newSelected.size) return newSelected;
|
|
547
|
+
// for (const id of newSelected) {
|
|
548
|
+
// if (!prev.has(id)) return newSelected; // found a difference, swap
|
|
549
|
+
// }
|
|
550
|
+
// return prev; // identical — return same reference, no re-render
|
|
551
|
+
// });
|
|
552
|
+
// }, [selectionBox, localTasks, localDocuments, canvasZoom, canvasViewport]);
|
|
553
|
+
// // ── Drag start (HTML Node side) ──────────────────────────────────────────────
|
|
554
|
+
// // Helper to extract coordinates regardless of event type
|
|
555
|
+
// const getPointerEvent = (e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent) => {
|
|
556
|
+
// if ('touches' in e && e.touches.length > 0) return e.touches[0];
|
|
557
|
+
// return e as { clientX: number; clientY: number };
|
|
558
|
+
// };
|
|
559
|
+
// const handleDragStart = (itemId: string, e: React.MouseEvent | React.TouchEvent) => {
|
|
560
|
+
// // 1. Safety check for the Fabric instance
|
|
561
|
+
// const canvas = fabricCanvas?.current;
|
|
562
|
+
// if (!canvas) return;
|
|
563
|
+
// // 2. Normalize the event (Touch vs Mouse)
|
|
564
|
+
// if (e.cancelable) e.preventDefault();
|
|
565
|
+
// const pointer = getPointerEvent(e);
|
|
566
|
+
// // 3. Determine which items are being dragged
|
|
567
|
+
// // selection update DOES NOT trigger before drag snapshot
|
|
568
|
+
// let itemsToDrag: string[];
|
|
569
|
+
// if (selectedIds.has(itemId)) {
|
|
570
|
+
// itemsToDrag = Array.from(selectedIds);
|
|
571
|
+
// } else {
|
|
572
|
+
// itemsToDrag = [itemId];
|
|
573
|
+
// }
|
|
574
|
+
// // 4. Capture current World Transform (Zoom & Pan)
|
|
575
|
+
// // We read directly from the canvas to ensure zero-frame lag
|
|
576
|
+
// const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
|
|
577
|
+
// const liveZoom = vpt[0];
|
|
578
|
+
// const liveVpX = vpt[4];
|
|
579
|
+
// const liveVpY = vpt[5];
|
|
580
|
+
// // 5. Convert the Click Position from Screen Pixels to World Units
|
|
581
|
+
// const clickWorldX = (pointer.clientX - liveVpX) / liveZoom;
|
|
582
|
+
// const clickWorldY = (pointer.clientY - liveVpY) / liveZoom;
|
|
583
|
+
// // 6. Get the clicked item's current World Position
|
|
584
|
+
// const clickedPos = getItemPosition(itemId);
|
|
585
|
+
// if (!clickedPos) return;
|
|
586
|
+
// // 7. Calculate the Offset in WORLD UNITS
|
|
587
|
+
// // This is the distance from the mouse to the node's top-left in the infinite grid.
|
|
588
|
+
// // This value remains constant even if you zoom during the drag.
|
|
589
|
+
// const worldOffsetX = clickWorldX - clickedPos.x;
|
|
590
|
+
// const worldOffsetY = clickWorldY - clickedPos.y;
|
|
591
|
+
// // 8. Snapshot starting positions for all selected HTML nodes
|
|
592
|
+
// const startPositions = new Map<string, { x: number; y: number }>();
|
|
593
|
+
// itemsToDrag.forEach((id) => {
|
|
594
|
+
// const pos = getItemPosition(id);
|
|
595
|
+
// if (pos) startPositions.set(id, pos);
|
|
596
|
+
// });
|
|
597
|
+
// // 9. Snapshot starting positions for all selected Fabric objects
|
|
598
|
+
// const canvasObjectsStartPos = new Map<FabricObject, { left: number; top: number }>();
|
|
599
|
+
// selectedCanvasObjects.forEach((obj) => {
|
|
600
|
+
// canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
|
|
601
|
+
// });
|
|
602
|
+
// // 10. Commit to the ref for the requestAnimationFrame loop
|
|
603
|
+
// dragStateRef.current = {
|
|
604
|
+
// isDragging: true,
|
|
605
|
+
// itemIds: itemsToDrag,
|
|
606
|
+
// startPositions,
|
|
607
|
+
// canvasObjectsStartPos,
|
|
608
|
+
// offsetX: clickWorldX, // Now stored as World Units
|
|
609
|
+
// offsetY: clickWorldY, // Now stored as World Units
|
|
610
|
+
// };
|
|
611
|
+
// if (!selectedIds.has(itemId) && dragStateRef.current.itemIds.length === 0) {
|
|
612
|
+
// setSelectedIds(new Set([itemId]));
|
|
613
|
+
// }
|
|
614
|
+
// // 11. Trigger UI states
|
|
615
|
+
// setDragging({ itemIds: itemsToDrag });
|
|
616
|
+
// document.body.style.cursor = "grabbing";
|
|
617
|
+
// document.body.style.userSelect = "none";
|
|
618
|
+
// document.body.style.touchAction = "none";
|
|
619
|
+
// };
|
|
620
|
+
// // ── Drag move (HTML Node side) ───────────────────────────────────────────────
|
|
621
|
+
// useEffect(() => {
|
|
622
|
+
// if (!dragging) return;
|
|
623
|
+
// // Inside the useEffect that watches [dragging, localTasks, localDocuments, fabricCanvas]
|
|
624
|
+
// const handleMove = (e: MouseEvent | TouchEvent) => {
|
|
625
|
+
// if (!dragStateRef.current.isDragging) return;
|
|
626
|
+
// if (e.cancelable) e.preventDefault();
|
|
627
|
+
// const pointer = getPointerEvent(e);
|
|
628
|
+
// // 1. Throttle updates using requestAnimationFrame for 120Hz/144Hz screen support
|
|
629
|
+
// if (rafIdRef.current !== null) cancelAnimationFrame(rafIdRef.current);
|
|
630
|
+
// rafIdRef.current = requestAnimationFrame(() => {
|
|
631
|
+
// const { itemIds, startPositions, canvasObjectsStartPos, offsetX, offsetY } = dragStateRef.current;
|
|
632
|
+
// const canvas = fabricCanvas?.current;
|
|
633
|
+
// if (!canvas) return;
|
|
634
|
+
// // 2. Read the "Source of Truth" transform from the canvas
|
|
635
|
+
// const vpt = canvas.viewportTransform!;
|
|
636
|
+
// const liveZoom = vpt[0]; // Scale
|
|
637
|
+
// const liveVpX = vpt[4]; // Pan X
|
|
638
|
+
// const liveVpY = vpt[5]; // Pan Y
|
|
639
|
+
// // 3. Convert current Mouse Screen Position → World Position
|
|
640
|
+
// const currentWorldX = (pointer.clientX - liveVpX) / liveZoom;
|
|
641
|
+
// const currentWorldY = (pointer.clientY - liveVpY) / liveZoom;
|
|
642
|
+
// // 4. Calculate where the "Anchor" node should be in World Units
|
|
643
|
+
// // (Current Mouse World - Initial World Offset from Start)
|
|
644
|
+
// const deltaX = currentWorldX - offsetX;
|
|
645
|
+
// const deltaY = currentWorldY - offsetY;
|
|
646
|
+
// // if (itemIds.length > 0) {
|
|
647
|
+
// // console.groupCollapsed(`Dragging Node: ${itemIds[0]}`);
|
|
648
|
+
// // console.log("Screen Mouse:", { x: pointer.clientX, y: pointer.clientY });
|
|
649
|
+
// // console.log("World Mouse:", { x: currentWorldX.toFixed(2), y: currentWorldY.toFixed(2) });
|
|
650
|
+
// // console.log("Canvas Zoom:", liveZoom.toFixed(2));
|
|
651
|
+
// // console.log("New Node Pos:", { x: deltaX.toFixed(2), y: deltaY.toFixed(2) });
|
|
652
|
+
// // console.groupEnd();
|
|
653
|
+
// // }
|
|
654
|
+
// // 5. Calculate the Movement Delta in World Units
|
|
655
|
+
// // We compare where the first item started vs where it is now.
|
|
656
|
+
// const firstId = itemIds[0];
|
|
657
|
+
// const firstStart = startPositions.get(firstId);
|
|
658
|
+
// if (!firstStart) return;
|
|
659
|
+
// // The real problem of task jumps
|
|
660
|
+
// // const deltaX = deltaX - firstStart.x;
|
|
661
|
+
// // 6. Update HTML Nodes (Batching these into one state update)
|
|
662
|
+
// setLocalTasks((prev) =>
|
|
663
|
+
// prev.map((t) => itemIds.includes(t.id) ? {
|
|
664
|
+
// ...t,
|
|
665
|
+
// x: (startPositions.get(t.id)?.x ?? t.x) + deltaX,
|
|
666
|
+
// y: (startPositions.get(t.id)?.y ?? t.y) + deltaY,
|
|
667
|
+
// } : t)
|
|
668
|
+
// );
|
|
669
|
+
// setLocalDocuments((prev) =>
|
|
670
|
+
// prev.map((d) => itemIds.includes(d.id) ? {
|
|
671
|
+
// ...d,
|
|
672
|
+
// x: (startPositions.get(d.id)?.x ?? d.x) + deltaX,
|
|
673
|
+
// y: (startPositions.get(d.id)?.y ?? d.y) + deltaY,
|
|
674
|
+
// } : d)
|
|
675
|
+
// );
|
|
676
|
+
// // 7. Sync Fabric Objects (Imperative update for performance)
|
|
677
|
+
// canvasObjectsStartPos.forEach((startPos, obj) => {
|
|
678
|
+
// obj.set({
|
|
679
|
+
// left: startPos.left + deltaX,
|
|
680
|
+
// top: startPos.top + deltaY,
|
|
681
|
+
// });
|
|
682
|
+
// obj.setCoords(); // Required for selection/intersection accuracy
|
|
683
|
+
// });
|
|
684
|
+
// // 8. Single render call for all Fabric changes
|
|
685
|
+
// canvas.requestRenderAll();
|
|
686
|
+
// });
|
|
687
|
+
// };
|
|
688
|
+
// const handleEnd = () => {
|
|
689
|
+
// if (rafIdRef.current !== null) cancelAnimationFrame(rafIdRef.current);
|
|
690
|
+
// dragStateRef.current.isDragging = false;
|
|
691
|
+
// setDragging(null);
|
|
692
|
+
// document.body.style.cursor = "";
|
|
693
|
+
// document.body.style.userSelect = "";
|
|
694
|
+
// document.body.style.touchAction = "";
|
|
695
|
+
// onTasksUpdate?.(localTasks);
|
|
696
|
+
// onDocumentsUpdate?.(localDocuments);
|
|
697
|
+
// };
|
|
698
|
+
// window.addEventListener("mousemove", handleMove, { passive: false });
|
|
699
|
+
// window.addEventListener("mouseup", handleEnd);
|
|
700
|
+
// window.addEventListener("touchmove", handleMove, { passive: false });
|
|
701
|
+
// window.addEventListener("touchend", handleEnd);
|
|
702
|
+
// window.addEventListener("touchcancel", handleEnd);
|
|
703
|
+
// return () => {
|
|
704
|
+
// window.removeEventListener("mousemove", handleMove);
|
|
705
|
+
// window.removeEventListener("mouseup", handleEnd);
|
|
706
|
+
// window.removeEventListener("touchmove", handleMove);
|
|
707
|
+
// window.removeEventListener("touchend", handleEnd);
|
|
708
|
+
// window.removeEventListener("touchcancel", handleEnd);
|
|
709
|
+
// };
|
|
710
|
+
// }, [dragging, localTasks, localDocuments, fabricCanvas]);
|
|
711
|
+
// // ── Selection, Status, Keyboard Logic ────────────────────────────────────────
|
|
712
|
+
// const handleSelect = (id: string, e?: React.MouseEvent) => {
|
|
713
|
+
// if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
|
|
714
|
+
// setSelectedIds((prev) => {
|
|
715
|
+
// const next = new Set(prev);
|
|
716
|
+
// next.has(id) ? next.delete(id) : next.add(id);
|
|
717
|
+
// return next;
|
|
718
|
+
// });
|
|
719
|
+
// } else {
|
|
720
|
+
// setSelectedIds(new Set([id]));
|
|
721
|
+
// }
|
|
722
|
+
// };
|
|
723
|
+
// const handleStatusChange = (taskId: string, newStatus: "todo" | "in-progress" | "done") => {
|
|
724
|
+
// const updated = localTasks.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t));
|
|
725
|
+
// setLocalTasks(updated);
|
|
726
|
+
// onTasksUpdate?.(updated);
|
|
727
|
+
// };
|
|
728
|
+
// useEffect(() => {
|
|
729
|
+
// const handleKeyDown = (e: KeyboardEvent) => {
|
|
730
|
+
// // Don't trigger if typing in input
|
|
731
|
+
// if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
732
|
+
// // Select All
|
|
733
|
+
// if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
|
734
|
+
// e.preventDefault();
|
|
735
|
+
// setSelectedIds(new Set([...localTasks.map((t) => t.id), ...localDocuments.map((d) => d.id)]));
|
|
736
|
+
// }
|
|
737
|
+
// // Clear selection
|
|
738
|
+
// if (e.key === "Escape") {
|
|
739
|
+
// setSelectedIds(new Set());
|
|
740
|
+
// }
|
|
741
|
+
// // ← ADD THIS: Delete selected nodes
|
|
742
|
+
// if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
|
|
743
|
+
// e.preventDefault();
|
|
744
|
+
// const updatedTasks = localTasks.filter((t) => !selectedIds.has(t.id));
|
|
745
|
+
// const updatedDocs = localDocuments.filter((d) => !selectedIds.has(d.id));
|
|
746
|
+
// setLocalTasks(updatedTasks);
|
|
747
|
+
// setLocalDocuments(updatedDocs);
|
|
748
|
+
// setSelectedIds(new Set());
|
|
749
|
+
// onTasksUpdate?.(updatedTasks);
|
|
750
|
+
// onDocumentsUpdate?.(updatedDocs);
|
|
751
|
+
// }
|
|
752
|
+
// };
|
|
753
|
+
// window.addEventListener("keydown", handleKeyDown);
|
|
754
|
+
// return () => window.removeEventListener("keydown", handleKeyDown);
|
|
755
|
+
// }, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
|
|
756
|
+
// // ── Render helper ────────────────────────────────────────────────────────────
|
|
757
|
+
// const renderItem = (id: string, x: number, y: number, children: React.ReactNode) => {
|
|
758
|
+
// const screenX = x * canvasZoom;
|
|
759
|
+
// const screenY = y * canvasZoom;
|
|
760
|
+
// // 1. Detect if the user is interacting with the canvas at all
|
|
761
|
+
// // 'dragging' is your existing state.
|
|
762
|
+
// // You might want to pass 'isZooming' or 'isPanning' from your main canvas component here.
|
|
763
|
+
// const isDragging = dragging?.itemIds.includes(id);
|
|
764
|
+
// return (
|
|
765
|
+
// <div
|
|
766
|
+
// key={id}
|
|
767
|
+
// className="pointer-events-auto absolute"
|
|
768
|
+
// style={{
|
|
769
|
+
// left: 0,
|
|
770
|
+
// top: 0,
|
|
771
|
+
// // 2. Use translate3d for GPU performance
|
|
772
|
+
// transform: `translate3d(${screenX}px, ${screenY}px, 0) scale(${canvasZoom})`,
|
|
773
|
+
// transformOrigin: "top left",
|
|
774
|
+
// // 3. THE FIX: Remove transition entirely during any viewport change
|
|
775
|
+
// // Any 'ease' during zoom causes the "shaking" behavior.
|
|
776
|
+
// transition: "none",
|
|
777
|
+
// // 4. Optimization
|
|
778
|
+
// willChange: "transform",
|
|
779
|
+
// zIndex: isDragging ? 1000 : 1,
|
|
780
|
+
// }}
|
|
781
|
+
// >
|
|
782
|
+
// {children}
|
|
783
|
+
// </div>
|
|
784
|
+
// );
|
|
785
|
+
// };
|
|
786
|
+
// return (
|
|
787
|
+
// <div
|
|
788
|
+
// ref={overlayRef}
|
|
789
|
+
// className="absolute inset-0 pointer-events-none"
|
|
790
|
+
// style={{ zIndex: 50 }}
|
|
791
|
+
// onWheel={handleOverlayWheel}
|
|
792
|
+
// onClick={(e) => {
|
|
793
|
+
// if (e.target === e.currentTarget) setSelectedIds(new Set());
|
|
794
|
+
// }}
|
|
795
|
+
// >
|
|
796
|
+
// <div
|
|
797
|
+
// className="absolute top-0 left-0 pointer-events-none"
|
|
798
|
+
// style={{
|
|
799
|
+
// transform: `translate(${canvasViewport.x}px, ${canvasViewport.y}px)`,
|
|
800
|
+
// transformOrigin: "top left",
|
|
801
|
+
// }}
|
|
802
|
+
// >
|
|
803
|
+
// {localTasks.map((task) =>
|
|
804
|
+
// renderItem(task.id, task.x, task.y,
|
|
805
|
+
// <TaskNode
|
|
806
|
+
// {...task}
|
|
807
|
+
// isSelected={selectedIds.has(task.id)}
|
|
808
|
+
// onSelect={handleSelect}
|
|
809
|
+
// onDragStart={handleDragStart}
|
|
810
|
+
// onStatusChange={handleStatusChange}
|
|
811
|
+
// zoom={1}
|
|
812
|
+
// />
|
|
813
|
+
// )
|
|
814
|
+
// )}
|
|
815
|
+
// {localDocuments.map((doc) =>
|
|
816
|
+
// renderItem(doc.id, doc.x, doc.y,
|
|
817
|
+
// <DocumentNode
|
|
818
|
+
// {...doc}
|
|
819
|
+
// isSelected={selectedIds.has(doc.id)}
|
|
820
|
+
// onSelect={handleSelect}
|
|
821
|
+
// onDragStart={handleDragStart}
|
|
822
|
+
// />
|
|
823
|
+
// )
|
|
824
|
+
// )}
|
|
825
|
+
// </div>
|
|
826
|
+
// </div>
|
|
827
|
+
// );
|
|
828
|
+
// }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-node-overlay-layer.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node-overlay-layer.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"custom-node-overlay-layer.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node-overlay-layer.tsx"],"names":[],"mappings":"AAIA,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,2CAglBzB"}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect, useRef
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
4
|
import TaskNode from "./custom-node";
|
|
5
5
|
import DocumentNode from "./document-node";
|
|
6
|
-
import { memo } from "react";
|
|
7
|
-
// Wrap TaskNode and DocumentNode
|
|
8
|
-
const MemoTaskNode = memo(TaskNode);
|
|
9
|
-
const MemoDocumentNode = memo(DocumentNode);
|
|
10
6
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
11
7
|
export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, onDocumentsUpdate, canvasZoom = 1, canvasViewport = { x: 0, y: 0 }, selectionBox = null, selectedCanvasObjects = [], fabricCanvas, }) {
|
|
12
8
|
const [localTasks, setLocalTasks] = useState(tasks);
|
|
@@ -18,13 +14,6 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
18
14
|
tasks: [],
|
|
19
15
|
documents: [],
|
|
20
16
|
});
|
|
21
|
-
// In render, replace TaskNode/DocumentNode with memoized versions
|
|
22
|
-
{
|
|
23
|
-
localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(MemoTaskNode, { ...task, isSelected: selectedIds.has(task.id), onSelect: handleSelect, onDragStart: handleDragStart, onStatusChange: handleStatusChange, zoom: 1 })));
|
|
24
|
-
}
|
|
25
|
-
{
|
|
26
|
-
localDocuments.map((doc) => renderItem(doc.id, doc.x, doc.y, _jsx(MemoDocumentNode, { ...doc, isSelected: selectedIds.has(doc.id), onSelect: handleSelect, onDragStart: handleDragStart })));
|
|
27
|
-
}
|
|
28
17
|
const dragStateRef = useRef({
|
|
29
18
|
isDragging: false,
|
|
30
19
|
itemIds: [],
|
|
@@ -36,16 +25,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
36
25
|
const rafIdRef = useRef(null);
|
|
37
26
|
const overlayRef = useRef(null);
|
|
38
27
|
const selectedIdsRef = useRef(selectedIds);
|
|
39
|
-
// Add these refs
|
|
40
|
-
const localTasksRef = useRef(localTasks);
|
|
41
|
-
const localDocsRef = useRef(localDocuments);
|
|
42
|
-
const onTasksUpdateRef = useRef(onTasksUpdate);
|
|
43
|
-
const onDocsUpdateRef = useRef(onDocumentsUpdate);
|
|
44
28
|
selectedIdsRef.current = selectedIds;
|
|
45
|
-
localTasksRef.current = localTasks;
|
|
46
|
-
localDocsRef.current = localDocuments;
|
|
47
|
-
onTasksUpdateRef.current = onTasksUpdate;
|
|
48
|
-
onDocsUpdateRef.current = onDocumentsUpdate;
|
|
49
29
|
// ── Sync props → local state ────────────────────────────────────────────────
|
|
50
30
|
useEffect(() => { setLocalTasks(tasks); }, [tasks]);
|
|
51
31
|
useEffect(() => { setLocalDocuments(documents); }, [documents]);
|
|
@@ -126,7 +106,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
126
106
|
return () => {
|
|
127
107
|
overlayEl.removeEventListener("wheel", handleGlobalWheel);
|
|
128
108
|
};
|
|
129
|
-
}, [fabricCanvas, canvasReady]); // Re-bind when zoom changes to keep closure fresh
|
|
109
|
+
}, [fabricCanvas, canvasZoom, canvasReady]); // Re-bind when zoom changes to keep closure fresh
|
|
130
110
|
// ── Fabric → Overlay Sync (Fixes Dragging from Fabric area) ──────────────────
|
|
131
111
|
useEffect(() => {
|
|
132
112
|
const canvas = fabricCanvas?.current;
|
|
@@ -185,7 +165,7 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
185
165
|
// Having selectedIds here caused the effect to re-register on every selection
|
|
186
166
|
// change, creating a new closure each time. The second drag captured a stale
|
|
187
167
|
// or empty selectedIds from the closure at re-registration time.
|
|
188
|
-
}, [fabricCanvas, canvasReady]);
|
|
168
|
+
}, [canvasZoom, fabricCanvas, canvasReady]);
|
|
189
169
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
190
170
|
const getItemPosition = (id) => {
|
|
191
171
|
const task = localTasks.find((t) => t.id === id);
|
|
@@ -239,61 +219,72 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
239
219
|
return e.touches[0];
|
|
240
220
|
return e;
|
|
241
221
|
};
|
|
242
|
-
const handleDragStart =
|
|
222
|
+
const handleDragStart = (itemId, e) => {
|
|
223
|
+
// 1. Safety check for the Fabric instance
|
|
243
224
|
const canvas = fabricCanvas?.current;
|
|
244
225
|
if (!canvas)
|
|
245
226
|
return;
|
|
227
|
+
// 2. Normalize the event (Touch vs Mouse)
|
|
246
228
|
if (e.cancelable)
|
|
247
229
|
e.preventDefault();
|
|
248
230
|
const pointer = getPointerEvent(e);
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
let itemsToDrag
|
|
252
|
-
|
|
253
|
-
|
|
231
|
+
// 3. Determine which items are being dragged
|
|
232
|
+
// selection update DOES NOT trigger before drag snapshot
|
|
233
|
+
let itemsToDrag;
|
|
234
|
+
if (selectedIds.has(itemId)) {
|
|
235
|
+
itemsToDrag = Array.from(selectedIds);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
itemsToDrag = [itemId];
|
|
239
|
+
}
|
|
240
|
+
// 4. Capture current World Transform (Zoom & Pan)
|
|
241
|
+
// We read directly from the canvas to ensure zero-frame lag
|
|
254
242
|
const vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
|
|
255
243
|
const liveZoom = vpt[0];
|
|
256
244
|
const liveVpX = vpt[4];
|
|
257
245
|
const liveVpY = vpt[5];
|
|
246
|
+
// 5. Convert the Click Position from Screen Pixels to World Units
|
|
258
247
|
const clickWorldX = (pointer.clientX - liveVpX) / liveZoom;
|
|
259
248
|
const clickWorldY = (pointer.clientY - liveVpY) / liveZoom;
|
|
260
|
-
//
|
|
261
|
-
const
|
|
262
|
-
const t = localTasksRef.current.find((t) => t.id === id);
|
|
263
|
-
if (t)
|
|
264
|
-
return { x: t.x, y: t.y };
|
|
265
|
-
const d = localDocsRef.current.find((d) => d.id === id);
|
|
266
|
-
if (d)
|
|
267
|
-
return { x: d.x, y: d.y };
|
|
268
|
-
};
|
|
269
|
-
const clickedPos = getPos(itemId);
|
|
249
|
+
// 6. Get the clicked item's current World Position
|
|
250
|
+
const clickedPos = getItemPosition(itemId);
|
|
270
251
|
if (!clickedPos)
|
|
271
252
|
return;
|
|
253
|
+
// 7. Calculate the Offset in WORLD UNITS
|
|
254
|
+
// This is the distance from the mouse to the node's top-left in the infinite grid.
|
|
255
|
+
// This value remains constant even if you zoom during the drag.
|
|
256
|
+
const worldOffsetX = clickWorldX - clickedPos.x;
|
|
257
|
+
const worldOffsetY = clickWorldY - clickedPos.y;
|
|
258
|
+
// 8. Snapshot starting positions for all selected HTML nodes
|
|
272
259
|
const startPositions = new Map();
|
|
273
260
|
itemsToDrag.forEach((id) => {
|
|
274
|
-
const pos =
|
|
261
|
+
const pos = getItemPosition(id);
|
|
275
262
|
if (pos)
|
|
276
263
|
startPositions.set(id, pos);
|
|
277
264
|
});
|
|
265
|
+
// 9. Snapshot starting positions for all selected Fabric objects
|
|
278
266
|
const canvasObjectsStartPos = new Map();
|
|
279
267
|
selectedCanvasObjects.forEach((obj) => {
|
|
280
268
|
canvasObjectsStartPos.set(obj, { left: obj.left || 0, top: obj.top || 0 });
|
|
281
269
|
});
|
|
270
|
+
// 10. Commit to the ref for the requestAnimationFrame loop
|
|
282
271
|
dragStateRef.current = {
|
|
283
272
|
isDragging: true,
|
|
284
273
|
itemIds: itemsToDrag,
|
|
285
274
|
startPositions,
|
|
286
275
|
canvasObjectsStartPos,
|
|
287
|
-
offsetX: clickWorldX,
|
|
288
|
-
offsetY: clickWorldY,
|
|
276
|
+
offsetX: clickWorldX, // Now stored as World Units
|
|
277
|
+
offsetY: clickWorldY, // Now stored as World Units
|
|
289
278
|
};
|
|
290
|
-
if (!
|
|
279
|
+
if (!selectedIds.has(itemId) && dragStateRef.current.itemIds.length === 0) {
|
|
291
280
|
setSelectedIds(new Set([itemId]));
|
|
281
|
+
}
|
|
282
|
+
// 11. Trigger UI states
|
|
292
283
|
setDragging({ itemIds: itemsToDrag });
|
|
293
284
|
document.body.style.cursor = "grabbing";
|
|
294
285
|
document.body.style.userSelect = "none";
|
|
295
286
|
document.body.style.touchAction = "none";
|
|
296
|
-
}
|
|
287
|
+
};
|
|
297
288
|
// ── Drag move (HTML Node side) ───────────────────────────────────────────────
|
|
298
289
|
useEffect(() => {
|
|
299
290
|
if (!dragging)
|
|
@@ -372,8 +363,8 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
372
363
|
document.body.style.cursor = "";
|
|
373
364
|
document.body.style.userSelect = "";
|
|
374
365
|
document.body.style.touchAction = "";
|
|
375
|
-
|
|
376
|
-
|
|
366
|
+
onTasksUpdate?.(localTasks);
|
|
367
|
+
onDocumentsUpdate?.(localDocuments);
|
|
377
368
|
};
|
|
378
369
|
window.addEventListener("mousemove", handleMove, { passive: false });
|
|
379
370
|
window.addEventListener("mouseup", handleEnd);
|
|
@@ -387,9 +378,9 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
387
378
|
window.removeEventListener("touchend", handleEnd);
|
|
388
379
|
window.removeEventListener("touchcancel", handleEnd);
|
|
389
380
|
};
|
|
390
|
-
}, [dragging, fabricCanvas]);
|
|
381
|
+
}, [dragging, localTasks, localDocuments, fabricCanvas]);
|
|
391
382
|
// ── Selection, Status, Keyboard Logic ────────────────────────────────────────
|
|
392
|
-
const handleSelect =
|
|
383
|
+
const handleSelect = (id, e) => {
|
|
393
384
|
if (e?.shiftKey || e?.ctrlKey || e?.metaKey) {
|
|
394
385
|
setSelectedIds((prev) => {
|
|
395
386
|
const next = new Set(prev);
|
|
@@ -400,38 +391,41 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
400
391
|
else {
|
|
401
392
|
setSelectedIds(new Set([id]));
|
|
402
393
|
}
|
|
403
|
-
}
|
|
404
|
-
const handleStatusChange =
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
});
|
|
410
|
-
}, []);
|
|
394
|
+
};
|
|
395
|
+
const handleStatusChange = (taskId, newStatus) => {
|
|
396
|
+
const updated = localTasks.map((t) => (t.id === taskId ? { ...t, status: newStatus } : t));
|
|
397
|
+
setLocalTasks(updated);
|
|
398
|
+
onTasksUpdate?.(updated);
|
|
399
|
+
};
|
|
411
400
|
useEffect(() => {
|
|
412
401
|
const handleKeyDown = (e) => {
|
|
402
|
+
// Don't trigger if typing in input
|
|
413
403
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
|
|
414
404
|
return;
|
|
405
|
+
// Select All
|
|
415
406
|
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
|
416
407
|
e.preventDefault();
|
|
417
|
-
setSelectedIds(new Set([...
|
|
408
|
+
setSelectedIds(new Set([...localTasks.map((t) => t.id), ...localDocuments.map((d) => d.id)]));
|
|
418
409
|
}
|
|
419
|
-
|
|
410
|
+
// Clear selection
|
|
411
|
+
if (e.key === "Escape") {
|
|
420
412
|
setSelectedIds(new Set());
|
|
421
|
-
|
|
413
|
+
}
|
|
414
|
+
// ← ADD THIS: Delete selected nodes
|
|
415
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
|
|
422
416
|
e.preventDefault();
|
|
423
|
-
const updatedTasks =
|
|
424
|
-
const updatedDocs =
|
|
417
|
+
const updatedTasks = localTasks.filter((t) => !selectedIds.has(t.id));
|
|
418
|
+
const updatedDocs = localDocuments.filter((d) => !selectedIds.has(d.id));
|
|
425
419
|
setLocalTasks(updatedTasks);
|
|
426
420
|
setLocalDocuments(updatedDocs);
|
|
427
421
|
setSelectedIds(new Set());
|
|
428
|
-
|
|
429
|
-
|
|
422
|
+
onTasksUpdate?.(updatedTasks);
|
|
423
|
+
onDocumentsUpdate?.(updatedDocs);
|
|
430
424
|
}
|
|
431
425
|
};
|
|
432
426
|
window.addEventListener("keydown", handleKeyDown);
|
|
433
427
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
434
|
-
}, [
|
|
428
|
+
}, [localTasks, localDocuments, selectedIds, onTasksUpdate, onDocumentsUpdate]);
|
|
435
429
|
// ── Render helper ────────────────────────────────────────────────────────────
|
|
436
430
|
const renderItem = (id, x, y, children) => {
|
|
437
431
|
const screenX = x * canvasZoom;
|
|
@@ -460,5 +454,5 @@ export default function CanvasOverlayLayer({ tasks, documents, onTasksUpdate, on
|
|
|
460
454
|
}, children: _jsxs("div", { className: "absolute top-0 left-0 pointer-events-none", style: {
|
|
461
455
|
transform: `translate(${canvasViewport.x}px, ${canvasViewport.y}px)`,
|
|
462
456
|
transformOrigin: "top left",
|
|
463
|
-
}, children: [localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(
|
|
457
|
+
}, children: [localTasks.map((task) => renderItem(task.id, task.x, task.y, _jsx(TaskNode, { ...task, isSelected: selectedIds.has(task.id), onSelect: handleSelect, onDragStart: handleDragStart, onStatusChange: handleStatusChange, zoom: 1 }))), localDocuments.map((doc) => renderItem(doc.id, doc.x, doc.y, _jsx(DocumentNode, { ...doc, isSelected: selectedIds.has(doc.id), onSelect: handleSelect, onDragStart: handleDragStart })))] }) }));
|
|
464
458
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePersistance.d.ts","sourceRoot":"","sources":["../../src/hooks/usePersistance.ts"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,GAAG,EAAE,CAAC;IACb,SAAS,EAAE,GAAG,EAAE,CAAC;CAClB;AAED,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;IAG7C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAElD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID,eAAO,MAAM,cAAc,GAAI,8GAS5B,mBAAmB,
|
|
1
|
+
{"version":3,"file":"usePersistance.d.ts","sourceRoot":"","sources":["../../src/hooks/usePersistance.ts"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,GAAG,EAAE,CAAC;IACb,SAAS,EAAE,GAAG,EAAE,CAAC;CAClB;AAED,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;IAG7C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAElD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAID,eAAO,MAAM,cAAc,GAAI,8GAS5B,mBAAmB,SA+GrB,CAAC"}
|
|
@@ -31,8 +31,6 @@ export const usePersistence = ({ fabricCanvas, tasks, documents, pushHistory, is
|
|
|
31
31
|
tasks: tasksRef.current,
|
|
32
32
|
documents: documentsRef.current,
|
|
33
33
|
};
|
|
34
|
-
// 🔍 ADD THIS LOG HERE:
|
|
35
|
-
console.log("🚀 FINAL WHITEBOARD STRUCTURE:", payload);
|
|
36
34
|
// Push to undo/redo history
|
|
37
35
|
pushHistoryRef.current(canvasJson);
|
|
38
36
|
// Notify consumer
|