From 5b4b6e7999130b0e000213bc3df23f2c8e1a2c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 20 Apr 2016 03:41:20 +0100 Subject: [PATCH 01/26] Merge pull request #6338 from sebmarkbage/reactnative2 Move React Core Integration and Injection to the Core Repo (cherry picked from commit c84ad52ddbb74095200215bc9c6abf51118e7f64) --- .babelrc | 1 + .travis.yml | 1 + Gruntfile.js | 6 + grunt/tasks/npm-react-native.js | 46 ++ grunt/tasks/version-check.js | 2 + gulpfile.js | 38 +- package.json | 1 + packages/react-native-renderer/README.md | 5 + packages/react-native-renderer/index.js | 3 + packages/react-native-renderer/package.json | 23 + packages/react/lib/ReactDOM.native.js | 12 - .../ReactIOS/IOSDefaultEventPluginOrder.js | 19 + .../ReactIOS/IOSNativeBridgeEventPlugin.js | 71 +++ .../native/ReactIOS/NativeMethodsMixin.js | 219 +++++++ .../native/ReactNative/ReactNative.js | 76 +++ .../ReactNativeAttributePayload.js | 546 ++++++++++++++++++ .../ReactNative/ReactNativeBaseComponent.js | 230 ++++++++ .../ReactNativeComponentEnvironment.js | 42 ++ .../ReactNative/ReactNativeComponentTree.js | 66 +++ .../ReactNative/ReactNativeContainerInfo.js | 21 + .../ReactNative/ReactNativeDOMIDOperations.js | 97 ++++ .../ReactNativeDefaultInjection.js | 113 ++++ .../ReactNative/ReactNativeEventEmitter.js | 215 +++++++ .../ReactNativeGlobalInteractionHandler.js | 33 ++ .../ReactNativeGlobalResponderHandler.js | 28 + .../native/ReactNative/ReactNativeMount.js | 238 ++++++++ .../ReactNative/ReactNativePropRegistry.js | 44 ++ .../ReactNativeReconcileTransaction.js | 103 ++++ .../ReactNative/ReactNativeTagHandles.js | 57 ++ .../ReactNative/ReactNativeTextComponent.js | 81 +++ .../ReactNative/ReactNativeTreeTraversal.js | 126 ++++ .../InitializeJavaScriptAppEngine.js | 14 + .../__mocks__/InteractionManager.js | 16 + .../__mocks__/JSTimersExecution.js | 14 + .../native/ReactNative/__mocks__/Platform.js | 16 + .../ReactNative/__mocks__/RCTEventEmitter.js | 14 + .../native/ReactNative/__mocks__/RCTLog.js | 14 + .../ReactNative/__mocks__/TextInputState.js | 17 + .../native/ReactNative/__mocks__/UIManager.js | 21 + .../ReactNative/__mocks__/deepDiffer.js | 63 ++ .../deepFreezeAndThrowOnMutationInDev.js | 16 + .../ReactNative/__mocks__/flattenStyle.js | 16 + .../native/ReactNative/__mocks__/merge.js | 18 + .../ReactNativeAttributePayload-test.js | 246 ++++++++ .../__tests__/ReactNativeMount-test.js | 61 ++ .../createReactNativeComponentClass.js | 48 ++ .../native/ReactNative/findNodeHandle.js | 111 ++++ .../shared/event/EventPluginUtils.js | 2 +- .../shared/event/eventPlugins/PanResponder.js | 379 ++++++++++++ .../event/eventPlugins/TouchHistoryMath.js | 122 ++++ 50 files changed, 3749 insertions(+), 22 deletions(-) create mode 100644 grunt/tasks/npm-react-native.js create mode 100644 packages/react-native-renderer/README.md create mode 100644 packages/react-native-renderer/index.js create mode 100644 packages/react-native-renderer/package.json delete mode 100644 packages/react/lib/ReactDOM.native.js create mode 100644 src/renderers/native/ReactIOS/IOSDefaultEventPluginOrder.js create mode 100644 src/renderers/native/ReactIOS/IOSNativeBridgeEventPlugin.js create mode 100644 src/renderers/native/ReactIOS/NativeMethodsMixin.js create mode 100644 src/renderers/native/ReactNative/ReactNative.js create mode 100644 src/renderers/native/ReactNative/ReactNativeAttributePayload.js create mode 100644 src/renderers/native/ReactNative/ReactNativeBaseComponent.js create mode 100644 src/renderers/native/ReactNative/ReactNativeComponentEnvironment.js create mode 100644 src/renderers/native/ReactNative/ReactNativeComponentTree.js create mode 100644 src/renderers/native/ReactNative/ReactNativeContainerInfo.js create mode 100644 src/renderers/native/ReactNative/ReactNativeDOMIDOperations.js create mode 100644 src/renderers/native/ReactNative/ReactNativeDefaultInjection.js create mode 100644 src/renderers/native/ReactNative/ReactNativeEventEmitter.js create mode 100644 src/renderers/native/ReactNative/ReactNativeGlobalInteractionHandler.js create mode 100644 src/renderers/native/ReactNative/ReactNativeGlobalResponderHandler.js create mode 100644 src/renderers/native/ReactNative/ReactNativeMount.js create mode 100644 src/renderers/native/ReactNative/ReactNativePropRegistry.js create mode 100644 src/renderers/native/ReactNative/ReactNativeReconcileTransaction.js create mode 100644 src/renderers/native/ReactNative/ReactNativeTagHandles.js create mode 100644 src/renderers/native/ReactNative/ReactNativeTextComponent.js create mode 100644 src/renderers/native/ReactNative/ReactNativeTreeTraversal.js create mode 100644 src/renderers/native/ReactNative/__mocks__/InitializeJavaScriptAppEngine.js create mode 100644 src/renderers/native/ReactNative/__mocks__/InteractionManager.js create mode 100644 src/renderers/native/ReactNative/__mocks__/JSTimersExecution.js create mode 100644 src/renderers/native/ReactNative/__mocks__/Platform.js create mode 100644 src/renderers/native/ReactNative/__mocks__/RCTEventEmitter.js create mode 100644 src/renderers/native/ReactNative/__mocks__/RCTLog.js create mode 100644 src/renderers/native/ReactNative/__mocks__/TextInputState.js create mode 100644 src/renderers/native/ReactNative/__mocks__/UIManager.js create mode 100644 src/renderers/native/ReactNative/__mocks__/deepDiffer.js create mode 100644 src/renderers/native/ReactNative/__mocks__/deepFreezeAndThrowOnMutationInDev.js create mode 100644 src/renderers/native/ReactNative/__mocks__/flattenStyle.js create mode 100644 src/renderers/native/ReactNative/__mocks__/merge.js create mode 100644 src/renderers/native/ReactNative/__tests__/ReactNativeAttributePayload-test.js create mode 100644 src/renderers/native/ReactNative/__tests__/ReactNativeMount-test.js create mode 100644 src/renderers/native/ReactNative/createReactNativeComponentClass.js create mode 100644 src/renderers/native/ReactNative/findNodeHandle.js create mode 100644 src/renderers/shared/event/eventPlugins/PanResponder.js create mode 100644 src/renderers/shared/event/eventPlugins/TouchHistoryMath.js diff --git a/.babelrc b/.babelrc index 73803b050c..2ff815b425 100644 --- a/.babelrc +++ b/.babelrc @@ -4,6 +4,7 @@ "plugins": [ "fbjs-scripts/babel-6/dev-expression", "syntax-trailing-function-commas", + "babel-plugin-transform-object-rest-spread", "transform-es2015-template-literals", "transform-es2015-literals", "transform-es2015-arrow-functions", diff --git a/.travis.yml b/.travis.yml index bfbdfeaa20..634cf98e55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,6 +58,7 @@ script: -F "react-dom-server.min=@build/react-dom-server.min.js" \ -F "npm-react=@build/packages/react.tgz" \ -F "npm-react-dom=@build/packages/react-dom.tgz" \ + -F "npm-react-native=@build/packages/react-native-renderer.tgz" \ -F "commit=$TRAVIS_COMMIT" \ -F "date=`git log --format='%ct' -1`" \ -F "pull_request=$TRAVIS_PULL_REQUEST" \ diff --git a/Gruntfile.js b/Gruntfile.js index c3ee8c20d6..f1f3c43567 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -74,6 +74,10 @@ module.exports = function(grunt) { grunt.registerTask('npm-react-dom:release', npmReactDOMTasks.buildRelease); grunt.registerTask('npm-react-dom:pack', npmReactDOMTasks.packRelease); + var npmReactNativeTasks = require('./grunt/tasks/npm-react-native'); + grunt.registerTask('npm-react-native:release', npmReactNativeTasks.buildRelease); + grunt.registerTask('npm-react-native:pack', npmReactNativeTasks.packRelease); + var npmReactAddonsTasks = require('./grunt/tasks/npm-react-addons'); grunt.registerTask('npm-react-addons:release', npmReactAddonsTasks.buildReleases); grunt.registerTask('npm-react-addons:pack', npmReactAddonsTasks.packReleases); @@ -127,6 +131,8 @@ module.exports = function(grunt) { 'npm-react:pack', 'npm-react-dom:release', 'npm-react-dom:pack', + 'npm-react-native:release', + 'npm-react-native:pack', 'npm-react-addons:release', 'npm-react-addons:pack', 'compare_size', diff --git a/grunt/tasks/npm-react-native.js b/grunt/tasks/npm-react-native.js new file mode 100644 index 0000000000..7157e08f6b --- /dev/null +++ b/grunt/tasks/npm-react-native.js @@ -0,0 +1,46 @@ +'use strict'; + +var fs = require('fs'); +var grunt = require('grunt'); + +var src = 'packages/react-native-renderer/'; +var dest = 'build/packages/react-native-renderer/'; + +function buildRelease() { + if (grunt.file.exists(dest)) { + grunt.file.delete(dest); + } + + // Copy to build/packages/react-native-renderer + var mappings = [].concat( + grunt.file.expandMapping('**/*', dest, {cwd: src}), + grunt.file.expandMapping('{LICENSE,PATENTS}', dest) + ); + mappings.forEach(function(mapping) { + var mappingSrc = mapping.src[0]; + var mappingDest = mapping.dest; + if (grunt.file.isDir(mappingSrc)) { + grunt.file.mkdir(mappingDest); + } else { + grunt.file.copy(mappingSrc, mappingDest); + } + }); +} + +function packRelease() { + var done = this.async(); + var spawnCmd = { + cmd: 'npm', + args: ['pack', 'packages/react-native-renderer'], + }; + grunt.util.spawn(spawnCmd, function() { + var buildSrc = 'react-native-renderer-' + grunt.config.data.pkg.version + '.tgz'; + var buildDest = 'build/packages/react-native-renderer.tgz'; + fs.rename(buildSrc, buildDest, done); + }); +} + +module.exports = { + buildRelease: buildRelease, + packRelease: packRelease, +}; diff --git a/grunt/tasks/version-check.js b/grunt/tasks/version-check.js index ff6da818c0..99f55feac0 100644 --- a/grunt/tasks/version-check.js +++ b/grunt/tasks/version-check.js @@ -11,6 +11,8 @@ module.exports = function() { grunt.file.readJSON('./packages/react/package.json').version, 'packages/react-dom/package.json': grunt.file.readJSON('./packages/react-dom/package.json').version, + 'packages/react-native-renderer/package.json': + grunt.file.readJSON('./packages/react-native-renderer/package.json').version, 'packages/react-addons/package.json (version)': addonsData.version, // Get the "version" without the range bit 'packages/react-addons/package.json (react dependency)': addonsData.peerDependencies.react.slice(1), diff --git a/gulpfile.js b/gulpfile.js index f885ea349a..2c2a7c9823 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -20,6 +20,7 @@ var paths = { react: { src: [ 'src/**/*.js', + '!src/**/__benchmarks__/**/*.js', '!src/**/__tests__/**/*.js', '!src/**/__mocks__/**/*.js', '!src/shared/vendor/**/*.js', @@ -28,17 +29,36 @@ var paths = { }, }; +var fbjsModuleMap = require('fbjs/module-map'); +var moduleMap = {}; +for (var key in fbjsModuleMap) { + moduleMap[key] = fbjsModuleMap[key]; +} +var whiteListNames = [ + 'deepDiffer', + 'deepFreezeAndThrowOnMutationInDev', + 'flattenStyle', + 'InitializeJavaScriptAppEngine', + 'InteractionManager', + 'JSTimersExecution', + 'merge', + 'Platform', + 'RCTEventEmitter', + 'RCTLog', + 'TextInputState', + 'UIManager', + 'View', +]; + +whiteListNames.forEach(function(name) { + moduleMap[name] = name; +}); + +moduleMap['object-assign'] = 'object-assign'; + var babelOpts = { plugins: [ - [babelPluginModules, { - map: Object.assign( - {}, - require('fbjs/module-map'), - { - 'object-assign': 'object-assign', - } - ), - }], + [babelPluginModules, { map: moduleMap }], ], }; diff --git a/package.json b/package.json index c1fc007c06..2ada0a662b 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "babel-plugin-transform-es2015-template-literals": "^6.5.2", "babel-plugin-transform-es3-member-expression-literals": "^6.5.0", "babel-plugin-transform-es3-property-literals": "^6.5.0", + "babel-plugin-transform-object-rest-spread": "^6.6.5", "babel-preset-react": "^6.5.0", "browserify": "^13.0.0", "bundle-collapser": "^1.1.1", diff --git a/packages/react-native-renderer/README.md b/packages/react-native-renderer/README.md new file mode 100644 index 0000000000..db3e7d3a59 --- /dev/null +++ b/packages/react-native-renderer/README.md @@ -0,0 +1,5 @@ +# `react-native-renderer` + +This package is the renderer that is used by the react-native package. +It is intended to be used inside the react-native environment. It is not +intended to be used stand alone. diff --git a/packages/react-native-renderer/index.js b/packages/react-native-renderer/index.js new file mode 100644 index 0000000000..6f35853e7c --- /dev/null +++ b/packages/react-native-renderer/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('react/lib/ReactNative'); diff --git a/packages/react-native-renderer/package.json b/packages/react-native-renderer/package.json new file mode 100644 index 0000000000..a9c487be12 --- /dev/null +++ b/packages/react-native-renderer/package.json @@ -0,0 +1,23 @@ +{ + "name": "react-native-renderer", + "version": "16.0.0-alpha", + "description": "React package for use inside react-native.", + "main": "index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/facebook/react.git" + }, + "keywords": [ + "react", + "react-native" + ], + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/facebook/react/issues" + }, + "homepage": "https://facebook.github.io/react-native/", + "dependencies": { + "fbjs": "^0.8.0", + "react": "^16.0.0-alpha" + } +} diff --git a/packages/react/lib/ReactDOM.native.js b/packages/react/lib/ReactDOM.native.js deleted file mode 100644 index bdd9044a8b..0000000000 --- a/packages/react/lib/ReactDOM.native.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -var ReactUpdates = require('./ReactUpdates'); - -// TODO: In React Native, ReactTestUtils depends on ./ReactDOM (for -// renderIntoDocument, which should never be called) and Relay depends on -// react-dom (for batching). Once those are fixed, nothing in RN should import -// this module and this file can go away. - -module.exports = { - unstable_batchedUpdates: ReactUpdates.batchedUpdates, -}; diff --git a/src/renderers/native/ReactIOS/IOSDefaultEventPluginOrder.js b/src/renderers/native/ReactIOS/IOSDefaultEventPluginOrder.js new file mode 100644 index 0000000000..eae211c40f --- /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..564e10f28c --- /dev/null +++ b/src/renderers/native/ReactIOS/IOSNativeBridgeEventPlugin.js @@ -0,0 +1,71 @@ +/** + * 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('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), + + /** + * @see {EventPluginHub.extractEvents} + */ + extractEvents: function( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget + ): ?Object { + var bubbleDispatchConfig = customBubblingEventTypes[topLevelType]; + var directDispatchConfig = customDirectEventTypes[topLevelType]; + var event = SyntheticEvent.getPooled( + bubbleDispatchConfig || directDispatchConfig, + targetInst, + nativeEvent, + nativeEventTarget + ); + 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..0d06e11fef --- /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('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 undefined; + } + return callback.apply(context, arguments); + }; +}; + +module.exports = NativeMethodsMixin; diff --git a/src/renderers/native/ReactNative/ReactNative.js b/src/renderers/native/ReactNative/ReactNative.js new file mode 100644 index 0000000000..e40c6b7576 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNative.js @@ -0,0 +1,76 @@ +/** + * 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 ReactNativeComponentTree = require('ReactNativeComponentTree'); +var ReactNativeDefaultInjection = require('ReactNativeDefaultInjection'); + +var ReactElement = require('ReactElement'); +var ReactNativeMount = require('ReactNativeMount'); +var ReactUpdates = require('ReactUpdates'); + +var findNodeHandle = require('findNodeHandle'); + +ReactNativeDefaultInjection.inject(); + +var render = function( + element: ReactElement, + mountInto: number, + callback?: ?(() => void) +): ?ReactComponent { + return ReactNativeMount.renderComponent(element, mountInto, callback); +}; + +var ReactNative = { + hasReactNativeInitialized: false, + findNodeHandle: findNodeHandle, + render: render, + unmountComponentAtNode: ReactNativeMount.unmountComponentAtNode, + + /* eslint-disable camelcase */ + unstable_batchedUpdates: ReactUpdates.batchedUpdates, + /* eslint-enable camelcase */ + + unmountComponentAtNodeAndRemoveContainer: ReactNativeMount.unmountComponentAtNodeAndRemoveContainer, +}; + +// 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({ + ComponentTree: { + getClosestInstanceFromNode: function(node) { + return ReactNativeComponentTree.getClosestInstanceFromNode(node); + }, + getNodeFromInstance: function(inst) { + // inst is an internal instance (but could be a composite) + while (inst._renderedComponent) { + inst = inst._renderedComponent; + } + if (inst) { + return ReactNativeComponentTree.getNodeFromInstance(inst); + } else { + return null; + } + }, + }, + Mount: ReactNativeMount, + Reconciler: require('ReactReconciler'), + }); +} + +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..ca1cd57030 --- /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) { + 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 (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..a15dae5bba --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeBaseComponent.js @@ -0,0 +1,230 @@ +/** + * 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 ReactNativeComponentTree = require('ReactNativeComponentTree'); +var ReactNativeEventEmitter = require('ReactNativeEventEmitter'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var ReactMultiChild = require('ReactMultiChild'); +var UIManager = require('UIManager'); + +var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); + +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; + }, + + unmountComponent: function() { + ReactNativeComponentTree.uncacheNode(this); + deleteAllListeners(this); + 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; + createdTags[i] = childTag; + } + 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( + 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, 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, key, nextProps[key]); + } else { + deleteListener(this, key); + } + } + } + }, + + /** + * Currently this still uses IDs for reconciliation so this can return null. + * + * @return {null} Null. + */ + getNativeNode: function() { + return this._rootNodeID; + }, + + /** + * @param {string} rootID Root ID of this subtree. + * @param {Transaction} transaction For creating/updating. + * @return {string} Unique iOS view tag. + */ + mountComponent: function(transaction, nativeParent, nativeContainerInfo, context) { + var tag = ReactNativeTagHandles.allocateTag(); + + this._rootNodeID = tag; + this._nativeParent = nativeParent; + this._nativeContainerInfo = nativeContainerInfo; + + 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 nativeTopRootTag = nativeContainerInfo._tag; + UIManager.createView( + tag, + this.viewConfig.uiViewClassName, + nativeTopRootTag, + updatePayload + ); + + ReactNativeComponentTree.precacheNode(this, tag); + + this._registerListenersUponCreation(this._currentElement.props); + this.initializeChildren( + this._currentElement.props.children, + tag, + transaction, + context + ); + return 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/ReactNativeComponentEnvironment.js b/src/renderers/native/ReactNative/ReactNativeComponentEnvironment.js new file mode 100644 index 0000000000..c8cff21b2e --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeComponentEnvironment.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, + + replaceNodeWithMarkup: 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/ReactNativeComponentTree.js b/src/renderers/native/ReactNative/ReactNativeComponentTree.js new file mode 100644 index 0000000000..82963ad4b1 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeComponentTree.js @@ -0,0 +1,66 @@ +/** + * Copyright 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. + * + * @providesModule ReactNativeComponentTree + */ + +'use strict'; + +var invariant = require('invariant'); + +var instanceCache = {}; + +/** + * Drill down (through composites and empty components) until we get a native or + * native text component. + * + * This is pretty polymorphic but unavoidable with the current structure we have + * for `_renderedChildren`. + */ +function getRenderedNativeOrTextFromComponent(component) { + var rendered; + while ((rendered = component._renderedComponent)) { + component = rendered; + } + return component; +} + +/** + * Populate `_nativeNode` on the rendered native/text component with the given + * DOM node. The passed `inst` can be a composite. + */ +function precacheNode(inst, tag) { + var nativeInst = getRenderedNativeOrTextFromComponent(inst); + instanceCache[tag] = nativeInst; +} + +function uncacheNode(inst) { + var tag = inst._rootNodeID; + if (tag) { + delete instanceCache[tag]; + } +} + +function getInstanceFromTag(tag) { + return instanceCache[tag] || null; +} + +function getTagFromInstance(inst) { + invariant(inst._rootNodeID, 'All native instances should have a tag.'); + return inst._rootNodeID; +} + +var ReactNativeComponentTree = { + getClosestInstanceFromNode: getInstanceFromTag, + getInstanceFromNode: getInstanceFromTag, + getNodeFromInstance: getTagFromInstance, + precacheNode: precacheNode, + uncacheNode: uncacheNode, +}; + +module.exports = ReactNativeComponentTree; diff --git a/src/renderers/native/ReactNative/ReactNativeContainerInfo.js b/src/renderers/native/ReactNative/ReactNativeContainerInfo.js new file mode 100644 index 0000000000..cc37ede4a0 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeContainerInfo.js @@ -0,0 +1,21 @@ +/** + * 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 ReactNativeContainerInfo + * @flow + */ +'use strict'; + +function ReactNativeContainerInfo(tag) { + var info = { + _tag: tag, + }; + return info; +} + +module.exports = ReactNativeContainerInfo; diff --git a/src/renderers/native/ReactNative/ReactNativeDOMIDOperations.js b/src/renderers/native/ReactNative/ReactNativeDOMIDOperations.js new file mode 100644 index 0000000000..0bb92fef55 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeDOMIDOperations.js @@ -0,0 +1,97 @@ +/** + * 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 ReactNativeComponentTree = require('ReactNativeComponentTree'); +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 {ReactNativeBaseComponent} 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(inst, childrenUpdates) { + if (!childrenUpdates.length) { + return; + } + + var containerTag = ReactNativeComponentTree.getNodeFromInstance(inst); + + var moveFromIndices; + var moveToIndices; + var addChildTags; + var addAtIndices; + var removeAtIndices; + + for (var i = 0; i < childrenUpdates.length; i++) { + var update = childrenUpdates[i]; + if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING) { + (moveFromIndices || (moveFromIndices = [])).push(update.fromIndex); + (moveToIndices || (moveToIndices = [])).push(update.toIndex); + } else if (update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) { + (removeAtIndices || (removeAtIndices = [])).push(update.fromIndex); + } else if (update.type === ReactMultiChildUpdateTypes.INSERT_MARKUP) { + var mountImage = update.content; + var tag = mountImage; + (addAtIndices || (addAtIndices = [])).push(update.toIndex); + (addChildTags || (addChildTags = [])).push(tag); + } + } + + UIManager.manageChildren( + containerTag, + moveFromIndices, + moveToIndices, + addChildTags, + addAtIndices, + 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 = id; + UIManager.replaceExistingNonRootView(oldTag, mountImage); + } + ), +}; + +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..2c603ec9aa --- /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 ReactElement = require('ReactElement'); +var ReactComponentEnvironment = require('ReactComponentEnvironment'); +var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); +var ReactEmptyComponent = require('ReactEmptyComponent'); +var ReactNativeComponentEnvironment = require('ReactNativeComponentEnvironment'); +var ReactNativeGlobalInteractionHandler = require('ReactNativeGlobalInteractionHandler'); +var ReactNativeGlobalResponderHandler = require('ReactNativeGlobalResponderHandler'); +var ReactNativeTextComponent = require('ReactNativeTextComponent'); +var ReactNativeTreeTraversal = require('ReactNativeTreeTraversal'); +var ReactNativeComponent = require('ReactNativeComponent'); +var ReactNativeComponentTree = require('ReactNativeComponentTree'); +var ReactSimpleEmptyComponent = require('ReactSimpleEmptyComponent'); +var ReactUpdates = require('ReactUpdates'); +var ResponderEventPlugin = require('ResponderEventPlugin'); + +var invariant = require('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); + EventPluginUtils.injection.injectComponentTree(ReactNativeComponentTree); + EventPluginUtils.injection.injectTreeTraversal(ReactNativeTreeTraversal); + + 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 = (instantiate) => { + // Can't import View at the top because it depends on React to make its composite + var View = require('View'); + return new ReactSimpleEmptyComponent( + ReactElement.createElement(View, { + collapsable: true, + style: { position: 'absolute' }, + }), + instantiate + ); + }; + + ReactEmptyComponent.injection.injectEmptyComponentFactory(EmptyComponent); + + 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); + }); +} + +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..1b252c6995 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeEventEmitter.js @@ -0,0 +1,215 @@ +/** + * 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 EventPluginRegistry = require('EventPluginRegistry'); +var ReactEventEmitterMixin = require('ReactEventEmitterMixin'); +var ReactNativeComponentTree = require('ReactNativeComponentTree'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var EventConstants = require('EventConstants'); + +var merge = require('merge'); +var warning = require('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: EventPluginRegistry.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: number, + topLevelType: string, + nativeEventParam: Object + ) { + var nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT; + var inst = ReactNativeComponentTree.getInstanceFromNode(rootNodeID); + ReactNativeEventEmitter.handleTopLevel( + topLevelType, + inst, + 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 = 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 = 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..b84408a0d5 --- /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..84bd45fb6f --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeGlobalResponderHandler.js @@ -0,0 +1,28 @@ +/** + * 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 + */ +'use strict'; + +var UIManager = require('UIManager'); + +var ReactNativeGlobalResponderHandler = { + onChange: function(from, to, blockNativeResponder) { + if (to !== null) { + UIManager.setJSResponder( + to._rootNodeID, + 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..50d300923a --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeMount.js @@ -0,0 +1,238 @@ +/** + * 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 ReactNativeContainerInfo = require('ReactNativeContainerInfo'); +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('emptyObject'); +var instantiateReactComponent = require('instantiateReactComponent'); +var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); + +/** + * 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} containerTag container element to mount into. + * @param {ReactReconcileTransaction} transaction + */ +function mountComponentIntoNode( + componentInstance, + containerTag, + transaction) { + var markup = ReactReconciler.mountComponent( + componentInstance, + transaction, + null, + ReactNativeContainerInfo(containerTag), + emptyObject + ); + componentInstance._renderedComponent._topLevelWrapper = componentInstance; + ReactNativeMount._mountImageIntoNode(markup, containerTag); +} + +/** + * Batched mount. + * + * @param {ReactComponent} componentInstance The instance to mount. + * @param {number} rootID ID of the root node. + * @param {number} containerTag container element to mount into. + */ +function batchedMountComponentIntoNode( + componentInstance, + containerTag) { + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + transaction.perform( + mountComponentIntoNode, + null, + componentInstance, + containerTag, + 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 = { + _instancesByContainerID: {}, + + // these two functions are needed by React Devtools + findNodeHandle: require('findNodeHandle'), + + /** + * @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 = containerTag; + 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 null; + } + + ReactNativeTagHandles.assertRootTag(containerTag); + + var instance = instantiateReactComponent(nextWrappedElement); + ReactNativeMount._instancesByContainerID[containerTag] = instance; + + // 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, + containerTag + ); + 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. + var childTag = mountImage; + UIManager.setChildren( + containerID, + [childTag] + ); + } + ), + + /** + * 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 instance = ReactNativeMount._instancesByContainerID[containerTag]; + if (!instance) { + return false; + } + ReactNativeMount.unmountComponentFromNode(instance, containerTag); + delete ReactNativeMount._instancesByContainerID[containerTag]; + 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); + UIManager.removeSubviewsFromContainerWithID(containerID); + }, + +}; + +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..1c40be74bc --- /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..2a4d7bd010 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeTagHandles.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 ReactNativeTagHandles + * @flow + */ +'use strict'; + +var invariant = require('invariant'); + +/** + * 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 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; + }, + + assertRootTag: function(tag: number): void { + invariant( + this.reactTagIsNativeTopRootID(tag), + 'Expect a native root tag, instead got %s', tag + ); + }, + + reactTagIsNativeTopRootID: function(reactTag: number): bool { + // We reserve all tags that are 1 mod 10 for native root views + return reactTag % 10 === 1; + }, +}; + +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..3de8a82dd2 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeTextComponent.js @@ -0,0 +1,81 @@ +/** + * 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 ReactNativeComponentTree = require('ReactNativeComponentTree'); +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var UIManager = require('UIManager'); + +var invariant = require('invariant'); + +var ReactNativeTextComponent = function(text) { + // This is really a ReactText (ReactNode), not a ReactElement + this._currentElement = text; + this._stringText = '' + text; + this._nativeParent = null; + this._rootNodeID = null; +}; + +Object.assign(ReactNativeTextComponent.prototype, { + + mountComponent: function(transaction, nativeParent, nativeContainerInfo, context) { + // TODO: nativeParent should have this context already. Stop abusing context. + invariant( + context.isInAParentText, + 'RawText "%s" must be wrapped in an explicit component.', + this._stringText + ); + this._nativeParent = nativeParent; + var tag = ReactNativeTagHandles.allocateTag(); + this._rootNodeID = tag; + var nativeTopRootTag = nativeContainerInfo._tag; + UIManager.createView( + tag, + 'RCTRawText', + nativeTopRootTag, + {text: this._stringText} + ); + + ReactNativeComponentTree.precacheNode(this, tag); + + return tag; + }, + + getNativeNode: function() { + return this._rootNodeID; + }, + + receiveComponent: function(nextText, transaction, context) { + if (nextText !== this._currentElement) { + this._currentElement = nextText; + var nextStringText = '' + nextText; + if (nextStringText !== this._stringText) { + this._stringText = nextStringText; + UIManager.updateView( + this._rootNodeID, + 'RCTRawText', + {text: this._stringText} + ); + } + } + }, + + unmountComponent: function() { + ReactNativeComponentTree.uncacheNode(this); + this._currentElement = null; + this._stringText = null; + this._rootNodeID = null; + }, + +}); + +module.exports = ReactNativeTextComponent; diff --git a/src/renderers/native/ReactNative/ReactNativeTreeTraversal.js b/src/renderers/native/ReactNative/ReactNativeTreeTraversal.js new file mode 100644 index 0000000000..58a3a9e479 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeTreeTraversal.js @@ -0,0 +1,126 @@ +/** + * Copyright 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 ReactNativeTreeTraversal + */ + +'use strict'; + +// Same as ReactDOMTreeTraversal without the invariants. + +/** + * Return the lowest common ancestor of A and B, or null if they are in + * different trees. + */ +function getLowestCommonAncestor(instA, instB) { + var depthA = 0; + for (var tempA = instA; tempA; tempA = tempA._nativeParent) { + depthA++; + } + var depthB = 0; + for (var tempB = instB; tempB; tempB = tempB._nativeParent) { + depthB++; + } + + // If A is deeper, crawl up. + while (depthA - depthB > 0) { + instA = instA._nativeParent; + depthA--; + } + + // If B is deeper, crawl up. + while (depthB - depthA > 0) { + instB = instB._nativeParent; + depthB--; + } + + // Walk in lockstep until we find a match. + var depth = depthA; + while (depth--) { + if (instA === instB) { + return instA; + } + instA = instA._nativeParent; + instB = instB._nativeParent; + } + return null; +} + +/** + * Return if A is an ancestor of B. + */ +function isAncestor(instA, instB) { + while (instB) { + if (instB === instA) { + return true; + } + instB = instB._nativeParent; + } + return false; +} + +/** + * Return the parent instance of the passed-in instance. + */ +function getParentInstance(inst) { + return inst._nativeParent; +} + +/** + * Simulates the traversal of a two-phase, capture/bubble event dispatch. + */ +function traverseTwoPhase(inst, fn, arg) { + var path = []; + while (inst) { + path.push(inst); + inst = inst._nativeParent; + } + var i; + for (i = path.length; i-- > 0;) { + fn(path[i], false, arg); + } + for (i = 0; i < path.length; i++) { + fn(path[i], true, arg); + } +} + +/** + * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that + * should would receive a `mouseEnter` or `mouseLeave` event. + * + * Does not invoke the callback on the nearest common ancestor because nothing + * "entered" or "left" that element. + */ +function traverseEnterLeave(from, to, fn, argFrom, argTo) { + var common = from && to ? getLowestCommonAncestor(from, to) : null; + var pathFrom = []; + while (from && from !== common) { + pathFrom.push(from); + from = from._nativeParent; + } + var pathTo = []; + while (to && to !== common) { + pathTo.push(to); + to = to._nativeParent; + } + var i; + for (i = 0; i < pathFrom.length; i++) { + fn(pathFrom[i], true, argFrom); + } + for (i = pathTo.length; i-- > 0;) { + fn(pathTo[i], false, argTo); + } +} + +module.exports = { + isAncestor: isAncestor, + getLowestCommonAncestor: getLowestCommonAncestor, + getParentInstance: getParentInstance, + traverseTwoPhase: traverseTwoPhase, + traverseEnterLeave: traverseEnterLeave, +}; diff --git a/src/renderers/native/ReactNative/__mocks__/InitializeJavaScriptAppEngine.js b/src/renderers/native/ReactNative/__mocks__/InitializeJavaScriptAppEngine.js new file mode 100644 index 0000000000..3f4b4fdd0f --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/InitializeJavaScriptAppEngine.js @@ -0,0 +1,14 @@ +/** + * Copyright 2013-2015, 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'; + +// Noop + +// TODO: Move all initialization callers back into react-native diff --git a/src/renderers/native/ReactNative/__mocks__/InteractionManager.js b/src/renderers/native/ReactNative/__mocks__/InteractionManager.js new file mode 100644 index 0000000000..de72e164a6 --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/InteractionManager.js @@ -0,0 +1,16 @@ +/** + * Copyright 2013-2015, 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'; + +// TODO: Figure out a way to drop this dependency + +var InteractionManager = {}; + +module.exports = InteractionManager; diff --git a/src/renderers/native/ReactNative/__mocks__/JSTimersExecution.js b/src/renderers/native/ReactNative/__mocks__/JSTimersExecution.js new file mode 100644 index 0000000000..3f4b4fdd0f --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/JSTimersExecution.js @@ -0,0 +1,14 @@ +/** + * Copyright 2013-2015, 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'; + +// Noop + +// TODO: Move all initialization callers back into react-native diff --git a/src/renderers/native/ReactNative/__mocks__/Platform.js b/src/renderers/native/ReactNative/__mocks__/Platform.js new file mode 100644 index 0000000000..fc04c24549 --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/Platform.js @@ -0,0 +1,16 @@ +/** + * Copyright 2013-2015, 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'; + +// Mock of the Native Hooks + +var Platform = {}; + +module.exports = Platform; diff --git a/src/renderers/native/ReactNative/__mocks__/RCTEventEmitter.js b/src/renderers/native/ReactNative/__mocks__/RCTEventEmitter.js new file mode 100644 index 0000000000..3f4b4fdd0f --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/RCTEventEmitter.js @@ -0,0 +1,14 @@ +/** + * Copyright 2013-2015, 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'; + +// Noop + +// TODO: Move all initialization callers back into react-native diff --git a/src/renderers/native/ReactNative/__mocks__/RCTLog.js b/src/renderers/native/ReactNative/__mocks__/RCTLog.js new file mode 100644 index 0000000000..3f4b4fdd0f --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/RCTLog.js @@ -0,0 +1,14 @@ +/** + * Copyright 2013-2015, 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'; + +// Noop + +// TODO: Move all initialization callers back into react-native diff --git a/src/renderers/native/ReactNative/__mocks__/TextInputState.js b/src/renderers/native/ReactNative/__mocks__/TextInputState.js new file mode 100644 index 0000000000..a0b4e576d7 --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/TextInputState.js @@ -0,0 +1,17 @@ +/** + * Copyright 2013-2015, 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'; + +// Mock of the Native Hooks +// TODO: Should this move into the components themselves? E.g. focusable + +var TextInputState = {}; + +module.exports = TextInputState; diff --git a/src/renderers/native/ReactNative/__mocks__/UIManager.js b/src/renderers/native/ReactNative/__mocks__/UIManager.js new file mode 100644 index 0000000000..0c51ab6e6b --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/UIManager.js @@ -0,0 +1,21 @@ +/** + * Copyright 2013-2015, 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'; + +// Mock of the Native Hooks + +var RCTUIManager = { + createView: jest.genMockFunction(), + setChildren: jest.genMockFunction(), + manageChildren: jest.genMockFunction(), + updateView: jest.genMockFunction(), +}; + +module.exports = RCTUIManager; diff --git a/src/renderers/native/ReactNative/__mocks__/deepDiffer.js b/src/renderers/native/ReactNative/__mocks__/deepDiffer.js new file mode 100644 index 0000000000..cc58e4b328 --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/deepDiffer.js @@ -0,0 +1,63 @@ +/** + * Copyright 2013-2015, 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'; + +// TODO: Move deepDiffer into react + +var deepDiffer = function(one: any, two: any): bool { + if (one === two) { + // Short circuit on identical object references instead of traversing them. + return false; + } + if ((typeof one === 'function') && (typeof two === 'function')) { + // We consider all functions equal + return false; + } + if ((typeof one !== 'object') || (one === null)) { + // Primitives can be directly compared + return one !== two; + } + if ((typeof two !== 'object') || (two === null)) { + // We know they are different because the previous case would have triggered + // otherwise. + return true; + } + if (one.constructor !== two.constructor) { + return true; + } + if (Array.isArray(one)) { + // We know two is also an array because the constructors are equal + var len = one.length; + if (two.length !== len) { + return true; + } + for (var ii = 0; ii < len; ii++) { + if (deepDiffer(one[ii], two[ii])) { + return true; + } + } + } else { + for (var key in one) { + if (deepDiffer(one[key], two[key])) { + return true; + } + } + for (var twoKey in two) { + // The only case we haven't checked yet is keys that are in two but aren't + // in one, which means they are different. + if (one[twoKey] === undefined && two[twoKey] !== undefined) { + return true; + } + } + } + return false; +}; + +module.exports = deepDiffer; diff --git a/src/renderers/native/ReactNative/__mocks__/deepFreezeAndThrowOnMutationInDev.js b/src/renderers/native/ReactNative/__mocks__/deepFreezeAndThrowOnMutationInDev.js new file mode 100644 index 0000000000..ebb81c3335 --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/deepFreezeAndThrowOnMutationInDev.js @@ -0,0 +1,16 @@ +/** + * Copyright 2013-2015, 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'; + +// TODO: move into react or fbjs + +var deepFreezeAndThrowOnMutationInDev = function() { }; + +module.exports = deepFreezeAndThrowOnMutationInDev; diff --git a/src/renderers/native/ReactNative/__mocks__/flattenStyle.js b/src/renderers/native/ReactNative/__mocks__/flattenStyle.js new file mode 100644 index 0000000000..a766c705fb --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/flattenStyle.js @@ -0,0 +1,16 @@ +/** + * Copyright 2013-2015, 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'; + +// TODO: Move flattenStyle into react + +var flattenStyle = function() { }; + +module.exports = flattenStyle; diff --git a/src/renderers/native/ReactNative/__mocks__/merge.js b/src/renderers/native/ReactNative/__mocks__/merge.js new file mode 100644 index 0000000000..0c2dc8bf5d --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/merge.js @@ -0,0 +1,18 @@ +/** + * Copyright 2013-2015, 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'; + +// TODO: Replace all callers with spread + +var merge = function(a, b) { + return {...a, ...b}; +}; + +module.exports = merge; 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..be65d175a2 --- /dev/null +++ b/src/renderers/native/ReactNative/__tests__/ReactNativeAttributePayload-test.js @@ -0,0 +1,246 @@ +/** + * 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/__tests__/ReactNativeMount-test.js b/src/renderers/native/ReactNative/__tests__/ReactNativeMount-test.js new file mode 100644 index 0000000000..74f4f7f57f --- /dev/null +++ b/src/renderers/native/ReactNative/__tests__/ReactNativeMount-test.js @@ -0,0 +1,61 @@ +/** + * Copyright 2013-2015, 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. + * + * @emails react-core + */ + +'use strict'; + +var React; +var ReactNative; +var createReactNativeComponentClass; +var UIManager; + +describe('ReactNative', function() { + beforeEach(function() { + React = require('React'); + ReactNative = require('ReactNative'); + UIManager = require('UIManager'); + createReactNativeComponentClass = require('createReactNativeComponentClass'); + }); + + it('should be able to create and render a native component', function() { + var View = createReactNativeComponentClass({ + validAttributes: { foo: true }, + uiViewClassName: 'View', + }); + + ReactNative.render(, 1); + expect(UIManager.createView).toBeCalled(); + expect(UIManager.setChildren).toBeCalled(); + expect(UIManager.manageChildren).not.toBeCalled(); + expect(UIManager.updateView).not.toBeCalled(); + }); + + it('should be able to create and update a native component', function() { + var View = createReactNativeComponentClass({ + validAttributes: { foo: true }, + uiViewClassName: 'View', + }); + + ReactNative.render(, 11); + + expect(UIManager.createView.mock.calls.length).toBe(2); + expect(UIManager.setChildren.mock.calls.length).toBe(2); + expect(UIManager.manageChildren).not.toBeCalled(); + expect(UIManager.updateView).not.toBeCalled(); + + ReactNative.render(, 11); + + expect(UIManager.createView.mock.calls.length).toBe(2); + expect(UIManager.setChildren.mock.calls.length).toBe(2); + expect(UIManager.manageChildren).not.toBeCalled(); + expect(UIManager.updateView).toBeCalledWith(3, 'View', { foo: 'bar' }); + }); + +}); diff --git a/src/renderers/native/ReactNative/createReactNativeComponentClass.js b/src/renderers/native/ReactNative/createReactNativeComponentClass.js new file mode 100644 index 0000000000..82d071b087 --- /dev/null +++ b/src/renderers/native/ReactNative/createReactNativeComponentClass.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule 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._topLevelWrapper = null; + this._nativeParent = null; + this._nativeContainerInfo = null; + 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..2979490de7 --- /dev/null +++ b/src/renderers/native/ReactNative/findNodeHandle.js @@ -0,0 +1,111 @@ +/** + * 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 invariant = require('invariant'); +var warning = require('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 internalInstance.getNativeNode(); + } else { + var rootNodeID = component._rootNodeID; + if (rootNodeID) { + return 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/shared/event/EventPluginUtils.js b/src/renderers/shared/event/EventPluginUtils.js index f7e8efe12f..33019d27d6 100644 --- a/src/renderers/shared/event/EventPluginUtils.js +++ b/src/renderers/shared/event/EventPluginUtils.js @@ -204,7 +204,7 @@ function executeDirectDispatch(event) { !Array.isArray(dispatchListener), 'executeDirectDispatch(...): Invalid `event`.' ); - event.currentTarget = EventPluginUtils.getNodeFromInstance(dispatchInstance); + event.currentTarget = dispatchListener ? EventPluginUtils.getNodeFromInstance(dispatchInstance) : null; var res = dispatchListener ? dispatchListener(event) : null; event.currentTarget = null; event._dispatchListeners = null; diff --git a/src/renderers/shared/event/eventPlugins/PanResponder.js b/src/renderers/shared/event/eventPlugins/PanResponder.js new file mode 100644 index 0000000000..a102747fe6 --- /dev/null +++ b/src/renderers/shared/event/eventPlugins/PanResponder.js @@ -0,0 +1,379 @@ +/** + * @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; + if (config.onPanResponderGrant) config.onPanResponderGrant(e, gestureState); + // TODO: t7467124 investigate if this can be removed + return config.onShouldBlockNativeResponder === undefined ? true : + config.onShouldBlockNativeResponder(); + }, + + onResponderReject: function(e) { + if (config.onPanResponderReject) config.onPanResponderReject(e, gestureState); + }, + + onResponderRelease: function(e) { + if (config.onPanResponderRelease) config.onPanResponderRelease(e, gestureState); + PanResponder._initializeGestureState(gestureState); + }, + + onResponderStart: function(e) { + var touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + if (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); + if (config.onPanResponderMove) config.onPanResponderMove(e, gestureState); + }, + + onResponderEnd: function(e) { + var touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + if (config.onPanResponderEnd) config.onPanResponderEnd(e, gestureState); + }, + + onResponderTerminate: function(e) { + if (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/shared/event/eventPlugins/TouchHistoryMath.js b/src/renderers/shared/event/eventPlugins/TouchHistoryMath.js new file mode 100644 index 0000000000..a2a725bd2f --- /dev/null +++ b/src/renderers/shared/event/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; From c81c29ae58eda387dc464dad66d86dd08761074f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Wed, 20 Apr 2016 10:34:44 -0700 Subject: [PATCH 02/26] Merge pull request #6553 from zpao/cleanuppkg Clean up package.json after #6338 (cherry picked from commit bddecc96963f75d0eade5bc303c09c0ebc9c0086) --- packages/react-native-renderer/package.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react-native-renderer/package.json b/packages/react-native-renderer/package.json index a9c487be12..4f43245a69 100644 --- a/packages/react-native-renderer/package.json +++ b/packages/react-native-renderer/package.json @@ -3,10 +3,7 @@ "version": "16.0.0-alpha", "description": "React package for use inside react-native.", "main": "index.js", - "repository": { - "type": "git", - "url": "git+https://github.com/facebook/react.git" - }, + "repository": "facebook/react", "keywords": [ "react", "react-native" @@ -17,7 +14,6 @@ }, "homepage": "https://facebook.github.io/react-native/", "dependencies": { - "fbjs": "^0.8.0", "react": "^16.0.0-alpha" } } From 3e8a2aaddbe45aff03873960dd314821b866e148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Wed, 20 Apr 2016 10:27:06 -0700 Subject: [PATCH 03/26] 15.0.2-alpha.1 --- package.json | 2 +- packages/react-addons/package.json | 4 ++-- packages/react-dom/package.json | 4 ++-- packages/react-native-renderer/package.json | 4 ++-- packages/react/package.json | 2 +- src/ReactVersion.js | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 2ada0a662b..e983139fa9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-build", "private": true, - "version": "15.0.1", + "version": "15.0.2-alpha.1", "devDependencies": { "async": "^1.5.0", "babel-cli": "^6.6.5", diff --git a/packages/react-addons/package.json b/packages/react-addons/package.json index bcb1d4d18e..5cec81f759 100644 --- a/packages/react-addons/package.json +++ b/packages/react-addons/package.json @@ -1,6 +1,6 @@ { "name": "react-addons-template", - "version": "15.0.1", + "version": "15.0.2-alpha.1", "main": "index.js", "repository": "facebook/react", "keywords": [ @@ -10,6 +10,6 @@ "license": "BSD-3-Clause", "dependencies": {}, "peerDependencies": { - "react": "^15.0.1" + "react": "^15.0.2-alpha.1" } } diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index 24ed72a2e9..c2edca1f68 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-dom", - "version": "15.0.1", + "version": "15.0.2-alpha.1", "description": "React package for working with the DOM.", "main": "index.js", "repository": "facebook/react", @@ -14,6 +14,6 @@ "homepage": "https://facebook.github.io/react/", "dependencies": {}, "peerDependencies": { - "react": "^15.0.1" + "react": "^15.0.2-alpha.1" } } diff --git a/packages/react-native-renderer/package.json b/packages/react-native-renderer/package.json index 4f43245a69..a802dbce35 100644 --- a/packages/react-native-renderer/package.json +++ b/packages/react-native-renderer/package.json @@ -1,6 +1,6 @@ { "name": "react-native-renderer", - "version": "16.0.0-alpha", + "version": "15.0.2-alpha.1", "description": "React package for use inside react-native.", "main": "index.js", "repository": "facebook/react", @@ -14,6 +14,6 @@ }, "homepage": "https://facebook.github.io/react-native/", "dependencies": { - "react": "^16.0.0-alpha" + "react": "^15.0.2-alpha.1" } } diff --git a/packages/react/package.json b/packages/react/package.json index 249db88cb2..d317e933a8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "react", "description": "React is a JavaScript library for building user interfaces.", - "version": "15.0.1", + "version": "15.0.2-alpha.1", "keywords": [ "react" ], diff --git a/src/ReactVersion.js b/src/ReactVersion.js index 9c9a092153..04f113ca5f 100644 --- a/src/ReactVersion.js +++ b/src/ReactVersion.js @@ -11,4 +11,4 @@ 'use strict'; -module.exports = '15.0.1'; +module.exports = '15.0.2-alpha.1'; From fd8aa077e32dd163ac826d28bad38fdd2a88078a Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Thu, 21 Apr 2016 15:14:56 -0700 Subject: [PATCH 04/26] Merge pull request #6572 from spicyj/rn-batch Batch event handling in React Native --- .../native/ReactNative/ReactNativeEventEmitter.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/renderers/native/ReactNative/ReactNativeEventEmitter.js b/src/renderers/native/ReactNative/ReactNativeEventEmitter.js index 1b252c6995..add29ce39d 100644 --- a/src/renderers/native/ReactNative/ReactNativeEventEmitter.js +++ b/src/renderers/native/ReactNative/ReactNativeEventEmitter.js @@ -16,6 +16,7 @@ var EventPluginRegistry = require('EventPluginRegistry'); var ReactEventEmitterMixin = require('ReactEventEmitterMixin'); var ReactNativeComponentTree = require('ReactNativeComponentTree'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var ReactUpdates = require('ReactUpdates'); var EventConstants = require('EventConstants'); var merge = require('merge'); @@ -119,12 +120,14 @@ var ReactNativeEventEmitter = merge(ReactEventEmitterMixin, { ) { var nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT; var inst = ReactNativeComponentTree.getInstanceFromNode(rootNodeID); - ReactNativeEventEmitter.handleTopLevel( - topLevelType, - inst, - nativeEvent, - nativeEvent.target - ); + ReactUpdates.batchedUpdates(function() { + ReactNativeEventEmitter.handleTopLevel( + topLevelType, + inst, + nativeEvent, + nativeEvent.target + ); + }); }, /** From 1fb7d641714568a12ba28650dd23a23bd3f101a8 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Thu, 21 Apr 2016 15:18:27 -0700 Subject: [PATCH 05/26] 15.0.2-alpha.2 --- package.json | 2 +- packages/react-addons/package.json | 4 ++-- packages/react-dom/package.json | 4 ++-- packages/react-native-renderer/package.json | 4 ++-- packages/react/package.json | 2 +- src/ReactVersion.js | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index e983139fa9..fe25c4a7fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-build", "private": true, - "version": "15.0.2-alpha.1", + "version": "15.0.2-alpha.2", "devDependencies": { "async": "^1.5.0", "babel-cli": "^6.6.5", diff --git a/packages/react-addons/package.json b/packages/react-addons/package.json index 5cec81f759..871dfe853e 100644 --- a/packages/react-addons/package.json +++ b/packages/react-addons/package.json @@ -1,6 +1,6 @@ { "name": "react-addons-template", - "version": "15.0.2-alpha.1", + "version": "15.0.2-alpha.2", "main": "index.js", "repository": "facebook/react", "keywords": [ @@ -10,6 +10,6 @@ "license": "BSD-3-Clause", "dependencies": {}, "peerDependencies": { - "react": "^15.0.2-alpha.1" + "react": "^15.0.2-alpha.2" } } diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index c2edca1f68..a92dc6bb2d 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-dom", - "version": "15.0.2-alpha.1", + "version": "15.0.2-alpha.2", "description": "React package for working with the DOM.", "main": "index.js", "repository": "facebook/react", @@ -14,6 +14,6 @@ "homepage": "https://facebook.github.io/react/", "dependencies": {}, "peerDependencies": { - "react": "^15.0.2-alpha.1" + "react": "^15.0.2-alpha.2" } } diff --git a/packages/react-native-renderer/package.json b/packages/react-native-renderer/package.json index a802dbce35..1caed5b0ff 100644 --- a/packages/react-native-renderer/package.json +++ b/packages/react-native-renderer/package.json @@ -1,6 +1,6 @@ { "name": "react-native-renderer", - "version": "15.0.2-alpha.1", + "version": "15.0.2-alpha.2", "description": "React package for use inside react-native.", "main": "index.js", "repository": "facebook/react", @@ -14,6 +14,6 @@ }, "homepage": "https://facebook.github.io/react-native/", "dependencies": { - "react": "^15.0.2-alpha.1" + "react": "^15.0.2-alpha.2" } } diff --git a/packages/react/package.json b/packages/react/package.json index d317e933a8..eb5ba1c103 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "react", "description": "React is a JavaScript library for building user interfaces.", - "version": "15.0.2-alpha.1", + "version": "15.0.2-alpha.2", "keywords": [ "react" ], diff --git a/src/ReactVersion.js b/src/ReactVersion.js index 04f113ca5f..b8f3bf8554 100644 --- a/src/ReactVersion.js +++ b/src/ReactVersion.js @@ -11,4 +11,4 @@ 'use strict'; -module.exports = '15.0.2-alpha.1'; +module.exports = '15.0.2-alpha.2'; From 3b2f29ef694713e740ce7b73fedb185947d9695e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 22 Apr 2016 16:54:59 -0700 Subject: [PATCH 06/26] Merge pull request #6584 from sahrens/patch-1 [RN] Don't hold interaction handles for all touches (cherry picked from commit aacb5467a0cb8be33fe3ca3241ae6c3dcd927fd2) --- .../native/ReactNative/ReactNativeDefaultInjection.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js b/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js index 2c603ec9aa..c0c5fd2073 100644 --- a/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js +++ b/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js @@ -27,7 +27,6 @@ var ReactComponentEnvironment = require('ReactComponentEnvironment'); var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); var ReactEmptyComponent = require('ReactEmptyComponent'); var ReactNativeComponentEnvironment = require('ReactNativeComponentEnvironment'); -var ReactNativeGlobalInteractionHandler = require('ReactNativeGlobalInteractionHandler'); var ReactNativeGlobalResponderHandler = require('ReactNativeGlobalResponderHandler'); var ReactNativeTextComponent = require('ReactNativeTextComponent'); var ReactNativeTreeTraversal = require('ReactNativeTreeTraversal'); @@ -56,10 +55,6 @@ function inject() { ReactNativeGlobalResponderHandler ); - ResponderEventPlugin.injection.injectGlobalInteractionHandler( - ReactNativeGlobalInteractionHandler - ); - /** * Some important event plugins included by default (without having to require * them). From c05ae26ec8f7903c455701447032809454d0cac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 22 Apr 2016 16:44:26 -0700 Subject: [PATCH 07/26] Merge pull request #6587 from sahrens/patch-2 Move PanResponder back to react-native repo temporarily. (cherry picked from commit ea504521d589f4395038e11e5a2a934f058581b3) --- .../shared/event/eventPlugins/PanResponder.js | 379 ------------------ 1 file changed, 379 deletions(-) delete mode 100644 src/renderers/shared/event/eventPlugins/PanResponder.js diff --git a/src/renderers/shared/event/eventPlugins/PanResponder.js b/src/renderers/shared/event/eventPlugins/PanResponder.js deleted file mode 100644 index a102747fe6..0000000000 --- a/src/renderers/shared/event/eventPlugins/PanResponder.js +++ /dev/null @@ -1,379 +0,0 @@ -/** - * @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; - if (config.onPanResponderGrant) config.onPanResponderGrant(e, gestureState); - // TODO: t7467124 investigate if this can be removed - return config.onShouldBlockNativeResponder === undefined ? true : - config.onShouldBlockNativeResponder(); - }, - - onResponderReject: function(e) { - if (config.onPanResponderReject) config.onPanResponderReject(e, gestureState); - }, - - onResponderRelease: function(e) { - if (config.onPanResponderRelease) config.onPanResponderRelease(e, gestureState); - PanResponder._initializeGestureState(gestureState); - }, - - onResponderStart: function(e) { - var touchHistory = e.touchHistory; - gestureState.numberActiveTouches = touchHistory.numberActiveTouches; - if (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); - if (config.onPanResponderMove) config.onPanResponderMove(e, gestureState); - }, - - onResponderEnd: function(e) { - var touchHistory = e.touchHistory; - gestureState.numberActiveTouches = touchHistory.numberActiveTouches; - if (config.onPanResponderEnd) config.onPanResponderEnd(e, gestureState); - }, - - onResponderTerminate: function(e) { - if (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; From ae6dfa9dae598e7eb1f161136c505282f160e3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 22 Apr 2016 16:55:08 -0700 Subject: [PATCH 08/26] Merge pull request #6588 from sahrens/patch-3 Delete ReactNativeGlobalInteractionHandler.js (cherry picked from commit 2f2ed71bd005853a03c9fd7cf459b9946676ec35) --- .../ReactNativeGlobalInteractionHandler.js | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 src/renderers/native/ReactNative/ReactNativeGlobalInteractionHandler.js diff --git a/src/renderers/native/ReactNative/ReactNativeGlobalInteractionHandler.js b/src/renderers/native/ReactNative/ReactNativeGlobalInteractionHandler.js deleted file mode 100644 index b84408a0d5..0000000000 --- a/src/renderers/native/ReactNative/ReactNativeGlobalInteractionHandler.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @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; From 33a9603e93b4a93f083e7ab43fb921c86496ad1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 23 Apr 2016 13:10:00 -0700 Subject: [PATCH 09/26] Ignore events fired on missing React Native instances This can happen if something gets unmounted before the event gets dispatched. I'm not sure how this works out exactly but this preserves previous behavior in this scenario. (cherry picked from commit 69b6869a3e6861b325ca2e9bb2d938cb81c7a090) --- src/renderers/native/ReactNative/ReactNativeEventEmitter.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderers/native/ReactNative/ReactNativeEventEmitter.js b/src/renderers/native/ReactNative/ReactNativeEventEmitter.js index add29ce39d..bf7179c2ae 100644 --- a/src/renderers/native/ReactNative/ReactNativeEventEmitter.js +++ b/src/renderers/native/ReactNative/ReactNativeEventEmitter.js @@ -120,6 +120,11 @@ var ReactNativeEventEmitter = merge(ReactEventEmitterMixin, { ) { var nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT; var inst = ReactNativeComponentTree.getInstanceFromNode(rootNodeID); + if (!inst) { + // If the original instance is already gone, we don't have to dispatch + // any events. + return; + } ReactUpdates.batchedUpdates(function() { ReactNativeEventEmitter.handleTopLevel( topLevelType, From 112a400662db6a992875c4220714acdf4db814ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Mon, 25 Apr 2016 12:54:56 -0700 Subject: [PATCH 10/26] 15.0.2-alpha.3 --- package.json | 2 +- packages/react-addons/package.json | 4 ++-- packages/react-dom/package.json | 4 ++-- packages/react-native-renderer/package.json | 4 ++-- packages/react/package.json | 2 +- src/ReactVersion.js | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index fe25c4a7fc..5c8357db11 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-build", "private": true, - "version": "15.0.2-alpha.2", + "version": "15.0.2-alpha.3", "devDependencies": { "async": "^1.5.0", "babel-cli": "^6.6.5", diff --git a/packages/react-addons/package.json b/packages/react-addons/package.json index 871dfe853e..95245a9763 100644 --- a/packages/react-addons/package.json +++ b/packages/react-addons/package.json @@ -1,6 +1,6 @@ { "name": "react-addons-template", - "version": "15.0.2-alpha.2", + "version": "15.0.2-alpha.3", "main": "index.js", "repository": "facebook/react", "keywords": [ @@ -10,6 +10,6 @@ "license": "BSD-3-Clause", "dependencies": {}, "peerDependencies": { - "react": "^15.0.2-alpha.2" + "react": "^15.0.2-alpha.3" } } diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index a92dc6bb2d..e8274674a3 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-dom", - "version": "15.0.2-alpha.2", + "version": "15.0.2-alpha.3", "description": "React package for working with the DOM.", "main": "index.js", "repository": "facebook/react", @@ -14,6 +14,6 @@ "homepage": "https://facebook.github.io/react/", "dependencies": {}, "peerDependencies": { - "react": "^15.0.2-alpha.2" + "react": "^15.0.2-alpha.3" } } diff --git a/packages/react-native-renderer/package.json b/packages/react-native-renderer/package.json index 1caed5b0ff..2652dfebe4 100644 --- a/packages/react-native-renderer/package.json +++ b/packages/react-native-renderer/package.json @@ -1,6 +1,6 @@ { "name": "react-native-renderer", - "version": "15.0.2-alpha.2", + "version": "15.0.2-alpha.3", "description": "React package for use inside react-native.", "main": "index.js", "repository": "facebook/react", @@ -14,6 +14,6 @@ }, "homepage": "https://facebook.github.io/react-native/", "dependencies": { - "react": "^15.0.2-alpha.2" + "react": "^15.0.2-alpha.3" } } diff --git a/packages/react/package.json b/packages/react/package.json index eb5ba1c103..792412fe48 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "react", "description": "React is a JavaScript library for building user interfaces.", - "version": "15.0.2-alpha.2", + "version": "15.0.2-alpha.3", "keywords": [ "react" ], diff --git a/src/ReactVersion.js b/src/ReactVersion.js index b8f3bf8554..c2aae1588e 100644 --- a/src/ReactVersion.js +++ b/src/ReactVersion.js @@ -11,4 +11,4 @@ 'use strict'; -module.exports = '15.0.2-alpha.2'; +module.exports = '15.0.2-alpha.3'; From 3655e30adbae73cd803b3daae32e03059b6f0149 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 14 Apr 2016 19:40:21 +0100 Subject: [PATCH 11/26] Merge pull request #6215 from nhunzaker/nh-fix-disabled-inputs Disabled inputs should not respond to clicks in IE (cherry picked from commit 36e4fe54a801a325d30359653e588e9705e7532b) --- .../dom/client/wrappers/DisabledInputUtils.js | 50 ++++++++ .../dom/client/wrappers/ReactDOMButton.js | 30 +---- .../dom/client/wrappers/ReactDOMInput.js | 3 +- .../dom/client/wrappers/ReactDOMSelect.js | 3 +- .../dom/client/wrappers/ReactDOMTextarea.js | 3 +- .../__tests__/DisabledInputUtil-test.js | 108 ++++++++++++++++++ .../wrappers/__tests__/ReactDOMButton-test.js | 93 --------------- 7 files changed, 166 insertions(+), 124 deletions(-) create mode 100644 src/renderers/dom/client/wrappers/DisabledInputUtils.js create mode 100644 src/renderers/dom/client/wrappers/__tests__/DisabledInputUtil-test.js delete mode 100644 src/renderers/dom/client/wrappers/__tests__/ReactDOMButton-test.js diff --git a/src/renderers/dom/client/wrappers/DisabledInputUtils.js b/src/renderers/dom/client/wrappers/DisabledInputUtils.js new file mode 100644 index 0000000000..37f8028351 --- /dev/null +++ b/src/renderers/dom/client/wrappers/DisabledInputUtils.js @@ -0,0 +1,50 @@ +/** + * Copyright 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. + * + * @providesModule DisabledInputUtils + */ + +'use strict'; + +var disableableMouseListenerNames = { + onClick: true, + onDoubleClick: true, + onMouseDown: true, + onMouseMove: true, + onMouseUp: true, + + onClickCapture: true, + onDoubleClickCapture: true, + onMouseDownCapture: true, + onMouseMoveCapture: true, + onMouseUpCapture: true, +}; + +/** + * Implements a native component that does not receive mouse events + * when `disabled` is set. + */ +var DisabledInputUtils = { + getNativeProps: function(inst, props) { + if (!props.disabled) { + return props; + } + + // Copy the props, except the mouse listeners + var nativeProps = {}; + for (var key in props) { + if (!disableableMouseListenerNames[key] && props.hasOwnProperty(key)) { + nativeProps[key] = props[key]; + } + } + + return nativeProps; + }, +}; + +module.exports = DisabledInputUtils; diff --git a/src/renderers/dom/client/wrappers/ReactDOMButton.js b/src/renderers/dom/client/wrappers/ReactDOMButton.js index bf32c45600..961a1ebebc 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMButton.js +++ b/src/renderers/dom/client/wrappers/ReactDOMButton.js @@ -11,40 +11,14 @@ 'use strict'; -var mouseListenerNames = { - onClick: true, - onDoubleClick: true, - onMouseDown: true, - onMouseMove: true, - onMouseUp: true, - - onClickCapture: true, - onDoubleClickCapture: true, - onMouseDownCapture: true, - onMouseMoveCapture: true, - onMouseUpCapture: true, -}; +var DisabledInputUtils = require('DisabledInputUtils'); /** * Implements a