129 lines
3.2 KiB
JavaScript
129 lines
3.2 KiB
JavaScript
|
|
import React, {
|
||
|
|
useContext,
|
||
|
|
useEffect,
|
||
|
|
useLayoutEffect,
|
||
|
|
useRef,
|
||
|
|
useState,
|
||
|
|
} from 'react';
|
||
|
|
import {createPortal} from 'react-dom';
|
||
|
|
import {RegistryContext} from './Contexts';
|
||
|
|
|
||
|
|
import styles from './ContextMenu.css';
|
||
|
|
|
||
|
|
function respositionToFit(element, pageX, pageY) {
|
||
|
|
const ownerWindow = element.ownerDocument.defaultView;
|
||
|
|
if (element !== null) {
|
||
|
|
if (pageY + element.offsetHeight >= ownerWindow.innerHeight) {
|
||
|
|
if (pageY - element.offsetHeight > 0) {
|
||
|
|
element.style.top = `${pageY - element.offsetHeight}px`;
|
||
|
|
} else {
|
||
|
|
element.style.top = '0px';
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
element.style.top = `${pageY}px`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (pageX + element.offsetWidth >= ownerWindow.innerWidth) {
|
||
|
|
if (pageX - element.offsetWidth > 0) {
|
||
|
|
element.style.left = `${pageX - element.offsetWidth}px`;
|
||
|
|
} else {
|
||
|
|
element.style.left = '0px';
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
element.style.left = `${pageX}px`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const HIDDEN_STATE = {
|
||
|
|
data: null,
|
||
|
|
isVisible: false,
|
||
|
|
pageX: 0,
|
||
|
|
pageY: 0,
|
||
|
|
};
|
||
|
|
|
||
|
|
type Props = {|
|
||
|
|
children: React$Node,
|
||
|
|
id: string,
|
||
|
|
|};
|
||
|
|
|
||
|
|
export default function ContextMenu({children, id}: Props) {
|
||
|
|
const {registerMenu} = useContext(RegistryContext);
|
||
|
|
|
||
|
|
const [state, setState] = useState(HIDDEN_STATE);
|
||
|
|
|
||
|
|
const bodyAccessorRef = useRef(null);
|
||
|
|
const containerRef = useRef(null);
|
||
|
|
const menuRef = useRef(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const ownerDocument = bodyAccessorRef.current.ownerDocument;
|
||
|
|
containerRef.current = ownerDocument.createElement('div');
|
||
|
|
ownerDocument.body.appendChild(containerRef.current);
|
||
|
|
return () => {
|
||
|
|
ownerDocument.body.removeChild(containerRef.current);
|
||
|
|
};
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(
|
||
|
|
() => {
|
||
|
|
const showMenu = ({data, pageX, pageY}) => {
|
||
|
|
setState({data, isVisible: true, pageX, pageY});
|
||
|
|
};
|
||
|
|
const hideMenu = () => setState(HIDDEN_STATE);
|
||
|
|
return registerMenu(id, showMenu, hideMenu);
|
||
|
|
},
|
||
|
|
[id],
|
||
|
|
);
|
||
|
|
|
||
|
|
useLayoutEffect(
|
||
|
|
() => {
|
||
|
|
if (!state.isVisible) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const menu = menuRef.current;
|
||
|
|
|
||
|
|
const hideUnlessContains = event => {
|
||
|
|
if (!menu.contains(event.target)) {
|
||
|
|
setState(HIDDEN_STATE);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const hide = event => {
|
||
|
|
setState(HIDDEN_STATE);
|
||
|
|
};
|
||
|
|
|
||
|
|
const ownerDocument = containerRef.current.ownerDocument;
|
||
|
|
ownerDocument.addEventListener('mousedown', hideUnlessContains);
|
||
|
|
ownerDocument.addEventListener('touchstart', hideUnlessContains);
|
||
|
|
ownerDocument.addEventListener('keydown', hideUnlessContains);
|
||
|
|
|
||
|
|
const ownerWindow = ownerDocument.defaultView;
|
||
|
|
ownerWindow.addEventListener('resize', hide);
|
||
|
|
|
||
|
|
respositionToFit(menu, state.pageX, state.pageY);
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
ownerDocument.removeEventListener('mousedown', hideUnlessContains);
|
||
|
|
ownerDocument.removeEventListener('touchstart', hideUnlessContains);
|
||
|
|
ownerDocument.removeEventListener('keydown', hideUnlessContains);
|
||
|
|
|
||
|
|
ownerWindow.removeEventListener('resize', hide);
|
||
|
|
};
|
||
|
|
},
|
||
|
|
[state],
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!state.isVisible) {
|
||
|
|
return <div ref={bodyAccessorRef} />;
|
||
|
|
} else {
|
||
|
|
return createPortal(
|
||
|
|
<div ref={menuRef} className={styles.ContextMenu}>
|
||
|
|
{children(state.data)}
|
||
|
|
</div>,
|
||
|
|
containerRef.current,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|