@logicflow/extension 2.0.15 → 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
@@ -11,19 +16,27 @@ export class SelectionSelect {
11
16
  private startPoint?: Position
12
17
  private endPoint?: Position
13
18
  private disabled = true
14
- private isDefaultStopMoveGraph:
19
+ private isWholeNode = true
20
+ private isWholeEdge = true
21
+ exclusiveMode = false // 框选独占模式:true 表示只能进行框选操作,false 表示可以同时进行其他画布操作
22
+ // 用于区分选区和点击事件
23
+ private mouseDownInfo: {
24
+ x: number
25
+ y: number
26
+ time: number
27
+ } | null = null
28
+ // 记录原始的 stopMoveGraph 设置
29
+ private originalStopMoveGraph:
15
30
  | boolean
16
31
  | 'horizontal'
17
32
  | 'vertical'
18
33
  | [number, number, number, number] = false
19
- private isWholeNode = true
20
- private isWholeEdge = true
21
34
 
22
- constructor({ lf }: LogicFlow.IExtensionProps) {
35
+ constructor({ lf, options }: LogicFlow.IExtensionProps) {
23
36
  this.lf = lf
24
- // 初始化isDefaultStopMoveGraph取值
25
- const { stopMoveGraph } = lf.getEditConfig()
26
- this.isDefaultStopMoveGraph = stopMoveGraph!
37
+
38
+ this.exclusiveMode = (options?.exclusiveMode as boolean) ?? false
39
+
27
40
  // TODO: 有没有既能将方法挂载到lf上,又能提供类型提示的方法?
28
41
  lf.openSelectionSelect = () => {
29
42
  this.openSelectionSelect()
@@ -31,44 +44,131 @@ export class SelectionSelect {
31
44
  lf.closeSelectionSelect = () => {
32
45
  this.closeSelectionSelect()
33
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)
34
55
  }
35
56
 
36
- render(lf: LogicFlow, domContainer: HTMLElement) {
57
+ render(_: LogicFlow, domContainer: HTMLElement) {
37
58
  this.container = domContainer
38
- lf.on('blank:mousedown', ({ e }: { e: MouseEvent }) => {
39
- const config = lf.getEditConfig()
40
- // 鼠标控制滚动移动画布的时候,不能选区。
41
- if (!config.stopMoveGraph || this.disabled) {
42
- return
43
- }
44
- // 禁用右键框选,修复可能导致画布出现多个框选框不消失的问题,见https://github.com/didi/LogicFlow/issues/985
45
- const isRightClick = e.button === 2
46
- if (isRightClick) {
47
- return
48
- }
49
- const {
50
- domOverlayPosition: { x, y },
51
- } = lf.getPointByClient(e.clientX, e.clientY)
52
- this.startPoint = {
53
- x,
54
- y,
55
- }
56
- this.endPoint = {
57
- x,
58
- y,
59
- }
60
- const wrapper = document.createElement('div')
61
- wrapper.className = 'lf-selection-select'
62
- wrapper.oncontextmenu = function prevent(ev: MouseEvent) {
63
- ev.preventDefault()
59
+ }
60
+
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)
64
70
  }
65
- wrapper.style.top = `${this.startPoint.y}px`
66
- wrapper.style.left = `${this.startPoint.x}px`
67
- domContainer.appendChild(wrapper)
68
- this.wrapper = wrapper
69
- document.addEventListener('mousemove', this.draw)
70
- document.addEventListener('mouseup', this.drawOff)
71
+ this.wrapper = undefined
72
+ }
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()
95
+ }
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)
110
+ }
111
+ }
112
+
113
+ private removeEventListeners() {
114
+ if (this.container) {
115
+ this.container.style.pointerEvents = 'none'
116
+ this.container.removeEventListener('mousedown', this.handleMouseDown)
117
+ }
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
+ // 禁用右键框选
136
+ const isRightClick = e.button === 2
137
+ if (isRightClick) return
138
+ // 清理之前可能存在的选区状态
139
+ this.cleanupSelectionState()
140
+ // 记录鼠标按下时的位置和时间
141
+ this.mouseDownInfo = {
142
+ x: e.clientX,
143
+ y: e.clientY,
144
+ time: Date.now(),
145
+ }
146
+
147
+ // 记录原始设置并临时禁止画布移动
148
+ this.originalStopMoveGraph = this.lf.getEditConfig().stopMoveGraph!
149
+ this.lf.updateEditConfig({
150
+ stopMoveGraph: true,
71
151
  })
152
+
153
+ const {
154
+ domOverlayPosition: { x, y },
155
+ } = this.lf.getPointByClient(e.clientX, e.clientY)
156
+
157
+ this.startPoint = { x, y }
158
+ this.endPoint = { x, y }
159
+
160
+ const wrapper = document.createElement('div')
161
+ wrapper.className = 'lf-selection-select'
162
+ wrapper.oncontextmenu = function prevent(ev: MouseEvent) {
163
+ ev.preventDefault()
164
+ }
165
+ wrapper.style.top = `${this.startPoint.y}px`
166
+ wrapper.style.left = `${this.startPoint.x}px`
167
+ this.container?.appendChild(wrapper)
168
+ this.wrapper = wrapper
169
+
170
+ document.addEventListener('mousemove', this.draw)
171
+ document.addEventListener('mouseup', this.drawOff)
72
172
  }
73
173
 
74
174
  /**
@@ -85,13 +185,14 @@ export class SelectionSelect {
85
185
  * 开启选区
86
186
  */
87
187
  openSelectionSelect() {
88
- const { stopMoveGraph } = this.lf.getEditConfig()
89
- if (!stopMoveGraph) {
90
- this.isDefaultStopMoveGraph = false
91
- this.lf.updateEditConfig({
92
- stopMoveGraph: true,
93
- })
188
+ if (!this.disabled) {
189
+ this.closeSelectionSelect()
190
+ }
191
+ if (!this.container) {
192
+ return
94
193
  }
194
+ this.cleanupSelectionState()
195
+ this.addEventListeners()
95
196
  this.open()
96
197
  }
97
198
 
@@ -99,11 +200,21 @@ export class SelectionSelect {
99
200
  * 关闭选区
100
201
  */
101
202
  closeSelectionSelect() {
102
- if (!this.isDefaultStopMoveGraph) {
103
- this.lf.updateEditConfig({
104
- stopMoveGraph: false,
203
+ if (!this.container) {
204
+ return
205
+ }
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,
105
213
  })
214
+ this.drawOff(lastEvent)
106
215
  }
216
+ this.cleanupSelectionState()
217
+ this.removeEventListeners()
107
218
  this.close()
108
219
  }
109
220
 
@@ -137,16 +248,37 @@ export class SelectionSelect {
137
248
  }
138
249
  }
139
250
  }
140
- 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)
141
269
  document.removeEventListener('mousemove', this.draw)
142
- document.removeEventListener('mouseup', this.drawOff)
143
- if (this.wrapper) {
144
- this.wrapper.oncontextmenu = null
145
- this.container?.removeChild(this.wrapper)
270
+ if (!this.exclusiveMode) {
271
+ document.removeEventListener('mouseup', this.drawOff)
146
272
  }
147
- if (this.startPoint && this.endPoint) {
148
- const { x, y } = this.startPoint
149
- 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
150
282
  // 返回框选范围,左上角和右下角的坐标
151
283
  const lt: PointTuple = [Math.min(x, x1), Math.min(y, y1)]
152
284
  const rb: PointTuple = [Math.max(x, x1), Math.max(y, y1)]
@@ -156,6 +288,13 @@ export class SelectionSelect {
156
288
  })
157
289
  // 选区太小的情况就忽略
158
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
+ }
159
298
  return
160
299
  }
161
300
  const elements = this.lf.graphModel.getAreaElement(
@@ -167,23 +306,62 @@ export class SelectionSelect {
167
306
  )
168
307
  const { dynamicGroup, group } = this.lf.graphModel
169
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
+
170
316
  elements.forEach((element) => {
171
317
  // 如果节点属于分组,则不选中节点,此处兼容旧版 Group 插件
172
- if (group && group.getNodeGroup(element.id)) {
173
- return
318
+ if (group) {
319
+ const elementGroup = group.getNodeGroup(element.id)
320
+ if (elements.includes(elementGroup)) {
321
+ // 当被选中的元素的父分组被选中时,不选中该元素
322
+ return
323
+ }
324
+ }
325
+ if (dynamicGroup) {
326
+ const elementGroup = dynamicGroup.getGroupByNodeId(element.id)
327
+ if (elements.includes(elementGroup)) {
328
+ // 当被选中的元素的父分组被选中时,不选中该元素
329
+ return
330
+ }
174
331
  }
175
- if (dynamicGroup && dynamicGroup.getGroupByNodeId(element.id)) {
332
+ // 在独占模式下,如果元素已经被选中,则取消选中
333
+ if (this.exclusiveMode && selectedIds.has(element.id)) {
334
+ this.lf.deselectElementById(element.id)
176
335
  return
177
336
  }
337
+
338
+ // 非独占模式下,或者元素未被选中时,选中元素
178
339
  this.lf.selectElementById(element.id, true)
179
340
  nonGroupedElements.push(element)
180
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 事件
181
351
  this.lf.emit('selection:selected', {
182
352
  elements: nonGroupedElements,
183
353
  leftTopPoint: lt,
184
354
  rightBottomPoint: rb,
185
355
  })
186
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
+ }
187
365
  }
188
366
 
189
367
  private open() {
@@ -178,8 +178,18 @@ export class DynamicGroup {
178
178
  this.topGroupZIndex = max
179
179
  }
180
180
 
181
- // 监听 LogicFlow 的相关事件,做对应的处理
182
- addNodeToGroup = ({ data: node }: CallbackArgs<'node:add'>) => {
181
+ onSelectionDrop = () => {
182
+ const { nodes: selectedNodes } = this.lf.graphModel.getSelectElements()
183
+ selectedNodes.forEach((node) => {
184
+ this.addNodeToGroup(node)
185
+ })
186
+ }
187
+
188
+ onNodeAddOrDrop = ({ data: node }: CallbackArgs<'node:add'>) => {
189
+ this.addNodeToGroup(node)
190
+ }
191
+
192
+ addNodeToGroup = (node: LogicFlow.NodeData) => {
183
193
  // 1. 如果该节点之前已经在 group 中了,则将其从之前的 group 移除
184
194
  const preGroupId = this.nodeGroupMap.get(node.id)
185
195
 
@@ -226,12 +236,10 @@ export class DynamicGroup {
226
236
  const group = this.getGroupByBounds(bounds, node)
227
237
  if (group) {
228
238
  const isAllowAppendIn = group.isAllowAppendIn(node)
229
- console.log('isAllowAppendIn', isAllowAppendIn)
230
239
  if (isAllowAppendIn) {
231
240
  group.addChild(node.id)
232
241
  // 建立节点与 group 的映射关系放在了 group.addChild 触发的事件中,与直接调用 addChild 的行为保持一致
233
- // TODO 下面这个是干什么的,是否需要一起移动到事件的逻辑中?
234
- group.setAllowAppendChild(true)
242
+ group.setAllowAppendChild(false)
235
243
  } else {
236
244
  // 抛出不允许插入的事件
237
245
  this.lf.emit('group:not-allowed', {
@@ -247,7 +255,6 @@ export class DynamicGroup {
247
255
  data: groupData,
248
256
  childId,
249
257
  }: CallbackArgs<'group:add-node'>) => {
250
- console.log('group:add-node', groupData)
251
258
  this.nodeGroupMap.set(childId, groupData.id)
252
259
  }
253
260
 
@@ -273,7 +280,17 @@ export class DynamicGroup {
273
280
  }
274
281
  }
275
282
 
276
- setActiveGroup = ({ data: node }: CallbackArgs<'node:drag'>) => {
283
+ onSelectionDrag = () => {
284
+ const { nodes: selectedNodes } = this.lf.graphModel.getSelectElements()
285
+ selectedNodes.forEach((node) => {
286
+ this.setActiveGroup(node)
287
+ })
288
+ }
289
+ onNodeDrag = ({ data: node }: CallbackArgs<'node:drag'>) => {
290
+ this.setActiveGroup(node)
291
+ }
292
+
293
+ setActiveGroup = (node: LogicFlow.NodeData) => {
277
294
  const nodeModel = this.lf.getNodeModelById(node.id)
278
295
  const bounds = nodeModel?.getBounds()
279
296
 
@@ -291,7 +308,6 @@ export class DynamicGroup {
291
308
  if (!isAllowAppendIn) return
292
309
 
293
310
  this.activeGroup = targetGroup
294
- console.log('this.activeGroup', this.activeGroup)
295
311
  this.activeGroup.setAllowAppendChild(true)
296
312
  }
297
313
  }
@@ -420,7 +436,6 @@ export class DynamicGroup {
420
436
  }
421
437
 
422
438
  onGraphRendered = ({ data }: CallbackArgs<'graph:rendered'>) => {
423
- console.log('data', data)
424
439
  forEach(data.nodes, (node) => {
425
440
  if (node.children) {
426
441
  forEach(node.children, (childId) => {
@@ -665,16 +680,15 @@ export class DynamicGroup {
665
680
  })
666
681
 
667
682
  graphModel.dynamicGroup = this
668
-
669
- lf.on('node:add,node:drop,node:dnd-add', this.addNodeToGroup)
683
+ lf.on('node:add,node:drop,node:dnd-add', this.onNodeAddOrDrop)
684
+ lf.on('selection:drop', this.onSelectionDrop)
670
685
  lf.on('node:delete', this.removeNodeFromGroup)
671
- lf.on('node:drag,node:dnd-drag', this.setActiveGroup)
686
+ lf.on('node:drag,node:dnd-drag', this.onNodeDrag)
687
+ lf.on('selection:drag', this.onSelectionDrag)
672
688
  lf.on('node:click', this.onNodeSelect)
673
689
  lf.on('node:mousemove', this.onNodeMove)
674
690
  lf.on('graph:rendered', this.onGraphRendered)
675
691
 
676
- lf.on('graph:updated', ({ data }) => console.log('data', data))
677
-
678
692
  lf.on('group:add-node', this.onGroupAddNode)
679
693
 
680
694
  // https://github.com/didi/LogicFlow/issues/1346
@@ -718,8 +732,6 @@ export class DynamicGroup {
718
732
  forEach(edgesInnerGroup, (edge) => {
719
733
  this.createEdge(edge, nodeIdMap, distance)
720
734
  })
721
-
722
- console.log('selectedEdges --->>>', selectedEdges)
723
735
  forEach(selectedEdges, (edge) => {
724
736
  elements.edges.push(this.createEdge(edge, nodeIdMap, distance))
725
737
  })
@@ -736,9 +748,11 @@ export class DynamicGroup {
736
748
 
737
749
  destroy() {
738
750
  // 销毁监听的事件,并移除渲染的 dom 内容
739
- this.lf.off('node:add,node:drop,node:dnd-add', this.addNodeToGroup)
751
+ this.lf.off('node:add,node:drop,node:dnd-add', this.onNodeAddOrDrop)
752
+ this.lf.off('selection:drop', this.onSelectionDrop)
740
753
  this.lf.off('node:delete', this.removeNodeFromGroup)
741
- this.lf.off('node:drag,node:dnd-drag', this.setActiveGroup)
754
+ this.lf.off('node:drag,node:dnd-drag', this.onNodeDrag)
755
+ this.lf.off('selection:drag', this.onSelectionDrag)
742
756
  this.lf.off('node:click', this.onNodeSelect)
743
757
  this.lf.off('node:mousemove', this.onNodeMove)
744
758
  this.lf.off('graph:rendered', this.onGraphRendered)
@@ -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) {