diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index 56686765b..fc31a90dc 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -31,8 +31,16 @@ var MovieScreen = require('./MovieScreen'); var fetch = require('fetch'); +/** + * This is for demo purposes only, and rate limited. + * In case you want to use the Rotten Tomatoes' API on a real app you should + * create an account at http://developer.rottentomatoes.com/ + */ var API_URL = 'http://api.rottentomatoes.com/api/public/v1.0/'; -var API_KEYS = ['7waqfqbprs7pajbz28mqf6vz', 'y4vwv8m33hed9ety83jmv52f']; +var API_KEYS = [ + '7waqfqbprs7pajbz28mqf6vz', + // 'y4vwv8m33hed9ety83jmv52f', Fallback api_key +]; // Results should be cached keyed by the query // with values of null meaning "being fetched" diff --git a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj index 48808c4e4..519b71cd2 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 00481BE81AC0C86700671115 /* libRCTWebSocketDebugger.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00481BE61AC0C7FA00671115 /* libRCTWebSocketDebugger.a */; }; 00481BEA1AC0C89D00671115 /* libicucore.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 00481BE91AC0C89D00671115 /* libicucore.dylib */; }; + 008F07F31AC5B25A0029DE68 /* main.jsbundle in Resources */ = {isa = PBXBuildFile; fileRef = 008F07F21AC5B25A0029DE68 /* main.jsbundle */; }; 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E61ABCBA2D00DB3ED1 /* libRCTAdSupport.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302B41ABCB8E700DB3ED1 /* libRCTAdSupport.a */; }; 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; @@ -92,6 +93,7 @@ /* Begin PBXFileReference section */ 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocketDebugger.xcodeproj; path = ../../Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj; sourceTree = ""; }; 00481BE91AC0C89D00671115 /* libicucore.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libicucore.dylib; path = usr/lib/libicucore.dylib; sourceTree = SDKROOT; }; + 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = main.jsbundle; path = iOS/main.jsbundle; sourceTree = ""; }; 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; 00C302AF1ABCB8E700DB3ED1 /* RCTAdSupport.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAdSupport.xcodeproj; path = ../../Libraries/AdSupport/RCTAdSupport.xcodeproj; sourceTree = ""; }; 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../../Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; @@ -189,6 +191,7 @@ 13B07FAE1A68108700A75B9A /* SampleApp */ = { isa = PBXGroup; children = ( + 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 13B07FB01A68108700A75B9A /* AppDelegate.m */, 13B07FB51A68108700A75B9A /* Images.xcassets */, @@ -405,6 +408,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 008F07F31AC5B25A0029DE68 /* main.jsbundle in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, ); diff --git a/Examples/SampleApp/iOS/AppDelegate.m b/Examples/SampleApp/iOS/AppDelegate.m index 1d8049ac6..5b6cc2d58 100644 --- a/Examples/SampleApp/iOS/AppDelegate.m +++ b/Examples/SampleApp/iOS/AppDelegate.m @@ -31,7 +31,7 @@ // OPTION 2 // Load from pre-bundled file on disk. To re-generate the static bundle, run // - // $ curl http://localhost:8081/Examples/SampleApp/index.ios.bundle -o main.jsbundle + // $ curl 'http://localhost:8081/Examples/SampleApp/index.ios.bundle?dev=false&minify=true' -o iOS/main.jsbundle // // and uncomment the next following line // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; diff --git a/Examples/SampleApp/iOS/main.jsbundle b/Examples/SampleApp/iOS/main.jsbundle new file mode 100644 index 000000000..7cc6a2adc --- /dev/null +++ b/Examples/SampleApp/iOS/main.jsbundle @@ -0,0 +1,5 @@ +// Offline JS +// To re-generate the offline bundle, run this from root of your project +// $ curl 'http://localhost:8081/Examples/SampleApp/index.ios.bundle?dev=false&minify=true' -o iOS/main.jsbundle + +throw new Error('Offline JS file is empty. See iOS/main.jsbundle for instructions'); diff --git a/Examples/UIExplorer/ListViewPagingExample.js b/Examples/UIExplorer/ListViewPagingExample.js index 2d26868e4..36cc82d86 100644 --- a/Examples/UIExplorer/ListViewPagingExample.js +++ b/Examples/UIExplorer/ListViewPagingExample.js @@ -226,9 +226,9 @@ var styles = StyleSheet.create({ var animations = { layout: { spring: { - duration: 0.75, + duration: 750, create: { - duration: 0.3, + duration: 300, type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.opacity, }, @@ -238,13 +238,13 @@ var animations = { }, }, easeInEaseOut: { - duration: 0.3, + duration: 300, create: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.scaleXY, }, update: { - delay: 0.1, + delay: 100, type: LayoutAnimation.Types.easeInEaseOut, }, }, diff --git a/Examples/UIExplorer/ScrollViewExample.js b/Examples/UIExplorer/ScrollViewExample.js index 62ec410f2..69f3ac9c7 100644 --- a/Examples/UIExplorer/ScrollViewExample.js +++ b/Examples/UIExplorer/ScrollViewExample.js @@ -33,7 +33,7 @@ exports.examples = [ return ( { console.log('onScroll!'); }} - throttleScrollCallbackMS={200} + scrollEventThrottle={200} contentInset={{top: -50}} style={styles.scrollView}> {THUMBS.map(createThumbRow)} diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index eef292fb5..4889bc472 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -76,6 +76,15 @@ exports.examples = [ ); }, +}, { + title: 'Padding', + render: function() { + return ( + + This text is indented by 10px padding on all sides. + + ); + }, }, { title: 'Font Family', render: function() { diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index a52284b85..4e54c38b8 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -174,6 +174,89 @@ exports.examples = [ ); } }, + { + title: 'Keyboard types', + render: function() { + var keyboardTypes = [ + 'default', + 'ascii-capable', + 'numbers-and-punctuation', + 'url', + 'number-pad', + 'phone-pad', + 'name-phone-pad', + 'email-address', + 'decimal-pad', + 'twitter', + 'web-search', + 'numeric', + ]; + var examples = keyboardTypes.map((type) => { + return ( + + + + ); + }); + return {examples}; + } + }, + { + title: 'Return key types', + render: function() { + var returnKeyTypes = [ + 'default', + 'go', + 'google', + 'join', + 'next', + 'route', + 'search', + 'send', + 'yahoo', + 'done', + 'emergency-call', + ]; + var examples = returnKeyTypes.map((type) => { + return ( + + + + ); + }); + return {examples}; + } + }, + { + title: 'Enable return key automatically', + render: function() { + return ( + + + + + + ); + } + }, + { + title: 'Secure text entry', + render: function() { + return ( + + + + + + ); + } + }, { title: 'Event handling', render: function(): ReactElement { return }, diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj.rej b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj.rej new file mode 100644 index 000000000..fa002c8b0 --- /dev/null +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj.rej @@ -0,0 +1,72 @@ +diff a/Libraries/FBReactKit/js/react-native-github/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Libraries/FBReactKit/js/react-native-github/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj (rejected hunks) +@@ -19,6 +19,7 @@ + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; ++ D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; }; + /* End PBXBuildFile section */ + + /* Begin PBXContainerItemProxy section */ +@@ -78,6 +79,13 @@ + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTActionSheet; + }; ++ D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */ = { ++ isa = PBXContainerItemProxy; ++ containerPortal = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */; ++ proxyType = 2; ++ remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; ++ remoteInfo = RCTVibration; ++ }; + /* End PBXContainerItemProxy section */ + + /* Begin PBXFileReference section */ +@@ -98,6 +106,7 @@ + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; ++ D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; + /* End PBXFileReference section */ + + /* Begin PBXFrameworksBuildPhase section */ +@@ -112,6 +121,7 @@ + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( ++ D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */, + 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */, + 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */, + 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */, +@@ -145,6 +155,7 @@ + 1316A21D1AA397F400C0188E /* Libraries */ = { + isa = PBXGroup; + children = ( ++ D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */, + 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */, + 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */, + 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */, +@@ -334,6 +353,10 @@ + ProjectRef = 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */; + }, + { ++ ProductGroup = D85B82921AB6D5CE003F4FE2 /* Products */; ++ ProjectRef = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */; ++ }, ++ { + ProductGroup = 13417FFB1AA91531003F314A /* Products */; + ProjectRef = 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */; + }, +@@ -396,6 +419,13 @@ + remoteRef = 147CED4A1AB34F8C00DA3E4C /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; ++ D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */ = { ++ isa = PBXReferenceProxy; ++ fileType = archive.ar; ++ path = libRCTVibration.a; ++ remoteRef = D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */; ++ sourceTree = BUILT_PRODUCTS_DIR; ++ }; + /* End PBXReferenceProxy section */ + + /* Begin PBXResourcesBuildPhase section */ diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Examples/UIExplorer/UIExplorer.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..e668abba3 --- /dev/null +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json index 65917726f..9c8120dff 100644 --- a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json +++ b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json @@ -1,10 +1,5 @@ { "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "story-background.png" - }, { "idiom" : "universal", "scale" : "2x", diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/story-background.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/story-background.png deleted file mode 100644 index cbb77db62..000000000 Binary files a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/story-background.png and /dev/null differ diff --git a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png index 044576efc..5b71135b9 100644 Binary files a/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp/testTextExampleSnapshot_1@2x.png differ diff --git a/Libraries/Animation/AnimationUtils.js b/Libraries/Animation/AnimationUtils.js index 56a3455b7..ae9be5ccf 100644 --- a/Libraries/Animation/AnimationUtils.js +++ b/Libraries/Animation/AnimationUtils.js @@ -211,21 +211,27 @@ var ticksPerSecond = 60; var lastUsedTag = 0; module.exports = { + /** + * Returns a new unique animation tag. + */ allocateTag: function(): number { return ++lastUsedTag; }, + /** + * Returns the values of the easing function at discrete ticks (60 per second). + */ evaluateEasingFunction: function(duration: number, easing: string | EasingFunction): Array { if (typeof easing === 'string') { easing = defaults[easing] || defaults.easeOutQuad; } var tickCount = Math.round(duration * ticksPerSecond / 1000); - var sample = []; + var samples = []; for (var i = 0; i <= tickCount; i++) { - sample.push(easing(i / tickCount)); + samples.push(easing.call(defaults, i / tickCount)); } - return sample; + return samples; }, }; diff --git a/Libraries/Animation/LayoutAnimation.js b/Libraries/Animation/LayoutAnimation.js index b68656984..12128055c 100644 --- a/Libraries/Animation/LayoutAnimation.js +++ b/Libraries/Animation/LayoutAnimation.js @@ -92,13 +92,13 @@ var LayoutAnimation = { configChecker: configChecker, Presets: { easeInEaseOut: create( - 0.3, Types.easeInEaseOut, Properties.opacity + 300, Types.easeInEaseOut, Properties.opacity ), linear: create( - 0.5, Types.linear, Properties.opacity + 500, Types.linear, Properties.opacity ), spring: { - duration: 0.7, + duration: 700, create: { type: Types.linear, property: Properties.opacity, diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index c9756908a..bb283e1ce 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -11,7 +11,6 @@ */ 'use strict'; -var NativeModules = require('NativeModules'); var NativeModules = require('NativeModules'); var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); var Subscribable = require('Subscribable'); diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 92cdab05a..ba6a2c8b1 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -64,7 +64,7 @@ var ScrollView = React.createClass({ showsHorizontalScrollIndicator: PropTypes.bool, showsVerticalScrollIndicator: PropTypes.bool, style: StyleSheetPropType(ViewStylePropTypes), - throttleScrollCallbackMS: PropTypes.number, // null + scrollEventThrottle: PropTypes.number, // null /** * When true, the scroll view bounces horizontally when it reaches the end @@ -211,12 +211,12 @@ var ScrollView = React.createClass({ ); } if (__DEV__) { - if (this.props.onScroll && !this.props.throttleScrollCallbackMS) { + if (this.props.onScroll && !this.props.scrollEventThrottle) { var onScroll = this.props.onScroll; this.props.onScroll = function() { console.log( 'You specified `onScroll` on a but not ' + - '`throttleScrollCallbackMS`. You will only receive one event. ' + + '`scrollEventThrottle`. You will only receive one event. ' + 'Using `16` you get all the events but be aware that it may ' + 'cause frame drops, use a bigger number if you don\'t need as ' + 'much precision.' @@ -325,7 +325,7 @@ var validAttributes = { showsHorizontalScrollIndicator: true, showsVerticalScrollIndicator: true, stickyHeaderIndices: {diff: deepDiffer}, - throttleScrollCallbackMS: true, + scrollEventThrottle: true, zoomScale: true, }; diff --git a/Libraries/Components/TextInput/TextInput.ios.js b/Libraries/Components/TextInput/TextInput.js similarity index 80% rename from Libraries/Components/TextInput/TextInput.ios.js rename to Libraries/Components/TextInput/TextInput.js index 02fec4d5a..0b30fd7a5 100644 --- a/Libraries/Components/TextInput/TextInput.ios.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -15,6 +15,7 @@ var DocumentSelectionState = require('DocumentSelectionState'); var EventEmitter = require('EventEmitter'); var NativeMethodsMixin = require('NativeMethodsMixin'); var RCTUIManager = require('NativeModules').UIManager; +var Platform = require('Platform'); var PropTypes = require('ReactPropTypes'); var React = require('React'); var ReactChildren = require('ReactChildren'); @@ -27,12 +28,12 @@ var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); var emptyFunction = require('emptyFunction'); -var getObjectValues = require('getObjectValues'); var invariant = require('invariant'); var merge = require('merge'); var autoCapitalizeConsts = RCTUIManager.UIText.AutocapitalizationType; -var clearButtonModeConsts = RCTUIManager.UITextField.clearButtonMode; +var keyboardTypeConsts = RCTUIManager.UIKeyboardType; +var returnKeyTypeConsts = RCTUIManager.UIReturnKeyType; var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { autoCorrect: true, @@ -44,6 +45,9 @@ var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { fontStyle: true, fontWeight: true, keyboardType: true, + returnKeyType: true, + enablesReturnKeyAutomatically: true, + secureTextEntry: true, mostRecentEventCounter: true, placeholder: true, placeholderTextColor: true, @@ -66,6 +70,27 @@ var notMultiline = { onSubmitEditing: true, }; +var TextInputAndroidAttributes = { + autoCapitalize: true, + autoCorrect: true, + autoFocus: true, + keyboardType: true, + multiline: true, + password: true, + placeholder: true, + value: true, + testID: true, +}; + +var AndroidTextInput = createReactIOSNativeComponentClass({ + validAttributes: TextInputAndroidAttributes, + uiViewClassName: 'AndroidTextInput', +}); + +var crossPlatformKeyboardTypeMap = { + 'numeric': 'decimal-pad', +}; + type DefaultProps = { bufferDelay: number; }; @@ -138,8 +163,47 @@ var TextInput = React.createClass({ */ keyboardType: PropTypes.oneOf([ 'default', + // iOS + 'ascii-capable', + 'numbers-and-punctuation', + 'url', + 'number-pad', + 'phone-pad', + 'name-phone-pad', + 'email-address', + 'decimal-pad', + 'twitter', + 'web-search', + // Cross-platform 'numeric', ]), + /** + * Determines how the return key should look. + */ + returnKeyType: PropTypes.oneOf([ + 'default', + 'go', + 'google', + 'join', + 'next', + 'route', + 'search', + 'send', + 'yahoo', + 'done', + 'emergency-call', + ]), + /** + * If true, the keyboard disables the return key when there is no text and + * automatically enables it when there is text. Default value is false. + */ + enablesReturnKeyAutomatically: PropTypes.bool, + + /** + * If true, the text input obscures the text entered so that sensitive text + * like passwords stay secure. Default value is false. + */ + secureTextEntry: PropTypes.bool, /** * If true, the text input can be multiple lines. Default value is false. */ @@ -157,10 +221,15 @@ var TextInput = React.createClass({ * * Callback that is called when the text input's text changes. */ + onChange: PropTypes.func, onChangeText: PropTypes.func, onEndEditing: PropTypes.func, onSubmitEditing: PropTypes.func, + /** + * If true, the TextInput will be a password field. Default value is false. + */ + password: PropTypes.bool, /** * The string that will be rendered before text input has been entered */ @@ -202,6 +271,10 @@ var TextInput = React.createClass({ ]), style: Text.propTypes.style, + /** + * Used to locate this view in end-to-end tests. + */ + testID: PropTypes.string, }, /** @@ -313,10 +386,24 @@ var TextInput = React.createClass({ }, render: function() { + if (Platform.OS === 'ios') { + return this._renderIOs(); + } else if (Platform.OS === 'android') { + return this._renderAndroid(); + } + }, + + _renderIOs: function() { var textContainer; var autoCapitalize = autoCapitalizeConsts[this.props.autoCapitalize]; - var clearButtonMode = clearButtonModeConsts[this.props.clearButtonMode]; + var clearButtonMode = RCTUIManager.UITextField.clearButtonMode[this.props.clearButtonMode]; + + var keyboardType = keyboardTypeConsts[ + crossPlatformKeyboardTypeMap[this.props.keyboardType] || + this.props.keyboardType + ]; + var returnKeyType = returnKeyTypeConsts[this.props.returnKeyType]; if (!this.props.multiline) { for (var propKey in onlyMultiline) { @@ -331,7 +418,10 @@ var TextInput = React.createClass({ ref="input" style={[styles.input, this.props.style]} enabled={this.props.editable} - keyboardType={this.props.keyboardType} + keyboardType={keyboardType} + returnKeyType={returnKeyType} + enablesReturnKeyAutomatically={this.props.enablesReturnKeyAutomatically} + secureTextEntry={this.props.secureTextEntry} onFocus={this._onFocus} onBlur={this._onBlur} onChange={this._onChange} @@ -373,6 +463,10 @@ var TextInput = React.createClass({ children={children} mostRecentEventCounter={this.state.mostRecentEventCounter} editable={this.props.editable} + keyboardType={keyboardType} + returnKeyType={returnKeyType} + enablesReturnKeyAutomatically={this.props.enablesReturnKeyAutomatically} + secureTextEntry={this.props.secureTextEntry} onFocus={this._onFocus} onBlur={this._onBlur} onChange={this._onChange} @@ -398,6 +492,27 @@ var TextInput = React.createClass({ ); }, + _renderAndroid: function() { + var autoCapitalize = autoCapitalizeConsts[this.props.autoCapitalize]; + return ( + + ); + }, + _onFocus: function(event: Event) { if (this.props.onFocus) { this.props.onFocus(event); diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index d2089fb56..080a6750c 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -50,7 +50,7 @@ var DEFAULT_PROPS = { * style={styles.button} * source={require('image!myButton')} * /> - * + * * ); * }, * ``` diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index fd9d05971..ff1df3851 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -39,7 +39,7 @@ var onlyChild = require('onlyChild'); * style={styles.button} * source={require('image!myButton')} * /> - * + * * ); * }, * ``` diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index b098fc0dc..ecf446725 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -325,8 +325,8 @@ var ListView = React.createClass({ stickyHeaderIndices: sectionHeaderIndices, } ); - if (!props.throttleScrollCallbackMS) { - props.throttleScrollCallbackMS = DEFAULT_SCROLL_CALLBACK_THROTTLE; + if (!props.scrollEventThrottle) { + props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE; } return ( diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 621fd4bfc..7c29e5c2c 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -985,7 +985,6 @@ var Navigator = React.createClass({ }, renderNavigationStackBar: function() { - var NavigationBarClass = this.props.NavigationBarClass; if (!this.props.navigationBar) { return null; } diff --git a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js index 580727dc8..3986064b1 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js +++ b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js @@ -46,15 +46,12 @@ 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; } + // TODO: rename `observedTopOfStack` to `presentedIndex` in `NavigatorIOS` + return navState.observedTopOfStack; }; @@ -85,7 +82,12 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({ titleContentForRoute: PropTypes.func, iconForRoute: PropTypes.func, }), - navigationBarStyles: PropTypes.number, + navState: React.PropTypes.shape({ + routeStack: React.PropTypes.arrayOf(React.PropTypes.object), + idStack: React.PropTypes.arrayOf(React.PropTypes.number), + presentedIndex: React.PropTypes.number, + }), + navigationBarStyles: View.propTypes.style, }, statics: { diff --git a/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js index 4e6c7c330..d88204950 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js +++ b/Libraries/CustomComponents/Navigator/NavigatorNavigationBar.js @@ -34,19 +34,31 @@ var View = require('View'); 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; } + // TODO: rename `observedTopOfStack` to `presentedIndex` in `NavigatorIOS` + return navState.observedTopOfStack; }; var NavigatorNavigationBar = React.createClass({ + propTypes: { + navigator: React.PropTypes.object, + navigationBarRouteMapper: React.PropTypes.shape({ + Title: React.PropTypes.func.isRequired, + LeftButton: React.PropTypes.func.isRequired, + RightButton: React.PropTypes.func.isRequired, + }), + navState: React.PropTypes.shape({ + routeStack: React.PropTypes.arrayOf(React.PropTypes.object), + idStack: React.PropTypes.arrayOf(React.PropTypes.number), + presentedIndex: React.PropTypes.number, + }), + navigationBarStyles: View.propTypes.style, + }, + statics: { Styles: NavigatorNavigationBarStyles, }, diff --git a/Libraries/Image/RCTImageDownloader.h b/Libraries/Image/RCTImageDownloader.h index 5c1490214..178bc734c 100644 --- a/Libraries/Image/RCTImageDownloader.h +++ b/Libraries/Image/RCTImageDownloader.h @@ -16,14 +16,30 @@ typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error); + (instancetype)sharedInstance; +/** + * Downloads a block of raw data and returns it. Note that the callback block + * will not be executed on the same thread you called the method from, nor on + * the main thread. Returns a token that can be used to cancel the download. + */ - (id)downloadDataForURL:(NSURL *)url block:(RCTDataDownloadBlock)block; +/** + * Downloads an image and decompresses it a the size specified. The compressed + * image will be cached in memory and to disk. Note that the callback block + * will not be executed on the same thread you called the method from, nor on + * the main thread. Returns a token that can be used to cancel the download. + */ - (id)downloadImageForURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale block:(RCTImageDownloadBlock)block; +/** + * Cancel an in-flight download. If multiple requets have been made for the + * same image, only the request that relates to the token passed will be + * cancelled. + */ - (void)cancelDownload:(id)downloadToken; @end diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index c1916dc19..3eb5c1b78 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -17,12 +17,12 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e @implementation RCTImageDownloader { RCTCache *_cache; + dispatch_queue_t _processingQueue; NSMutableDictionary *_pendingBlocks; } + (instancetype)sharedInstance { - RCTAssertMainThread(); static RCTImageDownloader *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -35,27 +35,32 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e { if ((self = [super init])) { _cache = [[RCTCache alloc] initWithName:@"RCTImageDownloader"]; - _pendingBlocks = [NSMutableDictionary dictionary]; + _processingQueue = dispatch_queue_create("com.facebook.React.DownloadProcessingQueue", DISPATCH_QUEUE_SERIAL); + _pendingBlocks = [[NSMutableDictionary alloc] init]; } return self; } -- (NSString *)cacheKeyForURL:(NSURL *)url +static NSString *RCTCacheKeyForURL(NSURL *url) { return url.absoluteString; } - (id)_downloadDataForURL:(NSURL *)url block:(RCTCachedDataDownloadBlock)block { - NSString *cacheKey = [self cacheKeyForURL:url]; + NSString *cacheKey = RCTCacheKeyForURL(url); __block BOOL cancelled = NO; __block NSURLSessionDataTask *task = nil; + dispatch_block_t cancel = ^{ + cancelled = YES; - NSMutableArray *pendingBlocks = _pendingBlocks[cacheKey]; - [pendingBlocks removeObject:block]; + dispatch_async(_processingQueue, ^{ + NSMutableArray *pendingBlocks = self->_pendingBlocks[cacheKey]; + [pendingBlocks removeObject:block]; + }); if (task) { [task cancel]; @@ -63,56 +68,60 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e } }; - NSMutableArray *pendingBlocks = _pendingBlocks[cacheKey]; - if (pendingBlocks) { - [pendingBlocks addObject:block]; - } else { - _pendingBlocks[cacheKey] = [NSMutableArray arrayWithObject:block]; - - __weak RCTImageDownloader *weakSelf = self; - RCTCachedDataDownloadBlock runBlocks = ^(BOOL cached, NSData *data, NSError *error) { - RCTImageDownloader *strongSelf = weakSelf; - NSArray *blocks = strongSelf->_pendingBlocks[cacheKey]; - [strongSelf->_pendingBlocks removeObjectForKey:cacheKey]; - - for (RCTCachedDataDownloadBlock block in blocks) { - block(cached, data, error); - } - }; - - if ([_cache hasDataForKey:cacheKey]) { - [_cache fetchDataForKey:cacheKey completionHandler:^(NSData *data) { - if (!cancelled) runBlocks(YES, data, nil); - }]; + dispatch_async(_processingQueue, ^{ + NSMutableArray *pendingBlocks = _pendingBlocks[cacheKey]; + if (pendingBlocks) { + [pendingBlocks addObject:block]; } else { - task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (!cancelled) runBlocks(NO, data, error); - }]; + _pendingBlocks[cacheKey] = [NSMutableArray arrayWithObject:block]; - [task resume]; + __weak RCTImageDownloader *weakSelf = self; + RCTCachedDataDownloadBlock runBlocks = ^(BOOL cached, NSData *data, NSError *error) { + + RCTImageDownloader *strongSelf = weakSelf; + NSArray *blocks = strongSelf->_pendingBlocks[cacheKey]; + [strongSelf->_pendingBlocks removeObjectForKey:cacheKey]; + for (RCTCachedDataDownloadBlock block in blocks) { + block(cached, data, error); + } + }; + + if ([_cache hasDataForKey:cacheKey]) { + [_cache fetchDataForKey:cacheKey completionHandler:^(NSData *data) { + if (!cancelled) { + runBlocks(YES, data, nil); + } + }]; + } else { + task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (!cancelled) { + runBlocks(NO, data, error); + } + }]; + + [task resume]; + } } - } + }); return [cancel copy]; } - (id)downloadDataForURL:(NSURL *)url block:(RCTDataDownloadBlock)block { - NSString *cacheKey = [self cacheKeyForURL:url]; + NSString *cacheKey = RCTCacheKeyForURL(url); __weak RCTImageDownloader *weakSelf = self; return [self _downloadDataForURL:url block:^(BOOL cached, NSData *data, NSError *error) { if (!cached) { RCTImageDownloader *strongSelf = weakSelf; [strongSelf->_cache setData:data forKey:cacheKey]; } - - dispatch_async(dispatch_get_main_queue(), ^{ - block(data, error); - }); + block(data, error); }]; } -- (id)downloadImageForURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale block:(RCTImageDownloadBlock)block +- (id)downloadImageForURL:(NSURL *)url size:(CGSize)size + scale:(CGFloat)scale block:(RCTImageDownloadBlock)block { return [self downloadDataForURL:url block:^(NSData *data, NSError *error) { @@ -142,20 +151,14 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); } - - // TODO: should we cache the decompressed image? - - dispatch_async(dispatch_get_main_queue(), ^{ - block(image, nil); - }); + block(image, nil); }]; } - (void)cancelDownload:(id)downloadToken { if (downloadToken) { - dispatch_block_t block = (id)downloadToken; - block(); + ((dispatch_block_t)downloadToken)(); } } diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 50bcfac75..3f8e3c75a 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -19,7 +19,8 @@ #import "RCTImageDownloader.h" #import "RCTLog.h" -NSError *errorWithMessage(NSString *message) { +NSError *errorWithMessage(NSString *message) +{ NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message}; NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]; return error; diff --git a/Libraries/Image/RCTNetworkImageView.m b/Libraries/Image/RCTNetworkImageView.m index c97b97cfd..6ad6a4c80 100644 --- a/Libraries/Image/RCTNetworkImageView.m +++ b/Libraries/Image/RCTNetworkImageView.m @@ -61,23 +61,24 @@ if ([imageURL.pathExtension caseInsensitiveCompare:@"gif"] == NSOrderedSame) { _downloadToken = [_imageDownloader downloadDataForURL:imageURL block:^(NSData *data, NSError *error) { if (data) { - CAKeyframeAnimation *animation = RCTGIFImageWithData(data); - CGImageRef firstFrame = (__bridge CGImageRef)animation.values.firstObject; - self.layer.bounds = CGRectMake(0, 0, CGImageGetWidth(firstFrame), CGImageGetHeight(firstFrame)); - self.layer.contentsScale = 1.0; - self.layer.contentsGravity = kCAGravityResizeAspect; - self.layer.minificationFilter = kCAFilterLinear; - self.layer.magnificationFilter = kCAFilterLinear; - [self.layer addAnimation:animation forKey:@"contents"]; + dispatch_async(dispatch_get_main_queue(), ^{ + CAKeyframeAnimation *animation = RCTGIFImageWithData(data); + self.layer.contentsScale = 1.0; + self.layer.minificationFilter = kCAFilterLinear; + self.layer.magnificationFilter = kCAFilterLinear; + [self.layer addAnimation:animation forKey:@"contents"]; + }); } // TODO: handle errors }]; } else { _downloadToken = [_imageDownloader downloadImageForURL:imageURL size:self.bounds.size scale:RCTScreenScale() block:^(UIImage *image, NSError *error) { if (image) { - [self.layer removeAnimationForKey:@"contents"]; - self.layer.contentsScale = image.scale; - self.layer.contents = (__bridge id)image.CGImage; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.layer removeAnimationForKey:@"contents"]; + self.layer.contentsScale = image.scale; + self.layer.contents = (__bridge id)image.CGImage; + }); } // TODO: handle errors }]; diff --git a/Libraries/LinkingIOS/LinkingIOS.js b/Libraries/LinkingIOS/LinkingIOS.js index 473bd0fa3..d3a845018 100644 --- a/Libraries/LinkingIOS/LinkingIOS.js +++ b/Libraries/LinkingIOS/LinkingIOS.js @@ -21,21 +21,86 @@ var _initialURL = RCTLinkingManager && var DEVICE_NOTIF_EVENT = 'openURL'; +/** + * `LinkingIOS` gives you a general interface to interact with both, incoming + * and outgoing app links. + * + * ### Basic Usage + * + * #### Handling deep links + * + * If your app was launched from a external url registered to your app you can + * access and handle it from any component you want with + * + * ``` + * componentDidMount() { + * var url = LinkingIOS.popInitialURL(); + * } + * ``` + * + * In case you also want to listen to incoming app links during your app's + * execution you'll need to add the following lines to you `*AppDelegate.m`: + * + * ``` + * - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { + * return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; + * } + * ``` + * + * And then on your React component you'll be able to listen to the events on + * `LinkingIOS` as follows + * + * ``` + * componentDidMount() { + * LinkingIOS.addEventListener('url', this._handleOpenURL); + * }, + * componentWillUnmount() { + * LinkingIOS.removeEventListener('url', this._handleOpenURL); + * }, + * _handleOpenURL(event) { + * console.log(event.url); + * } + * ``` + * + * #### Triggering App links + * + * To trigger an app link (browser, email or custom schemas) you call + * + * ``` + * LinkingIOS.openURL(url) + * ``` + * + * If you want to check if any installed app can handle a given url beforehand you can call + * ``` + * LinkingIOS.canOpenURL(url, (supported) => { + * if (!supported) { + * AlertIOS.alert('Can\'t handle url: ' + url); + * } else { + * LinkingIOS.openURL(url); + * } + * }); + * ``` + */ class LinkingIOS { - static addEventListener(type, handler) { + /** + * Add a handler to LinkingIOS changes by listening to the `url` event type + * and providing the handler + */ + static addEventListener(type: string, handler: Function) { invariant( type === 'url', 'LinkingIOS only supports `url` events' ); _notifHandlers[handler] = RCTDeviceEventEmitter.addListener( DEVICE_NOTIF_EVENT, - (notifData) => { - handler(new LinkingIOS(notifData)); - } + handler ); } - static removeEventListener(type, handler) { + /** + * Remove a handler by passing the `url` event type and the handler + */ + static removeEventListener(type: string, handler: Function ) { invariant( type === 'url', 'LinkingIOS only supports `url` events' @@ -47,7 +112,10 @@ class LinkingIOS { _notifHandlers[handler] = null; } - static openURL(url) { + /** + * Try to open the given `url` with any of the installed apps. + */ + static openURL(url: string) { invariant( typeof url === 'string', 'Invalid url: should be a string' @@ -55,7 +123,11 @@ class LinkingIOS { RCTLinkingManager.openURL(url); } - static canOpenURL(url, callback) { + /** + * Determine wether or not the an installed app can handle a given `url` + * The callback function will be called with `bool supported` as the only argument + */ + static canOpenURL(url: string, callback: Function) { invariant( typeof url === 'string', 'Invalid url: should be a string' @@ -67,7 +139,11 @@ class LinkingIOS { RCTLinkingManager.canOpenURL(url, callback); } - static popInitialURL() { + /** + * If the app launch was triggered by an app link, it will pop the link url, + * otherwise it will return `null` + */ + static popInitialURL(): ?string { var initialURL = _initialURL; _initialURL = null; return initialURL; diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js index 51be84ade..d98b997ca 100644 --- a/Libraries/Network/NetInfo.js +++ b/Libraries/Network/NetInfo.js @@ -34,7 +34,7 @@ type ReachabilityStateIOS = $Enum<{ * * ### reachabilityIOS * - * Asynchronously determine if the device is online and on a cellular network. + * Asyncronously determine if the device is online and on a cellular network. * * - `none` - device is offline * - `wifi` - device is online and connected via wifi, or is the iOS simulator diff --git a/Libraries/Portal/Portal.js b/Libraries/Portal/Portal.js new file mode 100644 index 000000000..762af3661 --- /dev/null +++ b/Libraries/Portal/Portal.js @@ -0,0 +1,70 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Portal + * @flow + */ +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var _portalRef: any; + +/* + * A container that renders all the modals on top of everything else in the application. + * + * Portal makes it possible for application code to pass modal views all the way up to + * the root element created in `renderApplication`. + * + * Never use `` in your code. There is only one Portal instance rendered + * by the top-level `renderApplication`. + */ +var Portal = React.createClass({ + statics: { + showModal: function(component) { + if (!_portalRef) { + console.error('Calling showModal but no Portal has been rendered'); + return; + } + _portalRef.setState({modal: component}); + }, + + closeModal: function() { + if (!_portalRef) { + console.error('Calling closeModal but no Portal has been rendered'); + return; + } + _portalRef.setState({modal: null}); + }, + }, + + getInitialState: function() { + return {modal: (null: any)}; + }, + + render: function() { + _portalRef = this; + if (!this.state.modal) { + return ; + } + return ( + + {this.state.modal} + + ); + } +}); + +var styles = StyleSheet.create({ + modalsContainer: { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + }, +}); + +module.exports = Portal; diff --git a/Libraries/ReactIOS/renderApplication.js b/Libraries/ReactIOS/renderApplication.js index 1d880653e..dbec1e853 100644 --- a/Libraries/ReactIOS/renderApplication.js +++ b/Libraries/ReactIOS/renderApplication.js @@ -11,7 +11,10 @@ */ 'use strict'; +var Portal = require('Portal'); var React = require('React'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); var invariant = require('invariant'); @@ -24,12 +27,27 @@ function renderApplication( rootTag, 'Expect to have a valid rootTag, instead got ', rootTag ); - React.render( - , + React.render( + + + + , rootTag ); } +var styles = StyleSheet.create({ + // This is needed so the application covers the whole screen + // and therefore the contents of the Portal are not clipped. + appContainer: { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + }, +}); + module.exports = renderApplication; diff --git a/Libraries/Text/RCTText.h b/Libraries/Text/RCTText.h index bb9fb6dfc..59b15668a 100644 --- a/Libraries/Text/RCTText.h +++ b/Libraries/Text/RCTText.h @@ -14,5 +14,6 @@ @property (nonatomic, copy) NSAttributedString *attributedText; @property (nonatomic, assign) NSLineBreakMode lineBreakMode; @property (nonatomic, assign) NSUInteger numberOfLines; +@property (nonatomic, assign) UIEdgeInsets contentInset; @end diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 457079bcb..a2f7f11cc 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -72,20 +72,26 @@ [self setNeedsDisplay]; } +- (CGRect)textFrame +{ + return UIEdgeInsetsInsetRect(self.bounds, _contentInset); +} + - (void)layoutSubviews { [super layoutSubviews]; // The header comment for `size` says that a height of 0.0 should be enough, // but it isn't. - _textContainer.size = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX); + _textContainer.size = CGSizeMake([self textFrame].size.width, CGFLOAT_MAX); } - (void)drawRect:(CGRect)rect { + CGPoint origin = [self textFrame].origin; NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer]; - [_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:CGPointZero]; - [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:CGPointZero]; + [_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:origin]; + [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:origin]; } - (NSNumber *)reactTagAtPoint:(CGPoint)point diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 95896fd05..8df67e59e 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -124,4 +124,13 @@ RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText) }; } +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView +{ + NSNumber *reactTag = shadowView.reactTag; + UIEdgeInsets padding = shadowView.paddingAsInsets; + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + ((RCTText *)viewRegistry[reactTag]).contentInset = padding; + }; +} + @end diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index c0bb989f4..46148bd86 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -69,6 +69,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { TestUtils: undefined, batchedUpdates: require('ReactUpdates').batchedUpdates, cloneWithProps: require('cloneWithProps'), + createFragment: require('ReactFragment').create, update: require('update'), }, }); diff --git a/React.podspec b/React.podspec index b2796ffa1..4ad740e4a 100644 --- a/React.podspec +++ b/React.podspec @@ -1,93 +1,78 @@ Pod::Spec.new do |s| - s.name = "React" - s.version = "0.3.1" - s.summary = "Build high quality mobile apps using React." - s.description = <<-DESC - React Native apps are built using the React JS - framework, and render directly to native UIKit - elements using a fully asynchronous architecture. - There is no browser and no HTML. We have picked what - we think is the best set of features from these and - other technologies to build what we hope to become - the best product development framework available, - with an emphasis on iteration speed, developer - delight, continuity of technology, and absolutely - beautiful and fast products with no compromises in - quality or capability. - DESC - s.homepage = "http://facebook.github.io/react-native/" - s.license = "BSD" - s.author = "Facebook" - s.source = { :git => "https://github.com/facebook/react-native.git", :tag => "v#{s.version}" } - s.default_subspec = 'Core' - s.requires_arc = true - s.platform = :ios, "7.0" - s.prepare_command = 'npm install' - s.preserve_paths = "cli.js", "Libraries/**/*.js", "lint", "linter.js", "node_modules", "package.json", "packager", "PATENTS", "react-native-cli" - - s.subspec 'Core' do |ss| - ss.source_files = "React/**/*.{c,h,m}" - ss.exclude_files = "**/__tests__/*", "IntegrationTests/*" - ss.frameworks = "JavaScriptCore" - end + s.name = "React" + s.version = "0.1.0" + s.summary = "Build high quality mobile apps using React." + s.description= <<-DESC + React Native apps are built using the React JS framework, + and render directly to native UIKit elements using a fully + asynchronous architecture. There is no browser and no HTML. + We have picked what we think is the best set of features from + these and other technologies to build what we hope to become + the best product development framework available, with an + emphasis on iteration speed, developer delight, continuity + of technology, and absolutely beautiful and fast products + with no compromises in quality or capability. + DESC + s.homepage = "http://facebook.github.io/react-native/" + s.license = "BSD" + s.author = "Facebook" + s.platform = :ios, "7.0" + s.source = { :git => "https://github.com/facebook/react-native.git", :tag => "v#{s.version}" } + s.source_files = "React/**/*.{c,h,m}" + s.resources = "Resources/*.png" + s.preserve_paths = "cli.js", "Libraries/**/*.js", "lint", "linter.js", "node_modules", "package.json", "packager", "PATENTS", "react-native-cli" + s.exclude_files = "**/__tests__/*", "IntegrationTests/*" + s.frameworks = "JavaScriptCore" + s.requires_arc = true + s.prepare_command = 'npm install' + s.libraries = 'icucore' s.subspec 'RCTActionSheet' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/ActionSheetIOS/*.{h,m}" + ss.source_files = "Libraries/ActionSheetIOS/*.{h,m}" ss.preserve_paths = "Libraries/ActionSheetIOS/*.js" end s.subspec 'RCTAdSupport' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/AdSupport/*.{h,m}" - ss.preserve_paths = "Libraries/AdSupport/*.js" + ss.source_files = "Libraries/RCTAdSupport/*.{h,m}" + ss.preserve_paths = "Libraries/RCTAdSupport/*.js" end s.subspec 'RCTAnimation' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Animation/*.{h,m}" - ss.preserve_paths = "Libraries/Animation/*.js" + ss.source_files = "Libraries/Animation/*.{h,m}" + ss.preserve_paths = "Libraries/Animation/*.js" end s.subspec 'RCTGeolocation' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Geolocation/*.{h,m}" - ss.preserve_paths = "Libraries/Geolocation/*.js" + ss.source_files = "Libraries/Geolocation/*.{h,m}" + ss.preserve_paths = "Libraries/Geolocation/*.js" end s.subspec 'RCTImage' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Image/*.{h,m}" - ss.preserve_paths = "Libraries/Image/*.js" + ss.source_files = "Libraries/Image/*.{h,m}" + ss.preserve_paths = "Libraries/Image/*.js" end s.subspec 'RCTNetwork' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Network/*.{h,m}" - ss.preserve_paths = "Libraries/Network/*.js" + ss.source_files = "Libraries/Network/*.{h,m}" + ss.preserve_paths = "Libraries/Network/*.js" end s.subspec 'RCTPushNotification' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/PushNotificationIOS/*.{h,m}" - ss.preserve_paths = "Libraries/PushNotificationIOS/*.js" + ss.source_files = "Libraries/PushNotificationIOS/*.{h,m}" + ss.preserve_paths = "Libraries/PushNotificationIOS/*.js" end s.subspec 'RCTWebSocketDebugger' do |ss| - ss.libraries = 'icucore' - ss.dependency 'React/Core' - ss.source_files = "Libraries/RCTWebSocketDebugger/*.{h,m}" + ss.source_files = "Libraries/RCTWebSocketDebugger/*.{h,m}" end s.subspec 'RCTText' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Text/*.{h,m}" - ss.preserve_paths = "Libraries/Text/*.js" + ss.source_files = "Libraries/Text/*.{h,m}" + ss.preserve_paths = "Libraries/Text/*.js" end s.subspec 'RCTVibration' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Vibration/*.{h,m}" - ss.preserve_paths = "Libraries/Vibration/*.js" + ss.source_files = "Libraries/Vibration/*.{h,m}" + ss.preserve_paths = "Libraries/Vibration/*.js" end end diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index ba8ef6986..666ce013e 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -52,6 +52,7 @@ + (UITextFieldViewMode)UITextFieldViewMode:(id)json; + (UIScrollViewKeyboardDismissMode)UIScrollViewKeyboardDismissMode:(id)json; + (UIKeyboardType)UIKeyboardType:(id)json; ++ (UIReturnKeyType)UIReturnKeyType:(id)json; + (UIViewContentMode)UIViewContentMode:(id)json; + (UIBarStyle)UIBarStyle:(id)json; @@ -123,10 +124,17 @@ BOOL RCTCopyProperty(id target, id source, NSString *keyPath); } #endif +/** + * This macro is used for creating simple converter functions that just call + * the specified getter method on the json value. + */ +#define RCT_CONVERTER(type, name, getter) \ +RCT_CUSTOM_CONVERTER(type, name, [json getter]) + /** * This macro is used for creating converter functions with arbitrary logic. */ -#define RCT_CONVERTER_CUSTOM(type, name, code) \ +#define RCT_CUSTOM_CONVERTER(type, name, code) \ + (type)name:(id)json \ { \ if (json == [NSNull null]) { \ @@ -143,20 +151,13 @@ BOOL RCTCopyProperty(id target, id source, NSString *keyPath); } \ } -/** - * This macro is used for creating simple converter functions that just call - * the specified getter method on the json value. - */ -#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]) +RCT_CUSTOM_CONVERTER(type, type, [[self NSNumber:json] getter]) /** * This macro is used for creating converters for enum types. @@ -189,57 +190,6 @@ RCT_CONVERTER_CUSTOM(type, type, [[self NSNumber:json] getter]) return value ? [value getter] : default; \ } -/** - * 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, _aliases) \ -+ (type)type:(id)json \ -{ \ - @try { \ - static NSArray *fields; \ - static NSUInteger count; \ - static dispatch_once_t onceToken; \ - dispatch_once(&onceToken, ^{ \ - fields = values; \ - count = [fields count]; \ - }); \ - type result; \ - if ([json isKindOfClass:[NSArray class]]) { \ - if ([json count] != count) { \ - 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] = [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] = [self CGFloat:json[fields[i]]]; \ - } \ - } else if (json && json != [NSNull null]) { \ - RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \ - } \ - return result; \ - } \ - @catch (__unused NSException *e) { \ - RCTLogError(@"JSON value '%@' cannot be converted to '%s'", json, #type); \ - type result; \ - return result; \ - } \ -} - /** * This macro is used for creating converter functions for typed arrays. */ diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 344fc3102..9aea12727 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -24,8 +24,8 @@ RCT_NUMBER_CONVERTER(uint64_t, unsignedLongLongValue); 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_CUSTOM_CONVERTER(NSArray *, NSArray, [NSArray arrayWithArray:json]) +RCT_CUSTOM_CONVERTER(NSDictionary *, NSDictionary, [NSDictionary dictionaryWithDictionary:json]) RCT_CONVERTER(NSString *, NSString, description) + (NSNumber *)NSNumber:(id)json @@ -33,7 +33,12 @@ RCT_CONVERTER(NSString *, NSString, description) if ([json isKindOfClass:[NSNumber class]]) { return json; } else if ([json isKindOfClass:[NSString class]]) { - NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; + static NSNumberFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [[NSNumberFormatter alloc] init]; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + }); NSNumber *number = [formatter numberFromString:json]; if (!number) { RCTLogError(@"JSON String '%@' could not be interpreted as a number", json); @@ -74,12 +79,35 @@ RCT_CONVERTER(NSString *, NSString, description) return [NSURLRequest requestWithURL:[self NSURL:json]]; } ++ (NSDate *)NSDate:(id)json +{ + if ([json isKindOfClass:[NSNumber class]]) { + return [NSDate dateWithTimeIntervalSince1970:[self NSTimeInterval:json]]; + } else if ([json isKindOfClass:[NSString class]]) { + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; + }); + NSDate *date = [formatter dateFromString:json]; + if (!date) { + RCTLogError(@"JSON String '%@' could not be interpreted as a date. Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json); + } + return date; + } else if (json && json != [NSNull null]) { + RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json class]); + } + return nil; +} + // JS Standard for time is milliseconds -RCT_CONVERTER_CUSTOM(NSDate *, NSDate, [NSDate dateWithTimeIntervalSince1970:[self double:json] / 1000.0]) -RCT_CONVERTER_CUSTOM(NSTimeInterval, NSTimeInterval, [self double:json] / 1000.0) +RCT_CUSTOM_CONVERTER(NSTimeInterval, NSTimeInterval, [self double:json] / 1000.0) // JS standard for time zones is minutes. -RCT_CONVERTER_CUSTOM(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[self double:json] * 60.0]) +RCT_CUSTOM_CONVERTER(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[self double:json] * 60.0]) RCT_ENUM_CONVERTER(NSTextAlignment, (@{ @"auto": @(NSTextAlignmentNatural), @@ -116,10 +144,33 @@ RCT_ENUM_CONVERTER(UIScrollViewKeyboardDismissMode, (@{ }), UIScrollViewKeyboardDismissModeNone, integerValue) RCT_ENUM_CONVERTER(UIKeyboardType, (@{ - @"numeric": @(UIKeyboardTypeDecimalPad), @"default": @(UIKeyboardTypeDefault), + @"ascii-capable": @(UIKeyboardTypeASCIICapable), + @"numbers-and-punctuation": @(UIKeyboardTypeNumbersAndPunctuation), + @"url": @(UIKeyboardTypeURL), + @"number-pad": @(UIKeyboardTypeNumberPad), + @"phone-pad": @(UIKeyboardTypePhonePad), + @"name-phone-pad": @(UIKeyboardTypeNamePhonePad), + @"email-address": @(UIKeyboardTypeEmailAddress), + @"decimal-pad": @(UIKeyboardTypeDecimalPad), + @"twitter": @(UIKeyboardTypeTwitter), + @"web-search": @(UIKeyboardTypeWebSearch), }), UIKeyboardTypeDefault, integerValue) +RCT_ENUM_CONVERTER(UIReturnKeyType, (@{ + @"default": @(UIReturnKeyDefault), + @"go": @(UIReturnKeyGo), + @"google": @(UIReturnKeyGoogle), + @"join": @(UIReturnKeyJoin), + @"next": @(UIReturnKeyNext), + @"route": @(UIReturnKeyRoute), + @"search": @(UIReturnKeySearch), + @"send": @(UIReturnKeySend), + @"yahoo": @(UIReturnKeyYahoo), + @"done": @(UIReturnKeyDone), + @"emergency-call": @(UIReturnKeyEmergencyCall), +}), UIReturnKeyDefault, integerValue) + RCT_ENUM_CONVERTER(UIViewContentMode, (@{ @"scale-to-fill": @(UIViewContentModeScaleToFill), @"scale-aspect-fit": @(UIViewContentModeScaleAspectFit), @@ -142,7 +193,58 @@ RCT_ENUM_CONVERTER(UIBarStyle, (@{ }), UIBarStyleDefault, integerValue) // 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]) +/** + * 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, _aliases) \ ++ (type)type:(id)json \ +{ \ + @try { \ + static NSArray *fields; \ + static NSUInteger count; \ + static dispatch_once_t onceToken; \ + dispatch_once(&onceToken, ^{ \ + fields = values; \ + count = [fields count]; \ + }); \ + type result; \ + if ([json isKindOfClass:[NSArray class]]) { \ + if ([json count] != count) { \ + 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] = [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] = [self CGFloat:json[fields[i]]]; \ + } \ + } else if (json && json != [NSNull null]) { \ + RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \ + } \ + return result; \ + } \ + @catch (__unused NSException *e) { \ + RCTLogError(@"JSON value '%@' cannot be converted to '%s'", json, #type); \ + type result; \ + return result; \ + } \ +} + +RCT_CUSTOM_CONVERTER(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"})) @@ -701,25 +803,32 @@ BOOL RCTSetProperty(id target, NSString *keyPath, SEL type, id json) return NO; } - // Get converted value - NSMethodSignature *signature = [RCTConvert methodSignatureForSelector:type]; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; - [invocation setArgument:&type atIndex:1]; - [invocation setArgument:&json atIndex:2]; - [invocation invokeWithTarget:[RCTConvert class]]; - NSUInteger length = [signature methodReturnLength]; - void *value = malloc(length); - [invocation getReturnValue:value]; + @try { + // Get converted value + NSMethodSignature *signature = [RCTConvert methodSignatureForSelector:type]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setArgument:&type atIndex:1]; + [invocation setArgument:&json atIndex:2]; + [invocation invokeWithTarget:[RCTConvert class]]; + NSUInteger length = [signature methodReturnLength]; + void *value = malloc(length); + [invocation getReturnValue:value]; - // Set converted value - signature = [target methodSignatureForSelector:setter]; - invocation = [NSInvocation invocationWithMethodSignature:signature]; - [invocation setArgument:&setter atIndex:1]; - [invocation setArgument:value atIndex:2]; - [invocation invokeWithTarget:target]; - free(value); + // Set converted value + signature = [target methodSignatureForSelector:setter]; + invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setArgument:&setter atIndex:1]; + [invocation setArgument:value atIndex:2]; + [invocation invokeWithTarget:target]; + free(value); - return YES; + return YES; + } + @catch (NSException *exception) { + RCTLogError(@"Exception thrown while attempting to set property '%@' of \ + '%@' with value '%@': %@", key, [target class], json, exception); + return NO; + } } BOOL RCTCopyProperty(id target, id source, NSString *keyPath) @@ -748,7 +857,7 @@ BOOL RCTCopyProperty(id target, id source, NSString *keyPath) return NO; } - // Get converted value + // Get value NSMethodSignature *signature = [source methodSignatureForSelector:getter]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setArgument:&getter atIndex:1]; @@ -757,7 +866,7 @@ BOOL RCTCopyProperty(id target, id source, NSString *keyPath) void *value = malloc(length); [invocation getReturnValue:value]; - // Set converted value + // Set value signature = [target methodSignatureForSelector:setter]; invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setArgument:&setter atIndex:1]; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index a0e7285c6..d460ea68d 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -80,10 +80,18 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio if ((self = [super init])) { _property = [RCTConvert NSString:config[@"property"]]; - // TODO: this should be provided in ms, not seconds - // (this will require changing all call sites to ms as well) - _duration = [RCTConvert NSTimeInterval:config[@"duration"]] * 1000.0 ?: duration; - _delay = [RCTConvert NSTimeInterval:config[@"delay"]] * 1000.0; + _duration = [RCTConvert NSTimeInterval:config[@"duration"]] ?: duration; + if (_duration > 0.0 && _duration < 0.01) { + RCTLogError(@"RCTLayoutAnimation expects timings to be in ms, not seconds."); + _duration = _duration * 1000.0; + } + + _delay = [RCTConvert NSTimeInterval:config[@"delay"]]; + if (_delay > 0.0 && _delay < 0.01) { + RCTLogError(@"RCTLayoutAnimation expects timings to be in ms, not seconds."); + _delay = _delay * 1000.0; + } + _animationType = [RCTConvert RCTAnimationType:config[@"type"]]; if (_animationType == RCTAnimationTypeSpring) { _springDamping = [RCTConvert CGFloat:config[@"springDamping"]]; @@ -142,9 +150,11 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio if ((self = [super init])) { - // TODO: this should be provided in ms, not seconds - // (this will require changing all call sites to ms as well) - NSTimeInterval duration = [RCTConvert NSTimeInterval:config[@"duration"]] * 1000.0; + NSTimeInterval duration = [RCTConvert NSTimeInterval:config[@"duration"]]; + if (duration > 0.0 && duration < 0.01) { + RCTLogError(@"RCTLayoutAnimation expects timings to be in ms, not seconds."); + duration = duration * 1000.0; + } _createAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"create"]]; _updateAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"update"]]; @@ -1291,6 +1301,32 @@ static void RCTMeasureLayout(RCTShadowView *view, @"always": @(UITextFieldViewModeAlways), }, }, + @"UIKeyboardType": @{ + @"default": @(UIKeyboardTypeDefault), + @"ascii-capable": @(UIKeyboardTypeASCIICapable), + @"numbers-and-punctuation": @(UIKeyboardTypeNumbersAndPunctuation), + @"url": @(UIKeyboardTypeURL), + @"number-pad": @(UIKeyboardTypeNumberPad), + @"phone-pad": @(UIKeyboardTypePhonePad), + @"name-phone-pad": @(UIKeyboardTypeNamePhonePad), + @"decimal-pad": @(UIKeyboardTypeDecimalPad), + @"email-address": @(UIKeyboardTypeEmailAddress), + @"twitter": @(UIKeyboardTypeTwitter), + @"web-search": @(UIKeyboardTypeWebSearch), + }, + @"UIReturnKeyType": @{ + @"default": @(UIReturnKeyDefault), + @"go": @(UIReturnKeyGo), + @"google": @(UIReturnKeyGoogle), + @"join": @(UIReturnKeyJoin), + @"next": @(UIReturnKeyNext), + @"route": @(UIReturnKeyRoute), + @"search": @(UIReturnKeySearch), + @"send": @(UIReturnKeySend), + @"yahoo": @(UIReturnKeyYahoo), + @"done": @(UIReturnKeyDone), + @"emergency-call": @(UIReturnKeyEmergencyCall), + }, @"UIView": @{ @"ContentMode": @{ @"ScaleToFill": @(UIViewContentModeScaleToFill), diff --git a/React/Views/RCTScrollView.h b/React/Views/RCTScrollView.h index db6aa7edd..f218ea6ea 100644 --- a/React/Views/RCTScrollView.h +++ b/React/Views/RCTScrollView.h @@ -43,7 +43,7 @@ @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; -@property (nonatomic, assign) NSUInteger throttleScrollCallbackMS; +@property (nonatomic, assign) NSTimeInterval scrollEventThrottle; @property (nonatomic, assign) BOOL centerContent; @property (nonatomic, copy) NSArray *stickyHeaderIndices; diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index e3e1c2f79..0376d2a9c 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -274,7 +274,7 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; _contentInset = UIEdgeInsetsZero; _contentSize = CGSizeZero; - _throttleScrollCallbackMS = 0; + _scrollEventThrottle = 0.0; _lastScrollDispatchTime = CACurrentMediaTime(); _cachedChildFrames = [[NSMutableArray alloc] init]; @@ -393,16 +393,15 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) [self updateClippedSubviews]; NSTimeInterval now = CACurrentMediaTime(); - NSTimeInterval throttleScrollCallbackSeconds = _throttleScrollCallbackMS / 1000.0; /** - * TODO: this logic looks wrong, and it may be because it is. Currently, if _throttleScrollCallbackMS + * TODO: this logic looks wrong, and it may be because it is. Currently, if _scrollEventThrottle * is set to zero (the default), the "didScroll" event is only sent once per scroll, instead of repeatedly * while scrolling as expected. However, if you "fix" that bug, ScrollView will generate repeated * warnings, and behave strangely (ListView works fine however), so don't fix it unless you fix that too! */ if (_allowNextScrollNoMatterWhat || - (_throttleScrollCallbackMS != 0 && throttleScrollCallbackSeconds < (now - _lastScrollDispatchTime))) { + (_scrollEventThrottle > 0 && _scrollEventThrottle < (now - _lastScrollDispatchTime))) { // Calculate changed frames NSMutableArray *updatedChildFrames = [[NSMutableArray alloc] init]; diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index 8001947d1..9c0c56f04 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -39,12 +39,14 @@ RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(scrollsToTop, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL) -RCT_EXPORT_VIEW_PROPERTY(stickyHeaderIndices, NSNumberArray); -RCT_EXPORT_VIEW_PROPERTY(throttleScrollCallbackMS, double); -RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat); -RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets); -RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets); -RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset, CGPoint); +RCT_EXPORT_VIEW_PROPERTY(stickyHeaderIndices, NSNumberArray) +RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval) +RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) +RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets) +RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset, CGPoint) + +RCT_DEPRECATED_VIEW_PROPERTY(throttleScrollCallbackMS, scrollEventThrottle) - (NSDictionary *)constantsToExport { diff --git a/React/Views/RCTTextField.h b/React/Views/RCTTextField.h index 99b5b7cba..47d76ad52 100644 --- a/React/Views/RCTTextField.h +++ b/React/Views/RCTTextField.h @@ -15,7 +15,7 @@ @property (nonatomic, assign) BOOL caretHidden; @property (nonatomic, assign) BOOL autoCorrect; -@property (nonatomic, assign) UIEdgeInsets paddingEdgeInsets; // TODO: contentInset +@property (nonatomic, assign) UIEdgeInsets contentInset; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/React/Views/RCTTextField.m b/React/Views/RCTTextField.m index 1ccf17d7f..0911e069f 100644 --- a/React/Views/RCTTextField.m +++ b/React/Views/RCTTextField.m @@ -31,7 +31,6 @@ [self addTarget:self action:@selector(_textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd]; [self addTarget:self action:@selector(_textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; _reactSubviews = [[NSMutableArray alloc] init]; - self.returnKeyType = UIReturnKeyDone; } return self; } @@ -71,7 +70,7 @@ - (CGRect)textRectForBounds:(CGRect)bounds { CGRect rect = [super textRectForBounds:bounds]; - return UIEdgeInsetsInsetRect(rect, _paddingEdgeInsets); + return UIEdgeInsetsInsetRect(rect, _contentInset); } - (CGRect)editingRectForBounds:(CGRect)bounds diff --git a/React/Views/RCTTextFieldManager.m b/React/Views/RCTTextFieldManager.m index 087dc5b26..041474643 100644 --- a/React/Views/RCTTextFieldManager.m +++ b/React/Views/RCTTextFieldManager.m @@ -29,6 +29,9 @@ RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString) RCT_EXPORT_VIEW_PROPERTY(text, NSString) RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode) RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType) +RCT_EXPORT_VIEW_PROPERTY(returnKeyType, UIReturnKeyType) +RCT_EXPORT_VIEW_PROPERTY(enablesReturnKeyAutomatically, BOOL) +RCT_EXPORT_VIEW_PROPERTY(secureTextEntry, BOOL) RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor) RCT_REMAP_VIEW_PROPERTY(autoCapitalize, autocapitalizationType, UITextAutocapitalizationType) RCT_CUSTOM_VIEW_PROPERTY(fontSize, CGFloat, RCTTextField) @@ -53,7 +56,7 @@ RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextField) NSNumber *reactTag = shadowView.reactTag; UIEdgeInsets padding = shadowView.paddingAsInsets; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - ((RCTTextField *)viewRegistry[reactTag]).paddingEdgeInsets = padding; + ((RCTTextField *)viewRegistry[reactTag]).contentInset = padding; }; } diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index 5c1c609d3..32babecc9 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -153,4 +153,19 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v #define RCT_IGNORE_SHADOW_PROPERTY(name) \ - (void)set_##name:(id)value forShadowView:(id)view withDefaultView:(id)defaultView {} +/** + * Used for when view property names change. Will log an error when used. + */ +#define RCT_DEPRECATED_VIEW_PROPERTY(oldName, newName) \ +- (void)set_##oldName:(id)json forView:(id)view withDefaultView:(id)defaultView { \ + RCTLogError(@"Property '%s' has been replaced by '%s'.", #oldName, #newName); \ + [self set_##newName:json forView:view withDefaultView:defaultView]; \ +} + +#define RCT_DEPRECATED_SHADOW_PROPERTY(oldName, newName) \ +- (void)set_##oldName:(id)json forShadowView:(id)view withDefaultView:(id)defaultView { \ + RCTLogError(@"Property '%s' has been replaced by '%s'.", #oldName, #newName); \ + [self set_##newName:json forView:view withDefaultView:defaultView]; \ +} + @end diff --git a/package.json b/package.json index ca60030f7..1db1dc05f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native", - "version": "0.3.1", + "version": "0.3.0", "description": "A framework for building native apps using React", "repository": { "type": "git", diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index 6f09c5e88..00c4a5c5c 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -713,6 +713,6 @@ function NotFoundError() { this.status = 404; } -NotFoundError.__proto__ = Error.prototype; +util.inherits(NotFoundError, Error); module.exports = DependecyGraph;