After posting my first technical blog post, Effectively detecting user input in JavaScript, I felt like I needed to expand on a few things that I didn’t cover. For instance, the fact that input is a bubbling event and propertychange is not, or that detecting support for input is made extra difficult by Firefox. Instead of expanding that article, I decided I would write a snippet to try and normalize the event a little better. I chose jQuery as a base for the code, mostly because I wanted to learn more about writing custom events for jQuery, but partly because I knew it would make a lot of things easier. The result is the input event plug-in below.
I wasn’t really sure where to start, but Googling turned up Ben Alman’s excellent jQuery special events post which tells you more or less everything you need to know. After that, I was able to create a basic plug-in that would provide an interface for jQuery.fn.bind("input"). The plug-in decides between attaching to input, propertychange or a host of fall back events that may or may not be supported in the browser. In short, it tries to get as much of the functionality of input as possible, but there’ll always be at least some functionality, for instance, plain keyboard input detection.
Note that the plug-in does have the following limitations in browsers that don’t support input:
- There’s no “catch-all-changes” fallback. I had considered running a timer, when an element is focused or moused over, that constantly checks for value changes. However, this would trigger when the value of the element is changed programmatically, which may not be desired.
- Because IE’s
propertychangetriggers when the value of an element is changed programmatically, that browser suffers from the problem described by the limitation above this one. It is possible that an infinite loop could be caused by always changing the value of the element in the event handler. A workaround, if you need to do this, might be to temporarily unbind whilst you change the value, rebinding after changing. - It currently only works with input and textarea elements. I may be able to get some support in for contentEditable elements in the future, but they don’t fire a
propertychangeevent in IE when typing or pasting, which makes them awkward for capturing input. Firefox, Opera and IE9 don’t support the event on contentEditable elements anyway. It will not work withEvent delegation works as of version 1.1live()ordelegate(). This is due to how thepropertychangeevent doesn’t bubble.It doesn’t work around the bugs Opera has with the input event. I’m not sure which versions are affected and it would be a pain to figure it all out.The plugin should now provide the best level of compatibility possible in all versions of Opera.
Changelog
- v1.1: Oct 24, 2011 — Fixed many browser implementation issues, introduced
live/delegatecompatibility, changed event special name totxtinput. see full post. - v1.0: Oct 18, 2010 — 1st version
The following fiddle (very basically) demonstrates the event:
[iframe http://jsfiddle.net/AndyE/sdkBs/embedded/result 99% 300px]
And here’s the full code.
/*
jQuery `input` special event v1.1
http://whattheheadsaid.com/projects/input-special-event
(c) 2010-2011 Andy Earnshaw
MIT license
www.opensource.org/licenses/mit-license.php
*/
(function($, udf) {
var ns = ".inputEvent ",
// A bunch of data strings that we use regularly
dataBnd = "bound.inputEvent",
dataVal = "value.inputEvent",
dataDlg = "delegated.inputEvent",
// Set up our list of events
bindTo = [
"input", "textInput", "propertychange", "paste", "cut", "keydown", "drop",
""].join(ns),
// Events required for delegate, mostly for IE support
dlgtTo = [ "focusin", "mouseover", "dragstart", "" ].join(ns),
// Elements supporting text input, not including contentEditable
supported = {TEXTAREA:udf, INPUT:udf},
// Events that fire before input value is updated
delay = { paste:udf, cut:udf, keydown:udf, drop:udf, textInput:udf };
$.event.special.txtinput = {
setup: function(data, namespaces, handler) {
var triggerTimer,
bndCount,
changeTimer,
// Get references to the element
elem = this,
$elem = $(this),
triggered = false;
if (elem.tagName in supported) {
bndCount = $.data(elem, dataBnd) || 0;
if (!bndCount)
$elem.bind(bindTo, handler);
$.data(elem, dataBnd, ++bndCount);
$.data(elem, dataVal, elem.value);
} else {
$elem.bind(dlgtTo, function (e) {
var target = e.target;
if (target.tagName in supported && !$.data(elem, dataDlg)) {
bndCount = $.data(target, dataBnd) || 0;
if (!bndCount)
target.bind(bindTo, handler);
// make sure we increase the count only once for each bound ancestor
$.data(elem, dataDlg, true);
$.data(target, dataBnd, ++bndCount);
$.data(target, dataVal, target.value);
}
});
}
function handler (e) {
var elem = e.target;
// Clear previous timers because we only need to know about 1 change
window.clearTimeout(timer), timer = null;
// Return if we've already triggered the event
if (triggered)
return;
// paste, cut, keydown and drop all fire before the value is updated
if (e.type in delay && !timer) {
// ...so we need to delay them until after the event has fired
timer = window.setTimeout(function () {
if (elem.value !== $.data(elem, dataVal)) {
$(elem).trigger("txtinput");
$.data(elem, dataVal, elem.value);
}
}, 0);
}
else if (e.type == "propertychange") {
if (e.originalEvent.propertyName == "value") {
$(elem).trigger("txtinput");
$.data(elem, dataVal, elem.value);
triggered = true;
window.setTimeout(function () {
triggered = false;
}, 0);
}
}
else {
$(elem).trigger("txtinput");
$.data(elem, dataVal, elem.value);
triggered = true;
window.setTimeout(function () {
triggered = false;
}, 0);
}
}
},
teardown: function () {
var elem = $(this);
elem.unbind(dlgtTo);
elem.find("input, textarea").andSelf().each(function () {
bndCount = $.data(this, dataBnd, ($.data(this, dataBnd) || 1)-1);
if (!bndCount)
elem.unbind(bindTo);
});
}
};
// Setup our jQuery shorthand method
$.fn.input = function (handler) {
return handler ? this.bind("txtinput", handler) : this.trigger("txtinput");
}
})(jQuery);
Thanks to: Nick Craver, Tim Down, Ben Alman and Daniel Friesen. Please post any issues you come across in the comments.