(function () { const VARIANTS = ["info", "warning", "error"]; const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'; window.Warpbox = window.Warpbox || {}; let dialogIdCounter = 0; function defaultTitle(variant) { if (variant === "error") { return "Error"; } if (variant === "warning") { return "Warning"; } return "Info"; } function normalizeOptions(options, message) { if (typeof options === "string") { options = { message: options }; } else { options = options || {}; } if (message) { options.message = message; } const variant = VARIANTS.includes(options.variant) ? options.variant : "info"; return { variant, title: options.title || defaultTitle(variant), message: options.message || "", body: options.body || null, actions: Array.isArray(options.actions) ? options.actions : [], dismissible: options.dismissible !== false, closable: options.closable !== false, onClose: typeof options.onClose === "function" ? options.onClose : null, }; } function focusableElements(container) { return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => el.offsetParent !== null); } function dialog(options, message) { const config = normalizeOptions(options, message); const previouslyFocused = document.activeElement; dialogIdCounter += 1; const titleId = "warpbox-dialog-title-" + dialogIdCounter; const overlay = document.createElement("div"); overlay.className = "warpbox-dialog-overlay"; const card = document.createElement("div"); card.className = "warpbox-dialog warpbox-dialog-" + config.variant; card.setAttribute("role", config.variant === "error" ? "alertdialog" : "dialog"); card.setAttribute("aria-modal", "true"); card.setAttribute("aria-labelledby", titleId); card.setAttribute("tabindex", "-1"); const head = document.createElement("div"); head.className = "warpbox-dialog-head"; const icon = document.createElement("span"); icon.className = "warpbox-dialog-icon"; icon.setAttribute("aria-hidden", "true"); icon.textContent = config.variant === "error" ? "!" : config.variant === "warning" ? "?" : "i"; const title = document.createElement("h2"); title.id = titleId; title.className = "warpbox-dialog-title"; title.textContent = config.title; head.append(icon, title); if (config.closable) { const close = document.createElement("button"); close.type = "button"; close.className = "warpbox-dialog-close"; close.setAttribute("aria-label", "Close dialog"); close.textContent = "x"; close.addEventListener("click", () => closeDialog()); head.append(close); } const body = document.createElement("div"); body.className = "warpbox-dialog-body"; if (config.message) { const text = document.createElement("p"); text.className = "warpbox-dialog-message"; text.textContent = config.message; body.append(text); } if (config.body) { const nodes = Array.isArray(config.body) ? config.body : [config.body]; nodes.forEach((node) => { if (node instanceof Node) { body.append(node); } }); } card.append(head, body); let autofocusTarget = null; if (config.actions.length > 0) { const actions = document.createElement("div"); actions.className = "warpbox-dialog-actions"; config.actions.forEach((action) => { const button = document.createElement("button"); button.type = "button"; button.className = "button " + (action.kind === "primary" ? "button-primary" : action.kind === "ghost" ? "button-ghost" : "button-outline"); button.textContent = action.label || "OK"; button.addEventListener("click", () => { if (typeof action.onClick === "function") { action.onClick(); } if (action.dismiss !== false) { closeDialog(); } }); if (action.autofocus) { autofocusTarget = button; } actions.append(button); }); card.append(actions); } overlay.append(card); document.body.append(overlay); document.documentElement.classList.add("warpbox-dialog-open"); window.requestAnimationFrame(() => { overlay.classList.add("is-visible"); (autofocusTarget || card).focus(); }); function handleKeydown(event) { if (event.key === "Escape") { if (config.dismissible) { event.preventDefault(); closeDialog(); } return; } if (event.key !== "Tab") { return; } const focusable = focusableElements(card); if (focusable.length === 0) { event.preventDefault(); 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(); } } function handleOverlayClick(event) { if (config.dismissible && event.target === overlay) { closeDialog(); } } document.addEventListener("keydown", handleKeydown, true); overlay.addEventListener("click", handleOverlayClick); let closed = false; function closeDialog() { if (closed) { return; } closed = true; document.removeEventListener("keydown", handleKeydown, true); overlay.removeEventListener("click", handleOverlayClick); overlay.classList.remove("is-visible"); document.documentElement.classList.remove("warpbox-dialog-open"); window.setTimeout(() => overlay.remove(), 180); if (previouslyFocused && typeof previouslyFocused.focus === "function") { previouslyFocused.focus(); } if (config.onClose) { config.onClose(); } } return { element: overlay, close: closeDialog, }; } window.Warpbox.dialog = dialog; window.Warpbox.alertDialog = function alertDialog(message, options) { const config = (typeof options === "object" && options) || {}; return new Promise((resolve) => { dialog({ ...config, message: typeof message === "string" ? message : config.message, actions: [{ label: config.okLabel || "OK", kind: "primary", autofocus: true }], onClose: () => { if (typeof config.onClose === "function") { config.onClose(); } resolve(); }, }); }); }; window.Warpbox.confirmDialog = function confirmDialog(message, options) { const config = (typeof options === "object" && options) || {}; return new Promise((resolve) => { let settled = false; function settle(value) { if (settled) { return; } settled = true; resolve(value); } dialog({ ...config, message: typeof message === "string" ? message : config.message, actions: [ { label: config.cancelLabel || "Cancel", kind: "outline", autofocus: true, onClick: () => settle(false) }, { label: config.confirmLabel || "Confirm", kind: "primary", onClick: () => settle(true) }, ], onClose: () => { if (typeof config.onClose === "function") { config.onClose(); } settle(false); }, }); }); }; window.Warpbox.promptDialog = function promptDialog(message, options) { const config = (typeof options === "object" && options) || {}; return new Promise((resolve) => { let settled = false; function settle(value) { if (settled) { return; } settled = true; resolve(value); } const field = document.createElement("input"); field.type = config.inputType || "text"; field.className = "warpbox-dialog-field"; if (config.placeholder) { field.placeholder = config.placeholder; } if (typeof config.value === "string") { field.value = config.value; } let controller = null; field.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); settle(field.value); if (controller) { controller.close(); } } }); controller = dialog({ ...config, message: typeof message === "string" ? message : config.message, body: field, actions: [ { label: config.cancelLabel || "Cancel", kind: "outline", onClick: () => settle(null) }, { label: config.okLabel || "OK", kind: "primary", onClick: () => settle(field.value) }, ], onClose: () => { if (typeof config.onClose === "function") { config.onClose(); } settle(null); }, }); window.requestAnimationFrame(() => field.focus()); }); }; })();