164 lines
6.1 KiB
TypeScript
164 lines
6.1 KiB
TypeScript
import { Box, Menu } from "@mantine/core";
|
|
import { createContext, useEffect, useState } from "react";
|
|
|
|
interface ContextMenuProviderContextType {
|
|
openContextMenu: (
|
|
items : ContextMenuItem[],
|
|
noRenderStandardItems?: boolean, noRenderDisabledItems?: boolean) => void;
|
|
}
|
|
|
|
export const ContextMenuContext = createContext<ContextMenuProviderContextType|null>(null);
|
|
|
|
interface ContextMenuProviderProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export interface ContextMenuItem {
|
|
label: string;
|
|
action: () => void;
|
|
icon: React.ReactNode;
|
|
cond?: () => boolean | Promise<boolean>;
|
|
__reserved_prerender_condition?: boolean;
|
|
}
|
|
|
|
const standardMenuItems: ContextMenuItem[] = [];
|
|
|
|
const animationDelay = 40;
|
|
|
|
export function ContextMenuProvider(props : ContextMenuProviderProps) {
|
|
const [coords, setCoords] = useState({
|
|
x: 0,
|
|
y: 0
|
|
});
|
|
const [open, setOpen] = useState<boolean>(false);
|
|
const [items, setItems] = useState<ContextMenuItem[]>([]);
|
|
const [noRenderStandardItems, setNoRenderStandardItems] = useState<boolean>(false);
|
|
const [standardItemsReady, setStandardItemsReady] = useState<ContextMenuItem[]>([]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
document.removeEventListener('contextmenu', contextMenuHandler);
|
|
document.removeEventListener('click', clickHandler);
|
|
document.addEventListener('contextmenu',contextMenuHandler);
|
|
document.addEventListener('click', clickHandler);
|
|
return () => {
|
|
document.removeEventListener('contextmenu', contextMenuHandler);
|
|
document.removeEventListener('click', clickHandler);
|
|
}
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
setStandardItemsReady(await translateConditionsToReservedField(standardMenuItems));
|
|
})();
|
|
}, [open]);
|
|
|
|
const contextMenuHandler = (event) => {
|
|
event.preventDefault();
|
|
setOpen(true);
|
|
setCoords({
|
|
x: event.clientX,
|
|
y: event.clientY
|
|
});
|
|
}
|
|
|
|
const clickHandler = () => {
|
|
if(open){
|
|
setOpen(false);
|
|
setTimeout(() => {
|
|
/**
|
|
* Ждем завершения анимации
|
|
*/
|
|
setItems([]);
|
|
}, animationDelay);
|
|
}
|
|
}
|
|
|
|
const translateConditionsToReservedField = async (fromItems : ContextMenuItem[], noRenderDisabledItems: boolean = false) : Promise<ContextMenuItem[]> => {
|
|
const newItems: ContextMenuItem[] = [];
|
|
for(const item of fromItems){
|
|
if(!item.cond){
|
|
newItems.push({
|
|
...item,
|
|
__reserved_prerender_condition: true
|
|
});
|
|
continue;
|
|
}
|
|
const condResult = await item.cond();
|
|
if(!condResult && noRenderDisabledItems){
|
|
continue;
|
|
}
|
|
newItems.push({
|
|
...item,
|
|
__reserved_prerender_condition: condResult
|
|
});
|
|
}
|
|
return newItems;
|
|
}
|
|
|
|
const openContextMenu = async (
|
|
items : ContextMenuItem[],
|
|
noRenderStandardItems: boolean = false,
|
|
noRenderDisabledItems: boolean = false
|
|
) => {
|
|
setItems(await translateConditionsToReservedField(items, noRenderDisabledItems));
|
|
setNoRenderStandardItems(noRenderStandardItems);
|
|
setOpen(true);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<ContextMenuContext.Provider value={{
|
|
openContextMenu
|
|
}}>
|
|
{standardItemsReady.length > 0 || items.length > 0 && (
|
|
<Box style={{
|
|
position: 'absolute',
|
|
top: coords.y > window.innerHeight - (items.concat(standardMenuItems).length * 45) ? (window.innerHeight - (items.concat(standardMenuItems).length * 45)) + 'px' : coords.y + 'px',
|
|
left: coords.x > window.innerWidth - 210 ? (window.innerWidth - 210) + 'px' : coords.x + 'px',
|
|
}}>
|
|
<Menu
|
|
trapFocus={false}
|
|
shadow="md"
|
|
opened={open}
|
|
transitionProps={{
|
|
duration: animationDelay,
|
|
transition: 'pop-top-left',
|
|
}}
|
|
closeDelay={0}
|
|
styles={{
|
|
dropdown: {
|
|
position: 'absolute',
|
|
top: coords.y > window.innerHeight - (items.concat(standardMenuItems).length * 45) ? (window.innerHeight - (items.concat(standardMenuItems).length * 45)) + 'px' : coords.y + 'px',
|
|
left: coords.x > window.innerWidth - 210 ? (window.innerWidth - 210) + 'px' : coords.x + 'px',
|
|
}
|
|
}}
|
|
width={150}>
|
|
<Menu.Dropdown>
|
|
{items.map((item, index) => (
|
|
<Menu.Item fz={'xs'} fw={500} key={index} disabled={!item.__reserved_prerender_condition} leftSection={item.icon} onClick={() => {
|
|
item.action();
|
|
setOpen(false);
|
|
}}>
|
|
{item.label}
|
|
</Menu.Item>
|
|
))}
|
|
{items.length > 0 && !noRenderStandardItems && standardMenuItems.length > 0 && <Menu.Divider></Menu.Divider>}
|
|
{!noRenderStandardItems && standardItemsReady.map((item, index) => (
|
|
<Menu.Item fz={'xs'} fw={500} key={index} disabled={!item.__reserved_prerender_condition} leftSection={item.icon} onClick={() => {
|
|
item.action();
|
|
setOpen(false);
|
|
}}>
|
|
{item.label}
|
|
</Menu.Item>
|
|
))}
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
</Box>
|
|
)}
|
|
{props.children}
|
|
</ContextMenuContext.Provider>
|
|
</>
|
|
);
|
|
} |