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>
110 lines
3.3 KiB
JavaScript
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();
|
|
}
|
|
}
|