@mui/x-internal-gestures
Version:
The core engine of GestureEvents, a modern and robust multi-pointer gesture detection library for JavaScript.
249 lines (226 loc) • 7.98 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
/**
* MoveGesture - Detects when a pointer enters, moves within, and leaves an element
*
* This gesture tracks pointer movements over an element, firing events when:
* - A pointer enters the element (start)
* - A pointer moves within the element (ongoing)
* - A pointer leaves the element (end)
*
* Unlike other gestures which often require specific actions to trigger,
* the move gesture fires automatically when pointers interact with the target element.
*
* This gesture only works with mouse pointers, not touch or pen.
*/
import { PointerGesture } from "../PointerGesture.js";
import { calculateCentroid, createEventName } from "../utils/index.js";
/**
* Configuration options for the MoveGesture
* Extends the base PointerGestureOptions
*/
/**
* Event data specific to move gesture events
* Includes the source pointer event and standard gesture data
*/
/**
* Type definition for the CustomEvent created by MoveGesture
*/
/**
* State tracking for the MoveGesture
*/
/**
* MoveGesture class for handling pointer movement over elements
*
* This gesture detects when pointers enter, move within, or leave target elements,
* and dispatches corresponding custom events.
*
* This gesture only works with hovering mouse pointers, not touch.
*/
export class MoveGesture extends PointerGesture {
constructor(options) {
super(options);
// Pre-bind handlers to this instance to maintain reference equality
this.state = {
lastPosition: 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;
// Store bound event handlers to properly remove them
this.handleElementEnterBound = void 0;
this.handleElementLeaveBound = void 0;
this.handleElementEnterBound = this.handleElementEnter.bind(this);
this.handleElementLeaveBound = this.handleElementLeave.bind(this);
this.threshold = options.threshold || 0;
}
clone(overrides) {
return new MoveGesture(_extends({
name: this.name,
preventDefault: this.preventDefault,
stopPropagation: this.stopPropagation,
threshold: this.threshold,
minPointers: this.minPointers,
maxPointers: this.maxPointers,
requiredKeys: [...this.requiredKeys],
pointerMode: [...this.pointerMode],
preventIf: [...this.preventIf]
}, overrides));
}
init(element, pointerManager, gestureRegistry, keyboardManager) {
super.init(element, pointerManager, gestureRegistry, keyboardManager);
// Add event listeners for entering and leaving elements
// These are different from pointer events handled by PointerManager
// @ts-expect-error, PointerEvent is correct.
this.element.addEventListener('pointerenter', this.handleElementEnterBound);
// @ts-expect-error, PointerEvent is correct.
this.element.addEventListener('pointerleave', this.handleElementLeaveBound);
}
destroy() {
// Remove event listeners using the same function references
// @ts-expect-error, PointerEvent is correct.
this.element.removeEventListener('pointerenter', this.handleElementEnterBound);
// @ts-expect-error, PointerEvent is correct.
this.element.removeEventListener('pointerleave', this.handleElementLeaveBound);
this.resetState();
super.destroy();
}
updateOptions(options) {
// Call parent method to handle common options
super.updateOptions(options);
}
resetState() {
this.isActive = false;
this.state = {
lastPosition: null
};
}
/**
* Handle pointer enter events for a specific element
* @param event The original pointer event
*/
handleElementEnter(event) {
if (event.pointerType !== 'mouse' && event.pointerType !== 'pen') {
return;
}
// Get pointers from the PointerManager
const pointers = this.pointerManager.getPointers() || new Map();
const pointersArray = Array.from(pointers.values());
// Only activate if we're within pointer count constraints
if (pointersArray.length >= this.minPointers && pointersArray.length <= this.maxPointers) {
this.isActive = true;
const currentPosition = {
x: event.clientX,
y: event.clientY
};
this.state.lastPosition = currentPosition;
// Emit start event
this.emitMoveEvent(this.element, 'start', pointersArray, event);
this.emitMoveEvent(this.element, 'ongoing', pointersArray, event);
}
}
/**
* Handle pointer leave events for a specific element
* @param event The original pointer event
*/
handleElementLeave(event) {
if (event.pointerType !== 'mouse' && event.pointerType !== 'pen') {
return;
}
if (!this.isActive) {
return;
}
// Get pointers from the PointerManager
const pointers = this.pointerManager.getPointers() || new Map();
const pointersArray = Array.from(pointers.values());
// Emit end event and reset state
this.emitMoveEvent(this.element, 'end', pointersArray, event);
this.resetState();
}
/**
* Handle pointer events for the move gesture (only handles move events now)
* @param pointers Map of active pointers
* @param event The original pointer event
*/
handlePointerEvent(pointers, event) {
if (event.type !== 'pointermove' || event.pointerType !== 'mouse' && event.pointerType !== 'pen') {
return;
}
if (this.preventDefault) {
event.preventDefault();
}
if (this.stopPropagation) {
event.stopPropagation();
}
const pointersArray = Array.from(pointers.values());
// Find which element (if any) is being targeted
const targetElement = this.getTargetElement(event);
if (!targetElement) {
return;
}
// Make sure we're still within pointer count constraints
if (pointersArray.length < this.minPointers || pointersArray.length > this.maxPointers) {
return;
}
if (this.shouldPreventGesture(targetElement)) {
if (!this.isActive) {
return;
}
this.resetState();
this.emitMoveEvent(targetElement, 'end', pointersArray, event);
return;
}
// Update position
const currentPosition = {
x: event.clientX,
y: event.clientY
};
this.state.lastPosition = currentPosition;
if (!this.isActive) {
this.isActive = true;
this.emitMoveEvent(targetElement, 'start', pointersArray, event);
}
// Emit ongoing event
this.emitMoveEvent(targetElement, 'ongoing', pointersArray, event);
}
/**
* Emit move-specific events
* @param element The DOM element the event is related to
* @param phase The current phase of the gesture (start, ongoing, end)
* @param pointers Array of active pointers
* @param event The original pointer event
*/
emitMoveEvent(element, phase, pointers, event) {
const currentPosition = this.state.lastPosition || calculateCentroid(pointers);
// Get list of active gestures
const activeGestures = this.gesturesRegistry.getActiveGestures(element);
// Create custom event data
const customEventData = {
gestureName: this.name,
centroid: currentPosition,
target: event.target,
srcEvent: event,
phase,
pointers,
timeStamp: event.timeStamp,
activeGestures,
customData: this.customData
};
// Event names to trigger
const eventName = 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);
}
}