Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to identify if a mouseover event object came from a touchscreen touch?

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 touchstarttouchendmouseoverclick (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.

like image 821
user56reinstatemonica8 Avatar asked Sep 13 '16 16:09

user56reinstatemonica8


People also ask

When the mouse over event is detected?

The mouseover event is fired at an Element when a pointing device (such as a mouse or trackpad) is used to move the cursor onto the element or one of its child elements.

Which event triggers when we bring mouse cursor over any element?

The mouseover event occurs when a mouse pointer comes over an element, and mouseout – when it leaves.

Is click event same as touch?

The touchstart event occurs when the user touches an element. But a click event is fired when the user clicks an element.


2 Answers

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:

  1. You can't detect a touchscreen (http://www.stucox.com/blog/you-cant-detect-a-touchscreen/)
  2. 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:

  1. 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/.
  2. For most devices / browsers, the mouseover event is always preceded by either pointerover, its MS counterpart MSPointerOver, or touchstart
  3. 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:

  1. 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
  2. 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.

like image 189
Perran Mitchell Avatar answered Oct 18 '22 23:10

Perran Mitchell


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;
}
<a href="#" onmouseover="onMouseOver(event)">Click me</a>
like image 6
Manuel Otto Avatar answered Oct 18 '22 22:10

Manuel Otto