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

时间:2016-09-13 16:30:18

标签: javascript cross-browser touch raphael

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

有时,如果触摸/点击和鼠标悬停用于执行不同的操作,则会导致不良行为。

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

event对象看起来完全相同。例如,在Chrome上,由用户触摸触摸屏引起的鼠标悬停事件为type: "mouseover",而我无法看到任何可将其识别为与触摸相关的内容。

我有想法将事件绑定到touchstart,它会改变鼠标悬停事件,然后将事件绑定到touchend,从而删除此更改。不幸的是,这不起作用,因为事件顺序似乎是touchstarttouchendmouseoverclick(我无法将normalize-mouseover函数附加到点击而不搞乱其他功能)。

我曾预计此问题会被提出,但现有的问题并未完全消除它:

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

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

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

5 个答案:

答案 0 :(得分:6)

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

问题:

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而不是鼠标/触摸事件,因为这些事件会给我们pointerTypehttps://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. 即使我们可以,仍然存在触摸屏幕上的非触摸事件的问题
  3. 然后我们只能使用有关鼠标事件本身的数据来确定其来源。正如我们已经建立的那样,浏览器不提供这个,所以我们需要自己添加它。唯一的方法是使用与鼠标事件同时触发的触摸事件。

    再次查看patrickhlauke资源,我们可以发表一些声明:

    1. mouseover后面始终跟有点击事件mousedown mouseupclick - 始终按此顺序排列。 (有时被其他事件分开)。这得到了W3C建议的支持:https://www.w3.org/TR/touch-events/
    2. 对于大多数设备/浏览器,mouseover事件始终以pointerover,其MS对应MSPointerOvertouchstart
    3. 开头
    4. 必须忽略事件顺序以mouseover开头的设备/浏览器。我们无法确定鼠标事件是在触发事件本身之前触发事件触发的。
    5. 鉴于此,我们可以在pointeroverMSPointerOvertouchstart期间设置一个标记,并在其中一个点击事件中删除它。这样做很有效,除了一小撮案例:

        其中一个触摸事件会调用
      1. event.preventDefault - 因为点击事件不会被调用,所以永远不会设置标志,因此此元素上任何未来的真正点击事件仍会被标记为触摸事件
      2. 如果在事件期间移动目标元素。 W3C建议书状态
      3.   

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

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

        我认为这是一个引人入胜的场景,所以这个答案将很快修改,以包含推荐的代码响应。现在,我会推荐@ibowankenobi提供的答案或@Manuel Otto提供的答案。

答案 1 :(得分:5)

我们所知道的是:

当用户不使用鼠标时

  • mouseover直接(在800毫秒内)在touchend或之后被解雇 touchstart(如果用户点按并按住)。
  • 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)

尝试一下:

&#13;
&#13;
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)
&#13;
a{
  font-family: Helvatica, Arial;
  font-size: 21pt;
}
&#13;
<a href="#" onmouseover="onMouseOver(event)">Click me</a>
&#13;
&#13;
&#13;

答案 2 :(得分:4)

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

  1. touchstart
  2. touchmove
  3. touchend
  4. 鼠标悬停
  5. 鼠标移动
  6. 鼠标按下
  7. 鼠标松开
  8. 点击
  9. 因此,您可以在onClick()中的onTouchStart()和isFromTouchEvent = true;中设置一些任意布尔值isFromTouchEvent = false;,并检查onMouseOver()内部的内容。这不能很好地工作,因为我们无法保证在我们试图听的元素中获得所有这些事件。

答案 3 :(得分:3)

我通常会使用几种常规方案,其中一种方法使用setTimeout的手动原理来触发属性。我将在这里解释一下,但首先尝试在触摸设备上使用touchstart,touchmove和touchend以及在destop上使用鼠标悬停。

如您所知,在任何touchevents中调用event.preventDefault(事件必须不是被动以使其与touchstart一起使用)将取消后续的鼠标调用,因此您不需要处理它们。但是如果这不是你想要的,那么我有时会使用这些(我将“库”称为你的dom操作库,并将“elem”称为你的元素):

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

没有setTimeout的另一个选择是认为mousover与touchstart和mouseout计数器对抗touc​​hend。因此,以前的事件(触摸事件)将设置属性,如果鼠标事件检测到该属性,则它们不会触发并将属性重置为其初始值,依此类推。在这种情况下,沿着这些方向的东西也会做:

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

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

答案 4 :(得分:2)

你可以使用modernizr!我刚刚在本地开发服务器上对此进行了测试,并且可以正常运行。

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

所以我会从那里开始?