feat(context-menu): add context-menu css theme, help config, ts define

This commit is contained in:
liujuping 2024-01-11 18:49:21 +08:00 committed by 林熠
parent 6f9359e042
commit 844ca783d7
8 changed files with 83 additions and 48 deletions

View File

@ -53,6 +53,11 @@ sidebar_position: 9
- `--color-text-reverse`: 反色情况下,文字颜色 - `--color-text-reverse`: 反色情况下,文字颜色
- `--color-text-disabled`: 禁用态文字颜色 - `--color-text-disabled`: 禁用态文字颜色
#### 菜单颜色
- `--color-context-menu-text`: 菜单项颜色
- `--color-context-menu-text-hover`: 菜单项 hover 颜色
- `--color-context-menu-text-disabled`: 菜单项 disabled 颜色
#### 字段和边框颜色 #### 字段和边框颜色
- `--color-field-label`: field 标签颜色 - `--color-field-label`: field 标签颜色

View File

@ -832,16 +832,22 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
doc.addEventListener('contextmenu', (e: MouseEvent) => { doc.addEventListener('contextmenu', (e: MouseEvent) => {
const targetElement = e.target as HTMLElement; const targetElement = e.target as HTMLElement;
const nodeInst = this.getNodeInstanceFromElement(targetElement); const nodeInst = this.getNodeInstanceFromElement(targetElement);
const editor = this.designer?.editor;
if (!nodeInst) { if (!nodeInst) {
editor?.eventBus.emit('designer.builtinSimulator.contextmenu', {
originalEvent: e,
});
return; return;
} }
const node = nodeInst.node || this.project.currentDocument?.focusNode; const node = nodeInst.node || this.project.currentDocument?.focusNode;
if (!node) { if (!node) {
editor?.eventBus.emit('designer.builtinSimulator.contextmenu', {
originalEvent: e,
});
return; return;
} }
// dirty code should refector // dirty code should refector
const editor = this.designer?.editor;
const npm = node?.componentMeta?.npm; const npm = node?.componentMeta?.npm;
const selected = const selected =
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') || [npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||

View File

@ -201,6 +201,8 @@ export class ContextMenuActions implements IContextMenuActions {
node: INode; node: INode;
originalEvent: MouseEvent; originalEvent: MouseEvent;
}) => { }) => {
originalEvent.stopPropagation();
originalEvent.preventDefault();
// 如果右键的节点不在 当前选中的节点中,选中该节点 // 如果右键的节点不在 当前选中的节点中,选中该节点
if (!designer.currentSelection.has(node.id)) { if (!designer.currentSelection.has(node.id)) {
designer.currentSelection.select(node.id); designer.currentSelection.select(node.id);

View File

@ -60,7 +60,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
material.addContextMenuOption({ material.addContextMenuOption({
name: 'selectComponent', name: 'selectComponent',
title: intl('SelectComponents'), title: intl('SelectComponents'),
condition: (nodes) => { condition: (nodes = []) => {
return nodes.length === 1; return nodes.length === 1;
}, },
items: [ items: [
@ -74,14 +74,17 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
material.addContextMenuOption({ material.addContextMenuOption({
name: 'copyAndPaste', name: 'copyAndPaste',
title: intl('CopyAndPaste'), title: intl('CopyAndPaste'),
disabled: (nodes) => { disabled: (nodes = []) => {
return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; 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];
if (!node) {
return;
}
const { document: doc, parent, index } = node; const { document: doc, parent, index } = node;
const data = getNodesSchema(nodes); const data = getNodesSchema(nodes);
clipboard.setData(data); clipboard.setData(data);
@ -96,11 +99,11 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
material.addContextMenuOption({ material.addContextMenuOption({
name: 'copy', name: 'copy',
title: intl('Copy'), title: intl('Copy'),
disabled: (nodes) => { disabled: (nodes = []) => {
return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0;
}, },
condition(nodes) { condition(nodes = []) {
return nodes.length > 0; return nodes?.length > 0;
}, },
action(nodes) { action(nodes) {
if (!nodes || nodes.length < 1) { if (!nodes || nodes.length < 1) {
@ -116,7 +119,7 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
name: 'pasteToBottom', name: 'pasteToBottom',
title: intl('PasteToTheBottom'), title: intl('PasteToTheBottom'),
condition: (nodes) => { condition: (nodes) => {
return nodes.length === 1; return nodes?.length === 1;
}, },
async action(nodes) { async action(nodes) {
if (!nodes || nodes.length < 1) { if (!nodes || nodes.length < 1) {
@ -163,15 +166,18 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
name: 'pasteToInner', name: 'pasteToInner',
title: intl('PasteToTheInside'), title: intl('PasteToTheInside'),
condition: (nodes) => { 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; return !node.isContainerNode;
}, },
async action(nodes) { async action(nodes) {
const node = nodes[0]; const node = nodes?.[0];
if (!node) {
return;
}
const { document: doc } = node; const { document: doc } = node;
try { try {
@ -210,14 +216,14 @@ export const defaultContextMenu = (ctx: IPublicModelPluginContext) => {
material.addContextMenuOption({ material.addContextMenuOption({
name: 'delete', name: 'delete',
title: intl('Delete'), title: intl('Delete'),
disabled(nodes) { disabled(nodes = []) {
return nodes?.filter((node) => !node?.canPerformAction('remove')).length > 0; return nodes?.filter((node) => !node?.canPerformAction('remove')).length > 0;
}, },
condition(nodes) { condition(nodes = []) {
return nodes.length > 0; return nodes.length > 0;
}, },
action(nodes) { action(nodes) {
nodes.forEach((node) => { nodes?.forEach((node) => {
node.remove(); node.remove();
}); });
}, },

View File

@ -25,17 +25,13 @@ export function ContextMenu({ children, menus, pluginContext }: {
const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, {
destroy, destroy,
pluginContext, pluginContext,
}), { }), { pluginContext });
pluginContext,
});
if (!children?.length) { if (!children?.length) {
return; return;
} }
destroyFn = createContextMenu(children, { destroyFn = createContextMenu(children, { event });
event,
});
}; };
// 克隆 children 并添加 onContextMenu 事件处理器 // 克隆 children 并添加 onContextMenu 事件处理器

View File

@ -1,6 +1,7 @@
import { IPublicEnumContextMenuType } from '../enum'; import { IPublicEnumContextMenuType } from '../enum';
import { IPublicModelNode } from '../model'; import { IPublicModelNode } from '../model';
import { IPublicTypeI18nData } from './i8n-data'; import { IPublicTypeI18nData } from './i8n-data';
import { IPublicTypeHelpTipConfig } from './widget-base-config';
export interface IPublicTypeContextMenuItem extends Omit<IPublicTypeContextMenuAction, 'condition' | 'disabled' | 'items'> { export interface IPublicTypeContextMenuItem extends Omit<IPublicTypeContextMenuAction, 'condition' | 'disabled' | 'items'> {
disabled?: boolean; disabled?: boolean;
@ -34,24 +35,29 @@ export interface IPublicTypeContextMenuAction {
* *
* Action to execute on click, optional * 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 * Sub-menu items or function to generate child node, optional
*/ */
items?: Omit<IPublicTypeContextMenuAction, 'items'>[] | ((nodes: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]); items?: Omit<IPublicTypeContextMenuAction, 'items'>[] | ((nodes?: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]);
/** /**
* *
* Function to determine display condition * Function to determine display condition
*/ */
condition?: (nodes: IPublicModelNode[]) => boolean; condition?: (nodes?: IPublicModelNode[]) => boolean;
/** /**
* *
* Function to determine disabled condition, optional * Function to determine disabled condition, optional
*/ */
disabled?: (nodes: IPublicModelNode[]) => boolean; disabled?: (nodes?: IPublicModelNode[]) => boolean;
/**
*
*/
help?: IPublicTypeHelpTipConfig;
} }

View File

@ -10,24 +10,31 @@
.engine-context-menu-item { .engine-context-menu-item {
.engine-context-menu-text { .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;
}
}
&.disabled {
&:hover .engine-context-menu-text, .engine-context-menu-text {
color: var(--color-context-menu-text-disabled, var(--color-text-disabled));
}
} }
&:hover { &:hover {
.engine-context-menu-text { .engine-context-menu-text {
color: var(--color-title); color: var(--color-context-menu-text-hover, var(--color-title));
}
}
&.disbale {
.engine-context-menu-text {
color: var(--color-text-disabled);
} }
} }
} }
.engine-context-menu-title { .engine-context-menu-title {
color: var(--color-text); color: var(--color-context-menu-text, var(--color-text));
cursor: pointer; cursor: pointer;
&:hover { &:hover {

View File

@ -64,8 +64,9 @@ const Tree = (props: {
let destroyFn: Function | undefined; let destroyFn: Function | undefined;
export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: IOptions): React.ReactNode[] { 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 { intl = (title: any) => title } = common?.utils || {};
const { HelpTip } = commonUI || {};
const children: React.ReactNode[] = []; const children: React.ReactNode[] = [];
menus.forEach((menu, index) => { menus.forEach((menu, index) => {
@ -79,7 +80,7 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
children.push(( children.push((
<PopupItem <PopupItem
className={classNames('engine-context-menu-item', { className={classNames('engine-context-menu-item', {
disbale: menu.disabled, disabled: menu.disabled,
})} })}
key={menu.name} key={menu.name}
label={<div className="engine-context-menu-text">{intl(menu.title)}</div>} label={<div className="engine-context-menu-text">{intl(menu.title)}</div>}
@ -93,14 +94,17 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
children.push(( children.push((
<Item <Item
className={classNames('engine-context-menu-item', { className={classNames('engine-context-menu-item', {
disbale: menu.disabled, disabled: menu.disabled,
})} })}
disabled={menu.disabled} disabled={menu.disabled}
onClick={menu.action} onClick={() => {
menu.action?.();
}}
key={menu.name} key={menu.name}
> >
<div className="engine-context-menu-text"> <div className="engine-context-menu-text">
{intl(menu.title)} { menu.title ? intl(menu.title) : null }
{ menu.help ? <HelpTip size="xs" help={menu.help} direction="right" /> : null }
</div> </div>
</Item> </Item>
)); ));
@ -135,12 +139,14 @@ export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction
name, name,
title, title,
type = IPublicEnumContextMenuType.MENU_ITEM, type = IPublicEnumContextMenuType.MENU_ITEM,
help,
} = menu; } = menu;
const result: IPublicTypeContextMenuItem = { const result: IPublicTypeContextMenuItem = {
name, name,
title, title,
type, type,
help,
action: () => { action: () => {
destroy?.(); destroy?.();
menu.action?.(nodes || [], options.event); menu.action?.(nodes || [], options.event);
@ -193,26 +199,27 @@ export function createContextMenu(children: React.ReactNode[], {
event: MouseEvent | React.MouseEvent; event: MouseEvent | React.MouseEvent;
offset?: [number, number]; offset?: [number, number];
}) { }) {
event.preventDefault();
event.stopPropagation();
const viewportWidth = window.innerWidth; const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
const dividerCount = React.Children.count(children.filter(child => React.isValidElement(child) && child.type === Divider)); 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 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 menuHeight = popupItemCount * parseInt(getMenuItemHeight(), 10) + dividerCount * 8 + 16;
const menuWidthLimit = 200; const menuWidthLimit = 200;
const target = event.target; let x = event.clientX + offset[0];
const { top, left } = (target as any)?.getBoundingClientRect(); let y = event.clientY + offset[1];
let x = event.clientX - left + offset[0]; if (x + menuWidthLimit > viewportWidth) {
let y = event.clientY - top + offset[1];
if (x + menuWidthLimit + left > viewportWidth) {
x = x - menuWidthLimit; x = x - menuWidthLimit;
} }
if (y + menuHeight + top > viewportHeight) { if (y + menuHeight > viewportHeight) {
y = y - menuHeight; y = y - menuHeight;
} }
const menuInstance = Menu.create({ const menuInstance = Menu.create({
target, target: document.body,
offset: [x, y, 0, 0], offset: [x, y],
children, children,
className: 'engine-context-menu', className: 'engine-context-menu',
}); });