CanvasContextMenu.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import { css } from '@emotion/css';
  2. import React, { useCallback, useEffect, useState } from 'react';
  3. import { first } from 'rxjs/operators';
  4. import { ContextMenu, MenuItem } from '@grafana/ui';
  5. import { Scene } from '../../../features/canvas/runtime/scene';
  6. import { LayerActionID } from './types';
  7. type Props = {
  8. scene: Scene;
  9. };
  10. type AnchorPoint = {
  11. x: number;
  12. y: number;
  13. };
  14. export const CanvasContextMenu = ({ scene }: Props) => {
  15. const [isMenuVisible, setIsMenuVisible] = useState<boolean>(false);
  16. const [anchorPoint, setAnchorPoint] = useState<AnchorPoint>({ x: 0, y: 0 });
  17. const styles = getStyles();
  18. const selectedElements = scene.selecto?.getSelectedTargets();
  19. const handleContextMenu = useCallback(
  20. (event) => {
  21. event.preventDefault();
  22. if (event.currentTarget) {
  23. scene.select({ targets: [event.currentTarget as HTMLElement | SVGElement] });
  24. }
  25. setAnchorPoint({ x: event.pageX, y: event.pageY });
  26. setIsMenuVisible(true);
  27. },
  28. [scene]
  29. );
  30. useEffect(() => {
  31. if (selectedElements && selectedElements.length === 1) {
  32. const element = selectedElements[0];
  33. element.addEventListener('contextmenu', handleContextMenu);
  34. }
  35. }, [selectedElements, handleContextMenu]);
  36. if (!selectedElements) {
  37. return <></>;
  38. }
  39. const closeContextMenu = () => {
  40. setIsMenuVisible(false);
  41. };
  42. const renderMenuItems = () => {
  43. return (
  44. <>
  45. <MenuItem
  46. label="Delete"
  47. onClick={() => {
  48. contextMenuAction(LayerActionID.Delete);
  49. closeContextMenu();
  50. }}
  51. className={styles.menuItem}
  52. />
  53. <MenuItem
  54. label="Duplicate"
  55. onClick={() => {
  56. contextMenuAction(LayerActionID.Duplicate);
  57. closeContextMenu();
  58. }}
  59. className={styles.menuItem}
  60. />
  61. <MenuItem
  62. label="Bring to front"
  63. onClick={() => {
  64. contextMenuAction(LayerActionID.MoveTop);
  65. closeContextMenu();
  66. }}
  67. className={styles.menuItem}
  68. />
  69. <MenuItem
  70. label="Send to back"
  71. onClick={() => {
  72. contextMenuAction(LayerActionID.MoveBottom);
  73. closeContextMenu();
  74. }}
  75. className={styles.menuItem}
  76. />
  77. </>
  78. );
  79. };
  80. const contextMenuAction = (actionType: string) => {
  81. scene.selection.pipe(first()).subscribe((currentSelectedElements) => {
  82. const currentSelectedElement = currentSelectedElements[0];
  83. const currentLayer = currentSelectedElement.parent!;
  84. switch (actionType) {
  85. case LayerActionID.Delete:
  86. currentLayer.doAction(LayerActionID.Delete, currentSelectedElement);
  87. break;
  88. case LayerActionID.Duplicate:
  89. currentLayer.doAction(LayerActionID.Duplicate, currentSelectedElement);
  90. break;
  91. case LayerActionID.MoveTop:
  92. currentLayer.doAction(LayerActionID.MoveTop, currentSelectedElement);
  93. break;
  94. case LayerActionID.MoveBottom:
  95. currentLayer.doAction(LayerActionID.MoveBottom, currentSelectedElement);
  96. break;
  97. }
  98. });
  99. };
  100. if (isMenuVisible) {
  101. return (
  102. <div
  103. onContextMenu={(event) => {
  104. event.preventDefault();
  105. closeContextMenu();
  106. }}
  107. >
  108. <ContextMenu
  109. x={anchorPoint.x}
  110. y={anchorPoint.y}
  111. onClose={closeContextMenu}
  112. renderMenuItems={renderMenuItems}
  113. focusOnOpen={false}
  114. />
  115. </div>
  116. );
  117. }
  118. return <></>;
  119. };
  120. const getStyles = () => ({
  121. menuItem: css`
  122. max-width: 60ch;
  123. overflow: hidden;
  124. `,
  125. });