Files
yawn/server/web/assets/sculpt/menus.js
T
Ampandheaust 347b676cf1 Add core-powered web sculpting studio with tablet controls
Add pressure-aware worker sculpting, bounded shared input, project files and history, and compact icon navigation with an XYZ orbit gizmo.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-09-28 08:53:50 +00:00

110 lines
3.3 KiB
JavaScript

// Panels overlay the canvas: revealing tools never resizes or replaces it.
export class Menus {
constructor(canOpen, ignoreOutsidePointer) {
this.canOpen = canOpen;
this.items = [
["tools-toggle", "tools-panel"],
["settings-toggle", "inspector"],
].map(([button, panel]) => ({
button: document.getElementById(button),
panel: document.getElementById(panel),
}));
for (const item of this.items) {
item.button.addEventListener("pointerenter", (e) => {
if (["pen", "mouse"].includes(e.pointerType) && e.buttons === 0)
this.open(item);
});
item.button.addEventListener("click", () => {
if (this.current === item && this.pinned) this.close();
else this.open(item, true);
});
item.button.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
this.open(item, true, true);
}
});
for (const el of [item.button, item.panel]) {
el.addEventListener("pointerenter", () => clearTimeout(this.timer));
el.addEventListener("pointerleave", () => {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
if (!this.pinned) this.close(false);
}, 250);
});
}
item.panel.addEventListener("pointerdown", () => {
this.pinned = true;
clearTimeout(this.timer);
});
item.panel.addEventListener("focusin", () => {
this.pinned = true;
clearTimeout(this.timer);
});
item.panel.querySelector("[data-close]").onclick = () => this.close();
}
document.addEventListener(
"pointerdown",
(e) => {
if (
this.current &&
!ignoreOutsidePointer(e) &&
!this.current.panel.contains(e.target) &&
!this.current.button.contains(e.target)
)
this.close(false);
},
true,
);
document.addEventListener(
"keydown",
(e) => {
if (
e.key === "Escape" &&
this.current &&
!document.querySelector("dialog[open]")
) {
e.preventDefault();
e.stopPropagation();
this.close();
}
},
true,
);
window.addEventListener("resize", () => this.close(false));
}
open(item, pin = false, focus = false) {
if (!this.canOpen() || document.querySelector("dialog[open]")) return;
clearTimeout(this.timer);
if (this.current !== item) {
this.close(false);
this.current = item;
this.pinned = false;
}
this.pinned ||= pin;
item.panel.hidden = false;
const top = Math.max(
12,
Math.min(
item.button.getBoundingClientRect().top,
window.innerHeight - item.panel.offsetHeight - 12,
),
);
item.panel.style.top = `${top}px`;
item.panel.style.maxHeight = `calc(100dvh - ${top + 12}px)`;
item.button.setAttribute("aria-expanded", "true");
if (focus)
item.panel.querySelector("button:not(:disabled),input,select")?.focus();
}
close(restore = true) {
clearTimeout(this.timer);
if (!this.current) return;
const item = this.current;
this.current = null;
this.pinned = false;
item.panel.hidden = true;
item.button.setAttribute("aria-expanded", "false");
if (restore) item.button.focus();
}
}