Added support for multiple-namespaced events (in bind, trigger, and unbind).
[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 ( jQuery.browser.msie && elem.setInterval )
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, function() {
30                                 // Pass arguments and context to original handler
31                                 return fn.apply(this, arguments);
32                         });
33
34                         // Store data in unique handler
35                         handler.data = data;
36                 }
37
38                 // Init the element's event structure
39                 var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
40                         handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
41                                 // Handle the second event of a trigger and when
42                                 // an event is called after a page has unloaded
43                                 if ( typeof jQuery !== "undefined" && !jQuery.event.triggered )
44                                         return jQuery.event.handle.apply(arguments.callee.elem, arguments);
45                         });
46                 // Add elem as a property of the handle function
47                 // This is to prevent a memory leak with non-native
48                 // event in IE.
49                 handle.elem = elem;
50
51                 // Handle multiple events separated by a space
52                 // jQuery(...).bind("mouseover mouseout", fn);
53                 jQuery.each(types.split(/\s+/), function(index, type) {
54                         // Namespaced event handlers
55                         var parts = type.split(".");
56                         type = parts.shift();
57                         handler.type = parts.sort().join(".");
58
59                         // Get the current list of functions bound to this event
60                         var handlers = events[type];
61
62                         // Init the event handler queue
63                         if (!handlers) {
64                                 handlers = events[type] = {};
65
66                                 // Check for a special event handler
67                                 // Only use addEventListener/attachEvent if the special
68                                 // events handler returns false
69                                 if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem,data) === false ) {
70                                         // Bind the global event handler to the element
71                                         if (elem.addEventListener)
72                                                 elem.addEventListener(type, handle, false);
73                                         else if (elem.attachEvent)
74                                                 elem.attachEvent("on" + type, handle);
75                                 }
76                         }
77
78                         // Add the function to the element's handler list
79                         handlers[handler.guid] = handler;
80
81                         // Keep track of which events have been used, for global triggering
82                         jQuery.event.global[type] = true;
83                 });
84
85                 // Nullify elem to prevent memory leaks in IE
86                 elem = null;
87         },
88
89         guid: 1,
90         global: {},
91
92         // Detach an event or set of events from an element
93         remove: function(elem, types, handler) {
94                 // don't do events on text and comment nodes
95                 if ( elem.nodeType == 3 || elem.nodeType == 8 )
96                         return;
97
98                 var events = jQuery.data(elem, "events"), ret, index;
99
100                 if ( events ) {
101                         // Unbind all events for the element
102                         if ( types === undefined || (typeof types === "string" && types.charAt(0) == ".") )
103                                 for ( var type in events )
104                                         this.remove( elem, type + (types || "") );
105                         else {
106                                 // types is actually an event object here
107                                 if ( types.type ) {
108                                         handler = types.handler;
109                                         types = types.type;
110                                 }
111
112                                 // Handle multiple events seperated by a space
113                                 // jQuery(...).unbind("mouseover mouseout", fn);
114                                 jQuery.each(types.split(/\s+/), function(index, type){
115                                         // Namespaced event handlers
116                                         var namespace = type.split(".");
117                                         type = namespace.shift();
118                                         namespace = RegExp(namespace.sort().join(".*\\.") + "(\\.|$)");
119
120                                         if ( events[type] ) {
121                                                 // remove the given handler for the given type
122                                                 if ( handler )
123                                                         delete events[type][handler.guid];
124
125                                                 // remove all handlers for the given type
126                                                 else
127                                                         for ( handler in events[type] )
128                                                                 // Handle the removal of namespaced events
129                                                                 if ( namespace.test(events[type][handler].type) )
130                                                                         delete events[type][handler];
131
132                                                 // remove generic event handler if no more handlers exist
133                                                 for ( ret in events[type] ) break;
134                                                 if ( !ret ) {
135                                                         if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem) === false ) {
136                                                                 if (elem.removeEventListener)
137                                                                         elem.removeEventListener(type, jQuery.data(elem, "handle"), false);
138                                                                 else if (elem.detachEvent)
139                                                                         elem.detachEvent("on" + type, jQuery.data(elem, "handle"));
140                                                         }
141                                                         ret = null;
142                                                         delete events[type];
143                                                 }
144                                         }
145                                 });
146                         }
147
148                         // Remove the expando if it's no longer used
149                         for ( ret in events ) break;
150                         if ( !ret ) {
151                                 var handle = jQuery.data( elem, "handle" );
152                                 if ( handle ) handle.elem = null;
153                                 jQuery.removeData( elem, "events" );
154                                 jQuery.removeData( elem, "handle" );
155                         }
156                 }
157         },
158
159         trigger: function(type, data, elem, donative, extra) {
160                 // Clone the incoming data, if any
161                 data = jQuery.makeArray(data);
162
163                 if ( type.indexOf("!") >= 0 ) {
164                         type = type.slice(0, -1);
165                         var exclusive = true;
166                 }
167
168                 // Handle a global trigger
169                 if ( !elem ) {
170                         // Only trigger if we've ever bound an event for it
171                         if ( this.global[type] )
172                                 jQuery.each( jQuery.cache, function(){
173                                         if ( this.events && this.events[type] )
174                                                 jQuery.event.trigger( type, data, this.handle.elem );
175                                 });
176
177                 // Handle triggering a single element
178                 } else {
179                         // don't do events on text and comment nodes
180                         if ( elem.nodeType == 3 || elem.nodeType == 8 )
181                                 return undefined;
182
183                         var val, ret, fn = jQuery.isFunction( elem[ type ] || null ),
184                                 // Check to see if we need to provide a fake event, or not
185                                 event = !data[0] || !data[0].preventDefault;
186
187                         // Pass along a fake event
188                         if ( event ) {
189                                 data.unshift({
190                                         type: type,
191                                         target: elem,
192                                         preventDefault: function(){},
193                                         stopPropagation: function(){},
194                                         stopImmediatePropagation:stopImmediatePropagation,
195                                         timeStamp: now()
196                                 });
197                                 data[0][expando] = true; // no need to fix fake event
198                         }
199
200                         // Enforce the right trigger type
201                         data[0].type = type;
202                         if ( exclusive )
203                                 data[0].exclusive = true;
204
205                         // Trigger the event, it is assumed that "handle" is a function
206                         var handle = jQuery.data(elem, "handle");
207                         if ( handle )
208                                 val = handle.apply( elem, data );
209
210                         // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
211                         if ( (!fn || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
212                                 val = false;
213
214                         // Extra functions don't get the custom event object
215                         if ( event )
216                                 data.shift();
217
218                         // Handle triggering of extra function
219                         if ( extra && jQuery.isFunction( extra ) ) {
220                                 // call the extra function and tack the current return value on the end for possible inspection
221                                 ret = extra.apply( elem, val == null ? data : data.concat( val ) );
222                                 // if anything is returned, give it precedence and have it overwrite the previous value
223                                 if ( ret !== undefined )
224                                         val = ret;
225                         }
226
227                         // Trigger the native events (except for clicks on links)
228                         if ( fn && donative !== false && val !== false && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
229                                 this.triggered = true;
230                                 try {
231                                         elem[ type ]();
232                                 // prevent IE from throwing an error for some hidden elements
233                                 } catch (e) {}
234                         }
235
236                         this.triggered = false;
237                 }
238
239                 return val;
240         },
241
242         handle: function(event) {
243                 // returned undefined or false
244                 var val, ret, namespace, all, handlers;
245
246                 event = arguments[0] = jQuery.event.fix( event || window.event );
247
248                 // Namespaced event handlers
249                 namespace = event.type.split(".");
250                 event.type = namespace.shift();
251                 namespace = RegExp(namespace.sort().join(".*\\.") + "(\\.|$)");
252                 // Cache this now, all = true means, any handler
253                 all = !namespace && !event.exclusive;
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                                 ret = handler.apply( this, arguments );
268
269                                 if ( val !== false )
270                                         val = ret;
271
272                                 if ( ret === false ) {
273                                         event.preventDefault();
274                                         event.stopPropagation();
275                                 }
276
277                                 if( event._sip )
278                                         break;
279
280                         }
281                 }
282
283                 return val;
284         },
285
286         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 timeStamp toElement type view wheelDelta which".split(" "),
287
288         fix: function(event) {
289                 if ( event[expando] )
290                         return event;
291
292                 // store a copy of the original event object
293                 // and "clone" to set read-only properties
294                 var originalEvent = event;
295                 event = { originalEvent: originalEvent };
296
297                 for ( var i = this.props.length, prop; i; ){
298                         prop = this.props[ --i ];
299                         event[ prop ] = originalEvent[ prop ];
300                 }
301
302                 // Mark it as fixed
303                 event[expando] = true;
304
305                 // add preventDefault and stopPropagation since
306                 // they will not work on the clone
307                 event.preventDefault = function() {
308                         // if preventDefault exists run it on the original event
309                         if (originalEvent.preventDefault)
310                                 originalEvent.preventDefault();
311                         // otherwise set the returnValue property of the original event to false (IE)
312                         originalEvent.returnValue = false;
313                 };
314                 event.stopPropagation = function() {
315                         // if stopPropagation exists run it on the original event
316                         if (originalEvent.stopPropagation)
317                                 originalEvent.stopPropagation();
318                         // otherwise set the cancelBubble property of the original event to true (IE)
319                         originalEvent.cancelBubble = true;
320                 };
321
322                 event.stopImmediatePropagation = stopImmediatePropagation;
323
324                 // Fix timeStamp
325                 event.timeStamp = event.timeStamp || now();
326
327                 // Fix target property, if necessary
328                 if ( !event.target )
329                         event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
330
331                 // check if target is a textnode (safari)
332                 if ( event.target.nodeType == 3 )
333                         event.target = event.target.parentNode;
334
335                 // Add relatedTarget, if necessary
336                 if ( !event.relatedTarget && event.fromElement )
337                         event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
338
339                 // Calculate pageX/Y if missing and clientX/Y available
340                 if ( event.pageX == null && event.clientX != null ) {
341                         var doc = document.documentElement, body = document.body;
342                         event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
343                         event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
344                 }
345
346                 // Add which for key events
347                 if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
348                         event.which = event.charCode || event.keyCode;
349
350                 // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
351                 if ( !event.metaKey && event.ctrlKey )
352                         event.metaKey = event.ctrlKey;
353
354                 // Add which for click: 1 == left; 2 == middle; 3 == right
355                 // Note: button is not normalized, so don't use it
356                 if ( !event.which && event.button )
357                         event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
358
359                 return event;
360         },
361
362         proxy: function( fn, proxy ){
363                 // Set the guid of unique handler to the same of original handler, so it can be removed
364                 proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
365                 // So proxy can be declared as an argument
366                 return proxy;
367         },
368
369         special: {
370                 ready: {
371                         // Make sure the ready event is setup
372                         setup: bindReady,
373                         teardown: function() {}
374                 }
375         }
376 };
377
378 function stopImmediatePropagation(){
379         this._sip = 1;
380         this.stopPropagation();
381 }
382
383 if ( !jQuery.browser.msie ){    
384         // Checks if an event happened on an element within another element
385         // Used in jQuery.event.special.mouseenter and mouseleave handlers
386         var withinElement = function(event) {
387                 // Check if mouse(over|out) are still within the same parent element
388                 var parent = event.relatedTarget;
389                 // Traverse up the tree
390                 while ( parent && parent != this )
391                         try { parent = parent.parentNode; }
392                         catch(e) { parent = this; }
393                 
394                 if( parent != this ){
395                         // set the correct event type
396                         event.type = event.data;
397                         // handle event if we actually just moused on to a non sub-element
398                         jQuery.event.handle.apply( this, arguments );
399                 }
400         };
401         
402         jQuery.each({ 
403                 mouseover: 'mouseenter', 
404                 mouseout: 'mouseleave'
405         }, function( orig, fix ){
406                 jQuery.event.special[ fix ] = {
407                         setup: function(){
408                                 jQuery.event.add( this, orig, withinElement, fix );
409                         },
410                         teardown: function(){
411                                 jQuery.event.remove( this, orig, withinElement );
412                         }
413                 };                         
414         });
415 }
416
417 jQuery.fn.extend({
418         bind: function( type, data, fn ) {
419                 return type == "unload" ? this.one(type, data, fn) : this.each(function(){
420                         jQuery.event.add( this, type, fn || data, fn && data );
421                 });
422         },
423
424         one: function( type, data, fn ) {
425                 var one = jQuery.event.proxy( fn || data, function(event) {
426                         jQuery(this).unbind(event, one);
427                         return (fn || data).apply( this, arguments );
428                 });
429                 return this.each(function(){
430                         jQuery.event.add( this, type, one, fn && data);
431                 });
432         },
433
434         unbind: function( type, fn ) {
435                 return this.each(function(){
436                         jQuery.event.remove( this, type, fn );
437                 });
438         },
439
440         trigger: function( type, data, fn ) {
441                 return this.each(function(){
442                         jQuery.event.trigger( type, data, this, true, fn );
443                 });
444         },
445
446         triggerHandler: function( type, data, fn ) {
447                 return this[0] && jQuery.event.trigger( type, data, this[0], false, fn );
448         },
449
450         toggle: function( fn ) {
451                 // Save reference to arguments for access in closure
452                 var args = arguments, i = 1;
453
454                 // link all the functions, so any of them can unbind this click handler
455                 while( i < args.length )
456                         jQuery.event.proxy( fn, args[i++] );
457
458                 return this.click( jQuery.event.proxy( fn, function(event) {
459                         // Figure out which function to execute
460                         this.lastToggle = ( this.lastToggle || 0 ) % i;
461
462                         // Make sure that clicks stop
463                         event.preventDefault();
464
465                         // and execute the function
466                         return args[ this.lastToggle++ ].apply( this, arguments ) || false;
467                 }));
468         },
469
470         hover: function(fnOver, fnOut) {
471                 return this.mouseenter(fnOver).mouseleave(fnOut);
472         },
473
474         ready: function(fn) {
475                 // Attach the listeners
476                 bindReady();
477
478                 // If the DOM is already ready
479                 if ( jQuery.isReady )
480                         // Execute the function immediately
481                         fn.call( document, jQuery );
482
483                 // Otherwise, remember the function for later
484                 else
485                         // Add the function to the wait list
486                         jQuery.readyList.push( function() { return fn.call(this, jQuery); } );
487
488                 return this;
489         }
490 });
491
492 jQuery.extend({
493         isReady: false,
494         readyList: [],
495         // Handle when the DOM is ready
496         ready: function() {
497                 // Make sure that the DOM is not already loaded
498                 if ( !jQuery.isReady ) {
499                         // Remember that the DOM is ready
500                         jQuery.isReady = true;
501
502                         // If there are functions bound, to execute
503                         if ( jQuery.readyList ) {
504                                 // Execute all of them
505                                 jQuery.each( jQuery.readyList, function(){
506                                         this.call( document );
507                                 });
508
509                                 // Reset the list of functions
510                                 jQuery.readyList = null;
511                         }
512
513                         // Trigger any bound ready events
514                         jQuery(document).triggerHandler("ready");
515                 }
516         }
517 });
518
519 var readyBound = false;
520
521 function bindReady(){
522         if ( readyBound ) return;
523         readyBound = true;
524
525         // Mozilla, Opera and webkit nightlies currently support this event
526         if ( document.addEventListener ) {
527                 // Use the handy event callback
528                 document.addEventListener( "DOMContentLoaded", function(){
529                         document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
530                         jQuery.ready();
531                 }, false );
532
533         // If IE event model is used
534         } else if ( document.attachEvent ) {
535                 // ensure firing before onload,
536                 // maybe late but safe also for iframes
537                 document.attachEvent("onreadystatechange", function(){
538                         if ( document.readyState === "complete" ) {
539                                 document.detachEvent( "onreadystatechange", arguments.callee );
540                                 jQuery.ready();
541                         }
542                 });
543
544                 // If IE and not an iframe
545                 // continually check to see if the document is ready
546                 if ( document.documentElement.doScroll && !window.frameElement ) (function(){
547                         if ( jQuery.isReady ) return;
548
549                         try {
550                                 // If IE is used, use the trick by Diego Perini
551                                 // http://javascript.nwbox.com/IEContentLoaded/
552                                 document.documentElement.doScroll("left");
553                         } catch( error ) {
554                                 setTimeout( arguments.callee, 0 );
555                                 return;
556                         }
557
558                         // and execute any waiting functions
559                         jQuery.ready();
560                 })();
561         }
562
563         // A fallback to window.onload, that will always work
564         jQuery.event.add( window, "load", jQuery.ready );
565 }
566
567 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
568         "mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," +
569         "change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){
570
571         // Handle event binding
572         jQuery.fn[name] = function(fn){
573                 return fn ? this.bind(name, fn) : this.trigger(name);
574         };
575 });
576
577 // Prevent memory leaks in IE
578 // And prevent errors on refresh with events like mouseover in other browsers
579 // Window isn't included so as not to unbind existing unload events
580 jQuery( window ).bind( 'unload', function(){ 
581         for ( var id in jQuery.cache )
582                 // Skip the window
583                 if ( id != 1 && jQuery.cache[ id ].handle )
584                         jQuery.event.remove( jQuery.cache[ id ].handle.elem );
585 });