Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(context-menu): fix context menu bugs #2825

Merged
merged 1 commit into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/docs/api/commonUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ CommonUI API 是一个专为低代码引擎设计的组件 UI 库,使用它开
|------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------|
| name | 动作的唯一标识符<br/>Unique identifier for the action | string | |
| title | 显示的标题,可以是字符串或国际化数据<br/>Display title, can be a string or internationalized data | string \| IPublicTypeI18nData (optional) | |
| type | 菜单项类型<br/>Menu item type | IPublicEnumContextMenuType (optional) | IPublicEnumPContextMenuType.MENU_ITEM |
| type | 菜单项类型<br/>Menu item type | IPublicEnumContextMenuType (optional) | IPublicEnumContextMenuType.MENU_ITEM |
| action | 点击时执行的动作,可选<br/>Action to execute on click, optional | (nodes: IPublicModelNode[]) => void (optional) | |
| items | 子菜单项或生成子节点的函数,可选,仅支持两级<br/>Sub-menu items or function to generate child node, optional | Omit<IPublicTypeContextMenuAction, 'items'>[] \| ((nodes: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]) (optional) | |
| condition | 显示条件函数<br/>Function to determine display condition | (nodes: IPublicModelNode[]) => boolean (optional) | |
Expand Down
121 changes: 103 additions & 18 deletions packages/designer/src/context-menu-actions.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<string, ContextMenuActions> = 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[] = [];
Expand All @@ -28,6 +121,8 @@ export class ContextMenuActions implements IContextMenuActions {

enableContextMenu: boolean;

id: string = uniqueId('contextMenu');;

constructor(designer: IDesigner) {
this.designer = designer;
this.dispose = [];
Expand All @@ -42,6 +137,8 @@ export class ContextMenuActions implements IContextMenuActions {
this.initEvent();
}
});

globalContextMenuActions.registerContextMenuActions(this);
}

handleContextMenu = (
Expand All @@ -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?.();
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -142,6 +227,6 @@ export class ContextMenuActions implements IContextMenuActions {
}

adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) {
this.adjustMenuLayoutFn = fn;
adjustMenuLayoutFn = fn;
}
}
4 changes: 2 additions & 2 deletions packages/engine/src/inner-plugins/default-context-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand All @@ -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;
},
Expand Down
2 changes: 1 addition & 1 deletion packages/engine/src/locale/en-US.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 2 additions & 2 deletions packages/engine/src/locale/zh-CN.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"NotValidNodeData": "不是有效的节点数据",
"SelectComponents": "选择组件",
"Copy": "复制",
"Copy.1": "拷贝",
"CopyAndPaste": "复制",
"Copy": "拷贝",
"PasteToTheBottom": "粘贴至下方",
"PasteToTheInside": "粘贴至内部",
"Delete": "删除"
Expand Down
16 changes: 12 additions & 4 deletions packages/shell/src/components/context-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import React from 'react';

export function ContextMenu({ children, menus }: {
menus: IPublicTypeContextMenuAction[];
children: React.ReactElement[];
}): React.ReactElement<any, string | React.JSXElementConstructor<any>>[] {
children: React.ReactElement[] | React.ReactElement;
}): React.ReactElement<any, string | React.JSXElementConstructor<any>> {
if (!engineConfig.get('enableContextMenu')) {
return children;
return (
<>{ children }</>
);
}

const handleContextMenu = (event: React.MouseEvent) => {
Expand All @@ -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],
Expand All @@ -42,5 +48,7 @@ export function ContextMenu({ children, menus }: {
{ onContextMenu: handleContextMenu },
));

return childrenWithContextMenu;
return (
<>{childrenWithContextMenu}</>
);
}
4 changes: 2 additions & 2 deletions packages/types/src/shell/api/commonUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ export interface IPublicApiCommonUI {

get ContextMenu(): (props: {
menus: IPublicTypeContextMenuAction[];
children: React.ReactElement[];
}) => ReactElement[];
children: React.ReactElement[] | React.ReactElement;
}) => ReactElement;
}
4 changes: 2 additions & 2 deletions packages/types/src/shell/type/context-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ export interface IPublicTypeContextMenuAction {
* 菜单项类型
* Menu item type
* @see IPublicEnumContextMenuType
* @default IPublicEnumPContextMenuType.MENU_ITEM
* @default IPublicEnumContextMenuType.MENU_ITEM
*/
type?: IPublicEnumContextMenuType;

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

/**
* 子菜单项或生成子节点的函数,可选,仅支持两级
Expand Down
73 changes: 46 additions & 27 deletions packages/utils/src/context-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,42 +89,61 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
return children;
}

let destroyFn: Function | undefined;
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], 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;
}
}, []);
}
Loading