/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/gesture.js
695 строк
20 KB
Shihua Zheng
commit (#39097)
06 июн 2023, 02:37
Не верифицирован
06 июн 2023, 02:37
649e4ac
Код
Авторство
О чём код?
import {devAssert} from '#core/assert/dev'; import {Observable} from '#core/data-structures/observable'; import {supportsPassiveEventListener} from '#core/dom/event-helper-listen'; import {findIndex} from '#core/types/array'; import {getWin, toWin} from '#core/window'; import {Pass} from './pass'; const PROP_ = '__AMP_Gestures'; /** * A gesture object contains the type and data of the gesture such as * a tap or a double-tap or a swipe. See {@link GestureRecognizer} for * more details. * @struct * @const * @template DATA */ export class Gesture { /** * @param {string} type The gesture's string type. * @param {DATA} data The data of the gesture. * @param {time} time The time that the gesture has been emitted. * @param {?Event} event An optional browser event that resulted in the * gesture. */ constructor(type, data, time, event) { /** @const {string} */ this.type = type; /** @const {DATA} */ this.data = data; /** @const {number} */ this.time = time; /** @const {?Event} */ this.event = event; } } /** * Gestures object manages all gestures on a particular element. It listens * to all pointer events and delegates them to individual gesture recognizers. * When a recognizer has recognized a gesture and ready to start emitting it * it requests permission to do so from this class which resolves conflicts * between competing recognizers to decide which gesture should go forward. */ export class Gestures { /** * Creates if not yet created and returns the shared Gestures instance for * the specified element. * @param {!Element} element * @param {boolean=} opt_shouldNotPreventDefault * @param {boolean=} opt_shouldStopPropagation * @return {!Gestures} */ static get( element, opt_shouldNotPreventDefault = false, opt_shouldStopPropagation = false ) { let res = element[PROP_]; if (!res) { res = new Gestures( element, opt_shouldNotPreventDefault, opt_shouldStopPropagation ); element[PROP_] = res; } return res; } /** * @param {!Element} element * @param {boolean} shouldNotPreventDefault * @param {boolean} shouldStopPropagation */ constructor( element, shouldNotPreventDefault = false, shouldStopPropagation = false ) { /** @private {!Element} */ this.element_ = element; /** @private {!Array<!GestureRecognizer>} */ this.recognizers_ = []; /** @private {!Array<boolean>} */ this.tracking_ = []; /** @private {!Array<time>} */ this.ready_ = []; /** @private {!Array<time>} */ this.pending_ = []; /** @private {?GestureRecognizer} */ this.eventing_ = null; const win = element.ownerDocument.defaultView; const passiveSupported = supportsPassiveEventListener(toWin(win)); /** @private {boolean} */ this.shouldNotPreventDefault_ = shouldNotPreventDefault || passiveSupported; /** @private {boolean} */ this.shouldStopPropagation_ = shouldStopPropagation; /** * This variable indicates that the eventing has stopped on this * event cycle. * @private {boolean} */ this.wasEventing_ = false; /** @private {!Pass} */ this.pass_ = new Pass(getWin(element), this.doPass_.bind(this)); /** @private {!Observable} */ this.pointerDownObservable_ = new Observable(); /** * Observers for each type of registered gesture types. * @private {!{[key: string]: !Observable<!Gesture>}} */ this.overservers_ = Object.create(null); /** @private @const {function(!Event)} */ this.boundOnTouchStart_ = this.onTouchStart_.bind(this); /** @private @const {function(!Event)} */ this.boundOnTouchEnd_ = this.onTouchEnd_.bind(this); /** @private @const {function(!Event)} */ this.boundOnTouchMove_ = this.onTouchMove_.bind(this); /** @private @const {function(!Event)} */ this.boundOnTouchCancel_ = this.onTouchCancel_.bind(this); this.element_.addEventListener( 'touchstart', this.boundOnTouchStart_, passiveSupported ? {passive: true} : false ); this.element_.addEventListener('touchend', this.boundOnTouchEnd_); this.element_.addEventListener( 'touchmove', this.boundOnTouchMove_, passiveSupported ? {passive: true} : false ); this.element_.addEventListener('touchcancel', this.boundOnTouchCancel_); /** @private {boolean} */ this.passAfterEvent_ = false; } /** * Unsubscribes from all pointer events and removes the shared cache instance. */ cleanup() { this.element_.removeEventListener('touchstart', this.boundOnTouchStart_); this.element_.removeEventListener('touchend', this.boundOnTouchEnd_); this.element_.removeEventListener('touchmove', this.boundOnTouchMove_); this.element_.removeEventListener('touchcancel', this.boundOnTouchCancel_); delete this.element_[PROP_]; this.pass_.cancel(); } /** * Subscribes to a gesture emitted by the specified recognizer. For a first * gesture handler registered in this method the recognizer is installed * and from that point on it participates in the event processing. * * @param {function(new:GestureRecognizer, !Gestures)} recognizerConstr * @param {function(!Gesture)} handler * @return {!UnlistenDef} * @template DATA */ onGesture(recognizerConstr, handler) { const recognizer = new recognizerConstr(this); const type = recognizer.getType(); let overserver = this.overservers_[type]; if (!overserver) { this.recognizers_.push(recognizer); overserver = new Observable(); this.overservers_[type] = overserver; } return overserver.add(handler); } /** * Unsubscribes all handlers from the given gesture recognizer. Returns * true if anything was done. Returns false if there were no handlers * registered on the given gesture recognizer in first place. * * @param {function(new:GestureRecognizer, !Gestures)} recognizerConstr * @return {boolean} */ removeGesture(recognizerConstr) { const type = new recognizerConstr(this).getType(); const overserver = this.overservers_[type]; if (overserver) { overserver.removeAll(); const index = findIndex(this.recognizers_, (e) => e.getType() == type); if (index < 0) { return false; } // Remove the recognizer as well as all associated tracking state this.recognizers_.splice(index, 1); this.ready_.splice(index, 1); this.pending_.splice(index, 1); this.tracking_.splice(index, 1); delete this.overservers_[type]; return true; } else { return false; } } /** * Subscribes to pointer down events, such as "touchstart" or "mousedown". * @param {!Function} handler * @return {!UnlistenDef} */ onPointerDown(handler) { return this.pointerDownObservable_.add(handler); } /** * Handles all "touchstart" events and dispatches them to the tracking * recognizers. * @param {!Event} event * @private */ onTouchStart_(event) { const now = Date.now(); this.wasEventing_ = false; this.pointerDownObservable_.fire(event); for (let i = 0; i < this.recognizers_.length; i++) { if (this.ready_[i]) { // If the recognizer is in the "ready" state, it won't receive // any more touch series until it's allowed to emit. continue; } if (this.pending_[i] && this.pending_[i] < now) { // Pending state expired. Reset. this.stopTracking_(i); } if (this.recognizers_[i].onTouchStart(event)) { // When a recognizer is interested in the touch series it returns "true" // from its onTouchStart method. For this recognizer we start tracking // the whole series of touch events from touchstart to touchend. Other // recognizers will not receive them unless they return "true" from // onTouchStart. this.startTracking_(i); } } this.afterEvent_(event); } /** * Handles all "touchmove" events and dispatches them to the tracking * recognizers. * @param {!Event} event * @private */ onTouchMove_(event) { const now = Date.now(); for (let i = 0; i < this.recognizers_.length; i++) { if (!this.tracking_[i]) { // The whole touch series are ignored for non-tracking recognizers. continue; } if (this.pending_[i] && this.pending_[i] < now) { // Pending state expired. Reset. this.stopTracking_(i); continue; } if (!this.recognizers_[i].onTouchMove(event)) { // Recognizer lost interest in the series. Reset. this.stopTracking_(i); } } this.afterEvent_(event); } /** * Handles all "touchend" events and dispatches them to the tracking * recognizers. * @param {!Event} event * @private */ onTouchEnd_(event) { const now = Date.now(); for (let i = 0; i < this.recognizers_.length; i++) { if (!this.tracking_[i]) { // The whole touch series are ignored for non-tracking recognizers. continue; } if (this.pending_[i] && this.pending_[i] < now) { // Pending state expired. Reset. this.stopTracking_(i); continue; } this.recognizers_[i].onTouchEnd(event); const isReady = !this.pending_[i]; const isExpired = this.pending_[i] < now; const isEventing = this.eventing_ == this.recognizers_[i]; if (!isEventing && (isReady || isExpired)) { this.stopTracking_(i); } } this.afterEvent_(event); } /** * Handles all "touchcancel" events. Cancels all tracking/emitting * recognizers. * @param {!Event} event * @private */ onTouchCancel_(event) { for (let i = 0; i < this.recognizers_.length; i++) { this.cancelEventing_(i); } this.afterEvent_(event); } /** * Callback for a gesture recognizer to communicate that it's ready to * start emitting gestures. Gestures instance may or may not allow the * recognizer to proceed. * @param {!GestureRecognizer} recognizer * @param {number} offset * @private * @restricted * @visibleForTesting */ signalReady_(recognizer, offset) { // Somebody got here first. if (this.eventing_) { recognizer.acceptCancel(); return; } // Set the recognizer as ready and wait for the pass to // make the decision. const now = Date.now(); for (let i = 0; i < this.recognizers_.length; i++) { if (this.recognizers_[i] == recognizer) { this.ready_[i] = now + offset; this.pending_[i] = 0; } } this.passAfterEvent_ = true; } /** * Callback for a gesture recognizer to communicate that it's close to * start emitting gestures, but needs more time to see more events. Once * this time expires the recognizer should either signal readiness or it * will be canceled. * @param {!GestureRecognizer} recognizer * @param {number} timeLeft * @private * @restricted * @visibleForTesting */ signalPending_(recognizer, timeLeft) { // Somebody got here first. if (this.eventing_) { recognizer.acceptCancel(); return; } const now = Date.now(); for (let i = 0; i < this.recognizers_.length; i++) { if (this.recognizers_[i] == recognizer) { this.pending_[i] = now + timeLeft; } } } /** * Callback for a gesture recognizer to communicate that it's done * emitting gestures. * @param {!GestureRecognizer} recognizer * @private * @restricted * @visibleForTesting */ signalEnd_(recognizer) { if (this.eventing_ == recognizer) { this.eventing_ = null; this.wasEventing_ = true; } } /** * Callback for a gesture emit the gesture. Only the currently emitting * recognizer is allowed to emit gestures. * @param {!GestureRecognizer} recognizer * @param {*} data * @param {?Event} event * @private * @restricted * @visibleForTesting */ signalEmit_(recognizer, data, event) { devAssert( this.eventing_ == recognizer, 'Recognizer is not currently allowed: %s', recognizer.getType() ); const overserver = this.overservers_[recognizer.getType()]; if (overserver) { overserver.fire( new Gesture(recognizer.getType(), data, Date.now(), event) ); } } /** * @param {!Event} event * @private */ afterEvent_(event) { let cancelEvent = !!this.eventing_ || this.wasEventing_; this.wasEventing_ = false; if (!cancelEvent) { const now = Date.now(); for (let i = 0; i < this.recognizers_.length; i++) { if (this.ready_[i] || (this.pending_[i] && this.pending_[i] >= now)) { cancelEvent = true; break; } } } if (cancelEvent) { event.stopPropagation(); if (!this.shouldNotPreventDefault_) { event.preventDefault(); } } else if (this.shouldStopPropagation_) { event.stopPropagation(); } if (this.passAfterEvent_) { this.passAfterEvent_ = false; this.doPass_(); } } /** * The pass that decides which recognizers can start emitting and which * are canceled. * @private */ doPass_() { const now = Date.now(); // The "most ready" recognizer is the youngest in the "ready" set. // Otherwise we wouldn't wait for it at all. let readyIndex = -1; for (let i = 0; i < this.recognizers_.length; i++) { if (!this.ready_[i]) { if (this.pending_[i] && this.pending_[i] < now) { // Pending state expired. Reset. this.stopTracking_(i); } continue; } if (readyIndex == -1 || this.ready_[i] > this.ready_[readyIndex]) { readyIndex = i; } } if (readyIndex == -1) { // Nothing to do. return; } // Look for conflicts. let waitTime = 0; for (let i = 0; i < this.recognizers_.length; i++) { if (this.ready_[i] || !this.tracking_[i]) { continue; } waitTime = Math.max(waitTime, this.pending_[i] - now); } if (waitTime < 2) { // We waited long enough. this.startEventing_(readyIndex); return; } // Some conflicts: have to wait to see who wins. this.pass_.schedule(waitTime); } /** * This recognizer is given "go ahead" and all others are canceled. * @param {number} index * @private */ startEventing_(index) { const recognizer = this.recognizers_[index]; for (let i = 0; i < this.recognizers_.length; i++) { if (i != index) { this.cancelEventing_(i); } } this.ready_[index] = 0; this.pending_[index] = 0; this.eventing_ = recognizer; recognizer.acceptStart(); } /** * @param {number} index * @private */ startTracking_(index) { this.tracking_[index] = true; this.pending_[index] = 0; } /** * @param {number} index * @private */ stopTracking_(index) { this.tracking_[index] = false; this.pending_[index] = 0; if (!this.ready_[index]) { this.recognizers_[index].acceptCancel(); } } /** * @param {number} index * @private */ cancelEventing_(index) { this.ready_[index] = 0; this.stopTracking_(index); } } /** * The gesture recognizer receives the pointer events from Gestures instance. * Based on these events, it can "recognize" the gesture it's responsible for, * request to start emitting and emit gestures. Gestures instances manages * several competing recognizers and decides which ones get to emit gestures * and which do not. * * The recognizer can be in several main states: * 1. Tracking state. In this state the recognizer is receiving the series of * touch events from touchstart to touchend. To get into this state the * recognizer has to return "true" from the {@link onTouchStart}. * 2. Pending state (optional). The recognizer matched part of the gesture, * but needs more time to get track more events. It requests more time * by calling {@link signalPending}, By the end of this time the recognizer * has either matched the gesture or has been canceled. * 3. Ready state. The recognizer matched the whole gesture and ready to start * emitting. It communicates to the Gestures this readiness by calling * {@link signalReady}. * 5. Emitting state. If Gestures decides to go ahead with this recognizer, it * will call {@link acceptStart} method. Otherwise, it will call * {@link acceptCancel} method. Once in the emitting state, the recognizer * can emit any number of events by calling {@link signalEmit}. * 6. Complete state. Once done, the recognizer can call {@link signalEnd} to * communicate that it's done. * * @template DATA */ export class GestureRecognizer { /** * @param {string} type * @param {!Gestures} manager */ constructor(type, manager) { /** @private @const {string} */ this.type_ = type; /** @private @const {!Gestures} */ this.manager_ = manager; } /** * Returns the type of the gesture emitted by the instance of this class. * It has to be unique in the scope of the Gestures instance. * @return {string} */ getType() { return this.type_; } /** * The recognizer can call this method to communicate that it's ready to * start emitting the gesture. Optionally it can pass a zero, positive or * negative offset - a time on how much the gesture should be penalized or * given advantage in conflict resolution. The recognizer at this point is * in the "ready" state. * @param {time} offset */ signalReady(offset) { this.manager_.signalReady_(this, offset); } /** * The recognizer can call this method to communicate that it needs more * time (timeLeft) to match the gesture. By the end of this time the * recognizer has to either transit to the ready state using * {@link signalReady} or it will be canceled. The recognizer is in the * "pending" state. * @param {time} timeLeft */ signalPending(timeLeft) { this.manager_.signalPending_(this, timeLeft); } /** * The recognizer can call this method to communicate that it's done * emitting the gestures. It will return to the waiting state. Recognizer * can only call this method if it has previously received the * {@link acceptStart} call. */ signalEnd() { this.manager_.signalEnd_(this); } /** * The recognizer can call this method to emit the gestures while in the * "emitting" state. Recognizer can only call this method if it has * previously received the {@link acceptStart} call. * @param {DATA} data * @param {?Event} event */ signalEmit(data, event) { this.manager_.signalEmit_(this, data, event); } /** * The Gestures instance calls this method to allow the recognizer to start * emitting the gestures. At this point the recognizer is in the "emitting" * state. It will be in this state until it calls {@link signalEnd} or * the {@link acceptCancel} is called by the Gestures instance. */ acceptStart() {} /** * The Gestures instance calls this method to reset the recognizer. At this * point the recognizer is in the initial waiting state. */ acceptCancel() {} /** * The Gestures instance calls this method for each "touchstart" event. If * the recognizer wants to receive other touch events in the series, it has * to return "true". * @param {!Event} unusedEvent * @return {boolean} */ onTouchStart(unusedEvent) { return false; } /** * The Gestures instance calls this method for each "touchmove" event. If * the recognizer wants to continue receiving touch events in the series, * it has to return "true". * @param {!Event} unusedEvent * @return {boolean} */ onTouchMove(unusedEvent) { return false; } /** * The Gestures instance calls this method for the "touchend" event. * Somewhere within this touch series the recognizer has to call * {@link signalReady} or {@link signalPending} or it will be reset for the * next touch series. * @param {!Event} unusedEvent */ onTouchEnd(unusedEvent) {} }