mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-28 12:15:37 +08:00
Updates from Tue 11 Aug
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
102
Libraries/Animation/Animated/SpringConfig.js
Normal file
102
Libraries/Animation/Animated/SpringConfig.js
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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: ' +
|
||||
|
||||
@@ -40,6 +40,10 @@ var NativeModules = {
|
||||
SourceCode: {
|
||||
scriptURL: null,
|
||||
},
|
||||
BuildInfo: {
|
||||
appVersion: '0',
|
||||
buildVersion: '0',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = NativeModules;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -173,7 +173,7 @@ var Image = React.createClass({
|
||||
resizeMode={resizeMode}
|
||||
tintColor={tintColor}
|
||||
src={source.uri}
|
||||
defaultSrc={defaultSource.uri}
|
||||
defaultImageSrc={defaultSource.uri}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
14
Libraries/Image/RCTImageEditingManager.h
Normal file
14
Libraries/Image/RCTImageEditingManager.h
Normal 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
|
||||
133
Libraries/Image/RCTImageEditingManager.m
Normal file
133
Libraries/Image/RCTImageEditingManager.m
Normal 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
|
||||
@@ -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?
|
||||
|
||||
@@ -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 ^{};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
48
Libraries/Modal/Modal.js
Normal 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;
|
||||
@@ -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};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -121,7 +121,8 @@ var ReactNativeEventEmitter = merge(ReactEventEmitterMixin, {
|
||||
topLevelType,
|
||||
rootNodeID,
|
||||
rootNodeID,
|
||||
nativeEvent
|
||||
nativeEvent,
|
||||
nativeEvent.target
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(), ^{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: '%@'",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}];
|
||||
|
||||
@@ -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;
|
||||
|
||||
1
Libraries/react-native/react-native.js
vendored
1
Libraries/react-native/react-native.js
vendored
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
348
Libraries/vendor/react/core/ReactInstanceHandles.js
vendored
348
Libraries/vendor/react/core/ReactInstanceHandles.js
vendored
@@ -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;
|
||||
Reference in New Issue
Block a user