@logicflow/extension 2.0.16 → 2.0.17

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.
@@ -1,8 +1,13 @@
1
1
  import LogicFlow from '@logicflow/core'
2
+ import { cloneDeep } from 'lodash-es'
2
3
 
3
4
  import Position = LogicFlow.Position
4
5
  import PointTuple = LogicFlow.PointTuple
5
6
 
7
+ export interface SelectionConfig {
8
+ exclusiveMode?: boolean
9
+ }
10
+
6
11
  export class SelectionSelect {
7
12
  static pluginName = 'selectionSelect'
8
13
  private container?: HTMLElement
@@ -13,15 +18,25 @@ export class SelectionSelect {
13
18
  private disabled = true
14
19
  private isWholeNode = true
15
20
  private isWholeEdge = true
21
+ exclusiveMode = false // 框选独占模式:true 表示只能进行框选操作,false 表示可以同时进行其他画布操作
16
22
  // 用于区分选区和点击事件
17
23
  private mouseDownInfo: {
18
24
  x: number
19
25
  y: number
20
26
  time: number
21
27
  } | null = null
28
+ // 记录原始的 stopMoveGraph 设置
29
+ private originalStopMoveGraph:
30
+ | boolean
31
+ | 'horizontal'
32
+ | 'vertical'
33
+ | [number, number, number, number] = false
22
34
 
23
- constructor({ lf }: LogicFlow.IExtensionProps) {
35
+ constructor({ lf, options }: LogicFlow.IExtensionProps) {
24
36
  this.lf = lf
37
+
38
+ this.exclusiveMode = (options?.exclusiveMode as boolean) ?? false
39
+
25
40
  // TODO: 有没有既能将方法挂载到lf上,又能提供类型提示的方法?
26
41
  lf.openSelectionSelect = () => {
27
42
  this.openSelectionSelect()
@@ -29,46 +44,119 @@ export class SelectionSelect {
29
44
  lf.closeSelectionSelect = () => {
30
45
  this.closeSelectionSelect()
31
46
  }
47
+ // 新增切换独占模式的方法
48
+ lf.setSelectionSelectMode = (exclusive: boolean) => {
49
+ this.setExclusiveMode(exclusive)
50
+ }
51
+ // 绑定方法的 this 上下文
52
+ this.handleMouseDown = this.handleMouseDown.bind(this)
53
+ this.draw = this.draw.bind(this)
54
+ this.drawOff = this.drawOff.bind(this)
32
55
  }
33
56
 
34
57
  render(_: LogicFlow, domContainer: HTMLElement) {
35
58
  this.container = domContainer
36
59
  }
37
60
 
38
- onToolContainerMouseDown = (e: MouseEvent) => {
39
- // 避免在其他插件元素上点击时开启选区
40
- if (e.target !== this.container) {
41
- return
61
+ /**
62
+ * 清理选区状态
63
+ */
64
+ private cleanupSelectionState() {
65
+ // 清理当前的选区状态
66
+ if (this.wrapper) {
67
+ this.wrapper.oncontextmenu = null
68
+ if (this.container && this.wrapper.parentNode === this.container) {
69
+ this.container.removeChild(this.wrapper)
70
+ }
71
+ this.wrapper = undefined
42
72
  }
43
- this.mouseDownInfo = {
44
- x: e.clientX,
45
- y: e.clientY,
46
- time: Date.now(),
73
+ this.startPoint = undefined
74
+ this.endPoint = undefined
75
+ this.mouseDownInfo = null
76
+
77
+ // 移除事件监听
78
+ document.removeEventListener('mousemove', this.draw)
79
+ document.removeEventListener('mouseup', this.drawOff)
80
+ }
81
+
82
+ /**
83
+ * 切换框选模式
84
+ * @param exclusive 是否为独占模式。true 表示只能进行框选操作,false 表示可以同时进行其他画布操作
85
+ */
86
+ setExclusiveMode(exclusive: boolean = false) {
87
+ if (this.exclusiveMode === exclusive) return
88
+
89
+ this.cleanupSelectionState()
90
+ this.exclusiveMode = exclusive
91
+ if (this.container && !this.disabled) {
92
+ // 切换事件监听方式
93
+ this.removeEventListeners()
94
+ this.addEventListeners()
47
95
  }
48
- const lf = this.lf
49
- const domContainer = this.container
50
- if (!domContainer) {
51
- return
96
+ }
97
+
98
+ private addEventListeners() {
99
+ if (!this.container) return
100
+
101
+ if (this.exclusiveMode) {
102
+ // 独占模式:监听 container 的 mousedown 事件
103
+ this.container.style.pointerEvents = 'auto'
104
+ this.container.addEventListener('mousedown', this.handleMouseDown)
105
+ } else {
106
+ // 非独占模式:监听画布的 blank:mousedown 事件
107
+ this.container.style.pointerEvents = 'none'
108
+ // 使用实例方法而不是箭头函数,这样可以正确移除事件监听
109
+ this.lf.on('blank:mousedown', this.handleBlankMouseDown)
52
110
  }
53
- if (this.disabled) {
54
- return
111
+ }
112
+
113
+ private removeEventListeners() {
114
+ if (this.container) {
115
+ this.container.style.pointerEvents = 'none'
116
+ this.container.removeEventListener('mousedown', this.handleMouseDown)
55
117
  }
56
- // 禁用右键框选,修复可能导致画布出现多个框选框不消失的问题,见https://github.com/didi/LogicFlow/issues/985
118
+ // 移除 blank:mousedown 事件监听
119
+ this.lf.off('blank:mousedown', this.handleBlankMouseDown)
120
+ }
121
+
122
+ /**
123
+ * 处理画布空白处鼠标按下事件(非独占模式)
124
+ */
125
+ private handleBlankMouseDown = ({ e }: { e: MouseEvent }) => {
126
+ this.handleMouseDown(e)
127
+ }
128
+
129
+ /**
130
+ * 处理鼠标按下事件
131
+ */
132
+ private handleMouseDown(e: MouseEvent) {
133
+ if (!this.container || this.disabled) return
134
+
135
+ // 禁用右键框选
57
136
  const isRightClick = e.button === 2
58
- if (isRightClick) {
59
- return
137
+ if (isRightClick) return
138
+ // 清理之前可能存在的选区状态
139
+ this.cleanupSelectionState()
140
+ // 记录鼠标按下时的位置和时间
141
+ this.mouseDownInfo = {
142
+ x: e.clientX,
143
+ y: e.clientY,
144
+ time: Date.now(),
60
145
  }
146
+
147
+ // 记录原始设置并临时禁止画布移动
148
+ this.originalStopMoveGraph = this.lf.getEditConfig().stopMoveGraph!
149
+ this.lf.updateEditConfig({
150
+ stopMoveGraph: true,
151
+ })
152
+
61
153
  const {
62
154
  domOverlayPosition: { x, y },
63
- } = lf.getPointByClient(e.clientX, e.clientY)
64
- this.startPoint = {
65
- x,
66
- y,
67
- }
68
- this.endPoint = {
69
- x,
70
- y,
71
- }
155
+ } = this.lf.getPointByClient(e.clientX, e.clientY)
156
+
157
+ this.startPoint = { x, y }
158
+ this.endPoint = { x, y }
159
+
72
160
  const wrapper = document.createElement('div')
73
161
  wrapper.className = 'lf-selection-select'
74
162
  wrapper.oncontextmenu = function prevent(ev: MouseEvent) {
@@ -76,28 +164,13 @@ export class SelectionSelect {
76
164
  }
77
165
  wrapper.style.top = `${this.startPoint.y}px`
78
166
  wrapper.style.left = `${this.startPoint.x}px`
79
- domContainer.appendChild(wrapper)
167
+ this.container?.appendChild(wrapper)
80
168
  this.wrapper = wrapper
169
+
81
170
  document.addEventListener('mousemove', this.draw)
82
171
  document.addEventListener('mouseup', this.drawOff)
83
172
  }
84
173
 
85
- onToolContainerMouseUp = (e: MouseEvent) => {
86
- if (this.mouseDownInfo) {
87
- const { x, y, time } = this.mouseDownInfo
88
- const now = Date.now()
89
- // 用 mouseDown 和 mouseUp 的位置偏移及时间间隔来判断是否是点击事件
90
- const isClickEvent =
91
- Math.abs(e.clientX - x) < 10 &&
92
- Math.abs(e.clientY - y) < 10 &&
93
- now - time < 100
94
- if (isClickEvent) {
95
- this.lf.clearSelectElements()
96
- }
97
- this.mouseDownInfo = null
98
- }
99
- }
100
-
101
174
  /**
102
175
  * 设置选中的灵敏度
103
176
  * @param isWholeEdge 是否要边的起点终点都在选区范围才算选中。默认true
@@ -118,11 +191,8 @@ export class SelectionSelect {
118
191
  if (!this.container) {
119
192
  return
120
193
  }
121
- this.mouseDownInfo = null
122
- this.container.addEventListener('mousedown', this.onToolContainerMouseDown)
123
- this.container.addEventListener('mouseup', this.onToolContainerMouseUp)
124
- // 取消点击事件的穿透,只让 ToolOverlay 接收事件,避免与图形元素的事件冲突
125
- this.container.style.pointerEvents = 'auto'
194
+ this.cleanupSelectionState()
195
+ this.addEventListeners()
126
196
  this.open()
127
197
  }
128
198
 
@@ -133,13 +203,18 @@ export class SelectionSelect {
133
203
  if (!this.container) {
134
204
  return
135
205
  }
136
- this.container.style.pointerEvents = 'none'
137
- this.mouseDownInfo = null
138
- this.container.removeEventListener(
139
- 'mousedown',
140
- this.onToolContainerMouseDown,
141
- )
142
- this.container.removeEventListener('mouseup', this.onToolContainerMouseUp)
206
+ // 如果还有未完成的框选,先触发 drawOff 完成框选
207
+ if (this.wrapper && this.startPoint && this.endPoint) {
208
+ // 记录上一次的结束点,用于触发 mouseup 事件
209
+ const lastEndPoint = cloneDeep(this.endPoint)
210
+ const lastEvent = new MouseEvent('mouseup', {
211
+ clientX: lastEndPoint.x,
212
+ clientY: lastEndPoint.y,
213
+ })
214
+ this.drawOff(lastEvent)
215
+ }
216
+ this.cleanupSelectionState()
217
+ this.removeEventListeners()
143
218
  this.close()
144
219
  }
145
220
 
@@ -173,16 +248,37 @@ export class SelectionSelect {
173
248
  }
174
249
  }
175
250
  }
176
- private drawOff = () => {
251
+ private drawOff = (e: MouseEvent) => {
252
+ // 处理鼠标抬起事件
253
+ // 首先判断是否是点击,如果是,则清空框选
254
+ if (this.mouseDownInfo) {
255
+ const { x, y, time } = this.mouseDownInfo
256
+ const isClick =
257
+ Math.abs(e.clientX - x) < 5 &&
258
+ Math.abs(e.clientY - y) < 5 &&
259
+ Date.now() - time < 200
260
+ if (isClick) {
261
+ this.lf.clearSelectElements()
262
+ this.cleanupSelectionState()
263
+ return
264
+ }
265
+ }
266
+
267
+ const curStartPoint = cloneDeep(this.startPoint)
268
+ const curEndPoint = cloneDeep(this.endPoint)
177
269
  document.removeEventListener('mousemove', this.draw)
178
- document.removeEventListener('mouseup', this.drawOff)
179
- if (this.wrapper) {
180
- this.wrapper.oncontextmenu = null
181
- this.container?.removeChild(this.wrapper)
270
+ if (!this.exclusiveMode) {
271
+ document.removeEventListener('mouseup', this.drawOff)
182
272
  }
183
- if (this.startPoint && this.endPoint) {
184
- const { x, y } = this.startPoint
185
- const { x: x1, y: y1 } = this.endPoint
273
+
274
+ // 恢复原始的 stopMoveGraph 设置
275
+ this.lf.updateEditConfig({
276
+ stopMoveGraph: this.originalStopMoveGraph,
277
+ })
278
+
279
+ if (curStartPoint && curEndPoint) {
280
+ const { x, y } = curStartPoint
281
+ const { x: x1, y: y1 } = curEndPoint
186
282
  // 返回框选范围,左上角和右下角的坐标
187
283
  const lt: PointTuple = [Math.min(x, x1), Math.min(y, y1)]
188
284
  const rb: PointTuple = [Math.max(x, x1), Math.max(y, y1)]
@@ -192,6 +288,13 @@ export class SelectionSelect {
192
288
  })
193
289
  // 选区太小的情况就忽略
194
290
  if (Math.abs(x1 - x) < 10 && Math.abs(y1 - y) < 10) {
291
+ if (this.wrapper) {
292
+ this.wrapper.oncontextmenu = null
293
+ if (this.container && this.wrapper.parentNode === this.container) {
294
+ this.container.removeChild(this.wrapper)
295
+ }
296
+ this.wrapper = undefined
297
+ }
195
298
  return
196
299
  }
197
300
  const elements = this.lf.graphModel.getAreaElement(
@@ -203,6 +306,13 @@ export class SelectionSelect {
203
306
  )
204
307
  const { dynamicGroup, group } = this.lf.graphModel
205
308
  const nonGroupedElements: typeof elements = []
309
+ const selectedElements = this.lf.getSelectElements()
310
+ // 同时记录节点和边的ID
311
+ const selectedIds = new Set([
312
+ ...selectedElements.nodes.map((node) => node.id),
313
+ ...selectedElements.edges.map((edge) => edge.id),
314
+ ])
315
+
206
316
  elements.forEach((element) => {
207
317
  // 如果节点属于分组,则不选中节点,此处兼容旧版 Group 插件
208
318
  if (group) {
@@ -219,15 +329,39 @@ export class SelectionSelect {
219
329
  return
220
330
  }
221
331
  }
332
+ // 在独占模式下,如果元素已经被选中,则取消选中
333
+ if (this.exclusiveMode && selectedIds.has(element.id)) {
334
+ this.lf.deselectElementById(element.id)
335
+ return
336
+ }
337
+
338
+ // 非独占模式下,或者元素未被选中时,选中元素
222
339
  this.lf.selectElementById(element.id, true)
223
340
  nonGroupedElements.push(element)
224
341
  })
342
+ // 重置起始点和终点
343
+ // 注意:这两个值必须在触发closeSelectionSelect方法前充值,否则会导致独占模式下元素无法选中的问题
344
+ this.startPoint = undefined
345
+ this.endPoint = undefined
346
+ // 如果有选中的元素,触发 selection:drop 事件
347
+ if (nonGroupedElements.length > 0) {
348
+ this.lf.emit('selection:drop', { e })
349
+ }
350
+ // 触发 selection:selected 事件
225
351
  this.lf.emit('selection:selected', {
226
352
  elements: nonGroupedElements,
227
353
  leftTopPoint: lt,
228
354
  rightBottomPoint: rb,
229
355
  })
230
356
  }
357
+
358
+ if (this.wrapper) {
359
+ this.wrapper.oncontextmenu = null
360
+ if (this.container && this.wrapper.parentNode === this.container) {
361
+ this.container.removeChild(this.wrapper)
362
+ }
363
+ this.wrapper = undefined
364
+ }
231
365
  }
232
366
 
233
367
  private open() {
@@ -236,7 +236,6 @@ export class DynamicGroup {
236
236
  const group = this.getGroupByBounds(bounds, node)
237
237
  if (group) {
238
238
  const isAllowAppendIn = group.isAllowAppendIn(node)
239
- console.log('isAllowAppendIn', isAllowAppendIn)
240
239
  if (isAllowAppendIn) {
241
240
  group.addChild(node.id)
242
241
  // 建立节点与 group 的映射关系放在了 group.addChild 触发的事件中,与直接调用 addChild 的行为保持一致
@@ -256,7 +255,6 @@ export class DynamicGroup {
256
255
  data: groupData,
257
256
  childId,
258
257
  }: CallbackArgs<'group:add-node'>) => {
259
- console.log('group:add-node', groupData)
260
258
  this.nodeGroupMap.set(childId, groupData.id)
261
259
  }
262
260
 
@@ -310,7 +308,6 @@ export class DynamicGroup {
310
308
  if (!isAllowAppendIn) return
311
309
 
312
310
  this.activeGroup = targetGroup
313
- console.log('this.activeGroup', this.activeGroup)
314
311
  this.activeGroup.setAllowAppendChild(true)
315
312
  }
316
313
  }
@@ -439,7 +436,6 @@ export class DynamicGroup {
439
436
  }
440
437
 
441
438
  onGraphRendered = ({ data }: CallbackArgs<'graph:rendered'>) => {
442
- console.log('data', data)
443
439
  forEach(data.nodes, (node) => {
444
440
  if (node.children) {
445
441
  forEach(node.children, (childId) => {
@@ -684,7 +680,6 @@ export class DynamicGroup {
684
680
  })
685
681
 
686
682
  graphModel.dynamicGroup = this
687
-
688
683
  lf.on('node:add,node:drop,node:dnd-add', this.onNodeAddOrDrop)
689
684
  lf.on('selection:drop', this.onSelectionDrop)
690
685
  lf.on('node:delete', this.removeNodeFromGroup)
@@ -694,8 +689,6 @@ export class DynamicGroup {
694
689
  lf.on('node:mousemove', this.onNodeMove)
695
690
  lf.on('graph:rendered', this.onGraphRendered)
696
691
 
697
- lf.on('graph:updated', ({ data }) => console.log('data', data))
698
-
699
692
  lf.on('group:add-node', this.onGroupAddNode)
700
693
 
701
694
  // https://github.com/didi/LogicFlow/issues/1346
@@ -739,8 +732,6 @@ export class DynamicGroup {
739
732
  forEach(edgesInnerGroup, (edge) => {
740
733
  this.createEdge(edge, nodeIdMap, distance)
741
734
  })
742
-
743
- console.log('selectedEdges --->>>', selectedEdges)
744
735
  forEach(selectedEdges, (edge) => {
745
736
  elements.edges.push(this.createEdge(edge, nodeIdMap, distance))
746
737
  })
@@ -321,7 +321,6 @@ export class GroupNodeModel extends RectResizeModel {
321
321
  getData() {
322
322
  const data = super.getData()
323
323
  data.children = []
324
- console.log('this.children', this.children)
325
324
  this.children.forEach((childId) => {
326
325
  const model = this.graphModel.getNodeModelById(childId)
327
326
  if (model && !model.virtual) {