@likec4/diagram 1.1.1 → 1.2.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/KeepAspectRatioContainer.d.ts +1 -1
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +7 -2
- package/dist/StaticLikeC4Diagram.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2517 -1326
- package/dist/mantine/EnsureMantine.d.ts +1 -1
- package/dist/state/DiagramContext.d.ts +2125 -10
- package/dist/state/WhenInitialized.d.ts +1 -1
- package/dist/state/diagramStore.d.ts +2133 -10
- package/dist/state/useDiagramStore.d.ts +2124 -9
- package/dist/style.css +210 -136
- package/dist/theme.css.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.d.ts +1 -2
- package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
- package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
- package/dist/ui/OptionsPanel.d.ts +1 -2
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/options/NodeOptions.d.ts +1 -2
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
- package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
- package/dist/xyflow/XYFlow.d.ts +4 -7
- package/dist/xyflow/XYFlowBackground.d.ts +5 -4
- package/dist/xyflow/XYFlowEvents.d.ts +57 -63
- package/dist/xyflow/XYFlowInner.d.ts +11 -0
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +14 -14
- package/dist/xyflow/edges/edges.css.d.ts +4 -2
- package/dist/xyflow/edges/utils.d.ts +2 -0
- package/dist/xyflow/hooks/useXYFlow.d.ts +126 -126
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
- package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
- package/dist/xyflow/types.d.ts +18 -18
- package/package.json +19 -16
- package/src/KeepAspectRatioContainer.tsx +1 -1
- package/src/LikeC4Diagram.css.ts +2 -0
- package/src/LikeC4Diagram.props.ts +9 -4
- package/src/LikeC4Diagram.tsx +17 -11
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/globals.css.ts +1 -0
- package/src/index.ts +0 -3
- package/src/state/DiagramContext.tsx +21 -12
- package/src/state/diagramStore.ts +177 -38
- package/src/theme.css.ts +1 -0
- package/src/ui/DiagramTitlePanel.css.ts +2 -1
- package/src/ui/DiagramTitlePanel.tsx +3 -4
- package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
- package/src/ui/DynamicViewWalkthrough.tsx +86 -0
- package/src/ui/OptionsPanel.tsx +5 -3
- package/src/ui/index.ts +1 -0
- package/src/ui/options/NodeOptions.tsx +23 -24
- package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -39
- package/src/xyflow/SyncWithDiagram.tsx +55 -107
- package/src/xyflow/XYFlow.tsx +51 -35
- package/src/xyflow/XYFlowBackground.tsx +7 -5
- package/src/xyflow/XYFlowEvents.tsx +97 -92
- package/src/xyflow/XYFlowInner.tsx +50 -0
- package/src/xyflow/diagram-to-xyflow.ts +16 -21
- package/src/xyflow/edges/RelationshipEdge.tsx +70 -34
- package/src/xyflow/edges/edges.css.ts +68 -34
- package/src/xyflow/edges/utils.ts +31 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +6 -2
- package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
- package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
- package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
- package/src/xyflow/nodes/element/element.css.ts +9 -2
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
- package/src/xyflow/types.ts +7 -8
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ActionIcon, Box, Button, rem, useComputedColorScheme } from '@mantine/core'
|
|
2
|
+
import {
|
|
3
|
+
IconHeart,
|
|
4
|
+
IconPhoto,
|
|
5
|
+
IconPlayerPlayFilled,
|
|
6
|
+
IconPlayerSkipBackFilled,
|
|
7
|
+
IconPlayerSkipForwardFilled,
|
|
8
|
+
IconPlayerStopFilled,
|
|
9
|
+
IconSettings
|
|
10
|
+
} from '@tabler/icons-react'
|
|
11
|
+
import clsx from 'clsx'
|
|
12
|
+
import { isNullish, isNumber } from 'remeda'
|
|
13
|
+
import { useDiagramState } from '../state/useDiagramStore'
|
|
14
|
+
import * as css from './DynamicViewWalkthrough.css'
|
|
15
|
+
|
|
16
|
+
export function DynamicViewWalkthrough() {
|
|
17
|
+
const {
|
|
18
|
+
nextDynamicStep,
|
|
19
|
+
stopDynamicView,
|
|
20
|
+
activeDynamicViewStep,
|
|
21
|
+
hasNextSteps
|
|
22
|
+
} = useDiagramState(s => ({
|
|
23
|
+
nextDynamicStep: s.nextDynamicStep,
|
|
24
|
+
stopDynamicView: s.stopDynamicView,
|
|
25
|
+
activeDynamicViewStep: s.activeDynamicViewStep,
|
|
26
|
+
hasNextSteps: (s.activeDynamicViewStep ?? 1) < s.view.edges.length
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
const buttonProps = {
|
|
30
|
+
className: css.btn,
|
|
31
|
+
size: 'compact-xl',
|
|
32
|
+
radius: 'xl'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nextStep = (increment = 1) => (e: React.MouseEvent) => {
|
|
36
|
+
e.stopPropagation()
|
|
37
|
+
nextDynamicStep(increment)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Box
|
|
42
|
+
className={clsx('react-flow__panel', css.container)}
|
|
43
|
+
>
|
|
44
|
+
{isNullish(activeDynamicViewStep)
|
|
45
|
+
&& (
|
|
46
|
+
<Button
|
|
47
|
+
{...buttonProps}
|
|
48
|
+
className={clsx(css.buttons, css.btn)}
|
|
49
|
+
rightSection={<IconPlayerPlayFilled />}
|
|
50
|
+
onClick={nextStep()}
|
|
51
|
+
px={'xl'}>
|
|
52
|
+
Start
|
|
53
|
+
</Button>
|
|
54
|
+
)}
|
|
55
|
+
{isNumber(activeDynamicViewStep) && (
|
|
56
|
+
<Button.Group className={css.buttons}>
|
|
57
|
+
<Button
|
|
58
|
+
{...buttonProps}
|
|
59
|
+
pl={'lg'}
|
|
60
|
+
disabled={activeDynamicViewStep === 1}
|
|
61
|
+
onClick={nextStep(-1)}>
|
|
62
|
+
<IconPlayerSkipBackFilled />
|
|
63
|
+
</Button>
|
|
64
|
+
|
|
65
|
+
<Button
|
|
66
|
+
{...buttonProps}
|
|
67
|
+
px={'sm'}
|
|
68
|
+
onClick={e => {
|
|
69
|
+
e.stopPropagation()
|
|
70
|
+
stopDynamicView()
|
|
71
|
+
}}>
|
|
72
|
+
<IconPlayerStopFilled />
|
|
73
|
+
</Button>
|
|
74
|
+
|
|
75
|
+
<Button
|
|
76
|
+
{...buttonProps}
|
|
77
|
+
pr={'lg'}
|
|
78
|
+
disabled={!hasNextSteps}
|
|
79
|
+
onClick={nextStep()}>
|
|
80
|
+
<IconPlayerSkipForwardFilled />
|
|
81
|
+
</Button>
|
|
82
|
+
</Button.Group>
|
|
83
|
+
)}
|
|
84
|
+
</Box>
|
|
85
|
+
)
|
|
86
|
+
}
|
package/src/ui/OptionsPanel.tsx
CHANGED
|
@@ -4,10 +4,13 @@ import clsx from 'clsx'
|
|
|
4
4
|
import { shallowEqual } from 'fast-equals'
|
|
5
5
|
import { AnimatePresence, motion } from 'framer-motion'
|
|
6
6
|
import { useState } from 'react'
|
|
7
|
-
import
|
|
7
|
+
import { isNullish, isTruthy } from 'remeda'
|
|
8
|
+
import { useDiagramState } from '../state'
|
|
8
9
|
import { NodeOptions } from './options/NodeOptions'
|
|
10
|
+
import * as styles from './OptionsPanel.css'
|
|
9
11
|
|
|
10
12
|
export default function OptionsPanel() {
|
|
13
|
+
const isFocusDisabled = useDiagramState(s => isNullish(s.focusedNodeId))
|
|
11
14
|
const [selectedNodes, setSelectedNodes] = useState([] as string[])
|
|
12
15
|
|
|
13
16
|
useOnSelectionChange({
|
|
@@ -19,10 +22,9 @@ export default function OptionsPanel() {
|
|
|
19
22
|
}
|
|
20
23
|
})
|
|
21
24
|
|
|
22
|
-
|
|
23
25
|
return (
|
|
24
26
|
<AnimatePresence mode="wait">
|
|
25
|
-
{selectedNodes.length > 0 && (
|
|
27
|
+
{isFocusDisabled && selectedNodes.length > 0 && (
|
|
26
28
|
<motion.div
|
|
27
29
|
initial={{ opacity: 0, scale: 0.85 }}
|
|
28
30
|
animate={{ opacity: 1, scale: 1 }}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ElementShapes,
|
|
3
|
-
defaultTheme,
|
|
4
|
-
invariant,
|
|
5
2
|
type BorderStyle,
|
|
3
|
+
defaultTheme,
|
|
6
4
|
type ElementShape,
|
|
5
|
+
ElementShapes,
|
|
6
|
+
invariant,
|
|
7
7
|
type NonEmptyArray,
|
|
8
8
|
type ThemeColor
|
|
9
9
|
} from '@likec4/core'
|
|
@@ -13,14 +13,14 @@ import {
|
|
|
13
13
|
ColorSwatch,
|
|
14
14
|
Divider,
|
|
15
15
|
Flex,
|
|
16
|
+
rem,
|
|
16
17
|
SegmentedControl,
|
|
17
18
|
Select,
|
|
18
19
|
Slider,
|
|
19
20
|
Stack,
|
|
20
21
|
Text,
|
|
21
22
|
Tooltip,
|
|
22
|
-
TooltipGroup
|
|
23
|
-
rem
|
|
23
|
+
TooltipGroup
|
|
24
24
|
} from '@mantine/core'
|
|
25
25
|
import { useEffect, useState } from 'react'
|
|
26
26
|
import { hasAtLeast, keys, takeWhile } from 'remeda'
|
|
@@ -57,7 +57,7 @@ export type ColorKey = typeof colors[0]['key']
|
|
|
57
57
|
type XYNodesData = Pick<XYFlowNode, 'id' | 'data' | 'type'>
|
|
58
58
|
|
|
59
59
|
export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] }) {
|
|
60
|
-
// export function NodeOptions(props: { nodes: XYFlowNode[] }) {
|
|
60
|
+
// export function NodeOptions(props: { nodes: XYFlowNode[] }) {
|
|
61
61
|
const diagramApi = useDiagramStoreApi()
|
|
62
62
|
// const nodes = useXYNodesData(props.nodes.map(node => node.id))
|
|
63
63
|
const nodes = useXYNodesData(selectedNodeIds)
|
|
@@ -104,25 +104,24 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
104
104
|
onColorChange={(color: ColorKey | ThemeColorKey) => {
|
|
105
105
|
triggerChange({ color })
|
|
106
106
|
}} />
|
|
107
|
+
|
|
108
|
+
{showOpacityOption && (
|
|
109
|
+
<Box key={firstNode.id}>
|
|
110
|
+
<Divider label="opacity and border" labelPosition="left" />
|
|
111
|
+
<OpacityOption
|
|
112
|
+
node={firstNode}
|
|
113
|
+
onOpacityChange={(opacity: number) => {
|
|
114
|
+
triggerChange({ opacity })
|
|
115
|
+
}} />
|
|
116
|
+
<BorderStyleOption
|
|
117
|
+
node={firstNode}
|
|
118
|
+
onChange={(border: BorderStyle) => {
|
|
119
|
+
triggerChange({ border })
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
</Box>
|
|
123
|
+
)}
|
|
107
124
|
<NavigateToOption nodes={nodes} />
|
|
108
|
-
<Box key={firstNode.id}>
|
|
109
|
-
{showOpacityOption && (
|
|
110
|
-
<>
|
|
111
|
-
<Divider label="opacity and border" labelPosition="left" />
|
|
112
|
-
<OpacityOption
|
|
113
|
-
node={firstNode}
|
|
114
|
-
onOpacityChange={(opacity: number) => {
|
|
115
|
-
triggerChange({ opacity })
|
|
116
|
-
}} />
|
|
117
|
-
<BorderStyleOption
|
|
118
|
-
node={firstNode}
|
|
119
|
-
onChange={(border: BorderStyle) => {
|
|
120
|
-
triggerChange({ border })
|
|
121
|
-
}}
|
|
122
|
-
/>
|
|
123
|
-
</>
|
|
124
|
-
)}
|
|
125
|
-
</Box>
|
|
126
125
|
</Stack>
|
|
127
126
|
)
|
|
128
127
|
}
|
|
@@ -1,99 +1,119 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { memo, useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useDebouncedEffect } from '@react-hookz/web'
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
4
3
|
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state'
|
|
5
|
-
import { useXYStore } from './hooks'
|
|
4
|
+
import { useXYStore, useXYStoreApi, type XYStoreApi } from './hooks'
|
|
6
5
|
import type { XYFlowState } from './types'
|
|
6
|
+
import { toDomPrecision } from './utils'
|
|
7
7
|
|
|
8
8
|
function selectDimensions(state: XYFlowState) {
|
|
9
9
|
return `${Math.round(state.width)}:${Math.round(state.height)}`
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function FitViewOnViewportResize({ diagramApi }: {
|
|
12
|
+
function FitViewOnViewportResize({ diagramApi, xyflowApi }: {
|
|
13
|
+
xyflowApi: XYStoreApi
|
|
14
|
+
diagramApi: DiagramStoreApi
|
|
15
|
+
}) {
|
|
13
16
|
const dimensions = useXYStore(selectDimensions)
|
|
14
17
|
const prevDimensionsRef = useRef(dimensions)
|
|
15
18
|
|
|
16
19
|
useDebouncedEffect(
|
|
17
20
|
() => {
|
|
18
|
-
|
|
21
|
+
const { focusedNodeId, fitDiagram } = diagramApi.getState()
|
|
22
|
+
if (focusedNodeId || prevDimensionsRef.current === dimensions) {
|
|
19
23
|
return
|
|
20
24
|
}
|
|
21
|
-
|
|
25
|
+
fitDiagram(xyflowApi)
|
|
22
26
|
prevDimensionsRef.current = dimensions
|
|
23
27
|
},
|
|
24
|
-
[dimensions],
|
|
25
|
-
|
|
28
|
+
[dimensions, diagramApi, xyflowApi],
|
|
29
|
+
200,
|
|
30
|
+
800
|
|
26
31
|
)
|
|
27
32
|
|
|
28
33
|
return null
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
function selector({ view, xyflowSynced, viewportChanged, lastOnNavigate, ...s }: DiagramState) {
|
|
37
|
+
const viewId = view.id + '_' + view.autoLayout + '_' + s.fitViewPadding
|
|
38
|
+
|
|
39
|
+
const waitCorrection = lastOnNavigate && lastOnNavigate.toView === view.id
|
|
40
|
+
&& lastOnNavigate.positionCorrected === false
|
|
41
|
+
const elTo = waitCorrection ? view.nodes.find(n => n.id === lastOnNavigate.element.id) : undefined
|
|
42
|
+
if (waitCorrection && elTo) {
|
|
43
|
+
return {
|
|
44
|
+
viewId,
|
|
45
|
+
xyflowSynced,
|
|
46
|
+
viewportChanged,
|
|
47
|
+
waitCorrection: true,
|
|
48
|
+
elFrom: lastOnNavigate.element,
|
|
49
|
+
elTo
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
viewId,
|
|
55
|
+
xyflowSynced,
|
|
56
|
+
viewportChanged,
|
|
57
|
+
waitCorrection: false,
|
|
58
|
+
fromElement: null,
|
|
59
|
+
toElement: null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
32
62
|
|
|
33
63
|
/**
|
|
34
64
|
* Fits the view when the view changes and nodes are initialized
|
|
35
65
|
*/
|
|
36
|
-
function
|
|
66
|
+
export function FitViewOnDiagramChange() {
|
|
67
|
+
const xyflowApi = useXYStoreApi()
|
|
68
|
+
const {
|
|
69
|
+
viewId: pendingViewId,
|
|
70
|
+
xyflowSynced,
|
|
71
|
+
viewportChanged,
|
|
72
|
+
waitCorrection,
|
|
73
|
+
elFrom,
|
|
74
|
+
elTo
|
|
75
|
+
} = useDiagramState(selector)
|
|
37
76
|
const diagramApi = useDiagramStoreApi()
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
),
|
|
60
|
-
[diagramApi, schedulePending]
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
useEffect(
|
|
64
|
-
() =>
|
|
65
|
-
diagramApi.subscribe(
|
|
66
|
-
s => s.viewportChanged,
|
|
67
|
-
setViewportMoved
|
|
68
|
-
),
|
|
69
|
-
[diagramApi, setViewportMoved]
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
useRafEffect(
|
|
73
|
-
() => {
|
|
74
|
-
if (!isReady || processedChangeId === pendingChangeId) {
|
|
75
|
-
return
|
|
77
|
+
const processedRef = useRef(pendingViewId)
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!xyflowSynced || pendingViewId === processedRef.current) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (waitCorrection && elTo && elFrom) {
|
|
84
|
+
const [x, y, zoom] = xyflowApi.getState().transform
|
|
85
|
+
const { xyflow, lastOnNavigate } = diagramApi.getState()
|
|
86
|
+
const nextZoom = Math.min(
|
|
87
|
+
elFrom.width / elTo.width,
|
|
88
|
+
elFrom.height / elTo.height
|
|
89
|
+
)
|
|
90
|
+
const centerFrom = lastOnNavigate!.elementCenterScreenPosition
|
|
91
|
+
if (nextZoom !== 1 && nextZoom < zoom) {
|
|
92
|
+
xyflowApi.setState({ transform: [x, y, nextZoom] })
|
|
93
|
+
xyflow.setViewport({
|
|
94
|
+
x,
|
|
95
|
+
y,
|
|
96
|
+
zoom: nextZoom
|
|
97
|
+
})
|
|
76
98
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
const centerTo = xyflow.flowToScreenPosition({
|
|
100
|
+
x: elTo.position[0] + elTo.width / 2,
|
|
101
|
+
y: elTo.position[1] + elTo.height / 2
|
|
102
|
+
}),
|
|
103
|
+
diff = {
|
|
104
|
+
x: toDomPrecision(centerFrom.x - centerTo.x),
|
|
105
|
+
y: toDomPrecision(centerFrom.y - centerTo.y)
|
|
106
|
+
}
|
|
107
|
+
xyflowApi.getState().panBy(diff)
|
|
108
|
+
diagramApi.setState({ lastOnNavigate: null })
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
processedRef.current = pendingViewId
|
|
112
|
+
diagramApi.getState().fitDiagram(xyflowApi)
|
|
113
|
+
}, [pendingViewId, xyflowSynced, waitCorrection])
|
|
114
|
+
|
|
115
|
+
if (!viewportChanged && xyflowSynced) {
|
|
116
|
+
return <FitViewOnViewportResize xyflowApi={xyflowApi} diagramApi={diagramApi} />
|
|
85
117
|
}
|
|
86
118
|
return null
|
|
87
119
|
}
|
|
88
|
-
|
|
89
|
-
const selectFitViewFlag = (s: DiagramState) => s.fitViewEnabled
|
|
90
|
-
|
|
91
|
-
export const FitViewOnDiagramChange = memo(() => {
|
|
92
|
-
const fitView = useDiagramState(selectFitViewFlag)
|
|
93
|
-
|
|
94
|
-
if (!fitView) {
|
|
95
|
-
return null
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return <FitViewOnDiagramChanges />
|
|
99
|
-
})
|
|
@@ -1,57 +1,75 @@
|
|
|
1
|
+
import { nonNullable } from '@likec4/core'
|
|
1
2
|
import type { EdgeChange, NodeChange } from '@xyflow/react'
|
|
2
|
-
import { useUpdateEffect } from '../hooks'
|
|
3
|
-
import { useDiagramState } from '../state/useDiagramStore'
|
|
3
|
+
import { useUpdateEffect } from '../hooks/useUpdateEffect'
|
|
4
|
+
import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
|
|
5
|
+
import { MinZoom } from './const'
|
|
4
6
|
import { useXYStoreApi } from './hooks/useXYFlow'
|
|
5
7
|
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
6
8
|
|
|
7
9
|
export function SelectEdgesOnNodeFocus() {
|
|
8
10
|
const focusedNodeId = useDiagramState(s => s.focusedNodeId)
|
|
11
|
+
const diagramStore = useDiagramStoreApi()
|
|
9
12
|
const xyflowApi = useXYStoreApi()
|
|
10
13
|
|
|
11
14
|
useUpdateEffect(() => {
|
|
15
|
+
if (!focusedNodeId) {
|
|
16
|
+
xyflowApi.getState().resetSelectedElements()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
|
|
13
21
|
const nodeChanges = [] as NodeChange<XYFlowNode>[]
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
for (const node of nodes) {
|
|
32
|
-
if (node.selected && node.id !== focusedNodeId) {
|
|
33
|
-
nodeChanges.push({
|
|
34
|
-
id: node.id,
|
|
35
|
-
type: 'select',
|
|
36
|
-
selected: false
|
|
37
|
-
})
|
|
22
|
+
const { edgeLookup, nodeLookup, transform, triggerNodeChanges, fitView, triggerEdgeChanges } = xyflowApi.getState()
|
|
23
|
+
const fitViewNodes = new Set<XYFlowNode>()
|
|
24
|
+
fitViewNodes.add(nonNullable(nodeLookup.get(focusedNodeId)))
|
|
25
|
+
|
|
26
|
+
for (const [, edge] of edgeLookup) {
|
|
27
|
+
if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
|
|
28
|
+
edgeChanges.push({
|
|
29
|
+
id: edge.id,
|
|
30
|
+
type: 'select',
|
|
31
|
+
selected: true
|
|
32
|
+
})
|
|
33
|
+
const anotherNode = nodeLookup.get(edge.source === focusedNodeId ? edge.target : edge.source)
|
|
34
|
+
if (anotherNode) {
|
|
35
|
+
fitViewNodes.add(anotherNode)
|
|
38
36
|
}
|
|
37
|
+
} else if (edge.selected) {
|
|
38
|
+
edgeChanges.push({
|
|
39
|
+
id: edge.id,
|
|
40
|
+
type: 'select',
|
|
41
|
+
selected: false
|
|
42
|
+
})
|
|
39
43
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (nodeChanges.length > 0) {
|
|
49
|
-
triggerNodeChanges(nodeChanges)
|
|
44
|
+
}
|
|
45
|
+
for (const [, node] of nodeLookup) {
|
|
46
|
+
if (node.selected && node.id !== focusedNodeId) {
|
|
47
|
+
nodeChanges.push({
|
|
48
|
+
id: node.id,
|
|
49
|
+
type: 'select',
|
|
50
|
+
selected: false
|
|
51
|
+
})
|
|
50
52
|
}
|
|
51
|
-
} else {
|
|
52
|
-
xyflowApi.getState().resetSelectedElements()
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
nodeChanges.push({
|
|
55
|
+
id: focusedNodeId,
|
|
56
|
+
type: 'select',
|
|
57
|
+
selected: true
|
|
58
|
+
})
|
|
59
|
+
if (edgeChanges.length > 0) {
|
|
60
|
+
triggerEdgeChanges(edgeChanges)
|
|
61
|
+
}
|
|
62
|
+
triggerNodeChanges(nodeChanges)
|
|
63
|
+
|
|
64
|
+
fitView({
|
|
65
|
+
includeHiddenNodes: true,
|
|
66
|
+
duration: 300,
|
|
67
|
+
padding: 0.1,
|
|
68
|
+
minZoom: MinZoom,
|
|
69
|
+
maxZoom: Math.max(1, transform[2]),
|
|
70
|
+
nodes: Array.from(fitViewNodes)
|
|
71
|
+
})
|
|
72
|
+
}, [diagramStore, xyflowApi, focusedNodeId])
|
|
55
73
|
|
|
56
74
|
return null
|
|
57
75
|
}
|
|
@@ -1,125 +1,73 @@
|
|
|
1
|
-
import { deepEqual as eq
|
|
2
|
-
import {
|
|
3
|
-
import type { XYFlowNode } from './types'
|
|
1
|
+
import { deepEqual as eq } from 'fast-equals'
|
|
2
|
+
import { useXYStoreApi } from './hooks'
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
import { useDiagramStoreApi } from '../state'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import { useDiagramState, useDiagramStoreApi } from '../state'
|
|
7
6
|
import { diagramViewToXYFlowData } from './diagram-to-xyflow'
|
|
8
|
-
import { toDomPrecision } from './utils'
|
|
9
|
-
|
|
10
|
-
const isSameNode = (a: XYFlowNode) => (b: XYFlowNode) =>
|
|
11
|
-
a.id === b.id && a.type === b.type && Object.is(a.parentId, b.parentId)
|
|
12
7
|
|
|
13
8
|
/**
|
|
14
9
|
* Syncs the diagram state with the XYFlow instance
|
|
15
10
|
*/
|
|
16
|
-
export
|
|
17
|
-
const
|
|
18
|
-
const xyflowRef = useRef(_xyflow)
|
|
19
|
-
xyflowRef.current = _xyflow
|
|
20
|
-
|
|
11
|
+
export function SyncWithDiagram() {
|
|
12
|
+
const xyflowSynced = useDiagramState(s => s.xyflowSynced)
|
|
21
13
|
const xyflowApi = useXYStoreApi()
|
|
22
14
|
const diagramStoreApi = useDiagramStoreApi()
|
|
23
15
|
|
|
24
16
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
focusedNodeId
|
|
39
|
-
} = diagramStoreApi.getState()
|
|
40
|
-
const updates = diagramViewToXYFlowData({ nodes, edges }, {
|
|
41
|
-
draggable: nodesDraggable,
|
|
42
|
-
selectable: nodesSelectable || focusedNodeId !== null
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
xyflowRef.current.setNodes(prev =>
|
|
46
|
-
updates.nodes.map(update => {
|
|
47
|
-
const existing = prev.find(isSameNode(update))
|
|
48
|
-
if (existing) {
|
|
49
|
-
if (eq(existing.data.element, update.data.element)) {
|
|
50
|
-
return existing
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
...existing,
|
|
54
|
-
...update
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return update
|
|
58
|
-
})
|
|
59
|
-
)
|
|
17
|
+
if (xyflowSynced) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
const {
|
|
21
|
+
view,
|
|
22
|
+
nodesDraggable,
|
|
23
|
+
nodesSelectable,
|
|
24
|
+
focusedNodeId
|
|
25
|
+
} = diagramStoreApi.getState()
|
|
26
|
+
const updates = diagramViewToXYFlowData(view, {
|
|
27
|
+
draggable: nodesDraggable,
|
|
28
|
+
selectable: nodesSelectable || focusedNodeId !== null
|
|
29
|
+
})
|
|
60
30
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
...update
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return update
|
|
71
|
-
})
|
|
72
|
-
)
|
|
31
|
+
const {
|
|
32
|
+
nodeLookup,
|
|
33
|
+
edgeLookup,
|
|
34
|
+
setNodes,
|
|
35
|
+
setEdges
|
|
36
|
+
} = xyflowApi.getState()
|
|
73
37
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
xyflowRef.current.setViewport({
|
|
88
|
-
x: v.x,
|
|
89
|
-
y: v.y,
|
|
90
|
-
zoom: nextZoom
|
|
91
|
-
})
|
|
92
|
-
}
|
|
38
|
+
setNodes(updates.nodes.map(update => {
|
|
39
|
+
const existing = nodeLookup.get(update.id)?.internals.userNode
|
|
40
|
+
if (existing && existing.type === update.type && eq(existing.parentId, update.parentId)) {
|
|
41
|
+
if (eq(existing.data.element, update.data.element)) {
|
|
42
|
+
return existing
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
...existing,
|
|
46
|
+
...update
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return update
|
|
50
|
+
}))
|
|
93
51
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
y: toDomPrecision(centerFrom.y - centerTo.y)
|
|
101
|
-
}
|
|
102
|
-
xyflowApi.getState().panBy(diff)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
diagramStoreApi.setState(
|
|
107
|
-
{
|
|
108
|
-
lastOnNavigate: {
|
|
109
|
-
...lastOnNavigate,
|
|
110
|
-
positionCorrected: true
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
false,
|
|
114
|
-
'positionCorrected'
|
|
115
|
-
)
|
|
52
|
+
setEdges(updates.edges.map(update => {
|
|
53
|
+
const existing = edgeLookup.get(update.id)
|
|
54
|
+
if (existing) {
|
|
55
|
+
return eq(existing.data.edge, update.data.edge) ? existing : {
|
|
56
|
+
...existing,
|
|
57
|
+
...update
|
|
116
58
|
}
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
equalityFn: shallowEqual
|
|
120
59
|
}
|
|
60
|
+
return update
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
diagramStoreApi.setState(
|
|
64
|
+
{
|
|
65
|
+
xyflowSynced: true
|
|
66
|
+
},
|
|
67
|
+
false,
|
|
68
|
+
'xyflowSynced'
|
|
121
69
|
)
|
|
122
|
-
}, [xyflowApi, diagramStoreApi])
|
|
70
|
+
}, [xyflowSynced, xyflowApi, diagramStoreApi])
|
|
123
71
|
|
|
124
72
|
return null
|
|
125
|
-
}
|
|
73
|
+
}
|