Commit d42afd0f authored by Justin Meyer's avatar Justin Meyer Committed by jeresig

Adding in support for bubbling submit and change events, thanks to the 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.
parent c6a0211e
...@@ -63,13 +63,7 @@ jQuery.event = { ...@@ -63,13 +63,7 @@ jQuery.event = {
var handlers = events[ type ], var handlers = events[ type ],
special = this.special[ 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 // Init the event handler queue
if ( !handlers ) { if ( !handlers ) {
...@@ -78,7 +72,7 @@ jQuery.event = { ...@@ -78,7 +72,7 @@ jQuery.event = {
// Check for a special event handler // Check for a special event handler
// Only use addEventListener/attachEvent if the special // Only use addEventListener/attachEvent if the special
// events handler returns false // 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 // Bind the global event handler to the element
if ( elem.addEventListener ) { if ( elem.addEventListener ) {
elem.addEventListener( type, handle, false ); elem.addEventListener( type, handle, false );
...@@ -87,7 +81,15 @@ jQuery.event = { ...@@ -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 // Add the function to the element's handler list
handlers[ handler.guid ] = handler; handlers[ handler.guid ] = handler;
...@@ -109,7 +111,7 @@ jQuery.event = { ...@@ -109,7 +111,7 @@ jQuery.event = {
return; return;
} }
var events = jQuery.data( elem, "events" ), ret, type; var events = jQuery.data( elem, "events" ), ret, type, fn;
if ( events ) { if ( events ) {
// Unbind all events for the element // Unbind all events for the element
...@@ -133,12 +135,14 @@ jQuery.event = { ...@@ -133,12 +135,14 @@ jQuery.event = {
var namespaces = type.split("."); var namespaces = type.split(".");
type = namespaces.shift(); type = namespaces.shift();
var all = !namespaces.length, 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 ] || {}; special = this.special[ type ] || {};
if ( events[ type ] ) { if ( events[ type ] ) {
// remove the given handler for the given type // remove the given handler for the given type
if ( handler ) { if ( handler ) {
fn = events[ type ][ handler.guid ];
delete events[ type ][ handler.guid ]; delete events[ type ][ handler.guid ];
// remove all handlers for the given type // remove all handlers for the given type
...@@ -152,7 +156,7 @@ jQuery.event = { ...@@ -152,7 +156,7 @@ jQuery.event = {
} }
if ( special.remove ) { if ( special.remove ) {
special.remove.call( elem, namespaces ); special.remove.call( elem, namespaces, fn);
} }
// remove generic event handler if no more handlers exist // remove generic event handler if no more handlers exist
...@@ -402,10 +406,12 @@ jQuery.event = { ...@@ -402,10 +406,12 @@ jQuery.event = {
}, },
live: { live: {
add: function( proxy, data, namespaces ) { add: function( proxy, data, namespaces, live ) {
jQuery.extend( proxy, data || {} ); 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 ) { remove: function( namespaces ) {
...@@ -422,7 +428,8 @@ jQuery.event = { ...@@ -422,7 +428,8 @@ jQuery.event = {
jQuery.event.remove( this, namespaces[0], liveHandler ); jQuery.event.remove( this, namespaces[0], liveHandler );
} }
} }
} },
special: {}
} }
} }
}; };
...@@ -542,40 +549,110 @@ jQuery.each({ ...@@ -542,40 +549,110 @@ jQuery.each({
}; };
}); });
(function() { // submit delegation
jQuery.event.special.submit = {
var event = jQuery.event, setup: function( data, namespaces, fn ) {
special = event.special, if ( !jQuery.support.submitBubbles && this.nodeName.toLowerCase() !== "form" ) {
handle = event.handle; jQuery.event.add(this, "click.specialSubmit." + fn.guid, function( e ) {
var elem = e.target, type = elem.type;
special.submit = {
setup: function(data, namespaces) { if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) {
if(data.selector) { return trigger( "submit", this, arguments );
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 ); 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 ) {
event.add(this, 'keypress.specialSubmit', function( e, eventData ) { return trigger( "submit", this, arguments );
if(jQuery(e.target).filter(":text, :password").closest(data.selector).length) { }
e.type = "submit"; });
return handle.call( this, e, eventData ); }
}
}); return false;
} else { },
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) { // always want to listen for change for trigger
event.remove(this, 'click.specialSubmit'); return false;
event.remove(this, 'keypress.specialSubmit'); },
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 // Create "bubbling" focus and blur events
jQuery.each({ jQuery.each({
...@@ -750,12 +827,19 @@ jQuery.fn.extend({ ...@@ -750,12 +827,19 @@ jQuery.fn.extend({
function liveHandler( event ) { function liveHandler( event ) {
var stop = true, elems = [], selectors = [], args = arguments, 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); live = jQuery.extend({}, jQuery.data( this, "events" ).live);
for ( j in live ) { for ( j in live ) {
if ( live[j].live === event.type ) { fn = live[j];
selectors.push( live[j].selector ); 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 { } else {
delete live[j]; delete live[j];
} }
...@@ -796,7 +880,9 @@ function liveHandler( event ) { ...@@ -796,7 +880,9 @@ function liveHandler( event ) {
} }
function liveConvert( type, selector ) { 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({ jQuery.extend({
......
...@@ -91,6 +91,25 @@ ...@@ -91,6 +91,25 @@
div = null; 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 // release memory in IE
root = script = div = all = a = null; root = script = div = all = a = null;
})(); })();
......
<html>
<head>
<script src='../dist/jquery.js' type='text/javascript'></script>
<style>
.red {
background-color: red;
border: solid 3px red;
}
</style>
</head>
<body>
<h2>Change Tests</h2>
<table>
<tr>
<td>
Change each:
</td>
<td>
<select class='select_test'>
<option value='one'>change me 1</option>
<option value='two'>change me 2</option>
<option value='three'>change me 3</option>
</select>
</td>
<td>
<select class='mselect_test' multiple="multiple">
<option value='one'>change me 1</option>
<option value='two'>change me 2</option>
<option value='three'>change me 3</option>
</select>
</td>
<td>
<input type="checkbox" class="checkbox_test" name="mycheckbox" id="checkbox1"/>
<label for="checkbox1">Checkbox 1 label</label><br/>
<input type="checkbox" class="checkbox_test" name="mycheckbox" id="checkbox2"/>
<label for="checkbox2">Checkbox 2 label</label>
</td>
<td>
<input type="radio" class="radio_test" name="myradio" id="radio1"/>
<label for="radio1">Radio 1 label</label><br/>
<input type="radio" class="radio_test" name="myradio" id="radio2"/>
<label for="radio2">Radio 2 label</label>
</td>
<td>
<input class='test' value='' id='input' size='10' />
</td>
<td>
<textarea rows='2'></textarea>
</td>
<td>$().bind('change')</td>
</tr>
<tr>
<td>Results:</td>
<td id='select' class="red">SELECT</td>
<td id='mselect' class="red">MULTI</td>
<td id='checkbox' class="red">CHECKBOX</td>
<td id='radio' class="red">RADIO</td>
<td id='text' class="red">TEXT</td>
<td id='textarea' class="red">TEXTAREA</td>
<td id='boundChange' class="red">DOCUMENT</td>
</tr>
</table>
<h2>Submit Tests</h2>
<table>
<tr>
<td>
Submit each:
</td>
<td>
<form action="" id="text_submit">
<input class='test' type='text' value='Key Return To Submit'/>
</form>
</td>
<td>
<form action="" id="password_submit">
<input class='test' type='password' value=''/>
</form>
</td>
<td>
<form action="" id="submit_submit">
<input type='submit' value="Click Me To Submit" />
</form>
</td>
<td>$().bind('submit')</td>
</tr>
<tr>
<td>Results:</td>
<td id='textSubmit' class="red">TEXT</td>
<td id='passwordSubmit' class="red">PASSWORD</td>
<td id='submitSubmit' class="red">BUTTON</td>
<td id='boundSubmit' class="red">DOCUMENT</td>
</tr>
</table>
<script type='text/javascript'>
makeChangeFunc = function(id, prevent){
return function(e){
if(prevent)
e.preventDefault();
$(id).css("backgroundColor","green").css("border","solid 3px green");
setTimeout(function(){
$(id).css("backgroundColor","");
}, 700)
}
}
$(".select_test").live("change",makeChangeFunc("#select"))
$(".mselect_test").live("change",makeChangeFunc("#mselect"))
$(".checkbox_test").live("change",makeChangeFunc("#checkbox"))
$(".radio_test").live("change",makeChangeFunc("#radio"))
$('textarea').live('change', makeChangeFunc("#textarea"))
$('#input').live('change', makeChangeFunc("#text"))
$().bind('change', makeChangeFunc("#boundChange"))
$("#text_submit").live("submit", makeChangeFunc("#textSubmit", true) )
$("#password_submit").live("submit", makeChangeFunc("#passwordSubmit", true) )
$("#submit_submit").live("submit", makeChangeFunc("#submitSubmit", true) )
$().bind('submit', makeChangeFunc("#boundSubmit"))
</script>
</body>
</html>
...@@ -820,20 +820,143 @@ test(".live()/.die()", function() { ...@@ -820,20 +820,143 @@ test(".live()/.die()", function() {
jQuery('span#liveSpan1').die('click'); jQuery('span#liveSpan1').die('click');
}); });
test("live with submit", function() { test("live with change", function(){
var count = 0; var selectChange = 0, checkboxChange = 0;
jQuery("#testForm").live("submit", function() { var select = jQuery("select[name='S1']")
count++; select.live("change", function() {
return false; 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(); // die all changes
jQuery("#testForm input[name=T1]").trigger({type: "keypress", keyCode: 13}); 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("#testForm").die("submit");
jQuery("body").die("submit");
}); });
test("live with focus/blur", function(){ test("live with focus/blur", function(){
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment