@mhamz.01/easyflow-whiteboard 2.168.0 → 2.171.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-layer.d.ts +2 -43
- package/dist/components/node/custom-node-overlay-layer.d.ts.map +1 -1
- package/dist/components/node/custom-node-overlay-layer.js +89 -568
- package/dist/components/node/custom-node.d.ts +2 -5
- package/dist/components/node/custom-node.d.ts.map +1 -1
- package/dist/components/node/custom-node.js +11 -22
- package/dist/components/node/document-node.d.ts +2 -5
- package/dist/components/node/document-node.d.ts.map +1 -1
- package/dist/components/node/document-node.js +25 -42
- package/dist/components/node/hooks/useFabricSync.d.ts +41 -0
- package/dist/components/node/hooks/useFabricSync.d.ts.map +1 -0
- package/dist/components/node/hooks/useFabricSync.js +129 -0
- package/dist/components/node/hooks/useKeyboardShortcuts.d.ts +22 -8
- package/dist/components/node/hooks/useKeyboardShortcuts.d.ts.map +1 -1
- package/dist/components/node/hooks/useKeyboardShortcuts.js +30 -21
- package/dist/components/node/hooks/useNodeDrag.d.ts +31 -18
- package/dist/components/node/hooks/useNodeDrag.d.ts.map +1 -1
- package/dist/components/node/hooks/useNodeDrag.js +139 -78
- package/dist/components/node/hooks/useNodeSelection.d.ts +28 -0
- package/dist/components/node/hooks/useNodeSelection.d.ts.map +1 -0
- package/dist/components/node/hooks/useNodeSelection.js +55 -0
- package/dist/components/node/hooks/useNodeState.d.ts +15 -0
- package/dist/components/node/hooks/useNodeState.d.ts.map +1 -0
- package/dist/components/node/hooks/useNodeState.js +24 -0
- package/dist/components/node/hooks/useSelectionBox.d.ts +14 -3
- package/dist/components/node/hooks/useSelectionBox.d.ts.map +1 -1
- package/dist/components/node/hooks/useSelectionBox.js +39 -18
- package/dist/components/node/hooks/useWheelZoom.d.ts +16 -6
- package/dist/components/node/hooks/useWheelZoom.d.ts.map +1 -1
- package/dist/components/node/hooks/useWheelZoom.js +41 -44
- package/dist/components/node/types/overlay-types.d.ts +11 -8
- package/dist/components/node/types/overlay-types.d.ts.map +1 -1
- package/dist/styles.css +0 -3
- package/package.json +1 -1
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { BaseNodeProps } from "./types/overlay-types";
|
|
2
|
+
interface TaskNodeProps extends BaseNodeProps {
|
|
3
3
|
title: string;
|
|
4
4
|
status: "todo" | "in-progress" | "done";
|
|
5
5
|
assignee?: string;
|
|
6
6
|
project?: string;
|
|
7
7
|
priority?: "low" | "medium" | "high";
|
|
8
8
|
dueDate?: string;
|
|
9
|
-
isSelected?: boolean;
|
|
10
|
-
onSelect?: (id: string, e?: React.MouseEvent) => void;
|
|
11
|
-
onDragStart?: (id: string, e: React.MouseEvent | React.TouchEvent) => void;
|
|
12
9
|
onStatusChange?: (id: string, newStatus: "todo" | "in-progress" | "done") => void;
|
|
13
10
|
zoom?: number;
|
|
14
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-node.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"custom-node.d.ts","sourceRoot":"","sources":["../../../src/components/node/custom-node.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,UAAU,aAAc,SAAQ,aAAa;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACxC,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;IACjB,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,KAAK,IAAI,CAAC;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,EAC/B,EAAE,EACF,KAAK,EACL,MAAM,EACN,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,OAAO,EACP,UAAkB,EAClB,QAAQ,EACR,WAAW,EACX,cAAc,EACd,IAAQ,GACT,EAAE,aAAa,2CAoIf"}
|
|
@@ -14,35 +14,24 @@ export default function TaskNode({ id, title, status, assignee, project, priorit
|
|
|
14
14
|
// Logic: Increase border thickness if zoomed out to keep it visible
|
|
15
15
|
const dynamicBorderWidth = zoom < 0.6 ? "2px" : "1px";
|
|
16
16
|
const focusRingSize = isSelected ? (zoom < 0.5 ? "6px" : "3px") : "0px";
|
|
17
|
-
// 2. Update the internal handler
|
|
18
17
|
const handleStart = (e) => {
|
|
19
18
|
const target = e.target;
|
|
20
|
-
|
|
21
|
-
if (target.closest('.interactive-element'))
|
|
19
|
+
if (target.closest(".interactive-element"))
|
|
22
20
|
return;
|
|
23
|
-
// For touch events, we don't want the browser to scroll the page
|
|
24
|
-
// but we only preventDefault if it's not an interactive element
|
|
25
|
-
if (e.type === 'touchstart' && e.cancelable) {
|
|
26
|
-
// We don't preventDefault here because it might block clicks on
|
|
27
|
-
// internal non-interactive elements, let handleDragStart handle it.
|
|
28
|
-
}
|
|
29
21
|
setIsDragging(true);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (onSelect)
|
|
33
|
-
onSelect(id, e); // Cast for compatibility
|
|
22
|
+
onDragStart?.(id, e);
|
|
23
|
+
onSelect?.(id, e);
|
|
34
24
|
};
|
|
35
|
-
// 3. Update the cleanup effect
|
|
36
25
|
useEffect(() => {
|
|
26
|
+
if (!isDragging)
|
|
27
|
+
return;
|
|
37
28
|
const handleUp = () => setIsDragging(false);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
45
|
-
}
|
|
29
|
+
window.addEventListener("mouseup", handleUp);
|
|
30
|
+
window.addEventListener("touchend", handleUp);
|
|
31
|
+
return () => {
|
|
32
|
+
window.removeEventListener("mouseup", handleUp);
|
|
33
|
+
window.removeEventListener("touchend", handleUp);
|
|
34
|
+
};
|
|
46
35
|
}, [isDragging]);
|
|
47
36
|
return (_jsxs("div", { onMouseDown: handleStart, onTouchStart: handleStart, className: `
|
|
48
37
|
relative w-[310px] bg-[#161617] rounded-2xl transition-all duration-300 ease-out
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { BaseNodeProps } from "./types/overlay-types";
|
|
2
|
+
interface DocumentNodeProps extends BaseNodeProps {
|
|
3
3
|
title: string;
|
|
4
4
|
project: string;
|
|
5
5
|
breadcrumb?: string[];
|
|
6
6
|
preview: string;
|
|
7
7
|
updatedAt?: string;
|
|
8
|
-
isSelected?: boolean;
|
|
9
|
-
onSelect?: (id: string, e?: React.MouseEvent) => void;
|
|
10
|
-
onDragStart?: (id: string, e: React.MouseEvent | React.TouchEvent) => void;
|
|
11
8
|
}
|
|
12
9
|
export default function DocumentNode({ id, title, project, breadcrumb, preview, updatedAt, isSelected, onSelect, onDragStart, }: DocumentNodeProps): import("react/jsx-runtime").JSX.Element;
|
|
13
10
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-node.d.ts","sourceRoot":"","sources":["../../../src/components/node/document-node.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"document-node.d.ts","sourceRoot":"","sources":["../../../src/components/node/document-node.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,UAAU,iBAAkB,SAAQ,aAAa;IAC/C,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;CACpB;AAED,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,EACnC,EAAE,EACF,KAAK,EACL,OAAO,EACP,UAAe,EACf,OAAO,EACP,SAAS,EACT,UAAkB,EAClB,QAAQ,EACR,WAAW,GACZ,EAAE,iBAAiB,2CAwHnB"}
|
|
@@ -5,54 +5,37 @@ import { ChevronDown, ChevronUp, Clock, ExternalLink, MoreHorizontal, Layers } f
|
|
|
5
5
|
export default function DocumentNode({ id, title, project, breadcrumb = [], preview, updatedAt, isSelected = false, onSelect, onDragStart, }) {
|
|
6
6
|
const [isDragging, setIsDragging] = useState(false);
|
|
7
7
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
8
|
-
// 2. Update the internal handler
|
|
9
8
|
const handleStart = (e) => {
|
|
10
9
|
const target = e.target;
|
|
11
|
-
|
|
12
|
-
if (target.closest('.interactive-element'))
|
|
10
|
+
if (target.closest(".interactive-element"))
|
|
13
11
|
return;
|
|
14
|
-
// For touch events, we don't want the browser to scroll the page
|
|
15
|
-
// but we only preventDefault if it's not an interactive element
|
|
16
|
-
if (e.type === 'touchstart' && e.cancelable) {
|
|
17
|
-
// We don't preventDefault here because it might block clicks on
|
|
18
|
-
// internal non-interactive elements, let handleDragStart handle it.
|
|
19
|
-
}
|
|
20
12
|
setIsDragging(true);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (onSelect)
|
|
24
|
-
onSelect(id, e); // Cast for compatibility
|
|
13
|
+
onDragStart?.(id, e);
|
|
14
|
+
onSelect?.(id, e);
|
|
25
15
|
};
|
|
26
|
-
// 3. Update the cleanup effect
|
|
27
16
|
useEffect(() => {
|
|
17
|
+
if (!isDragging)
|
|
18
|
+
return;
|
|
28
19
|
const handleUp = () => setIsDragging(false);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
36
|
-
}
|
|
20
|
+
window.addEventListener("mouseup", handleUp);
|
|
21
|
+
window.addEventListener("touchend", handleUp);
|
|
22
|
+
return () => {
|
|
23
|
+
window.removeEventListener("mouseup", handleUp);
|
|
24
|
+
window.removeEventListener("touchend", handleUp);
|
|
25
|
+
};
|
|
37
26
|
}, [isDragging]);
|
|
38
|
-
return (_jsxs("div", { onMouseDown: handleStart, onTouchStart: handleStart, className: `
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
_jsx(ChevronUp, { className: "w-4 h-4 text-gray-500" })
|
|
45
|
-
_jsx(ChevronDown, { className: "w-4 h-4 text-gray-500" }) })] }), _jsx("div", { className: "flex items-center gap-2 mt-2 overflow-x-auto no-scrollbar pb-1", children: breadcrumb.map((item, i) => (_jsx("div", { className: "flex items-center gap-2 flex-shrink-0", children: _jsx("span", { className: "text-[10px] px-2 py-0.5 bg-[#1C1C1E] border border-[#2C2C2E] text-gray-400 rounded-full", children: item }) }, i))) }), _jsxs("div", { className: `
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
53
|
-
background: #232326;
|
|
54
|
-
border-radius: 10px;
|
|
55
|
-
}
|
|
56
|
-
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #029AFF; }
|
|
57
|
-
` })] }));
|
|
27
|
+
return (_jsxs("div", { onMouseDown: handleStart, onTouchStart: handleStart, className: `
|
|
28
|
+
relative w-[320px] bg-[#121214] rounded-2xl border transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]
|
|
29
|
+
${isDragging ? "cursor-grabbing scale-[1.03] z-50 shadow-2xl" : "cursor-grab shadow-lg hover:border-[#3e3e42]"}
|
|
30
|
+
${isSelected ? "border-[#029AFF] ring-1 ring-[#029AFF]" : "border-[#28282b]"}
|
|
31
|
+
${isExpanded ? "w-[400px]" : "w-[320px]"}
|
|
32
|
+
`, children: [isSelected && (_jsx("div", { className: "absolute -inset-px bg-gradient-to-r from-[#029AFF]/20 to-[#7000FF]/20 rounded-2xl blur-md -z-10" })), _jsxs("div", { className: "flex items-center justify-between p-3 pb-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "p-1.5 bg-[#029AFF]/10 rounded-lg", children: _jsx(Layers, { className: "w-3.5 h-3.5 text-[#029AFF]" }) }), _jsx("span", { className: "text-[10px] font-bold text-gray-500 tracking-tighter uppercase", children: project })] }), _jsx("button", { className: "interactive-element p-1 hover:bg-white/5 rounded-md text-gray-600 transition-colors", children: _jsx(MoreHorizontal, { className: "w-4 h-4" }) })] }), _jsxs("div", { className: "p-4 pt-2", children: [_jsxs("div", { onClick: () => setIsExpanded(!isExpanded), className: "interactive-element flex items-start justify-between group/title cursor-pointer", children: [_jsx("h3", { className: "text-[16px] font-semibold text-white leading-tight group-hover/title:text-[#029AFF] transition-colors pr-2", children: title }), _jsx("div", { className: "mt-1", children: isExpanded
|
|
33
|
+
? _jsx(ChevronUp, { className: "w-4 h-4 text-gray-500" })
|
|
34
|
+
: _jsx(ChevronDown, { className: "w-4 h-4 text-gray-500" }) })] }), _jsx("div", { className: "flex items-center gap-2 mt-2 overflow-x-auto no-scrollbar pb-1", children: breadcrumb.map((item, i) => (_jsx("div", { className: "flex items-center gap-2 flex-shrink-0", children: _jsx("span", { className: "text-[10px] px-2 py-0.5 bg-[#1C1C1E] border border-[#2C2C2E] text-gray-400 rounded-full", children: item }) }, i))) }), _jsxs("div", { className: `mt-4 relative transition-all duration-500 ease-in-out overflow-hidden ${isExpanded ? "h-[300px]" : "h-[100px]"}`, children: [_jsxs("div", { onMouseDown: (e) => e.stopPropagation(), className: "interactive-element h-full w-full bg-[#09090A] border border-[#232326] rounded-xl overflow-y-auto p-3 text-[13px] text-gray-400 leading-relaxed custom-scrollbar selection:bg-[#029AFF]/40", children: [preview, isExpanded && (_jsxs("div", { className: "mt-4 pt-4 border-t border-[#232326] flex items-center justify-between", children: [_jsx("span", { className: "text-[11px] text-gray-500 italic", children: "End of document preview" }), _jsxs("button", { className: "flex items-center gap-2 text-[11px] text-[#029AFF] hover:underline font-medium", children: ["Open Full File ", _jsx(ExternalLink, { className: "w-3 h-3" })] })] }))] }), !isExpanded && (_jsx("div", { className: "absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-[#09090A] to-transparent pointer-events-none" }))] }), _jsxs("div", { className: "mt-4 flex items-center justify-between opacity-60", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[10px] text-gray-400", children: [_jsx(Clock, { className: "w-3 h-3" }), _jsxs("span", { children: ["Updated ", updatedAt || "recently"] })] }), _jsx("div", { className: "flex -space-x-1.5", children: [1, 2].map((i) => (_jsxs("div", { className: "w-5 h-5 rounded-full border-2 border-[#121214] bg-[#2C2C2E] flex items-center justify-center text-[8px] text-white", children: ["U", i] }, i))) })] })] }), _jsx("style", { children: `
|
|
35
|
+
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
36
|
+
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
37
|
+
.custom-scrollbar::-webkit-scrollbar { width: 5px; }
|
|
38
|
+
.custom-scrollbar::-webkit-scrollbar-thumb { background: #232326; border-radius: 10px; }
|
|
39
|
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #029AFF; }
|
|
40
|
+
` })] }));
|
|
58
41
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RefObject, MutableRefObject, Dispatch, SetStateAction } from "react";
|
|
2
|
+
import type { Canvas } from "fabric";
|
|
3
|
+
import type { Task, Document } from "../types/overlay-types";
|
|
4
|
+
interface UseFabricSyncProps {
|
|
5
|
+
fabricCanvas?: RefObject<Canvas | null>;
|
|
6
|
+
canvasReady: boolean;
|
|
7
|
+
dragSelectedIdsRef: MutableRefObject<Set<string>>;
|
|
8
|
+
selectedIdsRef: MutableRefObject<Set<string>>;
|
|
9
|
+
isHtmlSelectingRef: MutableRefObject<boolean>;
|
|
10
|
+
isSelectionBoxActiveRef: MutableRefObject<boolean>;
|
|
11
|
+
htmlNodesSelectedByBoxRef: MutableRefObject<boolean>;
|
|
12
|
+
setSelectedIds: Dispatch<SetStateAction<Set<string>>>;
|
|
13
|
+
setLocalTasks: Dispatch<SetStateAction<Task[]>>;
|
|
14
|
+
setLocalDocuments: Dispatch<SetStateAction<Document[]>>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* SRP: listens to Fabric canvas events and propagates changes to the HTML overlay.
|
|
18
|
+
*
|
|
19
|
+
* object:moving — mirrors Fabric drag onto HTML node positions (rAF-throttled).
|
|
20
|
+
* Uses an accumulated-delta ref so no frames are ever lost even
|
|
21
|
+
* when multiple mousemove events fire within one display frame.
|
|
22
|
+
* mouse:down — snapshots drag selection; clears HTML selection for Fabric drags.
|
|
23
|
+
* selection:created / selection:updated — clear HTML selection when Fabric owns it.
|
|
24
|
+
*
|
|
25
|
+
* Three correctness invariants maintained here:
|
|
26
|
+
*
|
|
27
|
+
* 1. _prevLeft/Top is tracked on the ACTIVE OBJECT (which may be an activeSelection
|
|
28
|
+
* group), not on `e.target` (which may be a member of the group). Both
|
|
29
|
+
* `mouse:down` and `object:moving` must agree on which object they track.
|
|
30
|
+
*
|
|
31
|
+
* 2. Deltas are ACCUMULATED in a ref between RAF frames. When the RAF guard blocks
|
|
32
|
+
* a frame, the delta is NOT discarded — it is added to the pending accumulator
|
|
33
|
+
* and flushed in the next RAF callback. This prevents HTML nodes from drifting
|
|
34
|
+
* behind Fabric objects during fast drags.
|
|
35
|
+
*
|
|
36
|
+
* 3. Guard refs (isHtmlSelecting, isSelectionBoxActive, htmlNodesSelectedByBox)
|
|
37
|
+
* prevent feedback loops between the two selection systems.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useFabricSync({ fabricCanvas, canvasReady, dragSelectedIdsRef, selectedIdsRef, isHtmlSelectingRef, isSelectionBoxActiveRef, htmlNodesSelectedByBoxRef, setSelectedIds, setLocalTasks, setLocalDocuments, }: UseFabricSyncProps): void;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=useFabricSync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFabricSync.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useFabricSync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AACnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAE7D,UAAU,kBAAkB;IAC1B,YAAY,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,WAAW,EAAE,OAAO,CAAC;IACrB,kBAAkB,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,cAAc,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,kBAAkB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC9C,uBAAuB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACnD,yBAAyB,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACrD,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACtD,aAAa,EAAE,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAChD,iBAAiB,EAAE,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,EAC5B,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,EACzB,cAAc,EACd,aAAa,EACb,iBAAiB,GAClB,EAAE,kBAAkB,QAoHpB"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* SRP: listens to Fabric canvas events and propagates changes to the HTML overlay.
|
|
4
|
+
*
|
|
5
|
+
* object:moving — mirrors Fabric drag onto HTML node positions (rAF-throttled).
|
|
6
|
+
* Uses an accumulated-delta ref so no frames are ever lost even
|
|
7
|
+
* when multiple mousemove events fire within one display frame.
|
|
8
|
+
* mouse:down — snapshots drag selection; clears HTML selection for Fabric drags.
|
|
9
|
+
* selection:created / selection:updated — clear HTML selection when Fabric owns it.
|
|
10
|
+
*
|
|
11
|
+
* Three correctness invariants maintained here:
|
|
12
|
+
*
|
|
13
|
+
* 1. _prevLeft/Top is tracked on the ACTIVE OBJECT (which may be an activeSelection
|
|
14
|
+
* group), not on `e.target` (which may be a member of the group). Both
|
|
15
|
+
* `mouse:down` and `object:moving` must agree on which object they track.
|
|
16
|
+
*
|
|
17
|
+
* 2. Deltas are ACCUMULATED in a ref between RAF frames. When the RAF guard blocks
|
|
18
|
+
* a frame, the delta is NOT discarded — it is added to the pending accumulator
|
|
19
|
+
* and flushed in the next RAF callback. This prevents HTML nodes from drifting
|
|
20
|
+
* behind Fabric objects during fast drags.
|
|
21
|
+
*
|
|
22
|
+
* 3. Guard refs (isHtmlSelecting, isSelectionBoxActive, htmlNodesSelectedByBox)
|
|
23
|
+
* prevent feedback loops between the two selection systems.
|
|
24
|
+
*/
|
|
25
|
+
export function useFabricSync({ fabricCanvas, canvasReady, dragSelectedIdsRef, selectedIdsRef, isHtmlSelectingRef, isSelectionBoxActiveRef, htmlNodesSelectedByBoxRef, setSelectedIds, setLocalTasks, setLocalDocuments, }) {
|
|
26
|
+
const fabricMoveRafRef = useRef(null);
|
|
27
|
+
// Accumulates ALL deltas between RAF frames so none are ever dropped.
|
|
28
|
+
const pendingDelta = useRef({ x: 0, y: 0 });
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const canvas = fabricCanvas?.current;
|
|
31
|
+
if (!canvas)
|
|
32
|
+
return;
|
|
33
|
+
const handleObjectMoving = (e) => {
|
|
34
|
+
// e.transform.target is the object actually being dragged by Fabric's
|
|
35
|
+
// interaction system. For an activeSelection it is the GROUP, not a member.
|
|
36
|
+
const target = e.transform?.target || e.target;
|
|
37
|
+
if (!target)
|
|
38
|
+
return;
|
|
39
|
+
const deltaX = target.left - (target._prevLeft ?? target.left);
|
|
40
|
+
const deltaY = target.top - (target._prevTop ?? target.top);
|
|
41
|
+
target._prevLeft = target.left;
|
|
42
|
+
target._prevTop = target.top;
|
|
43
|
+
if (deltaX === 0 && deltaY === 0)
|
|
44
|
+
return;
|
|
45
|
+
// Always accumulate — even when the RAF guard is active.
|
|
46
|
+
// This is the key fix for Bug 3: no delta is ever lost.
|
|
47
|
+
pendingDelta.current.x += deltaX;
|
|
48
|
+
pendingDelta.current.y += deltaY;
|
|
49
|
+
if (fabricMoveRafRef.current !== null)
|
|
50
|
+
return;
|
|
51
|
+
fabricMoveRafRef.current = requestAnimationFrame(() => {
|
|
52
|
+
fabricMoveRafRef.current = null;
|
|
53
|
+
// Drain the accumulator atomically.
|
|
54
|
+
const dx = pendingDelta.current.x;
|
|
55
|
+
const dy = pendingDelta.current.y;
|
|
56
|
+
pendingDelta.current.x = 0;
|
|
57
|
+
pendingDelta.current.y = 0;
|
|
58
|
+
const sel = dragSelectedIdsRef.current;
|
|
59
|
+
setLocalTasks((prev) => prev.map((t) => (sel.has(t.id) ? { ...t, x: t.x + dx, y: t.y + dy } : t)));
|
|
60
|
+
setLocalDocuments((prev) => prev.map((d) => (sel.has(d.id) ? { ...d, x: d.x + dx, y: d.y + dy } : d)));
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const handleMouseDown = (e) => {
|
|
64
|
+
// Bug 2 fix: track _prevLeft on the ACTIVE OBJECT, not e.target.
|
|
65
|
+
//
|
|
66
|
+
// When multiple objects are selected (activeSelection), Fabric fires
|
|
67
|
+
// object:moving with target = the activeSelection GROUP. But e.target in
|
|
68
|
+
// mouse:down can be an individual member object. If we set _prevLeft on
|
|
69
|
+
// the member and then object:moving reads it from the group, _prevLeft is
|
|
70
|
+
// undefined → first-frame delta is always 0 → HTML nodes don't move on
|
|
71
|
+
// frame 1 → visible drift at the start of every Fabric drag.
|
|
72
|
+
//
|
|
73
|
+
// canvas.getActiveObject() returns the activeSelection when multiple
|
|
74
|
+
// objects are selected, which is exactly what object:moving will give us.
|
|
75
|
+
const activeObj = canvas.getActiveObject();
|
|
76
|
+
const trackTarget = activeObj ?? e.target;
|
|
77
|
+
if (trackTarget) {
|
|
78
|
+
trackTarget._prevLeft = trackTarget.left;
|
|
79
|
+
trackTarget._prevTop = trackTarget.top;
|
|
80
|
+
}
|
|
81
|
+
// Reset the delta accumulator on every new drag.
|
|
82
|
+
pendingDelta.current.x = 0;
|
|
83
|
+
pendingDelta.current.y = 0;
|
|
84
|
+
const target = e.target;
|
|
85
|
+
const activeObjects = canvas.getActiveObjects();
|
|
86
|
+
const isClickingIntoActiveSelection = target && activeObjects.length > 1 && activeObjects.includes(target);
|
|
87
|
+
if (isClickingIntoActiveSelection) {
|
|
88
|
+
dragSelectedIdsRef.current = new Set(selectedIdsRef.current);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (isSelectionBoxActiveRef.current)
|
|
92
|
+
return;
|
|
93
|
+
dragSelectedIdsRef.current = new Set(selectedIdsRef.current);
|
|
94
|
+
htmlNodesSelectedByBoxRef.current = false;
|
|
95
|
+
setSelectedIds(new Set());
|
|
96
|
+
};
|
|
97
|
+
const handleSelectionCreated = () => {
|
|
98
|
+
if (isHtmlSelectingRef.current)
|
|
99
|
+
return;
|
|
100
|
+
if (isSelectionBoxActiveRef.current)
|
|
101
|
+
return;
|
|
102
|
+
if (htmlNodesSelectedByBoxRef.current)
|
|
103
|
+
return;
|
|
104
|
+
setSelectedIds(new Set());
|
|
105
|
+
};
|
|
106
|
+
const handleSelectionUpdated = () => {
|
|
107
|
+
if (isHtmlSelectingRef.current)
|
|
108
|
+
return;
|
|
109
|
+
if (isSelectionBoxActiveRef.current)
|
|
110
|
+
return;
|
|
111
|
+
if (htmlNodesSelectedByBoxRef.current)
|
|
112
|
+
return;
|
|
113
|
+
setSelectedIds(new Set());
|
|
114
|
+
};
|
|
115
|
+
canvas.on("object:moving", handleObjectMoving);
|
|
116
|
+
canvas.on("mouse:down", handleMouseDown);
|
|
117
|
+
canvas.on("selection:created", handleSelectionCreated);
|
|
118
|
+
canvas.on("selection:updated", handleSelectionUpdated);
|
|
119
|
+
return () => {
|
|
120
|
+
canvas.off("object:moving", handleObjectMoving);
|
|
121
|
+
canvas.off("mouse:down", handleMouseDown);
|
|
122
|
+
canvas.off("selection:created", handleSelectionCreated);
|
|
123
|
+
canvas.off("selection:updated", handleSelectionUpdated);
|
|
124
|
+
if (fabricMoveRafRef.current !== null) {
|
|
125
|
+
cancelAnimationFrame(fabricMoveRafRef.current);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}, [fabricCanvas, canvasReady]);
|
|
129
|
+
}
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MutableRefObject, Dispatch, SetStateAction } from "react";
|
|
2
|
+
import type { Task, Document } from "../types/overlay-types";
|
|
2
3
|
interface UseKeyboardShortcutsProps {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
localTasksRef: MutableRefObject<Task[]>;
|
|
5
|
+
localDocumentsRef: MutableRefObject<Document[]>;
|
|
6
|
+
selectedIdsRef: MutableRefObject<Set<string>>;
|
|
7
|
+
setSelectedIds: Dispatch<SetStateAction<Set<string>>>;
|
|
8
|
+
setLocalTasks: Dispatch<SetStateAction<Task[]>>;
|
|
9
|
+
setLocalDocuments: Dispatch<SetStateAction<Document[]>>;
|
|
10
|
+
onTasksUpdate?: (tasks: Task[]) => void;
|
|
11
|
+
onDocumentsUpdate?: (documents: Document[]) => void;
|
|
9
12
|
}
|
|
10
|
-
|
|
13
|
+
/**
|
|
14
|
+
* SRP: handles global keyboard shortcuts for the overlay.
|
|
15
|
+
*
|
|
16
|
+
* Uses refs (not raw state values) so the event listener is registered once
|
|
17
|
+
* and never re-registered due to selection/data changes — only stable
|
|
18
|
+
* callback props trigger a re-bind.
|
|
19
|
+
*
|
|
20
|
+
* Ctrl/Cmd+A — select all HTML nodes
|
|
21
|
+
* Escape — clear selection
|
|
22
|
+
* Delete/Backspace — delete selected nodes
|
|
23
|
+
*/
|
|
24
|
+
export declare function useKeyboardShortcuts({ localTasksRef, localDocumentsRef, selectedIdsRef, setSelectedIds, setLocalTasks, setLocalDocuments, onTasksUpdate, onDocumentsUpdate, }: UseKeyboardShortcutsProps): void;
|
|
11
25
|
export {};
|
|
12
26
|
//# sourceMappingURL=useKeyboardShortcuts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useKeyboardShortcuts.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useKeyboardShortcuts.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"useKeyboardShortcuts.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useKeyboardShortcuts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AACxE,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAE7D,UAAU,yBAAyB;IACjC,aAAa,EAAE,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;IACxC,iBAAiB,EAAE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChD,cAAc,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACtD,aAAa,EAAE,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAChD,iBAAiB,EAAE,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACxD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,iBAAiB,GAClB,EAAE,yBAAyB,QAwC3B"}
|
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* SRP: handles global keyboard shortcuts for the overlay.
|
|
4
|
+
*
|
|
5
|
+
* Uses refs (not raw state values) so the event listener is registered once
|
|
6
|
+
* and never re-registered due to selection/data changes — only stable
|
|
7
|
+
* callback props trigger a re-bind.
|
|
8
|
+
*
|
|
9
|
+
* Ctrl/Cmd+A — select all HTML nodes
|
|
10
|
+
* Escape — clear selection
|
|
11
|
+
* Delete/Backspace — delete selected nodes
|
|
12
|
+
*/
|
|
13
|
+
export function useKeyboardShortcuts({ localTasksRef, localDocumentsRef, selectedIdsRef, setSelectedIds, setLocalTasks, setLocalDocuments, onTasksUpdate, onDocumentsUpdate, }) {
|
|
3
14
|
useEffect(() => {
|
|
4
15
|
const handleKeyDown = (e) => {
|
|
5
|
-
// Don't trigger if typing in input
|
|
6
16
|
if (e.target instanceof HTMLInputElement ||
|
|
7
|
-
e.target instanceof HTMLTextAreaElement)
|
|
17
|
+
e.target instanceof HTMLTextAreaElement)
|
|
8
18
|
return;
|
|
9
|
-
}
|
|
10
|
-
// Select All (Ctrl+A / Cmd+A)
|
|
11
19
|
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
|
12
20
|
e.preventDefault();
|
|
13
|
-
|
|
21
|
+
setSelectedIds(new Set([
|
|
22
|
+
...localTasksRef.current.map((t) => t.id),
|
|
23
|
+
...localDocumentsRef.current.map((d) => d.id),
|
|
24
|
+
]));
|
|
25
|
+
return;
|
|
14
26
|
}
|
|
15
|
-
// Clear selection (Escape)
|
|
16
27
|
if (e.key === "Escape") {
|
|
17
|
-
|
|
28
|
+
setSelectedIds(new Set());
|
|
29
|
+
return;
|
|
18
30
|
}
|
|
19
|
-
|
|
20
|
-
if ((e.key === "Delete" || e.key === "Backspace") && selectedIds.size > 0) {
|
|
31
|
+
if ((e.key === "Delete" || e.key === "Backspace") && selectedIdsRef.current.size > 0) {
|
|
21
32
|
e.preventDefault();
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
33
|
+
const sel = selectedIdsRef.current;
|
|
34
|
+
const updatedTasks = localTasksRef.current.filter((t) => !sel.has(t.id));
|
|
35
|
+
const updatedDocs = localDocumentsRef.current.filter((d) => !sel.has(d.id));
|
|
36
|
+
setLocalTasks(updatedTasks);
|
|
37
|
+
setLocalDocuments(updatedDocs);
|
|
38
|
+
setSelectedIds(new Set());
|
|
39
|
+
onTasksUpdate?.(updatedTasks);
|
|
40
|
+
onDocumentsUpdate?.(updatedDocs);
|
|
25
41
|
}
|
|
26
42
|
};
|
|
27
43
|
window.addEventListener("keydown", handleKeyDown);
|
|
28
44
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
29
|
-
}, [
|
|
30
|
-
selectedIds,
|
|
31
|
-
localTasks,
|
|
32
|
-
localDocuments,
|
|
33
|
-
onSelectAll,
|
|
34
|
-
onClearSelection,
|
|
35
|
-
onDeleteSelected,
|
|
36
|
-
]);
|
|
45
|
+
}, [onTasksUpdate, onDocumentsUpdate]);
|
|
37
46
|
}
|
|
@@ -1,25 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Task, Document } from "../types/overlay-types";
|
|
1
|
+
import type { RefObject, MutableRefObject, Dispatch, SetStateAction, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from "react";
|
|
2
|
+
import type { Canvas } from "fabric";
|
|
3
|
+
import type { Task, Document } from "../types/overlay-types";
|
|
4
4
|
interface UseNodeDragProps {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
selectedIdsRef: MutableRefObject<Set<string>>;
|
|
6
|
+
dragSelectedIdsRef: MutableRefObject<Set<string>>;
|
|
7
|
+
localTasksRef: MutableRefObject<Task[]>;
|
|
8
|
+
localDocumentsRef: MutableRefObject<Document[]>;
|
|
9
|
+
fabricCanvas?: RefObject<Canvas | null>;
|
|
10
|
+
setLocalTasks: Dispatch<SetStateAction<Task[]>>;
|
|
11
|
+
setLocalDocuments: Dispatch<SetStateAction<Document[]>>;
|
|
12
|
+
onTasksUpdate?: (tasks: Task[]) => void;
|
|
13
|
+
onDocumentsUpdate?: (documents: Document[]) => void;
|
|
11
14
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* SRP: owns the full HTML-node drag lifecycle — start, move (rAF-throttled),
|
|
17
|
+
* and end — while keeping Fabric canvas objects in sync imperatively.
|
|
18
|
+
*
|
|
19
|
+
* Key design decisions:
|
|
20
|
+
* - All world-space math reads live viewport transform from Fabric so zoom/pan
|
|
21
|
+
* during a drag stays correct without extra state dependencies.
|
|
22
|
+
* - startPositions + anchor-offset approach prevents the "jump on drag start"
|
|
23
|
+
* bug that occurs when using raw mouse coords.
|
|
24
|
+
* - activeSelection group positions are resolved via calcTransformMatrix so
|
|
25
|
+
* multi-selected Fabric objects stay in sync when dragged from HTML nodes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useNodeDrag({ selectedIdsRef, dragSelectedIdsRef, localTasksRef, localDocumentsRef, fabricCanvas, setLocalTasks, setLocalDocuments, onTasksUpdate, onDocumentsUpdate, }: UseNodeDragProps): {
|
|
28
|
+
dragging: {
|
|
29
|
+
itemIds: string[];
|
|
30
|
+
} | null;
|
|
31
|
+
getItemPosition: (id: string) => {
|
|
18
32
|
x: number;
|
|
19
33
|
y: number;
|
|
20
|
-
} | undefined
|
|
21
|
-
|
|
22
|
-
handleDragEnd: () => void;
|
|
34
|
+
} | undefined;
|
|
35
|
+
handleDragStart: (itemId: string, e: ReactMouseEvent | ReactTouchEvent) => void;
|
|
23
36
|
};
|
|
24
37
|
export {};
|
|
25
38
|
//# sourceMappingURL=useNodeDrag.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useNodeDrag.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useNodeDrag.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"useNodeDrag.d.ts","sourceRoot":"","sources":["../../../../src/components/node/hooks/useNodeDrag.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,UAAU,IAAI,eAAe,EAC7B,UAAU,IAAI,eAAe,EAC9B,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAA4B,MAAM,wBAAwB,CAAC;AAEvF,UAAU,gBAAgB;IACxB,cAAc,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,kBAAkB,EAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,aAAa,EAAE,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;IACxC,iBAAiB,EAAE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChD,YAAY,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,aAAa,EAAE,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAChD,iBAAiB,EAAE,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACxD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACxC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;CACrD;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,EAC1B,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,iBAAiB,GAClB,EAAE,gBAAgB;;iBACmC,MAAM,EAAE;;0BAcrD,MAAM,KAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;8BAWzC,MAAM,KAAK,eAAe,GAAG,eAAe;EAkKxD"}
|