# 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) ```javascript 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: ```javascript 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: ```javascript 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()`: ```javascript // 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 |