diff --git a/.flowconfig b/.flowconfig index 82d818dcc0..8a7e861c5c 100644 --- a/.flowconfig +++ b/.flowconfig @@ -21,6 +21,7 @@ [libs] Libraries/react-native/react-native-interface.js +Examples/UIExplorer/ImageMocks.js [options] module.system=haste diff --git a/Examples/2048/2048.xcodeproj/project.pbxproj b/Examples/2048/2048.xcodeproj/project.pbxproj index ba1907ff15..696d9331d9 100644 --- a/Examples/2048/2048.xcodeproj/project.pbxproj +++ b/Examples/2048/2048.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 13ACB6741AC2117000FF4204 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13ACB6711AC2113600FF4204 /* libRCTAnimation.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; @@ -16,6 +17,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 13ACB6701AC2113600FF4204 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13ACB66C1AC2113500FF4204 /* RCTAnimation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTAnimation; + }; 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; @@ -33,6 +41,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 13ACB66C1AC2113500FF4204 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = ../../Libraries/Animation/RCTAnimation.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* 2048.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 2048.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = 2048/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = 2048/AppDelegate.m; sourceTree = ""; }; @@ -49,6 +58,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 13ACB6741AC2117000FF4204 /* libRCTAnimation.a in Frameworks */, 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */, 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, ); @@ -57,6 +67,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 13ACB66D1AC2113500FF4204 /* Products */ = { + isa = PBXGroup; + children = ( + 13ACB6711AC2113600FF4204 /* libRCTAnimation.a */, + ); + name = Products; + sourceTree = ""; + }; 13B07FAE1A68108700A75B9A /* 2048 */ = { isa = PBXGroup; children = ( @@ -75,6 +93,7 @@ children = ( 834D32361A76971A00F38302 /* ReactKit.xcodeproj */, 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, + 13ACB66C1AC2113500FF4204 /* RCTAnimation.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -153,6 +172,10 @@ productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectReferences = ( + { + ProductGroup = 13ACB66D1AC2113500FF4204 /* Products */; + ProjectRef = 13ACB66C1AC2113500FF4204 /* RCTAnimation.xcodeproj */; + }, { ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; @@ -170,6 +193,13 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + 13ACB6711AC2113600FF4204 /* libRCTAnimation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTAnimation.a; + remoteRef = 13ACB6701AC2113600FF4204 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index ea4247f719..b845c03a82 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -18,9 +18,9 @@ var { StyleSheet, Text, TextInput, - TimerMixin, View, } = React; +var TimerMixin = require('react-timer-mixin'); var MovieCell = require('./MovieCell'); var MovieScreen = require('./MovieScreen'); diff --git a/Examples/UIExplorer/ActivityIndicatorExample.js b/Examples/UIExplorer/ActivityIndicatorExample.js index 7d05bd88a7..99ae39741f 100644 --- a/Examples/UIExplorer/ActivityIndicatorExample.js +++ b/Examples/UIExplorer/ActivityIndicatorExample.js @@ -14,9 +14,9 @@ var React = require('react-native'); var { ActivityIndicatorIOS, StyleSheet, - TimerMixin, View, } = React; +var TimerMixin = require('react-timer-mixin'); var ToggleAnimatingActivityIndicator = React.createClass({ mixins: [TimerMixin], diff --git a/Examples/UIExplorer/ImageCapInsetsExample.js b/Examples/UIExplorer/ImageCapInsetsExample.js index 458cf74544..ab8838715b 100644 --- a/Examples/UIExplorer/ImageCapInsetsExample.js +++ b/Examples/UIExplorer/ImageCapInsetsExample.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ImageCapInsetsExample + * @flow */ 'use strict'; diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index 5fe6d580fc..fd85e2c556 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -5,6 +5,8 @@ * 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. + * + * @flow */ 'use strict'; diff --git a/Examples/UIExplorer/ImageMocks.js b/Examples/UIExplorer/ImageMocks.js new file mode 100644 index 0000000000..7968cb3c36 --- /dev/null +++ b/Examples/UIExplorer/ImageMocks.js @@ -0,0 +1,35 @@ +/** + * Copyright 2004-present Facebook. 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. + * + * @flow + */ +'use strict'; + +declare module 'image!story-background' { + declare var uri: string; + declare var isStatic: boolean; +} + +declare module 'image!uie_comment_highlighted' { + declare var uri: string; + declare var isStatic: boolean; +} + +declare module 'image!uie_comment_normal' { + declare var uri: string; + declare var isStatic: boolean; +} + +declare module 'image!uie_thumb_normal' { + declare var uri: string; + declare var isStatic: boolean; +} + +declare module 'image!uie_thumb_selected' { + declare var uri: string; + declare var isStatic: boolean; +} diff --git a/Examples/UIExplorer/JSNavigationStack/BreadcrumbNavSample.js b/Examples/UIExplorer/JSNavigationStack/BreadcrumbNavSample.js new file mode 100644 index 0000000000..51efdc0cca --- /dev/null +++ b/Examples/UIExplorer/JSNavigationStack/BreadcrumbNavSample.js @@ -0,0 +1,276 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule BreadcrumbNavSample + */ +'use strict'; + +var BreadcrumbNavigationBar = require('BreadcrumbNavigationBar'); +var JSNavigationStack = require('JSNavigationStack'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var ScrollView = require('ScrollView'); +var TabBarItemIOS = require('TabBarItemIOS'); +var TabBarIOS = require('TabBarIOS'); +var Text = require('Text'); +var TouchableBounce = require('TouchableBounce'); +var View = require('View'); + + + +var SAMPLE_TEXT = 'Top Pushes. Middle Replaces. Bottom Pops.'; + +var _getRandomRoute = function() { + return { + backButtonTitle: 'Back' + ('' + 10 * Math.random()).substr(0, 1), + content: + SAMPLE_TEXT + '\nHere\'s a random number ' + Math.random(), + title: Math.random() > 0.5 ? 'Hello' : 'There', + rightButtonTitle: Math.random() > 0.5 ? 'Right' : 'Button', + }; +}; + + +var SampleNavigationBarRouteMapper = { + rightContentForRoute: function(route, navigator) { + if (route.rightButtonTitle) { + return ( + + {route.rightButtonTitle} + + ); + } else { + return null; + } + }, + titleContentForRoute: function(route, navigator) { + return ( + navigator.push(_getRandomRoute())}> + + {route.title} + + + ); + }, + iconForRoute: function(route, navigator) { + var onPress = + navigator.popToRoute.bind(navigator, route); + return ( + + + + ); + }, + separatorForRoute: function(route, navigator) { + return ( + + + + ); + } +}; + +var _delay = 400; // Just to test for race conditions with native nav. + +var renderScene = function(route, navigator) { + var content = route.content; + return ( + + + + + request push soon + + + + + {content} + + + + + {content} + + + + + {content} + + + + + {content} + + + + + {content} + + + + + request pop soon + + + + + Immediate set two routes + + + + + pop to top soon + + + + + ); +}; + +var _popToTopLater = function(popToTop) { + return () => setTimeout(popToTop, _delay); +}; + +var _pushRouteLater = function(push) { + return () => setTimeout( + () => push(_getRandomRoute()), + _delay + ); +}; + +var _immediatelySetTwoItemsLater = function(immediatelyResetRouteStack) { + return () => setTimeout( + () => immediatelyResetRouteStack([ + _getRandomRoute(), + _getRandomRoute(), + ]) + ); +}; + +var _popRouteLater = function(pop) { + return () => setTimeout(pop, _delay); +}; + +var BreadcrumbNavSample = React.createClass({ + + getInitialState: function() { + return { + selectedTab: 0, + }; + }, + + render: function() { + var initialRoute = { + backButtonTitle: 'Start', // no back button for initial scene + content: SAMPLE_TEXT, + title: 'Campaigns', + rightButtonTitle: 'Filter', + }; + return ( + + + + } + /> + + + JSNavigationStack.AnimationConfigs.FloatFromBottom} + debugOverlay={false} + style={[styles.appContainer]} + initialRoute={initialRoute} + renderScene={renderScene} + navigationBar={ + + } + /> + + + ); + }, + + onTabSelect: function(tab, event) { + if (this.state.selectedTab !== tab) { + this.setState({selectedTab: tab}); + } + }, + +}); + +var styles = StyleSheet.create({ + navigationItem: { + backgroundColor: '#eeeeee', + }, + scene: { + paddingTop: 50, + flex: 1, + }, + button: { + backgroundColor: '#cccccc', + margin: 50, + marginTop: 26, + padding: 10, + }, + buttonText: { + fontSize: 12, + textAlign: 'center', + }, + appContainer: { + overflow: 'hidden', + backgroundColor: '#dddddd', + flex: 1, + }, + titleText: { + fontSize: 18, + color: '#666666', + textAlign: 'center', + fontWeight: 'bold', + lineHeight: 32, + }, + filterText: { + color: '#5577ff', + }, + // TODO: Accept icons from route. + crumbIconPlaceholder: { + flex: 1, + backgroundColor: '#666666', + }, + crumbSeparatorPlaceholder: { + flex: 1, + backgroundColor: '#aaaaaa', + }, +}); + +module.exports = BreadcrumbNavSample; diff --git a/Examples/UIExplorer/JSNavigationStack/JSNavigationStackExample.js b/Examples/UIExplorer/JSNavigationStack/JSNavigationStackExample.js new file mode 100644 index 0000000000..d96e134369 --- /dev/null +++ b/Examples/UIExplorer/JSNavigationStack/JSNavigationStackExample.js @@ -0,0 +1,99 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var React = require('React'); +var JSNavigationStack = require('JSNavigationStack'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var ScrollView = require('ScrollView'); +var TouchableHighlight = require('TouchableHighlight'); +var BreadcrumbNavSample = require('./BreadcrumbNavSample'); +var NavigationBarSample = require('./NavigationBarSample'); +var JumpingNavSample = require('./JumpingNavSample'); + +class NavMenu extends React.Component { + render() { + return ( + + { + this.props.navigator.push({ id: 'breadcrumbs' }); + }}> + Breadcrumbs Example + + { + this.props.navigator.push({ id: 'navbar' }); + }}> + Navbar Example + + { + this.props.navigator.push({ id: 'jumping' }); + }}> + Jumping Example + + { + this.props.onExampleExit(); + }}> + Exit JSNavigationStack Example + + + ); + } +} + +var TabBarExample = React.createClass({ + + statics: { + title: '', + description: 'JS-implemented navigation', + }, + + renderScene: function(route, nav) { + switch (route.id) { + case 'menu': + return ( + + ); + case 'navbar': + return ; + case 'breadcrumbs': + return ; + case 'jumping': + return ; + } + }, + + render: function() { + return ( + JSNavigationStack.AnimationConfigs.FloatFromBottom} + /> + ); + }, + +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + button: { + backgroundColor: 'white', + padding: 15, + }, + buttonText: { + }, + scene: { + flex: 1, + paddingTop: 64, + } +}); + +module.exports = TabBarExample; diff --git a/Examples/UIExplorer/JSNavigationStack/JumpingNavSample.js b/Examples/UIExplorer/JSNavigationStack/JumpingNavSample.js new file mode 100644 index 0000000000..67c7956d5f --- /dev/null +++ b/Examples/UIExplorer/JSNavigationStack/JumpingNavSample.js @@ -0,0 +1,192 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule JumpingNavSample + */ +'use strict'; + +var JSNavigationStack = require('JSNavigationStack'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var ScrollView = require('ScrollView'); +var Text = require('Text'); +var TouchableBounce = require('TouchableBounce'); +var View = require('View'); + +var _getRandomRoute = function() { + return { + randNumber: Math.random(), + }; +}; + +var INIT_ROUTE = _getRandomRoute(); +var ROUTE_STACK = [ + _getRandomRoute(), + _getRandomRoute(), + INIT_ROUTE, + _getRandomRoute(), + _getRandomRoute(), +]; +var renderScene = function(route, navigator) { + return ( + + + {route.randNumber} + { + navigator.jumpBack(); + }}> + + jumpBack + + + { + navigator.jumpForward(); + }}> + + jumpForward + + + { + navigator.jumpTo(INIT_ROUTE); + }}> + + jumpTo initial route + + + { + navigator.push(_getRandomRoute()); + }}> + + destructive: push + + + { + navigator.replace(_getRandomRoute()); + }}> + + destructive: replace + + + { + navigator.pop(); + }}> + + destructive: pop + + + { + navigator.immediatelyResetRouteStack([ + _getRandomRoute(), + _getRandomRoute(), + ]); + }}> + + destructive: Immediate set two routes + + + { + navigator.popToTop(); + }}> + + destructive: pop to top + + + + + ); +}; + +class JumpingNavBar extends React.Component { + render() { + return ( + + {this.props.routeStack.map((route, index) => ( + { + this.props.navigator.jumpTo(route); + }}> + + + {index} + + + + ))} + + ); + } +} + +var JumpingNavSample = React.createClass({ + + render: function() { + return ( + } + shouldJumpOnBackstackPop={true} + /> + ); + }, + +}); + +var styles = StyleSheet.create({ + scene: { + backgroundColor: '#eeeeee', + }, + scroll: { + flex: 1, + }, + button: { + backgroundColor: '#cccccc', + margin: 50, + marginTop: 26, + padding: 10, + }, + buttonText: { + fontSize: 12, + textAlign: 'center', + }, + appContainer: { + overflow: 'hidden', + backgroundColor: '#dddddd', + flex: 1, + }, + navBar: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: 90, + flexDirection: 'row', + }, + navButton: { + flex: 1, + }, + navButtonText: { + textAlign: 'center', + fontSize: 32, + marginTop: 25, + }, + navButtonActive: { + color: 'green', + }, +}); + +module.exports = JumpingNavSample; diff --git a/Examples/UIExplorer/JSNavigationStack/NavigationBarSample.js b/Examples/UIExplorer/JSNavigationStack/NavigationBarSample.js new file mode 100644 index 0000000000..f1bca6c84f --- /dev/null +++ b/Examples/UIExplorer/JSNavigationStack/NavigationBarSample.js @@ -0,0 +1,118 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule NavigationBarSample + */ +'use strict'; + +var JSNavigationStack = require('JSNavigationStack'); +var NavigationBar = require('NavigationBar'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var TouchableOpacity = require('TouchableOpacity'); +var View = require('View'); + +var cssVar = require('cssVar'); + + +var NavigationBarRouteMapper = { + + LeftButton: function(route, navigator, index, navState) { + if (index === 0) { + return null; + } + + var previousRoute = navState.routeStack[index - 1]; + return ( + navigator.pop()}> + + + {previousRoute.title} + + + + ); + }, + + RightButton: function(route, navigator, index, navState) { + return ( + navigator.push(newRandomRoute())}> + + + Next + + + + ); + }, + + Title: function(route, navigator, index, navState) { + return ( + + {route.title} [{index}] + + ); + }, + +}; + +function newRandomRoute() { + return { + content: 'Hello World!', + title: 'Random ' + Math.round(Math.random() * 100), + }; +} + +var NavigationBarSample = React.createClass({ + + render: function() { + return ( + + ( + + {route.content} + + )} + navigationBar={ + + } + /> + + ); + }, + +}); + +var styles = StyleSheet.create({ + appContainer: { + overflow: 'hidden', + backgroundColor: '#ffffff', + flex: 1, + }, + scene: { + paddingTop: 50, + flex: 1, + }, + navBarText: { + fontSize: 16, + marginVertical: 10, + }, + navBarTitleText: { + color: cssVar('fbui-bluegray-60'), + fontWeight: 'bold', + marginVertical: 9, + }, + navBarButtonText: { + color: cssVar('fbui-accent-blue'), + }, +}); + +module.exports = NavigationBarSample; diff --git a/Examples/UIExplorer/JSNavigationStack/NestedBreadcrumbNavSample.js b/Examples/UIExplorer/JSNavigationStack/NestedBreadcrumbNavSample.js new file mode 100644 index 0000000000..0551e23322 --- /dev/null +++ b/Examples/UIExplorer/JSNavigationStack/NestedBreadcrumbNavSample.js @@ -0,0 +1,211 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule NestedBreadcrumbNavSample + */ +'use strict'; + +var BreadcrumbNavigationBar = require('BreadcrumbNavigationBar'); +var JSNavigationStack = require('JSNavigationStack'); +var React = require('React'); +var ScrollView = require('ScrollView'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var TouchableBounce = require('TouchableBounce'); +var View = require('View'); + +var SAMPLE_TEXT = 'Top Pushes. Middle Replaces. Bottom Pops.'; + +var _getRandomRoute = function() { + return { + backButtonTitle: 'Back' + ('' + 10 * Math.random()).substr(0, 1), + content: + SAMPLE_TEXT + '\nHere\'s a random number ' + Math.random(), + title: 'Pushed!', + rightButtonTitle: Math.random() > 0.5 ? 'Right' : 'Button', + }; +}; + + +var HorizontalNavigationBarRouteMapper = { + rightContentForRoute: function(route, navigator) { + if (route.rightButtonTitle) { + return ( + + {route.rightButtonTitle} + + ); + } else { + return null; + } + }, + titleContentForRoute: function(route, navigator) { + return ( + () => { navigator.push(_getRandomRoute()); }}> + + {route.title} + + + ); + }, + iconForRoute: function(route, navigator) { + var onPress = + navigator.popToRoute.bind(navigator, route); + return ( + + + + ); + }, + separatorForRoute: function(route, navigator) { + return ( + + + + ); + } +}; + +var ThirdDeepRouteMapper = (route, navigator) => ( + + + + { navigator.push(_getRandomRoute()); }}> + + request push soon + + + + + +); + +var SecondDeepRouteMapper = (route, navigator) => ( + + { navigator.push(_getRandomRoute()); }}> + + Push Horizontal + + + + } + /> + +); + +var FirstDeepRouteMapper = (route, navigator) => ( + + { navigator.push(_getRandomRoute()); }}> + + Push Outer Vertical Stack + + + + } + /> + +); + +/** + * The outer component. + */ +var NestedBreadcrumbNavSample = React.createClass({ + render: function() { + var initialRoute = {title: 'Vertical'}; + // No navigation bar. + return ( + JSNavigationStack.AnimationConfigs.FloatFromBottom} + initialRoute={initialRoute} + renderScene={FirstDeepRouteMapper} + /> + ); + } +}); + +var styles = StyleSheet.create({ + navigationItem: { + backgroundColor: '#eeeeee', + shadowColor: 'black', + shadowRadius: 20, + shadowOffset: {w: 0, h: -10}, + }, + paddingForNavBar: { + paddingTop: 60, + }, + paddingForMenuBar: { + paddingTop: 10, + }, + button: { + backgroundColor: '#888888', + margin: 10, + marginTop: 10, + padding: 10, + marginRight: 20, + }, + buttonText: { + fontSize: 12, + textAlign: 'center', + color: 'white', + }, + appContainer: { + overflow: 'hidden', + backgroundColor: '#dddddd', + flex: 1, + }, + titleText: { + fontSize: 18, + color: '#666666', + textAlign: 'center', + fontWeight: 'bold', + lineHeight: 32, + }, + filterText: { + color: '#5577ff', + }, + // TODO: Accept icons from route. + crumbIconPlaceholder: { + flex: 1, + backgroundColor: '#666666', + }, + crumbSeparatorPlaceholder: { + flex: 1, + backgroundColor: '#aaaaaa', + }, + secondDeepNavigator: { + margin: 0, + borderColor: '#666666', + borderWidth: 0.5, + height: 400, + }, + thirdDeepNavigator: { + margin: 0, + borderColor: '#aaaaaa', + borderWidth: 0.5, + height: 400, + }, + thirdDeepScrollContent: { + height: 1000, + } +}); + +module.exports = NestedBreadcrumbNavSample; diff --git a/Examples/UIExplorer/MapViewExample.js b/Examples/UIExplorer/MapViewExample.js index 74c4f65336..22b2475a0d 100644 --- a/Examples/UIExplorer/MapViewExample.js +++ b/Examples/UIExplorer/MapViewExample.js @@ -5,6 +5,8 @@ * 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. + * + * @flow */ 'use strict'; @@ -188,7 +190,7 @@ exports.description = 'Base component to display maps'; exports.examples = [ { title: 'Map', - render() { return ; } + render(): ReactElement { return ; } }, { title: 'Map shows user location', diff --git a/Examples/UIExplorer/ResponderExample.js b/Examples/UIExplorer/ResponderExample.js new file mode 100644 index 0000000000..73398ed1b5 --- /dev/null +++ b/Examples/UIExplorer/ResponderExample.js @@ -0,0 +1,125 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + PanResponder, + View, +} = React; + +var CIRCLE_SIZE = 80; +var CIRCLE_COLOR = 'blue'; +var CIRCLE_HIGHLIGHT_COLOR = 'green'; + + +var NavigatorIOSExample = React.createClass({ + + statics: { + title: 'PanResponder Sample', + description: 'Basic gesture handling example', + }, + + _panResponder: {}, + _previousLeft: 0, + _previousTop: 0, + _circleStyles: {}, + circle: (null : ?React.Element), + + componentWillMount: function() { + this._panResponder = PanResponder.create({ + onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder, + onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, + onPanResponderGrant: this._handlePanResponderGrant, + onPanResponderMove: this._handlePanResponderMove, + onPanResponderRelease: this._handlePanResponderEnd, + onPanResponderTerminate: this._handlePanResponderEnd, + }); + this._previousLeft = 20; + this._previousTop = 84; + this._circleStyles = { + left: this._previousLeft, + top: this._previousTop, + }; + }, + + componentDidMount: function() { + this._updatePosition(); + }, + + render: function() { + return ( + + { + this.circle = circle; + }} + style={styles.circle} + {...this._panResponder.panHandlers} + /> + + ); + }, + + _highlight: function() { + this.circle && this.circle.setNativeProps({ + backgroundColor: CIRCLE_HIGHLIGHT_COLOR + }); + }, + + _unHighlight: function() { + this.circle && this.circle.setNativeProps({ + backgroundColor: CIRCLE_COLOR + }); + }, + + _updatePosition: function() { + this.circle && this.circle.setNativeProps(this._circleStyles); + }, + + _handleStartShouldSetPanResponder: function(e: Object, gestureState: Object): boolean { + // Should we become active when the user presses down on the circle? + return true; + }, + + _handleMoveShouldSetPanResponder: function(e: Object, gestureState: Object): boolean { + // Should we become active when the user moves a touch over the circle? + return true; + }, + + _handlePanResponderGrant: function(e: Object, gestureState: Object) { + this._highlight(); + }, + _handlePanResponderMove: function(e: Object, gestureState: Object) { + this._circleStyles.left = this._previousLeft + gestureState.dx; + this._circleStyles.top = this._previousTop + gestureState.dy; + this._updatePosition(); + }, + _handlePanResponderEnd: function(e: Object, gestureState: Object) { + this._unHighlight(); + this._previousLeft += gestureState.dx; + this._previousTop += gestureState.dy; + }, +}); + +var styles = StyleSheet.create({ + circle: { + width: CIRCLE_SIZE, + height: CIRCLE_SIZE, + borderRadius: CIRCLE_SIZE / 2, + backgroundColor: CIRCLE_COLOR, + position: 'absolute', + left: 0, + top: 0, + }, + container: { + flex: 1, + paddingTop: 64, + }, +}); + +module.exports = NavigatorIOSExample; diff --git a/Examples/UIExplorer/TabBarExample.js b/Examples/UIExplorer/TabBarExample.js index a38255ff4f..793bfd3092 100644 --- a/Examples/UIExplorer/TabBarExample.js +++ b/Examples/UIExplorer/TabBarExample.js @@ -5,6 +5,8 @@ * 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. + * + * @flow */ 'use strict'; @@ -31,7 +33,7 @@ var TabBarExample = React.createClass({ }; }, - _renderContent: function(color, pageText) { + _renderContent: function(color: string, pageText: string) { return ( {pageText} diff --git a/Examples/UIExplorer/TimerExample.js b/Examples/UIExplorer/TimerExample.js index ef05ebf93a..b341f086c8 100644 --- a/Examples/UIExplorer/TimerExample.js +++ b/Examples/UIExplorer/TimerExample.js @@ -15,10 +15,10 @@ var { AlertIOS, StyleSheet, Text, - TimerMixin, TouchableHighlight, View, } = React; +var TimerMixin = require('react-timer-mixin'); var Button = React.createClass({ render: function() { diff --git a/Examples/UIExplorer/UIExplorerApp.js b/Examples/UIExplorer/UIExplorerApp.js index 58b84956b9..82fe1bcff2 100644 --- a/Examples/UIExplorer/UIExplorerApp.js +++ b/Examples/UIExplorer/UIExplorerApp.js @@ -13,23 +13,42 @@ var React = require('react-native'); var UIExplorerList = require('./UIExplorerList'); - var { AppRegistry, NavigatorIOS, StyleSheet, } = React; - var UIExplorerApp = React.createClass({ + getInitialState: function() { + return { + openExternalExample: (null: ?React.Component), + }; + }, + render: function() { + if (this.state.openExternalExample) { + var Example = this.state.openExternalExample; + return ( + { + this.setState({ openExternalExample: null, }); + }} + /> + ); + } return ( { + this.setState({ openExternalExample: example, }); + }, + } }} itemWrapperStyle={styles.itemWrapper} tintColor='#008888' diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index 14de683873..8228589942 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -21,6 +21,7 @@ var { TouchableHighlight, View, } = React; +var JSNavigationStackExample = require('./JSNavigationStack/JSNavigationStackExample'); var createExamplePage = require('./createExamplePage'); @@ -32,6 +33,7 @@ var COMPONENTS = [ require('./ListViewSimpleExample'), require('./MapViewExample'), require('./NavigatorIOSExample'), + JSNavigationStackExample, require('./PickerExample'), require('./ScrollViewExample'), require('./SliderIOSExample'), @@ -57,6 +59,7 @@ var APIS = [ require('./PointerEventsExample'), require('./PushNotificationIOSExample'), require('./StatusBarIOSExample'), + require('./ResponderExample'), require('./TimerExample'), require('./VibrationIOSExample'), ]; @@ -143,6 +146,12 @@ class UIExplorerList extends React.Component { } _onPressRow(example) { + if (example === JSNavigationStackExample) { + this.props.onExternalExampleRequested( + JSNavigationStackExample + ); + return; + } var Component = example.examples ? createExamplePage(null, example) : example; diff --git a/Examples/UIExplorer/createExamplePage.js b/Examples/UIExplorer/createExamplePage.js index 8700512a7c..2bc50b15e9 100644 --- a/Examples/UIExplorer/createExamplePage.js +++ b/Examples/UIExplorer/createExamplePage.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule createExamplePage + * @flow */ 'use strict'; @@ -16,7 +17,19 @@ var UIExplorerPage = require('./UIExplorerPage'); var invariant = require('invariant'); -var createExamplePage = function(title, exampleModule) { +class Example extends React.Component { + title: string; + description: string; +} + +type ExampleModule = { + title: string; + description: string; + examples: Array; +}; + +var createExamplePage = function(title: ?string, exampleModule: ExampleModule) + : ReactClass { invariant(!!exampleModule.examples, 'The module must have examples'); var ExamplePage = React.createClass({ @@ -31,15 +44,17 @@ var createExamplePage = function(title, exampleModule) { var originalRenderComponent = React.renderComponent; var originalRender = React.render; var renderedComponent; - React.render = React.renderComponent = function(element, container) { + // TODO remove typecasts when Flow bug #6560135 is fixed + // and workaround is removed from react-native.js + (React: Object).render = (React: Object).renderComponent = function(element, container) { renderedComponent = element; }; var result = example.render(null); if (result) { renderedComponent = result; } - React.renderComponent = originalRenderComponent; - React.render = originalRender; + (React: Object).renderComponent = originalRenderComponent; + (React: Object).render = originalRender; return ( TestModule.verifySnapshot(this.done)); + }, + + done() { + TestModule.markTestCompleted(); + }, + + render() { + return ( + + + + + ); + } +}); + +var styles = StyleSheet.create({ + box1: { + width: 80, + height: 50, + backgroundColor: 'red', + }, + box2: { + top: -10, + left: 20, + width: 70, + height: 90, + backgroundColor: 'blue', + }, +}); + +module.exports = SimpleSnapshotTest; diff --git a/IntegrationTests/TimersTest.js b/IntegrationTests/TimersTest.js index bee3fe1834..4814d8b1db 100644 --- a/IntegrationTests/TimersTest.js +++ b/IntegrationTests/TimersTest.js @@ -13,9 +13,9 @@ var React = require('react-native'); var { StyleSheet, Text, - TimerMixin, View, } = React; +var TimerMixin = require('react-timer-mixin'); var TimersTest = React.createClass({ mixins: [TimerMixin], diff --git a/Libraries/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js index b84fedf0c7..20150d6f69 100644 --- a/Libraries/ActionSheetIOS/ActionSheetIOS.js +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ActionSheetIOS + * @flow */ 'use strict'; @@ -15,7 +16,7 @@ var RCTActionSheetManager = require('NativeModules').ActionSheetManager; var invariant = require('invariant'); var ActionSheetIOS = { - showActionSheetWithOptions(options, callback) { + showActionSheetWithOptions(options: Object, callback: Function) { invariant( typeof options === 'object' && options !== null, 'Options must a valid object' @@ -31,7 +32,11 @@ var ActionSheetIOS = { ); }, - showShareActionSheetWithOptions(options, failureCallback, successCallback) { + showShareActionSheetWithOptions( + options: Object, + failureCallback: Function, + successCallback: Function + ) { invariant( typeof options === 'object' && options !== null, 'Options must a valid object' diff --git a/Libraries/AdSupport/AdSupportIOS.js b/Libraries/AdSupport/AdSupportIOS.js index 0f2a4c2a6f..d9d315a1df 100644 --- a/Libraries/AdSupport/AdSupportIOS.js +++ b/Libraries/AdSupport/AdSupportIOS.js @@ -7,17 +7,18 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule AdSupportIOS + * @flow */ 'use strict'; var AdSupport = require('NativeModules').AdSupport; module.exports = { - getAdvertisingId: function(onSuccess, onFailure) { + getAdvertisingId: function(onSuccess: Function, onFailure: Function) { AdSupport.getAdvertisingId(onSuccess, onFailure); }, - getAdvertisingTrackingEnabled: function(onSuccess, onFailure) { + getAdvertisingTrackingEnabled: function(onSuccess: Function, onFailure: Function) { AdSupport.getAdvertisingTrackingEnabled(onSuccess, onFailure); }, }; diff --git a/Libraries/Animation/LayoutAnimation.js b/Libraries/Animation/LayoutAnimation.js index 8752e23228..b686569841 100644 --- a/Libraries/Animation/LayoutAnimation.js +++ b/Libraries/Animation/LayoutAnimation.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule LayoutAnimation + * @flow */ 'use strict'; @@ -42,6 +43,15 @@ var animChecker = createStrictShapeTypeChecker({ ), }); +type Anim = { + duration?: number; + delay?: number; + springDamping?: number; + initialVelocity?: number; + type?: $Enum; + property?: $Enum; +} + var configChecker = createStrictShapeTypeChecker({ duration: PropTypes.number.isRequired, create: animChecker, @@ -49,46 +59,56 @@ var configChecker = createStrictShapeTypeChecker({ delete: animChecker, }); +type Config = { + duration: number; + create?: Anim; + update?: Anim; + delete?: Anim; +} + +function configureNext(config: Config, onAnimationDidEnd?: Function, onError?: Function) { + configChecker({config}, 'config', 'LayoutAnimation.configureNext'); + RCTUIManager.configureNextLayoutAnimation(config, onAnimationDidEnd, onError); +} + +function create(duration: number, type, creationProp): Config { + return { + duration, + create: { + type, + property: creationProp, + }, + update: { + type, + }, + }; +} + var LayoutAnimation = { - configureNext(config, onAnimationDidEnd, onError) { - configChecker({config}, 'config', 'LayoutAnimation.configureNext'); - RCTUIManager.configureNextLayoutAnimation(config, onAnimationDidEnd, onError); - }, - create(duration, type, creationProp) { - return { - duration, + configureNext, + create, + Types, + Properties, + configChecker: configChecker, + Presets: { + easeInEaseOut: create( + 0.3, Types.easeInEaseOut, Properties.opacity + ), + linear: create( + 0.5, Types.linear, Properties.opacity + ), + spring: { + duration: 0.7, create: { - type, - property: creationProp, + type: Types.linear, + property: Properties.opacity, }, update: { - type, + type: Types.spring, + springDamping: 0.4, }, - }; - }, - Types: Types, - Properties: Properties, - configChecker: configChecker, -}; - -LayoutAnimation.Presets = { - easeInEaseOut: LayoutAnimation.create( - 0.3, Types.easeInEaseOut, Properties.opacity - ), - linear: LayoutAnimation.create( - 0.5, Types.linear, Properties.opacity - ), - spring: { - duration: 0.7, - create: { - type: Types.linear, - property: Properties.opacity, - }, - update: { - type: Types.spring, - springDamping: 0.4, }, - }, + } }; module.exports = LayoutAnimation; diff --git a/Libraries/Animation/RCTAnimation.xcodeproj/project.pbxproj b/Libraries/Animation/RCTAnimation.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..36b95fba1c --- /dev/null +++ b/Libraries/Animation/RCTAnimation.xcodeproj/project.pbxproj @@ -0,0 +1,256 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 13BE3DEE1AC21097009241FE /* RCTAnimationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* RCTAnimationManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814201AA4EA6300B7C361 /* libRCTAnimation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTAnimation.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 13BE3DEC1AC21097009241FE /* RCTAnimationManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationManager.h; sourceTree = ""; }; + 13BE3DED1AC21097009241FE /* RCTAnimationManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libRCTAnimation.a */, + ); + name = Products; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + 13BE3DEC1AC21097009241FE /* RCTAnimationManager.h */, + 13BE3DED1AC21097009241FE /* RCTAnimationManager.m */, + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* RCTAnimation */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTAnimation" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTAnimation; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libRCTAnimation.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTAnimation" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* RCTAnimation */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13BE3DEE1AC21097009241FE /* RCTAnimationManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "/Users/nicklockwood/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/ReactKit/build/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTAnimation; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "/Users/nicklockwood/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/ReactKit/build/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTAnimation; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTAnimation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTAnimation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/ReactKit/Modules/RCTAnimationManager.h b/Libraries/Animation/RCTAnimationManager.h similarity index 92% rename from ReactKit/Modules/RCTAnimationManager.h rename to Libraries/Animation/RCTAnimationManager.h index 5be9cadc05..1211143da3 100644 --- a/ReactKit/Modules/RCTAnimationManager.h +++ b/Libraries/Animation/RCTAnimationManager.h @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import #import #import "RCTBridgeModule.h" diff --git a/ReactKit/Modules/RCTAnimationManager.m b/Libraries/Animation/RCTAnimationManager.m similarity index 96% rename from ReactKit/Modules/RCTAnimationManager.m rename to Libraries/Animation/RCTAnimationManager.m index 12ad8e2681..21e76977fd 100644 --- a/ReactKit/Modules/RCTAnimationManager.m +++ b/Libraries/Animation/RCTAnimationManager.m @@ -9,7 +9,6 @@ #import "RCTAnimationManager.h" -#import #import #import "RCTSparseArray.h" @@ -54,10 +53,10 @@ - (instancetype)init return ^(CGFloat t) { const CGFloat *delta = deltaData.bytes; - const CGFloat *fromArray = fromData.bytes; + const CGFloat *_fromArray = fromData.bytes; CGFloat value[count]; - CG_APPEND(vDSP_vma,,D)(delta, 1, &t, 0, fromArray, 1, value, 1, count); + CG_APPEND(vDSP_vma,,D)(delta, 1, &t, 0, _fromArray, 1, value, 1, count); return [NSValue valueWithBytes:value objCType:typeName]; }; } @@ -84,10 +83,10 @@ - (void)startAnimationForTag:(NSNumber *)reactTag animationTag:(NSNumber *)anima } else if ([obj respondsToSelector:@selector(count)]) { switch ([obj count]) { case 2: - if ([obj respondsToSelector:@selector(objectForKey:)] && [obj objectForKey:@"w"]) { - toValue = [NSValue valueWithCGSize:[RCTConvert CGSize:obj]]; - } else { + if ([obj respondsToSelector:@selector(objectForKeyedSubscript:)] && obj[@"x"]) { toValue = [NSValue valueWithCGPoint:[RCTConvert CGPoint:obj]]; + } else { + toValue = [NSValue valueWithCGSize:[RCTConvert CGSize:obj]]; } break; case 4: diff --git a/Libraries/AppRegistry/AppRegistry.js b/Libraries/AppRegistry/AppRegistry.js index 40498adacc..f36f881323 100644 --- a/Libraries/AppRegistry/AppRegistry.js +++ b/Libraries/AppRegistry/AppRegistry.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule AppRegistry + * @flow */ 'use strict'; @@ -21,6 +22,12 @@ if (__DEV__) { var runnables = {}; +type AppConfig = { + appKey: string; + component: ReactClass; + run?: Function; +}; + /** * `AppRegistry` is the JS entry point to running all React Native apps. App * root components should register themselves with @@ -33,17 +40,18 @@ var runnables = {}; * `require`d. */ var AppRegistry = { - registerConfig: function(config) { + registerConfig: function(config: Array) { for (var i = 0; i < config.length; ++i) { - if (config[i].run) { - AppRegistry.registerRunnable(config[i].appKey, config[i].run); + var appConfig = config[i]; + if (appConfig.run) { + AppRegistry.registerRunnable(appConfig.appKey, appConfig.run); } else { - AppRegistry.registerComponent(config[i].appKey, config[i].component); + AppRegistry.registerComponent(appConfig.appKey, appConfig.component); } } }, - registerComponent: function(appKey, getComponentFunc) { + registerComponent: function(appKey: string, getComponentFunc: Function): string { runnables[appKey] = { run: (appParameters) => renderApplication(getComponentFunc(), appParameters.initialProps, appParameters.rootTag) @@ -51,12 +59,12 @@ var AppRegistry = { return appKey; }, - registerRunnable: function(appKey, func) { + registerRunnable: function(appKey: string, func: Function): string { runnables[appKey] = {run: func}; return appKey; }, - runApplication: function(appKey, appParameters) { + runApplication: function(appKey: string, appParameters: any): void { console.log( 'Running application "' + appKey + '" with appParams: ' + JSON.stringify(appParameters) + '. ' + diff --git a/Libraries/AppStateIOS/AppStateIOS.android.js b/Libraries/AppStateIOS/AppStateIOS.android.js index 9fbad761e6..b8c3e8def8 100644 --- a/Libraries/AppStateIOS/AppStateIOS.android.js +++ b/Libraries/AppStateIOS/AppStateIOS.android.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule AppStateIOS + * @flow */ 'use strict'; diff --git a/Libraries/AppStateIOS/AppStateIOS.ios.js b/Libraries/AppStateIOS/AppStateIOS.ios.js index ecb2ee49e4..af68935347 100644 --- a/Libraries/AppStateIOS/AppStateIOS.ios.js +++ b/Libraries/AppStateIOS/AppStateIOS.ios.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule AppStateIOS + * @flow */ 'use strict'; diff --git a/Libraries/Components/TextInput/TextInput.ios.js b/Libraries/Components/TextInput/TextInput.ios.js index 947bc0a4a8..6affc40422 100644 --- a/Libraries/Components/TextInput/TextInput.ios.js +++ b/Libraries/Components/TextInput/TextInput.ios.js @@ -21,7 +21,7 @@ var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); var Text = require('Text'); var TextInputState = require('TextInputState'); -var TimerMixin = require('TimerMixin'); +var TimerMixin = require('react-timer-mixin'); var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index 981208577c..6feaef0997 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -14,7 +14,7 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); -var TimerMixin = require('TimerMixin'); +var TimerMixin = require('react-timer-mixin'); var Touchable = require('Touchable'); var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var View = require('View'); diff --git a/Libraries/CustomComponents/JSNavigationStack.js b/Libraries/CustomComponents/JSNavigationStack.js new file mode 100644 index 0000000000..7f7936c6ca --- /dev/null +++ b/Libraries/CustomComponents/JSNavigationStack.js @@ -0,0 +1,860 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule JSNavigationStack + */ + +"use strict" + +var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule; +var Backstack = require('Backstack'); +var Dimensions = require('Dimensions'); +var InteractionMixin = require('InteractionMixin'); +var JSNavigationStackAnimationConfigs = require('JSNavigationStackAnimationConfigs'); +var PanResponder = require('PanResponder'); +var React = require('React'); +var StaticContainer = require('StaticContainer.react'); +var StyleSheet = require('StyleSheet'); +var Subscribable = require('Subscribable'); +var TimerMixin = require('react-timer-mixin'); +var View = require('View'); + +var clamp = require('clamp'); +var invariant = require('invariant'); +var keyMirror = require('keyMirror'); +var merge = require('merge'); +var rebound = require('rebound'); + +var PropTypes = React.PropTypes; + +var SCREEN_WIDTH = Dimensions.get('window').width; +var SCREEN_HEIGHT = Dimensions.get('window').height; + +var OFF_SCREEN = {style: {opacity: 0}}; + +var NAVIGATION_BAR_REF = 'navigationBar_ref'; + +var __uid = 0; +function getuid() { + return __uid++; +} + +var nextComponentUid = 0; + +// styles moved to the top of the file so getDefaultProps can refer to it +var styles = StyleSheet.create({ + container: { + flex: 1, + overflow: 'hidden', + }, + defaultSceneStyle: { + width: SCREEN_WIDTH, + height: SCREEN_HEIGHT, + }, + presentNavItem: { + position: 'absolute', + overflow: 'hidden', + left: 0, + right: 0, + bottom: 0, + top: 0, + }, + futureNavItem: { + overflow: 'hidden', + position: 'absolute', + left: 0, + opacity: 0, + }, + transitioner: { + flex: 1, + backgroundColor: '#555555', + overflow: 'hidden', + } +}); + +var JSNavigationStack = React.createClass({ + + propTypes: { + animationConfigRouteMapper: PropTypes.func, + // Returns the rendered scene to display when invoked with (route, navigator) + renderScene: PropTypes.func.isRequired, + initialRoute: PropTypes.object, + initialRouteStack: PropTypes.arrayOf(PropTypes.object), + // Will emit the target route on mounting and before each nav transition, + // overriding the handler in this.props.navigator + onWillFocus: PropTypes.func, + // Will emit the new route after mounting and after each nav transition, + // overriding the handler in this.props.navigator + onDidFocus: PropTypes.func, + // Will be called with (ref, indexInStack) when an item ref resolves + onItemRef: PropTypes.func, + // Define the component to use for the nav bar, which will get navState and navigator props + navigationBar: PropTypes.node, + // The navigator object from a parent JSNavigationStack + navigator: PropTypes.object, + + /** + * Should the backstack back button "jump" back instead of pop? Set to true + * if a jump forward might happen after the android back button is pressed, + * so the scenes will remain mounted + */ + shouldJumpOnBackstackPop: PropTypes.bool, + }, + + statics: { + AnimationConfigs: JSNavigationStackAnimationConfigs, + }, + + mixins: [TimerMixin, InteractionMixin, Subscribable.Mixin], + + getDefaultProps: function() { + return { + animationConfigRouteMapper: () => JSNavigationStackAnimationConfigs.PushFromRight, + sceneStyle: styles.defaultSceneStyle, + }; + }, + + getInitialState: function() { + var routeStack = this.props.initialRouteStack || []; + var initialRouteIndex = 0; + if (this.props.initialRoute && routeStack.length) { + initialRouteIndex = routeStack.indexOf(this.props.initialRoute); + invariant( + initialRouteIndex !== -1, + 'initialRoute is not in initialRouteStack.' + ); + } else if (this.props.initialRoute) { + routeStack = [this.props.initialRoute]; + } else { + invariant( + routeStack.length >= 1, + 'JSNavigationStack requires props.initialRoute or props.initialRouteStack.' + ); + } + return { + animationConfigStack: routeStack.map( + (route) => this.props.animationConfigRouteMapper(route) + ), + idStack: routeStack.map(() => getuid()), + routeStack, + // These are tracked to avoid rendering everything all the time. + updatingRangeStart: initialRouteIndex, + updatingRangeLength: initialRouteIndex + 1, + // Either animating or gesturing. + isAnimating: false, + jumpToIndex: routeStack.length - 1, + presentedIndex: initialRouteIndex, + isResponderOnlyToBlockTouches: false, + fromIndex: initialRouteIndex, + toIndex: initialRouteIndex, + }; + }, + + componentWillMount: function() { + this.memoizedNavigationOperations = { + jumpBack: this.jumpBack, + jumpForward: this.jumpForward, + jumpTo: this.jumpTo, + push: this.push, + pop: this.pop, + replace: this.replace, + replaceAtIndex: this.replaceAtIndex, + replacePrevious: this.replacePrevious, + replacePreviousAndPop: this.replacePreviousAndPop, + immediatelyResetRouteStack: this.immediatelyResetRouteStack, + resetTo: this.resetTo, + popToRoute: this.popToRoute, + popToTop: this.popToTop, + parentNavigator: this.props.navigator, + // We want to bubble focused routes to the top navigation stack. If we are + // a child, this will allow us to call this.props.navigator.on*Focus + onWillFocus: this.props.onWillFocus, + onDidFocus: this.props.onDidFocus, + }; + + this.panGesture = PanResponder.create({ + onStartShouldSetPanResponderCapture: this._handleStartShouldSetPanResponderCapture, + onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, + onPanResponderGrant: this._handlePanResponderGrant, + onPanResponderRelease: this._handlePanResponderRelease, + onPanResponderMove: this._handlePanResponderMove, + onPanResponderTerminate: this._handlePanResponderTerminate, + }); + this._itemRefs = {}; + this._interactionHandle = null; + this._backstackComponentKey = 'jsnavstack' + nextComponentUid; + nextComponentUid++; + + Backstack.eventEmitter && this.addListenerOn( + Backstack.eventEmitter, + 'popNavigation', + this._onBackstackPopState); + + this._emitWillFocus(this.state.presentedIndex); + }, + + _configureSpring: function(animationConfig) { + var config = this.spring.getSpringConfig(); + config.friction = animationConfig.springFriction; + config.tension = animationConfig.springTension; + }, + + componentDidMount: function() { + this.springSystem = new rebound.SpringSystem(); + this.spring = this.springSystem.createSpring(); + this.spring.setRestSpeedThreshold(0.05); + var animationConfig = this.state.animationConfigStack[this.state.presentedIndex]; + animationConfig && this._configureSpring(animationConfig); + this.spring.addListener(this); + this.onSpringUpdate(); + + // Fill up the Backstack with routes that have been provided in + // initialRouteStack + this._fillBackstackRange(0, this.state.routeStack.indexOf(this.props.initialRoute)); + this._emitDidFocus(this.state.presentedIndex); + }, + + componentWillUnmount: function() { + Backstack.removeComponentHistory(this._backstackComponentKey); + }, + + _onBackstackPopState: function(componentKey, stateKey, state) { + if (componentKey !== this._backstackComponentKey) { + return; + } + if (!this._canNavigate()) { + // A bit hacky: if we can't actually handle the pop, just push it back on the stack + Backstack.pushNavigation(componentKey, stateKey, state); + } else { + if (this.props.shouldJumpOnBackstackPop) { + this._jumpToWithoutBackstack(state.fromRoute); + } else { + this._popToRouteWithoutBackstack(state.fromRoute); + } + } + }, + + /** + * @param {RouteStack} nextRouteStack Next route stack to reinitialize. This + * doesn't accept stack item `id`s, which implies that all existing items are + * destroyed, and then potentially recreated according to `routeStack`. Does + * not animate, immediately replaces and rerenders navigation bar and stack + * items. + */ + immediatelyResetRouteStack: function(nextRouteStack) { + var destIndex = nextRouteStack.length - 1; + this.setState({ + idStack: nextRouteStack.map(getuid), + routeStack: nextRouteStack, + animationConfigStack: nextRouteStack.map( + this.props.animationConfigRouteMapper + ), + updatingRangeStart: 0, + updatingRangeLength: nextRouteStack.length, + presentedIndex: destIndex, + jumpToIndex: destIndex, + toIndex: destIndex, + fromIndex: destIndex, + }, () => { + this.onSpringUpdate(); + }); + }, + + /** + * TODO: Accept callback for spring completion. + */ + _requestTransitionTo: function(topOfStack) { + if (topOfStack !== this.state.presentedIndex) { + invariant(!this.state.isAnimating, 'Cannot navigate while transitioning'); + this.state.fromIndex = this.state.presentedIndex; + this.state.toIndex = topOfStack; + this.spring.setOvershootClampingEnabled(false); + if (AnimationsDebugModule) { + AnimationsDebugModule.startRecordingFps(); + } + this._transitionToToIndexWithVelocity( + this.state.animationConfigStack[this.state.fromIndex].defaultTransitionVelocity + ); + } + }, + + /** + * `onSpring*` spring delegate. Wired up via `spring.addListener(this)` + */ + onSpringEndStateChange: function() { + if (!this._interactionHandle) { + this._interactionHandle = this.createInteractionHandle(); + } + }, + + onSpringUpdate: function() { + this._transitionBetween( + this.state.fromIndex, + this.state.toIndex, + this.spring.getCurrentValue() + ); + }, + + onSpringAtRest: function() { + this.state.isAnimating = false; + this._completeTransition(); + this.spring.setCurrentValue(0).setAtRest(); + if (this._interactionHandle) { + this.clearInteractionHandle(this._interactionHandle); + this._interactionHandle = null; + } + }, + + _completeTransition: function() { + if (this.spring.getCurrentValue() === 1) { + var presentedIndex = this.state.toIndex; + this.state.fromIndex = presentedIndex; + this.state.presentedIndex = presentedIndex; + this._emitDidFocus(presentedIndex); + this._removePoppedRoutes(); + if (AnimationsDebugModule) { + AnimationsDebugModule.stopRecordingFps(); + } + this._hideOtherScenes(presentedIndex); + } + }, + + _transitionToToIndexWithVelocity: function(v) { + this._configureSpring( + // For visual consistency, the from index is always used to configure the spring + this.state.animationConfigStack[this.state.fromIndex] + ); + this.state.isAnimating = true; + this.spring.setVelocity(v); + this.spring.setEndValue(1); + this._emitWillFocus(this.state.toIndex); + }, + + _transitionToFromIndexWithVelocity: function(v) { + this._configureSpring( + this.state.animationConfigStack[this.state.fromIndex] + ); + this.state.isAnimating = true; + this.spring.setVelocity(v); + this.spring.setEndValue(0); + }, + + _emitDidFocus: function(index) { + var route = this.state.routeStack[index]; + if (this.props.onDidFocus) { + this.props.onDidFocus(route); + } else if (this.props.navigator && this.props.navigator.onDidFocus) { + this.props.navigator.onDidFocus(route); + } + }, + + _emitWillFocus: function(index) { + var route = this.state.routeStack[index]; + if (this.props.onWillFocus) { + this.props.onWillFocus(route); + } else if (this.props.navigator && this.props.navigator.onWillFocus) { + this.props.navigator.onWillFocus(route); + } + }, + + /** + * Does not delete the scenes - merely hides them. + */ + _hideOtherScenes: function(activeIndex) { + for (var i = 0; i < this.state.routeStack.length; i++) { + if (i === activeIndex) { + continue; + } + var sceneRef = 'scene_' + i; + this.refs[sceneRef] && + this.refs['scene_' + i].setNativeProps(OFF_SCREEN); + } + }, + + /** + * Becomes the responder on touch start (capture) while animating so that it + * blocks all touch interactions inside of it. However, this responder lock + * means nothing more than that. We record if the sole reason for being + * responder is to block interactions (`isResponderOnlyToBlockTouches`). + */ + _handleStartShouldSetPanResponderCapture: function(e, gestureState) { + return this.state.isAnimating; + }, + + _handleMoveShouldSetPanResponder: function(e, gestureState) { + var currentRoute = this.state.routeStack[this.state.presentedIndex]; + var animationConfig = this.state.animationConfigStack[this.state.presentedIndex]; + if (!animationConfig.enableGestures) { + return false; + } + var currentLoc = animationConfig.isVertical ? gestureState.moveY : gestureState.moveX; + var travelDist = animationConfig.isVertical ? gestureState.dy : gestureState.dx; + var oppositeAxisTravelDist = + animationConfig.isVertical ? gestureState.dx : gestureState.dy; + var moveStartedInRegion = currentLoc < animationConfig.edgeHitWidth; + var moveTravelledFarEnough = + travelDist >= animationConfig.gestureDetectMovement && + travelDist > oppositeAxisTravelDist * animationConfig.directionRatio; + return ( + !this.state.isResponderOnlyToBlockTouches && + moveStartedInRegion && + !this.state.isAnimating && + this.state.presentedIndex > 0 && + moveTravelledFarEnough + ); + }, + + _handlePanResponderGrant: function(e, gestureState) { + this.state.isResponderOnlyToBlockTouches = this.state.isAnimating; + if (!this.state.isAnimating) { + this.state.fromIndex = this.state.presentedIndex; + this.state.toIndex = this.state.presentedIndex - 1; + } + }, + + _handlePanResponderRelease: function(e, gestureState) { + if (this.state.isResponderOnlyToBlockTouches) { + this.state.isResponderOnlyToBlockTouches = false; + return; + } + var animationConfig = this.state.animationConfigStack[this.state.presentedIndex]; + var velocity = animationConfig.isVertical ? gestureState.vy : gestureState.vx; + // It's not the real location. There is no *real* location - that's the + // point of the pan gesture. + var pseudoLocation = animationConfig.isVertical ? + gestureState.y0 + gestureState.dy : + gestureState.x0 + gestureState.dx; + var still = Math.abs(velocity) < animationConfig.notMoving; + if (this.spring.getCurrentValue() === 0) { + this.spring.setCurrentValue(0).setAtRest(); + this._completeTransition(); + return; + } + var transitionVelocity = + still && animationConfig.pastPointOfNoReturn(pseudoLocation) ? animationConfig.snapVelocity : + still && !animationConfig.pastPointOfNoReturn(pseudoLocation) ? -animationConfig.snapVelocity : + clamp(-10, velocity, 10); // What are Rebound UoM? + + this.spring.setOvershootClampingEnabled(true); + if (transitionVelocity < 0) { + this._transitionToFromIndexWithVelocity(transitionVelocity); + } else { + this._manuallyPopBackstack(1); + this._transitionToToIndexWithVelocity(transitionVelocity); + } + }, + + _handlePanResponderTerminate: function(e, gestureState) { + this.state.isResponderOnlyToBlockTouches = false; + this._transitionToFromIndexWithVelocity(0); + }, + + _handlePanResponderMove: function(e, gestureState) { + if (!this.state.isResponderOnlyToBlockTouches) { + var animationConfig = this.state.animationConfigStack[this.state.presentedIndex]; + var distance = animationConfig.isVertical ? gestureState.dy : gestureState.dx; + var gestureDetectMovement = animationConfig.gestureDetectMovement; + var nextProgress = (distance - gestureDetectMovement) / + (animationConfig.screenDimension - gestureDetectMovement); + this.spring.setCurrentValue(clamp(0, nextProgress, 1)); + } + }, + + _transitionSceneStyle: function(fromIndex, toIndex, progress, index) { + var viewAtIndex = this.refs['scene_' + index]; + if (viewAtIndex === null || viewAtIndex === undefined) { + return; + } + // Use toIndex animation when we move forwards. Use fromIndex when we move back + var animationIndex = this.state.presentedIndex < toIndex ? toIndex : fromIndex; + var animationConfig = this.state.animationConfigStack[animationIndex]; + var styleToUse = {}; + var useFn = index < fromIndex || index < toIndex ? + animationConfig.interpolators.out : + animationConfig.interpolators.into; + var directionAdjustedProgress = fromIndex < toIndex ? progress : 1 - progress; + var didChange = useFn(styleToUse, directionAdjustedProgress); + if (didChange) { + viewAtIndex.setNativeProps({style: styleToUse}); + } + }, + + _transitionBetween: function(fromIndex, toIndex, progress) { + this._transitionSceneStyle(fromIndex, toIndex, progress, fromIndex); + this._transitionSceneStyle(fromIndex, toIndex, progress, toIndex); + var navBar = this.refs[NAVIGATION_BAR_REF]; + if (navBar && navBar.updateProgress) { + navBar.updateProgress(progress, fromIndex, toIndex); + } + }, + + _handleResponderTerminationRequest: function() { + return false; + }, + + _resetUpdatingRange: function() { + this.state.updatingRangeStart = 0; + this.state.updatingRangeLength = this.state.routeStack.length; + }, + + _canNavigate: function() { + return !this.state.isAnimating; + }, + + _jumpNWithoutBackstack: function(n) { + var destIndex = this._getDestIndexWithinBounds(n); + if (!this._canNavigate()) { + return; // It's busy animating or transitioning. + } + var requestTransitionAndResetUpdatingRange = () => { + this._requestTransitionTo(destIndex); + this._resetUpdatingRange(); + }; + this.setState({ + updatingRangeStart: destIndex, + updatingRangeLength: 1, + toIndex: destIndex, + }, requestTransitionAndResetUpdatingRange); + }, + + _fillBackstackRange: function(start, end) { + invariant( + start <= end, + 'Can only fill the backstack forward. Provide end index greater than start' + ); + for (var i = 0; i < (end - start); i++) { + var fromIndex = start + i; + var toIndex = start + i + 1; + Backstack.pushNavigation( + this._backstackComponentKey, + toIndex, + { + fromRoute: this.state.routeStack[fromIndex], + toRoute: this.state.routeStack[toIndex], + } + ); + } + }, + + _getDestIndexWithinBounds: function(n) { + var currentIndex = this.state.presentedIndex; + var destIndex = currentIndex + n; + invariant( + destIndex >= 0, + 'Cannot jump before the first route.' + ); + var maxIndex = this.state.routeStack.length - 1; + invariant( + maxIndex >= destIndex, + 'Cannot jump past the last route.' + ); + return destIndex; + }, + + _jumpN: function(n) { + var currentIndex = this.state.presentedIndex; + if (!this._canNavigate()) { + return; // It's busy animating or transitioning. + } + if (n > 0) { + this._fillBackstackRange(currentIndex, currentIndex + n); + } else { + var landingBeforeIndex = currentIndex + n + 1; + Backstack.resetToBefore( + this._backstackComponentKey, + landingBeforeIndex + ); + } + this._jumpNWithoutBackstack(n); + }, + + jumpTo: function(route) { + var destIndex = this.state.routeStack.indexOf(route); + invariant( + destIndex !== -1, + 'Cannot jump to route that is not in the route stack' + ); + this._jumpN(destIndex - this.state.presentedIndex); + }, + + _jumpToWithoutBackstack: function(route) { + var destIndex = this.state.routeStack.indexOf(route); + invariant( + destIndex !== -1, + 'Cannot jump to route that is not in the route stack' + ); + this._jumpNWithoutBackstack(destIndex - this.state.presentedIndex); + }, + + jumpForward: function() { + this._jumpN(1); + }, + + jumpBack: function() { + this._jumpN(-1); + }, + + push: function(route) { + invariant(!!route, 'Must supply route to push'); + if (!this._canNavigate()) { + return; // It's busy animating or transitioning. + } + var activeLength = this.state.presentedIndex + 1; + var activeStack = this.state.routeStack.slice(0, activeLength); + var activeIDStack = this.state.idStack.slice(0, activeLength); + var activeAnimationConfigStack = this.state.animationConfigStack.slice(0, activeLength); + var nextStack = activeStack.concat([route]); + var nextIDStack = activeIDStack.concat([getuid()]); + var nextAnimationConfigStack = activeAnimationConfigStack.concat([ + this.props.animationConfigRouteMapper(route), + ]); + var requestTransitionAndResetUpdatingRange = () => { + this._requestTransitionTo(nextStack.length - 1); + this._resetUpdatingRange(); + }; + var navigationState = { + toRoute: route, + fromRoute: this.state.routeStack[this.state.routeStack.length - 1], + }; + Backstack.pushNavigation( + this._backstackComponentKey, + this.state.routeStack.length, + navigationState); + + this.setState({ + idStack: nextIDStack, + routeStack: nextStack, + animationConfigStack: nextAnimationConfigStack, + jumpToIndex: nextStack.length - 1, + updatingRangeStart: nextStack.length - 1, + updatingRangeLength: 1, + }, requestTransitionAndResetUpdatingRange); + }, + + _manuallyPopBackstack: function(n) { + Backstack.resetToBefore(this._backstackComponentKey, this.state.routeStack.length - n); + }, + + /** + * Like popN, but doesn't also update the Backstack. + */ + _popNWithoutBackstack: function(n) { + if (n === 0 || !this._canNavigate()) { + return; + } + invariant( + this.state.presentedIndex - n >= 0, + 'Cannot pop below zero' + ); + this.state.jumpToIndex = this.state.presentedIndex - n; + this._requestTransitionTo( + this.state.presentedIndex - n + ); + }, + + popN: function(n) { + if (n === 0 || !this._canNavigate()) { + return; + } + this._popNWithoutBackstack(n); + this._manuallyPopBackstack(n); + }, + + pop: function() { + if (this.props.navigator && this.state.routeStack.length === 1) { + return this.props.navigator.pop(); + } + this.popN(1); + }, + + /** + * Replace a route in the navigation stack. + * + * `index` specifies the route in the stack that should be replaced. + * If it's negative, it counts from the back. + */ + replaceAtIndex: function(route, index) { + invariant(!!route, 'Must supply route to replace'); + if (index < 0) { + index += this.state.routeStack.length; + } + + if (this.state.routeStack.length <= index) { + return; + } + + // I don't believe we need to lock for a replace since there's no + // navigation actually happening + var nextIDStack = this.state.idStack.slice(); + var nextRouteStack = this.state.routeStack.slice(); + var nextAnimationModeStack = this.state.animationConfigStack.slice(); + nextIDStack[index] = getuid(); + nextRouteStack[index] = route; + nextAnimationModeStack[index] = this.props.animationConfigRouteMapper(route); + + this.setState({ + idStack: nextIDStack, + routeStack: nextRouteStack, + animationConfigStack: nextAnimationModeStack, + updatingRangeStart: index, + updatingRangeLength: 1, + }, () => { + this._resetUpdatingRange(); + if (index === this.state.presentedIndex) { + this._emitWillFocus(this.state.presentedIndex); + this._emitDidFocus(this.state.presentedIndex); + } + }); + }, + + /** + * Replaces the current scene in the stack. + */ + replace: function(route) { + this.replaceAtIndex(route, this.state.presentedIndex); + }, + + /** + * Replace the current route's parent. + */ + replacePrevious: function(route) { + this.replaceAtIndex(route, this.state.presentedIndex - 1); + }, + + popToTop: function() { + this.popToRoute(this.state.routeStack[0]); + }, + + _getNumToPopForRoute: function(route) { + var indexOfRoute = this.state.routeStack.indexOf(route); + invariant( + indexOfRoute !== -1, + 'Calling pop to route for a route that doesn\'t exist!' + ); + return this.state.routeStack.length - indexOfRoute - 1; + }, + + /** + * Like popToRoute, but doesn't update the Backstack, presumably because it's already up to date. + */ + _popToRouteWithoutBackstack: function(route) { + var numToPop = this._getNumToPopForRoute(route); + this._popNWithoutBackstack(numToPop); + }, + + popToRoute: function(route) { + var numToPop = this._getNumToPopForRoute(route); + this.popN(numToPop); + }, + + replacePreviousAndPop: function(route) { + if (this.state.routeStack.length < 2 || !this._canNavigate()) { + return; + } + this.replacePrevious(route); + this.pop(); + }, + + resetTo: function(route) { + invariant(!!route, 'Must supply route to push'); + if (this._canNavigate()) { + this.replaceAtIndex(route, 0); + this.popToRoute(route); + } + }, + + _onItemRef: function(itemId, ref) { + this._itemRefs[itemId] = ref; + var itemIndex = this.state.idStack.indexOf(itemId); + if (itemIndex === -1) { + return; + } + this.props.onItemRef && this.props.onItemRef(ref, itemIndex); + }, + + _removePoppedRoutes: function() { + var newStackLength = this.state.jumpToIndex + 1; + // Remove any unneeded rendered routes. + if (newStackLength < this.state.routeStack.length) { + var updatingRangeStart = newStackLength; // One past the top + var updatingRangeLength = this.state.routeStack.length - newStackLength + 1; + this.state.idStack.slice(newStackLength).map((removingId) => { + this._itemRefs[removingId] = null; + }); + this.setState({ + updatingRangeStart: updatingRangeStart, + updatingRangeLength: updatingRangeLength, + animationConfigStack: this.state.animationConfigStack.slice(0, newStackLength), + idStack: this.state.idStack.slice(0, newStackLength), + routeStack: this.state.routeStack.slice(0, newStackLength), + }, this._resetUpdatingRange); + } + }, + + _routeToOptimizedStackItem: function(route, i) { + var shouldUpdateChild = + this.state.updatingRangeLength !== 0 && + i >= this.state.updatingRangeStart && + i <= this.state.updatingRangeStart + this.state.updatingRangeLength; + var child = this.props.renderScene( + route, + this.memoizedNavigationOperations, + this._onItemRef.bind(null, this.state.idStack[i]) + ); + + var initialSceneStyle = + i === this.state.presentedIndex ? styles.presentNavItem : styles.futureNavItem; + return ( + + + {child} + + + ); + }, + + renderNavigationStackItems: function() { + var shouldRecurseToNavigator = this.state.updatingRangeLength !== 0; + // If not recursing update to navigator at all, may as well avoid + // computation of navigator children. + var items = shouldRecurseToNavigator ? + this.state.routeStack.map(this._routeToOptimizedStackItem) : null; + + return ( + + + {items} + + + ); + }, + + renderNavigationStackBar: function() { + var NavigationBarClass = this.props.NavigationBarClass; + if (!this.props.navigationBar) { + return null; + } + return React.cloneElement(this.props.navigationBar, { + ref: NAVIGATION_BAR_REF, + navigator: this.memoizedNavigationOperations, + navState: this.state, + }); + }, + + render: function() { + return ( + + {this.renderNavigationStackItems()} + {this.renderNavigationStackBar()} + + ); + }, +}); + +module.exports = JSNavigationStack; diff --git a/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBar.js b/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBar.js new file mode 100644 index 0000000000..3f16e44cd4 --- /dev/null +++ b/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBar.js @@ -0,0 +1,241 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule BreadcrumbNavigationBar + */ +'use strict'; + +var BreadcrumbNavigationBarStyles = require('BreadcrumbNavigationBarStyles'); +var PixelRatio = require('PixelRatio'); +var React = require('React'); +var NavigationBarStyles = require('NavigationBarStyles'); +var StaticContainer = require('StaticContainer.react'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var Interpolators = BreadcrumbNavigationBarStyles.Interpolators; +var PropTypes = React.PropTypes; + +/** + * Reusable props objects. + */ +var CRUMB_PROPS = Interpolators.map(() => {return {style: {}};}); +var ICON_PROPS = Interpolators.map(() => {return {style: {}};}); +var SEPARATOR_PROPS = Interpolators.map(() => {return {style: {}};}); +var TITLE_PROPS = Interpolators.map(() => {return {style: {}};}); +var RIGHT_BUTTON_PROPS = Interpolators.map(() => {return {style: {}};}); + + +/** + * TODO: Rename `observedTopOfStack` to `presentedIndex` in `NavigationStack`. + */ +var navStatePresentedIndex = function(navState) { + if (navState.presentedIndex !== undefined) { + return navState.presentedIndex; + } else { + return navState.observedTopOfStack; + } +}; + + +/** + * The first route is initially rendered using a different style than all + * future routes. + * + * @param {number} index Index of breadcrumb. + * @return {object} Style config for initial rendering of index. + */ +var initStyle = function(index, presentedIndex) { + return index === presentedIndex ? BreadcrumbNavigationBarStyles.Center[index] : + index < presentedIndex ? BreadcrumbNavigationBarStyles.Left[index] : + BreadcrumbNavigationBarStyles.Right[index]; +}; + +var BreadcrumbNavigationBar = React.createClass({ + propTypes: { + navigator: PropTypes.shape({ + push: PropTypes.func, + pop: PropTypes.func, + replace: PropTypes.func, + popToRoute: PropTypes.func, + popToTop: PropTypes.func, + }), + navigationBarRouteMapper: PropTypes.shape({ + rightContentForRoute: PropTypes.func, + titleContentForRoute: PropTypes.func, + iconForRoute: PropTypes.func, + }), + navigationBarStyles: PropTypes.number, + }, + + _updateIndexProgress: function(progress, index, fromIndex, toIndex) { + var amount = toIndex > fromIndex ? progress : (1 - progress); + var oldDistToCenter = index - fromIndex; + var newDistToCenter = index - toIndex; + var interpolate; + if (oldDistToCenter > 0 && newDistToCenter === 0 || + newDistToCenter > 0 && oldDistToCenter === 0) { + interpolate = Interpolators[index].RightToCenter; + } else if (oldDistToCenter < 0 && newDistToCenter === 0 || + newDistToCenter < 0 && oldDistToCenter === 0) { + interpolate = Interpolators[index].CenterToLeft; + } else if (oldDistToCenter === newDistToCenter) { + interpolate = Interpolators[index].RightToCenter; + } else { + interpolate = Interpolators[index].RightToLeft; + } + + if (interpolate.Crumb(CRUMB_PROPS[index].style, amount)) { + this.refs['crumb_' + index].setNativeProps(CRUMB_PROPS[index]); + } + if (interpolate.Icon(ICON_PROPS[index].style, amount)) { + this.refs['icon_' + index].setNativeProps(ICON_PROPS[index]); + } + if (interpolate.Separator(SEPARATOR_PROPS[index].style, amount)) { + this.refs['separator_' + index].setNativeProps(SEPARATOR_PROPS[index]); + } + if (interpolate.Title(TITLE_PROPS[index].style, amount)) { + this.refs['title_' + index].setNativeProps(TITLE_PROPS[index]); + } + var right = this.refs['right_' + index]; + if (right && + interpolate.RightItem(RIGHT_BUTTON_PROPS[index].style, amount)) { + right.setNativeProps(RIGHT_BUTTON_PROPS[index]); + } + }, + + updateProgress: function(progress, fromIndex, toIndex) { + var max = Math.max(fromIndex, toIndex); + var min = Math.min(fromIndex, toIndex); + for (var index = min; index <= max; index++) { + this._updateIndexProgress(progress, index, fromIndex, toIndex); + } + }, + + render: function() { + var navState = this.props.navState; + var icons = navState && navState.routeStack.map(this._renderOrReturnBreadcrumb); + var titles = navState.routeStack.map(this._renderOrReturnTitle); + var buttons = navState.routeStack.map(this._renderOrReturnRightButton); + return ( + + {titles} + {icons} + {buttons} + + ); + }, + + _renderOrReturnBreadcrumb: function(route, index) { + var uid = this.props.navState.idStack[index]; + var navBarRouteMapper = this.props.navigationBarRouteMapper; + var navOps = this.props.navigator; + var alreadyRendered = this.refs['crumbContainer' + uid]; + if (alreadyRendered) { + // Don't bother re-calculating the children + return ( + + ); + } + var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState)); + return ( + + + + {navBarRouteMapper.iconForRoute(route, navOps)} + + + {navBarRouteMapper.separatorForRoute(route, navOps)} + + + + ); + }, + + _renderOrReturnTitle: function(route, index) { + var navState = this.props.navState; + var uid = navState.idStack[index]; + var alreadyRendered = this.refs['titleContainer' + uid]; + if (alreadyRendered) { + // Don't bother re-calculating the children + return ( + + ); + } + var navBarRouteMapper = this.props.navigationBarRouteMapper; + var titleContent = navBarRouteMapper.titleContentForRoute( + navState.routeStack[index], + this.props.navigator + ); + var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState)); + return ( + + + {titleContent} + + + ); + }, + + _renderOrReturnRightButton: function(route, index) { + var navState = this.props.navState; + var navBarRouteMapper = this.props.navigationBarRouteMapper; + var uid = navState.idStack[index]; + var alreadyRendered = this.refs['rightContainer' + uid]; + if (alreadyRendered) { + // Don't bother re-calculating the children + return ( + + ); + } + var rightContent = navBarRouteMapper.rightContentForRoute( + navState.routeStack[index], + this.props.navigator + ); + if (!rightContent) { + return null; + } + var firstStyles = initStyle(index, navStatePresentedIndex(this.props.navState)); + return ( + + + {rightContent} + + + ); + }, +}); + +var styles = StyleSheet.create({ + breadCrumbContainer: { + overflow: 'hidden', + position: 'absolute', + height: NavigationBarStyles.General.TotalNavHeight, + top: 0, + left: 0, + width: NavigationBarStyles.General.ScreenWidth, + }, +}); + +module.exports = BreadcrumbNavigationBar; diff --git a/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBarStyles.ios.js b/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBarStyles.ios.js new file mode 100644 index 0000000000..c71772c23c --- /dev/null +++ b/Libraries/CustomComponents/JSNavigationStack/BreadcrumbNavigationBarStyles.ios.js @@ -0,0 +1,207 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule BreadcrumbNavigationBarStyles + */ +'use strict'; + +var NavigationBarStyles = require('NavigationBarStyles'); + +var buildStyleInterpolator = require('buildStyleInterpolator'); +var merge = require('merge'); + +var SCREEN_WIDTH = NavigationBarStyles.General.ScreenWidth; +var STATUS_BAR_HEIGHT = NavigationBarStyles.General.StatusBarHeight; +var NAV_BAR_HEIGHT = NavigationBarStyles.General.NavBarHeight; + +var SPACING = 4; +var ICON_WIDTH = 40; +var SEPARATOR_WIDTH = 9; +var CRUMB_WIDTH = ICON_WIDTH + SEPARATOR_WIDTH; +var RIGHT_BUTTON_WIDTH = 58; + +var OPACITY_RATIO = 100; +var ICON_INACTIVE_OPACITY = 0.6; +var MAX_BREADCRUMBS = 10; + +var CRUMB_BASE = { + position: 'absolute', + flexDirection: 'row', + top: STATUS_BAR_HEIGHT, + width: CRUMB_WIDTH, + height: NAV_BAR_HEIGHT, + backgroundColor: 'transparent', +}; + +var ICON_BASE = { + width: ICON_WIDTH, + height: NAV_BAR_HEIGHT, +}; + +var SEPARATOR_BASE = { + width: SEPARATOR_WIDTH, + height: NAV_BAR_HEIGHT, +}; + +var TITLE_BASE = { + position: 'absolute', + top: STATUS_BAR_HEIGHT, + height: NAV_BAR_HEIGHT, + backgroundColor: 'transparent', +}; + +// For first title styles, make sure first title is centered +var FIRST_TITLE_BASE = merge(TITLE_BASE, { + left: 0, + alignItems: 'center', + width: SCREEN_WIDTH, + height: NAV_BAR_HEIGHT, +}); + +var RIGHT_BUTTON_BASE = { + position: 'absolute', + top: STATUS_BAR_HEIGHT, + left: SCREEN_WIDTH - SPACING - RIGHT_BUTTON_WIDTH, + overflow: 'hidden', + opacity: 1, + width: RIGHT_BUTTON_WIDTH, + height: NAV_BAR_HEIGHT, + backgroundColor: 'transparent', +}; + +/** + * Precompute crumb styles so that they don't need to be recomputed on every + * interaction. + */ +var LEFT = []; +var CENTER = []; +var RIGHT = []; +for (var i = 0; i < MAX_BREADCRUMBS; i++) { + var crumbLeft = CRUMB_WIDTH * i + SPACING; + LEFT[i] = { + Crumb: merge(CRUMB_BASE, { left: crumbLeft }), + Icon: merge(ICON_BASE, { opacity: ICON_INACTIVE_OPACITY }), + Separator: merge(SEPARATOR_BASE, { opacity: 1 }), + Title: merge(TITLE_BASE, { left: crumbLeft, opacity: 0 }), + RightItem: merge(RIGHT_BUTTON_BASE, { opacity: 0 }), + }; + CENTER[i] = { + Crumb: merge(CRUMB_BASE, { left: crumbLeft }), + Icon: merge(ICON_BASE, { opacity: 1 }), + Separator: merge(SEPARATOR_BASE, { opacity: 0 }), + Title: merge(TITLE_BASE, { + left: crumbLeft + ICON_WIDTH, + opacity: 1, + }), + RightItem: merge(RIGHT_BUTTON_BASE, { opacity: 1 }), + }; + var crumbRight = SCREEN_WIDTH - 100; + RIGHT[i] = { + Crumb: merge(CRUMB_BASE, { left: crumbRight}), + Icon: merge(ICON_BASE, { opacity: 0 }), + Separator: merge(SEPARATOR_BASE, { opacity: 0 }), + Title: merge(TITLE_BASE, { + left: crumbRight + ICON_WIDTH, + opacity: 0, + }), + RightItem: merge(RIGHT_BUTTON_BASE, { opacity: 0 }), + }; +} + +// Special case the CENTER state of the first scene. +CENTER[0] = { + Crumb: merge(CRUMB_BASE, {left: SCREEN_WIDTH / 4}), + Icon: merge(ICON_BASE, {opacity: 0}), + Separator: merge(SEPARATOR_BASE, {opacity: 0}), + Title: merge(FIRST_TITLE_BASE, {opacity: 1}), + RightItem: CENTER[0].RightItem, +}; +LEFT[0].Title = merge(FIRST_TITLE_BASE, {left: - SCREEN_WIDTH / 4, opacity: 0}); +RIGHT[0].Title = merge(FIRST_TITLE_BASE, {opacity: 0}); + + +var buildIndexSceneInterpolator = function(startStyles, endStyles) { + return { + Crumb: buildStyleInterpolator({ + left: { + type: 'linear', + from: startStyles.Crumb.left, + to: endStyles.Crumb.left, + min: 0, + max: 1, + extrapolate: true, + }, + }), + Icon: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.Icon.opacity, + to: endStyles.Icon.opacity, + min: 0, + max: 1, + }, + }), + Separator: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.Separator.opacity, + to: endStyles.Separator.opacity, + min: 0, + max: 1, + }, + }), + Title: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.Title.opacity, + to: endStyles.Title.opacity, + min: 0, + max: 1, + }, + left: { + type: 'linear', + from: startStyles.Title.left, + to: endStyles.Title.left, + min: 0, + max: 1, + extrapolate: true, + }, + }), + RightItem: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.RightItem.opacity, + to: endStyles.RightItem.opacity, + min: 0, + max: 1, + round: OPACITY_RATIO, + }, + }), + }; +}; + +var Interpolators = CENTER.map(function(_, ii) { + return { + // Animating *into* the center stage from the right + RightToCenter: buildIndexSceneInterpolator(RIGHT[ii], CENTER[ii]), + // Animating out of the center stage, to the left + CenterToLeft: buildIndexSceneInterpolator(CENTER[ii], LEFT[ii]), + // Both stages (animating *past* the center stage) + RightToLeft: buildIndexSceneInterpolator(RIGHT[ii], LEFT[ii]), + }; +}); + +/** + * Contains constants that are used in constructing both `StyleSheet`s and + * inline styles during transitions. + */ +module.exports = { + Interpolators, + Left: LEFT, + Center: CENTER, + Right: RIGHT, + IconWidth: ICON_WIDTH, + IconHeight: NAV_BAR_HEIGHT, + SeparatorWidth: SEPARATOR_WIDTH, + SeparatorHeight: NAV_BAR_HEIGHT, +}; diff --git a/Libraries/CustomComponents/JSNavigationStack/NavigationBar.js b/Libraries/CustomComponents/JSNavigationStack/NavigationBar.js new file mode 100644 index 0000000000..e1e27b3df2 --- /dev/null +++ b/Libraries/CustomComponents/JSNavigationStack/NavigationBar.js @@ -0,0 +1,164 @@ +/** + * @providesModule NavigationBar + * @typechecks + */ +'use strict'; + +var React = require('React'); +var NavigationBarStyles = require('NavigationBarStyles'); +var StaticContainer = require('StaticContainer.react'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); +var Text = require('Text'); + +var COMPONENT_NAMES = ['Title', 'LeftButton', 'RightButton']; + +/** + * TODO (janzer): Rename `observedTopOfStack` to `presentedIndex` in `NavigationStack`. + */ +var navStatePresentedIndex = function(navState) { + if (navState.presentedIndex !== undefined) { + return navState.presentedIndex; + } else { + return navState.observedTopOfStack; + } +}; + +var NavigationBar = React.createClass({ + + _getReusableProps: function( + /*string*/componentName, + /*number*/index + ) /*object*/ { + if (!this._reusableProps) { + this._reusableProps = {}; + }; + var propStack = this._reusableProps[componentName]; + if (!propStack) { + propStack = this._reusableProps[componentName] = []; + } + var props = propStack[index]; + if (!props) { + props = propStack[index] = {style:{}}; + } + return props; + }, + + _updateIndexProgress: function( + /*number*/progress, + /*number*/index, + /*number*/fromIndex, + /*number*/toIndex + ) { + var amount = toIndex > fromIndex ? progress : (1 - progress); + var oldDistToCenter = index - fromIndex; + var newDistToCenter = index - toIndex; + var interpolate; + if (oldDistToCenter > 0 && newDistToCenter === 0 || + newDistToCenter > 0 && oldDistToCenter === 0) { + interpolate = NavigationBarStyles.Interpolators.RightToCenter; + } else if (oldDistToCenter < 0 && newDistToCenter === 0 || + newDistToCenter < 0 && oldDistToCenter === 0) { + interpolate = NavigationBarStyles.Interpolators.CenterToLeft; + } else if (oldDistToCenter === newDistToCenter) { + interpolate = NavigationBarStyles.Interpolators.RightToCenter; + } else { + interpolate = NavigationBarStyles.Interpolators.RightToLeft; + } + + COMPONENT_NAMES.forEach(function (componentName) { + var component = this.refs[componentName + index]; + var props = this._getReusableProps(componentName, index); + if (component && interpolate[componentName](props.style, amount)) { + component.setNativeProps(props); + } + }, this); + }, + + updateProgress: function( + /*number*/progress, + /*number*/fromIndex, + /*number*/toIndex + ) { + var max = Math.max(fromIndex, toIndex); + var min = Math.min(fromIndex, toIndex); + for (var index = min; index <= max; index++) { + this._updateIndexProgress(progress, index, fromIndex, toIndex); + } + }, + + render: function() { + var navState = this.props.navState; + var components = COMPONENT_NAMES.map(function (componentName) { + return navState.routeStack.map( + this._renderOrReturnComponent.bind(this, componentName) + ); + }, this); + + return ( + + {components} + + ); + }, + + _renderOrReturnComponent: function( + /*string*/componentName, + /*object*/route, + /*number*/index + ) /*object*/ { + var navState = this.props.navState; + var navBarRouteMapper = this.props.navigationBarRouteMapper; + var uid = navState.idStack[index]; + var containerRef = componentName + 'Container' + uid; + var alreadyRendered = this.refs[containerRef]; + if (alreadyRendered) { + // Don't bother re-calculating the children + return ( + + ); + } + + var content = navBarRouteMapper[componentName]( + navState.routeStack[index], + this.props.navigator, + index, + this.props.navState + ); + if (!content) { + return null; + } + + var initialStage = index === navStatePresentedIndex(this.props.navState) ? + NavigationBarStyles.Stages.Center : NavigationBarStyles.Stages.Left; + return ( + + + {content} + + + ); + }, + +}); + + +var styles = StyleSheet.create({ + navBarContainer: { + position: 'absolute', + height: NavigationBarStyles.General.TotalNavHeight, + top: 0, + left: 0, + width: NavigationBarStyles.General.ScreenWidth, + backgroundColor: 'transparent', + }, +}); + +module.exports = NavigationBar; diff --git a/Libraries/CustomComponents/JSNavigationStack/NavigationBarStyles.js b/Libraries/CustomComponents/JSNavigationStack/NavigationBarStyles.js new file mode 100644 index 0000000000..d27c60db41 --- /dev/null +++ b/Libraries/CustomComponents/JSNavigationStack/NavigationBarStyles.js @@ -0,0 +1,155 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule NavigationBarStyles + */ +'use strict'; + +var Dimensions = require('Dimensions'); + +var buildStyleInterpolator = require('buildStyleInterpolator'); +var merge = require('merge'); + +var SCREEN_WIDTH = Dimensions.get('window').width; +var NAV_BAR_HEIGHT = 44; +var STATUS_BAR_HEIGHT = 20; +var NAV_HEIGHT = NAV_BAR_HEIGHT + STATUS_BAR_HEIGHT; + +var BASE_STYLES = { + Title: { + position: 'absolute', + top: STATUS_BAR_HEIGHT, + left: 0, + alignItems: 'center', + width: SCREEN_WIDTH, + height: NAV_BAR_HEIGHT, + backgroundColor: 'transparent', + }, + LeftButton: { + position: 'absolute', + top: STATUS_BAR_HEIGHT, + left: 0, + overflow: 'hidden', + opacity: 1, + width: SCREEN_WIDTH / 3, + height: NAV_BAR_HEIGHT, + backgroundColor: 'transparent', + }, + RightButton: { + position: 'absolute', + top: STATUS_BAR_HEIGHT, + left: 2 * SCREEN_WIDTH / 3, + overflow: 'hidden', + opacity: 1, + alignItems: 'flex-end', + width: SCREEN_WIDTH / 3, + height: NAV_BAR_HEIGHT, + backgroundColor: 'transparent', + }, +}; + +// There are 3 stages: left, center, right. All previous navigation +// items are in the left stage. The current navigation item is in the +// center stage. All upcoming navigation items are in the right stage. +// Another way to think of the stages is in terms of transitions. When +// we move forward in the navigation stack, we perform a +// right-to-center transition on the new navigation item and a +// center-to-left transition on the current navigation item. +var Stages = { + Left: { + Title: merge(BASE_STYLES.Title, { left: - SCREEN_WIDTH / 2, opacity: 0 }), + LeftButton: merge(BASE_STYLES.LeftButton, { left: - SCREEN_WIDTH / 3, opacity: 1 }), + RightButton: merge(BASE_STYLES.RightButton, { left: SCREEN_WIDTH / 3, opacity: 0 }), + }, + Center: { + Title: merge(BASE_STYLES.Title, { left: 0, opacity: 1 }), + LeftButton: merge(BASE_STYLES.LeftButton, { left: 0, opacity: 1 }), + RightButton: merge(BASE_STYLES.RightButton, { left: 2 * SCREEN_WIDTH / 3 - 0, opacity: 1 }), + }, + Right: { + Title: merge(BASE_STYLES.Title, { left: SCREEN_WIDTH / 2, opacity: 0 }), + LeftButton: merge(BASE_STYLES.LeftButton, { left: 0, opacity: 0 }), + RightButton: merge(BASE_STYLES.RightButton, { left: SCREEN_WIDTH, opacity: 0 }), + }, +}; + + +var opacityRatio = 100; + +function buildSceneInterpolators(startStyles, endStyles) { + return { + Title: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.Title.opacity, + to: endStyles.Title.opacity, + min: 0, + max: 1, + }, + left: { + type: 'linear', + from: startStyles.Title.left, + to: endStyles.Title.left, + min: 0, + max: 1, + extrapolate: true, + }, + }), + LeftButton: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.LeftButton.opacity, + to: endStyles.LeftButton.opacity, + min: 0, + max: 1, + round: opacityRatio, + }, + left: { + type: 'linear', + from: startStyles.LeftButton.left, + to: endStyles.LeftButton.left, + min: 0, + max: 1, + }, + }), + RightButton: buildStyleInterpolator({ + opacity: { + type: 'linear', + from: startStyles.RightButton.opacity, + to: endStyles.RightButton.opacity, + min: 0, + max: 1, + round: opacityRatio, + }, + left: { + type: 'linear', + from: startStyles.RightButton.left, + to: endStyles.RightButton.left, + min: 0, + max: 1, + extrapolate: true, + }, + }), + }; +} + +var Interpolators = { + // Animating *into* the center stage from the right + RightToCenter: buildSceneInterpolators(Stages.Right, Stages.Center), + // Animating out of the center stage, to the left + CenterToLeft: buildSceneInterpolators(Stages.Center, Stages.Left), + // Both stages (animating *past* the center stage) + RightToLeft: buildSceneInterpolators(Stages.Right, Stages.Left), +}; + + +module.exports = { + General: { + NavBarHeight: NAV_BAR_HEIGHT, + StatusBarHeight: STATUS_BAR_HEIGHT, + TotalNavHeight: NAV_HEIGHT, + ScreenWidth: SCREEN_WIDTH, + }, + Interpolators, + Stages, +}; diff --git a/Libraries/CustomComponents/JSNavigationStackAnimationConfigs.js b/Libraries/CustomComponents/JSNavigationStackAnimationConfigs.js new file mode 100644 index 0000000000..b2d8137041 --- /dev/null +++ b/Libraries/CustomComponents/JSNavigationStackAnimationConfigs.js @@ -0,0 +1,279 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule JSNavigationStackAnimationConfigs + */ +'use strict'; + +var Dimensions = require('Dimensions'); +var PixelRatio = require('PixelRatio'); + +var buildStyleInterpolator = require('buildStyleInterpolator'); +var merge = require('merge'); + +var SCREEN_WIDTH = Dimensions.get('window').width; +var SCREEN_HEIGHT = Dimensions.get('window').height; + +var ToTheLeft = { + // Rotate *requires* you to break out each individual component of + // rotation (x, y, z, w) + transformTranslate: { + from: {x: 0, y: 0, z: 0}, + to: {x: -Math.round(Dimensions.get('window').width * 0.3), y: 0, z: 0}, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, + // Uncomment to try rotation: + // Quick guide to reasoning about rotations: + // http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-17-quaternions/#Quaternions + // transformRotateRadians: { + // from: {x: 0, y: 0, z: 0, w: 1}, + // to: {x: 0, y: 0, z: -0.47, w: 0.87}, + // min: 0, + // max: 1, + // type: 'linear', + // extrapolate: true + // }, + transformScale: { + from: {x: 1, y: 1, z: 1}, + to: {x: 0.95, y: 0.95, z: 1}, + min: 0, + max: 1, + type: 'linear', + extrapolate: true + }, + opacity: { + from: 1, + to: 0.3, + min: 0, + max: 1, + type: 'linear', + extrapolate: false, + round: 100, + }, + translateX: { + from: 0, + to: -Math.round(Dimensions.get('window').width * 0.3), + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, + scaleX: { + from: 1, + to: 0.95, + min: 0, + max: 1, + type: 'linear', + extrapolate: true + }, + scaleY: { + from: 1, + to: 0.95, + min: 0, + max: 1, + type: 'linear', + extrapolate: true + }, +}; + +var FromTheRight = { + opacity: { + value: 1.0, + type: 'constant', + }, + + transformTranslate: { + from: {x: Dimensions.get('window').width, y: 0, z: 0}, + to: {x: 0, y: 0, z: 0}, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, + + translateX: { + from: Dimensions.get('window').width, + to: 0, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, + + scaleX: { + value: 1, + type: 'constant', + }, + scaleY: { + value: 1, + type: 'constant', + }, +}; + + +var ToTheBack = { + // Rotate *requires* you to break out each individual component of + // rotation (x, y, z, w) + transformTranslate: { + from: {x: 0, y: 0, z: 0}, + to: {x: 0, y: 0, z: 0}, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, + transformScale: { + from: {x: 1, y: 1, z: 1}, + to: {x: 0.95, y: 0.95, z: 1}, + min: 0, + max: 1, + type: 'linear', + extrapolate: true + }, + opacity: { + from: 1, + to: 0.3, + min: 0, + max: 1, + type: 'linear', + extrapolate: false, + round: 100, + }, + scaleX: { + from: 1, + to: 0.95, + min: 0, + max: 1, + type: 'linear', + extrapolate: true + }, + scaleY: { + from: 1, + to: 0.95, + min: 0, + max: 1, + type: 'linear', + extrapolate: true + }, +}; + +var FromTheFront = { + opacity: { + value: 1.0, + type: 'constant', + }, + + transformTranslate: { + from: {x: 0, y: Dimensions.get('window').height, z: 0}, + to: {x: 0, y: 0, z: 0}, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, + translateY: { + from: Dimensions.get('window').height, + to: 0, + min: 0, + max: 1, + type: 'linear', + extrapolate: true, + round: PixelRatio.get(), + }, + scaleX: { + value: 1, + type: 'constant', + }, + scaleY: { + value: 1, + type: 'constant', + }, +}; + + +var Interpolators = { + Vertical: { + into: buildStyleInterpolator(FromTheFront), + out: buildStyleInterpolator(ToTheBack), + }, + Horizontal: { + into: buildStyleInterpolator(FromTheRight), + out: buildStyleInterpolator(ToTheLeft), + }, +}; + + +// These are meant to mimic iOS default behavior +var PastPointOfNoReturn = { + horizontal: function(location) { + return location > SCREEN_WIDTH * 3 / 5; + }, + vertical: function(location) { + return location > SCREEN_HEIGHT * 3 / 5; + }, +}; + +var BaseConfig = { + // When false, all gestures are ignored for this scene + enableGestures: true, + + // How far the swipe must drag to start transitioning + gestureDetectMovement: 2, + + // Amplitude of release velocity that is considered still + notMoving: 0.3, + + // Velocity to start at when transitioning without gesture + defaultTransitionVelocity: 1.5, + + // Fraction of directional move required. + directionRatio: 0.66, + + // Velocity to transition with when the gesture release was "not moving" + snapVelocity: 2, + + // Rebound spring parameters when transitioning FROM this scene + springFriction: 26, + springTension: 200, + + // Defaults for horizontal transitioning: + + isVertical: false, + screenDimension: SCREEN_WIDTH, + + // Region that can trigger swipe. iOS default is 30px from the left edge + edgeHitWidth: 30, + + // Point at which a non-velocity release will cause nav pop + pastPointOfNoReturn: PastPointOfNoReturn.horizontal, + + // Animation interpolators for this transition + interpolators: Interpolators.Horizontal, +}; + +var JSNavigationStackAnimationConfigs = { + PushFromRight: merge(BaseConfig, { + // We will want to customize this soon + }), + FloatFromRight: merge(BaseConfig, { + // We will want to customize this soon + }), + FloatFromBottom: merge(BaseConfig, { + edgeHitWidth: 150, + interpolators: Interpolators.Vertical, + isVertical: true, + pastPointOfNoReturn: PastPointOfNoReturn.vertical, + screenDimension: SCREEN_HEIGHT, + }), +}; + +module.exports = JSNavigationStackAnimationConfigs; diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 04f7615cca..4eb6308904 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -16,7 +16,7 @@ var RCTUIManager = require('NativeModules').UIManager; var ScrollView = require('ScrollView'); var ScrollResponder = require('ScrollResponder'); var StaticRenderer = require('StaticRenderer'); -var TimerMixin = require('TimerMixin'); +var TimerMixin = require('react-timer-mixin'); var logError = require('logError'); var merge = require('merge'); diff --git a/Libraries/Interaction/InteractionMixin.js b/Libraries/Interaction/InteractionMixin.js new file mode 100644 index 0000000000..633159c1f4 --- /dev/null +++ b/Libraries/Interaction/InteractionMixin.js @@ -0,0 +1,49 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule InteractionMixin + */ +'use strict'; + +var InteractionManager = require('InteractionManager'); + +/** + * This mixin provides safe versions of InteractionManager start/end methods + * that ensures `clearInteractionHandle` is always called + * once per start, even if the component is unmounted. + */ +var InteractionMixin = { + componentWillUnmount: function() { + while (this._interactionMixinHandles.length) { + InteractionManager.clearInteractionHandle( + this._interactionMixinHandles.pop() + ); + } + }, + + _interactionMixinHandles: [], + + createInteractionHandle: function() { + var handle = InteractionManager.createInteractionHandle(); + this._interactionMixinHandles.push(handle); + return handle; + }, + + clearInteractionHandle: function(clearHandle) { + InteractionManager.clearInteractionHandle(clearHandle); + this._interactionMixinHandles = this._interactionMixinHandles.filter( + handle => handle !== clearHandle + ); + }, + + /** + * Schedule work for after all interactions have completed. + * + * @param {function} callback + */ + runAfterInteractions: function(callback) { + InteractionManager.runAfterInteractions(callback); + }, +}; + +module.exports = InteractionMixin; diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index f5cc42eef1..2287ee7b27 100644 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -15,153 +15,32 @@ var RCTDataManager = require('NativeModules').DataManager; var crc32 = require('crc32'); -class XMLHttpRequest { +var XMLHttpRequestBase = require('XMLHttpRequestBase'); - UNSENT: number; - OPENED: number; - HEADERS_RECEIVED: number; - LOADING: number; - DONE: number; - - onreadystatechange: ?Function; - onload: ?Function; - upload: any; - readyState: number; - responseHeaders: ?Object; - responseText: ?string; - status: ?string; - - _method: ?string; - _url: ?string; - _headers: Object; - _sent: boolean; - _aborted: boolean; - - constructor() { - this.UNSENT = 0; - this.OPENED = 1; - this.HEADERS_RECEIVED = 2; - this.LOADING = 3; - this.DONE = 4; - - this.onreadystatechange = undefined; - this.upload = undefined; /* Upload not supported */ - this.readyState = this.UNSENT; - this.responseHeaders = undefined; - this.responseText = undefined; - this.status = undefined; - - this._method = null; - this._url = null; - this._headers = {}; - this._sent = false; - this._aborted = false; - } - - getAllResponseHeaders(): ?string { - /* Stub */ - return ''; - } - - getResponseHeader(header: string): ?string { - /* Stub */ - return ''; - } - - setRequestHeader(header: string, value: any): void { - this._headers[header] = value; - } - - open(method: string, url: string, async: ?boolean): void { - /* Other optional arguments are not supported */ - if (this.readyState !== this.UNSENT) { - throw new Error('Cannot open, already sending'); - } - if (async !== undefined && !async) { - // async is default - throw new Error('Synchronous http requests are not supported'); - } - this._method = method; - this._url = url; - this._aborted = false; - this._setReadyState(this.OPENED); - } - - send(data: any): void { - if (this.readyState !== this.OPENED) { - throw new Error('Request has not been opened'); - } - if (this._sent) { - throw new Error('Request has already been sent'); - } - this._sent = true; +class XMLHttpRequest extends XMLHttpRequestBase { + sendImpl(method: ?string, url: ?string, headers: Object, data: any): void { RCTDataManager.queryData( 'http', JSON.stringify({ - method: this._method, - url: this._url, + method: method, + url: url, data: data, - headers: this._headers, + headers: headers, }), - 'h' + crc32(this._method + '|' + this._url + '|' + data), + 'h' + crc32(method + '|' + url + '|' + data), (result) => { result = JSON.parse(result); - this._callback(result.status, result.responseHeaders, result.responseText); + this.callback(result.status, result.responseHeaders, result.responseText); } ); } - abort(): void { + abortImpl(): void { console.warn( 'XMLHttpRequest: abort() cancels JS callbacks ' + 'but not native HTTP request.' ); - // only call onreadystatechange if there is something to abort, - // below logic is per spec - if (!(this.readyState === this.UNSENT || - (this.readyState === this.OPENED && !this._sent) || - this.readyState === this.DONE)) { - this._sent = false; - this._setReadyState(this.DONE); - } - if (this.readyState === this.DONE) { - this._sendLoad(); - } - this.readyState = this.UNSENT; - this._aborted = true; - } - - _setReadyState(newState: number): void { - this.readyState = newState; - // TODO: workaround flow bug with nullable function checks - var onreadystatechange = this.onreadystatechange; - if (onreadystatechange) { - // We should send an event to handler, but since we don't process that - // event anywhere, let's leave it empty - onreadystatechange(null); - } - } - - _sendLoad(): void { - // TODO: workaround flow bug with nullable function checks - var onload = this.onload; - if (onload) { - // We should send an event to handler, but since we don't process that - // event anywhere, let's leave it empty - onload(null); - } - } - - _callback(status: string, responseHeaders: ?Object, responseText: string): void { - if (this._aborted) { - return; - } - this.status = status; - this.responseHeaders = responseHeaders; - this.responseText = responseText; - this._setReadyState(this.DONE); - this._sendLoad(); } } diff --git a/Libraries/Network/XMLHttpRequestBase.js b/Libraries/Network/XMLHttpRequestBase.js new file mode 100644 index 0000000000..d7619d0754 --- /dev/null +++ b/Libraries/Network/XMLHttpRequestBase.js @@ -0,0 +1,153 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @flow + * @providesModule XMLHttpRequestBase + */ +'use strict'; + +/** + * Shared base for platform-specific XMLHttpRequest implementations. + */ +class XMLHttpRequestBase { + + UNSENT: number; + OPENED: number; + HEADERS_RECEIVED: number; + LOADING: number; + DONE: number; + + onreadystatechange: ?Function; + onload: ?Function; + upload: any; + readyState: number; + responseHeaders: ?Object; + responseText: ?string; + status: ?string; + + _method: ?string; + _url: ?string; + _headers: Object; + _sent: boolean; + _aborted: boolean; + + constructor() { + this.UNSENT = 0; + this.OPENED = 1; + this.HEADERS_RECEIVED = 2; + this.LOADING = 3; + this.DONE = 4; + + this.onreadystatechange = undefined; + this.upload = undefined; /* Upload not supported */ + this.readyState = this.UNSENT; + this.responseHeaders = undefined; + this.responseText = undefined; + this.status = undefined; + + this._method = null; + this._url = null; + this._headers = {}; + this._sent = false; + this._aborted = false; + } + + getAllResponseHeaders(): ?string { + /* Stub */ + return ''; + } + + getResponseHeader(header: string): ?string { + /* Stub */ + return ''; + } + + setRequestHeader(header: string, value: any): void { + this._headers[header] = value; + } + + open(method: string, url: string, async: ?boolean): void { + /* Other optional arguments are not supported */ + if (this.readyState !== this.UNSENT) { + throw new Error('Cannot open, already sending'); + } + if (async !== undefined && !async) { + // async is default + throw new Error('Synchronous http requests are not supported'); + } + this._method = method; + this._url = url; + this._aborted = false; + this._setReadyState(this.OPENED); + } + + sendImpl(method: ?string, url: ?string, headers: Object, data: any): void { + throw new Error('Subclass must define sendImpl method'); + } + + abortImpl(): void { + throw new Error('Subclass must define abortImpl method'); + } + + send(data: any): void { + if (this.readyState !== this.OPENED) { + throw new Error('Request has not been opened'); + } + if (this._sent) { + throw new Error('Request has already been sent'); + } + this._sent = true; + this.sendImpl(this._method, this._url, this._headers, data); + } + + abort(): void { + this.abortImpl(); + // only call onreadystatechange if there is something to abort, + // below logic is per spec + if (!(this.readyState === this.UNSENT || + (this.readyState === this.OPENED && !this._sent) || + this.readyState === this.DONE)) { + this._sent = false; + this._setReadyState(this.DONE); + } + if (this.readyState === this.DONE) { + this._sendLoad(); + } + this.readyState = this.UNSENT; + this._aborted = true; + } + + callback(status: string, responseHeaders: ?Object, responseText: string): void { + if (this._aborted) { + return; + } + this.status = status; + this.responseHeaders = responseHeaders; + this.responseText = responseText; + this._setReadyState(this.DONE); + this._sendLoad(); + } + + _setReadyState(newState: number): void { + this.readyState = newState; + // TODO: workaround flow bug with nullable function checks + var onreadystatechange = this.onreadystatechange; + if (onreadystatechange) { + // We should send an event to handler, but since we don't process that + // event anywhere, let's leave it empty + onreadystatechange(null); + } + } + + _sendLoad(): void { + // TODO: workaround flow bug with nullable function checks + var onload = this.onload; + if (onload) { + // We should send an event to handler, but since we don't process that + // event anywhere, let's leave it empty + onload(null); + } + } +} + +module.exports = XMLHttpRequestBase; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 19a0f90ee9..396939ea58 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -50,9 +50,9 @@ - (void)dealloc + (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { -#ifdef __IPHONE_8_0 - [application registerForRemoteNotifications]; -#endif + if ([application respondsToSelector:@selector(registerForRemoteNotifications)]) { + [application registerForRemoteNotifications]; + } } + (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification @@ -93,11 +93,6 @@ + (void)setApplicationIconBadgeNumber:(NSInteger)number { RCT_EXPORT(); - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self requestPermissions]; - }); - [UIApplication sharedApplication].applicationIconBadgeNumber = number; } @@ -117,13 +112,22 @@ + (void)requestPermissions { RCT_EXPORT(); -#ifdef __IPHONE_8_0 - UIUserNotificationType types = UIUserNotificationTypeSound | UIUserNotificationTypeBadge | UIUserNotificationTypeAlert; - UIUserNotificationSettings *notificationSettings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; - [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; -#else - [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)]; +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 + + // if we are targeting iOS 7, *and* the new UIUserNotificationSettings + // class is not available, then register using the old mechanism + if (![UIUserNotificationSettings class]) { + [[UIApplication sharedApplication] registerForRemoteNotificationTypes: + UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert]; + return; + } + #endif + + UIUserNotificationType types = UIUserNotificationTypeSound | UIUserNotificationTypeBadge | UIUserNotificationTypeAlert; + UIUserNotificationSettings *notificationSettings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; + [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; + } + (void)checkPermissions:(RCTResponseSenderBlock)callback @@ -131,17 +135,11 @@ + (void)checkPermissions:(RCTResponseSenderBlock)callback RCT_EXPORT(); NSMutableDictionary *permissions = [[NSMutableDictionary alloc] init]; -#ifdef __IPHONE_8_0 + UIUserNotificationType types = [[[UIApplication sharedApplication] currentUserNotificationSettings] types]; permissions[@"alert"] = @((BOOL)(types & UIUserNotificationTypeAlert)); permissions[@"badge"] = @((BOOL)(types & UIUserNotificationTypeBadge)); permissions[@"sound"] = @((BOOL)(types & UIUserNotificationTypeSound)); -#else - UIRemoteNotificationType types = [[UIApplication sharedApplication] enabledRemoteNotificationTypes]; - permissions[@"alert"] = @((BOOL)(types & UIRemoteNotificationTypeAlert)); - permissions[@"badge"] = @((BOOL)(types & UIRemoteNotificationTypeBadge)); - permissions[@"sound"] = @((BOOL)(types & UIRemoteNotificationTypeSound)); -#endif callback(@[permissions]); } diff --git a/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.h b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.h new file mode 100644 index 0000000000..063e32a48d --- /dev/null +++ b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.h @@ -0,0 +1,99 @@ +/* + * 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 + +#import + +#import + +#ifndef FB_REFERENCE_IMAGE_DIR +#define FB_REFERENCE_IMAGE_DIR "\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\"" +#endif + +/** + Similar to our much-loved XCTAssert() macros. Use this to perform your test. No need to write an explanation, though. + @param view The view to snapshot + @param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method. + @param referenceImageDirectorySuffix An optional suffix, appended to the reference image directory path, such as "_iOS8" + */ +#define FBSnapshotVerifyViewWithReferenceDirectorySuffix(view__, identifier__, referenceImagesDirectorySuffix__) \ +{ \ +NSError *error__ = nil; \ +NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%s%@", FB_REFERENCE_IMAGE_DIR, referenceImagesDirectorySuffix__]; \ +BOOL comparisonSuccess__ = [self compareSnapshotOfView:(view__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) error:&error__]; \ +XCTAssertTrue(comparisonSuccess__, @"Snapshot comparison failed: %@", error__); \ +} + +#define FBSnapshotVerifyView(view__, identifier__) \ +{ \ +FBSnapshotVerifyViewWithReferenceDirectorySuffix(view__, identifier__, @""); \ +} + +/** + Similar to our much-loved XCTAssert() macros. Use this to perform your test. No need to write an explanation, though. + @param layer The layer to snapshot + @param identifier An optional identifier, used is there are multiple snapshot tests in a given -test method. + @param referenceImageDirectorySuffix An optional suffix, appended to the reference image directory path, such as "_iOS8" + */ +#define FBSnapshotVerifyLayerWithReferenceDirectorySuffix(layer__, identifier__, referenceImagesDirectorySuffix__) \ +{ \ +NSError *error__ = nil; \ +NSString *referenceImagesDirectory__ = [NSString stringWithFormat:@"%s%@", FB_REFERENCE_IMAGE_DIR, referenceImagesDirectorySuffix__]; \ +BOOL comparisonSuccess__ = [self compareSnapshotOfLayer:(layer__) referenceImagesDirectory:referenceImagesDirectory__ identifier:(identifier__) error:&error__]; \ +XCTAssertTrue(comparisonSuccess__, @"Snapshot comparison failed: %@", error__); \ +} + +#define FBSnapshotVerifyLayer(layer__, identifier__) \ +{ \ +FBSnapshotVerifyLayerWithReferenceDirectorySuffix(layer__, identifier__, @""); \ +} + +/** + The base class of view snapshotting tests. If you have small UI component, it's often easier to configure it in a test + and compare an image of the view to a reference image that write lots of complex layout-code tests. + + In order to flip the tests in your subclass to record the reference images set `recordMode` to YES before calling + -[super setUp]. + */ +@interface FBSnapshotTestCase : XCTestCase + +/** + When YES, the test macros will save reference images, rather than performing an actual test. + */ +@property (readwrite, nonatomic, assign) BOOL recordMode; + +/** + Performs the comparisong or records a snapshot of the layer if recordMode is YES. + @param layer The Layer to snapshot + @param referenceImagesDirectory The directory in which reference images are stored. + @param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method. + @param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc). + @returns YES if the comparison (or saving of the reference image) succeeded. + */ +- (BOOL)compareSnapshotOfLayer:(CALayer *)layer + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + +/** + Performs the comparisong or records a snapshot of the view if recordMode is YES. + @param view The view to snapshot + @param referenceImagesDirectory The directory in which reference images are stored. + @param identifier An optional identifier, used if there are multiple snapshot tests in a given -test method. + @param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc). + @returns YES if the comparison (or saving of the reference image) succeeded. + */ +- (BOOL)compareSnapshotOfView:(UIView *)view + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + +@end diff --git a/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.m b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.m new file mode 100644 index 0000000000..ebd311c648 --- /dev/null +++ b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestCase.m @@ -0,0 +1,82 @@ +/* + * 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 "FBSnapshotTestCase.h" + +#import "FBSnapshotTestController.h" + +@interface FBSnapshotTestCase () + +@property (readwrite, nonatomic, retain) FBSnapshotTestController *snapshotController; + +@end + +@implementation FBSnapshotTestCase + +- (void)setUp +{ + [super setUp]; + self.snapshotController = [[FBSnapshotTestController alloc] initWithTestName:NSStringFromClass([self class])]; +} + +- (void)tearDown +{ + self.snapshotController = nil; + [super tearDown]; +} + +- (BOOL)recordMode +{ + return self.snapshotController.recordMode; +} + +- (void)setRecordMode:(BOOL)recordMode +{ + self.snapshotController.recordMode = recordMode; +} + +- (BOOL)compareSnapshotOfLayer:(CALayer *)layer + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + return [self _compareSnapshotOfViewOrLayer:layer + referenceImagesDirectory:referenceImagesDirectory + identifier:identifier + error:errorPtr]; +} + +- (BOOL)compareSnapshotOfView:(UIView *)view + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + return [self _compareSnapshotOfViewOrLayer:view + referenceImagesDirectory:referenceImagesDirectory + identifier:identifier + error:errorPtr]; +} + +#pragma mark - +#pragma mark Private API + +- (BOOL)_compareSnapshotOfViewOrLayer:(id)viewOrLayer + referenceImagesDirectory:(NSString *)referenceImagesDirectory + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + _snapshotController.referenceImagesDirectory = referenceImagesDirectory; + return [_snapshotController compareSnapshotOfViewOrLayer:viewOrLayer + selector:self.invocation.selector + identifier:identifier + error:errorPtr]; +} + +@end diff --git a/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.h b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.h new file mode 100644 index 0000000000..349384d8f8 --- /dev/null +++ b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.h @@ -0,0 +1,152 @@ +/* + * 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 +#import + +typedef NS_ENUM(NSInteger, FBSnapshotTestControllerErrorCode) { + FBSnapshotTestControllerErrorCodeUnknown, + FBSnapshotTestControllerErrorCodeNeedsRecord, + FBSnapshotTestControllerErrorCodePNGCreationFailed, + FBSnapshotTestControllerErrorCodeImagesDifferentSizes, + FBSnapshotTestControllerErrorCodeImagesDifferent, +}; +/** + Errors returned by the methods of FBSnapshotTestController use this domain. + */ +extern NSString *const FBSnapshotTestControllerErrorDomain; + +/** + Errors returned by the methods of FBSnapshotTestController sometimes contain this key in the `userInfo` dictionary. + */ +extern NSString *const FBReferenceImageFilePathKey; + +/** + Provides the heavy-lifting for FBSnapshotTestCase. It loads and saves images, along with performing the actual pixel- + by-pixel comparison of images. + Instances are initialized with the test class, and directories to read and write to. + */ +@interface FBSnapshotTestController : NSObject + +/** + Record snapshots. + **/ +@property(readwrite, nonatomic, assign) BOOL recordMode; + +/** + @param testClass The subclass of FBSnapshotTestCase that is using this controller. + @param referenceImagesDirectory The directory where the reference images are stored. + @returns An instance of FBSnapshotTestController. + */ +- (id)initWithTestClass:(Class)testClass; + +/** + Designated initializer. + @param testName The name of the tests. + @param referenceImagesDirectory The directory where the reference images are stored. + @returns An instance of FBSnapshotTestController. + */ +- (id)initWithTestName:(NSString *)testName; + + +/** + Performs the comparison of the layer. + @param layer The Layer to snapshot. + @param referenceImagesDirectory The directory in which reference images are stored. + @param identifier An optional identifier, used is there are muliptle snapshot tests in a given -test method. + @param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc). + @returns YES if the comparison (or saving of the reference image) succeeded. + */ +- (BOOL)compareSnapshotOfLayer:(CALayer *)layer + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + +/** + Performs the comparison of the view. + @param view The view to snapshot. + @param referenceImagesDirectory The directory in which reference images are stored. + @param identifier An optional identifier, used is there are muliptle snapshot tests in a given -test method. + @param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc). + @returns YES if the comparison (or saving of the reference image) succeeded. + */ +- (BOOL)compareSnapshotOfView:(UIView *)view + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + +/** + Performs the comparison of a view or layer. + @param view The view or layer to snapshot. + @param referenceImagesDirectory The directory in which reference images are stored. + @param identifier An optional identifier, used is there are muliptle snapshot tests in a given -test method. + @param error An error to log in an XCTAssert() macro if the method fails (missing reference image, images differ, etc). + @returns YES if the comparison (or saving of the reference image) succeeded. + */ +- (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + + +/** + The directory in which referfence images are stored. + */ +@property (readwrite, nonatomic, copy) NSString *referenceImagesDirectory; + +/** + Loads a reference image. + @param selector The test method being run. + @param identifier The optional identifier, used when multiple images are tested in a single -test method. + @param error An error, if this methods returns nil, the error will be something useful. + @returns An image. + */ +- (UIImage *)referenceImageForSelector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)error; + +/** + Saves a reference image. + @param selector The test method being run. + @param identifier The optional identifier, used when multiple images are tested in a single -test method. + @param error An error, if this methods returns NO, the error will be something useful. + @returns An image. + */ +- (BOOL)saveReferenceImage:(UIImage *)image + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr; + +/** + Performs a pixel-by-pixel comparison of the two images. + @param referenceImage The reference (correct) image. + @param image The image to test against the reference. + @param error An error that indicates why the comparison failed if it does. + @param YES if the comparison succeeded and the images are the same. + */ +- (BOOL)compareReferenceImage:(UIImage *)referenceImage + toImage:(UIImage *)image + error:(NSError **)errorPtr; + +/** + Saves the reference image and the test image to `failedOutputDirectory`. + @param referenceImage The reference (correct) image. + @param testImage The image to test against the reference. + @param selector The test method being run. + @param identifier The optional identifier, used when multiple images are tested in a single -test method. + @param error An error that indicates why the comparison failed if it does. + @param YES if the save succeeded. + */ +- (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage + testImage:(UIImage *)testImage + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr; +@end diff --git a/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m new file mode 100644 index 0000000000..7d12736d19 --- /dev/null +++ b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m @@ -0,0 +1,392 @@ +/* + * 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 "FBSnapshotTestController.h" + +#import "UIImage+Compare.h" +#import "UIImage+Diff.h" + +#import + +#import + +NSString *const FBSnapshotTestControllerErrorDomain = @"FBSnapshotTestControllerErrorDomain"; + +NSString *const FBReferenceImageFilePathKey = @"FBReferenceImageFilePathKey"; + +typedef struct RGBAPixel { + char r; + char g; + char b; + char a; +} RGBAPixel; + +@interface FBSnapshotTestController () + +@property (readonly, nonatomic, copy) NSString *testName; + +@end + +@implementation FBSnapshotTestController +{ + NSFileManager *_fileManager; +} + +#pragma mark - +#pragma mark Lifecycle + +- (id)initWithTestClass:(Class)testClass; +{ + return [self initWithTestName:NSStringFromClass(testClass)]; +} + +- (id)initWithTestName:(NSString *)testName +{ + if ((self = [super init])) { + _testName = [testName copy]; + _fileManager = [[NSFileManager alloc] init]; + } + return self; +} + +#pragma mark - +#pragma mark Properties + +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@ %@", [super description], _referenceImagesDirectory]; +} + +#pragma mark - +#pragma mark Public API + +- (UIImage *)referenceImageForSelector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + NSString *filePath = [self _referenceFilePathForSelector:selector identifier:identifier]; + UIImage *image = [UIImage imageWithContentsOfFile:filePath]; + if (nil == image && NULL != errorPtr) { + BOOL exists = [_fileManager fileExistsAtPath:filePath]; + if (!exists) { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodeNeedsRecord + userInfo:@{ + FBReferenceImageFilePathKey: filePath, + NSLocalizedDescriptionKey: @"Unable to load reference image.", + NSLocalizedFailureReasonErrorKey: @"Reference image not found. You need to run the test in record mode", + }]; + } else { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodeUnknown + userInfo:nil]; + } + } + return image; +} + +- (BOOL)saveReferenceImage:(UIImage *)image + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + BOOL didWrite = NO; + if (nil != image) { + NSString *filePath = [self _referenceFilePathForSelector:selector identifier:identifier]; + NSData *pngData = UIImagePNGRepresentation(image); + if (nil != pngData) { + NSError *creationError = nil; + BOOL didCreateDir = [_fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent] + withIntermediateDirectories:YES + attributes:nil + error:&creationError]; + if (!didCreateDir) { + if (NULL != errorPtr) { + *errorPtr = creationError; + } + return NO; + } + didWrite = [pngData writeToFile:filePath options:NSDataWritingAtomic error:errorPtr]; + if (didWrite) { + NSLog(@"Reference image save at: %@", filePath); + } + } else { + if (nil != errorPtr) { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodePNGCreationFailed + userInfo:@{ + FBReferenceImageFilePathKey: filePath, + }]; + } + } + } + return didWrite; +} + +- (BOOL)saveFailedReferenceImage:(UIImage *)referenceImage + testImage:(UIImage *)testImage + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + NSData *referencePNGData = UIImagePNGRepresentation(referenceImage); + NSData *testPNGData = UIImagePNGRepresentation(testImage); + + NSString *referencePath = [self _failedFilePathForSelector:selector + identifier:identifier + fileNameType:FBTestSnapshotFileNameTypeFailedReference]; + + NSError *creationError = nil; + BOOL didCreateDir = [_fileManager createDirectoryAtPath:[referencePath stringByDeletingLastPathComponent] + withIntermediateDirectories:YES + attributes:nil + error:&creationError]; + if (!didCreateDir) { + if (NULL != errorPtr) { + *errorPtr = creationError; + } + return NO; + } + + if (![referencePNGData writeToFile:referencePath options:NSDataWritingAtomic error:errorPtr]) { + return NO; + } + + NSString *testPath = [self _failedFilePathForSelector:selector + identifier:identifier + fileNameType:FBTestSnapshotFileNameTypeFailedTest]; + + if (![testPNGData writeToFile:testPath options:NSDataWritingAtomic error:errorPtr]) { + return NO; + } + + NSString *diffPath = [self _failedFilePathForSelector:selector + identifier:identifier + fileNameType:FBTestSnapshotFileNameTypeFailedTestDiff]; + + UIImage *diffImage = [referenceImage diffWithImage:testImage]; + NSData *diffImageData = UIImagePNGRepresentation(diffImage); + + if (![diffImageData writeToFile:diffPath options:NSDataWritingAtomic error:errorPtr]) { + return NO; + } + + NSLog(@"If you have Kaleidoscope installed you can run this command to see an image diff:\n" + @"ksdiff \"%@\" \"%@\"", referencePath, testPath); + + return YES; +} + +- (BOOL)compareReferenceImage:(UIImage *)referenceImage toImage:(UIImage *)image error:(NSError **)errorPtr +{ + if (CGSizeEqualToSize(referenceImage.size, image.size)) { + + BOOL imagesEqual = [referenceImage compareWithImage:image]; + if (NULL != errorPtr) { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodeImagesDifferent + userInfo:@{ + NSLocalizedDescriptionKey: @"Images different", + }]; + } + return imagesEqual; + } + if (NULL != errorPtr) { + *errorPtr = [NSError errorWithDomain:FBSnapshotTestControllerErrorDomain + code:FBSnapshotTestControllerErrorCodeImagesDifferentSizes + userInfo:@{ + NSLocalizedDescriptionKey: @"Images different sizes", + NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"referenceImage:%@, image:%@", + NSStringFromCGSize(referenceImage.size), + NSStringFromCGSize(image.size)], + }]; + } + return NO; +} + +#pragma mark - +#pragma mark Private API + +typedef NS_ENUM(NSUInteger, FBTestSnapshotFileNameType) { + FBTestSnapshotFileNameTypeReference, + FBTestSnapshotFileNameTypeFailedReference, + FBTestSnapshotFileNameTypeFailedTest, + FBTestSnapshotFileNameTypeFailedTestDiff, +}; + +- (NSString *)_fileNameForSelector:(SEL)selector + identifier:(NSString *)identifier + fileNameType:(FBTestSnapshotFileNameType)fileNameType +{ + NSString *fileName = nil; + switch (fileNameType) { + case FBTestSnapshotFileNameTypeFailedReference: + fileName = @"reference_"; + break; + case FBTestSnapshotFileNameTypeFailedTest: + fileName = @"failed_"; + break; + case FBTestSnapshotFileNameTypeFailedTestDiff: + fileName = @"diff_"; + break; + default: + fileName = @""; + break; + } + fileName = [fileName stringByAppendingString:NSStringFromSelector(selector)]; + if (0 < identifier.length) { + fileName = [fileName stringByAppendingFormat:@"_%@", identifier]; + } + if ([[UIScreen mainScreen] scale] > 1.0) { + fileName = [fileName stringByAppendingFormat:@"@%.fx", [[UIScreen mainScreen] scale]]; + } + fileName = [fileName stringByAppendingPathExtension:@"png"]; + return fileName; +} + +- (NSString *)_referenceFilePathForSelector:(SEL)selector identifier:(NSString *)identifier +{ + NSString *fileName = [self _fileNameForSelector:selector + identifier:identifier + fileNameType:FBTestSnapshotFileNameTypeReference]; + NSString *filePath = [_referenceImagesDirectory stringByAppendingPathComponent:_testName]; + filePath = [filePath stringByAppendingPathComponent:fileName]; + return filePath; +} + +- (NSString *)_failedFilePathForSelector:(SEL)selector + identifier:(NSString *)identifier + fileNameType:(FBTestSnapshotFileNameType)fileNameType +{ + NSString *fileName = [self _fileNameForSelector:selector + identifier:identifier + fileNameType:fileNameType]; + NSString *folderPath = NSTemporaryDirectory(); + if (getenv("IMAGE_DIFF_DIR")) { + folderPath = @(getenv("IMAGE_DIFF_DIR")); + } + NSString *filePath = [folderPath stringByAppendingPathComponent:_testName]; + filePath = [filePath stringByAppendingPathComponent:fileName]; + return filePath; +} + +- (BOOL)compareSnapshotOfLayer:(CALayer *)layer + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + return [self compareSnapshotOfViewOrLayer:layer + selector:selector + identifier:identifier + error:errorPtr]; +} + +- (BOOL)compareSnapshotOfView:(UIView *)view + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + return [self compareSnapshotOfViewOrLayer:view + selector:selector + identifier:identifier + error:errorPtr]; +} + +- (BOOL)compareSnapshotOfViewOrLayer:(id)viewOrLayer + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + if (self.recordMode) { + return [self _recordSnapshotOfViewOrLayer:viewOrLayer selector:selector identifier:identifier error:errorPtr]; + } else { + return [self _performPixelComparisonWithViewOrLayer:viewOrLayer selector:selector identifier:identifier error:errorPtr]; + } +} + +#pragma mark - +#pragma mark Private API + +- (BOOL)_performPixelComparisonWithViewOrLayer:(UIView *)viewOrLayer + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + UIImage *referenceImage = [self referenceImageForSelector:selector identifier:identifier error:errorPtr]; + if (nil != referenceImage) { + UIImage *snapshot = [self _snapshotViewOrLayer:viewOrLayer]; + BOOL imagesSame = [self compareReferenceImage:referenceImage toImage:snapshot error:errorPtr]; + if (!imagesSame) { + [self saveFailedReferenceImage:referenceImage + testImage:snapshot + selector:selector + identifier:identifier + error:errorPtr]; + } + return imagesSame; + } + return NO; +} + +- (BOOL)_recordSnapshotOfViewOrLayer:(id)viewOrLayer + selector:(SEL)selector + identifier:(NSString *)identifier + error:(NSError **)errorPtr +{ + UIImage *snapshot = [self _snapshotViewOrLayer:viewOrLayer]; + return [self saveReferenceImage:snapshot selector:selector identifier:identifier error:errorPtr]; +} + +- (UIImage *)_snapshotViewOrLayer:(id)viewOrLayer +{ + CALayer *layer = nil; + + if ([viewOrLayer isKindOfClass:[UIView class]]) { + return [self _renderView:viewOrLayer]; + } else if ([viewOrLayer isKindOfClass:[CALayer class]]) { + layer = (CALayer *)viewOrLayer; + [layer layoutIfNeeded]; + return [self _renderLayer:layer]; + } else { + [NSException raise:@"Only UIView and CALayer classes can be snapshotted" format:@"%@", viewOrLayer]; + } + return nil; +} + +- (UIImage *)_renderLayer:(CALayer *)layer +{ + CGRect bounds = layer.bounds; + + NSAssert1(CGRectGetWidth(bounds), @"Zero width for layer %@", layer); + NSAssert1(CGRectGetHeight(bounds), @"Zero height for layer %@", layer); + + UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0); + CGContextRef context = UIGraphicsGetCurrentContext(); + NSAssert1(context, @"Could not generate context for layer %@", layer); + + CGContextSaveGState(context); + { + [layer renderInContext:context]; + } + CGContextRestoreGState(context); + + UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return snapshot; +} + +- (UIImage *)_renderView:(UIView *)view +{ + [view layoutIfNeeded]; + return [self _renderLayer:view.layer]; +} + +@end diff --git a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.h b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.h new file mode 100644 index 0000000000..11c6fa6385 --- /dev/null +++ b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.h @@ -0,0 +1,37 @@ +// +// Created by Gabriel Handford on 3/1/09. +// Copyright 2009-2013. All rights reserved. +// Created by John Boiles on 10/20/11. +// Copyright (c) 2011. All rights reserved +// Modified by Felix Schulze on 2/11/13. +// Copyright 2013. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// 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 +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS 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. +// + +#import + +@interface UIImage (Compare) + +- (BOOL)compareWithImage:(UIImage *)image; + +@end diff --git a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m new file mode 100644 index 0000000000..e38c6e4e81 --- /dev/null +++ b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m @@ -0,0 +1,91 @@ +// +// Created by Gabriel Handford on 3/1/09. +// Copyright 2009-2013. All rights reserved. +// Created by John Boiles on 10/20/11. +// Copyright (c) 2011. All rights reserved +// Modified by Felix Schulze on 2/11/13. +// Copyright 2013. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// 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 +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS 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. +// + +#import "UIImage+Compare.h" + +@implementation UIImage (Compare) + +- (BOOL)compareWithImage:(UIImage *)image +{ + NSAssert(CGSizeEqualToSize(self.size, image.size), @"Images must be same size."); + + // The images have the equal size, so we could use the smallest amount of bytes because of byte padding + size_t minBytesPerRow = MIN(CGImageGetBytesPerRow(self.CGImage), CGImageGetBytesPerRow(image.CGImage)); + size_t referenceImageSizeBytes = CGImageGetHeight(self.CGImage) * minBytesPerRow; + void *referenceImagePixels = calloc(1, referenceImageSizeBytes); + void *imagePixels = calloc(1, referenceImageSizeBytes); + + if (!referenceImagePixels || !imagePixels) { + free(referenceImagePixels); + free(imagePixels); + return NO; + } + + CGContextRef referenceImageContext = CGBitmapContextCreate(referenceImagePixels, + CGImageGetWidth(self.CGImage), + CGImageGetHeight(self.CGImage), + CGImageGetBitsPerComponent(self.CGImage), + minBytesPerRow, + CGImageGetColorSpace(self.CGImage), + (CGBitmapInfo)kCGImageAlphaPremultipliedLast + ); + CGContextRef imageContext = CGBitmapContextCreate(imagePixels, + CGImageGetWidth(image.CGImage), + CGImageGetHeight(image.CGImage), + CGImageGetBitsPerComponent(image.CGImage), + minBytesPerRow, + CGImageGetColorSpace(image.CGImage), + (CGBitmapInfo)kCGImageAlphaPremultipliedLast + ); + + CGFloat scaleFactor = [[UIScreen mainScreen] scale]; + CGContextScaleCTM(referenceImageContext, scaleFactor, scaleFactor); + CGContextScaleCTM(imageContext, scaleFactor, scaleFactor); + + if (!referenceImageContext || !imageContext) { + CGContextRelease(referenceImageContext); + CGContextRelease(imageContext); + free(referenceImagePixels); + free(imagePixels); + return NO; + } + + CGContextDrawImage(referenceImageContext, CGRectMake(0.0f, 0.0f, self.size.width, self.size.height), self.CGImage); + CGContextDrawImage(imageContext, CGRectMake(0.0f, 0.0f, image.size.width, image.size.height), image.CGImage); + CGContextRelease(referenceImageContext); + CGContextRelease(imageContext); + + BOOL imageEqual = (memcmp(referenceImagePixels, imagePixels, referenceImageSizeBytes) == 0); + free(referenceImagePixels); + free(imagePixels); + return imageEqual; +} + +@end diff --git a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.h b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.h new file mode 100644 index 0000000000..35595843f3 --- /dev/null +++ b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.h @@ -0,0 +1,37 @@ +// +// Created by Gabriel Handford on 3/1/09. +// Copyright 2009-2013. All rights reserved. +// Created by John Boiles on 10/20/11. +// Copyright (c) 2011. All rights reserved +// Modified by Felix Schulze on 2/11/13. +// Copyright 2013. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// 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 +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS 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. +// + +#import + +@interface UIImage (Diff) + +- (UIImage *)diffWithImage:(UIImage *)image; + +@end diff --git a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.m b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.m new file mode 100644 index 0000000000..44ecb59ee8 --- /dev/null +++ b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Diff.m @@ -0,0 +1,56 @@ +// +// Created by Gabriel Handford on 3/1/09. +// Copyright 2009-2013. All rights reserved. +// Created by John Boiles on 10/20/11. +// Copyright (c) 2011. All rights reserved +// Modified by Felix Schulze on 2/11/13. +// Copyright 2013. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// 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 +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS 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. +// + +#import "UIImage+Diff.h" + +@implementation UIImage (Diff) + +- (UIImage *)diffWithImage:(UIImage *)image +{ + if (!image) { + return nil; + } + CGSize imageSize = CGSizeMake(MAX(self.size.width, image.size.width), MAX(self.size.height, image.size.height)); + UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0.0); + CGContextRef context = UIGraphicsGetCurrentContext(); + [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)]; + CGContextSetAlpha(context, 0.5f); + CGContextBeginTransparencyLayer(context, NULL); + [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; + CGContextSetBlendMode(context, kCGBlendModeDifference); + CGContextSetFillColorWithColor(context,[UIColor whiteColor].CGColor); + CGContextFillRect(context, CGRectMake(0, 0, self.size.width, self.size.height)); + CGContextEndTransparencyLayer(context); + UIImage *returnImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return returnImage; +} + +@end diff --git a/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj b/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj index f377b4c982..bb4457b1f4 100644 --- a/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj +++ b/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj @@ -10,6 +10,9 @@ 585135371AB3C56F00882537 /* RCTTestModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 585135341AB3C56F00882537 /* RCTTestModule.m */; }; 585135381AB3C57000882537 /* RCTTestRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 585135361AB3C56F00882537 /* RCTTestRunner.m */; }; 585135391AB3C59A00882537 /* RCTTestRunner.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 585135351AB3C56F00882537 /* RCTTestRunner.h */; }; + 58E64FED1AB964CD007446E2 /* FBSnapshotTestController.m in Sources */ = {isa = PBXBuildFile; fileRef = 58E64FE71AB964CD007446E2 /* FBSnapshotTestController.m */; }; + 58E64FEE1AB964CD007446E2 /* UIImage+Compare.m in Sources */ = {isa = PBXBuildFile; fileRef = 58E64FE91AB964CD007446E2 /* UIImage+Compare.m */; }; + 58E64FEF1AB964CD007446E2 /* UIImage+Diff.m in Sources */ = {isa = PBXBuildFile; fileRef = 58E64FEB1AB964CD007446E2 /* UIImage+Diff.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -31,6 +34,14 @@ 585135341AB3C56F00882537 /* RCTTestModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTestModule.m; sourceTree = ""; }; 585135351AB3C56F00882537 /* RCTTestRunner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTestRunner.h; sourceTree = ""; }; 585135361AB3C56F00882537 /* RCTTestRunner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTestRunner.m; sourceTree = ""; }; + 58E64FE41AB964CD007446E2 /* FBSnapshotTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBSnapshotTestCase.h; sourceTree = ""; }; + 58E64FE51AB964CD007446E2 /* FBSnapshotTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSnapshotTestCase.m; sourceTree = ""; }; + 58E64FE61AB964CD007446E2 /* FBSnapshotTestController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBSnapshotTestController.h; sourceTree = ""; }; + 58E64FE71AB964CD007446E2 /* FBSnapshotTestController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSnapshotTestController.m; sourceTree = ""; }; + 58E64FE81AB964CD007446E2 /* UIImage+Compare.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+Compare.h"; sourceTree = ""; }; + 58E64FE91AB964CD007446E2 /* UIImage+Compare.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+Compare.m"; sourceTree = ""; }; + 58E64FEA1AB964CD007446E2 /* UIImage+Diff.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+Diff.h"; sourceTree = ""; }; + 58E64FEB1AB964CD007446E2 /* UIImage+Diff.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+Diff.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -51,6 +62,7 @@ 585135341AB3C56F00882537 /* RCTTestModule.m */, 585135351AB3C56F00882537 /* RCTTestRunner.h */, 585135361AB3C56F00882537 /* RCTTestRunner.m */, + 58E64FE31AB964CD007446E2 /* FBSnapshotTestCase */, 580C37701AB104AF0015E709 /* Products */, ); sourceTree = ""; @@ -63,6 +75,21 @@ name = Products; sourceTree = ""; }; + 58E64FE31AB964CD007446E2 /* FBSnapshotTestCase */ = { + isa = PBXGroup; + children = ( + 58E64FE41AB964CD007446E2 /* FBSnapshotTestCase.h */, + 58E64FE51AB964CD007446E2 /* FBSnapshotTestCase.m */, + 58E64FE61AB964CD007446E2 /* FBSnapshotTestController.h */, + 58E64FE71AB964CD007446E2 /* FBSnapshotTestController.m */, + 58E64FE81AB964CD007446E2 /* UIImage+Compare.h */, + 58E64FE91AB964CD007446E2 /* UIImage+Compare.m */, + 58E64FEA1AB964CD007446E2 /* UIImage+Diff.h */, + 58E64FEB1AB964CD007446E2 /* UIImage+Diff.m */, + ); + path = FBSnapshotTestCase; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -119,7 +146,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 58E64FEE1AB964CD007446E2 /* UIImage+Compare.m in Sources */, 585135371AB3C56F00882537 /* RCTTestModule.m in Sources */, + 58E64FEF1AB964CD007446E2 /* UIImage+Diff.m in Sources */, + 58E64FED1AB964CD007446E2 /* FBSnapshotTestController.m in Sources */, 585135381AB3C57000882537 /* RCTTestRunner.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Libraries/RCTTest/RCTTestModule.h b/Libraries/RCTTest/RCTTestModule.h index 1b50f5db2a..0f5adcd2cd 100644 --- a/Libraries/RCTTest/RCTTestModule.h +++ b/Libraries/RCTTest/RCTTestModule.h @@ -7,10 +7,20 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import "RCTBridgeModule.h" +@class FBSnapshotTestController; + @interface RCTTestModule : NSObject +// This is typically polled while running the runloop until true @property (nonatomic, readonly, getter=isDone) BOOL done; +// This is used to give meaningful names to snapshot image files. +@property (nonatomic, assign) SEL testSelector; + +- (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controller view:(UIView *)view; + @end diff --git a/Libraries/RCTTest/RCTTestModule.m b/Libraries/RCTTest/RCTTestModule.m index a02d7759c7..03f025f20f 100644 --- a/Libraries/RCTTest/RCTTestModule.m +++ b/Libraries/RCTTest/RCTTestModule.m @@ -9,7 +9,46 @@ #import "RCTTestModule.h" -@implementation RCTTestModule +#import "FBSnapshotTestController.h" +#import "RCTAssert.h" +#import "RCTLog.h" + +@implementation RCTTestModule { + __weak FBSnapshotTestController *_snapshotController; + __weak UIView *_view; + NSMutableDictionary *_snapshotCounter; +} + +- (instancetype)initWithSnapshotController:(FBSnapshotTestController *)controller view:(UIView *)view +{ + if ((self = [super init])) { + _snapshotController = controller; + _view = view; + _snapshotCounter = [NSMutableDictionary new]; + } + return self; +} + +- (void)verifySnapshot:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + if (!_snapshotController) { + RCTLogWarn(@"No snapshot controller configured."); + callback(@[]); + return; + } + + NSError *error = nil; + NSString *testName = NSStringFromSelector(_testSelector); + _snapshotCounter[testName] = @([_snapshotCounter[testName] integerValue] + 1); + BOOL success = [_snapshotController compareSnapshotOfView:_view + selector:_testSelector + identifier:[_snapshotCounter[testName] stringValue] + error:&error]; + RCTAssert(success, @"Snapshot comparison failed: %@", error); + callback(@[]); +} - (void)markTestCompleted { diff --git a/Libraries/RCTTest/RCTTestRunner.h b/Libraries/RCTTest/RCTTestRunner.h index 592afa4793..9d56202ceb 100644 --- a/Libraries/RCTTest/RCTTestRunner.h +++ b/Libraries/RCTTest/RCTTestRunner.h @@ -9,13 +9,63 @@ #import +/** + * Use the initRunnerForApp macro for typical usage. + * + * Add this to your test target's gcc preprocessor macros: + * + * FB_REFERENCE_IMAGE_DIR="\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\"" + */ +#define initRunnerForApp(app__) [[RCTTestRunner alloc] initWithApp:(app__) referenceDir:@FB_REFERENCE_IMAGE_DIR] + @interface RCTTestRunner : NSObject +@property (nonatomic, assign) BOOL recordMode; @property (nonatomic, copy) NSString *script; -- (instancetype)initWithApp:(NSString *)app; -- (void)runTest:(NSString *)moduleName; -- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)expectErrorRegex; -- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock; +/** + * Initialize a runner. It's recommended that you use the initRunnerForApp macro instead of calling this directly. + * + * @param app The path to the app bundle without suffixes, e.g. IntegrationTests/IntegrationTestsApp + * @param referencesDir The path for snapshot references images. The initRunnerForApp macro uses + * FB_REFERENCE_IMAGE_DIR for this automatically. + */ +- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir; + +/** + * Simplest runTest function simply mounts the specified JS module with no initialProps and waits for it to call + * + * RCTTestModule.markTestCompleted() + * + * JS errors/exceptions and timeouts will fail the test. Snapshot tests call RCTTestModule.verifySnapshot whenever they + * want to verify what has been rendered (typically via requestAnimationFrame to make sure the latest state has been + * rendered in native. + * + * @param test Selector of the test, usually just `_cmd`. + * @param moduleName Name of the JS component as registered by `AppRegistry.registerComponent` in JS. + */ +- (void)runTest:(SEL)test module:(NSString *)moduleName; + +/** + * Same as runTest:, but allows for passing initialProps for providing mock data or requesting different behaviors, and + * expectErrorRegex verifies that the error you expected was thrown. + * + * @param test Selector of the test, usually just `_cmd`. + * @param moduleName Name of the JS component as registered by `AppRegistry.registerComponent` in JS. + * @param initialProps props that are passed into the component when rendered. + * @param expectErrorRegex A regex that must match the error thrown. If no error is thrown, the test fails. + */ +- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)expectErrorRegex; + +/** + * Same as runTest:, but allows for passing initialProps for providing mock data or requesting different behaviors, and + * expectErrorBlock provides arbitrary logic for processing errors (nil will cause any error to fail the test). + * + * @param test Selector of the test, usually just `_cmd`. + * @param moduleName Name of the JS component as registered by `AppRegistry.registerComponent` in JS. + * @param initialProps props that are passed into the component when rendered. + * @param expectErrorBlock A block that takes the error message and returns NO to fail the test. + */ +- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock; @end diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 71459e51b0..a6b710e3b0 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -9,6 +9,7 @@ #import "RCTTestRunner.h" +#import "FBSnapshotTestController.h" #import "RCTRedBox.h" #import "RCTRootView.h" #import "RCTTestModule.h" @@ -17,33 +18,55 @@ #define TIMEOUT_SECONDS 240 @implementation RCTTestRunner +{ + FBSnapshotTestController *_snapshotController; +} -- (instancetype)initWithApp:(NSString *)app +- (instancetype)initWithApp:(NSString *)app referenceDir:(NSString *)referenceDir { - if (self = [super init]) { + if ((self = [super init])) { + NSString *sanitizedAppName = [app stringByReplacingOccurrencesOfString:@"/" withString:@"-"]; + sanitizedAppName = [sanitizedAppName stringByReplacingOccurrencesOfString:@"\\" withString:@"-"]; + _snapshotController = [[FBSnapshotTestController alloc] initWithTestName:sanitizedAppName]; + _snapshotController.referenceImagesDirectory = referenceDir; _script = [NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]; } return self; } -- (void)runTest:(NSString *)moduleName +- (void)setRecordMode:(BOOL)recordMode +{ + _snapshotController.recordMode = recordMode; +} + +- (BOOL)recordMode { - [self runTest:moduleName initialProps:nil expectErrorBlock:nil]; + return _snapshotController.recordMode; } -- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)errorRegex +- (void)runTest:(SEL)test module:(NSString *)moduleName { - [self runTest:moduleName initialProps:initialProps expectErrorBlock:^BOOL(NSString *error){ + [self runTest:test module:moduleName initialProps:nil expectErrorBlock:nil]; +} + +- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)errorRegex +{ + [self runTest:test module:moduleName initialProps:initialProps expectErrorBlock:^BOOL(NSString *error){ return [errorRegex numberOfMatchesInString:error options:0 range:NSMakeRange(0, [error length])] > 0; }]; } -- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock +- (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock { - RCTTestModule *testModule = [[RCTTestModule alloc] init]; - RCTRootView *rootView = [[RCTRootView alloc] init]; UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; - vc.view = rootView; + if ([vc.view isKindOfClass:[RCTRootView class]]) { + [(RCTRootView *)vc.view invalidate]; // Make sure the normal app view doesn't interfere + } + vc.view = [[UIView alloc] init]; + RCTRootView *rootView = [[RCTRootView alloc] initWithFrame:CGRectMake(0, 0, 320, 2000)]; // Constant size for testing on multiple devices + RCTTestModule *testModule = [[RCTTestModule alloc] initWithSnapshotController:_snapshotController view:rootView]; + testModule.testSelector = test; + [vc.view addSubview:rootView]; // Add as subview so it doesn't get resized rootView.moduleProvider = ^(void){ return @[testModule]; }; @@ -58,9 +81,13 @@ - (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; error = [[RCTRedBox sharedInstance] currentErrorMessage]; } + [rootView invalidate]; + [rootView removeFromSuperview]; + RCTAssert(vc.view.subviews.count == 0, @"There shouldn't be any other views: %@", vc.view); + vc.view = nil; [[RCTRedBox sharedInstance] dismiss]; if (expectErrorBlock) { - RCTAssert(expectErrorBlock(error), @"Expected an error but got none."); + RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched."); } else if (error) { RCTAssert(error == nil, @"RedBox error: %@", error); } else { diff --git a/Libraries/RCTWebSocketDebugger/SRWebSocket.m b/Libraries/RCTWebSocketDebugger/SRWebSocket.m index d643d63b9e..8cf5b4d74e 100644 --- a/Libraries/RCTWebSocketDebugger/SRWebSocket.m +++ b/Libraries/RCTWebSocketDebugger/SRWebSocket.m @@ -17,6 +17,8 @@ #import "SRWebSocket.h" +#import + #if TARGET_OS_IPHONE #define HAS_ICU #endif @@ -110,14 +112,19 @@ @interface _SRRunLoopThread : NSThread assert(length >= 0); assert(length <= UINT32_MAX); CC_SHA1(bytes, (CC_LONG)length, md); - + NSData *data = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH]; - - if ([data respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { - return [data base64EncodedStringWithOptions:0]; + +#if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0) \ + || (__MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9) + + if (![NSData instancesRespondToSelector:@selector(base64EncodedStringWithOptions:)]) { + return [data base64Encoding]; } - return [data base64Encoding]; +#endif + + return [data base64EncodedStringWithOptions:0]; } @implementation NSData (SRWebSocket) @@ -212,19 +219,19 @@ - (void)_connect; @implementation SRWebSocket { NSInteger _webSocketVersion; - + NSOperationQueue *_delegateOperationQueue; dispatch_queue_t _delegateDispatchQueue; - + dispatch_queue_t _workQueue; NSMutableArray *_consumers; NSInputStream *_inputStream; NSOutputStream *_outputStream; - + NSMutableData *_readBuffer; NSUInteger _readBufferOffset; - + NSMutableData *_outputBuffer; NSUInteger _outputBufferOffset; @@ -233,18 +240,18 @@ @implementation SRWebSocket { size_t _readOpCount; uint32_t _currentStringScanPosition; NSMutableData *_currentFrameData; - + NSString *_closeReason; - + NSString *_secKey; - + BOOL _pinnedCertFound; - + uint8_t _currentReadMaskKey[4]; size_t _currentReadMaskOffset; BOOL _consumerStopped; - + BOOL _closeWhenFinishedWriting; BOOL _failed; @@ -252,18 +259,18 @@ @implementation SRWebSocket { NSURLRequest *_urlRequest; CFHTTPMessageRef _receivedHTTPHeaders; - + BOOL _sentClose; BOOL _didFail; int _closeCode; - + BOOL _isPumping; - + NSMutableSet *_scheduledRunloops; - + // We use this to retain ourselves. __strong SRWebSocket *_selfRetain; - + NSArray *_requestedProtocols; SRIOConsumerPool *_consumerPool; } @@ -287,12 +294,12 @@ - (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; assert(request.URL); _url = request.URL; _urlRequest = request; - + _requestedProtocols = [protocols copy]; - + [self _SR_commonInit]; } - + return self; } @@ -314,39 +321,38 @@ - (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; - (void)_SR_commonInit; { - NSString *scheme = _url.scheme.lowercaseString; assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]); - + if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) { _secure = YES; } - + _readyState = SR_CONNECTING; _consumerStopped = YES; _webSocketVersion = 13; - + _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); - + // Going to set a specific on the queue so we can validate we're on the work queue dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL); - + _delegateDispatchQueue = dispatch_get_main_queue(); sr_dispatch_retain(_delegateDispatchQueue); - + _readBuffer = [[NSMutableData alloc] init]; _outputBuffer = [[NSMutableData alloc] init]; - + _currentFrameData = [[NSMutableData alloc] init]; _consumers = [[NSMutableArray alloc] init]; - + _consumerPool = [[SRIOConsumerPool alloc] init]; - + _scheduledRunloops = [[NSMutableSet alloc] init]; - + [self _initializeStreams]; - + // default handlers } @@ -362,15 +368,15 @@ - (void)dealloc [_inputStream close]; [_outputStream close]; - + sr_dispatch_release(_workQueue); _workQueue = NULL; - + if (_receivedHTTPHeaders) { CFRelease(_receivedHTTPHeaders); _receivedHTTPHeaders = NULL; } - + if (_delegateDispatchQueue) { sr_dispatch_release(_delegateDispatchQueue); _delegateDispatchQueue = NULL; @@ -499,17 +505,24 @@ - (void)didConnect { SRFastLog(@"Connected"); CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1); - + // Set host first so it defaults CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host)); - + NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16]; SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes); - if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { - _secKey = [keyBytes base64EncodedStringWithOptions:0]; - } else { +#if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0) \ + || (__MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9) + + if (![NSData instancesRespondToSelector:@selector(base64EncodedStringWithOptions:)]) { _secKey = [keyBytes base64Encoding]; + } else + +#endif + + { + _secKey = [keyBytes base64EncodedStringWithOptions:0]; } assert([_secKey length] == 24); diff --git a/Libraries/RKBackendNode/queryLayoutByID.js b/Libraries/RKBackendNode/queryLayoutByID.js index af02e87486..dcb74e4744 100644 --- a/Libraries/RKBackendNode/queryLayoutByID.js +++ b/Libraries/RKBackendNode/queryLayoutByID.js @@ -7,12 +7,25 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule queryLayoutByID + * @flow */ 'use strict'; var ReactIOSTagHandles = require('ReactIOSTagHandles'); var RCTUIManager = require('NativeModules').UIManager; +type OnSuccessCallback = ( + left: number, + top: number, + width: number, + height: number, + pageX: number, + pageY: number +) => void + +// I don't know what type error is... +type OnErrorCallback = (error: any) => void + /** * Queries the layout of a view. The layout does not reflect the element as * seen by the user, rather it reflects the position within the layout system, @@ -32,7 +45,11 @@ var RCTUIManager = require('NativeModules').UIManager; * @param {function} onError `func(error)` * @param {function} onSuccess `func(left, top, width, height, pageX, pageY)` */ -var queryLayoutByID = function(rootNodeID, onError, onSuccess) { +var queryLayoutByID = function( + rootNodeID: string, + onError: OnErrorCallback, + onSuccess: OnSuccessCallback +): void { // Native bridge doesn't *yet* surface errors. RCTUIManager.measure( ReactIOSTagHandles.rootNodeIDToTag[rootNodeID], diff --git a/Libraries/ReactIOS/ReactIOSGlobalInteractionHandler.js b/Libraries/ReactIOS/ReactIOSGlobalInteractionHandler.js index 315543529f..568acab8a1 100644 --- a/Libraries/ReactIOS/ReactIOSGlobalInteractionHandler.js +++ b/Libraries/ReactIOS/ReactIOSGlobalInteractionHandler.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactIOSGlobalInteractionHandler + * @flow */ 'use strict'; @@ -17,7 +18,7 @@ var InteractionManager = require('InteractionManager'); var interactionHandle = null; var ReactIOSGlobalInteractionHandler = { - onChange: function(numberActiveTouches) { + onChange: function(numberActiveTouches: number) { if (numberActiveTouches === 0) { if (interactionHandle) { InteractionManager.clearInteractionHandle(interactionHandle); diff --git a/Libraries/ReactIOS/ReactIOSGlobalResponderHandler.js b/Libraries/ReactIOS/ReactIOSGlobalResponderHandler.js index e9c33f212f..e5c16d05a0 100644 --- a/Libraries/ReactIOS/ReactIOSGlobalResponderHandler.js +++ b/Libraries/ReactIOS/ReactIOSGlobalResponderHandler.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactIOSGlobalResponderHandler + * @flow */ 'use strict'; @@ -14,7 +15,7 @@ var RCTUIManager = require('NativeModules').UIManager; var ReactIOSTagHandles = require('ReactIOSTagHandles'); var ReactIOSGlobalResponderHandler = { - onChange: function(from, to) { + onChange: function(from: string, to: string) { if (to !== null) { RCTUIManager.setJSResponder( ReactIOSTagHandles.mostRecentMountedNodeHandleForRootNodeID(to) diff --git a/Libraries/ReactIOS/ReactIOSMount.js b/Libraries/ReactIOS/ReactIOSMount.js index 9a9e3aaa3b..7e4094408f 100644 --- a/Libraries/ReactIOS/ReactIOSMount.js +++ b/Libraries/ReactIOS/ReactIOSMount.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactIOSMount + * @flow */ 'use strict'; @@ -83,7 +84,10 @@ var ReactIOSMount = { * @param {ReactComponent} instance Instance to render. * @param {containerTag} containerView Handle to native view tag */ - renderComponent: function(descriptor, containerTag) { + renderComponent: function( + descriptor: ReactComponent, + containerTag: number + ) { var instance = instantiateReactComponent(descriptor); if (!ReactIOSTagHandles.reactTagIsNativeTopRootID(containerTag)) { @@ -152,7 +156,9 @@ var ReactIOSMount = { * asynchronously, it's easier to just have this method be the one that calls * for removal of the view. */ - unmountComponentAtNodeAndRemoveContainer: function(containerTag) { + unmountComponentAtNodeAndRemoveContainer: function( + containerTag: number + ) { ReactIOSMount.unmountComponentAtNode(containerTag); // call back into native to remove all of the subviews from this container RCTUIManager.removeRootView(containerTag); @@ -163,7 +169,7 @@ var ReactIOSMount = { * that has been rendered and unmounting it. There should just be one child * component at this time. */ - unmountComponentAtNode: function(containerTag) { + unmountComponentAtNode: function(containerTag: number): bool { var containerID = ReactIOSTagHandles.tagToRootNodeID[containerTag]; invariant( @@ -185,20 +191,25 @@ var ReactIOSMount = { * Unmounts a component and sends messages back to iOS to remove its subviews. * * @param {ReactComponent} instance React component instance. - * @param {int} containerID ID of container we're removing from. + * @param {string} containerID ID of container we're removing from. * @final * @internal * @see {ReactIOSMount.unmountComponentAtNode} */ - unmountComponentFromNode: function(instance, containerID) { + unmountComponentFromNode: function( + instance: ReactComponent, + containerID: string + ) { // call back into native to remove all of the subviews from this container - instance.unmountComponent(); + // TODO: ReactComponent.prototype.unmountComponent is missing from Flow's + // react lib. + (instance: any).unmountComponent(); var containerTag = ReactIOSTagHandles.mostRecentMountedNodeHandleForRootNodeID(containerID); RCTUIManager.removeSubviewsFromContainerWithID(containerTag); }, - getNode: function(id) { + getNode: function(id: T): T { return id; } }; diff --git a/Libraries/ReactIOS/ReactIOSNativeComponent.js b/Libraries/ReactIOS/ReactIOSNativeComponent.js index 1bc6f39ed3..b9abd5965c 100644 --- a/Libraries/ReactIOS/ReactIOSNativeComponent.js +++ b/Libraries/ReactIOS/ReactIOSNativeComponent.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactIOSNativeComponent + * @flow */ 'use strict'; @@ -28,13 +29,20 @@ var registrationNames = ReactIOSEventEmitter.registrationNames; var putListener = ReactIOSEventEmitter.putListener; var deleteAllListeners = ReactIOSEventEmitter.deleteAllListeners; +type ReactIOSNativeComponentViewConfig = { + validAttributes: Object; + uiViewClassName: string; +} + /** * @constructor ReactIOSNativeComponent * @extends ReactComponent * @extends ReactMultiChild * @param {!object} UIKit View Configuration. */ -var ReactIOSNativeComponent = function(viewConfig) { +var ReactIOSNativeComponent = function( + viewConfig: ReactIOSNativeComponentViewConfig +) { this.viewConfig = viewConfig; }; diff --git a/Libraries/ReactIOS/ReactIOSReconcileTransaction.js b/Libraries/ReactIOS/ReactIOSReconcileTransaction.js index 788e82e2cf..229b1ba67b 100644 --- a/Libraries/ReactIOS/ReactIOSReconcileTransaction.js +++ b/Libraries/ReactIOS/ReactIOSReconcileTransaction.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactIOSReconcileTransaction - * @typechecks static-only + * @flow */ "use strict"; diff --git a/Libraries/ReactIOS/ReactIOSStyleAttributes.js b/Libraries/ReactIOS/ReactIOSStyleAttributes.js index 921e13bf7c..b332bec216 100644 --- a/Libraries/ReactIOS/ReactIOSStyleAttributes.js +++ b/Libraries/ReactIOS/ReactIOSStyleAttributes.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactIOSStyleAttributes + * @flow */ "use strict"; diff --git a/Libraries/ReactIOS/ReactIOSTagHandles.js b/Libraries/ReactIOS/ReactIOSTagHandles.js index fcc015ad3d..7ef7d83296 100644 --- a/Libraries/ReactIOS/ReactIOSTagHandles.js +++ b/Libraries/ReactIOS/ReactIOSTagHandles.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactIOSTagHandles + * @flow */ 'use strict'; @@ -31,7 +32,7 @@ var ReactIOSTagHandles = { tagsStartAt: INITIAL_TAG_COUNT, tagCount: INITIAL_TAG_COUNT, - allocateTag: function() { + allocateTag: function(): number { // Skip over root IDs as those are reserved for native while (this.reactTagIsNativeTopRootID(ReactIOSTagHandles.tagCount)) { ReactIOSTagHandles.tagCount++; @@ -50,13 +51,18 @@ var ReactIOSTagHandles = { * `unmountComponent` isn't the correct time because that doesn't imply that * the native node has been natively unmounted. */ - associateRootNodeIDWithMountedNodeHandle: function(rootNodeID, tag) { + associateRootNodeIDWithMountedNodeHandle: function( + rootNodeID: ?string, + tag: ?number + ) { warning(rootNodeID && tag, 'Root node or tag is null when associating'); - ReactIOSTagHandles.tagToRootNodeID[tag] = rootNodeID; - ReactIOSTagHandles.rootNodeIDToTag[rootNodeID] = tag; + if (rootNodeID && tag) { + ReactIOSTagHandles.tagToRootNodeID[tag] = rootNodeID; + ReactIOSTagHandles.rootNodeIDToTag[rootNodeID] = tag; + } }, - allocateRootNodeIDForTag: function(tag) { + allocateRootNodeIDForTag: function(tag: number): string { invariant( this.reactTagIsNativeTopRootID(tag), 'Expect a native root tag, instead got ', tag @@ -64,7 +70,7 @@ var ReactIOSTagHandles = { return '.r[' + tag + ']{TOP_LEVEL}'; }, - reactTagIsNativeTopRootID: function(reactTag) { + reactTagIsNativeTopRootID: function(reactTag: number): bool { // We reserve all tags that are 1 mod 10 for native root views return reactTag % 10 === 1; }, @@ -81,13 +87,15 @@ var ReactIOSTagHandles = { * @return {number} Tag ID of native view for most recent mounting of * `rootNodeID`. */ - mostRecentMountedNodeHandleForRootNodeID: function(rootNodeID) { + mostRecentMountedNodeHandleForRootNodeID: function( + rootNodeID: string + ): number { return ReactIOSTagHandles.rootNodeIDToTag[rootNodeID]; }, - tagToRootNodeID: [], + tagToRootNodeID: ([] : Array), - rootNodeIDToTag: {} + rootNodeIDToTag: ({} : {[key: string]: number}) }; module.exports = ReactIOSTagHandles; diff --git a/Libraries/ReactIOS/ReactIOSViewAttributes.js b/Libraries/ReactIOS/ReactIOSViewAttributes.js index fc000b54c6..069f00b249 100644 --- a/Libraries/ReactIOS/ReactIOSViewAttributes.js +++ b/Libraries/ReactIOS/ReactIOSViewAttributes.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactIOSViewAttributes + * @flow */ "use strict"; diff --git a/Libraries/ReactIOS/renderApplication.js b/Libraries/ReactIOS/renderApplication.js index 064c264996..1d880653e7 100644 --- a/Libraries/ReactIOS/renderApplication.js +++ b/Libraries/ReactIOS/renderApplication.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule renderApplication + * @flow */ 'use strict'; @@ -14,7 +15,11 @@ var React = require('React'); var invariant = require('invariant'); -function renderApplication(RootComponent, initialProps, rootTag) { +function renderApplication( + RootComponent: ReactClass, + initialProps: P, + rootTag: any +) { invariant( rootTag, 'Expect to have a valid rootTag, instead got ', rootTag diff --git a/Libraries/StyleSheet/EdgeInsetsPropType.js b/Libraries/StyleSheet/EdgeInsetsPropType.js index 9f39da9aac..3089fe7bd0 100644 --- a/Libraries/StyleSheet/EdgeInsetsPropType.js +++ b/Libraries/StyleSheet/EdgeInsetsPropType.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule EdgeInsetsPropType + * @flow */ 'use strict' diff --git a/Libraries/StyleSheet/LayoutPropTypes.js b/Libraries/StyleSheet/LayoutPropTypes.js index c872ad8a8f..df88ab8a06 100644 --- a/Libraries/StyleSheet/LayoutPropTypes.js +++ b/Libraries/StyleSheet/LayoutPropTypes.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule LayoutPropTypes + * @flow */ 'use strict'; diff --git a/Libraries/StyleSheet/PointPropType.js b/Libraries/StyleSheet/PointPropType.js index 1f6028b602..1e8fe95e91 100644 --- a/Libraries/StyleSheet/PointPropType.js +++ b/Libraries/StyleSheet/PointPropType.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule PointPropType + * @flow */ 'use strict' diff --git a/Libraries/StyleSheet/StyleSheet.js b/Libraries/StyleSheet/StyleSheet.js index c4285d68b5..6a8ccb60da 100644 --- a/Libraries/StyleSheet/StyleSheet.js +++ b/Libraries/StyleSheet/StyleSheet.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule StyleSheet + * @flow */ 'use strict'; @@ -58,7 +59,7 @@ var StyleSheetValidation = require('StyleSheetValidation'); * subsequent uses are going to refer an id (not implemented yet). */ class StyleSheet { - static create(obj) { + static create(obj: {[key: string]: any}): {[key: string]: number} { var result = {}; for (var key in obj) { StyleSheetValidation.validateStyle(key, obj); diff --git a/Libraries/StyleSheet/StyleSheetPropType.js b/Libraries/StyleSheet/StyleSheetPropType.js index 12f43e75bd..5980b8e387 100644 --- a/Libraries/StyleSheet/StyleSheetPropType.js +++ b/Libraries/StyleSheet/StyleSheetPropType.js @@ -7,15 +7,18 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule StyleSheetPropType + * @flow */ 'use strict'; var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker'); var flattenStyle = require('flattenStyle'); -function StyleSheetPropType(shape) { +function StyleSheetPropType( + shape: {[key: string]: ReactPropsCheckType} +): ReactPropsCheckType { var shapePropType = createStrictShapeTypeChecker(shape); - return function(props, propName, componentName, location) { + return function(props, propName, componentName, location?) { var newProps = props; if (props[propName]) { // Just make a dummy prop object with only the flattened style diff --git a/Libraries/StyleSheet/StyleSheetRegistry.js b/Libraries/StyleSheet/StyleSheetRegistry.js index 0791e029af..a05254fd2c 100644 --- a/Libraries/StyleSheet/StyleSheetRegistry.js +++ b/Libraries/StyleSheet/StyleSheetRegistry.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule StyleSheetRegistry + * @flow */ 'use strict'; @@ -15,7 +16,7 @@ var uniqueID = 1; var emptyStyle = {}; class StyleSheetRegistry { - static registerStyle(style) { + static registerStyle(style: Object): number { var id = ++uniqueID; if (__DEV__) { Object.freeze(style); @@ -24,7 +25,7 @@ class StyleSheetRegistry { return id; } - static getStyleByID(id) { + static getStyleByID(id: number): Object { if (!id) { // Used in the style={[condition && id]} pattern, // we want it to be a no-op when the value is false or null diff --git a/Libraries/StyleSheet/StyleSheetValidation.js b/Libraries/StyleSheet/StyleSheetValidation.js index 3757cdea65..c4875222d8 100644 --- a/Libraries/StyleSheet/StyleSheetValidation.js +++ b/Libraries/StyleSheet/StyleSheetValidation.js @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule StyleSheetValidation + * @flow */ 'use strict'; @@ -60,7 +61,7 @@ class StyleSheetValidation { } } -var styleError = function(message1, style, caller, message2) { +var styleError = function(message1, style, caller?, message2?) { invariant( false, message1 + '\n' + (caller || '<>') + ': ' + diff --git a/Libraries/StyleSheet/flattenStyle.js b/Libraries/StyleSheet/flattenStyle.js index 77fdb129ae..a18bce9887 100644 --- a/Libraries/StyleSheet/flattenStyle.js +++ b/Libraries/StyleSheet/flattenStyle.js @@ -7,12 +7,17 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule flattenStyle + * @flow */ 'use strict'; var StyleSheetRegistry = require('StyleSheetRegistry'); +var invariant = require('invariant'); var mergeIntoFast = require('mergeIntoFast'); +type Atom = number | bool | Object | Array +type StyleObj = Atom | Array + function getStyle(style) { if (typeof style === 'number') { return StyleSheetRegistry.getStyleByID(style); @@ -20,10 +25,14 @@ function getStyle(style) { return style; } -function flattenStyle(style) { +// TODO: Flow 0.7.0 doesn't refine bools properly so we have to use `any` to +// tell it that this can't be a bool anymore. Should be fixed in 0.8.0, +// after which this can take a ?StyleObj. +function flattenStyle(style: any): ?Object { if (!style) { return undefined; } + invariant(style !== true, 'style may be false but not true'); if (!Array.isArray(style)) { return getStyle(style); diff --git a/Libraries/StyleSheet/styleDiffer.js b/Libraries/StyleSheet/styleDiffer.js index d80b014360..d2e8049361 100644 --- a/Libraries/StyleSheet/styleDiffer.js +++ b/Libraries/StyleSheet/styleDiffer.js @@ -7,16 +7,17 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule styleDiffer + * @flow */ 'use strict'; var deepDiffer = require('deepDiffer'); -function styleDiffer(a, b) { +function styleDiffer(a: any, b: any): bool { return !styleEqual(a, b); } -function styleEqual(a, b) { +function styleEqual(a: any, b: any): bool { if (!a) { return !b; } diff --git a/Libraries/Text/RCTText.h b/Libraries/Text/RCTText.h index 6f69cd8523..bb9fb6dfcb 100644 --- a/Libraries/Text/RCTText.h +++ b/Libraries/Text/RCTText.h @@ -15,6 +15,4 @@ @property (nonatomic, assign) NSLineBreakMode lineBreakMode; @property (nonatomic, assign) NSUInteger numberOfLines; -- (NSNumber *)reactTagAtPoint:(CGPoint)point; - @end diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index ab4d01607e..ce7b4078e9 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule Text - * @typechecks static-only + * @flow */ 'use strict'; @@ -102,7 +102,7 @@ var Text = React.createClass({ }); }, - onStartShouldSetResponder: function() { + onStartShouldSetResponder: function(): bool { var shouldSetFromProps = this.props.onStartShouldSetResponder && this.props.onStartShouldSetResponder(); return shouldSetFromProps || !!this.props.onPress; @@ -111,7 +111,7 @@ var Text = React.createClass({ /* * Returns true to allow responder termination */ - handleResponderTerminationRequest: function() { + handleResponderTerminationRequest: function(): bool { // Allow touchable or props.onResponderTerminationRequest to deny // the request var allowTermination = this.touchableHandleResponderTerminationRequest(); @@ -121,25 +121,25 @@ var Text = React.createClass({ return allowTermination; }, - handleResponderGrant: function(e, dispatchID) { + handleResponderGrant: function(e: SyntheticEvent, dispatchID: string) { this.touchableHandleResponderGrant(e, dispatchID); this.props.onResponderGrant && this.props.onResponderGrant.apply(this, arguments); }, - handleResponderMove: function(e) { + handleResponderMove: function(e: SyntheticEvent) { this.touchableHandleResponderMove(e); this.props.onResponderMove && this.props.onResponderMove.apply(this, arguments); }, - handleResponderRelease: function(e) { + handleResponderRelease: function(e: SyntheticEvent) { this.touchableHandleResponderRelease(e); this.props.onResponderRelease && this.props.onResponderRelease.apply(this, arguments); }, - handleResponderTerminate: function(e) { + handleResponderTerminate: function(e: SyntheticEvent) { this.touchableHandleResponderTerminate(e); this.props.onResponderTerminate && this.props.onResponderTerminate.apply(this, arguments); @@ -167,7 +167,7 @@ var Text = React.createClass({ this.props.onPress && this.props.onPress(); }, - touchableGetPressRectOffset: function() { + touchableGetPressRectOffset: function(): RectOffset { return PRESS_RECT_OFFSET; }, @@ -193,6 +193,13 @@ var Text = React.createClass({ }, }); +type RectOffset = { + top: number; + left: number; + right: number; + bottom: number; +} + var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; var RCTText = createReactIOSNativeComponentClass(viewConfig); diff --git a/Libraries/Utilities/Backstack.ios.js b/Libraries/Utilities/Backstack.ios.js new file mode 100644 index 0000000000..00a538668f --- /dev/null +++ b/Libraries/Utilities/Backstack.ios.js @@ -0,0 +1,16 @@ +/** + * To lower the risk of breaking things on iOS, we are stubbing out the + * BackStack for now. See Backstack.android.js + * + * @providesModule Backstack + */ + +'use strict'; + +var Backstack = { + pushNavigation: () => {}, + resetToBefore: () => {}, + removeComponentHistory: () => {}, +}; + +module.exports = Backstack; diff --git a/Libraries/Utilities/CSSVarConfig.js b/Libraries/Utilities/CSSVarConfig.js new file mode 100644 index 0000000000..cb018be0af --- /dev/null +++ b/Libraries/Utilities/CSSVarConfig.js @@ -0,0 +1,62 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CSSVarConfig + */ +'use strict'; + +// this a partial list of the contants in CSSConstants:: from PHP that are applicable to mobile + +module.exports = { + 'fbui-accent-blue': '#5890ff', + 'fbui-blue-90': '#4e69a2', + 'fbui-blue-80': '#627aad', + 'fbui-blue-70': '#758ab7', + 'fbui-blue-60': '#899bc1', + 'fbui-blue-50': '#9daccb', + 'fbui-blue-40': '#b1bdd6', + 'fbui-blue-30': '#c4cde0', + 'fbui-blue-20': '#d8deea', + 'fbui-blue-10': '#ebeef4', + 'fbui-blue-5': '#f5f7fa', + 'fbui-blue-2': '#fbfcfd', + 'fbui-blueblack-90': '#06090f', + 'fbui-blueblack-80': '#0c121e', + 'fbui-blueblack-70': '#121b2e', + 'fbui-blueblack-60': '#18243d', + 'fbui-blueblack-50': '#1e2d4c', + 'fbui-blueblack-40': '#23355b', + 'fbui-blueblack-30': '#293e6b', + 'fbui-blueblack-20': '#2f477a', + 'fbui-blueblack-10': '#355089', + 'fbui-blueblack-5': '#385490', + 'fbui-blueblack-2': '#3a5795', + 'fbui-bluegray-90': '#080a10', + 'fbui-bluegray-80': '#141823', + 'fbui-bluegray-70': '#232937', + 'fbui-bluegray-60': '#373e4d', + 'fbui-bluegray-50': '#4e5665', + 'fbui-bluegray-40': '#6a7180', + 'fbui-bluegray-30': '#9197a3', + 'fbui-bluegray-20': '#bdc1c9', + 'fbui-bluegray-10': '#dcdee3', + 'fbui-bluegray-5': '#e9eaed', + 'fbui-bluegray-2': '#f6f7f8', + 'fbui-gray-90': '#191919', + 'fbui-gray-80': '#333333', + 'fbui-gray-70': '#4c4c4c', + 'fbui-gray-60': '#666666', + 'fbui-gray-50': '#7f7f7f', + 'fbui-gray-40': '#999999', + 'fbui-gray-30': '#b2b2b2', + 'fbui-gray-20': '#cccccc', + 'fbui-gray-10': '#e5e5e5', + 'fbui-gray-5': '#f2f2f2', + 'fbui-gray-2': '#fafafa', + 'fbui-red': '#da2929', + 'fbui-error': '#ce0d24', + 'x-mobile-dark-text': '#4e5665', + 'x-mobile-medium-text': '#6a7180', + 'x-mobile-light-text': '#9197a3', + 'x-mobile-base-wash': '#dcdee3', +}; diff --git a/Libraries/Utilities/TimerMixin.js b/Libraries/Utilities/TimerMixin.js deleted file mode 100644 index 0fc68457d6..0000000000 --- a/Libraries/Utilities/TimerMixin.js +++ /dev/null @@ -1,109 +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 TimerMixin - * @flow - */ -'use strict'; - -var setImmediate = require('setImmediate'); -var clearImmediate = require('clearImmediate'); - -/** - * Using bare setTimeout, setInterval, setImmediate and - * requestAnimationFrame calls is very dangerous because if you forget to cancel - * the request before the component is unmounted, you risk the callback throwing - * an exception. - * - * If you include TimerMixin, then you can replace your calls - * to `setTimeout(fn, 500)` - * with `this.setTimeout(fn, 500)` (just prepend `this.`) - * and everything will be properly cleaned up for you. - * - * Example: - * - * var Component = React.createClass({ - * mixins: [TimerMixin], - * componentDidMount: function() { - * this.setTimeout( - * () => { console.log('I do not leak!'); }, - * 500 - * ); - * } - * }); - */ - - var setter = function(setter, clearer, array) { - return function( - callback: () => void, - delta: number - ): number { - var id = setter(() => { - clearer.call(this, id); - callback.apply(this, arguments); - }, delta); - - if (!this[array]) { - this[array] = [id]; - } else { - this[array].push(id); - } - return id; - }; - }; - - var clearer = function(clearer, array) { - return function(id: number) { - if (this[array]) { - var index = this[array].indexOf(id); - if (index !== -1) { - this[array].splice(index, 1); - } - } - clearer(id); - }; - }; - - var _timeouts = 'TimerMixin_timeouts'; - var _clearTimeout = clearer(clearTimeout, _timeouts); - var _setTimeout = setter(setTimeout, _clearTimeout, _timeouts); - - var _intervals = 'TimerMixin_intervals'; - var _clearInterval = clearer(clearInterval, _intervals); - var _setInterval = setter(setInterval, () => {/* noop */}, _intervals); - - var _immediates = 'TimerMixin_immediates'; - var _clearImmediate = clearer(clearImmediate, _immediates); - var _setImmediate = setter(setImmediate, _clearImmediate, _immediates); - - var _rafs = 'TimerMixin_rafs'; - var _cancelAnimationFrame = clearer(window.cancelAnimationFrame, _rafs); - var _requestAnimationFrame = setter(window.requestAnimationFrame, _cancelAnimationFrame, _rafs); - -var TimerMixin = { - componentWillUnmount: function() { - this[_timeouts] && this[_timeouts].forEach(this.clearTimeout); - this[_intervals] && this[_intervals].forEach(this.clearInterval); - this[_immediates] && this[_immediates].forEach(this.clearImmediate); - this[_rafs] && this[_rafs].forEach(this.cancelAnimationFrame); - }, - - setTimeout: _setTimeout, - clearTimeout: _clearTimeout, - - setInterval: _setInterval, - clearInterval: _clearInterval, - - setImmediate: _setImmediate, - clearImmediate: _clearImmediate, - - requestAnimationFrame: _requestAnimationFrame, - cancelAnimationFrame: _cancelAnimationFrame, -}; - -module.exports = TimerMixin; diff --git a/Libraries/Utilities/buildStyleInterpolator.js b/Libraries/Utilities/buildStyleInterpolator.js new file mode 100644 index 0000000000..67f07cb415 --- /dev/null +++ b/Libraries/Utilities/buildStyleInterpolator.js @@ -0,0 +1,559 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule buildStyleInterpolator + */ + +/** + * Cannot "use strict" because we must use eval in this file. + */ + +var keyOf = require('keyOf'); + +var X_DIM = keyOf({x: null}); +var Y_DIM = keyOf({y: null}); +var Z_DIM = keyOf({z: null}); +var W_DIM = keyOf({w: null}); + +var TRANSFORM_ROTATE_NAME = keyOf({transformRotateRadians: null}); + +var ShouldAllocateReusableOperationVars = { + transformRotateRadians: true, + transformScale: true, + transformTranslate: true, +}; + +var InitialOperationField = { + transformRotateRadians: [0, 0, 0, 1], + transformTranslate: [0, 0, 0], + transformScale: [1, 1, 1], +}; + + +/** + * Creates a highly specialized animation function that may be evaluated every + * frame. For example: + * + * var ToTheLeft = { + * opacity: { + * from: 1, + * to: 0.7, + * min: 0, + * max: 1, + * type: 'linear', + * extrapolate: false, + * round: 100, + * }, + * left: { + * from: 0, + * to: -SCREEN_WIDTH * 0.3, + * min: 0, + * max: 1, + * type: 'linear', + * extrapolate: true, + * round: PixelRatio.get(), + * }, + * }; + * + * var toTheLeft = buildStyleInterpolator(ToTheLeft); + * + * Would returns a specialized function of the form: + * + * function(result, value) { + * var didChange = false; + * var nextScalarVal; + * var ratio; + * ratio = (value - 0) / 1; + * ratio = ratio > 1 ? 1 : (ratio < 0 ? 0 : ratio); + * nextScalarVal = Math.round(100 * (1 * (1 - ratio) + 0.7 * ratio)) / 100; + * if (!didChange) { + * var prevVal = result.opacity; + * result.opacity = nextScalarVal; + * didChange = didChange || (nextScalarVal !== prevVal); + * } else { + * result.opacity = nextScalarVal; + * } + * ratio = (value - 0) / 1; + * nextScalarVal = Math.round(2 * (0 * (1 - ratio) + -30 * ratio)) / 2; + * if (!didChange) { + * var prevVal = result.left; + * result.left = nextScalarVal; + * didChange = didChange || (nextScalarVal !== prevVal); + * } else { + * result.left = nextScalarVal; + * } + * return didChange; + * } + */ + +var ARGUMENT_NAMES_RE = /([^\s,]+)/g; +/** + * This is obviously a huge hack. Proper tooling would allow actual inlining. + * This only works in a few limited cases (where there is no function return + * value, and the function operates mutatively on parameters). + * + * Example: + * + * + * var inlineMe(a, b) { + * a = b + b; + * }; + * + * inline(inlineMe, ['hi', 'bye']); // "hi = bye + bye;" + * + * @param {function} func Any simple function whos arguments can be replaced via a regex. + * @param {array} replaceWithArgs Corresponding names of variables + * within an environment, to replace `func` args with. + * @return {string} Resulting function body string. + */ +var inline = function(func, replaceWithArgs) { + var fnStr = func.toString(); + var parameterNames = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')) + .match(ARGUMENT_NAMES_RE) || + []; + var replaceRegexStr = parameterNames.map(function(paramName) { + return '\\b' + paramName + '\\b'; + }).join('|'); + var replaceRegex = new RegExp(replaceRegexStr, 'g'); + var fnBody = fnStr.substring(fnStr.indexOf('{') + 1, fnStr.lastIndexOf('}') - 1); + var newFnBody = fnBody.replace(replaceRegex, function(parameterName) { + var indexInParameterNames = parameterNames.indexOf(parameterName); + var replacementName = replaceWithArgs[indexInParameterNames]; + return replacementName; + }); + return newFnBody.split('\n'); +}; + +/** + * Simply a convenient way to inline functions using the function's toString + * method. + */ +var MatrixOps = { + unroll: function(matVar, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) { + m0 = matVar[0]; + m1 = matVar[1]; + m2 = matVar[2]; + m3 = matVar[3]; + m4 = matVar[4]; + m5 = matVar[5]; + m6 = matVar[6]; + m7 = matVar[7]; + m8 = matVar[8]; + m9 = matVar[9]; + m10 = matVar[10]; + m11 = matVar[11]; + m12 = matVar[12]; + m13 = matVar[13]; + m14 = matVar[14]; + m15 = matVar[15]; + }, + + matrixDiffers: function(retVar, matVar, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) { + retVar = retVar || + m0 !== matVar[0] || + m1 !== matVar[1] || + m2 !== matVar[2] || + m3 !== matVar[3] || + m4 !== matVar[4] || + m5 !== matVar[5] || + m6 !== matVar[6] || + m7 !== matVar[7] || + m8 !== matVar[8] || + m9 !== matVar[9] || + m10 !== matVar[10] || + m11 !== matVar[11] || + m12 !== matVar[12] || + m13 !== matVar[13] || + m14 !== matVar[14] || + m15 !== matVar[15]; + }, + + transformScale: function(matVar, opVar) { + // Scaling matVar by opVar + var x = opVar[0]; + var y = opVar[1]; + var z = opVar[2]; + matVar[0] = matVar[0] * x; + matVar[1] = matVar[1] * x; + matVar[2] = matVar[2] * x; + matVar[3] = matVar[3] * x; + matVar[4] = matVar[4] * y; + matVar[5] = matVar[5] * y; + matVar[6] = matVar[6] * y; + matVar[7] = matVar[7] * y; + matVar[8] = matVar[8] * z; + matVar[9] = matVar[9] * z; + matVar[10] = matVar[10] * z; + matVar[11] = matVar[11] * z; + matVar[12] = matVar[12]; + matVar[13] = matVar[13]; + matVar[14] = matVar[14]; + matVar[15] = matVar[15]; + }, + + /** + * All of these matrix transforms are not general purpose utilities, and are + * only suitable for being inlined for the use of building up interpolators. + */ + transformTranslate: function(matVar, opVar) { + // Translating matVar by opVar + var x = opVar[0]; + var y = opVar[1]; + var z = opVar[2]; + matVar[12] = matVar[0] * x + matVar[4] * y + matVar[8] * z + matVar[12]; + matVar[13] = matVar[1] * x + matVar[5] * y + matVar[9] * z + matVar[13]; + matVar[14] = matVar[2] * x + matVar[6] * y + matVar[10] * z + matVar[14]; + matVar[15] = matVar[3] * x + matVar[7] * y + matVar[11] * z + matVar[15]; + }, + + /** + * @param {array} matVar Both the input, and the output matrix. + * @param {quaternion specification} q Four element array describing rotation. + */ + transformRotateRadians: function(matVar, q) { + // Rotating matVar by q + var xQuat = q[0], yQuat = q[1], zQuat = q[2], wQuat = q[3]; + var x2Quat = xQuat + xQuat; + var y2Quat = yQuat + yQuat; + var z2Quat = zQuat + zQuat; + var xxQuat = xQuat * x2Quat; + var xyQuat = xQuat * y2Quat; + var xzQuat = xQuat * z2Quat; + var yyQuat = yQuat * y2Quat; + var yzQuat = yQuat * z2Quat; + var zzQuat = zQuat * z2Quat; + var wxQuat = wQuat * x2Quat; + var wyQuat = wQuat * y2Quat; + var wzQuat = wQuat * z2Quat; + // Step 1: Inlines the construction of a quaternion matrix (`quatMat`) + var quatMat0 = 1 - (yyQuat + zzQuat); + var quatMat1 = xyQuat + wzQuat; + var quatMat2 = xzQuat - wyQuat; + var quatMat4 = xyQuat - wzQuat; + var quatMat5 = 1 - (xxQuat + zzQuat); + var quatMat6 = yzQuat + wxQuat; + var quatMat8 = xzQuat + wyQuat; + var quatMat9 = yzQuat - wxQuat; + var quatMat10 = 1 - (xxQuat + yyQuat); + // quatMat3/7/11/12/13/14 = 0, quatMat15 = 1 + + // Step 2: Inlines multiplication, takes advantage of constant quatMat cells + var a00 = matVar[0]; + var a01 = matVar[1]; + var a02 = matVar[2]; + var a03 = matVar[3]; + var a10 = matVar[4]; + var a11 = matVar[5]; + var a12 = matVar[6]; + var a13 = matVar[7]; + var a20 = matVar[8]; + var a21 = matVar[9]; + var a22 = matVar[10]; + var a23 = matVar[11]; + + var b0 = quatMat0, b1 = quatMat1, b2 = quatMat2; + matVar[0] = b0 * a00 + b1 * a10 + b2 * a20; + matVar[1] = b0 * a01 + b1 * a11 + b2 * a21; + matVar[2] = b0 * a02 + b1 * a12 + b2 * a22; + matVar[3] = b0 * a03 + b1 * a13 + b2 * a23; + b0 = quatMat4; b1 = quatMat5; b2 = quatMat6; + matVar[4] = b0 * a00 + b1 * a10 + b2 * a20; + matVar[5] = b0 * a01 + b1 * a11 + b2 * a21; + matVar[6] = b0 * a02 + b1 * a12 + b2 * a22; + matVar[7] = b0 * a03 + b1 * a13 + b2 * a23; + b0 = quatMat8; b1 = quatMat9; b2 = quatMat10; + matVar[8] = b0 * a00 + b1 * a10 + b2 * a20; + matVar[9] = b0 * a01 + b1 * a11 + b2 * a21; + matVar[10] = b0 * a02 + b1 * a12 + b2 * a22; + matVar[11] = b0 * a03 + b1 * a13 + b2 * a23; + } +}; + +// Optimized version of general operation applications that can be used when +// the target matrix is known to be the identity matrix. +var MatrixOpsInitial = { + transformScale: function(matVar, opVar) { + // Scaling matVar known to be identity by opVar + matVar[0] = opVar[0]; + matVar[1] = 0; + matVar[2] = 0; + matVar[3] = 0; + matVar[4] = 0; + matVar[5] = opVar[1]; + matVar[6] = 0; + matVar[7] = 0; + matVar[8] = 0; + matVar[9] = 0; + matVar[10] = opVar[2]; + matVar[11] = 0; + matVar[12] = 0; + matVar[13] = 0; + matVar[14] = 0; + matVar[15] = 1; + }, + + transformTranslate: function(matVar, opVar) { + // Translating matVar known to be identity by opVar'; + matVar[0] = 1; + matVar[1] = 0; + matVar[2] = 0; + matVar[3] = 0; + matVar[4] = 0; + matVar[5] = 1; + matVar[6] = 0; + matVar[7] = 0; + matVar[8] = 0; + matVar[9] = 0; + matVar[10] = 1; + matVar[11] = 0; + matVar[12] = opVar[0]; + matVar[13] = opVar[1]; + matVar[14] = opVar[2]; + matVar[15] = 1; + }, + + /** + * @param {array} matVar Both the input, and the output matrix - assumed to be + * identity. + * @param {quaternion specification} q Four element array describing rotation. + */ + transformRotateRadians: function(matVar, q) { + + // Rotating matVar which is known to be identity by q + var xQuat = q[0], yQuat = q[1], zQuat = q[2], wQuat = q[3]; + var x2Quat = xQuat + xQuat; + var y2Quat = yQuat + yQuat; + var z2Quat = zQuat + zQuat; + var xxQuat = xQuat * x2Quat; + var xyQuat = xQuat * y2Quat; + var xzQuat = xQuat * z2Quat; + var yyQuat = yQuat * y2Quat; + var yzQuat = yQuat * z2Quat; + var zzQuat = zQuat * z2Quat; + var wxQuat = wQuat * x2Quat; + var wyQuat = wQuat * y2Quat; + var wzQuat = wQuat * z2Quat; + // Step 1: Inlines the construction of a quaternion matrix (`quatMat`) + var quatMat0 = 1 - (yyQuat + zzQuat); + var quatMat1 = xyQuat + wzQuat; + var quatMat2 = xzQuat - wyQuat; + var quatMat4 = xyQuat - wzQuat; + var quatMat5 = 1 - (xxQuat + zzQuat); + var quatMat6 = yzQuat + wxQuat; + var quatMat8 = xzQuat + wyQuat; + var quatMat9 = yzQuat - wxQuat; + var quatMat10 = 1 - (xxQuat + yyQuat); + // quatMat3/7/11/12/13/14 = 0, quatMat15 = 1 + + // Step 2: Inlines the multiplication with identity matrix. + var b0 = quatMat0, b1 = quatMat1, b2 = quatMat2; + matVar[0] = b0; + matVar[1] = b1; + matVar[2] = b2; + matVar[3] = 0; + b0 = quatMat4; b1 = quatMat5; b2 = quatMat6; + matVar[4] = b0; + matVar[5] = b1; + matVar[6] = b2; + matVar[7] = 0; + b0 = quatMat8; b1 = quatMat9; b2 = quatMat10; + matVar[8] = b0; + matVar[9] = b1; + matVar[10] = b2; + matVar[11] = 0; + matVar[12] = 0; + matVar[13] = 0; + matVar[14] = 0; + matVar[15] = 1; + } +}; + + +var setNextValAndDetectChange = function(name, tmpVarName) { + return ( + ' if (!didChange) {\n' + + ' var prevVal = result.' + name +';\n' + + ' result.' + name + ' = ' + tmpVarName + ';\n' + + ' didChange = didChange || (' + tmpVarName + ' !== prevVal);\n' + + ' } else {\n' + + ' result.' + name + ' = ' + tmpVarName + ';\n' + + ' }\n' + ); +}; + +var computeNextValLinear = function(anim, from, to, tmpVarName) { + var hasRoundRatio = 'round' in anim; + var roundRatio = anim.round; + var fn = ' ratio = (value - ' + anim.min + ') / ' + (anim.max - anim.min) + ';\n'; + if (!anim.extrapolate) { + fn += ' ratio = ratio > 1 ? 1 : (ratio < 0 ? 0 : ratio);\n'; + } + + var roundOpen = (hasRoundRatio ? 'Math.round(' + roundRatio + ' * ' : '' ); + var roundClose = (hasRoundRatio ? ') / ' + roundRatio : '' ); + fn += + ' ' + tmpVarName + ' = ' + + roundOpen + + '(' + from + ' * (1 - ratio) + ' + to + ' * ratio)' + + roundClose + ';\n'; + return fn; +}; + +var computeNextValLinearScalar = function(anim) { + return computeNextValLinear(anim, anim.from, anim.to, 'nextScalarVal'); +}; + +var computeNextValConstant = function(anim) { + var constantExpression = JSON.stringify(anim.value); + return ' nextScalarVal = ' + constantExpression + ';\n'; +}; + +var computeNextValStep = function(anim) { + return ( + ' nextScalarVal = value >= ' + + (anim.threshold + ' ? ' + anim.to + ' : ' + anim.from) + ';\n' + ); +}; + +var computeNextValIdentity = function(anim) { + return ' nextScalarVal = value;\n'; +}; + +var operationVar = function(name) { + return name + 'ReuseOp'; +}; + +var createReusableOperationVars = function(anims) { + var ret = ''; + for (var name in anims) { + if (ShouldAllocateReusableOperationVars[name]) { + ret += 'var ' + operationVar(name) + ' = [];\n'; + } + } + return ret; +}; + +var newlines = function(statements) { + return '\n' + statements.join('\n') + '\n'; +}; + +/** + * @param {Animation} anim Configuration entry. + * @param {key} dimension Key to examine in `from`/`to`. + * @param {number} index Field in operationVar to set. + * @return {string} Code that sets the operation variable's field. + */ +var computeNextMatrixOperationField = function(anim, name, dimension, index) { + var fieldAccess = operationVar(name) + '[' + index + ']'; + if (anim.from[dimension] !== undefined && anim.to[dimension] !== undefined) { + return ' ' + anim.from[dimension] !== anim.to[dimension] ? + computeNextValLinear(anim, anim.from[dimension], anim.to[dimension], fieldAccess) : + fieldAccess + ' = ' + anim.from[dimension] + ';'; + } else { + return ' ' + fieldAccess + ' = ' + InitialOperationField[name][index] + ';'; + } +}; + +var unrolledVars = []; +for (var varIndex = 0; varIndex < 16; varIndex++) { + unrolledVars.push('m' + varIndex); +} +var setNextMatrixAndDetectChange = function(orderedMatrixOperations) { + var fn = [ + ' var transformMatrix = result.transformMatrix !== undefined ? ' + + 'result.transformMatrix : (result.transformMatrix = []);' + ]; + fn.push.apply( + fn, + inline(MatrixOps.unroll, ['transformMatrix'].concat(unrolledVars)) + ); + for (var i = 0; i < orderedMatrixOperations.length; i++) { + var opName = orderedMatrixOperations[i]; + if (i === 0) { + fn.push.apply( + fn, + inline(MatrixOpsInitial[opName], ['transformMatrix', operationVar(opName)]) + ); + } else { + fn.push.apply( + fn, + inline(MatrixOps[opName], ['transformMatrix', operationVar(opName)]) + ); + } + } + fn.push.apply( + fn, + inline(MatrixOps.matrixDiffers, ['didChange', 'transformMatrix'].concat(unrolledVars)) + ); + return fn; +}; + +var InterpolateMatrix = { + transformTranslate: true, + transformRotateRadians: true, + transformScale: true, +}; + +var createFunctionString = function(anims) { + // We must track the order they appear in so transforms are applied in the + // correct order. + var orderedMatrixOperations = []; + + // Wrapping function allows the final function to contain state (for + // caching). + var fn = 'return (function() {\n'; + fn += createReusableOperationVars(anims); + fn += 'return function(result, value) {\n'; + fn += ' var didChange = false;\n'; + fn += ' var nextScalarVal;\n'; + fn += ' var ratio;\n'; + + for (var name in anims) { + var anim = anims[name]; + if (anim.type === 'linear') { + if (InterpolateMatrix[name]) { + orderedMatrixOperations.push(name); + var setOperations = [ + computeNextMatrixOperationField(anim, name, X_DIM, 0), + computeNextMatrixOperationField(anim, name, Y_DIM, 1), + computeNextMatrixOperationField(anim, name, Z_DIM, 2) + ]; + if (name === TRANSFORM_ROTATE_NAME) { + setOperations.push(computeNextMatrixOperationField(anim, name, W_DIM, 3)); + } + fn += newlines(setOperations); + } else { + fn += computeNextValLinearScalar(anim, 'nextScalarVal'); + fn += setNextValAndDetectChange(name, 'nextScalarVal'); + } + } else if (anim.type === 'constant') { + fn += computeNextValConstant(anim); + fn += setNextValAndDetectChange(name, 'nextScalarVal'); + } else if (anim.type === 'step') { + fn += computeNextValStep(anim); + fn += setNextValAndDetectChange(name, 'nextScalarVal'); + } else if (anim.type === 'identity') { + fn += computeNextValIdentity(anim); + fn += setNextValAndDetectChange(name, 'nextScalarVal'); + } + } + if (orderedMatrixOperations.length) { + fn += newlines(setNextMatrixAndDetectChange(orderedMatrixOperations)); + } + fn += ' return didChange;\n'; + fn += '};\n'; + fn += '})()'; + return fn; +}; + +/** + * @param {object} anims Animation configuration by style property name. + * @return {function} Function accepting style object, that mutates that style + * object and returns a boolean describing if any update was actually applied. + */ +var buildStyleInterpolator = function(anims) { + return Function(createFunctionString(anims))(); +}; + + +module.exports = buildStyleInterpolator; diff --git a/Libraries/Utilities/cssVar.js b/Libraries/Utilities/cssVar.js new file mode 100644 index 0000000000..0ab64e7f9c --- /dev/null +++ b/Libraries/Utilities/cssVar.js @@ -0,0 +1,18 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule cssVar + * @typechecks + */ +'use strict'; + +var invariant = require('invariant'); +var CSSVarConfig = require('CSSVarConfig'); + +var cssVar = function(/*string*/ key) /*string*/ { + invariant(CSSVarConfig[key], 'invalid css variable ' + key); + + return CSSVarConfig[key]; +}; + +module.exports = cssVar; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 96080d09fa..12112a4bb6 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -25,6 +25,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { ListView: require('ListView'), MapView: require('MapView'), NavigatorIOS: require('NavigatorIOS'), + JSNavigationStack: require('JSNavigationStack'), PickerIOS: require('PickerIOS'), ScrollView: require('ScrollView'), SliderIOS: require('SliderIOS'), @@ -50,9 +51,9 @@ var ReactNative = Object.assign(Object.create(require('React')), { NetInfo: require('NetInfo'), PixelRatio: require('PixelRatio'), PushNotificationIOS: require('PushNotificationIOS'), + PanResponder: require('PanResponder'), StatusBarIOS: require('StatusBarIOS'), StyleSheet: require('StyleSheet'), - TimerMixin: require('TimerMixin'), VibrationIOS: require('VibrationIOS'), // Plugins @@ -60,11 +61,12 @@ var ReactNative = Object.assign(Object.create(require('React')), { NativeModules: require('NativeModules'), addons: { - batchedUpdates: require('ReactUpdates').batchedUpdates, LinkedStateMixin: require('LinkedStateMixin'), Perf: undefined, PureRenderMixin: require('ReactComponentWithPureRenderMixin'), + TestModule: require('NativeModules').TestModule, TestUtils: undefined, + batchedUpdates: require('ReactUpdates').batchedUpdates, cloneWithProps: require('cloneWithProps'), update: require('update'), }, diff --git a/Libraries/vendor/react/browser/eventPlugins/PanResponder.js b/Libraries/vendor/react/browser/eventPlugins/PanResponder.js new file mode 100644 index 0000000000..22bcaeffed --- /dev/null +++ b/Libraries/vendor/react/browser/eventPlugins/PanResponder.js @@ -0,0 +1,275 @@ +/** + * @providesModule PanResponder + */ + +"use strict"; + +var TouchHistoryMath = require('TouchHistoryMath'); + +var currentCentroidXOfTouchesChangedAfter = + TouchHistoryMath.currentCentroidXOfTouchesChangedAfter; +var currentCentroidYOfTouchesChangedAfter = + TouchHistoryMath.currentCentroidYOfTouchesChangedAfter; +var previousCentroidXOfTouchesChangedAfter = + TouchHistoryMath.previousCentroidXOfTouchesChangedAfter; +var previousCentroidYOfTouchesChangedAfter = + TouchHistoryMath.previousCentroidYOfTouchesChangedAfter; +var currentCentroidX = TouchHistoryMath.currentCentroidX; +var currentCentroidY = TouchHistoryMath.currentCentroidY; + +/** + * + * +----------------------------+ +--------------------------------+ + * | ResponderTouchHistoryStore | |TouchHistoryMath | + * +----------------------------+ +----------+---------------------+ + * |Global store of touchHistory| |Allocation-less math util | + * |including activeness, start | |on touch history (centroids | + * |position, prev/cur position.| |and multitouch movement etc) | + * | | | | + * +----^-----------------------+ +----^---------------------------+ + * | | + * | (records relevant history | + * | of touches relevant for | + * | implementing higher level | + * | gestures) | + * | | + * +----+-----------------------+ +----|---------------------------+ + * | ResponderEventPlugin | | | Your App/Component | + * +----------------------------+ +----|---------------------------+ + * |Negotiates which view gets | Low level | | High level | + * |onResponderMove events. | events w/ | +-+-------+ events w/ | + * |Also records history into | touchHistory| | Pan | multitouch + | + * |ResponderTouchHistoryStore. +---------------->Responder+-----> accumulative| + * +----------------------------+ attached to | | | distance and | + * each event | +---------+ velocity. | + * | | + * | | + * +--------------------------------+ + * + * + * + * Gesture that calculates cumulative movement over time in a way that just + * "does the right thing" for multiple touches. The "right thing" is very + * nuanced. When moving two touches in opposite directions, the cumulative + * distance is zero in each dimension. When two touches move in parallel five + * pixels in the same direction, the cumulative distance is five, not ten. If + * two touches start, one moves five in a direction, then stops and the other + * touch moves fives in the same direction, the cumulative distance is ten. + * + * This logic requires a kind of processing of time "clusters" of touch events + * so that two touch moves that essentially occur in parallel but move every + * other frame respectively, are considered part of the same movement. + * + * Explanation of some of the non-obvious fields: + * + * - moveX/moveY: If no move event has been observed, then `(moveX, moveY)` is + * invalid. If a move event has been observed, `(moveX, moveY)` is the + * centroid of the most recently moved "cluster" of active touches. + * (Currently all move have the same timeStamp, but later we should add some + * threshold for what is considered to be "moving"). If a palm is + * accidentally counted as a touch, but a finger is moving greatly, the palm + * will move slightly, but we only want to count the single moving touch. + * - x0/y0: Centroid location (non-cumulative) at the time of becoming + * responder. + * - dx/dy: Cumulative touch distance - not the same thing as sum of each touch + * distance. Accounts for touch moves that are clustered together in time, + * moving the same direction. Only valid when currently responder (otherwise, + * it only represents the drag distance below the threshold). + * - vx/vy: Velocity. + */ +var PanResponder = { + _initializeGestureState: function(gestureState) { + gestureState.moveX = 0; + gestureState.moveY = 0; + gestureState.x0 = 0; + gestureState.y0 = 0; + gestureState.dx = 0; + gestureState.dy = 0; + gestureState.vx = 0; + gestureState.vy = 0; + gestureState.numberActiveTouches = 0; + // All `gestureState` accounts for timeStamps up until: + gestureState._accountsForMovesUpTo = 0; + }, + + /** + * This is nuanced and is necessary. It is incorrect to continuously take all + * active *and* recently moved touches, find the centroid, and track how that + * result changes over time. Instead, we must take all recently moved + * touches, and calculate how the centroid has changed just for those + * recently moved touches, and append that change to an accumulator. This is + * to (at least) handle the case where the user is moving three fingers, and + * then one of the fingers stops but the other two continue. + * + * This is very different than taking all of the recently moved touches and + * storing their centroid as `dx/dy`. For correctness, we must *accumulate + * changes* in the centroid of recently moved touches. + * + * There is also some nuance with how we handle multiple moved touches in a + * single event. With the way `ReactIOSEventEmitter` dispatches touches as + * individual events, multiple touches generate two 'move' events, each of + * them triggering `onResponderMove`. But with the way `PanResponder` works, + * all of the gesture inference is performed on the first dispatch, since it + * looks at all of the touches (even the ones for which there hasn't been a + * native dispatch yet). Therefore, `PanResponder` does not call + * `onResponderMove` passed the first dispatch. This diverges from the + * typical responder callback pattern (without using `PanResponder`), but + * avoids more dispatches than necessary. + */ + _updateGestureStateOnMove: function(gestureState, touchHistory) { + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + gestureState.moveX = currentCentroidXOfTouchesChangedAfter( + touchHistory, + gestureState._accountsForMovesUpTo + ); + gestureState.moveY = currentCentroidYOfTouchesChangedAfter( + touchHistory, + gestureState._accountsForMovesUpTo + ); + var movedAfter = gestureState._accountsForMovesUpTo; + var prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); + var x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); + var prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); + var y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); + var nextDX = gestureState.dx + (x - prevX); + var nextDY = gestureState.dy + (y - prevY); + + // TODO: This must be filtered intelligently. + var dt = + (touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo); + gestureState.vx = (nextDX - gestureState.dx) / dt; + gestureState.vy = (nextDY - gestureState.dy) / dt; + + gestureState.dx = nextDX; + gestureState.dy = nextDY; + gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp; + }, + + /** + * @param {object} config Enhanced versions of all of the responder callbacks + * that accept not only the typical `ResponderSyntheticEvent`, but also the + * `PanResponder` gesture state. Simply replace the word `Responder` with + * `PanResponder` in each of the typical `onResponder*` callbacks. For + * example, the `config` object would look like: + * + * - onMoveShouldSetPanResponder: (e, gestureState) => {...} + * - onMoveShouldSetPanResponderCapture: (e, gestureState) => {...} + * - onStartShouldSetPanResponder: (e, gestureState) => {...} + * - onStartShouldSetPanResponderCapture: (e, gestureState) => {...} + * - onPanResponderReject: (e, gestureState) => {...} + * - onPanResponderGrant: (e, gestureState) => {...} + * - onPanResponderStart: (e, gestureState) => {...} + * - onPanResponderEnd: (e, gestureState) => {...} + * - onPanResponderRelease: (e, gestureState) => {...} + * - onPanResponderMove: (e, gestureState) => {...} + * - onPanResponderTerminate: (e, gestureState) => {...} + * - onPanResponderTerminationRequest: (e, gestureState) => {...} + * + * - In general, for events that have capture equivalents, we update the + * gestureState once in the capture phase and can use it in the bubble phase + * as well. + * + * - Be careful with onStartShould* callbacks. They only reflect updated + * `gestureState` for start/end events that bubble/capture to the Node. + * Once the node is the responder, you can rely on every start/end event + * being processed by the gesture and `gestureState` being updated + * accordingly. (numberActiveTouches) may not be totally accurate unless you + * are the responder. + */ + create: function(config) { + var gestureState = { + // Useful for debugging + stateID: Math.random(), + }; + PanResponder._initializeGestureState(gestureState); + var panHandlers = { + onStartShouldSetResponder: function(e) { + return config.onStartShouldSetPanResponder === undefined ? false : + config.onStartShouldSetPanResponder(e, gestureState); + }, + onMoveShouldSetResponder: function(e) { + return config.onMoveShouldSetPanResponder === undefined ? false : + config.onMoveShouldSetPanResponder(e, gestureState); + }, + onStartShouldSetResponderCapture: function(e) { + // TODO: Actually, we should reinitialize the state any time + // touches.length increases from 0 active to > 0 active. + if (e.nativeEvent.touches.length === 1) { + PanResponder._initializeGestureState(gestureState); + } + gestureState.numberActiveTouches = e.touchHistory.numberActiveTouches; + return config.onStartShouldSetPanResponderCapture !== undefined ? + config.onStartShouldSetPanResponderCapture(e, gestureState) : false; + }, + + onMoveShouldSetResponderCapture: function(e) { + var touchHistory = e.touchHistory; + // Responder system incorrectly dispatches should* to current responder + // Filter out any touch moves past the first one - we would have + // already processed multi-touch geometry during the first event. + if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { + return false; + } + PanResponder._updateGestureStateOnMove(gestureState, touchHistory); + return config.onMoveShouldSetResponderCapture ? + config.onMoveShouldSetPanResponderCapture(e, gestureState) : false; + }, + + onResponderGrant: function(e) { + gestureState.x0 = currentCentroidX(e.touchHistory); + gestureState.y0 = currentCentroidY(e.touchHistory); + gestureState.dx = 0; + gestureState.dy = 0; + config.onPanResponderGrant && config.onPanResponderGrant(e, gestureState); + }, + + onResponderReject: function(e) { + config.onPanResponderReject && config.onPanResponderReject(e, gestureState); + }, + + onResponderRelease: function(e) { + config.onPanResponderRelease && config.onPanResponderRelease(e, gestureState); + PanResponder._initializeGestureState(gestureState); + }, + + onResponderStart: function(e) { + var touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + config.onPanResponderStart && config.onPanResponderStart(e, gestureState); + }, + + onResponderMove: function(e) { + var touchHistory = e.touchHistory; + // Guard against the dispatch of two touch moves when there are two + // simultaneously changed touches. + if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { + return; + } + // Filter out any touch moves past the first one - we would have + // already processed multi-touch geometry during the first event. + PanResponder._updateGestureStateOnMove(gestureState, touchHistory); + config.onPanResponderMove && config.onPanResponderMove(e, gestureState); + }, + + onResponderEnd: function(e) { + var touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + config.onPanResponderEnd && config.onPanResponderEnd(e, gestureState); + }, + + onResponderTerminate: function(e) { + config.onPanResponderTerminate && + config.onPanResponderTerminate(e, gestureState); + PanResponder._initializeGestureState(gestureState); + }, + + onResponderTerminationRequest: function(e) { + return config.onPanResponderTerminationRequest === undefined ? true : + config.onPanResponderTerminationRequest(e, gestureState); + }, + }; + return {panHandlers: panHandlers}; + }, +}; + +module.exports = PanResponder; diff --git a/Libraries/vendor/react/browser/eventPlugins/ResponderTouchHistoryStore.js b/Libraries/vendor/react/browser/eventPlugins/ResponderTouchHistoryStore.js index 2576528c1a..805b9471cf 100644 --- a/Libraries/vendor/react/browser/eventPlugins/ResponderTouchHistoryStore.js +++ b/Libraries/vendor/react/browser/eventPlugins/ResponderTouchHistoryStore.js @@ -39,6 +39,13 @@ var touchHistory = { mostRecentTimeStamp: 0, }; +var timestampForTouch = function(touch) { + // The legacy internal implementation provides "timeStamp", which has been + // renamed to "timestamp". Let both work for now while we iron it out + // TODO (evv): rename timeStamp to timestamp in internal code + return touch.timeStamp || touch.timestamp; +}; + /** * TODO: Instead of making gestures recompute filtered velocity, we could * include a built in velocity computation that can be reused globally. @@ -47,29 +54,29 @@ var touchHistory = { var initializeTouchData = function(touch) { return { touchActive: true, - startTimeStamp: touch.timeStamp, + startTimeStamp: timestampForTouch(touch), startPageX: touch.pageX, startPageY: touch.pageY, currentPageX: touch.pageX, currentPageY: touch.pageY, - currentTimeStamp: touch.timeStamp, + currentTimeStamp: timestampForTouch(touch), previousPageX: touch.pageX, previousPageY: touch.pageY, - previousTimeStamp: touch.timeStamp, + previousTimeStamp: timestampForTouch(touch), }; }; var reinitializeTouchTrack = function(touchTrack, touch) { touchTrack.touchActive = true; - touchTrack.startTimeStamp = touch.timeStamp; + touchTrack.startTimeStamp = timestampForTouch(touch); touchTrack.startPageX = touch.pageX; touchTrack.startPageY = touch.pageY; touchTrack.currentPageX = touch.pageX; touchTrack.currentPageY = touch.pageY; - touchTrack.currentTimeStamp = touch.timeStamp; + touchTrack.currentTimeStamp = timestampForTouch(touch); touchTrack.previousPageX = touch.pageX; touchTrack.previousPageY = touch.pageY; - touchTrack.previousTimeStamp = touch.timeStamp; + touchTrack.previousTimeStamp = timestampForTouch(touch); }; var validateTouch = function(touch) { @@ -96,7 +103,7 @@ var recordStartTouchData = function(touch) { } else { reinitializeTouchTrack(touchTrack, touch); } - touchHistory.mostRecentTimeStamp = touch.timeStamp; + touchHistory.mostRecentTimeStamp = timestampForTouch(touch); }; var recordMoveTouchData = function(touch) { @@ -112,8 +119,8 @@ var recordMoveTouchData = function(touch) { touchTrack.previousTimeStamp = touchTrack.currentTimeStamp; touchTrack.currentPageX = touch.pageX; touchTrack.currentPageY = touch.pageY; - touchTrack.currentTimeStamp = touch.timeStamp; - touchHistory.mostRecentTimeStamp = touch.timeStamp; + touchTrack.currentTimeStamp = timestampForTouch(touch); + touchHistory.mostRecentTimeStamp = timestampForTouch(touch); }; var recordEndTouchData = function(touch) { @@ -128,9 +135,9 @@ var recordEndTouchData = function(touch) { touchTrack.previousTimeStamp = touchTrack.currentTimeStamp; touchTrack.currentPageX = touch.pageX; touchTrack.currentPageY = touch.pageY; - touchTrack.currentTimeStamp = touch.timeStamp; + touchTrack.currentTimeStamp = timestampForTouch(touch); touchTrack.touchActive = false; - touchHistory.mostRecentTimeStamp = touch.timeStamp; + touchHistory.mostRecentTimeStamp = timestampForTouch(touch); }; var ResponderTouchHistoryStore = { diff --git a/Libraries/vendor/react/browser/eventPlugins/TouchHistoryMath.js b/Libraries/vendor/react/browser/eventPlugins/TouchHistoryMath.js new file mode 100644 index 0000000000..1507d21f56 --- /dev/null +++ b/Libraries/vendor/react/browser/eventPlugins/TouchHistoryMath.js @@ -0,0 +1,122 @@ +/** + * @providesModule TouchHistoryMath + */ + +"use strict"; + +var TouchHistoryMath = { + /** + * This code is optimized and not intended to look beautiful. This allows + * computing of touch centroids that have moved after `touchesChangedAfter` + * timeStamp. You can compute the current centroid involving all touches + * moves after `touchesChangedAfter`, or you can compute the previous + * centroid of all touches that were moved after `touchesChangedAfter`. + * + * @param {TouchHistoryMath} touchHistory Standard Responder touch track + * data. + * @param {number} touchesChangedAfter timeStamp after which moved touches + * are considered "actively moving" - not just "active". + * @param {boolean} isXAxis Consider `x` dimension vs. `y` dimension. + * @param {boolean} ofCurrent Compute current centroid for actively moving + * touches vs. previous centroid of now actively moving touches. + * @return {number} value of centroid in specified dimension. + */ + centroidDimension: function(touchHistory, touchesChangedAfter, isXAxis, ofCurrent) { + var touchBank = touchHistory.touchBank; + var total = 0; + var count = 0; + + var oneTouchData = touchHistory.numberActiveTouches === 1 ? + touchHistory.touchBank[touchHistory.indexOfSingleActiveTouch] : null; + + if (oneTouchData !== null) { + if (oneTouchData.touchActive && oneTouchData.currentTimeStamp > touchesChangedAfter) { + total += ofCurrent && isXAxis ? oneTouchData.currentPageX : + ofCurrent && !isXAxis ? oneTouchData.currentPageY : + !ofCurrent && isXAxis ? oneTouchData.previousPageX : + oneTouchData.previousPageY; + count = 1; + } + } else { + for (var i = 0; i < touchBank.length; i++) { + var touchTrack = touchBank[i]; + if (touchTrack !== null && + touchTrack !== undefined && + touchTrack.touchActive && + touchTrack.currentTimeStamp >= touchesChangedAfter) { + var toAdd; // Yuck, program temporarily in invalid state. + if (ofCurrent && isXAxis) { + toAdd = touchTrack.currentPageX; + } else if (ofCurrent && !isXAxis) { + toAdd = touchTrack.currentPageY; + } else if (!ofCurrent && isXAxis) { + toAdd = touchTrack.previousPageX; + } else { + toAdd = touchTrack.previousPageY; + } + total += toAdd; + count++; + } + } + } + return count > 0 ? total / count : TouchHistoryMath.noCentroid; + }, + + currentCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension( + touchHistory, + touchesChangedAfter, + true, // isXAxis + true // ofCurrent + ); + }, + + currentCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension( + touchHistory, + touchesChangedAfter, + false, // isXAxis + true // ofCurrent + ); + }, + + previousCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension( + touchHistory, + touchesChangedAfter, + true, // isXAxis + false // ofCurrent + ); + }, + + previousCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension( + touchHistory, + touchesChangedAfter, + false, // isXAxis + false // ofCurrent + ); + }, + + currentCentroidX: function(touchHistory) { + return TouchHistoryMath.centroidDimension( + touchHistory, + 0, // touchesChangedAfter + true, // isXAxis + true // ofCurrent + ); + }, + + currentCentroidY: function(touchHistory) { + return TouchHistoryMath.centroidDimension( + touchHistory, + 0, // touchesChangedAfter + false, // isXAxis + true // ofCurrent + ); + }, + + noCentroid: -1, +}; + +module.exports = TouchHistoryMath; diff --git a/Libraries/vendor/react/core/clamp.js b/Libraries/vendor/react/core/clamp.js new file mode 100644 index 0000000000..cb7578ceec --- /dev/null +++ b/Libraries/vendor/react/core/clamp.js @@ -0,0 +1,35 @@ +/** + * @generated SignedSource<> + * + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * !! This file is a check-in of a static_upstream project! !! + * !! !! + * !! You should not modify this file directly. Instead: !! + * !! 1) Use `fjs use-upstream` to temporarily replace this with !! + * !! the latest version from upstream. !! + * !! 2) Make your changes, test them, etc. !! + * !! 3) Use `fjs push-upstream` to copy your changes back to !! + * !! static_upstream. !! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * + * @providesModule clamp + * @typechecks + */ + + /** + * @param {number} value + * @param {number} min + * @param {number} max + * @return {number} + */ +function clamp(min, value, max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +} + +module.exports = clamp; diff --git a/ReactKit/Base/RCTConvert.h b/ReactKit/Base/RCTConvert.h index 66d77e4165..f26dea8279 100644 --- a/ReactKit/Base/RCTConvert.h +++ b/ReactKit/Base/RCTConvert.h @@ -26,6 +26,9 @@ + (float)float:(id)json; + (int)int:(id)json; ++ (int64_t)int64_t:(id)json; ++ (uint64_t)uint64_t:(id)json; + + (NSInteger)NSInteger:(id)json; + (NSUInteger)NSUInteger:(id)json; @@ -70,6 +73,7 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)json size:(id)json weight:(id)json; + (NSArray *)NSStringArray:(id)json; ++ (NSArray *)NSURLArray:(id)json; + (NSArray *)NSNumberArray:(id)json; + (NSArray *)UIColorArray:(id)json; + (NSArray *)CGColorArray:(id)json; @@ -142,6 +146,14 @@ id RCTConvertValue(id target, NSString *keypath, id json); #define RCT_CONVERTER(type, name, getter) \ RCT_CONVERTER_CUSTOM(type, name, [json getter]) +/** + * This macro is similar to RCT_CONVERTER, but specifically geared towards + * numeric types. It will handle string input correctly, and provides more + * detailed error reporting if a wrong value is passed in. + */ +#define RCT_NUMBER_CONVERTER(type, getter) \ +RCT_CONVERTER_CUSTOM(type, type, [[self NSNumber:json] getter]) + /** * This macro is used for creating converters for enum types. */ @@ -177,7 +189,7 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter]) * This macro is used for creating converter functions for structs that consist * of a number of CGFloat properties, such as CGPoint, CGRect, etc. */ -#define RCT_CGSTRUCT_CONVERTER(type, values) \ +#define RCT_CGSTRUCT_CONVERTER(type, values, _aliases) \ + (type)type:(id)json \ { \ @try { \ @@ -194,12 +206,23 @@ RCT_CONVERTER_CUSTOM(type, name, [json getter]) RCTLogError(@"Expected array with count %zd, but count is %zd: %@", count, [json count], json); \ } else { \ for (NSUInteger i = 0; i < count; i++) { \ - ((CGFloat *)&result)[i] = [json[i] doubleValue]; \ + ((CGFloat *)&result)[i] = [self CGFloat:json[i]]; \ } \ } \ } else if ([json isKindOfClass:[NSDictionary class]]) { \ + NSDictionary *aliases = _aliases; \ + if (aliases.count) { \ + json = [json mutableCopy]; \ + for (NSString *alias in aliases) { \ + NSString *key = aliases[alias]; \ + NSNumber *number = json[key]; \ + if (number) { \ + ((NSMutableDictionary *)json)[key] = number; \ + } \ + } \ + } \ for (NSUInteger i = 0; i < count; i++) { \ - ((CGFloat *)&result)[i] = [json[fields[i]] doubleValue]; \ + ((CGFloat *)&result)[i] = [self CGFloat:json[fields[i]]]; \ } \ } else if (json && json != [NSNull null]) { \ RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \ diff --git a/ReactKit/Base/RCTConvert.m b/ReactKit/Base/RCTConvert.m index f91f4f5ffa..df0547da35 100644 --- a/ReactKit/Base/RCTConvert.m +++ b/ReactKit/Base/RCTConvert.m @@ -13,25 +13,39 @@ #import "RCTLog.h" -CGFloat const RCTDefaultFontSize = 14; -NSString *const RCTDefaultFontName = @"HelveticaNeue"; -NSString *const RCTDefaultFontWeight = @"normal"; -NSString *const RCTBoldFontWeight = @"bold"; - @implementation RCTConvert RCT_CONVERTER(BOOL, BOOL, boolValue) -RCT_CONVERTER(double, double, doubleValue) -RCT_CONVERTER(float, float, floatValue) -RCT_CONVERTER(int, int, intValue) +RCT_NUMBER_CONVERTER(double, doubleValue) +RCT_NUMBER_CONVERTER(float, floatValue) +RCT_NUMBER_CONVERTER(int, intValue) + +RCT_NUMBER_CONVERTER(int64_t, longLongValue); +RCT_NUMBER_CONVERTER(uint64_t, unsignedLongLongValue); -RCT_CONVERTER(NSInteger, NSInteger, integerValue) -RCT_CONVERTER_CUSTOM(NSUInteger, NSUInteger, [json unsignedIntegerValue]) +RCT_NUMBER_CONVERTER(NSInteger, integerValue) +RCT_NUMBER_CONVERTER(NSUInteger, unsignedIntegerValue) RCT_CONVERTER_CUSTOM(NSArray *, NSArray, [NSArray arrayWithArray:json]) RCT_CONVERTER_CUSTOM(NSDictionary *, NSDictionary, [NSDictionary dictionaryWithDictionary:json]) RCT_CONVERTER(NSString *, NSString, description) -RCT_CONVERTER_CUSTOM(NSNumber *, NSNumber, @([json doubleValue])) + ++ (NSNumber *)NSNumber:(id)json +{ + if ([json isKindOfClass:[NSNumber class]]) { + return json; + } else if ([json isKindOfClass:[NSString class]]) { + NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; + NSNumber *number = [formatter numberFromString:json]; + if (!number) { + RCTLogError(@"JSON String '%@' could not be interpreted as a number", json); + } + return number; + } else if (json && json != [NSNull null]) { + RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json class]); + } + return nil; +} + (NSURL *)NSURL:(id)json { @@ -47,7 +61,12 @@ + (NSURL *)NSURL:(id)json } else if ([path length]) { - return [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]]; + NSURL *URL = [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]]; + if ([URL isFileURL] &&![[NSFileManager defaultManager] fileExistsAtPath:[URL absoluteString]]) { + RCTLogWarn(@"The file '%@' does not exist", URL); + return nil; + } + return URL; } return nil; } @@ -58,11 +77,11 @@ + (NSURLRequest *)NSURLRequest:(id)json } // JS Standard for time is milliseconds -RCT_CONVERTER_CUSTOM(NSDate *, NSDate, [NSDate dateWithTimeIntervalSince1970:[json doubleValue] / 1000.0]) -RCT_CONVERTER_CUSTOM(NSTimeInterval, NSTimeInterval, [json doubleValue] / 1000.0) +RCT_CONVERTER_CUSTOM(NSDate *, NSDate, [NSDate dateWithTimeIntervalSince1970:[self double:json] / 1000.0]) +RCT_CONVERTER_CUSTOM(NSTimeInterval, NSTimeInterval, [self double:json] / 1000.0) // JS standard for time zones is minutes. -RCT_CONVERTER_CUSTOM(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[json doubleValue] * 60.0]) +RCT_CONVERTER_CUSTOM(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[self double:json] * 60.0]) RCT_ENUM_CONVERTER(NSTextAlignment, (@{ @"auto": @(NSTextAlignmentNatural), @@ -90,11 +109,12 @@ + (NSURLRequest *)NSURLRequest:(id)json @"default": @(UIKeyboardTypeDefault), }), UIKeyboardTypeDefault, integerValue) -RCT_CONVERTER(CGFloat, CGFloat, doubleValue) -RCT_CGSTRUCT_CONVERTER(CGPoint, (@[@"x", @"y"])) -RCT_CGSTRUCT_CONVERTER(CGSize, (@[@"w", @"h"])) -RCT_CGSTRUCT_CONVERTER(CGRect, (@[@"x", @"y", @"w", @"h"])) -RCT_CGSTRUCT_CONVERTER(UIEdgeInsets, (@[@"top", @"left", @"bottom", @"right"])) +// TODO: normalise the use of w/width so we can do away with the alias values (#6566645) +RCT_CONVERTER_CUSTOM(CGFloat, CGFloat, [self double:json]) +RCT_CGSTRUCT_CONVERTER(CGPoint, (@[@"x", @"y"]), nil) +RCT_CGSTRUCT_CONVERTER(CGSize, (@[@"width", @"height"]), (@{@"w": @"width", @"h": @"height"})) +RCT_CGSTRUCT_CONVERTER(CGRect, (@[@"x", @"y", @"width", @"height"]), (@{@"w": @"width", @"h": @"height"})) +RCT_CGSTRUCT_CONVERTER(UIEdgeInsets, (@[@"top", @"left", @"bottom", @"right"]), nil) RCT_ENUM_CONVERTER(CGLineJoin, (@{ @"miter": @(kCGLineJoinMiter), @@ -113,9 +133,9 @@ + (NSURLRequest *)NSURLRequest:(id)json @"m21", @"m22", @"m23", @"m24", @"m31", @"m32", @"m33", @"m34", @"m41", @"m42", @"m43", @"m44" -])) +]), nil) -RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"])) +RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"]), nil) + (UIColor *)UIColor:(id)json { @@ -328,19 +348,19 @@ + (UIColor *)UIColor:(id)json } else { // Color array - color = [UIColor colorWithRed:[json[0] doubleValue] - green:[json[1] doubleValue] - blue:[json[2] doubleValue] - alpha:[json count] > 3 ? [json[3] doubleValue] : 1]; + color = [UIColor colorWithRed:[self double:json[0]] + green:[self double:json[1]] + blue:[self double:json[2]] + alpha:[json count] > 3 ? [self double:json[3]] : 1]; } } else if ([json isKindOfClass:[NSDictionary class]]) { // Color dictionary - color = [UIColor colorWithRed:[json[@"r"] doubleValue] - green:[json[@"g"] doubleValue] - blue:[json[@"b"] doubleValue] - alpha:[json[@"a"] ?: @1 doubleValue]]; + color = [UIColor colorWithRed:[self double:json[@"r"]] + green:[self double:json[@"g"]] + blue:[self double:json[@"b"]] + alpha:[self double:json[@"a"] ?: @1]]; } else if (json && ![json isKindOfClass:[NSNull class]]) { @@ -415,6 +435,11 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)json + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family size:(id)size weight:(id)weight { + CGFloat const RCTDefaultFontSize = 14; + NSString *const RCTDefaultFontName = @"HelveticaNeue"; + NSString *const RCTDefaultFontWeight = @"normal"; + NSString *const RCTBoldFontWeight = @"bold"; + // Create descriptor UIFontDescriptor *fontDescriptor = font.fontDescriptor ?: [UIFontDescriptor fontDescriptorWithName:RCTDefaultFontName size:RCTDefaultFontSize]; @@ -427,7 +452,7 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family size:(id)size weight:(id // Get font family NSString *familyName = [self NSString:family]; if (familyName) { - if ([UIFont fontNamesForFamilyName:familyName].count == 0) { + if ([UIFont fontNamesForFamilyName:familyName].count == 0) { font = [UIFont fontWithName:familyName size:fontDescriptor.pointSize]; if (font) { // It's actually a font name, not a font family name, @@ -437,11 +462,14 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family size:(id)size weight:(id } else { // Not a valid font or family RCTLogError(@"Unrecognized font family '%@'", familyName); + familyName = [UIFont fontWithDescriptor:fontDescriptor size:0].familyName; } } else { // Set font family fontDescriptor = [fontDescriptor fontDescriptorWithFamily:familyName]; } + } else { + familyName = [UIFont fontWithDescriptor:fontDescriptor size:0].familyName; } // Get font weight @@ -451,29 +479,43 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family size:(id)size weight:(id static NSSet *values; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - values = [NSSet setWithObjects:@"bold", @"normal", nil]; + values = [NSSet setWithObjects:RCTDefaultFontWeight, RCTBoldFontWeight, nil]; }); if (fontWeight && ![values containsObject:fontWeight]) { RCTLogError(@"Unrecognized font weight '%@', must be one of %@", fontWeight, values); + fontWeight = RCTDefaultFontWeight; } - UIFontDescriptorSymbolicTraits symbolicTraits = fontDescriptor.symbolicTraits; + // this is hacky. we are appending the string -Medium because most fonts we currently use + // just need to have -Medium appended to get the bold we want. we're going to revamp this + // to make it easier to know which options are available in JS. t4996115 if ([fontWeight isEqualToString:RCTBoldFontWeight]) { - symbolicTraits |= UIFontDescriptorTraitBold; - } else { - symbolicTraits &= ~UIFontDescriptorTraitBold; + font = nil; + for (NSString *fontName in [UIFont fontNamesForFamilyName:familyName]) { + if ([fontName hasSuffix:@"-Medium"]) { + font = [UIFont fontWithName:fontName size:fontDescriptor.pointSize]; + break; + } + if ([fontName hasSuffix:@"-Bold"]) { + font = [UIFont fontWithName:fontName size:fontDescriptor.pointSize]; + // But keep searching in case there's a medium option + } + } + if (font) { + fontDescriptor = font.fontDescriptor; + } } - fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits]; } // TODO: font style // Create font - return [UIFont fontWithDescriptor:fontDescriptor size:fontDescriptor.pointSize]; + return [UIFont fontWithDescriptor:fontDescriptor size:0]; } RCT_ARRAY_CONVERTER(NSString) +RCT_ARRAY_CONVERTER(NSURL) RCT_ARRAY_CONVERTER(NSNumber) RCT_ARRAY_CONVERTER(UIColor) @@ -729,6 +771,9 @@ static id RCTConvertValueWithExplicitEncoding(id target, NSString *key, id json, @"extAlignment": ^(id val) { return [RCTConvert NSTextAlignment:val]; }, + @"ritingDirection": ^(id val) { + return [RCTConvert NSWritingDirection:val]; + }, @"Cap": ^(id val) { return [RCTConvert CGLineCap:val]; }, diff --git a/ReactKit/Base/RCTRootView.h b/ReactKit/Base/RCTRootView.h index 0ac716f3a1..15f99fdee4 100644 --- a/ReactKit/Base/RCTRootView.h +++ b/ReactKit/Base/RCTRootView.h @@ -11,7 +11,7 @@ #import "RCTBridge.h" -@interface RCTRootView : UIView +@interface RCTRootView : UIView /** * The URL of the bundled application script (required). diff --git a/ReactKit/Base/RCTRootView.m b/ReactKit/Base/RCTRootView.m index e7aea0f437..f411279f40 100644 --- a/ReactKit/Base/RCTRootView.m +++ b/ReactKit/Base/RCTRootView.m @@ -29,6 +29,7 @@ @implementation RCTRootView RCTBridge *_bridge; RCTTouchHandler *_touchHandler; id _executor; + BOOL _registered; } static Class _globalExecutorClass; @@ -36,7 +37,7 @@ @implementation RCTRootView + (void)initialize { -#if DEBUG +#if TARGET_IPHONE_SIMULATOR // Register Cmd-R as a global refresh key [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"r" @@ -106,12 +107,32 @@ - (void)dealloc [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" args:@[self.reactTag]]; + [self invalidate]; +} + +#pragma mark - RCTInvalidating + +- (BOOL)isValid +{ + return [_bridge isValid]; +} + +- (void)invalidate +{ + // Clear view + [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + + [self removeGestureRecognizer:_touchHandler]; + [_touchHandler invalidate]; + [_executor invalidate]; // TODO: eventually we'll want to be able to share the bridge between // multiple rootviews, in which case we'll need to move this elsewhere [_bridge invalidate]; } +#pragma mark Bundle loading + - (void)bundleFinishedLoading:(NSError *)error { if (error != nil) { @@ -124,6 +145,7 @@ - (void)bundleFinishedLoading:(NSError *)error } else { [_bridge.uiManager registerRootView:self]; + _registered = YES; NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @@ -137,8 +159,7 @@ - (void)bundleFinishedLoading:(NSError *)error - (void)loadBundle { - // Clear view - [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [self invalidate]; if (!_scriptURL) { return; @@ -150,6 +171,8 @@ - (void)loadBundle [_executor invalidate]; [_bridge invalidate]; + _registered = NO; + // Choose local executor if specified, followed by global, followed by default _executor = [[_executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class] alloc] init]; _bridge = [[RCTBridge alloc] initWithExecutor:_executor moduleProvider:_moduleProvider]; @@ -209,15 +232,20 @@ - (void)loadBundle [self bundleFinishedLoading:error]; return; } + if (!_bridge.isValid) { + return; // Bridge was invalidated in the meanwhile + } // Success! RCTSourceCode *sourceCodeModule = _bridge.modules[NSStringFromClass([RCTSourceCode class])]; sourceCodeModule.scriptURL = _scriptURL; sourceCodeModule.scriptText = rawText; - [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^(NSError *error) { + [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^(NSError *_error) { dispatch_async(dispatch_get_main_queue(), ^{ - [self bundleFinishedLoading:error]; + if (_bridge.isValid) { + [self bundleFinishedLoading:_error]; + } }); }]; @@ -236,9 +264,12 @@ - (void)setScriptURL:(NSURL *)scriptURL [self loadBundle]; } -- (BOOL)isReactRootView +- (void)layoutSubviews { - return YES; + [super layoutSubviews]; + if (_registered) { + [_bridge.uiManager setFrame:self.frame forRootView:self]; + } } - (void)reload diff --git a/ReactKit/Base/RCTTouchHandler.m b/ReactKit/Base/RCTTouchHandler.m index f55a732d7a..108e43cce6 100644 --- a/ReactKit/Base/RCTTouchHandler.m +++ b/ReactKit/Base/RCTTouchHandler.m @@ -129,13 +129,15 @@ - (void)_recordNewTouches:(NSSet *)touches // Find closest React-managed touchable view UIView *targetView = touch.view; while (targetView) { - if (targetView.reactTag && targetView.userInteractionEnabled) { // TODO: implement respondsToTouch: mechanism + if (targetView.reactTag && targetView.userInteractionEnabled && + [targetView reactRespondsToTouch:touch]) { break; } targetView = targetView.superview; } - if (!targetView.reactTag || !targetView.userInteractionEnabled) { + NSNumber *reactTag = [targetView reactTagAtPoint:[touch locationInView:targetView]]; + if (!reactTag || !targetView.userInteractionEnabled) { return; } @@ -155,7 +157,7 @@ - (void)_recordNewTouches:(NSSet *)touches // Create touch NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:9]; - reactTouch[@"target"] = [targetView reactTagAtPoint:[touch locationInView:targetView]]; + reactTouch[@"target"] = reactTag; reactTouch[@"identifier"] = @(touchID); reactTouch[@"touches"] = [NSNull null]; // We hijack this touchObj to serve both as an event reactTouch[@"changedTouches"] = [NSNull null]; // and as a Touch object, so making this JIT friendly. @@ -248,15 +250,15 @@ - (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventNa if (_recordingInteractionTiming) { [_bridgeInteractionTiming addObject:@{ - @"timeSeconds": @(touch.originatingTime), - @"operation": @"taskOriginated", - @"taskID": @(touch.id), - }]; + @"timeSeconds": @(touch.originatingTime), + @"operation": @"taskOriginated", + @"taskID": @(touch.id), + }]; [_bridgeInteractionTiming addObject:@{ - @"timeSeconds": @(enqueueTime), - @"operation": @"taskEnqueuedPending", - @"taskID": @(touch.id), - }]; + @"timeSeconds": @(enqueueTime), + @"operation": @"taskEnqueuedPending", + @"taskID": @(touch.id), + }]; } } @@ -273,18 +275,18 @@ - (void)_update:(CADisplayLink *)sender if (_recordingInteractionTiming) { for (RCTTouchEvent *touch in _pendingTouches) { [_bridgeInteractionTiming addObject:@{ - @"timeSeconds": @(sender.timestamp), - @"operation": @"frameAlignedDispatch", - @"taskID": @(touch.id), - }]; + @"timeSeconds": @(sender.timestamp), + @"operation": @"frameAlignedDispatch", + @"taskID": @(touch.id), + }]; } if (pendingCount > 0 || sender.timestamp - _mostRecentEnqueueJS < 0.1) { [_bridgeInteractionTiming addObject:@{ - @"timeSeconds": @(sender.timestamp), - @"operation": @"mainThreadDisplayLink", - @"taskID": @([RCTTouchEvent newID]), - }]; + @"timeSeconds": @(sender.timestamp), + @"operation": @"mainThreadDisplayLink", + @"taskID": @([RCTTouchEvent newID]), + }]; } } diff --git a/ReactKit/Base/RCTUtils.m b/ReactKit/Base/RCTUtils.m index ebc3fcd489..70ad4479aa 100644 --- a/ReactKit/Base/RCTUtils.m +++ b/ReactKit/Base/RCTUtils.m @@ -31,8 +31,14 @@ id RCTJSONParse(NSString *jsonString, NSError **error) } NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO]; if (!jsonData) { - RCTLog(@"RCTJSONParse received the following string, which could not be losslessly converted to UTF8 data: '%@'", jsonString); jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; + if (jsonData) { + RCTLogWarn(@"RCTJSONParse received the following string, which could not be losslessly converted to UTF8 data: '%@'", jsonString); + } else { + // If our backup conversion fails, log the issue so we can see what strings are causing this (t6452813) + RCTLogError(@"RCTJSONParse received the following string, which could not be converted to UTF8 data: '%@'", jsonString); + return nil; + } } return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:error]; } diff --git a/ReactKit/Executors/RCTContextExecutor.m b/ReactKit/Executors/RCTContextExecutor.m index 1d77ec460a..e13f296e7e 100644 --- a/ReactKit/Executors/RCTContextExecutor.m +++ b/ReactKit/Executors/RCTContextExecutor.m @@ -129,7 +129,8 @@ - (instancetype)init return [self initWithJavaScriptThread:javaScriptThread globalContextRef:NULL]; } -- (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread globalContextRef:(JSGlobalContextRef)context +- (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread + globalContextRef:(JSGlobalContextRef)context { if ((self = [super init])) { _javaScriptThread = javaScriptThread; diff --git a/ReactKit/Executors/RCTWebViewExecutor.m b/ReactKit/Executors/RCTWebViewExecutor.m index 288614afa2..a31a41a8ea 100644 --- a/ReactKit/Executors/RCTWebViewExecutor.m +++ b/ReactKit/Executors/RCTWebViewExecutor.m @@ -184,7 +184,8 @@ - (void)injectJSONText:(NSString *)script asGlobalObjectNamed:(NSString *)objectName callback:(RCTJavaScriptCompleteBlock)onComplete { - RCTAssert(!_objectsToInject[objectName], @"already injected object named %@", _objectsToInject[objectName]); + RCTAssert(!_objectsToInject[objectName], + @"already injected object named %@", _objectsToInject[objectName]); _objectsToInject[objectName] = script; onComplete(nil); } diff --git a/ReactKit/Modules/RCTUIManager.h b/ReactKit/Modules/RCTUIManager.h index bb794ac52d..894849ffc7 100644 --- a/ReactKit/Modules/RCTUIManager.h +++ b/ReactKit/Modules/RCTUIManager.h @@ -14,8 +14,6 @@ #import "RCTInvalidating.h" #import "RCTViewManager.h" -@class RCTRootView; - @protocol RCTScrollableProtocol; /** @@ -33,10 +31,15 @@ /** * Register a root view with the RCTUIManager. Theoretically, a single manager - * can support multiple root views, however this feature is not currently exposed - * and may eventually be removed. + * can support multiple root views, however this feature is not currently exposed. + */ +- (void)registerRootView:(UIView *)rootView; + +/** + * Update the frame of a root view. This might be in response to a screen rotation + * or some other layout event outsde of the React-managed view hierarchy. */ -- (void)registerRootView:(RCTRootView *)rootView; +- (void)setFrame:(CGRect)frame forRootView:(UIView *)rootView; /** * Schedule a block to be executed on the UI thread. Useful if you need to execute diff --git a/ReactKit/Modules/RCTUIManager.m b/ReactKit/Modules/RCTUIManager.m index 8a90b74661..73c6929a71 100644 --- a/ReactKit/Modules/RCTUIManager.m +++ b/ReactKit/Modules/RCTUIManager.m @@ -19,7 +19,6 @@ #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTLog.h" -#import "RCTNavigator.h" #import "RCTRootView.h" #import "RCTScrollableProtocol.h" #import "RCTShadowView.h" @@ -55,7 +54,7 @@ @interface RCTAnimation : NSObject @implementation RCTAnimation -UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType type) +static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType type) { switch (type) { case RCTAnimationTypeLinear: @@ -67,7 +66,7 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t case RCTAnimationTypeEaseInEaseOut: return UIViewAnimationCurveEaseInOut; default: - RCTCAssert(NO, @"Unsupported animation type %zd", type); + RCTLogError(@"Unsupported animation type %zd", type); return UIViewAnimationCurveEaseInOut; } } @@ -157,6 +156,15 @@ - (instancetype)initWithDictionary:(NSDictionary *)config callback:(RCTResponseS @end +@interface RCTUIManager () + +// NOTE: these are properties so that they can be accessed by unit tests +@property (nonatomic, strong) RCTSparseArray *viewManagerRegistry; // RCT thread only +@property (nonatomic, strong) RCTSparseArray *shadowViewRegistry; // RCT thread only +@property (nonatomic, strong) RCTSparseArray *viewRegistry; // Main thread only + +@end + @implementation RCTUIManager { dispatch_queue_t _shadowQueue; @@ -174,11 +182,6 @@ @implementation RCTUIManager NSMutableDictionary *_defaultShadowViews; // RCT thread only NSMutableDictionary *_defaultViews; // Main thread only NSDictionary *_viewManagers; - - // Keyed by React tag - RCTSparseArray *_viewManagerRegistry; // RCT thread only - RCTSparseArray *_shadowViewRegistry; // RCT thread only - RCTSparseArray *_viewRegistry; // Main thread only } @synthesize bridge =_bridge; @@ -277,11 +280,14 @@ - (void)invalidate [_pendingUIBlocksLock unlock]; } -- (void)registerRootView:(RCTRootView *)rootView; +- (void)registerRootView:(UIView *)rootView; { RCTAssertMainThread(); NSNumber *reactTag = rootView.reactTag; + RCTAssert(RCTIsReactRootView(reactTag), + @"View %@ with tag #%@ is not a root view", rootView, reactTag); + UIView *existingView = _viewRegistry[reactTag]; RCTCAssert(existingView == nil || existingView == rootView, @"Expect all root views to have unique tag. Added %@ twice", reactTag); @@ -291,7 +297,7 @@ - (void)registerRootView:(RCTRootView *)rootView; CGRect frame = rootView.frame; // Register manager (TODO: should we do this, or leave it nil?) - _viewManagerRegistry[reactTag] = _viewManagers[@"View"]; + _viewManagerRegistry[reactTag] = _viewManagers[@"RCTView"]; // Register shadow view dispatch_async(_shadowQueue, ^{ @@ -300,13 +306,31 @@ - (void)registerRootView:(RCTRootView *)rootView; shadowView.reactTag = reactTag; shadowView.frame = frame; shadowView.backgroundColor = [UIColor whiteColor]; - shadowView.reactRootView = YES; // can this just be inferred from the fact that it has no superview? _shadowViewRegistry[shadowView.reactTag] = shadowView; [_rootViewTags addObject:reactTag]; }); } +- (void)setFrame:(CGRect)frame forRootView:(UIView *)rootView +{ + RCTAssertMainThread(); + + NSNumber *reactTag = rootView.reactTag; + RCTAssert(RCTIsReactRootView(reactTag), @"Specified view %@ is not a root view", reactTag); + + dispatch_async(_bridge.shadowQueue, ^{ + RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag]; + RCTAssert(rootShadowView != nil, @"Could not locate root view with tag %@", reactTag); + rootShadowView.frame = frame; + [rootShadowView updateLayout]; + + RCTViewManagerUIBlock uiBlock = [self uiBlockWithLayoutUpdateForRootView:rootShadowView]; + [self addUIBlock:uiBlock]; + [self flushUIBlocks]; + }); +} + /** * Unregisters views from registries */ @@ -389,21 +413,6 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo UIView *view = viewRegistry[reactTag]; CGRect frame = [frames[ii] CGRectValue]; - // These frames are in terms of anchorPoint = topLeft, but internally the - // views are anchorPoint = center for easier scale and rotation animations. - // Convert the frame so it works with anchorPoint = center. - CGPoint position = {CGRectGetMidX(frame), CGRectGetMidY(frame)}; - CGRect bounds = {0, 0, frame.size}; - - // Avoid crashes due to nan coords - if (isnan(position.x) || isnan(position.y) || - isnan(bounds.origin.x) || isnan(bounds.origin.y) || - isnan(bounds.size.width) || isnan(bounds.size.height)) { - RCTLogError(@"Invalid layout for (%@)%@. position: %@. bounds: %@", - [view reactTag], self, NSStringFromCGPoint(position), NSStringFromCGRect(bounds)); - continue; - } - void (^completion)(BOOL finished) = ^(BOOL finished) { if (self->_layoutAnimation.callback) { self->_layoutAnimation.callback(@[@(finished)]); @@ -415,21 +424,22 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo RCTAnimation *updateAnimation = isNew ? nil: _layoutAnimation.updateAnimation; if (updateAnimation) { [updateAnimation performAnimations:^{ - view.layer.position = position; - view.layer.bounds = bounds; + [view reactSetFrame:frame]; for (RCTViewManagerUIBlock block in updateBlocks) { block(self, _viewRegistry); } } withCompletionBlock:completion]; } else { - view.layer.position = position; - view.layer.bounds = bounds; + [view reactSetFrame:frame]; for (RCTViewManagerUIBlock block in updateBlocks) { block(self, _viewRegistry); } completion(YES); } + // TODO: deprecate this + [view reactSetBorders]; + // Animate view creation BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; RCTAnimation *createAnimation = _layoutAnimation.createAnimation; @@ -461,7 +471,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo * TODO: this is quite inefficient. If this was handled via the * ViewManager instead, it could be done more efficiently. */ - RCTRootView *rootView = _viewRegistry[rootViewTag]; + UIView *rootView = _viewRegistry[rootViewTag]; RCTTraverseViewNodes(rootView, ^(id view) { if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) { [view reactBridgeDidFinishTransaction]; @@ -549,13 +559,15 @@ - (void)removeRootView:(NSNumber *)rootReactTag RCTShadowView *rootShadowView = _shadowViewRegistry[rootReactTag]; RCTAssert(rootShadowView.superview == nil, @"root view cannot have superview (ID %@)", rootReactTag); - [self _purgeChildren:@[rootShadowView] fromRegistry:_shadowViewRegistry]; + [self _purgeChildren:rootShadowView.reactSubviews fromRegistry:_shadowViewRegistry]; + _shadowViewRegistry[rootReactTag] = nil; [_rootViewTags removeObject:rootReactTag]; [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ RCTCAssertMainThread(); UIView *rootView = viewRegistry[rootReactTag]; - [uiManager _purgeChildren:@[rootView] fromRegistry:viewRegistry]; + [uiManager _purgeChildren:rootView.reactSubviews fromRegistry:viewRegistry]; + viewRegistry[rootReactTag] = nil; }]; } @@ -701,7 +713,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }]; // Update layout - [shadowView updateShadowViewLayout]; + [shadowView updateLayout]; } - (void)createAndRegisterViewWithReactTag:(NSNumber *)reactTag @@ -781,7 +793,9 @@ - (void)becomeResponder:(NSNumber *)reactTag if (!reactTag) return; [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *newResponder = viewRegistry[reactTag]; + [newResponder reactWillMakeFirstResponder]; [newResponder becomeFirstResponder]; + [newResponder reactDidMakeFirstResponder]; }]; } @@ -830,6 +844,13 @@ - (void)batchDidComplete _nextLayoutAnimation = nil; } + [self flushUIBlocks]; +} + +- (void)flushUIBlocks +{ + RCTAssert(![NSThread isMainThread], @"Should be called on shadow thread"); + // First copy the previous blocks into a temporary variable, then reset the // pending blocks to a new array. This guards against mutation while // processing the pending blocks in another thread. @@ -858,7 +879,7 @@ - (void)measure:(NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *view = viewRegistry[reactTag]; if (!view) { - RCTLogError(@"measure cannot find view with tag %zd", reactTag); + RCTLogError(@"measure cannot find view with tag %@", reactTag); return; } CGRect frame = view.frame; @@ -868,7 +889,8 @@ - (void)measure:(NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback rootView = rootView.superview; } - RCTCAssert([rootView isReactRootView], @"React view not inside RCTRootView"); + // TODO: this doesn't work because sometimes view is inside a modal window + // RCTCAssert([rootView isReactRootView], @"React view is not inside a react root view"); // By convention, all coordinates, whether they be touch coordinates, or // measurement coordinates are with respect to the root view. @@ -885,16 +907,9 @@ - (void)measure:(NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback }]; } -/** - * TODO: This could be modified to accept any `RCTViewNodeProtocol`, if - * appropriate changes were made to that protocol to support `superview` - * traversal - which is possibly more difficult than it sounds since a - * `superview` is not a "react superview". - */ -+ (void)measureLayoutOnNodes:(RCTShadowView *)view - ancestor:(RCTShadowView *)ancestor - errorCallback:(RCTResponseSenderBlock)errorCallback - callback:(__unused RCTResponseSenderBlock)callback +static void RCTMeasureLayout(RCTShadowView *view, + RCTShadowView *ancestor, + RCTResponseSenderBlock callback) { if (!view) { RCTLogError(@"Attempting to measure view that does not exist"); @@ -904,7 +919,7 @@ + (void)measureLayoutOnNodes:(RCTShadowView *)view RCTLogError(@"Attempting to measure relative to ancestor that does not exist"); return; } - CGRect result = [RCTShadowView measureLayout:view relativeTo:ancestor]; + CGRect result = [view measureLayoutRelativeToAncestor:ancestor]; if (CGRectIsNull(result)) { RCTLogError(@"view %@ (tag #%@) is not a decendant of %@ (tag #%@)", view, view.reactTag, ancestor, ancestor.reactTag); @@ -918,7 +933,7 @@ + (void)measureLayoutOnNodes:(RCTShadowView *)view RCTLogError(@"Attempted to measure layout but offset or dimensions were NaN"); return; } - callback(@[@(topOffset), @(leftOffset), @(width), @(height)]); + callback(@[@(leftOffset), @(topOffset), @(width), @(height)]); } /** @@ -937,7 +952,7 @@ - (void)measureLayout:(NSNumber *)reactTag RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTShadowView *ancestorShadowView = _shadowViewRegistry[ancestorReactTag]; - [RCTUIManager measureLayoutOnNodes:shadowView ancestor:ancestorShadowView errorCallback:errorCallback callback:callback]; + RCTMeasureLayout(shadowView, ancestorShadowView, callback); } /** @@ -954,7 +969,7 @@ - (void)measureLayoutRelativeToParent:(NSNumber *)reactTag RCT_EXPORT(); RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; - [RCTUIManager measureLayoutOnNodes:shadowView ancestor:[shadowView superview] errorCallback:errorCallback callback:callback]; + RCTMeasureLayout(shadowView, shadowView.reactSuperview, callback); } /** @@ -981,7 +996,7 @@ - (void)measureViewsInRect:(NSDictionary *)rect CGRect layoutRect = [RCTConvert CGRect:rect]; [childShadowViews enumerateObjectsUsingBlock:^(RCTShadowView *childShadowView, NSUInteger idx, BOOL *stop) { - CGRect childLayout = [RCTShadowView measureLayout:childShadowView relativeTo:shadowView]; + CGRect childLayout = [childShadowView measureLayoutRelativeToAncestor:shadowView]; if (CGRectIsNull(childLayout)) { RCTLogError(@"View %@ (tag #%@) is not a decendant of %@ (tag #%@)", childShadowView, childShadowView.reactTag, shadowView, shadowView.reactTag); @@ -1183,9 +1198,9 @@ - (NSDictionary *)customBubblingEventTypes }, } mutableCopy]; - [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { - if (RCTClassOverridesClassMethod(cls, @selector(customBubblingEventTypes))) { - NSDictionary *eventTypes = [cls customBubblingEventTypes]; + [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTViewManager *manager, BOOL *stop) { + if (RCTClassOverridesInstanceMethod([manager class], @selector(customBubblingEventTypes))) { + NSDictionary *eventTypes = [manager customBubblingEventTypes]; for (NSString *eventName in eventTypes) { RCTCAssert(!customBubblingEventTypesConfigs[eventName], @"Event '%@' registered multiple times.", eventName); @@ -1235,9 +1250,9 @@ - (NSDictionary *)customDirectEventTypes }, } mutableCopy]; - [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { - if (RCTClassOverridesClassMethod(cls, @selector(customDirectEventTypes))) { - NSDictionary *eventTypes = [cls customDirectEventTypes]; + [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTViewManager *manager, BOOL *stop) { + if (RCTClassOverridesInstanceMethod([manager class], @selector(customDirectEventTypes))) { + NSDictionary *eventTypes = [manager customDirectEventTypes]; for (NSString *eventName in eventTypes) { RCTCAssert(!customDirectEventTypes[eventName], @"Event '%@' registered multiple times.", eventName); } @@ -1266,7 +1281,7 @@ - (NSDictionary *)constantsToExport }, @"modalFullscreenView": @{ @"width": @(RCTScreenSize().width), - @"height": @(RCTScreenSize().width), + @"height": @(RCTScreenSize().height), }, }, @"StyleConstants": @{ @@ -1312,10 +1327,10 @@ - (NSDictionary *)constantsToExport }, } mutableCopy]; - [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { + [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTViewManager *manager, BOOL *stop) { // TODO: should these be inherited? - NSDictionary *constants = RCTClassOverridesClassMethod(cls, @selector(constantsToExport)) ? [cls constantsToExport] : nil; - if ([constants count]) { + NSDictionary *constants = RCTClassOverridesInstanceMethod([manager class], @selector(constantsToExport)) ? [manager constantsToExport] : nil; + if (constants.count) { NSMutableDictionary *constantsNamespace = [NSMutableDictionary dictionaryWithDictionary:allJSConstants[name]]; RCTAssert(constantsNamespace[@"Constants"] == nil , @"Cannot redefine Constants in namespace: %@", name); // add an additional 'Constants' namespace for each class @@ -1350,12 +1365,18 @@ - (void)startOrResetInteractionTiming NSSet *rootViewTags = [_rootViewTags copy]; [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { for (NSNumber *reactTag in rootViewTags) { - RCTRootView *rootView = viewRegistry[reactTag]; - [rootView startOrResetInteractionTiming]; + id rootView = viewRegistry[reactTag]; + if ([rootView respondsToSelector:@selector(startOrResetInteractionTiming)]) { + [rootView startOrResetInteractionTiming]; + } } }]; } +// TODO: remove horrible hack - this is only here so that +// [rootView endAndResetInteractionTiming] below doesn't raise warnings +- (NSDictionary *)endAndResetInteractionTiming { return nil; } + - (void)endAndResetInteractionTiming:(RCTResponseSenderBlock)onSuccess onError:(RCTResponseSenderBlock)onError { @@ -1365,8 +1386,8 @@ - (void)endAndResetInteractionTiming:(RCTResponseSenderBlock)onSuccess [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { NSMutableDictionary *timingData = [[NSMutableDictionary alloc] init]; for (NSNumber *reactTag in rootViewTags) { - RCTRootView *rootView = viewRegistry[reactTag]; - if (rootView) { + id rootView = viewRegistry[reactTag]; + if ([rootView respondsToSelector:@selector(endAndResetInteractionTiming)]) { timingData[reactTag.stringValue] = [rootView endAndResetInteractionTiming]; } } diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj index 7cd218891c..234793aede 100644 --- a/ReactKit/ReactKit.xcodeproj/project.pbxproj +++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj @@ -50,7 +50,6 @@ 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 830BA4541A8E3BDA00D53203 /* RCTCache.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; - 83C911101AAE6521001323A3 /* RCTAnimationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 83C9110F1AAE6521001323A3 /* RCTAnimationManager.m */; }; 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */; }; 83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */; }; 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA501A601E3B00E9B192 /* RCTUtils.m */; }; @@ -169,8 +168,6 @@ 830BA4541A8E3BDA00D53203 /* RCTCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCache.m; sourceTree = ""; }; 83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSparseArray.h; sourceTree = ""; }; 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSparseArray.m; sourceTree = ""; }; - 83C9110E1AAE6521001323A3 /* RCTAnimationManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationManager.h; sourceTree = ""; }; - 83C9110F1AAE6521001323A3 /* RCTAnimationManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationManager.m; sourceTree = ""; }; 83CBBA2E1A601D0E00E9B192 /* libReactKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; 83CBBA4A1A601E3B00E9B192 /* RCTAssert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAssert.h; sourceTree = ""; }; 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAssert.m; sourceTree = ""; }; @@ -230,8 +227,6 @@ 1372B7091AB030C200659ED6 /* RCTAppState.m */, 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */, 13B07FE81A69327A00A75B9A /* RCTAlertManager.m */, - 83C9110E1AAE6521001323A3 /* RCTAnimationManager.h */, - 83C9110F1AAE6521001323A3 /* RCTAnimationManager.m */, 58114A4F1AAE93D500E7D092 /* RCTAsyncLocalStorage.h */, 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */, 13B07FE91A69327A00A75B9A /* RCTExceptionsManager.h */, @@ -489,7 +484,6 @@ 13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */, 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */, 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */, - 83C911101AAE6521001323A3 /* RCTAnimationManager.m in Sources */, 13C156051AB1A2840079392D /* RCTWebView.m in Sources */, 83CBBA601A601EAA00E9B192 /* RCTBridge.m in Sources */, 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */, diff --git a/ReactKit/Views/RCTDatePickerManager.m b/ReactKit/Views/RCTDatePickerManager.m index 15eb052c24..2a2b6bc48b 100644 --- a/ReactKit/Views/RCTDatePickerManager.m +++ b/ReactKit/Views/RCTDatePickerManager.m @@ -44,6 +44,8 @@ - (void)onChange:(UIDatePicker *)sender - (NSDictionary *)constantsToExport { UIDatePicker *dp = [[UIDatePicker alloc] init]; + [dp layoutIfNeeded]; + return @{ @"ComponentHeight": @(CGRectGetHeight(dp.frame)), @"ComponentWidth": @(CGRectGetWidth(dp.frame)), diff --git a/ReactKit/Views/RCTNavigator.m b/ReactKit/Views/RCTNavigator.m index 7e393c2163..e3ee09a5ae 100644 --- a/ReactKit/Views/RCTNavigator.m +++ b/ReactKit/Views/RCTNavigator.m @@ -487,16 +487,14 @@ - (void)reactBridgeDidFinishTransaction // We can actually recover from this situation, but it would be nice to know // when this error happens. This simply means that JS hasn't caught up to a // back navigation before progressing. It's likely a bug in the JS code that - // catches up/schedules navigations. Eventually, let's recover from this - // error state, but in the mean time, let's get notified about any JS bugs. + // catches up/schedules navigations. + if (!(jsGettingAhead || + jsCatchingUp || + jsMakingNoProgressButNeedsToCatchUp || + jsMakingNoProgressAndDoesntNeedTo)) { + RCTLogError(@"JS has only made partial progress to catch up to UIKit"); + } RCTAssert( - jsGettingAhead || - jsCatchingUp || - jsMakingNoProgressButNeedsToCatchUp || - jsMakingNoProgressAndDoesntNeedTo, - @"JS has only made partial progress to catch up to UIKit" - ); - NSAssert( currentReactCount <= _currentViews.count, @"Cannot adjust current top of stack beyond available views" ); @@ -504,7 +502,7 @@ - (void)reactBridgeDidFinishTransaction // Views before the previous react count must not have changed. Views greater than previousReactCount // up to currentReactCount may have changed. for (NSInteger i = 0; i < MIN(_currentViews.count, MIN(_previousViews.count, previousReactCount)); i++) { - NSAssert(_currentViews[i] == _previousViews[i], @"current view should equal previous view"); + RCTAssert(_currentViews[i] == _previousViews[i], @"current view should equal previous view"); } RCTAssert(currentReactCount >= 1, @"should be at least one current view"); if (jsGettingAhead) { @@ -545,7 +543,7 @@ - (void)wrapperViewController:(RCTWrapperViewController *)wrapperViewController RCTAssert( (navigationController == nil || [_navigationController.viewControllers containsObject:wrapperViewController]), - @"if navigation controller is not nil, it should container the wrapper view controller" + @"if navigation controller is not nil, it should contain the wrapper view controller" ); RCTAssert(_navigationController.navigationLock == RCTNavigationLockJavaScript || _numberOfViewControllerMovesToIgnore == 0, diff --git a/ReactKit/Views/RCTShadowView.h b/ReactKit/Views/RCTShadowView.h index 1a042ee1aa..be1b2465e9 100644 --- a/ReactKit/Views/RCTShadowView.h +++ b/ReactKit/Views/RCTShadowView.h @@ -49,11 +49,6 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); */ @property (nonatomic, assign, getter=isNewView) BOOL newView; -/** - * Is this the shadowView for an RCTRootView - */ -@property (nonatomic, assign, getter=isReactRootView) BOOL reactRootView; - /** * Position and dimensions. * Defaults to { 0, 0, NAN, NAN }. @@ -149,7 +144,7 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); /** * Triggers a recalculation of the shadow view's layout. */ -- (void)updateShadowViewLayout; +- (void)updateLayout; /** * Computes the recursive offset, meaning the sum of all descendant offsets - @@ -157,10 +152,8 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); * sum of `top`/`left`s, as this function uses the *actual* positions of * children, not the style specified positions - it computes this based on the * resulting layout. It does not yet compensate for native scroll view insets or - * transforms or anchor points. Returns an array containing the `x, y, width, - * height` of the shadow view relative to the ancestor, or `nil` if the `view` - * is not a descendent of `ancestor`. + * transforms or anchor points. */ -+ (CGRect)measureLayout:(RCTShadowView *)view relativeTo:(RCTShadowView *)ancestor; +- (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor; @end diff --git a/ReactKit/Views/RCTShadowView.m b/ReactKit/Views/RCTShadowView.m index d977ac16c5..f43f9cd28b 100644 --- a/ReactKit/Views/RCTShadowView.m +++ b/ReactKit/Views/RCTShadowView.m @@ -17,8 +17,6 @@ typedef void (^RCTActionBlock)(RCTShadowView *shadowViewSelf, id value); typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf); -#define MAX_TREE_DEPTH 30 - const NSString *const RCTBackgroundColorProp = @"backgroundColor"; typedef enum { @@ -213,17 +211,18 @@ - (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame parentConstra [self applyLayoutNode:_cssNode viewsWithNewFrame:viewsWithNewFrame absolutePosition:CGPointZero]; } -+ (CGRect)measureLayout:(RCTShadowView *)shadowView relativeTo:(RCTShadowView *)ancestor +- (CGRect)measureLayoutRelativeToAncestor:(RCTShadowView *)ancestor { CGFloat totalOffsetTop = 0.0; CGFloat totalOffsetLeft = 0.0; - CGSize size = shadowView.frame.size; - NSInteger depth = 0; - while (depth < MAX_TREE_DEPTH && shadowView && shadowView != ancestor) { + CGSize size = self.frame.size; + NSInteger depth = 30; // max depth to search + RCTShadowView *shadowView = self; + while (depth && shadowView && shadowView != ancestor) { totalOffsetTop += shadowView.frame.origin.y; totalOffsetLeft += shadowView.frame.origin.x; shadowView = shadowView->_superview; - depth++; + depth--; } if (ancestor != shadowView) { return CGRectNull; @@ -259,6 +258,11 @@ - (instancetype)init return self; } +- (BOOL)isReactRootView +{ + return RCTIsReactRootView(self.reactTag); +} + - (void)dealloc { free_css_node(_cssNode); @@ -514,7 +518,7 @@ - (void)setBackgroundColor:(UIColor *)color [self dirtyPropagation]; } -- (void)updateShadowViewLayout +- (void)updateLayout { if (_recomputePadding) { RCTProcessMetaProps(_paddingMetaProps, _cssNode->style.padding); diff --git a/ReactKit/Views/RCTViewManager.h b/ReactKit/Views/RCTViewManager.h index 064d921a86..719a970ab5 100644 --- a/ReactKit/Views/RCTViewManager.h +++ b/ReactKit/Views/RCTViewManager.h @@ -73,7 +73,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * Note that this method is not inherited when you subclass a view module, and * you should not call [super customBubblingEventTypes] when overriding it. */ -+ (NSDictionary *)customBubblingEventTypes; +- (NSDictionary *)customBubblingEventTypes; /** * Returns a dictionary of config data passed to JS that defines eligible events @@ -89,7 +89,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v * Note that this method is not inherited when you subclass a view module, and * you should not call [super customDirectEventTypes] when overriding it. */ -+ (NSDictionary *)customDirectEventTypes; +- (NSDictionary *)customDirectEventTypes; /** * Called to notify manager that layout has finished, in case any calculated diff --git a/ReactKit/Views/RCTViewManager.m b/ReactKit/Views/RCTViewManager.m index a84a221201..2f3ed2d475 100644 --- a/ReactKit/Views/RCTViewManager.m +++ b/ReactKit/Views/RCTViewManager.m @@ -44,12 +44,12 @@ - (RCTShadowView *)shadowView return [[RCTShadowView alloc] init]; } -+ (NSDictionary *)customBubblingEventTypes +- (NSDictionary *)customBubblingEventTypes { return nil; } -+ (NSDictionary *)customDirectEventTypes +- (NSDictionary *)customDirectEventTypes { return nil; } @@ -101,7 +101,7 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) return; } - switch ([RCTConvert NSInteger:json]) { + switch ([RCTConvert RCTPointerEvents:json]) { case RCTPointerEventsUnspecified: // Pointer events "unspecified" acts as if a stylesheet had not specified, // which is different than "auto" in CSS (which cannot and will not be @@ -116,6 +116,12 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) RCTLogError(@"UIView base class does not support pointerEvent value: %@", json); } } +RCT_CUSTOM_VIEW_PROPERTY(removeClippedSubviews, RCTView) +{ + if ([view respondsToSelector:@selector(setRemoveClippedSubviews:)]) { + view.removeClippedSubviews = json ? [RCTConvert BOOL:json] : defaultView.removeClippedSubviews; + } +} #pragma mark - ShadowView properties @@ -149,4 +155,15 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *) view.positionType = json ? [RCTConvert css_position_type_t:json] : defaultView.positionType; } +// Border properties - to be deprecated + +RCT_REMAP_VIEW_PROPERTY(borderTopWidth, reactBorderTop.width); +RCT_REMAP_VIEW_PROPERTY(borderRightWidth, reactBorderRight.width); +RCT_REMAP_VIEW_PROPERTY(borderBottomWidth, reactBorderBottom.width); +RCT_REMAP_VIEW_PROPERTY(borderLeftWidth, reactBorderLeft.width); +RCT_REMAP_VIEW_PROPERTY(borderTopColor, reactBorderTop.color); +RCT_REMAP_VIEW_PROPERTY(borderRightColor, reactBorderRight.color); +RCT_REMAP_VIEW_PROPERTY(borderBottomColor, reactBorderBottom.color); +RCT_REMAP_VIEW_PROPERTY(borderLeftColor, reactBorderLeft.color); + @end diff --git a/ReactKit/Views/RCTViewNodeProtocol.h b/ReactKit/Views/RCTViewNodeProtocol.h index 6e4d1cec73..998c53bce1 100644 --- a/ReactKit/Views/RCTViewNodeProtocol.h +++ b/ReactKit/Views/RCTViewNodeProtocol.h @@ -22,7 +22,7 @@ - (id)reactSuperview; - (NSNumber *)reactTagAtPoint:(CGPoint)point; -// View is an RCTRootView +// View/ShadowView is a root view - (BOOL)isReactRootView; @optional diff --git a/ReactKit/Views/UIView+ReactKit.h b/ReactKit/Views/UIView+ReactKit.h index 4e71397df7..b2ba2ca9a7 100644 --- a/ReactKit/Views/UIView+ReactKit.h +++ b/ReactKit/Views/UIView+ReactKit.h @@ -15,6 +15,12 @@ @interface UIView (ReactKit) +/** + * Used by the UIIManager to set the view frame. + * May be overriden to disable animation, etc. + */ +- (void)reactSetFrame:(CGRect)frame; + /** * This method finds and returns the containing view controller for the view. */ @@ -28,4 +34,20 @@ */ - (void)addControllerToClosestParent:(UIViewController *)controller; +/** + * Responder overrides - to be deprecated. + */ +- (void)reactWillMakeFirstResponder; +- (void)reactDidMakeFirstResponder; +- (BOOL)reactRespondsToTouch:(UITouch *)touch; + +@end + +@interface UIView (ReactKitBorders) + +/** + * Borders stuff - pay no attention to this, it's going away (#6548297) + */ +- (void)reactSetBorders; + @end diff --git a/ReactKit/Views/UIView+ReactKit.m b/ReactKit/Views/UIView+ReactKit.m index 5a1d20c693..2ef2ce31b2 100644 --- a/ReactKit/Views/UIView+ReactKit.m +++ b/ReactKit/Views/UIView+ReactKit.m @@ -12,6 +12,7 @@ #import #import "RCTAssert.h" +#import "RCTLog.h" #import "RCTWrapperViewController.h" @implementation UIView (ReactKit) @@ -61,6 +62,27 @@ - (UIView *)reactSuperview return self.superview; } +- (void)reactSetFrame:(CGRect)frame +{ + // These frames are in terms of anchorPoint = topLeft, but internally the + // views are anchorPoint = center for easier scale and rotation animations. + // Convert the frame so it works with anchorPoint = center. + CGPoint position = {CGRectGetMidX(frame), CGRectGetMidY(frame)}; + CGRect bounds = {CGPointZero, frame.size}; + + // Avoid crashes due to nan coords + if (isnan(position.x) || isnan(position.y) || + isnan(bounds.origin.x) || isnan(bounds.origin.y) || + isnan(bounds.size.width) || isnan(bounds.size.height)) { + RCTLogError(@"Invalid layout for (%@)%@. position: %@. bounds: %@", + self.reactTag, self, NSStringFromCGPoint(position), NSStringFromCGRect(bounds)); + return; + } + + self.layer.position = position; + self.layer.bounds = bounds; +} + - (UIViewController *)backingViewController { id responder = [self nextResponder]; @@ -86,4 +108,167 @@ - (void)addControllerToClosestParent:(UIViewController *)controller } } +/** + * Responder overrides - to be deprecated. + */ +- (void)reactWillMakeFirstResponder {}; +- (void)reactDidMakeFirstResponder {}; +- (BOOL)reactRespondsToTouch:(UITouch *)touch +{ + return YES; +} + +@end + +#pragma mark - Borders + +// Note: the value of this enum determines their relative zPosition +typedef NS_ENUM(NSUInteger, RCTBorderSide) { + RCTBorderSideTop = 0, + RCTBorderSideRight = 1, + RCTBorderSideBottom = 2, + RCTBorderSideLeft = 3 +}; + +@interface RCTSingleSidedBorder : NSObject + +@property (nonatomic, readwrite, assign) CGFloat width; +@property (nonatomic, readwrite, strong) UIColor *color; +@property (nonatomic, readonly, assign) RCTBorderSide side; + +- (instancetype)initWithSide:(RCTBorderSide)side superlayer:(CALayer *)superlayer; + +- (void)superLayerBoundsDidChange; + +@end + +@implementation RCTSingleSidedBorder +{ + CALayer *_borderLayer; +} + +- (instancetype)initWithSide:(RCTBorderSide)side superlayer:(CALayer *)superlayer +{ + if (self = [super init]) { + _side = side; + + _borderLayer = [CALayer layer]; + _borderLayer.delegate = self; + _borderLayer.zPosition = INT_MAX - _side; + + [superlayer insertSublayer:_borderLayer atIndex:0]; + } + return self; +} + +- (void)dealloc +{ + _borderLayer.delegate = nil; +} + +- (void)setWidth:(CGFloat)width +{ + _width = width; + [_borderLayer setNeedsLayout]; +} + +- (void)setColor:(UIColor *)color +{ + _color = color; + _borderLayer.backgroundColor = _color.CGColor; + [_borderLayer setNeedsLayout]; +} + +- (void)superLayerBoundsDidChange +{ + [_borderLayer setNeedsLayout]; +} + +#pragma mark - CALayerDelegate + +- (void)layoutSublayersOfLayer:(CALayer *)layer +{ + CGSize superlayerSize = layer.superlayer.frame.size; + + CGFloat xPosition = 0.0f; + CGFloat yPosition = 0.0f; + + // Note: we ensure side layers are below top & bottom for snapshot test consistency + + switch (self.side) { + case RCTBorderSideTop: + layer.frame = CGRectMake(xPosition, yPosition, superlayerSize.width, self.width); + break; + case RCTBorderSideRight: + xPosition = superlayerSize.width - self.width; + layer.frame = CGRectMake(xPosition, yPosition, self.width, superlayerSize.height); + [layer.superlayer insertSublayer:layer atIndex:0]; + break; + case RCTBorderSideBottom: + yPosition = superlayerSize.height - self.width; + layer.frame = CGRectMake(xPosition, yPosition, superlayerSize.width, self.width); + break; + case RCTBorderSideLeft: + layer.frame = CGRectMake(xPosition, yPosition, self.width, superlayerSize.height); + [layer.superlayer insertSublayer:layer atIndex:0]; + break; + } +} + +// Disable animations for layer +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event +{ + return (id)[NSNull null]; +} + +@end + +@implementation UIView (ReactKitBorders) + +- (void)reactSetBorders +{ + NSMutableDictionary *borders = objc_getAssociatedObject(self, @selector(_createOrGetBorderWithSide:)); + if (borders) { + for (RCTSingleSidedBorder *border in [borders allValues]) { + [border superLayerBoundsDidChange]; + } + } +} + +- (RCTSingleSidedBorder *)reactBorderTop +{ + return [self _createOrGetBorderWithSide:RCTBorderSideTop]; +} + +- (RCTSingleSidedBorder *)reactBorderRight +{ + return [self _createOrGetBorderWithSide:RCTBorderSideRight]; +} + +- (RCTSingleSidedBorder *)reactBorderBottom +{ + return [self _createOrGetBorderWithSide:RCTBorderSideBottom]; +} + +- (RCTSingleSidedBorder *)reactBorderLeft +{ + return [self _createOrGetBorderWithSide:RCTBorderSideLeft]; +} + +- (RCTSingleSidedBorder *)_createOrGetBorderWithSide:(RCTBorderSide)side +{ + NSMutableDictionary *borders = objc_getAssociatedObject(self, _cmd); + if (!borders) { + borders = [[NSMutableDictionary alloc] init]; + objc_setAssociatedObject(self, _cmd, borders, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + + RCTSingleSidedBorder *border = [borders objectForKey:@(side)]; + if (!border) { + border = [[RCTSingleSidedBorder alloc] initWithSide:side superlayer:self.layer]; + [borders setObject:border forKey:@(side)]; + } + return border; +} + @end diff --git a/package.json b/package.json index 38ac7fd4ba..201942427c 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,11 @@ "dependencies": { "connect": "2.8.3", "jstransform": "10.0.1", + "react-timer-mixin": "^0.13.1", + "react-tools": "0.13.0-rc2", + "rebound": "^0.0.12", "source-map": "0.1.31", - "stacktrace-parser": "0.1.1", - "react-tools": "0.13.0-rc2" + "stacktrace-parser": "0.1.1" }, "devDependencies": { "ws": "0.4.31", diff --git a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js b/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js index 1388a610c1..c56593cfac 100644 --- a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js +++ b/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js @@ -32,6 +32,8 @@ function ModuleDescriptor(fields) { this.isAsset = fields.isAsset || false; + this.altId = fields.altId; + this._fields = fields; } diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js index 8ebe9c5a63..962a069468 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -58,8 +58,8 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - {id: 'index', path: '/root/index.js', dependencies: ['a']}, - {id: 'a', path: '/root/a.js', dependencies: []}, + {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['a']}, + {id: 'a', altId: '/root/a.js', path: '/root/a.js', dependencies: []}, ]); }); }); @@ -88,7 +88,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - {id: 'index', path: '/root/index.js', dependencies: ['image!a']}, + {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['image!a']}, { id: 'image!a', path: '/root/imgs/a.png', dependencies: [], @@ -124,8 +124,8 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - {id: 'index', path: '/root/index.js', dependencies: ['a']}, - {id: 'a', path: '/root/a.js', dependencies: ['index']}, + {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['a']}, + {id: 'a', altId: '/root/a.js', path: '/root/a.js', dependencies: ['index']}, ]); }); }); @@ -157,7 +157,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - {id: 'index', path: '/root/index.js', dependencies: ['aPackage']}, + {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, { id: 'aPackage/main', path: '/root/aPackage/main.js', dependencies: [] @@ -196,6 +196,41 @@ describe('DependencyGraph', function() { }); }); + pit('should have altId for a package with providesModule', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': 'require("aPackage")', + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + }), + 'index.js': [ + '/**', + ' * @providesModule EpicModule', + ' */', + ].join('\n'), + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + {id: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, + { id: 'EpicModule', + altId: 'aPackage/index', + path: '/root/aPackage/index.js', + dependencies: [] + }, + ]); + }); + }); + pit('should default use index.js if main is a dir', function() { var root = '/root'; fs.__setMockFilesystem({ @@ -229,6 +264,36 @@ describe('DependencyGraph', function() { }); }); + pit('should resolve require to index if it is a dir', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'package.json': JSON.stringify({ + name: 'test', + }), + 'index.js': 'require("./lib/")', + lib: { + 'index.js': 'lol', + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + {id: 'test/index', path: '/root/index.js', dependencies: ['./lib/']}, + { id: 'test/lib/index', + path: '/root/lib/index.js', + dependencies: [] + }, + ]); + }); + }); + pit('should ignore malformed packages', function() { var root = '/root'; fs.__setMockFilesystem({ @@ -253,7 +318,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - {id: 'index', path: '/root/index.js', dependencies: ['aPackage']}, + {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, ]); }); }); @@ -297,10 +362,12 @@ describe('DependencyGraph', function() { expect(dgraph.getOrderedDependencies('/root/somedir/somefile.js')) .toEqual([ { id: 'index', + altId: '/root/somedir/somefile.js', path: '/root/somedir/somefile.js', dependencies: ['c'] }, { id: 'c', + altId: '/root/c.js', path: '/root/c.js', dependencies: [] }, @@ -340,11 +407,12 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage'] }, { id: 'aPackage', + altId: '/root/b.js', path: '/root/b.js', dependencies: [] }, @@ -372,7 +440,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['lolomg'] } @@ -410,7 +478,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage/subdir/lolynot'] }, @@ -453,7 +521,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage/subdir/lolynot'] }, @@ -496,7 +564,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage'] }, @@ -570,7 +638,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage'] }, @@ -621,7 +689,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage'] }, @@ -671,7 +739,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage', 'foo'] }, @@ -730,7 +798,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage', 'foo'] }, @@ -739,10 +807,12 @@ describe('DependencyGraph', function() { dependencies: ['bar'] }, { id: 'bar', + altId: '/root/bar.js', path: '/root/bar.js', dependencies: ['foo'] }, { id: 'foo', + altId: '/root/foo.js', path: '/root/foo.js', dependencies: ['aPackage'] }, @@ -803,7 +873,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage', 'foo'] }, @@ -812,6 +882,7 @@ describe('DependencyGraph', function() { dependencies: ['bar'] }, { id: 'foo', + altId: '/root/foo.js', path: '/root/foo.js', dependencies: ['aPackage'] }, @@ -857,7 +928,7 @@ describe('DependencyGraph', function() { return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ - { id: 'index', + { id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage', 'foo'] }, @@ -866,6 +937,7 @@ describe('DependencyGraph', function() { dependencies: [] }, { id: 'foo', + altId: '/root/foo.js', path: '/root/foo.js', dependencies: ['aPackage'] }, diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index 4622864d28..adb0128284 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -216,6 +216,13 @@ DependecyGraph.prototype.resolveDependency = function( modulePath = withExtJs(path.join(dir, depModuleId)); dep = this._graph[modulePath]; + + if (dep == null) { + modulePath = path.join(dir, depModuleId, 'index.js'); + } + + dep = this._graph[modulePath]; + if (dep == null) { debug( 'WARNING: Cannot find required module `%s` from module `%s`.' + @@ -366,6 +373,10 @@ DependecyGraph.prototype._processModule = function(modulePath) { if (moduleDocBlock.providesModule || moduleDocBlock.provides) { moduleData.id = moduleDocBlock.providesModule || moduleDocBlock.provides; + + // Incase someone wants to require this module via + // packageName/path/to/module + moduleData.altId = self._lookupName(modulePath); } else { moduleData.id = self._lookupName(modulePath); } @@ -401,6 +412,10 @@ DependecyGraph.prototype._deleteModule = function(module) { if (this._moduleById[module.id] === module) { delete this._moduleById[module.id]; } + + if (module.altId && this._moduleById[module.altId] === module) { + delete this._moduleById[module.altId]; + } }; /** @@ -424,6 +439,12 @@ DependecyGraph.prototype._updateGraphWithModule = function(module) { } this._moduleById[module.id] = module; + + // Some module maybe refrenced by both @providesModule and + // require(package/moduleName). + if (module.altId != null && this._moduleById[module.altId] == null) { + this._moduleById[module.altId] = module; + } }; /** diff --git a/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js b/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js index 90ae588814..69354590d5 100644 --- a/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js @@ -220,7 +220,7 @@ describe('HasteDependencyResolver', function() { }); describe('wrapModule', function() { - it('should ', function() { + it('should resolve modules', function() { var depResolver = new HasteDependencyResolver({ projectRoot: '/root', }); diff --git a/packager/react-packager/src/Packager/Package.js b/packager/react-packager/src/Packager/Package.js index 99edbbe0a6..0f55c8edcc 100644 --- a/packager/react-packager/src/Packager/Package.js +++ b/packager/react-packager/src/Packager/Package.js @@ -68,10 +68,9 @@ Package.prototype._getSource = function() { Package.prototype._getInlineSourceMap = function() { if (this._inlineSourceMap == null) { var sourceMap = this.getSourceMap({excludeSource: true}); - this._inlineSourceMap = '\nRAW_SOURCE_MAP = ' + - JSON.stringify(sourceMap) + ';'; + var encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64'); + this._inlineSourceMap = 'data:application/json;base64,' + encoded; } - return this._inlineSourceMap; }; @@ -85,13 +84,14 @@ Package.prototype.getSource = function(options) { } var source = this._getSource(); + source += '\n\/\/@ sourceMappingURL='; if (options.inlineSourceMap) { source += this._getInlineSourceMap(); + } else { + source += this._sourceMapUrl; } - source += '\n\/\/@ sourceMappingURL=' + this._sourceMapUrl; - return source; }; diff --git a/packager/react-packager/src/Packager/__tests__/Package-test.js b/packager/react-packager/src/Packager/__tests__/Package-test.js index ee94437d12..5a7438d27f 100644 --- a/packager/react-packager/src/Packager/__tests__/Package-test.js +++ b/packager/react-packager/src/Packager/__tests__/Package-test.js @@ -29,11 +29,10 @@ describe('Package', function() { ppackage.addModule('transformed foo;', 'source foo', 'foo path'); ppackage.addModule('transformed bar;', 'source bar', 'bar path'); ppackage.finalize({}); - expect(ppackage.getSource({inlineSourceMap: true})).toBe([ + expect(ppackage.getSource()).toBe([ 'transformed foo;', 'transformed bar;', - 'RAW_SOURCE_MAP = "test-source-map";', - '\/\/@ sourceMappingURL=test_url', + '\/\/@ sourceMappingURL=test_url' ].join('\n')); }); @@ -42,11 +41,10 @@ describe('Package', function() { ppackage.addModule('transformed bar;', 'source bar', 'bar path'); ppackage.setMainModuleId('foo'); ppackage.finalize({runMainModule: true}); - expect(ppackage.getSource({inlineSourceMap: true})).toBe([ + expect(ppackage.getSource()).toBe([ 'transformed foo;', 'transformed bar;', ';require("foo");', - 'RAW_SOURCE_MAP = "test-source-map";', '\/\/@ sourceMappingURL=test_url', ].join('\n')); }); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 19ec003954..f40ebeeaf8 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -98,7 +98,7 @@ Server.prototype._rebuildPackages = function() { packages[key] = buildPackage(options).then(function(p) { // Make a throwaway call to getSource to cache the source string. p.getSource({ - inlineSourceMap: options.dev, + inlineSourceMap: options.inlineSourceMap, minify: options.minify, }); return p; @@ -228,7 +228,7 @@ Server.prototype.processRequest = function(req, res, next) { function(p) { if (requestType === 'bundle') { res.end(p.getSource({ - inlineSourceMap: options.dev, + inlineSourceMap: options.inlineSourceMap, minify: options.minify, })); Activity.endEvent(startReqEventId); @@ -264,6 +264,11 @@ function getOptionsFromUrl(reqUrl) { dev: getBoolOptionFromQuery(urlObj.query, 'dev', true), minify: getBoolOptionFromQuery(urlObj.query, 'minify'), runModule: getBoolOptionFromQuery(urlObj.query, 'runModule', true), + inlineSourceMap: getBoolOptionFromQuery( + urlObj.query, + 'inlineSourceMap', + false + ), }; } diff --git a/runXcodeTests.sh b/runXcodeTests.sh new file mode 100755 index 0000000000..ae3255fb51 --- /dev/null +++ b/runXcodeTests.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# Run from react-native root + +set -e + +xctool \ + -project IntegrationTests/IntegrationTests.xcodeproj \ + -scheme IntegrationTests \ + -sdk iphonesimulator8.1 \ + -destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \ + build test + +xctool \ + -project Examples/UIExplorer/UIExplorer.xcodeproj \ + -scheme UIExplorer \ + -sdk iphonesimulator8.1 \ + -destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \ + build test