diff --git a/resources/js/app.js b/resources/js/app.js index a4878d9..129251f 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -77,6 +77,47 @@ document.addEventListener('livewire:init', () => { }); document.addEventListener('alpine:init', () => { + // ── wire-elements/modal close-path guard (vendor bug workaround) ───── + // The package's modal.js closingModal() reads components[activeComponent].name + // with NO null-guard — unlike its sibling getActiveComponentModalAttribute(), + // which DOES guard the very same access (`components[activeComponent] !== undefined`). + // When Alpine's `activeComponent` and the Livewire-side `components` map are + // momentarily out of sync at close time — a window that @persist'ing the modal + // manager across wire:navigate widens (the manager + its Alpine state survive a + // navigation while Livewire re-hydrates `components`) — that read throws + // TypeError: Cannot read properties of undefined (reading 'name') + // on the click-away path (closeModalOnClickAway → closingModal). The modal still + // closes, so it is console noise, but it violates R12 (zero console errors). + // + // We wrap the global factory to apply the SAME guard the vendor already uses + // elsewhere: when there is no live component for the active id there is nothing to + // notify and nothing can veto the close, so we report "closing allowed" (return + // true) and let closeModal() proceed exactly as before. The in-sync path is + // untouched (delegates to the original closingModal, preserving a modal's ability + // to veto its own close). Runs at alpine:init — before Alpine evaluates the + // persisted manager's x-data="LivewireUIModal()" — so every instance is guarded. + // Keeps the @persist navigate fix and the spinner/error-toast UX + // intact (this file is the only thing touched). + const modalFactory = window.LivewireUIModal; + if (typeof modalFactory === 'function' && !modalFactory.__clusevGuarded) { + const guarded = function () { + const data = modalFactory(); + const original = data.closingModal; + if (typeof original === 'function') { + data.closingModal = function (eventName) { + const components = this.$wire?.get('components'); + if (!components || components[this.activeComponent] === undefined) { + return true; // desync / already torn down: nothing to notify, allow close + } + return original.call(this, eventName); + }; + } + return data; + }; + guarded.__clusevGuarded = true; + window.LivewireUIModal = guarded; + } + // ── Modal trigger ─────────────────────────────────────────────────── // Reused by . Gives EVERY modal-opening control an immediate // spinner (pending flips synchronously on click — no round-trip needed) that clears