@logicflow/extension 2.0.21 → 2.1.1

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,4 +1,4 @@
1
- import LogicFlow from '@logicflow/core'
1
+ import LogicFlow, { EditConfigModel } from '@logicflow/core'
2
2
 
3
3
  import GraphData = LogicFlow.GraphData
4
4
  import NodeData = LogicFlow.NodeData
@@ -10,7 +10,8 @@ type SetType = 'add' | 'reset'
10
10
  export type MenuItem = {
11
11
  text?: string
12
12
  className?: string
13
- icon?: boolean
13
+ icon?: boolean | string
14
+ disabled?: boolean
14
15
  callback: (element: any) => void
15
16
  }
16
17
 
@@ -18,13 +19,30 @@ export type MenuConfig = {
18
19
  nodeMenu?: MenuItem[] | false
19
20
  edgeMenu?: MenuItem[] | false
20
21
  graphMenu?: MenuItem[] | false
22
+ selectionMenu?: MenuItem[] | false
21
23
  }
22
24
 
25
+ export type MenuType = 'nodeMenu' | 'edgeMenu' | 'graphMenu' | 'selectionMenu'
26
+
23
27
  const DefaultNodeMenuKey = 'lf:defaultNodeMenu'
24
28
  const DefaultEdgeMenuKey = 'lf:defaultEdgeMenu'
25
29
  const DefaultGraphMenuKey = 'lf:defaultGraphMenu'
26
30
  const DefaultSelectionMenuKey = 'lf:defaultSelectionMenu'
27
31
 
32
+ const menuKeyMap: Record<MenuType, string> = {
33
+ nodeMenu: DefaultNodeMenuKey,
34
+ edgeMenu: DefaultEdgeMenuKey,
35
+ graphMenu: DefaultGraphMenuKey,
36
+ selectionMenu: DefaultSelectionMenuKey,
37
+ }
38
+
39
+ const defaultMenuConfig: MenuConfig = {
40
+ nodeMenu: [],
41
+ edgeMenu: [],
42
+ graphMenu: [],
43
+ selectionMenu: [],
44
+ }
45
+
28
46
  class Menu {
29
47
  lf: LogicFlow
30
48
  private __container?: HTMLElement
@@ -32,27 +50,38 @@ class Menu {
32
50
  private menuTypeMap?: Map<string, MenuItem[]>
33
51
  private __currentData: EdgeData | NodeData | GraphData | Position | null =
34
52
  null
53
+ private __isSilentMode: boolean = false
54
+ private __editConfigChangeHandler?: ({
55
+ data,
56
+ }: {
57
+ data: EditConfigModel
58
+ }) => void
35
59
  static pluginName = 'menu'
36
60
 
37
61
  constructor({ lf }) {
38
62
  this.lf = lf
39
- const {
40
- options: { isSilentMode },
41
- } = lf
42
- if (!isSilentMode) {
43
- this.__menuDOM = document.createElement('ul')
44
-
45
- this.menuTypeMap = new Map()
46
- this.init()
47
- this.lf.setMenuConfig = (config) => {
48
- this.setMenuConfig(config)
49
- }
50
- this.lf.addMenuConfig = (config) => {
51
- this.addMenuConfig(config)
52
- }
53
- this.lf.setMenuByType = (config) => {
54
- this.setMenuByType(config)
55
- }
63
+ this.__menuDOM = document.createElement('ul')
64
+ this.__isSilentMode = lf.graphModel.editConfigModel.isSilentMode
65
+
66
+ this.menuTypeMap = new Map()
67
+ this.init()
68
+ this.lf.setMenuConfig = (config) => {
69
+ this.setMenuConfig(config)
70
+ }
71
+ this.lf.addMenuConfig = (config) => {
72
+ this.addMenuConfig(config)
73
+ }
74
+ this.lf.setMenuByType = (config) => {
75
+ this.setMenuByType(config)
76
+ }
77
+ this.lf.changeMenuItemDisableStatus = (menuKey, text, disabled) => {
78
+ this.changeMenuItemDisableStatus(menuKey, text, disabled)
79
+ }
80
+ this.lf.getMenuConfig = (menuKey) => {
81
+ return this.getMenuConfig(menuKey)
82
+ }
83
+ this.lf.resetMenuConfigByType = (menuType) => {
84
+ this.resetMenuConfigByType(menuType)
56
85
  }
57
86
  }
58
87
 
@@ -60,7 +89,7 @@ class Menu {
60
89
  * 初始化设置默认内置菜单栏
61
90
  */
62
91
  private init() {
63
- const defaultNodeMenu = [
92
+ defaultMenuConfig.nodeMenu = [
64
93
  {
65
94
  text: '删除',
66
95
  callback: (node) => {
@@ -80,9 +109,9 @@ class Menu {
80
109
  },
81
110
  },
82
111
  ]
83
- this.menuTypeMap?.set(DefaultNodeMenuKey, defaultNodeMenu)
112
+ this.menuTypeMap?.set(DefaultNodeMenuKey, defaultMenuConfig.nodeMenu)
84
113
 
85
- const defaultEdgeMenu = [
114
+ defaultMenuConfig.edgeMenu = [
86
115
  {
87
116
  text: '删除',
88
117
  callback: (edge) => {
@@ -96,11 +125,11 @@ class Menu {
96
125
  },
97
126
  },
98
127
  ]
99
- this.menuTypeMap?.set(DefaultEdgeMenuKey, defaultEdgeMenu)
128
+ this.menuTypeMap?.set(DefaultEdgeMenuKey, defaultMenuConfig.edgeMenu)
100
129
 
101
- this.menuTypeMap?.set(DefaultGraphMenuKey, [])
130
+ defaultMenuConfig.graphMenu = []
102
131
 
103
- const DefaultSelectionMenu = [
132
+ defaultMenuConfig.selectionMenu = [
104
133
  {
105
134
  text: '删除',
106
135
  callback: (elements) => {
@@ -110,138 +139,16 @@ class Menu {
110
139
  },
111
140
  },
112
141
  ]
113
- this.menuTypeMap?.set(DefaultSelectionMenuKey, DefaultSelectionMenu)
114
- }
115
-
116
- render(lf: LogicFlow, container: HTMLElement) {
117
- if (lf.options.isSilentMode) return
118
- this.__container = container
119
- this.__currentData = null // 当前展示的菜单所属元素的model数据
120
- if (this.__menuDOM) {
121
- this.__menuDOM.className = 'lf-menu'
122
- container.appendChild(this.__menuDOM)
123
- // 将选项的click事件委托至menu容器
124
- // 在捕获阶段拦截并执行
125
- this.__menuDOM.addEventListener(
126
- 'click',
127
- (event) => {
128
- event.stopPropagation()
129
- let target = event.target as HTMLElement
130
- // 菜单有多层dom,需要精确获取菜单项所对应的dom
131
- // 除菜单项dom外,应考虑两种情况
132
- // 1. 菜单项的子元素 2. 菜单外层容器
133
- while (
134
- Array.from(target.classList).indexOf('lf-menu-item') === -1 &&
135
- Array.from(target.classList).indexOf('lf-menu') === -1
136
- ) {
137
- target = target?.parentElement as HTMLElement
138
- }
139
- if (Array.from(target.classList).indexOf('lf-menu-item') > -1) {
140
- // 如果点击区域在菜单项内
141
- ;(target as any).onclickCallback(this.__currentData)
142
- // 点击后隐藏menu
143
- if (this.__menuDOM) {
144
- this.__menuDOM.style.display = 'none'
145
- }
146
- this.__currentData = null
147
- } else {
148
- // 如果点击区域不在菜单项内
149
- console.warn('点击区域不在菜单项内,请检查代码!')
150
- }
151
- },
152
- true,
153
- )
154
- }
155
- // 通过事件控制菜单的显示和隐藏
156
- this.lf.on('node:contextmenu', ({ data, position, e }) => {
157
- const {
158
- domOverlayPosition: { x, y },
159
- } = position
160
- const { id } = data
161
- const model = this.lf.graphModel.getNodeModelById(id)
162
-
163
- if (!model) return
164
- let menuList: any = []
165
- const typeMenus = this.menuTypeMap?.get(model.type)
166
- // 1.如果单个节点自定义了菜单,以单个节点自定义为准
167
- if (model && model.menu && Array.isArray(model.menu)) {
168
- menuList = model.menu
169
- } else if (typeMenus) {
170
- // 2.如果当前节点类型定义了菜单,再取该配置
171
- menuList = typeMenus
172
- } else {
173
- // 3.最后取全局默认
174
- menuList = this.menuTypeMap?.get(DefaultNodeMenuKey)
175
- }
176
- this.__currentData = data
177
- this.showMenu(x, y, menuList, {
178
- width: model.width,
179
- height: model.height,
180
- clientX: e.clientX,
181
- clientY: e.clientY,
182
- })
183
- })
184
- this.lf.on('edge:contextmenu', ({ data, position, e }) => {
185
- const {
186
- domOverlayPosition: { x, y },
187
- } = position
188
- const { id } = data
189
- const model = this.lf.graphModel.getEdgeModelById(id)
190
- if (!model) return
191
- let menuList: any = []
192
- const typeMenus = this.menuTypeMap?.get(model.type)
193
- // 菜单优先级: model.menu > typeMenus > defaultEdgeMenu,注释同上节点
194
- if (model && model.menu && Array.isArray(model.menu)) {
195
- menuList = model.menu
196
- } else if (typeMenus) {
197
- menuList = typeMenus
198
- } else {
199
- menuList = this.menuTypeMap?.get(DefaultEdgeMenuKey) ?? []
200
- }
201
- this.__currentData = data
202
- this.showMenu(x, y, menuList, {
203
- width: model.width,
204
- height: model.height,
205
- clientX: e.clientX,
206
- clientY: e.clientY,
207
- })
208
- })
209
- this.lf.on('blank:contextmenu', ({ position }) => {
210
- const menuList = this.menuTypeMap?.get(DefaultGraphMenuKey) ?? []
211
- const {
212
- domOverlayPosition: { x, y },
213
- } = position
214
- this.__currentData = { ...position.canvasOverlayPosition }
215
- this.showMenu(x, y, menuList)
216
- })
217
- this.lf.on('selection:contextmenu', ({ data, position }) => {
218
- const menuList = this.menuTypeMap?.get(DefaultSelectionMenuKey)
219
- const {
220
- domOverlayPosition: { x, y },
221
- } = position
222
- this.__currentData = data
223
- this.showMenu(x, y, menuList)
224
- })
225
-
226
- this.lf.on('node:mousedown', () => {
227
- this.__menuDOM!.style.display = 'none'
228
- })
229
- this.lf.on('edge:click', () => {
230
- this.__menuDOM!.style.display = 'none'
231
- })
232
- this.lf.on('blank:click', () => {
233
- this.__menuDOM!.style.display = 'none'
234
- })
235
- }
236
-
237
- destroy() {
238
- if (this.__menuDOM) {
239
- this?.__container?.removeChild(this.__menuDOM)
240
- this.__menuDOM = undefined
241
- }
142
+ this.menuTypeMap?.set(
143
+ DefaultSelectionMenuKey,
144
+ defaultMenuConfig.selectionMenu,
145
+ )
242
146
  }
243
147
 
244
148
  private showMenu(x, y, menuList, options?) {
149
+ // 在静默模式下不显示菜单
150
+ if (this.__isSilentMode) return
151
+
245
152
  if (!menuList || !menuList.length) return
246
153
  const { __menuDOM: menu } = this
247
154
  if (menu) {
@@ -323,13 +230,109 @@ class Menu {
323
230
  }
324
231
 
325
232
  /**
326
- * 设置指定类型元素的菜单
233
+ * 通用的菜单配置处理方法
327
234
  */
328
- setMenuByType({ type, menu }: { type: string; menu: MenuItem[] }) {
329
- if (!type || !menu) {
235
+ private processMenuConfig(config: MenuConfig, operation: 'set' | 'add') {
236
+ if (!config) return
237
+
238
+ const menuTypes: MenuType[] = [
239
+ 'nodeMenu',
240
+ 'edgeMenu',
241
+ 'graphMenu',
242
+ 'selectionMenu',
243
+ ]
244
+
245
+ menuTypes.forEach((menuType) => {
246
+ const menuConfig = config[menuType]
247
+ const menuKey = menuKeyMap[menuType]
248
+
249
+ if (menuConfig === undefined) return
250
+
251
+ if (operation === 'set') {
252
+ // 设置菜单配置
253
+ this.menuTypeMap?.set(menuKey, menuConfig ? menuConfig : [])
254
+ } else if (operation === 'add' && Array.isArray(menuConfig)) {
255
+ // 追加菜单配置(只支持数组类型)
256
+ const existingMenuList = this.menuTypeMap?.get(menuKey) ?? []
257
+ this.menuTypeMap?.set(menuKey, existingMenuList.concat(menuConfig))
258
+ }
259
+ })
260
+ }
261
+
262
+ /**
263
+ * 创建图片元素
264
+ */
265
+ private createImageElement(src: string, alt: string): HTMLImageElement {
266
+ const img = document.createElement('img')
267
+ img.src = src
268
+ img.alt = alt
269
+ img.style.width = '16px'
270
+ img.style.height = '16px'
271
+ img.style.objectFit = 'contain'
272
+ return img
273
+ }
274
+
275
+ /**
276
+ * 检查是否为图片文件路径
277
+ */
278
+ private isImageFile(iconString: string): boolean {
279
+ const imageExtensions = [
280
+ '.png',
281
+ '.jpg',
282
+ '.jpeg',
283
+ '.gif',
284
+ '.svg',
285
+ '.webp',
286
+ '.ico',
287
+ '.bmp',
288
+ ]
289
+ return imageExtensions.some((ext) => iconString.toLowerCase().includes(ext))
290
+ }
291
+
292
+ /**
293
+ * 处理图标逻辑
294
+ */
295
+ private processIcon(
296
+ iconContainer: HTMLElement,
297
+ icon: boolean | string,
298
+ text?: string,
299
+ ) {
300
+ if (typeof icon !== 'string') {
301
+ // 如果icon是true,保持原有逻辑(创建空的图标容器)
330
302
  return
331
303
  }
332
- this.menuTypeMap?.set(type, menu)
304
+
305
+ const iconString = icon as string
306
+
307
+ // 1. base64格式的图片数据
308
+ if (iconString.startsWith('data:image/')) {
309
+ const img = this.createImageElement(iconString, text || 'icon')
310
+ iconContainer.appendChild(img)
311
+ return
312
+ }
313
+
314
+ // 2. 图片文件路径
315
+ if (this.isImageFile(iconString)) {
316
+ const img = this.createImageElement(iconString, text || 'icon')
317
+ iconContainer.appendChild(img)
318
+ return
319
+ }
320
+
321
+ // 3. HTML内容(包含< >标签)
322
+ if (iconString.includes('<') && iconString.includes('>')) {
323
+ iconContainer.innerHTML = iconString
324
+ return
325
+ }
326
+
327
+ // 4. CSS类名(以空格分隔的多个类名或以.开头)
328
+ if (iconString.includes(' ') || iconString.startsWith('.')) {
329
+ const iconClasses = iconString.replace(/^\./, '').split(' ')
330
+ iconContainer.classList.add(...iconClasses)
331
+ return
332
+ }
333
+
334
+ // 5. 单个CSS类名
335
+ iconContainer.classList.add(iconString)
333
336
  }
334
337
 
335
338
  /**
@@ -344,13 +347,14 @@ class Menu {
344
347
  list.forEach((item) => {
345
348
  const element = document.createElement('li')
346
349
  if (item.className) {
347
- element.className = `lf-menu-item ${item.className}`
350
+ element.className = `lf-menu-item ${item.disabled ? 'lf-menu-item__disabled' : ''} ${item.className}`
348
351
  } else {
349
- element.className = 'lf-menu-item'
352
+ element.className = `lf-menu-item ${item.disabled ? 'lf-menu-item__disabled' : ''}`
350
353
  }
351
- if (item.icon === true) {
354
+ if (item.icon) {
352
355
  const icon = document.createElement('span')
353
356
  icon.className = 'lf-menu-item-icon'
357
+ this.processIcon(icon, item.icon, item.text)
354
358
  element.appendChild(icon)
355
359
  }
356
360
  const text = document.createElement('span')
@@ -359,64 +363,198 @@ class Menu {
359
363
  text.innerText = item.text
360
364
  }
361
365
  element.appendChild(text)
366
+ if (item.disabled) {
367
+ element.setAttribute('disabled', 'true')
368
+ }
362
369
  ;(element as any).onclickCallback = item.callback
363
370
  menuList.push(element)
364
371
  })
365
372
  return menuList
366
373
  }
367
374
 
368
- // 复写菜单
369
- setMenuConfig(config: MenuConfig) {
370
- if (!config) {
375
+ /**
376
+ * 更新菜单项DOM元素的禁用状态
377
+ * @param text 菜单项文本
378
+ * @param disabled 是否禁用
379
+ */
380
+ private updateMenuItemDOMStatus(text: string, disabled: boolean) {
381
+ if (!this.__menuDOM || this.__menuDOM.style.display === 'none') {
371
382
  return
372
383
  }
373
- // node
374
- config.nodeMenu !== undefined &&
375
- this.menuTypeMap?.set(
376
- DefaultNodeMenuKey,
377
- config.nodeMenu ? config.nodeMenu : [],
378
- )
379
- // edge
380
- config.edgeMenu !== undefined &&
381
- this.menuTypeMap?.set(
382
- DefaultEdgeMenuKey,
383
- config.edgeMenu ? config.edgeMenu : [],
384
- )
385
- // graph
386
- config.graphMenu !== undefined &&
387
- this.menuTypeMap?.set(
388
- DefaultGraphMenuKey,
389
- config.graphMenu ? config.graphMenu : [],
390
- )
384
+
385
+ // 查找对应的菜单项DOM元素
386
+ const menuItems = Array.from(
387
+ this.__menuDOM.querySelectorAll('.lf-menu-item'),
388
+ )
389
+
390
+ const targetMenuItem = menuItems.find((menuItemElement) => {
391
+ const textElement = menuItemElement.querySelector('.lf-menu-item-text')
392
+ return textElement?.textContent === text
393
+ })
394
+
395
+ if (targetMenuItem) {
396
+ const element = targetMenuItem as HTMLElement
397
+
398
+ if (disabled) {
399
+ element.classList.add('lf-menu-item__disabled')
400
+ element.setAttribute('disabled', 'true')
401
+ } else {
402
+ element.classList.remove('lf-menu-item__disabled')
403
+ element.removeAttribute('disabled')
404
+ }
405
+ }
391
406
  }
392
407
 
393
- // 在默认菜单后面追加菜单项
394
- addMenuConfig(config: MenuConfig) {
395
- if (!config) {
408
+ /**
409
+ * 设置静默模式监听器
410
+ * isSilentMode 变化时,动态更新菜单的显隐状态
411
+ */
412
+ private setupSilentModeListener() {
413
+ // 创建并保存事件处理器引用
414
+ this.__editConfigChangeHandler = ({ data }) => {
415
+ const newIsSilentMode = data.isSilentMode
416
+ if (newIsSilentMode !== this.__isSilentMode) {
417
+ this.__isSilentMode = newIsSilentMode
418
+ this.updateMenuVisibility(!newIsSilentMode)
419
+ }
420
+ }
421
+ // 监听编辑配置变化
422
+ this.lf.on('editConfig:changed', this.__editConfigChangeHandler)
423
+ }
424
+
425
+ /**
426
+ * 更新菜单显隐状态
427
+ */
428
+ private updateMenuVisibility(visible: boolean) {
429
+ if (!this.__menuDOM) return
430
+
431
+ if (visible) {
432
+ if (this.__currentData) {
433
+ this.__menuDOM.style.display = 'block'
434
+ }
435
+ } else {
436
+ this.__menuDOM.style.display = 'none'
437
+ this.__currentData = null // 清除当前数据
438
+ }
439
+ }
440
+
441
+ /**
442
+ * 检查菜单是否正在显示并重新渲染
443
+ */
444
+ private refreshCurrentMenu() {
445
+ if (
446
+ !this.__menuDOM ||
447
+ this.__menuDOM.style.display === 'none' ||
448
+ !this.__currentData
449
+ ) {
396
450
  return
397
451
  }
398
- // 追加项时,只支持数组类型,对false不做操作
399
- if (Array.isArray(config.nodeMenu)) {
400
- const menuList = this.menuTypeMap?.get(DefaultNodeMenuKey) ?? []
401
- this.menuTypeMap?.set(
402
- DefaultNodeMenuKey,
403
- menuList.concat(config.nodeMenu),
404
- )
452
+
453
+ // 保存当前菜单的位置
454
+ const { left, top } = this.__menuDOM.style
455
+
456
+ // 根据当前数据类型获取对应的菜单配置
457
+ let menuList: any[] = []
458
+
459
+ // 判断当前数据类型并获取相应菜单
460
+ if (this.__currentData && typeof this.__currentData === 'object') {
461
+ if (
462
+ 'sourceNodeId' in this.__currentData &&
463
+ 'targetNodeId' in this.__currentData
464
+ ) {
465
+ // 边菜单
466
+ const model = this.lf.graphModel.getEdgeModelById(
467
+ (this.__currentData as any).id,
468
+ )
469
+ if (model) {
470
+ const typeMenus = this.menuTypeMap?.get(model.type)
471
+ if (model.menu && Array.isArray(model.menu)) {
472
+ menuList = model.menu
473
+ } else if (typeMenus) {
474
+ menuList = typeMenus
475
+ } else {
476
+ menuList = this.menuTypeMap?.get(DefaultEdgeMenuKey) ?? []
477
+ }
478
+ }
479
+ } else if ('id' in this.__currentData && 'type' in this.__currentData) {
480
+ // 节点菜单
481
+ const model = this.lf.graphModel.getNodeModelById(
482
+ (this.__currentData as any).id,
483
+ )
484
+ if (model) {
485
+ const typeMenus = this.menuTypeMap?.get(model.type)
486
+ if (model.menu && Array.isArray(model.menu)) {
487
+ menuList = model.menu
488
+ } else if (typeMenus) {
489
+ menuList = typeMenus
490
+ } else {
491
+ menuList = this.menuTypeMap?.get(DefaultNodeMenuKey) ?? []
492
+ }
493
+ }
494
+ } else if (
495
+ 'nodes' in this.__currentData &&
496
+ 'edges' in this.__currentData
497
+ ) {
498
+ // 选区菜单
499
+ menuList = this.menuTypeMap?.get(DefaultSelectionMenuKey) ?? []
500
+ } else {
501
+ // 画布菜单
502
+ menuList = this.menuTypeMap?.get(DefaultGraphMenuKey) ?? []
503
+ }
405
504
  }
406
- if (Array.isArray(config.edgeMenu)) {
407
- const menuList = this.menuTypeMap?.get(DefaultEdgeMenuKey) ?? []
408
- this.menuTypeMap?.set(
409
- DefaultEdgeMenuKey,
410
- menuList.concat(config.edgeMenu),
411
- )
505
+
506
+ // 重新渲染菜单
507
+ if (menuList && menuList.length > 0) {
508
+ this.__menuDOM.innerHTML = ''
509
+ this.__menuDOM.append(...this.__getMenuDom(menuList))
510
+
511
+ // 恢复菜单位置(如果有的话)
512
+ if (left) this.__menuDOM.style.left = left
513
+ if (top) this.__menuDOM.style.top = top
514
+ } else {
515
+ // 如果没有菜单项,隐藏菜单
516
+ this.__menuDOM.style.display = 'none'
517
+ this.__currentData = null
412
518
  }
413
- if (Array.isArray(config.graphMenu)) {
414
- const menuList = this.menuTypeMap?.get(DefaultGraphMenuKey) ?? []
415
- this.menuTypeMap?.set(
416
- DefaultGraphMenuKey,
417
- menuList.concat(config.graphMenu),
418
- )
519
+ }
520
+
521
+ /**
522
+ * 设置指定类型元素的菜单
523
+ */
524
+ setMenuByType({ type, menu }: { type: string; menu: MenuItem[] }) {
525
+ if (!type || !menu) {
526
+ return
419
527
  }
528
+ this.menuTypeMap?.set(type, menu)
529
+ this.refreshCurrentMenu() // 实时更新DOM
530
+ }
531
+
532
+ getMenuConfig(menuKey: MenuType) {
533
+ return this.menuTypeMap?.get(menuKeyMap[menuKey]) ?? []
534
+ }
535
+
536
+ resetMenuConfigByType(menuKey: MenuType) {
537
+ this.setMenuConfig({
538
+ [menuKey]: defaultMenuConfig[menuKey],
539
+ })
540
+ // setMenuConfig 已经包含了 refreshCurrentMenu 调用
541
+ }
542
+
543
+ resetAllMenuConfig() {
544
+ this.setMenuConfig(defaultMenuConfig)
545
+ // setMenuConfig 已经包含了 refreshCurrentMenu 调用
546
+ }
547
+
548
+ // 复写菜单
549
+ setMenuConfig(config: MenuConfig) {
550
+ this.processMenuConfig(config, 'set')
551
+ this.refreshCurrentMenu() // 实时更新DOM
552
+ }
553
+
554
+ // 在默认菜单后面追加菜单项
555
+ addMenuConfig(config: MenuConfig) {
556
+ this.processMenuConfig(config, 'add')
557
+ this.refreshCurrentMenu() // 实时更新DOM
420
558
  }
421
559
 
422
560
  /**
@@ -433,6 +571,176 @@ class Menu {
433
571
  "The first parameter of changeMenuConfig should be 'add' or 'reset'",
434
572
  )
435
573
  }
574
+ // addMenuConfig 和 setMenuConfig 已经包含了 refreshCurrentMenu 调用
575
+ }
576
+
577
+ changeMenuItemDisableStatus(
578
+ menuKey: MenuType,
579
+ text: string,
580
+ disabled: boolean,
581
+ ) {
582
+ if (!menuKey || !text) {
583
+ console.warn('params is vaild')
584
+ return
585
+ }
586
+ const menuList = this.menuTypeMap?.get(menuKeyMap[menuKey]) ?? []
587
+ if (!menuList.length) {
588
+ console.warn(`menuMap: ${menuKey} is not exist`)
589
+ return
590
+ }
591
+ const menuItem = menuList.find((item) => item.text === text)
592
+ if (!menuItem) {
593
+ console.warn(`menuItem: ${text} is not exist`)
594
+ return
595
+ }
596
+ menuItem.disabled = disabled
597
+
598
+ // 如果菜单当前正在显示,则同时更新DOM元素的样式
599
+ this.updateMenuItemDOMStatus(text, disabled)
600
+ }
601
+
602
+ render(lf: LogicFlow, container: HTMLElement) {
603
+ if (lf.graphModel.editConfigModel.isSilentMode) return
604
+ this.__container = container
605
+ this.__currentData = null // 当前展示的菜单所属元素的model数据
606
+
607
+ // 监听 isSilentMode 变化
608
+ this.setupSilentModeListener()
609
+
610
+ if (this.__menuDOM) {
611
+ this.__menuDOM.className = 'lf-menu'
612
+ container.appendChild(this.__menuDOM)
613
+ // 将选项的click事件委托至menu容器
614
+ // 在捕获阶段拦截并执行
615
+ this.__menuDOM.addEventListener(
616
+ 'click',
617
+ (event) => {
618
+ event.stopPropagation()
619
+ let target = event.target as HTMLElement
620
+
621
+ // 菜单有多层dom,需要精确获取菜单项所对应的dom
622
+ // 除菜单项dom外,应考虑两种情况
623
+ // 1. 菜单项的子元素 2. 菜单外层容器
624
+ while (
625
+ Array.from(target.classList).indexOf('lf-menu-item') === -1 &&
626
+ Array.from(target.classList).indexOf('lf-menu') === -1
627
+ ) {
628
+ target = target?.parentElement as HTMLElement
629
+ }
630
+ if (
631
+ Array.from(target.classList).indexOf('lf-menu-item__disabled') > -1
632
+ )
633
+ return
634
+ if (Array.from(target.classList).indexOf('lf-menu-item') > -1) {
635
+ // 如果菜单项被禁用,则不执行回调
636
+ ;(target as any).onclickCallback(this.__currentData, target)
637
+ // 点击后隐藏menu
638
+ if (this.__menuDOM) {
639
+ this.__menuDOM.style.display = 'none'
640
+ }
641
+ this.__currentData = null
642
+ } else {
643
+ // 如果点击区域不在菜单项内
644
+ console.warn('点击区域不在菜单项内,请检查代码!')
645
+ }
646
+ },
647
+ true,
648
+ )
649
+ }
650
+ // 通过事件控制菜单的显示和隐藏
651
+ this.lf.on('node:contextmenu', ({ data, position, e }) => {
652
+ const {
653
+ domOverlayPosition: { x, y },
654
+ } = position
655
+ const { id } = data
656
+ const model = this.lf.graphModel.getNodeModelById(id)
657
+
658
+ if (!model || this.__isSilentMode) return
659
+ let menuList: any = []
660
+ const typeMenus = this.menuTypeMap?.get(model.type)
661
+ // 1.如果单个节点自定义了菜单,以单个节点自定义为准
662
+ if (model && model.menu && Array.isArray(model.menu)) {
663
+ menuList = model.menu
664
+ } else if (typeMenus) {
665
+ // 2.如果当前节点类型定义了菜单,再取该配置
666
+ menuList = typeMenus
667
+ } else {
668
+ // 3.最后取全局默认
669
+ menuList = this.menuTypeMap?.get(DefaultNodeMenuKey)
670
+ }
671
+ this.__currentData = data
672
+ this.showMenu(x, y, menuList, {
673
+ width: model.width,
674
+ height: model.height,
675
+ clientX: e.clientX,
676
+ clientY: e.clientY,
677
+ })
678
+ })
679
+ this.lf.on('edge:contextmenu', ({ data, position, e }) => {
680
+ const {
681
+ domOverlayPosition: { x, y },
682
+ } = position
683
+ const { id } = data
684
+ const model = this.lf.graphModel.getEdgeModelById(id)
685
+ if (!model || this.__isSilentMode) return
686
+ let menuList: any = []
687
+ const typeMenus = this.menuTypeMap?.get(model.type)
688
+ // 菜单优先级: model.menu > typeMenus > defaultEdgeMenu,注释同上节点
689
+ if (model && model.menu && Array.isArray(model.menu)) {
690
+ menuList = model.menu
691
+ } else if (typeMenus) {
692
+ menuList = typeMenus
693
+ } else {
694
+ menuList = this.menuTypeMap?.get(DefaultEdgeMenuKey) ?? []
695
+ }
696
+ this.__currentData = data
697
+ this.showMenu(x, y, menuList, {
698
+ width: model.width,
699
+ height: model.height,
700
+ clientX: e.clientX,
701
+ clientY: e.clientY,
702
+ })
703
+ })
704
+ this.lf.on('blank:contextmenu', ({ position }) => {
705
+ if (this.__isSilentMode) return
706
+ const menuList = this.menuTypeMap?.get(DefaultGraphMenuKey) ?? []
707
+ const {
708
+ domOverlayPosition: { x, y },
709
+ } = position
710
+ this.__currentData = { ...position.canvasOverlayPosition }
711
+ this.showMenu(x, y, menuList)
712
+ })
713
+ this.lf.on('selection:contextmenu', ({ data, position }) => {
714
+ if (this.__isSilentMode) return
715
+ const menuList = this.menuTypeMap?.get(DefaultSelectionMenuKey)
716
+ const {
717
+ domOverlayPosition: { x, y },
718
+ } = position
719
+ this.__currentData = data
720
+ this.showMenu(x, y, menuList)
721
+ })
722
+
723
+ this.lf.on('node:mousedown', () => {
724
+ this.__menuDOM!.style.display = 'none'
725
+ })
726
+ this.lf.on('edge:click', () => {
727
+ this.__menuDOM!.style.display = 'none'
728
+ })
729
+ this.lf.on('blank:click', () => {
730
+ this.__menuDOM!.style.display = 'none'
731
+ })
732
+ }
733
+
734
+ destroy() {
735
+ // 清理事件监听器
736
+ if (this.__editConfigChangeHandler) {
737
+ this.lf.off('editConfig:changed', this.__editConfigChangeHandler)
738
+ }
739
+
740
+ if (this.__menuDOM) {
741
+ this?.__container?.removeChild(this.__menuDOM)
742
+ this.__menuDOM = undefined
743
+ }
436
744
  }
437
745
  }
438
746