From d3c0029cef88970a144ce35913b78ebeacfb964f Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 6 May 2015 15:51:59 -0700 Subject: [PATCH 01/19] [ReactNative] Do not throw when Image.source is null --- Libraries/Image/Image.ios.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index a41352f44..32965c213 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -123,8 +123,7 @@ var Image = React.createClass({ 'not be set directly on Image.'); } } - var source = resolveAssetSource(this.props.source); - invariant(source, 'source must be initialized'); + var source = resolveAssetSource(this.props.source) || {}; var {width, height} = source; var style = flattenStyle([{width, height}, styles.base, this.props.style]); From 8d83b7ff631cc5fc691fe8d21f819ad70329b73b Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Wed, 6 May 2015 16:14:10 -0700 Subject: [PATCH 02/19] [ReactNative] RCTWebViewExecutor - Fix string containing script tag support --- React/Executors/RCTWebViewExecutor.m | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/React/Executors/RCTWebViewExecutor.m b/React/Executors/RCTWebViewExecutor.m index a180bae2c..56323fb99 100644 --- a/React/Executors/RCTWebViewExecutor.m +++ b/React/Executors/RCTWebViewExecutor.m @@ -43,6 +43,7 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) UIWebView *_webView; NSMutableDictionary *_objectsToInject; NSRegularExpression *_commentsRegex; + NSRegularExpression *_scriptTagsRegex; } @synthesize valid = _valid; @@ -52,7 +53,8 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) if ((self = [super init])) { _objectsToInject = [[NSMutableDictionary alloc] init]; _webView = webView ?: [[UIWebView alloc] init]; - _commentsRegex = [NSRegularExpression regularExpressionWithPattern:@"(^ *?\\/\\/.*?$|\\/\\*\\*[\\s\\S]+?\\*\\/)" options:NSRegularExpressionAnchorsMatchLines error:NULL]; + _commentsRegex = [NSRegularExpression regularExpressionWithPattern:@"(^ *?\\/\\/.*?$|\\/\\*\\*[\\s\\S]*?\\*\\/)" options:NSRegularExpressionAnchorsMatchLines error:NULL], + _scriptTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\/?script[^>]*?)>" options:0 error:NULL], _webView.delegate = self; } return self; @@ -139,11 +141,6 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) onComplete(error); }; - script = [_commentsRegex stringByReplacingMatchesInString:script - options:0 - range:NSMakeRange(0, script.length) - withTemplate:@""]; - if (_objectsToInject.count > 0) { NSMutableString *scriptWithInjections = [[NSMutableString alloc] initWithString:@"/* BEGIN NATIVELY INJECTED OBJECTS */\n"]; [_objectsToInject enumerateKeysAndObjectsUsingBlock:^(NSString *objectName, NSString *blockScript, BOOL *stop) { @@ -158,6 +155,15 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) script = scriptWithInjections; } + script = [_commentsRegex stringByReplacingMatchesInString:script + options:0 + range:NSMakeRange(0, script.length) + withTemplate:@""]; + script = [_scriptTagsRegex stringByReplacingMatchesInString:script + options:0 + range:NSMakeRange(0, script.length) + withTemplate:@"\\\\<$1\\\\>"]; + NSString *runScript = [NSString stringWithFormat:@"", From 47164baaec01d4791d15db6ef5d4c5f94923e35e Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 6 May 2015 17:23:57 -0700 Subject: [PATCH 03/19] Fixed redbox error loop when camera access is disabled --- React/Base/RCTBridge.m | 16 ++++----- React/Modules/RCTExceptionsManager.m | 53 +++++++++++++--------------- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index a7c6a30a6..fd3ecaa24 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1495,17 +1495,13 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me return; } - if (!RCT_DEBUG) { + @try { [method invokeWithBridge:strongSelf module:module arguments:params context:context]; - } else { - @try { - [method invokeWithBridge:strongSelf module:module arguments:params context:context]; - } - @catch (NSException *exception) { - RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); - if ([exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { - @throw; - } + } + @catch (NSException *exception) { + RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); + if (!RCT_DEBUG && [exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { + @throw exception; } } diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index f391e6af0..ddea1275a 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -44,50 +44,45 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message return; } -#if RCT_DEBUG // Red box is only available in debug mode - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; -#else + if (!RCT_DEBUG) { - static NSUInteger reloadRetries = 0; - const NSUInteger maxMessageLength = 75; + static NSUInteger reloadRetries = 0; + const NSUInteger maxMessageLength = 75; - if (reloadRetries < _maxReloadAttempts) { + if (reloadRetries < _maxReloadAttempts) { - reloadRetries++; - [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification - object:nil]; + reloadRetries++; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification + object:nil]; - } else { + } else { - if (message.length > maxMessageLength) { - message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."]; + if (message.length > maxMessageLength) { + message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."]; + } + + NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"]; + for (NSDictionary *frame in stack) { + [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; + } + + NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:message]; + [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; } - - NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"]; - for (NSDictionary *frame in stack) { - [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; - } - - NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:message]; - [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; } - -#endif - } RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message stack:(NSArray *)stack) { + if (_delegate) { + [_delegate unhandledJSExceptionWithMessage:message stack:stack]; + return; + } -#if RCT_DEBUG // Red box is only available in debug mode - - [[RCTRedBox sharedInstance] updateErrorMessage:message withStack:stack]; - -#endif - + [[RCTRedBox sharedInstance] updateErrorMessage:message withStack:stack]; } @end From 0b844feedb5168d9025ecfdfd39ed720dfd5d1f5 Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Wed, 6 May 2015 17:02:03 -0700 Subject: [PATCH 04/19] [TouchableOpacity] Reset opacity to the inactiveValue rather than always 1.0 Summary: As per #941 - fixes bug with `TouchabeOpacity` always reseting child opacity to `1.0` after press. A note about the code: we could probably use a general `getNativeProp(propName, callback)` function rather than `getOpacity` but just used that as it was simpler for this specific PR, perhaps that refactor could be left to another - or maybe there is a way to do this already that I missed. Before: ![bug](https://cloud.githubusercontent.com/assets/90494/7287207/52d6a686-e907-11e4-8e16-04b2ddd0582c.gif) After: ![after](https://cloud.githubusercontent.com/assets/90494/7287689/5aca4776-e90c-11e4-8c40-aa6bd3e822d8.gif) Example code: ```javascript 'use strict'; var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, TouchableOpacity, } = React; var TestIt = React.createClass({ render() { return ( Closes https://github.com/facebook/react-native/pull/977 Github Author: Brent Vatne Test Plan: Imported from GitHub, without a `Test Plan:` line. --- Libraries/Components/Touchable/TouchableOpacity.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index e590999de..8e9a34146 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -20,6 +20,7 @@ var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); +var flattenStyle = require('flattenStyle'); var keyOf = require('keyOf'); var onlyChild = require('onlyChild'); @@ -105,12 +106,12 @@ var TouchableOpacity = React.createClass({ }, touchableHandleActivePressOut: function() { - this.setOpacityTo(1.0); + var childStyle = flattenStyle(this.refs[CHILD_REF].props.style) || {}; + this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity); this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { - this.setOpacityTo(1.0); this.props.onPress && this.props.onPress(); }, From f05494c96f7d4c43afd0023faad792aa1699bd04 Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Wed, 6 May 2015 21:18:03 -0700 Subject: [PATCH 05/19] Fix scrollview doc type - canimprove -> can improve Summary: Simple one character change - add a missing space Closes https://github.com/facebook/react-native/pull/1112 Github Author: Brent Vatne Test Plan: Imported from GitHub, without a `Test Plan:` line. --- Libraries/Components/ScrollView/ScrollView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index f8dc3bcba..3b5122b24 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -186,7 +186,7 @@ var ScrollView = React.createClass({ /** * Experimental: When true, offscreen child views (whose `overflow` value is * `hidden`) are removed from their native backing superview when offscreen. - * This canimprove scrolling performance on long lists. The default value is + * This can improve scrolling performance on long lists. The default value is * false. */ removeClippedSubviews: PropTypes.bool, From 205c22b9158df871cd3f7b216f014a6805f04677 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 7 May 2015 03:53:35 -0700 Subject: [PATCH 06/19] Fixed dev menu loop --- React/Base/RCTBridge.m | 41 +++++++++++++++++++++--------- React/Base/RCTDevMenu.m | 56 +++++++++++++++++++++++++++-------------- React/Base/RCTRedBox.m | 18 ++++++++++++- 3 files changed, 83 insertions(+), 32 deletions(-) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index fd3ecaa24..f442f1cf4 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1144,18 +1144,11 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me _latestJSExecutor = nil; } - /** - * Main Thread deallocations - */ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [_mainDisplayLink invalidate]; + void (^mainThreadInvalidate)(void) = ^{ - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - /** - * JS Thread deallocations - */ - [_javaScriptExecutor invalidate]; - [_jsDisplayLink invalidate]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_mainDisplayLink invalidate]; + _mainDisplayLink = nil; // Invalidate modules for (id target in _modulesByID.allObjects) { @@ -1165,11 +1158,35 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me } // Release modules (breaks retain cycle if module has strong bridge reference) - _javaScriptExecutor = nil; _frameUpdateObservers = nil; _modulesByID = nil; _queuesByID = nil; _modulesByName = nil; + }; + + if (!_javaScriptExecutor) { + + // No JS thread running + mainThreadInvalidate(); + return; + } + + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + + /** + * JS Thread deallocations + */ + [_javaScriptExecutor invalidate]; + _javaScriptExecutor = nil; + + [_jsDisplayLink invalidate]; + _jsDisplayLink = nil; + + /** + * Main Thread deallocations + */ + mainThreadInvalidate(); + }]; } diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 3d88caf55..f7e688df5 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -73,9 +73,6 @@ RCT_EXPORT_MODULE() { if ((self = [super init])) { - _defaults = [NSUserDefaults standardUserDefaults]; - [self updateSettings]; - NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self @@ -93,19 +90,27 @@ RCT_EXPORT_MODULE() name:RCTJavaScriptDidLoadNotification object:nil]; + _defaults = [NSUserDefaults standardUserDefaults]; + _settings = [[NSMutableDictionary alloc] init]; + + // Delay setup until after Bridge init + __weak RCTDevMenu *weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateSettings]; + }); + #if TARGET_IPHONE_SIMULATOR - __weak RCTDevMenu *weakSelf = self; RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; - // toggle debug menu + // Toggle debug menu [commands registerKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand action:^(UIKeyCommand *command) { [weakSelf toggle]; }]; - // reload in normal mode + // Reload in normal mode [commands registerKeyCommandWithInput:@"n" modifierFlags:UIKeyModifierCommand action:^(UIKeyCommand *command) { @@ -117,22 +122,23 @@ RCT_EXPORT_MODULE() return self; } +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + - (void)updateSettings { - __weak RCTDevMenu *weakSelf = self; - dispatch_async(dispatch_get_main_queue(), ^{ - RCTDevMenu *strongSelf = weakSelf; - if (!strongSelf) { - return; - } + NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey]; + if ([settings isEqualToDictionary:_settings]) { + return; + } - strongSelf->_settings = [NSMutableDictionary dictionaryWithDictionary:[strongSelf->_defaults objectForKey:RCTDevMenuSettingsKey]]; - - strongSelf.shakeToShow = [strongSelf->_settings[@"shakeToShow"] ?: @YES boolValue]; - strongSelf.profilingEnabled = [strongSelf->_settings[@"profilingEnabled"] ?: @NO boolValue]; - strongSelf.liveReloadEnabled = [strongSelf->_settings[@"liveReloadEnabled"] ?: @NO boolValue]; - strongSelf.executorClass = NSClassFromString(strongSelf->_settings[@"executorClass"]); - }); + [_settings setDictionary:settings]; + self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue]; + self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; + self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; + self.executorClass = NSClassFromString(_settings[@"executorClass"]); } - (void)jsLoaded @@ -161,6 +167,11 @@ RCT_EXPORT_MODULE() }); } +- (BOOL)isValid +{ + return NO; +} + - (void)dealloc { [_updateTask cancel]; @@ -170,6 +181,10 @@ RCT_EXPORT_MODULE() - (void)updateSetting:(NSString *)name value:(id)value { + id currentValue = _settings[name]; + if (currentValue == value || [currentValue isEqual:value]) { + return; + } if (value) { _settings[name] = value; } else { @@ -239,6 +254,9 @@ RCT_EXPORT_METHOD(reload) - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { _actionSheet = nil; + if (buttonIndex == actionSheet.cancelButtonIndex) { + return; + } switch (buttonIndex) { case 0: { diff --git a/React/Base/RCTRedBox.m b/React/Base/RCTRedBox.m index 0de61d172..9c5b6d3dd 100644 --- a/React/Base/RCTRedBox.m +++ b/React/Base/RCTRedBox.m @@ -76,10 +76,27 @@ reloadButton.frame = CGRectMake(buttonWidth, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); [_rootView addSubview:dismissButton]; [_rootView addSubview:reloadButton]; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter addObserver:self + selector:@selector(dismiss) + name:RCTReloadNotification + object:nil]; + + [notificationCenter addObserver:self + selector:@selector(dismiss) + name:RCTJavaScriptDidLoadNotification + object:nil]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (void)openStackFrameInEditor:(NSDictionary *)stackFrame { NSData *stackFrameJSON = [RCTJSONStringify(stackFrame, nil) dataUsingEncoding:NSUTF8StringEncoding]; @@ -125,7 +142,6 @@ - (void)reload { [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil userInfo:nil]; - [self dismiss]; } #pragma mark - TableView From b8bf4d0957a51964773dc3a06b8c0a68b71d26b8 Mon Sep 17 00:00:00 2001 From: James Ide Date: Thu, 7 May 2015 04:29:31 -0700 Subject: [PATCH 07/19] [Loader] Post a notification when the JS fails to load Summary: This provides a way to get notified when a bridge fails to load JS, allowing apps to handle the error. Closes https://github.com/facebook/react-native/pull/1085 Github Author: James Ide Test Plan: run the UIExplorer app with the packager server not running, and verify that the notification is posted. --- React/Base/RCTBridge.h | 5 +++++ React/Base/RCTBridge.m | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 77cbb215e..7c8af00cc 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -28,6 +28,11 @@ extern NSString *const RCTReloadNotification; */ extern NSString *const RCTJavaScriptDidLoadNotification; +/** + * This notification fires when the bridge failed to load. + */ +extern NSString *const RCTJavaScriptDidFailToLoadNotification; + /** * This block can be used to instantiate modules that require additional * init parameters, or additional configuration prior to being used. diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index f442f1cf4..28a218b07 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -31,6 +31,7 @@ NSString *const RCTReloadNotification = @"RCTReloadNotification"; NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; +NSString *const RCTJavaScriptDidFailToLoadNotification = @"RCTJavaScriptDidFailToLoadNotification"; dispatch_queue_t const RCTJSThread = nil; @@ -1082,16 +1083,18 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; [loader loadBundleAtURL:bundleURL onComplete:^(NSError *error, NSString *script) { + _loading = NO; if (!self.isValid) { return; } + RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; sourceCodeModule.scriptURL = bundleURL; sourceCodeModule.scriptText = script; - if (error != nil) { + if (error) { - NSArray *stack = [[error userInfo] objectForKey:@"stack"]; + NSArray *stack = [error userInfo][@"stack"]; if (stack) { [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] withStack:stack]; @@ -1100,10 +1103,17 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me withDetails:[error localizedFailureReason]]; } + NSDictionary *userInfo = @{@"error": error}; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification + object:self + userInfo:userInfo]; + } else { + [self enqueueApplicationScript:script url:bundleURL onComplete:^(NSError *loadError) { if (!loadError) { + /** * Register the display link to start sending js calls after everything * is setup From b97ce93ceae0dc4adc16706fbf054f14dcf208d0 Mon Sep 17 00:00:00 2001 From: James Ide Date: Thu, 7 May 2015 07:16:59 -0700 Subject: [PATCH 08/19] [Nav] Add support for bar button icons and left buttons Summary: NavigatorIOS supports four new properties: - **rightButtonImageSource:** The source of an image to display in the top right. This must be a static image since UINavigationController only supports UIImages. Adding support for UIImageViews (or arbitrary views) is more complicated because custom views do not fade on touch and do not have hit slop the same way that UIImage buttons do. Usage: `rightButtonImageSource: ix('ImageName')` - **backButtonImageSource:** Use a custom image for the back button. This does not replace the back caret (`<`) but instead replaces the text next to it. - **leftButtonTitle**: Text for the left nav button, which supersedes the previous nav item's back button when specified. The main use case for this is your initial screen/UIVC which has nothing to go back to (since it is the first VC on the stack) but need to display a left button. This does hide the back button if there would have been one otherwise. - **leftButtonImageSource:** Image source for the left button, super Closes https://github.com/facebook/react-native/pull/263 Github Author: James Ide Test Plan: Imported from GitHub, without a `Test Plan:` line. --- Examples/UIExplorer/ImageMocks.js | 5 + Examples/UIExplorer/NavigatorIOSExample.js | 25 +++++ .../NavBarButtonPlus.imageset/Contents.json | 21 ++++ .../NavBarButtonPlus@3x.png | Bin 0 -> 166 bytes .../Components/Navigation/NavigatorIOS.ios.js | 49 ++++++++- React/Modules/RCTUIManager.m | 6 ++ React/Views/RCTNavItem.h | 14 ++- React/Views/RCTNavItem.m | 101 +++++++++++++++++- React/Views/RCTNavItemManager.m | 20 ++-- React/Views/RCTWrapperViewController.m | 39 +++---- 10 files changed, 247 insertions(+), 33 deletions(-) create mode 100644 Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json create mode 100644 Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/NavBarButtonPlus@3x.png diff --git a/Examples/UIExplorer/ImageMocks.js b/Examples/UIExplorer/ImageMocks.js index b888acbf7..3f1883fa6 100644 --- a/Examples/UIExplorer/ImageMocks.js +++ b/Examples/UIExplorer/ImageMocks.js @@ -39,3 +39,8 @@ declare module 'image!uie_thumb_selected' { declare var uri: string; declare var isStatic: boolean; } + +declare module 'image!NavBarButtonPlus' { + declare var uri: string; + declare var isStatic: boolean; +} diff --git a/Examples/UIExplorer/NavigatorIOSExample.js b/Examples/UIExplorer/NavigatorIOSExample.js index eee731bd5..4a2011a65 100644 --- a/Examples/UIExplorer/NavigatorIOSExample.js +++ b/Examples/UIExplorer/NavigatorIOSExample.js @@ -19,6 +19,7 @@ var React = require('react-native'); var ViewExample = require('./ViewExample'); var createExamplePage = require('./createExamplePage'); var { + AlertIOS, PixelRatio, ScrollView, StyleSheet, @@ -92,6 +93,30 @@ var NavigatorIOSExample = React.createClass({ } }); })} + {this._renderRow('Custom Left & Right Icons', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + leftButtonTitle: 'Custom Left', + onLeftButtonPress: () => this.props.navigator.pop(), + rightButtonIcon: require('image!NavBarButtonPlus'), + onRightButtonPress: () => { + AlertIOS.alert( + 'Bar Button Action', + 'Recognized a tap on the bar button icon', + [ + { + text: 'OK', + onPress: () => console.log('Tapped OK'), + }, + ] + ); + }, + passProps: { + text: 'This page has an icon for the right button in the nav bar', + } + }); + })} {this._renderRow('Pop', () => { this.props.navigator.pop(); })} diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json new file mode 100644 index 000000000..13726b4e9 --- /dev/null +++ b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "NavBarButtonPlus@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/NavBarButtonPlus@3x.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/NavBarButtonPlus@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..706a9c9052240e9c88d4aef09f70c8ad77f1edf3 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^#vshW1|+Q(8fXD2!4lVqlHmNblJdl&REC1Q^yH$_ z;tYpU&(A=~08bakkc@k8uWaOGaNuBZ{GER*|A0#+bE25uG|!$Jy4`bm+0Z~*(T7(F k2??_#)=Q$vusw6;@BjN)n|rEo2+(W>Pgg&ebxsLQ0Dm|t*8l(j literal 0 HcmV?d00001 diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index 3babd1409..103e749f7 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -12,6 +12,7 @@ 'use strict'; var EventEmitter = require('EventEmitter'); +var Image = require('Image'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var RCTNavigatorManager = require('NativeModules').NavigatorManager; @@ -47,11 +48,16 @@ var RCTNavigatorItem = createReactIOSNativeComponentClass({ // NavigatorIOS does not use them all, because some are problematic title: true, barTintColor: true, + leftButtonIcon: true, + leftButtonTitle: true, + onNavLeftButtonTap: true, + rightButtonIcon: true, rightButtonTitle: true, onNavRightButtonTap: true, + backButtonIcon: true, + backButtonTitle: true, tintColor: true, navigationBarHidden: true, - backButtonTitle: true, titleTextColor: true, style: true, }, @@ -79,7 +85,12 @@ type Route = { title: string; passProps: Object; backButtonTitle: string; + backButtonIcon: Object; + leftButtonTitle: string; + leftButtonIcon: Object; + onLeftButtonPress: Function; rightButtonTitle: string; + rightButtonIcon: Object; onRightButtonPress: Function; wrapperStyle: any; }; @@ -212,6 +223,13 @@ var NavigatorIOS = React.createClass({ */ passProps: PropTypes.object, + /** + * If set, the left header button image will appear with this source. Note + * that this doesn't apply for the header of the current view, but the + * ones of the views that are pushed afterward. + */ + backButtonIcon: Image.propTypes.source, + /** * If set, the left header button will appear with this name. Note that * this doesn't apply for the header of the current view, but the ones @@ -219,6 +237,26 @@ var NavigatorIOS = React.createClass({ */ backButtonTitle: PropTypes.string, + /** + * If set, the left header button image will appear with this source + */ + leftButtonIcon: Image.propTypes.source, + + /** + * If set, the left header button will appear with this name + */ + leftButtonTitle: PropTypes.string, + + /** + * Called when the left header button is pressed + */ + onLeftButtonPress: PropTypes.func, + + /** + * If set, the right header button image will appear with this source + */ + rightButtonIcon: Image.propTypes.source, + /** * If set, the right header button will appear with this name */ @@ -560,7 +598,12 @@ var NavigatorIOS = React.createClass({ this.props.itemWrapperStyle, route.wrapperStyle ]} + backButtonIcon={this._imageNameFromSource(route.backButtonIcon)} backButtonTitle={route.backButtonTitle} + leftButtonIcon={this._imageNameFromSource(route.leftButtonIcon)} + leftButtonTitle={route.leftButtonTitle} + onNavLeftButtonTap={route.onLeftButtonPress} + rightButtonIcon={this._imageNameFromSource(route.rightButtonIcon)} rightButtonTitle={route.rightButtonTitle} onNavRightButtonTap={route.onRightButtonPress} navigationBarHidden={this.props.navigationBarHidden} @@ -577,6 +620,10 @@ var NavigatorIOS = React.createClass({ ); }, + _imageNameFromSource: function(source: ?Object) { + return source ? source.uri : undefined; + }, + renderNavigationStackItems: function() { var shouldRecurseToNavigator = this.state.makingNavigatorRequest || diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index d35cae03a..eac3a7735 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -1159,6 +1159,12 @@ RCT_EXPORT_METHOD(clearJSResponder) @"captured": @"onNavigationCompleteCapture" } }, + @"topNavLeftButtonTap": @{ + @"phasedRegistrationNames": @{ + @"bubbled": @"onNavLeftButtonTap", + @"captured": @"onNavLefttButtonTapCapture" + } + }, @"topNavRightButtonTap": @{ @"phasedRegistrationNames": @{ @"bubbled": @"onNavRightButtonTap", diff --git a/React/Views/RCTNavItem.h b/React/Views/RCTNavItem.h index 5ae874522..cd9833a44 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -12,11 +12,19 @@ @interface RCTNavItem : UIView @property (nonatomic, copy) NSString *title; +@property (nonatomic, strong) UIImage *leftButtonIcon; +@property (nonatomic, copy) NSString *leftButtonTitle; +@property (nonatomic, strong) UIImage *rightButtonIcon; @property (nonatomic, copy) NSString *rightButtonTitle; +@property (nonatomic, strong) UIImage *backButtonIcon; @property (nonatomic, copy) NSString *backButtonTitle; @property (nonatomic, assign) BOOL navigationBarHidden; -@property (nonatomic, copy) UIColor *tintColor; -@property (nonatomic, copy) UIColor *barTintColor; -@property (nonatomic, copy) UIColor *titleTextColor; +@property (nonatomic, strong) UIColor *tintColor; +@property (nonatomic, strong) UIColor *barTintColor; +@property (nonatomic, strong) UIColor *titleTextColor; + +@property (nonatomic, readonly) UIBarButtonItem *backButtonItem; +@property (nonatomic, readonly) UIBarButtonItem *leftButtonItem; +@property (nonatomic, readonly) UIBarButtonItem *rightButtonItem; @end diff --git a/React/Views/RCTNavItem.m b/React/Views/RCTNavItem.m index 6b1e92f44..56346a363 100644 --- a/React/Views/RCTNavItem.m +++ b/React/Views/RCTNavItem.m @@ -11,5 +11,104 @@ @implementation RCTNavItem -@end +@synthesize backButtonItem = _backButtonItem; +@synthesize leftButtonItem = _leftButtonItem; +@synthesize rightButtonItem = _rightButtonItem; +- (void)setBackButtonTitle:(NSString *)backButtonTitle +{ + _backButtonTitle = backButtonTitle; + _backButtonItem = nil; +} + +- (void)setBackButtonIcon:(UIImage *)backButtonIcon +{ + _backButtonIcon = backButtonIcon; + _backButtonItem = nil; +} + +- (UIBarButtonItem *)backButtonItem +{ + if (!_backButtonItem) { + if (_backButtonIcon) { + _backButtonItem = [[UIBarButtonItem alloc] initWithImage:_backButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_backButtonTitle.length) { + _backButtonItem = [[UIBarButtonItem alloc] initWithTitle:_backButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _backButtonItem = nil; + } + } + return _backButtonItem; +} + +- (void)setLeftButtonTitle:(NSString *)leftButtonTitle +{ + _leftButtonTitle = leftButtonTitle; + _leftButtonItem = nil; +} + +- (void)setLeftButtonIcon:(UIImage *)leftButtonIcon +{ + _leftButtonIcon = leftButtonIcon; + _leftButtonIcon = nil; +} + +- (UIBarButtonItem *)leftButtonItem +{ + if (!_leftButtonItem) { + if (_leftButtonIcon) { + _leftButtonItem = [[UIBarButtonItem alloc] initWithImage:_leftButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_leftButtonTitle.length) { + _leftButtonItem = [[UIBarButtonItem alloc] initWithTitle:_leftButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _leftButtonItem = nil; + } + } + return _leftButtonItem; +} + +- (void)setRightButtonTitle:(NSString *)rightButtonTitle +{ + _rightButtonTitle = rightButtonTitle; + _rightButtonItem = nil; +} + +- (void)setRightButtonIcon:(UIImage *)rightButtonIcon +{ + _rightButtonIcon = rightButtonIcon; + _rightButtonItem = nil; +} + +- (UIBarButtonItem *)rightButtonItem +{ + if (!_rightButtonItem) { + if (_rightButtonIcon) { + _rightButtonItem = [[UIBarButtonItem alloc] initWithImage:_rightButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_rightButtonTitle.length) { + _rightButtonItem = [[UIBarButtonItem alloc] initWithTitle:_rightButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _rightButtonItem = nil; + } + } + return _rightButtonItem; +} + +@end diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index fc601632f..33588c938 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -21,12 +21,20 @@ RCT_EXPORT_MODULE() return [[RCTNavItem alloc] init]; } +RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL) +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor) + RCT_EXPORT_VIEW_PROPERTY(title, NSString) -RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString); -RCT_EXPORT_VIEW_PROPERTY(backButtonTitle, NSString); -RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL); -RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor); -RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor); -RCT_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor); +RCT_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor) + +RCT_EXPORT_VIEW_PROPERTY(backButtonIcon, UIImage) +RCT_EXPORT_VIEW_PROPERTY(backButtonTitle, NSString) + +RCT_EXPORT_VIEW_PROPERTY(leftButtonTitle, NSString) +RCT_EXPORT_VIEW_PROPERTY(leftButtonIcon, UIImage) + +RCT_EXPORT_VIEW_PROPERTY(rightButtonIcon, UIImage) +RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString) @end diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index 53c2f16a7..400ce5fab 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -64,7 +64,6 @@ // TODO: find a way to make this less-tightly coupled to navigation controller if ([self.parentViewController isKindOfClass:[UINavigationController class]]) { - [self.navigationController setNavigationBarHidden:_navItem.navigationBarHidden animated:animated]; @@ -73,33 +72,23 @@ return; } - self.navigationItem.title = _navItem.title; - UINavigationBar *bar = self.navigationController.navigationBar; - if (_navItem.barTintColor) { - bar.barTintColor = _navItem.barTintColor; - } - if (_navItem.tintColor) { - bar.tintColor = _navItem.tintColor; - } + bar.barTintColor = _navItem.barTintColor; + bar.tintColor = _navItem.tintColor; if (_navItem.titleTextColor) { [bar setTitleTextAttributes:@{NSForegroundColorAttributeName : _navItem.titleTextColor}]; } - if (_navItem.rightButtonTitle.length > 0) { - self.navigationItem.rightBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.rightButtonTitle - style:UIBarButtonItemStyleDone - target:self - action:@selector(handleNavRightButtonTapped)]; + UINavigationItem *item = self.navigationItem; + item.title = _navItem.title; + item.backBarButtonItem = _navItem.backButtonItem; + if ((item.leftBarButtonItem = _navItem.leftButtonItem)) { + item.leftBarButtonItem.target = self; + item.leftBarButtonItem.action = @selector(handleNavLeftButtonTapped); } - - if (_navItem.backButtonTitle.length > 0) { - self.navigationItem.backBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.backButtonTitle - style:UIBarButtonItemStylePlain - target:nil - action:nil]; + if ((item.rightBarButtonItem = _navItem.rightButtonItem)) { + item.rightBarButtonItem.target = self; + item.rightBarButtonItem.action = @selector(handleNavRightButtonTapped); } } } @@ -114,6 +103,12 @@ self.view = _wrapperView; } +- (void)handleNavLeftButtonTapped +{ + [_eventDispatcher sendInputEventWithName:@"topNavLeftButtonTap" + body:@{@"target":_navItem.reactTag}]; +} + - (void)handleNavRightButtonTapped { [_eventDispatcher sendInputEventWithName:@"topNavRightButtonTap" From 6b38fad219588b2a2d12bacbead41bc418ab3289 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Thu, 7 May 2015 09:10:47 -0700 Subject: [PATCH 09/19] [ReactNative] Fix ref to eventDispatcher on RCTBridge --- React/Base/RCTBridge.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 28a218b07..6e839ed05 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -868,6 +868,11 @@ static id _latestJSExecutor; return _batchedBridge.modules; } +- (RCTEventDispatcher *)eventDispatcher +{ + return _eventDispatcher ?: _batchedBridge.eventDispatcher; +} + #define RCT_BRIDGE_WARN(...) \ - (void)__VA_ARGS__ \ { \ From 1ef4e00fba948c9684ef208b278df11dd85620de Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Thu, 7 May 2015 12:11:02 -0700 Subject: [PATCH 11/19] [ReactNative] Introduce onLayout events Summary: Simply add an `onLayout` callback to a native view component, and the callback will be invoked with the current layout information when the view is mounted and whenever the layout changes. The only limitation is that scroll position and other stuff the layout system isn't aware of is not taken into account. This is because onLayout events wouldn't be triggered for these changes and if they are desired they should be tracked separately (e.g. with `onScroll`) and combined. Also fixes some bugs with LayoutAnimation callbacks. @public Test Plan: - Run new LayoutEventsExample in UIExplorer and see it work correctly. - New integration test passes internally (IntegrationTest project seems busted). - New jest test case passes. {F22318433} ``` 2015-05-06 15:45:05.848 [info][tid:com.facebook.React.JavaScript] "Running application "UIExplorerApp" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF" 2015-05-06 15:45:05.881 [info][tid:com.facebook.React.JavaScript] "received text layout event ", {"target":27,"layout":{"y":123,"x":12.5,"width":140.5,"height":18}} 2015-05-06 15:45:05.882 [info][tid:com.facebook.React.JavaScript] "received image layout event ", {"target":23,"layout":{"y":12.5,"x":122,"width":50,"height":50}} 2015-05-06 15:45:05.883 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":70.5,"x":20,"width":294,"height":204}} 2015-05-06 15:45:05.897 [info][tid:com.facebook.React.JavaScript] "received text layout event ", {"target":27,"layout":{"y":206.5,"x":12.5,"width":140.5,"height":18}} 2015-05-06 15:45:05.897 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":70.5,"x":20,"width":294,"height":287.5}} 2015-05-06 15:45:09.847 [info][tid:com.facebook.React.JavaScript] "layout animation done." 2015-05-06 15:45:09.847 [info][tid:com.facebook.React.JavaScript] "received image layout event ", {"target":23,"layout":{"y":12.5,"x":82,"width":50,"height":50}} 2015-05-06 15:45:09.848 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":110.5,"x":60,"width":214,"height":287.5}} 2015-05-06 15:45:09.862 [info][tid:com.facebook.React.JavaScript] "received text layout event ", {"target":27,"layout":{"y":206.5,"x":12.5,"width":120,"height":68}} 2015-05-06 15:45:09.863 [info][tid:com.facebook.React.JavaScript] "received image layout event ", {"target":23,"layout":{"y":12.5,"x":55,"width":50,"height":50}} 2015-05-06 15:45:09.863 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":128,"x":60,"width":160,"height":337.5}} ``` --- Examples/UIExplorer/LayoutEventsExample.js | 150 ++++++++++++++++ Examples/UIExplorer/UIExplorerList.js | 1 + IntegrationTests/IntegrationTestsApp.js | 1 + .../IntegrationTestsTests.m | 5 + IntegrationTests/LayoutEventsTest.js | 167 ++++++++++++++++++ Libraries/Components/View/View.js | 5 + Libraries/ReactIOS/ReactIOSViewAttributes.js | 6 +- Libraries/ReactIOS/diffRawProperties.js | 20 +++ React/Modules/RCTUIManager.m | 42 ++++- React/Views/RCTShadowView.h | 1 + React/Views/RCTViewManager.m | 2 + 11 files changed, 389 insertions(+), 11 deletions(-) create mode 100644 Examples/UIExplorer/LayoutEventsExample.js create mode 100644 IntegrationTests/LayoutEventsTest.js diff --git a/Examples/UIExplorer/LayoutEventsExample.js b/Examples/UIExplorer/LayoutEventsExample.js new file mode 100644 index 000000000..6aec6257e --- /dev/null +++ b/Examples/UIExplorer/LayoutEventsExample.js @@ -0,0 +1,150 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Image, + LayoutAnimation, + StyleSheet, + Text, + View, +} = React; + +type LayoutEvent = { + nativeEvent: { + layout: { + x: number; + y: number; + width: number; + height: number; + }; + }; +}; + +var LayoutEventExample = React.createClass({ + getInitialState: function() { + return { + viewStyle: { + margin: 20, + }, + }; + }, + animateViewLayout: function() { + LayoutAnimation.configureNext( + LayoutAnimation.Presets.spring, + () => { + console.log('layout animation done.'); + this.addWrapText(); + }, + (error) => { throw new Error(JSON.stringify(error)); } + ); + this.setState({ + viewStyle: { + margin: this.state.viewStyle.margin > 20 ? 20 : 60, + } + }); + }, + addWrapText: function() { + this.setState( + {extraText: ' And a bunch more text to wrap around a few lines.'}, + this.changeContainer + ); + }, + changeContainer: function() { + this.setState({containerStyle: {width: 280}}); + }, + onViewLayout: function(e: LayoutEvent) { + console.log('received view layout event\n', e.nativeEvent); + this.setState({viewLayout: e.nativeEvent.layout}); + }, + onTextLayout: function(e: LayoutEvent) { + console.log('received text layout event\n', e.nativeEvent); + this.setState({textLayout: e.nativeEvent.layout}); + }, + onImageLayout: function(e: LayoutEvent) { + console.log('received image layout event\n', e.nativeEvent); + this.setState({imageLayout: e.nativeEvent.layout}); + }, + render: function() { + var viewStyle = [styles.view, this.state.viewStyle]; + var textLayout = this.state.textLayout || {width: '?', height: '?'}; + var imageLayout = this.state.imageLayout || {x: '?', y: '?'}; + return ( + + + onLayout events are called on mount and whenever layout is updated, + including after layout animations complete.{' '} + + Press here to change layout. + + + + + + ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'} + + + A simple piece of text.{this.state.extraText} + + + {'\n'} + Text w/h: {textLayout.width}/{textLayout.height + '\n'} + Image x/y: {imageLayout.x}/{imageLayout.y} + + + + ); + } +}); + +var styles = StyleSheet.create({ + view: { + padding: 12, + borderColor: 'black', + borderWidth: 0.5, + backgroundColor: 'transparent', + }, + text: { + alignSelf: 'flex-start', + borderColor: 'rgba(0, 0, 255, 0.2)', + borderWidth: 0.5, + }, + image: { + width: 50, + height: 50, + marginBottom: 10, + alignSelf: 'center', + }, + pressText: { + fontWeight: 'bold', + }, +}); + +exports.title = 'onLayout'; +exports.description = 'Layout events can be used to measure view size and position.'; +exports.examples = [ +{ + title: 'onLayout', + render: function(): ReactElement { + return ; + }, +}]; diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index a24ec1a54..dd2336df5 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -64,6 +64,7 @@ var APIS = [ require('./BorderExample'), require('./CameraRollExample.ios'), require('./GeolocationExample'), + require('./LayoutEventsExample'), require('./LayoutExample'), require('./NetInfoExample'), require('./PanResponderExample'), diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js index dbb5dde83..1e61a0dbc 100644 --- a/IntegrationTests/IntegrationTestsApp.js +++ b/IntegrationTests/IntegrationTestsApp.js @@ -25,6 +25,7 @@ var TESTS = [ require('./IntegrationTestHarnessTest'), require('./TimersTest'), require('./AsyncStorageTest'), + require('./LayoutEventsTest'), require('./SimpleSnapshotTest'), ]; diff --git a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m index e0a43e793..9bf1a4fc1 100644 --- a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m +++ b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m @@ -71,6 +71,11 @@ [_runner runTest:_cmd module:@"AsyncStorageTest"]; } +- (void)testLayoutEvents +{ + [_runner runTest:_cmd module:@"LayoutEventsTest"]; +} + #pragma mark Snapshot Tests - (void)testSimpleSnapshot diff --git a/IntegrationTests/LayoutEventsTest.js b/IntegrationTests/LayoutEventsTest.js new file mode 100644 index 000000000..7e8cd3a0d --- /dev/null +++ b/IntegrationTests/LayoutEventsTest.js @@ -0,0 +1,167 @@ +/** + * 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 LayoutEventsTest + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Image, + LayoutAnimation, + NativeModules, + StyleSheet, + Text, + View, +} = React; +var TestModule = NativeModules.TestModule || NativeModules.SnapshotTestManager; + +var deepDiffer = require('deepDiffer'); + +function debug() { + //console.log.apply(null, arguments); +} + +type LayoutEvent = { + nativeEvent: { + layout: { + x: number; + y: number; + width: number; + height: number; + }; + }; +}; + +var LayoutEventsTest = React.createClass({ + getInitialState: function() { + return { + didAnimation: false, + }; + }, + animateViewLayout: function() { + LayoutAnimation.configureNext( + LayoutAnimation.Presets.spring, + () => { + debug('layout animation done.'); + this.checkLayout(this.addWrapText); + }, + (error) => { throw new Error(JSON.stringify(error)); } + ); + this.setState({viewStyle: {margin: 60}}); + }, + addWrapText: function() { + this.setState( + {extraText: ' And a bunch more text to wrap around a few lines.'}, + () => this.checkLayout(this.changeContainer) + ); + }, + changeContainer: function() { + this.setState( + {containerStyle: {width: 280}}, + () => this.checkLayout(TestModule.markTestCompleted) + ); + }, + checkLayout: function(next?: ?Function) { + if (!this.isMounted()) { + return; + } + this.refs.view.measure((x, y, width, height) => { + this.compare('view', {x, y, width, height}, this.state.viewLayout); + if (typeof next === 'function') { + next(); + } else if (!this.state.didAnimation) { + // Trigger first state change after onLayout fires + this.animateViewLayout(); + this.state.didAnimation = true; + } + }); + this.refs.txt.measure((x, y, width, height) => { + this.compare('txt', {x, y, width, height}, this.state.textLayout); + }); + this.refs.img.measure((x, y, width, height) => { + this.compare('img', {x, y, width, height}, this.state.imageLayout); + }); + }, + compare: function(node: string, measured: any, onLayout: any): void { + if (deepDiffer(measured, onLayout)) { + var data = {measured, onLayout}; + throw new Error( + node + ' onLayout mismatch with measure ' + + JSON.stringify(data, null, ' ') + ); + } + }, + onViewLayout: function(e: LayoutEvent) { + debug('received view layout event\n', e.nativeEvent); + this.setState({viewLayout: e.nativeEvent.layout}, this.checkLayout); + }, + onTextLayout: function(e: LayoutEvent) { + debug('received text layout event\n', e.nativeEvent); + this.setState({textLayout: e.nativeEvent.layout}, this.checkLayout); + }, + onImageLayout: function(e: LayoutEvent) { + debug('received image layout event\n', e.nativeEvent); + this.setState({imageLayout: e.nativeEvent.layout}, this.checkLayout); + }, + render: function() { + var viewStyle = [styles.view, this.state.viewStyle]; + var textLayout = this.state.textLayout || {width: '?', height: '?'}; + var imageLayout = this.state.imageLayout || {x: '?', y: '?'}; + return ( + + + + + ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'} + + + A simple piece of text.{this.state.extraText} + + + {'\n'} + Text w/h: {textLayout.width}/{textLayout.height + '\n'} + Image x/y: {imageLayout.x}/{imageLayout.y} + + + + ); + } +}); + +var styles = StyleSheet.create({ + container: { + margin: 40, + }, + view: { + margin: 20, + padding: 12, + borderColor: 'black', + borderWidth: 0.5, + backgroundColor: 'transparent', + }, + text: { + alignSelf: 'flex-start', + borderColor: 'rgba(0, 0, 255, 0.2)', + borderWidth: 0.5, + }, + image: { + width: 50, + height: 50, + marginBottom: 10, + alignSelf: 'center', + }, +}); + +module.exports = LayoutEventsTest; diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index c7ca2ee26..aa69ab0eb 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -90,6 +90,11 @@ var View = React.createClass({ onStartShouldSetResponder: PropTypes.func, onStartShouldSetResponderCapture: PropTypes.func, + /** + * Invoked on mount and layout changes with {x, y, width, height}. + */ + onLayout: PropTypes.func, + /** * In the absence of `auto` property, `none` is much like `CSS`'s `none` * value. `box-none` is as if you had applied the `CSS` class: diff --git a/Libraries/ReactIOS/ReactIOSViewAttributes.js b/Libraries/ReactIOS/ReactIOSViewAttributes.js index 069f00b24..489741b05 100644 --- a/Libraries/ReactIOS/ReactIOSViewAttributes.js +++ b/Libraries/ReactIOS/ReactIOSViewAttributes.js @@ -9,8 +9,7 @@ * @providesModule ReactIOSViewAttributes * @flow */ - -"use strict"; +'use strict'; var merge = require('merge'); @@ -21,6 +20,7 @@ ReactIOSViewAttributes.UIView = { accessible: true, accessibilityLabel: true, testID: true, + onLayout: true, }; ReactIOSViewAttributes.RCTView = merge( @@ -31,7 +31,7 @@ ReactIOSViewAttributes.RCTView = merge( // For this property to be effective, it must be applied to a view that contains // many subviews that extend outside its bound. The subviews must also have // overflow: hidden, as should the containing view (or one of its superviews). - removeClippedSubviews: true + removeClippedSubviews: true, }); module.exports = ReactIOSViewAttributes; diff --git a/Libraries/ReactIOS/diffRawProperties.js b/Libraries/ReactIOS/diffRawProperties.js index 3a5de284f..ddd6edbea 100644 --- a/Libraries/ReactIOS/diffRawProperties.js +++ b/Libraries/ReactIOS/diffRawProperties.js @@ -42,6 +42,16 @@ function diffRawProperties( } prevProp = prevProps && prevProps[propKey]; nextProp = nextProps[propKey]; + + // functions are converted to booleans as markers that the associated + // events should be sent from native. + if (typeof prevProp === 'function') { + prevProp = true; + } + if (typeof nextProp === 'function') { + nextProp = true; + } + if (prevProp !== nextProp) { // If you want a property's diff to be detected, you must configure it // to be so - *or* it must be a scalar property. For now, we'll allow @@ -75,6 +85,16 @@ function diffRawProperties( } prevProp = prevProps[propKey]; nextProp = nextProps && nextProps[propKey]; + + // functions are converted to booleans as markers that the associated + // events should be sent from native. + if (typeof prevProp === 'function') { + prevProp = true; + } + if (typeof nextProp === 'function') { + nextProp = true; + } + if (prevProp !== nextProp) { if (nextProp === undefined) { nextProp = null; // null is a sentinel we explicitly send to native diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index eac3a7735..df90ff150 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -19,6 +19,7 @@ #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTDefines.h" +#import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTProfile.h" #import "RCTRootView.h" @@ -420,17 +421,31 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}]; - // Parallel arrays + // Parallel arrays are built and then handed off to main thread NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *areNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *parentsAreNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *onLayoutEvents = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; for (RCTShadowView *shadowView in viewsWithNewFrames) { [frameReactTags addObject:shadowView.reactTag]; [frames addObject:[NSValue valueWithCGRect:shadowView.frame]]; [areNew addObject:@(shadowView.isNewView)]; [parentsAreNew addObject:@(shadowView.superview.isNewView)]; + id event = [NSNull null]; + if (shadowView.hasOnLayout) { + event = @{ + @"target": shadowView.reactTag, + @"layout": @{ + @"x": @(shadowView.frame.origin.x), + @"y": @(shadowView.frame.origin.y), + @"width": @(shadowView.frame.size.width), + @"height": @(shadowView.frame.size.height), + }, + }; + } + [onLayoutEvents addObject:event]; } for (RCTShadowView *shadowView in viewsWithNewFrames) { @@ -448,20 +463,30 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa // Perform layout (possibly animated) NSNumber *rootViewTag = rootShadowView.reactTag; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTResponseSenderBlock callback = self->_layoutAnimation.callback; + __block NSInteger completionsCalled = 0; for (NSUInteger ii = 0; ii < frames.count; ii++) { NSNumber *reactTag = frameReactTags[ii]; UIView *view = viewRegistry[reactTag]; CGRect frame = [frames[ii] CGRectValue]; + id event = onLayoutEvents[ii]; + + BOOL isNew = [areNew[ii] boolValue]; + RCTAnimation *updateAnimation = isNew ? nil : _layoutAnimation.updateAnimation; + BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; + RCTAnimation *createAnimation = shouldAnimateCreation ? _layoutAnimation.createAnimation : nil; void (^completion)(BOOL finished) = ^(BOOL finished) { - if (self->_layoutAnimation.callback) { - self->_layoutAnimation.callback(@[@(finished)]); + completionsCalled++; + if (event != [NSNull null]) { + [self.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event]; + } + if (callback && completionsCalled == frames.count - 1) { + callback(@[@(finished)]); } }; // Animate view update - BOOL isNew = [areNew[ii] boolValue]; - RCTAnimation *updateAnimation = isNew ? nil: _layoutAnimation.updateAnimation; if (updateAnimation) { [updateAnimation performAnimations:^{ [view reactSetFrame:frame]; @@ -478,9 +503,7 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa } // Animate view creation - BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; - RCTAnimation *createAnimation = _layoutAnimation.createAnimation; - if (shouldAnimateCreation && createAnimation) { + if (createAnimation) { if ([createAnimation.property isEqualToString:@"scaleXY"]) { view.layer.transform = CATransform3DMakeScale(0, 0, 0); } else if ([createAnimation.property isEqualToString:@"opacity"]) { @@ -1262,6 +1285,9 @@ RCT_EXPORT_METHOD(clearJSResponder) @"topScrollAnimationEnd": @{ @"registrationName": @"onScrollAnimationEnd" }, + @"topLayout": @{ + @"registrationName": @"onLayout" + }, @"topSelectionChange": @{ @"registrationName": @"onSelectionChange" }, diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 8d68855f7..83350ac46 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -41,6 +41,7 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); @property (nonatomic, assign) BOOL isBGColorExplicitlySet; // Used to propagate to children @property (nonatomic, strong) UIColor *backgroundColor; // Used to propagate to children @property (nonatomic, assign) RCTUpdateLifecycle layoutLifecycle; +@property (nonatomic, assign) BOOL hasOnLayout; /** * isNewView - Used to track the first time the view is introduced into the hierarchy. It is initialized YES, then is diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 62fb29116..4dfb296fd 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -198,4 +198,6 @@ RCT_CUSTOM_SHADOW_PROPERTY(backgroundColor, UIColor, RCTShadowView) view.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; } +RCT_REMAP_SHADOW_PROPERTY(onLayout, hasOnLayout, BOOL) + @end From 7141948a057e9cc1c18a9eab30d7daa2d60bfa5b Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Thu, 7 May 2015 12:29:36 -0700 Subject: [PATCH 12/19] Bump fetch and add exports/globals for Headers, Request and Response Summary: Now on fetch 0.8.1, the latest tagged release. Previous version used was 0.7.0. See #1162 cc @vjeux @jtremback Closes https://github.com/facebook/react-native/pull/1192 Github Author: Brent Vatne Test Plan: I arc patched and ran movies demo and storyline, they work fine --- Examples/Movies/SearchScreen.js | 2 - Libraries/Fetch/fetch.js | 182 ++++++++++++------ .../InitializeJavaScriptAppEngine.js | 7 +- .../Initialization/loadSourceMap.js | 2 - .../react-native/react-native-interface.js | 5 + 5 files changed, 133 insertions(+), 65 deletions(-) diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index fc31a90dc..e484d0f65 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -29,8 +29,6 @@ var TimerMixin = require('react-timer-mixin'); var MovieCell = require('./MovieCell'); var MovieScreen = require('./MovieScreen'); -var fetch = require('fetch'); - /** * This is for demo purposes only, and rate limited. * In case you want to use the Rotten Tomatoes' API on a real app you should diff --git a/Libraries/Fetch/fetch.js b/Libraries/Fetch/fetch.js index 241172669..829f7c425 100644 --- a/Libraries/Fetch/fetch.js +++ b/Libraries/Fetch/fetch.js @@ -47,6 +47,23 @@ var self = {}; return } + function normalizeName(name) { + if (typeof name !== 'string') { + name = name.toString(); + } + if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { + throw new TypeError('Invalid character in header field name') + } + return name.toLowerCase() + } + + function normalizeValue(value) { + if (typeof value !== 'string') { + value = value.toString(); + } + return value + } + function Headers(headers) { this.map = {} @@ -66,7 +83,8 @@ var self = {}; } Headers.prototype.append = function(name, value) { - name = name.toLowerCase() + name = normalizeName(name) + value = normalizeValue(value) var list = this.map[name] if (!list) { list = [] @@ -76,24 +94,24 @@ var self = {}; } Headers.prototype['delete'] = function(name) { - delete this.map[name.toLowerCase()] + delete this.map[normalizeName(name)] } Headers.prototype.get = function(name) { - var values = this.map[name.toLowerCase()] + var values = this.map[normalizeName(name)] return values ? values[0] : null } Headers.prototype.getAll = function(name) { - return this.map[name.toLowerCase()] || [] + return this.map[normalizeName(name)] || [] } Headers.prototype.has = function(name) { - return this.map.hasOwnProperty(name.toLowerCase()) + return this.map.hasOwnProperty(normalizeName(name)) } Headers.prototype.set = function(name, value) { - this.map[name.toLowerCase()] = [value] + this.map[normalizeName(name)] = [normalizeValue(value)] } // Instead of iterable for now. @@ -134,22 +152,51 @@ var self = {}; return fileReaderReady(reader) } - var blobSupport = 'FileReader' in self && 'Blob' in self && (function() { - try { - new Blob(); - return true - } catch(e) { - return false - } - })(); + var support = { + blob: 'FileReader' in self && 'Blob' in self && (function() { + try { + new Blob(); + return true + } catch(e) { + return false + } + })(), + formData: 'FormData' in self + } function Body() { this.bodyUsed = false - if (blobSupport) { + + this._initBody = function(body) { + this._bodyInit = body + if (typeof body === 'string') { + this._bodyText = body + } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body + } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body + } else if (!body) { + this._bodyText = '' + } else { + throw new Error('unsupported BodyInit type') + } + } + + if (support.blob) { this.blob = function() { var rejected = consumed(this) - return rejected ? rejected : Promise.resolve(this._bodyBlob) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob') + } else { + return Promise.resolve(new Blob([this._bodyText])) + } } this.arrayBuffer = function() { @@ -157,7 +204,18 @@ var self = {}; } this.text = function() { - return this.blob().then(readBlobAsText) + var rejected = consumed(this) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return readBlobAsText(this._bodyBlob) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as text') + } else { + return Promise.resolve(this._bodyText) + } } } else { this.text = function() { @@ -166,7 +224,7 @@ var self = {}; } } - if ('FormData' in self) { + if (support.formData) { this.formData = function() { return this.text().then(decode) } @@ -190,12 +248,17 @@ var self = {}; function Request(url, options) { options = options || {} this.url = url - this._body = options.body + this.credentials = options.credentials || 'omit' this.headers = new Headers(options.headers) this.method = normalizeMethod(options.method || 'GET') this.mode = options.mode || null this.referrer = null + + if ((this.method === 'GET' || this.method === 'HEAD') && options.body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(options.body) } function decode(body) { @@ -223,11 +286,43 @@ var self = {}; return head } - Request.prototype.fetch = function() { - var self = this + Body.call(Request.prototype) + + function Response(bodyInit, options) { + if (!options) { + options = {} + } + + this._initBody(bodyInit) + this.type = 'default' + this.url = null + this.status = options.status + this.ok = this.status >= 200 && this.status < 300 + this.statusText = options.statusText + this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) + this.url = options.url || '' + } + + Body.call(Response.prototype) + + self.Headers = Headers; + self.Request = Request; + self.Response = Response; + + self.fetch = function(input, init) { + // TODO: Request constructor should accept input, init + var request + if (Request.prototype.isPrototypeOf(input) && !init) { + request = input + } else { + request = new Request(input, init) + } return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest() + if (request.credentials === 'cors') { + xhr.withCredentials = true; + } function responseURL() { if ('responseURL' in xhr) { @@ -262,57 +357,24 @@ var self = {}; reject(new TypeError('Network request failed')) } - xhr.open(self.method, self.url) - if ('responseType' in xhr && blobSupport) { + xhr.open(request.method, request.url, true) + + if ('responseType' in xhr && support.blob) { xhr.responseType = 'blob' } - self.headers.forEach(function(name, values) { + request.headers.forEach(function(name, values) { values.forEach(function(value) { xhr.setRequestHeader(name, value) }) }) - xhr.send((self._body === undefined) ? null : self._body) + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) }) } - - Body.call(Request.prototype) - - function Response(bodyInit, options) { - if (!options) { - options = {} - } - - if (blobSupport) { - if (typeof bodyInit === 'string') { - this._bodyBlob = new Blob([bodyInit]) - } else { - this._bodyBlob = bodyInit - } - } else { - this._bodyText = bodyInit - } - this.type = 'default' - this.url = null - this.status = options.status - this.statusText = options.statusText - this.headers = options.headers - this.url = options.url || '' - } - - Body.call(Response.prototype) - - self.Headers = Headers; - self.Request = Request; - self.Response = Response; - - self.fetch = function (url, options) { - return new Request(url, options).fetch() - } self.fetch.polyfill = true })(); /** End of the third-party code */ -module.exports = self.fetch; +module.exports = self; diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index bb0aa263c..7bddf87b6 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -135,7 +135,12 @@ function setupXHR() { // The native XMLHttpRequest in Chrome dev tools is CORS aware and won't // let you fetch anything from the internet GLOBAL.XMLHttpRequest = require('XMLHttpRequest'); - GLOBAL.fetch = require('fetch'); + + var fetchPolyfill = require('fetch'); + GLOBAL.fetch = fetchPolyfill.fetch; + GLOBAL.Headers = fetchPolyfill.Headers; + GLOBAL.Request = fetchPolyfill.Request; + GLOBAL.Response = fetchPolyfill.Response; } function setupGeolocation() { diff --git a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js index 4d4d21e25..a826db7bf 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js +++ b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js @@ -17,8 +17,6 @@ var RCTSourceCode = require('NativeModules').SourceCode; var SourceMapConsumer = require('SourceMap').SourceMapConsumer; var SourceMapURL = require('./source-map-url'); -var fetch = require('fetch'); - function loadSourceMap(): Promise { return fetchSourceMap() .then(map => new SourceMapConsumer(map)); diff --git a/Libraries/react-native/react-native-interface.js b/Libraries/react-native/react-native-interface.js index 6408526b8..b76518387 100644 --- a/Libraries/react-native/react-native-interface.js +++ b/Libraries/react-native/react-native-interface.js @@ -16,3 +16,8 @@ declare var __DEV__: boolean; declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{ inject: ?((stuff: Object) => void) };*/ + +declare var fetch: any; +declare var Headers: any; +declare var Request: any; +declare var Response: any; From 7b4ea51bf03bc76f8ff7b876744e87b28b788d1f Mon Sep 17 00:00:00 2001 From: Jiajie Zhu Date: Thu, 7 May 2015 16:20:08 -0700 Subject: [PATCH 13/19] [madman] map - fix bug that onRegionChangeComplete stopped emitting --- React/Views/RCTMap.h | 2 +- React/Views/RCTMap.m | 2 +- React/Views/RCTMapManager.m | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/React/Views/RCTMap.h b/React/Views/RCTMap.h index 89e4c0a80..d372db56e 100644 --- a/React/Views/RCTMap.h +++ b/React/Views/RCTMap.h @@ -21,7 +21,7 @@ extern const CGFloat RCTMapZoomBoundBuffer; @interface RCTMap: MKMapView @property (nonatomic, assign) BOOL followUserLocation; -@property (nonatomic, assign) BOOL hasStartedLoading; +@property (nonatomic, assign) BOOL hasStartedRendering; @property (nonatomic, assign) CGFloat minDelta; @property (nonatomic, assign) CGFloat maxDelta; @property (nonatomic, assign) UIEdgeInsets legalLabelInsets; diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 5037d3390..40b60508e 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -27,7 +27,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; { if ((self = [super init])) { - _hasStartedLoading = NO; + _hasStartedRendering = NO; // Find Apple link label for (UIView *subview in self.subviews) { diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m index f9a6b9175..8d334f60b 100644 --- a/React/Views/RCTMapManager.m +++ b/React/Views/RCTMapManager.m @@ -84,15 +84,15 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) [self _regionChanged:mapView]; // Don't send region did change events until map has - // started loading, as these won't represent the final location - if (mapView.hasStartedLoading) { + // started rendering, as these won't represent the final location + if (mapView.hasStartedRendering) { [self _emitRegionChangeEvent:mapView continuous:NO]; }; } -- (void)mapViewWillStartLoadingMap:(RCTMap *)mapView +- (void)mapViewWillStartRenderingMap:(RCTMap *)mapView { - mapView.hasStartedLoading = YES; + mapView.hasStartedRendering = YES; [self _emitRegionChangeEvent:mapView continuous:NO]; } From 736d86057106376267a15c3edc85fdefda0a00fe Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Thu, 7 May 2015 16:38:29 -0700 Subject: [PATCH 14/19] [ReactNative] Fix RCTScrollView setContentInset --- React/Views/RCTScrollView.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index ca8f94242..1373d8bd1 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -315,8 +315,14 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; - (void)setContentInset:(UIEdgeInsets)contentInset { + CGPoint contentOffset = _scrollView.contentOffset; + _contentInset = contentInset; - [self setNeedsLayout]; + [RCTView autoAdjustInsetsForView:self + withScrollView:_scrollView + updateOffset:NO]; + + _scrollView.contentOffset = contentOffset; } - (void)scrollToOffset:(CGPoint)offset From c76fb40ec4ad90f210425d33eb4981490dd1fd15 Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Thu, 7 May 2015 17:30:41 -0700 Subject: [PATCH 15/19] [ReactNative] Register assets with AssetRegistry --- Libraries/Image/AssetRegistry.js | 20 +++++++++ .../__tests__/resolveAssetSource-test.js | 44 ++++++++++--------- Libraries/Image/resolveAssetSource.js | 44 +++++++------------ .../src/Packager/__tests__/Packager-test.js | 8 ++-- packager/react-packager/src/Packager/index.js | 3 +- 5 files changed, 66 insertions(+), 53 deletions(-) create mode 100644 Libraries/Image/AssetRegistry.js diff --git a/Libraries/Image/AssetRegistry.js b/Libraries/Image/AssetRegistry.js new file mode 100644 index 000000000..df4173e78 --- /dev/null +++ b/Libraries/Image/AssetRegistry.js @@ -0,0 +1,20 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AssetRegistry + */ +'use strict'; + +var assets = []; + +function registerAsset(asset) { + // `push` returns new array length, so the first asset will + // get id 1 (not 0) to make the value truthy + return assets.push(asset); +} + +function getAssetByID(assetId) { + return assets[assetId - 1]; +} + +module.exports = { registerAsset, getAssetByID }; diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js index 632ff9656..c5fc3bbe1 100644 --- a/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -8,19 +8,24 @@ */ 'use strict'; -jest.dontMock('../resolveAssetSource'); +jest + .dontMock('AssetRegistry') + .dontMock('../resolveAssetSource'); var resolveAssetSource; var SourceCode; +var AssetRegistry; function expectResolvesAsset(input, expectedSource) { - expect(resolveAssetSource(input)).toEqual(expectedSource); + var assetId = AssetRegistry.registerAsset(input); + expect(resolveAssetSource(assetId)).toEqual(expectedSource); } describe('resolveAssetSource', () => { beforeEach(() => { jest.resetModuleRegistry(); SourceCode = require('NativeModules').SourceCode; + AssetRegistry = require('AssetRegistry'); resolveAssetSource = require('../resolveAssetSource'); }); @@ -32,6 +37,22 @@ describe('resolveAssetSource', () => { expect(resolveAssetSource(source2)).toBe(source2); }); + it('does not change deprecated assets', () => { + expect(resolveAssetSource({ + isStatic: true, + deprecated: true, + width: 100, + height: 200, + uri: 'logo', + })).toEqual({ + isStatic: true, + deprecated: true, + width: 100, + height: 200, + uri: 'logo', + }); + }); + it('ignores any weird data', () => { expect(resolveAssetSource(null)).toBe(null); expect(resolveAssetSource(42)).toBe(null); @@ -81,25 +102,6 @@ describe('resolveAssetSource', () => { }); }); - it('does not change deprecated assets', () => { - expectResolvesAsset({ - __packager_asset: true, - deprecated: true, - fileSystemLocation: '/root/app/module/a', - httpServerLocation: '/assets/module/a', - width: 100, - height: 200, - scales: [1], - hash: '5b6f00f', - name: 'logo', - type: 'png', - }, { - isStatic: true, - width: 100, - height: 200, - uri: 'logo', - }); - }); }); describe('bundle was loaded from file', () => { diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js index 021891558..29d59a9a5 100644 --- a/Libraries/Image/resolveAssetSource.js +++ b/Libraries/Image/resolveAssetSource.js @@ -10,6 +10,7 @@ */ 'use strict'; +var AssetRegistry = require('AssetRegistry'); var PixelRatio = require('PixelRatio'); var SourceCode = require('NativeModules').SourceCode; @@ -44,58 +45,47 @@ function pickScale(scales, deviceScale) { } function resolveAssetSource(source) { - if (!source || typeof source !== 'object') { - return null; - } - - if (!source.__packager_asset) { + if (typeof source === 'object') { return source; } - // Deprecated assets are managed by Xcode for now, - // just returning image name as `uri` - // Examples: - // require('image!deprecatd_logo_example') - // require('./new-hotness-logo-example.png') - if (source.deprecated) { - return { - width: source.width, - height: source.height, - isStatic: true, - uri: source.name || source.uri, // TODO(frantic): remove uri - }; + var asset = AssetRegistry.getAssetByID(source); + if (asset) { + return assetToImageSource(asset); } + return null; +} + +function assetToImageSource(asset) { // TODO(frantic): currently httpServerLocation is used both as // path in http URL and path within IPA. Should we have zipArchiveLocation? - var path = source.httpServerLocation; + var path = asset.httpServerLocation; if (path[0] === '/') { path = path.substr(1); } - var scale = pickScale(source.scales, PixelRatio.get()); + var scale = pickScale(asset.scales, PixelRatio.get()); var scaleSuffix = scale === 1 ? '' : '@' + scale + 'x'; - var fileName = source.name + scaleSuffix + '.' + source.type; + var fileName = asset.name + scaleSuffix + '.' + asset.type; var serverURL = getServerURL(); if (serverURL) { return { - width: source.width, - height: source.height, + width: asset.width, + height: asset.height, uri: serverURL + path + '/' + fileName + - '?hash=' + source.hash, + '?hash=' + asset.hash, isStatic: false, }; } else { return { - width: source.width, - height: source.height, + width: asset.width, + height: asset.height, uri: path + '/' + fileName, isStatic: true, }; } - - return source; } module.exports = resolveAssetSource; diff --git a/packager/react-packager/src/Packager/__tests__/Packager-test.js b/packager/react-packager/src/Packager/__tests__/Packager-test.js index 3f0934462..d4a1c0f36 100644 --- a/packager/react-packager/src/Packager/__tests__/Packager-test.js +++ b/packager/react-packager/src/Packager/__tests__/Packager-test.js @@ -166,12 +166,12 @@ describe('Packager', function() { }; expect(p.addModule.mock.calls[3][0]).toEqual({ - code: 'lol module.exports = ' + + code: 'lol module.exports = require("AssetRegistry").registerAsset(' + JSON.stringify(imgModule) + - '; lol', - sourceCode: 'module.exports = ' + + '); lol', + sourceCode: 'module.exports = require("AssetRegistry").registerAsset(' + JSON.stringify(imgModule) + - ';', + ');', sourcePath: '/root/img/new_image.png' }); diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js index c03a0432c..e647f7642 100644 --- a/packager/react-packager/src/Packager/index.js +++ b/packager/react-packager/src/Packager/index.js @@ -219,7 +219,8 @@ Packager.prototype.generateAssetModule = function(ppackage, module) { ppackage.addAsset(img); - var code = 'module.exports = ' + JSON.stringify(img) + ';'; + var ASSET_TEMPLATE = 'module.exports = require("AssetRegistry").registerAsset(%json);'; + var code = ASSET_TEMPLATE.replace('%json', JSON.stringify(img)); return new ModuleTransport({ code: code, From 61bd008ea03b33b1c2ee8b68dc5d47c6fbabda19 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Thu, 7 May 2015 18:53:41 -0700 Subject: [PATCH 16/19] [ReactNative] Fix TouchableOpacity crash when child props are missing --- Libraries/Components/Touchable/TouchableOpacity.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 8e9a34146..5c673947f 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -106,7 +106,10 @@ var TouchableOpacity = React.createClass({ }, touchableHandleActivePressOut: function() { - var childStyle = flattenStyle(this.refs[CHILD_REF].props.style) || {}; + var childStyle = ( + this.refs[CHILD_REF].props && + flattenStyle(this.refs[CHILD_REF].props.style) + ) || {}; this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity); this.props.onPressOut && this.props.onPressOut(); }, From e433b6a57ea85980165deaa928d96b82b452dae1 Mon Sep 17 00:00:00 2001 From: Gabe Levi Date: Thu, 7 May 2015 18:41:02 -0700 Subject: [PATCH 17/19] Bump jstransform version for react-native --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e059c3fa..61735be64 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "graceful-fs": "^3.0.6", "image-size": "0.3.5", "joi": "~5.1.0", - "jstransform": "10.1.0", + "jstransform": "11.0.1", "module-deps": "3.5.6", "optimist": "0.6.1", "promise": "^7.0.0", From ec9015d0050a4b694b8164397e0670a6a008aedf Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Thu, 7 May 2015 20:13:34 -0700 Subject: [PATCH 18/19] [ReactNative] Disable event deduping --- React/Base/RCTBridge.m | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 6e839ed05..c98e3648b 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -1332,48 +1332,6 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me return; } - /** - * Event deduping - * - * Right now we make a lot of assumptions about the arguments structure - * so just iterate if it's a `callFunctionReturnFlushedQueue()` - */ - if ([method isEqualToString:@"callFunctionReturnFlushedQueue"]) { - NSString *moduleName = RCTLocalModuleNames[[args[0] integerValue]]; - /** - * Keep going if it any event emmiter, e.g. RCT(Device|NativeApp)?EventEmitter - */ - if ([moduleName hasSuffix:@"EventEmitter"]) { - for (NSDictionary *call in [strongSelf->_scheduledCalls copy]) { - NSArray *callArgs = call[@"args"]; - /** - * If it's the same module && method call on the bridge && - * the same EventEmitter module && method - */ - if ( - [call[@"module"] isEqualToString:module] && - [call[@"method"] isEqualToString:method] && - [callArgs[0] isEqual:args[0]] && - [callArgs[1] isEqual:args[1]] - ) { - /** - * args[2] contains the actual arguments for the event call, where - * args[2][0] is the target for RCTEventEmitter or the eventName - * for the other EventEmitters - * if RCTEventEmitter we need to compare args[2][1] that will be - * the eventName - */ - if ( - [args[2][0] isEqual:callArgs[2][0]] && - (![moduleName isEqualToString:@"RCTEventEmitter"] || [args[2][1] isEqual:callArgs[2][1]]) - ) { - [strongSelf->_scheduledCalls removeObject:call]; - } - } - } - } - } - id call = @{ @"module": module, @"method": method, From 5e51fac8d58021b5969f5a12b274c3bc79277752 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Fri, 8 May 2015 08:26:29 -0700 Subject: [PATCH 19/19] [react-native] In TouchableOpacity, access .props on element, not component --- Libraries/Components/Touchable/TouchableOpacity.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 5c673947f..8d93cd841 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -106,10 +106,8 @@ var TouchableOpacity = React.createClass({ }, touchableHandleActivePressOut: function() { - var childStyle = ( - this.refs[CHILD_REF].props && - flattenStyle(this.refs[CHILD_REF].props.style) - ) || {}; + var child = onlyChild(this.props.children); + var childStyle = flattenStyle(child.props.style) || {}; this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity); this.props.onPressOut && this.props.onPressOut(); },