Simplify Event Core

Summary:
This makes a few changes to React Core, most notably `ReactEventEmitter` and `ReactEventTopLevelCallback`.

 - Changed `ReactEventEmitter` to use `EventListener` (instead of `NormalizedEventListener`).
 - Deleted `NormalizedEventListener` (which was previously broken).
 - Created `getEventTarget` which is used to get a normalized `target` from a native event.
 - Changed `ReactEventTopLevelCallback` to use `getEventTarget`.
 - Renamed `abstractEventType` to `reactEventType` in `AbstractEvent`.
 - Reanmed `abstractTargetID` to `reactTargetID` in `AbstractEvent`.
 - Removed `originatingTopLevelEventType` from `AbstractEvent` (unused and violates encapsulation).
 - Removed `nativeEvent.target === window` check when refreshing authoritative scroll values (unnecessary).

This actually fixes React because `NormalizedEventListener` does not currently do what it promises to do (which is normalizing `target` on the native event). The `target` event is read-only on native events.

This also revises documentation and adds `@typechecks` to a few modules.

NOTE: Most importantly, this sets the stage for replacing `AbstractEvent` with `ReactEvent` and subclasses, piecemeal.
This commit is contained in:
CommitSyncScript
2013-06-06 14:40:30 -07:00
committed by Paul O’Shannessy
parent 36d8ce8fab
commit ba6fea1bf5
15 changed files with 475 additions and 458 deletions

View File

@@ -14,33 +14,44 @@
* limitations under the License.
*
* @providesModule ReactEventEmitter
* @typechecks
*/
"use strict";
var BrowserEnv = require('BrowserEnv');
var EventConstants = require('EventConstants');
var EventListener = require('EventListener');
var EventPluginHub = require('EventPluginHub');
var ExecutionEnvironment = require('ExecutionEnvironment');
var NormalizedEventListener = require('NormalizedEventListener');
var invariant = require('invariant');
var isEventSupported = require('isEventSupported');
var registrationNames = EventPluginHub.registrationNames;
var topLevelTypes = EventConstants.topLevelTypes;
var listen = NormalizedEventListener.listen;
var capture = NormalizedEventListener.capture;
/**
* `ReactEventEmitter` is used to attach top-level event listeners. For example:
* Summary of `ReactEventEmitter` event handling:
*
* ReactEventEmitter.putListener('myID', 'onClick', myFunction);
* - We trap low level 'top-level' events.
*
* - We dedupe cross-browser event names into these 'top-level types' so that
* `DOMMouseScroll` or `mouseWheel` both become `topMouseWheel`.
*
* - At this point we have native browser events with the top-level type that
* was used to catch it at the top-level.
*
* - We continuously stream these native events (and their respective top-level
* types) to the event plugin system `EventPluginHub` and ask the plugin
* system if it was able to extract `AbstractEvent` objects. `AbstractEvent`
* objects are the events that applications actually deal with - they are not
* native browser events but cross-browser wrappers.
*
* - When returning the `AbstractEvent` objects, `EventPluginHub` will make
* sure each abstract event is annotated with "dispatches", which are the
* sequence of listeners (and IDs) that care about the event.
*
* - These `AbstractEvent` objects are fed back into the event plugin system,
* which in turn executes these dispatches.
*
* This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
*/
/**
* Overview of React and the event system:
*
* .
@@ -71,6 +82,64 @@ var capture = NormalizedEventListener.capture;
* React Core . General Purpose Event Plugin System
*/
/**
* Whether or not `ensureListening` has been invoked.
* @type {boolean}
* @private
*/
var _isListening = false;
/**
* 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
)
);
}
/**
* 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 = BrowserEnv.refreshAuthoritativeScrollValues;
EventListener.listen(window, 'scroll', refresh);
EventListener.listen(window, 'resize', refresh);
}
/**
* We listen for bubbled touch events on the document object.
*
@@ -89,96 +158,19 @@ var capture = NormalizedEventListener.capture;
* Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
* they bubble to document.
*
* @see http://www.quirksmode.org/dom/events/keys.html.
*/
var _isListening = false;
/**
* Traps top-level events that bubble. Delegates to the main dispatcher
* `handleTopLevel` after performing some basic normalization via
* `TopLevelCallbackCreator.createTopLevelCallback`.
*/
function trapBubbledEvent(topLevelType, handlerBaseName, onWhat) {
listen(
onWhat,
handlerBaseName,
ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback(
topLevelType
)
);
}
/**
* Traps a top-level event by using event capturing.
*/
function trapCapturedEvent(topLevelType, handlerBaseName, onWhat) {
capture(
onWhat,
handlerBaseName,
ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback(
topLevelType
)
);
}
/**
* Listens to document scroll and window resize events that may change the
* document scroll values. We store those results so as to discourage
* application code from asking the DOM itself which could trigger additional
* reflows.
*/
function registerDocumentScrollListener() {
listen(window, 'scroll', function(nativeEvent) {
if (nativeEvent.target === window) {
BrowserEnv.refreshAuthoritativeScrollValues();
}
});
}
function registerDocumentResizeListener() {
listen(window, 'resize', function(nativeEvent) {
if (nativeEvent.target === window) {
BrowserEnv.refreshAuthoritativeScrollValues();
}
});
}
/**
* Summary of `ReactEventEmitter` event handling:
*
* - We trap low level 'top-level' events.
*
* - We dedupe cross-browser event names into these 'top-level types' so that
* `DOMMouseScroll` or `mouseWheel` both become `topMouseWheel`.
*
* - At this point we have native browser events with the top-level type that
* was used to catch it at the top-level.
*
* - We continuously stream these native events (and their respective top-level
* types) to the event plugin system `EventPluginHub` and ask the plugin
* system if it was able to extract `AbstractEvent` objects. `AbstractEvent`
* objects are the events that applications actually deal with - they are not
* native browser events but cross-browser wrappers.
*
* - When returning the `AbstractEvent` objects, `EventPluginHub` will make
* sure each abstract event is annotated with "dispatches", which are the
* sequence of listeners (and IDs) that care about the event.
*
* - These `AbstractEvent` objects are fed back into the event plugin system,
* which in turn executes these dispatches.
*
* @param {boolean} touchNotMouse Listen to touch events instead of mouse.
* @private
* @see http://www.quirksmode.org/dom/events/keys.html.
*/
function listenAtTopLevel(touchNotMouse) {
invariant(
!_isListening,
'listenAtTopLevel(...): Cannot setup top-level listener more than once.'
);
var topLevelTypes = EventConstants.topLevelTypes;
var mountAt = document;
registerDocumentScrollListener();
registerDocumentResizeListener();
registerScrollValueMonitoring();
trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt);
trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt);
trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt);
@@ -226,82 +218,111 @@ function listenAtTopLevel(touchNotMouse) {
}
/**
* This is the heart of `ReactEventEmitter`. It simply streams the top-level
* native events to `EventPluginHub`.
* `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'.
*
* @param {object} topLevelType Record from `EventConstants`.
* @param {Event} nativeEvent A Standard Event with fixed `target` property.
* @param {DOMElement} renderedTarget Element of interest to the framework.
* @param {string} renderedTargetID string ID of `renderedTarget`.
* @internal
*/
function handleTopLevel(
topLevelType,
nativeEvent,
renderedTargetID,
renderedTarget) {
var abstractEvents = EventPluginHub.extractAbstractEvents(
topLevelType,
nativeEvent,
renderedTargetID,
renderedTarget
);
// The event queue being processed in the same cycle allows preventDefault.
EventPluginHub.enqueueAbstractEvents(abstractEvents);
EventPluginHub.processAbstractEventQueue();
}
function setEnabled(enabled) {
invariant(
ExecutionEnvironment.canUseDOM,
'setEnabled(...): Cannot toggle event listening in a Worker thread. This ' +
'is likely a bug in the framework. Please report immediately.'
);
ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled);
}
function isEnabled() {
return ReactEventEmitter.TopLevelCallbackCreator.isEnabled();
}
/**
* Ensures that top-level event delegation listeners are listening at `mountAt`.
* 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 {object} TopLevelCallbackCreator Module that can create top-level
* callback handlers.
* @internal
*/
function ensureListening(touchNotMouse, TopLevelCallbackCreator) {
invariant(
ExecutionEnvironment.canUseDOM,
'ensureListening(...): Cannot toggle event listening in a Worker thread. ' +
'This is likely a bug in the framework. Please report immediately.'
);
if (!_isListening) {
ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator;
listenAtTopLevel(touchNotMouse);
_isListening = true;
}
}
var ReactEventEmitter = {
TopLevelCallbackCreator: null, // Injectable callback creator.
handleTopLevel: handleTopLevel,
setEnabled: setEnabled,
isEnabled: isEnabled,
ensureListening: ensureListening,
registrationNames: registrationNames,
/**
* React references `ReactEventTopLevelCallback` using this property in order
* to allow dependency injection via `ensureListening`.
*/
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 {object} TopLevelCallbackCreator
*/
ensureListening: function(touchNotMouse, TopLevelCallbackCreator) {
invariant(
ExecutionEnvironment.canUseDOM,
'ensureListening(...): Cannot toggle event listening in a Worker ' +
'thread. This is likely a bug in the framework. Please report ' +
'immediately.'
);
if (!_isListening) {
ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator;
listenAtTopLevel(touchNotMouse);
_isListening = true;
}
},
/**
* Sets whether or not any created callbacks should be enabled.
*
* @param {boolean} enabled True if callbacks should be enabled.
*/
setEnabled: function(enabled) {
invariant(
ExecutionEnvironment.canUseDOM,
'setEnabled(...): Cannot toggle event listening in a Worker thread. ' +
'This is likely a bug in the framework. Please report immediately.'
);
if (ReactEventEmitter.TopLevelCallbackCreator) {
ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled);
}
},
/**
* @return {boolean} True if callbacks are enabled.
*/
isEnabled: function() {
return !!(
ReactEventEmitter.TopLevelCallbackCreator &&
ReactEventEmitter.TopLevelCallbackCreator.isEnabled()
);
},
/**
* Streams a fired top-level event to `EventPluginHub` where plugins have the
* opportunity to create `ReactEvent`s to be dispatched.
*
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
*/
handleTopLevel: function(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent) {
var abstractEvents = EventPluginHub.extractAbstractEvents(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent
);
// Event queue being processed in the same cycle allows `preventDefault`.
EventPluginHub.enqueueAbstractEvents(abstractEvents);
EventPluginHub.processAbstractEventQueue();
},
registrationNames: EventPluginHub.registrationNames,
putListener: EventPluginHub.putListener,
getListener: EventPluginHub.getListener,
deleteAllListeners: EventPluginHub.deleteAllListeners,
trapBubbledEvent: trapBubbledEvent,
trapCapturedEvent: trapCapturedEvent
};
module.exports = ReactEventEmitter;

View File

@@ -14,6 +14,7 @@
* limitations under the License.
*
* @providesModule ReactEventTopLevelCallback
* @typechecks
*/
"use strict";
@@ -23,51 +24,59 @@ var ReactEventEmitter = require('ReactEventEmitter');
var ReactInstanceHandles = require('ReactInstanceHandles');
var getDOMNodeID = require('getDOMNodeID');
var getEventTarget = require('getEventTarget');
/**
* @type {boolean}
* @private
*/
var _topLevelListenersEnabled = true;
/**
* Top-level callback creator used to implement event handling using delegation.
* This is used via dependency injection in `ReactEventEmitter.ensureListening`.
*/
var ReactEventTopLevelCallback = {
/**
* @param {boolean} enabled Whether or not all callbacks that have ever been
* created with this module should be enabled.
* Sets whether or not any created callbacks should be enabled.
*
* @param {boolean} enabled True if callbacks should be enabled.
*/
setEnabled: function(enabled) {
_topLevelListenersEnabled = !!enabled;
},
/**
* @return {boolean} True if callbacks are enabled.
*/
isEnabled: function() {
return _topLevelListenersEnabled;
},
/**
* For a given `topLevelType`, creates a callback that could be added as a
* listener to the document. That top level callback will simply fix the
* native events before invoking `handleTopLevel`.
* Creates a callback for the supplied `topLevelType` that could be added as
* a listener to the document. The callback computes a `topLevelTarget` which
* should be the root node of a mounted React component where the listener
* is attached.
*
* - Raw native events cannot be trusted to describe their targets correctly
* so we expect that the argument to the nested function has already been
* fixed. But the `target` property may not be something of interest to
* React, so we find the most suitable target. But even at that point, DOM
* Elements (the target ) can't be trusted to describe their IDs correctly
* so we obtain the ID in a reliable manner and pass it to
* `handleTopLevel`. The target/id that we found to be relevant to our
* framework are called `renderedTarget`/`renderedTargetID` respectively.
* @param {string} topLevelType Record from `EventConstants`.
* @return {function} Callback for handling top-level events.
*/
createTopLevelCallback: function(topLevelType) {
return function(fixedNativeEvent) {
return function(nativeEvent) {
if (!_topLevelListenersEnabled) {
return;
}
var renderedTarget = ReactInstanceHandles.getFirstReactDOM(
fixedNativeEvent.target
var topLevelTarget = ReactInstanceHandles.getFirstReactDOM(
getEventTarget(nativeEvent)
) || ExecutionEnvironment.global;
var renderedTargetID = getDOMNodeID(renderedTarget);
var topLevelTargetID = getDOMNodeID(topLevelTarget) || '';
ReactEventEmitter.handleTopLevel(
topLevelType,
fixedNativeEvent,
renderedTargetID,
renderedTarget
topLevelTarget,
topLevelTargetID,
nativeEvent
);
};
}

View File

@@ -14,6 +14,7 @@
* limitations under the License.
*
* @providesModule ReactInstanceHandles
* @typechecks
*/
"use strict";
@@ -57,13 +58,13 @@ function isValidID(id) {
/**
* True if the supplied `node` is rendered by React.
*
* @param {DOMElement} node DOM Element to check.
* @param {DOMEventTarget} node DOM Element to check.
* @return {boolean} True if the DOM Element appears to be rendered by React.
* @private
*/
function isRenderedByReact(node) {
var id = getDOMNodeID(node);
return id && id.charAt(0) === SEPARATOR;
return id ? id.charAt(0) === SEPARATOR : false;
}
/**
@@ -140,8 +141,8 @@ var ReactInstanceHandles = {
* Traverses up the ancestors of the supplied node to find a node that is a
* DOM representation of a React component.
*
* @param {DOMElement} node
* @return {?DOMElement}
* @param {?DOMEventTarget} node
* @return {?DOMEventTarget}
* @internal
*/
getFirstReactDOM: function(node) {
@@ -159,9 +160,9 @@ var ReactInstanceHandles = {
* Finds a node with the supplied `id` inside of the supplied `ancestorNode`.
* Exploits the ID naming scheme to perform the search quickly.
*
* @param {DOMElement} ancestorNode Search from this root.
* @param {DOMEventTarget} ancestorNode Search from this root.
* @pararm {string} id ID of the DOM representation of the component.
* @return {?DOMElement} DOM element with the supplied `id`, if one exists.
* @return {?DOMEventTarget} DOM node with the supplied `id`, if one exists.
* @internal
*/
findComponentRoot: function(ancestorNode, id) {
@@ -228,7 +229,7 @@ var ReactInstanceHandles = {
* contains the React component with the supplied DOM ID.
*
* @param {string} id DOM ID of a React component.
* @return {string} DOM ID of the React component that is the root.
* @return {?string} DOM ID of the React component that is the root.
* @internal
*/
getReactRootIDFromNodeID: function(id) {

View File

@@ -14,6 +14,7 @@
* limitations under the License.
*
* @providesModule getDOMNodeID
* @typechecks
*/
"use strict";
@@ -23,8 +24,8 @@
* control whose name or ID is "id". However, not all DOM nodes support
* `getAttributeNode` (document - which is not a form) so that is checked first.
*
* @param {Element} domNode DOM node element to return ID of.
* @returns {string} The ID of `domNode`.
* @param {DOMElement|DOMWindow|DOMDocument} domNode DOM node.
* @returns {string} ID of the supplied `domNode`.
*/
function getDOMNodeID(domNode) {
if (domNode.getAttributeNode) {

39
src/dom/getEventTarget.js Normal file
View File

@@ -0,0 +1,39 @@
/**
* Copyright 2013 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 getEventTarget
* @typechecks
*/
var ExecutionEnvironment = require('ExecutionEnvironment');
/**
* Gets the target node from a native browser event by accounting for
* inconsistencies in browser DOM APIs.
*
* @param {object} nativeEvent Native browser event.
* @return {DOMEventTarget} Target node.
*/
function getEventTarget(nativeEvent) {
var target =
nativeEvent.target ||
nativeEvent.srcElement ||
ExecutionEnvironment.global;
// Safari may fire events on text nodes (Node.TEXT_NODE is 3).
// @see http://www.quirksmode.org/js/events_properties.html
return target.nodeType === 3 ? target.parentNode : target;
}
module.exports = getEventTarget;

View File

@@ -44,14 +44,12 @@ var MAX_POOL_SIZE = 20;
* unreliable native event.
*/
function AbstractEvent(
abstractEventType,
abstractTargetID, // Allows the abstract target to differ from native.
originatingTopLevelEventType,
reactEventType,
reactTargetID, // Allows the abstract target to differ from native.
nativeEvent,
data) {
this.type = abstractEventType;
this.abstractTargetID = abstractTargetID || '';
this.originatingTopLevelEventType = originatingTopLevelEventType;
this.reactEventType = reactEventType;
this.reactTargetID = reactTargetID || '';
this.nativeEvent = nativeEvent;
this.data = data;
// TODO: Deprecate storing target - doesn't always make sense for some types
@@ -263,12 +261,10 @@ AbstractEvent.persistentCloneOf = function(abstractEvent) {
throwIf(!(abstractEvent instanceof AbstractEvent), CLONE_TYPE_ERR);
}
return new AbstractEvent(
abstractEvent.type,
abstractEvent.abstractTargetID,
abstractEvent.originatingTopLevelEventType,
abstractEvent.reactEventType,
abstractEvent.reactTargetID,
abstractEvent.nativeEvent,
abstractEvent.data,
abstractEvent.target
abstractEvent.data
);
};

View File

@@ -194,15 +194,16 @@ function recordAllRegistrationNames(eventType, PluginModule) {
* @param {AbstractEvent} abstractEvent to look at
*/
function getPluginModuleForAbstractEvent(abstractEvent) {
if (abstractEvent.type.registrationName) {
return registrationNames[abstractEvent.type.registrationName];
var reactEventType = abstractEvent.reactEventType;
if (reactEventType.registrationName) {
return registrationNames[reactEventType.registrationName];
} else {
for (var phase in abstractEvent.type.phasedRegistrationNames) {
if (!abstractEvent.type.phasedRegistrationNames.hasOwnProperty(phase)) {
for (var phase in reactEventType.phasedRegistrationNames) {
if (!reactEventType.phasedRegistrationNames.hasOwnProperty(phase)) {
continue;
}
var PluginModule = registrationNames[
abstractEvent.type.phasedRegistrationNames[phase]
reactEventType.phasedRegistrationNames[phase]
];
if (PluginModule) {
return PluginModule;
@@ -223,36 +224,36 @@ var deleteAllListeners = function(domID) {
* Accepts the stream of top level native events, and gives every registered
* plugin an opportunity to extract `AbstractEvent`s with annotated dispatches.
*
* @param {Enum} topLevelType Record from `EventConstants`.
* @param {Event} nativeEvent A Standard Event with fixed `target` property.
* @param {Element} renderedTarget Element of interest to the framework, usually
* the same as `nativeEvent.target` but occasionally an element immediately
* above `nativeEvent.target` (the first DOM node recognized as one "rendered"
* by the framework at hand.)
* @param {string} renderedTargetID string ID of `renderedTarget`.
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of `AbstractEvent`s.
*/
var extractAbstractEvents =
function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) {
var abstractEvents;
var plugins = injection.plugins;
var len = plugins.length;
for (var i = 0; i < len; i++) {
// Not every plugin in the ordering may be loaded at runtime.
var possiblePlugin = plugins[i];
var extractedAbstractEvents =
possiblePlugin &&
possiblePlugin.extractAbstractEvents(
topLevelType,
nativeEvent,
renderedTargetID,
renderedTarget
);
var extractAbstractEvents = function(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent) {
var abstractEvents;
var plugins = injection.plugins;
for (var i = 0, l = plugins.length; i < l; i++) {
// Not every plugin in the ordering may be loaded at runtime.
var possiblePlugin = plugins[i];
if (possiblePlugin) {
var extractedAbstractEvents = possiblePlugin.extractAbstractEvents(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent
);
if (extractedAbstractEvents) {
abstractEvents = accumulate(abstractEvents, extractedAbstractEvents);
}
}
return abstractEvents;
};
}
return abstractEvents;
};
var enqueueAbstractEvents = function(abstractEvents) {
if (abstractEvents) {

View File

@@ -58,7 +58,7 @@ var injection = {
*/
function listenerAtPhase(id, abstractEvent, propagationPhase) {
var registrationName =
abstractEvent.type.phasedRegistrationNames[propagationPhase];
abstractEvent.reactEventType.phasedRegistrationNames[propagationPhase];
return getListener(id, registrationName);
}
@@ -92,9 +92,9 @@ function accumulateDirectionalDispatches(domID, upwards, abstractEvent) {
* have a different target.
*/
function accumulateTwoPhaseDispatchesSingle(abstractEvent) {
if (abstractEvent && abstractEvent.type.phasedRegistrationNames) {
if (abstractEvent && abstractEvent.reactEventType.phasedRegistrationNames) {
injection.InstanceHandle.traverseTwoPhase(
abstractEvent.abstractTargetID,
abstractEvent.reactTargetID,
accumulateDirectionalDispatches,
abstractEvent
);
@@ -105,11 +105,12 @@ function accumulateTwoPhaseDispatchesSingle(abstractEvent) {
/**
* Accumulates without regard to direction, does not look for phased
* registration names. Same as `accumulateDirectDispatchesSingle` but without
* requiring that the `abstractTargetID` be the same as the dispatched ID.
* requiring that the `reactTargetID` be the same as the dispatched ID.
*/
function accumulateDispatches(id, ignoredDirection, abstractEvent) {
if (abstractEvent && abstractEvent.type.registrationName) {
var listener = getListener(id, abstractEvent.type.registrationName);
if (abstractEvent && abstractEvent.reactEventType.registrationName) {
var registrationName = abstractEvent.reactEventType.registrationName;
var listener = getListener(id, registrationName);
if (listener) {
abstractEvent._dispatchListeners =
accumulate(abstractEvent._dispatchListeners, listener);
@@ -120,12 +121,12 @@ function accumulateDispatches(id, ignoredDirection, abstractEvent) {
/**
* Accumulates dispatches on an `AbstractEvent`, but only for the
* `abstractTargetID`.
* `reactTargetID`.
* @param {AbstractEvent} abstractEvent
*/
function accumulateDirectDispatchesSingle(abstractEvent) {
if (abstractEvent && abstractEvent.type.registrationName) {
accumulateDispatches(abstractEvent.abstractTargetID, null, abstractEvent);
if (abstractEvent && abstractEvent.reactEventType.registrationName) {
accumulateDispatches(abstractEvent.reactTargetID, null, abstractEvent);
}
}

View File

@@ -1,84 +0,0 @@
/**
* Copyright 2013 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 NormalizedEventListener
*/
var EventListener = require('EventListener');
/**
* @param {?Event} eventParam Event parameter from an attached listener.
* @return {Event} Normalized event object.
* @private
*/
function normalizeEvent(eventParam) {
var nativeEvent = eventParam || window.event;
// In some browsers (OLD FF), setting the target throws an error. A good way
// to tell if setting the target will throw an error, is to check if the event
// has a `target` property. Safari events have a `target` but it's not always
// normalized. Even if a `target` property exists, it's good to only set the
// target property if we realize that a change will actually take place.
var hasTargetProperty = 'target' in nativeEvent;
var eventTarget = nativeEvent.target || nativeEvent.srcElement || window;
// Safari may fire events on text nodes (Node.TEXT_NODE is 3)
// @see http://www.quirksmode.org/js/events_properties.html
var textNodeNormalizedTarget =
(eventTarget.nodeType === 3) ? eventTarget.parentNode : eventTarget;
if (!hasTargetProperty || nativeEvent.target !== textNodeNormalizedTarget) {
// TODO: Normalize the object via `merge()` to work with strict mode.
nativeEvent.target = textNodeNormalizedTarget;
}
return nativeEvent;
}
function createNormalizedCallback(cb) {
return function(unfixedNativeEvent) {
cb(normalizeEvent(unfixedNativeEvent));
};
}
var NormalizedEventListener = {
/**
* Listens to bubbled events on a DOM node.
*
* NOTE: The listener will be invoked with a normalized event object.
*
* @param {DOMElement} el DOM element to register listener on.
* @param {string} handlerBaseName Event name, e.g. "click".
* @param {function} cb Callback function.
* @public
*/
listen: function(el, handlerBaseName, cb) {
EventListener.listen(el, handlerBaseName, createNormalizedCallback(cb));
},
/**
* Listens to captured events on a DOM node.
*
* NOTE: The listener will be invoked with a normalized event object.
*
* @param {DOMElement} el DOM element to register listener on.
* @param {string} handlerBaseName Event name, e.g. "click".
* @param {function} cb Callback function.
* @public
*/
capture: function(el, handlerBaseName, cb) {
EventListener.capture(el, handlerBaseName, createNormalizedCallback(cb));
}
};
module.exports = NormalizedEventListener;

View File

@@ -136,20 +136,25 @@ if (__DEV__) {
* This plugin does not really extract any abstract events. Rather it just looks
* at the top level event and bumps up counters as appropriate
*
* @see EventPluginHub.extractAbstractEvents
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of `AbstractEvent`s.
* @see {EventPluginHub.extractAbstractEvents}
*/
function extractAbstractEvents(
topLevelType,
nativeEvent,
renderedTargetID,
renderedTarget) {
topLevelTarget,
topLevelTargetID,
nativeEvent) {
var currentEvent = topLevelTypesToAnalyticsEvent[topLevelType];
if (!currentEvent || !renderedTarget || !renderedTarget.attributes) {
if (!currentEvent || !topLevelTarget || !topLevelTarget.attributes) {
return null;
}
var analyticsIDAttribute = renderedTarget.attributes[ANALYTICS_ID];
var analyticsEventsAttribute = renderedTarget.attributes[ANALYTICS_EVENTS];
var analyticsIDAttribute = topLevelTarget.attributes[ANALYTICS_ID];
var analyticsEventsAttribute = topLevelTarget.attributes[ANALYTICS_EVENTS];
if(!analyticsIDAttribute || !analyticsEventsAttribute) {
return null;
}

View File

@@ -14,6 +14,7 @@
* limitations under the License.
*
* @providesModule EnterLeaveEventPlugin
* @typechecks
*/
"use strict";
@@ -36,28 +37,32 @@ var abstractEventTypes = {
};
/**
* For almost every interaction we care about, there will be a top level
* `mouseOver` and `mouseOut` event that occur so we can usually only pay
* attention to one of the two (we'll pay attention to the `mouseOut` event) to
* avoid extracting a duplicate event. However, there's one interaction where
* there will be no `mouseOut` event to rely on - mousing from outside the
* browser *into* the chrome. We detect this scenario and only in that case, we
* use the `mouseOver` event.
* For almost every interaction we care about, there will be a top-level
* `mouseover` and `mouseout` event that occurs so only pay attention to one of
* the two (to avoid duplicate events). We use the `mouseout` event.
*
* @see EventPluginHub.extractAbstractEvents
* However, there's one interaction where there will be no `mouseout` event to
* rely on - mousing from outside the browser *into* the chrome. We detect this
* scenario and only in that case, we use the `mouseover` event.
*
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of `AbstractEvent`s.
* @see {EventPluginHub.extractAbstractEvents}
*/
var extractAbstractEvents = function(
topLevelType,
nativeEvent,
renderedTargetID,
renderedTarget) {
topLevelTarget,
topLevelTargetID,
nativeEvent) {
if (topLevelType === topLevelTypes.topMouseOver &&
(nativeEvent.relatedTarget || nativeEvent.fromElement)) {
return;
return null;
}
if (topLevelType !== topLevelTypes.topMouseOut &&
topLevelType !== topLevelTypes.topMouseOver){
topLevelType !== topLevelTypes.topMouseOver) {
return null; // Must not be a mouse in or mouse out - ignoring.
}
@@ -65,32 +70,33 @@ var extractAbstractEvents = function(
if (topLevelType === topLevelTypes.topMouseOut) {
to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement) ||
ExecutionEnvironment.global;
from = renderedTarget;
from = topLevelTarget;
} else {
to = renderedTarget;
to = topLevelTarget;
from = ExecutionEnvironment.global;
}
// Nothing pertains to our managed components.
if (from === to ) {
return;
if (from === to) {
return null;
}
var fromID = from ? getDOMNodeID(from) : '';
var toID = to ? getDOMNodeID(to) : '';
var leave = AbstractEvent.getPooled(
abstractEventTypes.mouseLeave,
fromID,
topLevelType,
nativeEvent
);
var enter = AbstractEvent.getPooled(
abstractEventTypes.mouseEnter,
toID,
topLevelType,
nativeEvent
);
EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID);
return [leave, enter];
};

View File

@@ -162,14 +162,13 @@ var abstractEventTypes = {
*/
/**
* @param {TopLevelTypes} topLevelType Top level event type being examined.
* @param {DOMEvent} nativeEvent Native DOM event.
* @param {string} topLevelType Record from `EventConstants`.
* @param {string} renderedTargetID ID of deepest React rendered element.
*
* @return {Accumulation<AbstractEvent>} Extracted events.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of extracted `AbstractEvent`s.
*/
var setResponderAndExtractTransfer =
function(topLevelType, nativeEvent, renderedTargetID) {
function(topLevelType, renderedTargetID, nativeEvent) {
var type;
var shouldSetEventType =
isStartish(topLevelType) ? abstractEventTypes.startShouldSetResponder :
@@ -235,7 +234,6 @@ var setResponderAndExtractTransfer =
return extracted;
};
/**
* A transfer is a negotiation between a currently set responder and the next
* element to claim responder status. Any start event could trigger a transfer
@@ -253,51 +251,60 @@ function canTriggerTransfer(topLevelType) {
(isPressing && isMoveish(topLevelType));
}
var extractAbstractEvents =
function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) {
var extracted;
// Must have missed an end event - reset the state here.
if (responderID && isStartish(topLevelType)) {
responderID = null;
/**
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of `AbstractEvent`s.
* @see {EventPluginHub.extractAbstractEvents}
*/
var extractAbstractEvents = function(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent) {
var extracted;
// Must have missed an end event - reset the state here.
if (responderID && isStartish(topLevelType)) {
responderID = null;
}
if (isStartish(topLevelType)) {
isPressing = true;
} else if (isEndish(topLevelType)) {
isPressing = false;
}
if (canTriggerTransfer(topLevelType)) {
var transfer = setResponderAndExtractTransfer(
topLevelType,
topLevelTargetID,
nativeEvent
);
if (transfer) {
extracted = accumulate(extracted, transfer);
}
if (isStartish(topLevelType)) {
isPressing = true;
} else if (isEndish(topLevelType)) {
isPressing = false;
}
if (canTriggerTransfer(topLevelType)) {
var transfer = setResponderAndExtractTransfer(
topLevelType,
nativeEvent,
renderedTargetID,
renderedTarget
);
if (transfer) {
extracted = accumulate(extracted, transfer);
}
}
// Now that we know the responder is set correctly, we can dispatch
// responder type events (directly to the responder).
var type = isMoveish(topLevelType) ? abstractEventTypes.responderMove :
isEndish(topLevelType) ? abstractEventTypes.responderRelease :
isStartish(topLevelType) ? abstractEventTypes.responderStart : null;
if (type) {
var data = AbstractEvent.normalizePointerData(nativeEvent);
var gesture = AbstractEvent.getPooled(
type,
responderID,
topLevelType,
nativeEvent,
data
);
EventPropagators.accumulateDirectDispatches(gesture);
extracted = accumulate(extracted, gesture);
}
if (type === abstractEventTypes.responderRelease) {
responderID = null;
}
return extracted;
};
}
// Now that we know the responder is set correctly, we can dispatch
// responder type events (directly to the responder).
var type = isMoveish(topLevelType) ? abstractEventTypes.responderMove :
isEndish(topLevelType) ? abstractEventTypes.responderRelease :
isStartish(topLevelType) ? abstractEventTypes.responderStart : null;
if (type) {
var data = AbstractEvent.normalizePointerData(nativeEvent);
var gesture = AbstractEvent.getPooled(
type,
responderID,
nativeEvent,
data
);
EventPropagators.accumulateDirectDispatches(gesture);
extracted = accumulate(extracted, gesture);
}
if (type === abstractEventTypes.responderRelease) {
responderID = null;
}
return extracted;
};
/**
* Event plugin for formalizing the negotiation between claiming locks on

View File

@@ -155,6 +155,7 @@ var SimpleEventPlugin = {
/**
* Same as the default implementation, except cancels the event when return
* value is false.
*
* @param {AbstractEvent} AbstractEvent to handle
* @param {function} Application-level callback
* @param {string} domID DOM id to pass to the callback.
@@ -168,48 +169,55 @@ var SimpleEventPlugin = {
},
/**
* @see EventPluginHub.extractAbstractEvents
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of `AbstractEvent`s.
* @see {EventPluginHub.extractAbstractEvents}
*/
extractAbstractEvents:
function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) {
var data;
var abstractEventType =
SimpleEventPlugin.topLevelTypesToAbstract[topLevelType];
if (!abstractEventType) {
return null;
}
switch(topLevelType) {
case topLevelTypes.topMouseWheel:
data = AbstractEvent.normalizeMouseWheelData(nativeEvent);
break;
case topLevelTypes.topScroll:
data = AbstractEvent.normalizeScrollDataFromTarget(renderedTarget);
break;
case topLevelTypes.topClick:
case topLevelTypes.topDoubleClick:
case topLevelTypes.topChange:
case topLevelTypes.topDOMCharacterDataModified:
case topLevelTypes.topMouseDown:
case topLevelTypes.topMouseUp:
case topLevelTypes.topMouseMove:
case topLevelTypes.topTouchMove:
case topLevelTypes.topTouchStart:
case topLevelTypes.topTouchEnd:
data = AbstractEvent.normalizePointerData(nativeEvent);
break;
default:
data = null;
}
var abstractEvent = AbstractEvent.getPooled(
abstractEventType,
renderedTargetID,
topLevelType,
nativeEvent,
data
);
EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
return abstractEvent;
extractAbstractEvents: function(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent) {
var data;
var abstractEventType =
SimpleEventPlugin.topLevelTypesToAbstract[topLevelType];
if (!abstractEventType) {
return null;
}
switch(topLevelType) {
case topLevelTypes.topMouseWheel:
data = AbstractEvent.normalizeMouseWheelData(nativeEvent);
break;
case topLevelTypes.topScroll:
data = AbstractEvent.normalizeScrollDataFromTarget(topLevelTarget);
break;
case topLevelTypes.topClick:
case topLevelTypes.topDoubleClick:
case topLevelTypes.topChange:
case topLevelTypes.topDOMCharacterDataModified:
case topLevelTypes.topMouseDown:
case topLevelTypes.topMouseUp:
case topLevelTypes.topMouseMove:
case topLevelTypes.topTouchMove:
case topLevelTypes.topTouchStart:
case topLevelTypes.topTouchEnd:
data = AbstractEvent.normalizePointerData(nativeEvent);
break;
default:
data = null;
}
var abstractEvent = AbstractEvent.getPooled(
abstractEventType,
topLevelTargetID,
nativeEvent,
data
);
EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
return abstractEvent;
}
};
SimpleEventPlugin.topLevelTypesToAbstract = {

View File

@@ -14,6 +14,7 @@
* limitations under the License.
*
* @providesModule TapEventPlugin
* @typechecks
*/
"use strict";
@@ -46,26 +47,27 @@ var abstractEventTypes = {
};
/**
* @see EventPluginHub.extractAbstractEvents
* @param {string} topLevelType Record from `EventConstants`.
* @param {DOMEventTarget} topLevelTarget The listening component root node.
* @param {string} topLevelTargetID ID of `topLevelTarget`.
* @param {object} nativeEvent Native browser event.
* @return {*} An accumulation of `AbstractEvent`s.
* @see {EventPluginHub.extractAbstractEvents}
*/
var extractAbstractEvents = function(
topLevelType,
nativeEvent,
renderedTargetID,
renderedTarget) {
topLevelTarget,
topLevelTargetID,
nativeEvent) {
if (!isStartish(topLevelType) && !isEndish(topLevelType)) {
return;
}
var abstractEvent;
var dist = eventDistance(startCoords, nativeEvent);
if (isEndish(topLevelType) && dist < tapMoveThreshold) {
var type = abstractEventTypes.touchTap;
var abstractTargetID = renderedTargetID;
abstractEvent = AbstractEvent.getPooled(
type,
abstractTargetID,
topLevelType,
abstractEventTypes.touchTap,
topLevelTargetID,
nativeEvent
);
}

View File

@@ -167,8 +167,9 @@ var existsInExtraction = function(extracted, test) {
function assertGrantEvent(id, extracted) {
var test = function(abstractEvent) {
return abstractEvent instanceof AbstractEvent &&
abstractEvent.type === responderAbstractEventTypes.responderGrant &&
abstractEvent.abstractTargetID === id;
abstractEvent.reactEventType ===
responderAbstractEventTypes.responderGrant &&
abstractEvent.reactTargetID === id;
};
expect(ResponderEventPlugin.getResponderID()).toBe(id);
expect(existsInExtraction(extracted, test)).toBe(true);
@@ -177,8 +178,9 @@ function assertGrantEvent(id, extracted) {
function assertResponderMoveEvent(id, extracted) {
var test = function(abstractEvent) {
return abstractEvent instanceof AbstractEvent &&
abstractEvent.type === responderAbstractEventTypes.responderMove &&
abstractEvent.abstractTargetID === id;
abstractEvent.reactEventType ===
responderAbstractEventTypes.responderMove &&
abstractEvent.reactTargetID === id;
};
expect(ResponderEventPlugin.getResponderID()).toBe(id);
expect(existsInExtraction(extracted, test)).toBe(true);
@@ -187,8 +189,9 @@ function assertResponderMoveEvent(id, extracted) {
function assertTerminateEvent(id, extracted) {
var test = function(abstractEvent) {
return abstractEvent instanceof AbstractEvent &&
abstractEvent.type === responderAbstractEventTypes.responderTerminate &&
abstractEvent.abstractTargetID === id;
abstractEvent.reactEventType ===
responderAbstractEventTypes.responderTerminate &&
abstractEvent.reactTargetID === id;
};
expect(ResponderEventPlugin.getResponderID()).not.toBe(id);
expect(existsInExtraction(extracted, test)).toBe(true);
@@ -197,8 +200,9 @@ function assertTerminateEvent(id, extracted) {
function assertRelease(id, extracted) {
var test = function(abstractEvent) {
return abstractEvent instanceof AbstractEvent &&
abstractEvent.type === responderAbstractEventTypes.responderRelease &&
abstractEvent.abstractTargetID === id;
abstractEvent.reactEventType ===
responderAbstractEventTypes.responderRelease &&
abstractEvent.reactTargetID === id;
};
expect(ResponderEventPlugin.getResponderID()).toBe(null);
expect(existsInExtraction(extracted, test)).toBe(true);