diff --git a/src/renderers/native/ReactIOS/IOSDefaultEventPluginOrder.js b/src/renderers/native/ReactIOS/IOSDefaultEventPluginOrder.js new file mode 100644 index 0000000000..80098fc414 --- /dev/null +++ b/src/renderers/native/ReactIOS/IOSDefaultEventPluginOrder.js @@ -0,0 +1,19 @@ +/** + * 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 IOSDefaultEventPluginOrder + * @flow + */ +'use strict'; + +var IOSDefaultEventPluginOrder = [ + 'ResponderEventPlugin', + 'IOSNativeBridgeEventPlugin' +]; + +module.exports = IOSDefaultEventPluginOrder; diff --git a/src/renderers/native/ReactIOS/IOSNativeBridgeEventPlugin.js b/src/renderers/native/ReactIOS/IOSNativeBridgeEventPlugin.js new file mode 100644 index 0000000000..d1ffc1d402 --- /dev/null +++ b/src/renderers/native/ReactIOS/IOSNativeBridgeEventPlugin.js @@ -0,0 +1,75 @@ +/** + * 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 IOSNativeBridgeEventPlugin + * @flow + */ +'use strict'; + +var EventPropagators = require('EventPropagators'); +var SyntheticEvent = require('SyntheticEvent'); +var UIManager = require('UIManager'); + +var merge = require('merge'); +var warning = require('fbjs/lib/warning'); + +var customBubblingEventTypes = UIManager.customBubblingEventTypes; +var customDirectEventTypes = UIManager.customDirectEventTypes; + +var allTypesByEventName = {}; + +for (var bubblingTypeName in customBubblingEventTypes) { + allTypesByEventName[bubblingTypeName] = customBubblingEventTypes[bubblingTypeName]; +} + +for (var directTypeName in customDirectEventTypes) { + warning( + !customBubblingEventTypes[directTypeName], + 'Event cannot be both direct and bubbling: %s', + directTypeName + ); + allTypesByEventName[directTypeName] = customDirectEventTypes[directTypeName]; +} + +var IOSNativeBridgeEventPlugin = { + + eventTypes: merge(customBubblingEventTypes, customDirectEventTypes), + + /** + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of synthetic events. + * @see {EventPluginHub.extractEvents} + */ + extractEvents: function( + topLevelType: string, + topLevelTarget: EventTarget, + topLevelTargetID: string, + nativeEvent: Event + ): ?Object { + var bubbleDispatchConfig = customBubblingEventTypes[topLevelType]; + var directDispatchConfig = customDirectEventTypes[topLevelType]; + var event = SyntheticEvent.getPooled( + bubbleDispatchConfig || directDispatchConfig, + topLevelTargetID, + nativeEvent + ); + if (bubbleDispatchConfig) { + EventPropagators.accumulateTwoPhaseDispatches(event); + } else if (directDispatchConfig) { + EventPropagators.accumulateDirectDispatches(event); + } else { + return null; + } + return event; + } +}; + +module.exports = IOSNativeBridgeEventPlugin; diff --git a/src/renderers/native/ReactIOS/NativeMethodsMixin.js b/src/renderers/native/ReactIOS/NativeMethodsMixin.js new file mode 100644 index 0000000000..88f451a58c --- /dev/null +++ b/src/renderers/native/ReactIOS/NativeMethodsMixin.js @@ -0,0 +1,219 @@ +/** + * 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 NativeMethodsMixin + * @flow + */ +'use strict'; + +var ReactNativeAttributePayload = require('ReactNativeAttributePayload'); +var TextInputState = require('TextInputState'); +var UIManager = require('UIManager'); + +var findNodeHandle = require('findNodeHandle'); +var invariant = require('fbjs/lib/invariant'); + +type MeasureOnSuccessCallback = ( + x: number, + y: number, + width: number, + height: number, + pageX: number, + pageY: number +) => void + +type MeasureInWindowOnSuccessCallback = ( + x: number, + y: number, + width: number, + height: number, +) => void + +type MeasureLayoutOnSuccessCallback = ( + left: number, + top: number, + width: number, + height: number +) => void + +function warnForStyleProps(props, validAttributes) { + for (var key in validAttributes.style) { + if (!(validAttributes[key] || props[key] === undefined)) { + console.error( + 'You are setting the style `{ ' + key + ': ... }` as a prop. You ' + + 'should nest it in a style object. ' + + 'E.g. `{ style: { ' + key + ': ... } }`' + ); + } + } +} + +/** + * `NativeMethodsMixin` provides methods to access the underlying native + * component directly. This can be useful in cases when you want to focus + * a view or measure its on-screen dimensions, for example. + * + * The methods described here are available on most of the default components + * provided by React Native. Note, however, that they are *not* available on + * composite components that aren't directly backed by a native view. This will + * generally include most components that you define in your own app. For more + * information, see [Direct + * Manipulation](docs/direct-manipulation.html). + */ +var NativeMethodsMixin = { + /** + * Determines the location on screen, width, and height of the given view and + * returns the values via an async callback. If successful, the callback will + * be called with the following arguments: + * + * - x + * - y + * - width + * - height + * - pageX + * - pageY + * + * Note that these measurements are not available until after the rendering + * has been completed in native. If you need the measurements as soon as + * possible, consider using the [`onLayout` + * prop](docs/view.html#onlayout) instead. + */ + measure: function(callback: MeasureOnSuccessCallback) { + UIManager.measure( + findNodeHandle(this), + mountSafeCallback(this, callback) + ); + }, + + /** + * Determines the location of the given view in the window and returns the + * values via an async callback. If the React root view is embedded in + * another native view, this will give you the absolute coordinates. If + * successful, the callback will be called with the following + * arguments: + * + * - x + * - y + * - width + * - height + * + * Note that these measurements are not available until after the rendering + * has been completed in native. + */ + measureInWindow: function(callback: MeasureInWindowOnSuccessCallback) { + UIManager.measureInWindow( + findNodeHandle(this), + mountSafeCallback(this, callback) + ); + }, + + /** + * Like [`measure()`](#measure), but measures the view relative an ancestor, + * specified as `relativeToNativeNode`. This means that the returned x, y + * are relative to the origin x, y of the ancestor view. + * + * As always, to obtain a native node handle for a component, you can use + * `React.findNodeHandle(component)`. + */ + measureLayout: function( + relativeToNativeNode: number, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail: () => void /* currently unused */ + ) { + UIManager.measureLayout( + findNodeHandle(this), + relativeToNativeNode, + mountSafeCallback(this, onFail), + mountSafeCallback(this, onSuccess) + ); + }, + + /** + * This function sends props straight to native. They will not participate in + * future diff process - this means that if you do not include them in the + * next render, they will remain active (see [Direct + * Manipulation](docs/direct-manipulation.html)). + */ + setNativeProps: function(nativeProps: Object) { + if (__DEV__) { + warnForStyleProps(nativeProps, this.viewConfig.validAttributes); + } + + var updatePayload = ReactNativeAttributePayload.create( + nativeProps, + this.viewConfig.validAttributes + ); + + UIManager.updateView( + findNodeHandle(this), + this.viewConfig.uiViewClassName, + updatePayload + ); + }, + + /** + * Requests focus for the given input or view. The exact behavior triggered + * will depend on the platform and type of view. + */ + focus: function() { + TextInputState.focusTextInput(findNodeHandle(this)); + }, + + /** + * Removes focus from an input or view. This is the opposite of `focus()`. + */ + blur: function() { + TextInputState.blurTextInput(findNodeHandle(this)); + } +}; + +function throwOnStylesProp(component, props) { + if (props.styles !== undefined) { + var owner = component._owner || null; + var name = component.constructor.displayName; + var msg = '`styles` is not a supported property of `' + name + '`, did ' + + 'you mean `style` (singular)?'; + if (owner && owner.constructor && owner.constructor.displayName) { + msg += '\n\nCheck the `' + owner.constructor.displayName + '` parent ' + + ' component.'; + } + throw new Error(msg); + } +} +if (__DEV__) { + // hide this from Flow since we can't define these properties outside of + // __DEV__ without actually implementing them (setting them to undefined + // isn't allowed by ReactClass) + var NativeMethodsMixin_DEV = (NativeMethodsMixin: any); + invariant( + !NativeMethodsMixin_DEV.componentWillMount && + !NativeMethodsMixin_DEV.componentWillReceiveProps, + 'Do not override existing functions.' + ); + NativeMethodsMixin_DEV.componentWillMount = function () { + throwOnStylesProp(this, this.props); + }; + NativeMethodsMixin_DEV.componentWillReceiveProps = function (newProps) { + throwOnStylesProp(this, newProps); + }; +} + +/** + * In the future, we should cleanup callbacks by cancelling them instead of + * using this. + */ +var mountSafeCallback = function(context: ReactComponent, callback: ?Function): any { + return function() { + if (!callback || (context.isMounted && !context.isMounted())) { + return; + } + return callback.apply(context, arguments); + }; +}; + +module.exports = NativeMethodsMixin; diff --git a/src/renderers/native/ReactIOS/YellowBox.js b/src/renderers/native/ReactIOS/YellowBox.js new file mode 100644 index 0000000000..e1dddc1454 --- /dev/null +++ b/src/renderers/native/ReactIOS/YellowBox.js @@ -0,0 +1,364 @@ +/** + * 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 YellowBox + * @flow + */ + +'use strict'; + +const EventEmitter = require('EventEmitter'); +import type EmitterSubscription from 'EmitterSubscription'; +const Platform = require('Platform'); +const React = require('React'); +const StyleSheet = require('StyleSheet'); + +const _warningEmitter = new EventEmitter(); +const _warningMap = new Map(); + +/** + * YellowBox renders warnings at the bottom of the app being developed. + * + * Warnings help guard against subtle yet significant issues that can impact the + * quality of the app. This "in your face" style of warning allows developers to + * notice and correct these issues as quickly as possible. + * + * By default, the warning box is enabled in `__DEV__`. Set the following flag + * to disable it (and call `console.warn` to update any rendered ): + * + * console.disableYellowBox = true; + * console.warn('YellowBox is disabled.'); + * + * Warnings can be ignored programmatically by setting the array: + * + * console.ignoredYellowBox = ['Warning: ...']; + * + * Strings in `console.ignoredYellowBox` can be a prefix of the warning that + * should be ignored. + */ + +if (__DEV__) { + const {error, warn} = console; + console.error = function() { + error.apply(console, arguments); + // Show yellow box for the `warning` module. + if (typeof arguments[0] === 'string' && + arguments[0].startsWith('Warning: ')) { + updateWarningMap.apply(null, arguments); + } + }; + console.warn = function() { + warn.apply(console, arguments); + updateWarningMap.apply(null, arguments); + }; +} + +/** + * Simple function for formatting strings. + * + * Replaces placeholders with values passed as extra arguments + * + * @param {string} format the base string + * @param ...args the values to insert + * @return {string} the replaced string + */ +function sprintf(format, ...args) { + var index = 0; + return format.replace(/%s/g, match => args[index++]); +} + +function updateWarningMap(format, ...args): void { + const stringifySafe = require('stringifySafe'); + + format = String(format); + const argCount = (format.match(/%s/g) || []).length; + const warning = [ + sprintf(format, ...args.slice(0, argCount)), + ...args.slice(argCount).map(stringifySafe), + ].join(' '); + + const count = _warningMap.has(warning) ? _warningMap.get(warning) : 0; + _warningMap.set(warning, count + 1); + _warningEmitter.emit('warning', _warningMap); +} + +function isWarningIgnored(warning: string): boolean { + return ( + Array.isArray(console.ignoredYellowBox) && + console.ignoredYellowBox.some( + ignorePrefix => warning.startsWith(ignorePrefix) + ) + ); +} + +const WarningRow = ({count, warning, onPress}) => { + const Text = require('Text'); + const TouchableHighlight = require('TouchableHighlight'); + const View = require('View'); + + const countText = count > 1 ? + {'(' + count + ') '} : + null; + + return ( + + + + {countText} + {warning} + + + + ); +}; + +const WarningInspector = ({ + count, + warning, + onClose, + onDismiss, + onDismissAll, +}) => { + const ScrollView = require('ScrollView'); + const Text = require('Text'); + const TouchableHighlight = require('TouchableHighlight'); + const View = require('View'); + + const countSentence = + 'Warning encountered ' + count + ' time' + (count - 1 ? 's' : '') + '.'; + + return ( + + + + {countSentence} + + + {warning} + + + + + Dismiss + + + + + Dismiss All + + + + + + ); +}; + +class YellowBox extends React.Component { + state: { + inspecting: ?string; + warningMap: Map; + }; + _listener: ?EmitterSubscription; + + constructor(props: mixed, context: mixed) { + super(props, context); + this.state = { + inspecting: null, + warningMap: _warningMap, + }; + this.dismissWarning = warning => { + const {inspecting, warningMap} = this.state; + if (warning) { + warningMap.delete(warning); + } else { + warningMap.clear(); + } + this.setState({ + inspecting: (warning && inspecting !== warning) ? inspecting : null, + warningMap, + }); + }; + } + + componentDidMount() { + let scheduled = null; + this._listener = _warningEmitter.addListener('warning', warningMap => { + // Use `setImmediate` because warnings often happen during render, but + // state cannot be set while rendering. + scheduled = scheduled || setImmediate(() => { + scheduled = null; + this.setState({ + warningMap, + }); + }); + }); + } + + componentWillUnmount() { + if (this._listener) { + this._listener.remove(); + } + } + + render() { + if (console.disableYellowBox || this.state.warningMap.size === 0) { + return null; + } + const ScrollView = require('ScrollView'); + const View = require('View'); + + const inspecting = this.state.inspecting; + const inspector = inspecting !== null ? + this.setState({inspecting: null})} + onDismiss={() => this.dismissWarning(inspecting)} + onDismissAll={() => this.dismissWarning(null)} + /> : + null; + + const rows = []; + this.state.warningMap.forEach((count, warning) => { + if (!isWarningIgnored(warning)) { + rows.push( + this.setState({inspecting: warning})} + onDismiss={() => this.dismissWarning(warning)} + /> + ); + } + }); + + const listStyle = [ + styles.list, + // Additional `0.4` so the 5th row can peek into view. + {height: Math.min(rows.length, 4.4) * (rowGutter + rowHeight)}, + ]; + return ( + + + {rows} + + {inspector} + + ); + } +} + +const backgroundColor = opacity => 'rgba(250, 186, 48, ' + opacity + ')'; +const textColor = 'white'; +const rowGutter = 1; +const rowHeight = 46; + +var styles = StyleSheet.create({ + fullScreen: { + backgroundColor: 'transparent', + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + inspector: { + backgroundColor: backgroundColor(0.95), + flex: 1, + }, + inspectorContainer: { + flex: 1, + }, + inspectorButtons: { + flexDirection: 'row', + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + }, + inspectorButton: { + flex: 1, + padding: 22, + }, + inspectorButtonText: { + color: textColor, + fontSize: 14, + opacity: 0.8, + textAlign: 'center', + }, + inspectorContent: { + flex: 1, + paddingTop: 5, + }, + inspectorCount: { + padding: 15, + paddingBottom: 0, + }, + inspectorCountText: { + color: textColor, + fontSize: 14, + }, + inspectorWarning: { + padding: 15, + position: 'absolute', + top: 39, + bottom: 60, + }, + inspectorWarningText: { + color: textColor, + fontSize: 16, + fontWeight: '600', + }, + list: { + backgroundColor: 'transparent', + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + }, + listRow: { + position: 'relative', + backgroundColor: backgroundColor(0.95), + flex: 1, + height: rowHeight, + marginTop: rowGutter, + }, + listRowContent: { + flex: 1, + }, + listRowCount: { + color: 'rgba(255, 255, 255, 0.5)', + }, + listRowText: { + color: textColor, + position: 'absolute', + left: 0, + top: Platform.OS === 'android' ? 5 : 7, + marginLeft: 15, + marginRight: 15, + }, +}); + +module.exports = YellowBox; diff --git a/src/renderers/native/ReactIOS/renderApplication.android.js b/src/renderers/native/ReactIOS/renderApplication.android.js new file mode 100644 index 0000000000..03191bbc03 --- /dev/null +++ b/src/renderers/native/ReactIOS/renderApplication.android.js @@ -0,0 +1,139 @@ +/** + * 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 renderApplication + */ + +'use strict'; + +var Inspector = require('Inspector'); +var Portal = require('Portal'); +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Subscribable = require('Subscribable'); +var View = require('View'); + +var invariant = require('fbjs/lib/invariant'); + +var YellowBox = __DEV__ ? require('YellowBox') : null; + +// require BackAndroid so it sets the default handler that exits the app if no listeners respond +require('BackAndroid'); + +var AppContainer = React.createClass({ + mixins: [Subscribable.Mixin], + + getInitialState: function() { + return { + enabled: __DEV__, + inspectorVisible: false, + rootNodeHandle: null, + rootImportanceForAccessibility: 'auto', + }; + }, + + toggleElementInspector: function() { + this.setState({ + inspectorVisible: !this.state.inspectorVisible, + rootNodeHandle: React.findNodeHandle(this.refs.main), + }); + }, + + componentDidMount: function() { + this.addListenerOn( + RCTDeviceEventEmitter, + 'toggleElementInspector', + this.toggleElementInspector + ); + + this._unmounted = false; + }, + + renderInspector: function() { + return this.state.inspectorVisible ? + : + null; + }, + + componentWillUnmount: function() { + this._unmounted = true; + }, + + setRootAccessibility: function(modalVisible) { + if (this._unmounted) { + return; + } + + this.setState({ + rootImportanceForAccessibility: modalVisible ? 'no-hide-descendants' : 'auto', + }); + }, + + render: function() { + var RootComponent = this.props.rootComponent; + var appView = + + + + ; + let yellowBox = null; + if (__DEV__) { + yellowBox = ; + } + return this.state.enabled ? + + {appView} + {yellowBox} + {this.renderInspector()} + : + appView; + } +}); + +function renderApplication( + RootComponent: ReactClass

, + initialProps: P, + rootTag: any +) { + invariant( + rootTag, + 'Expect to have a valid rootTag, instead got ', rootTag + ); + React.render( + , + rootTag + ); +} + +var styles = StyleSheet.create({ + // This is needed so the application covers the whole screen + // and therefore the contents of the Portal are not clipped. + appContainer: { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + }, +}); + +module.exports = renderApplication; diff --git a/src/renderers/native/ReactIOS/renderApplication.ios.js b/src/renderers/native/ReactIOS/renderApplication.ios.js new file mode 100644 index 0000000000..c3e4f81780 --- /dev/null +++ b/src/renderers/native/ReactIOS/renderApplication.ios.js @@ -0,0 +1,96 @@ +/** + * 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 renderApplication + * @noflow + */ + +'use strict'; + +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Subscribable = require('Subscribable'); +var View = require('View'); + +var invariant = require('fbjs/lib/invariant'); + +var Inspector = __DEV__ ? require('Inspector') : null; +var YellowBox = __DEV__ ? require('YellowBox') : null; + +var AppContainer = React.createClass({ + mixins: [Subscribable.Mixin], + + getInitialState: function() { + return { inspector: null }; + }, + + toggleElementInspector: function() { + var inspector = !__DEV__ || this.state.inspector + ? null + : ; + this.setState({inspector}); + }, + + componentDidMount: function() { + this.addListenerOn( + RCTDeviceEventEmitter, + 'toggleElementInspector', + this.toggleElementInspector + ); + }, + + render: function() { + let yellowBox = null; + if (__DEV__) { + yellowBox = ; + } + return ( + + + {this.props.children} + + {yellowBox} + {this.state.inspector} + + ); + } +}); + +function renderApplication( + RootComponent: ReactClass

, + initialProps: P, + rootTag: any +) { + invariant( + rootTag, + 'Expect to have a valid rootTag, instead got ', rootTag + ); + /* eslint-disable jsx-no-undef-with-namespace */ + React.render( + + + , + rootTag + ); + /* eslint-enable jsx-no-undef-with-namespace */ +} + +var styles = StyleSheet.create({ + appContainer: { + flex: 1, + }, +}); + +module.exports = renderApplication; diff --git a/src/renderers/native/ReactIOS/requireNativeComponent.js b/src/renderers/native/ReactIOS/requireNativeComponent.js new file mode 100644 index 0000000000..61993f799f --- /dev/null +++ b/src/renderers/native/ReactIOS/requireNativeComponent.js @@ -0,0 +1,127 @@ +/** + * 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 requireNativeComponent + * @flow + */ +'use strict'; + +var ReactNativeStyleAttributes = require('ReactNativeStyleAttributes'); +var UIManager = require('UIManager'); +var UnimplementedView = require('UnimplementedView'); + +var createReactNativeComponentClass = require('createReactNativeComponentClass'); + +var insetsDiffer = require('insetsDiffer'); +var pointsDiffer = require('pointsDiffer'); +var matricesDiffer = require('matricesDiffer'); +var processColor = require('processColor'); +var resolveAssetSource = require('resolveAssetSource'); +var sizesDiffer = require('sizesDiffer'); +var verifyPropTypes = require('verifyPropTypes'); +var warning = require('fbjs/lib/warning'); + +/** + * Used to create React components that directly wrap native component + * implementations. Config information is extracted from data exported from the + * UIManager module. You should also wrap the native component in a + * hand-written component with full propTypes definitions and other + * documentation - pass the hand-written component in as `componentInterface` to + * verify all the native props are documented via `propTypes`. + * + * If some native props shouldn't be exposed in the wrapper interface, you can + * pass null for `componentInterface` and call `verifyPropTypes` directly + * with `nativePropsToIgnore`; + * + * Common types are lined up with the appropriate prop differs with + * `TypeToDifferMap`. Non-scalar types not in the map default to `deepDiffer`. + */ +import type { ComponentInterface } from 'verifyPropTypes'; + +function requireNativeComponent( + viewName: string, + componentInterface?: ?ComponentInterface, + extraConfig?: ?{nativeOnly?: Object}, +): Function { + var viewConfig = UIManager[viewName]; + if (!viewConfig || !viewConfig.NativeProps) { + warning(false, 'Native component for "%s" does not exist', viewName); + return UnimplementedView; + } + var nativeProps = { + ...UIManager.RCTView.NativeProps, + ...viewConfig.NativeProps, + }; + viewConfig.uiViewClassName = viewName; + viewConfig.validAttributes = {}; + viewConfig.propTypes = componentInterface && componentInterface.propTypes; + for (var key in nativeProps) { + var useAttribute = false; + var attribute = {}; + + var differ = TypeToDifferMap[nativeProps[key]]; + if (differ) { + attribute.diff = differ; + useAttribute = true; + } + + var processor = TypeToProcessorMap[nativeProps[key]]; + if (processor) { + attribute.process = processor; + useAttribute = true; + } + + viewConfig.validAttributes[key] = useAttribute ? attribute : true; + } + + // Unfortunately, the current set up puts the style properties on the top + // level props object. We also need to add the nested form for API + // compatibility. This allows these props on both the top level and the + // nested style level. TODO: Move these to nested declarations on the + // native side. + viewConfig.validAttributes.style = ReactNativeStyleAttributes; + + if (__DEV__) { + componentInterface && verifyPropTypes( + componentInterface, + viewConfig, + extraConfig && extraConfig.nativeOnly + ); + } + return createReactNativeComponentClass(viewConfig); +} + +var TypeToDifferMap = { + // iOS Types + CATransform3D: matricesDiffer, + CGPoint: pointsDiffer, + CGSize: sizesDiffer, + UIEdgeInsets: insetsDiffer, + // Android Types + // (not yet implemented) +}; + +function processColorArray(colors: []): [] { + return colors && colors.map(processColor); +} + +var TypeToProcessorMap = { + // iOS Types + CGColor: processColor, + CGColorArray: processColorArray, + UIColor: processColor, + UIColorArray: processColorArray, + CGImage: resolveAssetSource, + UIImage: resolveAssetSource, + RCTImageSource: resolveAssetSource, + // Android Types + Color: processColor, + ColorArray: processColorArray, +}; + +module.exports = requireNativeComponent; diff --git a/src/renderers/native/ReactIOS/verifyPropTypes.js b/src/renderers/native/ReactIOS/verifyPropTypes.js new file mode 100644 index 0000000000..d284d60ec0 --- /dev/null +++ b/src/renderers/native/ReactIOS/verifyPropTypes.js @@ -0,0 +1,61 @@ +/** + * 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 verifyPropTypes + * @flow + */ +'use strict'; + +var ReactNativeStyleAttributes = require('ReactNativeStyleAttributes'); + +export type ComponentInterface = ReactClass | { + name?: string; + displayName?: string; + propTypes: Object; +}; + +function verifyPropTypes( + componentInterface: ComponentInterface, + viewConfig: Object, + nativePropsToIgnore?: ?Object +) { + if (!viewConfig) { + return; // This happens for UnimplementedView. + } + var componentName = componentInterface.name || + componentInterface.displayName || + 'unknown'; + if (!componentInterface.propTypes) { + throw new Error( + '`' + componentName + '` has no propTypes defined`' + ); + } + + var nativeProps = viewConfig.NativeProps; + for (var prop in nativeProps) { + if (!componentInterface.propTypes[prop] && + !ReactNativeStyleAttributes[prop] && + (!nativePropsToIgnore || !nativePropsToIgnore[prop])) { + var message; + if (componentInterface.propTypes.hasOwnProperty(prop)) { + message = '`' + componentName + '` has incorrectly defined propType for native prop `' + + viewConfig.uiViewClassName + '.' + prop + '` of native type `' + nativeProps[prop]; + } else { + message = '`' + componentName + '` has no propType for native prop `' + + viewConfig.uiViewClassName + '.' + prop + '` of native type `' + + nativeProps[prop] + '`'; + }; + message += '\nIf you haven\'t changed this prop yourself, this usually means that ' + + 'your versions of the native code and JavaScript code are out of sync. Updating both ' + + 'should make this error go away.'; + throw new Error(message); + } + } +} + +module.exports = verifyPropTypes; diff --git a/src/renderers/native/ReactNative/React.js b/src/renderers/native/ReactNative/React.js new file mode 100644 index 0000000000..9cd4464f28 --- /dev/null +++ b/src/renderers/native/ReactNative/React.js @@ -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. + * + * @providesModule React + * @flow + */ +'use strict'; + +module.exports = require('ReactNative'); diff --git a/src/renderers/native/ReactNative/ReactDOM.js b/src/renderers/native/ReactNative/ReactDOM.js new file mode 100644 index 0000000000..7ac737c132 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactDOM.js @@ -0,0 +1,16 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule ReactDOM + */ + +'use strict'; + +var ReactUpdates = require('ReactUpdates'); + +// Temporary shim required for ReactTestUtils and Relay. +var ReactDOM = { + unstable_batchedUpdates: ReactUpdates.batchedUpdates, +}; + +module.exports = ReactDOM; diff --git a/src/renderers/native/ReactNative/ReactNative.js b/src/renderers/native/ReactNative/ReactNative.js new file mode 100644 index 0000000000..148a366663 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNative.js @@ -0,0 +1,138 @@ +/** + * 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 ReactNative + * @flow + */ +'use strict'; + +// Require ReactNativeDefaultInjection first for its side effects of setting up +// the JS environment +var ReactNativeDefaultInjection = require('ReactNativeDefaultInjection'); + +var ReactChildren = require('ReactChildren'); +var ReactClass = require('ReactClass'); +var ReactComponent = require('ReactComponent'); +var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactElement = require('ReactElement'); +var ReactElementValidator = require('ReactElementValidator'); +var ReactInstanceHandles = require('ReactInstanceHandles'); +var ReactNativeMount = require('ReactNativeMount'); +var ReactPropTypes = require('ReactPropTypes'); +var ReactUpdates = require('ReactUpdates'); + +var findNodeHandle = require('findNodeHandle'); +var invariant = require('fbjs/lib/invariant'); +var onlyChild = require('onlyChild'); +var warning = require('fbjs/lib/warning'); + +ReactNativeDefaultInjection.inject(); + +var createElement = ReactElement.createElement; +var createFactory = ReactElement.createFactory; +var cloneElement = ReactElement.cloneElement; + +if (__DEV__) { + createElement = ReactElementValidator.createElement; + createFactory = ReactElementValidator.createFactory; + cloneElement = ReactElementValidator.cloneElement; +} + +var resolveDefaultProps = function(element) { + // Could be optimized, but not currently in heavy use. + var defaultProps = element.type.defaultProps; + var props = element.props; + for (var propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } +}; + +// Experimental optimized element creation +var augmentElement = function(element: ReactElement): ReactElement { + if (__DEV__) { + invariant( + false, + 'This optimized path should never be used in DEV mode because ' + + 'it does not provide validation. Check your JSX transform.' + ); + } + element._owner = ReactCurrentOwner.current; + if (element.type.defaultProps) { + resolveDefaultProps(element); + } + return element; +}; + +var render = function( + element: ReactElement, + mountInto: number, + callback?: ?(() => void) +): ?ReactComponent { + return ReactNativeMount.renderComponent(element, mountInto, callback); +}; + +var ReactNative = { + hasReactNativeInitialized: false, + Children: { + map: ReactChildren.map, + forEach: ReactChildren.forEach, + count: ReactChildren.count, + toArray: ReactChildren.toArray, + only: onlyChild + }, + Component: ReactComponent, + PropTypes: ReactPropTypes, + createClass: ReactClass.createClass, + createElement: createElement, + createFactory: createFactory, + cloneElement: cloneElement, + _augmentElement: augmentElement, + findNodeHandle: findNodeHandle, + render: render, + unmountComponentAtNode: ReactNativeMount.unmountComponentAtNode, + + /* eslint-disable camelcase */ + unstable_batchedUpdates: ReactUpdates.batchedUpdates, + /* eslint-enable camelcase */ + + // Hook for JSX spread, don't use this for anything else. + __spread: Object.assign, + + unmountComponentAtNodeAndRemoveContainer: ReactNativeMount.unmountComponentAtNodeAndRemoveContainer, + isValidClass: ReactElement.isValidFactory, + isValidElement: ReactElement.isValidElement, + + // Deprecations (remove for 0.13) + renderComponent: function( + element: ReactElement, + mountInto: number, + callback?: ?(() => void) + ): ?ReactComponent { + warning('Use React.render instead of React.renderComponent'); + return ReactNative.render(element, mountInto, callback); + }, +}; + +// Inject the runtime into a devtools global hook regardless of browser. +// Allows for debugging when the hook is injected on the page. +/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__ */ +if ( + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') { + __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({ + CurrentOwner: ReactCurrentOwner, + InstanceHandles: ReactInstanceHandles, + Mount: ReactNativeMount, + Reconciler: require('ReactReconciler'), + TextComponent: require('ReactNativeTextComponent'), + }); +} + +module.exports = ReactNative; diff --git a/src/renderers/native/ReactNative/ReactNativeAttributePayload.js b/src/renderers/native/ReactNative/ReactNativeAttributePayload.js new file mode 100644 index 0000000000..7adf0d27c7 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeAttributePayload.js @@ -0,0 +1,546 @@ +/** + * 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 ReactNativeAttributePayload + * @flow + */ +'use strict'; + +var Platform = require('Platform'); +var ReactNativePropRegistry = require('ReactNativePropRegistry'); + +var deepDiffer = require('deepDiffer'); +var flattenStyle = require('flattenStyle'); + +var emptyObject = {}; + +/** + * Create a payload that contains all the updates between two sets of props. + * + * These helpers are all encapsulated into a single module, because they use + * mutation as a performance optimization which leads to subtle shared + * dependencies between the code paths. To avoid this mutable state leaking + * across modules, I've kept them isolated to this module. + */ + +type AttributeDiffer = (prevProp: mixed, nextProp: mixed) => boolean; +type AttributePreprocessor = (nextProp: mixed) => mixed; + +type CustomAttributeConfiguration = + { diff: AttributeDiffer, process: AttributePreprocessor } | + { diff: AttributeDiffer } | + { process: AttributePreprocessor }; + +type AttributeConfiguration = + { [key: string]: ( + CustomAttributeConfiguration | AttributeConfiguration /*| boolean*/ + ) }; + +type NestedNode = Array | Object | number; + +// Tracks removed keys +var removedKeys = null; +var removedKeyCount = 0; + +function translateKey(propKey: string) : string { + if (propKey === 'transform') { + // We currently special case the key for `transform`. iOS uses the + // transformMatrix name and Android uses the decomposedMatrix name. + // TODO: We could unify these names and just use the name `transform` + // all the time. Just need to update the native side. + if (Platform.OS === 'android') { + return 'decomposedMatrix'; + } else { + return 'transformMatrix'; + } + } + return propKey; +} + +function defaultDiffer(prevProp: mixed, nextProp: mixed) : boolean { + if (typeof nextProp !== 'object' || nextProp === null) { + // Scalars have already been checked for equality + return true; + } else { + // For objects and arrays, the default diffing algorithm is a deep compare + return deepDiffer(prevProp, nextProp); + } +} + +function resolveObject(idOrObject: number | Object) : Object { + if (typeof idOrObject === 'number') { + return ReactNativePropRegistry.getByID(idOrObject); + } + return idOrObject; +} + +function restoreDeletedValuesInNestedArray( + updatePayload: Object, + node: NestedNode, + validAttributes: AttributeConfiguration +) { + if (Array.isArray(node)) { + var i = node.length; + while (i-- && removedKeyCount > 0) { + restoreDeletedValuesInNestedArray( + updatePayload, + node[i], + validAttributes + ); + } + } else if (node && removedKeyCount > 0) { + var obj = resolveObject(node); + for (var propKey in removedKeys) { + if (!removedKeys[propKey]) { + continue; + } + var nextProp = obj[propKey]; + if (nextProp === undefined) { + continue; + } + + var attributeConfig = validAttributes[propKey]; + if (!attributeConfig) { + continue; // not a valid native prop + } + + if (typeof nextProp === 'function') { + nextProp = true; + } + if (typeof nextProp === 'undefined') { + nextProp = null; + } + + if (typeof attributeConfig !== 'object') { + // case: !Object is the default case + updatePayload[propKey] = nextProp; + } else if (typeof attributeConfig.diff === 'function' || + typeof attributeConfig.process === 'function') { + // case: CustomAttributeConfiguration + var nextValue = typeof attributeConfig.process === 'function' ? + attributeConfig.process(nextProp) : + nextProp; + updatePayload[propKey] = nextValue; + } + removedKeys[propKey] = false; + removedKeyCount--; + } + } +} + +function diffNestedArrayProperty( + updatePayload:? Object, + prevArray: Array, + nextArray: Array, + validAttributes: AttributeConfiguration +) : ?Object { + var minLength = prevArray.length < nextArray.length ? + prevArray.length : + nextArray.length; + var i; + for (i = 0; i < minLength; i++) { + // Diff any items in the array in the forward direction. Repeated keys + // will be overwritten by later values. + updatePayload = diffNestedProperty( + updatePayload, + prevArray[i], + nextArray[i], + validAttributes + ); + } + for (; i < prevArray.length; i++) { + // Clear out all remaining properties. + updatePayload = clearNestedProperty( + updatePayload, + prevArray[i], + validAttributes + ); + } + for (; i < nextArray.length; i++) { + // Add all remaining properties. + updatePayload = addNestedProperty( + updatePayload, + nextArray[i], + validAttributes + ); + } + return updatePayload; +} + +function diffNestedProperty( + updatePayload:? Object, + prevProp: NestedNode, + nextProp: NestedNode, + validAttributes: AttributeConfiguration +) : ?Object { + + if (!updatePayload && prevProp === nextProp) { + // If no properties have been added, then we can bail out quickly on object + // equality. + return updatePayload; + } + + if (!prevProp || !nextProp) { + if (nextProp) { + return addNestedProperty( + updatePayload, + nextProp, + validAttributes + ); + } + if (prevProp) { + return clearNestedProperty( + updatePayload, + prevProp, + validAttributes + ); + } + return updatePayload; + } + + if (!Array.isArray(prevProp) && !Array.isArray(nextProp)) { + // Both are leaves, we can diff the leaves. + return diffProperties( + updatePayload, + resolveObject(prevProp), + resolveObject(nextProp), + validAttributes + ); + } + + if (Array.isArray(prevProp) && Array.isArray(nextProp)) { + // Both are arrays, we can diff the arrays. + return diffNestedArrayProperty( + updatePayload, + prevProp, + nextProp, + validAttributes + ); + } + + if (Array.isArray(prevProp)) { + return diffProperties( + updatePayload, + // $FlowFixMe - We know that this is always an object when the input is. + flattenStyle(prevProp), + // $FlowFixMe - We know that this isn't an array because of above flow. + resolveObject(nextProp), + validAttributes + ); + } + + return diffProperties( + updatePayload, + resolveObject(prevProp), + // $FlowFixMe - We know that this is always an object when the input is. + flattenStyle(nextProp), + validAttributes + ); +} + +/** + * addNestedProperty takes a single set of props and valid attribute + * attribute configurations. It processes each prop and adds it to the + * updatePayload. + */ +function addNestedProperty( + updatePayload:? Object, + nextProp: NestedNode, + validAttributes: AttributeConfiguration +) { + if (!nextProp) { + return updatePayload; + } + + if (!Array.isArray(nextProp)) { + // Add each property of the leaf. + return addProperties( + updatePayload, + resolveObject(nextProp), + validAttributes + ); + } + + for (var i = 0; i < nextProp.length; i++) { + // Add all the properties of the array. + updatePayload = addNestedProperty( + updatePayload, + nextProp[i], + validAttributes + ); + } + + return updatePayload; +} + +/** + * clearNestedProperty takes a single set of props and valid attributes. It + * adds a null sentinel to the updatePayload, for each prop key. + */ +function clearNestedProperty( + updatePayload:? Object, + prevProp: NestedNode, + validAttributes: AttributeConfiguration +) : ?Object { + if (!prevProp) { + return updatePayload; + } + + if (!Array.isArray(prevProp)) { + // Add each property of the leaf. + return clearProperties( + updatePayload, + resolveObject(prevProp), + validAttributes + ); + } + + for (var i = 0; i < prevProp.length; i++) { + // Add all the properties of the array. + updatePayload = clearNestedProperty( + updatePayload, + prevProp[i], + validAttributes + ); + } + return updatePayload; +} + +/** + * diffProperties takes two sets of props and a set of valid attributes + * and write to updatePayload the values that changed or were deleted. + * If no updatePayload is provided, a new one is created and returned if + * anything changed. + */ +function diffProperties( + updatePayload: ?Object, + prevProps: Object, + nextProps: Object, + validAttributes: AttributeConfiguration +): ?Object { + var attributeConfig : ?(CustomAttributeConfiguration | AttributeConfiguration); + var nextProp; + var prevProp; + var altKey; + + for (var propKey in nextProps) { + attributeConfig = validAttributes[propKey]; + if (!attributeConfig) { + continue; // not a valid native prop + } + + altKey = translateKey(propKey); + if (!validAttributes[altKey]) { + // If there is no config for the alternative, bail out. Helps ART. + altKey = propKey; + } + + prevProp = prevProps[propKey]; + nextProp = nextProps[propKey]; + + // functions are converted to booleans as markers that the associated + // events should be sent from native. + if (typeof nextProp === 'function') { + nextProp = (true : any); + // If nextProp is not a function, then don't bother changing prevProp + // since nextProp will win and go into the updatePayload regardless. + if (typeof prevProp === 'function') { + prevProp = (true : any); + } + } + + // An explicit value of undefined is treated as a null because it overrides + // any other preceeding value. + if (typeof nextProp === 'undefined') { + nextProp = (null : any); + if (typeof prevProp === 'undefined') { + prevProp = (null : any); + } + } + + if (removedKeys) { + removedKeys[propKey] = false; + } + + if (updatePayload && updatePayload[altKey] !== undefined) { + // Something else already triggered an update to this key because another + // value diffed. Since we're now later in the nested arrays our value is + // more important so we need to calculate it and override the existing + // value. It doesn't matter if nothing changed, we'll set it anyway. + + // Pattern match on: attributeConfig + if (typeof attributeConfig !== 'object') { + // case: !Object is the default case + updatePayload[altKey] = nextProp; + } else if (typeof attributeConfig.diff === 'function' || + typeof attributeConfig.process === 'function') { + // case: CustomAttributeConfiguration + var nextValue = typeof attributeConfig.process === 'function' ? + attributeConfig.process(nextProp) : + nextProp; + updatePayload[altKey] = nextValue; + } + continue; + } + + if (prevProp === nextProp) { + continue; // nothing changed + } + + // Pattern match on: attributeConfig + if (typeof attributeConfig !== 'object') { + // case: !Object is the default case + if (defaultDiffer(prevProp, nextProp)) { + // a normal leaf has changed + (updatePayload || (updatePayload = {}))[altKey] = nextProp; + } + } else if (typeof attributeConfig.diff === 'function' || + typeof attributeConfig.process === 'function') { + // case: CustomAttributeConfiguration + var shouldUpdate = prevProp === undefined || ( + typeof attributeConfig.diff === 'function' ? + attributeConfig.diff(prevProp, nextProp) : + defaultDiffer(prevProp, nextProp) + ); + if (shouldUpdate) { + var nextValue = typeof attributeConfig.process === 'function' ? + attributeConfig.process(nextProp) : + nextProp; + (updatePayload || (updatePayload = {}))[altKey] = nextValue; + } + } else { + // default: fallthrough case when nested properties are defined + removedKeys = null; + removedKeyCount = 0; + updatePayload = diffNestedProperty( + updatePayload, + prevProp, + nextProp, + attributeConfig + ); + if (removedKeyCount > 0 && updatePayload) { + restoreDeletedValuesInNestedArray( + updatePayload, + nextProp, + attributeConfig + ); + removedKeys = null; + } + } + } + + // Also iterate through all the previous props to catch any that have been + // removed and make sure native gets the signal so it can reset them to the + // default. + for (var propKey in prevProps) { + if (nextProps[propKey] !== undefined) { + continue; // we've already covered this key in the previous pass + } + attributeConfig = validAttributes[propKey]; + if (!attributeConfig) { + continue; // not a valid native prop + } + + altKey = translateKey(propKey); + if (!attributeConfig[altKey]) { + // If there is no config for the alternative, bail out. Helps ART. + altKey = propKey; + } + + if (updatePayload && updatePayload[altKey] !== undefined) { + // This was already updated to a diff result earlier. + continue; + } + + prevProp = prevProps[propKey]; + if (prevProp === undefined) { + continue; // was already empty anyway + } + // Pattern match on: attributeConfig + if (typeof attributeConfig !== 'object' || + typeof attributeConfig.diff === 'function' || + typeof attributeConfig.process === 'function') { + + // case: CustomAttributeConfiguration | !Object + // Flag the leaf property for removal by sending a sentinel. + (updatePayload || (updatePayload = {}))[altKey] = null; + if (!removedKeys) { + removedKeys = {}; + } + if (!removedKeys[propKey]) { + removedKeys[propKey] = true; + removedKeyCount++; + } + } else { + // default: + // This is a nested attribute configuration where all the properties + // were removed so we need to go through and clear out all of them. + updatePayload = clearNestedProperty( + updatePayload, + prevProp, + attributeConfig + ); + } + } + return updatePayload; +} + +/** + * addProperties adds all the valid props to the payload after being processed. + */ +function addProperties( + updatePayload: ?Object, + props: Object, + validAttributes: AttributeConfiguration +) : ?Object { + // TODO: Fast path + return diffProperties(updatePayload, emptyObject, props, validAttributes); +} + +/** + * clearProperties clears all the previous props by adding a null sentinel + * to the payload for each valid key. + */ +function clearProperties( + updatePayload: ?Object, + prevProps: Object, + validAttributes: AttributeConfiguration +) :? Object { + // TODO: Fast path + return diffProperties(updatePayload, prevProps, emptyObject, validAttributes); +} + +var ReactNativeAttributePayload = { + + create: function( + props: Object, + validAttributes: AttributeConfiguration + ) : ?Object { + return addProperties( + null, // updatePayload + props, + validAttributes + ); + }, + + diff: function( + prevProps: Object, + nextProps: Object, + validAttributes: AttributeConfiguration + ) : ?Object { + return diffProperties( + null, // updatePayload + prevProps, + nextProps, + validAttributes + ); + } + +}; + +module.exports = ReactNativeAttributePayload; diff --git a/src/renderers/native/ReactNative/ReactNativeBaseComponent.js b/src/renderers/native/ReactNative/ReactNativeBaseComponent.js new file mode 100644 index 0000000000..b879b45b6a --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeBaseComponent.js @@ -0,0 +1,239 @@ +/** + * 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 ReactNativeBaseComponent + * @flow + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var ReactNativeAttributePayload = require('ReactNativeAttributePayload'); +var ReactNativeEventEmitter = require('ReactNativeEventEmitter'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var ReactMultiChild = require('ReactMultiChild'); +var UIManager = require('UIManager'); + +var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); +var invariant = require('fbjs/lib/invariant'); +var warning = require('fbjs/lib/warning'); + +var registrationNames = ReactNativeEventEmitter.registrationNames; +var putListener = ReactNativeEventEmitter.putListener; +var deleteListener = ReactNativeEventEmitter.deleteListener; +var deleteAllListeners = ReactNativeEventEmitter.deleteAllListeners; + +type ReactNativeBaseComponentViewConfig = { + validAttributes: Object; + uiViewClassName: string; +} + +// require('UIManagerStatTracker').install(); // uncomment to enable + +/** + * @constructor ReactNativeBaseComponent + * @extends ReactComponent + * @extends ReactMultiChild + * @param {!object} UIKit View Configuration. + */ +var ReactNativeBaseComponent = function( + viewConfig: ReactNativeBaseComponentViewConfig +) { + this.viewConfig = viewConfig; +}; + +/** + * Mixin for containers that contain UIViews. NOTE: markup is rendered markup + * which is a `viewID` ... see the return value for `mountComponent` ! + */ +ReactNativeBaseComponent.Mixin = { + getPublicInstance: function() { + // TODO: This should probably use a composite wrapper + return this; + }, + + construct: function(element) { + this._currentElement = element; + }, + + unmountComponent: function() { + deleteAllListeners(this._rootNodeID); + this.unmountChildren(); + this._rootNodeID = null; + }, + + /** + * Every native component is responsible for allocating its own `tag`, and + * issuing the native `createView` command. But it is not responsible for + * recording the fact that its own `rootNodeID` is associated with a + * `nodeHandle`. Only the code that actually adds its `nodeHandle` (`tag`) as + * a child of a container can confidently record that in + * `ReactNativeTagHandles`. + */ + initializeChildren: function(children, containerTag, transaction, context) { + var mountImages = this.mountChildren(children, transaction, context); + // In a well balanced tree, half of the nodes are in the bottom row and have + // no children - let's avoid calling out to the native bridge for a large + // portion of the children. + if (mountImages.length) { + + // TODO: Pool these per platform view class. Reusing the `mountImages` + // array would likely be a jit deopt. + var createdTags = []; + for (var i = 0, l = mountImages.length; i < l; i++) { + var mountImage = mountImages[i]; + var childTag = mountImage.tag; + var childID = mountImage.rootNodeID; + warning( + mountImage && mountImage.rootNodeID && mountImage.tag, + 'Mount image returned does not have required data' + ); + ReactNativeTagHandles.associateRootNodeIDWithMountedNodeHandle( + childID, + childTag + ); + createdTags[i] = mountImage.tag; + } + UIManager.setChildren(containerTag, createdTags); + } + }, + + /** + * Updates the component's currently mounted representation. + * + * @param {object} nextElement + * @param {ReactReconcileTransaction} transaction + * @param {object} context + * @internal + */ + receiveComponent: function(nextElement, transaction, context) { + var prevElement = this._currentElement; + this._currentElement = nextElement; + + if (__DEV__) { + for (var key in this.viewConfig.validAttributes) { + if (nextElement.props.hasOwnProperty(key)) { + deepFreezeAndThrowOnMutationInDev(nextElement.props[key]); + } + } + } + + var updatePayload = ReactNativeAttributePayload.diff( + prevElement.props, + nextElement.props, + this.viewConfig.validAttributes + ); + + if (updatePayload) { + UIManager.updateView( + ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID(this._rootNodeID), + this.viewConfig.uiViewClassName, + updatePayload + ); + } + + this._reconcileListenersUponUpdate( + prevElement.props, + nextElement.props + ); + this.updateChildren(nextElement.props.children, transaction, context); + }, + + /** + * @param {object} initialProps Native component props. + */ + _registerListenersUponCreation: function(initialProps) { + for (var key in initialProps) { + // NOTE: The check for `!props[key]`, is only possible because this method + // registers listeners the *first* time a component is created. + if (registrationNames[key] && initialProps[key]) { + var listener = initialProps[key]; + putListener(this._rootNodeID, key, listener); + } + } + }, + + /** + * Reconciles event listeners, adding or removing if necessary. + * @param {object} prevProps Native component props including events. + * @param {object} nextProps Next native component props including events. + */ + _reconcileListenersUponUpdate: function(prevProps, nextProps) { + for (var key in nextProps) { + if (registrationNames[key] && (nextProps[key] !== prevProps[key])) { + if (nextProps[key]) { + putListener(this._rootNodeID, key, nextProps[key]); + } else { + deleteListener(this._rootNodeID, key); + } + } + } + }, + + /** + * @param {string} rootID Root ID of this subtree. + * @param {Transaction} transaction For creating/updating. + * @return {string} Unique iOS view tag. + */ + mountComponent: function(rootID, transaction, context) { + this._rootNodeID = rootID; + + var tag = ReactNativeTagHandles.allocateTag(); + + if (__DEV__) { + for (var key in this.viewConfig.validAttributes) { + if (this._currentElement.props.hasOwnProperty(key)) { + deepFreezeAndThrowOnMutationInDev(this._currentElement.props[key]); + } + } + } + + var updatePayload = ReactNativeAttributePayload.create( + this._currentElement.props, + this.viewConfig.validAttributes + ); + + var nativeTopRootID = ReactNativeTagHandles.getNativeTopRootIDFromNodeID(rootID); + if (nativeTopRootID == null) { + invariant( + false, + 'nativeTopRootID not found for tag ' + tag + ' view type ' + + this.viewConfig.uiViewClassName + ' with rootID ' + rootID); + } + UIManager.createView( + tag, + this.viewConfig.uiViewClassName, + ReactNativeTagHandles.rootNodeIDToTag[nativeTopRootID], + updatePayload + ); + + this._registerListenersUponCreation(this._currentElement.props); + this.initializeChildren( + this._currentElement.props.children, + tag, + transaction, + context + ); + return { + rootNodeID: rootID, + tag: tag + }; + } +}; + +/** + * Order of mixins is important. ReactNativeBaseComponent overrides methods in + * ReactMultiChild. + */ +Object.assign( + ReactNativeBaseComponent.prototype, + ReactMultiChild.Mixin, + ReactNativeBaseComponent.Mixin, + NativeMethodsMixin +); + +module.exports = ReactNativeBaseComponent; diff --git a/src/renderers/native/ReactNative/ReactNativeBaseComponentEnvironment.js b/src/renderers/native/ReactNative/ReactNativeBaseComponentEnvironment.js new file mode 100644 index 0000000000..6e038620d6 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeBaseComponentEnvironment.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 ReactNativeComponentEnvironment + * @flow + */ +'use strict'; + +var ReactNativeDOMIDOperations = require('ReactNativeDOMIDOperations'); +var ReactNativeReconcileTransaction = require('ReactNativeReconcileTransaction'); + +var ReactNativeComponentEnvironment = { + + processChildrenUpdates: ReactNativeDOMIDOperations.dangerouslyProcessChildrenUpdates, + + replaceNodeWithMarkupByID: ReactNativeDOMIDOperations.dangerouslyReplaceNodeWithMarkupByID, + + /** + * Nothing to do for UIKit bridge. + * + * @private + */ + unmountIDFromEnvironment: function(/*rootNodeID*/) { + + }, + + /** + * @param {DOMElement} Element to clear. + */ + clearNode: function(/*containerView*/) { + + }, + + ReactReconcileTransaction: ReactNativeReconcileTransaction, +}; + +module.exports = ReactNativeComponentEnvironment; diff --git a/src/renderers/native/ReactNative/ReactNativeDOMIDOperations.js b/src/renderers/native/ReactNative/ReactNativeDOMIDOperations.js new file mode 100644 index 0000000000..fbf90db503 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeDOMIDOperations.js @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactNativeDOMIDOperations + * @flow + */ +'use strict'; + +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); +var ReactPerf = require('ReactPerf'); +var UIManager = require('UIManager'); + +/** + * Updates a component's children by processing a series of updates. + * For each of the update/create commands, the `fromIndex` refers to the index + * that the item existed at *before* any of the updates are applied, and the + * `toIndex` refers to the index after *all* of the updates are applied + * (including deletes/moves). TODO: refactor so this can be shared with + * DOMChildrenOperations. + * + * @param {array} updates List of update configurations. + * @param {array} markup List of markup strings - in the case of React + * IOS, the ids of new components assumed to be already created. + */ +var dangerouslyProcessChildrenUpdates = function(childrenUpdates, markupList) { + if (!childrenUpdates.length) { + return; + } + var byContainerTag = {}; + // Group by parent ID - send them across the bridge in separate commands per + // containerID. + for (var i = 0; i < childrenUpdates.length; i++) { + var update = childrenUpdates[i]; + var containerTag = ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID(update.parentID); + var updates = byContainerTag[containerTag] || (byContainerTag[containerTag] = {}); + if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING) { + (updates.moveFromIndices || (updates.moveFromIndices = [])).push(update.fromIndex); + (updates.moveToIndices || (updates.moveToIndices = [])).push(update.toIndex); + } else if (update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) { + (updates.removeAtIndices || (updates.removeAtIndices = [])).push(update.fromIndex); + } else if (update.type === ReactMultiChildUpdateTypes.INSERT_MARKUP) { + var mountImage = markupList[update.markupIndex]; + var tag = mountImage.tag; + var rootNodeID = mountImage.rootNodeID; + ReactNativeTagHandles.associateRootNodeIDWithMountedNodeHandle(rootNodeID, tag); + (updates.addAtIndices || (updates.addAtIndices = [])).push(update.toIndex); + (updates.addChildTags || (updates.addChildTags = [])).push(tag); + } + } + // Note this enumeration order will be different on V8! Move `byContainerTag` + // to a sparse array as soon as we confirm there are not horrible perf + // penalties. + for (var updateParentTagString in byContainerTag) { + var updateParentTagNumber = +updateParentTagString; + var childUpdatesToSend = byContainerTag[updateParentTagNumber]; + UIManager.manageChildren( + updateParentTagNumber, + childUpdatesToSend.moveFromIndices, + childUpdatesToSend.moveToIndices, + childUpdatesToSend.addChildTags, + childUpdatesToSend.addAtIndices, + childUpdatesToSend.removeAtIndices + ); + } +}; + +/** + * Operations used to process updates to DOM nodes. This is made injectable via + * `ReactComponent.DOMIDOperations`. + */ +var ReactNativeDOMIDOperations = { + dangerouslyProcessChildrenUpdates: ReactPerf.measure( + // FIXME(frantic): #4441289 Hack to avoid modifying react-tools + 'ReactDOMIDOperations', + 'dangerouslyProcessChildrenUpdates', + dangerouslyProcessChildrenUpdates + ), + + /** + * Replaces a view that exists in the document with markup. + * + * @param {string} id ID of child to be replaced. + * @param {string} markup Mount image to replace child with id. + */ + dangerouslyReplaceNodeWithMarkupByID: ReactPerf.measure( + 'ReactDOMIDOperations', + 'dangerouslyReplaceNodeWithMarkupByID', + function(id, mountImage) { + var oldTag = ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID(id); + UIManager.replaceExistingNonRootView(oldTag, mountImage.tag); + ReactNativeTagHandles.associateRootNodeIDWithMountedNodeHandle(id, mountImage.tag); + } + ), +}; + +module.exports = ReactNativeDOMIDOperations; diff --git a/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js b/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js new file mode 100644 index 0000000000..ee97c2560e --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js @@ -0,0 +1,113 @@ +/** + * 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 ReactNativeDefaultInjection + * @flow + */ +'use strict'; + +/** + * Make sure essential globals are available and are patched correctly. Please don't remove this + * line. Bundles created by react-packager `require` it before executing any application code. This + * ensures it exists in the dependency graph and can be `require`d. + */ +require('InitializeJavaScriptAppEngine'); + +var EventPluginHub = require('EventPluginHub'); +var EventPluginUtils = require('EventPluginUtils'); +var IOSDefaultEventPluginOrder = require('IOSDefaultEventPluginOrder'); +var IOSNativeBridgeEventPlugin = require('IOSNativeBridgeEventPlugin'); +var NodeHandle = require('NodeHandle'); +var ReactElement = require('ReactElement'); +var ReactComponentEnvironment = require('ReactComponentEnvironment'); +var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); +var ReactEmptyComponent = require('ReactEmptyComponent'); +var ReactInstanceHandles = require('ReactInstanceHandles'); +var ReactNativeComponentEnvironment = require('ReactNativeComponentEnvironment'); +var ReactNativeGlobalInteractionHandler = require('ReactNativeGlobalInteractionHandler'); +var ReactNativeGlobalResponderHandler = require('ReactNativeGlobalResponderHandler'); +var ReactNativeMount = require('ReactNativeMount'); +var ReactNativeTextComponent = require('ReactNativeTextComponent'); +var ReactNativeComponent = require('ReactNativeComponent'); +var ReactUpdates = require('ReactUpdates'); +var ResponderEventPlugin = require('ResponderEventPlugin'); +var UniversalWorkerNodeHandle = require('UniversalWorkerNodeHandle'); + +var invariant = require('fbjs/lib/invariant'); + +// Just to ensure this gets packaged, since its only caller is from Native. +require('RCTEventEmitter'); +require('RCTLog'); +require('JSTimersExecution'); + +function inject() { + /** + * Inject module for resolving DOM hierarchy and plugin ordering. + */ + EventPluginHub.injection.injectEventPluginOrder(IOSDefaultEventPluginOrder); + EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles); + + ResponderEventPlugin.injection.injectGlobalResponderHandler( + ReactNativeGlobalResponderHandler + ); + + ResponderEventPlugin.injection.injectGlobalInteractionHandler( + ReactNativeGlobalInteractionHandler + ); + + /** + * Some important event plugins included by default (without having to require + * them). + */ + EventPluginHub.injection.injectEventPluginsByName({ + 'ResponderEventPlugin': ResponderEventPlugin, + 'IOSNativeBridgeEventPlugin': IOSNativeBridgeEventPlugin + }); + + ReactUpdates.injection.injectReconcileTransaction( + ReactNativeComponentEnvironment.ReactReconcileTransaction + ); + + ReactUpdates.injection.injectBatchingStrategy( + ReactDefaultBatchingStrategy + ); + + ReactComponentEnvironment.injection.injectEnvironment( + ReactNativeComponentEnvironment + ); + + var EmptyComponent = () => { + // Can't import View at the top because it depends on React to make its composite + var View = require('View'); + return ReactElement.createElement(View, { + collapsable: true, + style: { position: 'absolute' } + }); + }; + ReactEmptyComponent.injection.injectEmptyComponent(EmptyComponent); + + EventPluginUtils.injection.injectMount(ReactNativeMount); + + ReactNativeComponent.injection.injectTextComponentClass( + ReactNativeTextComponent + ); + ReactNativeComponent.injection.injectGenericComponentClass(function(tag) { + // Show a nicer error message for non-function tags + var info = ''; + if (typeof tag === 'string' && /^[a-z]/.test(tag)) { + info += ' Each component name should start with an uppercase letter.'; + } + invariant(false, 'Expected a component class, got %s.%s', tag, info); + }); + + NodeHandle.injection.injectImplementation(UniversalWorkerNodeHandle); +} + +module.exports = { + inject: inject, +}; diff --git a/src/renderers/native/ReactNative/ReactNativeEventEmitter.js b/src/renderers/native/ReactNative/ReactNativeEventEmitter.js new file mode 100644 index 0000000000..c7680257af --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeEventEmitter.js @@ -0,0 +1,214 @@ +/** + * 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 ReactNativeEventEmitter + * @flow + */ +'use strict'; + +var EventPluginHub = require('EventPluginHub'); +var ReactEventEmitterMixin = require('ReactEventEmitterMixin'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var NodeHandle = require('NodeHandle'); +var EventConstants = require('EventConstants'); + +var merge = require('merge'); +var warning = require('fbjs/lib/warning'); + +var topLevelTypes = EventConstants.topLevelTypes; + +/** + * Version of `ReactBrowserEventEmitter` that works on the receiving side of a + * serialized worker boundary. + */ + +// Shared default empty native event - conserve memory. +var EMPTY_NATIVE_EVENT = {}; + +/** + * Selects a subsequence of `Touch`es, without destroying `touches`. + * + * @param {Array} touches Deserialized touch objects. + * @param {Array} indices Indices by which to pull subsequence. + * @return {Array} Subsequence of touch objects. + */ +var touchSubsequence = function(touches, indices) { + var ret = []; + for (var i = 0; i < indices.length; i++) { + ret.push(touches[indices[i]]); + } + return ret; +}; + +/** + * TODO: Pool all of this. + * + * Destroys `touches` by removing touch objects at indices `indices`. This is + * to maintain compatibility with W3C touch "end" events, where the active + * touches don't include the set that has just been "ended". + * + * @param {Array} touches Deserialized touch objects. + * @param {Array} indices Indices to remove from `touches`. + * @return {Array} Subsequence of removed touch objects. + */ +var removeTouchesAtIndices = function( + touches: Array, + indices: Array +): Array { + var rippedOut = []; + // use an unsafe downcast to alias to nullable elements, + // so we can delete and then compact. + var temp: Array = (touches: Array); + for (var i = 0; i < indices.length; i++) { + var index = indices[i]; + rippedOut.push(touches[index]); + temp[index] = null; + } + var fillAt = 0; + for (var j = 0; j < temp.length; j++) { + var cur = temp[j]; + if (cur !== null) { + temp[fillAt++] = cur; + } + } + temp.length = fillAt; + return rippedOut; +}; + +/** + * `ReactNativeEventEmitter` is used to attach top-level event listeners. For example: + * + * ReactNativeEventEmitter.putListener('myID', 'onClick', myFunction); + * + * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'. + * + * @internal + */ +var ReactNativeEventEmitter = merge(ReactEventEmitterMixin, { + + registrationNames: EventPluginHub.registrationNameModules, + + putListener: EventPluginHub.putListener, + + getListener: EventPluginHub.getListener, + + deleteListener: EventPluginHub.deleteListener, + + deleteAllListeners: EventPluginHub.deleteAllListeners, + + /** + * Internal version of `receiveEvent` in terms of normalized (non-tag) + * `rootNodeID`. + * + * @see receiveEvent. + * + * @param {rootNodeID} rootNodeID React root node ID that event occurred on. + * @param {TopLevelType} topLevelType Top level type of event. + * @param {object} nativeEventParam Object passed from native. + */ + _receiveRootNodeIDEvent: function( + rootNodeID: ?string, + topLevelType: string, + nativeEventParam: Object + ) { + var nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT; + ReactNativeEventEmitter.handleTopLevel( + topLevelType, + rootNodeID, + rootNodeID, + nativeEvent, + nativeEvent.target + ); + }, + + /** + * Publicly exposed method on module for native objc to invoke when a top + * level event is extracted. + * @param {rootNodeID} rootNodeID React root node ID that event occurred on. + * @param {TopLevelType} topLevelType Top level type of event. + * @param {object} nativeEventParam Object passed from native. + */ + receiveEvent: function( + tag: number, + topLevelType: string, + nativeEventParam: Object + ) { + var rootNodeID = ReactNativeTagHandles.tagToRootNodeID[tag]; + ReactNativeEventEmitter._receiveRootNodeIDEvent( + rootNodeID, + topLevelType, + nativeEventParam + ); + }, + + /** + * Simple multi-wrapper around `receiveEvent` that is intended to receive an + * efficient representation of `Touch` objects, and other information that + * can be used to construct W3C compliant `Event` and `Touch` lists. + * + * This may create dispatch behavior that differs than web touch handling. We + * loop through each of the changed touches and receive it as a single event. + * So two `touchStart`/`touchMove`s that occur simultaneously are received as + * two separate touch event dispatches - when they arguably should be one. + * + * This implementation reuses the `Touch` objects themselves as the `Event`s + * since we dispatch an event for each touch (though that might not be spec + * compliant). The main purpose of reusing them is to save allocations. + * + * TODO: Dispatch multiple changed touches in one event. The bubble path + * could be the first common ancestor of all the `changedTouches`. + * + * One difference between this behavior and W3C spec: cancelled touches will + * not appear in `.touches`, or in any future `.touches`, though they may + * still be "actively touching the surface". + * + * Web desktop polyfills only need to construct a fake touch event with + * identifier 0, also abandoning traditional click handlers. + */ + receiveTouches: function( + eventTopLevelType: string, + touches: Array, + changedIndices: Array + ) { + var changedTouches = + eventTopLevelType === topLevelTypes.topTouchEnd || + eventTopLevelType === topLevelTypes.topTouchCancel ? + removeTouchesAtIndices(touches, changedIndices) : + touchSubsequence(touches, changedIndices); + + for (var jj = 0; jj < changedTouches.length; jj++) { + var touch = changedTouches[jj]; + // Touch objects can fulfill the role of `DOM` `Event` objects if we set + // the `changedTouches`/`touches`. This saves allocations. + touch.changedTouches = changedTouches; + touch.touches = touches; + var nativeEvent = touch; + var rootNodeID = null; + var target = nativeEvent.target; + if (target !== null && target !== undefined) { + if (target < ReactNativeTagHandles.tagsStartAt) { + if (__DEV__) { + warning( + false, + 'A view is reporting that a touch occured on tag zero.' + ); + } + } else { + rootNodeID = NodeHandle.getRootNodeID(target); + } + } + ReactNativeEventEmitter._receiveRootNodeIDEvent( + rootNodeID, + eventTopLevelType, + nativeEvent + ); + } + } +}); + +module.exports = ReactNativeEventEmitter; diff --git a/src/renderers/native/ReactNative/ReactNativeGlobalInteractionHandler.js b/src/renderers/native/ReactNative/ReactNativeGlobalInteractionHandler.js new file mode 100644 index 0000000000..d5c9b95447 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeGlobalInteractionHandler.js @@ -0,0 +1,33 @@ +/** + * 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 ReactNativeGlobalInteractionHandler + * @flow + */ +'use strict'; + +var InteractionManager = require('InteractionManager'); + +// Interaction handle is created/cleared when responder is granted or +// released/terminated. +var interactionHandle = null; + +var ReactNativeGlobalInteractionHandler = { + onChange: function(numberActiveTouches: number) { + if (numberActiveTouches === 0) { + if (interactionHandle) { + InteractionManager.clearInteractionHandle(interactionHandle); + interactionHandle = null; + } + } else if (!interactionHandle) { + interactionHandle = InteractionManager.createInteractionHandle(); + } + } +}; + +module.exports = ReactNativeGlobalInteractionHandler; diff --git a/src/renderers/native/ReactNative/ReactNativeGlobalResponderHandler.js b/src/renderers/native/ReactNative/ReactNativeGlobalResponderHandler.js new file mode 100644 index 0000000000..1094e60451 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeGlobalResponderHandler.js @@ -0,0 +1,30 @@ +/** + * 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 ReactNativeGlobalResponderHandler + * @flow + */ +'use strict'; + +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var UIManager = require('UIManager'); + +var ReactNativeGlobalResponderHandler = { + onChange: function(from: string, to: string, blockNativeResponder: boolean) { + if (to !== null) { + UIManager.setJSResponder( + ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID(to), + blockNativeResponder + ); + } else { + UIManager.clearJSResponder(); + } + } +}; + +module.exports = ReactNativeGlobalResponderHandler; diff --git a/src/renderers/native/ReactNative/ReactNativeMount.js b/src/renderers/native/ReactNative/ReactNativeMount.js new file mode 100644 index 0000000000..cf8c5bb336 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeMount.js @@ -0,0 +1,270 @@ +/** + * 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 ReactNativeMount + * @flow + */ +'use strict'; + +var ReactElement = require('ReactElement'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var ReactPerf = require('ReactPerf'); +var ReactReconciler = require('ReactReconciler'); +var ReactUpdateQueue = require('ReactUpdateQueue'); +var ReactUpdates = require('ReactUpdates'); +var UIManager = require('UIManager'); + +var emptyObject = require('fbjs/lib/emptyObject'); +var instantiateReactComponent = require('instantiateReactComponent'); +var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); + +function instanceNumberToChildRootID(rootNodeID, instanceNumber) { + return rootNodeID + '[' + instanceNumber + ']'; +} + +/** + * Temporary (?) hack so that we can store all top-level pending updates on + * composites instead of having to worry about different types of components + * here. + */ +var TopLevelWrapper = function() {}; +TopLevelWrapper.prototype.isReactComponent = {}; +if (__DEV__) { + TopLevelWrapper.displayName = 'TopLevelWrapper'; +} +TopLevelWrapper.prototype.render = function() { + // this.props is actually a ReactElement + return this.props; +}; + +/** + * Mounts this component and inserts it into the DOM. + * + * @param {ReactComponent} componentInstance The instance to mount. + * @param {number} rootID ID of the root node. + * @param {number} container container element to mount into. + * @param {ReactReconcileTransaction} transaction + */ +function mountComponentIntoNode( + componentInstance, + rootID, + container, + transaction) { + var markup = ReactReconciler.mountComponent( + componentInstance, rootID, transaction, emptyObject + ); + componentInstance._renderedComponent._topLevelWrapper = componentInstance; + ReactNativeMount._mountImageIntoNode(markup, container); +} + +/** + * Batched mount. + * + * @param {ReactComponent} componentInstance The instance to mount. + * @param {number} rootID ID of the root node. + * @param {number} container container element to mount into. + */ +function batchedMountComponentIntoNode( + componentInstance, + rootID, + container) { + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + transaction.perform( + mountComponentIntoNode, + null, + componentInstance, + rootID, + container, + transaction + ); + ReactUpdates.ReactReconcileTransaction.release(transaction); +} + +/** + * As soon as `ReactMount` is refactored to not rely on the DOM, we can share + * code between the two. For now, we'll hard code the ID logic. + */ +var ReactNativeMount = { + instanceCount: 0, + + _instancesByContainerID: {}, + + // these two functions are needed by React Devtools + findNodeHandle: require('findNodeHandle'), + nativeTagToRootNodeID: function (nativeTag: number): string { + return ReactNativeTagHandles.tagToRootNodeID[nativeTag]; + }, + + /** + * @param {ReactComponent} instance Instance to render. + * @param {containerTag} containerView Handle to native view tag + */ + renderComponent: function( + nextElement: ReactElement, + containerTag: number, + callback?: ?(() => void) + ): ?ReactComponent { + var nextWrappedElement = new ReactElement( + TopLevelWrapper, + null, + null, + null, + null, + null, + nextElement + ); + + var topRootNodeID = ReactNativeTagHandles.tagToRootNodeID[containerTag]; + if (topRootNodeID) { + var prevComponent = ReactNativeMount._instancesByContainerID[topRootNodeID]; + if (prevComponent) { + var prevWrappedElement = prevComponent._currentElement; + var prevElement = prevWrappedElement.props; + if (shouldUpdateReactComponent(prevElement, nextElement)) { + ReactUpdateQueue.enqueueElementInternal(prevComponent, nextWrappedElement); + if (callback) { + ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback); + } + return prevComponent; + } else { + ReactNativeMount.unmountComponentAtNode(containerTag); + } + } + } + + if (!ReactNativeTagHandles.reactTagIsNativeTopRootID(containerTag)) { + console.error('You cannot render into anything but a top root'); + return; + } + + var topRootNodeID = ReactNativeTagHandles.allocateRootNodeIDForTag(containerTag); + ReactNativeTagHandles.associateRootNodeIDWithMountedNodeHandle( + topRootNodeID, + containerTag + ); + + var instance = instantiateReactComponent(nextWrappedElement); + ReactNativeMount._instancesByContainerID[topRootNodeID] = instance; + + var childRootNodeID = instanceNumberToChildRootID( + topRootNodeID, + ReactNativeMount.instanceCount++ + ); + + // The initial render is synchronous but any updates that happen during + // rendering, in componentWillMount or componentDidMount, will be batched + // according to the current batching strategy. + + ReactUpdates.batchedUpdates( + batchedMountComponentIntoNode, + instance, + childRootNodeID, + topRootNodeID + ); + var component = instance.getPublicInstance(); + if (callback) { + callback.call(component); + } + return component; + }, + + /** + * @param {View} view View tree image. + * @param {number} containerViewID View to insert sub-view into. + */ + _mountImageIntoNode: ReactPerf.measure( + // FIXME(frantic): #4441289 Hack to avoid modifying react-tools + 'ReactComponentBrowserEnvironment', + 'mountImageIntoNode', + function(mountImage, containerID) { + // Since we now know that the `mountImage` has been mounted, we can + // mark it as such. + ReactNativeTagHandles.associateRootNodeIDWithMountedNodeHandle( + mountImage.rootNodeID, + mountImage.tag + ); + UIManager.setChildren( + ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID(containerID), + [mountImage.tag] + ); + } + ), + + /** + * Standard unmounting of the component that is rendered into `containerID`, + * but will also execute a command to remove the actual container view + * itself. This is useful when a client is cleaning up a React tree, and also + * knows that the container will no longer be needed. When executing + * asynchronously, it's easier to just have this method be the one that calls + * for removal of the view. + */ + unmountComponentAtNodeAndRemoveContainer: function( + containerTag: number + ) { + ReactNativeMount.unmountComponentAtNode(containerTag); + // call back into native to remove all of the subviews from this container + UIManager.removeRootView(containerTag); + }, + + /** + * Unmount component at container ID by iterating through each child component + * that has been rendered and unmounting it. There should just be one child + * component at this time. + */ + unmountComponentAtNode: function(containerTag: number): boolean { + if (!ReactNativeTagHandles.reactTagIsNativeTopRootID(containerTag)) { + console.error('You cannot render into anything but a top root'); + return false; + } + + var containerID = ReactNativeTagHandles.tagToRootNodeID[containerTag]; + var instance = ReactNativeMount._instancesByContainerID[containerID]; + if (!instance) { + return false; + } + ReactNativeMount.unmountComponentFromNode(instance, containerID); + delete ReactNativeMount._instancesByContainerID[containerID]; + return true; + }, + + /** + * Unmounts a component and sends messages back to iOS to remove its subviews. + * + * @param {ReactComponent} instance React component instance. + * @param {string} containerID ID of container we're removing from. + * @final + * @internal + * @see {ReactNativeMount.unmountComponentAtNode} + */ + unmountComponentFromNode: function( + instance: ReactComponent, + containerID: string + ) { + // Call back into native to remove all of the subviews from this container + ReactReconciler.unmountComponent(instance); + var containerTag = + ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID(containerID); + UIManager.removeSubviewsFromContainerWithID(containerTag); + }, + + getNode: function(rootNodeID: string): number { + return ReactNativeTagHandles.rootNodeIDToTag[rootNodeID]; + }, + + getID: function(nativeTag: number): string { + return ReactNativeTagHandles.tagToRootNodeID[nativeTag]; + } +}; + +ReactNativeMount.renderComponent = ReactPerf.measure( + 'ReactMount', + '_renderNewRootComponent', + ReactNativeMount.renderComponent +); + +module.exports = ReactNativeMount; diff --git a/src/renderers/native/ReactNative/ReactNativePropRegistry.js b/src/renderers/native/ReactNative/ReactNativePropRegistry.js new file mode 100644 index 0000000000..3cffb9acf3 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativePropRegistry.js @@ -0,0 +1,44 @@ +/** + * 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 ReactNativePropRegistry + * @flow + */ +'use strict'; + +var objects = {}; +var uniqueID = 1; +var emptyObject = {}; + +class ReactNativePropRegistry { + static register(object: Object): number { + var id = ++uniqueID; + if (__DEV__) { + Object.freeze(object); + } + objects[id] = object; + return id; + } + + static getByID(id: number): Object { + if (!id) { + // Used in the style={[condition && id]} pattern, + // we want it to be a no-op when the value is false or null + return emptyObject; + } + + var object = objects[id]; + if (!object) { + console.warn('Invalid style with id `' + id + '`. Skipping ...'); + return emptyObject; + } + return object; + } +} + +module.exports = ReactNativePropRegistry; diff --git a/src/renderers/native/ReactNative/ReactNativeReconcileTransaction.js b/src/renderers/native/ReactNative/ReactNativeReconcileTransaction.js new file mode 100644 index 0000000000..b672196498 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeReconcileTransaction.js @@ -0,0 +1,103 @@ +/** + * 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 ReactNativeReconcileTransaction + * @flow + */ +'use strict'; + +var CallbackQueue = require('CallbackQueue'); +var PooledClass = require('PooledClass'); +var Transaction = require('Transaction'); + +/** + * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks during + * the performing of the transaction. + */ +var ON_DOM_READY_QUEUEING = { + /** + * Initializes the internal `onDOMReady` queue. + */ + initialize: function() { + this.reactMountReady.reset(); + }, + + /** + * After DOM is flushed, invoke all registered `onDOMReady` callbacks. + */ + close: function() { + this.reactMountReady.notifyAll(); + } +}; + +/** + * Executed within the scope of the `Transaction` instance. Consider these as + * being member methods, but with an implied ordering while being isolated from + * each other. + */ +var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING]; + +/** + * Currently: + * - The order that these are listed in the transaction is critical: + * - Suppresses events. + * - Restores selection range. + * + * Future: + * - Restore document/overflow scroll positions that were unintentionally + * modified via DOM insertions above the top viewport boundary. + * - Implement/integrate with customized constraint based layout system and keep + * track of which dimensions must be remeasured. + * + * @class ReactNativeReconcileTransaction + */ +function ReactNativeReconcileTransaction() { + this.reinitializeTransaction(); + this.reactMountReady = CallbackQueue.getPooled(null); +} + +var Mixin = { + /** + * @see Transaction + * @abstract + * @final + * @return {array} List of operation wrap procedures. + * TODO: convert to array + */ + getTransactionWrappers: function() { + return TRANSACTION_WRAPPERS; + }, + + /** + * @return {object} The queue to collect `onDOMReady` callbacks with. + * TODO: convert to ReactMountReady + */ + getReactMountReady: function() { + return this.reactMountReady; + }, + + /** + * `PooledClass` looks for this, and will invoke this before allowing this + * instance to be reused. + */ + destructor: function() { + CallbackQueue.release(this.reactMountReady); + this.reactMountReady = null; + } +}; + +Object.assign( + ReactNativeReconcileTransaction.prototype, + Transaction.Mixin, + ReactNativeReconcileTransaction, + Mixin +); + +PooledClass.addPoolingTo(ReactNativeReconcileTransaction); + +module.exports = ReactNativeReconcileTransaction; diff --git a/src/renderers/native/ReactNative/ReactNativeTagHandles.js b/src/renderers/native/ReactNative/ReactNativeTagHandles.js new file mode 100644 index 0000000000..5a0de4cda3 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeTagHandles.js @@ -0,0 +1,113 @@ +/** + * 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 ReactNativeTagHandles + * @flow + */ +'use strict'; + +var invariant = require('fbjs/lib/invariant'); +var warning = require('fbjs/lib/warning'); + +/** + * Keeps track of allocating and associating native "tags" which are numeric, + * unique view IDs. All the native tags are negative numbers, to avoid + * collisions, but in the JS we keep track of them as positive integers to store + * them effectively in Arrays. So we must refer to them as "inverses" of the + * native tags (that are * normally negative). + * + * It *must* be the case that every `rootNodeID` always maps to the exact same + * `tag` forever. The easiest way to accomplish this is to never delete + * anything from this table. + * Why: Because `dangerouslyReplaceNodeWithMarkupByID` relies on being able to + * unmount a component with a `rootNodeID`, then mount a new one in its place, + */ +var INITIAL_TAG_COUNT = 1; +var NATIVE_TOP_ROOT_ID_SEPARATOR = '{TOP_LEVEL}'; +var ReactNativeTagHandles = { + tagsStartAt: INITIAL_TAG_COUNT, + tagCount: INITIAL_TAG_COUNT, + + allocateTag: function(): number { + // Skip over root IDs as those are reserved for native + while (this.reactTagIsNativeTopRootID(ReactNativeTagHandles.tagCount)) { + ReactNativeTagHandles.tagCount++; + } + var tag = ReactNativeTagHandles.tagCount; + ReactNativeTagHandles.tagCount++; + return tag; + }, + + /** + * This associates the *last* observed *native* mounting between `rootNodeID` + * and some `tag`. This association doesn't imply that `rootNodeID` is still + * natively mounted as `tag`. The only reason why we don't clear the + * association when the `rootNodeID` is unmounted, is that we don't have a + * convenient time to disassociate them (otherwise we would). + * `unmountComponent` isn't the correct time because that doesn't imply that + * the native node has been natively unmounted. + */ + associateRootNodeIDWithMountedNodeHandle: function( + rootNodeID: ?string, + tag: ?number + ) { + warning(rootNodeID && tag, 'Root node or tag is null when associating'); + if (rootNodeID && tag) { + ReactNativeTagHandles.tagToRootNodeID[tag] = rootNodeID; + ReactNativeTagHandles.rootNodeIDToTag[rootNodeID] = tag; + } + }, + + allocateRootNodeIDForTag: function(tag: number): string { + invariant( + this.reactTagIsNativeTopRootID(tag), + 'Expect a native root tag, instead got ', tag + ); + return '.r[' + tag + ']' + NATIVE_TOP_ROOT_ID_SEPARATOR; + }, + + reactTagIsNativeTopRootID: function(reactTag: number): bool { + // We reserve all tags that are 1 mod 10 for native root views + return reactTag % 10 === 1; + }, + + getNativeTopRootIDFromNodeID: function(nodeID: ?string): ?string { + if (!nodeID) { + return null; + } + var index = nodeID.indexOf(NATIVE_TOP_ROOT_ID_SEPARATOR); + if (index === -1) { + return null; + } + return nodeID.substr(0, index + NATIVE_TOP_ROOT_ID_SEPARATOR.length); + }, + + /** + * Returns the native `nodeHandle` (`tag`) that was most recently *natively* + * mounted at the `rootNodeID`. Just because a React component has been + * mounted, that doesn't mean that its native node has been mounted. The + * native node is mounted when we actually make the call to insert the + * `nodeHandle` (`tag`) into the native hierarchy. + * + * @param {string} rootNodeID Root node ID to find most recently mounted tag + * for. Again, this doesn't imply that it is still currently mounted. + * @return {number} Tag ID of native view for most recent mounting of + * `rootNodeID`. + */ + mostRecentMountedNodeHandleForRootNodeID: function( + rootNodeID: string + ): number { + return ReactNativeTagHandles.rootNodeIDToTag[rootNodeID]; + }, + + tagToRootNodeID: ([] : Array), + + rootNodeIDToTag: ({} : {[key: string]: number}) +}; + +module.exports = ReactNativeTagHandles; diff --git a/src/renderers/native/ReactNative/ReactNativeTextComponent.js b/src/renderers/native/ReactNative/ReactNativeTextComponent.js new file mode 100644 index 0000000000..7bd97467f2 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeTextComponent.js @@ -0,0 +1,78 @@ +/** + * 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 ReactNativeTextComponent + */ + +'use strict'; + +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var UIManager = require('UIManager'); + +var invariant = require('fbjs/lib/invariant'); + +var ReactNativeTextComponent = function(props) { + // This constructor and its argument is currently used by mocks. +}; + +Object.assign(ReactNativeTextComponent.prototype, { + + construct: function(text) { + // This is really a ReactText (ReactNode), not a ReactElement + this._currentElement = text; + this._stringText = '' + text; + this._rootNodeID = null; + }, + + mountComponent: function(rootID, transaction, context) { + invariant( + context.isInAParentText, + 'RawText "' + this._stringText + '" must be wrapped in an explicit ' + + ' component.' + ); + this._rootNodeID = rootID; + var tag = ReactNativeTagHandles.allocateTag(); + var nativeTopRootID = ReactNativeTagHandles.getNativeTopRootIDFromNodeID(rootID); + UIManager.createView( + tag, + 'RCTRawText', + nativeTopRootID ? ReactNativeTagHandles.rootNodeIDToTag[nativeTopRootID] : null, + {text: this._stringText} + ); + return { + rootNodeID: rootID, + tag: tag, + }; + }, + + receiveComponent: function(nextText, transaction, context) { + if (nextText !== this._currentElement) { + this._currentElement = nextText; + var nextStringText = '' + nextText; + if (nextStringText !== this._stringText) { + this._stringText = nextStringText; + UIManager.updateView( + ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID( + this._rootNodeID + ), + 'RCTRawText', + {text: this._stringText} + ); + } + } + }, + + unmountComponent: function() { + this._currentElement = null; + this._stringText = null; + this._rootNodeID = null; + } + +}); + +module.exports = ReactNativeTextComponent; diff --git a/src/renderers/native/ReactNative/UIManagerStatTracker.js b/src/renderers/native/ReactNative/UIManagerStatTracker.js new file mode 100644 index 0000000000..a3b8779234 --- /dev/null +++ b/src/renderers/native/ReactNative/UIManagerStatTracker.js @@ -0,0 +1,57 @@ +/** + * 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 UIManagerStatTracker + * @flow + */ +'use strict'; + +var UIManager = require('UIManager'); + +var installed = false; +var UIManagerStatTracker = { + install: function() { + if (installed) { + return; + } + installed = true; + var statLogHandle; + var stats = {}; + function printStats() { + console.log({UIManagerStatTracker: stats}); + statLogHandle = null; + } + function incStat(key: string, increment: number) { + stats[key] = (stats[key] || 0) + increment; + if (!statLogHandle) { + statLogHandle = setImmediate(printStats); + } + } + var createViewOrig = UIManager.createView; + UIManager.createView = function(tag, className, rootTag, props) { + incStat('createView', 1); + incStat('setProp', Object.keys(props || []).length); + createViewOrig(tag, className, rootTag, props); + }; + var updateViewOrig = UIManager.updateView; + UIManager.updateView = function(tag, className, props) { + incStat('updateView', 1); + incStat('setProp', Object.keys(props || []).length); + updateViewOrig(tag, className, props); + }; + var manageChildrenOrig = UIManager.manageChildren; + UIManager.manageChildren = function(tag, moveFrom, moveTo, addTags, addIndices, remove) { + incStat('manageChildren', 1); + incStat('move', Object.keys(moveFrom || []).length); + incStat('remove', Object.keys(remove || []).length); + manageChildrenOrig(tag, moveFrom, moveTo, addTags, addIndices, remove); + }; + }, +}; + +module.exports = UIManagerStatTracker; diff --git a/src/renderers/native/ReactNative/__tests__/ReactNativeAttributePayload-test.js b/src/renderers/native/ReactNative/__tests__/ReactNativeAttributePayload-test.js new file mode 100644 index 0000000000..6cbaa46a30 --- /dev/null +++ b/src/renderers/native/ReactNative/__tests__/ReactNativeAttributePayload-test.js @@ -0,0 +1,231 @@ +/** + * Copyright (c) 2013-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. + * + */ +'use strict'; + +jest.dontMock('ReactNativeAttributePayload'); +jest.dontMock('ReactNativePropRegistry'); +jest.dontMock('deepDiffer'); +jest.dontMock('flattenStyle'); + +var ReactNativeAttributePayload = require('ReactNativeAttributePayload'); +var ReactNativePropRegistry = require('ReactNativePropRegistry'); + +var diff = ReactNativeAttributePayload.diff; + +describe('ReactNativeAttributePayload', function() { + + it('should work with simple example', () => { + expect(diff( + {a: 1, c: 3}, + {b: 2, c: 3}, + {a: true, b: true} + )).toEqual({a: null, b: 2}); + }); + + it('should skip fields that are equal', () => { + expect(diff( + {a: 1, b: 'two', c: true, d: false, e: undefined, f: 0}, + {a: 1, b: 'two', c: true, d: false, e: undefined, f: 0}, + {a: true, b: true, c: true, d: true, e: true, f: true} + )).toEqual(null); + }); + + it('should remove fields', () => { + expect(diff( + {a: 1}, + {}, + {a: true} + )).toEqual({a: null}); + }); + + it('should remove fields that are set to undefined', () => { + expect(diff( + {a: 1}, + {a: undefined}, + {a: true} + )).toEqual({a: null}); + }); + + it('should ignore invalid fields', () => { + expect(diff( + {a: 1}, + {b: 2}, + {} + )).toEqual(null); + }); + + it('should use the diff attribute', () => { + var diffA = jest.genMockFunction().mockImpl((a, b) => true); + var diffB = jest.genMockFunction().mockImpl((a, b) => false); + expect(diff( + {a: [1], b: [3]}, + {a: [2], b: [4]}, + {a: {diff: diffA}, b: {diff: diffB}} + )).toEqual({a: [2]}); + expect(diffA).toBeCalledWith([1], [2]); + expect(diffB).toBeCalledWith([3], [4]); + }); + + it('should not use the diff attribute on addition/removal', () => { + var diffA = jest.genMockFunction(); + var diffB = jest.genMockFunction(); + expect(diff( + {a: [1]}, + {b: [2]}, + {a: {diff: diffA}, b: {diff: diffB}} + )).toEqual({a: null, b: [2]}); + expect(diffA).not.toBeCalled(); + expect(diffB).not.toBeCalled(); + }); + + it('should do deep diffs of Objects by default', () => { + expect(diff( + {a: [1], b: {k: [3,4]}, c: {k: [4,4]} }, + {a: [2], b: {k: [3,4]}, c: {k: [4,5]} }, + {a: true, b: true, c: true} + )).toEqual({a: [2], c: {k: [4,5]}}); + }); + + it('should work with undefined styles', () => { + expect(diff( + { style: { a: '#ffffff', b: 1 } }, + { style: undefined }, + { style: { b: true } } + )).toEqual({ b: null }); + expect(diff( + { style: undefined }, + { style: { a: '#ffffff', b: 1 } }, + { style: { b: true } } + )).toEqual({ b: 1 }); + expect(diff( + { style: undefined }, + { style: undefined }, + { style: { b: true } } + )).toEqual(null); + }); + + it('should work with empty styles', () => { + expect(diff( + {a: 1, c: 3}, + {}, + {a: true, b: true} + )).toEqual({a: null}); + expect(diff( + {}, + {a: 1, c: 3}, + {a: true, b: true} + )).toEqual({a: 1}); + expect(diff( + {}, + {}, + {a: true, b: true} + )).toEqual(null); + }); + + it('should flatten nested styles and predefined styles', () => { + var validStyleAttribute = { someStyle: { foo: true, bar: true } }; + + expect(diff( + {}, + { someStyle: [{ foo: 1 }, { bar: 2 }]}, + validStyleAttribute + )).toEqual({ foo: 1, bar: 2 }); + + expect(diff( + { someStyle: [{ foo: 1 }, { bar: 2 }]}, + {}, + validStyleAttribute + )).toEqual({ foo: null, bar: null }); + + var barStyle = ReactNativePropRegistry.register({ + bar: 3, + }); + + expect(diff( + {}, + { someStyle: [[{ foo: 1 }, { foo: 2 }], barStyle]}, + validStyleAttribute + )).toEqual({ foo: 2, bar: 3 }); + }); + + it('should reset a value to a previous if it is removed', () => { + var validStyleAttribute = { someStyle: { foo: true, bar: true } }; + + expect(diff( + { someStyle: [{ foo: 1 }, { foo: 3 }]}, + { someStyle: [{ foo: 1 }, { bar: 2 }]}, + validStyleAttribute + )).toEqual({ foo: 1, bar: 2 }); + }); + + it('should not clear removed props if they are still in another slot', () => { + var validStyleAttribute = { someStyle: { foo: true, bar: true } }; + + expect(diff( + { someStyle: [{}, { foo: 3, bar: 2 }]}, + { someStyle: [{ foo: 3 }, { bar: 2 }]}, + validStyleAttribute + )).toEqual({ foo: 3 }); // this should ideally be null. heuristic tradeoff. + + expect(diff( + { someStyle: [{}, { foo: 3, bar: 2 }]}, + { someStyle: [{ foo: 1, bar: 1 }, { bar: 2 }]}, + validStyleAttribute + )).toEqual({ bar: 2, foo: 1 }); + }); + + it('should clear a prop if a later style is explicit null/undefined', () => { + var validStyleAttribute = { someStyle: { foo: true, bar: true } }; + expect(diff( + { someStyle: [{}, { foo: 3, bar: 2 }]}, + { someStyle: [{ foo: 1 }, { bar: 2, foo: null }]}, + validStyleAttribute + )).toEqual({ foo: null }); + + expect(diff( + { someStyle: [{ foo: 3 }, { foo: null, bar: 2 }]}, + { someStyle: [{ foo: null }, { bar: 2 }]}, + validStyleAttribute + )).toEqual({ foo: null }); + + expect(diff( + { someStyle: [{ foo: 1 }, { foo: null }]}, + { someStyle: [{ foo: 2 }, { foo: null }]}, + validStyleAttribute + )).toEqual({ foo: null }); // this should ideally be null. heuristic. + + // Test the same case with object equality because an early bailout doesn't + // work in this case. + var fooObj = { foo: 3 }; + expect(diff( + { someStyle: [{ foo: 1 }, fooObj]}, + { someStyle: [{ foo: 2 }, fooObj]}, + validStyleAttribute + )).toEqual({ foo: 3 }); // this should ideally be null. heuristic. + + expect(diff( + { someStyle: [{ foo: 1 }, { foo: 3 }]}, + { someStyle: [{ foo: 2 }, { foo: undefined }]}, + validStyleAttribute + )).toEqual({ foo: null }); // this should ideally be null. heuristic. + }); + + // Function properties are just markers to native that events should be sent. + it('should convert functions to booleans', () => { + // Note that if the property changes from one function to another, we don't + // need to send an update. + expect(diff( + {a: function() { return 1; }, b: function() { return 2; }, c: 3}, + {b: function() { return 9; }, c: function() { return 3; }, }, + {a: true, b: true, c: true} + )).toEqual({a: null, c: true}); + }); + + }); diff --git a/src/renderers/native/ReactNative/createReactNativeComponentClass.js b/src/renderers/native/ReactNative/createReactNativeComponentClass.js new file mode 100644 index 0000000000..199e268891 --- /dev/null +++ b/src/renderers/native/ReactNative/createReactNativeComponentClass.js @@ -0,0 +1,46 @@ +/** + * 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 createReactNativeComponentClass + * @flow + */ + +'use strict'; + +var ReactNativeBaseComponent = require('ReactNativeBaseComponent'); + +// See also ReactNativeBaseComponent +type ReactNativeBaseComponentViewConfig = { + validAttributes: Object; + uiViewClassName: string; + propTypes?: Object, +} + +/** + * @param {string} config iOS View configuration. + * @private + */ +var createReactNativeComponentClass = function( + viewConfig: ReactNativeBaseComponentViewConfig +): ReactClass { + var Constructor = function(element) { + this._currentElement = element; + + this._rootNodeID = null; + this._renderedChildren = null; + }; + Constructor.displayName = viewConfig.uiViewClassName; + Constructor.viewConfig = viewConfig; + Constructor.propTypes = viewConfig.propTypes; + Constructor.prototype = new ReactNativeBaseComponent(viewConfig); + Constructor.prototype.constructor = Constructor; + + return ((Constructor: any): ReactClass); +}; + +module.exports = createReactNativeComponentClass; diff --git a/src/renderers/native/ReactNative/findNodeHandle.js b/src/renderers/native/ReactNative/findNodeHandle.js new file mode 100644 index 0000000000..dd3191b82d --- /dev/null +++ b/src/renderers/native/ReactNative/findNodeHandle.js @@ -0,0 +1,112 @@ +/** + * 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 findNodeHandle + * @flow + */ + +'use strict'; + +var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactInstanceMap = require('ReactInstanceMap'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); + +var invariant = require('fbjs/lib/invariant'); +var warning = require('fbjs/lib/warning'); + +/** + * ReactNative vs ReactWeb + * ----------------------- + * React treats some pieces of data opaquely. This means that the information + * is first class (it can be passed around), but cannot be inspected. This + * allows us to build infrastructure that reasons about resources, without + * making assumptions about the nature of those resources, and this allows that + * infra to be shared across multiple platforms, where the resources are very + * different. General infra (such as `ReactMultiChild`) reasons opaquely about + * the data, but platform specific code (such as `ReactNativeBaseComponent`) can + * make assumptions about the data. + * + * + * `rootNodeID`, uniquely identifies a position in the generated native view + * tree. Many layers of composite components (created with `React.createClass`) + * can all share the same `rootNodeID`. + * + * `nodeHandle`: A sufficiently unambiguous way to refer to a lower level + * resource (dom node, native view etc). The `rootNodeID` is sufficient for web + * `nodeHandle`s, because the position in a tree is always enough to uniquely + * identify a DOM node (we never have nodes in some bank outside of the + * document). The same would be true for `ReactNative`, but we must maintain a + * mapping that we can send efficiently serializable + * strings across native boundaries. + * + * Opaque name TodaysWebReact FutureWebWorkerReact ReactNative + * ---------------------------------------------------------------------------- + * nodeHandle N/A rootNodeID tag + */ + +function findNodeHandle(componentOrHandle: any): ?number { + if (__DEV__) { + var owner = ReactCurrentOwner.current; + if (owner !== null) { + warning( + owner._warnedAboutRefsInRender, + '%s is accessing findNodeHandle inside its render(). ' + + 'render() should be a pure function of props and state. It should ' + + 'never access something that requires stale data from the previous ' + + 'render, such as refs. Move this logic to componentDidMount and ' + + 'componentDidUpdate instead.', + owner.getName() || 'A component' + ); + owner._warnedAboutRefsInRender = true; + } + } + if (componentOrHandle == null) { + return null; + } + if (typeof componentOrHandle === 'number') { + // Already a node handle + return componentOrHandle; + } + + var component = componentOrHandle; + + // TODO (balpert): Wrap iOS native components in a composite wrapper, then + // ReactInstanceMap.get here will always succeed for mounted components + var internalInstance = ReactInstanceMap.get(component); + if (internalInstance) { + return ReactNativeTagHandles.rootNodeIDToTag[internalInstance._rootNodeID]; + } else { + var rootNodeID = component._rootNodeID; + if (rootNodeID) { + return ReactNativeTagHandles.rootNodeIDToTag[rootNodeID]; + } else { + invariant( + ( + // Native + typeof component === 'object' && + '_rootNodeID' in component + ) || ( + // Composite + component.render != null && + typeof component.render === 'function' + ), + 'findNodeHandle(...): Argument is not a component ' + + '(type: %s, keys: %s)', + typeof component, + Object.keys(component) + ); + invariant( + false, + 'findNodeHandle(...): Unable to find node handle for unmounted ' + + 'component.' + ); + } + } +} + +module.exports = findNodeHandle; diff --git a/src/renderers/native/vendor/react/browser/eventPlugins/PanResponder.js b/src/renderers/native/vendor/react/browser/eventPlugins/PanResponder.js new file mode 100644 index 0000000000..c633dd724b --- /dev/null +++ b/src/renderers/native/vendor/react/browser/eventPlugins/PanResponder.js @@ -0,0 +1,378 @@ +/** + * @providesModule PanResponder + */ + +"use strict"; + +var TouchHistoryMath = require('TouchHistoryMath'); + +var currentCentroidXOfTouchesChangedAfter = + TouchHistoryMath.currentCentroidXOfTouchesChangedAfter; +var currentCentroidYOfTouchesChangedAfter = + TouchHistoryMath.currentCentroidYOfTouchesChangedAfter; +var previousCentroidXOfTouchesChangedAfter = + TouchHistoryMath.previousCentroidXOfTouchesChangedAfter; +var previousCentroidYOfTouchesChangedAfter = + TouchHistoryMath.previousCentroidYOfTouchesChangedAfter; +var currentCentroidX = TouchHistoryMath.currentCentroidX; +var currentCentroidY = TouchHistoryMath.currentCentroidY; + +/** + * `PanResponder` reconciles several touches into a single gesture. It makes + * single-touch gestures resilient to extra touches, and can be used to + * recognize simple multi-touch gestures. + * + * It provides a predictable wrapper of the responder handlers provided by the + * [gesture responder system](docs/gesture-responder-system.html). + * For each handler, it provides a new `gestureState` object alongside the + * native event object: + * + * ``` + * onPanResponderMove: (event, gestureState) => {} + * ``` + * + * A native event is a synthetic touch event with the following form: + * + * - `nativeEvent` + * + `changedTouches` - Array of all touch events that have changed since the last event + * + `identifier` - The ID of the touch + * + `locationX` - The X position of the touch, relative to the element + * + `locationY` - The Y position of the touch, relative to the element + * + `pageX` - The X position of the touch, relative to the root element + * + `pageY` - The Y position of the touch, relative to the root element + * + `target` - The node id of the element receiving the touch event + * + `timestamp` - A time identifier for the touch, useful for velocity calculation + * + `touches` - Array of all current touches on the screen + * + * A `gestureState` object has the following: + * + * - `stateID` - ID of the gestureState- persisted as long as there at least + * one touch on screen + * - `moveX` - the latest screen coordinates of the recently-moved touch + * - `moveY` - the latest screen coordinates of the recently-moved touch + * - `x0` - the screen coordinates of the responder grant + * - `y0` - the screen coordinates of the responder grant + * - `dx` - accumulated distance of the gesture since the touch started + * - `dy` - accumulated distance of the gesture since the touch started + * - `vx` - current velocity of the gesture + * - `vy` - current velocity of the gesture + * - `numberActiveTouches` - Number of touches currently on screen + * + * ### Basic Usage + * + * ``` + * componentWillMount: function() { + * this._panResponder = PanResponder.create({ + * // Ask to be the responder: + * onStartShouldSetPanResponder: (evt, gestureState) => true, + * onStartShouldSetPanResponderCapture: (evt, gestureState) => true, + * onMoveShouldSetPanResponder: (evt, gestureState) => true, + * onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, + * + * onPanResponderGrant: (evt, gestureState) => { + * // The guesture has started. Show visual feedback so the user knows + * // what is happening! + * + * // gestureState.{x,y}0 will be set to zero now + * }, + * onPanResponderMove: (evt, gestureState) => { + * // The most recent move distance is gestureState.move{X,Y} + * + * // The accumulated gesture distance since becoming responder is + * // gestureState.d{x,y} + * }, + * onPanResponderTerminationRequest: (evt, gestureState) => true, + * onPanResponderRelease: (evt, gestureState) => { + * // The user has released all touches while this view is the + * // responder. This typically means a gesture has succeeded + * }, + * onPanResponderTerminate: (evt, gestureState) => { + * // Another component has become the responder, so this gesture + * // should be cancelled + * }, + * onShouldBlockNativeResponder: (evt, gestureState) => { + * // Returns whether this component should block native components from becoming the JS + * // responder. Returns true by default. Is currently only supported on android. + * return true; + * }, + * }); + * }, + * + * render: function() { + * return ( + * + * ); + * }, + * + * ``` + * + * ### Working Example + * + * To see it in action, try the + * [PanResponder example in UIExplorer](https://github.com/facebook/react-native/blob/master/Examples/UIExplorer/PanResponderExample.js) + */ + +var PanResponder = { + + /** + * + * A graphical explanation of the touch data flow: + * + * +----------------------------+ +--------------------------------+ + * | ResponderTouchHistoryStore | |TouchHistoryMath | + * +----------------------------+ +----------+---------------------+ + * |Global store of touchHistory| |Allocation-less math util | + * |including activeness, start | |on touch history (centroids | + * |position, prev/cur position.| |and multitouch movement etc) | + * | | | | + * +----^-----------------------+ +----^---------------------------+ + * | | + * | (records relevant history | + * | of touches relevant for | + * | implementing higher level | + * | gestures) | + * | | + * +----+-----------------------+ +----|---------------------------+ + * | ResponderEventPlugin | | | Your App/Component | + * +----------------------------+ +----|---------------------------+ + * |Negotiates which view gets | Low level | | High level | + * |onResponderMove events. | events w/ | +-+-------+ events w/ | + * |Also records history into | touchHistory| | Pan | multitouch + | + * |ResponderTouchHistoryStore. +---------------->Responder+-----> accumulative| + * +----------------------------+ attached to | | | distance and | + * each event | +---------+ velocity. | + * | | + * | | + * +--------------------------------+ + * + * + * + * Gesture that calculates cumulative movement over time in a way that just + * "does the right thing" for multiple touches. The "right thing" is very + * nuanced. When moving two touches in opposite directions, the cumulative + * distance is zero in each dimension. When two touches move in parallel five + * pixels in the same direction, the cumulative distance is five, not ten. If + * two touches start, one moves five in a direction, then stops and the other + * touch moves fives in the same direction, the cumulative distance is ten. + * + * This logic requires a kind of processing of time "clusters" of touch events + * so that two touch moves that essentially occur in parallel but move every + * other frame respectively, are considered part of the same movement. + * + * Explanation of some of the non-obvious fields: + * + * - moveX/moveY: If no move event has been observed, then `(moveX, moveY)` is + * invalid. If a move event has been observed, `(moveX, moveY)` is the + * centroid of the most recently moved "cluster" of active touches. + * (Currently all move have the same timeStamp, but later we should add some + * threshold for what is considered to be "moving"). If a palm is + * accidentally counted as a touch, but a finger is moving greatly, the palm + * will move slightly, but we only want to count the single moving touch. + * - x0/y0: Centroid location (non-cumulative) at the time of becoming + * responder. + * - dx/dy: Cumulative touch distance - not the same thing as sum of each touch + * distance. Accounts for touch moves that are clustered together in time, + * moving the same direction. Only valid when currently responder (otherwise, + * it only represents the drag distance below the threshold). + * - vx/vy: Velocity. + */ + + _initializeGestureState: function(gestureState) { + gestureState.moveX = 0; + gestureState.moveY = 0; + gestureState.x0 = 0; + gestureState.y0 = 0; + gestureState.dx = 0; + gestureState.dy = 0; + gestureState.vx = 0; + gestureState.vy = 0; + gestureState.numberActiveTouches = 0; + // All `gestureState` accounts for timeStamps up until: + gestureState._accountsForMovesUpTo = 0; + }, + + /** + * This is nuanced and is necessary. It is incorrect to continuously take all + * active *and* recently moved touches, find the centroid, and track how that + * result changes over time. Instead, we must take all recently moved + * touches, and calculate how the centroid has changed just for those + * recently moved touches, and append that change to an accumulator. This is + * to (at least) handle the case where the user is moving three fingers, and + * then one of the fingers stops but the other two continue. + * + * This is very different than taking all of the recently moved touches and + * storing their centroid as `dx/dy`. For correctness, we must *accumulate + * changes* in the centroid of recently moved touches. + * + * There is also some nuance with how we handle multiple moved touches in a + * single event. With the way `ReactNativeEventEmitter` dispatches touches as + * individual events, multiple touches generate two 'move' events, each of + * them triggering `onResponderMove`. But with the way `PanResponder` works, + * all of the gesture inference is performed on the first dispatch, since it + * looks at all of the touches (even the ones for which there hasn't been a + * native dispatch yet). Therefore, `PanResponder` does not call + * `onResponderMove` passed the first dispatch. This diverges from the + * typical responder callback pattern (without using `PanResponder`), but + * avoids more dispatches than necessary. + */ + _updateGestureStateOnMove: function(gestureState, touchHistory) { + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + gestureState.moveX = currentCentroidXOfTouchesChangedAfter( + touchHistory, + gestureState._accountsForMovesUpTo + ); + gestureState.moveY = currentCentroidYOfTouchesChangedAfter( + touchHistory, + gestureState._accountsForMovesUpTo + ); + var movedAfter = gestureState._accountsForMovesUpTo; + var prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); + var x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); + var prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); + var y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); + var nextDX = gestureState.dx + (x - prevX); + var nextDY = gestureState.dy + (y - prevY); + + // TODO: This must be filtered intelligently. + var dt = + (touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo); + gestureState.vx = (nextDX - gestureState.dx) / dt; + gestureState.vy = (nextDY - gestureState.dy) / dt; + + gestureState.dx = nextDX; + gestureState.dy = nextDY; + gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp; + }, + + /** + * @param {object} config Enhanced versions of all of the responder callbacks + * that provide not only the typical `ResponderSyntheticEvent`, but also the + * `PanResponder` gesture state. Simply replace the word `Responder` with + * `PanResponder` in each of the typical `onResponder*` callbacks. For + * example, the `config` object would look like: + * + * - `onMoveShouldSetPanResponder: (e, gestureState) => {...}` + * - `onMoveShouldSetPanResponderCapture: (e, gestureState) => {...}` + * - `onStartShouldSetPanResponder: (e, gestureState) => {...}` + * - `onStartShouldSetPanResponderCapture: (e, gestureState) => {...}` + * - `onPanResponderReject: (e, gestureState) => {...}` + * - `onPanResponderGrant: (e, gestureState) => {...}` + * - `onPanResponderStart: (e, gestureState) => {...}` + * - `onPanResponderEnd: (e, gestureState) => {...}` + * - `onPanResponderRelease: (e, gestureState) => {...}` + * - `onPanResponderMove: (e, gestureState) => {...}` + * - `onPanResponderTerminate: (e, gestureState) => {...}` + * - `onPanResponderTerminationRequest: (e, gestureState) => {...}` + * - `onShouldBlockNativeResponder: (e, gestureState) => {...}` + * + * In general, for events that have capture equivalents, we update the + * gestureState once in the capture phase and can use it in the bubble phase + * as well. + * + * Be careful with onStartShould* callbacks. They only reflect updated + * `gestureState` for start/end events that bubble/capture to the Node. + * Once the node is the responder, you can rely on every start/end event + * being processed by the gesture and `gestureState` being updated + * accordingly. (numberActiveTouches) may not be totally accurate unless you + * are the responder. + */ + create: function(config) { + var gestureState = { + // Useful for debugging + stateID: Math.random(), + }; + PanResponder._initializeGestureState(gestureState); + var panHandlers = { + onStartShouldSetResponder: function(e) { + return config.onStartShouldSetPanResponder === undefined ? false : + config.onStartShouldSetPanResponder(e, gestureState); + }, + onMoveShouldSetResponder: function(e) { + return config.onMoveShouldSetPanResponder === undefined ? false : + config.onMoveShouldSetPanResponder(e, gestureState); + }, + onStartShouldSetResponderCapture: function(e) { + // TODO: Actually, we should reinitialize the state any time + // touches.length increases from 0 active to > 0 active. + if (e.nativeEvent.touches.length === 1) { + PanResponder._initializeGestureState(gestureState); + } + gestureState.numberActiveTouches = e.touchHistory.numberActiveTouches; + return config.onStartShouldSetPanResponderCapture !== undefined ? + config.onStartShouldSetPanResponderCapture(e, gestureState) : false; + }, + + onMoveShouldSetResponderCapture: function(e) { + var touchHistory = e.touchHistory; + // Responder system incorrectly dispatches should* to current responder + // Filter out any touch moves past the first one - we would have + // already processed multi-touch geometry during the first event. + if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { + return false; + } + PanResponder._updateGestureStateOnMove(gestureState, touchHistory); + return config.onMoveShouldSetPanResponderCapture ? + config.onMoveShouldSetPanResponderCapture(e, gestureState) : false; + }, + + onResponderGrant: function(e) { + gestureState.x0 = currentCentroidX(e.touchHistory); + gestureState.y0 = currentCentroidY(e.touchHistory); + gestureState.dx = 0; + gestureState.dy = 0; + config.onPanResponderGrant && config.onPanResponderGrant(e, gestureState); + // TODO: t7467124 investigate if this can be removed + return config.onShouldBlockNativeResponder === undefined ? true : + config.onShouldBlockNativeResponder(); + }, + + onResponderReject: function(e) { + config.onPanResponderReject && config.onPanResponderReject(e, gestureState); + }, + + onResponderRelease: function(e) { + config.onPanResponderRelease && config.onPanResponderRelease(e, gestureState); + PanResponder._initializeGestureState(gestureState); + }, + + onResponderStart: function(e) { + var touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + config.onPanResponderStart && config.onPanResponderStart(e, gestureState); + }, + + onResponderMove: function(e) { + var touchHistory = e.touchHistory; + // Guard against the dispatch of two touch moves when there are two + // simultaneously changed touches. + if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { + return; + } + // Filter out any touch moves past the first one - we would have + // already processed multi-touch geometry during the first event. + PanResponder._updateGestureStateOnMove(gestureState, touchHistory); + config.onPanResponderMove && config.onPanResponderMove(e, gestureState); + }, + + onResponderEnd: function(e) { + var touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + config.onPanResponderEnd && config.onPanResponderEnd(e, gestureState); + }, + + onResponderTerminate: function(e) { + config.onPanResponderTerminate && + config.onPanResponderTerminate(e, gestureState); + PanResponder._initializeGestureState(gestureState); + }, + + onResponderTerminationRequest: function(e) { + return config.onPanResponderTerminationRequest === undefined ? true : + config.onPanResponderTerminationRequest(e, gestureState); + }, + }; + return {panHandlers: panHandlers}; + }, +}; + +module.exports = PanResponder; diff --git a/src/renderers/native/vendor/react/browser/eventPlugins/TouchHistoryMath.js b/src/renderers/native/vendor/react/browser/eventPlugins/TouchHistoryMath.js new file mode 100644 index 0000000000..1507d21f56 --- /dev/null +++ b/src/renderers/native/vendor/react/browser/eventPlugins/TouchHistoryMath.js @@ -0,0 +1,122 @@ +/** + * @providesModule TouchHistoryMath + */ + +"use strict"; + +var TouchHistoryMath = { + /** + * This code is optimized and not intended to look beautiful. This allows + * computing of touch centroids that have moved after `touchesChangedAfter` + * timeStamp. You can compute the current centroid involving all touches + * moves after `touchesChangedAfter`, or you can compute the previous + * centroid of all touches that were moved after `touchesChangedAfter`. + * + * @param {TouchHistoryMath} touchHistory Standard Responder touch track + * data. + * @param {number} touchesChangedAfter timeStamp after which moved touches + * are considered "actively moving" - not just "active". + * @param {boolean} isXAxis Consider `x` dimension vs. `y` dimension. + * @param {boolean} ofCurrent Compute current centroid for actively moving + * touches vs. previous centroid of now actively moving touches. + * @return {number} value of centroid in specified dimension. + */ + centroidDimension: function(touchHistory, touchesChangedAfter, isXAxis, ofCurrent) { + var touchBank = touchHistory.touchBank; + var total = 0; + var count = 0; + + var oneTouchData = touchHistory.numberActiveTouches === 1 ? + touchHistory.touchBank[touchHistory.indexOfSingleActiveTouch] : null; + + if (oneTouchData !== null) { + if (oneTouchData.touchActive && oneTouchData.currentTimeStamp > touchesChangedAfter) { + total += ofCurrent && isXAxis ? oneTouchData.currentPageX : + ofCurrent && !isXAxis ? oneTouchData.currentPageY : + !ofCurrent && isXAxis ? oneTouchData.previousPageX : + oneTouchData.previousPageY; + count = 1; + } + } else { + for (var i = 0; i < touchBank.length; i++) { + var touchTrack = touchBank[i]; + if (touchTrack !== null && + touchTrack !== undefined && + touchTrack.touchActive && + touchTrack.currentTimeStamp >= touchesChangedAfter) { + var toAdd; // Yuck, program temporarily in invalid state. + if (ofCurrent && isXAxis) { + toAdd = touchTrack.currentPageX; + } else if (ofCurrent && !isXAxis) { + toAdd = touchTrack.currentPageY; + } else if (!ofCurrent && isXAxis) { + toAdd = touchTrack.previousPageX; + } else { + toAdd = touchTrack.previousPageY; + } + total += toAdd; + count++; + } + } + } + return count > 0 ? total / count : TouchHistoryMath.noCentroid; + }, + + currentCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension( + touchHistory, + touchesChangedAfter, + true, // isXAxis + true // ofCurrent + ); + }, + + currentCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension( + touchHistory, + touchesChangedAfter, + false, // isXAxis + true // ofCurrent + ); + }, + + previousCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension( + touchHistory, + touchesChangedAfter, + true, // isXAxis + false // ofCurrent + ); + }, + + previousCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension( + touchHistory, + touchesChangedAfter, + false, // isXAxis + false // ofCurrent + ); + }, + + currentCentroidX: function(touchHistory) { + return TouchHistoryMath.centroidDimension( + touchHistory, + 0, // touchesChangedAfter + true, // isXAxis + true // ofCurrent + ); + }, + + currentCentroidY: function(touchHistory) { + return TouchHistoryMath.centroidDimension( + touchHistory, + 0, // touchesChangedAfter + false, // isXAxis + true // ofCurrent + ); + }, + + noCentroid: -1, +}; + +module.exports = TouchHistoryMath; diff --git a/src/renderers/native/vendor/react/core/clamp.js b/src/renderers/native/vendor/react/core/clamp.js new file mode 100644 index 0000000000..cb7578ceec --- /dev/null +++ b/src/renderers/native/vendor/react/core/clamp.js @@ -0,0 +1,35 @@ +/** + * @generated SignedSource<> + * + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * !! This file is a check-in of a static_upstream project! !! + * !! !! + * !! You should not modify this file directly. Instead: !! + * !! 1) Use `fjs use-upstream` to temporarily replace this with !! + * !! the latest version from upstream. !! + * !! 2) Make your changes, test them, etc. !! + * !! 3) Use `fjs push-upstream` to copy your changes back to !! + * !! static_upstream. !! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * + * @providesModule clamp + * @typechecks + */ + + /** + * @param {number} value + * @param {number} min + * @param {number} max + * @return {number} + */ +function clamp(min, value, max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +} + +module.exports = clamp; diff --git a/src/renderers/native/vendor/react/platform/NodeHandle.js b/src/renderers/native/vendor/react/platform/NodeHandle.js new file mode 100644 index 0000000000..c7c93545bf --- /dev/null +++ b/src/renderers/native/vendor/react/platform/NodeHandle.js @@ -0,0 +1,83 @@ +/** + * @providesModule NodeHandle + */ + + /** + * A "handle" is a serializable representation of the underlying platform's + * native node abstraction. This allows reasoning about nodes behind a thread + * (worker) boundary. On some platforms (DOM main thread) the node handle *is* + * an actual DOM node - `NodeHandle` (and potentially other libraries) + * abstract away those differences so you can write code that doesn't depend + * on whether or not you are running in a worker. For example, you could write + * application code: + * + * + * SomeLibrary.measureNodeHandle(myNodeHandle, cb) + * + * Where `measureNodeHandle` knows how to handle actual DOM nodes if running + * in a worker thread, and knows how to handle numeric IDs if running in a + * 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 + * `getRootNodeID`). + * + * +------------------+ +------------------+ +------------------+ + * | | | | | | + * | ReactJS | | YourUtilities | | Animation Utils | + * | | | | | | + * +------------------+ +------------------+ +------------------+ + * + * +------------------------------------------------------------+ + * | Async Platform Independent Node Interface | + * +------------------------------------------------------------+ + * | | + * | NodeIterface: | + * | -measure(nodeHandle, cb) | + * | -setProperties(nodeHandle, cb) | + * | -manageChildren(nodeHandle, nodeHandles, cb) | + * | ... | + * | | + * | Note: This may be a simplification. We could break up much | + * | of this functionality into several smaller libraries, each | + * | one requiring a . | + * +------------------------------------------------------------+ + * + * +------------------------------------------------------------+ + * | Platform Implementations | + * | ----------------------------------------- | + * | React Canvas | React DOM Worker | React DOM main | + * +------------------------------------------------------------+ + * | | | | + * |-measure(..) |-measure(..) |-measure(..) | + * |-setProperties(..) |-setProperties(..) |-setProperties(..) | + * |-manageChildren(..)|-manageChildren(..) |-manageChildren(..)| + * | ... | ... | ... | + * +-----------------------------o------------------------------+ + * | Worker simply ^ + * | marshals commands | + * | to Web DOM thread. | + * +---------------------+ + */ +var NodeHandle = { + /** + * Injection + */ + injection: { + injectImplementation: function(Impl) { + NodeHandle._Implementation = Impl; + } + }, + + _Implementation: null, + + /** + * @param {NodeHandle} nodeHandle The handle to the low level resource. + * @return {string} React root node ID. + */ + getRootNodeID: function(nodeHandle) { + return NodeHandle._Implementation.getRootNodeID(nodeHandle); + } +}; + +module.exports = NodeHandle; diff --git a/src/renderers/native/vendor/react/platformImplementations/universal/worker/UniversalWorkerNodeHandle.js b/src/renderers/native/vendor/react/platformImplementations/universal/worker/UniversalWorkerNodeHandle.js new file mode 100644 index 0000000000..ca7faa7fec --- /dev/null +++ b/src/renderers/native/vendor/react/platformImplementations/universal/worker/UniversalWorkerNodeHandle.js @@ -0,0 +1,19 @@ +/** + * @providesModule UniversalWorkerNodeHandle + */ + +var ReactNativeTagHandles = require('ReactNativeTagHandles'); + +var invariant = require('fbjs/lib/invariant'); + +var UniversalWorkerNodeHandle = { + getRootNodeID: function(nodeHandle) { + invariant( + nodeHandle !== undefined && nodeHandle !== null && nodeHandle !== 0, + 'No node handle defined' + ); + return ReactNativeTagHandles.tagToRootNodeID[nodeHandle]; + } +}; + +module.exports = UniversalWorkerNodeHandle; diff --git a/src/renderers/native/vendor/react/vendor/core/ExecutionEnvironment.android.js b/src/renderers/native/vendor/react/vendor/core/ExecutionEnvironment.android.js new file mode 100644 index 0000000000..23116e61cd --- /dev/null +++ b/src/renderers/native/vendor/react/vendor/core/ExecutionEnvironment.android.js @@ -0,0 +1,51 @@ +/** + * Copyright 2013-2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ExecutionEnvironment + * + * NB: This is a temporary override that has not yet been merged upstream. It is NOT actually part + * of react-core (yet) + * + * Stubs SignedSource<<7afee88a05412d0c4eef54817419648e>> + */ + +/*jslint evil: true */ + +"use strict"; + +var canUseDOM = false; + +/** + * Simple, lightweight module assisting with the detection and context of + * Worker. Helps avoid circular dependencies and allows code to reason about + * whether or not they are in a Worker, even if they never include the main + * `ReactWorker` dependency. + */ +var ExecutionEnvironment = { + + canUseDOM: canUseDOM, + + canUseWorkers: typeof Worker !== 'undefined', + + canUseEventListeners: + canUseDOM && !!(window.addEventListener || window.attachEvent), + + canUseViewport: canUseDOM && !!window.screen, + + isInWorker: !canUseDOM // For now, this is true - might change in the future. + +}; + +module.exports = ExecutionEnvironment; diff --git a/src/renderers/native/vendor/react/vendor/core/ExecutionEnvironment.ios.js b/src/renderers/native/vendor/react/vendor/core/ExecutionEnvironment.ios.js new file mode 100644 index 0000000000..3a59accdcd --- /dev/null +++ b/src/renderers/native/vendor/react/vendor/core/ExecutionEnvironment.ios.js @@ -0,0 +1,48 @@ +/** + * Copyright 2013-2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ExecutionEnvironment + * + * Stubs SignedSource<<7afee88a05412d0c4eef54817419648e>> + */ + +/*jslint evil: true */ + +"use strict"; + +var canUseDOM = false; + +/** + * Simple, lightweight module assisting with the detection and context of + * Worker. Helps avoid circular dependencies and allows code to reason about + * whether or not they are in a Worker, even if they never include the main + * `ReactWorker` dependency. + */ +var ExecutionEnvironment = { + + canUseDOM: canUseDOM, + + canUseWorkers: typeof Worker !== 'undefined', + + canUseEventListeners: + canUseDOM && !!(window.addEventListener || window.attachEvent), + + canUseViewport: canUseDOM && !!window.screen, + + isInWorker: !canUseDOM // For now, this is true - might change in the future. + +}; + +module.exports = ExecutionEnvironment;