Skip to content

Commit

Permalink
feat: optimize context menu details
Browse files Browse the repository at this point in the history
  • Loading branch information
liujuping committed Jan 10, 2024
1 parent 1472847 commit eb613f1
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 41 deletions.
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;
}

0 comments on commit eb613f1

Please sign in to comment.