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