You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

272 lines
12 KiB

/**
* 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.
*
* @flow
*/
/**
* 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 | |
| +------------------------+ |
| |
+ + */
import type { ResponderEvent } from './createResponderEvent';
import createResponderEvent from './createResponderEvent';
import { isCancelish, isEndish, isMoveish, isScroll, isSelectionChange, isStartish } from './ResponderEventTypes';
import { getLowestCommonAncestor, getResponderPaths, hasTargetTouches, hasValidSelection, isPrimaryPointerDown, setResponderId } from './utils';
import { ResponderTouchHistoryStore } from './ResponderTouchHistoryStore';
import canUseDOM from '../canUseDom';
/* ------------ TYPES ------------ */
type ResponderId = number;
type ActiveResponderInstance = {
id: ResponderId,
idPath: Array<number>,
node: any,
};
type EmptyResponderInstance = {
id: null,
idPath: null,
node: null,
};
type ResponderInstance = ActiveResponderInstance | EmptyResponderInstance;
export type ResponderConfig = {
// Direct responder events dispatched directly to responder. Do not bubble.
onResponderEnd?: ?(e: ResponderEvent) => void,
onResponderGrant?: ?(e: ResponderEvent) => void | boolean,
onResponderMove?: ?(e: ResponderEvent) => void,
onResponderRelease?: ?(e: ResponderEvent) => void,
onResponderReject?: ?(e: ResponderEvent) => void,
onResponderStart?: ?(e: ResponderEvent) => void,
onResponderTerminate?: ?(e: ResponderEvent) => void,
onResponderTerminationRequest?: ?(e: ResponderEvent) => boolean,
// On pointer down, should this element become the responder?
onStartShouldSetResponder?: ?(e: ResponderEvent) => boolean,
onStartShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
// On pointer move, should this element become the responder?
onMoveShouldSetResponder?: ?(e: ResponderEvent) => boolean,
onMoveShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
// On scroll, should this element become the responder? Do no bubble
onScrollShouldSetResponder?: ?(e: ResponderEvent) => boolean,
onScrollShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
// On text selection change, should this element become the responder?
onSelectionChangeShouldSetResponder?: ?(e: ResponderEvent) => boolean,
onSelectionChangeShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
};
const emptyObject = {};
/* ------------ IMPLEMENTATION ------------ */
const startRegistration = ['onStartShouldSetResponderCapture', 'onStartShouldSetResponder', {
bubbles: true
}];
const moveRegistration = ['onMoveShouldSetResponderCapture', 'onMoveShouldSetResponder', {
bubbles: true
}];
const scrollRegistration = ['onScrollShouldSetResponderCapture', 'onScrollShouldSetResponder', {
bubbles: false
}];
const shouldSetResponderEvents = {
touchstart: startRegistration,
mousedown: startRegistration,
touchmove: moveRegistration,
mousemove: moveRegistration,
scroll: scrollRegistration
};
const emptyResponder = {
id: null,
idPath: null,
node: null
};
const responderListenersMap = new Map();
let isEmulatingMouseEvents = false;
let trackedTouchCount = 0;
let currentResponder: ResponderInstance = {
id: null,
node: null,
idPath: null
};
const responderTouchHistoryStore = new ResponderTouchHistoryStore();
declare function changeCurrentResponder(responder: ResponderInstance): any;
declare function getResponderConfig(id: ResponderId): ResponderConfig | Object;
/**
* 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.
*/
declare function eventListener(domEvent: any): any;
/**
* 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.
*/
declare function findWantsResponder(eventPaths: any, domEvent: any, responderEvent: any): any;
/**
* Attempt to transfer the responder.
*/
declare function attemptTransfer(responderEvent: ResponderEvent, wantsResponder: ActiveResponderInstance): any;
/* ------------ PUBLIC API ------------ */
/**
* Attach Listeners
*
* Use native events as ReactDOM doesn't have a non-plugin API to implement
* this system.
*/
const documentEventsCapturePhase = ['blur', 'scroll'];
const documentEventsBubblePhase = [// mouse
'mousedown', 'mousemove', 'mouseup', 'dragstart', // touch
'touchstart', 'touchmove', 'touchend', 'touchcancel', // other
'contextmenu', 'select', 'selectionchange'];
declare export function attachListeners(): any;
/**
* Register a node with the ResponderSystem.
*/
declare export function addNode(id: ResponderId, node: any, config: ResponderConfig): any;
/**
* Unregister a node with the ResponderSystem.
*/
declare export function removeNode(id: ResponderId): any;
/**
* 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.
*/
declare export function terminateResponder(): any;
/**
* Allow unit tests to inspect the current responder in the system.
* FOR TESTING ONLY.
*/
declare export function getResponderNode(): any;