Skip to content

Commit

Permalink
feat: add context menu (#161)
Browse files Browse the repository at this point in the history
* feat: add more actions menu on SelectionTools

* feat: add view source context action

* feat: normalize hotkey by platform

* feat: add context menu to components tree

* feat: add context menu to viewport
  • Loading branch information
ccloli authored May 31, 2024
1 parent 3f3981b commit 28040fc
Show file tree
Hide file tree
Showing 18 changed files with 559 additions and 60 deletions.
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

0 comments on commit 28040fc

Please sign in to comment.