603 lines
22 KiB
JavaScript
603 lines
22 KiB
JavaScript
"use strict";
|
|
|
|
exports.__esModule = true;
|
|
exports.attachListeners = attachListeners;
|
|
exports.addNode = addNode;
|
|
exports.removeNode = removeNode;
|
|
exports.terminateResponder = terminateResponder;
|
|
exports.getResponderNode = getResponderNode;
|
|
|
|
var _ExecutionEnvironment = require("fbjs/lib/ExecutionEnvironment");
|
|
|
|
var _createResponderEvent = _interopRequireDefault(require("./createResponderEvent"));
|
|
|
|
var _ResponderEventTypes = require("./ResponderEventTypes");
|
|
|
|
var _utils = require("./utils");
|
|
|
|
var _ResponderTouchHistoryStore = _interopRequireDefault(require("./ResponderTouchHistoryStore"));
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
/**
|
|
* Copyright (c) Nicolas Gallagher
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* RESPONDER EVENT SYSTEM
|
|
*
|
|
* A single, global "interaction lock" on views. For a view to be the "responder" means
|
|
* that pointer interactions are exclusive to that view and none other. The "interaction
|
|
* lock" can be transferred (only) to ancestors of the current "responder" as long as
|
|
* pointers continue to be active.
|
|
*
|
|
* Responder being granted:
|
|
*
|
|
* A view can become the "responder" after the following events:
|
|
* * "pointerdown" (implemented using "touchstart", "mousedown")
|
|
* * "pointermove" (implemented using "touchmove", "mousemove")
|
|
* * "scroll" (while a pointer is down)
|
|
* * "selectionchange" (while a pointer is down)
|
|
*
|
|
* If nothing is already the "responder", the event propagates to (capture) and from
|
|
* (bubble) the event target until a view returns `true` for
|
|
* `on*ShouldSetResponder(Capture)`.
|
|
*
|
|
* If something is already the responder, the event propagates to (capture) and from
|
|
* (bubble) the lowest common ancestor of the event target and the current "responder".
|
|
* Then negotiation happens between the current "responder" and a view that wants to
|
|
* become the "responder": see the timing diagram below.
|
|
*
|
|
* (NOTE: Scrolled views either automatically become the "responder" or release the
|
|
* "interaction lock". A native scroll view that isn't built on top of the responder
|
|
* system must result in the current "responder" being notified that it no longer has
|
|
* the "interaction lock" - the native system has taken over.
|
|
*
|
|
* Responder being released:
|
|
*
|
|
* As soon as there are no more active pointers that *started* inside descendants
|
|
* of the *current* "responder", an `onResponderRelease` event is dispatched to the
|
|
* current "responder", and the responder lock is released.
|
|
*
|
|
* Typical sequence of events:
|
|
* * startShouldSetResponder
|
|
* * responderGrant/Reject
|
|
* * responderStart
|
|
* * responderMove
|
|
* * responderEnd
|
|
* * responderRelease
|
|
*/
|
|
|
|
/* Negotiation Performed
|
|
+-----------------------+
|
|
/ \
|
|
Process low level events to + Current Responder + wantsResponderID
|
|
determine who to perform negot-| (if any exists at all) |
|
|
iation/transition | Otherwise just pass through|
|
|
-------------------------------+----------------------------+------------------+
|
|
Bubble to find first ID | |
|
|
to return true:wantsResponderID| |
|
|
| |
|
|
+--------------+ | |
|
|
| onTouchStart | | |
|
|
+------+-------+ none | |
|
|
| return| |
|
|
+-----------v-------------+true| +------------------------+ |
|
|
|onStartShouldSetResponder|----->| onResponderStart (cur) |<-----------+
|
|
+-----------+-------------+ | +------------------------+ | |
|
|
| | | +--------+-------+
|
|
| returned true for| false:REJECT +-------->|onResponderReject
|
|
| wantsResponderID | | | +----------------+
|
|
| (now attempt | +------------------+-----+ |
|
|
| handoff) | | onResponder | |
|
|
+------------------->| TerminationRequest | |
|
|
| +------------------+-----+ |
|
|
| | | +----------------+
|
|
| true:GRANT +-------->|onResponderGrant|
|
|
| | +--------+-------+
|
|
| +------------------------+ | |
|
|
| | onResponderTerminate |<-----------+
|
|
| +------------------+-----+ |
|
|
| | | +----------------+
|
|
| +-------->|onResponderStart|
|
|
| | +----------------+
|
|
Bubble to find first ID | |
|
|
to return true:wantsResponderID| |
|
|
| |
|
|
+-------------+ | |
|
|
| onTouchMove | | |
|
|
+------+------+ none | |
|
|
| return| |
|
|
+-----------v-------------+true| +------------------------+ |
|
|
|onMoveShouldSetResponder |----->| onResponderMove (cur) |<-----------+
|
|
+-----------+-------------+ | +------------------------+ | |
|
|
| | | +--------+-------+
|
|
| returned true for| false:REJECT +-------->|onResponderReject
|
|
| wantsResponderID | | | +----------------+
|
|
| (now attempt | +------------------+-----+ |
|
|
| handoff) | | onResponder | |
|
|
+------------------->| TerminationRequest| |
|
|
| +------------------+-----+ |
|
|
| | | +----------------+
|
|
| true:GRANT +-------->|onResponderGrant|
|
|
| | +--------+-------+
|
|
| +------------------------+ | |
|
|
| | onResponderTerminate |<-----------+
|
|
| +------------------+-----+ |
|
|
| | | +----------------+
|
|
| +-------->|onResponderMove |
|
|
| | +----------------+
|
|
| |
|
|
| |
|
|
Some active touch started| |
|
|
inside current responder | +------------------------+ |
|
|
+------------------------->| onResponderEnd | |
|
|
| | +------------------------+ |
|
|
+---+---------+ | |
|
|
| onTouchEnd | | |
|
|
+---+---------+ | |
|
|
| | +------------------------+ |
|
|
+------------------------->| onResponderEnd | |
|
|
No active touches started| +-----------+------------+ |
|
|
inside current responder | | |
|
|
| v |
|
|
| +------------------------+ |
|
|
| | onResponderRelease | |
|
|
| +------------------------+ |
|
|
| |
|
|
+ + */
|
|
var emptyObject = {};
|
|
/* ------------ IMPLEMENTATION ------------ */
|
|
|
|
var startRegistration = ['onStartShouldSetResponderCapture', 'onStartShouldSetResponder', {
|
|
bubbles: true
|
|
}];
|
|
var moveRegistration = ['onMoveShouldSetResponderCapture', 'onMoveShouldSetResponder', {
|
|
bubbles: true
|
|
}];
|
|
var scrollRegistration = ['onScrollShouldSetResponderCapture', 'onScrollShouldSetResponder', {
|
|
bubbles: false
|
|
}];
|
|
var shouldSetResponderEvents = {
|
|
touchstart: startRegistration,
|
|
mousedown: startRegistration,
|
|
touchmove: moveRegistration,
|
|
mousemove: moveRegistration,
|
|
scroll: scrollRegistration
|
|
};
|
|
var emptyResponder = {
|
|
id: null,
|
|
idPath: null,
|
|
node: null
|
|
};
|
|
var responderListenersMap = new Map();
|
|
var isEmulatingMouseEvents = false;
|
|
var trackedTouchCount = 0;
|
|
var currentResponder = {
|
|
id: null,
|
|
node: null,
|
|
idPath: null
|
|
};
|
|
|
|
function changeCurrentResponder(responder) {
|
|
currentResponder = responder;
|
|
}
|
|
|
|
function getResponderConfig(id) {
|
|
var config = responderListenersMap.get(id);
|
|
return config != null ? config : emptyObject;
|
|
}
|
|
/**
|
|
* Process native events
|
|
*
|
|
* A single event listener is used to manage the responder system.
|
|
* All pointers are tracked in the ResponderTouchHistoryStore. Native events
|
|
* are interpreted in terms of the Responder System and checked to see if
|
|
* the responder should be transferred. Each host node that is attached to
|
|
* the Responder System has an ID, which is used to look up its associated
|
|
* callbacks.
|
|
*/
|
|
|
|
|
|
function eventListener(domEvent) {
|
|
var eventType = domEvent.type;
|
|
var eventTarget = domEvent.target;
|
|
/**
|
|
* Manage emulated events and early bailout.
|
|
* Since PointerEvent is not used yet (lack of support in older Safari), it's
|
|
* necessary to manually manage the mess of browser touch/mouse events.
|
|
* And bailout early for termination events when there is no active responder.
|
|
*/
|
|
// Flag when browser may produce emulated events
|
|
|
|
if (eventType === 'touchstart') {
|
|
isEmulatingMouseEvents = true;
|
|
} // Remove flag when browser will not produce emulated events
|
|
|
|
|
|
if (eventType === 'touchmove' || trackedTouchCount > 1) {
|
|
isEmulatingMouseEvents = false;
|
|
} // Ignore various events in particular circumstances
|
|
|
|
|
|
if ( // Ignore browser emulated mouse events
|
|
eventType === 'mousedown' && isEmulatingMouseEvents || eventType === 'mousemove' && isEmulatingMouseEvents || // Ignore mousemove if a mousedown didn't occur first
|
|
eventType === 'mousemove' && trackedTouchCount < 1) {
|
|
return;
|
|
} // Remove flag after emulated events are finished
|
|
|
|
|
|
if (isEmulatingMouseEvents && eventType === 'mouseup') {
|
|
if (trackedTouchCount === 0) {
|
|
isEmulatingMouseEvents = false;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var isStartEvent = (0, _ResponderEventTypes.isStartish)(eventType) && (0, _utils.isPrimaryPointerDown)(domEvent);
|
|
var isMoveEvent = (0, _ResponderEventTypes.isMoveish)(eventType);
|
|
var isEndEvent = (0, _ResponderEventTypes.isEndish)(eventType);
|
|
var isScrollEvent = (0, _ResponderEventTypes.isScroll)(eventType);
|
|
var isSelectionChangeEvent = (0, _ResponderEventTypes.isSelectionChange)(eventType);
|
|
var responderEvent = (0, _createResponderEvent.default)(domEvent);
|
|
/**
|
|
* Record the state of active pointers
|
|
*/
|
|
|
|
if (isStartEvent || isMoveEvent || isEndEvent) {
|
|
if (domEvent.touches) {
|
|
trackedTouchCount = domEvent.touches.length;
|
|
} else {
|
|
if (isStartEvent) {
|
|
trackedTouchCount = 1;
|
|
} else if (isEndEvent) {
|
|
trackedTouchCount = 0;
|
|
}
|
|
}
|
|
|
|
_ResponderTouchHistoryStore.default.recordTouchTrack(eventType, responderEvent.nativeEvent);
|
|
}
|
|
/**
|
|
* Responder System logic
|
|
*/
|
|
|
|
|
|
var eventPaths = (0, _utils.getResponderPaths)(domEvent);
|
|
var wasNegotiated = false;
|
|
var wantsResponder; // If an event occured that might change the current responder...
|
|
|
|
if (isStartEvent || isMoveEvent || isScrollEvent && trackedTouchCount > 0) {
|
|
// If there is already a responder, prune the event paths to the lowest common ancestor
|
|
// of the existing responder and deepest target of the event.
|
|
var currentResponderIdPath = currentResponder.idPath;
|
|
var eventIdPath = eventPaths.idPath;
|
|
|
|
if (currentResponderIdPath != null && eventIdPath != null) {
|
|
var lowestCommonAncestor = (0, _utils.getLowestCommonAncestor)(currentResponderIdPath, eventIdPath);
|
|
|
|
if (lowestCommonAncestor != null) {
|
|
var indexOfLowestCommonAncestor = eventIdPath.indexOf(lowestCommonAncestor); // Skip the current responder so it doesn't receive unexpected "shouldSet" events.
|
|
|
|
var index = indexOfLowestCommonAncestor + (lowestCommonAncestor === currentResponder.id ? 1 : 0);
|
|
eventPaths = {
|
|
idPath: eventIdPath.slice(index),
|
|
nodePath: eventPaths.nodePath.slice(index)
|
|
};
|
|
} else {
|
|
eventPaths = null;
|
|
}
|
|
}
|
|
|
|
if (eventPaths != null) {
|
|
// If a node wants to become the responder, attempt to transfer.
|
|
wantsResponder = findWantsResponder(eventPaths, domEvent, responderEvent);
|
|
|
|
if (wantsResponder != null) {
|
|
// Sets responder if none exists, or negotates with existing responder.
|
|
attemptTransfer(responderEvent, wantsResponder);
|
|
wasNegotiated = true;
|
|
}
|
|
}
|
|
} // If there is now a responder, invoke its callbacks for the lifecycle of the gesture.
|
|
|
|
|
|
if (currentResponder.id != null && currentResponder.node != null) {
|
|
var _currentResponder = currentResponder,
|
|
id = _currentResponder.id,
|
|
node = _currentResponder.node;
|
|
|
|
var _getResponderConfig = getResponderConfig(id),
|
|
onResponderStart = _getResponderConfig.onResponderStart,
|
|
onResponderMove = _getResponderConfig.onResponderMove,
|
|
onResponderEnd = _getResponderConfig.onResponderEnd,
|
|
onResponderRelease = _getResponderConfig.onResponderRelease,
|
|
onResponderTerminate = _getResponderConfig.onResponderTerminate,
|
|
onResponderTerminationRequest = _getResponderConfig.onResponderTerminationRequest;
|
|
|
|
responderEvent.bubbles = false;
|
|
responderEvent.cancelable = false;
|
|
responderEvent.currentTarget = node; // Start
|
|
|
|
if (isStartEvent) {
|
|
if (onResponderStart != null) {
|
|
onResponderStart(responderEvent);
|
|
}
|
|
} // Move
|
|
else if (isMoveEvent) {
|
|
if (onResponderMove != null) {
|
|
onResponderMove(responderEvent);
|
|
}
|
|
} else {
|
|
var isTerminateEvent = (0, _ResponderEventTypes.isCancelish)(eventType) || // native context menu
|
|
eventType === 'contextmenu' || // window blur
|
|
eventType === 'blur' && eventTarget === window || // responder (or ancestors) blur
|
|
eventType === 'blur' && eventTarget.contains(node) && domEvent.relatedTarget !== node || // native scroll without using a pointer
|
|
isScrollEvent && trackedTouchCount === 0 || // native scroll on node that is parent of the responder (allow siblings to scroll)
|
|
isScrollEvent && eventTarget.contains(node) && eventTarget !== node || // native select/selectionchange on node
|
|
isSelectionChangeEvent && (0, _utils.hasValidSelection)(domEvent);
|
|
var isReleaseEvent = isEndEvent && !isTerminateEvent && !(0, _utils.hasTargetTouches)(node, domEvent.touches); // End
|
|
|
|
if (isEndEvent) {
|
|
if (onResponderEnd != null) {
|
|
onResponderEnd(responderEvent);
|
|
}
|
|
} // Release
|
|
|
|
|
|
if (isReleaseEvent) {
|
|
if (onResponderRelease != null) {
|
|
onResponderRelease(responderEvent);
|
|
}
|
|
|
|
changeCurrentResponder(emptyResponder);
|
|
} // Terminate
|
|
|
|
|
|
if (isTerminateEvent) {
|
|
var shouldTerminate = true; // Responders can still avoid termination but only for these events.
|
|
|
|
if (eventType === 'contextmenu' || eventType === 'scroll' || eventType === 'selectionchange') {
|
|
if (wasNegotiated || // Only call this function is it wasn't already called during negotiation.
|
|
onResponderTerminationRequest != null && onResponderTerminationRequest(responderEvent) === false) {
|
|
shouldTerminate = false;
|
|
}
|
|
}
|
|
|
|
if (shouldTerminate) {
|
|
if (onResponderTerminate != null) {
|
|
onResponderTerminate(responderEvent);
|
|
}
|
|
|
|
changeCurrentResponder(emptyResponder);
|
|
isEmulatingMouseEvents = false;
|
|
trackedTouchCount = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Walk the event path to/from the target node. At each node, stop and call the
|
|
* relevant "shouldSet" functions for the given event type. If any of those functions
|
|
* call "stopPropagation" on the event, stop searching for a responder.
|
|
*/
|
|
|
|
|
|
function findWantsResponder(eventPaths, domEvent, responderEvent) {
|
|
var shouldSetCallbacks = shouldSetResponderEvents[domEvent.type]; // for Flow
|
|
|
|
if (shouldSetCallbacks != null) {
|
|
var idPath = eventPaths.idPath,
|
|
nodePath = eventPaths.nodePath;
|
|
var shouldSetCallbackCaptureName = shouldSetCallbacks[0];
|
|
var shouldSetCallbackBubbleName = shouldSetCallbacks[1];
|
|
var bubbles = shouldSetCallbacks[2].bubbles;
|
|
|
|
var check = function check(id, node, callbackName) {
|
|
var config = getResponderConfig(id);
|
|
var shouldSetCallback = config[callbackName];
|
|
|
|
if (shouldSetCallback != null) {
|
|
if (shouldSetCallback(responderEvent) === true) {
|
|
return {
|
|
id: id,
|
|
node: node,
|
|
idPath: idPath
|
|
};
|
|
}
|
|
}
|
|
}; // capture
|
|
|
|
|
|
for (var i = idPath.length - 1; i >= 0; i--) {
|
|
var id = idPath[i];
|
|
var node = nodePath[i];
|
|
var result = check(id, node, shouldSetCallbackCaptureName);
|
|
|
|
if (result != null) {
|
|
return result;
|
|
}
|
|
|
|
if (responderEvent.isPropagationStopped() === true) {
|
|
return;
|
|
}
|
|
} // bubble
|
|
|
|
|
|
if (bubbles) {
|
|
for (var _i = 0; _i < idPath.length; _i++) {
|
|
var _id = idPath[_i];
|
|
var _node = nodePath[_i];
|
|
|
|
var _result = check(_id, _node, shouldSetCallbackBubbleName);
|
|
|
|
if (_result != null) {
|
|
return _result;
|
|
}
|
|
|
|
if (responderEvent.isPropagationStopped() === true) {
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
var _id2 = idPath[0];
|
|
var _node2 = nodePath[0];
|
|
var target = domEvent.target;
|
|
|
|
if (target === _node2) {
|
|
return check(_id2, _node2, shouldSetCallbackBubbleName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Attempt to transfer the responder.
|
|
*/
|
|
|
|
|
|
function attemptTransfer(responderEvent, wantsResponder) {
|
|
var _currentResponder2 = currentResponder,
|
|
currentId = _currentResponder2.id,
|
|
currentNode = _currentResponder2.node;
|
|
var id = wantsResponder.id,
|
|
node = wantsResponder.node;
|
|
|
|
var _getResponderConfig2 = getResponderConfig(id),
|
|
onResponderGrant = _getResponderConfig2.onResponderGrant,
|
|
onResponderReject = _getResponderConfig2.onResponderReject;
|
|
|
|
responderEvent.bubbles = false;
|
|
responderEvent.cancelable = false;
|
|
responderEvent.currentTarget = node; // Set responder
|
|
|
|
if (currentId == null) {
|
|
if (onResponderGrant != null) {
|
|
responderEvent.currentTarget = node;
|
|
responderEvent.dispatchConfig.registrationName = 'onResponderGrant';
|
|
onResponderGrant(responderEvent);
|
|
}
|
|
|
|
changeCurrentResponder(wantsResponder);
|
|
} // Negotiate with current responder
|
|
else {
|
|
var _getResponderConfig3 = getResponderConfig(currentId),
|
|
onResponderTerminate = _getResponderConfig3.onResponderTerminate,
|
|
onResponderTerminationRequest = _getResponderConfig3.onResponderTerminationRequest;
|
|
|
|
var allowTransfer = onResponderTerminationRequest != null && onResponderTerminationRequest(responderEvent);
|
|
|
|
if (allowTransfer) {
|
|
// Terminate existing responder
|
|
if (onResponderTerminate != null) {
|
|
responderEvent.currentTarget = currentNode;
|
|
onResponderTerminate(responderEvent);
|
|
} // Grant next responder
|
|
|
|
|
|
if (onResponderGrant != null) {
|
|
onResponderGrant(responderEvent);
|
|
}
|
|
|
|
changeCurrentResponder(wantsResponder);
|
|
} else {
|
|
// Reject responder request
|
|
if (onResponderReject != null) {
|
|
onResponderReject(responderEvent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* ------------ PUBLIC API ------------ */
|
|
|
|
/**
|
|
* Attach Listeners
|
|
*
|
|
* Use native events as ReactDOM doesn't have a non-plugin API to implement
|
|
* this system.
|
|
*/
|
|
|
|
|
|
var documentEventsCapturePhase = ['blur', 'scroll'];
|
|
var documentEventsBubblePhase = [// mouse
|
|
'mousedown', 'mousemove', 'mouseup', 'dragstart', // touch
|
|
'touchstart', 'touchmove', 'touchend', 'touchcancel', // other
|
|
'contextmenu', 'select', 'selectionchange'];
|
|
|
|
function attachListeners() {
|
|
if (_ExecutionEnvironment.canUseDOM && window.__reactResponderSystemActive == null) {
|
|
window.addEventListener('blur', eventListener);
|
|
documentEventsBubblePhase.forEach(function (eventType) {
|
|
document.addEventListener(eventType, eventListener);
|
|
});
|
|
documentEventsCapturePhase.forEach(function (eventType) {
|
|
document.addEventListener(eventType, eventListener, true);
|
|
});
|
|
window.__reactResponderSystemActive = true;
|
|
}
|
|
}
|
|
/**
|
|
* Register a node with the ResponderSystem.
|
|
*/
|
|
|
|
|
|
function addNode(id, node, config) {
|
|
(0, _utils.setResponderId)(node, id);
|
|
responderListenersMap.set(id, config);
|
|
}
|
|
/**
|
|
* Unregister a node with the ResponderSystem.
|
|
*/
|
|
|
|
|
|
function removeNode(id) {
|
|
if (currentResponder.id === id) {
|
|
terminateResponder();
|
|
}
|
|
|
|
if (responderListenersMap.has(id)) {
|
|
responderListenersMap.delete(id);
|
|
}
|
|
}
|
|
/**
|
|
* Allow the current responder to be terminated from within components to support
|
|
* more complex requirements, such as use with other React libraries for working
|
|
* with scroll views, input views, etc.
|
|
*/
|
|
|
|
|
|
function terminateResponder() {
|
|
var _currentResponder3 = currentResponder,
|
|
id = _currentResponder3.id,
|
|
node = _currentResponder3.node;
|
|
|
|
if (id != null && node != null) {
|
|
var _getResponderConfig4 = getResponderConfig(id),
|
|
onResponderTerminate = _getResponderConfig4.onResponderTerminate;
|
|
|
|
if (onResponderTerminate != null) {
|
|
var event = (0, _createResponderEvent.default)({});
|
|
event.currentTarget = node;
|
|
onResponderTerminate(event);
|
|
}
|
|
|
|
changeCurrentResponder(emptyResponder);
|
|
}
|
|
|
|
isEmulatingMouseEvents = false;
|
|
trackedTouchCount = 0;
|
|
}
|
|
/**
|
|
* Allow unit tests to inspect the current responder in the system.
|
|
* FOR TESTING ONLY.
|
|
*/
|
|
|
|
|
|
function getResponderNode() {
|
|
return currentResponder.node;
|
|
} |