views:

593

answers:

6

I'd like some Javascript code to run when the mouse leaves the browser window. I only need to support Safari (WebKit.)

I tried putting a mouseout handler on window. That handler is reliably called when the mouse leaves the browser window. But because of bubbling it is also called when the mouse moves between elements in the document. I can't figure out how to determine when the mouse has actually left the window and when it has only moved between elements.

When the mouse leaves the window, exactly one event is generated, and the target element appears to be the element the mouse was actually over. So checking to see if the target element is window or document doesn't work. And wrapping the whole page in an invisible containing div doesn't work either: if the div is invisible, then the mouse will never be over it, so nothing changes.

(The same thing happens if I put the handler on document or document.body, except that surprisingly document.body does not get mouseover/mouseout events when the mouse enters or leaves an empty part of the window, such as the empty vertical space created by absolutely positioning an element with bottom:0. For that space, document and window will get mouseover/mouseout events with the target being <html>, but document.body will not.)

Some ideas I had:

  • On each mouseout event, get the actual position of the mouse and see if it is in fact over the window. But I don't know if this is actually possible and it sounds like it would be tricky to eliminate all of the race conditions.
  • Also register a mouseover handler and detect cases where mouseout is not proceeded by (or followed shortly by a) a mouseover. But that would require a timer.

We use prototype.js so ideally I'd like to express the solution in terms of prototype's Event.observe, but I can figure that part out.

Thanks for any suggestions!

A: 

You can use onmouseout event on a window instead

DVK
This is actually what I tried (sorry that wasn't clear. I edited the question to clarify.) How do I distinguish the case where the mouse has left the window from the case where the mouse has only moved between elements, and the handler is being called because of bubbling?
Geoff
+5  A: 

Using only javascript, no prototype or jquery etc.

<html>
<head>
<script type="text/javascript">
  var mouseX = 0;
  var mouseY = 0;
  var counter = 0;
var mouseIsIn = true;
function wireEvent() {
window.addEventListener("mouseout",
    function(e){
        mouseX = e.pageX;
        mouseY = e.pageY;
     if ((mouseY >= 0 && mouseY <= window.innerHeight)
     && (mouseX >= 0 && mouseX <= window.innerWidth))
      return;
        //do something for mouse out
     counter++;
     mouseIsIn = false;
     document.getElementById('in_out').innerHTML='out' + counter;
    },
    false);
window.addEventListener("mouseover",
    function(e){
     if(mouseIsIn)
      return;
        //do something for mouse over
     counter++;
     mouseIsIn = true;
     document.getElementById('in_out').innerHTML='in' + counter;
    },
    false);
}
</script> 
</head>
<body onload="wireEvent();">
<div id="in_out">&nbsp;</div>
<div style="width:300px; height: 200px; background: red;">Dummy element</div>
</body>
</html>

UPDATE:
Added check for mouse position on mouseout triggered when moving in/out elements within the body. If it's within the window, mouseout event is not triggered.
Also introduced a flag for current status of mouse 'in' or 'out' using mouseIsIn. If it is true, mouseover will not trigger too.

o.k.w
This appears to work, but actually doesn't. Both handlers get called every time the mouse moves over the red box ("Dummy element".) It's just that the mouseout (happens to?) gets called last, so you don't notice.You can see this by keeping a counter of the total number of times the handler has been called, and printing that out in addition to 'in' or 'out'. Or by using console.log().
Geoff
@Geoff: I see, thanks for pointing out. Let me check what else I can do...
o.k.w
@Geoff: I've implemented additional logic for the position of the mouse on `mouseout`. If it's within the window, ignore. This will prevent the unwanted triggering. I've tested it on Safari, seems to work.
o.k.w
Awesome, that looks great! Thanks a lot! I'm a little worried about the window being resized just as the event is handled, but given that this is a web page and not a radiation therapy machine, that is probably not a worry worth worrying about.
Geoff
@Geoff: Well, the chances of that happening is quite low, have you tested it? I can only think of resizing by keyboard with the window crossing/overlapping the mouse's location. I tested it on Windows Safari, the script still works! Not sure about MacOS though. Good luck!
o.k.w
+1  A: 

Perhaps you can set a listener for mouseover and mouseout document, body or some other element that wraps the entire document, and use that (by saving that it happened) as a trigger to determine whether it is a valid mouseout on window?

Failing that, your first idea (regarding position check) should work pretty good. Any event passes along the X/Y the event occured. If it is anything farther than height/width of the window, you left the actual window. If it is negative anything, you left the window. And, possibly, if it is exactly the height/width or exactly top: 0 or left: 0, then you left the window.

Kevin Peno
A: 

Your problem comes from mouseout events being generated for elements inside the window, which then bubble up as described in the W3C events spec. You can check which element the event was actually fired on:

function mouseoutFunction(event) {
  event = event || window.event;
  var sender = event.srcElement || event.target;
}
Gareth
That code will find the sender of the event, but how can I use that information to determine if the mouse has left the window? Even when the mouse leaves the window, sender is not window or document -- it's the lowest containing element, some random <p> or <div> depending on where the mouse was before it left the window.
Geoff
+2  A: 

SUMMARY: This can be done cleanly by checking the relatedTarget property during the mouseout event. If relatedTarget is not a child of document, then the mouse just left the window. It's easy to do yourself, but if you don't want to, some libraries (Mootools, future Prototype..) have baked-in functionality, and others (current Prototype) have extensions available. On IE, you could instead use mouseleave, which is a non-bubbling version of mouseout.

Details:

IE has events called mouseenter and mouseleave that are non-bubbling versions of mouseover and mouseout. Other browsers do not, but if they did, setting a mouseleave listener on window or document would do the trick.

A gentleman named Ken Snyder comes to the rescue:

On a mouseover, the relatedTarget property references the node from which the pointer came. On a mouseout, the relatedTarget property references the node to which the pointer went.On any event, the scope is the node to which the event is attached.When the relatedTarget is a not child of the currentTarget, a mouseover event is equivalent to a mouseenter event and a mouseout event is equivalent to a mouseleave event.

-- http://kendsnyder.com/archives/6-MouseEnter-and-MouseLeave.html

This makes it possible to implement mouseenter and mouseleave in other browsers. In fact, Ken provides same Prototype code to do so: http://kendsnyder.com/sandbox/enterleave/MouseEnterLeave.js

Duroth pointed out in comments that MooTools already includes something similar. (Thanks Duroth.) It sounds like the upcoming Prototype release (1.6.2) may include this functionality, but I can't find anything definite.

Geoff
A: 

Here is my solution based on a timer. The timer here is basically used to give a chance for other event handlers (specifically, onmouseover) to execute before deciding that the mosue is out of the window. The timeout of 1ms (actually around 33ms, there is a minimum timer resolution) gives a little time for the mouseover to occur if it already hasn't.

var inWin=0;
window.onmouseout = function(e)
{
   inWin--;
   setTimeout(checkIfOut, 1);
}
window.onmouseover = function(e)
{
   inWin++;
}

function checkIfOut()
{
   if(!inWin)
   {
     //Yay! Mouse is out of the window (probably)
   }
}
Raze