// TsTable template, by neilw, 2009
// Versions
// 1.00 7/27/2009 neilw First published version
// 1.10 7/29/2009 neilw Added pagination support (doesn't work with filter yet)
// 1.11 8/25/2009 neilw Fixed bug with pager and fewer than 10 rows
// 1.12 9/25/2009 neilw Added support for data as list of lists
// 1.13 10/8/2009 neilw Put in a spacer for empty table cells
// 1.14 10/9/2009 neilw Fixed a bug introduced in 1.13 (sigh)
// 1.15 10/12/2009 neilw Cleaned up CSS, especially for IE
// 1.16 10/13/2009 neilw Added default filter option, reduced spurious error alerts
// 1.17 2/12/2010 neilw Improved table header HTML for improved reliability
// 1.18 2/23/2010 neilw Added UNSAFECONTENT permission check
// 1.20 7/14/2010 neilw Added full support for "sorter" option in columns
// Added "nosort" option for table
// 1.21 7/19/2010 neilw Now properly recognize "0" as a number (new tablesorter script, new name!!!)
// 1.22 7/26/2010 neilw Improved "sort:false" option
// 1.23 8/13/2010 neilw Cleaned up stylesheet a *little*
// 1.24 11/22/2010 neilw Added "nodata" option
// LOCAL SETTINGS
//This template requires the following files to be installed as specified below:
//@himikel mod 2010-11-09: Using local image resources
var tablesorter_uri = "/skins/local/tstable/tstable.tablesorter.min.js";
var tablesorter_bg_uri = "/skins/local/tstable/tablesorter_bg.gif";
var tablesorter_asc_uri = "/skins/local/tstable/tablesorter_asc.gif";
var tablesorter_desc_uri = "/skins/local/tstable/tablesorter_desc.gif";
var pager_uri = "/skins/local/tstable/jquery.tablesorter.pager.js";
var pager_first_uri = "/skins/local/tstable/pager_first.png";
var pager_prev_uri = "/skins/local/tstable/pager_prev.png";
var pager_next_uri = "/skins/local/tstable/pager_next.png";
var pager_last_uri = "/skins/local/tstable/pager_last.png";
/*
var tablesorter_uri = "http://developer.mindtouch.com/@api/deki/files/6272/=tstable.tablesorter.min.js";
var tablesorter_bg_uri = "http://developer.mindtouch.com/@api/deki/files/4627/=tablesorter_bg.gif";
var tablesorter_asc_uri = "http://developer.mindtouch.com/@api/deki/files/4626/=tablesorter_asc.gif";
var tablesorter_desc_uri = "http://developer.mindtouch.com/@api/deki/files/4624/=tablesorter_desc.gif";
var pager_uri = "http://developer.mindtouch.com/@api/deki/files/4651/=jquery.tablesorter.pager.js";
var pager_first_uri = "http://developer.mindtouch.com/@api/deki/files/4648/=pager_first.png";
var pager_prev_uri = "http://developer.mindtouch.com/@api/deki/files/4647/=pager_prev.png";
var pager_next_uri = "http://developer.mindtouch.com/@api/deki/files/4650/=pager_next.png";
var pager_last_uri = "http://developer.mindtouch.com/@api/deki/files/4649/=pager_last.png";
*/
// The following variables define important styles for this template:
var thead_unsel_color = "#D0E0E0"; // Color of unselected header cell
var thead_sel_color = "#8DBDD8"; // Color of selected header cell
var zebra_color = "#ECECF0"; // Color of odd rows when "zebra" is specified
// USAGE: template.TsTable(options, columns, data)
// "columns": Optional array of column specifications, one for each table column. Each column spec can be either a
// simple string or a map. This argument only applies if "data" is also specified (GENERATE mode). If it's a
// string, then it specifies the key and title for the column data. If it's a map, then the possible fields are:
// key: keyname for the map element containing the cell data. THIS IS THE ONE AND ONLY FIELD THAT IS
// REQUIRED. If surrounded by parentheses, will be evaluated as a Dekiscript expression to provided
// the cell value (see docs).
// title: Title for the column. If not specified, will use "key".
// width: width of column
// style: style to apply to this cell. If surrounded by parentheses, will be evalauted as a Dekiscript
// expression (see docs)
// initial: set this column as initial sort column; 0 ==> ascending, 1 ==> descending
// sorter: control sortability on this column (this is case sensitive!):
// false ==> disable sorting on this column
// "text" ==> sort column as text
// "digit" ==> sort column as numbers
// "currency" ==> sort column as currency values (ignore leading currency symbol)
// "ipAddress" ==> sort as IP addresses
// "url" ==> sort as URL; ignore protocol prefix
// "isoDate" ==> sort as ISO formatted date (yyyy-M-d; separator may be "-" or "/")
// "percent" ==> sort as percentage (ignore trailing "%")
// "usLongDate" ==> sort as date (MMM dd, (yyyy | 'yy) [hh:mm:ss (AM|PM))
// "usShortDate" ==> sort as date (MM-dd-(yyyy|yy)
// "time" ==> sort as time (hh:mm (am|pm))
// date: TBD
// "options": Optional map containing an assortment of options that apply to the whole table:
// id: ID attribute of the table element. If we're applying tablesorter to an existing table, the template
// will look for a table with this name. If we're generating a table, its ID will be set to this value
// width: width of the entire table
// initial: initial sort order for the table. This is the "sortlist" parameter for the tablesorter extension.
// rowstyle: style applied to each row. If surrounded by parentheses, will be evaluated as a Dekiscript
// expression (see docs)
// zebra: set to true to enable zebra-striping of the table row
// pager: enable the table pager (doesn't work with filters yet!)
// sort: enable or disable sorting on the entire table (default:true)
// nodata: message to be printed when there is no data (default: "(no data)")
// "data": array of maps or lists containing data for each row of the table. The "columns" argument specified in the "key"
// element which element of the map will be used to supply data for this cell. Additional elements may be
// present in the map for purposes of style or content evaluation (see docs), or it will be ignored. If a data row is
// a list, then the elements will be used in order.
// "filter": array of 2-element arrays, each specifying optional filters that may be applied to the table.
// The first sub-array element specifies the name of the filter, which will be displayed in the
// <select> widget. The second element is Dekiscript code which is applied to each row in the table;
// if the code evaluates to "true" then the row will be displayed when that option is selected.
// The first selection, and the default, is always "show all items", so you don't need to specify
// that one. See docs for more details.
/**** Misc "globals" ****/
var options_arg = $1 ?? $options ?? {};
var columns_arg = $0 ?? $columns;
var data_arg = $2 ?? $data;
if (data_arg is map)
let data_arg = map.values(data_arg);
var filter_arg = $3 ?? $filter;
var apply = (data_arg == nil); // if no data, modify existing table (APPLY mode)
var eval_re = "(^\\((.*)\\)$)"; // regular expression to detect "eval"
var errors = []; // list of error messages
/**** Argument Processing ****/
//
// "columns" arg
//
var c_series = [];
var columns = []; // processed list of column specifications
var headers = {}; // "headers" arg for tablesorter
var sortlist = []; // "sortlist" arg for tablesorter
if (columns_arg == nil) {
if (!apply) let errors ..= [ "must specify COLUMNS when DATA is also specified ('generate' mode)" ];
}
else {
let c_series = num.series(0, #columns_arg - 1);
if (apply) let errors ..= [ "WARNING: ignoring COLUMNS in 'apply' mode" ];
else if (columns_arg is not list) let errors ..= [ 'COLUMNS: must be a list' ];
else foreach (var i in c_series) {
var c = columns_arg[i];
if (c is str)
let c = { key:c };
// check for bogus extra fields
foreach (var k in map.keys(c))
if (!list.contains([ "title","key","width","sorter","initial","style" ], //"date","type","hide" ]
string.tolower(k))) let errors ..= [ "COLUMNS["..i.."]: unknown field: '"..k.."'" ];
// "key" field
if (c.key == nil) {
let errors ..= [ "COLUMNS["..i.."]: no KEY supplied" ];
let c ..= { key:"xxx" };
}
else if (string.match(c.key, eval_re)) let c ..= { eval_key:true };
// "title" field
if (c.title == nil) let c ..= { title:c.key };
// "sorter" field
if (c.sorter is bool || c.sorter is str) {
var sorterOptions = [ "text", "digit", "currency", "ipAddress", "url", "isoDate",
"percent", "usLongDate", "shortDate", "time", "metadata" ];
if (c.sorter is str && !list.contains(sorterOptions, c.sorter))
let errors ..= [ "COLUMNS["..i.."]: invalid SORTER option '"..c.sorter.."'" ];
else
let headers ..= { (i):{ sorter:c.sorter } };
}
// "initial" field
if (c.initial != nil) {
if (!string.match(c.initial, "(^[01]$)")) let errors ..= [ "COLUMNS["..i.."]: INITIAL must be 0 or 1" ];
else let sortlist ..= [ [i,c.initial] ];
}
// "style" field
if (c.style != nil && string.match(c.style, eval_re)) let c ..= { eval_style:true };
// Build columns
let columns ..= [ c ];
}
}
//
// "options" arg
//
// check for bogus extra fields
foreach (var k in map.keys(options_arg))
if (!list.contains(["id","width","zebra","initial","rowstyle","pager","sort","nodata" ], string.tolower(k)))
let errors ..= [ 'OPTIONS: unknown option: ' .. k ];
// simple fields
var tname = options_arg.id?? (apply ? nil : @tname);
var find = (tname == nil);
if (find) let tname = @tname;
var width = options_arg.width;
var widgets = options_arg.zebra == true ? [ 'zebra' ] : [];
let sortlist = options_arg.initial ?? sortlist;
var pager = options_arg.pager ?? false;
var perpage = [ 10, 25, 50, 100, 250, 500 ];
var enableSorting = options_arg.sort ?? true;
var noData = options_arg.nodata;
if (noData is not str) let noData = "(no data)";
// "rowstyle" field
if (options_arg.rowstyle != nil && string.match(options_arg.rowstyle, eval_re))
let options_arg ..= { eval_rowstyle:true };
//
// "data" and "filter" arg
//
var filter = [];
var filter_default = -1;
var filter_code = [];
if (filter_arg) {
if (filter_arg is not list) let errors ..= [ "expected FILTER arg to be a list" ];
else if (pager) let errors ..= [ "filter won't work when pager is enabled (disabling filter)" ];
else {
let filter = [ ];
let filter_default = 0;
let filter_code = [ ];
foreach (var i in num.series(0,#filter_arg-1)) {
var f = filter_arg[i];
if (f is not list)
let errors ..= [ "FILTER: option "..i.." is not a list" ];
else {
if (#f != 2 && !(#f == 3 && f[2] is bool))
let errors ..= [ "FILTER: option "..i.." is not a list of two elementsvalid" ];
else {
let filter ..= [ f[0] ];
let filter_code ..= [ f[1] ];
if (f[2]) let filter_default = i;
}
}
}
}
}
var space = web.html(" ");
var data = []; // processed data array
if (!apply) {
foreach (var r in data_arg) {
if (r is list) let r = { (i):r[i] foreach var i in num.series(0,#r-1) };
if (r is map) {
let class = [];
let r ..= { rowstyle:(options_arg.eval_rowstyle ?
list.apply([r],options_arg.rowstyle)[0] : options_arg.rowstyle) };
foreach (var i in c_series) {
var c = columns[i];
if (c.eval_key || r[c.key] != nil)
let r ..= { (i): (c.eval_key ? list.apply([r],c.key)[0] : r[c.key]) };
}
foreach (var i in c_series) {
var c = columns[i];
if (c.style) let r ..= { ("style"..i): (c.eval_style ? list.apply([r],c.style)[0] : c.style ) };
}
if (#filter_code)
foreach (var i in num.series(0,#filter_code-1))
if (list.apply([r],filter_code[i])[0])
let class ..= [ "tsf"..i ];
let data ..= [ r..{ class:string.join(class," ") } ];
}
else
let errors ..= [ "DATA: item ".. __index .. " is not a map or list" ];
}
}
else if (filter_arg)
let errors ..= [ "WARNING: ignoring FILTER arg in 'apply' mode" ];
//
// Output XML
//
<html>
// Scripts and stylesheets go in the head
<head>
<script type="text/javascript" src=(tablesorter_uri)></script>;
<script type="text/javascript"> var garbage = 0; </script>;
<script type="text/javascript" src=(pager_uri)></script>;
<script type="text/javascript">"
// Call main tablesorter function
DekiWiki.$(document).ready(function($) {
tstableApply($, "..json.emit(apply)..","..json.emit(find)..","..json.emit(@id)..","..
json.emit(tname)..","..json.emit(headers)..","..json.emit(sortlist)..","..
json.emit(widgets)..","..json.emit(filter_default)..","..json.emit(pager)..","..json.emit(enableSorting)..");
});
"</script>;
<script type="text/javascript">"
// Apply tablesorter to table
function tstableApply($, apply, find, id, name, headers, sortlist, widgets, filter, pager, enable) {
// Find the table
var $table;
if (!find)
$table = $('table#'+name);
else {
var pid = 'p#' + id;
var $nodes = $(pid).nextAll();
for (var i = 0; i < $nodes.length; i++) {
$table = $nodes.eq(i);
if ($table.is('table')) break;
$table = $table.find('table:first');
if ($table.length) break;
}
if (!$table.length) {
alert(\"ERROR: TsTable: can't find table\");
//@himikel #rem 2010-11-04: Skip error message using with SecionUI
//alert(\"ERROR: TsTable: can't find table\");
return;
}
$table.attr({'id':name, 'border':'0', 'cellspacing':'0px', 'cellpadding':'3px'});
}
// set 'tablesorter' class if necessary
if (!$table.hasClass('tablesorter')) $table.addClass('tablesorter');
// Copy header row to 'thead' element if necessary
var $header = $table.find('thead tr');
if (!$header.length) {
var $ohr = $table.find('tr:eq(0)');
if (!$ohr.length) return; // table is empty, bail out
$table.prepend('<thead><tr>'+$ohr.html()+'</tr></thead>');
$ohr.remove();
$header = $table.find('thead tr');
}
// Convert header <td> to <th> if necessary
if (apply) {
var hdrhtml = '';
$header.children().each(function() { hdrhtml += '<th>'+$(this).html()+'</th>'; });
$header.html(hdrhtml);
}
// Convert format of each header element to reserve room for arrow
$header.children().each(function() {
$(this).css('padding-right','20px');
});
// Clean up leading and trailing space
$table.find('td,th').each(function() {
$(this).find('p:first-child').css({'margin-top':'0px','padding-top':'0px'});
$(this).find('p:last-child').css({'margin-bottom':'0px','padding-bottom':'0px'});
var html = $(this).html();
html = html.replace(/^(\\s*(\\ )*(\\<br ?\\/?\\>)*)*/,'');
html = html.replace( /(\\s*(\\ )*(\\<br ?\\/?\\>)*)*$/,'');
$(this).html(html);
});
// If we're not going to enable sorting, get out
if (!enable) return;
// Install filter
if (filter >= 0) {
var selectid = 'select#tsf' + id;
$(selectid).change(function() {
var $options = $(this).find('option:selected');
$table.find('tbody.tsf > tr').each(function() {
var show = false;
var $row = $(this);
$options.each(function() { show |= $row.hasClass($(this).attr('value')); } );
if (show) $row.show();
else $row.hide();
});
$table.trigger('applyWidgets');
return(false);
});
$(selectid).find('option').each(function(i) { $(this).attr('selected',i==filter?'selected':''); });
$(selectid).change();
}
// Now activate tablesorter
$table.tablesorter({
headers: headers, sortList: sortlist, widgets: widgets,
cssHeader: 'tsHeader', cssAsc: 'tsHeaderSortUp', cssDesc: 'tsHeaderSortDown'
});
if (pager) {
var pagerid = '#'+id+'pager';
$(pagerid).find('option').each(function(i) { $(this).attr('selected',i==0?'selected':''); });
$table.tablesorterPager({container: $('#'+id+'pager'), positionFixed:false});
}
}
"</script>;
// Styles
var tid = "table.tablesorter#"..tname;
<style type="text/css">"
.tablesorter {
border-collapse: separate;
border-left: solid #808080 1px;
border-top: solid #808080 1px;
}
.tablesorter > thead > tr > th, .tablesorter > tbody > tr > td, .tablesorter > tfoot > tr > td {
border-right: solid #808080 1px;
border-bottom: solid #808080 1px;
}
.tablesorter > thead > tr > th, .tablesorter > tfoot > tr > th {
background-color: "..thead_unsel_color..";
font-weight: bold;
}
.tsHeader {
background-image: url("..tablesorter_bg_uri..");
background-repeat: no-repeat;
background-position: center right;
cursor: pointer;
}
.tablesorter > tbody tr.odd { background-color:"..zebra_color.."; }
.tsHeader.tsHeaderSortUp {
background-image: url("..tablesorter_asc_uri..");
background-color: "..thead_sel_color..";
}
.tsHeader.tsHeaderSortDown {
background-image: url("..tablesorter_desc_uri..");
background-color: "..thead_sel_color..";
}
"</style>;
</head>
// Generate table HTML in the body
<body>
// Error messages
if (#errors)
<div style="color:red; font-weight:bold;"> "TSTABLE ERRORS:";
<ul> foreach (var e in errors) <li> e </li>; </ul>
</div>;
// Insert marker if applying
if (apply) { <p id=(@id) style="display:none;" /><div />; }
// Else generate table
else {
if (#filter) {
<p><span>"Show: "</span><select id=("tsf"..@id)>
foreach (var i in num.series(0,#filter-1))
<option value=("tsf"..i) selected=(i==0?"selected":"")> filter[i] </option>;
</select></p>;
}
<table class="tablesorter" id=(tname) width=(width) cellpadding="3px" cellspacing="0px">
// Header
<thead><tr> foreach (var c in columns) <th width=(c.width)>c.title</th>; </tr></thead>
// Body
<tbody class="tsf">
foreach (var r in data) <tr style=(r.rowstyle) class=(r.class)>
foreach (var i in c_series) <td style=(r["style"..i])> (r[i]==nil || r[i]=="") ? space : r[i] </td>;
</tr>;
if (!#data) <tr><td colspan=(#columns)> noData </td></tr>;
</tbody>
if (pager) {
<tfoot style=(#data > perpage[0] ? "" : "display:none")>
<tr bgcolor=(thead_unsel_color)><td id=(@id .. "pager") colspan=(#columns) align="center">
<img class="first" style="cursor:pointer" src=(pager_first_uri) />
<img class="prev" style="cursor:pointer" src=(pager_prev_uri) />
<img src="/skins/common/icons/icon-trans.gif" width="8" height="8" />
<span class="pagedisplay"> " " </span>
<img src="/skins/common/icons/icon-trans.gif" width="8" height="8" />
<img class="next" style="cursor:pointer" src=(pager_next_uri) />
<img class="last" style="cursor:pointer" src=(pager_last_uri) />
<select class="pagesize" style="float:right">
foreach (var pp in perpage) {
<option value=(pp) selected=(__index == 0 ? "selected" : "")> (pp>#data ? "all":pp) </option>;
if (pp > #data) break;
}
</select>
</td></tr></tfoot>;
}
</table>;
}
</body>
</html>;