@mui/x-internal-gestures
Version:
The core engine of GestureEvents, a modern and robust multi-pointer gesture detection library for JavaScript.
284 lines (258 loc) • 9.37 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PressGesture = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _PointerGesture = require("../PointerGesture");
var _utils = require("../utils");
/**
* PressGesture - Detects press and hold interactions
*
* This gesture tracks when users press and hold on an element for a specified duration, firing events when:
* - The press begins and passes the holding threshold time (start, ongoing)
* - The press ends (end)
* - The press is canceled by movement beyond threshold (cancel)
*
* This gesture is commonly used for contextual menus, revealing additional options, or alternate actions.
*/
/**
* Configuration options for PressGesture
* Extends PointerGestureOptions with press-specific options
*/
/**
* Event data specific to press gesture events
* Contains information about the press location and duration
*/
/**
* Type definition for the CustomEvent created by PressGesture
*/
/**
* State tracking for the PressGesture
*/
/**
* PressGesture class for handling press/hold interactions
*
* This gesture detects when users press and hold on an element for a specified duration,
* and dispatches press-related events when the user holds long enough.
*
* The `start` and `ongoing` events are dispatched at the same time once the press threshold is reached.
* If the press is canceled (event.g., by moving too far), a `cancel` event is dispatched before the `end` event.
*/
class PressGesture extends _PointerGesture.PointerGesture {
constructor(options) {
super(options);
this.state = {
startCentroid: null,
lastPosition: null,
timerId: null,
startTime: 0,
pressThresholdReached: false
};
this.isSinglePhase = void 0;
this.eventType = void 0;
this.optionsType = void 0;
this.mutableOptionsType = void 0;
this.mutableStateType = void 0;
/**
* Duration in milliseconds required to hold before the press gesture is recognized
*/
this.duration = void 0;
/**
* Maximum distance a pointer can move for a gesture to still be considered a press
*/
this.maxDistance = void 0;
this.duration = options.duration ?? 500;
this.maxDistance = options.maxDistance ?? 10;
}
clone(overrides) {
return new PressGesture((0, _extends2.default)({
name: this.name,
preventDefault: this.preventDefault,
stopPropagation: this.stopPropagation,
minPointers: this.minPointers,
maxPointers: this.maxPointers,
duration: this.duration,
maxDistance: this.maxDistance,
requiredKeys: [...this.requiredKeys],
pointerMode: [...this.pointerMode],
preventIf: [...this.preventIf]
}, overrides));
}
destroy() {
this.clearPressTimer();
this.resetState();
super.destroy();
}
updateOptions(options) {
super.updateOptions(options);
this.duration = options.duration ?? this.duration;
this.maxDistance = options.maxDistance ?? this.maxDistance;
}
resetState() {
this.clearPressTimer();
this.isActive = false;
this.state = (0, _extends2.default)({}, this.state, {
startCentroid: null,
lastPosition: null,
timerId: null,
startTime: 0,
pressThresholdReached: false
});
}
/**
* Clear the press timer if it's active
*/
clearPressTimer() {
if (this.state.timerId !== null) {
clearTimeout(this.state.timerId);
this.state.timerId = null;
}
}
/**
* Handle pointer events for the press gesture
*/
handlePointerEvent(pointers, event) {
const pointersArray = Array.from(pointers.values());
// Check for our forceCancel event to handle interrupted gestures (from contextmenu, blur)
if (event.type === 'forceCancel') {
// Reset all active press gestures when we get a force reset event
this.cancelPress(event.target, pointersArray, event);
return;
}
// Find which element (if any) is being targeted
const targetElement = this.getTargetElement(event);
if (!targetElement) {
return;
}
// Check if this gesture should be prevented by active gestures
if (this.shouldPreventGesture(targetElement)) {
if (this.isActive) {
// If the gesture was active but now should be prevented, cancel it gracefully
this.cancelPress(targetElement, pointersArray, event);
}
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 (relevantPointers.length < this.minPointers || relevantPointers.length > this.maxPointers) {
if (this.isActive) {
// Cancel or end the gesture if it was active
this.cancelPress(targetElement, relevantPointers, event);
}
return;
}
switch (event.type) {
case 'pointerdown':
if (!this.isActive && !this.state.startCentroid) {
// Calculate and store the starting centroid
this.state.startCentroid = (0, _utils.calculateCentroid)(relevantPointers);
this.state.lastPosition = (0, _extends2.default)({}, this.state.startCentroid);
this.state.startTime = event.timeStamp;
this.isActive = true;
// Store the original target element
this.originalTarget = targetElement;
// Start the timer for press recognition
this.clearPressTimer(); // Clear any existing timer first
this.state.timerId = setTimeout(() => {
if (this.isActive && this.state.startCentroid) {
this.state.pressThresholdReached = true;
const lastPosition = this.state.lastPosition;
// Emit press start event
this.emitPressEvent(targetElement, 'start', relevantPointers, event, lastPosition);
this.emitPressEvent(targetElement, 'ongoing', relevantPointers, event, lastPosition);
}
}, this.duration);
}
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 press gesture
if (distance > this.maxDistance) {
this.cancelPress(targetElement, relevantPointers, event);
}
}
break;
case 'pointerup':
if (this.isActive) {
if (this.state.pressThresholdReached) {
// Complete the press gesture if we've held long enough
const position = this.state.lastPosition || this.state.startCentroid;
this.emitPressEvent(targetElement, 'end', relevantPointers, event, position);
}
// Reset state
this.resetState();
}
break;
case 'pointercancel':
case 'forceCancel':
// Cancel the gesture
this.cancelPress(targetElement, relevantPointers, event);
break;
default:
break;
}
}
/**
* Emit press-specific events with additional data
*/
emitPressEvent(element, phase, pointers, event, position) {
// Get list of active gestures
const activeGestures = this.gesturesRegistry.getActiveGestures(element);
// Calculate current duration of the press
const currentDuration = event.timeStamp - this.state.startTime;
// Create custom event data
const customEventData = {
gestureName: this.name,
centroid: position,
target: event.target,
srcEvent: event,
phase,
pointers,
timeStamp: event.timeStamp,
x: position.x,
y: position.y,
duration: currentDuration,
activeGestures,
customData: this.customData
};
// Event names to trigger
const eventName = (0, _utils.createEventName)(this.name, phase);
// Dispatch custom events on the element
const domEvent = new CustomEvent(eventName, {
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 press gesture
*/
cancelPress(element, pointers, event) {
if (this.isActive && this.state.pressThresholdReached) {
const position = this.state.lastPosition || this.state.startCentroid;
this.emitPressEvent(element ?? this.element, 'cancel', pointers, event, position);
this.emitPressEvent(element ?? this.element, 'end', pointers, event, position);
}
this.resetState();
}
}
exports.PressGesture = PressGesture;