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

6.5 KiB

Event Propagation: IIFE Capture-Phase Handler vs Save Buttons

The Bug

On pages with the Universal Modal Manager IIFE (Section 14), clicking a Save button in the site-settings-modal closes the modal without saving. The form appears to work (checkboxes toggle, dropdowns change) but clicking Save does nothing.

Root Cause

The IIFE adds a capture-phase listener on document:

document.addEventListener('click', function(e) {
    var closeBtn = e.target.closest('[aria-label="Close"],[data-close-modal]');
    if (closeBtn) {
        e.preventDefault();
        e.stopPropagation();  // ← KILLS all subsequent handlers
        // ... closes modal
    }
}, true);  // capture phase

ensureSiteSettingsCloseAttributes() in settings.js sets data-close-modal on all three buttons: close, cancel, AND save. When the user clicks Save:

  1. Capture phase: IIFE matches [data-close-modal] on save button
  2. Calls e.stopPropagation() — prevents target phase (inline onclick) and bubble phase (addEventListener handlers) from ever firing
  3. Calls window.closeModal('site-settings-modal') — modal closes
  4. The save handler (window.saveSiteSettings or setupUnifiedSiteSettingsModalHandlers.saveSettings) never runs

Diagnosis Signal

The save works on pages WITHOUT the IIFE (e.g., index.html, appointments.html, customers.html) but silently fails on pages WITH it (repair-orders.html).

To confirm: check if ensureSiteSettingsCloseAttributes() sets data-close-modal on the save button, and whether the page has the capture-phase IIFE:

# Check for data-close-modal on save button
grep -n 'save-site-settings.*data-close-modal' page.html

# Check for capture-phase IIFE
grep -n "addEventListener('click'" page.html | grep 'true)'

The Fix (Three Parts)

Part 1: Don't tag Save buttons with data-close-modal

In settings.js, ensureSiteSettingsCloseAttributes():

// BEFORE (broken): sets data-close-modal on save button too
const map = [{
    modalId: 'site-settings-modal',
    closeId: 'close-site-settings',
    cancelId: 'cancel-site-settings',
    saveId: 'save-site-settings'       // ← REMOVE THIS
}];

// AFTER (fixed): only close and cancel
const map = [{
    modalId: 'site-settings-modal',
    closeId: 'close-site-settings',
    cancelId: 'cancel-site-settings'
}];

Same fix in dashboard.js if it also sets data-close-modal on save.

Part 2: Remove inline onclick from Save button

Remove onclick="saveSiteSettings()" from the HTML. This prevents a double-fire when both the inline handler and the module-level addEventListener handler run.

Part 3: Set onclick property in the IIFE with stopImmediatePropagation()

At the end of the IIFE (after the capture-phase handler), set the save button's onclick property directly:

// In the IIFE, after the capture-phase addEventListener
var saveSettingsBtn = document.getElementById('save-site-settings');
if (saveSettingsBtn) {
    saveSettingsBtn.onclick = function(e) {
        e.stopImmediatePropagation();  // prevents module-level addEventListener
        if (typeof window.saveSiteSettings === 'function') {
            window.saveSiteSettings();
        } else {
            // Fallback: direct localStorage save
            var s = {};
            try { var ex = localStorage.getItem('quoteGenSettings_v5');
                  if (ex) s = JSON.parse(ex); } catch(ign) {}
            var gv = function(id, ck) {
                var el = document.getElementById(id);
                if (!el) return ck ? false : '';
                return ck ? !!el.checked : el.value;
            };
            s.darkMode = false;
            s.refreshRate = parseInt(gv('refresh-rate')) || 60;
            s.itemsPerPage = parseInt(gv('items-per-page')) || 25;
            s.desktopNotifications = gv('desktop-notifications', true);
            s.soundAlerts = gv('sound-alerts', true);
            s.criticalAlerts = gv('critical-alerts', true);
            s.notifyBeforePromised = parseInt(gv('notify-before-promised')) || 0;
            s.timezone = gv('timezone-select') || 'America/New_York';
            s.autoSaveForms = gv('auto-save-forms', true);
            localStorage.setItem('quoteGenSettings_v5', JSON.stringify(s));
        }
        window.closeModal('site-settings-modal');
        return false;
    };
}

Why onclick property instead of addEventListener?

Event phases execute in this order:

  1. Capture phase — IIFE on document (ignores save button now, no data-close-modal)
  2. Target phaseonclick property fires → saves → calls stopImmediatePropagation()
  3. Bubble phase — blocked by stopImmediatePropagation(); module-level addEventListener never fires

The onclick property fires in the target phase, AFTER the IIFE capture handler has already decided to ignore it, but BEFORE any bubbling-phase handlers. stopImmediatePropagation() then prevents duplicate saves from module-level listeners.

Alternative: Add save handling to the capture-phase IIFE

Instead of Part 3 above, you can also handle the save in the capture-phase handler itself (before the close/cancel checks):

document.addEventListener('click', function(e) {
    // Check save button FIRST
    var saveBtn = e.target.closest('#save-site-settings');
    if (saveBtn) {
        e.preventDefault();
        e.stopPropagation();  // prevents everything
        if (typeof window.saveSiteSettings === 'function') {
            window.saveSiteSettings();
        } else {
            // ... fallback save ...
        }
        window.closeModal('site-settings-modal');
        return;
    }
    // Then close/cancel checks...
}, true);

This is simpler but re-adding data-close-modal would be required, and the capture handler must be kept in sync with the save logic.

Full Event Trace (After Fix)

For the Save button after all three fixes:

Phase Handler Action
Capture (document) IIFE addEventListener(..., true) No data-close-modal match → ignored
Target saveSettingsBtn.onclick Reads form, saves to localStorage, calls closeModal()
Bubble Module addEventListener Blocked by stopImmediatePropagation()

For Close/Cancel (unchanged):

Phase Handler Action
Capture (document) IIFE Matches [aria-label="Close"] or [data-close-modal] → closes
Target onclick attribute Blocked by stopPropagation()
Bubble Module addEventListener Blocked by stopPropagation()