UNPKG

12.4 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2013-present, Facebook, Inc.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 */
8
9'use strict';
10
11var _assign = require('object-assign');
12
13var EventPluginRegistry = require('./EventPluginRegistry');
14var ReactEventEmitterMixin = require('./ReactEventEmitterMixin');
15var ViewportMetrics = require('./ViewportMetrics');
16
17var getVendorPrefixedEventName = require('./getVendorPrefixedEventName');
18var isEventSupported = require('./isEventSupported');
19
20/**
21 * Summary of `ReactBrowserEventEmitter` event handling:
22 *
23 * - Top-level delegation is used to trap most native browser events. This
24 * may only occur in the main thread and is the responsibility of
25 * ReactEventListener, which is injected and can therefore support pluggable
26 * event sources. This is the only work that occurs in the main thread.
27 *
28 * - We normalize and de-duplicate events to account for browser quirks. This
29 * may be done in the worker thread.
30 *
31 * - Forward these native events (with the associated top-level type used to
32 * trap it) to `EventPluginHub`, which in turn will ask plugins if they want
33 * to extract any synthetic events.
34 *
35 * - The `EventPluginHub` will then process each event by annotating them with
36 * "dispatches", a sequence of listeners and IDs that care about that event.
37 *
38 * - The `EventPluginHub` then dispatches the events.
39 *
40 * Overview of React and the event system:
41 *
42 * +------------+ .
43 * | DOM | .
44 * +------------+ .
45 * | .
46 * v .
47 * +------------+ .
48 * | ReactEvent | .
49 * | Listener | .
50 * +------------+ . +-----------+
51 * | . +--------+|SimpleEvent|
52 * | . | |Plugin |
53 * +-----|------+ . v +-----------+
54 * | | | . +--------------+ +------------+
55 * | +-----------.--->|EventPluginHub| | Event |
56 * | | . | | +-----------+ | Propagators|
57 * | ReactEvent | . | | |TapEvent | |------------|
58 * | Emitter | . | |<---+|Plugin | |other plugin|
59 * | | . | | +-----------+ | utilities |
60 * | +-----------.--->| | +------------+
61 * | | | . +--------------+
62 * +-----|------+ . ^ +-----------+
63 * | . | |Enter/Leave|
64 * + . +-------+|Plugin |
65 * +-------------+ . +-----------+
66 * | application | .
67 * |-------------| .
68 * | | .
69 * | | .
70 * +-------------+ .
71 * .
72 * React Core . General Purpose Event Plugin System
73 */
74
75var hasEventPageXY;
76var alreadyListeningTo = {};
77var isMonitoringScrollValue = false;
78var reactTopListenersCounter = 0;
79
80// For events like 'submit' which don't consistently bubble (which we trap at a
81// lower node than `document`), binding at `document` would cause duplicate
82// events so we don't include them here
83var topEventMapping = {
84 topAbort: 'abort',
85 topAnimationEnd: getVendorPrefixedEventName('animationend') || 'animationend',
86 topAnimationIteration: getVendorPrefixedEventName('animationiteration') || 'animationiteration',
87 topAnimationStart: getVendorPrefixedEventName('animationstart') || 'animationstart',
88 topBlur: 'blur',
89 topCanPlay: 'canplay',
90 topCanPlayThrough: 'canplaythrough',
91 topChange: 'change',
92 topClick: 'click',
93 topCompositionEnd: 'compositionend',
94 topCompositionStart: 'compositionstart',
95 topCompositionUpdate: 'compositionupdate',
96 topContextMenu: 'contextmenu',
97 topCopy: 'copy',
98 topCut: 'cut',
99 topDoubleClick: 'dblclick',
100 topDrag: 'drag',
101 topDragEnd: 'dragend',
102 topDragEnter: 'dragenter',
103 topDragExit: 'dragexit',
104 topDragLeave: 'dragleave',
105 topDragOver: 'dragover',
106 topDragStart: 'dragstart',
107 topDrop: 'drop',
108 topDurationChange: 'durationchange',
109 topEmptied: 'emptied',
110 topEncrypted: 'encrypted',
111 topEnded: 'ended',
112 topError: 'error',
113 topFocus: 'focus',
114 topInput: 'input',
115 topKeyDown: 'keydown',
116 topKeyPress: 'keypress',
117 topKeyUp: 'keyup',
118 topLoadedData: 'loadeddata',
119 topLoadedMetadata: 'loadedmetadata',
120 topLoadStart: 'loadstart',
121 topMouseDown: 'mousedown',
122 topMouseMove: 'mousemove',
123 topMouseOut: 'mouseout',
124 topMouseOver: 'mouseover',
125 topMouseUp: 'mouseup',
126 topPaste: 'paste',
127 topPause: 'pause',
128 topPlay: 'play',
129 topPlaying: 'playing',
130 topProgress: 'progress',
131 topRateChange: 'ratechange',
132 topScroll: 'scroll',
133 topSeeked: 'seeked',
134 topSeeking: 'seeking',
135 topSelectionChange: 'selectionchange',
136 topStalled: 'stalled',
137 topSuspend: 'suspend',
138 topTextInput: 'textInput',
139 topTimeUpdate: 'timeupdate',
140 topTouchCancel: 'touchcancel',
141 topTouchEnd: 'touchend',
142 topTouchMove: 'touchmove',
143 topTouchStart: 'touchstart',
144 topTransitionEnd: getVendorPrefixedEventName('transitionend') || 'transitionend',
145 topVolumeChange: 'volumechange',
146 topWaiting: 'waiting',
147 topWheel: 'wheel'
148};
149
150/**
151 * To ensure no conflicts with other potential React instances on the page
152 */
153var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2);
154
155function getListeningForDocument(mountAt) {
156 // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
157 // directly.
158 if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
159 mountAt[topListenersIDKey] = reactTopListenersCounter++;
160 alreadyListeningTo[mountAt[topListenersIDKey]] = {};
161 }
162 return alreadyListeningTo[mountAt[topListenersIDKey]];
163}
164
165/**
166 * `ReactBrowserEventEmitter` is used to attach top-level event listeners. For
167 * example:
168 *
169 * EventPluginHub.putListener('myID', 'onClick', myFunction);
170 *
171 * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
172 *
173 * @internal
174 */
175var ReactBrowserEventEmitter = _assign({}, ReactEventEmitterMixin, {
176 /**
177 * Injectable event backend
178 */
179 ReactEventListener: null,
180
181 injection: {
182 /**
183 * @param {object} ReactEventListener
184 */
185 injectReactEventListener: function (ReactEventListener) {
186 ReactEventListener.setHandleTopLevel(ReactBrowserEventEmitter.handleTopLevel);
187 ReactBrowserEventEmitter.ReactEventListener = ReactEventListener;
188 }
189 },
190
191 /**
192 * Sets whether or not any created callbacks should be enabled.
193 *
194 * @param {boolean} enabled True if callbacks should be enabled.
195 */
196 setEnabled: function (enabled) {
197 if (ReactBrowserEventEmitter.ReactEventListener) {
198 ReactBrowserEventEmitter.ReactEventListener.setEnabled(enabled);
199 }
200 },
201
202 /**
203 * @return {boolean} True if callbacks are enabled.
204 */
205 isEnabled: function () {
206 return !!(ReactBrowserEventEmitter.ReactEventListener && ReactBrowserEventEmitter.ReactEventListener.isEnabled());
207 },
208
209 /**
210 * We listen for bubbled touch events on the document object.
211 *
212 * Firefox v8.01 (and possibly others) exhibited strange behavior when
213 * mounting `onmousemove` events at some node that was not the document
214 * element. The symptoms were that if your mouse is not moving over something
215 * contained within that mount point (for example on the background) the
216 * top-level listeners for `onmousemove` won't be called. However, if you
217 * register the `mousemove` on the document object, then it will of course
218 * catch all `mousemove`s. This along with iOS quirks, justifies restricting
219 * top-level listeners to the document object only, at least for these
220 * movement types of events and possibly all events.
221 *
222 * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
223 *
224 * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
225 * they bubble to document.
226 *
227 * @param {string} registrationName Name of listener (e.g. `onClick`).
228 * @param {object} contentDocumentHandle Document which owns the container
229 */
230 listenTo: function (registrationName, contentDocumentHandle) {
231 var mountAt = contentDocumentHandle;
232 var isListening = getListeningForDocument(mountAt);
233 var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
234
235 for (var i = 0; i < dependencies.length; i++) {
236 var dependency = dependencies[i];
237 if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
238 if (dependency === 'topWheel') {
239 if (isEventSupported('wheel')) {
240 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'wheel', mountAt);
241 } else if (isEventSupported('mousewheel')) {
242 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'mousewheel', mountAt);
243 } else {
244 // Firefox needs to capture a different mouse scroll event.
245 // @see http://www.quirksmode.org/dom/events/tests/scroll.html
246 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'DOMMouseScroll', mountAt);
247 }
248 } else if (dependency === 'topScroll') {
249 if (isEventSupported('scroll', true)) {
250 ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topScroll', 'scroll', mountAt);
251 } else {
252 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topScroll', 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
253 }
254 } else if (dependency === 'topFocus' || dependency === 'topBlur') {
255 if (isEventSupported('focus', true)) {
256 ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topFocus', 'focus', mountAt);
257 ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topBlur', 'blur', mountAt);
258 } else if (isEventSupported('focusin')) {
259 // IE has `focusin` and `focusout` events which bubble.
260 // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
261 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topFocus', 'focusin', mountAt);
262 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topBlur', 'focusout', mountAt);
263 }
264
265 // to make sure blur and focus event listeners are only attached once
266 isListening.topBlur = true;
267 isListening.topFocus = true;
268 } else if (topEventMapping.hasOwnProperty(dependency)) {
269 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
270 }
271
272 isListening[dependency] = true;
273 }
274 }
275 },
276
277 trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
278 return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle);
279 },
280
281 trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
282 return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle);
283 },
284
285 /**
286 * Protect against document.createEvent() returning null
287 * Some popup blocker extensions appear to do this:
288 * https://github.com/facebook/react/issues/6887
289 */
290 supportsEventPageXY: function () {
291 if (!document.createEvent) {
292 return false;
293 }
294 var ev = document.createEvent('MouseEvent');
295 return ev != null && 'pageX' in ev;
296 },
297
298 /**
299 * Listens to window scroll and resize events. We cache scroll values so that
300 * application code can access them without triggering reflows.
301 *
302 * ViewportMetrics is only used by SyntheticMouse/TouchEvent and only when
303 * pageX/pageY isn't supported (legacy browsers).
304 *
305 * NOTE: Scroll events do not bubble.
306 *
307 * @see http://www.quirksmode.org/dom/events/scroll.html
308 */
309 ensureScrollValueMonitoring: function () {
310 if (hasEventPageXY === undefined) {
311 hasEventPageXY = ReactBrowserEventEmitter.supportsEventPageXY();
312 }
313 if (!hasEventPageXY && !isMonitoringScrollValue) {
314 var refresh = ViewportMetrics.refreshScrollValues;
315 ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh);
316 isMonitoringScrollValue = true;
317 }
318 }
319});
320
321module.exports = ReactBrowserEventEmitter;
\No newline at end of file