Jump to content

User:Tcncv/sorttables.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Tcncv (talk | contribs) at 06:56, 5 January 2009 (Fix problem with unstable innerHTML in IE). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* 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 += '&nbsp;&nbsp;'
				+ '<a href="#" class="sortheader" '
				+ 'onclick="tcncv_resortTable(this);return false;">'
				+ '<span class="sortarrow">'
				+ '<img src="'
				+ tcncv_image_path
				+ tcncv_image_none
				+ '" alt="&darr;"/></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="&darr;"/>';
		newRows.reverse();
		span.setAttribute('sortdir','up');
	} else {
		arrowHTML = '<img src="'+ tcncv_image_path + tcncv_image_up + '" alt="&uarr;"/>';
		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="&darr;"/>';
	}
	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
 */