UNPKG

@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
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(); } }