import { css } from '@emotion/css'; import { useCallback, useEffect, useState } from 'react'; import * as React from 'react'; import { first } from 'rxjs/operators'; import { SelectableValue } from '@grafana/data'; import { ContextMenu, MenuItem, MenuItemProps } from '@grafana/ui'; import { ElementState } from 'app/features/canvas/runtime/element'; import { FrameState } from 'app/features/canvas/runtime/frame'; import { Scene } from 'app/features/canvas/runtime/scene'; import { findElementByTarget } from 'app/features/canvas/runtime/sceneElementManagement'; import { CanvasPanel } from '../CanvasPanel'; import { AnchorPoint, LayerActionID } from '../types'; import { getElementTypes, onAddItem } from '../utils'; type Props = { scene: Scene; panel: CanvasPanel; onVisibilityChange: (v: boolean) => void; }; export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) => { const inlineEditorOpen = panel.state.openInlineEdit; const [isMenuVisible, setIsMenuVisible] = useState(false); const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 }); const styles = getStyles(); const selectedElements = scene.selecto?.getSelectedTargets(); const rootLayer: FrameState | undefined = panel.context?.instanceState?.layer; const handleContextMenu = useCallback( (event: Event) => { if (!(event instanceof MouseEvent) || event.ctrlKey) { return; } event.preventDefault(); panel.setActivePanel(); const shouldSelectElement = event.currentTarget !== scene.div; if ( shouldSelectElement && (event.currentTarget instanceof HTMLElement || event.currentTarget instanceof SVGElement) ) { scene.select({ targets: [event.currentTarget] }); } setAnchorPoint({ x: event.pageX, y: event.pageY }); setIsMenuVisible(true); onVisibilityChange(true); }, [scene, panel, onVisibilityChange] ); useEffect(() => { if (scene.selecto) { scene.selecto.getSelectableElements().forEach((element) => { element.addEventListener('contextmenu', handleContextMenu); }); } }, [handleContextMenu, scene.selecto]); useEffect(() => { if (scene.div) { scene.div.addEventListener('contextmenu', handleContextMenu); } }, [handleContextMenu, scene.div]); const closeContextMenu = () => { setIsMenuVisible(false); onVisibilityChange(false); }; const renderMenuItems = () => { // This is disabled when panel is in edit mode because opening inline editor over panel editor is not ideal UX const openCloseEditorMenuItem = !scene.isPanelEditing && ( { if (scene.inlineEditingCallback) { if (inlineEditorOpen) { panel.closeInlineEdit(); } else { scene.inlineEditingCallback(); } } closeContextMenu(); }} className={styles.menuItem} /> ); const editElementMenuItem = () => { if (selectedElements?.length === 1) { const onClickEditElementMenuItem = () => { scene.editModeEnabled.next(true); closeContextMenu(); }; const element = findElementByTarget(selectedElements[0], scene.root.elements); return ( element && element.item.hasEditMode && ( ) ); } return null; }; const typeOptions = getElementTypes(scene.shouldShowAdvancedTypes).options; const getTypeOptionsSubmenu = () => { const submenuItems: Array< React.ReactElement, string | React.JSXElementConstructor> > = []; const onClickItem = (option: SelectableValue) => { let offsetY = anchorPoint.y; let offsetX = anchorPoint.x; if (scene.div) { const transformScale = scene.scale; const sceneContainerDimensions = scene.div.getBoundingClientRect(); offsetY = (offsetY - sceneContainerDimensions.top) / transformScale; offsetX = (offsetX - sceneContainerDimensions.left) / transformScale; } onAddItem(option, rootLayer, { ...anchorPoint, y: offsetY, x: offsetX, }); }; typeOptions.map((option) => { submenuItems.push( onClickItem(option)} /> ); }); return submenuItems; }; const addItemMenuItem = ( ); const setBackgroundMenuItem = ( { if (scene.setBackgroundCallback) { scene.setBackgroundCallback(anchorPoint); } closeContextMenu(); }} className={styles.menuItem} /> ); if (selectedElements && selectedElements.length >= 1) { return ( <> {editElementMenuItem()} { contextMenuAction(LayerActionID.Delete); closeContextMenu(); }} className={styles.menuItem} /> { contextMenuAction(LayerActionID.Duplicate); closeContextMenu(); }} className={styles.menuItem} /> { contextMenuAction(LayerActionID.MoveTop); closeContextMenu(); }} className={styles.menuItem} /> { contextMenuAction(LayerActionID.MoveBottom); closeContextMenu(); }} className={styles.menuItem} /> {openCloseEditorMenuItem} ); } else { return ( <> {openCloseEditorMenuItem} {setBackgroundMenuItem} {addItemMenuItem} ); } }; const contextMenuAction = (actionType: LayerActionID) => { scene.selection.pipe(first()).subscribe((currentSelectedElements) => { const currentLayer = currentSelectedElements[0].parent!; currentSelectedElements.forEach((currentSelectedElement: ElementState) => { currentLayer.doAction(actionType, currentSelectedElement); }); }); setTimeout(() => { scene.addToSelection(); scene.targetsToSelect.clear(); }); }; if (isMenuVisible) { return (
{ event.preventDefault(); closeContextMenu(); }} >
); } return <>; }; const getStyles = () => ({ menuItem: css({ maxWidth: '200px', }), });