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-disabled`: 禁用态文字颜色
#### 菜单颜色
- `--color-context-menu-text`: 菜单项颜色
- `--color-context-menu-text-hover`: 菜单项 hover 颜色
- `--color-context-menu-text-disabled`: 菜单项 disabled 颜色
#### 字段和边框颜色
- `--color-field-label`: field 标签颜色

View File

@ -832,16 +832,22 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
doc.addEventListener('contextmenu', (e: MouseEvent) => {
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('-') ||

View File

@ -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);

View File

@ -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();
});
},

View File

@ -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 事件处理器

View File

@ -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<IPublicTypeContextMenuAction, 'condition' | 'disabled' | 'items'> {
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<IPublicTypeContextMenuAction, 'items'>[] | ((nodes: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]);
items?: Omit<IPublicTypeContextMenuAction, 'items'>[] | ((nodes?: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]);
/**
*
* 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;
}

View File

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

View File

@ -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((
<PopupItem
className={classNames('engine-context-menu-item', {
disbale: menu.disabled,
disabled: menu.disabled,
})}
key={menu.name}
label={<div className="engine-context-menu-text">{intl(menu.title)}</div>}
@ -93,14 +94,17 @@ export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[],
children.push((
<Item
className={classNames('engine-context-menu-item', {
disbale: menu.disabled,
disabled: menu.disabled,
})}
disabled={menu.disabled}
onClick={menu.action}
onClick={() => {
menu.action?.();
}}
key={menu.name}
>
<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>
</Item>
));
@ -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',
});