diff --git a/.flowconfig b/.flowconfig index 4f9e8498e5c341..054b0d01d80f30 100644 --- a/.flowconfig +++ b/.flowconfig @@ -29,3 +29,6 @@ Examples/UIExplorer/ImageMocks.js [options] module.system=haste + +[version] +0.10.0 diff --git a/Examples/UIExplorer/BorderExample.js b/Examples/UIExplorer/BorderExample.js index d9c2acf9a654dc..1790dc49164d46 100644 --- a/Examples/UIExplorer/BorderExample.js +++ b/Examples/UIExplorer/BorderExample.js @@ -57,6 +57,17 @@ var styles = StyleSheet.create({ borderLeftWidth: 40, borderLeftColor: 'blue', }, + border5: { + borderRadius: 50, + borderTopWidth: 10, + borderTopColor: 'red', + borderRightWidth: 20, + borderRightColor: 'yellow', + borderBottomWidth: 30, + borderBottomColor: 'green', + borderLeftWidth: 40, + borderLeftColor: 'blue', + }, }); exports.title = 'Border'; @@ -71,7 +82,7 @@ exports.examples = [ }, { title: 'Equal-Width / Same-Color', - description: 'borderWidth & borderColor', + description: 'borderWidth & borderColor & borderRadius', render() { return ; } @@ -97,4 +108,11 @@ exports.examples = [ return ; } }, + { + title: 'Custom Borders', + description: 'border*Width & border*Color', + render() { + return ; + } + }, ]; diff --git a/Examples/UIExplorer/GeolocationExample.js b/Examples/UIExplorer/GeolocationExample.js index c55bd351b09ce4..9bd744678df243 100644 --- a/Examples/UIExplorer/GeolocationExample.js +++ b/Examples/UIExplorer/GeolocationExample.js @@ -50,7 +50,8 @@ var GeolocationExample = React.createClass({ componentDidMount: function() { navigator.geolocation.getCurrentPosition( (initialPosition) => this.setState({initialPosition}), - (error) => console.error(error) + (error) => console.error(error), + {enableHighAccuracy: true, timeout: 100, maximumAge: 1000} ); this.watchID = navigator.geolocation.watchPosition((lastPosition) => { this.setState({lastPosition}); diff --git a/Examples/UIExplorer/UIExplorerBlock.js b/Examples/UIExplorer/UIExplorerBlock.js index 924415e013e211..e7c2a2a8ea9d16 100644 --- a/Examples/UIExplorer/UIExplorerBlock.js +++ b/Examples/UIExplorer/UIExplorerBlock.js @@ -70,6 +70,7 @@ var styles = StyleSheet.create({ }, titleContainer: { borderWidth: 0.5, + borderRadius: 2.5, borderColor: '#d6d7da', backgroundColor: '#f6f7f8', paddingHorizontal: 10, @@ -78,8 +79,10 @@ var styles = StyleSheet.create({ titleRow: { flexDirection: 'row', justifyContent: 'space-between', + backgroundColor: 'transparent', }, titleText: { + backgroundColor: 'transparent', fontSize: 14, fontWeight: '500', }, @@ -97,6 +100,7 @@ var styles = StyleSheet.create({ height: 8, }, children: { + backgroundColor: 'transparent', padding: 10, } }); diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index 8813a8afdb1a01..d1e990cb4b4995 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -94,6 +94,7 @@ var WebViewExample = React.createClass({ automaticallyAdjustContentInsets={false} style={styles.webView} url={this.state.url} + javaScriptEnabledAndroid={true} onNavigationStateChange={this.onNavigationStateChange} startInLoadingState={true} /> diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js b/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js index 28da1bc321fe53..7eed36e01b1d87 100644 --- a/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js +++ b/Libraries/BatchedBridge/BatchedBridgedModules/__mocks__/NativeModules.js @@ -29,6 +29,7 @@ var NativeModules = { UIManager: { customBubblingEventTypes: {}, customDirectEventTypes: {}, + Dimensions: {}, }, AsyncLocalStorage: { getItem: jest.genMockFunction(), diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js index 3a44020a663590..a3f1fe6be8c3d5 100644 --- a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js +++ b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js @@ -18,21 +18,16 @@ var React = require('React'); var StyleSheet = require('StyleSheet'); var View = require('View'); -var keyMirror = require('keyMirror'); var requireNativeComponent = require('requireNativeComponent'); var verifyPropTypes = require('verifyPropTypes'); -var SpinnerSize = keyMirror({ - large: null, - small: null, -}); - var GRAY = '#999999'; type DefaultProps = { animating: boolean; - size: 'small' | 'large'; color: string; + hidesWhenStopped: boolean; + size: 'small' | 'large'; }; var ActivityIndicatorIOS = React.createClass({ @@ -47,7 +42,10 @@ var ActivityIndicatorIOS = React.createClass({ * The foreground color of the spinner (default is gray). */ color: PropTypes.string, - + /** + * Whether the indicator should hide when not animating (true by default). + */ + hidesWhenStopped: PropTypes.bool, /** * Size of the indicator. Small has a height of 20, large has a height of 36. */ @@ -60,27 +58,18 @@ var ActivityIndicatorIOS = React.createClass({ getDefaultProps: function(): DefaultProps { return { animating: true, - size: SpinnerSize.small, color: GRAY, + hidesWhenStopped: true, + size: 'small', }; }, render: function() { - var style = styles.sizeSmall; - var NativeConstants = NativeModules.UIManager.UIActivityIndicatorView.Constants; - var activityIndicatorViewStyle = NativeConstants.StyleWhite; - if (this.props.size === 'large') { - style = styles.sizeLarge; - activityIndicatorViewStyle = NativeConstants.StyleWhiteLarge; - } + var {style, ...props} = this.props; + var sizeStyle = (this.props.size === 'large') ? styles.sizeLarge : styles.sizeSmall; return ( - - + + ); } @@ -99,15 +88,15 @@ var styles = StyleSheet.create({ } }); -var UIActivityIndicatorView = requireNativeComponent( - 'UIActivityIndicatorView', +var RCTActivityIndicatorView = requireNativeComponent( + 'RCTActivityIndicatorView', null ); if (__DEV__) { var nativeOnlyProps = {activityIndicatorViewStyle: true}; verifyPropTypes( ActivityIndicatorIOS, - UIActivityIndicatorView.viewConfig, + RCTActivityIndicatorView.viewConfig, nativeOnlyProps ); } 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..9de1e15e92176f 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -445,6 +445,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} 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/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/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..16052c6fa69605 100644 --- a/Libraries/ReactIOS/renderApplication.ios.js +++ b/Libraries/ReactIOS/renderApplication.ios.js @@ -12,6 +12,9 @@ 'use strict'; var React = require('React'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); +var WarningBox = require('WarningBox'); var invariant = require('invariant'); @@ -24,12 +27,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/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/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/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/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 48fd672a3926d4..7dc2322f0c0813 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`. */ @@ -353,7 +355,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 +377,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 +453,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); } @@ -795,6 +816,7 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL _bundleURL = bundleURL; _moduleProvider = block; _launchOptions = [launchOptions copy]; + [self setUp]; [self bindKeys]; } @@ -872,6 +894,8 @@ - (void)setUp dispatch_queue_t queue = [module methodQueue]; if (queue) { _queuesByID[moduleID] = queue; + } else { + _queuesByID[moduleID] = [NSNull null]; } } }]; @@ -1128,6 +1152,16 @@ - (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); + } +} + - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args context:(NSNumber *)context { #if BATCHED_BRIDGE @@ -1235,10 +1269,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 +1306,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 +1335,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..eacb03b857a91d 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; } 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/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..0b4a3873c8c0cc 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -201,3 +201,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..f5089a9d6a76ca 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,22 @@ - (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(); - } + /** + * Always dispatch async, ensure there are no sync calls on the JS thread + * otherwise timers can cause a deadlock + */ + [self performSelector:@selector(_runBlock:) + 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..fce2aae428aa55 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -33,7 +33,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 */; }; @@ -136,8 +136,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 = ""; }; @@ -317,8 +317,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 */, @@ -499,7 +499,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 */, diff --git a/React/Views/RCTUIActivityIndicatorViewManager.h b/React/Views/RCTActivityIndicatorViewManager.h similarity index 64% rename from React/Views/RCTUIActivityIndicatorViewManager.h rename to React/Views/RCTActivityIndicatorViewManager.h index e5a10fdd75c746..cbd6816ae4cddf 100644 --- a/React/Views/RCTUIActivityIndicatorViewManager.h +++ b/React/Views/RCTActivityIndicatorViewManager.h @@ -9,6 +9,12 @@ #import "RCTViewManager.h" -@interface RCTUIActivityIndicatorViewManager : RCTViewManager +@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 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/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 a29d697447e32d..276f0a31046340 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,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/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); + } }); }); });