@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.
- package/.turbo/turbo-build.log +18 -17
- package/CHANGELOG.md +29 -0
- package/dist/index.css +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/es/components/menu/index.d.ts +52 -8
- package/es/components/menu/index.js +471 -188
- package/es/components/mini-map/index.js +3 -0
- package/es/index.css +1 -1
- package/es/style/index.css +1 -1
- package/es/style/raw.d.ts +1 -1
- package/es/style/raw.js +1 -1
- package/es/tools/label/index.js +10 -3
- package/es/tools/proximity-connect/index.js +1 -1
- package/es/tools/snapshot/index.d.ts +5 -0
- package/es/tools/snapshot/index.js +65 -8
- package/lib/components/menu/index.d.ts +52 -8
- package/lib/components/menu/index.js +471 -188
- package/lib/components/mini-map/index.js +3 -0
- package/lib/index.css +1 -1
- package/lib/style/index.css +1 -1
- package/lib/style/raw.d.ts +1 -1
- package/lib/style/raw.js +1 -1
- package/lib/tools/label/index.js +10 -3
- package/lib/tools/proximity-connect/index.js +1 -1
- package/lib/tools/snapshot/index.d.ts +5 -0
- package/lib/tools/snapshot/index.js +65 -8
- package/package.json +5 -3
- package/src/components/menu/index.ts +512 -204
- package/src/components/mini-map/index.ts +5 -0
- package/src/style/index.less +16 -4
- package/src/style/raw.ts +14 -4
- package/src/tools/label/index.ts +13 -5
- package/src/tools/proximity-connect/index.ts +1 -2
- package/src/tools/snapshot/index.ts +80 -9
- package/stats.html +1 -1
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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,
|
|
112
|
+
this.menuTypeMap?.set(DefaultNodeMenuKey, defaultMenuConfig.nodeMenu)
|
|
84
113
|
|
|
85
|
-
|
|
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,
|
|
128
|
+
this.menuTypeMap?.set(DefaultEdgeMenuKey, defaultMenuConfig.edgeMenu)
|
|
100
129
|
|
|
101
|
-
|
|
130
|
+
defaultMenuConfig.graphMenu = []
|
|
102
131
|
|
|
103
|
-
|
|
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(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
329
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
352
|
+
element.className = `lf-menu-item ${item.disabled ? 'lf-menu-item__disabled' : ''}`
|
|
350
353
|
}
|
|
351
|
-
if (item.icon
|
|
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
|
-
|
|
370
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
395
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|