Skip to content

Instantly share code, notes, and snippets.

@bor
Created June 26, 2012 09:42
Show Gist options
  • Save bor/2994710 to your computer and use it in GitHub Desktop.
Save bor/2994710 to your computer and use it in GitHub Desktop.
jQuery plugin: Scrollable Table with Sticky Headers
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>jQuery.plugin.convertTableToScrollable test</title>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.js"></script>
<script type="text/javascript" src="jQuery.plugin.convertTableToScrollable.js"></script>
<style type="text/css">
table {
border-collapse: collapse;
border-spacing: 0;
}
table td,
table th {
border: 1px dotted;
padding: 0 4px 0 8px;
text-align: left;
}
.clipped {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.uncollapse {
border-collapse: separate;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div style="margin: auto; width: 50%;">
<br>
<form action="">
<input type="checkbox" name="switch_collapse" value="none"> switch collapse
<input type="checkbox" name="display_none" value="none"> display: none
</form>
<h5>with 'height'</h5>
<div id="wrapper" style="height: 200px;">
<table id="table_id" style="width: 100%;">
<thead>
<tr>
<th>title 1</th>
<th>title 22</th>
<th>title 333</th>
<th>title</th>
<th> <div class="clipped">long title 55555</div> </th>
<th>title</th>
<th>title</th>
</tr>
</thead>
<tbody>
<tr>
<td>data 11</td>
<td>data 12</td>
<td>data 13</td>
<td>data</td>
<td> <div class="clipped">data</div> </td>
<td>data</td>
<td> <a id="row_add" href="" title="add row">+</a> &nbsp; <a id="row_del" href="" title="remove row">-</a> </td>
</tr>
<tr>
<td>data 21</td>
<td>data 22</td>
<td>data 23</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data 31</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data 81</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
</tbody>
</table>
</div>
<h5>with 'max-height'</h5>
<div id="wrapper_max_height" style="max-height: 200px;">
<table id="table_id_max_height" style="width: 100%;">
<thead>
<tr>
<th>title 1</th>
<th>title 22</th>
<th>title 333</th>
<th class="hidden">title</th>
<th>title 55555</th>
<th>title</th>
<th>title 7777777</th>
</tr>
</thead>
<tbody>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 333</td>
<td class="hidden">data 4444</td>
<td>data 55555</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td class="hidden">data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
</tbody>
</table>
</div>
</div>
<script type="text/javascript">
$(document).ready( function() {
// make #table_id_max_height bigger to check 'IE9 big table insert' bug
var $row = $('#table_id_max_height>tbody>tr:last');
for (var i = 0; i < 500 ; i++) {
$row.clone().appendTo('#table_id_max_height');
}
$('input[name=switch_collapse]').on('click', function() {
$('#wrapper').find('table').toggleClass('uncollapse');
$('#wrapper_max_height').find('table').toggleClass('uncollapse');
});
$('input[name=display_none]').on('click', function() {
$('#wrapper').toggle();
$('#wrapper_max_height').toggle();
//$('#wrapper').toggle( function() { if ( $('#table_id').is(':visible') ) $('#table_id').trigger('resize'); } );
});
$('#table_id_max_height').convertTableToScrollable({ extend: false });
$('#table_id').convertTableToScrollable({ scrollbar_detect: true });
$('#row_add').click( function() {
var $tbody = $(this).closest('table').children('tbody');
$('tr:last', $tbody).clone().appendTo($tbody);
return false;
} );
$('#row_del').click( function() {
var $tbody = $(this).closest('table').children('tbody');
$('tr:last', $tbody).remove();
return false;
} );
});
</script>
</body>
</html>
// jQuery plugin
// Scrollable Table with Sticky Headers
// Required: jQuery 1.8+ (use outerWidth as setter)
// Usage: $('#table_to_scrollable').convertTableToScrollable();
// Note: after show(if table was hidden) you may be want to do $('#table_to_scrollable').trigger('resize');
'use strict';
(function($) {
$.fn.convertTableToScrollable = function(options) {
options = $.extend({}, $.fn.convertTableToScrollable.defaults, options);
var get_max_height = function (el) {
var v = $(el).css('max-height');
if ( v === undefined || v === 'none' || parseInt(v, 10) === -1 ) return null; // or Number.MAX_VALUE
return parseInt(v, 10) || 0;
};
var get_width = function get_width(el) {
if (!$(el).is(':visible')) return 0; // handle hidden element
var width;
//if (navigator.userAgent.match(/webkit/i)) width = $(el).width() + ($(el).outerWidth() - $(el).innerWidth());
//else if (navigator.userAgent.match(/msie/i) && parseInt(navigator.appVersion,10) < 9) width = $(el).outerWidth() - 2;
//else
width = $(el).width(); // at this moment work for most browsers
// respect to max-width
var max_width = parseInt($(el).css('max-width'), 10);
if ( max_width && width > max_width ) width = max_width;
return width;
};
var table_convert = function table_convert(table) {
var $table = $(table);
// prevent double conversion
if ( $table.hasClass('_scrollable') || $table.parent().hasClass('_scrollable') || $table.parent().parent().hasClass('_scrollable') )
return;
var table_id = $table.attr('id');
$table.removeAttr('id');
// reset dimensions & layout on wrappers, tables
var dimensions_reset = function dimensions_reset() {
// clean up old hardcoded widths
$head_table_th_first.each( function(i) { if ($(this).is(':visible')) $(this).css('width', 'auto'); } );
$table_tb_first.each( function(i) { if ($(this).is(':visible')) $(this).css('width', 'auto'); } );
$wrapper.css({ height: 'auto', width: '100%' });
$table.css({ 'table-layout': 'auto', width: '100%' });
$head_wrapper.css('width', '100%');
$head_table.css({ 'table-layout': 'auto', 'width': '100%' });
};
// fix wrapper height if need
var heights_fix = function heights_fix() {
var height = Math.min( $wrapper.parent().height(), $wrapper.parent().parent().height() );
var max_height = options.max_height;
if (!max_height) max_height = $wrapper.parent().parent().height();
// fix height for body (considering head)
if ( $wrapper.height() > height ) $wrapper.css('height', height);
$wrapper.css({ 'max-height': Math.max( $wrapper.height(), max_height || 0 ) - $head_wrapper.height() + 'px' });
};
// fix wrapper & table widths
var widths_fix = function widths_fix() {
// setup wrapper/table widths directly
$wrapper.width($wrapper.width());
$head_wrapper.width($head_wrapper.width());
var head_table_width = $wrapper.width();
var table_width = head_table_width;
// if we has scrollbar - compensate it
if ( $wrapper[0].scrollHeight > $wrapper.innerHeight() + 1 ) { // +1 = hack for IE
// calculate scrollbar width
var scrollbar_width = $wrapper.width() - $wrapper[0].clientWidth;
// add space for compensate scrollbar
if (!options.extend) {
$head_wrapper.css( 'padding-right', scrollbar_width + 'px' );
head_table_width -= scrollbar_width;
}
table_width -= scrollbar_width;
}
else if (!options.extend) {
$head_wrapper.css( 'padding-right', 0 );
}
$head_table.outerWidth(head_table_width);
$table.outerWidth(table_width);
return table_width;
};
// resize table
var resize = function resize() {
var widths = [];
heights_fix();
$table.children('thead').hide(); // hide, but do not remove - need for resize in future
var table_width = widths_fix();
$table.children('thead').show(); // to be sure
// get table widths (in px)
$table_tb_first.each( function() {
widths.push(get_width(this));
} );
var n_last_col = widths.length - 1;
$table.children('thead').hide(); // hide, but do not remove - need for resize in future
// setup fixed layout
$head_table.css('table-layout', 'fixed');
$table.css('table-layout', 'fixed');
// fix widths for header columns & body columns
// left the last column's width as 'auto' to fix a couple of issues (especially IE bugs)
$head_table_th_first.each( function(i) { if ( i !== n_last_col ) $(this).width(widths[i]); } );
$table_tb_first.each( function(i) { if ( i !== n_last_col ) $(this).width(widths[i]); } );
// try to fix 'convert table to fixed' problem = +1px?
if ( $table.outerWidth() > table_width ) $table_tb_first.last().width('auto');
};
// reset & resize all again + process fixes
var resize_all = function resize_all() {
// dirty hack for sure table is visible
if ( $table.is(':hidden') ) return;
dimensions_reset();
resize();
if ( options.scrollbar_detect ) _scrollbar_detect(true);
};
// calculate max height if not passed
if ( !options.max_height ) {
if (get_max_height($table.parent())) options.max_height = get_max_height($table.parent());
if (get_max_height($table)) options.max_height = get_max_height($table);
}
// we collapse borders for better cross-browser support
//$table.css({ 'border-collapse': 'collapse', 'border-spacing': 0, 'empty-cells': 'show' });
// set width to 0 for hidden elements
$('thead>tr:first>th', $table).each( function() {
if (!$(this).is(':visible')) $(this).css('width', 0);
} );
// wrap the table (with just body at end)
var $wrapper = $table.wrap('<div></div>').parent().attr('id', table_id + '_body');
$wrapper.css({ 'border-width': 0, margin: 0, overflow: 'auto', width: '100%' });
// main table wrapper
var $main_wrapper = $wrapper.wrap('<div class="_scrollable"></div>').parent().attr('id', table_id);
$main_wrapper.css({ 'border-width': 0, margin: 0, overflow: 'hidden', width: '100%' });
// header wrapper
var $head_wrapper = $('<div></div>').attr('id', table_id + '_header').prependTo($main_wrapper);
$head_wrapper.css({ 'border-width': 0, margin: 0, overflow: 'hidden' });
// clone table to the header
var $head_table = $table.clone().appendTo($head_wrapper);
// remove the extra html
$head_table.children('tbody').empty().append('<tr><td/></tr>').hide();
// detect border difference
var width_diff = $wrapper.outerWidth() - $table.outerWidth();
// apply 'remove whitespace fix' for fix 'IE9 insert big table' bug
if ( navigator.userAgent.match(/msie/i) && parseInt(navigator.appVersion,10) === 9 && $table.find('tbody>tr').length > 200 ) {
var expr = new RegExp('>[ \t\r\n\v\f]*<', 'g');
var table_html = $table.html();
$table.html( table_html.replace(expr, '><') );
}
// define some common selecters
var $head_table_th_first = $('thead>tr:first>th', $head_table);
var $table_tb_first = $('tbody>tr:first>td', $table);
if ( options.scrollbar_detect ) $wrapper.data('scrollbar_height_prev', $wrapper[0].scrollHeight);
resize();
// detecting scrollbar appear
var _scrollbar_detect = function _scrollbar_detect(just_reset) {
if ( _resize_id !== false ) clearTimeout(_resize_id);
if ( $wrapper.data('scrollbar_height_prev') && $wrapper[0].scrollHeight != $wrapper.data('scrollbar_height_prev') ) {
$wrapper.data('scrollbar_height_prev', $wrapper[0].scrollHeight);
if ( $wrapper[0].scrollHeight > $wrapper.innerHeight()+1 && !$wrapper.data('scrollbar_exists') ) { // +1 = hack for IE
$wrapper.data('scrollbar_exists', true);
if (!just_reset) resize_all();
}
else if ( $wrapper[0].scrollHeight <= $wrapper.innerHeight() && $wrapper.data('scrollbar_exists') ) {
$wrapper.data('scrollbar_exists', false);
if (!just_reset) resize_all();
}
}
_resize_id = setTimeout(function() { _scrollbar_detect(false); }, 500);
};
// respond to changing window's size
var _resize_id;
$(window).resize( function() {
if ( _resize_id !== false ) clearTimeout(_resize_id);
_resize_id = setTimeout( resize_all, 500 );
});
// respond to appear/lost scrollbar (on wrapper element)
if ( options.scrollbar_detect ) _scrollbar_detect();
};
return this.each(function() {
table_convert(this);
});
};
$.fn.convertTableToScrollable.defaults = {
//debug: false, // debug on/off flag
extend: true, // extend last thead column (above scrollbar)
max_height: null, // max height of the scrollable area
scrollbar_detect: false // detect if scrollbar appear, performance hit!
};
})(jQuery);
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>jQuery.plugin.convertTableToScrollable test</title>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.js"></script>
<script type="text/javascript" src="jQuery.plugin.convertTableToScrollable.js"></script>
<style type="text/css">
table {
border-collapse: collapse;
border-spacing: 0;
}
table td,
table th {
border: 1px dotted;
padding: 0 4px 0 8px;
text-align: left;
}
.clipped {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.uncollapse {
border-collapse: separate;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div style="margin: auto; width: 50%;">
<br>
<form action="">
<input type="checkbox" name="switch_collapse" value="none"> switch collapse
<input type="checkbox" name="display_none" value="none"> display: none
</form>
<h5>with 'height'</h5>
<div id="wrapper" style="height: 200px;">
<table id="table_id" style="width: 100%;">
<thead>
<tr>
<th>title 1</th>
<th>title 22</th>
<th>title 333</th>
<th>title</th>
<th> <div class="clipped">long title 55555</div> </th>
<th>title</th>
<th>title</th>
</tr>
</thead>
<tbody>
<tr>
<td>data 11</td>
<td>data 12</td>
<td>data 13</td>
<td>data</td>
<td> <div class="clipped">data</div> </td>
<td>data</td>
<td> <a id="row_add" href="" title="add row">+</a> &nbsp; <a id="row_del" href="" title="remove row">-</a> </td>
</tr>
<tr>
<td>data 21</td>
<td>data 22</td>
<td>data 23</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data 31</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data 81</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
</tbody>
</table>
</div>
<h5>with 'max-height'</h5>
<div id="wrapper_max_height" style="max-height: 200px;">
<table id="table_id_max_height" style="width: 100%;">
<thead>
<tr>
<th>title 1</th>
<th>title 22</th>
<th>title 333</th>
<th class="hidden">title</th>
<th>title 55555</th>
<th>title</th>
<th>title 7777777</th>
</tr>
</thead>
<tbody>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 333</td>
<td class="hidden">data 4444</td>
<td>data 55555</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data 1</td>
<td>data 2</td>
<td>data 3</td>
<td class="hidden">data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
</tbody>
</table>
</div>
</div>
<script type="text/javascript">
$(document).ready( function() {
// make #table_id_max_height bigger to check 'IE9 big table insert' bug
var $row = $('#table_id_max_height>tbody>tr:last');
for (var i = 0; i < 500 ; i++) {
$row.clone().appendTo('#table_id_max_height');
}
$('input[name=switch_collapse]').on('click', function() {
$('#wrapper').find('table').toggleClass('uncollapse');
$('#wrapper_max_height').find('table').toggleClass('uncollapse');
});
$('input[name=display_none]').on('click', function() {
$('#wrapper').toggle();
$('#wrapper_max_height').toggle();
//$('#wrapper').toggle( function() { if ( $('#table_id').is(':visible') ) $('#table_id').trigger('resize'); } );
});
$('#table_id_max_height').convertTableToScrollable({ extend: false });
$('#table_id').convertTableToScrollable({ scrollbar_detect: true });
$('#row_add').click( function() {
var $tbody = $(this).closest('table').children('tbody');
$('tr:last', $tbody).clone().appendTo($tbody);
return false;
} );
$('#row_del').click( function() {
var $tbody = $(this).closest('table').children('tbody');
$('tr:last', $tbody).remove();
return false;
} );
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment