JavaScript touchend versus click dilemma
Asked Answered
L

6

57

I am working on some javascript UI, and using a lot of touch events like 'touchend' for improved response on touch devices. However, there are some logical issues which are bugging me ...

I have seen that many developers mingle 'touchend' and 'click' in the same event. In many cases it will not hurt, but essentially the function would fire twice on touch devices:

button.on('click touchend', function(event) {
  // this fires twice on touch devices
});

It has been suggested that one could detect touch capability, and set the event appropriately for example:

var myEvent = ('ontouchstart' in document.documentElement) ? 'touchend' : 'click';
button.on(myEvent, function(event) {
  // this fires only once regardless of device
});

The problem with the above, is that it will break on devices that support both touch and mouse. If the user is currently using mouse on a dual-input device, the 'click' will not fire because only 'touchend' is assigned to the button.

Another solution is to detect the device (e.g. "iOS") and assign an event based on that: Click event called twice on touchend in iPad. Of course, the solution in the link above is only for iOS (not Android or other devices), and seems more like a "hack" to solve something quite elementary.

Another solution would be to detect mouse-motion, and combine it with touch-capability to figure out if the user is on mouse or touch. Problem of course being that the user might not be moving the mouse from when you want to detect it ...

The most reliable solution I can think of, is to use a simple debounce function to simply make sure the function only triggers once within a short interval (for example 100ms):

button.on('click touchend', $.debounce(100, function(event) {
  // this fires only once on all devices
}));

Am I missing something, or does anyone have any better suggestions?

Edit: I found this link after my post, which suggests a similar solution as the above: How to bind 'touchstart' and 'click' events but not respond to both?

Lacedaemon answered 29/8, 2014 at 16:7 Comment(2)
That solves nothing ... Modernizr just detects 'touch'. The event will still fire twice on touch devices. Or if you assign 'touch' event for touch devices (through modernizr), it will stop working for users using the mouse on dual-input devices like many windows8 devices.Lacedaemon
Nowadays, couldn't you use the pointerup event in order to only emit once independent on the device used.Euphorbia
L
35

After a day of research, I figured the best solution is to just stick to click and use https://github.com/ftlabs/fastclick to remove the touch delay. I am not 100% sure this is as efficient as touchend, but not far from at least.

I did figure out a way to disable triggering events twice on touch by using stopPropagation and preventDefault, but this is dodgy as it could interfere with other touch gestures depending on the element where it is applied:

button.on('touchend click', function(event) {
  event.stopPropagation();
  event.preventDefault();
  // this fires once on all devices
});

I was in fact looking for a solution to combine touchstart on some UI elements, but I can't see how that can be combined with click other than the solution above.

Lacedaemon answered 30/8, 2014 at 15:14 Comment(2)
is there a native way to detect if a browser supports touchstart/end?Naker
How were you able to get this to work on the same button element without a parent wrapper for e.stopPropagation()? I've tried this by attaching (click) and (touchend) via angular with e.stopPropagation() on the method they both attach to and it still fires twice.Resuscitator
E
11

This question is answered but maybe needs to be updated.

According to a notice from Google, there will be no 300-350ms delay any more if we include the line below in the <head> element.

<meta name="viewport" content="width=device-width">

That's it! And there will be no difference between click and touch event anymore!

Effeminate answered 10/2, 2017 at 3:25 Comment(4)
That's interesting. Perhaps time to get rid of fastclick.js ... I will test it a bit in mobile browsers.Lacedaemon
Meta tags should be placed in the head tag, not the header tag.Vernonvernor
This has not been true for me.Silkstocking
how would not having a 300ms delay solve the issue of not having to need both click and touchend events? I heard that some android devices don't fire on the click event.Resuscitator
C
0

Yes disabling double-tap zoom (and hence the click delay) is usually the best option. And we finally have good advice for doing this that will soon work on all browsers.

If, for some reason, you don't want to do that. You can also use UIEvent.sourceCapabilities.firesTouchEvents to explicitly ignore the redundant click. The polyfill for this does something similar to your debouncing code.

Cytokinesis answered 4/1, 2016 at 17:2 Comment(1)
polyfill link is gone nowPathoneurosis
V
0

Hello you can implement the following way.

function eventHandler(event, selector) {
    event.stopPropagation(); // Stop event bubbling.
    event.preventDefault(); // Prevent default behaviour
    if (event.type === 'touchend') selector.off('click'); // If event type was touch turn off clicks to prevent phantom clicks.
}

// Implement
$('.class').on('touchend click', function(event) {
    eventHandler(event, $(this)); // Handle the event.
    // Do somethings...
});
Vasty answered 12/5, 2017 at 14:29 Comment(0)
E
0

Your debounce function will delay handling of every click for 100 ms:

button.on('click touchend', $.debounce(100, function(event) {
  // this is delayed a minimum of 100 ms
}));

Instead, I created a cancelDuplicates function that fires right away, but any subsequent calls within 10 ms will be cancelled:

function cancelDuplicates(fn, threshhold, scope) {
    if (typeof threshhold !== 'number') threshhold = 10;
    var last = 0;

    return function () {
        var now = +new Date;

        if (now >= last + threshhold) {
            last = now;
            fn.apply(scope || this, arguments);
        }
    };
}

Usage:

button.on('click touchend', cancelDuplicates(function(event) {
  // This fires right away, and calls within 10 ms after are cancelled.
}));
Expugnable answered 19/5, 2017 at 3:3 Comment(0)
G
0

For me using 'onclick' in the html element itself, worked for both touch and click.

<div onclick="cardClicked(this);">Click or Touch Me</div>
Gildagildas answered 18/11, 2022 at 19:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.