svg-layout-designer-react/src/Components/Menu/Menu.tsx
2023-02-14 15:00:18 +00:00

164 lines
4.5 KiB
TypeScript

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<string, IMenuAction[]>
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<React.SetStateAction<boolean>>,
setContextMenuPosition: React.Dispatch<React.SetStateAction<IPoint>>,
setTarget: React.Dispatch<React.SetStateAction<HTMLElement | undefined>>
): 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<IPoint>({
x: 0,
y: 0
});
const [target, setTarget] = React.useState<HTMLElement>();
const menuRef = React.useRef<HTMLDivElement>(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 (
<div
ref={menuRef}
className={`fixed context-menu ${MENU_VERTICAL_PADDING_CLASS} ${MENU_WIDTH_CLASS} ${props.className ?? ''} ${visible}`}
style={{
left: finalHorizontalPosition,
top: finalVerticalPosition
}}>
{ children }
</div>
);
}
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(<MenuItem
key={`contextmenu-item-${count}-${index}`}
className={'contextmenu-item'}
text={action.text}
title={action.title}
shortcut={action.shortcut}
onClick={() => action.action(target)} />);
});
children.push(<hr key={`contextmenu-hr-${count}`} className='border-slate-400' />);
}
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(<MenuItem
key={`contextmenu-item-${count}-${index}`}
className={'contextmenu-item'}
text={action.text}
title={action.title}
shortcut={action.shortcut}
onClick={() => action.action(target)} />);
});
}
return count;
}