From 007b75f78a1818a020523acb540c381e4d588f60 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:15:08 -0700 Subject: [PATCH 01/25] Flatten Children A Single Level This expects static children as additional arguments to the constructor and flattens any array arguments one level deep. Component(props, child1, child2, arrayOfChildren, child3) -> .props.children = [child1, child2, ...arrayOfChildren, child3] This can avoid an additional heap allocation for the unflat array. It allows you to pass nested arrays and objects like you used to. Those aren't immediately flattened. That makes this a fairly safe change. Passing a dynamic array without key properties will yield a warning (once). Might consider throwing later. Once we change the transpiler to use the new syntax, you'll end up with a single flat array in normal usage. This doesn't actually update the JSX transform. --- src/core/ReactComponent.js | 105 +++++++++++++++++- src/core/ReactCompositeComponent.js | 11 +- src/core/ReactDOM.js | 8 +- .../ReactMultiChildReconcile-test.js | 7 +- src/utils/flattenChildren.js | 23 +++- 5 files changed, 135 insertions(+), 19 deletions(-) diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index 4f4cffd07865b..5c42a8e86fba9 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -50,6 +50,60 @@ var ComponentLifeCycle = keyMirror({ UNMOUNTED: null }); +/** + * Warn if there's no key explicitly set on dynamic arrays of children. + * This allows us to keep track of children between updates. + */ + +var CHILD_HAS_NO_IDENTITY = + 'You are passing a dynamic array of children. You should set the ' + + 'property "key" to a string that uniquely identifies each child.'; + +var HAS_WARNED = false; + +/** + * Helpers for flattening child arguments onto a new array or use an existing + * one. + */ + +function isEmptyChild(child) { + return child == null || typeof child === 'boolean'; +} + +function assignKey(setKey, child, index) { + if (ReactComponent.isValidComponent(child)) { + var key = child._key || child.props.key; + if (__DEV__) { + if (!HAS_WARNED && !key) { + HAS_WARNED = true; + console && console.warn && console.warn(CHILD_HAS_NO_IDENTITY); + } + } + child._key = (setKey ? setKey + ':' : '') + (key || ('' + index)); + } +} + +function tryToReuseArray(children) { + for (var i = 0; i < children.length; i++) { + var child = children[i]; + if (isEmptyChild(child)) return false; + assignKey('', child, i); + } + return true; +} + +function appendNestedChildren(parentKey, sourceArray, targetArray) { + for (var i = 0; i < sourceArray.length; i++) { + var child = sourceArray[i]; + if (isEmptyChild(child)) continue; + assignKey(parentKey, child, i); + // TODO: Invalid components like strings could possibly need + // keys assigned to them here. Usually they're not stateful but + // CSS transitions and special events could make them stateful. + targetArray.push(child); + } +} + /** * Components are the basic units of composition in React. * @@ -206,13 +260,58 @@ var ReactComponent = { */ construct: function(initialProps, children) { this.props = initialProps || {}; - if (typeof children !== 'undefined') { - this.props.children = children; - } // Record the component responsible for creating this component. this.props[OWNER] = ReactCurrentOwner.current; // All components start unmounted. this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; + + // Children can be either an array or more than one argument + if (arguments.length < 2) { + return; + } + + if (arguments.length === 2) { + + // A single string or number child is treated as content, not an array. + var type = typeof children; + if (children == null || type === 'string' || type === 'number') { + this.props.children = children; + return; + } + + // A single array can be reused if it's already flat + if (Array.isArray(children) && tryToReuseArray(children)) { + this.props.children = children; + return; + } + + } + + // Subsequent arguments are rolled into one child array. Array arguments + // are flattened onto it. This is inlined to avoid extra heap allocation. + var targetArray = null; + for (var i = 1; i < arguments.length; i++) { + var child = arguments[i]; + if (Array.isArray(child)) { + if (child.length === 0) continue; + + if (targetArray === null) targetArray = []; + appendNestedChildren(i - 1, child, targetArray); + + } else if (!isEmptyChild(child)) { + + if (ReactComponent.isValidComponent(child) && !child._key) { + // This is a static node and therefore safe to key by index. + // No warning necessary. + child._key = child.props.key || ('' + (i - 1)); + } + + if (targetArray === null) targetArray = []; + targetArray.push(child); + + } + } + this.props.children = targetArray; }, /** diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index a16e4590c8119..562dadf675fe9 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -396,7 +396,8 @@ var ReactCompositeComponentMixin = { * @internal */ construct: function(initialProps, children) { - ReactComponent.Mixin.construct.call(this, initialProps, children); + // Children can be either an array or more than one argument + ReactComponent.Mixin.construct.apply(this, arguments); this.state = null; this._pendingState = null; this._compositeLifeCycleState = null; @@ -784,9 +785,7 @@ var ReactCompositeComponent = { * @public */ createClass: function(spec) { - var Constructor = function(initialProps, children) { - this.construct(initialProps, children); - }; + var Constructor = function() {}; Constructor.prototype = new ReactCompositeComponentBase(); Constructor.prototype.constructor = Constructor; mixSpecIntoComponent(Constructor, spec); @@ -796,7 +795,9 @@ var ReactCompositeComponent = { ); var ConvenienceConstructor = function(props, children) { - return new Constructor(props, children); + var instance = new Constructor(); + instance.construct.apply(instance, arguments); + return instance; }; ConvenienceConstructor.componentConstructor = Constructor; ConvenienceConstructor.originalSpec = spec; diff --git a/src/core/ReactDOM.js b/src/core/ReactDOM.js index d04fe2e0a6d47..2fe78e99a7e8d 100644 --- a/src/core/ReactDOM.js +++ b/src/core/ReactDOM.js @@ -40,15 +40,15 @@ var objMapKeyVal = require('objMapKeyVal'); * @private */ function createDOMComponentClass(tag, omitClose) { - var Constructor = function(initialProps, children) { - this.construct(initialProps, children); - }; + var Constructor = function() {}; Constructor.prototype = new ReactNativeComponent(tag, omitClose); Constructor.prototype.constructor = Constructor; return function(props, children) { - return new Constructor(props, children); + var instance = new Constructor(); + instance.construct.apply(instance, arguments); + return instance; }; } diff --git a/src/core/__tests__/ReactMultiChildReconcile-test.js b/src/core/__tests__/ReactMultiChildReconcile-test.js index 59a9a8b5b4514..bead6ae660f14 100644 --- a/src/core/__tests__/ReactMultiChildReconcile-test.js +++ b/src/core/__tests__/ReactMultiChildReconcile-test.js @@ -41,12 +41,11 @@ var stripEmptyValues = function(obj) { }; /** - * Children are currently named '{}' so we can retrieve the - * originally provided key by stripping out the braces. This relies on a tiny - * implementation detail of the rendering system. + * These children are wrapped in an array and therefore their keys are prefixed. + * This relies on a tiny implementation detail of the rendering system. */ var getOriginalKey = function(childName) { - return childName.substr(1, childName.length - 2); + return childName.substr(3); }; /** diff --git a/src/utils/flattenChildren.js b/src/utils/flattenChildren.js index 06c2e9e946769..f7a4bd3372728 100644 --- a/src/utils/flattenChildren.js +++ b/src/utils/flattenChildren.js @@ -37,15 +37,28 @@ if (__DEV__) { 'as a child of a React component.'; } +var DUPLICATE_KEY_ERROR = + 'You have two children with identical keys. Make sure that you set the ' + + '"key" property to a unique value such as a row ID.'; + /** * If there is only a single child, it still needs a name. */ var ONLY_CHILD_NAME = '0'; var flattenChildrenImpl = function(res, children, nameSoFar) { + var key, escapedKey; if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { - flattenChildrenImpl(res, children[i], nameSoFar + '[' + i + ']'); + var child = children[i]; + key = child && child.mountInContainerNode && + (child._key || child.props.key); + escapedKey = key ? escapeTextForBrowser(key) : ('' + i); + flattenChildrenImpl( + res, + child, + nameSoFar + ':' + escapedKey + ); } } else { var type = typeof children; @@ -55,16 +68,20 @@ var flattenChildrenImpl = function(res, children, nameSoFar) { res[storageName] = null; } else if (children.mountComponentIntoNode) { /* We found a component instance */ + if (__DEV__) { + throwIf(res.hasOwnProperty(storageName), DUPLICATE_KEY_ERROR); + } res[storageName] = children; } else { if (type === 'object') { throwIf(children && children.nodeType === 1, INVALID_CHILD); - for (var key in children) { + for (key in children) { if (children.hasOwnProperty(key)) { + escapedKey = escapeTextForBrowser(key); flattenChildrenImpl( res, children[key], - nameSoFar + '{' + escapeTextForBrowser(key) + '}' + nameSoFar + ':' + escapedKey ); } } From 93fc188afb016b44f0b4e464621bfca721979cc5 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:15:13 -0700 Subject: [PATCH 02/25] style prop improvements Some improvements to how style={{x:y}} is handled in React: * ignores null styles, rather than setting them. Codez: var highlighted = false;
Before:
After:
Respects that 0 has no units. --- src/domUtils/CSSProperty.js | 7 ++-- src/domUtils/CSSPropertyOperations.js | 6 +-- .../__tests__/CSSPropertyOperations-test.js | 41 ++++++++++++++++++- src/domUtils/dangerousStyleValue.js | 16 +++++--- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/domUtils/CSSProperty.js b/src/domUtils/CSSProperty.js index 389fccf3e7159..95586d5a7c1c2 100644 --- a/src/domUtils/CSSProperty.js +++ b/src/domUtils/CSSProperty.js @@ -19,20 +19,19 @@ "use strict"; /** - * CSS properties for which we do not append "px". + * CSS properties which accept numbers but are not in units of "px". */ -var isNumber = { +var isUnitlessNumber = { fillOpacity: true, fontWeight: true, opacity: true, orphans: true, - textDecoration: true, zIndex: true, zoom: true }; var CSSProperty = { - isNumber: isNumber + isUnitlessNumber: isUnitlessNumber }; module.exports = CSSProperty; diff --git a/src/domUtils/CSSPropertyOperations.js b/src/domUtils/CSSPropertyOperations.js index 76939a2b00a92..4c67a4cbe8844 100644 --- a/src/domUtils/CSSPropertyOperations.js +++ b/src/domUtils/CSSPropertyOperations.js @@ -42,7 +42,7 @@ var CSSPropertyOperations = { * Undefined values are ignored so that declarative programming is easier. * * @param {object} styles - * @return {string} + * @return {?string} */ createMarkupForStyles: function(styles) { var serialized = ''; @@ -51,12 +51,12 @@ var CSSPropertyOperations = { continue; } var styleValue = styles[styleName]; - if (typeof styleValue !== 'undefined') { + if (styleValue != null) { serialized += processStyleName(styleName) + ':'; serialized += dangerousStyleValue(styleName, styleValue) + ';'; } } - return serialized; + return serialized || null; }, /** diff --git a/src/domUtils/__tests__/CSSPropertyOperations-test.js b/src/domUtils/__tests__/CSSPropertyOperations-test.js index 8978fa1f2c04f..3918d801affe7 100644 --- a/src/domUtils/__tests__/CSSPropertyOperations-test.js +++ b/src/domUtils/__tests__/CSSPropertyOperations-test.js @@ -19,6 +19,8 @@ "use strict"; +var React = require('React'); + describe('CSSPropertyOperations', function() { var CSSPropertyOperations; @@ -41,12 +43,49 @@ describe('CSSPropertyOperations', function() { })).toBe('display:none;'); }); + it('should ignore null styles', function() { + expect(CSSPropertyOperations.createMarkupForStyles({ + backgroundColor: null, + display: 'none' + })).toBe('display:none;'); + }); + + it('should return null for no styles', function() { + expect(CSSPropertyOperations.createMarkupForStyles({ + backgroundColor: null, + display: null + })).toBe(null); + }); + it('should automatically append `px` to relevant styles', function() { expect(CSSPropertyOperations.createMarkupForStyles({ + left: 0, margin: 16, opacity: 0.5, padding: '4px' - })).toBe('margin:16px;opacity:0.5;padding:4px;'); + })).toBe('left:0;margin:16px;opacity:0.5;padding:4px;'); + }); + + it('should set style attribute when styles exist', function() { + var styles = { + backgroundColor: '#000', + display: 'none' + }; + var div =
; + var root = document.createElement('div'); + React.renderComponent(div, root); + expect(/style=".*"/.test(root.innerHTML)).toBe(true); + }); + + it('should not set style attribute when no styles exist', function() { + var styles = { + backgroundColor: null, + display: null + }; + var div =
; + var root = document.createElement('div'); + React.renderComponent(div, root); + expect(/style=".*"/.test(root.innerHTML)).toBe(false); }); }); diff --git a/src/domUtils/dangerousStyleValue.js b/src/domUtils/dangerousStyleValue.js index aef5c1d5556a3..9c12b12ab3ab9 100644 --- a/src/domUtils/dangerousStyleValue.js +++ b/src/domUtils/dangerousStyleValue.js @@ -23,7 +23,8 @@ var CSSProperty = require('CSSProperty'); /** * Convert a value into the proper css writable value. The `styleName` name - * name should be logical (no hyphens), as specified in `CSSProperty.isNumber`. + * name should be logical (no hyphens), as specified + * in `CSSProperty.isUnitlessNumber`. * * @param {string} styleName CSS property name such as `topMargin`. * @param {*} value CSS property value such as `10px`. @@ -39,13 +40,18 @@ function dangerousStyleValue(styleName, value) { // This is not an XSS hole but instead a potential CSS injection issue // which has lead to a greater discussion about how we're going to // trust URLs moving forward. See #2115901 - if (value === null || value === false || value === true || value === '') { + + var isEmpty = value == null || typeof value === 'boolean' || value === ''; + if (isEmpty) { return ''; } - if (isNaN(value)) { - return !value ? '' : '' + value; + + var isNonNumeric = isNaN(value); + if (isNonNumeric || value === 0 || CSSProperty.isUnitlessNumber[styleName]) { + return '' + value; // cast to string } - return CSSProperty.isNumber[styleName] ? '' + value : (value + 'px'); + + return value + 'px'; } module.exports = dangerousStyleValue; From 4b81de93d3733ecc5d4dcaab7efad2a5eef1937d Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:15:22 -0700 Subject: [PATCH 03/25] use key="foo" for all components flattenChildren was only using key when child.mountInContainerNode exists, which is defined on ReactCompositeComponent, and not ReactNativeComponent. This uses the isValidComponent() fn to see if we should use this key. --- src/core/__tests__/ReactIdentity-test.js | 83 ++++++++++++++++++++++++ src/utils/flattenChildren.js | 3 +- 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 src/core/__tests__/ReactIdentity-test.js diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js new file mode 100644 index 0000000000000..ecf71a8553143 --- /dev/null +++ b/src/core/__tests__/ReactIdentity-test.js @@ -0,0 +1,83 @@ +/** + * @jsx React.DOM + * @emails react-core + */ + +"use strict"; + +var React; +var ReactTestUtils; +var reactComponentExpect; + +describe('ReactIdentity', function() { + + beforeEach(function() { + require('mock-modules').autoMockOff().dumpCache(); + React = require('React'); + ReactTestUtils = require('ReactTestUtils'); + reactComponentExpect = require('reactComponentExpect'); + }); + + it('should allow keyed objects to express identity', function() { + var instance = +
+ {{ + first:
, + second:
+ }} +
; + + React.renderComponent(instance, document.createElement('div')); + var node = instance.getDOMNode(); + reactComponentExpect(instance).toBeDOMComponentWithChildCount(2); + expect(node.childNodes[0].id).toEqual('.reactRoot[0].:0:first'); + expect(node.childNodes[1].id).toEqual('.reactRoot[0].:0:second'); + }); + + it('should allow key property to express identity', function() { + var instance = +
+
+
+
; + + React.renderComponent(instance, document.createElement('div')); + var node = instance.getDOMNode(); + reactComponentExpect(instance).toBeDOMComponentWithChildCount(2); + expect(node.childNodes[0].id).toEqual('.reactRoot[0].:apple'); + expect(node.childNodes[1].id).toEqual('.reactRoot[0].:banana'); + }); + + it('should use instance identity', function() { + + var Wrapper = React.createClass({ + render: function() { + return {this.props.children}; + } + }); + + var instance = +
+ + + +
; + + React.renderComponent(instance, document.createElement('div')); + var node = instance.getDOMNode(); + reactComponentExpect(instance).toBeDOMComponentWithChildCount(3); + expect(node.childNodes[0].id) + .toEqual('.reactRoot[0].:wrap1'); + expect(node.childNodes[0].firstChild.id) + .toEqual('.reactRoot[0].:wrap1.:squirrel'); + expect(node.childNodes[1].id) + .toEqual('.reactRoot[0].:wrap2'); + expect(node.childNodes[1].firstChild.id) + .toEqual('.reactRoot[0].:wrap2.:bunny'); + expect(node.childNodes[2].id) + .toEqual('.reactRoot[0].:2'); + expect(node.childNodes[2].firstChild.id) + .toEqual('.reactRoot[0].:2.:chipmunk'); + }); + +}); diff --git a/src/utils/flattenChildren.js b/src/utils/flattenChildren.js index f7a4bd3372728..031b86e588268 100644 --- a/src/utils/flattenChildren.js +++ b/src/utils/flattenChildren.js @@ -51,8 +51,7 @@ var flattenChildrenImpl = function(res, children, nameSoFar) { if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { var child = children[i]; - key = child && child.mountInContainerNode && - (child._key || child.props.key); + key = child && (child._key || (child.props && child.props.key)); escapedKey = key ? escapeTextForBrowser(key) : ('' + i); flattenChildrenImpl( res, From 259392035daac9cf8d45d909a175e640c496f547 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:15:25 -0700 Subject: [PATCH 04/25] mapChildren mapChilden() is similar to Array.map() and objMap() but handles deep nested structures and follows similar rules to flattenChildren() --- src/utils/__tests__/mapChildren-test.js | 117 ++++++++++++++++++++++++ src/utils/mapChildren.js | 35 +++++++ 2 files changed, 152 insertions(+) create mode 100644 src/utils/__tests__/mapChildren-test.js create mode 100644 src/utils/mapChildren.js diff --git a/src/utils/__tests__/mapChildren-test.js b/src/utils/__tests__/mapChildren-test.js new file mode 100644 index 0000000000000..2d0bfec5a9c03 --- /dev/null +++ b/src/utils/__tests__/mapChildren-test.js @@ -0,0 +1,117 @@ +/** + * @emails react-core + * @jsx React.DOM + */ + +"use strict"; + +describe('mapChildren', function() { + + var React; + var ReactTestUtils; + + var mapChildren; + var reactComponentExpect; + + var Wrap; + + beforeEach(function() { + React = require('React'); + ReactTestUtils = require('ReactTestUtils'); + + mapChildren = require('mapChildren'); + reactComponentExpect = require('reactComponentExpect'); + + Wrap = React.createClass({ + render: function() { + return ( +
+ {mapChildren(this.props.children, this.props.mapFn, this)} +
+ ); + } + }); + + this.addMatchers({ + toHaveKeys: function(expected) { + if (this.actual.length != expected.length) { + return false; + } + return this.actual.every(function(component, index) { + return component._key === expected[index]; + }, this); + } + }); + }); + + + it('should support identity for simple', function() { + var mapFn = jasmine.createSpy().andCallFake(function (kid, key, index) { + return kid; + }); + + var simpleKid = ; + + var instance = {simpleKid}; + ReactTestUtils.renderIntoDocument(instance); + + var rendered = reactComponentExpect(instance) + .expectRenderedChild() + .instance(); + + expect(mapFn).toHaveBeenCalledWith(simpleKid, 'simple', 0); + expect(rendered.props.children[0]).toBe(simpleKid); + expect(rendered.props.children).toHaveKeys(['simple']); + }); + + it('should pass key to returned component', function() { + var mapFn = function (kid, key, index) { + return
{kid}
; + }; + + var simpleKid = ; + + var instance = {simpleKid}; + ReactTestUtils.renderIntoDocument(instance); + + var rendered = reactComponentExpect(instance) + .expectRenderedChild() + .instance(); + + expect(rendered.props.children[0]).not.toBe(simpleKid); + expect(rendered.props.children[0].props.children[0]).toBe(simpleKid); + expect(rendered.props.children).toHaveKeys(['simple']); + expect(rendered.props.children[0].props.children).toHaveKeys(['simple']); + }); + + it('should be called for each child', function() { + var mapFn = jasmine.createSpy().andCallFake(function (kid, key, index) { + return
{kid}
; + }); + + var kidOne =
; + var kidTwo =
; + var kidThree =
; + + var instance = ReactTestUtils.renderIntoDocument( + + {kidOne} + {null} + {kidTwo} + {null} + {kidThree} + + ); + + var rendered = reactComponentExpect(instance) + .expectRenderedChild() + .instance(); + + expect(mapFn.calls.length).toBe(3); + expect(mapFn).toHaveBeenCalledWith(kidOne, 'one', 0); + expect(mapFn).toHaveBeenCalledWith(kidTwo, 'two', 1); + expect(mapFn).toHaveBeenCalledWith(kidThree, 'three', 2); + expect(rendered.props.children).not.toEqual(instance.props.children); + expect(rendered.props.children).toHaveKeys(['one', 'two', 'three']); + }); +}); diff --git a/src/utils/mapChildren.js b/src/utils/mapChildren.js new file mode 100644 index 0000000000000..cdba3e4d90f64 --- /dev/null +++ b/src/utils/mapChildren.js @@ -0,0 +1,35 @@ +/** + * @providesModule mapChildren + */ + +"use strict"; + +/** + * Maps children that are typically specified as `props.children`. + * + * The provided mapFunction(child, key, index) will be called for each + * leaf child. + * + * Note: mapChildren assumes children have already been flattened. + * + * @param {array} children + * @param {function(*, string, int)} mapFunction + * @param {*} context + * @return {array} mirrored array with mapped children + */ +function mapChildren(children, mapFunction, context) { + if (children == null) { + return children; + } + var mappedChildren = []; + for (var ii = 0; ii < children.length; ii++) { + var child = children[ii]; + var key = child._key; + var mappedChild = mapFunction.call(context, child, key, ii); + mappedChild._key = key; + mappedChildren.push(mappedChild); + } + return mappedChildren; +} + +module.exports = mapChildren; From 54d3134da2c87fe96eae0df1ee1b05a4f0bbf117 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:15:29 -0700 Subject: [PATCH 05/25] Add `ReactProps.func` This adds ReactProps.func so people don't need to write the slightly-more-cryptic ReactProps.instanceOf(Function). We should have had this all along. --- src/core/ReactProps.js | 1 + src/core/__tests__/ReactProps-test.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/core/ReactProps.js b/src/core/ReactProps.js index fbb52195ffdc6..0a2c0d7c21d5a 100644 --- a/src/core/ReactProps.js +++ b/src/core/ReactProps.js @@ -73,6 +73,7 @@ var Props = { array: createPrimitiveTypeChecker('array'), bool: createPrimitiveTypeChecker('boolean'), + func: createPrimitiveTypeChecker('function'), number: createPrimitiveTypeChecker('number'), object: createPrimitiveTypeChecker('object'), string: createPrimitiveTypeChecker('string'), diff --git a/src/core/__tests__/ReactProps-test.js b/src/core/__tests__/ReactProps-test.js index 64ab79ffbe382..82cb58ebee637 100644 --- a/src/core/__tests__/ReactProps-test.js +++ b/src/core/__tests__/ReactProps-test.js @@ -56,6 +56,7 @@ describe('Primitive Types', function() { it("should not throw for valid values", function() { expect(typeCheck(Props.array, [])).not.toThrow(); expect(typeCheck(Props.bool, false)).not.toThrow(); + expect(typeCheck(Props.func, function() {})).not.toThrow(); expect(typeCheck(Props.number, 0)).not.toThrow(); expect(typeCheck(Props.object, {})).not.toThrow(); expect(typeCheck(Props.string, '')).not.toThrow(); From b581c8cfc784b74e74af80307a799cb4437f7845 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:15:40 -0700 Subject: [PATCH 06/25] Always reassign _key for every pass Currently we're mutating _key. Mutation here is fine, but it needs to be idempotent - which it's not. This is causing some issues. Instead I reassign the _key every time it passes through a flattening. This means that it's unique and stable for a single pass through a composite component. When it's repassed another level, it loses it previous identity and is rekeyed by it's new location. For auto-generated keys by index, this actually means it has the same semantics as before flattening. For explicit keys, it has the effect that keys need to be unique at every level. Regardless of how the key got there. Every component needs to ensure that it doesn't combine keys from two different sources that may collide. This is also inline with the old semantics but less intuitive in the new model. --- src/core/ReactComponent.js | 4 +- src/core/__tests__/ReactIdentity-test.js | 71 ++++++++++++++++++++++++ src/utils/mapChildren.js | 2 +- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index 5c42a8e86fba9..bc3769df9283a 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -72,7 +72,7 @@ function isEmptyChild(child) { function assignKey(setKey, child, index) { if (ReactComponent.isValidComponent(child)) { - var key = child._key || child.props.key; + var key = child.props.key; if (__DEV__) { if (!HAS_WARNED && !key) { HAS_WARNED = true; @@ -300,7 +300,7 @@ var ReactComponent = { } else if (!isEmptyChild(child)) { - if (ReactComponent.isValidComponent(child) && !child._key) { + if (ReactComponent.isValidComponent(child)) { // This is a static node and therefore safe to key by index. // No warning necessary. child._key = child.props.key || ('' + (i - 1)); diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index ecf71a8553143..e21be9d3bbd8f 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -80,4 +80,75 @@ describe('ReactIdentity', function() { .toEqual('.reactRoot[0].:2.:chipmunk'); }); + it('should let restructured components retain their uniqueness', function() { + var instance0 = ; + var instance1 = ; + var instance2 = ; + var wrapped =
{instance0} {instance1}
; + var unwrappedAndAdded = +
+ {instance2} + {wrapped.props.children[0]} + {wrapped.props.children[1]} +
; + + expect(function() { + + React.renderComponent(unwrappedAndAdded, document.createElement('div')); + + }).not.toThrow(); + }); + + it('should retain keys during updates in composite components', function() { + + var TestComponent = React.createClass({ + render: function() { + return
{this.props.children}
; + } + }); + + var TestContainer = React.createClass({ + + getInitialState: function() { + return { swapped: false }; + }, + + swap: function() { + this.setState({ swapped: true }); + }, + + render: function() { + return ( + + {this.state.swapped ? this.props.second : this.props.first} + {this.state.swapped ? this.props.first : this.props.second} + + ); + } + + }); + + var instance0 = ; + var instance1 = ; + + var wrapped = ; + + React.renderComponent(wrapped, document.createElement('div')); + + var beforeKey = wrapped + ._renderedComponent + ._renderedComponent + .props.children[0]._key; + + wrapped.swap(); + + var afterKey = wrapped + ._renderedComponent + ._renderedComponent + .props.children[0]._key; + + expect(beforeKey).not.toEqual(afterKey); + + }); + }); diff --git a/src/utils/mapChildren.js b/src/utils/mapChildren.js index cdba3e4d90f64..27079e8930e59 100644 --- a/src/utils/mapChildren.js +++ b/src/utils/mapChildren.js @@ -26,7 +26,7 @@ function mapChildren(children, mapFunction, context) { var child = children[ii]; var key = child._key; var mappedChild = mapFunction.call(context, child, key, ii); - mappedChild._key = key; + mappedChild.props.key = key; mappedChildren.push(mappedChild); } return mappedChildren; From 606d6b8fd4dfc88c16f56e8787ace37bf140440f Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:16:08 -0700 Subject: [PATCH 07/25] Revert `Object.create` in NormalizedEventListener It seems that the use of Object.create (to comply with strict mode) in NormalizedEventListener is not happy in IE8. --- src/event/NormalizedEventListener.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/event/NormalizedEventListener.js b/src/event/NormalizedEventListener.js index 97136ff430d70..4888607b1580e 100644 --- a/src/event/NormalizedEventListener.js +++ b/src/event/NormalizedEventListener.js @@ -16,8 +16,6 @@ * @providesModule NormalizedEventListener */ -"use strict"; - var EventListener = require('EventListener'); /** @@ -26,25 +24,23 @@ var EventListener = require('EventListener'); * @private */ function normalizeEvent(eventParam) { - var normalized = eventParam || window.event; + var nativeEvent = eventParam || window.event; // In some browsers (OLD FF), setting the target throws an error. A good way // to tell if setting the target will throw an error, is to check if the event // has a `target` property. Safari events have a `target` but it's not always // normalized. Even if a `target` property exists, it's good to only set the // target property if we realize that a change will actually take place. - var hasTargetProperty = 'target' in normalized; - var eventTarget = normalized.target || normalized.srcElement || window; + var hasTargetProperty = 'target' in nativeEvent; + var eventTarget = nativeEvent.target || nativeEvent.srcElement || window; // Safari may fire events on text nodes (Node.TEXT_NODE is 3) // @see http://www.quirksmode.org/js/events_properties.html var textNodeNormalizedTarget = (eventTarget.nodeType === 3) ? eventTarget.parentNode : eventTarget; - if (!hasTargetProperty || normalized.target !== textNodeNormalizedTarget) { - // Create an object that inherits from the native event so that we can set - // `target` on it. (It is read-only and setting it throws in strict mode). - normalized = Object.create(normalized); - normalized.target = textNodeNormalizedTarget; + if (!hasTargetProperty || nativeEvent.target !== textNodeNormalizedTarget) { + // TODO: Normalize the object via `merge()` to work with strict mode. + nativeEvent.target = textNodeNormalizedTarget; } - return normalized; + return nativeEvent; } function createNormalizedCallback(cb) { From 3e211bf662d0b8f67bbdea32566dae75a8c73485 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:16:20 -0700 Subject: [PATCH 08/25] [React] Removing invariant warning about updating owner state It seems like it's possible to render a component that ends up having an owner. Because you can end up rendering inside a render somehow. --- src/core/ReactComponent.js | 8 ------ .../__tests__/ReactComponentLifeCycle-test.js | 28 ------------------- 2 files changed, 36 deletions(-) diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index bc3769df9283a..d380e943c39da 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -235,14 +235,6 @@ var ReactComponent = { * @public */ replaceProps: function(props) { - invariant( - !this.props[OWNER], - 'replaceProps(...): You called `setProps` or `replaceProps` on a ' + - 'component with an owner. This is an anti-pattern since props will ' + - 'get reactively updated when rendered. Instead, change the owner\'s ' + - '`render` method to pass the correct value as props to the component ' + - 'where it is created.' - ); var transaction = ReactComponent.ReactReconcileTransaction.getPooled(); transaction.perform(this.receiveProps, this, props, transaction); ReactComponent.ReactReconcileTransaction.release(transaction); diff --git a/src/core/__tests__/ReactComponentLifeCycle-test.js b/src/core/__tests__/ReactComponentLifeCycle-test.js index 4c1f92e465a21..7fbbbcd56cd11 100644 --- a/src/core/__tests__/ReactComponentLifeCycle-test.js +++ b/src/core/__tests__/ReactComponentLifeCycle-test.js @@ -375,34 +375,6 @@ describe('ReactComponentLifeCycle', function() { expect(instance.state).toEqual(POST_WILL_UNMOUNT_STATE); }); - it('should throw when calling setProps() on an owned component', function() { - /** - * calls setProps in an componentDidMount. - */ - var PropsUpdaterInOnDOMReady = React.createClass({ - componentDidMount: function() { - this.refs.theSimpleComponent.setProps({ - value: this.props.valueToUseInOnDOMReady - }); - }, - render: function() { - return ( - - - ); - } - }); - var instance = - ; - expect(ReactTestUtils.renderIntoDocument.bind(ReactTestUtils, instance)) - .toThrow(); - }); - it('should allow state updates in componentDidMount', function() { /** * calls setState in an componentDidMount. From 100af48f53e292898562f76e0edc9fc7ce50b04e Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:17:00 -0700 Subject: [PATCH 09/25] Support rendering different components into same node var container = ...; // some DOM node React.renderComponent(
, container); React.renderComponent(, container); This should replace the rendered
with a , effectively reconciling at the root level. --- src/core/ReactMount.js | 14 +++++++++----- src/core/__tests__/ReactMount-test.js | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 src/core/__tests__/ReactMount-test.js diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index e07e209be7a39..4fb036c3ee834 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -106,11 +106,15 @@ var ReactMount = { renderComponent: function(nextComponent, container) { var prevComponent = instanceByReactRootID[getReactRootID(container)]; if (prevComponent) { - var nextProps = nextComponent.props; - ReactMount.scrollMonitor(container, function() { - prevComponent.replaceProps(nextProps); - }); - return prevComponent; + if (prevComponent.constructor === nextComponent.constructor) { + var nextProps = nextComponent.props; + ReactMount.scrollMonitor(container, function() { + prevComponent.replaceProps(nextProps); + }); + return prevComponent; + } else { + ReactMount.unmountAndReleaseReactRootNode(container); + } } ReactMount.prepareTopLevelEvents(ReactEventTopLevelCallback); diff --git a/src/core/__tests__/ReactMount-test.js b/src/core/__tests__/ReactMount-test.js new file mode 100644 index 0000000000000..de7e6d8b589bd --- /dev/null +++ b/src/core/__tests__/ReactMount-test.js @@ -0,0 +1,22 @@ +/** + * @jsx React.DOM + * @emails react-core + */ + +"use strict"; + +describe('ReactMount', function() { + var React = require('React'); + var ReactMount = require('ReactMount'); + + it('should render different components in same root', function() { + var container = document.createElement('container'); + document.documentElement.appendChild(container); + + ReactMount.renderComponent(
, container); + expect(container.firstChild.nodeName).toBe('DIV'); + + ReactMount.renderComponent(, container); + expect(container.firstChild.nodeName).toBe('SPAN'); + }); +}); From 1457850b7234deb3ee29dea9e42b5d0f2bca950b Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:46:22 -0700 Subject: [PATCH 10/25] Rename `domUtils` to `dom` --- src/{domUtils => dom}/CSSProperty.js | 0 src/{domUtils => dom}/CSSPropertyOperations.js | 0 src/{domUtils => dom}/DOMChildrenOperations.js | 0 src/{domUtils => dom}/DOMProperty.js | 0 src/{domUtils => dom}/DOMPropertyOperations.js | 0 src/{domUtils => dom}/Danger.js | 0 src/{domUtils => dom}/__tests__/CSSPropertyOperations-test.js | 0 src/{domUtils => dom}/__tests__/DOMPropertyOperations-test.js | 0 src/{domUtils => dom}/dangerousStyleValue.js | 0 src/{domUtils => dom}/getDOMNodeID.js | 0 src/{domUtils => dom}/getTextContentAccessor.js | 0 src/{domUtils => dom}/insertNodeAt.js | 0 src/{domUtils => dom}/isEventSupported.js | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename src/{domUtils => dom}/CSSProperty.js (100%) rename src/{domUtils => dom}/CSSPropertyOperations.js (100%) rename src/{domUtils => dom}/DOMChildrenOperations.js (100%) rename src/{domUtils => dom}/DOMProperty.js (100%) rename src/{domUtils => dom}/DOMPropertyOperations.js (100%) rename src/{domUtils => dom}/Danger.js (100%) rename src/{domUtils => dom}/__tests__/CSSPropertyOperations-test.js (100%) rename src/{domUtils => dom}/__tests__/DOMPropertyOperations-test.js (100%) rename src/{domUtils => dom}/dangerousStyleValue.js (100%) rename src/{domUtils => dom}/getDOMNodeID.js (100%) rename src/{domUtils => dom}/getTextContentAccessor.js (100%) rename src/{domUtils => dom}/insertNodeAt.js (100%) rename src/{domUtils => dom}/isEventSupported.js (100%) diff --git a/src/domUtils/CSSProperty.js b/src/dom/CSSProperty.js similarity index 100% rename from src/domUtils/CSSProperty.js rename to src/dom/CSSProperty.js diff --git a/src/domUtils/CSSPropertyOperations.js b/src/dom/CSSPropertyOperations.js similarity index 100% rename from src/domUtils/CSSPropertyOperations.js rename to src/dom/CSSPropertyOperations.js diff --git a/src/domUtils/DOMChildrenOperations.js b/src/dom/DOMChildrenOperations.js similarity index 100% rename from src/domUtils/DOMChildrenOperations.js rename to src/dom/DOMChildrenOperations.js diff --git a/src/domUtils/DOMProperty.js b/src/dom/DOMProperty.js similarity index 100% rename from src/domUtils/DOMProperty.js rename to src/dom/DOMProperty.js diff --git a/src/domUtils/DOMPropertyOperations.js b/src/dom/DOMPropertyOperations.js similarity index 100% rename from src/domUtils/DOMPropertyOperations.js rename to src/dom/DOMPropertyOperations.js diff --git a/src/domUtils/Danger.js b/src/dom/Danger.js similarity index 100% rename from src/domUtils/Danger.js rename to src/dom/Danger.js diff --git a/src/domUtils/__tests__/CSSPropertyOperations-test.js b/src/dom/__tests__/CSSPropertyOperations-test.js similarity index 100% rename from src/domUtils/__tests__/CSSPropertyOperations-test.js rename to src/dom/__tests__/CSSPropertyOperations-test.js diff --git a/src/domUtils/__tests__/DOMPropertyOperations-test.js b/src/dom/__tests__/DOMPropertyOperations-test.js similarity index 100% rename from src/domUtils/__tests__/DOMPropertyOperations-test.js rename to src/dom/__tests__/DOMPropertyOperations-test.js diff --git a/src/domUtils/dangerousStyleValue.js b/src/dom/dangerousStyleValue.js similarity index 100% rename from src/domUtils/dangerousStyleValue.js rename to src/dom/dangerousStyleValue.js diff --git a/src/domUtils/getDOMNodeID.js b/src/dom/getDOMNodeID.js similarity index 100% rename from src/domUtils/getDOMNodeID.js rename to src/dom/getDOMNodeID.js diff --git a/src/domUtils/getTextContentAccessor.js b/src/dom/getTextContentAccessor.js similarity index 100% rename from src/domUtils/getTextContentAccessor.js rename to src/dom/getTextContentAccessor.js diff --git a/src/domUtils/insertNodeAt.js b/src/dom/insertNodeAt.js similarity index 100% rename from src/domUtils/insertNodeAt.js rename to src/dom/insertNodeAt.js diff --git a/src/domUtils/isEventSupported.js b/src/dom/isEventSupported.js similarity index 100% rename from src/domUtils/isEventSupported.js rename to src/dom/isEventSupported.js From ca5d7bc6831c0bfb1f37ff9bed02774e1caae5a7 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:48:27 -0700 Subject: [PATCH 11/25] Add `getDefaultProps()` As it turns out, default values are very useful. This implements getDefaultProps(), a hook for components to provide prop values when a prop is not specified by the user. --- src/core/ReactCompositeComponent.js | 65 +++++++++++++------ .../__tests__/ReactCompositeComponent-test.js | 47 ++++++++++++++ 2 files changed, 91 insertions(+), 21 deletions(-) diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index 562dadf675fe9..aabbfb4e86911 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -91,6 +91,19 @@ var ReactCompositeComponentInterface = { // ==== Definition methods ==== + /** + * Invoked when the component is mounted and whenever new props are received. + * Values in the returned mapping will be set on `this.props` if that prop is + * not specified (i.e. using an `in` check). + * + * This method is invoked before `getInitialState` and therefore cannot rely + * on `this.state` or use `this.setState`. + * + * @return {object} + * @optional + */ + getDefaultProps: SpecPolicy.DEFINE_ONCE, + /** * Invoked once before the component is mounted. The return value will be used * as the initial value of `this.state`. @@ -419,9 +432,7 @@ var ReactCompositeComponentMixin = { this._lifeCycleState = ReactComponent.LifeCycle.UNMOUNTED; this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING; - if (this.constructor.propDeclarations) { - this._assertValidProps(this.props); - } + this._processProps(this.props); if (this.__reactAutoBindMap) { this._bindAutoBindMethods(); @@ -489,9 +500,7 @@ var ReactCompositeComponentMixin = { * @internal */ receiveProps: function(nextProps, transaction) { - if (this.constructor.propDeclarations) { - this._assertValidProps(nextProps); - } + this._processProps(nextProps); ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction); this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS; @@ -573,6 +582,35 @@ var ReactCompositeComponentMixin = { } }, + /** + * Processes props by setting default values for unspecified props and + * asserting that the props are valid. + * + * @param {object} props + * @private + */ + _processProps: function(props) { + var propName; + if (this.getDefaultProps) { + var defaultProps = this.getDefaultProps(); + for (propName in defaultProps) { + if (!(propName in props)) { + props[propName] = defaultProps[propName]; + } + } + } + var propDeclarations = this.constructor.propDeclarations; + if (propDeclarations) { + var componentName = this.constructor.displayName; + for (propName in propDeclarations) { + var checkProp = propDeclarations[propName]; + if (checkProp) { + checkProp(props, propName, componentName); + } + } + } + }, + /** * Receives next props and next state, and negotiates whether or not the * component should update as a result. @@ -696,21 +734,6 @@ var ReactCompositeComponentMixin = { return renderedComponent; }, - /** - * @param {object} props - * @private - */ - _assertValidProps: function(props) { - var propDeclarations = this.constructor.propDeclarations; - var componentName = this.constructor.displayName; - for (var propName in propDeclarations) { - var checkProp = propDeclarations[propName]; - if (checkProp) { - checkProp(props, propName, componentName); - } - } - }, - /** * @private */ diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index 463bfd6144f41..51575277b378e 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -194,6 +194,53 @@ describe('ReactCompositeComponent', function() { expect(retValAfterMountWithCrazyScope).toBe(RETURN_VALUE_AFTER_MOUNT); }); + it('should normalize props with default values', function() { + var Component = React.createClass({ + props: {key: ReactProps.string.isRequired}, + getDefaultProps: function() { + return {key: 'testKey'}; + }, + getInitialState: function() { + return {key: this.props.key + 'State'}; + }, + render: function() { + return {this.props.key}; + } + }); + + var instance = ; + ReactTestUtils.renderIntoDocument(instance); + reactComponentExpect(instance).scalarPropsEqual({key: 'testKey'}); + reactComponentExpect(instance).scalarStateEqual({key: 'testKeyState'}); + + expect(function() { + ReactTestUtils.renderIntoDocument(); + }).toThrow( + 'Invariant Violation: Required prop `key` was not specified in ' + + '`Component`.' + ); + }); + + it('should check default prop values', function() { + var Component = React.createClass({ + props: {key: ReactProps.string.isRequired}, + getDefaultProps: function() { + return {key: null}; + }, + render: function() { + return {this.props.key}; + } + }); + + var instance = ; + expect(function() { + ReactTestUtils.renderIntoDocument(instance); + }).toThrow( + 'Invariant Violation: Required prop `key` was not specified in ' + + '`Component`.' + ); + }); + it('should check declared prop types', function() { var Component = React.createClass({ props: { From 3ffbb4d096791e0b8f868adfd598a83e3296a671 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:52:20 -0700 Subject: [PATCH 12/25] Re-add invariant Bring back the invariant() that disallows setProps() and replaceProps() on owned components. --- src/core/ReactComponent.js | 8 ++++++ .../__tests__/ReactComponentLifeCycle-test.js | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index d380e943c39da..bc3769df9283a 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -235,6 +235,14 @@ var ReactComponent = { * @public */ replaceProps: function(props) { + invariant( + !this.props[OWNER], + 'replaceProps(...): You called `setProps` or `replaceProps` on a ' + + 'component with an owner. This is an anti-pattern since props will ' + + 'get reactively updated when rendered. Instead, change the owner\'s ' + + '`render` method to pass the correct value as props to the component ' + + 'where it is created.' + ); var transaction = ReactComponent.ReactReconcileTransaction.getPooled(); transaction.perform(this.receiveProps, this, props, transaction); ReactComponent.ReactReconcileTransaction.release(transaction); diff --git a/src/core/__tests__/ReactComponentLifeCycle-test.js b/src/core/__tests__/ReactComponentLifeCycle-test.js index 7fbbbcd56cd11..4c1f92e465a21 100644 --- a/src/core/__tests__/ReactComponentLifeCycle-test.js +++ b/src/core/__tests__/ReactComponentLifeCycle-test.js @@ -375,6 +375,34 @@ describe('ReactComponentLifeCycle', function() { expect(instance.state).toEqual(POST_WILL_UNMOUNT_STATE); }); + it('should throw when calling setProps() on an owned component', function() { + /** + * calls setProps in an componentDidMount. + */ + var PropsUpdaterInOnDOMReady = React.createClass({ + componentDidMount: function() { + this.refs.theSimpleComponent.setProps({ + value: this.props.valueToUseInOnDOMReady + }); + }, + render: function() { + return ( + + + ); + } + }); + var instance = + ; + expect(ReactTestUtils.renderIntoDocument.bind(ReactTestUtils, instance)) + .toThrow(); + }); + it('should allow state updates in componentDidMount', function() { /** * calls setState in an componentDidMount. From bae6100ae8528fcbc7e6085e4c1d6759824fcb36 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:52:24 -0700 Subject: [PATCH 13/25] Make ReactIdentity-test less fragile with respect to root IDs. --- src/core/__tests__/ReactIdentity-test.js | 35 +++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index e21be9d3bbd8f..25f25ae2132d9 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -18,6 +18,15 @@ describe('ReactIdentity', function() { reactComponentExpect = require('reactComponentExpect'); }); + var idExp = /^\.reactRoot\[\d+\](.*)$/; + function checkId(child, expectedId) { + var actual = idExp.exec(child.id); + var expected = idExp.exec(expectedId); + expect(actual).toBeTruthy(); + expect(expected).toBeTruthy(); + expect(actual[1]).toEqual(expected[1]); + } + it('should allow keyed objects to express identity', function() { var instance =
@@ -30,8 +39,8 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(2); - expect(node.childNodes[0].id).toEqual('.reactRoot[0].:0:first'); - expect(node.childNodes[1].id).toEqual('.reactRoot[0].:0:second'); + checkId(node.childNodes[0], '.reactRoot[0].:0:first'); + checkId(node.childNodes[1], '.reactRoot[0].:0:second'); }); it('should allow key property to express identity', function() { @@ -44,8 +53,8 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(2); - expect(node.childNodes[0].id).toEqual('.reactRoot[0].:apple'); - expect(node.childNodes[1].id).toEqual('.reactRoot[0].:banana'); + checkId(node.childNodes[0], '.reactRoot[0].:apple'); + checkId(node.childNodes[1], '.reactRoot[0].:banana'); }); it('should use instance identity', function() { @@ -66,18 +75,12 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(3); - expect(node.childNodes[0].id) - .toEqual('.reactRoot[0].:wrap1'); - expect(node.childNodes[0].firstChild.id) - .toEqual('.reactRoot[0].:wrap1.:squirrel'); - expect(node.childNodes[1].id) - .toEqual('.reactRoot[0].:wrap2'); - expect(node.childNodes[1].firstChild.id) - .toEqual('.reactRoot[0].:wrap2.:bunny'); - expect(node.childNodes[2].id) - .toEqual('.reactRoot[0].:2'); - expect(node.childNodes[2].firstChild.id) - .toEqual('.reactRoot[0].:2.:chipmunk'); + checkId(node.childNodes[0], '.reactRoot[0].:wrap1'); + checkId(node.childNodes[0].firstChild, '.reactRoot[0].:wrap1.:squirrel'); + checkId(node.childNodes[1], '.reactRoot[0].:wrap2'); + checkId(node.childNodes[1].firstChild, '.reactRoot[0].:wrap2.:bunny'); + checkId(node.childNodes[2], '.reactRoot[0].:2'); + checkId(node.childNodes[2].firstChild, '.reactRoot[0].:2.:chipmunk'); }); it('should let restructured components retain their uniqueness', function() { From 11a7cb5b734a1c28cce399fc8a56dbe43c7ce371 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:52:27 -0700 Subject: [PATCH 14/25] Only Allow `forceUpdate` on Mounted Components --- src/core/ReactCompositeComponent.js | 11 +++++++ .../__tests__/ReactCompositeComponent-test.js | 32 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index aabbfb4e86911..fdf3d8e7aed64 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -708,6 +708,17 @@ var ReactCompositeComponentMixin = { * @protected */ forceUpdate: function() { + var compositeLifeCycleState = this._compositeLifeCycleState; + invariant( + this._lifeCycleState === ReactComponent.LifeCycle.MOUNTED, + 'forceUpdate(...): Can only force an update on mounted components.' + ); + invariant( + compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE && + compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING, + 'forceUpdate(...): Cannot force an update while unmounting component ' + + 'or during an existing state transition (such as within `render`).' + ); var transaction = ReactComponent.ReactReconcileTransaction.getPooled(); transaction.perform( this._performComponentUpdate, diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index 51575277b378e..f2b666321c771 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -270,4 +270,36 @@ describe('ReactCompositeComponent', function() { }).not.toThrow(); }); + it('should not allow `forceUpdate` on unmounted components', function() { + var container = document.createElement('div'); + document.documentElement.appendChild(container); + + var Component = React.createClass({ + render: function() { + return
; + } + }); + + var instance = ; + expect(function() { + instance.forceUpdate(); + }).toThrow( + 'Invariant Violation: forceUpdate(...): Can only force an update on ' + + 'mounted components.' + ); + + React.renderComponent(instance, container); + expect(function() { + instance.forceUpdate(); + }).not.toThrow(); + + React.unmountAndReleaseReactRootNode(container); + expect(function() { + instance.forceUpdate(); + }).toThrow( + 'Invariant Violation: forceUpdate(...): Can only force an update on ' + + 'mounted components.' + ); + }); + }); From a06de4bc4f5735aba767740d243b270e272ac6fa Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:52:30 -0700 Subject: [PATCH 15/25] Cleanup `ReactCurrentOwner` on Fatal If a React component's render() fatals, it may contaminate ReactCurrentOwner. This will cause the owner to be set improperly for the next React.renderComponent() invocation (which causes an owner to be set when there shouldn't be one). --- src/core/ReactCompositeComponent.js | 11 +++++++++-- .../__tests__/ReactCompositeComponent-test.js | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index fdf3d8e7aed64..468c200511617 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -734,9 +734,16 @@ var ReactCompositeComponentMixin = { * @private */ _renderValidatedComponent: function() { + var renderedComponent; ReactCurrentOwner.current = this; - var renderedComponent = this.render(); - ReactCurrentOwner.current = null; + try { + renderedComponent = this.render(); + } catch (error) { + // IE8 requires `catch` in order to use `finally`. + throw error; + } finally { + ReactCurrentOwner.current = null; + } invariant( ReactComponent.isValidComponent(renderedComponent), '%s.render(): A valid ReactComponent must be returned.', diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index f2b666321c771..178edaee1d7e1 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -23,6 +23,7 @@ var MorphingComponent; var MorphingAutoBindComponent; var ChildUpdates; var React; +var ReactCurrentOwner; var ReactProps; var ReactTestUtils; @@ -35,6 +36,7 @@ describe('ReactCompositeComponent', function() { cx = require('cx'); reactComponentExpect = require('reactComponentExpect'); React = require('React'); + ReactCurrentOwner = require('ReactCurrentOwner'); ReactProps = require('ReactProps'); ReactTestUtils = require('ReactTestUtils'); @@ -302,4 +304,21 @@ describe('ReactCompositeComponent', function() { ); }); + it('should cleanup even if render() fatals', function() { + var BadComponent = React.createClass({ + render: function() { + throw new Error(); + } + }); + var instance = ; + + expect(ReactCurrentOwner.current).toBe(null); + + expect(function() { + ReactTestUtils.renderIntoDocument(instance); + }).toThrow(); + + expect(ReactCurrentOwner.current).toBe(null); + }); + }); From 9965b6b9dda2716e3618e4cb1acbf83ce29e3c2f Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:52:33 -0700 Subject: [PATCH 16/25] Fix Listener Cleanup on Unmount We need to make sure that deleteAllListeners() is invoked before we call the superclass's unmountComponent() method or else we will lose this._rootNodeID. I also added an invariant and unit test to make sure we do not break this in the future. --- src/core/ReactNativeComponent.js | 2 +- .../__tests__/ReactNativeComponent-test.js | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/core/ReactNativeComponent.js b/src/core/ReactNativeComponent.js index cbbc01a543049..af261226eef28 100644 --- a/src/core/ReactNativeComponent.js +++ b/src/core/ReactNativeComponent.js @@ -317,9 +317,9 @@ ReactNativeComponent.Mixin = { * @internal */ unmountComponent: function() { + ReactEvent.deleteAllListeners(this._rootNodeID); ReactComponent.Mixin.unmountComponent.call(this); this.unmountMultiChild(); - ReactEvent.deleteAllListeners(this._rootNodeID); } }; diff --git a/src/core/__tests__/ReactNativeComponent-test.js b/src/core/__tests__/ReactNativeComponent-test.js index ab1bb3276b8e8..80b4e793e05a8 100644 --- a/src/core/__tests__/ReactNativeComponent-test.js +++ b/src/core/__tests__/ReactNativeComponent-test.js @@ -239,4 +239,26 @@ describe('ReactNativeComponent', function() { }); }); + describe('unmountComponent', function() { + it("should clean up listeners", function() { + var React = require('React'); + var ReactEvent = require('ReactEvent'); + + var container = document.createElement('div'); + document.documentElement.appendChild(container); + + var callback = function() {}; + var instance =
; + React.renderComponent(instance, container); + + var rootNode = instance.getDOMNode(); + var rootNodeID = rootNode.id; + expect(ReactEvent.getListener(rootNodeID, 'onClick')).toBe(callback); + + React.unmountAndReleaseReactRootNode(container); + + expect(ReactEvent.getListener(rootNodeID, 'onClick')).toBe(undefined); + }); + }); + }); From 0614d3065403ad9daa5a0496781d9c0b8d8d2b51 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 12:57:26 -0700 Subject: [PATCH 17/25] Move test utils internally, update for consistency --- src/test/reactComponentExpect.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/reactComponentExpect.js b/src/test/reactComponentExpect.js index b17caea96d46c..0a359fe2d2f66 100644 --- a/src/test/reactComponentExpect.js +++ b/src/test/reactComponentExpect.js @@ -20,7 +20,7 @@ var ReactComponent = require('ReactComponent'); var ReactTestUtils = require('ReactTestUtils'); -var copyProperties = require('copyProperties'); +var mergeInto = require('mergeInto'); function reactComponentExpect(instance) { if (instance instanceof reactComponentExpect) { @@ -35,7 +35,7 @@ function reactComponentExpect(instance) { this.toBeValidReactComponent(); } -copyProperties(reactComponentExpect.prototype, { +mergeInto(reactComponentExpect.prototype, { // Getters ------------------------------------------------------------------- /** From 9d1055b3d22cbed777c44ee7996c12268ab5041b Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Tue, 4 Jun 2013 14:05:07 -0700 Subject: [PATCH 18/25] Rename `ReactEvent` to `ReactEventEmitter` ReactEvent should be reserved for the actual object created when an event fires. The current ReactEvent is more like EventEmitter than anything (e.g. it sets up delegation, provides methods to attach and remove listeners). --- src/core/ReactDOMForm.js | 4 +- .../{ReactEvent.js => ReactEventEmitter.js} | 101 ++++++++---------- src/core/ReactEventTopLevelCallback.js | 11 +- src/core/ReactMount.js | 4 +- src/core/ReactNativeComponent.js | 8 +- src/core/ReactReconcileTransaction.js | 12 +-- src/core/__tests__/ReactEvent-test.js | 88 +++++++-------- .../__tests__/ReactNativeComponent-test.js | 10 +- .../__tests__/AnalyticsEventPlugin-test.js | 6 +- src/test/ReactTestUtils.js | 10 +- 10 files changed, 125 insertions(+), 129 deletions(-) rename src/core/{ReactEvent.js => ReactEventEmitter.js} (77%) diff --git a/src/core/ReactDOMForm.js b/src/core/ReactDOMForm.js index 9d15130035e38..4aae407255fb3 100644 --- a/src/core/ReactDOMForm.js +++ b/src/core/ReactDOMForm.js @@ -20,7 +20,7 @@ var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactDOM = require('ReactDOM'); -var ReactEvent = require('ReactEvent'); +var ReactEventEmitter = require('ReactEventEmitter'); var EventConstants = require('EventConstants'); // Store a reference to the
`ReactNativeComponent`. @@ -41,7 +41,7 @@ var ReactDOMForm = ReactCompositeComponent.createClass({ }, componentDidMount: function(node) { - ReactEvent.trapBubbledEvent( + ReactEventEmitter.trapBubbledEvent( EventConstants.topLevelTypes.topSubmit, 'submit', node diff --git a/src/core/ReactEvent.js b/src/core/ReactEventEmitter.js similarity index 77% rename from src/core/ReactEvent.js rename to src/core/ReactEventEmitter.js index 702c8816c3c2c..5e722606c536b 100644 --- a/src/core/ReactEvent.js +++ b/src/core/ReactEventEmitter.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @providesModule ReactEvent + * @providesModule ReactEventEmitter */ "use strict"; @@ -33,9 +33,9 @@ var listen = NormalizedEventListener.listen; var capture = NormalizedEventListener.capture; /** - * `ReactEvent` is used to attach top-level event listeners. For example: + * `ReactEventEmitter` is used to attach top-level event listeners. For example: * - * ReactEvent.putListener('myID', 'onClick', myFunction); + * ReactEventEmitter.putListener('myID', 'onClick', myFunction); * * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'. */ @@ -43,36 +43,32 @@ var capture = NormalizedEventListener.capture; /** * Overview of React and the event system: * - * . - * +-------------+ . - * | DOM | . - * +-------------+ . +-----------+ - * + . +--------+|SimpleEvent| - * | . | |Plugin | - * +-----|-------+ . v +-----------+ - * | | | . +--------------+ +------------+ - * | +------------.---->|EventPluginHub| | Event | - * | | . | | +-----------+ | Propagators| - * | ReactEvent | . | | |TapEvent | |------------| - * | | . | |<---+|Plugin | |other plugin| - * | +------------.---------+ | +-----------+ | utilities | - * | | | . | | | +------------+ - * | | | . +---|----------+ - * | | | . | ^ +-----------+ - * | | | . | | |Enter/Leave| - * +-----| ------+ . | +-------+|Plugin | - * | . v +-----------+ - * + . +--------+ - * +-------------+ . |callback| - * | application | . |registry| - * |-------------| . +--------+ - * | | . - * | | . - * | | . - * | | . - * +-------------+ . - * . - * React Core . General Purpose Event Plugin System + * . + * +------------+ . + * | DOM | . + * +------------+ . +-----------+ + * + . +--------+|SimpleEvent| + * | . | |Plugin | + * +-----|------+ . v +-----------+ + * | | | . +--------------+ +------------+ + * | +-----------.--->|EventPluginHub| | Event | + * | | . | | +-----------+ | Propagators| + * | ReactEvent | . | | |TapEvent | |------------| + * | Emitter | . | |<---+|Plugin | |other plugin| + * | | . | | +-----------+ | utilities | + * | +-----------.---------+ | +------------+ + * | | | . +----|---------+ + * +-----|------+ . | ^ +-----------+ + * | . | | |Enter/Leave| + * + . | +-------+|Plugin | + * +-------------+ . v +-----------+ + * | application | . +----------+ + * |-------------| . | callback | + * | | . | registry | + * | | . +----------+ + * +-------------+ . + * . + * React Core . General Purpose Event Plugin System */ /** @@ -98,21 +94,6 @@ var capture = NormalizedEventListener.capture; var _isListening = false; -var EVENT_LISTEN_MISUSE; -var WORKER_DISABLE; - -if (__DEV__) { - EVENT_LISTEN_MISUSE = - 'You must register listeners at the top of the document, only once - ' + - 'and only in the main UI thread of a browser - if you are attempting ' + - 'listen in a worker, the framework is probably doing something wrong ' + - 'and you should report this immediately.'; - WORKER_DISABLE = - 'Cannot disable event listening in Worker thread. This is likely a ' + - 'bug in the framework. Please report immediately.'; -} - - /** * Traps top-level events that bubble. Delegates to the main dispatcher * `handleTopLevel` after performing some basic normalization via @@ -122,7 +103,9 @@ function trapBubbledEvent(topLevelType, handlerBaseName, onWhat) { listen( onWhat, handlerBaseName, - ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType) + ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( + topLevelType + ) ); } @@ -133,7 +116,9 @@ function trapCapturedEvent(topLevelType, handlerBaseName, onWhat) { capture( onWhat, handlerBaseName, - ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType) + ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( + topLevelType + ) ); } @@ -160,7 +145,7 @@ function registerDocumentResizeListener() { } /** - * Summary of `ReactEvent` event handling: + * Summary of `ReactEventEmitter` event handling: * * - We trap low level 'top-level' events. * @@ -241,8 +226,8 @@ function listenAtTopLevel(touchNotMouse) { } /** - * This is the heart of `ReactEvent`. It simply streams the top-level native - * events to `EventPluginHub`. + * This is the heart of `ReactEventEmitter`. It simply streams the top-level + * native events to `EventPluginHub`. * * @param {object} topLevelType Record from `EventConstants`. * @param {Event} nativeEvent A Standard Event with fixed `target` property. @@ -273,11 +258,11 @@ function setEnabled(enabled) { 'setEnabled(...): Cannot toggle event listening in a Worker thread. This ' + 'is likely a bug in the framework. Please report immediately.' ); - ReactEvent.TopLevelCallbackCreator.setEnabled(enabled); + ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled); } function isEnabled() { - return ReactEvent.TopLevelCallbackCreator.isEnabled(); + return ReactEventEmitter.TopLevelCallbackCreator.isEnabled(); } /** @@ -299,13 +284,13 @@ function ensureListening(touchNotMouse, TopLevelCallbackCreator) { 'This is likely a bug in the framework. Please report immediately.' ); if (!_isListening) { - ReactEvent.TopLevelCallbackCreator = TopLevelCallbackCreator; + ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator; listenAtTopLevel(touchNotMouse); _isListening = true; } } -var ReactEvent = { +var ReactEventEmitter = { TopLevelCallbackCreator: null, // Injectable callback creator. handleTopLevel: handleTopLevel, setEnabled: setEnabled, @@ -319,4 +304,4 @@ var ReactEvent = { trapCapturedEvent: trapCapturedEvent }; -module.exports = ReactEvent; +module.exports = ReactEventEmitter; diff --git a/src/core/ReactEventTopLevelCallback.js b/src/core/ReactEventTopLevelCallback.js index a6cf82ef94038..2b72eceb32313 100644 --- a/src/core/ReactEventTopLevelCallback.js +++ b/src/core/ReactEventTopLevelCallback.js @@ -19,7 +19,7 @@ "use strict"; var ExecutionEnvironment = require('ExecutionEnvironment'); -var ReactEvent = require('ReactEvent'); +var ReactEventEmitter = require('ReactEventEmitter'); var ReactInstanceHandles = require('ReactInstanceHandles'); var getDOMNodeID = require('getDOMNodeID'); @@ -63,9 +63,12 @@ var ReactEventTopLevelCallback = { fixedNativeEvent.target ) || ExecutionEnvironment.global; var renderedTargetID = getDOMNodeID(renderedTarget); - var event = fixedNativeEvent; - var target = renderedTarget; - ReactEvent.handleTopLevel(topLevelType, event, renderedTargetID, target); + ReactEventEmitter.handleTopLevel( + topLevelType, + fixedNativeEvent, + renderedTargetID, + renderedTarget + ); }; } diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index 4fb036c3ee834..d6734294afb89 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -18,7 +18,7 @@ "use strict"; -var ReactEvent = require('ReactEvent'); +var ReactEventEmitter = require('ReactEventEmitter'); var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); @@ -86,7 +86,7 @@ var ReactMount = { * @private */ prepareTopLevelEvents: function(TopLevelCallbackCreator) { - ReactEvent.ensureListening( + ReactEventEmitter.ensureListening( ReactMount.useTouchEvents, TopLevelCallbackCreator ); diff --git a/src/core/ReactNativeComponent.js b/src/core/ReactNativeComponent.js index af261226eef28..82bfcf7002904 100644 --- a/src/core/ReactNativeComponent.js +++ b/src/core/ReactNativeComponent.js @@ -22,7 +22,7 @@ var CSSPropertyOperations = require('CSSPropertyOperations'); var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactComponent = require('ReactComponent'); -var ReactEvent = require('ReactEvent'); +var ReactEventEmitter = require('ReactEventEmitter'); var ReactMultiChild = require('ReactMultiChild'); var escapeTextForBrowser = require('escapeTextForBrowser'); @@ -32,8 +32,8 @@ var keyOf = require('keyOf'); var merge = require('merge'); var mixInto = require('mixInto'); -var putListener = ReactEvent.putListener; -var registrationNames = ReactEvent.registrationNames; +var putListener = ReactEventEmitter.putListener; +var registrationNames = ReactEventEmitter.registrationNames; // For quickly matching children type, to test if can be treated as content. var CONTENT_TYPES = {'string': true, 'number': true}; @@ -317,7 +317,7 @@ ReactNativeComponent.Mixin = { * @internal */ unmountComponent: function() { - ReactEvent.deleteAllListeners(this._rootNodeID); + ReactEventEmitter.deleteAllListeners(this._rootNodeID); ReactComponent.Mixin.unmountComponent.call(this); this.unmountMultiChild(); } diff --git a/src/core/ReactReconcileTransaction.js b/src/core/ReactReconcileTransaction.js index f714782240da4..a44214dcb8adf 100644 --- a/src/core/ReactReconcileTransaction.js +++ b/src/core/ReactReconcileTransaction.js @@ -21,7 +21,7 @@ var ExecutionEnvironment = require('ExecutionEnvironment'); var PooledClass = require('PooledClass'); -var ReactEvent = require('ReactEvent'); +var ReactEventEmitter = require('ReactEventEmitter'); var ReactInputSelection = require('ReactInputSelection'); var ReactOnDOMReady = require('ReactOnDOMReady'); var Transaction = require('Transaction'); @@ -50,21 +50,21 @@ var SELECTION_RESTORATION = { */ var EVENT_SUPPRESSION = { /** - * @return {boolean} The enabled status of `ReactEvent` before the + * @return {boolean} The enabled status of `ReactEventEmitter` before the * reconciliation. */ initialize: function() { - var currentlyEnabled = ReactEvent.isEnabled(); - ReactEvent.setEnabled(false); + var currentlyEnabled = ReactEventEmitter.isEnabled(); + ReactEventEmitter.setEnabled(false); return currentlyEnabled; }, /** - * @param {boolean} previouslyEnabled The enabled status of `ReactEvent` + * @param {boolean} previouslyEnabled Enabled status of `ReactEventEmitter` * before the reconciliation occured. `close` restores the previous value. */ close: function(previouslyEnabled) { - ReactEvent.setEnabled(previouslyEnabled); + ReactEventEmitter.setEnabled(previouslyEnabled); } }; diff --git a/src/core/__tests__/ReactEvent-test.js b/src/core/__tests__/ReactEvent-test.js index 495bb74698ca8..9fb5fe9eb64b9 100644 --- a/src/core/__tests__/ReactEvent-test.js +++ b/src/core/__tests__/ReactEvent-test.js @@ -22,7 +22,7 @@ require('mock-modules') .dontMock('BrowserScroll') .dontMock('CallbackRegistry') .dontMock('EventPluginHub') - .dontMock('ReactEvent') + .dontMock('ReactEventEmitter') .dontMock('ReactInstanceHandles') .dontMock('EventPluginHub') .dontMock('TapEventPlugin') @@ -34,7 +34,7 @@ var keyOf = require('keyOf'); var mocks = require('mocks'); var EventPluginHub; -var ReactEvent; +var ReactEventEmitter; var ReactEventTopLevelCallback; var ReactTestUtils; var TapEventPlugin; @@ -58,12 +58,12 @@ var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null}); /** - * Since `ReactEvent` is fairly well separated from the DOM, we can test almost - * all of `ReactEvent` without ever rendering anything in the DOM. As long as we - * provide IDs that follow `React's` conventional id namespace hierarchy. - * The only reason why we create these DOM nodes, is so that when we feed them - * into `ReactEvent` (through `ReactTestUtils`), the event handlers may receive - * a DOM node to inspect. + * Since `ReactEventEmitter` is fairly well separated from the DOM, we can test + * almost all of `ReactEventEmitter` without ever rendering anything in the DOM. + * As long as we provide IDs that follow `React's` conventional id namespace + * hierarchy. The only reason why we create these DOM nodes, is so that when we + * feed them into `ReactEventEmitter` (through `ReactTestUtils`), the event + * handlers may receive a DOM node to inspect. */ var CHILD = document.createElement('div'); var PARENT = document.createElement('div'); @@ -73,24 +73,24 @@ PARENT.id = '.reactRoot.[0].[0]'; GRANDPARENT.id = '.reactRoot.[0]'; function registerSimpleTestHandler() { - ReactEvent.putListener(CHILD.id, ON_CLICK_KEY, LISTENER); - var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY); + ReactEventEmitter.putListener(CHILD.id, ON_CLICK_KEY, LISTENER); + var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); expect(listener).toEqual(LISTENER); - return ReactEvent.getListener(CHILD.id, ON_CLICK_KEY); + return ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); } -describe('ReactEvent', function() { +describe('ReactEventEmitter', function() { beforeEach(function() { require('mock-modules').dumpCache(); EventPluginHub = require('EventPluginHub'); TapEventPlugin = require('TapEventPlugin'); - ReactEvent = require('ReactEvent'); + ReactEventEmitter = require('ReactEventEmitter'); ReactTestUtils = require('ReactTestUtils'); ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); idCallOrder = []; tapMoveThreshold = TapEventPlugin.tapMoveThreshold; - ReactEvent.ensureListening(false, ReactEventTopLevelCallback); + ReactEventEmitter.ensureListening(false, ReactEventTopLevelCallback); EventPluginHub.injection.injectEventPluginsByName({ TapEventPlugin: TapEventPlugin }); @@ -98,20 +98,20 @@ describe('ReactEvent', function() { it('should store a listener correctly', function() { registerSimpleTestHandler(); - var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY); + var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); expect(listener).toBe(LISTENER); }); it('should retrieve a listener correctly', function() { registerSimpleTestHandler(); - var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY); + var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); expect(listener).toEqual(LISTENER); }); it('should clear all handlers when asked to', function() { registerSimpleTestHandler(); - ReactEvent.deleteAllListeners(CHILD.id); - var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY); + ReactEventEmitter.deleteAllListeners(CHILD.id); + var listener = ReactEventEmitter.getListener(CHILD.id, ON_CLICK_KEY); expect(listener).toBe(undefined); }); @@ -121,28 +121,28 @@ describe('ReactEvent', function() { expect(LISTENER.mock.calls.length).toBe(1); }); - it('should not invoke handlers when ReactEvent is disabled', function() { + it('should not invoke handlers if ReactEventEmitter is disabled', function() { registerSimpleTestHandler(); - ReactEvent.setEnabled(false); + ReactEventEmitter.setEnabled(false); ReactTestUtils.Simulate.click(CHILD); expect(LISTENER.mock.calls.length).toBe(0); - ReactEvent.setEnabled(true); + ReactEventEmitter.setEnabled(true); ReactTestUtils.Simulate.click(CHILD); expect(LISTENER.mock.calls.length).toBe(1); }); it('should bubble simply', function() { - ReactEvent.putListener( + ReactEventEmitter.putListener( CHILD.id, ON_CLICK_KEY, recordID.bind(null, CHILD.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( PARENT.id, ON_CLICK_KEY, recordID.bind(null, PARENT.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( GRANDPARENT.id, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT.id) @@ -155,17 +155,17 @@ describe('ReactEvent', function() { }); it('should support stopPropagation()', function() { - ReactEvent.putListener( + ReactEventEmitter.putListener( CHILD.id, ON_CLICK_KEY, recordID.bind(null, CHILD.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( PARENT.id, ON_CLICK_KEY, recordIDAndStopPropagation.bind(null, PARENT.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( GRANDPARENT.id, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT.id) @@ -177,17 +177,17 @@ describe('ReactEvent', function() { }); it('should stop after first dispatch if stopPropagation', function() { - ReactEvent.putListener( + ReactEventEmitter.putListener( CHILD.id, ON_CLICK_KEY, recordIDAndStopPropagation.bind(null, CHILD.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( PARENT.id, ON_CLICK_KEY, recordID.bind(null, PARENT.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( GRANDPARENT.id, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT.id) @@ -198,17 +198,17 @@ describe('ReactEvent', function() { }); it('should stopPropagation if false is returned', function() { - ReactEvent.putListener( + ReactEventEmitter.putListener( CHILD.id, ON_CLICK_KEY, recordIDAndReturnFalse.bind(null, CHILD.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( PARENT.id, ON_CLICK_KEY, recordID.bind(null, PARENT.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( GRANDPARENT.id, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT.id) @@ -230,10 +230,10 @@ describe('ReactEvent', function() { it('should invoke handlers that were removed while bubbling', function() { var handleParentClick = mocks.getMockFunction(); var handleChildClick = function(abstractEvent) { - ReactEvent.deleteAllListeners(PARENT.id); + ReactEventEmitter.deleteAllListeners(PARENT.id); }; - ReactEvent.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick); - ReactEvent.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick); + ReactEventEmitter.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick); + ReactEventEmitter.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick); ReactTestUtils.Simulate.click(CHILD); expect(handleParentClick.mock.calls.length).toBe(1); }); @@ -241,15 +241,15 @@ describe('ReactEvent', function() { it('should not invoke newly inserted handlers while bubbling', function() { var handleParentClick = mocks.getMockFunction(); var handleChildClick = function(abstractEvent) { - ReactEvent.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick); + ReactEventEmitter.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick); }; - ReactEvent.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick); + ReactEventEmitter.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick); ReactTestUtils.Simulate.click(CHILD); expect(handleParentClick.mock.calls.length).toBe(0); }); it('should infer onTouchTap from a touchStart/End', function() { - ReactEvent.putListener( + ReactEventEmitter.putListener( CHILD.id, ON_TOUCH_TAP_KEY, recordID.bind(null, CHILD.id) @@ -267,7 +267,7 @@ describe('ReactEvent', function() { }); it('should infer onTouchTap from when dragging below threshold', function() { - ReactEvent.putListener( + ReactEventEmitter.putListener( CHILD.id, ON_TOUCH_TAP_KEY, recordID.bind(null, CHILD.id) @@ -285,7 +285,7 @@ describe('ReactEvent', function() { }); it('should not onTouchTap from when dragging beyond threshold', function() { - ReactEvent.putListener( + ReactEventEmitter.putListener( CHILD.id, ON_TOUCH_TAP_KEY, recordID.bind(null, CHILD.id) @@ -303,17 +303,17 @@ describe('ReactEvent', function() { it('should bubble onTouchTap', function() { - ReactEvent.putListener( + ReactEventEmitter.putListener( CHILD.id, ON_TOUCH_TAP_KEY, recordID.bind(null, CHILD.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( PARENT.id, ON_TOUCH_TAP_KEY, recordID.bind(null, PARENT.id) ); - ReactEvent.putListener( + ReactEventEmitter.putListener( GRANDPARENT.id, ON_TOUCH_TAP_KEY, recordID.bind(null, GRANDPARENT.id) diff --git a/src/core/__tests__/ReactNativeComponent-test.js b/src/core/__tests__/ReactNativeComponent-test.js index 80b4e793e05a8..2219567ffc4c3 100644 --- a/src/core/__tests__/ReactNativeComponent-test.js +++ b/src/core/__tests__/ReactNativeComponent-test.js @@ -242,7 +242,7 @@ describe('ReactNativeComponent', function() { describe('unmountComponent', function() { it("should clean up listeners", function() { var React = require('React'); - var ReactEvent = require('ReactEvent'); + var ReactEventEmitter = require('ReactEventEmitter'); var container = document.createElement('div'); document.documentElement.appendChild(container); @@ -253,11 +253,15 @@ describe('ReactNativeComponent', function() { var rootNode = instance.getDOMNode(); var rootNodeID = rootNode.id; - expect(ReactEvent.getListener(rootNodeID, 'onClick')).toBe(callback); + expect( + ReactEventEmitter.getListener(rootNodeID, 'onClick') + ).toBe(callback); React.unmountAndReleaseReactRootNode(container); - expect(ReactEvent.getListener(rootNodeID, 'onClick')).toBe(undefined); + expect( + ReactEventEmitter.getListener(rootNodeID, 'onClick') + ).toBe(undefined); }); }); diff --git a/src/eventPlugins/__tests__/AnalyticsEventPlugin-test.js b/src/eventPlugins/__tests__/AnalyticsEventPlugin-test.js index cd135cbf276c7..9dc33d9d5b89e 100644 --- a/src/eventPlugins/__tests__/AnalyticsEventPlugin-test.js +++ b/src/eventPlugins/__tests__/AnalyticsEventPlugin-test.js @@ -23,7 +23,7 @@ require('mock-modules') .dontMock('AnalyticsEventPluginFactory') .dontMock('EventPluginHub') .dontMock('React') - .dontMock('ReactEvent') + .dontMock('ReactEventEmitter') .dontMock('ReactEventTopLevelCallback') .dontMock('ReactInstanceHandles') .dontMock('ReactTestUtils'); @@ -32,11 +32,11 @@ var AnalyticsEventPluginFactory = require('AnalyticsEventPluginFactory'); var EventPluginHub = require('EventPluginHub'); var mocks = require('mocks'); var React = require('React'); -var ReactEvent = require('ReactEvent'); +var ReactEventEmitter = require('ReactEventEmitter'); var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); var ReactTestUtils = require('ReactTestUtils'); -ReactEvent.ensureListening(false, ReactEventTopLevelCallback); +ReactEventEmitter.ensureListening(false, ReactEventTopLevelCallback); describe('AnalyticsEventPlugin', function() { it('should count events correctly', function() { diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index aa9fdd31b8333..a3520ede64d22 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -19,7 +19,7 @@ var EventConstants = require('EventConstants'); var React = require('React'); var ReactComponent = require('ReactComponent'); -var ReactEvent = require('ReactEvent'); +var ReactEventEmitter = require('ReactEventEmitter'); var ReactTextComponent = require('ReactTextComponent'); var ge = require('ge'); @@ -191,7 +191,9 @@ var ReactTestUtils = { */ simulateEventOnNode: function(topLevelType, node, fakeNativeEvent) { var virtualHandler = - ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType); + ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( + topLevelType + ); fakeNativeEvent.target = node; virtualHandler(fakeNativeEvent); }, @@ -209,7 +211,9 @@ var ReactTestUtils = { throw new Error('Simulating event on non-rendered component'); } var virtualHandler = - ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType); + ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( + topLevelType + ); var node = ge(reactRootID); fakeNativeEvent.target = node; /* jsdom is returning nodes without id's - fixing that issue here. */ From 83101b878ed55b9c43ffd49311654e02bd738c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20O=E2=80=99Shannessy?= Date: Tue, 4 Jun 2013 15:53:34 -0700 Subject: [PATCH 19/25] Add license headers to new files --- src/core/__tests__/ReactIdentity-test.js | 14 ++++++++++++++ src/core/__tests__/ReactMount-test.js | 14 ++++++++++++++ src/utils/__tests__/mapChildren-test.js | 14 ++++++++++++++ src/utils/mapChildren.js | 14 ++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index 25f25ae2132d9..f65a53a170f59 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -1,4 +1,18 @@ /** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * * @jsx React.DOM * @emails react-core */ diff --git a/src/core/__tests__/ReactMount-test.js b/src/core/__tests__/ReactMount-test.js index de7e6d8b589bd..d48aa1c0ee90d 100644 --- a/src/core/__tests__/ReactMount-test.js +++ b/src/core/__tests__/ReactMount-test.js @@ -1,4 +1,18 @@ /** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * * @jsx React.DOM * @emails react-core */ diff --git a/src/utils/__tests__/mapChildren-test.js b/src/utils/__tests__/mapChildren-test.js index 2d0bfec5a9c03..7933e0c211ece 100644 --- a/src/utils/__tests__/mapChildren-test.js +++ b/src/utils/__tests__/mapChildren-test.js @@ -1,4 +1,18 @@ /** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * * @emails react-core * @jsx React.DOM */ diff --git a/src/utils/mapChildren.js b/src/utils/mapChildren.js index 27079e8930e59..a357eefa06367 100644 --- a/src/utils/mapChildren.js +++ b/src/utils/mapChildren.js @@ -1,4 +1,18 @@ /** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * * @providesModule mapChildren */ From fac24d462f1c7b654278cd1af5087b3b34d55ce8 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Wed, 5 Jun 2013 14:47:17 -0700 Subject: [PATCH 20/25] React: Add @typechecks to `CallbackRegistry` --- src/event/CallbackRegistry.js | 36 +++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/event/CallbackRegistry.js b/src/event/CallbackRegistry.js index 4d157653bdfe6..a0c2b5aca5098 100644 --- a/src/event/CallbackRegistry.js +++ b/src/event/CallbackRegistry.js @@ -14,6 +14,7 @@ * limitations under the License. * * @providesModule CallbackRegistry + * @typechecks */ "use strict"; @@ -22,18 +23,21 @@ var listenerBank = {}; /** * Stores "listeners" by `registrationName`/`id`. There should be at most one - * "listener" per `registrationName/id` in the `listenerBank`. - * Access listeners via `listenerBank[registrationName][id]` + * "listener" per `registrationName`/`id` in the `listenerBank`. * - * @constructor CallbackRegistry + * Access listeners via `listenerBank[registrationName][id]`. + * + * @class CallbackRegistry + * @internal */ var CallbackRegistry = { /** - * Stores `listener` at `listenerBank[registrationName][id]. Is idempotent. - * @param {string} domID The id of the DOM node. - * @param {string} registrationName The name of listener (`onClick` etc). - * @param {Function} listener The callback to to store. + * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent. + * + * @param {string} id ID of the DOM node. + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @param {?function} listener The callback to store. */ putListener: function(id, registrationName, listener) { var bankForRegistrationName = @@ -42,9 +46,9 @@ var CallbackRegistry = { }, /** - * @param {string} id. - * @param {string} registrationName Name of registration (`onClick` etc). - * @return {Function?} The Listener + * @param {string} id ID of the DOM node. + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @return {?function} The stored callback. */ getListener: function(id, registrationName) { var bankForRegistrationName = listenerBank[registrationName]; @@ -52,9 +56,10 @@ var CallbackRegistry = { }, /** - * Deletes the listener from the registration bank. - * @param {string} id - * @param {string} registrationName (`onClick` etc). + * Deletes a listener from the registration bank. + * + * @param {string} id ID of the DOM node. + * @param {string} registrationName Name of listener (e.g. `onClick`). */ deleteListener: function(id, registrationName) { var bankForRegistrationName = listenerBank[registrationName]; @@ -63,10 +68,13 @@ var CallbackRegistry = { } }, - // This is needed for tests only. Do not use in real life + /** + * This is needed for tests only. Do not use! + */ __purge: function() { listenerBank = {}; } + }; module.exports = CallbackRegistry; From 153fd9246ed5baea0c2da006598667649249b1f2 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Wed, 5 Jun 2013 14:47:21 -0700 Subject: [PATCH 21/25] [React] Don't use autoMockOff --- src/core/__tests__/ReactIdentity-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index f65a53a170f59..8e00f1ebb0443 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -26,7 +26,7 @@ var reactComponentExpect; describe('ReactIdentity', function() { beforeEach(function() { - require('mock-modules').autoMockOff().dumpCache(); + require('mock-modules').dumpCache(); React = require('React'); ReactTestUtils = require('ReactTestUtils'); reactComponentExpect = require('reactComponentExpect'); From 36d8ce8fabdd770cf455b07ee8e7c6880fadc57f Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Thu, 6 Jun 2013 14:30:52 -0700 Subject: [PATCH 22/25] [React] remove deprecated Component.update() Summary: Since grepping for `update` and `updateAll` is pretty hard, I had these these functions call through but complain loudly. This noisy call through has been in prod for over a week and I haven't heard any complains, so let's take it out altogether. --- src/core/ReactComponent.js | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index bc3769df9283a..e59c37fd699a8 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -480,24 +480,4 @@ var ReactComponent = { }; -function logDeprecated(msg) { - if (__DEV__) { - throw new Error(msg); - } else { - console && console.warn && console.warn(msg); - } -} - -/** - * @deprecated - */ -ReactComponent.Mixin.update = function(props) { - logDeprecated('this.update() is deprecated. Use this.setProps()'); - this.setProps(props); -}; -ReactComponent.Mixin.updateAll = function(props) { - logDeprecated('this.updateAll() is deprecated. Use this.replaceProps()'); - this.replaceProps(props); -}; - module.exports = ReactComponent; From ba6fea1bf5eaab71c0abec7d55824419bcd4f3f4 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Thu, 6 Jun 2013 14:40:30 -0700 Subject: [PATCH 23/25] Simplify Event Core Summary: This makes a few changes to React Core, most notably `ReactEventEmitter` and `ReactEventTopLevelCallback`. - Changed `ReactEventEmitter` to use `EventListener` (instead of `NormalizedEventListener`). - Deleted `NormalizedEventListener` (which was previously broken). - Created `getEventTarget` which is used to get a normalized `target` from a native event. - Changed `ReactEventTopLevelCallback` to use `getEventTarget`. - Renamed `abstractEventType` to `reactEventType` in `AbstractEvent`. - Reanmed `abstractTargetID` to `reactTargetID` in `AbstractEvent`. - Removed `originatingTopLevelEventType` from `AbstractEvent` (unused and violates encapsulation). - Removed `nativeEvent.target === window` check when refreshing authoritative scroll values (unnecessary). This actually fixes React because `NormalizedEventListener` does not currently do what it promises to do (which is normalizing `target` on the native event). The `target` event is read-only on native events. This also revises documentation and adds `@typechecks` to a few modules. NOTE: Most importantly, this sets the stage for replacing `AbstractEvent` with `ReactEvent` and subclasses, piecemeal. --- src/core/ReactEventEmitter.js | 305 ++++++++++-------- src/core/ReactEventTopLevelCallback.js | 49 +-- src/core/ReactInstanceHandles.js | 15 +- src/dom/getDOMNodeID.js | 5 +- src/dom/getEventTarget.js | 39 +++ src/event/AbstractEvent.js | 18 +- src/event/EventPluginHub.js | 61 ++-- src/event/EventPropagators.js | 19 +- src/event/NormalizedEventListener.js | 84 ----- .../AnalyticsEventPluginFactory.js | 19 +- src/eventPlugins/EnterLeaveEventPlugin.js | 46 +-- src/eventPlugins/ResponderEventPlugin.js | 107 +++--- src/eventPlugins/SimpleEventPlugin.js | 88 ++--- src/eventPlugins/TapEventPlugin.js | 22 +- .../__tests__/ResponderEventPlugin-test.js | 20 +- 15 files changed, 457 insertions(+), 440 deletions(-) create mode 100644 src/dom/getEventTarget.js delete mode 100644 src/event/NormalizedEventListener.js diff --git a/src/core/ReactEventEmitter.js b/src/core/ReactEventEmitter.js index 5e722606c536b..dc2a1dd8f99d5 100644 --- a/src/core/ReactEventEmitter.js +++ b/src/core/ReactEventEmitter.js @@ -14,33 +14,44 @@ * limitations under the License. * * @providesModule ReactEventEmitter + * @typechecks */ "use strict"; var BrowserEnv = require('BrowserEnv'); var EventConstants = require('EventConstants'); +var EventListener = require('EventListener'); var EventPluginHub = require('EventPluginHub'); var ExecutionEnvironment = require('ExecutionEnvironment'); -var NormalizedEventListener = require('NormalizedEventListener'); var invariant = require('invariant'); var isEventSupported = require('isEventSupported'); -var registrationNames = EventPluginHub.registrationNames; -var topLevelTypes = EventConstants.topLevelTypes; -var listen = NormalizedEventListener.listen; -var capture = NormalizedEventListener.capture; - /** - * `ReactEventEmitter` is used to attach top-level event listeners. For example: + * Summary of `ReactEventEmitter` event handling: * - * ReactEventEmitter.putListener('myID', 'onClick', myFunction); + * - We trap low level 'top-level' events. + * + * - We dedupe cross-browser event names into these 'top-level types' so that + * `DOMMouseScroll` or `mouseWheel` both become `topMouseWheel`. + * + * - At this point we have native browser events with the top-level type that + * was used to catch it at the top-level. + * + * - We continuously stream these native events (and their respective top-level + * types) to the event plugin system `EventPluginHub` and ask the plugin + * system if it was able to extract `AbstractEvent` objects. `AbstractEvent` + * objects are the events that applications actually deal with - they are not + * native browser events but cross-browser wrappers. + * + * - When returning the `AbstractEvent` objects, `EventPluginHub` will make + * sure each abstract event is annotated with "dispatches", which are the + * sequence of listeners (and IDs) that care about the event. + * + * - These `AbstractEvent` objects are fed back into the event plugin system, + * which in turn executes these dispatches. * - * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'. - */ - -/** * Overview of React and the event system: * * . @@ -72,36 +83,23 @@ var capture = NormalizedEventListener.capture; */ /** - * We listen for bubbled touch events on the document object. - * - * Firefox v8.01 (and possibly others) exhibited strange behavior when mounting - * `onmousemove` events at some node that was not the document element. The - * symptoms were that if your mouse is not moving over something contained - * within that mount point (for example on the background) the top-level - * listeners for `onmousemove` won't be called. However, if you register the - * `mousemove` on the document object, then it will of course catch all - * `mousemove`s. This along with iOS quirks, justifies restricting top-level - * listeners to the document object only, at least for these movement types of - * events and possibly all events. - * - * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html - * - * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but - * they bubble to document. - * - * @see http://www.quirksmode.org/dom/events/keys.html. + * Whether or not `ensureListening` has been invoked. + * @type {boolean} + * @private */ - var _isListening = false; /** - * Traps top-level events that bubble. Delegates to the main dispatcher - * `handleTopLevel` after performing some basic normalization via - * `TopLevelCallbackCreator.createTopLevelCallback`. + * Traps top-level events by using event bubbling. + * + * @param {string} topLevelType Record from `EventConstants`. + * @param {string} handlerBaseName Event name (e.g. "click"). + * @param {DOMEventTarget} element Element on which to attach listener. + * @internal */ -function trapBubbledEvent(topLevelType, handlerBaseName, onWhat) { - listen( - onWhat, +function trapBubbledEvent(topLevelType, handlerBaseName, element) { + EventListener.listen( + element, handlerBaseName, ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( topLevelType @@ -111,10 +109,15 @@ function trapBubbledEvent(topLevelType, handlerBaseName, onWhat) { /** * Traps a top-level event by using event capturing. + * + * @param {string} topLevelType Record from `EventConstants`. + * @param {string} handlerBaseName Event name (e.g. "click"). + * @param {DOMEventTarget} element Element on which to attach listener. + * @internal */ -function trapCapturedEvent(topLevelType, handlerBaseName, onWhat) { - capture( - onWhat, +function trapCapturedEvent(topLevelType, handlerBaseName, element) { + EventListener.capture( + element, handlerBaseName, ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( topLevelType @@ -123,62 +126,51 @@ function trapCapturedEvent(topLevelType, handlerBaseName, onWhat) { } /** - * Listens to document scroll and window resize events that may change the - * document scroll values. We store those results so as to discourage - * application code from asking the DOM itself which could trigger additional - * reflows. + * Listens to window scroll and resize events. We cache scroll values so that + * application code can access them without triggering reflows. + * + * NOTE: Scroll events do not bubble. + * + * @private + * @see http://www.quirksmode.org/dom/events/scroll.html */ -function registerDocumentScrollListener() { - listen(window, 'scroll', function(nativeEvent) { - if (nativeEvent.target === window) { - BrowserEnv.refreshAuthoritativeScrollValues(); - } - }); -} - -function registerDocumentResizeListener() { - listen(window, 'resize', function(nativeEvent) { - if (nativeEvent.target === window) { - BrowserEnv.refreshAuthoritativeScrollValues(); - } - }); +function registerScrollValueMonitoring() { + var refresh = BrowserEnv.refreshAuthoritativeScrollValues; + EventListener.listen(window, 'scroll', refresh); + EventListener.listen(window, 'resize', refresh); } /** - * Summary of `ReactEventEmitter` event handling: - * - * - We trap low level 'top-level' events. - * - * - We dedupe cross-browser event names into these 'top-level types' so that - * `DOMMouseScroll` or `mouseWheel` both become `topMouseWheel`. - * - * - At this point we have native browser events with the top-level type that - * was used to catch it at the top-level. + * We listen for bubbled touch events on the document object. * - * - We continuously stream these native events (and their respective top-level - * types) to the event plugin system `EventPluginHub` and ask the plugin - * system if it was able to extract `AbstractEvent` objects. `AbstractEvent` - * objects are the events that applications actually deal with - they are not - * native browser events but cross-browser wrappers. + * Firefox v8.01 (and possibly others) exhibited strange behavior when mounting + * `onmousemove` events at some node that was not the document element. The + * symptoms were that if your mouse is not moving over something contained + * within that mount point (for example on the background) the top-level + * listeners for `onmousemove` won't be called. However, if you register the + * `mousemove` on the document object, then it will of course catch all + * `mousemove`s. This along with iOS quirks, justifies restricting top-level + * listeners to the document object only, at least for these movement types of + * events and possibly all events. * - * - When returning the `AbstractEvent` objects, `EventPluginHub` will make - * sure each abstract event is annotated with "dispatches", which are the - * sequence of listeners (and IDs) that care about the event. + * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html * - * - These `AbstractEvent` objects are fed back into the event plugin system, - * which in turn executes these dispatches. + * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but + * they bubble to document. * + * @param {boolean} touchNotMouse Listen to touch events instead of mouse. * @private + * @see http://www.quirksmode.org/dom/events/keys.html. */ function listenAtTopLevel(touchNotMouse) { invariant( !_isListening, 'listenAtTopLevel(...): Cannot setup top-level listener more than once.' ); + var topLevelTypes = EventConstants.topLevelTypes; var mountAt = document; - registerDocumentScrollListener(); - registerDocumentResizeListener(); + registerScrollValueMonitoring(); trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt); trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt); trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt); @@ -226,82 +218,111 @@ function listenAtTopLevel(touchNotMouse) { } /** - * This is the heart of `ReactEventEmitter`. It simply streams the top-level - * native events to `EventPluginHub`. + * `ReactEventEmitter` is used to attach top-level event listeners. For example: + * + * ReactEventEmitter.putListener('myID', 'onClick', myFunction); + * + * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'. * - * @param {object} topLevelType Record from `EventConstants`. - * @param {Event} nativeEvent A Standard Event with fixed `target` property. - * @param {DOMElement} renderedTarget Element of interest to the framework. - * @param {string} renderedTargetID string ID of `renderedTarget`. * @internal */ -function handleTopLevel( - topLevelType, - nativeEvent, - renderedTargetID, - renderedTarget) { - var abstractEvents = EventPluginHub.extractAbstractEvents( - topLevelType, - nativeEvent, - renderedTargetID, - renderedTarget - ); +var ReactEventEmitter = { - // The event queue being processed in the same cycle allows preventDefault. - EventPluginHub.enqueueAbstractEvents(abstractEvents); - EventPluginHub.processAbstractEventQueue(); -} + /** + * React references `ReactEventTopLevelCallback` using this property in order + * to allow dependency injection via `ensureListening`. + */ + TopLevelCallbackCreator: null, -function setEnabled(enabled) { - invariant( - ExecutionEnvironment.canUseDOM, - 'setEnabled(...): Cannot toggle event listening in a Worker thread. This ' + - 'is likely a bug in the framework. Please report immediately.' - ); - ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled); -} + /** + * Ensures that top-level event delegation listeners are installed. + * + * There are issues with listening to both touch events and mouse events on + * the top-level, so we make the caller choose which one to listen to. (If + * there's a touch top-level listeners, anchors don't receive clicks for some + * reason, and only in some cases). + * + * @param {boolean} touchNotMouse Listen to touch events instead of mouse. + * @param {object} TopLevelCallbackCreator + */ + ensureListening: function(touchNotMouse, TopLevelCallbackCreator) { + invariant( + ExecutionEnvironment.canUseDOM, + 'ensureListening(...): Cannot toggle event listening in a Worker ' + + 'thread. This is likely a bug in the framework. Please report ' + + 'immediately.' + ); + if (!_isListening) { + ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator; + listenAtTopLevel(touchNotMouse); + _isListening = true; + } + }, -function isEnabled() { - return ReactEventEmitter.TopLevelCallbackCreator.isEnabled(); -} + /** + * Sets whether or not any created callbacks should be enabled. + * + * @param {boolean} enabled True if callbacks should be enabled. + */ + setEnabled: function(enabled) { + invariant( + ExecutionEnvironment.canUseDOM, + 'setEnabled(...): Cannot toggle event listening in a Worker thread. ' + + 'This is likely a bug in the framework. Please report immediately.' + ); + if (ReactEventEmitter.TopLevelCallbackCreator) { + ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled); + } + }, -/** - * Ensures that top-level event delegation listeners are listening at `mountAt`. - * There are issues with listening to both touch events and mouse events on the - * top-level, so we make the caller choose which one to listen to. (If there's a - * touch top-level listeners, anchors don't receive clicks for some reason, and - * only in some cases). - * - * @param {boolean} touchNotMouse Listen to touch events instead of mouse. - * @param {object} TopLevelCallbackCreator Module that can create top-level - * callback handlers. - * @internal - */ -function ensureListening(touchNotMouse, TopLevelCallbackCreator) { - invariant( - ExecutionEnvironment.canUseDOM, - 'ensureListening(...): Cannot toggle event listening in a Worker thread. ' + - 'This is likely a bug in the framework. Please report immediately.' - ); - if (!_isListening) { - ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator; - listenAtTopLevel(touchNotMouse); - _isListening = true; - } -} + /** + * @return {boolean} True if callbacks are enabled. + */ + isEnabled: function() { + return !!( + ReactEventEmitter.TopLevelCallbackCreator && + ReactEventEmitter.TopLevelCallbackCreator.isEnabled() + ); + }, + + /** + * Streams a fired top-level event to `EventPluginHub` where plugins have the + * opportunity to create `ReactEvent`s to be dispatched. + * + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + */ + handleTopLevel: function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent) { + var abstractEvents = EventPluginHub.extractAbstractEvents( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent + ); + + // Event queue being processed in the same cycle allows `preventDefault`. + EventPluginHub.enqueueAbstractEvents(abstractEvents); + EventPluginHub.processAbstractEventQueue(); + }, + + registrationNames: EventPluginHub.registrationNames, -var ReactEventEmitter = { - TopLevelCallbackCreator: null, // Injectable callback creator. - handleTopLevel: handleTopLevel, - setEnabled: setEnabled, - isEnabled: isEnabled, - ensureListening: ensureListening, - registrationNames: registrationNames, putListener: EventPluginHub.putListener, + getListener: EventPluginHub.getListener, + deleteAllListeners: EventPluginHub.deleteAllListeners, + trapBubbledEvent: trapBubbledEvent, + trapCapturedEvent: trapCapturedEvent + }; module.exports = ReactEventEmitter; diff --git a/src/core/ReactEventTopLevelCallback.js b/src/core/ReactEventTopLevelCallback.js index 2b72eceb32313..689a96c559206 100644 --- a/src/core/ReactEventTopLevelCallback.js +++ b/src/core/ReactEventTopLevelCallback.js @@ -14,6 +14,7 @@ * limitations under the License. * * @providesModule ReactEventTopLevelCallback + * @typechecks */ "use strict"; @@ -23,51 +24,59 @@ var ReactEventEmitter = require('ReactEventEmitter'); var ReactInstanceHandles = require('ReactInstanceHandles'); var getDOMNodeID = require('getDOMNodeID'); +var getEventTarget = require('getEventTarget'); +/** + * @type {boolean} + * @private + */ var _topLevelListenersEnabled = true; +/** + * Top-level callback creator used to implement event handling using delegation. + * This is used via dependency injection in `ReactEventEmitter.ensureListening`. + */ var ReactEventTopLevelCallback = { /** - * @param {boolean} enabled Whether or not all callbacks that have ever been - * created with this module should be enabled. + * Sets whether or not any created callbacks should be enabled. + * + * @param {boolean} enabled True if callbacks should be enabled. */ setEnabled: function(enabled) { _topLevelListenersEnabled = !!enabled; }, + /** + * @return {boolean} True if callbacks are enabled. + */ isEnabled: function() { return _topLevelListenersEnabled; }, /** - * For a given `topLevelType`, creates a callback that could be added as a - * listener to the document. That top level callback will simply fix the - * native events before invoking `handleTopLevel`. + * Creates a callback for the supplied `topLevelType` that could be added as + * a listener to the document. The callback computes a `topLevelTarget` which + * should be the root node of a mounted React component where the listener + * is attached. * - * - Raw native events cannot be trusted to describe their targets correctly - * so we expect that the argument to the nested function has already been - * fixed. But the `target` property may not be something of interest to - * React, so we find the most suitable target. But even at that point, DOM - * Elements (the target ) can't be trusted to describe their IDs correctly - * so we obtain the ID in a reliable manner and pass it to - * `handleTopLevel`. The target/id that we found to be relevant to our - * framework are called `renderedTarget`/`renderedTargetID` respectively. + * @param {string} topLevelType Record from `EventConstants`. + * @return {function} Callback for handling top-level events. */ createTopLevelCallback: function(topLevelType) { - return function(fixedNativeEvent) { + return function(nativeEvent) { if (!_topLevelListenersEnabled) { return; } - var renderedTarget = ReactInstanceHandles.getFirstReactDOM( - fixedNativeEvent.target + var topLevelTarget = ReactInstanceHandles.getFirstReactDOM( + getEventTarget(nativeEvent) ) || ExecutionEnvironment.global; - var renderedTargetID = getDOMNodeID(renderedTarget); + var topLevelTargetID = getDOMNodeID(topLevelTarget) || ''; ReactEventEmitter.handleTopLevel( topLevelType, - fixedNativeEvent, - renderedTargetID, - renderedTarget + topLevelTarget, + topLevelTargetID, + nativeEvent ); }; } diff --git a/src/core/ReactInstanceHandles.js b/src/core/ReactInstanceHandles.js index 27c4e1b575f4e..7528cb7fb08eb 100644 --- a/src/core/ReactInstanceHandles.js +++ b/src/core/ReactInstanceHandles.js @@ -14,6 +14,7 @@ * limitations under the License. * * @providesModule ReactInstanceHandles + * @typechecks */ "use strict"; @@ -57,13 +58,13 @@ function isValidID(id) { /** * True if the supplied `node` is rendered by React. * - * @param {DOMElement} node DOM Element to check. + * @param {DOMEventTarget} node DOM Element to check. * @return {boolean} True if the DOM Element appears to be rendered by React. * @private */ function isRenderedByReact(node) { var id = getDOMNodeID(node); - return id && id.charAt(0) === SEPARATOR; + return id ? id.charAt(0) === SEPARATOR : false; } /** @@ -140,8 +141,8 @@ var ReactInstanceHandles = { * Traverses up the ancestors of the supplied node to find a node that is a * DOM representation of a React component. * - * @param {DOMElement} node - * @return {?DOMElement} + * @param {?DOMEventTarget} node + * @return {?DOMEventTarget} * @internal */ getFirstReactDOM: function(node) { @@ -159,9 +160,9 @@ var ReactInstanceHandles = { * Finds a node with the supplied `id` inside of the supplied `ancestorNode`. * Exploits the ID naming scheme to perform the search quickly. * - * @param {DOMElement} ancestorNode Search from this root. + * @param {DOMEventTarget} ancestorNode Search from this root. * @pararm {string} id ID of the DOM representation of the component. - * @return {?DOMElement} DOM element with the supplied `id`, if one exists. + * @return {?DOMEventTarget} DOM node with the supplied `id`, if one exists. * @internal */ findComponentRoot: function(ancestorNode, id) { @@ -228,7 +229,7 @@ var ReactInstanceHandles = { * contains the React component with the supplied DOM ID. * * @param {string} id DOM ID of a React component. - * @return {string} DOM ID of the React component that is the root. + * @return {?string} DOM ID of the React component that is the root. * @internal */ getReactRootIDFromNodeID: function(id) { diff --git a/src/dom/getDOMNodeID.js b/src/dom/getDOMNodeID.js index fbd9cfdcbf9cd..1f4abe6974a8b 100644 --- a/src/dom/getDOMNodeID.js +++ b/src/dom/getDOMNodeID.js @@ -14,6 +14,7 @@ * limitations under the License. * * @providesModule getDOMNodeID + * @typechecks */ "use strict"; @@ -23,8 +24,8 @@ * control whose name or ID is "id". However, not all DOM nodes support * `getAttributeNode` (document - which is not a form) so that is checked first. * - * @param {Element} domNode DOM node element to return ID of. - * @returns {string} The ID of `domNode`. + * @param {DOMElement|DOMWindow|DOMDocument} domNode DOM node. + * @returns {string} ID of the supplied `domNode`. */ function getDOMNodeID(domNode) { if (domNode.getAttributeNode) { diff --git a/src/dom/getEventTarget.js b/src/dom/getEventTarget.js new file mode 100644 index 0000000000000..9529e3932f6d9 --- /dev/null +++ b/src/dom/getEventTarget.js @@ -0,0 +1,39 @@ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule getEventTarget + * @typechecks + */ + +var ExecutionEnvironment = require('ExecutionEnvironment'); + +/** + * Gets the target node from a native browser event by accounting for + * inconsistencies in browser DOM APIs. + * + * @param {object} nativeEvent Native browser event. + * @return {DOMEventTarget} Target node. + */ +function getEventTarget(nativeEvent) { + var target = + nativeEvent.target || + nativeEvent.srcElement || + ExecutionEnvironment.global; + // Safari may fire events on text nodes (Node.TEXT_NODE is 3). + // @see http://www.quirksmode.org/js/events_properties.html + return target.nodeType === 3 ? target.parentNode : target; +} + +module.exports = getEventTarget; diff --git a/src/event/AbstractEvent.js b/src/event/AbstractEvent.js index 9d31b19e39dd4..6930fdd8bf78d 100644 --- a/src/event/AbstractEvent.js +++ b/src/event/AbstractEvent.js @@ -44,14 +44,12 @@ var MAX_POOL_SIZE = 20; * unreliable native event. */ function AbstractEvent( - abstractEventType, - abstractTargetID, // Allows the abstract target to differ from native. - originatingTopLevelEventType, + reactEventType, + reactTargetID, // Allows the abstract target to differ from native. nativeEvent, data) { - this.type = abstractEventType; - this.abstractTargetID = abstractTargetID || ''; - this.originatingTopLevelEventType = originatingTopLevelEventType; + this.reactEventType = reactEventType; + this.reactTargetID = reactTargetID || ''; this.nativeEvent = nativeEvent; this.data = data; // TODO: Deprecate storing target - doesn't always make sense for some types @@ -263,12 +261,10 @@ AbstractEvent.persistentCloneOf = function(abstractEvent) { throwIf(!(abstractEvent instanceof AbstractEvent), CLONE_TYPE_ERR); } return new AbstractEvent( - abstractEvent.type, - abstractEvent.abstractTargetID, - abstractEvent.originatingTopLevelEventType, + abstractEvent.reactEventType, + abstractEvent.reactTargetID, abstractEvent.nativeEvent, - abstractEvent.data, - abstractEvent.target + abstractEvent.data ); }; diff --git a/src/event/EventPluginHub.js b/src/event/EventPluginHub.js index 5a24d557ce52c..52c3db0b61139 100644 --- a/src/event/EventPluginHub.js +++ b/src/event/EventPluginHub.js @@ -194,15 +194,16 @@ function recordAllRegistrationNames(eventType, PluginModule) { * @param {AbstractEvent} abstractEvent to look at */ function getPluginModuleForAbstractEvent(abstractEvent) { - if (abstractEvent.type.registrationName) { - return registrationNames[abstractEvent.type.registrationName]; + var reactEventType = abstractEvent.reactEventType; + if (reactEventType.registrationName) { + return registrationNames[reactEventType.registrationName]; } else { - for (var phase in abstractEvent.type.phasedRegistrationNames) { - if (!abstractEvent.type.phasedRegistrationNames.hasOwnProperty(phase)) { + for (var phase in reactEventType.phasedRegistrationNames) { + if (!reactEventType.phasedRegistrationNames.hasOwnProperty(phase)) { continue; } var PluginModule = registrationNames[ - abstractEvent.type.phasedRegistrationNames[phase] + reactEventType.phasedRegistrationNames[phase] ]; if (PluginModule) { return PluginModule; @@ -223,36 +224,36 @@ var deleteAllListeners = function(domID) { * Accepts the stream of top level native events, and gives every registered * plugin an opportunity to extract `AbstractEvent`s with annotated dispatches. * - * @param {Enum} topLevelType Record from `EventConstants`. - * @param {Event} nativeEvent A Standard Event with fixed `target` property. - * @param {Element} renderedTarget Element of interest to the framework, usually - * the same as `nativeEvent.target` but occasionally an element immediately - * above `nativeEvent.target` (the first DOM node recognized as one "rendered" - * by the framework at hand.) - * @param {string} renderedTargetID string ID of `renderedTarget`. + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of `AbstractEvent`s. */ -var extractAbstractEvents = - function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) { - var abstractEvents; - var plugins = injection.plugins; - var len = plugins.length; - for (var i = 0; i < len; i++) { - // Not every plugin in the ordering may be loaded at runtime. - var possiblePlugin = plugins[i]; - var extractedAbstractEvents = - possiblePlugin && - possiblePlugin.extractAbstractEvents( - topLevelType, - nativeEvent, - renderedTargetID, - renderedTarget - ); +var extractAbstractEvents = function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent) { + var abstractEvents; + var plugins = injection.plugins; + for (var i = 0, l = plugins.length; i < l; i++) { + // Not every plugin in the ordering may be loaded at runtime. + var possiblePlugin = plugins[i]; + if (possiblePlugin) { + var extractedAbstractEvents = possiblePlugin.extractAbstractEvents( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent + ); if (extractedAbstractEvents) { abstractEvents = accumulate(abstractEvents, extractedAbstractEvents); } } - return abstractEvents; - }; + } + return abstractEvents; +}; var enqueueAbstractEvents = function(abstractEvents) { if (abstractEvents) { diff --git a/src/event/EventPropagators.js b/src/event/EventPropagators.js index c5cfe6bfe5eff..e7534f7a2de74 100644 --- a/src/event/EventPropagators.js +++ b/src/event/EventPropagators.js @@ -58,7 +58,7 @@ var injection = { */ function listenerAtPhase(id, abstractEvent, propagationPhase) { var registrationName = - abstractEvent.type.phasedRegistrationNames[propagationPhase]; + abstractEvent.reactEventType.phasedRegistrationNames[propagationPhase]; return getListener(id, registrationName); } @@ -92,9 +92,9 @@ function accumulateDirectionalDispatches(domID, upwards, abstractEvent) { * have a different target. */ function accumulateTwoPhaseDispatchesSingle(abstractEvent) { - if (abstractEvent && abstractEvent.type.phasedRegistrationNames) { + if (abstractEvent && abstractEvent.reactEventType.phasedRegistrationNames) { injection.InstanceHandle.traverseTwoPhase( - abstractEvent.abstractTargetID, + abstractEvent.reactTargetID, accumulateDirectionalDispatches, abstractEvent ); @@ -105,11 +105,12 @@ function accumulateTwoPhaseDispatchesSingle(abstractEvent) { /** * Accumulates without regard to direction, does not look for phased * registration names. Same as `accumulateDirectDispatchesSingle` but without - * requiring that the `abstractTargetID` be the same as the dispatched ID. + * requiring that the `reactTargetID` be the same as the dispatched ID. */ function accumulateDispatches(id, ignoredDirection, abstractEvent) { - if (abstractEvent && abstractEvent.type.registrationName) { - var listener = getListener(id, abstractEvent.type.registrationName); + if (abstractEvent && abstractEvent.reactEventType.registrationName) { + var registrationName = abstractEvent.reactEventType.registrationName; + var listener = getListener(id, registrationName); if (listener) { abstractEvent._dispatchListeners = accumulate(abstractEvent._dispatchListeners, listener); @@ -120,12 +121,12 @@ function accumulateDispatches(id, ignoredDirection, abstractEvent) { /** * Accumulates dispatches on an `AbstractEvent`, but only for the - * `abstractTargetID`. + * `reactTargetID`. * @param {AbstractEvent} abstractEvent */ function accumulateDirectDispatchesSingle(abstractEvent) { - if (abstractEvent && abstractEvent.type.registrationName) { - accumulateDispatches(abstractEvent.abstractTargetID, null, abstractEvent); + if (abstractEvent && abstractEvent.reactEventType.registrationName) { + accumulateDispatches(abstractEvent.reactTargetID, null, abstractEvent); } } diff --git a/src/event/NormalizedEventListener.js b/src/event/NormalizedEventListener.js deleted file mode 100644 index 4888607b1580e..0000000000000 --- a/src/event/NormalizedEventListener.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule NormalizedEventListener - */ - -var EventListener = require('EventListener'); - -/** - * @param {?Event} eventParam Event parameter from an attached listener. - * @return {Event} Normalized event object. - * @private - */ -function normalizeEvent(eventParam) { - var nativeEvent = eventParam || window.event; - // In some browsers (OLD FF), setting the target throws an error. A good way - // to tell if setting the target will throw an error, is to check if the event - // has a `target` property. Safari events have a `target` but it's not always - // normalized. Even if a `target` property exists, it's good to only set the - // target property if we realize that a change will actually take place. - var hasTargetProperty = 'target' in nativeEvent; - var eventTarget = nativeEvent.target || nativeEvent.srcElement || window; - // Safari may fire events on text nodes (Node.TEXT_NODE is 3) - // @see http://www.quirksmode.org/js/events_properties.html - var textNodeNormalizedTarget = - (eventTarget.nodeType === 3) ? eventTarget.parentNode : eventTarget; - if (!hasTargetProperty || nativeEvent.target !== textNodeNormalizedTarget) { - // TODO: Normalize the object via `merge()` to work with strict mode. - nativeEvent.target = textNodeNormalizedTarget; - } - return nativeEvent; -} - -function createNormalizedCallback(cb) { - return function(unfixedNativeEvent) { - cb(normalizeEvent(unfixedNativeEvent)); - }; -} - -var NormalizedEventListener = { - - /** - * Listens to bubbled events on a DOM node. - * - * NOTE: The listener will be invoked with a normalized event object. - * - * @param {DOMElement} el DOM element to register listener on. - * @param {string} handlerBaseName Event name, e.g. "click". - * @param {function} cb Callback function. - * @public - */ - listen: function(el, handlerBaseName, cb) { - EventListener.listen(el, handlerBaseName, createNormalizedCallback(cb)); - }, - - /** - * Listens to captured events on a DOM node. - * - * NOTE: The listener will be invoked with a normalized event object. - * - * @param {DOMElement} el DOM element to register listener on. - * @param {string} handlerBaseName Event name, e.g. "click". - * @param {function} cb Callback function. - * @public - */ - capture: function(el, handlerBaseName, cb) { - EventListener.capture(el, handlerBaseName, createNormalizedCallback(cb)); - } - -}; - -module.exports = NormalizedEventListener; diff --git a/src/eventPlugins/AnalyticsEventPluginFactory.js b/src/eventPlugins/AnalyticsEventPluginFactory.js index 698c7fb2b4f48..b06859e00d664 100644 --- a/src/eventPlugins/AnalyticsEventPluginFactory.js +++ b/src/eventPlugins/AnalyticsEventPluginFactory.js @@ -136,20 +136,25 @@ if (__DEV__) { * This plugin does not really extract any abstract events. Rather it just looks * at the top level event and bumps up counters as appropriate * - * @see EventPluginHub.extractAbstractEvents + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of `AbstractEvent`s. + * @see {EventPluginHub.extractAbstractEvents} */ function extractAbstractEvents( topLevelType, - nativeEvent, - renderedTargetID, - renderedTarget) { + topLevelTarget, + topLevelTargetID, + nativeEvent) { var currentEvent = topLevelTypesToAnalyticsEvent[topLevelType]; - if (!currentEvent || !renderedTarget || !renderedTarget.attributes) { + if (!currentEvent || !topLevelTarget || !topLevelTarget.attributes) { return null; } - var analyticsIDAttribute = renderedTarget.attributes[ANALYTICS_ID]; - var analyticsEventsAttribute = renderedTarget.attributes[ANALYTICS_EVENTS]; + var analyticsIDAttribute = topLevelTarget.attributes[ANALYTICS_ID]; + var analyticsEventsAttribute = topLevelTarget.attributes[ANALYTICS_EVENTS]; if(!analyticsIDAttribute || !analyticsEventsAttribute) { return null; } diff --git a/src/eventPlugins/EnterLeaveEventPlugin.js b/src/eventPlugins/EnterLeaveEventPlugin.js index 51b02c42f3aec..d4a39131e71e0 100644 --- a/src/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/eventPlugins/EnterLeaveEventPlugin.js @@ -14,6 +14,7 @@ * limitations under the License. * * @providesModule EnterLeaveEventPlugin + * @typechecks */ "use strict"; @@ -36,28 +37,32 @@ var abstractEventTypes = { }; /** - * For almost every interaction we care about, there will be a top level - * `mouseOver` and `mouseOut` event that occur so we can usually only pay - * attention to one of the two (we'll pay attention to the `mouseOut` event) to - * avoid extracting a duplicate event. However, there's one interaction where - * there will be no `mouseOut` event to rely on - mousing from outside the - * browser *into* the chrome. We detect this scenario and only in that case, we - * use the `mouseOver` event. + * For almost every interaction we care about, there will be a top-level + * `mouseover` and `mouseout` event that occurs so only pay attention to one of + * the two (to avoid duplicate events). We use the `mouseout` event. * - * @see EventPluginHub.extractAbstractEvents + * However, there's one interaction where there will be no `mouseout` event to + * rely on - mousing from outside the browser *into* the chrome. We detect this + * scenario and only in that case, we use the `mouseover` event. + * + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of `AbstractEvent`s. + * @see {EventPluginHub.extractAbstractEvents} */ var extractAbstractEvents = function( topLevelType, - nativeEvent, - renderedTargetID, - renderedTarget) { - + topLevelTarget, + topLevelTargetID, + nativeEvent) { if (topLevelType === topLevelTypes.topMouseOver && (nativeEvent.relatedTarget || nativeEvent.fromElement)) { - return; + return null; } if (topLevelType !== topLevelTypes.topMouseOut && - topLevelType !== topLevelTypes.topMouseOver){ + topLevelType !== topLevelTypes.topMouseOver) { return null; // Must not be a mouse in or mouse out - ignoring. } @@ -65,32 +70,33 @@ var extractAbstractEvents = function( if (topLevelType === topLevelTypes.topMouseOut) { to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement) || ExecutionEnvironment.global; - from = renderedTarget; + from = topLevelTarget; } else { - to = renderedTarget; + to = topLevelTarget; from = ExecutionEnvironment.global; } // Nothing pertains to our managed components. - if (from === to ) { - return; + if (from === to) { + return null; } var fromID = from ? getDOMNodeID(from) : ''; var toID = to ? getDOMNodeID(to) : ''; + var leave = AbstractEvent.getPooled( abstractEventTypes.mouseLeave, fromID, - topLevelType, nativeEvent ); var enter = AbstractEvent.getPooled( abstractEventTypes.mouseEnter, toID, - topLevelType, nativeEvent ); + EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID); + return [leave, enter]; }; diff --git a/src/eventPlugins/ResponderEventPlugin.js b/src/eventPlugins/ResponderEventPlugin.js index d06d16cd0c94e..73d60b54c8655 100644 --- a/src/eventPlugins/ResponderEventPlugin.js +++ b/src/eventPlugins/ResponderEventPlugin.js @@ -162,14 +162,13 @@ var abstractEventTypes = { */ /** - * @param {TopLevelTypes} topLevelType Top level event type being examined. - * @param {DOMEvent} nativeEvent Native DOM event. + * @param {string} topLevelType Record from `EventConstants`. * @param {string} renderedTargetID ID of deepest React rendered element. - * - * @return {Accumulation} Extracted events. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of extracted `AbstractEvent`s. */ var setResponderAndExtractTransfer = - function(topLevelType, nativeEvent, renderedTargetID) { + function(topLevelType, renderedTargetID, nativeEvent) { var type; var shouldSetEventType = isStartish(topLevelType) ? abstractEventTypes.startShouldSetResponder : @@ -235,7 +234,6 @@ var setResponderAndExtractTransfer = return extracted; }; - /** * A transfer is a negotiation between a currently set responder and the next * element to claim responder status. Any start event could trigger a transfer @@ -253,51 +251,60 @@ function canTriggerTransfer(topLevelType) { (isPressing && isMoveish(topLevelType)); } -var extractAbstractEvents = - function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) { - var extracted; - // Must have missed an end event - reset the state here. - if (responderID && isStartish(topLevelType)) { - responderID = null; - } - if (isStartish(topLevelType)) { - isPressing = true; - } else if (isEndish(topLevelType)) { - isPressing = false; - } - if (canTriggerTransfer(topLevelType)) { - var transfer = setResponderAndExtractTransfer( - topLevelType, - nativeEvent, - renderedTargetID, - renderedTarget - ); - if (transfer) { - extracted = accumulate(extracted, transfer); - } - } - // Now that we know the responder is set correctly, we can dispatch - // responder type events (directly to the responder). - var type = isMoveish(topLevelType) ? abstractEventTypes.responderMove : - isEndish(topLevelType) ? abstractEventTypes.responderRelease : - isStartish(topLevelType) ? abstractEventTypes.responderStart : null; - if (type) { - var data = AbstractEvent.normalizePointerData(nativeEvent); - var gesture = AbstractEvent.getPooled( - type, - responderID, - topLevelType, - nativeEvent, - data - ); - EventPropagators.accumulateDirectDispatches(gesture); - extracted = accumulate(extracted, gesture); - } - if (type === abstractEventTypes.responderRelease) { - responderID = null; +/** + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of `AbstractEvent`s. + * @see {EventPluginHub.extractAbstractEvents} + */ +var extractAbstractEvents = function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent) { + var extracted; + // Must have missed an end event - reset the state here. + if (responderID && isStartish(topLevelType)) { + responderID = null; + } + if (isStartish(topLevelType)) { + isPressing = true; + } else if (isEndish(topLevelType)) { + isPressing = false; + } + if (canTriggerTransfer(topLevelType)) { + var transfer = setResponderAndExtractTransfer( + topLevelType, + topLevelTargetID, + nativeEvent + ); + if (transfer) { + extracted = accumulate(extracted, transfer); } - return extracted; - }; + } + // Now that we know the responder is set correctly, we can dispatch + // responder type events (directly to the responder). + var type = isMoveish(topLevelType) ? abstractEventTypes.responderMove : + isEndish(topLevelType) ? abstractEventTypes.responderRelease : + isStartish(topLevelType) ? abstractEventTypes.responderStart : null; + if (type) { + var data = AbstractEvent.normalizePointerData(nativeEvent); + var gesture = AbstractEvent.getPooled( + type, + responderID, + nativeEvent, + data + ); + EventPropagators.accumulateDirectDispatches(gesture); + extracted = accumulate(extracted, gesture); + } + if (type === abstractEventTypes.responderRelease) { + responderID = null; + } + return extracted; +}; /** * Event plugin for formalizing the negotiation between claiming locks on diff --git a/src/eventPlugins/SimpleEventPlugin.js b/src/eventPlugins/SimpleEventPlugin.js index 0453fde60a5cc..ab794a17b4231 100644 --- a/src/eventPlugins/SimpleEventPlugin.js +++ b/src/eventPlugins/SimpleEventPlugin.js @@ -155,6 +155,7 @@ var SimpleEventPlugin = { /** * Same as the default implementation, except cancels the event when return * value is false. + * * @param {AbstractEvent} AbstractEvent to handle * @param {function} Application-level callback * @param {string} domID DOM id to pass to the callback. @@ -168,48 +169,55 @@ var SimpleEventPlugin = { }, /** - * @see EventPluginHub.extractAbstractEvents + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of `AbstractEvent`s. + * @see {EventPluginHub.extractAbstractEvents} */ - extractAbstractEvents: - function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) { - var data; - var abstractEventType = - SimpleEventPlugin.topLevelTypesToAbstract[topLevelType]; - if (!abstractEventType) { - return null; - } - switch(topLevelType) { - case topLevelTypes.topMouseWheel: - data = AbstractEvent.normalizeMouseWheelData(nativeEvent); - break; - case topLevelTypes.topScroll: - data = AbstractEvent.normalizeScrollDataFromTarget(renderedTarget); - break; - case topLevelTypes.topClick: - case topLevelTypes.topDoubleClick: - case topLevelTypes.topChange: - case topLevelTypes.topDOMCharacterDataModified: - case topLevelTypes.topMouseDown: - case topLevelTypes.topMouseUp: - case topLevelTypes.topMouseMove: - case topLevelTypes.topTouchMove: - case topLevelTypes.topTouchStart: - case topLevelTypes.topTouchEnd: - data = AbstractEvent.normalizePointerData(nativeEvent); - break; - default: - data = null; - } - var abstractEvent = AbstractEvent.getPooled( - abstractEventType, - renderedTargetID, - topLevelType, - nativeEvent, - data - ); - EventPropagators.accumulateTwoPhaseDispatches(abstractEvent); - return abstractEvent; + extractAbstractEvents: function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent) { + var data; + var abstractEventType = + SimpleEventPlugin.topLevelTypesToAbstract[topLevelType]; + if (!abstractEventType) { + return null; } + switch(topLevelType) { + case topLevelTypes.topMouseWheel: + data = AbstractEvent.normalizeMouseWheelData(nativeEvent); + break; + case topLevelTypes.topScroll: + data = AbstractEvent.normalizeScrollDataFromTarget(topLevelTarget); + break; + case topLevelTypes.topClick: + case topLevelTypes.topDoubleClick: + case topLevelTypes.topChange: + case topLevelTypes.topDOMCharacterDataModified: + case topLevelTypes.topMouseDown: + case topLevelTypes.topMouseUp: + case topLevelTypes.topMouseMove: + case topLevelTypes.topTouchMove: + case topLevelTypes.topTouchStart: + case topLevelTypes.topTouchEnd: + data = AbstractEvent.normalizePointerData(nativeEvent); + break; + default: + data = null; + } + var abstractEvent = AbstractEvent.getPooled( + abstractEventType, + topLevelTargetID, + nativeEvent, + data + ); + EventPropagators.accumulateTwoPhaseDispatches(abstractEvent); + return abstractEvent; + } }; SimpleEventPlugin.topLevelTypesToAbstract = { diff --git a/src/eventPlugins/TapEventPlugin.js b/src/eventPlugins/TapEventPlugin.js index 141b4aea4e185..3759bc1cbd68b 100644 --- a/src/eventPlugins/TapEventPlugin.js +++ b/src/eventPlugins/TapEventPlugin.js @@ -14,6 +14,7 @@ * limitations under the License. * * @providesModule TapEventPlugin + * @typechecks */ "use strict"; @@ -46,26 +47,27 @@ var abstractEventTypes = { }; /** - * @see EventPluginHub.extractAbstractEvents + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of `AbstractEvent`s. + * @see {EventPluginHub.extractAbstractEvents} */ var extractAbstractEvents = function( topLevelType, - nativeEvent, - renderedTargetID, - renderedTarget) { - + topLevelTarget, + topLevelTargetID, + nativeEvent) { if (!isStartish(topLevelType) && !isEndish(topLevelType)) { return; } var abstractEvent; var dist = eventDistance(startCoords, nativeEvent); if (isEndish(topLevelType) && dist < tapMoveThreshold) { - var type = abstractEventTypes.touchTap; - var abstractTargetID = renderedTargetID; abstractEvent = AbstractEvent.getPooled( - type, - abstractTargetID, - topLevelType, + abstractEventTypes.touchTap, + topLevelTargetID, nativeEvent ); } diff --git a/src/eventPlugins/__tests__/ResponderEventPlugin-test.js b/src/eventPlugins/__tests__/ResponderEventPlugin-test.js index 2c8a51a9490e6..58f478991ac42 100644 --- a/src/eventPlugins/__tests__/ResponderEventPlugin-test.js +++ b/src/eventPlugins/__tests__/ResponderEventPlugin-test.js @@ -167,8 +167,9 @@ var existsInExtraction = function(extracted, test) { function assertGrantEvent(id, extracted) { var test = function(abstractEvent) { return abstractEvent instanceof AbstractEvent && - abstractEvent.type === responderAbstractEventTypes.responderGrant && - abstractEvent.abstractTargetID === id; + abstractEvent.reactEventType === + responderAbstractEventTypes.responderGrant && + abstractEvent.reactTargetID === id; }; expect(ResponderEventPlugin.getResponderID()).toBe(id); expect(existsInExtraction(extracted, test)).toBe(true); @@ -177,8 +178,9 @@ function assertGrantEvent(id, extracted) { function assertResponderMoveEvent(id, extracted) { var test = function(abstractEvent) { return abstractEvent instanceof AbstractEvent && - abstractEvent.type === responderAbstractEventTypes.responderMove && - abstractEvent.abstractTargetID === id; + abstractEvent.reactEventType === + responderAbstractEventTypes.responderMove && + abstractEvent.reactTargetID === id; }; expect(ResponderEventPlugin.getResponderID()).toBe(id); expect(existsInExtraction(extracted, test)).toBe(true); @@ -187,8 +189,9 @@ function assertResponderMoveEvent(id, extracted) { function assertTerminateEvent(id, extracted) { var test = function(abstractEvent) { return abstractEvent instanceof AbstractEvent && - abstractEvent.type === responderAbstractEventTypes.responderTerminate && - abstractEvent.abstractTargetID === id; + abstractEvent.reactEventType === + responderAbstractEventTypes.responderTerminate && + abstractEvent.reactTargetID === id; }; expect(ResponderEventPlugin.getResponderID()).not.toBe(id); expect(existsInExtraction(extracted, test)).toBe(true); @@ -197,8 +200,9 @@ function assertTerminateEvent(id, extracted) { function assertRelease(id, extracted) { var test = function(abstractEvent) { return abstractEvent instanceof AbstractEvent && - abstractEvent.type === responderAbstractEventTypes.responderRelease && - abstractEvent.abstractTargetID === id; + abstractEvent.reactEventType === + responderAbstractEventTypes.responderRelease && + abstractEvent.reactTargetID === id; }; expect(ResponderEventPlugin.getResponderID()).toBe(null); expect(existsInExtraction(extracted, test)).toBe(true); From 88923f61a70b76b805e1ebc738a3fc3c0a6afe62 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Thu, 6 Jun 2013 14:44:56 -0700 Subject: [PATCH 24/25] Improve Browser Support for `wheel` Event This improved browser support for the `wheel` event. - Try to use `wheel` event (DOM Level 3 Specification). - Fallback to `mousewheel` event. - Fallback to `DOMMouseWheel` (older Firefox). Also, since `wheel` is the standard event name, let's use that in React. NOTE: The tricky part was detecting if `wheel` is supported for IE9+ because `onwheel` does not exist. Test Plan: Execute the following in the console on a page with React: var React = require('React'); React.renderComponent(React.DOM.div({ style: { width: 10000, height: 10000 }, onWheel: function() { console.log('wheel'); } }, null), document.body); Verified that mousewheel events are logged to the console. Verified in IE8-10, Firefox, Chrome, and Safari. --- src/core/ReactEventEmitter.js | 20 +- src/diff.diff | 1337 +++++++++++++++++ src/dom/isEventSupported.js | 14 +- src/event/EventConstants.js | 2 +- .../AnalyticsEventPluginFactory.js | 4 +- src/eventPlugins/SimpleEventPlugin.js | 10 +- 6 files changed, 1371 insertions(+), 16 deletions(-) create mode 100644 src/diff.diff diff --git a/src/core/ReactEventEmitter.js b/src/core/ReactEventEmitter.js index dc2a1dd8f99d5..ccd74c08a8b7f 100644 --- a/src/core/ReactEventEmitter.js +++ b/src/core/ReactEventEmitter.js @@ -33,8 +33,8 @@ var isEventSupported = require('isEventSupported'); * * - We trap low level 'top-level' events. * - * - We dedupe cross-browser event names into these 'top-level types' so that - * `DOMMouseScroll` or `mouseWheel` both become `topMouseWheel`. + * - We dedupe cross-browser event names into these 'top-level types' (e.g. so + * that `wheel`, `mousewheel`, and `DOMMouseScroll` fire one event). * * - At this point we have native browser events with the top-level type that * was used to catch it at the top-level. @@ -178,7 +178,6 @@ function listenAtTopLevel(touchNotMouse) { trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt); trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt); trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt); - trapBubbledEvent(topLevelTypes.topMouseWheel, 'mousewheel', mountAt); if (touchNotMouse) { trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt); trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt); @@ -196,10 +195,17 @@ function listenAtTopLevel(touchNotMouse) { mountAt ); - // Firefox needs to capture a different mouse scroll event. - // @see http://www.quirksmode.org/dom/events/tests/scroll.html - trapBubbledEvent(topLevelTypes.topMouseWheel, 'DOMMouseScroll', mountAt); - // IE < 9 doesn't support capturing so just trap the bubbled event there. + if (isEventSupported('wheel')) { + trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt); + } else if (isEventSupported('mousewheel')) { + trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt); + } else { + // Firefox needs to capture a different mouse scroll event. + // @see http://www.quirksmode.org/dom/events/tests/scroll.html + trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt); + } + + // IE<9 does not support capturing so just trap the bubbled event there. if (isEventSupported('scroll', true)) { trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); } else { diff --git a/src/diff.diff b/src/diff.diff new file mode 100644 index 0000000000000..baf90e4871fb0 --- /dev/null +++ b/src/diff.diff @@ -0,0 +1,1337 @@ +diff --git a/static_upstream/react/core/ReactEventEmitter.js b/static_upstream/react/core/ReactEventEmitter.js +index 0f392c1..b028ad4 100644 +--- a/static_upstream/react/core/ReactEventEmitter.js ++++ b/static_upstream/react/core/ReactEventEmitter.js +@@ -1,32 +1,43 @@ + /** + * @providesModule ReactEventEmitter ++ * @typechecks + */ + + "use strict"; + + var BrowserEnv = require('BrowserEnv'); + var EventConstants = require('EventConstants'); ++var EventListener = require('EventListener'); + var EventPluginHub = require('EventPluginHub'); + var ExecutionEnvironment = require('ExecutionEnvironment'); +-var NormalizedEventListener = require('NormalizedEventListener'); + + var invariant = require('invariant'); + var isEventSupported = require('isEventSupported'); + +-var registrationNames = EventPluginHub.registrationNames; +-var topLevelTypes = EventConstants.topLevelTypes; +-var listen = NormalizedEventListener.listen; +-var capture = NormalizedEventListener.capture; +- + /** +- * `ReactEventEmitter` is used to attach top-level event listeners. For example: ++ * Summary of `ReactEventEmitter` event handling: + * +- * ReactEventEmitter.putListener('myID', 'onClick', myFunction); ++ * - We trap low level 'top-level' events. ++ * ++ * - We dedupe cross-browser event names into these 'top-level types' so that ++ * `DOMMouseScroll` or `mouseWheel` both become `topMouseWheel`. ++ * ++ * - At this point we have native browser events with the top-level type that ++ * was used to catch it at the top-level. ++ * ++ * - We continuously stream these native events (and their respective top-level ++ * types) to the event plugin system `EventPluginHub` and ask the plugin ++ * system if it was able to extract `AbstractEvent` objects. `AbstractEvent` ++ * objects are the events that applications actually deal with - they are not ++ * native browser events but cross-browser wrappers. ++ * ++ * - When returning the `AbstractEvent` objects, `EventPluginHub` will make ++ * sure each abstract event is annotated with "dispatches", which are the ++ * sequence of listeners (and IDs) that care about the event. ++ * ++ * - These `AbstractEvent` objects are fed back into the event plugin system, ++ * which in turn executes these dispatches. + * +- * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'. +- */ +- +-/** + * Overview of React and the event system: + * + * . +@@ -58,36 +69,23 @@ var capture = NormalizedEventListener.capture; + */ + + /** +- * We listen for bubbled touch events on the document object. +- * +- * Firefox v8.01 (and possibly others) exhibited strange behavior when mounting +- * `onmousemove` events at some node that was not the document element. The +- * symptoms were that if your mouse is not moving over something contained +- * within that mount point (for example on the background) the top-level +- * listeners for `onmousemove` won't be called. However, if you register the +- * `mousemove` on the document object, then it will of course catch all +- * `mousemove`s. This along with iOS quirks, justifies restricting top-level +- * listeners to the document object only, at least for these movement types of +- * events and possibly all events. +- * +- * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html +- * +- * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but +- * they bubble to document. +- * +- * @see http://www.quirksmode.org/dom/events/keys.html. ++ * Whether or not `ensureListening` has been invoked. ++ * @type {boolean} ++ * @private + */ +- + var _isListening = false; + + /** +- * Traps top-level events that bubble. Delegates to the main dispatcher +- * `handleTopLevel` after performing some basic normalization via +- * `TopLevelCallbackCreator.createTopLevelCallback`. ++ * Traps top-level events by using event bubbling. ++ * ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @param {string} handlerBaseName Event name (e.g. "click"). ++ * @param {DOMEventTarget} element Element on which to attach listener. ++ * @internal + */ +-function trapBubbledEvent(topLevelType, handlerBaseName, onWhat) { +- listen( +- onWhat, ++function trapBubbledEvent(topLevelType, handlerBaseName, element) { ++ EventListener.listen( ++ element, + handlerBaseName, + ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( + topLevelType +@@ -97,10 +95,15 @@ function trapBubbledEvent(topLevelType, handlerBaseName, onWhat) { + + /** + * Traps a top-level event by using event capturing. ++ * ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @param {string} handlerBaseName Event name (e.g. "click"). ++ * @param {DOMEventTarget} element Element on which to attach listener. ++ * @internal + */ +-function trapCapturedEvent(topLevelType, handlerBaseName, onWhat) { +- capture( +- onWhat, ++function trapCapturedEvent(topLevelType, handlerBaseName, element) { ++ EventListener.capture( ++ element, + handlerBaseName, + ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( + topLevelType +@@ -109,62 +112,51 @@ function trapCapturedEvent(topLevelType, handlerBaseName, onWhat) { + } + + /** +- * Listens to document scroll and window resize events that may change the +- * document scroll values. We store those results so as to discourage +- * application code from asking the DOM itself which could trigger additional +- * reflows. ++ * Listens to window scroll and resize events. We cache scroll values so that ++ * application code can access them without triggering reflows. ++ * ++ * NOTE: Scroll events do not bubble. ++ * ++ * @private ++ * @see http://www.quirksmode.org/dom/events/scroll.html + */ +-function registerDocumentScrollListener() { +- listen(window, 'scroll', function(nativeEvent) { +- if (nativeEvent.target === window) { +- BrowserEnv.refreshAuthoritativeScrollValues(); +- } +- }); +-} +- +-function registerDocumentResizeListener() { +- listen(window, 'resize', function(nativeEvent) { +- if (nativeEvent.target === window) { +- BrowserEnv.refreshAuthoritativeScrollValues(); +- } +- }); ++function registerScrollValueMonitoring() { ++ var refresh = BrowserEnv.refreshAuthoritativeScrollValues; ++ EventListener.listen(window, 'scroll', refresh); ++ EventListener.listen(window, 'resize', refresh); + } + + /** +- * Summary of `ReactEventEmitter` event handling: +- * +- * - We trap low level 'top-level' events. +- * +- * - We dedupe cross-browser event names into these 'top-level types' so that +- * `DOMMouseScroll` or `mouseWheel` both become `topMouseWheel`. +- * +- * - At this point we have native browser events with the top-level type that +- * was used to catch it at the top-level. ++ * We listen for bubbled touch events on the document object. + * +- * - We continuously stream these native events (and their respective top-level +- * types) to the event plugin system `EventPluginHub` and ask the plugin +- * system if it was able to extract `AbstractEvent` objects. `AbstractEvent` +- * objects are the events that applications actually deal with - they are not +- * native browser events but cross-browser wrappers. ++ * Firefox v8.01 (and possibly others) exhibited strange behavior when mounting ++ * `onmousemove` events at some node that was not the document element. The ++ * symptoms were that if your mouse is not moving over something contained ++ * within that mount point (for example on the background) the top-level ++ * listeners for `onmousemove` won't be called. However, if you register the ++ * `mousemove` on the document object, then it will of course catch all ++ * `mousemove`s. This along with iOS quirks, justifies restricting top-level ++ * listeners to the document object only, at least for these movement types of ++ * events and possibly all events. + * +- * - When returning the `AbstractEvent` objects, `EventPluginHub` will make +- * sure each abstract event is annotated with "dispatches", which are the +- * sequence of listeners (and IDs) that care about the event. ++ * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html + * +- * - These `AbstractEvent` objects are fed back into the event plugin system, +- * which in turn executes these dispatches. ++ * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but ++ * they bubble to document. + * ++ * @param {boolean} touchNotMouse Listen to touch events instead of mouse. + * @private ++ * @see http://www.quirksmode.org/dom/events/keys.html. + */ + function listenAtTopLevel(touchNotMouse) { + invariant( + !_isListening, + 'listenAtTopLevel(...): Cannot setup top-level listener more than once.' + ); ++ var topLevelTypes = EventConstants.topLevelTypes; + var mountAt = document; + +- registerDocumentScrollListener(); +- registerDocumentResizeListener(); ++ registerScrollValueMonitoring(); + trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt); + trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt); + trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt); +@@ -212,82 +204,111 @@ function listenAtTopLevel(touchNotMouse) { + } + + /** +- * This is the heart of `ReactEventEmitter`. It simply streams the top-level +- * native events to `EventPluginHub`. ++ * `ReactEventEmitter` is used to attach top-level event listeners. For example: ++ * ++ * ReactEventEmitter.putListener('myID', 'onClick', myFunction); ++ * ++ * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'. + * +- * @param {object} topLevelType Record from `EventConstants`. +- * @param {Event} nativeEvent A Standard Event with fixed `target` property. +- * @param {DOMElement} renderedTarget Element of interest to the framework. +- * @param {string} renderedTargetID string ID of `renderedTarget`. + * @internal + */ +-function handleTopLevel( +- topLevelType, +- nativeEvent, +- renderedTargetID, +- renderedTarget) { +- var abstractEvents = EventPluginHub.extractAbstractEvents( +- topLevelType, +- nativeEvent, +- renderedTargetID, +- renderedTarget +- ); ++var ReactEventEmitter = { + +- // The event queue being processed in the same cycle allows preventDefault. +- EventPluginHub.enqueueAbstractEvents(abstractEvents); +- EventPluginHub.processAbstractEventQueue(); +-} ++ /** ++ * React references `ReactEventTopLevelCallback` using this property in order ++ * to allow dependency injection via `ensureListening`. ++ */ ++ TopLevelCallbackCreator: null, + +-function setEnabled(enabled) { +- invariant( +- ExecutionEnvironment.canUseDOM, +- 'setEnabled(...): Cannot toggle event listening in a Worker thread. This ' + +- 'is likely a bug in the framework. Please report immediately.' +- ); +- ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled); +-} ++ /** ++ * Ensures that top-level event delegation listeners are installed. ++ * ++ * There are issues with listening to both touch events and mouse events on ++ * the top-level, so we make the caller choose which one to listen to. (If ++ * there's a touch top-level listeners, anchors don't receive clicks for some ++ * reason, and only in some cases). ++ * ++ * @param {boolean} touchNotMouse Listen to touch events instead of mouse. ++ * @param {object} TopLevelCallbackCreator ++ */ ++ ensureListening: function(touchNotMouse, TopLevelCallbackCreator) { ++ invariant( ++ ExecutionEnvironment.canUseDOM, ++ 'ensureListening(...): Cannot toggle event listening in a Worker ' + ++ 'thread. This is likely a bug in the framework. Please report ' + ++ 'immediately.' ++ ); ++ if (!_isListening) { ++ ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator; ++ listenAtTopLevel(touchNotMouse); ++ _isListening = true; ++ } ++ }, + +-function isEnabled() { +- return ReactEventEmitter.TopLevelCallbackCreator.isEnabled(); +-} ++ /** ++ * Sets whether or not any created callbacks should be enabled. ++ * ++ * @param {boolean} enabled True if callbacks should be enabled. ++ */ ++ setEnabled: function(enabled) { ++ invariant( ++ ExecutionEnvironment.canUseDOM, ++ 'setEnabled(...): Cannot toggle event listening in a Worker thread. ' + ++ 'This is likely a bug in the framework. Please report immediately.' ++ ); ++ if (ReactEventEmitter.TopLevelCallbackCreator) { ++ ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled); ++ } ++ }, + +-/** +- * Ensures that top-level event delegation listeners are listening at `mountAt`. +- * There are issues with listening to both touch events and mouse events on the +- * top-level, so we make the caller choose which one to listen to. (If there's a +- * touch top-level listeners, anchors don't receive clicks for some reason, and +- * only in some cases). +- * +- * @param {boolean} touchNotMouse Listen to touch events instead of mouse. +- * @param {object} TopLevelCallbackCreator Module that can create top-level +- * callback handlers. +- * @internal +- */ +-function ensureListening(touchNotMouse, TopLevelCallbackCreator) { +- invariant( +- ExecutionEnvironment.canUseDOM, +- 'ensureListening(...): Cannot toggle event listening in a Worker thread. ' + +- 'This is likely a bug in the framework. Please report immediately.' +- ); +- if (!_isListening) { +- ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator; +- listenAtTopLevel(touchNotMouse); +- _isListening = true; +- } +-} ++ /** ++ * @return {boolean} True if callbacks are enabled. ++ */ ++ isEnabled: function() { ++ return !!( ++ ReactEventEmitter.TopLevelCallbackCreator && ++ ReactEventEmitter.TopLevelCallbackCreator.isEnabled() ++ ); ++ }, ++ ++ /** ++ * Streams a fired top-level event to `EventPluginHub` where plugins have the ++ * opportunity to create `ReactEvent`s to be dispatched. ++ * ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @param {DOMEventTarget} topLevelTarget The listening component root node. ++ * @param {string} topLevelTargetID ID of `topLevelTarget`. ++ * @param {object} nativeEvent Native browser event. ++ */ ++ handleTopLevel: function( ++ topLevelType, ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent) { ++ var abstractEvents = EventPluginHub.extractAbstractEvents( ++ topLevelType, ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent ++ ); ++ ++ // Event queue being processed in the same cycle allows `preventDefault`. ++ EventPluginHub.enqueueAbstractEvents(abstractEvents); ++ EventPluginHub.processAbstractEventQueue(); ++ }, ++ ++ registrationNames: EventPluginHub.registrationNames, + +-var ReactEventEmitter = { +- TopLevelCallbackCreator: null, // Injectable callback creator. +- handleTopLevel: handleTopLevel, +- setEnabled: setEnabled, +- isEnabled: isEnabled, +- ensureListening: ensureListening, +- registrationNames: registrationNames, + putListener: EventPluginHub.putListener, ++ + getListener: EventPluginHub.getListener, ++ + deleteAllListeners: EventPluginHub.deleteAllListeners, ++ + trapBubbledEvent: trapBubbledEvent, ++ + trapCapturedEvent: trapCapturedEvent ++ + }; + + module.exports = ReactEventEmitter; +diff --git a/static_upstream/react/core/ReactEventTopLevelCallback.js b/static_upstream/react/core/ReactEventTopLevelCallback.js +index e3e3c52..f60e30f 100644 +--- a/static_upstream/react/core/ReactEventTopLevelCallback.js ++++ b/static_upstream/react/core/ReactEventTopLevelCallback.js +@@ -1,5 +1,6 @@ + /** + * @providesModule ReactEventTopLevelCallback ++ * @typechecks + */ + + "use strict"; +@@ -9,51 +10,59 @@ var ReactEventEmitter = require('ReactEventEmitter'); + var ReactInstanceHandles = require('ReactInstanceHandles'); + + var getDOMNodeID = require('getDOMNodeID'); ++var getEventTarget = require('getEventTarget'); + ++/** ++ * @type {boolean} ++ * @private ++ */ + var _topLevelListenersEnabled = true; + ++/** ++ * Top-level callback creator used to implement event handling using delegation. ++ * This is used via dependency injection in `ReactEventEmitter.ensureListening`. ++ */ + var ReactEventTopLevelCallback = { + + /** +- * @param {boolean} enabled Whether or not all callbacks that have ever been +- * created with this module should be enabled. ++ * Sets whether or not any created callbacks should be enabled. ++ * ++ * @param {boolean} enabled True if callbacks should be enabled. + */ + setEnabled: function(enabled) { + _topLevelListenersEnabled = !!enabled; + }, + ++ /** ++ * @return {boolean} True if callbacks are enabled. ++ */ + isEnabled: function() { + return _topLevelListenersEnabled; + }, + + /** +- * For a given `topLevelType`, creates a callback that could be added as a +- * listener to the document. That top level callback will simply fix the +- * native events before invoking `handleTopLevel`. ++ * Creates a callback for the supplied `topLevelType` that could be added as ++ * a listener to the document. The callback computes a `topLevelTarget` which ++ * should be the root node of a mounted React component where the listener ++ * is attached. + * +- * - Raw native events cannot be trusted to describe their targets correctly +- * so we expect that the argument to the nested function has already been +- * fixed. But the `target` property may not be something of interest to +- * React, so we find the most suitable target. But even at that point, DOM +- * Elements (the target ) can't be trusted to describe their IDs correctly +- * so we obtain the ID in a reliable manner and pass it to +- * `handleTopLevel`. The target/id that we found to be relevant to our +- * framework are called `renderedTarget`/`renderedTargetID` respectively. ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @return {function} Callback for handling top-level events. + */ + createTopLevelCallback: function(topLevelType) { +- return function(fixedNativeEvent) { ++ return function(nativeEvent) { + if (!_topLevelListenersEnabled) { + return; + } +- var renderedTarget = ReactInstanceHandles.getFirstReactDOM( +- fixedNativeEvent.target ++ var topLevelTarget = ReactInstanceHandles.getFirstReactDOM( ++ getEventTarget(nativeEvent) + ) || ExecutionEnvironment.global; +- var renderedTargetID = getDOMNodeID(renderedTarget); ++ var topLevelTargetID = getDOMNodeID(topLevelTarget) || ''; + ReactEventEmitter.handleTopLevel( + topLevelType, +- fixedNativeEvent, +- renderedTargetID, +- renderedTarget ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent + ); + }; + } +diff --git a/static_upstream/react/core/ReactInstanceHandles.js b/static_upstream/react/core/ReactInstanceHandles.js +index 4eaf0e3..948ebcb 100644 +--- a/static_upstream/react/core/ReactInstanceHandles.js ++++ b/static_upstream/react/core/ReactInstanceHandles.js +@@ -1,5 +1,6 @@ + /** + * @providesModule ReactInstanceHandles ++ * @typechecks + */ + + "use strict"; +@@ -43,13 +44,13 @@ function isValidID(id) { + /** + * True if the supplied `node` is rendered by React. + * +- * @param {DOMElement} node DOM Element to check. ++ * @param {DOMEventTarget} node DOM Element to check. + * @return {boolean} True if the DOM Element appears to be rendered by React. + * @private + */ + function isRenderedByReact(node) { + var id = getDOMNodeID(node); +- return id && id.charAt(0) === SEPARATOR; ++ return id ? id.charAt(0) === SEPARATOR : false; + } + + /** +@@ -126,8 +127,8 @@ var ReactInstanceHandles = { + * Traverses up the ancestors of the supplied node to find a node that is a + * DOM representation of a React component. + * +- * @param {DOMElement} node +- * @return {?DOMElement} ++ * @param {?DOMEventTarget} node ++ * @return {?DOMEventTarget} + * @internal + */ + getFirstReactDOM: function(node) { +@@ -145,9 +146,9 @@ var ReactInstanceHandles = { + * Finds a node with the supplied `id` inside of the supplied `ancestorNode`. + * Exploits the ID naming scheme to perform the search quickly. + * +- * @param {DOMElement} ancestorNode Search from this root. ++ * @param {DOMEventTarget} ancestorNode Search from this root. + * @pararm {string} id ID of the DOM representation of the component. +- * @return {?DOMElement} DOM element with the supplied `id`, if one exists. ++ * @return {?DOMEventTarget} DOM node with the supplied `id`, if one exists. + * @internal + */ + findComponentRoot: function(ancestorNode, id) { +@@ -214,7 +215,7 @@ var ReactInstanceHandles = { + * contains the React component with the supplied DOM ID. + * + * @param {string} id DOM ID of a React component. +- * @return {string} DOM ID of the React component that is the root. ++ * @return {?string} DOM ID of the React component that is the root. + * @internal + */ + getReactRootIDFromNodeID: function(id) { +diff --git a/static_upstream/react/dom/getDOMNodeID.js b/static_upstream/react/dom/getDOMNodeID.js +index 503f752..738239a 100644 +--- a/static_upstream/react/dom/getDOMNodeID.js ++++ b/static_upstream/react/dom/getDOMNodeID.js +@@ -1,5 +1,6 @@ + /** + * @providesModule getDOMNodeID ++ * @typechecks + */ + + "use strict"; +@@ -9,8 +10,8 @@ + * control whose name or ID is "id". However, not all DOM nodes support + * `getAttributeNode` (document - which is not a form) so that is checked first. + * +- * @param {Element} domNode DOM node element to return ID of. +- * @returns {string} The ID of `domNode`. ++ * @param {DOMElement|DOMWindow|DOMDocument} domNode DOM node. ++ * @returns {string} ID of the supplied `domNode`. + */ + function getDOMNodeID(domNode) { + if (domNode.getAttributeNode) { +diff --git a/static_upstream/react/dom/getEventTarget.js b/static_upstream/react/dom/getEventTarget.js +new file mode 100644 +index 0000000..d9337fd +--- /dev/null ++++ b/static_upstream/react/dom/getEventTarget.js +@@ -0,0 +1,25 @@ ++/** ++ * @providesModule getEventTarget ++ * @typechecks ++ */ ++ ++var ExecutionEnvironment = require('ExecutionEnvironment'); ++ ++/** ++ * Gets the target node from a native browser event by accounting for ++ * inconsistencies in browser DOM APIs. ++ * ++ * @param {object} nativeEvent Native browser event. ++ * @return {DOMEventTarget} Target node. ++ */ ++function getEventTarget(nativeEvent) { ++ var target = ++ nativeEvent.target || ++ nativeEvent.srcElement || ++ ExecutionEnvironment.global; ++ // Safari may fire events on text nodes (Node.TEXT_NODE is 3). ++ // @see http://www.quirksmode.org/js/events_properties.html ++ return target.nodeType === 3 ? target.parentNode : target; ++} ++ ++module.exports = getEventTarget; +diff --git a/static_upstream/react/event/AbstractEvent.js b/static_upstream/react/event/AbstractEvent.js +index 444031f..3e59b9f 100644 +--- a/static_upstream/react/event/AbstractEvent.js ++++ b/static_upstream/react/event/AbstractEvent.js +@@ -30,14 +30,12 @@ var MAX_POOL_SIZE = 20; + * unreliable native event. + */ + function AbstractEvent( +- abstractEventType, +- abstractTargetID, // Allows the abstract target to differ from native. +- originatingTopLevelEventType, ++ reactEventType, ++ reactTargetID, // Allows the abstract target to differ from native. + nativeEvent, + data) { +- this.type = abstractEventType; +- this.abstractTargetID = abstractTargetID || ''; +- this.originatingTopLevelEventType = originatingTopLevelEventType; ++ this.reactEventType = reactEventType; ++ this.reactTargetID = reactTargetID || ''; + this.nativeEvent = nativeEvent; + this.data = data; + // TODO: Deprecate storing target - doesn't always make sense for some types +@@ -249,12 +247,10 @@ AbstractEvent.persistentCloneOf = function(abstractEvent) { + throwIf(!(abstractEvent instanceof AbstractEvent), CLONE_TYPE_ERR); + } + return new AbstractEvent( +- abstractEvent.type, +- abstractEvent.abstractTargetID, +- abstractEvent.originatingTopLevelEventType, ++ abstractEvent.reactEventType, ++ abstractEvent.reactTargetID, + abstractEvent.nativeEvent, +- abstractEvent.data, +- abstractEvent.target ++ abstractEvent.data + ); + }; + +diff --git a/static_upstream/react/event/EventPluginHub.js b/static_upstream/react/event/EventPluginHub.js +index 85cd51e..7092947 100644 +--- a/static_upstream/react/event/EventPluginHub.js ++++ b/static_upstream/react/event/EventPluginHub.js +@@ -180,15 +180,16 @@ function recordAllRegistrationNames(eventType, PluginModule) { + * @param {AbstractEvent} abstractEvent to look at + */ + function getPluginModuleForAbstractEvent(abstractEvent) { +- if (abstractEvent.type.registrationName) { +- return registrationNames[abstractEvent.type.registrationName]; ++ var reactEventType = abstractEvent.reactEventType; ++ if (reactEventType.registrationName) { ++ return registrationNames[reactEventType.registrationName]; + } else { +- for (var phase in abstractEvent.type.phasedRegistrationNames) { +- if (!abstractEvent.type.phasedRegistrationNames.hasOwnProperty(phase)) { ++ for (var phase in reactEventType.phasedRegistrationNames) { ++ if (!reactEventType.phasedRegistrationNames.hasOwnProperty(phase)) { + continue; + } + var PluginModule = registrationNames[ +- abstractEvent.type.phasedRegistrationNames[phase] ++ reactEventType.phasedRegistrationNames[phase] + ]; + if (PluginModule) { + return PluginModule; +@@ -209,36 +210,36 @@ var deleteAllListeners = function(domID) { + * Accepts the stream of top level native events, and gives every registered + * plugin an opportunity to extract `AbstractEvent`s with annotated dispatches. + * +- * @param {Enum} topLevelType Record from `EventConstants`. +- * @param {Event} nativeEvent A Standard Event with fixed `target` property. +- * @param {Element} renderedTarget Element of interest to the framework, usually +- * the same as `nativeEvent.target` but occasionally an element immediately +- * above `nativeEvent.target` (the first DOM node recognized as one "rendered" +- * by the framework at hand.) +- * @param {string} renderedTargetID string ID of `renderedTarget`. ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @param {DOMEventTarget} topLevelTarget The listening component root node. ++ * @param {string} topLevelTargetID ID of `topLevelTarget`. ++ * @param {object} nativeEvent Native browser event. ++ * @return {*} An accumulation of `AbstractEvent`s. + */ +-var extractAbstractEvents = +- function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) { +- var abstractEvents; +- var plugins = injection.plugins; +- var len = plugins.length; +- for (var i = 0; i < len; i++) { +- // Not every plugin in the ordering may be loaded at runtime. +- var possiblePlugin = plugins[i]; +- var extractedAbstractEvents = +- possiblePlugin && +- possiblePlugin.extractAbstractEvents( +- topLevelType, +- nativeEvent, +- renderedTargetID, +- renderedTarget +- ); ++var extractAbstractEvents = function( ++ topLevelType, ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent) { ++ var abstractEvents; ++ var plugins = injection.plugins; ++ for (var i = 0, l = plugins.length; i < l; i++) { ++ // Not every plugin in the ordering may be loaded at runtime. ++ var possiblePlugin = plugins[i]; ++ if (possiblePlugin) { ++ var extractedAbstractEvents = possiblePlugin.extractAbstractEvents( ++ topLevelType, ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent ++ ); + if (extractedAbstractEvents) { + abstractEvents = accumulate(abstractEvents, extractedAbstractEvents); + } + } +- return abstractEvents; +- }; ++ } ++ return abstractEvents; ++}; + + var enqueueAbstractEvents = function(abstractEvents) { + if (abstractEvents) { +diff --git a/static_upstream/react/event/EventPropagators.js b/static_upstream/react/event/EventPropagators.js +index 17c80f4..3d9103c 100644 +--- a/static_upstream/react/event/EventPropagators.js ++++ b/static_upstream/react/event/EventPropagators.js +@@ -44,7 +44,7 @@ var injection = { + */ + function listenerAtPhase(id, abstractEvent, propagationPhase) { + var registrationName = +- abstractEvent.type.phasedRegistrationNames[propagationPhase]; ++ abstractEvent.reactEventType.phasedRegistrationNames[propagationPhase]; + return getListener(id, registrationName); + } + +@@ -78,9 +78,9 @@ function accumulateDirectionalDispatches(domID, upwards, abstractEvent) { + * have a different target. + */ + function accumulateTwoPhaseDispatchesSingle(abstractEvent) { +- if (abstractEvent && abstractEvent.type.phasedRegistrationNames) { ++ if (abstractEvent && abstractEvent.reactEventType.phasedRegistrationNames) { + injection.InstanceHandle.traverseTwoPhase( +- abstractEvent.abstractTargetID, ++ abstractEvent.reactTargetID, + accumulateDirectionalDispatches, + abstractEvent + ); +@@ -91,11 +91,12 @@ function accumulateTwoPhaseDispatchesSingle(abstractEvent) { + /** + * Accumulates without regard to direction, does not look for phased + * registration names. Same as `accumulateDirectDispatchesSingle` but without +- * requiring that the `abstractTargetID` be the same as the dispatched ID. ++ * requiring that the `reactTargetID` be the same as the dispatched ID. + */ + function accumulateDispatches(id, ignoredDirection, abstractEvent) { +- if (abstractEvent && abstractEvent.type.registrationName) { +- var listener = getListener(id, abstractEvent.type.registrationName); ++ if (abstractEvent && abstractEvent.reactEventType.registrationName) { ++ var registrationName = abstractEvent.reactEventType.registrationName; ++ var listener = getListener(id, registrationName); + if (listener) { + abstractEvent._dispatchListeners = + accumulate(abstractEvent._dispatchListeners, listener); +@@ -106,12 +107,12 @@ function accumulateDispatches(id, ignoredDirection, abstractEvent) { + + /** + * Accumulates dispatches on an `AbstractEvent`, but only for the +- * `abstractTargetID`. ++ * `reactTargetID`. + * @param {AbstractEvent} abstractEvent + */ + function accumulateDirectDispatchesSingle(abstractEvent) { +- if (abstractEvent && abstractEvent.type.registrationName) { +- accumulateDispatches(abstractEvent.abstractTargetID, null, abstractEvent); ++ if (abstractEvent && abstractEvent.reactEventType.registrationName) { ++ accumulateDispatches(abstractEvent.reactTargetID, null, abstractEvent); + } + } + +diff --git a/static_upstream/react/event/NormalizedEventListener.js b/static_upstream/react/event/NormalizedEventListener.js +deleted file mode 100644 +index 5506fe7..0000000 +--- a/static_upstream/react/event/NormalizedEventListener.js ++++ /dev/null +@@ -1,70 +0,0 @@ +-/** +- * @providesModule NormalizedEventListener +- */ +- +-var EventListener = require('EventListener'); +- +-/** +- * @param {?Event} eventParam Event parameter from an attached listener. +- * @return {Event} Normalized event object. +- * @private +- */ +-function normalizeEvent(eventParam) { +- var nativeEvent = eventParam || window.event; +- // In some browsers (OLD FF), setting the target throws an error. A good way +- // to tell if setting the target will throw an error, is to check if the event +- // has a `target` property. Safari events have a `target` but it's not always +- // normalized. Even if a `target` property exists, it's good to only set the +- // target property if we realize that a change will actually take place. +- var hasTargetProperty = 'target' in nativeEvent; +- var eventTarget = nativeEvent.target || nativeEvent.srcElement || window; +- // Safari may fire events on text nodes (Node.TEXT_NODE is 3) +- // @see http://www.quirksmode.org/js/events_properties.html +- var textNodeNormalizedTarget = +- (eventTarget.nodeType === 3) ? eventTarget.parentNode : eventTarget; +- if (!hasTargetProperty || nativeEvent.target !== textNodeNormalizedTarget) { +- // TODO: Normalize the object via `merge()` to work with strict mode. +- nativeEvent.target = textNodeNormalizedTarget; +- } +- return nativeEvent; +-} +- +-function createNormalizedCallback(cb) { +- return function(unfixedNativeEvent) { +- cb(normalizeEvent(unfixedNativeEvent)); +- }; +-} +- +-var NormalizedEventListener = { +- +- /** +- * Listens to bubbled events on a DOM node. +- * +- * NOTE: The listener will be invoked with a normalized event object. +- * +- * @param {DOMElement} el DOM element to register listener on. +- * @param {string} handlerBaseName Event name, e.g. "click". +- * @param {function} cb Callback function. +- * @public +- */ +- listen: function(el, handlerBaseName, cb) { +- EventListener.listen(el, handlerBaseName, createNormalizedCallback(cb)); +- }, +- +- /** +- * Listens to captured events on a DOM node. +- * +- * NOTE: The listener will be invoked with a normalized event object. +- * +- * @param {DOMElement} el DOM element to register listener on. +- * @param {string} handlerBaseName Event name, e.g. "click". +- * @param {function} cb Callback function. +- * @public +- */ +- capture: function(el, handlerBaseName, cb) { +- EventListener.capture(el, handlerBaseName, createNormalizedCallback(cb)); +- } +- +-}; +- +-module.exports = NormalizedEventListener; +diff --git a/static_upstream/react/eventPlugins/AnalyticsEventPluginFactory.js b/static_upstream/react/eventPlugins/AnalyticsEventPluginFactory.js +index e1c4f38..8508047 100644 +--- a/static_upstream/react/eventPlugins/AnalyticsEventPluginFactory.js ++++ b/static_upstream/react/eventPlugins/AnalyticsEventPluginFactory.js +@@ -122,20 +122,25 @@ if (__DEV__) { + * This plugin does not really extract any abstract events. Rather it just looks + * at the top level event and bumps up counters as appropriate + * +- * @see EventPluginHub.extractAbstractEvents ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @param {DOMEventTarget} topLevelTarget The listening component root node. ++ * @param {string} topLevelTargetID ID of `topLevelTarget`. ++ * @param {object} nativeEvent Native browser event. ++ * @return {*} An accumulation of `AbstractEvent`s. ++ * @see {EventPluginHub.extractAbstractEvents} + */ + function extractAbstractEvents( + topLevelType, +- nativeEvent, +- renderedTargetID, +- renderedTarget) { ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent) { + var currentEvent = topLevelTypesToAnalyticsEvent[topLevelType]; +- if (!currentEvent || !renderedTarget || !renderedTarget.attributes) { ++ if (!currentEvent || !topLevelTarget || !topLevelTarget.attributes) { + return null; + } + +- var analyticsIDAttribute = renderedTarget.attributes[ANALYTICS_ID]; +- var analyticsEventsAttribute = renderedTarget.attributes[ANALYTICS_EVENTS]; ++ var analyticsIDAttribute = topLevelTarget.attributes[ANALYTICS_ID]; ++ var analyticsEventsAttribute = topLevelTarget.attributes[ANALYTICS_EVENTS]; + if(!analyticsIDAttribute || !analyticsEventsAttribute) { + return null; + } +diff --git a/static_upstream/react/eventPlugins/EnterLeaveEventPlugin.js b/static_upstream/react/eventPlugins/EnterLeaveEventPlugin.js +index 856b1d7..7a00e71 100644 +--- a/static_upstream/react/eventPlugins/EnterLeaveEventPlugin.js ++++ b/static_upstream/react/eventPlugins/EnterLeaveEventPlugin.js +@@ -1,5 +1,6 @@ + /** + * @providesModule EnterLeaveEventPlugin ++ * @typechecks + */ + + "use strict"; +@@ -22,28 +23,32 @@ var abstractEventTypes = { + }; + + /** +- * For almost every interaction we care about, there will be a top level +- * `mouseOver` and `mouseOut` event that occur so we can usually only pay +- * attention to one of the two (we'll pay attention to the `mouseOut` event) to +- * avoid extracting a duplicate event. However, there's one interaction where +- * there will be no `mouseOut` event to rely on - mousing from outside the +- * browser *into* the chrome. We detect this scenario and only in that case, we +- * use the `mouseOver` event. ++ * For almost every interaction we care about, there will be a top-level ++ * `mouseover` and `mouseout` event that occurs so only pay attention to one of ++ * the two (to avoid duplicate events). We use the `mouseout` event. + * +- * @see EventPluginHub.extractAbstractEvents ++ * However, there's one interaction where there will be no `mouseout` event to ++ * rely on - mousing from outside the browser *into* the chrome. We detect this ++ * scenario and only in that case, we use the `mouseover` event. ++ * ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @param {DOMEventTarget} topLevelTarget The listening component root node. ++ * @param {string} topLevelTargetID ID of `topLevelTarget`. ++ * @param {object} nativeEvent Native browser event. ++ * @return {*} An accumulation of `AbstractEvent`s. ++ * @see {EventPluginHub.extractAbstractEvents} + */ + var extractAbstractEvents = function( + topLevelType, +- nativeEvent, +- renderedTargetID, +- renderedTarget) { +- ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent) { + if (topLevelType === topLevelTypes.topMouseOver && + (nativeEvent.relatedTarget || nativeEvent.fromElement)) { +- return; ++ return null; + } + if (topLevelType !== topLevelTypes.topMouseOut && +- topLevelType !== topLevelTypes.topMouseOver){ ++ topLevelType !== topLevelTypes.topMouseOver) { + return null; // Must not be a mouse in or mouse out - ignoring. + } + +@@ -51,32 +56,33 @@ var extractAbstractEvents = function( + if (topLevelType === topLevelTypes.topMouseOut) { + to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement) || + ExecutionEnvironment.global; +- from = renderedTarget; ++ from = topLevelTarget; + } else { +- to = renderedTarget; ++ to = topLevelTarget; + from = ExecutionEnvironment.global; + } + + // Nothing pertains to our managed components. +- if (from === to ) { +- return; ++ if (from === to) { ++ return null; + } + + var fromID = from ? getDOMNodeID(from) : ''; + var toID = to ? getDOMNodeID(to) : ''; ++ + var leave = AbstractEvent.getPooled( + abstractEventTypes.mouseLeave, + fromID, +- topLevelType, + nativeEvent + ); + var enter = AbstractEvent.getPooled( + abstractEventTypes.mouseEnter, + toID, +- topLevelType, + nativeEvent + ); ++ + EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID); ++ + return [leave, enter]; + }; + +diff --git a/static_upstream/react/eventPlugins/ResponderEventPlugin.js b/static_upstream/react/eventPlugins/ResponderEventPlugin.js +index 4861cad..2e6b557 100644 +--- a/static_upstream/react/eventPlugins/ResponderEventPlugin.js ++++ b/static_upstream/react/eventPlugins/ResponderEventPlugin.js +@@ -148,14 +148,13 @@ var abstractEventTypes = { + */ + + /** +- * @param {TopLevelTypes} topLevelType Top level event type being examined. +- * @param {DOMEvent} nativeEvent Native DOM event. ++ * @param {string} topLevelType Record from `EventConstants`. + * @param {string} renderedTargetID ID of deepest React rendered element. +- * +- * @return {Accumulation} Extracted events. ++ * @param {object} nativeEvent Native browser event. ++ * @return {*} An accumulation of extracted `AbstractEvent`s. + */ + var setResponderAndExtractTransfer = +- function(topLevelType, nativeEvent, renderedTargetID) { ++ function(topLevelType, renderedTargetID, nativeEvent) { + var type; + var shouldSetEventType = + isStartish(topLevelType) ? abstractEventTypes.startShouldSetResponder : +@@ -221,7 +220,6 @@ var setResponderAndExtractTransfer = + return extracted; + }; + +- + /** + * A transfer is a negotiation between a currently set responder and the next + * element to claim responder status. Any start event could trigger a transfer +@@ -239,51 +237,60 @@ function canTriggerTransfer(topLevelType) { + (isPressing && isMoveish(topLevelType)); + } + +-var extractAbstractEvents = +- function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) { +- var extracted; +- // Must have missed an end event - reset the state here. +- if (responderID && isStartish(topLevelType)) { +- responderID = null; +- } +- if (isStartish(topLevelType)) { +- isPressing = true; +- } else if (isEndish(topLevelType)) { +- isPressing = false; +- } +- if (canTriggerTransfer(topLevelType)) { +- var transfer = setResponderAndExtractTransfer( +- topLevelType, +- nativeEvent, +- renderedTargetID, +- renderedTarget +- ); +- if (transfer) { +- extracted = accumulate(extracted, transfer); +- } +- } +- // Now that we know the responder is set correctly, we can dispatch +- // responder type events (directly to the responder). +- var type = isMoveish(topLevelType) ? abstractEventTypes.responderMove : +- isEndish(topLevelType) ? abstractEventTypes.responderRelease : +- isStartish(topLevelType) ? abstractEventTypes.responderStart : null; +- if (type) { +- var data = AbstractEvent.normalizePointerData(nativeEvent); +- var gesture = AbstractEvent.getPooled( +- type, +- responderID, +- topLevelType, +- nativeEvent, +- data +- ); +- EventPropagators.accumulateDirectDispatches(gesture); +- extracted = accumulate(extracted, gesture); +- } +- if (type === abstractEventTypes.responderRelease) { +- responderID = null; ++/** ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @param {DOMEventTarget} topLevelTarget The listening component root node. ++ * @param {string} topLevelTargetID ID of `topLevelTarget`. ++ * @param {object} nativeEvent Native browser event. ++ * @return {*} An accumulation of `AbstractEvent`s. ++ * @see {EventPluginHub.extractAbstractEvents} ++ */ ++var extractAbstractEvents = function( ++ topLevelType, ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent) { ++ var extracted; ++ // Must have missed an end event - reset the state here. ++ if (responderID && isStartish(topLevelType)) { ++ responderID = null; ++ } ++ if (isStartish(topLevelType)) { ++ isPressing = true; ++ } else if (isEndish(topLevelType)) { ++ isPressing = false; ++ } ++ if (canTriggerTransfer(topLevelType)) { ++ var transfer = setResponderAndExtractTransfer( ++ topLevelType, ++ topLevelTargetID, ++ nativeEvent ++ ); ++ if (transfer) { ++ extracted = accumulate(extracted, transfer); + } +- return extracted; +- }; ++ } ++ // Now that we know the responder is set correctly, we can dispatch ++ // responder type events (directly to the responder). ++ var type = isMoveish(topLevelType) ? abstractEventTypes.responderMove : ++ isEndish(topLevelType) ? abstractEventTypes.responderRelease : ++ isStartish(topLevelType) ? abstractEventTypes.responderStart : null; ++ if (type) { ++ var data = AbstractEvent.normalizePointerData(nativeEvent); ++ var gesture = AbstractEvent.getPooled( ++ type, ++ responderID, ++ nativeEvent, ++ data ++ ); ++ EventPropagators.accumulateDirectDispatches(gesture); ++ extracted = accumulate(extracted, gesture); ++ } ++ if (type === abstractEventTypes.responderRelease) { ++ responderID = null; ++ } ++ return extracted; ++}; + + /** + * Event plugin for formalizing the negotiation between claiming locks on +diff --git a/static_upstream/react/eventPlugins/SimpleEventPlugin.js b/static_upstream/react/eventPlugins/SimpleEventPlugin.js +index 8f2cdda..84310e27 100644 +--- a/static_upstream/react/eventPlugins/SimpleEventPlugin.js ++++ b/static_upstream/react/eventPlugins/SimpleEventPlugin.js +@@ -141,6 +141,7 @@ var SimpleEventPlugin = { + /** + * Same as the default implementation, except cancels the event when return + * value is false. ++ * + * @param {AbstractEvent} AbstractEvent to handle + * @param {function} Application-level callback + * @param {string} domID DOM id to pass to the callback. +@@ -154,48 +155,55 @@ var SimpleEventPlugin = { + }, + + /** +- * @see EventPluginHub.extractAbstractEvents ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @param {DOMEventTarget} topLevelTarget The listening component root node. ++ * @param {string} topLevelTargetID ID of `topLevelTarget`. ++ * @param {object} nativeEvent Native browser event. ++ * @return {*} An accumulation of `AbstractEvent`s. ++ * @see {EventPluginHub.extractAbstractEvents} + */ +- extractAbstractEvents: +- function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) { +- var data; +- var abstractEventType = +- SimpleEventPlugin.topLevelTypesToAbstract[topLevelType]; +- if (!abstractEventType) { +- return null; +- } +- switch(topLevelType) { +- case topLevelTypes.topMouseWheel: +- data = AbstractEvent.normalizeMouseWheelData(nativeEvent); +- break; +- case topLevelTypes.topScroll: +- data = AbstractEvent.normalizeScrollDataFromTarget(renderedTarget); +- break; +- case topLevelTypes.topClick: +- case topLevelTypes.topDoubleClick: +- case topLevelTypes.topChange: +- case topLevelTypes.topDOMCharacterDataModified: +- case topLevelTypes.topMouseDown: +- case topLevelTypes.topMouseUp: +- case topLevelTypes.topMouseMove: +- case topLevelTypes.topTouchMove: +- case topLevelTypes.topTouchStart: +- case topLevelTypes.topTouchEnd: +- data = AbstractEvent.normalizePointerData(nativeEvent); +- break; +- default: +- data = null; +- } +- var abstractEvent = AbstractEvent.getPooled( +- abstractEventType, +- renderedTargetID, +- topLevelType, +- nativeEvent, +- data +- ); +- EventPropagators.accumulateTwoPhaseDispatches(abstractEvent); +- return abstractEvent; ++ extractAbstractEvents: function( ++ topLevelType, ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent) { ++ var data; ++ var abstractEventType = ++ SimpleEventPlugin.topLevelTypesToAbstract[topLevelType]; ++ if (!abstractEventType) { ++ return null; + } ++ switch(topLevelType) { ++ case topLevelTypes.topMouseWheel: ++ data = AbstractEvent.normalizeMouseWheelData(nativeEvent); ++ break; ++ case topLevelTypes.topScroll: ++ data = AbstractEvent.normalizeScrollDataFromTarget(topLevelTarget); ++ break; ++ case topLevelTypes.topClick: ++ case topLevelTypes.topDoubleClick: ++ case topLevelTypes.topChange: ++ case topLevelTypes.topDOMCharacterDataModified: ++ case topLevelTypes.topMouseDown: ++ case topLevelTypes.topMouseUp: ++ case topLevelTypes.topMouseMove: ++ case topLevelTypes.topTouchMove: ++ case topLevelTypes.topTouchStart: ++ case topLevelTypes.topTouchEnd: ++ data = AbstractEvent.normalizePointerData(nativeEvent); ++ break; ++ default: ++ data = null; ++ } ++ var abstractEvent = AbstractEvent.getPooled( ++ abstractEventType, ++ topLevelTargetID, ++ nativeEvent, ++ data ++ ); ++ EventPropagators.accumulateTwoPhaseDispatches(abstractEvent); ++ return abstractEvent; ++ } + }; + + SimpleEventPlugin.topLevelTypesToAbstract = { +diff --git a/static_upstream/react/eventPlugins/TapEventPlugin.js b/static_upstream/react/eventPlugins/TapEventPlugin.js +index 29e03cf..92c8392 100644 +--- a/static_upstream/react/eventPlugins/TapEventPlugin.js ++++ b/static_upstream/react/eventPlugins/TapEventPlugin.js +@@ -1,5 +1,6 @@ + /** + * @providesModule TapEventPlugin ++ * @typechecks + */ + + "use strict"; +@@ -32,26 +33,27 @@ var abstractEventTypes = { + }; + + /** +- * @see EventPluginHub.extractAbstractEvents ++ * @param {string} topLevelType Record from `EventConstants`. ++ * @param {DOMEventTarget} topLevelTarget The listening component root node. ++ * @param {string} topLevelTargetID ID of `topLevelTarget`. ++ * @param {object} nativeEvent Native browser event. ++ * @return {*} An accumulation of `AbstractEvent`s. ++ * @see {EventPluginHub.extractAbstractEvents} + */ + var extractAbstractEvents = function( + topLevelType, +- nativeEvent, +- renderedTargetID, +- renderedTarget) { +- ++ topLevelTarget, ++ topLevelTargetID, ++ nativeEvent) { + if (!isStartish(topLevelType) && !isEndish(topLevelType)) { + return; + } + var abstractEvent; + var dist = eventDistance(startCoords, nativeEvent); + if (isEndish(topLevelType) && dist < tapMoveThreshold) { +- var type = abstractEventTypes.touchTap; +- var abstractTargetID = renderedTargetID; + abstractEvent = AbstractEvent.getPooled( +- type, +- abstractTargetID, +- topLevelType, ++ abstractEventTypes.touchTap, ++ topLevelTargetID, + nativeEvent + ); + } +diff --git a/static_upstream/react/eventPlugins/__tests__/ResponderEventPlugin-test.js b/static_upstream/react/eventPlugins/__tests__/ResponderEventPlugin-test.js +index c148a5a..1dfb44e 100644 +--- a/static_upstream/react/eventPlugins/__tests__/ResponderEventPlugin-test.js ++++ b/static_upstream/react/eventPlugins/__tests__/ResponderEventPlugin-test.js +@@ -153,8 +153,9 @@ var existsInExtraction = function(extracted, test) { + function assertGrantEvent(id, extracted) { + var test = function(abstractEvent) { + return abstractEvent instanceof AbstractEvent && +- abstractEvent.type === responderAbstractEventTypes.responderGrant && +- abstractEvent.abstractTargetID === id; ++ abstractEvent.reactEventType === ++ responderAbstractEventTypes.responderGrant && ++ abstractEvent.reactTargetID === id; + }; + expect(ResponderEventPlugin.getResponderID()).toBe(id); + expect(existsInExtraction(extracted, test)).toBe(true); +@@ -163,8 +164,9 @@ function assertGrantEvent(id, extracted) { + function assertResponderMoveEvent(id, extracted) { + var test = function(abstractEvent) { + return abstractEvent instanceof AbstractEvent && +- abstractEvent.type === responderAbstractEventTypes.responderMove && +- abstractEvent.abstractTargetID === id; ++ abstractEvent.reactEventType === ++ responderAbstractEventTypes.responderMove && ++ abstractEvent.reactTargetID === id; + }; + expect(ResponderEventPlugin.getResponderID()).toBe(id); + expect(existsInExtraction(extracted, test)).toBe(true); +@@ -173,8 +175,9 @@ function assertResponderMoveEvent(id, extracted) { + function assertTerminateEvent(id, extracted) { + var test = function(abstractEvent) { + return abstractEvent instanceof AbstractEvent && +- abstractEvent.type === responderAbstractEventTypes.responderTerminate && +- abstractEvent.abstractTargetID === id; ++ abstractEvent.reactEventType === ++ responderAbstractEventTypes.responderTerminate && ++ abstractEvent.reactTargetID === id; + }; + expect(ResponderEventPlugin.getResponderID()).not.toBe(id); + expect(existsInExtraction(extracted, test)).toBe(true); +@@ -183,8 +186,9 @@ function assertTerminateEvent(id, extracted) { + function assertRelease(id, extracted) { + var test = function(abstractEvent) { + return abstractEvent instanceof AbstractEvent && +- abstractEvent.type === responderAbstractEventTypes.responderRelease && +- abstractEvent.abstractTargetID === id; ++ abstractEvent.reactEventType === ++ responderAbstractEventTypes.responderRelease && ++ abstractEvent.reactTargetID === id; + }; + expect(ResponderEventPlugin.getResponderID()).toBe(null); + expect(existsInExtraction(extracted, test)).toBe(true); diff --git a/src/dom/isEventSupported.js b/src/dom/isEventSupported.js index b756e3c8ba087..330a7086ae568 100644 --- a/src/dom/isEventSupported.js +++ b/src/dom/isEventSupported.js @@ -20,9 +20,14 @@ var ExecutionEnvironment = require('ExecutionEnvironment'); -var testNode; +var testNode, useHasFeature; if (ExecutionEnvironment.canUseDOM) { testNode = document.createElement('div'); + useHasFeature = + document.implementation && + document.implementation.hasFeature && + // `hasFeature` always returns true in Firefox 19+. + document.implementation.hasFeature('', '') !== true; } /** @@ -44,6 +49,7 @@ function isEventSupported(eventNameSuffix, capture) { return false; } var element = document.createElement('div'); + var eventName = 'on' + eventNameSuffix; var isSupported = eventName in element; @@ -55,6 +61,12 @@ function isEventSupported(eventNameSuffix, capture) { } element.removeAttribute(eventName); } + + if (!isSupported && useHasFeature && eventNameSuffix === 'wheel') { + // This is the only way to test support for the `wheel` event in IE9+. + isSupported = document.implementation.hasFeature('Events.wheel', '3.0'); + } + element = null; return isSupported; } diff --git a/src/event/EventConstants.js b/src/event/EventConstants.js index 0af7b9d0b29b1..b99696d95bc30 100644 --- a/src/event/EventConstants.js +++ b/src/event/EventConstants.js @@ -41,7 +41,7 @@ var topLevelTypes = keyMirror({ topMouseOut: null, topMouseOver: null, topMouseUp: null, - topMouseWheel: null, + topWheel: null, topScroll: null, topSubmit: null, topTouchCancel: null, diff --git a/src/eventPlugins/AnalyticsEventPluginFactory.js b/src/eventPlugins/AnalyticsEventPluginFactory.js index b06859e00d664..ce79e575f10e7 100644 --- a/src/eventPlugins/AnalyticsEventPluginFactory.js +++ b/src/eventPlugins/AnalyticsEventPluginFactory.js @@ -99,7 +99,7 @@ var analyticsData = {}; var topLevelTypesToAnalyticsEvent = { topClick: 'click', topDoubleClick: 'doubleClick', - topMouseWheel: 'mouseWheel', + wheel: 'wheel', topTouchStart: 'touchStart', topTouchEnd: 'touchEnd', topTouchMove: 'touchMove', @@ -117,7 +117,7 @@ if (__DEV__) { var analyticsEventNameToTopLevelType = { 'click': topLevelTypes.topClick, 'doubleClick': topLevelTypes.topDoubleClick, - 'mouseWheel': topLevelTypes.topMouseWheel, + 'wheel': topLevelTypes.wheel, 'touchStart': topLevelTypes.topTouchStart, 'touchEnd': topLevelTypes.topTouchEnd, 'touchMove': topLevelTypes.topTouchMove, diff --git a/src/eventPlugins/SimpleEventPlugin.js b/src/eventPlugins/SimpleEventPlugin.js index ab794a17b4231..4087223718195 100644 --- a/src/eventPlugins/SimpleEventPlugin.js +++ b/src/eventPlugins/SimpleEventPlugin.js @@ -60,10 +60,10 @@ var SimpleEventPlugin = { captured: keyOf({onClickCapture: true}) } }, - mouseWheel: { + wheel: { phasedRegistrationNames: { - bubbled: keyOf({onMouseWheel: true}), - captured: keyOf({onMouseWheelCapture: true}) + bubbled: keyOf({onWheel: true}), + captured: keyOf({onWheelCapture: true}) } }, touchStart: { @@ -188,7 +188,7 @@ var SimpleEventPlugin = { return null; } switch(topLevelType) { - case topLevelTypes.topMouseWheel: + case topLevelTypes.topWheel: data = AbstractEvent.normalizeMouseWheelData(nativeEvent); break; case topLevelTypes.topScroll: @@ -226,7 +226,7 @@ SimpleEventPlugin.topLevelTypesToAbstract = { topMouseMove: SimpleEventPlugin.abstractEventTypes.mouseMove, topClick: SimpleEventPlugin.abstractEventTypes.click, topDoubleClick: SimpleEventPlugin.abstractEventTypes.doubleClick, - topMouseWheel: SimpleEventPlugin.abstractEventTypes.mouseWheel, + topWheel: SimpleEventPlugin.abstractEventTypes.wheel, topTouchStart: SimpleEventPlugin.abstractEventTypes.touchStart, topTouchEnd: SimpleEventPlugin.abstractEventTypes.touchEnd, topTouchMove: SimpleEventPlugin.abstractEventTypes.touchMove, From 2dc24fc2343b6120d8b80df9e9e070917cc197d9 Mon Sep 17 00:00:00 2001 From: CommitSyncScript Date: Thu, 6 Jun 2013 14:45:03 -0700 Subject: [PATCH 25/25] Add typecheck, cleanup Followup with some additional comments for https://github.com/facebook/react/pull/58 --- src/core/ReactMount.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index d6734294afb89..d8382a4099ed6 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -196,18 +196,19 @@ var ReactMount = { * Unmounts and destroys the React component rendered in the `container`. * * @param {DOMElement} container DOM element containing a React component. + * @return {boolean} True if a component was found in and unmounted from + * `container` */ unmountAndReleaseReactRootNode: function(container) { var reactRootID = getReactRootID(container); var component = instanceByReactRootID[reactRootID]; - if (component) { - component.unmountComponentFromNode(container); - delete instanceByReactRootID[reactRootID]; - delete containersByReactRootID[reactRootID]; - return true; - } else { + if (!component) { return false; } + component.unmountComponentFromNode(container); + delete instanceByReactRootID[reactRootID]; + delete containersByReactRootID[reactRootID]; + return true; }, /**