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

9.9 KiB

Repair Orders — Inline IIFE Modal Handler Architecture

The repair-orders.html page (lines 1376-1427) has an inline IIFE that defines window.openModal, window.closeModal, and a capture-phase click handler. This is important because it runs OUTSIDE the ES module system and handles modal interactions before any module code executes.

Functions

window.closeModal(id)

  • Adds classList.add('hidden') + style.setProperty('display','none','important') + aria-hidden='true'
  • Uses style.setProperty with !important to defeat all CSS cascade issues
  • Brief lime green outline flash for visual diagnostic confirmation
  • Logs console.warn if element not found

window.openModal(id)

  • Removes hidden class + style.removeProperty('display') + aria-hidden='false'
  • Sets style.zIndex='100000'
  • Uses removeProperty to properly clear any !important display style set by closeModal

Capture-Phase Click Handler (line 1396)

document.addEventListener('click', function(e) {
    // 1. Close buttons: aria-label="Close" or data-close-modal attribute
    var closeBtn = e.target.closest('[aria-label="Close"],[data-close-modal]');
    if (closeBtn) {
        e.preventDefault(); e.stopPropagation();
        // Check for data-close-modal attribute (set by settings.js ensureSiteSettingsCloseAttributes)
        // Falls back to DOM traversal: closeBtn.closest('.modal,...')
    }
    
    // 2. Cancel buttons: id starts with 'cancel-'
    if (e.target.id && e.target.id.indexOf('cancel-') === 0) {
        // closeModal on parent .modal or .fixed
    }
    
    // 3. Click-away: clicking on modal backdrop
    // target.classList.contains('modal') or (fixed + inset-0 + bg-opacity)
}, true);  // CAPTURE phase — fires BEFORE bubble handlers

CRITICAL: stopPropagation() suppresses onclick attributes

Because this handler runs in the capture phase and calls e.stopPropagation(), any onclick="closeModal(...)" attribute on the matching button will never fire. The capture handler itself must call closeModal() — it can't rely on the button's onclick.

This is why the X button's inline onclick="closeModal('site-settings-modal')" is redundant when the capture handler works — but it serves as a belt-and-suspenders defense.

Broken aria-label selector (FIXED)

The original selector was:

var closeBtn = e.target.closest('[aria-label=\\\"Close\\\"],[data-close-modal]');

In the HTML source, \\\" is interpreted by the browser's HTML parser as \" in the JavaScript string. The resulting CSS selector was [aria-label=\"Close\"] which looks for the literal attribute value "Close" (WITH double-quote characters). No element has aria-label='"Close"' — they have aria-label="Close".

Fix: Write it without escaping:

var closeBtn = e.target.closest('[aria-label="Close"],[data-close-modal]');

Same fix applies to: closeBtn.closest('.modal,[role="dialog"],.fixed')

Event Flow When X Button Clicked

  1. Capture phase (inline IIFE): Matches [aria-label="Close"] → finds data-close-modal="site-settings-modal" (set by settings.js) → calls closeModal('site-settings-modal')e.stopPropagation() prevents bubble phase
  2. Target phase: Skipped (stopPropagation in capture)
  3. Bubble phase: Skipped (stopPropagation in capture)
    • onclick="closeModal('site-settings-modal')" — never fires
    • settings.js closeBtn.addEventListener('click', ...) — never fires

Event Flow When Cancel Button Clicked

  1. Capture phase: Matches id.startsWith('cancel-')closeModal(parentModal.id) → modal closes
  2. Bubble: Skipped

Event Flow When Save Button Clicked (AFTER FINAL FIX — onclick property override)

The capture-phase approach (IIFE catches #save-site-settings, calls save, closes) was unreliable — it worked on some pages but not repair-orders. The issue: even with stopPropagation() in capture, the async window.saveSiteSettings() call could fire but not complete before closeModal(). And the stopPropagation() prevents settings.js's own handler from running as a backup.

Final fix: Use a direct onclick property override that fires in the target phase with stopImmediatePropagation():

// In the IIFE (non-module script, runs after all ES modules):
var saveSettingsBtn = document.getElementById('save-site-settings');
if (saveSettingsBtn) {
    saveSettingsBtn.onclick = function(e) {
        e.stopImmediatePropagation();  // prevents settings.js's addEventListener
        if (typeof window.saveSiteSettings === 'function') {
            window.saveSiteSettings();  // async — save + close
        } 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;
    };
}

Execution order when Save is clicked:

  1. Capture phase (IIFE on document): no [data-close-modal], no aria-label="Close", id doesn't start with cancel-ignored
  2. Target phase: onclick property handler fires → stopImmediatePropagation() blocks all other listeners → tries window.saveSiteSettings() (async save + close via settings.js) or falls back to direct localStorage → modal closes
  3. Bubble phase: blocked by stopImmediatePropagation() — settings.js's addEventListener never fires ✓

Why stopImmediatePropagation() not stopPropagation()

stopPropagation() called during target phase prevents the bubble phase, but does NOT prevent other target-phase listeners on the same element. Since settings.js's setupUnifiedSiteSettingsModalHandlers also adds a click listener via addEventListener to the same #save-site-settings button, stopPropagation() alone would let it fire (double save + double notification).

stopImmediatePropagation() prevents all remaining listeners on the current element — so the settings.js handler never fires. ✓

Why onclick property not capture-phase handler

The capture-phase approach was unreliable because:

  • stopPropagation() in capture kills ALL other handlers, including settings.js's (no backup if the IIFE save fails)
  • The async window.saveSiteSettings() call starts but closeModal() runs immediately after — if the save throws, the error is swallowed and the modal closes without saving
  • The stopPropagation() prevents settings.js's full handler (with its own try/catch and notification) from ever running as a safety net

The onclick approach is more surgical — it only blocks the same element's other listeners, not the entire event chain.

hasAttribute('onclick') vs property distinction

Setting saveBtn.onclick = fn in JavaScript sets the DOM property, NOT the HTML onclick attribute. So saveBtn.hasAttribute('onclick') returns false even after the property is set.

This means settings.js's guard if (!saveBtn.hasAttribute('onclick')) will NOT skip the save button when onclick is set via JS property. This is why stopImmediatePropagation() is essential — it's the only way to prevent the double-fire when using the property-set approach.

Alternative: set saveBtn.setAttribute('onclick', '...') which makes hasAttribute('onclick') return true. But this requires generating a string of JS code, which is fragile. The stopImmediatePropagation() approach is cleaner.

PITFALL: data-close-modal on Save buttons kills the save

Before the fix, ensureSiteSettingsCloseAttributes() and dashboard.js both set data-close-modal on the Save button. This caused:

  1. Capture phase: IIFE matches [data-close-modal] → calls e.preventDefault() + e.stopPropagation() → closes modal immediately
  2. Target phase: NEVER REACHED (stopPropagation killed the event)
  3. Result: Modal closes but settings are NOT saved. User sees the modal vanish with no change.

Fix (two parts):

  1. Only set data-close-modal on CLOSE and CANCEL buttons — never on SAVE buttons.
  2. Add a dedicated e.target.closest('#save-site-settings') check in the IIFE capture handler BEFORE the close/cancel logic (see § "Event Flow When Save Button Clicked (AFTER FINAL FIX)" above). This ensures the save ALWAYS fires before the close, and works even if the ES module hasn't loaded yet (localStorage fallback).

In settings.js, setupUnifiedSiteSettingsModalHandlers() skips addEventListener on save buttons that have an inline onclick attribute (to prevent double saves). But with the capture-handler approach this guard is now secondary — stopPropagation() in the capture handler already prevents bubble-phase handlers from firing.

IDs Used

Element ID Defined By
Modal site-settings-modal repair-orders.html
X button close-site-settings repair-orders.html
Cancel button cancel-site-settings repair-orders.html
Save button save-site-settings repair-orders.html
Save function window.saveSiteSettings settings.js (bridge)
Close function window.closeModal inline IIFE
Open function window.openModal inline IIFE