I'm making an Javascript web app and I can't for the life of me get the touchstart event to fire. I get the touchmove and touchend events no problem. This is a problem because as I see it the best way to distinguish between a tap and a scrolling motion is to zero a counter on the touchstart event, update it at touchmove and then compare it at touchend. I'm doing this so I can do some action at the end of tap but not a scroll. For instance, it would be very confusing if a page opened for an item in a listed after you finished scrolling down that list, but it would be nice to be able to tap on an item to open its page.
This is what I have:
// FIXME: this doesn't seem to ever fire
el.addEventListener('touchstart', function(e) {
// make sure that at the start of every touch we're not considered to be moving
alert("Touch starting");
app.__touchMoving = 0;
}, false);
el.addEventListener('touchmove', function(e) {
app.__touchMoving++;
}, false);
el.addEventListener('touchend', function(e) {
alert("Touch ended. We moved beforehand this many times: " + app.__touchMoving);
// if we are moving
if (app.__touchMoving > 0) {
// stop, since we're dragging, not tapping
return false;
}
// else we're no longer moving, so it was a tap
}
I never see the touchstart alert. If I scroll the touchend will fire and app__touchMoving will have some sort of decent value. On a side note, I've noticed that sometimes the touchend will seem to fire multiple times.
Am I missing something basic here? Plenty of people say that this should work just fine on Android (and iPhone) yet the first listener never seems to fire.
Update: I should mention that I've been testing on a Samsung Galaxy S running Android 2.1.
I don't know if u can use it: iScroll
Related
I have a mobile web app, which uses a lot of click event handlers on buttons, etc. All of this works fine if the user really "clicks" (i.e. "touchdown-touchup") the button. However if the user does a short swipe, then the click event does not fire. This causes a lot of complaints from my users that the app doesn't register clicks/taps and that other apps work correctly.
Of course, I can get coordinates of the touch in ontouchstart and ontouchend and compute the distance - but I need to also know whether that distance is under the maximum that the browser would treat as 'click'. I do not want to switch to using touchstart/touchend events instead of click.
I used to use fastclick.js library for handling clicks/taps in the past, but now use native 'click' events with touch-action: manipulation. Is there any way of specify/controlling the maximum movement of the finger on the button that still registers as a 'click'?
Update based on comments. The application is very large and there are hundreds if not thousands of event handler assignments throughout it (the app has been developed over the last 8 years). Changing all of these is not practical, therefore I'm looking for a solution that would allow me to either set the threshold once globally or solve the problem with a global-like touchstart/touchend handlers.
I thought this was an interesting problem so I took a shot at solving it for you. In a way it's somewhat similar to the problem of preventing a click event when a dblclick happens.
Using a distance threshold for a "short swipe" seems, to me at least, problematic in that the threshold distance might be system dependent. Instead of that I decided to trigger on if the "click" event actually happens. I used mousedown as a simulated touchstart and mouseup as a simulated touchend. mouseup always happens before click so it is similar to touchend in that respect.
Normally if you "click" (mousedown) on an element and then move your mouse pointer off the element, the click event does not happen. This is much like the situation you describe as being a "short swipe". After a certain distance the click event just doesn't happen. The code below will send a click event for the button even if you mousedown on it, move the pointer off it and then mouseup. I believe that this would solve the problem if you used it for touchstart and touchend instead
// The pre-exisiting click handler
function handleClick(ev) {
console.log('button clicked. do work.');
}
document.getElementById('theButton').addEventListener('click', handleClick);
// our global "touch" handlers
var touchHandler = {
curPending: null,
curElem: null,
handleTouch: function handleTouch(ev) {
switch (ev.type) {
case 'mousedown':
// capture the target that the click is being initiated on
touchHandler.curElem = ev.target;
// add an extra click handler so we know if the click event happens
ev.target.addEventListener('click', touchHandler.specialClick);
break;
case 'mouseup':
// start a pending click timer in case the click event doesn't happen
touchHandler.curPending = setTimeout(touchHandler.pendingClick, 1);
break;
}
},
specialClick: function(ev) {
// the click event happened
// clear our extra handler
touchHandler.curElem.removeEventListener('click', touchHandler.specialClick);
// make sure we don't send an extra click event
clearTimeout(touchHandler.curPending);
},
pendingClick: function() {
// we never heard the click event
// clear our extra handler
touchHandler.curElem.removeEventListener('click', touchHandler.specialClick);
// trigger a click event on the element that started it all
touchHandler.curElem.click();
}
};
// using "mousedown" as "touchstart" and "mouseup" as "touchend"
document.addEventListener('mouseup', touchHandler.handleTouch);
document.addEventListener('mousedown', touchHandler.handleTouch);
<p>I work when clicked normally but I also work when
mousedown, drag pointer off me, mouseup</p>
<button id="theButton">Click Me</button>
So, I have a problem. I want to respond to a user pressing the mouse button (on desktop) or touching a div (on mobile). I'm trying to be compatile with evergreen browsers. This is what I tried so far:
listen only to mouseDown event. This works on desktop but doesn't work in mobile if the user is dragging. I want the handler to be called as soon as the user touches the screen, no matter if they're moving their finger in the process.
listen only to touchStart event. This works on mobile and desktop, except for Edge and Safari desktop, which don't support touch events.
listen to both, then preventDefault. This causes a double handler call on Chrome mobile. It seems that touch events are passive to allow uninterrupted scrolling on mobile Chrome, so preventDefualt has no effect on them . What I get is a warning message saying "[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080" in the console, preventDefault is ignored and my event is called twice.
Obviously this can be solved by sniffing touch events, but the net is full of self-righteous rants on how one has to be device-agnostic and that it's dangerous to detect touch events before the user interacted.
So I guess that the question is: is there a way to do what I want to do without sniffing for touch events?
Below my sample React code:
function handler(e) {
console.log('handler called')
e.preventDefault()
}
export default function MyElement() {
return (
<div
onMouseDown={handler}
onTouchStart={handler}
>
Hello
</div>
)
}
It turn out it's not yet possible in React. One workaround is set a flag the first time touchStart it's received.
touchHandler = () => {
this.useTouch = true
this.realHandler()
}
mouseHandler = () => {
if (this.useTouch) return
this.realHandler()
}
With the caveat that the first touchStart can be lost in case of dragging.
Quite disappointing.
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?
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.
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!
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.
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...
});
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.
}));
For me using 'onclick' in the html element itself, worked for both touch and click.
<div onclick="cardClicked(this);">Click or Touch Me</div>
Solution: https://github.com/alexgibson/tap.js
I have a conflict between 'touchend' and 'touchmove' events on the iPad in mobile Safari. I have images sitting next to each other like a gallery and they have a 'touchend' event attached to flip when touchend. However, you can also slide from one image to the other (like on iPhone sliding home screen to the next screen).
Now I can't figure out how to prevent the 'touchend' event from firing when I want to slide to the next image. Obviously, I don't want the image to flip if I slide, only if I tap.
My solution so far:
var img = $('.show-video');
var sliding = false;
img.bind('touchend', function(e) {
if (sliding === false){
Animate($(this), 'flip');
}
});
img.bind('touchmove', function(){
sliding = true;
$(this).bind('touchend', function(){
window.setTimeout(function(){
sliding = false;
}, 200)
})
});
````
I think this can be done much better.
You basically have two events when it comes to tapping the screen: touchstart and touchend. When a user touches the screen, the first event is captured, when he stops touching the screen, the second event is fired. The touchmove event should be captured to analyse the movement of the finger.
I'm not entirely sure what you're trying to say, but I'm assuming you you are capturing those events. What you can do is:
Instead of doing something on touchstart do it on touchend. This is more natural anyway, since something should happen after you lift the finger
After touchstart see if there is any movement using touchmove. If none and touchend is called, do what you wanted to do for a tap. If there was movement, do whatever you wanted to do for sliding.
I hope this helps.
I've made a workaround on this once.
You been already explained what are the events that are happening, and also you have a clue as a question.
For example lets say you have your gallery on a #galleryOverlay, and you scroller is inside it.
Then all you have to do is something like
$('#galleryOverlay').bind('touchstart touchmove touchmove', function(e){e.stopPropagation();})
So this way you keep your events inside your gallery, but you prevent them to happen outside.
Good luck
This library adds a 'tap' event which you can use to watch for a "tap" instead of "touchend".
https://github.com/alexgibson/tap.js
I am trying to implement a scrollable element for a mobile app and it looks like you must use preventDefault on the initial touchStart event, otherwise the browser will not fire all the touchMove events (presumably for performance reasons).
So it would seem that if I want to allow touch scrolling on an overflown element, the user will not be able to scroll the page as per usual when touching that element. This is problematic if the overflown element takes up a large portion of the viewport.
Is their a workaround for this?
Take a look at this library
http://api.mutado.com/mobile/mtdtouch/js/
The "core" javascript includes a base UIComponent optimized for touch events (webkit).
The UI.Scroll component in example manage the "prevent default issue" for you.
Try to subclass the UIComponent and implement your own events handler like this
$MTD.YourOwnComponent = $.klass( $MTD.UIComponent, {
touchesBegan: function( e ) {
// your stuff
},
touchesMoved: function( e ) {
// your stuff
},
touchesEnded: function( e ) {
// your stuff
}
});
Hope this helps.
Here's a simple workaround: drop the touchstart handler. You can reconstruct most of what's going on with just the touchmove, touchend, and touchleaving handlers.
In the browser I tested with (Chrome), scrolling happens as long as you don't have a touchstart handler; it doesn't care about the other ones. As long as you aren't actively calling ev.preventDefault in the touchmove handlers, scrolling works.
Assuming what you want to do will work fine despite only finding out about a touch when the finger starts moving, instead of when the finger initially lands, this workaround should work acceptably.
... And also I'm assuming other browsers use the same logic as Chrome.