From 844ca783d720e5d8829a8e8e54314c97997ec275 Mon Sep 17 00:00:00 2001 From: liujuping Date: Thu, 11 Jan 2024 18:49:21 +0800 Subject: [PATCH] feat(context-menu): add context-menu css theme, help config, ts define --- docs/docs/guide/expand/editor/theme.md | 5 +++ .../designer/src/builtin-simulator/host.ts | 8 ++++- packages/designer/src/context-menu-actions.ts | 2 ++ .../src/inner-plugins/default-context-menu.ts | 36 +++++++++++-------- .../shell/src/components/context-menu.tsx | 8 ++--- packages/types/src/shell/type/context-menu.ts | 14 +++++--- packages/utils/src/context-menu.scss | 21 +++++++---- packages/utils/src/context-menu.tsx | 33 ++++++++++------- 8 files changed, 81 insertions(+), 46 deletions(-) diff --git a/docs/docs/guide/expand/editor/theme.md b/docs/docs/guide/expand/editor/theme.md index 1c442c513..897b6b360 100644 --- a/docs/docs/guide/expand/editor/theme.md +++ b/docs/docs/guide/expand/editor/theme.md @@ -53,6 +53,11 @@ sidebar_position: 9 - `--color-text-reverse`: 反色情况下,文字颜色 - `--color-text-disabled`: 禁用态文字颜色 +#### 菜单颜色 +- `--color-context-menu-text`: 菜单项颜色 +- `--color-context-menu-text-hover`: 菜单项 hover 颜色 +- `--color-context-menu-text-disabled`: 菜单项 disabled 颜色 + #### 字段和边框颜色 - `--color-field-label`: field 标签颜色 diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts index 6efe6a68b..57f856932 100644 --- a/packages/designer/src/builtin-simulator/host.ts +++ b/packages/designer/src/builtin-simulator/host.ts @@ -832,16 +832,22 @@ export class BuiltinSimulatorHost implements ISimulatorHost { const targetElement = e.target as HTMLElement; const nodeInst = this.getNodeInstanceFromElement(targetElement); + const editor = this.designer?.editor; if (!nodeInst) { + editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { + originalEvent: e, + }); return; } const node = nodeInst.node || this.project.currentDocument?.focusNode; if (!node) { + editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { + originalEvent: e, + }); return; } // dirty code should refector - const editor = this.designer?.editor; const npm = node?.componentMeta?.npm; const selected = [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || diff --git a/packages/designer/src/context-menu-actions.ts b/packages/designer/src/context-menu-actions.ts index c3bad37da..c88e03ac6 100644 --- a/packages/designer/src/context-menu-actions.ts +++ b/packages/designer/src/context-menu-actions.ts @@ -201,6 +201,8 @@ export class ContextMenuActions implements IContextMenuActions { node: INode; originalEvent: MouseEvent; }) => { + originalEvent.stopPropagation(); + originalEvent.preventDefault(); // 如果右键的节点不在 当前选中的节点中,选中该节点 if (!designer.currentSelection.has(node.id)) { designer.currentSelection.select(node.id); diff --git a/packages/engine/src/inner-plugins/default-context-menu.ts b/packages/engine/src/inner-plugins/default-context-menu.ts index db3d54a0c..c8997aa7e 100644 --- a/packages/engine/src/inner-plugins/default-context-menu.ts +++ b/packages/engine/src/inner-plugins/default-context-menu.ts @@ -60,7 +60,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { material.addContextMenuOption({ name: 'selectComponent', title: intl('SelectComponents'), - condition: (nodes) => { + condition: (nodes = []) => { return nodes.length === 1; }, items: [ @@ -74,14 +74,17 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { material.addContextMenuOption({ name: 'copyAndPaste', title: intl('CopyAndPaste'), - disabled: (nodes) => { + disabled: (nodes = []) => { return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; }, condition: (nodes) => { - return nodes.length === 1; + return nodes?.length === 1; }, action(nodes) { - const node = nodes[0]; + const node = nodes?.[0]; + if (!node) { + return; + } const { document: doc, parent, index } = node; const data = getNodesSchema(nodes); clipboard.setData(data); @@ -96,11 +99,11 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { material.addContextMenuOption({ name: 'copy', title: intl('Copy'), - disabled: (nodes) => { + disabled: (nodes = []) => { return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; }, - condition(nodes) { - return nodes.length > 0; + condition(nodes = []) { + return nodes?.length > 0; }, action(nodes) { if (!nodes || nodes.length < 1) { @@ -116,7 +119,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { name: 'pasteToBottom', title: intl('PasteToTheBottom'), condition: (nodes) => { - return nodes.length === 1; + return nodes?.length === 1; }, async action(nodes) { if (!nodes || nodes.length < 1) { @@ -163,15 +166,18 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { name: 'pasteToInner', title: intl('PasteToTheInside'), condition: (nodes) => { - return nodes.length === 1; + return nodes?.length === 1; }, - disabled: (nodes) => { + disabled: (nodes = []) => { // 获取粘贴数据 - const node = nodes[0]; + const node = nodes?.[0]; return !node.isContainerNode; }, async action(nodes) { - const node = nodes[0]; + const node = nodes?.[0]; + if (!node) { + return; + } const { document: doc } = node; try { @@ -210,14 +216,14 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { material.addContextMenuOption({ name: 'delete', title: intl('Delete'), - disabled(nodes) { + disabled(nodes = []) { return nodes?.filter((node) => !node?.canPerformAction('remove')).length > 0; }, - condition(nodes) { + condition(nodes = []) { return nodes.length > 0; }, action(nodes) { - nodes.forEach((node) => { + nodes?.forEach((node) => { node.remove(); }); }, diff --git a/packages/shell/src/components/context-menu.tsx b/packages/shell/src/components/context-menu.tsx index d2e10abbf..c752e7990 100644 --- a/packages/shell/src/components/context-menu.tsx +++ b/packages/shell/src/components/context-menu.tsx @@ -25,17 +25,13 @@ export function ContextMenu({ children, menus, pluginContext }: { const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { destroy, pluginContext, - }), { - pluginContext, - }); + }), { pluginContext }); if (!children?.length) { return; } - destroyFn = createContextMenu(children, { - event, - }); + destroyFn = createContextMenu(children, { event }); }; // 克隆 children 并添加 onContextMenu 事件处理器 diff --git a/packages/types/src/shell/type/context-menu.ts b/packages/types/src/shell/type/context-menu.ts index 1eeb93d69..dd6d583c2 100644 --- a/packages/types/src/shell/type/context-menu.ts +++ b/packages/types/src/shell/type/context-menu.ts @@ -1,6 +1,7 @@ import { IPublicEnumContextMenuType } from '../enum'; import { IPublicModelNode } from '../model'; import { IPublicTypeI18nData } from './i8n-data'; +import { IPublicTypeHelpTipConfig } from './widget-base-config'; export interface IPublicTypeContextMenuItem extends Omit { disabled?: boolean; @@ -34,24 +35,29 @@ export interface IPublicTypeContextMenuAction { * 点击时执行的动作,可选 * Action to execute on click, optional */ - action?: (nodes: IPublicModelNode[], event?: MouseEvent) => void; + action?: (nodes?: IPublicModelNode[], event?: MouseEvent) => void; /** * 子菜单项或生成子节点的函数,可选,仅支持两级 * Sub-menu items or function to generate child node, optional */ - items?: Omit[] | ((nodes: IPublicModelNode[]) => Omit[]); + items?: Omit[] | ((nodes?: IPublicModelNode[]) => Omit[]); /** * 显示条件函数 * Function to determine display condition */ - condition?: (nodes: IPublicModelNode[]) => boolean; + condition?: (nodes?: IPublicModelNode[]) => boolean; /** * 禁用条件函数,可选 * Function to determine disabled condition, optional */ - disabled?: (nodes: IPublicModelNode[]) => boolean; + disabled?: (nodes?: IPublicModelNode[]) => boolean; + + /** + * 帮助提示,可选 + */ + help?: IPublicTypeHelpTipConfig; } diff --git a/packages/utils/src/context-menu.scss b/packages/utils/src/context-menu.scss index 366d03e45..0b75ca3ec 100644 --- a/packages/utils/src/context-menu.scss +++ b/packages/utils/src/context-menu.scss @@ -10,24 +10,31 @@ .engine-context-menu-item { .engine-context-menu-text { - color: var(--color-text); + color: var(--color-context-menu-text, var(--color-text)); + display: flex; + align-items: center; + + .lc-help-tip { + margin-left: 4px; + opacity: 0.8; + } } - &:hover { - .engine-context-menu-text { - color: var(--color-title); + &.disabled { + &:hover .engine-context-menu-text, .engine-context-menu-text { + color: var(--color-context-menu-text-disabled, var(--color-text-disabled)); } } - &.disbale { + &:hover { .engine-context-menu-text { - color: var(--color-text-disabled); + color: var(--color-context-menu-text-hover, var(--color-title)); } } } .engine-context-menu-title { - color: var(--color-text); + color: var(--color-context-menu-text, var(--color-text)); cursor: pointer; &:hover { diff --git a/packages/utils/src/context-menu.tsx b/packages/utils/src/context-menu.tsx index 0f157e438..d783a9643 100644 --- a/packages/utils/src/context-menu.tsx +++ b/packages/utils/src/context-menu.tsx @@ -64,8 +64,9 @@ const Tree = (props: { let destroyFn: Function | undefined; export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: IOptions): React.ReactNode[] { - const { common } = options.pluginContext || {}; + const { common, commonUI } = options.pluginContext || {}; const { intl = (title: any) => title } = common?.utils || {}; + const { HelpTip } = commonUI || {}; const children: React.ReactNode[] = []; menus.forEach((menu, index) => { @@ -79,7 +80,7 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], children.push(( {intl(menu.title)}} @@ -93,14 +94,17 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], children.push(( { + menu.action?.(); + }} key={menu.name} >
- {intl(menu.title)} + { menu.title ? intl(menu.title) : null } + { menu.help ? : null }
)); @@ -135,12 +139,14 @@ export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction name, title, type = IPublicEnumContextMenuType.MENU_ITEM, + help, } = menu; const result: IPublicTypeContextMenuItem = { name, title, type, + help, action: () => { destroy?.(); menu.action?.(nodes || [], options.event); @@ -193,26 +199,27 @@ export function createContextMenu(children: React.ReactNode[], { event: MouseEvent | React.MouseEvent; offset?: [number, number]; }) { + event.preventDefault(); + event.stopPropagation(); + 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) { + let x = event.clientX + offset[0]; + let y = event.clientY + offset[1]; + if (x + menuWidthLimit > viewportWidth) { x = x - menuWidthLimit; } - if (y + menuHeight + top > viewportHeight) { + if (y + menuHeight > viewportHeight) { y = y - menuHeight; } const menuInstance = Menu.create({ - target, - offset: [x, y, 0, 0], + target: document.body, + offset: [x, y], children, className: 'engine-context-menu', });