Files
2026-07-12 10:17:17 -04:00

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 onclick or addEventListener exists)

Root Cause

When the user clicks Save:

  1. Capture phase — IIFE handler on document fires FIRST. Matches [data-close-modal] on the save button. Calls closeModal() immediately. Calls e.stopPropagation() which prevents the event from ever reaching the target phase or bubble phase.
  2. Target phase — NEVER REACHED. The inline onclick="saveSiteSettings()" never fires.
  3. 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

  1. Does the page have a capture-phase IIFE? Search for addEventListener('click', ..., true) or },true) in inline <script> blocks.
  2. Does the save button have data-close-modal? Check the HTML and any JS that sets attributes.
  3. Does the save button have an inline onclick or an addEventListener handler?
  4. If yes to all three → this is the bug.
  • Double save: If you remove data-close-modal from save but BOTH the IIFE's new save handler AND the JS module's addEventListener fire, you get duplicate saves and duplicate notifications. Use stopImmediatePropagation() or check hasAttribute('onclick') to gate one of them.
  • Module timing: If the save handler is defined in a JS module (e.g., window.saveSiteSettings in settings.js), and the module loads after the IIFE, the IIFE may call it before it exists. Always check typeof window.saveSiteSettings === 'function' before calling.
  • stopPropagation() vs stopImmediatePropagation(): 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.