react-native-webview/src/WebView.macos.tsx

348 lines
9.0 KiB
TypeScript

import React from 'react';
import {
UIManager as NotTypedUIManager,
View,
requireNativeComponent,
NativeModules,
Image,
findNodeHandle,
ImageSourcePropType,
} from 'react-native';
import invariant from 'invariant';
import {
defaultOriginWhitelist,
createOnShouldStartLoadWithRequest,
defaultRenderError,
defaultRenderLoading,
} from './WebViewShared';
import {
WebViewErrorEvent,
WebViewHttpErrorEvent,
WebViewMessageEvent,
WebViewNavigationEvent,
WebViewProgressEvent,
WebViewTerminatedEvent,
MacOSWebViewProps,
NativeWebViewMacOS,
ViewManager,
State,
RNCWebViewUIManagerMacOS,
} from './WebViewTypes';
import styles from './WebView.styles';
const UIManager = NotTypedUIManager as RNCWebViewUIManagerMacOS;
const { resolveAssetSource } = Image;
const RNCWebViewManager = NativeModules.RNCWebViewManager as ViewManager;
const RNCWebView: typeof NativeWebViewMacOS = requireNativeComponent(
'RNCWebView',
);
class WebView extends React.Component<MacOSWebViewProps, State> {
static defaultProps = {
javaScriptEnabled: true,
cacheEnabled: true,
originWhitelist: defaultOriginWhitelist,
useSharedProcessPool: true,
};
static isFileUploadSupported = async () => {
// no native implementation for macOS, depends only on permissions
return true;
};
state: State = {
viewState: this.props.startInLoadingState ? 'LOADING' : 'IDLE',
lastErrorEvent: null,
};
webViewRef = React.createRef<NativeWebViewMacOS>();
// eslint-disable-next-line react/sort-comp
getCommands = () => UIManager.getViewManagerConfig('RNCWebView').Commands;
/**
* Go forward one page in the web view's history.
*/
goForward = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().goForward,
undefined,
);
};
/**
* Go back one page in the web view's history.
*/
goBack = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().goBack,
undefined,
);
};
/**
* Reloads the current page.
*/
reload = () => {
this.setState({ viewState: 'LOADING' });
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().reload,
undefined,
);
};
/**
* Stop loading the current page.
*/
stopLoading = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().stopLoading,
undefined,
);
};
/**
* Request focus on WebView rendered page.
*/
requestFocus = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().requestFocus,
undefined,
);
};
/**
* Posts a message to the web view, which will emit a `message` event.
* Accepts one argument, `data`, which must be a string.
*
* In your webview, you'll need to something like the following.
*
* ```js
* document.addEventListener('message', e => { document.title = e.data; });
* ```
*/
postMessage = (data: string) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().postMessage,
[String(data)],
);
};
/**
* Injects a javascript string into the referenced WebView. Deliberately does not
* return a response because using eval() to return a response breaks this method
* on pages with a Content Security Policy that disallows eval(). If you need that
* functionality, look into postMessage/onMessage.
*/
injectJavaScript = (data: string) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().injectJavaScript,
[data],
);
};
/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
*/
updateNavigationState = (event: WebViewNavigationEvent) => {
if (this.props.onNavigationStateChange) {
this.props.onNavigationStateChange(event.nativeEvent);
}
};
/**
* Returns the native `WebView` node.
*/
getWebViewHandle = () => {
const nodeHandle = findNodeHandle(this.webViewRef.current);
invariant(nodeHandle != null, 'nodeHandle expected to be non-null');
return nodeHandle as number;
};
onLoadingStart = (event: WebViewNavigationEvent) => {
const { onLoadStart } = this.props;
if (onLoadStart) {
onLoadStart(event);
}
this.updateNavigationState(event);
};
onLoadingError = (event: WebViewErrorEvent) => {
event.persist(); // persist this event because we need to store it
const { onError, onLoadEnd } = this.props;
if (onLoadEnd) {
onLoadEnd(event);
}
if (onError) {
onError(event);
}
console.warn('Encountered an error loading page', event.nativeEvent);
this.setState({
lastErrorEvent: event.nativeEvent,
viewState: 'ERROR',
});
};
onHttpError = (event: WebViewHttpErrorEvent) => {
const { onHttpError } = this.props;
if (onHttpError) {
onHttpError(event);
}
}
onLoadingFinish = (event: WebViewNavigationEvent) => {
const { onLoad, onLoadEnd } = this.props;
if (onLoad) {
onLoad(event);
}
if (onLoadEnd) {
onLoadEnd(event);
}
this.setState({
viewState: 'IDLE',
});
this.updateNavigationState(event);
};
onMessage = (event: WebViewMessageEvent) => {
const { onMessage } = this.props;
if (onMessage) {
onMessage(event);
}
};
onLoadingProgress = (event: WebViewProgressEvent) => {
const { onLoadProgress } = this.props;
if (onLoadProgress) {
onLoadProgress(event);
}
};
onShouldStartLoadWithRequestCallback = (
shouldStart: boolean,
_url: string,
lockIdentifier: number,
) => {
const viewManager
= (this.props.nativeConfig && this.props.nativeConfig.viewManager)
|| RNCWebViewManager;
viewManager.startLoadWithResult(!!shouldStart, lockIdentifier);
};
onContentProcessDidTerminate = (event: WebViewTerminatedEvent) => {
const { onContentProcessDidTerminate } = this.props;
if (onContentProcessDidTerminate) {
onContentProcessDidTerminate(event);
}
};
componentDidUpdate(prevProps: MacOSWebViewProps) {
this.showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
this.showRedboxOnPropChanges(prevProps, 'incognito');
this.showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
}
showRedboxOnPropChanges(
prevProps: MacOSWebViewProps,
propName: keyof MacOSWebViewProps,
) {
if (this.props[propName] !== prevProps[propName]) {
console.error(
`Changes to property ${propName} do nothing after the initial render.`,
);
}
}
render() {
const {
nativeConfig = {},
onMessage,
onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp,
originWhitelist,
renderError,
renderLoading,
style,
containerStyle,
...otherProps
} = this.props;
let otherView = null;
if (this.state.viewState === 'LOADING') {
otherView = (renderLoading || defaultRenderLoading)();
} else if (this.state.viewState === 'ERROR') {
const errorEvent = this.state.lastErrorEvent;
invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
otherView = (renderError || defaultRenderError)(
errorEvent.domain,
errorEvent.code,
errorEvent.description,
);
} else if (this.state.viewState !== 'IDLE') {
console.error(
`RNCWebView invalid state encountered: ${this.state.viewState}`,
);
}
const webViewStyles = [styles.container, styles.webView, style];
const webViewContainerStyle = [styles.container, containerStyle];
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
this.onShouldStartLoadWithRequestCallback,
// casting cause it's in the default props
originWhitelist as readonly string[],
onShouldStartLoadWithRequestProp,
);
const NativeWebView
= (nativeConfig.component as typeof NativeWebViewMacOS | undefined)
|| RNCWebView;
const webView = (
<NativeWebView
key="webViewKey"
{...otherProps}
messagingEnabled={typeof onMessage === 'function'}
onLoadingError={this.onLoadingError}
onLoadingFinish={this.onLoadingFinish}
onLoadingProgress={this.onLoadingProgress}
onLoadingStart={this.onLoadingStart}
onHttpError={this.onHttpError}
onMessage={this.onMessage}
onScroll={this.props.onScroll}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
onContentProcessDidTerminate={this.onContentProcessDidTerminate}
ref={this.webViewRef}
// TODO: find a better way to type this.
source={resolveAssetSource(this.props.source as ImageSourcePropType)}
style={webViewStyles}
{...nativeConfig.props}
/>
);
return (
<View style={webViewContainerStyle}>
{webView}
{otherView}
</View>
);
}
}
export default WebView;