diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index fc31a90dc4..e484d0f659 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -29,8 +29,6 @@ var TimerMixin = require('react-timer-mixin'); var MovieCell = require('./MovieCell'); var MovieScreen = require('./MovieScreen'); -var fetch = require('fetch'); - /** * This is for demo purposes only, and rate limited. * In case you want to use the Rotten Tomatoes' API on a real app you should diff --git a/Examples/UIExplorer/ImageMocks.js b/Examples/UIExplorer/ImageMocks.js index b888acbf74..3f1883fa65 100644 --- a/Examples/UIExplorer/ImageMocks.js +++ b/Examples/UIExplorer/ImageMocks.js @@ -39,3 +39,8 @@ declare module 'image!uie_thumb_selected' { declare var uri: string; declare var isStatic: boolean; } + +declare module 'image!NavBarButtonPlus' { + declare var uri: string; + declare var isStatic: boolean; +} diff --git a/Examples/UIExplorer/LayoutEventsExample.js b/Examples/UIExplorer/LayoutEventsExample.js new file mode 100644 index 0000000000..6aec6257e7 --- /dev/null +++ b/Examples/UIExplorer/LayoutEventsExample.js @@ -0,0 +1,150 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Image, + LayoutAnimation, + StyleSheet, + Text, + View, +} = React; + +type LayoutEvent = { + nativeEvent: { + layout: { + x: number; + y: number; + width: number; + height: number; + }; + }; +}; + +var LayoutEventExample = React.createClass({ + getInitialState: function() { + return { + viewStyle: { + margin: 20, + }, + }; + }, + animateViewLayout: function() { + LayoutAnimation.configureNext( + LayoutAnimation.Presets.spring, + () => { + console.log('layout animation done.'); + this.addWrapText(); + }, + (error) => { throw new Error(JSON.stringify(error)); } + ); + this.setState({ + viewStyle: { + margin: this.state.viewStyle.margin > 20 ? 20 : 60, + } + }); + }, + addWrapText: function() { + this.setState( + {extraText: ' And a bunch more text to wrap around a few lines.'}, + this.changeContainer + ); + }, + changeContainer: function() { + this.setState({containerStyle: {width: 280}}); + }, + onViewLayout: function(e: LayoutEvent) { + console.log('received view layout event\n', e.nativeEvent); + this.setState({viewLayout: e.nativeEvent.layout}); + }, + onTextLayout: function(e: LayoutEvent) { + console.log('received text layout event\n', e.nativeEvent); + this.setState({textLayout: e.nativeEvent.layout}); + }, + onImageLayout: function(e: LayoutEvent) { + console.log('received image layout event\n', e.nativeEvent); + this.setState({imageLayout: e.nativeEvent.layout}); + }, + render: function() { + var viewStyle = [styles.view, this.state.viewStyle]; + var textLayout = this.state.textLayout || {width: '?', height: '?'}; + var imageLayout = this.state.imageLayout || {x: '?', y: '?'}; + return ( + + + onLayout events are called on mount and whenever layout is updated, + including after layout animations complete.{' '} + + Press here to change layout. + + + + + + ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'} + + + A simple piece of text.{this.state.extraText} + + + {'\n'} + Text w/h: {textLayout.width}/{textLayout.height + '\n'} + Image x/y: {imageLayout.x}/{imageLayout.y} + + + + ); + } +}); + +var styles = StyleSheet.create({ + view: { + padding: 12, + borderColor: 'black', + borderWidth: 0.5, + backgroundColor: 'transparent', + }, + text: { + alignSelf: 'flex-start', + borderColor: 'rgba(0, 0, 255, 0.2)', + borderWidth: 0.5, + }, + image: { + width: 50, + height: 50, + marginBottom: 10, + alignSelf: 'center', + }, + pressText: { + fontWeight: 'bold', + }, +}); + +exports.title = 'onLayout'; +exports.description = 'Layout events can be used to measure view size and position.'; +exports.examples = [ +{ + title: 'onLayout', + render: function(): ReactElement { + return ; + }, +}]; diff --git a/Examples/UIExplorer/NavigatorIOSExample.js b/Examples/UIExplorer/NavigatorIOSExample.js index eee731bd53..4a2011a654 100644 --- a/Examples/UIExplorer/NavigatorIOSExample.js +++ b/Examples/UIExplorer/NavigatorIOSExample.js @@ -19,6 +19,7 @@ var React = require('react-native'); var ViewExample = require('./ViewExample'); var createExamplePage = require('./createExamplePage'); var { + AlertIOS, PixelRatio, ScrollView, StyleSheet, @@ -92,6 +93,30 @@ var NavigatorIOSExample = React.createClass({ } }); })} + {this._renderRow('Custom Left & Right Icons', () => { + this.props.navigator.push({ + title: NavigatorIOSExample.title, + component: EmptyPage, + leftButtonTitle: 'Custom Left', + onLeftButtonPress: () => this.props.navigator.pop(), + rightButtonIcon: require('image!NavBarButtonPlus'), + onRightButtonPress: () => { + AlertIOS.alert( + 'Bar Button Action', + 'Recognized a tap on the bar button icon', + [ + { + text: 'OK', + onPress: () => console.log('Tapped OK'), + }, + ] + ); + }, + passProps: { + text: 'This page has an icon for the right button in the nav bar', + } + }); + })} {this._renderRow('Pop', () => { this.props.navigator.pop(); })} diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json new file mode 100644 index 0000000000..13726b4e94 --- /dev/null +++ b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "NavBarButtonPlus@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/NavBarButtonPlus@3x.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/NavBarButtonPlus@3x.png new file mode 100644 index 0000000000..706a9c9052 Binary files /dev/null and b/Examples/UIExplorer/UIExplorer/Images.xcassets/NavBarButtonPlus.imageset/NavBarButtonPlus@3x.png differ diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index a24ec1a54a..dd2336df59 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -64,6 +64,7 @@ var APIS = [ require('./BorderExample'), require('./CameraRollExample.ios'), require('./GeolocationExample'), + require('./LayoutEventsExample'), require('./LayoutExample'), require('./NetInfoExample'), require('./PanResponderExample'), diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js index dbb5dde835..1e61a0dbc1 100644 --- a/IntegrationTests/IntegrationTestsApp.js +++ b/IntegrationTests/IntegrationTestsApp.js @@ -25,6 +25,7 @@ var TESTS = [ require('./IntegrationTestHarnessTest'), require('./TimersTest'), require('./AsyncStorageTest'), + require('./LayoutEventsTest'), require('./SimpleSnapshotTest'), ]; diff --git a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m index e0a43e7931..9bf1a4fc14 100644 --- a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m +++ b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m @@ -71,6 +71,11 @@ [_runner runTest:_cmd module:@"AsyncStorageTest"]; } +- (void)testLayoutEvents +{ + [_runner runTest:_cmd module:@"LayoutEventsTest"]; +} + #pragma mark Snapshot Tests - (void)testSimpleSnapshot diff --git a/IntegrationTests/LayoutEventsTest.js b/IntegrationTests/LayoutEventsTest.js new file mode 100644 index 0000000000..7e8cd3a0dc --- /dev/null +++ b/IntegrationTests/LayoutEventsTest.js @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule LayoutEventsTest + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Image, + LayoutAnimation, + NativeModules, + StyleSheet, + Text, + View, +} = React; +var TestModule = NativeModules.TestModule || NativeModules.SnapshotTestManager; + +var deepDiffer = require('deepDiffer'); + +function debug() { + //console.log.apply(null, arguments); +} + +type LayoutEvent = { + nativeEvent: { + layout: { + x: number; + y: number; + width: number; + height: number; + }; + }; +}; + +var LayoutEventsTest = React.createClass({ + getInitialState: function() { + return { + didAnimation: false, + }; + }, + animateViewLayout: function() { + LayoutAnimation.configureNext( + LayoutAnimation.Presets.spring, + () => { + debug('layout animation done.'); + this.checkLayout(this.addWrapText); + }, + (error) => { throw new Error(JSON.stringify(error)); } + ); + this.setState({viewStyle: {margin: 60}}); + }, + addWrapText: function() { + this.setState( + {extraText: ' And a bunch more text to wrap around a few lines.'}, + () => this.checkLayout(this.changeContainer) + ); + }, + changeContainer: function() { + this.setState( + {containerStyle: {width: 280}}, + () => this.checkLayout(TestModule.markTestCompleted) + ); + }, + checkLayout: function(next?: ?Function) { + if (!this.isMounted()) { + return; + } + this.refs.view.measure((x, y, width, height) => { + this.compare('view', {x, y, width, height}, this.state.viewLayout); + if (typeof next === 'function') { + next(); + } else if (!this.state.didAnimation) { + // Trigger first state change after onLayout fires + this.animateViewLayout(); + this.state.didAnimation = true; + } + }); + this.refs.txt.measure((x, y, width, height) => { + this.compare('txt', {x, y, width, height}, this.state.textLayout); + }); + this.refs.img.measure((x, y, width, height) => { + this.compare('img', {x, y, width, height}, this.state.imageLayout); + }); + }, + compare: function(node: string, measured: any, onLayout: any): void { + if (deepDiffer(measured, onLayout)) { + var data = {measured, onLayout}; + throw new Error( + node + ' onLayout mismatch with measure ' + + JSON.stringify(data, null, ' ') + ); + } + }, + onViewLayout: function(e: LayoutEvent) { + debug('received view layout event\n', e.nativeEvent); + this.setState({viewLayout: e.nativeEvent.layout}, this.checkLayout); + }, + onTextLayout: function(e: LayoutEvent) { + debug('received text layout event\n', e.nativeEvent); + this.setState({textLayout: e.nativeEvent.layout}, this.checkLayout); + }, + onImageLayout: function(e: LayoutEvent) { + debug('received image layout event\n', e.nativeEvent); + this.setState({imageLayout: e.nativeEvent.layout}, this.checkLayout); + }, + render: function() { + var viewStyle = [styles.view, this.state.viewStyle]; + var textLayout = this.state.textLayout || {width: '?', height: '?'}; + var imageLayout = this.state.imageLayout || {x: '?', y: '?'}; + return ( + + + + + ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'} + + + A simple piece of text.{this.state.extraText} + + + {'\n'} + Text w/h: {textLayout.width}/{textLayout.height + '\n'} + Image x/y: {imageLayout.x}/{imageLayout.y} + + + + ); + } +}); + +var styles = StyleSheet.create({ + container: { + margin: 40, + }, + view: { + margin: 20, + padding: 12, + borderColor: 'black', + borderWidth: 0.5, + backgroundColor: 'transparent', + }, + text: { + alignSelf: 'flex-start', + borderColor: 'rgba(0, 0, 255, 0.2)', + borderWidth: 0.5, + }, + image: { + width: 50, + height: 50, + marginBottom: 10, + alignSelf: 'center', + }, +}); + +module.exports = LayoutEventsTest; diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index 3babd14095..103e749f78 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -12,6 +12,7 @@ 'use strict'; var EventEmitter = require('EventEmitter'); +var Image = require('Image'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var RCTNavigatorManager = require('NativeModules').NavigatorManager; @@ -47,11 +48,16 @@ var RCTNavigatorItem = createReactIOSNativeComponentClass({ // NavigatorIOS does not use them all, because some are problematic title: true, barTintColor: true, + leftButtonIcon: true, + leftButtonTitle: true, + onNavLeftButtonTap: true, + rightButtonIcon: true, rightButtonTitle: true, onNavRightButtonTap: true, + backButtonIcon: true, + backButtonTitle: true, tintColor: true, navigationBarHidden: true, - backButtonTitle: true, titleTextColor: true, style: true, }, @@ -79,7 +85,12 @@ type Route = { title: string; passProps: Object; backButtonTitle: string; + backButtonIcon: Object; + leftButtonTitle: string; + leftButtonIcon: Object; + onLeftButtonPress: Function; rightButtonTitle: string; + rightButtonIcon: Object; onRightButtonPress: Function; wrapperStyle: any; }; @@ -212,6 +223,13 @@ var NavigatorIOS = React.createClass({ */ passProps: PropTypes.object, + /** + * If set, the left header button image will appear with this source. Note + * that this doesn't apply for the header of the current view, but the + * ones of the views that are pushed afterward. + */ + backButtonIcon: Image.propTypes.source, + /** * If set, the left header button will appear with this name. Note that * this doesn't apply for the header of the current view, but the ones @@ -219,6 +237,26 @@ var NavigatorIOS = React.createClass({ */ backButtonTitle: PropTypes.string, + /** + * If set, the left header button image will appear with this source + */ + leftButtonIcon: Image.propTypes.source, + + /** + * If set, the left header button will appear with this name + */ + leftButtonTitle: PropTypes.string, + + /** + * Called when the left header button is pressed + */ + onLeftButtonPress: PropTypes.func, + + /** + * If set, the right header button image will appear with this source + */ + rightButtonIcon: Image.propTypes.source, + /** * If set, the right header button will appear with this name */ @@ -560,7 +598,12 @@ var NavigatorIOS = React.createClass({ this.props.itemWrapperStyle, route.wrapperStyle ]} + backButtonIcon={this._imageNameFromSource(route.backButtonIcon)} backButtonTitle={route.backButtonTitle} + leftButtonIcon={this._imageNameFromSource(route.leftButtonIcon)} + leftButtonTitle={route.leftButtonTitle} + onNavLeftButtonTap={route.onLeftButtonPress} + rightButtonIcon={this._imageNameFromSource(route.rightButtonIcon)} rightButtonTitle={route.rightButtonTitle} onNavRightButtonTap={route.onRightButtonPress} navigationBarHidden={this.props.navigationBarHidden} @@ -577,6 +620,10 @@ var NavigatorIOS = React.createClass({ ); }, + _imageNameFromSource: function(source: ?Object) { + return source ? source.uri : undefined; + }, + renderNavigationStackItems: function() { var shouldRecurseToNavigator = this.state.makingNavigatorRequest || diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index e590999deb..8d93cd8416 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -20,6 +20,7 @@ var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); +var flattenStyle = require('flattenStyle'); var keyOf = require('keyOf'); var onlyChild = require('onlyChild'); @@ -105,12 +106,13 @@ var TouchableOpacity = React.createClass({ }, touchableHandleActivePressOut: function() { - this.setOpacityTo(1.0); + var child = onlyChild(this.props.children); + var childStyle = flattenStyle(child.props.style) || {}; + this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity); this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { - this.setOpacityTo(1.0); this.props.onPress && this.props.onPress(); }, diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index c7ca2ee261..aa69ab0eb7 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -90,6 +90,11 @@ var View = React.createClass({ onStartShouldSetResponder: PropTypes.func, onStartShouldSetResponderCapture: PropTypes.func, + /** + * Invoked on mount and layout changes with {x, y, width, height}. + */ + onLayout: PropTypes.func, + /** * In the absence of `auto` property, `none` is much like `CSS`'s `none` * value. `box-none` is as if you had applied the `CSS` class: diff --git a/Libraries/Fetch/fetch.js b/Libraries/Fetch/fetch.js index 241172669f..829f7c4256 100644 --- a/Libraries/Fetch/fetch.js +++ b/Libraries/Fetch/fetch.js @@ -47,6 +47,23 @@ var self = {}; return } + function normalizeName(name) { + if (typeof name !== 'string') { + name = name.toString(); + } + if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { + throw new TypeError('Invalid character in header field name') + } + return name.toLowerCase() + } + + function normalizeValue(value) { + if (typeof value !== 'string') { + value = value.toString(); + } + return value + } + function Headers(headers) { this.map = {} @@ -66,7 +83,8 @@ var self = {}; } Headers.prototype.append = function(name, value) { - name = name.toLowerCase() + name = normalizeName(name) + value = normalizeValue(value) var list = this.map[name] if (!list) { list = [] @@ -76,24 +94,24 @@ var self = {}; } Headers.prototype['delete'] = function(name) { - delete this.map[name.toLowerCase()] + delete this.map[normalizeName(name)] } Headers.prototype.get = function(name) { - var values = this.map[name.toLowerCase()] + var values = this.map[normalizeName(name)] return values ? values[0] : null } Headers.prototype.getAll = function(name) { - return this.map[name.toLowerCase()] || [] + return this.map[normalizeName(name)] || [] } Headers.prototype.has = function(name) { - return this.map.hasOwnProperty(name.toLowerCase()) + return this.map.hasOwnProperty(normalizeName(name)) } Headers.prototype.set = function(name, value) { - this.map[name.toLowerCase()] = [value] + this.map[normalizeName(name)] = [normalizeValue(value)] } // Instead of iterable for now. @@ -134,22 +152,51 @@ var self = {}; return fileReaderReady(reader) } - var blobSupport = 'FileReader' in self && 'Blob' in self && (function() { - try { - new Blob(); - return true - } catch(e) { - return false - } - })(); + var support = { + blob: 'FileReader' in self && 'Blob' in self && (function() { + try { + new Blob(); + return true + } catch(e) { + return false + } + })(), + formData: 'FormData' in self + } function Body() { this.bodyUsed = false - if (blobSupport) { + + this._initBody = function(body) { + this._bodyInit = body + if (typeof body === 'string') { + this._bodyText = body + } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body + } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body + } else if (!body) { + this._bodyText = '' + } else { + throw new Error('unsupported BodyInit type') + } + } + + if (support.blob) { this.blob = function() { var rejected = consumed(this) - return rejected ? rejected : Promise.resolve(this._bodyBlob) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob') + } else { + return Promise.resolve(new Blob([this._bodyText])) + } } this.arrayBuffer = function() { @@ -157,7 +204,18 @@ var self = {}; } this.text = function() { - return this.blob().then(readBlobAsText) + var rejected = consumed(this) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return readBlobAsText(this._bodyBlob) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as text') + } else { + return Promise.resolve(this._bodyText) + } } } else { this.text = function() { @@ -166,7 +224,7 @@ var self = {}; } } - if ('FormData' in self) { + if (support.formData) { this.formData = function() { return this.text().then(decode) } @@ -190,12 +248,17 @@ var self = {}; function Request(url, options) { options = options || {} this.url = url - this._body = options.body + this.credentials = options.credentials || 'omit' this.headers = new Headers(options.headers) this.method = normalizeMethod(options.method || 'GET') this.mode = options.mode || null this.referrer = null + + if ((this.method === 'GET' || this.method === 'HEAD') && options.body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(options.body) } function decode(body) { @@ -223,11 +286,43 @@ var self = {}; return head } - Request.prototype.fetch = function() { - var self = this + Body.call(Request.prototype) + + function Response(bodyInit, options) { + if (!options) { + options = {} + } + + this._initBody(bodyInit) + this.type = 'default' + this.url = null + this.status = options.status + this.ok = this.status >= 200 && this.status < 300 + this.statusText = options.statusText + this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers) + this.url = options.url || '' + } + + Body.call(Response.prototype) + + self.Headers = Headers; + self.Request = Request; + self.Response = Response; + + self.fetch = function(input, init) { + // TODO: Request constructor should accept input, init + var request + if (Request.prototype.isPrototypeOf(input) && !init) { + request = input + } else { + request = new Request(input, init) + } return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest() + if (request.credentials === 'cors') { + xhr.withCredentials = true; + } function responseURL() { if ('responseURL' in xhr) { @@ -262,57 +357,24 @@ var self = {}; reject(new TypeError('Network request failed')) } - xhr.open(self.method, self.url) - if ('responseType' in xhr && blobSupport) { + xhr.open(request.method, request.url, true) + + if ('responseType' in xhr && support.blob) { xhr.responseType = 'blob' } - self.headers.forEach(function(name, values) { + request.headers.forEach(function(name, values) { values.forEach(function(value) { xhr.setRequestHeader(name, value) }) }) - xhr.send((self._body === undefined) ? null : self._body) + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) }) } - - Body.call(Request.prototype) - - function Response(bodyInit, options) { - if (!options) { - options = {} - } - - if (blobSupport) { - if (typeof bodyInit === 'string') { - this._bodyBlob = new Blob([bodyInit]) - } else { - this._bodyBlob = bodyInit - } - } else { - this._bodyText = bodyInit - } - this.type = 'default' - this.url = null - this.status = options.status - this.statusText = options.statusText - this.headers = options.headers - this.url = options.url || '' - } - - Body.call(Response.prototype) - - self.Headers = Headers; - self.Request = Request; - self.Response = Response; - - self.fetch = function (url, options) { - return new Request(url, options).fetch() - } self.fetch.polyfill = true })(); /** End of the third-party code */ -module.exports = self.fetch; +module.exports = self; diff --git a/Libraries/Image/AssetRegistry.js b/Libraries/Image/AssetRegistry.js new file mode 100644 index 0000000000..df4173e78f --- /dev/null +++ b/Libraries/Image/AssetRegistry.js @@ -0,0 +1,20 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AssetRegistry + */ +'use strict'; + +var assets = []; + +function registerAsset(asset) { + // `push` returns new array length, so the first asset will + // get id 1 (not 0) to make the value truthy + return assets.push(asset); +} + +function getAssetByID(assetId) { + return assets[assetId - 1]; +} + +module.exports = { registerAsset, getAssetByID }; diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index a41352f44c..32965c213a 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -123,8 +123,7 @@ var Image = React.createClass({ 'not be set directly on Image.'); } } - var source = resolveAssetSource(this.props.source); - invariant(source, 'source must be initialized'); + var source = resolveAssetSource(this.props.source) || {}; var {width, height} = source; var style = flattenStyle([{width, height}, styles.base, this.props.style]); diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js index 632ff96567..c5fc3bbe1e 100644 --- a/Libraries/Image/__tests__/resolveAssetSource-test.js +++ b/Libraries/Image/__tests__/resolveAssetSource-test.js @@ -8,19 +8,24 @@ */ 'use strict'; -jest.dontMock('../resolveAssetSource'); +jest + .dontMock('AssetRegistry') + .dontMock('../resolveAssetSource'); var resolveAssetSource; var SourceCode; +var AssetRegistry; function expectResolvesAsset(input, expectedSource) { - expect(resolveAssetSource(input)).toEqual(expectedSource); + var assetId = AssetRegistry.registerAsset(input); + expect(resolveAssetSource(assetId)).toEqual(expectedSource); } describe('resolveAssetSource', () => { beforeEach(() => { jest.resetModuleRegistry(); SourceCode = require('NativeModules').SourceCode; + AssetRegistry = require('AssetRegistry'); resolveAssetSource = require('../resolveAssetSource'); }); @@ -32,6 +37,22 @@ describe('resolveAssetSource', () => { expect(resolveAssetSource(source2)).toBe(source2); }); + it('does not change deprecated assets', () => { + expect(resolveAssetSource({ + isStatic: true, + deprecated: true, + width: 100, + height: 200, + uri: 'logo', + })).toEqual({ + isStatic: true, + deprecated: true, + width: 100, + height: 200, + uri: 'logo', + }); + }); + it('ignores any weird data', () => { expect(resolveAssetSource(null)).toBe(null); expect(resolveAssetSource(42)).toBe(null); @@ -81,25 +102,6 @@ describe('resolveAssetSource', () => { }); }); - it('does not change deprecated assets', () => { - expectResolvesAsset({ - __packager_asset: true, - deprecated: true, - fileSystemLocation: '/root/app/module/a', - httpServerLocation: '/assets/module/a', - width: 100, - height: 200, - scales: [1], - hash: '5b6f00f', - name: 'logo', - type: 'png', - }, { - isStatic: true, - width: 100, - height: 200, - uri: 'logo', - }); - }); }); describe('bundle was loaded from file', () => { diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js index 0218915587..29d59a9a5d 100644 --- a/Libraries/Image/resolveAssetSource.js +++ b/Libraries/Image/resolveAssetSource.js @@ -10,6 +10,7 @@ */ 'use strict'; +var AssetRegistry = require('AssetRegistry'); var PixelRatio = require('PixelRatio'); var SourceCode = require('NativeModules').SourceCode; @@ -44,58 +45,47 @@ function pickScale(scales, deviceScale) { } function resolveAssetSource(source) { - if (!source || typeof source !== 'object') { - return null; - } - - if (!source.__packager_asset) { + if (typeof source === 'object') { return source; } - // Deprecated assets are managed by Xcode for now, - // just returning image name as `uri` - // Examples: - // require('image!deprecatd_logo_example') - // require('./new-hotness-logo-example.png') - if (source.deprecated) { - return { - width: source.width, - height: source.height, - isStatic: true, - uri: source.name || source.uri, // TODO(frantic): remove uri - }; + var asset = AssetRegistry.getAssetByID(source); + if (asset) { + return assetToImageSource(asset); } + return null; +} + +function assetToImageSource(asset) { // TODO(frantic): currently httpServerLocation is used both as // path in http URL and path within IPA. Should we have zipArchiveLocation? - var path = source.httpServerLocation; + var path = asset.httpServerLocation; if (path[0] === '/') { path = path.substr(1); } - var scale = pickScale(source.scales, PixelRatio.get()); + var scale = pickScale(asset.scales, PixelRatio.get()); var scaleSuffix = scale === 1 ? '' : '@' + scale + 'x'; - var fileName = source.name + scaleSuffix + '.' + source.type; + var fileName = asset.name + scaleSuffix + '.' + asset.type; var serverURL = getServerURL(); if (serverURL) { return { - width: source.width, - height: source.height, + width: asset.width, + height: asset.height, uri: serverURL + path + '/' + fileName + - '?hash=' + source.hash, + '?hash=' + asset.hash, isStatic: false, }; } else { return { - width: source.width, - height: source.height, + width: asset.width, + height: asset.height, uri: path + '/' + fileName, isStatic: true, }; } - - return source; } module.exports = resolveAssetSource; diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index bb0aa263c9..7bddf87b69 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -135,7 +135,12 @@ function setupXHR() { // The native XMLHttpRequest in Chrome dev tools is CORS aware and won't // let you fetch anything from the internet GLOBAL.XMLHttpRequest = require('XMLHttpRequest'); - GLOBAL.fetch = require('fetch'); + + var fetchPolyfill = require('fetch'); + GLOBAL.fetch = fetchPolyfill.fetch; + GLOBAL.Headers = fetchPolyfill.Headers; + GLOBAL.Request = fetchPolyfill.Request; + GLOBAL.Response = fetchPolyfill.Response; } function setupGeolocation() { diff --git a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js index 4d4d21e25d..a826db7bf5 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js +++ b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js @@ -17,8 +17,6 @@ var RCTSourceCode = require('NativeModules').SourceCode; var SourceMapConsumer = require('SourceMap').SourceMapConsumer; var SourceMapURL = require('./source-map-url'); -var fetch = require('fetch'); - function loadSourceMap(): Promise { return fetchSourceMap() .then(map => new SourceMapConsumer(map)); diff --git a/Libraries/ReactIOS/ReactIOSViewAttributes.js b/Libraries/ReactIOS/ReactIOSViewAttributes.js index 069f00b249..489741b053 100644 --- a/Libraries/ReactIOS/ReactIOSViewAttributes.js +++ b/Libraries/ReactIOS/ReactIOSViewAttributes.js @@ -9,8 +9,7 @@ * @providesModule ReactIOSViewAttributes * @flow */ - -"use strict"; +'use strict'; var merge = require('merge'); @@ -21,6 +20,7 @@ ReactIOSViewAttributes.UIView = { accessible: true, accessibilityLabel: true, testID: true, + onLayout: true, }; ReactIOSViewAttributes.RCTView = merge( @@ -31,7 +31,7 @@ ReactIOSViewAttributes.RCTView = merge( // For this property to be effective, it must be applied to a view that contains // many subviews that extend outside its bound. The subviews must also have // overflow: hidden, as should the containing view (or one of its superviews). - removeClippedSubviews: true + removeClippedSubviews: true, }); module.exports = ReactIOSViewAttributes; diff --git a/Libraries/ReactIOS/diffRawProperties.js b/Libraries/ReactIOS/diffRawProperties.js index 3a5de284f4..ddd6edbea0 100644 --- a/Libraries/ReactIOS/diffRawProperties.js +++ b/Libraries/ReactIOS/diffRawProperties.js @@ -42,6 +42,16 @@ function diffRawProperties( } prevProp = prevProps && prevProps[propKey]; nextProp = nextProps[propKey]; + + // functions are converted to booleans as markers that the associated + // events should be sent from native. + if (typeof prevProp === 'function') { + prevProp = true; + } + if (typeof nextProp === 'function') { + nextProp = true; + } + if (prevProp !== nextProp) { // If you want a property's diff to be detected, you must configure it // to be so - *or* it must be a scalar property. For now, we'll allow @@ -75,6 +85,16 @@ function diffRawProperties( } prevProp = prevProps[propKey]; nextProp = nextProps && nextProps[propKey]; + + // functions are converted to booleans as markers that the associated + // events should be sent from native. + if (typeof prevProp === 'function') { + prevProp = true; + } + if (typeof nextProp === 'function') { + nextProp = true; + } + if (prevProp !== nextProp) { if (nextProp === undefined) { nextProp = null; // null is a sentinel we explicitly send to native diff --git a/Libraries/react-native/react-native-interface.js b/Libraries/react-native/react-native-interface.js index 6408526b81..b765183878 100644 --- a/Libraries/react-native/react-native-interface.js +++ b/Libraries/react-native/react-native-interface.js @@ -16,3 +16,8 @@ declare var __DEV__: boolean; declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: any; /*?{ inject: ?((stuff: Object) => void) };*/ + +declare var fetch: any; +declare var Headers: any; +declare var Request: any; +declare var Response: any; diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 77cbb215ec..7c8af00cce 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -28,6 +28,11 @@ extern NSString *const RCTReloadNotification; */ extern NSString *const RCTJavaScriptDidLoadNotification; +/** + * This notification fires when the bridge failed to load. + */ +extern NSString *const RCTJavaScriptDidFailToLoadNotification; + /** * This block can be used to instantiate modules that require additional * init parameters, or additional configuration prior to being used. diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index a7c6a30a69..c98e3648b5 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -31,6 +31,7 @@ NSString *const RCTReloadNotification = @"RCTReloadNotification"; NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; +NSString *const RCTJavaScriptDidFailToLoadNotification = @"RCTJavaScriptDidFailToLoadNotification"; dispatch_queue_t const RCTJSThread = nil; @@ -867,6 +868,11 @@ static id _latestJSExecutor; return _batchedBridge.modules; } +- (RCTEventDispatcher *)eventDispatcher +{ + return _eventDispatcher ?: _batchedBridge.eventDispatcher; +} + #define RCT_BRIDGE_WARN(...) \ - (void)__VA_ARGS__ \ { \ @@ -1082,16 +1088,18 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; [loader loadBundleAtURL:bundleURL onComplete:^(NSError *error, NSString *script) { + _loading = NO; if (!self.isValid) { return; } + RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; sourceCodeModule.scriptURL = bundleURL; sourceCodeModule.scriptText = script; - if (error != nil) { + if (error) { - NSArray *stack = [[error userInfo] objectForKey:@"stack"]; + NSArray *stack = [error userInfo][@"stack"]; if (stack) { [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] withStack:stack]; @@ -1100,10 +1108,17 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me withDetails:[error localizedFailureReason]]; } + NSDictionary *userInfo = @{@"error": error}; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification + object:self + userInfo:userInfo]; + } else { + [self enqueueApplicationScript:script url:bundleURL onComplete:^(NSError *loadError) { if (!loadError) { + /** * Register the display link to start sending js calls after everything * is setup @@ -1144,18 +1159,11 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me _latestJSExecutor = nil; } - /** - * Main Thread deallocations - */ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [_mainDisplayLink invalidate]; + void (^mainThreadInvalidate)(void) = ^{ - [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ - /** - * JS Thread deallocations - */ - [_javaScriptExecutor invalidate]; - [_jsDisplayLink invalidate]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_mainDisplayLink invalidate]; + _mainDisplayLink = nil; // Invalidate modules for (id target in _modulesByID.allObjects) { @@ -1165,11 +1173,35 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me } // Release modules (breaks retain cycle if module has strong bridge reference) - _javaScriptExecutor = nil; _frameUpdateObservers = nil; _modulesByID = nil; _queuesByID = nil; _modulesByName = nil; + }; + + if (!_javaScriptExecutor) { + + // No JS thread running + mainThreadInvalidate(); + return; + } + + [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ + + /** + * JS Thread deallocations + */ + [_javaScriptExecutor invalidate]; + _javaScriptExecutor = nil; + + [_jsDisplayLink invalidate]; + _jsDisplayLink = nil; + + /** + * Main Thread deallocations + */ + mainThreadInvalidate(); + }]; } @@ -1300,48 +1332,6 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me return; } - /** - * Event deduping - * - * Right now we make a lot of assumptions about the arguments structure - * so just iterate if it's a `callFunctionReturnFlushedQueue()` - */ - if ([method isEqualToString:@"callFunctionReturnFlushedQueue"]) { - NSString *moduleName = RCTLocalModuleNames[[args[0] integerValue]]; - /** - * Keep going if it any event emmiter, e.g. RCT(Device|NativeApp)?EventEmitter - */ - if ([moduleName hasSuffix:@"EventEmitter"]) { - for (NSDictionary *call in [strongSelf->_scheduledCalls copy]) { - NSArray *callArgs = call[@"args"]; - /** - * If it's the same module && method call on the bridge && - * the same EventEmitter module && method - */ - if ( - [call[@"module"] isEqualToString:module] && - [call[@"method"] isEqualToString:method] && - [callArgs[0] isEqual:args[0]] && - [callArgs[1] isEqual:args[1]] - ) { - /** - * args[2] contains the actual arguments for the event call, where - * args[2][0] is the target for RCTEventEmitter or the eventName - * for the other EventEmitters - * if RCTEventEmitter we need to compare args[2][1] that will be - * the eventName - */ - if ( - [args[2][0] isEqual:callArgs[2][0]] && - (![moduleName isEqualToString:@"RCTEventEmitter"] || [args[2][1] isEqual:callArgs[2][1]]) - ) { - [strongSelf->_scheduledCalls removeObject:call]; - } - } - } - } - } - id call = @{ @"module": module, @"method": method, @@ -1495,17 +1485,13 @@ RCT_BRIDGE_WARN(_invokeAndProcessModule:(NSString *)module method:(NSString *)me return; } - if (!RCT_DEBUG) { + @try { [method invokeWithBridge:strongSelf module:module arguments:params context:context]; - } else { - @try { - [method invokeWithBridge:strongSelf module:module arguments:params context:context]; - } - @catch (NSException *exception) { - RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); - if ([exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { - @throw; - } + } + @catch (NSException *exception) { + RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); + if (!RCT_DEBUG && [exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { + @throw exception; } } diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 3d88caf55b..f7e688df56 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -73,9 +73,6 @@ RCT_EXPORT_MODULE() { if ((self = [super init])) { - _defaults = [NSUserDefaults standardUserDefaults]; - [self updateSettings]; - NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self @@ -93,19 +90,27 @@ RCT_EXPORT_MODULE() name:RCTJavaScriptDidLoadNotification object:nil]; + _defaults = [NSUserDefaults standardUserDefaults]; + _settings = [[NSMutableDictionary alloc] init]; + + // Delay setup until after Bridge init + __weak RCTDevMenu *weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateSettings]; + }); + #if TARGET_IPHONE_SIMULATOR - __weak RCTDevMenu *weakSelf = self; RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; - // toggle debug menu + // Toggle debug menu [commands registerKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand action:^(UIKeyCommand *command) { [weakSelf toggle]; }]; - // reload in normal mode + // Reload in normal mode [commands registerKeyCommandWithInput:@"n" modifierFlags:UIKeyModifierCommand action:^(UIKeyCommand *command) { @@ -117,22 +122,23 @@ RCT_EXPORT_MODULE() return self; } +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + - (void)updateSettings { - __weak RCTDevMenu *weakSelf = self; - dispatch_async(dispatch_get_main_queue(), ^{ - RCTDevMenu *strongSelf = weakSelf; - if (!strongSelf) { - return; - } + NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey]; + if ([settings isEqualToDictionary:_settings]) { + return; + } - strongSelf->_settings = [NSMutableDictionary dictionaryWithDictionary:[strongSelf->_defaults objectForKey:RCTDevMenuSettingsKey]]; - - strongSelf.shakeToShow = [strongSelf->_settings[@"shakeToShow"] ?: @YES boolValue]; - strongSelf.profilingEnabled = [strongSelf->_settings[@"profilingEnabled"] ?: @NO boolValue]; - strongSelf.liveReloadEnabled = [strongSelf->_settings[@"liveReloadEnabled"] ?: @NO boolValue]; - strongSelf.executorClass = NSClassFromString(strongSelf->_settings[@"executorClass"]); - }); + [_settings setDictionary:settings]; + self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue]; + self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; + self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; + self.executorClass = NSClassFromString(_settings[@"executorClass"]); } - (void)jsLoaded @@ -161,6 +167,11 @@ RCT_EXPORT_MODULE() }); } +- (BOOL)isValid +{ + return NO; +} + - (void)dealloc { [_updateTask cancel]; @@ -170,6 +181,10 @@ RCT_EXPORT_MODULE() - (void)updateSetting:(NSString *)name value:(id)value { + id currentValue = _settings[name]; + if (currentValue == value || [currentValue isEqual:value]) { + return; + } if (value) { _settings[name] = value; } else { @@ -239,6 +254,9 @@ RCT_EXPORT_METHOD(reload) - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { _actionSheet = nil; + if (buttonIndex == actionSheet.cancelButtonIndex) { + return; + } switch (buttonIndex) { case 0: { diff --git a/React/Base/RCTRedBox.m b/React/Base/RCTRedBox.m index 0de61d1721..9c5b6d3dd3 100644 --- a/React/Base/RCTRedBox.m +++ b/React/Base/RCTRedBox.m @@ -76,10 +76,27 @@ reloadButton.frame = CGRectMake(buttonWidth, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); [_rootView addSubview:dismissButton]; [_rootView addSubview:reloadButton]; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter addObserver:self + selector:@selector(dismiss) + name:RCTReloadNotification + object:nil]; + + [notificationCenter addObserver:self + selector:@selector(dismiss) + name:RCTJavaScriptDidLoadNotification + object:nil]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (void)openStackFrameInEditor:(NSDictionary *)stackFrame { NSData *stackFrameJSON = [RCTJSONStringify(stackFrame, nil) dataUsingEncoding:NSUTF8StringEncoding]; @@ -125,7 +142,6 @@ - (void)reload { [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil userInfo:nil]; - [self dismiss]; } #pragma mark - TableView diff --git a/React/Executors/RCTWebViewExecutor.m b/React/Executors/RCTWebViewExecutor.m index a180bae2cf..56323fb99b 100644 --- a/React/Executors/RCTWebViewExecutor.m +++ b/React/Executors/RCTWebViewExecutor.m @@ -43,6 +43,7 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) UIWebView *_webView; NSMutableDictionary *_objectsToInject; NSRegularExpression *_commentsRegex; + NSRegularExpression *_scriptTagsRegex; } @synthesize valid = _valid; @@ -52,7 +53,8 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) if ((self = [super init])) { _objectsToInject = [[NSMutableDictionary alloc] init]; _webView = webView ?: [[UIWebView alloc] init]; - _commentsRegex = [NSRegularExpression regularExpressionWithPattern:@"(^ *?\\/\\/.*?$|\\/\\*\\*[\\s\\S]+?\\*\\/)" options:NSRegularExpressionAnchorsMatchLines error:NULL]; + _commentsRegex = [NSRegularExpression regularExpressionWithPattern:@"(^ *?\\/\\/.*?$|\\/\\*\\*[\\s\\S]*?\\*\\/)" options:NSRegularExpressionAnchorsMatchLines error:NULL], + _scriptTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\/?script[^>]*?)>" options:0 error:NULL], _webView.delegate = self; } return self; @@ -139,11 +141,6 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) onComplete(error); }; - script = [_commentsRegex stringByReplacingMatchesInString:script - options:0 - range:NSMakeRange(0, script.length) - withTemplate:@""]; - if (_objectsToInject.count > 0) { NSMutableString *scriptWithInjections = [[NSMutableString alloc] initWithString:@"/* BEGIN NATIVELY INJECTED OBJECTS */\n"]; [_objectsToInject enumerateKeysAndObjectsUsingBlock:^(NSString *objectName, NSString *blockScript, BOOL *stop) { @@ -158,6 +155,15 @@ static void RCTReportError(RCTJavaScriptCallback callback, NSString *fmt, ...) script = scriptWithInjections; } + script = [_commentsRegex stringByReplacingMatchesInString:script + options:0 + range:NSMakeRange(0, script.length) + withTemplate:@""]; + script = [_scriptTagsRegex stringByReplacingMatchesInString:script + options:0 + range:NSMakeRange(0, script.length) + withTemplate:@"\\\\<$1\\\\>"]; + NSString *runScript = [NSString stringWithFormat:@"", diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index f391e6af0d..ddea1275ac 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -44,50 +44,45 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message return; } -#if RCT_DEBUG // Red box is only available in debug mode - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; -#else + if (!RCT_DEBUG) { - static NSUInteger reloadRetries = 0; - const NSUInteger maxMessageLength = 75; + static NSUInteger reloadRetries = 0; + const NSUInteger maxMessageLength = 75; - if (reloadRetries < _maxReloadAttempts) { + if (reloadRetries < _maxReloadAttempts) { - reloadRetries++; - [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification - object:nil]; + reloadRetries++; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification + object:nil]; - } else { + } else { - if (message.length > maxMessageLength) { - message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."]; + if (message.length > maxMessageLength) { + message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."]; + } + + NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"]; + for (NSDictionary *frame in stack) { + [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; + } + + NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:message]; + [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; } - - NSMutableString *prettyStack = [NSMutableString stringWithString:@"\n"]; - for (NSDictionary *frame in stack) { - [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; - } - - NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:message]; - [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; } - -#endif - } RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message stack:(NSArray *)stack) { + if (_delegate) { + [_delegate unhandledJSExceptionWithMessage:message stack:stack]; + return; + } -#if RCT_DEBUG // Red box is only available in debug mode - - [[RCTRedBox sharedInstance] updateErrorMessage:message withStack:stack]; - -#endif - + [[RCTRedBox sharedInstance] updateErrorMessage:message withStack:stack]; } @end diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index d35cae03a1..df90ff1505 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -19,6 +19,7 @@ #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTDefines.h" +#import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTProfile.h" #import "RCTRootView.h" @@ -420,17 +421,31 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}]; - // Parallel arrays + // Parallel arrays are built and then handed off to main thread NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *areNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *parentsAreNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; + NSMutableArray *onLayoutEvents = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; for (RCTShadowView *shadowView in viewsWithNewFrames) { [frameReactTags addObject:shadowView.reactTag]; [frames addObject:[NSValue valueWithCGRect:shadowView.frame]]; [areNew addObject:@(shadowView.isNewView)]; [parentsAreNew addObject:@(shadowView.superview.isNewView)]; + id event = [NSNull null]; + if (shadowView.hasOnLayout) { + event = @{ + @"target": shadowView.reactTag, + @"layout": @{ + @"x": @(shadowView.frame.origin.x), + @"y": @(shadowView.frame.origin.y), + @"width": @(shadowView.frame.size.width), + @"height": @(shadowView.frame.size.height), + }, + }; + } + [onLayoutEvents addObject:event]; } for (RCTShadowView *shadowView in viewsWithNewFrames) { @@ -448,20 +463,30 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa // Perform layout (possibly animated) NSNumber *rootViewTag = rootShadowView.reactTag; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTResponseSenderBlock callback = self->_layoutAnimation.callback; + __block NSInteger completionsCalled = 0; for (NSUInteger ii = 0; ii < frames.count; ii++) { NSNumber *reactTag = frameReactTags[ii]; UIView *view = viewRegistry[reactTag]; CGRect frame = [frames[ii] CGRectValue]; + id event = onLayoutEvents[ii]; + + BOOL isNew = [areNew[ii] boolValue]; + RCTAnimation *updateAnimation = isNew ? nil : _layoutAnimation.updateAnimation; + BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; + RCTAnimation *createAnimation = shouldAnimateCreation ? _layoutAnimation.createAnimation : nil; void (^completion)(BOOL finished) = ^(BOOL finished) { - if (self->_layoutAnimation.callback) { - self->_layoutAnimation.callback(@[@(finished)]); + completionsCalled++; + if (event != [NSNull null]) { + [self.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event]; + } + if (callback && completionsCalled == frames.count - 1) { + callback(@[@(finished)]); } }; // Animate view update - BOOL isNew = [areNew[ii] boolValue]; - RCTAnimation *updateAnimation = isNew ? nil: _layoutAnimation.updateAnimation; if (updateAnimation) { [updateAnimation performAnimations:^{ [view reactSetFrame:frame]; @@ -478,9 +503,7 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewNa } // Animate view creation - BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; - RCTAnimation *createAnimation = _layoutAnimation.createAnimation; - if (shouldAnimateCreation && createAnimation) { + if (createAnimation) { if ([createAnimation.property isEqualToString:@"scaleXY"]) { view.layer.transform = CATransform3DMakeScale(0, 0, 0); } else if ([createAnimation.property isEqualToString:@"opacity"]) { @@ -1159,6 +1182,12 @@ RCT_EXPORT_METHOD(clearJSResponder) @"captured": @"onNavigationCompleteCapture" } }, + @"topNavLeftButtonTap": @{ + @"phasedRegistrationNames": @{ + @"bubbled": @"onNavLeftButtonTap", + @"captured": @"onNavLefttButtonTapCapture" + } + }, @"topNavRightButtonTap": @{ @"phasedRegistrationNames": @{ @"bubbled": @"onNavRightButtonTap", @@ -1256,6 +1285,9 @@ RCT_EXPORT_METHOD(clearJSResponder) @"topScrollAnimationEnd": @{ @"registrationName": @"onScrollAnimationEnd" }, + @"topLayout": @{ + @"registrationName": @"onLayout" + }, @"topSelectionChange": @{ @"registrationName": @"onSelectionChange" }, diff --git a/React/Views/RCTMap.h b/React/Views/RCTMap.h index 89e4c0a808..d372db56e4 100644 --- a/React/Views/RCTMap.h +++ b/React/Views/RCTMap.h @@ -21,7 +21,7 @@ extern const CGFloat RCTMapZoomBoundBuffer; @interface RCTMap: MKMapView @property (nonatomic, assign) BOOL followUserLocation; -@property (nonatomic, assign) BOOL hasStartedLoading; +@property (nonatomic, assign) BOOL hasStartedRendering; @property (nonatomic, assign) CGFloat minDelta; @property (nonatomic, assign) CGFloat maxDelta; @property (nonatomic, assign) UIEdgeInsets legalLabelInsets; diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 5037d3390b..40b60508e2 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -27,7 +27,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; { if ((self = [super init])) { - _hasStartedLoading = NO; + _hasStartedRendering = NO; // Find Apple link label for (UIView *subview in self.subviews) { diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m index f9a6b9175c..8d334f60ba 100644 --- a/React/Views/RCTMapManager.m +++ b/React/Views/RCTMapManager.m @@ -84,15 +84,15 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) [self _regionChanged:mapView]; // Don't send region did change events until map has - // started loading, as these won't represent the final location - if (mapView.hasStartedLoading) { + // started rendering, as these won't represent the final location + if (mapView.hasStartedRendering) { [self _emitRegionChangeEvent:mapView continuous:NO]; }; } -- (void)mapViewWillStartLoadingMap:(RCTMap *)mapView +- (void)mapViewWillStartRenderingMap:(RCTMap *)mapView { - mapView.hasStartedLoading = YES; + mapView.hasStartedRendering = YES; [self _emitRegionChangeEvent:mapView continuous:NO]; } diff --git a/React/Views/RCTNavItem.h b/React/Views/RCTNavItem.h index 5ae874522e..cd9833a447 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -12,11 +12,19 @@ @interface RCTNavItem : UIView @property (nonatomic, copy) NSString *title; +@property (nonatomic, strong) UIImage *leftButtonIcon; +@property (nonatomic, copy) NSString *leftButtonTitle; +@property (nonatomic, strong) UIImage *rightButtonIcon; @property (nonatomic, copy) NSString *rightButtonTitle; +@property (nonatomic, strong) UIImage *backButtonIcon; @property (nonatomic, copy) NSString *backButtonTitle; @property (nonatomic, assign) BOOL navigationBarHidden; -@property (nonatomic, copy) UIColor *tintColor; -@property (nonatomic, copy) UIColor *barTintColor; -@property (nonatomic, copy) UIColor *titleTextColor; +@property (nonatomic, strong) UIColor *tintColor; +@property (nonatomic, strong) UIColor *barTintColor; +@property (nonatomic, strong) UIColor *titleTextColor; + +@property (nonatomic, readonly) UIBarButtonItem *backButtonItem; +@property (nonatomic, readonly) UIBarButtonItem *leftButtonItem; +@property (nonatomic, readonly) UIBarButtonItem *rightButtonItem; @end diff --git a/React/Views/RCTNavItem.m b/React/Views/RCTNavItem.m index 6b1e92f44a..56346a363b 100644 --- a/React/Views/RCTNavItem.m +++ b/React/Views/RCTNavItem.m @@ -11,5 +11,104 @@ @implementation RCTNavItem -@end +@synthesize backButtonItem = _backButtonItem; +@synthesize leftButtonItem = _leftButtonItem; +@synthesize rightButtonItem = _rightButtonItem; +- (void)setBackButtonTitle:(NSString *)backButtonTitle +{ + _backButtonTitle = backButtonTitle; + _backButtonItem = nil; +} + +- (void)setBackButtonIcon:(UIImage *)backButtonIcon +{ + _backButtonIcon = backButtonIcon; + _backButtonItem = nil; +} + +- (UIBarButtonItem *)backButtonItem +{ + if (!_backButtonItem) { + if (_backButtonIcon) { + _backButtonItem = [[UIBarButtonItem alloc] initWithImage:_backButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_backButtonTitle.length) { + _backButtonItem = [[UIBarButtonItem alloc] initWithTitle:_backButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _backButtonItem = nil; + } + } + return _backButtonItem; +} + +- (void)setLeftButtonTitle:(NSString *)leftButtonTitle +{ + _leftButtonTitle = leftButtonTitle; + _leftButtonItem = nil; +} + +- (void)setLeftButtonIcon:(UIImage *)leftButtonIcon +{ + _leftButtonIcon = leftButtonIcon; + _leftButtonIcon = nil; +} + +- (UIBarButtonItem *)leftButtonItem +{ + if (!_leftButtonItem) { + if (_leftButtonIcon) { + _leftButtonItem = [[UIBarButtonItem alloc] initWithImage:_leftButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_leftButtonTitle.length) { + _leftButtonItem = [[UIBarButtonItem alloc] initWithTitle:_leftButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _leftButtonItem = nil; + } + } + return _leftButtonItem; +} + +- (void)setRightButtonTitle:(NSString *)rightButtonTitle +{ + _rightButtonTitle = rightButtonTitle; + _rightButtonItem = nil; +} + +- (void)setRightButtonIcon:(UIImage *)rightButtonIcon +{ + _rightButtonIcon = rightButtonIcon; + _rightButtonItem = nil; +} + +- (UIBarButtonItem *)rightButtonItem +{ + if (!_rightButtonItem) { + if (_rightButtonIcon) { + _rightButtonItem = [[UIBarButtonItem alloc] initWithImage:_rightButtonIcon + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else if (_rightButtonTitle.length) { + _rightButtonItem = [[UIBarButtonItem alloc] initWithTitle:_rightButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + } else { + _rightButtonItem = nil; + } + } + return _rightButtonItem; +} + +@end diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index fc601632f4..33588c938a 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -21,12 +21,20 @@ RCT_EXPORT_MODULE() return [[RCTNavItem alloc] init]; } +RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL) +RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor) + RCT_EXPORT_VIEW_PROPERTY(title, NSString) -RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString); -RCT_EXPORT_VIEW_PROPERTY(backButtonTitle, NSString); -RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL); -RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor); -RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor); -RCT_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor); +RCT_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor) + +RCT_EXPORT_VIEW_PROPERTY(backButtonIcon, UIImage) +RCT_EXPORT_VIEW_PROPERTY(backButtonTitle, NSString) + +RCT_EXPORT_VIEW_PROPERTY(leftButtonTitle, NSString) +RCT_EXPORT_VIEW_PROPERTY(leftButtonIcon, UIImage) + +RCT_EXPORT_VIEW_PROPERTY(rightButtonIcon, UIImage) +RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString) @end diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index ca8f94242d..1373d8bd16 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -315,8 +315,14 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; - (void)setContentInset:(UIEdgeInsets)contentInset { + CGPoint contentOffset = _scrollView.contentOffset; + _contentInset = contentInset; - [self setNeedsLayout]; + [RCTView autoAdjustInsetsForView:self + withScrollView:_scrollView + updateOffset:NO]; + + _scrollView.contentOffset = contentOffset; } - (void)scrollToOffset:(CGPoint)offset diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 8d68855f7f..83350ac469 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -41,6 +41,7 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); @property (nonatomic, assign) BOOL isBGColorExplicitlySet; // Used to propagate to children @property (nonatomic, strong) UIColor *backgroundColor; // Used to propagate to children @property (nonatomic, assign) RCTUpdateLifecycle layoutLifecycle; +@property (nonatomic, assign) BOOL hasOnLayout; /** * isNewView - Used to track the first time the view is introduced into the hierarchy. It is initialized YES, then is diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 62fb29116f..4dfb296fda 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -198,4 +198,6 @@ RCT_CUSTOM_SHADOW_PROPERTY(backgroundColor, UIColor, RCTShadowView) view.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; } +RCT_REMAP_SHADOW_PROPERTY(onLayout, hasOnLayout, BOOL) + @end diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index 53c2f16a75..400ce5fab6 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -64,7 +64,6 @@ // TODO: find a way to make this less-tightly coupled to navigation controller if ([self.parentViewController isKindOfClass:[UINavigationController class]]) { - [self.navigationController setNavigationBarHidden:_navItem.navigationBarHidden animated:animated]; @@ -73,33 +72,23 @@ return; } - self.navigationItem.title = _navItem.title; - UINavigationBar *bar = self.navigationController.navigationBar; - if (_navItem.barTintColor) { - bar.barTintColor = _navItem.barTintColor; - } - if (_navItem.tintColor) { - bar.tintColor = _navItem.tintColor; - } + bar.barTintColor = _navItem.barTintColor; + bar.tintColor = _navItem.tintColor; if (_navItem.titleTextColor) { [bar setTitleTextAttributes:@{NSForegroundColorAttributeName : _navItem.titleTextColor}]; } - if (_navItem.rightButtonTitle.length > 0) { - self.navigationItem.rightBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.rightButtonTitle - style:UIBarButtonItemStyleDone - target:self - action:@selector(handleNavRightButtonTapped)]; + UINavigationItem *item = self.navigationItem; + item.title = _navItem.title; + item.backBarButtonItem = _navItem.backButtonItem; + if ((item.leftBarButtonItem = _navItem.leftButtonItem)) { + item.leftBarButtonItem.target = self; + item.leftBarButtonItem.action = @selector(handleNavLeftButtonTapped); } - - if (_navItem.backButtonTitle.length > 0) { - self.navigationItem.backBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.backButtonTitle - style:UIBarButtonItemStylePlain - target:nil - action:nil]; + if ((item.rightBarButtonItem = _navItem.rightButtonItem)) { + item.rightBarButtonItem.target = self; + item.rightBarButtonItem.action = @selector(handleNavRightButtonTapped); } } } @@ -114,6 +103,12 @@ self.view = _wrapperView; } +- (void)handleNavLeftButtonTapped +{ + [_eventDispatcher sendInputEventWithName:@"topNavLeftButtonTap" + body:@{@"target":_navItem.reactTag}]; +} + - (void)handleNavRightButtonTapped { [_eventDispatcher sendInputEventWithName:@"topNavRightButtonTap" diff --git a/package.json b/package.json index 8e059c3fa5..61735be642 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "graceful-fs": "^3.0.6", "image-size": "0.3.5", "joi": "~5.1.0", - "jstransform": "10.1.0", + "jstransform": "11.0.1", "module-deps": "3.5.6", "optimist": "0.6.1", "promise": "^7.0.0", diff --git a/packager/react-packager/src/Packager/__tests__/Packager-test.js b/packager/react-packager/src/Packager/__tests__/Packager-test.js index 3f09344624..d4a1c0f36e 100644 --- a/packager/react-packager/src/Packager/__tests__/Packager-test.js +++ b/packager/react-packager/src/Packager/__tests__/Packager-test.js @@ -166,12 +166,12 @@ describe('Packager', function() { }; expect(p.addModule.mock.calls[3][0]).toEqual({ - code: 'lol module.exports = ' + + code: 'lol module.exports = require("AssetRegistry").registerAsset(' + JSON.stringify(imgModule) + - '; lol', - sourceCode: 'module.exports = ' + + '); lol', + sourceCode: 'module.exports = require("AssetRegistry").registerAsset(' + JSON.stringify(imgModule) + - ';', + ');', sourcePath: '/root/img/new_image.png' }); diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js index c03a0432c2..e647f7642c 100644 --- a/packager/react-packager/src/Packager/index.js +++ b/packager/react-packager/src/Packager/index.js @@ -219,7 +219,8 @@ Packager.prototype.generateAssetModule = function(ppackage, module) { ppackage.addAsset(img); - var code = 'module.exports = ' + JSON.stringify(img) + ';'; + var ASSET_TEMPLATE = 'module.exports = require("AssetRegistry").registerAsset(%json);'; + var code = ASSET_TEMPLATE.replace('%json', JSON.stringify(img)); return new ModuleTransport({ code: code,