PluginProbe
Extendify / 0.9.3
Extendify v0.9.3
3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 0.7.0 All 126 releases
extendify / src / Onboarding / components / StyledPreview.js

StyledPreview.js in Extendify 0.9.3, at src/Onboarding/components/StyledPreview.js

283 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { BlockPreview, transformStyles } from '@wordpress/block-editor'
2 import { rawHandler } from '@wordpress/blocks'
3 import {
4 useState,
5 useRef,
6 useEffect,
7 useMemo,
8 useLayoutEffect,
9 } from '@wordpress/element'
10 import { __ } from '@wordpress/i18n'
11 import classNames from 'classnames'
12 import { getThemeVariations, parseThemeJson } from '@onboarding/api/WPApi'
13 import { useFetch } from '@onboarding/hooks/useFetch'
14 import { useIsMounted } from '@onboarding/hooks/useIsMounted'
15 import { capitalize, lowerImageQuality } from '@onboarding/lib/util'
16 import { useUserSelectionStore } from '@onboarding/state/UserSelections'
17 import { SpinnerIcon } from '@onboarding/svg'
18
19 const fetcher = async (themeJson) => {
20 if (!themeJson) return '{}'
21 const res = await parseThemeJson(JSON.stringify(themeJson))
22 return res?.styles ?? '{}'
23 }
24 export const StylePreview = ({
25 style,
26 onSelect,
27 blockHeight,
28 context,
29 active = false,
30 onHover = null,
31 }) => {
32 const siteType = useUserSelectionStore((state) => state.siteType)
33 const isMounted = useIsMounted()
34 const [code, setCode] = useState('')
35 const [loaded, setLoaded] = useState(false)
36 const [inView, setInView] = useState(false)
37 const [hoverCleanup, setHoverCleanup] = useState(null)
38 const [variation, setVariation] = useState()
39 const previewContainer = useRef(null)
40 const content = useRef(null)
41 const blockRef = useRef(null)
42 const observer = useRef(null)
43 const loadTime = useRef(false)
44 const { data: themeJson } = useFetch(
45 inView && variation ? variation : null,
46 fetcher,
47 )
48 const { data: variations } = useFetch('variations', getThemeVariations)
49
50 const blocks = useMemo(
51 () => rawHandler({ HTML: lowerImageQuality(code) }),
52 [code],
53 )
54 const transformedStyles = useMemo(
55 () =>
56 themeJson
57 ? transformStyles(
58 [{ css: themeJson }],
59 '.editor-styles-wrapper',
60 )
61 : null,
62 [themeJson],
63 )
64
65 useLayoutEffect(() => {
66 if (!inView || !context.measure) return
67 const key = `${context.type}-${context.detail}`
68 // If the componeent is in view, start the timer
69 if (!loaded && !loadTime.current) {
70 performance.mark(key)
71 return
72 }
73 const time = performance.measure(key, {
74 start: key,
75 // The extendify key is used to filter only our measurements
76 detail: { context, extendify: true },
77 })
78
79 loadTime.current = time.duration
80 const q = new URLSearchParams(window.location.search)
81 if (q?.has('performance')) {
82 console.info(
83 `🚀 ${capitalize(context.type)} (${
84 context.detail
85 }) in ${time.duration.toFixed()}ms`,
86 )
87 }
88 }, [loaded, context, inView])
89
90 useEffect(() => {
91 if (!variations?.length) return
92
93 // Grab the styles from the theme.json variation
94 const variation = variations.find(
95 (theme) => theme.title === style.label,
96 )
97 setVariation(variation)
98 }, [style, variations])
99
100 useEffect(() => {
101 if (!themeJson || !style?.code) return
102 const code = [style?.headerCode, style?.code, style?.footerCode]
103 .filter(Boolean)
104 .join('')
105 .replace(
106 // <!-- wp:navigation --> <!-- /wp:navigation -->
107 /<!-- wp:navigation[.\S\s]*?\/wp:navigation -->/g,
108 '<!-- wp:paragraph {"className":"tmp-nav"} --><p class="tmp-nav">Home | About | Contact</p ><!-- /wp:paragraph -->',
109 )
110 .replace(
111 // <!-- wp:navigation /-->
112 /<!-- wp:navigation.*\/-->/g,
113 '<!-- wp:paragraph {"className":"tmp-nav"} --><p class="tmp-nav">Home | About | Contact</p ><!-- /wp:paragraph -->',
114 )
115 setCode(code)
116 }, [siteType?.slug, themeJson, style])
117
118 useEffect(() => {
119 let raf1, raf2
120 if (!content.current || !loaded) return
121 const p = previewContainer.current
122 const scale = p.offsetWidth / 1400
123 const iframe = content.current
124 const body = iframe.contentDocument.body
125 if (body?.style) {
126 body.style.transitionProperty = 'all'
127 body.style.top = 0
128 }
129
130 const handleIn = () => {
131 if (!body.offsetHeight) return
132 const dynBlockHeight =
133 (blockRef?.current?.offsetHeight ?? blockHeight) - 32
134 const bodyHeight =
135 body.getBoundingClientRect().height - dynBlockHeight / scale
136 body.style.transitionDuration =
137 Math.max(bodyHeight * 2, 3000) + 'ms'
138 raf1 = window.requestAnimationFrame(() => {
139 body.style.top = Math.max(0, bodyHeight) * -1 + 'px'
140 })
141 }
142 const handleOut = () => {
143 if (!body.offsetHeight) return
144 const dynBlockHeight =
145 (blockRef?.current?.offsetHeight ?? blockHeight) - 32
146 const bodyHeight = body.offsetHeight - dynBlockHeight / scale
147 body.style.transitionDuration = bodyHeight + 'ms'
148 raf2 = window.requestAnimationFrame(() => {
149 body.style.top = 0
150 })
151 }
152
153 p.addEventListener('focus', handleIn)
154 p.addEventListener('mouseenter', handleIn)
155 p.addEventListener('blur', handleOut)
156 p.addEventListener('mouseleave', handleOut)
157 return () => {
158 window.cancelAnimationFrame(raf1)
159 window.cancelAnimationFrame(raf2)
160 p.removeEventListener('focus', handleIn)
161 p.removeEventListener('mouseenter', handleIn)
162 p.removeEventListener('blur', handleOut)
163 p.removeEventListener('mouseleave', handleOut)
164 }
165 }, [blockHeight, loaded])
166
167 useEffect(() => {
168 if (!blocks?.length || !inView) return
169 let iframe
170
171 // Inserts theme styles after iframe is loaded
172 const handle = () => {
173 if (!iframe.contentDocument?.getElementById('ext-tj')) {
174 iframe.contentDocument?.head?.insertAdjacentHTML(
175 'beforeend',
176 `<style id="ext-tj">${transformedStyles}</style>`,
177 )
178 }
179 content.current = iframe
180 setTimeout(() => {
181 if (isMounted.current) setLoaded(true)
182 }, 100)
183 }
184 // The callback will attach a load event to the iframe
185 const observer = new MutationObserver(() => {
186 iframe = previewContainer.current.querySelector('iframe[title]')
187 iframe.addEventListener('load', handle)
188 setTimeout(() => {
189 // In some cases, the load event doesn't fire.
190 handle()
191 }, 2000)
192 observer.disconnect()
193 })
194 // observe the ref for when the iframe is injected by wp
195 observer.observe(previewContainer.current, {
196 attributes: false,
197 childList: true,
198 subtree: false,
199 })
200 return () => {
201 observer.disconnect()
202 iframe?.removeEventListener('load', handle)
203 }
204 }, [blocks, transformedStyles, isMounted, inView])
205
206 useEffect(() => {
207 // Only trigger the mutation observer if we are in view
208 if (!observer.current) {
209 observer.current = new IntersectionObserver((entries) => {
210 if (entries[0].isIntersecting) {
211 setInView(true)
212 }
213 })
214 }
215 observer.current.observe(blockRef.current)
216 return () => {
217 observer.current.disconnect()
218 }
219 }, [])
220
221 return (
222 <>
223 {loaded && code ? null : (
224 <div className="absolute inset-0 z-20 bg-gray-50 flex items-center justify-center">
225 <SpinnerIcon className="spin w-8" />
226 </div>
227 )}
228 <div
229 ref={blockRef}
230 role={onSelect ? 'button' : undefined}
231 tabIndex={onSelect ? 0 : undefined}
232 aria-label={
233 onSelect ? __('Press to select', 'extendify') : undefined
234 }
235 className={classNames(
236 'group w-full overflow-hidden bg-transparent z-10',
237 {
238 'relative min-h-full': loaded,
239 'absolute opacity-0': !loaded,
240 'button-focus button-card p-2': onSelect,
241 'ring-partner-primary-bg ring-offset-2 ring-offset-white ring-wp':
242 active,
243 },
244 )}
245 onKeyDown={(e) => {
246 if (['Enter', 'Space', ' '].includes(e.key)) {
247 onSelect && onSelect({ ...style, variation })
248 }
249 }}
250 onMouseEnter={() => {
251 if (!onHover) return
252 setHoverCleanup(onHover)
253 }}
254 onMouseLeave={() => {
255 if (hoverCleanup) {
256 hoverCleanup()
257 setHoverCleanup(null)
258 }
259 }}
260 onClick={
261 onSelect
262 ? () => onSelect({ ...style, variation })
263 : () => {}
264 }>
265 {window?.extOnbData?.devbuild ? (
266 <div className="-m-px absolute bg-gray-900 border border-t border-white bottom-0 group-hover:opacity-100 left-0 opacity-0 p-1 px-4 text-left text-sm text-white z-30 transition duration-300">
267 {style?.label} - {Number(loadTime.current).toFixed(2)}ms
268 </div>
269 ) : null}
270 <div ref={previewContainer} className="relative rounded-lg">
271 {inView ? (
272 <BlockPreview
273 blocks={blocks}
274 viewportWidth={1400}
275 live={false}
276 />
277 ) : null}
278 </div>
279 </div>
280 </>
281 )
282 }
283