Files
bitvid/js/ui/confirmDialog.js
T
thePR0M3TH3ANandClaude Opus 4.8 ad83c9f8db [ui] route all native alert()/confirm() through the site notification system
Every browser alert()/confirm() now uses the app's UI instead of native dialogs
(no prompt() existed). Two new pieces:

- js/ui/confirmDialog.js — showConfirm(message, opts) -> Promise<boolean>, a styled
  `bv-modal modal-always-on-top` dialog (Confirm/Cancel/backdrop/Escape; danger variant
  focuses Cancel). Importable anywhere, so future confirmations use it too. It stacks
  above other modals and is typable thanks to the recent stacked-modal focus fix.
- js/ui/appNotify.js — notifyError/notifyStatus/notifySuccess routing to the app's
  NotificationController, for modules without a local showError; re-exports showConfirm.

Migrated: UploadModal (10 alert -> this.showError, 1 confirm), subscriptionHistory
(3 alert -> notify*, 1 confirm), MyVideosController (2), ProfileStorage/Wallet/Relay
controllers, profileModalController (2), settingsRestorePrompt (default confirm now
showConfirm), nostr/client deleteAllVersions. confirmSyncOverwrite helpers made async
(syncConflict already awaits confirmOverwrite).

Tests: tests/confirm-dialog.test.mjs (6 — confirm/cancel/backdrop/Escape resolution,
DOM cleanup, danger focus, no-DOM fallback). Build + lint clean; affected suites green.
(Note: build/lint don't parse JS — caught a multi-line-import split + a wrong import
path via module-import checks.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 16:49:42 -04:00

109 lines
3.7 KiB
JavaScript

// Promise-based confirm dialog rendered with the site's modal styling, so yes/no
// confirmations live in the app's UI system instead of a native window.confirm.
//
// Importable anywhere (components AND services) — it builds its own
// `bv-modal modal-always-on-top` overlay, so it stacks above any open modal and is
// keyboard/click dismissible. Returns a Promise<boolean>: true = confirmed.
function makeButton(label, { ghost = false, danger = false } = {}) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = ghost ? "btn-ghost focus-ring" : "btn focus-ring";
if (danger) {
btn.setAttribute("data-variant", "danger");
}
btn.textContent = label;
return btn;
}
export function showConfirm(message, options = {}) {
const {
title = "",
confirmLabel = "Confirm",
cancelLabel = "Cancel",
danger = false,
} = options || {};
// No DOM (tests without jsdom / SSR): fall back to window.confirm, else decline.
if (typeof document === "undefined" || !document.body) {
if (typeof window !== "undefined" && typeof window.confirm === "function") {
return Promise.resolve(window.confirm(String(message || "")));
}
return Promise.resolve(false);
}
return new Promise((resolve) => {
const overlay = document.createElement("div");
overlay.className =
"bv-modal modal-always-on-top items-start justify-center md:items-center";
overlay.setAttribute("role", "dialog");
overlay.setAttribute("aria-modal", "true");
const backdrop = document.createElement("div");
backdrop.className = "bv-modal-backdrop";
overlay.appendChild(backdrop);
const sheet = document.createElement("div");
sheet.className = "modal-sheet w-full max-w-md flex flex-col";
sheet.tabIndex = -1;
overlay.appendChild(sheet);
if (title) {
const header = document.createElement("div");
header.className = "modal-header";
const heading = document.createElement("h2");
heading.className = "text-lg font-bold text-text";
heading.textContent = title;
header.appendChild(heading);
sheet.appendChild(header);
}
const body = document.createElement("div");
body.className = "p-5";
const text = document.createElement("p");
text.className = "text-sm text-muted-strong whitespace-pre-line";
text.textContent = String(message || "");
body.appendChild(text);
sheet.appendChild(body);
const footer = document.createElement("div");
footer.className =
"flex items-center justify-end gap-3 border-t border-border/60 p-4";
const cancelBtn = makeButton(cancelLabel, { ghost: true });
const confirmBtn = makeButton(confirmLabel, { danger });
footer.appendChild(cancelBtn);
footer.appendChild(confirmBtn);
sheet.appendChild(footer);
let settled = false;
const cleanup = (result) => {
if (settled) {
return;
}
settled = true;
document.removeEventListener("keydown", onKeydown, true);
overlay.remove();
resolve(result);
};
function onKeydown(event) {
if (event.key === "Escape") {
event.preventDefault();
cleanup(false);
} else if (event.key === "Enter" && document.activeElement === confirmBtn) {
cleanup(true);
}
}
cancelBtn.addEventListener("click", () => cleanup(false));
backdrop.addEventListener("click", () => cleanup(false));
confirmBtn.addEventListener("click", () => cleanup(true));
document.addEventListener("keydown", onKeydown, true);
document.body.appendChild(overlay);
// Focus the safe action by default (Cancel for destructive prompts).
(danger ? cancelBtn : confirmBtn).focus();
});
}
export default showConfirm;