Skip to content

Instantly share code, notes, and snippets.

@iammerrick
Created August 24, 2016 22:47
Show Gist options
  • Save iammerrick/1d1dbc4ff3291bf742fbcea0a1212034 to your computer and use it in GitHub Desktop.
Save iammerrick/1d1dbc4ff3291bf742fbcea0a1212034 to your computer and use it in GitHub Desktop.
Event Augmentation With React

Augmenting React's Events with Components

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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment