January 14, 2010
If you look at jQuery 1.3 documentation for live
method you will notice that
the live method is not supported for following events: blur, focus, mouseenter,
mouseleave, change and submit .
jQuery 1.4 fixed them all.
In this article I am going to discuss how jQuery brought support for these
methods in jQuery. If you want a little background on what is live
method and
how it works then you should read
this article
which I wrote sometime back.
IE and other browsers do not bubble focus
and blur
events. And that is in
compliance with the w3c events model. As per the
spec
focus event and blur event do not bubble.
However the spec also mentions two additional events called DOMFocusIn
and
DOMFocusOut
. As per the
spec
these two events should bubble. Firefox and other browsers implemented
DOMFocusIn/DOMFocusOut . However IE implemented focusin
and focusout
and IE
made sure that these two events do bubble up.
jQuery team decided to pick shorter name and introduced two new events:
focusin
and focusout
. These two events bubble and hence they can be used
with live method.
This commit
makes focusin/focusout work with live method. Here is code snippet.
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);
},
};
function handler(e) {
e = jQuery.event.fix(e);
e.type = fix;
return jQuery.event.handle.call(this, e);
}
});
}
Once again make sure that you are using focusin/focusout
instead of
focus/blur
when used with live
.
mouseenter
and mouseleave
events do not bubble in IE. However mouseover
and mouseout
do bubble in IE. If you are not sure of what the difference is
between mouseenter
and mouseover
then
watch this excellent
screencast by Ben.
The fix that was applied to map for focusin
can be replicated here to fix
mousetner
and mouseleave
issue.
This is the commit
that fixed mouseenter and mouseleave issue with live method.
jQuery.each(
{
mouseenter: "mouseover",
mouseleave: "mouseout",
},
function (orig, fix) {
jQuery.event.special[orig] = {
setup: function (data) {
jQuery.event.add(
this,
fix,
data && data.selector ? delegate : withinElement,
orig
);
},
teardown: function (data) {
jQuery.event.remove(
this,
fix,
data && data.selector ? delegate : withinElement
);
},
};
}
);
Two more events are left to be handled: submit and change. Before jQuery applies fix for these two events, jQuery needs a way to detect if a browser allows submit and change events to bubble or not. jQuery team does not favor browser sniffing. So how to go about detecting event support without browser sniffing.
Juriy Zaytsev posted an excellent blog titled Detecting event support without browser sniffing . Here is the short and concise way he proposes to find out if an event is supported by a browser.
var isEventSupported = (function () {
var TAGNAMES = {
select: "input",
change: "input",
submit: "form",
reset: "form",
error: "img",
load: "img",
abort: "img",
};
function isEventSupported(eventName) {
var el = document.createElement(TAGNAMES[eventName] || "div");
eventName = "on" + eventName;
var isSupported = eventName in el;
if (!isSupported) {
el.setAttribute(eventName, "return;");
isSupported = typeof el[eventName] == "function";
}
el = null;
return isSupported;
}
return isEventSupported;
})();
In the comments section John Resig mentioned that this technique can also be used to find out if an event bubbles or not.
John committed following code to jQuery.
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");
Next task is to actually make a change event or a submit event bubble if ,based on above code, it is determined that browse is not bubbling those events .
On a form a person can change so many things including checkbox, radio button, select menu, textarea etc. jQuery team implemented a full blown change tracker which would detect every single change on the form and will act accordingly.
Radio button, checkbox and select changes will be detected via change event. Here is the code.
click: function( e ) {
var elem = e.target, type = elem.type;
if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) {
return testChange.call( this, e );
}
},
In order to detect changes on other fields like input field, textarea etc
keydown
event would be used. Here it the code.
keydown: function( e ) {
var elem = e.target, type = elem.type;
if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") ||
(e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
type === "select-multiple" ) {
return testChange.call( this, e );
}
},
IE has a proprietary event called beforeactivate
which gets fired before any
change happens. This event is used to store the existing value of the field.
After the click or keydown event the changed value is captured. Then these two
values are matched to see if really a change has happened. Here is code for
detecting the match.
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);
}
}
Here is the commit that fixed this issue.
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 );
}
},
// 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;
if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") ||
(e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
type === "select-multiple" ) {
return testChange.call( this, e );
}
},
// 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 ) {
for ( var type in changeFilters ) {
jQuery.event.add( this, type + ".specialChange." + fn.guid, changeFilters[type] );
}
return formElems.test( this.nodeName );
},
remove: function( namespaces, fn ) {
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;
}
In order to detect submission of a form, one needs to watch for click event on a submit button or an image button. Additionally one can hit 'enter' using keyboard and can submit the form. All of these need be tracked.
jQuery.event.special.submit = {
setup: function (data, namespaces, fn) {
if (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);
}
});
}
},
remove: function (namespaces, fn) {
jQuery.event.remove(
this,
"click.specialSubmit" + (fn ? "." + fn.guid : "")
);
jQuery.event.remove(
this,
"keypress.specialSubmit" + (fn ? "." + fn.guid : "")
);
},
};
As you can see if a submit button or an image is clicked inside a form the submit event is triggered. Additionally keypress event is monitored and if the keyCode is 13 then the form is submitted.
live method is just pure awesome. It is great to see last few wrinkles getting
sorted out. A big Thank You
to Justin Meyer of
JavaScriptMVC who submitted most of the patch for
fixing this vexing issue.
If this blog was helpful, check out our full blog archive.