327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
|
/**
|
||
|
* Copyright 2013-present, Facebook, Inc.
|
||
|
* All rights reserved.
|
||
|
*
|
||
|
* This source code is licensed under the BSD-style license found in the
|
||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var _assign = require('object-assign');
|
||
|
|
||
|
var EventPluginRegistry = require('./EventPluginRegistry');
|
||
|
var ReactEventEmitterMixin = require('./ReactEventEmitterMixin');
|
||
|
var ViewportMetrics = require('./ViewportMetrics');
|
||
|
|
||
|
var getVendorPrefixedEventName = require('./getVendorPrefixedEventName');
|
||
|
var isEventSupported = require('./isEventSupported');
|
||
|
|
||
|
/**
|
||
|
* Summary of `ReactBrowserEventEmitter` event handling:
|
||
|
*
|
||
|
* - Top-level delegation is used to trap most native browser events. This
|
||
|
* may only occur in the main thread and is the responsibility of
|
||
|
* ReactEventListener, which is injected and can therefore support pluggable
|
||
|
* event sources. This is the only work that occurs in the main thread.
|
||
|
*
|
||
|
* - We normalize and de-duplicate events to account for browser quirks. This
|
||
|
* may be done in the worker thread.
|
||
|
*
|
||
|
* - Forward these native events (with the associated top-level type used to
|
||
|
* trap it) to `EventPluginHub`, which in turn will ask plugins if they want
|
||
|
* to extract any synthetic events.
|
||
|
*
|
||
|
* - The `EventPluginHub` will then process each event by annotating them with
|
||
|
* "dispatches", a sequence of listeners and IDs that care about that event.
|
||
|
*
|
||
|
* - The `EventPluginHub` then dispatches the events.
|
||
|
*
|
||
|
* Overview of React and the event system:
|
||
|
*
|
||
|
* +------------+ .
|
||
|
* | DOM | .
|
||
|
* +------------+ .
|
||
|
* | .
|
||
|
* v .
|
||
|
* +------------+ .
|
||
|
* | ReactEvent | .
|
||
|
* | Listener | .
|
||
|
* +------------+ . +-----------+
|
||
|
* | . +--------+|SimpleEvent|
|
||
|
* | . | |Plugin |
|
||
|
* +-----|------+ . v +-----------+
|
||
|
* | | | . +--------------+ +------------+
|
||
|
* | +-----------.--->|EventPluginHub| | Event |
|
||
|
* | | . | | +-----------+ | Propagators|
|
||
|
* | ReactEvent | . | | |TapEvent | |------------|
|
||
|
* | Emitter | . | |<---+|Plugin | |other plugin|
|
||
|
* | | . | | +-----------+ | utilities |
|
||
|
* | +-----------.--->| | +------------+
|
||
|
* | | | . +--------------+
|
||
|
* +-----|------+ . ^ +-----------+
|
||
|
* | . | |Enter/Leave|
|
||
|
* + . +-------+|Plugin |
|
||
|
* +-------------+ . +-----------+
|
||
|
* | application | .
|
||
|
* |-------------| .
|
||
|
* | | .
|
||
|
* | | .
|
||
|
* +-------------+ .
|
||
|
* .
|
||
|
* React Core . General Purpose Event Plugin System
|
||
|
*/
|
||
|
|
||
|
var hasEventPageXY;
|
||
|
var alreadyListeningTo = {};
|
||
|
var isMonitoringScrollValue = false;
|
||
|
var reactTopListenersCounter = 0;
|
||
|
|
||
|
// For events like 'submit' which don't consistently bubble (which we trap at a
|
||
|
// lower node than `document`), binding at `document` would cause duplicate
|
||
|
// events so we don't include them here
|
||
|
var topEventMapping = {
|
||
|
topAbort: 'abort',
|
||
|
topAnimationEnd: getVendorPrefixedEventName('animationend') || 'animationend',
|
||
|
topAnimationIteration: getVendorPrefixedEventName('animationiteration') || 'animationiteration',
|
||
|
topAnimationStart: getVendorPrefixedEventName('animationstart') || 'animationstart',
|
||
|
topBlur: 'blur',
|
||
|
topCanPlay: 'canplay',
|
||
|
topCanPlayThrough: 'canplaythrough',
|
||
|
topChange: 'change',
|
||
|
topClick: 'click',
|
||
|
topCompositionEnd: 'compositionend',
|
||
|
topCompositionStart: 'compositionstart',
|
||
|
topCompositionUpdate: 'compositionupdate',
|
||
|
topContextMenu: 'contextmenu',
|
||
|
topCopy: 'copy',
|
||
|
topCut: 'cut',
|
||
|
topDoubleClick: 'dblclick',
|
||
|
topDrag: 'drag',
|
||
|
topDragEnd: 'dragend',
|
||
|
topDragEnter: 'dragenter',
|
||
|
topDragExit: 'dragexit',
|
||
|
topDragLeave: 'dragleave',
|
||
|
topDragOver: 'dragover',
|
||
|
topDragStart: 'dragstart',
|
||
|
topDrop: 'drop',
|
||
|
topDurationChange: 'durationchange',
|
||
|
topEmptied: 'emptied',
|
||
|
topEncrypted: 'encrypted',
|
||
|
topEnded: 'ended',
|
||
|
topError: 'error',
|
||
|
topFocus: 'focus',
|
||
|
topInput: 'input',
|
||
|
topKeyDown: 'keydown',
|
||
|
topKeyPress: 'keypress',
|
||
|
topKeyUp: 'keyup',
|
||
|
topLoadedData: 'loadeddata',
|
||
|
topLoadedMetadata: 'loadedmetadata',
|
||
|
topLoadStart: 'loadstart',
|
||
|
topMouseDown: 'mousedown',
|
||
|
topMouseMove: 'mousemove',
|
||
|
topMouseOut: 'mouseout',
|
||
|
topMouseOver: 'mouseover',
|
||
|
topMouseUp: 'mouseup',
|
||
|
topPaste: 'paste',
|
||
|
topPause: 'pause',
|
||
|
topPlay: 'play',
|
||
|
topPlaying: 'playing',
|
||
|
topProgress: 'progress',
|
||
|
topRateChange: 'ratechange',
|
||
|
topScroll: 'scroll',
|
||
|
topSeeked: 'seeked',
|
||
|
topSeeking: 'seeking',
|
||
|
topSelectionChange: 'selectionchange',
|
||
|
topStalled: 'stalled',
|
||
|
topSuspend: 'suspend',
|
||
|
topTextInput: 'textInput',
|
||
|
topTimeUpdate: 'timeupdate',
|
||
|
topTouchCancel: 'touchcancel',
|
||
|
topTouchEnd: 'touchend',
|
||
|
topTouchMove: 'touchmove',
|
||
|
topTouchStart: 'touchstart',
|
||
|
topTransitionEnd: getVendorPrefixedEventName('transitionend') || 'transitionend',
|
||
|
topVolumeChange: 'volumechange',
|
||
|
topWaiting: 'waiting',
|
||
|
topWheel: 'wheel'
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* To ensure no conflicts with other potential React instances on the page
|
||
|
*/
|
||
|
var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2);
|
||
|
|
||
|
function getListeningForDocument(mountAt) {
|
||
|
// In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
|
||
|
// directly.
|
||
|
if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
|
||
|
mountAt[topListenersIDKey] = reactTopListenersCounter++;
|
||
|
alreadyListeningTo[mountAt[topListenersIDKey]] = {};
|
||
|
}
|
||
|
return alreadyListeningTo[mountAt[topListenersIDKey]];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* `ReactBrowserEventEmitter` is used to attach top-level event listeners. For
|
||
|
* example:
|
||
|
*
|
||
|
* EventPluginHub.putListener('myID', 'onClick', myFunction);
|
||
|
*
|
||
|
* This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
|
||
|
*
|
||
|
* @internal
|
||
|
*/
|
||
|
var ReactBrowserEventEmitter = _assign({}, ReactEventEmitterMixin, {
|
||
|
|
||
|
/**
|
||
|
* Injectable event backend
|
||
|
*/
|
||
|
ReactEventListener: null,
|
||
|
|
||
|
injection: {
|
||
|
/**
|
||
|
* @param {object} ReactEventListener
|
||
|
*/
|
||
|
injectReactEventListener: function (ReactEventListener) {
|
||
|
ReactEventListener.setHandleTopLevel(ReactBrowserEventEmitter.handleTopLevel);
|
||
|
ReactBrowserEventEmitter.ReactEventListener = ReactEventListener;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Sets whether or not any created callbacks should be enabled.
|
||
|
*
|
||
|
* @param {boolean} enabled True if callbacks should be enabled.
|
||
|
*/
|
||
|
setEnabled: function (enabled) {
|
||
|
if (ReactBrowserEventEmitter.ReactEventListener) {
|
||
|
ReactBrowserEventEmitter.ReactEventListener.setEnabled(enabled);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* @return {boolean} True if callbacks are enabled.
|
||
|
*/
|
||
|
isEnabled: function () {
|
||
|
return !!(ReactBrowserEventEmitter.ReactEventListener && ReactBrowserEventEmitter.ReactEventListener.isEnabled());
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* 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.
|
||
|
*
|
||
|
* @param {string} registrationName Name of listener (e.g. `onClick`).
|
||
|
* @param {object} contentDocumentHandle Document which owns the container
|
||
|
*/
|
||
|
listenTo: function (registrationName, contentDocumentHandle) {
|
||
|
var mountAt = contentDocumentHandle;
|
||
|
var isListening = getListeningForDocument(mountAt);
|
||
|
var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
|
||
|
|
||
|
for (var i = 0; i < dependencies.length; i++) {
|
||
|
var dependency = dependencies[i];
|
||
|
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
|
||
|
if (dependency === 'topWheel') {
|
||
|
if (isEventSupported('wheel')) {
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'wheel', mountAt);
|
||
|
} else if (isEventSupported('mousewheel')) {
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'mousewheel', mountAt);
|
||
|
} else {
|
||
|
// Firefox needs to capture a different mouse scroll event.
|
||
|
// @see http://www.quirksmode.org/dom/events/tests/scroll.html
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'DOMMouseScroll', mountAt);
|
||
|
}
|
||
|
} else if (dependency === 'topScroll') {
|
||
|
|
||
|
if (isEventSupported('scroll', true)) {
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topScroll', 'scroll', mountAt);
|
||
|
} else {
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topScroll', 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
|
||
|
}
|
||
|
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
|
||
|
|
||
|
if (isEventSupported('focus', true)) {
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topFocus', 'focus', mountAt);
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topBlur', 'blur', mountAt);
|
||
|
} else if (isEventSupported('focusin')) {
|
||
|
// IE has `focusin` and `focusout` events which bubble.
|
||
|
// @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topFocus', 'focusin', mountAt);
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topBlur', 'focusout', mountAt);
|
||
|
}
|
||
|
|
||
|
// to make sure blur and focus event listeners are only attached once
|
||
|
isListening.topBlur = true;
|
||
|
isListening.topFocus = true;
|
||
|
} else if (topEventMapping.hasOwnProperty(dependency)) {
|
||
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
|
||
|
}
|
||
|
|
||
|
isListening[dependency] = true;
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
|
||
|
return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle);
|
||
|
},
|
||
|
|
||
|
trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
|
||
|
return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Protect against document.createEvent() returning null
|
||
|
* Some popup blocker extensions appear to do this:
|
||
|
* https://github.com/facebook/react/issues/6887
|
||
|
*/
|
||
|
supportsEventPageXY: function () {
|
||
|
if (!document.createEvent) {
|
||
|
return false;
|
||
|
}
|
||
|
var ev = document.createEvent('MouseEvent');
|
||
|
return ev != null && 'pageX' in ev;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Listens to window scroll and resize events. We cache scroll values so that
|
||
|
* application code can access them without triggering reflows.
|
||
|
*
|
||
|
* ViewportMetrics is only used by SyntheticMouse/TouchEvent and only when
|
||
|
* pageX/pageY isn't supported (legacy browsers).
|
||
|
*
|
||
|
* NOTE: Scroll events do not bubble.
|
||
|
*
|
||
|
* @see http://www.quirksmode.org/dom/events/scroll.html
|
||
|
*/
|
||
|
ensureScrollValueMonitoring: function () {
|
||
|
if (hasEventPageXY === undefined) {
|
||
|
hasEventPageXY = ReactBrowserEventEmitter.supportsEventPageXY();
|
||
|
}
|
||
|
if (!hasEventPageXY && !isMonitoringScrollValue) {
|
||
|
var refresh = ViewportMetrics.refreshScrollValues;
|
||
|
ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh);
|
||
|
isMonitoringScrollValue = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
});
|
||
|
|
||
|
module.exports = ReactBrowserEventEmitter;
|