From 6fc0f1ccd7076eca8099a4009261083ac0fed8a2 Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 24 Aug 2026 12:51:35 +0200 Subject: [PATCH] fix(app): remove popout stylesheets when the main document drops them (#1949) --- .../components/popout/PopoutWindowDocument.ts | 65 +++++++++++++------ 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/fluxer_app/src/features/voice/components/popout/PopoutWindowDocument.ts b/fluxer_app/src/features/voice/components/popout/PopoutWindowDocument.ts index c2d759651..7ed373886 100644 --- a/fluxer_app/src/features/voice/components/popout/PopoutWindowDocument.ts +++ b/fluxer_app/src/features/voice/components/popout/PopoutWindowDocument.ts @@ -40,11 +40,26 @@ function syncRootClassAttribute(sourceValue: string, targetRoot: HTMLElement): v targetRoot.removeAttribute('class'); } -function inlineStylesheetFallback(sourceLink: HTMLLinkElement, targetDocument: Document): void { +interface DocumentStylesheetMirror { + mirroredLinks: WeakMap; + mirroredStyles: WeakMap; +} + +const documentMirrors = new WeakMap(); + +function getDocumentMirror(targetDocument: Document): DocumentStylesheetMirror { + const existing = documentMirrors.get(targetDocument); + if (existing) return existing; + const stylesheetMirror: DocumentStylesheetMirror = {mirroredLinks: new WeakMap(), mirroredStyles: new WeakMap()}; + documentMirrors.set(targetDocument, stylesheetMirror); + return stylesheetMirror; +} + +function inlineStylesheetFallback(sourceLink: HTMLLinkElement, targetDocument: Document): HTMLStyleElement | null { let cssText = ''; try { const rules = sourceLink.sheet?.cssRules; - if (!rules) return; + if (!rules) return null; const ruleTexts: Array = []; for (let index = 0; index < rules.length; index += 1) { ruleTexts.push(rules[index].cssText); @@ -52,14 +67,16 @@ function inlineStylesheetFallback(sourceLink: HTMLLinkElement, targetDocument: D cssText = ruleTexts.join('\n'); } catch (error) { logger.warn('Failed to inline popout stylesheet fallback', {href: sourceLink.href, error}); - return; + return null; } const styleElement = targetDocument.createElement('style'); styleElement.textContent = cssText; targetDocument.head.appendChild(styleElement); + return styleElement; } function copyLinkStylesheet(sourceLink: HTMLLinkElement, targetDocument: Document): void { + const stylesheetMirror = getDocumentMirror(targetDocument); const linkElement = targetDocument.createElement('link'); linkElement.rel = 'stylesheet'; linkElement.href = sourceLink.href; @@ -68,11 +85,23 @@ function copyLinkStylesheet(sourceLink: HTMLLinkElement, targetDocument: Documen } linkElement.addEventListener('error', () => { linkElement.remove(); - inlineStylesheetFallback(sourceLink, targetDocument); + stylesheetMirror.mirroredLinks.delete(sourceLink); + const fallbackStyle = inlineStylesheetFallback(sourceLink, targetDocument); + if (fallbackStyle) { + stylesheetMirror.mirroredStyles.set(sourceLink, fallbackStyle); + } }); + stylesheetMirror.mirroredLinks.set(sourceLink, linkElement); targetDocument.head.appendChild(linkElement); } +function copyStyleElement(sourceStyle: HTMLStyleElement, targetDocument: Document): void { + const styleElement = targetDocument.createElement('style'); + styleElement.textContent = sourceStyle.textContent; + getDocumentMirror(targetDocument).mirroredStyles.set(sourceStyle, styleElement); + targetDocument.head.appendChild(styleElement); +} + export function copyStylesheetsIntoDocument(sourceDocument: Document, targetDocument: Document): void { const styleNodes = sourceDocument.querySelectorAll('link[rel="stylesheet"], style'); const copyCount = Math.min(styleNodes.length, POPOUT_STYLESHEET_COPY_MAX); @@ -82,31 +111,24 @@ export function copyStylesheetsIntoDocument(sourceDocument: Document, targetDocu copyLinkStylesheet(node, targetDocument); continue; } - const styleElement = targetDocument.createElement('style'); - styleElement.textContent = node.textContent; - targetDocument.head.appendChild(styleElement); + if (node instanceof HTMLStyleElement) { + copyStyleElement(node, targetDocument); + } } } -function copyStyleElement(sourceStyle: HTMLStyleElement, targetDocument: Document): HTMLStyleElement { - const styleElement = targetDocument.createElement('style'); - styleElement.textContent = sourceStyle.textContent; - targetDocument.head.appendChild(styleElement); - return styleElement; -} - export function observeDocumentStylesheets(sourceDocument: Document, targetDocument: Document): () => void { if (typeof MutationObserver === 'undefined') { return () => undefined; } - const mirroredStyles = new WeakMap(); + const stylesheetMirror = getDocumentMirror(targetDocument); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'characterData' || mutation.target instanceof HTMLStyleElement) { const sourceStyle = mutation.target instanceof HTMLStyleElement ? mutation.target : mutation.target.parentElement; if (sourceStyle instanceof HTMLStyleElement) { - const mirroredStyle = mirroredStyles.get(sourceStyle); + const mirroredStyle = stylesheetMirror.mirroredStyles.get(sourceStyle); if (mirroredStyle) { mirroredStyle.textContent = sourceStyle.textContent; } @@ -118,14 +140,19 @@ export function observeDocumentStylesheets(sourceDocument: Document, targetDocum continue; } if (node instanceof HTMLStyleElement) { - mirroredStyles.set(node, copyStyleElement(node, targetDocument)); + copyStyleElement(node, targetDocument); } } for (const node of Array.from(mutation.removedNodes)) { - const mirroredStyle = mirroredStyles.get(node); + const mirroredStyle = stylesheetMirror.mirroredStyles.get(node); if (mirroredStyle) { mirroredStyle.remove(); - mirroredStyles.delete(node); + stylesheetMirror.mirroredStyles.delete(node); + } + const mirroredLink = stylesheetMirror.mirroredLinks.get(node); + if (mirroredLink) { + mirroredLink.remove(); + stylesheetMirror.mirroredLinks.delete(node); } } }