You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.tsx 2.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import { useCallback, useRef, useState } from 'react';
  2. import { NodeMouseHandler, useReactFlow } from 'reactflow';
  3. import styles from './index.less';
  4. export interface INodeContextMenu {
  5. id: string;
  6. top: number;
  7. left: number;
  8. right?: number;
  9. bottom?: number;
  10. [key: string]: unknown;
  11. }
  12. export function NodeContextMenu({
  13. id,
  14. top,
  15. left,
  16. right,
  17. bottom,
  18. ...props
  19. }: INodeContextMenu) {
  20. const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
  21. const duplicateNode = useCallback(() => {
  22. const node = getNode(id);
  23. const position = {
  24. x: node?.position?.x || 0 + 50,
  25. y: node?.position?.y || 0 + 50,
  26. };
  27. addNodes({
  28. ...(node || {}),
  29. data: node?.data,
  30. selected: false,
  31. dragging: false,
  32. id: `${node?.id}-copy`,
  33. position,
  34. });
  35. }, [id, getNode, addNodes]);
  36. const deleteNode = useCallback(() => {
  37. setNodes((nodes) => nodes.filter((node) => node.id !== id));
  38. setEdges((edges) => edges.filter((edge) => edge.source !== id));
  39. }, [id, setNodes, setEdges]);
  40. return (
  41. <div
  42. style={{ top, left, right, bottom }}
  43. className={styles.contextMenu}
  44. {...props}
  45. >
  46. <p style={{ margin: '0.5em' }}>
  47. <small>node: {id}</small>
  48. </p>
  49. <button onClick={duplicateNode} type={'button'}>
  50. duplicate
  51. </button>
  52. <button onClick={deleteNode} type={'button'}>
  53. delete
  54. </button>
  55. </div>
  56. );
  57. }
  58. export const useHandleNodeContextMenu = (sideWidth: number) => {
  59. const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu);
  60. const ref = useRef<any>(null);
  61. const onNodeContextMenu: NodeMouseHandler = useCallback(
  62. (event, node) => {
  63. // Prevent native context menu from showing
  64. event.preventDefault();
  65. // Calculate position of the context menu. We want to make sure it
  66. // doesn't get positioned off-screen.
  67. const pane = ref.current?.getBoundingClientRect();
  68. // setMenu({
  69. // id: node.id,
  70. // top: event.clientY < pane.height - 200 ? event.clientY : 0,
  71. // left: event.clientX < pane.width - 200 ? event.clientX : 0,
  72. // right: event.clientX >= pane.width - 200 ? pane.width - event.clientX : 0,
  73. // bottom:
  74. // event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
  75. // });
  76. setMenu({
  77. id: node.id,
  78. top: event.clientY - 144,
  79. left: event.clientX - sideWidth,
  80. // top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0,
  81. // left: event.clientX < pane.width - 200 ? event.clientX : 0,
  82. });
  83. },
  84. [sideWidth],
  85. );
  86. // Close the context menu if it's open whenever the window is clicked.
  87. const onPaneClick = useCallback(
  88. () => setMenu({} as INodeContextMenu),
  89. [setMenu],
  90. );
  91. return { onNodeContextMenu, menu, onPaneClick, ref };
  92. };