diff --git a/.flowconfig b/.flowconfig index bf06f66d3f8545..56d38f30838ddc 100644 --- a/.flowconfig +++ b/.flowconfig @@ -40,8 +40,9 @@ suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-2]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-2]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] 0.12.0 diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index e7eb7f2b4a299d..21819df4665f97 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -77,12 +77,14 @@ var SearchScreen = React.createClass({ var apiKey = API_KEYS[this.state.queryNumber % API_KEYS.length]; if (query) { return ( + // $FlowFixMe(>=0.13.0) - pageNumber may be null or undefined API_URL + 'movies.json?apikey=' + apiKey + '&q=' + encodeURIComponent(query) + '&page_limit=20&page=' + pageNumber ); } else { // With no query, load latest movies return ( + // $FlowFixMe(>=0.13.0) - pageNumber may be null or undefined API_URL + 'lists/movies/in_theaters.json?apikey=' + apiKey + '&page_limit=20&page=' + pageNumber ); diff --git a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj index 05480738f35f33..e714ca03812afc 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -600,7 +600,7 @@ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", ); - INFOPLIST_FILE = "$(SRCROOT)/iOS/Info.plist"; + INFOPLIST_FILE = "iOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = SampleApp; @@ -616,7 +616,7 @@ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", ); - INFOPLIST_FILE = "$(SRCROOT)/iOS/Info.plist"; + INFOPLIST_FILE = "iOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = SampleApp; diff --git a/Examples/UIExplorer/DatePickerIOSExample.js b/Examples/UIExplorer/DatePickerIOSExample.js index 36ff9cd52bbbb2..fc7686880adff5 100644 --- a/Examples/UIExplorer/DatePickerIOSExample.js +++ b/Examples/UIExplorer/DatePickerIOSExample.js @@ -125,6 +125,7 @@ var Heading = React.createClass({ } }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Select dates and times using the native UIDatePicker.'; exports.examples = [ diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index b4c54f997d158f..60a4a5ab1ab24a 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -25,6 +25,7 @@ var { var ImageCapInsetsExample = require('./ImageCapInsetsExample'); +exports.displayName = (undefined: ?string); exports.framework = 'React'; exports.title = ''; exports.description = 'Base component for displaying different types of images.'; diff --git a/Examples/UIExplorer/MapViewExample.js b/Examples/UIExplorer/MapViewExample.js index 23789c7f1346b8..0d1061acef49b8 100644 --- a/Examples/UIExplorer/MapViewExample.js +++ b/Examples/UIExplorer/MapViewExample.js @@ -236,6 +236,7 @@ var styles = StyleSheet.create({ }, }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Base component to display maps'; exports.examples = [ diff --git a/Examples/UIExplorer/Navigator/NavigationBarSample.js b/Examples/UIExplorer/Navigator/NavigationBarSample.js index 2b3f8e2503c0f9..545f76b828ad6e 100644 --- a/Examples/UIExplorer/Navigator/NavigationBarSample.js +++ b/Examples/UIExplorer/Navigator/NavigationBarSample.js @@ -92,6 +92,31 @@ function newRandomRoute() { var NavigationBarSample = React.createClass({ + componentWillMount: function() { + var navigator = this.props.navigator; + + var callback = (event) => { + console.log( + `NavigationBarSample : event ${event.type}`, + { + route: JSON.stringify(event.data.route), + target: event.target, + type: event.type, + } + ); + }; + + // Observe focus change events from this component. + this._listeners = [ + navigator.navigationContext.addListener('willfocus', callback), + navigator.navigationContext.addListener('didfocus', callback), + ]; + }, + + componentWillUnmount: function() { + this._listeners && this._listeners.forEach(listener => listener.remove()); + }, + render: function() { return ( listener.remove()); + }, + + _setNavigatorRef: function(navigator) { + if (navigator !== this._navigator) { + this._navigator = navigator; + + if (navigator) { + var callback = (event) => { + console.log( + `TabBarExample: event ${event.type}`, + { + route: JSON.stringify(event.data.route), + target: event.target, + type: event.type, + } + ); + }; + // Observe focus change events from the owner. + this._listeners = [ + navigator.navigationContext.addListener('willfocus', callback), + navigator.navigationContext.addListener('didfocus', callback), + ]; + } + } + }, }); var styles = StyleSheet.create({ diff --git a/Examples/UIExplorer/PickerIOSExample.js b/Examples/UIExplorer/PickerIOSExample.js index 14361e7608165d..31c81ccccdad5c 100644 --- a/Examples/UIExplorer/PickerIOSExample.js +++ b/Examples/UIExplorer/PickerIOSExample.js @@ -112,6 +112,7 @@ var PickerExample = React.createClass({ }, }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Render lists of selectable options with UIPickerView.'; exports.examples = [ diff --git a/Examples/UIExplorer/ProgressViewIOSExample.js b/Examples/UIExplorer/ProgressViewIOSExample.js index f0a17a7c6e78f2..e294a33708122a 100644 --- a/Examples/UIExplorer/ProgressViewIOSExample.js +++ b/Examples/UIExplorer/ProgressViewIOSExample.js @@ -60,6 +60,7 @@ var ProgressViewExample = React.createClass({ }, }); +exports.displayName = (undefined: ?string); exports.framework = 'React'; exports.title = 'ProgressViewIOS'; exports.description = 'ProgressViewIOS'; diff --git a/Examples/UIExplorer/ScrollViewExample.js b/Examples/UIExplorer/ScrollViewExample.js index 69f3ac9c72d254..1ca8baf9a8530c 100644 --- a/Examples/UIExplorer/ScrollViewExample.js +++ b/Examples/UIExplorer/ScrollViewExample.js @@ -23,6 +23,7 @@ var { Image } = React; +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Component that enables scrolling through child components'; exports.examples = [ diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index ebc67b672068ab..5abfae323f6964 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -25,7 +25,7 @@ var { var Entity = React.createClass({ render: function() { return ( - + {this.props.children} ); @@ -34,7 +34,12 @@ var Entity = React.createClass({ var AttributeToggler = React.createClass({ getInitialState: function() { - return {fontWeight: '500', fontSize: 15}; + return {fontWeight: 'bold', fontSize: 15}; + }, + toggleWeight: function() { + this.setState({ + fontWeight: this.state.fontWeight === 'bold' ? 'normal' : 'bold' + }); }, increaseSize: function() { this.setState({ @@ -42,22 +47,26 @@ var AttributeToggler = React.createClass({ }); }, render: function() { - var curStyle = {fontSize: this.state.fontSize}; + var curStyle = {fontWeight: this.state.fontWeight, fontSize: this.state.fontSize}; return ( - + Tap the controls below to change attributes. - See how it will even work on{' '} - - this nested text - - - {'>> Increase Size <<'} - + See how it will even work on this nested text - + + Toggle Weight + + + Increase Size + + ); } }); @@ -206,6 +215,12 @@ exports.examples = [ render: function() { return ( + + auto (default) - english LTR + + + أحب اللغة العربية auto (default) - arabic RTL + left left left left left left left left left left left left left left left @@ -282,43 +297,21 @@ exports.examples = [ description: 'backgroundColor is inherited from all types of views.', render: function() { return ( - - - Yellow background inherited from View parent, - - {' '}red background, - - {' '}blue background, - - {' '}inherited blue background, - - {' '}nested green background. - + + Yellow container background, + + {' '}red background, + + {' '}blue background, + + {' '}inherited blue background, + + {' '}nested green background. - - ); - }, -}, { - title: 'containerBackgroundColor attribute', - render: function() { - return ( - - - - - - - Default containerBackgroundColor (inherited) + backgroundColor wash - - - {"containerBackgroundColor: 'transparent' + backgroundColor wash"} - - + ); }, }, { @@ -346,8 +339,4 @@ var styles = StyleSheet.create({ marginBottom: 0, backgroundColor: 'rgba(100, 100, 100, 0.3)' }, - entity: { - fontWeight: '500', - color: '#527fe4', - }, }); diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index 10064491d6e660..06cc12ee383266 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -133,6 +133,7 @@ var styles = StyleSheet.create({ }, }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Single and multi-line text inputs.'; exports.examples = [ diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index acbba362921ea0..494d7771d8b383 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -26,6 +26,7 @@ var { View, } = React; +exports.displayName = (undefined: ?string); exports.title = ' and onPress'; exports.examples = [ { diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index c3dcde8eb494fe..e2f84182e74e9d 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -1,7 +1,7 @@ + version = "1.8"> @@ -22,10 +22,10 @@ + buildForAnalyzing = "NO"> + buildForAnalyzing = "NO"> 0 && !foundElement && !redboxError) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - - redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; - foundElement = [self findSubviewInView:vc.view matching:^(UIView *view) { - if ([view.accessibilityLabel isEqualToString:@""]) { - return YES; - } - return NO; - }]; - } - - XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); - XCTAssertTrue(foundElement, @"Couldn't find element with '' text in %d seconds", TIMEOUT_SECONDS); } #define RCT_SNAPSHOT_TEST(name, reRecord) \ @@ -102,10 +54,4 @@ - (void)test##name##Snapshot \ RCT_SNAPSHOT_TEST(SliderExample, NO) RCT_SNAPSHOT_TEST(TabBarExample, NO) -// Make sure this test runs last -- (void)testZZZ_NotInRecordMode -{ - RCTAssert(_runner.recordMode == NO, @"Don't forget to turn record mode back to NO before commit."); -} - @end diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/AppEventsTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/AppEventsTest.js index e46b956b4d59d9..bf7931b5a48a21 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/AppEventsTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/AppEventsTest.js @@ -53,6 +53,8 @@ var AppEventsTest = React.createClass({ } }); +AppEventsTest.displayName = 'AppEventsTest'; + var styles = StyleSheet.create({ container: { margin: 40, diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/AsyncStorageTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/AsyncStorageTest.js index 911887d3e2dd89..43df4df182e0b1 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/AsyncStorageTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/AsyncStorageTest.js @@ -53,15 +53,18 @@ function expectEqual(lhs, rhs, testname) { ); } -function expectAsyncNoError(err) { - expectTrue(err === null, 'Unexpected Async error: ' + JSON.stringify(err)); +function expectAsyncNoError(place, err) { + if (err instanceof Error) { + err = err.message; + } + expectTrue(err === null, 'Unexpected error in ' + place + ': ' + JSON.stringify(err)); } function testSetAndGet() { AsyncStorage.setItem(KEY_1, VAL_1, (err1) => { - expectAsyncNoError(err1); + expectAsyncNoError('testSetAndGet/setItem', err1); AsyncStorage.getItem(KEY_1, (err2, result) => { - expectAsyncNoError(err2); + expectAsyncNoError('testSetAndGet/getItem', err2); expectEqual(result, VAL_1, 'testSetAndGet setItem'); updateMessage('get(key_1) correctly returned ' + result); runTestCase('should get null for missing key', testMissingGet); @@ -71,7 +74,7 @@ function testSetAndGet() { function testMissingGet() { AsyncStorage.getItem(KEY_2, (err, result) => { - expectAsyncNoError(err); + expectAsyncNoError('testMissingGet/setItem', err); expectEqual(result, null, 'testMissingGet'); updateMessage('missing get(key_2) correctly returned ' + result); runTestCase('check set twice results in a single key', testSetTwice); @@ -82,7 +85,7 @@ function testSetTwice() { AsyncStorage.setItem(KEY_1, VAL_1, ()=>{ AsyncStorage.setItem(KEY_1, VAL_1, ()=>{ AsyncStorage.getItem(KEY_1, (err, result) => { - expectAsyncNoError(err); + expectAsyncNoError('testSetTwice/setItem', err); expectEqual(result, VAL_1, 'testSetTwice'); updateMessage('setTwice worked as expected'); runTestCase('test removeItem', testRemoveItem); @@ -95,17 +98,17 @@ function testRemoveItem() { AsyncStorage.setItem(KEY_1, VAL_1, ()=>{ AsyncStorage.setItem(KEY_2, VAL_2, ()=>{ AsyncStorage.getAllKeys((err, result) => { - expectAsyncNoError(err); + expectAsyncNoError('testRemoveItem/getAllKeys', err); expectTrue( result.indexOf(KEY_1) >= 0 && result.indexOf(KEY_2) >= 0, 'Missing KEY_1 or KEY_2 in ' + '(' + result + ')' ); updateMessage('testRemoveItem - add two items'); AsyncStorage.removeItem(KEY_1, (err2) => { - expectAsyncNoError(err2); + expectAsyncNoError('testRemoveItem/removeItem', err2); updateMessage('delete successful '); AsyncStorage.getItem(KEY_1, (err3, result2) => { - expectAsyncNoError(err3); + expectAsyncNoError('testRemoveItem/getItem', err3); expectEqual( result2, null, @@ -113,7 +116,7 @@ function testRemoveItem() { ); updateMessage('key properly removed '); AsyncStorage.getAllKeys((err4, result3) => { - expectAsyncNoError(err4); + expectAsyncNoError('testRemoveItem/getAllKeys', err4); expectTrue( result3.indexOf(KEY_1) === -1, 'Unexpected: KEY_1 present in ' + result3 @@ -130,11 +133,11 @@ function testRemoveItem() { function testMerge() { AsyncStorage.setItem(KEY_MERGE, JSON.stringify(VAL_MERGE_1), (err1) => { - expectAsyncNoError(err1); + expectAsyncNoError('testMerge/setItem', err1); AsyncStorage.mergeItem(KEY_MERGE, JSON.stringify(VAL_MERGE_2), (err2) => { - expectAsyncNoError(err2); + expectAsyncNoError('testMerge/mergeItem', err2); AsyncStorage.getItem(KEY_MERGE, (err3, result) => { - expectAsyncNoError(err3); + expectAsyncNoError('testMerge/setItem', err3); expectEqual(JSON.parse(result), VAL_MERGE_EXPECT, 'testMerge'); updateMessage('objects deeply merged\nDone!'); done(); @@ -173,4 +176,6 @@ var AsyncStorageTest = React.createClass({ } }); +AsyncStorageTest.displayName = 'AsyncStorageTest'; + module.exports = AsyncStorageTest; diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestHarnessTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestHarnessTest.js index adf57ab3d6b83f..12243a0fec90fa 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestHarnessTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestHarnessTest.js @@ -59,4 +59,6 @@ var IntegrationTestHarnessTest = React.createClass({ } }); +IntegrationTestHarnessTest.displayName = 'IntegrationTestHarnessTest'; + module.exports = IntegrationTestHarnessTest; diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/LayoutEventsTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/LayoutEventsTest.js index 7e8cd3a0dcad3b..260a1c8c9ec5cd 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/LayoutEventsTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/LayoutEventsTest.js @@ -164,4 +164,6 @@ var styles = StyleSheet.create({ }, }); +LayoutEventsTest.displayName = 'LayoutEventsTest'; + module.exports = LayoutEventsTest; diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/PromiseTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/PromiseTest.js index 38660d3d8b575a..14e96fc723fe3d 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/PromiseTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/PromiseTest.js @@ -22,7 +22,7 @@ var PromiseTest = React.createClass({ Promise.all([ this.testShouldResolve(), this.testShouldReject(), - ]).then(() => RCTTestModule.finish( + ]).then(() => RCTTestModule.markTestPassed( this.shouldResolve && this.shouldReject )); }, @@ -42,9 +42,11 @@ var PromiseTest = React.createClass({ }, render() { - return ; + return ; } }); +PromiseTest.displayName = 'PromiseTest'; + module.exports = PromiseTest; diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/SimpleSnapshotTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/SimpleSnapshotTest.js index 1715f093f8d7dd..20eacec48998f9 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/SimpleSnapshotTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/SimpleSnapshotTest.js @@ -24,8 +24,8 @@ var SimpleSnapshotTest = React.createClass({ requestAnimationFrame(() => TestModule.verifySnapshot(this.done)); }, - done() { - TestModule.markTestCompleted(); + done(success) { + TestModule.markTestPassed(success); }, render() { @@ -53,4 +53,6 @@ var styles = StyleSheet.create({ }, }); +SimpleSnapshotTest.displayName = 'SimpleSnapshotTest'; + module.exports = SimpleSnapshotTest; diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/TimersTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/TimersTest.js index 4814d8b1dbd9e0..c84dc31927dea3 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/TimersTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/TimersTest.js @@ -152,4 +152,6 @@ var styles = StyleSheet.create({ }, }); +TimersTest.displayName = 'TimersTest'; + module.exports = TimersTest; diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index a8050b33b92442..5af5a32c648f92 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -115,7 +115,7 @@ COMPONENTS.concat(APIS).forEach((Example) => { // View is still blank after first RAF :\ global.requestAnimationFrame(() => global.requestAnimationFrame(() => TestModule.verifySnapshot( - TestModule.markTestCompleted + TestModule.markTestPassed ) )); }, diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m index ea6af0805410c9..13bcdd7b426c28 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m @@ -79,25 +79,19 @@ @interface RCTBridgeTests : XCTestCase { RCTBridge *_bridge; BOOL _testMethodCalled; - dispatch_queue_t _queue; } @end @implementation RCTBridgeTests -RCT_EXPORT_MODULE(TestModule) +@synthesize methodQueue = _methodQueue; -- (dispatch_queue_t)methodQueue -{ - return _queue; -} +RCT_EXPORT_MODULE(TestModule) - (void)setUp { [super setUp]; - _queue = dispatch_queue_create("com.facebook.React.TestQueue", DISPATCH_QUEUE_SERIAL); - _bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[self]; } launchOptions:nil]; @@ -151,7 +145,7 @@ - (void)testCallNativeMethod [_bridge.batchedBridge _handleBuffer:buffer context:RCTGetExecutorID(executor)]; - dispatch_sync(_queue, ^{ + dispatch_sync(_methodQueue, ^{ // clear the queue XCTAssertTrue(_testMethodCalled); }); diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m new file mode 100644 index 00000000000000..afe708c5eacac1 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m @@ -0,0 +1,99 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "RCTShadowView.h" + +@interface RCTShadowViewTests : XCTestCase + +@end + +@implementation RCTShadowViewTests + +// Just a basic sanity test to ensure css-layout is applied correctly in the context of our shadow view hierarchy. +// +// ==================================== +// || header || +// ==================================== +// || || || || +// || left || center || right || +// || || || || +// ==================================== +// || footer || +// ==================================== +// +- (void)testApplyingLayoutRecursivelyToShadowView +{ + RCTShadowView *leftView = [self _shadowViewWithStyle:^(css_style_t *style) { + style->flex = 1; + }]; + + RCTShadowView *centerView = [self _shadowViewWithStyle:^(css_style_t *style) { + style->flex = 2; + style->margin[0] = 10; + style->margin[2] = 10; + }]; + + RCTShadowView *rightView = [self _shadowViewWithStyle:^(css_style_t *style) { + style->flex = 1; + }]; + + RCTShadowView *mainView = [self _shadowViewWithStyle:^(css_style_t *style) { + style->flex_direction = CSS_FLEX_DIRECTION_ROW; + style->flex = 2; + style->margin[1] = 10; + style->margin[3] = 10; + }]; + + [mainView insertReactSubview:leftView atIndex:0]; + [mainView insertReactSubview:centerView atIndex:1]; + [mainView insertReactSubview:rightView atIndex:2]; + + RCTShadowView *headerView = [self _shadowViewWithStyle:^(css_style_t *style) { + style->flex = 1; + }]; + + RCTShadowView *footerView = [self _shadowViewWithStyle:^(css_style_t *style) { + style->flex = 1; + }]; + + RCTShadowView *parentView = [self _shadowViewWithStyle:^(css_style_t *style) { + style->flex_direction = CSS_FLEX_DIRECTION_COLUMN; + style->padding[0] = 10; + style->padding[1] = 10; + style->padding[2] = 10; + style->padding[3] = 10; + style->dimensions[0] = 440; + style->dimensions[1] = 440; + }]; + + [parentView insertReactSubview:headerView atIndex:0]; + [parentView insertReactSubview:mainView atIndex:1]; + [parentView insertReactSubview:footerView atIndex:2]; + + [parentView collectRootUpdatedFrames:nil parentConstraint:CGSizeZero]; + + XCTAssertTrue(CGRectEqualToRect([parentView measureLayoutRelativeToAncestor:parentView], CGRectMake(0, 0, 440, 440))); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets([parentView paddingAsInsets], UIEdgeInsetsMake(10, 10, 10, 10))); + + XCTAssertTrue(CGRectEqualToRect([headerView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 10, 420, 100))); + XCTAssertTrue(CGRectEqualToRect([mainView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 120, 420, 200))); + XCTAssertTrue(CGRectEqualToRect([footerView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 330, 420, 100))); + + XCTAssertTrue(CGRectEqualToRect([leftView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 120, 100, 200))); + XCTAssertTrue(CGRectEqualToRect([centerView measureLayoutRelativeToAncestor:parentView], CGRectMake(120, 120, 200, 200))); + XCTAssertTrue(CGRectEqualToRect([rightView measureLayoutRelativeToAncestor:parentView], CGRectMake(330, 120, 100, 200))); +} + +- (RCTShadowView *)_shadowViewWithStyle:(void(^)(css_style_t *style))styleBlock +{ + RCTShadowView *shadowView = [[RCTShadowView alloc] init]; + + css_style_t style = shadowView.cssNode->style; + styleBlock(&style); + shadowView.cssNode->style = style; + + return shadowView; +} + +@end diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index d1e990cb4b4995..fe3cbef6f7d590 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -43,6 +43,7 @@ var WebViewExample = React.createClass({ backButtonEnabled: false, forwardButtonEnabled: false, loading: true, + scalesPageToFit: true, }; }, @@ -97,6 +98,7 @@ var WebViewExample = React.createClass({ javaScriptEnabledAndroid={true} onNavigationStateChange={this.onNavigationStateChange} startInLoadingState={true} + scalesPageToFit={this.state.scalesPageToFit} /> {this.state.status} @@ -124,6 +126,7 @@ var WebViewExample = React.createClass({ url: navState.url, status: navState.title, loading: navState.loading, + scalesPageToFit: true }); }, @@ -217,6 +220,7 @@ var styles = StyleSheet.create({ }, }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Base component to display web content'; exports.examples = [ diff --git a/Libraries/AdSupport/RCTAdSupport.h b/Libraries/AdSupport/RCTAdSupport.h index d55d503a17c491..56e561f745301c 100644 --- a/Libraries/AdSupport/RCTAdSupport.h +++ b/Libraries/AdSupport/RCTAdSupport.h @@ -7,8 +7,6 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import - #import "RCTBridgeModule.h" @interface RCTAdSupport : NSObject diff --git a/Libraries/AdSupport/RCTAdSupport.m b/Libraries/AdSupport/RCTAdSupport.m index 1b2ad92de8e032..dff794e343f4f6 100644 --- a/Libraries/AdSupport/RCTAdSupport.m +++ b/Libraries/AdSupport/RCTAdSupport.m @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import "RCTAdSupport.h" @implementation RCTAdSupport diff --git a/Libraries/Animation/RCTAnimationExperimentalManager.m b/Libraries/Animation/RCTAnimationExperimentalManager.m index 6bcda39ae30d83..cff4ece15b9ca7 100644 --- a/Libraries/Animation/RCTAnimationExperimentalManager.m +++ b/Libraries/Animation/RCTAnimationExperimentalManager.m @@ -217,6 +217,11 @@ static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NS } NSValue *fromValue = [view.layer.presentationLayer valueForKeyPath:keypath]; +#if !CGFLOAT_IS_DOUBLE + if ([fromValue isKindOfClass:[NSNumber class]]) { + fromValue = [NSNumber numberWithFloat:[(NSNumber *)fromValue doubleValue]]; + } +#endif CGFloat fromFields[count]; [fromValue getValue:fromFields]; @@ -235,7 +240,6 @@ static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NS @try { [view.layer setValue:toValue forKey:keypath]; NSString *animationKey = [@"RCT" stringByAppendingString:RCTJSONStringify(@{@"tag": animationTag, @"key": keypath}, nil)]; - [view.layer addAnimation:animation forKey:animationKey]; if (!completionBlockSet) { strongSelf->_callbackRegistry[animationTag] = callback; [CATransaction setCompletionBlock:^{ @@ -247,6 +251,7 @@ static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NS }]; completionBlockSet = YES; } + [view.layer addAnimation:animation forKey:animationKey]; } @catch (NSException *exception) { return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, toValue); diff --git a/Libraries/AppRegistry/AppRegistry.js b/Libraries/AppRegistry/AppRegistry.js index 157cbaa379b134..7465d85b925d32 100644 --- a/Libraries/AppRegistry/AppRegistry.js +++ b/Libraries/AppRegistry/AppRegistry.js @@ -68,7 +68,7 @@ var AppRegistry = { console.log( 'Running application "' + appKey + '" with appParams: ' + JSON.stringify(appParameters) + '. ' + - '__DEV__ === ' + __DEV__ + + '__DEV__ === ' + String(__DEV__) + ', development-level warning are ' + (__DEV__ ? 'ON' : 'OFF') + ', performance optimizations are ' + (__DEV__ ? 'OFF' : 'ON') ); diff --git a/Libraries/BatchedBridge/BatchedBridge.js b/Libraries/BatchedBridge/BatchedBridge.js new file mode 100644 index 00000000000000..ea49b202fc01a2 --- /dev/null +++ b/Libraries/BatchedBridge/BatchedBridge.js @@ -0,0 +1,20 @@ +/** + * 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 BatchedBridge + */ +'use strict'; + +let MessageQueue = require('MessageQueue'); + +let BatchedBridge = new MessageQueue( + __fbBatchedBridgeConfig.remoteModuleConfig, + __fbBatchedBridgeConfig.localModulesConfig, +); + +module.exports = BatchedBridge; diff --git a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridge.js b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridge.js deleted file mode 100644 index 249e27e76a285d..00000000000000 --- a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridge.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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 BatchedBridge - */ -'use strict'; - -var BatchedBridgeFactory = require('BatchedBridgeFactory'); -var MessageQueue = require('MessageQueue'); - -/** - * Signature that matches the native IOS modules/methods that are exposed. We - * indicate which ones accept a callback. The order of modules and methods - * within them implicitly define their numerical *ID* that will be used to - * describe method calls across the wire. This is so that memory is used - * efficiently and we do not need to copy strings in native land - or across any - * wire. - */ - -var remoteModulesConfig = __fbBatchedBridgeConfig.remoteModuleConfig; -var localModulesConfig = __fbBatchedBridgeConfig.localModulesConfig; - - -var BatchedBridge = BatchedBridgeFactory.create( - MessageQueue, - remoteModulesConfig, - localModulesConfig -); - -BatchedBridge._config = remoteModulesConfig; - -module.exports = BatchedBridge; diff --git a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js deleted file mode 100644 index 3243fb1456b697..00000000000000 --- a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * 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 BatchedBridgeFactory - */ -'use strict'; - -var invariant = require('invariant'); -var keyMirror = require('keyMirror'); -var mapObject = require('mapObject'); -var warning = require('warning'); - -var slice = Array.prototype.slice; - -var MethodTypes = keyMirror({ - remote: null, - remoteAsync: null, - local: null, -}); - -type ErrorData = { - message: string; - domain: string; - code: number; - nativeStackIOS?: string; -}; - -/** - * Creates remotely invokable modules. - */ -var BatchedBridgeFactory = { - MethodTypes: MethodTypes, - /** - * @param {MessageQueue} messageQueue Message queue that has been created with - * the `moduleConfig` (among others perhaps). - * @param {object} moduleConfig Configuration of module names/method - * names to callback types. - * @return {object} Remote representation of configured module. - */ - _createBridgedModule: function(messageQueue, moduleConfig, moduleName) { - var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) { - switch (methodConfig.type) { - case MethodTypes.remoteAsync: - return function(...args) { - return new Promise((resolve, reject) => { - messageQueue.call(moduleName, memberName, args, resolve, (errorData) => { - var error = _createErrorFromErrorData(errorData); - reject(error); - }); - }); - }; - - case MethodTypes.local: - return null; - - default: - return function() { - var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null; - var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null; - var hasSuccCB = typeof lastArg === 'function'; - var hasErrorCB = typeof secondLastArg === 'function'; - hasErrorCB && invariant( - hasSuccCB, - 'Cannot have a non-function arg after a function arg.' - ); - var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0); - var args = slice.call(arguments, 0, arguments.length - numCBs); - var onSucc = hasSuccCB ? lastArg : null; - var onFail = hasErrorCB ? secondLastArg : null; - return messageQueue.call(moduleName, memberName, args, onFail, onSucc); - }; - } - }); - for (var constName in moduleConfig.constants) { - warning(!remoteModule[constName], 'saw constant and method named %s', constName); - remoteModule[constName] = moduleConfig.constants[constName]; - } - return remoteModule; - }, - - create: function(MessageQueue, modulesConfig, localModulesConfig) { - var messageQueue = new MessageQueue(modulesConfig, localModulesConfig); - return { - callFunction: messageQueue.callFunction.bind(messageQueue), - callFunctionReturnFlushedQueue: - messageQueue.callFunctionReturnFlushedQueue.bind(messageQueue), - invokeCallback: messageQueue.invokeCallback.bind(messageQueue), - invokeCallbackAndReturnFlushedQueue: - messageQueue.invokeCallbackAndReturnFlushedQueue.bind(messageQueue), - flushedQueue: messageQueue.flushedQueue.bind(messageQueue), - RemoteModules: mapObject(modulesConfig, this._createBridgedModule.bind(this, messageQueue)), - setLoggingEnabled: messageQueue.setLoggingEnabled.bind(messageQueue), - getLoggedOutgoingItems: messageQueue.getLoggedOutgoingItems.bind(messageQueue), - getLoggedIncomingItems: messageQueue.getLoggedIncomingItems.bind(messageQueue), - replayPreviousLog: messageQueue.replayPreviousLog.bind(messageQueue), - processBatch: messageQueue.processBatch.bind(messageQueue), - }; - } -}; - -function _createErrorFromErrorData(errorData: ErrorData): Error { - var { - message, - ...extraErrorInfo, - } = errorData; - var error = new Error(message); - error.framesToPop = 1; - return Object.assign(error, extraErrorInfo); -} - -module.exports = BatchedBridgeFactory; diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js index 67fa5083048dfd..1f5c6c22a1582f 100644 --- a/Libraries/CameraRoll/CameraRoll.js +++ b/Libraries/CameraRoll/CameraRoll.js @@ -71,6 +71,11 @@ var getPhotosParamChecker = createStrictShapeTypeChecker({ * Specifies filter on asset type */ assetType: ReactPropTypes.oneOf(ASSET_TYPE_OPTIONS), + + /** + * Filter by mimetype (e.g. image/jpeg). + */ + mimeTypes: ReactPropTypes.arrayOf(ReactPropTypes.string), }); /** diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js index c229b60c321540..53390cabed3e6a 100644 --- a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js +++ b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js @@ -88,9 +88,11 @@ var styles = StyleSheet.create({ justifyContent: 'center', }, sizeSmall: { + width: 20, height: 20, }, sizeLarge: { + width: 36, height: 36, } }); diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 8952fb0d1e624a..512d8dbd6a4192 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -42,6 +42,14 @@ var INNERVIEW = 'InnerScrollView'; * Component that wraps platform ScrollView while providing * integration with touch locking "responder" system. * + * Keep in mind that ScrollViews must have a bounded height in order to work, + * since they contain unbounded-height children into a bounded container (via + * a scroll interaction). In order to bound the height of a ScrollView, either + * set the height of the view directly (discouraged) or make sure all parent + * views have bounded height. Forgetting to transfer `{flex: 1}` down the + * view stack can lead to errors here, which the element inspector makes + * easy to debug. + * * Doesn't yet support other contained responders from blocking this scroll * view from becoming the responder. */ diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 00a27e3c1d4901..c0b1981e1439c5 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -44,12 +44,16 @@ var AndroidTextInputAttributes = { autoCapitalize: true, autoCorrect: true, autoFocus: true, + textAlign: true, + textAlignVertical: true, keyboardType: true, multiline: true, password: true, placeholder: true, + placeholderTextColor: true, text: true, testID: true, + underlineColorAndroid: true, }; var viewConfigAndroid = { @@ -68,8 +72,8 @@ type Event = Object; /** * A foundational component for inputting text into the app via a - * keyboard. Props provide configurability for several features, such as auto- - * correction, auto-capitalization, placeholder text, and different keyboard + * keyboard. Props provide configurability for several features, such as + * auto-correction, auto-capitalization, placeholder text, and different keyboard * types, such as a numeric keypad. * * The simplest use case is to plop down a `TextInput` and subscribe to the @@ -123,6 +127,19 @@ var TextInput = React.createClass({ * If true, focuses the input on componentDidMount. Default value is false. */ autoFocus: PropTypes.bool, + /** + * Set the position of the cursor from where editing will begin. + */ + textAlign: PropTypes.oneOf([ + 'start', + 'center', + 'end', + ]), + textAlignVertical: PropTypes.oneOf([ + 'top', + 'center', + 'bottom', + ]), /** * If false, text is not editable. Default value is true. */ @@ -260,6 +277,10 @@ var TextInput = React.createClass({ * Used to locate this view in end-to-end tests. */ testID: PropTypes.string, + /** + * The color of the textInput underline. Is only supported on Android. + */ + underlineColorAndroid: PropTypes.string, }, /** @@ -461,6 +482,10 @@ var TextInput = React.createClass({ _renderAndroid: function() { var autoCapitalize = RCTUIManager.UIText.AutocapitalizationType[this.props.autoCapitalize]; + var textAlign = + RCTUIManager.AndroidTextInput.Constants.TextAlign[this.props.textAlign]; + var textAlignVertical = + RCTUIManager.AndroidTextInput.Constants.TextAlignVertical[this.props.textAlignVertical]; var children = this.props.children; var childCount = 0; ReactChildren.forEach(children, () => ++childCount); @@ -477,6 +502,8 @@ var TextInput = React.createClass({ style={[this.props.style]} autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} + textAlign={textAlign} + textAlignVertical={textAlignVertical} keyboardType={this.props.keyboardType} multiline={this.props.multiline} onFocus={this._onFocus} @@ -488,7 +515,9 @@ var TextInput = React.createClass({ onLayout={this.props.onLayout} password={this.props.password || this.props.secureTextEntry} placeholder={this.props.placeholder} + placeholderTextColor={this.props.placeholderTextColor} text={this.state.bufferedValue} + underlineColorAndroid={this.props.underlineColorAndroid} children={children} />; diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 83af4a8adc3910..15ab9e676dec38 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -95,6 +95,11 @@ var WebView = React.createClass({ * Used for android only, JS is enabled by default for WebView on iOS */ javaScriptEnabledAndroid: PropTypes.bool, + /** + * Used for iOS only, sets whether the webpage scales to fit the view and the + * user can change the scale + */ + scalesPageToFit: PropTypes.bool, }, getInitialState: function() { @@ -155,6 +160,7 @@ var WebView = React.createClass({ onLoadingStart={this.onLoadingStart} onLoadingFinish={this.onLoadingFinish} onLoadingError={this.onLoadingError} + scalesPageToFit={this.props.scalesPageToFit} />; return ( diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 857f476c12f87d..dda32340c22fab 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -275,6 +275,10 @@ var ListView = React.createClass({ } }, + componentDidUpdate: function() { + this._measureAndUpdateScrollProps(); + }, + onRowHighlighted: function(sectionID, rowID) { this.setState({highlightedRow: {sectionID, rowID}}); }, @@ -368,7 +372,6 @@ var ListView = React.createClass({ if (!props.scrollEventThrottle) { props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE; } - return ( diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js new file mode 100644 index 00000000000000..35ed24e3ad7aab --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule NavigationContext + */ +'use strict'; + +var NavigationEventEmitter = require('NavigationEventEmitter'); + +var emptyFunction = require('emptyFunction'); +var invariant = require('invariant'); + +import type * as NavigationEvent from 'NavigationEvent'; +import type * as EventSubscription from 'EventSubscription'; + +/** + * Class that contains the info and methods for app navigation. + */ +class NavigationContext { + _eventEmitter: ?NavigationEventEmitter; + _currentRoute: any; + + constructor() { + this._eventEmitter = new NavigationEventEmitter(this); + this._currentRoute = null; + this.addListener('willfocus', this._onFocus, this); + this.addListener('didfocus', this._onFocus, this); + } + + // TODO: @flow does not like this getter. Will add @flow check back once + // getter/setter is supported. + get currentRoute(): any { + return this._currentRoute; + } + + addListener( + eventType: string, + listener: Function, + context: ?Object + ): EventSubscription { + var emitter = this._eventEmitter; + if (emitter) { + return emitter.addListener(eventType, listener, context); + } else { + return {remove: emptyFunction}; + } + } + + emit(eventType: String, data: any): void { + var emitter = this._eventEmitter; + if (emitter) { + emitter.emit(eventType, data); + } + } + + dispose(): void { + var emitter = this._eventEmitter; + if (emitter) { + // clean up everything. + emitter.removeAllListeners(); + this._eventEmitter = null; + this._currentRoute = null; + } + } + + _onFocus(event: NavigationEvent): void { + invariant( + event.data && event.data.hasOwnProperty('route'), + 'didfocus event should provide route' + ); + this._currentRoute = event.data.route; + } +} + +module.exports = NavigationContext; diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js new file mode 100644 index 00000000000000..b6923b4f2b0549 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js @@ -0,0 +1,21 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule NavigationEvent + * @flow + */ +'use strict'; + +class NavigationEvent { + type: String; + target: Object; + data: any; + + constructor(type: String, target: Object, data: any) { + this.type = type; + this.target = target; + this.data = data; + } +} + +module.exports = NavigationEvent; diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js new file mode 100644 index 00000000000000..db9e785540f164 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule NavigationEventEmitter + * @flow + */ +'use strict'; + +var EventEmitter = require('EventEmitter'); +var NavigationEvent = require('NavigationEvent'); + +type EventParams = { + eventType: String; + data: any; +}; + +class NavigationEventEmitter extends EventEmitter { + _emitQueue: Array; + _emitting: boolean; + _target: Object; + + constructor(target: Object) { + super(); + this._emitting = false; + this._emitQueue = []; + this._target = target; + } + + emit(eventType: String, data: any): void { + if (this._emitting) { + // An event cycle that was previously created hasn't finished yet. + // Put this event cycle into the queue and will finish them later. + this._emitQueue.push({eventType, data}); + return; + } + + this._emitting = true; + var event = new NavigationEvent(eventType, this._target, data); + super.emit(eventType, event); + this._emitting = false; + + while (this._emitQueue.length) { + var arg = this._emitQueue.shift(); + this.emit(arg.eventType, arg.data); + } + } +} + +module.exports = NavigationEventEmitter; diff --git a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js new file mode 100644 index 00000000000000..f81c9314affeb3 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +'use strict'; + +jest + .dontMock('EmitterSubscription') + .dontMock('EventSubscription') + .dontMock('EventEmitter') + .dontMock('EventSubscriptionVendor') + .dontMock('NavigationContext') + .dontMock('NavigationEvent') + .dontMock('NavigationEventEmitter') + .dontMock('invariant'); + +var NavigationContext = require('NavigationContext'); + +describe('NavigationContext', () => { + it('defaults `currentRoute` to null', () => { + var context = new NavigationContext(); + expect(context.currentRoute).toEqual(null); + }); + + it('updates `currentRoute`', () => { + var context = new NavigationContext(); + context.emit('didfocus', {route: {name: 'a'}}); + expect(context.currentRoute.name).toEqual('a'); + }); +}); + + diff --git a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js new file mode 100644 index 00000000000000..2a8d7d82a73cf0 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +'use strict'; + +jest + .dontMock('EmitterSubscription') + .dontMock('EventEmitter') + .dontMock('EventSubscriptionVendor') + .dontMock('NavigationEvent') + .dontMock('NavigationEventEmitter'); + +var NavigationEventEmitter = require('NavigationEventEmitter'); + +describe('NavigationEventEmitter', () => { + it('emit event', () => { + var target = {}; + var emitter = new NavigationEventEmitter(target); + var focusCounter = 0; + var focusTarget; + + emitter.addListener('focus', (event) => { + focusCounter++; + focusTarget = event.target; + }); + + emitter.emit('focus'); + emitter.emit('blur'); + + expect(focusCounter).toBe(1); + expect(focusTarget).toBe(target); + }); + + it('put nested emit call in queue', () => { + var target = {}; + var emitter = new NavigationEventEmitter(target); + var logs = []; + + emitter.addListener('one', () => { + logs.push(1); + emitter.emit('two'); + logs.push(2); + }); + + emitter.addListener('two', () => { + logs.push(3); + emitter.emit('three'); + logs.push(4); + }); + + emitter.addListener('three', () => { + logs.push(5); + }); + + emitter.emit('one'); + + expect(logs).toEqual([1, 2, 3, 4, 5]); + }); +}); diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index af10348fcac51e..d9e452d22d9db1 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -30,11 +30,11 @@ var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule; var Dimensions = require('Dimensions'); var InteractionMixin = require('InteractionMixin'); +var NavigationContext = require('NavigationContext'); var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar'); var NavigatorNavigationBar = require('NavigatorNavigationBar'); var NavigatorSceneConfigs = require('NavigatorSceneConfigs'); var PanResponder = require('PanResponder'); -var Platform = require('Platform'); var React = require('React'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); @@ -203,11 +203,17 @@ var Navigator = React.createClass({ initialRouteStack: PropTypes.arrayOf(PropTypes.object), /** + * @deprecated + * Use `navigationContext.addListener('willfocus', callback)` instead. + * * Will emit the target route upon mounting and before each nav transition */ onWillFocus: PropTypes.func, /** + * @deprecated + * Use `navigationContext.addListener('didfocus', callback)` instead. + * * Will be called with the new route of each scene after the transition is * complete or after the initial mounting */ @@ -283,6 +289,9 @@ var Navigator = React.createClass({ }, componentWillMount: function() { + // TODO(t7489503): Don't need this once ES6 Class landed. + this.__defineGetter__('navigationContext', this._getNavigationContext); + this._subRouteFocus = []; this.parentNavigator = this.props.navigator; this._handlers = {}; @@ -321,7 +330,10 @@ var Navigator = React.createClass({ }, componentWillUnmount: function() { - + if (this._navigationContext) { + this._navigationContext.dispose(); + this._navigationContext = null; + } }, /** @@ -400,13 +412,11 @@ var Navigator = React.createClass({ ); } else if (this.state.activeGesture != null) { var presentedToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); - if (presentedToIndex > -1) { - this._transitionBetween( - this.state.presentedIndex, - presentedToIndex, - this.spring.getCurrentValue() - ); - } + this._transitionBetween( + this.state.presentedIndex, + presentedToIndex, + this.spring.getCurrentValue() + ); } }, @@ -461,12 +471,16 @@ var Navigator = React.createClass({ }, _emitDidFocus: function(route) { + this.navigationContext.emit('didfocus', {route: route}); + if (this.props.onDidFocus) { this.props.onDidFocus(route); } }, _emitWillFocus: function(route) { + this.navigationContext.emit('willfocus', {route: route}); + var navBar = this._navBar; if (navBar && navBar.handleWillFocus) { navBar.handleWillFocus(route); @@ -806,7 +820,7 @@ var Navigator = React.createClass({ this._transitionSceneStyle(fromIndex, toIndex, progress, fromIndex); this._transitionSceneStyle(fromIndex, toIndex, progress, toIndex); var navBar = this._navBar; - if (navBar && navBar.updateProgress) { + if (navBar && navBar.updateProgress && toIndex >= 0 && fromIndex >= 0) { navBar.updateProgress(progress, fromIndex, toIndex); } }, @@ -1139,6 +1153,13 @@ var Navigator = React.createClass({ ); }, + + _getNavigationContext: function() { + if (!this._navigationContext) { + this._navigationContext = new NavigationContext(); + } + return this._navigationContext; + } }); module.exports = Navigator; diff --git a/Libraries/DebugComponentHierarchy/RCTDebugComponentOwnership.js b/Libraries/DebugComponentHierarchy/RCTDebugComponentOwnership.js new file mode 100644 index 00000000000000..4ee5a1f0397668 --- /dev/null +++ b/Libraries/DebugComponentHierarchy/RCTDebugComponentOwnership.js @@ -0,0 +1,55 @@ +/** + * 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. + * + * Utility class to provide the component owner hierarchy to native code for + * debugging purposes. + * + * @providesModule RCTDebugComponentOwnership + * @flow + */ + +'use strict'; + +var DebugComponentOwnershipModule = require('NativeModules').DebugComponentOwnershipModule; +var InspectorUtils = require('InspectorUtils'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); + +function componentToString(component) { + return component.getName ? component.getName() : 'Unknown'; +} + +function getRootTagForTag(tag: number): ?number { + var rootNodeID = ReactNativeTagHandles.tagToRootNodeID[tag]; + if (!rootNodeID) { + return null; + } + var rootID = ReactNativeTagHandles.getNativeTopRootIDFromNodeID(rootNodeID); + if (!rootID) { + return null; + } + return ReactNativeTagHandles.rootNodeIDToTag[rootID]; +} + +module.exports = { + + /** + * Asynchronously returns the owner hierarchy as an array of strings. Request id is + * passed along to the native module so that the native module can identify the + * particular call instance. + * + * Example returned owner hierarchy: ['RootView', 'Dialog', 'TitleView', 'Text'] + */ + getOwnerHierarchy: function(requestID: number, tag: number) { + var rootTag = getRootTagForTag(tag); + var instance = InspectorUtils.findInstanceByNativeTag(rootTag, tag); + var ownerHierarchy = instance ? + InspectorUtils.getOwnerHierarchy(instance).map(componentToString) : + null; + DebugComponentOwnershipModule.receiveOwnershipHierarchy(requestID, tag, ownerHierarchy); + }, +}; diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 23e2c7a6372bc5..7a629ce9a60c48 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -178,7 +178,6 @@ var nativeOnlyProps = { src: true, defaultImageSrc: true, imageTag: true, - resizeMode: true, }; if (__DEV__) { verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps); diff --git a/Libraries/Image/ImagePickerIOS.js b/Libraries/Image/ImagePickerIOS.js new file mode 100644 index 00000000000000..9b2f75e5b92d9a --- /dev/null +++ b/Libraries/Image/ImagePickerIOS.js @@ -0,0 +1,40 @@ +/** + * 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 ImagePickerIOS + * @flow + */ +'use strict'; + +var RCTImagePicker = require('NativeModules').ImagePickerIOS; + +var ImagePickerIOS = { + canRecordVideos: function(callback: Function) { + return RCTImagePicker.canRecordVideos(callback); + }, + canUseCamera: function(callback: Function) { + return RCTImagePicker.canUseCamera(callback); + }, + openCameraDialog: function(config: Object, successCallback: Function, cancelCallback: Function) { + config = { + videoMode: false, + ...config, + } + return RCTImagePicker.openCameraDialog(config, successCallback, cancelCallback); + }, + openSelectDialog: function(config: Object, successCallback: Function, cancelCallback: Function) { + config = { + showImages: true, + showVideos: false, + ...config, + } + return RCTImagePicker.openSelectDialog(config, successCallback, cancelCallback); + }, +}; + +module.exports = ImagePickerIOS; diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 9e5427bf30a4a8..1e3cf75c894e05 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */; }; 1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; }; 1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1345A8381B26592900583190 /* RCTImageRequestHandler.m */; }; + 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137620341B31C53500677FF0 /* RCTImagePickerManager.m */; }; 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; }; 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */; }; @@ -39,6 +40,8 @@ 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImage.m; sourceTree = ""; }; 1345A8371B26592900583190 /* RCTImageRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageRequestHandler.h; sourceTree = ""; }; 1345A8381B26592900583190 /* RCTImageRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageRequestHandler.m; sourceTree = ""; }; + 137620331B31C53500677FF0 /* RCTImagePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImagePickerManager.h; sourceTree = ""; }; + 137620341B31C53500677FF0 /* RCTImagePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImagePickerManager.m; sourceTree = ""; }; 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCameraRollManager.h; sourceTree = ""; }; 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = ""; }; 143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = ""; }; @@ -74,6 +77,8 @@ 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */, 58B511891A9E6BD600147676 /* RCTImageDownloader.h */, 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */, + 137620331B31C53500677FF0 /* RCTImagePickerManager.h */, + 137620341B31C53500677FF0 /* RCTImagePickerManager.m */, 1345A8371B26592900583190 /* RCTImageRequestHandler.h */, 1345A8381B26592900583190 /* RCTImageRequestHandler.m */, 58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */, @@ -155,6 +160,7 @@ buildActionMask = 2147483647; files = ( 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */, + 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */, 58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */, 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */, 1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */, diff --git a/Libraries/Image/RCTImagePickerManager.h b/Libraries/Image/RCTImagePickerManager.h new file mode 100644 index 00000000000000..a008c46f37a022 --- /dev/null +++ b/Libraries/Image/RCTImagePickerManager.h @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2013, 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. + * + */ + +#import "RCTBridgeModule.h" + +@interface RCTImagePickerManager : NSObject + +@end diff --git a/Libraries/Image/RCTImagePickerManager.m b/Libraries/Image/RCTImagePickerManager.m new file mode 100644 index 00000000000000..7fad953b0d6a5a --- /dev/null +++ b/Libraries/Image/RCTImagePickerManager.m @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2013, 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. + * + */ + +#import "RCTImagePickerManager.h" +#import "RCTRootView.h" + +#import + +#import + +@interface RCTImagePickerManager () + +@end + +@implementation RCTImagePickerManager +{ + NSMutableArray *_pickers; + NSMutableArray *_pickerCallbacks; + NSMutableArray *_pickerCancelCallbacks; +} + +RCT_EXPORT_MODULE(ImagePickerIOS); + +- (instancetype)init +{ + if ((self = [super init])) { + _pickers = [[NSMutableArray alloc] init]; + _pickerCallbacks = [[NSMutableArray alloc] init]; + _pickerCancelCallbacks = [[NSMutableArray alloc] init]; + } + return self; +} + +RCT_EXPORT_METHOD(canRecordVideos:(RCTResponseSenderBlock)callback) +{ + NSArray *availableMediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; + callback(@[@([availableMediaTypes containsObject:(NSString *)kUTTypeMovie])]); +} + +RCT_EXPORT_METHOD(canUseCamera:(RCTResponseSenderBlock)callback) +{ + callback(@[@([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])]); +} + +RCT_EXPORT_METHOD(openCameraDialog:(NSDictionary *)config + successCallback:(RCTResponseSenderBlock)callback + cancelCallback:(RCTResponseSenderBlock)cancelCallback) +{ + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIViewController *rootViewController = keyWindow.rootViewController; + + UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; + imagePicker.delegate = self; + imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; + + if ([config[@"videoMode"] boolValue]) { + imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo; + } + + [_pickers addObject:imagePicker]; + [_pickerCallbacks addObject:callback]; + [_pickerCancelCallbacks addObject:cancelCallback]; + + [rootViewController presentViewController:imagePicker animated:YES completion:nil]; +} + +RCT_EXPORT_METHOD(openSelectDialog:(NSDictionary *)config + successCallback:(RCTResponseSenderBlock)callback + cancelCallback:(RCTResponseSenderBlock)cancelCallback) +{ + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIViewController *rootViewController = keyWindow.rootViewController; + + UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; + imagePicker.delegate = self; + imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + + NSMutableArray *allowedTypes = [[NSMutableArray alloc] init]; + if ([config[@"showImages"] boolValue]) { + [allowedTypes addObject:(NSString *)kUTTypeImage]; + } + if ([config[@"showVideos"] boolValue]) { + [allowedTypes addObject:(NSString *)kUTTypeMovie]; + } + + imagePicker.mediaTypes = allowedTypes; + + [_pickers addObject:imagePicker]; + [_pickerCallbacks addObject:callback]; + [_pickerCancelCallbacks addObject:cancelCallback]; + + [rootViewController presentViewController:imagePicker animated:YES completion:nil]; +} + +- (void)imagePickerController:(UIImagePickerController *)picker +didFinishPickingMediaWithInfo:(NSDictionary *)info +{ + NSUInteger index = [_pickers indexOfObject:picker]; + RCTResponseSenderBlock callback = _pickerCallbacks[index]; + + [_pickers removeObjectAtIndex:index]; + [_pickerCallbacks removeObjectAtIndex:index]; + [_pickerCancelCallbacks removeObjectAtIndex:index]; + + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIViewController *rootViewController = keyWindow.rootViewController; + [rootViewController dismissViewControllerAnimated:YES completion:nil]; + + callback(@[[info[UIImagePickerControllerReferenceURL] absoluteString]]); +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker +{ + NSUInteger index = [_pickers indexOfObject:picker]; + RCTResponseSenderBlock callback = _pickerCancelCallbacks[index]; + + [_pickers removeObjectAtIndex:index]; + [_pickerCallbacks removeObjectAtIndex:index]; + [_pickerCancelCallbacks removeObjectAtIndex:index]; + + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIViewController *rootViewController = keyWindow.rootViewController; + [rootViewController dismissViewControllerAnimated:YES completion:nil]; + + callback(@[]); +} + +@end diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index 46615d967b1902..6b1ac5789dfe9b 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -26,6 +26,7 @@ class Inspector extends React.Component { this.state = { panelPos: 'bottom', inspecting: true, + perfing: false, inspected: null, }; } @@ -59,21 +60,25 @@ class Inspector extends React.Component { }); } + setPerfing(val: bool) { + this.setState({ + perfing: val, + inspecting: false, + inspected: null, + }); + } + setInspecting(val: bool) { this.setState({ inspecting: val, + inspected: null }); } render() { - var panelPosition; - if (this.state.panelPos === 'bottom') { - panelPosition = {bottom: -Dimensions.get('window').height}; - } else { - panelPosition = {top: 0}; - } + var panelContainerStyle = (this.state.panelPos === 'bottom') ? {bottom: 0} : {top: 0}; return ( - + {this.state.inspecting && } - + ); + } else if (this.props.perfing) { + contents = ( + + ); } else { contents = ( @@ -58,7 +63,12 @@ class InspectorPanel extends React.Component {