diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index 711d1157d..b7108681e 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -40,6 +40,7 @@ var EXAMPLES = [ require('./AsyncStorageExample'), require('./CameraRollExample.ios'), require('./MapViewExample'), + require('./WebViewExample'), require('./AppStateIOSExample'), require('./AlertIOSExample'), require('./AdSupportIOSExample'), diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js new file mode 100644 index 000000000..4b7a1513d --- /dev/null +++ b/Examples/UIExplorer/WebViewExample.js @@ -0,0 +1,264 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var React = require('react-native'); +var StyleSheet = require('StyleSheet'); +var { + ActivityIndicatorIOS, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, + WebView +} = React; + +var HEADER = '#3b5998'; +var BGWASH = 'rgba(255,255,255,0.8)'; +var DISABLED_WASH = 'rgba(255,255,255,0.25)'; + +var TEXT_INPUT_REF = 'urlInput'; +var WEBVIEW_REF = 'webview'; +var DEFAULT_URL = 'https://m.facebook.com'; + +var WebViewExample = React.createClass({ + + getInitialState: function() { + return { + url: DEFAULT_URL, + status: 'No Page Loaded', + backButtonEnabled: false, + forwardButtonEnabled: false, + loading: true, + }; + }, + + handleTextInputChange: function(event) { + this.inputText = event.nativeEvent.text; + }, + + render: function() { + this.inputText = this.state.url; + + return ( + + + + + + {'<'} + + + + + + + {'>'} + + + + + + + + Go! + + + + + + + {this.state.status} + + + ); + }, + + goBack: function() { + this.refs[WEBVIEW_REF].goBack(); + }, + + goForward: function() { + this.refs[WEBVIEW_REF].goForward(); + }, + + reload: function() { + this.refs[WEBVIEW_REF].reload(); + }, + + onNavigationStateChange: function(navState) { + this.setState({ + backButtonEnabled: navState.canGoBack, + forwardButtonEnabled: navState.canGoForward, + url: navState.url, + status: navState.title, + loading: navState.loading, + }); + }, + + renderErrorView: function(errorDomain, errorCode, errorDesc) { + return ( + + + Error loading page + + + {'Domain: ' + errorDomain} + + + {'Error Code: ' + errorCode} + + + {'Description: ' + errorDesc} + + + ); + }, + + renderLoadingView: function() { + return ( + + + + ); + }, + + onSubmitEditing: function(event) { + this.pressGoButton(); + }, + + pressGoButton: function() { + var url = this.inputText.toLowerCase(); + if (url === this.state.url) { + this.reload(); + } else { + this.setState({ + url: url, + }); + } + // dismiss keyoard + this.refs[TEXT_INPUT_REF].blur(); + }, + +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: HEADER, + }, + addressBarRow: { + flexDirection: 'row', + padding: 8, + }, + webView: { + backgroundColor: BGWASH, + height: 350, + }, + addressBarTextInput: { + backgroundColor: BGWASH, + borderColor: 'transparent', + borderRadius: 3, + borderWidth: 1, + height: 24, + paddingLeft: 10, + paddingTop: 3, + paddingBottom: 3, + flex: 1, + fontSize: 14, + }, + navButton: { + width: 20, + padding: 3, + marginRight: 3, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: BGWASH, + borderColor: 'transparent', + borderRadius: 3, + }, + disabledButton: { + width: 20, + padding: 3, + marginRight: 3, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: DISABLED_WASH, + borderColor: 'transparent', + borderRadius: 3, + }, + goButton: { + height: 24, + padding: 3, + marginLeft: 8, + alignItems: 'center', + backgroundColor: BGWASH, + borderColor: 'transparent', + borderRadius: 3, + alignSelf: 'stretch', + }, + loadingView: { + backgroundColor: BGWASH, + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: BGWASH, + }, + errorTextTitle: { + fontSize: 15, + fontWeight: 'bold', + marginBottom: 10, + }, + errorText: { + fontSize: 14, + textAlign: 'center', + marginBottom: 2, + }, + statusBar: { + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 5, + height: 22, + }, + statusBarText: { + color: 'white', + fontSize: 13, + }, + spinner: { + width: 20, + marginRight: 6, + }, +}); + +exports.title = ''; +exports.description = 'Base component to display web content'; +exports.examples = [ + { + title: 'WebView', + render() { return ; } + } +]; diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js new file mode 100644 index 000000000..8190258ae --- /dev/null +++ b/Libraries/Components/WebView/WebView.android.js @@ -0,0 +1,169 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule WebView + */ +'use strict'; + +var EdgeInsetsPropType = require('EdgeInsetsPropType'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var keyMirror = require('keyMirror'); +var merge = require('merge'); + +var PropTypes = React.PropTypes; +var RKUIManager = require('NativeModules').RKUIManager; + +var RK_WEBVIEW_REF = 'webview'; + +var WebViewState = keyMirror({ + IDLE: null, + LOADING: null, + ERROR: null, +}); + +var WebView = React.createClass({ + + propTypes: { + renderErrorView: PropTypes.func.isRequired, // view to show if there's an error + renderLoadingView: PropTypes.func.isRequired, // loading indicator to show + url: PropTypes.string.isRequired, + automaticallyAdjustContentInsets: PropTypes.bool, + contentInset: EdgeInsetsPropType, + onNavigationStateChange: PropTypes.func, + startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load + style: View.propTypes.style, + /** + * Used to locate this view in end-to-end tests. + */ + testID: PropTypes.string, + }, + + getInitialState: function() { + return { + viewState: WebViewState.IDLE, + lastErrorEvent: null, + 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.renderLoadingView(); + } else if (this.state.viewState === WebViewState.ERROR) { + var errorEvent = this.state.lastErrorEvent; + otherView = this.props.renderErrorView( + 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, 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 webView = + ; + + return ( + + {webView} + {otherView} + + ); + }, + + goForward: function() { + RKUIManager.webViewGoForward(this.getWebWiewHandle()); + }, + + goBack: function() { + RKUIManager.webViewGoBack(this.getWebWiewHandle()); + }, + + reload: function() { + RKUIManager.webViewReload(this.getWebWiewHandle()); + }, + + /** + * We return an event with a bunch of fields including: + * url, title, loading, canGoBack, canGoForward + */ + updateNavigationState: function(event) { + if (this.props.onNavigationStateChange) { + this.props.onNavigationStateChange(event.nativeEvent); + } + }, + + getWebWiewHandle: function() { + return this.refs[RK_WEBVIEW_REF].getNodeHandle(); + }, + + onLoadingStart: function(event) { + this.updateNavigationState(event); + }, + + onLoadingError: function(event) { + event.persist(); // persist this event because we need to store it + console.error("encountered an error loading page", event.nativeEvent); + + this.setState({ + lastErrorEvent: event.nativeEvent, + viewState: WebViewState.ERROR + }); + }, + + onLoadingFinish: function(event) { + this.setState({ + viewState: WebViewState.IDLE, + }); + this.updateNavigationState(event); + }, +}); + +var RCTWebView = createReactIOSNativeComponentClass({ + validAttributes: merge(ReactIOSViewAttributes.UIView, { + url: true, + }), + uiViewClassName: 'RCTWebView', +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + hidden: { + height: 0, + flex: 0, // disable 'flex:1' when hiding a View + }, +}); + +module.exports = WebView; diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js new file mode 100644 index 000000000..ff916f8e7 --- /dev/null +++ b/Libraries/Components/WebView/WebView.ios.js @@ -0,0 +1,182 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule WebView + */ +'use strict'; + +var EdgeInsetsPropType = require('EdgeInsetsPropType'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var keyMirror = require('keyMirror'); +var insetsDiffer = require('insetsDiffer'); +var merge = require('merge'); + +var PropTypes = React.PropTypes; +var { RKWebViewManager } = require('NativeModules'); + +var RK_WEBVIEW_REF = 'webview'; + +var WebViewState = keyMirror({ + IDLE: null, + LOADING: null, + ERROR: null, +}); + +var NavigationType = { + click: RKWebViewManager.NavigationType.LinkClicked, + formsubmit: RKWebViewManager.NavigationType.FormSubmitted, + backforward: RKWebViewManager.NavigationType.BackForward, + reload: RKWebViewManager.NavigationType.Reload, + formresubmit: RKWebViewManager.NavigationType.FormResubmitted, + other: RKWebViewManager.NavigationType.Other, +}; + +var WebView = React.createClass({ + statics: { + NavigationType: NavigationType, + }, + + propTypes: { + renderErrorView: PropTypes.func.isRequired, // view to show if there's an error + renderLoadingView: PropTypes.func.isRequired, // loading indicator to show + url: PropTypes.string.isRequired, + automaticallyAdjustContentInsets: PropTypes.bool, + shouldInjectAJAXHandler: PropTypes.bool, + contentInset: EdgeInsetsPropType, + onNavigationStateChange: PropTypes.func, + startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load + style: View.propTypes.style, + }, + + getInitialState: function() { + return { + viewState: WebViewState.IDLE, + lastErrorEvent: null, + 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.renderLoadingView(); + } else if (this.state.viewState === WebViewState.ERROR) { + var errorEvent = this.state.lastErrorEvent; + otherView = this.props.renderErrorView( + errorEvent.domain, + errorEvent.code, + errorEvent.description); + } else if (this.state.viewState !== WebViewState.IDLE) { + console.error("RKWebView invalid state encountered: " + this.state.loading); + } + + var webViewStyles = [styles.container, 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 webView = + ; + + return ( + + {webView} + {otherView} + + ); + }, + + goForward: function() { + RKWebViewManager.goForward(this.getWebWiewHandle()); + }, + + goBack: function() { + RKWebViewManager.goBack(this.getWebWiewHandle()); + }, + + reload: function() { + RKWebViewManager.reload(this.getWebWiewHandle()); + }, + + /** + * We return an event with a bunch of fields including: + * url, title, loading, canGoBack, canGoForward + */ + updateNavigationState: function(event) { + if (this.props.onNavigationStateChange) { + this.props.onNavigationStateChange(event.nativeEvent); + } + }, + + getWebWiewHandle: function() { + return this.refs[RK_WEBVIEW_REF].getNodeHandle(); + }, + + onLoadingStart: function(event) { + this.updateNavigationState(event); + }, + + onLoadingError: function(event) { + event.persist(); // persist this event because we need to store it + console.error("encountered an error loading page", event.nativeEvent); + + this.setState({ + lastErrorEvent: event.nativeEvent, + viewState: WebViewState.ERROR + }); + }, + + onLoadingFinish: function(event) { + this.setState({ + viewState: WebViewState.IDLE, + }); + this.updateNavigationState(event); + }, +}); + +var RCTWebView = createReactIOSNativeComponentClass({ + validAttributes: merge(ReactIOSViewAttributes.UIView, { + url: true, + contentInset: {diff: insetsDiffer}, + automaticallyAdjustContentInsets: true, + shouldInjectAJAXHandler: true + }), + uiViewClassName: 'RCTWebView', +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + hidden: { + height: 0, + flex: 0, // disable 'flex:1' when hiding a View + }, +}); + +module.exports = WebView; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 5f4de8c1f..6f0b22df5 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -37,6 +37,7 @@ var ReactNative = { TouchableOpacity: require('TouchableOpacity'), TouchableWithoutFeedback: require('TouchableWithoutFeedback'), View: require('View'), + WebView: require('WebView'), invariant: require('invariant'), ix: require('ix'), }; diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj index eed3a13e7..d00c6b2c8 100644 --- a/ReactKit/ReactKit.xcodeproj/project.pbxproj +++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */; }; 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */; }; 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; }; + 13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; }; + 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156041AB1A2840079392D /* RCTWebViewManager.m */; }; 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067491A70F434002CDEE1 /* RCTUIManager.m */; }; 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */; }; 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; @@ -124,6 +126,10 @@ 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIActivityIndicatorViewManager.m; sourceTree = ""; }; 13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = ""; }; 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = ""; }; + 13C156011AB1A2840079392D /* RCTWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebView.h; sourceTree = ""; }; + 13C156021AB1A2840079392D /* RCTWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebView.m; sourceTree = ""; }; + 13C156031AB1A2840079392D /* RCTWebViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebViewManager.h; sourceTree = ""; }; + 13C156041AB1A2840079392D /* RCTWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebViewManager.m; sourceTree = ""; }; 13C325261AA63B6A0048765F /* RCTAutoInsetsProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAutoInsetsProtocol.h; sourceTree = ""; }; 13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTScrollableProtocol.h; sourceTree = ""; }; 13C325281AA63B6A0048765F /* RCTViewNodeProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewNodeProtocol.h; sourceTree = ""; }; @@ -299,6 +305,10 @@ 13E0674D1A70F44B002CDEE1 /* RCTViewManager.h */, 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */, 13C325281AA63B6A0048765F /* RCTViewNodeProtocol.h */, + 13C156011AB1A2840079392D /* RCTWebView.h */, + 13C156021AB1A2840079392D /* RCTWebView.m */, + 13C156031AB1A2840079392D /* RCTWebViewManager.h */, + 13C156041AB1A2840079392D /* RCTWebViewManager.m */, 13B080231A694A8400A75B9A /* RCTWrapperViewController.h */, 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */, 13E067531A70F44B002CDEE1 /* UIView+ReactKit.h */, @@ -480,7 +490,9 @@ 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */, 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */, 83C911101AAE6521001323A3 /* RCTAnimationManager.m in Sources */, + 13C156051AB1A2840079392D /* RCTWebView.m in Sources */, 83CBBA601A601EAA00E9B192 /* RCTBridge.m in Sources */, + 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */, 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */, 137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */, 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */, diff --git a/ReactKit/Views/RCTWebView.h b/ReactKit/Views/RCTWebView.h new file mode 100644 index 000000000..ec3c9d6eb --- /dev/null +++ b/ReactKit/Views/RCTWebView.h @@ -0,0 +1,20 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTView.h" + +@class RCTEventDispatcher; + +@interface RCTWebView : RCTView + +@property (nonatomic, strong) NSURL *URL; +@property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, assign) BOOL shouldInjectAJAXHandler; +@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +- (void)goForward; +- (void)goBack; +- (void)reload; + +@end diff --git a/ReactKit/Views/RCTWebView.m b/ReactKit/Views/RCTWebView.m new file mode 100644 index 000000000..cc5a07578 --- /dev/null +++ b/ReactKit/Views/RCTWebView.m @@ -0,0 +1,180 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTWebView.h" + +#import + +#import "RCTAutoInsetsProtocol.h" +#import "RCTEventDispatcher.h" +#import "RCTLog.h" +#import "RCTUtils.h" +#import "RCTView.h" +#import "UIView+ReactKit.h" + +@interface RCTWebView () + +@end + +@implementation RCTWebView +{ + RCTEventDispatcher *_eventDispatcher; + UIWebView *_webView; +} + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { + _automaticallyAdjustContentInsets = YES; + _contentInset = UIEdgeInsetsZero; + _eventDispatcher = eventDispatcher; + _webView = [[UIWebView alloc] initWithFrame:self.bounds]; + _webView.delegate = self; + [self addSubview:_webView]; + } + return self; +} + +- (void)goForward +{ + [_webView goForward]; +} + +- (void)goBack +{ + [_webView goBack]; +} + +- (void)reload +{ + [_webView reload]; +} + +- (void)setURL:(NSURL *)URL +{ + // Because of the way React works, as pages redirect, we actually end up + // passing the redirect urls back here, so we ignore them if trying to load + // the same url. We'll expose a call to 'reload' to allow a user to load + // the existing page. + if ([URL isEqual:_webView.request.URL]) { + return; + } + if (!URL) { + // Clear the webview + [_webView loadHTMLString:nil baseURL:nil]; + return; + } + [_webView loadRequest:[NSURLRequest requestWithURL:URL]]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + _webView.frame = self.bounds; + [RCTView autoAdjustInsetsForView:self + withScrollView:_webView.scrollView + updateOffset:YES]; +} + +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + _contentInset = contentInset; + [RCTView autoAdjustInsetsForView:self + withScrollView:_webView.scrollView + updateOffset:NO]; +} + +- (NSMutableDictionary *)baseEvent +{ + NSURL *url = _webView.request.URL; + NSString *title = [_webView stringByEvaluatingJavaScriptFromString:@"document.title"]; + NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary: @{ + @"target": self.reactTag, + @"url": url ? [url absoluteString] : @"", + @"loading" : @(_webView.loading), + @"title": title, + @"canGoBack": @([_webView canGoBack]), + @"canGoForward" : @([_webView canGoForward]), + }]; + + return event; +} + +#pragma mark - UIWebViewDelegate methods + +static NSString *const RCTJSAJAXScheme = @"react-ajax"; + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType +{ + // We have this check to filter out iframe requests and whatnot + BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; + if (isTopFrame) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"url": [request.URL absoluteString], + @"navigationType": @(navigationType) + }]; + [_eventDispatcher sendInputEventWithName:@"topLoadingStart" body:event]; + } + + // AJAX handler + return ![request.URL.scheme isEqualToString:RCTJSAJAXScheme]; +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error +{ + if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { + // NSURLErrorCancelled is reported when a page has a redirect OR if you load + // a new URL in the WebView before the previous one came back. We can just + // ignore these since they aren't real errors. + // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os + return; + } + + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"domain": error.domain, + @"code": @(error.code), + @"description": [error localizedDescription], + }]; + [_eventDispatcher sendInputEventWithName:@"topLoadingError" body:event]; +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView +{ + if (_shouldInjectAJAXHandler) { + + // From http://stackoverflow.com/questions/5353278/uiwebviewdelegate-not-monitoring-xmlhttprequest + + [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"\ + var s_ajaxListener = new Object(); \n\ + s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open; \n\ + s_ajaxListener.tempSend = XMLHttpRequest.prototype.send; \n\ + s_ajaxListener.callback = function() { \n\ + window.location.href = '%@://' + this.url; \n\ + } \n\ + XMLHttpRequest.prototype.open = function(a,b) { \n\ + s_ajaxListener.tempOpen.apply(this, arguments); \n\ + s_ajaxListener.method = a; \n\ + s_ajaxListener.url = b; \n\ + if (a.toLowerCase() === 'get') { \n\ + s_ajaxListener.data = (b.split('?'))[1]; \n\ + } \n\ + } \n\ + XMLHttpRequest.prototype.send = function(a,b) { \n\ + s_ajaxListener.tempSend.apply(this, arguments); \n\ + if (s_ajaxListener.method.toLowerCase() === 'post') { \n\ + s_ajaxListener.data = a; \n\ + } \n\ + s_ajaxListener.callback(); \n\ + } \n\ + ", RCTJSAJAXScheme]]; + } + + // we only need the final 'finishLoad' call so only fire the event when we're actually done loading. + if (!webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) { + [_eventDispatcher sendInputEventWithName:@"topLoadingFinish" body:[self baseEvent]]; + } +} + +@end diff --git a/ReactKit/Views/RCTWebViewManager.h b/ReactKit/Views/RCTWebViewManager.h new file mode 100644 index 000000000..d375cbdab --- /dev/null +++ b/ReactKit/Views/RCTWebViewManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTViewManager.h" + +@interface RCTWebViewManager : RCTViewManager + +@end diff --git a/ReactKit/Views/RCTWebViewManager.m b/ReactKit/Views/RCTWebViewManager.m new file mode 100644 index 000000000..8e68d9eb8 --- /dev/null +++ b/ReactKit/Views/RCTWebViewManager.m @@ -0,0 +1,76 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTWebViewManager.h" + +#import "RCTBridge.h" +#import "RCTSparseArray.h" +#import "RCTUIManager.h" +#import "RCTWebView.h" + +@implementation RCTWebViewManager + +- (UIView *)view +{ + return [[RCTWebView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_REMAP_VIEW_PROPERTY(url, URL); +RCT_EXPORT_VIEW_PROPERTY(contentInset); +RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets); +RCT_EXPORT_VIEW_PROPERTY(shouldInjectAJAXHandler); + +- (NSDictionary *)constantsToExport +{ + return @{ + @"NavigationType": @{ + @"LinkClicked": @(UIWebViewNavigationTypeLinkClicked), + @"FormSubmitted": @(UIWebViewNavigationTypeFormSubmitted), + @"BackForward": @(UIWebViewNavigationTypeBackForward), + @"Reload": @(UIWebViewNavigationTypeReload), + @"FormResubmitted": @(UIWebViewNavigationTypeFormResubmitted), + @"Other": @(UIWebViewNavigationTypeOther) + }, + }; +} + +- (void)goBack:(NSNumber *)reactTag +{ + RCT_EXPORT(); + + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RKWebView, got: %@", view); + } + [view goBack]; + }]; +} + +- (void)goForward:(NSNumber *)reactTag +{ + RCT_EXPORT(); + + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RKWebView, got: %@", view); + } + [view goForward]; + }]; +} + + +- (void)reload:(NSNumber *)reactTag +{ + RCT_EXPORT(); + + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWebView class]]) { + RCTLogMustFix(@"Invalid view returned from registry, expecting RKWebView, got: %@", view); + } + [view reload]; + }]; +} + +@end