import * as React from 'react'; import { forwardRef, Ref, useEffect, useImperativeHandle, useRef, useState, } from 'react'; import { Props, WebViewControl } from './types'; import { View, ViewStyle } from 'react-native'; import makeSandboxedIframe from '@joplin/lib/utils/dom/makeSandboxedIframe'; import Logger from '@joplin/utils/Logger'; const logger = Logger.create('ExtendedWebView'); // At present, react-native-webview doesn't support web. As such, ExtendedWebView.web.tsx // uses an iframe when running on web. const iframeContainerStyles = { height: '100%', width: '100%' }; const wrapperStyle: ViewStyle = { height: '100%', width: '100%', flex: 1 }; const ExtendedWebView = (props: Props, ref: Ref) => { const iframeRef = useRef(null); useImperativeHandle(ref, (): WebViewControl => { return { injectJS(js: string) { if (!iframeRef.current) { logger.warn(`WebView(${props.webviewInstanceId}): Tried to inject JavaScript after the iframe has unloaded.`); return; } // react-native-webview doesn't seem to show a warning in the case where JavaScript // is injected before the first page loads. if (!iframeRef.current.contentWindow) { return; } iframeRef.current.contentWindow.postMessage({ injectJs: js, }, '*'); }, postMessage(message: unknown) { if (!iframeRef.current || !iframeRef.current.contentWindow) { logger.warn(`WebView(${props.webviewInstanceId}): Tried to post a message to an unloaded iframe.`); return; } iframeRef.current.contentWindow.postMessage({ postMessage: message, }, '*'); }, }; }, [props.webviewInstanceId]); const [containerElement, setContainerElement] = useState(); const containerRef = useRef(containerElement); containerRef.current = containerElement; const onMessageRef = useRef(props.onMessage); onMessageRef.current = props.onMessage; const onLoadEndRef = useRef(props.onLoadEnd); onLoadEndRef.current = props.onLoadEnd; const onLoadStartRef = useRef(props.onLoadStart); onLoadStartRef.current = props.onLoadStart; // Don't re-load when injected JS changes. This should match the behavior of the native webview. const injectedJavaScriptRef = useRef(props.injectedJavaScript); injectedJavaScriptRef.current = props.injectedJavaScript; useEffect(() => { const headHtml = ` `; const scripts = [ ` window.ReactNativeWebView = { postMessage: (message) => { parent.postMessage(message, '*'); }, supportsNonStringMessages: true, }; window.addEventListener('message', (event) => { if (event.source !== parent || event.origin === 'react-native') { return; } if (event.data.postMessage) { window.dispatchEvent( new MessageEvent( 'message', { data: event.data.postMessage, origin: 'react-native' }, ), ); } else if (event.data.injectJs) { eval('(() => { ' + event.data.injectJs + ' })()'); } }); `, injectedJavaScriptRef.current, ]; const { iframe } = makeSandboxedIframe({ bodyHtml: props.html, headHtml: headHtml, scripts, // allow-popups-to-escape-sandbox: Allows PDF previews to work on target="_blank" links. // allow-popups: Allows links to open in a new tab. permissions: 'allow-scripts allow-modals allow-popups allow-popups-to-escape-sandbox', allow: 'clipboard-write=(self) fullscreen=(self) autoplay=(self) local-fonts=* encrypted-media=*', }); if (containerRef.current) { containerRef.current.replaceChildren(iframe); } iframeRef.current = iframe; iframe.style.height = '100%'; iframe.style.width = '100%'; iframe.style.border = 'none'; const messageListener = (event: MessageEvent) => { if (event.source !== iframe.contentWindow) { return; } onMessageRef.current?.({ nativeEvent: { data: event.data } }); }; window.addEventListener('message', messageListener); if (!iframe.loading) { onLoadStartRef.current?.(); onLoadEndRef.current?.(); } else { iframe.onload = () => onLoadEndRef.current?.(); iframe.onloadstart = () => onLoadStartRef.current?.(); } return () => { window.removeEventListener('message', messageListener); if (iframeRef.current.parentElement) { iframeRef.current.remove(); } iframeRef.current = null; }; }, [props.html]); useEffect(() => { if (!iframeRef.current || !containerElement) return; if (iframeRef.current.parentElement) { iframeRef.current.remove(); } containerElement.replaceChildren(iframeRef.current); }, [containerElement]); return (
); }; export default forwardRef(ExtendedWebView);