diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js
index fc31a90dc4d6b7..e484d0f6591ce0 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 b888acbf74ca41..3f1883fa65c5bb 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 00000000000000..6aec6257e77629
--- /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 eee731bd53834a..4a2011a654611c 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 00000000000000..13726b4e948939
--- /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 00000000000000..706a9c9052240e
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 a24ec1a54a20ff..dd2336df59dff0 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 dbb5dde835b5cf..1e61a0dbc155e9 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 e0a43e7931fae6..9bf1a4fc1450b9 100644
--- a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m
+++ b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m
@@ -71,6 +71,11 @@ - (void)testAsyncStorage
[_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 00000000000000..7e8cd3a0dcad3b
--- /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 3babd140954201..103e749f78b237 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 e590999debd23f..8d93cd8416bf4c 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 c7ca2ee261ab91..aa69ab0eb70c66 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 241172669f9066..829f7c4256d63a 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 00000000000000..df4173e78fe584
--- /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 a41352f44ce4b0..32965c213ac449 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 632ff96567ee1c..c5fc3bbe1ea3dc 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 02189155877006..29d59a9a5dfc5b 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 bb0aa263c96470..7bddf87b69c786 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 4d4d21e25d6942..a826db7bf5cb34 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 069f00b2491925..489741b053b5ea 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 3a5de284f4910e..ddd6edbea05c71 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 6408526b812e6a..b76518387826eb 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 77cbb215ec6c24..7c8af00cce73a3 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 a7c6a30a6914da..c98e3648b56b8b 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 @@ - (NSDictionary *)modules
return _batchedBridge.modules;
}
+- (RCTEventDispatcher *)eventDispatcher
+{
+ return _eventDispatcher ?: _batchedBridge.eventDispatcher;
+}
+
#define RCT_BRIDGE_WARN(...) \
- (void)__VA_ARGS__ \
{ \
@@ -1082,16 +1088,18 @@ - (void)initJS
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 @@ - (void)initJS
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 @@ - (void)invalidate
_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 @@ - (void)invalidate
}
// 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 @@ - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arg
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 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
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 3d88caf55b41ea..f7e688df560258 100644
--- a/React/Base/RCTDevMenu.m
+++ b/React/Base/RCTDevMenu.m
@@ -73,9 +73,6 @@ - (instancetype)init
{
if ((self = [super init])) {
- _defaults = [NSUserDefaults standardUserDefaults];
- [self updateSettings];
-
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
@@ -93,19 +90,27 @@ - (instancetype)init
name:RCTJavaScriptDidLoadNotification
object:nil];
-#if TARGET_IPHONE_SIMULATOR
+ _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
+
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 @@ - (instancetype)init
return self;
}
-- (void)updateSettings
+- (dispatch_queue_t)methodQueue
{
- __weak RCTDevMenu *weakSelf = self;
- dispatch_async(dispatch_get_main_queue(), ^{
- RCTDevMenu *strongSelf = weakSelf;
- if (!strongSelf) {
- return;
- }
+ return dispatch_get_main_queue();
+}
- strongSelf->_settings = [NSMutableDictionary dictionaryWithDictionary:[strongSelf->_defaults objectForKey:RCTDevMenuSettingsKey]];
+- (void)updateSettings
+{
+ NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey];
+ if ([settings isEqualToDictionary:_settings]) {
+ return;
+ }
- 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 @@ - (void)jsLoaded
});
}
+- (BOOL)isValid
+{
+ return NO;
+}
+
- (void)dealloc
{
[_updateTask cancel];
@@ -170,6 +181,10 @@ - (void)dealloc
- (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 @@ - (void)toggle
- (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 0de61d1721c487..9c5b6d3dd3bf0e 100644
--- a/React/Base/RCTRedBox.m
+++ b/React/Base/RCTRedBox.m
@@ -76,10 +76,27 @@ - (id)initWithFrame:(CGRect)frame
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)dismiss
- (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 a180bae2cf3c7e..56323fb99be674 100644
--- a/React/Executors/RCTWebViewExecutor.m
+++ b/React/Executors/RCTWebViewExecutor.m
@@ -43,6 +43,7 @@ @implementation RCTWebViewExecutor
UIWebView *_webView;
NSMutableDictionary *_objectsToInject;
NSRegularExpression *_commentsRegex;
+ NSRegularExpression *_scriptTagsRegex;
}
@synthesize valid = _valid;
@@ -52,7 +53,8 @@ - (instancetype)initWithWebView:(UIWebView *)webView
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 @@ - (void)executeApplicationScript:(NSString *)script
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 @@ - (void)executeApplicationScript:(NSString *)script
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 f391e6af0dbf88..ddea1275acabd3 100644
--- a/React/Modules/RCTExceptionsManager.m
+++ b/React/Modules/RCTExceptionsManager.m
@@ -44,50 +44,45 @@ - (instancetype)init
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"]];
- }
+ 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];
+ 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 d35cae03a1e4e9..df90ff1505c7bb 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 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo
[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 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo
// 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 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo
}
// 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 @@ - (NSDictionary *)customBubblingEventTypes
@"captured": @"onNavigationCompleteCapture"
}
},
+ @"topNavLeftButtonTap": @{
+ @"phasedRegistrationNames": @{
+ @"bubbled": @"onNavLeftButtonTap",
+ @"captured": @"onNavLefttButtonTapCapture"
+ }
+ },
@"topNavRightButtonTap": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onNavRightButtonTap",
@@ -1256,6 +1285,9 @@ - (NSDictionary *)customDirectEventTypes
@"topScrollAnimationEnd": @{
@"registrationName": @"onScrollAnimationEnd"
},
+ @"topLayout": @{
+ @"registrationName": @"onLayout"
+ },
@"topSelectionChange": @{
@"registrationName": @"onSelectionChange"
},
diff --git a/React/Views/RCTMap.h b/React/Views/RCTMap.h
index 89e4c0a8088ff7..d372db56e465d9 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 5037d3390b1423..40b60508e26da4 100644
--- a/React/Views/RCTMap.m
+++ b/React/Views/RCTMap.m
@@ -27,7 +27,7 @@ - (instancetype)init
{
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 f9a6b9175ccbf0..8d334f60ba6d8c 100644
--- a/React/Views/RCTMapManager.m
+++ b/React/Views/RCTMapManager.m
@@ -84,15 +84,15 @@ - (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated
[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 5ae874522e8a5d..cd9833a4477183 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 6b1e92f44a3493..56346a363b10ac 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 fc601632f4c4e4..33588c938acdb7 100644
--- a/React/Views/RCTNavItemManager.m
+++ b/React/Views/RCTNavItemManager.m
@@ -21,12 +21,20 @@ - (UIView *)view
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 ca8f94242df8c7..1373d8bd16bbbb 100644
--- a/React/Views/RCTScrollView.m
+++ b/React/Views/RCTScrollView.m
@@ -315,8 +315,14 @@ - (void)layoutSubviews
- (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 8d68855f7f186a..83350ac469cb7e 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 62fb29116f5540..4dfb296fda35de 100644
--- a/React/Views/RCTViewManager.m
+++ b/React/Views/RCTViewManager.m
@@ -198,4 +198,6 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)
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 53c2f16a75c73c..400ce5fab6a6f2 100644
--- a/React/Views/RCTWrapperViewController.m
+++ b/React/Views/RCTWrapperViewController.m
@@ -64,7 +64,6 @@ - (void)viewWillAppear:(BOOL)animated
// 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 @@ - (void)viewWillAppear:(BOOL)animated
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 @@ - (void)loadView
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 8e059c3fa5b66c..61735be6422bd0 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 3f09344624df16..d4a1c0f36e3ac4 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 c03a0432c20c26..e647f7642c1fb1 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,