Мышь: движение mouseover/out, mouseenter/leave

В этой главе мы рассмотрим события, возникающие при движении мыши над элементами.

События mouseover/mouseout, свойство relatedTarget

Событие mouseover происходит, когда мышь появляется над элементом, а mouseout – когда уходит из него.

При этом мы можем узнать, с какого элемента пришла (или на какой ушла) мышь, используя дополнительное свойство объекта события relatedTarget.

Например, в обработчике события mouseover:

  • event.target – элемент, на который пришла мышь, то есть на котором возникло событие.
  • event.relatedTarget – элемент, с которого пришла мышь.

Для mouseout всё наоборот:

  • event.target – элемент, с которого ушла мышь, то есть на котором возникло событие.
  • event.relatedTarget – элемент, на который перешла мышь.

В примере ниже, если у вас есть мышь, вы можете наглядно посмотреть события mouseover/out, возникающие на всех элементах.

Результат
script.js
style.css
index.html
document.body.onmouseover = document.body.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 {
  height: 130px;
  margin: 0;
  padding: 0;
}

[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 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>

  <textarea id="log" style="width:400px;height:100px">Здесь будут события и relatedTarget
  </textarea>


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

</body>

</html>
relatedTarget может быть null

Свойство relatedTarget может быть равно null.

Это вполне нормально и означает, что мышь пришла не с другого элемента, а из-за пределов окна (или ушла за окно). Мы обязательно должны иметь в виду такую возможность, когда пишем код, который обращается к свойствам event.relatedTarget.

Частота событий

Событие mousemove срабатывает при передвижении мыши. Но это не значит, что каждый пиксель экрана порождает отдельное событие!

События mousemove и mouseover/mouseout срабатывают так часто, насколько это позволяет внутренняя система взаимодействия с мышью браузера.

Это означает, что если посетитель двигает мышью быстро, то DOM-элементы, через которые мышь проходит на большой скорости, могут быть пропущены.

При быстром движении с элемента #FROM до элемента #TO, как изображено на картинке выше – промежуточные <DIV> будут пропущены. Сработает только событие mouseout на #FROM и mouseover на #TO.

На практике это полезно, потому что таких промежуточных элементов может быть много, и если обрабатывать заход и уход с каждого, будут дополнительные вычислительные затраты.

С другой стороны, мы должны это понимать и не рассчитывать на то, что мышь аккуратно пройдёт с одного элемента на другой и так далее. Нет, она «прыгает».

В частности, возможна ситуация, когда курсор прыгает в середину страницы, и при этом relatedTarget=null, то есть он пришёл «ниоткуда» (на самом деле извне окна):

Обратим внимание ещё на такую деталь. При быстром движении курсор окажется над #TO сразу, даже если этот элемент глубоко в DOM. Его родители при движении сквозь них события не поймают.

Попробуйте увидеть это «вживую» на тестовом стенде ниже.

Его HTML представляет собой два вложенных div'а.

Молниеносно проведите мышью над вложенными элементами. При этом может не быть ни одного события или их получит только красный div, а может быть только зеленый.

А еще попробуйте зайти курсором мыши на красный div и потом быстро вывести мышь из него куда-нибудь сквозь зеленый. Если движение мыши достаточно быстрое, то родительский элемент будет проигнорирован.

Результат
script.js
style.css
index.html
green.onmouseover = green.onmouseout = green.onmousemove = handler;

function handler(event) {
  var type = event.type;
  while (type < 11) type += ' ';

  log(type + " target=" + event.target.id)
  return false;
}


function clearText() {
  text.value = "";
  lastMessage = "";
}

var lastMessageTime = 0;
var lastMessage = "";
var repeatCounter = 1;

function log(message) {
  if (lastMessageTime == 0) lastMessageTime = new Date();

  var 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;
}
#green {
  height: 50px;
  width: 160px;
  background: green;
}

#red {
  height: 20px;
  width: 110px;
  background: red;
  color: white;
  font-weight: bold;
  padding: 5px;
  text-align: center;
  margin: 20px;
}

#text {
  font-size: 12px;
  height: 200px;
  width: 360px;
  display: block;
}
<!doctype html>
<html lang="en">

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

<body>

  <div id="green">
    <div id="red">Тест</div>
  </div>

  <input onclick="clearText()" value="Очистить" type="button">

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

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

</body>

</html>

Важно иметь в виду эту особенность событий, чтобы не написать код, который рассчитан на последовательный проход над элементами. В остальном это вполне удобно.

«Лишний» mouseout при уходе на потомка

Представьте ситуацию – курсор зашёл на элемент. Сработал mouseover на нём. Потом курсор идёт на дочерний… И, оказывается, на элементе-родителе при этом происходит mouseout! Как будто курсор с него ушёл, хотя он всего лишь перешёл на потомка.

При переходе на потомка срабатывает mouseout на родителе.

Это кажется странным, но легко объяснимо.

Согласно браузерной логике, курсор мыши может быть только над одним элементом – самым глубоким в DOM (и верхним по z-index).

Так что если он перешел куда-нибудь, то автоматически ушёл с предыдущего элемента. Всё просто.

Самое забавное начинается чуть позже.

Ведь события mouseover и mouseout всплывают.

Получается, что если поставить обработчики mouseover и mouseout на #FROM и #TO, то последовательность срабатывания при переходе #FROM#TO будет следующей:

  1. mouseout на #FROMevent.target=#FROM, event.relatedTarget=#TO).
  2. mouseover на #TOevent.target=#TO, event.relatedTarget=#FROM).
  3. Событие mouseover после срабатывания на #TO всплывает выше, запуская обработчики mouseover на родителях. Ближайший родитель – как раз #FROM, то есть сработает обработчик mouseover на нём, с теми же значениями target/relatedTarget.

Если посмотреть на 1) и 3), то видно, что на #FROM сработает сначала mouseout, а затем с #TO всплывёт mouseover.

Если по mouseover мы что-то показываем, а по mouseout – скрываем, то получится «мигание».

У обработчиков создаётся впечатление, что курсор ушёл mouseout с родителя, а затем тут же перешёл mouseover на него (за счёт всплытия mouseover с потомка).

Это можно увидеть в примере ниже. В нём красный div вложен в синий. На синем стоит обработчик, который записывает его mouseover/mouseout.

Зайдите на синий элемент, а потом переведите мышь на красный – и наблюдайте за событиями:

Результат
script.js
style.css
index.html
function mouselog(event) {
  text.value += event.type + ' [target: ' + event.target.className + ']\n'
  text.scrollTop = text.scrollHeight
}
.blue {
  background: blue;
  width: 160px;
  height: 160px;
  position: relative;
}

.red {
  background: red;
  width: 100px;
  height: 100px;
  position: absolute;
  left: 30px;
  top: 30px;
}

textarea {
  height: 100px;
  width: 400px;
  display: block;
}
<!doctype html>
<html>

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

<body>

  <div class="blue" onmouseover="mouselog(event)" onmouseout="mouselog(event)">
    <div class="red"></div>
  </div>

  <textarea id="text">mouseover [target: blue] mouseout [target: blue] mouseover [target: red]
  </textarea>
  <input type="button" onclick="text.value=''" value="Очистить">

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

</body>

</html>
  1. При заходе на синий – на нём сработает mouseover [target: blue].
  2. При переходе с синего на красный – будет mouseout [target: blue] – уход с родителя.
  3. …И тут же mouseover [target: red] – как ни странно, «обратный переход» на родителя.

На самом деле, обратного перехода нет. Событие mouseover сработало на потомке (видно по target: red), а затем всплыло.

Если действия при наведении и уходе курсора с родителя простые, например скрытие/показ подсказки, то можно вообще ничего не заметить. Ведь события происходят сразу одно за другим, подсказка будет скрыта по mouseout и тут же показана по mouseover.

Если же происходит что-то более сложное, то бывает важно отследить момент «настоящего» ухода, то есть понять, когда элемент зашёл на родителя, а когда ушёл – без учёта переходов по дочерним элементам.

Для этого можно использовать события mouseenter/mouseleave, которые мы рассмотрим далее.

События mouseenter и mouseleave

События mouseenter/mouseleave похожи на mouseover/mouseout. Они тоже срабатывают, когда курсор заходит на элемент и уходит с него, но с двумя отличиями.

  1. Не учитываются переходы внутри элемента.
  2. События mouseenter/mouseleave не всплывают.

Эти события более интуитивно понятны.

Курсор заходит на элемент – срабатывает mouseenter, а затем – неважно, куда он внутри него переходит, mouseleave будет, когда курсор окажется за пределами элемента.

Вы можете увидеть, как они работают проведя курсором над голубым DIV'ом ниже. Обработчик стоит только на внешнем, синем элементе. Обратите внимание – лишних событий при переходе на красного потомка нет!

Результат
script.js
style.css
index.html
function log(event) {
  text.value += event.type + ' [target: ' + event.target.id + ']\n';
  text.scrollTop = text.scrollHeight;
}
#blue {
  background: blue;
  width: 160px;
  height: 160px;
  position: relative;
}

#red {
  background: red;
  width: 70px;
  height: 70px;
  position: absolute;
  left: 45px;
  top: 45px;
}

#text {
  display: block;
  height: 100px;
  width: 400px;
}
<!DOCTYPE HTML>
<html>

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

<body>

  <div id="blue" onmouseenter="log(event)" onmouseleave="log(event)">
    <div id="red"></div>
  </div>

  <textarea id="text"></textarea>
  <input type="button" onclick="text.value=''" value="Очистить">

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

</body>

</html>

Делегирование

События mouseenter/leave более наглядны и понятны, но они не всплывают, а значит с ними нельзя использовать делегирование.

Представим, что нам нужно обработать вход/выход мыши для ячеек таблицы. А в таблице таких ячеек тысяча.

Естественное решение – поставить обработчик на верхний элемент <table> и ловить все события в нём. Но события mouseenter/leave не всплывают, они срабатывают именно на том элементе, на котором стоит обработчик и только на нём.

Если обработчики mouseenter/leave стоят на <table>, то они сработают при входе-выходе из таблицы, но получить из них какую-то информацию о переходах по её ячейкам невозможно.

Не беда – воспользуемся mouseover/mouseout.

Простейший вариант обработчиков выглядит так:

table.onmouseover = function(event) {
  var target = event.target;
  target.style.background = 'pink';
};

table.onmouseout = function(event) {
  var target = event.target;
  target.style.background = '';
};
Результат
script.js
style.css
index.html
table.onmouseover = function(event) {
  var target = event.target;
  target.style.background = 'pink';
  text.value += "mouseover " + target.tagName + "\n";
  text.scrollTop = text.scrollHeight;
};

table.onmouseout = function(event) {
  var target = event.target;
  target.style.background = '';
  text.value += "mouseout " + 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;
}

#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="Очистить">


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

</body>

</html>

В таком виде они срабатывают при переходе с любого элемента на любой. Нас же интересуют переходы строго с одной ячейки <td> на другую.

Нужно фильтровать события.

Один из вариантов:

  • Запоминать текущий подсвеченный <td> в переменной.
  • При mouseover проверять, остались ли мы внутри того же <td>, если да – игнорировать.
  • При mouseout проверять, если мы ушли с текущего <td>, а не перешли куда-то внутрь него, то игнорировать.

Детали кода вы можете посмотреть в примере ниже, который демонстрирует этот подход:

Результат
script.js
style.css
index.html
// элемент TD, внутри которого сейчас курсор
var currentElem = null;

table.onmouseover = function(event) {
  if (currentElem) {
    // перед тем, как зайти в новый элемент, курсор всегда выходит из предыдущего
    //
    // если мы еще не вышли, значит это переход внутри элемента, отфильтруем его
    return;
  }

  // посмотрим, куда пришёл курсор
  var target = event.target;

  // уж не на TD ли?
  while (target != this) {
    if (target.tagName == 'TD') break;
    target = target.parentNode;
  }
  if (target == this) return;

  // да, элемент перешёл внутрь TD!
  currentElem = target;
  target.style.background = 'pink';
};


table.onmouseout = function(event) {
  // если курсор и так снаружи - игнорируем это событие
  if (!currentElem) return;

  // произошёл уход с элемента - проверим, куда, может быть на потомка?
  var relatedTarget = event.relatedTarget;
  if (relatedTarget) { // может быть relatedTarget = null
    while (relatedTarget) {
      // идём по цепочке родителей и проверяем,
      // если переход внутрь currentElem - игнорируем это событие
      if (relatedTarget == currentElem) return;
      relatedTarget = relatedTarget.parentNode;
    }
  }

  // произошло событие mouseout, курсор ушёл
  currentElem.style.background = '';
  currentElem = null;
};
#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;
}

#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>

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

</body>

</html>

Попробуйте по-разному, быстро или медленно заходить и выходить в ячейки таблицы. Обработчики mouseover/mouseout стоят на table, но при помощи делегирования корректно обрабатывают вход-выход.

Особенности IE8-

В IE8- нет свойства relatedTarget. Вместо него используется fromElement для mouseover и toElement для mouseout.

Можно «исправить» несовместимость с relatedTarget так:

function fixRelatedTarget(e) {
  if (e.relatedTarget === undefined) {
    if (e.type == 'mouseover') e.relatedTarget = e.fromElement;
    if (e.type == 'mouseout') e.relatedTarget = e.toElement;
  }
}

Итого

У mouseover, mousemove, mouseout есть следующие особенности:

  • При быстром движении мыши события mouseover, mousemove, mouseout могут пропускать промежуточные элементы.
  • События mouseover и mouseout – единственные, у которых есть вторая цель: relatedTarget (toElement/fromElement в IE).
  • События mouseover/mouseout подразумевают, что курсор находится над одним, самым глубоким элементом. Они срабатывают при переходе с родительского элемента на дочерний.

События mouseenter/mouseleave не всплывают и не учитывают переходы внутри элемента.

Задачи

важность: 5

Напишите JS-код, который будет показывать всплывающую подсказку над элементом, если у него есть атрибут data-tooltip.

Условие аналогично задаче Поведение "подсказка", но здесь необходима поддержка вложенных элементов. При наведении показывается самая вложенная подсказка.

Например:

<div data-tooltip="Это – внутренность дома" id="house">
  <div data-tooltip="Это – крыша" id="roof"></div>
  ...
  <a href="http://ru.wikipedia.org/wiki/Три_поросёнка" data-tooltip="Читать дальше">Наведи на меня</a>
</div>

Результат в ифрейме с документом:

Вы можете использовать как заготовку решение задачи Поведение "подсказка".

Открыть песочницу для задачи.

важность: 5

Нужно написать функцию, которая показывает подсказку при наведении на элемент, но не при быстром проходе над ним.

То есть, если посетитель именно навёл курсор мыши на элемент и почти остановился – подсказку показать, а если быстро провёл над ним, то не надо, зачем излишнее мигание?

Технически – можно измерять скорость движения мыши над элементом, если она маленькая, то считаем, что это «наведение на элемент» (показать подсказку), если большая – «быстрый проход мимо элемента» (не показывать).

Реализуйте это через универсальный объект new HoverIntent(options), с параметрами options:

  • elem – элемент, наведение на который нужно отслеживать.
  • over – функция-обработчик наведения на элемент.
  • out – функция-обработчик ухода с элемента (если было наведение).

Пример использования такого объекта для подсказки:

// образец подсказки
var tooltip = document.createElement('div');
tooltip.className = "tooltip";
tooltip.innerHTML = "Подсказка";

// при "наведении на элемент" показать подсказку
new HoverIntent({
  elem: elem,
  over: function() {
    tooltip.style.left = this.getBoundingClientRect().left + 'px';
    tooltip.style.top = this.getBoundingClientRect().bottom + 5 + 'px';
    document.body.appendChild(tooltip);
  },
  out: function() {
    document.body.removeChild(tooltip);
  }
});

Демо этого кода:

Если провести мышкой над «часиками» быстро, то ничего не будет, а если медленно или остановиться на них, то появится подсказка.

Обратите внимание – подсказка не «мигает» при проходе мыши внутри «часиков», по подэлементам.

Открыть песочницу с тестами для задачи.

Будем замерять скорость движения курсора.

Для этого можно запустить setInterval, который каждые 100 мс (или другой интервал) будет сравнивать текущие координаты курсора с предыдущими и, если расстояние пройдено маленькое, считаем, что посетитель «навёл указатель на элемент», вызвать options.over.

В браузере нет способа «просто получить» текущие координаты. Это может сделать обработчик события, в данном случае mousemove. Поэтому нужно будет поставить обработчик на mousemove и при каждом движении запоминать текущие координаты, чтобы setInterval мог раз в 100 мс сравнивать их.

Имеет смысл начинать анализ координат и отслеживание mousemove при заходе на элемент, а заканчивать – при выходе с него.

Чтобы точно отловить момент входа и выхода, без учёта подэлементов (во избежание мигания), можно использовать mouseenter/mouseleave.

В решении, предложенном ниже, однако, используется mouseover/mouseout, так как это позволит легко «прикрутить» к такому объекту делегирование, если потребуется. А, чтобы не было лишних срабатываний, лишние переходы отфильтровываются.

При этом при обнаружении «наведения на элемент» это запоминается в переменной isHover и вызывается options.over, а затем, при выходе с элемента, если было «наведение», вызывается options.out.

Открыть решение с тестами в песочнице.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.