diff --git a/src/components/swipe-container.jsx b/src/components/swipe-container.jsx index c7f4da680..fe21963cb 100644 --- a/src/components/swipe-container.jsx +++ b/src/components/swipe-container.jsx @@ -1,5 +1,6 @@ import React, {Component, PropTypes} from 'react'; import _ from 'underscore'; +import {exec} from 'child_process'; const Phase = { // No wheel events received yet, container is inactive. @@ -15,6 +16,25 @@ const Phase = { Settling: 'settling', } +let SwipeInverted = false; + +if (process.platform === 'darwin') { + exec("defaults read -g com.apple.swipescrolldirection", (err, stdout)=> { + if (err !== null) { + return; + } + if (stdout.toString().trim() === '1') { + SwipeInverted = true; + } + }); +} else if (process.platform === 'win32') { + // Currently does not matter because we don't support trackpad gestures on Win. + // It appears there's a config key called FlipFlopWheel which we might have to + // check, but it also looks like disabling natural scroll on Win32 only changes + // vertical, not horizontal, behavior. +} + + export default class SwipeContainer extends Component { static displayName = 'SwipeContainer'; @@ -29,6 +49,7 @@ export default class SwipeContainer extends Component { constructor(props) { super(props); this.tracking = false; + this.trackingTouchIdentifier = null; this.phase = Phase.None; this.fired = false; this.state = { @@ -44,11 +65,6 @@ export default class SwipeContainer extends Component { window.addEventListener('scroll-touch-end', this._onScrollTouchEnd); } - // componentWillReceiveProps(nextProps) { - // if (nextProps.children !== this.props.children) { - // this.setState({currentX: 0, velocity: 0, targetX: 0, settling: false}); - // } - // } componentDidUpdate() { if (this.phase === Phase.Settling) { window.requestAnimationFrame(()=> { @@ -69,11 +85,21 @@ export default class SwipeContainer extends Component { } _onWheel = (e)=> { + let velocity = e.deltaX / 3; + if (SwipeInverted) { + velocity = -velocity; + } + this._onDragWithVelocity(velocity); + + if (this.phase === Phase.GestureConfirmed) { + e.preventDefault(); + } + } + + _onDragWithVelocity = (velocity)=> { if ((this.tracking === false) || !this._isEnabled()) { return; } - - const velocity = e.deltaX / 3; const velocityConfirmsGesture = Math.abs(velocity) > 3; if (this.phase === Phase.None) { @@ -84,10 +110,6 @@ export default class SwipeContainer extends Component { this.phase = Phase.GestureConfirmed; } - if (this.phase === Phase.GestureConfirmed) { - e.preventDefault(); - } - let {fullDistance, thresholdDistance} = this.state; if (fullDistance === 'unknown') { @@ -99,13 +121,13 @@ export default class SwipeContainer extends Component { const estimatedSettleX = currentX + velocity * 8; let targetX = 0; - if (this.props.onSwipeLeft && (estimatedSettleX > thresholdDistance)) { + if (this.props.onSwipeRight && (estimatedSettleX > thresholdDistance)) { targetX = fullDistance; } - if (this.props.onSwipeRight && (estimatedSettleX < -thresholdDistance)) { + if (this.props.onSwipeLeft && (estimatedSettleX < -thresholdDistance)) { targetX = -fullDistance; } - this.setState({thresholdDistance, fullDistance, velocity, currentX, targetX}) + this.setState({thresholdDistance, fullDistance, velocity, currentX, targetX}); } _onScrollTouchBegin = ()=> { @@ -121,6 +143,56 @@ export default class SwipeContainer extends Component { } } + _onTouchStart = (e)=> { + if ((this.trackingTouchIdentifier === null) && (e.targetTouches.length > 0)) { + const touch = e.targetTouches.item(0); + this.trackingTouchIdentifier = touch.identifier; + this.trackingTouchX = touch.clientX; + this._onScrollTouchBegin(); + } + } + + _onTouchMove = (e)=> { + if (this.trackingTouchIdentifier === null) { + return; + } + if (e.cancelable === false) { + // Chrome has already started interpreting these touch events as a scroll. + // We can no longer call preventDefault to make them ours. + return; + } + let trackingTouch = null; + for (let ii = 0; ii < e.changedTouches.length; ii++) { + const touch = e.changedTouches.item(ii); + if (touch.identifier === this.trackingTouchIdentifier) { + trackingTouch = touch; + break; + } + } + if (trackingTouch !== null) { + const velocity = (trackingTouch.clientX - this.trackingTouchX); + this.trackingTouchX = trackingTouch.clientX; + this._onDragWithVelocity(velocity); + + if (this.phase === Phase.GestureConfirmed) { + e.preventDefault(); + } + } + } + + _onTouchEnd = (e)=> { + if (this.trackingTouchIdentifier === null) { + return; + } + for (let ii = 0; ii < e.changedTouches.length; ii++) { + if (e.changedTouches.item(ii).identifier === this.trackingTouchIdentifier) { + this.trackingTouchIdentifier = null; + this._onScrollTouchEnd(); + break; + } + } + } + _settle() { const {currentX, targetX} = this.state; let {velocity} = this.state; @@ -149,10 +221,10 @@ export default class SwipeContainer extends Component { if (shouldFire) { this.fired = true; - if (targetX < 0) { + if (targetX > 0) { this.props.onSwipeRight(); } - if (targetX > 0) { + if (targetX < 0) { this.props.onSwipeLeft(); } } @@ -182,25 +254,30 @@ export default class SwipeContainer extends Component { const backingStyles = {top: 0, bottom: 0, position: 'absolute'}; let backingClass = 'swipe-backing'; - if (currentX > 0) { + if (currentX < 0) { backingClass += ' ' + this.props.onSwipeLeftClass; backingStyles.right = 0; - backingStyles.width = currentX + 1; - if (targetX > 0) { - backingClass += ' confirmed'; - } - } else if (currentX < 0) { - backingClass += ' ' + this.props.onSwipeRightClass; - backingStyles.left = 0; backingStyles.width = -currentX + 1; if (targetX < 0) { backingClass += ' confirmed'; } + } else if (currentX > 0) { + backingClass += ' ' + this.props.onSwipeRightClass; + backingStyles.left = 0; + backingStyles.width = currentX + 1; + if (targetX > 0) { + backingClass += ' confirmed'; + } } return ( -
+
-
+
{this.props.children}