react-tappable
Version:
Touch / Tappable Event Handling Component for React
376 lines (333 loc) • 11.3 kB
JavaScript
'use strict';
var PropTypes = require('prop-types');
var React = require('react');
var ReactDOM = require('react-dom');
var SPACE_KEY = 32;
var ENTER_KEY = 13;
function getTouchProps(touch) {
if (!touch) return {};
return {
pageX: touch.pageX,
pageY: touch.pageY,
clientX: touch.clientX,
clientY: touch.clientY
};
}
var Mixin = {
propTypes: {
moveThreshold: PropTypes.number, // pixels to move before cancelling tap
moveXThreshold: PropTypes.number, // pixels on the x axis to move before cancelling tap (overrides moveThreshold)
moveYThreshold: PropTypes.number, // pixels on the y axis to move before cancelling tap (overrides moveThreshold)
allowReactivation: PropTypes.bool, // after moving outside of the moveThreshold will you allow
// reactivation by moving back within the moveThreshold?
activeDelay: PropTypes.number, // ms to wait before adding the `-active` class
pressDelay: PropTypes.number, // ms to wait before detecting a press
pressMoveThreshold: PropTypes.number, // pixels to move before cancelling press
preventDefault: PropTypes.bool, // whether to preventDefault on all events
stopPropagation: PropTypes.bool, // whether to stopPropagation on all events
onTap: PropTypes.func, // fires when a tap is detected
onPress: PropTypes.func, // fires when a press is detected
onTouchStart: PropTypes.func, // pass-through touch event
onTouchMove: PropTypes.func, // pass-through touch event
onTouchEnd: PropTypes.func, // pass-through touch event
onMouseDown: PropTypes.func, // pass-through mouse event
onMouseUp: PropTypes.func, // pass-through mouse event
onMouseMove: PropTypes.func, // pass-through mouse event
onMouseOut: PropTypes.func, // pass-through mouse event
onKeyDown: PropTypes.func, // pass-through key event
onKeyUp: PropTypes.func // pass-through key event
},
getDefaultProps: function getDefaultProps() {
return {
activeDelay: 0,
allowReactivation: true,
moveThreshold: 100,
pressDelay: 1000,
pressMoveThreshold: 5
};
},
getInitialState: function getInitialState() {
return {
isActive: false,
touchActive: false,
pinchActive: false
};
},
componentDidMount: function componentDidMount() {
this.isMounted = true;
},
componentWillUnmount: function componentWillUnmount() {
this.isMounted = false;
this.cleanupScrollDetection();
this.cancelPressDetection();
this.clearActiveTimeout();
},
componentWillUpdate: function componentWillUpdate(nextProps, nextState) {
if (this.state.isActive && !nextState.isActive) {
this.props.onDeactivate && this.props.onDeactivate();
} else if (!this.state.isActive && nextState.isActive) {
this.props.onReactivate && this.props.onReactivate();
}
},
processEvent: function processEvent(event) {
if (this.props.preventDefault) event.preventDefault();
if (this.props.stopPropagation) event.stopPropagation();
},
onTouchStart: function onTouchStart(event) {
if (this.props.onTouchStart && this.props.onTouchStart(event) === false) return;
this.processEvent(event);
window._blockMouseEvents = true;
if (event.touches.length === 1) {
this._initialTouch = this._lastTouch = getTouchProps(event.touches[0]);
this.initScrollDetection();
this.initPressDetection(event, this.endTouch);
this.initTouchmoveDetection();
if (this.props.activeDelay > 0) {
this._activeTimeout = setTimeout(this.makeActive, this.props.activeDelay);
} else {
this.makeActive();
}
} else if (this.onPinchStart && (this.props.onPinchStart || this.props.onPinchMove || this.props.onPinchEnd) && event.touches.length === 2) {
this.onPinchStart(event);
}
},
makeActive: function makeActive() {
if (!this.isMounted) return;
this.clearActiveTimeout();
this.setState({
isActive: true
});
},
clearActiveTimeout: function clearActiveTimeout() {
clearTimeout(this._activeTimeout);
this._activeTimeout = false;
},
initScrollDetection: function initScrollDetection() {
this._scrollPos = { top: 0, left: 0 };
this._scrollParents = [];
this._scrollParentPos = [];
var node = ReactDOM.findDOMNode(this);
while (node) {
if (node.scrollHeight > node.offsetHeight || node.scrollWidth > node.offsetWidth) {
this._scrollParents.push(node);
this._scrollParentPos.push(node.scrollTop + node.scrollLeft);
this._scrollPos.top += node.scrollTop;
this._scrollPos.left += node.scrollLeft;
}
node = node.parentNode;
}
},
initTouchmoveDetection: function initTouchmoveDetection() {
this._touchmoveTriggeredTimes = 0;
},
cancelTouchmoveDetection: function cancelTouchmoveDetection() {
if (this._touchmoveDetectionTimeout) {
clearTimeout(this._touchmoveDetectionTimeout);
this._touchmoveDetectionTimeout = null;
this._touchmoveTriggeredTimes = 0;
}
},
calculateMovement: function calculateMovement(touch) {
return {
x: Math.abs(touch.clientX - this._initialTouch.clientX),
y: Math.abs(touch.clientY - this._initialTouch.clientY)
};
},
detectScroll: function detectScroll() {
var currentScrollPos = { top: 0, left: 0 };
for (var i = 0; i < this._scrollParents.length; i++) {
currentScrollPos.top += this._scrollParents[i].scrollTop;
currentScrollPos.left += this._scrollParents[i].scrollLeft;
}
return !(currentScrollPos.top === this._scrollPos.top && currentScrollPos.left === this._scrollPos.left);
},
cleanupScrollDetection: function cleanupScrollDetection() {
this._scrollParents = undefined;
this._scrollPos = undefined;
},
initPressDetection: function initPressDetection(event, callback) {
if (!this.props.onPress) return;
// SyntheticEvent objects are pooled, so persist the event so it can be referenced asynchronously
event.persist();
this._pressTimeout = setTimeout(function () {
this.props.onPress(event);
callback();
}.bind(this), this.props.pressDelay);
},
cancelPressDetection: function cancelPressDetection() {
clearTimeout(this._pressTimeout);
},
onTouchMove: function onTouchMove(event) {
if (this._initialTouch) {
this.processEvent(event);
if (this.detectScroll()) {
return this.endTouch(event);
} else {
if (this._touchmoveTriggeredTimes++ === 0) {
this._touchmoveDetectionTimeout = setTimeout(function () {
if (this._touchmoveTriggeredTimes === 1) {
this.endTouch(event);
}
}.bind(this), 64);
}
}
this.props.onTouchMove && this.props.onTouchMove(event);
this._lastTouch = getTouchProps(event.touches[0]);
var movement = this.calculateMovement(this._lastTouch);
if (movement.x > this.props.pressMoveThreshold || movement.y > this.props.pressMoveThreshold) {
this.cancelPressDetection();
}
if (movement.x > (this.props.moveXThreshold || this.props.moveThreshold) || movement.y > (this.props.moveYThreshold || this.props.moveThreshold)) {
if (this.state.isActive) {
if (this.props.allowReactivation) {
this.setState({
isActive: false
});
} else {
return this.endTouch(event);
}
} else if (this._activeTimeout) {
this.clearActiveTimeout();
}
} else {
if (!this.state.isActive && !this._activeTimeout) {
this.setState({
isActive: true
});
}
}
} else if (this._initialPinch && event.touches.length === 2 && this.onPinchMove) {
this.onPinchMove(event);
event.preventDefault();
}
},
onTouchEnd: function onTouchEnd(event) {
var _this = this;
if (this._initialTouch) {
this.processEvent(event);
var afterEndTouch;
var movement = this.calculateMovement(this._lastTouch);
if (movement.x <= (this.props.moveXThreshold || this.props.moveThreshold) && movement.y <= (this.props.moveYThreshold || this.props.moveThreshold) && this.props.onTap) {
event.preventDefault();
afterEndTouch = function afterEndTouch() {
var finalParentScrollPos = _this._scrollParents.map(function (node) {
return node.scrollTop + node.scrollLeft;
});
var stoppedMomentumScroll = _this._scrollParentPos.some(function (end, i) {
return end !== finalParentScrollPos[i];
});
if (!stoppedMomentumScroll) {
_this.props.onTap(event);
}
};
}
this.endTouch(event, afterEndTouch);
} else if (this.onPinchEnd && this._initialPinch && event.touches.length + event.changedTouches.length === 2) {
this.onPinchEnd(event);
event.preventDefault();
}
},
endTouch: function endTouch(event, callback) {
this.cancelTouchmoveDetection();
this.cancelPressDetection();
this.clearActiveTimeout();
if (event && this.props.onTouchEnd) {
this.props.onTouchEnd(event);
}
this._initialTouch = null;
this._lastTouch = null;
if (callback) {
callback();
}
if (this.state.isActive) {
this.setState({
isActive: false
});
}
},
onMouseDown: function onMouseDown(event) {
if (window._blockMouseEvents) {
window._blockMouseEvents = false;
return;
}
if (this.props.onMouseDown && this.props.onMouseDown(event) === false) return;
this.processEvent(event);
this.initPressDetection(event, this.endMouseEvent);
this._mouseDown = true;
this.setState({
isActive: true
});
},
onMouseMove: function onMouseMove(event) {
if (window._blockMouseEvents || !this._mouseDown) return;
this.processEvent(event);
this.props.onMouseMove && this.props.onMouseMove(event);
},
onMouseUp: function onMouseUp(event) {
if (window._blockMouseEvents || !this._mouseDown) return;
this.processEvent(event);
this.props.onMouseUp && this.props.onMouseUp(event);
this.props.onTap && this.props.onTap(event);
this.endMouseEvent();
},
onMouseOut: function onMouseOut(event) {
if (window._blockMouseEvents || !this._mouseDown) return;
this.processEvent(event);
this.props.onMouseOut && this.props.onMouseOut(event);
this.endMouseEvent();
},
endMouseEvent: function endMouseEvent() {
this.cancelPressDetection();
this._mouseDown = false;
this.setState({
isActive: false
});
},
onKeyUp: function onKeyUp(event) {
if (!this._keyDown) return;
this.processEvent(event);
this.props.onKeyUp && this.props.onKeyUp(event);
this.props.onTap && this.props.onTap(event);
this._keyDown = false;
this.cancelPressDetection();
this.setState({
isActive: false
});
},
onKeyDown: function onKeyDown(event) {
if (this.props.onKeyDown && this.props.onKeyDown(event) === false) return;
if (event.which !== SPACE_KEY && event.which !== ENTER_KEY) return;
if (this._keyDown) return;
this.initPressDetection(event, this.endKeyEvent);
this.processEvent(event);
this._keyDown = true;
this.setState({
isActive: true
});
},
endKeyEvent: function endKeyEvent() {
this.cancelPressDetection();
this._keyDown = false;
this.setState({
isActive: false
});
},
cancelTap: function cancelTap() {
this.endTouch();
this._mouseDown = false;
},
handlers: function handlers() {
return {
onTouchStart: this.onTouchStart,
onTouchMove: this.onTouchMove,
onTouchEnd: this.onTouchEnd,
onMouseDown: this.onMouseDown,
onMouseUp: this.onMouseUp,
onMouseMove: this.onMouseMove,
onMouseOut: this.onMouseOut,
onKeyDown: this.onKeyDown,
onKeyUp: this.onKeyUp
};
}
};
module.exports = Mixin;