feat: optimize context menu details

This commit is contained in:
liujuping 2024-01-10 15:13:44 +08:00 committed by 林熠
parent 14728476b6
commit 3627ae326a
6 changed files with 174 additions and 41 deletions

View File

@ -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
获取指定名称的物料元数据

View File

@ -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';
@ -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() {

View File

@ -1,13 +1,16 @@
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));
@ -15,6 +18,15 @@ function getNodesSchema(nodes: IPublicModelNode[]) {
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 读取剪贴板内容
@ -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();
@ -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;
},
@ -101,7 +122,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
});
material.addContextMenuOption({
name: 'zhantieToBottom',
name: 'pasteToBottom',
title: intl('PasteToTheBottom'),
condition: (nodes) => {
return nodes.length === 1;
@ -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);
@ -128,7 +169,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
});
material.addContextMenuOption({
name: 'zhantieToInner',
name: 'pasteToInner',
title: intl('PasteToTheInside'),
condition: (nodes) => {
return nodes.length === 1;
@ -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);
}
@ -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;
},

View File

@ -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,
}) || {
@ -11,4 +11,4 @@ const { intl } = createIntl?.({
},
};
export { intl, enUS, zhCN };
export { intl, enUS, zhCN, getLocale };

View File

@ -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';
@ -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?.();
@ -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 事件处理器

View File

@ -53,6 +53,8 @@ const Tree = (props: {
);
};
let destroyFn: Function | undefined;
export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: {
nodes?: IPublicModelNode[] | null;
destroy?: Function;
@ -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) {
@ -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;
}