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