@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
JavaScript
"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;