;
+ enabled: boolean;
+};
+
+var SEGMENTED_CONTROL_REFERENCE = 'segmentedcontrol';
+
+type Event = Object;
+
+/**
+ * Use `SegmentedControlIOS` to render a UISegmentedControl iOS.
+ */
+var SegmentedControlIOS = React.createClass({
+ mixins: [NativeMethodsMixin],
+
+ propTypes: {
+ /**
+ * The labels for the control's segment buttons, in order.
+ */
+ values: PropTypes.arrayOf(PropTypes.string),
+
+ /**
+ * The index in `props.values` of the segment to be pre-selected
+ */
+ selectedIndex: PropTypes.number,
+
+ /**
+ * Callback that is called when the user taps a segment;
+ * passes the segment's value as an argument
+ */
+ onValueChange: PropTypes.func,
+
+ /**
+ * Callback that is called when the user taps a segment;
+ * passes the event as an argument
+ */
+ onChange: PropTypes.func,
+
+ /**
+ * If false the user won't be able to interact with the control.
+ * Default value is true.
+ */
+ enabled: PropTypes.bool,
+
+ /**
+ * Accent color of the control.
+ */
+ tintColor: PropTypes.string,
+
+ /**
+ * If true, then selecting a segment won't persist visually.
+ * The `onValueChange` callback will still work as expected.
+ */
+ momentary: PropTypes.bool
+ },
+
+ getDefaultProps: function(): DefaultProps {
+ return {
+ values: [],
+ enabled: true
+ };
+ },
+
+ _onChange: function(event: Event) {
+ this.props.onChange && this.props.onChange(event);
+ this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value);
+ },
+
+ render: function() {
+ return (
+
+ );
+ }
+});
+
+var styles = StyleSheet.create({
+ segmentedControl: {
+ height: NativeModules.SegmentedControlManager.ComponentHeight
+ },
+});
+
+var RCTSegmentedControl = requireNativeComponent(
+ 'RCTSegmentedControl',
+ null
+);
+if (__DEV__) {
+ verifyPropTypes(
+ RCTSegmentedControl,
+ RCTSegmentedControl.viewConfig
+ );
+}
+
+module.exports = SegmentedControlIOS;
diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js
index 2baaee21b416c6..b27e22d4b1e92c 100644
--- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js
+++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js
@@ -66,6 +66,9 @@ var TabBarItemIOS = React.createClass({
* blank content, you probably forgot to add a selected one.
*/
selected: React.PropTypes.bool,
+ /**
+ * React style object.
+ */
style: View.propTypes.style,
/**
* Text that appears under the icon. It is ignored when a system icon
diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js
index dfd3ab1a128469..c21184b7da5130 100644
--- a/Libraries/Components/TextInput/TextInput.js
+++ b/Libraries/Components/TextInput/TextInput.js
@@ -38,6 +38,7 @@ var returnKeyTypeConsts = RCTUIManager.UIReturnKeyType;
var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, {
autoCorrect: true,
autoCapitalize: true,
+ clearTextOnFocus: true,
color: true,
editable: true,
fontFamily: true,
@@ -48,6 +49,7 @@ var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, {
returnKeyType: true,
enablesReturnKeyAutomatically: true,
secureTextEntry: true,
+ selectTextOnFocus: true,
mostRecentEventCounter: true,
placeholder: true,
placeholderTextColor: true,
@@ -445,6 +447,7 @@ var TextInput = React.createClass({
onSubmitEditing={this.props.onSubmitEditing}
onSelectionChangeShouldSetResponder={() => true}
placeholder={this.props.placeholder}
+ placeholderTextColor={this.props.placeholderTextColor}
text={this.state.bufferedValue}
autoCapitalize={autoCapitalize}
autoCorrect={this.props.autoCorrect}
@@ -498,6 +501,8 @@ var TextInput = React.createClass({
autoCapitalize={autoCapitalize}
autoCorrect={this.props.autoCorrect}
clearButtonMode={clearButtonMode}
+ selectTextOnFocus={this.props.selectTextOnFocus}
+ clearTextOnFocus={this.props.clearTextOnFocus}
/>;
}
diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js
index 0da57f554257e5..c7ca2ee261ab91 100644
--- a/Libraries/Components/View/View.js
+++ b/Libraries/Components/View/View.js
@@ -29,7 +29,7 @@ var stylePropType = StyleSheetPropType(ViewStylePropTypes);
* container that supports layout with flexbox, style, some touch handling, and
* accessibility controls, and is designed to be nested inside other views and
* to have 0 to many children of any type. `View` maps directly to the native
- * view equivalent on whatever platform react is running on, whether that is a
+ * view equivalent on whatever platform React is running on, whether that is a
* `UIView`, ``, `android.view`, etc. This example creates a `View` that
* wraps two colored boxes and custom component in a row with padding.
*
diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js
index bfc823f9f85928..959422bbc9888a 100644
--- a/Libraries/Components/WebView/WebView.android.js
+++ b/Libraries/Components/WebView/WebView.android.js
@@ -42,6 +42,7 @@ var WebView = React.createClass({
onNavigationStateChange: PropTypes.func,
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
style: View.propTypes.style,
+ javaScriptEnabledAndroid: PropTypes.bool,
/**
* Used to locate this view in end-to-end tests.
*/
@@ -90,6 +91,7 @@ var WebView = React.createClass({
key="webViewKey"
style={webViewStyles}
url={this.props.url}
+ javaScriptEnabledAndroid={this.props.javaScriptEnabledAndroid}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets}
onLoadingStart={this.onLoadingStart}
@@ -157,6 +159,7 @@ var WebView = React.createClass({
var RCTWebView = createReactIOSNativeComponentClass({
validAttributes: merge(ReactIOSViewAttributes.UIView, {
url: true,
+ javaScriptEnabledAndroid: true,
}),
uiViewClassName: 'RCTWebView',
});
diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js
index c4e4fbcd3299c8..ed2c98fae02f5a 100644
--- a/Libraries/Components/WebView/WebView.ios.js
+++ b/Libraries/Components/WebView/WebView.ios.js
@@ -92,6 +92,10 @@ var WebView = React.createClass({
onNavigationStateChange: PropTypes.func,
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
style: View.propTypes.style,
+ /**
+ * Used for android only, JS is enabled by default for WebView on iOS
+ */
+ javaScriptEnabledAndroid: PropTypes.bool,
},
getInitialState: function() {
diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js
index da4d2ab10f52ca..6d3d55c316f097 100644
--- a/Libraries/CustomComponents/Navigator/Navigator.js
+++ b/Libraries/CustomComponents/Navigator/Navigator.js
@@ -267,7 +267,8 @@ var Navigator = React.createClass({
},
contextTypes: {
- navigator: PropTypes.object,
+ // TODO (t6707746) Re-enable this when owner context switches to parent context
+ // navigator: PropTypes.object,
},
statics: {
@@ -314,13 +315,11 @@ var Navigator = React.createClass({
// On first render, we will render every scene in the initialRouteStack
updatingRangeStart: 0,
updatingRangeLength: routeStack.length,
- // Either animating or gesturing.
- isAnimating: false,
- jumpToIndex: routeStack.length - 1,
presentedIndex: initialRouteIndex,
- isResponderOnlyToBlockTouches: false,
- fromIndex: initialRouteIndex,
- toIndex: initialRouteIndex,
+ transitionFromIndex: null,
+ activeGesture: null,
+ pendingGestureProgress: null,
+ transitionQueue: [],
};
},
@@ -355,9 +354,24 @@ var Navigator = React.createClass({
popToTop: this.popToTop,
};
this._handlers = {};
-
+ this.springSystem = new rebound.SpringSystem();
+ this.spring = this.springSystem.createSpring();
+ this.spring.setRestSpeedThreshold(0.05);
+ this.spring.setCurrentValue(0).setAtRest();
+ this.spring.addListener({
+ onSpringEndStateChange: () => {
+ if (!this._interactionHandle) {
+ this._interactionHandle = this.createInteractionHandle();
+ }
+ },
+ onSpringUpdate: () => {
+ this._handleSpringUpdate();
+ },
+ onSpringAtRest: () => {
+ this._completeTransition();
+ },
+ });
this.panGesture = PanResponder.create({
- onStartShouldSetPanResponderCapture: this._handleStartShouldSetPanResponderCapture,
onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderRelease: this._handlePanResponderRelease,
@@ -426,20 +440,8 @@ var Navigator = React.createClass({
this._handlers[this.state.routeStack.indexOf(route)] = handler;
},
- _configureSpring: function(animationConfig) {
- var config = this.spring.getSpringConfig();
- config.friction = animationConfig.springFriction;
- config.tension = animationConfig.springTension;
- },
-
componentDidMount: function() {
- this.springSystem = new rebound.SpringSystem();
- this.spring = this.springSystem.createSpring();
- this.spring.setRestSpeedThreshold(0.05);
- var animationConfig = this.state.sceneConfigStack[this.state.presentedIndex];
- animationConfig && this._configureSpring(animationConfig);
- this.spring.addListener(this);
- this.onSpringUpdate();
+ this._handleSpringUpdate();
this._emitDidFocus(this.state.routeStack[this.state.presentedIndex]);
if (this.parentNavigator) {
this.parentNavigator.setHandler(this._handleRequest);
@@ -483,98 +485,113 @@ var Navigator = React.createClass({
updatingRangeStart: 0,
updatingRangeLength: nextRouteStack.length,
presentedIndex: destIndex,
- jumpToIndex: destIndex,
- toIndex: destIndex,
- fromIndex: destIndex,
+ activeGesture: null,
+ transitionFromIndex: null,
+ transitionQueue: [],
}, () => {
- this.onSpringUpdate();
+ this._handleSpringUpdate();
});
},
+ _transitionTo: function(destIndex, velocity, jumpSpringTo, cb) {
+ if (destIndex === this.state.presentedIndex) {
+ return;
+ }
+ if (this.state.transitionFromIndex !== null) {
+ this.state.transitionQueue.push({
+ destIndex,
+ velocity,
+ cb,
+ });
+ return;
+ }
+ this.state.transitionFromIndex = this.state.presentedIndex;
+ this.state.presentedIndex = destIndex;
+ this.state.transitionCb = cb;
+ this._onAnimationStart();
+ if (AnimationsDebugModule) {
+ AnimationsDebugModule.startRecordingFps();
+ }
+ var sceneConfig = this.state.sceneConfigStack[this.state.transitionFromIndex] ||
+ this.state.sceneConfigStack[this.state.presentedIndex];
+ invariant(
+ sceneConfig,
+ 'Cannot configure scene at index ' + this.state.transitionFromIndex
+ );
+ if (jumpSpringTo != null) {
+ this.spring.setCurrentValue(jumpSpringTo);
+ }
+ this.spring.setOvershootClampingEnabled(true);
+ this.spring.getSpringConfig().friction = sceneConfig.springFriction;
+ this.spring.getSpringConfig().tension = sceneConfig.springTension;
+ this.spring.setVelocity(velocity || sceneConfig.defaultTransitionVelocity);
+ this.spring.setEndValue(1);
+ var willFocusRoute = this._subRouteFocus[this.state.presentedIndex] || this.state.routeStack[this.state.presentedIndex];
+ this._emitWillFocus(willFocusRoute);
+ },
+
/**
- * TODO: Accept callback for spring completion.
+ * This happens for each frame of either a gesture or a transition. If both are
+ * happening, we only set values for the transition and the gesture will catch up later
*/
- _requestTransitionTo: function(topOfStack) {
- if (topOfStack !== this.state.presentedIndex) {
- invariant(!this.state.isAnimating, 'Cannot navigate while transitioning');
- this.state.fromIndex = this.state.presentedIndex;
- this.state.toIndex = topOfStack;
- this.spring.setOvershootClampingEnabled(false);
- if (AnimationsDebugModule) {
- AnimationsDebugModule.startRecordingFps();
- }
- this._transitionToToIndexWithVelocity(
- this.state.sceneConfigStack[this.state.fromIndex].defaultTransitionVelocity
+ _handleSpringUpdate: function() {
+ // Prioritize handling transition in progress over a gesture:
+ if (this.state.transitionFromIndex != null) {
+ this._transitionBetween(
+ this.state.transitionFromIndex,
+ this.state.presentedIndex,
+ this.spring.getCurrentValue()
+ );
+ } else if (this.state.activeGesture != null) {
+ this._transitionBetween(
+ this.state.presentedIndex,
+ this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture),
+ this.spring.getCurrentValue()
);
}
},
/**
- * `onSpring*` spring delegate. Wired up via `spring.addListener(this)`
+ * This happens at the end of a transition started by transitionTo
*/
- onSpringEndStateChange: function() {
- if (!this._interactionHandle) {
- this._interactionHandle = this.createInteractionHandle();
+ _completeTransition: function() {
+ if (this.spring.getCurrentValue() !== 1) {
+ if (this.state.pendingGestureProgress) {
+ this.state.pendingGestureProgress = null;
+ }
+ return;
}
- },
-
- onSpringUpdate: function() {
- this._transitionBetween(
- this.state.fromIndex,
- this.state.toIndex,
- this.spring.getCurrentValue()
- );
- },
-
- onSpringAtRest: function() {
- this.state.isAnimating = false;
- this._completeTransition();
+ this._onAnimationEnd();
+ var presentedIndex = this.state.presentedIndex;
+ var didFocusRoute = this._subRouteFocus[presentedIndex] || this.state.routeStack[presentedIndex];
+ this._emitDidFocus(didFocusRoute);
+ if (AnimationsDebugModule) {
+ AnimationsDebugModule.stopRecordingFps(Date.now());
+ }
+ this.state.transitionFromIndex = null;
this.spring.setCurrentValue(0).setAtRest();
+ this._hideScenes();
+ if (this.state.transitionCb) {
+ this.state.transitionCb();
+ this.state.transitionCb = null;
+ }
if (this._interactionHandle) {
this.clearInteractionHandle(this._interactionHandle);
this._interactionHandle = null;
}
- },
-
- _completeTransition: function() {
- if (this.spring.getCurrentValue() === 1) {
- this._onAnimationEnd();
- var presentedIndex = this.state.toIndex;
- this.state.presentedIndex = presentedIndex;
- this.state.fromIndex = presentedIndex;
- var didFocusRoute = this._subRouteFocus[presentedIndex] || this.state.routeStack[presentedIndex];
- this._emitDidFocus(didFocusRoute);
- this._removePoppedRoutes();
- if (AnimationsDebugModule) {
- AnimationsDebugModule.stopRecordingFps(Date.now());
- }
- } else {
- this.state.fromIndex = this.state.presentedIndex;
- this.state.toIndex = this.state.presentedIndex;
+ if (this.state.pendingGestureProgress) {
+ this.spring.setEndValue(this.state.pendingGestureProgress);
+ return;
+ }
+ if (this.state.transitionQueue.length) {
+ var queuedTransition = this.state.transitionQueue.shift();
+ this._transitionTo(
+ queuedTransition.destIndex,
+ queuedTransition.velocity,
+ null,
+ queuedTransition.cb
+ );
}
- this._hideOtherScenes(this.state.presentedIndex);
- },
-
- _transitionToToIndexWithVelocity: function(v) {
- this._configureSpring(
- // For visual consistency, the from index is always used to configure the spring
- this.state.sceneConfigStack[this.state.fromIndex]
- );
- this._onAnimationStart();
- this.state.isAnimating = true;
- this.spring.setVelocity(v);
- this.spring.setEndValue(1);
- var willFocusRoute = this._subRouteFocus[this.state.toIndex] || this.state.routeStack[this.state.toIndex];
- this._emitWillFocus(willFocusRoute);
- },
-
- _transitionToFromIndexWithVelocity: function(v) {
- this._configureSpring(
- this.state.sceneConfigStack[this.state.fromIndex]
- );
- this.state.isAnimating = true;
- this.spring.setVelocity(v);
- this.spring.setEndValue(0);
},
_emitDidFocus: function(route) {
@@ -608,9 +625,11 @@ var Navigator = React.createClass({
/**
* Does not delete the scenes - merely hides them.
*/
- _hideOtherScenes: function(activeIndex) {
+ _hideScenes: function() {
for (var i = 0; i < this.state.routeStack.length; i++) {
- if (i === activeIndex) {
+ // This gets called when we detach a gesture, so there will not be a
+ // current gesture, but there might be a transition in progress
+ if (i === this.state.presentedIndex || i === this.state.transitionFromIndex) {
continue;
}
var sceneRef = 'scene_' + i;
@@ -620,22 +639,31 @@ var Navigator = React.createClass({
},
_onAnimationStart: function() {
- this._setRenderSceneToHarwareTextureAndroid(this.state.fromIndex, true);
- this._setRenderSceneToHarwareTextureAndroid(this.state.toIndex, true);
+ var fromIndex = this.state.presentedIndex;
+ var toIndex = this.state.presentedIndex;
+ if (this.state.transitionFromIndex != null) {
+ fromIndex = this.state.transitionFromIndex;
+ } else if (this.state.activeGesture) {
+ toIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture);
+ }
+ this._setRenderSceneToHarwareTextureAndroid(fromIndex, true);
+ this._setRenderSceneToHarwareTextureAndroid(toIndex, true);
var navBar = this._navBar;
if (navBar && navBar.onAnimationStart) {
- navBar.onAnimationStart(this.state.fromIndex, this.state.toIndex);
+ navBar.onAnimationStart(fromIndex, toIndex);
}
},
_onAnimationEnd: function() {
- this._setRenderSceneToHarwareTextureAndroid(this.state.fromIndex, false);
- this._setRenderSceneToHarwareTextureAndroid(this.state.toIndex, false);
+ var max = this.state.routeStack.length - 1;
+ for (var index = 0; index <= max; index++) {
+ this._setRenderSceneToHarwareTextureAndroid(index, false);
+ }
var navBar = this._navBar;
if (navBar && navBar.onAnimationEnd) {
- navBar.onAnimationEnd(this.state.fromIndex, this.state.toIndex);
+ navBar.onAnimationEnd();
}
},
@@ -647,16 +675,6 @@ var Navigator = React.createClass({
viewAtIndex.setNativeProps({renderToHardwareTextureAndroid: shouldRenderToHardwareTexture});
},
- /**
- * Becomes the responder on touch start (capture) while animating so that it
- * blocks all touch interactions inside of it. However, this responder lock
- * means nothing more than that. We record if the sole reason for being
- * responder is to block interactions (`isResponderOnlyToBlockTouches`).
- */
- _handleStartShouldSetPanResponderCapture: function(e, gestureState) {
- return this.state.isAnimating;
- },
-
_handleMoveShouldSetPanResponder: function(e, gestureState) {
var currentRoute = this.state.routeStack[this.state.presentedIndex];
var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex];
@@ -674,18 +692,12 @@ var Navigator = React.createClass({
_handlePanResponderGrant: function(e, gestureState) {
invariant(
- this._expectingGestureGrant || this.state.isAnimating,
+ this._expectingGestureGrant,
'Responder granted unexpectedly.'
);
- this._activeGestureAction = this._expectingGestureGrant;
+ this._attachGesture(this._expectingGestureGrant);
+ this._onAnimationStart();
this._expectingGestureGrant = null;
- this.state.isResponderOnlyToBlockTouches = this.state.isAnimating;
- if (!this.state.isAnimating) {
- this.state.fromIndex = this.state.presentedIndex;
- var gestureSceneDelta = this._deltaForGestureAction(this._activeGestureAction);
- this.state.toIndex = this.state.presentedIndex + gestureSceneDelta;
- this._onAnimationStart();
- }
},
_deltaForGestureAction: function(gestureAction) {
@@ -703,13 +715,13 @@ var Navigator = React.createClass({
_handlePanResponderRelease: function(e, gestureState) {
var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex];
- var releaseGestureAction = this._activeGestureAction;
- this._activeGestureAction = null;
- if (this.state.isResponderOnlyToBlockTouches) {
- this.state.isResponderOnlyToBlockTouches = false;
+ var releaseGestureAction = this.state.activeGesture;
+ if (!releaseGestureAction) {
+ // The gesture may have been detached while responder, so there is no action here
return;
}
var releaseGesture = sceneConfig.gestures[releaseGestureAction];
+ var destIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture);
if (this.spring.getCurrentValue() === 0) {
// The spring is at zero, so the gesture is already complete
this.spring.setCurrentValue(0).setAtRest();
@@ -732,38 +744,86 @@ var Navigator = React.createClass({
var hasGesturedEnoughToComplete = gestureDistance > releaseGesture.fullDistance * releaseGesture.stillCompletionRatio;
transitionVelocity = hasGesturedEnoughToComplete ? releaseGesture.snapVelocity : -releaseGesture.snapVelocity;
}
- this.spring.setOvershootClampingEnabled(true);
if (transitionVelocity < 0 || this._doesGestureOverswipe(releaseGestureAction)) {
- this._transitionToFromIndexWithVelocity(transitionVelocity);
+ // This gesture is to an overswiped region or does not have enough velocity to complete
+ // If we are currently mid-transition, then this gesture was a pending gesture. Because this gesture takes no action, we can stop here
+ if (this.state.transitionFromIndex == null) {
+ // There is no current transition, so we need to transition back to the presented index
+ var transitionBackToPresentedIndex = this.state.presentedIndex;
+ // slight hack: change the presented index for a moment in order to transitionTo correctly
+ this.state.presentedIndex = destIndex;
+ this._transitionTo(
+ transitionBackToPresentedIndex,
+ - transitionVelocity,
+ 1 - this.spring.getCurrentValue()
+ );
+ }
} else {
- this._transitionToToIndexWithVelocity(transitionVelocity);
+ // The gesture has enough velocity to complete, so we transition to the gesture's destination
+ this._transitionTo(destIndex, transitionVelocity);
}
+ this._detachGesture();
},
_handlePanResponderTerminate: function(e, gestureState) {
- this._activeGestureAction = null;
- this.state.isResponderOnlyToBlockTouches = false;
- this._transitionToFromIndexWithVelocity(0);
+ var destIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture);
+ this._detachGesture();
+ var transitionBackToPresentedIndex = this.state.presentedIndex;
+ // slight hack: change the presented index for a moment in order to transitionTo correctly
+ this.state.presentedIndex = destIndex;
+ this._transitionTo(
+ transitionBackToPresentedIndex,
+ null,
+ 1 - this.spring.getCurrentValue()
+ );
+ },
+
+ _attachGesture: function(gestureId) {
+ this.state.activeGesture = gestureId;
+ },
+
+ _detachGesture: function() {
+ this.state.activeGesture = null;
+ this.state.pendingGestureProgress = null;
+ this._hideScenes();
},
_handlePanResponderMove: function(e, gestureState) {
- if (!this.state.isResponderOnlyToBlockTouches) {
- var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex];
- var gesture = sceneConfig.gestures[this._activeGestureAction];
- var isTravelVertical = gesture.direction === 'top-to-bottom' || gesture.direction === 'bottom-to-top';
- var isTravelInverted = gesture.direction === 'right-to-left' || gesture.direction === 'bottom-to-top';
- var distance = isTravelVertical ? gestureState.dy : gestureState.dx;
- distance = isTravelInverted ? - distance : distance;
- var gestureDetectMovement = gesture.gestureDetectMovement;
- var nextProgress = (distance - gestureDetectMovement) /
- (gesture.fullDistance - gestureDetectMovement);
- if (this._doesGestureOverswipe(this._activeGestureAction)) {
- var frictionConstant = gesture.overswipe.frictionConstant;
- var frictionByDistance = gesture.overswipe.frictionByDistance;
- var frictionRatio = 1 / ((frictionConstant) + (Math.abs(nextProgress) * frictionByDistance));
- nextProgress *= frictionRatio;
- }
- this.spring.setCurrentValue(clamp(0, nextProgress, 1));
+ var sceneConfig = this.state.sceneConfigStack[this.state.presentedIndex];
+ if (this.state.activeGesture) {
+ var gesture = sceneConfig.gestures[this.state.activeGesture];
+ return this._moveAttachedGesture(gesture, gestureState);
+ }
+ var matchedGesture = this._matchGestureAction(sceneConfig.gestures, gestureState);
+ if (matchedGesture) {
+ this._attachGesture(matchedGesture);
+ }
+ },
+
+ _moveAttachedGesture: function(gesture, gestureState) {
+ var isTravelVertical = gesture.direction === 'top-to-bottom' || gesture.direction === 'bottom-to-top';
+ var isTravelInverted = gesture.direction === 'right-to-left' || gesture.direction === 'bottom-to-top';
+ var distance = isTravelVertical ? gestureState.dy : gestureState.dx;
+ distance = isTravelInverted ? - distance : distance;
+ var gestureDetectMovement = gesture.gestureDetectMovement;
+ var nextProgress = (distance - gestureDetectMovement) /
+ (gesture.fullDistance - gestureDetectMovement);
+ if (nextProgress < 0 && gesture.isDetachable) {
+ this._detachGesture();
+ }
+ if (this._doesGestureOverswipe(this.state.activeGesture)) {
+ var frictionConstant = gesture.overswipe.frictionConstant;
+ var frictionByDistance = gesture.overswipe.frictionByDistance;
+ var frictionRatio = 1 / ((frictionConstant) + (Math.abs(nextProgress) * frictionByDistance));
+ nextProgress *= frictionRatio;
+ }
+ nextProgress = clamp(0, nextProgress, 1);
+ if (this.state.transitionFromIndex != null) {
+ this.state.pendingGestureProgress = nextProgress;
+ } else if (this.state.pendingGestureProgress) {
+ this.spring.setEndValue(nextProgress);
+ } else {
+ this.spring.setCurrentValue(nextProgress);
}
},
@@ -771,9 +831,6 @@ var Navigator = React.createClass({
if (!gestures) {
return null;
}
- if (this.state.isResponderOnlyToBlockTouches || this.state.isAnimating) {
- return null;
- }
var matchedGesture = null;
GESTURE_ACTIONS.some((gestureName) => {
var gesture = gestures[gestureName];
@@ -814,7 +871,7 @@ var Navigator = React.createClass({
return;
}
// Use toIndex animation when we move forwards. Use fromIndex when we move back
- var sceneConfigIndex = this.state.presentedIndex < toIndex ? toIndex : fromIndex;
+ var sceneConfigIndex = fromIndex < toIndex ? toIndex : fromIndex;
var sceneConfig = this.state.sceneConfigStack[sceneConfigIndex];
// this happens for overswiping when there is no scene at toIndex
if (!sceneConfig) {
@@ -849,10 +906,6 @@ var Navigator = React.createClass({
this.state.updatingRangeLength = this.state.routeStack.length;
},
- _canNavigate: function() {
- return !this.state.isAnimating;
- },
-
_getDestIndexWithinBounds: function(n) {
var currentIndex = this.state.presentedIndex;
var destIndex = currentIndex + n;
@@ -870,17 +923,13 @@ var Navigator = React.createClass({
_jumpN: function(n) {
var destIndex = this._getDestIndexWithinBounds(n);
- if (!this._canNavigate()) {
- return; // It's busy animating or transitioning.
- }
var requestTransitionAndResetUpdatingRange = () => {
- this._requestTransitionTo(destIndex);
+ this._transitionTo(destIndex);
this._resetUpdatingRange();
};
this.setState({
updatingRangeStart: destIndex,
updatingRangeLength: 1,
- toIndex: destIndex,
}, requestTransitionAndResetUpdatingRange);
},
@@ -903,9 +952,6 @@ var Navigator = React.createClass({
push: function(route) {
invariant(!!route, 'Must supply route to push');
- if (!this._canNavigate()) {
- return; // It's busy animating or transitioning.
- }
var activeLength = this.state.presentedIndex + 1;
var activeStack = this.state.routeStack.slice(0, activeLength);
var activeIDStack = this.state.idStack.slice(0, activeLength);
@@ -916,34 +962,34 @@ var Navigator = React.createClass({
this.props.configureScene(route),
]);
var requestTransitionAndResetUpdatingRange = () => {
- this._requestTransitionTo(nextStack.length - 1);
+ this._transitionTo(nextStack.length - 1);
this._resetUpdatingRange();
};
- var navigationState = {
- toRoute: route,
- fromRoute: this.state.routeStack[this.state.routeStack.length - 1],
- };
this.setState({
idStack: nextIDStack,
routeStack: nextStack,
sceneConfigStack: nextAnimationConfigStack,
- jumpToIndex: nextStack.length - 1,
updatingRangeStart: nextStack.length - 1,
updatingRangeLength: 1,
}, requestTransitionAndResetUpdatingRange);
},
_popN: function(n) {
- if (n === 0 || !this._canNavigate()) {
+ if (n === 0) {
return;
}
invariant(
this.state.presentedIndex - n >= 0,
'Cannot pop below zero'
);
- this.state.jumpToIndex = this.state.presentedIndex - n;
- this._requestTransitionTo(
- this.state.presentedIndex - n
+ var popIndex = this.state.presentedIndex - n;
+ this._transitionTo(
+ popIndex,
+ null, // default velocity
+ null, // no spring jumping
+ () => {
+ this._cleanScenesPastIndex(popIndex);
+ }
);
},
@@ -957,7 +1003,7 @@ var Navigator = React.createClass({
* `index` specifies the route in the stack that should be replaced.
* If it's negative, it counts from the back.
*/
- replaceAtIndex: function(route, index) {
+ replaceAtIndex: function(route, index, cb) {
invariant(!!route, 'Must supply route to replace');
if (index < 0) {
index += this.state.routeStack.length;
@@ -988,6 +1034,7 @@ var Navigator = React.createClass({
this._emitWillFocus(route);
this._emitDidFocus(route);
}
+ cb && cb();
});
},
@@ -1024,7 +1071,7 @@ var Navigator = React.createClass({
},
replacePreviousAndPop: function(route) {
- if (this.state.routeStack.length < 2 || !this._canNavigate()) {
+ if (this.state.routeStack.length < 2) {
return;
}
this.replacePrevious(route);
@@ -1033,10 +1080,9 @@ var Navigator = React.createClass({
resetTo: function(route) {
invariant(!!route, 'Must supply route to push');
- if (this._canNavigate()) {
- this.replaceAtIndex(route, 0);
+ this.replaceAtIndex(route, 0, () => {
this.popToRoute(route);
- }
+ });
},
getCurrentRoutes: function() {
@@ -1052,8 +1098,8 @@ var Navigator = React.createClass({
this.props.onItemRef && this.props.onItemRef(ref, itemIndex);
},
- _removePoppedRoutes: function() {
- var newStackLength = this.state.jumpToIndex + 1;
+ _cleanScenesPastIndex: function(index) {
+ var newStackLength = index + 1;
// Remove any unneeded rendered routes.
if (newStackLength < this.state.routeStack.length) {
var updatingRangeStart = newStackLength; // One past the top
diff --git a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js
index fa62b3f455baf6..a43f2dafdebba9 100644
--- a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js
+++ b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js
@@ -33,6 +33,8 @@ var StaticContainer = require('StaticContainer.react');
var StyleSheet = require('StyleSheet');
var View = require('View');
+var invariant = require('invariant');
+
var Interpolators = NavigatorBreadcrumbNavigationBarStyles.Interpolators;
var PropTypes = React.PropTypes;
@@ -99,6 +101,10 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({
var oldDistToCenter = index - fromIndex;
var newDistToCenter = index - toIndex;
var interpolate;
+ invariant(
+ Interpolators[index],
+ 'Cannot find breadcrumb interpolators for ' + index
+ );
if (oldDistToCenter > 0 && newDistToCenter === 0 ||
newDistToCenter > 0 && oldDistToCenter === 0) {
interpolate = Interpolators[index].RightToCenter;
@@ -146,10 +152,9 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({
}
},
- onAnimationEnd: function(fromIndex, toIndex) {
- var max = Math.max(fromIndex, toIndex);
- var min = Math.min(fromIndex, toIndex);
- for (var index = min; index <= max; index++) {
+ onAnimationEnd: function() {
+ var max = this.props.navState.routeStack.length - 1;
+ for (var index = 0; index <= max; index++) {
this._setRenderViewsToHardwareTextureAndroid(index, false);
}
},
diff --git a/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js b/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js
index ac16542adc2ea7..3b4666b9388ce5 100644
--- a/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js
+++ b/Libraries/CustomComponents/Navigator/NavigatorSceneConfigs.js
@@ -101,6 +101,30 @@ var FadeToTheLeft = {
},
};
+var FadeIn = {
+ opacity: {
+ from: 0,
+ to: 1,
+ min: 0.5,
+ max: 1,
+ type: 'linear',
+ extrapolate: false,
+ round: 100,
+ },
+};
+
+var FadeOut = {
+ opacity: {
+ from: 1,
+ to: 0,
+ min: 0,
+ max: 0.5,
+ type: 'linear',
+ extrapolate: false,
+ round: 100,
+ },
+};
+
var ToTheLeft = {
transformTranslate: {
from: {x: 0, y: 0, z: 0},
@@ -115,8 +139,17 @@ var ToTheLeft = {
value: 1.0,
type: 'constant',
},
-};
+ translateX: {
+ from: 0,
+ to: -Dimensions.get('window').width,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+};
var FromTheRight = {
opacity: {
@@ -236,6 +269,43 @@ var FromTheFront = {
},
};
+var ToTheBackAndroid = {
+ opacity: {
+ value: 1,
+ type: 'constant',
+ },
+};
+
+var FromTheFrontAndroid = {
+ opacity: {
+ from: 0,
+ to: 1,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: false,
+ round: 100,
+ },
+ transformTranslate: {
+ from: {x: 0, y: 50, z: 0},
+ to: {x: 0, y: 0, z: 0},
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+ translateY: {
+ from: 50,
+ to: 0,
+ min: 0,
+ max: 1,
+ type: 'linear',
+ extrapolate: true,
+ round: PixelRatio.get(),
+ },
+};
+
var BaseOverswipeConfig = {
frictionConstant: 1,
frictionByDistance: 1.5,
@@ -243,6 +313,9 @@ var BaseOverswipeConfig = {
var BaseLeftToRightGesture = {
+ // If the gesture can end and restart during one continuous touch
+ isDetachable: false,
+
// How far the swipe must drag to start transitioning
gestureDetectMovement: 2,
@@ -316,6 +389,22 @@ var NavigatorSceneConfigs = {
out: buildStyleInterpolator(ToTheBack),
},
},
+ FloatFromBottomAndroid: {
+ ...BaseConfig,
+ gestures: null,
+ animationInterpolators: {
+ into: buildStyleInterpolator(FromTheFrontAndroid),
+ out: buildStyleInterpolator(ToTheBackAndroid),
+ },
+ },
+ FadeAndroid: {
+ ...BaseConfig,
+ gestures: null,
+ animationInterpolators: {
+ into: buildStyleInterpolator(FadeIn),
+ out: buildStyleInterpolator(FadeOut),
+ },
+ },
HorizontalSwipeJump: {
...BaseConfig,
gestures: {
@@ -323,11 +412,13 @@ var NavigatorSceneConfigs = {
...BaseLeftToRightGesture,
overswipe: BaseOverswipeConfig,
edgeHitWidth: null,
+ isDetachable: true,
},
jumpForward: {
...BaseRightToLeftGesture,
overswipe: BaseOverswipeConfig,
edgeHitWidth: null,
+ isDetachable: true,
},
},
animationInterpolators: {
diff --git a/Libraries/Geolocation/Geolocation.js b/Libraries/Geolocation/Geolocation.js
index 13fe40a2364447..fae309aef5fe68 100644
--- a/Libraries/Geolocation/Geolocation.js
+++ b/Libraries/Geolocation/Geolocation.js
@@ -22,6 +22,12 @@ var subscriptions = [];
var updatesEnabled = false;
+type GeoOptions = {
+ timeout: number;
+ maximumAge: number;
+ enableHighAccuracy: bool;
+}
+
/**
* You need to include the `NSLocationWhenInUseUsageDescription` key
* in Info.plist to enable geolocation. Geolocation is enabled by default
@@ -32,10 +38,14 @@ var updatesEnabled = false;
*/
var Geolocation = {
+ /*
+ * Invokes the success callback once with the latest location info. Supported
+ * options: timeout (ms), maximumAge (ms), enableHighAccuracy (bool)
+ */
getCurrentPosition: function(
geo_success: Function,
geo_error?: Function,
- geo_options?: Object
+ geo_options?: GeoOptions
) {
invariant(
typeof geo_success === 'function',
@@ -48,7 +58,11 @@ var Geolocation = {
);
},
- watchPosition: function(success: Function, error?: Function, options?: Object): number {
+ /*
+ * Invokes the success callback whenever the location changes. Supported
+ * options: timeout (ms), maximumAge (ms), enableHighAccuracy (bool)
+ */
+ watchPosition: function(success: Function, error?: Function, options?: GeoOptions): number {
if (!updatesEnabled) {
RCTLocationObserver.startObserving(options || {});
updatesEnabled = true;
diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m
index 5d56caccbe314b..3e864657b82c5d 100644
--- a/Libraries/Geolocation/RCTLocationObserver.m
+++ b/Libraries/Geolocation/RCTLocationObserver.m
@@ -163,12 +163,12 @@ - (void)timeout:(NSTimer *)timer
#pragma mark - Public API
-RCT_EXPORT_METHOD(startObserving:(NSDictionary *)optionsJSON)
+RCT_EXPORT_METHOD(startObserving:(RCTLocationOptions)options)
{
[self checkLocationConfig];
// Select best options
- _observerOptions = [RCTConvert RCTLocationOptions:optionsJSON];
+ _observerOptions = options;
for (RCTLocationRequest *request in _pendingRequests) {
_observerOptions.accuracy = MIN(_observerOptions.accuracy, request.options.accuracy);
}
@@ -189,7 +189,7 @@ - (void)timeout:(NSTimer *)timer
}
}
-RCT_EXPORT_METHOD(getCurrentPosition:(NSDictionary *)optionsJSON
+RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options
withSuccessCallback:(RCTResponseSenderBlock)successBlock
errorCallback:(RCTResponseSenderBlock)errorBlock)
{
@@ -219,7 +219,6 @@ - (void)timeout:(NSTimer *)timer
}
// Check if previous recorded location exists and is good enough
- RCTLocationOptions options = [RCTConvert RCTLocationOptions:optionsJSON];
if (_lastLocationEvent &&
CFAbsoluteTimeGetCurrent() - [RCTConvert NSTimeInterval:_lastLocationEvent[@"timestamp"]] < options.maximumAge &&
[_lastLocationEvent[@"coords"][@"accuracy"] doubleValue] >= options.accuracy) {
diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js
index e917b6b637355b..a41352f44ce4b0 100644
--- a/Libraries/Image/Image.ios.js
+++ b/Libraries/Image/Image.ios.js
@@ -31,7 +31,7 @@ var verifyPropTypes = require('verifyPropTypes');
var warning = require('warning');
/**
- * A react component for displaying different types of images,
+ * A React component for displaying different types of images,
* including network images, static resources, temporary local images, and
* images from local disk, such as the camera roll.
*
diff --git a/Libraries/Image/__tests__/resolveAssetSource-test.js b/Libraries/Image/__tests__/resolveAssetSource-test.js
index 26f3c9ea32e999..632ff96567ee1c 100644
--- a/Libraries/Image/__tests__/resolveAssetSource-test.js
+++ b/Libraries/Image/__tests__/resolveAssetSource-test.js
@@ -32,6 +32,12 @@ describe('resolveAssetSource', () => {
expect(resolveAssetSource(source2)).toBe(source2);
});
+ it('ignores any weird data', () => {
+ expect(resolveAssetSource(null)).toBe(null);
+ expect(resolveAssetSource(42)).toBe(null);
+ expect(resolveAssetSource('nonsense')).toBe(null);
+ });
+
describe('bundle was loaded from network', () => {
beforeEach(() => {
SourceCode.scriptURL = 'http://10.0.0.1:8081/main.bundle';
diff --git a/Libraries/Image/resolveAssetSource.js b/Libraries/Image/resolveAssetSource.js
index da136e9a73d583..02189155877006 100644
--- a/Libraries/Image/resolveAssetSource.js
+++ b/Libraries/Image/resolveAssetSource.js
@@ -43,10 +43,11 @@ function pickScale(scales, deviceScale) {
return scales[scales.length - 1] || 1;
}
-// TODO(frantic):
-// * Pick best scale and append @Nx to file path
-// * We are currently using httpServerLocation for both http and in-app bundle
function resolveAssetSource(source) {
+ if (!source || typeof source !== 'object') {
+ return null;
+ }
+
if (!source.__packager_asset) {
return source;
}
diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js
index c5476eaab5cfe3..b5868139c7c135 100644
--- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js
+++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js
@@ -16,6 +16,7 @@ var RCTExceptionsManager = require('NativeModules').ExceptionsManager;
var loadSourceMap = require('loadSourceMap');
var parseErrorStack = require('parseErrorStack');
+var stringifySafe = require('stringifySafe');
var sourceMapPromise;
@@ -25,17 +26,11 @@ type Exception = {
message: string;
}
-function handleException(e: Exception) {
- var stack = parseErrorStack(e);
- console.error(
- 'Err0r: ' +
- '\n stack: \n' + stackToString(stack) +
- '\n URL: ' + e.sourceURL +
- '\n line: ' + e.line +
- '\n message: ' + e.message
- );
-
+function reportException(e: Exception, stack?: any) {
if (RCTExceptionsManager) {
+ if (!stack) {
+ stack = parseErrorStack(e);
+ }
RCTExceptionsManager.reportUnhandledException(e.message, stack);
if (__DEV__) {
(sourceMapPromise = sourceMapPromise || loadSourceMap())
@@ -50,6 +45,47 @@ function handleException(e: Exception) {
}
}
+function handleException(e: Exception) {
+ var stack = parseErrorStack(e);
+ var msg =
+ 'Error: ' + e.message +
+ '\n stack: \n' + stackToString(stack) +
+ '\n URL: ' + e.sourceURL +
+ '\n line: ' + e.line +
+ '\n message: ' + e.message;
+ if (console.errorOriginal) {
+ console.errorOriginal(msg);
+ } else {
+ console.error(msg);
+ }
+ reportException(e, stack);
+}
+
+/**
+ * Shows a redbox with stacktrace for all console.error messages. Disable by
+ * setting `console.reportErrorsAsExceptions = false;` in your app.
+ */
+function installConsoleErrorReporter() {
+ if (console.reportException) {
+ return; // already installed
+ }
+ console.reportException = reportException;
+ console.errorOriginal = console.error.bind(console);
+ console.error = function reactConsoleError() {
+ console.errorOriginal.apply(null, arguments);
+ if (!console.reportErrorsAsExceptions) {
+ return;
+ }
+ var str = Array.prototype.map.call(arguments, stringifySafe).join(', ');
+ var error: any = new Error('console.error: ' + str);
+ error.framesToPop = 1;
+ reportException(error);
+ };
+ if (console.reportErrorsAsExceptions === undefined) {
+ console.reportErrorsAsExceptions = true; // Individual apps can disable this
+ }
+}
+
function stackToString(stack) {
var maxLength = Math.max.apply(null, stack.map(frame => frame.methodName.length));
return stack.map(frame => stackFrameToString(frame, maxLength)).join('\n');
@@ -71,4 +107,4 @@ function fillSpaces(n) {
return new Array(n + 1).join(' ');
}
-module.exports = { handleException };
+module.exports = { handleException, installConsoleErrorReporter };
diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTDataManager.m
index 634d325e913359..1d0a793de8e454 100644
--- a/Libraries/Network/RCTDataManager.m
+++ b/Libraries/Network/RCTDataManager.m
@@ -61,7 +61,7 @@ @implementation RCTDataManager
responseJSON = @{
@"status": @0,
@"responseHeaders": @{},
- @"responseText": [connectionError localizedDescription]
+ @"responseText": [connectionError localizedDescription] ?: [NSNull null]
};
}
diff --git a/Libraries/ReactIOS/ReactIOSComponentMixin.js b/Libraries/ReactIOS/ReactIOSComponentMixin.js
index 94c708a433e863..03d7f5ad04fc3b 100644
--- a/Libraries/ReactIOS/ReactIOSComponentMixin.js
+++ b/Libraries/ReactIOS/ReactIOSComponentMixin.js
@@ -46,7 +46,7 @@ var ReactInstanceMap = require('ReactInstanceMap');
*
* `mountImage`: A way to represent the potential to create lower level
* resources whos `nodeHandle` can be discovered immediately by knowing the
- * `rootNodeID`. Today's web react represents this with `innerHTML` annotated
+ * `rootNodeID`. Today's web React represents this with `innerHTML` annotated
* with DOM ids that match the `rootNodeID`.
*
* Opaque name TodaysWebReact FutureWebWorkerReact ReactNative
diff --git a/Libraries/ReactIOS/ReactIOSMount.js b/Libraries/ReactIOS/ReactIOSMount.js
index ab46d6fe9d3144..9b1428fdd6d27a 100644
--- a/Libraries/ReactIOS/ReactIOSMount.js
+++ b/Libraries/ReactIOS/ReactIOSMount.js
@@ -191,7 +191,7 @@ var ReactIOSMount = {
* that has been rendered and unmounting it. There should just be one child
* component at this time.
*/
- unmountComponentAtNode: function(containerTag: number): bool {
+ unmountComponentAtNode: function(containerTag: number): boolean {
if (!ReactIOSTagHandles.reactTagIsNativeTopRootID(containerTag)) {
console.error('You cannot render into anything but a top root');
return false;
diff --git a/Libraries/ReactIOS/WarningBox.js b/Libraries/ReactIOS/WarningBox.js
new file mode 100644
index 00000000000000..37076ef5c299fe
--- /dev/null
+++ b/Libraries/ReactIOS/WarningBox.js
@@ -0,0 +1,398 @@
+/**
+ * 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 WarningBox
+ */
+'use strict';
+
+var AsyncStorage = require('AsyncStorage');
+var EventEmitter = require('EventEmitter');
+var Map = require('Map');
+var PanResponder = require('PanResponder');
+var React = require('React');
+var StyleSheet = require('StyleSheet');
+var Text = require('Text');
+var TouchableOpacity = require('TouchableOpacity');
+var View = require('View');
+
+var invariant = require('invariant');
+var rebound = require('rebound');
+var stringifySafe = require('stringifySafe');
+
+var SCREEN_WIDTH = require('Dimensions').get('window').width;
+var IGNORED_WARNINGS_KEY = '__DEV_WARNINGS_IGNORED';
+
+var consoleWarn = console.warn.bind(console);
+
+var warningCounts = new Map();
+var ignoredWarnings: Array = [];
+var totalWarningCount = 0;
+var warningCountEvents = new EventEmitter();
+
+/**
+ * WarningBox renders warnings on top of the app being developed. Warnings help
+ * guard against subtle yet significant issues that can impact the quality of
+ * your application, such as accessibility and memory leaks. This "in your
+ * face" style of warning allows developers to notice and correct these issues
+ * as quickly as possible.
+ *
+ * The warning box is currently opt-in. Set the following flag to enable it:
+ *
+ * `console.yellowBoxEnabled = true;`
+ *
+ * If "ignore" is tapped on a warning, the WarningBox will record that warning
+ * and will not display it again. This is useful for hiding errors that already
+ * exist or have been introduced elsewhere. To re-enable all of the errors, set
+ * the following:
+ *
+ * `console.yellowBoxResetIgnored = true;`
+ *
+ * This can also be set permanently, and ignore will only silence the warnings
+ * until the next refresh.
+ */
+
+if (__DEV__) {
+ console.warn = function() {
+ consoleWarn.apply(null, arguments);
+ if (!console.yellowBoxEnabled) {
+ return;
+ }
+ var warning = Array.prototype.map.call(arguments, stringifySafe).join(' ');
+ if (!console.yellowBoxResetIgnored &&
+ ignoredWarnings.indexOf(warning) !== -1) {
+ return;
+ }
+ var count = warningCounts.has(warning) ? warningCounts.get(warning) + 1 : 1;
+ warningCounts.set(warning, count);
+ totalWarningCount += 1;
+ warningCountEvents.emit('count', totalWarningCount);
+ };
+}
+
+function saveIgnoredWarnings() {
+ AsyncStorage.setItem(
+ IGNORED_WARNINGS_KEY,
+ JSON.stringify(ignoredWarnings),
+ function(err) {
+ if (err) {
+ console.warn('Could not save ignored warnings.', err);
+ }
+ }
+ );
+}
+
+AsyncStorage.getItem(IGNORED_WARNINGS_KEY, function(err, data) {
+ if (!err && data && !console.yellowBoxResetIgnored) {
+ ignoredWarnings = JSON.parse(data);
+ }
+});
+
+var WarningRow = React.createClass({
+ componentWillMount: function() {
+ this.springSystem = new rebound.SpringSystem();
+ this.dismissalSpring = this.springSystem.createSpring();
+ this.dismissalSpring.setRestSpeedThreshold(0.05);
+ this.dismissalSpring.setCurrentValue(0);
+ this.dismissalSpring.addListener({
+ onSpringUpdate: () => {
+ var val = this.dismissalSpring.getCurrentValue();
+ this.text && this.text.setNativeProps({
+ left: SCREEN_WIDTH * val,
+ });
+ this.container && this.container.setNativeProps({
+ opacity: 1 - val,
+ });
+ this.closeButton && this.closeButton.setNativeProps({
+ opacity: 1 - (val * 5),
+ });
+ },
+ onSpringAtRest: () => {
+ if (this.dismissalSpring.getCurrentValue()) {
+ this.collapseSpring.setEndValue(1);
+ }
+ },
+ });
+ this.collapseSpring = this.springSystem.createSpring();
+ this.collapseSpring.setRestSpeedThreshold(0.05);
+ this.collapseSpring.setCurrentValue(0);
+ this.collapseSpring.getSpringConfig().friction = 20;
+ this.collapseSpring.getSpringConfig().tension = 200;
+ this.collapseSpring.addListener({
+ onSpringUpdate: () => {
+ var val = this.collapseSpring.getCurrentValue();
+ this.container && this.container.setNativeProps({
+ height: Math.abs(46 - (val * 46)),
+ });
+ },
+ onSpringAtRest: () => {
+ this.props.onDismissed();
+ },
+ });
+ this.panGesture = PanResponder.create({
+ onStartShouldSetPanResponder: () => {
+ return !! this.dismissalSpring.getCurrentValue();
+ },
+ onMoveShouldSetPanResponder: () => true,
+ onPanResponderGrant: () => {
+ this.isResponderOnlyToBlockTouches =
+ !! this.dismissalSpring.getCurrentValue();
+ },
+ onPanResponderMove: (e, gestureState) => {
+ if (this.isResponderOnlyToBlockTouches) {
+ return;
+ }
+ this.dismissalSpring.setCurrentValue(gestureState.dx / SCREEN_WIDTH);
+ },
+ onPanResponderRelease: (e, gestureState) => {
+ if (this.isResponderOnlyToBlockTouches) {
+ return;
+ }
+ var gestureCompletion = gestureState.dx / SCREEN_WIDTH;
+ var doesGestureRelease = (gestureState.vx + gestureCompletion) > 0.5;
+ this.dismissalSpring.setEndValue(doesGestureRelease ? 1 : 0);
+ }
+ });
+ },
+ render: function() {
+ var countText;
+ if (warningCounts.get(this.props.warning) > 1) {
+ countText = (
+
+ ({warningCounts.get(this.props.warning)}){" "}
+
+ );
+ }
+ return (
+ { this.container = container; }}
+ {...this.panGesture.panHandlers}>
+
+
+ { this.text = text; }}>
+ {countText}
+ {this.props.warning}
+
+
+
+ { this.closeButton = closeButton; }}
+ style={styles.closeButton}>
+ {
+ this.dismissalSpring.setEndValue(1);
+ }}>
+ ✕
+
+
+
+ );
+ }
+});
+
+var WarningBoxOpened = React.createClass({
+ render: function() {
+ var countText;
+ if (warningCounts.get(this.props.warning) > 1) {
+ countText = (
+
+ ({warningCounts.get(this.props.warning)}){" "}
+
+ );
+ }
+ return (
+
+
+
+ {countText}
+ {this.props.warning}
+
+
+
+
+
+ Dismiss
+
+
+
+
+
+
+ Ignore
+
+
+
+
+
+
+ );
+ },
+});
+
+var canMountWarningBox = true;
+
+var WarningBox = React.createClass({
+ getInitialState: function() {
+ return {
+ totalWarningCount,
+ openWarning: null,
+ };
+ },
+ componentWillMount: function() {
+ if (console.yellowBoxResetIgnored) {
+ AsyncStorage.setItem(IGNORED_WARNINGS_KEY, '[]', function(err) {
+ if (err) {
+ console.warn('Could not reset ignored warnings.', err);
+ }
+ });
+ ignoredWarnings = [];
+ }
+ },
+ componentDidMount: function() {
+ invariant(
+ canMountWarningBox,
+ 'There can only be one WarningBox'
+ );
+ canMountWarningBox = false;
+ warningCountEvents.addListener(
+ 'count',
+ this._onWarningCount
+ );
+ },
+ componentWillUnmount: function() {
+ warningCountEvents.removeAllListeners();
+ canMountWarningBox = true;
+ },
+ _onWarningCount: function(totalWarningCount) {
+ // Must use setImmediate because warnings often happen during render and
+ // state cannot be set while rendering
+ setImmediate(() => {
+ this.setState({ totalWarningCount, });
+ });
+ },
+ _onDismiss: function(warning) {
+ warningCounts.delete(warning);
+ this.setState({
+ openWarning: null,
+ });
+ },
+ render: function() {
+ if (warningCounts.size === 0) {
+ return ;
+ }
+ if (this.state.openWarning) {
+ return (
+ {
+ this.setState({ openWarning: null });
+ }}
+ onDismissed={this._onDismiss.bind(this, this.state.openWarning)}
+ onIgnored={() => {
+ ignoredWarnings.push(this.state.openWarning);
+ saveIgnoredWarnings();
+ this._onDismiss(this.state.openWarning);
+ }}
+ />
+ );
+ }
+ var warningRows = [];
+ warningCounts.forEach((count, warning) => {
+ warningRows.push(
+ {
+ this.setState({ openWarning: warning });
+ }}
+ onDismissed={this._onDismiss.bind(this, warning)}
+ warning={warning}
+ />
+ );
+ });
+ return (
+
+ {warningRows}
+
+ );
+ },
+});
+
+var styles = StyleSheet.create({
+ bold: {
+ fontWeight: 'bold',
+ },
+ closeButton: {
+ position: 'absolute',
+ right: 0,
+ height: 46,
+ width: 46,
+ },
+ closeButtonText: {
+ color: 'white',
+ fontSize: 32,
+ position: 'relative',
+ left: 8,
+ },
+ warningContainer: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0
+ },
+ warningBox: {
+ position: 'relative',
+ backgroundColor: 'rgba(171, 124, 36, 0.9)',
+ flex: 1,
+ height: 46,
+ },
+ warningText: {
+ color: 'white',
+ position: 'absolute',
+ left: 0,
+ marginLeft: 15,
+ marginRight: 46,
+ top: 7,
+ },
+ yellowBox: {
+ backgroundColor: 'rgba(171, 124, 36, 0.9)',
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ padding: 15,
+ paddingTop: 35,
+ },
+ yellowBoxText: {
+ color: 'white',
+ fontSize: 20,
+ },
+ yellowBoxButtons: {
+ flexDirection: 'row',
+ position: 'absolute',
+ bottom: 0,
+ },
+ yellowBoxButton: {
+ flex: 1,
+ padding: 25,
+ },
+ yellowBoxButtonText: {
+ color: 'white',
+ fontSize: 16,
+ }
+});
+
+module.exports = WarningBox;
diff --git a/Libraries/ReactIOS/renderApplication.ios.js b/Libraries/ReactIOS/renderApplication.ios.js
index 084390ac50057d..39e5720f520eb9 100644
--- a/Libraries/ReactIOS/renderApplication.ios.js
+++ b/Libraries/ReactIOS/renderApplication.ios.js
@@ -11,7 +11,12 @@
*/
'use strict';
+require('ExceptionsManager').installConsoleErrorReporter();
+
var React = require('React');
+var StyleSheet = require('StyleSheet');
+var View = require('View');
+var WarningBox = require('WarningBox');
var invariant = require('invariant');
@@ -24,12 +29,27 @@ function renderApplication(
rootTag,
'Expect to have a valid rootTag, instead got ', rootTag
);
+ var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled;
+ var warningBox = shouldRenderWarningBox ? : null;
React.render(
- ,
+
+
+ {warningBox}
+ ,
rootTag
);
}
+var styles = StyleSheet.create({
+ appContainer: {
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ },
+});
+
module.exports = renderApplication;
diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj
index 3c4bcf5bae846f..224c7e6b97fa7e 100644
--- a/Libraries/Text/RCTText.xcodeproj/project.pbxproj
+++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
+ 131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */; };
+ 131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */; };
58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */; };
58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C91A9E6C5C00147676 /* RCTShadowRawText.m */; };
58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; };
@@ -27,6 +29,10 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 131B6ABC1AF0CD0600FFC3E0 /* RCTTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextView.h; sourceTree = ""; };
+ 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextView.m; sourceTree = ""; };
+ 131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextViewManager.h; sourceTree = ""; };
+ 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextViewManager.m; sourceTree = ""; };
58B5119B1A9E6C1200147676 /* libRCTText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTText.a; sourceTree = BUILT_PRODUCTS_DIR; };
58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRawTextManager.h; sourceTree = ""; };
58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRawTextManager.m; sourceTree = ""; };
@@ -64,6 +70,10 @@
58B512141A9E6EFF00147676 /* RCTText.m */,
58B511CC1A9E6C5C00147676 /* RCTTextManager.h */,
58B511CD1A9E6C5C00147676 /* RCTTextManager.m */,
+ 131B6ABC1AF0CD0600FFC3E0 /* RCTTextView.h */,
+ 131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */,
+ 131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */,
+ 131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */,
58B5119C1A9E6C1200147676 /* Products */,
);
indentWidth = 2;
@@ -135,8 +145,10 @@
buildActionMask = 2147483647;
files = (
58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */,
+ 131B6AC01AF0CD0600FFC3E0 /* RCTTextView.m in Sources */,
58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */,
58B512161A9E6EFF00147676 /* RCTText.m in Sources */,
+ 131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */,
58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */,
58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */,
);
diff --git a/Libraries/Text/RCTTextManager.h b/Libraries/Text/RCTTextManager.h
index 13e8f854642da4..91ac87ba88fdbc 100644
--- a/Libraries/Text/RCTTextManager.h
+++ b/Libraries/Text/RCTTextManager.h
@@ -12,4 +12,3 @@
@interface RCTTextManager : RCTViewManager
@end
-
diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m
index e5e9ad00a1ad23..ef518d20483d70 100644
--- a/Libraries/Text/RCTTextManager.m
+++ b/Libraries/Text/RCTTextManager.m
@@ -123,7 +123,7 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowVie
UIEdgeInsets padding = shadowView.paddingAsInsets;
return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
- RCTText *text = (RCTText *)viewRegistry[reactTag];
+ RCTText *text = viewRegistry[reactTag];
text.contentInset = padding;
text.layoutManager = shadowView.layoutManager;
text.textContainer = shadowView.textContainer;
diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h
new file mode 100644
index 00000000000000..19f2fea397b8c5
--- /dev/null
+++ b/Libraries/Text/RCTTextView.h
@@ -0,0 +1,29 @@
+/**
+ * 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
+
+#import "RCTView.h"
+#import "UIView+React.h"
+
+@class RCTEventDispatcher;
+
+@interface RCTTextView : RCTView
+
+@property (nonatomic, assign) BOOL autoCorrect;
+@property (nonatomic, assign) BOOL clearTextOnFocus;
+@property (nonatomic, assign) BOOL selectTextOnFocus;
+@property (nonatomic, assign) UIEdgeInsets contentInset;
+@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
+@property (nonatomic, strong) UIColor *placeholderTextColor;
+@property (nonatomic, assign) UIFont *font;
+
+- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
+
+@end
diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m
new file mode 100644
index 00000000000000..c5947f317e0bd8
--- /dev/null
+++ b/Libraries/Text/RCTTextView.m
@@ -0,0 +1,216 @@
+/**
+ * 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 "RCTTextView.h"
+
+#import "RCTConvert.h"
+#import "RCTEventDispatcher.h"
+#import "RCTUtils.h"
+#import "UIView+React.h"
+
+@implementation RCTTextView
+{
+ RCTEventDispatcher *_eventDispatcher;
+ BOOL _jsRequestingFirstResponder;
+ NSString *_placeholder;
+ UITextView *_placeholderView;
+ UITextView *_textView;
+}
+
+- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
+{
+ if ((self = [super initWithFrame:CGRectZero])) {
+ _contentInset = UIEdgeInsetsZero;
+ _eventDispatcher = eventDispatcher;
+ _placeholderTextColor = [self defaultPlaceholderTextColor];
+
+ _textView = [[UITextView alloc] initWithFrame:self.bounds];
+ _textView.backgroundColor = [UIColor clearColor];
+ _textView.delegate = self;
+ [self addSubview:_textView];
+ }
+
+ return self;
+}
+
+- (void)updateFrames
+{
+ // Adjust the insets so that they are as close as possible to single-line
+ // RCTTextField defaults
+ UIEdgeInsets adjustedInset = (UIEdgeInsets){
+ _contentInset.top - 5, _contentInset.left - 4,
+ _contentInset.bottom, _contentInset.right
+ };
+
+ [_textView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)];
+ [_placeholderView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)];
+}
+
+- (void)updatePlaceholder
+{
+ [_placeholderView removeFromSuperview];
+ _placeholderView = nil;
+
+ if (_placeholder) {
+ _placeholderView = [[UITextView alloc] initWithFrame:self.bounds];
+ _placeholderView.backgroundColor = [UIColor clearColor];
+ _placeholderView.scrollEnabled = false;
+ _placeholderView.attributedText =
+ [[NSAttributedString alloc] initWithString:_placeholder attributes:@{
+ NSFontAttributeName : (_textView.font ? _textView.font : [self defaultPlaceholderFont]),
+ NSForegroundColorAttributeName : _placeholderTextColor
+ }];
+
+ [self insertSubview:_placeholderView belowSubview:_textView];
+ [self _setPlaceholderVisibility];
+ }
+}
+
+- (void)setFont:(UIFont *)font
+{
+ _font = font;
+ _textView.font = _font;
+ [self updatePlaceholder];
+}
+
+- (void)setTextColor:(UIColor *)textColor
+{
+ _textView.textColor = textColor;
+}
+
+- (void)setPlaceholder:(NSString *)placeholder
+{
+ _placeholder = placeholder;
+ [self updatePlaceholder];
+}
+
+- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor
+{
+ if (placeholderTextColor) {
+ _placeholderTextColor = placeholderTextColor;
+ } else {
+ _placeholderTextColor = [self defaultPlaceholderTextColor];
+ }
+ [self updatePlaceholder];
+}
+
+- (void)setContentInset:(UIEdgeInsets)contentInset
+{
+ _contentInset = contentInset;
+ [self updateFrames];
+}
+
+- (void)setText:(NSString *)text
+{
+ if (![text isEqualToString:_textView.text]) {
+ [_textView setText:text];
+ [self _setPlaceholderVisibility];
+ }
+}
+
+- (void)_setPlaceholderVisibility
+{
+ if (_textView.text.length > 0) {
+ [_placeholderView setHidden:YES];
+ } else {
+ [_placeholderView setHidden:NO];
+ }
+}
+
+- (void)setAutoCorrect:(BOOL)autoCorrect
+{
+ _textView.autocorrectionType = (autoCorrect ? UITextAutocorrectionTypeYes : UITextAutocorrectionTypeNo);
+}
+
+- (BOOL)autoCorrect
+{
+ return _textView.autocorrectionType == UITextAutocorrectionTypeYes;
+}
+
+- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
+{
+ if (_selectTextOnFocus) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [textView selectAll:nil];
+ });
+ }
+ return YES;
+}
+
+- (void)textViewDidBeginEditing:(UITextView *)textView
+{
+ if (_clearTextOnFocus) {
+ [_textView setText:@""];
+ _textView.text = @"";
+ [self _setPlaceholderVisibility];
+ }
+
+ [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
+ reactTag:self.reactTag
+ text:textView.text];
+}
+
+- (void)textViewDidChange:(UITextView *)textView
+{
+ [self _setPlaceholderVisibility];
+ [_eventDispatcher sendTextEventWithType:RCTTextEventTypeChange
+ reactTag:self.reactTag
+ text:textView.text];
+
+}
+
+- (void)textViewDidEndEditing:(UITextView *)textView
+{
+ [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
+ reactTag:self.reactTag
+ text:textView.text];
+}
+
+- (BOOL)becomeFirstResponder
+{
+ _jsRequestingFirstResponder = YES;
+ BOOL result = [_textView becomeFirstResponder];
+ _jsRequestingFirstResponder = NO;
+ return result;
+}
+
+- (BOOL)resignFirstResponder
+{
+ [super resignFirstResponder];
+ BOOL result = [_textView resignFirstResponder];
+ if (result) {
+ [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
+ reactTag:self.reactTag
+ text:_textView.text];
+ }
+ return result;
+}
+
+- (void)layoutSubviews
+{
+ [super layoutSubviews];
+ [self updateFrames];
+}
+
+- (BOOL)canBecomeFirstResponder
+{
+ return _jsRequestingFirstResponder;
+}
+
+- (UIFont *)defaultPlaceholderFont
+{
+ return [UIFont fontWithName:@"Helvetica" size:17];
+}
+
+- (UIColor *)defaultPlaceholderTextColor
+{
+ return [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22];
+}
+
+@end
diff --git a/Libraries/Text/RCTTextViewManager.h b/Libraries/Text/RCTTextViewManager.h
new file mode 100644
index 00000000000000..fd2f2b44d3d334
--- /dev/null
+++ b/Libraries/Text/RCTTextViewManager.h
@@ -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 "RCTViewManager.h"
+
+@interface RCTTextViewManager : RCTViewManager
+
+@end
diff --git a/Libraries/Text/RCTTextViewManager.m b/Libraries/Text/RCTTextViewManager.m
new file mode 100644
index 00000000000000..570a511157bee5
--- /dev/null
+++ b/Libraries/Text/RCTTextViewManager.m
@@ -0,0 +1,65 @@
+/**
+ * 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 "RCTTextViewManager.h"
+
+#import "RCTBridge.h"
+#import "RCTConvert.h"
+#import "RCTShadowView.h"
+#import "RCTSparseArray.h"
+#import "RCTTextView.h"
+
+@implementation RCTTextViewManager
+
+RCT_EXPORT_MODULE()
+
+- (UIView *)view
+{
+ return [[RCTTextView alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
+}
+
+RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL)
+RCT_REMAP_VIEW_PROPERTY(editable, textView.editable, BOOL)
+RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
+RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
+RCT_EXPORT_VIEW_PROPERTY(text, NSString)
+RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL)
+RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
+RCT_REMAP_VIEW_PROPERTY(keyboardType, textView.keyboardType, UIKeyboardType)
+RCT_REMAP_VIEW_PROPERTY(returnKeyType, textView.returnKeyType, UIReturnKeyType)
+RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, textView.enablesReturnKeyAutomatically, BOOL)
+RCT_REMAP_VIEW_PROPERTY(color, textColor, UIColor)
+RCT_REMAP_VIEW_PROPERTY(autoCapitalize, textView.autocapitalizationType, UITextAutocapitalizationType)
+RCT_CUSTOM_VIEW_PROPERTY(fontSize, CGFloat, RCTTextView)
+{
+ view.font = [RCTConvert UIFont:view.font withSize:json ?: @(defaultView.font.pointSize)];
+}
+RCT_CUSTOM_VIEW_PROPERTY(fontWeight, NSString, RCTTextView)
+{
+ view.font = [RCTConvert UIFont:view.font withWeight:json]; // defaults to normal
+}
+RCT_CUSTOM_VIEW_PROPERTY(fontStyle, NSString, RCTTextView)
+{
+ view.font = [RCTConvert UIFont:view.font withStyle:json]; // defaults to normal
+}
+RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView)
+{
+ view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName];
+}
+
+- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView
+{
+ NSNumber *reactTag = shadowView.reactTag;
+ UIEdgeInsets padding = shadowView.paddingAsInsets;
+ return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
+ ((RCTTextView *)viewRegistry[reactTag]).contentInset = padding;
+ };
+}
+
+@end
diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js
index ce7b4078e9acd4..a67abacb087ec5 100644
--- a/Libraries/Text/Text.js
+++ b/Libraries/Text/Text.js
@@ -33,7 +33,7 @@ var viewConfig = {
};
/**
- * A react component for displaying text which supports nesting,
+ * A React component for displaying text which supports nesting,
* styling, and touch handling. In the following example, the nested title and
* body text will inherit the `fontFamily` from `styles.baseText`, but the title
* provides its own additional styles. The title and body will stack on top of
diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js
index fe28a7684c9d46..b93000a33a8f50 100644
--- a/Libraries/Utilities/Dimensions.js
+++ b/Libraries/Utilities/Dimensions.js
@@ -20,7 +20,7 @@ var dimensions = NativeModules.UIManager.Dimensions;
// We calculate the window dimensions in JS so that we don't encounter loss of
// precision in transferring the dimensions (which could be non-integers) over
// the bridge.
-if (dimensions.windowPhysicalPixels) {
+if (dimensions && dimensions.windowPhysicalPixels) {
// parse/stringify => Clone hack
dimensions = JSON.parse(JSON.stringify(dimensions));
diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js
index 576aaa9917bba2..97658c2356b3d6 100644
--- a/Libraries/Utilities/MessageQueue.js
+++ b/Libraries/Utilities/MessageQueue.js
@@ -21,6 +21,9 @@ var JSTimersExecution = require('JSTimersExecution');
var INTERNAL_ERROR = 'Error in MessageQueue implementation';
+// Prints all bridge traffic to console.log
+var DEBUG_SPY_MODE = false;
+
type ModulesConfig = {
[key:string]: {
moduleID: number;
@@ -263,6 +266,9 @@ var MessageQueueMixin = {
'both the success callback and the error callback.',
cbID
);
+ if (DEBUG_SPY_MODE) {
+ console.log('N->JS: Callback#' + cbID + '(' + JSON.stringify(args) + ')');
+ }
cb.apply(scope, args);
} catch(ie_requires_catch) {
throw ie_requires_catch;
@@ -292,6 +298,11 @@ var MessageQueueMixin = {
var moduleName = this._localModuleIDToModuleName[moduleID];
var methodName = this._localModuleNameToMethodIDToName[moduleName][methodID];
+ if (DEBUG_SPY_MODE) {
+ console.log(
+ 'N->JS: ' + moduleName + '.' + methodName +
+ '(' + JSON.stringify(params) + ')');
+ }
var ret = jsCall(this._requireFunc(moduleName), methodName, params);
return ret;
@@ -460,6 +471,17 @@ var MessageQueueMixin = {
var ret = currentOutgoingItems[REQUEST_MODULE_IDS].length ||
currentOutgoingItems[RESPONSE_RETURN_VALUES].length ? currentOutgoingItems : null;
+ if (DEBUG_SPY_MODE && ret) {
+ for (var i = 0; i < currentOutgoingItems[0].length; i++) {
+ var moduleName = this._remoteModuleIDToModuleName[currentOutgoingItems[0][i]];
+ var methodName =
+ this._remoteModuleNameToMethodIDToName[moduleName][currentOutgoingItems[1][i]];
+ console.log(
+ 'JS->N: ' + moduleName + '.' + methodName +
+ '(' + JSON.stringify(currentOutgoingItems[2][i]) + ')');
+ }
+ }
+
return ret;
},
diff --git a/Libraries/Utilities/stringifySafe.js b/Libraries/Utilities/stringifySafe.js
new file mode 100644
index 00000000000000..053ea69849e821
--- /dev/null
+++ b/Libraries/Utilities/stringifySafe.js
@@ -0,0 +1,42 @@
+/**
+ * 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 stringifySafe
+ * @flow
+ */
+'use strict';
+
+/**
+ * Tries to stringify with JSON.stringify and toString, but catches exceptions
+ * (e.g. from circular objects) and always returns a string and never throws.
+ */
+function stringifySafe(arg: any): string {
+ var ret;
+ if (arg === undefined) {
+ ret = 'undefined';
+ } else if (arg === null) {
+ ret = 'null';
+ } else if (typeof arg === 'string') {
+ ret = '"' + arg + '"';
+ } else {
+ // Perform a try catch, just in case the object has a circular
+ // reference or stringify throws for some other reason.
+ try {
+ ret = JSON.stringify(arg);
+ } catch (e) {
+ if (typeof arg.toString === 'function') {
+ try {
+ ret = arg.toString();
+ } catch (E) {}
+ }
+ }
+ }
+ return ret || '["' + typeof arg + '" failed to stringify]';
+}
+
+module.exports = stringifySafe;
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index 01bff7eae5dfb0..b94b172f37de61 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -27,6 +27,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
NavigatorIOS: require('NavigatorIOS'),
PickerIOS: require('PickerIOS'),
Navigator: require('Navigator'),
+ SegmentedControlIOS: require('SegmentedControlIOS'),
ScrollView: require('ScrollView'),
SliderIOS: require('SliderIOS'),
SwitchIOS: require('SwitchIOS'),
diff --git a/Libraries/vendor/react/platform/NodeHandle.js b/Libraries/vendor/react/platform/NodeHandle.js
index 19f74c6ece0b73..c7c93545bf0317 100644
--- a/Libraries/vendor/react/platform/NodeHandle.js
+++ b/Libraries/vendor/react/platform/NodeHandle.js
@@ -19,7 +19,7 @@
* worker thread.
*
* The only other requirement of a platform/environment is that it always be
- * possible to extract the react rootNodeID in a blocking manner (see
+ * possible to extract the React rootNodeID in a blocking manner (see
* `getRootNodeID`).
*
* +------------------+ +------------------+ +------------------+
diff --git a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js
index 020731fde52cbe..37c42382712627 100644
--- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js
+++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js
@@ -436,6 +436,7 @@ var TouchableMixin = {
if (isTouchWithinActive) {
this._receiveSignal(Signals.ENTER_PRESS_RECT, e);
} else {
+ this._cancelLongPressDelayTimeout();
this._receiveSignal(Signals.LEAVE_PRESS_RECT, e);
}
},
diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m
index 48fd672a3926d4..5b6a92682afbdf 100644
--- a/React/Base/RCTBridge.m
+++ b/React/Base/RCTBridge.m
@@ -31,6 +31,8 @@
NSString *const RCTReloadNotification = @"RCTReloadNotification";
NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification";
+dispatch_queue_t const RCTJSThread = nil;
+
/**
* Must be kept in sync with `MessageQueue.js`.
*/
@@ -189,6 +191,7 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
superclass = class_getSuperclass(superclass);
}
}
+ free(classes);
}
});
@@ -353,7 +356,7 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
#define RCT_CONVERT_CASE(_value, _type) \
case _value: { \
- _type (*convert)(id, SEL, id) = (typeof(convert))[RCTConvert methodForSelector:selector]; \
+ _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}
@@ -375,12 +378,27 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
RCT_CONVERT_CASE('B', BOOL)
RCT_CONVERT_CASE('@', id)
RCT_CONVERT_CASE('^', void *)
- case '{':
- RCTAssert(NO, @"Argument %zd of %C[%@ %@] is defined as %@, however RCT_EXPORT_METHOD() "
- "does not currently support struct-type arguments.", i - 2,
- [reactMethodName characterAtIndex:0], _moduleClassName,
- objCMethodName, argumentName);
- break;
+
+ case '{': {
+ [argumentBlocks addObject:^(RCTBridge *bridge, NSNumber *context, NSInvocation *invocation, NSUInteger index, id json) {
+ NSUInteger size;
+ NSGetSizeAndAlignment(argumentType, &size, NULL);
+ void *returnValue = malloc(size);
+ NSMethodSignature *methodSignature = [RCTConvert methodSignatureForSelector:selector];
+ NSInvocation *_invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+ [_invocation setTarget:[RCTConvert class]];
+ [_invocation setSelector:selector];
+ [_invocation setArgument:&json atIndex:2];
+ [_invocation invoke];
+ [_invocation getReturnValue:returnValue];
+
+ [invocation setArgument:returnValue atIndex:index];
+
+ free(returnValue);
+ }];
+ break;
+ }
+
default:
defaultCase(argumentType);
}
@@ -436,6 +454,10 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
RCT_SIMPLE_CASE('d', double, doubleValue)
RCT_SIMPLE_CASE('B', BOOL, boolValue)
+ case '{':
+ RCTLogMustFix(@"Cannot convert JSON to struct %s", argumentType);
+ break;
+
default:
defaultCase(argumentType);
}
@@ -650,6 +672,8 @@ - (NSString *)description
*/
static NSMutableDictionary *RCTLocalModuleIDs;
static NSMutableDictionary *RCTLocalMethodIDs;
+static NSMutableArray *RCTLocalModuleNames;
+static NSMutableArray *RCTLocalMethodNames;
static NSDictionary *RCTLocalModulesConfig()
{
static NSMutableDictionary *localModules;
@@ -658,6 +682,8 @@ - (NSString *)description
RCTLocalModuleIDs = [[NSMutableDictionary alloc] init];
RCTLocalMethodIDs = [[NSMutableDictionary alloc] init];
+ RCTLocalModuleNames = [[NSMutableArray alloc] init];
+ RCTLocalMethodNames = [[NSMutableArray alloc] init];
localModules = [[NSMutableDictionary alloc] init];
for (NSString *moduleDotMethod in RCTJSMethods()) {
@@ -689,6 +715,8 @@ - (NSString *)description
// Add module and method lookup
RCTLocalModuleIDs[moduleDotMethod] = module[@"moduleID"];
RCTLocalMethodIDs[moduleDotMethod] = methods[methodName][@"methodID"];
+ [RCTLocalModuleNames addObject:moduleName];
+ [RCTLocalMethodNames addObject:methodName];
}
});
@@ -795,6 +823,7 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL
_bundleURL = bundleURL;
_moduleProvider = block;
_launchOptions = [launchOptions copy];
+
[self setUp];
[self bindKeys];
}
@@ -872,6 +901,8 @@ - (void)setUp
dispatch_queue_t queue = [module methodQueue];
if (queue) {
_queuesByID[moduleID] = queue;
+ } else {
+ _queuesByID[moduleID] = [NSNull null];
}
}
}];
@@ -1047,7 +1078,7 @@ - (void)invalidate
#pragma mark - RCTBridge methods
/**
- * Like JS::call, for objective-c.
+ * Public. Can be invoked from any thread.
*/
- (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args
{
@@ -1058,12 +1089,10 @@ - (void)enqueueJSCall:(NSString *)moduleDotMethod args:(NSArray *)args
NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod];
RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod);
- if (!_loading) {
- [self _invokeAndProcessModule:@"BatchedBridge"
- method:@"callFunctionReturnFlushedQueue"
- arguments:@[moduleID, methodID, args ?: @[]]
- context:RCTGetExecutorID(_javaScriptExecutor)];
- }
+ [self _invokeAndProcessModule:@"BatchedBridge"
+ method:@"callFunctionReturnFlushedQueue"
+ arguments:@[moduleID, methodID, args ?: @[]]
+ context:RCTGetExecutorID(_javaScriptExecutor)];
}
/**
@@ -1081,10 +1110,17 @@ - (void)_immediatelyCallTimer:(NSNumber *)timer
if (!_loading) {
#if BATCHED_BRIDGE
- [self _actuallyInvokeAndProcessModule:@"BatchedBridge"
- method:@"callFunctionReturnFlushedQueue"
- arguments:@[moduleID, methodID, @[@[timer]]]
- context:RCTGetExecutorID(_javaScriptExecutor)];
+ dispatch_block_t block = ^{
+ [self _actuallyInvokeAndProcessModule:@"BatchedBridge"
+ method:@"callFunctionReturnFlushedQueue"
+ arguments:@[moduleID, methodID, @[@[timer]]]
+ context:RCTGetExecutorID(_javaScriptExecutor)];
+ };
+ if ([_javaScriptExecutor respondsToSelector:@selector(executeAsyncBlockOnJavaScriptQueue:)]) {
+ [_javaScriptExecutor executeAsyncBlockOnJavaScriptQueue:block];
+ } else {
+ [_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
+ }
#else
@@ -1128,33 +1164,93 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:
#pragma mark - Payload Generation
+- (void)dispatchBlock:(dispatch_block_t)block forModule:(NSNumber *)moduleID
+{
+ id queue = _queuesByID[moduleID];
+ if (queue == [NSNull null]) {
+ [_javaScriptExecutor executeBlockOnJavaScriptQueue:block];
+ } else {
+ dispatch_async(queue ?: _methodQueue, block);
+ }
+}
+
+/**
+ * Called by enqueueJSCall from any thread, or from _immediatelyCallTimer,
+ * on the JS thread, but only in non-batched mode.
+ */
- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context
{
#if BATCHED_BRIDGE
- RCTProfileBeginEvent();
- if ([module isEqualToString:@"RCTEventEmitter"]) {
- for (NSDictionary *call in _scheduledCalls) {
- if ([call[@"module"] isEqualToString:module] && [call[@"method"] isEqualToString:method] && [call[@"args"][0] isEqualToString:args[0]]) {
- [_scheduledCalls removeObject:call];
+ __weak NSMutableArray *weakScheduledCalls = _scheduledCalls;
+ __weak RCTSparseArray *weakScheduledCallbacks = _scheduledCallbacks;
+
+ [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{
+ RCTProfileBeginEvent();
+
+ NSMutableArray *scheduledCalls = weakScheduledCalls;
+ RCTSparseArray *scheduledCallbacks = weakScheduledCallbacks;
+ if (!scheduledCalls || !scheduledCallbacks) {
+ return;
+ }
+
+ /**
+ * Event deduping
+ *
+ * Right now we make a lot of assumptions about the arguments structure
+ * so just iterate if it's a `callFunctionReturnFlushedQueue()`
+ */
+ if ([method isEqualToString:@"callFunctionReturnFlushedQueue"]) {
+ NSString *moduleName = RCTLocalModuleNames[[args[0] integerValue]];
+ /**
+ * Keep going if it any event emmiter, e.g. RCT(Device|NativeApp)?EventEmitter
+ */
+ if ([moduleName hasSuffix:@"EventEmitter"]) {
+ for (NSDictionary *call in [scheduledCalls copy]) {
+ NSArray *callArgs = call[@"args"];
+ /**
+ * If it's the same module && method call on the bridge &&
+ * the same EventEmitter module && method
+ */
+ if (
+ [call[@"module"] isEqualToString:module] &&
+ [call[@"method"] isEqualToString:method] &&
+ [callArgs[0] isEqual:args[0]] &&
+ [callArgs[1] isEqual:args[1]]
+ ) {
+ /**
+ * args[2] contains the actual arguments for the event call, where
+ * args[2][0] is the target for RCTEventEmitter or the eventName
+ * for the other EventEmitters
+ * if RCTEventEmitter we need to compare args[2][1] that will be
+ * the eventName
+ */
+ if (
+ [args[2][0] isEqual:callArgs[2][0]] &&
+ ([moduleName isEqualToString:@"RCTEventEmitter"] ? [args[2][1] isEqual:callArgs[2][1]] : YES)
+ ) {
+ [scheduledCalls removeObject:call];
+ }
+ }
+ }
}
}
- }
- id call = @{
- @"module": module,
- @"method": method,
- @"args": args,
- @"context": context ?: @0,
- };
+ id call = @{
+ @"module": module,
+ @"method": method,
+ @"args": args,
+ @"context": context ?: @0,
+ };
- if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) {
- _scheduledCallbacks[args[0]] = call;
- } else {
- [_scheduledCalls addObject:call];
- }
+ if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) {
+ scheduledCallbacks[args[0]] = call;
+ } else {
+ [scheduledCalls addObject:call];
+ }
- RCTProfileEndEvent(@"enqueue_call", @"objc_call", call);
+ RCTProfileEndEvent(@"enqueue_call", @"objc_call", call);
+ }];
}
- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context
@@ -1235,10 +1331,9 @@ - (void)_handleBuffer:(id)buffer context:(NSNumber *)context
// TODO: batchDidComplete is only used by RCTUIManager - can we eliminate this special case?
[_modulesByID enumerateObjectsUsingBlock:^(id module, NSNumber *moduleID, BOOL *stop) {
if ([module respondsToSelector:@selector(batchDidComplete)]) {
- dispatch_queue_t queue = _queuesByID[moduleID];
- dispatch_async(queue ?: _methodQueue, ^{
+ [self dispatchBlock:^{
[module batchDidComplete];
- });
+ } forModule:moduleID];
}
}];
}
@@ -1273,8 +1368,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
}
__weak RCTBridge *weakSelf = self;
- dispatch_queue_t queue = _queuesByID[moduleID];
- dispatch_async(queue ?: _methodQueue, ^{
+ [self dispatchBlock:^{
RCTProfileBeginEvent();
__strong RCTBridge *strongSelf = weakSelf;
@@ -1303,7 +1397,7 @@ - (BOOL)_handleRequestNumber:(NSUInteger)i
@"method": method.JSMethodName,
@"selector": NSStringFromSelector(method.selector),
});
- });
+ } forModule:@(moduleID)];
return YES;
}
diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h
index dd6f61e235e279..34b861ff3f8f10 100644
--- a/React/Base/RCTBridgeModule.h
+++ b/React/Base/RCTBridgeModule.h
@@ -17,6 +17,16 @@
*/
typedef void (^RCTResponseSenderBlock)(NSArray *response);
+/**
+ * This constant can be returned from +methodQueue to force module
+ * methods to be called on the JavaScript thread. This can have serious
+ * implications for performance, so only use this if you're sure it's what
+ * you need.
+ *
+ * NOTE: RCTJSThread is not a real libdispatch queue
+ */
+extern const dispatch_queue_t RCTJSThread;
+
/**
* Provides the interface needed to register a bridge module.
*/
diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m
index f1ed77298dafdb..465d1f0bb65413 100644
--- a/React/Base/RCTConvert.m
+++ b/React/Base/RCTConvert.m
@@ -49,11 +49,11 @@ + (NSNumber *)NSNumber:(id)json
});
NSNumber *number = [formatter numberFromString:json];
if (!number) {
- RCTLogError(@"JSON String '%@' could not be interpreted as a number", json);
+ RCTLogConvertError(json, "a number");
}
return number;
} else if (json && json != [NSNull null]) {
- RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json classForCoder]);
+ RCTLogConvertError(json, "a number");
}
return nil;
}
@@ -66,30 +66,38 @@ + (NSData *)NSData:(id)json
+ (NSURL *)NSURL:(id)json
{
- if (!json || json == (id)kCFNull) {
+ NSString *path = [self NSString:json];
+ if (!path.length) {
return nil;
}
- if (![json isKindOfClass:[NSString class]]) {
- RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json);
- return nil;
- }
+ @try { // NSURL has a history of crashing with bad input, so let's be safe
- NSString *path = json;
- if ([path isAbsolutePath])
- {
+ NSURL *URL = [NSURL URLWithString:path];
+ if (URL.scheme) { // Was a well-formed absolute URL
+ return URL;
+ }
+
+ // Check if it has a scheme
+ if ([path rangeOfString:@"[a-zA-Z][a-zA-Z._-]+:" options:NSRegularExpressionSearch].location == 0) {
+ path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ URL = [NSURL URLWithString:path];
+ if (URL) {
+ return URL;
+ }
+ }
+
+ // Assume that it's a local path
+ path = [path stringByRemovingPercentEncoding];
+ if (![path isAbsolutePath]) {
+ path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:path];
+ }
return [NSURL fileURLWithPath:path];
}
- else if ([path length])
- {
- NSURL *URL = [NSURL URLWithString:path relativeToURL:[[NSBundle mainBundle] resourceURL]];
- if ([URL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) {
- RCTLogWarn(@"The file '%@' does not exist", URL);
- return nil;
- }
- return URL;
+ @catch (__unused NSException *e) {
+ RCTLogConvertError(json, "a valid URL");
+ return nil;
}
- return nil;
}
+ (NSURLRequest *)NSURLRequest:(id)json
@@ -112,11 +120,12 @@ + (NSDate *)NSDate:(id)json
});
NSDate *date = [formatter dateFromString:json];
if (!date) {
- RCTLogError(@"JSON String '%@' could not be interpreted as a date. Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json);
+ RCTLogError(@"JSON String '%@' could not be interpreted as a date. "
+ "Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json);
}
return date;
} else if (json && json != [NSNull null]) {
- RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json classForCoder]);
+ RCTLogConvertError(json, "a date");
}
return nil;
}
@@ -698,35 +707,48 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family
const RCTFontWeight RCTDefaultFontWeight = UIFontWeightRegular;
const CGFloat RCTDefaultFontSize = 14;
- // Get existing properties
+ // Initialize properties to defaults
+ CGFloat fontSize = RCTDefaultFontSize;
+ RCTFontWeight fontWeight = RCTDefaultFontWeight;
+ NSString *familyName = RCTDefaultFontFamily;
BOOL isItalic = NO;
BOOL isCondensed = NO;
- RCTFontWeight fontWeight = RCTDefaultFontWeight;
+
if (font) {
- family = font.familyName;
+ familyName = font.familyName ?: RCTDefaultFontFamily;
+ fontSize = font.pointSize ?: RCTDefaultFontSize;
fontWeight = RCTWeightOfFont(font);
isItalic = RCTFontIsItalic(font);
isCondensed = RCTFontIsCondensed(font);
}
+ // Get font size
+ fontSize = [self CGFloat:size] ?: fontSize;
+
+ // Get font family
+ familyName = [self NSString:family] ?: familyName;
+
// Get font style
if (style) {
isItalic = [self RCTFontStyle:style];
}
- // Get font size
- CGFloat fontSize = [self CGFloat:size] ?: RCTDefaultFontSize;
+ // Get font weight
+ if (weight) {
+ fontWeight = [self RCTFontWeight:weight];
+ }
- // Get font family
- NSString *familyName = [self NSString:family] ?: RCTDefaultFontFamily;
+ // Gracefully handle being given a font name rather than font family, for
+ // example: "Helvetica Light Oblique" rather than just "Helvetica".
if ([UIFont fontNamesForFamilyName:familyName].count == 0) {
font = [UIFont fontWithName:familyName size:fontSize];
if (font) {
// It's actually a font name, not a font family name,
// but we'll do what was meant, not what was said.
familyName = font.familyName;
- NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute];
- fontWeight = [traits[UIFontWeightTrait] doubleValue];
+ fontWeight = RCTWeightOfFont(font);
+ isItalic = RCTFontIsItalic(font);
+ isCondensed = RCTFontIsCondensed(font);
} else {
// Not a valid font or family
RCTLogError(@"Unrecognized font family '%@'", familyName);
@@ -734,14 +756,16 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family
}
}
- // Get font weight
- if (weight) {
- fontWeight = [self RCTFontWeight:weight];
+ // Get the closest font that matches the given weight for the fontFamily
+ UIFont *bestMatch = [UIFont fontWithName:font.fontName size: fontSize];
+ CGFloat closestWeight;
+
+ if (font && [font.familyName isEqualToString: familyName]) {
+ closestWeight = RCTWeightOfFont(font);
+ } else {
+ closestWeight = INFINITY;
}
- // Get closest match
- UIFont *bestMatch = font;
- CGFloat closestWeight = font ? RCTWeightOfFont(font) : INFINITY;
for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) {
UIFont *match = [UIFont fontWithName:name size:fontSize];
if (isItalic == RCTFontIsItalic(match) &&
diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h
index dd6bd8ed6f8aa2..15cb180210f6e6 100644
--- a/React/Base/RCTEventDispatcher.h
+++ b/React/Base/RCTEventDispatcher.h
@@ -50,7 +50,7 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) {
/**
* Send a user input event. The body dictionary must contain a "target"
- * parameter, representing the react tag of the view sending the event
+ * parameter, representing the React tag of the view sending the event
*/
- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body;
diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m
index fb4e02eae30f75..8487556e576a53 100644
--- a/React/Base/RCTEventDispatcher.m
+++ b/React/Base/RCTEventDispatcher.m
@@ -44,7 +44,7 @@ - (void)sendDeviceEventWithName:(NSString *)name body:(id)body
- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body
{
RCTAssert([body[@"target"] isKindOfClass:[NSNumber class]],
- @"Event body dictionary must include a 'target' property containing a react tag");
+ @"Event body dictionary must include a 'target' property containing a React tag");
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent"
args:body ? @[body[@"target"], name, body] : @[body[@"target"], name]];
diff --git a/React/Base/RCTJavaScriptExecutor.h b/React/Base/RCTJavaScriptExecutor.h
index eb7fd7d31947cd..8f1eb8a98c076a 100644
--- a/React/Base/RCTJavaScriptExecutor.h
+++ b/React/Base/RCTJavaScriptExecutor.h
@@ -49,6 +49,15 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error);
*/
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block;
+@optional
+
+/**
+ * Special case for Timers + ContextExecutor - instead of the default
+ * if jsthread then call else dispatch call on jsthread
+ * ensure the call is made async on the jsthread
+ */
+- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block;
+
@end
static const char *RCTJavaScriptExecutorID = "RCTJavaScriptExecutorID";
diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m
index dd8fab461868fd..2e7d21b9442fe7 100755
--- a/React/Base/RCTJavaScriptLoader.m
+++ b/React/Base/RCTJavaScriptLoader.m
@@ -10,47 +10,15 @@
#import "RCTJavaScriptLoader.h"
#import "RCTBridge.h"
-#import "RCTInvalidating.h"
-#import "RCTLog.h"
-#import "RCTRedBox.h"
+#import "RCTConvert.h"
#import "RCTSourceCode.h"
#import "RCTUtils.h"
-#define NO_REMOTE_MODULE @"Could not fetch module bundle %@. Ensure node server is running.\n\nIf it timed out, try reloading."
-#define NO_LOCAL_BUNDLE @"Could not load local bundle %@. Ensure file exists."
-
-#define CACHE_DIR @"RCTJSBundleCache"
-
-#pragma mark - Application Engine
-
-/**
- * TODO:
- * - Add window resize rotation events matching the DOM API.
- * - Device pixel ration hooks.
- * - Source maps.
- */
@implementation RCTJavaScriptLoader
{
__weak RCTBridge *_bridge;
}
-/**
- * `CADisplayLink` code copied from Ejecta but we've placed the JavaScriptCore
- * engine in its own dedicated thread.
- *
- * TODO: Try adding to the `RCTJavaScriptExecutor`'s thread runloop. Removes one
- * additional GCD dispatch per frame and likely makes it so that other UIThread
- * operations don't delay the dispatch (so we can begin working in JS much
- * faster.) Event handling must still be sent via a GCD dispatch, of course.
- *
- * We must add the display link to two runloops in order to get setTimeouts to
- * fire during scrolling. (`NSDefaultRunLoopMode` and `UITrackingRunLoopMode`)
- * TODO: We can invent a `requestAnimationFrame` and
- * `requestAvailableAnimationFrame` to control if callbacks can be fired during
- * an animation.
- * http://stackoverflow.com/questions/12622800/why-does-uiscrollview-pause-my-cadisplaylink
- *
- */
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super init])) {
@@ -61,92 +29,86 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
- (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(void (^)(NSError *))onComplete
{
- if (scriptURL == nil) {
+ // Sanitize the script URL
+ scriptURL = [RCTConvert NSURL:scriptURL.absoluteString];
+
+ if (!scriptURL ||
+ ([scriptURL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:scriptURL.path])) {
NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{
- NSLocalizedDescriptionKey: @"No script URL provided"
+ NSLocalizedDescriptionKey: scriptURL ? [NSString stringWithFormat:@"Script at '%@' could not be found.", scriptURL] : @"No script URL provided"
}];
onComplete(error);
return;
}
- if ([scriptURL isFileURL]) {
- NSString *bundlePath = [[NSBundle bundleForClass:[self class]] resourcePath];
- NSString *localPath = [scriptURL.absoluteString substringFromIndex:@"file://".length];
-
- if (![localPath hasPrefix:bundlePath]) {
- NSString *absolutePath = [NSString stringWithFormat:@"%@/%@", bundlePath, localPath];
- scriptURL = [NSURL fileURLWithPath:absolutePath];
- }
- }
-
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
- // Handle general request errors
- if (error) {
- if ([[error domain] isEqualToString:NSURLErrorDomain]) {
- NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]];
- NSDictionary *userInfo = @{
- NSLocalizedDescriptionKey: desc,
- NSLocalizedFailureReasonErrorKey: [error localizedDescription],
- NSUnderlyingErrorKey: error,
- };
- error = [NSError errorWithDomain:@"JSServer"
- code:error.code
- userInfo:userInfo];
- }
- onComplete(error);
- return;
- }
-
- // Parse response as text
- NSStringEncoding encoding = NSUTF8StringEncoding;
- if (response.textEncodingName != nil) {
- CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
- if (cfEncoding != kCFStringEncodingInvalidId) {
- encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
- }
- }
- NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
-
- // Handle HTTP errors
- if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) {
- NSDictionary *userInfo;
- NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
- if ([errorDetails isKindOfClass:[NSDictionary class]] &&
- [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
- NSMutableArray *fakeStack = [[NSMutableArray alloc] init];
- for (NSDictionary *err in errorDetails[@"errors"]) {
- [fakeStack addObject: @{
- @"methodName": err[@"description"] ?: @"",
- @"file": err[@"filename"] ?: @"",
- @"lineNumber": err[@"lineNumber"] ?: @0
- }];
- }
- userInfo = @{
- NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
- @"stack": fakeStack,
- };
- } else {
- userInfo = @{NSLocalizedDescriptionKey: rawText};
- }
- error = [NSError errorWithDomain:@"JSServer"
- code:[(NSHTTPURLResponse *)response statusCode]
- userInfo:userInfo];
-
- onComplete(error);
- return;
- }
- RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
- sourceCodeModule.scriptURL = scriptURL;
- sourceCodeModule.scriptText = rawText;
+ // Handle general request errors
+ if (error) {
+ if ([[error domain] isEqualToString:NSURLErrorDomain]) {
+ NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]];
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey: desc,
+ NSLocalizedFailureReasonErrorKey: [error localizedDescription],
+ NSUnderlyingErrorKey: error,
+ };
+ error = [NSError errorWithDomain:@"JSServer"
+ code:error.code
+ userInfo:userInfo];
+ }
+ onComplete(error);
+ return;
+ }
- [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) {
- dispatch_async(dispatch_get_main_queue(), ^{
- onComplete(scriptError);
- });
- }];
- }];
+ // Parse response as text
+ NSStringEncoding encoding = NSUTF8StringEncoding;
+ if (response.textEncodingName != nil) {
+ CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
+ if (cfEncoding != kCFStringEncodingInvalidId) {
+ encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
+ }
+ }
+ NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
+
+ // Handle HTTP errors
+ if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) {
+ NSDictionary *userInfo;
+ NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
+ if ([errorDetails isKindOfClass:[NSDictionary class]] &&
+ [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
+ NSMutableArray *fakeStack = [[NSMutableArray alloc] init];
+ for (NSDictionary *err in errorDetails[@"errors"]) {
+ [fakeStack addObject: @{
+ @"methodName": err[@"description"] ?: @"",
+ @"file": err[@"filename"] ?: @"",
+ @"lineNumber": err[@"lineNumber"] ?: @0
+ }];
+ }
+ userInfo = @{
+ NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
+ @"stack": fakeStack,
+ };
+ } else {
+ userInfo = @{NSLocalizedDescriptionKey: rawText};
+ }
+ error = [NSError errorWithDomain:@"JSServer"
+ code:[(NSHTTPURLResponse *)response statusCode]
+ userInfo:userInfo];
+
+ onComplete(error);
+ return;
+ }
+ RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
+ sourceCodeModule.scriptURL = scriptURL;
+ sourceCodeModule.scriptText = rawText;
+
+ [_bridge enqueueApplicationScript:rawText url:scriptURL onComplete:^(NSError *scriptError) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ onComplete(scriptError);
+ });
+ }];
+ }];
[task resume];
}
diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m
index 45624efdcc1e8a..54556d41810ba4 100644
--- a/React/Base/RCTRootView.m
+++ b/React/Base/RCTRootView.m
@@ -23,16 +23,6 @@
#import "RCTWebViewExecutor.h"
#import "UIView+React.h"
-/**
- * HACK(t6568049) This should be removed soon, hiding to prevent people from
- * relying on it
- */
-@interface RCTBridge (RCTRootView)
-
-- (void)setJavaScriptExecutor:(id)executor;
-
-@end
-
@interface RCTUIManager (RCTRootView)
- (NSNumber *)allocateRootTag;
@@ -120,11 +110,11 @@ - (void)bundleFinishedLoading
dispatch_async(dispatch_get_main_queue(), ^{
/**
- * Every root view that is created must have a unique react tag.
+ * Every root view that is created must have a unique React tag.
* Numbering of these tags goes from 1, 11, 21, 31, etc
*
* NOTE: Since the bridge persists, the RootViews might be reused, so now
- * the react tag is assigned every time we load new content.
+ * the React tag is assigned every time we load new content.
*/
[_contentView removeFromSuperview];
_contentView = [[UIView alloc] initWithFrame:self.bounds];
diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m
index bd731a99844715..7af26da74eab70 100644
--- a/React/Base/RCTTouchHandler.m
+++ b/React/Base/RCTTouchHandler.m
@@ -26,7 +26,7 @@ @implementation RCTTouchHandler
/**
* Arrays managed in parallel tracking native touch object along with the
- * native view that was touched, and the react touch data dictionary.
+ * native view that was touched, and the React touch data dictionary.
* This must be kept track of because `UIKit` destroys the touch targets
* if touches are canceled and we have no other way to recover this information.
*/
diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h
index 812a651222a85c..1c04125684fd11 100644
--- a/React/Base/RCTUtils.h
+++ b/React/Base/RCTUtils.h
@@ -46,3 +46,6 @@ RCT_EXTERN BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector);
// TODO(#6472857): create NSErrors and automatically convert them over the bridge.
RCT_EXTERN NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData);
RCT_EXTERN NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData);
+
+// Returns YES if React is running in a test environment
+RCT_EXTERN BOOL RCTRunningInTestEnvironment(void);
diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m
index cea45c324c63e7..3c644dbff4e788 100644
--- a/React/Base/RCTUtils.m
+++ b/React/Base/RCTUtils.m
@@ -35,8 +35,7 @@ id RCTJSONParse(NSString *jsonString, NSError **error)
if (jsonData) {
RCTLogWarn(@"RCTJSONParse received the following string, which could not be losslessly converted to UTF8 data: '%@'", jsonString);
} else {
- // If our backup conversion fails, log the issue so we can see what strings are causing this (t6452813)
- RCTLogError(@"RCTJSONParse received the following string, which could not be converted to UTF8 data: '%@'", jsonString);
+ RCTLogError(@"RCTJSONParse received invalid UTF8 data");
return nil;
}
}
@@ -201,3 +200,13 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector)
RCTLogError(@"\nError: %@", error);
return error;
}
+
+BOOL RCTRunningInTestEnvironment(void)
+{
+ static BOOL _isTestEnvironment = NO;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ _isTestEnvironment = (NSClassFromString(@"SenTestCase") != nil || NSClassFromString(@"XCTest") != nil);
+ });
+ return _isTestEnvironment;
+}
diff --git a/React/Executors/RCTContextExecutor.h b/React/Executors/RCTContextExecutor.h
index 159965a2fbed01..a41fcf31419a43 100644
--- a/React/Executors/RCTContextExecutor.h
+++ b/React/Executors/RCTContextExecutor.h
@@ -23,6 +23,6 @@
* You probably don't want to use this; use -init instead.
*/
- (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread
- globalContextRef:(JSGlobalContextRef)context;
+ globalContextRef:(JSGlobalContextRef)context NS_DESIGNATED_INITIALIZER;
@end
diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m
index 86444dd2a7f903..412ffd25693c36 100644
--- a/React/Executors/RCTContextExecutor.m
+++ b/React/Executors/RCTContextExecutor.m
@@ -55,6 +55,11 @@ - (void)invalidate
}
}
+- (void)dealloc
+{
+ CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]);
+}
+
@end
@implementation RCTContextExecutor
@@ -74,12 +79,12 @@ @implementation RCTContextExecutor
static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception)
{
if (argumentCount > 0) {
- JSStringRef string = JSValueToStringCopy(context, arguments[0], exception);
- if (!string) {
+ JSStringRef messageRef = JSValueToStringCopy(context, arguments[0], exception);
+ if (!messageRef) {
return JSValueMakeUndefined(context);
}
- NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string);
- JSStringRelease(string);
+ NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, messageRef);
+ JSStringRelease(messageRef);
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:
@"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).includeRequire.runModule.bundle(:[0-9]+:[0-9]+)"
options:NSRegularExpressionCaseInsensitive
@@ -89,14 +94,11 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object,
range:(NSRange){0, message.length}
withTemplate:@"[$4$5] \t$2"];
- // TODO: it would be good if log level was sent as a param, instead of this hack
RCTLogLevel level = RCTLogLevelInfo;
- if ([message rangeOfString:@"error" options:NSCaseInsensitiveSearch].length) {
- level = RCTLogLevelError;
- } else if ([message rangeOfString:@"warning" options:NSCaseInsensitiveSearch].length) {
- level = RCTLogLevelWarning;
+ if (argumentCount > 1) {
+ level = MAX(level, JSValueToNumber(context, arguments[1], exception) - 1);
}
- _RCTLogFormat(level, NULL, -1, @"%@", message);
+ RCTGetLogFunction()(level, nil, nil, message);
}
return JSValueMakeUndefined(context);
@@ -156,15 +158,12 @@ + (void)runRunLoopThread
- (instancetype)init
{
- static NSThread *javaScriptThread;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- // All JS is single threaded, so a serial queue is our only option.
- javaScriptThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoopThread) object:nil];
- [javaScriptThread setName:@"com.facebook.React.JavaScript"];
- [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]];
- [javaScriptThread start];
- });
+ NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class]
+ selector:@selector(runRunLoopThread)
+ object:nil];
+ [javaScriptThread setName:@"com.facebook.React.JavaScript"];
+ [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]];
+ [javaScriptThread start];
return [self initWithJavaScriptThread:javaScriptThread globalContextRef:NULL];
}
@@ -172,6 +171,9 @@ - (instancetype)init
- (instancetype)initWithJavaScriptThread:(NSThread *)javaScriptThread
globalContextRef:(JSGlobalContextRef)context
{
+ RCTAssert(javaScriptThread != nil,
+ @"Can't initialize RCTContextExecutor without a javaScriptThread");
+
if ((self = [super init])) {
_javaScriptThread = javaScriptThread;
__weak RCTContextExecutor *weakSelf = self;
@@ -305,17 +307,30 @@ - (void)executeApplicationScript:(NSString *)script
}
onComplete(error);
}
- }), @"js_call", (@{ @"url": sourceURL }))];
+ }), @"js_call", (@{ @"url": sourceURL.absoluteString }))];
}
- (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block
{
- if ([NSThread currentThread] != _javaScriptThread) {
- [self performSelector:@selector(executeBlockOnJavaScriptQueue:)
- onThread:_javaScriptThread withObject:block waitUntilDone:NO];
- } else {
- block();
- }
+ if ([NSThread currentThread] != _javaScriptThread) {
+ [self performSelector:@selector(executeBlockOnJavaScriptQueue:)
+ onThread:_javaScriptThread withObject:block waitUntilDone:NO];
+ } else {
+ block();
+ }
+}
+
+- (void)executeAsyncBlockOnJavaScriptQueue:(dispatch_block_t)block
+{
+ [self performSelector:@selector(executeBlockOnJavaScriptQueue:)
+ onThread:_javaScriptThread
+ withObject:block
+ waitUntilDone:NO];
+}
+
+- (void)_runBlock:(dispatch_block_t)block
+{
+ block();
}
- (void)injectJSONText:(NSString *)script
diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m
index 1d99c1a2d42663..e2df5befca18eb 100644
--- a/React/Modules/RCTTiming.m
+++ b/React/Modules/RCTTiming.m
@@ -110,7 +110,7 @@ - (void)dealloc
- (dispatch_queue_t)methodQueue
{
- return dispatch_get_main_queue();
+ return RCTJSThread;
}
- (BOOL)isValid
@@ -131,8 +131,6 @@ - (void)stopTimers
- (void)startTimers
{
- RCTAssertMainThread();
-
if (![self isValid] || _timers.count == 0) {
return;
}
diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m
index 451a343d04960d..496b9c3513a677 100644
--- a/React/Modules/RCTUIManager.m
+++ b/React/Modules/RCTUIManager.m
@@ -661,7 +661,7 @@ - (void)_manageChildren:(NSNumber *)containerReactTag
{
id container = registry[containerReactTag];
RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count);
- RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one react child to add");
+ RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add");
// Removes (both permanent and temporary moves) are using "before" indices
NSArray *permanentlyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
@@ -918,7 +918,7 @@ - (void)flushUIBlocks
}
// TODO: this doesn't work because sometimes view is inside a modal window
- // RCTAssert([rootView isReactRootView], @"React view is not inside a react root view");
+ // RCTAssert([rootView isReactRootView], @"React view is not inside a React root view");
// By convention, all coordinates, whether they be touch coordinates, or
// measurement coordinates are with respect to the root view.
@@ -995,18 +995,17 @@ static void RCTMeasureLayout(RCTShadowView *view,
}
/**
- * Returns an array of computed offset layouts in a dictionary form. The layouts are of any react subviews
+ * Returns an array of computed offset layouts in a dictionary form. The layouts are of any React subviews
* that are immediate descendants to the parent view found within a specified rect. The dictionary result
* contains left, top, width, height and an index. The index specifies the position among the other subviews.
* Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the
* passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts.
*/
-RCT_EXPORT_METHOD(measureViewsInRect:(id)rectJSON
+RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect
parentView:(NSNumber *)reactTag
errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback)
{
- CGRect rect = [RCTConvert CGRect:rectJSON];
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
if (!shadowView) {
RCTLogError(@"Attempting to measure view that does not exist (tag #%@)", reactTag);
@@ -1102,9 +1101,8 @@ static void RCTMeasureLayout(RCTShadowView *view,
}
RCT_EXPORT_METHOD(zoomToRect:(NSNumber *)reactTag
- withRect:(id)rectJSON)
+ withRect:(CGRect)rect)
{
- CGRect rect = [RCTConvert CGRect:rectJSON];
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
UIView *view = viewRegistry[reactTag];
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj
index 3364cce76fbfe8..10867e9cbc5aa9 100644
--- a/React/React.xcodeproj/project.pbxproj
+++ b/React/React.xcodeproj/project.pbxproj
@@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; };
00C1A2B31AC0B7E000E89A1C /* RCTDevMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 00C1A2B21AC0B7E000E89A1C /* RCTDevMenu.m */; };
+ 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */; };
+ 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */; };
13456E931ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E921ADAD2DE009F94A7 /* RCTConvert+CoreLocation.m */; };
13456E961ADAD482009F94A7 /* RCTConvert+MapKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13456E951ADAD482009F94A7 /* RCTConvert+MapKit.m */; };
134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */; };
@@ -33,7 +35,7 @@
13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080131A69489C00A75B9A /* RCTNavItemManager.m */; };
13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080151A69489C00A75B9A /* RCTTextField.m */; };
13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */; };
- 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */; };
+ 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */; };
13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; };
13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; };
13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156041AB1A2840079392D /* RCTWebViewManager.m */; };
@@ -84,6 +86,10 @@
000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = ""; };
00C1A2B11AC0B7E000E89A1C /* RCTDevMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDevMenu.h; sourceTree = ""; };
00C1A2B21AC0B7E000E89A1C /* RCTDevMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenu.m; sourceTree = ""; };
+ 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControl.h; sourceTree = ""; };
+ 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControl.m; sourceTree = ""; };
+ 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSegmentedControlManager.h; sourceTree = ""; };
+ 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSegmentedControlManager.m; sourceTree = ""; };
13442BF21AA90E0B0037E5B0 /* RCTAnimationType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationType.h; sourceTree = ""; };
13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; };
13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewControllerProtocol.h; sourceTree = ""; };
@@ -136,8 +142,8 @@
13B080151A69489C00A75B9A /* RCTTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextField.m; sourceTree = ""; };
13B080161A69489C00A75B9A /* RCTTextFieldManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextFieldManager.h; sourceTree = ""; };
13B080171A69489C00A75B9A /* RCTTextFieldManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextFieldManager.m; sourceTree = ""; };
- 13B080181A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIActivityIndicatorViewManager.h; sourceTree = ""; };
- 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIActivityIndicatorViewManager.m; sourceTree = ""; };
+ 13B080181A69489C00A75B9A /* RCTActivityIndicatorViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActivityIndicatorViewManager.h; sourceTree = ""; };
+ 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActivityIndicatorViewManager.m; sourceTree = ""; };
13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = ""; };
13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = ""; };
13C156011AB1A2840079392D /* RCTWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebView.h; sourceTree = ""; };
@@ -290,6 +296,10 @@
58114A141AAE854800E7D092 /* RCTPickerManager.h */,
58114A151AAE854800E7D092 /* RCTPickerManager.m */,
13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */,
+ 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */,
+ 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */,
+ 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */,
+ 131B6AF31AF1093D00FFC3E0 /* RCTSegmentedControlManager.m */,
13B07FF61A6947C200A75B9A /* RCTScrollView.h */,
13B07FF71A6947C200A75B9A /* RCTScrollView.m */,
13B07FF81A6947C200A75B9A /* RCTScrollViewManager.h */,
@@ -317,8 +327,8 @@
13B080151A69489C00A75B9A /* RCTTextField.m */,
13B080161A69489C00A75B9A /* RCTTextFieldManager.h */,
13B080171A69489C00A75B9A /* RCTTextFieldManager.m */,
- 13B080181A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.h */,
- 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */,
+ 13B080181A69489C00A75B9A /* RCTActivityIndicatorViewManager.h */,
+ 13B080191A69489C00A75B9A /* RCTActivityIndicatorViewManager.m */,
13E0674F1A70F44B002CDEE1 /* RCTView.h */,
13E067501A70F44B002CDEE1 /* RCTView.m */,
13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */,
@@ -489,6 +499,7 @@
13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */,
13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */,
83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */,
+ 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */,
830A229E1A66C68A008503DA /* RCTRootView.m in Sources */,
13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */,
83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */,
@@ -499,7 +510,7 @@
14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */,
14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */,
14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */,
- 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */,
+ 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */,
13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */,
58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */,
13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */,
@@ -529,6 +540,7 @@
58114A161AAE854800E7D092 /* RCTPicker.m in Sources */,
137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */,
13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */,
+ 131B6AF51AF1093D00FFC3E0 /* RCTSegmentedControlManager.m in Sources */,
58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */,
13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */,
830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */,
diff --git a/React/Views/RCTActivityIndicatorViewManager.h b/React/Views/RCTActivityIndicatorViewManager.h
new file mode 100644
index 00000000000000..cbd6816ae4cddf
--- /dev/null
+++ b/React/Views/RCTActivityIndicatorViewManager.h
@@ -0,0 +1,20 @@
+/**
+ * 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 "RCTViewManager.h"
+
+@interface RCTConvert (UIActivityIndicatorView)
+
++ (UIActivityIndicatorViewStyle)UIActivityIndicatorViewStyle:(id)json;
+
+@end
+
+@interface RCTActivityIndicatorViewManager : RCTViewManager
+
+@end
diff --git a/React/Views/RCTUIActivityIndicatorViewManager.m b/React/Views/RCTActivityIndicatorViewManager.m
similarity index 52%
rename from React/Views/RCTUIActivityIndicatorViewManager.m
rename to React/Views/RCTActivityIndicatorViewManager.m
index e2c9b3d353bf06..3876400dff3714 100644
--- a/React/Views/RCTUIActivityIndicatorViewManager.m
+++ b/React/Views/RCTActivityIndicatorViewManager.m
@@ -7,35 +7,37 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
-#import "RCTUIActivityIndicatorViewManager.h"
+#import "RCTActivityIndicatorViewManager.h"
#import "RCTConvert.h"
@implementation RCTConvert (UIActivityIndicatorView)
+// NOTE: It's pointless to support UIActivityIndicatorViewStyleGray
+// as we can set the color to any arbitrary value that we want to
+
RCT_ENUM_CONVERTER(UIActivityIndicatorViewStyle, (@{
- @"white-large": @(UIActivityIndicatorViewStyleWhiteLarge),
- @"large-white": @(UIActivityIndicatorViewStyleWhiteLarge),
- @"white": @(UIActivityIndicatorViewStyleWhite),
- @"gray": @(UIActivityIndicatorViewStyleGray),
+ @"large": @(UIActivityIndicatorViewStyleWhiteLarge),
+ @"small": @(UIActivityIndicatorViewStyleWhite),
}), UIActivityIndicatorViewStyleWhiteLarge, integerValue)
@end
-@implementation RCTUIActivityIndicatorViewManager
+@implementation RCTActivityIndicatorViewManager
-RCT_EXPORT_MODULE(UIActivityIndicatorViewManager)
+RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[UIActivityIndicatorView alloc] init];
}
-RCT_EXPORT_VIEW_PROPERTY(activityIndicatorViewStyle, UIActivityIndicatorViewStyle)
RCT_EXPORT_VIEW_PROPERTY(color, UIColor)
+RCT_EXPORT_VIEW_PROPERTY(hidesWhenStopped, BOOL)
+RCT_REMAP_VIEW_PROPERTY(size, activityIndicatorViewStyle, UIActivityIndicatorViewStyle)
RCT_CUSTOM_VIEW_PROPERTY(animating, BOOL, UIActivityIndicatorView)
{
- BOOL animating = json ? [json boolValue] : [defaultView isAnimating];
+ BOOL animating = json ? [RCTConvert BOOL:json] : [defaultView isAnimating];
if (animating != [view isAnimating]) {
if (animating) {
[view startAnimating];
@@ -45,14 +47,4 @@ - (UIView *)view
}
}
-- (NSDictionary *)constantsToExport
-{
- return
- @{
- @"StyleWhite": @(UIActivityIndicatorViewStyleWhite),
- @"StyleWhiteLarge": @(UIActivityIndicatorViewStyleWhiteLarge),
- @"StyleGray": @(UIActivityIndicatorViewStyleGray),
- };
-}
-
@end
diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m
index f3ebb6554a2cb8..a4cb338fb3c88b 100644
--- a/React/Views/RCTNavigator.m
+++ b/React/Views/RCTNavigator.m
@@ -60,7 +60,7 @@ @interface RCTNavigationController : UINavigationController
+
+@class RCTEventDispatcher;
+
+@interface RCTSegmentedControl : UISegmentedControl
+
+- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
+
+@property (nonatomic, copy) NSArray *values;
+@property (nonatomic, assign) NSInteger selectedIndex;
+
+@end
diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m
new file mode 100644
index 00000000000000..59e4cfb86b5fa4
--- /dev/null
+++ b/React/Views/RCTSegmentedControl.m
@@ -0,0 +1,57 @@
+//
+// RCTSegmentedControl.m
+// React
+//
+// Created by Clay Allsopp on 3/31/15.
+// Copyright (c) 2015 Facebook. All rights reserved.
+//
+
+#import "RCTSegmentedControl.h"
+
+#import "RCTConvert.h"
+#import "RCTEventDispatcher.h"
+#import "UIView+React.h"
+
+@implementation RCTSegmentedControl
+{
+ RCTEventDispatcher *_eventDispatcher;
+}
+
+- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
+{
+ if ((self = [super initWithFrame:CGRectZero])) {
+ _eventDispatcher = eventDispatcher;
+ _selectedIndex = self.selectedSegmentIndex;
+ [self addTarget:self action:@selector(onChange:)
+ forControlEvents:UIControlEventValueChanged];
+ }
+ return self;
+}
+
+- (void)setValues:(NSArray *)values
+{
+ _values = [values copy];
+ [self removeAllSegments];
+ for (NSString *value in values) {
+ [self insertSegmentWithTitle:value atIndex:self.numberOfSegments animated:NO];
+ }
+ super.selectedSegmentIndex = _selectedIndex;
+}
+
+- (void)setSelectedIndex:(NSInteger)selectedIndex
+{
+ _selectedIndex = selectedIndex;
+ super.selectedSegmentIndex = selectedIndex;
+}
+
+- (void)onChange:(UISegmentedControl *)sender
+{
+ NSDictionary *event = @{
+ @"target": self.reactTag,
+ @"value": [self titleForSegmentAtIndex:sender.selectedSegmentIndex],
+ @"selectedSegmentIndex": @(sender.selectedSegmentIndex)
+ };
+ [_eventDispatcher sendInputEventWithName:@"topChange" body:event];
+}
+
+@end
diff --git a/React/Views/RCTSegmentedControlManager.h b/React/Views/RCTSegmentedControlManager.h
new file mode 100644
index 00000000000000..03647c72edb9bf
--- /dev/null
+++ b/React/Views/RCTSegmentedControlManager.h
@@ -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 "RCTViewManager.h"
+
+@interface RCTSegmentedControlManager : RCTViewManager
+
+@end
diff --git a/React/Views/RCTSegmentedControlManager.m b/React/Views/RCTSegmentedControlManager.m
new file mode 100644
index 00000000000000..d7e1156ff00725
--- /dev/null
+++ b/React/Views/RCTSegmentedControlManager.m
@@ -0,0 +1,39 @@
+/**
+ * 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 "RCTSegmentedControlManager.h"
+
+#import "RCTBridge.h"
+#import "RCTConvert.h"
+#import "RCTSegmentedControl.h"
+
+@implementation RCTSegmentedControlManager
+
+RCT_EXPORT_MODULE()
+
+- (UIView *)view
+{
+ return [[RCTSegmentedControl alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
+}
+
+RCT_EXPORT_VIEW_PROPERTY(values, NSStringArray)
+RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger)
+RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
+RCT_EXPORT_VIEW_PROPERTY(momentary, BOOL)
+RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL)
+
+- (NSDictionary *)constantsToExport
+{
+ RCTSegmentedControl *view = [[RCTSegmentedControl alloc] init];
+ return @{
+ @"ComponentHeight": @(view.intrinsicContentSize.height),
+ };
+}
+
+@end
diff --git a/React/Views/RCTTextField.h b/React/Views/RCTTextField.h
index bd1be9c187bd39..ef0a07887faabc 100644
--- a/React/Views/RCTTextField.h
+++ b/React/Views/RCTTextField.h
@@ -17,6 +17,7 @@
@property (nonatomic, assign) BOOL autoCorrect;
@property (nonatomic, assign) BOOL selectTextOnFocus;
@property (nonatomic, assign) UIEdgeInsets contentInset;
+@property (nonatomic, strong) UIColor *placeholderTextColor;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
diff --git a/React/Views/RCTTextField.m b/React/Views/RCTTextField.m
index 35eb84d9653dde..12d52b1b8b7561 100644
--- a/React/Views/RCTTextField.m
+++ b/React/Views/RCTTextField.m
@@ -42,6 +42,30 @@ - (void)setText:(NSString *)text
}
}
+static void RCTUpdatePlaceholder(RCTTextField *self)
+{
+ if (self.placeholder.length > 0 && self.placeholderTextColor) {
+ self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder
+ attributes:@{
+ NSForegroundColorAttributeName : self.placeholderTextColor
+ }];
+ } else if (self.placeholder.length) {
+ self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder];
+ }
+}
+
+- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor
+{
+ _placeholderTextColor = placeholderTextColor;
+ RCTUpdatePlaceholder(self);
+}
+
+- (void)setPlaceholder:(NSString *)placeholder
+{
+ super.placeholder = placeholder;
+ RCTUpdatePlaceholder(self);
+}
+
- (NSArray *)reactSubviews
{
// TODO: do we support subviews of textfield in React?
diff --git a/React/Views/RCTTextFieldManager.m b/React/Views/RCTTextFieldManager.m
index 6e78d86a3b1c39..ff401a719c7c7a 100644
--- a/React/Views/RCTTextFieldManager.m
+++ b/React/Views/RCTTextFieldManager.m
@@ -10,7 +10,6 @@
#import "RCTTextFieldManager.h"
#import "RCTBridge.h"
-#import "RCTConvert.h"
#import "RCTShadowView.h"
#import "RCTSparseArray.h"
#import "RCTTextField.h"
@@ -28,6 +27,7 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(autoCorrect, BOOL)
RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
+RCT_EXPORT_VIEW_PROPERTY(placeholderTextColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_EXPORT_VIEW_PROPERTY(clearButtonMode, UITextFieldViewMode)
RCT_REMAP_VIEW_PROPERTY(clearTextOnFocus, clearsOnBeginEditing, BOOL)
diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h
index 73fe2c7cbb0338..1a4bcb40007e75 100644
--- a/React/Views/RCTView.h
+++ b/React/Views/RCTView.h
@@ -13,13 +13,6 @@
#import "RCTPointerEvents.h"
-typedef NS_ENUM(NSInteger, RCTBorderSide) {
- RCTBorderSideTop,
- RCTBorderSideRight,
- RCTBorderSideBottom,
- RCTBorderSideLeft
-};
-
@protocol RCTAutoInsetsProtocol;
@interface RCTView : UIView
diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m
index d40798302b2a94..c0786b5abfa863 100644
--- a/React/Views/RCTView.m
+++ b/React/Views/RCTView.m
@@ -12,9 +12,10 @@
#import "RCTAutoInsetsProtocol.h"
#import "RCTConvert.h"
#import "RCTLog.h"
+#import "RCTUtils.h"
#import "UIView+React.h"
-static const RCTBorderSide RCTBorderSideCount = 4;
+static void *RCTViewCornerRadiusKVOContext = &RCTViewCornerRadiusKVOContext;
static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event)
{
@@ -30,6 +31,10 @@
return nil;
}
+static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2]);
+static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform);
+static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise);
+
@implementation UIView (RCTViewUnmounting)
- (void)react_remountAllSubviews
@@ -107,8 +112,39 @@ - (UIView *)react_findClipView
@implementation RCTView
{
NSMutableArray *_reactSubviews;
- CAShapeLayer *_borderLayers[RCTBorderSideCount];
- CGFloat _borderWidths[RCTBorderSideCount];
+ UIColor *_backgroundColor;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+ if ((self = [super initWithFrame:frame])) {
+ _borderWidth = -1;
+ _borderTopWidth = -1;
+ _borderRightWidth = -1;
+ _borderBottomWidth = -1;
+ _borderLeftWidth = -1;
+
+ _backgroundColor = [super backgroundColor];
+ [super setBackgroundColor:[UIColor clearColor]];
+
+ [self.layer addObserver:self forKeyPath:@"cornerRadius" options:0 context:RCTViewCornerRadiusKVOContext];
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [self.layer removeObserver:self forKeyPath:@"cornerRadius" context:RCTViewCornerRadiusKVOContext];
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
+{
+ if (context == RCTViewCornerRadiusKVOContext) {
+ [self.layer setNeedsDisplay];
+ } else {
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+ }
}
- (NSString *)accessibilityLabel
@@ -381,189 +417,353 @@ - (void)layoutSubviews
if (_reactSubviews) {
[self updateClippedSubviews];
}
-
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- if (_borderLayers[side]) [self updatePathForShapeLayerForSide:side];
- }
}
-- (void)layoutSublayersOfLayer:(CALayer *)layer
+#pragma mark - Borders
+
+- (UIColor *)backgroundColor
{
- [super layoutSublayersOfLayer:layer];
+ return _backgroundColor;
+}
- const CGRect bounds = layer.bounds;
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- _borderLayers[side].frame = bounds;
+- (void)setBackgroundColor:(UIColor *)backgroundColor
+{
+ if ([_backgroundColor isEqual:backgroundColor]) {
+ return;
}
+ _backgroundColor = backgroundColor;
+ [self.layer setNeedsDisplay];
}
-- (BOOL)getTrapezoidPoints:(CGPoint[4])outPoints forSide:(RCTBorderSide)side
+- (UIImage *)generateBorderImage:(out CGRect *)contentsCenter
{
- const CGRect bounds = self.layer.bounds;
- const CGFloat minX = CGRectGetMinX(bounds);
- const CGFloat maxX = CGRectGetMaxX(bounds);
- const CGFloat minY = CGRectGetMinY(bounds);
- const CGFloat maxY = CGRectGetMaxY(bounds);
+ const CGFloat maxRadius = MIN(self.bounds.size.height, self.bounds.size.width) / 2.0;
+ const CGFloat radius = MAX(0, MIN(self.layer.cornerRadius, maxRadius));
-#define BW(SIDE) [self borderWidthForSide:RCTBorderSide##SIDE]
+ const CGFloat borderWidth = MAX(0, _borderWidth);
+ const CGFloat topWidth = _borderTopWidth >= 0 ? _borderTopWidth : borderWidth;
+ const CGFloat rightWidth = _borderRightWidth >= 0 ? _borderRightWidth : borderWidth;
+ const CGFloat bottomWidth = _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth;
+ const CGFloat leftWidth = _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth;
- switch (side) {
- case RCTBorderSideRight:
- outPoints[0] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom));
- outPoints[1] = CGPointMake(maxX - BW(Right), minY + BW(Top));
- outPoints[2] = CGPointMake(maxX, minY);
- outPoints[3] = CGPointMake(maxX, maxY);
- break;
- case RCTBorderSideBottom:
- outPoints[0] = CGPointMake(minX + BW(Left), maxY - BW(Bottom));
- outPoints[1] = CGPointMake(maxX - BW(Right), maxY - BW(Bottom));
- outPoints[2] = CGPointMake(maxX, maxY);
- outPoints[3] = CGPointMake(minX, maxY);
- break;
- case RCTBorderSideLeft:
- outPoints[0] = CGPointMake(minX + BW(Left), minY + BW(Top));
- outPoints[1] = CGPointMake(minX + BW(Left), maxY - BW(Bottom));
- outPoints[2] = CGPointMake(minX, maxY);
- outPoints[3] = CGPointMake(minX, minY);
- break;
- case RCTBorderSideTop:
- outPoints[0] = CGPointMake(maxX - BW(Right), minY + BW(Top));
- outPoints[1] = CGPointMake(minX + BW(Left), minY + BW(Top));
- outPoints[2] = CGPointMake(minX, minY);
- outPoints[3] = CGPointMake(maxX, minY);
- break;
- }
+ const CGFloat topRadius = MAX(0, radius - topWidth);
+ const CGFloat rightRadius = MAX(0, radius - rightWidth);
+ const CGFloat bottomRadius = MAX(0, radius - bottomWidth);
+ const CGFloat leftRadius = MAX(0, radius - leftWidth);
- return YES;
-}
+ const UIEdgeInsets edgeInsets = UIEdgeInsetsMake(topWidth + topRadius, leftWidth + leftRadius, bottomWidth + bottomRadius, rightWidth + rightRadius);
+ const CGSize size = CGSizeMake(edgeInsets.left + 1 + edgeInsets.right, edgeInsets.top + 1 + edgeInsets.bottom);
-- (CAShapeLayer *)createShapeLayerIfNotExistsForSide:(RCTBorderSide)side
-{
- CAShapeLayer *borderLayer = _borderLayers[side];
- if (!borderLayer) {
- borderLayer = [CAShapeLayer layer];
- borderLayer.fillColor = self.layer.borderColor;
- [self.layer addSublayer:borderLayer];
- _borderLayers[side] = borderLayer;
- }
- return borderLayer;
-}
+ UIScreen *screen = self.window.screen ?: [UIScreen mainScreen];
+ UIGraphicsBeginImageContextWithOptions(size, NO, screen.scale * 2);
-- (void)updatePathForShapeLayerForSide:(RCTBorderSide)side
-{
- CAShapeLayer *borderLayer = [self createShapeLayerIfNotExistsForSide:side];
+ CGContextRef ctx = UIGraphicsGetCurrentContext();
+ const CGRect rect = {CGPointZero, size};
+ CGPathRef path = CGPathCreateWithRoundedRect(rect, radius, radius, NULL);
- CGPoint trapezoidPoints[4];
- [self getTrapezoidPoints:trapezoidPoints forSide:side];
+ if (_backgroundColor) {
+ CGContextSaveGState(ctx);
- CGMutablePathRef path = CGPathCreateMutable();
- CGPathAddLines(path, NULL, trapezoidPoints, 4);
- CGPathCloseSubpath(path);
- borderLayer.path = path;
+ CGContextAddPath(ctx, path);
+ CGContextSetFillColorWithColor(ctx, _backgroundColor.CGColor);
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
+ }
+
+ CGContextAddPath(ctx, path);
CGPathRelease(path);
-}
-- (void)updateBorderLayers
-{
- BOOL widthsAndColorsSame = YES;
- CGFloat width = _borderWidths[0];
- CGColorRef color = _borderLayers[0].fillColor;
- for (RCTBorderSide side = 1; side < RCTBorderSideCount; side++) {
- CAShapeLayer *layer = _borderLayers[side];
- if (_borderWidths[side] != width || (layer && !CGColorEqualToColor(layer.fillColor, color))) {
- widthsAndColorsSame = NO;
- break;
+ if (radius > 0 && topWidth > 0 && rightWidth > 0 && bottomWidth > 0 && leftWidth > 0) {
+ const UIEdgeInsets insetEdgeInsets = UIEdgeInsetsMake(topWidth, leftWidth, bottomWidth, rightWidth);
+ const CGRect insetRect = UIEdgeInsetsInsetRect(rect, insetEdgeInsets);
+ CGPathRef insetPath = RCTPathCreateWithRoundedRect(insetRect, leftRadius, topRadius, rightRadius, topRadius, leftRadius, bottomRadius, rightRadius, bottomRadius, NULL);
+ CGContextAddPath(ctx, insetPath);
+ CGPathRelease(insetPath);
+ }
+
+ CGContextEOClip(ctx);
+
+ BOOL hasEqualColor = !_borderTopColor && !_borderRightColor && !_borderBottomColor && !_borderLeftColor;
+ BOOL hasEqualBorder = _borderWidth >= 0 && _borderTopWidth < 0 && _borderRightWidth < 0 && _borderBottomWidth < 0 && _borderLeftWidth < 0;
+ if (radius <= 0 && hasEqualBorder && hasEqualColor) {
+ CGContextSetStrokeColorWithColor(ctx, _borderColor);
+ CGContextSetLineWidth(ctx, 2 * _borderWidth);
+ CGContextClipToRect(ctx, rect);
+ CGContextStrokeRect(ctx, rect);
+ } else if (radius <= 0 && hasEqualColor) {
+ CGContextSetFillColorWithColor(ctx, _borderColor);
+ CGContextAddRect(ctx, rect);
+ const CGRect insetRect = UIEdgeInsetsInsetRect(rect, edgeInsets);
+ CGContextAddRect(ctx, insetRect);
+ CGContextEOFillPath(ctx);
+ } else {
+ BOOL didSet = NO;
+ CGPoint topLeft;
+ if (topRadius > 0 && leftRadius > 0) {
+ CGPoint points[2];
+ RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, topWidth, 2 * leftRadius, 2 * topRadius), CGPointMake(0, 0), CGPointMake(leftWidth, topWidth), points);
+ if (!isnan(points[1].x) && !isnan(points[1].y)) {
+ topLeft = points[1];
+ didSet = YES;
+ }
}
- }
- if (widthsAndColorsSame) {
- // Set main layer border
- if (width) {
- _borderWidth = self.layer.borderWidth = width;
+ if (!didSet) {
+ topLeft = CGPointMake(leftWidth, topWidth);
}
- if (color) {
- self.layer.borderColor = color;
+
+ didSet = NO;
+ CGPoint bottomLeft;
+ if (bottomRadius > 0 && leftRadius > 0) {
+ CGPoint points[2];
+ RCTEllipseGetIntersectionsWithLine(CGRectMake(leftWidth, (size.height - bottomWidth) - 2 * bottomRadius, 2 * leftRadius, 2 * bottomRadius), CGPointMake(0, size.height), CGPointMake(leftWidth, size.height - bottomWidth), points);
+ if (!isnan(points[1].x) && !isnan(points[1].y)) {
+ bottomLeft = points[1];
+ didSet = YES;
+ }
}
- // Remove border layers
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- [_borderLayers[side] removeFromSuperlayer];
- _borderLayers[side] = nil;
+ if (!didSet) {
+ bottomLeft = CGPointMake(leftWidth, size.height - bottomWidth);
}
- } else {
+ didSet = NO;
+ CGPoint topRight;
+ if (topRadius > 0 && rightRadius > 0) {
+ CGPoint points[2];
+ RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * rightRadius, topWidth, 2 * rightRadius, 2 * topRadius), CGPointMake(size.width, 0), CGPointMake(size.width - rightWidth, topWidth), points);
+ if (!isnan(points[0].x) && !isnan(points[0].y)) {
+ topRight = points[0];
+ didSet = YES;
+ }
+ }
+
+ if (!didSet) {
+ topRight = CGPointMake(size.width - rightWidth, topWidth);
+ }
+
+ didSet = NO;
+ CGPoint bottomRight;
+ if (bottomRadius > 0 && rightRadius > 0) {
+ CGPoint points[2];
+ RCTEllipseGetIntersectionsWithLine(CGRectMake((size.width - rightWidth) - 2 * rightRadius, (size.height - bottomWidth) - 2 * bottomRadius, 2 * rightRadius, 2 * bottomRadius), CGPointMake(size.width, size.height), CGPointMake(size.width - rightWidth, size.height - bottomWidth), points);
+ if (!isnan(points[0].x) && !isnan(points[0].y)) {
+ bottomRight = points[0];
+ didSet = YES;
+ }
+ }
- // Clear main layer border
- self.layer.borderWidth = 0;
+ if (!didSet) {
+ bottomRight = CGPointMake(size.width - rightWidth, size.height - bottomWidth);
+ }
+
+ // RIGHT
+ if (rightWidth > 0) {
+ CGContextSaveGState(ctx);
+
+ const CGPoint points[] = {
+ CGPointMake(size.width, 0),
+ topRight,
+ bottomRight,
+ CGPointMake(size.width, size.height),
+ };
+
+ CGContextSetFillColorWithColor(ctx, _borderRightColor ?: _borderColor);
+ CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points));
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
+ }
- // Set up border layers
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- [self updatePathForShapeLayerForSide:side];
+ // BOTTOM
+ if (bottomWidth > 0) {
+ CGContextSaveGState(ctx);
+
+ const CGPoint points[] = {
+ CGPointMake(0, size.height),
+ bottomLeft,
+ bottomRight,
+ CGPointMake(size.width, size.height),
+ };
+
+ CGContextSetFillColorWithColor(ctx, _borderBottomColor ?: _borderColor);
+ CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points));
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
+ }
+
+ // LEFT
+ if (leftWidth > 0) {
+ CGContextSaveGState(ctx);
+
+ const CGPoint points[] = {
+ CGPointMake(0, 0),
+ topLeft,
+ bottomLeft,
+ CGPointMake(0, size.height),
+ };
+
+ CGContextSetFillColorWithColor(ctx, _borderLeftColor ?: _borderColor);
+ CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points));
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
+ }
+
+ // TOP
+ if (topWidth > 0) {
+ CGContextSaveGState(ctx);
+
+ const CGPoint points[] = {
+ CGPointMake(0, 0),
+ topLeft,
+ topRight,
+ CGPointMake(size.width, 0),
+ };
+
+ CGContextSetFillColorWithColor(ctx, _borderTopColor ?: _borderColor);
+ CGContextAddLines(ctx, points, sizeof(points)/sizeof(*points));
+ CGContextFillPath(ctx);
+
+ CGContextRestoreGState(ctx);
}
}
-}
-- (CGFloat)borderWidthForSide:(RCTBorderSide)side
-{
- return _borderWidths[side] ?: _borderWidth;
+ UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ *contentsCenter = CGRectMake(edgeInsets.left / size.width, edgeInsets.top / size.height, 1.0 / size.width, 1.0 / size.height);
+ return [image resizableImageWithCapInsets:edgeInsets];
}
-- (void)setBorderWidth:(CGFloat)width forSide:(RCTBorderSide)side
+- (void)displayLayer:(CALayer *)layer
{
- _borderWidths[side] = width;
- [self updateBorderLayers];
-}
+ CGRect contentsCenter;
+ UIImage *image = [self generateBorderImage:&contentsCenter];
-#define BORDER_WIDTH(SIDE) \
-- (CGFloat)border##SIDE##Width { return [self borderWidthForSide:RCTBorderSide##SIDE]; } \
-- (void)setBorder##SIDE##Width:(CGFloat)width { [self setBorderWidth:width forSide:RCTBorderSide##SIDE]; }
+ if (RCTRunningInTestEnvironment()) {
+ const CGSize size = self.bounds.size;
+ UIGraphicsBeginImageContextWithOptions(size, NO, image.scale);
+ [image drawInRect:(CGRect){CGPointZero, size}];
+ image = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
-BORDER_WIDTH(Top)
-BORDER_WIDTH(Right)
-BORDER_WIDTH(Bottom)
-BORDER_WIDTH(Left)
+ contentsCenter = CGRectMake(0, 0, 1, 1);
+ }
-- (CGColorRef)borderColorForSide:(RCTBorderSide)side
-{
- return _borderLayers[side].fillColor ?: self.layer.borderColor;
+ layer.contents = (id)image.CGImage;
+ layer.contentsCenter = contentsCenter;
+ layer.contentsScale = image.scale;
+ layer.magnificationFilter = kCAFilterNearest;
}
-- (void)setBorderColor:(CGColorRef)color forSide:(RCTBorderSide)side
-{
- [self createShapeLayerIfNotExistsForSide:side].fillColor = color;
- [self updateBorderLayers];
-}
+#pragma mark Border Color
-#define BORDER_COLOR(SIDE) \
-- (CGColorRef)border##SIDE##Color { return [self borderColorForSide:RCTBorderSide##SIDE]; } \
-- (void)setBorder##SIDE##Color:(CGColorRef)color { [self setBorderColor:color forSide:RCTBorderSide##SIDE]; }
+#define setBorderColor(side) \
+ - (void)setBorder##side##Color:(CGColorRef)border##side##Color \
+ { \
+ if (CGColorEqualToColor(_border##side##Color, border##side##Color)) { \
+ return; \
+ } \
+ _border##side##Color = border##side##Color; \
+ [self.layer setNeedsDisplay]; \
+ }
-BORDER_COLOR(Top)
-BORDER_COLOR(Right)
-BORDER_COLOR(Bottom)
-BORDER_COLOR(Left)
+setBorderColor()
+setBorderColor(Top)
+setBorderColor(Right)
+setBorderColor(Bottom)
+setBorderColor(Left)
-- (void)setBorderWidth:(CGFloat)borderWidth
-{
- _borderWidth = borderWidth;
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- _borderWidths[side] = borderWidth;
+#pragma mark - Border Width
+
+#define setBorderWidth(side) \
+ - (void)setBorder##side##Width:(CGFloat)border##side##Width \
+ { \
+ if (_border##side##Width == border##side##Width) { \
+ return; \
+ } \
+ _border##side##Width = border##side##Width; \
+ [self.layer setNeedsDisplay]; \
}
- [self updateBorderLayers];
-}
-- (void)setBorderColor:(CGColorRef)borderColor
+setBorderWidth()
+setBorderWidth(Top)
+setBorderWidth(Right)
+setBorderWidth(Bottom)
+setBorderWidth(Left)
+
+@end
+
+static void RCTPathAddEllipticArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat xRadius, CGFloat yRadius, CGFloat startAngle, CGFloat endAngle, bool clockwise)
{
- self.layer.borderColor = borderColor;
- for (RCTBorderSide side = 0; side < RCTBorderSideCount; side++) {
- _borderLayers[side].fillColor = borderColor;
+ CGFloat xScale = 1, yScale = 1, radius = 0;
+ if (xRadius != 0) {
+ xScale = 1;
+ yScale = yRadius / xRadius;
+ radius = xRadius;
+ } else if (yRadius != 0) {
+ xScale = xRadius / yRadius;
+ yScale = 1;
+ radius = yRadius;
}
- [self updateBorderLayers];
+
+ CGAffineTransform t = CGAffineTransformMakeTranslation(x, y);
+ t = CGAffineTransformScale(t, xScale, yScale);
+ if (m != NULL) {
+ t = CGAffineTransformConcat(t, *m);
+ }
+
+ CGPathAddArc(path, &t, 0, 0, radius, startAngle, endAngle, clockwise);
}
-- (CGColorRef)borderColor
+static CGPathRef RCTPathCreateWithRoundedRect(CGRect rect, CGFloat topLeftRadiusX, CGFloat topLeftRadiusY, CGFloat topRightRadiusX, CGFloat topRightRadiusY, CGFloat bottomLeftRadiusX, CGFloat bottomLeftRadiusY, CGFloat bottomRightRadiusX, CGFloat bottomRightRadiusY, const CGAffineTransform *transform)
{
- return self.layer.borderColor;
+ const CGFloat minX = CGRectGetMinX(rect);
+ const CGFloat minY = CGRectGetMinY(rect);
+ const CGFloat maxX = CGRectGetMaxX(rect);
+ const CGFloat maxY = CGRectGetMaxY(rect);
+
+ CGMutablePathRef path = CGPathCreateMutable();
+ RCTPathAddEllipticArc(path, transform, minX + topLeftRadiusX, minY + topLeftRadiusY, topLeftRadiusX, topLeftRadiusY, M_PI, 3 * M_PI_2, false);
+ RCTPathAddEllipticArc(path, transform, maxX - topRightRadiusX, minY + topRightRadiusY, topRightRadiusX, topRightRadiusY, 3 * M_PI_2, 0, false);
+ RCTPathAddEllipticArc(path, transform, maxX - bottomRightRadiusX, maxY - bottomRightRadiusY, bottomRightRadiusX, bottomRightRadiusY, 0, M_PI_2, false);
+ RCTPathAddEllipticArc(path, transform, minX + bottomLeftRadiusX, maxY - bottomLeftRadiusY, bottomLeftRadiusX, bottomLeftRadiusY, M_PI_2, M_PI, false);
+ CGPathCloseSubpath(path);
+ return path;
}
-@end
+static BOOL RCTEllipseGetIntersectionsWithLine(CGRect ellipseBoundingRect, CGPoint p1, CGPoint p2, CGPoint intersections[2])
+{
+ const CGFloat ellipseCenterX = CGRectGetMidX(ellipseBoundingRect);
+ const CGFloat ellipseCenterY = CGRectGetMidY(ellipseBoundingRect);
+
+ // ellipseBoundingRect.origin.x -= ellipseCenterX;
+ // ellipseBoundingRect.origin.y -= ellipseCenterY;
+
+ p1.x -= ellipseCenterX;
+ p1.y -= ellipseCenterY;
+
+ p2.x -= ellipseCenterX;
+ p2.y -= ellipseCenterY;
+
+ const CGFloat m = (p2.y - p1.y) / (p2.x - p1.x);
+ const CGFloat a = ellipseBoundingRect.size.width / 2;
+ const CGFloat b = ellipseBoundingRect.size.height / 2;
+ const CGFloat c = p1.y - m * p1.x;
+ const CGFloat A = (b * b + a * a * m * m);
+ const CGFloat B = 2 * a * a * c * m;
+ const CGFloat D = sqrt((a * a * (b * b - c * c)) / A + pow(B / (2 * A), 2));
+
+ const CGFloat x_ = -B / (2 * A);
+ const CGFloat x1 = x_ + D;
+ const CGFloat x2 = x_ - D;
+ const CGFloat y1 = m * x1 + c;
+ const CGFloat y2 = m * x2 + c;
+
+ intersections[0] = CGPointMake(x1 + ellipseCenterX, y1 + ellipseCenterY);
+ intersections[1] = CGPointMake(x2 + ellipseCenterX, y2 + ellipseCenterY);
+ return YES;
+}
diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m
index 8388b83cfb489c..62fb29116f5540 100644
--- a/React/Views/RCTViewManager.m
+++ b/React/Views/RCTViewManager.m
@@ -164,7 +164,9 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)
RCT_EXPORT_SHADOW_PROPERTY(borderRightWidth, CGFloat);
RCT_EXPORT_SHADOW_PROPERTY(borderBottomWidth, CGFloat);
RCT_EXPORT_SHADOW_PROPERTY(borderLeftWidth, CGFloat);
-RCT_EXPORT_SHADOW_PROPERTY(borderWidth, CGFloat);
+RCT_CUSTOM_SHADOW_PROPERTY(borderWidth, CGFloat, RCTShadowView) {
+ [view setBorderWidth:[RCTConvert CGFloat:json]];
+}
RCT_EXPORT_SHADOW_PROPERTY(marginTop, CGFloat);
RCT_EXPORT_SHADOW_PROPERTY(marginRight, CGFloat);
diff --git a/React/Views/RCTViewNodeProtocol.h b/React/Views/RCTViewNodeProtocol.h
index 691aaaba15af3a..e78cc2ce7b26fc 100644
--- a/React/Views/RCTViewNodeProtocol.h
+++ b/React/Views/RCTViewNodeProtocol.h
@@ -35,7 +35,7 @@
@end
// TODO: this is kinda dumb - let's come up with a
-// better way of identifying root react views please!
+// better way of identifying root React views please!
static inline BOOL RCTIsReactRootView(NSNumber *reactTag) {
return reactTag.integerValue % 10 == 1;
}
diff --git a/package.json b/package.json
index be9d9b943b67bb..fde82307eaefa9 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"main": "Libraries/react-native/react-native.js",
"files": [
"React",
+ "React.podspec",
"Examples/SampleApp",
"Libraries",
"packager",
@@ -54,7 +55,7 @@
"optimist": "0.6.1",
"promise": "^7.0.0",
"react-timer-mixin": "^0.13.1",
- "react-tools": "0.13.1",
+ "react-tools": "0.13.2",
"rebound": "^0.0.12",
"sane": "1.0.3",
"source-map": "0.1.31",
diff --git a/packager/packager.js b/packager/packager.js
index 212e17f715e547..48552d93edbc7d 100644
--- a/packager/packager.js
+++ b/packager/packager.js
@@ -213,7 +213,8 @@ function runServer(
.use(openStackFrameInEditor)
.use(getDevToolsLauncher(options))
.use(statusPageMiddleware)
- .use(getFlowTypeCheckMiddleware(options))
+ // Temporarily disable flow check until it's more stable
+ //.use(getFlowTypeCheckMiddleware(options))
.use(getAppMiddleware(options));
options.projectRoots.forEach(function(root) {
diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js
index 8df5bbcc05ab41..80e62f05dee332 100644
--- a/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js
+++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js
@@ -3,53 +3,14 @@
*
* @provides Array.prototype.es6
* @polyfill
- * @requires __DEV__
*/
/*eslint-disable */
/*jslint bitwise: true */
-(function (undefined) {
- if (__DEV__) {
- // Define DEV-only setter that blows up when someone incorrectly
- // iterates over arrays.
- try {
- Object.defineProperty && Object.defineProperty(
- Array.prototype,
- '__ARRAY_ENUMERATION_GUARD__',
- {
- configurable: true,
- enumerable: true,
- get: function() {
- console.error(
- 'Your code is broken! Do not iterate over arrays with ' +
- 'for...in.'
- );
- }
- }
- );
- } catch (e) {
- // Nothing
- }
- }
-
+(function(undefined) {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
function findIndex(predicate, context) {
- /**
- * Why am I seeing this "findIndex" method as a value in my array!?
- *
- * We polyfill the "findIndex" method -- called like
- * `[1, 2, 3].findIndex(1)` -- for older browsers. A side effect of the way
- * we do that is that the method is enumerable. If you were incorrectly
- * iterating over your array using the object property iterator syntax
- * `for (key in obj)` you will see the method name "findIndex" as a key.
- *
- * To fix your code please do one of the following:
- *
- * - Use a regular for loop with index.
- * - Use one of the array methods: a.forEach, a.map, etc.
- * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check.
- */
if (this == null) {
throw new TypeError(
'Array.prototype.findIndex called on null or undefined'
@@ -69,32 +30,29 @@
}
if (!Array.prototype.findIndex) {
- Array.prototype.findIndex = findIndex;
+ Object.defineProperty(Array.prototype, 'findIndex', {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: findIndex
+ });
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
if (!Array.prototype.find) {
- Array.prototype.find = function(predicate, context) {
- /**
- * Why am I seeing this "find" method as a value in my array!?
- *
- * We polyfill the "find" method -- called like
- * `[1, 2, 3].find(1)` -- for older browsers. A side effect of the way
- * we do that is that the method is enumerable. If you were incorrectly
- * iterating over your array using the object property iterator syntax
- * `for (key in obj)` you will see the method name "find" as a key.
- *
- * To fix your code please do one of the following:
- *
- * - Use a regular for loop with index.
- * - Use one of the array methods: a.forEach, a.map, etc.
- * - Guard your body of your loop with a `arr.hasOwnProperty(key)` check.
- */
- if (this == null) {
- throw new TypeError('Array.prototype.find called on null or undefined');
+ Object.defineProperty(Array.prototype, 'find', {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function(predicate, context) {
+ if (this == null) {
+ throw new TypeError(
+ 'Array.prototype.find called on null or undefined'
+ );
+ }
+ var index = findIndex.call(this, predicate, context);
+ return index === -1 ? undefined : this[index];
}
- var index = findIndex.call(this, predicate, context);
- return index === -1 ? undefined : this[index];
- };
+ });
}
})();
diff --git a/packager/react-packager/src/FileWatcher/index.js b/packager/react-packager/src/FileWatcher/index.js
index 38ad19bf952675..cd1a28e558cdbb 100644
--- a/packager/react-packager/src/FileWatcher/index.js
+++ b/packager/react-packager/src/FileWatcher/index.js
@@ -26,7 +26,7 @@ var detectingWatcherClass = new Promise(function(resolve) {
module.exports = FileWatcher;
-var MAX_WAIT_TIME = 10000;
+var MAX_WAIT_TIME = 25000;
// Singleton
var fileWatcher = null;
diff --git a/packager/webSocketProxy.js b/packager/webSocketProxy.js
index 8223bbf24b0e78..f863621362e421 100644
--- a/packager/webSocketProxy.js
+++ b/packager/webSocketProxy.js
@@ -34,7 +34,12 @@ function attachToServer(server, path) {
ws.on('message', function(message) {
allClientsExcept(ws).forEach(function(cn) {
- cn.send(message);
+ try {
+ // Sometimes this call throws 'not opened'
+ cn.send(message);
+ } catch(e) {
+ console.warn('WARN: ' + e.message);
+ }
});
});
});