To avoid introducing extra DOM elements in render():
return React.Children.only(this.props.children);
And combine with, ReactDOM.findDOMNode()
to attach events to the element manually.
export const getPointRelativeToElement = (point, element) => { | |
const rect = element.getBoundingClientRect(); | |
return { | |
x: point.x - rect.left, | |
y: point.y - rect.top, | |
}; | |
}; | |
export const getDistanceBetweenPoints = (pointA, pointB) => ( | |
Math.sqrt(Math.pow(pointA.y - pointB.y, 2) + Math.pow(pointA.x - pointB.x, 2)) | |
); |
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import { getPointRelativeToElement, getDistanceBetweenPoints } from 'utilities/geometry'; | |
const THRESHOLD = 150; | |
const RANGE = 80; | |
const SCROLL_RANGE = 5; | |
const COMPLETE = 1; | |
const SPEED = 0.015; | |
const pointFromTouch = (touch) => ({ x: touch.clientX, y: touch.clientY }); | |
class LongPressGesture extends React.Component { | |
constructor() { | |
super(...arguments); | |
this.handleTouchStart = this.handleTouchStart.bind(this); | |
this.handleTouchMove = this.handleTouchMove.bind(this); | |
this.handleTouchEnd = this.handleTouchEnd.bind(this); | |
this.handleTick = this.handleTick.bind(this); | |
this.startPoint = null; | |
this.timer = null; | |
this.progress = 0; | |
this.startDate = null; | |
this.isRecognizing = false; | |
} | |
componentDidMount() { | |
this.el = ReactDOM.findDOMNode(this); | |
if (this.props.when) { | |
this.listen(); | |
} | |
} | |
componentWillUnmount() { | |
if (this.props.when) { | |
this.unlisten(); | |
} | |
} | |
componentWillReceiveProps(next) { | |
if (this.props.when !== next.when) { | |
if (next.when) { | |
this.listen(); | |
} else { | |
this.unlisten(); | |
} | |
} | |
} | |
listen() { | |
this.el.addEventListener('touchstart', this.handleTouchStart, false); | |
this.el.addEventListener('touchmove', this.handleTouchMove, false); | |
this.el.addEventListener('touchend', this.handleTouchEnd, false); | |
} | |
unlisten() { | |
this.el.removeEventListener('touchstart', this.handleTouchStart, false); | |
this.el.removeEventListener('touchmove', this.handleTouchMove, false); | |
this.el.removeEventListener('touchend', this.handleTouchEnd, false); | |
} | |
handleTouchStart(event) { | |
if (event.touches.length !== 1) return null; | |
this.startDate = Date.now(); | |
this.isTouching = true; | |
this.startTouchPoint = getPointRelativeToElement(pointFromTouch(event.touches[0]), this.el); | |
this.startOffsetPoint = { | |
x: window.scrollX, | |
y: window.scrollY, | |
}; | |
if (this.lastTouchPoint && getDistanceBetweenPoints(this.startTouchPoint, this.lastTouchPoint) > RANGE) { | |
this.props.onLongPressCancel(this.lastTouchPoint.x, this.lastTouchPoint.y); | |
this.reset(); | |
} | |
this.lastTouchPoint = this.startTouchPoint; | |
if (!this.timer) { | |
this.props.onLongPressStart(this.lastTouchPoint.x, this.lastTouchPoint.y); | |
this.timer = setInterval(this.handleTick, 1000 / 60); | |
} | |
} | |
handleTouchMove(event) { | |
if (event.touches.length !== 1) return null; | |
this.lastTouchPoint = getPointRelativeToElement(pointFromTouch(event.touches[0]), this.el); | |
} | |
handleTouchEnd(event) { | |
if (event.touches.length !== 0) return null; | |
this.isTouching = false; | |
} | |
handleTick() { | |
if (!this.isRecognizing && Date.now() > this.startDate + THRESHOLD) return this.isRecognizing = true; | |
const distance = getDistanceBetweenPoints(this.startTouchPoint, this.lastTouchPoint); | |
const scrollDistance = getDistanceBetweenPoints(this.startOffsetPoint, { x: window.scrollX, y: window.scrollY}); | |
if (this.isTouching && distance < RANGE && scrollDistance < SCROLL_RANGE ) { | |
this.tickUp(); | |
} else { | |
this.tickDown(); | |
} | |
} | |
tickUp() { | |
if (this.progress >= COMPLETE) { | |
this.props.onLongPressProgress(this.lastTouchPoint.x, this.lastTouchPoint.y, 1); | |
this.props.onLongPressEnd(this.lastTouchPoint.x, this.lastTouchPoint.y); | |
this.reset(); | |
} else { | |
this.progress += SPEED; | |
this.props.onLongPressProgress(this.lastTouchPoint.x, this.lastTouchPoint.y, this.progress); | |
} | |
} | |
tickDown() { | |
if (this.progress <= 0) { | |
this.props.onLongPressCancel(this.lastTouchPoint.x, this.lastTouchPoint.y); | |
this.reset(); | |
} else { | |
this.progress -= SPEED; | |
this.props.onLongPressProgress(this.lastTouchPoint.x, this.lastTouchPoint.y, this.progress); | |
} | |
} | |
reset() { | |
clearInterval(this.timer); | |
this.timer = null; | |
this.progress = 0; | |
this.isRecognizing = false; | |
this.startDate = null; | |
} | |
render() { | |
return React.Children.only(this.props.children); | |
} | |
} | |
LongPressGesture.propTypes = { | |
onLongPressStart: React.PropTypes.func.isRequired, | |
onLongPressProgress: React.PropTypes.func.isRequired, | |
onLongPressCancel: React.PropTypes.func.isRequired, | |
onLongPressEnd: React.PropTypes.func.isRequired, | |
}; | |
export default LongPressGesture; |
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import { getPointRelativeToElement, getDistanceBetweenPoints } from 'utilities/geometry'; | |
const THRESHOLD = 10; | |
const pointFromTouch = (touch) => ({ x: touch.clientX, y: touch.clientY }); | |
class PanGesture extends React.Component { | |
constructor() { | |
super(...arguments); | |
this.handleTouchStart = this.handleTouchStart.bind(this); | |
this.handleTouchMove = this.handleTouchMove.bind(this); | |
this.handleTouchEnd = this.handleTouchEnd.bind(this); | |
this.startPoint = null; | |
this.point = null; | |
this.isRecognizing = false; | |
} | |
componentDidMount() { | |
this.el = ReactDOM.findDOMNode(this); | |
this.el.addEventListener('touchstart', this.handleTouchStart, false); | |
this.el.addEventListener('touchmove', this.handleTouchMove, false); | |
this.el.addEventListener('touchend', this.handleTouchEnd, false); | |
} | |
componentWillUnmount() { | |
this.el.removeEventListener('touchstart', this.handleTouchStart, false); | |
this.el.removeEventListener('touchmove', this.handleTouchMove, false); | |
this.el.removeEventListener('touchend', this.handleTouchEnd, false); | |
} | |
handleTouchStart(event) { | |
if (event.touches.length !== 1) return null; | |
this.startPoint = this.point = pointFromTouch(event.touches[0]); | |
} | |
handleTouchMove(event) { | |
if (event.touches.length !== 1) return null; | |
this.point = getPointRelativeToElement(pointFromTouch(event.touches[0]), this.el); | |
if (this.isRecognizing) { | |
this.props.onPan(this.point.x, this.point.y); | |
} else { | |
if (getDistanceBetweenPoints(this.startPoint, this.point) >= THRESHOLD) { | |
this.isRecognizing = true; | |
} | |
} | |
} | |
handleTouchEnd() { | |
this.props.onPanEnd(this.point.x, this.point.y); | |
this.point = null; | |
this.startPoint = null; | |
this.isRecognizing = false; | |
} | |
render() { | |
return React.Children.only(this.props.children); | |
} | |
} | |
PanGesture.propTypes = { | |
onPanEnd: React.PropTypes.func.isRequired, | |
onPan: React.PropTypes.func.isRequired, | |
}; | |
export default PanGesture; |
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import { getPointRelativeToElement } from 'utilities/geometry'; | |
const THRESHOLD = 200; | |
class TapGesture extends React.Component { | |
constructor() { | |
super(...arguments); | |
this.handleTouchStart = this.handleTouchStart.bind(this); | |
this.handleTouchEnd = this.handleTouchEnd.bind(this); | |
this.point = null; | |
this.touchStartDate = null; | |
} | |
componentDidMount() { | |
this.el = ReactDOM.findDOMNode(this); | |
this.el.addEventListener('touchstart', this.handleTouchStart, false); | |
this.el.addEventListener('touchend', this.handleTouchEnd, false); | |
} | |
componentWillUnmount() { | |
this.el.removeEventListener('touchstart', this.handleTouchStart, false); | |
this.el.removeEventListener('touchend', this.handleTouchEnd, false); | |
} | |
handleTouchStart(event) { | |
if (event.touches.length !== 1) return null; | |
const touch = event.touches[0]; | |
this.point = getPointRelativeToElement({ | |
x: touch.clientX, | |
y: touch.clientY, | |
}, this.el); | |
this.touchStartDate = Date.now(); | |
} | |
handleTouchEnd() { | |
if (event.touches.length !== 0) return null; | |
if (Date.now() > (this.touchStartDate + THRESHOLD)) return null; | |
this.props.onTap(this.point.x, this.point.y); | |
} | |
render() { | |
return React.Children.only(this.props.children); | |
} | |
} | |
TapGesture.propTypes = { | |
onTap: React.PropTypes.func.isRequired, | |
}; | |
export default TapGesture; |