You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
12 KiB
351 lines
12 KiB
/** |
|
* Copyright 2013-2015, 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. |
|
* |
|
* @providesModule ReactBrowserEventEmitter |
|
* @typechecks static-only |
|
*/ |
|
|
|
'use strict'; |
|
|
|
var EventConstants = require("./EventConstants"); |
|
var EventPluginHub = require("./EventPluginHub"); |
|
var EventPluginRegistry = require("./EventPluginRegistry"); |
|
var ReactEventEmitterMixin = require("./ReactEventEmitterMixin"); |
|
var ViewportMetrics = require("./ViewportMetrics"); |
|
|
|
var assign = require("./Object.assign"); |
|
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 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 = { |
|
topBlur: 'blur', |
|
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', |
|
topFocus: 'focus', |
|
topInput: 'input', |
|
topKeyDown: 'keydown', |
|
topKeyPress: 'keypress', |
|
topKeyUp: 'keyup', |
|
topMouseDown: 'mousedown', |
|
topMouseMove: 'mousemove', |
|
topMouseOut: 'mouseout', |
|
topMouseOver: 'mouseover', |
|
topMouseUp: 'mouseup', |
|
topPaste: 'paste', |
|
topScroll: 'scroll', |
|
topSelectionChange: 'selectionchange', |
|
topTextInput: 'textInput', |
|
topTouchCancel: 'touchcancel', |
|
topTouchEnd: 'touchend', |
|
topTouchMove: 'touchmove', |
|
topTouchStart: 'touchstart', |
|
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: |
|
* |
|
* ReactBrowserEventEmitter.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]; |
|
|
|
var topLevelTypes = EventConstants.topLevelTypes; |
|
for (var i = 0, l = dependencies.length; i < l; i++) { |
|
var dependency = dependencies[i]; |
|
if (!( |
|
(isListening.hasOwnProperty(dependency) && isListening[dependency]) |
|
)) { |
|
if (dependency === topLevelTypes.topWheel) { |
|
if (isEventSupported('wheel')) { |
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( |
|
topLevelTypes.topWheel, |
|
'wheel', |
|
mountAt |
|
); |
|
} else if (isEventSupported('mousewheel')) { |
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( |
|
topLevelTypes.topWheel, |
|
'mousewheel', |
|
mountAt |
|
); |
|
} else { |
|
// Firefox needs to capture a different mouse scroll event. |
|
// @see http://www.quirksmode.org/dom/events/tests/scroll.html |
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( |
|
topLevelTypes.topWheel, |
|
'DOMMouseScroll', |
|
mountAt |
|
); |
|
} |
|
} else if (dependency === topLevelTypes.topScroll) { |
|
|
|
if (isEventSupported('scroll', true)) { |
|
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( |
|
topLevelTypes.topScroll, |
|
'scroll', |
|
mountAt |
|
); |
|
} else { |
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( |
|
topLevelTypes.topScroll, |
|
'scroll', |
|
ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE |
|
); |
|
} |
|
} else if (dependency === topLevelTypes.topFocus || |
|
dependency === topLevelTypes.topBlur) { |
|
|
|
if (isEventSupported('focus', true)) { |
|
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( |
|
topLevelTypes.topFocus, |
|
'focus', |
|
mountAt |
|
); |
|
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( |
|
topLevelTypes.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( |
|
topLevelTypes.topFocus, |
|
'focusin', |
|
mountAt |
|
); |
|
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( |
|
topLevelTypes.topBlur, |
|
'focusout', |
|
mountAt |
|
); |
|
} |
|
|
|
// to make sure blur and focus event listeners are only attached once |
|
isListening[topLevelTypes.topBlur] = true; |
|
isListening[topLevelTypes.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 |
|
); |
|
}, |
|
|
|
/** |
|
* Listens to window scroll and resize events. We cache scroll values so that |
|
* application code can access them without triggering reflows. |
|
* |
|
* NOTE: Scroll events do not bubble. |
|
* |
|
* @see http://www.quirksmode.org/dom/events/scroll.html |
|
*/ |
|
ensureScrollValueMonitoring: function() { |
|
if (!isMonitoringScrollValue) { |
|
var refresh = ViewportMetrics.refreshScrollValues; |
|
ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh); |
|
isMonitoringScrollValue = true; |
|
} |
|
}, |
|
|
|
eventNameDispatchConfigs: EventPluginHub.eventNameDispatchConfigs, |
|
|
|
registrationNameModules: EventPluginHub.registrationNameModules, |
|
|
|
putListener: EventPluginHub.putListener, |
|
|
|
getListener: EventPluginHub.getListener, |
|
|
|
deleteListener: EventPluginHub.deleteListener, |
|
|
|
deleteAllListeners: EventPluginHub.deleteAllListeners |
|
|
|
}); |
|
|
|
module.exports = ReactBrowserEventEmitter;
|
|
|