This commit is contained in:
rosetta
2026-01-30 05:01:05 +02:00
commit 83f38dc63f
327 changed files with 18725 additions and 0 deletions

View 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>
</>
);
}

View File

@@ -0,0 +1,10 @@
import { useContext } from "react";
import { ContextMenuContext } from "./ContextMenuProvider";
export function useContextMenu() {
const context = useContext(ContextMenuContext);
if (!context) {
throw new Error('useContextMenu must be used within a ContextMenuProvider');
}
return context.openContextMenu;
}