如何识别鼠标悬停事件对象是否来自触摸屏触摸



在几乎所有当前的浏览器(广泛的细节来自github上的patrickhlauke,我总结了一个SO答案,还有一些来自QuirksMode的信息),触摸屏触摸触发mouseover事件(有时创建一个不可见的伪光标,它停留在用户触摸的地方,直到他们触摸到其他地方)。

有时这会导致不希望的行为,在情况下,触摸/点击和鼠标悬停打算做不同的事情。

从响应鼠标悬停事件的函数内部,已经传递给event对象,是否有任何方法我可以检查这是否是一个"真正的"鼠标悬停从一个移动的光标从外部移动到元素内部,或者如果它是由这个触摸屏行为引起的触摸屏触摸?

event对象看起来相同。例如,在chrome上,由用户触摸触摸屏引起的鼠标悬停事件具有type: "mouseover",并且我看不到任何可以将其识别为与触摸相关的事件。

我的想法是绑定一个事件到touchstart,改变鼠标悬停事件,然后绑定一个事件到touchend,删除这个更改。不幸的是,这不起作用,因为事件顺序似乎是touchstarttouchendmouseoverclick(我不能在不搞乱其他功能的情况下附加normalize -mouseover功能来点击)


我希望这个问题以前就有人问过,但现有的问题不太合适:

  • 如何处理鼠标over和鼠标离开事件在Windows 8.1触摸屏是关于c#/ASP。. Net应用程序,而不是浏览器中的网页
  • JQuery .on("click")触发触摸设备上的"mouseover"是类似的,但是关于JQuery的,答案是一个糟糕的方法(猜测一个硬编码的触摸屏用户代理列表,当创建新的设备ui时,它会中断,并且错误地假设所有设备都是鼠标触摸屏)
  • 防止触摸在Android浏览器中产生mouseOver和mouseMove事件是我能找到的最接近的,但它只关于Android,是关于防止不识别鼠标over触摸,并且没有答案
  • 浏览器处理触摸设备的鼠标over事件导致错误的点击事件是相关的,但他们试图消除iOS的两点交互模式,而且唯一的答案是错误地假设触摸和鼠标/点击是相互排斥的。

我能想到的最好的是有一个触摸事件,设置一些全局可访问的变量标志,比如touchstart上的window.touchedRecently = true;,但不点击,然后在500ms setTimeout之后删除这个标志。这是一个丑陋的hack。


注意-我们不能假设触摸屏设备没有鼠标一样的游标,或者相反,因为有许多设备使用触摸屏和鼠标一样的笔,当光标在屏幕附近徘徊时移动光标,或者使用触摸屏和鼠标(例如触摸屏笔记本电脑)。在我的回答如何检测浏览器是否支持鼠标悬停事件的更多细节?

注#2 -这是不是一个jQuery问题,我的事件是来自拉斐尔.js路径的jQuery不是一个选项,它给了一个普通的香草浏览器event对象。如果有一个拉斐尔特有的解决方案,我会接受,但这是非常不可能的,一个原始的javascript解决方案会更好。

考虑到问题的复杂性,我认为有必要详细说明任何可能的解决方案所涉及的问题和边缘情况。

问题:

1 -跨设备和浏览器的触摸事件的不同实现 。对某些人有效的方法不一定对另一些人有效。你只需要浏览一下patrickhlauke的资源,就能知道在不同的设备和浏览器中,点击触摸屏的过程是多么不同。

2 -事件处理程序不提供关于其初始触发器的线索。 你说event对象是完全相同的(当然在绝大多数情况下),通过与鼠标交互调度的鼠标事件和通过触摸交互调度的鼠标事件。

3 -任何涵盖所有设备的解决方案都可能是短暂的 ,因为当前的W3C推荐标准没有对如何处理触摸/点击事件进行足够的详细说明(https://www.w3.org/TR/touch-events/),因此浏览器将继续有不同的实现。此外,Touch Events标准文档在过去5年里似乎没有改变,所以这个问题不会很快得到解决。https://www.w3.org/standards/history/touch-events

4 -理想情况下,解决方案不应该使用超时 ,因为没有从触摸事件到鼠标事件的定义时间,并且根据规范,很可能不会很快出现超时。不幸的是,超时几乎是不可避免的,我将在后面解释。


未来的解决方案:

在未来,解决方案可能是使用 Pointer Events来代替鼠标/触摸事件,因为这些给了我们pointerType (https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events),但不幸的是,我们还没有建立一个标准,因此跨浏览器兼容性(https://caniuse.com/#search=pointer%20events)很差。


我们现在如何解决这个问题

如果我们接受:

  1. 无法检测触摸屏(http://www.stucox.com/blog/you-cant-detect-a-touchscreen/)
  2. 即使我们可以,在触摸屏上仍然存在非触摸事件的问题

那么我们只能使用鼠标事件本身的数据来确定它的起源。正如我们所建立的,浏览器不提供这个功能,所以我们需要自己添加它。唯一的方法就是使用与鼠标事件同时触发的触摸事件。

再次查看patrickhlauke的资源,我们可以做出一些声明:

  1. mouseover总是跟在点击事件mousedownmouseupclick之后——总是按这个顺序。(有时被其他事件分开)。这得到了W3C建议的支持:https://www.w3.org/TR/touch-events/.
  2. 对于大多数设备/浏览器,mouseover事件之前总是有pointerover,其MS对应的MSPointerOvertouchstart
  3. 事件顺序以mouseover开头的设备/浏览器必须忽略。在触摸事件本身被触发之前,我们不能确定鼠标事件是由触摸事件触发的。

鉴于此,我们可以在pointeroverMSPointerOvertouchstart期间设置一个标志,并在其中一个单击事件期间删除它。这将很好地工作,除了少数情况:

  1. event.preventDefault在一个触摸事件中被调用——这个标志将永远不会被取消,因为点击事件不会被调用,所以任何未来在这个元素上的真正的点击事件仍然会被标记为触摸事件
  2. 如果目标元素在事件期间被移动。W3C推荐状态

如果文档的内容在处理过程中发生了变化触摸事件,那么用户代理可以将鼠标事件分派给与触摸事件不同的目标


不幸的是,这意味着我们总是需要使用超时。据我所知,既没有办法确定触摸事件何时调用event.preventDefault,也没有办法理解触摸元素何时在DOM内移动,点击事件何时在另一个元素上触发。

我认为这是一个令人着迷的场景,所以这个答案将很快进行修改,以包含推荐的代码响应。现在,我推荐@ibowankenobi提供的答案或者@Manuel Otto提供的答案

我们知道的是:

当用户不使用鼠标时

  • mouseover直接(在800ms内)在touchend或atouchstart(如果用户轻按并按住).
  • mouseovertouchstart/touchend的位置相同。

当用户使用鼠标/笔时

  • mouseover在触摸事件之前被触发,即使没有,mouseover的位置在99%的情况下也不会与触摸事件的位置匹配。

牢记这些要点,我编写了一个代码片段,如果满足列出的条件,它将向事件添加一个标志triggeredByTouch = true。此外,您可以将此行为添加到其他鼠标事件或设置kill = true以便完全丢弃由触摸触发的鼠标事件。

(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)

试一试:

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>

根据https://www.html5rocks.com/en/mobile/touchandmouse/
对于单次点击,事件顺序为:

  1. touchstart
  2. touchmove
  3. touchend
  4. 鼠标移至
  5. mousemove
  6. mousedown
  7. mouseup
  8. 点击

所以你可以在onTouchStart()和onClick()中设置一些任意的布尔值isFromTouchEvent = true;isFromTouchEvent = false;,并检查onMouseOver()的内部。这不是很好地工作,因为我们不能保证在我们试图侦听的元素中获得所有这些事件。

我通常有几个通用的方案,其中一个使用setTimeout的手动原理来触发属性。我将在这里解释这个,但首先尝试推理在触摸设备上使用touchstart, touchmove和touchend以及在destop上使用mouseover。

如你所知,调用事件。任何touchevent中的preventDefault(事件必须不是被动的才能与touchstart一起工作)都会取消后续的鼠标调用,所以你不需要处理它们。但如果这不是你想要的,下面是我有时使用的(我将"library"称为dom操作库,并将"elem"称为元素):

与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
})

另一个没有setTimeout的选项是认为mousover是touchstart的计数器,mouseout是touchend的计数器。以前的事件(触摸事件)会设置一个属性,如果鼠标事件检测到那个属性,那么它们不会触发并将属性重置为初始值,等等。在这种情况下,沿着这些行也会做:

没有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*/
                });

我删除了很多细节,但我想这是主要思想。

您可以使用modernizr !我刚刚在本地开发服务器上测试了这个,它可以工作。

if (Modernizr.touch) { 
  console.log('Touch Screen');
} else { 
  console.log('No Touch Screen');
} 

所以我要从这里开始?

指针事件现在被广泛支持。所以现在我们可以使用pointerenter并检查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>

相关内容

最新更新