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