原文链接:https://javascript.info/mouse-events-basics,translate with ❤️ by zhangbao.

鼠标事件不是仅来自于“鼠标操作”,在触屏设备上(为了兼容)也能触发鼠标事件。

在本章中,我们将详细介绍鼠标事件及其属性。

鼠标事件类型

我们可以把鼠标事件分成两类:“简单”和“复杂”。

简单事件

最常用的包括:

mousedown/mouseup

鼠标按键在元素上点击/释放。

mouseover/mouseout

鼠标光标进入/离开某个元素。

mousemove

鼠标在元素上移动。

……还有其他一些事件类型,之后介绍。

复杂事件

click``

点击鼠标左键时,在 mousedown`、**mouseup`** 事件之后触发。

contextmenu``

点击鼠标右键时,在 mousedownmouseup 事件之后触发。

dblclick

在一个元素上鼠标双击之后触发。

复杂事件由简单事件组成,因此理论上我们可以没有它们。但是复杂事件的存在,也为我们的编程带来便利。

事件顺序

一个操作可能会触发多个事件。

例如,当单击按下按钮时,首先触发 mousedown,然后在释放时触发 mouseupclick 事件。

单个操作触发的多个事件,触发的事件顺序始终固定的。比如,点击事件是按照 mousedownmouseupclick 这样的顺序触发。事件处理程序也是按照这个顺序调用:onclick 的处理程序在 onmouseup 的处理程序之后执行。

点击鼠标左键执行的事件顺序:

鼠标事件基础 - 图1

点击鼠标右键执行的事件顺序:

鼠标事件基础 - 图2

which 属性记录着我们按下的是哪个鼠标按键。

点击的鼠标按键:which

点击相关的事件对象中总是包含一个 which 属性,用来获得点击的是哪个鼠标按键。

它不用于 clickcontextmenu 事件,因为前者仅在左键单击时发生,后者仅在右键单击时发生。

但是如果我们跟踪 mousedownmouseup,那么我们就需要它,因为这些事件是在任何按钮上都会触发的,由此就可以区分“右鼠标按下”和“坐鼠标按下”。

event.which 属性有三个可能的取值:

  • 1:左键。

  • 2:中键。

  • 3:右键。

中间按钮现在有些奇怪,很少使用。

修饰键:Shift, Alt, Ctrl 和 Meta

所有的鼠标事件,都包含按下的修饰键信息。

这些修饰键属性分别是:

  • shiftKey

  • altKey

  • ctrlKey

  • metaKey(Mac 系统的 Cmd 键)

例如,下面的按钮只对“Alt+Shift+点击”有效:

  1. <button id="button">来 Alt+Shift+点击 我!</button>
  2. <script>
  3. button.onclick = function (event) {
  4. if (event.altKey && event.shiftKey) {
  5. alert('万岁!');
  6. }
  7. };
  8. </script>

鼠标事件基础 - 图3注意:**在 Mac 系统中,通常使用 **Cmd** 而非 ****Ctrl**

在 Windows 和 Linux 系统中,可用的修饰键是 AltShiftCtrl。在 Mac 系统中,还多了一个是 Cmd 键,对应属性 metaKey

多数场景下,在 Windows 系统中使用的 Ctrl 键,在 Mac 系统中就变成使用 Cmd 键。例如,Windows 系统用户按下 Ctrl+Enter 或者 Ctrl+A,对应到 Mac 上就是 Cmd+EnterCmd+A(Mac 中的大多数软件也都是选择使用 Cmd 键而非 Ctrl 键)。

所以,如果我们想要支持组合键 Ctrl+点击,那么在 Mac 中就要注意——是使用 Cmd+点击,对于 Mac 用户来说,这更符合使用习惯。

即使强制 Mac 用户使用快捷键 Ctrl+点击,也是挺困难的,问题是:Ctrl+左键点击 会被 Mac 系统理解为右击,进而触发 contextmenu 事件,而不是像在 Windows/Linux 中那样被当成点击事件。

因此,如果我们想让所有操作系统的用户都感觉满意,那么就得同时支持 ctrlKeymetaKey 了。

用 JavaScript 代码我们就应该这样检查:if (event.ctrlKey || event.metaKey)

鼠标事件基础 - 图4注意移动设备

键盘组合键是一种工作方式,只要用户有键盘,就能操作。但是如果没有键盘设备,就需要使用另外一种替代方式。

坐标:clientX/Y,pageX/Y

所有鼠标事件都有两种坐标表示:

  • 窗口坐标:clientXclientY

  • 文档坐标:pageXpageY

例如,我们有一个 500x500 大小的窗口。当鼠标在窗口左上角位置时,clientXclientY 值都为 0,即此时鼠标所在位置的窗口坐标是 (0, 0);如果鼠标在窗口中间位置时,那么 clientXclientY 值都为 250。而且不管文档滚动到在什么位置,这两个位置的窗口坐标始终不变,类似 position:fixed

查阅下面的代码结果,

  1. <input onmousemove="this.value=event.clientX+':'+event.clientY" value="Mouse over me">

当我们的鼠标在输入框上移动的时候,输入框会实时显示当前鼠标的窗口坐标值。

文档坐标的原点位于文档左上角,而非窗口左上角。pageXpageY 属性值类似 position: absolute,反应的是文档坐标值。

关于坐标的更多的信息,会在《坐标系》一章中讲解。

mousedown 的时候不要选中

鼠标点击有副作用,可能会令人不安,那就是双击后选择文本。

如果我们想自己处理点击事件,那么“额外”选择功能看起来不太好。

例如,双击下面的文本除了会调用事件处理程序之外,还会选择它:

  1. <b ondblclick="alert('dblclick')">双击我</b>

有一种 CSS 方法可以停止选择:《CSS UI Draft》中的 user-select 属性。

大多数浏览器使用该属性时候,都需要添加前缀:

  1. <style>
  2. b {
  3. -webkit-user-select: none;
  4. -moz-user-select: none;
  5. -ms-user-select: none;
  6. user-select: none;
  7. }
  8. </style>
  9. Before...
  10. <b ondblclick="alert('test')">
  11. Unselectable
  12. </b>
  13. ...After

现在双击“Unselectable”文本的话,就不会有选中效果了。

但是有个问题,就是文本是真的选中不了了。即使用户是从“Before…”开始一直拉到“…After”,选中部分也不包括“Unselectable”部分。我们真的想让我们的文字无法选中吗?

大多数时候,我们没有。用户可能有正当理由选择文本,复制或其他需要。如果我们不允许他们这样做,那可能会很不方便。所以这个解决方案并不是那么好。

我们想要的是避免双击选择,就是这样。

文本选择是 mousedown 事件的默认浏览器行为,所以替代解决方案是处理 mousedown 并阻止它,如下所示:

  1. Before...
  2. <b ondblclick="alert('test')" onmousedown="return false">
  3. Unselectable
  4. </b>
  5. ...After

现在加粗文字不会在鼠标双击的时候被选中。

但是加粗文本还是可以被选中的,但是不能从自身开始被选中,而是得经过之前或者之后的文本去选中它。但这样已经足够好了。

现在,双击时不会选中粗体文本。

而粗体文本仍然是可选的。但是,选择不能从粗体开始,而是从这段文本的开始或结束位置开始,这种方式通常更好。

鼠标事件基础 - 图5取消选中

我们也可以选择不阻止选中,而是在“事后”取消文本选中。

这样做的:

  1. Before...
  2. <b ondblclick="getSelection().removeAllRanges()">
  3. Unselectable
  4. </b>
  5. ...After

如果双击粗体元素,则会出现选择,然后立即删除。 虽然看起来不太好看。

鼠标事件基础 - 图6阻止复制

如果我们想要阻止选中效果,避免我们的内容被盗取,可以提使用另一个事件:oncopy

  1. <div oncopy="alert('禁止复制'); return false">
  2. 亲,
  3. 我们是不允许复制的页面内容。
  4. 如果你知道 JS 或者 HTML 的话,可以从源代码里获得。
  5. </div>

如果您尝试复制

中的一段文本,那将无效,因为 oncopy 的默认行为被阻止了。

当然,这不能阻止用户打开 HTML 源代码,但不是每个人都知道如何做到这一点。

总结

鼠标事件具有下列属性:

  • 按钮:which

  • 修饰键(按下则为 true):altKeyctrlKeyshiftKeymetaKey(Mac 系统)。

    • 如果想要处理 Ctrl,不要忘记 Mac 用户,他们使用的是 Cmd,所以最好的检查方式是 if (e.metaKey || e.ctrlKey)
  • 窗口坐标:clientX/clientY

  • 文档坐标:pageX/pageY

将文本选择视为点击的不良副作用也很重要。

这里提供了几种方式来阻止这个副作用:

  1. CSS 属性 user-select: none(大多数浏览器中都需要使用浏览器前缀形式),完全禁止文本选中。

  2. “事后”取消文字选中:getSelection().removeAllRanges()

  3. 阻止 mousedown 事件的默认行为(通常来说是最好的方式)。

练习题

问题

一、可选中列表项

创建一个列表,列表项都是可选的,就像文件管理器一样。

  • 列表中只能有一个列表项被选中(添加类名 .selected),其他列表项不能选中。

  • 鼠标点击时,如果同时按下 Ctrl 键,实现的是在点击上的切换选中效果,但不影响其他元素修改。

结果如下图:

鼠标事件基础 - 图7

PS. 本任务中的列表项仅包含文本,不存在内嵌标签。在点击文本的时候,阻止默认的文本选中行为。

答案

一、可选中列表项

  1. ul.onclick = function(event) {
  2. if (event.target.tagName === 'LI') {
  3. if (event.ctrlKey || event.metaKey) {
  4. toggleSelect(event.target);
  5. } else {
  6. singleSelect(event.target);
  7. }
  8. }
  9. }
  10. // 阻止点击时选中文本的默认行为
  11. ul.onmousedown = function() {
  12. return false;
  13. };
  14. function toggleSelect(li) {
  15. li.classList.toggle('selected');
  16. }
  17. function singleSelect(li) {
  18. let selected = ul.querySelectorAll('.selected');
  19. for(let elem of selected) {
  20. elem.classList.remove('selected');
  21. }
  22. li.classList.add('selected');
  23. }

(完)