excalidraw/src/components/ContextMenu.tsx

148 lines
4.2 KiB
TypeScript
Raw Normal View History

2020-01-07 07:50:59 +05:00
import { render, unmountComponentAtNode } from "react-dom";
import clsx from "clsx";
import { Popover } from "./Popover";
import { t } from "../i18n";
2020-01-07 07:50:59 +05:00
2020-04-10 18:09:29 -04:00
import "./ContextMenu.scss";
import {
getShortcutFromShortcutName,
ShortcutName,
} from "../actions/shortcuts";
import { Action } from "../actions/types";
import { ActionManager } from "../actions/manager";
import { AppState } from "../types";
import { NonDeletedExcalidrawElement } from "../element/types";
2020-01-07 07:50:59 +05:00
export type ContextMenuOption = "separator" | Action;
2020-01-07 07:50:59 +05:00
type ContextMenuProps = {
2020-01-07 07:50:59 +05:00
options: ContextMenuOption[];
onCloseRequest?(): void;
top: number;
left: number;
actionManager: ActionManager;
appState: Readonly<AppState>;
elements: readonly NonDeletedExcalidrawElement[];
2020-01-07 07:50:59 +05:00
};
const ContextMenu = ({
options,
onCloseRequest,
top,
left,
actionManager,
appState,
elements,
}: ContextMenuProps) => {
return (
2021-04-05 17:26:37 +02:00
<Popover
onCloseRequest={onCloseRequest}
top={top}
left={left}
fitInViewport={true}
offsetLeft={appState.offsetLeft}
offsetTop={appState.offsetTop}
viewportWidth={appState.width}
viewportHeight={appState.height}
>
2021-04-05 17:26:37 +02:00
<ul
className="context-menu"
onContextMenu={(event) => event.preventDefault()}
>
2021-04-05 17:26:37 +02:00
{options.map((option, idx) => {
if (option === "separator") {
return <hr key={idx} className="context-menu-option-separator" />;
}
2021-04-05 17:26:37 +02:00
const actionName = option.name;
let label = "";
if (option.contextItemLabel) {
if (typeof option.contextItemLabel === "function") {
label = t(option.contextItemLabel(elements, appState));
} else {
label = t(option.contextItemLabel);
}
}
2021-04-05 17:26:37 +02:00
return (
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
<button
className={clsx("context-menu-option", {
dangerous: actionName === "deleteSelectedElements",
checkmark: option.checked?.(appState),
})}
onClick={() => actionManager.executeAction(option)}
>
<div className="context-menu-option__label">{label}</div>
<kbd className="context-menu-option__shortcut">
{actionName
? getShortcutFromShortcutName(actionName as ShortcutName)
: ""}
</kbd>
</button>
</li>
);
})}
</ul>
</Popover>
);
};
2020-01-07 07:50:59 +05:00
2021-04-05 17:26:37 +02:00
const contextMenuNodeByContainer = new WeakMap<HTMLElement, HTMLDivElement>();
const getContextMenuNode = (container: HTMLElement): HTMLDivElement => {
let contextMenuNode = contextMenuNodeByContainer.get(container);
2020-01-07 07:50:59 +05:00
if (contextMenuNode) {
return contextMenuNode;
}
2021-04-05 17:26:37 +02:00
contextMenuNode = document.createElement("div");
container
.querySelector(".excalidraw-contextMenuContainer")!
.appendChild(contextMenuNode);
contextMenuNodeByContainer.set(container, contextMenuNode);
return contextMenuNode;
};
2020-01-07 07:50:59 +05:00
type ContextMenuParams = {
options: (ContextMenuOption | false | null | undefined)[];
top: ContextMenuProps["top"];
left: ContextMenuProps["left"];
actionManager: ContextMenuProps["actionManager"];
appState: Readonly<AppState>;
2021-04-05 17:26:37 +02:00
container: HTMLElement;
elements: readonly NonDeletedExcalidrawElement[];
2020-01-07 07:50:59 +05:00
};
2021-04-05 17:26:37 +02:00
const handleClose = (container: HTMLElement) => {
const contextMenuNode = contextMenuNodeByContainer.get(container);
if (contextMenuNode) {
unmountComponentAtNode(contextMenuNode);
contextMenuNode.remove();
contextMenuNodeByContainer.delete(container);
}
};
2020-01-07 07:50:59 +05:00
export default {
push(params: ContextMenuParams) {
const options = Array.of<ContextMenuOption>();
params.options.forEach((option) => {
2020-01-07 07:50:59 +05:00
if (option) {
options.push(option);
}
});
if (options.length) {
render(
<ContextMenu
top={params.top}
left={params.left}
options={options}
2021-04-05 17:26:37 +02:00
onCloseRequest={() => handleClose(params.container)}
actionManager={params.actionManager}
appState={params.appState}
elements={params.elements}
2020-01-07 07:50:59 +05:00
/>,
2021-04-05 17:26:37 +02:00
getContextMenuNode(params.container),
2020-01-07 07:50:59 +05:00
);
}
2020-01-24 12:04:54 +02:00
},
2020-01-07 07:50:59 +05:00
};