import { useState, useEffect, useRef, useCallback } from '@wordpress/element'; import { debounce } from 'lodash'; import { useGlobalsStore } from '@library/state/global'; import { requiredCSSVars } from '@library/util/css'; import { hasCSSVar } from '@library/util/dom'; import { useIsMounted } from './useIsMounted'; const originalHeights = new WeakMap(); export const usePreviewIframe = ({ container, onIFrameLoaded, ready, loadDelay, }) => { const isMounted = useIsMounted(); const [waitForIframe, setWaitForIframe] = useState(0); const [iFrame, setIFrame] = useState(null); const [maybeOk, setMaybeOk] = useState(false); const isUpdating = useRef(false); const { addMissingCSSVar } = useGlobalsStore(); const addMissingThemeVars = useCallback( (frame) => { if (!frame?.contentDocument) return; const styles = getComputedStyle(frame.contentDocument.documentElement); const styleSheets = frame.contentDocument.styleSheets; for (let key in requiredCSSVars) { // CSS variable was found applied somewhere if (styles.getPropertyValue(key)) continue; const varUsed = Array.from(styleSheets) .filter((sheet) => { try { return sheet.cssRules; } catch (error) { return false; } }) .some((sheet) => hasCSSVar(key, sheet.cssRules)); // CSS variable was found in a stylesheet somewhere if (varUsed) continue; // Add the missing variable to the global state to be imported later addMissingCSSVar(key); // Dive into iframe and add the variables to at the :root scope frame.contentDocument.documentElement.style.setProperty( key, requiredCSSVars[key], ); } }, [addMissingCSSVar], ); const updateCoverBlocks = useCallback(async (frame, cntnr) => { const ft = frame.getBoundingClientRect().top; const ct = cntnr.getBoundingClientRect().top; // If they have scrolled, don't mess with it if (ft < ct) return; isUpdating.current = true; let scale = cntnr .querySelector('[style*="scale"]') ?.style?.transform?.match(/scale\((.*?)\)/)?.[1]; scale = scale ? parseFloat(scale) : null; const cntnrHScaled = cntnr.offsetHeight / (scale ?? 1); frame.style.setProperty('max-height', `${cntnrHScaled}px`, 'important'); const coverBlocks = frame.contentDocument.querySelectorAll('.wp-block-cover'); for (const el of coverBlocks) { if (!originalHeights.has(el)) { // Cache the original 'vh' value originalHeights.set(el, el.style.minHeight); } // Reapply the original 'vh' value so it can be used in computations el.style.minHeight = originalHeights.get(el); } cntnr.offsetHeight; // Force a reflow // Give the browser time to paint await new Promise((resolve) => requestAnimationFrame(resolve)); await new Promise((resolve) => requestAnimationFrame(resolve)); for (const el of coverBlocks) { if (!frame.contentDocument?.defaultView) return; // Get the computed height in px and use it for your calculation const computedHeight = parseFloat( frame.contentDocument.defaultView.getComputedStyle(el).height, ); el.offsetHeight; // Force a reflow el.style.minHeight = computedHeight > 500 ? '500px' : computedHeight + 'px'; } frame.style.setProperty('max-height', 'none', 'important'); isUpdating.current = false; }, []); useEffect(() => { if (!ready) return; // continuously check for iframe const interval = setTimeout(() => { if (iFrame) return; const frame = container?.querySelector('iframe[title]'); // If not found, retry by updating state if (!frame) return setWaitForIframe((prev) => prev + 1); setIFrame(frame); requestAnimationFrame(() => onIFrameLoaded(frame, container)); }, 100); return () => clearTimeout(interval); }, [iFrame, ready, waitForIframe, container, onIFrameLoaded]); useEffect(() => { setMaybeOk(false); // After the iFrame is found, wait for it to load // Note: using load event is not reliable if (!iFrame?.contentDocument) return; const config = { attributes: false, childList: true, subtree: true, }; // Run once in case the iframe is already loaded requestAnimationFrame(() => loaded(iFrame, container)); // Continuously check for changes const loaded = debounce(async () => { if (!isMounted.current || isUpdating.current) return; m.disconnect(); await updateCoverBlocks(iFrame, container); if (window.extSharedData.themeSlug !== 'extendable') { await addMissingThemeVars(iFrame, container); } // Reconnect observer after making changes setTimeout(() => setMaybeOk(true), loadDelay); if (isMounted.current) m.observe(iFrame.contentDocument, config); }, 300); const m = new MutationObserver(loaded); m.observe(iFrame.contentDocument, config); return () => { loaded.cancel(); m?.disconnect(); }; }, [ iFrame, container, isMounted, ready, updateCoverBlocks, addMissingThemeVars, loadDelay, ]); return { loading: !iFrame, ready: maybeOk }; };