/** * 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. * * @providesModule WebView * @flow */ 'use strict'; var ActivityIndicatorIOS = require('ActivityIndicatorIOS'); var EdgeInsetsPropType = require('EdgeInsetsPropType'); var React = require('React'); var StyleSheet = require('StyleSheet'); var Text = require('Text'); var UIManager = require('UIManager'); var View = require('View'); var ScrollView = require('ScrollView') var processDecelerationRate = require('processDecelerationRate'); var invariant = require('invariant'); var keyMirror = require('keyMirror'); var requireNativeComponent = require('requireNativeComponent'); var PropTypes = React.PropTypes; var RCTWebViewManager = require('NativeModules').WebViewManager; var BGWASH = 'rgba(255,255,255,0.8)'; var RCT_WEBVIEW_REF = 'webview'; var WebViewState = keyMirror({ IDLE: null, LOADING: null, ERROR: null, }); var NavigationType = { click: RCTWebViewManager.NavigationType.LinkClicked, formsubmit: RCTWebViewManager.NavigationType.FormSubmitted, backforward: RCTWebViewManager.NavigationType.BackForward, reload: RCTWebViewManager.NavigationType.Reload, formresubmit: RCTWebViewManager.NavigationType.FormResubmitted, other: RCTWebViewManager.NavigationType.Other, }; var JSNavigationScheme = RCTWebViewManager.JSNavigationScheme; type ErrorEvent = { domain: any; code: any; description: any; } type Event = Object; var defaultRenderLoading = () => ( ); var defaultRenderError = (errorDomain, errorCode, errorDesc) => ( Error loading page {'Domain: ' + errorDomain} {'Error Code: ' + errorCode} {'Description: ' + errorDesc} ); /** * Renders a native WebView. */ var WebView = React.createClass({ statics: { JSNavigationScheme: JSNavigationScheme, NavigationType: NavigationType, }, propTypes: { ...View.propTypes, url: PropTypes.string, html: PropTypes.string, /** * 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, /** * Invoked when load finish */ onLoad: PropTypes.func, /** * Invoked when load either succeeds or fails */ onLoadEnd: PropTypes.func, /** * Invoked on load start */ onLoadStart: PropTypes.func, /** * Invoked when load fails */ onError: PropTypes.func, /** * @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 string * shortcuts `"normal"` and `"fast"` which match the underlying iOS settings * for `UIScrollViewDecelerationRateNormal` and * `UIScrollViewDecelerationRateFast` respectively. * - Normal: 0.998 * - Fast: 0.9 (the default for iOS WebView) * @platform ios */ decelerationRate: ScrollView.propTypes.decelerationRate, /** * @platform ios */ scrollEnabled: PropTypes.bool, automaticallyAdjustContentInsets: PropTypes.bool, contentInset: EdgeInsetsPropType, onNavigationStateChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load style: View.propTypes.style, /** * Used on Android only, JS is enabled by default for WebView on iOS * @platform android */ javaScriptEnabled: PropTypes.bool, /** * Used on Android only, controls whether DOM Storage is enabled or not * @platform android */ domStorageEnabled: PropTypes.bool, /** * Sets the JS to be injected when the webpage loads. */ injectedJavaScript: PropTypes.string, /** * Sets whether the webpage scales to fit the view and the user can change the scale. * @platform ios */ scalesPageToFit: PropTypes.bool, /** * Allows custom handling of any webview requests by a JS handler. Return true * or false from this method to continue loading the request. * @platform ios */ onShouldStartLoadWithRequest: PropTypes.func, /** * Determines whether HTML5 videos play inline or use the native full-screen * controller. * default value `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, }, getInitialState: function() { return { viewState: WebViewState.IDLE, lastErrorEvent: (null: ?ErrorEvent), startInLoadingState: true, }; }, componentWillMount: function() { if (this.props.startInLoadingState) { this.setState({viewState: WebViewState.LOADING}); } }, render: function() { var otherView = null; if (this.state.viewState === WebViewState.LOADING) { otherView = (this.props.renderLoading || defaultRenderLoading)(); } else if (this.state.viewState === WebViewState.ERROR) { var 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( 'RCTWebView invalid state encountered: ' + this.state.loading ); } var 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); } var onShouldStartLoadWithRequest = this.props.onShouldStartLoadWithRequest && ((event: Event) => { var shouldStart = this.props.onShouldStartLoadWithRequest && this.props.onShouldStartLoadWithRequest(event.nativeEvent); RCTWebViewManager.startLoadWithResult(!!shouldStart, event.nativeEvent.lockIdentifier); }); var {javaScriptEnabled, domStorageEnabled} = this.props; if (this.props.javaScriptEnabledAndroid) { console.warn('javaScriptEnabledAndroid is deprecated. Use javaScriptEnabled instead'); javaScriptEnabled = this.props.javaScriptEnabledAndroid; } if (this.props.domStorageEnabledAndroid) { console.warn('domStorageEnabledAndroid is deprecated. Use domStorageEnabled instead'); domStorageEnabled = this.props.domStorageEnabledAndroid; } var decelerationRate = processDecelerationRate(this.props.decelerationRate); var webView = ; return ( {webView} {otherView} ); }, goForward: function() { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RCTWebView.Commands.goForward, null ); }, goBack: function() { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RCTWebView.Commands.goBack, null ); }, reload: function() { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RCTWebView.Commands.reload, null ); }, /** * We return an event with a bunch of fields including: * url, title, loading, canGoBack, canGoForward */ updateNavigationState: function(event: Event) { if (this.props.onNavigationStateChange) { this.props.onNavigationStateChange(event.nativeEvent); } }, getWebViewHandle: function(): any { return React.findNodeHandle(this.refs[RCT_WEBVIEW_REF]); }, onLoadingStart: function(event: Event) { var onLoadStart = this.props.onLoadStart; onLoadStart && onLoadStart(event); this.updateNavigationState(event); }, onLoadingError: function(event: Event) { event.persist(); // persist this event because we need to store it var {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 }); }, onLoadingFinish: function(event: Event) { var {onLoad, onLoadEnd} = this.props; onLoad && onLoad(event); onLoadEnd && onLoadEnd(event); this.setState({ viewState: WebViewState.IDLE, }); this.updateNavigationState(event); }, }); var RCTWebView = requireNativeComponent('RCTWebView', WebView, { nativeOnly: { onLoadingStart: true, onLoadingError: true, onLoadingFinish: true, }, }); 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, }, hidden: { height: 0, flex: 0, // disable 'flex:1' when hiding a View }, loadingView: { backgroundColor: BGWASH, flex: 1, justifyContent: 'center', alignItems: 'center', }, webView: { backgroundColor: '#ffffff', } }); module.exports = WebView;