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