views:

161

answers:

2

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):

+------------------+
|                  |
|                  +--+
|                     |
|                     |
|                  +--+
|                  |
+------------------+
A: 

I think you simply need to stopPropagation

Josh Stodola
Hi Josh, I understand stopPropagation will stop the bubbling, meaning that my event listener will not receive the mouseover event when I enter from the right (where I enter the child before the parent).. essentially what I need to do is treat the entire visible surface of my element (and its children) as one thing, and have a single 'over' and 'out' event happen as I enter or leave the entire surface.
meandmycode
+1  A: 

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"&gt; 
<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.

Crescent Fresh
Ah I wasn't aware there was standardization already here, however the problem is that the explanation uses the same technique as mine (it filters the events to see if the relatedTarget is, or is a child of the original evented element).It could be that this is the easiest way as to why there arent any other solutions (why write something else?), the problem is I cannot use the relatedTarget property.. guess I'm stuck with an unsolvable problem!
meandmycode
@meandmycode: relying on `relatedTarget` is fine. See my updated answer (self contained) now.
Crescent Fresh
Unfortunately the problem isn't that I don't think relatedTarget is standard, just literally cannot be used for this. I appreicate the effort to help and given I didn't have the bizzare requirement to not using relatedTarget, your advise is sound.. so I'll mark it as correct.
meandmycode
You mentioned in the question that you are looking for an answer that conforms to the W3C spec...? This answer certainly does not fit those requirements.
Josh Stodola
@Josh: I read "against w3c event spec" the wrong way....
Crescent Fresh
Josh, yes my question says I want it against w3c spec, but with the condition not to use relatedTarget.. for anybody searching for this problem and hitting this question, they'll not have the requirement to not use relatedTarget, so this answer will be useful.
meandmycode
@meandmycode: I didn't glean from your question that you were trying to *avoid* `relatedTarget`. Why do you think you should avoid it?
Crescent Fresh
Actually my real problem is with drag events, but being new that they are I figured asking the question regarding mouse events would gain more response, the same trick should be possible with drag events as is with the mouse events, only the w3c spec states that the relatedTarget property of the dragleave event should be null, it doesnt actually state why however.
meandmycode
@meandmycode: Wow, ok. So this answer is kind of useless then isn't it. Took me a while actually.
Crescent Fresh
The amount of time you dedicate to answering questions is completely up to you.
meandmycode
Turns out this may be a bug, firefox 3.6 DOES have a relatedTarget set, and does have a bug report to backup the change stating that relatedTarget is null unless explicitely set when the drag event starts. Again your solution and time wasn't a waste, simply I would change the string where you capture mouse in/out and change to drag in/out and it would work.
meandmycode