From b264f789b9a6995ad0e7eec6771fafa852717571 Mon Sep 17 00:00:00 2001 From: Brandon Aaron Date: Sat, 15 Dec 2007 05:55:33 +0000 Subject: [PATCH] new special events api, ready is now a first class event that you can use bind, unbind or the ready helper, two new events: mouseenter and mouseleave, the hover helper method now uses mouseenter and mouseleave, bind and unbind can now take a space sperated list of event types --- src/event.js | 359 ++++++++++++++++++++++++--------------------- test/unit/event.js | 31 +++- 2 files changed, 219 insertions(+), 171 deletions(-) diff --git a/src/event.js b/src/event.js index a3801306..cc3338b0 100644 --- a/src/event.js +++ b/src/event.js @@ -7,7 +7,7 @@ jQuery.event = { // Bind an event to an element // Original by Dean Edwards - add: function(element, type, handler, data) { + add: function(element, types, handler, data) { if ( element.nodeType == 3 || element.nodeType == 8 ) return; @@ -38,99 +38,113 @@ jQuery.event = { handler.guid = fn.guid; } - // Namespaced event handlers - var parts = type.split("."); - type = parts[0]; - handler.type = parts[1]; - // Init the element's event structure - var events = jQuery.data(element, "events") || jQuery.data(element, "events", {}); + var events = jQuery.data(element, "events") || jQuery.data(element, "events", {}), + handle = jQuery.data(element, "handle") || jQuery.data(element, "handle", function(){ + // returned undefined or false + var val; + + // Handle the second event of a trigger and when + // an event is called after a page has unloaded + if ( typeof jQuery == "undefined" || jQuery.event.triggered ) + return val; + + val = jQuery.event.handle.apply(element, arguments); - var handle = jQuery.data(element, "handle") || jQuery.data(element, "handle", function(){ - // returned undefined or false - var val; - - // Handle the second event of a trigger and when - // an event is called after a page has unloaded - if ( typeof jQuery == "undefined" || jQuery.event.triggered ) return val; + }); - val = jQuery.event.handle.apply(element, arguments); - - return val; - }); + // Handle multiple events seperated by a space + // jQuery(...).bind("mouseover mouseout", fn); + jQuery.each(types.split(/\s+/), function(index, type) { + // Namespaced event handlers + var parts = type.split("."); + type = parts[0]; + handler.type = parts[1]; - // Get the current list of functions bound to this event - var handlers = events[type]; + // Get the current list of functions bound to this event + var handlers = events[type]; - // Init the event handler queue - if (!handlers) { - handlers = events[type] = {}; - - // And bind the global event handler to the element - if (element.addEventListener) - element.addEventListener(type, handle, false); - else - element.attachEvent("on" + type, handle); - } + // Init the event handler queue + if (!handlers) { + handlers = events[type] = {}; + + // Check for a special event handler + // Only use addEventListener/attachEvent if the special + // events handler returns false + if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(element) === false ) { + // Bind the global event handler to the element + if (element.addEventListener) + element.addEventListener(type, handle, false); + else if (element.attachEvent) + element.attachEvent("on" + type, handle); + } + } - // Add the function to the element's handler list - handlers[handler.guid] = handler; + // Add the function to the element's handler list + handlers[handler.guid] = handler; - // Keep track of which events have been used, for global triggering - this.global[type] = true; + // Keep track of which events have been used, for global triggering + jQuery.event.global[type] = true; + }); }, guid: 1, global: {}, // Detach an event or set of events from an element - remove: function(element, type, handler) { + remove: function(element, types, handler) { // don't do events on text and comment nodes if ( element.nodeType == 3 || element.nodeType == 8 ) return; var events = jQuery.data(element, "events"), ret, index; - // Namespaced event handlers - if ( typeof type == "string" ) { - var parts = type.split("."); - type = parts[0]; - } - if ( events ) { - // type is actually an event object here - if ( type && type.type ) { - handler = type.handler; - type = type.type; - } - - if ( !type ) { - for ( type in events ) + // Unbind all events for the element + if ( !types ) + for ( var type in events ) this.remove( element, type ); - - } else if ( events[type] ) { - // remove the given handler for the given type - if ( handler ) - delete events[type][handler.guid]; - - // remove all handlers for the given type - else - for ( handler in events[type] ) - // Handle the removal of namespaced events - if ( !parts[1] || events[type][handler].type == parts[1] ) - delete events[type][handler]; - - // remove generic event handler if no more handlers exist - for ( ret in events[type] ) break; - if ( !ret ) { - if (element.removeEventListener) - element.removeEventListener(type, jQuery.data(element, "handle"), false); - else - element.detachEvent("on" + type, jQuery.data(element, "handle")); - ret = null; - delete events[type]; + else { + // types is actually an event object here + if ( types.type ) { + handler = types.handler; + types = types.type; } + + // Handle multiple events seperated by a space + // jQuery(...).unbind("mouseover mouseout", fn); + jQuery.each(types.split(/\s+/), function(index, type){ + // Namespaced event handlers + var parts = type.split("."); + type = parts[0]; + + if ( events[type] ) { + // remove the given handler for the given type + if ( handler ) + delete events[type][handler.guid]; + + // remove all handlers for the given type + else + for ( handler in events[type] ) + // Handle the removal of namespaced events + if ( !parts[1] || events[type][handler].type == parts[1] ) + delete events[type][handler]; + + // remove generic event handler if no more handlers exist + for ( ret in events[type] ) break; + if ( !ret ) { + if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(this, element) === false ) { + if (element.removeEventListener) + element.removeEventListener(type, jQuery.data(element, "handle"), false); + else if (element.detachEvent) + element.detachEvent("on" + type, jQuery.data(element, "handle")); + } + ret = null; + delete events[type]; + } + } + }); } // Remove the expando if it's no longer used @@ -304,6 +318,98 @@ jQuery.event = { event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); return event; + }, + + special: { + ready: { + setup: function() { + var handler = jQuery.event.special.ready.handler; + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", handler, false ); + + // If Safari or IE is used + // Continually check to see if the document is ready + if (jQuery.browser.msie || jQuery.browser.safari ) (function(){ + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + if ( jQuery.browser.msie || document.readyState != "loaded" && document.readyState != "complete" ) + document.documentElement.doScroll("left"); + } catch( error ) { + return setTimeout( arguments.callee, 0 ); + } + + // and execute any waiting functions + handler(); + })(); + + // A fallback to window.onload, that will always work + jQuery.event.add( window, "load", handler ); + }, + + teardown: function() {return;}, + + handler: function() { + // Make sure that the DOM is not already loaded + if ( !jQuery.isReady ) { + // Remember that the DOM is ready + jQuery.isReady = true; + jQuery(document).triggerHandler("ready"); + jQuery(document).unbind("ready"); + } + } + }, + + mouseenter: { + setup: function() { + if (jQuery.browser.msie) return false; + jQuery(this).bind('mouseover', jQuery.event.special.mouseenter.handler); + }, + + teardown: function() { + if (jQuery.browser.msie) return false; + jQuery(this).unbind('mouseover', jQuery.event.special.mouseenter.handler); + }, + + handler: function(event) { + var args = Array.prototype.slice.call( arguments, 1 ); + // If we actually just moused on to a sub-element, ignore it + if ( withinElement(event, this) ) return; + // Execute the right handlers by setting the event type to mouseenter + event.type = 'mouseenter'; + // Include the event object as the first argument + args.unshift(event); + var val = jQuery.event.handle.apply(this, args); + return val; + } + }, + + mouseleave: { + setup: function() { + if (jQuery.browser.msie) return false; + jQuery(this).bind('mouseout', jQuery.event.special.mouseleave.handler); + }, + + teardown: function() { + if (jQuery.browser.msie) return false; + jQuery(this).unbind('mouseout', jQuery.event.special.mouseleave.handler); + }, + + handler: function(event) { + var args = Array.prototype.slice.call( arguments, 1 ); + // If we actually just moused on to a sub-element, ignore it + if ( withinElement(event, this) ) return false; + // Execute the right handlers by setting the event type to mouseleave + event.type = 'mouseleave'; + // Include the event object as the first argument + args.unshift(event); + var val = jQuery.event.handle.apply(this, args); + return val; + } + } } }; @@ -357,78 +463,15 @@ jQuery.fn.extend({ }, hover: function(fnOver, fnOut) { - - // A private function for handling mouse 'hovering' - function handleHover(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 ) try { parent = parent.parentNode; } catch(error) { parent = this; }; - - // If we actually just moused on to a sub-element, ignore it - if ( parent == this ) - return true; - - // Execute the right function - return (event.type == "mouseover" ? fnOver : fnOut).apply(this, [event]); - } - - // Bind the function to the two event listeners - return this.mouseover(handleHover).mouseout(handleHover); - }, - - ready: function(fn) { - // Attach the listeners - bindReady(); - - // If the DOM is already ready - if ( jQuery.isReady ) - // Execute the function immediately - fn.apply( document, [jQuery] ); - - // Otherwise, remember the function for later - else - // Add the function to the wait list - jQuery.readyList.push( function() { return fn.apply(this, [jQuery]); } ); - - return this; + return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut); } }); jQuery.extend({ - /* - * All the code that makes DOM Ready work nicely. - */ - isReady: false, - readyList: [], - - // Handle when the DOM is ready - ready: function() { - // Make sure that the DOM is not already loaded - if ( !jQuery.isReady ) { - // Remember that the DOM is ready - jQuery.isReady = true; - - // If there are functions bound, to execute - if ( jQuery.readyList ) { - // Execute all of them - jQuery.each( jQuery.readyList, function(){ - this.apply( document ); - }); - - // Reset the list of functions - jQuery.readyList = null; - } - // Remove event listener to avoid memory leak - if ( document.removeEventListener ) - document.removeEventListener( "DOMContentLoaded", jQuery.ready, false ); - } - } + isReady: false }); - -jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," + +jQuery.each( ("blur,focus,load,ready,resize,scroll,unload,click,dblclick," + "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + "submit,keydown,keypress,keyup,error").split(","), function(i, name){ @@ -438,39 +481,19 @@ jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," + }; }); -var readyBound = false; - -function bindReady(){ - if ( readyBound ) return; - readyBound = true; - - // Mozilla, Opera and webkit nightlies currently support this event - if ( document.addEventListener ) - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", jQuery.ready, false ); - - // If Safari or IE is used - // Continually check to see if the document is ready - if (jQuery.browser.msie || jQuery.browser.safari ) (function(){ - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - if ( jQuery.browser.msie || document.readyState != "loaded" && document.readyState != "complete" ) - document.documentElement.doScroll("left"); - } catch( error ) { - return setTimeout( arguments.callee, 0 ); - } - - // and execute any waiting functions - jQuery.ready(); - })(); - - // A fallback to window.onload, that will always work - jQuery.event.add( window, "load", jQuery.ready ); -} +// Checks if an event happened on an element within another element +// Used in jQuery.event.special.mouseenter and mouseleave handlers +var withinElement = function(event, element) { + // Check if mouse(over|out) are still within the same parent element + var parent = event.relatedTarget; + // Traverse up the tree + while ( parent && parent != element ) try { parent = parent.parentNode } catch(error) { parent = element; }; + // Return true if we actually just moused on to a sub-element + return parent == element; +}; // Prevent memory leaks in IE -// And prevent errors on refresh with events like mouseover +// And prevent errors on refresh with events like mouseover in other browsers // Window isn't included so as not to unbind existing unload events jQuery(window).bind("unload", function() { jQuery("*").add(document).unbind(); diff --git a/test/unit/event.js b/test/unit/event.js index d0897e46..73ce17cb 100644 --- a/test/unit/event.js +++ b/test/unit/event.js @@ -1,7 +1,7 @@ module("event"); test("bind()", function() { - expect(16); + expect(18); var handler = function(event) { ok( event.data, "bind() with data, check passed data exists" ); @@ -18,7 +18,20 @@ test("bind()", function() { ok( data, "Check trigger data" ); ok( data.bar == "foo", "Check value of trigger data" ); }; - $("#firstp").bind("click", {foo: "bar"}, handler).trigger("click", [{bar: "foo"}]).unbind(handler); + $("#firstp").bind("click", {foo: "bar"}, handler).trigger("click", [{bar: "foo"}]).unbind("click", handler); + + reset(); + var clickCounter = mouseoverCounter = 0; + var handler = function(event) { + if (event.type == "click") + clickCounter += 1; + else if (event.type == "mouseover") + mouseoverCounter += 1; + }; + $("#firstp").bind("click mouseover", handler).trigger("click").trigger("mouseover"); + ok( clickCounter == 1, "bind() with multiple events at once" ); + ok( mouseoverCounter == 1, "bind() with multiple events at once" ); + reset(); var handler = function(event) { @@ -96,7 +109,7 @@ test("click()", function() { }); test("unbind(event)", function() { - expect(6); + expect(8); var el = $("#firstp"); el.click(function() { ok( true, "Fake normal bind" ); @@ -118,6 +131,18 @@ test("unbind(event)", function() { el.unbind('click'); ok( !jQuery.data(el[0], "events"), "Removed the events expando after all handlers are unbound." ); + + reset(); + var clickCounter = mouseoverCounter = 0; + var handler = function(event) { + if (event.type == "click") + clickCounter += 1; + else if (event.type == "mouseover") + mouseoverCounter += 1; + }; + $("#firstp").bind("click mouseover", handler).unbind("click mouseover", handler).trigger("click").trigger("mouseover"); + ok( clickCounter == 0, "unbind() with multiple events at once" ); + ok( mouseoverCounter == 0, "unbind() with multiple events at once" ); }); test("trigger(event, [data], [fn])", function() {