@likec4/diagram 1.6.0 → 1.7.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/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +10 -11
- package/dist/hooks/useUpdateEffect.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +769 -8587
- package/dist/state/DiagramContext.d.ts +746 -181
- package/dist/state/diagram-to-xyflow.d.ts +10 -0
- package/dist/state/diagramStore.d.ts +782 -202
- package/dist/style.css +273 -275
- package/dist/types.d.ts +20 -0
- package/dist/xyflow/EdgeMarkers.d.ts +1 -0
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -1
- package/dist/xyflow/XYFlow.d.ts +1 -4
- package/dist/xyflow/XYFlowEvents.d.ts +41 -12
- package/dist/xyflow/XYFlowInner.d.ts +0 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -3
- package/dist/xyflow/hooks/index.d.ts +0 -1
- package/dist/xyflow/hooks/useXYFlow.d.ts +27 -27
- package/dist/xyflow/types.d.ts +11 -19
- package/dist/xyflow/utils.d.ts +1 -0
- package/package.json +11 -11
- package/src/LikeC4Diagram.props.ts +17 -13
- package/src/LikeC4Diagram.tsx +67 -106
- package/src/globals.css.ts +10 -6
- package/src/hooks/useUpdateEffect.ts +16 -1
- package/src/index.ts +3 -0
- package/src/state/DiagramContext.tsx +19 -27
- package/src/{xyflow → state}/diagram-to-xyflow.ts +19 -18
- package/src/state/diagramStore.ts +404 -192
- package/src/types.ts +22 -0
- package/src/ui/BackwardForwardPanel.tsx +24 -7
- package/src/xyflow/EdgeMarkers.tsx +2 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +42 -75
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +1 -1
- package/src/xyflow/XYFlow.tsx +57 -35
- package/src/xyflow/XYFlowEvents.tsx +58 -10
- package/src/xyflow/XYFlowInner.tsx +9 -8
- package/src/xyflow/edges/RelationshipEdge.tsx +43 -29
- package/src/xyflow/edges/edges.css.ts +3 -3
- package/src/xyflow/edges/utils.ts +0 -1
- package/src/xyflow/hooks/index.ts +0 -1
- package/src/xyflow/hooks/useLayoutConstraints.ts +42 -65
- package/src/xyflow/nodes/element/ElementNode.tsx +8 -11
- package/src/xyflow/nodes/element/element.css.ts +8 -6
- package/src/xyflow/types.ts +14 -32
- package/src/xyflow/utils.ts +4 -0
- package/dist/xyflow/SyncWithDiagram.d.ts +0 -4
- package/dist/xyflow/diagram-to-xyflow.d.ts +0 -7
- package/src/xyflow/SyncWithDiagram.tsx +0 -85
package/src/types.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Fqn, RelationID, ViewID } from '@likec4/core'
|
|
2
|
+
import type { Simplify } from 'type-fest'
|
|
3
|
+
|
|
4
|
+
type TaggedUnion<
|
|
5
|
+
TagKey extends string,
|
|
6
|
+
UnionMembers extends Record<string, Record<string, unknown>>
|
|
7
|
+
> = {
|
|
8
|
+
[Name in keyof UnionMembers]: Simplify<{ [Key in TagKey]: Name } & UnionMembers[Name]>
|
|
9
|
+
}[keyof UnionMembers]
|
|
10
|
+
|
|
11
|
+
export type DiagramEditorCommand = TaggedUnion<'type', {
|
|
12
|
+
// Sources
|
|
13
|
+
showElement: {
|
|
14
|
+
element: Fqn
|
|
15
|
+
}
|
|
16
|
+
showRelation: {
|
|
17
|
+
relation: RelationID
|
|
18
|
+
}
|
|
19
|
+
showView: {
|
|
20
|
+
view: ViewID
|
|
21
|
+
}
|
|
22
|
+
}>
|
|
@@ -2,12 +2,23 @@ import { ActionIcon, Group } from '@mantine/core'
|
|
|
2
2
|
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'
|
|
3
3
|
import clsx from 'clsx'
|
|
4
4
|
import { AnimatePresence, m } from 'framer-motion'
|
|
5
|
-
import {
|
|
5
|
+
import { type DiagramState, useDiagramState } from '../state/hooks'
|
|
6
6
|
import * as css from './BackwardForwardPanel.css'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const historySelector = (s: DiagramState) => ({
|
|
9
|
+
hasStepBack: s.navigationHistoryIndex > 0,
|
|
10
|
+
hasStepForward: s.navigationHistoryIndex < s.navigationHistory.length - 1,
|
|
11
|
+
goBack: s.goBack,
|
|
12
|
+
goForward: s.goForward
|
|
13
|
+
})
|
|
10
14
|
|
|
15
|
+
export function BackwardForwardPanel() {
|
|
16
|
+
const {
|
|
17
|
+
hasStepBack,
|
|
18
|
+
hasStepForward,
|
|
19
|
+
goBack,
|
|
20
|
+
goForward
|
|
21
|
+
} = useDiagramState(historySelector)
|
|
11
22
|
return (
|
|
12
23
|
<Group
|
|
13
24
|
className={clsx(
|
|
@@ -17,7 +28,7 @@ export function BackwardForwardPanel() {
|
|
|
17
28
|
)}
|
|
18
29
|
gap={'xs'}>
|
|
19
30
|
<AnimatePresence>
|
|
20
|
-
{
|
|
31
|
+
{hasStepBack && (
|
|
21
32
|
<m.div
|
|
22
33
|
layout
|
|
23
34
|
initial={{ opacity: 0.05, transform: 'translateX(-10px)' }}
|
|
@@ -30,12 +41,15 @@ export function BackwardForwardPanel() {
|
|
|
30
41
|
<ActionIcon
|
|
31
42
|
variant="light"
|
|
32
43
|
color="gray"
|
|
33
|
-
onClick={
|
|
44
|
+
onClick={e => {
|
|
45
|
+
e.stopPropagation()
|
|
46
|
+
goBack()
|
|
47
|
+
}}>
|
|
34
48
|
<IconChevronLeft />
|
|
35
49
|
</ActionIcon>
|
|
36
50
|
</m.div>
|
|
37
51
|
)}
|
|
38
|
-
{
|
|
52
|
+
{hasStepForward && (
|
|
39
53
|
<m.div
|
|
40
54
|
layout
|
|
41
55
|
initial={{ opacity: 0.05, transform: 'translateX(10px)' }}
|
|
@@ -48,7 +62,10 @@ export function BackwardForwardPanel() {
|
|
|
48
62
|
<ActionIcon
|
|
49
63
|
variant="light"
|
|
50
64
|
color="gray"
|
|
51
|
-
onClick={
|
|
65
|
+
onClick={e => {
|
|
66
|
+
e.stopPropagation()
|
|
67
|
+
goForward()
|
|
68
|
+
}}>
|
|
52
69
|
<IconChevronRight />
|
|
53
70
|
</ActionIcon>
|
|
54
71
|
</m.div>
|
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
import { invariant } from '@likec4/core'
|
|
2
1
|
import { useDebouncedEffect } from '@react-hookz/web'
|
|
3
2
|
import { shallowEqual } from 'fast-equals'
|
|
4
|
-
import {
|
|
3
|
+
import { useRef, useState } from 'react'
|
|
5
4
|
import { type DiagramState, type DiagramStoreApi, useDiagramState, useDiagramStoreApi } from '../state/hooks'
|
|
6
|
-
import { useXYStore
|
|
7
|
-
import type { XYFlowState } from './types'
|
|
8
|
-
import { toDomPrecision } from './utils'
|
|
5
|
+
import { useXYStore } from './hooks/useXYFlow'
|
|
9
6
|
|
|
10
|
-
function
|
|
7
|
+
function selectXYFlowSize(state: { width: number; height: number }): string {
|
|
11
8
|
return `${Math.round(state.width)}:${Math.round(state.height)}`
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
function
|
|
15
|
-
|
|
11
|
+
function selectDiagramsize({ view, fitViewPadding }: DiagramState) {
|
|
12
|
+
return [
|
|
13
|
+
view.bounds.x,
|
|
14
|
+
view.bounds.y,
|
|
15
|
+
view.bounds.width,
|
|
16
|
+
view.bounds.height,
|
|
17
|
+
fitViewPadding
|
|
18
|
+
].map(Math.round).join(':')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function FitViewOnViewportResize({ diagramApi }: {
|
|
16
22
|
diagramApi: DiagramStoreApi
|
|
17
23
|
}) {
|
|
18
|
-
const
|
|
24
|
+
const xyflowsize = useXYStore(selectXYFlowSize)
|
|
25
|
+
const viewsize = useDiagramState(selectDiagramsize)
|
|
26
|
+
const dimensions = xyflowsize + viewsize
|
|
19
27
|
const prevDimensionsRef = useRef(dimensions)
|
|
20
28
|
|
|
21
29
|
useDebouncedEffect(
|
|
@@ -24,92 +32,51 @@ function FitViewOnViewportResize({ diagramApi, xyflowApi }: {
|
|
|
24
32
|
if (focusedNodeId || prevDimensionsRef.current === dimensions) {
|
|
25
33
|
return
|
|
26
34
|
}
|
|
27
|
-
fitDiagram(300)
|
|
28
35
|
prevDimensionsRef.current = dimensions
|
|
36
|
+
fitDiagram(250)
|
|
29
37
|
},
|
|
30
|
-
[dimensions, diagramApi
|
|
31
|
-
|
|
38
|
+
[dimensions, diagramApi],
|
|
39
|
+
250,
|
|
32
40
|
1000
|
|
33
41
|
)
|
|
34
42
|
|
|
35
43
|
return null
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
function selector({ view,
|
|
39
|
-
const viewId = view.id + '_' + view.autoLayout + '_' + s.fitViewPadding
|
|
40
|
-
|
|
41
|
-
const waitCorrection = lastOnNavigate && lastOnNavigate.toView === view.id
|
|
42
|
-
&& lastOnNavigate.positionCorrected === false
|
|
43
|
-
const elTo = waitCorrection ? view.nodes.find(n => n.id === lastOnNavigate.element.id) : undefined
|
|
44
|
-
if (waitCorrection && elTo) {
|
|
45
|
-
return {
|
|
46
|
-
viewId,
|
|
47
|
-
xyflowSynced,
|
|
48
|
-
viewportChanged,
|
|
49
|
-
waitCorrection: true,
|
|
50
|
-
elFrom: lastOnNavigate.element,
|
|
51
|
-
elTo
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
46
|
+
function selector({ view, viewportChanged, fitViewPadding }: DiagramState) {
|
|
55
47
|
return {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
viewportChanged,
|
|
59
|
-
waitCorrection: false,
|
|
60
|
-
fromElement: null,
|
|
61
|
-
toElement: null
|
|
48
|
+
layoutId: view.id + '_' + view.autoLayout + '_' + fitViewPadding,
|
|
49
|
+
viewportNotMoved: !viewportChanged
|
|
62
50
|
}
|
|
63
51
|
}
|
|
64
52
|
|
|
65
53
|
/**
|
|
66
|
-
* Fits the view when
|
|
54
|
+
* Fits the view when diagram changes
|
|
67
55
|
*/
|
|
68
56
|
export function FitViewOnDiagramChange() {
|
|
69
|
-
const xyflowApi = useXYStoreApi()
|
|
70
57
|
const {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
viewportChanged,
|
|
74
|
-
waitCorrection,
|
|
75
|
-
elFrom,
|
|
76
|
-
elTo
|
|
58
|
+
layoutId,
|
|
59
|
+
viewportNotMoved
|
|
77
60
|
} = useDiagramState(selector, shallowEqual)
|
|
78
61
|
const diagramApi = useDiagramStoreApi()
|
|
79
|
-
const
|
|
80
|
-
|
|
62
|
+
const [currentLayoutId, setCurrent] = useState(layoutId)
|
|
63
|
+
|
|
64
|
+
const requiresFit = layoutId !== currentLayoutId
|
|
81
65
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
x: elTo.position[0], // + elFrom.width / 2,
|
|
94
|
-
y: elTo.position[1] // + elFrom.height / 2
|
|
95
|
-
}),
|
|
96
|
-
diff = {
|
|
97
|
-
x: toDomPrecision(fromPos.x - toPos.x),
|
|
98
|
-
y: toDomPrecision(fromPos.y - toPos.y)
|
|
99
|
-
}
|
|
100
|
-
xyflowApi.getState().panBy(diff)
|
|
101
|
-
durationRef.current = 500
|
|
102
|
-
diagramApi.setState({ lastOnNavigate: null })
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
processedRef.current = pendingViewId
|
|
106
|
-
diagramApi.getState().fitDiagram(durationRef.current)
|
|
107
|
-
// Reset duration
|
|
108
|
-
durationRef.current = 300
|
|
109
|
-
}, [pendingViewId, xyflowSynced, waitCorrection])
|
|
66
|
+
useDebouncedEffect(
|
|
67
|
+
() => {
|
|
68
|
+
if (layoutId === currentLayoutId) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
setCurrent(layoutId)
|
|
72
|
+
diagramApi.getState().fitDiagram(450)
|
|
73
|
+
},
|
|
74
|
+
[requiresFit, diagramApi],
|
|
75
|
+
50
|
|
76
|
+
)
|
|
110
77
|
|
|
111
|
-
if (
|
|
112
|
-
return <FitViewOnViewportResize
|
|
78
|
+
if (viewportNotMoved && !requiresFit) {
|
|
79
|
+
return <FitViewOnViewportResize diagramApi={diagramApi} />
|
|
113
80
|
}
|
|
114
81
|
return null
|
|
115
82
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { nonNullable } from '@likec4/core'
|
|
2
2
|
import type { EdgeChange, NodeChange } from '@xyflow/react'
|
|
3
|
-
import { getBoundsOfRects, getViewportForBounds, nodeToRect } from '@xyflow/system'
|
|
3
|
+
import { getBoundsOfRects, getNodesBounds, getViewportForBounds, nodeToRect } from '@xyflow/system'
|
|
4
4
|
import { useEffect } from 'react'
|
|
5
5
|
import { useDiagramStoreApi } from '../state/useDiagramStore'
|
|
6
6
|
import { MinZoom } from './const'
|
package/src/xyflow/XYFlow.tsx
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { ReactFlow, useOnViewportChange } from '@xyflow/react'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { type CSSProperties, type PropsWithChildren, useCallback } from 'react'
|
|
5
|
-
import useTilg from 'tilg'
|
|
2
|
+
import { deepEqual as eq, shallowEqual } from 'fast-equals'
|
|
3
|
+
import { type CSSProperties, type PropsWithChildren, useCallback, useMemo } from 'react'
|
|
6
4
|
import type { DiagramState } from '../state/diagramStore'
|
|
7
5
|
import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
|
|
8
6
|
import { MinZoom } from './const'
|
|
@@ -13,6 +11,7 @@ import { CompoundNode } from './nodes/compound'
|
|
|
13
11
|
import { ElementNode } from './nodes/element'
|
|
14
12
|
import { XYFlowEdge, XYFlowNode } from './types'
|
|
15
13
|
import { useXYFlowEvents } from './XYFlowEvents'
|
|
14
|
+
// import { useLogger } from '@mantine/hooks'
|
|
16
15
|
|
|
17
16
|
const nodeTypes = {
|
|
18
17
|
element: ElementNode,
|
|
@@ -25,8 +24,6 @@ const edgeTypes = {
|
|
|
25
24
|
type XYFlowWrapperProps = PropsWithChildren<{
|
|
26
25
|
colorMode?: 'system' | 'light' | 'dark'
|
|
27
26
|
className?: string | undefined
|
|
28
|
-
defaultNodes: XYFlowNode[]
|
|
29
|
-
defaultEdges: XYFlowEdge[]
|
|
30
27
|
style?: CSSProperties | undefined
|
|
31
28
|
}>
|
|
32
29
|
|
|
@@ -41,15 +38,15 @@ type XYFlowWrapperProps = PropsWithChildren<{
|
|
|
41
38
|
// }
|
|
42
39
|
|
|
43
40
|
const selector = (s: DiagramState) => ({
|
|
41
|
+
nodes: s.xynodes,
|
|
42
|
+
edges: s.xyedges,
|
|
43
|
+
onInit: s.onInit,
|
|
44
|
+
onNodesChange: s.onNodesChange,
|
|
45
|
+
onEdgesChange: s.onEdgesChange,
|
|
44
46
|
nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
|
|
45
47
|
nodesDraggable: s.nodesDraggable,
|
|
46
48
|
fitView: s.fitViewEnabled,
|
|
47
|
-
|
|
48
|
-
minZoom: MinZoom,
|
|
49
|
-
maxZoom: 1,
|
|
50
|
-
padding: s.fitViewPadding,
|
|
51
|
-
includeHiddenNodes: true
|
|
52
|
-
},
|
|
49
|
+
fitViewPadding: s.fitViewPadding,
|
|
53
50
|
hasOnNavigateTo: !!s.onNavigateTo,
|
|
54
51
|
hasOnNodeClick: !!s.onNodeClick,
|
|
55
52
|
hasOnNodeContextMenu: !!s.onNodeContextMenu,
|
|
@@ -57,25 +54,30 @@ const selector = (s: DiagramState) => ({
|
|
|
57
54
|
hasOnEdgeContextMenu: !!s.onEdgeContextMenu,
|
|
58
55
|
hasOnEdgeClick: !!s.onEdgeClick,
|
|
59
56
|
zoomable: s.zoomable,
|
|
60
|
-
pannable: s.pannable
|
|
57
|
+
pannable: s.pannable,
|
|
58
|
+
// If fitView is not enabled
|
|
59
|
+
// And diagram starts with a negative x or y - we need to translate viewprot
|
|
60
|
+
translateX: s.fitViewEnabled ? 0 : -Math.min(s.view.bounds.x, 0),
|
|
61
|
+
translateY: s.fitViewEnabled ? 0 : -Math.min(s.view.bounds.y, 0)
|
|
61
62
|
})
|
|
62
63
|
|
|
63
64
|
export function XYFlow({
|
|
64
65
|
colorMode = 'system',
|
|
65
66
|
className,
|
|
66
67
|
children,
|
|
67
|
-
defaultNodes,
|
|
68
|
-
defaultEdges,
|
|
69
68
|
style
|
|
70
69
|
}: XYFlowWrapperProps) {
|
|
71
|
-
DEV && useTilg()
|
|
72
70
|
const xyflowApi = useXYStoreApi()
|
|
73
|
-
const diagramApi = useDiagramStoreApi()
|
|
74
71
|
const {
|
|
72
|
+
nodes,
|
|
73
|
+
edges,
|
|
74
|
+
onInit,
|
|
75
|
+
onNodesChange,
|
|
76
|
+
onEdgesChange,
|
|
75
77
|
nodesSelectable,
|
|
76
78
|
nodesDraggable,
|
|
77
79
|
fitView,
|
|
78
|
-
|
|
80
|
+
fitViewPadding,
|
|
79
81
|
pannable,
|
|
80
82
|
zoomable,
|
|
81
83
|
hasOnNodeClick,
|
|
@@ -83,8 +85,14 @@ export function XYFlow({
|
|
|
83
85
|
hasOnNodeContextMenu,
|
|
84
86
|
hasOnCanvasContextMenu,
|
|
85
87
|
hasOnEdgeContextMenu,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
translateX,
|
|
89
|
+
translateY
|
|
90
|
+
} = useDiagramState(selector, shallowEqual)
|
|
91
|
+
|
|
92
|
+
// useLogger('XYFlow',[
|
|
93
|
+
// nodes,
|
|
94
|
+
// edges,
|
|
95
|
+
// ])
|
|
88
96
|
|
|
89
97
|
const layoutConstraints = useLayoutConstraints()
|
|
90
98
|
|
|
@@ -98,12 +106,10 @@ export function XYFlow({
|
|
|
98
106
|
*/
|
|
99
107
|
useOnViewportChange({
|
|
100
108
|
onEnd: ({ x, y, zoom }) => {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (x !== rounded.x || y !== rounded.y) {
|
|
106
|
-
xyflowApi.setState({ transform: [rounded.x, rounded.y, zoom] })
|
|
109
|
+
const roundedX = Math.round(x),
|
|
110
|
+
roundedY = Math.round(y)
|
|
111
|
+
if (x !== roundedX || y !== roundedY) {
|
|
112
|
+
xyflowApi.setState({ transform: [roundedX, roundedY, zoom] })
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
115
|
})
|
|
@@ -113,8 +119,8 @@ export function XYFlow({
|
|
|
113
119
|
className={className}
|
|
114
120
|
style={style}
|
|
115
121
|
{...colorMode && { colorMode }}
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
nodes={nodes}
|
|
123
|
+
edges={edges}
|
|
118
124
|
nodeTypes={nodeTypes}
|
|
119
125
|
edgeTypes={edgeTypes}
|
|
120
126
|
zoomOnPinch={zoomable}
|
|
@@ -125,26 +131,43 @@ export function XYFlow({
|
|
|
125
131
|
maxZoom={zoomable ? 1.9 : 1}
|
|
126
132
|
minZoom={zoomable ? MinZoom : 1}
|
|
127
133
|
fitView={fitView}
|
|
128
|
-
fitViewOptions={
|
|
134
|
+
fitViewOptions={useMemo(() => ({
|
|
135
|
+
minZoom: MinZoom,
|
|
136
|
+
maxZoom: 1,
|
|
137
|
+
padding: fitViewPadding,
|
|
138
|
+
includeHiddenNodes: true
|
|
139
|
+
}), [fitViewPadding])}
|
|
140
|
+
{...(!fitView && {
|
|
141
|
+
viewport: {
|
|
142
|
+
x: translateX,
|
|
143
|
+
y: translateY,
|
|
144
|
+
zoom: 1
|
|
145
|
+
}
|
|
146
|
+
})}
|
|
129
147
|
preventScrolling={zoomable || pannable}
|
|
130
148
|
defaultMarkerColor="var(--xy-edge-stroke)"
|
|
131
149
|
noDragClassName="nodrag"
|
|
132
150
|
noPanClassName="nopan"
|
|
133
151
|
panOnScroll={pannable}
|
|
134
152
|
panOnDrag={pannable}
|
|
153
|
+
{...(!pannable && {
|
|
154
|
+
selectionKeyCode: null
|
|
155
|
+
})}
|
|
135
156
|
elementsSelectable={nodesSelectable}
|
|
136
157
|
nodesFocusable={nodesDraggable || nodesSelectable || hasOnNodeClick || hasOnNavigateTo}
|
|
137
|
-
edgesFocusable={
|
|
158
|
+
edgesFocusable={false}
|
|
138
159
|
nodesDraggable={nodesDraggable}
|
|
139
160
|
{...nodesDraggable && {
|
|
140
161
|
onNodeDragStart: layoutConstraints.onNodeDragStart,
|
|
141
162
|
onNodeDrag: layoutConstraints.onNodeDrag,
|
|
142
163
|
onNodeDragStop: layoutConstraints.onNodeDragStop
|
|
143
164
|
}}
|
|
165
|
+
nodeDragThreshold={3}
|
|
144
166
|
zoomOnDoubleClick={false}
|
|
145
|
-
elevateNodesOnSelect={false} // or edges are not visible after select
|
|
167
|
+
elevateNodesOnSelect={false} // or edges are not visible after select\
|
|
146
168
|
selectNodesOnDrag={false} // or weird camera movement
|
|
147
|
-
|
|
169
|
+
onNodesChange={onNodesChange}
|
|
170
|
+
onEdgesChange={onEdgesChange}
|
|
148
171
|
onDoubleClick={handlers.onDoubleClick}
|
|
149
172
|
onPaneClick={handlers.onPaneClick}
|
|
150
173
|
onMoveEnd={handlers.onMoveEnd}
|
|
@@ -155,9 +178,8 @@ export function XYFlow({
|
|
|
155
178
|
onNodeClick={handlers.onNodeClick}
|
|
156
179
|
onNodeDoubleClick={handlers.onNodeDoubleClick}
|
|
157
180
|
onEdgeClick={handlers.onEdgeClick}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}, [diagramApi])}
|
|
181
|
+
onEdgeDoubleClick={handlers.onEdgeDoubleClick}
|
|
182
|
+
onInit={onInit}
|
|
161
183
|
{...(hasOnNodeContextMenu && {
|
|
162
184
|
onNodeContextMenu: handlers.onNodeContextMenu
|
|
163
185
|
})}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { extractStep } from '@likec4/core'
|
|
1
2
|
import type { ReactFlowProps } from '@xyflow/react'
|
|
2
3
|
import { useMemo, useRef } from 'react'
|
|
4
|
+
import { isNonNullish, isTruthy } from 'remeda'
|
|
3
5
|
import type { Simplify } from 'type-fest'
|
|
4
6
|
import { useDiagramStoreApi } from '../state/useDiagramStore'
|
|
5
7
|
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
@@ -13,6 +15,7 @@ export type XYFlowEventHandlers = Simplify<
|
|
|
13
15
|
| 'onNodeClick'
|
|
14
16
|
| 'onNodeDoubleClick'
|
|
15
17
|
| 'onEdgeClick'
|
|
18
|
+
| 'onEdgeDoubleClick'
|
|
16
19
|
| 'onMoveEnd'
|
|
17
20
|
| 'onNodeContextMenu'
|
|
18
21
|
| 'onEdgeContextMenu'
|
|
@@ -110,22 +113,20 @@ export function useXYFlowEvents() {
|
|
|
110
113
|
},
|
|
111
114
|
onNodeClick: (event, xynode) => {
|
|
112
115
|
const {
|
|
113
|
-
zoomable,
|
|
114
|
-
fitViewEnabled,
|
|
115
116
|
focusedNodeId,
|
|
116
117
|
fitDiagram,
|
|
117
118
|
focusOnNode,
|
|
118
119
|
onNodeClick,
|
|
120
|
+
enableFocusMode,
|
|
119
121
|
lastClickedNodeId,
|
|
120
122
|
setLastClickedNode
|
|
121
123
|
} = diagramApi.getState()
|
|
122
124
|
setLastClickedNode(xynode.id)
|
|
123
|
-
const focusPossible = zoomable && fitViewEnabled
|
|
124
125
|
// if we focused on a node, and clicked on another node - focus on the clicked node
|
|
125
126
|
const shallChangeFocus = !!focusedNodeId && focusedNodeId !== xynode.id
|
|
126
127
|
// if user clicked on the same node twice in a short time, focus on it
|
|
127
128
|
const clickedRecently = lastClickedNodeId === xynode.id && lastClickWasRecent()
|
|
128
|
-
if (
|
|
129
|
+
if (enableFocusMode) {
|
|
129
130
|
let stopPropagation = false
|
|
130
131
|
switch (true) {
|
|
131
132
|
case !focusedNodeId && clickedRecently:
|
|
@@ -153,15 +154,14 @@ export function useXYFlowEvents() {
|
|
|
153
154
|
onNodeDoubleClick: (event, xynode) => {
|
|
154
155
|
const {
|
|
155
156
|
focusedNodeId,
|
|
156
|
-
|
|
157
|
-
fitViewEnabled,
|
|
157
|
+
enableFocusMode,
|
|
158
158
|
fitDiagram,
|
|
159
159
|
focusOnNode,
|
|
160
160
|
setLastClickedNode
|
|
161
161
|
} = diagramApi.getState()
|
|
162
162
|
setLastClickedNode(xynode.id)
|
|
163
163
|
lastClickTimestamp.current = Date.now()
|
|
164
|
-
if (
|
|
164
|
+
if (isNonNullish(focusedNodeId) || enableFocusMode) {
|
|
165
165
|
// if we are already focused on the node, cancel
|
|
166
166
|
if (focusedNodeId === xynode.id) {
|
|
167
167
|
fitDiagram()
|
|
@@ -172,14 +172,36 @@ export function useXYFlowEvents() {
|
|
|
172
172
|
}
|
|
173
173
|
},
|
|
174
174
|
onEdgeClick: (event, xyedge) => {
|
|
175
|
-
diagramApi.getState().setLastClickedEdge(xyedge.id)
|
|
176
175
|
const {
|
|
176
|
+
lastClickedEdgeId,
|
|
177
|
+
isDynamicView,
|
|
178
|
+
enableDynamicViewWalkthrough,
|
|
179
|
+
activateDynamicStep,
|
|
180
|
+
activeDynamicViewStep,
|
|
177
181
|
focusedNodeId,
|
|
178
182
|
focusOnNode,
|
|
179
|
-
onEdgeClick
|
|
183
|
+
onEdgeClick,
|
|
184
|
+
setLastClickedEdge
|
|
180
185
|
} = diagramApi.getState()
|
|
186
|
+
if (lastClickedEdgeId !== xyedge.id) {
|
|
187
|
+
setLastClickedEdge(xyedge.id)
|
|
188
|
+
}
|
|
189
|
+
const isNotAFirstClick = lastClickedEdgeId === xyedge.id
|
|
190
|
+
const isEdgeOfFocusedNode = isTruthy(focusedNodeId)
|
|
191
|
+
&& (focusedNodeId === xyedge.source || focusedNodeId === xyedge.target)
|
|
192
|
+
if (
|
|
193
|
+
isDynamicView && enableDynamicViewWalkthrough
|
|
194
|
+
&& (isEdgeOfFocusedNode || isNotAFirstClick || isNonNullish(activeDynamicViewStep))
|
|
195
|
+
) {
|
|
196
|
+
const nextStep = extractStep(xyedge.data.edge.id)
|
|
197
|
+
if (activeDynamicViewStep !== nextStep) {
|
|
198
|
+
activateDynamicStep(nextStep)
|
|
199
|
+
event.stopPropagation()
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
}
|
|
181
203
|
// if we focused on a node, and clicked on an edge connected to it - focus on the other node
|
|
182
|
-
if (
|
|
204
|
+
if (isEdgeOfFocusedNode) {
|
|
183
205
|
focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source)
|
|
184
206
|
if (!onEdgeClick) {
|
|
185
207
|
// user did not provide a custom handler, stop propagation
|
|
@@ -192,6 +214,32 @@ export function useXYFlowEvents() {
|
|
|
192
214
|
event
|
|
193
215
|
})
|
|
194
216
|
},
|
|
217
|
+
onEdgeDoubleClick: (event, xyedge) => {
|
|
218
|
+
diagramApi.getState().setLastClickedEdge(xyedge.id)
|
|
219
|
+
const {
|
|
220
|
+
enableFocusMode,
|
|
221
|
+
isDynamicView,
|
|
222
|
+
enableDynamicViewWalkthrough,
|
|
223
|
+
focusOnNode,
|
|
224
|
+
activateDynamicStep,
|
|
225
|
+
activeDynamicViewStep
|
|
226
|
+
} = diagramApi.getState()
|
|
227
|
+
// if we are in dynamic view, and clicked on an edge, activate the step
|
|
228
|
+
if (isDynamicView && enableDynamicViewWalkthrough) {
|
|
229
|
+
const nextStep = extractStep(xyedge.data.edge.id)
|
|
230
|
+
if (activeDynamicViewStep !== nextStep) {
|
|
231
|
+
activateDynamicStep(extractStep(xyedge.data.edge.id))
|
|
232
|
+
event.stopPropagation()
|
|
233
|
+
}
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (enableFocusMode) {
|
|
238
|
+
focusOnNode(xyedge.source)
|
|
239
|
+
event.stopPropagation()
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
},
|
|
195
243
|
onMoveEnd: (event, _viewport) => {
|
|
196
244
|
// if event is present, the move was triggered by user
|
|
197
245
|
const viewportChanged = !!event
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Controls } from '@xyflow/react'
|
|
2
2
|
import { shallowEqual } from 'fast-equals'
|
|
3
3
|
import { memo, type PropsWithChildren } from 'react'
|
|
4
|
-
import {
|
|
4
|
+
import { isNonNullish } from 'remeda'
|
|
5
5
|
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
6
6
|
import { useDiagramState } from '../state/useDiagramStore'
|
|
7
7
|
import { BackwardForwardPanel } from '../ui/BackwardForwardPanel'
|
|
@@ -15,7 +15,6 @@ type XYFlowInnerProps = PropsWithChildren<{
|
|
|
15
15
|
controls: boolean
|
|
16
16
|
showDiagramTitle: boolean
|
|
17
17
|
showNavigationButtons: boolean
|
|
18
|
-
enableDynamicViewWalkthrough: boolean
|
|
19
18
|
}>
|
|
20
19
|
|
|
21
20
|
export const XYFlowInner = memo(function XYFlowInnerR({
|
|
@@ -23,22 +22,23 @@ export const XYFlowInner = memo(function XYFlowInnerR({
|
|
|
23
22
|
background,
|
|
24
23
|
controls,
|
|
25
24
|
showDiagramTitle,
|
|
26
|
-
showNavigationButtons
|
|
27
|
-
enableDynamicViewWalkthrough
|
|
25
|
+
showNavigationButtons
|
|
28
26
|
}: XYFlowInnerProps) {
|
|
29
27
|
const {
|
|
30
28
|
isDynamicView,
|
|
31
29
|
isDynamicViewActive,
|
|
32
|
-
readonly
|
|
30
|
+
readonly,
|
|
31
|
+
enableDynamicViewWalkthrough
|
|
33
32
|
} = useDiagramState(s => ({
|
|
34
|
-
isDynamicView: s.isDynamicView
|
|
33
|
+
isDynamicView: s.isDynamicView,
|
|
35
34
|
readonly: s.readonly,
|
|
36
|
-
|
|
35
|
+
enableDynamicViewWalkthrough: s.enableDynamicViewWalkthrough,
|
|
36
|
+
isDynamicViewActive: isNonNullish(s.activeDynamicViewStep)
|
|
37
37
|
}))
|
|
38
38
|
|
|
39
39
|
const isBgWithPattern = background !== 'transparent' && background !== 'solid'
|
|
40
40
|
|
|
41
|
-
const isDiagramTitleVisible =
|
|
41
|
+
const isDiagramTitleVisible = showDiagramTitle === true && !isDynamicViewActive
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
<>
|
|
@@ -52,3 +52,4 @@ export const XYFlowInner = memo(function XYFlowInnerR({
|
|
|
52
52
|
</>
|
|
53
53
|
)
|
|
54
54
|
}, shallowEqual)
|
|
55
|
+
XYFlowInner.displayName = 'XYFlowInner'
|