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