rc-touchable
Version:
React Touchable Component
650 lines (630 loc) • 26.4 kB
JavaScript
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
// inspired by react-native
import React from 'react';
import ReactDOM from 'react-dom';
import PressEvent, { shouldFirePress } from './PressEvent';
function keyMirror(obj) {
Object.keys(obj).forEach(function (k) {
return obj[k] = k;
});
return obj;
}
function copy(from, list) {
var to = {};
list.forEach(function (k) {
to[k] = from[k];
});
return to;
}
function extractSingleTouch(_nativeEvent) {
var nativeEvent = _nativeEvent;
if (nativeEvent.nativeEvent) {
nativeEvent = nativeEvent.nativeEvent;
}
var touches = nativeEvent.touches;
var changedTouches = nativeEvent.changedTouches;
var hasTouches = touches && touches.length > 0;
var hasChangedTouches = changedTouches && changedTouches.length > 0;
return !hasTouches && hasChangedTouches ? changedTouches[0] : hasTouches ? touches[0] : nativeEvent;
}
/**
* Touchable states.
*/
var States = keyMirror({
NOT_RESPONDER: null,
RESPONDER_INACTIVE_PRESS_IN: null,
RESPONDER_INACTIVE_PRESS_OUT: null,
RESPONDER_ACTIVE_PRESS_IN: null,
RESPONDER_ACTIVE_PRESS_OUT: null,
RESPONDER_ACTIVE_LONG_PRESS_IN: null,
RESPONDER_ACTIVE_LONG_PRESS_OUT: null,
ERROR: null
});
/**
* Quick lookup map for states that are considered to be "active"
*/
var IsActive = {
RESPONDER_ACTIVE_PRESS_OUT: true,
RESPONDER_ACTIVE_PRESS_IN: true
};
/**
* Quick lookup for states that are considered to be "pressing" and are
* therefore eligible to result in a "selection" if the press stops.
*/
var IsPressingIn = {
RESPONDER_INACTIVE_PRESS_IN: true,
RESPONDER_ACTIVE_PRESS_IN: true,
RESPONDER_ACTIVE_LONG_PRESS_IN: true
};
var IsLongPressingIn = {
RESPONDER_ACTIVE_LONG_PRESS_IN: true
};
/**
* Inputs to the state machine.
*/
var Signals = keyMirror({
DELAY: null,
RESPONDER_GRANT: null,
RESPONDER_RELEASE: null,
RESPONDER_TERMINATED: null,
ENTER_PRESS_RECT: null,
LEAVE_PRESS_RECT: null,
LONG_PRESS_DETECTED: null
});
/**
* Mapping from States x Signals => States
*/
var Transitions = {
NOT_RESPONDER: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN,
RESPONDER_RELEASE: States.ERROR,
RESPONDER_TERMINATED: States.ERROR,
ENTER_PRESS_RECT: States.ERROR,
LEAVE_PRESS_RECT: States.ERROR,
LONG_PRESS_DETECTED: States.ERROR
},
RESPONDER_INACTIVE_PRESS_IN: {
DELAY: States.RESPONDER_ACTIVE_PRESS_IN,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT,
LONG_PRESS_DETECTED: States.ERROR
},
RESPONDER_INACTIVE_PRESS_OUT: {
DELAY: States.RESPONDER_ACTIVE_PRESS_OUT,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT,
LONG_PRESS_DETECTED: States.ERROR
},
RESPONDER_ACTIVE_PRESS_IN: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT,
LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN
},
RESPONDER_ACTIVE_PRESS_OUT: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT,
LONG_PRESS_DETECTED: States.ERROR
},
RESPONDER_ACTIVE_LONG_PRESS_IN: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT,
LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN
},
RESPONDER_ACTIVE_LONG_PRESS_OUT: {
DELAY: States.ERROR,
RESPONDER_GRANT: States.ERROR,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT,
LONG_PRESS_DETECTED: States.ERROR
},
error: {
DELAY: States.NOT_RESPONDER,
RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN,
RESPONDER_RELEASE: States.NOT_RESPONDER,
RESPONDER_TERMINATED: States.NOT_RESPONDER,
ENTER_PRESS_RECT: States.NOT_RESPONDER,
LEAVE_PRESS_RECT: States.NOT_RESPONDER,
LONG_PRESS_DETECTED: States.NOT_RESPONDER
}
};
// ==== Typical Constants for integrating into UI components ====
// const HIT_EXPAND_PX = 20;
// const HIT_VERT_OFFSET_PX = 10;
var HIGHLIGHT_DELAY_MS = 130;
var PRESS_EXPAND_PX = 20;
var LONG_PRESS_THRESHOLD = 500;
var LONG_PRESS_DELAY_MS = LONG_PRESS_THRESHOLD - HIGHLIGHT_DELAY_MS;
var LONG_PRESS_ALLOWED_MOVEMENT = 10;
var lastClickTime = 0;
var pressDelay = 200;
function isAllowPress() {
// avoid click penetration
return Date.now() - lastClickTime >= pressDelay;
}
var Touchable = function (_React$Component) {
_inherits(Touchable, _React$Component);
function Touchable() {
_classCallCheck(this, Touchable);
var _this = _possibleConstructorReturn(this, (Touchable.__proto__ || Object.getPrototypeOf(Touchable)).apply(this, arguments));
_this.state = {
active: false
};
_this.touchable = { touchState: undefined };
_this.onTouchStart = function (e) {
_this.callChildEvent('onTouchStart', e);
_this.lockMouse = true;
if (_this.releaseLockTimer) {
clearTimeout(_this.releaseLockTimer);
}
_this.touchableHandleResponderGrant(e.nativeEvent);
};
_this.onTouchMove = function (e) {
_this.callChildEvent('onTouchMove', e);
_this.touchableHandleResponderMove(e.nativeEvent);
};
_this.onTouchEnd = function (e) {
_this.callChildEvent('onTouchEnd', e);
_this.releaseLockTimer = setTimeout(function () {
_this.lockMouse = false;
}, 300);
_this.touchableHandleResponderRelease(new PressEvent(e.nativeEvent));
};
_this.onTouchCancel = function (e) {
_this.callChildEvent('onTouchCancel', e);
_this.releaseLockTimer = setTimeout(function () {
_this.lockMouse = false;
}, 300);
_this.touchableHandleResponderTerminate(e.nativeEvent);
};
_this.onMouseDown = function (e) {
_this.callChildEvent('onMouseDown', e);
if (_this.lockMouse) {
return;
}
_this.touchableHandleResponderGrant(e.nativeEvent);
document.addEventListener('mousemove', _this.touchableHandleResponderMove, false);
document.addEventListener('mouseup', _this.onMouseUp, false);
};
_this.onMouseUp = function (e) {
document.removeEventListener('mousemove', _this.touchableHandleResponderMove, false);
document.removeEventListener('mouseup', _this.onMouseUp, false);
_this.touchableHandleResponderRelease(new PressEvent(e));
};
_this.touchableHandleResponderMove = function (e) {
if (!_this.touchable.startMouse) {
return;
}
// Measurement may not have returned yet.
if (!_this.touchable.dimensionsOnActivate || _this.touchable.touchState === States.NOT_RESPONDER) {
return;
}
// Not enough time elapsed yet, wait for highlight -
// this is just a perf optimization.
if (_this.touchable.touchState === States.RESPONDER_INACTIVE_PRESS_IN) {
return;
}
var touch = extractSingleTouch(e);
var pageX = touch && touch.pageX;
var pageY = touch && touch.pageY;
if (_this.pressInLocation) {
var movedDistance = _this._getDistanceBetweenPoints(pageX, pageY, _this.pressInLocation.pageX, _this.pressInLocation.pageY);
if (movedDistance > LONG_PRESS_ALLOWED_MOVEMENT) {
_this._cancelLongPressDelayTimeout();
}
}
if (_this.checkTouchWithinActive(e)) {
_this._receiveSignal(Signals.ENTER_PRESS_RECT, e);
var curState = _this.touchable.touchState;
if (curState === States.RESPONDER_INACTIVE_PRESS_IN) {
_this._cancelLongPressDelayTimeout();
}
} else {
_this._cancelLongPressDelayTimeout();
_this._receiveSignal(Signals.LEAVE_PRESS_RECT, e);
}
};
return _this;
}
_createClass(Touchable, [{
key: 'componentDidMount',
value: function componentDidMount() {
this.root = ReactDOM.findDOMNode(this);
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate() {
this.root = ReactDOM.findDOMNode(this);
// disabled auto clear active state
if (this.props.disabled && this.state.active) {
this.setState({
active: false
});
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (this.releaseLockTimer) {
clearTimeout(this.releaseLockTimer);
}
if (this.touchableDelayTimeout) {
clearTimeout(this.touchableDelayTimeout);
}
if (this.longPressDelayTimeout) {
clearTimeout(this.longPressDelayTimeout);
}
if (this.pressOutDelayTimeout) {
clearTimeout(this.pressOutDelayTimeout);
}
}
}, {
key: 'callChildEvent',
value: function callChildEvent(event, e) {
var childHandle = React.Children.only(this.props.children).props[event];
if (childHandle) {
childHandle(e);
}
}
}, {
key: '_remeasureMetricsOnInit',
value: function _remeasureMetricsOnInit(e) {
var root = this.root;
var touch = extractSingleTouch(e);
var boundingRect = root.getBoundingClientRect();
this.touchable = {
touchState: this.touchable.touchState,
startMouse: {
pageX: touch.pageX,
pageY: touch.pageY
},
positionOnGrant: {
left: boundingRect.left + window.pageXOffset,
top: boundingRect.top + window.pageYOffset,
width: boundingRect.width,
height: boundingRect.height,
clientLeft: boundingRect.left,
clientTop: boundingRect.top
}
};
}
}, {
key: 'processActiveStopPropagation',
value: function processActiveStopPropagation(e) {
var nativeEvent = e.nativeEvent || e;
this.shouldActive = !nativeEvent.__activeStopPropagation;
if (this.props.activeStopPropagation) {
nativeEvent.__activeStopPropagation = 1;
}
}
}, {
key: 'touchableHandleResponderGrant',
value: function touchableHandleResponderGrant(e) {
var _this2 = this;
this.touchable.touchState = States.NOT_RESPONDER;
if (this.pressOutDelayTimeout) {
clearTimeout(this.pressOutDelayTimeout);
this.pressOutDelayTimeout = null;
}
if (this.props.fixClickPenetration && !isAllowPress()) {
return;
}
this._remeasureMetricsOnInit(e);
this._receiveSignal(Signals.RESPONDER_GRANT, e);
var _props = this.props,
delayMS = _props.delayPressIn,
longDelayMS = _props.delayLongPress;
this.processActiveStopPropagation(e);
if (delayMS) {
this.touchableDelayTimeout = setTimeout(function () {
_this2._handleDelay(e);
}, delayMS);
} else {
this._handleDelay(e);
}
var longPressEvent = new PressEvent(e);
this.longPressDelayTimeout = setTimeout(function () {
_this2._handleLongDelay(longPressEvent);
}, longDelayMS + delayMS);
}
}, {
key: 'checkScroll',
value: function checkScroll(e) {
var positionOnGrant = this.touchable.positionOnGrant;
// container or window scroll
var boundingRect = this.root.getBoundingClientRect();
if (boundingRect.left !== positionOnGrant.clientLeft || boundingRect.top !== positionOnGrant.clientTop) {
this._receiveSignal(Signals.RESPONDER_TERMINATED, e);
return true;
}
return false;
}
}, {
key: 'touchableHandleResponderRelease',
value: function touchableHandleResponderRelease(e) {
if (!this.touchable.startMouse) {
return;
}
var touch = extractSingleTouch(e);
if (Math.abs(touch.pageX - this.touchable.startMouse.pageX) > 30 || Math.abs(touch.pageY - this.touchable.startMouse.pageY) > 30) {
this._receiveSignal(Signals.RESPONDER_TERMINATED, e);
return;
}
if (this.checkScroll(e)) {
return;
}
this._receiveSignal(Signals.RESPONDER_RELEASE, e);
}
}, {
key: 'touchableHandleResponderTerminate',
value: function touchableHandleResponderTerminate(e) {
if (!this.touchable.startMouse) {
return;
}
this._receiveSignal(Signals.RESPONDER_TERMINATED, e);
}
}, {
key: 'checkTouchWithinActive',
value: function checkTouchWithinActive(e) {
var positionOnGrant = this.touchable.positionOnGrant;
var _props2 = this.props,
_props2$pressRetentio = _props2.pressRetentionOffset,
pressRetentionOffset = _props2$pressRetentio === undefined ? {} : _props2$pressRetentio,
hitSlop = _props2.hitSlop;
var pressExpandLeft = pressRetentionOffset.left;
var pressExpandTop = pressRetentionOffset.top;
var pressExpandRight = pressRetentionOffset.right;
var pressExpandBottom = pressRetentionOffset.bottom;
if (hitSlop) {
pressExpandLeft += hitSlop.left;
pressExpandTop += hitSlop.top;
pressExpandRight += hitSlop.right;
pressExpandBottom += hitSlop.bottom;
}
var touch = extractSingleTouch(e);
var pageX = touch && touch.pageX;
var pageY = touch && touch.pageY;
return pageX > positionOnGrant.left - pressExpandLeft && pageY > positionOnGrant.top - pressExpandTop && pageX < positionOnGrant.left + positionOnGrant.width + pressExpandRight && pageY < positionOnGrant.top + positionOnGrant.height + pressExpandBottom;
}
}, {
key: 'callProp',
value: function callProp(name, e) {
if (this.props[name] && !this.props.disabled) {
this.props[name](e);
}
}
}, {
key: 'touchableHandleActivePressIn',
value: function touchableHandleActivePressIn(e) {
if (this.shouldActive) {
this.setActive(true);
}
this.callProp('onPressIn', e);
}
}, {
key: 'touchableHandleActivePressOut',
value: function touchableHandleActivePressOut(e) {
this.setActive(false);
this.callProp('onPressOut', e);
}
}, {
key: 'touchableHandlePress',
value: function touchableHandlePress(e) {
if (shouldFirePress(e)) {
this.callProp('onPress', e);
}
lastClickTime = Date.now();
}
}, {
key: 'touchableHandleLongPress',
value: function touchableHandleLongPress(e) {
if (shouldFirePress(e)) {
this.callProp('onLongPress', e);
}
}
}, {
key: 'setActive',
value: function setActive(active) {
if (this.state.active !== active && (this.props.activeClassName || this.props.activeStyle)) {
this.setState({
active: active
});
}
}
}, {
key: '_remeasureMetricsOnActivation',
value: function _remeasureMetricsOnActivation() {
this.touchable.dimensionsOnActivate = this.touchable.positionOnGrant;
}
}, {
key: '_handleDelay',
value: function _handleDelay(e) {
this.touchableDelayTimeout = null;
this._receiveSignal(Signals.DELAY, e);
}
}, {
key: '_handleLongDelay',
value: function _handleLongDelay(e) {
this.longPressDelayTimeout = null;
var curState = this.touchable.touchState;
if (curState !== States.RESPONDER_ACTIVE_PRESS_IN && curState !== States.RESPONDER_ACTIVE_LONG_PRESS_IN) {
console.error('Attempted to transition from state `' + curState + '` to `' + States.RESPONDER_ACTIVE_LONG_PRESS_IN + '`, which is not supported. This is ' + 'most likely due to `Touchable.longPressDelayTimeout` not being cancelled.');
} else {
this._receiveSignal(Signals.LONG_PRESS_DETECTED, e);
}
}
}, {
key: '_receiveSignal',
value: function _receiveSignal(signal, e) {
var curState = this.touchable.touchState;
var nextState = Transitions[curState] && Transitions[curState][signal];
if (!nextState) {
return;
}
if (nextState === States.ERROR) {
return;
}
if (curState !== nextState) {
this._performSideEffectsForTransition(curState, nextState, signal, e);
this.touchable.touchState = nextState;
}
}
}, {
key: '_cancelLongPressDelayTimeout',
value: function _cancelLongPressDelayTimeout() {
if (this.longPressDelayTimeout) {
clearTimeout(this.longPressDelayTimeout);
this.longPressDelayTimeout = null;
}
}
}, {
key: '_isHighlight',
value: function _isHighlight(state) {
return state === States.RESPONDER_ACTIVE_PRESS_IN || state === States.RESPONDER_ACTIVE_LONG_PRESS_IN;
}
}, {
key: '_savePressInLocation',
value: function _savePressInLocation(e) {
var touch = extractSingleTouch(e);
var pageX = touch && touch.pageX;
var pageY = touch && touch.pageY;
this.pressInLocation = { pageX: pageX, pageY: pageY };
}
}, {
key: '_getDistanceBetweenPoints',
value: function _getDistanceBetweenPoints(aX, aY, bX, bY) {
var deltaX = aX - bX;
var deltaY = aY - bY;
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
}, {
key: '_performSideEffectsForTransition',
value: function _performSideEffectsForTransition(curState, nextState, signal, e) {
var curIsHighlight = this._isHighlight(curState);
var newIsHighlight = this._isHighlight(nextState);
var isFinalSignal = signal === Signals.RESPONDER_TERMINATED || signal === Signals.RESPONDER_RELEASE;
if (isFinalSignal) {
this._cancelLongPressDelayTimeout();
}
if (!IsActive[curState] && IsActive[nextState]) {
this._remeasureMetricsOnActivation();
}
if (IsPressingIn[curState] && signal === Signals.LONG_PRESS_DETECTED) {
this.touchableHandleLongPress(e);
}
if (newIsHighlight && !curIsHighlight) {
this._startHighlight(e);
} else if (!newIsHighlight && curIsHighlight) {
this._endHighlight(e);
}
if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) {
var hasLongPressHandler = !!this.props.onLongPress;
var pressIsLongButStillCallOnPress = IsLongPressingIn[curState] && ( // We *are* long pressing..
!hasLongPressHandler || // But either has no long handler
!this.props.longPressCancelsPress // or we're told to ignore it.
);
var shouldInvokePress = !IsLongPressingIn[curState] || pressIsLongButStillCallOnPress;
if (shouldInvokePress) {
if (!newIsHighlight && !curIsHighlight) {
// we never highlighted because of delay, but we should highlight now
this._startHighlight(e);
this._endHighlight(e);
}
this.touchableHandlePress(e);
}
}
if (this.touchableDelayTimeout) {
clearTimeout(this.touchableDelayTimeout);
this.touchableDelayTimeout = null;
}
}
}, {
key: '_startHighlight',
value: function _startHighlight(e) {
this._savePressInLocation(e);
this.touchableHandleActivePressIn(e);
}
}, {
key: '_endHighlight',
value: function _endHighlight(e) {
var _this3 = this;
if (this.props.delayPressOut) {
this.pressOutDelayTimeout = setTimeout(function () {
_this3.touchableHandleActivePressOut(e);
}, this.props.delayPressOut);
} else {
this.touchableHandleActivePressOut(e);
}
}
}, {
key: 'render',
value: function render() {
var _props3 = this.props,
children = _props3.children,
disabled = _props3.disabled,
activeStyle = _props3.activeStyle,
activeClassName = _props3.activeClassName;
var events = disabled ? undefined : copy(this, ['onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel', 'onMouseDown']);
var child = React.Children.only(children);
if (!disabled && this.state.active) {
var _child$props = child.props,
style = _child$props.style,
className = _child$props.className;
if (activeStyle) {
style = _extends({}, style, activeStyle);
}
if (activeClassName) {
if (className) {
className += ' ' + activeClassName;
} else {
className = activeClassName;
}
}
return React.cloneElement(child, _extends({ className: className,
style: style }, events));
}
return React.cloneElement(child, events);
}
}]);
return Touchable;
}(React.Component);
export default Touchable;
Touchable.defaultProps = {
fixClickPenetration: false,
disabled: false,
delayPressIn: HIGHLIGHT_DELAY_MS,
delayLongPress: LONG_PRESS_DELAY_MS,
delayPressOut: 100,
pressRetentionOffset: {
left: PRESS_EXPAND_PX,
right: PRESS_EXPAND_PX,
top: PRESS_EXPAND_PX,
bottom: PRESS_EXPAND_PX
},
hitSlop: undefined,
longPressCancelsPress: true
};