The Ashes
Creating a querySelector for IE that runs at “native speed”
Hello Ajaxians, my name is Paul Young and I am the co-founder of Skybound Software. We’re the company behind Stylizer, which is a real-time CSS editing tool. We’re taking a pretty radical approach to CSS editing, and as such, a lot of what I do is “web technology research”, which is looking for better ways of doing things with a web browser, ultimately so that Stylizer can automate more of the web development process. This has allowed me the chance to discover a few things that have had significant impact on a web developer’s workflow, as was the case with State Scope Image Replacement.
In conducting some research for a future version of Stylizer I found a way to create a querySelector method for IE 7 and lower. querySelector
is a very useful feature of newer web browsers. It takes a CSS selector as string parameter and returns an array containing the HTML elements that match the selector. Unfortunately it doesn’t work in IE 7 and lower… until now 🙂
My querySelector
technique works in IE7 back to IE4 (although I only tested it back to version 6). It’s only 327 characters minified, and runs ultra-fast because it doesn’t parse strings or traverse the DOM. Keep in mind though that it’s using the CSS selector engine built into the browser, which means no funky CSS 3 selector features will work, as they weren’t implemented old versions of IE.
In order to explain how it works, I’ll need to first cover something else I discovered: Single Execution CSS Expressions (SECEs). These are an enormously powerful technique that unfortunately has been made impossible in IE8. We’re using these extensively in the built-in CSS reset feature in Stylizer to eradicate bugs and to add critical but missing features in IE 6 and 7.
About CSS Expressions
The problem with IE’s CSS expressions feature is that because they’re recalculated hundreds of times per second, they’re notoriously slow. Fortunately, I’ve found a way to ensure they’re only executed once per matched element. Examine the code below:
-
-
DIV { -singlex: expression(this.singlex ? 0 : (function(t) { alert(t.tagName); t.singlex = 0; } )(this)); }
-
There are 3 things you need to know to understand how this works:
- CSS expressions are executed regardless of whether the CSS property name is valid. So “-singlex” is just an arbitrary name.
- CSS expressions are executed on every matched element. So in the case above, on every div on the page.
- Inside a CSS expression, the “this” keyword refers to the current matched HTML element.
- The code above checks to see if the matched element has a property called “singlex”. If it does, it just returns 0. Otherwise, it executes an inline function, passing it a reference to the matched element. Inside that function, we can perform whatever processing we want. At the end, we set a flag on the element to ensure that the function won’t be executed again.
This is how you can do complex processing inside a CSS expression without having to worry about a performance hit. The function only executes on the first execution of the expression, and then on every subsequent execution, 0 is returned instantly. The performance cost of running an if test and returning 0 is negligible, even if you’re doing it thousands of times per second. If you paste that CSS into a page with 3 div elements on it, in IE it will alert “DIV” 3 times.
The things you can do with SECE’s are basically limited only by your imagination. Here is an example of using one to fake the CSS content property in IE 7 and lower:
-
-
/* Newer browsers */ DIV:after { content: “Generated content!”; } /* IE 7 and lower */ DIV { -singlex: expression(this.singlex ? 0 : (function(t) { t.innerHTML += “Generated content!”; this.singlex = 0; } )(this)); }
-
You can use SECEs to fake all sorts of things like generated content, min-width and min-height, CSS counters, CSS outlines, and more. But most importantly, you can…
Create a querySelector method with Single Execution CSS Expressions!
Creating a querySelector
method is just a matter of dynamically adding a SECE to a page that copies a reference to each matched HTML element into a globally accessible array, and then returning that array. Examine the code below:
-
-
/*@cc_on if (!document.querySelector) document.querySelector = function(selector) { // Add a new style sheet to the page var head = document.documentElement.firstChild; var styleTag = document.createElement("STYLE"); head.appendChild(styleTag); // Create a globally accessible element array document.__qsResult = []; // Create the SECE that copies all matched // elements to document.__qsResult. styleTag.styleSheet.cssText = selector + "{qs: expression(this.__qs?0:(function(t){document.__qsResult.push(t);t.__qs=0;})(this));}"; // Reflow the page. Without this, the SECE won’t execute. window.scrollBy(0, 0); // Clean up and return head.removeChild(styleTag); return document.__qsResult; } @*/
-
There you have it! The function is wrapped in a conditional compilation comment to make sure only IE 7 and lower can see it. Here is a minified version for convenience:
-
-
/*@cc_on if(!document.querySelector)document.querySelector=function(s){d=document;h=d.documentElement.firstChild;t=d.createElement("STYLE");h.appendChild(t);d.__q=[];t.styleSheet.cssText=s+"{x:expression(this.__q?0:(function(t){document.__q.push(t);t.__q=0;})(this));}";window.scrollBy(0, 0);h.removeChild(t);return d.__q;}@*/
-
I hope you can make use of SECEs and this querySelector method. Maybe it even has a place in jQuery? I don’t know. (John Resig, are you reading? 🙂
UPDATE
After thinking this through a little further, I realized that you don’t need to use a SECE at all for this, just a CSS expression added with JavaScript will do. Also, one of the commenters (Jordan1) pointed out that the code wouldn’t return elements that had already been queried once. I’ve posted an update to the code below that should rectify the issue:
-
-
/*@cc_on if (!document.querySelector)
-
document.querySelector = function(selector)
-
{
-
var head = document.documentElement.firstChild;
-
var styleTag = document.createElement("STYLE");
-
head.appendChild(styleTag);
-
document.__qsResult = [];
-
-
styleTag.styleSheet.cssText = selector + "{x:expression(document.__qsResult.push(this))}";
-
window.scrollBy(0, 0);
-
head.removeChild(styleTag);
-
-
var result = [];
-
for (var i in document.__qsResult)
-
result.push(document.__qsResult[i]);
-
return result;
-
}
-
@*/
-
NOTE: Fancy writing a guest post for Ajaxian? Got some tips on content that we haven’t covered? Please email us, or tweet me 🙂
No Comments