2022 年 4 月 17 日

滑鼠移動:mouseover/out、mouseenter/leave

讓我們深入探討當滑鼠在元素之間移動時發生的事件。

mouseover/mouseout 事件、relatedTarget

當滑鼠指標移到元素上方時,會發生 mouseover 事件,而當滑鼠指標離開元素時,會發生 mouseout 事件。

這些事件很特別,因為它們有屬性 relatedTarget。此屬性補充了 target。當滑鼠從一個元素移到另一個元素時,其中一個會變成 target,另一個會變成 relatedTarget

對於 mouseover

  • event.target - 滑鼠移入的元素。
  • event.relatedTarget - 滑鼠移出的元素 (relatedTargettarget)。

對於 mouseout 則相反

  • event.target - 滑鼠移出的元素。
  • event.relatedTarget - 滑鼠移入的新元素 (targetrelatedTarget)。

在以下範例中,每個臉孔及其特徵都是獨立的元素。當您移動滑鼠時,您可以在文字區域中看到滑鼠事件。

每個事件都有關於 targetrelatedTarget 的資訊

結果
script.js
style.css
index.html
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 = ''
  }
}
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%;
}
<!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>
relatedTarget 可以是 null

relatedTarget 屬性可以是 null

這是正常的,表示滑鼠不是從另一個元素移入,而是從視窗外移入。或者表示滑鼠移出視窗。

在我們的程式碼中使用 event.relatedTarget 時,我們應該記住這種可能性。如果我們存取 event.relatedTarget.tagName,就會發生錯誤。

略過元素

當滑鼠移動時,會觸發 mousemove 事件。但這並不表示每個像素都會產生一個事件。

瀏覽器會不時檢查滑鼠位置。如果它偵測到變更,就會觸發事件。

這表示如果訪客移動滑鼠的速度非常快,則可能會略過一些 DOM 元素

如果滑鼠從 #FROM 元素移動到 #TO 元素的速度非常快,如上所述,則可能會略過中間的 <div> 元素(或其中一些元素)。mouseout 事件可能會在 #FROM 上觸發,然後立即在 #TO 上觸發 mouseover

這對效能有益,因為可能會有很多中間元素。我們並不想處理每個元素的進入和離開。

另一方面,我們應該記住滑鼠指標不會「拜訪」沿途的所有元素。它可能會「跳躍」。

特別是,指標有可能直接從視窗外跳到頁面中間。在這種情況下,relatedTargetnull,因為它來自「無處」。

您可以在以下測試台上「現場」查看。

它的 HTML 有兩個巢狀元素:<div id="child"><div id="parent"> 內部。如果您快速地將滑鼠移過它們,那麼可能只有子 div 會觸發事件,或者可能是父 div,或者可能根本不會有任何事件。

此外,將指標移到子 div 中,然後快速地向下移出父 div。如果移動夠快,那麼父元素將被忽略。滑鼠將越過父元素而沒有注意到它。

結果
script.js
style.css
index.html
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;
}
#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;
}
<!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>
如果觸發了 mouseover,則一定有 mouseout

在滑鼠快速移動的情況下,中間元素可能會被忽略,但有一件事我們可以肯定:如果指標「正式」進入一個元素(產生 mouseover 事件),那麼在離開它時,我們總是會得到 mouseout

離開子元素時觸發 Mouseout

mouseout 的一個重要特點是,當指標從一個元素移動到它的後代時,它會觸發,例如,從這個 HTML 中的 #parent 移動到 #child

<div id="parent">
  <div id="child">...</div>
</div>

如果我們在 #parent 上,然後將指標更深入地移到 #child 中,我們會在 #parent 上得到 mouseout

這可能看起來很奇怪,但很容易解釋。

根據瀏覽器的邏輯,滑鼠游標在任何時候只能在一個單一元素上——最巢狀的元素和 z 軸索引最高的元素。

因此,如果它轉到另一個元素(即使是後代),那麼它就會離開前一個元素。

請注意事件處理的另一個重要細節。

後代上的 mouseover 事件會冒泡。因此,如果 #parentmouseover 處理常式,它會觸發

您可以在下面的範例中很好地看到這一點:<div id="child"><div id="parent"> 內部。#parent 元素上有 mouseover/out 處理常式,用於輸出事件詳細資料。

如果您將滑鼠從 #parent 移到 #child,您會在 #parent 上看到兩個事件

  1. mouseout [target: parent](離開父元素),然後
  2. mouseover [target: child](來到子元素,冒泡)。
結果
script.js
style.css
index.html
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 {
  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;
}
<!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 元素移動到 #child 時,父元素上會觸發兩個處理常式:mouseoutmouseover

parent.onmouseout = function(event) {
  /* event.target: parent element */
};
parent.onmouseover = function(event) {
  /* event.target: child element (bubbled) */
};

如果我們不檢查處理常式內的 event.target,那麼看起來滑鼠指標離開了 #parent 元素,然後立即又回到它上面。

但事實並非如此!指標仍然在父元素上,它只是更深入地移動到子元素中。

如果在離開父元素時有一些動作,例如動畫在 parent.onmouseout 中執行,那麼當指標只是更深入地進入 #parent 時,我們通常不希望它發生。

為避免這種情況,我們可以在處理常式中檢查 relatedTarget,如果滑鼠仍然在元素內部,則忽略此類事件。

或者,我們可以使用其他事件:mouseentermouseleave,我們現在將介紹它們,因為它們沒有這樣的問題。

事件 mouseenter 和 mouseleave

事件 mouseenter/mouseleave 類似於 mouseover/mouseout。當滑鼠指標進入/離開元素時,它們會觸發。

但有兩個重要的差異

  1. 元素內部、傳遞到/從後代的轉換不會計算在內。
  2. 事件 mouseenter/mouseleave 不會冒泡。

這些事件非常簡單。

當指標進入元素時,會觸發 mouseenter。指標在元素或其後代中的確切位置並不重要。

當指標離開元素時,會觸發 mouseleave

此範例類似於上述範例,但現在頂層元素具有 mouseenter/mouseleave,而不是 mouseover/mouseout

如你所見,唯一產生的事件是與將指標移入和移出頂層元素相關的事件。當指標移動到子代並返回時,不會發生任何事。後代之間的轉換會被忽略

結果
script.js
style.css
index.html
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 {
  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;
}
<!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>

事件委派

事件 mouseenter/leave 非常簡單且易於使用。但它們不會冒泡。因此,我們無法對它們使用事件委派。

想像一下,我們想要處理表格儲存格的滑鼠進入/離開。而且有數百個儲存格。

自然的解決方案是,在 <table> 上設定處理常式,並在那裡處理事件。但 mouseenter/leave 沒有冒泡。因此,如果此類事件發生在 <td> 上,則只有該 <td> 上的處理常式才能捕捉到它。

<table> 上的 mouseenter/leave 處理常式僅在指標進入/離開整個表格時觸發。無法取得其內部轉換的任何資訊。

因此,讓我們使用 mouseover/mouseout

讓我們從簡單的處理常式開始,它會突顯滑鼠下的元素

// let's highlight an element under the pointer
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';
};

它們正在執行中。當滑鼠移動到此表格的元素上時,會突顯目前的元素

結果
script.js
style.css
index.html
table.onmouseover = function(event) {
  let target = event.target;
  target.style.background = 'pink';

  text.value += `over -> ${target.tagName}\n`;
  text.scrollTop = text.scrollHeight;
};

table.onmouseout = function(event) {
  let target = event.target;
  target.style.background = '';

  text.value += `out <- ${target.tagName}\n`;
  text.scrollTop = text.scrollHeight;
};
#text {
  display: block;
  height: 100px;
  width: 456px;
}

#table th {
  text-align: center;
  font-weight: bold;
}

#table td {
  width: 150px;
  white-space: nowrap;
  text-align: center;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 12px;
  cursor: pointer;
}

#table .nw {
  background: #999;
}

#table .n {
  background: #03f;
  color: #fff;
}

#table .ne {
  background: #ff6;
}

#table .w {
  background: #ff0;
}

#table .c {
  background: #60c;
  color: #fff;
}

#table .e {
  background: #09f;
  color: #fff;
}

#table .sw {
  background: #963;
  color: #fff;
}

#table .s {
  background: #f60;
  color: #fff;
}

#table .se {
  background: #0c3;
  color: #fff;
}

#table .highlight {
  background: red;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>


  <table id="table">
    <tr>
      <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
    </tr>
    <tr>
      <td class="nw"><strong>Northwest</strong>
        <br>Metal
        <br>Silver
        <br>Elders
      </td>
      <td class="n"><strong>North</strong>
        <br>Water
        <br>Blue
        <br>Change
      </td>
      <td class="ne"><strong>Northeast</strong>
        <br>Earth
        <br>Yellow
        <br>Direction
      </td>
    </tr>
    <tr>
      <td class="w"><strong>West</strong>
        <br>Metal
        <br>Gold
        <br>Youth
      </td>
      <td class="c"><strong>Center</strong>
        <br>All
        <br>Purple
        <br>Harmony
      </td>
      <td class="e"><strong>East</strong>
        <br>Wood
        <br>Blue
        <br>Future
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Southwest</strong>
        <br>Earth
        <br>Brown
        <br>Tranquility
      </td>
      <td class="s"><strong>South</strong>
        <br>Fire
        <br>Orange
        <br>Fame
      </td>
      <td class="se"><strong>Southeast</strong>
        <br>Wood
        <br>Green
        <br>Romance
      </td>
    </tr>

  </table>

  <textarea id="text"></textarea>

  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>
</html>

在我們的案例中,我們想要處理表格儲存格 <td> 之間的轉換:進入儲存格和離開儲存格。其他轉換,例如儲存格內部或任何儲存格外部,我們不感興趣。讓我們將它們篩選出來。

以下是我們可以執行的操作

  • 記住目前突顯的 <td> 在變數中,我們稱之為 currentElem
  • mouseover 上,如果我們仍位於目前的 <td> 內,則忽略事件。
  • mouseout 上,如果我們沒有離開目前的 <td>,則忽略事件。

以下是考量所有可能情況的程式碼範例

// <td> under the mouse right now (if any)
let currentElem = null;

table.onmouseover = function(event) {
  // before entering a new element, the mouse always leaves the previous one
  // if currentElem is set, we didn't leave the previous <td>,
  // that's a mouseover inside it, ignore the event
  if (currentElem) return;

  let target = event.target.closest('td');

  // we moved not into a <td> - ignore
  if (!target) return;

  // moved into <td>, but outside of our table (possible in case of nested tables)
  // ignore
  if (!table.contains(target)) return;

  // hooray! we entered a new <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function(event) {
  // if we're outside of any <td> now, then ignore the event
  // that's probably a move inside the table, but out of <td>,
  // e.g. from <tr> to another <tr>
  if (!currentElem) return;

  // we're leaving the element – where to? Maybe to a descendant?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // go up the parent chain and check – if we're still inside currentElem
    // then that's an internal transition – ignore it
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // we left the <td>. really.
  onLeave(currentElem);
  currentElem = null;
};

// any functions to handle entering/leaving an element
function onEnter(elem) {
  elem.style.background = 'pink';

  // show that in textarea
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

function onLeave(elem) {
  elem.style.background = '';

  // show that in textarea
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}

再次強調,重要的功能有

  1. 它使用事件委派來處理表格內任何 <td> 的進入/離開。因此它依賴於 mouseover/out,而不是不會冒泡且因此不允許委派的 mouseenter/leave
  2. 會過濾掉額外的事件,例如在 <td> 的後代之間移動,以便 onEnter/Leave 僅在指標離開或進入整個 <td> 時才執行。

以下是包含所有詳細資料的完整範例

結果
script.js
style.css
index.html
// <td> under the mouse right now (if any)
let currentElem = null;

table.onmouseover = function(event) {
  // before entering a new element, the mouse always leaves the previous one
  // if currentElem is set, we didn't leave the previous <td>,
  // that's a mouseover inside it, ignore the event
  if (currentElem) return;

  let target = event.target.closest('td');

  // we moved not into a <td> - ignore
  if (!target) return;

  // moved into <td>, but outside of our table (possible in case of nested tables)
  // ignore
  if (!table.contains(target)) return;

  // hooray! we entered a new <td>
  currentElem = target;
  onEnter(currentElem);
};


table.onmouseout = function(event) {
  // if we're outside of any <td> now, then ignore the event
  // that's probably a move inside the table, but out of <td>,
  // e.g. from <tr> to another <tr>
  if (!currentElem) return;

  // we're leaving the element – where to? Maybe to a descendant?
  let relatedTarget = event.relatedTarget;

  while (relatedTarget) {
    // go up the parent chain and check – if we're still inside currentElem
    // then that's an internal transition – ignore it
    if (relatedTarget == currentElem) return;

    relatedTarget = relatedTarget.parentNode;
  }

  // we left the <td>. really.
  onLeave(currentElem);
  currentElem = null;
};

// any functions to handle entering/leaving an element
function onEnter(elem) {
  elem.style.background = 'pink';

  // show that in textarea
  text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
  text.scrollTop = 1e6;
}

function onLeave(elem) {
  elem.style.background = '';

  // show that in textarea
  text.value += `out <- ${elem.tagName}.${elem.className}\n`;
  text.scrollTop = 1e6;
}
#text {
  display: block;
  height: 100px;
  width: 456px;
}

#table th {
  text-align: center;
  font-weight: bold;
}

#table td {
  width: 150px;
  white-space: nowrap;
  text-align: center;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 12px;
  cursor: pointer;
}

#table .nw {
  background: #999;
}

#table .n {
  background: #03f;
  color: #fff;
}

#table .ne {
  background: #ff6;
}

#table .w {
  background: #ff0;
}

#table .c {
  background: #60c;
  color: #fff;
}

#table .e {
  background: #09f;
  color: #fff;
}

#table .sw {
  background: #963;
  color: #fff;
}

#table .s {
  background: #f60;
  color: #fff;
}

#table .se {
  background: #0c3;
  color: #fff;
}

#table .highlight {
  background: red;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>


  <table id="table">
    <tr>
      <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
    </tr>
    <tr>
      <td class="nw"><strong>Northwest</strong>
        <br>Metal
        <br>Silver
        <br>Elders
      </td>
      <td class="n"><strong>North</strong>
        <br>Water
        <br>Blue
        <br>Change
      </td>
      <td class="ne"><strong>Northeast</strong>
        <br>Earth
        <br>Yellow
        <br>Direction
      </td>
    </tr>
    <tr>
      <td class="w"><strong>West</strong>
        <br>Metal
        <br>Gold
        <br>Youth
      </td>
      <td class="c"><strong>Center</strong>
        <br>All
        <br>Purple
        <br>Harmony
      </td>
      <td class="e"><strong>East</strong>
        <br>Wood
        <br>Blue
        <br>Future
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Southwest</strong>
        <br>Earth
        <br>Brown
        <br>Tranquility
      </td>
      <td class="s"><strong>South</strong>
        <br>Fire
        <br>Orange
        <br>Fame
      </td>
      <td class="se"><strong>Southeast</strong>
        <br>Wood
        <br>Green
        <br>Romance
      </td>
    </tr>

  </table>

  <textarea id="text"></textarea>

  <input type="button" onclick="text.value=''" value="Clear">

  <script src="script.js"></script>

</body>
</html>

嘗試將游標移入和移出表格儲存格,以及在儲存格內移動。快速或緩慢都沒有關係。與之前的範例不同,只有整個 <td> 會被反白顯示。

摘要

我們介紹了事件 mouseovermouseoutmousemovemouseentermouseleave

以下事項值得注意

  • 快速移動滑鼠可能會跳過中間元素。
  • 事件 mouseover/outmouseenter/leave 有額外的屬性:relatedTarget。那是我們來自/前往的元素,與 target 互補。

即使我們從父元素移動到子元素,事件 mouseover/out 仍會觸發。瀏覽器假設滑鼠一次只能在一個元素上,也就是最深的元素。

事件 mouseenter/leave 在這方面不同:它們僅在滑鼠進入和離開整個元素時觸發。此外,它們不會冒泡。

任務

重要性:5

撰寫 JavaScript,在具有屬性 data-tooltip 的元素上顯示提示工具。此屬性的值應成為提示工具文字。

這類似於任務 提示工具行為,但此處標註的元素可以巢狀。顯示最深層巢狀的提示工具。

一次只能顯示一個提示工具。

例如

<div data-tooltip="Here – is the house interior" id="house">
  <div data-tooltip="Here – is the roof" id="roof"></div>
  ...
  <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a>
</div>

iframe 中的結果

為任務開啟沙盒。

重要性:5

撰寫一個函式,僅在訪客將滑鼠移動到元素上(而不是透過元素)時,才在元素上顯示提示工具。

換句話說,如果訪客將滑鼠移動到元素上並停在那裡,則顯示提示工具。如果他們只是將滑鼠移過元素,則不需要,誰想要額外的閃爍?

技術上,我們可以測量滑鼠在元素上的速度,如果速度很慢,則我們假設滑鼠「移到元素上」並顯示提示工具,如果速度很快,則我們忽略它。

為它製作一個通用物件 new HoverIntent(options)

它的 options

  • elem – 要追蹤的元素。
  • over – 如果滑鼠移到元素上,要呼叫的函式:也就是滑鼠緩慢移動或停留在元素上。
  • out – 如果滑鼠離開元素(如果已呼叫 over),要呼叫的函式。

使用此類物件作為提示工具的範例

// a sample tooltip
let tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Tooltip";

// the object will track mouse and call 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();
  }
});

示範

如果你快速將滑鼠移到「時鐘」上,什麼事都不會發生;如果你緩慢移動或停留在時鐘上,就會出現提示工具。

請注意:當游標在時鐘子元素之間移動時,提示工具不會「閃爍」。

開啟沙盒進行測試。

演算法看起來很簡單

  1. 在元素上放置 onmouseover/out 處理常式。也可以在此使用 onmouseenter/leave,但它們較不通用,如果我們引入委派,它們將無法運作。
  2. 當滑鼠游標進入元素時,開始在 mousemove 上測量速度。
  3. 如果速度很慢,則執行 over
  4. 當我們離開元素,且已執行 over 時,執行 out

但是如何測量速度?

第一個想法可能是:每 100ms 執行一次函式,並測量前一個和新的座標之間的距離。如果距離很小,則速度很慢。

不幸的是,在 JavaScript 中沒有辦法取得「目前滑鼠座標」。沒有像 getCurrentMouseCoordinates() 這樣的函式。

取得座標的唯一方法是偵聽滑鼠事件,例如 mousemove,並從事件物件取得座標。

因此,讓我們在 mousemove 上設定一個處理常式,以追蹤座標並記住它們。然後每 100ms 比較它們一次。

附註:請注意:解法測試使用 dispatchEvent 來查看提示工具是否運作正常。

在沙盒中開啟包含測試的解法。

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改善建議 - 請 提交 GitHub 問題 或提交拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容 - 請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,對於多行程式碼 - 將其包覆在 <pre> 標籤中,對於超過 10 行的程式碼 - 使用沙盒 (plnkrjsbincodepen…)