定义 ContextMenu 组件

  1. import React, { useRef, useEffect, FC } from "react"
  2. import { getParentElement } from "@/util"
  3. import { ActionItem } from './index'
  4. import styles from "./index.less"
  5. interface IProps {
  6. actions: ActionItem[],
  7. triggerClass?: string
  8. }
  9. const ContextMenu: FC<IProps> = (props) => {
  10. const menuRef = useRef()
  11. const componentId = useRef<string>('')
  12. useEffect(() => {
  13. document.addEventListener('contextmenu', triggerContextMenu);
  14. document.addEventListener('click', handleClick);
  15. return () => {
  16. document.removeEventListener('contextmenu', triggerContextMenu);
  17. document.removeEventListener('click', handleClick);
  18. };
  19. }, []);
  20. const triggerContextMenu = (e: MouseEvent) => {
  21. const menuContainer = menuRef.current as HTMLDivElement
  22. const warpperElement = getParentElement(e.target as HTMLElement, props.triggerClass);
  23. if (warpperElement) {
  24. e.preventDefault();
  25. menuContainer.style.display = 'block';
  26. menuContainer.style.top = e.pageY + 'px';
  27. menuContainer.style.left = e.pageX + 'px';
  28. // 可以通过 warpperElement data-component-id 拿到对应的 id
  29. componentId.current = warpperElement.getAttribute("data-component-id")
  30. }
  31. };
  32. const handleClick = () => {
  33. const menuContainer = menuRef.current as HTMLDivElement
  34. menuContainer.style.display = "none"
  35. }
  36. return (
  37. <ul
  38. className={styles['menu-container']}
  39. ref={menuRef}
  40. id="menuContainer"
  41. style={{ display: "none" }}
  42. >
  43. {props.actions.map((item, index) => {
  44. return (
  45. <li onClick={() => item.action(componentId.current)} key={index}>
  46. {item.text} {item.shortcut}
  47. </li>
  48. );
  49. })}
  50. </ul>
  51. );
  52. };
  53. ContextMenu.defaultProps = {
  54. triggerClass: 'edit-wrapper-box'
  55. }
  56. export default ContextMenu

对 contextMenu 事件进行监听、然后利用 ReactDOM 添加到 body

  1. export interface ActionItem {
  2. action: (cid?: string) => void;
  3. text: string;
  4. shortcut: string;
  5. }
  6. export default function createContextMenu(actions: ActionItem[], triggerClass = "edit-wrapper-box") {
  7. const container = document.createElement("div")
  8. // ReactDOM.createPortal 也可以添加到外部
  9. ReactDOM.render(<ContextMenu actions={actions} triggerClass={triggerClass} />, container)
  10. document.body.append(container)
  11. }


插件化 ContextMenu


我们可以对不同区域进行不同的右键菜单功能开发、如下

  1. import { useEffect, useContext } from "react"
  2. import { ActionItem } from "@/components/contextMenu"
  3. import createContextMenu from '@/components/contextMenu'
  4. import { AppContext, IContextProps } from '@/store/context';
  5. import { SETACTIVE, DELETECOMPONENT, PASTECOPIEDCOMPONENT, COPYCOMPONENT } from '@/store/contant'
  6. export default function InitContextMenu() {
  7. const { dispatch } = useContext<IContextProps>(AppContext);
  8. const editorActions: ActionItem[] = [
  9. {
  10. shortcut: '⌘C / Ctrl+C', text: "拷贝图层", action: (cid: string) => {
  11. dispatch({
  12. type: COPYCOMPONENT,
  13. data: {
  14. id: cid
  15. }
  16. });
  17. }
  18. },
  19. {
  20. shortcut: '⌘V / Ctrl+V', text: "粘贴图层", action: (cid: string) => {
  21. dispatch({
  22. type: PASTECOPIEDCOMPONENT,
  23. });
  24. }
  25. },
  26. {
  27. shortcut: 'Backspace / Delete', text: "删除图层", action: (cid: string) => {
  28. dispatch({
  29. type: DELETECOMPONENT,
  30. data: {
  31. id: cid
  32. }
  33. });
  34. }
  35. },
  36. {
  37. shortcut: 'ESC', text: "取消选中", action: (cid: string) => {
  38. dispatch({
  39. type: SETACTIVE,
  40. data: {
  41. value: '',
  42. },
  43. });
  44. }
  45. },
  46. {
  47. shortcut: '⌘Z / Ctrl+Z', text: "撤销", action: (cid: string) => {
  48. alert("待实现")
  49. }
  50. },
  51. {
  52. shortcut: ' ⌘⇧Z / Ctrl+Shift+Z', text: "重做", action: (cid: string) => {
  53. alert("待实现")
  54. }
  55. },
  56. ]
  57. const settingAction: ActionItem[] = [
  58. { shortcut: 'cv', text: "复制配置", action: () => { console.log("复制配置") } }
  59. ]
  60. useEffect(() => {
  61. // 画布右键操作
  62. createContextMenu(editorActions, "edit-wrapper-box")
  63. // 表单编辑右键操作
  64. createContextMenu(settingAction, "pane-setting")
  65. }, [dispatch])
  66. }


最后我们只需要在 main 里面调用下 自定义的 Hooks 即可、但是我们实现了业务和逻辑的结偶、利用自定义的 hooks 我们可以随意自定义 右键菜单并进行不同的操作