Merge pull request #6338 from sebmarkbage/reactnative2

Move React Core Integration and Injection to the Core Repo
This commit is contained in:
Sebastian Markbåge 2016-04-20 03:41:20 +01:00
commit c84ad52ddb
50 changed files with 3749 additions and 22 deletions

View File

@ -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",

View File

@ -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" \

View File

@ -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',

View File

@ -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,
};

View File

@ -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),

View File

@ -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 }],
],
};

View File

@ -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",

View File

@ -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.

View File

@ -0,0 +1,3 @@
'use strict';
module.exports = require('react/lib/ReactNative');

View File

@ -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"
}
}

View File

@ -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,
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<NestedNode> | 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<NestedNode>,
nextArray: Array<NestedNode>,
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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<string>} 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;

View File

@ -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,
};

View File

@ -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<Touch>} touches Deserialized touch objects.
* @param {Array<number>} indices Indices by which to pull subsequence.
* @return {Array<Touch>} 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<Touch>} touches Deserialized touch objects.
* @param {Array<number>} indices Indices to remove from `touches`.
* @return {Array<Touch>} Subsequence of removed touch objects.
*/
var removeTouchesAtIndices = function(
touches: Array<Object>,
indices: Array<number>
): Array<Object> {
var rippedOut = [];
// use an unsafe downcast to alias to nullable elements,
// so we can delete and then compact.
var temp: Array<?Object> = (touches: Array<any>);
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<Object>,
changedIndices: Array<number>
) {
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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<object>} List of operation wrap procedures.
* TODO: convert to array<TransactionWrapper>
*/
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;

View File

@ -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;

View File

@ -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 <Text> 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;

View File

@ -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,
};

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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});
});
});

View File

@ -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(<View foo="test" />, 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(<View foo="foo" />, 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(<View foo="bar" />, 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' });
});
});

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule 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<any> {
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;

View File

@ -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;

View File

@ -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;

View File

@ -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 (
* <View {...this._panResponder.panHandlers} />
* );
* },
*
* ```
*
* ### 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;

View File

@ -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;