@likec4/diagram 1.6.1 → 1.7.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 +10 -11
- package/dist/globals.css.d.ts +1 -0
- package/dist/hooks/useUpdateEffect.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +572 -464
- 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 -874
- 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 +6 -6
- package/src/LikeC4Diagram.props.ts +17 -13
- package/src/LikeC4Diagram.tsx +62 -96
- package/src/globals.css.ts +12 -6
- package/src/hooks/useUpdateEffect.ts +16 -1
- package/src/index.ts +1 -2
- 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 -32
- package/src/xyflow/XYFlowEvents.tsx +58 -10
- package/src/xyflow/XYFlowInner.tsx +8 -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 +0 -4
- 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,6 +1,6 @@
|
|
|
1
1
|
import { ReactFlow, useOnViewportChange } from '@xyflow/react'
|
|
2
|
-
import { deepEqual as eq } from 'fast-equals'
|
|
3
|
-
import { type CSSProperties, type PropsWithChildren, useCallback } from 'react'
|
|
2
|
+
import { deepEqual as eq, shallowEqual } from 'fast-equals'
|
|
3
|
+
import { type CSSProperties, type PropsWithChildren, useCallback, useMemo } from 'react'
|
|
4
4
|
import type { DiagramState } from '../state/diagramStore'
|
|
5
5
|
import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
|
|
6
6
|
import { MinZoom } from './const'
|
|
@@ -11,6 +11,7 @@ import { CompoundNode } from './nodes/compound'
|
|
|
11
11
|
import { ElementNode } from './nodes/element'
|
|
12
12
|
import { XYFlowEdge, XYFlowNode } from './types'
|
|
13
13
|
import { useXYFlowEvents } from './XYFlowEvents'
|
|
14
|
+
// import { useLogger } from '@mantine/hooks'
|
|
14
15
|
|
|
15
16
|
const nodeTypes = {
|
|
16
17
|
element: ElementNode,
|
|
@@ -23,8 +24,6 @@ const edgeTypes = {
|
|
|
23
24
|
type XYFlowWrapperProps = PropsWithChildren<{
|
|
24
25
|
colorMode?: 'system' | 'light' | 'dark'
|
|
25
26
|
className?: string | undefined
|
|
26
|
-
defaultNodes: XYFlowNode[]
|
|
27
|
-
defaultEdges: XYFlowEdge[]
|
|
28
27
|
style?: CSSProperties | undefined
|
|
29
28
|
}>
|
|
30
29
|
|
|
@@ -39,15 +38,15 @@ type XYFlowWrapperProps = PropsWithChildren<{
|
|
|
39
38
|
// }
|
|
40
39
|
|
|
41
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,
|
|
42
46
|
nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
|
|
43
47
|
nodesDraggable: s.nodesDraggable,
|
|
44
48
|
fitView: s.fitViewEnabled,
|
|
45
|
-
|
|
46
|
-
minZoom: MinZoom,
|
|
47
|
-
maxZoom: 1,
|
|
48
|
-
padding: s.fitViewPadding,
|
|
49
|
-
includeHiddenNodes: true
|
|
50
|
-
},
|
|
49
|
+
fitViewPadding: s.fitViewPadding,
|
|
51
50
|
hasOnNavigateTo: !!s.onNavigateTo,
|
|
52
51
|
hasOnNodeClick: !!s.onNodeClick,
|
|
53
52
|
hasOnNodeContextMenu: !!s.onNodeContextMenu,
|
|
@@ -55,24 +54,30 @@ const selector = (s: DiagramState) => ({
|
|
|
55
54
|
hasOnEdgeContextMenu: !!s.onEdgeContextMenu,
|
|
56
55
|
hasOnEdgeClick: !!s.onEdgeClick,
|
|
57
56
|
zoomable: s.zoomable,
|
|
58
|
-
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)
|
|
59
62
|
})
|
|
60
63
|
|
|
61
64
|
export function XYFlow({
|
|
62
65
|
colorMode = 'system',
|
|
63
66
|
className,
|
|
64
67
|
children,
|
|
65
|
-
defaultNodes,
|
|
66
|
-
defaultEdges,
|
|
67
68
|
style
|
|
68
69
|
}: XYFlowWrapperProps) {
|
|
69
70
|
const xyflowApi = useXYStoreApi()
|
|
70
|
-
const diagramApi = useDiagramStoreApi()
|
|
71
71
|
const {
|
|
72
|
+
nodes,
|
|
73
|
+
edges,
|
|
74
|
+
onInit,
|
|
75
|
+
onNodesChange,
|
|
76
|
+
onEdgesChange,
|
|
72
77
|
nodesSelectable,
|
|
73
78
|
nodesDraggable,
|
|
74
79
|
fitView,
|
|
75
|
-
|
|
80
|
+
fitViewPadding,
|
|
76
81
|
pannable,
|
|
77
82
|
zoomable,
|
|
78
83
|
hasOnNodeClick,
|
|
@@ -80,8 +85,14 @@ export function XYFlow({
|
|
|
80
85
|
hasOnNodeContextMenu,
|
|
81
86
|
hasOnCanvasContextMenu,
|
|
82
87
|
hasOnEdgeContextMenu,
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
translateX,
|
|
89
|
+
translateY
|
|
90
|
+
} = useDiagramState(selector, shallowEqual)
|
|
91
|
+
|
|
92
|
+
// useLogger('XYFlow',[
|
|
93
|
+
// nodes,
|
|
94
|
+
// edges,
|
|
95
|
+
// ])
|
|
85
96
|
|
|
86
97
|
const layoutConstraints = useLayoutConstraints()
|
|
87
98
|
|
|
@@ -95,12 +106,10 @@ export function XYFlow({
|
|
|
95
106
|
*/
|
|
96
107
|
useOnViewportChange({
|
|
97
108
|
onEnd: ({ x, y, zoom }) => {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (x !== rounded.x || y !== rounded.y) {
|
|
103
|
-
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] })
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
})
|
|
@@ -110,8 +119,8 @@ export function XYFlow({
|
|
|
110
119
|
className={className}
|
|
111
120
|
style={style}
|
|
112
121
|
{...colorMode && { colorMode }}
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
nodes={nodes}
|
|
123
|
+
edges={edges}
|
|
115
124
|
nodeTypes={nodeTypes}
|
|
116
125
|
edgeTypes={edgeTypes}
|
|
117
126
|
zoomOnPinch={zoomable}
|
|
@@ -122,26 +131,43 @@ export function XYFlow({
|
|
|
122
131
|
maxZoom={zoomable ? 1.9 : 1}
|
|
123
132
|
minZoom={zoomable ? MinZoom : 1}
|
|
124
133
|
fitView={fitView}
|
|
125
|
-
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
|
+
})}
|
|
126
147
|
preventScrolling={zoomable || pannable}
|
|
127
148
|
defaultMarkerColor="var(--xy-edge-stroke)"
|
|
128
149
|
noDragClassName="nodrag"
|
|
129
150
|
noPanClassName="nopan"
|
|
130
151
|
panOnScroll={pannable}
|
|
131
152
|
panOnDrag={pannable}
|
|
153
|
+
{...(!pannable && {
|
|
154
|
+
selectionKeyCode: null
|
|
155
|
+
})}
|
|
132
156
|
elementsSelectable={nodesSelectable}
|
|
133
157
|
nodesFocusable={nodesDraggable || nodesSelectable || hasOnNodeClick || hasOnNavigateTo}
|
|
134
|
-
edgesFocusable={
|
|
158
|
+
edgesFocusable={false}
|
|
135
159
|
nodesDraggable={nodesDraggable}
|
|
136
160
|
{...nodesDraggable && {
|
|
137
161
|
onNodeDragStart: layoutConstraints.onNodeDragStart,
|
|
138
162
|
onNodeDrag: layoutConstraints.onNodeDrag,
|
|
139
163
|
onNodeDragStop: layoutConstraints.onNodeDragStop
|
|
140
164
|
}}
|
|
165
|
+
nodeDragThreshold={3}
|
|
141
166
|
zoomOnDoubleClick={false}
|
|
142
|
-
elevateNodesOnSelect={false} // or edges are not visible after select
|
|
167
|
+
elevateNodesOnSelect={false} // or edges are not visible after select\
|
|
143
168
|
selectNodesOnDrag={false} // or weird camera movement
|
|
144
|
-
|
|
169
|
+
onNodesChange={onNodesChange}
|
|
170
|
+
onEdgesChange={onEdgesChange}
|
|
145
171
|
onDoubleClick={handlers.onDoubleClick}
|
|
146
172
|
onPaneClick={handlers.onPaneClick}
|
|
147
173
|
onMoveEnd={handlers.onMoveEnd}
|
|
@@ -152,9 +178,8 @@ export function XYFlow({
|
|
|
152
178
|
onNodeClick={handlers.onNodeClick}
|
|
153
179
|
onNodeDoubleClick={handlers.onNodeDoubleClick}
|
|
154
180
|
onEdgeClick={handlers.onEdgeClick}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}, [diagramApi])}
|
|
181
|
+
onEdgeDoubleClick={handlers.onEdgeDoubleClick}
|
|
182
|
+
onInit={onInit}
|
|
158
183
|
{...(hasOnNodeContextMenu && {
|
|
159
184
|
onNodeContextMenu: handlers.onNodeContextMenu
|
|
160
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
|
<>
|