jquery core: closes #2771
[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
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) === 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("*").add([window, document]).trigger(type, data);
172
173                 // Handle triggering a single element
174                 } else {
175                         // don't do events on text and comment nodes
176                         if ( elem.nodeType == 3 || elem.nodeType == 8 )
177                                 return undefined;
178
179                         var val, ret, fn = jQuery.isFunction( elem[ type ] || null ),
180                                 // Check to see if we need to provide a fake event, or not
181                                 event = !data[0] || !data[0].preventDefault;
182                         
183                         // Pass along a fake event
184                         if ( event ) {
185                                 data.unshift({ 
186                                         type: type, 
187                                         target: elem, 
188                                         preventDefault: function(){}, 
189                                         stopPropagation: function(){}, 
190                                         timeStamp: now()
191                                 });
192                                 data[0][expando] = true; // no need to fix fake event
193                         }
194
195                         // Enforce the right trigger type
196                         data[0].type = type;
197                         if ( exclusive )
198                                 data[0].exclusive = true;
199
200                         // Trigger the event, it is assumed that "handle" is a function
201                         var handle = jQuery.data(elem, "handle"); 
202                         if ( handle ) 
203                                 val = handle.apply( elem, data );
204
205                         // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
206                         if ( (!fn || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
207                                 val = false;
208
209                         // Extra functions don't get the custom event object
210                         if ( event )
211                                 data.shift();
212
213                         // Handle triggering of extra function
214                         if ( extra && jQuery.isFunction( extra ) ) {
215                                 // call the extra function and tack the current return value on the end for possible inspection
216                                 ret = extra.apply( elem, val == null ? data : data.concat( val ) );
217                                 // if anything is returned, give it precedence and have it overwrite the previous value
218                                 if (ret !== undefined)
219                                         val = ret;
220                         }
221
222                         // Trigger the native events (except for clicks on links)
223                         if ( fn && donative !== false && val !== false && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
224                                 this.triggered = true;
225                                 try {
226                                         elem[ type ]();
227                                 // prevent IE from throwing an error for some hidden elements
228                                 } catch (e) {}
229                         }
230
231                         this.triggered = false;
232                 }
233
234                 return val;
235         },
236
237         handle: function(event) {
238                 // returned undefined or false
239                 var val, ret, namespace, all, handlers;
240
241                 event = arguments[0] = jQuery.event.fix( event || window.event );
242
243                 // Namespaced event handlers
244                 namespace = event.type.split(".");
245                 event.type = namespace[0];
246                 namespace = namespace[1];
247                 all = !namespace && !event.exclusive; //cache this now, all = true means, any handler
248
249                 handlers = ( jQuery.data(this, "events") || {} )[event.type];
250
251                 for ( var j in handlers ) {
252                         var handler = handlers[j];
253
254                         // Filter the functions by class
255                         if ( all || handler.type == namespace ) {
256                                 // Pass in a reference to the handler function itself
257                                 // So that we can later remove it
258                                 event.handler = handler;
259                                 event.data = handler.data;
260                                 
261                                 ret = handler.apply( this, arguments );
262
263                                 if ( val !== false )
264                                         val = ret;
265
266                                 if ( ret === false ) {
267                                         event.preventDefault();
268                                         event.stopPropagation();
269                                 }
270                         }
271                 }
272
273                 return val;
274         },
275
276         fix: function(event) {
277                 if ( event[expando] == true ) 
278                         return event;
279                 
280                 // store a copy of the original event object 
281                 // and "clone" to set read-only properties
282                 var originalEvent = event;
283                 event = { originalEvent: originalEvent };
284                 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(" ");
285                 for ( var i=props.length; i; i-- )
286                         event[ props[i] ] = originalEvent[ props[i] ];
287                 
288                 // Mark it as fixed
289                 event[expando] = true;
290                 
291                 // add preventDefault and stopPropagation since 
292                 // they will not work on the clone
293                 event.preventDefault = function() {
294                         // if preventDefault exists run it on the original event
295                         if (originalEvent.preventDefault)
296                                 originalEvent.preventDefault();
297                         // otherwise set the returnValue property of the original event to false (IE)
298                         originalEvent.returnValue = false;
299                 };
300                 event.stopPropagation = function() {
301                         // if stopPropagation exists run it on the original event
302                         if (originalEvent.stopPropagation)
303                                 originalEvent.stopPropagation();
304                         // otherwise set the cancelBubble property of the original event to true (IE)
305                         originalEvent.cancelBubble = true;
306                 };
307                 
308                 // Fix timeStamp
309                 event.timeStamp = event.timeStamp || now();
310                 
311                 // Fix target property, if necessary
312                 if ( !event.target )
313                         event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
314                                 
315                 // check if target is a textnode (safari)
316                 if ( event.target.nodeType == 3 )
317                         event.target = event.target.parentNode;
318
319                 // Add relatedTarget, if necessary
320                 if ( !event.relatedTarget && event.fromElement )
321                         event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
322
323                 // Calculate pageX/Y if missing and clientX/Y available
324                 if ( event.pageX == null && event.clientX != null ) {
325                         var doc = document.documentElement, body = document.body;
326                         event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
327                         event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
328                 }
329                         
330                 // Add which for key events
331                 if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
332                         event.which = event.charCode || event.keyCode;
333                 
334                 // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
335                 if ( !event.metaKey && event.ctrlKey )
336                         event.metaKey = event.ctrlKey;
337
338                 // Add which for click: 1 == left; 2 == middle; 3 == right
339                 // Note: button is not normalized, so don't use it
340                 if ( !event.which && event.button )
341                         event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
342                         
343                 return event;
344         },
345         
346         proxy: function( fn, proxy ){
347                 // Set the guid of unique handler to the same of original handler, so it can be removed 
348                 proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
349                 return proxy;//so proxy can be declared as an argument
350         },
351         
352         special: {
353                 ready: {
354                         setup: function() {
355                                 // Make sure the ready event is setup
356                                 bindReady();
357                                 return;
358                         },
359                         
360                         teardown: function() { return; }
361                 },
362                 
363                 mouseenter: {
364                         setup: function() {
365                                 if ( jQuery.browser.msie ) return false;
366                                 jQuery(this).bind("mouseover", jQuery.event.special.mouseenter.handler);
367                                 return true;
368                         },
369                 
370                         teardown: function() {
371                                 if ( jQuery.browser.msie ) return false;
372                                 jQuery(this).unbind("mouseover", jQuery.event.special.mouseenter.handler);
373                                 return true;
374                         },
375                         
376                         handler: function(event) {
377                                 // If we actually just moused on to a sub-element, ignore it
378                                 if ( withinElement(event, this) ) return true;
379                                 // Execute the right handlers by setting the event type to mouseenter
380                                 event.type = "mouseenter";
381                                 return jQuery.event.handle.apply(this, arguments);
382                         }
383                 },
384         
385                 mouseleave: {
386                         setup: function() {
387                                 if ( jQuery.browser.msie ) return false;
388                                 jQuery(this).bind("mouseout", jQuery.event.special.mouseleave.handler);
389                                 return true;
390                         },
391                 
392                         teardown: function() {
393                                 if ( jQuery.browser.msie ) return false;
394                                 jQuery(this).unbind("mouseout", jQuery.event.special.mouseleave.handler);
395                                 return true;
396                         },
397                         
398                         handler: function(event) {
399                                 // If we actually just moused on to a sub-element, ignore it
400                                 if ( withinElement(event, this) ) return true;
401                                 // Execute the right handlers by setting the event type to mouseleave
402                                 event.type = "mouseleave";
403                                 return jQuery.event.handle.apply(this, arguments);
404                         }
405                 }
406         }
407 };
408
409 jQuery.fn.extend({
410         bind: function( type, data, fn ) {
411                 return type == "unload" ? this.one(type, data, fn) : this.each(function(){
412                         jQuery.event.add( this, type, fn || data, fn && data );
413                 });
414         },
415         
416         one: function( type, data, fn ) {
417                 var one = jQuery.event.proxy( fn || data, function(event) {
418                         jQuery(this).unbind(event, one);
419                         return (fn || data).apply( this, arguments );
420                 });
421                 return this.each(function(){
422                         jQuery.event.add( this, type, one, fn && data);
423                 });
424         },
425
426         unbind: function( type, fn ) {
427                 return this.each(function(){
428                         jQuery.event.remove( this, type, fn );
429                 });
430         },
431
432         trigger: function( type, data, fn ) {
433                 return this.each(function(){
434                         jQuery.event.trigger( type, data, this, true, fn );
435                 });
436         },
437
438         triggerHandler: function( type, data, fn ) {
439                 return this[0] && jQuery.event.trigger( type, data, this[0], false, fn );
440         },
441
442         toggle: function( fn ) {
443                 // Save reference to arguments for access in closure
444                 var args = arguments, i = 1;
445
446                 // link all the functions, so any of them can unbind this click handler
447                 while( i < args.length )
448                         jQuery.event.proxy( fn, args[i++] );
449
450                 return this.click( jQuery.event.proxy( fn, function(event) {
451                         // Figure out which function to execute
452                         this.lastToggle = ( this.lastToggle || 0 ) % i;
453                         
454                         // Make sure that clicks stop
455                         event.preventDefault();
456                         
457                         // and execute the function
458                         return args[ this.lastToggle++ ].apply( this, arguments ) || false;
459                 }));
460         },
461
462         hover: function(fnOver, fnOut) {
463                 return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut);
464         },
465         
466         ready: function(fn) {
467                 // Attach the listeners
468                 bindReady();
469
470                 // If the DOM is already ready
471                 if ( jQuery.isReady )
472                         // Execute the function immediately
473                         fn.call( document, jQuery );
474                         
475                 // Otherwise, remember the function for later
476                 else
477                         // Add the function to the wait list
478                         jQuery.readyList.push( function() { return fn.call(this, jQuery); } );
479         
480                 return this;
481         }
482 });
483
484 jQuery.extend({
485         isReady: false,
486         readyList: [],
487         // Handle when the DOM is ready
488         ready: function() {
489                 // Make sure that the DOM is not already loaded
490                 if ( !jQuery.isReady ) {
491                         // Remember that the DOM is ready
492                         jQuery.isReady = true;
493                         
494                         // If there are functions bound, to execute
495                         if ( jQuery.readyList ) {
496                                 // Execute all of them
497                                 jQuery.each( jQuery.readyList, function(){
498                                         this.apply( document );
499                                 });
500                                 
501                                 // Reset the list of functions
502                                 jQuery.readyList = null;
503                         }
504                 
505                         // Trigger any bound ready events
506                         jQuery(document).triggerHandler("ready");
507                 }
508         }
509 });
510
511 var readyBound = false;
512
513 function bindReady(){
514         if ( readyBound ) return;
515         readyBound = true;
516
517         // Mozilla, Opera (see further below for it) and webkit nightlies currently support this event
518         if ( document.addEventListener && !jQuery.browser.opera)
519                 // Use the handy event callback
520                 document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
521         
522         // If IE is used and is not in a frame
523         // Continually check to see if the document is ready
524         if ( jQuery.browser.msie && window == top ) (function(){
525                 if (jQuery.isReady) return;
526                 try {
527                         // If IE is used, use the trick by Diego Perini
528                         // http://javascript.nwbox.com/IEContentLoaded/
529                         document.documentElement.doScroll("left");
530                 } catch( error ) {
531                         setTimeout( arguments.callee, 0 );
532                         return;
533                 }
534                 // and execute any waiting functions
535                 jQuery.ready();
536         })();
537
538         if ( jQuery.browser.opera )
539                 document.addEventListener( "DOMContentLoaded", function () {
540                         if (jQuery.isReady) return;
541                         for (var i = 0; i < document.styleSheets.length; i++)
542                                 if (document.styleSheets[i].disabled) {
543                                         setTimeout( arguments.callee, 0 );
544                                         return;
545                                 }
546                         // and execute any waiting functions
547                         jQuery.ready();
548                 }, false);
549
550         if ( jQuery.browser.safari ) {
551                 var numStyles;
552                 (function(){
553                         if (jQuery.isReady) return;
554                         if ( document.readyState != "loaded" && document.readyState != "complete" ) {
555                                 setTimeout( arguments.callee, 0 );
556                                 return;
557                         }
558                         if ( numStyles === undefined )
559                                 numStyles = jQuery("style, link[rel=stylesheet]").length;
560                         if ( document.styleSheets.length != numStyles ) {
561                                 setTimeout( arguments.callee, 0 );
562                                 return;
563                         }
564                         // and execute any waiting functions
565                         jQuery.ready();
566                 })();
567         }
568
569         // A fallback to window.onload, that will always work
570         jQuery.event.add( window, "load", jQuery.ready );
571 }
572
573 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
574         "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + 
575         "submit,keydown,keypress,keyup,error").split(","), function(i, name){
576         
577         // Handle event binding
578         jQuery.fn[name] = function(fn){
579                 return fn ? this.bind(name, fn) : this.trigger(name);
580         };
581 });
582
583 // Checks if an event happened on an element within another element
584 // Used in jQuery.event.special.mouseenter and mouseleave handlers
585 var withinElement = function(event, elem) {
586         // Check if mouse(over|out) are still within the same parent element
587         var parent = event.relatedTarget;
588         // Traverse up the tree
589         while ( parent && parent != elem ) try { parent = parent.parentNode; } catch(error) { parent = elem; }
590         // Return true if we actually just moused on to a sub-element
591         return parent == elem;
592 };
593
594 // Prevent memory leaks in IE
595 // And prevent errors on refresh with events like mouseover in other browsers
596 // Window isn't included so as not to unbind existing unload events
597 jQuery(window).bind("unload", function() {
598         jQuery("*").add(document).unbind();
599 });