Question

Imagine I have the following elements:

+------------------+
|                  |
|    +----------------+
|    |                |
|    |                |
|    +----------------+
|                  |
+------------------+

The inner element has positioning that mean it can visually sit outside of its parent, and I put an event listener on the parent like such:

parent.addEventListener('mouseover', function(e) {
  // log mouse is over me
}, false);

If I mouse directly over the parent from the bottom, I get a direct event, but because of event routing if I mouse over from the side (hitting the child first), I'll also receive the event from the child as it bubbles up.

If I keep going left until I'm out of the child element and in the parent space, I'll get a direct event, if I go right again, I'll get another bubbled event from the child element.

This is by design and the design is absolutely fine, however- if I want to do a SINGLE action when the mouse overs me (including my child items), I need to do some extra work to block events I'm not interested in.. for example, this would work:

var isOver = false;

parent.addEventListener('mouseover', function(e) {
  if (isOver) return;
  isOver = true;
  // log mouse is over me
}, false);

parent.addEventListener('mouseout', function(e) {
  if (e.relatedTarget == this || e.relatedTarget.isDescendantOf(this)) return;
  isOver = false;
  // log mouse is leaving me
}, false);

Essentially the code has a state variable to determine if the mouse has 'overed' the parent (either via its child or not), and if the event fires again whilst 'overed' those events are ignored.. we also capture the mouse out event and ask it.. is the relatedTarget (the target you are about to enter) ME? (the parent), or an eventual child of ME? if so, then don't do anything.. otherwise set the 'overed' state to false.

So that code actually works, but imagine I cannot use the relatedTarget property to determine the next target, how would you go about doing this behavior?

I hope that makes sense, I have the feeling theres not enough context in the events to do it without relatedTarget but felt there might well be an angle regarding changing behavior on the eventPhase property (to determine if the event is direct or bubbled).

Also I'm not looking for answers saying 'this won't work in ie, yadda yadda'.. I'm working against w3c event spec browsers only right now.

Thanks in advance, Stephen.


Edit, just to clarify, I want the events to happen as if I have a single element that looked like this (if the element was actually as above example):

+------------------+
|                  |
|                  +--+
|                     |
|                     |
|                  +--+
|                  |
+------------------+
Was it helpful?

Solution

I think what you are after are IE's "mouseenter" event (fires when the user moves the mouse pointer inside the boundaries of the object) and "mouseleave" event (fires when the user moves the mouse pointer outside the boundaries of the object):

Unlike the onmouseover event, the onmouseenter event does not bubble. In other words, the onmouseenter event does not fire when the user moves the mouse pointer over elements contained by the object, whereas onmouseover does fire.

Ditto for onmouseleave.

Here's an example: http://jsbin.com/uputi (add /edit to the url to edit the source code):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> 
<head> 
<title>Sandbox</title> 
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<style type="text/css" media="screen"> 
#outer { width:200px; height:200px; border:1px solid #333; }
#inner { float:right; width:125px; height:80px; margin-right:-80px; border:1px solid #333; }
</style> 

<script> 
window.onload = function() {
  function mouseenter() {
    document.getElementById('dbg').innerHTML = 'mouse entered #outer boundary';
  }

  function mouseleave() {
    document.getElementById('dbg').innerHTML = 'mouse left #outer boundary';
  }

  var outer = document.getElementById('outer');

  if ('onmouseenter' in document.body) {
    // browser supports mouseenter/mouseleave natively (i.e., IE)
    outer.onmouseenter = mouseenter;
    outer.onmouseleave = mouseleave;
  }
  else {
    // simulate mouseenter/mouseleave in other browsers
    function wrap(fn) {
      return function(event) {
        var parent = event.relatedTarget;

        // Check if we're still within the same parent (traverse up parentNodes)
        while (parent && parent != this) {
          parent = parent.parentNode
        }

        if(parent != this) {
          // call the *real* mouseover/mouseout handler
          fn.call(this, event)
        }
      }
    }

    outer.onmouseover = wrap(mouseenter);
    outer.onmouseout = wrap(mouseleave);
  }
}
</script> 
</head> 
<body> 
  <div id="outer"> 
    <div><code>#outer</code></div> 
    <div>&nbsp;</div> 
    <div>&nbsp;</div> 
    <div id="inner"><code>#inner</code></div> 
  </div> 
  <div id="dbg"></div> 
</body> 
</html>

It uses relatedTarget as you do (which works fine in standards browsers FYI), but without the state flag. See this article for more details of the emulation shown here. You can see only one mouseenter event is fired when entering the #outer element, as well as the #inner element. This is the "boundary" effect you are going after (I think).

If you have the option of including jQuery in your application, jQuery brings these fantastic events to other browsers. (note jQuery uses the technique outlined her internally, with some minor modifications).

Prototype 1.6.1 also has these events simulated.

OTHER TIPS

I think you simply need to stopPropagation

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top