mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-10 18:03:01 +00:00
feat(engine): add context menu
This commit is contained in:
parent
d47c2d2f91
commit
1b00c61a32
2
.gitignore
vendored
2
.gitignore
vendored
@ -108,3 +108,5 @@ typings/
|
|||||||
# codealike
|
# codealike
|
||||||
codealike.json
|
codealike.json
|
||||||
.node
|
.node
|
||||||
|
|
||||||
|
.must.config.js
|
||||||
@ -29,6 +29,26 @@ CommonUI API 是一个专为低代码引擎设计的组件 UI 库,使用它开
|
|||||||
| className | className | string (optional) | |
|
| className | className | string (optional) | |
|
||||||
| onClick | 点击事件 | () => void (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
|
||||||
详细文档: [Balloon Documentation](https://fusion.design/pc/component/balloon)
|
详细文档: [Balloon Documentation](https://fusion.design/pc/component/balloon)
|
||||||
|
|
||||||
|
|||||||
@ -185,6 +185,12 @@ config.set('enableCondition', false)
|
|||||||
|
|
||||||
`@type {boolean}` `@default {false}`
|
`@type {boolean}` `@default {false}`
|
||||||
|
|
||||||
|
#### enableContextMenu - 开启右键菜单
|
||||||
|
|
||||||
|
`@type {boolean}` `@default {false}`
|
||||||
|
|
||||||
|
是否开启右键菜单
|
||||||
|
|
||||||
#### disableDetecting
|
#### disableDetecting
|
||||||
|
|
||||||
`@type {boolean}` `@default {false}`
|
`@type {boolean}` `@default {false}`
|
||||||
|
|||||||
@ -128,6 +128,7 @@ sidebar_position: 9
|
|||||||
- `--pane-title-height`: 面板标题高度
|
- `--pane-title-height`: 面板标题高度
|
||||||
- `--pane-title-font-size`: 面板标题字体大小
|
- `--pane-title-font-size`: 面板标题字体大小
|
||||||
- `--pane-title-padding`: 面板标题边距
|
- `--pane-title-padding`: 面板标题边距
|
||||||
|
- `--context-menu-item-height`: 右键菜单项高度
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
packages/designer/src/context-menu-actions.scss
Normal file
10
packages/designer/src/context-menu-actions.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
packages/designer/src/context-menu-actions.ts
Normal file
145
packages/designer/src/context-menu-actions.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ import {
|
|||||||
} from '@alilc/lowcode-types';
|
} from '@alilc/lowcode-types';
|
||||||
import { mergeAssets, IPublicTypeAssetsJson, isNodeSchema, isDragNodeObject, isDragNodeDataObject, isLocationChildrenDetail, Logger } from '@alilc/lowcode-utils';
|
import { mergeAssets, IPublicTypeAssetsJson, isNodeSchema, isDragNodeObject, isDragNodeDataObject, isLocationChildrenDetail, Logger } from '@alilc/lowcode-utils';
|
||||||
import { IProject, Project } from '../project';
|
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 { ComponentMeta, IComponentMeta } from '../component-meta';
|
||||||
import { INodeSelector, Component } from '../simulator';
|
import { INodeSelector, Component } from '../simulator';
|
||||||
import { Scroller } from './scroller';
|
import { Scroller } from './scroller';
|
||||||
@ -32,6 +32,7 @@ import { OffsetObserver, createOffsetObserver } from './offset-observer';
|
|||||||
import { ISettingTopEntry, SettingTopEntry } from './setting';
|
import { ISettingTopEntry, SettingTopEntry } from './setting';
|
||||||
import { BemToolsManager } from '../builtin-simulator/bem-tools/manager';
|
import { BemToolsManager } from '../builtin-simulator/bem-tools/manager';
|
||||||
import { ComponentActions } from '../component-actions';
|
import { ComponentActions } from '../component-actions';
|
||||||
|
import { ContextMenuActions, IContextMenuActions } from '../context-menu-actions';
|
||||||
|
|
||||||
const logger = new Logger({ level: 'warn', bizName: 'designer' });
|
const logger = new Logger({ level: 'warn', bizName: 'designer' });
|
||||||
|
|
||||||
@ -72,12 +73,16 @@ export interface IDesigner {
|
|||||||
|
|
||||||
get componentActions(): ComponentActions;
|
get componentActions(): ComponentActions;
|
||||||
|
|
||||||
|
get contextMenuActions(): ContextMenuActions;
|
||||||
|
|
||||||
get editor(): IPublicModelEditor;
|
get editor(): IPublicModelEditor;
|
||||||
|
|
||||||
get detecting(): Detecting;
|
get detecting(): Detecting;
|
||||||
|
|
||||||
get simulatorComponent(): ComponentType<any> | undefined;
|
get simulatorComponent(): ComponentType<any> | undefined;
|
||||||
|
|
||||||
|
get currentSelection(): ISelection;
|
||||||
|
|
||||||
createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller;
|
createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller;
|
||||||
|
|
||||||
refreshComponentMetasMap(): void;
|
refreshComponentMetasMap(): void;
|
||||||
@ -122,6 +127,8 @@ export class Designer implements IDesigner {
|
|||||||
|
|
||||||
readonly componentActions = new ComponentActions();
|
readonly componentActions = new ComponentActions();
|
||||||
|
|
||||||
|
readonly contextMenuActions: IContextMenuActions;
|
||||||
|
|
||||||
readonly activeTracker = new ActiveTracker();
|
readonly activeTracker = new ActiveTracker();
|
||||||
|
|
||||||
readonly detecting = new Detecting();
|
readonly detecting = new Detecting();
|
||||||
@ -198,6 +205,8 @@ export class Designer implements IDesigner {
|
|||||||
this.postEvent('dragstart', e);
|
this.postEvent('dragstart', e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.contextMenuActions = new ContextMenuActions(this);
|
||||||
|
|
||||||
this.dragon.onDrag((e) => {
|
this.dragon.onDrag((e) => {
|
||||||
if (this.props?.onDrag) {
|
if (this.props?.onDrag) {
|
||||||
this.props.onDrag(e);
|
this.props.onDrag(e);
|
||||||
|
|||||||
@ -6,3 +6,4 @@ export * from './project';
|
|||||||
export * from './builtin-simulator';
|
export * from './builtin-simulator';
|
||||||
export * from './plugin';
|
export * from './plugin';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
export * from './context-menu-actions';
|
||||||
|
|||||||
@ -159,6 +159,11 @@ const VALID_ENGINE_OPTIONS = {
|
|||||||
type: 'function',
|
type: 'function',
|
||||||
description: '应用级设计模式下,窗口为空时展示的占位组件',
|
description: '应用级设计模式下,窗口为空时展示的占位组件',
|
||||||
},
|
},
|
||||||
|
enableContextMenu: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: '是否开启右键菜单',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
hideComponentAction: {
|
hideComponentAction: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: '是否隐藏设计器辅助层',
|
description: '是否隐藏设计器辅助层',
|
||||||
|
|||||||
@ -62,6 +62,7 @@ import { setterRegistry } from './inner-plugins/setter-registry';
|
|||||||
import { defaultPanelRegistry } from './inner-plugins/default-panel-registry';
|
import { defaultPanelRegistry } from './inner-plugins/default-panel-registry';
|
||||||
import { shellModelFactory } from './modules/shell-model-factory';
|
import { shellModelFactory } from './modules/shell-model-factory';
|
||||||
import { builtinHotkey } from './inner-plugins/builtin-hotkey';
|
import { builtinHotkey } from './inner-plugins/builtin-hotkey';
|
||||||
|
import { defaultContextMenu } from './inner-plugins/default-context-menu';
|
||||||
import { OutlinePlugin } from '@alilc/lowcode-plugin-outline-pane';
|
import { OutlinePlugin } from '@alilc/lowcode-plugin-outline-pane';
|
||||||
|
|
||||||
export * from './modules/skeleton-types';
|
export * from './modules/skeleton-types';
|
||||||
@ -78,6 +79,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins
|
|||||||
await plugins.register(defaultPanelRegistryPlugin);
|
await plugins.register(defaultPanelRegistryPlugin);
|
||||||
await plugins.register(builtinHotkey);
|
await plugins.register(builtinHotkey);
|
||||||
await plugins.register(registerDefaults, {}, { autoInit: true });
|
await plugins.register(registerDefaults, {}, { autoInit: true });
|
||||||
|
await plugins.register(defaultContextMenu);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
plugins.delete(OutlinePlugin.pluginName);
|
plugins.delete(OutlinePlugin.pluginName);
|
||||||
@ -86,6 +88,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins
|
|||||||
plugins.delete(defaultPanelRegistryPlugin.pluginName);
|
plugins.delete(defaultPanelRegistryPlugin.pluginName);
|
||||||
plugins.delete(builtinHotkey.pluginName);
|
plugins.delete(builtinHotkey.pluginName);
|
||||||
plugins.delete(registerDefaults.pluginName);
|
plugins.delete(registerDefaults.pluginName);
|
||||||
|
plugins.delete(defaultContextMenu.pluginName);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
172
packages/engine/src/inner-plugins/default-context-menu.ts
Normal file
172
packages/engine/src/inner-plugins/default-context-menu.ts
Normal 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___';
|
||||||
9
packages/engine/src/locale/en-US.json
Normal file
9
packages/engine/src/locale/en-US.json
Normal 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"
|
||||||
|
}
|
||||||
14
packages/engine/src/locale/index.ts
Normal file
14
packages/engine/src/locale/index.ts
Normal 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 };
|
||||||
9
packages/engine/src/locale/zh-CN.json
Normal file
9
packages/engine/src/locale/zh-CN.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"NotValidNodeData": "不是有效的节点数据",
|
||||||
|
"SelectComponents": "选择组件",
|
||||||
|
"Copy": "复制",
|
||||||
|
"Copy.1": "拷贝",
|
||||||
|
"PasteToTheBottom": "粘贴至下方",
|
||||||
|
"PasteToTheInside": "粘贴至内部",
|
||||||
|
"Delete": "删除"
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import {
|
|||||||
Title as InnerTitle,
|
Title as InnerTitle,
|
||||||
} from '@alilc/lowcode-editor-core';
|
} 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 { 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 {
|
export class CommonUI implements IPublicApiCommonUI {
|
||||||
Balloon = Balloon;
|
Balloon = Balloon;
|
||||||
@ -40,4 +41,7 @@ export class CommonUI implements IPublicApiCommonUI {
|
|||||||
get Title() {
|
get Title() {
|
||||||
return InnerTitle;
|
return InnerTitle;
|
||||||
}
|
}
|
||||||
|
get ContextMenu() {
|
||||||
|
return ContextMenu;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import {
|
|||||||
IPublicTypeNpmInfo,
|
IPublicTypeNpmInfo,
|
||||||
IPublicModelEditor,
|
IPublicModelEditor,
|
||||||
IPublicTypeDisposable,
|
IPublicTypeDisposable,
|
||||||
|
IPublicTypeContextMenuAction,
|
||||||
|
IPublicTypeContextMenuItem,
|
||||||
} from '@alilc/lowcode-types';
|
} from '@alilc/lowcode-types';
|
||||||
import { Workspace as InnerWorkspace } from '@alilc/lowcode-workspace';
|
import { Workspace as InnerWorkspace } from '@alilc/lowcode-workspace';
|
||||||
import { editorSymbol, designerSymbol } from '../symbols';
|
import { editorSymbol, designerSymbol } from '../symbols';
|
||||||
@ -190,4 +192,16 @@ export class Material implements IPublicApiMaterial {
|
|||||||
dispose.forEach(d => d && d());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
packages/shell/src/components/context-menu.tsx
Normal file
46
packages/shell/src/components/context-menu.tsx
Normal 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;
|
||||||
|
}
|
||||||
@ -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';
|
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 {
|
export interface IPublicApiCommonUI {
|
||||||
@ -45,4 +46,9 @@ export interface IPublicApiCommonUI {
|
|||||||
match?: boolean;
|
match?: boolean;
|
||||||
keywords?: string | null;
|
keywords?: string | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
get ContextMenu(): (props: {
|
||||||
|
menus: IPublicTypeContextMenuAction[];
|
||||||
|
children: React.ReactElement[];
|
||||||
|
}) => ReactElement[];
|
||||||
}
|
}
|
||||||
@ -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 { IPublicModelComponentMeta } from '../model';
|
||||||
import { ComponentType } from 'react';
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
@ -128,4 +128,22 @@ export interface IPublicApiMaterial {
|
|||||||
* @since v1.1.7
|
* @since v1.1.7
|
||||||
*/
|
*/
|
||||||
refreshComponentMetasMap(): void;
|
refreshComponentMetasMap(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加右键菜单项
|
||||||
|
* @param action
|
||||||
|
*/
|
||||||
|
addContextMenuOption(action: IPublicTypeContextMenuAction): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除特定右键菜单项
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
removeContextMenuOption(name: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整右键菜单项布局
|
||||||
|
* @param actions
|
||||||
|
*/
|
||||||
|
adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]): void;
|
||||||
}
|
}
|
||||||
|
|||||||
7
packages/types/src/shell/enum/context-menu.ts
Normal file
7
packages/types/src/shell/enum/context-menu.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export enum IPublicEnumContextMenuType {
|
||||||
|
SEPARATOR = 'separator',
|
||||||
|
// 'menuItem'
|
||||||
|
MENU_ITEM = 'menuItem',
|
||||||
|
// 'nodeTree'
|
||||||
|
NODE_TREE = 'nodeTree',
|
||||||
|
}
|
||||||
@ -3,4 +3,5 @@ export * from './transition-type';
|
|||||||
export * from './transform-stage';
|
export * from './transform-stage';
|
||||||
export * from './drag-object-type';
|
export * from './drag-object-type';
|
||||||
export * from './prop-value-changed-type';
|
export * from './prop-value-changed-type';
|
||||||
export * from './plugin-register-level';
|
export * from './plugin-register-level';
|
||||||
|
export * from './context-menu';
|
||||||
57
packages/types/src/shell/type/context-menu.ts
Normal file
57
packages/types/src/shell/type/context-menu.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
@ -178,6 +178,12 @@ export interface IPublicTypeEngineOptions {
|
|||||||
*/
|
*/
|
||||||
enableAutoOpenFirstWindow?: boolean;
|
enableAutoOpenFirstWindow?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @default false
|
||||||
|
* 开启右键菜单能力
|
||||||
|
*/
|
||||||
|
enableContextMenu?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default false
|
* @default false
|
||||||
* 隐藏设计器辅助层
|
* 隐藏设计器辅助层
|
||||||
|
|||||||
@ -91,4 +91,5 @@ export * from './hotkey-callback-config';
|
|||||||
export * from './hotkey-callbacks';
|
export * from './hotkey-callbacks';
|
||||||
export * from './scrollable';
|
export * from './scrollable';
|
||||||
export * from './simulator-renderer';
|
export * from './simulator-renderer';
|
||||||
export * from './config-transducer';
|
export * from './config-transducer';
|
||||||
|
export * from './context-menu';
|
||||||
33
packages/utils/src/context-menu.scss
Normal file
33
packages/utils/src/context-menu.scss
Normal 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);
|
||||||
|
}
|
||||||
138
packages/utils/src/context-menu.tsx
Normal file
138
packages/utils/src/context-menu.tsx
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -31,3 +31,4 @@ export * as css from './css-helper';
|
|||||||
export { transactionManager } from './transaction-manager';
|
export { transactionManager } from './transaction-manager';
|
||||||
export * from './check-types';
|
export * from './check-types';
|
||||||
export * from './workspace';
|
export * from './workspace';
|
||||||
|
export * from './context-menu';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user