Probably not the optimal solution, but tests pass.
[jquery.git] / src / event.js
index 232fa27..0f976a0 100644 (file)
@@ -57,7 +57,7 @@ jQuery.event = {
                        // Namespaced event handlers
                        var namespaces = type.split(".");
                        type = namespaces.shift();
-                       handler.type = namespaces.slice().sort().join(".");
+                       handler.type = namespaces.slice(0).sort().join(".");
 
                        // Get the current list of functions bound to this event
                        var handlers = events[ type ],
@@ -66,7 +66,7 @@ jQuery.event = {
                        if ( special.add ) {
                                var modifiedHandler = special.add.call( elem, handler, data, namespaces );
                                if ( modifiedHandler && jQuery.isFunction( modifiedHandler ) ) {
-                                       modifiedHandler.guid = handler.guid;
+                                       modifiedHandler.guid = modifiedHandler.guid || handler.guid;
                                        handler = modifiedHandler;
                                }
                        }
@@ -133,7 +133,7 @@ jQuery.event = {
                                        var namespaces = type.split(".");
                                        type = namespaces.shift();
                                        var all = !namespaces.length,
-                                               namespace = new RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)"),
+                                               namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join(".*\\.") + "(\\.|$)"),
                                                special = this.special[ type ] || {};
 
                                        if ( events[ type ] ) {
@@ -160,7 +160,7 @@ jQuery.event = {
                                                        break;
                                                }
                                                if ( !ret ) {
-                                                       if ( !this.special[ type ] || this.special[ type ].teardown.call( elem, namespaces ) === false ) {
+                                                       if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
                                                                if ( elem.removeEventListener ) {
                                                                        elem.removeEventListener( type, jQuery.data( elem, "handle" ), false );
                                                                } else if ( elem.detachEvent ) {
@@ -190,9 +190,10 @@ jQuery.event = {
        },
 
        // bubbling is internal
-       trigger: function( event, data, elem, bubbling ) {
+       trigger: function( event, data, elem /*, bubbling */ ) {
                // Event object or event type
-               var type = event.type || event;
+               var type = event.type || event,
+                       bubbling = arguments[3];
 
                if ( !bubbling ) {
                        event = typeof event === "object" ?
@@ -214,11 +215,11 @@ jQuery.event = {
                                event.stopPropagation();
                                // Only trigger if we've ever bound an event for it
                                if ( this.global[ type ] ) {
-                                       for ( var cached in jQuery.cache ) {
-                                               if ( cached.events && cached.events[ type ] ) {
-                                                       this.trigger( event, data, cached.handle.elem );
+                                       jQuery.each( jQuery.cache, function() {
+                                               if ( this.events && this.events[type] ) {
+                                                       jQuery.event.trigger( event, data, this.handle.elem );
                                                }
-                                       }
+                                       });
                                }
                        }
 
@@ -246,16 +247,22 @@ jQuery.event = {
                        handle.apply( elem, data );
                }
 
+               var nativeFn, nativeHandler;
+               try {
+                       nativeFn = elem[ type ];
+                       nativeHandler = elem[ "on" + type ];
+               // prevent IE from throwing an error for some elements with some event types, see #3533
+               } catch (e) {}
                // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
-               if ( (!elem[ type ] || (jQuery.nodeName(elem, 'a') && type === "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false ) {
+               if ( (!nativeFn || (jQuery.nodeName(elem, 'a') && type === "click")) && nativeHandler && nativeHandler.apply( elem, data ) === false ) {
                        event.result = false;
                }
 
                // Trigger the native events (except for clicks on links)
-               if ( !bubbling && elem[ type ] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type === "click") ) {
+               if ( !bubbling && nativeFn && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type === "click") ) {
                        this.triggered = true;
                        try {
-                               elem[ type ]();
+                               nativeFn();
                        // prevent IE from throwing an error for some hidden elements
                        } catch (e) {}
                }
@@ -284,7 +291,7 @@ jQuery.event = {
                // Cache this now, all = true means, any handler
                all = !namespaces.length && !event.exclusive;
 
-               var namespace = new RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
+               var namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join(".*\\.") + "(\\.|$)");
 
                handlers = ( jQuery.data(this, "events") || {} )[ event.type ];
 
@@ -351,8 +358,8 @@ jQuery.event = {
                // Calculate pageX/Y if missing and clientX/Y available
                if ( event.pageX == null && event.clientX != null ) {
                        var doc = document.documentElement, body = document.body;
-                       event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
-                       event.pageY = event.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc.clientTop  || 0);
+                       event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
+                       event.pageY = event.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
                }
 
                // Add which for key events
@@ -367,15 +374,20 @@ jQuery.event = {
 
                // Add which for click: 1 == left; 2 == middle; 3 == right
                // Note: button is not normalized, so don't use it
-               if ( !event.which && event.button ) {
+               if ( !event.which && event.button !== undefined ) {
                        event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
                }
 
                return event;
        },
 
-       proxy: function( fn, proxy ) {
-               proxy = proxy || function() { return fn.apply( this, arguments ); };
+       proxy: function( fn, proxy, thisObject ) {
+               if ( proxy !== undefined && !jQuery.isFunction( proxy ) ) {
+                       thisObject = proxy;
+                       proxy = undefined;
+               }
+               // FIXME: Should proxy be redefined to be applied with thisObject if defined?
+               proxy = proxy || function() { return fn.apply( thisObject !== undefined ? thisObject : this, arguments ); };
                // Set the guid of unique handler to the same of original handler, so it can be removed
                proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
                // So proxy can be declared as an argument
@@ -388,16 +400,28 @@ jQuery.event = {
                        setup: bindReady,
                        teardown: function() {}
                },
-               
+
                live: {
                        add: function( proxy, data, namespaces ) {
                                jQuery.extend( proxy, data || {} );
                                proxy.guid += data.selector + data.live;
-                               jQuery.event.add( this, data.live, liveHandler );
+                               jQuery.event.add( this, data.live, liveHandler, data );
                        },
-                       
-                       teardown: function( namespaces ) {
-                               jQuery.event.remove( this, namespaces[0], liveHandler );
+
+                       remove: function( namespaces ) {
+                               if ( namespaces.length ) {
+                                       var remove = 0, name = new RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)");
+
+                                       jQuery.each( (jQuery.data(this, "events").live || {}), function() {
+                                               if ( name.test(this.type) ) {
+                                                       remove++;
+                                               }
+                                       });
+
+                                       if ( remove < 1 ) {
+                                               jQuery.event.remove( this, namespaces[0], liveHandler );
+                                       }
+                               }
                        }
                }
        }
@@ -443,6 +467,7 @@ jQuery.Event.prototype = {
                if ( !e ) {
                        return;
                }
+               
                // if preventDefault exists run it on the original event
                if ( e.preventDefault ) {
                        e.preventDefault();
@@ -479,8 +504,11 @@ var withinElement = function( event ) {
        var parent = event.relatedTarget;
        // Traverse up the tree
        while ( parent && parent != this ) {
+               // Firefox sometimes assigns relatedTarget a XUL element
+               // which we cannot access the parentNode property of
                try { parent = parent.parentNode; }
-               catch(e) { parent = this; }
+               // assuming we've left the element since we most likely mousedover a xul element
+               catch(e) { break; }
        }
 
        if ( parent != this ) {
@@ -491,9 +519,10 @@ var withinElement = function( event ) {
        }
 };
 
+// Create mouseenter and mouseleave events
 jQuery.each({
-       mouseover: 'mouseenter',
-       mouseout: 'mouseleave'
+       mouseover: "mouseenter",
+       mouseout: "mouseleave"
 }, function( orig, fix ) {
        jQuery.event.special[ fix ] = {
                setup: function(){
@@ -505,24 +534,126 @@ jQuery.each({
        };
 });
 
+(function() {
+       
+       var event = jQuery.event,
+               special = event.special,
+               handle  = event.handle;
+
+       special.submit = {
+               setup: function(data, namespaces) {
+                       if(data.selector) {
+                               event.add(this, 'click.specialSubmit', function(e, eventData) {
+                                       if(jQuery(e.target).filter(":submit, :image").closest(data.selector).length) {
+                                               e.type = "submit";
+                                               return handle.call( this, e, eventData );
+                                       }
+                               });
+                               
+                               event.add(this, 'keypress.specialSubmit', function( e, eventData ) {
+                                       if(jQuery(e.target).filter(":text, :password").closest(data.selector).length) {
+                                               e.type = "submit";
+                                               return handle.call( this, e, eventData );
+                                       }
+                               });
+                       } else {
+                               return false;
+                       }
+               },
+               
+               remove: function(namespaces) {
+                       event.remove(this, 'click.specialSubmit');
+                       event.remove(this, 'keypress.specialSubmit');
+               }
+       };
+       
+})();
+
+// Create "bubbling" focus and blur events
+jQuery.each({
+       focus: "focusin",
+       blur: "focusout"
+}, function( orig, fix ){
+       var event = jQuery.event,
+               special = event.special,
+               handle = event.handle;
+       
+       function ieHandler() { 
+               arguments[0].type = orig;
+               return handle.apply(this, arguments);
+       }
+
+       special[orig] = {
+               setup:function() {
+                       if ( this.addEventListener )
+                               this.addEventListener( orig, handle, true );
+                       else
+                               jQuery.event.add( this, fix, ieHandler );
+               }, 
+               teardown:function() { 
+                       if ( this.removeEventListener )
+                               this.removeEventListener( orig, handle, true );
+                       else
+                               jQuery.event.remove( this, fix, ieHandler );
+               }
+       };
+});
+
 jQuery.fn.extend({
-       bind: function( type, data, fn ) {
-               return type === "unload" ? this.one(type, data, fn) : this.each(function() {
-                       jQuery.event.add( this, type, fn || data, fn && data );
+       // TODO: make bind(), unbind() and one() DRY!
+       bind: function( type, data, fn, thisObject ) {
+               // Handle object literals
+               if ( typeof type === "object" ) {
+                       for ( var key in type ) {
+                               this.bind(key, data, type[key], fn);
+                       }
+                       return this;
+               }
+               
+               if ( jQuery.isFunction( data ) ) {
+                       thisObject = fn;
+                       fn = data;
+                       data = undefined;
+               }
+               fn = thisObject === undefined ? fn : jQuery.event.proxy( fn, thisObject );
+               return type === "unload" ? this.one(type, data, fn, thisObject) : this.each(function() {
+                       jQuery.event.add( this, type, fn, data );
                });
        },
 
-       one: function( type, data, fn ) {
-               var one = jQuery.event.proxy( fn || data, function( event ) {
+       one: function( type, data, fn, thisObject ) {
+               // Handle object literals
+               if ( typeof type === "object" ) {
+                       for ( var key in type ) {
+                               this.one(key, data, type[key], fn);
+                       }
+                       return this;
+               }
+               
+               if ( jQuery.isFunction( data ) ) {
+                       thisObject = fn;
+                       fn = data;
+                       data = undefined;
+               }
+               fn = thisObject === undefined ? fn : jQuery.event.proxy( fn, thisObject );
+               var one = jQuery.event.proxy( fn, function( event ) {
                        jQuery( this ).unbind( event, one );
-                       return (fn || data).apply( this, arguments );
+                       return fn.apply( this, arguments );
                });
                return this.each(function() {
-                       jQuery.event.add( this, type, one, fn && data );
+                       jQuery.event.add( this, type, one, data );
                });
        },
 
        unbind: function( type, fn ) {
+               // Handle object literals
+               if ( typeof type === "object" && !type.preventDefault ) {
+                       for ( var key in type ) {
+                               this.unbind(key, type[key]);
+                       }
+                       return this;
+               }
+               
                return this.each(function() {
                        jQuery.event.remove( this, type, fn );
                });
@@ -566,7 +697,7 @@ jQuery.fn.extend({
        },
 
        hover: function( fnOver, fnOut ) {
-               return this.mouseenter( fnOver ).mouseleave( fnOut );
+               return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
        },
 
        ready: function( fn ) {
@@ -587,10 +718,17 @@ jQuery.fn.extend({
                return this;
        },
 
-       live: function( type, data, fn ) {
+       live: function( type, data, fn, thisObject ) {
+               if ( jQuery.isFunction( data ) ) {
+                       if ( fn !== undefined ) {
+                               thisObject = fn;
+                       }
+                       fn = data;
+                       data = undefined;
+               }
                jQuery( this.context ).bind( liveConvert( type, this.selector ), {
-                       data: fn && data, selector: this.selector, live: type
-               }, fn || data );
+                       data: data, selector: this.selector, live: type
+               }, fn, thisObject );
                return this;
        },
 
@@ -618,7 +756,7 @@ function liveHandler( event ) {
 
        jQuery.each(elems, function() {
                event.currentTarget = this.elem;
-               event.data = this.fn.data
+               event.data = this.fn.data;
                if ( this.fn.apply( this.elem, args ) === false ) {
                        return (stop = false);
                }
@@ -665,6 +803,12 @@ function bindReady() {
        if ( readyBound ) return;
        readyBound = true;
 
+       // Catch cases where $(document).ready() is called after the
+       // browser event has already occurred.
+       if ( document.readyState === "complete" ) {
+               return jQuery.ready();
+       }
+
        // Mozilla, Opera and webkit nightlies currently support this event
        if ( document.addEventListener ) {
                // Use the handy event callback
@@ -715,16 +859,15 @@ jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
 
        // Handle event binding
        jQuery.fn[ name ] = function( fn ) {
-               return fn ? this.bind (name, fn ) : this.trigger( name );
+               return fn ? this.bind( name, fn ) : this.trigger( name );
        };
 });
 
 // Prevent memory leaks in IE
-// And prevent errors on refresh with events like mouseover in other browsers
 // Window isn't included so as not to unbind existing unload events
 // More info:
 //  - http://isaacschlueter.com/2006/10/msie-memory-leaks/
-//  - https://bugzilla.mozilla.org/show_bug.cgi?id=252542
+/*@cc_on
 jQuery( window ).bind( 'unload', function() {
        for ( var id in jQuery.cache ) {
                // Skip the window
@@ -733,3 +876,4 @@ jQuery( window ).bind( 'unload', function() {
                }
        }
 });
+@*/