Reputation: 5476
Suppose I have an element containing several children and want to run some code whenever the mouse enters or leaves the container. If I naively write:
var onHover = function (el, f) {
el.addEventListener('mouseover', function () {
f(true);
});
el.addEventListener('mouseout', function () {
f(false);
});
};
Then I get the desired behavior in some cases - depending on the nature of the callback f
. However, when the mouse moves from child to child within the container, f(false)
runs immediately followed by f(true)
. I don't want this to happen - I only want f
to be run when the mouse enters or leaves the container as a whole, not called machine-gun style as the user drags their mouse over the elements that are inside the container.
Here's the solution that I came up with:
var onHover = function (el, f) {
var previousMouseover = false;
var receivedMouseover = false;
var pushing = false;
var pushEv = function () {
if (pushing) { return; }
pushing = true;
setTimeout(function () {
pushing = false;
if (previousMouseover !== receivedMouseover) {
f(receivedMouseover);
previousMouseover = receivedMouseover;
}
});
};
el.addEventListener('mouseover', function () {
receivedMouseover = true;
pushEv();
});
el.addEventListener('mouseout', function () {
receivedMouseover = false;
pushEv();
});
};
This solution, like the first solution, assumes and works by the virtue that the mouseout
event is sent before the mouseover
event is. I would also like to know whether that is formally specified by any W3C documentation, but that is not the topic of this question, and even if it were not the case, it would be easy to write a functioning algorithm in spite of that by setting two separate variables, say receivedMouseover
and receivedMouseout
inside of the mouseover
and mouseout
callbacks, both of which are then inspected inside of the setTimeout
callback.
The question is: Is it required that both the mouseover
and mouseout
events be processed before any setTimeout
callbacks signed up by either event are run?
Upvotes: 0
Views: 67
Reputation: 26201
Since you have attached the event listener to the parent element you may compare the event origin (event.target
) with the parent element (this
or event.currentTarget
) before you take an action. You may do as follows;
var onHover = function (el, f) {
el.addEventListener('mouseover', function (evt) {
this === evt.target && f(true);
});
el.addEventListener('mouseout', function (evt) {
this === evt.target && f(false);
});
};
Most of the elements bubble so at some point this might be the right way to do this job.
Edit: As mentioned in the comments the mouseover
and mouseout
events can be problematic under some circumstances such as when the parent element has no padding or margins defined and children cover all the parent. Even if they don't the speed of the mouse could be fast enough to make the JS engine fail to sample the mouse over the parent element. This fact is beautifuly explained in this article.
So, as mentioned in the accepted answer, i suppose the mouseenter
and mouseleave
events are there to solve this problem. Accordingly the right code should be like;
var onHover = function (el, f) {
el.addEventListener('mouseenter', () => f(true));
el.addEventListener('mouseleave', () => f(false));
};
Edit 2: Well... Actually there is a safe way of using mouseover
and mouseout
in this particular condition. It's about using CSS pointer-events
property on the children which disables them from event emitting for mouse activity.
var container = document.getElementById('container');
container.addEventListener('mouseover', function (ev) {
console.log(container === ev.target);
});
container.addEventListener('mouseout', function (ev) {
console.log(container === ev.target);
});
#container * {
pointer-events: none
}
<div id="container">
<div>
<span>text</span>
</div>
</div>
Upvotes: 0
Reputation: 65166
Use the mouseenter
and mouseleave
events instead of mouseover
and mouseout
.
Upvotes: 1