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
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; |