This commit is contained in:
Ilya Kantor 2014-11-16 01:40:20 +03:00
parent 962caebbb7
commit 87bf53d076
1825 changed files with 94929 additions and 0 deletions

View file

@ -0,0 +1,172 @@
# Мышь: движение mouseover/out, mouseenter/leave
В этой главе мы рассмотрим события, возникающие при движении мыши над элементами.
[cut]
## События mouseover/mouseout, свойство relatedTarget
Событие `mouseover` происходит, когда мышь появляется над элементом, а `mouseout` -- когда уходит из него.
При этом мы можем узнать, с какого элемента пришла (или на какой ушла) мышь, используя дополнительное свойство `relatedTarget`.
В случае `mouseover` оно содержит элемент, *с которого* пришла мышь, а для `mouseout` -- *на который* ушла.
В примере ниже вы можете наглядно посмотреть события `mouseover/out`, возникающие на всех элементах.
[example src="mouseoverout" height=220]
Можно заметить, что в некоторых случаях значение `relatedTarget` может быть `null`. Это вполне нормально и означает, что мышь пришла из-за пределов окна (или ушла за окно).
## Частота событий mousemove и mouseover/out
Событие `mousemove` срабатывает при передвижении мыши. Но это не значит, что каждый пиксель экрана порождает отдельное событие!
События `mousemove` и `mouseover/mouseout` срабатывают так часто, насколько это позволяет внутренняя система взаимодействия с мышью браузера.
Это означает, что если вы двигаете мышью очень быстро, то DOM-элементы, через которые мышь проходит на большой скорости, могут быть пропущены.
К примеру:
<ul>
<li>Курсор может быстро перейти *над* родительским элементом в дочерний, при этом *не вызвав событий на родителе*, как будто он через родителя никогда не проходил.</li>
<li>Курсор может быстро выйти из дочернего элемента без генерации событий на родителе.</li>
</ul>
Попробуйте увидеть это "вживую" на тестовом стенде ниже.
Его HTML представляет собой два вложенных `div'а`.
Молниеносно проведите мышью над вложенными элементами. При этом может не быть ни одного события или их получит только красный `div`, а может быть только зеленый.
А еще попробуйте зайти курсором мыши на красный `div` и потом быстро вывести мышь из него куда-нибудь сквозь зеленый. Если движение мыши достаточно быстрое, то родительский элемент будет проигнорирован.
[example height=360 src="mouseoverout-fast"]
Важно иметь в виду эту особенность событий, чтобы не написать код, который рассчитан на последовательный проход над элементами.
## "Лишний" mouseout при уходе на потомка
Представьте ситуацию -- курсор зашёл на элемент. Сработал `mouseover` на нём. Потом курсор идёт на дочерний... И, оказывается, на элементе-родителе при этом происходит `mouseout`! Как будто курсор с него ушёл, хотя он всего лишь перешёл на потомка.
**При переходе на потомка срабатывает `mouseout` на родителе.**
Это кажется странным, но легко объяснимо.
**Согласно браузерной логике, курсор мыши может быть только над *одним* элементом -- самым глубоким в DOM (и верхним по z-index).**
Так что если он перешел куда-нибудь, то автоматически ушёл с предыдущего элемента. Всё просто.
К чему это приводит на практике, можно увидеть в примере ниже. В нём красный `div` вложен в синий. На синем стоит обработчик, который записывает его `mouseover/mouseout`.
Зайдите на синий элемент, а потом переведите мышь на красный -- и наблюдайте за событиями:
[example height=360 src="mouseoverout-child"]
<ol>
<li>При заходе на синий -- на нём сработает `mouseover [target: blue]`.</li>
<li>При переходе с синего на красный -- будет `mouseout [target: blue]` -- уход с родителя.</li>
<li>...И тут же `mouseover [target: red]` -- как ни странно, "обратный переход" на родителя.</li>
</ol>
На самом деле, обратного перехода нет. Событие `mouseover` сработало на потомке (видно по `target: red`), а затем всплыло.
**То есть, у кода создаётся впечатление, что курсор ушёл `mouseout` с родителя, а затем тут же перешёл `mouseover` на него (за счёт всплытия `mouseover` с потомка).**
Как это влияет на его поведение?
Если действия при наведении и уходе курсора с родителя простые, например скрытие/показ подсказки, то можно вообще ничего не заметить. Ведь события происходят одновременно, подсказка будет скрыта по `mouseout` и тут же показана по `mouseover`.
Если же происходит что-то более сложное, то бывает важно отследить момент "настоящего" ухода, то есть понять, когда элемент зашёл на родителя, а когда ушёл -- без учёта переходов по дочерним элементам.
Для этого можно использовать события `mouseenter/mouseleave`, которые мы рассмотрим далее.
## События mouseenter и mouseleave
События `mouseenter/mouseleave` похожи на `mouseover/mouseout`. Они тоже срабатывают, когда курсор заходит на элемент и уходит с него, но с двумя отличиями.
<ol>
<li>При переходе на потомка курсор не уходит с родителя.</li>
<li>События `mouseenter/mouseleave` не всплывают.</li>
</ol>
Эти события более интуитивно понятны. Курсор заходит на элемент -- срабатывает `mouseenter`, а затем -- неважно, куда он внутри него переходит, `mouseleave` будет, когда курсор окажется за пределами элемента.
Вы можете увидеть, как они работают проведя курсором над голубым `DIV'ом` ниже. Обработчик стоит только на внешнем, синем элементе. Обратите внимание -- лишних событий при переходе на красного потомка нет!
[example height=340 src="mouseleave"]
## Делегирование -- проблема mouseenter/leave
События `mouseenter/leave` более наглядны и понятны, но они не всплывают, а значит с ними нельзя использовать делегирование.
Представьте себе, что нам нужно обработать вход/выход мыши для ячеек таблицы. А в таблице таких ячеек тысяча.
Естественное решение -- поставить обработчик на верхний элемент `<table>` и ловить все события в нём. Но события `mouseenter/leave` не всплывают, а срабатывают именно на том элементе, на котором стоит обработчик и только на нём.
Это легко видеть в примере ниже: обработчики `mouseenter/leave` стоят на `<table>` и сработают при входе-выходе из таблицы, получить из них какую-то информацию о переходах по её ячейкам не представляется возможным:
[example height=480 src="mouseleave-table"]
Не беда -- воспользуемся `mouseover/mouseout`.
Но мы хотели бы, чтобы наши действия выполнялись только при входе-выходе в ячейку, без учета переходов внутри самих ячеек.
Если нужно делегирование -- нужно использовать `mouseover/out`.
Получится так:
[example height=450 src="mouseenter-mouseleave-delegation"]
В этом примере код обработчиков выглядит так:
```js
table.onmouseover = function(event) {
var target = event.target;
target.style.background = 'pink';
};
table.onmouseout = function(event) {
var target = event.target;
target.style.background = '';
};
```
Пока что они срабатывают на всём подряд. Их нужно фильтровать:
<ul>
<li>Вход-выход срабатывает на любых элементах, а нам нужны именно ячейки таблицы.
Для этого по `mouseover` нужно проверять, находится ли `event.target` был внутри `<td>`, это стандартная проверка при делегировании.</li>
<li>Лишними для нас будут `mouseover/mouseout`, связанные с переходом на дочерние элементы внутри `<td>`. Проверить их можно по `event.target/event.relatedTarget`.</li>
</ul>
Детали кода вы можете посмотреть в примере ниже, который демонстрирует этот подход:
[example height=450 src="mouseenter-mouseleave-delegation-2"]
Попробуйте по-разному, быстро или медленно заходить и выходить в ячейки таблицы. Обработчики `mouseover/mouseout` стоят на `table`, но при помощи делегирования корректно обрабатывают вход-выход.
## Особенности IE8-
В IE8- нет свойства `relatedTarget`. Вместо него используется `fromElement` для `mouseover` и `toElement` для `mouseout`.
Можно "исправить" несовместимость с `relatedTarget` так:
```js
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` есть следующие особенности:
<ol>
<li>События `mouseover` и `mouseout` -- единственные, у которых есть вторая цель: `relatedTarget` (`toElement/fromElement` в IE).</li>
<li>Событие `mouseout` срабатывает, когда мышь уходит с родительского элемента на дочерний. Используйте `mouseenter/mouseleave` или фильтруйте их, чтобы избежать излишнего реагирования.</li>
<li>При быстром движении мыши события `mouseover, mousemove, mouseout` могут пропускать промежуточные элементы. Мышь может моментально возникнуть над потомком, миновав при этом его родителя.</li>
</ol>