@canmingir/link 1.2.61 → 1.2.63
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/package.json +1 -1
- package/src/lib/Flow/core/Flow.jsx +33 -13
- package/src/lib/Flow/core/FlowBoard.jsx +32 -12
- package/src/lib/Flow/core/FlowNode.jsx +39 -19
- package/src/lib/Flow/core/FlowViewport.jsx +175 -24
- package/src/lib/Flow/layouts/CardLayout.jsx +8 -7
- package/src/lib/Flow/layouts/InfoNode.jsx +3 -3
- package/src/lib/Flow/utils/viewportUtils.js +81 -0
- package/src/stories/FlowFitView.stories.jsx +173 -0
package/package.json
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { Box, alpha } from "@mui/material";
|
|
2
|
-
import React, { useMemo, useState } from "react";
|
|
2
|
+
import React, { forwardRef, useMemo, useState } from "react";
|
|
3
3
|
import { assertLinkedGraph, buildTreeFromLinked } from "../utils/flowUtils";
|
|
4
4
|
|
|
5
5
|
import FlowNode from "./FlowNode";
|
|
6
6
|
import { useGraphOperations } from "../hooks/useGraphOperations";
|
|
7
7
|
|
|
8
|
-
export const Flow = (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
export const Flow = forwardRef(function Flow(
|
|
9
|
+
{
|
|
10
|
+
data,
|
|
11
|
+
variant = "simple",
|
|
12
|
+
style,
|
|
13
|
+
plugin,
|
|
14
|
+
editable = false,
|
|
15
|
+
onChange,
|
|
16
|
+
height,
|
|
17
|
+
initialZoom,
|
|
18
|
+
centered = false,
|
|
19
|
+
minZoom,
|
|
20
|
+
maxZoom,
|
|
21
|
+
fitViewPadding,
|
|
22
|
+
fitViewMaxZoom,
|
|
23
|
+
fitViewOnMount,
|
|
24
|
+
fitViewOnResize,
|
|
25
|
+
fitViewOnNodesChange,
|
|
26
|
+
onInit,
|
|
27
|
+
},
|
|
28
|
+
ref,
|
|
29
|
+
) {
|
|
19
30
|
const [floatingNodes, setFloatingNodes] = useState([]);
|
|
20
31
|
|
|
21
32
|
const { nodesById, roots } = useMemo(() => assertLinkedGraph(data), [data]);
|
|
@@ -79,6 +90,7 @@ export const Flow = ({
|
|
|
79
90
|
}}
|
|
80
91
|
>
|
|
81
92
|
<FlowNode
|
|
93
|
+
ref={ref}
|
|
82
94
|
node={treeData}
|
|
83
95
|
variant={variant}
|
|
84
96
|
style={style}
|
|
@@ -92,9 +104,17 @@ export const Flow = ({
|
|
|
92
104
|
height={height}
|
|
93
105
|
initialZoom={initialZoom}
|
|
94
106
|
centered={centered}
|
|
107
|
+
minZoom={minZoom}
|
|
108
|
+
maxZoom={maxZoom}
|
|
109
|
+
fitViewPadding={fitViewPadding}
|
|
110
|
+
fitViewMaxZoom={fitViewMaxZoom}
|
|
111
|
+
fitViewOnMount={fitViewOnMount}
|
|
112
|
+
fitViewOnResize={fitViewOnResize}
|
|
113
|
+
fitViewOnNodesChange={fitViewOnNodesChange}
|
|
114
|
+
onInit={onInit}
|
|
95
115
|
/>
|
|
96
116
|
</Box>
|
|
97
117
|
);
|
|
98
|
-
};
|
|
118
|
+
});
|
|
99
119
|
|
|
100
120
|
export default Flow;
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import React, { useMemo } from "react";
|
|
1
|
+
import React, { forwardRef, useMemo } from "react";
|
|
2
2
|
|
|
3
3
|
import FlowBoardItem from "./FlowBoardItem";
|
|
4
4
|
import FlowViewport from "./FlowViewport";
|
|
5
5
|
import { SelectionProvider } from "../selection/SelectionContext";
|
|
6
6
|
import { getBaseStyleForVariant } from "../styles";
|
|
7
7
|
|
|
8
|
-
export const FlowBoard = (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
export const FlowBoard = forwardRef(function FlowBoard(
|
|
9
|
+
{
|
|
10
|
+
flows = [],
|
|
11
|
+
variant = "simple",
|
|
12
|
+
style,
|
|
13
|
+
plugin,
|
|
14
|
+
initialZoom = 1,
|
|
15
|
+
height = "100vh",
|
|
16
|
+
gap = 480,
|
|
17
|
+
onFlowPositionChange,
|
|
18
|
+
minZoom,
|
|
19
|
+
maxZoom,
|
|
20
|
+
fitViewPadding,
|
|
21
|
+
fitViewMaxZoom,
|
|
22
|
+
fitViewOnMount,
|
|
23
|
+
fitViewOnResize,
|
|
24
|
+
fitViewOnNodesChange,
|
|
25
|
+
onInit,
|
|
26
|
+
},
|
|
27
|
+
ref,
|
|
28
|
+
) {
|
|
18
29
|
const baseStyle = getBaseStyleForVariant(variant);
|
|
19
30
|
const selectionColor = baseStyle.selectionColor ?? "#64748b";
|
|
20
31
|
|
|
@@ -44,6 +55,7 @@ export const FlowBoard = ({
|
|
|
44
55
|
return (
|
|
45
56
|
<SelectionProvider>
|
|
46
57
|
<FlowViewport
|
|
58
|
+
ref={ref}
|
|
47
59
|
selectionColor={selectionColor}
|
|
48
60
|
nodesById={mergedNodesById}
|
|
49
61
|
variant={variant}
|
|
@@ -51,6 +63,14 @@ export const FlowBoard = ({
|
|
|
51
63
|
plugin={plugin}
|
|
52
64
|
height={height}
|
|
53
65
|
initialZoom={initialZoom}
|
|
66
|
+
minZoom={minZoom}
|
|
67
|
+
maxZoom={maxZoom}
|
|
68
|
+
fitViewPadding={fitViewPadding}
|
|
69
|
+
fitViewMaxZoom={fitViewMaxZoom}
|
|
70
|
+
fitViewOnMount={fitViewOnMount}
|
|
71
|
+
fitViewOnResize={fitViewOnResize}
|
|
72
|
+
fitViewOnNodesChange={fitViewOnNodesChange}
|
|
73
|
+
onInit={onInit}
|
|
54
74
|
>
|
|
55
75
|
{positionedFlows.map(({ flow, id, position }) => (
|
|
56
76
|
<FlowBoardItem
|
|
@@ -73,6 +93,6 @@ export const FlowBoard = ({
|
|
|
73
93
|
</FlowViewport>
|
|
74
94
|
</SelectionProvider>
|
|
75
95
|
);
|
|
76
|
-
};
|
|
96
|
+
});
|
|
77
97
|
|
|
78
98
|
export default FlowBoard;
|
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
import FlowNodeView from "../nodes/FlowNodeView";
|
|
2
2
|
import FlowViewport from "./FlowViewport";
|
|
3
|
-
import React from "react";
|
|
3
|
+
import React, { forwardRef } from "react";
|
|
4
4
|
import { SelectionProvider } from "../selection/SelectionContext";
|
|
5
5
|
import { getBaseStyleForVariant } from "../styles";
|
|
6
6
|
|
|
7
|
-
const FlowNode = (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
const FlowNode = forwardRef(function FlowNode(
|
|
8
|
+
{
|
|
9
|
+
isRoot = false,
|
|
10
|
+
onAddNode,
|
|
11
|
+
variant,
|
|
12
|
+
nodesById,
|
|
13
|
+
onPaste,
|
|
14
|
+
onCut,
|
|
15
|
+
onConnect,
|
|
16
|
+
floatingNodes,
|
|
17
|
+
style,
|
|
18
|
+
plugin,
|
|
19
|
+
node,
|
|
20
|
+
height,
|
|
21
|
+
initialZoom,
|
|
22
|
+
centered,
|
|
23
|
+
minZoom,
|
|
24
|
+
maxZoom,
|
|
25
|
+
fitViewPadding,
|
|
26
|
+
fitViewMaxZoom,
|
|
27
|
+
fitViewOnMount,
|
|
28
|
+
fitViewOnResize,
|
|
29
|
+
fitViewOnNodesChange,
|
|
30
|
+
onInit,
|
|
31
|
+
...props
|
|
32
|
+
},
|
|
33
|
+
ref,
|
|
34
|
+
) {
|
|
24
35
|
if (!isRoot) {
|
|
25
36
|
if (!node) return null;
|
|
26
37
|
return (
|
|
@@ -42,6 +53,7 @@ const FlowNode = ({
|
|
|
42
53
|
return (
|
|
43
54
|
<SelectionProvider>
|
|
44
55
|
<FlowViewport
|
|
56
|
+
ref={ref}
|
|
45
57
|
selectionColor={selectionColor}
|
|
46
58
|
nodesById={nodesById}
|
|
47
59
|
onPaste={onPaste}
|
|
@@ -54,6 +66,14 @@ const FlowNode = ({
|
|
|
54
66
|
height={height}
|
|
55
67
|
initialZoom={initialZoom}
|
|
56
68
|
centered={centered}
|
|
69
|
+
minZoom={minZoom}
|
|
70
|
+
maxZoom={maxZoom}
|
|
71
|
+
fitViewPadding={fitViewPadding}
|
|
72
|
+
fitViewMaxZoom={fitViewMaxZoom}
|
|
73
|
+
fitViewOnMount={fitViewOnMount}
|
|
74
|
+
fitViewOnResize={fitViewOnResize}
|
|
75
|
+
fitViewOnNodesChange={fitViewOnNodesChange}
|
|
76
|
+
onInit={onInit}
|
|
57
77
|
>
|
|
58
78
|
{node && (
|
|
59
79
|
<FlowNodeView
|
|
@@ -69,6 +89,6 @@ const FlowNode = ({
|
|
|
69
89
|
</FlowViewport>
|
|
70
90
|
</SelectionProvider>
|
|
71
91
|
);
|
|
72
|
-
};
|
|
92
|
+
});
|
|
73
93
|
|
|
74
94
|
export default FlowNode;
|
|
@@ -1,28 +1,55 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_FIT_VIEW_PADDING,
|
|
3
|
+
DEFAULT_MAX_ZOOM,
|
|
4
|
+
DEFAULT_MIN_ZOOM,
|
|
5
|
+
clampZoomValue,
|
|
6
|
+
computeFitViewState,
|
|
7
|
+
} from "../utils/viewportUtils";
|
|
8
|
+
import React, {
|
|
9
|
+
forwardRef,
|
|
10
|
+
useCallback,
|
|
11
|
+
useEffect,
|
|
12
|
+
useImperativeHandle,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
} from "react";
|
|
2
16
|
|
|
3
17
|
import { Box } from "@mui/material";
|
|
4
18
|
import FloatingGraph from "../graph/FloatingGraph";
|
|
5
19
|
import SelectionOverlay from "../selection/SelectionOverlay";
|
|
6
20
|
import { useSelection } from "../selection/SelectionContext";
|
|
7
21
|
|
|
8
|
-
const FlowViewport = (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const FlowViewport = forwardRef(function FlowViewport(
|
|
23
|
+
{
|
|
24
|
+
children,
|
|
25
|
+
selectionColor = "#64748b",
|
|
26
|
+
nodesById,
|
|
27
|
+
onPaste,
|
|
28
|
+
onCut,
|
|
29
|
+
onConnect,
|
|
30
|
+
floatingNodes = [],
|
|
31
|
+
variant,
|
|
32
|
+
style,
|
|
33
|
+
plugin,
|
|
34
|
+
height = "100vh",
|
|
35
|
+
initialZoom = 1,
|
|
36
|
+
centered = false,
|
|
37
|
+
minZoom = DEFAULT_MIN_ZOOM,
|
|
38
|
+
maxZoom = DEFAULT_MAX_ZOOM,
|
|
39
|
+
fitViewPadding = DEFAULT_FIT_VIEW_PADDING,
|
|
40
|
+
fitViewMaxZoom = 1,
|
|
41
|
+
fitViewOnMount = false,
|
|
42
|
+
fitViewOnResize = false,
|
|
43
|
+
fitViewOnNodesChange = false,
|
|
44
|
+
onInit,
|
|
45
|
+
sx = {},
|
|
46
|
+
...rest
|
|
47
|
+
},
|
|
48
|
+
ref,
|
|
49
|
+
) {
|
|
50
|
+
const clampZoom = (z) => clampZoomValue(z, minZoom, maxZoom);
|
|
51
|
+
|
|
52
|
+
const usesFitView = fitViewOnMount || fitViewOnResize || fitViewOnNodesChange;
|
|
26
53
|
|
|
27
54
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
28
55
|
const [zoom, setZoom] = useState(initialZoom);
|
|
@@ -46,7 +73,7 @@ const FlowViewport = ({
|
|
|
46
73
|
} = useSelection();
|
|
47
74
|
|
|
48
75
|
useEffect(() => {
|
|
49
|
-
if (centered) return;
|
|
76
|
+
if (centered || usesFitView) return;
|
|
50
77
|
|
|
51
78
|
const container = containerRef.current;
|
|
52
79
|
const inner = innerRef.current;
|
|
@@ -67,7 +94,123 @@ const FlowViewport = ({
|
|
|
67
94
|
observer.observe(container);
|
|
68
95
|
|
|
69
96
|
return () => observer.disconnect();
|
|
70
|
-
}, [centered]);
|
|
97
|
+
}, [centered, usesFitView]);
|
|
98
|
+
|
|
99
|
+
const runFitView = useCallback(
|
|
100
|
+
(options = {}) => {
|
|
101
|
+
const fit = computeFitViewState(containerRef.current, {
|
|
102
|
+
zoom,
|
|
103
|
+
offset,
|
|
104
|
+
padding: options.padding ?? fitViewPadding,
|
|
105
|
+
minZoom: options.minZoom ?? minZoom,
|
|
106
|
+
maxZoom: options.maxZoom ?? fitViewMaxZoom,
|
|
107
|
+
});
|
|
108
|
+
if (!fit) return;
|
|
109
|
+
setZoom(fit.zoom);
|
|
110
|
+
setOffset(fit.offset);
|
|
111
|
+
},
|
|
112
|
+
[zoom, offset, fitViewPadding, minZoom, fitViewMaxZoom],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const zoomIn = useCallback(
|
|
116
|
+
(step = 1.2) => setZoom((z) => clampZoom(z * step)),
|
|
117
|
+
[minZoom, maxZoom],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const zoomOut = useCallback(
|
|
121
|
+
(step = 1.2) => setZoom((z) => clampZoom(z / step)),
|
|
122
|
+
[minZoom, maxZoom],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const setZoomPublic = useCallback(
|
|
126
|
+
(z) => setZoom(clampZoom(z)),
|
|
127
|
+
[minZoom, maxZoom],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const setCenter = useCallback(
|
|
131
|
+
(x, y, options = {}) => {
|
|
132
|
+
const targetZoom = clampZoom(options.zoom ?? zoom);
|
|
133
|
+
setZoom(targetZoom);
|
|
134
|
+
setOffset({ x: -targetZoom * x, y: -targetZoom * y });
|
|
135
|
+
},
|
|
136
|
+
[zoom, minZoom, maxZoom],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const getZoom = useCallback(() => zoom, [zoom]);
|
|
140
|
+
|
|
141
|
+
const runFitViewRef = useRef(runFitView);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
runFitViewRef.current = runFitView;
|
|
144
|
+
}, [runFitView]);
|
|
145
|
+
|
|
146
|
+
useImperativeHandle(
|
|
147
|
+
ref,
|
|
148
|
+
() => ({
|
|
149
|
+
fitView: runFitView,
|
|
150
|
+
zoomIn,
|
|
151
|
+
zoomOut,
|
|
152
|
+
setZoom: setZoomPublic,
|
|
153
|
+
setCenter,
|
|
154
|
+
getZoom,
|
|
155
|
+
}),
|
|
156
|
+
[runFitView, zoomIn, zoomOut, setZoomPublic, setCenter, getZoom],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!fitViewOnMount) return;
|
|
161
|
+
runFitView();
|
|
162
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!fitViewOnResize) return;
|
|
167
|
+
|
|
168
|
+
const container = containerRef.current;
|
|
169
|
+
if (!container) return;
|
|
170
|
+
|
|
171
|
+
let frame = null;
|
|
172
|
+
let isFirstCallback = true;
|
|
173
|
+
const observer = new ResizeObserver(() => {
|
|
174
|
+
if (isFirstCallback) {
|
|
175
|
+
isFirstCallback = false;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (frame) cancelAnimationFrame(frame);
|
|
179
|
+
frame = requestAnimationFrame(() => runFitViewRef.current());
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
observer.observe(container);
|
|
183
|
+
|
|
184
|
+
return () => {
|
|
185
|
+
if (frame) cancelAnimationFrame(frame);
|
|
186
|
+
observer.disconnect();
|
|
187
|
+
};
|
|
188
|
+
}, [fitViewOnResize]);
|
|
189
|
+
|
|
190
|
+
const nodeCount = nodesById ? Object.keys(nodesById).length : 0;
|
|
191
|
+
const previousNodeCountRef = useRef(nodeCount);
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!fitViewOnNodesChange) return;
|
|
195
|
+
|
|
196
|
+
if (previousNodeCountRef.current !== nodeCount) {
|
|
197
|
+
previousNodeCountRef.current = nodeCount;
|
|
198
|
+
const frame = requestAnimationFrame(() => runFitViewRef.current());
|
|
199
|
+
return () => cancelAnimationFrame(frame);
|
|
200
|
+
}
|
|
201
|
+
}, [fitViewOnNodesChange, nodeCount]);
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
onInit?.({
|
|
205
|
+
fitView: runFitView,
|
|
206
|
+
zoomIn,
|
|
207
|
+
zoomOut,
|
|
208
|
+
setZoom: setZoomPublic,
|
|
209
|
+
setCenter,
|
|
210
|
+
getZoom,
|
|
211
|
+
});
|
|
212
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
213
|
+
}, []);
|
|
71
214
|
|
|
72
215
|
useEffect(() => {
|
|
73
216
|
const handleMouseMove = (e) => {
|
|
@@ -313,11 +456,19 @@ const FlowViewport = ({
|
|
|
313
456
|
height: height,
|
|
314
457
|
display: "flex",
|
|
315
458
|
alignItems: "center",
|
|
316
|
-
justifyContent:
|
|
459
|
+
justifyContent:
|
|
460
|
+
centered || (!usesFitView && shouldCenter)
|
|
461
|
+
? "center"
|
|
462
|
+
: "flex-start",
|
|
317
463
|
transition: isDragging ? "none" : "transform 0.1s ease-out",
|
|
318
464
|
pointerEvents: "auto",
|
|
319
465
|
position: "relative",
|
|
320
|
-
pl:
|
|
466
|
+
pl:
|
|
467
|
+
centered || (!usesFitView && shouldCenter)
|
|
468
|
+
? 0
|
|
469
|
+
: variant === "horizontal"
|
|
470
|
+
? 4
|
|
471
|
+
: 0,
|
|
321
472
|
}}
|
|
322
473
|
>
|
|
323
474
|
{children}
|
|
@@ -340,6 +491,6 @@ const FlowViewport = ({
|
|
|
340
491
|
</Box>
|
|
341
492
|
</Box>
|
|
342
493
|
);
|
|
343
|
-
};
|
|
494
|
+
});
|
|
344
495
|
|
|
345
496
|
export default FlowViewport;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
|
|
3
1
|
import { Box, Card, Stack, Typography } from "@mui/material";
|
|
4
2
|
import { alpha, useTheme } from "@mui/material/styles";
|
|
5
3
|
|
|
4
|
+
import React from "react";
|
|
5
|
+
|
|
6
6
|
const STYLES = {
|
|
7
7
|
width: "100%",
|
|
8
8
|
display: "-webkit-box",
|
|
@@ -158,7 +158,7 @@ export function SideStripeCard({
|
|
|
158
158
|
const theme = useTheme();
|
|
159
159
|
|
|
160
160
|
return (
|
|
161
|
-
<Box sx={{ cursor: "
|
|
161
|
+
<Box sx={{ cursor: "inherit" }}>
|
|
162
162
|
<Card
|
|
163
163
|
sx={{
|
|
164
164
|
display: "flex",
|
|
@@ -315,7 +315,7 @@ export function HeaderCard({
|
|
|
315
315
|
left: 0,
|
|
316
316
|
background: `linear-gradient(135deg, ${alpha(
|
|
317
317
|
theme.palette.secondary.light,
|
|
318
|
-
0.2
|
|
318
|
+
0.2,
|
|
319
319
|
)}, ${alpha(theme.palette.primary.main, 0.3)})`,
|
|
320
320
|
borderRadius: "8px 8px 0 0",
|
|
321
321
|
transition: "height 0.3s ease-in-out",
|
|
@@ -369,7 +369,7 @@ export function AvatarRoleCard({
|
|
|
369
369
|
left: 0,
|
|
370
370
|
background: `linear-gradient(135deg, ${alpha(
|
|
371
371
|
theme.palette.secondary.light,
|
|
372
|
-
0.2
|
|
372
|
+
0.2,
|
|
373
373
|
)}, ${alpha(theme.palette.primary.main, 0.3)})`,
|
|
374
374
|
borderRadius: "8px 8px 0 0",
|
|
375
375
|
transition: "height 0.3s ease-in-out",
|
|
@@ -385,8 +385,9 @@ export function AvatarRoleCard({
|
|
|
385
385
|
direction="row"
|
|
386
386
|
sx={{
|
|
387
387
|
justifyContent: "space-between",
|
|
388
|
-
alignItems: "flex-start"
|
|
389
|
-
}}
|
|
388
|
+
alignItems: "flex-start",
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
390
391
|
{leftContent}
|
|
391
392
|
{topRightContent}
|
|
392
393
|
</Stack>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Iconify } from "@canmingir/link/platform/components";
|
|
2
|
-
|
|
3
1
|
import React, { useEffect, useState } from "react";
|
|
4
2
|
import { alpha, styled } from "@mui/material/styles";
|
|
5
3
|
|
|
4
|
+
import { Iconify } from "@canmingir/link/platform/components";
|
|
5
|
+
|
|
6
6
|
const ANIMATION_DELAY_MS = 200;
|
|
7
7
|
|
|
8
8
|
const MainContainer = styled("div", {
|
|
@@ -33,7 +33,7 @@ const MainContainer = styled("div", {
|
|
|
33
33
|
border: `1px solid ${borderColor}`,
|
|
34
34
|
backdropFilter: "blur(10px)",
|
|
35
35
|
transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
36
|
-
cursor: "
|
|
36
|
+
cursor: "inherit",
|
|
37
37
|
position: "relative",
|
|
38
38
|
overflow: "hidden",
|
|
39
39
|
minWidth,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const DEFAULT_MIN_ZOOM = 0.25;
|
|
2
|
+
export const DEFAULT_MAX_ZOOM = 2.5;
|
|
3
|
+
export const DEFAULT_FIT_VIEW_PADDING = 40;
|
|
4
|
+
|
|
5
|
+
export const clampZoomValue = (zoom, minZoom, maxZoom) =>
|
|
6
|
+
Math.min(maxZoom, Math.max(minZoom, zoom));
|
|
7
|
+
|
|
8
|
+
export const computeFitTransform = ({
|
|
9
|
+
bounds,
|
|
10
|
+
containerWidth,
|
|
11
|
+
containerHeight,
|
|
12
|
+
padding = DEFAULT_FIT_VIEW_PADDING,
|
|
13
|
+
minZoom = DEFAULT_MIN_ZOOM,
|
|
14
|
+
maxZoom = DEFAULT_MAX_ZOOM,
|
|
15
|
+
}) => {
|
|
16
|
+
const availableWidth = Math.max(0, containerWidth - 2 * padding);
|
|
17
|
+
const availableHeight = Math.max(0, containerHeight - 2 * padding);
|
|
18
|
+
|
|
19
|
+
const scaleX = bounds.width > 0 ? availableWidth / bounds.width : Infinity;
|
|
20
|
+
const scaleY = bounds.height > 0 ? availableHeight / bounds.height : Infinity;
|
|
21
|
+
const rawZoom = Math.min(scaleX, scaleY);
|
|
22
|
+
|
|
23
|
+
const zoom = clampZoomValue(rawZoom, minZoom, maxZoom);
|
|
24
|
+
|
|
25
|
+
const centerX = bounds.x + bounds.width / 2;
|
|
26
|
+
const centerY = bounds.y + bounds.height / 2;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
zoom,
|
|
30
|
+
offset: {
|
|
31
|
+
x: -zoom * centerX,
|
|
32
|
+
y: -zoom * centerY,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const computeContentBounds = (containerEl, { zoom, offset }) => {
|
|
38
|
+
if (!containerEl) return null;
|
|
39
|
+
|
|
40
|
+
const nodeEls = containerEl.querySelectorAll("[data-node-id]");
|
|
41
|
+
if (nodeEls.length === 0) return null;
|
|
42
|
+
|
|
43
|
+
const containerRect = containerEl.getBoundingClientRect();
|
|
44
|
+
const originX = containerRect.left + containerRect.width / 2;
|
|
45
|
+
const originY = containerRect.top + containerRect.height / 2;
|
|
46
|
+
|
|
47
|
+
const toContentX = (screenX) => (screenX - originX - offset.x) / zoom;
|
|
48
|
+
const toContentY = (screenY) => (screenY - originY - offset.y) / zoom;
|
|
49
|
+
|
|
50
|
+
let minX = Infinity;
|
|
51
|
+
let minY = Infinity;
|
|
52
|
+
let maxX = -Infinity;
|
|
53
|
+
let maxY = -Infinity;
|
|
54
|
+
|
|
55
|
+
nodeEls.forEach((el) => {
|
|
56
|
+
const rect = el.getBoundingClientRect();
|
|
57
|
+
minX = Math.min(minX, toContentX(rect.left));
|
|
58
|
+
maxX = Math.max(maxX, toContentX(rect.right));
|
|
59
|
+
minY = Math.min(minY, toContentY(rect.top));
|
|
60
|
+
maxY = Math.max(maxY, toContentY(rect.bottom));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const computeFitViewState = (
|
|
67
|
+
containerEl,
|
|
68
|
+
{ zoom, offset, padding, minZoom, maxZoom },
|
|
69
|
+
) => {
|
|
70
|
+
const bounds = computeContentBounds(containerEl, { zoom, offset });
|
|
71
|
+
if (!bounds) return null;
|
|
72
|
+
|
|
73
|
+
return computeFitTransform({
|
|
74
|
+
bounds,
|
|
75
|
+
containerWidth: containerEl.clientWidth,
|
|
76
|
+
containerHeight: containerEl.clientHeight,
|
|
77
|
+
padding,
|
|
78
|
+
minZoom,
|
|
79
|
+
maxZoom,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Button, Stack } from "@mui/material";
|
|
2
|
+
import React, { useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { Flow } from "../lib/Flow";
|
|
5
|
+
|
|
6
|
+
const treeToLinked = (tree) => {
|
|
7
|
+
if (!tree) return { nodes: {}, roots: [] };
|
|
8
|
+
|
|
9
|
+
const nodes = {};
|
|
10
|
+
const roots = [];
|
|
11
|
+
|
|
12
|
+
const walk = (node, parentId = null) => {
|
|
13
|
+
if (!node || !node.id) return;
|
|
14
|
+
|
|
15
|
+
const { children, ...rest } = node;
|
|
16
|
+
if (!nodes[node.id]) {
|
|
17
|
+
nodes[node.id] = { ...rest, id: node.id };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const kids = Array.isArray(children) ? children : [];
|
|
21
|
+
const nextIds = kids.map((c) => c.id).filter(Boolean);
|
|
22
|
+
|
|
23
|
+
if (nextIds.length === 1) {
|
|
24
|
+
nodes[node.id].next = nextIds[0];
|
|
25
|
+
} else if (nextIds.length > 1) {
|
|
26
|
+
nodes[node.id].next = nextIds;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (parentId) {
|
|
30
|
+
nodes[node.id].previous = parentId;
|
|
31
|
+
} else if (!roots.includes(node.id)) {
|
|
32
|
+
roots.push(node.id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
kids.forEach((child) => walk(child, node.id));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
walk(tree);
|
|
39
|
+
return { nodes, roots };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
title: "Components/FlowFitView",
|
|
44
|
+
component: Flow,
|
|
45
|
+
parameters: {
|
|
46
|
+
layout: "fullscreen",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const wideTree = {
|
|
51
|
+
id: "root",
|
|
52
|
+
label: "Root",
|
|
53
|
+
children: [
|
|
54
|
+
{
|
|
55
|
+
id: "branch1",
|
|
56
|
+
label: "Branch 1",
|
|
57
|
+
children: [
|
|
58
|
+
{ id: "leaf1-1", label: "Leaf 1.1", children: [] },
|
|
59
|
+
{ id: "leaf1-2", label: "Leaf 1.2", children: [] },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "branch2",
|
|
64
|
+
label: "Branch 2",
|
|
65
|
+
children: [
|
|
66
|
+
{ id: "leaf2-1", label: "Leaf 2.1", children: [] },
|
|
67
|
+
{ id: "leaf2-2", label: "Leaf 2.2", children: [] },
|
|
68
|
+
{ id: "leaf2-3", label: "Leaf 2.3", children: [] },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "branch3",
|
|
73
|
+
label: "Branch 3",
|
|
74
|
+
children: [{ id: "leaf3-1", label: "Leaf 3.1", children: [] }],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const FitViewDemo = {
|
|
80
|
+
render: () => {
|
|
81
|
+
const flowRef = useRef(null);
|
|
82
|
+
const data = treeToLinked(wideTree);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Stack sx={{ height: "100vh" }}>
|
|
86
|
+
<Stack direction="row" spacing={1} sx={{ p: 1 }}>
|
|
87
|
+
<Button
|
|
88
|
+
variant="contained"
|
|
89
|
+
onClick={() => flowRef.current?.fitView()}
|
|
90
|
+
>
|
|
91
|
+
Fit View
|
|
92
|
+
</Button>
|
|
93
|
+
<Button variant="outlined" onClick={() => flowRef.current?.zoomIn()}>
|
|
94
|
+
Zoom In
|
|
95
|
+
</Button>
|
|
96
|
+
<Button
|
|
97
|
+
variant="outlined"
|
|
98
|
+
onClick={() => flowRef.current?.zoomOut()}
|
|
99
|
+
>
|
|
100
|
+
Zoom Out
|
|
101
|
+
</Button>
|
|
102
|
+
</Stack>
|
|
103
|
+
<Flow
|
|
104
|
+
ref={flowRef}
|
|
105
|
+
data={data}
|
|
106
|
+
variant="simple"
|
|
107
|
+
height="calc(100vh - 56px)"
|
|
108
|
+
minZoom={0.25}
|
|
109
|
+
maxZoom={2.5}
|
|
110
|
+
fitViewPadding={60}
|
|
111
|
+
fitViewMaxZoom={1}
|
|
112
|
+
fitViewOnMount
|
|
113
|
+
/>
|
|
114
|
+
</Stack>
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const FitViewOnNodesChangeDemo = {
|
|
120
|
+
render: () => {
|
|
121
|
+
const flowRef = useRef(null);
|
|
122
|
+
const [tree, setTree] = useState(wideTree);
|
|
123
|
+
const data = treeToLinked(tree);
|
|
124
|
+
|
|
125
|
+
const addLeafToBranch1 = () => {
|
|
126
|
+
setTree((prev) => {
|
|
127
|
+
const branch1 = prev.children.find((c) => c.id === "branch1");
|
|
128
|
+
const nextIndex = branch1.children.length + 1;
|
|
129
|
+
const newLeaf = {
|
|
130
|
+
id: `leaf1-${nextIndex}-${Date.now()}`,
|
|
131
|
+
label: `Leaf 1.${nextIndex}`,
|
|
132
|
+
children: [],
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
...prev,
|
|
136
|
+
children: prev.children.map((c) =>
|
|
137
|
+
c.id === "branch1"
|
|
138
|
+
? { ...c, children: [...c.children, newLeaf] }
|
|
139
|
+
: c,
|
|
140
|
+
),
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Stack sx={{ height: "100vh" }}>
|
|
147
|
+
<Stack direction="row" spacing={1} sx={{ p: 1 }}>
|
|
148
|
+
<Button variant="contained" onClick={addLeafToBranch1}>
|
|
149
|
+
Add Node
|
|
150
|
+
</Button>
|
|
151
|
+
<Button
|
|
152
|
+
variant="outlined"
|
|
153
|
+
onClick={() => flowRef.current?.fitView()}
|
|
154
|
+
>
|
|
155
|
+
Fit View
|
|
156
|
+
</Button>
|
|
157
|
+
</Stack>
|
|
158
|
+
<Flow
|
|
159
|
+
ref={flowRef}
|
|
160
|
+
data={data}
|
|
161
|
+
variant="simple"
|
|
162
|
+
height="calc(100vh - 56px)"
|
|
163
|
+
minZoom={0.25}
|
|
164
|
+
maxZoom={2.5}
|
|
165
|
+
fitViewPadding={60}
|
|
166
|
+
fitViewMaxZoom={1}
|
|
167
|
+
fitViewOnMount
|
|
168
|
+
fitViewOnNodesChange
|
|
169
|
+
/>
|
|
170
|
+
</Stack>
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
};
|