85 lines
2.5 KiB
JavaScript
85 lines
2.5 KiB
JavaScript
// retoor <retoor@molodetz.nl>
|
|||
|
|
export class ModalManager {
|
||
|
|
constructor() {
|
||
|
|
this.stack = [];
|
||
|
|
document.addEventListener("click", (event) => this.onClick(event));
|
||
|
|
document.addEventListener("keydown", (event) => {
|
||
|
|
if (event.key === "Escape" && this.stack.length) {
|
||
|
|
this.close(this.stack[this.stack.length - 1]);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
window.addEventListener("popstate", () => {
|
||
|
|
if (this.stack.length) {
|
||
|
|
this.close(this.stack[this.stack.length - 1], false);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
new MutationObserver(() => this.wire()).observe(document.body, { childList: true, subtree: true });
|
||
|
|
this.wire();
|
||
|
|
}
|
||
|
|
|
||
|
|
wire() {
|
||
|
|
document.querySelectorAll(".modal-overlay:not([data-wired])").forEach((overlay) => {
|
||
|
|
overlay.dataset.wired = "1";
|
||
|
|
overlay.setAttribute("role", overlay.getAttribute("role") || "dialog");
|
||
|
|
overlay.setAttribute("aria-modal", "true");
|
||
|
|
overlay.addEventListener("keydown", (event) => this.trap(event, overlay));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
onClick(event) {
|
||
|
|
const trigger = event.target.closest("[data-modal]");
|
||
|
|
if (trigger) {
|
||
|
|
event.preventDefault();
|
||
|
|
this.open(document.getElementById(trigger.dataset.modal));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (event.target.closest(".modal-close")) {
|
||
|
|
this.close(event.target.closest(".modal-overlay"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (event.target.classList?.contains("modal-overlay")) {
|
||
|
|
this.close(event.target);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
open(overlay) {
|
||
|
|
if (!overlay) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
overlay.classList.add("visible");
|
||
|
|
this.stack.push(overlay);
|
||
|
|
history.pushState({ modal: overlay.id }, "");
|
||
|
|
overlay.querySelector("button, a, input, select, textarea")?.focus();
|
||
|
|
}
|
||
|
|
|
||
|
|
close(overlay, popHistory = true) {
|
||
|
|
if (!overlay) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
overlay.classList.remove("visible");
|
||
|
|
this.stack = this.stack.filter((item) => item !== overlay);
|
||
|
|
if (popHistory && history.state?.modal === overlay.id) {
|
||
|
|
history.back();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
trap(event, overlay) {
|
||
|
|
if (event.key !== "Tab") {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const focusable = [...overlay.querySelectorAll("button, a[href], input, select, textarea")].filter((el) => !el.disabled);
|
||
|
|
if (!focusable.length) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const first = focusable[0];
|
||
|
|
const last = focusable[focusable.length - 1];
|
||
|
|
if (event.shiftKey && document.activeElement === first) {
|
||
|
|
event.preventDefault();
|
||
|
|
last.focus();
|
||
|
|
} else if (!event.shiftKey && document.activeElement === last) {
|
||
|
|
event.preventDefault();
|
||
|
|
first.focus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|