Updates from Tue 11 Aug

This commit is contained in:
Christopher Chedeau
2015-08-11 08:42:07 -07:00
186 changed files with 5731 additions and 3622 deletions

View File

@@ -87,7 +87,7 @@ RCT_ENUM_CONVERTER(CTTextAlignment, (@{
}
NSDictionary *fontDict = dict[@"font"];
CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"]];
CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"] scaleMultiplier:1.0];
if (!font) {
return frame;
}
@@ -144,7 +144,7 @@ RCT_ENUM_CONVERTER(CTTextAlignment, (@{
+ (ARTBrush *)ARTBrush:(id)json
{
NSArray *arr = [self NSArray:json];
NSUInteger type = [self NSUInteger:arr[0]];
NSUInteger type = [self NSUInteger:arr.firstObject];
switch (type) {
case 0: // solid color
// These are probably expensive allocations since it's often the same value.

View File

@@ -9,6 +9,7 @@
#import "RCTActionSheetManager.h"
#import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTUtils.h"
@@ -72,13 +73,13 @@ RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options
successCallback:(RCTResponseSenderBlock)successCallback)
{
NSMutableArray *items = [NSMutableArray array];
id message = options[@"message"];
id url = options[@"url"];
if ([message isKindOfClass:[NSString class]]) {
NSString *message = [RCTConvert NSString:options[@"message"]];
if (message) {
[items addObject:message];
}
if ([url isKindOfClass:[NSString class]]) {
[items addObject:[NSURL URLWithString:url]];
NSURL *URL = [RCTConvert NSURL:options[@"url"]];
if (URL) {
[items addObject:URL];
}
if ([items count] == 0) {
failureCallback(@[@"No `url` or `message` to share"]);

View File

@@ -17,12 +17,12 @@ var InteractionManager = require('InteractionManager');
var Interpolation = require('Interpolation');
var React = require('React');
var Set = require('Set');
var SpringConfig = require('SpringConfig');
var Text = require('Text');
var View = require('View');
var invariant = require('invariant');
var flattenStyle = require('flattenStyle');
var rebound = require('rebound');
var requestAnimationFrame = require('requestAnimationFrame');
import type InterpolationConfigType from 'Interpolation';
@@ -351,12 +351,12 @@ class SpringAnimation extends Animation {
config.tension === undefined && config.friction === undefined,
'You can only define bounciness/speed or tension/friction but not both',
);
springConfig = rebound.SpringConfig.fromBouncinessAndSpeed(
springConfig = SpringConfig.fromBouncinessAndSpeed(
withDefault(config.bounciness, 8),
withDefault(config.speed, 12),
);
} else {
springConfig = rebound.SpringConfig.fromOrigamiTensionAndFriction(
springConfig = SpringConfig.fromOrigamiTensionAndFriction(
withDefault(config.tension, 40),
withDefault(config.friction, 7),
);

View File

@@ -0,0 +1,102 @@
/**
* 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 SpringConfig
* @flow
*/
'use strict';
type SpringConfigType = {
tension: number,
friction: number,
};
function tensionFromOrigamiValue(oValue) {
return (oValue - 30) * 3.62 + 194;
}
function frictionFromOrigamiValue(oValue) {
return (oValue - 8) * 3 + 25;
}
function fromOrigamiTensionAndFriction(
tension: number,
friction: number,
): SpringConfigType {
return {
tension: tensionFromOrigamiValue(tension),
friction: frictionFromOrigamiValue(friction)
};
}
function fromBouncinessAndSpeed(
bounciness: number,
speed: number,
): SpringConfigType {
function normalize(value, startValue, endValue) {
return (value - startValue) / (endValue - startValue);
}
function projectNormal(n, start, end) {
return start + (n * (end - start));
}
function linearInterpolation(t, start, end) {
return t * end + (1 - t) * start;
}
function quadraticOutInterpolation(t, start, end) {
return linearInterpolation(2 * t - t * t, start, end);
}
function b3Friction1(x) {
return (0.0007 * Math.pow(x, 3)) -
(0.031 * Math.pow(x, 2)) + 0.64 * x + 1.28;
}
function b3Friction2(x) {
return (0.000044 * Math.pow(x, 3)) -
(0.006 * Math.pow(x, 2)) + 0.36 * x + 2;
}
function b3Friction3(x) {
return (0.00000045 * Math.pow(x, 3)) -
(0.000332 * Math.pow(x, 2)) + 0.1078 * x + 5.84;
}
function b3Nobounce(tension) {
if (tension <= 18) {
return b3Friction1(tension);
} else if (tension > 18 && tension <= 44) {
return b3Friction2(tension);
} else {
return b3Friction3(tension);
}
}
var b = normalize(bounciness / 1.7, 0, 20);
b = projectNormal(b, 0, 0.8);
var s = normalize(speed / 1.7, 0, 20);
var bouncyTension = projectNormal(s, 0.5, 200);
var bouncyFriction = quadraticOutInterpolation(
b,
b3Nobounce(bouncyTension),
0.01
);
return {
tension: tensionFromOrigamiValue(bouncyTension),
friction: frictionFromOrigamiValue(bouncyFriction)
};
}
module.exports = {
fromOrigamiTensionAndFriction,
fromBouncinessAndSpeed,
};

View File

@@ -69,9 +69,11 @@ type Config = {
delete?: Anim;
}
function configureNext(config: Config, onAnimationDidEnd?: Function, onError?: Function) {
function configureNext(config: Config, onAnimationDidEnd?: Function) {
configChecker({config}, 'config', 'LayoutAnimation.configureNext');
RCTUIManager.configureNextLayoutAnimation(config, onAnimationDidEnd, onError);
RCTUIManager.configureNextLayoutAnimation(
config, onAnimationDidEnd || function() {}, function() { /* unused */ }
);
}
function create(duration: number, type, creationProp): Config {
@@ -107,8 +109,32 @@ var Presets = {
},
};
/**
* Automatically animates views to their new positions when the
* next layout happens.
*
* A common way to use this API is to call `LayoutAnimation.configureNext`
* before calling `setState`.
*/
var LayoutAnimation = {
/**
* Schedules an animation to happen on the next layout.
*
* @param config Specifies animation properties:
*
* - `duration` in milliseconds
* - `create`, config for animating in new views (see `Anim` type)
* - `update`, config for animating views that have been updated
* (see `Anim` type)
*
* @param onAnimationDidEnd Called when the animation finished.
* Only supported on iOS.
* @param onError Called on error. Only supported on iOS.
*/
configureNext,
/**
* Helper for creating a config for `configureNext`.
*/
create,
Types,
Properties,

View File

@@ -22,9 +22,11 @@ if (__DEV__) {
var runnables = {};
type ComponentProvider = () => ReactClass<any, any, any>;
type AppConfig = {
appKey: string;
component: ReactClass<any, any, any>;
component?: ComponentProvider;
run?: Function;
};
@@ -46,12 +48,13 @@ var AppRegistry = {
if (appConfig.run) {
AppRegistry.registerRunnable(appConfig.appKey, appConfig.run);
} else {
invariant(appConfig.component, 'No component provider passed in');
AppRegistry.registerComponent(appConfig.appKey, appConfig.component);
}
}
},
registerComponent: function(appKey: string, getComponentFunc: Function): string {
registerComponent: function(appKey: string, getComponentFunc: ComponentProvider): string {
runnables[appKey] = {
run: (appParameters) =>
renderApplication(getComponentFunc(), appParameters.initialProps, appParameters.rootTag)
@@ -64,6 +67,10 @@ var AppRegistry = {
return appKey;
},
getAppKeys: function(): Array<string> {
return Object.keys(runnables);
},
runApplication: function(appKey: string, appParameters: any): void {
console.log(
'Running application "' + appKey + '" with appParams: ' +

View File

@@ -40,6 +40,10 @@ var NativeModules = {
SourceCode: {
scriptURL: null,
},
BuildInfo: {
appVersion: '0',
buildVersion: '0',
},
};
module.exports = NativeModules;

View File

@@ -333,7 +333,8 @@ var ScrollView = React.createClass({
<View
ref={INNERVIEW}
style={contentContainerStyle}
removeClippedSubviews={this.props.removeClippedSubviews}>
removeClippedSubviews={this.props.removeClippedSubviews}
collapsable={false}>
{this.props.children}
</View>;

View File

@@ -47,6 +47,7 @@ var AndroidTextInputAttributes = {
textAlign: true,
textAlignVertical: true,
keyboardType: true,
mostRecentEventCount: true,
multiline: true,
password: true,
placeholder: true,
@@ -54,6 +55,7 @@ var AndroidTextInputAttributes = {
text: true,
testID: true,
underlineColorAndroid: true,
editable : true,
};
var viewConfigAndroid = {
@@ -142,7 +144,6 @@ var TextInput = React.createClass({
]),
/**
* If false, text is not editable. The default value is true.
* @platform ios
*/
editable: PropTypes.bool,
/**
@@ -481,6 +482,7 @@ var TextInput = React.createClass({
textAlign={textAlign}
textAlignVertical={textAlignVertical}
keyboardType={this.props.keyboardType}
mostRecentEventCount={this.state.mostRecentEventCount}
multiline={this.props.multiline}
onFocus={this._onFocus}
onBlur={this._onBlur}
@@ -495,6 +497,7 @@ var TextInput = React.createClass({
text={this._getText()}
underlineColorAndroid={this.props.underlineColorAndroid}
children={children}
editable={this.props.editable}
/>;
return (
@@ -519,6 +522,12 @@ var TextInput = React.createClass({
},
_onChange: function(event: Event) {
if (Platform.OS === 'android') {
// Android expects the event count to be updated as soon as possible.
this.refs.input.setNativeProps({
mostRecentEventCount: event.nativeEvent.eventCount,
});
}
var text = event.nativeEvent.text;
var eventCount = event.nativeEvent.eventCount;
this.props.onChange && this.props.onChange(event);

View File

@@ -15,6 +15,7 @@
*/
'use strict';
var Platform = require('Platform');
var RCTUIManager = require('NativeModules').UIManager;
var TextInputState = {
@@ -39,7 +40,15 @@ var TextInputState = {
focusTextInput: function(textFieldID: ?number) {
if (this._currentlyFocusedID !== textFieldID && textFieldID !== null) {
this._currentlyFocusedID = textFieldID;
RCTUIManager.focus(textFieldID);
if (Platform.OS === 'ios') {
RCTUIManager.focus(textFieldID);
} else if (Platform.OS === 'android') {
RCTUIManager.dispatchViewManagerCommand(
textFieldID,
RCTUIManager.AndroidTextInput.Commands.focusTextInput,
null
);
}
}
},
@@ -51,7 +60,15 @@ var TextInputState = {
blurTextInput: function(textFieldID: ?number) {
if (this._currentlyFocusedID === textFieldID && textFieldID !== null) {
this._currentlyFocusedID = null;
RCTUIManager.blur(textFieldID);
if (Platform.OS === 'ios') {
RCTUIManager.blur(textFieldID);
} else if (Platform.OS === 'android') {
RCTUIManager.dispatchViewManagerCommand(
textFieldID,
RCTUIManager.AndroidTextInput.Commands.blurTextInput,
null
);
}
}
}
};

View File

@@ -206,6 +206,7 @@ var TouchableHighlight = React.createClass({
render: function() {
return (
<View
accessible={true}
ref={UNDERLAY_REF}
style={this.state.underlayStyle}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
@@ -218,7 +219,6 @@ var TouchableHighlight = React.createClass({
onlyChild(this.props.children),
{
ref: CHILD_REF,
accessible: true,
testID: this.props.testID,
}
)}

View File

@@ -44,6 +44,7 @@ var AccessibilityTraits = [
'pageTurn',
];
// <<<<< WARNING >>>>>
// If adding any properties to View that could change the way layout-only status
// works on iOS, make sure to update ReactNativeViewAttributes.js and
@@ -96,6 +97,27 @@ var View = React.createClass({
*/
accessibilityLabel: PropTypes.string,
/**
* Indicates to accessibility services to treat UI component like a
* native one. Works for Android only.
*/
accessibilityComponentType: PropTypes.oneOf([
'none',
'button',
]),
/**
* Indicates to accessibility services whether the user should be notified
* when this view changes. Works for Android API >= 19 only.
* See http://developer.android.com/reference/android/view/View.html#attr_android:accessibilityLiveRegion
* for references.
*/
accessibilityLiveRegion: PropTypes.oneOf([
'none',
'polite',
'assertive',
]),
/**
* Provides additional traits to screen reader. By default no traits are
* provided unless specified otherwise in element
@@ -118,7 +140,8 @@ var View = React.createClass({
onMagicTap: PropTypes.func,
/**
* Used to locate this view in end-to-end tests.
* Used to locate this view in end-to-end tests. NB: disables the 'layout-only
* view removal' optimization for this view!
*/
testID: PropTypes.string,
@@ -203,8 +226,29 @@ var View = React.createClass({
* different parameters. The downside is that this can use up limited video
* memory, so this prop should be set back to false at the end of the
* interaction/animation.
* @platform android
*/
renderToHardwareTextureAndroid: PropTypes.bool,
/**
* Whether this view should be rendered as a bitmap before compositing.
*
* On iOS, this is useful for animations and interactions that do not
* modify this component's dimensions nor its children; for example, when
* translating the position of a static view, rasterization allows the
* renderer to reuse a cached bitmap of a static view and quickly composite
* it during each frame.
*
* Rasterization incurs an off-screen drawing pass and the bitmap consumes
* memory. Test and measure when using this property.
* @platform ios
*/
shouldRasterizeIOS: PropTypes.bool,
/**
* @platform android
*/
collapsable: PropTypes.bool,
},
render: function() {

View File

@@ -2,64 +2,125 @@
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule NavigationRouteStack
* @flow
*/
'use strict';
var immutable = require('immutable');
var invariant = require('invariant');
var {List} = immutable;
type IterationCallback = (route: any, index: number, key: string) => void;
var {List, Set} = immutable;
function isRouteEmpty(route: any): boolean {
return (route === undefined || route === null || route === '') || false;
}
var _nextID = 0;
class RouteNode {
key: string;
value: any;
constructor(route: any) {
// Key value gets bigger incrementally. Developer can compare the
// keys of two routes then know which route is added to the stack
// earlier.
this.key = String(_nextID++);
this.value = route;
}
}
var StackDiffRecord = immutable.Record({
key: null,
route: null,
index: null,
});
/**
* The immutable routes stack.
* The immutable route stack.
*/
class RouteStack {
_index: number;
_routes: List;
_routeNodes: List<RouteNode>;
constructor(index: number, routes: List) {
constructor(index: number, routeNodes: List<RouteNode>) {
invariant(
routes.size > 0,
routeNodes.size > 0,
'size must not be empty'
);
invariant(
index > -1 && index <= routes.size - 1,
index > -1 && index <= routeNodes.size - 1,
'index out of bound'
);
this._routes = routes;
this._routeNodes = routeNodes;
this._index = index;
}
/* $FlowFixMe - get/set properties not yet supported */
get size(): number {
return this._routes.size;
return this._routeNodes.size;
}
/* $FlowFixMe - get/set properties not yet supported */
get index(): number {
return this._index;
}
toArray(): Array {
return this._routes.toJS();
toArray(): Array<any> {
var result = [];
var ii = 0;
var nodes = this._routeNodes;
while (ii < nodes.size) {
result.push(nodes.get(ii).value);
ii++;
}
return result;
}
get(index: number): any {
if (index < 0 || index > this._routes.size - 1) {
if (index < 0 || index > this._routeNodes.size - 1) {
return null;
}
return this._routes.get(index);
return this._routeNodes.get(index).value;
}
/**
* Returns the key associated with the route.
* When a route is added to a stack, the stack creates a key for this route.
* The key will persist until the initial stack and its derived stack
* no longer contains this route.
*/
keyOf(route: any): ?string {
if (isRouteEmpty(route)) {
return null;
}
var index = this.indexOf(route);
return index > -1 ?
this._routeNodes.get(index).key :
null;
}
indexOf(route: any): number {
return this._routes.indexOf(route);
if (isRouteEmpty(route)) {
return -1;
}
var finder = (node) => {
return (node: RouteNode).value === route;
};
return this._routeNodes.findIndex(finder, this);
}
slice(begin: ?number, end: ?number): RouteStack {
var routes = this._routes.slice(begin, end);
var index = Math.min(this._index, routes.size - 1);
return this._update(index, routes);
var routeNodes = this._routeNodes.slice(begin, end);
var index = Math.min(this._index, routeNodes.size - 1);
return this._update(index, routeNodes);
}
/**
@@ -67,21 +128,20 @@ class RouteStack {
* starting at this stack size.
*/
push(route: any): RouteStack {
invariant(
route === 0 ||
route === false ||
!!route,
!isRouteEmpty(route),
'Must supply route to push'
);
invariant(this._routes.indexOf(route) === -1, 'route must be unique');
invariant(this._routeNodes.indexOf(route) === -1, 'route must be unique');
// When pushing, removes the rest of the routes past the current index.
var routes = this._routes.withMutations((list: List) => {
list.slice(0, this._index + 1).push(route);
var routeNodes = this._routeNodes.withMutations((list: List) => {
list.slice(0, this._index + 1).push(new RouteNode(route));
});
return this._update(routes.size - 1, routes);
return this._update(routeNodes.size - 1, routeNodes);
}
/**
@@ -89,20 +149,20 @@ class RouteStack {
* excluding the last index in this stack.
*/
pop(): RouteStack {
invariant(this._routes.size > 1, 'shoud not pop routes stack to empty');
invariant(this._routeNodes.size > 1, 'shoud not pop routeNodes stack to empty');
// When popping, removes the rest of the routes past the current index.
var routes = this._routes.slice(0, this._index);
return this._update(routes.size - 1, routes);
var routeNodes = this._routeNodes.slice(0, this._index);
return this._update(routeNodes.size - 1, routeNodes);
}
jumpToIndex(index: number): RouteStack {
invariant(
index > -1 && index < this._routes.size,
index > -1 && index < this._routeNodes.size,
'index out of bound'
);
return this._update(index, this._routes);
return this._update(index, this._routeNodes);
}
/**
@@ -113,9 +173,7 @@ class RouteStack {
*/
replaceAtIndex(index: number, route: any): RouteStack {
invariant(
route === 0 ||
route === false ||
!!route,
!isRouteEmpty(route),
'Must supply route to replace'
);
@@ -123,26 +181,64 @@ class RouteStack {
return this;
}
invariant(this._routes.indexOf(route) === -1, 'route must be unique');
invariant(this.indexOf(route) === -1, 'route must be unique');
if (index < 0) {
index += this._routes.size;
index += this._routeNodes.size;
}
invariant(
index > -1 && index < this._routes.size,
index > -1 && index < this._routeNodes.size,
'index out of bound'
);
var routes = this._routes.set(index, route);
return this._update(index, routes);
var routeNodes = this._routeNodes.set(index, new RouteNode(route));
return this._update(index, routeNodes);
}
_update(index: number, routes: List): RouteStack {
if (this._index === index && this._routes === routes) {
// Iterations
forEach(callback: IterationCallback, context: ?Object): void {
var ii = 0;
var nodes = this._routeNodes;
while (ii < nodes.size) {
var node = nodes.get(ii);
callback.call(context, node.value, ii, node.key);
ii++;
}
}
mapToArray(callback: IterationCallback, context: ?Object): Array<any> {
var result = [];
this.forEach((route, index, key) => {
result.push(callback.call(context, route, index, key));
});
return result;
}
/**
* Returns a Set excluding any routes contained within the stack given.
*/
subtract(stack: RouteStack): Set<StackDiffRecord> {
var items = [];
this._routeNodes.forEach((node: RouteNode, index: number) => {
if (!stack._routeNodes.contains(node)) {
items.push(
new StackDiffRecord({
route: node.value,
index: index,
key: node.key,
})
);
}
});
return new Set(items);
}
_update(index: number, routeNodes: List): RouteStack {
if (this._index === index && this._routeNodes === routeNodes) {
return this;
}
return new RouteStack(index, routes);
return new RouteStack(index, routeNodes);
}
}
@@ -151,12 +247,17 @@ class RouteStack {
* stack of routes.
*/
class NavigationRouteStack extends RouteStack {
constructor(index: number, routes: Array) {
constructor(index: number, routeNodes: Array<any>) {
// For now, `RouteStack` internally, uses an immutable `List` to keep
// track of routes. Since using `List` is really just the implementation
// detail, we don't want to accept `routes` as `list` from constructor
// track of routeNodes. Since using `List` is really just the implementation
// detail, we don't want to accept `routeNodes` as `list` from constructor
// for developer.
super(index, new List(routes));
var nodes = routeNodes.map((route) => {
invariant(!isRouteEmpty(route), 'route must not be mepty');
return new RouteNode(route);
});
super(index, new List(nodes));
}
}

View File

@@ -24,15 +24,27 @@
*/
'use strict';
jest
.dontMock('NavigationRouteStack')
.dontMock('clamp')
.dontMock('invariant')
.dontMock('immutable');
.autoMockOff()
.mock('ErrorUtils');
var NavigationRouteStack = require('NavigationRouteStack');
function assetStringNotEmpty(str) {
expect(!!str && typeof str === 'string').toBe(true);
}
describe('NavigationRouteStack:', () => {
// Different types of routes.
var ROUTES = [
'foo',
1,
true,
{foo: 'bar'},
['foo'],
];
// Basic
it('gets index', () => {
var stack = new NavigationRouteStack(1, ['a', 'b', 'c']);
@@ -76,6 +88,92 @@ describe('NavigationRouteStack:', () => {
expect(stack.indexOf('c')).toBe(-1);
});
// Key
it('gets key for route', () => {
var test = (route) => {
var stack = new NavigationRouteStack(0, ['a']);
var key = stack.push(route).keyOf(route);
expect(typeof key).toBe('string');
expect(!!key).toBe(true);
};
ROUTES.forEach(test);
});
it('gets a key of larger value for route', () => {
var lastKey = '';
var test = (route) => {
var stack = new NavigationRouteStack(0, ['a']);
var key = stack.push(route).keyOf(route);
expect(key > lastKey).toBe(true);
lastKey = key;
};
ROUTES.forEach(test);
});
it('gets an unique key for a different route', () => {
var stack = new NavigationRouteStack(0, ['a']);
var keys = {};
var test = (route) => {
stack = stack.push(route);
var key = stack.keyOf(route);
expect(keys[key]).toBe(undefined);
keys[key] = true;
};
ROUTES.forEach(test);
});
it('gets the same unique key for the same route', () => {
var test = (route) => {
var stack = new NavigationRouteStack(0, [route]);
expect(stack.keyOf(route)).toBe(stack.keyOf(route));
};
ROUTES.forEach(test);
});
it('gets the same unique key form the derived stack', () => {
var test = (route) => {
var stack = new NavigationRouteStack(0, [route]);
var derivedStack = stack.push('wow').pop().slice(0, 10).push('blah');
expect(derivedStack.keyOf(route)).toBe(stack.keyOf(route));
};
ROUTES.forEach(test);
});
it('gets a different key from a different stack', () => {
var test = (route) => {
var stack1 = new NavigationRouteStack(0, [route]);
var stack2 = new NavigationRouteStack(0, [route]);
expect(stack1.keyOf(route)).not.toBe(stack2.keyOf(route));
};
ROUTES.forEach(test);
});
it('gets no key for a route that does not contains this route', () => {
var stack = new NavigationRouteStack(0, ['a']);
expect(stack.keyOf('b')).toBe(null);
});
it('gets a new key for a route that was removed and added again', () => {
var test = (route) => {
var stack = new NavigationRouteStack(0, ['a']);
var key1 = stack.push(route).keyOf(route);
var key2 = stack.push(route).pop().push(route).keyOf(route);
expect(key1).not.toBe(key2);
};
ROUTES.forEach(test);
});
// Slice
it('slices', () => {
var stack1 = new NavigationRouteStack(1, ['a', 'b', 'c', 'd']);
var stack2 = stack1.slice(1, 3);
@@ -226,4 +324,110 @@ describe('NavigationRouteStack:', () => {
stack.replaceAtIndex(100, 'x');
}).toThrow();
});
// Iteration
it('iterates each item', () => {
var stack = new NavigationRouteStack(0, ['a', 'b']);
var logs = [];
var keys = {};
var context = {name: 'yo'};
stack.forEach(function (route, index, key) {
assetStringNotEmpty(key);
if (!keys.hasOwnProperty(key)) {
keys[key] = true;
logs.push([
route,
index,
this.name,
]);
}
}, context);
expect(logs).toEqual([
['a', 0, 'yo'],
['b', 1, 'yo'],
]);
});
it('Maps to an array', () => {
var stack = new NavigationRouteStack(0, ['a', 'b']);
var keys = {};
var context = {name: 'yo'};
var logs = stack.mapToArray(function(route, index, key) {
assetStringNotEmpty(key);
if (!keys.hasOwnProperty(key)) {
keys[key] = true;
return [
route,
index,
this.name,
];
}
}, context);
expect(logs).toEqual([
['a', 0, 'yo'],
['b', 1, 'yo'],
]);
});
// Diff
it('subtracts stack', () => {
var stack1 = new NavigationRouteStack(2, ['a', 'b', 'c']);
var stack2 = stack1.pop().pop().push('x').push('y');
var diff = stack1.subtract(stack2);
var result = diff.toJS().map((record) => {
assetStringNotEmpty(record.key);
return {
index: record.index,
route: record.route,
};
});
// route `b` and `c` are no longer in the stack.
expect(result).toEqual([
{
index: 1,
route: 'b',
},
{
index: 2,
route: 'c',
},
]);
});
it('only subtracts the derived stack', () => {
var stack1 = new NavigationRouteStack(2, ['a', 'b', 'c']);
var stack2 = new NavigationRouteStack(0, ['a']);
var diff = stack1.subtract(stack2);
var result = diff.toJS().map((record) => {
assetStringNotEmpty(record.key);
return {
index: record.index,
route: record.route,
};
});
expect(result).toEqual([
{
index: 0,
route: 'a',
},
{
index: 1,
route: 'b',
},
{
index: 2,
route: 'c',
},
]);
});
});

View File

@@ -59,6 +59,7 @@ var SCENE_DISABLED_NATIVE_PROPS = {
pointerEvents: 'none',
style: {
top: SCREEN_HEIGHT,
bottom: -SCREEN_HEIGHT,
opacity: 0,
},
};
@@ -109,6 +110,7 @@ var styles = StyleSheet.create({
},
disabledScene: {
top: SCREEN_HEIGHT,
bottom: -SCREEN_HEIGHT,
},
transitioner: {
flex: 1,
@@ -535,6 +537,7 @@ var Navigator = React.createClass({
pointerEvents: 'auto',
style: {
top: sceneStyle.top,
bottom: sceneStyle.bottom,
},
};
if (sceneIndex !== this.state.transitionFromIndex &&
@@ -922,7 +925,19 @@ var Navigator = React.createClass({
},
pop: function() {
this._popN(1);
if (this.state.transitionQueue.length) {
// This is the workaround to prevent user from firing multiple `pop()`
// calls that may pop the routes beyond the limit.
// Because `this.state.presentedIndex` does not update until the
// transition starts, we can't reliably use `this.state.presentedIndex`
// to know whether we can safely keep popping the routes or not at this
// moment.
return;
}
if (this.state.presentedIndex > 0) {
this._popN(1);
}
},
/**

View File

@@ -173,7 +173,7 @@ var Image = React.createClass({
resizeMode={resizeMode}
tintColor={tintColor}
src={source.uri}
defaultSrc={defaultSource.uri}
defaultImageSrc={defaultSource.uri}
/>
);
}

View File

@@ -1,21 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
typedef void (^RCTDataCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error);
typedef void (^RCTDataProgressBlock)(int64_t written, int64_t total);
@interface RCTDownloadTaskWrapper : NSObject <NSURLSessionDownloadDelegate>
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration delegateQueue:(NSOperationQueue *)delegateQueue;
- (NSURLSessionDownloadTask *)downloadData:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock completionBlock:(RCTDataCompletionBlock)completionBlock;
@end

View File

@@ -1,98 +0,0 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTDownloadTaskWrapper.h"
#import <objc/runtime.h>
@interface NSObject (RCTDownloadTaskWrapper)
@property (nonatomic, copy) RCTDataCompletionBlock reactCompletionBlock;
@property (nonatomic, copy) RCTDataProgressBlock reactProgressBlock;
@end
@implementation NSObject (RCTDownloadTaskWrapper)
- (RCTDataCompletionBlock)reactCompletionBlock
{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setReactCompletionBlock:(RCTDataCompletionBlock)completionBlock
{
objc_setAssociatedObject(self, @selector(reactCompletionBlock), completionBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (RCTDataProgressBlock)reactProgressBlock
{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setReactProgressBlock:(RCTDataProgressBlock)progressBlock
{
objc_setAssociatedObject(self, @selector(reactProgressBlock), progressBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
@implementation RCTDownloadTaskWrapper
{
NSURLSession *_URLSession;
}
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration delegateQueue:(NSOperationQueue *)delegateQueue
{
if ((self = [super init])) {
_URLSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
}
return self;
}
- (NSURLSessionDownloadTask *)downloadData:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock completionBlock:(RCTDataCompletionBlock)completionBlock
{
NSURLSessionDownloadTask *task = [_URLSession downloadTaskWithURL:url];
task.reactCompletionBlock = completionBlock;
task.reactProgressBlock = progressBlock;
return task;
}
#pragma mark - NSURLSessionTaskDelegate methods
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
if (downloadTask.reactCompletionBlock) {
NSData *data = [NSData dataWithContentsOfURL:location];
dispatch_async(dispatch_get_main_queue(), ^{
downloadTask.reactCompletionBlock(downloadTask.response, data, nil);
});
}
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)didWriteData totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
{
if (downloadTask.reactProgressBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
downloadTask.reactProgressBlock(totalBytesWritten, totalBytesExpectedToWrite);
});
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error && task.reactCompletionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
task.reactCompletionBlock(nil, nil, error);
});
}
}
@end

View File

@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
03559E7F1B064DAF00730281 /* RCTDownloadTaskWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */; };
1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTImageView.m */; };
1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */; };
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; };
@@ -17,6 +16,7 @@
143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; };
143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; };
35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */; };
354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 354631671B69857700AA0B86 /* RCTImageEditingManager.m */; };
58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */; };
/* End PBXBuildFile section */
@@ -33,8 +33,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
03559E7D1B064D3A00730281 /* RCTDownloadTaskWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTDownloadTaskWrapper.h; sourceTree = "<group>"; };
03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTaskWrapper.m; sourceTree = "<group>"; };
1304D5A71AA8C4A30002E2BE /* RCTImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageView.h; sourceTree = "<group>"; };
1304D5A81AA8C4A30002E2BE /* RCTImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageView.m; sourceTree = "<group>"; };
1304D5A91AA8C4A30002E2BE /* RCTImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageViewManager.h; sourceTree = "<group>"; };
@@ -53,6 +51,8 @@
143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = "<group>"; };
35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageStoreManager.h; sourceTree = "<group>"; };
35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageStoreManager.m; sourceTree = "<group>"; };
354631661B69857700AA0B86 /* RCTImageEditingManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageEditingManager.h; sourceTree = "<group>"; };
354631671B69857700AA0B86 /* RCTImageEditingManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageEditingManager.m; sourceTree = "<group>"; };
58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; };
58B511891A9E6BD600147676 /* RCTImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageDownloader.h; sourceTree = "<group>"; };
58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageDownloader.m; sourceTree = "<group>"; };
@@ -74,12 +74,12 @@
children = (
143879331AAD238D00F088A5 /* RCTCameraRollManager.h */,
143879341AAD238D00F088A5 /* RCTCameraRollManager.m */,
03559E7D1B064D3A00730281 /* RCTDownloadTaskWrapper.h */,
03559E7E1B064DAF00730281 /* RCTDownloadTaskWrapper.m */,
1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */,
1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */,
58B511891A9E6BD600147676 /* RCTImageDownloader.h */,
58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */,
354631661B69857700AA0B86 /* RCTImageEditingManager.h */,
354631671B69857700AA0B86 /* RCTImageEditingManager.m */,
143879361AAD32A300F088A5 /* RCTImageLoader.h */,
143879371AAD32A300F088A5 /* RCTImageLoader.m */,
137620331B31C53500677FF0 /* RCTImagePickerManager.h */,
@@ -172,7 +172,7 @@
1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */,
143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */,
143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */,
03559E7F1B064DAF00730281 /* RCTDownloadTaskWrapper.m in Sources */,
354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */,
1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */,
134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */,
);
@@ -275,6 +275,7 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../React/**",
"$(SRCROOT)/../Network/**",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
@@ -295,6 +296,7 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../React/**",
"$(SRCROOT)/../Network/**",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -9,24 +9,10 @@
#import <UIKit/UIKit.h>
#import "RCTDownloadTaskWrapper.h"
#import "RCTBridge.h"
#import "RCTImageLoader.h"
typedef void (^RCTDataDownloadBlock)(NSData *data, NSError *error);
typedef void (^RCTImageDownloadBlock)(UIImage *image, NSError *error);
typedef void (^RCTImageDownloadCancellationBlock)(void);
@interface RCTImageDownloader : NSObject
+ (RCTImageDownloader *)sharedInstance;
/**
* Downloads a block of raw data and returns it. Note that the callback block
* will not be executed on the same thread you called the method from, nor on
* the main thread. Returns a token that can be used to cancel the download.
*/
- (RCTImageDownloadCancellationBlock)downloadDataForURL:(NSURL *)url
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTDataDownloadBlock)block;
@interface RCTImageDownloader : NSObject <RCTBridgeModule>
/**
* Downloads an image and decompresses it a the size specified. The compressed
@@ -34,13 +20,17 @@ typedef void (^RCTImageDownloadCancellationBlock)(void);
* will not be executed on the same thread you called the method from, nor on
* the main thread. Returns a token that can be used to cancel the download.
*/
- (RCTImageDownloadCancellationBlock)downloadImageForURL:(NSURL *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
tintColor:(UIColor *)tintColor
backgroundColor:(UIColor *)backgroundColor
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTImageDownloadBlock)block;
- (RCTImageLoaderCancellationBlock)downloadImageForURL:(NSURL *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressBlock
completionBlock:(RCTImageLoaderCompletionBlock)block;
@end
@interface RCTBridge (RCTImageDownloader)
@property (nonatomic, readonly) RCTImageDownloader *imageDownloader;
@end

View File

@@ -9,14 +9,12 @@
#import "RCTImageDownloader.h"
#import "RCTDownloadTaskWrapper.h"
#import "RCTGIFImage.h"
#import "RCTImageUtils.h"
#import "RCTLog.h"
#import "RCTNetworking.h"
#import "RCTUtils.h"
typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSURLResponse *response,
NSData *data, NSError *error);
CGSize RCTTargetSizeForClipRect(CGRect);
CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
@@ -24,10 +22,12 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
{
NSURLCache *_cache;
dispatch_queue_t _processingQueue;
NSMutableDictionary *_pendingBlocks;
RCTDownloadTaskWrapper *_downloadTaskWrapper;
}
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
+ (RCTImageDownloader *)sharedInstance
{
static RCTImageDownloader *sharedInstance;
@@ -43,122 +43,88 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
if ((self = [super init])) {
_cache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 diskCapacity:200 * 1024 * 1024 diskPath:@"React/RCTImageDownloader"];
_processingQueue = dispatch_queue_create("com.facebook.React.DownloadProcessingQueue", DISPATCH_QUEUE_SERIAL);
_pendingBlocks = [[NSMutableDictionary alloc] init];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
_downloadTaskWrapper = [[RCTDownloadTaskWrapper alloc] initWithSessionConfiguration:config delegateQueue:nil];
}
return self;
}
- (RCTImageDownloadCancellationBlock)_downloadDataForURL:(NSURL *)url
progressBlock:progressBlock
block:(RCTCachedDataDownloadBlock)block
/**
* Downloads a block of raw data and returns it. Note that the callback block
* will not be executed on the same thread you called the method from, nor on
* the main thread. Returns a token that can be used to cancel the download.
*/
- (RCTImageLoaderCancellationBlock)downloadDataForURL:(NSURL *)url
progressBlock:(RCTImageLoaderProgressBlock)progressBlock
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
{
NSString *const cacheKey = url.absoluteString;
if (![_bridge respondsToSelector:NSSelectorFromString(@"networking")]) {
RCTLogError(@"You need to import the RCTNetworking library in order to download remote images.");
return ^{};
}
__block BOOL cancelled = NO;
__block NSURLSessionDownloadTask *task = nil;
__weak RCTImageDownloader *weakSelf = self;
RCTURLRequestCompletionBlock runBlocks = ^(NSURLResponse *response, NSData *data, NSError *error) {
RCTImageDownloadCancellationBlock cancel = ^{
cancelled = YES;
if (!error && [response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode != 200) {
data = nil;
error = [[NSError alloc] initWithDomain:NSURLErrorDomain
code:httpResponse.statusCode
userInfo:nil];
}
}
dispatch_async(_processingQueue, ^{
NSMutableArray *pendingBlocks = self->_pendingBlocks[cacheKey];
[pendingBlocks removeObject:block];
completionBlock(error, data);
});
if (task) {
[task cancel];
task = nil;
}
};
dispatch_async(_processingQueue, ^{
NSMutableArray *pendingBlocks = _pendingBlocks[cacheKey];
if (pendingBlocks) {
[pendingBlocks addObject:block];
} else {
_pendingBlocks[cacheKey] = [NSMutableArray arrayWithObject:block];
__weak RCTImageDownloader *weakSelf = self;
RCTCachedDataDownloadBlock runBlocks = ^(BOOL cached, NSURLResponse *response, NSData *data, NSError *error) {
if (!error && [response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode != 200) {
data = nil;
error = [[NSError alloc] initWithDomain:NSURLErrorDomain
code:httpResponse.statusCode
userInfo:nil];
}
}
dispatch_async(_processingQueue, ^{
RCTImageDownloader *strongSelf = weakSelf;
NSArray *blocks = strongSelf->_pendingBlocks[cacheKey];
[strongSelf->_pendingBlocks removeObjectForKey:cacheKey];
for (RCTCachedDataDownloadBlock downloadBlock in blocks) {
downloadBlock(cached, response, data, error);
}
});
};
NSURLRequest *request = [NSURLRequest requestWithURL:url];
task = [_downloadTaskWrapper downloadData:url progressBlock:progressBlock completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!cancelled) {
runBlocks(NO, response, data, error);
}
if (response && !error) {
RCTImageDownloader *strongSelf = weakSelf;
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data userInfo:nil storagePolicy:NSURLCacheStorageAllowed];
[strongSelf->_cache storeCachedResponse:cachedResponse forRequest:request];
}
task = nil;
}];
NSCachedURLResponse *cachedResponse = [_cache cachedResponseForRequest:request];
if (cancelled) {
return;
}
if (cachedResponse) {
runBlocks(YES, cachedResponse.response, cachedResponse.data, nil);
} else {
[task resume];
}
NSURLRequest *request = [NSURLRequest requestWithURL:url];
{
NSCachedURLResponse *cachedResponse = [_cache cachedResponseForRequest:request];
if (cachedResponse) {
runBlocks(cachedResponse.response, cachedResponse.data, nil);
return ^{};
}
});
}
return [cancel copy];
}
- (RCTImageDownloadCancellationBlock)downloadDataForURL:(NSURL *)url
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTDataDownloadBlock)block
{
return [self _downloadDataForURL:url progressBlock:progressBlock block:^(BOOL cached, NSURLResponse *response, NSData *data, NSError *error) {
block(data, error);
RCTDownloadTask *task = [_bridge.networking downloadTaskWithRequest:request completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
if (response && !error) {
RCTImageDownloader *strongSelf = weakSelf;
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data userInfo:nil storagePolicy:NSURLCacheStorageAllowed];
[strongSelf->_cache storeCachedResponse:cachedResponse forRequest:request];
}
runBlocks(response, data, error);
}];
if (progressBlock) {
task.downloadProgressBlock = progressBlock;
}
return ^{ [task cancel]; };
}
- (RCTImageDownloadCancellationBlock)downloadImageForURL:(NSURL *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
tintColor:(UIColor *)tintColor
backgroundColor:(UIColor *)backgroundColor
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTImageDownloadBlock)block
- (RCTImageLoaderCancellationBlock)downloadImageForURL:(NSURL *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressBlock
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
{
scale = scale ?: RCTScreenScale();
return [self downloadDataForURL:url progressBlock:progressBlock block:^(NSData *data, NSError *error) {
return [self downloadDataForURL:url progressBlock:progressBlock completionBlock:^(NSError *error, id data) {
if (!data || error) {
block(nil, error);
completionBlock(error, nil);
return;
}
if ([url.path.lowercaseString hasSuffix:@".gif"]) {
id image = RCTGIFImageWithData(data);
if (!image && !error) {
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load GIF image: %@", url];
error = RCTErrorWithMessage(errorMessage);
}
completionBlock(error, image);
return;
}
@@ -169,35 +135,25 @@ CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
CGRect imageRect = RCTClipRect(image.size, scale, size, scale, resizeMode);
CGSize destSize = RCTTargetSizeForClipRect(imageRect);
// Opacity optimizations
UIColor *blendColor = nil;
BOOL opaque = !RCTImageHasAlpha(image.CGImage);
if (!opaque && backgroundColor) {
CGFloat alpha;
[backgroundColor getRed:NULL green:NULL blue:NULL alpha:&alpha];
if (alpha > 0.999) { // no benefit to blending if background is translucent
opaque = YES;
blendColor = backgroundColor;
}
}
// Decompress image at required size
BOOL opaque = !RCTImageHasAlpha(image.CGImage);
UIGraphicsBeginImageContextWithOptions(destSize, opaque, scale);
if (blendColor) {
[blendColor setFill];
UIRectFill((CGRect){CGPointZero, destSize});
}
if (tintColor) {
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[tintColor setFill];
}
[image drawInRect:imageRect];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
block(image, nil);
completionBlock(nil, image);
}];
}
@end
@implementation RCTBridge (RCTImageDownloader)
- (RCTImageDownloader *)imageDownloader
{
return self.modules[RCTBridgeModuleNameForClass([RCTImageDownloader class])];
}
@end

View File

@@ -0,0 +1,14 @@
/**
* 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.
*/
#import "RCTBridgeModule.h"
@interface RCTImageEditingManager : NSObject <RCTBridgeModule>
@end

View File

@@ -0,0 +1,133 @@
/**
* 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.
*/
#import "RCTImageEditingManager.h"
#import <UIKit/UIKit.h>
#import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTUtils.h"
#import "RCTImageStoreManager.h"
#import "RCTImageLoader.h"
@implementation RCTImageEditingManager
RCT_EXPORT_MODULE()
@synthesize bridge = _bridge;
/**
* Crops an image and adds the result to the image store.
*
* @param imageTag A URL, a string identifying an asset etc.
* @param cropData Dictionary with `offset`, `size` and `displaySize`.
* `offset` and `size` are relative to the full-resolution image size.
* `displaySize` is an optimization - if specified, the image will
* be scaled down to `displaySize` rather than `size`.
* All units are in px (not points).
*/
RCT_EXPORT_METHOD(cropImage:(NSString *)imageTag
cropData:(NSDictionary *)cropData
successCallback:(RCTResponseSenderBlock)successCallback
errorCallback:(RCTResponseErrorBlock)errorCallback)
{
NSDictionary *offset = cropData[@"offset"];
NSDictionary *size = cropData[@"size"];
NSDictionary *displaySize = cropData[@"displaySize"];
NSString *resizeMode = cropData[@"resizeMode"] ?: @"contain";
if (!offset[@"x"] || !offset[@"y"] || !size[@"width"] || !size[@"height"]) {
NSString *errorMessage = [NSString stringWithFormat:@"Invalid cropData: %@", cropData];
RCTLogError(@"%@", errorMessage);
errorCallback(RCTErrorWithMessage(errorMessage));
return;
}
[_bridge.imageLoader loadImageWithTag:imageTag callback:^(NSError *error, UIImage *image) {
if (error) {
errorCallback(error);
return;
}
CGRect rect = (CGRect){
[RCTConvert CGPoint:offset],
[RCTConvert CGSize:size]
};
// Crop image
CGRect rectToDrawIn = {{-rect.origin.x, -rect.origin.y}, image.size};
UIGraphicsBeginImageContextWithOptions(rect.size, !RCTImageHasAlpha(image.CGImage), image.scale);
[image drawInRect:rectToDrawIn];
UIImage *croppedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (displaySize && displaySize[@"width"] && displaySize[@"height"]) {
CGSize targetSize = [RCTConvert CGSize:displaySize];
croppedImage = [self scaleImage:croppedImage targetSize:targetSize resizeMode:resizeMode];
}
[_bridge.imageStoreManager storeImage:croppedImage withBlock:^(NSString *croppedImageTag) {
if (!croppedImageTag) {
NSString *errorMessage = @"Error storing cropped image in RCTImageStoreManager";
RCTLogWarn(@"%@", errorMessage);
errorCallback(RCTErrorWithMessage(errorMessage));
return;
}
successCallback(@[croppedImageTag]);
}];
}];
}
- (UIImage *)scaleImage:(UIImage *)image targetSize:(CGSize)targetSize resizeMode:(NSString *)resizeMode
{
if (CGSizeEqualToSize(image.size, targetSize)) {
return image;
}
CGFloat imageRatio = image.size.width / image.size.height;
CGFloat targetRatio = targetSize.width / targetSize.height;
CGFloat newWidth = targetSize.width;
CGFloat newHeight = targetSize.height;
// contain vs cover
// http://blog.vjeux.com/2013/image/css-container-and-cover.html
if ([resizeMode isEqualToString:@"contain"]) {
if (imageRatio <= targetRatio) {
newWidth = targetSize.height * imageRatio;
newHeight = targetSize.height;
} else {
newWidth = targetSize.width;
newHeight = targetSize.width / imageRatio;
}
} else if ([resizeMode isEqualToString:@"cover"]) {
if (imageRatio <= targetRatio) {
newWidth = targetSize.width;
newHeight = targetSize.width / imageRatio;
} else {
newWidth = targetSize.height * imageRatio;
newHeight = targetSize.height;
}
} // else assume we're stretching the image
// prevent upscaling
newWidth = MIN(newWidth, image.size.width);
newHeight = MIN(newHeight, image.size.height);
// perform the scaling @1x because targetSize is in actual pixel width/height
UIGraphicsBeginImageContextWithOptions(targetSize, NO, 1.0f);
[image drawInRect:CGRectMake(0.f, 0.f, newWidth, newHeight)];
UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return scaledImage;
}
@end

View File

@@ -13,7 +13,7 @@
@class ALAssetsLibrary;
typedef void (^RCTImageLoaderProgressBlock)(int64_t written, int64_t total);
typedef void (^RCTImageLoaderProgressBlock)(int64_t progress, int64_t total);
typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, id image /* UIImage or CAAnimation */);
typedef void (^RCTImageLoaderCancellationBlock)(void);
@@ -34,8 +34,8 @@ typedef void (^RCTImageLoaderCancellationBlock)(void);
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progress
completionBlock:(RCTImageLoaderCompletionBlock)completion;
progressBlock:(RCTImageLoaderProgressBlock)progressBlock
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock;
/**
* Is the specified image tag an asset library image?

View File

@@ -61,7 +61,7 @@ RCT_EXPORT_MODULE()
{
return [self loadImageWithTag:imageTag
size:CGSizeZero
scale:0
scale:1
resizeMode:UIViewContentModeScaleToFill
progressBlock:nil
completionBlock:callback];
@@ -123,11 +123,11 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation,
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progress
completionBlock:(RCTImageLoaderCompletionBlock)completion
progressBlock:(RCTImageLoaderProgressBlock)progressBlock
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
{
if ([imageTag hasPrefix:@"assets-library://"]) {
[[self assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) {
[[self assetsLibrary] assetForURL:[RCTConvert NSURL:imageTag] resultBlock:^(ALAsset *asset) {
if (asset) {
// ALAssetLibrary API is async and will be multi-threaded. Loading a few full
// resolution images at once will spike the memory up to store the image data,
@@ -151,18 +151,18 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation,
image = RCTScaledImageForAsset(representation, size, scale, resizeMode, &error);
}
RCTDispatchCallbackOnMainQueue(completion, error, image);
RCTDispatchCallbackOnMainQueue(completionBlock, error, image);
}
});
} else {
NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@ with no error message.", imageTag];
NSError *error = RCTErrorWithMessage(errorText);
RCTDispatchCallbackOnMainQueue(completion, error, nil);
RCTDispatchCallbackOnMainQueue(completionBlock, error, nil);
}
} failureBlock:^(NSError *loadError) {
NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@.\niOS Error: %@", imageTag, loadError];
NSError *error = RCTErrorWithMessage(errorText);
RCTDispatchCallbackOnMainQueue(completion, error, nil);
RCTDispatchCallbackOnMainQueue(completionBlock, error, nil);
}];
return ^{};
} else if ([imageTag hasPrefix:@"ph://"]) {
@@ -175,7 +175,7 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation,
if (results.count == 0) {
NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", phAssetID];
NSError *error = RCTErrorWithMessage(errorText);
RCTDispatchCallbackOnMainQueue(completion, error, nil);
RCTDispatchCallbackOnMainQueue(completionBlock, error, nil);
return ^{};
}
@@ -200,65 +200,54 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation,
}
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:imageOptions resultHandler:^(UIImage *result, NSDictionary *info) {
if (result) {
RCTDispatchCallbackOnMainQueue(completion, nil, result);
RCTDispatchCallbackOnMainQueue(completionBlock, nil, result);
} else {
NSString *errorText = [NSString stringWithFormat:@"Failed to load PHAsset with local identifier %@ with no error message.", phAssetID];
NSError *error = RCTErrorWithMessage(errorText);
RCTDispatchCallbackOnMainQueue(completion, error, nil);
RCTDispatchCallbackOnMainQueue(completionBlock, error, nil);
return;
}
}];
return ^{};
} else if ([imageTag hasPrefix:@"http"]) {
NSURL *url = [NSURL URLWithString:imageTag];
NSURL *url = [RCTConvert NSURL:imageTag];
if (!url) {
NSString *errorMessage = [NSString stringWithFormat:@"Invalid URL: %@", imageTag];
RCTDispatchCallbackOnMainQueue(completion, RCTErrorWithMessage(errorMessage), nil);
RCTDispatchCallbackOnMainQueue(completionBlock, RCTErrorWithMessage(errorMessage), nil);
return ^{};
}
if ([imageTag.lowercaseString hasSuffix:@".gif"]) {
return [[RCTImageDownloader sharedInstance] downloadDataForURL:url progressBlock:progress block:^(NSData *data, NSError *error) {
id image = RCTGIFImageWithFileURL([RCTConvert NSURL:imageTag]);
if (!image && !error) {
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load GIF image: %@", imageTag];
error = RCTErrorWithMessage(errorMessage);
}
RCTDispatchCallbackOnMainQueue(completion, error, image);
}];
} else {
return [[RCTImageDownloader sharedInstance] downloadImageForURL:url size:size scale:scale resizeMode:resizeMode tintColor:nil backgroundColor:nil progressBlock:progress block:^(UIImage *image, NSError *error) {
RCTDispatchCallbackOnMainQueue(completion, error, image);
}];
}
return [_bridge.imageDownloader downloadImageForURL:url size:size scale:scale resizeMode:resizeMode progressBlock:progressBlock completionBlock:^(NSError *error, id image) {
RCTDispatchCallbackOnMainQueue(completionBlock, error, image);
}];
} else if ([imageTag hasPrefix:@"rct-image-store://"]) {
[_bridge.imageStoreManager getImageForTag:imageTag withBlock:^(UIImage *image) {
if (image) {
RCTDispatchCallbackOnMainQueue(completion, nil, image);
RCTDispatchCallbackOnMainQueue(completionBlock, nil, image);
} else {
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load image from image store: %@", imageTag];
NSError *error = RCTErrorWithMessage(errorMessage);
RCTDispatchCallbackOnMainQueue(completion, error, nil);
RCTDispatchCallbackOnMainQueue(completionBlock, error, nil);
}
}];
return ^{};
} else if ([imageTag.lowercaseString hasSuffix:@".gif"]) {
id image = RCTGIFImageWithFileURL([RCTConvert NSURL:imageTag]);
if (image) {
RCTDispatchCallbackOnMainQueue(completion, nil, image);
RCTDispatchCallbackOnMainQueue(completionBlock, nil, image);
} else {
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load GIF image: %@", imageTag];
NSError *error = RCTErrorWithMessage(errorMessage);
RCTDispatchCallbackOnMainQueue(completion, error, nil);
RCTDispatchCallbackOnMainQueue(completionBlock, error, nil);
}
return ^{};
} else {
UIImage *image = [RCTConvert UIImage:imageTag];
if (image) {
RCTDispatchCallbackOnMainQueue(completion, nil, image);
RCTDispatchCallbackOnMainQueue(completionBlock, nil, image);
} else {
NSString *errorMessage = [NSString stringWithFormat:@"Unrecognized tag protocol: %@", imageTag];
NSError *error = RCTErrorWithMessage(errorMessage);
RCTDispatchCallbackOnMainQueue(completion, error, nil);
RCTDispatchCallbackOnMainQueue(completionBlock, error, nil);
}
return ^{};
}

View File

@@ -51,10 +51,20 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
sourceSize.width *= scale;
sourceSize.height *= scale;
// Calculate aspect ratios if needed (don't bother if resizeMode == stretch)
CGFloat aspect = 0.0, targetAspect = 0.0;
CGFloat aspect = sourceSize.width / sourceSize.height;
// If only one dimension in destSize is non-zero (for example, an Image
// with `flex: 1` whose height is indeterminate), calculate the unknown
// dimension based on the aspect ratio of sourceSize
if (destSize.width == 0) {
destSize.width = destSize.height * aspect;
}
if (destSize.height == 0) {
destSize.height = destSize.width / aspect;
}
// Calculate target aspect ratio if needed (don't bother if resizeMode == stretch)
CGFloat targetAspect = 0.0;
if (resizeMode != UIViewContentModeScaleToFill) {
aspect = sourceSize.width / sourceSize.height;
targetAspect = destSize.width / destSize.height;
if (aspect == targetAspect) {
resizeMode = UIViewContentModeScaleToFill;

View File

@@ -69,8 +69,9 @@ RCT_NOT_IMPLEMENTED(-init)
- (void)setImage:(UIImage *)image
{
image = image ?: _defaultImage;
if (image != super.image) {
super.image = image ?: _defaultImage;
super.image = image;
[self _updateImage];
}
}
@@ -110,11 +111,11 @@ RCT_NOT_IMPLEMENTED(-init)
RCTImageLoaderProgressBlock progressHandler = nil;
if (_onProgress) {
progressHandler = ^(int64_t loaded, int64_t total) {
progressHandler = ^(int64_t loaded, int64_t total) {
NSDictionary *event = @{
@"target": self.reactTag,
@"loaded": @(loaded),
@"total": @(total),
@"loaded": @((double)loaded),
@"total": @((double)total),
};
[_bridge.eventDispatcher sendInputEventWithName:@"progress" body:event];
};
@@ -177,21 +178,14 @@ RCT_NOT_IMPLEMENTED(-init)
}
}
- (void)willMoveToSuperview:(UIView *)newSuperview
- (void)didMoveToWindow
{
[super willMoveToSuperview:newSuperview];
[super didMoveToWindow];
if (!newSuperview) {
if (!self.window) {
[self.layer removeAnimationForKey:@"contents"];
self.image = nil;
}
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
if (self.superview && self.src) {
} else if (self.src) {
[self reloadImage];
}
}

View File

@@ -43,15 +43,15 @@ RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
}
}
- (NSDictionary *)customDirectEventTypes
- (NSArray *)customDirectEventTypes
{
return @{
@"loadStart": @{ @"registrationName": @"onLoadStart" },
@"progress": @{ @"registrationName": @"onProgress" },
@"error": @{ @"registrationName": @"onError" },
@"load": @{ @"registrationName": @"onLoad" },
@"loadEnd": @{ @"registrationName": @"onLoadEnd" },
};
return @[
@"loadStart",
@"progress",
@"error",
@"load",
@"loadEnd",
];
}
@end

View File

@@ -26,6 +26,7 @@
require('RCTDebugComponentOwnership');
require('RCTDeviceEventEmitter');
require('PerformanceLogger');
require('regenerator/runtime');
if (typeof GLOBAL === 'undefined') {
GLOBAL = this;
@@ -89,7 +90,7 @@ function setUpAlert() {
message: '' + text,
buttons: [{'cancel': 'OK'}],
};
RCTAlertManager.alertWithArgs(alertOpts, null);
RCTAlertManager.alertWithArgs(alertOpts, function () {});
};
}
}
@@ -128,6 +129,10 @@ function setupProfile() {
require('BridgeProfiling').swizzleReactPerf();
}
function setUpProcessEnv() {
GLOBAL.process = {env: {NODE_ENV: __DEV__ ? 'development' : 'production'}};
}
setUpRedBoxErrorHandler();
setUpTimers();
setUpAlert();
@@ -137,3 +142,4 @@ setUpRedBoxConsoleErrorHandler();
setUpGeolocation();
setUpWebSockets();
setupProfile();
setUpProcessEnv();

View File

@@ -47,7 +47,7 @@ var JSTimers = {
return func.apply(undefined, args);
};
JSTimersExecution.types[freeIndex] = JSTimersExecution.Type.setTimeout;
RCTTiming.createTimer(newID, duration, Date.now(), /** recurring */ false);
RCTTiming.createTimer(newID, duration || 0, Date.now(), /** recurring */ false);
return newID;
},
@@ -63,7 +63,7 @@ var JSTimers = {
return func.apply(undefined, args);
};
JSTimersExecution.types[freeIndex] = JSTimersExecution.Type.setInterval;
RCTTiming.createTimer(newID, duration, Date.now(), /** recurring */ true);
RCTTiming.createTimer(newID, duration || 0, Date.now(), /** recurring */ true);
return newID;
},

48
Libraries/Modal/Modal.js Normal file
View File

@@ -0,0 +1,48 @@
/**
* 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 Modal
* @flow
*/
'use strict';
var React = require('React');
var StyleSheet = require('StyleSheet');
var View = require('View');
var requireNativeComponent = require('requireNativeComponent');
var RCTModalHostView = requireNativeComponent('RCTModalHostView', null);
class Modal extends React.Component {
render(): ?ReactElement {
if (this.props.visible === false) {
return null;
}
return (
<RCTModalHostView animated={this.props.animated} style={styles.modal}>
<View style={styles.container}>
{this.props.children}
</View>
</RCTModalHostView>
);
}
}
var styles = StyleSheet.create({
modal: {
position: 'absolute',
},
container: {
left: 0,
position: 'absolute',
top: 0,
}
});
module.exports = Modal;

View File

@@ -12,15 +12,42 @@
'use strict';
type FormDataValue = any;
type FormDataPart = [string, FormDataValue];
type FormDataNameValuePair = [string, FormDataValue];
type Headers = {[name: string]: string};
type FormDataPart = {
string: string;
headers: Headers;
} | {
uri: string;
headers: Headers;
name?: string;
type?: string;
};
/**
* Polyfill for XMLHttpRequest2 FormData API, allowing multipart POST requests
* with mixed data (string, native files) to be submitted via XMLHttpRequest.
*
* Example:
*
* var photo = {
* uri: uriFromCameraRoll,
* type: 'image/jpeg',
* name: 'photo.jpg',
* };
*
* var body = new FormData();
* body.append('authToken', 'secret');
* body.append('photo', photo);
* body.append('title', 'A beautiful photo!');
*
* xhr.open('POST', serverURL);
* xhr.send(body);
*/
class FormData {
_parts: Array<FormDataPart>;
_partsByKey: {[key: string]: FormDataPart};
_parts: Array<FormDataNameValuePair>;
_partsByKey: {[key: string]: FormDataNameValuePair};
constructor() {
this._parts = [];
@@ -42,24 +69,25 @@ class FormData {
this._partsByKey[key] = parts;
}
getParts(): Array<FormDataValue> {
getParts(): Array<FormDataPart> {
return this._parts.map(([name, value]) => {
if (typeof value === 'string') {
return {
string: value,
headers: {
'content-disposition': 'form-data; name="' + name + '"',
},
};
}
var contentDisposition = 'form-data; name="' + name + '"';
if (typeof value.name === 'string') {
contentDisposition += '; filename="' + value.name + '"';
var headers: Headers = {'content-disposition': contentDisposition};
if (typeof value === 'string') {
return {string: value, headers};
}
return {
...value,
headers: {'content-disposition': contentDisposition},
};
// The body part is a "blob", which in React Native just means
// an object with a `uri` attribute. Optionally, it can also
// have a `name` and `type` attribute to specify filename and
// content type (cf. web Blob interface.)
if (typeof value.name === 'string') {
headers['content-disposition'] += '; filename="' + value.name + '"';
}
if (typeof value.type === 'string') {
headers['content-type'] = value.type;
}
return {...value, headers};
});
}
}

View File

@@ -15,7 +15,7 @@
typedef void (^RCTURLRequestCompletionBlock)(NSURLResponse *response, NSData *data, NSError *error);
typedef void (^RCTURLRequestCancellationBlock)(void);
typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data);
typedef void (^RCTURLRequestProgressBlock)(double progress, double total);
typedef void (^RCTURLRequestProgressBlock)(int64_t progress, int64_t total);
typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
@interface RCTDownloadTask : NSObject <RCTURLRequestDelegate>

View File

@@ -61,42 +61,66 @@ RCT_NOT_IMPLEMENTED(-init)
[self invalidate];
}
- (BOOL)validateRequestToken:(id)requestToken
{
if (![requestToken isEqual:_requestToken]) {
if (RCT_DEBUG) {
RCTAssert([requestToken isEqual:_requestToken],
@"Unrecognized request token: %@", requestToken);
}
if (_completionBlock) {
_completionBlock(_response, _data, [NSError errorWithDomain:RCTErrorDomain code:0
userInfo:@{NSLocalizedDescriptionKey: @"Unrecognized request token."}]);
[self invalidate];
}
return NO;
}
return YES;
}
- (void)URLRequest:(id)requestToken didSendDataWithProgress:(int64_t)bytesSent
{
RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken);
if (_uploadProgressBlock) {
_uploadProgressBlock(bytesSent, _request.HTTPBody.length);
if ([self validateRequestToken:requestToken]) {
if (_uploadProgressBlock) {
_uploadProgressBlock(bytesSent, _request.HTTPBody.length);
}
}
}
- (void)URLRequest:(id)requestToken didReceiveResponse:(NSURLResponse *)response
{
RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken);
_response = response;
if (_responseBlock) {
_responseBlock(response);
if ([self validateRequestToken:requestToken]) {
_response = response;
if (_responseBlock) {
_responseBlock(response);
}
}
}
- (void)URLRequest:(id)requestToken didReceiveData:(NSData *)data
{
RCTAssert([requestToken isEqual:_requestToken], @"Unrecognized request token: %@", requestToken);
if (!_data) {
_data = [[NSMutableData alloc] init];
}
[_data appendData:data];
if (_incrementalDataBlock) {
_incrementalDataBlock(data);
}
if (_downloadProgressBlock && _response.expectedContentLength > 0) {
_downloadProgressBlock(_data.length, _response.expectedContentLength);
if ([self validateRequestToken:requestToken]) {
if (!_data) {
_data = [[NSMutableData alloc] init];
}
[_data appendData:data];
if (_incrementalDataBlock) {
_incrementalDataBlock(data);
}
if (_downloadProgressBlock && _response.expectedContentLength > 0) {
_downloadProgressBlock(_data.length, _response.expectedContentLength);
}
}
}
- (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error
{
_completionBlock(_response, _data, error);
[self invalidate];
if ([self validateRequestToken:requestToken]) {
if (_completionBlock) {
_completionBlock(_response, _data, error);
[self invalidate];
}
}
}
@end

View File

@@ -9,8 +9,18 @@
#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"
#import "RCTBridge.h"
#import "RCTDownloadTask.h"
@interface RCTNetworking : NSObject <RCTBridgeModule>
- (RCTDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
completionBlock:(RCTURLRequestCompletionBlock)completionBlock;
@end
@interface RCTBridge (RCTNetworking)
@property (nonatomic, readonly) RCTNetworking *networking;
@end

View File

@@ -233,13 +233,8 @@ RCT_EXPORT_MODULE()
NSURLRequest *request = [RCTConvert NSURLRequest:query[@"uri"]];
if (request) {
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
if (!handler) {
return callback(nil, nil);
}
__block RCTURLRequestCancellationBlock cancellationBlock = nil;
RCTDownloadTask *task = [[RCTDownloadTask alloc] initWithRequest:request handler:handler completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
RCTDownloadTask *task = [self downloadTaskWithRequest:request completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
cancellationBlock = callback(error, data ? @{@"body": data, @"contentType": RCTNullIfNil(response.MIMEType)} : nil);
}];
@@ -291,16 +286,11 @@ RCT_EXPORT_MODULE()
incrementalUpdates:(BOOL)incrementalUpdates
responseSender:(RCTResponseSenderBlock)responseSender
{
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
if (!handler) {
return;
}
__block RCTDownloadTask *task;
RCTURLRequestProgressBlock uploadProgressBlock = ^(double progress, double total) {
RCTURLRequestProgressBlock uploadProgressBlock = ^(int64_t progress, int64_t total) {
dispatch_async(_methodQueue, ^{
NSArray *responseJSON = @[task.requestID, @(progress), @(total)];
NSArray *responseJSON = @[task.requestID, @((double)progress), @((double)total)];
[_bridge.eventDispatcher sendDeviceEventWithName:@"didSendNetworkData" body:responseJSON];
});
};
@@ -345,10 +335,7 @@ RCT_EXPORT_MODULE()
});
};
task = [[RCTDownloadTask alloc] initWithRequest:request
handler:handler
completionBlock:completionBlock];
task = [self downloadTaskWithRequest:request completionBlock:completionBlock];
task.incrementalDataBlock = incrementalDataBlock;
task.responseBlock = responseBlock;
task.uploadProgressBlock = uploadProgressBlock;
@@ -359,6 +346,21 @@ RCT_EXPORT_MODULE()
}
}
#pragma mark - Public API
- (RCTDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
completionBlock:(RCTURLRequestCompletionBlock)completionBlock
{
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
if (!handler) {
return nil;
}
return [[RCTDownloadTask alloc] initWithRequest:request
handler:handler
completionBlock:completionBlock];
}
#pragma mark - JS API
RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query
@@ -376,10 +378,19 @@ RCT_EXPORT_METHOD(sendRequest:(NSDictionary *)query
}];
}
RCT_EXPORT_METHOD(cancelRequest:(NSNumber *)requestID)
RCT_EXPORT_METHOD(cancelRequest:(nonnull NSNumber *)requestID)
{
[_tasksByRequestID[requestID] cancel];
[_tasksByRequestID removeObjectForKey:requestID];
}
@end
@implementation RCTBridge (RCTNetworking)
- (RCTNetworking *)networking
{
return self.modules[RCTBridgeModuleNameForClass([RCTNetworking class])];
}
@end

View File

@@ -52,7 +52,7 @@ var resolveDefaultProps = function(element) {
};
// Experimental optimized element creation
var augmentElement = function(element: ReactElement) {
var augmentElement = function(element: ReactElement): ReactElement {
if (__DEV__) {
invariant(
false,

View File

@@ -121,7 +121,8 @@ var ReactNativeEventEmitter = merge(ReactEventEmitterMixin, {
topLevelType,
rootNodeID,
rootNodeID,
nativeEvent
nativeEvent,
nativeEvent.target
);
},

View File

@@ -253,12 +253,12 @@ var ReactNativeMount = {
RCTUIManager.removeSubviewsFromContainerWithID(containerTag);
},
getNode: function<T>(id: T): T {
return id;
getNode: function(rootNodeID: string): number {
return ReactNativeTagHandles.rootNodeIDToTag[rootNodeID];
},
getID: function<T>(id: T): T {
return id;
getID: function(nativeTag: number): string {
return ReactNativeTagHandles.tagToRootNodeID[nativeTag];
}
};

View File

@@ -19,11 +19,15 @@ ReactNativeViewAttributes.UIView = {
pointerEvents: true,
accessible: true,
accessibilityLabel: true,
accessibilityComponentType: true,
accessibilityLiveRegion: true,
accessibilityTraits: true,
testID: true,
shouldRasterizeIOS: true,
onLayout: true,
onAccessibilityTap: true,
onMagicTap: true,
collapsable: true,
};
ReactNativeViewAttributes.RCTView = merge(

View File

@@ -9,8 +9,32 @@
#import "RCTShadowRawText.h"
#import "RCTUIManager.h"
@implementation RCTShadowRawText
- (instancetype)init
{
if ((self = [super init])) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeMultiplierDidChange:)
name:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)contentSizeMultiplierDidChange:(NSNotification *)note
{
[self dirtyLayout];
[self dirtyText];
}
- (void)setText:(NSString *)text
{
if (_text != text) {

View File

@@ -30,6 +30,8 @@ extern NSString *const RCTReactTagAttributeName;
@property (nonatomic, strong) UIColor *textDecorationColor;
@property (nonatomic, assign) NSUnderlineStyle textDecorationStyle;
@property (nonatomic, assign) RCTTextDecorationLineType textDecorationLine;
@property (nonatomic, assign) CGFloat fontSizeMultiplier;
@property (nonatomic, assign) BOOL allowFontScaling;
- (void)recomputeText;

View File

@@ -9,6 +9,9 @@
#import "RCTShadowText.h"
#import "RCTAccessibilityManager.h"
#import "RCTUIManager.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTShadowRawText.h"
@@ -51,16 +54,31 @@ static css_dim_t RCTMeasure(void *context, float width)
_letterSpacing = NAN;
_isHighlighted = NO;
_textDecorationStyle = NSUnderlineStyleSingle;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeMultiplierDidChange:)
name:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSString *)description
{
NSString *superDescription = super.description;
return [[superDescription substringToIndex:superDescription.length - 1] stringByAppendingFormat:@"; text: %@>", [self attributedString].string];
}
- (void)contentSizeMultiplierDidChange:(NSNotification *)note
{
[self dirtyLayout];
[self dirtyText];
}
- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks
parentProperties:(NSDictionary *)parentProperties
{
@@ -190,7 +208,9 @@ static css_dim_t RCTMeasure(void *context, float width)
[self _addAttribute:NSBackgroundColorAttributeName withValue:self.backgroundColor toAttributedString:attributedString];
}
UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily size:fontSize weight:fontWeight style:fontStyle];
UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily
size:fontSize weight:fontWeight style:fontStyle
scaleMultiplier:(_allowFontScaling && _fontSizeMultiplier > 0.0 ? _fontSizeMultiplier : 1.0)];
[self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString];
[self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString];
[self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString];
@@ -232,8 +252,9 @@ static css_dim_t RCTMeasure(void *context, float width)
[attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, [attributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value) {
NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value;
if ([paragraphStyle maximumLineHeight] > _lineHeight) {
self.lineHeight = [paragraphStyle maximumLineHeight];
CGFloat maximumLineHeight = round([paragraphStyle maximumLineHeight] / self.fontSizeMultiplier);
if (maximumLineHeight > self.lineHeight) {
self.lineHeight = maximumLineHeight;
}
hasParagraphStyle = YES;
}
@@ -247,8 +268,9 @@ static css_dim_t RCTMeasure(void *context, float width)
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = _textAlign;
paragraphStyle.baseWritingDirection = _writingDirection;
paragraphStyle.minimumLineHeight = _lineHeight;
paragraphStyle.maximumLineHeight = _lineHeight;
CGFloat lineHeight = round(_lineHeight * self.fontSizeMultiplier);
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.maximumLineHeight = lineHeight;
[attributedString addAttribute:NSParagraphStyleAttributeName
value:paragraphStyle
range:(NSRange){0, attributedString.length}];
@@ -321,4 +343,26 @@ RCT_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, RCTTextDecorationLine
RCT_TEXT_PROPERTY(TextDecorationStyle, _textDecorationStyle, NSUnderlineStyle);
RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection)
- (void)setAllowFontScaling:(BOOL)allowFontScaling
{
_allowFontScaling = allowFontScaling;
for (RCTShadowView *child in [self reactSubviews]) {
if ([child isKindOfClass:[RCTShadowText class]]) {
[(RCTShadowText *)child setAllowFontScaling:allowFontScaling];
}
}
[self dirtyText];
}
- (void)setFontSizeMultiplier:(CGFloat)fontSizeMultiplier
{
_fontSizeMultiplier = fontSizeMultiplier;
for (RCTShadowView *child in [self reactSubviews]) {
if ([child isKindOfClass:[RCTShadowText class]]) {
[(RCTShadowText *)child setFontSizeMultiplier:fontSizeMultiplier];
}
}
[self dirtyText];
}
@end

View File

@@ -62,7 +62,7 @@
[_reactSubviews removeObject:subview];
}
- (NSMutableArray *)reactSubviews
- (NSArray *)reactSubviews
{
return _reactSubviews;
}
@@ -84,17 +84,20 @@
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin];
__block UIBezierPath *highlightPath = nil;
[layoutManager.textStorage enumerateAttributesInRange:glyphRange options:0 usingBlock:^(NSDictionary *attrs, NSRange range, __unused BOOL *stop){
if ([attrs[RCTIsHighlightedAttributeName] boolValue]) {
[layoutManager enumerateEnclosingRectsForGlyphRange:range withinSelectedGlyphRange:range inTextContainer:textContainer usingBlock:^(CGRect r, __unused BOOL *s){
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(r, -2, -2) cornerRadius:2];
if (highlightPath) {
[highlightPath appendPath:path];
} else {
highlightPath = path;
}
}];
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
[layoutManager.textStorage enumerateAttribute:RCTIsHighlightedAttributeName inRange:characterRange options:0 usingBlock:^(NSNumber *value, NSRange range, BOOL *_) {
if (!value.boolValue) {
return;
}
[layoutManager enumerateEnclosingRectsForGlyphRange:range withinSelectedGlyphRange:range inTextContainer:textContainer usingBlock:^(CGRect enclosingRect, __unused BOOL *__) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) cornerRadius:2];
if (highlightPath) {
[highlightPath appendPath:path];
} else {
highlightPath = path;
}
}];
}];
if (highlightPath) {
@@ -130,6 +133,22 @@
return reactTag;
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (!self.window) {
self.layer.contents = nil;
if (_highlightLayer) {
[_highlightLayer removeFromSuperlayer];
_highlightLayer = nil;
}
} else if (_textStorage.length) {
[self setNeedsDisplay];
}
}
#pragma mark - Accessibility
- (NSString *)accessibilityLabel

View File

@@ -11,7 +11,7 @@
@class RCTEventDispatcher;
@interface RCTTextField : UITextField<UITextFieldDelegate>
@interface RCTTextField : UITextField
@property (nonatomic, assign) BOOL caretHidden;
@property (nonatomic, assign) BOOL autoCorrect;
@@ -22,5 +22,6 @@
@property (nonatomic, strong) NSNumber *maxLength;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
- (void)textFieldDidChange;
@end

View File

@@ -27,12 +27,11 @@
if ((self = [super initWithFrame:CGRectZero])) {
RCTAssert(eventDispatcher, @"eventDispatcher is a required parameter");
_eventDispatcher = eventDispatcher;
[self addTarget:self action:@selector(_textFieldDidChange) forControlEvents:UIControlEventEditingChanged];
[self addTarget:self action:@selector(_textFieldBeginEditing) forControlEvents:UIControlEventEditingDidBegin];
[self addTarget:self action:@selector(_textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd];
[self addTarget:self action:@selector(_textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit];
[self addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged];
[self addTarget:self action:@selector(textFieldBeginEditing) forControlEvents:UIControlEventEditingDidBegin];
[self addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd];
[self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit];
_reactSubviews = [[NSMutableArray alloc] init];
self.delegate = self;
}
return self;
}
@@ -40,31 +39,6 @@
RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame)
RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (_maxLength == nil || [string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return
return YES;
}
NSUInteger allowedLength = _maxLength.integerValue - textField.text.length + range.length;
if (string.length > allowedLength) {
if (string.length > 1) {
// Truncate the input string so the result is exactly maxLength
NSString *limitedString = [string substringToIndex:allowedLength];
NSMutableString *newString = textField.text.mutableCopy;
[newString replaceCharactersInRange:range withString:limitedString];
textField.text = newString;
// Collapse selection at end of insert to match normal paste behavior
UITextPosition *insertEnd = [textField positionFromPosition:textField.beginningOfDocument
offset:(range.location + allowedLength)];
textField.selectedTextRange = [textField textRangeFromPosition:insertEnd toPosition:insertEnd];
[self _textFieldDidChange];
}
return NO;
} else {
return YES;
}
}
- (void)setText:(NSString *)text
{
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
@@ -73,7 +47,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
[super setText:text];
self.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
RCTLogWarn(@"Native TextInput(%@) is %ld events ahead of JS - try to make your JS faster.", self.text, (long)eventLag);
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
}
}
@@ -154,7 +128,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
return self.autocorrectionType == UITextAutocorrectionTypeYes;
}
- (void)_textFieldDidChange
- (void)textFieldDidChange
{
_nativeEventCount++;
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange
@@ -163,14 +137,14 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
eventCount:_nativeEventCount];
}
- (void)_textFieldEndEditing
- (void)textFieldEndEditing
{
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
reactTag:self.reactTag
text:self.text
eventCount:_nativeEventCount];
}
- (void)_textFieldSubmitEditing
- (void)textFieldSubmitEditing
{
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit
reactTag:self.reactTag
@@ -178,7 +152,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
eventCount:_nativeEventCount];
}
- (void)_textFieldBeginEditing
- (void)textFieldBeginEditing
{
if (_selectTextOnFocus) {
dispatch_async(dispatch_get_main_queue(), ^{

View File

@@ -14,13 +14,44 @@
#import "RCTSparseArray.h"
#import "RCTTextField.h"
@interface RCTTextFieldManager() <UITextFieldDelegate>
@end
@implementation RCTTextFieldManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[RCTTextField alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
RCTTextField *textField = [[RCTTextField alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
textField.delegate = self;
return textField;
}
- (BOOL)textField:(RCTTextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (textField.maxLength == nil || [string isEqualToString:@"\n"]) { // Make sure forms can be submitted via return
return YES;
}
NSUInteger allowedLength = textField.maxLength.integerValue - textField.text.length + range.length;
if (string.length > allowedLength) {
if (string.length > 1) {
// Truncate the input string so the result is exactly maxLength
NSString *limitedString = [string substringToIndex:allowedLength];
NSMutableString *newString = textField.text.mutableCopy;
[newString replaceCharactersInRange:range withString:limitedString];
textField.text = newString;
// Collapse selection at end of insert to match normal paste behavior
UITextPosition *insertEnd = [textField positionFromPosition:textField.beginningOfDocument
offset:(range.location + allowedLength)];
textField.selectedTextRange = [textField textRangeFromPosition:insertEnd toPosition:insertEnd];
[textField textFieldDidChange];
}
return NO;
} else {
return YES;
}
}
RCT_EXPORT_VIEW_PROPERTY(caretHidden, BOOL)

View File

@@ -9,6 +9,7 @@
#import "RCTTextManager.h"
#import "RCTAccessibilityManager.h"
#import "RCTAssert.h"
#import "RCTConvert.h"
#import "RCTLog.h"
@@ -49,6 +50,7 @@ RCT_EXPORT_SHADOW_PROPERTY(textDecorationStyle, NSUnderlineStyle)
RCT_EXPORT_SHADOW_PROPERTY(textDecorationColor, UIColor)
RCT_EXPORT_SHADOW_PROPERTY(textDecorationLine, RCTTextDecorationLineType)
RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection)
RCT_EXPORT_SHADOW_PROPERTY(allowFontScaling, BOOL)
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry
{
@@ -69,6 +71,7 @@ RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection)
RCTAssert([shadowView isTextDirty], @"Don't process any nodes that don't have dirty text");
if ([shadowView isKindOfClass:[RCTShadowText class]]) {
[(RCTShadowText *)shadowView setFontSizeMultiplier:self.bridge.accessibilityManager.multiplier];
[(RCTShadowText *)shadowView recomputeText];
} else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) {
RCTLogError(@"Raw text cannot be used outside of a <Text> tag. Not rendering string: '%@'",

View File

@@ -159,7 +159,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder)
[self _setPlaceholderVisibility];
_textView.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
RCTLogWarn(@"Native TextInput(%@) is %ld events ahead of JS - try to make your JS faster.", self.text, (long)eventLag);
RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag);
}
}

View File

@@ -30,6 +30,7 @@ var viewConfig = {
validAttributes: merge(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
numberOfLines: true,
allowFontScaling: true,
}),
uiViewClassName: 'RCTText',
};
@@ -99,15 +100,25 @@ var Text = React.createClass({
* Used to locate this view in end-to-end tests.
*/
testID: React.PropTypes.string,
/**
* Specifies should fonts scale to respect Text Size accessibility setting on iOS.
*/
allowFontScaling: React.PropTypes.bool,
},
viewConfig: viewConfig,
getInitialState: function() {
getInitialState: function(): Object {
return merge(this.touchableGetInitialState(), {
isHighlighted: false,
});
},
getDefaultProps: function(): Object {
return {
allowFontScaling: true,
};
},
onStartShouldSetResponder: function(): bool {
var shouldSetFromProps = this.props.onStartShouldSetResponder &&

View File

@@ -84,7 +84,9 @@ var PerformanceLogger = {
logTimespans() {
for (var key in timespans) {
console.log(key + ': ' + timespans[key].totalTime + 'ms');
if (timespans[key].totalTime) {
console.log(key + ': ' + timespans[key].totalTime + 'ms');
}
}
},

View File

@@ -13,7 +13,7 @@
/**
* Faster version of `merge` that doesn't check its arguments and
* also merges prototye inherited properties.
* also merges prototype inherited properties.
*
* @param {object} one Any non-null object.
* @param {object} two Any non-null object.

View File

@@ -13,6 +13,7 @@
#import "RCTWebSocketExecutor.h"
#import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTSparseArray.h"
#import "RCTUtils.h"
@@ -38,7 +39,7 @@ RCT_EXPORT_MODULE()
- (instancetype)init
{
return [self initWithURL:[NSURL URLWithString:@"http://localhost:8081/debugger-proxy"]];
return [self initWithURL:[RCTConvert NSURL:@"http://localhost:8081/debugger-proxy"]];
}
- (instancetype)initWithURL:(NSURL *)URL
@@ -149,7 +150,11 @@ RCT_EXPORT_MODULE()
- (void)executeApplicationScript:(NSString *)script sourceURL:(NSURL *)URL onComplete:(RCTJavaScriptCompleteBlock)onComplete
{
NSDictionary *message = @{@"method": @"executeApplicationScript", @"url": [URL absoluteString], @"inject": _injectedObjects};
NSDictionary *message = @{
@"method": @"executeApplicationScript",
@"url": RCTNullIfNil([URL absoluteString]),
@"inject": _injectedObjects,
};
[self sendMessage:message waitForReply:^(NSError *error, NSDictionary *reply) {
onComplete(error);
}];

View File

@@ -58,7 +58,7 @@ RCT_EXPORT_MODULE()
}
}
RCT_EXPORT_METHOD(connect:(NSURL *)URL socketID:(NSNumber *)socketID)
RCT_EXPORT_METHOD(connect:(NSURL *)URL socketID:(nonnull NSNumber *)socketID)
{
RCTSRWebSocket *webSocket = [[RCTSRWebSocket alloc] initWithURL:URL];
webSocket.delegate = self;
@@ -67,12 +67,12 @@ RCT_EXPORT_METHOD(connect:(NSURL *)URL socketID:(NSNumber *)socketID)
[webSocket open];
}
RCT_EXPORT_METHOD(send:(NSString *)message socketID:(NSNumber *)socketID)
RCT_EXPORT_METHOD(send:(NSString *)message socketID:(nonnull NSNumber *)socketID)
{
[_sockets[socketID] send:message];
}
RCT_EXPORT_METHOD(close:(NSNumber *)socketID)
RCT_EXPORT_METHOD(close:(nonnull NSNumber *)socketID)
{
[_sockets[socketID] close];
_sockets[socketID] = nil;

View File

@@ -24,6 +24,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
Image: require('Image'),
ListView: require('ListView'),
MapView: require('MapView'),
Modal: require('Modal'),
Navigator: require('Navigator'),
NavigatorIOS: require('NavigatorIOS'),
PickerIOS: require('PickerIOS'),

View File

@@ -1,17 +1,10 @@
/**
* Copyright 2013-2014 Facebook, Inc.
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* 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 ResponderEventPlugin
*/
@@ -21,7 +14,6 @@
var EventConstants = require('EventConstants');
var EventPluginUtils = require('EventPluginUtils');
var EventPropagators = require('EventPropagators');
var NodeHandle = require('NodeHandle');
var ReactInstanceHandles = require('ReactInstanceHandles');
var ResponderSyntheticEvent = require('ResponderSyntheticEvent');
var ResponderTouchHistoryStore = require('ResponderTouchHistoryStore');
@@ -75,8 +67,8 @@ var eventTypes = {
startShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onStartShouldSetResponder: null}),
captured: keyOf({onStartShouldSetResponderCapture: null})
}
captured: keyOf({onStartShouldSetResponderCapture: null}),
},
},
/**
@@ -91,8 +83,8 @@ var eventTypes = {
scrollShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onScrollShouldSetResponder: null}),
captured: keyOf({onScrollShouldSetResponderCapture: null})
}
captured: keyOf({onScrollShouldSetResponderCapture: null}),
},
},
/**
@@ -105,8 +97,8 @@ var eventTypes = {
selectionChangeShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onSelectionChangeShouldSetResponder: null}),
captured: keyOf({onSelectionChangeShouldSetResponderCapture: null})
}
captured: keyOf({onSelectionChangeShouldSetResponderCapture: null}),
},
},
/**
@@ -116,8 +108,8 @@ var eventTypes = {
moveShouldSetResponder: {
phasedRegistrationNames: {
bubbled: keyOf({onMoveShouldSetResponder: null}),
captured: keyOf({onMoveShouldSetResponderCapture: null})
}
captured: keyOf({onMoveShouldSetResponderCapture: null}),
},
},
/**
@@ -128,11 +120,11 @@ var eventTypes = {
responderEnd: {registrationName: keyOf({onResponderEnd: null})},
responderRelease: {registrationName: keyOf({onResponderRelease: null})},
responderTerminationRequest: {
registrationName: keyOf({onResponderTerminationRequest: null})
registrationName: keyOf({onResponderTerminationRequest: null}),
},
responderGrant: {registrationName: keyOf({onResponderGrant: null})},
responderReject: {registrationName: keyOf({onResponderReject: null})},
responderTerminate: {registrationName: keyOf({onResponderTerminate: null})}
responderTerminate: {registrationName: keyOf({onResponderTerminate: null})},
};
/**
@@ -141,7 +133,7 @@ var eventTypes = {
* ----------------
*
* - A global, solitary "interaction lock" on a view.
* - If a `NodeHandle` becomes the responder, it should convey visual feedback
* - If a node becomes the responder, it should convey visual feedback
* immediately to indicate so, either by highlighting or moving accordingly.
* - To be the responder means, that touches are exclusively important to that
* responder view, and no other view.
@@ -334,7 +326,8 @@ to return true:wantsResponderID| |
function setResponderAndExtractTransfer(
topLevelType,
topLevelTargetID,
nativeEvent) {
nativeEvent,
nativeEventTarget) {
var shouldSetEventType =
isStartish(topLevelType) ? eventTypes.startShouldSetResponder :
isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder :
@@ -345,7 +338,7 @@ function setResponderAndExtractTransfer(
// TODO: stop one short of the the current responder.
var bubbleShouldSetFrom = !responderID ?
topLevelTargetID :
ReactInstanceHandles._getFirstCommonAncestorID(responderID, topLevelTargetID);
ReactInstanceHandles.getFirstCommonAncestorID(responderID, topLevelTargetID);
// When capturing/bubbling the "shouldSet" event, we want to skip the target
// (deepest ID) if it happens to be the current responder. The reasoning:
@@ -355,7 +348,8 @@ function setResponderAndExtractTransfer(
var shouldSetEvent = ResponderSyntheticEvent.getPooled(
shouldSetEventType,
bubbleShouldSetFrom,
nativeEvent
nativeEvent,
nativeEventTarget
);
shouldSetEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
if (skipOverBubbleShouldSetFrom) {
@@ -375,7 +369,8 @@ function setResponderAndExtractTransfer(
var grantEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderGrant,
wantsResponderID,
nativeEvent
nativeEvent,
nativeEventTarget
);
grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
@@ -386,7 +381,8 @@ function setResponderAndExtractTransfer(
var terminationRequestEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderTerminationRequest,
responderID,
nativeEvent
nativeEvent,
nativeEventTarget
);
terminationRequestEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(terminationRequestEvent);
@@ -401,7 +397,8 @@ function setResponderAndExtractTransfer(
var terminateEvent = ResponderSyntheticEvent.getPooled(
terminateType,
responderID,
nativeEvent
nativeEvent,
nativeEventTarget
);
terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(terminateEvent);
@@ -411,7 +408,8 @@ function setResponderAndExtractTransfer(
var rejectEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderReject,
wantsResponderID,
nativeEvent
nativeEvent,
nativeEventTarget
);
rejectEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(rejectEvent);
@@ -447,7 +445,7 @@ function canTriggerTransfer(topLevelType, topLevelTargetID) {
* longer any touches that started inside of the current `responderID`.
*
* @param {NativeEvent} nativeEvent Native touch end event.
* @return {bool} Whether or not this touch end event ends the responder.
* @return {boolean} Whether or not this touch end event ends the responder.
*/
function noResponderTouches(nativeEvent) {
var touches = nativeEvent.touches;
@@ -459,12 +457,12 @@ function noResponderTouches(nativeEvent) {
var target = activeTouch.target;
if (target !== null && target !== undefined && target !== 0) {
// Is the original touch location inside of the current responder?
var commonAncestor =
ReactInstanceHandles._getFirstCommonAncestorID(
var isAncestor =
ReactInstanceHandles.isAncestorIDOf(
responderID,
NodeHandle.getRootNodeID(target)
EventPluginUtils.getID(target)
);
if (commonAncestor === responderID) {
if (isAncestor) {
return false;
}
}
@@ -497,8 +495,8 @@ var ResponderEventPlugin = {
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent) {
nativeEvent,
nativeEventTarget) {
if (isStartish(topLevelType)) {
trackedTouchCount += 1;
} else if (isEndish(topLevelType)) {
@@ -509,10 +507,14 @@ var ResponderEventPlugin = {
);
}
ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);
ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent, nativeEventTarget);
var extracted = canTriggerTransfer(topLevelType, topLevelTargetID) ?
setResponderAndExtractTransfer(topLevelType, topLevelTargetID, nativeEvent) :
setResponderAndExtractTransfer(
topLevelType,
topLevelTargetID,
nativeEvent,
nativeEventTarget) :
null;
// Responder may or may not have transfered on a new touch start/move.
// Regardless, whoever is the responder after any potential transfer, we
@@ -535,7 +537,12 @@ var ResponderEventPlugin = {
if (incrementalTouch) {
var gesture =
ResponderSyntheticEvent.getPooled(incrementalTouch, responderID, nativeEvent);
ResponderSyntheticEvent.getPooled(
incrementalTouch,
responderID,
nativeEvent,
nativeEventTarget
);
gesture.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(gesture);
extracted = accumulate(extracted, gesture);
@@ -555,7 +562,7 @@ var ResponderEventPlugin = {
null;
if (finalTouch) {
var finalEvent =
ResponderSyntheticEvent.getPooled(finalTouch, responderID, nativeEvent);
ResponderSyntheticEvent.getPooled(finalTouch, responderID, nativeEvent, nativeEventTarget);
finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
EventPropagators.accumulateDirectDispatches(finalEvent);
extracted = accumulate(extracted, finalEvent);
@@ -595,7 +602,7 @@ var ResponderEventPlugin = {
injectGlobalInteractionHandler: function(GlobalInteractionHandler) {
ResponderEventPlugin.GlobalInteractionHandler = GlobalInteractionHandler;
},
}
},
};
module.exports = ResponderEventPlugin;

View File

@@ -1,47 +0,0 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ResponderSyntheticEvent
* @typechecks static-only
*/
"use strict";
var SyntheticEvent = require('SyntheticEvent');
/**
* `touchHistory` isn't actually on the native event, but putting it in the
* interface will ensure that it is cleaned up when pooled/destroyed. The
* `ResponderEventPlugin` will populate it appropriately.
*/
var ResponderEventInterface = {
touchHistory: function(nativeEvent) {
return null; // Actually doesn't even look at the native event.
},
};
/**
* @param {object} dispatchConfig Configuration used to dispatch this event.
* @param {string} dispatchMarker Marker identifying the event target.
* @param {object} nativeEvent Native event.
* @extends {SyntheticEvent}
*/
function ResponderSyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent) {
SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent);
}
SyntheticEvent.augmentClass(ResponderSyntheticEvent, ResponderEventInterface);
module.exports = ResponderSyntheticEvent;

View File

@@ -1,178 +0,0 @@
/**
* @providesModule ResponderTouchHistoryStore
*/
"use strict";
var EventPluginUtils = require('EventPluginUtils');
var invariant = require('invariant');
var isMoveish = EventPluginUtils.isMoveish;
var isStartish = EventPluginUtils.isStartish;
var isEndish = EventPluginUtils.isEndish;
var MAX_TOUCH_BANK = 20;
/**
* Touch position/time tracking information by touchID. Typically, we'll only
* see IDs with a range of 1-20 (they are recycled when touches end and then
* start again). This data is commonly needed by many different interaction
* logic modules so precomputing it is very helpful to do once.
* Each touch object in `touchBank` is of the following form:
* { touchActive: boolean,
* startTimeStamp: number,
* startPageX: number,
* startPageY: number,
* currentPageX: number,
* currentPageY: number,
* currentTimeStamp: number
* }
*/
var touchHistory = {
touchBank: [ ],
numberActiveTouches: 0,
// If there is only one active touch, we remember its location. This prevents
// us having to loop through all of the touches all the time in the most
// common case.
indexOfSingleActiveTouch: -1,
mostRecentTimeStamp: 0,
};
var timestampForTouch = function(touch) {
// The legacy internal implementation provides "timeStamp", which has been
// renamed to "timestamp". Let both work for now while we iron it out
// TODO (evv): rename timeStamp to timestamp in internal code
return touch.timeStamp || touch.timestamp;
};
/**
* TODO: Instead of making gestures recompute filtered velocity, we could
* include a built in velocity computation that can be reused globally.
* @param {Touch} touch Native touch object.
*/
var initializeTouchData = function(touch) {
return {
touchActive: true,
startTimeStamp: timestampForTouch(touch),
startPageX: touch.pageX,
startPageY: touch.pageY,
currentPageX: touch.pageX,
currentPageY: touch.pageY,
currentTimeStamp: timestampForTouch(touch),
previousPageX: touch.pageX,
previousPageY: touch.pageY,
previousTimeStamp: timestampForTouch(touch),
};
};
var reinitializeTouchTrack = function(touchTrack, touch) {
touchTrack.touchActive = true;
touchTrack.startTimeStamp = timestampForTouch(touch);
touchTrack.startPageX = touch.pageX;
touchTrack.startPageY = touch.pageY;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
touchTrack.currentTimeStamp = timestampForTouch(touch);
touchTrack.previousPageX = touch.pageX;
touchTrack.previousPageY = touch.pageY;
touchTrack.previousTimeStamp = timestampForTouch(touch);
};
var validateTouch = function(touch) {
var identifier = touch.identifier;
invariant(identifier != null, 'Touch object is missing identifier');
if (identifier > MAX_TOUCH_BANK) {
console.warn(
'Touch identifier ' + identifier + ' is greater than maximum ' +
'supported ' + MAX_TOUCH_BANK + ' which causes performance issues ' +
'backfilling array locations for all of the indices.'
);
}
};
var recordStartTouchData = function(touch) {
var touchBank = touchHistory.touchBank;
var identifier = touch.identifier;
var touchTrack = touchBank[identifier];
if (__DEV__) {
validateTouch(touch);
}
if (!touchTrack) {
touchBank[touch.identifier] = initializeTouchData(touch);
} else {
reinitializeTouchTrack(touchTrack, touch);
}
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var recordMoveTouchData = function(touch) {
var touchBank = touchHistory.touchBank;
var touchTrack = touchBank[touch.identifier];
if (__DEV__) {
validateTouch(touch);
invariant(touchTrack, 'Touch data should have been recorded on start');
}
touchTrack.touchActive = true;
touchTrack.previousPageX = touchTrack.currentPageX;
touchTrack.previousPageY = touchTrack.currentPageY;
touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
touchTrack.currentTimeStamp = timestampForTouch(touch);
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var recordEndTouchData = function(touch) {
var touchBank = touchHistory.touchBank;
var touchTrack = touchBank[touch.identifier];
if (__DEV__) {
validateTouch(touch);
invariant(touchTrack, 'Touch data should have been recorded on start');
}
touchTrack.previousPageX = touchTrack.currentPageX;
touchTrack.previousPageY = touchTrack.currentPageY;
touchTrack.previousTimeStamp = touchTrack.currentTimeStamp;
touchTrack.currentPageX = touch.pageX;
touchTrack.currentPageY = touch.pageY;
touchTrack.currentTimeStamp = timestampForTouch(touch);
touchTrack.touchActive = false;
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
};
var ResponderTouchHistoryStore = {
recordTouchTrack: function(topLevelType, nativeEvent) {
var touchBank = touchHistory.touchBank;
if (isMoveish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordMoveTouchData);
} else if (isStartish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordStartTouchData);
touchHistory.numberActiveTouches = nativeEvent.touches.length;
if (touchHistory.numberActiveTouches === 1) {
touchHistory.indexOfSingleActiveTouch = nativeEvent.touches[0].identifier;
}
} else if (isEndish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordEndTouchData);
touchHistory.numberActiveTouches = nativeEvent.touches.length;
if (touchHistory.numberActiveTouches === 1) {
for (var i = 0; i < touchBank.length; i++) {
var touchTrackToCheck = touchBank[i];
if (touchTrackToCheck != null && touchTrackToCheck.touchActive) {
touchHistory.indexOfSingleActiveTouch = i;
break;
}
}
if (__DEV__) {
var activeTouchData = touchBank[touchHistory.indexOfSingleActiveTouch];
var foundActive = activeTouchData != null && !!activeTouchData.touchActive;
invariant(foundActive, 'Cannot find single active touch');
}
}
}
},
touchHistory: touchHistory,
};
module.exports = ResponderTouchHistoryStore;

View File

@@ -1,348 +0,0 @@
/**
* Copyright 2013-2014 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @providesModule ReactInstanceHandles
* @typechecks static-only
*/
"use strict";
var ReactRootIndex = require('ReactRootIndex');
var invariant = require('invariant');
var SEPARATOR = '.';
var SEPARATOR_LENGTH = SEPARATOR.length;
/**
* Maximum depth of traversals before we consider the possibility of a bad ID.
*/
var MAX_TREE_DEPTH = 100;
/**
* Creates a DOM ID prefix to use when mounting React components.
*
* @param {number} index A unique integer
* @return {string} React root ID.
* @internal
*/
function getReactRootIDString(index) {
return SEPARATOR + index.toString(36);
}
/**
* Checks if a character in the supplied ID is a separator or the end.
*
* @param {string} id A React DOM ID.
* @param {number} index Index of the character to check.
* @return {boolean} True if the character is a separator or end of the ID.
* @private
*/
function isBoundary(id, index) {
return id.charAt(index) === SEPARATOR || index === id.length;
}
/**
* Checks if the supplied string is a valid React DOM ID.
*
* @param {string} id A React DOM ID, maybe.
* @return {boolean} True if the string is a valid React DOM ID.
* @private
*/
function isValidID(id) {
return id === '' || (
id.charAt(0) === SEPARATOR && id.charAt(id.length - 1) !== SEPARATOR
);
}
/**
* Checks if the first ID is an ancestor of or equal to the second ID.
*
* @param {string} ancestorID
* @param {string} descendantID
* @return {boolean} True if `ancestorID` is an ancestor of `descendantID`.
* @internal
*/
function isAncestorIDOf(ancestorID, descendantID) {
return (
descendantID.indexOf(ancestorID) === 0 &&
isBoundary(descendantID, ancestorID.length)
);
}
/**
* Gets the parent ID of the supplied React DOM ID, `id`.
*
* @param {string} id ID of a component.
* @return {string} ID of the parent, or an empty string.
* @private
*/
function getParentID(id) {
return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : '';
}
/**
* Gets the next DOM ID on the tree path from the supplied `ancestorID` to the
* supplied `destinationID`. If they are equal, the ID is returned.
*
* @param {string} ancestorID ID of an ancestor node of `destinationID`.
* @param {string} destinationID ID of the destination node.
* @return {string} Next ID on the path from `ancestorID` to `destinationID`.
* @private
*/
function getNextDescendantID(ancestorID, destinationID) {
invariant(
isValidID(ancestorID) && isValidID(destinationID),
'getNextDescendantID(%s, %s): Received an invalid React DOM ID.',
ancestorID,
destinationID
);
invariant(
isAncestorIDOf(ancestorID, destinationID),
'getNextDescendantID(...): React has made an invalid assumption about ' +
'the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.',
ancestorID,
destinationID
);
if (ancestorID === destinationID) {
return ancestorID;
}
// Skip over the ancestor and the immediate separator. Traverse until we hit
// another separator or we reach the end of `destinationID`.
var start = ancestorID.length + SEPARATOR_LENGTH;
for (var i = start; i < destinationID.length; i++) {
if (isBoundary(destinationID, i)) {
break;
}
}
return destinationID.substr(0, i);
}
/**
* Gets the nearest common ancestor ID of two IDs.
*
* Using this ID scheme, the nearest common ancestor ID is the longest common
* prefix of the two IDs that immediately preceded a "marker" in both strings.
*
* @param {string} oneID
* @param {string} twoID
* @return {string} Nearest common ancestor ID, or the empty string if none.
* @private
*/
function getFirstCommonAncestorID(oneID, twoID) {
var minLength = Math.min(oneID.length, twoID.length);
if (minLength === 0) {
return '';
}
var lastCommonMarkerIndex = 0;
// Use `<=` to traverse until the "EOL" of the shorter string.
for (var i = 0; i <= minLength; i++) {
if (isBoundary(oneID, i) && isBoundary(twoID, i)) {
lastCommonMarkerIndex = i;
} else if (oneID.charAt(i) !== twoID.charAt(i)) {
break;
}
}
var longestCommonID = oneID.substr(0, lastCommonMarkerIndex);
invariant(
isValidID(longestCommonID),
'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s',
oneID,
twoID,
longestCommonID
);
return longestCommonID;
}
/**
* Traverses the parent path between two IDs (either up or down). The IDs must
* not be the same, and there must exist a parent path between them. If the
* callback returns `false`, traversal is stopped.
*
* @param {?string} start ID at which to start traversal.
* @param {?string} stop ID at which to end traversal.
* @param {function} cb Callback to invoke each ID with.
* @param {?boolean} skipFirst Whether or not to skip the first node.
* @param {?boolean} skipLast Whether or not to skip the last node.
* @private
*/
function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
start = start || '';
stop = stop || '';
invariant(
start !== stop,
'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.',
start
);
var traverseUp = isAncestorIDOf(stop, start);
invariant(
traverseUp || isAncestorIDOf(start, stop),
'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' +
'not have a parent path.',
start,
stop
);
// Traverse from `start` to `stop` one depth at a time.
var depth = 0;
var traverse = traverseUp ? getParentID : getNextDescendantID;
for (var id = start; /* until break */; id = traverse(id, stop)) {
var ret;
if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {
ret = cb(id, traverseUp, arg);
}
if (ret === false || id === stop) {
// Only break //after// visiting `stop`.
break;
}
invariant(
depth++ < MAX_TREE_DEPTH,
'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' +
'traversing the React DOM ID tree. This may be due to malformed IDs: %s',
start, stop
);
}
}
/**
* Manages the IDs assigned to DOM representations of React components. This
* uses a specific scheme in order to traverse the DOM efficiently (e.g. in
* order to simulate events).
*
* @internal
*/
var ReactInstanceHandles = {
/**
* Constructs a React root ID
* @return {string} A React root ID.
*/
createReactRootID: function() {
return getReactRootIDString(ReactRootIndex.createReactRootIndex());
},
/**
* Constructs a React ID by joining a root ID with a name.
*
* @param {string} rootID Root ID of a parent component.
* @param {string} name A component's name (as flattened children).
* @return {string} A React ID.
* @internal
*/
createReactID: function(rootID, name) {
return rootID + name;
},
/**
* Gets the DOM ID of the React component that is the root of the tree that
* contains the React component with the supplied DOM ID.
*
* @param {string} id DOM ID of a React component.
* @return {?string} DOM ID of the React component that is the root.
* @internal
*/
getReactRootIDFromNodeID: function(id) {
if (id && id.charAt(0) === SEPARATOR && id.length > 1) {
var index = id.indexOf(SEPARATOR, 1);
return index > -1 ? id.substr(0, index) : id;
}
return null;
},
/**
* Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
* should would receive a `mouseEnter` or `mouseLeave` event.
*
* NOTE: Does not invoke the callback on the nearest common ancestor because
* nothing "entered" or "left" that element.
*
* @param {string} leaveID ID being left.
* @param {string} enterID ID being entered.
* @param {function} cb Callback to invoke on each entered/left ID.
* @param {*} upArg Argument to invoke the callback with on left IDs.
* @param {*} downArg Argument to invoke the callback with on entered IDs.
* @internal
*/
traverseEnterLeave: function(leaveID, enterID, cb, upArg, downArg) {
var ancestorID = getFirstCommonAncestorID(leaveID, enterID);
if (ancestorID !== leaveID) {
traverseParentPath(leaveID, ancestorID, cb, upArg, false, true);
}
if (ancestorID !== enterID) {
traverseParentPath(ancestorID, enterID, cb, downArg, true, false);
}
},
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*
* NOTE: This traversal happens on IDs without touching the DOM.
*
* @param {string} targetID ID of the target node.
* @param {function} cb Callback to invoke.
* @param {*} arg Argument to invoke the callback with.
* @internal
*/
traverseTwoPhase: function(targetID, cb, arg) {
if (targetID) {
traverseParentPath('', targetID, cb, arg, true, false);
traverseParentPath(targetID, '', cb, arg, false, true);
}
},
/**
* Same as `traverseTwoPhase` but skips the `targetID`.
*/
traverseTwoPhaseSkipTarget: function(targetID, cb, arg) {
if (targetID) {
traverseParentPath('', targetID, cb, arg, true, true);
traverseParentPath(targetID, '', cb, arg, true, true);
}
},
/**
* Traverse a node ID, calling the supplied `cb` for each ancestor ID. For
* example, passing `.0.$row-0.1` would result in `cb` getting called
* with `.0`, `.0.$row-0`, and `.0.$row-0.1`.
*
* NOTE: This traversal happens on IDs without touching the DOM.
*
* @param {string} targetID ID of the target node.
* @param {function} cb Callback to invoke.
* @param {*} arg Argument to invoke the callback with.
* @internal
*/
traverseAncestors: function(targetID, cb, arg) {
traverseParentPath('', targetID, cb, arg, true, false);
},
/**
* Exposed for unit testing.
* @private
*/
_getFirstCommonAncestorID: getFirstCommonAncestorID,
/**
* Exposed for unit testing.
* @private
*/
_getNextDescendantID: getNextDescendantID,
isAncestorIDOf: isAncestorIDOf,
SEPARATOR: SEPARATOR
};
module.exports = ReactInstanceHandles;