Displaying a Table With Frozen Headers |
This is a 'minimal fuss' implementation that relies on moving and clipping the CSS boxes representing the column and row headers to reflect the position of the scrollbars in the main data table.
The layout looks as follows:
+---------------------+ | ColumnHeaders table | +-----------+---------------------+ | RowHeaders| Data table | | table | | +-----------+---------------------+
where all three (HTML) tables are encapsulated in a common container (with CSS attribute position:relative, so that the whole construction can be positioned in the 'flow' of the document).
The Data table has class 'data' which has the CSS position attribute set to 'relative', ensuring scrollbars if the data do not fit in the allocated box.
The RowHeaders (class 'row-headers') and ColumnHeader (class 'column-headers') tables are 'scrolled' programmatically by a javascript 'scroll_headers' function which is described below.
The main idea is that e.g. 'scrolling' the ColumnHeaders table is accomplished by
ROW_HEADERS.WIDTH | The width of the box displaying the RowHeaders table. |
COLUMN_HEADERS.HEIGHT | The height of the box displaying the ColumnHeaders table. |
DATA.WIDTH | The width of the box displaying the Data table |
DATA.HEIGHT | The height of the box displaying the Data table |
DATA_TABLE_TOTAL_WIDTH | The 'real' width of the Data table. |
/* It is important to have 'table-layout' fixed, at least for * the Data and ColumnHeaders tables. This way, the browser will * compute the size of the columns from the total width of the * table and the number of columns. Hence, one should ensure * that * Data.width = ColumnHeaders.width * and, naturally, that * Data and ColumnHeaders have the same number of columns. */ table { table-layout: fixed; } td { color:blue; } th { color:red; } /* The whole table sits in a container which has position relative * so that coordinates of the table are relative to this * container's box. */ div.container { position: relative; } /* The 'overflow:hidden' attribute specification ensures that the * headers do not extend to the right of their allocated window. * The 'left' attribute is such that it leaves room for the * row-headers to the left of the column-headers. * The left/top/height/width attributes define a 'viewing box' * for the ColumnHeader table: in particular, the 'width' attribute * limits the number of visible columns. Note that * column-headers.width == data.width * and * column-headers.height == row-headers.top == data.top * should be true. */ div.column-headers { position:absolute; overflow:hidden; left:ROW_HEADERS.WIDTHpx; top:0px; height:COLUMN_HEADERS.HEIGHTpx; width:DATA.WIDTHpx; } /* Similar to the row-headers style. Here the 'height' attribute * is important: * row-headers.height == data.height and * Also, * row-headers.width == column-headers.left == data.left == RowHeaders.width * should hold. */ div.row-headers { position:absolute; overflow:hidden; left:0px; top:COLUMN_HEADERS.HEIGHTpx; height:DATA.HEIGHT; width:ROW_HEADERS.WIDTHpx; } /* Style for the actual data table. The 'overflow:auto' attribute * ensures that a scrollbar will appear if necessary. * The values for the left/top/height/width attributes * follow from the above equalities. */ div.data { position:absolute; overflow:auto; left:ROW_HEADERS.WIDTHpx; top:COLUMN_HEADERS.HEIGHTpx; height:DATA.HEIGHT; width:DATA.WIDTHpx; }
/* This function handles scrolling events by adjusting the display * of row-headers or column-headers. */ function scroll_headers() { var h_delta = document.getElementById("Data").scrollLeft /* The data table was scrolled (horizontally) by h_delta px. * Conceptually, this means that * the box containing the data has moved to the left by h_delta px. * We do the same with the ColumnHeaders by * - setting the width of its box to data.width + h_delta * - moving the left border of its box to data.left - h_delta * The clipping window (specified by the 'clip' style attribute) * of the ColumnHeaders should however stay * 'in the same place'. But, since the clip area * rect(top, right, bottom, left) * is defined relative to the top left corner of the box, * it should be redefined to stay in the same area of the * screen. Specifically, the 'left' border of the clipping * area is now at h_delta px and the 'right' border is * at data.width + h_delta . The 'top' and 'bottom' * border of the clipping area should be constant * where top = 0px and bottom = column-headers.height. * * Note that firefox (2.0.0.2) does not seem to handle * statements like * xxx.style.clip.left = * but instead expects a CSS type specification of the * form * xxx.style.clip = "rect(top, right, bottom, left)" */ document.getElementById("ColumnHeaders").style.width = (DATA.WIDTH+h_delta) + "px" document.getElementById("ColumnHeaders").style.left = (ROW_HEADERS.WIDTH-h_delta) + "px" document.getElementById("ColumnHeaders").style.clip = "rect(0px,"+(DATA.WIDTH+h_delta)+"px,"+"COLUMN_HEADERS.HEIGHTpx,"+h_delta+"px"+")" /* The data table was scrolled (vertically) by v_delta px. * Conceptually, this means that * the box containing the data has moved up by v_delta px. * We do the same with the RowHeaders by * - setting the height of its box to data.height + v_delta * - moving the top border of its box to data.top - v_delta * The clipping window (specified by the 'clip' style attribute) * of the RowHeaders should however stay * 'in the same place'. But, since the clip area * rect(top, right, bottom, left) * is defined relative to the top left corner of the box, * it should be redefined to stay in the same area of the * screen. Specifically, the 'top' border of the clipping * area is now at v_delta px and the 'bottom' border is * at data.height + v_delta . The 'left' and 'right' borders * of the clipping area should be constant, i.e. * left = 0px, right = row-headers.width. */ var v_delta = document.getElementById("Data").scrollTop document.getElementById("RowHeaders").style.height = (DATA.HEIGHT+v_delta) + "px" document.getElementById("RowHeaders").style.top = (COLUMN_HEADERS.HEIGHT-v_delta) + "px" document.getElementById("RowHeaders").style.clip = "rect("+ v_delta + "px," +"ROW_HEADERS.WIDTHpx,"+ (DATA.HEIGHT+v_delta) + "px,"+"0px"+")" }
<div class="container"> <div class="data" id="Data" onscroll="scroll_headers()"> <table border="1px" width="DATA_TABLE_TOTAL_WIDTHpx"> .. </table> </div> <div class="column-headers" id="ColumnHeaders"> <table border="1px" width="DATA_TABLE_TOTAL_WIDTHpx"> .. </table> </div> <div class="row-headers" id="RowHeaders"> <table border="1px" width="ROW_HEADERS.WIDTHpx"> .. </table> </div> </div>
Another, more convenient and probably more powerful, but also much more resource-hungry, complicated and heavy approach to the problem can be found at Cross-Browser.com. In one aspect, the present approach is more general, though: the row headers table can contain more than one column, and similarly for the column headers table.