Archive for March 27th, 2009

The Firefox caption bug is still present in version 37, in 2015.

<table> elements are among the most frustrating to use if you are trying to style them with Javascript, because their sizes are so unpredictable and poorly controlled with CSS. I was trying to set the size of a table (a calendar, if you must know) by setting the size of each <td> and hoped the surrounding <div> would expand to fit (the way a div containing any other, fixed size, element would) but the table was shrunk to the smallest possible size. Compare:


<div style="width: 150px; overflow: visible; background:#fedcba;border: 5px solid purple">This is 150px wide</div>
<div style="width: 150px; overflow: visible; border: 5px solid purple">
	<table border="1">
		<caption>A simple table</caption>
		<tbody>
			<tr><td style="width: 200px">1</td><td style="width: 200px">2</td></tr>
		</tbody>
	</table>
</div>
<div style="width: 150px; overflow: visible; border: 5px solid purple">
	<div style="width: 400px; background: #fedcba">a simple div 400px wide</div>
</div>

Notice how the lower <div> has width: 400px and actually is 400px wide; the surrounding <div> is 150px wide but has overflow: visible so you can see the bigger <div> extending past it. The <table>, on the other hand, even though it has two <td>s that are supposed to be 200px wide, is shrunk to fit the enclosing <div>. Not good.

The only solution I found was to calculate the width that the <table> should have had, and set the width of the enclosing <div> to that value. But how to find the "natural" width of the table? We have to set the size of all the enclosing elements to something huge so the table will not be shrunk, get the width of the <table>, then set everything back. And, to make things harder, the table has to be visible in order to get its width; all the parameters like offsetWidth and clientWidth are set to zero if the table is not visible. And that includes the case where any of the parent elements have display: none, so jQuery's innerWidth hack (set the CSS of the element itself to {display: 'block'; visible: 'hidden'}, get the offsetWidth, then switch the CSS back) won't work; it only affects the visibility of the element itself.

Making the <table> itself have position: absolute temporarily solves the shrinking problem but not the visibility problem.

The key was to use jQuery's undocumented $.swap function. It is what jQuery itself uses to calculate sizes of hidden elements. It takes an element, a set of CSS properties and a callback, and temporarily sets the element to have those CSS properies, calls the callback, then restores the CSS. Now all I have to do is use that recursively:


function truewidth (e, parent){
	if (arguments.length == 1) parent = e.parentNode;
	var ret = 0;
	// we assume the table is in the document, so we eventually reach <body>, and we assume the body is visible
	if (!parent || parent.nodeName.toLowerCase() == 'body') return e.offsetWidth;
	$.swap (
		parent,
		{ position: 'absolute', width: '9999px' , display: 'block' },
		function() { ret = truewidth(e, parent.parentNode) }
	);
	return ret;
}

And I can use it:


<div style="width: 150px; overflow: visible; background:#fedcba;border: 5px solid purple">This is 150px wide</div>
<div id="widthexample" style="width: 150px; overflow: visible; border: 5px solid purple">
	<table border="1">
		<caption>A simple table</caption>
		<tbody>
			<tr><td style="width: 200px">1</td><td style="width: 200px">2</td></tr>
		</tbody>
	</table>
</div>
<div style="width: 150px; overflow: visible; border: 5px solid purple">
	<div style="width: 400px; background: #fedcba">a simple div 400px wide</div>
</div>

$('#widthexample').width(truewidth($('#widthexample table')[0]));

Table height

One more problem with tables: I wanted to use a <caption> for the calendar header (the month/year part) and wanted it to be animated when it changed, so I needed to know the height of the table so I could set the size of the containing element. Easy, no? Just $('table').height(). Unfortunately, I discovered a heretofore undocumented bug in Firefox: it does not include the caption in the height. IE, Opera, Safari and Chrome all do.

The easiest way I've found to correct for this without browser sniffing is to remove the caption, calculate the height, then put the caption back:


function trueheight(e){
	for (var child = e.firstChild; child; child = child.nextSibling){
		if (child.nodeName.toLowerCase() == 'caption'){
			e.removeChild(child);
			var h = e.offsetHeight;
			e.insertBefore(child, e.firstChild);
			return h + child.offsetHeight;
		}
	}
	return e.offsetHeight;
}

Note that if the table is hidden, you may need to use the $.swap technique above. Note also that it uses offsetHeight, which does not include the margin. I can't offhand imagine using margins on a caption, but if you do, you will have to add it back (doing $(child).outerHeight(true) is reasonable).

Demonstrating:


<div style="height: 5em">
	<div class="heightexample" style="border: 1px solid purple">
		<table border="1" style="position: absolute">
			<caption>A simple table</caption>
			<tbody>
				<tr>
					<td>the surrounding div does not</td>
					<td>correct for caption size</td>
				</tr>
			</tbody>
		</table>
	</div>
</div>
<div style="height: 5em">
	<div class="heightexample" style="border: 1px solid purple">
		<table border="1" style="position: absolute">
			<caption>A simple table</caption>
			<tbody>
				<tr>
					<td>the surrounding div corrects</td>
					<td>for caption size</td>
				</tr>
			</tbody>
		</table>
	</div>
</div>

$('.heightexample:eq(0)').height($('.heightexample:eq(0) table').height());
$('.heightexample:eq(1)').height(trueheight($('.heightexample:eq(1) table')[0]));