Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add context menu #161

Merged
merged 5 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/core/src/models/designer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ export class Designer {
*/
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };

/**
* 是否显示右键菜单
*/
_showContextMenu = false;

/**
* 右键菜单在 iframe 上的位置
*/
_contextMenuPosition = { clientX: 0, clientY: 0 };

/**
* 是否显示右侧面板
*/
Expand Down Expand Up @@ -136,6 +146,14 @@ export class Designer {
return this._addComponentPopoverPosition;
}

get showContextMenu() {
return this._showContextMenu;
}

get contextMenuPosition() {
return this._contextMenuPosition;
}

get menuData() {
return this._menuData ?? ([] as MenuDataType);
}
Expand Down Expand Up @@ -178,6 +196,8 @@ export class Designer {
_showRightPanel: observable,
_showAddComponentPopover: observable,
_addComponentPopoverPosition: observable,
_showContextMenu: observable,
_contextMenuPosition: observable,
_menuData: observable,
_isPreview: observable,
simulator: computed,
Expand All @@ -189,6 +209,8 @@ export class Designer {
showSmartWizard: computed,
showAddComponentPopover: computed,
addComponentPopoverPosition: computed,
showContextMenu: computed,
contextMenuPosition: computed,
menuData: computed,
setSimulator: action,
setViewport: action,
Expand All @@ -199,6 +221,7 @@ export class Designer {
toggleSmartWizard: action,
toggleIsPreview: action,
toggleAddComponentPopover: action,
toggleContextMenu: action,
});
}

Expand Down Expand Up @@ -264,4 +287,15 @@ export class Designer {
this.workspace.selectSource.clear();
}
}

toggleContextMenu(
value: boolean,
position: {
clientX: number;
clientY: number;
} = this.contextMenuPosition,
) {
this._showContextMenu = value;
this._contextMenuPosition = position;
}
}
125 changes: 125 additions & 0 deletions packages/designer/src/components/context-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React from 'react';
import { getWidget } from '../widgets';
import { Menu, MenuProps } from 'antd';
import { observer, useWorkspace } from '@music163/tango-context';
import { ISelectedItemData, isString } from '@music163/tango-helpers';
import { Box, css } from 'coral-system';
import { IconFont } from '@music163/tango-ui';

const contextMenuStyle = css`
.ant-dropdown-menu {
width: 240px;
}

.ant-dropdown-menu-title-content {
padding-left: 20px;
}

.ant-dropdown-menu-item-icon + .ant-dropdown-menu-title-content {
padding-left: 0;
}

.ant-dropdown-menu-submenu-popup {
padding: 0;
margin-top: -4px;
}
`;

const ParentNodesMenuItem = ({
record,
onClick,
key,
}: {
record: ISelectedItemData;
onClick: () => any;
key?: string;
}) => {
const workspace = useWorkspace();
const componentPrototype = workspace.componentPrototypes.get(record.name);
const icon = componentPrototype?.icon || 'icon-placeholder';

const iconRender = icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={icon} />
) : (
<img className="material-icon" src={icon} alt={componentPrototype.name} />
);

return (
<Menu.Item key={key || record.id} icon={iconRender} onClick={onClick}>
{record.name}
{!!record.codeId && ` (${record.codeId})`}
</Menu.Item>
);
};

const ParentNodesMenu = observer(() => {
const workspace = useWorkspace();
const selectSource = workspace.selectSource;
const parents = selectSource.first?.parents;

if (!parents?.length) {
return null;
}

return (
<Menu.SubMenu key="parentNodes" title="选取父节点">
{parents.map((item, index) => (
<ParentNodesMenuItem
key={item.id}
record={item}
onClick={() => {
selectSource.select({
...item,
parents: parents.slice(index + 1),
});
}}
/>
))}
</Menu.SubMenu>
);
});

export interface ContextMenuProps extends MenuProps {
/**
* 动作列表,默认列出全部
*/
actions?: Array<string | React.ReactElement>;
/**
* 是否显示父节点选项
*/
showParents?: boolean;
className?: string;
style?: React.CSSProperties;
menuStyle?: React.CSSProperties;
}

export const ContextMenu = observer(
({
showParents,
actions: actionsProp,
className,
style,
menuStyle,
...rest
}: ContextMenuProps) => {
const actions = actionsProp || Object.keys(getWidget('contextMenu'));
const menus = actions.map((item) => {
if (isString(item)) {
const widget = getWidget(['contextMenu', item].join('.'));
return widget ? React.createElement(widget) : null;
}
return item;
});
if (showParents) {
menus.unshift(<ParentNodesMenu />);
}

return (
<Box display="inline-block" css={contextMenuStyle} className={className} style={style}>
<Menu activeKey={null} {...rest} style={menuStyle}>
{menus}
</Menu>
</Box>
);
},
);
1 change: 1 addition & 0 deletions packages/designer/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './context-menu';
export * from './drag-box';
export * from './input-kv';
export * from './variable-tree';
Expand Down
19 changes: 19 additions & 0 deletions packages/designer/src/context-menu/copy-node.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { useWorkspace, observer } from '@music163/tango-context';
import { ContextAction } from '@music163/tango-ui';
import { CopyOutlined } from '@ant-design/icons';

export const CopyNodeContextAction = observer(() => {
const workspace = useWorkspace();
return (
<ContextAction
icon={<CopyOutlined />}
hotkey="Command+C"
onClick={() => {
workspace.copySelectedNode();
}}
>
复制节点
</ContextAction>
);
});
19 changes: 19 additions & 0 deletions packages/designer/src/context-menu/delete-node.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { useWorkspace, observer } from '@music163/tango-context';
import { ContextAction } from '@music163/tango-ui';
import { DeleteOutlined } from '@ant-design/icons';

export const DeleteNodeContextAction = observer(() => {
const workspace = useWorkspace();
return (
<ContextAction
icon={<DeleteOutlined />}
hotkey="Backspace"
onClick={() => {
workspace.removeSelectedNode();
}}
>
删除节点
</ContextAction>
);
});
4 changes: 4 additions & 0 deletions packages/designer/src/context-menu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './copy-node';
export * from './delete-node';
export * from './paste-node';
export * from './view-source';
19 changes: 19 additions & 0 deletions packages/designer/src/context-menu/paste-node.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { useWorkspace, observer } from '@music163/tango-context';
import { ContextAction } from '@music163/tango-ui';
import { SnippetsOutlined } from '@ant-design/icons';

export const PasteNodeContextAction = observer(() => {
const workspace = useWorkspace();
return (
<ContextAction
icon={<SnippetsOutlined />}
hotkey="Command+V"
onClick={() => {
workspace.pasteSelectedNode();
}}
>
粘贴节点
</ContextAction>
);
});
18 changes: 18 additions & 0 deletions packages/designer/src/context-menu/view-source.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { useDesigner, observer } from '@music163/tango-context';
import { ContextAction } from '@music163/tango-ui';
import { CodeOutlined } from '@ant-design/icons';

export const ViewSourceContextAction = observer(() => {
const designer = useDesigner();
return (
<ContextAction
icon={<CodeOutlined />}
onClick={() => {
designer.setActiveView('code');
}}
>
查看源码
</ContextAction>
);
});
57 changes: 57 additions & 0 deletions packages/designer/src/dnd/use-dnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export function useDnd({
if (designer.isPreview) {
return;
}
if (designer.showContextMenu) {
designer.toggleContextMenu(false);
}

const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
selectSource.setStart({
Expand Down Expand Up @@ -128,6 +131,9 @@ export function useDnd({
};

const onClick = (e: React.MouseEvent) => {
if (designer.showContextMenu) {
designer.toggleContextMenu(false);
}
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
if (data && data.id) {
selectSource.select(data);
Expand Down Expand Up @@ -388,6 +394,56 @@ export function useDnd({
}
};

const onContextMenu = (event: React.MouseEvent) => {
if (designer.showContextMenu) {
designer.toggleContextMenu(false);
}
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
return;
}
const { clientX, clientY } = event;
let target;
if (workspace.selectSource.isSelected) {
for (const item of workspace.selectSource.selected) {
if (
// 如果选中的节点是页面根节点(无 parents),则忽略
item.parents?.length &&
item.bounding &&
clientX >= item.bounding.left &&
clientX <= item.bounding.left + item.bounding.width &&
clientY >= item.bounding.top &&
clientY <= item.bounding.top + item.bounding.height
) {
// 右键坐标已经在当前选中组件的选区内,直接展示右键菜单
target = item;
break;
}
}
}
// 否则,根据右键的元素选中最接近的组件
if (!target) {
target = sandboxQuery.getDraggableParentsData(event.target as HTMLElement, true);
}
if (target && target.id) {
if (!target.parents?.length) {
// 页面根节点不展示右键菜单操作
return;
}
// 右键时高亮选中当前元素
// 以防之前选区有多个元素,即便已经是选中的元素也再选中一遍
event.preventDefault();
selectSource.select(target);
// 在下一周期再展示右键菜单,以让先前的菜单先被销毁
requestAnimationFrame(() => {
designer.toggleContextMenu(true, {
clientX,
clientY,
});
});
}
};

const onTango = (e: CustomEvent) => {
const detail = e.detail || {};

Expand Down Expand Up @@ -437,6 +493,7 @@ export function useDnd({
onDragEnd,
onScroll,
onKeyDown,
onContextMenu,
onTango,
...selectHandler,
};
Expand Down
1 change: 1 addition & 0 deletions packages/designer/src/selection-menu/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './copy-node';
export * from './delete-node';
export * from './more-actions';
export * from './select-parent-node';
export * from './view-source';
16 changes: 16 additions & 0 deletions packages/designer/src/selection-menu/more-actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { observer } from '@music163/tango-context';
import { SelectAction } from '@music163/tango-ui';
import { MoreOutlined } from '@ant-design/icons';
import { Dropdown } from 'antd';
import { ContextMenu } from '../components';

export const MoreActionsAction = observer(() => {
return (
<Dropdown placement="bottomRight" overlay={<ContextMenu showParents />}>
<SelectAction tooltip="更多">
<MoreOutlined />
</SelectAction>
</Dropdown>
);
});
Loading
Loading