Fixed the attribution for the DOM Ready fix - the previously used changes weren't...
[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(element, type, handler, data) {
11                 // For whatever reason, IE has trouble passing the window object
12                 // around, causing it to be cloned in the process
13                 if ( jQuery.browser.msie && element.setInterval != undefined )
14                         element = window;
15
16                 // Make sure that the function being executed has a unique ID
17                 if ( !handler.guid )
18                         handler.guid = this.guid++;
19                         
20                 // if data is passed, bind to handler 
21                 if( data != undefined ) { 
22                         // Create temporary function pointer to original handler 
23                         var fn = handler; 
24
25                         // Create unique handler function, wrapped around original handler 
26                         handler = function() { 
27                                 // Pass arguments and context to original handler 
28                                 return fn.apply(this, arguments); 
29                         };
30
31                         // Store data in unique handler 
32                         handler.data = data;
33
34                         // Set the guid of unique handler to the same of original handler, so it can be removed 
35                         handler.guid = fn.guid;
36                 }
37
38                 // Namespaced event handlers
39                 var parts = type.split(".");
40                 type = parts[0];
41                 handler.type = parts[1];
42
43                 // Init the element's event structure
44                 var events = jQuery.data(element, "events") || jQuery.data(element, "events", {});
45                 
46                 var handle = jQuery.data(element, "handle", function(){
47                         // returned undefined or false
48                         var val;
49
50                         // Handle the second event of a trigger and when
51                         // an event is called after a page has unloaded
52                         if ( typeof jQuery == "undefined" || jQuery.event.triggered )
53                                 return val;
54                         
55                         val = jQuery.event.handle.apply(element, arguments);
56                         
57                         return val;
58                 });
59
60                 // Get the current list of functions bound to this event
61                 var handlers = events[type];
62
63                 // Init the event handler queue
64                 if (!handlers) {
65                         handlers = events[type] = {};   
66                         
67                         // And bind the global event handler to the element
68                         if (element.addEventListener)
69                                 element.addEventListener(type, handle, false);
70                         else
71                                 element.attachEvent("on" + type, handle);
72                 }
73
74                 // Add the function to the element's handler list
75                 handlers[handler.guid] = handler;
76
77                 // Keep track of which events have been used, for global triggering
78                 this.global[type] = true;
79         },
80
81         guid: 1,
82         global: {},
83
84         // Detach an event or set of events from an element
85         remove: function(element, type, handler) {
86                 var events = jQuery.data(element, "events"), ret, index;
87
88                 // Namespaced event handlers
89                 if ( typeof type == "string" ) {
90                         var parts = type.split(".");
91                         type = parts[0];
92                 }
93
94                 if ( events ) {
95                         // type is actually an event object here
96                         if ( type && type.type ) {
97                                 handler = type.handler;
98                                 type = type.type;
99                         }
100                         
101                         if ( !type ) {
102                                 for ( type in events )
103                                         this.remove( element, type );
104
105                         } else if ( events[type] ) {
106                                 // remove the given handler for the given type
107                                 if ( handler )
108                                         delete events[type][handler.guid];
109                                 
110                                 // remove all handlers for the given type
111                                 else
112                                         for ( handler in events[type] )
113                                                 // Handle the removal of namespaced events
114                                                 if ( !parts[1] || events[type][handler].type == parts[1] )
115                                                         delete events[type][handler];
116
117                                 // remove generic event handler if no more handlers exist
118                                 for ( ret in events[type] ) break;
119                                 if ( !ret ) {
120                                         if (element.removeEventListener)
121                                                 element.removeEventListener(type, jQuery.data(element, "handle"), false);
122                                         else
123                                                 element.detachEvent("on" + type, jQuery.data(element, "handle"));
124                                         ret = null;
125                                         delete events[type];
126                                 }
127                         }
128
129                         // Remove the expando if it's no longer used
130                         for ( ret in events ) break;
131                         if ( !ret ) {
132                                 jQuery.removeData( element, "events" );
133                                 jQuery.removeData( element, "handle" );
134                         }
135                 }
136         },
137
138         trigger: function(type, data, element, donative, extra) {
139                 // Clone the incoming data, if any
140                 data = jQuery.makeArray(data || []);
141
142                 // Handle a global trigger
143                 if ( !element ) {
144                         // Only trigger if we've ever bound an event for it
145                         if ( this.global[type] )
146                                 jQuery("*").add([window, document]).trigger(type, data);
147
148                 // Handle triggering a single element
149                 } else {
150                         var val, ret, fn = jQuery.isFunction( element[ type ] || null ),
151                                 // Check to see if we need to provide a fake event, or not
152                                 evt = !data[0] || !data[0].preventDefault;
153                         
154                         // Pass along a fake event
155                         if ( evt )
156                                 data.unshift( this.fix({ type: type, target: element }) );
157
158                         // Enforce the right trigger type
159                         data[0].type = type;
160
161                         // Trigger the event
162                         if ( jQuery.isFunction( jQuery.data(element, "handle") ) )
163                                 val = jQuery.data(element, "handle").apply( element, data );
164
165                         // Handle triggering native .onfoo handlers
166                         if ( !fn && element["on"+type] && element["on"+type].apply( element, data ) === false )
167                                 val = false;
168
169                         // Extra functions don't get the custom event object
170                         if ( evt )
171                                 data.shift();
172
173                         // Handle triggering of extra function
174                         if ( extra && extra.apply( element, data ) === false )
175                                 val = false;
176
177                         // Trigger the native events (except for clicks on links)
178                         if ( fn && donative !== false && val !== false && !(jQuery.nodeName(element, 'a') && type == "click") ) {
179                                 this.triggered = true;
180                                 element[ type ]();
181                         }
182
183                         this.triggered = false;
184                 }
185
186                 return val;
187         },
188
189         handle: function(event) {
190                 // returned undefined or false
191                 var val;
192
193                 // Empty object is for triggered events with no data
194                 event = jQuery.event.fix( event || window.event || {} ); 
195
196                 // Namespaced event handlers
197                 var parts = event.type.split(".");
198                 event.type = parts[0];
199
200                 var c = jQuery.data(this, "events") && jQuery.data(this, "events")[event.type], args = Array.prototype.slice.call( arguments, 1 );
201                 args.unshift( event );
202
203                 for ( var j in c ) {
204                         // Pass in a reference to the handler function itself
205                         // So that we can later remove it
206                         args[0].handler = c[j];
207                         args[0].data = c[j].data;
208
209                         // Filter the functions by class
210                         if ( !parts[1] || c[j].type == parts[1] ) {
211                                 var tmp = c[j].apply( this, args );
212
213                                 if ( val !== false )
214                                         val = tmp;
215
216                                 if ( tmp === false ) {
217                                         event.preventDefault();
218                                         event.stopPropagation();
219                                 }
220                         }
221                 }
222
223                 // Clean up added properties in IE to prevent memory leak
224                 if (jQuery.browser.msie)
225                         event.target = event.preventDefault = event.stopPropagation =
226                                 event.handler = event.data = null;
227
228                 return val;
229         },
230
231         fix: function(event) {
232                 // store a copy of the original event object 
233                 // and clone to set read-only properties
234                 var originalEvent = event;
235                 event = jQuery.extend({}, originalEvent);
236                 
237                 // add preventDefault and stopPropagation since 
238                 // they will not work on the clone
239                 event.preventDefault = function() {
240                         // if preventDefault exists run it on the original event
241                         if (originalEvent.preventDefault)
242                                 originalEvent.preventDefault();
243                         // otherwise set the returnValue property of the original event to false (IE)
244                         originalEvent.returnValue = false;
245                 };
246                 event.stopPropagation = function() {
247                         // if stopPropagation exists run it on the original event
248                         if (originalEvent.stopPropagation)
249                                 originalEvent.stopPropagation();
250                         // otherwise set the cancelBubble property of the original event to true (IE)
251                         originalEvent.cancelBubble = true;
252                 };
253                 
254                 // Fix target property, if necessary
255                 if ( !event.target && event.srcElement )
256                         event.target = event.srcElement;
257                                 
258                 // check if target is a textnode (safari)
259                 if (jQuery.browser.safari && event.target.nodeType == 3)
260                         event.target = originalEvent.target.parentNode;
261
262                 // Add relatedTarget, if necessary
263                 if ( !event.relatedTarget && event.fromElement )
264                         event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
265
266                 // Calculate pageX/Y if missing and clientX/Y available
267                 if ( event.pageX == null && event.clientX != null ) {
268                         var e = document.documentElement, b = document.body;
269                         event.pageX = event.clientX + (e && e.scrollLeft || b && b.scrollLeft || 0);
270                         event.pageY = event.clientY + (e && e.scrollTop || b && b.scrollTop || 0);
271                 }
272                         
273                 // Add which for key events
274                 if ( !event.which && (event.charCode || event.keyCode) )
275                         event.which = event.charCode || event.keyCode;
276                 
277                 // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
278                 if ( !event.metaKey && event.ctrlKey )
279                         event.metaKey = event.ctrlKey;
280
281                 // Add which for click: 1 == left; 2 == middle; 3 == right
282                 // Note: button is not normalized, so don't use it
283                 if ( !event.which && event.button )
284                         event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
285                         
286                 return event;
287         }
288 };
289
290 jQuery.fn.extend({
291         bind: function( type, data, fn ) {
292                 return type == "unload" ? this.one(type, data, fn) : this.each(function(){
293                         jQuery.event.add( this, type, fn || data, fn && data );
294                 });
295         },
296         
297         one: function( type, data, fn ) {
298                 return this.each(function(){
299                         jQuery.event.add( this, type, function(event) {
300                                 jQuery(this).unbind(event);
301                                 return (fn || data).apply( this, arguments);
302                         }, fn && data);
303                 });
304         },
305
306         unbind: function( type, fn ) {
307                 return this.each(function(){
308                         jQuery.event.remove( this, type, fn );
309                 });
310         },
311
312         trigger: function( type, data, fn ) {
313                 return this.each(function(){
314                         jQuery.event.trigger( type, data, this, true, fn );
315                 });
316         },
317
318         triggerHandler: function( type, data, fn ) {
319                 if ( this[0] )
320                         return jQuery.event.trigger( type, data, this[0], false, fn );
321         },
322
323         toggle: function() {
324                 // Save reference to arguments for access in closure
325                 var a = arguments;
326
327                 return this.click(function(e) {
328                         // Figure out which function to execute
329                         this.lastToggle = 0 == this.lastToggle ? 1 : 0;
330                         
331                         // Make sure that clicks stop
332                         e.preventDefault();
333                         
334                         // and execute the function
335                         return a[this.lastToggle].apply( this, [e] ) || false;
336                 });
337         },
338
339         hover: function(f,g) {
340                 
341                 // A private function for handling mouse 'hovering'
342                 function handleHover(e) {
343                         // Check if mouse(over|out) are still within the same parent element
344                         var p = e.relatedTarget;
345         
346                         // Traverse up the tree
347                         while ( p && p != this ) try { p = p.parentNode; } catch(e) { p = this; };
348                         
349                         // If we actually just moused on to a sub-element, ignore it
350                         if ( p == this ) return false;
351                         
352                         // Execute the right function
353                         return (e.type == "mouseover" ? f : g).apply(this, [e]);
354                 }
355                 
356                 // Bind the function to the two event listeners
357                 return this.mouseover(handleHover).mouseout(handleHover);
358         },
359         
360         ready: function(f) {
361                 // Attach the listeners
362                 bindReady();
363
364                 // If the DOM is already ready
365                 if ( jQuery.isReady )
366                         // Execute the function immediately
367                         f.apply( document, [jQuery] );
368                         
369                 // Otherwise, remember the function for later
370                 else
371                         // Add the function to the wait list
372                         jQuery.readyList.push( function() { return f.apply(this, [jQuery]); } );
373         
374                 return this;
375         }
376 });
377
378 jQuery.extend({
379         /*
380          * All the code that makes DOM Ready work nicely.
381          */
382         isReady: false,
383         readyList: [],
384         
385         // Handle when the DOM is ready
386         ready: function() {
387                 // Make sure that the DOM is not already loaded
388                 if ( !jQuery.isReady ) {
389                         // Remember that the DOM is ready
390                         jQuery.isReady = true;
391                         
392                         // If there are functions bound, to execute
393                         if ( jQuery.readyList ) {
394                                 // Execute all of them
395                                 jQuery.each( jQuery.readyList, function(){
396                                         this.apply( document );
397                                 });
398                                 
399                                 // Reset the list of functions
400                                 jQuery.readyList = null;
401                         }
402                         // Remove event listener to avoid memory leak
403                         if ( jQuery.browser.mozilla || jQuery.browser.opera )
404                                 document.removeEventListener( "DOMContentLoaded", jQuery.ready, false );
405                 }
406         }
407 });
408
409
410 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
411         "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + 
412         "submit,keydown,keypress,keyup,error").split(","), function(i,o){
413         
414         // Handle event binding
415         jQuery.fn[o] = function(f){
416                 return f ? this.bind(o, f) : this.trigger(o);
417         };
418 });
419
420 var readyBound = false;
421
422 function bindReady(){
423         if ( readyBound ) return;
424         readyBound = true;
425
426         // If Mozilla is used
427         if ( jQuery.browser.mozilla || jQuery.browser.opera )
428                 // Use the handy event callback
429                 document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
430         
431         // If Safari or IE is used
432         else
433                 // Continually check to see if the document is ready
434                 (function timer() {
435                         try {
436                                 // If IE is used, use the trick by Diego Perini
437                                 // http://javascript.nwbox.com/IEContentLoaded/
438                                 if ( jQuery.browser.msie || document.readyState != "loaded" && document.readyState != "complete" )
439                                         document.documentElement.doScroll("left");
440         
441                                 // and execute any waiting functions
442                                 jQuery.ready();
443                         } catch( error ) {
444                                 setTimeout( timer, 0 );
445                         }
446                 })();
447
448         // A fallback to window.onload, that will always work
449         jQuery.event.add( window, "load", jQuery.ready );
450 }