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