@likec4/diagram 1.0.2 → 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/KeepAspectRatioContainer.d.ts +9 -0
- package/dist/LikeC4Diagram.css.d.ts +1 -0
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +16 -11
- package/dist/index.mjs +639 -762
- package/dist/state/DiagramContext.d.ts +54 -18
- package/dist/state/diagramStore.d.ts +61 -21
- package/dist/state/useDiagramStore.d.ts +54 -18
- package/dist/style.css +690 -79
- 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/ui/index.d.ts +0 -1
- package/dist/xyflow/SelectEdgesOnNodeFocus.d.ts +1 -0
- package/dist/xyflow/{XYFlowWrapper.d.ts → XYFlow.d.ts} +2 -2
- package/dist/xyflow/XYFlowEvents.d.ts +133 -13
- package/dist/xyflow/edges/RelationshipEdge.d.ts +2 -2
- package/dist/xyflow/edges/edges.css.d.ts +1 -0
- package/dist/xyflow/hooks/useXYFlow.d.ts +20 -20
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +7 -5
- package/dist/xyflow/types.d.ts +2 -2
- package/package.json +5 -3
- package/src/{ui/KeepAspectRatio.tsx → KeepAspectRatioContainer.tsx} +7 -6
- package/src/LikeC4Diagram.css.ts +8 -1
- package/src/LikeC4Diagram.props.ts +17 -12
- package/src/LikeC4Diagram.tsx +9 -6
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/globals.css.ts +48 -38
- package/src/state/diagramStore.ts +125 -68
- 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/index.ts +0 -1
- package/src/ui/options/NodeOptions.tsx +107 -30
- package/src/xyflow/FitviewOnDiagramChange.tsx +26 -20
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -0
- package/src/xyflow/SyncWithDiagram.tsx +35 -26
- package/src/xyflow/{XYFlowWrapper.tsx → XYFlow.tsx} +24 -27
- package/src/xyflow/XYFlowEvents.tsx +175 -81
- package/src/xyflow/diagram-to-xyflow.ts +4 -4
- 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 +94 -44
- package/src/xyflow/nodes/compound/CompoundNode.tsx +29 -31
- package/src/xyflow/nodes/element/ElementNode.tsx +2 -2
- package/src/xyflow/nodes/element/element.css.ts +1 -0
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +2 -1
- package/dist/ui/KeepAspectRatio.css.d.ts +0 -1
- package/dist/ui/KeepAspectRatio.d.ts +0 -9
- package/src/ui/KeepAspectRatio.css.ts +0 -7
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useDebouncedCallback, useDebouncedEffect, useRafEffect } from '@react-hookz/web'
|
|
2
2
|
import { useNodesInitialized } from '@xyflow/react'
|
|
3
|
-
import { shallowEqual } from 'fast-equals'
|
|
4
3
|
import { memo, useEffect, useRef, useState } from 'react'
|
|
5
4
|
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state'
|
|
6
5
|
import { useXYStore } from './hooks'
|
|
@@ -38,29 +37,36 @@ function FitViewOnDiagramChanges() {
|
|
|
38
37
|
const diagramApi = useDiagramStoreApi()
|
|
39
38
|
const [viewportMoved, setViewportMoved] = useState(diagramApi.getState().viewportChanged)
|
|
40
39
|
const [processedChangeId, setProcessed] = useState(diagramChangeId(diagramApi.getState()))
|
|
41
|
-
const [pendingChangeId, setPending] =
|
|
40
|
+
const [pendingChangeId, setPending] = useState(processedChangeId)
|
|
42
41
|
|
|
43
42
|
const isReady = useNodesInitialized({
|
|
44
43
|
includeHiddenNodes: true
|
|
45
44
|
})
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
() => {
|
|
49
|
-
|
|
50
|
-
s => ({
|
|
51
|
-
viewportMoved: s.viewportChanged,
|
|
52
|
-
changeid: diagramChangeId(s)
|
|
53
|
-
}),
|
|
54
|
-
({ viewportMoved, changeid }) => {
|
|
55
|
-
setPending(changeid)
|
|
56
|
-
setViewportMoved(viewportMoved)
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
equalityFn: shallowEqual
|
|
60
|
-
}
|
|
61
|
-
)
|
|
46
|
+
const schedulePending = useDebouncedCallback(
|
|
47
|
+
(pendingChangeId: string) => {
|
|
48
|
+
setPending(pendingChangeId)
|
|
62
49
|
},
|
|
63
|
-
[
|
|
50
|
+
[setPending],
|
|
51
|
+
75
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
useEffect(
|
|
55
|
+
() =>
|
|
56
|
+
diagramApi.subscribe(
|
|
57
|
+
s => diagramChangeId(s),
|
|
58
|
+
schedulePending
|
|
59
|
+
),
|
|
60
|
+
[diagramApi, schedulePending]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
useEffect(
|
|
64
|
+
() =>
|
|
65
|
+
diagramApi.subscribe(
|
|
66
|
+
s => s.viewportChanged,
|
|
67
|
+
setViewportMoved
|
|
68
|
+
),
|
|
69
|
+
[diagramApi, setViewportMoved]
|
|
64
70
|
)
|
|
65
71
|
|
|
66
72
|
useRafEffect(
|
|
@@ -71,7 +77,7 @@ function FitViewOnDiagramChanges() {
|
|
|
71
77
|
diagramApi.getState().fitDiagram()
|
|
72
78
|
setProcessed(pendingChangeId)
|
|
73
79
|
},
|
|
74
|
-
[isReady, processedChangeId, pendingChangeId]
|
|
80
|
+
[isReady, processedChangeId, pendingChangeId, diagramApi]
|
|
75
81
|
)
|
|
76
82
|
|
|
77
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 => {
|
|
@@ -65,33 +71,36 @@ export const SyncWithDiagram = memo(() => {
|
|
|
65
71
|
})
|
|
66
72
|
)
|
|
67
73
|
|
|
68
|
-
if (lastOnNavigate?.toView === viewId &&
|
|
74
|
+
if (lastOnNavigate?.toView === viewId && lastOnNavigate.positionCorrected !== true) {
|
|
69
75
|
const elFrom = lastOnNavigate.element
|
|
70
76
|
const elTo = nodes.find(n => n.id === elFrom.id)
|
|
71
77
|
if (elTo) {
|
|
72
78
|
const centerFrom = lastOnNavigate.elementCenterScreenPosition
|
|
73
|
-
|
|
79
|
+
let nextZoom = Math.min(elFrom.width / elTo.width, elFrom.height / elTo.height)
|
|
74
80
|
|
|
81
|
+
// TODO: investigate why 2 animation frames are needed
|
|
75
82
|
requestAnimationFrame(() => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const centerTo = xyflowRef.current.flowToScreenPosition({
|
|
86
|
-
x: elTo.position[0] + elTo.width / 2,
|
|
87
|
-
y: elTo.position[1] + elTo.height / 2
|
|
88
|
-
}),
|
|
89
|
-
diff = {
|
|
90
|
-
x: toDomPrecision(centerFrom.x - centerTo.x),
|
|
91
|
-
y: toDomPrecision(centerFrom.y - centerTo.y)
|
|
83
|
+
requestAnimationFrame(() => {
|
|
84
|
+
const v = xyflowRef.current.getViewport()
|
|
85
|
+
if (nextZoom < v.zoom) {
|
|
86
|
+
nextZoom = Math.max(nextZoom, v.zoom * 0.5)
|
|
87
|
+
xyflowRef.current.setViewport({
|
|
88
|
+
x: v.x,
|
|
89
|
+
y: v.y,
|
|
90
|
+
zoom: nextZoom
|
|
91
|
+
})
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
|
|
94
|
+
const centerTo = xyflowRef.current.flowToScreenPosition({
|
|
95
|
+
x: elTo.position[0] + elTo.width / 2,
|
|
96
|
+
y: elTo.position[1] + elTo.height / 2
|
|
97
|
+
}),
|
|
98
|
+
diff = {
|
|
99
|
+
x: toDomPrecision(centerFrom.x - centerTo.x),
|
|
100
|
+
y: toDomPrecision(centerFrom.y - centerTo.y)
|
|
101
|
+
}
|
|
102
|
+
xyflowApi.getState().panBy(diff)
|
|
103
|
+
})
|
|
95
104
|
})
|
|
96
105
|
}
|
|
97
106
|
diagramStoreApi.setState(
|
|
@@ -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'
|
|
@@ -26,8 +28,6 @@ type OnlyExpectedProps = Required<
|
|
|
26
28
|
LikeC4DiagramProperties,
|
|
27
29
|
| 'className'
|
|
28
30
|
| 'controls'
|
|
29
|
-
| 'pannable'
|
|
30
|
-
| 'zoomable'
|
|
31
31
|
| 'background'
|
|
32
32
|
>
|
|
33
33
|
>
|
|
@@ -43,7 +43,7 @@ type XYFlowWrapperProps = Simplify<
|
|
|
43
43
|
>
|
|
44
44
|
|
|
45
45
|
const selector = (s: DiagramState) => ({
|
|
46
|
-
nodesSelectable: s.nodesSelectable,
|
|
46
|
+
nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
|
|
47
47
|
nodesDraggable: s.nodesDraggable,
|
|
48
48
|
fitView: s.fitViewEnabled,
|
|
49
49
|
fitViewPadding: s.fitViewPadding,
|
|
@@ -53,7 +53,8 @@ const selector = (s: DiagramState) => ({
|
|
|
53
53
|
hasOnCanvasContextMenu: !!s.onCanvasContextMenu,
|
|
54
54
|
hasOnEdgeContextMenu: !!s.onEdgeContextMenu,
|
|
55
55
|
hasOnEdgeClick: !!s.onEdgeClick,
|
|
56
|
-
|
|
56
|
+
zoomable: s.zoomable,
|
|
57
|
+
pannable: s.pannable
|
|
57
58
|
})
|
|
58
59
|
|
|
59
60
|
function XYFlowWrapper({
|
|
@@ -61,8 +62,6 @@ function XYFlowWrapper({
|
|
|
61
62
|
children,
|
|
62
63
|
defaultNodes,
|
|
63
64
|
defaultEdges,
|
|
64
|
-
pannable,
|
|
65
|
-
zoomable,
|
|
66
65
|
controls,
|
|
67
66
|
background,
|
|
68
67
|
style
|
|
@@ -73,6 +72,8 @@ function XYFlowWrapper({
|
|
|
73
72
|
nodesDraggable,
|
|
74
73
|
fitView,
|
|
75
74
|
fitViewPadding,
|
|
75
|
+
pannable,
|
|
76
|
+
zoomable,
|
|
76
77
|
...editor
|
|
77
78
|
} = useDiagramState(selector)
|
|
78
79
|
|
|
@@ -120,33 +121,28 @@ function XYFlowWrapper({
|
|
|
120
121
|
selectionKeyCode: null
|
|
121
122
|
})}
|
|
122
123
|
nodesDraggable={nodesDraggable}
|
|
123
|
-
{...
|
|
124
|
+
{...nodesDraggable && {
|
|
125
|
+
onNodeDragStart: layoutConstraints.onNodeDragStart,
|
|
126
|
+
onNodeDrag: layoutConstraints.onNodeDrag,
|
|
127
|
+
onNodeDragStop: layoutConstraints.onNodeDragStop
|
|
128
|
+
}}
|
|
124
129
|
zoomOnDoubleClick={false}
|
|
125
130
|
elevateNodesOnSelect={false} // or edges are not visible after select
|
|
126
131
|
selectNodesOnDrag={false} // or weird camera movement
|
|
132
|
+
onDoubleClick={handlers.onDoubleClick}
|
|
127
133
|
onPaneClick={handlers.onPaneClick}
|
|
128
|
-
{...(editor.hasOnNodeClick && {
|
|
129
|
-
onNodeClick: handlers.onNodeClick
|
|
130
|
-
})}
|
|
131
|
-
{...(editor.hasOnEdgeClick && {
|
|
132
|
-
onEdgeClick: handlers.onEdgeClick
|
|
133
|
-
})}
|
|
134
134
|
onMoveEnd={handlers.onMoveEnd}
|
|
135
|
+
onNodeMouseEnter={handlers.onNodeMouseEnter}
|
|
136
|
+
onNodeMouseLeave={handlers.onNodeMouseLeave}
|
|
137
|
+
onEdgeMouseEnter={handlers.onEdgeMouseEnter}
|
|
138
|
+
onEdgeMouseLeave={handlers.onEdgeMouseLeave}
|
|
139
|
+
onNodeClick={handlers.onNodeClick}
|
|
140
|
+
onNodeDoubleClick={handlers.onNodeDoubleClick}
|
|
141
|
+
onEdgeClick={handlers.onEdgeClick}
|
|
142
|
+
onViewportChange={handlers.onViewportChange}
|
|
135
143
|
onInit={() => {
|
|
136
144
|
diagramApi.setState({ initialized: true }, false, 'initialized')
|
|
137
145
|
}}
|
|
138
|
-
onNodeMouseEnter={(_event, node) => {
|
|
139
|
-
diagramApi.getState().setHoveredNode(node.id)
|
|
140
|
-
}}
|
|
141
|
-
onNodeMouseLeave={() => {
|
|
142
|
-
diagramApi.getState().setHoveredNode(null)
|
|
143
|
-
}}
|
|
144
|
-
onEdgeMouseEnter={(_event, edge) => {
|
|
145
|
-
diagramApi.getState().setHoveredEdge(edge.id)
|
|
146
|
-
}}
|
|
147
|
-
onEdgeMouseLeave={() => {
|
|
148
|
-
diagramApi.getState().setHoveredEdge(null)
|
|
149
|
-
}}
|
|
150
146
|
{...(editor.hasOnNodeContextMenu && {
|
|
151
147
|
onNodeContextMenu: handlers.onNodeContextMenu
|
|
152
148
|
})}
|
|
@@ -157,7 +153,8 @@ function XYFlowWrapper({
|
|
|
157
153
|
onPaneContextMenu: handlers.onPaneContextMenu
|
|
158
154
|
})}>
|
|
159
155
|
{isBgWithPattern && <XYFlowBackground background={background} />}
|
|
160
|
-
{controls && <Controls />}
|
|
156
|
+
{controls && <Controls position={'bottom-right'} />}
|
|
157
|
+
{zoomable && !editor.hasOnNodeClick && <SelectEdgesOnNodeFocus />}
|
|
161
158
|
{children}
|
|
162
159
|
</ReactFlow>
|
|
163
160
|
)
|