X-Git-Url: http://git.asbjorn.biz/?a=blobdiff_plain;f=src%2Fevent.js;h=02349984abcdbc84a13acf124693d3300c634f4b;hb=8e53f7b5d6716e60d8c8ea7e167f2b187aae9d89;hp=d9401c99e743ff58c650cf4e57aeb83bad5f27aa;hpb=aea5b091954b3c823b318b392660b22c7b1e978a;p=jquery.git diff --git a/src/event.js b/src/event.js index d9401c9..0234998 100644 --- a/src/event.js +++ b/src/event.js @@ -1,3 +1,9 @@ +var fcleanup = function( nm ) { + return nm.replace(/[^\w\s\.\|`]/g, function( ch ) { + return "\\" + ch; + }); +}; + /* * A number of helper functions used for managing events. * Many of the ideas behind this code originated from @@ -20,7 +26,7 @@ jQuery.event = { // Make sure that the function being executed has a unique ID if ( !handler.guid ) { - handler.guid = this.guid++; + handler.guid = jQuery.guid++; } // if data is passed, bind to handler @@ -29,7 +35,7 @@ jQuery.event = { var fn = handler; // Create unique handler function, wrapped around original handler - handler = this.proxy( fn ); + handler = jQuery.proxy( fn ); // Store data in unique handler handler.data = data; @@ -37,13 +43,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. @@ -101,7 +114,6 @@ jQuery.event = { elem = null; }, - guid: 1, global: {}, // Detach an event or set of events from an element @@ -135,7 +147,7 @@ jQuery.event = { var namespaces = type.split("."); type = namespaces.shift(); var all = !namespaces.length, - cleaned = jQuery.map( namespaces.slice(0).sort() , function(nm){ return nm.replace(/[^\w\s\.\|`]/g, function(ch){return "\\"+ch; }); }), + cleaned = jQuery.map( namespaces.slice(0).sort(), fcleanup ), namespace = new RegExp("(^|\\.)" + cleaned.join("\\.(?:.*\\.)?") + "(\\.|$)"), special = this.special[ type ] || {}; @@ -217,6 +229,7 @@ jQuery.event = { if ( !elem ) { // Don't bubble custom events when global (to avoid too much overhead) event.stopPropagation(); + // Only trigger if we've ever bound an event for it if ( this.global[ type ] ) { jQuery.each( jQuery.cache, function() { @@ -253,8 +266,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 +279,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; } @@ -380,7 +395,7 @@ jQuery.event = { event.metaKey = event.ctrlKey; } - // Add which for click: 1 == left; 2 == middle; 3 == right + // 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 !== undefined ) { event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); @@ -389,24 +404,17 @@ jQuery.event = { return event; }, - 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 - return proxy; - }, + // Deprecated, use jQuery.guid instead + guid: 1E8, + + // Deprecated, use jQuery.proxy instead + proxy: jQuery.proxy, special: { ready: { // Make sure the ready event is setup - setup: bindReady, - teardown: function() {} + setup: jQuery.bindReady, + teardown: jQuery.noop }, live: { @@ -453,7 +461,7 @@ jQuery.event = { } }; -jQuery.Event = function( src ){ +jQuery.Event = function( src ) { // Allow instantiation without the 'new' keyword if ( !this.preventDefault ) { return new jQuery.Event( src ); @@ -515,7 +523,7 @@ jQuery.Event.prototype = { // otherwise set the cancelBubble property of the original event to true (IE) e.cancelBubble = true; }, - stopImmediatePropagation: function(){ + stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); }, @@ -523,23 +531,30 @@ jQuery.Event.prototype = { isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse }; + // Checks if an event happened on an element within another element // Used in jQuery.event.special.mouseenter and mouseleave handlers var withinElement = function( event ) { // Check if mouse(over|out) are still within the same parent element var parent = event.relatedTarget; + // Traverse up the tree - while ( parent && parent != this ) { + while ( parent && parent !== this ) { // Firefox sometimes assigns relatedTarget a XUL element // which we cannot access the parentNode property of - try { parent = parent.parentNode; } + try { + parent = parent.parentNode; + // assuming we've left the element since we most likely mousedover a xul element - catch(e) { break; } + } catch(e) { + break; + } } - if ( parent != this ) { + if ( parent !== this ) { // set the correct event type event.type = event.data; + // handle event if we actually just moused on to a non sub-element jQuery.event.handle.apply( this, arguments ); } @@ -559,19 +574,21 @@ jQuery.each({ mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { - setup: function(data){ + setup: function( data ) { jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); }, - teardown: function(data){ + teardown: function( data ) { jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); } }; }); // 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; @@ -587,9 +604,10 @@ jQuery.event.special.submit = { return trigger( "submit", this, arguments ); } }); - } - return false; + } else { + return false; + } }, remove: function( namespaces, fn ) { @@ -598,139 +616,144 @@ 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; +} - if ( elem.nodeName.toLowerCase() === "input" && elem.type === "radio" && !elem.checked ) { - return trigger( "change", this, arguments ); +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 ( 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, which 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" ) { + 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); - } - - event.special[orig] = { - setup:function() { - if ( this.addEventListener ) { - this.addEventListener( orig, handle, true ); - } else { - event.add( this, fix, ieHandler ); +if ( document.addEventListener ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + jQuery.event.special[ fix ] = { + setup: function() { + this.addEventListener( orig, handler, true ); + }, + teardown: function() { + this.removeEventListener( orig, handler, true ); } - }, - teardown:function() { - if ( this.removeEventListener ) { - this.removeEventListener( orig, handle, true ); - } else { - event.remove( this, fix, ieHandler ); - } - } - }; -}); + }; -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; + function handler( e ) { + e = jQuery.event.fix( e ); + e.type = fix; + return jQuery.event.handle.call( this, e ); } - 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 ) { // 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; } @@ -740,16 +763,21 @@ jQuery.fn.extend({ 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 ); + + var handler = name === "one" ? jQuery.proxy( fn, function( event ) { + jQuery( this ).unbind( event, handler ); return fn.apply( this, arguments ); - }); - return this.each(function() { - jQuery.event.add( this, type, one, data ); - }); - }, + }) : fn; + + return type === "unload" && name !== "one" ? + this.one( type, data, fn, thisObject ) : + this.each(function() { + jQuery.event.add( this, type, handler, data ); + }); + }; +}); +jQuery.fn.extend({ unbind: function( type, fn ) { // Handle object literals if ( typeof type === "object" && !type.preventDefault ) { @@ -758,12 +786,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 ); @@ -785,14 +812,14 @@ jQuery.fn.extend({ var args = arguments, i = 1; // link all the functions, so any of them can unbind this click handler - while( i < args.length ) { - jQuery.event.proxy( fn, args[ i++ ] ); + while ( i < args.length ) { + jQuery.proxy( fn, args[ i++ ] ); } - return this.click( jQuery.event.proxy( fn, function( event ) { + 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(); @@ -806,35 +833,16 @@ 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 ) { + live: function( type, data, fn ) { if ( jQuery.isFunction( data ) ) { - if ( fn !== undefined ) { - thisObject = fn; - } fn = data; data = undefined; } + jQuery( this.context ).bind( liveConvert( type, this.selector ), { data: data, selector: this.selector, live: type - }, fn, thisObject ); + }, fn ); + return this; }, @@ -899,109 +907,10 @@ function liveHandler( event ) { } function liveConvert( type, selector ) { - return ["live", type, selector//.replace(/[^\w\s\.]/g, function(ch){ return "\\"+ch}) - .replace(/\./g, "`") - .replace(/ /g, "|")].join("."); + return ["live", type, selector.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 ); -} - -jQuery.each( ("blur focus load resize scroll unload click dblclick " + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "change select submit keydown keypress keyup error").split(" "), function( i, name ) { @@ -1009,19 +918,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.attrFn ) { + jQuery.attrFn[ 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) {} + } } - } -}); -@*/ + }); +}