检测浏览器是否没有鼠标且仅支持触摸操作

178
我正在开发一个Web应用程序(不是有趣文本页面的网站),它具有非常不同的触摸界面(当您单击时,手指会隐藏屏幕)和鼠标界面(严重依赖悬停预览)。如何检测我的用户没有鼠标以向他呈现正确的界面?我计划为拥有鼠标和触摸功能的人留下一个开关(例如某些笔记本电脑)。
浏览器中的触摸事件能力实际上并不意味着用户正在使用触摸设备(例如,Modernizr并不能满足要求)。正确回答该问题的代码应该在设备有鼠标时返回false,在没有鼠标的情况下返回true。对于既有鼠标又有触摸的设备,它应该返回false(而不是仅限触摸)。
顺便说一句,我的触摸界面也可能适用于只有键盘的设备,因此我要检测的更多是缺少鼠标。
为了使需求更清晰,这里是我要实现的API:
// Level 1


// The current answers provide a way to do that.
hasTouch();

// Returns true if a mouse is expected.
// Note: as explained by the OP, this is not !hasTouch()
// I don't think we have this in the answers already, that why I offer a bounty
hasMouse();

// Level 2 (I don't think it's possible, but maybe I'm wrong, so why not asking)

// callback is called when the result of "hasTouch()" changes.
listenHasTouchChanges(callback);

// callback is called when the result of "hasMouse()" changes.
listenHasMouseChanges(callback);

29
也许是因为在谷歌上没有得到帮助,所以我才来这里问。 - nraynaud
2
你尝试过使用jQuery的mouseenter文档吗?$(document).mouseenter(function(e) { alert("mouse"); }); - Parag Gajjar
我的问题非常明确,就是指“完全没有鼠标”。当时的目标是默认为正确的人群提供正确的界面,以便在户外泥泞地带、行驶中的车辆或办公室的个人电脑上使用该应用程序。 - nraynaud
4
在考虑了近十个看似有前途的方向,但每个方向都在几分钟内被否决后,这个问题让我感到非常心烦意乱。 - Jordan Gray
显示剩余3条评论
23个回答

100

截至2018年,有一种好的可靠方法来检测浏览器是否具有鼠标(或类似的输入设备):CSS4媒体交互功能。现在几乎所有现代浏览器都支持它(除了IE 11和特殊移动浏览器)。

W3C:

指针媒体特性用于查询诸如鼠标之类指针设备的存在和准确性。

请查看以下选项:

    /* The primary input mechanism of the device includes a 
pointing device of limited accuracy. */
    @media (pointer: coarse) { ... }
    
    /* The primary input mechanism of the device 
includes an accurate pointing device. */
    @media (pointer: fine) { ... }
    
    /* The primary input mechanism of the 
device does not include a pointing device. */
    @media (pointer: none) { ... }

    /* Primary input mechanism system can 
       hover over elements with ease */
    @media (hover: hover) { ... }
    
    /* Primary input mechanism cannot hover 
       at all or cannot conveniently hover 
       (e.g., many mobile devices emulate hovering
       when the user performs an inconvenient long tap), 
       or there is no primary pointing input mechanism */
    @media (hover: none) { ... }
    
    /* One or more available input mechanism(s) 
       can hover over elements with ease */
    @media (any-hover: hover) { ... }
    
    
    /* One or more available input mechanism(s) cannot 
       hover (or there are no pointing input mechanisms) */
    @media (any-hover: none) { ... }

媒体查询也可以在JS中使用:

if(window.matchMedia("(any-hover: none)").matches) {
    // do something
}

相关:

W3文档:https://www.w3.org/TR/mediaqueries-4/#mf-interaction

浏览器支持:https://caniuse.com/#search=media%20features

类似问题:Detect if a client device supports :hover and :focus states


我个人喜欢这个答案,但截至目前(10/19),根据caniuse.com的数据,@media hover和pointer CSS查询仅在全球约85%的设备上可用。当然不算太差,95%或以上更好。希望这很快会成为设备上的标准。 - MQuiggGeorgia
2
@MQuiggGeorgia 基本上我同意你的批评,因为它还没有得到普遍支持。但是对于我来说,caniuse.com显示它的支持率为91.2%(https://caniuse.com/#feat=css-media-interaction)。仔细看一下,除了IE 11和移动设备上的特殊浏览器之外,它在任何地方都得到了支持。公平地说,这对于任何现代功能来说都是真实的,因为微软早就停止实现IE功能了。对于IE 11,您可以使用其他答案中的回退。 - Blackbam
1
window.matchMedia("(any-pointer: fine)").matches 在我的所有移动浏览器和桌面上都返回 true,原因不明。 即使在没有鼠标的移动设备上,window.matchMedia("(any-hover: hover)").matches 也总是返回 true。 只有 window.matchMedia("(any-pointer: coarse)").matches 在移动设备上返回 true,在桌面上返回 false,但它没有考虑连接的鼠标或 s-pen。 - Cluster
@Cluster 这篇文章从实用的角度详细介绍了这些功能:https://css-tricks.com/touch-devices-not-judged-size/ - Blackbam
在任何桌面上的Chromium浏览器中,window.matchMedia("(any-hover: none)").matches总是返回false,而在任何触摸笔记本电脑上的Chromium浏览器中,window.matchMedia("(any-pointer: fine)").matches也返回false。因此,尽管caniuse说这个功能可以工作,但在Chromium浏览器中它绝对不能正确返回值! - Robin93K
显示剩余6条评论

77
主要问题在于您有以下不同类型的设备/用例:
  1. 鼠标和键盘(桌面)
  2. 仅触摸(手机/平板电脑)
  3. 鼠标、键盘和触摸(触摸笔记本电脑)
  4. 触摸和键盘(平板电脑上的蓝牙键盘)
  5. 仅鼠标(残疾用户/浏览偏好)
  6. 仅键盘(残疾用户/浏览偏好)
  7. 触摸和鼠标(即来自Galaxy Note 2笔的悬停事件)
更糟糕的是,一个人可以从其中一些类别过渡到另一些类别(插入鼠标、连接键盘),或者用户可能看起来像是在普通的笔记本电脑上,直到他们伸手触摸屏幕。
您正确地认为,浏览器中事件构造函数的存在不是前进的好方法(而且有些不一致)。此外,除非您正在跟踪非常特定的事件或仅尝试排除上述几个类别中的少数几个,否则仅使用事件本身并不完美。
例如,假设您已经发现用户发出了一个真正的mousemove事件(而不是来自触摸事件的虚假事件,请参见http://www.html5rocks.com/en/mobile/touchandmouse/)。
然后呢?
您启用悬停样式吗?您添加更多按钮吗?
无论哪种方式,您都在增加时间到玻璃,因为您必须等待事件触发。
但是,当您的用户决定拔掉鼠标并进行全触摸时,会发生什么情况?您是否要等待他触摸您现在拥挤的界面,然后在他费力地定位了您现在拥挤的UI之后立即更改它?
引用https://github.com/Modernizr/Modernizr/issues/869#issuecomment-15264101中的stucox所说,在项目符号形式上:
We want to detect the presence of a mouse. We probably can't detect it before an event is fired. As such, what we're detecting is if a mouse has been used in this session — it won't be immediately from page load. We probably also can't detect that there isn't a mouse — it'd be undefined until true (I think this makes more sense than setting it false until proven). And we probably can't detect if a mouse is disconnected mid-session — that'll be indistinguishable from the user just giving up with their mouse.
An aside: the browser DOES know when a user plugs in a mouse/connects to a keyboard, but doesn't expose it to JavaScript.. dang!
This should lead you to the following:
"跟踪给定用户的当前功能是复杂、不可靠且毫无意义的。"

渐进增强的思想在这里非常适用。构建一个体验,无论用户的上下文如何都能顺畅地运行。然后根据浏览器特性/媒体查询做出假设,在假定的上下文中添加相关功能。鼠标的存在只是不同用户在不同设备上体验您的网站的众多方式之一。创建一个具有内在价值的东西,不要太担心人们如何点击按钮。


3
好的回答。希望用户总是有屏幕!我认为构建一个界面,适应用户当前的交互模式是有意义的。在触摸笔记本电脑上,当用户从鼠标切换到触摸时,调整应用程序(例如:hover元素和类似的东西)是有意义的。目前似乎不太可能用户同时使用鼠标和触摸(我的意思是,这就像将两个鼠标连接到同一台计算机上哈哈哈)。 - Sebastien Lorber
1
@SebastienLorber - 很抱歉打破你的幻想,但用户并不总是拥有屏幕。(是否可能使用JavaScript检测用户机器上是否运行屏幕阅读器?) - ashleedawg

64

你可以尝试在文档上监听mousemove事件。当你检测到该事件时,就可以认为设备支持鼠标操作,否则就默认为触摸或键盘控制。

var mouseDetected = false;
function onMouseMove(e) {
  unlisten('mousemove', onMouseMove, false);
  mouseDetected = true;
  // initializeMouseBehavior();
}
listen('mousemove', onMouseMove, false);

(其中的listenunlisten委托给addEventListenerattachEvent适当的方法。)

希望这不会导致太多的视觉卡顿,如果你需要基于模式进行大规模重新布局,那就糟糕了...


3
这是一个好主意,但不幸的是,响应延迟会导致当应用程序的用户界面依赖于鼠标是否可用时,它无法使用。特别是如果该应用程序可能被嵌入到一个 iframe 中,那么只有当鼠标移动到 iframe 上时,鼠标事件才会命中它。 - Jon Gjengset
7
如果应用程序以闪屏和“继续”按钮开始,则这个方法可能可行。如果在第一次鼠标按下事件之前鼠标移动,则表示您有一个鼠标。只有当按钮直接加载在鼠标下并且用户手持非常稳定(即使只移动1像素也应该被检测到)时,此方法才会失败。 - SpliFF
56
好的,但是在我们的测试中似乎无法正常工作。iPad触发此事件。 - Jeff Atwood
3
@JeffAtwood,你最终在你的情况下是怎么做的? - Michael Haren
3
iPad肯定会触发mousemove事件,在mousedown事件之前。我发现mousedown计数> 0且mousedown计数== mousemove计数是检测没有鼠标的好方法。我无法用真正的鼠标复制这个结果。 - Peter Wooster
显示剩余11条评论

24

@Wyatt的回答很好,让我们有很多思考。

在我的情况下,我选择先监听第一次交互,然后再设置行为。因此,即使用户有鼠标,如果第一次交互是触摸,则我将其视为触摸设备。

考虑到事件被处理的给定顺序

  1. touchstart
  2. touchmove
  3. touchend
  4. mouseover
  5. mousemove
  6. mousedown
  7. mouseup
  8. click

我们可以假设如果鼠标事件在触摸事件之前被触发,则它是一个真正的鼠标事件,而不是模拟出来的。例如(使用jQuery):

$(document).ready(function() {
    var $body = $('body');
    var detectMouse = function(e){
        if (e.type === 'mousedown') {
            alert('Mouse interaction!');
        }
        else if (e.type === 'touchstart') {
            alert('Touch interaction!');
        }
        // remove event bindings, so it only runs once
        $body.off('mousedown touchstart', detectMouse);
    }
    // attach both events to body
    $body.on('mousedown touchstart', detectMouse);
});

那对我起作用了。


这段代码对我不起作用,iPad Safari(IOS8.3)也会检测到鼠标。 - netzaffin
3
@netzaffin,感谢您的反馈。我发现使用mousedown比mouseover更一致。您能否在您的IOS上查看此链接并让我知道结果?谢谢。链接如下:https://jsfiddle.net/bkwb0qen/15/embedded/result/ - Hugo Silva
1
如果您有带鼠标的触摸屏,只有首先使用的输入方法才会被检测到。 - 0xcaff

13

只有可能检测浏览器是否具有触摸功能。无法知道它实际上是具有触摸屏幕还是连接了鼠标。

如果检测到触摸功能,则可以通过监听触摸事件而不是鼠标事件来优先使用。

要跨浏览器检测触摸功能:

function hasTouch() {
    return (('ontouchstart' in window) ||       // html5 browsers
            (navigator.maxTouchPoints > 0) ||   // future IE
            (navigator.msMaxTouchPoints > 0));  // current IE10
}

然后可以使用这个来检查:

if (!hasTouch()) alert('Sorry, need touch!);

或者选择要监听哪个事件:

var eventName = hasTouch() ? 'touchend' : 'click';
someElement.addEventListener(eventName , handlerFunction, false);

或者针对触摸和非触摸使用不同的方法:

if (hasTouch() === true) {
    someElement.addEventListener('touchend' , touchHandler, false);

} else {
    someElement.addEventListener('click' , mouseHandler, false);

}
function touchHandler(e) {
    /// stop event somehow
    e.stopPropagation();
    e.preventDefault();
    window.event.cancelBubble = true;
    // ...
    return false; // :-)
}
function mouseHandler(e) {
    // sorry, touch only - or - do something useful and non-restrictive for user
}

对于鼠标,我们只能检测到它是否正在使用,而不能检测到它是否存在。我们可以设置一个全局标志来指示鼠标是否被使用过(类似于现有答案,但稍微简化一些):

var hasMouse = false;

window.onmousemove = function() {
    hasMouse = true;
}

不能包含 mouseupmousedown,因为这些事件也可能由触摸触发。

浏览器限制访问低级系统 API,以便能够检测系统的硬件功能等特性。

也许有可能编写一个插件/扩展程序来访问这些内容,但通过 JavaScript 和 DOM 进行此类检测在此目的上是受限的,您必须针对各个操作系统平台编写特定的插件。

因此总结起来:这种检测只能通过“猜测”来估计。


8

当浏览器支持 Media Queries Level 4 时,我们可以使用“pointer”和“hover”查询来检测带有鼠标的设备。

如果我们真的想将这些信息传达给Javascript,我们可以使用CSS查询来根据设备类型设置特定的样式,然后在Javascript中使用getComputedStyle读取该样式,并从中推导出原始设备类型。

但是鼠标随时可能会被连接或拔出,用户可能想要在触摸和鼠标之间切换。因此,我们可能需要检测这种变化,并提供更改界面或自动更改的选项。


1
具体来说,any-pointerany-hover将让您调查所有适用的设备功能。很高兴能够一窥我们未来如何解决这个问题的方法! :) - Jordan Gray
2
window.matchMedia("(any-pointer: coarse)").matches === true ? - 4esn0k

7

既然你计划提供切换界面的方法,那么是否可以简单地让用户点击链接或按钮“进入”正确版本的应用程序呢?然后您可以记住他们的偏好以备将来使用。这不是高科技,但是它是100%可靠的 :-)


2
这实际上是一个相当不错的建议,但它会延迟用户进入真正界面的时间。另外,我必须提供一种在初始选择后进行切换的方式。最终比只需简单检测就能完成更多的工作。 - Jon Gjengset
1
询问用户显然是最好的方式 - 虽然并非总是万无一失 - 并且为您提供了一个方便的位置来发布升级通知等内容。我认为你过于考虑这个“问题”了。 - T4NK3R

6

在类似的情况下,这对我很有帮助。基本上,假设用户没有鼠标,直到您看到一系列短而连续的鼠标移动,没有介入mousedown或mouseup。虽然不太优雅,但它可以工作。

var mousedown = false;
var mousemovecount = 0;
function onMouseDown(e){
    mousemovecount = 0;
    mousedown = true;
}
function onMouseUp(e){
    mousedown = false;
    mousemovecount = 0;
}
function onMouseMove(e) {
    if(!mousedown) {
        mousemovecount++;
        if(mousemovecount > 5){
            window.removeEventListener('mousemove', onMouseMove, false);
            console.log("mouse moved");
            $('body').addClass('has-mouse');
        }
    } else {
        mousemovecount = 0;
    }
}
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('mousedown', onMouseDown, false);
window.addEventListener('mouseup', onMouseUp, false);

4

@SamuelRossille,很遗憾我不知道有哪些浏览器可以公开(或者不公开)鼠标的存在。

因此,我们只能尽力利用现有的选项...事件。我知道这不完全是你要找的...同意它目前离理想还有很远的路要走。

我们可以尽力确定用户在任何时刻是使用鼠标还是触摸。以下是一个快速而简单的示例,使用jQuery和Knockout:

//namespace
window.ns = {};

// for starters, we'll briefly assume if touch exists, they are using it - default behavior
ns.usingTouch = ko.observable(Modernizr.touch); //using Modernizr here for brevity.  Substitute any touch detection method you desire

// now, let's sort out the mouse
ns.usingMouse = ko.computed(function () {
    //touch
    if (ns.usingTouch()) {
        //first, kill the base mousemove event
        //I really wish browsers would stop trying to handle this within touch events in the first place
        window.document.body.addEventListener('mousemove', function (e) {
            e.preventDefault();
            e.stopImmediatePropagation();
        }, true);

        //remove mouse class from body
        $('body').removeClass("mouse");

        //switch off touch listener
        $(document).off(".ns-touch");

        // switch on a mouse listener
        $(document).on('mousemove.ns-mouse', function (e) {
            if (Math.abs(window.lastX - e.clientX) > 0 || window.lastY !== e.clientY) {
                ns.usingTouch(false);  //this will trigger re-evaluation of ns.usingMouse() and result in ns.usingMouse() === true
            }
        });

        return false;
    }
    //mouse
    else {
        //add mouse class to body for styling
        $('body').addClass("mouse");

        //switch off mouse listener
        $(document).off(".ns-mouse");

        //switch on a touch listener
        $(document).on('touchstart.ns-touch', function () { ns.usingTouch(true) });

        return true;
    }
});

//tests:
//ns.usingMouse()
//$('body').hasClass('mouse');

现在你可以使用usingMouse()usingTouch()进行绑定/订阅,或者使用body.mouse类来样式化你的界面。一旦检测到鼠标光标或touchstart事件,界面将自动切换。

希望浏览器厂商很快能提供更好的选项。


2
为什么不检测它是否具有感应触摸和/或对鼠标移动做出反应的能力?
// This will also return false on
// touch-enabled browsers like Chrome
function has_touch() {
  return !!('ontouchstart' in window);
}

function has_mouse() {
  return !!('onmousemove' in window);
}

4
因为某些浏览器(例如IE9)会报告该函数存在,即使它永远不会被触发。我认为这也是“正确”的行为。 - Jon Gjengset
好的,至少在OS X的Chrome 47上运行正常。报告没有ontouchstart。 - phreakhead

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接