193 lines
6.8 KiB
TypeScript
193 lines
6.8 KiB
TypeScript
import * as loader from "tc-loader";
|
|
import {Stage} from "tc-loader";
|
|
import {
|
|
closeContextMenu,
|
|
ContextMenuEntry,
|
|
ContextMenuEntryNormal,
|
|
ContextMenuFactory, MenuEntryLabel,
|
|
} 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 = () => {
|
|
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 {
|
|
spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[], closeCallback: () => void) {
|
|
entries.forEach(generateUniqueIds);
|
|
refRenderer.current?.setState({
|
|
entries: entries,
|
|
pageX: position.pageX,
|
|
pageY: position.pageY,
|
|
callbackClose: closeCallback
|
|
});
|
|
}
|
|
|
|
closeContextMenu() {
|
|
if(refRenderer.current?.state.entries?.length) {
|
|
const callback = refRenderer.current?.state.callbackClose;
|
|
if(callback) { callback(); }
|
|
|
|
refRenderer.current?.setState({ callbackClose: undefined, entries: [] });
|
|
}
|
|
}
|
|
};
|
|
}
|
|
})
|