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,379 @@
1
+ <template>
2
+ <g
3
+ :class="classes"
4
+ :data-id="conn.id"
5
+ @mouseenter="onGroupMouseEnter"
6
+ @mouseleave="onGroupMouseLeave"
7
+ >
8
+ <!-- Hover zone around label + settings icon area (below interaction path in z-order) -->
9
+ <rect
10
+ :x="pathData.labelX - 60"
11
+ :y="pathData.labelY - 18"
12
+ width="120"
13
+ height="36"
14
+ fill="transparent"
15
+ pointer-events="all"
16
+ @click.stop="onClick"
17
+ />
18
+ <!-- Interaction path (wider, invisible) -->
19
+ <path
20
+ :d="pathData.path"
21
+ class="vue-flow__edge-interaction"
22
+ @click.stop="onClick"
23
+ @dblclick.stop="onDoubleClick"
24
+ @contextmenu.stop="onContextMenu"
25
+ />
26
+ <!-- Visible path -->
27
+ <path
28
+ :d="pathData.path"
29
+ :style="connStyle"
30
+ :marker-start="markerStartUrl"
31
+ :marker-end="markerEndUrl"
32
+ />
33
+ <!-- Label + Settings icon (merged into one foreignObject for correct relative positioning) -->
34
+ <foreignObject
35
+ :x="pathData.labelX - 100"
36
+ :y="pathData.labelY - 18"
37
+ width="200"
38
+ height="36"
39
+ style="overflow: visible; pointer-events: none;"
40
+ >
41
+ <div class="vue-flow__edge-label-area" xmlns="http://www.w3.org/1999/xhtml">
42
+ <span class="vue-flow__edge-label-group">
43
+ <WPopup
44
+ v-if="conn.name"
45
+ v-model="infoPopupShow"
46
+ placement="bottom"
47
+ modeHide="mousedown"
48
+ :editable="infoPopupEditable"
49
+ :minWidth="null"
50
+ :maxWidth="null"
51
+ :autoFitMinWidth="false"
52
+ :autoFitMaxWidth="false"
53
+ :backgroundColor="inforPopupBackgroundColor"
54
+ :textFontSize="inforPopupTitleTextFontSize"
55
+ :paddingStyle="{v:8,h:12}"
56
+ :cmpZIndex="10000"
57
+ >
58
+ <template v-slot:trigger>
59
+ <span class="vue-flow__edge-label" :style="labelStyle" @mousedown="onLabelMouseDown">{{ conn.name }}</span>
60
+ </template>
61
+ <template v-slot:content>
62
+ <slot name="conn-popup" :conn="conn">
63
+ <div v-if="conn.name || conn.description" style="min-width:120px">
64
+ <div v-if="conn.name" :style="{ fontSize: inforPopupTitleTextFontSize, color: inforPopupTitleTextColor, fontWeight: 500 }">{{ conn.name }}</div>
65
+ <div v-if="conn.description" :style="{ fontSize: inforPopupDescriptionTextFontSize, color: inforPopupDescriptionTextColor, marginTop: '4px' }">{{ conn.description }}</div>
66
+ </div>
67
+ </slot>
68
+ </template>
69
+ </WPopup>
70
+ <transition name="vue-flow__fade">
71
+ <span v-if="(hovered || settingsPopupShow) && interactive && !locked" class="vue-flow__edge-settings-anchor">
72
+ <WPopup
73
+ v-model="settingsPopupShow"
74
+ placement="right-start"
75
+ modeHide="mousedown"
76
+ :minWidth="null"
77
+ :maxWidth="null"
78
+ :autoFitMinWidth="false"
79
+ :autoFitMaxWidth="false"
80
+ :backgroundColor="settingsPopupBackgroundColor"
81
+ :textColor="settingsPopupTextColor"
82
+ :paddingStyle="{v:8,h:8}"
83
+ :cmpZIndex="10000"
84
+ @show="$emit('conn-settings-click', { conn: conn })"
85
+ >
86
+ <template v-slot:trigger>
87
+ <span class="vue-flow__edge-settings" @mousedown.stop>
88
+ <svg viewBox="0 0 20 20" width="14" height="14" fill="currentColor">
89
+ <path d="M11.078 0l.294 1.833a7.587 7.587 0 0 1 2.174 1.25l1.725-.618 1.078 1.87-1.43 1.217a7.508 7.508 0 0 1 0 2.498l1.43 1.217-1.078 1.87-1.725-.618a7.587 7.587 0 0 1-2.174 1.25L11.078 14H8.922l-.294-1.833a7.587 7.587 0 0 1-2.174-1.25l-1.725.618-1.078-1.87 1.43-1.217a7.508 7.508 0 0 1 0-2.498L3.65 4.733l1.078-1.87 1.725.618a7.587 7.587 0 0 1 2.174-1.25L8.922 0h2.156zM10 4.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z" transform="translate(0 3)"/>
90
+ </svg>
91
+ </span>
92
+ </template>
93
+ <template v-slot:content>
94
+ <ConnSettingsForm
95
+ :conn="conn"
96
+ :def-conn="dc"
97
+ :text-font-size="settingsPopupTextFontSize"
98
+ @update="onSettingsUpdate"
99
+ @delete="onSettingsDelete"
100
+ />
101
+ </template>
102
+ </WPopup>
103
+ </span>
104
+ </transition>
105
+ </span>
106
+ </div>
107
+ </foreignObject>
108
+ </g>
109
+ </template>
110
+
111
+ <script>
112
+ import { getBezierPath, getStraightPath, getStepPath, getSmoothStepPath } from '../../js/edge-path'
113
+ import ConnSettingsForm from '../ui/ConnSettingsForm.vue'
114
+ import WPopup from 'w-component-vue/src/components/WPopup.vue'
115
+
116
+ // Wrapper that forces HTML namespace for children inside SVG foreignObject.
117
+ // Fixes Vue 2 bug: components inside foreignObject inherit SVG namespace
118
+ // from context.$vnode.ns, causing all child elements to be created as
119
+ // SVGElement instead of HTMLElement.
120
+ const pathFunctions = {
121
+ bezier: getBezierPath,
122
+ straight: getStraightPath,
123
+ step: getStepPath,
124
+ smoothstep: getSmoothStepPath,
125
+ }
126
+
127
+ export default {
128
+ name: 'EdgeWrapper',
129
+ components: { ConnSettingsForm, WPopup },
130
+ inject: { getDefConn: { default: () => () => ({}) } },
131
+ props: {
132
+ conn: { type: Object, required: true },
133
+ sourceX: { type: Number, required: true },
134
+ sourceY: { type: Number, required: true },
135
+ sourcePosition: { type: String, default: 'bottom' },
136
+ targetX: { type: Number, required: true },
137
+ targetY: { type: Number, required: true },
138
+ targetPosition: { type: String, default: 'top' },
139
+ selected: { type: Boolean, default: false },
140
+ interactive: { type: Boolean, default: true },
141
+ locked: { type: Boolean, default: false },
142
+ settingsPopupBackgroundColor: { type: String, default: '#fff' },
143
+ settingsPopupTextColor: { type: String, default: '#333' },
144
+ settingsPopupTextFontSize: { type: String, default: '12px' },
145
+ inforPopupBackgroundColor: { type: String, default: '#fff' },
146
+ inforPopupTitleTextColor: { type: String, default: '#333' },
147
+ inforPopupTitleTextFontSize: { type: String, default: '12px' },
148
+ inforPopupDescriptionTextColor: { type: String, default: '#888' },
149
+ inforPopupDescriptionTextFontSize: { type: String, default: '10px' },
150
+ allNodes: { type: Array, default: () => [] },
151
+ nodeInternals: { type: Object, default: () => ({}) },
152
+ },
153
+ data() {
154
+ return {
155
+ hovered: false,
156
+ infoPopupShow: false,
157
+ infoPopupEditable: true,
158
+ settingsPopupShow: false,
159
+ }
160
+ },
161
+ watch: {
162
+ settingsPopupShow(val) {
163
+ if (val) this.infoPopupShow = false
164
+ },
165
+ infoPopupShow(val) {
166
+ if (val) this.settingsPopupShow = false
167
+ },
168
+ },
169
+ computed: {
170
+ dc() {
171
+ return this.getDefConn()
172
+ },
173
+ classes() {
174
+ const connClasses = this.conn.class
175
+ ? (Array.isArray(this.conn.class) ? this.conn.class : [this.conn.class])
176
+ : []
177
+ return [
178
+ 'vue-flow__edge',
179
+ `vue-flow__edge-${this.conn.type || this.dc.type || 'bezier'}`,
180
+ ...connClasses,
181
+ {
182
+ 'vue-flow__edge--selected': this.selected,
183
+ 'vue-flow__edge--animated': this.conn.animated,
184
+ },
185
+ ]
186
+ },
187
+ pathData() {
188
+ const type = this.conn.type || this.dc.type || 'bezier'
189
+ const fn = pathFunctions[type] || pathFunctions.bezier
190
+ return fn({
191
+ sourceX: this.sourceX,
192
+ sourceY: this.sourceY,
193
+ sourcePosition: this.sourcePosition,
194
+ targetX: this.targetX,
195
+ targetY: this.targetY,
196
+ targetPosition: this.targetPosition,
197
+ curvature: this.conn.curvature,
198
+ allNodes: this.allNodes,
199
+ nodeInternals: this.nodeInternals,
200
+ connFromId: this.conn.from,
201
+ connToId: this.conn.to,
202
+ offset: this.dc.defOffset,
203
+ })
204
+ },
205
+ connStyle() {
206
+ const d = this.dc
207
+ const base = this.conn.style ? { ...this.conn.style } : {}
208
+ base.stroke = this.conn.edgeColor || d.edgeColor || '#b1b1b7'
209
+ if (this.conn.edgeWidth !== undefined) base.strokeWidth = this.conn.edgeWidth
210
+ else if (d.edgeWidth !== undefined) base.strokeWidth = d.edgeWidth
211
+ let dash = this.conn.edgeDasharray !== undefined ? this.conn.edgeDasharray : d.edgeDasharray
212
+ if (dash) base.strokeDasharray = dash
213
+ return base
214
+ },
215
+ markerStartUrl() {
216
+ return this.getMarkerUrl(this.conn.markerStart)
217
+ },
218
+ markerEndUrl() {
219
+ return this.getMarkerUrl(this.conn.markerEnd)
220
+ },
221
+ labelStyle() {
222
+ const d = this.dc
223
+ const s = {}
224
+ const fontSize = this.conn.fontSize || d.fontSize
225
+ const fontColor = this.conn.fontColor || d.fontColor
226
+ if (fontSize) s.fontSize = fontSize + 'px'
227
+ if (fontColor) s.color = fontColor
228
+ return s
229
+ },
230
+ },
231
+ methods: {
232
+ getMarkerUrl(marker) {
233
+ if (!marker) return null
234
+ const config = typeof marker === 'string' ? { type: marker } : marker
235
+ const color = config.color || '#b1b1b7'
236
+ return `url(#vue-flow__${config.type}_${color.replace('#', '')})`
237
+ },
238
+ onGroupMouseEnter(event) {
239
+ this.hovered = true
240
+ this.$emit('conn-mouseenter', { conn: this.conn, event })
241
+ },
242
+ onGroupMouseLeave(event) {
243
+ this.hovered = false
244
+ this.$emit('conn-mouseleave', { conn: this.conn, event })
245
+ },
246
+ onClick(event) {
247
+ this.$emit('conn-click', { conn: this.conn, event })
248
+ },
249
+ onDoubleClick(event) {
250
+ this.$emit('conn-double-click', { conn: this.conn, event })
251
+ },
252
+ onContextMenu(event) {
253
+ this.$emit('conn-context-menu', { conn: this.conn, event })
254
+ },
255
+ onLabelMouseDown(event) {
256
+ this.infoPopupShow = false
257
+ const startX = event.clientX
258
+ const startY = event.clientY
259
+ const onMove = (e) => {
260
+ if (Math.abs(e.clientX - startX) > 2 || Math.abs(e.clientY - startY) > 2) {
261
+ this.infoPopupEditable = false
262
+ document.removeEventListener('mousemove', onMove)
263
+ }
264
+ }
265
+ const onUp = () => {
266
+ document.removeEventListener('mousemove', onMove)
267
+ document.removeEventListener('mouseup', onUp)
268
+ if (!this.infoPopupEditable) {
269
+ setTimeout(() => {
270
+ this.infoPopupEditable = true
271
+ }, 0)
272
+ }
273
+ }
274
+ document.addEventListener('mousemove', onMove)
275
+ document.addEventListener('mouseup', onUp)
276
+ },
277
+ onSettingsUpdate(key, value) {
278
+ this.$emit('conn-settings-update', { conn: this.conn, key, value })
279
+ },
280
+ onSettingsDelete() {
281
+ this.$emit('conn-settings-delete', { conn: this.conn })
282
+ },
283
+ },
284
+ }
285
+ </script>
286
+
287
+ <style scoped>
288
+ /* Only target direct-child paths (edge paths), not SVG paths inside settings icon */
289
+ .vue-flow__edge > path {
290
+ stroke: #b1b1b7;
291
+ stroke-width: 1;
292
+ fill: none;
293
+ pointer-events: none;
294
+ transition: stroke 0.3s ease, filter 0.18s ease;
295
+ }
296
+ .vue-flow__edge-interaction {
297
+ stroke: transparent !important;
298
+ stroke-width: 20 !important;
299
+ fill: none;
300
+ pointer-events: stroke !important;
301
+ cursor: pointer;
302
+ }
303
+ .vue-flow__edge:hover > path {
304
+ stroke: #555;
305
+ }
306
+ .vue-flow__edge--selected > path,
307
+ .vue-flow__edge--selected:hover > path {
308
+ filter: drop-shadow(0 0 2px rgba(220, 38, 38, 0.8)) drop-shadow(0 0 4px rgba(220, 38, 38, 0.5)) drop-shadow(0 0 6px rgba(220, 38, 38, 0.25));
309
+ }
310
+ .vue-flow__edge--animated > path:not(.vue-flow__edge-interaction) {
311
+ stroke-dasharray: 5;
312
+ animation: vue-flow-dash 0.5s linear infinite;
313
+ }
314
+ @keyframes vue-flow-dash {
315
+ to { stroke-dashoffset: -10; }
316
+ }
317
+ .vue-flow__edge-label-area {
318
+ display: flex;
319
+ justify-content: center;
320
+ align-items: center;
321
+ width: 100%;
322
+ height: 100%;
323
+ pointer-events: none;
324
+ }
325
+ .vue-flow__edge-label-group {
326
+ position: relative;
327
+ display: inline-flex;
328
+ align-items: center;
329
+ pointer-events: all;
330
+ }
331
+ .vue-flow__edge-label {
332
+ pointer-events: all;
333
+ cursor: pointer;
334
+ font-family: 'Microsoft JhengHei', '微軟正黑體', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
335
+ font-size: 10px;
336
+ background: #fff;
337
+ padding: 2px 4px;
338
+ border-radius: 2px;
339
+ white-space: nowrap;
340
+ user-select: none;
341
+ text-align: center;
342
+ display: inline-block;
343
+ }
344
+ .vue-flow__edge-settings-anchor {
345
+ position: absolute;
346
+ top: -8px;
347
+ right: -8px;
348
+ z-index: 2;
349
+ pointer-events: all;
350
+ }
351
+ .vue-flow__edge-settings {
352
+ width: 20px;
353
+ height: 20px;
354
+ border-radius: 50%;
355
+ background: #fff;
356
+ border: 1px solid #ccc;
357
+ display: inline-flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ cursor: pointer;
361
+ color: #888;
362
+ pointer-events: all;
363
+ transition: border-color 0.15s ease, background 0.15s ease;
364
+ }
365
+ .vue-flow__edge-settings:hover {
366
+ border-color: #666;
367
+ background: #f0f0f0;
368
+ color: #333;
369
+ }
370
+ /* Fade transition for settings icon */
371
+ .vue-flow__fade-enter-active,
372
+ .vue-flow__fade-leave-active {
373
+ transition: opacity 0.15s ease;
374
+ }
375
+ .vue-flow__fade-enter,
376
+ .vue-flow__fade-leave-to {
377
+ opacity: 0;
378
+ }
379
+ </style>
@@ -0,0 +1,276 @@
1
+ <template>
2
+ <div class="vue-flow__node-basic">
3
+ <Handle
4
+ type="target"
5
+ :position="nodeFromPosition"
6
+ :connectable="connectable"
7
+ :locked="locked"
8
+ :offset="targetOffset"
9
+ :custom-style="targetHandleStyle"
10
+ @connect-start="$emit('connect-start', $event)"
11
+ />
12
+ <div class="vue-flow__node-label" :style="labelStyle">{{ node.name }}</div>
13
+ <Handle
14
+ type="source"
15
+ :position="nodeToPosition"
16
+ :connectable="connectable"
17
+ :locked="locked"
18
+ :offset="sourceOffset"
19
+ :custom-style="sourceHandleStyle"
20
+ @connect-start="$emit('connect-start', $event)"
21
+ />
22
+ </div>
23
+ </template>
24
+
25
+ <script>
26
+ import Handle from './Handle.vue'
27
+
28
+ export default {
29
+ name: 'DefaultNode',
30
+ components: { Handle },
31
+ inject: { getDefNode: { default: () => () => ({}) } },
32
+ props: {
33
+ node: { type: Object, required: true },
34
+ connectable: { type: Boolean, default: true },
35
+ locked: { type: Boolean, default: false },
36
+ },
37
+ computed: {
38
+ dn() {
39
+ return this.getDefNode()
40
+ },
41
+ nodeToPosition() {
42
+ return this.node.toPosition || this.dn.toPosition || 'bottom'
43
+ },
44
+ nodeFromPosition() {
45
+ return this.node.fromPosition || this.dn.fromPosition || 'top'
46
+ },
47
+ sameSide() {
48
+ return this.nodeToPosition === this.nodeFromPosition
49
+ },
50
+ isDiamond() {
51
+ return this.node.shape === 'diamond'
52
+ },
53
+ isEllipse() {
54
+ return this.node.shape === 'ellipse'
55
+ },
56
+ isTriangle() {
57
+ let s = this.node.shape
58
+ return s === 'triangle' || s === 'triangle-right' || s === 'triangle-down' || s === 'triangle-left'
59
+ },
60
+ isSvgShape() {
61
+ return this.isDiamond || this.isEllipse || this.isTriangle
62
+ },
63
+ targetOffset() {
64
+ if (this.isSvgShape) return null
65
+ return this.sameSide ? '33%' : null
66
+ },
67
+ sourceOffset() {
68
+ if (this.isSvgShape) return null
69
+ return this.sameSide ? '67%' : null
70
+ },
71
+ targetHandleStyle() {
72
+ if (this.isTriangle) {
73
+ let pos = this.nodeFromPosition
74
+ return this.getTriangleHandleStyle(pos, this.sameSide ? 0.33 : 0.5)
75
+ }
76
+ if (this.isEllipse && this.sameSide) {
77
+ return this.getEllipseHandleStyle(this.nodeFromPosition, 0.33)
78
+ }
79
+ if (!this.isDiamond || !this.sameSide) return null
80
+ return this.getDiamondOffsetStyle(this.nodeFromPosition, 0.33)
81
+ },
82
+ sourceHandleStyle() {
83
+ if (this.isTriangle) {
84
+ let pos = this.nodeToPosition
85
+ return this.getTriangleHandleStyle(pos, this.sameSide ? 0.67 : 0.5)
86
+ }
87
+ if (this.isEllipse && this.sameSide) {
88
+ return this.getEllipseHandleStyle(this.nodeToPosition, 0.67)
89
+ }
90
+ if (!this.isDiamond || !this.sameSide) return null
91
+ return this.getDiamondOffsetStyle(this.nodeToPosition, 0.67)
92
+ },
93
+ labelStyle() {
94
+ if (!this.isTriangle) return null
95
+ let w = this.node.width || this.dn.width || 150
96
+ let h = this.node.height || this.dn.height || 40
97
+ let s = this.node.shape
98
+ let x = 0
99
+ let y = 0
100
+ if (s === 'triangle-right') x = Math.round(-w / 6)
101
+ else if (s === 'triangle-down') y = Math.round(-h / 6)
102
+ else if (s === 'triangle-left') x = Math.round(w / 6)
103
+ else y = Math.round(h / 6)
104
+ return { transform: `translate(${x}px, ${y}px)` }
105
+ },
106
+ },
107
+ methods: {
108
+ getTriangleHandleStyle(side, ratio) {
109
+ // Get vertices based on triangle direction
110
+ let v = this.triangleVertices()
111
+ let pt = this.pointOnTriangleSide(v, side, ratio)
112
+ return { left: pt.x + '%', top: pt.y + '%', transform: 'translate(-50%, -50%)' }
113
+ },
114
+ triangleVertices() {
115
+ // Returns 3 vertices in % coords: { apex, baseA, baseB }
116
+ // Also returns which sides correspond to 'top','right','bottom','left'
117
+ let s = this.node.shape
118
+ if (s === 'triangle-right') {
119
+ // apex right-center, base on left
120
+ return {
121
+ apex: { x: 100, y: 50 },
122
+ baseA: { x: 0, y: 0 },
123
+ baseB: { x: 0, y: 100 },
124
+ apexSide: 'right',
125
+ baseSide: 'left',
126
+ edgeA: 'top',
127
+ edgeB: 'bottom'
128
+ }
129
+ }
130
+ if (s === 'triangle-down') {
131
+ // apex bottom-center, base on top
132
+ return {
133
+ apex: { x: 50, y: 100 },
134
+ baseA: { x: 0, y: 0 },
135
+ baseB: { x: 100, y: 0 },
136
+ apexSide: 'bottom',
137
+ baseSide: 'top',
138
+ edgeA: 'left',
139
+ edgeB: 'right'
140
+ }
141
+ }
142
+ if (s === 'triangle-left') {
143
+ // apex left-center, base on right
144
+ return {
145
+ apex: { x: 0, y: 50 },
146
+ baseA: { x: 100, y: 0 },
147
+ baseB: { x: 100, y: 100 },
148
+ apexSide: 'left',
149
+ baseSide: 'right',
150
+ edgeA: 'top',
151
+ edgeB: 'bottom'
152
+ }
153
+ }
154
+ // triangle (up): apex top-center, base on bottom
155
+ return {
156
+ apex: { x: 50, y: 0 },
157
+ baseA: { x: 0, y: 100 },
158
+ baseB: { x: 100, y: 100 },
159
+ apexSide: 'top',
160
+ baseSide: 'bottom',
161
+ edgeA: 'left',
162
+ edgeB: 'right'
163
+ }
164
+ },
165
+ pointOnTriangleSide(v, side, ratio) {
166
+ // apexSide: the side with the apex vertex (a single point, but for same-side we sweep across adjacent edges)
167
+ // baseSide: the straight base edge
168
+ // edgeA: the edge from baseA to apex
169
+ // edgeB: the edge from apex to baseB
170
+ if (side === v.apexSide) {
171
+ // Sweep from edgeA near base → apex → edgeB near base
172
+ if (ratio <= 0.5) {
173
+ let t = ratio * 2
174
+ return { x: v.baseA.x + (v.apex.x - v.baseA.x) * t, y: v.baseA.y + (v.apex.y - v.baseA.y) * t }
175
+ }
176
+ else {
177
+ let t2 = (ratio - 0.5) * 2
178
+ return { x: v.apex.x + (v.baseB.x - v.apex.x) * t2, y: v.apex.y + (v.baseB.y - v.apex.y) * t2 }
179
+ }
180
+ }
181
+ if (side === v.baseSide) {
182
+ // Straight line from baseA to baseB
183
+ return { x: v.baseA.x + (v.baseB.x - v.baseA.x) * ratio, y: v.baseA.y + (v.baseB.y - v.baseA.y) * ratio }
184
+ }
185
+ if (side === v.edgeA) {
186
+ // Edge from apex to baseA
187
+ return { x: v.apex.x + (v.baseA.x - v.apex.x) * ratio, y: v.apex.y + (v.baseA.y - v.apex.y) * ratio }
188
+ }
189
+ if (side === v.edgeB) {
190
+ // Edge from apex to baseB
191
+ return { x: v.apex.x + (v.baseB.x - v.apex.x) * ratio, y: v.apex.y + (v.baseB.y - v.apex.y) * ratio }
192
+ }
193
+ return { x: 50, y: 50 }
194
+ },
195
+ getEllipseHandleStyle(side, ratio) {
196
+ // Map ratio (0..1) to an angle on the ellipse quadrant for the given side
197
+ // For 'top': angle goes from PI (left) to 0 (right), ratio 0.5 = top center
198
+ // We position the handle on the ellipse border using parametric coords
199
+ let angle
200
+ switch (side) {
201
+ case 'top':
202
+ angle = Math.PI * (1 - ratio); break
203
+ case 'bottom':
204
+ angle = Math.PI * (ratio - 1); break
205
+ case 'left':
206
+ angle = Math.PI * (0.5 + ratio); break
207
+ case 'right':
208
+ angle = Math.PI * (0.5 - ratio); break
209
+ default:
210
+ angle = 0
211
+ }
212
+ // Ellipse parametric: x = 50% + 50% * cos(angle), y = 50% - 50% * sin(angle)
213
+ let leftPct = 50 + 50 * Math.cos(angle)
214
+ let topPct = 50 - 50 * Math.sin(angle)
215
+ return { left: leftPct + '%', top: topPct + '%', transform: 'translate(-50%, -50%)' }
216
+ },
217
+ getDiamondOffsetStyle(side, ratio) {
218
+ switch (side) {
219
+ case 'top':
220
+ if (ratio <= 0.5) {
221
+ let t = ratio * 2
222
+ return { left: (t * 50) + '%', top: ((1 - t) * 50) + '%', transform: 'translate(-50%, -50%)' }
223
+ }
224
+ else {
225
+ let t2 = (ratio - 0.5) * 2
226
+ return { left: (50 + t2 * 50) + '%', top: (t2 * 50) + '%', transform: 'translate(-50%, -50%)' }
227
+ }
228
+ case 'bottom':
229
+ if (ratio <= 0.5) {
230
+ let t3 = ratio * 2
231
+ return { left: (t3 * 50) + '%', top: (50 + t3 * 50) + '%', transform: 'translate(-50%, -50%)' }
232
+ }
233
+ else {
234
+ let t4 = (ratio - 0.5) * 2
235
+ return { left: (50 + t4 * 50) + '%', top: (100 - t4 * 50) + '%', transform: 'translate(-50%, -50%)' }
236
+ }
237
+ case 'left':
238
+ if (ratio <= 0.5) {
239
+ let t5 = ratio * 2
240
+ return { left: ((1 - t5) * 50) + '%', top: (t5 * 50) + '%', transform: 'translate(-50%, -50%)' }
241
+ }
242
+ else {
243
+ let t6 = (ratio - 0.5) * 2
244
+ return { left: (t6 * 50) + '%', top: (50 + t6 * 50) + '%', transform: 'translate(-50%, -50%)' }
245
+ }
246
+ case 'right':
247
+ if (ratio <= 0.5) {
248
+ let t7 = ratio * 2
249
+ return { left: (50 + t7 * 50) + '%', top: (t7 * 50) + '%', transform: 'translate(-50%, -50%)' }
250
+ }
251
+ else {
252
+ let t8 = (ratio - 0.5) * 2
253
+ return { left: (100 - t8 * 50) + '%', top: (50 + t8 * 50) + '%', transform: 'translate(-50%, -50%)' }
254
+ }
255
+ default:
256
+ return null
257
+ }
258
+ },
259
+ },
260
+ }
261
+ </script>
262
+
263
+ <style scoped>
264
+ .vue-flow__node-basic {
265
+ position: absolute;
266
+ top: 0;
267
+ left: 0;
268
+ right: 0;
269
+ bottom: 0;
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ box-sizing: border-box;
274
+ padding: 10px 20px;
275
+ }
276
+ </style>