stacked context menus and more group improvements

This commit is contained in:
MathMan05
2025-11-20 16:23:38 -06:00
parent 56160ed928
commit e72ec01314
3 changed files with 146 additions and 16 deletions
+115 -11
View File
@@ -11,7 +11,14 @@ type iconJson =
};
interface menuPart<x, y> {
makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void;
group?: string;
makeContextHTML(
obj1: x,
obj2: y,
menu: HTMLDivElement,
layered: contextCluster<unknown, unknown>[],
processed: WeakSet<menuPart<unknown, unknown>>,
): void;
}
class ContextButton<x, y> implements menuPart<x, y> {
@@ -22,6 +29,7 @@ class ContextButton<x, y> implements menuPart<x, y> {
private enabled?: (this: x, arg: y) => boolean;
//TODO there *will* be more colors
private color?: "red" | "blue";
group?: string;
constructor(
text: ContextButton<x, y>["text"],
onClick: ContextButton<x, y>["onClick"],
@@ -30,6 +38,7 @@ class ContextButton<x, y> implements menuPart<x, y> {
visable?: (this: x, arg: y) => boolean;
enabled?: (this: x, arg: y) => boolean;
color?: "red" | "blue";
group?: string;
} = {},
) {
this.text = text;
@@ -38,6 +47,7 @@ class ContextButton<x, y> implements menuPart<x, y> {
this.visable = addProps.visable;
this.enabled = addProps.enabled;
this.color = addProps.color;
this.group = addProps.group;
}
isVisable(obj1: x, obj2: y): boolean {
if (!this.visable) return true;
@@ -102,10 +112,50 @@ class ContextButton<x, y> implements menuPart<x, y> {
return this.text;
}
}
class ContextGroup<x, y> implements menuPart<x, y> {
private visable?: (this: x, arg: y) => boolean;
groupSel: string;
group = undefined;
constructor(
group: string,
addProps: {
visable?: (this: x, arg: y) => boolean;
} = {},
) {
this.visable = addProps.visable;
this.groupSel = group;
}
isVisable(obj1: x, obj2: y): boolean {
if (!this.visable) return true;
return this.visable.call(obj1, obj2);
}
makeContextHTML(
x: x,
y: y,
menuHtml: HTMLDivElement,
layered: contextCluster<unknown, unknown>[],
processed: WeakSet<menuPart<unknown, unknown>>,
) {
if (!this.isVisable(x, y)) {
return;
}
for (const [menu, x, y] of layered) {
for (const part of menu.buttons) {
if (part.group === this.groupSel && !processed.has(part)) {
processed.add(part);
part.makeContextHTML(x, y, menuHtml, [], processed);
}
}
}
}
}
class Seperator<x, y> implements menuPart<x, y> {
private visable?: (obj1: x, obj2: y) => boolean;
constructor(visable?: (obj1: x, obj2: y) => boolean) {
group?: string;
constructor(visable?: (obj1: x, obj2: y) => boolean, group?: string) {
this.visable = visable;
this.group = group;
}
makeContextHTML(obj1: x, obj2: y, menu: HTMLDivElement): void {
if (!this.visable || this.visable(obj1, obj2)) {
@@ -116,6 +166,28 @@ class Seperator<x, y> implements menuPart<x, y> {
}
}
}
declare global {
interface HTMLElementEventMap {
layered: LayeredEvent;
}
}
type contextCluster<X, Y> = [Contextmenu<X, Y>, X, Y];
class LayeredEvent extends CustomEvent<unknown> {
menus: contextCluster<unknown, unknown>[];
primary?: contextCluster<unknown, unknown>;
constructor(mouse: MouseEvent, menus: LayeredEvent["menus"]) {
super("layered", {bubbles: true});
this.menus = menus;
queueMicrotask(() => {
console.log(this);
const pop = this.primary || menus.pop();
if (!pop) return;
const [menu, addinfo, other] = pop;
menu.makemenu(mouse.clientX, mouse.clientY, addinfo, other, undefined, menus);
});
}
}
class Contextmenu<x, y> {
static currentmenu: HTMLElement | "" = "";
static prevmenus: HTMLElement[] = [];
@@ -152,8 +224,10 @@ class Contextmenu<x, y> {
}
});
}
constructor(name: string) {
private layered = false;
constructor(name: string, layered = false) {
this.name = name;
this.layered = layered;
this.buttons = [];
}
@@ -165,24 +239,45 @@ class Contextmenu<x, y> {
visable?: (this: x, arg: y) => boolean;
enabled?: (this: x, arg: y) => boolean;
color?: "red" | "blue";
group?: string;
} = {},
) {
this.buttons.push(new ContextButton(text, onClick, addProps));
}
addSeperator(visable?: (obj1: x, obj2: y) => boolean) {
this.buttons.push(new Seperator(visable));
addSeperator(visable?: (obj1: x, obj2: y) => boolean, group?: string) {
this.buttons.push(new Seperator(visable, group));
}
makemenu(x: number, y: number, addinfo: x, other: y, keep: boolean | HTMLElement = false) {
addGroup(
group: string,
addprops?: {
visable?: (this: x, arg: y) => boolean;
},
) {
this.buttons.push(new ContextGroup<x, y>(group, addprops));
}
makemenu(
x: number,
y: number,
addinfo: x,
other: y,
keep: boolean | HTMLElement = false,
layered: LayeredEvent["menus"] = [],
) {
const div = document.createElement("div");
div.classList.add("contextmenu", "flexttb");
const processed = new WeakSet<menuPart<unknown, unknown>>();
for (const button of this.buttons) {
button.makeContextHTML(addinfo, other, div);
button.makeContextHTML(addinfo, other, div, layered, processed);
}
if (div.children[div.children.length - 1]?.tagName === "HR") {
if (div.children[div.children.length - 1]?.tagName !== "HR") {
div.append(document.createElement("hr"));
}
new ContextGroup<x, y>("default").makeContextHTML(addinfo, other, div, layered, processed);
while (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;
Contextmenu.declareMenu(div, keep);
@@ -223,10 +318,19 @@ class Contextmenu<x, y> {
}
}
}
event.preventDefault();
event.stopImmediatePropagation();
this.makemenu(event.clientX, event.clientY, addinfo, other);
event.preventDefault();
const layered = new LayeredEvent(event, []);
obj.dispatchEvent(layered);
};
obj.addEventListener("layered", (layered) => {
if (this.layered) {
layered.menus.push([this, addinfo, other]);
} else if (!layered.primary) {
layered.primary = [this, addinfo, other];
}
return;
});
if (click === "right") {
obj.addEventListener("contextmenu", func);
} else {
+25 -1
View File
@@ -434,6 +434,30 @@ class Group extends Channel {
users: User[];
owner_id?: string;
static contextmenu = new Contextmenu<Group, undefined>("channel menu");
static groupMenu = this.makeGroupMenu();
static makeGroupMenu() {
const menu = new Contextmenu<Group, User>("group menu", true);
menu.addButton(
function (user) {
return I18n.member.kick(user.name, this.name);
},
function (user) {
fetch(this.info.api + `/channels/${this.id}/recipients/${user.id}`, {
method: "DELETE",
headers: this.headers,
});
},
{
group: "default",
visable: function (user) {
return this.localuser.user.id !== user.id && this.owner_id === this.localuser.user.id;
},
color: "red",
},
);
return menu;
}
static setupcontextmenu() {
this.contextmenu.addButton(
() => I18n.DMs.markRead(),
@@ -451,7 +475,7 @@ class Group extends Channel {
},
{
visable: function () {
return this.users.length !== 1;
return this.type !== 1;
},
},
);
+6 -4
View File
@@ -1119,7 +1119,7 @@ class Localuser {
if (channel.voice && this.voiceAllowed) {
return;
}
if (guild.id === "@me" && (channel as Group).users.length === 1) {
if (guild.id === "@me" && (channel as Group).type === 1) {
return;
}
@@ -1221,17 +1221,19 @@ class Localuser {
bot.textContent = I18n.bot();
username.appendChild(bot);
}
member.bind(username);
user.bind(memberdiv, member instanceof Member ? member.guild : undefined, false);
memberdiv.append(pfp, username);
if (channel instanceof Group) {
console.log(channel.owner_id);
Group.groupMenu.bindContextmenu(memberdiv, channel, user);
if (channel.owner_id === user.id) {
const crown = document.createElement("span");
crown.classList.add("svg-crown");
memberdiv.append(crown);
}
}
member.bind(username);
user.bind(memberdiv, member instanceof Member ? member.guild : undefined, false);
memberdiv.classList.add("flexltr", "liststyle", "memberListStyle");
membershtml.append(memberdiv);
}