feat(context-menu): add context-menu css theme, help config, ts define
This commit is contained in:
parent
6f9359e042
commit
844ca783d7
|
@ -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 标签颜色
|
||||
|
|
|
@ -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('-') ||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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 事件处理器
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue