/*html.ts - ESP3D WebUI helpers file
Copyright (c) 2021 Luc LEBOSSE. All rights reserved.
This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with This code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
interface IframeCache {
element: HTMLIFrameElement
contentWindow: Window | null
id: string
isVisible: boolean
}
// Cache for iframe elements to avoid repeated DOM queries
let iframeCache: IframeCache[] = []
let cacheInvalidated = true
// Debounce timer for batched cache updates
let cacheUpdateTimer: ReturnType | null = null
// Mark cache as invalid when extensions are added/removed
const invalidateIframeCache = (): void => {
cacheInvalidated = true
// Debounce cache updates to avoid thrashing during rapid changes
if (cacheUpdateTimer) {
clearTimeout(cacheUpdateTimer)
}
cacheUpdateTimer = setTimeout(() => {
updateIframeCache()
}, 100)
}
// Update the iframe cache
const updateIframeCache = (): void => {
const iframeList = document.querySelectorAll("iframe.extensionContainer")
iframeCache = Array.from(iframeList).map(element => ({
element,
contentWindow: element.contentWindow,
id: element.id,
// Cache visibility state for performance
isVisible: element.parentElement ? element.parentElement.style.display !== 'none' : false
}))
cacheInvalidated = false
}
// Setup MutationObserver to detect when iframes are added/removed
if (typeof window !== 'undefined') {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setupIframeObserver()
})
} else {
setupIframeObserver()
}
}
function setupIframeObserver(): void {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
// Check if any added/removed nodes are iframes or contain iframes
const hasIframeChanges = Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes))
.some(node => {
if (node.nodeType === 1) { // Element node
const element = node as Element
return element.tagName === 'IFRAME' ||
(element.classList && element.classList.contains('extensionContainer')) ||
(element.querySelector && element.querySelector('iframe.extensionContainer') !== null)
}
return false
})
if (hasIframeChanges) {
invalidateIframeCache()
break
}
}
}
})
observer.observe(document.body, {
childList: true,
subtree: true
})
}
interface MessageData {
type: string
content: any
id: string
}
const dispatchToExtensions = (type: string, data: any, targetId?: string): void => {
// Update cache if invalidated, or if any cached elements are no longer in the DOM
if (cacheInvalidated || iframeCache.some(cache => !cache.element || !cache.element.isConnected)) {
updateIframeCache()
}
// Broadcast to all extensions
for (const cache of iframeCache) {
// Skip hidden extensions for non-critical message types
// Always send notification, modal, and toast messages
const isCriticalMessage = type === 'notification' || type === 'modal' || type === 'toast' || type === 'translate' || type === 'icon'
if (isCriticalMessage || cache.isVisible) {
try {
if (cache.element.contentWindow) {
cache.element.contentWindow.postMessage(
{ type: type, content: data, id: targetId },
"*"
)
}
} catch (e) {
console.error(`Failed to send message to extension ${cache.id}:`, e)
}
}
}
}
// Extend Document interface for vendor-prefixed fullscreen APIs
declare global {
interface Document {
webkitFullscreenElement?: Element
mozFullScreenElement?: Element
msFullscreenElement?: Element
webkitFullscreenEnabled?: boolean
mozFullScreenEnabled?: boolean
msFullscreenEnabled?: boolean
}
}
const getFullscreenElement = (): Element | null => {
return (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement ||
null
)
}
const isFullscreenSupported = (): boolean => {
return (
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled ||
false
)
}
const isFullscreenActive = (): boolean => {
return (
!!document.fullscreenElement ||
!!document.webkitFullscreenElement ||
!!document.mozFullScreenElement ||
!!document.msFullscreenElement
)
}
export { dispatchToExtensions, getFullscreenElement, isFullscreenSupported, isFullscreenActive, invalidateIframeCache }
export type { IframeCache, MessageData }