Skip to content

Instantly share code, notes, and snippets.

@denised denised/fillRows.js
Created Mar 28, 2017

Embed
What would you like to do?
Left-justify columns in html tables (datatables implementation, generally applicable concept)
/*
* Create a justified table, while *also* enabling wrapable columns to mainain their width.
* This is not possible today with pure css, so we resort to javascript.
* This code is a tweak of the way datatables computes column widths (no way would I have been
* able to write this from scratch!).
*
* Usage: give the header of one column in the table the class 'dt-rowFiller'. As the most
* common use case, to have a natural looking, left-justified, table, add an extra empty column
* at the end, with this class. Then invoke one of the functions: fillRows() will cause the filling
* to happen a single time, while autoFillRows() watches for events that may cause table resizing and
* automatically resizes when they happen.
*
* This functionality replaces the datatable column sizing functionality, so the
* datatable should have the option autoWidth: false set. (And it probably doesn't work with any
* features or extensions that depend on autowidth.)
*
* How it works. Fillrows() works by creating a temporary, hidden, copy of part of the table,
* enough to determine it's natural width, and then using that to determine what widths all the
* columns should have. If the table is naturally as wide or wider than the space available to it,
* nothing happens. But if the table is naturally smaller than the desired width, the rowFiller
* column gets enough extra width to make the table fill it's space.
*
* Like datatables column sizing, fillRows sets these widths as style properties on the header
* cells of the table. This can cause some strange behaviors when changes happen to the table
* that make the widths no longer correct. If/when this happens, another call to fillRows()
* will fix it.
*/
/**
* @param dt datatables api instance, as returned by the DataTables constructor
*/
function autoFillRows(dt) {
var $ = jQuery;
var resize_timer;
fillRows(dt);
dt.draw('page');
// Watch for window resizing
$(window).on('resize',function() { // this gizmo waits until resize has 'stopped' to redraw
clearTimeout(resize_timer);
resize_timer = setTimeout(function() {
fillRows(dt);
dt.draw('page');
},200);
});
// Also watch for changes to column visibility
dt.on('column-visibility.dt',function() { fillRows(dt); dt.draw('page'); });
}
/*
* This is a modified copy of _fnCalculateColumnWidths
*/
function fillRows(dt) {
var
$ = jQuery,
fns = dt.context[0].oApi,
oSettings = dt.settings()[0],
table = oSettings.nTable,
columns = oSettings.aoColumns,
scroll = oSettings.oScroll,
scrollY = scroll.sY,
scrollX = scroll.sX,
visibleColumns = fns._fnGetColumns( oSettings, 'bVisible' ),
headerCells = fns._fnGetUniqueThs( oSettings, oSettings.nTHead ),
tableWidthAttr = table.getAttribute('width'), // from DOM element
tableContainer = table.parentNode,
desiredWidth,
i, column, columnIdx, width,
browser = oSettings.oBrowser,
fillerColumnIdx = -1; // in visible-column/header-cell space
for ( i=0 ; i<visibleColumns.length ; i++ ) {
if( $(headerCells[i]).hasClass('dt-rowFiller') ) {
fillerColumnIdx = i;
break;
}
}
if ( fillerColumnIdx < 0 ) {
// no filler column, no filling to do.
//console.log('no filler column');
return;
}
var styleWidth = table.style.width;
if ( styleWidth && styleWidth.indexOf('%') !== -1 ) {
tableWidthAttr = styleWidth;
}
/* Convert any user input sizes into pixel sizes */
for ( i=0 ; i<visibleColumns.length ; i++ ) {
column = columns[ visibleColumns[i] ];
if ( column.sWidth !== null ) {
column.sWidth = fns._fnConvertToWidth( column.sWidthOrig, tableContainer );
}
}
// Construct a single row, worst case, table with the widest
// node in the data, assign any user defined widths, then insert it into
// the DOM and allow the browser to do all the hard work of calculating
// table widths
var tmpTable = $(table).clone() // don't use cloneNode - IE8 will remove events on the main table
.css( 'visibility', 'hidden' )
.removeAttr( 'id' );
// Clean up the table body
tmpTable.find('tbody tr').remove();
var tr = $('<tr/>').appendTo( tmpTable.find('tbody') );
// Clone the table header and footer - we can't use the header / footer
// from the cloned table, since if scrolling is active, the table's
// real header and footer are contained in different table tags
tmpTable.find('thead, tfoot').remove();
tmpTable
.append( $(oSettings.nTHead).clone() )
.append( $(oSettings.nTFoot).clone() );
// Remove any assigned widths from the footer (from scrolling)
tmpTable.find('tfoot th, tfoot td').css('width', '');
// Apply custom sizing to the cloned header
headerCells = fns._fnGetUniqueThs( oSettings, tmpTable.find('thead')[0] );
for ( i=0 ; i<visibleColumns.length ; i++ ) {
column = columns[ visibleColumns[i] ];
if ( i == fillerColumnIdx ) {
headerCells[i].style.width = '0px';
}
else {
headerCells[i].style.width = column.sWidthOrig !== null && column.sWidthOrig !== '' ?
fns._fnStringToCss( column.sWidthOrig ) :
'';
// For scrollX we need to force the column width otherwise the
// browser will collapse it. If this width is smaller than the
// width the column requires, then it will have no effect
if ( column.sWidthOrig && scrollX ) {
$( headerCells[i] ).append( $('<div/>').css( {
width: column.sWidthOrig,
margin: 0,
padding: 0,
border: 0,
height: 1
} ) );
}
}
}
// Find the widest cell for each column and put it into the table
if ( oSettings.aoData.length ) {
for ( i=0 ; i<visibleColumns.length ; i++ ) {
if ( i == fillerColumnIdx ) continue;
columnIdx = visibleColumns[i];
column = columns[ columnIdx ];
$( fns._fnGetWidestNode( oSettings, columnIdx ) )
.clone( false )
.append( column.sContentPadding )
.appendTo( tr );
}
}
// Tidy the temporary table - remove name attributes so there aren't
// duplicated in the dom (radio elements for example)
$('[name]', tmpTable).removeAttr('name');
// Table has been built, attach to the document so we can work with it.
// A holding element is used, positioned at the top of the container
// with minimal height, so it has no effect on if the container scrolls
// or not. Otherwise it might trigger scrolling when it actually isn't
// needed
var holder = $('<div/>').css( scrollX || scrollY ?
{
position: 'absolute',
top: 0,
left: 0,
height: 1,
right: 0,
overflow: 'hidden'
} :
{}
)
.append( tmpTable )
.appendTo( tableContainer );
// remove constraints on width.
tmpTable.css( 'width', 'auto' );
tmpTable.removeAttr('width');
desiredWidth = ( tableWidthAttr || tableContainer.clientWidth );
//console.log('desired width ' + desiredWidth);
//console.log('actual width ' + tmpTable.width());
// Is the table still at least as big as it's space?
// If so, there is no filling to be done.
if ( tmpTable.width() >= desiredWidth ) {
holder.remove();
//console.log('...is less than ' + tmpTable.width() + ' so skipping');
// remove any previously assigned width.
// todo: restore any user-asigned width instead?
columns[ visibleColumns[fillerColumnIdx] ].nTh.style.width = '';
return;
}
// Get the width of each column in the constructed table - we need to
// know the inner width (so it can be assigned to the other table's
// cells) and the outer width so we can calculate the full width of the
// table. This is safe since DataTables requires a unique cell for each
// column, but if ever a header can span multiple columns, this will
// need to be modified.
var total = 0;
for ( i=0 ; i<visibleColumns.length ; i++ ) {
if ( i == fillerColumnIdx ) continue;
var cell = $(headerCells[i]);
var border = cell.outerWidth() - cell.width();
// Use getBounding... where possible (not IE8-) because it can give
// sub-pixel accuracy, which we then want to round up!
var bounding = browser.bBounding ?
Math.ceil( headerCells[i].getBoundingClientRect().width ) :
cell.outerWidth();
// Total is tracked to remove any sub-pixel errors as the outerWidth
// of the table might not equal the total given here (IE!).
total += bounding;
// Width for each regular column to use
columns[ visibleColumns[i] ].sWidth = fns._fnStringToCss( bounding - border );
}
//console.log('total from columns is ' + total);
// Finished with the table - ditch it
holder.remove();
// Copy over to the table css.
var cIdx = visibleColumns[fillerColumnIdx];
for ( i=0 ; i<columns.length ; i++ ) {
if ( i == cIdx ) continue;
//console.log('Assigned width ' + columns[i].sWidth + ' to column ' + i);
columns[i].nTh.style.width = columns[i].sWidth;
}
// and set the filler column to have the leftovers.
//console.log('filler column gets ' + (desiredWidth-total));
columns[cIdx].nTh.style.width = fns._fnStringToCss( desiredWidth-total );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.