@mui/x-internal-gestures
Version:
The core engine of GestureEvents, a modern and robust multi-pointer gesture detection library for JavaScript.
280 lines (257 loc) • 9.86 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PressAndDragGesture = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _PointerGesture = require("../PointerGesture");
var _utils = require("../utils");
var _PanGesture = require("./PanGesture");
var _PressGesture = require("./PressGesture");
/**
* PressAndDragGesture - Detects press followed by drag gestures using composition
*
* This gesture uses internal PressGesture and PanGesture instances to:
* 1. First, detect a press (hold for specified duration without movement)
* 2. Then, track drag movements from the press position
*
* The gesture fires events when:
* - A press is completed (press phase)
* - Drag movement begins and passes threshold (dragStart)
* - Drag movement continues (drag)
* - Drag movement ends (dragEnd)
* - The gesture is canceled at any point
*
* This is ideal for panning operations where you want to hold first, then drag.
*/
/**
* Configuration options for PressAndDragGesture
* Extends PointerGestureOptions with press and drag specific settings
*/
/**
* Event data specific to press and drag gesture events
* Contains information about the gesture state, position, and movement
*/
/**
* Type definition for the CustomEvent created by PressAndDragGesture
*/
/**
* Represents the current phase of the PressAndDrag gesture
*/
/**
* State tracking for the PressAndDragGesture
*/
/**
* PressAndDragGesture class for handling press followed by drag interactions
*
* This gesture composes press and drag logic patterns from PressGesture and PanGesture
* into a single coordinated gesture that handles press-then-drag interactions.
*/
class PressAndDragGesture extends _PointerGesture.PointerGesture {
state = {
phase: 'waitingForPress',
dragTimeoutId: null
};
/**
* Duration required for press recognition
*/
/**
* Maximum distance a pointer can move during press for it to still be considered a press
*/
/**
* Maximum time between press completion and drag start
*/
/**
* Movement threshold for drag activation
*/
/**
* Allowed directions for the drag gesture
*/
constructor(options) {
super(options);
this.pressDuration = options.pressDuration ?? 500;
this.pressMaxDistance = options.pressMaxDistance ?? 10;
this.dragTimeout = options.dragTimeout ?? 1000;
this.dragThreshold = options.dragThreshold ?? 0;
this.dragDirection = options.dragDirection || ['up', 'down', 'left', 'right'];
this.pressGesture = new _PressGesture.PressGesture({
name: `${this.name}-press`,
duration: this.pressDuration,
maxDistance: this.pressMaxDistance,
maxPointers: this.maxPointers,
pointerMode: this.pointerMode,
requiredKeys: this.requiredKeys,
preventIf: this.preventIf,
pointerOptions: structuredClone(this.pointerOptions)
});
this.panGesture = new _PanGesture.PanGesture({
name: `${this.name}-pan`,
minPointers: this.minPointers,
maxPointers: this.maxPointers,
threshold: this.dragThreshold,
direction: this.dragDirection,
pointerMode: this.pointerMode,
requiredKeys: this.requiredKeys,
preventIf: this.preventIf,
pointerOptions: structuredClone(this.pointerOptions)
});
}
clone(overrides) {
return new PressAndDragGesture((0, _extends2.default)({
name: this.name,
preventDefault: this.preventDefault,
stopPropagation: this.stopPropagation,
minPointers: this.minPointers,
maxPointers: this.maxPointers,
pressDuration: this.pressDuration,
pressMaxDistance: this.pressMaxDistance,
dragTimeout: this.dragTimeout,
dragThreshold: this.dragThreshold,
dragDirection: [...this.dragDirection],
requiredKeys: [...this.requiredKeys],
pointerMode: [...this.pointerMode],
preventIf: [...this.preventIf],
pointerOptions: structuredClone(this.pointerOptions)
}, overrides));
}
init(element, pointerManager, gestureRegistry, keyboardManager) {
super.init(element, pointerManager, gestureRegistry, keyboardManager);
this.pressGesture.init(element, pointerManager, gestureRegistry, keyboardManager);
this.panGesture.init(element, pointerManager, gestureRegistry, keyboardManager);
// Listen to press gesture events
this.element.addEventListener(this.pressGesture.name, this.pressHandler);
// Listen to pan gesture events for dragging
// @ts-expect-error, PointerEvent is correct.
this.element.addEventListener(`${this.panGesture.name}Start`, this.dragStartHandler);
// @ts-expect-error, PointerEvent is correct.
this.element.addEventListener(this.panGesture.name, this.dragMoveHandler);
// @ts-expect-error, PointerEvent is correct.
this.element.addEventListener(`${this.panGesture.name}End`, this.dragEndHandler);
// @ts-expect-error, PointerEvent is correct.
this.element.addEventListener(`${this.panGesture.name}Cancel`, this.dragEndHandler);
}
destroy() {
this.resetState();
this.pressGesture.destroy();
this.panGesture.destroy();
this.element.removeEventListener(this.pressGesture.name, this.pressHandler);
// @ts-expect-error, PointerEvent is correct.
this.element.removeEventListener(`${this.panGesture.name}Start`, this.dragStartHandler);
// @ts-expect-error, PointerEvent is correct.
this.element.removeEventListener(this.panGesture.name, this.dragMoveHandler);
// @ts-expect-error, PointerEvent is correct.
this.element.removeEventListener(`${this.panGesture.name}End`, this.dragEndHandler);
// @ts-expect-error, PointerEvent is correct.
this.element.removeEventListener(`${this.panGesture.name}Cancel`, this.dragEndHandler);
super.destroy();
}
updateOptions(options) {
super.updateOptions(options);
this.pressDuration = options.pressDuration ?? this.pressDuration;
this.pressMaxDistance = options.pressMaxDistance ?? this.pressMaxDistance;
this.dragTimeout = options.dragTimeout ?? this.dragTimeout;
this.dragThreshold = options.dragThreshold ?? this.dragThreshold;
this.dragDirection = options.dragDirection || this.dragDirection;
// Update internal gesture options
this.element.dispatchEvent(new CustomEvent(`${this.panGesture.name}ChangeOptions`, {
detail: {
minPointers: this.minPointers,
maxPointers: this.maxPointers,
threshold: this.dragThreshold,
direction: this.dragDirection,
pointerMode: this.pointerMode,
requiredKeys: this.requiredKeys,
preventIf: this.preventIf,
pointerOptions: structuredClone(this.pointerOptions)
}
}));
this.element.dispatchEvent(new CustomEvent(`${this.pressGesture.name}ChangeOptions`, {
detail: {
duration: this.pressDuration,
maxDistance: this.pressMaxDistance,
maxPointers: this.maxPointers,
pointerMode: this.pointerMode,
requiredKeys: this.requiredKeys,
preventIf: this.preventIf,
pointerOptions: structuredClone(this.pointerOptions)
}
}));
}
resetState() {
if (this.state.dragTimeoutId !== null) {
clearTimeout(this.state.dragTimeoutId);
}
this.restoreTouchAction();
this.isActive = false;
this.state = {
phase: 'waitingForPress',
dragTimeoutId: null
};
}
/**
* This can be empty because the PressAndDragGesture relies on PressGesture and PanGesture to handle pointer events
* The internal gestures will manage their own state and events, while this class coordinates between them
*/
handlePointerEvent() {}
pressHandler = () => {
if (this.state.phase !== 'waitingForPress') {
return;
}
this.state.phase = 'pressDetected';
this.setTouchAction();
// Start timeout to wait for drag start
this.state.dragTimeoutId = setTimeout(() => {
// Timeout expired, reset gesture
this.resetState();
}, this.dragTimeout);
};
dragStartHandler = event => {
if (this.state.phase !== 'pressDetected') {
return;
}
// Clear the drag timeout as drag has started
if (this.state.dragTimeoutId !== null) {
clearTimeout(this.state.dragTimeoutId);
this.state.dragTimeoutId = null;
}
// Restore touch action since we're now dragging
this.restoreTouchAction();
this.state.phase = 'dragging';
this.isActive = true;
// Fire start event
this.element.dispatchEvent(new CustomEvent((0, _utils.createEventName)(this.name, event.detail.phase), event));
};
dragMoveHandler = event => {
if (this.state.phase !== 'dragging') {
return;
}
// Fire move event
this.element.dispatchEvent(new CustomEvent((0, _utils.createEventName)(this.name, event.detail.phase), event));
};
dragEndHandler = event => {
if (this.state.phase !== 'dragging') {
return;
}
this.resetState();
// Fire end event
this.element.dispatchEvent(new CustomEvent((0, _utils.createEventName)(this.name, event.detail.phase), event));
};
setTouchAction() {
this.element.addEventListener('touchstart', _utils.preventDefault, {
passive: false
});
this.element.addEventListener('touchmove', _utils.preventDefault, {
passive: false
});
this.element.addEventListener('touchend', _utils.preventDefault, {
passive: false
});
}
restoreTouchAction() {
this.element.removeEventListener('touchstart', _utils.preventDefault);
this.element.removeEventListener('touchmove', _utils.preventDefault);
this.element.removeEventListener('touchend', _utils.preventDefault);
}
}
exports.PressAndDragGesture = PressAndDragGesture;