From 62be81a74ebd4ea5cd9ae1c86473e0f6b53e597b Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 21 May 2025 14:58:08 -0400 Subject: [PATCH 1/2] feat: better alerts around theme awareness --- iframe-test.html | 94 +++++++++++++++++++++++++--------- src/theme/Layout/ThemeSync.tsx | 24 +++++++-- 2 files changed, 89 insertions(+), 29 deletions(-) diff --git a/iframe-test.html b/iframe-test.html index f5380aa74..a690d9e71 100644 --- a/iframe-test.html +++ b/iframe-test.html @@ -62,6 +62,21 @@ display: flex; align-items: center; } + .theme-status { + margin-left: 15px; + padding: 5px 10px; + border-radius: 4px; + font-size: 14px; + background-color: #f0f0f0; + } + .status-waiting { + color: #856404; + background-color: #fff3cd; + } + .status-ready { + color: #155724; + background-color: #d4edda; + } @@ -103,6 +118,7 @@

Iframe Test Page

+ Waiting for theme sync... @@ -123,13 +139,41 @@

Iframe Test Page

const iframeContainer = document.querySelector('.iframe-container'); const themeMode = document.getElementById('theme-mode'); const applyThemeButton = document.getElementById('apply-theme'); + const themeStatus = document.getElementById('theme-status'); + + // Theme sync status + let isThemeSyncReady = false; + + // Update theme status display + function updateThemeStatus(ready) { + isThemeSyncReady = ready; + if (ready) { + themeStatus.textContent = 'Theme sync ready'; + themeStatus.classList.remove('status-waiting'); + themeStatus.classList.add('status-ready'); + applyThemeButton.disabled = false; + } else { + themeStatus.textContent = 'Waiting for theme sync...'; + themeStatus.classList.add('status-waiting'); + themeStatus.classList.remove('status-ready'); + applyThemeButton.disabled = true; + } + } + + // Initialize theme status + updateThemeStatus(false); // Function to send theme to iframe function sendThemeToIframe(theme) { - iframe.contentWindow.postMessage( - { type: 'theme-update', theme: theme }, - '*' - ); + if (isThemeSyncReady) { + iframe.contentWindow.postMessage( + { type: 'theme-update', theme: theme }, + '*' + ); + console.log('Sent theme to iframe:', theme); + } else { + console.log('Theme sync not ready yet, cannot send theme'); + } } // Apply theme when button is clicked @@ -140,28 +184,21 @@

Iframe Test Page

// Auto-apply theme when iframe loads iframe.addEventListener('load', function() { - // Wait a moment for Docusaurus to initialize - setTimeout(() => { - sendThemeToIframe(themeMode.value); - }, 500); - }); - - // Load iframe with selected URL - loadButton.addEventListener('click', function() { - let url = customUrl.value.trim(); - - if (!url && presetUrls.value) { - url = presetUrls.value; - } + console.log('Iframe loaded, waiting for theme-ready message...'); + // Reset the theme sync status when iframe loads + updateThemeStatus(false); - if (url) { - // If URL doesn't start with http or /, add / at the beginning - if (!url.startsWith('http') && !url.startsWith('/')) { - url = '/' + url; + // Set a timeout for theme sync readiness + const syncTimeout = setTimeout(() => { + if (!isThemeSyncReady) { + console.warn('Theme sync ready message not received within timeout period'); + // Enable theme controls anyway after timeout + updateThemeStatus(true); } - - iframe.src = url; - } + }, 5000); + + // Store the timeout in a property so we can clear it if needed + iframe.syncTimeoutId = syncTimeout; }); // Listen for messages from the iframe @@ -171,6 +208,15 @@

Iframe Test Page

// Handle theme-ready message if (event.data && event.data.type === 'theme-ready') { console.log('Docusaurus is ready for theme sync'); + + // Clear any existing timeout + if (iframe.syncTimeoutId) { + clearTimeout(iframe.syncTimeoutId); + } + + // Update theme status + updateThemeStatus(true); + // Send current theme sendThemeToIframe(themeMode.value); } diff --git a/src/theme/Layout/ThemeSync.tsx b/src/theme/Layout/ThemeSync.tsx index 016b06979..3e92d4225 100644 --- a/src/theme/Layout/ThemeSync.tsx +++ b/src/theme/Layout/ThemeSync.tsx @@ -7,12 +7,26 @@ import { isInIframe } from "./utils"; */ export function ThemeSync(): JSX.Element | null { const [isInIframeState, setIsInIframeState] = useState(false); + const [lastSentTheme, setLastSentTheme] = useState(null); const { colorMode, setColorMode } = useColorMode(); useEffect(() => { setIsInIframeState(isInIframe()); }, []); + // Ensure theme system is ready and notify parent + useEffect(() => { + if (isInIframeState) { + // Wait a short time to ensure Docusaurus theme system is fully initialized + const readyTimer = setTimeout(() => { + // Send ready message to parent + window.parent.postMessage({ type: 'theme-ready' }, '*'); + }, 20); + + return () => clearTimeout(readyTimer); + } + }, [isInIframeState]); + // Handle theme message events from parent useEffect(() => { if (isInIframeState) { @@ -34,9 +48,6 @@ export function ThemeSync(): JSX.Element | null { // Add event listener window.addEventListener('message', handleMessage); - // Send ready message to parent - window.parent.postMessage({ type: 'theme-ready' }, '*'); - // Clean up return () => { window.removeEventListener('message', handleMessage); @@ -46,14 +57,17 @@ export function ThemeSync(): JSX.Element | null { // Notify parent when the theme changes useEffect(() => { - if (isInIframeState) { + if (isInIframeState && colorMode !== lastSentTheme) { // Send theme change notification to parent window.parent.postMessage({ type: 'theme-changed', theme: colorMode }, '*'); + + // Update the last sent theme + setLastSentTheme(colorMode); } - }, [colorMode, isInIframeState]); + }, [colorMode, isInIframeState, lastSentTheme]); // This component doesn't render anything return null; From a2b8df52605a74e6f85506ee8bad9619cef86acb Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 21 May 2025 15:03:34 -0400 Subject: [PATCH 2/2] feat: use iFrame hook --- src/hooks/useIframe.ts | 22 ++++++++++++++++++++++ src/theme/Layout/IframeNavigation.tsx | 10 +++------- src/theme/Layout/ThemeSync.tsx | 8 ++------ src/theme/Layout/index.tsx | 11 +++-------- src/theme/Layout/utils.ts | 6 ------ 5 files changed, 30 insertions(+), 27 deletions(-) create mode 100644 src/hooks/useIframe.ts delete mode 100644 src/theme/Layout/utils.ts diff --git a/src/hooks/useIframe.ts b/src/hooks/useIframe.ts new file mode 100644 index 000000000..1bfa635e9 --- /dev/null +++ b/src/hooks/useIframe.ts @@ -0,0 +1,22 @@ +import { useState, useEffect } from 'react'; + +/** + * Determines if the current window is running inside an iframe + */ +function isInIframe(): boolean { + return typeof window !== 'undefined' && window.self !== window.top; +} + +/** + * React hook that detects if the current page is displayed inside an iframe + * @returns boolean indicating if the current page is in an iframe + */ +export function useIframe(): boolean { + const [isInIframeState, setIsInIframeState] = useState(false); + + useEffect(() => { + setIsInIframeState(isInIframe()); + }, []); + + return isInIframeState; +} \ No newline at end of file diff --git a/src/theme/Layout/IframeNavigation.tsx b/src/theme/Layout/IframeNavigation.tsx index 31eafe4d3..8acd125f3 100644 --- a/src/theme/Layout/IframeNavigation.tsx +++ b/src/theme/Layout/IframeNavigation.tsx @@ -1,17 +1,13 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { useLocation } from "@docusaurus/router"; -import { isInIframe } from "./utils"; +import { useIframe } from "../../hooks/useIframe"; /** * Component that handles navigation events between iframe and parent window */ export function IframeNavigation(): JSX.Element | null { const location = useLocation(); - const [isInIframeState, setIsInIframeState] = useState(false); - - useEffect(() => { - setIsInIframeState(isInIframe()); - }, []); + const isInIframeState = useIframe(); // Handle navigation events useEffect(() => { diff --git a/src/theme/Layout/ThemeSync.tsx b/src/theme/Layout/ThemeSync.tsx index 3e92d4225..cd6323d6e 100644 --- a/src/theme/Layout/ThemeSync.tsx +++ b/src/theme/Layout/ThemeSync.tsx @@ -1,19 +1,15 @@ import React, { useEffect, useState } from "react"; import { useColorMode } from "@docusaurus/theme-common"; -import { isInIframe } from "./utils"; +import { useIframe } from "../../hooks/useIframe"; /** * Component that handles theme synchronization between iframe and parent window */ export function ThemeSync(): JSX.Element | null { - const [isInIframeState, setIsInIframeState] = useState(false); + const isInIframeState = useIframe(); const [lastSentTheme, setLastSentTheme] = useState(null); const { colorMode, setColorMode } = useColorMode(); - useEffect(() => { - setIsInIframeState(isInIframe()); - }, []); - // Ensure theme system is ready and notify parent useEffect(() => { if (isInIframeState) { diff --git a/src/theme/Layout/index.tsx b/src/theme/Layout/index.tsx index fa5cab5a9..b7803cee0 100644 --- a/src/theme/Layout/index.tsx +++ b/src/theme/Layout/index.tsx @@ -1,16 +1,11 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import Layout from "@theme-original/Layout"; import { ThemeSync } from "./ThemeSync"; import { IframeNavigation } from "./IframeNavigation"; -import { isInIframe } from "./utils"; +import { useIframe } from "../../hooks/useIframe"; export default function LayoutWrapper(props) { - const [isInIframeState, setIsInIframeState] = useState(false); - - useEffect(() => { - setIsInIframeState(isInIframe()); - }, []); - + const isInIframeState = useIframe(); const dataAttributes = isInIframeState ? { 'data-iframe': 'true' } : {}; return ( diff --git a/src/theme/Layout/utils.ts b/src/theme/Layout/utils.ts deleted file mode 100644 index 4011a2f86..000000000 --- a/src/theme/Layout/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Determines if the current window is running inside an iframe - */ -export function isInIframe(): boolean { - return typeof window !== 'undefined' && window.self !== window.top; -} \ No newline at end of file