From bc8edbcd50ba0770af269cf809c6733a071ed9ba Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 12:00:28 +0200 Subject: [PATCH] feat(modals): click intercept + openModal + fetchAndShow a[data-modal] clicks push ?modal= to URL and trigger htmx.ajax into #modal-content with the HX-Modal header. window.openModal exposed for non- trigger sites (files-overlay row clicks). Race guard via currentModalPath token. Close/popstate/bootstrap follow. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/l4d2web/static/js/modal-router.js | 51 +++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/l4d2web/l4d2web/static/js/modal-router.js b/l4d2web/l4d2web/static/js/modal-router.js index 7450ecd..234de70 100644 --- a/l4d2web/l4d2web/static/js/modal-router.js +++ b/l4d2web/l4d2web/static/js/modal-router.js @@ -1,6 +1,51 @@ -// URL-addressable modal router (see docs/superpowers/specs/2026-05-17-url-addressable-modals-design.md). -// Implementation lands in subsequent tasks; this stub keeps base.html's -// script include from 404'ing during the staged rollout. +// URL-addressable modal router (see spec 2026-05-17-url-addressable-modals). +// Click intercept on a[data-modal] → ?modal= in URL → htmx swap into +// #modal-content → showModal(). Close/popstate/bootstrap in later tasks. (function () { "use strict"; + + let currentModalPath = null; // race-guard against stale swaps + + function openModal(path) { + const url = new URL(window.location.href); + url.searchParams.set("modal", path); + history.pushState({ modal: path }, "", url.toString()); + fetchAndShow(path); + } + + function fetchAndShow(path) { + currentModalPath = path; + if (typeof window.htmx === "undefined") { + console.error("[modal-router] htmx not loaded; cannot fetch modal"); + return; + } + window.htmx.ajax("GET", path, { + target: "#modal-content", + swap: "innerHTML", + headers: { "HX-Modal": "1" }, + }).then(() => { + // Race guard: if the user clicked again during the fetch, abandon + // this swap; the newer click will win. + if (currentModalPath !== path) return; + const dlg = document.getElementById("modal-container"); + if (dlg && !dlg.open) dlg.showModal(); + }).catch((err) => { + console.error("[modal-router] fetch failed", err); + }); + } + + document.addEventListener("click", (event) => { + const link = event.target.closest("a[data-modal]"); + if (!link) return; + if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + if (event.button !== 0) return; + const href = link.getAttribute("href"); + if (!href) return; + event.preventDefault(); + openModal(href); + }); + + // Public API — used by files-overlay.js to open the editor from row clicks + // that aren't a literal (existing event delegation). + window.openModal = openModal; })();