Adds configuration options and environment variables to manage box owner policies, including settings for refresh counts and expiry.
259 lines
9.0 KiB
JavaScript
259 lines
9.0 KiB
JavaScript
window.WarpBoxAccountUI = (() => {
|
|
let toastTimer = null;
|
|
let activeConfirmResolve = null;
|
|
|
|
function initStickyTaskbar(options = {}) {
|
|
const taskbar = options.taskbar || document.querySelector(".top-taskbar");
|
|
if (!taskbar) return;
|
|
|
|
const update = () => {
|
|
taskbar.classList.toggle("is-scrolled", window.scrollY > 2);
|
|
};
|
|
|
|
update();
|
|
window.addEventListener("scroll", update, { passive: true });
|
|
}
|
|
|
|
function closeMenus(root = document) {
|
|
root.querySelectorAll(".menu-item.is-open").forEach((item) => {
|
|
item.classList.remove("is-open");
|
|
item.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
|
|
});
|
|
}
|
|
|
|
function openMenu(item) {
|
|
if (!item) return;
|
|
closeMenus(item.closest(".menu-bar") || document);
|
|
item.classList.add("is-open");
|
|
item.querySelector(".menu-button")?.setAttribute("aria-expanded", "true");
|
|
}
|
|
|
|
function initMenus(options = {}) {
|
|
const root = options.root || document;
|
|
root.addEventListener("click", (event) => {
|
|
const button = event.target.closest(".menu-button");
|
|
if (button) {
|
|
const item = button.closest(".menu-item");
|
|
const isOpen = item?.classList.contains("is-open");
|
|
closeMenus(root);
|
|
if (!isOpen) openMenu(item);
|
|
return;
|
|
}
|
|
|
|
if (!event.target.closest(".menu-item")) {
|
|
closeMenus(root);
|
|
}
|
|
});
|
|
|
|
root.querySelectorAll(".menu-item").forEach((item) => {
|
|
item.addEventListener("mouseenter", () => {
|
|
if (!root.querySelector(".menu-item.is-open")) return;
|
|
openMenu(item);
|
|
});
|
|
});
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") closeMenus(root);
|
|
});
|
|
}
|
|
|
|
function toast(message, type = "info", options = {}) {
|
|
if (window.WarpBoxUI?.toast && !options.forceAccountToast) {
|
|
window.WarpBoxUI.toast(message, type, options);
|
|
return;
|
|
}
|
|
|
|
const target = options.target || document.querySelector("#account-toast") || document.querySelector("#toast");
|
|
if (!target) return;
|
|
|
|
target.textContent = message;
|
|
target.classList.remove("toast-info", "toast-success", "toast-warning", "toast-error", "is-visible");
|
|
target.classList.add(`toast-${type}`, "is-visible");
|
|
clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => target.classList.remove("is-visible"), options.duration || 2600);
|
|
}
|
|
|
|
function modalElements(options = {}) {
|
|
return {
|
|
modal: options.modal || document.querySelector("#account-modal"),
|
|
title: options.title || document.querySelector("#account-modal-title"),
|
|
body: options.body || document.querySelector("#account-modal-body"),
|
|
backdrop: options.backdrop || document.querySelector("#account-modal-backdrop") || document.querySelector("#modal-backdrop"),
|
|
};
|
|
}
|
|
|
|
function openModal(titleText, html, options = {}) {
|
|
const parts = modalElements(options);
|
|
if (!parts.modal || !parts.title || !parts.body) {
|
|
if (window.WarpBoxUI?.openPopup) {
|
|
window.WarpBoxUI.openPopup(titleText, html, options);
|
|
}
|
|
return;
|
|
}
|
|
|
|
parts.title.textContent = titleText;
|
|
if (options.text) {
|
|
parts.body.textContent = html;
|
|
} else {
|
|
parts.body.innerHTML = html;
|
|
}
|
|
parts.modal.classList.add("is-visible");
|
|
parts.backdrop?.classList.add("is-visible");
|
|
parts.modal.querySelector("[data-modal-close]")?.focus();
|
|
}
|
|
|
|
function closeModal(options = {}) {
|
|
const parts = modalElements(options);
|
|
parts.modal?.classList.remove("is-visible");
|
|
parts.backdrop?.classList.remove("is-visible");
|
|
if (window.WarpBoxUI?.closePopup && !parts.modal) {
|
|
window.WarpBoxUI.closePopup(options);
|
|
}
|
|
}
|
|
|
|
function confirm(message, options = {}) {
|
|
const title = options.title || "Confirm action";
|
|
const confirmLabel = options.confirmLabel || "OK";
|
|
const cancelLabel = options.cancelLabel || "Cancel";
|
|
const html = `
|
|
<p>${htmlEscape(message)}</p>
|
|
<div class="modal-actions">
|
|
<button class="win98-button" type="button" data-confirm-cancel>${htmlEscape(cancelLabel)}</button>
|
|
<button class="win98-button" type="button" data-confirm-ok>${htmlEscape(confirmLabel)}</button>
|
|
</div>
|
|
`;
|
|
|
|
const parts = modalElements(options);
|
|
if (!parts.modal) {
|
|
return Promise.resolve(window.confirm(message));
|
|
}
|
|
|
|
openModal(title, html, options);
|
|
return new Promise((resolve) => {
|
|
activeConfirmResolve = resolve;
|
|
parts.modal.querySelector("[data-confirm-ok]")?.focus();
|
|
});
|
|
}
|
|
|
|
function finishConfirm(result) {
|
|
if (activeConfirmResolve) {
|
|
activeConfirmResolve(result);
|
|
activeConfirmResolve = null;
|
|
}
|
|
closeModal();
|
|
}
|
|
|
|
function setDirtyState(isDirty, options = {}) {
|
|
const target = options.target || document.querySelector("[data-dirty-chip]");
|
|
if (!target) return;
|
|
target.classList.toggle("is-dirty", Boolean(isDirty));
|
|
target.textContent = isDirty ? (options.dirtyText || "unsaved changes") : (options.cleanText || "");
|
|
}
|
|
|
|
function bindFormDirtyState(form, options = {}) {
|
|
const targetForm = typeof form === "string" ? document.querySelector(form) : form;
|
|
if (!targetForm) return;
|
|
|
|
let baseline = new FormData(targetForm);
|
|
const serialize = () => new URLSearchParams(new FormData(targetForm)).toString();
|
|
let baselineValue = new URLSearchParams(baseline).toString();
|
|
|
|
const update = () => setDirtyState(serialize() !== baselineValue, options);
|
|
targetForm.addEventListener("input", update);
|
|
targetForm.addEventListener("change", update);
|
|
targetForm.addEventListener("submit", () => {
|
|
baseline = new FormData(targetForm);
|
|
baselineValue = new URLSearchParams(baseline).toString();
|
|
setDirtyState(false, options);
|
|
});
|
|
update();
|
|
}
|
|
|
|
function bindConfirmActions(root = document) {
|
|
root.addEventListener("click", async (event) => {
|
|
const ok = event.target.closest("[data-confirm-ok]");
|
|
if (ok) {
|
|
finishConfirm(true);
|
|
return;
|
|
}
|
|
|
|
const cancel = event.target.closest("[data-confirm-cancel], [data-modal-close]");
|
|
if (cancel) {
|
|
finishConfirm(false);
|
|
return;
|
|
}
|
|
|
|
const action = event.target.closest("[data-confirm]");
|
|
if (!action) return;
|
|
if (action.dataset.confirmAccepted === "true") {
|
|
delete action.dataset.confirmAccepted;
|
|
return;
|
|
}
|
|
|
|
const message = action.getAttribute("data-confirm");
|
|
if (!message) return;
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const accepted = await confirm(message, {
|
|
title: action.getAttribute("data-confirm-title") || "Confirm action",
|
|
confirmLabel: action.getAttribute("data-confirm-label") || "OK",
|
|
cancelLabel: action.getAttribute("data-cancel-label") || "Cancel",
|
|
});
|
|
if (!accepted) return;
|
|
|
|
if (action instanceof HTMLAnchorElement && action.href) {
|
|
window.location.href = action.href;
|
|
return;
|
|
}
|
|
|
|
const form = action.closest("form");
|
|
const type = (action.getAttribute("type") || "").toLowerCase();
|
|
if (form && (type === "submit" || type === "")) {
|
|
form.requestSubmit(action);
|
|
return;
|
|
}
|
|
|
|
action.dataset.confirmAccepted = "true";
|
|
action.click();
|
|
});
|
|
}
|
|
|
|
function htmlEscape(value) {
|
|
return String(value || "")
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function init(root = document) {
|
|
initStickyTaskbar();
|
|
initMenus({ root });
|
|
bindConfirmActions(root);
|
|
document.querySelector("#account-modal-backdrop")?.addEventListener("click", () => closeModal());
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") closeModal();
|
|
});
|
|
}
|
|
|
|
return {
|
|
init,
|
|
initStickyTaskbar,
|
|
initMenus,
|
|
toast,
|
|
confirm,
|
|
openModal,
|
|
closeModal,
|
|
setDirtyState,
|
|
bindFormDirtyState,
|
|
closeMenus,
|
|
};
|
|
})();
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
window.WarpBoxAccountUI.init();
|
|
});
|