jQuery MSIE7 Attribute Traversal/Clone Crash Bug

I’ve been spending some time writing a client-side JavaScript library (more on this in a later post) that does a fair amount of DOM manipulation and has had its share of cross-browser, erm, idiosyncrasies. The foundation of the library depends rather heavily on jQuery for traversal, DOM insertion and removal, and the likes. It’s also fully unit-tested and therefore it’s relatively trivial to wire-in another browser, run up the unit tests, and see what works (or doesn’t). Unsurprisingly, every browser except most flavors of MSIE have worked without issues, but many of the MSIE issues I encountered have relied on minor workarounds for missing features or misbehavior that’s more noisome than frustrating.

That is to say until I discovered it’s possible to crash MSIE7 using a mix of traversal, detaching, and cloning. Needless to say, the unit tests barely finished, and I was left surprised. I’ve since fixed the problem, but it’s nevertheless been a curio in need of a simplified test case.

First, I want to point out that I suspect this may be tangentially related to this bug, although I’m reluctant to call it a jQuery “bug” considering that it works fine in every other version of MSIE (including 6 and ever other browser I’ve tested). Furthermore, since the release of jQuery 2.x drops support for MSIE 6, 7, and 8, the importance of addressing this “bug” is largely moot. Most applications, particularly single-page client-side applications that perform a great deal of work directly on the DOM, target newer browsers anyway. Only those with a large or predominantly non-technical audience ought to worry, and many of those sites likely don’t make use of much JavaScript outside advertising and analytics.

The bug manifests itself whenever the user manipulates the .data() method on an element and subsequently detaches that element and clones it. Actually, that’s a lie: Detaching and .data() manipulation can occur in any sequence. The important bit to take from this is that any manipulation of .data() on an element that has been or will be detached and further cloned will duplicate this behavior. Here’s a short example.

Given the HTML:

<div class="crash-me">Loop over this node's data, detach it, and clone it
to crash MSIE7.</div>

And the JavaScript (using jQuery):

var $element = $(".crash-me");
 
// Looping over data elements. The crash will occur even if they're empty.
$.each($($element[0]).data(), function(key, value){
  $("#values").append(key+": "+value+"<br>");
});
 
// Now to detach.
$element.detach();
 
// Crash.
for (var i = 0; i < 5; i++) {
  // You won't see the clones in MSIE7 'cause it's dead.
  // Technically, you only need the $element.clone() call.
  $("#nodes").append($element.clone());
}

You can also replace the $.each with a for-loop to the same effect:

var attributes = $element[0].attributes;
$($element[0]).data("test", 1);
for (var i = 0; i < attributes.length; i++) {
  // You only need to access nodeName or nodeValue to do this horrible deed.
  if (attributes[i].nodeName) {
    $("#values").append(attributes[i].nodeName+": "+attributes[i].nodeValue+"<br>");
  }
}

The difference, however, is that the $.each loop will trigger the crash whether or not the inner function does anything (since the nodes’ attributes have already been accessed); the for-loop requires that you do something with attributes, such as fetching the nodeName or nodeValue. This is essentially what $.each does anyway, so the two methods accomplish roughly the same thing.

The workaround for this is to simply avoid touching .data() on any node that is (or will be) detached from the DOM and operate only on the clones. In my case, I have a traverse() method that traverses over all DOM elements, looking for data-* attributes, and then examines their children for more data-* elements. To avoid this bug, I have a flag for the attribute handlers that indicates whether or not it detaches its children; if it does, traverse() simply ignores it and carries on with its business. It’s up to the handler to call traverse() on the clones of its detached children, never operating directly on the original children themselves. Since each handler that relies on traversal uses the same traverse() method for DOM manipulation, it’s easy to manage the fix in a single location.

Of course, if you have little choice but to examine the data attributes of a detached node before cloning them, you’re be out of luck. Fortunately, MSIE7 usage is declining, and if you’re outside the financial sector or government, you should be safe! My condolences otherwise.

No comments.
***

jQuery Autosuggest

So a week or two ago, I was looking around for a decent autosuggest plugin that had behavior similar to WordPress’ tag manager. The best one I found required jQuery UI which, for various reasons, I didn’t want to use. Furthermore, I needed a plugin that had some extensibility and could differentiate between delimited items.

Failing in my searches, I decided to write my own. You can download it from here. You’ll also need the supporting CSS, but it should be fairly easy to draft your own once you see mine. Your backend script will need to supply a JSON-encoded response roughly as follows (PHP-based example):

echo json_encode(array(array('text' => 'Match 1', 'id' => '1'), array('text' => 'Match 2', 'id' => 2), array('text' => 'Match 3', 'id' => 3)));

I’ll be posting a more feature complete back-end example shortly.

XSuggest usage is also fairly basic. Simply bind it to a text input field and pass in some useful options. Here’s an example:

HTML:

<style type="text/css">
.search {
    background: #eaeaea;
    border: 1px solid #ddd;
    border-radius: 5px;
    padding: 10px 20px;
}
</style>
<div class="search">
  <input type="text" name="search" />
</div>

JavaScript:

(function($){
    $(document).ready(function(){
 
        $("input[name=search]").suggest({
            searchUrl: "search.php"
        });
 
    });
})(jQuery);

searchUrl is the only required option that you must pass to the back end. There’s quite a few other options that can control the behavior of XSuggest, although it isn’t complete, and I expect to add a few more–particularly event handlers. The options you can currently set are:

  • searchUrl – URL of the back end script that supplies a JSON-encoded response listing matching data for XSuggest.
  • searchVariable – Search variable to attach the user’s input to. This is sent via GET (query string) normally, unless you override it; the default is s
  • searchData – Optional data to send along with the search. You may use this to tweak the back end’s response.
  • searchMethod – Request method. Default: GET.
  • cssClass – CSS class of the dropdown field. Default: xsuggest-dropdown. You may change this to anything you want, but be sure to update the CSS you’re using. You have to use this class–that’s not negotiable. It’s used for selectors as well as styling.
  • activate – Function called whenever the user activates the control by pressing enter. This is useful for overriding the form’s default behavior and adding the matched text to the page, similar to what WordPress does with tags. The function can optionally accept one argument containing the jQuery object representing the source control (i.e. your input field). This is useful for reading the field’s contents. One example might be: activate: function(element){$(".search").append("
    "+element.val()+"

    ");.

  • append – Allow the control to append data from the user’s input. Mostly useful for tags.
  • appendChar – Character to use for appending user input to the control. Default: “,”.
  • I’ll have more complete documentation and a better tutorial posted later. This plugin does have some potential performance issues (see source) and a few other bugs. For now, it’s pretty good for basic usage, but it might do weird things on high latency connections.

    Enjoy.

    No comments.
***

Detecting IE6 with jQuery (sort of)

…and not using $.browser.

Or how to do a dirty deed with IE’s own quirks

Okay, so I admit two things. First, I hate MSIE6 with a passion; IE7 isn’t much better; IE8 is an improvement (unless it’s in quirks mode, but let’s not talk about that). Second, I came upon this after my initial searching turned up nothing. Once I got this trick to work, I was convinced this shouldn’t work and that there had to be a better way of doing it. So, I did a little more digging and then stumbled upon the very last response to a similar predicament on Stack Overflow. The poster obviously didn’t check their reply, because two fragments that look like they should have had example code are blank. It’s worth reading, though, and it gave me comfort in knowing that someone else was doing the same naughty things I was!

Either way, it didn’t matter–I figured out a separate way to trick MSIE without having to litter the page with new CSS classes or make any unusual modifications to the DOM. Better yet, my solution gives you some programmatic control. It does rely on a similar trick, however, and should probably be taken with a grain of salt. Of course, there’s about a dozen different ways you could do this, including many more that others have undoubtedly pointed out over the years (the earliest reference I’ve found in my searches goes back to 2008, but good luck finding them through Google).

Here it is:

<script type="text/javascript">
var ie6 = false;
$(document).ready(function(){
    // Do your stuff here.
 
    if (ie6) {
        // Do IE6-specific stuff here.
    }
});
</script>
<!--[if IE 6]>
<script type="text/javascript">
ie6 = true;
</script>
<![endif-->

Obligatory Disclaimer

Obviously, this isn’t the best solution. If you’re having to do weird things specifically to support a browser that is well on its way out of this world, you’re probably doing it wrong. There probably is a better solution (really!), but I can think of a handful of legitimate use cases where some trick like this is a necessity. For instance: Displaying some fancy code for people still running MSIE6 telling them to upgrade. Maybe even displaying a virtual sympathy card for being a corporate user chained to the dark past… :)

This has nothing to do with…

…jQuery? Nope, it doesn’t. I’m aware of that. However, because there are so many similar questions (“how do I detect browser X…”) related to jQuery and very few answers that actually provide a working solution that doesn’t include the deprecated $.browser methods, I figured that it couldn’t hurt to write something that has the words “detect,” “ie6,” and “jQuery” all in the same post title.

No comments.
***