import React, { Component, HTMLAttributes, DragEvent, ElementType, MouseEvent } from 'react'; import PropTypes, { Requireable } from 'prop-types'; import { getSupportedEvents, coordX, coordY, touchEnabled, VKUITouchEvent, VKUITouchEventHander, } from '../../lib/touch'; import { HasRootRef, OldRef } from '../../types'; import { canUseDOM } from '../../lib/dom'; export interface TouchProps extends HTMLAttributes, HasRootRef { onStart?(outputEvent: TouchEvent): void; onStartX?(outputEvent: TouchEvent): void; onStartY?(outputEvent: TouchEvent): void; onMove?(outputEvent: TouchEvent): void; onMoveX?(outputEvent: TouchEvent): void; onMoveY?(outputEvent: TouchEvent): void; onEnd?(outputEvent: TouchEvent): void; onEndX?(outputEvent: TouchEvent): void; onEndY?(outputEvent: TouchEvent): void; useCapture?: boolean; Component?: ElementType; } export interface TouchContext { document: Requireable<{}>; } export interface Gesture { startX?: number; startY?: number; startT?: Date; isPressed?: boolean; isY?: boolean; isX?: boolean; isSlideX?: boolean; isSlideY?: boolean; isSlide?: boolean; shiftX?: number; shiftY?: number; shiftXAbs?: number; shiftYAbs?: number; } export interface TouchEvent extends Gesture { originalEvent: VKUITouchEvent; } export type TouchEventHandler = (e: TouchEvent) => void; export type ClickHandler = (e: MouseEvent) => void; export type DragHandler = (e: DragEvent) => void; const events = getSupportedEvents(); export default class Touch extends Component { constructor(props: TouchProps) { super(props); this.cancelClick = false; } cancelClick: boolean; gesture: Partial = {}; container: HTMLElement; static defaultProps: TouchProps = { Component: 'div', children: '', useCapture: false, }; static contextTypes: TouchContext = { document: PropTypes.object, }; get document() { return this.context.document || document; } componentDidMount() { if (canUseDOM) { this.container.addEventListener(events[0], this.onStart, { capture: this.props.useCapture, passive: false }); touchEnabled && this.subscribe(this.container); } } componentWillUnmount() { this.container.removeEventListener(events[0], this.onStart); touchEnabled && this.unsubscribe(this.container); } /** * Обработчик событий touchstart * * @param {Object} e Браузерное событие * @return {void} */ onStart: VKUITouchEventHander = (e: VKUITouchEvent) => { this.gesture = { startX: coordX(e), startY: coordY(e), startT: new Date(), isPressed: true, }; // Вызываем нужные колбеки из props const outputEvent = { ...this.gesture, originalEvent: e, }; if (this.props.onStart) { this.props.onStart(outputEvent); } if (this.props.onStartX) { this.props.onStartX(outputEvent); } if (this.props.onStartY) { this.props.onStartY(outputEvent); } !touchEnabled && this.subscribe(this.document); }; /** * Обработчик событий touchmove * * @param {Object} e Браузерное событие * @return {void} */ onMove: VKUITouchEventHander = (e: VKUITouchEvent) => { const { isPressed, isX, isY, startX, startY } = this.gesture; if (isPressed) { // смещения const shiftX = coordX(e) - startX; const shiftY = coordY(e) - startY; // абсолютные значения смещений const shiftXAbs = Math.abs(shiftX); const shiftYAbs = Math.abs(shiftY); // Если определяем мультитач, то прерываем жест if (!!e.touches && e.touches.length > 1) { return this.onEnd(e); } // если мы ещё не определились if (!isX && !isY) { let willBeX = shiftXAbs >= 5 && shiftXAbs > shiftYAbs; let willBeY = shiftYAbs >= 5 && shiftYAbs > shiftXAbs; let willBeSlidedX = willBeX && !!this.props.onMoveX || !!this.props.onMove; let willBeSlidedY = willBeY && !!this.props.onMoveY || !!this.props.onMove; this.gesture.isY = willBeY; this.gesture.isX = willBeX; this.gesture.isSlideX = willBeSlidedX; this.gesture.isSlideY = willBeSlidedY; this.gesture.isSlide = willBeSlidedX || willBeSlidedY; } if (this.gesture.isSlide) { this.gesture.shiftX = shiftX; this.gesture.shiftY = shiftY; this.gesture.shiftXAbs = shiftXAbs; this.gesture.shiftYAbs = shiftYAbs; // Вызываем нужные колбеки из props const outputEvent: TouchEvent = { ...this.gesture, originalEvent: e, }; if (this.props.onMove) { this.props.onMove(outputEvent); } if (this.gesture.isSlideX && this.props.onMoveX) { this.props.onMoveX(outputEvent); } if (this.gesture.isSlideY && this.props.onMoveY) { this.props.onMoveY(outputEvent); } } } }; /** * Обработчик событий touchend, touchcancel * * @param {Object} e Браузерное событие * @return {void} */ onEnd: VKUITouchEventHander = (e: VKUITouchEvent) => { const { isPressed, isSlide, isSlideX, isSlideY } = this.gesture; if (isPressed) { // Вызываем нужные колбеки из props const outputEvent: TouchEvent = { ...this.gesture, originalEvent: e, }; if (this.props.onEnd) { this.props.onEnd(outputEvent); } if (isSlideY && this.props.onEndY) { this.props.onEndY(outputEvent); } if (isSlideX && this.props.onEndX) { this.props.onEndX(outputEvent); } } const target = e.target as HTMLElement; // Если закончили жест на ссылке, выставляем флаг для отмены перехода this.cancelClick = target.tagName === 'A' && isSlide; this.gesture = {}; !touchEnabled && this.unsubscribe(this.document); }; subscribe(element: HTMLElement) { const listenerParams = { capture: this.props.useCapture, passive: false }; element.addEventListener(events[1], this.onMove, listenerParams); element.addEventListener(events[2], this.onEnd, listenerParams); element.addEventListener(events[3], this.onEnd, listenerParams); } unsubscribe(element: HTMLElement) { // Здесь нужен последний аргумент с такими же параметрами, потому что // некоторые браузеры на странных вендорах типа Meizu не удаляют обработчик. // https://github.com/VKCOM/VKUI/issues/444 const listenerParams = { capture: this.props.useCapture, passive: false }; element.removeEventListener(events[1], this.onMove, listenerParams); element.removeEventListener(events[2], this.onEnd, listenerParams); element.removeEventListener(events[3], this.onEnd, listenerParams); } /** * Обработчик событий dragstart * Отменяет нативное браузерное поведение для вложенных ссылок и изображений * * @param {Object} e Браузерное событие * @return {void} */ onDragStart: DragHandler = (e: DragEvent) => { const target = e.target as HTMLElement; if (target.tagName === 'A' || target.tagName === 'IMG') { e.preventDefault(); } }; /** * Обработчик клика по компоненту * Отменяет переход по вложенной ссылке, если был зафиксирован свайп * * @param {Object} e Браузерное событие * @return {void} */ onClick: ClickHandler = (e: MouseEvent) => { if (this.cancelClick) { this.cancelClick = false; e.preventDefault(); } this.props.onClick && this.props.onClick(e); }; getRef: OldRef = (container: HTMLElement) => { this.container = container; const getRootRef = this.props.getRootRef; if (getRootRef) { if (typeof getRootRef === 'function') { getRootRef(container); } else { getRootRef.current = container; } } }; render() { const { onStart, onStartX, onStartY, onMove, onMoveX, onMoveY, onEnd, onEndX, onEndY, useCapture, Component, getRootRef, ...restProps } = this.props; return ( {this.props.children} ); } }