jQuery and Cufon Don't Mix!
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)
If this post was helpful to you please help pass the word by liking it, thanks all!
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
Paul reported this as a bug:
http://github.com/jeresig/sizzle/issues/#issue/13
I created a simple testcase:
https://gist.github.com/8c5702fb5ef57c4bc811
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