JavaScript Перемещение мыши: mouseover/out, mouseenter/leave
Разница между mouseover/mouseout и mouseenter/mouseleave в JavaScript: всплытие, relatedTarget, особенности дочерних элементов и примеры кода.
Понимание событий перемещения мыши в JavaScript: Mouseover, Mouseout, Mouseenter и Mouseleave
События перемещения мыши events в JavaScript дают разработчикам возможность реагировать на движение курсора над элементами веб-страницы. Эти события необходимы для создания интерактивных и отзывчивых интерфейсов, которые реагируют на действия пользователя. В этом руководстве мы рассмотрим различия между событиями mouseover, mouseout, mouseenter и mouseleave и приведём практические примеры их использования.
Две пары событий в сравнении
JavaScript предоставляет две пары событий для одного и того же физического действия — перемещения указателя на элемент и с элемента. На первый взгляд они взаимозаменяемы, но ведут себя очень по-разному при наличии дочерних элементов, и выбор неправильной пары является одной из наиболее распространённых ошибок в UI.
| Событие | Срабатывает когда указатель… | Всплывает? | Повторно срабатывает на дочерних элементах? |
|---|---|---|---|
mouseover | входит в элемент или любой его потомок | Да | Да |
mouseout | покидает элемент или любой его потомок | Да | Да |
mouseenter | пересекает границу самого элемента | Нет | Нет |
mouseleave | покидает границу самого элемента | Нет | Нет |
Mouseover и Mouseout
mouseover: Срабатывает, когда мышь входит в элемент или любой из его дочерних элементов. Поскольку событие всплывает, оно является правильным выбором для делегирования событий — один обработчик на контейнере может управлять наведением на множество дочерних элементов.mouseout: Зеркальное отражениеmouseover— срабатывает, когда мышь покидает элемент или любой из его дочерних элементов.
Особенность в том, что при перемещении указателя с родительского элемента на дочерний срабатывает mouseout для родителя (указатель «покинул» текстовую область родителя) сразу же за ним следует mouseover (он «вошёл» в дочерний элемент, который по-прежнему считается частью родителя). Таким образом, одно визуальное наведение может порождать шумный поток событий для родительского элемента с дочерними элементами.
Mouseenter и Mouseleave
mouseenter: Похоже наmouseover, но событие не всплывает и не срабатывает повторно при пересечении указателем границы дочернего элемента. Оно срабатывает ровно один раз, когда указатель впервые входит в границу элемента — идеально для поведения «выделить карточку при наведении».mouseleave: Срабатывает один раз, когда указатель покидает внешнюю границу элемента, игнорируя перемещения между потомками.
Практическое правило: используйте
mouseenter/mouseleave, когда нужна чистая логика «находится ли указатель над этим элементом?», иmouseover/mouseoutтолько тогда, когда вам нужно всплытие для делегирования событий.
Свойство relatedTarget
Оба события предоставляют свойство event.relatedTarget, которое указывает на другой элемент, участвующий в переходе:
- При
mouseover/mouseenterсвойствоrelatedTargetсодержит элемент, с которого пришёл указатель. - При
mouseout/mouseleaveсвойствоrelatedTargetсодержит элемент, на который перемещается указатель.
Именно так воспроизводится поведение mouseenter при использовании всплывающих событий mouseover/mouseout: нужно проверить, находится ли relatedTarget внутри текущего элемента, и игнорировать событие, если это так.
element.addEventListener('mouseout', function (event) {
// Ignore transitions to a descendant — only react to truly leaving.
if (this.contains(event.relatedTarget)) return;
console.log('Pointer really left the element');
});Обратите внимание, что relatedTarget может быть null — например, когда указатель приходит из-за пределов окна браузера — поэтому перед вызовом contains() нужно проверять это значение.
Особенность: «быстрое движение мыши»
События mouseover/mouseout не гарантированно срабатывают для каждого элемента, над которым проходит указатель. Если пользователь перемещает мышь очень быстро, промежуточные элементы могут быть полностью пропущены, и можно получить mouseout для одного элемента без соответствующего mouseover для следующего. Код, который использует оба события в паре, должен быть рассчитан на отсутствие одного из них. События mouseenter/mouseleave всегда сбалансированы для элемента, к которому они привязаны, что является ещё одной причиной предпочитать их для отслеживания состояния.
Практические примеры событий перемещения мыши
Эти примеры демонстрируют, как реализовать события перемещения мыши для улучшения пользовательского опыта с помощью интерактивных элементов.
Пример 1: Использование Mouseover и Mouseout
Этот пример показывает, как изменить цвет фона блока при входе и выходе курсора мыши, включая его дочерние элементы.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mouseover and Mouseout Example</title>
<style>
#box {
width: 200px;
height: 200px;
background-color: lightblue;
}
#innerBox {
width: 100px;
height: 100px;
background-color: lightcoral;
margin: 50px;
}
</style>
</head>
<body>
<div id="box">
Hover over me!
<div id="innerBox"></div>
</div>
<script>
document.getElementById('box').addEventListener('mouseover', function() {
this.style.backgroundColor = 'cyan';
});
document.getElementById('box').addEventListener('mouseout', function() {
this.style.backgroundColor = 'lightblue';
});
</script>
</body>
</html>Пояснение:
- Событие
mouseoverизменяет цвет фона блока на голубой, в том числе при наведении на внутренний блок. - Событие
mouseoutсбрасывает цвет фона, когда мышь покидает блок, с учётом внутреннего блока.
Пример 2: Использование Mouseenter и Mouseleave
Этот пример улучшает взаимодействие с пользователем, демонстрируя применение mouseenter и mouseleave для более точной реакции без влияния на дочерние элементы.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mouseenter and Mouseleave Visual Example</title>
<style>
#parent {
width: 400px;
height: 300px;
background-color: lightblue; /* Initial background color */
padding: 20px;
box-sizing: border-box;
position: relative;
display: flex;
justify-content: space-around;
align-items: center;
transition: background-color 0.3s ease;
}
.child {
width: 90px;
height: 90px;
background-color: lightsalmon;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease;
}
#feedback {
position: fixed;
bottom: 10px;
left: 10px;
background: white;
padding: 10px;
border: 1px solid #ccc;
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<div id="parent">
Parent Element
<div class="child">Child 1</div>
<div class="child">Child 2</div>
<div class="child">Child 3</div>
</div>
<div id="feedback">Hover over elements to see interactions.</div>
<script>
const parent = document.getElementById('parent');
const children = document.querySelectorAll('.child');
const feedback = document.getElementById('feedback');
parent.addEventListener('mouseenter', function() {
parent.style.backgroundColor = 'cyan'; // Highlight the parent on mouse enter
feedback.textContent = 'Mouse entered the parent element';
});
parent.addEventListener('mouseleave', function() {
parent.style.backgroundColor = 'lightblue'; // Revert color on mouse leave
feedback.textContent = 'Mouse left the parent element';
});
// Update feedback for child interactions
children.forEach(child => {
child.addEventListener('mouseenter', function() {
feedback.textContent = `Mouse entered ${this.textContent}`;
this.style.backgroundColor = '#ffcccb'; // Highlight child on mouse enter
});
child.addEventListener('mouseleave', function() {
feedback.textContent = `Mouse left ${this.textContent}`;
this.style.backgroundColor = 'lightsalmon'; // Revert child color on mouse leave
});
});
</script>
</body>
</html>Этот пример наглядно демонстрирует, что события mouseenter и mouseleave срабатывают точечно и не всплывают, обеспечивая отдельные и изолированные взаимодействия с вложенными элементами.
Пример 3: Имитация mouseleave с помощью relatedTarget
Иногда нужно одновременно использовать всплытие (чтобы применить единственный делегированный обработчик) и чистое поведение «только когда указатель действительно покидает элемент». Это можно совместить, прослушивая всплывающий mouseout и игнорируя переходы к потомкам с помощью relatedTarget. Логика та же, что показана выше, оформленная в виде небольшого переиспользуемого вспомогательного метода:
function reallyLeft(event, element) {
// True only when the pointer moves to something OUTSIDE `element`.
const to = event.relatedTarget;
return to === null || !element.contains(to);
}
// Demonstrate without a browser: simulate a mouseout whose related
// target is a child (should be ignored) and one to an outside node.
const card = { contains: (node) => node === 'child' };
console.log(reallyLeft({ relatedTarget: 'child' }, card)); // false (still inside)
console.log(reallyLeft({ relatedTarget: 'outside' }, card)); // true (really left)
console.log(reallyLeft({ relatedTarget: null }, card)); // true (left the window)Этот паттерн лежит в основе реализации надёжных выпадающих меню в библиотеках: один всплывающий обработчик размещается на корне меню, а меню сворачивается только тогда, когда указатель покидает всё поддерево.
Заключение
События перемещения мыши позволяют создавать тонкие и отзывчивые взаимодействия вокруг указателя пользователя. Главный вывод — правильный выбор пары:
- Используйте
mouseenter/mouseleaveдля чистого отслеживания состояния наведения для каждого элемента — они срабатывают один раз и игнорируют дочерние элементы. - Используйте
mouseover/mouseoutкогда вам нужно всплытие для делегирования событий, и опирайтесь наrelatedTargetдля фильтрации переходов к потомкам.
Для дальнейшего изучения ознакомьтесь с основами событий мыши для кликов и кнопок, а также с введением в события браузера для общего понимания того, как работают события.