Files
jank-client-fork/src/webpage/contextmenu.ts
2025-03-14 14:11:16 -05:00

252 lines
6.5 KiB
TypeScript

import {mobile} from "./utils/utils.js";
type iconJson =
| {
src: string;
}
| {
css: string;
}
| {
html: HTMLElement;
};
interface menuPart<x, y> {
makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void;
}
class ContextButton<x, y> implements menuPart<x, y> {
private text: string | (() => string);
private onClick: (this: x, arg: y, e: MouseEvent) => void;
private icon?: iconJson;
private visable?: (this: x, arg: y) => boolean;
private enabled?: (this: x, arg: y) => boolean;
//TODO there *will* be more colors
private color?: "red" | "blue";
constructor(
text: ContextButton<x, y>["text"],
onClick: ContextButton<x, y>["onClick"],
addProps: {
icon?: iconJson;
visable?: (this: x, arg: y) => boolean;
enabled?: (this: x, arg: y) => boolean;
color?: "red" | "blue";
} = {},
) {
this.text = text;
this.onClick = onClick;
this.icon = addProps.icon;
this.visable = addProps.visable;
this.enabled = addProps.enabled;
this.color = addProps.color;
}
isVisable(obj1: x, obj2: y): boolean {
if (!this.visable) return true;
return this.visable.call(obj1, obj2);
}
makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement) {
if (!this.isVisable(obj1, obj2)) {
return;
}
const intext = document.createElement("button");
intext.classList.add("contextbutton");
intext.append(this.textContent);
intext.disabled = !!this.enabled && !this.enabled.call(obj1, obj2);
if (this.icon) {
if ("src" in this.icon) {
const icon = document.createElement("img");
icon.classList.add("svgicon");
icon.src = this.icon.src;
intext.append(icon);
} else if ("css" in this.icon) {
const icon = document.createElement("span");
icon.classList.add(this.icon.css, "svgicon");
switch (this.color) {
case "red":
icon.style.background = "var(--red)";
break;
case "blue":
icon.style.background = "var(--blue)";
break;
}
intext.append(icon);
} else {
intext.append(this.icon.html);
}
}
switch (this.color) {
case "red":
intext.style.color = "var(--red)";
break;
case "blue":
intext.style.color = "var(--blue)";
break;
}
intext.onclick = (e) => {
e.preventDefault();
e.stopImmediatePropagation();
menu.remove();
this.onClick.call(obj1, obj2, e);
};
menu.append(intext);
}
get textContent() {
if (this.text instanceof Function) {
return this.text();
}
return this.text;
}
}
class Seperator<x, y> implements menuPart<x, y> {
private visable?: (obj1: x, obj2: y) => boolean;
constructor(visable?: (obj1: x, obj2: y) => boolean) {
this.visable = visable;
}
makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void {
if (!this.visable || this.visable(obj1, obj2)) {
if (menu.children[menu.children.length - 1].tagName === "HR") {
return;
}
menu.append(document.createElement("hr"));
}
}
}
class Contextmenu<x, y> {
static currentmenu: HTMLElement | "";
name: string;
buttons: menuPart<x, y>[];
div!: HTMLDivElement;
static setup() {
Contextmenu.currentmenu = "";
document.addEventListener("click", (event) => {
if (Contextmenu.currentmenu === "") {
return;
}
if (!Contextmenu.currentmenu.contains(event.target as Node)) {
Contextmenu.currentmenu.remove();
Contextmenu.currentmenu = "";
}
});
}
constructor(name: string) {
this.name = name;
this.buttons = [];
}
addButton(
text: ContextButton<x, y>["text"],
onClick: ContextButton<x, y>["onClick"],
addProps: {
icon?: iconJson;
visable?: (this: x, arg: y) => boolean;
enabled?: (this: x, arg: y) => boolean;
color?: "red" | "blue";
} = {},
) {
this.buttons.push(new ContextButton(text, onClick, addProps));
}
addSeperator(visable?: (obj1: x, obj2: y) => boolean) {
this.buttons.push(new Seperator(visable));
}
makemenu(x: number, y: number, addinfo: x, other: y) {
const div = document.createElement("div");
div.classList.add("contextmenu", "flexttb");
for (const button of this.buttons) {
button.makeContextHTML(addinfo, other, div);
}
if (div.children[div.children.length - 1].tagName === "HR") {
div.children[div.children.length - 1].remove();
}
//NOTE I don't know if this'll ever actually happen in reality
if (div.childNodes.length === 0) return;
if (Contextmenu.currentmenu !== "") {
Contextmenu.currentmenu.remove();
}
div.style.top = y + "px";
div.style.left = x + "px";
document.body.appendChild(div);
Contextmenu.keepOnScreen(div);
console.log(div);
Contextmenu.currentmenu = div;
return this.div;
}
bindContextmenu(
obj: HTMLElement,
addinfo: x,
other: y,
touchDrag: (x: number, y: number) => unknown = () => {},
touchEnd: (x: number, y: number) => unknown = () => {},
) {
const func = (event: MouseEvent) => {
event.preventDefault();
event.stopImmediatePropagation();
this.makemenu(event.clientX, event.clientY, addinfo, other);
};
obj.addEventListener("contextmenu", func);
//NOTE not sure if this code is correct, seems fine at least for now
if (mobile) {
let hold: NodeJS.Timeout | undefined;
let x!: number;
let y!: number;
obj.addEventListener(
"touchstart",
(event: TouchEvent) => {
x = event.touches[0].pageX;
y = event.touches[0].pageY;
if (event.touches.length > 1) {
event.preventDefault();
event.stopImmediatePropagation();
this.makemenu(event.touches[0].clientX, event.touches[0].clientY, addinfo, other);
} else {
//
event.stopImmediatePropagation();
hold = setTimeout(() => {
if (lastx ** 2 + lasty ** 2 > 10 ** 2) return;
this.makemenu(event.touches[0].clientX, event.touches[0].clientY, addinfo, other);
console.log(obj);
}, 500);
}
},
{passive: false},
);
let lastx = 0;
let lasty = 0;
obj.addEventListener("touchend", () => {
if (hold) {
clearTimeout(hold);
}
touchEnd(lastx, lasty);
});
obj.addEventListener("touchmove", (event) => {
lastx = event.touches[0].pageX - x;
lasty = event.touches[0].pageY - y;
touchDrag(lastx, lasty);
});
}
return func;
}
static keepOnScreen(obj: HTMLElement) {
const html = document.documentElement.getBoundingClientRect();
const docheight = html.height;
const docwidth = html.width;
const box = obj.getBoundingClientRect();
console.log(box, docheight, docwidth);
if (box.right > docwidth) {
console.log("test");
obj.style.left = docwidth - box.width + "px";
}
if (box.bottom > docheight) {
obj.style.top = docheight - box.height + "px";
}
}
}
Contextmenu.setup();
export {Contextmenu};