Mouseenter and mouseleave events for Firefox (and other non-IE browsers)

Generally I favor Firefox and other W3C browsers over IE, and I think most people know that about me. Still, I like to think that I'm a fair person, and that means giving credit where credit is due. And sometimes, IE deserves credit where other browsers fall short.

Such is the case with two non-standard (but rather convenient) events that IE exposes: mouseenter and mouseleave.

The mouseenter and mouseleave events are similar to the more familiar mouseover/mouseout events, but with one important difference:

The mouseenter and mouseleave events don't bubble.

Now you might think then, that duplicating this effect is just a matter of stopping event propagation in non-IE browsers, but it's actually more complicated than that.

Read on to learn more.

To help illustrate more clearly what mouseenter/mouseleave allow you to do, consider the following:

<ul id = "theList">
   <li>List Item One</li>
   <li>List Item Two</li>
   <li>List Item Three</li>
   <li>List Item Four</li>
</ul>

<script type = "text/javascript">
   document.getElementById('theList').attachEvent('onmouseenter', myFn);
   document.getElementById('theList').attachEvent('onmouseleave', myFn);

   function myFn(e)
   {
      alert(e.type + ': ' + e.srcElement.id);
   }
</script>

In the preceding example, the function, myFn(), will execute each time you move the mouse cursor into #theList, but it won't execute when you move the mouse cursor in or out of the child list items that belong to #theList (something that it would do had we used the mouseover/mouseout events instead).

As I mentioned previously, the major different between mouseenter/mouseleave events and mouseover/mouseout events is that the former don't bubble. And this is really what makes the events useful. In our example, the child list items of #theList still receive the mouseenter/mouseleave events (even if you aren't listening for them) but the events won't bubble up to their parent <ul> element.

Of course you could achieve this same effect in non-IE browsers by stopping event propagation, but not by stopping it on the #theList (which is the element to which you'd be wiring up the mouseover/mouseout events).

No, you'd have to utilize .addEventListener(..) for each child element of #theList, hooking up functions to the mouseover/mouseout events that would take care of stopping the events from propagating (in this case bubbling) up to the parent element, and that would be a royal pain (not to mention that you might want the mouseover/mouseout events to bubble up for some (unrelated) reason).

A better approach would be to write a cross-browser addEvent(..) function that knows how to treat 'mouseenter' and 'mouseleave' as valid events.

Here's the code to make it happen, with the explanation to follow.

function addEvent(_elem, _evtName, _fn, _useCapture)
{
   if (typeof _elem.addEventListener != 'undefined')
   {
      if (_evtName === 'mouseenter')
         { _elem.addEventListener('mouseover', mouseEnter(_fn), _useCapture); }
      else if (_evtName === 'mouseleave')
         { _elem.addEventListener('mouseout', mouseEnter(_fn), _useCapture); }
      else
         { _elem.addEventListener(_evtName, _fn, _useCapture); }
   }
   else if (typeof _elem.attachEvent != 'undefined')
   {
      _elem.attachEvent('on' + _evtName, _fn);
   }
   else
   {
      _elem['on' + _evtName] = _fn;
   }
}

function mouseEnter(_fn)
{
   return function(_evt)
   {
      var relTarget = _evt.relatedTarget;
      if (this === relTarget || isAChildOf(this, relTarget))
         { return; }

      _fn.call(this, _evt);
   }
};

function isAChildOf(_parent, _child)
{
   if (_parent === _child) { return false; }
      while (_child && _child !== _parent)
   { _child = _child.parentNode; }

   return _child === _parent;
}

Several things to notice here:

First, if the browser supports .addEventListener(..) we need to do a little extra work to support mouseenter/mouseleave.

But (and I can't stress this enough), this concept is absolutely flawed! We're assuming that if the browser supports .addEventListener(..) then it must not support mouseenter/mouseleave.

While this happens to be true at present (to the best of my knowledge) it's unwise to assume that this will always be the case. The problem is, there is no (highly reliable) way to determine whether or not a browser supports a particular event (at least not that I know of). I'm okay with this approach for now, only because we're writing the code in such a way, that even if a browser did support mouseenter/mouseleave, our custom implementation of it shouldn't cause any major problems.

Second, you'll notice that when we wire up the event handler, we don't directly use the _fn variable that the user passed in. Instead we use it indirectly by proxying it through the mouseEnter(..) function.

Third, note that we call the same mouseEnter(..) function regardless of whether we're dealing withing with mouseenter or mouseleave. Remember though, that mouseenter/mouseleave are just "psuedo" events (if you will). The actually events we're wiring up are mouseover and mouseout which, in our function, map to mouseenter and mouseleave respectively. This allows us to use one function to handle both events.

Fourth, the mouseEnter(..) function returns a function. I talked about this a while back in post about closures, so I won't go into a lot of detail here, but I did feel that it was wort pointing out in case you hadn't noticed.

The mouseEnter(..) function:

The mouseEnter(..) is where half the magic happens (the other half being in the isAChildOf(..) function (which I'll get to shortly). The .mouseEnter(..) function basically just "intercepts" mouseover or mouseout events and "decides" whether or not to dispatch them (by invoking _fn through the use of the .call(..) function).

It does this by obtaining a reference to the related target. In the case of mouseover, the related target would be the element being moused from, and in the case of mouseout, the related target would be the element being moused to.

Once we know the related target, all we need to do before calling the actual function is make sure of two things:

  1. That the related target is not child of the element to which we wired up the event (that would be the this element).
  2. That the related target is not, itself, the element to which we wired up the event

Number two in the above list is easy. We accomplish by just checking: if (this === relTarget). Number one is slightly more difficult, so for that one, we call on the help of another function: isAChildOf(..).

The isAChildOf(..) function:

As its name imples, isAChildOf(..) tells you if one element is a child element of another. Simply pass in the (suspected) parent element as the first parameter, and the (possible) child element as the second parameter, and the function will return true or false accordingly.

Some of you might be wondering why I didn't just leverage .prototype to add a .contains(..) function to the native HTMLElement. I chose not to do this, because I think I remember reading somewhere that Safari does not expose the prototype of HTMLElement (though I hear there is a way around this). However, not being a Mac user, the best I could do was test in Konqueror, and while it worked in that browser, that wasn't really enough to make me feel good about this approach.

The isAChildOf(..) function is pretty simple. It works by repeatedly setting the _child variable to its .parentNode, thus cycling it up the chain. Eventually, the _child will be either "false-like" (probably null) or it will be equal to the _parent. In either case, we break out of the while loop.

Once out of the while loop, we return (_child === _parent). This works because any element that was originally a child of the _parent element, would eventually be the parent element (as it gets cycled up the chain). Otherwise, it must not have been a child in the first place. Cool!

So if the related target itself, is either the element to which the event was wired up, or if the related target is a child of the of the element to which the event was wired up, we don't want to dispatch the event. Programmatically, we simply return (prematurely) if either one of these conditions turns out to be true. Otherwise, we can go ahead and call the desired function, and we should end up with a very nice mouseenter/mouseleave simulation.

Final thoughts:

There are a few important things to point out before I close with this entry:

First, the addEvent(..) function we wrote here would be a lot better if we merged it with the xb.addEventListener(..) function that was described in the blog entry Fixing IE's .attachEvent(..) Failures. For the sake of brevity though, I chose not to do that here.

Second, our simulation of mouseenter/mouseleave does fall short with regards to the event object's .type property. Since what we're really using under the hood is mouseover/mouseout, any request for _evt.type is going to return either 'mouseover' or 'mouseout', but never 'mouseenter' or 'mouseleave' :-(

Finally, there is one problem I haven't yet mentioned with our implementation here, and unfortunately, it's a doozy!

Since the _fn passed into addEvent(..) is not called directly, but rather, proxy'd through the MouseEnter(..) function, we're unable to remove any event listener that's been wired up as 'mouseenter' or 'mouseleave'... ever!

And boy does that suck! This is a result of the fact that MouseEnter(..) returns an anonymous function, and, as discussed in a previous post, you can't detach anonymous functions.

It's worth noting that this is not a browser-specific problem. All browsers will handle this the same way, even IE (had we been using the closure technique for that browser).

I do have a work around, and it's eerily similar to the event hash concept used in the xb.addEventListener(..) function for the IE branch of code. I'm not thrilled with the solution, and I'm convinced there is a cleaner way. So my intention is to hold off until I figure out what that is.

In the meantime, if anyone out there has an elegant solution that would allow us to detach the functions wired up with 'mouseenter' or 'mouseleave', please feel free to send your comments. I'd be really interested to see how others might attack this problem.

Live demo:

To see a live demo, visit this page.

Comments appreciated!

22 Responses

  1. btr Says:

    good code, but if the layer contains a SelectBox, when a mouse is between box and "open layer's box" event equal mouseout. (tested in IE and firefox)

    Modified code ( if (_evt.relatedTarget) ) but don't resolve this problem on IE :

    mouseEnter: function(_pFn) {
    return function(_evt) {
    if (_evt.relatedTarget) {
    var relTarget = _evt.relatedTarget;
    if (this == relTarget || xb.isAChildOf(this, relTarget)) return;
    _pFn.call(this, _evt);
    }
    else
    return;
    }
    },

    If you have a solution for IE, I'll m very happy…

    Sorry for my english.

  2. sstchur Says:

    btr:

    I actually noticed this myself at one point, but the interesting thing is that it has nothing to do with this code! IE supports mouseenter and mouseleave natively, so it's all native code that is executing when you're running IE, which means that IE's own, proprietary events don't even work right! Grr!

    One thing you could do is force IE to use this code form this post (but you'd need to update it to handle .toElement / .fromElement in addition to .relatedTarget)

  3. btr Says:

    yes, I tried to force IE to use this code, but I don't have enough proficiency for do it.
    I replaced in IE part code :

    _elem.attachEvent('on' + _evtName, f);

    into

    if (_evtName == 'mouseenter')
    { _elem.attachEvent('onmouseover', xb.mouseEnter(f), _useCapture); }
    else if (_evtName == 'mouseleave')
    { _elem.attachEvent('onmouseout', xb.mouseEnter(f), _useCapture); }
    else
    { _elem.attachEvent('on' + _evtName, f, _useCapture); }

    And in a method mouseEnter :
    return function(_evt) {
    var relTarget = _evt.relatedTarget || _evt.fromElement;
    if (relTarget) {
    if (this == relTarget || xb.isAChildOf(this, relTarget)) return;
    _pFn.call(this, _evt);
    }
    else
    return;
    }

    But on mouseEnter method, "this" is undefined on IE. I don't know why.

    Can you help me ?

  4. blupark.net » Blog Archive » Pointer sensitive menus for Web pages Says:

    [...] The condition for closing the menu is: the mouse has not entered (event.relatedTarget) the top menu item and it has not entered any other element that is a child of the top menu item. This second part may need a supporting function to determine whether an element is a child of another element, example (see isAChildOf function): Mouseenter and mouseleave events for Firefox (and other non-IE browsers) [...]

  5. to Says:

    i'm working on an ajax program where i'm using three panels with the CollapsiblePanelExtender. each panel is collapsed to 30px and there is a button within the panels so it looks like when you mouseover the button a menu flys out but what is happening is on mouse over the panel expands from 30 to what ever the content height is.

    the problem is with your code i can get more than one panel to work at a time.
    which ever panel I reference last is the one that functions.

    Here the javascript:

    function pageLoad(s,e) {
    var panel1 = $get('pnlCustomerTotal');
    xb.addEvent(panel1, 'mouseenter', over, false);
    xb.addEvent(panel1, 'mouseleave', out, false);

    }
    function over(e) {

    $find('CollapsiblePanelExtender2').set_Collapsed(true);
    $find('CollapsiblePanelExtender2').togglePanel(e);

    }

    function out(e) {

    $find('CollapsiblePanelExtender2').set_Collapsed(false);
    $find('CollapsiblePanelExtender2').togglePanel(e);

    }

    function pageLoad(s, e) {
    var panel1 = $get('pnlYourTotal');
    xbb.addEvent(panel1, 'mouseenter', over, false);
    xbb.addEvent(panel1, 'mouseleave', out, false);
    }
    function over(e) {

    $find('CollapsiblePanelExtender1').set_Collapsed(true);
    $find('CollapsiblePanelExtender1').togglePanel(e);
    }
    function out(e) {

    $find('CollapsiblePanelExtender1').set_Collapsed(false);
    $find('CollapsiblePanelExtender1').togglePanel(e);
    }

    and there's an external file by the name of mouseenter.js that has the following:
    var xb =
    {
    evtHash: [],

    ieGetUniqueID: function(_elem)
    {
    if (_elem === window) { return 'theWindow'; }
    else if (_elem === document) { return 'theDocument'; }
    else { return _elem.uniqueID; }
    },

    addEvent: function(_elem, _evtName, _fn, _useCapture)
    {
    if (typeof _elem.addEventListener != 'undefined')
    {
    if (_evtName == 'mouseenter')
    { _elem.addEventListener('mouseover', xb.mouseEnter(_fn), _useCapture); }
    else if (_evtName == 'mouseleave')
    { _elem.addEventListener('mouseout', xb.mouseEnter(_fn), _useCapture); }
    else
    { _elem.addEventListener(_evtName, _fn, _useCapture); }
    }
    else if (typeof _elem.attachEvent != 'undefined')
    {
    var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) + '::evt_' + _evtName + '::fn_' + _fn + '}';
    var f = xb.evtHash[key];
    if (typeof f != 'undefined')
    { return; }

    f = function()
    {
    _fn.call(_elem);
    };

    xb.evtHash[key] = f;
    _elem.attachEvent('on' + _evtName, f);

    // attach unload event to the window to clean up possibly IE memory leaks
    window.attachEvent('onunload', function()
    {
    _elem.detachEvent('on' + _evtName, f);
    });

    key = null;
    //f = null; /* DON'T null this out, or we won't be able to detach it */
    }
    else
    { _elem['on' + _evtName] = _fn; }
    },

    removeEvent: function(_elem, _evtName, _fn, _useCapture)
    {
    if (typeof _elem.removeEventListener != 'undefined')
    { _elem.removeEventListener(_evtName, _fn, _useCapture); }
    else if (typeof _elem.detachEvent != 'undefined')
    {
    var key = '{FNKEY::obj_' + xb.ieGetUniqueID(_elem) + '::evt' + _evtName + '::fn_' + _fn + '}';
    var f = xb.evtHash[key];
    if (typeof f != 'undefined')
    {
    _elem.detachEvent('on' + _evtName, f);
    delete xb.evtHash[key];
    }

    key = null;
    //f = null; /* DON'T null this out, or we won't be able to detach it */
    }
    },

    mouseEnter: function(_pFn)
    {
    return function(_evt)
    {
    var relTarget = _evt.relatedTarget;
    if (this == relTarget || xb.isAChildOf(this, relTarget))
    { return; }

    _pFn.call(this, _evt);
    }
    },

    isAChildOf: function(_parent, _child)
    {
    if (_parent == _child) { return false };

    while (_child && _child != _parent)
    { _child = _child.parentNode; }

    return _child == _parent;
    }
    };

    I've tried to change var to create unique references but no success.

    any suggestions?

    thanks a million

  6. to Says:

    sorry,
    one typo i found

    from:
    the problem is with your code i can get more than one panel to work at a time.
    which ever panel I reference last is the one that functions.

    to:
    the problem is with your code i CAN'T get more than one panel to work at a time.
    which ever panel I reference last is the one that functions.

    THANKS

  7. sstchur Says:

    Well, I'm no expert in ASP.NET AJAX, but I know a little bit. When you add events with ASP.NET AJAX's $addHandler function, it doesn't use a true-blue event object. It uses its own custom event object.

    In your code, is .togglePanel() a native ASP.NET AJAX function or a custom one you wrote? If it's native to ASP.NET AJAX then it probably expect its own custom event object (which is not what it's going to get in this case).

    If you email me some code, I might be able to help more.

    Also, you might want to check out Gimme which is my Javascript library, and which contains that addEvent function, but with several updates and bug fixes.

  8. Casper Says:

    In Chrome i get 2 alerts for each mouseenter/mouseleave

  9. sstchur Says:

    @Casper:

    It is possible that the alert is taking focus from the window temporarily and then returning it, thereby causing a second event.

    Maybe give it a try where you log the result to input text box or something like that, instead of an alert.

  10. Adam Says:

    You mentioned that you could not test in Safari since you are not a Mac user. However, if you are using Windows at all (I assume so as you've tested in IE), you can download Safari for Windows which should use an identical DOM model and should allow for proper testing of access to the HTMLElement prototype.

  11. sstchur Says:

    @Adam:

    This is true today.

    This wasn't true at the time I wrote the post. Hence, my note about Safari :-)

  12. Franklin Smather Says:

    So does this code only work for html lists? or would it also work for a nested div? For example, I have a nav bar that on mouseover it shows a div with the submenu. When you rollover the submenu div inside the main div popup it hides the main div because it is triggering the mouseout event. Your solution looks like the interior elements don't pop the "bubble" by triggering an event.

    Thanks,
    Franklin

  13. sstchur Says:

    @Franklin:

    The solution should work for any nested DOM elements. It will not work if the elements merely appear to contain one another (via CSS styling or something).

    But if you actually have a hierarchy of nested elements, then this solution should work.

    See Event Re-routing in Javscript for additional relevant info.

  14. techblog » Blog Archive Says:

    [...] Stchur has written a script to mimic IE's proprietary mouseenter and mouseleave Javascript events. This is useful when you have one element nested inside another, you register an event on the [...]

  15. johnny Says:

    Z6PxJW Thanks for good post

  16. foswaldcollinston Says:

    Hey, you have a great blog here! I'm definitely going to bookmark you!

  17. Jaap Says:

    There are some typo's in the code in the original article (mainly with _fn being called f or _pFn).
    Please fix them for future reference if possible.

  18. sstchur Says:

    @Jaap:

    Thanks for pointing this out. I will fix it a little later on today.

  19. Jaap Says:

    Thx sstchur.
    Found another one: _parent being called parent (in isAChildOf).

  20. sstchur Says:

    @Jaap:

    DOH! It's fixed. Thanks. No doubt the result of copying/pasting from various versions of the functions when I wrote the post.

    FYI, mouseenter/mouseleave support is built into my addEvent method, which is available for download here: http://blog.stchur.com/blogcode/ultimate_addevent/ultimate.zip

  21. Neil Says:

    Hi
    I am not sure how useful this is to anyone but MooTools gives mouseenterat least:

    http://demos.mootools.net/Mouseenter

    Might be the simplest way around though to be honest, i've not looked into it to check it fully though in my experience, mootools is usually very reliable.

    Cheers

  22. sstchur Says:

    @Neil:

    Thanks for the link. I believe a lot of the popular Javascript libraries out there offer mouseenter/mouseleave support (including mine).

    The point of this post was more to make developers aware of it — aware that it was an IE-only event and that it could be simulated in other browsers.

    Think of it as mostly a blog post aimed at helping developers learn.