我們將深入研究鼠標在元素之間移動時發(fā)生的事件。
當鼠標指針移到某個元素上時,mouseover
事件就會發(fā)生,而當鼠標離開該元素時,mouseout
事件就會發(fā)生。
這些事件很特別,因為它們具有 relatedTarget
屬性。此屬性是對 target
的補充。當鼠標從一個元素離開并去往另一個元素時,其中一個元素就變成了 target
,另一個就變成了 relatedTarget
。
對于 mouseover
:
event.target
? —— 是鼠標移過的那個元素。event.relatedTarget
? —— 是鼠標來自的那個元素(?relatedTarget
? → ?target
?)。mouseout
則與之相反:
event.target
? —— 是鼠標離開的元素。event.relatedTarget
? —— 是鼠標移動到的,當前指針位置下的元素(?target
? → ?relatedTarget
?)。在下面這個示例中,每張臉及其功能都是單獨的元素。當你移動鼠標時,你可以在文本區(qū)域中看到鼠標事件。
每個事件都具有關于 target
和 relatedTarget
的信息:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="container">
<div class="smiley-green">
<div class="left-eye"></div>
<div class="right-eye"></div>
<div class="smile"></div>
</div>
<div class="smiley-yellow">
<div class="left-eye"></div>
<div class="right-eye"></div>
<div class="smile"></div>
</div>
<div class="smiley-red">
<div class="left-eye"></div>
<div class="right-eye"></div>
<div class="smile"></div>
</div>
</div>
<textarea id="log">Events will show up here!
</textarea>
<script src="script.js"></script>
</body>
</html>
body,
html {
margin: 0;
padding: 0;
}
#container {
border: 1px solid brown;
padding: 10px;
width: 330px;
margin-bottom: 5px;
box-sizing: border-box;
}
#log {
height: 120px;
width: 350px;
display: block;
box-sizing: border-box;
}
[class^="smiley-"] {
display: inline-block;
width: 70px;
height: 70px;
border-radius: 50%;
margin-right: 20px;
}
.smiley-green {
background: #a9db7a;
border: 5px solid #92c563;
position: relative;
}
.smiley-green .left-eye {
width: 18%;
height: 18%;
background: #84b458;
position: relative;
top: 29%;
left: 22%;
border-radius: 50%;
float: left;
}
.smiley-green .right-eye {
width: 18%;
height: 18%;
border-radius: 50%;
position: relative;
background: #84b458;
top: 29%;
right: 22%;
float: right;
}
.smiley-green .smile {
position: absolute;
top: 67%;
left: 16.5%;
width: 70%;
height: 20%;
overflow: hidden;
}
.smiley-green .smile:after,
.smiley-green .smile:before {
content: "";
position: absolute;
top: -50%;
left: 0%;
border-radius: 50%;
background: #84b458;
height: 100%;
width: 97%;
}
.smiley-green .smile:after {
background: #84b458;
height: 80%;
top: -40%;
left: 0%;
}
.smiley-yellow {
background: #eed16a;
border: 5px solid #dbae51;
position: relative;
}
.smiley-yellow .left-eye {
width: 18%;
height: 18%;
background: #dba652;
position: relative;
top: 29%;
left: 22%;
border-radius: 50%;
float: left;
}
.smiley-yellow .right-eye {
width: 18%;
height: 18%;
border-radius: 50%;
position: relative;
background: #dba652;
top: 29%;
right: 22%;
float: right;
}
.smiley-yellow .smile {
position: absolute;
top: 67%;
left: 19%;
width: 65%;
height: 14%;
background: #dba652;
overflow: hidden;
border-radius: 8px;
}
.smiley-red {
background: #ee9295;
border: 5px solid #e27378;
position: relative;
}
.smiley-red .left-eye {
width: 18%;
height: 18%;
background: #d96065;
position: relative;
top: 29%;
left: 22%;
border-radius: 50%;
float: left;
}
.smiley-red .right-eye {
width: 18%;
height: 18%;
border-radius: 50%;
position: relative;
background: #d96065;
top: 29%;
right: 22%;
float: right;
}
.smiley-red .smile {
position: absolute;
top: 57%;
left: 16.5%;
width: 70%;
height: 20%;
overflow: hidden;
}
.smiley-red .smile:after,
.smiley-red .smile:before {
content: "";
position: absolute;
top: 50%;
left: 0%;
border-radius: 50%;
background: #d96065;
height: 100%;
width: 97%;
}
.smiley-red .smile:after {
background: #d96065;
height: 80%;
top: 60%;
left: 0%;
}
container.onmouseover = container.onmouseout = handler;
function handler(event) {
function str(el) {
if (!el) return "null"
return el.className || el.tagName;
}
log.value += event.type + ': ' +
'target=' + str(event.target) +
', relatedTarget=' + str(event.relatedTarget) + "\n";
log.scrollTop = log.scrollHeight;
if (event.type == 'mouseover') {
event.target.style.background = 'pink'
}
if (event.type == 'mouseout') {
event.target.style.background = ''
}
}
?
relatedTarget
? 可以為 ?null
?
relatedTarget
屬性可以為null
。
這是正?,F象,僅僅是意味著鼠標不是來自另一個元素,而是來自窗口之外?;蛘咚x開了窗口。
當我們在代碼中使用
event.relatedTarget
時,我們應該牢記這種可能性。如果我們訪問event.relatedTarget.tagName
,那么就會出現錯誤。
當鼠標移動時,就會觸發(fā) mousemove
事件。但這并不意味著每個像素都會導致一個事件。
瀏覽器會一直檢查鼠標的位置。如果發(fā)現了變化,就會觸發(fā)事件。
這意味著,如果訪問者非常快地移動鼠標,那么某些 DOM 元素就可能被跳過:
如果鼠標從上圖所示的 #FROM
快速移動到 #TO
元素,則中間的 <div>
(或其中的一些)元素可能會被跳過。mouseout
事件可能會在 #FROM
上被觸發(fā),然后立即在 #TO
上觸發(fā) mouseover
。
這對性能很有好處,因為可能有很多中間元素。我們并不真的想要處理每一個移入和離開的過程。
另一方面,我們應該記住,鼠標指針并不會“訪問”所有元素。它可以“跳過”一些元素。
特別是,鼠標指針可能會從窗口外跳到頁面的中間。在這種情況下,relatedTarget
為 null
,因為它是從石頭縫里蹦出來的(nowhere):
它的 HTML 有兩個嵌套的元素:<div id="child">
在 <div id="parent">
內部。如果將鼠標快速移動到它們上,則可能只有 <div id="child">
或者只有 <div id="parent">
觸發(fā)事件,或者根本沒有事件觸發(fā)。
還可以將鼠標指針移動到 <div id="child">
中,然后將其快速向下移動過其父級元素。如果移動速度足夠快,則父元素就會被忽略。鼠標會越過父元素而不會引起其注意。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="parent">parent
<div id="child">child</div>
</div>
<textarea id="text"></textarea>
<input onclick="clearText()" value="Clear" type="button">
<script src="script.js"></script>
</body>
</html>
#parent {
background: #99C0C3;
width: 160px;
height: 120px;
position: relative;
}
#child {
background: #FFDE99;
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
textarea {
height: 140px;
width: 300px;
display: block;
}
let parent = document.getElementById('parent');
parent.onmouseover = parent.onmouseout = parent.onmousemove = handler;
function handler(event) {
let type = event.type;
while (type.length < 11) type += ' ';
log(type + " target=" + event.target.id)
return false;
}
function clearText() {
text.value = "";
lastMessage = "";
}
let lastMessageTime = 0;
let lastMessage = "";
let repeatCounter = 1;
function log(message) {
if (lastMessageTime == 0) lastMessageTime = new Date();
let time = new Date();
if (time - lastMessageTime > 500) {
message = '------------------------------\n' + message;
}
if (message === lastMessage) {
repeatCounter++;
if (repeatCounter == 2) {
text.value = text.value.trim() + ' x 2\n';
} else {
text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + repeatCounter + "\n";
}
} else {
repeatCounter = 1;
text.value += message + "\n";
}
text.scrollTop = text.scrollHeight;
lastMessageTime = time;
lastMessage = message;
}
如果 ?
mouseover
? 被觸發(fā)了,則必須有 ?mouseout
?在鼠標快速移動的情況下,中間元素可能會被忽略,但是我們可以肯定一件事:如果鼠標指針“正式地”進入了一個元素(生成了
mouseover
事件),那么一旦它離開,我們就會得到mouseout
。
mouseout
的一個重要功能 —— 當鼠標指針從元素移動到其后代時觸發(fā),例如在下面的這個 HTML 中,從 #parent
到 #child
:
<div id="parent">
<div id="child">...</div>
</div>
如果我們在 #parent
上,然后將鼠標指針更深入地移入 #child
,在 #parent
上我們會得到 mouseout
!
這聽起來很奇怪,但很容易解釋。
根據瀏覽器的邏輯,鼠標指針隨時可能位于單個元素上 —— 嵌套最多的那個元素(z-index 最大的那個)。
因此,如果它轉到另一個元素(甚至是一個后代),那么它將離開前一個元素。
請注意事件處理的另一個重要的細節(jié)。
后代的 mouseover
事件會冒泡。因此,如果 #parent
具有 mouseover
處理程序,它將被觸發(fā):
你可以在下面這個示例中很清晰地看到這一點:<div id="child">
位于 <div id="parent">
內部。#parent
元素上有 mouseover/out
的處理程序,這些處理程序用于輸出事件詳細信息。
如果你將鼠標從 #parent
移動到 #child
,那么你會看到在 #parent
上有兩個事件:
mouseout [target: parent]
?(離開 parent),然后mouseover [target: child]
?(來到 child,冒泡)。<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="parent" onmouseover="mouselog(event)" onmouseout="mouselog(event)">parent
<div id="child">child</div>
</div>
<textarea id="text"></textarea>
<input type="button" onclick="text.value=''" value="Clear">
<script src="script.js"></script>
</body>
</html>
#parent {
background: #99C0C3;
width: 160px;
height: 120px;
position: relative;
}
#child {
background: #FFDE99;
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
textarea {
height: 140px;
width: 300px;
display: block;
}
function mouselog(event) {
let d = new Date();
text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
text.scrollTop = text.scrollHeight;
}
如上例所示,當鼠標指針從 #parent
元素移動到 #child
時,會在父元素上觸發(fā)兩個處理程序:mouseout
和 mouseover
:
parent.onmouseout = function(event) {
/* event.target: parent element */
};
parent.onmouseover = function(event) {
/* event.target: child element (bubbled) */
};
如果我們不檢查處理程序中的 event.target
,那么似乎鼠標指針離開了 #parent
元素,然后立即回到了它上面。
但是事實并非如此!鼠標指針仍然位于父元素上,它只是更深入地移入了子元素。
如果離開父元素時有一些行為(action),例如一個動畫在 parent.onmouseout
中運行,當鼠標指針深入 #parent
時,我們并不希望發(fā)生這種行為。
為了避免它,我們可以在處理程序中檢查 relatedTarget
,如果鼠標指針仍在元素內,則忽略此類事件。
另外,我們可以使用其他事件:mouseenter
和 mouseleave
,它們沒有此類問題,接下來我們就對其進行詳細介紹。
事件 mouseenter/mouseleave
類似于 mouseover/mouseout
。它們在鼠標指針進入/離開元素時觸發(fā)。
但是有兩個重要的區(qū)別:
mouseenter/mouseleave
? 不會冒泡。這些事件非常簡單。
當鼠標指針進入一個元素時 —— 會觸發(fā) mouseenter
。而鼠標指針在元素或其后代中的確切位置無關緊要。
當鼠標指針離開該元素時,事件 mouseleave
才會觸發(fā)。
這個例子和上面的例子相似,但是現在最頂部的元素有 mouseenter/mouseleave
而不是 mouseover/mouseout
。
正如你所看到的,唯一生成的事件是與將鼠標指針移入或移出頂部元素有關的事件。當鼠標指針進入 child 并返回時,什么也沒發(fā)生。在后代之間的移動會被忽略。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="parent" onmouseenter="mouselog(event)" onmouseleave="mouselog(event)">parent
<div id="child">child</div>
</div>
<textarea id="text"></textarea>
<input type="button" onclick="text.value=''" value="Clear">
<script src="script.js"></script>
</body>
</html>
#parent {
background: #99C0C3;
width: 160px;
height: 120px;
position: relative;
}
#child {
background: #FFDE99;
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
textarea {
height: 140px;
width: 300px;
display: block;
}
function mouselog(event) {
let d = new Date();
text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
text.scrollTop = text.scrollHeight;
}
事件 mouseenter/leave
非常簡單且易用。但它們不會冒泡。因此,我們不能使用它們來進行事件委托。
假設我們要處理表格的單元格的鼠標進入/離開。并且這里有數百個單元格。
通常的解決方案是 —— 在 <table>
中設置處理程序,并在那里處理事件。但 mouseenter/leave
不會冒泡。因此,如果類似的事件發(fā)生在 <td>
上,那么只有 <td>
上的處理程序才能捕獲到它。
<table>
上的 mouseenter/leave
的處理程序僅在鼠標指針進入/離開整個表格時才會觸發(fā)。無法獲取有關其內部移動的任何信息。
因此,讓我們使用 mouseover/mouseout
。
讓我們從高亮顯示鼠標指針下的元素的簡單處理程序開始:
// 高亮顯示鼠標指針下的元素
table.onmouseover = function(event) {
let target = event.target;
target.style.background = 'pink';
};
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
};
現在它們已經激活了。當鼠標在下面這個表格的各個元素上移動時,當前位于鼠標指針下的元素會被高亮顯示:
在我們的例子中,我們想要處理表格的單元格 <td>
之間的移動:進入一個單元格并離開它。我們對其他移動并不感興趣,例如在單元格內部或在所有單元格的外部。讓我們把這些過濾掉。
我們可以這樣做:
<td>
?,讓我們稱它為 ?currentElem
?。mouseover
? —— 如果我們仍然在當前的 ?<td>
? 中,則忽略該事件。mouseout
? —— 如果沒有離開當前的 ?<td>
?,則忽略。這是說明所有可能情況的代碼示例:
// 現在位于鼠標下方的 <td>(如果有)
let currentElem = null;
table.onmouseover = function(event) {
// 在進入一個新的元素前,鼠標總是會先離開前一個元素
// 如果設置了 currentElem,那么我們就沒有鼠標所懸停在的前一個 <td>,
// 忽略此事件
if (currentElem) return;
let target = event.target.closest('td');
// 我們移動到的不是一個 <td> —— 忽略
if (!target) return;
// 現在移動到了 <td> 上,但在處于了我們表格的外部(可能因為是嵌套的表格)
// 忽略
if (!table.contains(target)) return;
// 給力!我們進入了一個新的 <td>
currentElem = target;
onEnter(currentElem);
};
table.onmouseout = function(event) {
// 如果我們現在處于所有 <td> 的外部,則忽略此事件
// 這可能是一個表格內的移動,但是在 <td> 外,
// 例如從一個 <tr> 到另一個 <tr>
if (!currentElem) return;
// 我們將要離開這個元素 —— 去哪兒?可能是去一個后代?
let relatedTarget = event.relatedTarget;
while (relatedTarget) {
// 到父鏈上并檢查 —— 我們是否還在 currentElem 內
// 然后發(fā)現,這只是一個內部移動 —— 忽略它
if (relatedTarget == currentElem) return;
relatedTarget = relatedTarget.parentNode;
}
// 我們離開了 <td>。真的。
onLeave(currentElem);
currentElem = null;
};
// 任何處理進入/離開一個元素的函數
function onEnter(elem) {
elem.style.background = 'pink';
// 在文本區(qū)域顯示它
text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
text.scrollTop = 1e6;
}
function onLeave(elem) {
elem.style.background = '';
// 在文本區(qū)域顯示它
text.value += `out <- ${elem.tagName}.${elem.className}\n`;
text.scrollTop = 1e6;
}
再次,重要的功能是:
<td>
? 的進入/離開。因此,它依賴于 ?mouseover/out
? 而不是 ?mouseenter/leave
?,?mouseenter/leave
? 不會冒泡,因此也不允許事件委托。<td>
? 的后代之間移動都會被過濾掉,因此 ?onEnter/Leave
? 僅在鼠標指針進入/離開 ?<td>
? 整體時才會運行。這是帶有所有詳細信息的完整示例:
嘗試將鼠標指針移入和移出表格單元格及其內部??爝€是慢都沒關系。與前面的示例不同,只有 <td>
被作為一個整體高亮顯示。
我們講了 mouseover
,mouseout
,mousemove
,mouseenter
和 mouseleave
事件。
以下這些內容要注意:
mouseover/out
? 和 ?mouseenter/leave
? 事件還有一個附加屬性:?relatedTarget
?。這就是我們來自/到的元素,是對 ?target
? 的補充。即使我們從父元素轉到子元素時,也會觸發(fā) mouseover/out
事件。瀏覽器假定鼠標一次只會位于一個元素上 —— 最深的那個。
mouseenter/leave
事件在這方面不同:它們僅在鼠標進入和離開元素時才觸發(fā)。并且它們不會冒泡。
編寫 JavaScript,在帶有 data-tooltip
特性(attribute)的元素上顯示一個工具提示。該特性的值應該成為工具提示的文本。
與任務 工具提示行為 類似,但這里可以嵌套帶有注解(annotated)的元素。并且顯示的是嵌套最深的工具提示。
同一時間只能顯示一個工具提示。
例如:
<div data-tooltip="這是房子的內部" id="house">
<div data-tooltip="這里是屋頂" id="roof"></div>
...
<a rel="external nofollow" target="_blank" data-tooltip="Read on…">鼠標懸浮在我上</a>
</div>
在 iframe 中的結果:
編寫一個函數,該函數僅在訪問者將鼠標 移至 元素而不是 移過 元素的情況下,在該元素上顯示工具提示。
換句話說,如果訪問者將鼠標移至元素上,并停下來 —— 顯示工具提示。如果他們只是將鼠標移過元素,那就沒必要顯示,誰想要多余的閃爍呢?
從技術上說,我們可以測量元素上的鼠標移動速度,如果速度很慢,那么我們就假定它 在元素上,并顯示工具提示,如果速度很快 —— 那么我們就忽略它。
為此,我們創(chuàng)建一個通用對象 new HoverIntent(options)
。
其 options
:
elem
? —— 要跟蹤的元素。over
? —— 鼠標移動到元素上時要調用的函數:即,鼠標在元素上的移動速度很慢,或者停在該元素上。out
? —— 當鼠標離開元素時調用的函數(如果 ?over
? 已經被調用過了)。在工具提示中使用此類對象的示例:
// 一個簡單的工具提示
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";
// 該對象將跟蹤鼠標,并調用 over/out
new HoverIntent({
elem,
over() {
tooltip.style.left = elem.getBoundingClientRect().left + 'px';
tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px';
document.body.append(tooltip);
},
out() {
tooltip.remove();
}
});
如果你將鼠標快速地從“時鐘”上移動過去,那么什么都不會發(fā)生,如果你使用鼠標在“時鐘”上慢慢移動,或者停在“時鐘”上,則會出現一個工具提示。
請注意:當鼠標指針在“時鐘”的元素之間移動時,工具提示不會“閃爍”
算法看起來很簡單:
onmouseover/out
? 處理程序放在元素上。在這里也可以使用 ?onmouseenter/leave
?,但是它們的通用性較差,如果我們想引入事件委托時,它則無法使用。mousemove
? 上的速度。over
?。over
? 也執(zhí)行了,則會運行 ?out
?。但是如何測量速度?
第一個想法是:每 100ms
運行一次函數,并測量前坐標和新坐標之間的距離。如果很小,那么速度就很小。
不幸的是,在 JavaScript 中無法獲取“鼠標當前坐標”。沒有像 getCurrentMouseCoordinates()
這樣的函數。
獲取坐標的唯一方法是監(jiān)聽例如 mousemove
這樣的鼠標事件。
因此,我們可以在 mousemove
上設置一個處理程序來跟蹤坐標并記住它們。然后我每 100ms
比較一次。
P.S. 請注意:解決方案測試使用 dispatchEvent
來檢查工具提示是否正確。
更多建議: