diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index 41f6b4d1a..f8183b12b 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -96,6 +96,7 @@ var WebViewExample = React.createClass({ url={this.state.url} javaScriptEnabledAndroid={true} onNavigationStateChange={this.onNavigationStateChange} + onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest} startInLoadingState={true} scalesPageToFit={this.state.scalesPageToFit} /> @@ -118,6 +119,11 @@ var WebViewExample = React.createClass({ this.refs[WEBVIEW_REF].reload(); }, + onShouldStartLoadWithRequest: function(event) { + // Implement any custom loading logic here, don't forget to return! + return true; + }, + onNavigationStateChange: function(navState) { this.setState({ backButtonEnabled: navState.canGoBack, diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 6c7f5484d..9256c3c86 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -113,6 +113,12 @@ var WebView = React.createClass({ * user can change the scale */ 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. + */ + onShouldStartLoadWithRequest: PropTypes.func, }, getInitialState: function() { @@ -158,6 +164,12 @@ var WebView = React.createClass({ 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 webView = ; diff --git a/React/Views/RCTWebView.h b/React/Views/RCTWebView.h index fdb192a39..3d514dd47 100644 --- a/React/Views/RCTWebView.h +++ b/React/Views/RCTWebView.h @@ -9,6 +9,8 @@ #import "RCTView.h" +@class RCTWebView; + /** * Special scheme used to pass messages to the injectedJavaScript * code without triggering a page load. Usage: @@ -17,8 +19,18 @@ */ extern NSString *const RCTJSNavigationScheme; +@protocol RCTWebViewDelegate + +- (BOOL)webView:(RCTWebView *)webView +shouldStartLoadForRequest:(NSMutableDictionary *)request + withCallback:(RCTDirectEventBlock)callback; + +@end + @interface RCTWebView : RCTView +@property (nonatomic, weak) id delegate; + @property (nonatomic, strong) NSURL *URL; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index 8b78f1bcf..2c35bc453 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -25,6 +25,7 @@ NSString *const RCTJSNavigationScheme = @"react-js-navigation"; @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; @property (nonatomic, copy) RCTDirectEventBlock onLoadingError; +@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; @end @@ -119,7 +120,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (NSMutableDictionary *)baseEvent { - NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary: @{ + NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:@{ @"url": _webView.request.URL.absoluteString ?: @"", @"loading" : @(_webView.loading), @"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"], @@ -142,6 +143,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { + BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme]; + + // skip this for the JS Navigation handler + if (!isJSNavigation && _onShouldStartLoadWithRequest) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"url": (request.URL).absoluteString, + @"navigationType": @(navigationType) + }]; + if (![self.delegate webView:self + shouldStartLoadForRequest:event + withCallback:_onShouldStartLoadWithRequest]) { + return NO; + } + } + if (_onLoadingStart) { // We have this check to filter out iframe requests and whatnot BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; @@ -156,13 +173,12 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) } // JS Navigation handler - return ![request.URL.scheme isEqualToString:RCTJSNavigationScheme]; + return !isJSNavigation; } - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error { if (_onLoadingError) { - 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 @@ -172,7 +188,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) } NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{ + [event addEntriesFromDictionary:@{ @"domain": error.domain, @"code": @(error.code), @"description": error.localizedDescription, @@ -185,8 +201,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) { if (_injectedJavaScript != nil) { NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript]; + NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{@"jsEvaluationValue":jsEvaluationValue}]; + event[@"jsEvaluationValue"] = jsEvaluationValue; + _onLoadingFinish(event); } // we only need the final 'finishLoad' call so only fire the event when we're actually done loading. diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m index 8779a970b..7512000b0 100644 --- a/React/Views/RCTWebViewManager.m +++ b/React/Views/RCTWebViewManager.m @@ -14,13 +14,22 @@ #import "RCTUIManager.h" #import "RCTWebView.h" -@implementation RCTWebViewManager +@interface RCTWebViewManager () + +@end + +@implementation RCTWebViewManager { + NSConditionLock *_shouldStartLoadLock; + BOOL _shouldStartLoad; +} RCT_EXPORT_MODULE() - (UIView *)view { - return [RCTWebView new]; + RCTWebView *webView = [RCTWebView new]; + webView.delegate = self; + return webView; } RCT_REMAP_VIEW_PROPERTY(url, URL, NSURL); @@ -34,6 +43,7 @@ RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL); RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock); - (NSDictionary *)constantsToExport { @@ -86,4 +96,38 @@ RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag) }]; } +#pragma mark - Exported synchronous methods + +- (BOOL)webView:(__unused RCTWebView *)webView +shouldStartLoadForRequest:(NSMutableDictionary *)request + withCallback:(RCTDirectEventBlock)callback +{ + _shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()]; + _shouldStartLoad = YES; + request[@"lockIdentifier"] = @(_shouldStartLoadLock.condition); + callback(request); + + // Block the main thread for a maximum of 250ms until the JS thread returns + if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.25]]) { + BOOL returnValue = _shouldStartLoad; + [_shouldStartLoadLock unlock]; + _shouldStartLoadLock = nil; + return returnValue; + } else { + RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES"); + return YES; + } +} + +RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(NSInteger)lockIdentifier) +{ + if ([_shouldStartLoadLock tryLockWhenCondition:lockIdentifier]) { + _shouldStartLoad = result; + [_shouldStartLoadLock unlockWithCondition:0]; + } else { + RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: " + "got %zd, expected %zd", lockIdentifier, _shouldStartLoadLock.condition); + } +} + @end