diff --git a/docs/docs/api/commonUI.md b/docs/docs/api/commonUI.md
index 82b57c389..f3afe1fc2 100644
--- a/docs/docs/api/commonUI.md
+++ b/docs/docs/api/commonUI.md
@@ -42,7 +42,7 @@ CommonUI API 是一个专为低代码引擎设计的组件 UI 库,使用它开
|------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------|
| name | 动作的唯一标识符
Unique identifier for the action | string | |
| title | 显示的标题,可以是字符串或国际化数据
Display title, can be a string or internationalized data | string \| IPublicTypeI18nData (optional) | |
-| type | 菜单项类型
Menu item type | IPublicEnumContextMenuType (optional) | IPublicEnumPContextMenuType.MENU_ITEM |
+| type | 菜单项类型
Menu item type | IPublicEnumContextMenuType (optional) | IPublicEnumContextMenuType.MENU_ITEM |
| action | 点击时执行的动作,可选
Action to execute on click, optional | (nodes: IPublicModelNode[]) => void (optional) | |
| items | 子菜单项或生成子节点的函数,可选,仅支持两级
Sub-menu items or function to generate child node, optional | Omit[] \| ((nodes: IPublicModelNode[]) => Omit[]) (optional) | |
| condition | 显示条件函数
Function to determine display condition | (nodes: IPublicModelNode[]) => boolean (optional) | |
diff --git a/packages/designer/src/context-menu-actions.ts b/packages/designer/src/context-menu-actions.ts
index 55a83ef2f..8c210d89e 100644
--- a/packages/designer/src/context-menu-actions.ts
+++ b/packages/designer/src/context-menu-actions.ts
@@ -1,6 +1,6 @@
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
import { IDesigner, INode } from './designer';
-import { parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
+import { parseContextMenuAsReactNode, parseContextMenuProperties, uniqueId } from '@alilc/lowcode-utils';
import { Menu } from '@alifd/next';
import { engineConfig } from '@alilc/lowcode-editor-core';
import './context-menu-actions.scss';
@@ -17,7 +17,100 @@ export interface IContextMenuActions {
adjustMenuLayout: IPublicApiMaterial['adjustContextMenuLayout'];
}
-let destroyFn: Function | undefined;
+let adjustMenuLayoutFn: Function = (actions: IPublicTypeContextMenuAction[]) => actions;
+
+export class GlobalContextMenuActions {
+ enableContextMenu: boolean;
+
+ dispose: Function[];
+
+ contextMenuActionsMap: Map = new Map();
+
+ constructor() {
+ this.dispose = [];
+
+ engineConfig.onGot('enableContextMenu', (enable) => {
+ if (this.enableContextMenu === enable) {
+ return;
+ }
+ this.enableContextMenu = enable;
+ this.dispose.forEach(d => d());
+ if (enable) {
+ this.initEvent();
+ }
+ });
+ }
+
+ handleContextMenu = (
+ event: MouseEvent,
+ ) => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ const actions: IPublicTypeContextMenuAction[] = [];
+ this.contextMenuActionsMap.forEach((contextMenu) => {
+ actions.push(...contextMenu.actions);
+ });
+
+ let destroyFn: Function | undefined;
+
+ const destroy = () => {
+ destroyFn?.();
+ };
+
+ const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
+ nodes: [],
+ destroy,
+ event,
+ });
+
+ if (!menus.length) {
+ return;
+ }
+
+ const layoutMenu = adjustMenuLayoutFn(menus);
+
+ const menuNode = parseContextMenuAsReactNode(layoutMenu, {
+ destroy,
+ nodes: [],
+ });
+
+ const target = event.target;
+
+ const { top, left } = target?.getBoundingClientRect();
+
+ const menuInstance = Menu.create({
+ target: event.target,
+ offset: [event.clientX - left, event.clientY - top],
+ children: menuNode,
+ className: 'engine-context-menu',
+ });
+
+ destroyFn = (menuInstance as any).destroy;
+ };
+
+ initEvent() {
+ this.dispose.push(
+ (() => {
+ const handleContextMenu = (e: MouseEvent) => {
+ this.handleContextMenu(e);
+ };
+
+ document.addEventListener('contextmenu', handleContextMenu);
+
+ return () => {
+ document.removeEventListener('contextmenu', handleContextMenu);
+ };
+ })(),
+ );
+ }
+
+ registerContextMenuActions(contextMenu: ContextMenuActions) {
+ this.contextMenuActionsMap.set(contextMenu.id, contextMenu);
+ }
+}
+
+const globalContextMenuActions = new GlobalContextMenuActions();
export class ContextMenuActions implements IContextMenuActions {
actions: IPublicTypeContextMenuAction[] = [];
@@ -28,6 +121,8 @@ export class ContextMenuActions implements IContextMenuActions {
enableContextMenu: boolean;
+ id: string = uniqueId('contextMenu');;
+
constructor(designer: IDesigner) {
this.designer = designer;
this.dispose = [];
@@ -42,6 +137,8 @@ export class ContextMenuActions implements IContextMenuActions {
this.initEvent();
}
});
+
+ globalContextMenuActions.registerContextMenuActions(this);
}
handleContextMenu = (
@@ -57,7 +154,7 @@ export class ContextMenuActions implements IContextMenuActions {
const { bounds } = designer.project.simulator?.viewport || { bounds: { left: 0, top: 0 } };
const { left: simulatorLeft, top: simulatorTop } = bounds;
- destroyFn?.();
+ let destroyFn: Function | undefined;
const destroy = () => {
destroyFn?.();
@@ -66,13 +163,14 @@ export class ContextMenuActions implements IContextMenuActions {
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
destroy,
+ event,
});
if (!menus.length) {
return;
}
- const layoutMenu = designer.contextMenuActions.adjustMenuLayoutFn(menus);
+ const layoutMenu = adjustMenuLayoutFn(menus);
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
destroy,
@@ -111,22 +209,9 @@ export class ContextMenuActions implements IContextMenuActions {
const nodes = designer.currentSelection.getNodes();
this.handleContextMenu(nodes, originalEvent);
}),
- (() => {
- const handleContextMenu = (e: MouseEvent) => {
- this.handleContextMenu([], e);
- };
-
- document.addEventListener('contextmenu', handleContextMenu);
-
- return () => {
- document.removeEventListener('contextmenu', handleContextMenu);
- };
- })(),
);
}
- adjustMenuLayoutFn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[] = (actions) => actions;
-
addMenuAction(action: IPublicTypeContextMenuAction) {
this.actions.push({
type: IPublicEnumContextMenuType.MENU_ITEM,
@@ -142,6 +227,6 @@ export class ContextMenuActions implements IContextMenuActions {
}
adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) {
- this.adjustMenuLayoutFn = fn;
+ adjustMenuLayoutFn = fn;
}
}
\ No newline at end of file
diff --git a/packages/engine/src/inner-plugins/default-context-menu.ts b/packages/engine/src/inner-plugins/default-context-menu.ts
index 50a86fcec..fc1da96b4 100644
--- a/packages/engine/src/inner-plugins/default-context-menu.ts
+++ b/packages/engine/src/inner-plugins/default-context-menu.ts
@@ -70,7 +70,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
material.addContextMenuOption({
name: 'copyAndPaste',
- title: intl('Copy'),
+ title: intl('CopyAndPaste'),
condition: (nodes) => {
return nodes.length === 1;
},
@@ -86,7 +86,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
material.addContextMenuOption({
name: 'copy',
- title: intl('Copy.1'),
+ title: intl('Copy'),
condition(nodes) {
return nodes.length > 0;
},
diff --git a/packages/engine/src/locale/en-US.json b/packages/engine/src/locale/en-US.json
index 1cdb06f28..e93160707 100644
--- a/packages/engine/src/locale/en-US.json
+++ b/packages/engine/src/locale/en-US.json
@@ -1,8 +1,8 @@
{
"NotValidNodeData": "Not valid node data",
"SelectComponents": "Select components",
+ "CopyAndPaste": "Copy and Paste",
"Copy": "Copy",
- "Copy.1": "Copy",
"PasteToTheBottom": "Paste to the bottom",
"PasteToTheInside": "Paste to the inside",
"Delete": "Delete"
diff --git a/packages/engine/src/locale/zh-CN.json b/packages/engine/src/locale/zh-CN.json
index ba0f87f8e..9b68b7149 100644
--- a/packages/engine/src/locale/zh-CN.json
+++ b/packages/engine/src/locale/zh-CN.json
@@ -1,8 +1,8 @@
{
"NotValidNodeData": "不是有效的节点数据",
"SelectComponents": "选择组件",
- "Copy": "复制",
- "Copy.1": "拷贝",
+ "CopyAndPaste": "复制",
+ "Copy": "拷贝",
"PasteToTheBottom": "粘贴至下方",
"PasteToTheInside": "粘贴至内部",
"Delete": "删除"
diff --git a/packages/shell/src/components/context-menu.tsx b/packages/shell/src/components/context-menu.tsx
index 0085e1c77..f12f6ca93 100644
--- a/packages/shell/src/components/context-menu.tsx
+++ b/packages/shell/src/components/context-menu.tsx
@@ -6,10 +6,12 @@ import React from 'react';
export function ContextMenu({ children, menus }: {
menus: IPublicTypeContextMenuAction[];
- children: React.ReactElement[];
-}): React.ReactElement>[] {
+ children: React.ReactElement[] | React.ReactElement;
+}): React.ReactElement> {
if (!engineConfig.get('enableContextMenu')) {
- return children;
+ return (
+ <>{ children }>
+ );
}
const handleContextMenu = (event: React.MouseEvent) => {
@@ -26,6 +28,10 @@ export function ContextMenu({ children, menus }: {
destroy,
}));
+ if (!children?.length) {
+ return;
+ }
+
const menuInstance = Menu.create({
target: event.target,
offset: [event.clientX - left, event.clientY - top],
@@ -42,5 +48,7 @@ export function ContextMenu({ children, menus }: {
{ onContextMenu: handleContextMenu },
));
- return childrenWithContextMenu;
+ return (
+ <>{childrenWithContextMenu}>
+ );
}
\ No newline at end of file
diff --git a/packages/types/src/shell/api/commonUI.ts b/packages/types/src/shell/api/commonUI.ts
index dcc6fab0c..71e2bbe83 100644
--- a/packages/types/src/shell/api/commonUI.ts
+++ b/packages/types/src/shell/api/commonUI.ts
@@ -49,6 +49,6 @@ export interface IPublicApiCommonUI {
get ContextMenu(): (props: {
menus: IPublicTypeContextMenuAction[];
- children: React.ReactElement[];
- }) => ReactElement[];
+ children: React.ReactElement[] | React.ReactElement;
+ }) => ReactElement;
}
\ No newline at end of file
diff --git a/packages/types/src/shell/type/context-menu.ts b/packages/types/src/shell/type/context-menu.ts
index 595893d32..1eeb93d69 100644
--- a/packages/types/src/shell/type/context-menu.ts
+++ b/packages/types/src/shell/type/context-menu.ts
@@ -26,7 +26,7 @@ export interface IPublicTypeContextMenuAction {
* 菜单项类型
* Menu item type
* @see IPublicEnumContextMenuType
- * @default IPublicEnumPContextMenuType.MENU_ITEM
+ * @default IPublicEnumContextMenuType.MENU_ITEM
*/
type?: IPublicEnumContextMenuType;
@@ -34,7 +34,7 @@ export interface IPublicTypeContextMenuAction {
* 点击时执行的动作,可选
* Action to execute on click, optional
*/
- action?: (nodes: IPublicModelNode[]) => void;
+ action?: (nodes: IPublicModelNode[], event?: MouseEvent) => void;
/**
* 子菜单项或生成子节点的函数,可选,仅支持两级
diff --git a/packages/utils/src/context-menu.tsx b/packages/utils/src/context-menu.tsx
index f65bc312f..f28619df6 100644
--- a/packages/utils/src/context-menu.tsx
+++ b/packages/utils/src/context-menu.tsx
@@ -89,42 +89,61 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
return children;
}
+let destroyFn: Function | undefined;
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit)[], options: {
nodes?: IPublicModelNode[] | null;
destroy?: Function;
+ event?: MouseEvent;
}, level = 1): IPublicTypeContextMenuItem[] {
+ destroyFn?.();
+ destroyFn = options.destroy;
+
const { nodes, destroy } = options;
if (level > MAX_LEVEL) {
logger.warn('context menu level is too deep, please check your context menu config');
return [];
}
- return menus.filter(menu => !menu.condition || (menu.condition && menu.condition(nodes || []))).map((menu) => {
- const {
- name,
- title,
- type = IPublicEnumContextMenuType.MENU_ITEM,
- } = menu;
-
- const result: IPublicTypeContextMenuItem = {
- name,
- title,
- type,
- action: () => {
- destroy?.();
- menu.action?.(nodes || []);
- },
- disabled: menu.disabled && menu.disabled(nodes || []) || false,
- };
-
- if ('items' in menu && menu.items) {
- result.items = parseContextMenuProperties(
- typeof menu.items === 'function' ? menu.items(nodes || []) : menu.items,
- options,
- level + 1,
- );
- }
+ return menus
+ .filter(menu => !menu.condition || (menu.condition && menu.condition(nodes || [])))
+ .map((menu) => {
+ const {
+ name,
+ title,
+ type = IPublicEnumContextMenuType.MENU_ITEM,
+ } = menu;
+
+ const result: IPublicTypeContextMenuItem = {
+ name,
+ title,
+ type,
+ action: () => {
+ destroy?.();
+ menu.action?.(nodes || [], options.event);
+ },
+ disabled: menu.disabled && menu.disabled(nodes || []) || false,
+ };
+
+ if ('items' in menu && menu.items) {
+ result.items = parseContextMenuProperties(
+ typeof menu.items === 'function' ? menu.items(nodes || []) : menu.items,
+ options,
+ level + 1,
+ );
+ }
- return result;
- });
+ return result;
+ })
+ .reduce((menus: IPublicTypeContextMenuItem[], currentMenu: IPublicTypeContextMenuItem) => {
+ if (!currentMenu.name) {
+ return menus.concat([currentMenu]);
+ }
+
+ const index = menus.find(item => item.name === currentMenu.name);
+ if (!index) {
+ return menus.concat([currentMenu]);
+ } else {
+ return menus;
+ }
+ }, []);
}
\ No newline at end of file