User:Tcncv/sorttables.js

/* * 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.  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 += ' ' + '' + ' '						+ ' '; }				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 = ''; span.setAttribute('sortdir','up'); } else { arrowHTML = ''; 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 = ''; }	}	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 */