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) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-09 15:18:27 +02:00
parent 5eac51a93e
commit 87d56a0910
No known key found for this signature in database

View file

@ -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);
// 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.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;
}
document.querySelectorAll("dialog.modal").forEach((dialog) => {
dialog.querySelectorAll("[data-modal-close]").forEach((closer) => {
closer.addEventListener("click", (event) => {
const closer = event.target.closest("[data-modal-close]");
if (closer) {
const dialog = closer.closest("dialog.modal");
if (dialog) {
event.preventDefault();
dialog.close();
});
});
}
return;
}
dialog.addEventListener("click", (event) => {
if (event.target === dialog) {
dialog.close();
// 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();
}
});
});
});