fix(panes): adopt into the real pane document, not about:blank

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 <topkoa@gmail.com>
This commit is contained in:
topkoa
2026-07-12 18:20:57 -04:00
parent 188bdaa837
commit 9e9f0fdac6
+37 -23
View File
@@ -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) {