29 Apr
jQuery and Cufon Don't Mix!

jQuery and Cufon Don't Mix!

When using Sizzle (the selector engine in jQuery 1.3+, Prototype 1.7+, dojo, and others) and Cufon together, you need to make sure that your selectors are excluding Cufon elements. Internet Explorer 7 and Internet Explorer 8 both have a bug that cause Sizzle to break for some selectors with VML elements. I'll describe the problem, and some simple fixes, after the break.

We recently ran into a strange JavaScript error in IE 7 and 8 where the browser would inexplicably fail, and give no indication as to why. After some in-depth analysis of Sizzle and jQuery, it turns out that our font replacement script caused Sizzle to fizzle out.

The Problem

Custom fonts have always been a pain. There's plenty of ways to do it, which I won't go over here. One solution is a script called Cufon. With the use of javascript, simple things like headers and banners can be easily converted into the font of your choice. To do this, the javascript replaces the text with those vector graphics (SVG or VML) that show up on the page as new HTML entities (actually they aren't quite HTML, more on that later).

Sizzle is a very powerful framework that looks up elements using CSS selectors. We've talk a lot about it here on this blog, though we usually do so in the context of jQuery. It's selectors give Sizzle considerable power over how to manipulate elements. As an example, lets quickly dive into the ":text" selector. Below is some fake code to show kind of what it does.
function getTextElements(parent)  {
    var elems = (parent || document).getElementsByTagName('*');
    var elem;
    var text = [];
    for(var i = 0, len = elems.length; i< len; i += 1) {
        elem = elems[i];
        if('text' === elem.type) {
            text.push(elems[i]);
        }
    }
    return text;
}
The real Sizzle is more complicated, so I've simplified it here. But the core of the problem is within this code. The offending line is "'text' === elem.type". If you want to view this in Sizzle, it's located on line 524, here.

The IE7+8 bug occurs when asking a VML object for a property that has not been defined. Because Cufon uses VML in IE7+8, any time a Cufon generated element leaks into a Sizzle form selector, it will break. What's worse, this error is very difficult to detect. The browser puts out a generic error code 0 with a equally generic message of "Failed". Who would think a common selector would break a popular browser like IE7 or IE8?

What is Affected?

Which selectors are affected? Any selector that uses undefined VML properties. That is pretty much all of the input selectors. But here's a list: (with jQuery 1.4.2 error codes)

  • :enabled (Line: 82, Char: 168, Error: Failed, Code: 0)
  • :text (Line: 83, Char: 18, Error: Failed, Code: 0)
  • :radio (Line: 83, Char: 59, Error: Failed, Code: 0)
  • :checkbox (Line: 83, Char: 104, Error: Failed, Code: 0)
  • :file (Line: 83, Char: 148, Error: Failed, Code: 0)
  • :password (Line: 83, Char: 192, Error: Failed, Code: 0)
  • :submit (Line: 83, Char: 238, Error: Failed, Code: 0)
  • :image (Line: 83, Char: 281, Error: Failed, Code: 0)
  • :reset (Line: 83, Char: 323, Error: Failed, Code: 0)
  • :button (Line: 83, Char: 366, Error: Failed, Code: 0)

The Fix

So what do we do about this? Oddly enough, the fix is a performance trick. In the example above, just be specific about what element tags you are looking for. Since ":text" only applies to inputs, you can use "input:text" instead. This will yield the same results, but will do so with much quicker lookups. This is because the browser can use the native getElementsByTagName to get the specific tags. My example code is changed below.
function getTextElements(parent) {
    var elems = (parent || document).getElementsByTagName('input');
    var elem;
    var text = [];
    for(var i = 0, len = elems.length; i< len; i += 1) {
        elem = elems[i];
        if('text' === elem.type) {
            text.push(elems[i]);
        }
    }
    return text;
}
The code is almost the exact same, except now elems doesn't include things like divs, spans, paragraphs, or those pesky VMLs. Since those can't be text inputs anyways this is a considerable speed up. Generally speaking, it is a good idea to use an ID, then tag name, before any other selectors as a way to narrow down the elements before the heavy lifting by the selector engine.

I reported this to the Sizzle group two months ago, but unfortunately they haven't fixed it yet. Since these are both widely used libraries, I'm doing a write up on this to help other developers avoid this disastrous and obscure issue. If you find more selector errors like this with Cufon (or VML in general), let me know and I'll add it to the list. I suspect, but have not tested, that other VML based libraries are susceptible to the same bug (like the various canvas libraries).

Update: (2010-07-14)

After digging into this further, it turns out there are a bunch more selectors that will bomb in IE 7 and 8. I've reported it here. Here's just a few:

  • .class (Line: 80, Char: 316, Error: Failed, Code: 0)
  • [name=name] (Line: 88, Char: 381, Error: Failed, Code: 0)
  • * (Line: 91, Char: 437, Error: Failed, Code: 0)
  • [style] (Line: 86, Char: 381, Error: Failed, Code: 0)
It turns out just about every attribute selector bombs (a class is just a fancy attribute). This is really bad new for anyone who uses Cufon. My recommendation is to only use Cufon on non IE browsers. Luckily, IE has long supported custom fonts in other ways.

If this post was helpful to you please help pass the word by liking it, thanks all!
Browser Bugs, CSS, Javascript
by Paul Sayre | 4/29/2010 10:30am

Can you provide a test case so that others can test their selector engines please?

Left by Dean Edwards | Apr 30, 2010

@Dean, I spend some time this morning and created this demo file.
http://www.sitecrafting.com/files/library/4432f98952a4397b.html

Left by Paul Sayre | Apr 30, 2010

Left by John Firebaugh | May 26, 2010

I am so thankful you found this. I've been pounding my head against a wall for DAYS to sort this out.

I'm still a little clear on the fix, however. To take the situation I'm in:

I've got a div with a headline in it that is being replaced by Cufon. If I use the following code:

$j("div.product-top").click(function()
{
//click function goes here
}

...I get the error you describe above. Disabling Cufon removes the error, so I assume I'm getting the error you describe. So how do I avoid this error? Ideally, I'd like to keep the entire DIV clickable (since it's an expanding div), but then that means Sizzle will select the H2 tag and therefore the VML too, won't it?

Left by TheCosmonaut | Jul 9, 2010

@TheCosmonaut, It sounds like there is a problem with the elem.className code too. I dug into it a bit and found that, yes, it too breaks. I'm going to look into this more because I found that elem.className, elem.tagName, and elem.name all cause errors. This appears to be a bigger issue then I previously thought. I'll post an update here once I do more testing.

Left by Paul Sayre | Jul 14, 2010

I just had the exact same problem, so I went and google'd it and found this: http://www.sitecrafting.com/blog/jquery-cufon-dont-mix/ . I am very disappointed that I can't dev Cufon side by side with jQuery in IE from now on. O well. Hopefully there will be a fix soon. All of the best :)

Left by Marc Uberstein | Aug 30, 2010

:) Copy and pasted the wrong link...damn! Anyway. I actually found a way so that it works cross-browser.

The Fix:
Don't use Cufon.now.
Place all your Cufon Replace methods into 1 seperate javascript file.

Just before the closing body tag add a DIV with an ID e.g. cufonScripts. And include your javascript file that includes all your Cufon methods.

Note that I don't include the Cufon.js script on my HTML page, I include the Cufon.js script using jQuery.

In my generalCufon.js file you must add the following line of code:
$('#cufonScripts').html('[Write your Cufon.js and Font.js Includes]'); //This sets the DIV's HTML on your front end to include Cufon scripts once everything is loaded.

....so my generalCufon.js file will look like this:
$('#cufonScripts').html('[Write your Cufon.js and Font.js Includes]');

Cufon.replace('h1', {
letterSpacing: '-1px'
, forceHitArea: true
});

BREAKDOWN:
I include my generalCufon.js file that has all my Cufon script includes and methods (remember a div with ID that we will reference from generalCufon.js).
Once generalCufon.js is loaded, will only happen at the end because it is placed before closing body tag, I use jQuery to add the Cufon.js, Font.js includes and my Cufon Replace methods.

I don't have any problems anymore.
Only Con is that IE has the delay before switching to Cufon.

Enjoy Guys

Left by Marc Uberstein | Sep 10, 2010

Hi, the amount of problem ive had between jQuery and Cufon are many, however i found that removing the forceHitArea sometimes helps.

just thought i'd share that :)

Left by Nikolaj Carøe | Sep 14, 2010

I can not get this site to work in IE v8 - it works on firefox, safari and ie chrome - can someone help me please?

Left by Pam | Sep 23, 2010

I seem to have the same problem in Wordpress with Contact Form 7.. it uses a $("form").find("input:submit"). I'll try to post a fix if I ever find one..

Left by mark | Mar 23, 2011

Hi Paul,

Glad to stumble upon your post, thanks for the information! I was going crazy when having a simple setup with Cufon and jQuery Address. The latter sometimes binds submit events to links, these submit events are called by jQuery's custom 'click.specialSubmit' function, which tries to get the 'type' attribute of the element that triggered the event. When you click a VML object, MSIE7/8 will alert the well known error.

I uploaded some sample pages:
http://www.wishdesign.nl/fora/msie.vml.attribute.bug/
http://www.wishdesign.nl/fora/msie.vml.attribute.bug/jquery.htm
http://www.wishdesign.nl/fora/msie.vml.attribute.bug/jquery.address.htm

Your link to this issue committed on GitHub does not work anymore, try https://github.com/jquery/sizzle/issues/17. There we see that this problem seems solved for Sizzle by Mdumic (by checking the tagName before trying to get the 'type' attribute), but unfortunately the latest release of jQuery (v1.6.2) still checks this attribute without first testing the tagName on at least one occasion. So I reported it to jQuery: http://bugs.jquery.com/ticket/9807

Left by Laurens Meurs | Jul 12, 2011

Leave a Comment




* required    Comment Guidelines