import useSize from '@react-hook/size'; import * as React from 'react'; import { IPoint } from '../../Interfaces/IPoint'; import { MenuItem } from './MenuItem'; interface IMenuProps { getListener: () => HTMLElement | null actions: Map className?: string } export interface IMenuAction { /** displayed */ text: string /** title to show on hover */ title?: string /** description of the shortcut */ shortcut?: string /** function to be called on button click */ action: (target: HTMLElement) => void } function UseMouseEvents( getListener: () => HTMLElement | null, setIsOpen: React.Dispatch>, setContextMenuPosition: React.Dispatch>, setTarget: React.Dispatch> ): void { React.useEffect(() => { function OnContextMenu(event: MouseEvent): void { event.preventDefault(); const contextMenuPosition: IPoint = { x: event.pageX, y: event.pageY }; setIsOpen(true); setContextMenuPosition(contextMenuPosition); setTarget(event.target as HTMLElement); // this is wrong since target can be null and not undefined } function OnLeftClick(): void { setIsOpen(false); } getListener()?.addEventListener( 'contextmenu', OnContextMenu ); window.addEventListener( 'click', OnLeftClick ); return () => { getListener()?.removeEventListener( 'contextmenu', OnContextMenu ); window.removeEventListener( 'click', OnLeftClick ); }; }); } const MENU_WIDTH_CLASS = 'w-52'; const MENU_VERTICAL_PADDING_CLASS = 'py-1'; export function Menu(props: IMenuProps): JSX.Element { const [isOpen, setIsOpen] = React.useState(false); const [contextMenuPosition, setContextMenuPosition] = React.useState({ x: 0, y: 0 }); const [target, setTarget] = React.useState(); const menuRef = React.useRef(null); const [menuWidth, menuHeight] = useSize(menuRef); UseMouseEvents( props.getListener, setIsOpen, setContextMenuPosition, setTarget ); const children: JSX.Element[] = []; if (target !== undefined) { let count = 0; count = AddClassSpecificActions(target, count, props, children); // Add universal actions AddUniversalActions(props, children, count, target); } const visible = isOpen && children.length > 0 ? 'visible opacity-1' : 'invisible opacity-0'; const isOutOfBoundHorizontally = contextMenuPosition.x + menuWidth > window.innerWidth; const isOutOfBoundVertically = contextMenuPosition.y + menuHeight > window.innerHeight; const finalHorizontalPosition = isOutOfBoundHorizontally ? contextMenuPosition.x - menuWidth : contextMenuPosition.x; const finalVerticalPosition = isOutOfBoundVertically ? contextMenuPosition.y - menuWidth : contextMenuPosition.y; return (
{ children }
); } function AddClassSpecificActions( target: HTMLElement, count: number, props: IMenuProps, children: JSX.Element[] ): number { for (const className of target.classList) { count++; const actions = props.actions.get(className); // Only select action where classname matches if (actions === undefined) { continue; } actions.forEach((action, index) => { children.push( action.action(target)} />); }); children.push(
); } return count; } function AddUniversalActions(props: IMenuProps, children: JSX.Element[], count: number, target: HTMLElement): number { const actions = props.actions.get(''); if (actions !== undefined) { count++; actions.forEach((action, index) => { children.push( action.action(target)} />); }); } return count; }