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 / Library / hooks / usePreviewIframe.js

usePreviewIframe.js in Extendify 2.2.0, at src/Library/hooks/usePreviewIframe.js

166 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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