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