Being Firebug, pt2: which rules, which elements?
Ever wondered how Firebug, Dragonfly and other debug tools manage to detect which styles apply to an element, whether they're currently active, what stylesheet (if any) they're coming from, and other such info?
In this second part of a two-part blog post, I'll be showing you how to do just that.
You read part 1, right?
Before we begin, make sure you read part one, which I posted last week, as this post continues from there and assumes you understood what I was waffling on about there.
I read it - so now what?
So you read part one - therefore you understand how Javascript can read the CSS rules and styles coming into a page from stylesheets, and how it can also read 'computed styles' on a given element.
Now we're going to look at how to see which CSS rules apply to a given element - and which of its styles are active or have been overridden.
Detecting if a rule applies to an element
This would be horrible to do in native Javascript. Possible, but horrible. Thankfully, an unsuspecting jQuery method, .is(), is going to save us a hell of lot of code writing.
If you're not familiar with .is(), it returns true or false based on whether the element(s) on the jQuery stack matches the selector you pass to it. So for example:
1$('div').is('div'); //obviously returns true
2$('#someElement').is(':visible'); //returns true if #someElement is visible
Pretty simple. So how does that help with what we're doing? Well, as we saw in part one, Javascript's DOM CSS Rule object allows us to access not only the styles of a particular rule but also the selector text, i.e. the part before the {.
If we pass that to .is() on the element we're interested in, we find out if the rule applies to that element. Pretty neat, eh? Observe.
1//grab an element
2var el = document.getElementById('myElement');
3
4//grab the first stylesheet loaded into the page
5var sheet = document.styleSheets[0];
6
7//get its rules (remember, approach depends on browser)
8var rules = sheet.rules ? sheet.rules : sheet.cssRules;
9
10//we'll log in an array the selector text of the rules that apply to the element
11var rulesThatApply = [];
12
13//iterate over the rules
14for (var o=0, numRules = rules.length; o 15 16 //see if the rule targets our element - if it does, log it in our array 17 if ($(el).is(rules[o].selectorText)) 18 rulesThatApply.push(rules[o].selectorText);
Note I'm skipping the validation - i.e. checking that el actually finds an element with ID 'myElement', and that there really is a stylesheet[0] - just to keep the code succinct, but you'd obviously want to check for these things in a real-world environment.
If you console.log our rulesThatApply array, you'll see that it contains the selector text of the rules that apply to our element, just as we'd hoped. So far, so good.
In case you missed it...
See what we did there? See how the .is() method is they key? The method, as I mentioned above, returns true or false depending on whether the element(s) in the jQuery stack matches - in other words, is targetable by - the selector you pass to the method.
That is NOT to say that the CSS rule's selector text must match exactly the jQuery selector we use to target the element (if we use jQuery at all; in the above code, I target an element natively) - rather, that it must ultimately select the same element. So for example:
1//JS
2var el = $('#topNav ul');
1/* CSS */
2ul { border: solid 1px #f90; }
When our code runs, and the loop gets to that rule, we are effectively asking:
el.is('ul'); //true, of course, so the rule applies to this element
Detecting if a style is active or overridden
Detecting what rules are talking to an element is one thing. But that does not mean, of course, that all the styles of a particular rule are active on the element - they may have been overridden by other rules or by inline styles.
The key here is to test the implicitly-set CSS value of the style against the computed - i.e. current - value of the style. If they match, it means the CSS style is still in effect. It they don't, it has been overridden.
Simple, right? Well, pretty much, but there are a few pitfalls.
First, colours. You probably set colours in your CSS via HEX syntax - however most browsers return computed colours in RGB format. So if you set #f90 in your CSS, when you read it back from the computed styles you'll be told rgb(255, 153, 0).
Secondly, you'll want to harmonise string format before attempting comparison. The most obvious example here is the dashes-vs-camel-case thing going on between CSS and JS - i.e. text-decoration, but textDecoration in JS.
So, let's get to work. For demo purposes, let's work with just one CSS rule and its styles.
1//grab an element
2var el = document.getElementById('myElement');
3
4//grab the first stylesheet loaded into the page
5var sheet = document.styleSheets[0];
6
7//get the sheet's first rule
8var firstRule = (sheet.rules ? sheet.rules : sheet.cssRules)[0];
The first two parts of that are exactly the same as we did in the code earlier in this post. The last line simple grabs the first rule of the stylesheet. This returns to us a DOM CSS Rule object. This is an object of the various styles that the rule sets, e.g. color.
Let's iterate over these styles and, for each, compare it to the current, computed style. As I mentioned above, we'll need to harmonise the values so they are comparable. The most complex part of this is colours - we'll convert them all to HEX format.
1//create an object in which we'll log which styles are active and which have been overridden
2var styles = {active: [], overridden: []};
3
4for (var g=0; g 5 6 //get the name of the style and harmonise to JS format, lowercase 7 var styleName = firstRule.style[g].replace(/\-(\w)/g, function($0, $1) { return $1.toUpperCase(); }); 8 9 //get this style value and the current, computed equivalent 10 var styleVal = firstRule.style[styleName]; 11 var currEquiv = el.currentStyle ? el.currentStyle[styleName] : getComputedStyle(el, null)[styleName]; 12 13 //if it's a colour style, harmonise to HEX format 14 if (styleName.indexOf('color') != -1) { 15 styleVal = RGBToHex(styleVal); 16 currEquiv = RGBToHex(curr&quiv); 17 } 18 19 //lastly, log in our feedback object whether the style is active or overriden 20 styles[styleVal == currEquiv ? 'active' : 'overridden'].push(styleName); 21 22}
You might be wondering where that RGBToHex function comes from. Well, I wrote it - and here it is, so you'll need this, too.
1function RGBToHex(RGBStr) {
2 var rgb = RGBStr.match(/\d+/g), hexStages = '0123456789ABCDEF';
3 if (rgb && rgb.length == 3) {
4 if (rgb[i] < 0 || rgb[i] > 255) return false;
5 var ret = '#';
6 for (var i in rgb) ret += hexStages.substr(Math.floor(rgb[i] / 16, 1), 1)+hexStages.substr(rgb[i] % 16, 1);
7 } else
8 ret = RGBStr;
9 return ret.toLowerCase();
10},
That function is beyond the scope of this article, but hopefully it's fairly simple in what it does. Basically, you feed it an RGB colour (e.g. rgb(255, 0, 0) or even just 255,0,0) and it returns the colour in HEX format. If the colour is already in HEX format it merely returns what you give it, unchanged.
So, what did we just do?
Let's look at a few things in that code. As I said, in order to see if a style is active, we need to compare it to the current, computed equivalent. This means harmonising two values:
- Line 7: the name of style, from dashes to camel case, e.g. text-align to textAlign
- Lines 15/16: colours into HEX format, because most browsers return computed colours in RGB format (even if you set them in HEX!)
So there you have it
A lot to take in, but hopefully you've got an idea of how the likes of Firebug and Dragonfly do their thing.
Oh and do remember that, like all code that deals with the DOM, you'll want to use the code from this post inside a window.onload callback or, if using jQuery, inside a document ready handler.
3 comments | post newBeing Firebug, pt1: reading CSS in Javascript
Ever wondered how Firebug, Dragonfly and other debug tools manage to detect which styles apply to an element, whether they're currently active, what stylesheet (if any) they're coming from, and other such info?
In a two-part post, I'll be looking at how that's done in Javascript. Yes, it's all Javascript.
I find many junior-to-intermediate JS developers are surprised to find out JS has a rich API for this sort of thing (given Javascript's limited API generally).
In this first post I'll be looking at how you read the styles loaded into your page. In the second post, in the coming week, I'll look at how you can then work out which styles apply to a particular element - and whether they're currently active or have been overridden by another style.
The API
There's two key parts here: one to detect rules and styles from loaded stylesheets (or inline tags), and another to detect current styles - i.e. the current styles active on an element, from whatever source.
The latter are called computed styles - i.e. the current style, from whatever source.
Getting computed styles
As is often the case, this is a case of Microsoft vs. the rest. Non-IE browsers define a function, getComputedStyle, which accepts two arguments, but you'll nearly always pass only one (more on that later) - the first one - which should be a reference to an element. IE, however, defines an object on each element's prototype, currentStyle, which contains sub-properties for each current style.
Let's get the computed colour of the first paragraph in a page. As ever in a situation like this, where there's differing approaches for different browser, a spot of feature detection is in order:
1var para = document.getElementsByTagName('p')[0];
2var colour = para.currentStyle ? para.currentStyle.color : getComputedStyle(para, null).color;
That should be pretty obvious what's happening - if element.currentStyle is supported, we use that - else we use getComputedStyle().
Sidenote: I actually prefer IE's way of doing things here. I know, I know, it's not often you hear a developer say that. But it just seems so much more sensible that, since these properties are, by their nature, dynamic, and not unchangably set at runtime, they live ON the element. The non-IE way of defining a function instead seems at odds with Javascript's prototypal inheritance philosophy. After all, plenty of other dynamic properties live on the elements themselves, in all browsers, such as offsetHeight.
Getting CSS rules/styles
Javascript has an API for reading the rules and styles of every linked stylesheet or inline tag in your page. It does this through the document.styleSheets object.
alert('There are '+document.styleSheets.length+' stylesheets or style tags in this page');
Each element of the object represents a linked stylesheet or document.styleSheets in your page. Go a level deeper, and you can access the rules and styles within. Again, however, there's differences in the name of this property between IE and the rest: cssRules in non-IE browsers, and rules in IE.
Let's get the total styles for the whole page, into a nice, tidy array in the following format:
1var css = [
2 {
3 href: 'some/stylesheet.css',
4 rules: {
5 '#some .thing a': {
6 color: '#FF0000'
7 }
8 }
9 }
10]
Hopefully the structure is pretty obvious: an array containing a sub-object for each stylesheet/style tag, which in turn contains its href (or, if inline, simply 'inline') and a rules sub-object, named after the selector text of the rule, and which contains the styles/values themsleves. So let's get it on:
1//set up a log
2var css = [];
3
4//iterate over stylesheets and style tags
5for (var u=0, numSheets = document.styleSheets.length; u 6 7 //make a shortcut alias to this sheet or tag 8 var sheet = document.styleSheets[u]; 9 10 //log this sheet or tag's rules (depends on browser) 11 var rules = sheet.rules ? sheet.rules : sheet.cssRules; 12 13 //make a sub-object for this sheet or style tag 14 var sheetObj = {}; 15 16 //log its href or, if inline, simply "inline" 17 sheetObj.href = sheet.href ? sheet.href : 'inline' 18 19 //add a sub-object for its rules 20 sheetObj.rules = {}; 21 22 //iterate over the rules and log each in sub-object 23 for (var o=0, numRules = rules.length; o 24 25 //make a sub-object to log this rule's its styles 26 var ruleObj = {}; 27 28 //iterate over its styles and log each in a sub-object 29 for (var i=0, numStyles = rules[o].style.length; i 30 31 //get the style's name, e.g. "color", and value 32 var styleName = rules[o].style[i], 33 styleVal = rules[o].style[styleName]; 34 35 //log this style and its value in our sub-object 36 ruleObj[styleName] = styleVal; 37 38 } 39 40 //log this rule's object in the sheet rules object, 41 //using the rule's selector text as its name 42 sheetObj.rules[[rules[o].selectorText]] = ruleObj; 43 44 } 45 46 //lastly, log this sheet's object in our css array 47 css.push(sheetObj); 48 49} 50 51//log our findings in the console 52console.log(css);
Hopefully the comments in that code block make it quite clear what's going on. Essentially, it's a case of rather unprettily drilling down, through a series of nested for loops (urgh...) from the stylesheet to the rules, to the actual styles.
Summary
So there you have it. Note that you'll need to run the above only once the window (or at least DOM) has loaded, otherwise the browser won't be ready to talk to Javascript about stylesheets just yet.
Look out for the next part in this post, later in the coming week, in which we'll see not only how to detect what styles are present in a page, but which apply to a particular element and whether they're currently active, ala Firebug, Dragonfly and other debug tools.
Oh and be sure to bookmark the Javascript API references for the DOM StyleSheet and DOM CSS rule objects.
Enjoy.
2 comments | post newAdditions to XML tree; jQuery XML parsing
You may have seen my XMLTree plugin, posted a few weeks ago.
If you'll excuse the trumpet-blowing I do find this plugin among my most useful; I'm working on a number of projects right now that involve XML in the browser, and the ability to visualise it in a traversable tree is quiet satisfying.
New features (all documented on the script page) include:
- callback on resultant HTML - you can now specify a callback function which will be invoked after the tree has been generated. It is automatically passed a DOM reference to the tree ul so you can act on it.
- attributes - can now be ignored, hidden or shown. Hiding them is useful if you plan to traverse or interrogate the tree based on their existence or value, without wanting to show them to the user.
Bug fixes
This was an interesting one. With XMLTree you can either point it at an XML file for it to load, or manually pass it a well-formed string of XML.
In the latter case, it exploits a well-known (yet somewhat abused) perk of jQuery - that you can pass it a DOM or XML string and it will turn it into a nodeset.
But because this is intended for DOM creation, not XML creation, it assumes you're parsing an HTML document, not XML. The problem? If your XML contains any tags that share their names with any self-closing HTML tags, jQuery renders your XML invalid.
Consider the following example:
1var xml = "
2console.log($(xml).html());
The output of that will be:
/>http://mitya.co.uk
...which is obviously not what you'd wanted.
As a fix for this XMLTree now renames (secretly) all of the node names - but restores their original names on output.
Ideally, of course, I wouldn't be lazy and rely on jQuery to parse XML - there are native ways (albeit different ones for different browsers). But for now this fix works just fine.
Head over here to download, get usage info or view a demo.
post a commentBAPS (browser and plugin sniffer)
BAPS, apart from being an acronym chosen purely for its its puerile humour, stands for browser and plugin sniffer - and I've just posted it in the scripts section.
As promised earlier this week, this is a utility to accurately detect information about the user's environment, such as browser name, version, language and installed plugins.
This is an area that is famously annoying, since different browsers expose different information to the Javascript's native navigator object, which exists to tell developers about the user's environment. Thus, it's never been standardised.
BAPS clears this up, as it caters for browsers' inconsistencies. It caters, for example, with the fact that Opera 11.52 declares itself as Opera 9.8. And for the fact that Chrome declares itself as, er, Netscape.
It sets a global variable, baps, which is an object containing various information about the user's environment, such as browser, version, language and plugins.
In terms of plugins, it detects Flash, WMP and QuickTime, but you can easily extend it to look for more.
Head over here to download, get usage info or view a demo.
post a comment