Home & blog  /  2011  /  Dec  /  view post  /

Being Firebug, pt2: which rules, which elements?

posted: 12 Dec '11 18:27 tags: css, stylesheet, Firebug, Dragonfly

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<numRules; 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<firstRule.style.length; 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.

post new comment

Comments (3)

Dan Rathbone, at 31/01/'12 12:26, said:
Thanks for this post, it's helped me think differently about the problem I'm trying to solve :)

I may be giving firebug, dragonfly et al too much credit here but I believe there is something they do that your approach doesn't cater for. For sure your approach can find out which CSS rules match up with a given element's computed style but it can report false positives. If you consider the case where multiple rules in the cascade match a given element yet issue the same style value, how do you know which rule _actually_ applied its style?

For example:

<style>
  div {color: red }
  div.blah {color: red }
</style>
<div class="blah">hello</div>

Clearly the word hello is in red but because of the higher specificity of the second rule, that is the rule that turned it red, not the first rule. Your approach (if I've understood it correctly) will report that *both* of these rules apply to the element. The correct answer is that the first rule is overridden and the second rule has been applied.

For this frivolous case it may not actually matter (the div is still red regardless) but it feels like there are more edge cases out there that might have more serious consequences.

All this said, I do not know how these DOM inspectors solve the above case or even if they do (I haven't proven it with a test case). I would love to find out though! Of course firebug and drgaonfly are both open source so if I have to I may well dig deeper to find out :)
Mitya, at 1/02/'12 22:16, said:
Hi Dan

Thanks for your post. You're right, I did side-step the issue of false positives, purely because I thought it would take the post too far and detract from the main points (computed style, $.fn.is() etc) that I was trying to get across.

There are a few points of note with regards the point you raise.

Firstly, you could argue that, if two rules' style values match the computed value, it is academic which is deemed to be 'active', since it amounts to the same thing.

Indeed, I'm sure I've seen browsers give varying results (I'm looking at you, IE dev tools) in cases where this arises.

The correct answer is that it's down to the CSS cascade algorithm. As you probably know, CSS rules jostle for superiority based on a cascade system that ranks them on a) specificity and b) order (in which they were loaded into the page). Therefore, Firebug, Dragonfly et al would presumably implement this algorithm against the rules.

Therefore, the rule with the highest cascade value would be deemed active, but, as I say, this is really just an academic point.

Thanks for raising it, though!
Andy
Dan Rathbone, at 11/02/'12 12:14, said:
Hi Andy,

Thanks for the follow-up. I think you're probably right about DOM inspector tools having to implement the cascade rules. The more I think about it and look into the options the more I think this is true! It just seems wrong for them to have to do this though. The browser engine is sat there with the rules already written into it and moreover already _applied_ to the page, yet poor old Dragonfly has to re-implement the rules itself. 

I'm still waiting to discover some magic DOM method that exposes the inner workings of the applied cascade rules but as yet it has not revealed itself! :)

Dan