improve(omnibox): harden shadow host against page removal
- commit
- a7f39d5a06f24e06c08ce5cc08d810325d34703d
- author
- bamen · · 2025-09-16 20:35 UTC
- committer
- bamen · · 2025-09-16 16:40 UTC
- parent
- 59edcdf
- signature
- unsigned
2 files · +57 -13
M CHANGELOG.md +1 -0
| @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. | |||
| 14 | 14 | - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided. | |
| 15 | 15 | - Configuration loading writes the missing `use_dom_zoom` flag into existing config files so DOM zoom defaults persist without manual edits. | |
| 16 | 16 | - Omnibox UI now relies on shared Tailwind theme tokens and the injected color-scheme service for light/dark parity. | |
| 17 | + | - Omnibox overlay now self-heals if hostile pages remove the injected shadow host, ensuring Ctrl/Cmd+L works everywhere. | |
| 17 | 18 | ||
| 18 | 19 | ## [0.8.0] - 2025-09-17 | |
| 19 | 20 | ||
| @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. | |||||
| 14 | - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided. | 14 | - Omnibox-triggered navigations now reuse the navigation controller, so saved zoom levels are applied before first paint and redundant lookups are avoided. | ||
| 15 | - Configuration loading writes the missing `use_dom_zoom` flag into existing config files so DOM zoom defaults persist without manual edits. | 15 | - Configuration loading writes the missing `use_dom_zoom` flag into existing config files so DOM zoom defaults persist without manual edits. | ||
| 16 | - Omnibox UI now relies on shared Tailwind theme tokens and the injected color-scheme service for light/dark parity. | 16 | - Omnibox UI now relies on shared Tailwind theme tokens and the injected color-scheme service for light/dark parity. | ||
| 17 | + | - Omnibox overlay now self-heals if hostile pages remove the injected shadow host, ensuring Ctrl/Cmd+L works everywhere. | |||
| 17 | 18 | ||||
| 18 | ## [0.8.0] - 2025-09-17 | 19 | ## [0.8.0] - 2025-09-17 | ||
| 19 | 20 | ||||
M gui/src/injected/modules/shadowHost.ts +56 -13
| @@ -14,18 +14,7 @@ import globalStyles from '../../styles/tailwind.css?inline'; | |||
| 14 | 14 | // Track shadow-root initialization to avoid duplicate reset injection | |
| 15 | 15 | const shadowResetApplied = new WeakSet<ShadowRoot>(); | |
| 16 | 16 | ||
| 17 | - | export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root'; | |
| 18 | - | ||
| 19 | - | /** | |
| 20 | - | * Ensure and return the global ShadowRoot used by injected UI. | |
| 21 | - | */ | |
| 22 | - | export function getGlobalShadowRoot(): ShadowRoot { | |
| 23 | - | // Create or find the host container | |
| 24 | - | let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null; | |
| 25 | - | if (!host) { | |
| 26 | - | host = document.createElement('div'); | |
| 27 | - | host.id = GLOBAL_SHADOW_HOST_ID; | |
| 28 | - | host.style.cssText = ` | |
| 17 | + | const HOST_STYLES = ` | |
| 29 | 18 | position: fixed !important; | |
| 30 | 19 | top: 0 !important; | |
| 31 | 20 | left: 0 !important; | |
| @@ -40,9 +29,63 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||
| 40 | 29 | isolation: isolate !important; | |
| 41 | 30 | contain: layout style !important; | |
| 42 | 31 | `; | |
| 43 | - | document.documentElement.appendChild(host); | |
| 32 | + | ||
| 33 | + | let cachedHost: HTMLElement | null = null; | |
| 34 | + | let hostObserver: MutationObserver | null = null; | |
| 35 | + | let hostKeepaliveTimer: number | null = null; | |
| 36 | + | ||
| 37 | + | function ensureHostPresence(host: HTMLElement): void { | |
| 38 | + | const docEl = document.documentElement; | |
| 39 | + | if (docEl && !host.isConnected) { | |
| 40 | + | docEl.appendChild(host); | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | if (typeof MutationObserver !== 'undefined') { | |
| 44 | + | if (!hostObserver) { | |
| 45 | + | hostObserver = new MutationObserver(() => { | |
| 46 | + | if (cachedHost && !cachedHost.isConnected && document.documentElement) { | |
| 47 | + | document.documentElement.appendChild(cachedHost); | |
| 48 | + | } | |
| 49 | + | }); | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | try { | |
| 53 | + | hostObserver.disconnect(); | |
| 54 | + | if (docEl) { | |
| 55 | + | hostObserver.observe(docEl, { childList: true }); | |
| 56 | + | } | |
| 57 | + | } catch { | |
| 58 | + | // MutationObserver might not be allowed by the page; fall back to polling in that case. | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | if (hostKeepaliveTimer === null && typeof window !== 'undefined') { | |
| 63 | + | hostKeepaliveTimer = window.setInterval(() => { | |
| 64 | + | if (cachedHost && !cachedHost.isConnected && document.documentElement) { | |
| 65 | + | document.documentElement.appendChild(cachedHost); | |
| 66 | + | } | |
| 67 | + | }, 2000); | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root'; | |
| 72 | + | ||
| 73 | + | /** | |
| 74 | + | * Ensure and return the global ShadowRoot used by injected UI. | |
| 75 | + | */ | |
| 76 | + | export function getGlobalShadowRoot(): ShadowRoot { | |
| 77 | + | // Create or find the host container | |
| 78 | + | let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null; | |
| 79 | + | if (!host) { | |
| 80 | + | host = document.createElement('div'); | |
| 81 | + | host.id = GLOBAL_SHADOW_HOST_ID; | |
| 44 | 82 | } | |
| 45 | 83 | ||
| 84 | + | host.style.cssText = HOST_STYLES; | |
| 85 | + | ||
| 86 | + | cachedHost = host; | |
| 87 | + | ensureHostPresence(host); | |
| 88 | + | ||
| 46 | 89 | const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' }); | |
| 47 | 90 | ||
| 48 | 91 | // Reflect current theme from document root to the shadow host so :host(.dark) works | |
| @@ -14,18 +14,7 @@ import globalStyles from '../../styles/tailwind.css?inline'; | |||||
| 14 | // Track shadow-root initialization to avoid duplicate reset injection | 14 | // Track shadow-root initialization to avoid duplicate reset injection | ||
| 15 | const shadowResetApplied = new WeakSet<ShadowRoot>(); | 15 | const shadowResetApplied = new WeakSet<ShadowRoot>(); | ||
| 16 | 16 | ||||
| 17 | - | export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root'; | 17 | + | const HOST_STYLES = ` |
| 18 | - | ||||
| 19 | - | /** | |||
| 20 | - | * Ensure and return the global ShadowRoot used by injected UI. | |||
| 21 | - | */ | |||
| 22 | - | export function getGlobalShadowRoot(): ShadowRoot { | |||
| 23 | - | // Create or find the host container | |||
| 24 | - | let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null; | |||
| 25 | - | if (!host) { | |||
| 26 | - | host = document.createElement('div'); | |||
| 27 | - | host.id = GLOBAL_SHADOW_HOST_ID; | |||
| 28 | - | host.style.cssText = ` | |||
| 29 | position: fixed !important; | 18 | position: fixed !important; | ||
| 30 | top: 0 !important; | 19 | top: 0 !important; | ||
| 31 | left: 0 !important; | 20 | left: 0 !important; | ||
| @@ -40,9 +29,63 @@ export function getGlobalShadowRoot(): ShadowRoot { | |||||
| 40 | isolation: isolate !important; | 29 | isolation: isolate !important; | ||
| 41 | contain: layout style !important; | 30 | contain: layout style !important; | ||
| 42 | `; | 31 | `; | ||
| 43 | - | document.documentElement.appendChild(host); | 32 | + | |
| 33 | + | let cachedHost: HTMLElement | null = null; | |||
| 34 | + | let hostObserver: MutationObserver | null = null; | |||
| 35 | + | let hostKeepaliveTimer: number | null = null; | |||
| 36 | + | ||||
| 37 | + | function ensureHostPresence(host: HTMLElement): void { | |||
| 38 | + | const docEl = document.documentElement; | |||
| 39 | + | if (docEl && !host.isConnected) { | |||
| 40 | + | docEl.appendChild(host); | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | if (typeof MutationObserver !== 'undefined') { | |||
| 44 | + | if (!hostObserver) { | |||
| 45 | + | hostObserver = new MutationObserver(() => { | |||
| 46 | + | if (cachedHost && !cachedHost.isConnected && document.documentElement) { | |||
| 47 | + | document.documentElement.appendChild(cachedHost); | |||
| 48 | + | } | |||
| 49 | + | }); | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | try { | |||
| 53 | + | hostObserver.disconnect(); | |||
| 54 | + | if (docEl) { | |||
| 55 | + | hostObserver.observe(docEl, { childList: true }); | |||
| 56 | + | } | |||
| 57 | + | } catch { | |||
| 58 | + | // MutationObserver might not be allowed by the page; fall back to polling in that case. | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | if (hostKeepaliveTimer === null && typeof window !== 'undefined') { | |||
| 63 | + | hostKeepaliveTimer = window.setInterval(() => { | |||
| 64 | + | if (cachedHost && !cachedHost.isConnected && document.documentElement) { | |||
| 65 | + | document.documentElement.appendChild(cachedHost); | |||
| 66 | + | } | |||
| 67 | + | }, 2000); | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | export const GLOBAL_SHADOW_HOST_ID = 'dumber-ui-root'; | |||
| 72 | + | ||||
| 73 | + | /** | |||
| 74 | + | * Ensure and return the global ShadowRoot used by injected UI. | |||
| 75 | + | */ | |||
| 76 | + | export function getGlobalShadowRoot(): ShadowRoot { | |||
| 77 | + | // Create or find the host container | |||
| 78 | + | let host = document.getElementById(GLOBAL_SHADOW_HOST_ID) as HTMLElement | null; | |||
| 79 | + | if (!host) { | |||
| 80 | + | host = document.createElement('div'); | |||
| 81 | + | host.id = GLOBAL_SHADOW_HOST_ID; | |||
| 44 | } | 82 | } | ||
| 45 | 83 | ||||
| 84 | + | host.style.cssText = HOST_STYLES; | |||
| 85 | + | ||||
| 86 | + | cachedHost = host; | |||
| 87 | + | ensureHostPresence(host); | |||
| 88 | + | ||||
| 46 | const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' }); | 89 | const shadowRoot = host.shadowRoot ?? host.attachShadow({ mode: 'open' }); | ||
| 47 | 90 | ||||
| 48 | // Reflect current theme from document root to the shadow host so :host(.dark) works | 91 | // Reflect current theme from document root to the shadow host so :host(.dark) works | ||