import React, { Component, ElementType, HTMLAttributes } from 'react';
import Touch, { TouchEvent, TouchEventHandler, TouchProps } from '../Touch/Touch';
import TouchRootContext from '../Touch/TouchContext';
import classNames from '../../lib/classNames';
import getClassName from '../../helpers/getClassName';
import { ANDROID } from '../../lib/platform';
import { getOffsetRect } from '../../lib/offset';
import { coordX, coordY, VKUITouchEvent, VKUITouchEventHander } from '../../lib/touch';
import { HasPlatform, HasRootRef, OldRef, RefWithCurrent } from '../../types';
import withPlatform from '../../hoc/withPlatform';
export interface TappableProps extends HTMLAttributes, HasRootRef, HasPlatform {
Component?: ElementType;
activeEffectDelay?: number;
disabled?: boolean;
stopPropagation?: boolean;
href?: string;
target?: string;
}
export interface TappableState {
clicks?: {
[index: string]: {
x: number;
y: number;
};
};
active?: boolean;
ts?: number;
}
export interface RootComponentProps extends TouchProps {
ref?: OldRef | RefWithCurrent;
}
export interface StorageItem {
activeTimeout: number;
timeout?: number;
stop(): void;
}
export interface Storage {
[index: string]: StorageItem;
}
export type GetStorage = () => StorageItem;
const ts = () => +Date.now();
export const ACTIVE_DELAY = 70;
export const ACTIVE_EFFECT_DELAY = 600;
let storage: Storage = {};
/*
* Очищает таймауты и хранилище для всех экземпляров компонента, кроме переданного
*/
function deactivateOtherInstances(exclude?: string) {
Object.keys(storage).filter((id: string) => id !== exclude).forEach((id: string) => {
clearTimeout(storage[id].activeTimeout);
clearTimeout(storage[id].timeout);
storage[id].stop();
delete storage[id];
});
}
class Tappable extends Component {
constructor(props: TappableProps) {
super(props);
this.id = Math.round(Math.random() * 1e8).toString(16);
this.state = {
clicks: {},
active: false,
ts: null,
};
this.isSlide = false;
}
id: string;
isSlide: boolean;
insideTouchRoot: boolean;
container: HTMLElement;
timeout: number;
wavesTimeout: number;
static defaultProps: TappableProps = {
Component: 'div',
role: 'button',
stopPropagation: false,
disabled: false,
activeEffectDelay: ACTIVE_EFFECT_DELAY,
};
/*
* Обрабатывает событие touchstart
*/
onStart: TouchEventHandler = ({ originalEvent }: TouchEvent) => {
!this.insideTouchRoot && this.props.stopPropagation && originalEvent.stopPropagation();
if (originalEvent.touches && originalEvent.touches.length > 1) {
deactivateOtherInstances();
return;
}
if (this.props.platform === ANDROID) {
this.onDown(originalEvent);
}
storage[this.id] = {
stop: this.stop,
activeTimeout: window.setTimeout(this.start, ACTIVE_DELAY),
};
};
/*
* Обрабатывает событие touchmove
*/
onMove: TouchEventHandler = ({ originalEvent, shiftXAbs, shiftYAbs }: TouchEvent) => {
!this.insideTouchRoot && this.props.stopPropagation && originalEvent.stopPropagation();
if (shiftXAbs > 20 || shiftYAbs > 20) {
this.isSlide = true;
this.stop();
}
};
/*
* Обрабатывает событие touchend
*/
onEnd: TouchEventHandler = ({ originalEvent }: TouchEvent) => {
!this.insideTouchRoot && this.props.stopPropagation && originalEvent.stopPropagation();
const now = ts();
if (originalEvent.touches && originalEvent.touches.length > 0) {
this.isSlide = false;
return;
}
if (this.state.active) {
if (now - this.state.ts >= 100) {
// Долгий тап, выключаем подсветку
this.stop();
} else {
// Короткий тап, оставляем подсветку
const timeout = window.setTimeout(this.stop, this.props.activeEffectDelay - now + this.state.ts);
const store = this.getStorage();
if (store) {
store.timeout = timeout;
}
}
} else if (!this.isSlide) {
// Очень короткий тап, включаем подсветку
this.start();
const timeout = window.setTimeout(this.stop, this.props.activeEffectDelay);
if (this.getStorage()) {
clearTimeout(this.getStorage().activeTimeout);
this.getStorage().timeout = timeout;
} else {
this.timeout = timeout;
}
}
this.isSlide = false;
};
/*
* Реализует эффект при тапе для Андроида
*/
onDown: VKUITouchEventHander = (e: VKUITouchEvent) => {
if (this.props.platform === ANDROID) {
const { top, left } = getOffsetRect(this.container);
const x = coordX(e) - left;
const y = coordY(e) - top;
const key = 'wave' + Date.now().toString();
this.setState((state: TappableState): TappableState => {
return {
clicks: {
...state.clicks,
[key]: {
x,
y,
},
},
};
});
this.wavesTimeout = window.setTimeout(() => {
this.setState((state: TappableState): TappableState => {
let clicks = { ...state.clicks };
delete clicks[key];
return { clicks };
});
}, 225);
}
};
/*
* Устанавливает активное выделение
*/
start: VoidFunction = () => {
if (!this.state.active) {
this.setState({
active: true,
ts: ts(),
});
}
deactivateOtherInstances(this.id);
};
/*
* Снимает активное выделение
*/
stop: VoidFunction = () => {
if (this.state.active) {
this.setState({
active: false,
ts: null,
});
}
if (this.getStorage()) {
clearTimeout(this.getStorage().activeTimeout);
delete storage[this.id];
}
};
/*
* Возвращает хранилище для экземпляра компонента
*/
getStorage: GetStorage = () => {
return storage[this.id];
};
/*
* Берет ref на DOM-ноду из экземпляра Touch
*/
getRef: OldRef = (container: HTMLElement) => {
this.container = container;
const getRootRef = this.props.getRootRef;
if (getRootRef) {
if (typeof getRootRef === 'function') {
getRootRef(container);
} else {
getRootRef.current = container;
}
}
};
componentWillUnmount() {
if (storage[this.id]) {
clearTimeout(storage[this.id].timeout);
clearTimeout(storage[this.id].activeTimeout);
delete storage[this.id];
}
clearTimeout(this.wavesTimeout);
}
render() {
const { clicks, active } = this.state;
const { children, className, Component, activeEffectDelay,
stopPropagation, getRootRef, platform, ...restProps } = this.props;
const classes = classNames(getClassName('Tappable', platform), className, {
'Tappable--active': active,
'Tappable--inactive': !active,
});
const RootComponent = !restProps.disabled ? Touch : Component;
let props: RootComponentProps = {};
if (!restProps.disabled) {
props.Component = Component;
/* eslint-disable */
props.onStart = this.onStart;
props.onMove = this.onMove;
props.onEnd = this.onEnd;
/* eslint-enable */
props.getRootRef = this.getRef;
} else {
props.ref = this.getRef;
}
return (
{(insideTouchRoot: boolean) => {
this.insideTouchRoot = insideTouchRoot;
return (
{platform === ANDROID &&
{Object.keys(clicks).map((k: string) => {
return (
);
})}
}
{children}
);
}}
);
}
}
export default withPlatform(Tappable);