2020-01-07 07:50:59 +05:00
|
|
|
import { render, unmountComponentAtNode } from "react-dom";
|
2020-10-19 17:14:28 +03:00
|
|
|
import clsx from "clsx";
|
|
|
|
import { Popover } from "./Popover";
|
2021-01-28 00:41:17 +05:30
|
|
|
import { t } from "../i18n";
|
2020-01-07 07:50:59 +05:00
|
|
|
|
2020-04-10 18:09:29 -04:00
|
|
|
import "./ContextMenu.scss";
|
2020-12-12 23:03:58 +01:00
|
|
|
import {
|
|
|
|
getShortcutFromShortcutName,
|
|
|
|
ShortcutName,
|
|
|
|
} from "../actions/shortcuts";
|
2021-01-28 00:41:17 +05:30
|
|
|
import { Action } from "../actions/types";
|
|
|
|
import { ActionManager } from "../actions/manager";
|
2021-01-29 23:38:37 +05:30
|
|
|
import { AppState } from "../types";
|
2020-01-07 07:50:59 +05:00
|
|
|
|
2021-02-02 02:26:42 +05:30
|
|
|
export type ContextMenuOption = "separator" | Action;
|
2020-01-07 07:50:59 +05:00
|
|
|
|
2021-01-28 00:41:17 +05:30
|
|
|
type ContextMenuProps = {
|
2020-01-07 07:50:59 +05:00
|
|
|
options: ContextMenuOption[];
|
|
|
|
onCloseRequest?(): void;
|
|
|
|
top: number;
|
|
|
|
left: number;
|
2021-01-28 00:41:17 +05:30
|
|
|
actionManager: ActionManager;
|
2021-01-29 23:38:37 +05:30
|
|
|
appState: Readonly<AppState>;
|
2020-01-07 07:50:59 +05:00
|
|
|
};
|
|
|
|
|
2021-01-28 00:41:17 +05:30
|
|
|
const ContextMenu = ({
|
|
|
|
options,
|
|
|
|
onCloseRequest,
|
|
|
|
top,
|
|
|
|
left,
|
|
|
|
actionManager,
|
2021-01-29 23:38:37 +05:30
|
|
|
appState,
|
2021-01-28 00:41:17 +05:30
|
|
|
}: ContextMenuProps) => {
|
2020-09-26 02:48:45 +05:30
|
|
|
return (
|
2021-04-05 17:26:37 +02:00
|
|
|
<Popover
|
|
|
|
onCloseRequest={onCloseRequest}
|
|
|
|
top={top}
|
|
|
|
left={left}
|
|
|
|
fitInViewport={true}
|
2022-01-23 17:28:38 +01:00
|
|
|
offsetLeft={appState.offsetLeft}
|
|
|
|
offsetTop={appState.offsetTop}
|
|
|
|
viewportWidth={appState.width}
|
|
|
|
viewportHeight={appState.height}
|
2020-10-19 17:14:28 +03:00
|
|
|
>
|
2021-04-05 17:26:37 +02:00
|
|
|
<ul
|
|
|
|
className="context-menu"
|
|
|
|
onContextMenu={(event) => event.preventDefault()}
|
2020-09-26 02:48:45 +05:30
|
|
|
>
|
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-01-28 00:41:17 +05:30
|
|
|
|
2021-04-05 17:26:37 +02:00
|
|
|
const actionName = option.name;
|
|
|
|
const label = option.contextItemLabel
|
|
|
|
? t(option.contextItemLabel)
|
|
|
|
: "";
|
|
|
|
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-09-26 02:48:45 +05:30
|
|
|
);
|
|
|
|
};
|
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-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-07 07:50:59 +05:00
|
|
|
|
|
|
|
type ContextMenuParams = {
|
|
|
|
options: (ContextMenuOption | false | null | undefined)[];
|
2021-01-28 00:41:17 +05:30
|
|
|
top: ContextMenuProps["top"];
|
|
|
|
left: ContextMenuProps["left"];
|
|
|
|
actionManager: ContextMenuProps["actionManager"];
|
2021-01-29 23:38:37 +05:30
|
|
|
appState: Readonly<AppState>;
|
2021-04-05 17:26:37 +02:00
|
|
|
container: HTMLElement;
|
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-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-07 07:50:59 +05:00
|
|
|
|
|
|
|
export default {
|
|
|
|
push(params: ContextMenuParams) {
|
|
|
|
const options = Array.of<ContextMenuOption>();
|
2020-03-23 13:05:07 +02:00
|
|
|
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)}
|
2021-01-28 00:41:17 +05:30
|
|
|
actionManager={params.actionManager}
|
2021-01-29 23:38:37 +05:30
|
|
|
appState={params.appState}
|
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
|
|
|
};
|