feat(engine): add context menu

This commit is contained in:
liujuping 2024-01-08 14:46:27 +08:00 committed by 林熠
parent d47c2d2f91
commit 1b00c61a32
27 changed files with 743 additions and 5 deletions

2
.gitignore vendored
View File

@ -108,3 +108,5 @@ typings/
# codealike
codealike.json
.node
.must.config.js

View File

@ -29,6 +29,26 @@ CommonUI API 是一个专为低代码引擎设计的组件 UI 库,使用它开
| className | className | string (optional) | |
| onClick | 点击事件 | () => void (optional) | |
### ContextMenu
| 参数 | 说明 | 类型 | 默认值 |
|--------|----------------------------------------------------|------------------------------------|--------|
| menus | 定义上下文菜单的动作数组 | IPublicTypeContextMenuAction[] | |
| children | 组件的子元素 | React.ReactElement[] | |
**IPublicTypeContextMenuAction Interface**
| 参数 | 说明 | 类型 | 默认值 |
|------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------|
| 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 |
| 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) | |
| disabled | 禁用条件函数,可选<br>Function to determine disabled condition, optional | (nodes: IPublicModelNode[]) => boolean (optional) | |
### Balloon
详细文档: [Balloon Documentation](https://fusion.design/pc/component/balloon)

View File

@ -185,6 +185,12 @@ config.set('enableCondition', false)
`@type {boolean}` `@default {false}`
#### enableContextMenu - 开启右键菜单
`@type {boolean}` `@default {false}`
是否开启右键菜单
#### disableDetecting
`@type {boolean}` `@default {false}`

View File

@ -128,6 +128,7 @@ sidebar_position: 9
- `--pane-title-height`: 面板标题高度
- `--pane-title-font-size`: 面板标题字体大小
- `--pane-title-padding`: 面板标题边距
- `--context-menu-item-height`: 右键菜单项高度

View File

@ -0,0 +1,10 @@
.engine-context-menu {
&.next-menu.next-ver .next-menu-item {
padding-right: 30px;
.next-menu-item-inner {
height: var(--context-menu-item-height, 30px);
line-height: var(--context-menu-item-height, 30px);
}
}
}

View File

@ -0,0 +1,145 @@
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
import { IDesigner, INode } from './designer';
import { parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
import { Menu } from '@alifd/next';
import { engineConfig } from '@alilc/lowcode-editor-core';
import './context-menu-actions.scss';
export interface IContextMenuActions {
actions: IPublicTypeContextMenuAction[];
adjustMenuLayoutFn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[];
addMenuAction: IPublicApiMaterial['addContextMenuOption'];
removeMenuAction: IPublicApiMaterial['removeContextMenuOption'];
adjustMenuLayout: IPublicApiMaterial['adjustContextMenuLayout'];
}
export class ContextMenuActions implements IContextMenuActions {
actions: IPublicTypeContextMenuAction[] = [];
designer: IDesigner;
dispose: Function[];
enableContextMenu: boolean;
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();
}
});
}
handleContextMenu = (
nodes: INode[],
event: MouseEvent,
) => {
const designer = this.designer;
event.stopPropagation();
event.preventDefault();
const actions = designer.contextMenuActions.actions;
const { bounds } = designer.project.simulator?.viewport || { bounds: { left: 0, top: 0 } };
const { left: simulatorLeft, top: simulatorTop } = bounds;
let destroyFn: Function | undefined;
const destroy = () => {
destroyFn?.();
};
const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, {
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
destroy,
});
if (!menus.length) {
return;
}
const layoutMenu = designer.contextMenuActions.adjustMenuLayoutFn(menus);
const menuNode = parseContextMenuAsReactNode(layoutMenu, {
destroy,
nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!),
designer,
});
const target = event.target;
const { top, left } = target?.getBoundingClientRect();
const menuInstance = Menu.create({
target: event.target,
offset: [event.clientX - left + simulatorLeft, event.clientY - top + simulatorTop],
children: menuNode,
className: 'engine-context-menu',
});
destroyFn = (menuInstance as any).destroy;
};
initEvent() {
const designer = this.designer;
this.dispose.push(
designer.editor.eventBus.on('designer.builtinSimulator.contextmenu', ({
node,
originalEvent,
}: {
node: INode;
originalEvent: MouseEvent;
}) => {
// 如果右键的节点不在 当前选中的节点中,选中该节点
if (!designer.currentSelection.has(node.id)) {
designer.currentSelection.select(node.id);
}
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,
...action,
});
}
removeMenuAction(name: string) {
const i = this.actions.findIndex((action) => action.name === name);
if (i > -1) {
this.actions.splice(i, 1);
}
}
adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) {
this.adjustMenuLayoutFn = fn;
}
}

View File

@ -20,7 +20,7 @@ import {
} from '@alilc/lowcode-types';
import { mergeAssets, IPublicTypeAssetsJson, isNodeSchema, isDragNodeObject, isDragNodeDataObject, isLocationChildrenDetail, Logger } from '@alilc/lowcode-utils';
import { IProject, Project } from '../project';
import { Node, DocumentModel, insertChildren, INode } from '../document';
import { Node, DocumentModel, insertChildren, INode, ISelection } from '../document';
import { ComponentMeta, IComponentMeta } from '../component-meta';
import { INodeSelector, Component } from '../simulator';
import { Scroller } from './scroller';
@ -32,6 +32,7 @@ import { OffsetObserver, createOffsetObserver } from './offset-observer';
import { ISettingTopEntry, SettingTopEntry } from './setting';
import { BemToolsManager } from '../builtin-simulator/bem-tools/manager';
import { ComponentActions } from '../component-actions';
import { ContextMenuActions, IContextMenuActions } from '../context-menu-actions';
const logger = new Logger({ level: 'warn', bizName: 'designer' });
@ -72,12 +73,16 @@ export interface IDesigner {
get componentActions(): ComponentActions;
get contextMenuActions(): ContextMenuActions;
get editor(): IPublicModelEditor;
get detecting(): Detecting;
get simulatorComponent(): ComponentType<any> | undefined;
get currentSelection(): ISelection;
createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller;
refreshComponentMetasMap(): void;
@ -122,6 +127,8 @@ export class Designer implements IDesigner {
readonly componentActions = new ComponentActions();
readonly contextMenuActions: IContextMenuActions;
readonly activeTracker = new ActiveTracker();
readonly detecting = new Detecting();
@ -198,6 +205,8 @@ export class Designer implements IDesigner {
this.postEvent('dragstart', e);
});
this.contextMenuActions = new ContextMenuActions(this);
this.dragon.onDrag((e) => {
if (this.props?.onDrag) {
this.props.onDrag(e);

View File

@ -6,3 +6,4 @@ export * from './project';
export * from './builtin-simulator';
export * from './plugin';
export * from './types';
export * from './context-menu-actions';

View File

@ -159,6 +159,11 @@ const VALID_ENGINE_OPTIONS = {
type: 'function',
description: '应用级设计模式下,窗口为空时展示的占位组件',
},
enableContextMenu: {
type: 'boolean',
description: '是否开启右键菜单',
default: false,
},
hideComponentAction: {
type: 'boolean',
description: '是否隐藏设计器辅助层',

View File

@ -62,6 +62,7 @@ import { setterRegistry } from './inner-plugins/setter-registry';
import { defaultPanelRegistry } from './inner-plugins/default-panel-registry';
import { shellModelFactory } from './modules/shell-model-factory';
import { builtinHotkey } from './inner-plugins/builtin-hotkey';
import { defaultContextMenu } from './inner-plugins/default-context-menu';
import { OutlinePlugin } from '@alilc/lowcode-plugin-outline-pane';
export * from './modules/skeleton-types';
@ -78,6 +79,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins
await plugins.register(defaultPanelRegistryPlugin);
await plugins.register(builtinHotkey);
await plugins.register(registerDefaults, {}, { autoInit: true });
await plugins.register(defaultContextMenu);
return () => {
plugins.delete(OutlinePlugin.pluginName);
@ -86,6 +88,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins
plugins.delete(defaultPanelRegistryPlugin.pluginName);
plugins.delete(builtinHotkey.pluginName);
plugins.delete(registerDefaults.pluginName);
plugins.delete(defaultContextMenu.pluginName);
};
}

View File

@ -0,0 +1,172 @@
import {
IPublicEnumContextMenuType,
IPublicEnumTransformStage,
IPublicModelNode,
IPublicModelPluginContext,
IPublicTypeNodeSchema,
} from '@alilc/lowcode-types';
import { isProjectSchema } from '@alilc/lowcode-utils';
import { Notification } from '@alifd/next';
import { intl } from '../locale';
function getNodesSchema(nodes: IPublicModelNode[]) {
const componentsTree = nodes.map((node) => node?.exportSchema(IPublicEnumTransformStage.Clone));
const data = { type: 'nodeSchema', componentsMap: {}, componentsTree };
return data;
}
async function getClipboardText(): Promise<IPublicTypeNodeSchema[]> {
return new Promise((resolve, reject) => {
// 使用 Clipboard API 读取剪贴板内容
navigator.clipboard.readText().then(
(text) => {
try {
const data = JSON.parse(text);
if (isProjectSchema(data)) {
resolve(data.componentsTree);
} else {
Notification.open({
content: intl('NotValidNodeData'),
type: 'error',
});
reject(
new Error(intl('NotValidNodeData')),
);
}
} catch (error) {
Notification.open({
content: intl('NotValidNodeData'),
type: 'error',
});
reject(error);
}
},
(err) => {
reject(err);
},
);
});
}
export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
const { material, canvas } = ctx;
const { clipboard } = canvas;
return {
init() {
material.addContextMenuOption({
name: 'selectComponent',
title: intl('SelectComponents'),
condition: (nodes) => {
return nodes.length === 1;
},
items: [
{
name: 'nodeTree',
type: IPublicEnumContextMenuType.NODE_TREE,
},
],
});
material.addContextMenuOption({
name: 'copyAndPaste',
title: intl('Copy'),
condition: (nodes) => {
return nodes.length === 1;
},
action(nodes) {
const node = nodes[0];
const { document: doc, parent, index } = node;
if (parent) {
const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true);
newNode?.select();
}
},
});
material.addContextMenuOption({
name: 'copy',
title: intl('Copy.1'),
action(nodes) {
if (!nodes || nodes.length < 1) {
return;
}
const data = getNodesSchema(nodes);
clipboard.setData(data);
},
});
material.addContextMenuOption({
name: 'zhantieToBottom',
title: intl('PasteToTheBottom'),
condition: (nodes) => {
return nodes.length === 1;
},
async action(nodes) {
if (!nodes || nodes.length < 1) {
return;
}
const node = nodes[0];
const { document: doc, parent, index } = node;
try {
const nodeSchema = await getClipboardText();
if (parent) {
nodeSchema.forEach((schema, schemaIndex) => {
doc?.insertNode(parent, schema, (index ?? 0) + 1 + schemaIndex, true);
});
}
} catch (error) {
console.error(error);
}
},
});
material.addContextMenuOption({
name: 'zhantieToInner',
title: intl('PasteToTheInside'),
condition: (nodes) => {
return nodes.length === 1;
},
disabled: (nodes) => {
// 获取粘贴数据
const node = nodes[0];
return !node.isContainerNode;
},
async action(nodes) {
const node = nodes[0];
const { document: doc, parent } = node;
try {
const nodeSchema = await getClipboardText();
if (parent) {
const index = node.children?.size || 0;
if (parent) {
nodeSchema.forEach((schema, schemaIndex) => {
doc?.insertNode(node, schema, (index ?? 0) + 1 + schemaIndex, true);
});
}
}
} catch (error) {
console.error(error);
}
},
});
material.addContextMenuOption({
name: 'delete',
title: intl('Delete'),
action(nodes) {
nodes.forEach((node) => {
node.remove();
});
},
});
},
};
};
defaultContextMenu.pluginName = '___default_context_menu___';

View File

@ -0,0 +1,9 @@
{
"NotValidNodeData": "Not valid node data",
"SelectComponents": "Select components",
"Copy": "Copy",
"Copy.1": "Copy",
"PasteToTheBottom": "Paste to the bottom",
"PasteToTheInside": "Paste to the inside",
"Delete": "Delete"
}

View File

@ -0,0 +1,14 @@
import { createIntl } from '@alilc/lowcode-editor-core';
import enUS from './en-US.json';
import zhCN from './zh-CN.json';
const { intl } = createIntl?.({
'en-US': enUS,
'zh-CN': zhCN,
}) || {
intl: (id) => {
return zhCN[id];
},
};
export { intl, enUS, zhCN };

View File

@ -0,0 +1,9 @@
{
"NotValidNodeData": "不是有效的节点数据",
"SelectComponents": "选择组件",
"Copy": "复制",
"Copy.1": "拷贝",
"PasteToTheBottom": "粘贴至下方",
"PasteToTheInside": "粘贴至内部",
"Delete": "删除"
}

View File

@ -4,6 +4,7 @@ import {
Title as InnerTitle,
} from '@alilc/lowcode-editor-core';
import { Balloon, Breadcrumb, Button, Card, Checkbox, DatePicker, Dialog, Dropdown, Form, Icon, Input, Loading, Message, Overlay, Pagination, Radio, Search, Select, SplitButton, Step, Switch, Tab, Table, Tree, TreeSelect, Upload, Divider } from '@alifd/next';
import { ContextMenu } from '../components/context-menu';
export class CommonUI implements IPublicApiCommonUI {
Balloon = Balloon;
@ -40,4 +41,7 @@ export class CommonUI implements IPublicApiCommonUI {
get Title() {
return InnerTitle;
}
get ContextMenu() {
return ContextMenu;
}
}

View File

@ -13,6 +13,8 @@ import {
IPublicTypeNpmInfo,
IPublicModelEditor,
IPublicTypeDisposable,
IPublicTypeContextMenuAction,
IPublicTypeContextMenuItem,
} from '@alilc/lowcode-types';
import { Workspace as InnerWorkspace } from '@alilc/lowcode-workspace';
import { editorSymbol, designerSymbol } from '../symbols';
@ -190,4 +192,16 @@ export class Material implements IPublicApiMaterial {
dispose.forEach(d => d && d());
};
}
addContextMenuOption(option: IPublicTypeContextMenuAction) {
this[designerSymbol].contextMenuActions.addMenuAction(option);
}
removeContextMenuOption(name: string) {
this[designerSymbol].contextMenuActions.removeMenuAction(name);
}
adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) {
this[designerSymbol].contextMenuActions.adjustMenuLayout(fn);
}
}

View File

@ -0,0 +1,46 @@
import { Menu } from '@alifd/next';
import { parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
import { engineConfig } from '@alilc/lowcode-editor-core';
import { IPublicTypeContextMenuAction } from '@alilc/lowcode-types';
import React from 'react';
export function ContextMenu({ children, menus }: {
menus: IPublicTypeContextMenuAction[];
children: React.ReactElement[];
}): React.ReactElement<any, string | React.JSXElementConstructor<any>>[] {
if (!engineConfig.get('enableContextMenu')) {
return children;
}
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
const target = event.target;
const { top, left } = target?.getBoundingClientRect();
let destroyFn: Function | undefined;
const destroy = () => {
destroyFn?.();
};
const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, {
destroy,
}));
const menuInstance = Menu.create({
target: event.target,
offset: [event.clientX - left, event.clientY - top],
children,
});
destroyFn = (menuInstance as any).destroy;
};
// 克隆 children 并添加 onContextMenu 事件处理器
const childrenWithContextMenu = React.Children.map(children, (child) =>
React.cloneElement(
child,
{ onContextMenu: handleContextMenu },
));
return childrenWithContextMenu;
}

View File

@ -1,4 +1,5 @@
import { IPublicTypeTitleContent } from '../type';
import { ReactElement } from 'react';
import { IPublicTypeContextMenuAction, IPublicTypeTitleContent } from '../type';
import { Balloon, Breadcrumb, Button, Card, Checkbox, DatePicker, Dialog, Dropdown, Form, Icon, Input, Loading, Message, Overlay, Pagination, Radio, Search, Select, SplitButton, Step, Switch, Tab, Table, Tree, TreeSelect, Upload, Divider } from '@alifd/next';
export interface IPublicApiCommonUI {
@ -45,4 +46,9 @@ export interface IPublicApiCommonUI {
match?: boolean;
keywords?: string | null;
}>;
get ContextMenu(): (props: {
menus: IPublicTypeContextMenuAction[];
children: React.ReactElement[];
}) => ReactElement[];
}

View File

@ -1,4 +1,4 @@
import { IPublicTypeAssetsJson, IPublicTypeMetadataTransducer, IPublicTypeComponentAction, IPublicTypeNpmInfo, IPublicTypeDisposable } from '../type';
import { IPublicTypeAssetsJson, IPublicTypeMetadataTransducer, IPublicTypeComponentAction, IPublicTypeNpmInfo, IPublicTypeDisposable, IPublicTypeContextMenuAction, IPublicTypeContextMenuItem } from '../type';
import { IPublicModelComponentMeta } from '../model';
import { ComponentType } from 'react';
@ -128,4 +128,22 @@ export interface IPublicApiMaterial {
* @since v1.1.7
*/
refreshComponentMetasMap(): void;
/**
*
* @param action
*/
addContextMenuOption(action: IPublicTypeContextMenuAction): void;
/**
*
* @param name
*/
removeContextMenuOption(name: string): void;
/**
*
* @param actions
*/
adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]): void;
}

View File

@ -0,0 +1,7 @@
export enum IPublicEnumContextMenuType {
SEPARATOR = 'separator',
// 'menuItem'
MENU_ITEM = 'menuItem',
// 'nodeTree'
NODE_TREE = 'nodeTree',
}

View File

@ -3,4 +3,5 @@ export * from './transition-type';
export * from './transform-stage';
export * from './drag-object-type';
export * from './prop-value-changed-type';
export * from './plugin-register-level';
export * from './plugin-register-level';
export * from './context-menu';

View File

@ -0,0 +1,57 @@
import { IPublicEnumContextMenuType } from '../enum';
import { IPublicModelNode } from '../model';
import { IPublicTypeI18nData } from './i8n-data';
export interface IPublicTypeContextMenuItem extends Omit<IPublicTypeContextMenuAction, 'condition' | 'disabled' | 'items'> {
disabled?: boolean;
items?: Omit<IPublicTypeContextMenuItem, 'items'>[];
}
export interface IPublicTypeContextMenuAction {
/**
*
* Unique identifier for the action
*/
name: string;
/**
*
* Display title, can be a string or internationalized data
*/
title?: string | IPublicTypeI18nData;
/**
*
* Menu item type
* @see IPublicEnumContextMenuType
* @default IPublicEnumPContextMenuType.MENU_ITEM
*/
type?: IPublicEnumContextMenuType;
/**
*
* Action to execute on click, optional
*/
action?: (nodes: IPublicModelNode[]) => void;
/**
*
* Sub-menu items or function to generate child node, optional
*/
items?: Omit<IPublicTypeContextMenuAction, 'items'>[] | ((nodes: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]);
/**
*
* Function to determine display condition
*/
condition?: (nodes: IPublicModelNode[]) => boolean;
/**
*
* Function to determine disabled condition, optional
*/
disabled?: (nodes: IPublicModelNode[]) => boolean;
}

View File

@ -178,6 +178,12 @@ export interface IPublicTypeEngineOptions {
*/
enableAutoOpenFirstWindow?: boolean;
/**
* @default false
*
*/
enableContextMenu?: boolean;
/**
* @default false
*

View File

@ -91,4 +91,5 @@ export * from './hotkey-callback-config';
export * from './hotkey-callbacks';
export * from './scrollable';
export * from './simulator-renderer';
export * from './config-transducer';
export * from './config-transducer';
export * from './context-menu';

View File

@ -0,0 +1,33 @@
.context-menu-tree-wrap {
position: relative;
padding: 4px 10px 4px 24px;
}
.context-menu-tree-children {
margin-left: 8px;
line-height: 24px;
}
.context-menu-tree-bg {
position: absolute;
left: 0;
right: 0;
cursor: pointer;
}
.context-menu-tree-bg-inner {
position: absolute;
height: 24px;
top: -24px;
width: 100%;
&:hover {
background-color: var(--color-block-background-light);
}
}
.context-menu-tree-selected-icon {
position: absolute;
left: 10px;
color: var(--color-icon-active);
}

View File

@ -0,0 +1,138 @@
import { Menu, Icon } from '@alifd/next';
import { IDesigner } from '@alilc/lowcode-designer';
import { IPublicEnumContextMenuType, IPublicModelNode, IPublicTypeContextMenuAction, IPublicTypeContextMenuItem } from '@alilc/lowcode-types';
import { Logger } from '@alilc/lowcode-utils';
import React from 'react';
import './context-menu.scss';
const logger = new Logger({ level: 'warn', bizName: 'designer' });
const { Item, Divider, PopupItem } = Menu;
const MAX_LEVEL = 2;
const Tree = (props: {
node?: IPublicModelNode;
children?: React.ReactNode;
options: {
nodes?: IPublicModelNode[] | null;
destroy?: Function;
designer?: IDesigner;
};
}) => {
const { node } = props;
if (!node) {
return null;
}
if (!node.parent) {
return (
<div className="context-menu-tree-wrap">
<div className="context-menu-tree-children">
{props.children}
</div>
</div>
);
}
const commonUI = props.options.designer?.editor?.get('commonUI');
const Title = commonUI?.Title;
return (
<Tree {...props} node={node.parent} >
{props.options.nodes?.[0].id === node.id ? (<Icon className="context-menu-tree-selected-icon" size="small" type="success" />) : null}
<Title title={node.title} />
<div
className="context-menu-tree-children"
>
<div
className="context-menu-tree-bg"
onClick={() => {
props.options.destroy?.();
node.select();
}}
>
<div className="context-menu-tree-bg-inner" />
</div>
{ props.children }
</div>
</Tree>
);
};
export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: {
nodes?: IPublicModelNode[] | null;
destroy?: Function;
designer?: IDesigner;
} = {}): React.ReactNode[] {
const children: React.ReactNode[] = [];
menus.forEach((menu, index) => {
if (menu.type === IPublicEnumContextMenuType.SEPARATOR) {
children.push(<Divider key={menu.name || index} />);
return;
}
if (menu.type === IPublicEnumContextMenuType.MENU_ITEM) {
if (menu.items && menu.items.length) {
children.push((
<PopupItem key={menu.name} label={menu.title}>
<Menu className="next-context engine-context-menu">
{ parseContextMenuAsReactNode(menu.items, options) }
</Menu>
</PopupItem>
));
} else {
children.push((<Item disabled={menu.disabled} onClick={menu.action} key={menu.name}>{menu.title}</Item>));
}
}
if (menu.type === IPublicEnumContextMenuType.NODE_TREE) {
children.push((
<Tree node={options.nodes?.[0]} options={options} />
));
}
});
return children;
}
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: {
nodes?: IPublicModelNode[] | null;
destroy?: Function;
}, level = 1): IPublicTypeContextMenuItem[] {
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 result;
});
}

View File

@ -31,3 +31,4 @@ export * as css from './css-helper';
export { transactionManager } from './transaction-manager';
export * from './check-types';
export * from './workspace';
export * from './context-menu';