TeaWeb/web/app/ui/context-menu/ReactRenderer.tsx

194 lines
6.8 KiB
TypeScript
Raw Normal View History

import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {
closeContextMenu,
ContextMenuEntry,
ContextMenuEntryNormal,
ContextMenuFactory, MenuEntryLabel,
2020-09-30 20:28:30 +02:00
} from "tc-shared/ui/ContextMenu";
import * as React from "react";
import * as ReactDOM from "react-dom";
import {IconRenderer, RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {useContext} from "react";
const cssStyle = require("./ReactRenderer.scss");
const CloseCallback = React.createContext<() => void>(undefined);
let globalMouseListener;
let globalContainer: HTMLDivElement;
let refRenderer = React.createRef<ContextMenuRenderer>();
const MenuEntryIconRenderer = (props: { entry: ContextMenuEntryNormal }) => {
if(!props.entry.icon || typeof props.entry.icon === "string") {
return <IconRenderer icon={props.entry.icon as any} className={cssStyle.icon} />;
} else {
return <RemoteIconRenderer icon={props.entry.icon} className={cssStyle.icon} />;
}
};
const MenuLabelRenderer = (props: { label: MenuEntryLabel }) => {
let text;
let classes = [];
if(typeof props.label === "string") {
text = props.label;
} else {
text = props.label.text;
if(props.label.bold) {
classes.push(cssStyle.bold);
}
}
classes.push(cssStyle.label);
return <div className={classes.join(" ")}>{text}</div>;
}
const MenuEntryRenderer = (props: { entry: ContextMenuEntry }) => {
const closeCallback = useContext(CloseCallback);
const clickListener = () => {
2020-09-29 15:02:00 +02:00
try {
if("click" in props.entry && typeof props.entry.click === "function") {
props.entry.click();
}
} finally {
closeCallback();
}
};
if(typeof props.entry.visible === "boolean" && !props.entry.visible) { return null; }
switch (props.entry.type) {
case "separator":
return <hr key={"hr"} />;
case "checkbox":
return (
<div
className={cssStyle.entry + " " + (typeof props.entry.enabled === "boolean" && !props.entry.enabled ? cssStyle.disabled : "")}
onClick={clickListener}
>
<label className={cssStyle.checkbox}>
<input type={"checkbox"} checked={props.entry.checked || false} readOnly={true} />
<span className={cssStyle.checkmark} />
</label>
<MenuLabelRenderer label={props.entry.label} />
</div>
);
case "normal":
return (
<div
className={cssStyle.entry + " " + (props.entry.subMenu?.length ? cssStyle.subContainer : "") + " " + (typeof props.entry.enabled === "boolean" && !props.entry.enabled ? cssStyle.disabled : "")}
onClick={clickListener}
>
<MenuEntryIconRenderer entry={props.entry} />
<MenuLabelRenderer label={props.entry.label} />
{!props.entry.subMenu?.length ? undefined :
<React.Fragment>
<div className={cssStyle.arrow} />
<MenuRenderer entries={props.entry.subMenu} subMenu={true} />
</React.Fragment>
}
</div>
);
}
return null;
}
const MenuRenderer = (props: { entries: ContextMenuEntry[], subMenu: boolean }) => {
return (
<div className={cssStyle.menuContainer + " " + (props.subMenu ? cssStyle.subMenu : "")}>
{props.entries.map(entry => <MenuEntryRenderer entry={entry} key={entry.uniqueId} />)}
</div>
)
};
class ContextMenuRenderer extends React.Component<{}, { entries: ContextMenuEntry[], pageX: number, pageY: number, callbackClose: () => void }> {
constructor(props) {
super(props);
this.state = {
pageY: 0,
pageX: 0,
entries: [],
callbackClose: () => {}
}
}
render() {
return (
<CloseCallback.Provider value={() => {
if(this.state.callbackClose) {
this.state.callbackClose();
}
this.setState({ entries: [], callbackClose: undefined });
}}>
<div
className={cssStyle.container + " " + (this.state.entries.length ? cssStyle.shown : "")}
style={{ top: this.state.pageY, left: this.state.pageX }}
>
<MenuRenderer entries={this.state.entries} subMenu={false} />
</div>
</CloseCallback.Provider>
)
}
}
let uniqueIdIndex = 0;
function generateUniqueIds(entry: ContextMenuEntry) {
if(typeof entry.uniqueId !== "string") {
entry.uniqueId = "_" + (++uniqueIdIndex);
}
if(entry.type === "normal" && entry.subMenu) {
entry.subMenu.forEach(generateUniqueIds);
}
}
export let reactContextMenuInstance: ContextMenuFactory;
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
priority: 80,
name: "context menu init",
function: async () => {
document.addEventListener("mousedown", globalMouseListener = event => {
if(refRenderer.current?.state.entries?.length) {
let target: HTMLElement = event.target as any;
while (target) {
if(target.classList.contains(cssStyle.container)) {
return;
}
target = target.parentElement;
}
closeContextMenu();
}
});
globalContainer = document.createElement("div");
globalContainer.classList.add(cssStyle.globalContainer);
document.body.append(globalContainer);
ReactDOM.render(<ContextMenuRenderer ref={refRenderer} />, globalContainer);
reactContextMenuInstance = new class implements ContextMenuFactory {
2020-09-28 09:37:48 +02:00
spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[], closeCallback: () => void) {
entries.forEach(generateUniqueIds);
refRenderer.current?.setState({
entries: entries,
pageX: position.pageX,
2020-09-28 09:37:48 +02:00
pageY: position.pageY,
callbackClose: closeCallback
});
}
closeContextMenu() {
if(refRenderer.current?.state.entries?.length) {
2020-09-28 09:37:48 +02:00
const callback = refRenderer.current?.state.callbackClose;
if(callback) { callback(); }
refRenderer.current?.setState({ callbackClose: undefined, entries: [] });
}
}
};
}
})