1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
joplin/packages/app-mobile/components/ExtendedWebView/index.web.tsx
2024-08-02 14:51:49 +01:00

177 lines
5.0 KiB
TypeScript

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<WebViewControl>) => {
const iframeRef = useRef<HTMLIFrameElement|null>(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<HTMLDivElement>();
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 = `
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<meta charset="utf-8"/>
<!-- Open links in a new window by default -->
<base target="_blank"/>
`;
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 (
<View style={[wrapperStyle, props.style]}>
<div
ref={setContainerElement}
className='iframe-container'
style={iframeContainerStyles}
></div>
</View>
);
};
export default forwardRef(ExtendedWebView);