w-flow-vue 1.0.21 → 1.0.23

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.
@@ -43,7 +43,7 @@
43
43
  />
44
44
 
45
45
  <!-- w-flow-vue -->
46
- <script src="https://cdn.jsdelivr.net/npm/w-flow-vue@1.0.21/dist/w-flow-vue.umd.js"></script>
46
+ <script src="https://cdn.jsdelivr.net/npm/w-flow-vue@1.0.23/dist/w-flow-vue.umd.js"></script>
47
47
 
48
48
  <!-- lodash -->
49
49
  <script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script>
@@ -43,7 +43,7 @@
43
43
  />
44
44
 
45
45
  <!-- w-flow-vue -->
46
- <script src="https://cdn.jsdelivr.net/npm/w-flow-vue@1.0.21/dist/w-flow-vue.umd.js"></script>
46
+ <script src="https://cdn.jsdelivr.net/npm/w-flow-vue@1.0.23/dist/w-flow-vue.umd.js"></script>
47
47
 
48
48
  <!-- lodash -->
49
49
  <script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script>
package/docs/global.html CHANGED
@@ -2354,7 +2354,7 @@ Used when nodes are too close for OrthConnector (tooShort).</p></li></ul></dd>
2354
2354
  <br class="clear">
2355
2355
 
2356
2356
  <footer>
2357
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 22 2026 22:01:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
2357
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Aug 23 2026 15:03:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
2358
2358
  </footer>
2359
2359
 
2360
2360
  <script>prettyPrint();</script>
package/docs/index.html CHANGED
@@ -71,7 +71,7 @@
71
71
  <br class="clear">
72
72
 
73
73
  <footer>
74
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 22 2026 22:01:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Aug 23 2026 15:03:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
75
75
  </footer>
76
76
 
77
77
  <script>prettyPrint();</script>
@@ -92,7 +92,7 @@ export const CONN_DEFAULTS = {
92
92
  <br class="clear">
93
93
 
94
94
  <footer>
95
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 22 2026 22:01:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
95
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Aug 23 2026 15:03:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
96
96
  </footer>
97
97
 
98
98
  <script>prettyPrint();</script>
@@ -342,7 +342,7 @@ function labelAtHalfLength(pts) {
342
342
  <br class="clear">
343
343
 
344
344
  <footer>
345
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 22 2026 22:01:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
345
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Aug 23 2026 15:03:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
346
346
  </footer>
347
347
 
348
348
  <script>prettyPrint();</script>
@@ -85,7 +85,7 @@ export default fixSvgNs
85
85
  <br class="clear">
86
86
 
87
87
  <footer>
88
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 22 2026 22:01:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
88
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Aug 23 2026 15:03:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
89
89
  </footer>
90
90
 
91
91
  <script>prettyPrint();</script>
@@ -285,7 +285,7 @@ export function snapPosition(position, gridSize) {
285
285
  <br class="clear">
286
286
 
287
287
  <footer>
288
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 22 2026 22:01:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
288
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Aug 23 2026 15:03:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
289
289
  </footer>
290
290
 
291
291
  <script>prettyPrint();</script>
@@ -96,7 +96,7 @@ export function generateId() {
96
96
  <br class="clear">
97
97
 
98
98
  <footer>
99
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 22 2026 22:01:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
99
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Aug 23 2026 15:03:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
100
100
  </footer>
101
101
 
102
102
  <script>prettyPrint();</script>
@@ -333,7 +333,7 @@ function segmentFallback(sx, sy, tx, ty, sObs, tObs) {
333
333
  <br class="clear">
334
334
 
335
335
  <footer>
336
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 22 2026 22:01:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
336
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Aug 23 2026 15:03:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
337
337
  </footer>
338
338
 
339
339
  <script>prettyPrint();</script>
@@ -4186,7 +4186,7 @@ Any value of the wrong type (or an empty string) falls back to the default liste
4186
4186
  <br class="clear">
4187
4187
 
4188
4188
  <footer>
4189
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Aug 22 2026 22:01:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
4189
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Aug 23 2026 15:03:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
4190
4190
  </footer>
4191
4191
 
4192
4192
  <script>prettyPrint();</script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "w-flow-vue",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "main": "dist/w-flow-vue.umd.js",
5
5
  "dependencies": {
6
6
  "w-component-vue": "^2.5.2",
@@ -74,6 +74,7 @@
74
74
  :snap-grid-size="snapToGrid ? snapGridSize : null"
75
75
  :settings-enabled="nodesSettingsEnabled"
76
76
  :settings-excludes="nodesSettingsExcludes"
77
+ @drag-prepare="onNodeDragPrepare"
77
78
  @drag-start="onNodeDragStart"
78
79
  @node-click="onNodeClick"
79
80
  @node-double-click="onNodeDoubleClick"
@@ -377,6 +378,7 @@ export default {
377
378
  document.addEventListener('keyup', this.onKeyUp)
378
379
  document.addEventListener('mousemove', this.onDocMouseMove)
379
380
  document.addEventListener('mouseup', this.onDocMouseUp)
381
+ window.addEventListener('blur', this.onWindowBlur)
380
382
  },
381
383
  beforeDestroy() {
382
384
  if (this._panAnimId) {
@@ -387,6 +389,7 @@ export default {
387
389
  document.removeEventListener('keyup', this.onKeyUp)
388
390
  document.removeEventListener('mousemove', this.onDocMouseMove)
389
391
  document.removeEventListener('mouseup', this.onDocMouseUp)
392
+ window.removeEventListener('blur', this.onWindowBlur)
390
393
  },
391
394
  computed: {
392
395
  widthInp() {
@@ -766,6 +769,8 @@ export default {
766
769
  },
767
770
  onCanvasWheel(event) {
768
771
  if (!this.zoomOnScroll) return
772
+ //滾輪縮放亦為viewport寫入者, 先取消程式動畫避免二者互相覆寫
773
+ this.cancelViewportAnimation()
769
774
  const delta = -event.deltaY * 0.001
770
775
  const currentZoom = this.viewport.zoom
771
776
  // fitView may set zoom below zoomMin; use the current level as the
@@ -789,6 +794,14 @@ export default {
789
794
 
790
795
  // --- Document-level mouse ---
791
796
  onDocMouseMove(event) {
797
+ //按鍵已放開卻仍處於進行中狀態: 代表收尾事件未送達(於瀏覽器視窗外放開/視窗失焦/手勢被原生拖曳接管),
798
+ //此時須主動走既有收尾路徑清除狀態; 只return不清狀態無效, 旗標續留為真下次移動仍會誤判
799
+ if ((event.buttons & 1) === 0) {
800
+ if (this.isPanning || this.isDraggingNode || this.isConnecting || this.isSelecting) {
801
+ this.onDocMouseUp(event)
802
+ }
803
+ return
804
+ }
792
805
  if (this.isPanning) {
793
806
  this.doPan(event)
794
807
  }
@@ -802,6 +815,19 @@ export default {
802
815
  this.doSelection(event)
803
816
  }
804
817
  },
818
+ onWindowBlur(event) {
819
+ //視窗失焦後不會再收到mouseup與keyup, 於此統一收尾避免狀態黏住
820
+ if (this.isPanning) {
821
+ this.endPan()
822
+ }
823
+ //節點拖曳同樣須收尾: 否則isDraggingNode殘留為true, 回到視窗後在觸發mousemove守衛前
824
+ //點擊其他節點, 該次node-click會被onNodeClick之拖曳守衛以過期狀態誤擋
825
+ //event為blur事件(非MouseEvent), endDrag不讀取座標僅原樣轉交node-drag-stop, 故不偽造mouseup
826
+ if (this.isDraggingNode) {
827
+ this.endDrag(event)
828
+ }
829
+ this.keysPressed = {}
830
+ },
805
831
  onDocMouseUp(event) {
806
832
  if (this.isPanning) {
807
833
  this.endPan()
@@ -819,6 +845,8 @@ export default {
819
845
 
820
846
  // --- Pan ---
821
847
  startPan(event) {
848
+ //手動平移優先於程式動畫, 否則二者同時寫viewport而互相覆寫
849
+ this.cancelViewportAnimation()
822
850
  this.isPanning = true
823
851
  this.panStartPos = { x: event.clientX, y: event.clientY }
824
852
  },
@@ -846,16 +874,20 @@ export default {
846
874
  },
847
875
 
848
876
  // --- Node drag ---
849
- onNodeDragStart({ node, event }) {
877
+ //於節點mousedown當下先行選取(拖曳本身延後至跨越位移門檻才由onNodeDragStart啟動)。
878
+ //守衛與選取條件皆與修正前之onNodeDragStart一致, 故選取時機不因拖曳延後而改變。
879
+ onNodeDragPrepare({ node }) {
850
880
  if (this.locked || !this.nodesDraggable) return
851
- this.isDraggingNode = true
852
- this.draggingNodeId = node.id
853
- this.dragStartPos = { x: event.clientX, y: event.clientY }
854
-
855
881
  if (this.selectNodesOnDrag && !this.isMultiSelectPressed) {
856
882
  this.setSelectedNodes([node.id])
857
883
  this.setSelectedConns([])
858
884
  }
885
+ },
886
+ onNodeDragStart({ node, event, moveEvent }) {
887
+ if (this.locked || !this.nodesDraggable) return
888
+ this.isDraggingNode = true
889
+ this.draggingNodeId = node.id
890
+ this.dragStartPos = { x: event.clientX, y: event.clientY }
859
891
 
860
892
  // Cache start positions for drag
861
893
  const starts = {}
@@ -878,6 +910,12 @@ export default {
878
910
  }
879
911
 
880
912
  this.$emit('node-drag-start', { node, event })
913
+
914
+ //立即套用跨門檻的這一次mousemove: document層之onDocMouseMove於本輪事件已先看過
915
+ //(當時isDraggingNode仍為false而返回), 不補這一次則跨門檻後立刻放開將完全沒有位移
916
+ if (moveEvent && this.isDraggingNode) {
917
+ this.doDrag(moveEvent)
918
+ }
881
919
  },
882
920
  doDrag(event) {
883
921
  const zoom = this.viewport.zoom
@@ -1014,6 +1052,10 @@ export default {
1014
1052
 
1015
1053
  // --- Selection ---
1016
1054
  onNodeClick({ node, event }) {
1055
+ //本次手勢已被接受為拖曳者不再視為點擊: NodeWrapper之@mouseup綁在節點元素上,
1056
+ //必先於document層之onDocMouseUp→endDrag觸發, 故此刻isDraggingNode仍為true即代表拖曳成立。
1057
+ //(涵蓋「跨門檻拖出後又移回原點放開」之情形——該情形最終位移為0, 僅靠距離判準無法辨識)
1058
+ if (this.isDraggingNode) return
1017
1059
  if (!this.elementsSelectable) return
1018
1060
  if (this.isMultiSelectPressed) {
1019
1061
  const idx = this.selectedNodes.indexOf(node.id)
@@ -1252,6 +1294,19 @@ export default {
1252
1294
  emitViewportChange() {
1253
1295
  this.$emit('viewport-change', { ...this.viewport })
1254
1296
  },
1297
+ getViewport() {
1298
+ //即時視口: viewport-change僅於手勢結束才發出, 平移途中呼叫端需要當下值時走此方法
1299
+ //(呼叫端若以viewport-change快取之值為基準回寫setViewport, 會於平移途中以過期值覆寫使用者正在進行的平移)
1300
+ let vp = this.viewport
1301
+ return { x: vp.x, y: vp.y, zoom: vp.zoom }
1302
+ },
1303
+ cancelViewportAnimation() {
1304
+ //取消進行中之視口動畫(panToNode), 否則動畫與手動手勢會同時寫入同一個viewport
1305
+ if (this._panAnimId) {
1306
+ cancelAnimationFrame(this._panAnimId)
1307
+ this._panAnimId = null
1308
+ }
1309
+ },
1255
1310
  emitSelectionChange() {
1256
1311
  this.$emit('selection-change', this.getSelectedElements())
1257
1312
  },
@@ -21,6 +21,7 @@
21
21
  :snap-grid-size="snapGridSize"
22
22
  :settings-enabled="settingsEnabled"
23
23
  :settings-excludes="settingsExcludes"
24
+ @drag-prepare="$emit('drag-prepare', $event)"
24
25
  @drag-start="$emit('drag-start', $event)"
25
26
  @node-click="$emit('node-click', $event)"
26
27
  @node-double-click="$emit('node-double-click', $event)"
@@ -208,6 +208,12 @@ export default {
208
208
  updated() {
209
209
  this.reportDimensions()
210
210
  },
211
+ beforeDestroy() {
212
+ //拖曳/縮放進行中被銷毀時, 掛在document上的監聽器與插入document.head之全域游標樣式
213
+ //都不會自行移除, 於此一併收尾
214
+ this.endMouseGesture(false)
215
+ this.endResizeGesture(false)
216
+ },
211
217
  methods: {
212
218
  reportDimensions() {
213
219
  if (!this.$el) return
@@ -218,13 +224,57 @@ export default {
218
224
  this.cachedH = h
219
225
  this.$emit('dimensions', { nodeId: this.node.id, width: w, height: h })
220
226
  },
227
+ //收掉本次mousedown手勢之document監聽與暫存態; restorePopup=false供beforeDestroy使用(元件將銷毀不需再排程還原)
228
+ endMouseGesture(restorePopup = true) {
229
+ const g = this._mouseGesture
230
+ if (!g) return
231
+ this._mouseGesture = null
232
+ this._mouseDownPos = null
233
+ document.removeEventListener('mousemove', g.onDragMove)
234
+ document.removeEventListener('mouseup', g.onDragEnd)
235
+ window.removeEventListener('blur', g.onDragEnd)
236
+ if (restorePopup && !this.infoPopupEditable) {
237
+ setTimeout(() => {
238
+ this.infoPopupEditable = true
239
+ }, 0)
240
+ }
241
+ },
242
+ //收掉本次resize手勢之document監聽與插入document.head之全域游標樣式;
243
+ //回傳是否確實收到一個進行中之手勢(供onMouseUp判斷該不該再發node-resize-end)
244
+ //why: 全域樣式為`* { cursor: X !important; }`, 若因元件銷毀或視窗外放開而未移除,
245
+ // 整頁每個元素都會卡在該游標直到重新整理(已於jsdom確定性重現: 銷毀後樣式仍留在head)
246
+ endResizeGesture(restorePopup = true) {
247
+ const g = this._resizeGesture
248
+ if (!g) return false
249
+ this._resizeGesture = null
250
+ document.removeEventListener('mousemove', g.onMouseMove)
251
+ document.removeEventListener('mouseup', g.onMouseUp)
252
+ window.removeEventListener('blur', g.onMouseUp)
253
+ if (g.cursorStyle && g.cursorStyle.parentNode) {
254
+ g.cursorStyle.parentNode.removeChild(g.cursorStyle)
255
+ }
256
+ if (restorePopup) {
257
+ setTimeout(() => {
258
+ this.infoPopupEditable = true
259
+ }, 0)
260
+ }
261
+ return true
262
+ },
221
263
  onMouseDown(event) {
264
+ //新手勢開始前先收掉上一次殘留者(如上次mouseup落在視窗外未送達document), 避免監聽器疊加
265
+ this.endMouseGesture()
222
266
  //點設定齒輪(及其popup錨區)不啟動節點拖曳/點擊: 齒輪刻意不用@mousedown.stop(stopPropagation會擋掉window層WPopup互斥協調),
223
267
  //故於此明確排除, 使點齒輪只開設定popup、不移動節點座標(避免尚未變更設定就先改到座標→誤觸發變更儲存)
224
268
  if (event.target.closest && event.target.closest('.vue-flow__node-settings-anchor')) {
225
269
  this._mouseDownPos = null
226
270
  return
227
271
  }
272
+ //非主鍵(右鍵/中鍵)不啟動拖曳與點擊: 判準對齊畫布平移之onCanvasMouseDown(event.button === 0),
273
+ //使同套件內「按下開始拖動」採同一套判準; contextmenu走@contextmenu.stop另一條路徑不受影響
274
+ if (event.button !== 0) {
275
+ this._mouseDownPos = null
276
+ return
277
+ }
228
278
  this._mouseDownPos = { x: event.clientX, y: event.clientY }
229
279
  if (!this.draggable) return
230
280
  if (this.node.dragHandle) {
@@ -232,26 +282,40 @@ export default {
232
282
  if (!handle) return
233
283
  }
234
284
  this.infoPopupShow = false
235
- this.$emit('drag-start', { node: this.node, event })
285
+ //選取仍於mousedown完成(與修正前一致): 拖曳延後但選取不可延後,
286
+ //否則按住節點未移動時, 原本立即出現的選取回饋會拖到mouseup才出現
287
+ this.$emit('drag-prepare', { node: this.node, event })
236
288
  const startX = event.clientX
237
289
  const startY = event.clientY
238
- const onDragMove = (e) => {
290
+ const gesture = { crossed: false, onDragMove: null, onDragEnd: null }
291
+ gesture.onDragMove = (e) => {
292
+ //mouseup落在視窗外時不會送達document, 監聽器會殘留;
293
+ //此處確認主鍵仍按著, 否則於無按鍵狀態下跨門檻會誤啟動拖曳(幽靈拖曳)
294
+ if ((e.buttons & 1) === 0) {
295
+ this.endMouseGesture()
296
+ return
297
+ }
298
+ if (gesture.crossed) return
239
299
  if (Math.abs(e.clientX - startX) > 2 || Math.abs(e.clientY - startY) > 2) {
300
+ gesture.crossed = true
240
301
  this.infoPopupEditable = false
241
- document.removeEventListener('mousemove', onDragMove)
302
+ //拖曳延後至跨越位移門檻才啟動
303
+ //why: 若於mousedown即emit drag-start, 無位移之純點擊於mouseup仍會走endDrag→回寫座標→emit update:nodes,
304
+ // 宿主收到未變更之全量節點而誤判有未儲存變更
305
+ //event傳原始mousedown事件, 使WFlowVue之dragStartPos取按下當下座標(改傳本次mousemove會少算門檻位移);
306
+ //moveEvent供WFlowVue於啟動當下立即補跑一次doDrag, 否則跨門檻後立刻放開會完全沒有位移
307
+ this.$emit('drag-start', { node: this.node, event, moveEvent: e })
242
308
  }
309
+ //刻意不移除mousemove: 後續仍需靠它做buttons清理, crossed旗標已保證不會重複啟動
243
310
  }
244
- const onDragEnd = () => {
245
- document.removeEventListener('mousemove', onDragMove)
246
- document.removeEventListener('mouseup', onDragEnd)
247
- if (!this.infoPopupEditable) {
248
- setTimeout(() => {
249
- this.infoPopupEditable = true
250
- }, 0)
251
- }
311
+ gesture.onDragEnd = () => {
312
+ this.endMouseGesture()
252
313
  }
253
- document.addEventListener('mousemove', onDragMove)
254
- document.addEventListener('mouseup', onDragEnd)
314
+ this._mouseGesture = gesture
315
+ document.addEventListener('mousemove', gesture.onDragMove)
316
+ document.addEventListener('mouseup', gesture.onDragEnd)
317
+ //視窗失焦後收不到mouseup, 本地監聽與popup態同樣需收尾(WFlowVue之onWindowBlur只收父層狀態)
318
+ window.addEventListener('blur', gesture.onDragEnd)
255
319
  },
256
320
  onMouseUp(event) {
257
321
  if (!this._mouseDownPos) return
@@ -376,12 +440,7 @@ export default {
376
440
  let lastY = startPosY
377
441
 
378
442
  const onMouseUp = () => {
379
- document.removeEventListener('mousemove', onMouseMove)
380
- document.removeEventListener('mouseup', onMouseUp)
381
- document.head.removeChild(cursorStyle)
382
- setTimeout(() => {
383
- this.infoPopupEditable = true
384
- }, 0)
443
+ if (!this.endResizeGesture()) return
385
444
  this.$emit('node-resize-end', {
386
445
  nodeId: this.node.id,
387
446
  width: lastW,
@@ -391,8 +450,11 @@ export default {
391
450
  })
392
451
  }
393
452
 
453
+ this._resizeGesture = { onMouseMove, onMouseUp, cursorStyle }
394
454
  document.addEventListener('mousemove', onMouseMove)
395
455
  document.addEventListener('mouseup', onMouseUp)
456
+ //視窗失焦後收不到mouseup, 全域游標樣式與監聽器同樣需收尾
457
+ window.addEventListener('blur', onMouseUp)
396
458
  },
397
459
  },
398
460
  }
@@ -122,10 +122,12 @@ describe('ConnSettingsForm', () => {
122
122
  })
123
123
 
124
124
  // 3. Node Resize
125
+ //縮放ghost已由獨立之 resizeOverlay 改為併入 dragPositions 之 per-key 機制(WFlowVue.onNodeResize),
126
+ //斷言意圖不變: 縮放中ghost帶有該次尺寸/座標, 結束或被鎖定時不留ghost
125
127
  describe('Node Resize', () => {
126
128
  test('onNodeResize sets overlay', () => {
127
129
  const w = createWrapper(); w.vm.onNodeResize({ nodeId: '1', width: 200, height: 80, x: 50, y: 50 })
128
- expect(w.vm.resizeOverlay).toEqual({ id: '1', width: 200, height: 80, x: 50, y: 50 }); w.destroy()
130
+ expect(w.vm.dragPositions['1']).toEqual({ x: 50, y: 50, width: 200, height: 80 }); w.destroy()
129
131
  })
130
132
  test('onNodeResizeEnd updates node', () => {
131
133
  const w = createWrapper()
@@ -134,12 +136,12 @@ describe('Node Resize', () => {
134
136
  const n = w.vm.nodeById('1')
135
137
  expect(n.width).toBe(200); expect(n.height).toBe(80)
136
138
  expect(n.position.x).toBe(60); expect(n.position.y).toBe(70)
137
- expect(w.vm.resizeOverlay).toBeNull(); w.destroy()
139
+ expect(w.vm.dragPositions['1']).toBeNull(); w.destroy()
138
140
  })
139
141
  test('blocked when locked', () => {
140
142
  const w = createWrapper(); w.vm.toggleInteractive()
141
143
  w.vm.onNodeResize({ nodeId: '1', width: 200, height: 80, x: 50, y: 50 })
142
- expect(w.vm.resizeOverlay).toBeNull(); w.destroy()
144
+ expect(w.vm.dragPositions['1']).toBeFalsy(); w.destroy()
143
145
  })
144
146
  test('emits nodes-change', () => {
145
147
  const w = createWrapper()
@@ -350,8 +352,8 @@ describe('Snap-to-Grid integration', () => {
350
352
  const w = createWrapper({ snapToGrid: false })
351
353
  // Directly call onNodeResize with non-round values
352
354
  w.vm.onNodeResize({ nodeId: '1', width: 137, height: 63, x: 51, y: 49 })
353
- expect(w.vm.resizeOverlay.width).toBe(137)
354
- expect(w.vm.resizeOverlay.height).toBe(63)
355
+ expect(w.vm.dragPositions['1'].width).toBe(137)
356
+ expect(w.vm.dragPositions['1'].height).toBe(63)
355
357
  w.destroy()
356
358
  })
357
359
  test('snapGridSize passed to NodeRenderer', () => {
@@ -434,7 +436,7 @@ describe('onDocMouseMove dispatching', () => {
434
436
  const w = createWrapper()
435
437
  w.vm.viewport = { x: 0, y: 0, zoom: 1 }
436
438
  w.vm.startPan({ clientX: 100, clientY: 100 })
437
- w.vm.onDocMouseMove({ clientX: 150, clientY: 120 })
439
+ w.vm.onDocMouseMove({ clientX: 150, clientY: 120, buttons: 1 })
438
440
  expect(w.vm.viewport.x).toBe(50)
439
441
  expect(w.vm.viewport.y).toBe(20)
440
442
  w.destroy()
@@ -443,7 +445,7 @@ describe('onDocMouseMove dispatching', () => {
443
445
  const w = createWrapper()
444
446
  w.vm.$refs.canvas = { getContainerRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) }
445
447
  w.vm.startSelection({ clientX: 100, clientY: 100 })
446
- w.vm.onDocMouseMove({ clientX: 200, clientY: 200 })
448
+ w.vm.onDocMouseMove({ clientX: 200, clientY: 200, buttons: 1 })
447
449
  expect(w.vm.selectionBox.width).toBe(100)
448
450
  expect(w.vm.selectionBox.height).toBe(100)
449
451
  w.destroy()
@@ -451,7 +453,7 @@ describe('onDocMouseMove dispatching', () => {
451
453
  test('does nothing when idle', () => {
452
454
  const w = createWrapper()
453
455
  w.vm.viewport = { x: 0, y: 0, zoom: 1 }
454
- w.vm.onDocMouseMove({ clientX: 200, clientY: 200 })
456
+ w.vm.onDocMouseMove({ clientX: 200, clientY: 200, buttons: 1 })
455
457
  expect(w.vm.viewport.x).toBe(0)
456
458
  w.destroy()
457
459
  })