@mui/x-internal-gestures
Version:
The core engine of GestureEvents, a modern and robust multi-pointer gesture detection library for JavaScript.
342 lines (310 loc) • 11.9 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PanGesture = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _PointerGesture = require("../PointerGesture");
var _utils = require("../utils");
/**
* PanGesture - Detects panning (dragging) movements
*
* This gesture tracks pointer dragging movements across elements, firing events when:
* - The drag movement begins and passes the threshold distance (start)
* - The drag movement continues (ongoing)
* - The drag movement ends (end)
*
* The gesture can be configured to recognize movement only in specific directions.
*/
/**
* The direction of movement for the pan gesture
* This type defines the detected directions based on the vertical and horizontal components
* The values can be 'up', 'down', 'left', 'right' or null if not applicable.
*
* The null values indicate that the gesture is not moving in that direction.
*/
/**
* Configuration options for PanGesture
* Extends PointerGestureOptions with direction constraints
*/
/**
* Event data specific to pan gesture events
* Contains information about movement distance, direction, and velocity
*/
/**
* Type definition for the CustomEvent created by PanGesture
*/
/**
* State tracking for the PanGesture
*/
/**
* PanGesture class for handling panning/dragging interactions
*
* This gesture detects when users drag across elements with one or more pointers,
* and dispatches directional movement events with delta and velocity information.
*/
class PanGesture extends _PointerGesture.PointerGesture {
constructor(options) {
super(options);
this.state = {
startPointers: new Map(),
startCentroid: null,
lastCentroid: null,
movementThresholdReached: false,
totalDeltaX: 0,
totalDeltaY: 0,
activeDeltaX: 0,
activeDeltaY: 0,
lastDirection: {
vertical: null,
horizontal: null,
mainAxis: null
},
lastDeltas: null
};
this.isSinglePhase = void 0;
this.eventType = void 0;
this.optionsType = void 0;
this.mutableOptionsType = void 0;
this.mutableStateType = void 0;
/**
* Movement threshold in pixels that must be exceeded before the gesture activates.
* Higher values reduce false positive gesture detection for small movements.
*/
this.threshold = void 0;
/**
* Allowed directions for the pan gesture
* Default allows all directions
*/
this.direction = void 0;
this.direction = options.direction || ['up', 'down', 'left', 'right'];
this.threshold = options.threshold || 0;
}
clone(overrides) {
return new PanGesture((0, _extends2.default)({
name: this.name,
preventDefault: this.preventDefault,
stopPropagation: this.stopPropagation,
threshold: this.threshold,
minPointers: this.minPointers,
maxPointers: this.maxPointers,
direction: [...this.direction],
requiredKeys: [...this.requiredKeys],
pointerMode: [...this.pointerMode],
preventIf: [...this.preventIf]
}, overrides));
}
destroy() {
this.resetState();
super.destroy();
}
updateOptions(options) {
super.updateOptions(options);
this.direction = options.direction || this.direction;
}
resetState() {
this.isActive = false;
this.state = (0, _extends2.default)({}, this.state, {
startPointers: new Map(),
startCentroid: null,
lastCentroid: null,
lastDeltas: null,
activeDeltaX: 0,
activeDeltaY: 0,
movementThresholdReached: false,
lastDirection: {
vertical: null,
horizontal: null,
mainAxis: null
}
});
}
/**
* Handle pointer events for the pan 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 pan gestures when we get a force reset event
this.cancel(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 the gesture was active but now should be prevented, cancel it gracefully
this.cancel(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) {
// Cancel or end the gesture if it was active
this.cancel(targetElement, relevantPointers, event);
return;
}
switch (event.type) {
case 'pointerdown':
if (!this.isActive && !this.state.startCentroid) {
// Store initial pointers
relevantPointers.forEach(pointer => {
this.state.startPointers.set(pointer.pointerId, pointer);
});
// Store the original target element
this.originalTarget = targetElement;
// Calculate and store the starting centroid
this.state.startCentroid = (0, _utils.calculateCentroid)(relevantPointers);
this.state.lastCentroid = (0, _extends2.default)({}, this.state.startCentroid);
}
break;
case 'pointermove':
if (this.state.startCentroid && relevantPointers.length >= this.minPointers) {
// Calculate current centroid
const currentCentroid = (0, _utils.calculateCentroid)(relevantPointers);
// Calculate delta from start
const distanceDeltaX = currentCentroid.x - this.state.startCentroid.x;
const distanceDeltaY = currentCentroid.y - this.state.startCentroid.y;
// Calculate movement distance
const distance = Math.sqrt(distanceDeltaX * distanceDeltaX + distanceDeltaY * distanceDeltaY);
// Determine movement direction
const moveDirection = (0, _utils.getDirection)(this.state.lastCentroid ?? this.state.startCentroid, currentCentroid);
// Calculate change in position since last move
const lastDeltaX = this.state.lastCentroid ? currentCentroid.x - this.state.lastCentroid.x : 0;
const lastDeltaY = this.state.lastCentroid ? currentCentroid.y - this.state.lastCentroid.y : 0;
// Check if movement passes the threshold and is in an allowed direction
if (!this.state.movementThresholdReached && distance >= this.threshold && (0, _utils.isDirectionAllowed)(moveDirection, this.direction)) {
this.state.movementThresholdReached = true;
this.isActive = true;
// Update total accumulated delta
this.state.lastDeltas = {
x: lastDeltaX,
y: lastDeltaY
};
this.state.totalDeltaX += lastDeltaX;
this.state.totalDeltaY += lastDeltaY;
this.state.activeDeltaX += lastDeltaX;
this.state.activeDeltaY += lastDeltaY;
// Emit start event
this.emitPanEvent(targetElement, 'start', relevantPointers, event, currentCentroid);
this.emitPanEvent(targetElement, 'ongoing', relevantPointers, event, currentCentroid);
}
// If we've already crossed the threshold, continue tracking
else if (this.state.movementThresholdReached && this.isActive) {
// Update total accumulated delta
this.state.lastDeltas = {
x: lastDeltaX,
y: lastDeltaY
};
this.state.totalDeltaX += lastDeltaX;
this.state.totalDeltaY += lastDeltaY;
this.state.activeDeltaX += lastDeltaX;
this.state.activeDeltaY += lastDeltaY;
// Emit ongoing event
this.emitPanEvent(targetElement, 'ongoing', relevantPointers, event, currentCentroid);
}
// Update last centroid
this.state.lastCentroid = currentCentroid;
this.state.lastDirection = moveDirection;
}
break;
case 'pointerup':
case 'pointercancel':
case 'forceCancel':
// If the gesture was active (threshold was reached), emit end event
if (this.isActive && this.state.movementThresholdReached) {
// If we have less than the minimum required pointers, end the gesture
if (relevantPointers.filter(p => p.type !== 'pointerup' && p.type !== 'pointercancel').length < this.minPointers) {
// End the gesture
const currentCentroid = this.state.lastCentroid || this.state.startCentroid;
if (event.type === 'pointercancel') {
this.emitPanEvent(targetElement, 'cancel', relevantPointers, event, currentCentroid);
}
this.emitPanEvent(targetElement, 'end', relevantPointers, event, currentCentroid);
this.resetState();
}
} else {
this.resetState();
}
break;
default:
break;
}
}
/**
* Emit pan-specific events with additional data
*/
emitPanEvent(element, phase, pointers, event, currentCentroid) {
if (!this.state.startCentroid) {
return;
}
const deltaX = this.state.lastDeltas?.x ?? 0;
const deltaY = this.state.lastDeltas?.y ?? 0;
// Calculate velocity - time difference in seconds
const firstPointer = this.state.startPointers.values().next().value;
const timeElapsed = firstPointer ? (event.timeStamp - firstPointer.timeStamp) / 1000 : 0;
const velocityX = timeElapsed > 0 ? deltaX / timeElapsed : 0;
const velocityY = timeElapsed > 0 ? deltaY / timeElapsed : 0;
const velocity = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
// Get list of active gestures
const activeGestures = this.gesturesRegistry.getActiveGestures(element);
// Create custom event data
const customEventData = {
gestureName: this.name,
initialCentroid: this.state.startCentroid,
centroid: currentCentroid,
target: event.target,
srcEvent: event,
phase,
pointers,
timeStamp: event.timeStamp,
deltaX,
deltaY,
direction: this.state.lastDirection,
velocityX,
velocityY,
velocity,
totalDeltaX: this.state.totalDeltaX,
totalDeltaY: this.state.totalDeltaY,
activeDeltaX: this.state.activeDeltaX,
activeDeltaY: this.state.activeDeltaY,
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 gesture
*/
cancel(element, pointers, event) {
if (this.isActive) {
const el = element ?? this.element;
this.emitPanEvent(el, 'cancel', pointers, event, this.state.lastCentroid);
this.emitPanEvent(el, 'end', pointers, event, this.state.lastCentroid);
}
this.resetState();
}
}
exports.PanGesture = PanGesture;