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: optimize context menu details #2828

Merged
merged 1 commit into from
Jan 10, 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
37 changes: 37 additions & 0 deletions docs/docs/api/material.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,43 @@ material.modifyBuiltinComponentAction('remove', (action) => {
});
```

### 右键菜单项
#### addContextMenuOption

添加右键菜单项

```typescript
/**
* 添加右键菜单项
* @param action
*/
addContextMenuOption(action: IPublicTypeContextMenuAction): void;
```

#### removeContextMenuOption

删除特定右键菜单项

```typescript
/**
* 删除特定右键菜单项
* @param name
*/
removeContextMenuOption(name: string): void;
```

#### adjustContextMenuLayout

调整右键菜单项布局,每次调用都会覆盖之前注册的调整函数,只有最后注册的函数会被应用。

```typescript
/**
* 调整右键菜单项布局
* @param actions
*/
adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]): void;
```

### 物料元数据
#### getComponentMeta
获取指定名称的物料元数据
Expand Down
16 changes: 4 additions & 12 deletions packages/designer/src/context-menu-actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
import { IDesigner, INode } from './designer';
import { parseContextMenuAsReactNode, parseContextMenuProperties, uniqueId } from '@alilc/lowcode-utils';
import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties, uniqueId } from '@alilc/lowcode-utils';
import { Menu } from '@alifd/next';
import { engineConfig } from '@alilc/lowcode-editor-core';
import './context-menu-actions.scss';
Expand Down Expand Up @@ -178,18 +178,10 @@ export class ContextMenuActions implements IContextMenuActions {
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 = createContextMenu(menuNode, {
event,
offset: [simulatorLeft, simulatorTop],
});

destroyFn = (menuInstance as any).destroy;
};

initEvent() {
Expand Down
90 changes: 75 additions & 15 deletions packages/engine/src/inner-plugins/default-context-menu.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import {
IPublicEnumContextMenuType,
IPublicEnumDragObjectType,
IPublicEnumTransformStage,
IPublicModelNode,
IPublicModelPluginContext,
IPublicTypeDragNodeDataObject,
IPublicTypeI18nData,
IPublicTypeNodeSchema,
} from '@alilc/lowcode-types';
import { isProjectSchema } from '@alilc/lowcode-utils';
import { isI18nData, isProjectSchema } from '@alilc/lowcode-utils';
import { Notification } from '@alifd/next';
import { intl } from '../locale';
import { intl, getLocale } from '../locale';

function getNodesSchema(nodes: IPublicModelNode[]) {
const componentsTree = nodes.map((node) => node?.exportSchema(IPublicEnumTransformStage.Clone));
const data = { type: 'nodeSchema', componentsMap: {}, componentsTree };
return data;
}

function getIntlStr(data: string | IPublicTypeI18nData) {
if (!isI18nData(data)) {
return data;
}

const locale = getLocale();
return data[locale] || data['zh-CN'] || data['zh_CN'] || data['en-US'] || data['en_US'] || '';
}

async function getClipboardText(): Promise<IPublicTypeNodeSchema[]> {
return new Promise((resolve, reject) => {
// 使用 Clipboard API 读取剪贴板内容
Expand Down Expand Up @@ -71,12 +83,18 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
material.addContextMenuOption({
name: 'copyAndPaste',
title: intl('CopyAndPaste'),
disabled: (nodes) => {
return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0;
},
condition: (nodes) => {
return nodes.length === 1;
},
action(nodes) {
const node = nodes[0];
const { document: doc, parent, index } = node;
const data = getNodesSchema(nodes);
clipboard.setData(data);

if (parent) {
const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true);
newNode?.select();
Expand All @@ -87,6 +105,9 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
material.addContextMenuOption({
name: 'copy',
title: intl('Copy'),
disabled: (nodes) => {
return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0;
},
condition(nodes) {
return nodes.length > 0;
},
Expand All @@ -101,7 +122,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
});

material.addContextMenuOption({
name: 'zhantieToBottom',
name: 'pasteToBottom',
title: intl('PasteToTheBottom'),
condition: (nodes) => {
return nodes.length === 1;
Expand All @@ -116,10 +137,30 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {

try {
const nodeSchema = await getClipboardText();
if (nodeSchema.length === 0) {
return;
}
if (parent) {
nodeSchema.forEach((schema, schemaIndex) => {
doc?.insertNode(parent, schema, (index ?? 0) + 1 + schemaIndex, true);
let canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => {
const dragNodeObject: IPublicTypeDragNodeDataObject = {
type: IPublicEnumDragObjectType.NodeData,
data: nodeSchema,
};
return doc?.checkNesting(parent, dragNodeObject);
});
if (canAddNodes.length === 0) {
Notification.open({
content: `${nodeSchema.map(d => getIntlStr(d.title || d.componentName)).join(',')}等组件无法放置到${getIntlStr(parent.title || parent.componentName as any)}内`,
type: 'error',
});
return;
}
const nodes: IPublicModelNode[] = [];
canAddNodes.forEach((schema, schemaIndex) => {
const node = doc?.insertNode(parent, schema, (index ?? 0) + 1 + schemaIndex, true);
node && nodes.push(node);
});
doc?.selection.selectAll(nodes.map((node) => node?.id));
}
} catch (error) {
console.error(error);
Expand All @@ -128,7 +169,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
});

material.addContextMenuOption({
name: 'zhantieToInner',
name: 'pasteToInner',
title: intl('PasteToTheInside'),
condition: (nodes) => {
return nodes.length === 1;
Expand All @@ -140,19 +181,35 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
},
async action(nodes) {
const node = nodes[0];
const { document: doc, parent } = node;
const { document: doc } = 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);
});
}
const index = node.children?.size || 0;
if (nodeSchema.length === 0) {
return;
}
let canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => {
const dragNodeObject: IPublicTypeDragNodeDataObject = {
type: IPublicEnumDragObjectType.NodeData,
data: nodeSchema,
};
return doc?.checkNesting(node, dragNodeObject);
});
if (canAddNodes.length === 0) {
Notification.open({
content: `${nodeSchema.map(d => getIntlStr(d.title || d.componentName)).join(',')}等组件无法放置到${getIntlStr(node.title || node.componentName as any)}内`,
type: 'error',
});
return;
}

const nodes: IPublicModelNode[] = [];
nodeSchema.forEach((schema, schemaIndex) => {
const newNode = doc?.insertNode(node, schema, (index ?? 0) + 1 + schemaIndex, true);
newNode && nodes.push(newNode);
});
doc?.selection.selectAll(nodes.map((node) => node?.id));
} catch (error) {
console.error(error);
}
Expand All @@ -162,6 +219,9 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
material.addContextMenuOption({
name: 'delete',
title: intl('Delete'),
disabled(nodes) {
return nodes?.filter((node) => !node?.canPerformAction('remove')).length > 0;
},
condition(nodes) {
return nodes.length > 0;
},
Expand Down
4 changes: 2 additions & 2 deletions packages/engine/src/locale/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createIntl } from '@alilc/lowcode-editor-core';
import enUS from './en-US.json';
import zhCN from './zh-CN.json';

const { intl } = createIntl?.({
const { intl, getLocale } = createIntl?.({
'en-US': enUS,
'zh-CN': zhCN,
}) || {
Expand All @@ -11,4 +11,4 @@ const { intl } = createIntl?.({
},
};

export { intl, enUS, zhCN };
export { intl, enUS, zhCN, getLocale };
13 changes: 3 additions & 10 deletions packages/shell/src/components/context-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Menu } from '@alifd/next';
import { parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
import { engineConfig } from '@alilc/lowcode-editor-core';
import { IPublicTypeContextMenuAction } from '@alilc/lowcode-types';
import React from 'react';
Expand All @@ -18,8 +17,6 @@ export function ContextMenu({ children, menus }: {
event.preventDefault();
event.stopPropagation();

const target = event.target;
const { top, left } = target?.getBoundingClientRect();
let destroyFn: Function | undefined;
const destroy = () => {
destroyFn?.();
Expand All @@ -32,13 +29,9 @@ export function ContextMenu({ children, menus }: {
return;
}

const menuInstance = Menu.create({
target: event.target,
offset: [event.clientX - left, event.clientY - top],
children,
destroyFn = createContextMenu(children, {
event,
});

destroyFn = (menuInstance as any).destroy;
};

// 克隆 children 并添加 onContextMenu 事件处理器
Expand Down
55 changes: 53 additions & 2 deletions packages/utils/src/context-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const Tree = (props: {
);
};

let destroyFn: Function | undefined;

export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: {
nodes?: IPublicModelNode[] | null;
destroy?: Function;
Expand Down Expand Up @@ -89,14 +91,12 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
return children;
}

let destroyFn: Function | undefined;
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: {
nodes?: IPublicModelNode[] | null;
destroy?: Function;
event?: MouseEvent;
}, level = 1): IPublicTypeContextMenuItem[] {
destroyFn?.();
destroyFn = options.destroy;

const { nodes, destroy } = options;
if (level > MAX_LEVEL) {
Expand Down Expand Up @@ -146,4 +146,55 @@ export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction
return menus;
}
}, []);
}

let cachedMenuItemHeight: string | undefined;

function getMenuItemHeight() {
if (cachedMenuItemHeight) {
return cachedMenuItemHeight;
}
const root = document.documentElement;
const styles = getComputedStyle(root);
// Access the value of the CSS variable
const menuItemHeight = styles.getPropertyValue('--context-menu-item-height').trim();
cachedMenuItemHeight = menuItemHeight;

return menuItemHeight;
}

export function createContextMenu(children: React.ReactNode[], {
event,
offset = [0, 0],
}: {
event: MouseEvent | React.MouseEvent;
offset?: [number, number];
}) {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const dividerCount = React.Children.count(children.filter(child => React.isValidElement(child) && child.type === Divider));
const popupItemCount = React.Children.count(children.filter(child => React.isValidElement(child) && (child.type === PopupItem || child.type === Item)));
const menuHeight = popupItemCount * parseInt(getMenuItemHeight(), 10) + dividerCount * 8 + 16;
const menuWidthLimit = 200;
const target = event.target;
const { top, left } = (target as any)?.getBoundingClientRect();
let x = event.clientX - left + offset[0];
let y = event.clientY - top + offset[1];
if (x + menuWidthLimit + left > viewportWidth) {
x = x - menuWidthLimit;
}
if (y + menuHeight + top > viewportHeight) {
y = y - menuHeight;
}

const menuInstance = Menu.create({
target,
offset: [x, y, 0, 0],
children,
className: 'engine-context-menu',
});

destroyFn = (menuInstance as any).destroy;

return destroyFn;
}
Loading