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() {