131 lines
5.1 KiB
Markdown
131 lines
5.1 KiB
Markdown
# 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`:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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):
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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):
|
|
|
|
```javascript
|
|
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.
|
|
|
|
## Related Pitfalls
|
|
|
|
- **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.
|