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,475 @@
1
+ <template>
2
+ <div
3
+ v-if="!node.hidden"
4
+ :class="classes"
5
+ :style="wrapperStyle"
6
+ :data-id="node.id"
7
+ @mousedown="onMouseDown"
8
+ @mouseup="onMouseUp"
9
+ @mouseenter="onMouseEnter"
10
+ @mouseleave="onMouseLeave"
11
+ @dblclick.stop="onDoubleClick"
12
+ @contextmenu.stop="onContextMenu"
13
+ >
14
+ <WPopup
15
+ v-model="infoPopupShow"
16
+ placement="bottom"
17
+ modeHide="mousedown"
18
+ :editable="infoPopupEditable"
19
+ :minWidth="null"
20
+ :maxWidth="null"
21
+ :autoFitMinWidth="false"
22
+ :autoFitMaxWidth="false"
23
+ :backgroundColor="inforPopupBackgroundColor"
24
+ :textFontSize="inforPopupTitleTextFontSize"
25
+ :paddingStyle="{v:8,h:12}"
26
+ :cmpZIndex="10000"
27
+ >
28
+ <template v-slot:trigger>
29
+ <NodeBody
30
+ :node="node"
31
+ :connectable="connectable"
32
+ :selected="selected"
33
+ :resizable="resizable"
34
+ :locked="locked"
35
+ :hovered="hovered"
36
+ :lastW="cachedW"
37
+ :lastH="cachedH"
38
+ @resize-start="onResizeStart($event.event, $event.edge)"
39
+ @connect-start="onConnectStart"
40
+ />
41
+ </template>
42
+ <template v-slot:content>
43
+ <slot name="node-popup" :node="node">
44
+ <div v-if="node.name || node.description" style="min-width:120px">
45
+ <div v-if="node.name" :style="{ fontSize: inforPopupTitleTextFontSize, color: inforPopupTitleTextColor, fontWeight: 500 }">{{ node.name }}</div>
46
+ <div v-if="node.description" :style="{ fontSize: inforPopupDescriptionTextFontSize, color: inforPopupDescriptionTextColor, marginTop: '4px' }">{{ node.description }}</div>
47
+ </div>
48
+ </slot>
49
+ </template>
50
+ </WPopup>
51
+ <!-- Settings popup -->
52
+ <transition name="vue-flow__fade">
53
+ <div v-if="(hovered || settingsPopupShow) && draggable && !locked" class="vue-flow__node-settings-anchor">
54
+ <WPopup
55
+ v-model="settingsPopupShow"
56
+ placement="right-start"
57
+ modeHide="mousedown"
58
+ :minWidth="null"
59
+ :maxWidth="null"
60
+ :autoFitMinWidth="false"
61
+ :autoFitMaxWidth="false"
62
+ :backgroundColor="settingsPopupBackgroundColor"
63
+ :textColor="settingsPopupTextColor"
64
+ :paddingStyle="{v:8,h:8}"
65
+ :cmpZIndex="10000"
66
+ @show="$emit('node-settings-click', { node: node })"
67
+ >
68
+ <template v-slot:trigger>
69
+ <div class="vue-flow__node-settings">
70
+ <svg viewBox="0 0 20 20" width="14" height="14" fill="currentColor">
71
+ <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)"/>
72
+ </svg>
73
+ </div>
74
+ </template>
75
+ <template v-slot:content>
76
+ <NodeSettingsForm
77
+ :node="node"
78
+ :def-node="dn"
79
+ :text-font-size="settingsPopupTextFontSize"
80
+ @update="onSettingsUpdate"
81
+ @delete="onSettingsDelete"
82
+ />
83
+ </template>
84
+ </WPopup>
85
+ </div>
86
+ </transition>
87
+ </div>
88
+ </template>
89
+
90
+ <script>
91
+ import NodeBody from './NodeBody.vue'
92
+ import NodeSettingsForm from '../ui/NodeSettingsForm.vue'
93
+ import WPopup from 'w-component-vue/src/components/WPopup.vue'
94
+
95
+ export default {
96
+ name: 'NodeWrapper',
97
+ components: { NodeBody, NodeSettingsForm, WPopup },
98
+ inject: { getDefNode: { default: () => () => ({}) } },
99
+ props: {
100
+ node: { type: Object, required: true },
101
+ selected: { type: Boolean, default: false },
102
+ draggable: { type: Boolean, default: true },
103
+ connectable: { type: Boolean, default: true },
104
+ resizable: { type: Boolean, default: true },
105
+ locked: { type: Boolean, default: false },
106
+ settingsPopupBackgroundColor: { type: String, default: '#fff' },
107
+ settingsPopupTextColor: { type: String, default: '#333' },
108
+ settingsPopupTextFontSize: { type: String, default: '12px' },
109
+ inforPopupBackgroundColor: { type: String, default: '#fff' },
110
+ inforPopupTitleTextColor: { type: String, default: '#333' },
111
+ inforPopupTitleTextFontSize: { type: String, default: '12px' },
112
+ inforPopupDescriptionTextColor: { type: String, default: '#888' },
113
+ inforPopupDescriptionTextFontSize: { type: String, default: '10px' },
114
+ snapGridSize: { type: Number, default: null },
115
+ },
116
+ computed: {
117
+ dn() {
118
+ return this.getDefNode()
119
+ },
120
+ isDiamond() {
121
+ return this.node.shape === 'diamond'
122
+ },
123
+ isEllipse() {
124
+ return this.node.shape === 'ellipse'
125
+ },
126
+ isTriangle() {
127
+ let s = this.node.shape
128
+ return s === 'triangle' || s === 'triangle-right' || s === 'triangle-down' || s === 'triangle-left'
129
+ },
130
+ isSvgShape() {
131
+ return this.isDiamond || this.isEllipse || this.isTriangle
132
+ },
133
+ classes() {
134
+ const nodeClasses = this.node.class
135
+ ? (Array.isArray(this.node.class) ? this.node.class : [this.node.class])
136
+ : []
137
+ return [
138
+ 'vue-flow__node',
139
+ `vue-flow__node-${this.node.type || 'basic'}`,
140
+ ...nodeClasses,
141
+ {
142
+ 'vue-flow__node--selected': this.selected,
143
+ 'vue-flow__node--dragging': this.isDragging,
144
+ 'vue-flow__node--locked': this.locked,
145
+ 'vue-flow__node--diamond': this.isDiamond,
146
+ 'vue-flow__node--ellipse': this.isEllipse,
147
+ 'vue-flow__node--triangle': this.isTriangle,
148
+ },
149
+ ]
150
+ },
151
+ wrapperStyle() {
152
+ const d = this.dn
153
+ const n = this.node
154
+ const style = {
155
+ transform: `translate(${n.position.x}px, ${n.position.y}px)`,
156
+ zIndex: n.zIndex || 0,
157
+ ...(n.style || {}),
158
+ }
159
+ if (n.width) style.width = typeof n.width === 'number' ? `${n.width}px` : n.width
160
+ if (n.height) style.height = typeof n.height === 'number' ? `${n.height}px` : n.height
161
+ if (!this.isSvgShape) {
162
+ let fColor = n.faceColor || d.faceColor
163
+ if (fColor) style.background = fColor
164
+ let eColor = n.edgeColor || d.edgeColor
165
+ if (eColor) style.borderColor = eColor
166
+ let eWidth = n.edgeWidth !== undefined ? n.edgeWidth : d.edgeWidth
167
+ if (eWidth !== undefined) style.borderWidth = eWidth + 'px'
168
+ }
169
+ let fs = n.fontSize || d.fontSize
170
+ if (fs) style.fontSize = fs + 'px'
171
+ let fc = n.fontColor || d.fontColor
172
+ if (fc) style.color = fc
173
+ return style
174
+ },
175
+ },
176
+ data() {
177
+ return {
178
+ isDragging: false,
179
+ hovered: false,
180
+ infoPopupShow: false,
181
+ infoPopupEditable: true,
182
+ settingsPopupShow: false,
183
+ cachedW: 0,
184
+ cachedH: 0,
185
+ }
186
+ },
187
+ watch: {
188
+ settingsPopupShow(val) {
189
+ if (val) this.infoPopupShow = false
190
+ },
191
+ infoPopupShow(val) {
192
+ if (val) this.settingsPopupShow = false
193
+ },
194
+ },
195
+ mounted() {
196
+ this.$nextTick(() => this.reportDimensions())
197
+ },
198
+ updated() {
199
+ this.reportDimensions()
200
+ },
201
+ methods: {
202
+ reportDimensions() {
203
+ if (!this.$el) return
204
+ const w = this.$el.offsetWidth
205
+ const h = this.$el.offsetHeight
206
+ if (w === this.cachedW && h === this.cachedH) return
207
+ this.cachedW = w
208
+ this.cachedH = h
209
+ this.$emit('dimensions', { nodeId: this.node.id, width: w, height: h })
210
+ },
211
+ onMouseDown(event) {
212
+ this._mouseDownPos = { x: event.clientX, y: event.clientY }
213
+ if (!this.draggable) return
214
+ if (this.node.dragHandle) {
215
+ const handle = event.target.closest(this.node.dragHandle)
216
+ if (!handle) return
217
+ }
218
+ this.infoPopupShow = false
219
+ this.$emit('drag-start', { node: this.node, event })
220
+ const startX = event.clientX
221
+ const startY = event.clientY
222
+ const onDragMove = (e) => {
223
+ if (Math.abs(e.clientX - startX) > 2 || Math.abs(e.clientY - startY) > 2) {
224
+ this.infoPopupEditable = false
225
+ document.removeEventListener('mousemove', onDragMove)
226
+ }
227
+ }
228
+ const onDragEnd = () => {
229
+ document.removeEventListener('mousemove', onDragMove)
230
+ document.removeEventListener('mouseup', onDragEnd)
231
+ if (!this.infoPopupEditable) {
232
+ setTimeout(() => {
233
+ this.infoPopupEditable = true
234
+ }, 0)
235
+ }
236
+ }
237
+ document.addEventListener('mousemove', onDragMove)
238
+ document.addEventListener('mouseup', onDragEnd)
239
+ },
240
+ onMouseUp(event) {
241
+ if (!this._mouseDownPos) return
242
+ const dx = event.clientX - this._mouseDownPos.x
243
+ const dy = event.clientY - this._mouseDownPos.y
244
+ this._mouseDownPos = null
245
+ if (Math.abs(dx) < 3 && Math.abs(dy) < 3) {
246
+ this.$emit('node-click', { node: this.node, event })
247
+ }
248
+ },
249
+ onDoubleClick(event) {
250
+ this.$emit('node-double-click', { node: this.node, event })
251
+ },
252
+ onContextMenu(event) {
253
+ this.$emit('node-context-menu', { node: this.node, event })
254
+ },
255
+ onConnectStart(payload) {
256
+ this.$emit('connect-start', { ...payload, nodeId: this.node.id })
257
+ },
258
+ onMouseEnter(event) {
259
+ this.hovered = true
260
+ this.$emit('node-mouseenter', { node: this.node, event })
261
+ },
262
+ onMouseLeave(event) {
263
+ this.hovered = false
264
+ this.$emit('node-mouseleave', { node: this.node, event })
265
+ },
266
+ onSettingsUpdate(key, value) {
267
+ this.$emit('node-settings-update', { node: this.node, key, value })
268
+ },
269
+ onSettingsDelete() {
270
+ this.$emit('node-settings-delete', { node: this.node })
271
+ },
272
+ onResizeStart(event, edge) {
273
+ this.infoPopupShow = false
274
+ this.$nextTick(() => {
275
+ this.infoPopupEditable = false
276
+ })
277
+ event.preventDefault()
278
+
279
+ // Lock cursor for the entire drag duration
280
+ const cursorMap = {
281
+ 'top-left': 'nwse-resize',
282
+ 'bottom-right': 'nwse-resize',
283
+ 'top-right': 'nesw-resize',
284
+ 'bottom-left': 'nesw-resize',
285
+ }
286
+ const lockedCursor = cursorMap[edge] || 'default'
287
+ const cursorStyle = document.createElement('style')
288
+ cursorStyle.textContent = '* { cursor: ' + lockedCursor + ' !important; }'
289
+ document.head.appendChild(cursorStyle)
290
+
291
+ const startX = event.clientX
292
+ const startY = event.clientY
293
+ const startW = this.node.width || this.$el.offsetWidth
294
+ const startH = this.node.height || this.$el.offsetHeight
295
+ const startPosX = this.node.position.x
296
+ const startPosY = this.node.position.y
297
+ const snap = this.snapGridSize
298
+ const minSize = snap || 10
299
+ // Get zoom from the viewport transform
300
+ const viewport = this.$el.closest('.vue-flow__viewport')
301
+ const zoom = viewport ? parseFloat(viewport.style.transform.match(/scale\(([^)]+)\)/)?.[1] || 1) : 1
302
+
303
+ const snapVal = (v) => snap ? Math.max(snap, Math.round(v / snap) * snap) : Math.max(minSize, Math.round(v))
304
+
305
+ const resizeRight = (dx) => snapVal(startW + dx)
306
+ const resizeLeft = (dx) => {
307
+ const newW = snapVal(startW - dx)
308
+ return { w: newW, x: startPosX + (startW - newW) }
309
+ }
310
+ const resizeBottom = (dy) => snapVal(startH + dy)
311
+ const resizeTop = (dy) => {
312
+ const newH = snapVal(startH - dy)
313
+ return { h: newH, y: startPosY + (startH - newH) }
314
+ }
315
+
316
+ const onMouseMove = (e) => {
317
+ const dx = (e.clientX - startX) / zoom
318
+ const dy = (e.clientY - startY) / zoom
319
+ let newW = startW; let newH = startH; let newX = startPosX; let newY = startPosY
320
+
321
+ if (edge === 'top-left') {
322
+ let rl2 = resizeLeft(dx); newW = rl2.w; newX = rl2.x
323
+ let rt2 = resizeTop(dy); newH = rt2.h; newY = rt2.y
324
+ }
325
+ else if (edge === 'top-right') {
326
+ newW = resizeRight(dx)
327
+ let rt3 = resizeTop(dy); newH = rt3.h; newY = rt3.y
328
+ }
329
+ else if (edge === 'bottom-left') {
330
+ let rl3 = resizeLeft(dx); newW = rl3.w; newX = rl3.x
331
+ newH = resizeBottom(dy)
332
+ }
333
+ else if (edge === 'bottom-right') {
334
+ newW = resizeRight(dx)
335
+ newH = resizeBottom(dy)
336
+ }
337
+
338
+ this.$emit('node-resize', {
339
+ nodeId: this.node.id,
340
+ width: newW,
341
+ height: newH,
342
+ x: newX,
343
+ y: newY,
344
+ })
345
+ }
346
+
347
+ const onMouseUp = () => {
348
+ document.removeEventListener('mousemove', onMouseMove)
349
+ document.removeEventListener('mouseup', onMouseUp)
350
+ document.head.removeChild(cursorStyle)
351
+ setTimeout(() => {
352
+ this.infoPopupEditable = true
353
+ }, 0)
354
+ this.$emit('node-resize-end', {
355
+ nodeId: this.node.id,
356
+ width: this.node.width || this.$el.offsetWidth,
357
+ height: this.node.height || this.$el.offsetHeight,
358
+ x: this.node.position.x,
359
+ y: this.node.position.y,
360
+ })
361
+ }
362
+
363
+ document.addEventListener('mousemove', onMouseMove)
364
+ document.addEventListener('mouseup', onMouseUp)
365
+ },
366
+ },
367
+ }
368
+ </script>
369
+
370
+ <style scoped>
371
+ .vue-flow__node {
372
+ position: absolute;
373
+ cursor: grab;
374
+ user-select: none;
375
+ pointer-events: all;
376
+ box-sizing: border-box;
377
+ border: 1px solid #bbb;
378
+ border-radius: 3px;
379
+ background: #fff;
380
+ font-family: 'Microsoft JhengHei', '微軟正黑體', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
381
+ font-size: 12px;
382
+ text-align: center;
383
+ white-space: nowrap;
384
+ transition: border-color 0.2s ease, box-shadow 0.3s ease;
385
+ }
386
+ .vue-flow__node:hover {
387
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
388
+ }
389
+ .vue-flow__node--selected {
390
+ box-shadow: 0 0 8px 2px rgba(220, 38, 38, 0.5);
391
+ }
392
+ .vue-flow__node--selected:hover {
393
+ box-shadow: 0 0 8px 2px rgba(220, 38, 38, 0.5);
394
+ }
395
+ .vue-flow__node--dragging {
396
+ cursor: grabbing;
397
+ z-index: 1000 !important;
398
+ }
399
+
400
+ /* Settings icon anchor (positioning only) */
401
+ .vue-flow__node-settings-anchor {
402
+ position: absolute;
403
+ top: -8px;
404
+ right: -8px;
405
+ z-index: 2;
406
+ pointer-events: all;
407
+ }
408
+ /* Settings icon (appearance only) */
409
+ .vue-flow__node-settings {
410
+ width: 20px;
411
+ height: 20px;
412
+ border-radius: 50%;
413
+ background: #fff;
414
+ border: 1px solid #ccc;
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: center;
418
+ cursor: pointer;
419
+ transition: border-color 0.15s ease, background 0.15s ease;
420
+ color: #888;
421
+ }
422
+ .vue-flow__node-settings:hover {
423
+ border-color: #666;
424
+ background: #f0f0f0;
425
+ color: #333;
426
+ }
427
+
428
+
429
+ /* Shared SVG shape base styles */
430
+ .vue-flow__node--diamond,
431
+ .vue-flow__node--ellipse,
432
+ .vue-flow__node--triangle {
433
+ background: transparent !important;
434
+ border-color: transparent !important;
435
+ border-radius: 0 !important;
436
+ }
437
+ .vue-flow__node--diamond:hover,
438
+ .vue-flow__node--ellipse:hover,
439
+ .vue-flow__node--triangle:hover {
440
+ box-shadow: none !important;
441
+ }
442
+ .vue-flow__node--diamond.vue-flow__node--selected,
443
+ .vue-flow__node--ellipse.vue-flow__node--selected,
444
+ .vue-flow__node--triangle.vue-flow__node--selected {
445
+ border-color: transparent !important;
446
+ box-shadow: none !important;
447
+ }
448
+ .vue-flow__node--diamond.vue-flow__node--selected:hover,
449
+ .vue-flow__node--ellipse.vue-flow__node--selected:hover,
450
+ .vue-flow__node--triangle.vue-flow__node--selected:hover {
451
+ border-color: transparent !important;
452
+ box-shadow: none !important;
453
+ }
454
+ /* SVG shape hover */
455
+ .vue-flow__node--diamond:hover .vue-flow__shape-svg polygon,
456
+ .vue-flow__node--triangle:hover .vue-flow__shape-svg polygon,
457
+ .vue-flow__node--ellipse:hover .vue-flow__shape-svg ellipse {
458
+ filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.15));
459
+ }
460
+ /* SVG shape selected: red shadow */
461
+ .vue-flow__node--diamond.vue-flow__node--selected .vue-flow__shape-svg polygon,
462
+ .vue-flow__node--triangle.vue-flow__node--selected .vue-flow__shape-svg polygon,
463
+ .vue-flow__node--ellipse.vue-flow__node--selected .vue-flow__shape-svg ellipse {
464
+ filter: drop-shadow(0 0 6px rgba(220, 38, 38, 0.6));
465
+ }
466
+ /* Fade transition for settings icon and resize handles */
467
+ .vue-flow__fade-enter-active,
468
+ .vue-flow__fade-leave-active {
469
+ transition: opacity 0.15s ease;
470
+ }
471
+ .vue-flow__fade-enter,
472
+ .vue-flow__fade-leave-to {
473
+ opacity: 0;
474
+ }
475
+ </style>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div class="vue-flow__node-output">
3
+ <Handle
4
+ type="target"
5
+ :position="node.fromPosition || dn.fromPosition || 'top'"
6
+ :connectable="connectable"
7
+ :locked="locked"
8
+ @connect-start="$emit('connect-start', $event)"
9
+ />
10
+ <div class="vue-flow__node-label">{{ node.name }}</div>
11
+ </div>
12
+ </template>
13
+
14
+ <script>
15
+ import Handle from './Handle.vue'
16
+
17
+ export default {
18
+ name: 'OutputNode',
19
+ components: { Handle },
20
+ inject: { getDefNode: { default: () => () => ({}) } },
21
+ props: {
22
+ node: { type: Object, required: true },
23
+ connectable: { type: Boolean, default: true },
24
+ locked: { type: Boolean, default: false },
25
+ },
26
+ computed: {
27
+ dn() {
28
+ return this.getDefNode()
29
+ },
30
+ },
31
+ }
32
+ </script>
33
+
34
+ <style scoped>
35
+ .vue-flow__node-output {
36
+ position: absolute;
37
+ top: 0;
38
+ left: 0;
39
+ right: 0;
40
+ bottom: 0;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ box-sizing: border-box;
45
+ padding: 10px 20px;
46
+ }
47
+ </style>
@@ -0,0 +1,158 @@
1
+ <template>
2
+ <div class="vue-flow__settings-form" :style="formStyle">
3
+ <label>Name
4
+ <input type="text" :value="conn.name || ''" @input="$emit('update', 'name', $event.target.value)">
5
+ </label>
6
+ <label>Description
7
+ <input type="text" :value="conn.description || ''" @input="$emit('update', 'description', $event.target.value)">
8
+ </label>
9
+ <label>Type
10
+ <select :value="conn.type || defConn.type" @input="$emit('update', 'type', $event.target.value)">
11
+ <option value="bezier">Bezier</option>
12
+ <option value="straight">Straight</option>
13
+ <option value="step">Step</option>
14
+ <option value="smoothstep">Smooth Step</option>
15
+ </select>
16
+ </label>
17
+ <label>Font Size
18
+ <input type="number" :value="conn.fontSize || defConn.fontSize" :min="defConn.fontSizeMin" :max="defConn.fontSizeMax" @input="onFontSizeInput($event.target.value)">
19
+ </label>
20
+ <label>Font Color
21
+ <WColorSelect :value="conn.fontColor || defConn.fontColor" :size="160" :colorBlockSize="16" :showColorText="false" @input="$emit('update', 'fontColor', $event)" />
22
+ </label>
23
+ <label>Animated
24
+ <input type="checkbox" :checked="!!conn.animated" @change="$emit('update', 'animated', $event.target.checked)">
25
+ </label>
26
+ <label>Edge Color
27
+ <WColorSelect :value="conn.edgeColor || defConn.edgeColor" :size="160" :colorBlockSize="16" :showColorText="false" @input="$emit('update', 'edgeColor', $event)" />
28
+ </label>
29
+ <label>Edge Width
30
+ <input type="number" :value="conn.edgeWidth !== undefined ? conn.edgeWidth : defConn.edgeWidth" min="1" max="24" @input="onEdgeWidthInput($event.target.value)">
31
+ </label>
32
+ <label>Marker End
33
+ <select :value="conn.markerEnd || defConn.markerEnd" @input="$emit('update', 'markerEnd', $event.target.value || undefined)">
34
+ <option value="">None</option>
35
+ <option value="arrow">Arrow</option>
36
+ <option value="arrowclosed">Arrow Closed</option>
37
+ </select>
38
+ </label>
39
+ <div class="vue-flow__delete-area">
40
+ <button v-if="!confirmDelete" class="vue-flow__delete-btn" @click="confirmDelete = true">刪除連接線</button>
41
+ <template v-else>
42
+ <span class="vue-flow__delete-warn">確定要刪除此連接線?</span>
43
+ <div class="vue-flow__delete-confirm-row">
44
+ <button class="vue-flow__delete-btn vue-flow__delete-btn--confirm" @click="$emit('delete')">確認刪除</button>
45
+ <button class="vue-flow__delete-btn vue-flow__delete-btn--cancel" @click="confirmDelete = false">取消</button>
46
+ </div>
47
+ </template>
48
+ </div>
49
+ </div>
50
+ </template>
51
+
52
+ <script>
53
+ import WColorSelect from 'w-component-vue/src/components/WColorSelect.vue'
54
+
55
+ export default {
56
+ components: { WColorSelect },
57
+ props: {
58
+ conn: { type: Object, required: true },
59
+ defConn: { type: Object, required: true },
60
+ textFontSize: { type: String, default: '' },
61
+ },
62
+ data() {
63
+ return { confirmDelete: false }
64
+ },
65
+ computed: {
66
+ formStyle() {
67
+ let s = {}
68
+ if (this.textFontSize) s.fontSize = this.textFontSize
69
+ return s
70
+ },
71
+ },
72
+ methods: {
73
+ onFontSizeInput(val) {
74
+ let n = Number(val)
75
+ let d = this.defConn
76
+ if (!val || isNaN(n) || n < d.fontSizeMin) return
77
+ if (n > d.fontSizeMax) n = d.fontSizeMax
78
+ this.$emit('update', 'fontSize', n)
79
+ },
80
+ onEdgeWidthInput(val) {
81
+ let n = Number(val)
82
+ if (!val || isNaN(n) || n < 1) return
83
+ if (n > 24) n = 24
84
+ this.$emit('update', 'edgeWidth', n)
85
+ },
86
+ },
87
+ }
88
+ </script>
89
+
90
+ <style>
91
+ .vue-flow__settings-form {
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: 8px;
95
+ min-width: 180px;
96
+ }
97
+ .vue-flow__settings-form label {
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
101
+ gap: 8px;
102
+ font-size: 12px;
103
+ }
104
+ .vue-flow__settings-form select,
105
+ .vue-flow__settings-form input[type="number"],
106
+ .vue-flow__settings-form input[type="text"] {
107
+ width: 100px;
108
+ font-size: 12px;
109
+ padding: 1px 4px;
110
+ border: 1px solid #ccc;
111
+ border-radius: 3px;
112
+ }
113
+ .vue-flow__settings-form input[type="color"] {
114
+ width: 32px;
115
+ height: 24px;
116
+ padding: 0;
117
+ border: 1px solid #ccc;
118
+ cursor: pointer;
119
+ flex-shrink: 0;
120
+ }
121
+ .vue-flow__delete-area {
122
+ margin-top: 4px;
123
+ padding-top: 8px;
124
+ border-top: 1px solid #eee;
125
+ }
126
+ .vue-flow__delete-warn {
127
+ display: block;
128
+ font-size: 11px;
129
+ color: #c00;
130
+ margin-bottom: 4px;
131
+ }
132
+ .vue-flow__delete-confirm-row {
133
+ display: flex;
134
+ gap: 6px;
135
+ }
136
+ .vue-flow__delete-btn {
137
+ padding: 3px 10px;
138
+ font-size: 11px;
139
+ border: 1px solid #ccc;
140
+ border-radius: 3px;
141
+ background: #fff;
142
+ cursor: pointer;
143
+ }
144
+ .vue-flow__delete-btn:hover {
145
+ background: #f5f5f5;
146
+ }
147
+ .vue-flow__delete-btn--confirm {
148
+ color: #fff;
149
+ background: #dc2626;
150
+ border-color: #dc2626;
151
+ }
152
+ .vue-flow__delete-btn--confirm:hover {
153
+ background: #b91c1c;
154
+ }
155
+ .vue-flow__delete-btn--cancel {
156
+ color: #666;
157
+ }
158
+ </style>