'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); 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; }; }(); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _reactDom = require('react-dom'); var _reactDom2 = _interopRequireDefault(_reactDom); var _PressEvent = require('./PressEvent'); var _PressEvent2 = _interopRequireDefault(_PressEvent); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 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 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 _PressEvent2['default'](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 _PressEvent2['default'](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 = _reactDom2['default'].findDOMNode(this); } }, { key: 'componentDidUpdate', value: function componentDidUpdate() { this.root = _reactDom2['default'].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 = _react2['default'].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 _PressEvent2['default'](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 ((0, _PressEvent.shouldFirePress)(e)) { this.callProp('onPress', e); } lastClickTime = Date.now(); } }, { key: 'touchableHandleLongPress', value: function touchableHandleLongPress(e) { if ((0, _PressEvent.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 = _react2['default'].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 _react2['default'].cloneElement(child, _extends({ className: className, style: style }, events)); } return _react2['default'].cloneElement(child, events); } }]); return Touchable; }(_react2['default'].Component); exports['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 }; module.exports = exports['default'];