import { BlockPreview, transformStyles } from '@wordpress/block-editor' import { rawHandler } from '@wordpress/blocks' import { useState, useRef, useEffect, useMemo, useLayoutEffect, } from '@wordpress/element' import { __ } from '@wordpress/i18n' import classNames from 'classnames' import { getThemeVariations, parseThemeJson } from '@onboarding/api/WPApi' import { SkeletonLoader } from '@onboarding/components/SkeletonLoader' import { useFetch } from '@onboarding/hooks/useFetch' import { useIsMounted } from '@onboarding/hooks/useIsMounted' import { capitalize, lowerImageQuality } from '@onboarding/lib/util' import { useUserSelectionStore } from '@onboarding/state/UserSelections' const fetcher = async (themeJson) => { if (!themeJson) return '{}' const res = await parseThemeJson(JSON.stringify(themeJson)) if (!res?.styles) { throw new Error('Invalid theme json') } return { data: res.styles } } export const StylePreview = ({ style, onSelect, blockHeight, context, active = false, onHover = null, }) => { const siteType = useUserSelectionStore((state) => state.siteType) const isMounted = useIsMounted() const [code, setCode] = useState('') const [loaded, setLoaded] = useState(false) const [inView, setInView] = useState(false) const [hoverCleanup, setHoverCleanup] = useState(null) const [variation, setVariation] = useState() const previewContainer = useRef(null) const content = useRef(null) const blockRef = useRef(null) const observer = useRef(null) const startTime = useRef(null) const loadTime = useRef(false) const { data: themeJson } = useFetch( inView && variation ? variation : null, fetcher, ) const { data: variations } = useFetch('variations', getThemeVariations) const theme = variation?.settings?.color?.palette?.theme const blocks = useMemo( () => rawHandler({ HTML: lowerImageQuality(code) }), [code], ) const transformedStyles = useMemo( () => themeJson ? transformStyles( [{ css: themeJson }], '.editor-styles-wrapper', ) : null, [themeJson], ) useLayoutEffect(() => { if (!inView || !context.measure) return const key = `${context.type}-${context.detail}` // If the component is in view, start the timer if (!loaded && !loadTime.current) { loadTime.current = 0 startTime.current = performance.now() return } let time try { time = performance.measure(key, { start: startTime.current, // The extendify key is used to filter only our measurements detail: { context, extendify: true }, }) } catch (e) { console.error(e) } loadTime.current = time?.duration ?? 0 const q = new URLSearchParams(window.location.search) if (q?.has('performance') && loadTime.current) { console.info( `🚀 ${capitalize(context.type)} (${ context.detail }) in ${loadTime.current.toFixed()}ms`, ) } }, [loaded, context, inView]) useEffect(() => { if (!variations?.length) return // Grab the styles from the theme.json variation const variation = variations.find( (theme) => theme.title === style.label, ) setVariation(variation) }, [style, variations]) useEffect(() => { if (!themeJson || !style?.code) return const code = [style?.headerCode, style?.code, style?.footerCode] .filter(Boolean) .join('') .replace( // //g, '
Home | About | Contact
', ) .replace( // //g, 'Home | About | Contact
', ) setCode(code) }, [siteType?.slug, themeJson, style]) useEffect(() => { let raf1, raf2 if (!content.current || !loaded) return const p = previewContainer.current const scale = p.offsetWidth / 1400 const iframe = content.current const body = iframe.contentDocument.body if (body?.style) { body.style.transitionProperty = 'all' body.style.top = 0 } const handleIn = () => { if (!body.offsetHeight) return const dynBlockHeight = (blockRef?.current?.offsetHeight ?? blockHeight) - 32 const bodyHeight = body.getBoundingClientRect().height - dynBlockHeight / scale body.style.transitionDuration = Math.max(bodyHeight * 2, 3000) + 'ms' raf1 = window.requestAnimationFrame(() => { body.style.top = Math.max(0, bodyHeight) * -1 + 'px' }) } const handleOut = () => { if (!body.offsetHeight) return const dynBlockHeight = (blockRef?.current?.offsetHeight ?? blockHeight) - 32 const bodyHeight = body.offsetHeight - dynBlockHeight / scale body.style.transitionDuration = bodyHeight + 'ms' raf2 = window.requestAnimationFrame(() => { body.style.top = 0 }) } p.addEventListener('focus', handleIn) p.addEventListener('mouseenter', handleIn) p.addEventListener('blur', handleOut) p.addEventListener('mouseleave', handleOut) return () => { window.cancelAnimationFrame(raf1) window.cancelAnimationFrame(raf2) p.removeEventListener('focus', handleIn) p.removeEventListener('mouseenter', handleIn) p.removeEventListener('blur', handleOut) p.removeEventListener('mouseleave', handleOut) } }, [blockHeight, loaded]) useEffect(() => { if (!blocks?.length || !inView) return let iframe // Inserts theme styles after iframe is loaded const handle = () => { if (!iframe.contentDocument?.getElementById('ext-tj')) { iframe.contentDocument?.head?.insertAdjacentHTML( 'beforeend', ``, ) } content.current = iframe setTimeout(() => { if (isMounted.current) setLoaded(true) }, 100) } // The callback will attach a load event to the iframe const observer = new MutationObserver(() => { iframe = previewContainer.current.querySelector('iframe[title]') iframe.addEventListener('load', handle) setTimeout(() => { // In some cases, the load event doesn't fire. handle() }, 2000) observer.disconnect() }) // observe the ref for when the iframe is injected by wp observer.observe(previewContainer.current, { attributes: false, childList: true, subtree: false, }) return () => { observer.disconnect() iframe?.removeEventListener('load', handle) } }, [blocks, transformedStyles, isMounted, inView]) useEffect(() => { // Only trigger the mutation observer if we are in view if (!observer.current) { observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { setInView(true) } }) } observer.current.observe(blockRef.current) return () => { observer.current.disconnect() } }, []) return ( <> {loaded && code ? null : ( <>