/*
* Draft fix for: https://bugzilla.wikimedia.org/show_bug.cgi?id=8028
*
* Current status: Shelved due to limited interest.
*
* Enhancements:
* 1. Will explode rowspans, so that rows are self contained and can be sorted
* without corrupting 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.
* Colspans are preserved during sorting; they are not split.
* 3. After sorting, some cell ranges may be recombined under certain restrictive
* conditions. The class="autorowspan" option can be applied to column
* headers 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 if 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.
* 4. Supports multi-row headers with a mix of rowspans and colspans. Clicking on
* the sort icon in a colspan'd header will perform a multi-column sort. The
* "unsortable" class name can be used in the header to limit sort icon creation.
*
* Bugs/Limitations:
* 1. Conflicting (partially overlapping) rowspans/colspans are not supported. (This
* is invalid in HTML, so is not a worry.)
* 2. Rowspans that add extra table rows are not supported.
* 3. Table row attributes are not compared when combining cells.
* 4. Some intermittent problems observed very early in development when using table
* sorting + Twinkle + IE + with complex pages having large amounts of table data.
* (This problem has not been duplicated.)
*
* Todo:
* 1. Restore thead/tbody/tfoot support (see bugzilla bug id 4740)
* 2. Merge with any recent released source changes.
* 3. Test on other browser configurations.
* 4. Solicit feature discussion and code review.
* 5. Prepare formal test cases and submit to bugzilla.
*
* Related:
* 1. [[Help:Sorting]] - Document new capabilities with examples.
* 2. [[Wikipedia:Catalogue of CSS classes]] - Add new "autorowspan" class and
* update "unsortable" to document new use.
*
* Other possible enhancements (extra complexity may not be justified):
* 1. Consider if it be useful to apply autorowspan to the original table before
* its initial display? (Might same some tedious table formatting effort.)
* 2. Option to automatically apply initial sort.
* 3. Implement mixed mode sorting option (numbers, dates, and text) in a manner
* similar to Excel, and possibly compound sorting of mixed data (9Z < 10A).
* 4. Support fixed columns whose cells do not move with the sort (a lot of work).
*
*/
/* The following is based on code extracted from wikibits
* (/trunk/phase3/skins/common/wikibits.js) revision 45304, Fri Jan 2, 2009.
* (http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/skins/common/wikibits.js?view=log)
* All global variables and functions were renamed from a "ts_" prefix to
* "tsx_" (Table Sort eXperimental) prefix. The table class affected was
* changed from "sortable" to "tsx_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 tsx_image_path = stylepath+"/common/images/";
var tsx_image_up = "sort_up.gif";
var tsx_image_down = "sort_down.gif";
var tsx_image_none = "sort_none.gif";
var tsx_europeandate = mw.config.get('wgContentLanguage') != "en"; // The non-American-inclined can change to "true"
var tsx_alternate_row_colors = false;
var tsx_number_transform_table = null;
var tsx_number_regex = null;
var tsx_SortedColumnRanges = new Array;
function tsx_sortables_init() {
var idnum = 0;
// Find all tables with class sortable and make them sortable
var tables = document.querySelectorAll("table.tsx_sortable");
for (var ti = 0; ti < tables.length ; ti++) {
if (!tables[ti].id) {
tables[ti].setAttribute('id','sortable_table_id_tsx_'+idnum);
++idnum;
}
tsx_makeSortable(tables[ti]);
}
}
addOnloadHook(tsx_sortables_init);
function tsx_makeSortable(table) {
if (!table.rows || table.rows.length == 0) return;
// Count header rows. First row is always considered part of the header.
// Also include rows having class=sortheader" or containing only TH cells.
var numHeaders = 1; // Also equals rowStart
var isHeader = true;
for (var r = 1; r < table.rows.length && isHeader; r++) {
if ((" "+table.rows[r].className+" ").indexOf(" sortheader ") == -1) {
for (var i = 0; i < table.rows[r].cells.length && isHeader; i++) {
if (table.rows[r].cells[i].nodeName.toUpperCase() != "TH") {
isHeader = false;
}
}
}
if (isHeader) numHeaders = r + 1;
}
if (table.rows.length - numHeaders < 2) return;
var repeatedCells = new Array();
for (var r = 0; r < numHeaders; 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
repeatedCells[c].remaining--; // remaining_repeats
c += repeatedCells[c].cell.colSpan;
}
else if (i < row.cells.length ) {
// Use existing defined cell. If rowspan, save for later duplication.
var cell = row.cells[i];
if ((" "+cell.className+" ").indexOf(" unsortable ") == -1) {
cell.innerHTML += ' '
+ '<a href="#" class="sortheader" '
+ 'onclick="tsx_resortTable(this,'
+ numHeaders.toString()
+ ',' + c.toString()
+ ',' + cell.colSpan.toString()
+ ');return false;">'
+ '<span class="sortarrow">'
+ '<img src="'
+ tsx_image_path
+ tsx_image_none
+ '" alt="⇅"/></span></a>';
}
if (cell.rowSpan > 1) {
repeatedCells[c] = new tsx_RepeatedCell(cell);
}
c += cell.colSpan;
i++;
}
else {
c += 1; //undefined cell
i++;
}
}
}
if (tsx_alternate_row_colors) {
tsx_alternate(table);
}
}
function tsx_copyCell(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 tsx_compareCells(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;
}
// Construct object to track remaining occurrences or rowspanned cell
function tsx_RepeatedCell(cell) {
this.cell = cell;
this.remaining = cell.rowSpan - 1;
}
// Identify and duplicate rowspanned cells so that each row has its own copy
function tsx_explodeRowspans(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);
tsx_copyCell(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);
}
repeatedCells[c] = new tsx_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--;
}
}
// Construct object to hold range of adjacent sorted columns
function tsx_SortedColumnRange(table, sortColumn, sortSpan) {
this.id = table.id;
this.from = sortColumn;
this.thru = sortColumn + sortSpan - 1;
this.extend = function tsx_SortedColumnRange_extend(sortColumn, sortSpan) {
// Track columns sorted in sequence from right to left. Reset if jump
var sortThru = sortColumn + sortSpan - 1;
if (sortThru < this.from - 1 || sortThru > this.thru ) this.thru = sortThru;
this.from = sortColumn;
return this;
}
}
// Get and extend range of sorted columns
function tsx_GetSortedColumnRange(table, sortColumn, sortSpan) {
for (var i = 0; i < tsx_SortedColumnRanges.length; i++) {
if (table.id == tsx_SortedColumnRanges[i].id) {
return tsx_SortedColumnRanges[i].extend(sortColumn, sortSpan);
}
}
tsx_SortedColumnRanges.push(new tsx_SortedColumnRange(table, sortColumn, sortSpan));
return tsx_SortedColumnRanges[tsx_SortedColumnRanges.length-1];
}
// Build array, indexed by column number, with a flag indicating if autorowspan is enabled
function tsx_GetAutoRowSpanColumns(table, rowStart, sortColumn, sortSpan) {
var autoRowSpanTable = ((" "+table.className+" ").indexOf(" autorowspan ") >= 0);
var autoRowSpanColumns = new Array();
var repeatedCells = new Array();
for (var r = 0; r < rowStart; 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) {
// Repeat prior rowspanned cell
repeatedCells[c].remaining--; // remaining_repeats
c += repeatedCells[c].cell.colSpan;
}
else if (i < row.cells.length ) {
// Use given cell
var cell = row.cells[i];
if ( autoRowSpanTable || (" "+cell.className+" ").indexOf(" autorowspan ") >= 0 ) {
for (var j = 0; j < cell.colSpan; j++) {
autoRowSpanColumns[c+j] = true;
}
}
if (cell.rowSpan > 1) {
repeatedCells[c] = new tsx_RepeatedCell(cell);
}
c += cell.colSpan;
i++;
}
else {
// Skip undefined cell
c += 1;
i++;
}
}
}
return autoRowSpanColumns;
}
// After sorting, scan for and combine repeated cells, where allowed
function tsx_combineRowspans(table, rowStart, sortColumn, sortSpan) {
var SortedColumnRange = tsx_GetSortedColumnRange(table, sortColumn, sortSpan);
var autoRowSpanColumns = tsx_GetAutoRowSpanColumns(table, rowStart, sortColumn, sortSpan);
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 >= SortedColumnRange.from && c <= SortedColumnRange.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) )
&& tsx_compareCells(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 tsx_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 tsx_resortTable(lnk, rowStart, sortColumn, sortSpan) {
// Get the span containing the sort icon and determine sort direction
var span = lnk.getElementsByTagName('span')[0];
var reverse = (span.getAttribute("sortdir") == 'down');
// Get the table
var td = lnk.parentNode;
var tr = td.parentNode;
var table = tr.parentNode;
while (table && !(table.tagName && table.tagName.toLowerCase() == 'table'))
table = table.parentNode;
if (!table) return;
// Generate the number transform table if it's not done already
if (tsx_number_transform_table == null) {
tsx_initTransformTable();
}
// Expand any rowspan'ed cells that could potentially be split by sort
tsx_explodeRowspans(table,rowStart);
// Sort each column in the selected range from right to left
for (var i = sortSpan - 1; i >= 0 ; i--) {
tsx_sortColumn(table, rowStart, sortColumn + i, reverse);
}
// Merge cells into rowspans, where possible
tsx_combineRowspans(table, rowStart, sortColumn, sortSpan);
var arrowHTML;
if (reverse) {
arrowHTML = '<img src="'+ tsx_image_path + tsx_image_down + '" alt="↓"/>';
span.setAttribute('sortdir','up');
} else {
arrowHTML = '<img src="'+ tsx_image_path + tsx_image_up + '" alt="↑"/>';
span.setAttribute('sortdir','down');
}
// Delete any other arrows there may be showing
for (var r = 0; r < rowStart; r++) {
var spans = table.rows[r].querySelectorAll("span.sortarrow");
for (var i = 0; i < spans.length; i++) {
spans[i].innerHTML = '<img src="'+ tsx_image_path + tsx_image_none + '" alt="⇅"/>';
}
}
span.innerHTML = arrowHTML;
if (tsx_alternate_row_colors) {
tsx_alternate(table);
}
}
function tsx_sortColumn(table, rowStart, column, reverse) {
// Work out a type for the column
var itm = "";
for (var i = rowStart; i < table.rows.length; i++) {
if (table.rows[i].cells.length > column) {
itm = tsx_getInnerText(table.rows[i],column);
itm = itm.replace(/^[\s\xa0]+/, "").replace(/[\s\xa0]+$/, "");
if (itm != "") break;
}
}
// TODO: bug 8226, localised date formats
var sortfn = tsx_sort_generic;
var preprocessor = tsx_toLowerCase;
if (/^\d\d[\/. -][a-zA-Z]{3}[\/. -]\d\d\d\d$/.test(itm)) {
preprocessor = tsx_dateToSortKey;
} else if (/^\d\d[\/.-]\d\d[\/.-]\d\d\d\d$/.test(itm)) {
preprocessor = tsx_dateToSortKey;
} else if (/^\d\d[\/.-]\d\d[\/.-]\d\d$/.test(itm)) {
preprocessor = tsx_dateToSortKey;
// pound dollar euro yen currency cents
} else if (/(^[\u00a3$\u20ac\u00a4\u00a5]|\u00a2$)/.test(itm)) {
preprocessor = tsx_currencyToSortKey;
} else if (tsx_number_regex.test(itm)) {
preprocessor = tsx_parseFloat;
}
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 = tsx_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);
if (reverse) newRows.reverse();
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]);
}
}
function tsx_initTransformTable() {
if ( typeof mw.config.get('wgSeparatorTransformTable') == "undefined"
|| ( mw.config.get('wgSeparatorTransformTable')[0] == '' && mw.config.get('wgDigitTransformTable')[2] == '' ) )
{
digitClass = "[0-9,.]";
tsx_number_transform_table = false;
} else {
tsx_number_transform_table = {};
// Unpack the transform table
// Separators
ascii = mw.config.get('wgSeparatorTransformTable')[0].split("\t");
localised = mw.config.get('wgSeparatorTransformTable')[1].split("\t");
for ( var i = 0; i < ascii.length; i++ ) {
tsx_number_transform_table[localised[i]] = ascii[i];
}
// Digits
ascii = mw.config.get('wgDigitTransformTable')[0].split("\t");
localised = mw.config.get('wgDigitTransformTable')[1].split("\t");
for ( var i = 0; i < ascii.length; i++ ) {
tsx_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 tsx_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.
tsx_number_regex = new RegExp(
"^(" +
"[+-]?[0-9][0-9,]*(\\.[0-9,]*)?(E[+-]?[0-9][0-9,]*)?" + // Fortran-style scientific
"|" +
"[+-]?" + digitClass + "+%?" + // Generic localised
")$", "i"
);
}
function tsx_toLowerCase( s ) {
return s.toLowerCase();
}
function tsx_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 (tsx_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 (tsx_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 tsx_parseFloat( s ) {
if ( !s ) {
return 0;
}
if (tsx_number_transform_table != false) {
var newNum = '', c;
for ( var p = 0; p < s.length; p++ ) {
c = s.charAt( p );
if (c in tsx_number_transform_table) {
newNum += tsx_number_transform_table[c];
} else {
newNum += c;
}
}
s = newNum;
}
num = parseFloat(s.replace(/,/g, ""));
return (isNaN(num) ? s : num);
}
function tsx_currencyToSortKey( s ) {
return tsx_parseFloat(s.replace(/[^0-9.,]/g,''));
}
function tsx_sort_generic(a, b) {
return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : a[2] - b[2];
}
function tsx_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
*/