PluginProbe
Extendify / 2.2.0
Extendify v2.2.0
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 / Launch / components / SmallPreview.jsx

SmallPreview.jsx in Extendify 2.2.0, at src/Launch/components/SmallPreview.jsx

228 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { BlockPreview } from '@wordpress/block-editor';
2 import { rawHandler } from '@wordpress/blocks';
3 import {
4 useState,
5 useRef,
6 useCallback,
7 useEffect,
8 useMemo,
9 } from '@wordpress/element';
10 import { __ } from '@wordpress/i18n';
11 import { pageNames } from '@shared/lib/pages';
12 import classNames from 'classnames';
13 import { colord } from 'colord';
14 import { AnimatePresence, motion } from 'framer-motion';
15 import themeJSON from '@launch/_data/theme-processed.json';
16 import { usePreviewIframe } from '@launch/hooks/usePreviewIframe';
17 import { getFontOverrides } from '@launch/lib/preview-helpers';
18 import { hexTomatrixValues, lowerImageQuality } from '@launch/lib/util';
19
20 export const SmallPreview = ({
21 style,
22 onSelect,
23 selected,
24 siteTitle,
25 showNav = true,
26 }) => {
27 const previewContainer = useRef(null);
28 const blockRef = useRef(null);
29 const observer = useRef(null);
30 const [ready, setReady] = useState(false);
31 const variation = style?.variation;
32 const theme = variation?.settings?.color?.palette?.theme;
33
34 const onLoad = useCallback(
35 (frame) => {
36 // Run this 150 times at an interval of 100ms (15s)
37 // This is a brute force check that the styles are there
38 let lastRun = performance.now();
39 let counter = 0;
40 const variationTitle = variation?.slug
41 ?.split('-')
42 .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
43 .join(' ');
44
45 const variationStyles = themeJSON[variationTitle];
46 const { customFontLinks, fontOverrides } = getFontOverrides(variation);
47
48 const checkOnStyles = () => {
49 if (counter >= 150) return;
50 const now = performance.now();
51 // Don't pass here until we've waited 100ms
52 if (now - lastRun < 100) return requestAnimationFrame(checkOnStyles);
53 lastRun = now;
54 const content = frame?.contentDocument;
55 if (content) {
56 content.querySelector('[href*=load-styles]')?.remove();
57 const siteTitleElement = content.querySelector('[href*=site-title]');
58 if (siteTitleElement.textContent !== siteTitle) {
59 siteTitleElement.textContent = siteTitle;
60 }
61 }
62 const primaryColor = theme?.find(
63 ({ slug }) => slug === 'primary',
64 )?.color;
65 const [r, g, b] = primaryColor
66 ? hexTomatrixValues(primaryColor)
67 : [0, 0, 0];
68
69 // Add custom font links if not already present
70 if (
71 customFontLinks &&
72 !frame.contentDocument?.querySelector('[id^="ext-custom-font"]')
73 ) {
74 frame.contentDocument?.head?.insertAdjacentHTML(
75 'beforeend',
76 customFontLinks,
77 );
78 }
79
80 if (!frame.contentDocument?.getElementById('ext-tj')) {
81 frame.contentDocument?.body?.insertAdjacentHTML(
82 'beforeend',
83 `<style id="ext-tj">
84 ${variationStyles}
85 ${fontOverrides}
86 .wp-block-missing { display: none !important }
87 img.custom-logo, [class*=wp-duotone-] img[src^="data"] {
88 filter: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><filter id="solid-color"><feColorMatrix color-interpolation-filters="sRGB" type="matrix" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0 0 0 1 0"/></filter></svg>#solid-color') !important;
89 }
90 </style>`,
91 );
92 }
93
94 counter++;
95 requestAnimationFrame(checkOnStyles); // recursive
96 };
97 checkOnStyles();
98 },
99 [variation, theme, siteTitle],
100 );
101
102 const { loading, ready: show } = usePreviewIframe({
103 container: blockRef.current,
104 ready,
105 onLoad,
106 // Additional time to wait after we think we are ready
107 loadDelay: 750,
108 });
109 const blocks = useMemo(() => {
110 const links = [
111 pageNames.about.title,
112 pageNames.blog.title,
113 pageNames.contact.title,
114 ];
115
116 const code = [
117 style?.headerCode,
118 style?.patterns
119 .map(({ code }) => code)
120 .flat()
121 .slice(0, 3)
122 .join('\n'),
123 style?.footerCode,
124 ]
125 .filter(Boolean)
126 .join('')
127 .replace(
128 // <!-- wp:navigation --> <!-- /wp:navigation -->
129 /<!-- wp:navigation[.\S\s]*?\/wp:navigation -->/g,
130 showNav
131 ? `<!-- wp:paragraph {"className":"tmp-nav"} --><p class="tmp-nav" style="display: flex; gap: 2rem;">${links.map((link) => `<span>${link}</span>`).join('')}</p ><!-- /wp:paragraph -->`
132 : '',
133 )
134 .replace(
135 // <!-- wp:navigation /-->
136 /<!-- wp:navigation.*\/-->/g,
137 showNav
138 ? `<!-- wp:paragraph {"className":"tmp-nav"} --><p class="tmp-nav" style="display: flex; gap: 2rem;">${links.map((link) => `<span>${link}</span>`).join('')}</p ><!-- /wp:paragraph -->`
139 : '',
140 )
141 .replace(
142 /<!--\s*wp:social-links\b[^>]*>.*?<!--\s*\/wp:social-links\s*-->/gis,
143 // dont replace if showNav is true
144 (match) => (showNav ? match : ''),
145 )
146 .replace(
147 /<!-- wp:site-logo.*\/-->/g,
148 '<!-- wp:paragraph {"className":"custom-logo"} --><p class="custom-logo" style="display:flex; align-items: center;"><img alt="" class="custom-logo" style="height: 32px;" src="https://images.extendify-cdn.com/demo-content/logos/ext-custom-logo-default.webp"></p ><!-- /wp:paragraph -->',
149 );
150 return rawHandler({ HTML: lowerImageQuality(code) });
151 }, [style, showNav]);
152
153 useEffect(() => {
154 if (observer.current) return;
155 observer.current = new IntersectionObserver((entries) => {
156 entries[0].isIntersecting && setReady(true);
157 });
158 observer.current.observe(blockRef.current);
159 return () => observer.current.disconnect();
160 }, []);
161
162 return (
163 <>
164 <div
165 data-test="layout-preview"
166 className="relative h-full w-full overflow-hidden"
167 ref={blockRef}
168 role={onSelect ? 'button' : undefined}
169 tabIndex={onSelect ? 0 : undefined}
170 aria-label={
171 onSelect ? __('Press to select', 'extendify-local') : undefined
172 }
173 aria-selected={onSelect ? selected : undefined}
174 onKeyDown={(e) => {
175 if (['Enter', 'Space', ' '].includes(e.key)) {
176 onSelect && onSelect({ ...style, variation });
177 }
178 }}
179 onClick={onSelect ? () => onSelect({ ...style, variation }) : () => {}}>
180 {ready ? (
181 <motion.div
182 ref={previewContainer}
183 className={classNames('absolute inset-0 z-20', {
184 'opacity-0': !show,
185 })}
186 initial={{ opacity: 0 }}
187 animate={{ opacity: loading ? 0 : 1 }}>
188 <BlockPreview
189 blocks={blocks}
190 viewportWidth={1400}
191 additionalStyles={[
192 // TODO: { css: themeJSON[style.variation.title] },
193 {
194 css: '.rich-text [data-rich-text-placeholder]:after { content: "" }',
195 },
196 ]}
197 />
198 </motion.div>
199 ) : null}
200 <AnimatePresence>
201 {show || (
202 <motion.div
203 initial={{ opacity: 0.7 }}
204 animate={{ opacity: 1 }}
205 exit={{ opacity: 0 }}
206 transition={{ duration: 0.5 }}
207 className="absolute inset-0 z-30"
208 style={{
209 backgroundColor: colord(
210 theme?.find(({ slug }) => slug === 'primary')?.color ??
211 '#ccc',
212 )
213 .alpha(0.25)
214 .toRgbString(),
215 backgroundImage:
216 'linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0) 100%)',
217 backgroundSize: '600% 600%',
218 animation:
219 'extendify-loading-skeleton 10s ease-in-out infinite',
220 }}
221 />
222 )}
223 </AnimatePresence>
224 </div>
225 </>
226 );
227 };
228