mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
177 lines
5.0 KiB
TypeScript
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);
|