Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(engine): add context menu #2821

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 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
Loading