w-flow-vue 1.0.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 (144) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/AGENT.md +223 -0
  6. package/LICENSE +21 -0
  7. package/README.md +37 -0
  8. package/SECURITY.md +5 -0
  9. package/babel.config.js +16 -0
  10. package/dist/w-flow-vue.umd.js +15 -0
  11. package/dist/w-flow-vue.umd.js.map +1 -0
  12. package/docs/components_WFlowVue.vue.html +1214 -0
  13. package/docs/examples/app.html +62 -0
  14. package/docs/examples/app.umd.js +20 -0
  15. package/docs/examples/app.umd.js.map +1 -0
  16. package/docs/examples/ex-AppBasic.html +440 -0
  17. package/docs/examples/ex-AppConnectivity.html +131 -0
  18. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  19. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  20. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  21. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  22. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  24. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  25. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  26. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  27. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  28. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  29. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  35. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  36. package/docs/global.html +1919 -0
  37. package/docs/index.html +84 -0
  38. package/docs/js_defaults.mjs.html +105 -0
  39. package/docs/js_edge-path.mjs.html +237 -0
  40. package/docs/js_geometry.mjs.html +298 -0
  41. package/docs/js_graph.mjs.html +103 -0
  42. package/docs/js_step-routing.mjs.html +346 -0
  43. package/docs/module-WFlowVue.html +2790 -0
  44. package/docs/scripts/collapse.js +39 -0
  45. package/docs/scripts/commonNav.js +28 -0
  46. package/docs/scripts/linenumber.js +25 -0
  47. package/docs/scripts/nav.js +12 -0
  48. package/docs/scripts/polyfill.js +4 -0
  49. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  50. package/docs/scripts/prettify/lang-css.js +2 -0
  51. package/docs/scripts/prettify/prettify.js +28 -0
  52. package/docs/scripts/search.js +99 -0
  53. package/docs/styles/jsdoc.css +776 -0
  54. package/docs/styles/prettify.css +80 -0
  55. package/jest.config.js +20 -0
  56. package/package.json +80 -0
  57. package/public/index.html +38 -0
  58. package/script.txt +22 -0
  59. package/src/App.vue +326 -0
  60. package/src/AppBasic.vue +125 -0
  61. package/src/AppConnectivity.vue +186 -0
  62. package/src/components/WFlowVue.vue +1142 -0
  63. package/src/components/canvas/BackgroundLayer.vue +78 -0
  64. package/src/components/canvas/FlowCanvas.vue +64 -0
  65. package/src/components/canvas/SelectionBox.vue +36 -0
  66. package/src/components/canvas/ViewportTransform.vue +35 -0
  67. package/src/components/edges/ConnectionLine.vue +65 -0
  68. package/src/components/edges/EdgeMarkerDefs.vue +76 -0
  69. package/src/components/edges/EdgeRenderer.vue +120 -0
  70. package/src/components/edges/EdgeWrapper.vue +379 -0
  71. package/src/components/nodes/DefaultNode.vue +276 -0
  72. package/src/components/nodes/Handle.vue +101 -0
  73. package/src/components/nodes/InputNode.vue +47 -0
  74. package/src/components/nodes/NodeBody.vue +103 -0
  75. package/src/components/nodes/NodeFace.vue +128 -0
  76. package/src/components/nodes/NodeRenderer.vue +95 -0
  77. package/src/components/nodes/NodeWrapper.vue +475 -0
  78. package/src/components/nodes/OutputNode.vue +47 -0
  79. package/src/components/ui/ConnSettingsForm.vue +158 -0
  80. package/src/components/ui/Controls.vue +83 -0
  81. package/src/components/ui/NodeSettingsForm.vue +185 -0
  82. package/src/js/defaults.mjs +33 -0
  83. package/src/js/edge-path.mjs +165 -0
  84. package/src/js/geometry.mjs +226 -0
  85. package/src/js/graph.mjs +31 -0
  86. package/src/js/step-routing.mjs +274 -0
  87. package/src/main.js +22 -0
  88. package/test/WFlowVue-features.test.mjs +760 -0
  89. package/test/WFlowVue.test.mjs +421 -0
  90. package/test/components-canvas.test.mjs +102 -0
  91. package/test/components-edge.test.mjs +147 -0
  92. package/test/components-node.test.mjs +174 -0
  93. package/test/components-ui.test.mjs +69 -0
  94. package/test/defaults.test.mjs +86 -0
  95. package/test/edge-path.test.mjs +102 -0
  96. package/test/generate-routing-snapshots.mjs +77 -0
  97. package/test/generate-visual-baselines.mjs +206 -0
  98. package/test/geometry.test.mjs +236 -0
  99. package/test/graph.test.mjs +72 -0
  100. package/test/jsons/routing-snapshots.json +24994 -0
  101. package/test/pics/_check2.png +0 -0
  102. package/test/pics/_check3.png +0 -0
  103. package/test/pics/_check4.png +0 -0
  104. package/test/pics/_check5.png +0 -0
  105. package/test/pics/_v1.png +0 -0
  106. package/test/pics/_v2.png +0 -0
  107. package/test/pics/_v3.png +0 -0
  108. package/test/pics/_v4.png +0 -0
  109. package/test/pics/_v5.png +0 -0
  110. package/test/pics/_v6.png +0 -0
  111. package/test/pics/_v7.png +0 -0
  112. package/test/pics/vb-edge-hovered.png +0 -0
  113. package/test/pics/vb-edges-normal.png +0 -0
  114. package/test/pics/vb-locked-edge-hovered.png +0 -0
  115. package/test/pics/vb-locked-node-hovered.png +0 -0
  116. package/test/pics/vb-locked-node-selected.png +0 -0
  117. package/test/pics/vb-locked-overview.png +0 -0
  118. package/test/pics/vb-node-1.png +0 -0
  119. package/test/pics/vb-node-10.png +0 -0
  120. package/test/pics/vb-node-11.png +0 -0
  121. package/test/pics/vb-node-12.png +0 -0
  122. package/test/pics/vb-node-2.png +0 -0
  123. package/test/pics/vb-node-3.png +0 -0
  124. package/test/pics/vb-node-4.png +0 -0
  125. package/test/pics/vb-node-5.png +0 -0
  126. package/test/pics/vb-node-6.png +0 -0
  127. package/test/pics/vb-node-7.png +0 -0
  128. package/test/pics/vb-node-8.png +0 -0
  129. package/test/pics/vb-node-9.png +0 -0
  130. package/test/pics/vb-node-hovered.png +0 -0
  131. package/test/pics/vb-node-selected.png +0 -0
  132. package/test/pics/vb-overview.png +0 -0
  133. package/test/step-routing-connectivity.test.mjs +78 -0
  134. package/test/step-routing.test.mjs +88 -0
  135. package/test/visual-regression.test.mjs +274 -0
  136. package/toolg/addVersion.mjs +4 -0
  137. package/toolg/cleanFolder.mjs +4 -0
  138. package/toolg/gDistApp.mjs +34 -0
  139. package/toolg/gDistRollupComps.mjs +22 -0
  140. package/toolg/gDocExams.mjs +47 -0
  141. package/toolg/gExtractHtml.mjs +179 -0
  142. package/toolg/modifyReadme.mjs +4 -0
  143. package/vue.config.js +9 -0
  144. package/vue2/344/271/213foreignObject/345/205/247/346/270/262/346/237/223/345/225/217/351/241/214/350/210/207/344/277/256/346/255/243.md +151 -0
@@ -0,0 +1,1142 @@
1
+ <template>
2
+ <div :style="`width:${widthInp}px; height:${heightInp}px;`">
3
+ <FlowCanvas
4
+ v-if="inited"
5
+ ref="canvas"
6
+ @canvas-mousedown="onCanvasMouseDown"
7
+ @canvas-mousemove="onCanvasMouseMove"
8
+ @canvas-mouseup="onCanvasMouseUp"
9
+ @canvas-wheel="onCanvasWheel"
10
+ @canvas-dblclick="onCanvasDblClick"
11
+ @canvas-click="onCanvasClick"
12
+ @canvas-contextmenu="onCanvasContextMenu"
13
+ >
14
+ <BackgroundLayer
15
+ :variant="platformBackgroundPatternType"
16
+ :gap="platformBackgroundPatternGap"
17
+ :size="platformBackgroundPatternSize"
18
+ :pattern-color="platformBackgroundPatternColor"
19
+ :bg-color="platformBackgroundColor"
20
+ :viewport-x="viewport.x"
21
+ :viewport-y="viewport.y"
22
+ :viewport-zoom="viewport.zoom"
23
+ />
24
+
25
+ <ViewportTransform
26
+ :x="viewport.x"
27
+ :y="viewport.y"
28
+ :zoom="viewport.zoom"
29
+ >
30
+ <EdgeRenderer
31
+ :conns="conns"
32
+ :nodes="renderNodes"
33
+ :node-internals="nodeInternals"
34
+ :selected-conn-ids="selectedConns"
35
+ :interactive="elementsSelectable"
36
+ :locked="locked"
37
+ :settings-popup-background-color="settingsPopupBackgroundColor"
38
+ :settings-popup-text-color="settingsPopupTextColor"
39
+ :settings-popup-text-font-size="settingsPopupTextFontSize"
40
+ :infor-popup-background-color="inforPopupBackgroundColor"
41
+ :infor-popup-title-text-color="inforPopupTitleTextColor"
42
+ :infor-popup-title-text-font-size="inforPopupTitleTextFontSize"
43
+ :infor-popup-description-text-color="inforPopupDescriptionTextColor"
44
+ :infor-popup-description-text-font-size="inforPopupDescriptionTextFontSize"
45
+ @conn-click="onConnClick"
46
+ @conn-double-click="onConnDoubleClick"
47
+ @conn-context-menu="onConnContextMenu"
48
+ @conn-mouseenter="onConnMouseEnter"
49
+ @conn-mouseleave="onConnMouseLeave"
50
+ @conn-settings-click="onConnSettingsClick"
51
+ @conn-settings-update="onConnSettingsUpdate"
52
+ @conn-settings-delete="onConnSettingsDelete"
53
+ />
54
+
55
+ <NodeRenderer
56
+ :nodes="renderNodes"
57
+ :selected-node-ids="selectedNodes"
58
+ :nodes-draggable="nodesDraggable"
59
+ :nodes-connectable="nodesConnectable"
60
+ :locked="locked"
61
+ :nodes-resizable="nodesResizable"
62
+ :settings-popup-background-color="settingsPopupBackgroundColor"
63
+ :settings-popup-text-color="settingsPopupTextColor"
64
+ :settings-popup-text-font-size="settingsPopupTextFontSize"
65
+ :infor-popup-background-color="inforPopupBackgroundColor"
66
+ :infor-popup-title-text-color="inforPopupTitleTextColor"
67
+ :infor-popup-title-text-font-size="inforPopupTitleTextFontSize"
68
+ :infor-popup-description-text-color="inforPopupDescriptionTextColor"
69
+ :infor-popup-description-text-font-size="inforPopupDescriptionTextFontSize"
70
+ :snap-grid-size="snapToGrid ? snapGridSize : null"
71
+ @drag-start="onNodeDragStart"
72
+ @node-click="onNodeClick"
73
+ @node-double-click="onNodeDoubleClick"
74
+ @node-context-menu="onNodeContextMenu"
75
+ @node-settings-click="onNodeSettingsClick"
76
+ @node-settings-update="onNodeSettingsUpdate"
77
+ @node-settings-delete="onNodeSettingsDelete"
78
+ @node-mouseenter="onNodeMouseEnter"
79
+ @node-mouseleave="onNodeMouseLeave"
80
+ @connect-start="onConnectStart"
81
+ @dimensions="onNodeDimensions"
82
+ @node-resize="onNodeResize"
83
+ @node-resize-end="onNodeResizeEnd"
84
+ />
85
+
86
+ <ConnectionLine
87
+ :active="isConnecting"
88
+ :source-x="connLineFromX"
89
+ :source-y="connLineFromY"
90
+ :source-position="connLineFromPosition"
91
+ :target-x="connLineToX"
92
+ :target-y="connLineToY"
93
+ :type="defConnCreatingType"
94
+ :line-style="defConnCreatingStyle"
95
+ />
96
+
97
+ <slot name="viewport-overlay" />
98
+ </ViewportTransform>
99
+
100
+ <SelectionBox :box="selectionBox" />
101
+
102
+ <Controls
103
+ :locked="locked"
104
+ position="top-left"
105
+ @zoom-in="zoomIn"
106
+ @zoom-out="zoomOut"
107
+ @fit-view="fitView"
108
+ @toggle-interactive="toggleInteractive"
109
+ />
110
+
111
+ </FlowCanvas>
112
+ </div>
113
+ </template>
114
+
115
+ <script>
116
+ import FlowCanvas from './canvas/FlowCanvas.vue'
117
+ import ViewportTransform from './canvas/ViewportTransform.vue'
118
+ import BackgroundLayer from './canvas/BackgroundLayer.vue'
119
+ import SelectionBox from './canvas/SelectionBox.vue'
120
+ import NodeRenderer from './nodes/NodeRenderer.vue'
121
+ import EdgeRenderer from './edges/EdgeRenderer.vue'
122
+ import ConnectionLine from './edges/ConnectionLine.vue'
123
+ import Controls from './ui/Controls.vue'
124
+ import { getHandlePosition, getOverlappingNodes, snapPosition, clampPosition } from '../js/geometry'
125
+ import { clearStepCache } from '../js/edge-path'
126
+ import { isValidConnection, generateId } from '../js/graph'
127
+ import { NODE_DEFAULTS, CONN_DEFAULTS } from '../js/defaults'
128
+
129
+ /**
130
+ * WFlowVue — Vue 2 flow/graph editor component.
131
+ *
132
+ * All configuration is passed via the `opt` prop object.
133
+ *
134
+ * @prop {Object} opt
135
+ *
136
+ * ─── Canvas ────────────────────────────────────────────────────────────
137
+ * @prop {number} [opt.width=800] Canvas width (px)
138
+ * @prop {number} [opt.height=600] Canvas height (px)
139
+ * @prop {Array} [opt.nodes=[]] Node data array
140
+ * @prop {Array} [opt.conns=[]] Connection data array
141
+ *
142
+ * ─── Interaction ───────────────────────────────────────────────────────
143
+ * @prop {boolean} [opt.nodesDraggable=true] Allow node dragging
144
+ * @prop {boolean} [opt.nodesConnectable=true] Allow creating connections
145
+ * @prop {boolean} [opt.nodesResizable=true] Allow resizing nodes (per-node override: node.resizable)
146
+ * @prop {boolean} [opt.elementsSelectable=true] Allow selecting nodes/conns
147
+ * @prop {boolean} [opt.selectNodesOnDrag=true] Select node when drag starts
148
+ * @prop {boolean} [opt.deleteKeyEnabled=false] Enable keyboard deletion of selected elements
149
+ * @prop {string} [opt.deleteKeyCode='Backspace'] Key to delete selected elements (requires deleteKeyEnabled)
150
+ * @prop {boolean} [opt.multiSelectEnabled=true] Enable multi-selection (box select + Shift+Click)
151
+ * @prop {string} [opt.boxSelectionKeyCode='Shift'] Key to hold for box selection (drag on canvas)
152
+ * @prop {string} [opt.multiSelectionKeyCode='Shift'] Key to hold for Shift+Click add/remove selection
153
+ * @prop {boolean} [opt.zoomOnScroll=true] Zoom with mouse wheel
154
+ * @prop {number} [opt.zoomMin=0.5] Minimum zoom level
155
+ * @prop {number} [opt.zoomMax=2] Maximum zoom level
156
+ * @prop {boolean} [opt.panOnDrag=true] Pan canvas by dragging background
157
+ * @prop {Array} [opt.center=[0,0]] Initial viewport center [x, y]
158
+ * @prop {number} [opt.zoom=1] Initial viewport zoom level
159
+ * @prop {Array} [opt.panLimits=null] Pan limits [[minX,minY],[maxX,maxY]]
160
+ * @prop {boolean} [opt.snapToGrid=false] Snap node positions to grid
161
+ * @prop {number} [opt.snapGridSize=20] Grid cell size (px, used for both drag snap and resize snap)
162
+ *
163
+ * ─── Platform ────────────────────────────────────────────────────────
164
+ * @prop {string} [opt.platformBackgroundPatternType='dots'] Background pattern: 'dots' | 'lines' | 'cross'
165
+ * @prop {number} [opt.platformBackgroundPatternGap=20] Pattern spacing (px)
166
+ * @prop {number} [opt.platformBackgroundPatternSize=1] Pattern element size
167
+ * @prop {string} [opt.platformBackgroundPatternColor='#81818a'] Pattern color
168
+ * @prop {string} [opt.platformBackgroundColor='#fff'] Canvas background color
169
+ *
170
+ * ─── Settings Popup ────────────────────────────────────────────────────
171
+ * @prop {string} [opt.settingsPopupBackgroundColor='#fff'] Settings popup background
172
+ * @prop {string} [opt.settingsPopupTextColor='#333'] Settings popup text color
173
+ * @prop {string} [opt.settingsPopupTextFontSize='12px'] Settings popup font size
174
+ *
175
+ * ─── Infor Popup ────────────────────────────────────────────────────────
176
+ * @prop {string} [opt.inforPopupBackgroundColor='#fff'] Info popup background
177
+ * @prop {string} [opt.inforPopupTitleTextColor='#333'] Info popup title text color
178
+ * @prop {string} [opt.inforPopupTitleTextFontSize='12px'] Info popup title font size
179
+ * @prop {string} [opt.inforPopupDescriptionTextColor='#888'] Info popup description text color
180
+ * @prop {string} [opt.inforPopupDescriptionTextFontSize='10px'] Info popup description font size
181
+ *
182
+ * ─── Default Node ──────────────────────────────────────────
183
+ * @prop {string} [opt.defNodeType='basic'] Default node type: 'input' | 'basic' | 'output'
184
+ * @prop {string} [opt.defNodeShape='rectangle'] Default shape: 'rectangle' | 'diamond' | 'ellipse' | 'triangle' | ...
185
+ * @prop {number} [opt.defNodeWidth=100] Default node width (px)
186
+ * @prop {number} [opt.defNodeHeight=40] Default node height (px)
187
+ * @prop {number} [opt.defNodeFontSize=12] Default node font size (px)
188
+ * @prop {number} [opt.defNodeFontSizeMin=1] Min font size in settings
189
+ * @prop {number} [opt.defNodeFontSizeMax=72] Max font size in settings
190
+ * @prop {string} [opt.defNodeFontColor='#333333'] Default node text color
191
+ * @prop {string} [opt.defNodeFaceColor='#ffffff'] Default node fill color
192
+ * @prop {string} [opt.defNodeEdgeColor='#bbbbbb'] Default node border color
193
+ * @prop {number} [opt.defNodeEdgeWidth=1] Default node border width (px)
194
+ * @prop {string} [opt.defNodeToPosition='bottom'] Default outgoing handle position: 'top' | 'bottom' | 'left' | 'right'
195
+ * @prop {string} [opt.defNodeFromPosition='top'] Default incoming handle position
196
+ * @prop {string} [opt.defNodePopupDirection='right'] Default settings popup direction
197
+ *
198
+ * ─── Default Creating Connection ────────────────────────────────────────────────────────
199
+ * @prop {string} [opt.defConnCreatingType='bezier'] Drag-line type: 'bezier' | 'straight' | 'step' | 'smoothstep'
200
+ * @prop {string} [opt.defConnCreatingEdgeColor='#b1b1b7'] Drag-line color
201
+ * @prop {number} [opt.defConnCreatingEdgeWidth=1] Drag-line width (px)
202
+ * @prop {string} [opt.defConnCreatingEdgeDasharray='5 5'] Drag-line dash pattern ('' for solid)
203
+ * @prop {Function} [opt.funValidConnCreating=null] Custom connection validator fn(connection) → boolean
204
+ *
205
+ * ─── Default Connection ────────────────────────────────────
206
+ * @prop {string} [opt.defConnType='bezier'] Default conn type: 'bezier' | 'straight' | 'step' | 'smoothstep'
207
+ * @prop {number} [opt.defConnFontSize=10] Default conn label font size (px)
208
+ * @prop {number} [opt.defConnFontSizeMin=1] Min font size in settings
209
+ * @prop {number} [opt.defConnFontSizeMax=72] Max font size in settings
210
+ * @prop {string} [opt.defConnFontColor='#333333'] Default conn label text color
211
+ * @prop {string} [opt.defConnEdgeColor='#b1b1b7'] Default conn line color
212
+ * @prop {number} [opt.defConnEdgeWidth=1] Default conn line width (px)
213
+ * @prop {string} [opt.defConnEdgeDasharray=''] Default conn dash pattern ('' for solid, '5 5' for dashed)
214
+ * @prop {string} [opt.defConnMarkerEnd=''] Default arrow marker: '' | 'arrow' | 'arrowclosed'
215
+ * @prop {boolean} [opt.defConnAnimated=false] Default conn animation (dashed flow)
216
+ * @prop {number} [opt.defOffset=24] Step/smoothstep routing buffer (px)
217
+ */
218
+ export default {
219
+ components: {
220
+ FlowCanvas,
221
+ ViewportTransform,
222
+ BackgroundLayer,
223
+ SelectionBox,
224
+ NodeRenderer,
225
+ EdgeRenderer,
226
+ ConnectionLine,
227
+ Controls,
228
+ },
229
+ props: {
230
+ opt: {
231
+ type: Object,
232
+ default: () => ({}),
233
+ },
234
+ },
235
+ provide() {
236
+ return {
237
+ getDefNode: () => this.defNode,
238
+ getDefConn: () => this.defConn,
239
+ }
240
+ },
241
+ data() {
242
+ return {
243
+ inited: false,
244
+
245
+ // Viewport
246
+ viewport: { x: 0, y: 0, zoom: 1 },
247
+
248
+ // Selection
249
+ selectedNodes: [],
250
+ selectedConns: [],
251
+
252
+ // UI state
253
+ selectionBox: null,
254
+ nodeInternals: {},
255
+
256
+ // Interactive lock state
257
+ locked: false,
258
+
259
+ // Drag state
260
+ isDraggingNode: false,
261
+ draggingNodeId: null,
262
+ dragStartPos: null,
263
+ dragNodeStartPositions: null,
264
+ dragPositions: null,
265
+ resizeOverlay: null,
266
+
267
+ // Pan state
268
+ isPanning: false,
269
+ panStartPos: null,
270
+
271
+ // Connection state
272
+ isConnecting: false,
273
+ connectingFrom: null,
274
+ connLineFromX: 0,
275
+ connLineFromY: 0,
276
+ connLineFromPosition: 'bottom',
277
+ connLineToX: 0,
278
+ connLineToY: 0,
279
+
280
+ // Selection state
281
+ isSelecting: false,
282
+ selectionStartPos: null,
283
+
284
+ // Key state
285
+ keysPressed: {},
286
+
287
+ }
288
+ },
289
+ watch: {
290
+ opt: {
291
+ handler() {
292
+ if (!this.inited) {
293
+ this.inited = true
294
+ let vc = this.center
295
+ if (vc) {
296
+ this.viewport.x = vc[0] || 0
297
+ this.viewport.y = vc[1] || 0
298
+ }
299
+ this.viewport.zoom = this.zoom
300
+ this.$emit('init')
301
+ }
302
+ },
303
+ immediate: true,
304
+ },
305
+ },
306
+ mounted() {
307
+ document.addEventListener('keydown', this.onKeyDown)
308
+ document.addEventListener('keyup', this.onKeyUp)
309
+ document.addEventListener('mousemove', this.onDocMouseMove)
310
+ document.addEventListener('mouseup', this.onDocMouseUp)
311
+ },
312
+ beforeDestroy() {
313
+ document.removeEventListener('keydown', this.onKeyDown)
314
+ document.removeEventListener('keyup', this.onKeyUp)
315
+ document.removeEventListener('mousemove', this.onDocMouseMove)
316
+ document.removeEventListener('mouseup', this.onDocMouseUp)
317
+ },
318
+ computed: {
319
+ widthInp() {
320
+ return this.opt.width || 800
321
+ },
322
+ heightInp() {
323
+ return this.opt.height || 600
324
+ },
325
+ nodes() {
326
+ return this.opt.nodes || []
327
+ },
328
+ conns() {
329
+ return this.opt.conns || []
330
+ },
331
+
332
+ nodesDraggable() {
333
+ return this.opt.nodesDraggable !== undefined ? this.opt.nodesDraggable : true
334
+ },
335
+ nodesConnectable() {
336
+ return this.opt.nodesConnectable !== undefined ? this.opt.nodesConnectable : true
337
+ },
338
+ nodesResizable() {
339
+ return this.opt.nodesResizable !== undefined ? this.opt.nodesResizable : true
340
+ },
341
+ elementsSelectable() {
342
+ return this.opt.elementsSelectable !== undefined ? this.opt.elementsSelectable : true
343
+ },
344
+ selectNodesOnDrag() {
345
+ return this.opt.selectNodesOnDrag !== undefined ? this.opt.selectNodesOnDrag : true
346
+ },
347
+ deleteKeyEnabled() {
348
+ return this.opt.deleteKeyEnabled !== undefined ? this.opt.deleteKeyEnabled : false
349
+ },
350
+ deleteKeyCode() {
351
+ return this.opt.deleteKeyCode || 'Backspace'
352
+ },
353
+
354
+ defConnCreatingType() {
355
+ return this.opt.defConnCreatingType || 'bezier'
356
+ },
357
+ defConnCreatingEdgeColor() {
358
+ return this.opt.defConnCreatingEdgeColor || '#b1b1b7'
359
+ },
360
+ defConnCreatingEdgeWidth() {
361
+ return this.opt.defConnCreatingEdgeWidth !== undefined ? this.opt.defConnCreatingEdgeWidth : 1
362
+ },
363
+ defConnCreatingEdgeDasharray() {
364
+ return this.opt.defConnCreatingEdgeDasharray || '5 5'
365
+ },
366
+ defConnCreatingStyle() {
367
+ return {
368
+ stroke: this.defConnCreatingEdgeColor,
369
+ strokeWidth: this.defConnCreatingEdgeWidth,
370
+ strokeDasharray: this.defConnCreatingEdgeDasharray,
371
+ }
372
+ },
373
+ zoomOnScroll() {
374
+ return this.opt.zoomOnScroll !== undefined ? this.opt.zoomOnScroll : true
375
+ },
376
+ panOnDrag() {
377
+ return this.opt.panOnDrag !== undefined ? this.opt.panOnDrag : true
378
+ },
379
+ zoomMin() {
380
+ return this.opt.zoomMin !== undefined ? this.opt.zoomMin : 0.5
381
+ },
382
+ zoomMax() {
383
+ return this.opt.zoomMax !== undefined ? this.opt.zoomMax : 2
384
+ },
385
+ center() {
386
+ return this.opt.center || [0, 0]
387
+ },
388
+ zoom() {
389
+ return this.opt.zoom !== undefined ? this.opt.zoom : 1
390
+ },
391
+ panLimits() {
392
+ return this.opt.panLimits || null
393
+ },
394
+
395
+ multiSelectEnabled() {
396
+ return this.opt.multiSelectEnabled !== undefined ? this.opt.multiSelectEnabled : true
397
+ },
398
+ boxSelectionKeyCode() {
399
+ return this.opt.boxSelectionKeyCode || 'Shift'
400
+ },
401
+ multiSelectionKeyCode() {
402
+ return this.opt.multiSelectionKeyCode || 'Shift'
403
+ },
404
+
405
+ snapToGrid() {
406
+ return this.opt.snapToGrid !== undefined ? this.opt.snapToGrid : false
407
+ },
408
+ snapGridSize() {
409
+ return this.opt.snapGridSize || 20
410
+ },
411
+
412
+ platformBackgroundPatternType() {
413
+ return this.opt.platformBackgroundPatternType || 'dots'
414
+ },
415
+ platformBackgroundPatternGap() {
416
+ return this.opt.platformBackgroundPatternGap !== undefined ? this.opt.platformBackgroundPatternGap : 20
417
+ },
418
+ platformBackgroundPatternSize() {
419
+ return this.opt.platformBackgroundPatternSize !== undefined ? this.opt.platformBackgroundPatternSize : 1
420
+ },
421
+ platformBackgroundPatternColor() {
422
+ return this.opt.platformBackgroundPatternColor || '#81818a'
423
+ },
424
+ platformBackgroundColor() {
425
+ return this.opt.platformBackgroundColor || '#fff'
426
+ },
427
+
428
+ // --- Settings popup styling ---
429
+ settingsPopupBackgroundColor() {
430
+ return this.opt.settingsPopupBackgroundColor || '#fff'
431
+ },
432
+ settingsPopupTextColor() {
433
+ return this.opt.settingsPopupTextColor || '#333'
434
+ },
435
+ settingsPopupTextFontSize() {
436
+ return this.opt.settingsPopupTextFontSize || '12px'
437
+ },
438
+ inforPopupBackgroundColor() {
439
+ return this.opt.inforPopupBackgroundColor || '#fff'
440
+ },
441
+ inforPopupTitleTextColor() {
442
+ return this.opt.inforPopupTitleTextColor || '#333'
443
+ },
444
+ inforPopupTitleTextFontSize() {
445
+ return this.opt.inforPopupTitleTextFontSize || '12px'
446
+ },
447
+ inforPopupDescriptionTextColor() {
448
+ return this.opt.inforPopupDescriptionTextColor || '#888'
449
+ },
450
+ inforPopupDescriptionTextFontSize() {
451
+ return this.opt.inforPopupDescriptionTextFontSize || '10px'
452
+ },
453
+
454
+ funValidConnCreating() {
455
+ return this.opt.funValidConnCreating || null
456
+ },
457
+
458
+ defNode() {
459
+ let o = this.opt
460
+ let d = NODE_DEFAULTS
461
+ return {
462
+ type: o.defNodeType || d.type,
463
+ shape: o.defNodeShape || d.shape,
464
+ width: o.defNodeWidth || d.width,
465
+ height: o.defNodeHeight || d.height,
466
+ fontSize: o.defNodeFontSize || d.fontSize,
467
+ fontSizeMin: o.defNodeFontSizeMin || d.fontSizeMin,
468
+ fontSizeMax: o.defNodeFontSizeMax || d.fontSizeMax,
469
+ fontColor: o.defNodeFontColor || d.fontColor,
470
+ faceColor: o.defNodeFaceColor || d.faceColor,
471
+ edgeColor: o.defNodeEdgeColor || d.edgeColor,
472
+ edgeWidth: o.defNodeEdgeWidth !== undefined ? o.defNodeEdgeWidth : d.edgeWidth,
473
+ toPosition: o.defNodeToPosition || d.toPosition,
474
+ fromPosition: o.defNodeFromPosition || d.fromPosition,
475
+ popupDirection: o.defNodePopupDirection || d.popupDirection,
476
+ }
477
+ },
478
+ defConn() {
479
+ let o = this.opt
480
+ let d = CONN_DEFAULTS
481
+ return {
482
+ type: o.defConnType || d.type,
483
+ fontSize: o.defConnFontSize || d.fontSize,
484
+ fontSizeMin: o.defConnFontSizeMin || d.fontSizeMin,
485
+ fontSizeMax: o.defConnFontSizeMax || d.fontSizeMax,
486
+ fontColor: o.defConnFontColor || d.fontColor,
487
+ edgeColor: o.defConnEdgeColor || d.edgeColor,
488
+ edgeWidth: o.defConnEdgeWidth !== undefined ? o.defConnEdgeWidth : d.edgeWidth,
489
+ edgeDasharray: o.defConnEdgeDasharray || '',
490
+ markerEnd: o.defConnMarkerEnd || d.markerEnd,
491
+ animated: o.defConnAnimated !== undefined ? o.defConnAnimated : d.animated,
492
+ defOffset: o.defOffset != null ? o.defOffset : d.defOffset,
493
+ }
494
+ },
495
+
496
+ renderNodes() {
497
+ let dp = this.dragPositions
498
+ let ro = this.resizeOverlay
499
+ if (!dp && !ro) return this.nodes
500
+ return this.nodes.map(n => {
501
+ if (dp && dp[n.id]) return { ...n, position: dp[n.id] }
502
+ if (ro && ro.id === n.id) return { ...n, position: { x: ro.x, y: ro.y }, width: ro.width, height: ro.height }
503
+ return n
504
+ })
505
+ },
506
+ isBoxSelectPressed() {
507
+ return this.multiSelectEnabled && !!this.keysPressed[this.boxSelectionKeyCode]
508
+ },
509
+ isMultiSelectPressed() {
510
+ return this.multiSelectEnabled && !!this.keysPressed[this.multiSelectionKeyCode]
511
+ },
512
+ },
513
+ methods: {
514
+ // --- Helpers (replace store methods) ---
515
+ nodeById(id) {
516
+ return this.nodes.find(n => n.id === id) || null
517
+ },
518
+ connById(id) {
519
+ return this.conns.find(c => c.id === id) || null
520
+ },
521
+ setSelectedNodes(ids) {
522
+ this.selectedNodes.splice(0, this.selectedNodes.length, ...ids)
523
+ },
524
+ setSelectedConns(ids) {
525
+ this.selectedConns.splice(0, this.selectedConns.length, ...ids)
526
+ },
527
+ clearSelection() {
528
+ this.selectedNodes.splice(0, this.selectedNodes.length)
529
+ this.selectedConns.splice(0, this.selectedConns.length)
530
+ },
531
+ removeNode(id) {
532
+ let nodes = this.nodes
533
+ let idx = nodes.findIndex(n => n.id === id)
534
+ if (idx === -1) return
535
+ nodes.splice(idx, 1)
536
+ // Remove connected conns
537
+ let conns = this.conns
538
+ for (let i = conns.length - 1; i >= 0; i--) {
539
+ if (conns[i].from === id || conns[i].to === id) {
540
+ conns.splice(i, 1)
541
+ }
542
+ }
543
+ let selIdx = this.selectedNodes.indexOf(id)
544
+ if (selIdx !== -1) this.selectedNodes.splice(selIdx, 1)
545
+ },
546
+ removeConn(id) {
547
+ let conns = this.conns
548
+ let idx = conns.findIndex(c => c.id === id)
549
+ if (idx === -1) return
550
+ conns.splice(idx, 1)
551
+ let selIdx = this.selectedConns.indexOf(id)
552
+ if (selIdx !== -1) this.selectedConns.splice(selIdx, 1)
553
+ },
554
+ addConn(conn) {
555
+ if (!conn.id || !conn.from || !conn.to) return
556
+ if (this.connById(conn.id)) return
557
+ if (!this.nodeById(conn.from) || !this.nodeById(conn.to)) return
558
+ this.conns.push(conn)
559
+ },
560
+ updateNodeInternals(id, internals) {
561
+ let existing = this.nodeInternals[id]
562
+ if (existing && existing.width === internals.width && existing.height === internals.height) return
563
+ this.$set(this.nodeInternals, id, internals)
564
+ },
565
+ setViewport({ x, y, zoom }) {
566
+ if (x !== undefined) this.viewport.x = x
567
+ if (y !== undefined) this.viewport.y = y
568
+ if (zoom !== undefined) this.viewport.zoom = zoom
569
+ },
570
+
571
+ // --- Key handling ---
572
+ onKeyDown(e) {
573
+ this.keysPressed = { ...this.keysPressed, [e.key]: true }
574
+ if (!this.locked && this.deleteKeyEnabled && (e.key === this.deleteKeyCode || e.key === 'Delete')) {
575
+ this.deleteSelectedElements()
576
+ }
577
+ },
578
+ onKeyUp(e) {
579
+ const copy = { ...this.keysPressed }
580
+ delete copy[e.key]
581
+ this.keysPressed = copy
582
+ },
583
+
584
+ // --- Canvas events ---
585
+ onCanvasClick(event) {
586
+ if (event.target.closest('.vue-flow__popup')) return
587
+ if (!event.target.closest('.vue-flow__node') && !event.target.closest('.vue-flow__edge')) {
588
+ this.clearSelection()
589
+ this.$emit('pane-click', event)
590
+ }
591
+ },
592
+ onCanvasContextMenu(event) {
593
+ this.$emit('pane-context-menu', event)
594
+ },
595
+ onCanvasDblClick(event) {
596
+ // Calculate flow-space position from the click
597
+ const rect = this.$refs.canvas.getContainerRect()
598
+ if (!rect) return
599
+ const vp = this.viewport
600
+ const flowX = (event.clientX - rect.left - vp.x) / vp.zoom
601
+ const flowY = (event.clientY - rect.top - vp.y) / vp.zoom
602
+
603
+ this.$emit('canvas-dblclick', {
604
+ event,
605
+ flowX,
606
+ flowY,
607
+ clientX: event.clientX,
608
+ clientY: event.clientY,
609
+ })
610
+ },
611
+ onCanvasMouseDown(event) {
612
+ if (event.target.closest && event.target.closest('.vue-flow__popup')) return
613
+ if (!this.locked && this.isBoxSelectPressed) {
614
+ this.startSelection(event)
615
+ return
616
+ }
617
+ if (this.panOnDrag && event.button === 0) {
618
+ const target = event.target
619
+ const isOnNode = target.closest && target.closest('.vue-flow__node')
620
+ const isOnHandle = target.closest && target.closest('.vue-flow__handle')
621
+ if (!isOnNode && !isOnHandle) {
622
+ this.startPan(event)
623
+ }
624
+ }
625
+ },
626
+ onCanvasMouseMove(event) {
627
+ // Handled by document-level listener
628
+ },
629
+ onCanvasMouseUp(event) {
630
+ // Handled by document-level listener
631
+ },
632
+ onCanvasWheel(event) {
633
+ if (!this.zoomOnScroll) return
634
+ const delta = -event.deltaY * 0.001
635
+ const currentZoom = this.viewport.zoom
636
+ const newZoom = Math.max(this.zoomMin, Math.min(this.zoomMax, currentZoom + delta * currentZoom))
637
+
638
+ const rect = this.$refs.canvas.getContainerRect()
639
+ if (!rect) return
640
+ const mouseX = event.clientX - rect.left
641
+ const mouseY = event.clientY - rect.top
642
+
643
+ const vp = this.viewport
644
+ const scale = newZoom / currentZoom
645
+ const newX = mouseX - (mouseX - vp.x) * scale
646
+ const newY = mouseY - (mouseY - vp.y) * scale
647
+
648
+ this.setViewport({ x: newX, y: newY, zoom: newZoom })
649
+ this.emitViewportChange()
650
+ },
651
+
652
+ // --- Document-level mouse ---
653
+ onDocMouseMove(event) {
654
+ if (this.isPanning) {
655
+ this.doPan(event)
656
+ }
657
+ else if (this.isDraggingNode) {
658
+ this.doDrag(event)
659
+ }
660
+ else if (this.isConnecting) {
661
+ this.doConnect(event)
662
+ }
663
+ else if (this.isSelecting) {
664
+ this.doSelection(event)
665
+ }
666
+ },
667
+ onDocMouseUp(event) {
668
+ if (this.isPanning) {
669
+ this.endPan()
670
+ }
671
+ if (this.isDraggingNode) {
672
+ this.endDrag(event)
673
+ }
674
+ if (this.isConnecting) {
675
+ this.endConnect(event)
676
+ }
677
+ if (this.isSelecting) {
678
+ this.endSelection(event)
679
+ }
680
+ },
681
+
682
+ // --- Pan ---
683
+ startPan(event) {
684
+ this.isPanning = true
685
+ this.panStartPos = { x: event.clientX, y: event.clientY }
686
+ },
687
+ doPan(event) {
688
+ const dx = event.clientX - this.panStartPos.x
689
+ const dy = event.clientY - this.panStartPos.y
690
+ this.panStartPos = { x: event.clientX, y: event.clientY }
691
+
692
+ let x = this.viewport.x + dx
693
+ let y = this.viewport.y + dy
694
+
695
+ if (this.panLimits) {
696
+ const clamped = clampPosition({ x, y }, this.panLimits)
697
+ x = clamped.x
698
+ y = clamped.y
699
+ }
700
+
701
+ this.viewport.x = x
702
+ this.viewport.y = y
703
+ },
704
+ endPan() {
705
+ this.isPanning = false
706
+ this.panStartPos = null
707
+ this.emitViewportChange()
708
+ },
709
+
710
+ // --- Node drag ---
711
+ onNodeDragStart({ node, event }) {
712
+ if (this.locked || !this.nodesDraggable) return
713
+ this.isDraggingNode = true
714
+ this.draggingNodeId = node.id
715
+ this.dragStartPos = { x: event.clientX, y: event.clientY }
716
+
717
+ if (this.selectNodesOnDrag && !this.isMultiSelectPressed) {
718
+ this.setSelectedNodes([node.id])
719
+ this.setSelectedConns([])
720
+ }
721
+
722
+ // Cache start positions for drag
723
+ const starts = {}
724
+ this.selectedNodes.forEach(id => {
725
+ const n = this.nodeById(id)
726
+ if (n) starts[id] = { x: n.position.x, y: n.position.y }
727
+ })
728
+ if (!starts[node.id]) {
729
+ const n = this.nodeById(node.id)
730
+ if (n) starts[node.id] = { x: n.position.x, y: n.position.y }
731
+ }
732
+ this.dragNodeStartPositions = starts
733
+
734
+ this.$emit('node-drag-start', { node, event })
735
+ },
736
+ doDrag(event) {
737
+ const zoom = this.viewport.zoom
738
+ const dx = (event.clientX - this.dragStartPos.x) / zoom
739
+ const dy = (event.clientY - this.dragStartPos.y) / zoom
740
+ const snap = this.snapToGrid
741
+ const dp = {}
742
+
743
+ for (let id in this.dragNodeStartPositions) {
744
+ const start = this.dragNodeStartPositions[id]
745
+ let x = start.x + dx
746
+ let y = start.y + dy
747
+ if (snap) {
748
+ const s = snapPosition({ x, y }, this.snapGridSize)
749
+ x = s.x
750
+ y = s.y
751
+ }
752
+ dp[id] = { x, y }
753
+ }
754
+ this.dragPositions = dp
755
+ },
756
+ endDrag(event) {
757
+ // Write final positions back to opt.nodes
758
+ if (this.dragPositions) {
759
+ for (let id in this.dragPositions) {
760
+ let node = this.nodeById(id)
761
+ if (node) {
762
+ let pos = this.dragPositions[id]
763
+ node.position.x = pos.x
764
+ node.position.y = pos.y
765
+ }
766
+ }
767
+ }
768
+ const dragNode = this.nodeById(this.draggingNodeId)
769
+ this.isDraggingNode = false
770
+ this.draggingNodeId = null
771
+ this.dragStartPos = null
772
+ this.dragNodeStartPositions = null
773
+ this.dragPositions = null
774
+ clearStepCache()
775
+ if (dragNode) {
776
+ this.$emit('node-drag-stop', { node: dragNode, event })
777
+ }
778
+ this.emitNodesUpdate()
779
+ },
780
+
781
+ // --- Connection ---
782
+ onConnectStart(payload) {
783
+ if (this.locked || !this.nodesConnectable) return
784
+ this.isConnecting = true
785
+ this.connectingFrom = payload
786
+ // Lock cursor to default during connecting, only handles show crosshair
787
+ this._connectCursorStyle = document.createElement('style')
788
+ this._connectCursorStyle.textContent = '* { cursor: default !important; } .vue-flow__handle { cursor: crosshair !important; } .vue-flow__node-settings, .vue-flow__edge-settings, .vue-flow__resize { opacity: 0 !important; pointer-events: none !important; }'
789
+ document.head.appendChild(this._connectCursorStyle)
790
+
791
+ const node = this.nodeById(payload.nodeId)
792
+ if (!node) return
793
+ const pos = getHandlePosition(
794
+ node, payload.handlePosition,
795
+ this.nodeInternals[payload.nodeId] || {}
796
+ )
797
+ this.connLineFromX = pos.x
798
+ this.connLineFromY = pos.y
799
+ this.connLineFromPosition = payload.handlePosition
800
+ this.connLineToX = pos.x
801
+ this.connLineToY = pos.y
802
+
803
+ this.$emit('connect-start', {
804
+ nodeId: payload.nodeId,
805
+ handleId: payload.handleId,
806
+ handleType: payload.handleType,
807
+ })
808
+ },
809
+ doConnect(event) {
810
+ const rect = this.$refs.canvas.getContainerRect()
811
+ if (!rect) return
812
+ const vp = this.viewport
813
+ this.connLineToX = (event.clientX - rect.left - vp.x) / vp.zoom
814
+ this.connLineToY = (event.clientY - rect.top - vp.y) / vp.zoom
815
+ },
816
+ endConnect(event) {
817
+ // Find handle element under cursor
818
+ const el = document.elementFromPoint(event.clientX, event.clientY)
819
+ const handleEl = el && el.closest && el.closest('.vue-flow__handle')
820
+
821
+ if (handleEl && this.connectingFrom) {
822
+ const toNodeEl = handleEl.closest('.vue-flow__node')
823
+ const toNodeId = toNodeEl ? toNodeEl.dataset.id : null
824
+
825
+ if (toNodeId) {
826
+ const connection = {
827
+ from: this.connectingFrom.nodeId,
828
+ to: toNodeId,
829
+ }
830
+
831
+ const valid = isValidConnection(
832
+ connection,
833
+ this.nodes,
834
+ this.conns,
835
+ this.funValidConnCreating
836
+ )
837
+
838
+ if (valid) {
839
+ const connId = `e${connection.from}-${connection.to}`
840
+ const conn = {
841
+ id: this.connById(connId) ? generateId() : connId,
842
+ ...connection,
843
+ }
844
+
845
+ this.addConn(conn)
846
+ this.emitConnsUpdate()
847
+ this.$emit('connect', connection)
848
+ }
849
+ }
850
+ }
851
+
852
+ this.$emit('connect-end', event)
853
+ this.isConnecting = false
854
+ this.connectingFrom = null
855
+ if (this._connectCursorStyle) {
856
+ document.head.removeChild(this._connectCursorStyle)
857
+ this._connectCursorStyle = null
858
+ }
859
+ },
860
+
861
+ // --- Selection ---
862
+ onNodeClick({ node, event }) {
863
+ if (!this.elementsSelectable) return
864
+ if (this.isMultiSelectPressed) {
865
+ const idx = this.selectedNodes.indexOf(node.id)
866
+ if (idx === -1) {
867
+ this.selectedNodes.push(node.id)
868
+ }
869
+ else {
870
+ this.selectedNodes.splice(idx, 1)
871
+ }
872
+ }
873
+ else {
874
+ this.setSelectedNodes([node.id])
875
+ this.setSelectedConns([])
876
+ }
877
+ this.emitSelectionChange()
878
+
879
+ this.$emit('node-click', { node, event })
880
+ },
881
+ onNodeDoubleClick(payload) {
882
+ this.$emit('node-double-click', payload)
883
+ },
884
+ onNodeContextMenu(payload) {
885
+ this.$emit('node-context-menu', payload)
886
+ },
887
+ onNodeSettingsClick(payload) {
888
+ this.$emit('node-settings-click', payload)
889
+ },
890
+ onNodeSettingsUpdate({ node, key, value }) {
891
+ let n = this.nodeById(node.id)
892
+ if (!n) return
893
+ let oldType = n.type
894
+ this.$set(n, key, value)
895
+ if (key === 'type' && oldType !== value) {
896
+ let nodeId = n.id
897
+ let hasTo = value === 'input' || value === 'basic'
898
+ let hasFrom = value === 'output' || value === 'basic'
899
+ let conns = this.conns
900
+ for (let i = conns.length - 1; i >= 0; i--) {
901
+ let c = conns[i]
902
+ if ((!hasTo && c.from === nodeId) || (!hasFrom && c.to === nodeId)) {
903
+ conns.splice(i, 1)
904
+ }
905
+ }
906
+ }
907
+ },
908
+ onNodeSettingsDelete({ node }) {
909
+ this.removeNode(node.id)
910
+ clearStepCache()
911
+ this.emitNodesUpdate()
912
+ this.emitConnsUpdate()
913
+ },
914
+ onNodeMouseEnter({ node, event }) {
915
+ this.$emit('node-mouseenter', { node, event })
916
+ },
917
+ onNodeMouseLeave({ node, event }) {
918
+ this.$emit('node-mouseleave', { node, event })
919
+ },
920
+ onConnClick({ conn, event }) {
921
+ if (!this.elementsSelectable) return
922
+ if (this.isMultiSelectPressed) {
923
+ const idx = this.selectedConns.indexOf(conn.id)
924
+ if (idx === -1) {
925
+ this.selectedConns.push(conn.id)
926
+ }
927
+ else {
928
+ this.selectedConns.splice(idx, 1)
929
+ }
930
+ }
931
+ else {
932
+ this.setSelectedConns([conn.id])
933
+ this.setSelectedNodes([])
934
+ }
935
+ this.emitSelectionChange()
936
+
937
+ this.$emit('conn-click', { conn, event })
938
+ },
939
+ onConnDoubleClick(payload) {
940
+ this.$emit('conn-double-click', payload)
941
+ },
942
+ onConnContextMenu(payload) {
943
+ this.$emit('conn-context-menu', payload)
944
+ },
945
+ onConnMouseEnter({ conn, event }) {
946
+ this.$emit('conn-mouseenter', { conn, event })
947
+ },
948
+ onConnMouseLeave({ conn, event }) {
949
+ this.$emit('conn-mouseleave', { conn, event })
950
+ },
951
+ onConnSettingsClick(payload) {
952
+ this.$emit('conn-settings-click', payload)
953
+ },
954
+ onConnSettingsUpdate({ conn, key, value }) {
955
+ let c = this.connById(conn.id)
956
+ if (c) {
957
+ this.$set(c, key, value)
958
+ }
959
+ },
960
+ onConnSettingsDelete({ conn }) {
961
+ this.removeConn(conn.id)
962
+ clearStepCache()
963
+ this.emitConnsUpdate()
964
+ },
965
+
966
+ startSelection(event) {
967
+ this.isSelecting = true
968
+ const rect = this.$refs.canvas.getContainerRect()
969
+ if (!rect) return
970
+ this.selectionStartPos = {
971
+ x: event.clientX - rect.left,
972
+ y: event.clientY - rect.top,
973
+ }
974
+ this.selectionBox = {
975
+ x: this.selectionStartPos.x,
976
+ y: this.selectionStartPos.y,
977
+ width: 0,
978
+ height: 0,
979
+ }
980
+ },
981
+ doSelection(event) {
982
+ const rect = this.$refs.canvas.getContainerRect()
983
+ if (!rect) return
984
+ const currentX = event.clientX - rect.left
985
+ const currentY = event.clientY - rect.top
986
+ const x = Math.min(this.selectionStartPos.x, currentX)
987
+ const y = Math.min(this.selectionStartPos.y, currentY)
988
+ const width = Math.abs(currentX - this.selectionStartPos.x)
989
+ const height = Math.abs(currentY - this.selectionStartPos.y)
990
+ this.selectionBox = { x, y, width, height }
991
+ },
992
+ endSelection() {
993
+ if (this.selectionBox) {
994
+ const box = this.selectionBox
995
+ const vp = this.viewport
996
+ // Convert screen-space box to graph-space
997
+ const graphBox = {
998
+ x: (box.x - vp.x) / vp.zoom,
999
+ y: (box.y - vp.y) / vp.zoom,
1000
+ width: box.width / vp.zoom,
1001
+ height: box.height / vp.zoom,
1002
+ }
1003
+ const overlapping = getOverlappingNodes(graphBox, this.nodes, this.nodeInternals)
1004
+ const nodeIds = overlapping.map(n => n.id)
1005
+ this.setSelectedNodes(nodeIds)
1006
+ // Auto-select conns whose both ends are within selected nodes
1007
+ let nodeIdSet = new Set(nodeIds)
1008
+ let connIds = this.conns
1009
+ .filter(c => nodeIdSet.has(c.from) && nodeIdSet.has(c.to))
1010
+ .map(c => c.id)
1011
+ this.setSelectedConns(connIds)
1012
+ this.emitSelectionChange()
1013
+ }
1014
+ this.isSelecting = false
1015
+ this.selectionStartPos = null
1016
+ this.selectionBox = null
1017
+ },
1018
+
1019
+ // --- Delete ---
1020
+ deleteSelectedElements() {
1021
+ const { nodes, conns } = this.getSelectedElements()
1022
+ if (nodes.length === 0 && conns.length === 0) return
1023
+
1024
+ nodes.forEach(n => {
1025
+ if (n.deletable !== false) this.removeNode(n.id)
1026
+ })
1027
+ conns.forEach(e => {
1028
+ if (e.deletable !== false) this.removeConn(e.id)
1029
+ })
1030
+ this.clearSelection()
1031
+
1032
+ this.$emit('delete', { nodes, conns })
1033
+ this.emitNodesUpdate()
1034
+ this.emitConnsUpdate()
1035
+ },
1036
+
1037
+ // --- Node dimensions ---
1038
+ onNodeDimensions({ nodeId, width, height }) {
1039
+ this.updateNodeInternals(nodeId, { width, height })
1040
+ },
1041
+
1042
+ onNodeResize({ nodeId, width, height, x, y }) {
1043
+ if (this.locked) return
1044
+ this.resizeOverlay = { id: nodeId, width, height, x, y }
1045
+ this.updateNodeInternals(nodeId, { width, height })
1046
+ },
1047
+ onNodeResizeEnd({ nodeId, width, height, x, y }) {
1048
+ let node = this.nodeById(nodeId)
1049
+ if (node) {
1050
+ node.width = width
1051
+ node.height = height
1052
+ node.position.x = x
1053
+ node.position.y = y
1054
+ }
1055
+ this.resizeOverlay = null
1056
+ clearStepCache()
1057
+ this.emitNodesUpdate()
1058
+ },
1059
+
1060
+ // --- Helpers ---
1061
+ updateNodePosition(id, position) {
1062
+ let node = this.nodeById(id)
1063
+ if (!node) return
1064
+ node.position.x = position.x
1065
+ node.position.y = position.y
1066
+ },
1067
+ getSelectedElements() {
1068
+ return {
1069
+ nodes: this.nodes.filter(n => this.selectedNodes.includes(n.id)),
1070
+ conns: this.conns.filter(c => this.selectedConns.includes(c.id)),
1071
+ }
1072
+ },
1073
+
1074
+ // --- Emit helpers ---
1075
+ emitNodesUpdate() {
1076
+ this.$emit('update:nodes', [...this.nodes])
1077
+ },
1078
+ emitConnsUpdate() {
1079
+ this.$emit('update:conns', [...this.conns])
1080
+ },
1081
+ emitViewportChange() {
1082
+ this.$emit('viewport-change', { ...this.viewport })
1083
+ },
1084
+ emitSelectionChange() {
1085
+ this.$emit('selection-change', this.getSelectedElements())
1086
+ },
1087
+
1088
+ // --- Public API ---
1089
+ fitView(padding) {
1090
+ padding = padding || 50
1091
+ let nodes = this.nodes.filter(n => !n.hidden)
1092
+ if (nodes.length === 0) return
1093
+ let internals = this.nodeInternals
1094
+ let minX = Infinity
1095
+ let minY = Infinity
1096
+ let maxX = -Infinity
1097
+ let maxY = -Infinity
1098
+ nodes.forEach(n => {
1099
+ let w = (internals[n.id] && internals[n.id].width) || n.width || 150
1100
+ let h = (internals[n.id] && internals[n.id].height) || n.height || 40
1101
+ minX = Math.min(minX, n.position.x)
1102
+ minY = Math.min(minY, n.position.y)
1103
+ maxX = Math.max(maxX, n.position.x + w)
1104
+ maxY = Math.max(maxY, n.position.y + h)
1105
+ })
1106
+ let rect = this.$refs.canvas ? this.$refs.canvas.getContainerRect() : null
1107
+ let cw = rect ? rect.width : this.widthInp
1108
+ let ch = rect ? rect.height : this.heightInp
1109
+ let gw = maxX - minX + padding * 2
1110
+ let gh = maxY - minY + padding * 2
1111
+ let zoom = Math.min(cw / gw, ch / gh, 2)
1112
+ this.viewport.zoom = zoom
1113
+ this.viewport.x = (cw - (maxX + minX) * zoom) / 2
1114
+ this.viewport.y = (ch - (maxY + minY) * zoom) / 2
1115
+ this.emitViewportChange()
1116
+ },
1117
+ zoomIn() {
1118
+ this.viewport.zoom = Math.min(this.viewport.zoom * 1.2, this.zoomMax)
1119
+ this.emitViewportChange()
1120
+ },
1121
+ zoomOut() {
1122
+ this.viewport.zoom = Math.max(this.viewport.zoom / 1.2, this.zoomMin)
1123
+ this.emitViewportChange()
1124
+ },
1125
+ toggleInteractive() {
1126
+ this.locked = !this.locked
1127
+ this.$emit('toggle-interactive', this.locked)
1128
+ },
1129
+ getFlowData() {
1130
+ return {
1131
+ nodes: JSON.parse(JSON.stringify(this.nodes)),
1132
+ conns: JSON.parse(JSON.stringify(this.conns)),
1133
+ }
1134
+ },
1135
+ },
1136
+ }
1137
+ </script>
1138
+
1139
+ <style scoped>
1140
+
1141
+
1142
+ </style>