diff --git a/src/core/React.js b/src/core/React.js index da486916e4d5..f82f4f787e2e 100644 --- a/src/core/React.js +++ b/src/core/React.js @@ -19,6 +19,7 @@ "use strict"; var DOMPropertyOperations = require('DOMPropertyOperations'); +var EventPluginUtils = require('EventPluginUtils'); var ReactComponent = require('ReactComponent'); var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactContext = require('ReactContext'); @@ -40,7 +41,7 @@ var React = { DOM: ReactDOM, PropTypes: ReactPropTypes, initializeTouchEvents: function(shouldUseTouch) { - ReactMount.useTouchEvents = shouldUseTouch; + EventPluginUtils.useTouchEvents = shouldUseTouch; }, createClass: ReactCompositeComponent.createClass, constructAndRenderComponent: ReactMount.constructAndRenderComponent, diff --git a/src/core/ReactDOMComponent.js b/src/core/ReactDOMComponent.js index 540ed03adec6..44e983510f0b 100644 --- a/src/core/ReactDOMComponent.js +++ b/src/core/ReactDOMComponent.js @@ -24,6 +24,7 @@ var DOMProperty = require('DOMProperty'); var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactComponent = require('ReactComponent'); var ReactEventEmitter = require('ReactEventEmitter'); +var ReactMount = require('ReactMount'); var ReactMultiChild = require('ReactMultiChild'); var ReactPerf = require('ReactPerf'); @@ -33,15 +34,17 @@ var keyOf = require('keyOf'); var merge = require('merge'); var mixInto = require('mixInto'); -var putListener = ReactEventEmitter.putListener; var deleteListener = ReactEventEmitter.deleteListener; -var registrationNames = ReactEventEmitter.registrationNames; +var listenTo = ReactEventEmitter.listenTo; +var registrationNameModules = ReactEventEmitter.registrationNameModules; // For quickly matching children type, to test if can be treated as content. var CONTENT_TYPES = {'string': true, 'number': true}; var STYLE = keyOf({style: null}); +var ELEMENT_NODE_TYPE = 1; + /** * @param {?object} props */ @@ -61,6 +64,19 @@ function assertValidProps(props) { ); } +function putListener(id, registrationName, listener) { + var container = ReactMount.findReactContainerForID(id); + if (container) { + var doc = container.nodeType === ELEMENT_NODE_TYPE ? + container.ownerDocument : + container; + listenTo(registrationName, doc); + } + + ReactEventEmitter.putListener(id, registrationName, listener); +} + + /** * @constructor ReactDOMComponent * @extends ReactComponent @@ -126,7 +142,7 @@ ReactDOMComponent.Mixin = { if (propValue == null) { continue; } - if (registrationNames[propKey]) { + if (registrationNameModules[propKey]) { putListener(this._rootNodeID, propKey, propValue); } else { if (propKey === STYLE) { @@ -243,7 +259,7 @@ ReactDOMComponent.Mixin = { styleUpdates[styleName] = ''; } } - } else if (registrationNames[propKey]) { + } else if (registrationNameModules[propKey]) { deleteListener(this._rootNodeID, propKey); } else if ( DOMProperty.isStandardName[propKey] || @@ -285,7 +301,7 @@ ReactDOMComponent.Mixin = { // Relies on `updateStylesByID` not mutating `styleUpdates`. styleUpdates = nextProp; } - } else if (registrationNames[propKey]) { + } else if (registrationNameModules[propKey]) { putListener(this._rootNodeID, propKey, nextProp); } else if ( DOMProperty.isStandardName[propKey] || diff --git a/src/core/ReactEventEmitter.js b/src/core/ReactEventEmitter.js index 4d523957cc87..f98fc0a86bc5 100644 --- a/src/core/ReactEventEmitter.js +++ b/src/core/ReactEventEmitter.js @@ -22,6 +22,7 @@ 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'); @@ -75,6 +76,62 @@ var merge = require('merge'); * React Core . General Purpose Event Plugin System */ +var alreadyListeningTo = {}; +var isMonitoringScrollValue = false; +var reactTopListenersCounter = 0; +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', + topSubmit: 'submit', + 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 getListeningDocument(mountAt) { + if (mountAt[topListenersIDKey] == null) { + mountAt[topListenersIDKey] = reactTopListenersCounter++; + alreadyListeningTo[mountAt[topListenersIDKey]] = {}; + } + return alreadyListeningTo[mountAt[topListenersIDKey]]; +} + /** * Traps top-level events by using event bubbling. * @@ -111,21 +168,6 @@ function trapCapturedEvent(topLevelType, handlerBaseName, element) { ); } -/** - * 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. - * - * @private - * @see http://www.quirksmode.org/dom/events/scroll.html - */ -function registerScrollValueMonitoring() { - var refresh = ViewportMetrics.refreshScrollValues; - EventListener.listen(window, 'scroll', refresh); - EventListener.listen(window, 'resize', refresh); -} - /** * `ReactEventEmitter` is used to attach top-level event listeners. For example: * @@ -143,39 +185,6 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { */ TopLevelCallbackCreator: null, - /** - * Ensures that top-level event delegation listeners are installed. - * - * There are issues with listening to both touch events and mouse events on - * the top-level, so we make the caller choose which one to listen to. (If - * there's a touch top-level listeners, anchors don't receive clicks for some - * reason, and only in some cases). - * - * @param {boolean} touchNotMouse Listen to touch events instead of mouse. - * @param {DOMDocument} contentDocument DOM document to listen on - */ - ensureListening: function(touchNotMouse, contentDocument) { - invariant( - ExecutionEnvironment.canUseDOM, - 'ensureListening(...): Cannot toggle event listening in a Worker ' + - 'thread. This is likely a bug in the framework. Please report ' + - 'immediately.' - ); - invariant( - ReactEventEmitter.TopLevelCallbackCreator, - 'ensureListening(...): Cannot be called without a top level callback ' + - 'creator being injected.' - ); - // Call out to base implementation. - ReactEventEmitterMixin.ensureListening.call( - ReactEventEmitter, - { - touchNotMouse: touchNotMouse, - contentDocument: contentDocument - } - ); - }, - /** * Sets whether or not any created callbacks should be enabled. * @@ -220,108 +229,84 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but * they bubble to document. * - * @param {boolean} touchNotMouse Listen to touch events instead of mouse. + * @param {string} registrationName Name of listener (e.g. `onClick`). * @param {DOMDocument} contentDocument Document which owns the container - * @private - * @see http://www.quirksmode.org/dom/events/keys.html. */ - listenAtTopLevel: function(touchNotMouse, contentDocument) { - invariant( - !contentDocument._isListening, - 'listenAtTopLevel(...): Cannot setup top-level listener more than once.' - ); - var topLevelTypes = EventConstants.topLevelTypes; + listenTo: function(registrationName, contentDocument) { var mountAt = contentDocument; + var listeningDocument = getListeningDocument(mountAt); + var dependencies = EventPluginRegistry. + registrationNameDependencies[registrationName]; - registerScrollValueMonitoring(); - trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt); - trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt); - trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt); - trapBubbledEvent(topLevelTypes.topMouseMove, 'mousemove', mountAt); - trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt); - trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt); - trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt); - trapBubbledEvent(topLevelTypes.topContextMenu, 'contextmenu', mountAt); - if (touchNotMouse) { - trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt); - trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt); - trapBubbledEvent(topLevelTypes.topTouchMove, 'touchmove', mountAt); - trapBubbledEvent(topLevelTypes.topTouchCancel, 'touchcancel', mountAt); - } - trapBubbledEvent(topLevelTypes.topKeyUp, 'keyup', mountAt); - trapBubbledEvent(topLevelTypes.topKeyPress, 'keypress', mountAt); - trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt); - trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt); - trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt); - trapBubbledEvent( - topLevelTypes.topSelectionChange, - 'selectionchange', - mountAt - ); + var topLevelTypes = EventConstants.topLevelTypes; + for (var i = 0, l = dependencies.length; i < l; i++) { + var dependency = dependencies[i]; + if (!listeningDocument[dependency]) { + var topLevelType = topLevelTypes[dependency]; - trapBubbledEvent( - topLevelTypes.topCompositionEnd, - 'compositionend', - mountAt - ); - trapBubbledEvent( - topLevelTypes.topCompositionStart, - 'compositionstart', - mountAt - ); - trapBubbledEvent( - topLevelTypes.topCompositionUpdate, - 'compositionupdate', - mountAt - ); + 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('drag')) { - trapBubbledEvent(topLevelTypes.topDrag, 'drag', mountAt); - trapBubbledEvent(topLevelTypes.topDragEnd, 'dragend', mountAt); - trapBubbledEvent(topLevelTypes.topDragEnter, 'dragenter', mountAt); - trapBubbledEvent(topLevelTypes.topDragExit, 'dragexit', mountAt); - trapBubbledEvent(topLevelTypes.topDragLeave, 'dragleave', mountAt); - trapBubbledEvent(topLevelTypes.topDragOver, 'dragover', mountAt); - trapBubbledEvent(topLevelTypes.topDragStart, 'dragstart', mountAt); - trapBubbledEvent(topLevelTypes.topDrop, 'drop', mountAt); - } + 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('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); - } + 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); + } - // IE<9 does not support capturing so just trap the bubbled event there. - if (isEventSupported('scroll', true)) { - trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); - } else { - trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window); - } + // to make sure blur and focus event listeners are only attached once + listeningDocument[topLevelTypes.topBlur] = true; + listeningDocument[topLevelTypes.topFocus] = true; + } else { + trapBubbledEvent(topLevelType, topEventMapping[dependency], mountAt); + } - 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); + listeningDocument[dependency] = true; + } } + }, - if (isEventSupported('copy')) { - trapBubbledEvent(topLevelTypes.topCopy, 'copy', mountAt); - trapBubbledEvent(topLevelTypes.topCut, 'cut', mountAt); - trapBubbledEvent(topLevelTypes.topPaste, 'paste', mountAt); + /** + * 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; } }, - registrationNames: EventPluginHub.registrationNames, + registrationNameModules: EventPluginHub.registrationNameModules, putListener: EventPluginHub.putListener, @@ -337,5 +322,4 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { }); - module.exports = ReactEventEmitter; diff --git a/src/core/ReactEventEmitterMixin.js b/src/core/ReactEventEmitterMixin.js index 805a091759fd..b4180a2e70ae 100644 --- a/src/core/ReactEventEmitterMixin.js +++ b/src/core/ReactEventEmitterMixin.js @@ -27,38 +27,6 @@ function runEventQueueInBatch(events) { } var ReactEventEmitterMixin = { - /** - * Whether or not `ensureListening` has been invoked. - * @type {boolean} - * @private - */ - _isListening: false, - - /** - * Function, must be implemented. Listens to events on the top level of the - * application. - * - * @abstract - * - * listenAtTopLevel: null, - */ - - /** - * Ensures that top-level event delegation listeners are installed. - * - * There are issues with listening to both touch events and mouse events on - * the top-level, so we make the caller choose which one to listen to. (If - * there's a touch top-level listeners, anchors don't receive clicks for some - * reason, and only in some cases). - * - * @param {*} config Configuration passed through to `listenAtTopLevel`. - */ - ensureListening: function(config) { - if (!config.contentDocument._reactIsListening) { - this.listenAtTopLevel(config.touchNotMouse, config.contentDocument); - config.contentDocument._reactIsListening = true; - } - }, /** * Streams a fired top-level event to `EventPluginHub` where plugins have the diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index 598c5633fccd..7947c91bdeaf 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -208,27 +208,6 @@ var ReactMount = { renderCallback(); }, - /** - * Ensures that the top-level event delegation listener is set up. This will - * be invoked some time before the first time any React component is rendered. - * @param {DOMElement} container container we're rendering into - * - * @private - */ - prepareEnvironmentForDOM: function(container) { - invariant( - container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE - ), - 'prepareEnvironmentForDOM(...): Target container is not a DOM element.' - ); - var doc = container.nodeType === ELEMENT_NODE_TYPE ? - container.ownerDocument : - container; - ReactEventEmitter.ensureListening(ReactMount.useTouchEvents, doc); - }, - /** * Take a component that's already mounted into the DOM and replace its props * @param {ReactComponent} prevComponent component instance already in the DOM @@ -256,13 +235,22 @@ var ReactMount = { }, /** - * Register a component into the instance map and start the events system. + * Register a component into the instance map and starts scroll value + * monitoring * @param {ReactComponent} nextComponent component instance to render * @param {DOMElement} container container to render into * @return {string} reactRoot ID prefix */ _registerComponent: function(nextComponent, container) { - ReactMount.prepareEnvironmentForDOM(container); + invariant( + container && ( + container.nodeType === ELEMENT_NODE_TYPE || + container.nodeType === DOC_NODE_TYPE + ), + '_registerComponent(...): Target container is not a DOM element.' + ); + + ReactEventEmitter.ensureScrollValueMonitoring(); var reactRootID = ReactMount.registerContainer(container); instancesByReactRootID[reactRootID] = nextComponent; diff --git a/src/core/__tests__/ReactComponent-test.js b/src/core/__tests__/ReactComponent-test.js index 95e0882bd5f7..7b045c1dcc39 100644 --- a/src/core/__tests__/ReactComponent-test.js +++ b/src/core/__tests__/ReactComponent-test.js @@ -37,14 +37,14 @@ describe('ReactComponent', function() { expect(function() { React.renderComponent(
, [container]); }).toThrow( - 'Invariant Violation: prepareEnvironmentForDOM(...): Target container ' + + 'Invariant Violation: _registerComponent(...): Target container ' + 'is not a DOM element.' ); expect(function() { React.renderComponent(
, null); }).toThrow( - 'Invariant Violation: prepareEnvironmentForDOM(...): Target container ' + + 'Invariant Violation: _registerComponent(...): Target container ' + 'is not a DOM element.' ); }); diff --git a/src/core/__tests__/ReactEventEmitter-test.js b/src/core/__tests__/ReactEventEmitter-test.js index cbe794f91f7d..55bbf8af5251 100644 --- a/src/core/__tests__/ReactEventEmitter-test.js +++ b/src/core/__tests__/ReactEventEmitter-test.js @@ -41,6 +41,7 @@ var setID = ReactMount.setID; var ReactEventEmitter; var ReactTestUtils; var TapEventPlugin; +var EventListener; var tapMoveThreshold; var idCallOrder = []; @@ -58,6 +59,7 @@ var recordIDAndReturnFalse = function(id, event) { var LISTENER = mocks.getMockFunction(); var ON_CLICK_KEY = keyOf({onClick: null}); var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null}); +var ON_CHANGE_KEY = keyOf({onChange: null}); /** @@ -90,13 +92,13 @@ describe('ReactEventEmitter', function() { EventPluginHub = require('EventPluginHub'); TapEventPlugin = require('TapEventPlugin'); ReactMount = require('ReactMount'); + EventListener = require('EventListener'); getID = ReactMount.getID; setID = ReactMount.setID; ReactEventEmitter = require('ReactEventEmitter'); ReactTestUtils = require('ReactTestUtils'); idCallOrder = []; tapMoveThreshold = TapEventPlugin.tapMoveThreshold; - ReactEventEmitter.ensureListening(false, document); EventPluginHub.injection.injectEventPluginsByName({ TapEventPlugin: TapEventPlugin }); @@ -315,6 +317,45 @@ describe('ReactEventEmitter', function() { expect(idCallOrder.length).toBe(0); }); + it('should listen to events only once', function() { + spyOn(EventListener, 'listen'); + ReactEventEmitter.listenTo(ON_CLICK_KEY, document); + ReactEventEmitter.listenTo(ON_CLICK_KEY, document); + expect(EventListener.listen.callCount).toBe(1); + }); + + it('should work with event plugins without dependencies', function() { + spyOn(EventListener, 'listen'); + + ReactEventEmitter.listenTo(ON_CLICK_KEY, document); + + expect(EventListener.listen.argsForCall[0][1]).toBe('click'); + }); + + it('should work with event plugins with dependencies', function() { + spyOn(EventListener, 'listen'); + spyOn(EventListener, 'capture'); + + ReactEventEmitter.listenTo(ON_CHANGE_KEY, document); + + var setEventListeners = []; + var listenCalls = EventListener.listen.argsForCall; + var captureCalls = EventListener.capture.argsForCall; + for (var i = 0, l = listenCalls.length; i < l; i++) { + setEventListeners.push(listenCalls[i][1]); + } + for (i = 0, l = captureCalls.length; i < l; i++) { + setEventListeners.push(captureCalls[i][1]); + } + + var module = ReactEventEmitter.registrationNameModules[ON_CHANGE_KEY]; + var dependencies = module.eventTypes.change.dependencies; + expect(setEventListeners.length).toEqual(dependencies.length); + + for (i = 0, l = setEventListeners.length; i < l; i++) { + expect(dependencies.indexOf(setEventListeners[i])).toBeTruthy(); + } + }); it('should bubble onTouchTap', function() { ReactEventEmitter.putListener( diff --git a/src/event/EventPluginHub.js b/src/event/EventPluginHub.js index ef75bb770432..3fda08ac2c4f 100644 --- a/src/event/EventPluginHub.js +++ b/src/event/EventPluginHub.js @@ -104,7 +104,7 @@ var EventPluginHub = { }, - registrationNames: EventPluginRegistry.registrationNames, + registrationNameModules: EventPluginRegistry.registrationNameModules, putListener: CallbackRegistry.putListener, diff --git a/src/event/EventPluginRegistry.js b/src/event/EventPluginRegistry.js index b2c31760489e..a2ab2305d111 100644 --- a/src/event/EventPluginRegistry.js +++ b/src/event/EventPluginRegistry.js @@ -63,7 +63,7 @@ function recomputePluginOrdering() { var publishedEvents = PluginModule.eventTypes; for (var eventName in publishedEvents) { invariant( - publishEventForPlugin(publishedEvents[eventName], PluginModule), + publishEventForPlugin(publishedEvents[eventName], PluginModule, eventName), 'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.', eventName, pluginName @@ -80,18 +80,18 @@ function recomputePluginOrdering() { * @return {boolean} True if the event was successfully published. * @private */ -function publishEventForPlugin(dispatchConfig, PluginModule) { +function publishEventForPlugin(dispatchConfig, PluginModule, eventName) { var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames; if (phasedRegistrationNames) { for (var phaseName in phasedRegistrationNames) { if (phasedRegistrationNames.hasOwnProperty(phaseName)) { var phasedRegistrationName = phasedRegistrationNames[phaseName]; - publishRegistrationName(phasedRegistrationName, PluginModule); + publishRegistrationName(phasedRegistrationName, PluginModule, eventName); } } return true; } else if (dispatchConfig.registrationName) { - publishRegistrationName(dispatchConfig.registrationName, PluginModule); + publishRegistrationName(dispatchConfig.registrationName, PluginModule, eventName); return true; } return false; @@ -105,14 +105,16 @@ function publishEventForPlugin(dispatchConfig, PluginModule) { * @param {object} PluginModule Plugin publishing the event. * @private */ -function publishRegistrationName(registrationName, PluginModule) { +function publishRegistrationName(registrationName, PluginModule, eventName) { invariant( - !EventPluginRegistry.registrationNames[registrationName], + !EventPluginRegistry.registrationNameModules[registrationName], 'EventPluginHub: More than one plugin attempted to publish the same ' + 'registration name, `%s`.', registrationName ); - EventPluginRegistry.registrationNames[registrationName] = PluginModule; + EventPluginRegistry.registrationNameModules[registrationName] = PluginModule; + EventPluginRegistry.registrationNameDependencies[registrationName] = + PluginModule.eventTypes[eventName].dependencies; } /** @@ -130,7 +132,12 @@ var EventPluginRegistry = { /** * Mapping from registration names to plugin modules. */ - registrationNames: {}, + registrationNameModules: {}, + + /** + * Mapping from registration name to event name + */ + registrationNameDependencies: {}, /** * Injects an ordering of plugins (by plugin name). This allows the ordering @@ -194,7 +201,7 @@ var EventPluginRegistry = { getPluginModuleForEvent: function(event) { var dispatchConfig = event.dispatchConfig; if (dispatchConfig.registrationName) { - return EventPluginRegistry.registrationNames[ + return EventPluginRegistry.registrationNameModules[ dispatchConfig.registrationName ] || null; } @@ -202,7 +209,7 @@ var EventPluginRegistry = { if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) { continue; } - var PluginModule = EventPluginRegistry.registrationNames[ + var PluginModule = EventPluginRegistry.registrationNameModules[ dispatchConfig.phasedRegistrationNames[phase] ]; if (PluginModule) { @@ -224,10 +231,10 @@ var EventPluginRegistry = { } } EventPluginRegistry.plugins.length = 0; - var registrationNames = EventPluginRegistry.registrationNames; - for (var registrationName in registrationNames) { - if (registrationNames.hasOwnProperty(registrationName)) { - delete registrationNames[registrationName]; + var registrationNameModules = EventPluginRegistry.registrationNameModules; + for (var registrationName in registrationNameModules) { + if (registrationNameModules.hasOwnProperty(registrationName)) { + delete registrationNameModules[registrationName]; } } } diff --git a/src/event/EventPluginUtils.js b/src/event/EventPluginUtils.js index c9d063f6206b..d7bee7510986 100644 --- a/src/event/EventPluginUtils.js +++ b/src/event/EventPluginUtils.js @@ -179,7 +179,8 @@ var EventPluginUtils = { executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue, executeDirectDispatch: executeDirectDispatch, hasDispatches: hasDispatches, - executeDispatch: executeDispatch + executeDispatch: executeDispatch, + useTouchEvents: false }; module.exports = EventPluginUtils; diff --git a/src/event/__tests__/EventPluginRegistry-test.js b/src/event/__tests__/EventPluginRegistry-test.js index 7474525e56f6..2c18076447a0 100644 --- a/src/event/__tests__/EventPluginRegistry-test.js +++ b/src/event/__tests__/EventPluginRegistry-test.js @@ -174,16 +174,16 @@ describe('EventPluginRegistry', function() { EventPluginRegistry.injectEventPluginsByName({one: OnePlugin}); EventPluginRegistry.injectEventPluginOrder(['one', 'two']); - expect(Object.keys(EventPluginRegistry.registrationNames).length).toBe(2); - expect(EventPluginRegistry.registrationNames.onClick).toBe(OnePlugin); - expect(EventPluginRegistry.registrationNames.onFocus).toBe(OnePlugin); + expect(Object.keys(EventPluginRegistry.registrationNameModules).length).toBe(2); + expect(EventPluginRegistry.registrationNameModules.onClick).toBe(OnePlugin); + expect(EventPluginRegistry.registrationNameModules.onFocus).toBe(OnePlugin); EventPluginRegistry.injectEventPluginsByName({two: TwoPlugin}); - expect(Object.keys(EventPluginRegistry.registrationNames).length).toBe(4); - expect(EventPluginRegistry.registrationNames.onMagicBubble).toBe(TwoPlugin); + expect(Object.keys(EventPluginRegistry.registrationNameModules).length).toBe(4); + expect(EventPluginRegistry.registrationNameModules.onMagicBubble).toBe(TwoPlugin); expect( - EventPluginRegistry.registrationNames.onMagicCapture + EventPluginRegistry.registrationNameModules.onMagicCapture ).toBe(TwoPlugin); }); diff --git a/src/eventPlugins/ChangeEventPlugin.js b/src/eventPlugins/ChangeEventPlugin.js index ad8fa70f1202..601208ddf7e9 100644 --- a/src/eventPlugins/ChangeEventPlugin.js +++ b/src/eventPlugins/ChangeEventPlugin.js @@ -35,7 +35,15 @@ var eventTypes = { phasedRegistrationNames: { bubbled: keyOf({onChange: null}), captured: keyOf({onChangeCapture: null}) - } + }, + dependencies: [ + topLevelTypes.topBlur, + topLevelTypes.topChange, + topLevelTypes.topFocus, + topLevelTypes.topInput, + topLevelTypes.topKeyDown, + topLevelTypes.topKeyUp + ] } }; diff --git a/src/eventPlugins/CompositionEventPlugin.js b/src/eventPlugins/CompositionEventPlugin.js index 9387cd2c4894..dbe44d14cb17 100644 --- a/src/eventPlugins/CompositionEventPlugin.js +++ b/src/eventPlugins/CompositionEventPlugin.js @@ -42,19 +42,43 @@ var eventTypes = { phasedRegistrationNames: { bubbled: keyOf({onCompositionEnd: null}), captured: keyOf({onCompositionEndCapture: null}) - } + }, + dependencies: [ + topLevelTypes.topBlur, + topLevelTypes.topCompositionEnd, + topLevelTypes.topKeyDown, + topLevelTypes.topKeyPress, + topLevelTypes.topKeyUp, + topLevelTypes.topMouseDown + ] }, compositionStart: { phasedRegistrationNames: { bubbled: keyOf({onCompositionStart: null}), captured: keyOf({onCompositionStartCapture: null}) - } + }, + dependencies: [ + topLevelTypes.topBlur, + topLevelTypes.topCompositionStart, + topLevelTypes.topKeyDown, + topLevelTypes.topKeyPress, + topLevelTypes.topKeyUp, + topLevelTypes.topMouseDown + ] }, compositionUpdate: { phasedRegistrationNames: { bubbled: keyOf({onCompositionUpdate: null}), captured: keyOf({onCompositionUpdateCapture: null}) - } + }, + dependencies: [ + topLevelTypes.topBlur, + topLevelTypes.topCompositionUpdate, + topLevelTypes.topKeyDown, + topLevelTypes.topKeyPress, + topLevelTypes.topKeyUp, + topLevelTypes.topMouseDown + ] } }; diff --git a/src/eventPlugins/EnterLeaveEventPlugin.js b/src/eventPlugins/EnterLeaveEventPlugin.js index d11df76d4e61..3ec57cb2c9bf 100644 --- a/src/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/eventPlugins/EnterLeaveEventPlugin.js @@ -30,8 +30,20 @@ var topLevelTypes = EventConstants.topLevelTypes; var getFirstReactDOM = ReactMount.getFirstReactDOM; var eventTypes = { - mouseEnter: {registrationName: keyOf({onMouseEnter: null})}, - mouseLeave: {registrationName: keyOf({onMouseLeave: null})} + mouseEnter: { + registrationName: keyOf({onMouseEnter: null}), + dependencies: [ + topLevelTypes.topMouseOut, + topLevelTypes.topMouseOver + ] + }, + mouseLeave: { + registrationName: keyOf({onMouseLeave: null}), + dependencies: [ + topLevelTypes.topMouseOut, + topLevelTypes.topMouseOver + ] + } }; var extractedEvents = [null, null]; diff --git a/src/eventPlugins/SelectEventPlugin.js b/src/eventPlugins/SelectEventPlugin.js index 4c01c435836c..3b19274f849d 100644 --- a/src/eventPlugins/SelectEventPlugin.js +++ b/src/eventPlugins/SelectEventPlugin.js @@ -37,7 +37,15 @@ var eventTypes = { phasedRegistrationNames: { bubbled: keyOf({onSelect: null}), captured: keyOf({onSelectCapture: null}) - } + }, + dependencies: [ + topLevelTypes.topBlur, + topLevelTypes.topFocus, + topLevelTypes.topKeyDown, + topLevelTypes.topMouseDown, + topLevelTypes.topMouseUp, + topLevelTypes.topSelectionChange + ] } }; diff --git a/src/eventPlugins/SimpleEventPlugin.js b/src/eventPlugins/SimpleEventPlugin.js index 3e0eb38c7198..b3512894e9a7 100644 --- a/src/eventPlugins/SimpleEventPlugin.js +++ b/src/eventPlugins/SimpleEventPlugin.js @@ -253,6 +253,10 @@ var topLevelEventsToDispatchConfig = { topWheel: eventTypes.wheel }; +for (var topLevelType in topLevelEventsToDispatchConfig) { + topLevelEventsToDispatchConfig[topLevelType].dependencies = [topLevelType]; +} + var SimpleEventPlugin = { eventTypes: eventTypes, diff --git a/src/eventPlugins/TapEventPlugin.js b/src/eventPlugins/TapEventPlugin.js index c748da79e72b..779ecadf09eb 100644 --- a/src/eventPlugins/TapEventPlugin.js +++ b/src/eventPlugins/TapEventPlugin.js @@ -19,6 +19,7 @@ "use strict"; +var EventConstants = require('EventConstants'); var EventPluginUtils = require('EventPluginUtils'); var EventPropagators = require('EventPropagators'); var SyntheticUIEvent = require('SyntheticUIEvent'); @@ -26,6 +27,7 @@ var TouchEventUtils = require('TouchEventUtils'); var ViewportMetrics = require('ViewportMetrics'); var keyOf = require('keyOf'); +var topLevelTypes = EventConstants.topLevelTypes; var isStartish = EventPluginUtils.isStartish; var isEndish = EventPluginUtils.isEndish; @@ -61,12 +63,28 @@ function getDistance(coords, nativeEvent) { ); } +var dependencies = [ + topLevelTypes.topMouseDown, + topLevelTypes.topMouseMove, + topLevelTypes.topMouseUp +]; + +if (EventPluginUtils.useTouchEvents) { + dependencies.push( + topLevelTypes.topTouchCancel, + topLevelTypes.topTouchEnd, + topLevelTypes.topTouchStart, + topLevelTypes.topTouchMove + ); +} + var eventTypes = { touchTap: { phasedRegistrationNames: { bubbled: keyOf({onTouchTap: null}), captured: keyOf({onTouchTapCapture: null}) - } + }, + dependencies: dependencies } }; diff --git a/src/eventPlugins/__tests__/AnalyticsEventPlugin-test.js b/src/eventPlugins/__tests__/AnalyticsEventPlugin-test.js index 428352997495..2bf79820ff72 100644 --- a/src/eventPlugins/__tests__/AnalyticsEventPlugin-test.js +++ b/src/eventPlugins/__tests__/AnalyticsEventPlugin-test.js @@ -61,7 +61,6 @@ describe('AnalyticsEventPlugin', function() { 'ChangeEventPlugin': ChangeEventPlugin }); - ReactEventEmitter.ensureListening(false, document); }); it('should count events correctly', function() {