5be849f5615997399662e02063415380e9abcd41
[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 ( jQuery.browser.msie && elem.setInterval )
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                                 if ( typeof jQuery !== "undefined" && !jQuery.event.triggered )
44                                         return jQuery.event.handle.apply(arguments.callee.elem, arguments);
45                         });
46                 // Add elem as a property of the handle function
47                 // This is to prevent a memory leak with non-native
48                 // event in IE.
49                 handle.elem = elem;
50
51                 // Handle multiple events separated by a space
52                 // jQuery(...).bind("mouseover mouseout", fn);
53                 jQuery.each(types.split(/\s+/), function(index, type) {
54                         // Namespaced event handlers
55                         var parts = type.split(".");
56                         type = parts[0];
57                         handler.type = parts[1];
58
59                         // Get the current list of functions bound to this event
60                         var handlers = events[type];
61
62                         // Init the event handler queue
63                         if (!handlers) {
64                                 handlers = events[type] = {};
65
66                                 // Check for a special event handler
67                                 // Only use addEventListener/attachEvent if the special
68                                 // events handler returns false
69                                 if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem,data) === false ) {
70                                         // Bind the global event handler to the element
71                                         if (elem.addEventListener)
72                                                 elem.addEventListener(type, handle, false);
73                                         else if (elem.attachEvent)
74                                                 elem.attachEvent("on" + type, handle);
75                                 }
76                         }
77
78                         // Add the function to the element's handler list
79                         handlers[handler.guid] = handler;
80
81                         // Keep track of which events have been used, for global triggering
82                         jQuery.event.global[type] = true;
83                 });
84
85                 // Nullify elem to prevent memory leaks in IE
86                 elem = null;
87         },
88
89         guid: 1,
90         global: {},
91
92         // Detach an event or set of events from an element
93         remove: function(elem, types, handler) {
94                 // don't do events on text and comment nodes
95                 if ( elem.nodeType == 3 || elem.nodeType == 8 )
96                         return;
97
98                 var events = jQuery.data(elem, "events"), ret, index;
99
100                 if ( events ) {
101                         // Unbind all events for the element
102                         if ( types === undefined || (typeof types === "string" && types.charAt(0) == ".") )
103                                 for ( var type in events )
104                                         this.remove( elem, type + (types || "") );
105                         else {
106                                 // types is actually an event object here
107                                 if ( types.type ) {
108                                         handler = types.handler;
109                                         types = types.type;
110                                 }
111
112                                 // Handle multiple events seperated by a space
113                                 // jQuery(...).unbind("mouseover mouseout", fn);
114                                 jQuery.each(types.split(/\s+/), function(index, type){
115                                         // Namespaced event handlers
116                                         var parts = type.split(".");
117                                         type = parts[0];
118
119                                         if ( events[type] ) {
120                                                 // remove the given handler for the given type
121                                                 if ( handler )
122                                                         delete events[type][handler.guid];
123
124                                                 // remove all handlers for the given type
125                                                 else
126                                                         for ( handler in events[type] )
127                                                                 // Handle the removal of namespaced events
128                                                                 if ( !parts[1] || events[type][handler].type == parts[1] )
129                                                                         delete events[type][handler];
130
131                                                 // remove generic event handler if no more handlers exist
132                                                 for ( ret in events[type] ) break;
133                                                 if ( !ret ) {
134                                                         if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem) === false ) {
135                                                                 if (elem.removeEventListener)
136                                                                         elem.removeEventListener(type, jQuery.data(elem, "handle"), false);
137                                                                 else if (elem.detachEvent)
138                                                                         elem.detachEvent("on" + type, jQuery.data(elem, "handle"));
139                                                         }
140                                                         ret = null;
141                                                         delete events[type];
142                                                 }
143                                         }
144                                 });
145                         }
146
147                         // Remove the expando if it's no longer used
148                         for ( ret in events ) break;
149                         if ( !ret ) {
150                                 var handle = jQuery.data( elem, "handle" );
151                                 if ( handle ) handle.elem = null;
152                                 jQuery.removeData( elem, "events" );
153                                 jQuery.removeData( elem, "handle" );
154                         }
155                 }
156         },
157
158         trigger: function(type, data, elem, donative, extra) {
159                 // Clone the incoming data, if any
160                 data = jQuery.makeArray(data);
161
162                 if ( type.indexOf("!") >= 0 ) {
163                         type = type.slice(0, -1);
164                         var exclusive = true;
165                 }
166
167                 // Handle a global trigger
168                 if ( !elem ) {
169                         // Only trigger if we've ever bound an event for it
170                         if ( this.global[type] )
171                                 jQuery.each( jQuery.cache, function(){
172                                         if ( this.events && this.events[type] )
173                                                 jQuery.event.trigger( type, data, this.handle.elem );
174                                 });
175
176                 // Handle triggering a single element
177                 } else {
178                         // don't do events on text and comment nodes
179                         if ( elem.nodeType == 3 || elem.nodeType == 8 )
180                                 return undefined;
181
182                         var val, ret, fn = jQuery.isFunction( elem[ type ] || null ),
183                                 // Check to see if we need to provide a fake event, or not
184                                 event = !data[0] || !data[0].preventDefault;
185
186                         // Pass along a fake event
187                         if ( event ) {
188                                 data.unshift({
189                                         type: type,
190                                         target: elem,
191                                         preventDefault: function(){},
192                                         stopPropagation: function(){},
193                                         stopImmediatePropagation:stopImmediatePropagation,
194                                         timeStamp: now()
195                                 });
196                                 data[0][expando] = true; // no need to fix fake event
197                         }
198
199                         // Enforce the right trigger type
200                         data[0].type = type;
201                         if ( exclusive )
202                                 data[0].exclusive = true;
203
204                         // Trigger the event, it is assumed that "handle" is a function
205                         var handle = jQuery.data(elem, "handle");
206                         if ( handle )
207                                 val = handle.apply( elem, data );
208
209                         // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
210                         if ( (!fn || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
211                                 val = false;
212
213                         // Extra functions don't get the custom event object
214                         if ( event )
215                                 data.shift();
216
217                         // Handle triggering of extra function
218                         if ( extra && jQuery.isFunction( extra ) ) {
219                                 // call the extra function and tack the current return value on the end for possible inspection
220                                 ret = extra.apply( elem, val == null ? data : data.concat( val ) );
221                                 // if anything is returned, give it precedence and have it overwrite the previous value
222                                 if ( ret !== undefined )
223                                         val = ret;
224                         }
225
226                         // Trigger the native events (except for clicks on links)
227                         if ( fn && donative !== false && val !== false && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
228                                 this.triggered = true;
229                                 try {
230                                         elem[ type ]();
231                                 // prevent IE from throwing an error for some hidden elements
232                                 } catch (e) {}
233                         }
234
235                         this.triggered = false;
236                 }
237
238                 return val;
239         },
240
241         handle: function(event) {
242                 // returned undefined or false
243                 var val, ret, namespace, all, handlers;
244
245                 event = arguments[0] = jQuery.event.fix( event || window.event );
246
247                 // Namespaced event handlers
248                 namespace = event.type.split(".");
249                 event.type = namespace[0];
250                 namespace = namespace[1];
251                 // Cache this now, all = true means, any handler
252                 all = !namespace && !event.exclusive;
253
254                 handlers = ( jQuery.data(this, "events") || {} )[event.type];
255
256                 for ( var j in handlers ) {
257                         var handler = handlers[j];
258
259                         // Filter the functions by class
260                         if ( all || handler.type == namespace ) {
261                                 // Pass in a reference to the handler function itself
262                                 // So that we can later remove it
263                                 event.handler = handler;
264                                 event.data = handler.data;
265
266                                 ret = handler.apply( this, arguments );
267
268                                 if ( val !== false )
269                                         val = ret;
270
271                                 if ( ret === false ) {
272                                         event.preventDefault();
273                                         event.stopPropagation();
274                                 }
275
276                                 if( event._sip )
277                                         break;
278
279                         }
280                 }
281
282                 return val;
283         },
284
285         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 timeStamp toElement type view wheelDelta which".split(" "),
286
287         fix: function(event) {
288                 if ( event[expando] )
289                         return event;
290
291                 // store a copy of the original event object
292                 // and "clone" to set read-only properties
293                 var originalEvent = event;
294                 event = { originalEvent: originalEvent };
295
296                 for ( var i = this.props.length, prop; i; ){
297                         prop = this.props[ --i ];
298                         event[ prop ] = originalEvent[ prop ];
299                 }
300
301                 // Mark it as fixed
302                 event[expando] = true;
303
304                 // add preventDefault and stopPropagation since
305                 // they will not work on the clone
306                 event.preventDefault = function() {
307                         // if preventDefault exists run it on the original event
308                         if (originalEvent.preventDefault)
309                                 originalEvent.preventDefault();
310                         // otherwise set the returnValue property of the original event to false (IE)
311                         originalEvent.returnValue = false;
312                 };
313                 event.stopPropagation = function() {
314                         // if stopPropagation exists run it on the original event
315                         if (originalEvent.stopPropagation)
316                                 originalEvent.stopPropagation();
317                         // otherwise set the cancelBubble property of the original event to true (IE)
318                         originalEvent.cancelBubble = true;
319                 };
320
321                 event.stopImmediatePropagation = stopImmediatePropagation;
322
323                 // Fix timeStamp
324                 event.timeStamp = event.timeStamp || now();
325
326                 // Fix target property, if necessary
327                 if ( !event.target )
328                         event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
329
330                 // check if target is a textnode (safari)
331                 if ( event.target.nodeType == 3 )
332                         event.target = event.target.parentNode;
333
334                 // Add relatedTarget, if necessary
335                 if ( !event.relatedTarget && event.fromElement )
336                         event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
337
338                 // Calculate pageX/Y if missing and clientX/Y available
339                 if ( event.pageX == null && event.clientX != null ) {
340                         var doc = document.documentElement, body = document.body;
341                         event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
342                         event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
343                 }
344
345                 // Add which for key events
346                 if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
347                         event.which = event.charCode || event.keyCode;
348
349                 // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
350                 if ( !event.metaKey && event.ctrlKey )
351                         event.metaKey = event.ctrlKey;
352
353                 // Add which for click: 1 == left; 2 == middle; 3 == right
354                 // Note: button is not normalized, so don't use it
355                 if ( !event.which && event.button )
356                         event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
357
358                 return event;
359         },
360
361         proxy: function( fn, proxy ){
362                 // Set the guid of unique handler to the same of original handler, so it can be removed
363                 proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
364                 // So proxy can be declared as an argument
365                 return proxy;
366         },
367
368         special: {
369                 ready: {
370                         // Make sure the ready event is setup
371                         setup: bindReady,
372                         teardown: function() {}
373                 }
374         }
375 };
376
377 function stopImmediatePropagation(){
378         this._sip = 1;
379         this.stopPropagation();
380 }
381
382 if ( !jQuery.browser.msie ){    
383         // Checks if an event happened on an element within another element
384         // Used in jQuery.event.special.mouseenter and mouseleave handlers
385         var withinElement = function(event) {
386                 // Check if mouse(over|out) are still within the same parent element
387                 var parent = event.relatedTarget;
388                 // Traverse up the tree
389                 while ( parent && parent != this )
390                         try { parent = parent.parentNode; }
391                         catch(e) { parent = this; }
392                 
393                 if( parent != this ){
394                         // set the correct event type
395                         event.type = event.data;
396                         // handle event if we actually just moused on to a non sub-element
397                         jQuery.event.handle.apply( this, arguments );
398                 }
399         };
400         
401         jQuery.each({ 
402                 mouseover: 'mouseenter', 
403                 mouseout: 'mouseleave'
404         }, function( orig, fix ){
405                 jQuery.event.special[ fix ] = {
406                         setup: function(){
407                                 jQuery.event.add( this, orig, withinElement, fix );
408                         },
409                         teardown: function(){
410                                 jQuery.event.remove( this, orig, withinElement );
411                         }
412                 };                         
413         });
414 }
415
416 jQuery.fn.extend({
417         bind: function( type, data, fn ) {
418                 return type == "unload" ? this.one(type, data, fn) : this.each(function(){
419                         jQuery.event.add( this, type, fn || data, fn && data );
420                 });
421         },
422
423         one: function( type, data, fn ) {
424                 var one = jQuery.event.proxy( fn || data, function(event) {
425                         jQuery(this).unbind(event, one);
426                         return (fn || data).apply( this, arguments );
427                 });
428                 return this.each(function(){
429                         jQuery.event.add( this, type, one, fn && data);
430                 });
431         },
432
433         unbind: function( type, fn ) {
434                 return this.each(function(){
435                         jQuery.event.remove( this, type, fn );
436                 });
437         },
438
439         trigger: function( type, data, fn ) {
440                 return this.each(function(){
441                         jQuery.event.trigger( type, data, this, true, fn );
442                 });
443         },
444
445         triggerHandler: function( type, data, fn ) {
446                 return this[0] && jQuery.event.trigger( type, data, this[0], false, fn );
447         },
448
449         toggle: function( fn ) {
450                 // Save reference to arguments for access in closure
451                 var args = arguments, i = 1;
452
453                 // link all the functions, so any of them can unbind this click handler
454                 while( i < args.length )
455                         jQuery.event.proxy( fn, args[i++] );
456
457                 return this.click( jQuery.event.proxy( fn, function(event) {
458                         // Figure out which function to execute
459                         this.lastToggle = ( this.lastToggle || 0 ) % i;
460
461                         // Make sure that clicks stop
462                         event.preventDefault();
463
464                         // and execute the function
465                         return args[ this.lastToggle++ ].apply( this, arguments ) || false;
466                 }));
467         },
468
469         hover: function(fnOver, fnOut) {
470                 return this.mouseenter(fnOver).mouseleave(fnOut);
471         },
472
473         ready: function(fn) {
474                 // Attach the listeners
475                 bindReady();
476
477                 // If the DOM is already ready
478                 if ( jQuery.isReady )
479                         // Execute the function immediately
480                         fn.call( document, jQuery );
481
482                 // Otherwise, remember the function for later
483                 else
484                         // Add the function to the wait list
485                         jQuery.readyList.push( function() { return fn.call(this, jQuery); } );
486
487                 return this;
488         }
489 });
490
491 jQuery.extend({
492         isReady: false,
493         readyList: [],
494         // Handle when the DOM is ready
495         ready: function() {
496                 // Make sure that the DOM is not already loaded
497                 if ( !jQuery.isReady ) {
498                         // Remember that the DOM is ready
499                         jQuery.isReady = true;
500
501                         // If there are functions bound, to execute
502                         if ( jQuery.readyList ) {
503                                 // Execute all of them
504                                 jQuery.each( jQuery.readyList, function(){
505                                         this.call( document );
506                                 });
507
508                                 // Reset the list of functions
509                                 jQuery.readyList = null;
510                         }
511
512                         // Trigger any bound ready events
513                         jQuery(document).triggerHandler("ready");
514                 }
515         }
516 });
517
518 var readyBound = false;
519
520 function bindReady(){
521         if ( readyBound ) return;
522         readyBound = true;
523
524         // Mozilla, Opera and webkit nightlies currently support this event
525         if ( document.addEventListener ) {
526                 // Use the handy event callback
527                 document.addEventListener( "DOMContentLoaded", function(){
528                         document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
529                         jQuery.ready();
530                 }, false );
531
532         // If IE event model is used
533         } else if ( document.attachEvent ) {
534                 // ensure firing before onload,
535                 // maybe late but safe also for iframes
536                 document.attachEvent("onreadystatechange", function(){
537                         if ( document.readyState === "complete" ) {
538                                 document.detachEvent( "onreadystatechange", arguments.callee );
539                                 jQuery.ready();
540                         }
541                 });
542
543                 // If IE and not an iframe
544                 // continually check to see if the document is ready
545                 if ( document.documentElement.doScroll && !window.frameElement ) (function(){
546                         if ( jQuery.isReady ) return;
547
548                         try {
549                                 // If IE is used, use the trick by Diego Perini
550                                 // http://javascript.nwbox.com/IEContentLoaded/
551                                 document.documentElement.doScroll("left");
552                         } catch( error ) {
553                                 setTimeout( arguments.callee, 0 );
554                                 return;
555                         }
556
557                         // and execute any waiting functions
558                         jQuery.ready();
559                 })();
560         }
561
562         // A fallback to window.onload, that will always work
563         jQuery.event.add( window, "load", jQuery.ready );
564 }
565
566 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
567         "mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," +
568         "change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){
569
570         // Handle event binding
571         jQuery.fn[name] = function(fn){
572                 return fn ? this.bind(name, fn) : this.trigger(name);
573         };
574 });
575
576 // Prevent memory leaks in IE
577 // And prevent errors on refresh with events like mouseover in other browsers
578 // Window isn't included so as not to unbind existing unload events
579 jQuery( window ).bind( 'unload', function(){ 
580         for ( var id in jQuery.cache )
581                 // Skip the window
582                 if ( id != 1 && jQuery.cache[ id ].handle )
583                         jQuery.event.remove( jQuery.cache[ id ].handle.elem );
584 });