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