react-native-webview/js/WebView.ios.js

511 lines
13 KiB
JavaScript
Raw Normal View History

/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
2018-09-10 04:40:32 +00:00
* @flow
*/
2018-07-31 05:34:16 +00:00
import React from 'react';
import {
ActivityIndicator,
Linking,
StyleSheet,
Text,
UIManager,
View,
requireNativeComponent,
NativeModules,
2018-10-12 22:40:20 +00:00
Image,
findNodeHandle,
2018-07-31 05:34:16 +00:00
} from 'react-native';
import invariant from 'fbjs/lib/invariant';
import keyMirror from 'fbjs/lib/keyMirror';
import WebViewShared from './WebViewShared';
2018-09-10 04:40:32 +00:00
import type {
WebViewEvent,
2018-09-10 18:13:20 +00:00
WebViewError,
WebViewErrorEvent,
WebViewMessageEvent,
WebViewNavigationEvent,
2018-09-10 04:40:32 +00:00
WebViewSharedProps,
WebViewSource,
WebViewProgressEvent,
2018-09-10 04:40:32 +00:00
} from './WebViewTypes';
2018-07-31 05:34:16 +00:00
const resolveAssetSource = Image.resolveAssetSource;
// Imported from https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/processDecelerationRate.js
function processDecelerationRate(decelerationRate) {
if (decelerationRate === 'normal') {
decelerationRate = 0.998;
} else if (decelerationRate === 'fast') {
decelerationRate = 0.99;
}
return decelerationRate;
}
2018-09-18 22:46:19 +00:00
const RNCUIWebViewManager = NativeModules.RNCUIWebViewManager;
const RNCWKWebViewManager = NativeModules.RNCWKWebViewManager;
2018-07-31 03:21:48 +00:00
const BGWASH = 'rgba(255,255,255,0.8)';
const WebViewState = keyMirror({
IDLE: null,
LOADING: null,
ERROR: null,
});
const NavigationType = keyMirror({
click: true,
formsubmit: true,
backforward: true,
reload: true,
formresubmit: true,
other: true,
});
const JSNavigationScheme = 'react-js-navigation';
2018-09-10 04:40:32 +00:00
type State = {|
viewState: WebViewState,
2018-09-10 18:13:20 +00:00
lastErrorEvent: ?WebViewError,
2018-09-10 04:40:32 +00:00
|};
2018-07-31 03:21:48 +00:00
const DataDetectorTypes = [
'phoneNumber',
'link',
'address',
'calendarEvent',
'trackingNumber',
'flightNumber',
'lookupSuggestion',
2018-07-31 03:21:48 +00:00
'none',
'all',
];
const defaultRenderLoading = () => (
<View style={styles.loadingView}>
<ActivityIndicator />
</View>
);
const defaultRenderError = (errorDomain, errorCode, errorDesc) => (
<View style={styles.errorContainer}>
<Text style={styles.errorTextTitle}>Error loading page</Text>
<Text style={styles.errorText}>{'Domain: ' + errorDomain}</Text>
<Text style={styles.errorText}>{'Error Code: ' + errorCode}</Text>
<Text style={styles.errorText}>{'Description: ' + errorDesc}</Text>
</View>
);
/**
* `WebView` renders web content in a native view.
*
*```
* import React, { Component } from 'react';
* import { WebView } from 'react-native';
*
* class MyWeb extends Component {
* render() {
* return (
* <WebView
* source={{uri: 'https://github.com/facebook/react-native'}}
* style={{marginTop: 20}}
* />
* );
* }
* }
*```
*
* You can use this component to navigate back and forth in the web view's
* history and configure various properties for the web content.
*/
2018-09-10 04:40:32 +00:00
class WebView extends React.Component<WebViewSharedProps, State> {
2018-07-31 03:21:48 +00:00
static JSNavigationScheme = JSNavigationScheme;
static NavigationType = NavigationType;
static defaultProps = {
useWebKit: true,
2018-07-31 03:21:48 +00:00
originWhitelist: WebViewShared.defaultOriginWhitelist,
};
static isFileUploadSupported = async () => {
// no native implementation for iOS, depends only on permissions
return true;
}
2018-07-31 03:21:48 +00:00
state = {
2018-10-12 22:40:20 +00:00
viewState: this.props.startInLoadingState
? WebViewState.LOADING
: WebViewState.IDLE,
2018-09-10 18:13:20 +00:00
lastErrorEvent: null,
2018-07-31 03:21:48 +00:00
};
2018-09-18 22:46:19 +00:00
webViewRef = React.createRef();
2018-07-31 03:21:48 +00:00
UNSAFE_componentWillMount() {
if (
this.props.useWebKit === true &&
this.props.scalesPageToFit !== undefined
) {
console.warn(
'The scalesPageToFit property is not supported when useWebKit = true',
);
}
if (
!this.props.useWebKit &&
this.props.allowsBackForwardNavigationGestures
) {
console.warn(
'The allowsBackForwardNavigationGestures property is not supported when useWebKit = false',
);
}
2018-07-31 03:21:48 +00:00
}
render() {
let otherView = null;
let scalesPageToFit;
if (this.props.useWebKit) {
({ scalesPageToFit } = this.props);
} else {
({ scalesPageToFit = true } = this.props);
}
2018-07-31 03:21:48 +00:00
if (this.state.viewState === WebViewState.LOADING) {
otherView = (this.props.renderLoading || defaultRenderLoading)();
} else if (this.state.viewState === WebViewState.ERROR) {
const errorEvent = this.state.lastErrorEvent;
invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
otherView = (this.props.renderError || defaultRenderError)(
errorEvent.domain,
errorEvent.code,
errorEvent.description,
);
} else if (this.state.viewState !== WebViewState.IDLE) {
console.error(
2018-09-18 22:46:19 +00:00
'RNCWebView invalid state encountered: ' + this.state.viewState,
2018-07-31 03:21:48 +00:00
);
}
const webViewStyles = [styles.container, styles.webView, this.props.style];
if (
this.state.viewState === WebViewState.LOADING ||
this.state.viewState === WebViewState.ERROR
) {
// if we're in either LOADING or ERROR states, don't show the webView
webViewStyles.push(styles.hidden);
}
const nativeConfig = this.props.nativeConfig || {};
let viewManager = nativeConfig.viewManager;
2018-07-31 03:21:48 +00:00
if (this.props.useWebKit) {
2018-09-18 22:46:19 +00:00
viewManager = viewManager || RNCWKWebViewManager;
} else {
2018-09-18 22:46:19 +00:00
viewManager = viewManager || RNCUIWebViewManager;
}
const compiledWhitelist = [
'about:blank',
...(this.props.originWhitelist || []),
].map(WebViewShared.originWhitelistToRegex);
2018-10-12 22:40:20 +00:00
const onShouldStartLoadWithRequest = event => {
2018-07-31 03:21:48 +00:00
let shouldStart = true;
const { url } = event.nativeEvent;
const origin = WebViewShared.extractOrigin(url);
const passesWhitelist = compiledWhitelist.some(x =>
new RegExp(x).test(origin),
);
shouldStart = shouldStart && passesWhitelist;
if (!passesWhitelist) {
Linking.openURL(url);
}
if (this.props.onShouldStartLoadWithRequest) {
shouldStart =
shouldStart &&
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
}
2018-09-10 04:40:32 +00:00
invariant(viewManager != null, 'viewManager expected to be non-null');
2018-07-31 03:21:48 +00:00
viewManager.startLoadWithResult(
!!shouldStart,
event.nativeEvent.lockIdentifier,
);
};
const decelerationRate = processDecelerationRate(
this.props.decelerationRate,
);
2018-09-10 18:13:20 +00:00
let source: WebViewSource = this.props.source || {};
2018-09-10 04:40:32 +00:00
if (!this.props.source && this.props.html) {
source = { html: this.props.html };
} else if (!this.props.source && this.props.url) {
source = { uri: this.props.url };
2018-07-31 03:21:48 +00:00
}
const messagingEnabled = typeof this.props.onMessage === 'function';
let NativeWebView = nativeConfig.component;
if (this.props.useWebKit) {
2018-09-18 22:46:19 +00:00
NativeWebView = NativeWebView || RNCWKWebView;
} else {
2018-09-18 22:46:19 +00:00
NativeWebView = NativeWebView || RNCUIWebView;
}
2018-07-31 03:21:48 +00:00
const webView = (
<NativeWebView
2018-09-18 23:25:02 +00:00
ref={this.webViewRef}
2018-07-31 03:21:48 +00:00
key="webViewKey"
style={webViewStyles}
source={resolveAssetSource(source)}
injectedJavaScript={this.props.injectedJavaScript}
bounces={this.props.bounces}
scrollEnabled={this.props.scrollEnabled}
decelerationRate={decelerationRate}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={
this.props.automaticallyAdjustContentInsets
}
hideKeyboardAccessoryView={this.props.hideKeyboardAccessoryView}
allowsBackForwardNavigationGestures={this.props.allowsBackForwardNavigationGestures}
userAgent={this.props.userAgent}
2018-07-31 03:21:48 +00:00
onLoadingStart={this._onLoadingStart}
onLoadingFinish={this._onLoadingFinish}
onLoadingError={this._onLoadingError}
onLoadingProgress={this._onLoadingProgress}
2018-07-31 03:21:48 +00:00
messagingEnabled={messagingEnabled}
onMessage={this._onMessage}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
scalesPageToFit={scalesPageToFit}
2018-07-31 03:21:48 +00:00
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
mediaPlaybackRequiresUserAction={
this.props.mediaPlaybackRequiresUserAction
}
dataDetectorTypes={this.props.dataDetectorTypes}
{...nativeConfig.props}
/>
);
return (
<View style={styles.container}>
{webView}
{otherView}
</View>
);
}
_getCommands() {
if (!this.props.useWebKit) {
2018-09-18 22:46:19 +00:00
return UIManager.RNCUIWebView.Commands;
}
2018-09-18 22:46:19 +00:00
return UIManager.RNCWKWebView.Commands;
}
2018-07-31 03:21:48 +00:00
/**
* Go forward one page in the web view's history.
*/
goForward = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this._getCommands().goForward,
2018-07-31 03:21:48 +00:00
null,
);
};
/**
* Go back one page in the web view's history.
*/
goBack = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this._getCommands().goBack,
2018-07-31 03:21:48 +00:00
null,
);
};
/**
* Reloads the current page.
*/
reload = () => {
this.setState({ viewState: WebViewState.LOADING });
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this._getCommands().reload,
2018-07-31 03:21:48 +00:00
null,
);
};
/**
* Stop loading the current page.
*/
stopLoading = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this._getCommands().stopLoading,
2018-07-31 03:21:48 +00:00
null,
);
};
/**
* 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; });
* ```
*/
2018-09-10 04:40:32 +00:00
postMessage = (data: string) => {
2018-07-31 03:21:48 +00:00
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this._getCommands().postMessage,
2018-07-31 03:21:48 +00:00
[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.
*/
2018-09-10 04:40:32 +00:00
injectJavaScript = (data: string) => {
2018-07-31 03:21:48 +00:00
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this._getCommands().injectJavaScript,
2018-07-31 03:21:48 +00:00
[data],
);
};
/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
*/
2018-09-10 18:13:20 +00:00
_updateNavigationState = (event: WebViewNavigationEvent) => {
2018-07-31 03:21:48 +00:00
if (this.props.onNavigationStateChange) {
this.props.onNavigationStateChange(event.nativeEvent);
}
};
/**
* Returns the native `WebView` node.
*/
getWebViewHandle = () => {
2018-10-12 22:40:20 +00:00
return findNodeHandle(this.webViewRef.current);
2018-07-31 03:21:48 +00:00
};
2018-09-10 18:13:20 +00:00
_onLoadingStart = (event: WebViewNavigationEvent) => {
2018-07-31 03:21:48 +00:00
const onLoadStart = this.props.onLoadStart;
onLoadStart && onLoadStart(event);
this._updateNavigationState(event);
};
2018-09-10 18:13:20 +00:00
_onLoadingError = (event: WebViewErrorEvent) => {
2018-07-31 03:21:48 +00:00
event.persist(); // persist this event because we need to store it
const { onError, onLoadEnd } = this.props;
onError && onError(event);
onLoadEnd && onLoadEnd(event);
console.warn('Encountered an error loading page', event.nativeEvent);
this.setState({
lastErrorEvent: event.nativeEvent,
viewState: WebViewState.ERROR,
});
};
2018-09-10 18:13:20 +00:00
_onLoadingFinish = (event: WebViewNavigationEvent) => {
2018-07-31 03:21:48 +00:00
const { onLoad, onLoadEnd } = this.props;
onLoad && onLoad(event);
onLoadEnd && onLoadEnd(event);
this.setState({
viewState: WebViewState.IDLE,
});
this._updateNavigationState(event);
};
2018-09-10 18:13:20 +00:00
_onMessage = (event: WebViewMessageEvent) => {
2018-07-31 03:21:48 +00:00
const { onMessage } = this.props;
onMessage && onMessage(event);
};
_onLoadingProgress = (event: WebViewProgressEvent) => {
const {onLoadProgress} = this.props;
onLoadProgress && onLoadProgress(event);
}
2018-09-10 04:40:32 +00:00
componentDidUpdate(prevProps: WebViewSharedProps) {
if (!(prevProps.useWebKit && this.props.useWebKit)) {
return;
}
this._showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
this._showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
this._showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
if (this.props.scalesPageToFit !== undefined) {
console.warn(
'The scalesPageToFit property is not supported when useWebKit = true',
);
}
}
2018-09-10 04:40:32 +00:00
_showRedboxOnPropChanges(prevProps, propName: string) {
if (this.props[propName] !== prevProps[propName]) {
console.error(
`Changes to property ${propName} do nothing after the initial render.`,
);
}
}
2018-07-31 03:21:48 +00:00
}
2018-09-18 22:46:19 +00:00
const RNCUIWebView = requireNativeComponent('RNCUIWebView');
const RNCWKWebView = requireNativeComponent('RNCWKWebView');
2018-07-31 03:21:48 +00:00
const styles = StyleSheet.create({
container: {
flex: 1,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: BGWASH,
},
errorText: {
fontSize: 14,
textAlign: 'center',
marginBottom: 2,
},
errorTextTitle: {
fontSize: 15,
fontWeight: '500',
marginBottom: 10,
},
hidden: {
height: 0,
flex: 0, // disable 'flex:1' when hiding a View
},
loadingView: {
backgroundColor: BGWASH,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: 100,
},
webView: {
backgroundColor: '#ffffff',
},
});
module.exports = WebView;