react-native/Libraries/Components/WebView/WebView.ios.js

591 lines
16 KiB
JavaScript
Raw Normal View History

2015-03-14 08:22:25 +00:00
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
2015-03-14 08:22:25 +00:00
*
* @providesModule WebView
* @noflow
2015-03-14 08:22:25 +00:00
*/
'use strict';
var ActivityIndicator = require('ActivityIndicator');
2015-03-14 08:22:25 +00:00
var EdgeInsetsPropType = require('EdgeInsetsPropType');
var React = require('React');
var ReactNative = require('ReactNative');
2015-03-14 08:22:25 +00:00
var StyleSheet = require('StyleSheet');
var Text = require('Text');
var UIManager = require('UIManager');
2015-03-14 08:22:25 +00:00
var View = require('View');
var ScrollView = require('ScrollView');
2015-03-14 08:22:25 +00:00
var deprecatedPropType = require('deprecatedPropType');
var invariant = require('fbjs/lib/invariant');
var keyMirror = require('fbjs/lib/keyMirror');
var processDecelerationRate = require('processDecelerationRate');
var requireNativeComponent = require('requireNativeComponent');
var resolveAssetSource = require('resolveAssetSource');
2015-03-14 08:22:25 +00:00
var PropTypes = React.PropTypes;
var RCTWebViewManager = require('NativeModules').WebViewManager;
2015-03-14 08:22:25 +00:00
var BGWASH = 'rgba(255,255,255,0.8)';
2015-03-17 10:08:46 +00:00
var RCT_WEBVIEW_REF = 'webview';
2015-03-14 08:22:25 +00:00
var WebViewState = keyMirror({
IDLE: null,
LOADING: null,
ERROR: null,
});
const NavigationType = keyMirror({
click: true,
formsubmit: true,
backforward: true,
reload: true,
formresubmit: true,
other: true,
});
2015-03-14 08:22:25 +00:00
const JSNavigationScheme = 'react-js-navigation';
2015-03-25 19:55:10 +00:00
type ErrorEvent = {
domain: any,
code: any,
description: any,
2015-03-25 19:55:10 +00:00
}
type Event = Object;
const DataDetectorTypes = [
'phoneNumber',
'link',
'address',
'calendarEvent',
'none',
'all',
];
var defaultRenderLoading = () => (
<View style={styles.loadingView}>
<ActivityIndicator />
</View>
);
var 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.
*/
class WebView extends React.Component {
static JSNavigationScheme = JSNavigationScheme;
static NavigationType = NavigationType;
2015-03-14 08:22:25 +00:00
static propTypes = {
...View.propTypes,
html: deprecatedPropType(
PropTypes.string,
'Use the `source` prop instead.'
),
url: deprecatedPropType(
PropTypes.string,
'Use the `source` prop instead.'
),
/**
* Loads static html or a uri (with optional headers) in the WebView.
*/
source: PropTypes.oneOfType([
PropTypes.shape({
/*
* The URI to load in the `WebView`. Can be a local or remote file.
*/
uri: PropTypes.string,
/*
* The HTTP Method to use. Defaults to GET if not specified.
* NOTE: On Android, only GET and POST are supported.
*/
method: PropTypes.string,
/*
* Additional HTTP headers to send with the request.
* NOTE: On Android, this can only be used with GET requests.
*/
headers: PropTypes.object,
/*
* The HTTP body to send with the request. This must be a valid
* UTF-8 string, and will be sent exactly as specified, with no
* additional encoding (e.g. URL-escaping or base64) applied.
* NOTE: On Android, this can only be used with POST requests.
*/
body: PropTypes.string,
}),
PropTypes.shape({
/*
* A static HTML page to display in the WebView.
*/
html: PropTypes.string,
/*
* The base URL to be used for any relative links in the HTML.
*/
baseUrl: PropTypes.string,
}),
/*
* Used internally by packager.
*/
PropTypes.number,
]),
/**
* Function that returns a view to show if there's an error.
*/
renderError: PropTypes.func, // view to show if there's an error
/**
* Function that returns a loading indicator.
*/
renderLoading: PropTypes.func,
/**
* Function that is invoked when the `WebView` has finished loading.
*/
onLoad: PropTypes.func,
/**
* Function that is invoked when the `WebView` load succeeds or fails.
*/
onLoadEnd: PropTypes.func,
/**
* Function that is invoked when the `WebView` starts loading.
*/
onLoadStart: PropTypes.func,
/**
* Function that is invoked when the `WebView` load fails.
*/
onError: PropTypes.func,
/**
* Boolean value that determines whether the web view bounces
* when it reaches the edge of the content. The default value is `true`.
* @platform ios
*/
bounces: PropTypes.bool,
/**
* A floating-point number that determines how quickly the scroll view
* decelerates after the user lifts their finger. You may also use the
* string shortcuts `"normal"` and `"fast"` which match the underlying iOS
* settings for `UIScrollViewDecelerationRateNormal` and
* `UIScrollViewDecelerationRateFast` respectively:
*
* - normal: 0.998
* - fast: 0.99 (the default for iOS web view)
* @platform ios
*/
decelerationRate: ScrollView.propTypes.decelerationRate,
/**
* Boolean value that determines whether scrolling is enabled in the
* `WebView`. The default value is `true`.
* @platform ios
*/
scrollEnabled: PropTypes.bool,
/**
* Controls whether to adjust the content inset for web views that are
* placed behind a navigation bar, tab bar, or toolbar. The default value
* is `true`.
*/
2015-03-14 08:22:25 +00:00
automaticallyAdjustContentInsets: PropTypes.bool,
/**
* The amount by which the web view content is inset from the edges of
* the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
*/
2015-03-14 08:22:25 +00:00
contentInset: EdgeInsetsPropType,
/**
* Function that is invoked when the `WebView` loading starts or ends.
*/
2015-03-14 08:22:25 +00:00
onNavigationStateChange: PropTypes.func,
/**
* A function that is invoked when the webview calls `window.postMessage`.
* Setting this property will inject a `postMessage` global into your
* webview, but will still call pre-existing values of `postMessage`.
*
* `window.postMessage` accepts one argument, `data`, which will be
* available on the event object, `event.nativeEvent.data`. `data`
* must be a string.
*/
onMessage: PropTypes.func,
/**
* Boolean value that forces the `WebView` to show the loading view
* on the first load.
*/
startInLoadingState: PropTypes.bool,
/**
* The style to apply to the `WebView`.
*/
2015-03-14 08:22:25 +00:00
style: View.propTypes.style,
/**
* Determines the types of data converted to clickable URLs in the web views content.
* By default only phone numbers are detected.
*
* You can provide one type or an array of many types.
*
* Possible values for `dataDetectorTypes` are:
*
* - `'phoneNumber'`
* - `'link'`
* - `'address'`
* - `'calendarEvent'`
* - `'none'`
* - `'all'`
*
* @platform ios
*/
dataDetectorTypes: PropTypes.oneOfType([
PropTypes.oneOf(DataDetectorTypes),
PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)),
]),
/**
* Boolean value to enable JavaScript in the `WebView`. Used on Android only
* as JavaScript is enabled by default on iOS. The default value is `true`.
* @platform android
*/
javaScriptEnabled: PropTypes.bool,
/**
* Boolean value to control whether DOM Storage is enabled. Used only in
* Android.
* @platform android
*/
domStorageEnabled: PropTypes.bool,
/**
* Set this to provide JavaScript that will be injected into the web page
* when the view loads.
*/
injectedJavaScript: PropTypes.string,
/**
* Sets the user-agent for the `WebView`.
* @platform android
*/
userAgent: PropTypes.string,
/**
* Boolean that controls whether the web content is scaled to fit
* the view and enables the user to change the scale. The default value
* is `true`.
*/
scalesPageToFit: PropTypes.bool,
/**
* Function that allows custom handling of any web view requests. Return
* `true` from the function to continue loading the request and `false`
* to stop loading.
* @platform ios
*/
onShouldStartLoadWithRequest: PropTypes.func,
/**
* Boolean that determines whether HTML5 videos play inline or use the
* native full-screen controller. The default value is `false`.
*
* **NOTE** : In order for video to play inline, not only does this
* property need to be set to `true`, but the video element in the HTML
* document must also include the `webkit-playsinline` attribute.
* @platform ios
*/
allowsInlineMediaPlayback: PropTypes.bool,
/**
* Boolean that determines whether HTML5 audio and video requires the user
* to tap them before they start playing. The default value is `true`.
*/
mediaPlaybackRequiresUserAction: PropTypes.bool,
};
2015-03-14 08:22:25 +00:00
state = {
viewState: WebViewState.IDLE,
lastErrorEvent: (null: ?ErrorEvent),
startInLoadingState: true,
};
2015-03-14 08:22:25 +00:00
componentWillMount() {
2015-03-14 08:22:25 +00:00
if (this.props.startInLoadingState) {
this.setState({viewState: WebViewState.LOADING});
}
}
2015-03-14 08:22:25 +00:00
render() {
2015-03-14 08:22:25 +00:00
var otherView = null;
if (this.state.viewState === WebViewState.LOADING) {
otherView = (this.props.renderLoading || defaultRenderLoading)();
2015-03-14 08:22:25 +00:00
} else if (this.state.viewState === WebViewState.ERROR) {
var errorEvent = this.state.lastErrorEvent;
2015-03-25 19:55:10 +00:00
invariant(
errorEvent != null,
'lastErrorEvent expected to be non-null'
);
otherView = (this.props.renderError || defaultRenderError)(
2015-03-14 08:22:25 +00:00
errorEvent.domain,
errorEvent.code,
errorEvent.description
);
2015-03-14 08:22:25 +00:00
} else if (this.state.viewState !== WebViewState.IDLE) {
console.error(
'RCTWebView invalid state encountered: ' + this.state.loading
);
2015-03-14 08:22:25 +00:00
}
var webViewStyles = [styles.container, styles.webView, this.props.style];
2015-03-14 08:22:25 +00:00
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);
}
var onShouldStartLoadWithRequest = this.props.onShouldStartLoadWithRequest && ((event: Event) => {
var shouldStart = this.props.onShouldStartLoadWithRequest &&
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
RCTWebViewManager.startLoadWithResult(!!shouldStart, event.nativeEvent.lockIdentifier);
});
var decelerationRate = processDecelerationRate(this.props.decelerationRate);
var source = this.props.source || {};
if (this.props.html) {
source.html = this.props.html;
} else if (this.props.url) {
source.uri = this.props.url;
}
const messagingEnabled = typeof this.props.onMessage === 'function';
2015-03-14 08:22:25 +00:00
var webView =
<RCTWebView
2015-03-17 10:08:46 +00:00
ref={RCT_WEBVIEW_REF}
2015-03-14 08:22:25 +00:00
key="webViewKey"
style={webViewStyles}
source={resolveAssetSource(source)}
injectedJavaScript={this.props.injectedJavaScript}
bounces={this.props.bounces}
scrollEnabled={this.props.scrollEnabled}
decelerationRate={decelerationRate}
2015-03-14 08:22:25 +00:00
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets}
onLoadingStart={this._onLoadingStart}
onLoadingFinish={this._onLoadingFinish}
onLoadingError={this._onLoadingError}
messagingEnabled={messagingEnabled}
onMessage={this._onMessage}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
scalesPageToFit={this.props.scalesPageToFit}
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
mediaPlaybackRequiresUserAction={this.props.mediaPlaybackRequiresUserAction}
dataDetectorTypes={this.props.dataDetectorTypes}
2015-03-14 08:22:25 +00:00
/>;
return (
<View style={styles.container}>
{webView}
{otherView}
</View>
);
}
2015-03-14 08:22:25 +00:00
/**
* Go forward one page in the web view's history.
*/
goForward = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.goForward,
null
);
};
2015-03-14 08:22:25 +00:00
/**
* Go back one page in the web view's history.
*/
goBack = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.goBack,
null
);
};
2015-03-14 08:22:25 +00:00
/**
* Reloads the current page.
*/
reload = () => {
this.setState({viewState: WebViewState.LOADING});
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.reload,
null
);
};
2015-03-14 08:22:25 +00:00
/**
* Stop loading the current page.
*/
stopLoading = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.stopLoading,
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; });
* ```
*/
postMessage = (data) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.postMessage,
[String(data)]
);
};
2015-03-14 08:22:25 +00:00
/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
*/
_updateNavigationState = (event: Event) => {
2015-03-14 08:22:25 +00:00
if (this.props.onNavigationStateChange) {
this.props.onNavigationStateChange(event.nativeEvent);
}
};
2015-03-14 08:22:25 +00:00
/**
* Returns the native `WebView` node.
*/
getWebViewHandle = (): any => {
return ReactNative.findNodeHandle(this.refs[RCT_WEBVIEW_REF]);
};
2015-03-14 08:22:25 +00:00
_onLoadingStart = (event: Event) => {
var onLoadStart = this.props.onLoadStart;
onLoadStart && onLoadStart(event);
this._updateNavigationState(event);
};
2015-03-14 08:22:25 +00:00
_onLoadingError = (event: Event) => {
2015-03-14 08:22:25 +00:00
event.persist(); // persist this event because we need to store it
var {onError, onLoadEnd} = this.props;
onError && onError(event);
onLoadEnd && onLoadEnd(event);
2015-08-27 21:04:59 +00:00
console.warn('Encountered an error loading page', event.nativeEvent);
2015-03-14 08:22:25 +00:00
this.setState({
lastErrorEvent: event.nativeEvent,
viewState: WebViewState.ERROR
});
};
2015-03-14 08:22:25 +00:00
_onLoadingFinish = (event: Event) => {
var {onLoad, onLoadEnd} = this.props;
onLoad && onLoad(event);
onLoadEnd && onLoadEnd(event);
2015-03-14 08:22:25 +00:00
this.setState({
viewState: WebViewState.IDLE,
});
this._updateNavigationState(event);
};
_onMessage = (event: Event) => {
var {onMessage} = this.props;
onMessage && onMessage(event);
}
}
2015-03-14 08:22:25 +00:00
var RCTWebView = requireNativeComponent('RCTWebView', WebView, {
nativeOnly: {
onLoadingStart: true,
onLoadingError: true,
onLoadingFinish: true,
onMessage: true,
messagingEnabled: PropTypes.bool,
},
});
2015-03-14 08:22:25 +00:00
var 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,
},
2015-03-14 08:22:25 +00:00
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',
}
2015-03-14 08:22:25 +00:00
});
module.exports = WebView;