'init'
This commit is contained in:
164
app/providers/ContextMenuProvider/ContextMenuProvider.tsx
Normal file
164
app/providers/ContextMenuProvider/ContextMenuProvider.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user