Конспект JS-course

События движения: "mouseover/out/move/leave/enter"

Источник: http://learn.javascript.ru/mousemove-events

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

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

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

В этих событиях мышь переходит с одного элемента на другой. Оба этих элемента можно получить из свойств объекта события.

  • mouseover
    • Элемент под курсором — event.target (IE: srcElement).
    • Элемент, с которого курсор пришел — event.relatedTarget (IE: fromElement)
  • mouseout
    • Элемент, с которого курсор пришел — event.target (IE: srcElement).
    • Элемент под курсором — event.relatedTarget (IE: toElement).

Как вы видите, спецификация W3C объединяет fromElement и toElement в одно свойство relatedTarget, которое работает, как fromElement для mouseover и как toElement для mouseout.

В IE это свойство можно поставить так:

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

Значение relatedTarget (toElement/fromElement) может быть null Такое бывает, например, когда мышь приходит из-за пределов окна у mouseover будет relatedTarget = null.

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

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

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

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

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

Несмотря на некоторую концептуальную странность такого подхода, он весьма разумен. Хотя браузер и может пропустить промежуточные элементы, он гарантирует, что если уж мышь зашла на элемент (сработало событие mouseover), то при выходе с него сработает и mouseout. Не может быть mouseover без mouseout и наоборот.

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

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

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

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

Так что если он перешел на потомка — значит ушёл с родителя.

Получается, что при переходе на потомка курсор уходит mouseout с родителя, а затем тут же переходит mouseover на него. Причем возвращение происходит за счёт всплытия mouseover с потомка.

События mouseenter и mouseleave.

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

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

События mouseenter/mouseleave не всплывают.

Превращение mouseover/out в mouseenter/leave

Для браузеров, в которых нет поддержки этих событий, можно повесить обработчик на mouseover/mouseout, а лишние события — фильтровать.

При mouseout можно получить элемент, на который осуществляется переход (e.relatedTarget), и проверить — является ли новый элемент потомком родителя. Если да — мышь с родителя не уходила, игнорировать это событие. При mouseover — аналогичным образом проверить, мышь «пришла» с потомка? Если с потомка, то это не настоящий переход на родителя, игнорировать. Посмотрим, как это выглядит, на примере кода:

<div style="padding:10px; margin:10px; border: 2px solid blue" id="outer">
 <p style="border: 1px solid green">
   Обработчики mouseover/mouseout стоят на синем родителе.
 </p>
 <blockquote style="border: 1px solid red">
   ..Но срабатывают и при любых переходах по его потомкам!
  </blockquote>
</div>
<b id="info">Тут будет информация о событиях.</b>

<script>
var outer = document.getElementById('outer')
var info = document.getElementById('info');

outer.onmouseout = function(e) {
  e = e || event;
  var target = e.target || e.srcElement;
  info.innerHTML = e.type+', target:'+target.tagName;
};

outer.onmouseover = function(e) {
  e = e || event;
  var target = e.target || e.srcElement;
  info.innerHTML = e.type+', target:'+target.tagName;
};

</script>

Итого

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

  1. События mouseove и mouseout — единственные, у которых есть вторая цель: relatedTarget (toElement/fromElement в IE).
  2. Событие mouseout срабатывает, когда мышь уходит с родительского элемента на дочерний. Используйте mouseenter/mouseleave или фильтруйте их, чтобы избежать излишнего реагирования.
  3. При быстром движении мыши события mouseover, mousemove, mouseout могут пропускать промежуточные элементы. Мышь может моментально возникнуть над потомком, миновав при этом его родителя.

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