react-native-gesture-handler
Version:
Declarative API exposing native platform touch and gesture system to React Native
258 lines (253 loc) • 8.53 kB
JavaScript
"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