Skip to content

Commit c381b85

Browse files
liujupingJackLian
authored andcommitted
fix(context-menu): fix context menu bugs
1 parent 8f0291f commit c381b85

File tree

9 files changed

+171
-59
lines changed

9 files changed

+171
-59
lines changed

docs/docs/api/commonUI.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ CommonUI API 是一个专为低代码引擎设计的组件 UI 库,使用它开
4242
|------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------|
4343
| name | 动作的唯一标识符<br/>Unique identifier for the action | string | |
4444
| title | 显示的标题,可以是字符串或国际化数据<br/>Display title, can be a string or internationalized data | string \| IPublicTypeI18nData (optional) | |
45-
| type | 菜单项类型<br/>Menu item type | IPublicEnumContextMenuType (optional) | IPublicEnumPContextMenuType.MENU_ITEM |
45+
| type | 菜单项类型<br/>Menu item type | IPublicEnumContextMenuType (optional) | IPublicEnumContextMenuType.MENU_ITEM |
4646
| action | 点击时执行的动作,可选<br/>Action to execute on click, optional | (nodes: IPublicModelNode[]) => void (optional) | |
4747
| items | 子菜单项或生成子节点的函数,可选,仅支持两级<br/>Sub-menu items or function to generate child node, optional | Omit<IPublicTypeContextMenuAction, 'items'>[] \| ((nodes: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]) (optional) | |
4848
| condition | 显示条件函数<br/>Function to determine display condition | (nodes: IPublicModelNode[]) => boolean (optional) | |

packages/designer/src/context-menu-actions.ts

Lines changed: 103 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
22
import { IDesigner, INode } from './designer';
3-
import { parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
3+
import { parseContextMenuAsReactNode, parseContextMenuProperties, uniqueId } from '@alilc/lowcode-utils';
44
import { Menu } from '@alifd/next';
55
import { engineConfig } from '@alilc/lowcode-editor-core';
66
import './context-menu-actions.scss';
@@ -17,7 +17,100 @@ export interface IContextMenuActions {
1717
adjustMenuLayout: IPublicApiMaterial['adjustContextMenuLayout'];
1818
}
1919

20-
let destroyFn: Function | undefined;
20+
let adjustMenuLayoutFn: Function = (actions: IPublicTypeContextMenuAction[]) => actions;
21+
22+
export class GlobalContextMenuActions {
23+
enableContextMenu: boolean;
24+
25+
dispose: Function[];
26+
27+
contextMenuActionsMap: Map<string, ContextMenuActions> = new Map();
28+
29+
constructor() {
30+
this.dispose = [];
31+
32+
engineConfig.onGot('enableContextMenu', (enable) => {
33+
if (this.enableContextMenu === enable) {
34+
return;
35+
}
36+
this.enableContextMenu = enable;
37+
this.dispose.forEach(d => d());
38+
if (enable) {
39+
this.initEvent();
40+
}
41+
});
42+
}
43+
44+
handleContextMenu = (
45+
event: MouseEvent,
46+
) => {
47+
event.stopPropagation();
48+
event.preventDefault();
49+
50+
const actions: IPublicTypeContextMenuAction[] = [];
51+
this.contextMenuActionsMap.forEach((contextMenu) => {
52+
actions.push(...contextMenu.actions);
53+
});
54+
55+
let destroyFn: Function | undefined;
56+
57+
const destroy = () => {
58+
destroyFn?.();
59+
};
60+
61+
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
62+
nodes: [],
63+
destroy,
64+
event,
65+
});
66+
67+
if (!menus.length) {
68+
return;
69+
}
70+
71+
const layoutMenu = adjustMenuLayoutFn(menus);
72+
73+
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
74+
destroy,
75+
nodes: [],
76+
});
77+
78+
const target = event.target;
79+
80+
const { top, left } = target?.getBoundingClientRect();
81+
82+
const menuInstance = Menu.create({
83+
target: event.target,
84+
offset: [event.clientX - left, event.clientY - top],
85+
children: menuNode,
86+
className: 'engine-context-menu',
87+
});
88+
89+
destroyFn = (menuInstance as any).destroy;
90+
};
91+
92+
initEvent() {
93+
this.dispose.push(
94+
(() => {
95+
const handleContextMenu = (e: MouseEvent) => {
96+
this.handleContextMenu(e);
97+
};
98+
99+
document.addEventListener('contextmenu', handleContextMenu);
100+
101+
return () => {
102+
document.removeEventListener('contextmenu', handleContextMenu);
103+
};
104+
})(),
105+
);
106+
}
107+
108+
registerContextMenuActions(contextMenu: ContextMenuActions) {
109+
this.contextMenuActionsMap.set(contextMenu.id, contextMenu);
110+
}
111+
}
112+
113+
const globalContextMenuActions = new GlobalContextMenuActions();
21114

22115
export class ContextMenuActions implements IContextMenuActions {
23116
actions: IPublicTypeContextMenuAction[] = [];
@@ -28,6 +121,8 @@ export class ContextMenuActions implements IContextMenuActions {
28121

29122
enableContextMenu: boolean;
30123

124+
id: string = uniqueId('contextMenu');;
125+
31126
constructor(designer: IDesigner) {
32127
this.designer = designer;
33128
this.dispose = [];
@@ -42,6 +137,8 @@ export class ContextMenuActions implements IContextMenuActions {
42137
this.initEvent();
43138
}
44139
});
140+
141+
globalContextMenuActions.registerContextMenuActions(this);
45142
}
46143

47144
handleContextMenu = (
@@ -57,7 +154,7 @@ export class ContextMenuActions implements IContextMenuActions {
57154
const { bounds } = designer.project.simulator?.viewport || { bounds: { left: 0, top: 0 } };
58155
const { left: simulatorLeft, top: simulatorTop } = bounds;
59156

60-
destroyFn?.();
157+
let destroyFn: Function | undefined;
61158

62159
const destroy = () => {
63160
destroyFn?.();
@@ -66,13 +163,14 @@ export class ContextMenuActions implements IContextMenuActions {
66163
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
67164
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
68165
destroy,
166+
event,
69167
});
70168

71169
if (!menus.length) {
72170
return;
73171
}
74172

75-
const layoutMenu = designer.contextMenuActions.adjustMenuLayoutFn(menus);
173+
const layoutMenu = adjustMenuLayoutFn(menus);
76174

77175
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
78176
destroy,
@@ -111,22 +209,9 @@ export class ContextMenuActions implements IContextMenuActions {
111209
const nodes = designer.currentSelection.getNodes();
112210
this.handleContextMenu(nodes, originalEvent);
113211
}),
114-
(() => {
115-
const handleContextMenu = (e: MouseEvent) => {
116-
this.handleContextMenu([], e);
117-
};
118-
119-
document.addEventListener('contextmenu', handleContextMenu);
120-
121-
return () => {
122-
document.removeEventListener('contextmenu', handleContextMenu);
123-
};
124-
})(),
125212
);
126213
}
127214

128-
adjustMenuLayoutFn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[] = (actions) => actions;
129-
130215
addMenuAction(action: IPublicTypeContextMenuAction) {
131216
this.actions.push({
132217
type: IPublicEnumContextMenuType.MENU_ITEM,
@@ -142,6 +227,6 @@ export class ContextMenuActions implements IContextMenuActions {
142227
}
143228

144229
adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) {
145-
this.adjustMenuLayoutFn = fn;
230+
adjustMenuLayoutFn = fn;
146231
}
147232
}

packages/engine/src/inner-plugins/default-context-menu.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
7070

7171
material.addContextMenuOption({
7272
name: 'copyAndPaste',
73-
title: intl('Copy'),
73+
title: intl('CopyAndPaste'),
7474
condition: (nodes) => {
7575
return nodes.length === 1;
7676
},
@@ -86,7 +86,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
8686

8787
material.addContextMenuOption({
8888
name: 'copy',
89-
title: intl('Copy.1'),
89+
title: intl('Copy'),
9090
condition(nodes) {
9191
return nodes.length > 0;
9292
},

packages/engine/src/locale/en-US.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"NotValidNodeData": "Not valid node data",
33
"SelectComponents": "Select components",
4+
"CopyAndPaste": "Copy and Paste",
45
"Copy": "Copy",
5-
"Copy.1": "Copy",
66
"PasteToTheBottom": "Paste to the bottom",
77
"PasteToTheInside": "Paste to the inside",
88
"Delete": "Delete"

packages/engine/src/locale/zh-CN.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"NotValidNodeData": "不是有效的节点数据",
33
"SelectComponents": "选择组件",
4-
"Copy": "复制",
5-
"Copy.1": "拷贝",
4+
"CopyAndPaste": "复制",
5+
"Copy": "拷贝",
66
"PasteToTheBottom": "粘贴至下方",
77
"PasteToTheInside": "粘贴至内部",
88
"Delete": "删除"

packages/shell/src/components/context-menu.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import React from 'react';
66

77
export function ContextMenu({ children, menus }: {
88
menus: IPublicTypeContextMenuAction[];
9-
children: React.ReactElement[];
10-
}): React.ReactElement<any, string | React.JSXElementConstructor<any>>[] {
9+
children: React.ReactElement[] | React.ReactElement;
10+
}): React.ReactElement<any, string | React.JSXElementConstructor<any>> {
1111
if (!engineConfig.get('enableContextMenu')) {
12-
return children;
12+
return (
13+
<>{ children }</>
14+
);
1315
}
1416

1517
const handleContextMenu = (event: React.MouseEvent) => {
@@ -26,6 +28,10 @@ export function ContextMenu({ children, menus }: {
2628
destroy,
2729
}));
2830

31+
if (!children?.length) {
32+
return;
33+
}
34+
2935
const menuInstance = Menu.create({
3036
target: event.target,
3137
offset: [event.clientX - left, event.clientY - top],
@@ -42,5 +48,7 @@ export function ContextMenu({ children, menus }: {
4248
{ onContextMenu: handleContextMenu },
4349
));
4450

45-
return childrenWithContextMenu;
51+
return (
52+
<>{childrenWithContextMenu}</>
53+
);
4654
}

packages/types/src/shell/api/commonUI.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ export interface IPublicApiCommonUI {
4949

5050
get ContextMenu(): (props: {
5151
menus: IPublicTypeContextMenuAction[];
52-
children: React.ReactElement[];
53-
}) => ReactElement[];
52+
children: React.ReactElement[] | React.ReactElement;
53+
}) => ReactElement;
5454
}

packages/types/src/shell/type/context-menu.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ export interface IPublicTypeContextMenuAction {
2626
* 菜单项类型
2727
* Menu item type
2828
* @see IPublicEnumContextMenuType
29-
* @default IPublicEnumPContextMenuType.MENU_ITEM
29+
* @default IPublicEnumContextMenuType.MENU_ITEM
3030
*/
3131
type?: IPublicEnumContextMenuType;
3232

3333
/**
3434
* 点击时执行的动作,可选
3535
* Action to execute on click, optional
3636
*/
37-
action?: (nodes: IPublicModelNode[]) => void;
37+
action?: (nodes: IPublicModelNode[], event?: MouseEvent) => void;
3838

3939
/**
4040
* 子菜单项或生成子节点的函数,可选,仅支持两级

packages/utils/src/context-menu.tsx

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -89,42 +89,61 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
8989
return children;
9090
}
9191

92+
let destroyFn: Function | undefined;
9293
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: {
9394
nodes?: IPublicModelNode[] | null;
9495
destroy?: Function;
96+
event?: MouseEvent;
9597
}, level = 1): IPublicTypeContextMenuItem[] {
98+
destroyFn?.();
99+
destroyFn = options.destroy;
100+
96101
const { nodes, destroy } = options;
97102
if (level > MAX_LEVEL) {
98103
logger.warn('context menu level is too deep, please check your context menu config');
99104
return [];
100105
}
101106

102-
return menus.filter(menu => !menu.condition || (menu.condition && menu.condition(nodes || []))).map((menu) => {
103-
const {
104-
name,
105-
title,
106-
type = IPublicEnumContextMenuType.MENU_ITEM,
107-
} = menu;
108-
109-
const result: IPublicTypeContextMenuItem = {
110-
name,
111-
title,
112-
type,
113-
action: () => {
114-
destroy?.();
115-
menu.action?.(nodes || []);
116-
},
117-
disabled: menu.disabled && menu.disabled(nodes || []) || false,
118-
};
119-
120-
if ('items' in menu && menu.items) {
121-
result.items = parseContextMenuProperties(
122-
typeof menu.items === 'function' ? menu.items(nodes || []) : menu.items,
123-
options,
124-
level + 1,
125-
);
126-
}
107+
return menus
108+
.filter(menu => !menu.condition || (menu.condition && menu.condition(nodes || [])))
109+
.map((menu) => {
110+
const {
111+
name,
112+
title,
113+
type = IPublicEnumContextMenuType.MENU_ITEM,
114+
} = menu;
115+
116+
const result: IPublicTypeContextMenuItem = {
117+
name,
118+
title,
119+
type,
120+
action: () => {
121+
destroy?.();
122+
menu.action?.(nodes || [], options.event);
123+
},
124+
disabled: menu.disabled && menu.disabled(nodes || []) || false,
125+
};
126+
127+
if ('items' in menu && menu.items) {
128+
result.items = parseContextMenuProperties(
129+
typeof menu.items === 'function' ? menu.items(nodes || []) : menu.items,
130+
options,
131+
level + 1,
132+
);
133+
}
127134

128-
return result;
129-
});
135+
return result;
136+
})
137+
.reduce((menus: IPublicTypeContextMenuItem[], currentMenu: IPublicTypeContextMenuItem) => {
138+
if (!currentMenu.name) {
139+
return menus.concat([currentMenu]);
140+
}
141+
142+
const index = menus.find(item => item.name === currentMenu.name);
143+
if (!index) {
144+
return menus.concat([currentMenu]);
145+
} else {
146+
return menus;
147+
}
148+
}, []);
130149
}

0 commit comments

Comments
 (0)