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

Posted

技术标签:

【中文标题】如何识别鼠标悬停事件对象是不是来自触摸屏触摸?【英文标题】:How to identify if a mouseover event object came from a touchscreen touch?如何识别鼠标悬停事件对象是否来自触摸屏触摸? 【发布时间】:2017-01-21 08:13:38 【问题描述】:

在几乎所有当前的浏览器上(广泛的details from patrickhlauke on github,其中I summarised in an SO answer,还有更多信息from QuirksMode),触摸屏触摸触发mouseover事件(有时会创建一个不可见的伪光标,停留在用户触摸的位置直到他们接触到其他地方)。

在触摸/单击和鼠标悬停旨在执行不同操作的情况下,有时这会导致不良行为。

从响应鼠标悬停事件的函数内部,已传递event 对象,有什么方法可以检查这是否是从元素外部移动到内部的移动光标的“真实”鼠标悬停它,或者如果它是由触摸屏触摸的这种触摸屏行为引起的?

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

我的想法是将一个事件绑定到 touchstart 以更改鼠标悬停事件,然后将一个事件绑定到 touchend 以删除此更改。不幸的是,这不起作用,因为事件顺序似乎是 touchstarttouchendmouseoverclick (我无法附加 normalise-mouseover 功能来单击而不弄乱其他功能)。


我希望以前有人问过这个问题,但现有的问题并没有完全解决:

How to handle mouseover and mouseleave events in Windows 8.1 Touchscreen 是关于 Windows 上的 C# / ASP.Net 应用程序,而不是浏览器中的网页 JQuery .on(“click”) triggers “mouseover” on touch device 类似,但与 jQuery 有关,答案是一种不好的方法(猜测触摸屏用户代理的硬编码列表,在创建新设备 UA 时会中断,并且错误地假设所有设备都是鼠标 触摸屏) Preventing touch from generating mouseOver and mouseMove events in android browser 是我能找到的最接近的,但它只是关于 Android,是关于防止在触摸时无法识别鼠标悬停,并且没有答案 Browser handling mouseover event for touch devices causes wrong click event to fire 是相关的,但他们试图阐明 ios 两次点击交互模式,并且唯一的答案犯了假设触摸和鼠标/点击是互斥的错误。

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


注意 - 我们不能假设触摸屏设备没有类似鼠标的移动光标,反之亦然,因为有许多设备使用触摸屏和类似鼠标的笔,当鼠标悬停在附近时会移动光标屏幕,或者使用触摸屏和鼠标(例如触摸屏笔记本电脑)。更多细节在我对How do I detect whether a browser supports mouseover events?的回答中。

注意 #2 - 这不是一个 jQuery 问题,我的事件来自 Raphael.js 路径,其中 jQuery 不是一个选项,它提供了一个普通的浏览器 event 对象。如果有特定于 Raphael 的解决方案,我会接受,但这不太可能,原始 javascript 解决方案会更好。

【问题讨论】:

在 Google 寻求相同解决方案时发现了这个,已经吞噬了 Patrick 的 superb 研究。鉴于赏金猎人想要的这个问题的质量和清晰度,当前的答案是糟糕。暂时,我的朋友在这个article on input device capabilities后面找到了这个excellent Google doc 【参考方案1】:

您可以为此使用modernizr!我刚刚在本地开发服务器上对此进行了测试,它可以工作。

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

所以我会从那里开始?

【讨论】:

遗憾的是没那么简单,请看问题末尾的第一个注释。 “设备有触摸屏”!==“鼠标悬停事件来自触摸”,有很多设备既有触摸屏又有普通的鼠标输入。此外,即使用户的配置当前不包括触摸屏,几个桌面浏览器也报告了 Modernizr.touch 的真实情况(请参阅 Modernizr 自己在 Modernizr.touch returns true on firefox browser 上的回答【参考方案2】:

根据https://www.html5rocks.com/en/mobile/touchandmouse/ 单击事件的顺序为:

    触摸开始 触摸移动 触摸结束 鼠标悬停 鼠标移动 鼠标按下 鼠标移动 点击

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

【讨论】:

【参考方案3】:

我们所知道的是:

当用户不使用鼠标时

mouseovertouchendtouchstart(如果用户点击并按住)。 mouseovertouchstart/touchend 的位置相同。

当用户使用鼠标/笔时

mouseover 在触摸事件之前被触发,即使没有,mouseover 的位置也不会在 99% 的时间内与触摸事件的位置匹配。

牢记这些要点,我做了一个 sn-p,如果满足列出的条件,它将在事件中添加一个标志 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;
&lt;a href="#" onmouseover="onMouseOver(event)"&gt;Click me&lt;/a&gt;

【讨论】:

【参考方案4】:

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

如您所知,在任何触摸事件中调用 event.preventDefault(事件必须不是被动的才能与 touchstart 一起使用)将取消随后的鼠标调用,因此您无需处理它们。但如果这不是你想要的,这就是我有时使用的(我将您的 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 的另一个选项是认为鼠标悬停是与 touchstart 的计数器,而 mouseout 是与 touchend 的计数器。因此,以前的事件(触摸事件)将设置一个属性,如果鼠标事件检测到该属性,则它们不会触发并将该属性重置为其初始值等等。在这种情况下,也可以这样做:

没有设置超时

....
.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*/
                );

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

【讨论】:

【参考方案5】:

鉴于问题的复杂性,我认为值得详细说明任何潜在解决方案中涉及的问题和边缘案例。

问题:

1 - 跨设备和浏览器的触摸事件的不同实现。对某些人有用的东西肯定不会对其他人有用。您只需浏览这些 patrickhlauke 资源,即可了解当前在不同设备和浏览器中点击触摸屏的过程有何不同。

2 - 事件处理程序没有提供关于其初始触发器的任何线索。 你也绝对正确地说 event 对象是相同的(当然在绝大多数情况下)在通过与鼠标交互调度的鼠标事件和通过触摸交互调度的鼠标事件之间。

3 - 任何涵盖所有设备的问题解决方案都可能是短暂的,因为当前的 W3C 建议没有详细说明如何触摸/点击应该处理事件(https://www.w3.org/TR/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) 很差。


我们目前如何解决这个问题

如果我们接受:

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

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

再看patrickhlauke的资源,我们可以做一些陈述:

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

鉴于此,我们可以在pointeroverMSPointerOvertouchstart 期间设置一个标志,并在其中一个点击事件期间将其移除。这会很好用,除了少数情况:

    event.preventDefault 在其中一个触摸事件上被调用 - 该标志永远不会被取消设置,因为不会调用点击事件,因此该元素上的任何未来真正的点击事件仍将被标记为触摸事件 如果目标元素在事件期间移动。 W3C 建议声明

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


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

我认为这是一个引人入胜的场景,因此不久将修改此答案以包含推荐的代码响应。目前,我会推荐@ibowankenobi 提供的答案或@Manuel Otto 提供的答案。

【讨论】:

以上是关于如何识别鼠标悬停事件对象是不是来自触摸屏触摸?的主要内容,如果未能解决你的问题,请参考以下文章

css clone-events属性允许控制HTML元素如何响应鼠标/触摸事件 - 包括CSS悬停/活动状态,cli

检测到浏览器没有鼠标且仅触摸

text 鼠标悬停(悬停)在触摸设备上,双选项卡

如何用触摸数据模拟鼠标点击?

悬停 CSS 上的 iPad/iPhone 触摸事件

css 在鼠标悬停或单击时创建工具提示(用于支持触摸界面)。