UNPKG

react-native-gesture-handler

Version:

Declarative API exposing native platform touch and gesture system to React Native

377 lines (302 loc) 9.96 kB
import findNodeHandle from '../../findNodeHandle'; import { MouseButton } from '../../handlers/gestureHandlerCommon'; import { State } from '../../State'; import { tagMessage } from '../../utils'; import { SingleGestureName } from '../../v3/types'; import type IGestureHandler from '../handlers/IGestureHandler'; import type { SVGRef } from '../interfaces'; import { getEffectiveBoundingRect, isPointerInBounds, isRNSVGElement, } from '../utils'; import type EventManager from './EventManager'; import type { GestureHandlerDelegate, MeasureResult, } from './GestureHandlerDelegate'; import KeyboardEventManager from './KeyboardEventManager'; import PointerEventManager from './PointerEventManager'; import WheelEventManager from './WheelEventManager'; interface DefaultViewStyles { userSelect: string; touchAction: string; } export class GestureHandlerWebDelegate implements GestureHandlerDelegate<HTMLElement, IGestureHandler> { private isInitialized = false; private _view: HTMLElement | null = null; private gestureHandler!: IGestureHandler; private eventManagers: EventManager<unknown>[] = []; private defaultViewStyles: DefaultViewStyles = { userSelect: '', touchAction: '', }; private areContextMenuListenersAdded = false; private wasContextMenuEnabled = false; init(viewRef: number, handler: IGestureHandler): void { if (!viewRef) { throw new Error( `Cannot find HTML Element for handler ${handler.handlerTag}` ); } this.gestureHandler = handler; this.view = handler.usesNativeOrVirtualDetector() && !isRNSVGElement(viewRef as unknown as SVGRef) ? (viewRef as unknown as HTMLElement) : (findNodeHandle(viewRef) as unknown as HTMLElement); this.defaultViewStyles = { userSelect: this.view.style.userSelect, touchAction: this.view.style.touchAction, }; const shouldSendHoverEvents = handler.name === SingleGestureName.Hover; this.eventManagers.push( new PointerEventManager(this.view, shouldSendHoverEvents) ); this.eventManagers.push(new KeyboardEventManager(this.view)); this.eventManagers.push(new WheelEventManager(this.view)); this.eventManagers.forEach((manager) => this.gestureHandler.attachEventManager(manager) ); this.updateDOM(); this.isInitialized = true; } detach(): void { this.restoreDefaultViewStyles(); this.defaultViewStyles = { userSelect: '', touchAction: '', }; this.eventManagers.forEach((manager) => { manager.setEnabled(false); }); this.removeContextMenuListeners(); this._view = null; this.eventManagers = []; this.isInitialized = false; } restoreDefaultViewStyles(): void { this.ensureView(this.view); this.setViewStyle('userSelect', this.defaultViewStyles.userSelect); this.setViewStyle('webkitUserSelect', this.defaultViewStyles.userSelect); this.setViewStyle('touchAction', this.defaultViewStyles.touchAction); this.setViewStyle('WebkitTouchCallout', this.defaultViewStyles.touchAction); } updateDOM(): void { this.setUserSelect(); this.setTouchAction(); this.setContextMenu(); } isPointerInBounds({ x, y }: { x: number; y: number }): boolean { if (!this.view) { return false; } return isPointerInBounds(this.view, { x, y }); } measureView(): MeasureResult { if (!this.view) { throw new Error(tagMessage('Cannot measure a null view')); } const rect = getEffectiveBoundingRect(this.view); return { pageX: rect.left, pageY: rect.top, width: rect.width, height: rect.height, }; } absoluteToLocal( absoluteX: number, absoluteY: number ): { x: number; y: number } { if (!this.view) { throw new Error(tagMessage('Cannot convert coords on a null view')); } const rect = getEffectiveBoundingRect(this.view); const transform = getComputedStyle(this.view).transform; const matrix = transform && transform !== 'none' ? new DOMMatrix(transform) : new DOMMatrix(); // Zero out translation — it's already reflected in the bounding rect // center, so we only need to invert the rotation+scale part. matrix.e = 0; matrix.f = 0; const inverse = matrix.inverse(); // Offset from the visual center of the bounding rect const rectCenterX = rect.left + rect.width / 2; const rectCenterY = rect.top + rect.height / 2; const dx = absoluteX - rectCenterX; const dy = absoluteY - rectCenterY; // Apply inverse rotation+scale to get local-space offset from center const localOffset = inverse.transformPoint(new DOMPoint(dx, dy)); // Add back the local center (untransformed dimensions) const localCenterX = this.view.offsetWidth / 2; const localCenterY = this.view.offsetHeight / 2; return { x: localCenterX + localOffset.x, y: localCenterY + localOffset.y, }; } reset(): void { this.eventManagers.forEach((manager: EventManager<unknown>) => manager.resetManager() ); } tryResetCursor() { const activeCursor = this.gestureHandler.activeCursor; if ( activeCursor && activeCursor !== 'auto' && this.gestureHandler.state === State.ACTIVE && this.view ) { this.view.style.cursor = 'auto'; } } private shouldDisableContextMenu() { return ( (this.gestureHandler.enableContextMenu === undefined && this.gestureHandler.isButtonInConfig(MouseButton.RIGHT)) || this.gestureHandler.enableContextMenu === false ); } private addContextMenuListeners(): void { this.ensureView(this.view); if (this.areContextMenuListenersAdded) { return; } if (this.shouldDisableContextMenu()) { this.wasContextMenuEnabled = false; this.view.addEventListener('contextmenu', this.disableContextMenu); this.areContextMenuListenersAdded = true; } else if (this.gestureHandler.enableContextMenu) { this.wasContextMenuEnabled = true; this.view.addEventListener('contextmenu', this.enableContextMenu); this.areContextMenuListenersAdded = true; } } private removeContextMenuListeners(): void { if (!this.initialized || !this.areContextMenuListenersAdded) { return; } this.ensureView(this.view); if (!this.areContextMenuListenersAdded) { return; } if (!this.wasContextMenuEnabled) { this.view.removeEventListener('contextmenu', this.disableContextMenu); this.areContextMenuListenersAdded = false; } else { this.view.removeEventListener('contextmenu', this.enableContextMenu); this.areContextMenuListenersAdded = false; } } private disableContextMenu(this: void, e: MouseEvent): void { e.preventDefault(); } private enableContextMenu(this: void, e: MouseEvent): void { e.stopPropagation(); } private setUserSelect() { const userSelect = this.gestureHandler.userSelect; this.ensureView(this.view); const value = this.gestureHandler.enabled ? (userSelect ?? 'none') : this.defaultViewStyles.userSelect; this.setViewStyle('userSelect', value); this.setViewStyle('webkitUserSelect', value); } private setTouchAction() { const touchAction = this.gestureHandler.touchAction; this.ensureView(this.view); const value = this.gestureHandler.enabled ? (touchAction ?? 'none') : this.defaultViewStyles.touchAction; this.setViewStyle('touchAction', value); this.setViewStyle('WebkitTouchCallout', value); } private setContextMenu() { if (!this.gestureHandler.enabled) { this.removeContextMenuListeners(); return; } if (!this.wasContextMenuEnabled) { this.removeContextMenuListeners(); } this.addContextMenuListeners(); } onEnabledChange(): void { if (!this.isInitialized) { return; } this.updateDOM(); this.eventManagers.forEach((manager) => { manager.setEnabled(this.gestureHandler.enabled); }); } onBegin(): void { // no-op for now } onActivate(): void { this.ensureView(this.view); if ( (!this.view.style.cursor || this.view.style.cursor === 'auto') && this.gestureHandler.activeCursor ) { this.view.style.cursor = this.gestureHandler.activeCursor; } } onEnd(): void { this.tryResetCursor(); } onCancel(): void { this.tryResetCursor(); } onFail(): void { this.tryResetCursor(); } public destroy(): void { this.removeContextMenuListeners(); this.eventManagers.forEach((manager) => { manager.unregisterListeners(); }); this.isInitialized = false; } private setViewStyle( property: Extract<keyof CSSStyleDeclaration, string> | 'WebkitTouchCallout', value: string ): void { this.ensureView(this.view); const hasDisplayContents = this.view.style.display === 'contents' || getComputedStyle(this.view).display === 'contents'; if (hasDisplayContents) { for (const child of Array.from(this.view.children)) { if (child instanceof HTMLElement) { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access (child.style as any)[property] = value; } } } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access (this.view.style as any)[property] = value; } } private ensureView(view: any): asserts view is HTMLElement { if (!view) { throw new Error(tagMessage('Expected delegate view to be HTMLElement')); } } public get view(): HTMLElement | null { return this._view; } public set view(value: HTMLElement) { this._view = value; } get initialized(): boolean { return this.isInitialized; } }