mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-12 11:09:21 +00:00
fix(context-menu): fix context menu bugs
This commit is contained in:
parent
8f0291fc3e
commit
c381b85f0a
@ -42,7 +42,7 @@ CommonUI API 是一个专为低代码引擎设计的组件 UI 库,使用它开
|
|||||||
|------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------|
|
|------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------|
|
||||||
| name | 动作的唯一标识符<br/>Unique identifier for the action | string | |
|
| name | 动作的唯一标识符<br/>Unique identifier for the action | string | |
|
||||||
| title | 显示的标题,可以是字符串或国际化数据<br/>Display title, can be a string or internationalized data | string \| IPublicTypeI18nData (optional) | |
|
| 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) | |
|
| 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) | |
|
| 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) | |
|
| condition | 显示条件函数<br/>Function to determine display condition | (nodes: IPublicModelNode[]) => boolean (optional) | |
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
|
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
|
||||||
import { IDesigner, INode } from './designer';
|
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 { Menu } from '@alifd/next';
|
||||||
import { engineConfig } from '@alilc/lowcode-editor-core';
|
import { engineConfig } from '@alilc/lowcode-editor-core';
|
||||||
import './context-menu-actions.scss';
|
import './context-menu-actions.scss';
|
||||||
@ -17,19 +17,16 @@ export interface IContextMenuActions {
|
|||||||
adjustMenuLayout: IPublicApiMaterial['adjustContextMenuLayout'];
|
adjustMenuLayout: IPublicApiMaterial['adjustContextMenuLayout'];
|
||||||
}
|
}
|
||||||
|
|
||||||
let destroyFn: Function | undefined;
|
let adjustMenuLayoutFn: Function = (actions: IPublicTypeContextMenuAction[]) => actions;
|
||||||
|
|
||||||
export class ContextMenuActions implements IContextMenuActions {
|
export class GlobalContextMenuActions {
|
||||||
actions: IPublicTypeContextMenuAction[] = [];
|
enableContextMenu: boolean;
|
||||||
|
|
||||||
designer: IDesigner;
|
|
||||||
|
|
||||||
dispose: Function[];
|
dispose: Function[];
|
||||||
|
|
||||||
enableContextMenu: boolean;
|
contextMenuActionsMap: Map<string, ContextMenuActions> = new Map();
|
||||||
|
|
||||||
constructor(designer: IDesigner) {
|
constructor() {
|
||||||
this.designer = designer;
|
|
||||||
this.dispose = [];
|
this.dispose = [];
|
||||||
|
|
||||||
engineConfig.onGot('enableContextMenu', (enable) => {
|
engineConfig.onGot('enableContextMenu', (enable) => {
|
||||||
@ -44,6 +41,106 @@ export class ContextMenuActions implements IContextMenuActions {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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[] = [];
|
||||||
|
|
||||||
|
designer: IDesigner;
|
||||||
|
|
||||||
|
dispose: Function[];
|
||||||
|
|
||||||
|
enableContextMenu: boolean;
|
||||||
|
|
||||||
|
id: string = uniqueId('contextMenu');;
|
||||||
|
|
||||||
|
constructor(designer: IDesigner) {
|
||||||
|
this.designer = designer;
|
||||||
|
this.dispose = [];
|
||||||
|
|
||||||
|
engineConfig.onGot('enableContextMenu', (enable) => {
|
||||||
|
if (this.enableContextMenu === enable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.enableContextMenu = enable;
|
||||||
|
this.dispose.forEach(d => d());
|
||||||
|
if (enable) {
|
||||||
|
this.initEvent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
globalContextMenuActions.registerContextMenuActions(this);
|
||||||
|
}
|
||||||
|
|
||||||
handleContextMenu = (
|
handleContextMenu = (
|
||||||
nodes: INode[],
|
nodes: INode[],
|
||||||
event: MouseEvent,
|
event: MouseEvent,
|
||||||
@ -57,7 +154,7 @@ export class ContextMenuActions implements IContextMenuActions {
|
|||||||
const { bounds } = designer.project.simulator?.viewport || { bounds: { left: 0, top: 0 } };
|
const { bounds } = designer.project.simulator?.viewport || { bounds: { left: 0, top: 0 } };
|
||||||
const { left: simulatorLeft, top: simulatorTop } = bounds;
|
const { left: simulatorLeft, top: simulatorTop } = bounds;
|
||||||
|
|
||||||
destroyFn?.();
|
let destroyFn: Function | undefined;
|
||||||
|
|
||||||
const destroy = () => {
|
const destroy = () => {
|
||||||
destroyFn?.();
|
destroyFn?.();
|
||||||
@ -66,13 +163,14 @@ export class ContextMenuActions implements IContextMenuActions {
|
|||||||
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
|
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
|
||||||
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
|
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
|
||||||
destroy,
|
destroy,
|
||||||
|
event,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!menus.length) {
|
if (!menus.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const layoutMenu = designer.contextMenuActions.adjustMenuLayoutFn(menus);
|
const layoutMenu = adjustMenuLayoutFn(menus);
|
||||||
|
|
||||||
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
|
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
|
||||||
destroy,
|
destroy,
|
||||||
@ -111,22 +209,9 @@ export class ContextMenuActions implements IContextMenuActions {
|
|||||||
const nodes = designer.currentSelection.getNodes();
|
const nodes = designer.currentSelection.getNodes();
|
||||||
this.handleContextMenu(nodes, originalEvent);
|
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) {
|
addMenuAction(action: IPublicTypeContextMenuAction) {
|
||||||
this.actions.push({
|
this.actions.push({
|
||||||
type: IPublicEnumContextMenuType.MENU_ITEM,
|
type: IPublicEnumContextMenuType.MENU_ITEM,
|
||||||
@ -142,6 +227,6 @@ export class ContextMenuActions implements IContextMenuActions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) {
|
adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) {
|
||||||
this.adjustMenuLayoutFn = fn;
|
adjustMenuLayoutFn = fn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
|
|||||||
|
|
||||||
material.addContextMenuOption({
|
material.addContextMenuOption({
|
||||||
name: 'copyAndPaste',
|
name: 'copyAndPaste',
|
||||||
title: intl('Copy'),
|
title: intl('CopyAndPaste'),
|
||||||
condition: (nodes) => {
|
condition: (nodes) => {
|
||||||
return nodes.length === 1;
|
return nodes.length === 1;
|
||||||
},
|
},
|
||||||
@ -86,7 +86,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
|
|||||||
|
|
||||||
material.addContextMenuOption({
|
material.addContextMenuOption({
|
||||||
name: 'copy',
|
name: 'copy',
|
||||||
title: intl('Copy.1'),
|
title: intl('Copy'),
|
||||||
condition(nodes) {
|
condition(nodes) {
|
||||||
return nodes.length > 0;
|
return nodes.length > 0;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"NotValidNodeData": "Not valid node data",
|
"NotValidNodeData": "Not valid node data",
|
||||||
"SelectComponents": "Select components",
|
"SelectComponents": "Select components",
|
||||||
|
"CopyAndPaste": "Copy and Paste",
|
||||||
"Copy": "Copy",
|
"Copy": "Copy",
|
||||||
"Copy.1": "Copy",
|
|
||||||
"PasteToTheBottom": "Paste to the bottom",
|
"PasteToTheBottom": "Paste to the bottom",
|
||||||
"PasteToTheInside": "Paste to the inside",
|
"PasteToTheInside": "Paste to the inside",
|
||||||
"Delete": "Delete"
|
"Delete": "Delete"
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"NotValidNodeData": "不是有效的节点数据",
|
"NotValidNodeData": "不是有效的节点数据",
|
||||||
"SelectComponents": "选择组件",
|
"SelectComponents": "选择组件",
|
||||||
"Copy": "复制",
|
"CopyAndPaste": "复制",
|
||||||
"Copy.1": "拷贝",
|
"Copy": "拷贝",
|
||||||
"PasteToTheBottom": "粘贴至下方",
|
"PasteToTheBottom": "粘贴至下方",
|
||||||
"PasteToTheInside": "粘贴至内部",
|
"PasteToTheInside": "粘贴至内部",
|
||||||
"Delete": "删除"
|
"Delete": "删除"
|
||||||
|
|||||||
@ -6,10 +6,12 @@ import React from 'react';
|
|||||||
|
|
||||||
export function ContextMenu({ children, menus }: {
|
export function ContextMenu({ children, menus }: {
|
||||||
menus: IPublicTypeContextMenuAction[];
|
menus: IPublicTypeContextMenuAction[];
|
||||||
children: React.ReactElement[];
|
children: React.ReactElement[] | React.ReactElement;
|
||||||
}): React.ReactElement<any, string | React.JSXElementConstructor<any>>[] {
|
}): React.ReactElement<any, string | React.JSXElementConstructor<any>> {
|
||||||
if (!engineConfig.get('enableContextMenu')) {
|
if (!engineConfig.get('enableContextMenu')) {
|
||||||
return children;
|
return (
|
||||||
|
<>{ children }</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleContextMenu = (event: React.MouseEvent) => {
|
const handleContextMenu = (event: React.MouseEvent) => {
|
||||||
@ -26,6 +28,10 @@ export function ContextMenu({ children, menus }: {
|
|||||||
destroy,
|
destroy,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (!children?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const menuInstance = Menu.create({
|
const menuInstance = Menu.create({
|
||||||
target: event.target,
|
target: event.target,
|
||||||
offset: [event.clientX - left, event.clientY - top],
|
offset: [event.clientX - left, event.clientY - top],
|
||||||
@ -42,5 +48,7 @@ export function ContextMenu({ children, menus }: {
|
|||||||
{ onContextMenu: handleContextMenu },
|
{ onContextMenu: handleContextMenu },
|
||||||
));
|
));
|
||||||
|
|
||||||
return childrenWithContextMenu;
|
return (
|
||||||
|
<>{childrenWithContextMenu}</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -49,6 +49,6 @@ export interface IPublicApiCommonUI {
|
|||||||
|
|
||||||
get ContextMenu(): (props: {
|
get ContextMenu(): (props: {
|
||||||
menus: IPublicTypeContextMenuAction[];
|
menus: IPublicTypeContextMenuAction[];
|
||||||
children: React.ReactElement[];
|
children: React.ReactElement[] | React.ReactElement;
|
||||||
}) => ReactElement[];
|
}) => ReactElement;
|
||||||
}
|
}
|
||||||
@ -26,7 +26,7 @@ export interface IPublicTypeContextMenuAction {
|
|||||||
* 菜单项类型
|
* 菜单项类型
|
||||||
* Menu item type
|
* Menu item type
|
||||||
* @see IPublicEnumContextMenuType
|
* @see IPublicEnumContextMenuType
|
||||||
* @default IPublicEnumPContextMenuType.MENU_ITEM
|
* @default IPublicEnumContextMenuType.MENU_ITEM
|
||||||
*/
|
*/
|
||||||
type?: IPublicEnumContextMenuType;
|
type?: IPublicEnumContextMenuType;
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ export interface IPublicTypeContextMenuAction {
|
|||||||
* 点击时执行的动作,可选
|
* 点击时执行的动作,可选
|
||||||
* Action to execute on click, optional
|
* Action to execute on click, optional
|
||||||
*/
|
*/
|
||||||
action?: (nodes: IPublicModelNode[]) => void;
|
action?: (nodes: IPublicModelNode[], event?: MouseEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 子菜单项或生成子节点的函数,可选,仅支持两级
|
* 子菜单项或生成子节点的函数,可选,仅支持两级
|
||||||
|
|||||||
@ -89,42 +89,61 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let destroyFn: Function | undefined;
|
||||||
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: {
|
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: {
|
||||||
nodes?: IPublicModelNode[] | null;
|
nodes?: IPublicModelNode[] | null;
|
||||||
destroy?: Function;
|
destroy?: Function;
|
||||||
|
event?: MouseEvent;
|
||||||
}, level = 1): IPublicTypeContextMenuItem[] {
|
}, level = 1): IPublicTypeContextMenuItem[] {
|
||||||
|
destroyFn?.();
|
||||||
|
destroyFn = options.destroy;
|
||||||
|
|
||||||
const { nodes, destroy } = options;
|
const { nodes, destroy } = options;
|
||||||
if (level > MAX_LEVEL) {
|
if (level > MAX_LEVEL) {
|
||||||
logger.warn('context menu level is too deep, please check your context menu config');
|
logger.warn('context menu level is too deep, please check your context menu config');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return menus.filter(menu => !menu.condition || (menu.condition && menu.condition(nodes || []))).map((menu) => {
|
return menus
|
||||||
const {
|
.filter(menu => !menu.condition || (menu.condition && menu.condition(nodes || [])))
|
||||||
name,
|
.map((menu) => {
|
||||||
title,
|
const {
|
||||||
type = IPublicEnumContextMenuType.MENU_ITEM,
|
name,
|
||||||
} = menu;
|
title,
|
||||||
|
type = IPublicEnumContextMenuType.MENU_ITEM,
|
||||||
|
} = menu;
|
||||||
|
|
||||||
const result: IPublicTypeContextMenuItem = {
|
const result: IPublicTypeContextMenuItem = {
|
||||||
name,
|
name,
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
action: () => {
|
action: () => {
|
||||||
destroy?.();
|
destroy?.();
|
||||||
menu.action?.(nodes || []);
|
menu.action?.(nodes || [], options.event);
|
||||||
},
|
},
|
||||||
disabled: menu.disabled && menu.disabled(nodes || []) || false,
|
disabled: menu.disabled && menu.disabled(nodes || []) || false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if ('items' in menu && menu.items) {
|
if ('items' in menu && menu.items) {
|
||||||
result.items = parseContextMenuProperties(
|
result.items = parseContextMenuProperties(
|
||||||
typeof menu.items === 'function' ? menu.items(nodes || []) : menu.items,
|
typeof menu.items === 'function' ? menu.items(nodes || []) : menu.items,
|
||||||
options,
|
options,
|
||||||
level + 1,
|
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…
x
Reference in New Issue
Block a user