Skip to content

Commit d9fa125

Browse files
committed
Handle browser refresh in Playground iframe
1 parent 3c42f5e commit d9fa125

7 files changed

Lines changed: 411 additions & 2 deletions

File tree

packages/playground/personal-wp/src/components/playground-viewport/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
useAppDispatch,
1818
useAppSelector,
1919
} from '../../lib/state/redux/store';
20-
import { removeClientInfo } from '../../lib/state/redux/slice-clients';
20+
import {
21+
removeClientInfo,
22+
selectClientInfoBySiteSlug,
23+
} from '../../lib/state/redux/slice-clients';
2124
import { bootSiteClient } from '../../lib/state/redux/boot-site-client';
2225
import { selectSiteBySlug } from '../../lib/state/redux/slice-sites';
2326
import {
@@ -42,6 +45,7 @@ import {
4245
} from './blueprint-install';
4346
import type { BlueprintInstallPreview } from './blueprint-install';
4447
import { isAllowedBlueprintUrl } from '../../lib/blueprint-url';
48+
import { useBrowserRefreshShortcut } from '../../lib/hooks/use-browser-refresh-shortcut';
4549
// @ts-ignore
4650
import { corsProxyUrl } from 'virtual:cors-proxy-url';
4751

@@ -854,6 +858,9 @@ export const JustViewport = function JustViewport({
854858
const internalIframeRef = useRef<HTMLIFrameElement>(null);
855859
const iframeRef = externalIframeRef || internalIframeRef;
856860
const site = useAppSelector((state) => selectSiteBySlug(state, siteSlug))!;
861+
const clientInfo = useAppSelector((state) =>
862+
selectClientInfoBySiteSlug(state, siteSlug)
863+
);
857864

858865
const dispatch = useAppDispatch();
859866
const runtimeConfigString = JSON.stringify(
@@ -885,6 +892,11 @@ export const JustViewport = function JustViewport({
885892
const errorDetails = useAppSelector(selectActiveSiteErrorDetails);
886893
const activeSiteSlug = useAppSelector((state) => state.ui.activeSite?.slug);
887894
const showOverlay = error && activeSiteSlug === siteSlug;
895+
useBrowserRefreshShortcut({
896+
client: clientInfo?.client,
897+
enabled: activeSiteSlug === siteSlug,
898+
iframeRef,
899+
});
888900

889901
return (
890902
<>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { useEffect } from 'react';
2+
import type { RefObject } from 'react';
3+
import type { PlaygroundClient } from '@wp-playground/remote';
4+
import { logger } from '@php-wasm/logger';
5+
6+
const PLAYGROUND_REFRESH_RELAY_TYPE = 'playground-refresh';
7+
8+
export function useBrowserRefreshShortcut({
9+
client,
10+
enabled = true,
11+
iframeRef,
12+
}: {
13+
client?: PlaygroundClient;
14+
enabled?: boolean;
15+
iframeRef: RefObject<HTMLIFrameElement>;
16+
}) {
17+
useEffect(() => {
18+
if (!enabled || !client) {
19+
return;
20+
}
21+
const playground = client;
22+
23+
function handleKeyDown(event: KeyboardEvent) {
24+
if (!isBrowserRefreshShortcut(event)) {
25+
return;
26+
}
27+
28+
event.preventDefault();
29+
event.stopPropagation();
30+
void reloadPlaygroundClient(playground).catch((error) =>
31+
logger.error(error)
32+
);
33+
}
34+
35+
window.addEventListener('keydown', handleKeyDown, true);
36+
return () => {
37+
window.removeEventListener('keydown', handleKeyDown, true);
38+
};
39+
}, [client, enabled]);
40+
41+
useEffect(() => {
42+
if (!enabled || !client) {
43+
return;
44+
}
45+
const playground = client;
46+
47+
function handleMessage(event: MessageEvent) {
48+
if (
49+
event.origin !== window.location.origin ||
50+
!isPlaygroundRefreshMessage(event.data) ||
51+
!isMessageFromIframeTree(event, iframeRef.current)
52+
) {
53+
return;
54+
}
55+
56+
void reloadPlaygroundClient(playground).catch((error) =>
57+
logger.error(error)
58+
);
59+
}
60+
61+
window.addEventListener('message', handleMessage);
62+
return () => {
63+
window.removeEventListener('message', handleMessage);
64+
};
65+
}, [client, enabled, iframeRef]);
66+
}
67+
68+
async function reloadPlaygroundClient(client: PlaygroundClient) {
69+
await client.goTo(await client.getCurrentURL());
70+
}
71+
72+
export function isBrowserRefreshShortcut(
73+
event: Pick<
74+
KeyboardEvent,
75+
| 'altKey'
76+
| 'ctrlKey'
77+
| 'defaultPrevented'
78+
| 'isComposing'
79+
| 'key'
80+
| 'metaKey'
81+
| 'repeat'
82+
>
83+
): boolean {
84+
return (
85+
!event.defaultPrevented &&
86+
!event.altKey &&
87+
!event.isComposing &&
88+
!event.repeat &&
89+
event.key.toLowerCase() === 'r' &&
90+
(event.metaKey || event.ctrlKey)
91+
);
92+
}
93+
94+
function isPlaygroundRefreshMessage(data: unknown): boolean {
95+
return (
96+
typeof data === 'object' &&
97+
data !== null &&
98+
(data as { type?: unknown }).type === 'relay' &&
99+
(data as { relayType?: unknown }).relayType ===
100+
PLAYGROUND_REFRESH_RELAY_TYPE
101+
);
102+
}
103+
104+
function isMessageFromIframeTree(
105+
event: MessageEvent,
106+
iframe: HTMLIFrameElement | null
107+
): boolean {
108+
if (!iframe?.contentWindow || !event.source) {
109+
return false;
110+
}
111+
if (event.source === iframe.contentWindow) {
112+
return true;
113+
}
114+
return isDescendantWindow(iframe.contentWindow, event.source);
115+
}
116+
117+
function isDescendantWindow(
118+
root: Window,
119+
candidate: MessageEventSource
120+
): boolean {
121+
try {
122+
for (let i = 0; i < root.frames.length; i++) {
123+
const child = root.frames[i];
124+
if (child === candidate || isDescendantWindow(child, candidate)) {
125+
return true;
126+
}
127+
}
128+
} catch {
129+
// Cross-origin descendants are not inspectable and are not accepted.
130+
}
131+
return false;
132+
}

packages/playground/remote/src/lib/boot-playground-remote.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ export async function bootPlaygroundRemote() {
562562
phpRemoteApi,
563563
phpWorkerApi
564564
);
565+
setupBrowserRefreshShortcut(() => reloadWordPressFrame(phpRemoteApi));
565566

566567
/*
567568
* An assertion to make sure Playground Client is compatible
@@ -624,6 +625,48 @@ function getOrigin(url: string) {
624625
return new URL(url, 'https://example.com').origin;
625626
}
626627

628+
function setupBrowserRefreshShortcut(reload: () => Promise<void>) {
629+
window.addEventListener(
630+
'keydown',
631+
(event) => {
632+
if (!isBrowserRefreshShortcut(event)) {
633+
return;
634+
}
635+
636+
event.preventDefault();
637+
event.stopPropagation();
638+
void reload().catch((error) => logger.error(error));
639+
},
640+
true
641+
);
642+
}
643+
644+
async function reloadWordPressFrame(playground: PHPRemoteApi) {
645+
await playground.goTo(await playground.getCurrentURL());
646+
}
647+
648+
function isBrowserRefreshShortcut(
649+
event: Pick<
650+
KeyboardEvent,
651+
| 'altKey'
652+
| 'ctrlKey'
653+
| 'defaultPrevented'
654+
| 'isComposing'
655+
| 'key'
656+
| 'metaKey'
657+
| 'repeat'
658+
>
659+
): boolean {
660+
return (
661+
!event.defaultPrevented &&
662+
!event.altKey &&
663+
!event.isComposing &&
664+
!event.repeat &&
665+
event.key.toLowerCase() === 'r' &&
666+
(event.metaKey || event.ctrlKey)
667+
);
668+
}
669+
627670
/**
628671
* When the service worker fails for any reason, the page displayed inside
629672
* the iframe won't be a WordPress instance we expect from the service worker.

packages/playground/remote/src/lib/playground-mu-plugin/0-playground-php52.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,44 @@ function _pg52_dummy_transports() {
6969
return array('Dummy');
7070
}
7171

72+
// Ask the embedding Playground shell to reload only the WordPress iframe.
73+
add_action('wp_head', '_pg52_reload_iframe_on_browser_refresh_shortcut');
74+
add_action('admin_head', '_pg52_reload_iframe_on_browser_refresh_shortcut');
75+
add_action('login_head', '_pg52_reload_iframe_on_browser_refresh_shortcut');
76+
function _pg52_reload_iframe_on_browser_refresh_shortcut() {
77+
?>
78+
<script>
79+
(function () {
80+
function isRefreshShortcut(event) {
81+
var key = event.key || '';
82+
return !event.defaultPrevented &&
83+
!event.altKey &&
84+
!event.isComposing &&
85+
!event.repeat &&
86+
key.toLowerCase() === 'r' &&
87+
(event.metaKey || event.ctrlKey);
88+
}
89+
90+
window.addEventListener('keydown', function (event) {
91+
if (!isRefreshShortcut(event) || window.parent === window) {
92+
return;
93+
}
94+
95+
event.preventDefault();
96+
event.stopPropagation();
97+
window.parent.postMessage(
98+
{
99+
type: 'relay',
100+
relayType: 'playground-refresh'
101+
},
102+
'*'
103+
);
104+
}, true);
105+
})();
106+
</script>
107+
<?php
108+
}
109+
72110
// Disable WP Cron on legacy WordPress only. On PHP 5.2 the HTTP API
73111
// is stubbed with Wp_Http_Dummy (see above), so every spawn-cron
74112
// request would return false and WordPress would quietly retry

packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,46 @@ function playground_report_url_to_parent() {
189189
add_action('wp_head', 'playground_report_url_to_parent');
190190
add_action('admin_head', 'playground_report_url_to_parent');
191191

192+
/**
193+
* Asks the embedding Playground shell to reload only the WordPress iframe.
194+
*/
195+
function playground_reload_iframe_on_browser_refresh_shortcut() {
196+
?>
197+
<script>
198+
(function () {
199+
function isRefreshShortcut(event) {
200+
var key = event.key || '';
201+
return !event.defaultPrevented &&
202+
!event.altKey &&
203+
!event.isComposing &&
204+
!event.repeat &&
205+
key.toLowerCase() === 'r' &&
206+
(event.metaKey || event.ctrlKey);
207+
}
208+
209+
window.addEventListener('keydown', function (event) {
210+
if (!isRefreshShortcut(event) || window.parent === window) {
211+
return;
212+
}
213+
214+
event.preventDefault();
215+
event.stopPropagation();
216+
window.parent.postMessage(
217+
{
218+
type: 'relay',
219+
relayType: 'playground-refresh'
220+
},
221+
'*'
222+
);
223+
}, true);
224+
})();
225+
</script>
226+
<?php
227+
}
228+
add_action('wp_head', 'playground_reload_iframe_on_browser_refresh_shortcut');
229+
add_action('admin_head', 'playground_reload_iframe_on_browser_refresh_shortcut');
230+
add_action('login_head', 'playground_reload_iframe_on_browser_refresh_shortcut');
231+
192232
/**
193233
* The default WordPress requests transports have been disabled
194234
* at this point. However, the Requests class requires at least

packages/playground/website/src/components/playground-viewport/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
useAppDispatch,
1010
useAppSelector,
1111
} from '../../lib/state/redux/store';
12-
import { removeClientInfo } from '../../lib/state/redux/slice-clients';
12+
import {
13+
removeClientInfo,
14+
selectClientInfoBySiteSlug,
15+
} from '../../lib/state/redux/slice-clients';
1316
import { bootSiteClient } from '../../lib/state/redux/boot-site-client';
1417
import {
1518
selectSiteBySlug,
@@ -18,6 +21,7 @@ import {
1821
} from '../../lib/state/redux/slice-sites';
1922
import classNames from 'classnames';
2023
import { SiteErrorModal } from '../site-error-modal';
24+
import { useBrowserRefreshShortcut } from '../../lib/hooks/use-browser-refresh-shortcut';
2125

2226
export const supportedDisplayModes = [
2327
'browser-full-screen',
@@ -201,6 +205,9 @@ export const JustViewport = function JustViewport({
201205
}) {
202206
const iframeRef = useRef<HTMLIFrameElement>(null);
203207
const site = useAppSelector((state) => selectSiteBySlug(state, siteSlug))!;
208+
const clientInfo = useAppSelector((state) =>
209+
selectClientInfoBySiteSlug(state, siteSlug)
210+
);
204211

205212
const dispatch = useAppDispatch();
206213
const runtimeConfigString = JSON.stringify(
@@ -230,6 +237,11 @@ export const JustViewport = function JustViewport({
230237
const errorDetails = useAppSelector(selectActiveSiteErrorDetails);
231238
const activeSiteSlug = useAppSelector((state) => state.ui.activeSite?.slug);
232239
const showOverlay = error && activeSiteSlug === siteSlug;
240+
useBrowserRefreshShortcut({
241+
client: clientInfo?.client,
242+
enabled: activeSiteSlug === siteSlug,
243+
iframeRef,
244+
});
233245

234246
return (
235247
<>

0 commit comments

Comments
 (0)