From d42afd0f657d12d6daba6894d40226bea83fe1b6 Mon Sep 17 00:00:00 2001 From: Justin Meyer Date: Fri, 4 Dec 2009 11:28:50 -0500 Subject: [PATCH] Adding in support for bubbling submit and change events, thanks to the patch by Justin Meyer. Includes a delegation test suite for manually testing to see if the events work as intended. --- src/event.js | 186 +++++++++++++++++++++++++++++++++++------------- src/support.js | 19 +++++ test/delegatetest.html | 123 ++++++++++++++++++++++++++++++++ test/unit/event.js | 139 +++++++++++++++++++++++++++++++++--- 4 files changed, 409 insertions(+), 58 deletions(-) create mode 100644 test/delegatetest.html diff --git a/src/event.js b/src/event.js index 7b765ce..acf363b 100644 --- a/src/event.js +++ b/src/event.js @@ -63,13 +63,7 @@ jQuery.event = { var handlers = events[ type ], special = this.special[ type ] || {}; - if ( special.add ) { - var modifiedHandler = special.add.call( elem, handler, data, namespaces ); - if ( modifiedHandler && jQuery.isFunction( modifiedHandler ) ) { - modifiedHandler.guid = modifiedHandler.guid || handler.guid; - handler = modifiedHandler; - } - } + // Init the event handler queue if ( !handlers ) { @@ -78,7 +72,7 @@ jQuery.event = { // Check for a special event handler // Only use addEventListener/attachEvent if the special // events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces ) === false ) { + if ( !special.setup || special.setup.call( elem, data, namespaces, handler) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { elem.addEventListener( type, handle, false ); @@ -87,7 +81,15 @@ jQuery.event = { } } } - + + if ( special.add ) { + var modifiedHandler = special.add.call( elem, handler, data, namespaces, handlers ); + if ( modifiedHandler && jQuery.isFunction( modifiedHandler ) ) { + modifiedHandler.guid = modifiedHandler.guid || handler.guid; + handler = modifiedHandler; + } + } + // Add the function to the element's handler list handlers[ handler.guid ] = handler; @@ -109,7 +111,7 @@ jQuery.event = { return; } - var events = jQuery.data( elem, "events" ), ret, type; + var events = jQuery.data( elem, "events" ), ret, type, fn; if ( events ) { // Unbind all events for the element @@ -133,12 +135,14 @@ jQuery.event = { var namespaces = type.split("."); type = namespaces.shift(); var all = !namespaces.length, - namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)"), + cleaned = jQuery.map( namespaces.slice(0).sort() , function(nm){ return nm.replace(/[^\w\s\.\|`]/g, function(ch){return "\\"+ch }) }), + namespace = new RegExp("(^|\\.)" + cleaned.join("\\.(?:.*\\.)?") + "(\\.|$)"), special = this.special[ type ] || {}; if ( events[ type ] ) { // remove the given handler for the given type if ( handler ) { + fn = events[ type ][ handler.guid ]; delete events[ type ][ handler.guid ]; // remove all handlers for the given type @@ -152,7 +156,7 @@ jQuery.event = { } if ( special.remove ) { - special.remove.call( elem, namespaces ); + special.remove.call( elem, namespaces, fn); } // remove generic event handler if no more handlers exist @@ -402,10 +406,12 @@ jQuery.event = { }, live: { - add: function( proxy, data, namespaces ) { + add: function( proxy, data, namespaces, live ) { jQuery.extend( proxy, data || {} ); - proxy.guid += data.selector + data.live; - jQuery.event.add( this, data.live, liveHandler, data ); + + proxy.guid += data.selector + data.live; + jQuery.event.add( this, data.live, liveHandler, data ); + }, remove: function( namespaces ) { @@ -422,7 +428,8 @@ jQuery.event = { jQuery.event.remove( this, namespaces[0], liveHandler ); } } - } + }, + special: {} } } }; @@ -542,40 +549,110 @@ jQuery.each({ }; }); -(function() { - - var event = jQuery.event, - special = event.special, - handle = event.handle; - - special.submit = { - setup: function(data, namespaces) { - if(data.selector) { - event.add(this, 'click.specialSubmit', function(e, eventData) { - if(jQuery(e.target).filter(":submit, :image").closest(data.selector).length) { - e.type = "submit"; - return handle.call( this, e, eventData ); - } - }); - - event.add(this, 'keypress.specialSubmit', function( e, eventData ) { - if(jQuery(e.target).filter(":text, :password").closest(data.selector).length) { - e.type = "submit"; - return handle.call( this, e, eventData ); - } - }); - } else { - return false; +// submit delegation +jQuery.event.special.submit = { + setup: function( data, namespaces, fn ) { + if ( !jQuery.support.submitBubbles && this.nodeName.toLowerCase() !== "form" ) { + jQuery.event.add(this, "click.specialSubmit." + fn.guid, function( e ) { + var elem = e.target, type = elem.type; + + if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { + return trigger( "submit", this, arguments ); + } + }); + + jQuery.event.add(this, "keypress.specialSubmit." + fn.guid, function( e ) { + var elem = e.target, type = elem.type; + + if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { + return trigger( "submit", this, arguments ); + } + }); + } + + return false; + }, + + remove: function( namespaces, fn ) { + jQuery.event.remove( this, "click.specialSubmit" + (fn ? "."+fn.guid : "") ); + jQuery.event.remove( this, "keypress.specialSubmit" + (fn ? "."+fn.guid : "") ); + } +}; + +// change delegation, happens here so we have bind. +jQuery.event.special.change = { + filters: { + click: function( e ) { + var elem = e.target; + + if ( elem.nodeName.toLowerCase() === "input" && elem.type === "checkbox" ) { + return trigger( "change", this, arguments ); + } + + return changeFilters.keyup.call( this, e ); + }, + keyup: function( e ) { + var elem = e.target, data, index = elem.selectedIndex + ""; + + if ( elem.nodeName.toLowerCase() === "select" ) { + data = jQuery.data( elem, "_change_data" ); + jQuery.data( elem, "_change_data", index ); + + if ( (elem.type === "select-multiple" || data != null) && data !== index ) { + return trigger( "change", this, arguments ); + } + } + }, + beforeactivate: function( e ) { + var elem = e.target; + + if ( elem.nodeName.toLowerCase() === "input" && elem.type === "radio" && !elem.checked ) { + return trigger( "change", this, arguments ); } }, + 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 ) { + + return trigger( "change", this, arguments ); + } + }, + 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 ); + } + } + }, + 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] ); + } + } - remove: function(namespaces) { - event.remove(this, 'click.specialSubmit'); - event.remove(this, 'keypress.specialSubmit'); + // always want to listen for change for trigger + return false; + }, + remove: function( namespaces, fn ) { + if ( !jQuery.support.changeBubbles ) { + for ( var type in changeFilters ) { + jQuery.event.remove( this, type + ".specialChange" + (fn ? "."+fn.guid : ""), changeFilters[type] ); + } } - }; - -})(); + } +}; + +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({ @@ -750,12 +827,19 @@ jQuery.fn.extend({ function liveHandler( event ) { var stop = true, elems = [], selectors = [], args = arguments, - related, match, fn, elem, j, i, + related, match, fn, elem, j, i, data, live = jQuery.extend({}, jQuery.data( this, "events" ).live); for ( j in live ) { - if ( live[j].live === event.type ) { - selectors.push( live[j].selector ); + fn = live[j]; + if ( fn.live === event.type || + fn.altLive && jQuery.inArray(event.type, fn.altLive) > -1 ) { + + data = fn.data; + if ( !(data.beforeFilter && data.beforeFilter[event.type] && + !data.beforeFilter[event.type](event)) ) { + selectors.push( fn.selector ); + } } else { delete live[j]; } @@ -796,7 +880,9 @@ function liveHandler( event ) { } function liveConvert( type, selector ) { - return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "|")].join("."); + return ["live", type, selector//.replace(/[^\w\s\.]/g, function(ch){ return "\\"+ch}) + .replace(/\./g, "`") + .replace(/ /g, "|")].join("."); } jQuery.extend({ diff --git a/src/support.js b/src/support.js index 5fa45d2..ad8566d 100644 --- a/src/support.js +++ b/src/support.js @@ -91,6 +91,25 @@ div = null; }); + // Technique from Juriy Zaytsev + // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ + var eventSupported = function( eventName ) { + var el = document.createElement("div"); + eventName = "on" + eventName; + + var isSupported = (eventName in el); + if ( !isSupported ) { + el.setAttribute(eventName, "return;"); + isSupported = typeof el[eventName] === "function"; + } + el = null; + + return isSupported; + }; + + jQuery.support.submitBubbles = eventSupported("submit"); + jQuery.support.changeBubbles = eventSupported("change"); + // release memory in IE root = script = div = all = a = null; })(); diff --git a/test/delegatetest.html b/test/delegatetest.html new file mode 100644 index 0000000..c580b29 --- /dev/null +++ b/test/delegatetest.html @@ -0,0 +1,123 @@ + + + + + + +

Change Tests

+ + + + + + + + + + + + + + + + + + + + + +
+ Change each: + + + + + + +
+ + +
+ +
+ + +
+ + + + $().bind('change')
Results:SELECTMULTICHECKBOXRADIOTEXTTEXTAREADOCUMENT
+

Submit Tests

+ + + + + + + + + + + + + + + +
+ Submit each: + +
+ +
+
+
+ +
+
+
+ +
+
$().bind('submit')
Results:TEXTPASSWORDBUTTONDOCUMENT
+ + + + + diff --git a/test/unit/event.js b/test/unit/event.js index 65b08af..1ad7c3d 100644 --- a/test/unit/event.js +++ b/test/unit/event.js @@ -820,20 +820,143 @@ test(".live()/.die()", function() { jQuery('span#liveSpan1').die('click'); }); -test("live with submit", function() { - var count = 0; +test("live with change", function(){ + var selectChange = 0, checkboxChange = 0; - jQuery("#testForm").live("submit", function() { - count++; - return false; + var select = jQuery("select[name='S1']") + select.live("change", function() { + selectChange++; + }); + + var checkbox = jQuery("#check2"), + checkboxFunction = function(){ + checkboxChange++; + } + checkbox.live("change", checkboxFunction); + + // test click on select + + // first click sets data + if ( !jQuery.support.changeBubbles ) { + select[0].selectedIndex = 1; + select.trigger("keyup"); + } + + // second click that changed it + selectChange = 0; + select[0].selectedIndex = select[0].selectedIndex ? 0 : 1; + select.trigger(jQuery.support.changeBubbles ? "change" : "click"); + equals( selectChange, 1, "Change on click." ); + + // test keys on select + selectChange = 0; + select[0].selectedIndex = select[0].selectedIndex ? 0 : 1; + select.trigger(jQuery.support.changeBubbles ? "change" : "keyup"); + equals( selectChange, 1, "Change on keyup." ); + + // test click on checkbox + checkbox.trigger(jQuery.support.changeBubbles ? "change" : "click"); + equals( checkboxChange, 1, "Change on checkbox." ); + + // test before activate on radio + + // test blur/focus on textarea + var textarea = jQuery("#area1"), textareaChange = 0, oldVal = textarea.val(); + textarea.live("change", function() { + textareaChange++; + }); + + if ( !jQuery.support.changeBubbles ) { + textarea.trigger("focus"); + } + + textarea.val(oldVal + "foo"); + textarea.trigger(jQuery.support.changeBubbles ? "change" : "blur"); + equals( textareaChange, 1, "Change on textarea." ); + + textarea.val(oldVal); + textarea.die("change"); + + // test blur/focus on text + var text = jQuery("#name"), textChange = 0, oldTextVal = text.val(); + text.live("change", function() { + textChange++; + }); + + if ( !jQuery.support.changeBubbles ) { + text.trigger("focus"); + } + + text.val(oldVal+"foo"); + text.trigger(jQuery.support.changeBubbles ? "change" : "blur"); + equals( textChange, 1, "Change on text input." ); + + text.val(oldTextVal); + text.die("change"); + + // test blur/focus on password + var password = jQuery("#name"), passwordChange = 0, oldPasswordVal = password.val(); + password.live("change", function() { + passwordChange++; }); + + if ( !jQuery.support.changeBubbles ) { + password.trigger("focus"); + } + + password.val(oldPasswordVal + "foo"); + password.trigger(jQuery.support.changeBubbles ? "change" : "blur"); + equals( passwordChange, 1, "Change on password input." ); + + password.val(oldPasswordVal); + password.die("change"); + + // make sure die works - jQuery("#testForm input[name=sub1]")[0].click(); - jQuery("#testForm input[name=T1]").trigger({type: "keypress", keyCode: 13}); + // die all changes + selectChange = 0; + select.die("change"); + select[0].selectedIndex = select[0].selectedIndex ? 0 : 1; + select.trigger(jQuery.support.changeBubbles ? "change" : "click"); + equals( selectChange, 0, "Die on click works." ); + + selectChange = 0; + select[0].selectedIndex = select[0].selectedIndex ? 0 : 1; + select.trigger(jQuery.support.changeBubbles ? "change" : "keyup"); + equals( selectChange, 0, "Die on keyup works." ); + + // die specific checkbox + checkbox.die("change", checkboxFunction); + checkbox.trigger(jQuery.support.changeBubbles ? "change" : "click"); + equals( checkboxChange, 1, "Die on checkbox." ); +}); + +test("live with submit", function() { + var count1 = 0, count2 = 0; - equals(2, count); + jQuery("#testForm").live("submit", function(ev) { + count1++; + ev.preventDefault(); + }); + + jQuery("body").live("submit", function(ev) { + count2++; + ev.preventDefault(); + }); + + if ( jQuery.support.submitBubbles ) { + jQuery("#testForm input[name=sub1]")[0].click(); + equals(count1,1 ); + equals(count2,1); + } else { + jQuery("#testForm input[name=sub1]")[0].click(); + jQuery("#testForm input[name=T1]").trigger({type: "keypress", keyCode: 13}); + equals(count1,2); + equals(count2,2); + } jQuery("#testForm").die("submit"); + jQuery("body").die("submit"); }); test("live with focus/blur", function(){ -- 1.7.10.4