UNPKG

@mui/x-internal-gestures

Version:

The core engine of GestureEvents, a modern and robust multi-pointer gesture detection library for JavaScript.

283 lines (256 loc) 9.07 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.TapGesture = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _PointerGesture = require("../PointerGesture"); var _utils = require("../utils"); /** * TapGesture - Detects tap (quick touch without movement) gestures * * This gesture tracks simple tap interactions on elements, firing a single event when: * - A complete tap is detected (pointerup after brief touch without excessive movement) * - The tap is canceled (event.g., moved too far or held too long) */ /** * Configuration options for TapGesture * Extends PointerGestureOptions with tap-specific settings */ /** * Event data specific to tap gesture events * Contains information about the tap location and counts */ /** * Type definition for the CustomEvent created by TapGesture */ /** * State tracking for the TapGesture */ /** * TapGesture class for handling tap interactions * * This gesture detects when users tap on elements without significant movement, * and can recognize single taps, double taps, or other multi-tap sequences. */ class TapGesture extends _PointerGesture.PointerGesture { constructor(options) { super(options); this.state = { startCentroid: null, currentTapCount: 0, lastTapTime: 0, lastPosition: null }; this.isSinglePhase = void 0; this.eventType = void 0; this.optionsType = void 0; this.mutableOptionsType = void 0; this.mutableStateType = void 0; /** * Maximum distance a pointer can move for a gesture to still be considered a tap */ this.maxDistance = void 0; /** * Number of consecutive taps to detect */ this.taps = void 0; this.maxDistance = options.maxDistance ?? 10; this.taps = options.taps ?? 1; } clone(overrides) { return new TapGesture((0, _extends2.default)({ name: this.name, preventDefault: this.preventDefault, stopPropagation: this.stopPropagation, minPointers: this.minPointers, maxPointers: this.maxPointers, maxDistance: this.maxDistance, taps: this.taps, requiredKeys: [...this.requiredKeys], pointerMode: [...this.pointerMode], preventIf: [...this.preventIf] }, overrides)); } destroy() { this.resetState(); super.destroy(); } updateOptions(options) { super.updateOptions(options); this.maxDistance = options.maxDistance ?? this.maxDistance; this.taps = options.taps ?? this.taps; } resetState() { this.isActive = false; this.state = { startCentroid: null, currentTapCount: 0, lastTapTime: 0, lastPosition: null }; } /** * Handle pointer events for the tap gesture */ handlePointerEvent(pointers, event) { const pointersArray = Array.from(pointers.values()); // Find which element (if any) is being targeted const targetElement = this.getTargetElement(event); if (!targetElement) { return; } // Filter pointers to only include those targeting our element or its children const relevantPointers = this.getRelevantPointers(pointersArray, targetElement); // Check if we have enough pointers and not too many if (this.shouldPreventGesture(targetElement) || relevantPointers.length < this.minPointers || relevantPointers.length > this.maxPointers) { if (this.isActive) { // Cancel the gesture if it was active this.cancelTap(targetElement, relevantPointers, event); } return; } switch (event.type) { case 'pointerdown': if (!this.isActive) { // Calculate and store the starting centroid this.state.startCentroid = (0, _utils.calculateCentroid)(relevantPointers); this.state.lastPosition = (0, _extends2.default)({}, this.state.startCentroid); this.isActive = true; // Store the original target element this.originalTarget = targetElement; } break; case 'pointermove': if (this.isActive && this.state.startCentroid) { // Calculate current position const currentPosition = (0, _utils.calculateCentroid)(relevantPointers); this.state.lastPosition = currentPosition; // Calculate distance from start position const deltaX = currentPosition.x - this.state.startCentroid.x; const deltaY = currentPosition.y - this.state.startCentroid.y; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // If moved too far, cancel the tap gesture if (distance > this.maxDistance) { this.cancelTap(targetElement, relevantPointers, event); } } break; case 'pointerup': if (this.isActive) { // For valid tap: increment tap count this.state.currentTapCount += 1; // Make sure we have a valid position before firing the tap event const position = this.state.lastPosition || this.state.startCentroid; if (!position) { this.cancelTap(targetElement, relevantPointers, event); return; } // Check if we've reached the desired number of taps if (this.state.currentTapCount >= this.taps) { // The complete tap sequence has been detected - fire the tap event this.fireTapEvent(targetElement, relevantPointers, event, position); // Reset state after successful tap this.resetState(); } else { // Store the time of this tap for multi-tap detection this.state.lastTapTime = event.timeStamp; // Reset active state but keep the tap count for multi-tap detection this.isActive = false; // For multi-tap detection: keep track of the last tap position // but clear the start centroid to prepare for next tap this.state.startCentroid = null; // Start a timeout to reset the tap count if the next tap doesn't come soon enough setTimeout(() => { if (this.state && this.state.currentTapCount > 0 && this.state.currentTapCount < this.taps) { this.state.currentTapCount = 0; } }, 300); // 300ms is a typical double-tap detection window } } break; case 'pointercancel': case 'forceCancel': // Cancel the gesture this.cancelTap(targetElement, relevantPointers, event); break; default: break; } } /** * Fire the main tap event when a valid tap is detected */ fireTapEvent(element, pointers, event, position) { // Get list of active gestures const activeGestures = this.gesturesRegistry.getActiveGestures(element); // Create custom event data for the tap event const customEventData = { gestureName: this.name, centroid: position, target: event.target, srcEvent: event, phase: 'end', // The tap is complete, so we use 'end' state for the event data pointers, timeStamp: event.timeStamp, x: position.x, y: position.y, tapCount: this.state.currentTapCount, activeGestures, customData: this.customData }; // Dispatch a single 'tap' event (not 'tapStart', 'tapEnd', etc.) const domEvent = new CustomEvent(this.name, { bubbles: true, cancelable: true, composed: true, detail: customEventData }); element.dispatchEvent(domEvent); // Apply preventDefault/stopPropagation if configured if (this.preventDefault) { event.preventDefault(); } if (this.stopPropagation) { event.stopPropagation(); } } /** * Cancel the current tap gesture */ cancelTap(element, pointers, event) { if (this.state.startCentroid || this.state.lastPosition) { const position = this.state.lastPosition || this.state.startCentroid; // Get list of active gestures const activeGestures = this.gesturesRegistry.getActiveGestures(element); // Create custom event data for the cancel event const customEventData = { gestureName: this.name, centroid: position, target: event.target, srcEvent: event, phase: 'cancel', pointers, timeStamp: event.timeStamp, x: position.x, y: position.y, tapCount: this.state.currentTapCount, activeGestures, customData: this.customData }; // Dispatch a 'tapCancel' event const eventName = (0, _utils.createEventName)(this.name, 'cancel'); const domEvent = new CustomEvent(eventName, { bubbles: true, cancelable: true, composed: true, detail: customEventData }); element.dispatchEvent(domEvent); } this.resetState(); } } exports.TapGesture = TapGesture;