UNPKG

react-native-gesture-handler

Version:

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

258 lines (253 loc) 8.53 kB
"use strict"; import findNodeHandle from '../../findNodeHandle'; import { MouseButton } from '../../handlers/gestureHandlerCommon'; import { State } from '../../State'; import { tagMessage } from '../../utils'; import { SingleGestureName } from '../../v3/types'; import { getEffectiveBoundingRect, isPointerInBounds, isRNSVGElement } from '../utils'; import KeyboardEventManager from './KeyboardEventManager'; import PointerEventManager from './PointerEventManager'; import WheelEventManager from './WheelEventManager'; export class GestureHandlerWebDelegate { isInitialized = false; _view = null; eventManagers = []; defaultViewStyles = { userSelect: '', touchAction: '' }; areContextMenuListenersAdded = false; wasContextMenuEnabled = false; init(viewRef, handler) { if (!viewRef) { throw new Error(`Cannot find HTML Element for handler ${handler.handlerTag}`); } this.gestureHandler = handler; this.view = handler.usesNativeOrVirtualDetector() && !isRNSVGElement(viewRef) ? viewRef : findNodeHandle(viewRef); 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() { this.restoreDefaultViewStyles(); this.defaultViewStyles = { userSelect: '', touchAction: '' }; this.eventManagers.forEach(manager => { manager.setEnabled(false); }); this.removeContextMenuListeners(); this._view = null; this.eventManagers = []; this.isInitialized = false; } restoreDefaultViewStyles() { 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() { this.setUserSelect(); this.setTouchAction(); this.setContextMenu(); } isPointerInBounds({ x, y }) { if (!this.view) { return false; } return isPointerInBounds(this.view, { x, y }); } measureView() { 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, absoluteY) { 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() { this.eventManagers.forEach(manager => manager.resetManager()); } tryResetCursor() { const activeCursor = this.gestureHandler.activeCursor; if (activeCursor && activeCursor !== 'auto' && this.gestureHandler.state === State.ACTIVE && this.view) { this.view.style.cursor = 'auto'; } } shouldDisableContextMenu() { return this.gestureHandler.enableContextMenu === undefined && this.gestureHandler.isButtonInConfig(MouseButton.RIGHT) || this.gestureHandler.enableContextMenu === false; } addContextMenuListeners() { 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; } } removeContextMenuListeners() { 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; } } disableContextMenu(e) { e.preventDefault(); } enableContextMenu(e) { e.stopPropagation(); } 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); } 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); } setContextMenu() { if (!this.gestureHandler.enabled) { this.removeContextMenuListeners(); return; } if (!this.wasContextMenuEnabled) { this.removeContextMenuListeners(); } this.addContextMenuListeners(); } onEnabledChange() { if (!this.isInitialized) { return; } this.updateDOM(); this.eventManagers.forEach(manager => { manager.setEnabled(this.gestureHandler.enabled); }); } onBegin() { // no-op for now } onActivate() { 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() { this.tryResetCursor(); } onCancel() { this.tryResetCursor(); } onFail() { this.tryResetCursor(); } destroy() { this.removeContextMenuListeners(); this.eventManagers.forEach(manager => { manager.unregisterListeners(); }); this.isInitialized = false; } setViewStyle(property, value) { 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[property] = value; } } } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access this.view.style[property] = value; } } ensureView(view) { if (!view) { throw new Error(tagMessage('Expected delegate view to be HTMLElement')); } } get view() { return this._view; } set view(value) { this._view = value; } get initialized() { return this.isInitialized; } } //# sourceMappingURL=GestureHandlerWebDelegate.js.map