Make sure that focusin/focusout bubbles in non-IE browsers.
[jquery.git] / src / event.js
index 1fec18c..d830c4d 100644 (file)
@@ -1,11 +1,14 @@
 (function( jQuery ) {
 
 var rnamespaces = /\.(.*)$/,
+       rformElems = /^(?:textarea|input|select)$/i,
+       rperiod = /\./g,
+       rspace = / /g,
+       rescape = /[^\w\s.|`]/g,
        fcleanup = function( nm ) {
-               return nm.replace(/[^\w\s\.\|`]/g, function( ch ) {
-                       return "\\" + ch;
-               });
-       };
+               return nm.replace(rescape, "\\$&");
+       },
+       focusCounts = { focusin: 0, focusout: 0 };
 
 /*
  * A number of helper functions used for managing events.
@@ -23,7 +26,7 @@ jQuery.event = {
 
                // For whatever reason, IE has trouble passing the window object
                // around, causing it to be cloned in the process
-               if ( elem.setInterval && ( elem !== window && !elem.frameElement ) ) {
+               if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) {
                        elem = window;
                }
 
@@ -52,8 +55,28 @@ jQuery.event = {
                        return;
                }
 
-               var events = elemData.events = elemData.events || {},
+               // Use a key less likely to result in collisions for plain JS objects.
+               // Fixes bug #7150.
+               var eventKey = elem.nodeType ? "events" : "__events__",
+                       events = elemData[ eventKey ],
                        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(){};
+                       }
+
+                       elemData.events = events = {};
+               }
 
                if ( !eventHandle ) {
                        elemData.handle = eventHandle = function() {
@@ -151,12 +174,18 @@ jQuery.event = {
                }
 
                var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
+                       eventKey = elem.nodeType ? "events" : "__events__",
                        elemData = jQuery.data( elem ),
-                       events = elemData && elemData.events;
+                       events = elemData && elemData[ eventKey ];
 
                if ( !elemData || !events ) {
                        return;
                }
+               
+               if ( typeof events === "function" ) {
+                       elemData = events;
+                       events = events.events;
+               }
 
                // types is actually an event object here
                if ( types && types.type ) {
@@ -257,7 +286,10 @@ jQuery.event = {
                        delete elemData.events;
                        delete elemData.handle;
 
-                       if ( jQuery.isEmptyObject( elemData ) ) {
+                       if ( typeof elemData === "function" ) {
+                               jQuery.removeData( elem, eventKey );
+
+                       } else if ( jQuery.isEmptyObject( elemData ) ) {
                                jQuery.removeData( elem );
                        }
                }
@@ -317,7 +349,10 @@ jQuery.event = {
                event.currentTarget = elem;
 
                // Trigger the event, it is assumed that "handle" is a function
-               var handle = jQuery.data( elem, "handle" );
+               var handle = elem.nodeType ?
+                       jQuery.data( elem, "handle" ) :
+                       (jQuery.data( elem, "__events__" ) || {}).handle;
+
                if ( handle ) {
                        handle.apply( elem, data );
                }
@@ -329,6 +364,7 @@ jQuery.event = {
                        if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) {
                                if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) {
                                        event.result = false;
+                                       event.preventDefault();
                                }
                        }
 
@@ -339,7 +375,7 @@ jQuery.event = {
                        jQuery.event.trigger( event, data, parent, true );
 
                } else if ( !event.isDefaultPrevented() ) {
-                       var target = event.target, old, targetType = type.replace(/\..*$/, ""),
+                       var target = event.target, old, targetType = type.replace(rnamespaces, ""),
                                isClick = jQuery.nodeName(target, "a") && targetType === "click",
                                special = jQuery.event.special[ targetType ] || {};
 
@@ -389,7 +425,12 @@ jQuery.event = {
 
                event.namespace = event.namespace || namespace_sort.join(".");
 
-               events = jQuery.data(this, "events");
+               events = jQuery.data(this, this.nodeType ? "events" : "__events__");
+
+               if ( typeof events === "function" ) {
+                       events = events.events;
+               }
+
                handlers = (events || {})[ event.type ];
 
                if ( events && handlers ) {
@@ -427,7 +468,7 @@ jQuery.event = {
                return event.result;
        },
 
-       props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+       props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
 
        fix: function( event ) {
                if ( event[ jQuery.expando ] ) {
@@ -467,8 +508,8 @@ jQuery.event = {
                }
 
                // Add which for key events
-               if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) {
-                       event.which = event.charCode || event.keyCode;
+               if ( event.which == null && (event.charCode != null || event.keyCode != null) ) {
+                       event.which = event.charCode != null ? event.charCode : event.keyCode;
                }
 
                // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
@@ -513,7 +554,7 @@ jQuery.event = {
                beforeunload: {
                        setup: function( data, namespaces, eventHandle ) {
                                // We only want to do this special case on windows
-                               if ( this.setInterval ) {
+                               if ( jQuery.isWindow( this ) ) {
                                        this.onbeforeunload = eventHandle;
                                }
                        },
@@ -670,6 +711,7 @@ if ( !jQuery.support.submitBubbles ) {
                                        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 );
                                        }
                                });
@@ -678,6 +720,7 @@ if ( !jQuery.support.submitBubbles ) {
                                        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 );
                                        }
                                });
@@ -697,9 +740,7 @@ if ( !jQuery.support.submitBubbles ) {
 // change delegation, happens here so we have bind.
 if ( !jQuery.support.changeBubbles ) {
 
-       var formElems = /textarea|input|select/i,
-
-       changeFilters,
+       var changeFilters,
 
        getVal = function( elem ) {
                var type = elem.type, val = elem.value;
@@ -724,7 +765,7 @@ if ( !jQuery.support.changeBubbles ) {
        testChange = function testChange( e ) {
                var elem = e.target, data, val;
 
-               if ( !formElems.test( elem.nodeName ) || elem.readOnly ) {
+               if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) {
                        return;
                }
 
@@ -742,6 +783,7 @@ if ( !jQuery.support.changeBubbles ) {
 
                if ( data != null || val ) {
                        e.type = "change";
+                       e.liveFired = undefined;
                        return jQuery.event.trigger( e, arguments[1], elem );
                }
        };
@@ -772,7 +814,7 @@ if ( !jQuery.support.changeBubbles ) {
 
                        // 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) );
@@ -788,17 +830,20 @@ if ( !jQuery.support.changeBubbles ) {
                                jQuery.event.add( this, type + ".specialChange", changeFilters[type] );
                        }
 
-                       return formElems.test( this.nodeName );
+                       return rformElems.test( this.nodeName );
                },
 
                teardown: function( namespaces ) {
                        jQuery.event.remove( this, ".specialChange" );
 
-                       return formElems.test( this.nodeName );
+                       return rformElems.test( this.nodeName );
                }
        };
 
        changeFilters = jQuery.event.special.change.filters;
+
+       // Handle when the input is .focus()'d
+       changeFilters.focus = changeFilters.beforeactivate;
 }
 
 function trigger( type, elem, args ) {
@@ -811,17 +856,21 @@ if ( document.addEventListener ) {
        jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
                jQuery.event.special[ fix ] = {
                        setup: function() {
-                               this.addEventListener( orig, handler, true );
+                               if ( focusCounts[fix]++ === 0 ) {
+                                       document.addEventListener( orig, handler, true );
+                               }
                        }, 
                        teardown: function() { 
-                               this.removeEventListener( orig, handler, true );
+                               if ( --focusCounts[fix] === 0 ) {
+                                       document.removeEventListener( orig, handler, true );
+                               }
                        }
                };
 
                function handler( e ) { 
                        e = jQuery.event.fix( e );
                        e.type = fix;
-                       return jQuery.event.handle.call( this, e );
+                       return jQuery.event.trigger( e, null, e.target );
                }
        });
 }
@@ -944,6 +993,14 @@ 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;
+               }
 
                if ( jQuery.isFunction( data ) ) {
                        fn = data;
@@ -995,8 +1052,12 @@ jQuery.each(["live", "die"], function( i, name ) {
 
 function liveHandler( event ) {
        var stop, maxLevel, elems = [], selectors = [],
-               related, match, handleObj, elem, j, i, l, data, close, namespace,
-               events = jQuery.data( this, "events" );
+               related, match, handleObj, elem, j, i, l, data, close, namespace, ret,
+               events = jQuery.data( this, this.nodeType ? "events" : "__events__" );
+
+       if ( typeof events === "function" ) {
+               events = events.events;
+       }
 
        // Make sure we avoid non-left-click bubbling in Firefox (#3861)
        if ( event.liveFired === this || !events || !events.live || event.button && event.type === "click" ) {
@@ -1073,7 +1134,7 @@ function liveHandler( event ) {
 }
 
 function liveConvert( type, selector ) {
-       return (type && type !== "*" ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&");
+       return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&");
 }
 
 jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
@@ -1102,7 +1163,7 @@ jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblcl
 // More info:
 //  - http://isaacschlueter.com/2006/10/msie-memory-leaks/
 if ( window.attachEvent && !window.addEventListener ) {
-       window.attachEvent("onunload", function() {
+       jQuery(window).bind("unload", function() {
                for ( var id in jQuery.cache ) {
                        if ( jQuery.cache[ id ].handle ) {
                                // Try/Catch is to handle iframes being unloaded, see #4280