('link[rel="stylesheet"], style').forEach((node) => {
const clone = node.cloneNode(true) as HTMLLinkElement | HTMLStyleElement
if (clone instanceof HTMLLinkElement && node instanceof HTMLLinkElement) clone.href = node.href
target.head.append(clone)
})
}
/**
* Neemt de al geladen lettertypegezichten van het hoofdvenster over.
*
* Een FontFace hangt niet aan een document, dus een gezicht dat hier al
* ingeladen is kan rechtstreeks aan de FontFaceSet van het nieuwe venster
* worden toegevoegd. Dat scheelt niet alleen een tweede download, het omzeilt
* ook dat het laden in een about:blank-document nooit voltooit.
*/
function copyLoadedFonts(target: Window): void {
const doel = target.document.fonts
if (!doel || typeof document.fonts === 'undefined') return
document.fonts.forEach((gezicht) => {
try {
doel.add(gezicht)
} catch {
// Al aanwezig, of dit gezicht laat zich niet overdragen.
}
})
}
function initialiseSecondaryDocument(target: Window, onClose: () => void): HTMLElement {
const targetDocument = target.document
targetDocument.title = 'GeoIntel · Analyseconsole'
targetDocument.documentElement.lang = document.documentElement.lang || 'nl'
targetDocument.head.replaceChildren()
const viewport = targetDocument.createElement('meta')
viewport.name = 'viewport'
viewport.content = 'width=device-width, initial-scale=1'
targetDocument.head.append(viewport)
copyDocumentStyles(targetDocument)
copyLoadedFonts(target)
const shell = targetDocument.createElement('div')
shell.className = 'secondary-display-shell'
const header = targetDocument.createElement('header')
header.className = 'secondary-display-header'
header.innerHTML = `
LIVE GEKOPPELDAnalyseconsole
Gesynchroniseerd met de kaart
`
const closeButton = targetDocument.createElement('button')
closeButton.type = 'button'
closeButton.className = 'secondary-display-close'
closeButton.textContent = 'Sluit console'
closeButton.addEventListener('click', onClose)
header.append(closeButton)
const content = targetDocument.createElement('main')
content.className = 'secondary-display-content'
content.id = 'secondary-display-content'
content.setAttribute('aria-label', 'Live analyse en resultaten')
shell.append(header, content)
targetDocument.body.replaceChildren(shell)
targetDocument.body.className = 'secondary-display-body'
return content
}
export function useSecondaryDisplay() {
const popupRef = useRef(null)
const monitorTimerRef = useRef(null)
const [container, setContainer] = useState(null)
const [error, setError] = useState(null)
const stopMonitoring = useCallback(() => {
if (monitorTimerRef.current !== null) {
window.clearInterval(monitorTimerRef.current)
monitorTimerRef.current = null
}
}, [])
const rememberGeometry = useCallback((popup: Window) => {
const geometry: SecondaryDisplayGeometry = {
left: popup.screenX,
top: popup.screenY,
width: popup.outerWidth,
height: popup.outerHeight,
}
try {
window.localStorage.setItem(SECONDARY_DISPLAY_GEOMETRY_KEY, JSON.stringify(geometry))
} catch {
// Window positioning is a convenience; storage restrictions must never
// break the live analysis console.
}
}, [])
const close = useCallback(() => {
const popup = popupRef.current
if (popup && !popup.closed) {
rememberGeometry(popup)
popup.close()
}
popupRef.current = null
setContainer(null)
stopMonitoring()
}, [rememberGeometry, stopMonitoring])
const open = useCallback(() => {
const existing = popupRef.current
if (existing && !existing.closed) {
existing.focus()
return
}
let saved: SecondaryDisplayGeometry | null = null
try {
saved = parseSecondaryDisplayGeometry(window.localStorage.getItem(SECONDARY_DISPLAY_GEOMETRY_KEY))
} catch {
saved = null
}
const popup = window.open('', SECONDARY_DISPLAY_NAME, secondaryDisplayFeatures(saved ?? defaultGeometry()))
if (!popup) {
setError('De browser blokkeerde het tweede venster. Sta pop-ups voor GeoIntel toe en probeer opnieuw.')
return
}
setError(null)
popupRef.current = popup
const content = initialiseSecondaryDocument(popup, close)
setContainer(content)
popup.focus()
stopMonitoring()
monitorTimerRef.current = window.setInterval(() => {
const current = popupRef.current
if (!current || current.closed) {
popupRef.current = null
setContainer(null)
stopMonitoring()
return
}
rememberGeometry(current)
}, 1_000)
}, [close, rememberGeometry, stopMonitoring])
useEffect(() => close, [close])
return {
container,
error,
isOpen: container !== null,
open,
close,
}
}
interface SecondaryDisplayTargetProps {
container: HTMLElement | null
children: ReactNode
}
export function SecondaryDisplayTarget({ container, children }: SecondaryDisplayTargetProps) {
return container ? createPortal(children, container) : children
}