@canmingir/link 1.2.10 → 1.2.11

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.
@@ -0,0 +1,214 @@
1
+ import { Box } from "@mui/material";
2
+ import DefaultNodeCard from "./DefaultCard";
3
+ import DraggableNode from "./DraggableNode";
4
+ import DynamicConnector from "../connectors/DynamicConnector";
5
+ import FlowNode from "../core/FlowNode";
6
+ import { getContentParts } from "../utils/flowUtils";
7
+ import { toPxNumber } from "../styles";
8
+ import { useNodeStyle } from "../hooks/useNodeStyle";
9
+
10
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
11
+
12
+ const FlowNodeView = ({
13
+ node,
14
+ type,
15
+ variant,
16
+ style,
17
+ plugin,
18
+ registerRef,
19
+ onDrag,
20
+ onConnect,
21
+ }) => {
22
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0;
23
+
24
+ const {
25
+ baseStyle,
26
+ nodeStyle,
27
+ plugin: _plugin,
28
+ } = useNodeStyle({
29
+ node,
30
+ type,
31
+ variant,
32
+ style,
33
+ plugin,
34
+ });
35
+
36
+ const {
37
+ direction = "vertical",
38
+ lineColor = baseStyle.lineColor,
39
+ lineWidth = baseStyle.lineWidth,
40
+ lineStyle = baseStyle.lineStyle,
41
+ gap = baseStyle.gap,
42
+ levelGap = baseStyle.levelGap ?? 2.5,
43
+ nodeSx = {},
44
+ borderWidth,
45
+ borderColor = baseStyle.borderColor,
46
+ cardWidth,
47
+ shape,
48
+ shadowLevel,
49
+ minHeight,
50
+ showDots = baseStyle.showDots ?? false,
51
+ dotRadius = baseStyle.dotRadius ?? 4,
52
+ dotColor = baseStyle.dotColor,
53
+ showArrow = baseStyle.showArrow ?? true,
54
+ arrowSize = baseStyle.arrowSize ?? 6,
55
+ animated = baseStyle.animated ?? false,
56
+ animationSpeed = baseStyle.animationSpeed ?? 1,
57
+ gradient = baseStyle.gradient ?? null,
58
+ curvature = baseStyle.curvature ?? 0.5,
59
+ selectionColor = baseStyle.selectionColor ?? "#64748b",
60
+ } = nodeStyle;
61
+
62
+ const isHorizontal = direction === "horizontal";
63
+
64
+ const strokeWidth = toPxNumber(lineWidth, 1.5);
65
+ const dashStyle =
66
+ lineStyle === "dashed" || lineStyle === "dotted" ? lineStyle : "solid";
67
+
68
+ const containerRef = useRef(null);
69
+ const parentRef = useRef(null);
70
+ const childRefs = useRef({});
71
+ const [childElList, setChildElList] = useState([]);
72
+
73
+ const [connectorTick, setConnectorTick] = useState(0);
74
+
75
+ const handleDrag = (newOffset) => {
76
+ setConnectorTick((t) => t + 1);
77
+ if (onDrag) onDrag(newOffset);
78
+ };
79
+
80
+ useLayoutEffect(() => {
81
+ const els = (node.children || [])
82
+ .map((c) => childRefs.current[c.id])
83
+ .filter(Boolean);
84
+ setChildElList(els);
85
+ }, [node.children]);
86
+
87
+ useEffect(() => {
88
+ const t = setTimeout(() => {
89
+ const els = (node.children || [])
90
+ .map((c) => childRefs.current[c.id])
91
+ .filter(Boolean);
92
+ setChildElList(els);
93
+ }, 0);
94
+ return () => clearTimeout(t);
95
+ }, [node.children]);
96
+
97
+ const { title, subtitle, metaEntries } = getContentParts(node);
98
+
99
+ const renderContent = () => {
100
+ if (_plugin && typeof _plugin.node === "function") {
101
+ return _plugin.node({
102
+ node,
103
+ title,
104
+ subtitle,
105
+ metaEntries,
106
+ nodeStyle,
107
+ baseStyle,
108
+ });
109
+ }
110
+ return (
111
+ <DefaultNodeCard
112
+ title={title}
113
+ subtitle={subtitle}
114
+ metaEntries={metaEntries}
115
+ nodeStyle={nodeStyle}
116
+ baseStyle={baseStyle}
117
+ variant={variant}
118
+ borderWidth={borderWidth}
119
+ borderColor={borderColor}
120
+ cardWidth={cardWidth}
121
+ shape={shape}
122
+ shadowLevel={shadowLevel}
123
+ minHeight={minHeight}
124
+ nodeSx={nodeSx}
125
+ />
126
+ );
127
+ };
128
+
129
+ return (
130
+ <Box
131
+ ref={containerRef}
132
+ sx={{
133
+ display: "inline-flex",
134
+ flexDirection: isHorizontal ? "row" : "column",
135
+ alignItems: "center",
136
+ position: "relative",
137
+ }}
138
+ >
139
+ <DraggableNode
140
+ registerRef={(el) => {
141
+ parentRef.current = el;
142
+ if (registerRef) registerRef(el);
143
+ }}
144
+ onDrag={handleDrag}
145
+ nodeId={node.id}
146
+ selectionColor={selectionColor}
147
+ initialPosition={node._pastePosition}
148
+ onConnect={onConnect}
149
+ >
150
+ {renderContent()}
151
+ </DraggableNode>
152
+
153
+ {hasChildren && (
154
+ <>
155
+ <DynamicConnector
156
+ containerEl={containerRef.current}
157
+ parentEl={parentRef.current}
158
+ childEls={childElList}
159
+ stroke={lineColor}
160
+ strokeWidth={strokeWidth}
161
+ lineStyle={dashStyle}
162
+ tick={connectorTick}
163
+ orientation={direction}
164
+ showDots={showDots}
165
+ dotRadius={dotRadius}
166
+ dotColor={dotColor}
167
+ showArrow={showArrow}
168
+ arrowSize={arrowSize}
169
+ animated={animated}
170
+ animationSpeed={animationSpeed}
171
+ gradient={gradient}
172
+ curvature={curvature}
173
+ />
174
+
175
+ <Box
176
+ sx={{
177
+ display: "flex",
178
+ flexDirection: isHorizontal ? "column" : "row",
179
+ ...(isHorizontal
180
+ ? {
181
+ marginLeft: levelGap,
182
+ rowGap: gap,
183
+ }
184
+ : {
185
+ marginTop: levelGap,
186
+ columnGap: gap,
187
+ }),
188
+ position: "relative",
189
+ alignItems: "flex-start",
190
+ justifyContent: "center",
191
+ }}
192
+ >
193
+ {node.children.map((child) => (
194
+ <FlowNode
195
+ key={child.id}
196
+ node={child}
197
+ type={type}
198
+ variant={variant}
199
+ style={style}
200
+ plugin={plugin}
201
+ registerRef={(el) => (childRefs.current[child.id] = el)}
202
+ onDrag={() => setConnectorTick((t) => t + 1)}
203
+ isRoot={false}
204
+ onConnect={onConnect}
205
+ />
206
+ ))}
207
+ </Box>
208
+ </>
209
+ )}
210
+ </Box>
211
+ );
212
+ };
213
+
214
+ export default FlowNodeView;
@@ -0,0 +1,259 @@
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+
10
+ const SelectionContext = createContext(null);
11
+
12
+ const DEFAULT_CONTEXT = {
13
+ selectedIds: new Set(),
14
+ selectNode: () => {},
15
+ deselectNode: () => {},
16
+ toggleSelection: () => {},
17
+ clearSelection: () => {},
18
+ selectMultiple: () => {},
19
+ addToSelection: () => {},
20
+ isSelected: () => false,
21
+ registerNodeHandlers: () => () => {},
22
+ moveSelectedNodes: () => {},
23
+ cutSelectedNodes: () => {},
24
+ pasteNodes: () => {},
25
+ hasClipboard: false,
26
+ };
27
+
28
+ const filterNextToSelection = (next, selectedSet) => {
29
+ if (!next) return undefined;
30
+
31
+ if (Array.isArray(next)) {
32
+ const filtered = next.filter((id) => selectedSet.has(id));
33
+ return filtered.length > 0 ? filtered : undefined;
34
+ }
35
+
36
+ return selectedSet.has(next) ? next : undefined;
37
+ };
38
+
39
+ const buildClipboardNode = (id, node, selectedSet) => {
40
+ const filteredNext = filterNextToSelection(node.next, selectedSet);
41
+ const hasPreviousInSelection =
42
+ node.previous && selectedSet.has(node.previous);
43
+
44
+ return {
45
+ ...node,
46
+ id,
47
+ _originalId: id,
48
+ next: filteredNext,
49
+ previous: hasPreviousInSelection ? node.previous : undefined,
50
+ };
51
+ };
52
+
53
+ const buildPasteStructure = (clipboardNodes, position) => {
54
+ if (!clipboardNodes?.length) return null;
55
+
56
+ const seenIds = new Set();
57
+ const nodes = {};
58
+
59
+ clipboardNodes.forEach((clipNode) => {
60
+ const nodeId = clipNode._originalId || clipNode.id;
61
+ if (!nodeId || seenIds.has(nodeId)) return;
62
+
63
+ seenIds.add(nodeId);
64
+
65
+ nodes[nodeId] = {
66
+ id: nodeId,
67
+ label: clipNode.label,
68
+ next: clipNode.next,
69
+ previous: clipNode.previous,
70
+ };
71
+
72
+ Object.keys(clipNode).forEach((key) => {
73
+ if (!["_originalId", "id", "next", "previous", "label"].includes(key)) {
74
+ nodes[nodeId][key] = clipNode[key];
75
+ }
76
+ });
77
+ });
78
+
79
+ const rootIds = Object.keys(nodes).filter((id) => !nodes[id].previous);
80
+ const uniqueRoots = [...new Set(rootIds)];
81
+ const finalRoots =
82
+ uniqueRoots.length > 0 ? uniqueRoots : [Object.keys(nodes)[0]];
83
+
84
+ return {
85
+ nodes,
86
+ roots: finalRoots,
87
+ _pastePosition: position,
88
+ };
89
+ };
90
+
91
+ export const SelectionProvider = ({ children }) => {
92
+ const [selectedIds, setSelectedIds] = useState(new Set());
93
+ const [clipboard, setClipboard] = useState([]);
94
+ const nodeHandlersRef = useRef(new Map());
95
+ const isPastingRef = useRef(false);
96
+
97
+ const selectNode = useCallback((id, addToSelection = false) => {
98
+ setSelectedIds((prev) => {
99
+ const next = new Set(addToSelection ? prev : []);
100
+ next.add(id);
101
+ return next;
102
+ });
103
+ }, []);
104
+
105
+ const deselectNode = useCallback((id) => {
106
+ setSelectedIds((prev) => {
107
+ const next = new Set(prev);
108
+ next.delete(id);
109
+ return next;
110
+ });
111
+ }, []);
112
+
113
+ const toggleSelection = useCallback((id) => {
114
+ setSelectedIds((prev) => {
115
+ const next = new Set(prev);
116
+ next.has(id) ? next.delete(id) : next.add(id);
117
+ return next;
118
+ });
119
+ }, []);
120
+
121
+ const clearSelection = useCallback(() => setSelectedIds(new Set()), []);
122
+
123
+ const selectMultiple = useCallback((ids) => setSelectedIds(new Set(ids)), []);
124
+
125
+ const addToSelection = useCallback(
126
+ (ids) => setSelectedIds((prev) => new Set([...prev, ...ids])),
127
+ []
128
+ );
129
+
130
+ const isSelected = useCallback((id) => selectedIds.has(id), [selectedIds]);
131
+
132
+ const registerNodeHandlers = useCallback((id, handlers) => {
133
+ nodeHandlersRef.current.set(id, handlers);
134
+ return () => nodeHandlersRef.current.delete(id);
135
+ }, []);
136
+
137
+ const moveSelectedNodes = useCallback(
138
+ (deltaX, deltaY, excludeId = null) => {
139
+ selectedIds.forEach((id) => {
140
+ if (id === excludeId) return;
141
+
142
+ const handlers = nodeHandlersRef.current.get(id);
143
+ if (!handlers) return;
144
+
145
+ handlers.setOffset?.((prev) => ({
146
+ x: prev.x + deltaX,
147
+ y: prev.y + deltaY,
148
+ }));
149
+ handlers.onDrag?.();
150
+ });
151
+ },
152
+ [selectedIds]
153
+ );
154
+
155
+ const cutSelectedNodes = useCallback(
156
+ (nodesById, onCut) => {
157
+ if (!nodesById || selectedIds.size === 0) return;
158
+
159
+ const selectedSet = new Set(selectedIds);
160
+ const seenIds = new Set();
161
+ const cutNodes = [];
162
+
163
+ [...selectedIds].forEach((id) => {
164
+ if (!nodesById[id] || seenIds.has(id)) return;
165
+ seenIds.add(id);
166
+ cutNodes.push(buildClipboardNode(id, nodesById[id], selectedSet));
167
+ });
168
+
169
+ setClipboard(cutNodes);
170
+ onCut?.([...selectedIds]);
171
+ setSelectedIds(new Set());
172
+ },
173
+ [selectedIds]
174
+ );
175
+
176
+ const pasteNodes = useCallback(
177
+ (callback, x = 0, y = 0) => {
178
+ if (!clipboard.length || !callback || isPastingRef.current) return;
179
+
180
+ isPastingRef.current = true;
181
+ const nodesToPaste = [...clipboard];
182
+
183
+ const pasteData = buildPasteStructure(nodesToPaste, { x, y });
184
+ if (!pasteData) {
185
+ isPastingRef.current = false;
186
+ return;
187
+ }
188
+
189
+ let result;
190
+ try {
191
+ result = callback(pasteData, { x, y });
192
+ } catch (error) {
193
+ isPastingRef.current = false;
194
+ throw error;
195
+ }
196
+
197
+ if (result && typeof result.then === "function") {
198
+ return result
199
+ .then(() => {
200
+ setClipboard([]);
201
+ })
202
+ .finally(() => {
203
+ isPastingRef.current = false;
204
+ });
205
+ }
206
+
207
+ setClipboard([]);
208
+ isPastingRef.current = false;
209
+ return result;
210
+ },
211
+ [clipboard]
212
+ );
213
+
214
+ const hasClipboard = clipboard.length > 0;
215
+
216
+ const contextValue = useMemo(
217
+ () => ({
218
+ selectedIds,
219
+ selectNode,
220
+ deselectNode,
221
+ toggleSelection,
222
+ clearSelection,
223
+ selectMultiple,
224
+ addToSelection,
225
+ isSelected,
226
+ registerNodeHandlers,
227
+ moveSelectedNodes,
228
+ cutSelectedNodes,
229
+ pasteNodes,
230
+ hasClipboard,
231
+ }),
232
+ [
233
+ selectedIds,
234
+ selectNode,
235
+ deselectNode,
236
+ toggleSelection,
237
+ clearSelection,
238
+ selectMultiple,
239
+ addToSelection,
240
+ isSelected,
241
+ registerNodeHandlers,
242
+ moveSelectedNodes,
243
+ cutSelectedNodes,
244
+ pasteNodes,
245
+ hasClipboard,
246
+ ]
247
+ );
248
+
249
+ return (
250
+ <SelectionContext.Provider value={contextValue}>
251
+ {children}
252
+ </SelectionContext.Provider>
253
+ );
254
+ };
255
+
256
+ export const useSelection = () =>
257
+ useContext(SelectionContext) || DEFAULT_CONTEXT;
258
+
259
+ export default SelectionContext;
@@ -0,0 +1,31 @@
1
+ import { Box } from "@mui/material";
2
+ import { hexToRgba } from "../utils/flowUtils";
3
+
4
+ const SelectionOverlay = ({ box, selectionColor = "#64748b" }) => {
5
+ if (!box) return null;
6
+
7
+ const { startX, startY, currentX, currentY } = box;
8
+ const left = Math.min(startX, currentX);
9
+ const top = Math.min(startY, currentY);
10
+ const width = Math.abs(currentX - startX);
11
+ const height = Math.abs(currentY - startY);
12
+
13
+ return (
14
+ <Box
15
+ sx={{
16
+ position: "fixed",
17
+ left,
18
+ top,
19
+ width,
20
+ height,
21
+ border: `2px solid ${selectionColor}`,
22
+ backgroundColor: hexToRgba(selectionColor, 0.1),
23
+ pointerEvents: "none",
24
+ zIndex: 9999,
25
+ borderRadius: "4px",
26
+ }}
27
+ />
28
+ );
29
+ };
30
+
31
+ export default SelectionOverlay;