Skip to content

Commit d02dea5

Browse files
committed
feat(engine): add context menu
1 parent ba53d6c commit d02dea5

File tree

27 files changed

+743
-5
lines changed

27 files changed

+743
-5
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,5 @@ typings/
108108
# codealike
109109
codealike.json
110110
.node
111+
112+
.must.config.js

docs/docs/api/commonUI.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,26 @@ CommonUI API 是一个专为低代码引擎设计的组件 UI 库,使用它开
2929
| className | className | string (optional) | |
3030
| onClick | 点击事件 | () => void (optional) | |
3131

32+
### ContextMenu
33+
34+
| 参数 | 说明 | 类型 | 默认值 |
35+
|--------|----------------------------------------------------|------------------------------------|--------|
36+
| menus | 定义上下文菜单的动作数组 | IPublicTypeContextMenuAction[] | |
37+
| children | 组件的子元素 | React.ReactElement[] | |
38+
39+
**IPublicTypeContextMenuAction Interface**
40+
41+
| 参数 | 说明 | 类型 | 默认值 |
42+
|------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------|
43+
| name | 动作的唯一标识符<br>Unique identifier for the action | string | |
44+
| 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 |
46+
| action | 点击时执行的动作,可选<br>Action to execute on click, optional | (nodes: IPublicModelNode[]) => void (optional) | |
47+
| items | 子菜单项或生成子节点的函数,可选,仅支持两级<br>Sub-menu items or function to generate child node, optional | Omit<IPublicTypeContextMenuAction, 'items'>[] \| ((nodes: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]) (optional) | |
48+
| condition | 显示条件函数<br>Function to determine display condition | (nodes: IPublicModelNode[]) => boolean (optional) | |
49+
| disabled | 禁用条件函数,可选<br>Function to determine disabled condition, optional | (nodes: IPublicModelNode[]) => boolean (optional) | |
50+
51+
3252
### Balloon
3353
详细文档: [Balloon Documentation](https://fusion.design/pc/component/balloon)
3454

docs/docs/api/configOptions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ config.set('enableCondition', false)
185185

186186
`@type {boolean}` `@default {false}`
187187

188+
#### enableContextMenu - 开启右键菜单
189+
190+
`@type {boolean}` `@default {false}`
191+
192+
是否开启右键菜单
193+
188194
#### disableDetecting
189195

190196
`@type {boolean}` `@default {false}`

docs/docs/guide/expand/editor/theme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ sidebar_position: 9
128128
- `--pane-title-height`: 面板标题高度
129129
- `--pane-title-font-size`: 面板标题字体大小
130130
- `--pane-title-padding`: 面板标题边距
131+
- `--context-menu-item-height`: 右键菜单项高度
131132

132133

133134

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.engine-context-menu {
2+
&.next-menu.next-ver .next-menu-item {
3+
padding-right: 30px;
4+
5+
.next-menu-item-inner {
6+
height: var(--context-menu-item-height, 30px);
7+
line-height: var(--context-menu-item-height, 30px);
8+
}
9+
}
10+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
2+
import { IDesigner, INode } from './designer';
3+
import { parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
4+
import { Menu } from '@alifd/next';
5+
import { engineConfig } from '@alilc/lowcode-editor-core';
6+
import './context-menu-actions.scss';
7+
8+
export interface IContextMenuActions {
9+
actions: IPublicTypeContextMenuAction[];
10+
11+
adjustMenuLayoutFn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[];
12+
13+
addMenuAction: IPublicApiMaterial['addContextMenuOption'];
14+
15+
removeMenuAction: IPublicApiMaterial['removeContextMenuOption'];
16+
17+
adjustMenuLayout: IPublicApiMaterial['adjustContextMenuLayout'];
18+
}
19+
20+
export class ContextMenuActions implements IContextMenuActions {
21+
actions: IPublicTypeContextMenuAction[] = [];
22+
23+
designer: IDesigner;
24+
25+
dispose: Function[];
26+
27+
enableContextMenu: boolean;
28+
29+
constructor(designer: IDesigner) {
30+
this.designer = designer;
31+
this.dispose = [];
32+
33+
engineConfig.onGot('enableContextMenu', (enable) => {
34+
if (this.enableContextMenu === enable) {
35+
return;
36+
}
37+
this.enableContextMenu = enable;
38+
this.dispose.forEach(d => d());
39+
if (enable) {
40+
this.initEvent();
41+
}
42+
});
43+
}
44+
45+
handleContextMenu = (
46+
nodes: INode[],
47+
event: MouseEvent,
48+
) => {
49+
const designer = this.designer;
50+
event.stopPropagation();
51+
event.preventDefault();
52+
53+
const actions = designer.contextMenuActions.actions;
54+
55+
const { bounds } = designer.project.simulator?.viewport || { bounds: { left: 0, top: 0 } };
56+
const { left: simulatorLeft, top: simulatorTop } = bounds;
57+
58+
let destroyFn: Function | undefined;
59+
60+
const destroy = () => {
61+
destroyFn?.();
62+
};
63+
64+
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
65+
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
66+
destroy,
67+
});
68+
69+
if (!menus.length) {
70+
return;
71+
}
72+
73+
const layoutMenu = designer.contextMenuActions.adjustMenuLayoutFn(menus);
74+
75+
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
76+
destroy,
77+
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
78+
designer,
79+
});
80+
81+
const target = event.target;
82+
83+
const { top, left } = target?.getBoundingClientRect();
84+
85+
const menuInstance = Menu.create({
86+
target: event.target,
87+
offset: [event.clientX - left + simulatorLeft, event.clientY - top + simulatorTop],
88+
children: menuNode,
89+
className: 'engine-context-menu',
90+
});
91+
92+
destroyFn = (menuInstance as any).destroy;
93+
};
94+
95+
initEvent() {
96+
const designer = this.designer;
97+
this.dispose.push(
98+
designer.editor.eventBus.on('designer.builtinSimulator.contextmenu', ({
99+
node,
100+
originalEvent,
101+
}: {
102+
node: INode;
103+
originalEvent: MouseEvent;
104+
}) => {
105+
// 如果右键的节点不在 当前选中的节点中,选中该节点
106+
if (!designer.currentSelection.has(node.id)) {
107+
designer.currentSelection.select(node.id);
108+
}
109+
const nodes = designer.currentSelection.getNodes();
110+
this.handleContextMenu(nodes, originalEvent);
111+
}),
112+
(() => {
113+
const handleContextMenu = (e: MouseEvent) => {
114+
this.handleContextMenu([], e);
115+
};
116+
117+
document.addEventListener('contextmenu', handleContextMenu);
118+
119+
return () => {
120+
document.removeEventListener('contextmenu', handleContextMenu);
121+
};
122+
})(),
123+
);
124+
}
125+
126+
adjustMenuLayoutFn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[] = (actions) => actions;
127+
128+
addMenuAction(action: IPublicTypeContextMenuAction) {
129+
this.actions.push({
130+
type: IPublicEnumContextMenuType.MENU_ITEM,
131+
...action,
132+
});
133+
}
134+
135+
removeMenuAction(name: string) {
136+
const i = this.actions.findIndex((action) => action.name === name);
137+
if (i > -1) {
138+
this.actions.splice(i, 1);
139+
}
140+
}
141+
142+
adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) {
143+
this.adjustMenuLayoutFn = fn;
144+
}
145+
}

packages/designer/src/designer/designer.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '@alilc/lowcode-types';
2121
import { mergeAssets, IPublicTypeAssetsJson, isNodeSchema, isDragNodeObject, isDragNodeDataObject, isLocationChildrenDetail, Logger } from '@alilc/lowcode-utils';
2222
import { IProject, Project } from '../project';
23-
import { Node, DocumentModel, insertChildren, INode } from '../document';
23+
import { Node, DocumentModel, insertChildren, INode, ISelection } from '../document';
2424
import { ComponentMeta, IComponentMeta } from '../component-meta';
2525
import { INodeSelector, Component } from '../simulator';
2626
import { Scroller } from './scroller';
@@ -32,6 +32,7 @@ import { OffsetObserver, createOffsetObserver } from './offset-observer';
3232
import { ISettingTopEntry, SettingTopEntry } from './setting';
3333
import { BemToolsManager } from '../builtin-simulator/bem-tools/manager';
3434
import { ComponentActions } from '../component-actions';
35+
import { ContextMenuActions, IContextMenuActions } from '../context-menu-actions';
3536

3637
const logger = new Logger({ level: 'warn', bizName: 'designer' });
3738

@@ -72,12 +73,16 @@ export interface IDesigner {
7273

7374
get componentActions(): ComponentActions;
7475

76+
get contextMenuActions(): ContextMenuActions;
77+
7578
get editor(): IPublicModelEditor;
7679

7780
get detecting(): Detecting;
7881

7982
get simulatorComponent(): ComponentType<any> | undefined;
8083

84+
get currentSelection(): ISelection;
85+
8186
createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller;
8287

8388
refreshComponentMetasMap(): void;
@@ -122,6 +127,8 @@ export class Designer implements IDesigner {
122127

123128
readonly componentActions = new ComponentActions();
124129

130+
readonly contextMenuActions: IContextMenuActions;
131+
125132
readonly activeTracker = new ActiveTracker();
126133

127134
readonly detecting = new Detecting();
@@ -198,6 +205,8 @@ export class Designer implements IDesigner {
198205
this.postEvent('dragstart', e);
199206
});
200207

208+
this.contextMenuActions = new ContextMenuActions(this);
209+
201210
this.dragon.onDrag((e) => {
202211
if (this.props?.onDrag) {
203212
this.props.onDrag(e);

packages/designer/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './project';
66
export * from './builtin-simulator';
77
export * from './plugin';
88
export * from './types';
9+
export * from './context-menu-actions';

packages/editor-core/src/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ const VALID_ENGINE_OPTIONS = {
159159
type: 'function',
160160
description: '应用级设计模式下,窗口为空时展示的占位组件',
161161
},
162+
enableContextMenu: {
163+
type: 'boolean',
164+
description: '是否开启右键菜单',
165+
default: false,
166+
},
162167
hideComponentAction: {
163168
type: 'boolean',
164169
description: '是否隐藏设计器辅助层',

packages/engine/src/engine-core.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { setterRegistry } from './inner-plugins/setter-registry';
6262
import { defaultPanelRegistry } from './inner-plugins/default-panel-registry';
6363
import { shellModelFactory } from './modules/shell-model-factory';
6464
import { builtinHotkey } from './inner-plugins/builtin-hotkey';
65+
import { defaultContextMenu } from './inner-plugins/default-context-menu';
6566
import { OutlinePlugin } from '@alilc/lowcode-plugin-outline-pane';
6667

6768
export * from './modules/skeleton-types';
@@ -78,6 +79,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins
7879
await plugins.register(defaultPanelRegistryPlugin);
7980
await plugins.register(builtinHotkey);
8081
await plugins.register(registerDefaults, {}, { autoInit: true });
82+
await plugins.register(defaultContextMenu);
8183

8284
return () => {
8385
plugins.delete(OutlinePlugin.pluginName);
@@ -86,6 +88,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins
8688
plugins.delete(defaultPanelRegistryPlugin.pluginName);
8789
plugins.delete(builtinHotkey.pluginName);
8890
plugins.delete(registerDefaults.pluginName);
91+
plugins.delete(defaultContextMenu.pluginName);
8992
};
9093
}
9194

0 commit comments

Comments
 (0)