isMouseLeaveOrEnter

or... What's going on with my mouseout/mouseover events?

If I had a penny for every time I've seen a post in a forum from someone wondering why their mouseout script isn't working properly I'd have an awful lot of pennys. The function provided on this page, isMouseLeaveOrEnter, will fix about 95% of the problems people have.

Example

The problem encountered by most is that the mouseout and mouseover events are fired a lot more often than you might think. Here's a couple of examples that demonstrate the issue.

Mouseover/Mouseout

In this example the counters are updated on every mouseout or mouseover event caught by the external div. Notice that a number of mouse events are fired as you move over the nested div.

Try moving the mouse in and out of this div
and the nested div

mouseovers mouseouts

Mouseover/Mouseout with the isMouseLeaveOrEnter

In this example the counters are only updated once the isMouseLeaveOrEnter function checks that the mouse has actually moved over or away from the external div. Now moving over the nested element no longer triggers anything.

Try moving the mouse in and out of this div
and the nested div

mouseenters mouseleaves

Why is this happenning?

If you look at the example above you will notice that an onmouseout event is fired when you move the mouse either away or onto the nested div. Most people are unaware that a mouseout would be fired at all in either of these cases.

The mouseout when moving onto the nested div is easily explained. A mouseout is fired whenever the element that is directly under the mouse changes. It doesn't matter whether you move to an element that is a direct child or not, the event will still fire.

In order to understand why a mouseout is recordered when moving away from the nested element you need to be aware of event bubbling. Event bubbling refers to the way events are propogated. If you move the mouse over an element you will trigger a mouseover for that element. If the element has a handler for that event it will fire. The event will then be passed to the elements parent. This happens whether or not the element handled the event.

The same process then occurs again, if the parent has a handler for the event it will fire. The event will be passed on to its parent. This process continues all the way up to the document object. Check out the event bubbling demo to see this in action.

What this means is that when you declare a handler for an event on an element you need to be aware that it will catch not only events fired by the element itself, but also events fired by any of it's children.

A very common complaint from people trying to build menu systems is that the menu flickers or dissappears at inappropriate times. This is almost always caused by event bubbling. Usually a handler for the mouseout event is attached to the menu so that it will dissappear when the user moves the mouse off it. If you ask the question 'when will this event handler catch an event?' The answer is 'A lot more times than you want!'. A mouseout will fire whenever you move the mouse from the parent element to one of it's children or outside the element. Because of event bubbling it will also catch any mouseouts from it's child elements.

So how do I fix it?

Under internet explorer there are 2 events you don't see in other browsers, mouseleave and mouseenter. These 2 events operate the way most people think mouseout and mouseover work until they find themselves in difficulty. They fire only when the mouse enters or leaves the element and ignores all of the elements children. If they weren't microsoft only this article would never have been written.

The only cross-browser tools available are mouseout & mouseover so we'll work with them. What we need to do is check that these events are not being fired because of interactions with the elements children. To do this I've written the following function.

// this function determines whether the event is the equivalent of the microsoft // mouseleave or mouseenter events. function isMouseLeaveOrEnter(e, handler) { if (e.type != 'mouseout' && e.type != 'mouseover') return false; var reltg = e.relatedTarget ? e.relatedTarget : e.type == 'mouseout' ? e.toElement : e.fromElement; while (reltg && reltg != handler) reltg = reltg.parentNode; return (reltg != handler); }

Update!
Now that we have a way to differentiate between onmouseover and onmouseleave it would be great to just enable these events for non IE browsers. The enableMouseLeaveAndMouseEnter script below means you can just use these events and it will work everywhere. Big thanks to Sven Sass for the inspiration for this script.

/* Iterates through the DOM and enables the onmouseenter and onmouseleave events for non IE browsers */ function enableMouseLeaveAndMouseEnter() { var elems = document.body.getElementsByTagName("*"); var length = elems.length; for (var i=0; i<length; i++) { var elem = elems[i] if (elem.getAttribute("onmouseleave")) { var body = (elem.onmouseout ? elem.onmouseout.toString().replace(/^([\s\S]*?){([\s\S]*)}$/g,"$2") + ';' : "") + 'if(isMouseLeaveOrEnter(event,this)){' + elem.getAttribute("onmouseleave").replace(/^([\s\S]*?){([\s\S]*)}$/g,"$2") + '};'; elem.onmouseout = new Function('event', body); } if (elem.getAttribute("onmouseenter")) { var body = (elem.onmouseover ? elem.onmouseover.toString().replace(/^([\s\S]*?){([\s\S]*)}$/g,"$2") + ';' : "") + 'if(isMouseLeaveOrEnter(event,this)){' + elem.getAttribute("onmouseenter").replace(/^([\s\S]*?){([\s\S]*)}$/g,"$2") + '};'; elem.onmouseover = new Function('event', body); } } } // addEventListener is supported by all browsers except IE. Exactly what we are wanting to fix. if (document.addEventListener) { document.addEventListener('DOMContentLoaded',enableMouseLeaveAndMouseEnter, false); }

If you use this script it would be friendly to provide a link back to this site.

isMouseLeaveOrEnter explained

The function essentially checks that the related element for the event was not a child of the event handler or the event handler itself.

The function takes 2 parameters. The event (e) & the event handler (handler).

The 1st line checks that the event is a mouseout or a mouseover, if not the function returns false and we're done.

The 2nd line determines the related element for the event. For a mouseout that's where the mouse was moved and for a mouseover it's where the mouse came from.

The 3rd line works its way up the DOM tree until it gets to the top of the tree or finds the event handler.

When we reach the last line reltg will either equal the event handler meaning the event was fired by a child element or be null meaning the top of the DOM tree was reached without finding the handler.

Have a look at the source for the examples above too see how the function can be used.

Comments

If you've got any questions or feedback about this article please post them in the Dynamic Tools Javascript Forum.