@likec4/diagram 1.1.0 → 1.1.1
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/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +16 -11
- package/dist/index.mjs +413 -101
- package/dist/state/DiagramContext.d.ts +18 -9
- package/dist/state/diagramStore.d.ts +19 -9
- package/dist/state/useDiagramStore.d.ts +18 -9
- package/dist/style.css +99 -35
- package/dist/ui/DiagramTitlePanel.css.d.ts +4 -0
- package/dist/ui/DiagramTitlePanel.d.ts +2 -0
- package/dist/ui/OptionsPanel.d.ts +1 -2
- package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +1 -0
- package/dist/xyflow/XYFlowEvents.d.ts +46 -40
- package/dist/xyflow/edges/edges.css.d.ts +1 -0
- package/package.json +4 -2
- package/src/LikeC4Diagram.css.ts +2 -1
- package/src/LikeC4Diagram.props.ts +17 -12
- package/src/LikeC4Diagram.tsx +3 -0
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/globals.css.ts +6 -1
- package/src/state/diagramStore.ts +101 -63
- package/src/ui/DiagramTitlePanel.css.ts +32 -0
- package/src/ui/DiagramTitlePanel.tsx +139 -0
- package/src/ui/OptionsPanel.tsx +4 -23
- package/src/ui/options/NodeOptions.tsx +107 -30
- package/src/xyflow/FitviewOnDiagramChange.tsx +2 -2
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -0
- package/src/xyflow/SyncWithDiagram.tsx +12 -6
- package/src/xyflow/XYFlow.tsx +7 -3
- package/src/xyflow/XYFlowEvents.tsx +145 -104
- package/src/xyflow/edges/RelationshipEdge.tsx +43 -47
- package/src/xyflow/edges/edges.css.ts +77 -36
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +17 -13
- package/src/xyflow/nodes/element/element.css.ts +1 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Anchor, Box, Button, CopyButton, Group, Paper, Spoiler, Stack, Text } from '@mantine/core'
|
|
2
|
+
import { useToggle } from '@react-hookz/web'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
import { AnimatePresence, motion } from 'framer-motion'
|
|
5
|
+
import { type DiagramState, useDiagramState } from '../state'
|
|
6
|
+
import * as css from './DiagramTitlePanel.css'
|
|
7
|
+
import { useLocalStorage } from '@mantine/hooks'
|
|
8
|
+
|
|
9
|
+
const selector = (s: DiagramState) => ({
|
|
10
|
+
id: s.view.id,
|
|
11
|
+
title: s.view.title ?? 'untitled',
|
|
12
|
+
description: s.view.description,
|
|
13
|
+
links: s.view.links
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export default function DiagramTitlePanel() {
|
|
17
|
+
const { id, title, description, links } = useDiagramState(selector)
|
|
18
|
+
const [isCollapsed, setCollapsed] = useLocalStorage({
|
|
19
|
+
key: 'diagram-title-panel-collapsed',
|
|
20
|
+
defaultValue: false
|
|
21
|
+
})
|
|
22
|
+
const toggle = () => setCollapsed(v => !v)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<AnimatePresence mode="wait">
|
|
26
|
+
<motion.div
|
|
27
|
+
key={id}
|
|
28
|
+
initial={{ opacity: 0.05, scale: 0.7 }}
|
|
29
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
30
|
+
exit={{
|
|
31
|
+
opacity: 0.05,
|
|
32
|
+
scale: 0.6
|
|
33
|
+
}}
|
|
34
|
+
className={clsx('react-flow__panel', css.container)}
|
|
35
|
+
style={{
|
|
36
|
+
transformOrigin: 'left center'
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<Paper
|
|
40
|
+
radius="sm"
|
|
41
|
+
withBorder={!isCollapsed}
|
|
42
|
+
className={css.paper}
|
|
43
|
+
p={isCollapsed ? 'sm' : 'md'}
|
|
44
|
+
onDoubleClick={e => e.stopPropagation()}>
|
|
45
|
+
<Stack gap={'sm'} justify="stretch" align="stretch">
|
|
46
|
+
<Group justify="stretch" wrap="nowrap" onClick={toggle} style={{ cursor: 'pointer' }}>
|
|
47
|
+
<Text
|
|
48
|
+
component={'div'}
|
|
49
|
+
flex={'1'}
|
|
50
|
+
size={isCollapsed ? 'sm' : 'lg'}
|
|
51
|
+
fw={500}
|
|
52
|
+
lh={1.1}
|
|
53
|
+
className={css.title}
|
|
54
|
+
>
|
|
55
|
+
{title}
|
|
56
|
+
</Text>
|
|
57
|
+
<Text
|
|
58
|
+
hidden={isCollapsed}
|
|
59
|
+
component={'div'}
|
|
60
|
+
flex={'0 0 auto'}
|
|
61
|
+
inline
|
|
62
|
+
size="xs"
|
|
63
|
+
fz={9}
|
|
64
|
+
fw={500}
|
|
65
|
+
c={'dimmed'}
|
|
66
|
+
style={{
|
|
67
|
+
userSelect: 'all'
|
|
68
|
+
}}>
|
|
69
|
+
<span style={{ userSelect: 'none' }}>id:{' '}</span>
|
|
70
|
+
{id}
|
|
71
|
+
</Text>
|
|
72
|
+
</Group>
|
|
73
|
+
{!isCollapsed && (
|
|
74
|
+
<>
|
|
75
|
+
{description && (
|
|
76
|
+
<Spoiler
|
|
77
|
+
maxHeight={42}
|
|
78
|
+
showLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">show more</Button>}
|
|
79
|
+
hideLabel={<Button color="gray" variant="light" fz={'10'} size="compact-xs">hide</Button>}>
|
|
80
|
+
<Text
|
|
81
|
+
component={'div'}
|
|
82
|
+
size="sm"
|
|
83
|
+
className={css.description}>
|
|
84
|
+
{description || 'no description'}
|
|
85
|
+
</Text>
|
|
86
|
+
</Spoiler>
|
|
87
|
+
)}
|
|
88
|
+
{!description && (
|
|
89
|
+
<Text
|
|
90
|
+
component={'div'}
|
|
91
|
+
size="xs"
|
|
92
|
+
c={'dimmed'}>
|
|
93
|
+
no description
|
|
94
|
+
</Text>
|
|
95
|
+
)}
|
|
96
|
+
{links && (
|
|
97
|
+
<Stack
|
|
98
|
+
gap={3}
|
|
99
|
+
justify="stretch"
|
|
100
|
+
align="stretch">
|
|
101
|
+
{links.map((link) => (
|
|
102
|
+
<Group key={link} wrap="nowrap" align='center' gap={'sm'}>
|
|
103
|
+
<Box flex={'1'} style={{ overflow: 'hidden' }}>
|
|
104
|
+
<Anchor
|
|
105
|
+
href={link}
|
|
106
|
+
target="_blank"
|
|
107
|
+
fz="xs"
|
|
108
|
+
truncate="end"
|
|
109
|
+
display={'inline-block'}
|
|
110
|
+
w={'100%'}>
|
|
111
|
+
{link}
|
|
112
|
+
</Anchor>
|
|
113
|
+
</Box>
|
|
114
|
+
<CopyButton value={link}>
|
|
115
|
+
{({ copied, copy }) => (
|
|
116
|
+
<Button
|
|
117
|
+
size="compact-xs"
|
|
118
|
+
fz={'10'}
|
|
119
|
+
variant="light"
|
|
120
|
+
onClick={copy}
|
|
121
|
+
color={copied
|
|
122
|
+
? 'teal'
|
|
123
|
+
: 'gray'}>
|
|
124
|
+
{copied ? 'copied' : 'copy'}
|
|
125
|
+
</Button>
|
|
126
|
+
)}
|
|
127
|
+
</CopyButton>
|
|
128
|
+
</Group>
|
|
129
|
+
))}
|
|
130
|
+
</Stack>
|
|
131
|
+
)}
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
</Stack>
|
|
135
|
+
</Paper>
|
|
136
|
+
</motion.div>
|
|
137
|
+
</AnimatePresence>
|
|
138
|
+
)
|
|
139
|
+
}
|
package/src/ui/OptionsPanel.tsx
CHANGED
|
@@ -3,11 +3,11 @@ import { useOnSelectionChange } from '@xyflow/react'
|
|
|
3
3
|
import clsx from 'clsx'
|
|
4
4
|
import { shallowEqual } from 'fast-equals'
|
|
5
5
|
import { AnimatePresence, motion } from 'framer-motion'
|
|
6
|
-
import {
|
|
7
|
-
import { NodeOptions } from './options/NodeOptions'
|
|
6
|
+
import { useState } from 'react'
|
|
8
7
|
import * as styles from './OptionsPanel.css'
|
|
8
|
+
import { NodeOptions } from './options/NodeOptions'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
export default function OptionsPanel() {
|
|
11
11
|
const [selectedNodes, setSelectedNodes] = useState([] as string[])
|
|
12
12
|
|
|
13
13
|
useOnSelectionChange({
|
|
@@ -16,26 +16,9 @@ const OptionsPanelMemo = /* @__PURE__ */ memo(function OptionsPanel() {
|
|
|
16
16
|
setSelectedNodes(prev => {
|
|
17
17
|
return shallowEqual(prev, next) ? prev : next
|
|
18
18
|
})
|
|
19
|
-
// if (nodes.length === 0 && edges.length === 0) {
|
|
20
|
-
// setSelectedNodes([])
|
|
21
|
-
// // setSelectedEdges([])
|
|
22
|
-
// return
|
|
23
|
-
// }
|
|
24
|
-
// const selected = new Set([
|
|
25
|
-
// ...nodes.map((n) => n.id),
|
|
26
|
-
// ...edges.flatMap((edge) => [
|
|
27
|
-
// edge.source,
|
|
28
|
-
// edge.target
|
|
29
|
-
// ])
|
|
30
|
-
// ])
|
|
31
|
-
// setSelectedNodes([...selected])
|
|
32
|
-
// setSelectedEdges(edges.map(e => e.id))
|
|
33
19
|
}
|
|
34
20
|
})
|
|
35
21
|
|
|
36
|
-
// useUpdateEffect(() => {
|
|
37
|
-
// setSelectedNodes([])
|
|
38
|
-
// }, [viewId])
|
|
39
22
|
|
|
40
23
|
return (
|
|
41
24
|
<AnimatePresence mode="wait">
|
|
@@ -60,6 +43,4 @@ const OptionsPanelMemo = /* @__PURE__ */ memo(function OptionsPanel() {
|
|
|
60
43
|
)}
|
|
61
44
|
</AnimatePresence>
|
|
62
45
|
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export default OptionsPanelMemo
|
|
46
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ElementShapes,
|
|
2
3
|
defaultTheme,
|
|
4
|
+
invariant,
|
|
5
|
+
type BorderStyle,
|
|
3
6
|
type ElementShape,
|
|
4
|
-
ElementShapes,
|
|
5
|
-
type Fqn,
|
|
6
7
|
type NonEmptyArray,
|
|
7
8
|
type ThemeColor
|
|
8
9
|
} from '@likec4/core'
|
|
@@ -12,14 +13,18 @@ import {
|
|
|
12
13
|
ColorSwatch,
|
|
13
14
|
Divider,
|
|
14
15
|
Flex,
|
|
15
|
-
|
|
16
|
+
SegmentedControl,
|
|
16
17
|
Select,
|
|
18
|
+
Slider,
|
|
17
19
|
Stack,
|
|
18
20
|
Text,
|
|
19
21
|
Tooltip,
|
|
20
|
-
TooltipGroup
|
|
22
|
+
TooltipGroup,
|
|
23
|
+
rem
|
|
21
24
|
} from '@mantine/core'
|
|
25
|
+
import { useEffect, useState } from 'react'
|
|
22
26
|
import { hasAtLeast, keys, takeWhile } from 'remeda'
|
|
27
|
+
import type { Changes } from '../../LikeC4Diagram.props'
|
|
23
28
|
import { useDiagramStoreApi } from '../../state'
|
|
24
29
|
import { useXYNodesData } from '../../xyflow/hooks'
|
|
25
30
|
import { XYFlowNode } from '../../xyflow/types'
|
|
@@ -52,15 +57,34 @@ export type ColorKey = typeof colors[0]['key']
|
|
|
52
57
|
type XYNodesData = Pick<XYFlowNode, 'id' | 'data' | 'type'>
|
|
53
58
|
|
|
54
59
|
export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] }) {
|
|
60
|
+
// export function NodeOptions(props: { nodes: XYFlowNode[] }) {
|
|
55
61
|
const diagramApi = useDiagramStoreApi()
|
|
62
|
+
// const nodes = useXYNodesData(props.nodes.map(node => node.id))
|
|
56
63
|
const nodes = useXYNodesData(selectedNodeIds)
|
|
57
64
|
if (!hasAtLeast(nodes, 1)) {
|
|
58
65
|
return null
|
|
59
66
|
}
|
|
67
|
+
if (nodes.length !== selectedNodeIds.length) {
|
|
68
|
+
throw new Error('NodeOptions: nodes and props.nodes should have the same length')
|
|
69
|
+
}
|
|
60
70
|
// Makes sense to show shape option only if there is at least one element node
|
|
61
71
|
const showShapeOption = nodes.some(node => node.type === 'element')
|
|
62
72
|
|
|
63
73
|
const [firstNode, ...rest] = nodes
|
|
74
|
+
|
|
75
|
+
// Makes sense to show opacity option only if there is at least one compound node
|
|
76
|
+
const showOpacityOption = rest.length === 0 && firstNode.type === 'compound'
|
|
77
|
+
|
|
78
|
+
const triggerChange = (style: Changes.ChangeElementStyle['style']) => {
|
|
79
|
+
const targets = nodes.map(node => node.data.element.id)
|
|
80
|
+
invariant(hasAtLeast(targets, 1), 'At least one target is required')
|
|
81
|
+
diagramApi.getState().triggerOnChange([{
|
|
82
|
+
op: 'change-element-style',
|
|
83
|
+
style,
|
|
84
|
+
targets
|
|
85
|
+
}])
|
|
86
|
+
}
|
|
87
|
+
|
|
64
88
|
return (
|
|
65
89
|
<Stack>
|
|
66
90
|
<Box maw={200}>
|
|
@@ -72,39 +96,33 @@ export function NodeOptions({ selectedNodeIds }: { selectedNodeIds: string[] })
|
|
|
72
96
|
<ShapeOption
|
|
73
97
|
nodes={nodes}
|
|
74
98
|
onShapeChange={(shape: ElementShape) => {
|
|
75
|
-
|
|
76
|
-
for (const nd of nodes) {
|
|
77
|
-
if (nd.data.element.shape !== shape) {
|
|
78
|
-
targets.push(nd.data.element.id)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (hasAtLeast(targets, 1)) {
|
|
82
|
-
diagramApi.getState().triggerOnChange([{
|
|
83
|
-
op: 'change-shape',
|
|
84
|
-
shape,
|
|
85
|
-
targets
|
|
86
|
-
}])
|
|
87
|
-
}
|
|
99
|
+
triggerChange({ shape })
|
|
88
100
|
}} />
|
|
89
101
|
)}
|
|
90
102
|
<Colors
|
|
91
103
|
nodes={nodes}
|
|
92
104
|
onColorChange={(color: ColorKey | ThemeColorKey) => {
|
|
93
|
-
|
|
94
|
-
for (const nd of nodes) {
|
|
95
|
-
if (nd.data.element.color !== color) {
|
|
96
|
-
targets.push(nd.data.element.id)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (hasAtLeast(targets, 1)) {
|
|
100
|
-
diagramApi.getState().triggerOnChange([{
|
|
101
|
-
op: 'change-color',
|
|
102
|
-
color,
|
|
103
|
-
targets
|
|
104
|
-
}])
|
|
105
|
-
}
|
|
105
|
+
triggerChange({ color })
|
|
106
106
|
}} />
|
|
107
107
|
<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>
|
|
108
126
|
</Stack>
|
|
109
127
|
)
|
|
110
128
|
}
|
|
@@ -251,3 +269,62 @@ function NavigateToOption({
|
|
|
251
269
|
</Box>
|
|
252
270
|
)
|
|
253
271
|
}
|
|
272
|
+
|
|
273
|
+
function OpacityOption({
|
|
274
|
+
node,
|
|
275
|
+
onOpacityChange
|
|
276
|
+
}: {
|
|
277
|
+
node: XYNodesData
|
|
278
|
+
onOpacityChange: (opacity: number) => void
|
|
279
|
+
}) {
|
|
280
|
+
let selectedOpacity = node.data.element.style.opacity ?? 100
|
|
281
|
+
const [value, setValue] = useState(selectedOpacity)
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
setValue(selectedOpacity)
|
|
284
|
+
}, [selectedOpacity])
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<Slider
|
|
288
|
+
mt={'xs'}
|
|
289
|
+
size={'sm'}
|
|
290
|
+
color={'dark'}
|
|
291
|
+
value={value}
|
|
292
|
+
onChange={setValue}
|
|
293
|
+
onChangeEnd={onOpacityChange} />
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function BorderStyleOption({
|
|
298
|
+
node,
|
|
299
|
+
onChange
|
|
300
|
+
}: {
|
|
301
|
+
node: XYNodesData
|
|
302
|
+
onChange: (borderStyle: BorderStyle) => void
|
|
303
|
+
}) {
|
|
304
|
+
let selecteBorderStyle = node.data.element.style.border ?? 'dashed'
|
|
305
|
+
const [value, setValue] = useState(selecteBorderStyle)
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
setValue(selecteBorderStyle)
|
|
308
|
+
}, [selecteBorderStyle])
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<Box mt={'md'}>
|
|
312
|
+
<SegmentedControl
|
|
313
|
+
size="xs"
|
|
314
|
+
fullWidth
|
|
315
|
+
withItemsBorders={false}
|
|
316
|
+
value={value}
|
|
317
|
+
onChange={v => {
|
|
318
|
+
setValue(v as BorderStyle)
|
|
319
|
+
onChange(v as BorderStyle)
|
|
320
|
+
}}
|
|
321
|
+
data={[
|
|
322
|
+
{ label: 'Solid', value: 'solid' },
|
|
323
|
+
{ label: 'Dashed', value: 'dashed' },
|
|
324
|
+
{ label: 'Dotted', value: 'dotted' },
|
|
325
|
+
{ label: 'None', value: 'none' }
|
|
326
|
+
]}
|
|
327
|
+
/>
|
|
328
|
+
</Box>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
@@ -66,7 +66,7 @@ function FitViewOnDiagramChanges() {
|
|
|
66
66
|
s => s.viewportChanged,
|
|
67
67
|
setViewportMoved
|
|
68
68
|
),
|
|
69
|
-
[diagramApi]
|
|
69
|
+
[diagramApi, setViewportMoved]
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
useRafEffect(
|
|
@@ -77,7 +77,7 @@ function FitViewOnDiagramChanges() {
|
|
|
77
77
|
diagramApi.getState().fitDiagram()
|
|
78
78
|
setProcessed(pendingChangeId)
|
|
79
79
|
},
|
|
80
|
-
[isReady, processedChangeId, pendingChangeId]
|
|
80
|
+
[isReady, processedChangeId, pendingChangeId, diagramApi]
|
|
81
81
|
)
|
|
82
82
|
|
|
83
83
|
if (!viewportMoved && isReady) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { EdgeChange, NodeChange } from '@xyflow/react'
|
|
2
|
+
import { useUpdateEffect } from '../hooks'
|
|
3
|
+
import { useDiagramState } from '../state/useDiagramStore'
|
|
4
|
+
import { useXYStoreApi } from './hooks/useXYFlow'
|
|
5
|
+
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
6
|
+
|
|
7
|
+
export function SelectEdgesOnNodeFocus() {
|
|
8
|
+
const focusedNodeId = useDiagramState(s => s.focusedNodeId)
|
|
9
|
+
const xyflowApi = useXYStoreApi()
|
|
10
|
+
|
|
11
|
+
useUpdateEffect(() => {
|
|
12
|
+
const edgeChanges = [] as EdgeChange<XYFlowEdge>[]
|
|
13
|
+
const nodeChanges = [] as NodeChange<XYFlowNode>[]
|
|
14
|
+
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = xyflowApi.getState()
|
|
15
|
+
if (focusedNodeId) {
|
|
16
|
+
for (const edge of edges) {
|
|
17
|
+
if (edge.source === focusedNodeId || edge.target === focusedNodeId) {
|
|
18
|
+
edgeChanges.push({
|
|
19
|
+
id: edge.id,
|
|
20
|
+
type: 'select',
|
|
21
|
+
selected: true
|
|
22
|
+
})
|
|
23
|
+
} else if (edge.selected) {
|
|
24
|
+
edgeChanges.push({
|
|
25
|
+
id: edge.id,
|
|
26
|
+
type: 'select',
|
|
27
|
+
selected: false
|
|
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
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
nodeChanges.push({
|
|
41
|
+
id: focusedNodeId,
|
|
42
|
+
type: 'select',
|
|
43
|
+
selected: true
|
|
44
|
+
})
|
|
45
|
+
if (edgeChanges.length > 0) {
|
|
46
|
+
triggerEdgeChanges(edgeChanges)
|
|
47
|
+
}
|
|
48
|
+
if (nodeChanges.length > 0) {
|
|
49
|
+
triggerNodeChanges(nodeChanges)
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
xyflowApi.getState().resetSelectedElements()
|
|
53
|
+
}
|
|
54
|
+
}, [focusedNodeId])
|
|
55
|
+
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
@@ -27,14 +27,20 @@ export const SyncWithDiagram = memo(() => {
|
|
|
27
27
|
state => ({
|
|
28
28
|
viewId: state.view.id,
|
|
29
29
|
nodes: state.view.nodes,
|
|
30
|
-
edges: state.view.edges
|
|
31
|
-
draggable: state.nodesDraggable,
|
|
32
|
-
selectable: state.nodesSelectable
|
|
30
|
+
edges: state.view.edges
|
|
33
31
|
}),
|
|
34
32
|
// listener
|
|
35
|
-
({ viewId, nodes, edges
|
|
36
|
-
const {
|
|
37
|
-
|
|
33
|
+
({ viewId, nodes, edges }) => {
|
|
34
|
+
const {
|
|
35
|
+
lastOnNavigate,
|
|
36
|
+
nodesDraggable,
|
|
37
|
+
nodesSelectable,
|
|
38
|
+
focusedNodeId
|
|
39
|
+
} = diagramStoreApi.getState()
|
|
40
|
+
const updates = diagramViewToXYFlowData({ nodes, edges }, {
|
|
41
|
+
draggable: nodesDraggable,
|
|
42
|
+
selectable: nodesSelectable || focusedNodeId !== null
|
|
43
|
+
})
|
|
38
44
|
|
|
39
45
|
xyflowRef.current.setNodes(prev =>
|
|
40
46
|
updates.nodes.map(update => {
|
package/src/xyflow/XYFlow.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMantineColorScheme } from '@mantine/core'
|
|
2
|
-
import { Controls, ReactFlow } from '@xyflow/react'
|
|
2
|
+
import { Controls, PanOnScrollMode, ReactFlow } from '@xyflow/react'
|
|
3
|
+
import { DEV } from 'esm-env'
|
|
3
4
|
import { type CSSProperties, memo, type PropsWithChildren } from 'react'
|
|
4
5
|
import type { SetNonNullable, Simplify } from 'type-fest'
|
|
5
6
|
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
@@ -9,6 +10,7 @@ import { RelationshipEdge } from './edges/RelationshipEdge'
|
|
|
9
10
|
import { useLayoutConstraints } from './hooks/useLayoutConstraints'
|
|
10
11
|
import { CompoundNode } from './nodes/compound'
|
|
11
12
|
import { ElementNode } from './nodes/element'
|
|
13
|
+
import { SelectEdgesOnNodeFocus } from './SelectEdgesOnNodeFocus'
|
|
12
14
|
import { XYFlowEdge, XYFlowNode } from './types'
|
|
13
15
|
import { XYFlowBackground } from './XYFlowBackground'
|
|
14
16
|
import { useXYFlowEvents } from './XYFlowEvents'
|
|
@@ -41,7 +43,7 @@ type XYFlowWrapperProps = Simplify<
|
|
|
41
43
|
>
|
|
42
44
|
|
|
43
45
|
const selector = (s: DiagramState) => ({
|
|
44
|
-
nodesSelectable: s.nodesSelectable,
|
|
46
|
+
nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
|
|
45
47
|
nodesDraggable: s.nodesDraggable,
|
|
46
48
|
fitView: s.fitViewEnabled,
|
|
47
49
|
fitViewPadding: s.fitViewPadding,
|
|
@@ -137,6 +139,7 @@ function XYFlowWrapper({
|
|
|
137
139
|
onNodeClick={handlers.onNodeClick}
|
|
138
140
|
onNodeDoubleClick={handlers.onNodeDoubleClick}
|
|
139
141
|
onEdgeClick={handlers.onEdgeClick}
|
|
142
|
+
onViewportChange={handlers.onViewportChange}
|
|
140
143
|
onInit={() => {
|
|
141
144
|
diagramApi.setState({ initialized: true }, false, 'initialized')
|
|
142
145
|
}}
|
|
@@ -150,7 +153,8 @@ function XYFlowWrapper({
|
|
|
150
153
|
onPaneContextMenu: handlers.onPaneContextMenu
|
|
151
154
|
})}>
|
|
152
155
|
{isBgWithPattern && <XYFlowBackground background={background} />}
|
|
153
|
-
{controls && <Controls />}
|
|
156
|
+
{controls && <Controls position={'bottom-right'} />}
|
|
157
|
+
{zoomable && !editor.hasOnNodeClick && <SelectEdgesOnNodeFocus />}
|
|
154
158
|
{children}
|
|
155
159
|
</ReactFlow>
|
|
156
160
|
)
|