On virtually all current browsers (extensive details from patrickhlauke on github, which I summarised in an SO answer, and also some more info from QuirksMode), touchscreen touches trigger mouseover events (sometimes creating an invisible pseudo-cursor that stays where the user touched until they touch elsewhere).
Sometimes this causes undesirable behaviour in cases where touch/click and mouseover are intended to do different things.
From inside a function responding to a mouseover event, that has been passed the event object, is there any way I can check if this was a "real" mouseover from a moving cursor that moved from outside an element to inside it, or if it was caused by this touchscreen behaviour from a touchscreen touch?
The event object looks identical. For example, on chrome, a mouseover event caused by a user touching a touchscreen has type: "mouseover" and nothing I can see that would identify it as touch related.
I had the idea of binding an event to touchstart that alters mouseover events then an event to touchend that removes this alteration. Unfortunately, this doesn't work, because the event order appears to be touchstart → touchend → mouseover → click (I can't attach the normalise-mouseover function to click without messing up other functionality).
I'd expected this question to have been asked before but existing questions don't quite cut it:
How to handle mouseover and mouseleave events in Windows 8.1 Touchscreen is about C# / ASP.Net applications on Windows, not web pages in a browser
JQuery .on(“click”) triggers “mouseover” on touch device is similar but is about jQuery and the answer is a bad approach (guessing a hard-coded list of touchscreen user agents, which would break when new device UAs are created, and which falsely assumes all devices are mouse or touchscreen)
Preventing touch from generating mouseOver and mouseMove events in Android browser is the closest I could find, but it is only about Android, is about preventing not identifying mouseover on touch, and has no answer
Browser handling mouseover event for touch devices causes wrong click event to fire is related, but they're trying to elumate the iOS two-tap interaction pattern, and also the only answer makes that mistake of assuming that touches and mouse/clicks are mutually exclusive.
The best I can think of is to have a touch event that sets some globally accessible variable flag like, say, window.touchedRecently = true; on touchstart but not click, then removes this flag after, say, a 500ms setTimeout. This is an ugly hack though.
Note - we cannot assume that touchscreen devices have no mouse-like roving cursor or visa versa, because there are many devices that use a touchscreen and mouse-like pen that moves a cursor while hovering near the screen, or that use a touchscreen and a mouse (e.g. touchscreen laptops). More details in my answer to How do I detect whether a browser supports mouseover events?.
Note #2 - this is not a jQuery question, my events are coming from Raphael.js paths for which jQuery isn't an option and which give a plain vanilla browser event object. If there is a Raphael-specific solution I'd accept that, but it's very unlikely and a raw-javascript solution would be better.
Given the complexity of the issue, I thought it was worth detailing the issues and edge cases involved in any potential solution.
The issues:
1 - Different implementations of touch events across devices and browsers. What works for some will definitely not work for others. You only need to glance at those patrickhlauke resources to get an idea of how differently the process of tapping a touch-screen is currently handled across devices and browsers.
2 - The event handler gives no clue as to its initial trigger. You are also absolutely right in saying that the event object is identical (certainly in the vast majority of cases) between mouse events dispatched by interaction with a mouse, and mouse events dispatched by a touch interaction.
3 - Any solution to this problem which covers all devices could well be short-lived as the current W3C Recommendations do not go into enough detail on how touch/click events should be handled (https://www.w3.org/TR/touch-events/), so browsers will continue to have different implementations. It also appears that the Touch Events standards document has not changed in the past 5 years, so this isn't going to fix itself soon. https://www.w3.org/standards/history/touch-events
4 - Ideally, solutions should not use timeouts as there is no defined time from touch event to mouse event, and given the spec, there most probably won't be any time soon. Unfortunately, timeouts are almost inevitable as I will explain later.
A future solution:
In the future, the solution will probably be to use Pointer Events instead of mouse / touch events as these give us the pointerType (https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events), but unfortunately we're not there yet in terms of an established standard, and so cross-browser compatibility (https://caniuse.com/#search=pointer%20events) is poor.
How do we solve this at the moment
If we accept that:
You can't detect a touchscreen (http://www.stucox.com/blog/you-cant-detect-a-touchscreen/)
Even if we could, there's still the issue of non-touch events on a touch capable screen
Then we can only use data about the mouse event itself to determine its origin. As we've established, the browser doesn't provide this, so we need to add it ourselves. The only way to do this is using the touch events which are triggered around the same time as the mouse event.
Looking at the patrickhlauke resources again, we can make some statements:
mouseover is always followed by the click events mousedown mouseup and click - always in that order. (Sometimes separated by other events). This is backed up by the W3C recommendations: https://www.w3.org/TR/touch-events/.
For most devices / browsers, the mouseover event is always preceded by either pointerover, its MS counterpart MSPointerOver, or touchstart
The devices / browsers whose event order begins with mouseover have to be ignored. We can't establish that the mouse event was triggered by a touch event before the touch event itself has been triggered.
Given this, we could set a flag during pointerover, MSPointerOver, and touchstart, and remove it during one of the click events. This would work well, except for a handfull of cases:
event.preventDefault is called on one of the touch events - the flag will never be unset as the click events will not be called, and so any future genuine click events on this element would still be marked as a touch event
if the target element is moved during the event. The W3C Recommendations state
If the contents of the document have changed during processing of the
touch events, then the user agent may dispatch the mouse events to a
different target than the touch events.
Unfortunately this means that we will always need to use timeouts. To my knowledge there is no way of either establishing when a touch event has called event.preventDefault, nor understanding when the touch element has been moved within the DOM and the click event triggered on another element.
I think this is a fascinating scenario, so this answer will be amended shortly to contain a recommended code response. For now, I would recommend the answer provided by #ibowankenobi or the answer provided by #Manuel Otto.
What we do know is:
When the user uses no mouse
the mouseover is directly (within 800ms) fired after either a touchend or a
touchstart (if the user tapped and held).
the position of the mouseover and the touchstart/touchend are identical.
When the user uses a mouse/pen
The mouseover is fired before the touch events, even if not, the position of the mouseover will not match the touch events' position 99% of time.
Keeping these points in mind, I made a snippet, which will add a flag triggeredByTouch = true to the event if the listed conditions are met. Additionally you can add this behaviour to other mouse events or set kill = true in order to discard mouseevents triggered by touch completely.
(function (target){
var keep_ms = 1000 // how long to keep the touchevents
var kill = false // wether to kill any mouse events triggered by touch
var touchpoints = []
function registerTouch(e){
var touch = e.touches[0] || e.changedTouches[0]
var point = {x:touch.pageX,y:touch.pageY}
touchpoints.push(point)
setTimeout(function (){
// remove touchpoint from list after keep_ms
touchpoints.splice(touchpoints.indexOf(point),1)
},keep_ms)
}
function handleMouseEvent(e){
for(var i in touchpoints){
//check if mouseevent's position is (almost) identical to any previously registered touch events' positions
if(Math.abs(touchpoints[i].x-e.pageX)<2 && Math.abs(touchpoints[i].y-e.pageY)<2){
//set flag on event
e.triggeredByTouch = true
//if wanted, kill the event
if(kill){
e.cancel = true
e.returnValue = false
e.cancelBubble = true
e.preventDefault()
e.stopPropagation()
}
return
}
}
}
target.addEventListener('touchstart',registerTouch,true)
target.addEventListener('touchend',registerTouch,true)
// which mouse events to monitor
target.addEventListener('mouseover',handleMouseEvent,true)
//target.addEventListener('click',handleMouseEvent,true) - uncomment or add others if wanted
})(document)
Try it out:
function onMouseOver(e){
console.log('triggered by touch:',e.triggeredByTouch ? 'yes' : 'no')
}
(function (target){
var keep_ms = 1000 // how long to keep the touchevents
var kill = false // wether to kill any mouse events triggered by touch
var touchpoints = []
function registerTouch(e){
var touch = e.touches[0] || e.changedTouches[0]
var point = {x:touch.pageX,y:touch.pageY}
touchpoints.push(point)
setTimeout(function (){
// remove touchpoint from list after keep_ms
touchpoints.splice(touchpoints.indexOf(point),1)
},keep_ms)
}
function handleMouseEvent(e){
for(var i in touchpoints){
//check if mouseevent's position is (almost) identical to any previously registered touch events' positions
if(Math.abs(touchpoints[i].x-e.pageX)<2 && Math.abs(touchpoints[i].y-e.pageY)<2){
//set flag on event
e.triggeredByTouch = true
//if wanted, kill the event
if(kill){
e.cancel = true
e.returnValue = false
e.cancelBubble = true
e.preventDefault()
e.stopPropagation()
}
return
}
}
}
target.addEventListener('touchstart',registerTouch,true)
target.addEventListener('touchend',registerTouch,true)
// which mouse events to monitor
target.addEventListener('mouseover',handleMouseEvent,true)
//target.addEventListener('click',handleMouseEvent,true) - uncomment or add others if wanted
})(document)
a{
font-family: Helvatica, Arial;
font-size: 21pt;
}
Click me
According to https://www.html5rocks.com/en/mobile/touchandmouse/
For a single click the order of events is:
touchstart
touchmove
touchend
mouseover
mousemove
mousedown
mouseup
click
So you might be able to set some arbitrary boolean isFromTouchEvent = true; in onTouchStart() and isFromTouchEvent = false; in onClick() and check for that inside of onMouseOver(). This doesn't work very well since we're not guaranteed to get all those events in the element that we're trying to listen on.
I usually have couple of general schemes which I use for this, one of them uses a manual principle of setTimeout to trigger a property. I will explain this one here, but first try to reason about using touchstart, touchmove and touchend on touch devices and use mouseover on destop.
As you know, calling event.preventDefault (event has to be not passive for this to work with touchstart) in any of the touchevents will cancel the subsequent mousecalls so you do not need to deal with them. But in case this is not what you want, here is what I use sometimes (I refer as "library" to your dom manipulation library, and "elem" as your element):
with setTimeout
library.select(elem) //select the element
.property("_detectTouch",function(){//add a _detectTouch method that will set a property on the element for an arbitrary time
return function(){
this._touchDetected = true;
clearTimeout(this._timeout);
this._timeout = setTimeout(function(self){
self._touchDetected = false;//set this accordingly, I deal with either touch or desktop so I can make this 10000. Otherwise make it ~400ms. (iOS mouse emulation delay is around 300ms)
},10000,this);
}
}).on("click",function(){
/*some action*/
}).on("mouseover",function(){
if (this._touchDetected) {
/*coming from touch device*/
} else {
/*desktop*/
}
}).on("touchstart",function(){
this._detectTouch();//the property method as described at the beginning
toggleClass(document.body,"lock-scroll",true);//disable scroll on body by overflow-y hidden;
}).on("touchmove",function(){
disableScroll();//if the above overflow-y hidden don't work, another function to disable scroll on iOS.
}).on("touchend",function(){
library.event.preventDefault();//now we call this, if you do this on touchstart chrome will complain (unless not passive)
this._detectTouch();
var touchObj = library.event.tagetTouches && library.event.tagetTouches.length
? library.event.tagetTouches[0]
: library.event.changedTouches[0];
if (elem.contains(document.elementFromPoint(touchObj.clientX,touchObj.clientY))) {//check if we are still on the element.
this.click();//click will never be fired since default prevented, so we call it here. Alternatively add the same function ref to this event.
}
toggleClass(document.body,"lock-scroll",false);//enable scroll
enableScroll();//enableScroll
})
Another option without setTimeout is to think mousover is counter to touchstart and mouseout counter to touchend. So former events (the touch events) will set a property, if the mouse events detect that property then they do not fire and reset the property to its initial value and so on. In that case something along these lines will also do:
without setTimeout
....
.on("mouseover",function(dd,ii){
if (this._touchStarted) {//touch device
this._touchStarted = false;//set it back to false, so that next round it can fire incase touch is not detected.
return;
}
/*desktop*/
})
.on("mouseout",function(dd,ii){//same as above
if(this._touchEnded){
this._touchEnded = false;
return;
}
})
.on("touchstart",function(dd,ii){
this._touchStarted = true;
/*some action*/
})
.on("touchend",function(dd,ii){
library.event.preventDefault();//at this point emulations should not fire at all, but incase they do, we have the attached properties
this._touchEnded = true;
/*some action*/
});
I removed a lot of details but I guess this is the main idea.
You can use modernizr for that! I just tested this on a local development server and it works.
if (Modernizr.touch) {
console.log('Touch Screen');
} else {
console.log('No Touch Screen');
}
So I would start there?
Pointer Events are widely supported now. So now we can use pointerenter and check event.pointerType:
const element = document.getElementById("hoverableElement")
element.addEventListener("pointerenter", (event) => {
if (event.pointerType === "mouse") {
alert("Hovered")
}
})
<div id="hoverableElement">Trigger on hover, but not on touch</div>
I've written a little drag and drop script in Javascript, I'm aiming to make it as efficient as possible (element following the cursor at the exact spot it was grabbed and no lag spikes) to accomplish this you usually try to limit the amount of times your move function gets fired (throttling the mousemove event) but I tried some other method, thing is I'm not sure if it's actualy doing what I'm expecting.
This is how I'm handling it;
Firstly, on mousedown, a requestAnimationFrame loop starts
var handleMovement = function(){
self.ele.style.top = self.dragElPos.pageTop+'px';
self.ele.style.left = self.dragElPos.pageLeft+'px';
self.animFrame = requestAnimationFrame(handleMovement);
}
And secondly, the mousemove event calls this function
var setCoordinates = function (e) {
self.dragElPos.pageTop = (e.pageY-self.dragElPos.innerTop);
self.dragElPos.pageLeft = (e.pageX-self.dragElPos.innerLeft);
}
All this does is store the position in some object, the handleMovement() function retrieves this and due to the animation loop the element gets moved.
The behaviour I'm expecting is that requestAnimationFrame is optimizing the loop so the animation runs smoothly, but because the coordinates are being set by some other function I wonder if it knows when it's optimized.
It looks pretty smooth, but if it's not being optimized I'd rather use mousemove throttling.
P.S. Browser compatibility is no concern.
I'm having problems getting standard javascript timers to clear on mobile devices (Android and iOS, phone and tablet).
My page contains 2 buttons, a play/pause toggle and a stop button (both FontAwesome icons), the simple HTML for which is:
<span class="fa fa-pause control-button" id="pause-button"></span>
<span class="fa fa-stop control-button" id="stop-button"></span>
The interval is initiated with the following function:
var interval = function() {
$('.control-button').fadeIn(300);
//initiate the interval
infiniteInterval = window.setInterval(Tiles.infiniteTick, speed);
};
Where speed is defined in an earlier function (default is 300). infiniteTick is a very simple function which is working fine. I haven't explained it here as it would require an explanation of the whole program but I can provide code if required.
The play and pause toggles are as follows:
$('body').on('click touchstart', '#pause-button', function() {
if ($(this).hasClass('fa-pause')) {
window.clearInterval(infiniteInterval);
$(this).removeClass('fa-pause');
$(this).addClass('fa-play');
} else {
infiniteInterval = window.setInterval(Tiles.infiniteTick, speed);
$(this).removeClass('fa-play');
$(this).addClass('fa-pause');
}
});
Finally, the interval is terminated with this (some purely aesthetic extras removed for simplicity)
$('body').on('click touchstart', '#stop-button', function() {
window.clearInterval(infiniteInterval);
$('.control-button').fadeOut(300);
});
I initially thought from researching this that it was due to click events not being properly registered, but as you can see I have added touchstart to all the click events and that has made no difference. It's working absolutely fine on all desktop browsers.
Any help is greatly appreciated, and I'd be happy to answer any further questions.
Thanks,
Ben
I've managed to fix the problem, which it turns out was twofold.
Firstly, the click event was firing twice. This was fixed using this SO question: jquery mobile click event fires twice
Secondly, I wasn't properly clearing the intervals.
Edited with #MjrKusanagi's comments
A simple call to clearInterval() before every setInterval() call has fixed the problem, making sure that the interval was always reset before starting again.
Original sketchy workaround:
I've called
infiniteInterval = null;
after every clearInterval() call, as well as wrapping the setInterval() calls with
if (infiniteInterval === null)
Thanks to everyone who commented and hopefully this will help someone sometime :)
First, your click event is firing twice because of this sentence:
$('body').on('click touchstart', '#pause-button', function() { ...
It listens to two events click and touchstart, thus it will be triggered twice, once on click event and once on touchstart event. This is also why your code works well on pc because there's no touchstart event in pc browsers.
So every time you touch that button, things happened like this:
1st event triggered
interval set, handle id is 1 (for example)
infiniteInterval = 1
2nd event triggered
another interval set, handle id is 2
infiniteInterval = 2
And now there's two timing cycles running instead of one, and you only have track of the second one. When you invoke clearInterval, only the handle id = 2 interval is cleared, and 1 is still running.
So the solution is:
Fix the twice-triggered events problem, by only listen to click. (try fastclick or zepto or other lib to deal with the click latency on mobile devices)
As your own answer said, set infiniteInterval to null, and if it is not null do not ever start another interval. (I think it is more elegant than "always clear before setting" works, as infiniteInterval works as a flag of running interval)
Hope these could solve your problem.
Is there a js method to detect when a vertical flick on a long iOS safari page has stopped moving, such as an equivalent to the pseudo:
window.element.momentumScroll == false
This is to detect if the decelerating content is still moving or the scroll event has finished.
Any and all clues gratefully received.
ADDENDUM I have not implemented any external libraries in my code (no jQuery etc) and need to find a native js listener/method to tell me when the flick scroll has ended.
doc.addeventlistener("scroll", function(e){setvariable to 1}, false)
doc.addeventlistener("noscroll", function(e){setvariable to 0}, false)
Method:
startTop = window.pageYOffset on touchStart
currTop = window.pageYOffset on touchEnd
deltaTop = startTop - currTop
deltaTop == 0 means no momentum scrolling occurred during another event.
I'm not sure if I understood the question correctly. I believe u are trying to achieve something like loading new content when the page reaches its bottom? (forgive me for assuming)
I think you are looking for some javascript gesture library, if your event is based on touches.
There are Mootools library on this
Powertools: http://cpojer.net/PowerTools/#!
Drag.Flick: http://mootools.net/forge/p/drag_flick
There should be equal implementation in other framework as well. (jQuery: http://jgestures.codeplex.com/)
Possible solution is to look for an event that can return the current position of touches that exceeds document.body.clientHeight (read: not cross platform) .
Hope I manage to point to the right way.
just do a setTimeout in the touchend event. The timeout will fire once the touchend has stopped working. Timers get paused during touch event. On ios set timeout will fire once the page has stopped scrolling and there is no longer momentum.
body.addeventlistener("ontouchend", function(e){
setTimeout(function(){
alert("done moving")
},0);
}, false);
or
$('body').on('touchend.scroll', function () {
setTimeout(function(){
alert("done moving")
},0);
});
Note that Android will fire the event as soon as you let your finger go. Timers dont seem to be paused.
I am scrolling an overflowing DIV's content by changing the scrollLeft property in Javascript:
setInterval(function(){
$('#scrollbox').scrollLeft($('#scrollbox').scrollLeft()+1);
}, 50);
However, I want to stop this as soon as the user scrolls the content themselves, using the mouse. I tried to detect this using the scroll event
$('#scrollbox').scroll(function(){...});
however, my automatic scrolling above also triggers that event. How can I distinguish this and only react to user-initiated scrolling? (or: how can I stop the above code from firing a scroll event? That would also do the trick)
You could use the .hover(): function to stop the scrolling when the mouse is over the scrollbox element:
http://jsfiddle.net/bGHAH/1/
setInterval(function(){
if(!mouseover)
{
$('#scrollbox').scrollLeft($('#scrollbox').scrollLeft()+1);
}
}, 50);
var mouseover = false;
$('#scrollbox').hover(function(){
mouseover = true;
},function(){
mouseover = false;
});
Edit
Based on your comments I managed to find a jquery plugin from the following site: special scroll events for jquery.
This plugin contains an event which attempts to determine whether scrolling has stopped based on the period of time that has elapsed between the last scroll step and the time the check was made.
To get this to work I needed to slow your interval to just over the latency used by the plugin which worked out to be 310 milliseconds. Doing this meant I had to increase the scroll step to keep it visibly moving.
Here is the link:
http://jsfiddle.net/EWACn/1/
and here is the code:
var stopAutoScroll = false;
$(document).ready(function(){
setInterval(function(){
if(!stopAutoScroll)
{
$('#status').html('scrolling');
$('#scrollbox').scrollLeft($('#scrollbox').scrollLeft()+10);
}else{
$('#status').html('not scrolling');
}
}, 310);
$('#scrollbox').bind('scrollstart', function(e){
stopAutoScroll = true;
});
$('#scrollbox').bind('scrollstop', function(e){
stopAutoScroll = false;
});
});
Hope this helps.
For FF (Mozilla):
document.addEventListener('DOMMouseScroll', handler, false);
For IE, Opera and Chrome:
document.onmousewheel = handler;
Another option is to have an external flag that you can set prior to the programmatic scrolling, and then reset afterwords. If the scroll event is fired and this flag isn't set you know that the user is responsible and can act accordingly.
Unfortunately while this is browser independent and easy to read it could lead you to believe that some user scrolls are programmatic ones. However I would think the occurrences of this is small and may be worth it depending on the app you are writing.
Try wheel event, for most modern browsers
The wheel event is fired when a wheel button of a pointing device (usually a mouse) is rotated.