UNPKG

react-addons

Version:

Simple packaging of react addons to avoid fiddly 'react/addons' npm module.

340 lines (306 loc) 12.1 kB
/** * Copyright 2013-2014 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @providesModule ReactEventEmitter * @typechecks static-only */ "use strict"; var EventConstants = require("./EventConstants"); var EventListener = require("./EventListener"); var EventPluginHub = require("./EventPluginHub"); var EventPluginRegistry = require("./EventPluginRegistry"); var ExecutionEnvironment = require("./ExecutionEnvironment"); var ReactEventEmitterMixin = require("./ReactEventEmitterMixin"); var ViewportMetrics = require("./ViewportMetrics"); var invariant = require("./invariant"); var isEventSupported = require("./isEventSupported"); var merge = require("./merge"); /** * Summary of `ReactEventEmitter` event handling: * * - Top-level delegation is used to trap native browser events. We normalize * and de-duplicate events to account for browser quirks. * * - 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 | . * +------------+ . +-----------+ * + . +--------+|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', 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) { if (mountAt[topListenersIDKey] == null) { mountAt[topListenersIDKey] = reactTopListenersCounter++; alreadyListeningTo[mountAt[topListenersIDKey]] = {}; } return alreadyListeningTo[mountAt[topListenersIDKey]]; } /** * Traps top-level events by using event bubbling. * * @param {string} topLevelType Record from `EventConstants`. * @param {string} handlerBaseName Event name (e.g. "click"). * @param {DOMEventTarget} element Element on which to attach listener. * @internal */ function trapBubbledEvent(topLevelType, handlerBaseName, element) { EventListener.listen( element, handlerBaseName, ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( topLevelType ) ); } /** * Traps a top-level event by using event capturing. * * @param {string} topLevelType Record from `EventConstants`. * @param {string} handlerBaseName Event name (e.g. "click"). * @param {DOMEventTarget} element Element on which to attach listener. * @internal */ function trapCapturedEvent(topLevelType, handlerBaseName, element) { EventListener.capture( element, handlerBaseName, ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( topLevelType ) ); } /** * `ReactEventEmitter` is used to attach top-level event listeners. For example: * * ReactEventEmitter.putListener('myID', 'onClick', myFunction); * * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'. * * @internal */ var ReactEventEmitter = merge(ReactEventEmitterMixin, { /** * React references `ReactEventTopLevelCallback` using this property in order * to allow dependency injection. */ TopLevelCallbackCreator: null, injection: { /** * @param {function} TopLevelCallbackCreator */ injectTopLevelCallbackCreator: function(TopLevelCallbackCreator) { ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator; } }, /** * Sets whether or not any created callbacks should be enabled. * * @param {boolean} enabled True if callbacks should be enabled. */ setEnabled: function(enabled) { ("production" !== process.env.NODE_ENV ? invariant( ExecutionEnvironment.canUseDOM, 'setEnabled(...): Cannot toggle event listening in a Worker thread. ' + 'This is likely a bug in the framework. Please report immediately.' ) : invariant(ExecutionEnvironment.canUseDOM)); if (ReactEventEmitter.TopLevelCallbackCreator) { ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled); } }, /** * @return {boolean} True if callbacks are enabled. */ isEnabled: function() { return !!( ReactEventEmitter.TopLevelCallbackCreator && ReactEventEmitter.TopLevelCallbackCreator.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 {DOMDocument} contentDocument Document which owns the container */ listenTo: function(registrationName, contentDocument) { var mountAt = contentDocument; 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[dependency]) { var topLevelType = topLevelTypes[dependency]; if (topLevelType === topLevelTypes.topWheel) { if (isEventSupported('wheel')) { trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt); } else if (isEventSupported('mousewheel')) { 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 trapBubbledEvent( topLevelTypes.topWheel, 'DOMMouseScroll', mountAt); } } else if (topLevelType === topLevelTypes.topScroll) { if (isEventSupported('scroll', true)) { trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); } else { trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window); } } else if (topLevelType === topLevelTypes.topFocus || topLevelType === topLevelTypes.topBlur) { if (isEventSupported('focus', true)) { trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt); 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 trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt); 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[dependency]) { trapBubbledEvent(topLevelType, topEventMapping[dependency], mountAt); } isListening[dependency] = true; } } }, /** * 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; EventListener.listen(window, 'scroll', refresh); EventListener.listen(window, 'resize', refresh); isMonitoringScrollValue = true; } }, eventNameDispatchConfigs: EventPluginHub.eventNameDispatchConfigs, registrationNameModules: EventPluginHub.registrationNameModules, putListener: EventPluginHub.putListener, getListener: EventPluginHub.getListener, deleteListener: EventPluginHub.deleteListener, deleteAllListeners: EventPluginHub.deleteAllListeners, trapBubbledEvent: trapBubbledEvent, trapCapturedEvent: trapCapturedEvent }); module.exports = ReactEventEmitter;