Used the patch from Alexander as the basis for a rewrite of the IE change event logic...
[jquery.git] / src / event.js
index bf7798b..983e9e5 100644 (file)
@@ -37,13 +37,20 @@ jQuery.event = {
 
                // Init the element's event structure
                var events = jQuery.data( elem, "events" ) || jQuery.data( elem, "events", {} ),
-                       handle = jQuery.data( elem, "handle" ) || jQuery.data( elem, "handle", function() {
+                       handle = jQuery.data( elem, "handle" ), eventHandle;
+
+               if ( !handle ) {
+                       eventHandle = function() {
                                // 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 ?
-                                       jQuery.event.handle.apply( arguments.callee.elem, arguments ) :
+                                       jQuery.event.handle.apply( eventHandle.elem, arguments ) :
                                        undefined;
-                       });
+                       };
+
+                       handle = jQuery.data( elem, "handle", eventHandle );
+               }
+
                // Add elem as a property of the handle function
                // This is to prevent a memory leak with non-native
                // event in IE.
@@ -253,8 +260,10 @@ jQuery.event = {
 
                var nativeFn, nativeHandler;
                try {
-                       nativeFn = elem[ type ];
-                       nativeHandler = elem[ "on" + type ];
+                       if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) {
+                               nativeFn = elem[ type ];
+                               nativeHandler = elem[ "on" + type ];
+                       }
                // prevent IE from throwing an error for some elements with some event types, see #3533
                } catch (e) {}
 
@@ -264,12 +273,12 @@ jQuery.event = {
                if ( !bubbling && nativeFn && !event.isDefaultPrevented() && !isClick ) {
                        this.triggered = true;
                        try {
-                               nativeFn();
+                               elem[ type ]();
                        // prevent IE from throwing an error for some hidden elements
                        } catch (e) {}
 
                // Handle triggering native .onfoo handlers
-               } else if ( nativeHandler && nativeHandler.apply( elem, data ) === false ) {
+               } else if ( nativeHandler && elem[ "on" + type ].apply( elem, data ) === false ) {
                        event.result = false;
                }
 
@@ -327,6 +336,8 @@ 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(" "),
@@ -403,7 +414,7 @@ jQuery.event = {
        special: {
                ready: {
                        // Make sure the ready event is setup
-                       setup: bindReady,
+                       setup: jQuery.bindReady,
                        teardown: function() {}
                },
 
@@ -432,6 +443,21 @@ jQuery.event = {
                                }
                        },
                        special: {}
+               },
+               beforeunload: {
+                       setup: function( data, namespaces, fn ) {
+                               // We only want to do this special case on windows
+                               if ( this.setInterval ) {
+                                       this.onbeforeunload = fn;
+                               }
+
+                               return false;
+                       },
+                       teardown: function( namespaces, fn ) {
+                               if ( this.onbeforeunload === fn ) {
+                                       this.onbeforeunload = null;
+                               }
+                       }
                }
        }
 };
@@ -552,9 +578,11 @@ jQuery.each({
 });
 
 // submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
 jQuery.event.special.submit = {
        setup: function( data, namespaces, fn ) {
-               if ( !jQuery.support.submitBubbles && this.nodeName.toLowerCase() !== "form" ) {
+               if ( this.nodeName.toLowerCase() !== "form" ) {
                        jQuery.event.add(this, "click.specialSubmit." + fn.guid, function( e ) {
                                var elem = e.target, type = elem.type;
 
@@ -571,8 +599,6 @@ jQuery.event.special.submit = {
                                }
                        });
                }
-
-               return false;
        },
 
        remove: function( namespaces, fn ) {
@@ -581,139 +607,145 @@ jQuery.event.special.submit = {
        }
 };
 
+}
+
 // change delegation, happens here so we have bind.
-jQuery.event.special.change = {
-       filters: {
-               click: function( e ) { 
-                       var elem = e.target;
+if ( !jQuery.support.changeBubbles ) {
 
-                       if ( elem.nodeName.toLowerCase() === "input" && elem.type === "checkbox" ) {
-                               return trigger( "change", this, arguments );
-                       }
+var formElems = /textarea|input|select/i;
 
-                       return changeFilters.keyup.call( this, e );
-               }, 
-               keyup: function( e ) { 
-                       var elem = e.target, data, index = elem.selectedIndex + "";
+function getVal( elem ) {
+       var type = elem.type, val = elem.value;
 
-                       if ( elem.nodeName.toLowerCase() === "select" ) {
-                               data = jQuery.data( elem, "_change_data" );
-                               jQuery.data( elem, "_change_data", index );
+       if ( type === "radio" || type === "checkbox" ) {
+               val = elem.checked;
 
-                               if ( (elem.type === "select-multiple" || data != null) && data !== index ) {
-                                       return trigger( "change", this, arguments );
-                               }
-                       }
-               },
-               beforeactivate: function( e ) {
-                       var elem = e.target;
+       } else if ( type === "select-multiple" ) {
+               val = elem.selectedIndex > -1 ?
+                       jQuery.map( elem.options, function( elem ) {
+                               return elem.selected;
+                       }).join("-") :
+                       "";
+
+       } else if ( elem.nodeName.toLowerCase() === "select" ) {
+               val = elem.selectedIndex;
+       }
+
+       return val;
+}
+
+function testChange( e ) {
+               var elem = e.target, data, val;
+
+               if ( !formElems.test( elem.nodeName ) || elem.readOnly ) {
+                       return;
+               }
+
+               data = jQuery.data( elem, "_change_data" );
+               val = getVal(elem);
 
-                       if ( elem.nodeName.toLowerCase() === "input" && elem.type === "radio" && !elem.checked ) {
-                               return trigger( "change", this, arguments );
+               if ( val === data ) {
+                       return;
+               }
+
+               // the current data will be also retrieved by beforeactivate
+               if ( e.type !== "focusout" || elem.type !== "radio" ) {
+                       jQuery.data( elem, "_change_data", val );
+               }
+
+               if ( elem.type !== "select" && (data != null || val) ) {
+                       e.type = "change";
+                       return jQuery.event.trigger( e, arguments[1], this );
+               }
+}
+
+jQuery.event.special.change = {
+       filters: {
+               focusout: 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 );
                        }
                },
-               blur: function( e ) {
-                       var elem = e.target, nodeName = elem.nodeName.toLowerCase();
 
-                       if ( (nodeName === "textarea" || (nodeName === "input" && (elem.type === "text" || elem.type === "password")))
-                               && jQuery.data(elem, "_change_data") !== elem.value ) {
+               // Change has to be called before submit
+               // Keydown will be called before keypress, wich is used in submit-event delegation
+               keydown: function( e ) {
+                       var elem = e.target, type = elem.type;
 
-                               return trigger( "change", this, arguments );
+                       if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") ||
+                               (e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
+                               type === "select-multiple" ) {
+                               return testChange.call( this, e );
                        }
                },
-               focus: function( e ) {
-                       var elem = e.target, nodeName = elem.nodeName.toLowerCase();
 
-                       if ( nodeName === "textarea" || (nodeName === "input" && (elem.type === "text" || elem.type === "password" ) ) ) {
-                               jQuery.data( elem, "_change_data", elem.value );
+               // 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
+               beforeactivate: function( e ) {
+                       var elem = e.target;
+
+                       if ( elem.nodeName.toLowerCase() === "input" && elem.type === "radio" ) {
+                               return jQuery.data( elem, "_change_data", getVal(elem) );
                        }
                }
        },
        setup: function( data, namespaces, fn ) {
-               // return false if we bubble
-               if ( !jQuery.support.changeBubbles ) {
-                       for ( var type in changeFilters ) {
-                               jQuery.event.add( this, type + ".specialChange." + fn.guid, changeFilters[type] );
-                       }
+               for ( var type in changeFilters ) {
+                       jQuery.event.add( this, type + ".specialChange." + fn.guid, changeFilters[type] );
                }
-               
-               // always want to listen for change for trigger
-               return false;
+
+               return formElems.test( this.nodeName );
        },
        remove: function( namespaces, fn ) {
-               if ( !jQuery.support.changeBubbles ) {
-                       for ( var type in changeFilters ) {
-                               jQuery.event.remove( this, type + ".specialChange" + (fn ? "."+fn.guid : ""), changeFilters[type] );
-                       }
+               for ( var type in changeFilters ) {
+                       jQuery.event.remove( this, type + ".specialChange" + (fn ? "."+fn.guid : ""), changeFilters[type] );
                }
+
+               return formElems.test( this.nodeName );
        }
 };
 
 var changeFilters = jQuery.event.special.change.filters;
 
+}
+
 function trigger( type, elem, args ) {
        args[0].type = type;
        return jQuery.event.handle.apply( elem, args );
 }
 
 // Create "bubbling" focus and blur events
-jQuery.each({
-       focus: "focusin",
-       blur: "focusout"
-}, function( orig, fix ){
-       var event = jQuery.event,
-               handle = event.handle;
-       
-       function ieHandler() { 
-               arguments[0].type = orig;
-               return handle.apply(this, arguments);
-       }
+if ( !jQuery.support.focusBubbles ) {
 
-       event.special[orig] = {
-               setup:function() {
-                       if ( this.addEventListener ) {
-                               this.addEventListener( orig, handle, true );
-                       } else {
-                               event.add( this, fix, ieHandler );
-                       }
+jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ){
+       jQuery.event.special[ orig ] = {
+               setup: function() {
+                       jQuery.event.add( this, fix, ieHandler );
                }, 
-               teardown:function() { 
-                       if ( this.removeEventListener ) {
-                               this.removeEventListener( orig, handle, true );
-                       } else {
-                               event.remove( this, fix, ieHandler );
-                       }
+               teardown: function() { 
+                       jQuery.event.remove( this, fix, ieHandler );
                }
        };
+
+       function ieHandler() { 
+               arguments[0].type = orig;
+               return jQuery.event.handle.apply(this, arguments);
+       }
 });
 
-jQuery.fn.extend({
-       // 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, thisObject ) {
+jQuery.each(["bind", "one"], function(i, name) {
+       jQuery.fn[ name ] = function( type, data, fn, thisObject ) {
                // Handle object literals
                if ( typeof type === "object" ) {
                        for ( var key in type ) {
-                               this.one(key, data, type[key], fn);
+                               this[ name ](key, data, type[key], fn);
                        }
                        return this;
                }
@@ -724,15 +756,17 @@ jQuery.fn.extend({
                        data = undefined;
                }
                fn = thisObject === undefined ? fn : jQuery.event.proxy( fn, thisObject );
-               var one = jQuery.event.proxy( fn, function( event ) {
-                       jQuery( this ).unbind( event, one );
+               var handler = name == "one" ? jQuery.event.proxy( fn, function( event ) {
+                       jQuery( this ).unbind( event, handler );
                        return fn.apply( this, arguments );
+               }) : fn;
+               return type === "unload" ? this.one(type, data, handler, thisObject) : this.each(function() {
+                       jQuery.event.add( this, type, handler, data );
                });
-               return this.each(function() {
-                       jQuery.event.add( this, type, one, data );
-               });
-       },
+       };
+});
 
+jQuery.fn.extend({
        unbind: function( type, fn ) {
                // Handle object literals
                if ( typeof type === "object" && !type.preventDefault ) {
@@ -741,12 +775,11 @@ jQuery.fn.extend({
                        }
                        return this;
                }
-               
+
                return this.each(function() {
                        jQuery.event.remove( this, type, fn );
                });
        },
-
        trigger: function( type, data ) {
                return this.each(function() {
                        jQuery.event.trigger( type, data, this );
@@ -789,24 +822,6 @@ jQuery.fn.extend({
                return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
        },
 
-       ready: function( fn ) {
-               // Attach the listeners
-               bindReady();
-
-               // If the DOM is already ready
-               if ( jQuery.isReady ) {
-                       // Execute the function immediately
-                       fn.call( document, jQuery );
-
-               // Otherwise, remember the function for later
-               } else {
-                       // Add the function to the wait list
-                       jQuery.readyList.push( fn );
-               }
-
-               return this;
-       },
-
        live: function( type, data, fn, thisObject ) {
                if ( jQuery.isFunction( data ) ) {
                        if ( fn !== undefined ) {
@@ -882,106 +897,7 @@ function liveHandler( event ) {
 }
 
 function liveConvert( type, selector ) {
-       return ["live", type, selector//.replace(/[^\w\s\.]/g, function(ch){ return "\\"+ch})
-                                                                 .replace(/\./g, "`")
-                                                                 .replace(/ /g, "|")].join(".");
-}
-
-jQuery.extend({
-       isReady: false,
-       readyList: [],
-       // Handle when the DOM is ready
-       ready: function() {
-               // Make sure that the DOM is not already loaded
-               if ( !jQuery.isReady ) {
-                       if ( !document.body ) {
-                               return setTimeout( jQuery.ready, 13 );
-                       }
-
-                       // Remember that the DOM is ready
-                       jQuery.isReady = true;
-
-                       // If there are functions bound, to execute
-                       if ( jQuery.readyList ) {
-                               // Execute all of them
-                               var fn, i = 0;
-                               while ( (fn = jQuery.readyList[ i++ ]) ) {
-                                       fn.call( document, jQuery );
-                               }
-
-                               // Reset the list of functions
-                               jQuery.readyList = null;
-                       }
-
-                       // Trigger any bound ready events
-                       jQuery( document ).triggerHandler( "ready" );
-               }
-       }
-});
-
-var readyBound = false;
-
-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
-               document.addEventListener( "DOMContentLoaded", function() {
-                       document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
-                       jQuery.ready();
-               }, false );
-
-       // If IE event model is used
-       } else if ( document.attachEvent ) {
-               // ensure firing before onload,
-               // maybe late but safe also for iframes
-               document.attachEvent("onreadystatechange", function() {
-                       // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
-                       if ( document.readyState === "complete" ) {
-                               document.detachEvent( "onreadystatechange", arguments.callee );
-                               jQuery.ready();
-                       }
-               });
-
-               // If IE and not a frame
-               // continually check to see if the document is ready
-               var toplevel = false;
-
-               try {
-                       toplevel = window.frameElement == null;
-               } catch(e){}
-
-               if ( document.documentElement.doScroll && toplevel ) {
-                       (function() {
-                               if ( jQuery.isReady ) {
-                                       return;
-                               }
-
-                               try {
-                                       // If IE is used, use the trick by Diego Perini
-                                       // http://javascript.nwbox.com/IEContentLoaded/
-                                       document.documentElement.doScroll("left");
-                               } catch( error ) {
-                                       setTimeout( arguments.callee, 0 );
-                                       return;
-                               }
-
-                               // and execute any waiting functions
-                               jQuery.ready();
-                       })();
-               }
-       }
-
-       // A fallback to window.onload, that will always work
-       jQuery.event.add( window, "load", jQuery.ready );
+       return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "&")].join(".");
 }
 
 jQuery.each( ("blur focus load resize scroll unload click dblclick " +
@@ -992,19 +908,25 @@ jQuery.each( ("blur focus load resize scroll unload click dblclick " +
        jQuery.fn[ name ] = function( fn ) {
                return fn ? this.bind( name, fn ) : this.trigger( name );
        };
+
+       if ( jQuery.fnAttr ) {
+               jQuery.fnAttr[ name ] = true;
+       }
 });
 
 // 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/
-/*@cc_on
-jQuery( window ).bind( 'unload', function() {
-       for ( var id in jQuery.cache ) {
-               // Skip the window
-               if ( id != 1 && jQuery.cache[ id ].handle ) {
-                       jQuery.event.remove( jQuery.cache[ id ].handle.elem );
+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) {}
+                       }
                }
-       }
-});
-@*/
+       });
+}