Fixes #7340. Use a single capturing handler to simulate bubbling focusin/focusout...
[jquery.git] / src / event.js
index c44d7d0..61c8a93 100644 (file)
@@ -23,14 +23,22 @@ jQuery.event = {
                        return;
                }
 
-               // For whatever reason, IE has trouble passing the window object
-               // around, causing it to be cloned in the process
-               if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) {
-                       elem = window;
+               // TODO :: Use a try/catch until it's safe to pull this out (likely 1.6)
+               // Minor release fix for bug #8018
+               try {
+                       // For whatever reason, IE has trouble passing the window object
+                       // around, causing it to be cloned in the process
+                       if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) {
+                               elem = window;
+                       }
                }
+               catch ( e ) {}
 
                if ( handler === false ) {
                        handler = returnFalse;
+               } else if ( !handler ) {
+                       // Fixes bug #7229. Fix recommended by jdalton
+                       return;
                }
 
                var handleObjIn, handleObj;
@@ -46,7 +54,7 @@ jQuery.event = {
                }
 
                // Init the element's event structure
-               var elemData = jQuery.data( elem );
+               var elemData = jQuery._data( elem );
 
                // If no elemData is found then we must be trying to bind to one of the
                // banned noData elements
@@ -56,29 +64,16 @@ jQuery.event = {
 
                var events = elemData.events,
                        eventHandle = elemData.handle;
-                       
-               if ( typeof events === "function" ) {
-                       // On plain objects events is a fn that holds the the data
-                       // which prevents this data from being JSON serialized
-                       // the function does not need to be called, it just contains the data
-                       eventHandle = events.handle;
-                       events = events.events;
-
-               } else if ( !events ) {
-                       if ( !elem.nodeType ) {
-                               // On plain objects, create a fn that acts as the holder
-                               // of the values to avoid JSON serialization of event data
-                               elemData.events = elemData = function(){};
-                       }
 
+               if ( !events ) {
                        elemData.events = events = {};
                }
 
                if ( !eventHandle ) {
-                       elemData.handle = eventHandle = function() {
+                       elemData.handle = eventHandle = function( e ) {
                                // Handle the second event of a trigger and when
                                // an event is called after a page has unloaded
-                               return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
+                               return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
                                        jQuery.event.handle.apply( eventHandle.elem, arguments ) :
                                        undefined;
                        };
@@ -136,9 +131,9 @@ jQuery.event = {
                                        }
                                }
                        }
-                       
-                       if ( special.add ) { 
-                               special.add.call( elem, handleObj ); 
+
+                       if ( special.add ) {
+                               special.add.call( elem, handleObj );
 
                                if ( !handleObj.handler.guid ) {
                                        handleObj.handler.guid = handler.guid;
@@ -170,17 +165,12 @@ jQuery.event = {
                }
 
                var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
-                       elemData = jQuery.data( elem ),
+                       elemData = jQuery.hasData( elem ) && jQuery._data( elem ),
                        events = elemData && elemData.events;
 
                if ( !elemData || !events ) {
                        return;
                }
-               
-               if ( typeof events === "function" ) {
-                       elemData = events;
-                       events = events.events;
-               }
 
                // types is actually an event object here
                if ( types && types.type ) {
@@ -214,7 +204,7 @@ jQuery.event = {
                                namespaces = type.split(".");
                                type = namespaces.shift();
 
-                               namespace = new RegExp("(^|\\.)" + 
+                               namespace = new RegExp("(^|\\.)" +
                                        jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)");
                        }
 
@@ -281,11 +271,8 @@ jQuery.event = {
                        delete elemData.events;
                        delete elemData.handle;
 
-                       if ( typeof elemData === "function" ) {
-                               delete elem.events;
-
-                       } else if ( jQuery.isEmptyObject( elemData ) ) {
-                               jQuery.removeData( elem );
+                       if ( jQuery.isEmptyObject( elemData ) ) {
+                               jQuery.removeData( elem, undefined, true );
                        }
                }
        },
@@ -317,9 +304,16 @@ jQuery.event = {
 
                                // Only trigger if we've ever bound an event for it
                                if ( jQuery.event.global[ type ] ) {
+                                       // XXX This code smells terrible. event.js should not be directly
+                                       // inspecting the data cache
                                        jQuery.each( jQuery.cache, function() {
-                                               if ( this.events && this.events[type] ) {
-                                                       jQuery.event.trigger( event, data, this.handle.elem );
+                                               // internalKey variable is just used to make it easier to find
+                                               // and potentially change this stuff later; currently it just
+                                               // points to jQuery.expando
+                                               var internalKey = jQuery.expando,
+                                                       internalCache = this[ internalKey ];
+                                               if ( internalCache && internalCache.events && internalCache.events[ type ] ) {
+                                                       jQuery.event.trigger( event, data, internalCache.handle.elem );
                                                }
                                        });
                                }
@@ -344,9 +338,7 @@ jQuery.event = {
                event.currentTarget = elem;
 
                // Trigger the event, it is assumed that "handle" is a function
-               var handle = elem.nodeType ?
-                       jQuery.data( elem, "handle" ) :
-                       elem.events && elem.events.handle;
+               var handle = jQuery._data( elem, "handle" );
 
                if ( handle ) {
                        handle.apply( elem, data );
@@ -370,11 +362,13 @@ jQuery.event = {
                        jQuery.event.trigger( event, data, parent, true );
 
                } else if ( !event.isDefaultPrevented() ) {
-                       var target = event.target, old, targetType = type.replace(rnamespaces, ""),
-                               isClick = jQuery.nodeName(target, "a") && targetType === "click",
+                       var old,
+                               target = event.target,
+                               targetType = type.replace( rnamespaces, "" ),
+                               isClick = jQuery.nodeName( target, "a" ) && targetType === "click",
                                special = jQuery.event.special[ targetType ] || {};
 
-                       if ( (!special._default || special._default.call( elem, event ) === false) && 
+                       if ( (!special._default || special._default.call( elem, event ) === false) &&
                                !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) {
 
                                try {
@@ -386,7 +380,7 @@ jQuery.event = {
                                                        target[ "on" + targetType ] = null;
                                                }
 
-                                               jQuery.event.triggered = true;
+                                               jQuery.event.triggered = event.type;
                                                target[ targetType ]();
                                        }
 
@@ -397,13 +391,15 @@ jQuery.event = {
                                        target[ "on" + targetType ] = old;
                                }
 
-                               jQuery.event.triggered = false;
+                               jQuery.event.triggered = undefined;
                        }
                }
        },
 
        handle: function( event ) {
-               var all, handlers, namespaces, namespace_sort = [], namespace_re, events, args = jQuery.makeArray( arguments );
+               var all, handlers, namespaces, namespace_re, events,
+                       namespace_sort = [],
+                       args = jQuery.makeArray( arguments );
 
                event = args[0] = jQuery.event.fix( event || window.event );
                event.currentTarget = this;
@@ -420,11 +416,7 @@ jQuery.event = {
 
                event.namespace = event.namespace || namespace_sort.join(".");
 
-               events = jQuery.data(this, "events");
-
-               if ( typeof events === "function" ) {
-                       events = events.events;
-               }
+               events = jQuery._data(this, "events");
 
                handlers = (events || {})[ event.type ];
 
@@ -442,7 +434,7 @@ jQuery.event = {
                                        event.handler = handleObj.handler;
                                        event.data = handleObj.data;
                                        event.handleObj = handleObj;
-       
+
                                        var ret = handleObj.handler.apply( this, args );
 
                                        if ( ret !== undefined ) {
@@ -482,7 +474,8 @@ jQuery.event = {
 
                // Fix target property, if necessary
                if ( !event.target ) {
-                       event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
+                       // Fixes #1925 where srcElement might not be defined either
+                       event.target = event.srcElement || document;
                }
 
                // check if target is a textnode (safari)
@@ -497,7 +490,9 @@ 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;
+                       var doc = document.documentElement,
+                               body = document.body;
+
                        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);
                }
@@ -538,7 +533,7 @@ jQuery.event = {
                        add: function( handleObj ) {
                                jQuery.event.add( this,
                                        liveConvert( handleObj.origType, handleObj.selector ),
-                                       jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); 
+                                       jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) );
                        },
 
                        remove: function( handleObj ) {
@@ -568,7 +563,7 @@ jQuery.removeEvent = document.removeEventListener ?
                if ( elem.removeEventListener ) {
                        elem.removeEventListener( type, handle, false );
                }
-       } : 
+       } :
        function( elem, type, handle ) {
                if ( elem.detachEvent ) {
                        elem.detachEvent( "on" + type, handle );
@@ -585,6 +580,12 @@ jQuery.Event = function( src ) {
        if ( src && src.type ) {
                this.originalEvent = src;
                this.type = src.type;
+
+               // Events bubbling up the document may have been marked as prevented
+               // by a handler lower down the tree; reflect the correct value.
+               this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false ||
+                       src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse;
+
        // Event type
        } else {
                this.type = src;
@@ -615,7 +616,7 @@ jQuery.Event.prototype = {
                if ( !e ) {
                        return;
                }
-               
+
                // if preventDefault exists run it on the original event
                if ( e.preventDefault ) {
                        e.preventDefault();
@@ -657,6 +658,12 @@ var withinElement = function( event ) {
        // Firefox sometimes assigns relatedTarget a XUL element
        // which we cannot access the parentNode property of
        try {
+
+               // Chrome does something similar, the parentNode property
+               // can be accessed but is null.
+               if ( parent !== document && !parent.parentNode ) {
+                       return;
+               }
                // Traverse up the tree
                while ( parent && parent !== this ) {
                        parent = parent.parentNode;
@@ -701,22 +708,22 @@ if ( !jQuery.support.submitBubbles ) {
 
        jQuery.event.special.submit = {
                setup: function( data, namespaces ) {
-                       if ( this.nodeName.toLowerCase() !== "form" ) {
+                       if ( this.nodeName && this.nodeName.toLowerCase() !== "form" ) {
                                jQuery.event.add(this, "click.specialSubmit", function( e ) {
-                                       var elem = e.target, type = elem.type;
+                                       var elem = e.target,
+                                               type = elem.type;
 
                                        if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) {
-                                               e.liveFired = undefined;
-                                               return trigger( "submit", this, arguments );
+                                               trigger( "submit", this, arguments );
                                        }
                                });
-        
+
                                jQuery.event.add(this, "keypress.specialSubmit", function( e ) {
-                                       var elem = e.target, type = elem.type;
+                                       var elem = e.target,
+                                               type = elem.type;
 
                                        if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) {
-                                               e.liveFired = undefined;
-                                               return trigger( "submit", this, arguments );
+                                               trigger( "submit", this, arguments );
                                        }
                                });
 
@@ -764,14 +771,14 @@ if ( !jQuery.support.changeBubbles ) {
                        return;
                }
 
-               data = jQuery.data( elem, "_change_data" );
+               data = jQuery._data( elem, "_change_data" );
                val = getVal(elem);
 
                // the current data will be also retrieved by beforeactivate
                if ( e.type !== "focusout" || elem.type !== "radio" ) {
-                       jQuery.data( elem, "_change_data", val );
+                       jQuery._data( elem, "_change_data", val );
                }
-               
+
                if ( data === undefined || val === data ) {
                        return;
                }
@@ -779,19 +786,21 @@ if ( !jQuery.support.changeBubbles ) {
                if ( data != null || val ) {
                        e.type = "change";
                        e.liveFired = undefined;
-                       return jQuery.event.trigger( e, arguments[1], elem );
+                       jQuery.event.trigger( e, arguments[1], elem );
                }
        };
 
        jQuery.event.special.change = {
                filters: {
-                       focusout: testChange, 
+                       focusout: testChange,
+
+                       beforedeactivate: testChange,
 
                        click: function( e ) {
                                var elem = e.target, type = elem.type;
 
                                if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) {
-                                       return testChange.call( this, e );
+                                       testChange.call( this, e );
                                }
                        },
 
@@ -803,16 +812,16 @@ if ( !jQuery.support.changeBubbles ) {
                                if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") ||
                                        (e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
                                        type === "select-multiple" ) {
-                                       return testChange.call( this, e );
+                                       testChange.call( this, e );
                                }
                        },
 
                        // Beforeactivate happens also before the previous element is blurred
                        // with this event you can't trigger a change event, but you can store
-                       // information/focus[in] is not needed anymore
+                       // information
                        beforeactivate: function( e ) {
                                var elem = e.target;
-                               jQuery.data( elem, "_change_data", getVal(elem) );
+                               jQuery._data( elem, "_change_data", getVal(elem) );
                        }
                },
 
@@ -836,29 +845,56 @@ if ( !jQuery.support.changeBubbles ) {
        };
 
        changeFilters = jQuery.event.special.change.filters;
+
+       // Handle when the input is .focus()'d
+       changeFilters.focus = changeFilters.beforeactivate;
 }
 
 function trigger( type, elem, args ) {
-       args[0].type = type;
-       return jQuery.event.handle.apply( elem, args );
+       // Piggyback on a donor event to simulate a different one.
+       // Fake originalEvent to avoid donor's stopPropagation, but if the
+       // simulated event prevents default then we do the same on the donor.
+       // Don't pass args or remember liveFired; they apply to the donor event.
+       var event = jQuery.extend( {}, args[ 0 ] );
+       event.type = type;
+       event.originalEvent = {};
+       event.liveFired = undefined;
+       jQuery.event.handle.call( elem, event );
+       if ( event.isDefaultPrevented() ) {
+               args[ 0 ].preventDefault();
+       }
 }
 
 // Create "bubbling" focus and blur events
 if ( document.addEventListener ) {
        jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+       
+               // Attach a single capturing handler while someone wants focusin/focusout
+               var attaches = 0;
+               
                jQuery.event.special[ fix ] = {
                        setup: function() {
-                               this.addEventListener( orig, handler, true );
-                       }, 
-                       teardown: function() { 
-                               this.removeEventListener( orig, handler, true );
+                               if ( attaches++ === 0 ) {
+                                       document.addEventListener( orig, handler, true );
+                               }
+                       },
+                       teardown: function() {
+                               if ( --attaches === 0 ) {
+                                       document.removeEventListener( orig, handler, true );
+                               }
                        }
                };
 
-               function handler( e ) { 
-                       e = jQuery.event.fix( e );
+               function handler( donor ) {
+                       // Donor event is always a native one; fix it and switch its type.
+                       // Let focusin/out handler cancel the donor focus/blur event.
+                       var e = jQuery.event.fix( donor );
                        e.type = fix;
-                       return jQuery.event.handle.call( this, e );
+                       e.originalEvent = {};
+                       jQuery.event.trigger( e, null, e.target );
+                       if ( e.isDefaultPrevented() ) {
+                               donor.preventDefault();
+                       }
                }
        });
 }
@@ -872,7 +908,7 @@ jQuery.each(["bind", "one"], function( i, name ) {
                        }
                        return this;
                }
-               
+
                if ( jQuery.isFunction( data ) || data === false ) {
                        fn = data;
                        data = undefined;
@@ -912,20 +948,20 @@ jQuery.fn.extend({
 
                return this;
        },
-       
+
        delegate: function( selector, types, data, fn ) {
                return this.live( types, data, fn, selector );
        },
-       
+
        undelegate: function( selector, types, fn ) {
                if ( arguments.length === 0 ) {
                                return this.unbind( "live" );
-               
+
                } else {
                        return this.die( types, null, fn, selector );
                }
        },
-       
+
        trigger: function( type, data ) {
                return this.each(function() {
                        jQuery.event.trigger( type, data, this );
@@ -944,7 +980,8 @@ jQuery.fn.extend({
 
        toggle: function( fn ) {
                // Save reference to arguments for access in closure
-               var args = arguments, i = 1;
+               var args = arguments,
+                       i = 1;
 
                // link all the functions, so any of them can unbind this click handler
                while ( i < args.length ) {
@@ -953,8 +990,8 @@ jQuery.fn.extend({
 
                return this.click( jQuery.proxy( fn, function( event ) {
                        // Figure out which function to execute
-                       var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i;
-                       jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+                       var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+                       jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
 
                        // Make sure that clicks stop
                        event.preventDefault();
@@ -981,12 +1018,12 @@ jQuery.each(["live", "die"], function( i, name ) {
                var type, i = 0, match, namespaces, preType,
                        selector = origSelector || this.selector,
                        context = origSelector ? this : jQuery( this.context );
-               
+
                if ( typeof types === "object" && !types.preventDefault ) {
                        for ( var key in types ) {
                                context[ name ]( key, data, types[key], selector );
                        }
-                       
+
                        return this;
                }
 
@@ -1033,22 +1070,19 @@ jQuery.each(["live", "die"], function( i, name ) {
                                context.unbind( "live." + liveConvert( type, selector ), fn );
                        }
                }
-               
+
                return this;
        };
 });
 
 function liveHandler( event ) {
-       var stop, maxLevel, elems = [], selectors = [],
-               related, match, handleObj, elem, j, i, l, data, close, namespace, ret,
-               events = jQuery.data( this, "events" );
-
-       if ( typeof events === "function" ) {
-               events = events.events;
-       }
+       var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret,
+               elems = [],
+               selectors = [],
+               events = jQuery._data( this, "events" );
 
-       // Make sure we avoid non-left-click bubbling in Firefox (#3861)
-       if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) {
+       // Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911)
+       if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) {
                return;
        }
 
@@ -1079,7 +1113,7 @@ function liveHandler( event ) {
                for ( j = 0; j < live.length; j++ ) {
                        handleObj = live[j];
 
-                       if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) ) {
+                       if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) && !close.elem.disabled ) {
                                elem = close.elem;
                                related = null;
 
@@ -1115,6 +1149,9 @@ function liveHandler( event ) {
                        if ( ret === false ) {
                                stop = false;
                        }
+                       if ( event.isImmediatePropagationStopped() ) {
+                               break;
+                       }
                }
        }
 
@@ -1146,21 +1183,4 @@ jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblcl
        }
 });
 
-// Prevent memory leaks in IE
-// Window isn't included so as not to unbind existing unload events
-// More info:
-//  - http://isaacschlueter.com/2006/10/msie-memory-leaks/
-if ( window.attachEvent && !window.addEventListener ) {
-       window.attachEvent("onunload", function() {
-               for ( var id in jQuery.cache ) {
-                       if ( jQuery.cache[ id ].handle ) {
-                               // Try/Catch is to handle iframes being unloaded, see #4280
-                               try {
-                                       jQuery.event.remove( jQuery.cache[ id ].handle.elem );
-                               } catch(e) {}
-                       }
-               }
-       });
-}
-
 })( jQuery );