jcarpenter2
jcarpenter2

Reputation: 5476

DOM - timing of simultaneous events vs setTimeout

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

Answers (2)

Redu
Redu

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

Matti Virkkunen
Matti Virkkunen

Reputation: 65166

Use the mouseenter and mouseleave events instead of mouseover and mouseout.

Upvotes: 1

Related Questions