feat(config): add box owner policy settings
Adds configuration options and environment variables to manage box owner policies, including settings for refresh counts and expiry.
This commit is contained in:
258
static/js/account-ui.js
Normal file
258
static/js/account-ui.js
Normal file
@@ -0,0 +1,258 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user