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. |
/*
* Draft fix for: https://bugzilla.wikimedia.org/show_bug.cgi?id=8028
*
* 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. Current merge rules:
* a. Only merge cells in adjacent sorted columns, selected right to left.
* b. Only merge if cells to left also merged, or if leftmost sorted column.
* c. Only merge if cells have same ID or class="autorowspan" is active.
* d. And of course, cells must be equivalent (content and attributes).
* e. Do not merge header, footer (sortbottom) or fixed (unsortable) rows.
*
* Todo:
* 1. (done) Get recombine and autorowspan working on IE.
* 2. (done) Continue refining the recombine and autorowspan behavior.
* 3. (defer) 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.)
* 5. Possible separate enhancement - Support for complex headers possibly including
* single-click multi-column sorting
*/
/* The followinhg is based on code extracted from wikibits
* (/trunk/phase3/skins/common/wikibits.js) revision 45304, Fri Jan 2.
* All global variables and functions were renamed from a ts_ prefix to
* tcncv_ prefix. The table class affected was changed from "sortable"
* to "tcncv_sortable"
*/
/*
* 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;
var tcncv_SortColumnRanges = new Array;
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 tcncv_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;
//row.cells[i].innerHTML = row.cells[i].innerHTML; /* IE morphs this. Needed for compare. */
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 tcncv_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--;
}
}
// Prototype object to hold range of adjacent sorted columns
function tcncv_SortColumnRange(table,sortColumn) {
this.id = table.id;
this.from = sortColumn;
this.thru = sortColumn;
this.extend = function tcncv_SortColumnRange_extend(sortColumn) {
// Track columns sorted in sequence from right to left. Reset if jump
if (sortColumn < this.from - 1 || sortColumn > this.from) this.thru = sortColumn;
this.from = sortColumn;
return this;
}
}
// Get and extend range of sorted columns
function tcncv_GetSortColumnRange(table, sortColumn) {
for (var i = 0; i < tcncv_SortColumnRanges.length; i++) {
if (table.id == tcncv_SortColumnRanges[i].id) {
return tcncv_SortColumnRanges[i].extend(sortColumn);
}
}
tcncv_SortColumnRanges.push(new tcncv_SortColumnRange(table,sortColumn));
return tcncv_SortColumnRanges[tcncv_SortColumnRanges.length-1];
}
// Build array, indexed by column number, with flag indicating if autorowspan is active
function tcncv_GetAutoRowSpanColumns(table, headerRow) {
var autoRowSpanTable = ((" "+table.className+" ").indexOf(" autorowspan ") >= 0);
var autoRowSpanColumns = new Array();
for (var c = 0; c < headerRow.cells.length; c++) {
autoRowSpanColumns[c] = (
autoRowSpanTable
|| (" "+headerRow.cells[c].className+" ").indexOf(" autorowspan ") >= 0
);
}
return autoRowSpanColumns;
}
// After sorting, scan for and combine repeated cells, where allowed
function tcncv_combine_rowspans(table, rowStart, headerRow, sortColumn) {
var sortColumnRange = tcncv_GetSortColumnRange(table,sortColumn);
var autoRowSpanColumns = tcncv_GetAutoRowSpanColumns(table,headerRow, sortColumn);
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)
{
priorCells.length = 0; // Reset - Do skip and not span across fixed rows
}
else {
var c = 0; // column number and priorCells index
var i = 0; // cells index (may be less than column number)
var merging = false;
while (i < row.cells.length) {
// (1) Only merge cells in adjacent sorted columns, selected right to left.
// (2) Only merge if cells to left also merged, or if leftmost sorted column.
// (3) Merge only if cells have same ID or class="autorowspan" is active
// (4) And of course, cells must be equivalent.
if (c >= sortColumnRange.from && c <= sortColumnRange.thru
&& (c == sortColumn || merging)
&& c < priorCells.length && priorCells[c]
&& ( (autoRowSpanColumns.length > c && autoRowSpanColumns[c])
|| (row.cells[i].id != "" && row.cells[i].id == priorCells[c].id) )
&& tcncv_compare_cells(row.cells[i],priorCells[c]) )
{
merging = true;
// Match - update rowspan in prior row's tableCell 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 {
merging = false;
// No match or not allowed - 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
*/