mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-10 18:03:01 +00:00
feat: optimize context menu details
This commit is contained in:
parent
14728476b6
commit
3627ae326a
@ -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
|
||||
获取指定名称的物料元数据
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 事件处理器
|
||||
|
||||
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user