mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-11 18:42:56 +00:00
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-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 标签颜色
|
||||||
|
|||||||
@ -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('-') ||
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 事件处理器
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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',
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user