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