5.1 KiB
IIFE Capture-Phase Handler Kills Save Button
Pattern
A page has an inline IIFE <script> block (not type="module") that registers a capture-phase click handler on document:
document.addEventListener('click', function(e) {
var closeBtn = e.target.closest('[aria-label="Close"],[data-close-modal]');
if (closeBtn) {
e.preventDefault();
e.stopPropagation(); // ← THIS IS THE KILLER
var id = closeBtn.getAttribute('data-close-modal');
if (id) { closeModal(id); return; }
// ... fallback close logic ...
}
// ... backdrop click handling ...
}, true); // ← capture phase
Some JS module (e.g., settings.js) sets data-close-modal on close, cancel, AND save buttons:
saveSiteSettings.setAttribute('data-close-modal', 'site-settings-modal');
Symptom
- Close (X) and Cancel buttons work correctly — modal closes
- Save button closes the modal but settings are never persisted
- No console errors
- The save function is defined and reachable (inline
onclickoraddEventListenerexists)
Root Cause
When the user clicks Save:
- Capture phase — IIFE handler on
documentfires FIRST. Matches[data-close-modal]on the save button. CallscloseModal()immediately. Callse.stopPropagation()which prevents the event from ever reaching the target phase or bubble phase. - Target phase — NEVER REACHED. The inline
onclick="saveSiteSettings()"never fires. - Bubble phase — NEVER REACHED. Any
addEventListener('click', handler)on the button never fires.
The modal closes (step 1 did that), but the save logic never executes.
Why Close/Cancel Still Work
For close and cancel buttons, closing the modal IS the correct action. The IIFE intercepting and closing is fine — no additional behavior was needed.
Fix (Three Options)
Option A: Remove data-close-modal from save button (cleanest)
In the code that sets attributes (ensureSiteSettingsCloseAttributes or dashboard init):
// BEFORE (broken)
setAttr(closeId); // ✓ fine
setAttr(cancelId); // ✓ fine
setAttr(saveId); // ✗ kills save handler
// AFTER (fixed)
setAttr(closeId); // ✓
setAttr(cancelId); // ✓
// Do NOT set on saveId — let addEventListener handle it
Then wire the save button via addEventListener (non-capture) so it fires after the IIFE ignores it:
saveBtn.addEventListener('click', async function() {
// read form, save settings
await saveAllSettings();
closeModal('site-settings-modal');
});
The IIFE no longer matches the save button → capture phase passes through → target/bubble handlers fire normally.
Option B: Add save handling to the IIFE itself
Insert a save-button check BEFORE the close-button check in the capture handler:
document.addEventListener('click', function(e) {
// Check save button FIRST
var saveBtn = e.target.closest('#save-site-settings');
if (saveBtn) {
e.stopPropagation();
// Save directly or call window.saveSiteSettings()
window.saveSiteSettings();
window.closeModal('site-settings-modal');
return;
}
// Then check close/cancel as before
var closeBtn = e.target.closest('[aria-label="Close"],[data-close-modal]');
// ...
}, true);
This works but couples the IIFE to specific button IDs. Option A is cleaner.
Option C: Direct onclick property override (nuclear option)
Set saveBtn.onclick from the IIFE (firing in target phase, before bubbling):
var btn = document.getElementById('save-site-settings');
if (btn) {
btn.onclick = function(e) {
e.stopImmediatePropagation();
// save logic
window.closeModal('site-settings-modal');
return false;
};
}
stopImmediatePropagation() prevents other listeners (from JS modules) on the same element from firing, avoiding duplicate saves.
Diagnosis Checklist
- Does the page have a capture-phase IIFE? Search for
addEventListener('click', ..., true)or},true)in inline<script>blocks. - Does the save button have
data-close-modal? Check the HTML and any JS that sets attributes. - Does the save button have an inline
onclickor anaddEventListenerhandler? - If yes to all three → this is the bug.
Related Pitfalls
- Double save: If you remove
data-close-modalfrom save but BOTH the IIFE's new save handler AND the JS module's addEventListener fire, you get duplicate saves and duplicate notifications. UsestopImmediatePropagation()or checkhasAttribute('onclick')to gate one of them. - Module timing: If the save handler is defined in a JS module (e.g.,
window.saveSiteSettingsinsettings.js), and the module loads after the IIFE, the IIFE may call it before it exists. Always checktypeof window.saveSiteSettings === 'function'before calling. stopPropagation()vsstopImmediatePropagation():stopPropagation()in the target phase prevents bubbling but NOT other target-phase handlers on the same element.stopImmediatePropagation()prevents ALL remaining handlers on the element regardless of phase.