Headbank digital
Design

Stretching Panels - without tables!

This project began in the seedy underworld of Web Standards. My career on the Web was born in the age of standards, but wasn't born into it. As a result, my layout strategy depended on tables. Judicious use of tables could place any content anywhere within the page - I made whole sites contained within tables. One of their greatest powers was percentage-based rendering: make a table occupy x% of the page width, make a cell occupy y% of the row height.

So, what's the problem?

Well, it seems we are bastardising the <table> tag by using it in this way. If the Good Lord (or Sir Tim to his friends) had intended us to use them this way, he'd have no doubt called them <layoutgrid> or something. Tables are for data, saith the Lord; their wondrous stretchy properties exist to allow populating them with abstruse, dynamic data. GOT IT?

All of which left this troubled agnostic with a dilemma: how could I possibly recreate those lovely layouts without tables? OK, DIVs and CSS are purported to do all this and more, but could they really handle a complex, dynamic layout?

As a challenging starting-point, I decided to look at an old favourite: the pretty picture-frame. Here's how it worked in a table-based design:


<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="18" width="18" style="background-image:url(topLeft.gif);"></td>
<td height="18" style="background-image:url(top.gif);"></td>
<td height="18" width="21" style="background-image:url(topRight.gif);"></td>
</tr>
<tr>
<td width="18" style="background-image:url(left.gif);"></td>
<td bgcolor="#FFFFFF">
<p>Content Here</p>
</td>
<td width="21" style="background-image:url(right.gif);"></td>
</tr>
<tr>
<td height="21" width="18" style="background-image:url(bottomLeft.gif);"></td>
<td height="21" style="background-image:url(bottom.gif);"></td>
<td height="21" width="21" style="background-image:url(bottomRight.gif);"></td>
</tr>
</table>

Click here to see an example

Beautifully flexible, and (fairly) straightforward. So if we are to dispense with tables in a situation like this, what can we replace them with?

DIVs are certainly packed with potential. They can be styled eight ways from Sunday, including (crucially) their positioning. They can be inline, block, or free-floating. And their dimensions can, like tables, be rendered as an absolute or as a percentage. But these two strengths don't combine to give the kind of compound structure that makes tables so indispensable.

The table design above was designed with dynamic content in mind: the width is a percentage of the page-width, and the height is unspecified. The volume of content in the central cell, and the width of the window, can change without distorting the look of the border. The table container holds all eight surrounding cells in the right places relative to the centre.

DIVs, however, can't be meshed together in this way. Even having all nine units nested inside a tenth, container DIV couldn't put each image in the right place, tile the side images correctly, or have them all move harmoniously together when resizing. By this frustrated stage, I had reached the following conclusions:

I was pretty new to JavaScript, and still associated it with non-compliance on some level. Maybe it was borne of my very first HTML tutorial, which mentioned <script> tags only in passing, and insisted on <noscript> tags for the 'non-JavaScript' users. But I'd never actually met such a person, so on I went.

The principle was simple enough: the central DIV's rendered dimensions and position had to be read and processed to yield those of the surrounding eight, in a function that would be called by both the onLoad and onResize events. It would ideally be portable enough to be re-used by many (perhaps differently-sized) panel-sets throughout a site.

In this model we'll name the individual cell DIVs based on position, with rows a, b, c and columns 1, 2, 3 - for example, the top-left cell is named a3. Assuming there will be more than one panel-set using this function, we extend the naming convention to include a unique identifier that groups all nine cells. So for a set whose centre cell is named content1, its related top-left cell will be content1a3.

As with the table-based design, some dimension attributes can be predefined, namely the heights of rows a and c, and the widths of columns 1 and 3. Since DIV dimensions are defined by CSS style, I made a stylesheet with eight styles named a1 through c3 for the respective cells, setting the dimensions where applicable, and the background images. These two are a corner and an edge:


.a1 {
  background-image: url(topLeft.gif);
  overflow: hidden;
  position: absolute;
  height: 18px;
  width: 18px;
}

.a2 {
  background-image: url(top.gif);
  overflow: hidden;
  position: absolute;
  height: 18px;
}

The values that need to be worked out are:

Of these, midWidth and MidHeight are the easiest to assign, because they are the same as the centre cell's rendered dimensions. After much trial-and-error, I found that the offset- properties were the most reliable cross-browser measurement:


var midWidth = document.getElementById('content1').offsetWidth;

var midHeight = document.getElementById('content1').offsetHeight;

// Note: 'px' will have to be appended for acceptable CSS dimension/position
	values.

The remaining values - the tops and lefts - requires a little arithmetic. Thankfully not much, as I was a Maths dropout.


var leftLeft = document.getElementById('content1').offsetLeft
    - document.getElementById('content1a1').offsetWidth;

var midLeft = document.getElementById('content1').offsetLeft;

var rightLeft = document.getElementById('content1').offsetLeft + midWidth;

var topTop = document.getElementById('content1').offsetTop
    - document.getElementById('content1a1').offsetHeight;

var midTop = document.getElementById('content1').offsetTop;

var botTop = document.getElementById('content1').offsetTop + midHeight;

Now we just append 'px' to all of those, and they can be assigned to the DIVs. Rather than just referencing each cell by ID, we want to be able to apply the values to ANY set of cells that fit the bill. The naming convention helps here, but to select the right eight DIVs to match each centre cell, I opted to exploit a coding convention: each set of nine follow the same order on the page, and thus also in the DOM array document.getElementsByTagName('div'). So, assuming our structure of a function that takes the centre's ID as an argument, we get its index from that array:


function makePanel(div)
 {
 var allDivs = document.getElementsByTagName('div');

 for(i=0; i<allDivs.length; i++)
  if(allDivs[i].id == div)
   var theIndex = i;
 }

// As long as the other eight DIVs follow this one
// in order in the code, they can be accessed as
// a1 = allDivs[theIndex + 1]; and so on.

Rather than assign each and every value in a big list, I referred to the naming convention once again, this time using regular expressions to select sets of cells to recieve the same value.


var theDiv = document.getElementById(div);
        
for(j=theIndex+1; j<theIndex+9; j++)
 {
 //set top row tops
 if(allDivs[j].id.search(/a\d/) != -1)
  allDivs[j].style.top = (theDiv.offsetTop - allDivs[theIndex+1].offsetHeight)+'px';

 //set middle row tops and heights
 if(allDivs[j].id.search(/b\d/)!= -1)
  {
  allDivs[j].style.top = theDiv.offsetTop+'px';
  allDivs[j].style.height = theDiv.offsetHeight+'px';
  }

 //set bottom row tops
 if(allDivs[j].id.search(/c\d/)!= -1)
  allDivs[j].style.top = (theDiv.offsetTop+theDiv.offsetHeight)+'px';

 //set col1 lefts
 if(allDivs[j].id.search(/[abc]1/)!= -1)
  allDivs[j].style.left = (theDiv.offsetLeft-allDivs[theIndex+1].offsetWidth)+'px';

 //set col2 lefts and widths
 if(allDivs[j].id.search(/[abc]2/)!= -1)
  {
  allDivs[j].style.left = theDiv.offsetLeft+'px';
  allDivs[j].style.width = theDiv.offsetWidth+'px';
  }

 //set col3 lefts
 if(allDivs[j].id.search(/[abc]3/)!= -1)
  allDivs[j].style.left = (theDiv.offsetLeft+theDiv.offsetWidth)+'px';
 }

Now we have our function, there's just the matter of deploying it on the page. In cases involving several panel-sets, given that the body onload handler can only reliably execute one function, we need a simple 'wrapper' function to combine all the makePanel() calls.


function wrapper()
 {
 var allPanels = new Array('content1','content2','content3');
 for(t=0; t<allPanels.length; t++)
  makePanel(allPanels[t]);
 }

Two problems remained: for some reason the function failed in some browsers on the onresize call; and, more crucially, onresize is now deprecated in HTML 4.01 and XHTML 1.0 Transitional. My interpretation of the first problem was that the function was running before the centre-cell had been redrawn, so it would have to be put on a delay. The second took more research, but eventually I learned that the onresize event could have its handler set by script.


function delay() {
 setTimeout('wrapper()',100);
}
window.onresize = delay;

Click here to see the final result!

So there you have it. On the page, each panel-set has a considerably smaller footprint than and equivalent table-based layout, and almost everything else is re-usable code. All the JavaScript is placed in an external file, apart from the wrapper() function which is specific to each page. Similarly, one CSS file holds all the cell styles - though in practice I place most of the center-cells' styles within the page as they tend to vary in the finer details of positioning etc.

This technique is now used extensively on this very site. It could still be considered a work in progress, as I'm sure it can be made more economical. One limitation I've come across is that further DIVs can't be used inside the centre-cell without displacing the order of document.getElementsByTagName('div') and borking the function. This can be fixed simply enough though, by rewriting the HTML (and the function) to place the eight outer cells before the centre, but thankfully that can wait until the site actually needs a panel with more DIVs inside. And for my part, I could go a long time without seeing another DIV...

STOP PRESS! I learned that the panel-sets are not lining up with their centres in IE (Mac) or Safari: absolutely- and relatively-positioned DIVs are affected respectively. So, after further hair-pulling, I concieved this 'fixer' addition to the makePanel() function. Sadly, there is no such property in the DOM as element.style.position, so it required adding an additional (optional) argument to the function to specify when the centre-cell's positioning is relative. This last part was necessary because Safari renders document.body.offsetTop as 14px, whereas it seems to be 0px for all others, and that 14px is included in a relative-positioned element's offsetTop - but not in that of an absolute-positioned element.


 var correctLeft = allDivs[theIndex].offsetLeft - allDivs[theIndex+1].offsetWidth
		- allDivs[theIndex+1].offsetLeft;
if (arguments[1] == 'rel')
 correctLeft += document.body.offsetLeft;
if (correctLeft != 0)
 for(k=theIndex+1; k<theIndex+9; k++)
  {
  var oldLeft = allDivs[k].style.left.replace('px','');
  allDivs[k].style.left = (parseInt(oldLeft)+correctLeft)+'px';
  }

var correctTop = allDivs[theIndex].offsetTop - allDivs[theIndex+1].offsetHeight
- allDivs[theIndex+1].offsetTop;
if (arguments[1] == 'rel')
 correctTop += document.body.offsetTop;
if (correctTop != 0)
 for(k=theIndex+1; k<theIndex+9; k++)
  {
  var oldTop = allDivs[k].style.top.replace('px','');
  allDivs[k].style.top = (parseInt(oldTop)+correctTop)+'px';
  }

Sort of a brute-force solution: find the difference between where the panel-set actually is, and where it should be, and add it to the previously-calculated positions. Pleasingly, browser- or element-sniffing has been almost completely avoided, which makes the script relatively future-proof. So, what's left? Tell me what you think, and be in on the further optimization of this script!