Skip to content

Instantly share code, notes, and snippets.

@mekanics
Last active October 14, 2025 15:06
Show Gist options
  • Save mekanics/ceebb8f66afe9c49ecae446d9eda655a to your computer and use it in GitHub Desktop.
Save mekanics/ceebb8f66afe9c49ecae446d9eda655a to your computer and use it in GitHub Desktop.
React WindowPortal
import { ReactNode, useEffect, useRef, useState } from 'react'
import { createRoot, Root } from 'react-dom/client'
import { ResourceResolver, InjectionStrategy, HybridInjectionStrategy } from './ResourceResolver'
export type IndependentWindowConfig = {
width?: number
height?: number
left?: number
top?: number
resizable?: boolean
scrollbars?: boolean
menubar?: boolean
toolbar?: boolean
location?: boolean
status?: boolean
}
export type WindowMessage<T = unknown> = {
type: string
payload?: T
timestamp?: number
}
export type IndependentWindowPortalProps = {
title: string
children: ReactNode
onClose?: () => void
config?: IndependentWindowConfig
injectionStrategy?: InjectionStrategy
onError?: (error: Error) => void
onReady?: () => void
onMessage?: (message: WindowMessage) => void
allowedOrigins?: string[] // For postMessage security
}
const DEFAULT_CONFIG: IndependentWindowConfig = {
width: 800,
height: 600,
resizable: true,
scrollbars: true,
menubar: false,
toolbar: false,
location: false,
status: false,
}
/**
* IndependentWindowPortal - Renders a completely separate React application
* in a new window with its own React root.
*
* Key Differences from WindowPortalV2:
* - Creates a separate React root (not a portal)
* - Independent React context (no shared providers)
* - Own router, state management, etc.
* - Communication via postMessage API
* - Can be configured to survive parent window closing
*
* Use Cases:
* - Help documentation windows
* - Independent tools/utilities
* - Multi-window applications with isolated state
* - Pop-out widgets that don't need parent context
*/
export function IndependentWindowPortal({
title,
children,
onClose,
config = {},
injectionStrategy,
onError,
onReady,
onMessage,
allowedOrigins = ['*'],
}: IndependentWindowPortalProps) {
const windowRef = useRef<Window | null>(null)
const reactRootRef = useRef<Root | null>(null)
const [error, setError] = useState<Error | null>(null)
const onCloseRef = useRef(onClose)
const onMessageRef = useRef(onMessage)
const titleRef = useRef(title)
// Update refs without triggering effect
useEffect(() => {
onCloseRef.current = onClose
}, [onClose])
useEffect(() => {
onMessageRef.current = onMessage
}, [onMessage])
useEffect(() => {
titleRef.current = title
}, [title])
// Update title without recreating window
useEffect(() => {
if (windowRef.current && !windowRef.current.closed) {
windowRef.current.document.title = title
}
}, [title])
// Update content when children change
useEffect(() => {
if (reactRootRef.current && windowRef.current && !windowRef.current.closed) {
reactRootRef.current.render(children)
}
}, [children])
// Main initialization effect - only runs once
useEffect(() => {
const initializeWindow = async () => {
try {
const mergedConfig = { ...DEFAULT_CONFIG, ...config }
const features = buildWindowFeatures(mergedConfig)
// Open window
const newWindow = window.open('', '', features)
if (!newWindow) {
throw new Error('Failed to open window. Please check popup blocker settings.')
}
windowRef.current = newWindow
// Set title
newWindow.document.title = titleRef.current
// Inject styles with resource resolution
const resolver = new ResourceResolver()
const strategy = injectionStrategy || new HybridInjectionStrategy()
await strategy.inject(newWindow, resolver)
// Create root container
const rootContainer = newWindow.document.createElement('div')
rootContainer.id = 'independent-app-root'
rootContainer.style.cssText = 'width: 100%; height: 100%; margin: 0; padding: 0;'
newWindow.document.body.appendChild(rootContainer)
// Reset body styles for better default layout
newWindow.document.body.style.cssText = 'margin: 0; padding: 0; overflow: auto;'
// Create independent React root
const root = createRoot(rootContainer)
reactRootRef.current = root
// Render the independent app
root.render(children)
// Set up message handler for parent → child communication
const messageHandler = (event: MessageEvent) => {
// Security check
if (allowedOrigins.length > 0 && !allowedOrigins.includes('*')) {
if (!allowedOrigins.includes(event.origin)) {
console.warn('Message from unauthorized origin:', event.origin)
return
}
}
// Call onMessage callback
onMessageRef.current?.(event.data)
}
newWindow.addEventListener('message', messageHandler)
// Set up close handler
const unloadHandler = () => {
onCloseRef.current?.()
}
newWindow.addEventListener('beforeunload', unloadHandler)
// Mark as ready
onReady?.()
// Cleanup
return () => {
newWindow.removeEventListener('message', messageHandler)
newWindow.removeEventListener('beforeunload', unloadHandler)
if (reactRootRef.current) {
reactRootRef.current.unmount()
reactRootRef.current = null
}
if (!newWindow.closed) {
newWindow.close()
}
}
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred')
setError(error)
onError?.(error)
}
}
const cleanup = initializeWindow()
return () => {
cleanup.then(cleanupFn => cleanupFn?.())
}
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Error state - render nothing
if (error) {
return null
}
// This component renders nothing in the parent window
// All rendering happens in the child window via the independent React root
return null
}
/**
* Hook to communicate from child window back to parent
* Use this inside components rendered in IndependentWindowPortal
*/
export function useWindowMessaging() {
const sendToParent = <T,>(type: string, payload?: T) => {
if (!window.opener) {
console.warn('No parent window available')
return
}
const message: WindowMessage<T> = {
type,
payload,
timestamp: Date.now(),
}
window.opener.postMessage(message, '*')
}
const sendToChild = <T,>(childWindow: Window, type: string, payload?: T) => {
if (!childWindow || childWindow.closed) {
console.warn('Child window not available')
return
}
const message: WindowMessage<T> = {
type,
payload,
timestamp: Date.now(),
}
childWindow.postMessage(message, '*')
}
return {
sendToParent,
sendToChild,
}
}
/**
* Builds window.open() features string from config
*/
function buildWindowFeatures(config: IndependentWindowConfig): string {
const features: string[] = []
if (config.width) features.push(`width=${config.width}`)
if (config.height) features.push(`height=${config.height}`)
if (config.left !== undefined) features.push(`left=${config.left}`)
if (config.top !== undefined) features.push(`top=${config.top}`)
features.push(`resizable=${config.resizable ? 'yes' : 'no'}`)
features.push(`scrollbars=${config.scrollbars ? 'yes' : 'no'}`)
features.push(`menubar=${config.menubar ? 'yes' : 'no'}`)
features.push(`toolbar=${config.toolbar ? 'yes' : 'no'}`)
features.push(`location=${config.location ? 'yes' : 'no'}`)
features.push(`status=${config.status ? 'yes' : 'no'}`)
return features.join(',')
}
/**
* Resource Resolution Service
* Handles conversion of relative URLs to absolute URLs in CSS and other resources
* for use in window portals where the base URL context differs from the parent window
*/
export class ResourceResolver {
private baseUrl: string
constructor(baseUrl: string = window.location.href) {
this.baseUrl = baseUrl
}
/**
* Resolves a relative URL to an absolute URL based on the base URL
*/
private resolveUrl(url: string, contextUrl: string = this.baseUrl): string {
try {
return new URL(url, contextUrl).href
} catch {
// If URL parsing fails, return original
return url
}
}
/**
* Resolves all url() references in CSS content to absolute URLs
*/
resolveCssUrls(cssContent: string, stylesheetUrl?: string): string {
const baseForResolution = stylesheetUrl || this.baseUrl
// Match url() with various quote styles: url("..."), url('...'), url(...)
const urlPattern = /url\s*\(\s*(['"]?)([^'")]+)\1\s*\)/gi
return cssContent.replace(urlPattern, (match, quote, url) => {
// Skip data URLs and absolute URLs
if (url.startsWith('data:') || url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
return match
}
const resolvedUrl = this.resolveUrl(url.trim(), baseForResolution)
return `url(${quote}${resolvedUrl}${quote})`
})
}
/**
* Resolves URLs in @import statements
*/
private resolveCssImports(cssContent: string, stylesheetUrl?: string): string {
const baseForResolution = stylesheetUrl || this.baseUrl
// Match @import with various formats
const importPattern = /@import\s+(['"])([^'"]+)\1/gi
return cssContent.replace(importPattern, (match, quote, url) => {
// Skip absolute URLs
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
return match
}
const resolvedUrl = this.resolveUrl(url.trim(), baseForResolution)
return `@import ${quote}${resolvedUrl}${quote}`
})
}
/**
* Resolves all CSS resources (urls and imports)
*/
resolveAllCssResources(cssContent: string, stylesheetUrl?: string): string {
let resolved = this.resolveCssUrls(cssContent, stylesheetUrl)
resolved = this.resolveCssImports(resolved, stylesheetUrl)
return resolved
}
/**
* Fetches and resolves a stylesheet from a URL
*/
async fetchAndResolveStylesheet(stylesheetHref: string): Promise<string> {
try {
const response = await fetch(stylesheetHref)
if (!response.ok) {
throw new Error(`Failed to fetch stylesheet: ${response.statusText}`)
}
const cssContent = await response.text()
return this.resolveAllCssResources(cssContent, stylesheetHref)
} catch (error) {
console.error(`Error fetching stylesheet ${stylesheetHref}:`, error)
throw error
}
}
/**
* Processes inline style elements and returns resolved CSS
*/
processInlineStyle(styleElement: HTMLStyleElement): string {
const cssContent = styleElement.textContent || ''
return this.resolveAllCssResources(cssContent)
}
/**
* Checks if a URL is same-origin (can be fetched)
*/
private isSameOrigin(url: string): boolean {
try {
const urlObj = new URL(url, this.baseUrl)
return urlObj.origin === window.location.origin
} catch {
return false
}
}
/**
* Gets all stylesheets from the document with metadata
*/
getStylesheetInfo(): Array<{
type: 'link' | 'inline'
href?: string
content?: string
canFetch: boolean
element: HTMLLinkElement | HTMLStyleElement
}> {
const styles = document.querySelectorAll('link[rel="stylesheet"], style')
const info: Array<{
type: 'link' | 'inline'
href?: string
content?: string
canFetch: boolean
element: HTMLLinkElement | HTMLStyleElement
}> = []
styles.forEach(style => {
if (style.tagName === 'LINK') {
const link = style as HTMLLinkElement
info.push({
type: 'link',
href: link.href,
canFetch: this.isSameOrigin(link.href),
element: link,
})
} else if (style.tagName === 'STYLE') {
const styleEl = style as HTMLStyleElement
info.push({
type: 'inline',
content: styleEl.textContent || '',
canFetch: true, // Always can process inline
element: styleEl,
})
}
})
return info
}
}
/**
* Strategy interface for different injection approaches
*/
export interface InjectionStrategy {
inject(win: Window, resolver: ResourceResolver): Promise<void>
}
/**
* Inline strategy - converts all stylesheets to inline <style> with resolved URLs
* Pros: No additional HTTP requests, works cross-origin
* Cons: Can be slow, duplicates CSS
*/
export class InlineInjectionStrategy implements InjectionStrategy {
async inject(win: Window, resolver: ResourceResolver): Promise<void> {
const stylesheets = resolver.getStylesheetInfo()
for (const stylesheet of stylesheets) {
const style = win.document.createElement('style')
if (stylesheet.type === 'inline') {
// Process inline styles
style.textContent = resolver.processInlineStyle(stylesheet.element as HTMLStyleElement)
} else if (stylesheet.type === 'link' && stylesheet.canFetch && stylesheet.href) {
// Fetch and inline external stylesheets
try {
style.textContent = await resolver.fetchAndResolveStylesheet(stylesheet.href)
} catch (error) {
console.warn(`Could not fetch stylesheet ${stylesheet.href}, using link fallback`)
// Fallback to link
const link = win.document.createElement('link')
link.rel = 'stylesheet'
link.href = stylesheet.href
win.document.head.appendChild(link)
continue
}
} else if (stylesheet.type === 'link' && stylesheet.href) {
// Cross-origin or can't fetch - use link as-is
const link = win.document.createElement('link')
link.rel = 'stylesheet'
link.href = stylesheet.href
win.document.head.appendChild(link)
continue
}
win.document.head.appendChild(style)
}
}
}
/**
* Link strategy - uses <link> tags but sets base URL
* Pros: Fast, no duplication
* Cons: Requires base URL, may not work with all configurations
*/
export class LinkWithBaseStrategy implements InjectionStrategy {
async inject(win: Window, resolver: ResourceResolver): Promise<void> {
// Set base URL first
const base = win.document.createElement('base')
base.href = window.location.href
win.document.head.appendChild(base)
const stylesheets = resolver.getStylesheetInfo()
for (const stylesheet of stylesheets) {
if (stylesheet.type === 'link' && stylesheet.href) {
const link = win.document.createElement('link')
link.rel = 'stylesheet'
link.href = stylesheet.href
win.document.head.appendChild(link)
} else if (stylesheet.type === 'inline') {
// Still need to resolve URLs in inline styles
const style = win.document.createElement('style')
style.textContent = resolver.processInlineStyle(stylesheet.element as HTMLStyleElement)
win.document.head.appendChild(style)
}
}
}
}
/**
* Hybrid strategy - uses links for external, inline for embedded styles
* Pros: Balanced approach
* Cons: More complex
*/
export class HybridInjectionStrategy implements InjectionStrategy {
async inject(win: Window, resolver: ResourceResolver): Promise<void> {
const stylesheets = resolver.getStylesheetInfo()
for (const stylesheet of stylesheets) {
if (stylesheet.type === 'link' && stylesheet.href) {
// Use link for external stylesheets (browser handles caching)
const link = win.document.createElement('link')
link.rel = 'stylesheet'
link.href = stylesheet.href // Already absolute
win.document.head.appendChild(link)
} else if (stylesheet.type === 'inline') {
// Resolve and inline for embedded styles
const style = win.document.createElement('style')
style.textContent = resolver.processInlineStyle(stylesheet.element as HTMLStyleElement)
win.document.head.appendChild(style)
}
}
}
}
/**
* Usage examples for WindowPortal
* This file demonstrates various usage patterns
*/
import { useState } from 'react'
import { WindowPortalV2, InlineInjectionStrategy, LinkWithBaseStrategy, HybridInjectionStrategy } from './index'
/**
* Example 1: Basic usage with default settings
*/
export function BasicExample() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Portal</button>
{isOpen && (
<WindowPortalV2 title="My Portal" onClose={() => setIsOpen(false)}>
<div className="p-4">
<h1 className="text-2xl font-bold">Hello from Portal!</h1>
<p>This content renders in a new window.</p>
<p>All fonts and styles are properly resolved.</p>
</div>
</WindowPortalV2>
)}
</div>
)
}
/**
* Example 2: Custom window configuration
*/
export function CustomConfigExample() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Large Portal</button>
{isOpen && (
<WindowPortalV2
title="Large Portal"
onClose={() => setIsOpen(false)}
config={{
width: 1200,
height: 800,
left: 100,
top: 100,
resizable: true,
scrollbars: true,
}}>
<div className="min-h-screen bg-gray-100 p-8">
<h1 className="text-4xl">Large Window</h1>
</div>
</WindowPortalV2>
)}
</div>
)
}
/**
* Example 3: Using inline injection strategy for maximum compatibility
*/
export function InlineStrategyExample() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Open with Inline Strategy</button>
{isOpen && (
<WindowPortalV2
title="Inline Strategy Portal"
onClose={() => setIsOpen(false)}
injectionStrategy={new InlineInjectionStrategy()}>
<div className="p-4">
<p>Using inline strategy - all CSS is inlined with resolved URLs</p>
<p className="font-custom">Custom fonts will work!</p>
</div>
</WindowPortalV2>
)}
</div>
)
}
/**
* Example 4: Using link with base strategy for better performance
*/
export function LinkStrategyExample() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Open with Link Strategy</button>
{isOpen && (
<WindowPortalV2
title="Link Strategy Portal"
onClose={() => setIsOpen(false)}
injectionStrategy={new LinkWithBaseStrategy()}>
<div className="p-4">
<p>Using link strategy with base URL - faster loading</p>
</div>
</WindowPortalV2>
)}
</div>
)
}
/**
* Example 5: With error handling and ready callback
*/
export function ErrorHandlingExample() {
const [isOpen, setIsOpen] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [isReady, setIsReady] = useState(false)
const handleOpen = () => {
setError(null)
setIsReady(false)
setIsOpen(true)
}
return (
<div>
<button onClick={handleOpen}>Open Portal</button>
{error && <div className="mt-2 p-4 bg-red-100 text-red-700 rounded">Error: {error.message}</div>}
{isReady && <div className="mt-2 p-4 bg-green-100 text-green-700 rounded">Portal is ready!</div>}
{isOpen && (
<WindowPortalV2
title="Error Handling Portal"
onClose={() => setIsOpen(false)}
onError={err => {
setError(err)
setIsOpen(false)
}}
onReady={() => setIsReady(true)}>
<div className="p-4">
<h1>Portal Content</h1>
</div>
</WindowPortalV2>
)}
</div>
)
}
/**
* Example 6: With script copying (use carefully!)
*/
export function WithScriptsExample() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Open with Scripts</button>
{isOpen && (
<WindowPortalV2
title="Portal with Scripts"
onClose={() => setIsOpen(false)}
copyScripts={true} // Enables script copying
>
<div className="p-4">
<p>Scripts are copied (analytics/tracking excluded)</p>
</div>
</WindowPortalV2>
)}
</div>
)
}
/**
* Example 7: Dynamic title updates
*/
export function DynamicTitleExample() {
const [isOpen, setIsOpen] = useState(false)
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Portal</button>
{isOpen && (
<WindowPortalV2
title={`Portal - Count: ${count}`} // Title updates without recreating window
onClose={() => setIsOpen(false)}>
<div className="p-4">
<h1 className="text-2xl mb-4">Dynamic Title</h1>
<p className="mb-4">Count: {count}</p>
<button onClick={() => setCount(c => c + 1)} className="px-4 py-2 bg-blue-500 text-white rounded">
Increment (watch title)
</button>
</div>
</WindowPortalV2>
)}
</div>
)
}
/**
* Example 8: Hybrid strategy (recommended default)
*/
export function HybridStrategyExample() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Open with Hybrid Strategy</button>
{isOpen && (
<WindowPortalV2
title="Hybrid Strategy Portal"
onClose={() => setIsOpen(false)}
injectionStrategy={new HybridInjectionStrategy()} // Default
config={{
width: 800,
height: 600,
}}>
<div className="p-8 bg-gradient-to-br from-blue-50 to-purple-50">
<h1 className="text-3xl font-bold mb-4 text-gray-800">Hybrid Strategy</h1>
<p className="text-gray-600 mb-4">
This uses link tags for external stylesheets (fast, cached) and inlines embedded styles with resolved
URLs.
</p>
<ul className="list-disc list-inside space-y-2 text-gray-700">
<li>External stylesheets: link tags (cached)</li>
<li>Inline styles: resolved URLs</li>
<li>Fonts: properly loaded ✓</li>
<li>Background images: properly loaded ✓</li>
</ul>
</div>
</WindowPortalV2>
)}
</div>
)
}
import { FC, ReactNode, useState, useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { ResourceResolver, InjectionStrategy, HybridInjectionStrategy } from './ResourceResolver'
export type WindowPortalConfig = {
width?: number
height?: number
left?: number
top?: number
resizable?: boolean
scrollbars?: boolean
menubar?: boolean
toolbar?: boolean
location?: boolean
status?: boolean
}
export type WindowPortalProps = {
title: string
children: ReactNode
onClose?: () => void
config?: WindowPortalConfig
injectionStrategy?: InjectionStrategy
copyScripts?: boolean // Default false for safety
onError?: (error: Error) => void
onReady?: () => void
}
const DEFAULT_CONFIG: WindowPortalConfig = {
width: 600,
height: 400,
resizable: true,
scrollbars: true,
menubar: false,
toolbar: false,
location: false,
status: false,
}
/**
* WindowPortal V2 - Enhanced version with proper resource resolution
*
* Features:
* - Resolves relative URLs in CSS (fonts, images, etc.)
* - Configurable injection strategies
* - Better error handling
* - Stable lifecycle management
* - Optional script copying (disabled by default)
*/
export const WindowPortalV2: FC<WindowPortalProps> = ({
title,
children,
onClose,
config = {},
injectionStrategy,
copyScripts = false,
onError,
onReady,
}) => {
const [container, setContainer] = useState<HTMLDivElement | null>(null)
const [isReady, setIsReady] = useState(false)
const [error, setError] = useState<Error | null>(null)
const windowRef = useRef<Window | null>(null)
const onCloseRef = useRef(onClose)
const titleRef = useRef(title)
// Update refs without triggering effect
useEffect(() => {
onCloseRef.current = onClose
}, [onClose])
useEffect(() => {
titleRef.current = title
}, [title])
// Update title without recreating window
useEffect(() => {
if (windowRef.current && !windowRef.current.closed) {
windowRef.current.document.title = title
}
}, [title])
// Main initialization effect - only runs once
useEffect(() => {
const initializeWindow = async () => {
try {
const mergedConfig = { ...DEFAULT_CONFIG, ...config }
const features = buildWindowFeatures(mergedConfig)
// Open window
const newWindow = window.open('', '', features)
if (!newWindow) {
throw new Error('Failed to open window. Please check popup blocker settings.')
}
windowRef.current = newWindow
// Set title
newWindow.document.title = titleRef.current
// Inject styles with resource resolution
const resolver = new ResourceResolver()
const strategy = injectionStrategy || new HybridInjectionStrategy()
await strategy.inject(newWindow, resolver)
// Optionally copy scripts (careful with this!)
if (copyScripts) {
injectScripts(newWindow)
}
// Create container for portal
const containerDiv = newWindow.document.createElement('div')
containerDiv.id = 'portal-root'
newWindow.document.body.appendChild(containerDiv)
setContainer(containerDiv)
// Set up close handler
const unloadHandler = () => {
onCloseRef.current?.()
}
newWindow.addEventListener('beforeunload', unloadHandler)
// Mark as ready
setIsReady(true)
onReady?.()
// Cleanup
return () => {
newWindow.removeEventListener('beforeunload', unloadHandler)
if (!newWindow.closed) {
newWindow.close()
}
}
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred')
setError(error)
onError?.(error)
}
}
const cleanup = initializeWindow()
return () => {
cleanup.then(cleanupFn => cleanupFn?.())
}
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Error state
if (error) {
return null // Or render error UI if needed
}
// Loading state
if (!container || !isReady) {
return null // Or render loading indicator if needed
}
return createPortal(children, container)
}
/**
* Builds window.open() features string from config
*/
function buildWindowFeatures(config: WindowPortalConfig): string {
const features: string[] = []
if (config.width) features.push(`width=${config.width}`)
if (config.height) features.push(`height=${config.height}`)
if (config.left !== undefined) features.push(`left=${config.left}`)
if (config.top !== undefined) features.push(`top=${config.top}`)
features.push(`resizable=${config.resizable ? 'yes' : 'no'}`)
features.push(`scrollbars=${config.scrollbars ? 'yes' : 'no'}`)
features.push(`menubar=${config.menubar ? 'yes' : 'no'}`)
features.push(`toolbar=${config.toolbar ? 'yes' : 'no'}`)
features.push(`location=${config.location ? 'yes' : 'no'}`)
features.push(`status=${config.status ? 'yes' : 'no'}`)
return features.join(',')
}
/**
* Injects scripts from parent window (use with caution)
*/
function injectScripts(win: Window): void {
const mainWindowScripts = document.querySelectorAll('script')
// Filter out scripts that should not be copied
const scriptsToIgnore = [
/analytics/i,
/gtag/i,
/google-analytics/i,
/hotjar/i,
/segment/i,
/mixpanel/i,
// Add more patterns as needed
]
mainWindowScripts.forEach(script => {
// Check if script should be ignored
const src = script.src || script.textContent || ''
if (scriptsToIgnore.some(pattern => pattern.test(src))) {
return
}
const newScript = win.document.createElement('script')
if (script.src) {
newScript.src = script.src
newScript.async = (script as HTMLScriptElement).async
newScript.defer = (script as HTMLScriptElement).defer
} else {
newScript.textContent = script.textContent
}
win.document.body.appendChild(newScript)
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment