@samitouri / QOS-React-2 / commits / 80d7d2d0f8

Listen to events on demand

Fixes #381 This is a squashed version of https://github.com/facebook/react/pull/462

SanderSpies committed Jan 3, 2014 at 23:09 UTC 80d7d2d0f8ec0f4f1f3c2dbe613f36bda572ce11
18 files changed +311 -232
src/core/React.js
+2 -1
@@ -19,6 +19,7 @@
19 "use strict";
20
21 var DOMPropertyOperations = require('DOMPropertyOperations');
22 +var EventPluginUtils = require('EventPluginUtils');
23 var ReactComponent = require('ReactComponent');
24 var ReactCompositeComponent = require('ReactCompositeComponent');
25 var ReactContext = require('ReactContext');
@@ -40,7 +41,7 @@ var React = {
41 DOM: ReactDOM,
42 PropTypes: ReactPropTypes,
43 initializeTouchEvents: function(shouldUseTouch) {
43 - ReactMount.useTouchEvents = shouldUseTouch;
44 + EventPluginUtils.useTouchEvents = shouldUseTouch;
45 },
46 createClass: ReactCompositeComponent.createClass,
47 constructAndRenderComponent: ReactMount.constructAndRenderComponent,
src/core/ReactDOMComponent.js
+21 -5
@@ -24,6 +24,7 @@ var DOMProperty = require('DOMProperty');
24 var DOMPropertyOperations = require('DOMPropertyOperations');
25 var ReactComponent = require('ReactComponent');
26 var ReactEventEmitter = require('ReactEventEmitter');
27 +var ReactMount = require('ReactMount');
28 var ReactMultiChild = require('ReactMultiChild');
29 var ReactPerf = require('ReactPerf');
30
@@ -33,15 +34,17 @@ var keyOf = require('keyOf');
34 var merge = require('merge');
35 var mixInto = require('mixInto');
36
36 -var putListener = ReactEventEmitter.putListener;
37 var deleteListener = ReactEventEmitter.deleteListener;
38 -var registrationNames = ReactEventEmitter.registrationNames;
38 +var listenTo = ReactEventEmitter.listenTo;
39 +var registrationNameModules = ReactEventEmitter.registrationNameModules;
40
41 // For quickly matching children type, to test if can be treated as content.
42 var CONTENT_TYPES = {'string': true, 'number': true};
43
44 var STYLE = keyOf({style: null});
45
46 +var ELEMENT_NODE_TYPE = 1;
47 +
48 /**
49 * @param {?object} props
50 */
@@ -61,6 +64,19 @@ function assertValidProps(props) {
64 );
65 }
66
67 +function putListener(id, registrationName, listener) {
68 + var container = ReactMount.findReactContainerForID(id);
69 + if (container) {
70 + var doc = container.nodeType === ELEMENT_NODE_TYPE ?
71 + container.ownerDocument :
72 + container;
73 + listenTo(registrationName, doc);
74 + }
75 +
76 + ReactEventEmitter.putListener(id, registrationName, listener);
77 +}
78 +
79 +
80 /**
81 * @constructor ReactDOMComponent
82 * @extends ReactComponent
@@ -126,7 +142,7 @@ ReactDOMComponent.Mixin = {
142 if (propValue == null) {
143 continue;
144 }
129 - if (registrationNames[propKey]) {
145 + if (registrationNameModules[propKey]) {
146 putListener(this._rootNodeID, propKey, propValue);
147 } else {
148 if (propKey === STYLE) {
@@ -243,7 +259,7 @@ ReactDOMComponent.Mixin = {
259 styleUpdates[styleName] = '';
260 }
261 }
246 - } else if (registrationNames[propKey]) {
262 + } else if (registrationNameModules[propKey]) {
263 deleteListener(this._rootNodeID, propKey);
264 } else if (
265 DOMProperty.isStandardName[propKey] ||
@@ -285,7 +301,7 @@ ReactDOMComponent.Mixin = {
301 // Relies on `updateStylesByID` not mutating `styleUpdates`.
302 styleUpdates = nextProp;
303 }
288 - } else if (registrationNames[propKey]) {
304 + } else if (registrationNameModules[propKey]) {
305 putListener(this._rootNodeID, propKey, nextProp);
306 } else if (
307 DOMProperty.isStandardName[propKey] ||
src/core/ReactEventEmitter.js
+121 -137
@@ -22,6 +22,7 @@
22 var EventConstants = require('EventConstants');
23 var EventListener = require('EventListener');
24 var EventPluginHub = require('EventPluginHub');
25 +var EventPluginRegistry = require('EventPluginRegistry');
26 var ExecutionEnvironment = require('ExecutionEnvironment');
27 var ReactEventEmitterMixin = require('ReactEventEmitterMixin');
28 var ViewportMetrics = require('ViewportMetrics');
@@ -75,6 +76,62 @@ var merge = require('merge');
76 * React Core . General Purpose Event Plugin System
77 */
78
79 +var alreadyListeningTo = {};
80 +var isMonitoringScrollValue = false;
81 +var reactTopListenersCounter = 0;
82 +var topEventMapping = {
83 + topBlur: 'blur',
84 + topChange: 'change',
85 + topClick: 'click',
86 + topCompositionEnd: 'compositionend',
87 + topCompositionStart: 'compositionstart',
88 + topCompositionUpdate: 'compositionupdate',
89 + topContextMenu: 'contextmenu',
90 + topCopy: 'copy',
91 + topCut: 'cut',
92 + topDoubleClick: 'dblclick',
93 + topDrag: 'drag',
94 + topDragEnd: 'dragend',
95 + topDragEnter: 'dragenter',
96 + topDragExit: 'dragexit',
97 + topDragLeave: 'dragleave',
98 + topDragOver: 'dragover',
99 + topDragStart: 'dragstart',
100 + topDrop: 'drop',
101 + topFocus: 'focus',
102 + topInput: 'input',
103 + topKeyDown: 'keydown',
104 + topKeyPress: 'keypress',
105 + topKeyUp: 'keyup',
106 + topMouseDown: 'mousedown',
107 + topMouseMove: 'mousemove',
108 + topMouseOut: 'mouseout',
109 + topMouseOver: 'mouseover',
110 + topMouseUp: 'mouseup',
111 + topPaste: 'paste',
112 + topScroll: 'scroll',
113 + topSelectionChange: 'selectionchange',
114 + topSubmit: 'submit',
115 + topTouchCancel: 'touchcancel',
116 + topTouchEnd: 'touchend',
117 + topTouchMove: 'touchmove',
118 + topTouchStart: 'touchstart',
119 + topWheel: 'wheel'
120 +};
121 +
122 +/**
123 + * To ensure no conflicts with other potential React instances on the page
124 + */
125 +var topListenersIDKey = "_reactListenersID" + String(Math.random()).slice(2);
126 +
127 +function getListeningDocument(mountAt) {
128 + if (mountAt[topListenersIDKey] == null) {
129 + mountAt[topListenersIDKey] = reactTopListenersCounter++;
130 + alreadyListeningTo[mountAt[topListenersIDKey]] = {};
131 + }
132 + return alreadyListeningTo[mountAt[topListenersIDKey]];
133 +}
134 +
135 /**
136 * Traps top-level events by using event bubbling.
137 *
@@ -111,21 +168,6 @@ function trapCapturedEvent(topLevelType, handlerBaseName, element) {
168 );
169 }
170
114 -/**
115 - * Listens to window scroll and resize events. We cache scroll values so that
116 - * application code can access them without triggering reflows.
117 - *
118 - * NOTE: Scroll events do not bubble.
119 - *
120 - * @private
121 - * @see http://www.quirksmode.org/dom/events/scroll.html
122 - */
123 -function registerScrollValueMonitoring() {
124 - var refresh = ViewportMetrics.refreshScrollValues;
125 - EventListener.listen(window, 'scroll', refresh);
126 - EventListener.listen(window, 'resize', refresh);
127 -}
128 -
171 /**
172 * `ReactEventEmitter` is used to attach top-level event listeners. For example:
173 *
@@ -143,39 +185,6 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, {
185 */
186 TopLevelCallbackCreator: null,
187
146 - /**
147 - * Ensures that top-level event delegation listeners are installed.
148 - *
149 - * There are issues with listening to both touch events and mouse events on
150 - * the top-level, so we make the caller choose which one to listen to. (If
151 - * there's a touch top-level listeners, anchors don't receive clicks for some
152 - * reason, and only in some cases).
153 - *
154 - * @param {boolean} touchNotMouse Listen to touch events instead of mouse.
155 - * @param {DOMDocument} contentDocument DOM document to listen on
156 - */
157 - ensureListening: function(touchNotMouse, contentDocument) {
158 - invariant(
159 - ExecutionEnvironment.canUseDOM,
160 - 'ensureListening(...): Cannot toggle event listening in a Worker ' +
161 - 'thread. This is likely a bug in the framework. Please report ' +
162 - 'immediately.'
163 - );
164 - invariant(
165 - ReactEventEmitter.TopLevelCallbackCreator,
166 - 'ensureListening(...): Cannot be called without a top level callback ' +
167 - 'creator being injected.'
168 - );
169 - // Call out to base implementation.
170 - ReactEventEmitterMixin.ensureListening.call(
171 - ReactEventEmitter,
172 - {
173 - touchNotMouse: touchNotMouse,
174 - contentDocument: contentDocument
175 - }
176 - );
177 - },
178 -
188 /**
189 * Sets whether or not any created callbacks should be enabled.
190 *
@@ -220,108 +229,84 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, {
229 * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
230 * they bubble to document.
231 *
223 - * @param {boolean} touchNotMouse Listen to touch events instead of mouse.
232 + * @param {string} registrationName Name of listener (e.g. `onClick`).
233 * @param {DOMDocument} contentDocument Document which owns the container
225 - * @private
226 - * @see http://www.quirksmode.org/dom/events/keys.html.
234 */
228 - listenAtTopLevel: function(touchNotMouse, contentDocument) {
229 - invariant(
230 - !contentDocument._isListening,
231 - 'listenAtTopLevel(...): Cannot setup top-level listener more than once.'
232 - );
233 - var topLevelTypes = EventConstants.topLevelTypes;
235 + listenTo: function(registrationName, contentDocument) {
236 var mountAt = contentDocument;
237 + var listeningDocument = getListeningDocument(mountAt);
238 + var dependencies = EventPluginRegistry.
239 + registrationNameDependencies[registrationName];
240
236 - registerScrollValueMonitoring();
237 - trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt);
238 - trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt);
239 - trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt);
240 - trapBubbledEvent(topLevelTypes.topMouseMove, 'mousemove', mountAt);
241 - trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt);
242 - trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt);
243 - trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt);
244 - trapBubbledEvent(topLevelTypes.topContextMenu, 'contextmenu', mountAt);
245 - if (touchNotMouse) {
246 - trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt);
247 - trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt);
248 - trapBubbledEvent(topLevelTypes.topTouchMove, 'touchmove', mountAt);
249 - trapBubbledEvent(topLevelTypes.topTouchCancel, 'touchcancel', mountAt);
250 - }
251 - trapBubbledEvent(topLevelTypes.topKeyUp, 'keyup', mountAt);
252 - trapBubbledEvent(topLevelTypes.topKeyPress, 'keypress', mountAt);
253 - trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt);
254 - trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt);
255 - trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt);
256 - trapBubbledEvent(
257 - topLevelTypes.topSelectionChange,
258 - 'selectionchange',
259 - mountAt
260 - );
241 + var topLevelTypes = EventConstants.topLevelTypes;
242 + for (var i = 0, l = dependencies.length; i < l; i++) {
243 + var dependency = dependencies[i];
244 + if (!listeningDocument[dependency]) {
245 + var topLevelType = topLevelTypes[dependency];
246
262 - trapBubbledEvent(
263 - topLevelTypes.topCompositionEnd,
264 - 'compositionend',
265 - mountAt
266 - );
267 - trapBubbledEvent(
268 - topLevelTypes.topCompositionStart,
269 - 'compositionstart',
270 - mountAt
271 - );
272 - trapBubbledEvent(
273 - topLevelTypes.topCompositionUpdate,
274 - 'compositionupdate',
275 - mountAt
276 - );
247 + if (topLevelType === topLevelTypes.topWheel) {
248 + if (isEventSupported('wheel')) {
249 + trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
250 + } else if (isEventSupported('mousewheel')) {
251 + trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
252 + } else {
253 + // Firefox needs to capture a different mouse scroll event.
254 + // @see http://www.quirksmode.org/dom/events/tests/scroll.html
255 + trapBubbledEvent(
256 + topLevelTypes.topWheel,
257 + 'DOMMouseScroll',
258 + mountAt);
259 + }
260 + } else if (topLevelType === topLevelTypes.topScroll) {
261
278 - if (isEventSupported('drag')) {
279 - trapBubbledEvent(topLevelTypes.topDrag, 'drag', mountAt);
280 - trapBubbledEvent(topLevelTypes.topDragEnd, 'dragend', mountAt);
281 - trapBubbledEvent(topLevelTypes.topDragEnter, 'dragenter', mountAt);
282 - trapBubbledEvent(topLevelTypes.topDragExit, 'dragexit', mountAt);
283 - trapBubbledEvent(topLevelTypes.topDragLeave, 'dragleave', mountAt);
284 - trapBubbledEvent(topLevelTypes.topDragOver, 'dragover', mountAt);
285 - trapBubbledEvent(topLevelTypes.topDragStart, 'dragstart', mountAt);
286 - trapBubbledEvent(topLevelTypes.topDrop, 'drop', mountAt);
287 - }
262 + if (isEventSupported('scroll', true)) {
263 + trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
264 + } else {
265 + trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window);
266 + }
267 + } else if (topLevelType === topLevelTypes.topFocus ||
268 + topLevelType === topLevelTypes.topBlur) {
269
289 - if (isEventSupported('wheel')) {
290 - trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
291 - } else if (isEventSupported('mousewheel')) {
292 - trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
293 - } else {
294 - // Firefox needs to capture a different mouse scroll event.
295 - // @see http://www.quirksmode.org/dom/events/tests/scroll.html
296 - trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt);
297 - }
270 + if (isEventSupported('focus', true)) {
271 + trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
272 + trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
273 + } else if (isEventSupported('focusin')) {
274 + // IE has `focusin` and `focusout` events which bubble.
275 + // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
276 + trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
277 + trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
278 + }
279
299 - // IE<9 does not support capturing so just trap the bubbled event there.
300 - if (isEventSupported('scroll', true)) {
301 - trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
302 - } else {
303 - trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window);
304 - }
280 + // to make sure blur and focus event listeners are only attached once
281 + listeningDocument[topLevelTypes.topBlur] = true;
282 + listeningDocument[topLevelTypes.topFocus] = true;
283 + } else {
284 + trapBubbledEvent(topLevelType, topEventMapping[dependency], mountAt);
285 + }
286
306 - if (isEventSupported('focus', true)) {
307 - trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
308 - trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
309 - } else if (isEventSupported('focusin')) {
310 - // IE has `focusin` and `focusout` events which bubble.
311 - // @see
312 - // http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
313 - trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
314 - trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
287 + listeningDocument[dependency] = true;
288 + }
289 }
290 + },
291
317 - if (isEventSupported('copy')) {
318 - trapBubbledEvent(topLevelTypes.topCopy, 'copy', mountAt);
319 - trapBubbledEvent(topLevelTypes.topCut, 'cut', mountAt);
320 - trapBubbledEvent(topLevelTypes.topPaste, 'paste', mountAt);
292 + /**
293 + * Listens to window scroll and resize events. We cache scroll values so that
294 + * application code can access them without triggering reflows.
295 + *
296 + * NOTE: Scroll events do not bubble.
297 + *
298 + * @see http://www.quirksmode.org/dom/events/scroll.html
299 + */
300 + ensureScrollValueMonitoring: function(){
301 + if (!isMonitoringScrollValue) {
302 + var refresh = ViewportMetrics.refreshScrollValues;
303 + EventListener.listen(window, 'scroll', refresh);
304 + EventListener.listen(window, 'resize', refresh);
305 + isMonitoringScrollValue = true;
306 }
307 },
308
324 - registrationNames: EventPluginHub.registrationNames,
309 + registrationNameModules: EventPluginHub.registrationNameModules,
310
311 putListener: EventPluginHub.putListener,
312
@@ -337,5 +322,4 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, {
322
323 });
324
340 -
325 module.exports = ReactEventEmitter;
src/core/ReactEventEmitterMixin.js
-32
@@ -27,38 +27,6 @@ function runEventQueueInBatch(events) {
27 }
28
29 var ReactEventEmitterMixin = {
30 - /**
31 - * Whether or not `ensureListening` has been invoked.
32 - * @type {boolean}
33 - * @private
34 - */
35 - _isListening: false,
36 -
37 - /**
38 - * Function, must be implemented. Listens to events on the top level of the
39 - * application.
40 - *
41 - * @abstract
42 - *
43 - * listenAtTopLevel: null,
44 - */
45 -
46 - /**
47 - * Ensures that top-level event delegation listeners are installed.
48 - *
49 - * There are issues with listening to both touch events and mouse events on
50 - * the top-level, so we make the caller choose which one to listen to. (If
51 - * there's a touch top-level listeners, anchors don't receive clicks for some
52 - * reason, and only in some cases).
53 - *
54 - * @param {*} config Configuration passed through to `listenAtTopLevel`.
55 - */
56 - ensureListening: function(config) {
57 - if (!config.contentDocument._reactIsListening) {
58 - this.listenAtTopLevel(config.touchNotMouse, config.contentDocument);
59 - config.contentDocument._reactIsListening = true;
60 - }
61 - },
30
31 /**
32 * Streams a fired top-level event to `EventPluginHub` where plugins have the
src/core/ReactMount.js
+11 -23
@@ -208,27 +208,6 @@ var ReactMount = {
208 renderCallback();
209 },
210
211 - /**
212 - * Ensures that the top-level event delegation listener is set up. This will
213 - * be invoked some time before the first time any React component is rendered.
214 - * @param {DOMElement} container container we're rendering into
215 - *
216 - * @private
217 - */
218 - prepareEnvironmentForDOM: function(container) {
219 - invariant(
220 - container && (
221 - container.nodeType === ELEMENT_NODE_TYPE ||
222 - container.nodeType === DOC_NODE_TYPE
223 - ),
224 - 'prepareEnvironmentForDOM(...): Target container is not a DOM element.'
225 - );
226 - var doc = container.nodeType === ELEMENT_NODE_TYPE ?
227 - container.ownerDocument :
228 - container;
229 - ReactEventEmitter.ensureListening(ReactMount.useTouchEvents, doc);
230 - },
231 -
211 /**
212 * Take a component that's already mounted into the DOM and replace its props
213 * @param {ReactComponent} prevComponent component instance already in the DOM
@@ -256,13 +235,22 @@ var ReactMount = {
235 },
236
237 /**
259 - * Register a component into the instance map and start the events system.
238 + * Register a component into the instance map and starts scroll value
239 + * monitoring
240 * @param {ReactComponent} nextComponent component instance to render
241 * @param {DOMElement} container container to render into
242 * @return {string} reactRoot ID prefix
243 */
244 _registerComponent: function(nextComponent, container) {
265 - ReactMount.prepareEnvironmentForDOM(container);
245 + invariant(
246 + container && (
247 + container.nodeType === ELEMENT_NODE_TYPE ||
248 + container.nodeType === DOC_NODE_TYPE
249 + ),
250 + '_registerComponent(...): Target container is not a DOM element.'
251 + );
252 +
253 + ReactEventEmitter.ensureScrollValueMonitoring();
254
255 var reactRootID = ReactMount.registerContainer(container);
256 instancesByReactRootID[reactRootID] = nextComponent;
src/core/__tests__/ReactComponent-test.js
+2 -2
@@ -37,14 +37,14 @@ describe('ReactComponent', function() {
37 expect(function() {
38 React.renderComponent(<div></div>, [container]);
39 }).toThrow(
40 - 'Invariant Violation: prepareEnvironmentForDOM(...): Target container ' +
40 + 'Invariant Violation: _registerComponent(...): Target container ' +
41 'is not a DOM element.'
42 );
43
44 expect(function() {
45 React.renderComponent(<div></div>, null);
46 }).toThrow(
47 - 'Invariant Violation: prepareEnvironmentForDOM(...): Target container ' +
47 + 'Invariant Violation: _registerComponent(...): Target container ' +
48 'is not a DOM element.'
49 );
50 });
src/core/__tests__/ReactEventEmitter-test.js
+42 -1
@@ -41,6 +41,7 @@ var setID = ReactMount.setID;
41 var ReactEventEmitter;
42 var ReactTestUtils;
43 var TapEventPlugin;
44 +var EventListener;
45
46 var tapMoveThreshold;
47 var idCallOrder = [];
@@ -58,6 +59,7 @@ var recordIDAndReturnFalse = function(id, event) {
59 var LISTENER = mocks.getMockFunction();
60 var ON_CLICK_KEY = keyOf({onClick: null});
61 var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null});
62 +var ON_CHANGE_KEY = keyOf({onChange: null});
63
64
65 /**
@@ -90,13 +92,13 @@ describe('ReactEventEmitter', function() {
92 EventPluginHub = require('EventPluginHub');
93 TapEventPlugin = require('TapEventPlugin');
94 ReactMount = require('ReactMount');
95 + EventListener = require('EventListener');
96 getID = ReactMount.getID;
97 setID = ReactMount.setID;
98 ReactEventEmitter = require('ReactEventEmitter');
99 ReactTestUtils = require('ReactTestUtils');
100 idCallOrder = [];
101 tapMoveThreshold = TapEventPlugin.tapMoveThreshold;
99 - ReactEventEmitter.ensureListening(false, document);
102 EventPluginHub.injection.injectEventPluginsByName({
103 TapEventPlugin: TapEventPlugin
104 });
@@ -315,6 +317,45 @@ describe('ReactEventEmitter', function() {
317 expect(idCallOrder.length).toBe(0);
318 });
319
320 + it('should listen to events only once', function() {
321 + spyOn(EventListener, 'listen');
322 + ReactEventEmitter.listenTo(ON_CLICK_KEY, document);
323 + ReactEventEmitter.listenTo(ON_CLICK_KEY, document);
324 + expect(EventListener.listen.callCount).toBe(1);
325 + });
326 +
327 + it('should work with event plugins without dependencies', function() {
328 + spyOn(EventListener, 'listen');
329 +
330 + ReactEventEmitter.listenTo(ON_CLICK_KEY, document);
331 +
332 + expect(EventListener.listen.argsForCall[0][1]).toBe('click');
333 + });
334 +
335 + it('should work with event plugins with dependencies', function() {
336 + spyOn(EventListener, 'listen');
337 + spyOn(EventListener, 'capture');
338 +
339 + ReactEventEmitter.listenTo(ON_CHANGE_KEY, document);
340 +
341 + var setEventListeners = [];
342 + var listenCalls = EventListener.listen.argsForCall;
343 + var captureCalls = EventListener.capture.argsForCall;
344 + for (var i = 0, l = listenCalls.length; i < l; i++) {
345 + setEventListeners.push(listenCalls[i][1]);
346 + }
347 + for (i = 0, l = captureCalls.length; i < l; i++) {
348 + setEventListeners.push(captureCalls[i][1]);
349 + }
350 +
351 + var module = ReactEventEmitter.registrationNameModules[ON_CHANGE_KEY];
352 + var dependencies = module.eventTypes.change.dependencies;
353 + expect(setEventListeners.length).toEqual(dependencies.length);
354 +
355 + for (i = 0, l = setEventListeners.length; i < l; i++) {
356 + expect(dependencies.indexOf(setEventListeners[i])).toBeTruthy();
357 + }
358 + });
359
360 it('should bubble onTouchTap', function() {
361 ReactEventEmitter.putListener(
src/event/EventPluginHub.js
+1 -1
@@ -104,7 +104,7 @@ var EventPluginHub = {
104
105 },
106
107 - registrationNames: EventPluginRegistry.registrationNames,
107 + registrationNameModules: EventPluginRegistry.registrationNameModules,
108
109 putListener: CallbackRegistry.putListener,
110
src/event/EventPluginRegistry.js
+21 -14
@@ -63,7 +63,7 @@ function recomputePluginOrdering() {
63 var publishedEvents = PluginModule.eventTypes;
64 for (var eventName in publishedEvents) {
65 invariant(
66 - publishEventForPlugin(publishedEvents[eventName], PluginModule),
66 + publishEventForPlugin(publishedEvents[eventName], PluginModule, eventName),
67 'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.',
68 eventName,
69 pluginName
@@ -80,18 +80,18 @@ function recomputePluginOrdering() {
80 * @return {boolean} True if the event was successfully published.
81 * @private
82 */
83 -function publishEventForPlugin(dispatchConfig, PluginModule) {
83 +function publishEventForPlugin(dispatchConfig, PluginModule, eventName) {
84 var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;
85 if (phasedRegistrationNames) {
86 for (var phaseName in phasedRegistrationNames) {
87 if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
88 var phasedRegistrationName = phasedRegistrationNames[phaseName];
89 - publishRegistrationName(phasedRegistrationName, PluginModule);
89 + publishRegistrationName(phasedRegistrationName, PluginModule, eventName);
90 }
91 }
92 return true;
93 } else if (dispatchConfig.registrationName) {
94 - publishRegistrationName(dispatchConfig.registrationName, PluginModule);
94 + publishRegistrationName(dispatchConfig.registrationName, PluginModule, eventName);
95 return true;
96 }
97 return false;
@@ -105,14 +105,16 @@ function publishEventForPlugin(dispatchConfig, PluginModule) {
105 * @param {object} PluginModule Plugin publishing the event.
106 * @private
107 */
108 -function publishRegistrationName(registrationName, PluginModule) {
108 +function publishRegistrationName(registrationName, PluginModule, eventName) {
109 invariant(
110 - !EventPluginRegistry.registrationNames[registrationName],
110 + !EventPluginRegistry.registrationNameModules[registrationName],
111 'EventPluginHub: More than one plugin attempted to publish the same ' +
112 'registration name, `%s`.',
113 registrationName
114 );
115 - EventPluginRegistry.registrationNames[registrationName] = PluginModule;
115 + EventPluginRegistry.registrationNameModules[registrationName] = PluginModule;
116 + EventPluginRegistry.registrationNameDependencies[registrationName] =
117 + PluginModule.eventTypes[eventName].dependencies;
118 }
119
120 /**
@@ -130,7 +132,12 @@ var EventPluginRegistry = {
132 /**
133 * Mapping from registration names to plugin modules.
134 */
133 - registrationNames: {},
135 + registrationNameModules: {},
136 +
137 + /**
138 + * Mapping from registration name to event name
139 + */
140 + registrationNameDependencies: {},
141
142 /**
143 * Injects an ordering of plugins (by plugin name). This allows the ordering
@@ -194,7 +201,7 @@ var EventPluginRegistry = {
201 getPluginModuleForEvent: function(event) {
202 var dispatchConfig = event.dispatchConfig;
203 if (dispatchConfig.registrationName) {
197 - return EventPluginRegistry.registrationNames[
204 + return EventPluginRegistry.registrationNameModules[
205 dispatchConfig.registrationName
206 ] || null;
207 }
@@ -202,7 +209,7 @@ var EventPluginRegistry = {
209 if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) {
210 continue;
211 }
205 - var PluginModule = EventPluginRegistry.registrationNames[
212 + var PluginModule = EventPluginRegistry.registrationNameModules[
213 dispatchConfig.phasedRegistrationNames[phase]
214 ];
215 if (PluginModule) {
@@ -224,10 +231,10 @@ var EventPluginRegistry = {
231 }
232 }
233 EventPluginRegistry.plugins.length = 0;
227 - var registrationNames = EventPluginRegistry.registrationNames;
228 - for (var registrationName in registrationNames) {
229 - if (registrationNames.hasOwnProperty(registrationName)) {
230 - delete registrationNames[registrationName];
234 + var registrationNameModules = EventPluginRegistry.registrationNameModules;
235 + for (var registrationName in registrationNameModules) {
236 + if (registrationNameModules.hasOwnProperty(registrationName)) {
237 + delete registrationNameModules[registrationName];
238 }
239 }
240 }
src/event/EventPluginUtils.js
+2 -1
@@ -179,7 +179,8 @@ var EventPluginUtils = {
179 executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
180 executeDirectDispatch: executeDirectDispatch,
181 hasDispatches: hasDispatches,
182 - executeDispatch: executeDispatch
182 + executeDispatch: executeDispatch,
183 + useTouchEvents: false
184 };
185
186 module.exports = EventPluginUtils;
src/event/__tests__/EventPluginRegistry-test.js
+6 -6
@@ -174,16 +174,16 @@ describe('EventPluginRegistry', function() {
174 EventPluginRegistry.injectEventPluginsByName({one: OnePlugin});
175 EventPluginRegistry.injectEventPluginOrder(['one', 'two']);
176
177 - expect(Object.keys(EventPluginRegistry.registrationNames).length).toBe(2);
178 - expect(EventPluginRegistry.registrationNames.onClick).toBe(OnePlugin);
179 - expect(EventPluginRegistry.registrationNames.onFocus).toBe(OnePlugin);
177 + expect(Object.keys(EventPluginRegistry.registrationNameModules).length).toBe(2);
178 + expect(EventPluginRegistry.registrationNameModules.onClick).toBe(OnePlugin);
179 + expect(EventPluginRegistry.registrationNameModules.onFocus).toBe(OnePlugin);
180
181 EventPluginRegistry.injectEventPluginsByName({two: TwoPlugin});
182
183 - expect(Object.keys(EventPluginRegistry.registrationNames).length).toBe(4);
184 - expect(EventPluginRegistry.registrationNames.onMagicBubble).toBe(TwoPlugin);
183 + expect(Object.keys(EventPluginRegistry.registrationNameModules).length).toBe(4);
184 + expect(EventPluginRegistry.registrationNameModules.onMagicBubble).toBe(TwoPlugin);
185 expect(
186 - EventPluginRegistry.registrationNames.onMagicCapture
186 + EventPluginRegistry.registrationNameModules.onMagicCapture
187 ).toBe(TwoPlugin);
188 });
189
src/eventPlugins/ChangeEventPlugin.js
+9 -1
@@ -36,7 +36,15 @@ var eventTypes = {
36 phasedRegistrationNames: {
37 bubbled: keyOf({onChange: null}),
38 captured: keyOf({onChangeCapture: null})
39 - }
39 + },
40 + dependencies: [
41 + topLevelTypes.topBlur,
42 + topLevelTypes.topChange,
43 + topLevelTypes.topFocus,
44 + topLevelTypes.topInput,
45 + topLevelTypes.topKeyDown,
46 + topLevelTypes.topKeyUp
47 + ]
48 }
49 };
50
src/eventPlugins/CompositionEventPlugin.js
+27 -3
@@ -42,19 +42,43 @@ var eventTypes = {
42 phasedRegistrationNames: {
43 bubbled: keyOf({onCompositionEnd: null}),
44 captured: keyOf({onCompositionEndCapture: null})
45 - }
45 + },
46 + dependencies: [
47 + topLevelTypes.topBlur,
48 + topLevelTypes.topCompositionEnd,
49 + topLevelTypes.topKeyDown,
50 + topLevelTypes.topKeyPress,
51 + topLevelTypes.topKeyUp,
52 + topLevelTypes.topMouseDown
53 + ]
54 },
55 compositionStart: {
56 phasedRegistrationNames: {
57 bubbled: keyOf({onCompositionStart: null}),
58 captured: keyOf({onCompositionStartCapture: null})
51 - }
59 + },
60 + dependencies: [
61 + topLevelTypes.topBlur,
62 + topLevelTypes.topCompositionStart,
63 + topLevelTypes.topKeyDown,
64 + topLevelTypes.topKeyPress,
65 + topLevelTypes.topKeyUp,
66 + topLevelTypes.topMouseDown
67 + ]
68 },
69 compositionUpdate: {
70 phasedRegistrationNames: {
71 bubbled: keyOf({onCompositionUpdate: null}),
72 captured: keyOf({onCompositionUpdateCapture: null})
57 - }
73 + },
74 + dependencies: [
75 + topLevelTypes.topBlur,
76 + topLevelTypes.topCompositionUpdate,
77 + topLevelTypes.topKeyDown,
78 + topLevelTypes.topKeyPress,
79 + topLevelTypes.topKeyUp,
80 + topLevelTypes.topMouseDown
81 + ]
82 }
83 };
84
src/eventPlugins/EnterLeaveEventPlugin.js
+14 -2
@@ -30,8 +30,20 @@ var topLevelTypes = EventConstants.topLevelTypes;
30 var getFirstReactDOM = ReactMount.getFirstReactDOM;
31
32 var eventTypes = {
33 - mouseEnter: {registrationName: keyOf({onMouseEnter: null})},
34 - mouseLeave: {registrationName: keyOf({onMouseLeave: null})}
33 + mouseEnter: {
34 + registrationName: keyOf({onMouseEnter: null}),
35 + dependencies: [
36 + topLevelTypes.topMouseOut,
37 + topLevelTypes.topMouseOver
38 + ]
39 + },
40 + mouseLeave: {
41 + registrationName: keyOf({onMouseLeave: null}),
42 + dependencies: [
43 + topLevelTypes.topMouseOut,
44 + topLevelTypes.topMouseOver
45 + ]
46 + }
47 };
48
49 var extractedEvents = [null, null];
src/eventPlugins/SelectEventPlugin.js
+9 -1
@@ -37,7 +37,15 @@ var eventTypes = {
37 phasedRegistrationNames: {
38 bubbled: keyOf({onSelect: null}),
39 captured: keyOf({onSelectCapture: null})
40 - }
40 + },
41 + dependencies: [
42 + topLevelTypes.topBlur,
43 + topLevelTypes.topFocus,
44 + topLevelTypes.topKeyDown,
45 + topLevelTypes.topMouseDown,
46 + topLevelTypes.topMouseUp,
47 + topLevelTypes.topSelectionChange
48 + ]
49 }
50 };
51
src/eventPlugins/SimpleEventPlugin.js
+4
@@ -253,6 +253,10 @@ var topLevelEventsToDispatchConfig = {
253 topWheel: eventTypes.wheel
254 };
255
256 +for (var topLevelType in topLevelEventsToDispatchConfig) {
257 + topLevelEventsToDispatchConfig[topLevelType].dependencies = [topLevelType];
258 +}
259 +
260 var SimpleEventPlugin = {
261
262 eventTypes: eventTypes,
src/eventPlugins/TapEventPlugin.js
+19 -1
@@ -19,6 +19,7 @@
19
20 "use strict";
21
22 +var EventConstants = require('EventConstants');
23 var EventPluginUtils = require('EventPluginUtils');
24 var EventPropagators = require('EventPropagators');
25 var SyntheticUIEvent = require('SyntheticUIEvent');
@@ -26,6 +27,7 @@ var TouchEventUtils = require('TouchEventUtils');
27 var ViewportMetrics = require('ViewportMetrics');
28
29 var keyOf = require('keyOf');
30 +var topLevelTypes = EventConstants.topLevelTypes;
31
32 var isStartish = EventPluginUtils.isStartish;
33 var isEndish = EventPluginUtils.isEndish;
@@ -61,12 +63,28 @@ function getDistance(coords, nativeEvent) {
63 );
64 }
65
66 +var dependencies = [
67 + topLevelTypes.topMouseDown,
68 + topLevelTypes.topMouseMove,
69 + topLevelTypes.topMouseUp
70 +];
71 +
72 +if (EventPluginUtils.useTouchEvents) {
73 + dependencies.push(
74 + topLevelTypes.topTouchCancel,
75 + topLevelTypes.topTouchEnd,
76 + topLevelTypes.topTouchStart,
77 + topLevelTypes.topTouchMove
78 + );
79 +}
80 +
81 var eventTypes = {
82 touchTap: {
83 phasedRegistrationNames: {
84 bubbled: keyOf({onTouchTap: null}),
85 captured: keyOf({onTouchTapCapture: null})
69 - }
86 + },
87 + dependencies: dependencies
88 }
89 };
90
src/eventPlugins/__tests__/AnalyticsEventPlugin-test.js
-1
@@ -61,7 +61,6 @@ describe('AnalyticsEventPlugin', function() {
61 'ChangeEventPlugin': ChangeEventPlugin
62 });
63
64 - ReactEventEmitter.ensureListening(false, document);
64 });
65
66 it('should count events correctly', function() {