Skip to content

Commit

Permalink
feat(engine): add context menu
Browse files Browse the repository at this point in the history
  • Loading branch information
liujuping committed Jan 8, 2024
1 parent ba53d6c commit d02dea5
Show file tree
Hide file tree
Showing 27 changed files with 743 additions and 5 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,5 @@ typings/
# codealike
codealike.json
.node

.must.config.js
20 changes: 20 additions & 0 deletions docs/docs/api/commonUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions docs/docs/api/configOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ config.set('enableCondition', false)

`@type {boolean}` `@default {false}`

#### enableContextMenu - 开启右键菜单

`@type {boolean}` `@default {false}`

是否开启右键菜单

#### disableDetecting

`@type {boolean}` `@default {false}`
Expand Down
1 change: 1 addition & 0 deletions docs/docs/guide/expand/editor/theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ sidebar_position: 9
- `--pane-title-height`: 面板标题高度
- `--pane-title-font-size`: 面板标题字体大小
- `--pane-title-padding`: 面板标题边距
- `--context-menu-item-height`: 右键菜单项高度



Expand Down
10 changes: 10 additions & 0 deletions packages/designer/src/context-menu-actions.scss
Original file line number Diff line number Diff line change
@@ -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 changes: 145 additions & 0 deletions packages/designer/src/context-menu-actions.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
11 changes: 10 additions & 1 deletion packages/designer/src/designer/designer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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' });

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -122,6 +127,8 @@ export class Designer implements IDesigner {

readonly componentActions = new ComponentActions();

readonly contextMenuActions: IContextMenuActions;

readonly activeTracker = new ActiveTracker();

readonly detecting = new Detecting();
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/designer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './project';
export * from './builtin-simulator';
export * from './plugin';
export * from './types';
export * from './context-menu-actions';
5 changes: 5 additions & 0 deletions packages/editor-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ const VALID_ENGINE_OPTIONS = {
type: 'function',
description: '应用级设计模式下,窗口为空时展示的占位组件',
},
enableContextMenu: {
type: 'boolean',
description: '是否开启右键菜单',
default: false,
},
hideComponentAction: {
type: 'boolean',
description: '是否隐藏设计器辅助层',
Expand Down
3 changes: 3 additions & 0 deletions packages/engine/src/engine-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -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);
};
}

Expand Down
Loading

0 comments on commit d02dea5

Please sign in to comment.