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