424

How can you detect that a user swiped his finger in some direction over a web page with JavaScript?

I was wondering if there was one solution that would work for websites on both the iPhone and an Android phone.

827
  • 4,864
  • 4
  • 19
  • 24
  • 6
    For swipe recognition, I would recommend [Hammer.js](http://hammerjs.github.io/). It's quite small, and it supports many gestures: - **Swipe** - Rotate - Pinch - Press (long hold) - Tap - Pan – Will Brickner Nov 04 '16 at 00:01
  • There is an event: "touchmove" – Clay May 23 '20 at 12:19
  • 1
    @Clay that one still does not work in Safari so no iPhone. – Jakuje Jun 12 '20 at 20:30
  • @Jakuje "Can I Use" website says that Safari on iOS supports touch events, so iPhones yes. – Piotr Śródka Dec 18 '22 at 07:56
  • latest versions of iOS have native ootb handling of left-right swipe gestures over an HTML5 video element; the gestures will seek a video with some dynamic animated UI popup to indicate the current position in the video; so probably this case should be detected explicitly unless it's intended to override this behaviour. – ccpizza Feb 23 '23 at 10:59

28 Answers28

529

Simple vanilla JS code sample:

document.addEventListener('touchstart', handleTouchStart, false);        
document.addEventListener('touchmove', handleTouchMove, false);

var xDown = null;                                                        
var yDown = null;

function getTouches(evt) {
  return evt.touches ||             // browser API
         evt.originalEvent.touches; // jQuery
}                                                     
                                                                         
function handleTouchStart(evt) {
    const firstTouch = getTouches(evt)[0];                                      
    xDown = firstTouch.clientX;                                      
    yDown = firstTouch.clientY;                                      
};                                                
                                                                         
function handleTouchMove(evt) {
    if ( ! xDown || ! yDown ) {
        return;
    }

    var xUp = evt.touches[0].clientX;                                    
    var yUp = evt.touches[0].clientY;

    var xDiff = xDown - xUp;
    var yDiff = yDown - yUp;
                                                                         
    if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/
        if ( xDiff > 0 ) {
            /* right swipe */ 
        } else {
            /* left swipe */
        }                       
    } else {
        if ( yDiff > 0 ) {
            /* down swipe */ 
        } else { 
            /* up swipe */
        }                                                                 
    }
    /* reset values */
    xDown = null;
    yDown = null;                                             
};

Tested in Android.

Niket Pathak
  • 6,323
  • 1
  • 39
  • 51
givanse
  • 14,503
  • 8
  • 51
  • 75
  • 2
    Looks cool and simple, any idea what is the support for this events `touchstart`, `touchmove` ? – d.raev Jun 05 '14 at 07:46
  • 1
    It works pretty well but has a problem detecting straight movements. I will post another answer at this topic that fixed this as JQuery (desktop) solution. It also adds the mouse version of these swipe events and add a sensitivity option. – Codebeat Apr 22 '15 at 12:12
  • 1
    Damn. Topic is closed so cannot add my answer! – Codebeat Apr 22 '15 at 12:13
  • 6
    This works great, but left/right and up/down are backwards. – Peter Eisentraut Feb 16 '16 at 02:44
  • Works great !! Any idea how to get the exact distance swiped ?? I tried using yDiff for swipe down, but it keeps giving me different values for almost equal swipes. – mrid Oct 11 '16 at 09:47
  • I tried this - when I swipe the page scrolls instead of the event triggering – quantumpotato Oct 24 '16 at 01:02
  • 7
    originalEvent is a JQuery property. It should be left out if you run pure javascript without JQuery. The current code raises an exception if run without JQuery. – Jan Derk May 21 '17 at 20:16
  • It works good also on desktop Chrome with touchscreen (on Windows). But there's already build-in function makes history back and history forward on same gestures. It's possible to turn it off, but you'r mess with user which expecting different behaviour. – iiic Nov 07 '17 at 13:54
  • 1
    A problem with this is that it does not consider the size of the gesture: even a small gesture (of a few pixels) may cause an involuntary swipe... – collimarco Nov 27 '19 at 11:23
  • if ```preventDefault()``` is added the touch controls sometimes get wrong coords!! – Panagiss May 26 '20 at 16:02
  • how do we know the div where swipe is started? – user1090751 Nov 02 '20 at 16:33
  • @collimarco See my answer below, derived from this one; it solves this problem. – Basj Dec 04 '20 at 22:23
  • If you do a touch "pinch zoom" with two fingers, one finger nearly fixed, and the other finger moving away, I think it is detected as a "swipe" by this code... Isn't it? – Basj Dec 04 '20 at 22:24
  • how could i add a range before the swipe event will be casted? like if it's just a small right or left movment i have to ignore it while when it's significant i have to cast it – NiceToMytyuk Feb 26 '21 at 14:30
  • xDiff and yDiff would be the numbers to gate under a treshold – givanse Feb 26 '21 at 19:03
  • It should be noted that "right swipe" is referring to swiping right-to-left, the same goes for the rest of the swipes: [listed]-to-[opposite]. – grepgrok Jul 06 '22 at 20:24
245

Simple vanilla JS example for horizontal swipe:

let touchstartX = 0
let touchendX = 0
    
function checkDirection() {
  if (touchendX < touchstartX) alert('swiped left!')
  if (touchendX > touchstartX) alert('swiped right!')
}

document.addEventListener('touchstart', e => {
  touchstartX = e.changedTouches[0].screenX
})

document.addEventListener('touchend', e => {
  touchendX = e.changedTouches[0].screenX
  checkDirection()
})

You can use pretty same logic for vertical swipe.

Damjan Pavlica
  • 31,277
  • 10
  • 71
  • 76
63

I merged a few of the answers here into a script that uses CustomEvent to fire swiped events in the DOM. Add the 0.7k swiped-events.min.js script to your page and listen for swiped events:

swiped

document.addEventListener('swiped', function(e) {
    console.log(e.target); // the element that was swiped
    console.log(e.detail.dir); // swiped direction
});

swiped-left

document.addEventListener('swiped-left', function(e) {
    console.log(e.target); // the element that was swiped
});

swiped-right

document.addEventListener('swiped-right', function(e) {
    console.log(e.target); // the element that was swiped
});

swiped-up

document.addEventListener('swiped-up', function(e) {
    console.log(e.target); // the element that was swiped
});

swiped-down

document.addEventListener('swiped-down', function(e) {
    console.log(e.target); // the element that was swiped
});

You can also attach directly to an element:

document.getElementById('myBox').addEventListener('swiped-down', function(e) {
    console.log(e.target); // the element that was swiped
});

Optional config

You can specify the following attributes to tweak how swipe interaction functions in your page (these are optional).

<div data-swipe-threshold="10"
     data-swipe-timeout="1000"
     data-swipe-ignore="false">
      Swiper, get swiping!
</div>

To set defaults application wide, set config attributes on topmost element:

<body data-swipe-threshold="100" data-swipe-timeout="250">
    <div>Swipe me</div>
    <div>or me</div>
</body>

Source code is available on Github

John Doherty
  • 3,669
  • 36
  • 38
  • I came here because pure-swipe was not working for me on MOBILE – StefanBob May 04 '18 at 15:28
  • @StefanBob if you raise a tick on the [github repo](https://github.com/john-doherty/pure-swipe/issues) with enough information to allow me to reproduce the issue, I will look into it – John Doherty Jan 10 '19 at 15:15
  • 2
    Thanks, it works perfectly! I replaced Hammer.js with your library, because the former doesn't work with browser zoom and that is a serious usability issue. With this library the zoom works properly (tested on Android) – collimarco Nov 26 '19 at 22:24
  • 3
    Hammer.js doesn't seem to be maintained anymore – AlexandreS Nov 19 '20 at 14:07
  • 1
    @JohnDoherty would you extend it so it also works on desktop(non-touch) devices. – Zahidul Islam Ruhel Jul 06 '22 at 18:36
47

Based on @givanse's answer, this is how you could do it with classes:

class Swipe {
    constructor(element) {
        this.xDown = null;
        this.yDown = null;
        this.element = typeof(element) === 'string' ? document.querySelector(element) : element;

        this.element.addEventListener('touchstart', function(evt) {
            this.xDown = evt.touches[0].clientX;
            this.yDown = evt.touches[0].clientY;
        }.bind(this), false);

    }

    onLeft(callback) {
        this.onLeft = callback;

        return this;
    }

    onRight(callback) {
        this.onRight = callback;

        return this;
    }

    onUp(callback) {
        this.onUp = callback;

        return this;
    }

    onDown(callback) {
        this.onDown = callback;

        return this;
    }

    handleTouchMove(evt) {
        if ( ! this.xDown || ! this.yDown ) {
            return;
        }

        var xUp = evt.touches[0].clientX;
        var yUp = evt.touches[0].clientY;

        this.xDiff = this.xDown - xUp;
        this.yDiff = this.yDown - yUp;

        if ( Math.abs( this.xDiff ) > Math.abs( this.yDiff ) ) { // Most significant.
            if ( this.xDiff > 0 ) {
                this.onLeft();
            } else {
                this.onRight();
            }
        } else {
            if ( this.yDiff > 0 ) {
                this.onUp();
            } else {
                this.onDown();
            }
        }

        // Reset values.
        this.xDown = null;
        this.yDown = null;
    }

    run() {
        this.element.addEventListener('touchmove', function(evt) {
            this.handleTouchMove(evt).bind(this);
        }.bind(this), false);
    }
}

You can than use it like this:

// Use class to get element by string.
var swiper = new Swipe('#my-element');
swiper.onLeft(function() { alert('You swiped left.') });
swiper.run();

// Get the element yourself.
var swiper = new Swipe(document.getElementById('#my-element'));
swiper.onLeft(function() { alert('You swiped left.') });
swiper.run();

// One-liner.
(new Swipe('#my-element')).onLeft(function() { alert('You swiped left.') }).run();
Marwelln
  • 28,492
  • 21
  • 93
  • 117
  • 8
    this code probably wont work because you'll get an exception while trying to call `.bind` of undefined because your `handleTouchMove` actually didn't return anything. also it's useless to call bind when calling function with `this.` because it's already bound to current context – nick.skriabin Oct 14 '16 at 12:02
  • 3
    I just removed `.bind(this);` and it worked gracefully. thank you @nicholas_r – Ali Ghanavatian Jul 29 '17 at 14:58
  • Part get the element yourself I just remove '#' in document.getElementById('my-element') and it worked good. Thank you @Marwelln :) – Blue Tram Mar 25 '19 at 06:01
  • 6
    If you want to wait until the swipe ENDS (meaning after they lift their finger or onmouseup), change `touches[0]` to `changedTouches[0]` and the event handler type `handleTouchMove` to `handleTouchEnd` – TetraDev Apr 19 '19 at 01:41
  • 1
    call `run()` twice and you get a nasty memory leak – Mat Apr 19 '19 at 12:09
  • @Marwelln Hi Marwelln, can you say where you got this code from? – kishore kingmaker Nov 13 '19 at 14:15
  • How can we make this work for every "class" ? // One-liner. (new Swipe('.everyElementHasThisClass')).onLeft(function() { alert('You swiped left.') }).run(); – smlnl Feb 19 '21 at 07:46
20

I have found @givanse brilliant answer to be the most reliable and compatible across multiple mobile browsers for registering swipe actions.

However, there's a change in his code required to make it work in modern day mobile browsers that are using jQuery.

event.toucheswon't exist if jQuery is used and results in undefined and should be replaced by event.originalEvent.touches. Without jQuery, event.touches should work fine.

So the solution becomes,

document.addEventListener('touchstart', handleTouchStart, false);        
document.addEventListener('touchmove', handleTouchMove, false);

var xDown = null;                                                        
var yDown = null;                                                        

function handleTouchStart(evt) {                                         
    xDown = evt.originalEvent.touches[0].clientX;                                      
    yDown = evt.originalEvent.touches[0].clientY;                                      
};                                                

function handleTouchMove(evt) {
    if ( ! xDown || ! yDown ) {
        return;
    }

    var xUp = evt.originalEvent.touches[0].clientX;                                    
    var yUp = evt.originalEvent.touches[0].clientY;

    var xDiff = xDown - xUp;
    var yDiff = yDown - yUp;

    if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/
        if ( xDiff > 0 ) {
            /* left swipe */ 
        } else {
            /* right swipe */
        }                       
    } else {
        if ( yDiff > 0 ) {
            /* up swipe */ 
        } else { 
            /* down swipe */
        }                                                                 
    }
    /* reset values */
    xDown = null;
    yDown = null;                                             
};

Tested on:

  • Android: Chrome, UC Browser
  • iOS: Safari, Chrome, UC Browser
nashcheez
  • 5,067
  • 1
  • 27
  • 53
  • originalEvent is a JQuery property. It does not even exist in pure Javascript. – Jan Derk May 21 '17 at 20:19
  • 1
    As per [this SO answer](http://stackoverflow.com/a/16675056/3290256), a touch event if supported by the browser will be exposed through `event.originalEvent`. The thing is `event.touches` has ceased to exist now and results in `undefined`. – nashcheez May 21 '17 at 20:35
  • event.touches only ceased to exist when using JQuery. Try your code without JQuery and you will get an error that evt.originalEvent is undefined. JQuery totally replaces event with its own and puts the native browser event in originalevent. Short version: Your code only works with JQuery. It works without JQuery if you remove originalevent. – Jan Derk May 22 '17 at 08:34
  • 1
    Yeah I researched a bit and realized you were right about the availability of jquery enabling `event.originalEvent`. I will update my answer. Thanks! :) – nashcheez May 22 '17 at 11:06
  • Changed xDown = evt.originalEvent.touches[0].clientX; yDown = evt.originalEvent.touches[0].clientY; to xDown = evt.offsetX; yDown = evt.offsetY; and now it works like charm on normal JS. I like this solution. – Kviz Majster Sep 03 '20 at 20:22
16

what i've used before is you have to detect the mousedown event, record its x,y location (whichever is relevant) then detect the mouseup event, and subtract the two values.

alex
  • 479,566
  • 201
  • 878
  • 984
helloandre
  • 10,541
  • 8
  • 47
  • 64
  • 31
    I believe it's touchstart, touchmove, touchcancel, and touchend that one would work with, not mousedown or mouseup. – Volomike Jan 16 '11 at 21:42
13

jQuery Mobile also includes swipe support: http://api.jquerymobile.com/swipe/

Example

$("#divId").on("swipe", function(event) {
    alert("It's a swipe!");
});
Niklas Ekman
  • 943
  • 1
  • 9
  • 26
Grinn
  • 5,370
  • 38
  • 51
  • 4
    If one does not want jQuery mobile to manipulate the UI, see: http://stackoverflow.com/questions/8648596/how-to-use-jquery-mobile-for-its-touch-event-support-only-no-ui-enhancements#answer-12631480 – Niklas Ekman Nov 28 '13 at 13:20
13

Some mod of uppest answer(can't comment...) to deal with to short swipes

document.addEventListener('touchstart', handleTouchStart, false);        
document.addEventListener('touchmove', handleTouchMove, false);
var xDown = null;                                                        
var yDown = null;                                                        
function handleTouchStart(evt) {                                         
    xDown = evt.touches[0].clientX;                                      
    yDown = evt.touches[0].clientY;                                      
};                                                
function handleTouchMove(evt) {
    if ( ! xDown || ! yDown ) {
        return;
    }

    var xUp = evt.touches[0].clientX;                                    
    var yUp = evt.touches[0].clientY;

    var xDiff = xDown - xUp;
    var yDiff = yDown - yUp;
    if(Math.abs( xDiff )+Math.abs( yDiff )>150){ //to deal with to short swipes

    if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/
        if ( xDiff > 0 ) {/* left swipe */ 
            alert('left!');
        } else {/* right swipe */
            alert('right!');
        }                       
    } else {
        if ( yDiff > 0 ) {/* up swipe */
            alert('Up!'); 
        } else { /* down swipe */
            alert('Down!');
        }                                                                 
    }
    /* reset values */
    xDown = null;
    yDown = null;
    }
};
rmnsh
  • 131
  • 1
  • 5
  • Added `if ( ! xDown || ! yDown || e.touches.length === 2 ) {` for horizontal pinch zoom – Jack Jun 03 '23 at 22:25
9

I've repackaged TouchWipe as a short jquery plugin: detectSwipe

Marc-André Lafortune
  • 78,216
  • 16
  • 166
  • 166
7

I wanted to detect left and right swipe only, but trigger the action only when the touch event ends, so I slightly modified the @givanse's great answer to do that.

Why to do that? If for example, while swiping, the user notices he finally doesn't want to swipe, he can move his finger at the original position (a very popular "dating" phone application does this ;)), and then the "swipe right" event is cancelled.

So in order to avoid a "swipe right" event just because there is a 3px difference horizontally, I added a threshold under which an event is discarded: in order to have a "swipe right" event, the user has to swipe of at least 1/3 of the browser width (of course you can modify this).

All these small details enhance the user experience.

Note that currently, a "touch pinch zoom" might be detected as a swipe if one of the two fingers does a big horizontal move during the pinch zoom.

Here is the (Vanilla JS) code:

var xDown = null, yDown = null, xUp = null, yUp = null;
document.addEventListener('touchstart', touchstart, false);        
document.addEventListener('touchmove', touchmove, false);
document.addEventListener('touchend', touchend, false);
function touchstart(evt) { const firstTouch = (evt.touches || evt.originalEvent.touches)[0]; xDown = firstTouch.clientX; yDown = firstTouch.clientY; }
function touchmove(evt) { if (!xDown || !yDown ) return; xUp = evt.touches[0].clientX; yUp = evt.touches[0].clientY; }
function touchend(evt) { 
    var xDiff = xUp - xDown, yDiff = yUp - yDown;
    if ((Math.abs(xDiff) > Math.abs(yDiff)) && (Math.abs(xDiff) > 0.33 * document.body.clientWidth)) { 
        if (xDiff < 0) 
            document.getElementById('leftnav').click();
        else
            document.getElementById('rightnav').click();
    } 
    xDown = null, yDown = null;
}
Basj
  • 41,386
  • 99
  • 383
  • 673
6

threshold, timeout swipe, swipeBlockElems add.

document.addEventListener('touchstart', handleTouchStart, false);
document.addEventListener('touchmove', handleTouchMove, false);
document.addEventListener('touchend', handleTouchEnd, false);     

const SWIPE_BLOCK_ELEMS = [
  'swipBlock',
  'handle',
  'drag-ruble'
]

let xDown = null;
let yDown = null; 
let xDiff = null;
let yDiff = null;
let timeDown = null;
const  TIME_THRESHOLD = 200;
const  DIFF_THRESHOLD = 130;

function handleTouchEnd() {

let timeDiff = Date.now() - timeDown; 
if (Math.abs(xDiff) > Math.abs(yDiff)) { /*most significant*/
  if (Math.abs(xDiff) > DIFF_THRESHOLD && timeDiff < TIME_THRESHOLD) {
    if (xDiff > 0) {
      // console.log(xDiff, TIME_THRESHOLD, DIFF_THRESHOLD)
      SWIPE_LEFT(LEFT) /* left swipe */
    } else {
      // console.log(xDiff)
      SWIPE_RIGHT(RIGHT) /* right swipe */
    }
  } else {
    console.log('swipeX trashhold')
  }
} else {
  if (Math.abs(yDiff) > DIFF_THRESHOLD && timeDiff < TIME_THRESHOLD) {
    if (yDiff > 0) {
      /* up swipe */
    } else {
      /* down swipe */
    }
  } else {
    console.log('swipeY trashhold')
  }
 }
 /* reset values */
 xDown = null;
 yDown = null;
 timeDown = null; 
}
function containsClassName (evntarget , classArr) {
 for (var i = classArr.length - 1; i >= 0; i--) {
   if( evntarget.classList.contains(classArr[i]) ) {
      return true;
    }
  }
}
function handleTouchStart(evt) {
  let touchStartTarget = evt.target;
  if( containsClassName(touchStartTarget, SWIPE_BLOCK_ELEMS) ) {
    return;
  }
  timeDown = Date.now()
  xDown = evt.touches[0].clientX;
  yDown = evt.touches[0].clientY;
  xDiff = 0;
  yDiff = 0;

}

function handleTouchMove(evt) {
  if (!xDown || !yDown) {
    return;
  }

  var xUp = evt.touches[0].clientX;
  var yUp = evt.touches[0].clientY;


  xDiff = xDown - xUp;
  yDiff = yDown - yUp;
}
Kerem
  • 11,377
  • 5
  • 59
  • 58
Sergey Guns
  • 451
  • 5
  • 4
5

Adding to this answer here. This one adds support for mouse events for testing on desktop:

<!--scripts-->
class SwipeEventDispatcher {
    constructor(element, options = {}) {
        this.evtMap = {
            SWIPE_LEFT: [],
            SWIPE_UP: [],
            SWIPE_DOWN: [],
            SWIPE_RIGHT: []
        };

        this.xDown = null;
        this.yDown = null;
        this.element = element;
        this.isMouseDown = false;
        this.listenForMouseEvents = true;
        this.options = Object.assign({ triggerPercent: 0.3 }, options);

        element.addEventListener('touchstart', evt => this.handleTouchStart(evt), false);
        element.addEventListener('touchend', evt => this.handleTouchEnd(evt), false);
        element.addEventListener('mousedown', evt => this.handleMouseDown(evt), false);
        element.addEventListener('mouseup', evt => this.handleMouseUp(evt), false);
    }

    on(evt, cb) {
        this.evtMap[evt].push(cb);
    }

    off(evt, lcb) {
        this.evtMap[evt] = this.evtMap[evt].filter(cb => cb !== lcb);
    }

    trigger(evt, data) {
        this.evtMap[evt].map(handler => handler(data));
    }

    handleTouchStart(evt) {
        this.xDown = evt.touches[0].clientX;
        this.yDown = evt.touches[0].clientY;
    }

    handleMouseDown(evt) {
        if (this.listenForMouseEvents==false) return;
        this.xDown = evt.clientX;
        this.yDown = evt.clientY;
        this.isMouseDown = true;
    }

    handleMouseUp(evt) {
        if (this.isMouseDown == false) return;
        const deltaX = evt.clientX - this.xDown;
        const deltaY = evt.clientY - this.yDown;
        const distMoved = Math.abs(Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY);
        const activePct = distMoved / this.element.offsetWidth;

        if (activePct > this.options.triggerPercent) {
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                deltaX < 0 ? this.trigger('SWIPE_LEFT') : this.trigger('SWIPE_RIGHT');
            } else {
                deltaY > 0 ? this.trigger('SWIPE_UP') : this.trigger('SWIPE_DOWN');
            }
        }
    }

    handleTouchEnd(evt) {
        const deltaX = evt.changedTouches[0].clientX - this.xDown;
        const deltaY = evt.changedTouches[0].clientY - this.yDown;
        const distMoved = Math.abs(Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY);
        const activePct = distMoved / this.element.offsetWidth;

        if (activePct > this.options.triggerPercent) {
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                deltaX < 0 ? this.trigger('SWIPE_LEFT') : this.trigger('SWIPE_RIGHT');
            } else {
                deltaY > 0 ? this.trigger('SWIPE_UP') : this.trigger('SWIPE_DOWN');
            }
        }
    }
}

// add a listener on load
window.addEventListener("load", function(event) {
    const dispatcher = new SwipeEventDispatcher(document.body);
    dispatcher.on('SWIPE_RIGHT', () => { console.log('I swiped right!') })
    dispatcher.on('SWIPE_LEFT', () => { console.log('I swiped left!') })
});
1.21 gigawatts
  • 16,517
  • 32
  • 123
  • 231
4

If anyone is trying to use jQuery Mobile on Android and has problems with JQM swipe detection

(I had some on Xperia Z1, Galaxy S3, Nexus 4 and some Wiko phones too) this can be useful :

 //Fix swipe gesture on android
    if(android){ //Your own device detection here
        $.event.special.swipe.verticalDistanceThreshold = 500
        $.event.special.swipe.horizontalDistanceThreshold = 10
    }

Swipe on android wasn't detected unless it was a very long, precise and fast swipe.

With these two lines it works correctly

Rayjax
  • 7,494
  • 11
  • 56
  • 82
  • I also needed to add: `$.event.special.swipe.scrollSupressionThreshold = 8;` but you put me in the right direction! Thank you! – Philip G Dec 26 '14 at 11:39
4

I had trouble with touchend handler firing continuously while the user was dragging a finger around. I don't know if that's due to something I'm doing wrong or not but I rewired this to accumulate moves with touchmove and touchend actually fires the callback.

I also needed to have a large number of these instances and so I added enable/disable methods.

And a threshold where a short swipe doesn't fire. Touchstart zero's the counters each time.

You can change the target_node on the fly. Enable on creation is optional.

/** Usage: */
touchevent = new Modules.TouchEventClass(callback, target_node);
touchevent.enable();
touchevent.disable();

/** 
*
*   Touch event module
*
*   @param method   set_target_mode
*   @param method   __touchstart
*   @param method   __touchmove
*   @param method   __touchend
*   @param method   enable
*   @param method   disable
*   @param function callback
*   @param node     target_node
*/
Modules.TouchEventClass = class {

    constructor(callback, target_node, enable=false) {

        /** callback function */
        this.callback = callback;

        this.xdown = null;
        this.ydown = null;
        this.enabled = false;
        this.target_node = null;

        /** move point counts [left, right, up, down] */
        this.counts = [];

        this.set_target_node(target_node);

        /** Enable on creation */
        if (enable === true) {
            this.enable();
        }

    }

    /** 
    *   Set or reset target node
    *
    *   @param string/node target_node
    *   @param string      enable (optional)
    */
    set_target_node(target_node, enable=false) {

        /** check if we're resetting target_node */
        if (this.target_node !== null) {

            /** remove old listener */
           this.disable();
        }

        /** Support string id of node */
        if (target_node.nodeName === undefined) {
            target_node = document.getElementById(target_node);
        }

        this.target_node = target_node;

        if (enable === true) {
            this.enable();
        }
    }

    /** enable listener */
    enable() {
        this.enabled = true;
        this.target_node.addEventListener("touchstart", this.__touchstart.bind(this));
        this.target_node.addEventListener("touchmove", this.__touchmove.bind(this));
        this.target_node.addEventListener("touchend", this.__touchend.bind(this));
    }

    /** disable listener */
    disable() {
        this.enabled = false;
        this.target_node.removeEventListener("touchstart", this.__touchstart);
        this.target_node.removeEventListener("touchmove", this.__touchmove);
        this.target_node.removeEventListener("touchend", this.__touchend);
    }

    /** Touchstart */
    __touchstart(event) {
        event.stopPropagation();
        this.xdown = event.touches[0].clientX;
        this.ydown = event.touches[0].clientY;

        /** reset count of moves in each direction, [left, right, up, down] */
        this.counts = [0, 0, 0, 0];
    }

    /** Touchend */
    __touchend(event) {
        let max_moves = Math.max(...this.counts);
        if (max_moves > 500) { // set this threshold appropriately
            /** swipe happened */
            let index = this.counts.indexOf(max_moves);
            if (index == 0) {
                this.callback("left");
            } else if (index == 1) {
                this.callback("right");
            } else if (index == 2) {
                this.callback("up");
            } else {
                this.callback("down");
            }
        }
    }

    /** Touchmove */
    __touchmove(event) {

        event.stopPropagation();
        if (! this.xdown || ! this.ydown) {
            return;
        }

        let xup = event.touches[0].clientX;
        let yup = event.touches[0].clientY;

        let xdiff = this.xdown - xup;
        let ydiff = this.ydown - yup;

        /** Check x or y has greater distance */
        if (Math.abs(xdiff) > Math.abs(ydiff)) {
            if (xdiff > 0) {
                this.counts[0] += Math.abs(xdiff);
            } else {
                this.counts[1] += Math.abs(xdiff);
            }
        } else {
            if (ydiff > 0) {
                this.counts[2] += Math.abs(ydiff);
            } else {
                this.counts[3] += Math.abs(ydiff);
            }
        }
    }
}
3

If you just need swipe, you are better off size wise just using only the part you need. This should work on any touch device.

This is ~450 bytes' after gzip compression, minification, babel etc.

I wrote the below class based on the other answers, it uses percentage moved instead of pixels, and a event dispatcher pattern to hook/unhook things.

Use it like so:

const dispatcher = new SwipeEventDispatcher(myElement);
dispatcher.on('SWIPE_RIGHT', () => { console.log('I swiped right!') })

export class SwipeEventDispatcher {
 constructor(element, options = {}) {
  this.evtMap = {
   SWIPE_LEFT: [],
   SWIPE_UP: [],
   SWIPE_DOWN: [],
   SWIPE_RIGHT: []
  };

  this.xDown = null;
  this.yDown = null;
  this.element = element;
  this.options = Object.assign({ triggerPercent: 0.3 }, options);

  element.addEventListener('touchstart', evt => this.handleTouchStart(evt), false);
  element.addEventListener('touchend', evt => this.handleTouchEnd(evt), false);
 }

 on(evt, cb) {
  this.evtMap[evt].push(cb);
 }

 off(evt, lcb) {
  this.evtMap[evt] = this.evtMap[evt].filter(cb => cb !== lcb);
 }

 trigger(evt, data) {
  this.evtMap[evt].map(handler => handler(data));
 }

 handleTouchStart(evt) {
  this.xDown = evt.touches[0].clientX;
  this.yDown = evt.touches[0].clientY;
 }

 handleTouchEnd(evt) {
  const deltaX = evt.changedTouches[0].clientX - this.xDown;
  const deltaY = evt.changedTouches[0].clientY - this.yDown;
  const distMoved = Math.abs(Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY);
  const activePct = distMoved / this.element.offsetWidth;

  if (activePct > this.options.triggerPercent) {
   if (Math.abs(deltaX) > Math.abs(deltaY)) {
    deltaX < 0 ? this.trigger('SWIPE_LEFT') : this.trigger('SWIPE_RIGHT');
   } else {
    deltaY > 0 ? this.trigger('SWIPE_UP') : this.trigger('SWIPE_DOWN');
   }
  }
 }
}

export default SwipeEventDispatcher;
Vargr
  • 1,558
  • 1
  • 11
  • 10
3

I have merged a few of the answers too, mostly the first one and the second with classes, and here is my version:

export default class Swipe {
    constructor(options) {
        this.xDown = null;
        this.yDown = null;

        this.options = options;

        this.handleTouchStart = this.handleTouchStart.bind(this);
        this.handleTouchMove = this.handleTouchMove.bind(this);

        document.addEventListener('touchstart', this.handleTouchStart, false);
        document.addEventListener('touchmove', this.handleTouchMove, false);

    }

    onLeft() {
        this.options.onLeft();
    }

    onRight() {
        this.options.onRight();
    }

    onUp() {
        this.options.onUp();
    }

    onDown() {
        this.options.onDown();
    }

    static getTouches(evt) {
        return evt.touches      // browser API

    }

    handleTouchStart(evt) {
        const firstTouch = Swipe.getTouches(evt)[0];
        this.xDown = firstTouch.clientX;
        this.yDown = firstTouch.clientY;
    }

    handleTouchMove(evt) {
        if ( ! this.xDown || ! this.yDown ) {
            return;
        }

        let xUp = evt.touches[0].clientX;
        let yUp = evt.touches[0].clientY;

        let xDiff = this.xDown - xUp;
        let yDiff = this.yDown - yUp;


        if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/
            if ( xDiff > 0 && this.options.onLeft) {
                /* left swipe */
                this.onLeft();
            } else if (this.options.onRight) {
                /* right swipe */
                this.onRight();
            }
        } else {
            if ( yDiff > 0 && this.options.onUp) {
                /* up swipe */
                this.onUp();
            } else if (this.options.onDown){
                /* down swipe */
                this.onDown();
            }
        }

        /* reset values */
        this.xDown = null;
        this.yDown = null;
    }
}

Afterward can use it as the following:

let swiper = new Swipe({
                    onLeft() {
                        console.log('You swiped left.');
                    }
});

It helps to avoid console errors when you want to call only let's say "onLeft" method.

2

Used two:

jQuery mobile: work in most of cases and specially when you are developing applicaion which uses other jQuery plugin then better to use jQuery mobile controls for this. Visit it here: https://www.w3schools.com/jquerymobile/jquerymobile_events_touch.asp

Hammer Time ! one of the best,lightweight and fast javascript based library. Visit it here: https://hammerjs.github.io/

Star
  • 3,222
  • 5
  • 32
  • 48
2

I reworked @givanse's solution to function as a React hook. Input is some optional event listeners, output is a functional ref (needs to be functional so the hook can re-run when/if the ref changes).

Also added in vertical/horizontal swipe threshold param, so that small motions don't accidentally trigger the event listeners, but these can be set to 0 to mimic original answer more closely.

Tip: for best performance, the event listener input functions should be memoized.

function useSwipeDetector({
    // Event listeners.
    onLeftSwipe,
    onRightSwipe,
    onUpSwipe,
    onDownSwipe,

    // Threshold to detect swipe.
    verticalSwipeThreshold = 50,
    horizontalSwipeThreshold = 30,
}) {
    const [domRef, setDomRef] = useState(null);
    const xDown = useRef(null);
    const yDown = useRef(null);

    useEffect(() => {
        if (!domRef) {
            return;
        }

        function handleTouchStart(evt) {
            const [firstTouch] = evt.touches;
            xDown.current = firstTouch.clientX;
            yDown.current = firstTouch.clientY;
        };

        function handleTouchMove(evt) {
            if (!xDown.current || !yDown.current) {
                return;
            }

            const [firstTouch] = evt.touches;
            const xUp = firstTouch.clientX;
            const yUp = firstTouch.clientY;
            const xDiff = xDown.current - xUp;
            const yDiff = yDown.current - yUp;

            if (Math.abs(xDiff) > Math.abs(yDiff)) {/*most significant*/
                if (xDiff > horizontalSwipeThreshold) {
                    if (onRightSwipe) onRightSwipe();
                } else if (xDiff < -horizontalSwipeThreshold) {
                    if (onLeftSwipe) onLeftSwipe();
                }
            } else {
                if (yDiff > verticalSwipeThreshold) {
                    if (onUpSwipe) onUpSwipe();
                } else if (yDiff < -verticalSwipeThreshold) {
                    if (onDownSwipe) onDownSwipe();
                }
            }
        };

        function handleTouchEnd() {
            xDown.current = null;
            yDown.current = null;
        }

        domRef.addEventListener("touchstart", handleTouchStart, false);
        domRef.addEventListener("touchmove", handleTouchMove, false);
        domRef.addEventListener("touchend", handleTouchEnd, false);

        return () => {
            domRef.removeEventListener("touchstart", handleTouchStart);
            domRef.removeEventListener("touchmove", handleTouchMove);
            domRef.removeEventListener("touchend", handleTouchEnd);
        };
    }, [domRef, onLeftSwipe, onRightSwipe, onUpSwipe, onDownSwipe, verticalSwipeThreshold, horizontalSwipeThreshold]);

    return (ref) => setDomRef(ref);
};
Ruben Martinez Jr.
  • 3,199
  • 5
  • 42
  • 76
2

On top of what was suggested here, I would keep track of finger numbers, because if you touch with two fingers at the same time, it will pick up the X position without the ~swipe~ motion which leads to a weird behavior, and also, you could want to set a "distance" minimum so the user won't trigger the swipe by mistake when touch through your website or application.

//Swipe 
let touchstartX = 0
let touchendX = 0
let fingerCount = 0
    
const checkDirection = () => {

  const distance = 50 //Minimum distance for the swipe to work

  //left
  if (touchendX < touchstartX && (touchstartX - touchendX) > distance ){

  //Do something cool
   
  } 
  //right
  if (touchendX > touchstartX && (touchendX - touchstartX) > distance){

    //Do something cooler
}

document.addEventListener('touchstart', e => {

    fingerCount = e.touches.length
    touchstartX = e.changedTouches[0].clientX  
     
})

document.addEventListener('touchend', e => {

    touchendX = e.changedTouches[0].clientX
    if(fingerCount === 1){ 
        checkDirection() 
    }

})
1

You might have an easier time first implementing it with mouse events to prototype.

There are many answers here, including the top, should be used with caution as they do not consider edge cases especially around bounding boxes.

See:

You will need to experiment to catch edge cases and behaviours such as the pointer moving outside of the element before ending.

A swipe is a very basic gesture which is a higher level of interface pointer interaction processing roughly sitting between processing raw events and handwriting recognition.

There's no single exact method for detecting a swipe or a fling though virtually all generally follow a basic principle of detecting a motion across an element with a threshold of distance and speed or velocity. You might simply say that if there is a movement across 65% of the screen size in a given direction within a given time then it is a swipe. Exactly where you draw the line and how you calculate it is up to you.

Some might also look at it from the perspective of momentum in a direction and how far off the screen it has been pushed when the element is released. This is clearer with sticky swipes where the element can be dragged and then on release will either bounce back or fly off the screen as if the elastic broke.

It's probably ideal to try to find a gesture library that you can either port or reuse that's commonly used for consistency. Many of the examples here are excessively simplistic, registering a swipe as the slightest touch in any direction.

Android would be the obvious choice though has the opposite problem, it's overly complex.

Many people appear to have misinterpreted the question as any movement in a direction. A swipe is a broad and relatively brief motion overwhelmingly in a single direction (though may be arced and have certain acceleration properties). A fling is similar though intends to casually propel an item away a fair distance under its own momentum.

The two are sufficiently similar that some libraries might only provide fling or swipe, which can be used interchangeably. On a flat screen it's difficult to truly separate the two gestures and generally speaking people are doing both (swiping the physical screen but flinging the UI element displayed on the screen).

You best option is to not do it yourself. There are already a large number of JavaScript libraries for detecting simple gestures.

jgmjgm
  • 4,240
  • 1
  • 25
  • 18
0

An example of how to use with offset.

// at least 100 px are a swipe
// you can use the value relative to screen size: window.innerWidth * .1
const offset = 100;
let xDown, yDown

window.addEventListener('touchstart', e => {
  const firstTouch = getTouch(e);

  xDown = firstTouch.clientX;
  yDown = firstTouch.clientY;
});

window.addEventListener('touchend', e => {
  if (!xDown || !yDown) {
    return;
  }

  const {
    clientX: xUp,
    clientY: yUp
  } = getTouch(e);
  const xDiff = xDown - xUp;
  const yDiff = yDown - yUp;
  const xDiffAbs = Math.abs(xDown - xUp);
  const yDiffAbs = Math.abs(yDown - yUp);

  // at least <offset> are a swipe
  if (Math.max(xDiffAbs, yDiffAbs) < offset ) {
    return;
  }

  if (xDiffAbs > yDiffAbs) {
    if ( xDiff > 0 ) {
      console.log('left');
    } else {
      console.log('right');
    }
  } else {
    if ( yDiff > 0 ) {
      console.log('up');
    } else {
      console.log('down');
    }
  }
});

function getTouch (e) {
  return e.changedTouches[0]
}
  • 1
    Currently using this version. How would I prevent this from firing multiple times if swiped in repetition? I utilize this with the animate feature for a sidescrolling form and when I swipe multiple times, things get a bit screwy and my divs start overlapping in the visible area. – NMALM Apr 10 '20 at 19:26
0

I had to write a simple script for a carousel to detect swipe left or right.

I utilised Pointer Events instead of Touch Events.

I hope this is useful to individuals and I welcome any insights to improve my code; I feel rather sheepish to join this thread with significantly superior JS developers.

function getSwipeX({elementId}) {

  this.e               = document.getElementsByClassName(elementId)[0];
  this.initialPosition = 0;
  this.lastPosition    = 0;
  this.threshold       = 200;
  this.diffInPosition  = null;
  this.diffVsThreshold = null;
  this.gestureState    = 0;

  this.getTouchStart = (event) => {
    event.preventDefault();
    if (window.PointerEvent) {
      this.e.setPointerCapture(event.pointerId);
    }
    return this.initalTouchPos = this.getGesturePoint(event);
  }

  this.getTouchMove  = (event) => {
    event.preventDefault();
    return this.lastPosition = this.getGesturePoint(event);
  }

  this.getTouchEnd   = (event) => {
    event.preventDefault();
    if (window.PointerEvent) {
      this.e.releasePointerCapture(event.pointerId);
    }
    this.doSomething();
    this.initialPosition = 0;
  }

  this.getGesturePoint = (event) => {
    this.point = event.pageX
    return this.point;
  }

  this.whatGestureDirection = (event) => {
    this.diffInPosition  = this.initalTouchPos - this.lastPosition;
    this.diffVsThreshold = Math.abs(this.diffInPosition) > this.threshold;
    (Math.sign(this.diffInPosition) > 0) ? this.gestureState = 'L' : (Math.sign(this.diffInPosition) < 0) ? this.gestureState = 'R' : this.gestureState = 'N';
    
    return [this.diffInPosition, this.diffVsThreshold, this.gestureState];
  }

  this.doSomething = (event) => {
    let [gestureDelta,gestureThreshold,gestureDirection] = this.whatGestureDirection();

    // USE THIS TO DEBUG
    console.log(gestureDelta,gestureThreshold,gestureDirection);

    if (gestureThreshold) {
      (gestureDirection == 'L') ? // LEFT ACTION : // RIGHT ACTION
    }
  }

  if (window.PointerEvent) {
    this.e.addEventListener('pointerdown', this.getTouchStart, true);
    this.e.addEventListener('pointermove', this.getTouchMove, true);
    this.e.addEventListener('pointerup', this.getTouchEnd, true);
    this.e.addEventListener('pointercancel', this.getTouchEnd, true);
  }
}

You can call the function using new.

window.addEventListener('load', () => {
  let test = new getSwipeX({
    elementId: 'your_div_here'
  });
})
dimButTries
  • 661
  • 7
  • 15
0

I reworked @ruben-martinez answer for using the amazing solution from @givanse for handling swipe events using custom react hooks.

import React, { useEffect, useRef, useState } from "react";

export default function useSwiper() {
  const [domRef, setDomRef] = useState<any>();

  const xDown: React.MutableRefObject<number | null> = useRef(null);
  const yDown: React.MutableRefObject<number | null> = useRef(null);

  useEffect(() => {
if (!domRef) return;

function getTouches(event: React.TouchEvent<HTMLDivElement>) {
  return event.touches;
}

function handleTouchStart(event: any) {
  const firstTouch = getTouches(event)[0];
  xDown.current = firstTouch.clientX;
  yDown.current = firstTouch.clientY;
}

function handleTouchMove(event: React.TouchEvent<HTMLDivElement>) {
  if (!xDown.current || !yDown.current) return;

  const firstTouch = getTouches(event)[0];
  const xUp = firstTouch.clientX;
  const yUp = firstTouch.clientY;

  const xDiff = xDown.current - xUp;
  const yDiff = yDown.current - yUp;

  if (Math.abs(xDiff) > Math.abs(yDiff)) {
    // handle horizontal swipes
    if (xDiff > 0) {
      // we swiped right
      console.log("right");
    } else {
      // we swiped left
      console.log("left");
    }
  } else {
    // handle vertical swipes
    if (yDiff > 0) {
      // we swiped down
      console.log("down");
    } else {
      // we swiped up
      console.log("up");
    }
  }
}

function handleTouchEnd(event: React.TouchEvent<HTMLDivElement>) {
  xDown.current = null;
  yDown.current = null;
}


  domRef.addEventListener("touchstart", handleTouchStart, false);
  domRef.addEventListener("touchmove", handleTouchMove, false);
  domRef.addEventListener("touchend", handleTouchEnd, false);

return () => {
    domRef.removeEventListener("touchstart", handleTouchStart, false);
    domRef.removeEventListener("touchmove", handleTouchMove, false);
    domRef.removeEventListener("touchend", handleTouchEnd, false);
};
  }, [domRef]);

  return (ref: any) => setDomRef(ref);
}

My major challenge with implementing his answer was not knowing how to bind the swipe element's ref to the ref from the custom hook.

Basically, what is happening is that we return a function from the custom hook. This function would allow us pass in a ref from the element we want to listen to swipe actions on. The custom hook on receipt of the ref then updates the hook state with the element's ref which triggers a re render so we have the actual element!

This functional ref style also allows us to use the hook for multiple elements. As shown below, I wanted to use it for a list of items to enable swipe to delete :)

import useSwiper from "./hooks/useSwipe";

const EntryCard = ({ entry, godMode, reload }: EntryProps) => {
const swiperRef = useSwiper();

const handleEntryClick =
(entry: Entry) => async (event: React.MouseEvent<HTMLDivElement>) => {
  if (!godMode) return;

  try {
    reload((state) => !state);
  } catch (err) {
    console.log("Error deleting entry: ", err);
  }
};

return (
  <div className="item" onClick={handleEntryClick(entry)} ref={swiperRef}>
    <div className="username">{entry.userName}</div>
    <div className="score">{entry.weekScore}</div>
  </div>
 );
};

PS: You can pass in functions to your hook to receive the swipe values. Thank YOU :) Vote if you like :)

Segebee
  • 69
  • 6
0

handle by touchStart and touchEnd :

var handleSwipe = function(elem,callbackOnRight, callbackOnLeft, callbackOnDown, 
      callbackOnUp) => {

        elem.ontouchstart = handleTouchStart;
        elem.ontouchend = handleTouchEnd;

        var xDown = null;
        var yDown = null;

        function getTouches(evt) {
            return evt.touches ||             // browser API
                evt.originalEvent.touches; // jQuery
        }

        function handleTouchStart(evt) {
            const firstTouch = getTouches(evt)[0];
            xDown = firstTouch.clientX;
            yDown = firstTouch.clientY;
        };

        function handleTouchEnd(evt) {
            if (!xDown || !yDown) {
                return;
            }

            var xUp = evt.changedTouches[0].clientX;
            var yUp = evt.changedTouches[0].clientY;

            var xDiff = xDown - xUp;
            var yDiff = yDown - yUp;
            var minDif = 30;

            console.log(`xDiff:${xDiff}, yDiff:${yDiff}`);

            if (Math.abs(xDiff) > Math.abs(yDiff)) {
                if (xDiff > minDif) {
                    if (callbackOnLeft)
                        callbackOnLeft();
                } else if (xDiff < -1 * minDif){
                    if (callbackOnRight)
                        callbackOnRight();
                }
            } else {
                if (yDiff > minDif) {
                    if (callbackOnDown)
                        callbackOnDown();
                } else if (yDiff < -1* minDif){
                    if (callbackOnUp)
                        callbackOnUp();
                }
            }
            
            xDown = null;
            yDown = null;
        };
    }
javad hemati
  • 236
  • 1
  • 2
  • 6
0

You can listen to touchstart and touchend events and compute the direction and force based on the event data (Codepen):

let start = null;
document.addEventListener('touchstart', e => {
  const touch = e.changedTouches[0];
  start = [touch.clientX, touch.clientY];
});
document.addEventListener('touchend', e => {
  const touch = e.changedTouches[0];
  const end = [touch.clientX, touch.clientY];
  document.body.innerText = `${end[0] - start[0]},${end[1] - start[1]}`;
});
Swipe here

Or you can build a more ergonomic API around this same concept (Codepen):

const removeListener = addSwipeRightListener(document, (force, e) => {
  console.info('Swiped right with force: ' + force);
});
// removeListener()

// swipe.js
const {
  addSwipeLeftListener,
  addSwipeRightListener,
  addSwipeUpListener,
  addSwipeDownListener,
} = (function() {
  // <element, {listeners: [...], handleTouchstart, handleTouchend}>
  const elements = new WeakMap();
  
  function readTouch(e) {
    const touch = e.changedTouches[0];
    if (touch == undefined) {
      return null;
    }
    return [touch.clientX, touch.clientY];
  }

  function addListener(element, cb) {
    let elementValues = elements.get(element);
    if (elementValues === undefined) {
      const listeners = new Set();
      const handleTouchstart = e => {
        elementValues.start = readTouch(e);
      };
      const handleTouchend = e => {
        const start = elementValues.start;
        if (start === null) {
          return;
        }
        const end = readTouch(e);
        for (const listener of listeners) {
          listener([end[0] - start[0], end[1] - start[1]], e);
        }
      };
      element.addEventListener('touchstart', handleTouchstart);
      element.addEventListener('touchend', handleTouchend);
      
      elementValues = {
        start: null,
        listeners,
        handleTouchstart,
        handleTouchend, 
      };
      elements.set(element, elementValues);
    }
    elementValues.listeners.add(cb);
    return () => deleteListener(element, cb);
  }
  
  function deleteListener(element, cb) {
    const elementValues = elements.get(element);
    const listeners = elementValues.listeners;
    listeners.delete(cb);
    if (listeners.size === 0) {
      elements.delete(element);
      element.removeEventListener('touchstart', elementValues.handleTouchstart);
      element.removeEventListener('touchend', elementValues.handleTouchend);
    }
  }
  
  function addSwipeLeftListener(element, cb) {
    return addListener(element, (force, e) => {
      const [x, y] = force;
      if (x < 0 && -x > Math.abs(y)) {
        cb(x, e);
      }
    });
  }

  function addSwipeRightListener(element, cb) {
    return addListener(element, (force, e) => {
      const [x, y] = force;
      if (x > 0 && x > Math.abs(y)) {
        cb(x, e);
      }
    });
  }
  
  function addSwipeUpListener(element, cb) {
    return addListener(element, (force, e) => {
      const [x, y] = force;
      if (y < 0 && -y > Math.abs(x)) {
        cb(x, e);
      }
    });
  }
  
  function addSwipeDownListener(element, cb) {
    return addListener(element, (force, e) => {
      const [x, y] = force;
      if (y > 0 && y > Math.abs(x)) {
        cb(x, e);
      }
    });
  }

  return {
    addSwipeLeftListener,
    addSwipeRightListener,
    addSwipeUpListener,
    addSwipeDownListener,
  }
})();

// app.js

function print(direction, force) {
  document.querySelector('#direction').innerText = direction;
  document.querySelector('#data').innerText = force;
}

addSwipeLeftListener(document, (force, e) => {
  print('left', force);
});

addSwipeRightListener(document, (force, e) => {
  print('right', force);
});

addSwipeUpListener(document, (force, e) => {
  print('up', force);
});

addSwipeDownListener(document, (force, e) => {
  print('down', force);
});
<h1>Swipe <span id="direction"></span></h1>
Force (px): <span id="data"></span>
zurfyx
  • 31,043
  • 20
  • 111
  • 145
0

This is JQuery. Ideally you want to skip the swipe action if the swipe wasn't significant enough.

$('.slider')
    .off('touchstart touchend swipedaction')
    .on('touchstart', function (e) {
        //Set the starting point directly on self
        $(this).touchstartX = e.changedTouches[0].screenX;
    })
    .on('touchend', function (e) {
        //Set the end point directly on self
        let $self = $(this);
        $self.touchendX = e.changedTouches[0].screenX;

        // Swipe more than 50px, else don't action it.
        if (Math.abs($self.touchendX - $self.touchstartX) > 50) {
            if ($self.touchendX < $self.touchstartX) {
                $self.trigger('swipedaction', ['left']);
            } else {
                $self.trigger('swipedaction', ['right']);
            }
        } else {
            e.stopPropagation();
        }
    })
    .on('swipedaction', function(e, direction) {
        if (direction === 'left') {
            // Swiped left, move right
        } else {
            // Swiped right, move left
        }
    });
Pierre
  • 8,397
  • 4
  • 64
  • 80
  • I encountered an undefined error on `e.changedTouches[0]`, but changing it to `e.originalEvent.changedTouches[0]` worked (https://stackoverflow.com/a/46790285/6247322). Also, `touchstartX` does not get stored here `$(this).touchstartX = e.changedTouches[0].screenX;`. I had to create a variable outside of the listener (i.e. `let $self`), then within `.on('touchstart', function(e) {` I set the variable `$self = $(this); $self.touchStartX = e.originalEvent.changedTouches[0].screenX;` – limciana May 11 '23 at 02:33
  • 1
    If that is the case for you, you can also replace `$(this)` and `$self` with the selector `$('.slider')` as well. It stores the variables on the JQuery instance of the element. WIth regards to the undefined error you are getting, it is probably the best then to check for both `e.changedTouches` and `e.originalEvent.changedTouches` before using it. – Pierre May 12 '23 at 06:32
-1

Function checks both horizontal and vertical direction to determine which swipe was longer to prevent executing 2 instructions, because it's impossible to make a perfect one-directioned swipe. A swipe always has deviation on X and Y.

let touchstartX = 0;
let touchendX = 0;
let touchstartY = 0;
let touchendY = 0;   

function checkDirection() {
    let difX = touchstartX - touchendX;
    let difY = touchstartY - touchendY;
if (Math.abs(difX) > Math.abs(difY)) {
    if (touchendX < touchstartX) {/*left*/}
    if (touchendX > touchstartX) {/*right*/}
} else {
    if (touchendY < touchstartY) {/*up*/}
    if (touchendY > touchstartY) {/*down*/}
}
};
document.addEventListener('touchstart', e => {
    e.preventDefault();
    touchstartX = e.changedTouches[0].screenX;
    touchstartY = e.changedTouches[0].screenY;
});

document.addEventListener('touchend', e => {
    e.preventDefault();
    touchendX = e.changedTouches[0].screenX;
    touchendY = e.changedTouches[0].screenY;
    checkDirection();
});
-1
class Carousel {
    constructor(carouselWrapper, carouselItems, carouselPrev, carouselNext, dotContainer) {
        this.carouselWrapper = document.querySelectorAll(carouselWrapper);
        this.carouselItems = Array.from(document.querySelectorAll(carouselItems));
        this.carouselPrev = document.querySelector(carouselPrev);
        this.carouselNext = document.querySelector(carouselNext);
        this.dotContainer = document.querySelector(dotContainer);
        this.currentItem = 0;
        this.maxItem = this.carouselItems.length;

        this.isDragging = false;
        this.startPos = 0;
        this.currentTranslate = 0;
        this.prevTranslate = 0;

        this.#init();
    }

    #init() {
        document.addEventListener('keydown', this.#keyBoardHandler.bind(this));

        this.carouselPrev.addEventListener('click', this.#prevSlide.bind(this));
        this.carouselNext.addEventListener('click', this.#nextSlide.bind(this));
        this.dotContainer.addEventListener('click', this.#dotHandler.bind(this))

        this.#createDots();
        this.#gotoSlide(0)
        this.#activeDots(0)

        this.#touchHandler();
        this.#disableoncontextmenu();

    }

    #touchHandler() {
        this.carouselItems.forEach((slide, index) => {
            const img = slide.querySelector('.carousel__bgimg');
            if(!img) return;
            
            img.addEventListener('dragstart', (e) => e.preventDefault());

            slide.addEventListener('touchstart', this.#touchStart.bind(this));
            slide.addEventListener('touchend', this.#touchEnd.bind(this));
            slide.addEventListener('touchmove', this.#touchMove.bind(this));

        });
    }

    #touchStart() {
        this.isDragging = true;
        this.startPos = this.#getpositionX(event);
    }

    #touchMove() {
        if(this.isDragging) {
            const currentPosition = this.#getpositionX(event);
            this.currentTranslate = this.prevTranslate + currentPosition - this.startPos;
        }
    }

    #touchEnd() {
        this.isDragging = false;
        const movedBy = this.currentTranslate - this.prevTranslate;
        if(movedBy < -100) {
            this.#nextSlide();
        };

        if(movedBy > 100) {
            this.#prevSlide();
        };

    }

    #getpositionX(event) {
        return event.type.includes('mouse') ? event.pageX : event.touches[0].clientX;
    }

    #createDots() {
        this.carouselItems.forEach((_, i) => {
            this.dotContainer.insertAdjacentHTML('beforeend', `<div class="bullet" data-slide="${i}"></div>`)
        });
    }

    #activeDots(slide) {
        document.querySelectorAll('.bullet').forEach(function(dot) {
            dot.classList.remove('active');
        });

        document.querySelector(`.bullet[data-slide="${slide}"]`)
            .classList.add('active');
    }

    #gotoSlide(slide) {
        this.carouselWrapper.forEach((s, i) => {
            s.style.transform = `translate3d(${100 * (i - slide)}%, 0px, 0px)`;
        });
    }

    #prevSlide() {
        if(this.currentItem === 0) {
            this.currentItem = this.maxItem - 1;
        } else {
            this.currentItem--;
        };
        this.#gotoSlide(this.currentItem);
        this.#activeDots(this.currentItem);
    }

    #nextSlide() {
        if(this.currentItem === this.maxItem -1) {
            this.currentItem = 0;
        } else {
            this.currentItem++;
        };
        this.#gotoSlide(this.currentItem);
        this.#activeDots(this.currentItem);
    }

    #dotHandler(e) {
        if(e.target.classList.contains('bullet')) {
            const { slide } = e.target.dataset;
            this.#gotoSlide(slide);
            this.#activeDots(slide);
        }
    }

    #keyBoardHandler(e) {
        if(e.keyCode === 39) this.#nextSlide();
        e.keyCode === 37 && this.#prevSlide();
    }

    #disableoncontextmenu() {
        this.carouselWrapper.forEach(function(item) {
            item.oncontextmenu = function(event) {
                event.preventDefault()
                event.stopPropagation()
                return false
            }
        });
    }
}

document.addEventListener('DOMContentLoaded', function() {
    const slider = new Carousel(
        '.carousel__wrapper',
        '.carousel__item',
        '.prev-ctrl',
        '.next-ctrl',
        '.dots',
        );
});
vsam
  • 533
  • 3
  • 23