A few entries back, I wrote about CSS Computed Style. In that blog entry, I discussed a cross-browser javascript function to retrieve the computed style of a DOM element on the page.
If you need a quick refresher… javascript doesn't allow you to access arbitrary style properties of a DOM element via .style unless you've previously set that property using javascript OR if you've defined the style using an inline style attribute of the DOM element.
This is where CSS Computed Style can really come in handy. By retrieving an element's computed style, you can retrieve style properties just as they were when you defined them in your CSS file (regardless of whether or not you've ever set them in javascript).
But there's a gotcha (isn't there always). If you remember, IE doesn't really support computed style. Instead, IE offers .currentStyle which, while similar, is most definitely NOT the same thing.
Perhaps the most inconvenient shortcoming of .currentStyle is the fact that it fails to normalize all units to pixels, as the W3C recommends.
In this blog entry, I'm going to explain how to write a function that will convert these non-pixel values to pixels for you.
Read on to learn more.
Writing a convertToPixels(..) function is actually rather tricky. When I first set out to do this, I failed to account for the fact that many of the units we deal with in modern CSS are relative units. For example, em, ex, or % are all units that have no meaning without context.
As you'll see shortly, this has serious implications for the technique I'm going to use for this function.
The basic concept:
The basic concept for this function is fairly simple. We'll pass in a string to our function, something like '5em' or '12pt'. We'll then create an invisible <div> element, set either its height or its border, add it to the page, and then measure it using .offsetHeight to get a pixel value.
The full code for the function is below, with an explanation to follow:
if (!sstchur.web) { sstchur.web = {}; }
if (!sstchur.web.xb) { sstchur.web.xb = {}; }
sstchur.web.xb.convertToPixels = function(_str, _context)
{
if (/px$/.test(_str)) { return parseInt(_str); }
var tmp = document.createElement('div');
tmp.style.visbility = 'hidden';
tmp.style.position = 'absolute';
tmp.style.lineHeight = '0';
if (/%$/.test(_str))
{
_context = _context.parentNode || _context;
tmp.style.height = _str;
}
else
{
tmp.style.borderStyle = 'solid';
tmp.style.borderBottomWidth = '0';
tmp.style.borderTopWidth = _str;
}
if (!_context) { _context = document.body; }
_context.appendChild(tmp);
var px = tmp.offsetHeight;
_context.removeChild(tmp);
return px + 'px';
};
The first thing you should notice about this function is that we first test whether or not the string that was passed into the function is already in pixels units (i.e. it ends with 'px'). If it does, we'll simply return that value right back (no sense "converting" what doesn't need conversion.
Next, we create a <div> div element and set a number of style properties:
.style.visibilityto 'hidden' so that this element never shows up on the page..style.positionto 'absolute' so that the element won't shift other elements around on the page..style.lineHeightto '0' because IE will add mysterious white-space to our element that will throw off our measurement if we don't.
The next part is a little tricky. If the units we're trying to convert have been specified as a percentage, then we need to set the height of our invisible <div>, and adjust the _context element to be .parentNode of the _elem that was passed in (% generally always means "percent of the parent element"). However, if the units are anything else, then it's best to leave our element's height as 0, and set it's top border only.
Why? Well, trying to set the border of an element as a percentage isn't going to work. You might think then, that we should just use the height technique all the time (I thought that at first). The problem with this however, is that IE seems to think the a non-specified border is 'medium'. Trying to set the height of an element to 'medium' isn't going to be very useful.
The long and short of it is this: A border can be almost anything (px, em, ex, pt, a keyword, like 'medium' or 'large') but it can't be a percentage, so if we're dealing with a percentage, it's best to set the height and leave the border alone.
{ tmp.style.height = _str; }
Now, if the unit is not a percentage, then we'll use the border technique. This means setting the element's top border to the specified value and explicitly setting the bottom border to 0 (we're not interested in the right or left borders so we can safely ignore them). We also need to remember to set the border style to 'solid' or else this technique won't work as expected.
{
tmp.style.borderStyle = 'solid';
tmp.style.borderBottomWidth = '0';
tmp.style.borderTopWidth = _str;
}
We're almost ready to measure our invisible element, but there's one thing we must do first.
If the units we're trying to convert are relative units, then the equivalent pixel value depends upon context. In other words, '10%' when specified on an element that is 200px would be 20px. But that same '10%' when specified on an element that is 500px would be 50px. If we want the right value, we need to consider this context.
That is why convertToPixels(..) takes an optional second parameter, _context. If this parameter is not specified, we'll default to the <body> element, but if it is specified, we'll use it.
The preceding line of code ensures that the _context variable won't be null (and thus wreak havoc on our function).
Finally, we append the element to the _context element, measure its .offsetHeight, remove it from _context and then return the value we just measured.
var px = tmp.offsetHeight;
_context.removeChild(tmp);
return px + 'px';
Why add the + 'px' to the value you ask? Well, the major goal here is to make the cross-browser getComputedStyle(..) function we wrote to act more like a W3C style function when used in IE. Since W3C browsers would return the value with 'px' tacked on to the end, we'll go ahead and do this manually in our function so that we remain consistent.
Now that our convertToPixels(..) function is complete, it would be good if we modified getComputedStyle(..) to take advantage of it.
We'll do that in Part 2.
Pages: 1 2
7 Responses
This has been very helpful. Thanks!
This is perfect: I'm writing a image replacement solution that will benefit hugely from this. Thanks for sharing it!
This returns height of the element in pixels.
In firefox 2.0.0.14 and IE 7.
document.getElementById('div1').scrollHeight
It seems to me that the browsers are implemented the way to complicate the things as much as possible. You must cheat all the time
)
Vlado,
Thanks for the tip. I've only run across .scrollHeight once or twice and haven't really utilized it much. Do you know if it give the overall height of an element, including borders and padding? Or just the internal height of the element (minus borders and padding)?
What does IE6 think of this property?
if _context is optional and units are percentual, function raise an error when calling convertToPixels('75%')
in this line:
_context = _context.parentNode || _context;
great stuff! Now… How do I convert from pixels to percent? I have a div within a div, and I want to find the percent the inner div is of the outerdiv. I tried (slice/pie)*100; and then set the innerdiv to that percent, but sometimes it's off by a pixel.
Joe:
This is a little bit tricky because you have to consider the fact that when you specify a dimension as a percentage in CSS, it's a percentage of the parent's STYLE height (that is, its height as specified in CSS). Bear in mind that this could be rather different from its rendered height on screen.
For instance, if you specify that an element have height: 400px, but then also give it 10px padding all around, and a 20px border all around, the total height (offsetHeight) will actually be 60px more (30px on top and bottom). However, that extra 60px doesn't come into play when calculating the height of some child element specified as a percentage.
What you really want then is the style height of the outer element (that is, you need the height in pixels of the outer element as specified in the CSS). And to further complicate your life, that value might not be specified in px, and if you're in IE, then you'll need to convert it to px
And you'll need the same value of the inner element. Probably, your favorite Javascript library has a way of getting the height (or width) of an element as specified in CSS (note that reading .style.height will not work if the height was not specified via .style.height or via an inline style attribute).
I don't know how to do this in jQuery or Prototype or Dojo, but in Gimme (my library) it would look like:
var innerElemHeight = parseInt(Gimme('#innerElem').get_style('height'));
var outerElemHeight = parseInt(Gimme('#outerElem').get_style('height'));
var pct = innerElemHeight / outerElemHeight;
Hope this helps!