prevent touchstart when swiping
Asked Answered
T

12

49

I have a scrollable list on a mobile device. They want people to be able to scroll the list via swiping, and also select a row by tapping.

The catch is combining the two. I don't want a row to be selected if you are actually scrolling the list. Here's what I've found:

Doesn't trigger when scrolling:

  • click
  • mouseup

Does trigger when scrolling:

  • mousedown
  • touchstart
  • touchend

The simple solution is to just stick with the click event. But what we're finding is that on certain blackberry devices, there is a VERY noticeable lag between touchstart and it then triggering either click or mouseup. This delay is significant enough to make it unusable on those devices.

So that leaves us with the other options. However, with those options, you can scroll the list without triggering the row you touched to start the scroll.

What is the best practice here to resolve this?

Trilly answered 15/8, 2011 at 19:8 Comment(7)
I have the same issue, I have added a bounty to your question.Kazmirci
had some of the same issues, ended up building native.Slyviasm
I'll write up an answer. What we eventually did was log the position at touchstart, and then log the position at touchend. If they were significantly different, we did nothing as we assumed the intention was to scroll. If they are close, we then check the box assuming it was meant to be a tap. To still allow for click events (needed for keyboards) we would add a class on touchstart that would temporarily block any click events.Trilly
Can you give an example of how to do this with jQuery?Kazmirci
You there, buddy? Drop a reply when you can.Kazmirci
@lolwut I'm here! I have to dig through some old code tonight but hopefully we'll get something to you by end of day.Trilly
@lolwut see my answer. Compare it to Netlight_Digital_Media's answer too, that might be a better option than mine.Trilly
T
65
var touchmoved;
$('button').on('touchend', function(e){
    if(touchmoved != true){
        // button click action
    }
}).on('touchmove', function(e){
    touchmoved = true;
}).on('touchstart', function(){
    touchmoved = false;
});
Traver answered 20/8, 2015 at 14:14 Comment(3)
Doing this somehow cancels the scrolling action for me.Teryn
Nice! The ordering of the events - or better say the assignment of the touchmoved variable is very important. It doesn't work if you switch them around (at least with vanilla JS it doesn't)Baggage
Thank you. I was 5 hors try to find this solution!Mcminn
L
23

What you basically want to do is to detect what is a swipe and what is a click.

We may set some conditions:

  1. Swipe is when you touch at point p1, then move your finger to point p2 while still having the finger on the screen, then releaseing.
  2. A click is when you tap start tapping and end tapping on the same element.

So, if you store the coordinates of where your touchStart occured, you can measure the difference at touchEnd. If the change is large enough, consider it a swipe, otherwise, consider it a click.

Also, if you want to do it really neat, you can also detect which element you are "hovering" over with your finger during a touchMove, and if you're not still at the element on which you started the click, you can run a clickCancel method which removes highlights etc.

// grab an element which you can click just as an example
var clickable = document.getElementById("clickableItem"),
// set up some variables that we need for later
currentElement,
clickedElement;

// set up touchStart event handler
var onTouchStart = function(e) {
    // store which element we're currently clicking on
    clickedElement = this;
    // listen to when the user moves finger
    this.addEventListener("touchMove" onTouchMove);
    // add listener to when touch end occurs
    this.addEventListener("touchEnd", onTouchEnd);
};
// when the user swipes, update element positions to swipe
var onTouchMove = function(e) {
    // ... do your scrolling here

    // store current element
    currentElement = document.elementFromPoint(x, y);
    // if the current element is no longer the same as we clicked from the beginning, remove highlight
    if(clickedElement !== currentElement) {
        removeHighlight(clickedElement);
    }
};
// this is what is executed when the user stops the movement
var onTouchEnd = function(e) {
    if(clickedElement === currentElement) {
        removeHighlight(clickedElement);
        // .... execute click action
    }

    // clean up event listeners
    this.removeEventListener("touchMove" onTouchMove);
    this.removeEventListener("touchEnd", onTouchEnd);
};
function addHighlight(element) {
    element.className = "highlighted";
}
function removeHighlight(element) {
    element.className = "";
}
clickable.addEventListener("touchStart", onTouchStart);

Then, you will have to add listeners to you scrollable element also, but there you won't have to worry about what happens if the finger has moved inbetween touchStart and touchEnd.

var scrollable = document.getElementById("scrollableItem");

// set up touchStart event handler
var onTouchStartScrollable = function(e) {
    // listen to when the user moves finger
    this.addEventListener("touchMove" onTouchMoveScrollable);
    // add listener to when touch end occurs
    this.addEventListener("touchEnd", onTouchEndScrollable);
};
// when the user swipes, update element positions to swipe
var onTouchMoveScrollable = function(e) {
    // ... do your scrolling here
};
// this is what is executed when the user stops the movement
var onTouchEndScrollable = function(e) {
    // clean up event listeners
    this.removeEventListener("touchMove" onTouchMoveScrollable);
    this.removeEventListener("touchEnd", onTouchEndScrollable);
};
scrollable.addEventListener("touchStart", onTouchStartScrollable);

// Simon A.

Laxity answered 20/2, 2012 at 14:39 Comment(0)
T
17

Here's what I eventually came up with to allow for a list of items to be scrollable via swipe, but also each item to be 'triggerable' via a tap. In addition, you can still use with a keyboard (using onclick).

I think this is similar to Netlight_Digital_Media's answer. I need to study that one a bit more.

$(document)
// log the position of the touchstart interaction
.bind('touchstart', function(e){ 
  touchStartPos = $(window).scrollTop();
})
// log the position of the touchend interaction
.bind('touchend', function(e){
  // calculate how far the page has moved between
  // touchstart and end. 
  var distance = touchStartPos - $(window).scrollTop();

  var $clickableItem; // the item I want to be clickable if it's NOT a swipe

  // adding this class for devices that
  // will trigger a click event after
  // the touchend event finishes. This 
  // tells the click event that we've 
  // already done things so don't repeat

  $clickableItem.addClass("touched");      

  if (distance > 20 || distance < -20){
        // the distance was more than 20px
        // so we're assuming they intended
        // to swipe to scroll the list and
        // not selecting a row. 
    } else {
        // we'll assume it was a tap 
        whateverFunctionYouWantToTriggerOnTapOrClick()
    }
});


$($clickableItem).live('click',function(e){
 // for any non-touch device, we need 
 // to still apply a click event
 // but we'll first check to see
 // if there was a previous touch
 // event by checking for the class
 // that was left by the touch event.
if ($(this).hasClass("touched")){
  // this item's event was already triggered via touch
  // so we won't call the function and reset this for
  // the next touch by removing the class
  $(this).removeClass("touched");
} else {
  // there wasn't a touch event. We're
  // instead using a mouse or keyboard
  whateverFunctionYouWantToTriggerOnTapOrClick()
}
});
Trilly answered 21/2, 2012 at 15:19 Comment(5)
Hmm, my iPhone logged an error saying TypeError: 'undefined' is not an object when I tap on a row. I replaced whateverFunctionYouWantToTriggerOnTapOrClick() with alert('tapped'), but that doesn't get called.Kazmirci
yea, that's not working code...you'll need to put in your own logic as needed. Merely an example to show the logic being used.Trilly
lolwut: you will have to fetch your rows and assign them to the variable $clickableItem, otherwise clickableItem will be undefined when you call addClass on it. Like so: var clickableItem = $("#row")[0];Laxity
Another issue I see here is the use of .live() which has been deprecated as of jQuery v1.7 and removed in 1.9. Use .on() instead.Parenthesis
I tested this approach in my project. The only issue is that if the user scroll up, then scroll down (to almost the first position) it's assumed as a tap.Lacewing
P
7

Quoting from DA.:

This is a working example:

var touch_pos;
$(document).on('touchstart', '.action-feature', function(e) {
  e.preventDefault();
  touch_pos = $(window).scrollTop();
}).on('click touchend', '.action-feature', function(e) {
  e.preventDefault();
  if(e.type=='touchend' && (Math.abs(touch_pos-$(window).scrollTop())>3)) return;
  alert("only accessed when it's a click or not a swipe");
});
Psychotic answered 29/5, 2013 at 9:59 Comment(1)
This worked perfectly for me with a few modifications for my use case. Much appreciated!Burkey
F
3

Some of these solutions worked for me, but in the end I found that this lightweight library was simpler to setup.

Tocca.js: https://github.com/GianlucaGuarini/Tocca.js

It's quite flexible and detects touch as well as swipe, double-tap etc.

Farthest answered 27/5, 2014 at 1:8 Comment(1)
Agree. I had a number of elements that had to be clicked twice rather than once in mobile only, so I replaced all 'click' events with 'tap' events, and now desktop & mobile work identically. :)Ingather
A
2

I had the same problem, here's a quick solution which works for me

$(document).on('touchstart', 'button', function(evt){ 
    var oldScrollTop = $(window).scrollTop();
    window.setTimeout( function() {
        var newScrollTop = $(window).scrollTop();
        if (Math.abs(oldScrollTop-newScrollTop)<3) $button.addClass('touchactive');
    }, 200);
});

basically instead of handling touchstart immediately, wait for some milliseconds (200ms in this example), then check the scroll position, had scrollposition changed, then we need not to handle touchstart.

Argentous answered 27/3, 2014 at 15:9 Comment(0)
S
1

I came across this elegant solution that works like a charm using jQuery. My problem was preventing list items from calling their touch start event during scrolling. This should also work for swiping.

  1. bind touchstart to each item that will be scrolled or swiped using a class 'listObject'

    $('.listObject').live('touchstart', touchScroll);
    
  2. Then to each item assign a data-object attr defining the function to be called

    <button class='listObject' data-object=alert('You are alerted !')>Alert Me</button>
    

The following function will effectively differentiate between a tap and scrolling or swiping.

function touchScroll(e){

    var objTarget = $(event.target);

    if(objTarget.attr('data-object')){
        var fn = objTarget.attr('data-object'); //function to call if tapped    
    }   

    if(!touchEnabled){// default if not touch device
        eval(fn);
        console.log("clicked", 1);
        return;
    }

    $(e.target).on('touchend', function(e){
        eval(fn); //trigger the function
        console.log("touchEnd")      
        $(e.target).off('touchend');
    });

    $(e.target).on('touchmove', function(e){
        $(e.target).off('touchend');
        console.log("moved")
    }); 

}
Sims answered 29/5, 2013 at 21:49 Comment(1)
Hmmm, too bad this doesn't eliminate the ghost click on the stock android browser, otherwise this solution would've been golden. Need to experiment more...Catwalk
B
1

I came up with this, since i wanted a global event that also can prevent linking dynamically. So preventDefault() must be callable in the event listener, for this the touch event is referenced.

The detection is like most solutions posted here so far. On the end of the touch check if the element is still the same or if we moved some amount.

$('li a').on('ontouchstart' in document.documentElement === true ? 'touchClick' : 'click', handleClick);

if('ontouchstart' in document.documentElement === true) {
    var clickedEl = null;
    var touchClickEvent = $.Event('touchClick');
    var validClick = false;
    var pos = null;
    var moveTolerance = 15;
    $(window).on('touchstart', function(e) {
        /* only handle touch with one finger */
        if(e.changedTouches.length === 1) {
            clickedEl = document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY);
            pos = {x: e.touches[0].clientX, y: e.touches[0].clientY};
            validClick = true;
        } else {
            validClick = false;
        }
    }).on("touchmove", function(e) {
        var currentEl = document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY);
        if(
            e.changedTouches.length > 1 ||
            (!$(clickedEl).is(currentEl) && !$.contains(clickedEl, currentEl)) ||
            (Math.abs(pos.y - e.touches[0].clientY) > moveTolerance && Math.abs(pos.x - e.touches[0].clientX) > moveTolerance)
        ) {
            validClick = false;
        }
    }).on("touchend", function(e) {
        if(validClick) {
            /* this allowes calling of preventDefault on the touch chain */
            touchClickEvent.originalEvent = e;
            /* target is used in jQuery event delegation */
            touchClickEvent.target = e.target;
            $(clickedEl).trigger(touchClickEvent);
        }
    });
}
Booking answered 15/1, 2021 at 8:49 Comment(0)
C
0

I did this with a bit of a different work around. It's definitely not very elegant and certainly not suited to most situations, but it worked for me.

I have been using jQuery's toggleSlide() to open and close input divs, firing the slide on touchstart. The problem was that when the user wanted to scroll, the touched div would open up. To stop this from happening, (or to reverse it before the user noticed) I added a touchslide event to the document which would close the last touched div.

In more depth, here is a code snippet:

var lastTouched;

document.addEventListener('touchmove',function(){
    lastTouched.hide();
});

$('#button').addEventListener('touchstart',function(){
    $('#slide').slideToggle();
    lastTouched = $('#slide');
});

The global variable stores the last touched div, and if the user swipes, the document.touchmove event hides that div. Sometimes you get a flicker of a div poking out but it works for what I need it to, and is simple enough for me to come up with.

Curia answered 2/7, 2012 at 11:35 Comment(0)
C
0

I use this bit of code so that buttons are only triggered (on touchend) if not being swiped on:

var startY;
var yDistance;

function touchHandler(event) {
    touch = event.changedTouches[0];
    event.preventDefault();
}

$('.button').on("touchstart", touchHandler, true);
$('.button').on("touchmove", touchHandler, true);

$('.button').on("touchstart", function(){
    startY = touch.clientY;
});

$('.button').on('touchend', function(){

    yDistance = startY - touch.clientY;

    if(Math.abs(yDist) < 30){

        //button response here, only if user is not swiping
        console.log("button pressed")
    }
});
Charioteer answered 21/11, 2014 at 21:10 Comment(0)
H
0

jQuery Mobile has a .tap() event which seems to have the behavior you'd expect:

The jQuery Mobile tap event triggers after a quick, complete touch event that occurs on a single target object. It is the gesture equivalent of a standard click event that is triggered on the release state of the touch gesture.

This might not necessarily answer the question, but might be a useful alternative to some.

Heteronomous answered 28/1, 2015 at 18:58 Comment(0)
V
0

if you want to do this for multiple elements and also need mouse and pointer events:

var elems = $('YOURMULTISELECTOR'); // selector for multiple elements
elems.unbind('mousdown pointerdown touchstart touchmove mouseup pointerup touchend');
var elem = null;
elems.on('mousdown pointerdown touchstart', function (e) {
    elem = yourSingleSelector(e);
}).on('touchmove', function (e) {
    elem = null;                
}).on('mouseup pointerup touchend', function (e) { 
    if (elem == yourSingleSelector(e)) {                    
        // do something
    }
});
Vinegarish answered 12/12, 2017 at 7:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.