@stonecrop/node-editor 0.30.0 → 0.32.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.
Files changed (34) hide show
  1. package/dist/assets/index.css +430 -0
  2. package/dist/node-editor.js +821 -642
  3. package/dist/node-editor.js.map +1 -1
  4. package/dist/src/components/EditableEdge.vue.d.ts +11 -0
  5. package/dist/src/components/EditableEdge.vue.d.ts.map +1 -0
  6. package/dist/src/components/EditableNode.vue.d.ts +10 -0
  7. package/dist/src/components/EditableNode.vue.d.ts.map +1 -0
  8. package/dist/src/components/NodeEditor.vue.d.ts +14 -0
  9. package/dist/src/components/NodeEditor.vue.d.ts.map +1 -0
  10. package/dist/src/components/SelfLoopEdge.vue.d.ts +11 -0
  11. package/dist/src/components/SelfLoopEdge.vue.d.ts.map +1 -0
  12. package/dist/src/components/StateEditor.vue.d.ts +69 -0
  13. package/dist/src/components/StateEditor.vue.d.ts.map +1 -0
  14. package/dist/src/index.d.ts +1 -0
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/{tsdoc-metadata.json → src/tsdoc-metadata.json} +1 -1
  17. package/package.json +35 -28
  18. package/dist/node-editor.css +0 -1
  19. package/dist/node-editor.d.ts +0 -58
  20. package/dist/node_editor.tsbuildinfo +0 -1
  21. package/dist/src/index.js +0 -12
  22. package/dist/src/types/index.js +0 -0
  23. package/dist/src/utils/autoLayout.js +0 -50
  24. package/dist/src/utils/stateTransforms.js +0 -148
  25. package/src/components/EditableEdge.vue +0 -92
  26. package/src/components/EditableNode.vue +0 -65
  27. package/src/components/NodeEditor.vue +0 -397
  28. package/src/components/SelfLoopEdge.vue +0 -88
  29. package/src/components/StateEditor.vue +0 -46
  30. package/src/index.ts +0 -17
  31. package/src/shims-vue.d.ts +0 -5
  32. package/src/types/index.ts +0 -33
  33. package/src/utils/autoLayout.ts +0 -67
  34. package/src/utils/stateTransforms.ts +0 -164
@@ -1,397 +0,0 @@
1
- <template>
2
- <div
3
- class="node-editor-wrapper"
4
- :class="nodeContainerClass"
5
- @contextmenu.prevent
6
- @mouseover="hover = true"
7
- @mouseleave="hover = false">
8
- <div class="chart-controls">
9
- <div class="chart-controls-left">
10
- <div><b>Selected Node:</b> {{ activeElementKey ? activeElementKey : 'none' }}</div>
11
- </div>
12
- <div class="chart-controls-right">
13
- <div>
14
- <button class="button-default" @click="addNode">Add Node</button>
15
- </div>
16
- <div>
17
- <button class="button-default" @click="fitView">Center</button>
18
- </div>
19
- <div>
20
- <button class="button-default" @click="autoArrange">Auto-arrange</button>
21
- </div>
22
- <div v-if="activeElementIndex > -1">
23
- <button class="button-default" @click="shiftInput">Shift Input Position</button>
24
- </div>
25
- <div v-if="activeElementIndex > -1">
26
- <button class="button-default" @click="shiftOutput">Shift Output Position</button>
27
- </div>
28
- </div>
29
- </div>
30
-
31
- <VueFlow
32
- v-if="vueFlowElements && vueFlowElements.length"
33
- v-model="vueFlowElements"
34
- class="nowheel"
35
- :prevent-scrolling="true"
36
- :zoom-on-scroll="false"
37
- :fit-view-on-init="true"
38
- :pan-activation-key-code="null"
39
- @connect="handleConnect"
40
- @pane-ready="setInstance"
41
- @node-click="handleNodeClick"
42
- @edge-click="handleEdgeClick"
43
- @edge-context-menu="handleEdgeContextMenu"
44
- @node-drag-stop="emitElements"
45
- @wheel.prevent="onWheel">
46
- <template #node-editable="props">
47
- <EditableNode v-bind="props" @change="labelChanged($event, props.id)" />
48
- </template>
49
- <template #edge-editable="props">
50
- <EditableEdge v-bind="props" @change="labelChanged($event, props.id)" @remove="removeEdge(props.id)" />
51
- </template>
52
- <template #edge-selfloop="props">
53
- <SelfLoopEdge v-bind="props" @change="labelChanged($event, props.id)" @remove="removeEdge(props.id)" />
54
- </template>
55
- </VueFlow>
56
- </div>
57
- </template>
58
-
59
- <script setup lang="ts">
60
- import {
61
- type VueFlowStore,
62
- Position,
63
- VueFlow,
64
- useVueFlow,
65
- Connection,
66
- Node,
67
- NodeMouseEvent,
68
- EdgeMouseEvent,
69
- DefaultEdge,
70
- } from '@vue-flow/core'
71
- import { type HTMLAttributes, ref, computed, nextTick, onBeforeUnmount, onMounted } from 'vue'
72
-
73
- import EditableEdge from './EditableEdge.vue'
74
- import EditableNode from './EditableNode.vue'
75
- import SelfLoopEdge from './SelfLoopEdge.vue'
76
- import { autoLayout } from '../utils/autoLayout'
77
- import type { FlowElements } from '../types'
78
-
79
- const { modelValue, nodeContainerClass = '' } = defineProps<{
80
- modelValue: FlowElements
81
- nodeContainerClass?: HTMLAttributes['class']
82
- }>()
83
- const emit = defineEmits(['update:modelValue'])
84
-
85
- const hover = ref(false)
86
- const vueFlowElements = ref<FlowElements>([])
87
- const vueFlowInstance = ref<VueFlowStore>()
88
-
89
- const activeElementKey = ref('')
90
- const activeElementIndex = computed(() =>
91
- vueFlowElements.value.findIndex(element => element.id === activeElementKey.value)
92
- )
93
-
94
- const elements = computed({
95
- get: () => {
96
- const items = modelValue
97
-
98
- // Add rendering flags without clobbering caller-provided data. Edges carry `data.actionKey`
99
- // (the stable action identity from stateTransforms); wiping data here would drop it before it
100
- // round-trips back through emitElements, silently re-keying actions on every graph edit.
101
- for (const element of items) {
102
- element.data = { ...element.data }
103
- if (element.type === 'input') {
104
- element.data.hasInput = false
105
- element.data.hasOutput = true
106
- } else if (element.type === 'output') {
107
- element.data.hasInput = true
108
- element.data.hasOutput = false
109
- } else {
110
- element.data.hasInput = true
111
- element.data.hasOutput = true
112
- }
113
- element.class = 'vue-flow__node-default'
114
- // A self-loop edge (source === target) routes to the SelfLoopEdge arc renderer; everything
115
- // else (nodes and cross-state edges) uses the default editable slot. getBezierPath draws a
116
- // useless near-straight segment for a self-loop, so it must not fall through to 'editable'.
117
- element.type = 'source' in element && element.source === element.target ? 'selfloop' : 'editable'
118
- }
119
-
120
- return items
121
- },
122
- set: newValue => {
123
- emit('update:modelValue', JSON.parse(JSON.stringify(newValue)))
124
- },
125
- })
126
- const { addEdges, removeEdges } = useVueFlow()
127
-
128
- onMounted(() => {
129
- document.removeEventListener('keypress', handleKeypress)
130
- document.addEventListener('keypress', handleKeypress)
131
- })
132
-
133
- onBeforeUnmount(() => {
134
- document.removeEventListener('keypress', handleKeypress)
135
- })
136
-
137
- const setInstance = (instance: VueFlowStore) => {
138
- vueFlowInstance.value = instance
139
- }
140
-
141
- vueFlowElements.value = elements.value
142
-
143
- // Methods
144
- const shiftTerminal = (currentTerminal: Position) => {
145
- return {
146
- [Position.Top]: Position.Right,
147
- [Position.Right]: Position.Bottom,
148
- [Position.Bottom]: Position.Left,
149
- [Position.Left]: Position.Top,
150
- }[currentTerminal]
151
- }
152
-
153
- const shiftOutput = () => {
154
- if (activeElementIndex.value > -1) {
155
- const activeNode = vueFlowElements.value[activeElementIndex.value] as Node
156
- if (!activeNode.sourcePosition) return
157
- activeNode.sourcePosition = shiftTerminal(activeNode.sourcePosition)
158
- emitElements()
159
- }
160
- }
161
-
162
- const shiftInput = () => {
163
- if (activeElementIndex.value > -1) {
164
- const activeNode = vueFlowElements.value[activeElementIndex.value] as Node
165
- if (!activeNode.targetPosition) return
166
- activeNode.targetPosition = shiftTerminal(activeNode.targetPosition)
167
- emitElements()
168
- }
169
- }
170
-
171
- const onWheel = (event: WheelEvent) => {
172
- window.scrollBy(0, event.deltaY)
173
- }
174
-
175
- const handleKeypress = (event: KeyboardEvent) => {
176
- if (hover.value && event.ctrlKey == true) {
177
- if (event.key == '+' || event.key == '=') {
178
- void vueFlowInstance.value?.zoomIn()
179
- }
180
- if (event.key == '-') {
181
- void vueFlowInstance.value?.zoomOut()
182
- }
183
- }
184
- }
185
-
186
- const fitView = async () => {
187
- await vueFlowInstance.value?.fitView()
188
- }
189
-
190
- const autoArrange = async () => {
191
- // Read measured node sizes from the VueFlow store so the layout fits actual box widths (state
192
- // labels differ in length); autoLayout falls back to default dimensions for any unmeasured node.
193
- const store = vueFlowInstance.value
194
- const dimensions: Record<string, { width: number; height: number }> = {}
195
- for (const el of vueFlowElements.value) {
196
- if ('source' in el) continue
197
- const dims = store?.findNode?.(el.id)?.dimensions
198
- if (dims?.width && dims?.height) dimensions[el.id] = { width: dims.width, height: dims.height }
199
- }
200
-
201
- const laid = autoLayout(vueFlowElements.value, { dimensions })
202
- const positionById = new Map<string, { x: number; y: number }>()
203
- for (const el of laid) {
204
- if ('source' in el) continue
205
- positionById.set(el.id, el.position)
206
- }
207
- // Apply positions onto the v-model elements — VueFlow reacts to these, same as shiftInput/Output.
208
- for (const el of vueFlowElements.value) {
209
- if ('source' in el) continue
210
- const position = positionById.get(el.id)
211
- if (position) el.position = position
212
- }
213
-
214
- emitElements()
215
- await nextTick()
216
- await fitView()
217
- }
218
-
219
- const addNode = () => {
220
- let makeEdge = false
221
- let newNodePosition = { x: Math.random() * 200, y: Math.random() * 200 }
222
- if (activeElementIndex.value > -1) {
223
- const activeNode = vueFlowElements.value[activeElementIndex.value]
224
- if (activeNode.data?.hasOutput) {
225
- newNodePosition = { x: (activeNode as Node).position.x + 200, y: (activeNode as Node).position.y + 50 }
226
- makeEdge = true
227
- }
228
- }
229
-
230
- const id = vueFlowElements.value.length
231
- const nodeId = `node-${id}`
232
- vueFlowElements.value.push({
233
- id: nodeId,
234
- label: 'Node ' + id,
235
- sourcePosition: Position.Right,
236
- targetPosition: Position.Left,
237
- class: 'vue-flow__node-default',
238
- type: 'editable',
239
- data: {
240
- hasInput: true,
241
- hasOutput: true,
242
- },
243
- position: newNodePosition,
244
- })
245
-
246
- if (makeEdge) {
247
- const edgeId = `edge-${id + 1}`
248
- vueFlowElements.value.push({
249
- id: edgeId,
250
- source: activeElementKey.value,
251
- target: nodeId,
252
- type: 'editable',
253
- label: `EDGE ${id + 1}`,
254
- animated: true,
255
- })
256
- }
257
- emitElements()
258
- }
259
-
260
- const labelChanged = (event: DefaultEdge['label'], id: DefaultEdge['id']) => {
261
- for (let j = 0; j < vueFlowElements.value.length; j++) {
262
- if (vueFlowElements.value[j].id == id) {
263
- vueFlowElements.value[j].label = event
264
- break
265
- }
266
- }
267
- emitElements()
268
- }
269
-
270
- const handleNodeClick = ({ node }: NodeMouseEvent) => {
271
- activeElementKey.value = node.id
272
- }
273
-
274
- const handleEdgeClick = ({ edge }: EdgeMouseEvent) => {
275
- activeElementKey.value = edge.id
276
- }
277
-
278
- const handleConnect = async (event: Connection) => {
279
- const id = vueFlowElements.value.length
280
- // A node connected to itself is a self-transition (mutate-in-place) — render it as a loop.
281
- const isSelfLoop = event.source === event.target
282
- const newEdge = {
283
- id: `edge-${id}`,
284
- source: event.source,
285
- target: event.target,
286
- type: isSelfLoop ? 'selfloop' : 'editable',
287
- label: `New Edge`,
288
- interactionWidth: 400,
289
- animated: true,
290
- }
291
- addEdges([newEdge])
292
- await nextTick()
293
- emitElements()
294
- }
295
-
296
- const handleEdgeContextMenu = async (event: EdgeMouseEvent) => {
297
- removeEdges([event.edge.id])
298
- await nextTick()
299
- emitElements()
300
- }
301
-
302
- const removeEdge = async (id: string) => {
303
- removeEdges([id])
304
- await nextTick()
305
- emitElements()
306
- }
307
-
308
- const emitElements = () => {
309
- emit('update:modelValue', JSON.parse(JSON.stringify(vueFlowElements.value)))
310
- }
311
- </script>
312
-
313
- <style>
314
- @import '@vue-flow/core/dist/style.css';
315
- @import '@vue-flow/core/dist/theme-default.css';
316
-
317
- .chart-controls-left,
318
- .chart-controls-right {
319
- height: 1.8em;
320
- display: flex;
321
- flex-direction: row;
322
- align-items: center;
323
- padding-top: 0.2em;
324
- }
325
-
326
- .chart-controls-right div {
327
- margin-left: 5px;
328
- }
329
-
330
- .chart-controls {
331
- position: absolute;
332
- bottom: 0.5rem;
333
- left: 0.5rem;
334
- z-index: 10;
335
- display: flex;
336
- flex-direction: column;
337
- align-items: flex-start;
338
- gap: 0.35rem;
339
- padding: 0.4rem 0.5rem;
340
- background: rgba(255, 255, 255, 0.92);
341
- border: 1px solid #ccc;
342
- border-radius: 4px;
343
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
344
- }
345
-
346
- .chart-controls div {
347
- margin-bottom: 0;
348
- }
349
-
350
- .defaultContainerClass {
351
- height: 90vh;
352
- width: 100%;
353
- border: 1px solid #ccc;
354
- }
355
-
356
- .default-input-node.vue-flow__node-input,
357
- .default-output-node.vue-flow__node-output {
358
- border-color: #000;
359
- }
360
-
361
- .default-input-node.vue-flow__node-input .vue-flow__handle,
362
- .default-output-node.vue-flow__node-output .vue-flow__handle {
363
- background-color: #000;
364
- }
365
-
366
- .default-input-node.vue-flow__node-input.selected,
367
- .default-output-node.vue-flow__node-output.selected {
368
- box-shadow: 0 0 0 0.5px #000;
369
- }
370
-
371
- button.button-default {
372
- background-color: #ffffff;
373
- padding: 1px 12px;
374
- border-radius: 3px;
375
- border: 1px solid #ccc;
376
- cursor: pointer;
377
- white-space: nowrap;
378
- }
379
-
380
- button.button-default:hover {
381
- background-color: #f2f2f2;
382
- }
383
-
384
- .vue-flow {
385
- background-size: 40px 40px;
386
- background-image:
387
- linear-gradient(to right, #ccc 1px, transparent 1px), linear-gradient(to bottom, #ccc 1px, transparent 1px);
388
- }
389
-
390
- input.label-editor {
391
- position: absolute;
392
- }
393
-
394
- .node-editor-wrapper {
395
- position: relative;
396
- }
397
- </style>
@@ -1,88 +0,0 @@
1
- <template>
2
- <!-- transparent ghost path gives a ~20px click zone around the 2px visible stroke -->
3
- <path :d="path.d" fill="none" stroke="transparent" stroke-width="20" class="vue-flow__edge-interaction" />
4
- <path :id="id" :style="style" class="vue-flow__edge-path" :d="path.d" :marker-end="markerEnd" />
5
-
6
- <EdgeLabelRenderer>
7
- <div
8
- :style="{
9
- pointerEvents: 'all',
10
- position: 'absolute',
11
- transform: `translate(-50%, -50%) translate(${path.labelX}px,${path.labelY}px)`,
12
- }"
13
- class="nodrag nopan editable-edge-label"
14
- @click="labelOnClick()"
15
- @contextmenu.prevent="$emit('remove', id)">
16
- <div class="vue-flow__edge-label">{{ label }}</div>
17
- <div v-if="showInput" class="label-input-wrapper">
18
- <input
19
- ref="labelInput"
20
- v-model="newLabel"
21
- class="label-input"
22
- @blur="showInput = false"
23
- @keypress.enter="submitNewLabel" />
24
- </div>
25
- </div>
26
- </EdgeLabelRenderer>
27
- </template>
28
-
29
- <script setup lang="ts">
30
- import { type EdgeProps, EdgeLabelRenderer } from '@vue-flow/core'
31
- import { computed, ref, nextTick, useTemplateRef } from 'vue'
32
-
33
- const props = defineProps<EdgeProps>()
34
- const emit = defineEmits(['change', 'remove'])
35
-
36
- // A self-loop connects a node's source handle to its own target handle. getBezierPath draws a nearly
37
- // straight segment across the node for that (the two handles are on the same box), so it's useless
38
- // here — we hand-author an arc that leaves the source handle, loops up and over the node top, and
39
- // returns to the target handle. LOOP_HEIGHT is how far above the handles the arc bulges.
40
- const LOOP_HEIGHT = 90
41
- const LOOP_SPREAD = 45
42
-
43
- const path = computed(() => {
44
- const { sourceX, sourceY, targetX, targetY } = props
45
- const d = `M ${sourceX},${sourceY} C ${sourceX + LOOP_SPREAD},${sourceY - LOOP_HEIGHT} ${
46
- targetX - LOOP_SPREAD
47
- },${targetY - LOOP_HEIGHT} ${targetX},${targetY}`
48
- // Place the label near the arc's apex (a cubic bezier peaks around 0.75 of the control offset).
49
- const labelX = (sourceX + targetX) / 2
50
- const labelY = Math.min(sourceY, targetY) - LOOP_HEIGHT * 0.72
51
- return { d, labelX, labelY }
52
- })
53
-
54
- const inputRef = useTemplateRef<HTMLInputElement>('labelInput')
55
- const newLabel = ref<EdgeProps['label']>('')
56
- const showInput = ref(false)
57
- let lastClick = 0
58
-
59
- const labelOnClick = async () => {
60
- let now = Date.now()
61
- if (now - lastClick < 500 && !showInput.value) {
62
- await showLabelInput()
63
- }
64
- lastClick = now
65
- }
66
-
67
- const showLabelInput = async () => {
68
- newLabel.value = props.label
69
- showInput.value = true
70
- await nextTick()
71
- inputRef.value?.focus()
72
- }
73
-
74
- const submitNewLabel = () => {
75
- showInput.value = false
76
- emit('change', newLabel.value)
77
- }
78
- </script>
79
-
80
- <script lang="ts">
81
- export default {
82
- inheritAttrs: false,
83
- }
84
- </script>
85
-
86
- <style>
87
- /* Label + input styles are shared with EditableEdge (defined there, global scope). */
88
- </style>
@@ -1,46 +0,0 @@
1
- <template>
2
- <div>
3
- <NodeEditor v-model="elements" :node-container-class="nodeContainerClass" />
4
- </div>
5
- </template>
6
-
7
- <script setup lang="ts">
8
- import { type HTMLAttributes, computed, onMounted } from 'vue'
9
- import type { WorkflowMeta } from '@stonecrop/schema'
10
-
11
- import NodeEditor from './NodeEditor.vue'
12
- import type { FlowElements, Layout } from '../types'
13
- import { autoLayout } from '../utils/autoLayout'
14
- import { statesToFlowElements, flowElementsToStates } from '../utils/stateTransforms'
15
-
16
- const workflow = defineModel<WorkflowMeta>()
17
- const layout = defineModel<Layout>('layout')
18
- const { nodeContainerClass = '' } = defineProps<{
19
- nodeContainerClass?: HTMLAttributes['class']
20
- }>()
21
-
22
- onMounted(() => {
23
- if (layout.value === undefined) {
24
- console.warn('[StateEditor] v-model:layout is not bound. Node position changes will not be persisted.')
25
- }
26
- })
27
-
28
- const elements = computed<FlowElements>({
29
- get: () => {
30
- if (!workflow.value) return []
31
- const els = statesToFlowElements(workflow.value, layout.value)
32
- // Seed an un-arranged workflow with an auto-computed dagre layout instead of the naive row.
33
- // Ephemeral: these positions are persisted only once the author drags a node (which routes
34
- // through flowElementsToStates → layout). A workflow with any saved layout keeps it as-is.
35
- const noSavedLayout = !layout.value || Object.keys(layout.value).length === 0
36
- return noSavedLayout ? autoLayout(els) : els
37
- },
38
- set: newValue => {
39
- const { workflow: nextWorkflow, layout: nextLayout } = flowElementsToStates(newValue, workflow.value)
40
- workflow.value = nextWorkflow
41
- if (layout.value !== undefined) {
42
- layout.value = nextLayout
43
- }
44
- },
45
- })
46
- </script>
package/src/index.ts DELETED
@@ -1,17 +0,0 @@
1
- import { App } from 'vue'
2
-
3
- import NodeEditor from './components/NodeEditor.vue'
4
- import StateEditor from './components/StateEditor.vue'
5
- export type * from './types'
6
-
7
- /**
8
- * Install all Node Editor components
9
- * @param app - Vue app instance
10
- * @public
11
- */
12
- function install(app: App) {
13
- app.component('NodeEditor', NodeEditor)
14
- app.component('StateEditor', StateEditor)
15
- }
16
-
17
- export { install, NodeEditor, StateEditor }
@@ -1,5 +0,0 @@
1
- declare module '*.vue' {
2
- import { ComponentOptions } from 'vue'
3
- const Component: ComponentOptions
4
- export default Component
5
- }
@@ -1,33 +0,0 @@
1
- import { type Elements, type Element, type XYPosition, Position } from '@vue-flow/core'
2
-
3
- /**
4
- * Flow elements
5
- * @public
6
- */
7
- export type FlowElements = Elements<
8
- { hasInput?: boolean; hasOutput?: boolean },
9
- // `actionKey` is the stable action identity carried on an edge, decoupled from its visible
10
- // `label` (the display name). It lets an edge be relabeled without re-keying the action.
11
- { hasInput?: boolean; hasOutput?: boolean; actionKey?: string }
12
- >
13
-
14
- /**
15
- * Flow element
16
- * @public
17
- */
18
- export type FlowElement = Element<
19
- { hasInput?: boolean; hasOutput?: boolean },
20
- { hasInput?: boolean; hasOutput?: boolean; actionKey?: string }
21
- >
22
-
23
- /**
24
- * Node layout
25
- * @public
26
- */
27
- export type Layout = {
28
- [key: string]: {
29
- position?: XYPosition
30
- targetPosition?: Position
31
- sourcePosition?: Position
32
- }
33
- }
@@ -1,67 +0,0 @@
1
- import { Graph, layout } from '@dagrejs/dagre'
2
-
3
- import type { FlowElements } from '../types'
4
-
5
- /**
6
- * Options for {@link autoLayout}.
7
- * @public
8
- */
9
- export interface AutoLayoutOptions {
10
- /** Layout direction. Defaults to `'LR'` to match the editor's Left/Right handle defaults. */
11
- rankdir?: 'LR' | 'RL' | 'TB' | 'BT'
12
- /** Fallback node width when no measured size is supplied (the seed path has no DOM to measure). */
13
- nodeWidth?: number
14
- /** Fallback node height when no measured size is supplied. */
15
- nodeHeight?: number
16
- /** Measured per-node sizes (node id → box) — supplied by the on-demand "Auto-arrange" button. */
17
- dimensions?: Record<string, { width: number; height: number }>
18
- }
19
-
20
- /**
21
- * Compute node positions for a workflow graph with dagre's layered (Sugiyama) algorithm.
22
- *
23
- * Pure and synchronous: takes VueFlow elements (nodes + edges) and returns a new elements array with
24
- * each node's `position` replaced by a laid-out coordinate; edges and every other node field are
25
- * passed through untouched. Used two ways — as the seed for an un-arranged workflow (fixed default
26
- * dimensions, no DOM) and behind the toolbar button (measured `dimensions`).
27
- *
28
- * Self-loop edges (`source === target`) are skipped: dagre doesn't rank them, and they're reserved
29
- * for the separate mutate-in-place self-transition feature.
30
- * @public
31
- */
32
- export function autoLayout(elements: FlowElements, options: AutoLayoutOptions = {}): FlowElements {
33
- const { rankdir = 'LR', nodeWidth = 160, nodeHeight = 40, dimensions } = options
34
- const sizeOf = (id: string) => dimensions?.[id] ?? { width: nodeWidth, height: nodeHeight }
35
-
36
- // Nodes are the elements without a `source` (the same discriminator flowElementsToStates uses).
37
- const nodeIds: string[] = []
38
- for (const el of elements) {
39
- if (!('source' in el)) nodeIds.push(el.id)
40
- }
41
- if (nodeIds.length === 0) return [...elements]
42
-
43
- const graph = new Graph()
44
- graph.setGraph({ rankdir, nodesep: 40, ranksep: 80, edgesep: 20 })
45
- graph.setDefaultEdgeLabel(() => ({}))
46
-
47
- for (const id of nodeIds) {
48
- const { width, height } = sizeOf(id)
49
- graph.setNode(id, { width, height })
50
- }
51
- for (const el of elements) {
52
- if (!('source' in el)) continue
53
- if (el.source === el.target) continue
54
- graph.setEdge(el.source, el.target)
55
- }
56
-
57
- layout(graph)
58
-
59
- // dagre reports each node by its CENTER; VueFlow positions nodes by their TOP-LEFT corner.
60
- return elements.map(el => {
61
- if ('source' in el) return el
62
- const center = graph.node(el.id)
63
- if (!center) return el
64
- const { width, height } = sizeOf(el.id)
65
- return { ...el, position: { x: center.x - width / 2, y: center.y - height / 2 } }
66
- })
67
- }