diff --git a/templates/header.templ b/templates/header.templ index 3f2ff19e..050010dc 100644 --- a/templates/header.templ +++ b/templates/header.templ @@ -352,4 +352,24 @@ templ SidebarSignIn(currentPath string) { } + + + } diff --git a/web/src/admin.ts b/web/src/admin.ts index 5d830f2b..1353d9a2 100644 --- a/web/src/admin.ts +++ b/web/src/admin.ts @@ -1,3 +1,4 @@ +import { confirmDialog } from "./confirm"; import { Alpine } from "./alpine"; import { showToast } from "./toast"; @@ -423,9 +424,9 @@ async function purgeArchivedItems(): Promise { if (!token) return; if ( - !window.confirm( + !(await confirmDialog( "Permanently delete all archived items? Their reading progress, notes, and highlights will be lost.", - ) + )) ) { return; } @@ -475,9 +476,9 @@ async function deleteArchivedItem(id: string): Promise { const token = requireToken(); if (!token) return; if ( - !window.confirm( + !(await confirmDialog( "Permanently delete this item? Its reading progress, notes, and highlights will be lost.", - ) + )) ) { return; } @@ -504,9 +505,9 @@ async function mergeArchivedItem(sourceId: string, targetId: string): Promise { async purgeDeletedAnnotation(annotationType: string, annotationId: string) { if ( - !confirm( + !(await confirmDialog( "Permanently delete this annotation? This cannot be undone and it will not reappear on any device.", - ) + )) ) { return; } @@ -1002,9 +1003,9 @@ Alpine.data("bookDetail", () => { async resetMetadata() { if (this.resetting) return; if ( - !window.confirm( + !(await confirmDialog( "Reset all metadata to scanned defaults? Manual edits and custom covers will be discarded.", - ) + )) ) { return; } diff --git a/web/src/confirm.ts b/web/src/confirm.ts new file mode 100644 index 00000000..cd1b4244 --- /dev/null +++ b/web/src/confirm.ts @@ -0,0 +1,80 @@ +// Global confirm-dialog replacement (issue #5): ONE styled modal +// answers every hx-confirm sitewide (via the htmx:confirm event) and +// exposes a promise API for the legacy JS call sites. No more native +// window.confirm anywhere. + +let activeResolve: ((ok: boolean) => void) | null = null; + +function modalElements(): { overlay: HTMLElement | null; message: HTMLElement | null } { + return { + overlay: document.getElementById("confirm-modal"), + message: document.getElementById("confirm-modal-message"), + }; +} + +function hide(ok: boolean): void { + const { overlay } = modalElements(); + if (overlay) overlay.classList.add("hidden"); + const resolve = activeResolve; + activeResolve = null; + if (resolve) resolve(ok); +} + +/** + * confirmDialog shows the global modal and resolves with the answer. + * Safe to call from any module (import this instead of window.confirm). + */ +export function confirmDialog(message: string): Promise { + const { overlay, message: messageEl } = modalElements(); + if (!overlay || !messageEl) { + // Header missing (bare layouts): degrade to the native dialog + // rather than silently swallowing the confirmation. + return Promise.resolve(window.confirm(message)); + } + messageEl.textContent = message; + overlay.classList.remove("hidden"); + return new Promise((resolve) => { + activeResolve = resolve; + }); +} + +// Wire the static buttons once. +function wireButtons(): void { + const { overlay } = modalElements(); + const accept = document.getElementById("confirm-modal-accept"); + const dismiss = document.getElementById("confirm-modal-cancel"); + if (accept && !accept.hasAttribute("data-wired")) { + accept.setAttribute("data-wired", "1"); + accept.addEventListener("click", () => hide(true)); + } + if (dismiss && !dismiss.hasAttribute("data-wired")) { + dismiss.setAttribute("data-wired", "1"); + dismiss.addEventListener("click", () => hide(false)); + } + // Backdrop click cancels. + if (overlay && !overlay.hasAttribute("data-wired")) { + overlay.setAttribute("data-wired", "1"); + overlay.addEventListener("click", (e) => { + if (e.target === overlay) hide(false); + }); + } +} + +// htmx:confirm interceptor: every element with hx-confirm gets the +// modal instead of the browser dialog; confirming issues the request. +// (Canonical htmx pattern — see htmx docs, "confirming with a modal".) +document.body.addEventListener("htmx:confirm", function (evt) { + const custom = evt as CustomEvent<{ + element: HTMLElement; + issueRequest: (override?: boolean) => void; + }>; + const elt = custom.detail.element; + if (!elt) return; + const question = elt.getAttribute("hx-confirm"); + if (question === null) return; + evt.preventDefault(); + wireButtons(); + confirmDialog(question).then((ok) => { + if (ok) custom.detail.issueRequest(true); + }); +}); diff --git a/web/src/main.ts b/web/src/main.ts index a93db44b..1b74ab31 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -25,6 +25,7 @@ import "./login"; import "./password_validation"; import "./processing_issues"; import "./podcasts"; +import "./confirm"; import "./profile"; import "./profile-modal"; import "./register";