User:Tcncv/sorttables.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | Documentation for this user script can be added at User:Tcncv/sorttables. |
/* Test version of wikibits table sorting based on 2009-01-02 version */
/*
* Enhancements:
* 1. Will explode rowspans, so that rows are self contained and can be sorted
* without garbling the table structure.
* 2. Will recognize colspans, so that the proper value is retrieved from each
* row. Each column in a colspan range is treated as having the same value.
* Also, colspans are preserved, they are not split.
* 3. After sorting, some cell ranges may be recombined under certain restrictive
* conditions (still being refined). Also, the class="autorowspan" option can
* be applied to columns or the entire table to enable more aggressive rowspan
* combines, such as combining cells in the currently sorted column that were
* not originally combined.
* Todo:
* 1. (done) Get recombine and autorowspan working on IE.
* 2. Continue refining the recombine and autorowspan behavior.
* 3. Add logic to better handle rowspan/colspan conflicts.
* 4. Consider if it be useful to apply autorowspan to the original table before
* its initial display? (Might same some tedious table formatting effort.)
*/
/*
* Table sorting script based on one (c) 1997-2006 Stuart Langridge and Joost
* de Valk:
* http://www.joostdevalk.nl/code/sortable-table/
* http://www.kryogenix.org/code/browser/sorttable/
*
* @todo don't break on colspans/rowspans (bug 8028)
* @todo language-specific digit grouping/decimals (bug 8063)
* @todo support all accepted date formats (bug 8226)
*/
var tcncv_image_path = stylepath+"/common/images/";
var tcncv_image_up = "sort_up.gif";
var tcncv_image_down = "sort_down.gif";
var tcncv_image_none = "sort_none.gif";
var tcncv_europeandate = wgContentLanguage != "en"; // The non-American-inclined can change to "true"
var tcncv_alternate_row_colors = false;
var tcncv_number_transform_table = null;
var tcncv_number_regex = null;
function tcncv_sortables_init() {
var idnum = 0;
// Find all tables with class sortable and make them sortable
var tables = getElementsByClassName(document, "table", "tcncv_sortable");
for (var ti = 0; ti < tables.length ; ti++) {
if (!tables[ti].id) {
tables[ti].setAttribute('id','sortable_table_id_tcncv_'+idnum);
++idnum;
}
tcncv_makeSortable(tables[ti]);
}
}
addOnloadHook(tcncv_sortables_init);
function tcncv_makeSortable(table) {
var firstRow;
if (table.rows && table.rows.length > 0) {
if (table.tHead && table.tHead.rows.length > 0) {
firstRow = table.tHead.rows[table.tHead.rows.length-1];
} else {
firstRow = table.rows[0];
}
}
if (!firstRow) return;
// We have a first row: assume it's the header, and make its contents clickable links
for (var i = 0; i < firstRow.cells.length; i++) {
var cell = firstRow.cells[i];
if ((" "+cell.className+" ").indexOf(" unsortable ") == -1) {
cell.innerHTML += ' '
+ '<a href="#" class="sortheader" '
+ 'onclick="tcncv_resortTable(this);return false;">'
+ '<span class="sortarrow">'
+ '<img src="'
+ tcncv_image_path
+ tcncv_image_none
+ '" alt="↓"/></span></a>';
}
}
if (tcncv_alternate_row_colors) {
tcncv_alternate(table);
}
}
function tcncv_copy_cell(to_cell, from_cell) {
to_cell.innerHTML = from_cell.innerHTML;
from_cell.innerHTML = from_cell.innerHTML; // Copy to self - IE morphs some values
for (var i = 0; i < from_cell.attributes.length; i++) {
var nodeName = from_cell.attributes[i].nodeName;
var nodeValue = from_cell.getAttribute(nodeName);
var nodeValueType = typeof nodeValue;
if (nodeValue!=null
&& (nodeValueType == "string" || nodeValueType == "number" || nodeValueType == "boolean"))
{
to_cell.setAttribute(nodeName, nodeValue);
}
}
to_cell.innerHTML = from_cell.innerHTML; // Overkill
from_cell.innerHTML = from_cell.innerHTML; // Overkill
}
function tcncv_compare_cells(lhs, rhs) {
if (lhs.innerHTML != rhs.innerHTML) return false;
for (var i = 0; i < lhs.attributes.length; i++) {
var nodeName = lhs.attributes[i].nodeName;
var nodeNameLower = nodeName.toLowerCase(); /* IE uses mixed case */
var nodeValue = lhs.attributes[i].nodeValue;
var nodeValueType = typeof nodeValue;
if (nodeNameLower != "id" && nodeNameLower != "rowspan"
&& nodeValue!=null
&& (nodeValueType == "string" || nodeValueType == "number" || nodeValueType == "boolean"))
{
if (rhs.getAttribute(nodeName) != lhs.getAttribute(nodeName)) {
return false;
}
}
}
for (var i = 0; i < rhs.attributes.length; i++) {
var nodeName = rhs.attributes[i].nodeName;
var nodeNameLower = nodeName.toLowerCase(); /* IE uses mixed case */
var nodeValue = rhs.attributes[i].nodeValue;
var nodeValueType = typeof nodeValue;
if (nodeNameLower != "id" && nodeNameLower != "rowspan"
&& nodeValue!=null
&& (nodeValueType == "string" || nodeValueType == "number" || nodeValueType == "boolean"))
{
if (rhs.getAttribute(nodeName) != lhs.getAttribute(nodeName)) {
return false;
}
}
}
return true;
}
function RepeatedCell(cell) { // prototype
this.cell = cell;
this.remaining = cell.rowSpan - 1;
}
function tcncv_explode_rowspans(table, rowStart) {
var rowspangroup_seq = 0; // Used to generate ids for rowspan cell groups
var repeatedCells = new Array();
for (var r = rowStart; r < table.rows.length; r++) {
var row = table.rows[r];
var c = 0; // column number and repeatedCells index
var i = 0; // cells index (may be less than column number)
while (i < row.cells.length || c < repeatedCells.length) {
if (c < repeatedCells.length && repeatedCells[c] && repeatedCells[c].remaining > 0) {
// Use repeated cell
row.insertCell(i);
tcncv_copy_cell(row.cells[i], repeatedCells[c].cell);
row.cells[i].rowSpan = 1;
repeatedCells[c].remaining--; // remaining_repeats
}
else if (i < row.cells.length ) {
// Use existing defined cell. If rowspan, save for later duplication.
if (row.cells[i].rowSpan > 1) {
if (row.cells[i].id == "" ) {
row.cells[i].id = table.id + ".rowspangroup." + (++rowspangroup_seq);
//if (rowspangroup_seq <= 10) alert("row.cells[i].id=" + row.cells[i].id);
}
repeatedCells[c] = new RepeatedCell(row.cells[i]);
row.cells[i].rowSpan = 1;
}
}
else {
// Insert filler cell
row.insertCell(i);
}
c += row.cells[i].colSpan; // Note: Conflicting rowspan/colspan are not supported
i++;
}
// Trim any trailing completed rowspans (and trailing null elements)
while (repeatedCells.length > 0
&& (!repeatedCells[repeatedCells.length-1] || repeatedCells[repeatedCells.length-1].remaining == 0))
repeatedCells.length--;
}
}
// After sorting, scan for and combine repeated cells, where allowed
function tcncv_combine_rowspans(table, rowStart, headerRow, sortColumn) {
var autoRowSpanTable = ((" "+table.className+" ").indexOf(" autorowspan ") >= 0);
var autoRowSpanColumn = new Array();
for (var i = 0; i < headerRow.cells.length; i++) {
var cell = headerRow.cells[i];
autoRowSpanColumn[i] = ((" "+headerRow.cells[i].className+" ").indexOf(" autorowspan ") >= 0);
}
var priorCells = new Array();
for (var r = rowStart; r < table.rows.length; r++) {
var row = table.rows[r];
if ((" "+row.className+" ").indexOf(" unsortable ") != -1 ||
(" "+row.className+" ").indexOf(" sortbottom ") != -1)
{
// Reset. Do skip and not span across fixed rows
priorCells.length = 0;
}
else {
var c = 0; // column number and priorCells index
var i = 0; // cells index (may be less than column number)
while (i < row.cells.length) {
if (row.cells[i].rowSpan > 1) return; //Not supported
if (c < priorCells.length && priorCells[c]
//&& i == 0 // **** For now, only merge if all cells to left also merged ***
//&& c == 0 // **** For now, limit merge to leftmost column only ***
&& c == sortColumn // **** For now, limit merge to sorted column only ***
&& tcncv_compare_cells(row.cells[i],priorCells[c])
// Limit to cells that were originally combined, unless overridden by table or column option
&& ( (row.cells[i].id != "" && row.cells[i].id == priorCells[c].id)
|| (autoRowSpanColumn.length > c && autoRowSpanColumn[c])
|| autoRowSpanTable))
{
// Matches prior row cell, and either id match or autorowspan is enabled
// - update prior rowspan and delete current.
priorCells[c].rowSpan++;
for (var j = 1; j < row.cells[i].colSpan; j++) priorCells[c+j] = null; // Skipped
c += row.cells[i].colSpan;
row.deleteCell(i);
}
else {
// Not a match - save, but leave unchanged.
priorCells[c] = row.cells[i];
for (var j = 1; j < row.cells[i].colSpan; j++) priorCells[c+j] = null;
c += row.cells[i].colSpan;
i++;
}
}
priorCells.length = c;
}
}
}
function tcncv_getInnerText(row,column) {
var i = 0;
var c = 0;
var ncells = row.cells.length;
while (i < ncells && c <= column) {
if (column >= c && column < c + row.cells[i].colSpan) {
return getInnerText( row.cells[i] );
}
c += row.cells[i].colSpan;
i++;
}
return "";
}
function tcncv_resortTable(lnk) {
// get the span
var span = lnk.getElementsByTagName('span')[0];
var td = lnk.parentNode;
var tr = td.parentNode;
var column = td.cellIndex;
var table = tr.parentNode;
while (table && !(table.tagName && table.tagName.toLowerCase() == 'table'))
table = table.parentNode;
if (!table) return;
if (table.rows.length <= 1) return;
// Generate the number transform table if it's not done already
if (tcncv_number_transform_table == null) {
tcncv_initTransformTable();
}
// Work out a type for the column
// Skip the first row if that's where the headings are
var rowStart = (table.tHead && table.tHead.rows.length > 0 ? 0 : 1);
// Expand any rowspan'ed cells that could potentially be split by sort
tcncv_explode_rowspans(table,rowStart);
var itm = "";
for (var i = rowStart; i < table.rows.length; i++) {
if (table.rows[i].cells.length > column) {
itm = tcncv_getInnerText(table.rows[i],column);
itm = itm.replace(/^[\s\xa0]+/, "").replace(/[\s\xa0]+$/, "");
if (itm != "") break;
}
}
// TODO: bug 8226, localised date formats
var sortfn = tcncv_sort_generic;
var preprocessor = tcncv_toLowerCase;
if (/^\d\d[\/. -][a-zA-Z]{3}[\/. -]\d\d\d\d$/.test(itm)) {
preprocessor = tcncv_dateToSortKey;
} else if (/^\d\d[\/.-]\d\d[\/.-]\d\d\d\d$/.test(itm)) {
preprocessor = tcncv_dateToSortKey;
} else if (/^\d\d[\/.-]\d\d[\/.-]\d\d$/.test(itm)) {
preprocessor = tcncv_dateToSortKey;
// pound dollar euro yen currency cents
} else if (/(^[\u00a3$\u20ac\u00a4\u00a5]|\u00a2$)/.test(itm)) {
preprocessor = tcncv_currencyToSortKey;
} else if (tcncv_number_regex.test(itm)) {
preprocessor = tcncv_parseFloat;
}
var reverse = (span.getAttribute("sortdir") == 'down');
var newRows = new Array();
var staticRows = new Array();
for (var j = rowStart; j < table.rows.length; j++) {
var row = table.rows[j];
if((" "+row.className+" ").indexOf(" unsortable ") < 0) {
var keyText = tcncv_getInnerText(row,column);
var oldIndex = (reverse ? -j : j);
var preprocessed = preprocessor( keyText );
newRows[newRows.length] = new Array(row, preprocessed, oldIndex);
} else staticRows[staticRows.length] = new Array(row, false, j-rowStart);
}
newRows.sort(sortfn);
var arrowHTML;
if (reverse) {
arrowHTML = '<img src="'+ tcncv_image_path + tcncv_image_down + '" alt="↓"/>';
newRows.reverse();
span.setAttribute('sortdir','up');
} else {
arrowHTML = '<img src="'+ tcncv_image_path + tcncv_image_up + '" alt="↑"/>';
span.setAttribute('sortdir','down');
}
//for(var i in staticRows) {
for (var i = 0; i < staticRows.length; i++) {
var row = staticRows[i];
newRows.splice(row[2], 0, row);
}
// We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
// don't do sortbottom rows
for (var i = 0; i < newRows.length; i++) {
if ((" "+newRows[i][0].className+" ").indexOf(" sortbottom ") == -1)
table.tBodies[0].appendChild(newRows[i][0]);
}
// do sortbottom rows only
for (var i = 0; i < newRows.length; i++) {
if ((" "+newRows[i][0].className+" ").indexOf(" sortbottom ") != -1)
table.tBodies[0].appendChild(newRows[i][0]);
}
// Merge cells into rowspans, where possible
tcncv_combine_rowspans(table, rowStart, tr, column);
// Delete any other arrows there may be showing
var spans = getElementsByClassName(tr, "span", "sortarrow");
for (var i = 0; i < spans.length; i++) {
spans[i].innerHTML = '<img src="'+ tcncv_image_path + tcncv_image_none + '" alt="↓"/>';
}
span.innerHTML = arrowHTML;
if (tcncv_alternate_row_colors) {
tcncv_alternate(table);
}
}
function tcncv_initTransformTable() {
if ( typeof wgSeparatorTransformTable == "undefined"
|| ( wgSeparatorTransformTable[0] == '' && wgDigitTransformTable[2] == '' ) )
{
digitClass = "[0-9,.]";
tcncv_number_transform_table = false;
} else {
tcncv_number_transform_table = {};
// Unpack the transform table
// Separators
ascii = wgSeparatorTransformTable[0].split("\t");
localised = wgSeparatorTransformTable[1].split("\t");
for ( var i = 0; i < ascii.length; i++ ) {
tcncv_number_transform_table[localised[i]] = ascii[i];
}
// Digits
ascii = wgDigitTransformTable[0].split("\t");
localised = wgDigitTransformTable[1].split("\t");
for ( var i = 0; i < ascii.length; i++ ) {
tcncv_number_transform_table[localised[i]] = ascii[i];
}
// Construct regex for number identification
digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',', '\\.'];
maxDigitLength = 1;
for ( var digit in tcncv_number_transform_table ) {
// Escape regex metacharacters
digits.push(
digit.replace( /[\\\\$\*\+\?\.\(\)\|\{\}\[\]\-]/,
function( s ) { return '\\' + s; } )
);
if (digit.length > maxDigitLength) {
maxDigitLength = digit.length;
}
}
if ( maxDigitLength > 1 ) {
digitClass = '[' + digits.join( '', digits ) + ']';
} else {
digitClass = '(' + digits.join( '|', digits ) + ')';
}
}
// We allow a trailing percent sign, which we just strip. This works fine
// if percents and regular numbers aren't being mixed.
tcncv_number_regex = new RegExp(
"^(" +
"[+-]?[0-9][0-9,]*(\\.[0-9,]*)?(E[+-]?[0-9][0-9,]*)?" + // Fortran-style scientific
"|" +
"[+-]?" + digitClass + "+%?" + // Generic localised
")$", "i"
);
}
function tcncv_toLowerCase( s ) {
return s.toLowerCase();
}
function tcncv_dateToSortKey(date) {
// y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
if (date.length == 11) {
switch (date.substr(3,3).toLowerCase()) {
case "jan": var month = "01"; break;
case "feb": var month = "02"; break;
case "mar": var month = "03"; break;
case "apr": var month = "04"; break;
case "may": var month = "05"; break;
case "jun": var month = "06"; break;
case "jul": var month = "07"; break;
case "aug": var month = "08"; break;
case "sep": var month = "09"; break;
case "oct": var month = "10"; break;
case "nov": var month = "11"; break;
case "dec": var month = "12"; break;
// default: var month = "00";
}
return date.substr(7,4)+month+date.substr(0,2);
} else if (date.length == 10) {
if (tcncv_europeandate == false) {
return date.substr(6,4)+date.substr(0,2)+date.substr(3,2);
} else {
return date.substr(6,4)+date.substr(3,2)+date.substr(0,2);
}
} else if (date.length == 8) {
yr = date.substr(6,2);
if (parseInt(yr) < 50) {
yr = '20'+yr;
} else {
yr = '19'+yr;
}
if (tcncv_europeandate == true) {
return yr+date.substr(3,2)+date.substr(0,2);
} else {
return yr+date.substr(0,2)+date.substr(3,2);
}
}
return "00000000";
}
function tcncv_parseFloat( s ) {
if ( !s ) {
return 0;
}
if (tcncv_number_transform_table != false) {
var newNum = '', c;
for ( var p = 0; p < s.length; p++ ) {
c = s.charAt( p );
if (c in tcncv_number_transform_table) {
newNum += tcncv_number_transform_table[c];
} else {
newNum += c;
}
}
s = newNum;
}
num = parseFloat(s.replace(/,/g, ""));
return (isNaN(num) ? s : num);
}
function tcncv_currencyToSortKey( s ) {
return tcncv_parseFloat(s.replace(/[^0-9.,]/g,''));
}
function tcncv_sort_generic(a, b) {
return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : a[2] - b[2];
}
function tcncv_alternate(table) {
// Take object table and get all it's tbodies.
var tableBodies = table.getElementsByTagName("tbody");
// Loop through these tbodies
for (var i = 0; i < tableBodies.length; i++) {
// Take the tbody, and get all it's rows
var tableRows = tableBodies[i].getElementsByTagName("tr");
// Loop through these rows
// Start at 1 because we want to leave the heading row untouched
for (var j = 0; j < tableRows.length; j++) {
// Check if j is even, and apply classes for both possible results
var oldClasses = tableRows[j].className.split(" ");
var newClassName = "";
for (var k = 0; k < oldClasses.length; k++) {
if (oldClasses[k] != "" && oldClasses[k] != "even" && oldClasses[k] != "odd")
newClassName += oldClasses[k] + " ";
}
tableRows[j].className = newClassName + (j % 2 == 0 ? "even" : "odd");
}
}
}
/*
* End of table sorting code
*/