From 9e9f0fdac6845a81776c1d29d95911be411e61f3 Mon Sep 17 00:00:00 2001 From: topkoa Date: Sun, 12 Jul 2026 18:20:57 -0400 Subject: [PATCH] fix(panes): adopt into the real pane document, not about:blank MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both pop-outs opened blank. window.open() returns immediately, and the window it hands back already has a document — an `about:blank` one, whose readyState is 'complete'. So the host cheerfully adopted the panel into THAT, it worked for a few milliseconds, and then /pane finished loading, replaced the document, and took the panel with it. Blank window, vanished element. Waiting for 'load' is no better: it may already have fired for about:blank before we could listen. So don't trust readyState and don't trust 'load' — wait for the one thing that exists only in the document we actually want: pane.html's #fb-pane-root. Poll for it (guarding the cross-document window while it is mid-swap), give up after 10s, and on failure bring the element home rather than stranding it in a window that never loaded. Also drop the popup's 'beforeunload' listener: it was registered on the about:blank window and discarded along with it, so it never fired. The `closed` poll is what notices a user shutting a pane window — as it must be anyway, since a crashed renderer never says goodbye either. Signed-off-by: topkoa --- static/panes/pane-window-host.js | 60 ++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/static/panes/pane-window-host.js b/static/panes/pane-window-host.js index 767dc0b..e602a66 100644 --- a/static/panes/pane-window-host.js +++ b/static/panes/pane-window-host.js @@ -68,11 +68,33 @@ } catch (e) { /* non-fatal */ } } - function _adopt(w, spec, el) { + // Wait for the REAL pane document. + // + // window.open() returns immediately, with an `about:blank` document that is + // already readyState 'complete'. Adopt into that and it works for a few + // milliseconds — and then /pane finishes loading, replaces the document, and + // takes the panel with it. The window is left blank and the element is gone. + // + // So we do not trust readyState, and we do not trust 'load' (which may have + // fired for about:blank before we could listen). We wait for the one thing that + // only exists in the document we actually want: pane.html's #fb-pane-root. + function _whenReady(w, onReady, onFail) { + const deadline = performance.now() + 10000; + const tick = () => { + if (w.closed) return; + let root = null; + try { root = w.document && w.document.getElementById('fb-pane-root'); } + catch (e) { root = null; } // mid-navigation: the document is being swapped + if (root) { onReady(root); return; } + if (performance.now() > deadline) { onFail(new Error('the pane window never loaded')); return; } + setTimeout(tick, 25); + }; + tick(); + } + + function _adopt(w, root, spec, el) { const doc = w.document; _copyStyles(doc); - - const root = doc.getElementById('fb-pane-root') || doc.body; // The panel was almost certainly a fixed/absolute overlay pinned to a // corner of the app. In a window of its own that positioning is nonsense — // it would sit 72px from the top of a 380px window, still 288px wide, still @@ -111,30 +133,22 @@ wins.set(spec.id, w); _startReaper(); - // The document may or may not have parsed yet. Both paths must work, and - // must not run twice — a double adopt would move the element into the - // window and then move it in again, firing the plugin's own observers for - // no reason. - let done = false; - const go = () => { - if (done || w.closed) return; - done = true; - try { _adopt(w, spec, el); } + _whenReady(w, (root) => { + try { _adopt(w, root, spec, el); } catch (e) { console.error('[panes] failed to move', spec.id, 'into its window', e); - panes.close(spec.id); // returns the element home + panes.close(spec.id); // brings the element home } - }; - if (w.document && w.document.readyState === 'complete') go(); - else w.addEventListener('load', go, { once: true }); - - // The window is ours and must not outlive the document that owns the - // element inside it. - w.addEventListener('beforeunload', () => { - // Only react to the user closing the window — not to us closing it - // during a dock, which has already taken the element back. - if (wins.get(spec.id) === w && panes.isOpen(spec.id)) setTimeout(() => panes.close(spec.id), 0); + }, (err) => { + console.error('[panes]', spec.id, err); + panes.close(spec.id); // never strand the element in a dead window }); + + // Note there is no 'beforeunload' listener on the popup. A listener added + // now would be attached to its throwaway about:blank window and thrown away + // with it when /pane loads. The `closed` poll above is what notices the user + // shutting a pane window — and it has to be, since a crashed renderer never + // gets to say goodbye either. } function unplace(id, el) {