feat(modals): close, popstate, dismiss, Esc, backdrop, response-error
Centralizes state cleanup on the dialog's native 'close' event: every close source (Esc cancel event, backdrop click, [data-modal-dismiss], browser back, htmx:responseError on the modal fetch, or programmatic closeModal()) just calls dialog.close() and the single 'close' listener clears ?modal= from the URL and resets currentModalPath. This avoids the trap where legacy modal.js's backdrop close didn't sync our URL, and the trap where a 4xx response opened an empty modal. window.closeModal exposed for callers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3de68b7539
commit
6e66375233
1 changed files with 70 additions and 0 deletions
|
|
@ -34,6 +34,55 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
const dlg = document.getElementById("modal-container");
|
||||||
|
if (dlg && dlg.open) dlg.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const dlg = document.getElementById("modal-container");
|
||||||
|
if (!dlg) return;
|
||||||
|
|
||||||
|
// Single source of truth: every close path funnels through the dialog's
|
||||||
|
// native 'close' event, so URL/state cleanup runs exactly once no matter
|
||||||
|
// who triggered the close (Esc, backdrop, dismiss button, response error,
|
||||||
|
// or the legacy modal.js backdrop handler).
|
||||||
|
dlg.addEventListener("close", () => {
|
||||||
|
currentModalPath = null;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (url.searchParams.has("modal")) {
|
||||||
|
url.searchParams.delete("modal");
|
||||||
|
history.pushState({}, "", url.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Esc key fires 'cancel' on a <dialog>; preventDefault so we control the
|
||||||
|
// close path (the default action would close() too, but we want one path).
|
||||||
|
dlg.addEventListener("cancel", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
dlg.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backdrop click on our own slot. The legacy modal.js handler also
|
||||||
|
// matches dialog.modal and closes; this listener is harmless when the
|
||||||
|
// legacy one wins because dialog.close() is idempotent.
|
||||||
|
dlg.addEventListener("click", (event) => {
|
||||||
|
if (event.target === dlg) dlg.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Browser back/forward: re-evaluate URL state and either fetch+show a new
|
||||||
|
// modal or close the current one. Closing routes through dialog.close() so
|
||||||
|
// the close-event cleanup fires (no separate state update here).
|
||||||
|
window.addEventListener("popstate", () => {
|
||||||
|
const path = new URL(window.location.href).searchParams.get("modal");
|
||||||
|
if (path) {
|
||||||
|
fetchAndShow(path);
|
||||||
|
} else {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
const link = event.target.closest("a[data-modal]");
|
const link = event.target.closest("a[data-modal]");
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
@ -45,7 +94,28 @@
|
||||||
openModal(href);
|
openModal(href);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dismiss triggers inside modal content (e.g. the editor's × and Cancel
|
||||||
|
// buttons render with data-modal-dismiss). Document-level delegation so
|
||||||
|
// HTMX-swapped content gets the behavior without re-binding.
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (event.target.closest("[data-modal-dismiss]")) {
|
||||||
|
event.preventDefault();
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// HTMX response error (4xx/5xx from the modal fetch): close the modal so
|
||||||
|
// the user isn't left with an error fragment open and a stale ?modal= URL.
|
||||||
|
document.body.addEventListener("htmx:responseError", (event) => {
|
||||||
|
const target = event.detail && event.detail.target;
|
||||||
|
if (target && target.id === "modal-content") {
|
||||||
|
console.warn("[modal-router] server returned error, closing modal", event.detail.xhr && event.detail.xhr.status);
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Public API — used by files-overlay.js to open the editor from row clicks
|
// Public API — used by files-overlay.js to open the editor from row clicks
|
||||||
// that aren't a literal <a data-modal> (existing event delegation).
|
// that aren't a literal <a data-modal> (existing event delegation).
|
||||||
window.openModal = openModal;
|
window.openModal = openModal;
|
||||||
|
window.closeModal = closeModal;
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue