Fixed #1887 - trigger fails if extra is not a function
[jquery.git] / src / event.js
1 /*\r
2  * A number of helper functions used for managing events.\r
3  * Many of the ideas behind this code orignated from \r
4  * Dean Edwards' addEvent library.\r
5  */\r
6 jQuery.event = {\r
7 \r
8         // Bind an event to an element\r
9         // Original by Dean Edwards\r
10         add: function(elem, types, handler, data) {\r
11                 if ( elem.nodeType == 3 || elem.nodeType == 8 )\r
12                         return;\r
13 \r
14                 // For whatever reason, IE has trouble passing the window object\r
15                 // around, causing it to be cloned in the process\r
16                 if ( jQuery.browser.msie && elem.setInterval != undefined )\r
17                         elem = window;\r
18 \r
19                 // Make sure that the function being executed has a unique ID\r
20                 if ( !handler.guid )\r
21                         handler.guid = this.guid++;\r
22                         \r
23                 // if data is passed, bind to handler \r
24                 if( data != undefined ) { \r
25                         // Create temporary function pointer to original handler \r
26                         var fn = handler; \r
27 \r
28                         // Create unique handler function, wrapped around original handler \r
29                         handler = function() { \r
30                                 // Pass arguments and context to original handler \r
31                                 return fn.apply(this, arguments); \r
32                         };\r
33 \r
34                         // Store data in unique handler \r
35                         handler.data = data;\r
36 \r
37                         // Set the guid of unique handler to the same of original handler, so it can be removed \r
38                         handler.guid = fn.guid;\r
39                 }\r
40 \r
41                 // Init the element's event structure\r
42                 var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),\r
43                         handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){\r
44                                 // returned undefined or false\r
45                                 var val;\r
46 \r
47                                 // Handle the second event of a trigger and when\r
48                                 // an event is called after a page has unloaded\r
49                                 if ( typeof jQuery == "undefined" || jQuery.event.triggered )\r
50                                         return val;\r
51                 \r
52                                 val = jQuery.event.handle.apply(elem, arguments);\r
53                 \r
54                                 return val;\r
55                         });\r
56                         \r
57                         // Handle multiple events seperated by a space\r
58                         // jQuery(...).bind("mouseover mouseout", fn);\r
59                         jQuery.each(types.split(/\s+/), function(index, type) {\r
60                                 // Namespaced event handlers\r
61                                 var parts = type.split(".");\r
62                                 type = parts[0];\r
63                                 handler.type = parts[1];\r
64 \r
65                                 // Get the current list of functions bound to this event\r
66                                 var handlers = events[type];\r
67 \r
68                                 // Init the event handler queue\r
69                                 if (!handlers) {\r
70                                         handlers = events[type] = {};\r
71                 \r
72                                         // Check for a special event handler\r
73                                         // Only use addEventListener/attachEvent if the special\r
74                                         // events handler returns false\r
75                                         if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem) === false ) {\r
76                                                 // Bind the global event handler to the element\r
77                                                 if (elem.addEventListener)\r
78                                                         elem.addEventListener(type, handle, false);\r
79                                                 else if (elem.attachEvent)\r
80                                                         elem.attachEvent("on" + type, handle);\r
81                                         }\r
82                                 }\r
83 \r
84                                 // Add the function to the element's handler list\r
85                                 handlers[handler.guid] = handler;\r
86 \r
87                                 // Keep track of which events have been used, for global triggering\r
88                                 jQuery.event.global[type] = true;\r
89                         });\r
90         },\r
91 \r
92         guid: 1,\r
93         global: {},\r
94 \r
95         // Detach an event or set of events from an element\r
96         remove: function(elem, types, handler) {\r
97                 // don't do events on text and comment nodes\r
98                 if ( elem.nodeType == 3 || elem.nodeType == 8 )\r
99                         return;\r
100 \r
101                 var events = jQuery.data(elem, "events"), ret, index;\r
102 \r
103                 if ( events ) {\r
104                         // Unbind all events for the element\r
105                         if ( !types )\r
106                                 for ( var type in events )\r
107                                         this.remove( elem, type );\r
108                         else {\r
109                                 // types is actually an event object here\r
110                                 if ( types.type ) {\r
111                                         handler = types.handler;\r
112                                         types = types.type;\r
113                                 }\r
114                                 \r
115                                 // Handle multiple events seperated by a space\r
116                                 // jQuery(...).unbind("mouseover mouseout", fn);\r
117                                 jQuery.each(types.split(/\s+/), function(index, type){\r
118                                         // Namespaced event handlers\r
119                                         var parts = type.split(".");\r
120                                         type = parts[0];\r
121                                         \r
122                                         if ( events[type] ) {\r
123                                                 // remove the given handler for the given type\r
124                                                 if ( handler )\r
125                                                         delete events[type][handler.guid];\r
126                         \r
127                                                 // remove all handlers for the given type\r
128                                                 else\r
129                                                         for ( handler in events[type] )\r
130                                                                 // Handle the removal of namespaced events\r
131                                                                 if ( !parts[1] || events[type][handler].type == parts[1] )\r
132                                                                         delete events[type][handler];\r
133 \r
134                                                 // remove generic event handler if no more handlers exist\r
135                                                 for ( ret in events[type] ) break;\r
136                                                 if ( !ret ) {\r
137                                                         if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem) === false ) {\r
138                                                                 if (elem.removeEventListener)\r
139                                                                         elem.removeEventListener(type, jQuery.data(elem, "handle"), false);\r
140                                                                 else if (elem.detachEvent)\r
141                                                                         elem.detachEvent("on" + type, jQuery.data(elem, "handle"));\r
142                                                         }\r
143                                                         ret = null;\r
144                                                         delete events[type];\r
145                                                 }\r
146                                         }\r
147                                 });\r
148                         }\r
149 \r
150                         // Remove the expando if it's no longer used\r
151                         for ( ret in events ) break;\r
152                         if ( !ret ) {\r
153                                 jQuery.removeData( elem, "events" );\r
154                                 jQuery.removeData( elem, "handle" );\r
155                         }\r
156                 }\r
157         },\r
158 \r
159         trigger: function(type, data, elem, donative, extra) {\r
160                 // Clone the incoming data, if any\r
161                 data = jQuery.makeArray(data || []);\r
162 \r
163                 // Handle a global trigger\r
164                 if ( !elem ) {\r
165                         // Only trigger if we've ever bound an event for it\r
166                         if ( this.global[type] )\r
167                                 jQuery("*").add([window, document]).trigger(type, data);\r
168 \r
169                 // Handle triggering a single element\r
170                 } else {\r
171                         // don't do events on text and comment nodes\r
172                         if ( elem.nodeType == 3 || elem.nodeType == 8 )\r
173                                 return undefined;\r
174 \r
175                         var val, ret, fn = jQuery.isFunction( elem[ type ] || null ),\r
176                                 // Check to see if we need to provide a fake event, or not\r
177                                 event = !data[0] || !data[0].preventDefault;\r
178                         \r
179                         // Pass along a fake event\r
180                         if ( event )\r
181                                 data.unshift( this.fix({ type: type, target: elem }) );\r
182 \r
183                         // Enforce the right trigger type\r
184                         data[0].type = type;\r
185 \r
186                         // Trigger the event\r
187                         if ( jQuery.isFunction( jQuery.data(elem, "handle") ) )\r
188                                 val = jQuery.data(elem, "handle").apply( elem, data );\r
189 \r
190                         // Handle triggering native .onfoo handlers\r
191                         if ( !fn && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )\r
192                                 val = false;\r
193 \r
194                         // Extra functions don't get the custom event object\r
195                         if ( event )\r
196                                 data.shift();\r
197 \r
198                         // Handle triggering of extra function\r
199                         if ( extra && jQuery.isFunction( extra ) ) {\r
200                                 // call the extra function and tack the current return value on the end for possible inspection\r
201                                 var ret = extra.apply( elem, data.concat( val ) );\r
202                                 // if anything is returned, give it precedence and have it overwrite the previous value\r
203                                 if (ret !== undefined)\r
204                                         val = ret;\r
205                         }\r
206 \r
207                         // Trigger the native events (except for clicks on links)\r
208                         if ( fn && donative !== false && val !== false && !(jQuery.nodeName(elem, 'a') && type == "click") ) {\r
209                                 this.triggered = true;\r
210                                 try {\r
211                                         elem[ type ]();\r
212                                 // prevent IE from throwing an error for some hidden elements\r
213                                 } catch (e) {}\r
214                         }\r
215 \r
216                         this.triggered = false;\r
217                 }\r
218 \r
219                 return val;\r
220         },\r
221 \r
222         handle: function(event) {\r
223                 // returned undefined or false\r
224                 var val;\r
225 \r
226                 // Empty object is for triggered events with no data\r
227                 event = jQuery.event.fix( event || window.event || {} ); \r
228 \r
229                 // Namespaced event handlers\r
230                 var parts = event.type.split(".");\r
231                 event.type = parts[0];\r
232 \r
233                 var handlers = jQuery.data(this, "events") && jQuery.data(this, "events")[event.type], args = Array.prototype.slice.call( arguments, 1 );\r
234                 args.unshift( event );\r
235 \r
236                 for ( var j in handlers ) {\r
237                         var handler = handlers[j];\r
238                         // Pass in a reference to the handler function itself\r
239                         // So that we can later remove it\r
240                         args[0].handler = handler;\r
241                         args[0].data = handler.data;\r
242 \r
243                         // Filter the functions by class\r
244                         if ( !parts[1] || handler.type == parts[1] ) {\r
245                                 var ret = handler.apply( this, args );\r
246 \r
247                                 if ( val !== false )\r
248                                         val = ret;\r
249 \r
250                                 if ( ret === false ) {\r
251                                         event.preventDefault();\r
252                                         event.stopPropagation();\r
253                                 }\r
254                         }\r
255                 }\r
256 \r
257                 // Clean up added properties in IE to prevent memory leak\r
258                 if (jQuery.browser.msie)\r
259                         event.target = event.preventDefault = event.stopPropagation =\r
260                                 event.handler = event.data = null;\r
261 \r
262                 return val;\r
263         },\r
264 \r
265         fix: function(event) {\r
266                 // store a copy of the original event object \r
267                 // and clone to set read-only properties\r
268                 var originalEvent = event;\r
269                 event = jQuery.extend({}, originalEvent);\r
270                 \r
271                 // add preventDefault and stopPropagation since \r
272                 // they will not work on the clone\r
273                 event.preventDefault = function() {\r
274                         // if preventDefault exists run it on the original event\r
275                         if (originalEvent.preventDefault)\r
276                                 originalEvent.preventDefault();\r
277                         // otherwise set the returnValue property of the original event to false (IE)\r
278                         originalEvent.returnValue = false;\r
279                 };\r
280                 event.stopPropagation = function() {\r
281                         // if stopPropagation exists run it on the original event\r
282                         if (originalEvent.stopPropagation)\r
283                                 originalEvent.stopPropagation();\r
284                         // otherwise set the cancelBubble property of the original event to true (IE)\r
285                         originalEvent.cancelBubble = true;\r
286                 };\r
287                 \r
288                 // Fix target property, if necessary\r
289                 if ( !event.target )\r
290                         event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either\r
291                                 \r
292                 // check if target is a textnode (safari)\r
293                 if ( event.target.nodeType == 3 )\r
294                         event.target = originalEvent.target.parentNode;\r
295 \r
296                 // Add relatedTarget, if necessary\r
297                 if ( !event.relatedTarget && event.fromElement )\r
298                         event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;\r
299 \r
300                 // Calculate pageX/Y if missing and clientX/Y available\r
301                 if ( event.pageX == null && event.clientX != null ) {\r
302                         var doc = document.documentElement, body = document.body;\r
303                         event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);\r
304                         event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);\r
305                 }\r
306                         \r
307                 // Add which for key events\r
308                 if ( !event.which && (event.charCode || event.keyCode) )\r
309                         event.which = event.charCode || event.keyCode;\r
310                 \r
311                 // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)\r
312                 if ( !event.metaKey && event.ctrlKey )\r
313                         event.metaKey = event.ctrlKey;\r
314 \r
315                 // Add which for click: 1 == left; 2 == middle; 3 == right\r
316                 // Note: button is not normalized, so don't use it\r
317                 if ( !event.which && event.button )\r
318                         event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));\r
319                         \r
320                 return event;\r
321         },\r
322         \r
323         special: {\r
324                 ready: {\r
325                         setup: function() {\r
326                                 // Make sure the ready event is setup\r
327                                 bindReady();\r
328                                 return;\r
329                         },\r
330                         \r
331                         teardown: function() { return; }\r
332                 },\r
333                 \r
334                 mouseenter: {\r
335                         setup: function() {\r
336                                 if ( jQuery.browser.msie ) return false;\r
337                                 jQuery(this).bind("mouseover", jQuery.event.special.mouseenter.handler);\r
338                                 return true;\r
339                         },\r
340                 \r
341                         teardown: function() {\r
342                                 if ( jQuery.browser.msie ) return false;\r
343                                 jQuery(this).unbind("mouseover", jQuery.event.special.mouseenter.handler);\r
344                                 return true;\r
345                         },\r
346                         \r
347                         handler: function(event) {\r
348                                 // If we actually just moused on to a sub-element, ignore it\r
349                                 if ( withinElement(event, this) ) return true;\r
350                                 // Execute the right handlers by setting the event type to mouseenter\r
351                                 arguments[0].type = "mouseenter";\r
352                                 return jQuery.event.handle.apply(this, arguments);\r
353                         }\r
354                 },\r
355         \r
356                 mouseleave: {\r
357                         setup: function() {\r
358                                 if ( jQuery.browser.msie ) return false;\r
359                                 jQuery(this).bind("mouseout", jQuery.event.special.mouseleave.handler);\r
360                                 return true;\r
361                         },\r
362                 \r
363                         teardown: function() {\r
364                                 if ( jQuery.browser.msie ) return false;\r
365                                 jQuery(this).unbind("mouseout", jQuery.event.special.mouseleave.handler);\r
366                                 return true;\r
367                         },\r
368                         \r
369                         handler: function(event) {\r
370                                 // If we actually just moused on to a sub-element, ignore it\r
371                                 if ( withinElement(event, this) ) return true;\r
372                                 // Execute the right handlers by setting the event type to mouseleave\r
373                                 arguments[0].type = "mouseleave";\r
374                                 return jQuery.event.handle.apply(this, arguments);\r
375                         }\r
376                 }\r
377         }\r
378 };\r
379 \r
380 jQuery.fn.extend({\r
381         bind: function( type, data, fn ) {\r
382                 return type == "unload" ? this.one(type, data, fn) : this.each(function(){\r
383                         jQuery.event.add( this, type, fn || data, fn && data );\r
384                 });\r
385         },\r
386         \r
387         one: function( type, data, fn ) {\r
388                 return this.each(function(){\r
389                         jQuery.event.add( this, type, function(event) {\r
390                                 jQuery(this).unbind(event);\r
391                                 return (fn || data).apply( this, arguments);\r
392                         }, fn && data);\r
393                 });\r
394         },\r
395 \r
396         unbind: function( type, fn ) {\r
397                 return this.each(function(){\r
398                         jQuery.event.remove( this, type, fn );\r
399                 });\r
400         },\r
401 \r
402         trigger: function( type, data, fn ) {\r
403                 return this.each(function(){\r
404                         jQuery.event.trigger( type, data, this, true, fn );\r
405                 });\r
406         },\r
407 \r
408         triggerHandler: function( type, data, fn ) {\r
409                 if ( this[0] )\r
410                         return jQuery.event.trigger( type, data, this[0], false, fn );\r
411                 return undefined;\r
412         },\r
413 \r
414         toggle: function() {\r
415                 // Save reference to arguments for access in closure\r
416                 var args = arguments;\r
417 \r
418                 return this.click(function(event) {\r
419                         // Figure out which function to execute\r
420                         this.lastToggle = 0 == this.lastToggle ? 1 : 0;\r
421                         \r
422                         // Make sure that clicks stop\r
423                         event.preventDefault();\r
424                         \r
425                         // and execute the function\r
426                         return args[this.lastToggle].apply( this, arguments ) || false;\r
427                 });\r
428         },\r
429 \r
430         hover: function(fnOver, fnOut) {\r
431                 return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut);\r
432         },\r
433         \r
434         ready: function(fn) {\r
435                 // Attach the listeners\r
436                 bindReady();\r
437 \r
438                 // If the DOM is already ready\r
439                 if ( jQuery.isReady )\r
440                         // Execute the function immediately\r
441                         fn.call( document, jQuery );\r
442                         \r
443                 // Otherwise, remember the function for later\r
444                 else\r
445                         // Add the function to the wait list\r
446                         jQuery.readyList.push( function() { return fn.call(this, jQuery); } );\r
447         \r
448                 return this;\r
449         }\r
450 });\r
451 \r
452 jQuery.extend({\r
453         isReady: false,\r
454         readyList: [],\r
455         // Handle when the DOM is ready\r
456         ready: function() {\r
457                 // Make sure that the DOM is not already loaded\r
458                 if ( !jQuery.isReady ) {\r
459                         // Remember that the DOM is ready\r
460                         jQuery.isReady = true;\r
461                         \r
462                         // If there are functions bound, to execute\r
463                         if ( jQuery.readyList ) {\r
464                                 // Execute all of them\r
465                                 jQuery.each( jQuery.readyList, function(){\r
466                                         this.apply( document );\r
467                                 });\r
468                                 \r
469                                 // Reset the list of functions\r
470                                 jQuery.readyList = null;\r
471                         }\r
472                 \r
473                         // Trigger any bound ready events\r
474                         $(document).triggerHandler("ready");\r
475                 }\r
476         }\r
477 });\r
478 \r
479 var readyBound = false;\r
480 \r
481 function bindReady(){\r
482         if ( readyBound ) return;\r
483         readyBound = true;\r
484 \r
485         // Mozilla, Opera and webkit nightlies currently support this event\r
486         if ( document.addEventListener )\r
487                 // Use the handy event callback\r
488                 document.addEventListener( "DOMContentLoaded", jQuery.ready, false );\r
489         \r
490         // If Safari or IE is used\r
491         // Continually check to see if the document is ready\r
492         if (jQuery.browser.msie || jQuery.browser.safari ) (function(){\r
493                 try {\r
494                         // If IE is used, use the trick by Diego Perini\r
495                         // http://javascript.nwbox.com/IEContentLoaded/\r
496                         if ( jQuery.browser.msie || document.readyState != "loaded" && document.readyState != "complete" )\r
497                                 document.documentElement.doScroll("left");\r
498                 } catch( error ) {\r
499                         return setTimeout( arguments.callee, 0 );\r
500                 }\r
501 \r
502                 // and execute any waiting functions\r
503                 jQuery.ready();\r
504         })();\r
505 \r
506         // A fallback to window.onload, that will always work\r
507         jQuery.event.add( window, "load", jQuery.ready );\r
508 }\r
509 \r
510 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +\r
511         "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + \r
512         "submit,keydown,keypress,keyup,error").split(","), function(i, name){\r
513         \r
514         // Handle event binding\r
515         jQuery.fn[name] = function(fn){\r
516                 return fn ? this.bind(name, fn) : this.trigger(name);\r
517         };\r
518 });\r
519 \r
520 // Checks if an event happened on an element within another element\r
521 // Used in jQuery.event.special.mouseenter and mouseleave handlers\r
522 var withinElement = function(event, elem) {\r
523         // Check if mouse(over|out) are still within the same parent element\r
524         var parent = event.relatedTarget;\r
525         // Traverse up the tree\r
526         while ( parent && parent != elem ) try { parent = parent.parentNode } catch(error) { parent = elem; };\r
527         // Return true if we actually just moused on to a sub-element\r
528         return parent == elem;\r
529 };\r
530 \r
531 // Prevent memory leaks in IE\r
532 // And prevent errors on refresh with events like mouseover in other browsers\r
533 // Window isn't included so as not to unbind existing unload events\r
534 jQuery(window).bind("unload", function() {\r
535         jQuery("*").add(document).unbind();\r
536 });\r