mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-11 18:42:56 +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
|
#### getComponentMeta
|
||||||
获取指定名称的物料元数据
|
获取指定名称的物料元数据
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
|
import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial } from '@alilc/lowcode-types';
|
||||||
import { IDesigner, INode } from './designer';
|
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 { Menu } from '@alifd/next';
|
||||||
import { engineConfig } from '@alilc/lowcode-editor-core';
|
import { engineConfig } from '@alilc/lowcode-editor-core';
|
||||||
import './context-menu-actions.scss';
|
import './context-menu-actions.scss';
|
||||||
@ -178,18 +178,10 @@ export class ContextMenuActions implements IContextMenuActions {
|
|||||||
designer,
|
designer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const target = event.target;
|
destroyFn = createContextMenu(menuNode, {
|
||||||
|
event,
|
||||||
const { top, left } = target?.getBoundingClientRect();
|
offset: [simulatorLeft, simulatorTop],
|
||||||
|
|
||||||
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() {
|
initEvent() {
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
IPublicEnumContextMenuType,
|
IPublicEnumContextMenuType,
|
||||||
|
IPublicEnumDragObjectType,
|
||||||
IPublicEnumTransformStage,
|
IPublicEnumTransformStage,
|
||||||
IPublicModelNode,
|
IPublicModelNode,
|
||||||
IPublicModelPluginContext,
|
IPublicModelPluginContext,
|
||||||
|
IPublicTypeDragNodeDataObject,
|
||||||
|
IPublicTypeI18nData,
|
||||||
IPublicTypeNodeSchema,
|
IPublicTypeNodeSchema,
|
||||||
} from '@alilc/lowcode-types';
|
} from '@alilc/lowcode-types';
|
||||||
import { isProjectSchema } from '@alilc/lowcode-utils';
|
import { isI18nData, isProjectSchema } from '@alilc/lowcode-utils';
|
||||||
import { Notification } from '@alifd/next';
|
import { Notification } from '@alifd/next';
|
||||||
import { intl } from '../locale';
|
import { intl, getLocale } from '../locale';
|
||||||
|
|
||||||
function getNodesSchema(nodes: IPublicModelNode[]) {
|
function getNodesSchema(nodes: IPublicModelNode[]) {
|
||||||
const componentsTree = nodes.map((node) => node?.exportSchema(IPublicEnumTransformStage.Clone));
|
const componentsTree = nodes.map((node) => node?.exportSchema(IPublicEnumTransformStage.Clone));
|
||||||
@ -15,6 +18,15 @@ function getNodesSchema(nodes: IPublicModelNode[]) {
|
|||||||
return data;
|
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[]> {
|
async function getClipboardText(): Promise<IPublicTypeNodeSchema[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 使用 Clipboard API 读取剪贴板内容
|
// 使用 Clipboard API 读取剪贴板内容
|
||||||
@ -71,12 +83,18 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
|
|||||||
material.addContextMenuOption({
|
material.addContextMenuOption({
|
||||||
name: 'copyAndPaste',
|
name: 'copyAndPaste',
|
||||||
title: intl('CopyAndPaste'),
|
title: intl('CopyAndPaste'),
|
||||||
|
disabled: (nodes) => {
|
||||||
|
return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0;
|
||||||
|
},
|
||||||
condition: (nodes) => {
|
condition: (nodes) => {
|
||||||
return nodes.length === 1;
|
return nodes.length === 1;
|
||||||
},
|
},
|
||||||
action(nodes) {
|
action(nodes) {
|
||||||
const node = nodes[0];
|
const node = nodes[0];
|
||||||
const { document: doc, parent, index } = node;
|
const { document: doc, parent, index } = node;
|
||||||
|
const data = getNodesSchema(nodes);
|
||||||
|
clipboard.setData(data);
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true);
|
const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true);
|
||||||
newNode?.select();
|
newNode?.select();
|
||||||
@ -87,6 +105,9 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
|
|||||||
material.addContextMenuOption({
|
material.addContextMenuOption({
|
||||||
name: 'copy',
|
name: 'copy',
|
||||||
title: intl('Copy'),
|
title: intl('Copy'),
|
||||||
|
disabled: (nodes) => {
|
||||||
|
return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0;
|
||||||
|
},
|
||||||
condition(nodes) {
|
condition(nodes) {
|
||||||
return nodes.length > 0;
|
return nodes.length > 0;
|
||||||
},
|
},
|
||||||
@ -101,7 +122,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
material.addContextMenuOption({
|
material.addContextMenuOption({
|
||||||
name: 'zhantieToBottom',
|
name: 'pasteToBottom',
|
||||||
title: intl('PasteToTheBottom'),
|
title: intl('PasteToTheBottom'),
|
||||||
condition: (nodes) => {
|
condition: (nodes) => {
|
||||||
return nodes.length === 1;
|
return nodes.length === 1;
|
||||||
@ -116,10 +137,30 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await getClipboardText();
|
const nodeSchema = await getClipboardText();
|
||||||
|
if (nodeSchema.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (parent) {
|
if (parent) {
|
||||||
nodeSchema.forEach((schema, schemaIndex) => {
|
let canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => {
|
||||||
doc?.insertNode(parent, schema, (index ?? 0) + 1 + schemaIndex, true);
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -128,7 +169,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
material.addContextMenuOption({
|
material.addContextMenuOption({
|
||||||
name: 'zhantieToInner',
|
name: 'pasteToInner',
|
||||||
title: intl('PasteToTheInside'),
|
title: intl('PasteToTheInside'),
|
||||||
condition: (nodes) => {
|
condition: (nodes) => {
|
||||||
return nodes.length === 1;
|
return nodes.length === 1;
|
||||||
@ -140,19 +181,35 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
|
|||||||
},
|
},
|
||||||
async action(nodes) {
|
async action(nodes) {
|
||||||
const node = nodes[0];
|
const node = nodes[0];
|
||||||
const { document: doc, parent } = node;
|
const { document: doc } = node;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nodeSchema = await getClipboardText();
|
const nodeSchema = await getClipboardText();
|
||||||
if (parent) {
|
const index = node.children?.size || 0;
|
||||||
const index = node.children?.size || 0;
|
if (nodeSchema.length === 0) {
|
||||||
|
return;
|
||||||
if (parent) {
|
|
||||||
nodeSchema.forEach((schema, schemaIndex) => {
|
|
||||||
doc?.insertNode(node, schema, (index ?? 0) + 1 + schemaIndex, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -162,6 +219,9 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
|
|||||||
material.addContextMenuOption({
|
material.addContextMenuOption({
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
title: intl('Delete'),
|
title: intl('Delete'),
|
||||||
|
disabled(nodes) {
|
||||||
|
return nodes?.filter((node) => !node?.canPerformAction('remove')).length > 0;
|
||||||
|
},
|
||||||
condition(nodes) {
|
condition(nodes) {
|
||||||
return nodes.length > 0;
|
return nodes.length > 0;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { createIntl } from '@alilc/lowcode-editor-core';
|
|||||||
import enUS from './en-US.json';
|
import enUS from './en-US.json';
|
||||||
import zhCN from './zh-CN.json';
|
import zhCN from './zh-CN.json';
|
||||||
|
|
||||||
const { intl } = createIntl?.({
|
const { intl, getLocale } = createIntl?.({
|
||||||
'en-US': enUS,
|
'en-US': enUS,
|
||||||
'zh-CN': zhCN,
|
'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 { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
|
||||||
import { parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils';
|
|
||||||
import { engineConfig } from '@alilc/lowcode-editor-core';
|
import { engineConfig } from '@alilc/lowcode-editor-core';
|
||||||
import { IPublicTypeContextMenuAction } from '@alilc/lowcode-types';
|
import { IPublicTypeContextMenuAction } from '@alilc/lowcode-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -18,8 +17,6 @@ export function ContextMenu({ children, menus }: {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const target = event.target;
|
|
||||||
const { top, left } = target?.getBoundingClientRect();
|
|
||||||
let destroyFn: Function | undefined;
|
let destroyFn: Function | undefined;
|
||||||
const destroy = () => {
|
const destroy = () => {
|
||||||
destroyFn?.();
|
destroyFn?.();
|
||||||
@ -32,13 +29,9 @@ export function ContextMenu({ children, menus }: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuInstance = Menu.create({
|
destroyFn = createContextMenu(children, {
|
||||||
target: event.target,
|
event,
|
||||||
offset: [event.clientX - left, event.clientY - top],
|
|
||||||
children,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
destroyFn = (menuInstance as any).destroy;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 克隆 children 并添加 onContextMenu 事件处理器
|
// 克隆 children 并添加 onContextMenu 事件处理器
|
||||||
|
|||||||
@ -53,6 +53,8 @@ const Tree = (props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let destroyFn: Function | undefined;
|
||||||
|
|
||||||
export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: {
|
export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: {
|
||||||
nodes?: IPublicModelNode[] | null;
|
nodes?: IPublicModelNode[] | null;
|
||||||
destroy?: Function;
|
destroy?: Function;
|
||||||
@ -89,14 +91,12 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
let destroyFn: Function | undefined;
|
|
||||||
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: {
|
export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: {
|
||||||
nodes?: IPublicModelNode[] | null;
|
nodes?: IPublicModelNode[] | null;
|
||||||
destroy?: Function;
|
destroy?: Function;
|
||||||
event?: MouseEvent;
|
event?: MouseEvent;
|
||||||
}, level = 1): IPublicTypeContextMenuItem[] {
|
}, level = 1): IPublicTypeContextMenuItem[] {
|
||||||
destroyFn?.();
|
destroyFn?.();
|
||||||
destroyFn = options.destroy;
|
|
||||||
|
|
||||||
const { nodes, destroy } = options;
|
const { nodes, destroy } = options;
|
||||||
if (level > MAX_LEVEL) {
|
if (level > MAX_LEVEL) {
|
||||||
@ -146,4 +146,55 @@ export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction
|
|||||||
return menus;
|
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