jQuery.event.fix performance boost. An example: click event was previously ~3ms and...
[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 != undefined )
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 = 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 seperated 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: +new Date
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 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 || +new Date;
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         special: {
350                 ready: {
351                         setup: function() {
352                                 // Make sure the ready event is setup
353                                 bindReady();
354                                 return;
355                         },
356                         
357                         teardown: function() { return; }
358                 },
359                 
360                 mouseenter: {
361                         setup: function() {
362                                 if ( jQuery.browser.msie ) return false;
363                                 jQuery(this).bind("mouseover", jQuery.event.special.mouseenter.handler);
364                                 return true;
365                         },
366                 
367                         teardown: function() {
368                                 if ( jQuery.browser.msie ) return false;
369                                 jQuery(this).unbind("mouseover", jQuery.event.special.mouseenter.handler);
370                                 return true;
371                         },
372                         
373                         handler: function(event) {
374                                 // If we actually just moused on to a sub-element, ignore it
375                                 if ( withinElement(event, this) ) return true;
376                                 // Execute the right handlers by setting the event type to mouseenter
377                                 arguments[0].type = "mouseenter";
378                                 return jQuery.event.handle.apply(this, arguments);
379                         }
380                 },
381         
382                 mouseleave: {
383                         setup: function() {
384                                 if ( jQuery.browser.msie ) return false;
385                                 jQuery(this).bind("mouseout", jQuery.event.special.mouseleave.handler);
386                                 return true;
387                         },
388                 
389                         teardown: function() {
390                                 if ( jQuery.browser.msie ) return false;
391                                 jQuery(this).unbind("mouseout", jQuery.event.special.mouseleave.handler);
392                                 return true;
393                         },
394                         
395                         handler: function(event) {
396                                 // If we actually just moused on to a sub-element, ignore it
397                                 if ( withinElement(event, this) ) return true;
398                                 // Execute the right handlers by setting the event type to mouseleave
399                                 arguments[0].type = "mouseleave";
400                                 return jQuery.event.handle.apply(this, arguments);
401                         }
402                 }
403         }
404 };
405
406 jQuery.fn.extend({
407         bind: function( type, data, fn ) {
408                 return type == "unload" ? this.one(type, data, fn) : this.each(function(){
409                         jQuery.event.add( this, type, fn || data, fn && data );
410                 });
411         },
412         
413         one: function( type, data, fn ) {
414                 return this.each(function(){
415                         jQuery.event.add( this, type, function(event) {
416                                 jQuery(this).unbind(event);
417                                 return (fn || data).apply( this, arguments);
418                         }, fn && data);
419                 });
420         },
421
422         unbind: function( type, fn ) {
423                 return this.each(function(){
424                         jQuery.event.remove( this, type, fn );
425                 });
426         },
427
428         trigger: function( type, data, fn ) {
429                 return this.each(function(){
430                         jQuery.event.trigger( type, data, this, true, fn );
431                 });
432         },
433
434         triggerHandler: function( type, data, fn ) {
435                 if ( this[0] )
436                         return jQuery.event.trigger( type, data, this[0], false, fn );
437                 return undefined;
438         },
439
440         toggle: function() {
441                 // Save reference to arguments for access in closure
442                 var args = arguments;
443
444                 return this.click(function(event) {
445                         // Figure out which function to execute
446                         this.lastToggle = 0 == this.lastToggle ? 1 : 0;
447                         
448                         // Make sure that clicks stop
449                         event.preventDefault();
450                         
451                         // and execute the function
452                         return args[this.lastToggle].apply( this, arguments ) || false;
453                 });
454         },
455
456         hover: function(fnOver, fnOut) {
457                 return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut);
458         },
459         
460         ready: function(fn) {
461                 // Attach the listeners
462                 bindReady();
463
464                 // If the DOM is already ready
465                 if ( jQuery.isReady )
466                         // Execute the function immediately
467                         fn.call( document, jQuery );
468                         
469                 // Otherwise, remember the function for later
470                 else
471                         // Add the function to the wait list
472                         jQuery.readyList.push( function() { return fn.call(this, jQuery); } );
473         
474                 return this;
475         }
476 });
477
478 jQuery.extend({
479         isReady: false,
480         readyList: [],
481         // Handle when the DOM is ready
482         ready: function() {
483                 // Make sure that the DOM is not already loaded
484                 if ( !jQuery.isReady ) {
485                         // Remember that the DOM is ready
486                         jQuery.isReady = true;
487                         
488                         // If there are functions bound, to execute
489                         if ( jQuery.readyList ) {
490                                 // Execute all of them
491                                 jQuery.each( jQuery.readyList, function(){
492                                         this.apply( document );
493                                 });
494                                 
495                                 // Reset the list of functions
496                                 jQuery.readyList = null;
497                         }
498                 
499                         // Trigger any bound ready events
500                         jQuery(document).triggerHandler("ready");
501                 }
502         }
503 });
504
505 var readyBound = false;
506
507 function bindReady(){
508         if ( readyBound ) return;
509         readyBound = true;
510
511         // Mozilla, Opera (see further below for it) and webkit nightlies currently support this event
512         if ( document.addEventListener && !jQuery.browser.opera)
513                 // Use the handy event callback
514                 document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
515         
516         // If IE is used and is not in a frame
517         // Continually check to see if the document is ready
518         if ( jQuery.browser.msie && window == top ) (function(){
519                 if (jQuery.isReady) return;
520                 try {
521                         // If IE is used, use the trick by Diego Perini
522                         // http://javascript.nwbox.com/IEContentLoaded/
523                         document.documentElement.doScroll("left");
524                 } catch( error ) {
525                         setTimeout( arguments.callee, 0 );
526                         return;
527                 }
528                 // and execute any waiting functions
529                 jQuery.ready();
530         })();
531
532         if ( jQuery.browser.opera )
533                 document.addEventListener( "DOMContentLoaded", function () {
534                         if (jQuery.isReady) return;
535                         for (var i = 0; i < document.styleSheets.length; i++)
536                                 if (document.styleSheets[i].disabled) {
537                                         setTimeout( arguments.callee, 0 );
538                                         return;
539                                 }
540                         // and execute any waiting functions
541                         jQuery.ready();
542                 }, false);
543
544         if ( jQuery.browser.safari ) {
545                 var numStyles;
546                 (function(){
547                         if (jQuery.isReady) return;
548                         if ( document.readyState != "loaded" && document.readyState != "complete" ) {
549                                 setTimeout( arguments.callee, 0 );
550                                 return;
551                         }
552                         if ( numStyles === undefined )
553                                 numStyles = jQuery("style, link[rel=stylesheet]").length;
554                         if ( document.styleSheets.length != numStyles ) {
555                                 setTimeout( arguments.callee, 0 );
556                                 return;
557                         }
558                         // and execute any waiting functions
559                         jQuery.ready();
560                 })();
561         }
562
563         // A fallback to window.onload, that will always work
564         jQuery.event.add( window, "load", jQuery.ready );
565 }
566
567 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
568         "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + 
569         "submit,keydown,keypress,keyup,error").split(","), function(i, name){
570         
571         // Handle event binding
572         jQuery.fn[name] = function(fn){
573                 return fn ? this.bind(name, fn) : this.trigger(name);
574         };
575 });
576
577 // Checks if an event happened on an element within another element
578 // Used in jQuery.event.special.mouseenter and mouseleave handlers
579 var withinElement = function(event, elem) {
580         // Check if mouse(over|out) are still within the same parent element
581         var parent = event.relatedTarget;
582         // Traverse up the tree
583         while ( parent && parent != elem ) try { parent = parent.parentNode; } catch(error) { parent = elem; }
584         // Return true if we actually just moused on to a sub-element
585         return parent == elem;
586 };
587
588 // Prevent memory leaks in IE
589 // And prevent errors on refresh with events like mouseover in other browsers
590 // Window isn't included so as not to unbind existing unload events
591 jQuery(window).bind("unload", function() {
592         jQuery("*").add(document).unbind();
593 });