鼠标事件不单单来自于鼠标,还可能来自手机或平板,它们对此类操作进行了模拟以实现兼容。
mousedown/mouseup
在元素上点击/释放鼠标按钮。
mouseover/mouseout
鼠标指针从一个元素上移入/移出。
mousemove
鼠标在元素上的每个移动都会触发此事件。
click
如果使用的是鼠标左键,则在同一个元素上的 mousedown
及 mouseup
相继触发后,触发该事件。
dblclick
在短时间内双击同一元素后触发。如今已经很少使用了。
contextmenu
在鼠标右键被按下时触发。还有其他打开上下文菜单的方式,例如使用特殊的键盘按键,在这种情况下它也会被触发,因此它并不完全是鼠标事件。
一个操作可能会触发多个事件。
在单动作触发多个事件时,事件是固定的,例如鼠标被按下时,会触发mousedown
—> mouseup
—>click
的顺序。
与点击相关的事件中有一个event.button
属性,这个属性可以知道用户具体点击了哪个鼠标按钮。
一般来说,我们不会在click
和contextmenu
事件中使用,因为我们已经知道了,前者用的是左键,后者用的是右键。
但是mousedown
和mouseup
事件中则可能需要用到这个属性,因为不管用户点到哪个键,都会触发这两个事件,所以我们需要知道用户到底按的是鼠标的哪个按钮。
event.button
的所有可能值如下:
鼠标按键状态 | event.button |
---|---|
左键 (主要按键) | 0 |
中键 (辅助按键) | 1 |
右键 (次要按键) | 2 |
X1 键 (后退按键) | 3 |
X2 键 (前进按键) | 4 |
还有一个 event.buttons
属性,其中以整数的形式存储着当前所有按下的鼠标按键。
鼠标是可以跟键盘按键组合的,鼠标事件中会包含组合键的信息
shiftKey
:ShiftaltKey
:Alt(或对于 Mac 是 Opt)ctrlKey
:CtrlmetaKey
:对于 Mac 是 Cmd,在 Mac 上我们通常使用Cmd
代替Ctrl
如果在事件期间按下了相应的键,则它们为 true
。
比如,下面这个按钮仅在 Alt+Shift+click 时才有效:
<button id="button">Alt+Shift+Click on me!</button>
<script>
button.onclick = function(event) {
if (event.altKey && event.shiftKey) {
alert('Hooray!');
}
};
</script>
- 相对于窗口的坐标:
clientX
和clientY
。 - 相对于文档的坐标:
pageX
和pageY
。
双击鼠标后它会选择文本。
如果按下鼠标左键,不松开的情况下移动鼠标,也会选择文本。
如果不想要这种默认行为,最合理的方式是防止浏览器对mousedown
进行操作的默认行为。
elem.addEventListener("mousedown", function (event) {
//这样就不会对文本选择
event.preventDefault();
});
如果我们允许用户选择文本,但是不允许用户复制内容,我们可以用另外一个事件:copy
在copy
事件上禁止默认行为就可以保护页面内容不被复制。
elem.addEventListener("copy", function (event) {
event.preventDefault();
});
当然,用户如果打开开发者工具还是可以复制的...
鼠标有以下属性:
-
button
—用户点的是鼠标的哪个键 -
组合键,如果按下则为
true
:altKey
,ctrlKey
,shiftKey
和metaKey
(Mac)。mac
下的metaKey
等于window
下的ctrlKey
-
文档坐标
pageX/Y
-
窗口坐标
clientX/Y
mousedown
的默认浏览器操作是文本选择,如果不想要,则可以取消这个默认行为。
mouseover
:鼠标指针移动上去触发mouseout
:鼠标指针移动出去触发
这些事件很特别,因为它们具有relatedTarget
属性。这个属性是对target
的补充。
当鼠标从一个元素离开去另一个元素时,其中一个元素就变成了target
,另一个就变成了relatedTarget
。
对于mouseover
:
event.target
—— 是鼠标移过的那个元素。event.relatedTarget
—— 是鼠标来自的那个元素(relatedTarget
→target
)。
mouseout
则刚好相反:
event.target
—— 是鼠标移出的那个元素。event.relatedTarget
—— 是鼠标当前移动到那个元素(target
→relatedTarget
)。
这很合理,当鼠标移入a
元素时,此时触发mouseover
事件,target
自然是当前a
元素。
当鼠标从a
元素转为移入b
元素时,对a
来说,触发了mouseout
事件,对b
来说,触发了mouseover
事件,所以mouseout
下的target
就是a
,mouseover
(此时为b
元素触发的事件)中的relatedTarget
则是a
。
relatedTarget
可以是null
,比如鼠标来自窗口之外,或者离开了窗口。
当鼠标移动时,会触发mousemove
事件。但不意味着每个元素都会导致一个事件。
浏览器会一直检查鼠标的位置,如果发生了变化,就会触发事件。
如果访问者非常快速地移动鼠标,那么某些DOM元素就可能被跳过。
如果鼠标从上图所示的 #FROM
快速移动到 #TO
元素,则中间的 <div>
(或其中的一些)元素可能会被跳过。mouseout
事件可能会在 #FROM
上被触发,然后立即在 #TO
上触发 mouseover
。
这对性能有好处,因为可能有很多中间元素,我们并不想真的要处理每一个移入和离开的过程。
鼠标指针并不会访问所有元素,它可以跳过一些元素。
因此,当鼠标从窗口外调到页面中间,这种情况下,relatedTarget
就可能是null
。
在鼠标快速移动下,中间元素可能会被忽略。但如果mouseover
被触发了,当鼠标离开元素时,则一定会有mouseout
事件
mouseout
有一个重要的知识点——当鼠标指针从元素移动到其后代的时候会触发,例如下面这个结构,从#parent
到#child
:
<div id="parent">
<div id="child">...</div>
</div>
如果我们在#parent
上,将鼠标指针移到#child
上,#parent
会得到一个mouseout
事件。
根据浏览器的逻辑,鼠标指针可能随时位于单个元素上 —— 嵌套最多的那个元素(或z-index最大的那个)。
因此,如果他转到另一个元素(可能是后代),那么它将离开前一个元素。
这里的事件处理有一个重要的细节:
后代的mouseover
事件会冒泡。因此如果#parent
具有mouseover
处理程序,它也将被触发。
所以当鼠标移动到#child
时,会触发#parent
的moustout
事件和mouseover
事件
我们并不希望发生这种情况,我们可以通过检查relatedTarget
来避免这种事的发生。
但还有一种更好的方式:mouseenter
和mouseleave
事件,它们没有这样的问题。
当鼠标指针进入一个元素时 —— 会触发 mouseenter
。而鼠标指针在元素或其后代中的确切位置无关紧要。
当鼠标指针离开该元素时,事件 mouseleave
才会触发。
这两个事件跟mouseover
和mouseleave
差不多,但它们有以下区别:
- 在元素内部与后代之间的转换不会有影响
- 不会冒泡
当鼠标移入子元素时,就算此时触发了子元素的mouseenter
,但是并不会冒泡给父元素。
由于mouseenter
和mouseleave
并不会冒泡,所以我们不能用它们来进行事件委托。
但如果我们一定要用到事件委托,就只能够用mouseover
和mouseout
来做额外的处理。
- 快速地鼠标移动可能会忽略掉某些元素
mouseover/out
和mouseenter/leave
事件还有一个附加属性:relatedTarget
。- 如果我们从父元素转到子元素,也会触发
mouseover/out
事件。浏览器假定鼠标一次只会在最深的那个元素。 mouseenter/leave
跟over/out
不同,它们仅在鼠标进入和离开时才会触发。它们并不会冒泡。
本质上来说,一个鼠标的位置变动就是让我们设置元素的left
和top
。
基本的拖放算法是这样的:
- 鼠标点击后,获取元素的坐标:元素当前相对于窗口的坐标、鼠标点击的坐标、鼠标到元素边缘的距离
- 鼠标移动,设置元素的属性为
position:absolute
,并用left
和top
来移动它的位置 - 鼠标抬起后取消移动事件
预览:
直接贴代码:
<style>
#ball {
position: absolute;
}
</style>
<body>
<img id="ball" src="https://js.cx/clipart/ball.svg" />
<script>
let mouseX, mouseY, elemX, elemY, shiftX, shiftY;
// 1 取消浏览器对图片和一些其他元素的拖放处理的默认行为
ball.ondragstart = function () {
return false;
};
function onMove(event) {
// 4
ball.style.left = event.pageX - shiftX + "px";
ball.style.top = event.pageY - shiftY + "px";
}
// 2
ball.addEventListener("mousedown", function (event) {
//取坐标
mouseX = event.clientX; //鼠标位置x
mouseY = event.clientY; //鼠标位置y
elemX = this.getBoundingClientRect().left; //元素的窗口位置x
elemY = this.getBoundingClientRect().top; //元素的窗口位置Y
// 3
shiftX = mouseX - elemX; //鼠标位置距离元素左边的坐标
shiftY = mouseY - elemY; //鼠标位置距离元素右边的坐标
//2 向document添加移动事件监听
document.addEventListener("mousemove", onMove);
});
ball.addEventListener("mouseup", function (event) {
//取消移动事件 5
document.removeEventListener("mousemove", onMove);
});
</script>
</body>
这里一共需要三个API,分别为鼠标按下去mousedown
、鼠标移动mousemove
、鼠标抬起mouseup
。
-
由于拖动的元素是图片,浏览器有自己的对图片和一些其他元素的拖放处理,所以要禁止默认行为
ondragstart
。 -
mousemove
会经常被触发,但不会针对每个像素都如此。因此,在快速移动鼠标后,鼠标指针可能会从球上跳转至文档中间的某个位置(甚至跳转至窗口外)。总而言之,快速的鼠标移动让浏览器跳过了ball
上的鼠标移动事件,所以这里需要在document
上跟踪mousemove
,而不是ball
上。 -
shiftX
跟shiftY
代表的是鼠标的位置距离元素左侧/上侧边缘坐标的距离。本质上来说,一个鼠标的位置变动就是让我们设置元素的
left
和top
。我们需要计算鼠标在窗口的位置 - 元素在窗口的位置,就能够得到鼠标距离元素边缘的
shiftX
和shiftY
shiftX
=鼠标的pageX
- 元素的窗口坐标getBoundingClientRect().left
shiftY
=鼠标的pageY
- 元素的窗口坐标getBoundingClientRect().top
有个太先进而导致浏览器还未全部兼容的API,offsetX跟offsetY,可以直接得到shiftX和shiftY的值。
-
移动后的鼠标相对于文档(不是窗口)的位置减去
shiftX/Y
就可以得到left
和top
。 -
当鼠标抬起代表移动要结束,直接删除移动事件就可以了。
球可以放置在任何地方,下面完成一个当球放置在目标元素时,目标元素高亮的功能。
我们需要知道目标元素是什么。当我们拖动时,我们可以利用document.elementFromPoint(clientX,clientY)
通过坐标的位置来知道当前被移动到的是哪个元素。
问题是我们的鼠标是一直在球这个元素上的,所以我们需要另外处理一下,否则document.elementFromPoint(clientX,clientY)
将会一直返回球(因为它在最上面)。
处理方式如下:
ball.hidden = true;// 先隐藏球
currentDrop = document.elementFromPoint(event.clientX, event.clientY);//获取球下面的元素
ball.hidden = false;
当返回的元素是目标元素或者是它的子元素时,我们就可以处理让目标元素高亮的元素了。
if (currentDrop.closest("#div")) {//匹配球下面的元素的祖先(或自己)的id是否为目标元素的。
enterDroppable(div);
} else {
leaveDroppable(div);
}
实现效果:
完整代码:
<style>
body {
height: 2000px;
width: 100%;
}
#ball {
position: absolute;
}
#div {
width: 200px;
height: 100px;
border: 1px solid red;
}
</style>
<body>
<div id="div"></div>
<img id="ball" src="https://js.cx/clipart/ball.svg" />
<script>
let mouseX, mouseY, elemX, elemY, shiftX, shiftY, currentDrop;
// 1 取消浏览器对图片和一些其他元素的拖放处理的默认行为
ball.ondragstart = function () {
return false;
};
function enterDroppable(elem) {
elem.style.background = "red";
}
function leaveDroppable(elem) {
elem.style.background = "";
}
function onMove(event) {
// 4
ball.style.left = event.pageX - shiftX + "px";
ball.style.top = event.pageY - shiftY + "px";
ball.hidden = true;
currentDrop = document.elementFromPoint(event.clientX, event.clientY);
ball.hidden = false;
if (currentDrop.closest("#div")) {
enterDroppable(div);
} else {
leaveDroppable(div);
}
}
// 2
ball.addEventListener("mousedown", function (event) {
//取坐标
mouseX = event.clientX; //鼠标位置x
mouseY = event.clientY; //鼠标位置y
elemX = this.getBoundingClientRect().left; //元素的窗口位置x
elemY = this.getBoundingClientRect().top; //元素的窗口位置Y
// 3
shiftX = mouseX - elemX; //鼠标位置距离元素左边的坐标
shiftY = mouseY - elemY; //鼠标位置距离元素右边的坐标
//2 向document添加移动事件监听
document.addEventListener("mousemove", onMove);
});
ball.addEventListener("mouseup", function (event) {
//取消移动事件 5
document.removeEventListener("mousemove", onMove);
});
</script>
</body>
基础的使用鼠标的拖放方法是这样的:
- 事件构成:
mousedown
—>mousemove
—>mouseup
(由于浏览器对图片等有默认拖拽行为,所以这里取消dragstart
的默认行为) - 拖放本质上就是给被拖放的元素设置
left
和top
,这里需要获取到鼠标指针对于元素的边缘的坐标距离shiftX/shiftY
,也可以直接用offsetX/Y
但兼容性不好 - 使用
document.elementFromPoint
检测鼠标指针下的 “droppable” 的元素。 - 由于
mousemove
事件触发频繁,有可能会忽略元素,所以需要设置在顶层document
上。
keydown
键盘按下keyup
键盘抬起
事件对象的key
属性允许获得字符,而code
属性则允许获取物理按键代码。
以下以按键z/Z为例
Key | event.key |
event.code |
---|---|---|
Z | z (小写) |
KeyZ |
Shift+Z | Z (大写) |
KeyZ |
F1 | F1 |
F1 |
Backspace | Backspace |
Backspace |
Shift | Shift |
ShiftRight 或 ShiftLeft |
同一个按键 Z,可以与或不与 Shift
一起按下。我们会得到两个不同的字符:小写的 z
和大写的 Z
。
event.key
正是这个字符,并且它将是不同的。但是,event.code
是相同的。
event.code
可以标明是左边的还是右边的shift被按下了。
我们要处理一个热键:Ctrl+Z(或 Mac 上的 Cmd+Z),则可以用以下代码:
document.addEventListener('keydown', function(event) {
if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) {
alert('Undo!')
}
});
event.code
有一个问题。对于不同的键盘布局,相同的按键可能会具有不同的字符。
下面是美式布局(“QWERTY”)和德式布局(“QWERTZ”)
对于同一个按键,美式布局为 “Z”,而德式布局为 “Y”(字母被替换了)。
从字面上看,对于使用德式布局键盘的人来说,但他们按下 Y 时,event.code
将等于 KeyZ
。
因此,event.code
可能由于意外的键盘布局而与错误的字符进行了匹配。不同键盘布局中的相同字母可能会映射到不同的物理键,从而导致了它们有不同的代码。
为了可靠地跟踪与受键盘布局影响的字符,使用 event.key
可能是一个更好的方式。
另一方面,event.code
的好处是,即使访问者更改了语言,绑定到物理键位置的 event.code
会始终保持不变。因此,即使在切换了语言的情况下,依赖于它的热键也能正常工作。
我们想要处理与布局有关的按键?那么 event.key
是我们必选的方式。
或者我们希望一个热键即使在切换了语言后,仍能正常使用?那么 event.code
可能会更好。
如果按下一个键足够长的时间,它就会开始“自动重复”:keydown
会被一次又一次地触发,然后当按键被释放时,我们最终会得到 keyup
。因此,有很多 keydown
却只有一个 keyup
是很正常的。
对于由自动重复触发的事件,event
对象的 event.repeat
属性被设置为 true
。
默认行为各不相同,因为键盘可能会触发很多可能的东西。
例如:
- 出现在屏幕上的一个字符(最明显的结果)。
- 一个字符被删除(Delete 键)。
- 滚动页面(PageDown 键)。
- 浏览器打开“保存页面”对话框(Ctrl+S)
- ……等。
阻止对 keydown
的默认行为可以取消大多数的行为,但基于 OS 的特殊按键除外。例如,在 Windows 中,Alt+F4 会关闭当前浏览器窗口。并且无法通过在 JavaScript 中阻止默认行为来阻止它。
过去曾经有一个 keypress
事件,还有事件对象的 keyCode
、charCode
和 which
属性。
大多数浏览器对它们都存在兼容性问题,虽然浏览器还在支持它们,但现在完全没必要再使用它们。
按一个按键总是会产生一个键盘事件,无论是符号键,还是例如 Shift 或 Ctrl 等特殊按键。唯一的例外是有时会出现在笔记本电脑的键盘上的 Fn 键。它没有键盘事件,因为它通常是被在比 OS 更低的级别上实现的。
键盘事件:
keydown
—— 在按下键时(如果长按按键,则将自动重复),keyup
—— 释放按键时。
键盘事件的主要属性:
code
—— “按键代码”("KeyA"
,"ArrowLeft"
等),特定于键盘上按键的物理位置。key
—— 字符("A"
,"a"
等),对于非字符(non-character)的按键,通常具有与code
相同的值。
过去,键盘事件有时会被用于跟踪表单字段中的用户输入。这并不可靠,因为输入可能来自各种来源。
scroll
事件在window
和可滚动元素上都可以运行。
下面是一个滚动事件的示例:
window.addEventListener("scroll", function (event) {
console.log(this.pageXOffset);
console.log(this.pageYOffset);
});
我们不能通过在 onscroll
监听器中使用 event.preventDefault()
来阻止滚动,因为默认事件是在滚动发生 之后 才触发的。
我们可以设置overflow: hidden;
来让防止滚动。