Fixes #7340. Use a single capturing handler to simulate bubbling focusin/focusout...
[jquery.git] / src / event.js
index 7fa5488..61c8a93 100644 (file)
@@ -7,9 +7,7 @@ var rnamespaces = /\.(.*)$/,
        rescape = /[^\w\s.|`]/g,
        fcleanup = function( nm ) {
                return nm.replace(rescape, "\\$&");
-       },
-       focusCounts = { focusin: 0, focusout: 0 },
-       eventKey = "events";
+       };
 
 /*
  * A number of helper functions used for managing events.
@@ -25,17 +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;
+                       return;
                }
 
                var handleObjIn, handleObj;
@@ -59,31 +62,18 @@ jQuery.event = {
                        return;
                }
 
-               var events = elemData[ eventKey ],
+               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[ eventKey ] = 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;
                        };
@@ -176,17 +166,12 @@ jQuery.event = {
 
                var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
                        elemData = jQuery.hasData( elem ) && jQuery._data( elem ),
-                       events = elemData && elemData[ eventKey ];
+                       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 ) {
                        handler = types.handler;
@@ -286,10 +271,7 @@ jQuery.event = {
                        delete elemData.events;
                        delete elemData.handle;
 
-                       if ( typeof elemData === "function" ) {
-                               jQuery.removeData( elem, eventKey, true );
-
-                       } else if ( jQuery.isEmptyObject( elemData ) ) {
+                       if ( jQuery.isEmptyObject( elemData ) ) {
                                jQuery.removeData( elem, undefined, true );
                        }
                }
@@ -330,7 +312,7 @@ jQuery.event = {
                                                // points to jQuery.expando
                                                var internalKey = jQuery.expando,
                                                        internalCache = this[ internalKey ];
-                                               if ( internalCache && internalCache.events && internalCache.events[type] ) {
+                                               if ( internalCache && internalCache.events && internalCache.events[ type ] ) {
                                                        jQuery.event.trigger( event, data, internalCache.handle.elem );
                                                }
                                        });
@@ -356,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" ) :
-                       (jQuery._data( elem, eventKey ) || {}).handle;
+               var handle = jQuery._data( elem, "handle" );
 
                if ( handle ) {
                        handle.apply( elem, data );
@@ -400,7 +380,7 @@ jQuery.event = {
                                                        target[ "on" + targetType ] = null;
                                                }
 
-                                               jQuery.event.triggered = true;
+                                               jQuery.event.triggered = event.type;
                                                target[ targetType ]();
                                        }
 
@@ -411,7 +391,7 @@ jQuery.event = {
                                        target[ "on" + targetType ] = old;
                                }
 
-                               jQuery.event.triggered = false;
+                               jQuery.event.triggered = undefined;
                        }
                }
        },
@@ -436,11 +416,7 @@ jQuery.event = {
 
                event.namespace = event.namespace || namespace_sort.join(".");
 
-               events = jQuery._data(this, eventKey);
-
-               if ( typeof events === "function" ) {
-                       events = events.events;
-               }
+               events = jQuery._data(this, "events");
 
                handlers = (events || {})[ event.type ];
 
@@ -682,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;
@@ -732,8 +714,7 @@ if ( !jQuery.support.submitBubbles ) {
                                                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 );
                                        }
                                });
 
@@ -742,8 +723,7 @@ if ( !jQuery.support.submitBubbles ) {
                                                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 );
                                        }
                                });
 
@@ -806,7 +786,7 @@ 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 );
                }
        };
 
@@ -820,7 +800,7 @@ if ( !jQuery.support.changeBubbles ) {
                                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 );
                                }
                        },
 
@@ -832,7 +812,7 @@ 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 );
                                }
                        },
 
@@ -871,30 +851,50 @@ if ( !jQuery.support.changeBubbles ) {
 }
 
 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() {
-                               if ( focusCounts[fix]++ === 0 ) {
+                               if ( attaches++ === 0 ) {
                                        document.addEventListener( orig, handler, true );
                                }
                        },
                        teardown: function() {
-                               if ( --focusCounts[fix] === 0 ) {
+                               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.trigger( e, null, e.target );
+                       e.originalEvent = {};
+                       jQuery.event.trigger( e, null, e.target );
+                       if ( e.isDefaultPrevented() ) {
+                               donor.preventDefault();
+                       }
                }
        });
 }
@@ -1079,11 +1079,7 @@ function liveHandler( event ) {
        var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret,
                elems = [],
                selectors = [],
-               events = jQuery._data( this, eventKey );
-
-       if ( typeof events === "function" ) {
-               events = events.events;
-       }
+               events = jQuery._data( this, "events" );
 
        // 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" ) {
@@ -1117,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;