Skip to content

Commit 3627ae3

Browse files
liujupingJackLian
authored andcommitted
feat: optimize context menu details
1 parent 1472847 commit 3627ae3

File tree

6 files changed

+174
-41
lines changed

6 files changed

+174
-41
lines changed

docs/docs/api/material.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,43 @@ material.modifyBuiltinComponentAction('remove', (action) => {
237237
});
238238
```
239239

240+
### 右键菜单项
241+
#### addContextMenuOption
242+
243+
添加右键菜单项
244+
245+
```typescript
246+
/**
247+
* 添加右键菜单项
248+
* @param action
249+
*/
250+
addContextMenuOption(action: IPublicTypeContextMenuAction): void;
251+
```
252+
253+
#### removeContextMenuOption
254+
255+
删除特定右键菜单项
256+
257+
```typescript
258+
/**
259+
* 删除特定右键菜单项
260+
* @param name
261+
*/
262+
removeContextMenuOption(name: string): void;
263+
```
264+
265+
#### adjustContextMenuLayout
266+
267+
调整右键菜单项布局,每次调用都会覆盖之前注册的调整函数,只有最后注册的函数会被应用。
268+
269+
```typescript
270+
/**
271+
* 调整右键菜单项布局
272+
* @param actions
273+
*/
274+
adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]): void;
275+
```
276+
240277
### 物料元数据
241278
#### getComponentMeta
242279
获取指定名称的物料元数据

packages/designer/src/context-menu-actions.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
22
import { IDesigner, INode } from './designer';
3-
import { parseContextMenuAsReactNode, parseContextMenuProperties, uniqueId } from '@alilc/lowcode-utils';
3+
import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties, uniqueId } from '@alilc/lowcode-utils';
44
import { Menu } from '@alifd/next';
55
import { engineConfig } from '@alilc/lowcode-editor-core';
66
import './context-menu-actions.scss';
@@ -178,18 +178,10 @@ export class ContextMenuActions implements IContextMenuActions {
178178
designer,
179179
});
180180

181-
const target = event.target;
182-
183-
const { top, left } = target?.getBoundingClientRect();
184-
185-
const menuInstance = Menu.create({
186-
target: event.target,
187-
offset: [event.clientX - left + simulatorLeft, event.clientY - top + simulatorTop],
188-
children: menuNode,
189-
className: 'engine-context-menu',
181+
destroyFn = createContextMenu(menuNode, {
182+
event,
183+
offset: [simulatorLeft, simulatorTop],
190184
});
191-
192-
destroyFn = (menuInstance as any).destroy;
193185
};
194186

195187
initEvent() {

packages/engine/src/inner-plugins/default-context-menu.ts

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
11
import {
22
IPublicEnumContextMenuType,
3+
IPublicEnumDragObjectType,
34
IPublicEnumTransformStage,
45
IPublicModelNode,
56
IPublicModelPluginContext,
7+
IPublicTypeDragNodeDataObject,
8+
IPublicTypeI18nData,
69
IPublicTypeNodeSchema,
710
} from '@alilc/lowcode-types';
8-
import { isProjectSchema } from '@alilc/lowcode-utils';
11+
import { isI18nData, isProjectSchema } from '@alilc/lowcode-utils';
912
import { Notification } from '@alifd/next';
10-
import { intl } from '../locale';
13+
import { intl, getLocale } from '../locale';
1114

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

21+
function getIntlStr(data: string | IPublicTypeI18nData) {
22+
if (!isI18nData(data)) {
23+
return data;
24+
}
25+
26+
const locale = getLocale();
27+
return data[locale] || data['zh-CN'] || data['zh_CN'] || data['en-US'] || data['en_US'] || '';
28+
}
29+
1830
async function getClipboardText(): Promise<IPublicTypeNodeSchema[]> {
1931
return new Promise((resolve, reject) => {
2032
// 使用 Clipboard API 读取剪贴板内容
@@ -71,12 +83,18 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
7183
material.addContextMenuOption({
7284
name: 'copyAndPaste',
7385
title: intl('CopyAndPaste'),
86+
disabled: (nodes) => {
87+
return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0;
88+
},
7489
condition: (nodes) => {
7590
return nodes.length === 1;
7691
},
7792
action(nodes) {
7893
const node = nodes[0];
7994
const { document: doc, parent, index } = node;
95+
const data = getNodesSchema(nodes);
96+
clipboard.setData(data);
97+
8098
if (parent) {
8199
const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true);
82100
newNode?.select();
@@ -87,6 +105,9 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
87105
material.addContextMenuOption({
88106
name: 'copy',
89107
title: intl('Copy'),
108+
disabled: (nodes) => {
109+
return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0;
110+
},
90111
condition(nodes) {
91112
return nodes.length > 0;
92113
},
@@ -101,7 +122,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
101122
});
102123

103124
material.addContextMenuOption({
104-
name: 'zhantieToBottom',
125+
name: 'pasteToBottom',
105126
title: intl('PasteToTheBottom'),
106127
condition: (nodes) => {
107128
return nodes.length === 1;
@@ -116,10 +137,30 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
116137

117138
try {
118139
const nodeSchema = await getClipboardText();
140+
if (nodeSchema.length === 0) {
141+
return;
142+
}
119143
if (parent) {
120-
nodeSchema.forEach((schema, schemaIndex) => {
121-
doc?.insertNode(parent, schema, (index ?? 0) + 1 + schemaIndex, true);
144+
let canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => {
145+
const dragNodeObject: IPublicTypeDragNodeDataObject = {
146+
type: IPublicEnumDragObjectType.NodeData,
147+
data: nodeSchema,
148+
};
149+
return doc?.checkNesting(parent, dragNodeObject);
150+
});
151+
if (canAddNodes.length === 0) {
152+
Notification.open({
153+
content: `${nodeSchema.map(d => getIntlStr(d.title || d.componentName)).join(',')}等组件无法放置到${getIntlStr(parent.title || parent.componentName as any)}内`,
154+
type: 'error',
155+
});
156+
return;
157+
}
158+
const nodes: IPublicModelNode[] = [];
159+
canAddNodes.forEach((schema, schemaIndex) => {
160+
const node = doc?.insertNode(parent, schema, (index ?? 0) + 1 + schemaIndex, true);
161+
node && nodes.push(node);
122162
});
163+
doc?.selection.selectAll(nodes.map((node) => node?.id));
123164
}
124165
} catch (error) {
125166
console.error(error);
@@ -128,7 +169,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
128169
});
129170

130171
material.addContextMenuOption({
131-
name: 'zhantieToInner',
172+
name: 'pasteToInner',
132173
title: intl('PasteToTheInside'),
133174
condition: (nodes) => {
134175
return nodes.length === 1;
@@ -140,19 +181,35 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
140181
},
141182
async action(nodes) {
142183
const node = nodes[0];
143-
const { document: doc, parent } = node;
184+
const { document: doc } = node;
144185

145186
try {
146187
const nodeSchema = await getClipboardText();
147-
if (parent) {
148-
const index = node.children?.size || 0;
149-
150-
if (parent) {
151-
nodeSchema.forEach((schema, schemaIndex) => {
152-
doc?.insertNode(node, schema, (index ?? 0) + 1 + schemaIndex, true);
153-
});
154-
}
188+
const index = node.children?.size || 0;
189+
if (nodeSchema.length === 0) {
190+
return;
155191
}
192+
let canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => {
193+
const dragNodeObject: IPublicTypeDragNodeDataObject = {
194+
type: IPublicEnumDragObjectType.NodeData,
195+
data: nodeSchema,
196+
};
197+
return doc?.checkNesting(node, dragNodeObject);
198+
});
199+
if (canAddNodes.length === 0) {
200+
Notification.open({
201+
content: `${nodeSchema.map(d => getIntlStr(d.title || d.componentName)).join(',')}等组件无法放置到${getIntlStr(node.title || node.componentName as any)}内`,
202+
type: 'error',
203+
});
204+
return;
205+
}
206+
207+
const nodes: IPublicModelNode[] = [];
208+
nodeSchema.forEach((schema, schemaIndex) => {
209+
const newNode = doc?.insertNode(node, schema, (index ?? 0) + 1 + schemaIndex, true);
210+
newNode && nodes.push(newNode);
211+
});
212+
doc?.selection.selectAll(nodes.map((node) => node?.id));
156213
} catch (error) {
157214
console.error(error);
158215
}
@@ -162,6 +219,9 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
162219
material.addContextMenuOption({
163220
name: 'delete',
164221
title: intl('Delete'),
222+
disabled(nodes) {
223+
return nodes?.filter((node) => !node?.canPerformAction('remove')).length > 0;
224+
},
165225
condition(nodes) {
166226
return nodes.length > 0;
167227
},

packages/engine/src/locale/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createIntl } from '@alilc/lowcode-editor-core';
22
import enUS from './en-US.json';
33
import zhCN from './zh-CN.json';
44

5-
const { intl } = createIntl?.({
5+
const { intl, getLocale } = createIntl?.({
66
'en-US': enUS,
77
'zh-CN': zhCN,
88
}) || {
@@ -11,4 +11,4 @@ const { intl } = createIntl?.({
1111
},
1212
};
1313

14-
export { intl, enUS, zhCN };
14+
export { intl, enUS, zhCN, getLocale };

packages/shell/src/components/context-menu.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { Menu } from '@alifd/next';
2-
import { parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
1+
import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
32
import { engineConfig } from '@alilc/lowcode-editor-core';
43
import { IPublicTypeContextMenuAction } from '@alilc/lowcode-types';
54
import React from 'react';
@@ -18,8 +17,6 @@ export function ContextMenu({ children, menus }: {
1817
event.preventDefault();
1918
event.stopPropagation();
2019

21-
const target = event.target;
22-
const { top, left } = target?.getBoundingClientRect();
2320
let destroyFn: Function | undefined;
2421
const destroy = () => {
2522
destroyFn?.();
@@ -32,13 +29,9 @@ export function ContextMenu({ children, menus }: {
3229
return;
3330
}
3431

35-
const menuInstance = Menu.create({
36-
target: event.target,
37-
offset: [event.clientX - left, event.clientY - top],
38-
children,
32+
destroyFn = createContextMenu(children, {
33+
event,
3934
});
40-
41-
destroyFn = (menuInstance as any).destroy;
4235
};
4336

4437
// 克隆 children 并添加 onContextMenu 事件处理器

packages/utils/src/context-menu.tsx

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ const Tree = (props: {
5353
);
5454
};
5555

56+
let destroyFn: Function | undefined;
57+
5658
export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: {
5759
nodes?: IPublicModelNode[] | null;
5860
destroy?: Function;
@@ -89,14 +91,12 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
8991
return children;
9092
}
9193

92-
let destroyFn: Function | undefined;
9394
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: {
9495
nodes?: IPublicModelNode[] | null;
9596
destroy?: Function;
9697
event?: MouseEvent;
9798
}, level = 1): IPublicTypeContextMenuItem[] {
9899
destroyFn?.();
99-
destroyFn = options.destroy;
100100

101101
const { nodes, destroy } = options;
102102
if (level > MAX_LEVEL) {
@@ -146,4 +146,55 @@ export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction
146146
return menus;
147147
}
148148
}, []);
149+
}
150+
151+
let cachedMenuItemHeight: string | undefined;
152+
153+
function getMenuItemHeight() {
154+
if (cachedMenuItemHeight) {
155+
return cachedMenuItemHeight;
156+
}
157+
const root = document.documentElement;
158+
const styles = getComputedStyle(root);
159+
// Access the value of the CSS variable
160+
const menuItemHeight = styles.getPropertyValue('--context-menu-item-height').trim();
161+
cachedMenuItemHeight = menuItemHeight;
162+
163+
return menuItemHeight;
164+
}
165+
166+
export function createContextMenu(children: React.ReactNode[], {
167+
event,
168+
offset = [0, 0],
169+
}: {
170+
event: MouseEvent | React.MouseEvent;
171+
offset?: [number, number];
172+
}) {
173+
const viewportWidth = window.innerWidth;
174+
const viewportHeight = window.innerHeight;
175+
const dividerCount = React.Children.count(children.filter(child => React.isValidElement(child) && child.type === Divider));
176+
const popupItemCount = React.Children.count(children.filter(child => React.isValidElement(child) && (child.type === PopupItem || child.type === Item)));
177+
const menuHeight = popupItemCount * parseInt(getMenuItemHeight(), 10) + dividerCount * 8 + 16;
178+
const menuWidthLimit = 200;
179+
const target = event.target;
180+
const { top, left } = (target as any)?.getBoundingClientRect();
181+
let x = event.clientX - left + offset[0];
182+
let y = event.clientY - top + offset[1];
183+
if (x + menuWidthLimit + left > viewportWidth) {
184+
x = x - menuWidthLimit;
185+
}
186+
if (y + menuHeight + top > viewportHeight) {
187+
y = y - menuHeight;
188+
}
189+
190+
const menuInstance = Menu.create({
191+
target,
192+
offset: [x, y, 0, 0],
193+
children,
194+
className: 'engine-context-menu',
195+
});
196+
197+
destroyFn = (menuInstance as any).destroy;
198+
199+
return destroyFn;
149200
}

0 commit comments

Comments
 (0)