Merge pull request #1203 from frantic/sync-2015-05-08

Sync 2015 05 08
This commit is contained in:
Alexander Kotliarskyi 2015-05-08 11:54:24 -07:00
commit 62b90cfcc5
46 changed files with 1015 additions and 297 deletions

View File

@ -15,6 +15,12 @@
.*/node_modules/react-tools/src/core/ReactInstanceHandles.js
.*/node_modules/react-tools/src/event/EventPropagators.js
# Ignore commoner tests
.*/node_modules/react-tools/node_modules/commoner/test/.*
# See https://github.com/facebook/flow/issues/442
.*/react-tools/node_modules/commoner/lib/reader.js
# Ignore jest
.*/react-native/node_modules/jest-cli/.*

View File

@ -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

View File

@ -15,6 +15,12 @@
.*/node_modules/react-tools/src/core/ReactInstanceHandles.js
.*/node_modules/react-tools/src/event/EventPropagators.js
# Ignore commoner tests
.*/node_modules/react-tools/node_modules/commoner/test/.*
# See https://github.com/facebook/flow/issues/442
.*/react-tools/node_modules/commoner/lib/reader.js
# Ignore jest
.*/react-native/node_modules/jest-cli/.*

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6751" systemVersion="14C1510" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="7702" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6736"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7701"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
</dependencies>
<objects>

View File

@ -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;
}

View File

@ -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 (
<View style={this.state.containerStyle}>
<Text>
onLayout events are called on mount and whenever layout is updated,
including after layout animations complete.{' '}
<Text style={styles.pressText} onPress={this.animateViewLayout}>
Press here to change layout.
</Text>
</Text>
<View ref="view" onLayout={this.onViewLayout} style={viewStyle}>
<Image
ref="img"
onLayout={this.onImageLayout}
style={styles.image}
source={{uri: 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851561_767334496626293_1958532586_n.png'}}
/>
<Text>
ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'}
</Text>
<Text ref="txt" onLayout={this.onTextLayout} style={styles.text}>
A simple piece of text.{this.state.extraText}
</Text>
<Text>
{'\n'}
Text w/h: {textLayout.width}/{textLayout.height + '\n'}
Image x/y: {imageLayout.x}/{imageLayout.y}
</Text>
</View>
</View>
);
}
});
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 <LayoutEventExample />;
},
}];

View File

@ -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();
})}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

View File

@ -64,6 +64,7 @@ var APIS = [
require('./BorderExample'),
require('./CameraRollExample.ios'),
require('./GeolocationExample'),
require('./LayoutEventsExample'),
require('./LayoutExample'),
require('./NetInfoExample'),
require('./PanResponderExample'),

View File

@ -25,6 +25,7 @@ var TESTS = [
require('./IntegrationTestHarnessTest'),
require('./TimersTest'),
require('./AsyncStorageTest'),
require('./LayoutEventsTest'),
require('./SimpleSnapshotTest'),
];

View File

@ -71,6 +71,11 @@
[_runner runTest:_cmd module:@"AsyncStorageTest"];
}
- (void)testLayoutEvents
{
[_runner runTest:_cmd module:@"LayoutEventsTest"];
}
#pragma mark Snapshot Tests
- (void)testSimpleSnapshot

View File

@ -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 (
<View style={[styles.container, this.state.containerStyle]}>
<View ref="view" onLayout={this.onViewLayout} style={viewStyle}>
<Image
ref="img"
onLayout={this.onImageLayout}
style={styles.image}
source={{uri: 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851561_767334496626293_1958532586_n.png'}}
/>
<Text>
ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'}
</Text>
<Text ref="txt" onLayout={this.onTextLayout} style={styles.text}>
A simple piece of text.{this.state.extraText}
</Text>
<Text>
{'\n'}
Text w/h: {textLayout.width}/{textLayout.height + '\n'}
Image x/y: {imageLayout.x}/{imageLayout.y}
</Text>
</View>
</View>
);
}
});
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;

View File

@ -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 ||

View File

@ -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();
},

View File

@ -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:

View File

@ -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;

View File

@ -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 };

View File

@ -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]);

View File

@ -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', () => {

View File

@ -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;

View File

@ -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() {

View File

@ -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));

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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.

View File

@ -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<RCTJavaScriptExecutor> _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;
}
}

View File

@ -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: {

View File

@ -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

View File

@ -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:@"<html><head></head><body><script type='text/javascript'>%@</script></body></html>",

View File

@ -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

View File

@ -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"
},

View File

@ -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;

View File

@ -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) {

View File

@ -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];
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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",

View File

@ -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'
});

View File

@ -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,