jquery event: from #2249, adding $.event.proxy to link event handlers, and implementi...
[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 = 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 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         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                                 arguments[0].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                                 arguments[0].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                 if ( this[0] )
443                         return jQuery.event.trigger( type, data, this[0], false, fn );
444                 return undefined;
445         },
446
447         toggle: function( fn ) {
448                 // Save reference to arguments for access in closure
449                 var args = arguments, i = 1;
450
451                 // link all the functions, so any of them can unbind this click handler
452                 while( i < args.length )
453                         jQuery.event.proxy( fn, args[i++] );
454
455                 return this.click( jQuery.event.proxy( fn, function(event) {
456                         // Figure out which function to execute
457                         this.lastToggle = ( this.lastToggle || 0 ) % i;
458                         
459                         // Make sure that clicks stop
460                         event.preventDefault();
461                         
462                         // and execute the function
463                         return args[ this.lastToggle++ ].apply( this, arguments ) || false;
464                 }));
465         },
466
467         hover: function(fnOver, fnOut) {
468                 return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut);
469         },
470         
471         ready: function(fn) {
472                 // Attach the listeners
473                 bindReady();
474
475                 // If the DOM is already ready
476                 if ( jQuery.isReady )
477                         // Execute the function immediately
478                         fn.call( document, jQuery );
479                         
480                 // Otherwise, remember the function for later
481                 else
482                         // Add the function to the wait list
483                         jQuery.readyList.push( function() { return fn.call(this, jQuery); } );
484         
485                 return this;
486         }
487 });
488
489 jQuery.extend({
490         isReady: false,
491         readyList: [],
492         // Handle when the DOM is ready
493         ready: function() {
494                 // Make sure that the DOM is not already loaded
495                 if ( !jQuery.isReady ) {
496                         // Remember that the DOM is ready
497                         jQuery.isReady = true;
498                         
499                         // If there are functions bound, to execute
500                         if ( jQuery.readyList ) {
501                                 // Execute all of them
502                                 jQuery.each( jQuery.readyList, function(){
503                                         this.apply( document );
504                                 });
505                                 
506                                 // Reset the list of functions
507                                 jQuery.readyList = null;
508                         }
509                 
510                         // Trigger any bound ready events
511                         jQuery(document).triggerHandler("ready");
512                 }
513         }
514 });
515
516 var readyBound = false;
517
518 function bindReady(){
519         if ( readyBound ) return;
520         readyBound = true;
521
522         // Mozilla, Opera (see further below for it) and webkit nightlies currently support this event
523         if ( document.addEventListener && !jQuery.browser.opera)
524                 // Use the handy event callback
525                 document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
526         
527         // If IE is used and is not in a frame
528         // Continually check to see if the document is ready
529         if ( jQuery.browser.msie && window == top ) (function(){
530                 if (jQuery.isReady) return;
531                 try {
532                         // If IE is used, use the trick by Diego Perini
533                         // http://javascript.nwbox.com/IEContentLoaded/
534                         document.documentElement.doScroll("left");
535                 } catch( error ) {
536                         setTimeout( arguments.callee, 0 );
537                         return;
538                 }
539                 // and execute any waiting functions
540                 jQuery.ready();
541         })();
542
543         if ( jQuery.browser.opera )
544                 document.addEventListener( "DOMContentLoaded", function () {
545                         if (jQuery.isReady) return;
546                         for (var i = 0; i < document.styleSheets.length; i++)
547                                 if (document.styleSheets[i].disabled) {
548                                         setTimeout( arguments.callee, 0 );
549                                         return;
550                                 }
551                         // and execute any waiting functions
552                         jQuery.ready();
553                 }, false);
554
555         if ( jQuery.browser.safari ) {
556                 var numStyles;
557                 (function(){
558                         if (jQuery.isReady) return;
559                         if ( document.readyState != "loaded" && document.readyState != "complete" ) {
560                                 setTimeout( arguments.callee, 0 );
561                                 return;
562                         }
563                         if ( numStyles === undefined )
564                                 numStyles = jQuery("style, link[rel=stylesheet]").length;
565                         if ( document.styleSheets.length != numStyles ) {
566                                 setTimeout( arguments.callee, 0 );
567                                 return;
568                         }
569                         // and execute any waiting functions
570                         jQuery.ready();
571                 })();
572         }
573
574         // A fallback to window.onload, that will always work
575         jQuery.event.add( window, "load", jQuery.ready );
576 }
577
578 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
579         "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + 
580         "submit,keydown,keypress,keyup,error").split(","), function(i, name){
581         
582         // Handle event binding
583         jQuery.fn[name] = function(fn){
584                 return fn ? this.bind(name, fn) : this.trigger(name);
585         };
586 });
587
588 // Checks if an event happened on an element within another element
589 // Used in jQuery.event.special.mouseenter and mouseleave handlers
590 var withinElement = function(event, elem) {
591         // Check if mouse(over|out) are still within the same parent element
592         var parent = event.relatedTarget;
593         // Traverse up the tree
594         while ( parent && parent != elem ) try { parent = parent.parentNode; } catch(error) { parent = elem; }
595         // Return true if we actually just moused on to a sub-element
596         return parent == elem;
597 };
598
599 // Prevent memory leaks in IE
600 // And prevent errors on refresh with events like mouseover in other browsers
601 // Window isn't included so as not to unbind existing unload events
602 jQuery(window).bind("unload", function() {
603         jQuery("*").add(document).unbind();
604 });