jquery event: Fixing event.currentTarget for live().
[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                 event.currentTarget = this;
244                 
245                 // Namespaced event handlers
246                 var namespaces = event.type.split(".");
247                 event.type = namespaces.shift();
248
249                 // Cache this now, all = true means, any handler
250                 all = !namespaces.length && !event.exclusive;
251                 
252                 var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
253
254                 handlers = ( jQuery.data(this, "events") || {} )[event.type];
255
256                 for ( var j in handlers ) {
257                         var handler = handlers[j];
258
259                         // Filter the functions by class
260                         if ( all || namespace.test(handler.type) ) {
261                                 // Pass in a reference to the handler function itself
262                                 // So that we can later remove it
263                                 event.handler = handler;
264                                 event.data = handler.data;
265
266                                 var ret = handler.apply(this, arguments);
267
268                                 if( ret !== undefined ){
269                                         event.result = ret;
270                                         if ( ret === false ) {
271                                                 event.preventDefault();
272                                                 event.stopPropagation();
273                                         }
274                                 }
275
276                                 if( event.isImmediatePropagationStopped() )
277                                         break;
278
279                         }
280                 }
281         },
282
283         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(" "),
284
285         fix: function(event) {
286                 if ( event[expando] )
287                         return event;
288
289                 // store a copy of the original event object
290                 // and "clone" to set read-only properties
291                 var originalEvent = event;
292                 event = jQuery.Event( originalEvent );
293
294                 for ( var i = this.props.length, prop; i; ){
295                         prop = this.props[ --i ];
296                         event[ prop ] = originalEvent[ prop ];
297                 }
298
299                 // Fix target property, if necessary
300                 if ( !event.target )
301                         event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
302
303                 // check if target is a textnode (safari)
304                 if ( event.target.nodeType == 3 )
305                         event.target = event.target.parentNode;
306
307                 // Add relatedTarget, if necessary
308                 if ( !event.relatedTarget && event.fromElement )
309                         event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
310
311                 // Calculate pageX/Y if missing and clientX/Y available
312                 if ( event.pageX == null && event.clientX != null ) {
313                         var doc = document.documentElement, body = document.body;
314                         event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
315                         event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
316                 }
317
318                 // Add which for key events
319                 if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
320                         event.which = event.charCode || event.keyCode;
321
322                 // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
323                 if ( !event.metaKey && event.ctrlKey )
324                         event.metaKey = event.ctrlKey;
325
326                 // Add which for click: 1 == left; 2 == middle; 3 == right
327                 // Note: button is not normalized, so don't use it
328                 if ( !event.which && event.button )
329                         event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
330
331                 return event;
332         },
333
334         proxy: function( fn, proxy ){
335                 proxy = proxy || function(){ return fn.apply(this, arguments); };
336                 // Set the guid of unique handler to the same of original handler, so it can be removed
337                 proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
338                 // So proxy can be declared as an argument
339                 return proxy;
340         },
341
342         special: {
343                 ready: {
344                         // Make sure the ready event is setup
345                         setup: bindReady,
346                         teardown: function() {}
347                 }
348         },
349         
350         specialAll: {
351                 live: {
352                         setup: function( selector, namespaces ){
353                                 jQuery.event.add( this, namespaces[0], liveHandler );
354                         },
355                         teardown:  function( namespaces ){
356                                 if ( namespaces.length ) {
357                                         var remove = 0, name = RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)");
358                                         
359                                         jQuery.each( (jQuery.data(this, "events").live || {}), function(){
360                                                 if ( name.test(this.type) )
361                                                         remove++;
362                                         });
363                                         
364                                         if ( remove < 1 )
365                                                 jQuery.event.remove( this, namespaces[0], liveHandler );
366                                 }
367                         }
368                 }
369         }
370 };
371
372 jQuery.Event = function( src ){
373         // Allow instantiation without the 'new' keyword
374         if( !this.preventDefault )
375                 return new jQuery.Event(src);
376         
377         // Event object
378         if( src && src.type ){
379                 this.originalEvent = src;
380                 this.type = src.type;
381         // Event type
382         }else
383                 this.type = src;
384
385         // timeStamp is buggy for some events on Firefox(#3843)
386         // So we won't rely on the native value
387         this.timeStamp = now();
388         
389         // Mark it as fixed
390         this[expando] = true;
391 };
392
393 function returnFalse(){
394         return false;
395 }
396 function returnTrue(){
397         return true;
398 }
399
400 // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
401 // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
402 jQuery.Event.prototype = {
403         preventDefault: function() {
404                 this.isDefaultPrevented = returnTrue;
405
406                 var e = this.originalEvent;
407                 if( !e )
408                         return;
409                 // if preventDefault exists run it on the original event
410                 if (e.preventDefault)
411                         e.preventDefault();
412                 // otherwise set the returnValue property of the original event to false (IE)
413                 e.returnValue = false;
414         },
415         stopPropagation: function() {
416                 this.isPropagationStopped = returnTrue;
417
418                 var e = this.originalEvent;
419                 if( !e )
420                         return;
421                 // if stopPropagation exists run it on the original event
422                 if (e.stopPropagation)
423                         e.stopPropagation();
424                 // otherwise set the cancelBubble property of the original event to true (IE)
425                 e.cancelBubble = true;
426         },
427         stopImmediatePropagation:function(){
428                 this.isImmediatePropagationStopped = returnTrue;
429                 this.stopPropagation();
430         },
431         isDefaultPrevented: returnFalse,
432         isPropagationStopped: returnFalse,
433         isImmediatePropagationStopped: returnFalse
434 };
435 // Checks if an event happened on an element within another element
436 // Used in jQuery.event.special.mouseenter and mouseleave handlers
437 var withinElement = function(event) {
438         // Check if mouse(over|out) are still within the same parent element
439         var parent = event.relatedTarget;
440         // Traverse up the tree
441         while ( parent && parent != this )
442                 try { parent = parent.parentNode; }
443                 catch(e) { parent = this; }
444         
445         if( parent != this ){
446                 // set the correct event type
447                 event.type = event.data;
448                 // handle event if we actually just moused on to a non sub-element
449                 jQuery.event.handle.apply( this, arguments );
450         }
451 };
452         
453 jQuery.each({ 
454         mouseover: 'mouseenter', 
455         mouseout: 'mouseleave'
456 }, function( orig, fix ){
457         jQuery.event.special[ fix ] = {
458                 setup: function(){
459                         jQuery.event.add( this, orig, withinElement, fix );
460                 },
461                 teardown: function(){
462                         jQuery.event.remove( this, orig, withinElement );
463                 }
464         };                         
465 });
466
467 jQuery.fn.extend({
468         bind: function( type, data, fn ) {
469                 return type == "unload" ? this.one(type, data, fn) : this.each(function(){
470                         jQuery.event.add( this, type, fn || data, fn && data );
471                 });
472         },
473
474         one: function( type, data, fn ) {
475                 var one = jQuery.event.proxy( fn || data, function(event) {
476                         jQuery(this).unbind(event, one);
477                         return (fn || data).apply( this, arguments );
478                 });
479                 return this.each(function(){
480                         jQuery.event.add( this, type, one, fn && data);
481                 });
482         },
483
484         unbind: function( type, fn ) {
485                 return this.each(function(){
486                         jQuery.event.remove( this, type, fn );
487                 });
488         },
489
490         trigger: function( type, data ) {
491                 return this.each(function(){
492                         jQuery.event.trigger( type, data, this );
493                 });
494         },
495
496         triggerHandler: function( type, data ) {
497                 if( this[0] ){
498                         var event = jQuery.Event(type);
499                         event.preventDefault();
500                         event.stopPropagation();
501                         jQuery.event.trigger( event, data, this[0] );
502                         return event.result;
503                 }               
504         },
505
506         toggle: function( fn ) {
507                 // Save reference to arguments for access in closure
508                 var args = arguments, i = 1;
509
510                 // link all the functions, so any of them can unbind this click handler
511                 while( i < args.length )
512                         jQuery.event.proxy( fn, args[i++] );
513
514                 return this.click( jQuery.event.proxy( fn, function(event) {
515                         // Figure out which function to execute
516                         this.lastToggle = ( this.lastToggle || 0 ) % i;
517
518                         // Make sure that clicks stop
519                         event.preventDefault();
520
521                         // and execute the function
522                         return args[ this.lastToggle++ ].apply( this, arguments ) || false;
523                 }));
524         },
525
526         hover: function(fnOver, fnOut) {
527                 return this.mouseenter(fnOver).mouseleave(fnOut);
528         },
529
530         ready: function(fn) {
531                 // Attach the listeners
532                 bindReady();
533
534                 // If the DOM is already ready
535                 if ( jQuery.isReady )
536                         // Execute the function immediately
537                         fn.call( document, jQuery );
538
539                 // Otherwise, remember the function for later
540                 else
541                         // Add the function to the wait list
542                         jQuery.readyList.push( fn );
543
544                 return this;
545         },
546         
547         live: function( type, fn ){
548                 var proxy = jQuery.event.proxy( fn );
549                 proxy.guid += this.selector + type;
550
551                 jQuery(document).bind( liveConvert(type, this.selector), this.selector, proxy );
552
553                 return this;
554         },
555         
556         die: function( type, fn ){
557                 jQuery(document).unbind( liveConvert(type, this.selector), fn ? { guid: fn.guid + this.selector + type } : null );
558                 return this;
559         }
560 });
561
562 function liveHandler( event ){
563         var check = RegExp("(^|\\.)" + event.type + "(\\.|$)"),
564                 stop = true,
565                 elems = [];
566
567         jQuery.each(jQuery.data(this, "events").live || [], function(i, fn){
568                 if ( check.test(fn.type) ) {
569                         var elem = jQuery(event.target).closest(fn.data)[0];
570                         if ( elem )
571                                 elems.push({ elem: elem, fn: fn });
572                 }
573         });
574
575         elems.sort(function(a,b) {
576                 return jQuery.data(a.elem, "closest") - jQuery.data(b.elem, "closest");
577         });
578         
579         jQuery.each(elems, function(){
580                 event.currentTarget = this.elem;
581                 if ( this.fn.call(this.elem, event, this.fn.data) === false )
582                         return (stop = false);
583         });
584
585         return stop;
586 }
587
588 function liveConvert(type, selector){
589         return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "|")].join(".");
590 }
591
592 jQuery.extend({
593         isReady: false,
594         readyList: [],
595         // Handle when the DOM is ready
596         ready: function() {
597                 // Make sure that the DOM is not already loaded
598                 if ( !jQuery.isReady ) {
599                         // Remember that the DOM is ready
600                         jQuery.isReady = true;
601
602                         // If there are functions bound, to execute
603                         if ( jQuery.readyList ) {
604                                 // Execute all of them
605                                 jQuery.each( jQuery.readyList, function(){
606                                         this.call( document, jQuery );
607                                 });
608
609                                 // Reset the list of functions
610                                 jQuery.readyList = null;
611                         }
612
613                         // Trigger any bound ready events
614                         jQuery(document).triggerHandler("ready");
615                 }
616         }
617 });
618
619 var readyBound = false;
620
621 function bindReady(){
622         if ( readyBound ) return;
623         readyBound = true;
624
625         // Mozilla, Opera and webkit nightlies currently support this event
626         if ( document.addEventListener ) {
627                 // Use the handy event callback
628                 document.addEventListener( "DOMContentLoaded", function(){
629                         document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
630                         jQuery.ready();
631                 }, false );
632
633         // If IE event model is used
634         } else if ( document.attachEvent ) {
635                 // ensure firing before onload,
636                 // maybe late but safe also for iframes
637                 document.attachEvent("onreadystatechange", function(){
638                         if ( document.readyState === "complete" ) {
639                                 document.detachEvent( "onreadystatechange", arguments.callee );
640                                 jQuery.ready();
641                         }
642                 });
643
644                 // If IE and not an iframe
645                 // continually check to see if the document is ready
646                 if ( document.documentElement.doScroll && window == window.top ) (function(){
647                         if ( jQuery.isReady ) return;
648
649                         try {
650                                 // If IE is used, use the trick by Diego Perini
651                                 // http://javascript.nwbox.com/IEContentLoaded/
652                                 document.documentElement.doScroll("left");
653                         } catch( error ) {
654                                 setTimeout( arguments.callee, 0 );
655                                 return;
656                         }
657
658                         // and execute any waiting functions
659                         jQuery.ready();
660                 })();
661         }
662
663         // A fallback to window.onload, that will always work
664         jQuery.event.add( window, "load", jQuery.ready );
665 }
666
667 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
668         "mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," +
669         "change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){
670
671         // Handle event binding
672         jQuery.fn[name] = function(fn){
673                 return fn ? this.bind(name, fn) : this.trigger(name);
674         };
675 });
676
677 // Prevent memory leaks in IE
678 // And prevent errors on refresh with events like mouseover in other browsers
679 // Window isn't included so as not to unbind existing unload events
680 jQuery( window ).bind( 'unload', function(){ 
681         for ( var id in jQuery.cache )
682                 // Skip the window
683                 if ( id != 1 && jQuery.cache[ id ].handle )
684                         jQuery.event.remove( jQuery.cache[ id ].handle.elem );
685 });