From 87d56a09102af3fc3fe2c47b183b56f7dbbe1cd3 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sat, 9 May 2026 15:18:27 +0200 Subject: [PATCH] fix(web): event-delegate modal triggers so HTMX-swapped buttons work The previous wiring attached click listeners on DOMContentLoaded, so any [data-modal-open] / [data-modal-close] / dialog.modal element that came in via a later HTMX partial swap silently lost its behaviour. The server-detail Actions partial reloads its reset/delete triggers on every state change, so reset was unclickable after the first state change post-load. Switch to a single delegated click handler on document. Same logic, but matches via Element.closest() so it works regardless of when an element was added to the DOM. No re-bind needed after HTMX swaps. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/static/js/modal.js | 55 +++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/l4d2web/static/js/modal.js b/l4d2web/static/js/modal.js index e354ce5..e263719 100644 --- a/l4d2web/static/js/modal.js +++ b/l4d2web/static/js/modal.js @@ -1,27 +1,34 @@ -document.addEventListener("DOMContentLoaded", () => { - document.querySelectorAll("[data-modal-open]").forEach((trigger) => { - trigger.addEventListener("click", (event) => { - const targetId = trigger.getAttribute("data-modal-open"); - const dialog = document.getElementById(targetId); - if (dialog && typeof dialog.showModal === "function") { - event.preventDefault(); - dialog.showModal(); - } - }); - }); +// Event delegation on document so partials swapped in via HTMX (or any +// later DOM mutation) still get modal behaviour without re-binding. The +// previous per-element wiring on DOMContentLoaded silently broke for +// buttons that didn't exist at page load — e.g., the server-detail +// Actions partial reloads its reset/delete triggers on every state +// change, and only the very first ones were ever wired up. - document.querySelectorAll("dialog.modal").forEach((dialog) => { - dialog.querySelectorAll("[data-modal-close]").forEach((closer) => { - closer.addEventListener("click", (event) => { - event.preventDefault(); - dialog.close(); - }); - }); +document.addEventListener("click", (event) => { + const opener = event.target.closest("[data-modal-open]"); + if (opener) { + const dialog = document.getElementById(opener.getAttribute("data-modal-open")); + if (dialog && typeof dialog.showModal === "function") { + event.preventDefault(); + dialog.showModal(); + } + return; + } - dialog.addEventListener("click", (event) => { - if (event.target === dialog) { - dialog.close(); - } - }); - }); + const closer = event.target.closest("[data-modal-close]"); + if (closer) { + const dialog = closer.closest("dialog.modal"); + if (dialog) { + event.preventDefault(); + dialog.close(); + } + return; + } + + // Backdrop click: target IS the dialog (clicks on inner content + // don't bubble up as the dialog itself). + if (event.target.matches && event.target.matches("dialog.modal")) { + event.target.close(); + } });