|
// retoor <retoor@molodetz.nl>
|
|
|
|
export class ModalManager {
|
|
constructor() {
|
|
this.initPasswordToggles();
|
|
this.initModals();
|
|
this.initConfirmations();
|
|
}
|
|
|
|
initPasswordToggles() {
|
|
document.querySelectorAll(".auth-toggle-pw").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const input = btn.parentElement.querySelector("input");
|
|
if (!input) {
|
|
return;
|
|
}
|
|
const type = input.type === "password" ? "text" : "password";
|
|
input.type = type;
|
|
btn.textContent = type === "password" ? "\u{1F441}" : "\u{1F648}";
|
|
});
|
|
});
|
|
}
|
|
|
|
initModals() {
|
|
document.querySelectorAll("[data-modal]").forEach((trigger) => {
|
|
trigger.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
const modalId = trigger.dataset.modal;
|
|
const modal = document.getElementById(modalId);
|
|
if (modal) {
|
|
modal.classList.add("visible");
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll(".modal-overlay").forEach((modal) => {
|
|
modal.addEventListener("click", (e) => {
|
|
if (e.target === modal) {
|
|
modal.classList.remove("visible");
|
|
}
|
|
});
|
|
modal.querySelectorAll(".modal-close").forEach((closeBtn) => {
|
|
closeBtn.addEventListener("click", () => {
|
|
modal.classList.remove("visible");
|
|
});
|
|
});
|
|
this.enhanceModal(modal);
|
|
});
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key !== "Escape") return;
|
|
const open = [...document.querySelectorAll(".modal-overlay.visible")].pop();
|
|
if (open) open.classList.remove("visible");
|
|
});
|
|
}
|
|
|
|
enhanceModal(modal) {
|
|
if (!modal.getAttribute("role")) modal.setAttribute("role", "dialog");
|
|
modal.setAttribute("aria-modal", "true");
|
|
const heading = modal.querySelector(
|
|
".modal-title, .modal-header h1, .modal-header h2, .modal-header h3, h1, h2, h3"
|
|
);
|
|
if (heading) {
|
|
if (!heading.id) {
|
|
heading.id = `modal-title-${Math.random().toString(36).slice(2, 9)}`;
|
|
}
|
|
modal.setAttribute("aria-labelledby", heading.id);
|
|
}
|
|
|
|
const trap = (e) => {
|
|
if (e.key !== "Tab") return;
|
|
const items = this.focusable(modal);
|
|
if (!items.length) return;
|
|
const first = items[0];
|
|
const last = items[items.length - 1];
|
|
if (e.shiftKey && document.activeElement === first) {
|
|
e.preventDefault();
|
|
last.focus();
|
|
} else if (!e.shiftKey && document.activeElement === last) {
|
|
e.preventDefault();
|
|
first.focus();
|
|
}
|
|
};
|
|
|
|
const observer = new MutationObserver(() => {
|
|
const visible = modal.classList.contains("visible");
|
|
if (visible && !modal._a11yOpen) {
|
|
modal._a11yOpen = true;
|
|
modal._lastFocus = document.activeElement;
|
|
modal.addEventListener("keydown", trap);
|
|
const items = this.focusable(modal);
|
|
if (items.length) setTimeout(() => items[0].focus(), 20);
|
|
} else if (!visible && modal._a11yOpen) {
|
|
modal._a11yOpen = false;
|
|
modal.removeEventListener("keydown", trap);
|
|
if (modal._lastFocus && modal._lastFocus.focus) modal._lastFocus.focus();
|
|
}
|
|
});
|
|
observer.observe(modal, { attributes: true, attributeFilter: ["class"] });
|
|
}
|
|
|
|
focusable(root) {
|
|
return [
|
|
...root.querySelectorAll(
|
|
'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
),
|
|
].filter((el) => el.offsetParent !== null || el === document.activeElement);
|
|
}
|
|
|
|
initConfirmations() {
|
|
document.addEventListener("click", (e) => {
|
|
const el = e.target.closest("[data-confirm]");
|
|
if (!el) return;
|
|
if (el.hasAttribute("data-auto-submit")) return;
|
|
if (el.dataset.confirmed === "1") {
|
|
delete el.dataset.confirmed;
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
const proceed = () => {
|
|
if (!el.isConnected) return;
|
|
el.dataset.confirmed = "1";
|
|
el.click();
|
|
};
|
|
const dialog = window.app && window.app.dialog;
|
|
if (!dialog) {
|
|
if (confirm(el.dataset.confirm)) proceed();
|
|
return;
|
|
}
|
|
dialog.confirm({
|
|
message: el.dataset.confirm,
|
|
title: el.dataset.confirmTitle || "Please confirm",
|
|
confirmLabel: el.dataset.confirmLabel,
|
|
danger: el.dataset.confirmDanger !== undefined,
|
|
}).then((ok) => {
|
|
if (ok) proceed();
|
|
});
|
|
}, true);
|
|
}
|
|
}
|