| 1 |
import { useState, useEffect, useRef, useCallback } from '@wordpress/element'; |
| 2 |
import { debounce } from 'lodash'; |
| 3 |
import { useIsMounted } from '@library/hooks/useIsMounted'; |
| 4 |
import { useGlobalsStore } from '@library/state/global'; |
| 5 |
import { requiredCSSVars } from '@library/util/css'; |
| 6 |
import { hasCSSVar } from '@library/util/dom'; |
| 7 |
|
| 8 |
const originalHeights = new WeakMap(); |
| 9 |
|
| 10 |
export const usePreviewIframe = ({ |
| 11 |
container, |
| 12 |
onIFrameLoaded, |
| 13 |
ready, |
| 14 |
loadDelay, |
| 15 |
}) => { |
| 16 |
const isMounted = useIsMounted(); |
| 17 |
const [waitForIframe, setWaitForIframe] = useState(0); |
| 18 |
const [iFrame, setIFrame] = useState(null); |
| 19 |
const [maybeOk, setMaybeOk] = useState(false); |
| 20 |
const isUpdating = useRef(false); |
| 21 |
const { addMissingCSSVar } = useGlobalsStore(); |
| 22 |
|
| 23 |
const addMissingThemeVars = useCallback( |
| 24 |
(frame) => { |
| 25 |
if (!frame?.contentDocument) return; |
| 26 |
const styles = getComputedStyle(frame.contentDocument.documentElement); |
| 27 |
const styleSheets = frame.contentDocument.styleSheets; |
| 28 |
for (let key in requiredCSSVars) { |
| 29 |
// CSS variable was found applied somewhere |
| 30 |
if (styles.getPropertyValue(key)) continue; |
| 31 |
const varUsed = Array.from(styleSheets) |
| 32 |
.filter((sheet) => { |
| 33 |
try { |
| 34 |
return sheet.cssRules; |
| 35 |
} catch (error) { |
| 36 |
return false; |
| 37 |
} |
| 38 |
}) |
| 39 |
.some((sheet) => hasCSSVar(key, sheet.cssRules)); |
| 40 |
// CSS variable was found in a stylesheet somewhere |
| 41 |
if (varUsed) continue; |
| 42 |
|
| 43 |
// Add the missing variable to the global state to be imported later |
| 44 |
addMissingCSSVar(key); |
| 45 |
// Dive into iframe and add the variables to at the :root scope |
| 46 |
frame.contentDocument.documentElement.style.setProperty( |
| 47 |
key, |
| 48 |
requiredCSSVars[key], |
| 49 |
); |
| 50 |
} |
| 51 |
}, |
| 52 |
[addMissingCSSVar], |
| 53 |
); |
| 54 |
|
| 55 |
const updateCoverBlocks = useCallback(async (frame, cntnr) => { |
| 56 |
const ft = frame.getBoundingClientRect().top; |
| 57 |
const ct = cntnr.getBoundingClientRect().top; |
| 58 |
// If they have scrolled, don't mess with it |
| 59 |
if (ft < ct) return; |
| 60 |
|
| 61 |
isUpdating.current = true; |
| 62 |
|
| 63 |
let scale = cntnr |
| 64 |
.querySelector('[style*="scale"]') |
| 65 |
?.style?.transform?.match(/scale\((.*?)\)/)?.[1]; |
| 66 |
scale = scale ? parseFloat(scale) : null; |
| 67 |
|
| 68 |
const cntnrHScaled = cntnr.offsetHeight / (scale ?? 1); |
| 69 |
frame.style.setProperty('max-height', `${cntnrHScaled}px`, 'important'); |
| 70 |
|
| 71 |
const coverBlocks = |
| 72 |
frame.contentDocument.querySelectorAll('.wp-block-cover'); |
| 73 |
for (const el of coverBlocks) { |
| 74 |
if (!originalHeights.has(el)) { |
| 75 |
// Cache the original 'vh' value |
| 76 |
originalHeights.set(el, el.style.minHeight); |
| 77 |
} |
| 78 |
|
| 79 |
// Reapply the original 'vh' value so it can be used in computations |
| 80 |
el.style.minHeight = originalHeights.get(el); |
| 81 |
} |
| 82 |
|
| 83 |
cntnr.offsetHeight; // Force a reflow |
| 84 |
// Give the browser time to paint |
| 85 |
await new Promise((resolve) => requestAnimationFrame(resolve)); |
| 86 |
await new Promise((resolve) => requestAnimationFrame(resolve)); |
| 87 |
|
| 88 |
for (const el of coverBlocks) { |
| 89 |
if (!frame.contentDocument?.defaultView) return; |
| 90 |
// Get the computed height in px and use it for your calculation |
| 91 |
const computedHeight = parseFloat( |
| 92 |
frame.contentDocument.defaultView.getComputedStyle(el).height, |
| 93 |
); |
| 94 |
el.offsetHeight; // Force a reflow |
| 95 |
el.style.minHeight = |
| 96 |
computedHeight > 500 ? '500px' : computedHeight + 'px'; |
| 97 |
} |
| 98 |
|
| 99 |
frame.style.setProperty('max-height', 'none', 'important'); |
| 100 |
isUpdating.current = false; |
| 101 |
}, []); |
| 102 |
|
| 103 |
useEffect(() => { |
| 104 |
if (!ready) return; |
| 105 |
// continuously check for iframe |
| 106 |
const interval = setTimeout(() => { |
| 107 |
if (iFrame) return; |
| 108 |
const frame = container?.querySelector('iframe[title]'); |
| 109 |
|
| 110 |
// If not found, retry by updating state |
| 111 |
if (!frame) return setWaitForIframe((prev) => prev + 1); |
| 112 |
setIFrame(frame); |
| 113 |
requestAnimationFrame(() => onIFrameLoaded(frame, container)); |
| 114 |
}, 100); |
| 115 |
return () => clearTimeout(interval); |
| 116 |
}, [iFrame, ready, waitForIframe, container, onIFrameLoaded]); |
| 117 |
|
| 118 |
useEffect(() => { |
| 119 |
setMaybeOk(false); |
| 120 |
// After the iFrame is found, wait for it to load |
| 121 |
// Note: using load event is not reliable |
| 122 |
if (!iFrame?.contentDocument) return; |
| 123 |
|
| 124 |
const config = { |
| 125 |
attributes: false, |
| 126 |
childList: true, |
| 127 |
subtree: true, |
| 128 |
}; |
| 129 |
|
| 130 |
// Run once in case the iframe is already loaded |
| 131 |
requestAnimationFrame(() => loaded(iFrame, container)); |
| 132 |
|
| 133 |
// Continuously check for changes |
| 134 |
const loaded = debounce(async () => { |
| 135 |
if (!isMounted.current || isUpdating.current) return; |
| 136 |
m.disconnect(); |
| 137 |
|
| 138 |
await updateCoverBlocks(iFrame, container); |
| 139 |
if (window.extSharedData.themeSlug !== 'extendable') { |
| 140 |
await addMissingThemeVars(iFrame, container); |
| 141 |
} |
| 142 |
// Reconnect observer after making changes |
| 143 |
setTimeout(() => setMaybeOk(true), loadDelay); |
| 144 |
if (isMounted.current) m.observe(iFrame.contentDocument, config); |
| 145 |
}, 300); |
| 146 |
|
| 147 |
const m = new MutationObserver(loaded); |
| 148 |
m.observe(iFrame.contentDocument, config); |
| 149 |
|
| 150 |
return () => { |
| 151 |
loaded.cancel(); |
| 152 |
m?.disconnect(); |
| 153 |
}; |
| 154 |
}, [ |
| 155 |
iFrame, |
| 156 |
container, |
| 157 |
isMounted, |
| 158 |
ready, |
| 159 |
updateCoverBlocks, |
| 160 |
addMissingThemeVars, |
| 161 |
loadDelay, |
| 162 |
]); |
| 163 |
|
| 164 |
return { loading: !iFrame, ready: maybeOk }; |
| 165 |
}; |
| 166 |
|