@likec4/diagram 1.1.1 → 1.2.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 +1 -1
- package/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +10 -5
- package/dist/StaticLikeC4Diagram.d.ts +1 -1
- package/dist/hooks/useUpdateEffect.d.ts +1 -1
- package/dist/icons/Clipboard.d.ts +1 -1
- package/dist/icons/Link.d.ts +1 -1
- package/dist/icons/ZoomIn.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2604 -1502
- package/dist/mantine/EnsureMantine.d.ts +1 -1
- package/dist/state/DiagramContext.d.ts +2307 -192
- package/dist/state/WhenInitialized.d.ts +1 -1
- package/dist/state/diagramStore.d.ts +2220 -97
- package/dist/state/useDiagramStore.d.ts +2124 -9
- package/dist/style.css +210 -136
- package/dist/theme.css.d.ts +1 -0
- package/dist/ui/DiagramTitlePanel.d.ts +1 -2
- package/dist/ui/DynamicViewWalkthrough.css.d.ts +3 -0
- package/dist/ui/DynamicViewWalkthrough.d.ts +1 -0
- package/dist/ui/OptionsPanel.d.ts +1 -2
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/options/NodeOptions.d.ts +1 -2
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +4 -2
- package/dist/xyflow/SyncWithDiagram.d.ts +1 -2
- package/dist/xyflow/XYFlow.d.ts +5 -8
- package/dist/xyflow/XYFlowBackground.d.ts +6 -5
- package/dist/xyflow/XYFlowEvents.d.ts +60 -66
- package/dist/xyflow/XYFlowInner.d.ts +11 -0
- package/dist/xyflow/diagram-to-xyflow.d.ts +2 -2
- package/dist/xyflow/edges/RelationshipEdge.d.ts +17 -17
- package/dist/xyflow/edges/edges.css.d.ts +4 -2
- package/dist/xyflow/edges/utils.d.ts +3 -1
- package/dist/xyflow/hooks/useLayoutConstraints.d.ts +1 -1
- package/dist/xyflow/hooks/useXYFlow.d.ts +162 -162
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +1 -0
- package/dist/xyflow/nodes/compound/CompoundNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementIcon.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementLink.d.ts +1 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +2 -2
- package/dist/xyflow/nodes/element/ElementShapeSvg.d.ts +2 -2
- package/dist/xyflow/nodes/element/element.css.d.ts +2 -1
- package/dist/xyflow/nodes/shared/NavigateToBtn.d.ts +1 -2
- package/dist/xyflow/types.d.ts +23 -23
- package/package.json +23 -21
- package/src/KeepAspectRatioContainer.tsx +1 -1
- package/src/LikeC4Diagram.css.ts +2 -0
- package/src/LikeC4Diagram.props.ts +9 -4
- package/src/LikeC4Diagram.tsx +17 -11
- package/src/StaticLikeC4Diagram.tsx +1 -0
- package/src/globals.css.ts +1 -0
- package/src/index.ts +0 -3
- package/src/state/DiagramContext.tsx +21 -12
- package/src/state/diagramStore.ts +177 -38
- package/src/theme.css.ts +1 -0
- package/src/ui/DiagramTitlePanel.css.ts +2 -1
- package/src/ui/DiagramTitlePanel.tsx +3 -4
- package/src/ui/DynamicViewWalkthrough.css.ts +46 -0
- package/src/ui/DynamicViewWalkthrough.tsx +86 -0
- package/src/ui/OptionsPanel.tsx +5 -3
- package/src/ui/index.ts +1 -0
- package/src/ui/options/NodeOptions.tsx +23 -24
- package/src/xyflow/FitviewOnDiagramChange.tsx +89 -69
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +57 -39
- package/src/xyflow/SyncWithDiagram.tsx +55 -107
- package/src/xyflow/XYFlow.tsx +51 -35
- package/src/xyflow/XYFlowBackground.tsx +7 -5
- package/src/xyflow/XYFlowEvents.tsx +100 -95
- package/src/xyflow/XYFlowInner.tsx +50 -0
- package/src/xyflow/diagram-to-xyflow.ts +16 -21
- package/src/xyflow/edges/RelationshipEdge.tsx +70 -34
- package/src/xyflow/edges/edges.css.ts +68 -34
- package/src/xyflow/edges/utils.ts +31 -0
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +6 -2
- package/src/xyflow/nodes/compound/CompoundNode.tsx +6 -3
- package/src/xyflow/nodes/element/ElementLink.tsx +13 -3
- package/src/xyflow/nodes/element/ElementNode.tsx +24 -19
- package/src/xyflow/nodes/element/element.css.ts +9 -2
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +1 -0
- package/src/xyflow/types.ts +7 -8
|
@@ -1,125 +1,73 @@
|
|
|
1
|
-
import { deepEqual as eq
|
|
2
|
-
import {
|
|
3
|
-
import type { XYFlowNode } from './types'
|
|
1
|
+
import { deepEqual as eq } from 'fast-equals'
|
|
2
|
+
import { useXYStoreApi } from './hooks'
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
import { useDiagramStoreApi } from '../state'
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import { useDiagramState, useDiagramStoreApi } from '../state'
|
|
7
6
|
import { diagramViewToXYFlowData } from './diagram-to-xyflow'
|
|
8
|
-
import { toDomPrecision } from './utils'
|
|
9
|
-
|
|
10
|
-
const isSameNode = (a: XYFlowNode) => (b: XYFlowNode) =>
|
|
11
|
-
a.id === b.id && a.type === b.type && Object.is(a.parentId, b.parentId)
|
|
12
7
|
|
|
13
8
|
/**
|
|
14
9
|
* Syncs the diagram state with the XYFlow instance
|
|
15
10
|
*/
|
|
16
|
-
export
|
|
17
|
-
const
|
|
18
|
-
const xyflowRef = useRef(_xyflow)
|
|
19
|
-
xyflowRef.current = _xyflow
|
|
20
|
-
|
|
11
|
+
export function SyncWithDiagram() {
|
|
12
|
+
const xyflowSynced = useDiagramState(s => s.xyflowSynced)
|
|
21
13
|
const xyflowApi = useXYStoreApi()
|
|
22
14
|
const diagramStoreApi = useDiagramStoreApi()
|
|
23
15
|
|
|
24
16
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
focusedNodeId
|
|
39
|
-
} = diagramStoreApi.getState()
|
|
40
|
-
const updates = diagramViewToXYFlowData({ nodes, edges }, {
|
|
41
|
-
draggable: nodesDraggable,
|
|
42
|
-
selectable: nodesSelectable || focusedNodeId !== null
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
xyflowRef.current.setNodes(prev =>
|
|
46
|
-
updates.nodes.map(update => {
|
|
47
|
-
const existing = prev.find(isSameNode(update))
|
|
48
|
-
if (existing) {
|
|
49
|
-
if (eq(existing.data.element, update.data.element)) {
|
|
50
|
-
return existing
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
...existing,
|
|
54
|
-
...update
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return update
|
|
58
|
-
})
|
|
59
|
-
)
|
|
17
|
+
if (xyflowSynced) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
const {
|
|
21
|
+
view,
|
|
22
|
+
nodesDraggable,
|
|
23
|
+
nodesSelectable,
|
|
24
|
+
focusedNodeId
|
|
25
|
+
} = diagramStoreApi.getState()
|
|
26
|
+
const updates = diagramViewToXYFlowData(view, {
|
|
27
|
+
draggable: nodesDraggable,
|
|
28
|
+
selectable: nodesSelectable || focusedNodeId !== null
|
|
29
|
+
})
|
|
60
30
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
...update
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return update
|
|
71
|
-
})
|
|
72
|
-
)
|
|
31
|
+
const {
|
|
32
|
+
nodeLookup,
|
|
33
|
+
edgeLookup,
|
|
34
|
+
setNodes,
|
|
35
|
+
setEdges
|
|
36
|
+
} = xyflowApi.getState()
|
|
73
37
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
xyflowRef.current.setViewport({
|
|
88
|
-
x: v.x,
|
|
89
|
-
y: v.y,
|
|
90
|
-
zoom: nextZoom
|
|
91
|
-
})
|
|
92
|
-
}
|
|
38
|
+
setNodes(updates.nodes.map(update => {
|
|
39
|
+
const existing = nodeLookup.get(update.id)?.internals.userNode
|
|
40
|
+
if (existing && existing.type === update.type && eq(existing.parentId, update.parentId)) {
|
|
41
|
+
if (eq(existing.data.element, update.data.element)) {
|
|
42
|
+
return existing
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
...existing,
|
|
46
|
+
...update
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return update
|
|
50
|
+
}))
|
|
93
51
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
y: toDomPrecision(centerFrom.y - centerTo.y)
|
|
101
|
-
}
|
|
102
|
-
xyflowApi.getState().panBy(diff)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
diagramStoreApi.setState(
|
|
107
|
-
{
|
|
108
|
-
lastOnNavigate: {
|
|
109
|
-
...lastOnNavigate,
|
|
110
|
-
positionCorrected: true
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
false,
|
|
114
|
-
'positionCorrected'
|
|
115
|
-
)
|
|
52
|
+
setEdges(updates.edges.map(update => {
|
|
53
|
+
const existing = edgeLookup.get(update.id)
|
|
54
|
+
if (existing) {
|
|
55
|
+
return eq(existing.data.edge, update.data.edge) ? existing : {
|
|
56
|
+
...existing,
|
|
57
|
+
...update
|
|
116
58
|
}
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
equalityFn: shallowEqual
|
|
120
59
|
}
|
|
60
|
+
return update
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
diagramStoreApi.setState(
|
|
64
|
+
{
|
|
65
|
+
xyflowSynced: true
|
|
66
|
+
},
|
|
67
|
+
false,
|
|
68
|
+
'xyflowSynced'
|
|
121
69
|
)
|
|
122
|
-
}, [xyflowApi, diagramStoreApi])
|
|
70
|
+
}, [xyflowSynced, xyflowApi, diagramStoreApi])
|
|
123
71
|
|
|
124
72
|
return null
|
|
125
|
-
}
|
|
73
|
+
}
|
package/src/xyflow/XYFlow.tsx
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { useMantineColorScheme } from '@mantine/core'
|
|
2
|
-
import { Controls,
|
|
3
|
-
import {
|
|
2
|
+
import { Controls, ReactFlow, useOnViewportChange } from '@xyflow/react'
|
|
3
|
+
import { deepEqual as eq, shallowEqual } from 'fast-equals'
|
|
4
4
|
import { type CSSProperties, memo, type PropsWithChildren } from 'react'
|
|
5
|
+
import { omit, omitBy } from 'remeda'
|
|
5
6
|
import type { SetNonNullable, Simplify } from 'type-fest'
|
|
6
7
|
import type { LikeC4DiagramProperties } from '../LikeC4Diagram.props'
|
|
7
8
|
import { type DiagramState, useDiagramState, useDiagramStoreApi } from '../state'
|
|
8
9
|
import { MinZoom } from './const'
|
|
9
10
|
import { RelationshipEdge } from './edges/RelationshipEdge'
|
|
10
11
|
import { useLayoutConstraints } from './hooks/useLayoutConstraints'
|
|
12
|
+
import { useXYStoreApi } from './hooks/useXYFlow'
|
|
11
13
|
import { CompoundNode } from './nodes/compound'
|
|
12
14
|
import { ElementNode } from './nodes/element'
|
|
13
|
-
import { SelectEdgesOnNodeFocus } from './SelectEdgesOnNodeFocus'
|
|
14
15
|
import { XYFlowEdge, XYFlowNode } from './types'
|
|
15
16
|
import { XYFlowBackground } from './XYFlowBackground'
|
|
16
17
|
import { useXYFlowEvents } from './XYFlowEvents'
|
|
@@ -23,24 +24,22 @@ const edgeTypes = {
|
|
|
23
24
|
relationship: RelationshipEdge
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
type
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
>
|
|
33
|
-
>
|
|
27
|
+
type XYFlowWrapperProps = PropsWithChildren<{
|
|
28
|
+
className?: string | undefined
|
|
29
|
+
defaultNodes: XYFlowNode[]
|
|
30
|
+
defaultEdges: XYFlowEdge[]
|
|
31
|
+
style?: CSSProperties | undefined
|
|
32
|
+
}>
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
// const propsAreEqual = (
|
|
35
|
+
// prev: XYFlowWrapperProps,
|
|
36
|
+
// next: XYFlowWrapperProps
|
|
37
|
+
// ) => {
|
|
38
|
+
// return shallowEqual(
|
|
39
|
+
// omit(prev, ['children']),
|
|
40
|
+
// omit(next, ['children'])
|
|
41
|
+
// )
|
|
42
|
+
// }
|
|
44
43
|
|
|
45
44
|
const selector = (s: DiagramState) => ({
|
|
46
45
|
nodesSelectable: s.nodesSelectable || s.focusedNodeId !== null,
|
|
@@ -57,15 +56,14 @@ const selector = (s: DiagramState) => ({
|
|
|
57
56
|
pannable: s.pannable
|
|
58
57
|
})
|
|
59
58
|
|
|
60
|
-
function
|
|
59
|
+
export function XYFlow({
|
|
61
60
|
className,
|
|
62
61
|
children,
|
|
63
62
|
defaultNodes,
|
|
64
63
|
defaultEdges,
|
|
65
|
-
controls,
|
|
66
|
-
background,
|
|
67
64
|
style
|
|
68
65
|
}: XYFlowWrapperProps) {
|
|
66
|
+
const xyflowApi = useXYStoreApi()
|
|
69
67
|
const diagramApi = useDiagramStoreApi()
|
|
70
68
|
const {
|
|
71
69
|
nodesSelectable,
|
|
@@ -74,17 +72,39 @@ function XYFlowWrapper({
|
|
|
74
72
|
fitViewPadding,
|
|
75
73
|
pannable,
|
|
76
74
|
zoomable,
|
|
77
|
-
|
|
75
|
+
hasOnNodeClick,
|
|
76
|
+
hasOnNavigateTo,
|
|
77
|
+
hasOnNodeContextMenu,
|
|
78
|
+
hasOnCanvasContextMenu,
|
|
79
|
+
hasOnEdgeContextMenu,
|
|
80
|
+
hasOnEdgeClick
|
|
78
81
|
} = useDiagramState(selector)
|
|
79
82
|
|
|
80
83
|
const layoutConstraints = useLayoutConstraints()
|
|
81
84
|
|
|
82
85
|
const handlers = useXYFlowEvents()
|
|
83
|
-
const isBgWithPattern = background !== 'transparent' && background !== 'solid'
|
|
84
86
|
|
|
85
87
|
const { colorScheme } = useMantineColorScheme()
|
|
86
88
|
const colorMode = colorScheme !== 'auto' ? colorScheme : undefined
|
|
87
89
|
|
|
90
|
+
/**
|
|
91
|
+
* WORKAROUND - Called on viewport change
|
|
92
|
+
* Viewport transform is not rounded to integers which results in blurry nodes on some resolution
|
|
93
|
+
* https://github.com/xyflow/xyflow/issues/3282
|
|
94
|
+
* https://github.com/likec4/likec4/issues/734
|
|
95
|
+
*/
|
|
96
|
+
useOnViewportChange({
|
|
97
|
+
onEnd: ({ x, y, zoom }) => {
|
|
98
|
+
const rounded = {
|
|
99
|
+
x: Math.round(x),
|
|
100
|
+
y: Math.round(y)
|
|
101
|
+
}
|
|
102
|
+
if (x !== rounded.x || y !== rounded.y) {
|
|
103
|
+
xyflowApi.setState({ transform: [rounded.x, rounded.y, zoom] })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
88
108
|
return (
|
|
89
109
|
<ReactFlow<XYFlowNode, XYFlowEdge>
|
|
90
110
|
className={className}
|
|
@@ -115,8 +135,8 @@ function XYFlowWrapper({
|
|
|
115
135
|
panOnScroll={pannable}
|
|
116
136
|
panOnDrag={pannable}
|
|
117
137
|
elementsSelectable={nodesSelectable}
|
|
118
|
-
nodesFocusable={nodesDraggable || nodesSelectable ||
|
|
119
|
-
edgesFocusable={
|
|
138
|
+
nodesFocusable={nodesDraggable || nodesSelectable || hasOnNodeClick || hasOnNavigateTo}
|
|
139
|
+
edgesFocusable={hasOnEdgeClick}
|
|
120
140
|
{...(!nodesSelectable && {
|
|
121
141
|
selectionKeyCode: null
|
|
122
142
|
})}
|
|
@@ -139,25 +159,21 @@ function XYFlowWrapper({
|
|
|
139
159
|
onNodeClick={handlers.onNodeClick}
|
|
140
160
|
onNodeDoubleClick={handlers.onNodeDoubleClick}
|
|
141
161
|
onEdgeClick={handlers.onEdgeClick}
|
|
142
|
-
onViewportChange={handlers.onViewportChange}
|
|
143
162
|
onInit={() => {
|
|
144
163
|
diagramApi.setState({ initialized: true }, false, 'initialized')
|
|
145
164
|
}}
|
|
146
|
-
{...(
|
|
165
|
+
{...(hasOnNodeContextMenu && {
|
|
147
166
|
onNodeContextMenu: handlers.onNodeContextMenu
|
|
148
167
|
})}
|
|
149
|
-
{...(
|
|
168
|
+
{...(hasOnEdgeContextMenu && {
|
|
150
169
|
onEdgeContextMenu: handlers.onEdgeContextMenu
|
|
151
170
|
})}
|
|
152
|
-
{...(
|
|
171
|
+
{...(hasOnCanvasContextMenu && {
|
|
153
172
|
onPaneContextMenu: handlers.onPaneContextMenu
|
|
154
173
|
})}>
|
|
155
|
-
{isBgWithPattern && <XYFlowBackground background={background} />}
|
|
156
|
-
{controls && <Controls position={'bottom-right'} />}
|
|
157
|
-
{zoomable && !editor.hasOnNodeClick && <SelectEdgesOnNodeFocus />}
|
|
158
174
|
{children}
|
|
159
175
|
</ReactFlow>
|
|
160
176
|
)
|
|
161
177
|
}
|
|
162
178
|
|
|
163
|
-
export const XYFlow = memo(XYFlowWrapper) as typeof XYFlowWrapper
|
|
179
|
+
// export const XYFlow = memo(XYFlowWrapper, propsAreEqual) as typeof XYFlowWrapper
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { nonexhaustive } from '@likec4/core'
|
|
2
2
|
import { Background, type BackgroundProps, BackgroundVariant } from '@xyflow/react'
|
|
3
|
-
import { deepEqual } from 'fast-equals'
|
|
4
|
-
import { memo } from 'react'
|
|
5
3
|
import type { OverrideProperties } from 'type-fest'
|
|
6
4
|
|
|
7
5
|
export type XYBackgroundVariant = 'dots' | 'lines' | 'cross'
|
|
8
|
-
export type
|
|
6
|
+
export type XYBackground =
|
|
9
7
|
| XYBackgroundVariant
|
|
10
8
|
| OverrideProperties<BackgroundProps, {
|
|
11
9
|
variant: XYBackgroundVariant
|
|
12
10
|
}>
|
|
13
11
|
|
|
12
|
+
export type XYBackgroundProps = {
|
|
13
|
+
background: XYBackground
|
|
14
|
+
}
|
|
15
|
+
|
|
14
16
|
function literalToEnum(value: XYBackgroundVariant): BackgroundVariant {
|
|
15
17
|
switch (value) {
|
|
16
18
|
case 'dots':
|
|
@@ -24,10 +26,10 @@ function literalToEnum(value: XYBackgroundVariant): BackgroundVariant {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export
|
|
29
|
+
export function XYFlowBackground({ background }: XYBackgroundProps) {
|
|
28
30
|
if (typeof background === 'string') {
|
|
29
31
|
return <Background variant={literalToEnum(background)} />
|
|
30
32
|
}
|
|
31
33
|
const { variant, ...rest } = background
|
|
32
34
|
return <Background variant={literalToEnum(variant)} {...rest} />
|
|
33
|
-
}
|
|
35
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ReactFlowProps } from '@xyflow/react'
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo, useRef } from 'react'
|
|
3
3
|
import type { Simplify } from 'type-fest'
|
|
4
4
|
import { useDiagramStoreApi } from '../state'
|
|
5
5
|
import { useXYStoreApi } from './hooks'
|
|
6
|
-
import type { XYFlowEdge, XYFlowNode
|
|
6
|
+
import type { XYFlowEdge, XYFlowNode } from './types'
|
|
7
7
|
|
|
8
8
|
type XYFlowEventHandlers = Simplify<
|
|
9
9
|
Required<
|
|
@@ -22,23 +22,17 @@ type XYFlowEventHandlers = Simplify<
|
|
|
22
22
|
| 'onNodeMouseLeave'
|
|
23
23
|
| 'onEdgeMouseEnter'
|
|
24
24
|
| 'onEdgeMouseLeave'
|
|
25
|
-
| 'onViewportChange'
|
|
26
25
|
>
|
|
27
26
|
>
|
|
28
27
|
>
|
|
29
28
|
|
|
30
|
-
const viewportDomNode = (state: XYFlowState) =>
|
|
31
|
-
state.domNode?.querySelector<HTMLDivElement>('.react-flow__viewport') ?? null
|
|
32
|
-
|
|
33
29
|
export function useXYFlowEvents() {
|
|
34
30
|
const diagramApi = useDiagramStoreApi()
|
|
35
31
|
const xyflowApi = useXYStoreApi()
|
|
36
32
|
|
|
37
33
|
const dblclickTimeout = useRef<number>()
|
|
38
34
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const dbclickGuarg = () => {
|
|
35
|
+
const dbclickLock = () => {
|
|
42
36
|
if (dblclickTimeout.current !== undefined) {
|
|
43
37
|
return true
|
|
44
38
|
}
|
|
@@ -48,46 +42,41 @@ export function useXYFlowEvents() {
|
|
|
48
42
|
return false
|
|
49
43
|
}
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
useEffect(() =>
|
|
55
|
-
xyflowApi.subscribe((state, prev) => {
|
|
56
|
-
if (state.domNode !== prev.domNode || (state.domNode && !viewportRef.current)) {
|
|
57
|
-
viewportRef.current = viewportDomNode(state)
|
|
58
|
-
}
|
|
59
|
-
}), [xyflowApi])
|
|
45
|
+
// If we are in focused mode, on edge enter we want to "highlight" the other node
|
|
46
|
+
// This ref contains the id of this node
|
|
47
|
+
const hoveredNodeFromOnEdgeEnterRef = useRef('')
|
|
60
48
|
|
|
61
49
|
return useMemo(() =>
|
|
62
50
|
({
|
|
63
51
|
onDoubleClick: (event) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
52
|
+
const {
|
|
53
|
+
fitViewEnabled,
|
|
54
|
+
onCanvasDblClick,
|
|
55
|
+
resetLastClicked,
|
|
56
|
+
fitDiagram
|
|
57
|
+
} = diagramApi.getState()
|
|
58
|
+
if (fitViewEnabled) {
|
|
59
|
+
fitDiagram(xyflowApi)
|
|
60
|
+
if (!onCanvasDblClick) {
|
|
61
|
+
event.stopPropagation()
|
|
62
|
+
}
|
|
74
63
|
}
|
|
64
|
+
resetLastClicked()
|
|
65
|
+
onCanvasDblClick?.(event)
|
|
75
66
|
},
|
|
76
67
|
onPaneClick: (event) => {
|
|
77
|
-
if (
|
|
68
|
+
if (dbclickLock()) {
|
|
78
69
|
return
|
|
79
70
|
}
|
|
80
71
|
const { focusedNodeId, fitDiagram, onCanvasClick, resetLastClicked } = diagramApi.getState()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
onCanvasClick
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// reset focus if clicked on empty space
|
|
87
|
-
if (focusedNodeId) {
|
|
88
|
-
fitDiagram()
|
|
89
|
-
event.stopPropagation()
|
|
72
|
+
if (!!focusedNodeId) {
|
|
73
|
+
fitDiagram(xyflowApi)
|
|
74
|
+
if (!onCanvasClick) {
|
|
75
|
+
event.stopPropagation()
|
|
76
|
+
}
|
|
90
77
|
}
|
|
78
|
+
resetLastClicked()
|
|
79
|
+
onCanvasClick?.(event)
|
|
91
80
|
},
|
|
92
81
|
onNodeContextMenu: (event, xynode) => {
|
|
93
82
|
diagramApi.getState().setLastClickedNode(xynode.id)
|
|
@@ -110,49 +99,63 @@ export function useXYFlowEvents() {
|
|
|
110
99
|
})
|
|
111
100
|
},
|
|
112
101
|
onNodeClick: (event, xynode) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
onNodeClick
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
// if we clicked on a node that is not focused, fit the diagram
|
|
102
|
+
const {
|
|
103
|
+
focusedNodeId,
|
|
104
|
+
focusOnNode,
|
|
105
|
+
onNodeClick,
|
|
106
|
+
setLastClickedNode
|
|
107
|
+
} = diagramApi.getState()
|
|
108
|
+
setLastClickedNode(xynode.id)
|
|
109
|
+
// if we focused on a node, and clicked on another node - focus on the clicked node
|
|
124
110
|
if (!!focusedNodeId && focusedNodeId !== xynode.id) {
|
|
125
|
-
|
|
126
|
-
|
|
111
|
+
focusOnNode(xynode.id)
|
|
112
|
+
if (!onNodeClick) {
|
|
113
|
+
// user did not provide a custom handler, stop propagation
|
|
114
|
+
event.stopPropagation()
|
|
115
|
+
}
|
|
127
116
|
}
|
|
117
|
+
onNodeClick?.({
|
|
118
|
+
element: xynode.data.element,
|
|
119
|
+
xynode,
|
|
120
|
+
event
|
|
121
|
+
})
|
|
128
122
|
},
|
|
129
123
|
onNodeDoubleClick: (event, xynode) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
124
|
+
const {
|
|
125
|
+
focusedNodeId,
|
|
126
|
+
zoomable,
|
|
127
|
+
fitViewEnabled,
|
|
128
|
+
fitDiagram,
|
|
129
|
+
focusOnNode,
|
|
130
|
+
setLastClickedNode
|
|
131
|
+
} = diagramApi.getState()
|
|
132
|
+
setLastClickedNode(xynode.id)
|
|
133
|
+
if (!!focusedNodeId || (zoomable && fitViewEnabled)) {
|
|
134
|
+
// if we are already focused on the node, cancel
|
|
135
|
+
if (focusedNodeId === xynode.id) {
|
|
136
|
+
fitDiagram(xyflowApi)
|
|
137
|
+
} else {
|
|
138
|
+
focusOnNode(xynode.id)
|
|
139
|
+
}
|
|
137
140
|
event.stopPropagation()
|
|
138
141
|
}
|
|
139
142
|
},
|
|
140
143
|
onEdgeClick: (event, xyedge) => {
|
|
141
144
|
diagramApi.getState().setLastClickedEdge(xyedge.id)
|
|
142
|
-
const { focusedNodeId,
|
|
143
|
-
if
|
|
144
|
-
onEdgeClick({
|
|
145
|
-
relation: xyedge.data.edge,
|
|
146
|
-
xyedge,
|
|
147
|
-
event
|
|
148
|
-
})
|
|
149
|
-
return
|
|
150
|
-
}
|
|
145
|
+
const { focusedNodeId, focusOnNode, onEdgeClick } = diagramApi.getState()
|
|
146
|
+
// if we focused on a node, and clicked on an edge connected to it - focus on the other node
|
|
151
147
|
if (focusedNodeId && (focusedNodeId === xyedge.source || focusedNodeId === xyedge.target)) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
focusOnNode(focusedNodeId === xyedge.source ? xyedge.target : xyedge.source)
|
|
149
|
+
if (!onEdgeClick) {
|
|
150
|
+
// user did not provide a custom handler, stop propagation
|
|
151
|
+
event.stopPropagation()
|
|
152
|
+
}
|
|
155
153
|
}
|
|
154
|
+
onEdgeClick?.({
|
|
155
|
+
relation: xyedge.data.edge,
|
|
156
|
+
xyedge,
|
|
157
|
+
event
|
|
158
|
+
})
|
|
156
159
|
},
|
|
157
160
|
onMoveEnd: (event, _viewport) => {
|
|
158
161
|
// if event is present, the move was triggered by user
|
|
@@ -161,34 +164,36 @@ export function useXYFlowEvents() {
|
|
|
161
164
|
diagramApi.setState({ viewportChanged }, false, `viewport-changed: ${viewportChanged}`)
|
|
162
165
|
}
|
|
163
166
|
},
|
|
164
|
-
onNodeMouseEnter: (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
onNodeMouseEnter: (_event, xynode) => {
|
|
168
|
+
hoveredNodeFromOnEdgeEnterRef.current = ''
|
|
169
|
+
const { hoveredNodeId, setHoveredNode } = diagramApi.getState()
|
|
170
|
+
if (hoveredNodeId !== xynode.id) {
|
|
171
|
+
setHoveredNode(xynode.id)
|
|
172
|
+
}
|
|
169
173
|
},
|
|
170
|
-
|
|
171
|
-
diagramApi.getState()
|
|
174
|
+
onNodeMouseLeave: (_event, xynode) => {
|
|
175
|
+
const { hoveredNodeId, setHoveredNode } = diagramApi.getState()
|
|
176
|
+
if (hoveredNodeId === xynode.id) {
|
|
177
|
+
setHoveredNode(null)
|
|
178
|
+
}
|
|
172
179
|
},
|
|
173
|
-
|
|
174
|
-
diagramApi.getState()
|
|
180
|
+
onEdgeMouseEnter: (_event, { id, source, target }) => {
|
|
181
|
+
const { hoveredNodeId, focusedNodeId, setHoveredEdge, setHoveredNode } = diagramApi.getState()
|
|
182
|
+
setHoveredEdge(id)
|
|
183
|
+
if ((focusedNodeId === source || focusedNodeId === target) && focusedNodeId !== hoveredNodeId) {
|
|
184
|
+
const next = hoveredNodeFromOnEdgeEnterRef.current = source === focusedNodeId ? target : source
|
|
185
|
+
setHoveredNode(next)
|
|
186
|
+
}
|
|
175
187
|
},
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
cbRef.current = undefined
|
|
186
|
-
if (viewportRef.current && viewportRef.current.style.transform !== transformRef.current) {
|
|
187
|
-
viewportRef.current.style.transform = transformRef.current
|
|
188
|
-
}
|
|
189
|
-
}, {
|
|
190
|
-
timeout: 1000
|
|
191
|
-
})
|
|
188
|
+
onEdgeMouseLeave: (_event, xyedge) => {
|
|
189
|
+
const { hoveredEdgeId, setHoveredEdge, hoveredNodeId, setHoveredNode } = diagramApi.getState()
|
|
190
|
+
if (hoveredEdgeId === xyedge.id) {
|
|
191
|
+
setHoveredEdge(null)
|
|
192
|
+
}
|
|
193
|
+
if (hoveredNodeId === hoveredNodeFromOnEdgeEnterRef.current) {
|
|
194
|
+
hoveredNodeFromOnEdgeEnterRef.current = ''
|
|
195
|
+
setHoveredNode(null)
|
|
196
|
+
}
|
|
192
197
|
}
|
|
193
198
|
}) satisfies XYFlowEventHandlers, [diagramApi, xyflowApi])
|
|
194
199
|
}
|