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