Track Document-Level Clicks With jQuery MouseDown Events
The other day, I dicussed jQuery's live() event method and why understanding the mechanics behind it was extremely important; the reason being that the live() event method depends completely on the ability for a given event type to bubble up through the DOM (Document Object Model) tree. That post got me thinking about event bubbling in general and how important it is in the construction of rich user-interface interactions. Then, I started to think about all of the times that I have used click-event-handling at the Document level in order to capture the non-clicking of a given element. This made me very nervous.
As I talked about in my previous post, tracking anything at the document level requires the bubbling chain for the target event to remain in tact; this holds true for both live() and non-live() event handling. The reason that this realization made me nervous was that I often capture click events below the document level and return(false) from my click event handlers. At the click level, this is appropriate as I don't want the item being clicked to exhibit any default behavior and returning false prevents the browser's default behavior. Returning false, however, also prevents the bubbling of the given click event up through the DOM tree. As such, you can easily imagine a situation where you need to track a click event at the document level, but you leave open the opportunity for the end user to click an element that prevents that event from reaching the document.
To see this in action, take a look at the above video.
Now, I'm just one man, so I'm pretty sure I've messed this up a bunch of times. But, the people on the jQuery development team are complete badasses, so I figured they got this right. As such, I wanted to take a look at how they handled this kind of situation. The scenario that seemed to be most in alignment with what I'm talking about here was the jQuery UI Date Picker widget; the jQuery UI Date Picker widget needs to close when the user clicks on anything that is NOT the Date Picker interface.
To see how they wired this up, I installed the jQuery UI library, created a date picker, and then looked at the events bound to the document object:
$( document ).data( "events" )
As it turns out, they don't track click events at the document level at all - they track mouse down events. Ahhh, very clever! That makes perfect sense; since our UI interactions almost always track click events, it's a safe bet that a mousedown event will never be halted in its propagation path. To put this methodology to the test, I set up this small demo page:
<!DOCTYPE HTML>
<html>
<head>
<title>Track Document-Level Clicks With jQuery MouseDown Events</title>
<script type="text/javascript" src="jquery-1.3.2.js"></script>
<script type="text/javascript">
jQuery(function( $ ){
// The name of the event that we are using to capture
// the extra-popup de-focusing events (so that we know
// when to close the pop-up).
var eventType = "mousedown";
// Get a collection for the popup.
var popup = $( "#popup" );
// Bind the catching event to the document so that we
// can catch any events that bubble up (which would
// represent a de-focusing of the pop-up window) such
// that we can close the pop-up.
$( document ).bind(
eventType,
function(){
// Hide the popup.
popup.hide();
}
);
// Bind the catching event to the pop-up itself so
// that it can stop propagation of the event up to
// the document (where the event would take on a
// very different meaning - closing the pop-up).
popup.bind(
eventType,
function( event ){
event.stopPropagation();
}
);
// Hook up the pop-up trigger link to open up the
// pop-up window when it is clicked.
$( "#popup-trigger" )
.attr( "href", "javascript:void( 0 )" )
.click(
function( event ){
// Show or hide the popup window.
$( "#popup" ).toggle();
// Cancel default event.
return( false );
}
)
;
// Hook up the other link to do some processing when
// it is clicked; the actually processing here is not
// relevant, only that it cancels the default event.
$( "#other-link" )
.attr( "href", "javascript:void( 0 )" )
.click(
function( event ){
// ...
// ... do some non-link processing ...
// ...
// Cancel default event.
return( false );
}
)
;
});
</script>
<style type="text/css">
#popup {
background-color: #F0F0F0 ;
border: 1px solid #CCCCCC ;
display: none ;
height: 100px ;
left: 50% ;
margin: -55px 0px 0px -155px ;
padding: 5px 5px 5px 5px ;
position: absolute ;
width: 300px ;
top: 50% ;
z-index: 1000 ;
}
</style>
</head>
<body>
<h1>
Track Document-Level Clicks With jQuery MouseDown Events
</h1>
<p>
<a id="popup-trigger">Show the pop-up window!</a>
</p>
<p>
I am <a id="other-link">some other link</a>.
</p>
<div id="popup">
Hello! I am a pop-up window.
</div>
</body>
</html>
As you can see above, the two links in the page capture click events and return(false) to override the browser's default click-event handling. But, since we are tracking mousedown events at the document level, rather than click events, we'll know when someone clicks outside of the pop-up, even if they, by chance, click on one of the links.
Very slick; the jQuery UI team definitely thought this one through quite thoroughly.
Want to use code from this post? Check out the license.
Reader Comments
Very cool. Once again, thanks for sharing! Also, the Jing audio/visual component is perfect. This kind of thing is conveyed so quick, clear and concise in video form.
@Jamie,
Thanks my man. The video actually has a 5 minute limit in JING, so I think my last word got cut off :) But it's such a great tool.
Ben,
The root issue is that <code>return false</code> both prevents the default action and bubbling.
<code>event.preventDefault()</code> does what you want without relying on <code>mousedown</code> to get there first (<code>click</code> is <code>mousedown</code> followed by <code>mouseup</code>).
Some jQuery specific documentation @ http://docs.jquery.com/Events/jQuery.Event although it's the same as the W3C approach.
@John,
Right you are, but, if for not other reason than brevity and easy-of-use, I think that people would be more likely to write return(false) than to use preventDefault() and / or stopPropagation(). Especially as it doesn't cause any problems... until it does :)
Great post! I would actually recommend that people eschew "return false;" if they really mean preventDefault or stopPropagation but not both. It's a convenient shorthand that's easy to abuse, similar to $(function(){}). I've found myself throwing all my code into DOMReady, when most of it could wait till onload or even some time after.
Couple of questions on your code:
Why are you bothering to overwrite the href property if you'll canceling the default action anyways?
What's up with the parens on return(false)? Just your personal style or is there a functional reason for it?
Nice. Keep up the good work!
good post Ben...
very informative.
Thanks,
Raghuram Reddy
Certified Professional in Advance Coldfusion
Bangalore India
@Sasha,
The return(val) is just my personal style. I use every opportunity to add punctuation where available - it makes me happy. Any time something is optional, I generally opt into it so there is no chance of miscommunication.
As far as setting the HREF value, it's simply cause I don't like seeing "#" in the status bar of the browser. Again, just a personal desire.
Loved the screencast! Keep them coming!
@JP,
Thanks, glad you like them. Anything in particular you'd like to see?
@Ben,
I'm infinitely more interested in jQuery than ColdFusion.
@JP,
OK cool, I'll keep it coming.
@Ben,
Thanks for this post. It helped me out a bunch. I had a lot of problems with this and in the end I opted to use a brute force approach because my dropdown menu was very complicated. Although not as graceful as your approach this might do the trick for some.
If you want to figure out when to hide your popup then bind some function to document.mousedown. In that function check to see if the event.target contains the popup by using event.target.find(popup). If event.target contains the popup then event.target cannot be the popup. So, call popup.hide().
The "popup" cannot contain itself, so, call popup.hide() when anything else contains popup.
@Craig,
Glad I could help - sorry for the long delay; sounds like you were on the right track regardless.