RSS
 

Update: HTML5 oninput event plugin for jQuery

24 Oct

About a year ago, I wrote my first special events plugin for jQuery — a cross-browser implementation of the HTML5 oninput event. A while afterwards, though, it became apparent that there were a multitude of issues with browsers that already implemented the event.

  • Opera 10.x doesn’t fire the event for cut, paste, undo & drop operations on <input> elements. They fixed this in Opera 11, though.
  • Safari 4.x and lower don’t support the event for <textarea> elements. The textInput event is supported on them for 3.1+, however.
  • Internet Explorer 9 forgets to fire the event when the backspace key is used to delete text. Other than that majorly horrendous oversight, they almost got it right.
  • Firefox has issues that make feature detection very difficult. In fact, detection is only possible via another Firefox proprietary feature, which I suppose is rather lucky.

With these problems stacking up, my code was stacking up too — how could I detect and work around each of these problems? On top of that, I couldn’t easily make the event work with delegate() or live(). Soon, it became apparent that I was going to have to completely rethink the approach I made in the original plugin.

Feature detection

The original code used feature detection to discover whether browsers supported the event. "oninput" in document.documentElement is enough for this in most browsers, but the aforementioned Firefox issues required an excellent little snippet from Daniel Friesen, which I slightly modified. It was a shame though, as this extra work bulked up the plugin a little more than I would have liked. Safari didn’t like our feature detection either, because even though our snippet told us the browser implemented support for the event, it doesn’t tell us that <textarea> won’t fire it.

I eventually decided that feature detection wasn’t going to work — graceful degradation, on the other hand, might. This involves binding to all known events that fire when an input element is modified, and then setting a flag to tell the event handlers whether or not they need to execute. For events that fire pre-DOM modification, we set a timer with timeout value of 0ms (this will be adjusted to browser’s minimum allowable value). The events that fire post-DOM modification cancel this timer. So, let’s say our browser is Internet Explorer 6, we have some text in our clipboard and we just pressed Ctrl+V in a <textarea> that has our events bound. For the Ctrl key, the onkeydown event sets the timer but when it executes the value hasn’t changed, so the handler does nothing. For the V key, the plugin will handle the events in the following sequence:

Pre-DOM modification
    onkeydown -> timer is set
    onpaste   -> previous timer is cleared, new timer is set

Post-DOM modification - DOM is modified, textarea's value is updated but element hasn't repainted yet
    onpropertychange -> timer is cleared, the handler is triggered

Opera 9, however, the best possible compatibility we can provide is via the onkeydown event, so that sets the timer which executes almost instantly, triggering our special event handler. In Safari 4, the textInput event fires post-DOM modification, so that clears the timer and triggers the handler, giving seamless `oninput` compatibility to the developer.

Late binding

No, not that kind of late binding. In order to mitigate the lack of event bubbling for our Internet Explorer polyfill, I opted to have the plugin not bind the input events to the element right away if it wasn’t an <input> or <textarea>. Instead, it binds several bubbling events — onfocusin, onmouseover and ondragstart — to the element. When one of those events fires, event.target.tagName is checked and if it is “INPUT” or “TEXTAREA” then we bind our input events to it. This allows delegate() to work really well in IE and I suspect, but haven’t verified, this is how jQuery is able to offer event delegation for non-propagating events such as onresize.

Problems with the new approach

Undoubtedly, the biggest problem is naming the special event. I can’t think of a way to have a special event named `input` that binds to the native `input` event and completely overrides it because, when we come to trigger the event later, we can’t. In my oh-so finite wisdom, I decided that the best solution I could provide was a differently named special event. So, when using the plugin now, you need to bind to "txtinput". $(selector).input(function () {}); will still work, though.

Another problem was teardown. Suppose we have a DOM structure like body -> div -> input — now, what happens if we bind to the <body> and <div> elements? Actually, this works completely as expected but, what if we unbind from one of those elements? Our teardown function needs to make sure that no other handlers are relying on our bound events before we remove them. The solution makes use of data and, every time an event is bound, increases an internal count to keep track. This counter is decreased for each of those elements in the teardown.

You can see the updated plugin at the original project page.

 

Tags: , , ,

Leave a Reply

 

 
  1. Jack

    November 6th, 2011 at 8:03 am

    I have been struggling with the spotty support of this event. Thanks for the informative write-up.

     
  2. Brian S.

    April 30th, 2012 at 3:12 pm

    Thanks for sharing your nice work on this. One question. On my page I dynamically add inputs (type=text) and in my document ready function I try to set it up such that with this I can handle the txtinput event not only for the inputs that exist when the page loads, but also for inputs that are added after the page loads:

    $(‘input[type=text]‘).on(‘txtinput’, function(e) { … }

    This technique is working for me with other events like keypress, focusin, etc, but is not working for the txtinput event. The result is that while I can handle txtinput events for existing controls, I’m not able to catch and handle that event for inputs that are added after the page loads.

    I also tried repeating that line of code:

    $(‘input[type=text]‘).on(‘txtinput’, function(e) { … }

    each just I add a new input, just after where that input is added, but I still get the same behavior.

    Where am I going wrong? Any tips most appreciated!!

    Thanks,
    Brian S.

     
  3. Francois P.

    October 15th, 2013 at 12:25 pm

    Thanks for sharing your work. Unfortunately, it seems that the link to the project page is broken, is there any way you could fix it ?

    Thanks,
    Francois P.