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>的后代之间移动)被筛选掉,以便仅当指针离开或进入整个<td>时,onEnter/Leave才会运行。

以下是包含所有详细信息的完整示例

结果
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 比较一次。

P.S. 请注意:解决方案测试使用 dispatchEvent 来查看工具提示是否正常工作。

在沙盒中打开带有测试的解决方案。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行代码 – 将其包装在 <pre> 标记中,对于 10 行以上的代码 – 使用沙盒 (plnkrjsbincodepen…)