Fixed bubbling of live events (if an inner element handles an event first - and stops...
[jquery.git] / src / event.js
1 /*
2  * A number of helper functions used for managing events.
3  * Many of the ideas behind this code originated from
4  * Dean Edwards' addEvent library.
5  */
6 jQuery.event = {
7
8         // Bind an event to an element
9         // Original by Dean Edwards
10         add: function(elem, types, handler, data) {
11                 if ( elem.nodeType == 3 || elem.nodeType == 8 )
12                         return;
13
14                 // For whatever reason, IE has trouble passing the window object
15                 // around, causing it to be cloned in the process
16                 if ( elem.setInterval && elem != window )
17                         elem = window;
18
19                 // Make sure that the function being executed has a unique ID
20                 if ( !handler.guid )
21                         handler.guid = this.guid++;
22
23                 // if data is passed, bind to handler
24                 if ( data !== undefined ) {
25                         // Create temporary function pointer to original handler
26                         var fn = handler;
27
28                         // Create unique handler function, wrapped around original handler
29                         handler = this.proxy( fn );
30
31                         // Store data in unique handler
32                         handler.data = data;
33                 }
34
35                 // Init the element's event structure
36                 var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
37                         handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
38                                 // Handle the second event of a trigger and when
39                                 // an event is called after a page has unloaded
40                                 return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
41                                         jQuery.event.handle.apply(arguments.callee.elem, arguments) :
42                                         undefined;
43                         });
44                 // Add elem as a property of the handle function
45                 // This is to prevent a memory leak with non-native
46                 // event in IE.
47                 handle.elem = elem;
48
49                 // Handle multiple events separated by a space
50                 // jQuery(...).bind("mouseover mouseout", fn);
51                 jQuery.each(types.split(/\s+/), function(index, type) {
52                         // Namespaced event handlers
53                         var namespaces = type.split(".");
54                         type = namespaces.shift();
55                         handler.type = namespaces.slice().sort().join(".");
56
57                         // Get the current list of functions bound to this event
58                         var handlers = events[type];
59                         
60                         if ( jQuery.event.specialAll[type] )
61                                 jQuery.event.specialAll[type].setup.call(elem, data, namespaces);
62
63                         // Init the event handler queue
64                         if (!handlers) {
65                                 handlers = events[type] = {};
66
67                                 // Check for a special event handler
68                                 // Only use addEventListener/attachEvent if the special
69                                 // events handler returns false
70                                 if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false ) {
71                                         // Bind the global event handler to the element
72                                         if (elem.addEventListener)
73                                                 elem.addEventListener(type, handle, false);
74                                         else if (elem.attachEvent)
75                                                 elem.attachEvent("on" + type, handle);
76                                 }
77                         }
78
79                         // Add the function to the element's handler list
80                         handlers[handler.guid] = handler;
81
82                         // Keep track of which events have been used, for global triggering
83                         jQuery.event.global[type] = true;
84                 });
85
86                 // Nullify elem to prevent memory leaks in IE
87                 elem = null;
88         },
89
90         guid: 1,
91         global: {},
92
93         // Detach an event or set of events from an element
94         remove: function(elem, types, handler) {
95                 // don't do events on text and comment nodes
96                 if ( elem.nodeType == 3 || elem.nodeType == 8 )
97                         return;
98
99                 var events = jQuery.data(elem, "events"), ret, index;
100
101                 if ( events ) {
102                         // Unbind all events for the element
103                         if ( types === undefined || (typeof types === "string" && types.charAt(0) == ".") )
104                                 for ( var type in events )
105                                         this.remove( elem, type + (types || "") );
106                         else {
107                                 // types is actually an event object here
108                                 if ( types.type ) {
109                                         handler = types.handler;
110                                         types = types.type;
111                                 }
112
113                                 // Handle multiple events seperated by a space
114                                 // jQuery(...).unbind("mouseover mouseout", fn);
115                                 jQuery.each(types.split(/\s+/), function(index, type){
116                                         // Namespaced event handlers
117                                         var namespaces = type.split(".");
118                                         type = namespaces.shift();
119                                         var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
120
121                                         if ( events[type] ) {
122                                                 // remove the given handler for the given type
123                                                 if ( handler )
124                                                         delete events[type][handler.guid];
125
126                                                 // remove all handlers for the given type
127                                                 else
128                                                         for ( var handle in events[type] )
129                                                                 // Handle the removal of namespaced events
130                                                                 if ( namespace.test(events[type][handle].type) )
131                                                                         delete events[type][handle];
132                                                                         
133                                                 if ( jQuery.event.specialAll[type] )
134                                                         jQuery.event.specialAll[type].teardown.call(elem, namespaces);
135
136                                                 // remove generic event handler if no more handlers exist
137                                                 for ( ret in events[type] ) break;
138                                                 if ( !ret ) {
139                                                         if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem, namespaces) === false ) {
140                                                                 if (elem.removeEventListener)
141                                                                         elem.removeEventListener(type, jQuery.data(elem, "handle"), false);
142                                                                 else if (elem.detachEvent)
143                                                                         elem.detachEvent("on" + type, jQuery.data(elem, "handle"));
144                                                         }
145                                                         ret = null;
146                                                         delete events[type];
147                                                 }
148                                         }
149                                 });
150                         }
151
152                         // Remove the expando if it's no longer used
153                         for ( ret in events ) break;
154                         if ( !ret ) {
155                                 var handle = jQuery.data( elem, "handle" );
156                                 if ( handle ) handle.elem = null;
157                                 jQuery.removeData( elem, "events" );
158                                 jQuery.removeData( elem, "handle" );
159                         }
160                 }
161         },
162
163         // bubbling is internal
164         trigger: function( event, data, elem, bubbling ) {
165                 // Event object or event type
166                 var type = event.type || event;
167
168                 if( !bubbling ){
169                         event = typeof event === "object" ?
170                                 // jQuery.Event object
171                                 event[expando] ? event :
172                                 // Object literal
173                                 jQuery.extend( jQuery.Event(type), event ) :
174                                 // Just the event type (string)
175                                 jQuery.Event(type);
176
177                         if ( type.indexOf("!") >= 0 ) {
178                                 event.type = type = type.slice(0, -1);
179                                 event.exclusive = true;
180                         }
181
182                         // Handle a global trigger
183                         if ( !elem ) {
184                                 // Don't bubble custom events when global (to avoid too much overhead)
185                                 event.stopPropagation();
186                                 // Only trigger if we've ever bound an event for it
187                                 if ( this.global[type] )
188                                         jQuery.each( jQuery.cache, function(){
189                                                 if ( this.events && this.events[type] )
190                                                         jQuery.event.trigger( event, data, this.handle.elem );
191                                         });
192                         }
193
194                         // Handle triggering a single element
195
196                         // don't do events on text and comment nodes
197                         if ( !elem || elem.nodeType == 3 || elem.nodeType == 8 )
198                                 return undefined;
199                         
200                         // Clean up in case it is reused
201                         event.result = undefined;
202                         event.target = elem;
203                         
204                         // Clone the incoming data, if any
205                         data = jQuery.makeArray(data);
206                         data.unshift( event );
207                 }
208
209                 event.currentTarget = elem;
210
211                 // Trigger the event, it is assumed that "handle" is a function
212                 var handle = jQuery.data(elem, "handle");
213                 if ( handle )
214                         handle.apply( elem, data );
215
216                 // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
217                 if ( (!elem[type] || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
218                         event.result = false;
219
220                 // Trigger the native events (except for clicks on links)
221                 if ( !bubbling && elem[type] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
222                         this.triggered = true;
223                         try {
224                                 elem[ type ]();
225                         // prevent IE from throwing an error for some hidden elements
226                         } catch (e) {}
227                 }
228
229                 this.triggered = false;
230
231                 if ( !event.isPropagationStopped() ) {
232                         var parent = elem.parentNode || elem.ownerDocument;
233                         if ( parent )
234                                 jQuery.event.trigger(event, data, parent, true);
235                 }
236         },
237
238         handle: function(event) {
239                 // returned undefined or false
240                 var all, handlers;
241
242                 event = arguments[0] = jQuery.event.fix( event || window.event );
243
244                 // Namespaced event handlers
245                 var namespaces = event.type.split(".");
246                 event.type = namespaces.shift();
247
248                 // Cache this now, all = true means, any handler
249                 all = !namespaces.length && !event.exclusive;
250                 
251                 var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
252
253                 handlers = ( jQuery.data(this, "events") || {} )[event.type];
254
255                 for ( var j in handlers ) {
256                         var handler = handlers[j];
257
258                         // Filter the functions by class
259                         if ( all || namespace.test(handler.type) ) {
260                                 // Pass in a reference to the handler function itself
261                                 // So that we can later remove it
262                                 event.handler = handler;
263                                 event.data = handler.data;
264
265                                 var ret = handler.apply(this, arguments);
266
267                                 if( ret !== undefined ){
268                                         event.result = ret;
269                                         if ( ret === false ) {
270                                                 event.preventDefault();
271                                                 event.stopPropagation();
272                                         }
273                                 }
274
275                                 if( event.isImmediatePropagationStopped() )
276                                         break;
277
278                         }
279                 }
280         },
281
282         props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
283
284         fix: function(event) {
285                 if ( event[expando] )
286                         return event;
287
288                 // store a copy of the original event object
289                 // and "clone" to set read-only properties
290                 var originalEvent = event;
291                 event = jQuery.Event( originalEvent );
292
293                 for ( var i = this.props.length, prop; i; ){
294                         prop = this.props[ --i ];
295                         event[ prop ] = originalEvent[ prop ];
296                 }
297
298                 // Fix target property, if necessary
299                 if ( !event.target )
300                         event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
301
302                 // check if target is a textnode (safari)
303                 if ( event.target.nodeType == 3 )
304                         event.target = event.target.parentNode;
305
306                 // Add relatedTarget, if necessary
307                 if ( !event.relatedTarget && event.fromElement )
308                         event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
309
310                 // Calculate pageX/Y if missing and clientX/Y available
311                 if ( event.pageX == null && event.clientX != null ) {
312                         var doc = document.documentElement, body = document.body;
313                         event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
314                         event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
315                 }
316
317                 // Add which for key events
318                 if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
319                         event.which = event.charCode || event.keyCode;
320
321                 // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
322                 if ( !event.metaKey && event.ctrlKey )
323                         event.metaKey = event.ctrlKey;
324
325                 // Add which for click: 1 == left; 2 == middle; 3 == right
326                 // Note: button is not normalized, so don't use it
327                 if ( !event.which && event.button )
328                         event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
329
330                 return event;
331         },
332
333         proxy: function( fn, proxy ){
334                 proxy = proxy || function(){ return fn.apply(this, arguments); };
335                 // Set the guid of unique handler to the same of original handler, so it can be removed
336                 proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
337                 // So proxy can be declared as an argument
338                 return proxy;
339         },
340
341         special: {
342                 ready: {
343                         // Make sure the ready event is setup
344                         setup: bindReady,
345                         teardown: function() {}
346                 }
347         },
348         
349         specialAll: {
350                 live: {
351                         setup: function( selector, namespaces ){
352                                 jQuery.event.add( this, namespaces[0], liveHandler );
353                         },
354                         teardown:  function( namespaces ){
355                                 if ( namespaces.length ) {
356                                         var remove = 0, name = RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)");
357                                         
358                                         jQuery.each( (jQuery.data(this, "events").live || {}), function(){
359                                                 if ( name.test(this.type) )
360                                                         remove++;
361                                         });
362                                         
363                                         if ( remove < 1 )
364                                                 jQuery.event.remove( this, namespaces[0], liveHandler );
365                                 }
366                         }
367                 }
368         }
369 };
370
371 jQuery.Event = function( src ){
372         // Allow instantiation without the 'new' keyword
373         if( !this.preventDefault )
374                 return new jQuery.Event(src);
375         
376         // Event object
377         if( src && src.type ){
378                 this.originalEvent = src;
379                 this.type = src.type;
380         // Event type
381         }else
382                 this.type = src;
383
384         // timeStamp is buggy for some events on Firefox(#3843)
385         // So we won't rely on the native value
386         this.timeStamp = now();
387         
388         // Mark it as fixed
389         this[expando] = true;
390 };
391
392 function returnFalse(){
393         return false;
394 }
395 function returnTrue(){
396         return true;
397 }
398
399 // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
400 // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
401 jQuery.Event.prototype = {
402         preventDefault: function() {
403                 this.isDefaultPrevented = returnTrue;
404
405                 var e = this.originalEvent;
406                 if( !e )
407                         return;
408                 // if preventDefault exists run it on the original event
409                 if (e.preventDefault)
410                         e.preventDefault();
411                 // otherwise set the returnValue property of the original event to false (IE)
412                 e.returnValue = false;
413         },
414         stopPropagation: function() {
415                 this.isPropagationStopped = returnTrue;
416
417                 var e = this.originalEvent;
418                 if( !e )
419                         return;
420                 // if stopPropagation exists run it on the original event
421                 if (e.stopPropagation)
422                         e.stopPropagation();
423                 // otherwise set the cancelBubble property of the original event to true (IE)
424                 e.cancelBubble = true;
425         },
426         stopImmediatePropagation:function(){
427                 this.isImmediatePropagationStopped = returnTrue;
428                 this.stopPropagation();
429         },
430         isDefaultPrevented: returnFalse,
431         isPropagationStopped: returnFalse,
432         isImmediatePropagationStopped: returnFalse
433 };
434 // Checks if an event happened on an element within another element
435 // Used in jQuery.event.special.mouseenter and mouseleave handlers
436 var withinElement = function(event) {
437         // Check if mouse(over|out) are still within the same parent element
438         var parent = event.relatedTarget;
439         // Traverse up the tree
440         while ( parent && parent != this )
441                 try { parent = parent.parentNode; }
442                 catch(e) { parent = this; }
443         
444         if( parent != this ){
445                 // set the correct event type
446                 event.type = event.data;
447                 // handle event if we actually just moused on to a non sub-element
448                 jQuery.event.handle.apply( this, arguments );
449         }
450 };
451         
452 jQuery.each({ 
453         mouseover: 'mouseenter', 
454         mouseout: 'mouseleave'
455 }, function( orig, fix ){
456         jQuery.event.special[ fix ] = {
457                 setup: function(){
458                         jQuery.event.add( this, orig, withinElement, fix );
459                 },
460                 teardown: function(){
461                         jQuery.event.remove( this, orig, withinElement );
462                 }
463         };                         
464 });
465
466 jQuery.fn.extend({
467         bind: function( type, data, fn ) {
468                 return type == "unload" ? this.one(type, data, fn) : this.each(function(){
469                         jQuery.event.add( this, type, fn || data, fn && data );
470                 });
471         },
472
473         one: function( type, data, fn ) {
474                 var one = jQuery.event.proxy( fn || data, function(event) {
475                         jQuery(this).unbind(event, one);
476                         return (fn || data).apply( this, arguments );
477                 });
478                 return this.each(function(){
479                         jQuery.event.add( this, type, one, fn && data);
480                 });
481         },
482
483         unbind: function( type, fn ) {
484                 return this.each(function(){
485                         jQuery.event.remove( this, type, fn );
486                 });
487         },
488
489         trigger: function( type, data ) {
490                 return this.each(function(){
491                         jQuery.event.trigger( type, data, this );
492                 });
493         },
494
495         triggerHandler: function( type, data ) {
496                 if( this[0] ){
497                         var event = jQuery.Event(type);
498                         event.preventDefault();
499                         event.stopPropagation();
500                         jQuery.event.trigger( event, data, this[0] );
501                         return event.result;
502                 }               
503         },
504
505         toggle: function( fn ) {
506                 // Save reference to arguments for access in closure
507                 var args = arguments, i = 1;
508
509                 // link all the functions, so any of them can unbind this click handler
510                 while( i < args.length )
511                         jQuery.event.proxy( fn, args[i++] );
512
513                 return this.click( jQuery.event.proxy( fn, function(event) {
514                         // Figure out which function to execute
515                         this.lastToggle = ( this.lastToggle || 0 ) % i;
516
517                         // Make sure that clicks stop
518                         event.preventDefault();
519
520                         // and execute the function
521                         return args[ this.lastToggle++ ].apply( this, arguments ) || false;
522                 }));
523         },
524
525         hover: function(fnOver, fnOut) {
526                 return this.mouseenter(fnOver).mouseleave(fnOut);
527         },
528
529         ready: function(fn) {
530                 // Attach the listeners
531                 bindReady();
532
533                 // If the DOM is already ready
534                 if ( jQuery.isReady )
535                         // Execute the function immediately
536                         fn.call( document, jQuery );
537
538                 // Otherwise, remember the function for later
539                 else
540                         // Add the function to the wait list
541                         jQuery.readyList.push( fn );
542
543                 return this;
544         },
545         
546         live: function( type, fn ){
547                 var proxy = jQuery.event.proxy( fn );
548                 proxy.guid += this.selector + type;
549
550                 jQuery(document).bind( liveConvert(type, this.selector), this.selector, proxy );
551
552                 return this;
553         },
554         
555         die: function( type, fn ){
556                 jQuery(document).unbind( liveConvert(type, this.selector), fn ? { guid: fn.guid + this.selector + type } : null );
557                 return this;
558         }
559 });
560
561 function liveHandler( event ){
562         var check = RegExp("(^|\\.)" + event.type + "(\\.|$)"),
563                 stop = true,
564                 elems = [];
565
566         jQuery.each(jQuery.data(this, "events").live || [], function(i, fn){
567                 if ( check.test(fn.type) ) {
568                         var elem = jQuery(event.target).closest(fn.data)[0];
569                         if ( elem )
570                                 elems.push({ elem: elem, fn: fn });
571                 }
572         });
573
574         elems.sort(function(a,b) {
575                 return jQuery.data(a.elem, "closest") - jQuery.data(b.elem, "closest");
576         });
577         
578         jQuery.each(elems, function(){
579                 if ( this.fn.call(this.elem, event, this.fn.data) === false )
580                         return (stop = false);
581         });
582
583         return stop;
584 }
585
586 function liveConvert(type, selector){
587         return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "|")].join(".");
588 }
589
590 jQuery.extend({
591         isReady: false,
592         readyList: [],
593         // Handle when the DOM is ready
594         ready: function() {
595                 // Make sure that the DOM is not already loaded
596                 if ( !jQuery.isReady ) {
597                         // Remember that the DOM is ready
598                         jQuery.isReady = true;
599
600                         // If there are functions bound, to execute
601                         if ( jQuery.readyList ) {
602                                 // Execute all of them
603                                 jQuery.each( jQuery.readyList, function(){
604                                         this.call( document, jQuery );
605                                 });
606
607                                 // Reset the list of functions
608                                 jQuery.readyList = null;
609                         }
610
611                         // Trigger any bound ready events
612                         jQuery(document).triggerHandler("ready");
613                 }
614         }
615 });
616
617 var readyBound = false;
618
619 function bindReady(){
620         if ( readyBound ) return;
621         readyBound = true;
622
623         // Mozilla, Opera and webkit nightlies currently support this event
624         if ( document.addEventListener ) {
625                 // Use the handy event callback
626                 document.addEventListener( "DOMContentLoaded", function(){
627                         document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
628                         jQuery.ready();
629                 }, false );
630
631         // If IE event model is used
632         } else if ( document.attachEvent ) {
633                 // ensure firing before onload,
634                 // maybe late but safe also for iframes
635                 document.attachEvent("onreadystatechange", function(){
636                         if ( document.readyState === "complete" ) {
637                                 document.detachEvent( "onreadystatechange", arguments.callee );
638                                 jQuery.ready();
639                         }
640                 });
641
642                 // If IE and not an iframe
643                 // continually check to see if the document is ready
644                 if ( document.documentElement.doScroll && window == window.top ) (function(){
645                         if ( jQuery.isReady ) return;
646
647                         try {
648                                 // If IE is used, use the trick by Diego Perini
649                                 // http://javascript.nwbox.com/IEContentLoaded/
650                                 document.documentElement.doScroll("left");
651                         } catch( error ) {
652                                 setTimeout( arguments.callee, 0 );
653                                 return;
654                         }
655
656                         // and execute any waiting functions
657                         jQuery.ready();
658                 })();
659         }
660
661         // A fallback to window.onload, that will always work
662         jQuery.event.add( window, "load", jQuery.ready );
663 }
664
665 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
666         "mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," +
667         "change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){
668
669         // Handle event binding
670         jQuery.fn[name] = function(fn){
671                 return fn ? this.bind(name, fn) : this.trigger(name);
672         };
673 });
674
675 // Prevent memory leaks in IE
676 // And prevent errors on refresh with events like mouseover in other browsers
677 // Window isn't included so as not to unbind existing unload events
678 jQuery( window ).bind( 'unload', function(){ 
679         for ( var id in jQuery.cache )
680                 // Skip the window
681                 if ( id != 1 && jQuery.cache[ id ].handle )
682                         jQuery.event.remove( jQuery.cache[ id ].handle.elem );
683 });