@mui/x-internal-gestures
Version:
The core engine of GestureEvents, a modern and robust multi-pointer gesture detection library for JavaScript.
277 lines (250 loc) • 9.45 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
/**
* PointerManager - Centralized manager for pointer events in the gesture recognition system
*
* This singleton class abstracts the complexity of working with pointer events by:
* 1. Capturing and tracking all active pointers (touch, mouse, pen)
* 2. Normalizing pointer data into a consistent format
* 3. Managing pointer capture for proper tracking across elements
* 4. Distributing events to registered gesture recognizers
*/
/**
* Normalized representation of a pointer, containing all relevant information
* from the original PointerEvent plus additional tracking data.
*
* This data structure encapsulates everything gesture recognizers need to know
* about a pointer's current state.
*/
/**
* Configuration options for initializing the PointerManager.
*/
/**
* Manager for handling pointer events across the application.
*
* PointerManager serves as the foundational layer for gesture recognition,
* providing a centralized system for tracking active pointers and distributing
* pointer events to gesture recognizers.
*
* It normalizes browser pointer events into a consistent format and simplifies
* multi-touch handling by managing pointer capture and tracking multiple
* simultaneous pointers.
*/
export class PointerManager {
/** Root element where pointer events are captured */
/** CSS touch-action property value applied to the root element */
/** Whether to use passive event listeners */
/** Whether to prevent interrupt events like blur or contextmenu */
preventEventInterruption = true;
/** Map of all currently active pointers by their pointerId */
pointers = (() => new Map())();
/** Set of registered gesture handlers that receive pointer events */
gestureHandlers = (() => new Set())();
constructor(options) {
this.root =
// User provided root element
options.root ??
// Fallback to document root or body, this fixes shadow DOM scenarios
document.getRootNode({
composed: true
}) ??
// Fallback to document body, for some testing environments
document.body;
this.touchAction = options.touchAction || 'auto';
this.passive = options.passive ?? false;
this.preventEventInterruption = options.preventEventInterruption ?? true;
this.setupEventListeners();
}
/**
* Register a handler function to receive pointer events.
*
* The handler will be called whenever pointer events occur within the root element.
* It receives the current map of all active pointers and the original event.
*
* @param {Function} handler - Function to receive pointer events and current pointer state
* @returns {Function} An unregister function that removes this handler when called
*/
registerGestureHandler(handler) {
this.gestureHandlers.add(handler);
// Return unregister function
return () => {
this.gestureHandlers.delete(handler);
};
}
/**
* Get a copy of the current active pointers map.
*
* Returns a new Map containing all currently active pointers.
* Modifying the returned map will not affect the internal pointers state.
*
* @returns A new Map containing all active pointers
*/
getPointers() {
return new Map(this.pointers);
}
/**
* Set up event listeners for pointer events on the root element.
*
* This method attaches all necessary event listeners and configures
* the CSS touch-action property on the root element.
*/
setupEventListeners() {
// Set touch-action CSS property
if (this.touchAction !== 'auto') {
this.root.style.touchAction = this.touchAction;
}
// Add event listeners
this.root.addEventListener('pointerdown', this.handlePointerEvent, {
passive: this.passive
});
this.root.addEventListener('pointermove', this.handlePointerEvent, {
passive: this.passive
});
this.root.addEventListener('pointerup', this.handlePointerEvent, {
passive: this.passive
});
this.root.addEventListener('pointercancel', this.handlePointerEvent, {
passive: this.passive
});
// @ts-expect-error, forceCancel is not a standard event, but used for custom handling
this.root.addEventListener('forceCancel', this.handlePointerEvent, {
passive: this.passive
});
// Add blur and contextmenu event listeners to interrupt all gestures
this.root.addEventListener('blur', this.handleInterruptEvents);
this.root.addEventListener('contextmenu', this.handleInterruptEvents);
}
/**
* Handle events that should interrupt all gestures.
* This clears all active pointers and notifies handlers with a pointercancel-like event.
*
* @param event - The event that triggered the interruption (blur or contextmenu)
*/
handleInterruptEvents = event => {
if (this.preventEventInterruption && 'pointerType' in event && event.pointerType === 'touch') {
event.preventDefault();
return;
}
// Create a synthetic pointer cancel event
const cancelEvent = new PointerEvent('forceCancel', {
bubbles: false,
cancelable: false
});
const firstPointer = this.pointers.values().next().value;
if (this.pointers.size > 0 && firstPointer) {
// If there are active pointers, use the first one as a template for coordinates
// Update the synthetic event with the pointer's coordinates
Object.defineProperties(cancelEvent, {
clientX: {
value: firstPointer.clientX
},
clientY: {
value: firstPointer.clientY
},
pointerId: {
value: firstPointer.pointerId
},
pointerType: {
value: firstPointer.pointerType
}
});
// Force update of all pointers to have type 'forceCancel'
for (const [pointerId, pointer] of this.pointers.entries()) {
const updatedPointer = _extends({}, pointer, {
type: 'forceCancel'
});
this.pointers.set(pointerId, updatedPointer);
}
}
// Notify all handlers about the interruption
this.notifyHandlers(cancelEvent);
// Clear all pointers
this.pointers.clear();
};
/**
* Event handler for all pointer events.
*
* This method:
* 1. Updates the internal pointers map based on the event type
* 2. Manages pointer capture for tracking pointers outside the root element
* 3. Notifies all registered handlers with the current state
*
* @param event - The original pointer event from the browser
*/
handlePointerEvent = event => {
const {
type,
pointerId
} = event;
// Create or update pointer data
if (type === 'pointerdown' || type === 'pointermove') {
this.pointers.set(pointerId, this.createPointerData(event));
}
// Remove pointer data on up or cancel
else if (type === 'pointerup' || type === 'pointercancel' || type === 'forceCancel') {
// Update one last time before removing
this.pointers.set(pointerId, this.createPointerData(event));
// Notify handlers with current state
this.notifyHandlers(event);
// Then remove the pointer
this.pointers.delete(pointerId);
return;
}
this.notifyHandlers(event);
};
/**
* Notify all registered gesture handlers about a pointer event.
*
* Each handler receives the current map of active pointers and the original event.
*
* @param event - The original pointer event that triggered this notification
*/
notifyHandlers(event) {
this.gestureHandlers.forEach(handler => handler(this.pointers, event));
}
/**
* Create a normalized PointerData object from a browser PointerEvent.
*
* This method extracts all relevant information from the original event
* and formats it in a consistent way for gesture recognizers to use.
*
* @param event - The original browser pointer event
* @returns A new PointerData object representing this pointer
*/
createPointerData(event) {
return {
pointerId: event.pointerId,
clientX: event.clientX,
clientY: event.clientY,
pageX: event.pageX,
pageY: event.pageY,
target: event.target,
timeStamp: event.timeStamp,
type: event.type,
isPrimary: event.isPrimary,
pressure: event.pressure,
width: event.width,
height: event.height,
pointerType: event.pointerType,
srcEvent: event
};
}
/**
* Clean up all event listeners and reset the PointerManager state.
*
* This method should be called when the PointerManager is no longer needed
* to prevent memory leaks. It removes all event listeners, clears the
* internal state, and resets the singleton instance.
*/
destroy() {
this.root.removeEventListener('pointerdown', this.handlePointerEvent);
this.root.removeEventListener('pointermove', this.handlePointerEvent);
this.root.removeEventListener('pointerup', this.handlePointerEvent);
this.root.removeEventListener('pointercancel', this.handlePointerEvent);
// @ts-expect-error, forceCancel is not a standard event, but used for custom handling
this.root.removeEventListener('forceCancel', this.handlePointerEvent);
this.root.removeEventListener('blur', this.handleInterruptEvents);
this.root.removeEventListener('contextmenu', this.handleInterruptEvents);
this.pointers.clear();
this.gestureHandlers.clear();
}
}