Skip to content

Instantly share code, notes, and snippets.

@KraXen72
Created July 14, 2022 13:34
Show Gist options
  • Save KraXen72/49dabbe50990bf7c8e73b485c86b7ba8 to your computer and use it in GitHub Desktop.
Save KraXen72/49dabbe50990bf7c8e73b485c86b7ba8 to your computer and use it in GitHub Desktop.
tablesorter-mediawiki-patched visually merges same rows back together
/*!
* TableSorter for MediaWiki
*
* Written 2011 Leo Koppelkamm
* Based on tablesorter.com plugin, written (c) 2007 Christian Bach.
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* Depends on mw.config (wgDigitTransformTable, wgDefaultDateFormat, wgPageContentLanguage)
* and mw.language.months.
*
* Uses 'tableSorterCollation' in mw.config (if available)
*
* Create a sortable table with multi-column sorting capabilities
*
* // Create a simple tablesorter interface
* $( 'table' ).tablesorter();
*
* // Create a tablesorter interface, initially sorting on the first and second column
* $( 'table' ).tablesorter( { sortList: [ { 0: 'desc' }, { 1: 'asc' } ] } );
*
* @param {string} [cssHeader="headerSort"] A string of the class name to be appended to sortable
* tr elements in the thead of the table.
*
* @param {string} [cssAsc="headerSortUp"] A string of the class name to be appended to
* sortable tr elements in the thead on a ascending sort.
*
* @param {string} [cssDesc="headerSortDown"] A string of the class name to be appended to
* sortable tr elements in the thead on a descending sort.
*
* @param {string} [sortMultisortKey="shiftKey"] A string of the multi-column sort key.
*
* @param {boolean} [cancelSelection=true] Boolean flag indicating iftablesorter should cancel
* selection of the table headers text.
*
* @param {Array} [sortList] An array containing objects specifying sorting. By passing more
* than one object, multi-sorting will be applied. Object structure:
* { <Integer column index>: <String 'asc' or 'desc'> }
*
* @event sortEnd.tablesorter: Triggered as soon as any sorting has been applied.
*
* @author Christian Bach/christian.bach@polyester.se
*/
// source for this is https://phabricator.wikimedia.org/source/mediawiki/browse/master/resources/src/jquery.tablesorter/jquery.tablesorter.js
// stuff i added. i also added line 1057
function mergeClonedRows(table, nthType) {
const tbody = table.querySelector("tbody")
const rows = [...tbody.querySelectorAll("tr")]
const groups = []
for (let i = 0; i < rows.length; i++) {
const thisTd = rows[i].querySelector(`td:nth-of-type(${nthType})`)
thisTd.style.display = "table-cell"
thisTd.setAttribute("rowspan", 1)
if (i !== 0) {
const lastTd = rows[i-1].querySelector(`td:nth-of-type(${nthType})`)
if (lastTd.innerHTML === thisTd.innerHTML) {
groups[groups.length - 1].push(thisTd) // add into last existing group
} else {
groups.push([thisTd]) // make a new group
}
} else {
groups.push([thisTd]) // make anew group since it's the first element
}
}
// the first one in the group gets to stay with wide rowspan, others get hidden
groups.forEach(group => {
group[0].setAttribute("rowspan", group.length)
const remainder = group.slice(1)
remainder.forEach(r => r.style.display = "none")
})
}
// rest of the code from the source
( function () {
var ts,
parsers = [];
/* Parser utility functions */
function getParserById( name ) {
for ( var i = 0; i < parsers.length; i++ ) {
if ( parsers[ i ].id.toLowerCase() === name.toLowerCase() ) {
return parsers[ i ];
}
}
return false;
}
/**
* @param {HTMLElement} node
* @return {string}
*/
function getElementSortKey( node ) {
// Get data-sort-value attribute. Uses jQuery to allow live value
// changes from other code paths via data(), which reside only in jQuery.
// Must use $().data() instead of $.data(), as the latter *only*
// accesses the live values, without reading HTML5 attribs first (T40152).
var data = $( node ).data( 'sortValue' );
if ( data !== null && data !== undefined ) {
// Cast any numbers or other stuff to a string, methods
// like charAt, toLowerCase and split are expected.
return String( data );
}
if ( node.tagName.toLowerCase() === 'img' ) {
return node.alt;
}
// Iterate the NodeList (not an array).
// Also uses null-return as filter in the same pass.
// eslint-disable-next-line no-jquery/no-map-util
return $.map( node.childNodes, function ( elem ) {
if ( elem.nodeType === Node.ELEMENT_NODE ) {
if ( elem.nodeName.toLowerCase() === 'style' ) {
return null;
}
if ( elem.classList.contains( 'reference' ) ) {
return null;
}
return getElementSortKey( elem );
}
if ( elem.nodeType === Node.TEXT_NODE ) {
return elem.textContent;
}
// Ignore other node types, such as HTML comments.
return null;
} ).join( '' );
}
function detectParserForColumn( table, rows, column ) {
var l = parsers.length,
config = $( table ).data( 'tablesorter' ).config,
nextRow = false,
// Start with 1 because 0 is the fallback parser
i = 1,
lastRowIndex = -1,
rowIndex = 0,
concurrent = 0,
empty = 0,
needed = ( rows.length > 4 ) ? 5 : rows.length;
while ( i < l ) {
var cellIndex;
var nodeValue;
// if this is a child row, continue to the next row (as buildCache())
// eslint-disable-next-line no-jquery/no-class-state
if ( rows[ rowIndex ] && !$( rows[ rowIndex ] ).hasClass( config.cssChildRow ) ) {
if ( rowIndex !== lastRowIndex ) {
lastRowIndex = rowIndex;
cellIndex = $( rows[ rowIndex ] ).data( 'columnToCell' )[ column ];
nodeValue = getElementSortKey( rows[ rowIndex ].cells[ cellIndex ] ).trim();
}
} else {
nodeValue = '';
}
if ( nodeValue !== '' ) {
if ( parsers[ i ].is( nodeValue, table ) ) {
concurrent++;
nextRow = true;
if ( concurrent >= needed ) {
// Confirmed the parser for multiple cells, let's return it
return parsers[ i ];
}
} else if ( parsers[ i ].id.match( /isoDate/ ) && /^\D*(\d{1,4}) ?(\[.+\])?$/.test( nodeValue ) ) {
// For 1-4 digits and maybe reference(s) parser "isoDate" or "number" is possible, check next row
empty++;
nextRow = true;
} else {
// Check next parser, reset rows
i++;
rowIndex = 0;
concurrent = 0;
empty = 0;
nextRow = false;
}
} else {
// Empty cell
empty++;
nextRow = true;
}
if ( nextRow ) {
nextRow = false;
rowIndex++;
if ( rowIndex >= rows.length ) {
if ( concurrent > 0 && concurrent >= rows.length - empty ) {
// Confirmed the parser for all filled cells
return parsers[ i ];
}
// Check next parser, reset rows
i++;
rowIndex = 0;
concurrent = 0;
empty = 0;
}
}
}
// 0 is always the generic parser (text)
return parsers[ 0 ];
}
function buildParserCache( table, $headers ) {
var rows = table.tBodies[ 0 ].rows,
config = $( table ).data( 'tablesorter' ).config,
cachedParsers = [];
if ( rows[ 0 ] ) {
for ( var j = 0; j < config.columns; j++ ) {
var parser = false;
var sortType = $headers.eq( config.columnToHeader[ j ] ).data( 'sortType' );
if ( sortType !== undefined ) {
parser = getParserById( sortType );
}
if ( parser === false ) {
parser = detectParserForColumn( table, rows, j );
}
cachedParsers.push( parser );
}
}
return cachedParsers;
}
/* Other utility functions */
function buildCache( table ) {
var totalRows = ( table.tBodies[ 0 ] && table.tBodies[ 0 ].rows.length ) || 0,
config = $( table ).data( 'tablesorter' ).config,
cachedParsers = config.parsers,
cellIndex,
cache = {
row: [],
normalized: []
};
for ( var i = 0; i < totalRows; i++ ) {
// Add the table data to main data array
var $row = $( table.tBodies[ 0 ].rows[ i ] );
var cols = [];
// if this is a child row, add it to the last row's children and
// continue to the next row
// eslint-disable-next-line no-jquery/no-class-state
if ( $row.hasClass( config.cssChildRow ) ) {
cache.row[ cache.row.length - 1 ] = cache.row[ cache.row.length - 1 ].add( $row );
// go to the next for loop
continue;
}
cache.row.push( $row );
if ( $row.data( 'initialOrder' ) === undefined ) {
$row.data( 'initialOrder', i );
}
for ( var j = 0; j < cachedParsers.length; j++ ) {
cellIndex = $row.data( 'columnToCell' )[ j ];
cols.push( cachedParsers[ j ].format( getElementSortKey( $row[ 0 ].cells[ cellIndex ] ) ) );
}
// Store the initial sort order, from when the page was loaded
cols.push( $row.data( 'initialOrder' ) );
// Store the current sort order, before rows are re-sorted
cols.push( cache.normalized.length );
cache.normalized.push( cols );
cols = null;
}
return cache;
}
function appendToTable( table, cache ) {
var row = cache.row,
normalized = cache.normalized,
totalRows = normalized.length,
checkCell = ( normalized[ 0 ].length - 1 ),
fragment = document.createDocumentFragment();
for ( var i = 0; i < totalRows; i++ ) {
var pos = normalized[ i ][ checkCell ];
var l = row[ pos ].length;
for ( var j = 0; j < l; j++ ) {
fragment.appendChild( row[ pos ][ j ] );
}
}
table.tBodies[ 0 ].appendChild( fragment );
$( table ).trigger( 'sortEnd.tablesorter' );
}
/**
* Find all header rows in a thead-less table and put them in a <thead> tag.
* This only treats a row as a header row if it contains only <th>s (no <td>s)
* and if it is preceded entirely by header rows. The algorithm stops when
* it encounters the first non-header row.
*
* After this, it will look at all rows at the bottom for footer rows
* And place these in a tfoot using similar rules.
*
* @param {jQuery} $table object for a <table>
*/
function emulateTHeadAndFoot( $table ) {
var $rows = $table.find( '> tbody > tr' );
if ( !$table.get( 0 ).tHead ) {
var $thead = $( '<thead>' );
$rows.each( function () {
if ( $( this ).children( 'td' ).length ) {
// This row contains a <td>, so it's not a header row
// Stop here
return false;
}
$thead.append( this );
} );
$table.find( '> tbody' ).first().before( $thead );
}
if ( !$table.get( 0 ).tFoot ) {
var $tfoot = $( '<tfoot>' );
var len = $rows.length;
for ( var i = len - 1; i >= 0; i-- ) {
if ( $( $rows[ i ] ).children( 'td' ).length ) {
break;
}
$tfoot.prepend( $( $rows[ i ] ) );
}
$table.append( $tfoot );
}
}
function uniqueElements( array ) {
var uniques = [];
array.forEach( function ( elem ) {
if ( elem !== undefined && uniques.indexOf( elem ) === -1 ) {
uniques.push( elem );
}
} );
return uniques;
}
function buildHeaders( table, msg ) {
var config = $( table ).data( 'tablesorter' ).config,
maxSeen = 0,
colspanOffset = 0,
$tableHeaders = $( [] ),
$tableRows = $( table ).find( 'thead' ).eq( 0 ).find( '> tr:not(.sorttop)' );
if ( $tableRows.length <= 1 ) {
$tableHeaders = $tableRows.children( 'th' );
} else {
var exploded = [];
// Loop through all the dom cells of the thead
$tableRows.each( function ( rowIndex, row ) {
// eslint-disable-next-line no-jquery/no-each-util
$.each( row.cells, function ( columnIndex, cell ) {
var rowspan = Number( cell.rowSpan );
var colspan = Number( cell.colSpan );
// Skip the spots in the exploded matrix that are already filled
while ( exploded[ rowIndex ] && exploded[ rowIndex ][ columnIndex ] !== undefined ) {
++columnIndex;
}
var matrixRowIndex,
matrixColumnIndex;
// Find the actual dimensions of the thead, by placing each cell
// in the exploded matrix rowspan times colspan times, with the proper offsets
for ( matrixColumnIndex = columnIndex; matrixColumnIndex < columnIndex + colspan; ++matrixColumnIndex ) {
for ( matrixRowIndex = rowIndex; matrixRowIndex < rowIndex + rowspan; ++matrixRowIndex ) {
if ( !exploded[ matrixRowIndex ] ) {
exploded[ matrixRowIndex ] = [];
}
exploded[ matrixRowIndex ][ matrixColumnIndex ] = cell;
}
}
} );
} );
var longestTR;
// We want to find the row that has the most columns (ignoring colspan)
exploded.forEach( function ( cellArray, index ) {
var headerCount = $( uniqueElements( cellArray ) ).filter( 'th' ).length;
if ( headerCount >= maxSeen ) {
maxSeen = headerCount;
longestTR = index;
}
} );
// We cannot use $.unique() here because it sorts into dom order, which is undesirable
$tableHeaders = $( uniqueElements( exploded[ longestTR ] ) ).filter( 'th' );
}
// as each header can span over multiple columns (using colspan=N),
// we have to bidirectionally map headers to their columns and columns to their headers
config.columnToHeader = [];
config.headerToColumns = [];
config.headerList = [];
var headerIndex = 0;
$tableHeaders.each( function () {
var $cell = $( this );
var columns = [];
// eslint-disable-next-line no-jquery/no-class-state
if ( !$cell.hasClass( config.unsortableClass ) ) {
$cell
// The following classes are used here:
// * headerSort
// * other passed by config
.addClass( config.cssHeader )
.prop( 'tabIndex', 0 )
.attr( {
role: 'columnheader button',
title: msg[ 2 ]
} );
for ( var k = 0; k < this.colSpan; k++ ) {
config.columnToHeader[ colspanOffset + k ] = headerIndex;
columns.push( colspanOffset + k );
}
config.headerToColumns[ headerIndex ] = columns;
$cell.data( {
headerIndex: headerIndex,
order: 0,
count: 0
} );
// add only sortable cells to headerList
config.headerList[ headerIndex ] = this;
headerIndex++;
}
colspanOffset += this.colSpan;
} );
// number of columns with extended colspan, inclusive unsortable
// parsers[j], cache[][j], columnToHeader[j], columnToCell[j] have so many elements
config.columns = colspanOffset;
return $tableHeaders.not( '.' + config.unsortableClass );
}
function isValueInArray( v, a ) {
for ( var i = 0; i < a.length; i++ ) {
if ( a[ i ][ 0 ] === v ) {
return true;
}
}
return false;
}
/**
* Sets the sort count of the columns that are not affected by the sorting to have them sorted
* in default (ascending) order when their header cell is clicked the next time.
*
* @param {jQuery} $headers
* @param {Array} sortList 2D number array
* @param {Array} headerToColumns 2D number array
*/
function setHeadersOrder( $headers, sortList, headerToColumns ) {
// Loop through all headers to retrieve the indices of the columns the header spans across:
headerToColumns.forEach( function ( columns, headerIndex ) {
columns.forEach( function ( columnIndex, i ) {
var header = $headers[ headerIndex ],
$header = $( header );
if ( !isValueInArray( columnIndex, sortList ) ) {
// Column shall not be sorted: Reset header count and order.
$header.data( {
order: 0,
count: 0
} );
} else {
// Column shall be sorted: Apply designated count and order.
for ( var j = 0; j < sortList.length; j++ ) {
var sortColumn = sortList[ j ];
if ( sortColumn[ 0 ] === i ) {
$header.data( {
order: sortColumn[ 1 ],
count: sortColumn[ 1 ] + 1
} );
break;
}
}
}
} );
} );
}
function setHeadersCss( table, $headers, list, css, msg, columnToHeader ) {
// Remove all header information and reset titles to default message
// The following classes are used here:
// * headerSortUp
// * headerSortDown
$headers.removeClass( css ).attr( 'title', msg[ 2 ] );
for ( var i = 0; i < list.length; i++ ) {
// The following classes are used here:
// * headerSortUp
// * headerSortDown
$headers
.eq( columnToHeader[ list[ i ][ 0 ] ] )
.addClass( css[ list[ i ][ 1 ] ] )
.attr( 'title', msg[ list[ i ][ 1 ] ] );
}
}
function sortText( a, b ) {
return ts.collator.compare( a, b );
}
function sortNumeric( a, b ) {
return ( ( a < b ) ? -1 : ( ( a > b ) ? 1 : 0 ) );
}
function multisort( table, sortList, cache ) {
var sortFn = [],
cachedParsers = $( table ).data( 'tablesorter' ).config.parsers;
for ( var i = 0; i < sortList.length; i++ ) {
// Android doesn't support Intl.Collator
if ( window.Intl && Intl.Collator && cachedParsers[ sortList[ i ][ 0 ] ].type === 'text' ) {
sortFn[ i ] = sortText;
} else {
sortFn[ i ] = sortNumeric;
}
}
cache.normalized.sort( function ( array1, array2 ) {
for ( var n = 0; n < sortList.length; n++ ) {
var col = sortList[ n ][ 0 ];
var ret;
if ( sortList[ n ][ 1 ] === 2 ) {
// initial order
var orderIndex = array1.length - 2;
ret = sortNumeric.call( this, array1[ orderIndex ], array2[ orderIndex ] );
} else if ( sortList[ n ][ 1 ] === 1 ) {
// descending
ret = sortFn[ n ].call( this, array2[ col ], array1[ col ] );
} else {
// ascending
ret = sortFn[ n ].call( this, array1[ col ], array2[ col ] );
}
if ( ret !== 0 ) {
return ret;
}
}
// Fall back to index number column to ensure stable sort
return sortText.call( this, array1[ array1.length - 1 ], array2[ array2.length - 1 ] );
} );
return cache;
}
function buildTransformTable() {
var digits = '0123456789,.'.split( '' ),
separatorTransformTable = mw.config.get( 'wgSeparatorTransformTable' ),
digitTransformTable = mw.config.get( 'wgDigitTransformTable' );
if ( separatorTransformTable === null || ( separatorTransformTable[ 0 ] === '' && digitTransformTable[ 2 ] === '' ) ) {
ts.transformTable = false;
} else {
ts.transformTable = {};
// Unpack the transform table
var ascii = separatorTransformTable[ 0 ].split( '\t' ).concat( digitTransformTable[ 0 ].split( '\t' ) );
var localised = separatorTransformTable[ 1 ].split( '\t' ).concat( digitTransformTable[ 1 ].split( '\t' ) );
// Construct regexes for number identification
for ( var i = 0; i < ascii.length; i++ ) {
ts.transformTable[ localised[ i ] ] = ascii[ i ];
digits.push( mw.util.escapeRegExp( localised[ i ] ) );
}
}
var 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.
ts.numberRegex = new RegExp(
'^(' +
'[-+\u2212]?[0-9][0-9,]*(\\.[0-9,]*)?(E[-+\u2212]?[0-9][0-9,]*)?' + // Fortran-style scientific
'|' +
'[-+\u2212]?' + digitClass + '+[\\s\\xa0]*%?' + // Generic localised
')$',
'i'
);
}
function buildDateTable() {
var regex = [];
ts.monthNames = {};
for ( var i = 0; i < 12; i++ ) {
var name = mw.language.months.names[ i ].toLowerCase();
ts.monthNames[ name ] = i + 1;
regex.push( mw.util.escapeRegExp( name ) );
name = mw.language.months.genitive[ i ].toLowerCase();
ts.monthNames[ name ] = i + 1;
regex.push( mw.util.escapeRegExp( name ) );
name = mw.language.months.abbrev[ i ].toLowerCase().replace( '.', '' );
ts.monthNames[ name ] = i + 1;
regex.push( mw.util.escapeRegExp( name ) );
}
// Build piped string
regex = regex.join( '|' );
// Build RegEx
// Any date formated with . , ' - or /
ts.dateRegex[ 0 ] = new RegExp( /^\s*(\d{1,2})[,.\-/'\s]{1,2}(\d{1,2})[,.\-/'\s]{1,2}(\d{2,4})\s*?/i );
// Written Month name, dmy
ts.dateRegex[ 1 ] = new RegExp(
'^\\s*(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(' +
regex +
')' +
'[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$',
'i'
);
// Written Month name, mdy
ts.dateRegex[ 2 ] = new RegExp(
'^\\s*(' + regex + ')' +
'[\\,\\.\\-\\/\'\\s]+(\\d{1,2})[\\,\\.\\-\\/\'\\s]+(\\d{2,4})\\s*$',
'i'
);
}
/**
* Replace all rowspanned cells in the body with clones in each row, so sorting
* need not worry about them.
*
* @param {jQuery} $table jQuery object for a <table>
*/
function explodeRowspans( $table ) {
var spanningRealCellIndex, colSpan,
rowspanCells = $table.find( '> tbody > tr > [rowspan]' ).get();
// Short circuit
if ( !rowspanCells.length ) {
return;
}
// First, we need to make a property like cellIndex but taking into
// account colspans. We also cache the rowIndex to avoid having to take
// cell.parentNode.rowIndex in the sorting function below.
$table.find( '> tbody > tr' ).each( function () {
var col = 0;
for ( var c = 0; c < this.cells.length; c++ ) {
$( this.cells[ c ] ).data( 'tablesorter', {
realCellIndex: col,
realRowIndex: this.rowIndex
} );
col += this.cells[ c ].colSpan;
}
} );
// Split multi row cells into multiple cells with the same content.
// Sort by column then row index to avoid problems with odd table structures.
// Re-sort whenever a rowspanned cell's realCellIndex is changed, because it
// might change the sort order.
function resortCells() {
rowspanCells = rowspanCells.sort( function ( a, b ) {
var cellAData = $.data( a, 'tablesorter' );
var cellBData = $.data( b, 'tablesorter' );
var ret = cellAData.realCellIndex - cellBData.realCellIndex;
if ( !ret ) {
ret = cellAData.realRowIndex - cellBData.realRowIndex;
}
return ret;
} );
rowspanCells.forEach( function ( cellNode ) {
$.data( cellNode, 'tablesorter' ).needResort = false;
} );
}
resortCells();
function filterfunc() {
return $.data( this, 'tablesorter' ).realCellIndex >= spanningRealCellIndex;
}
function fixTdCellIndex() {
$.data( this, 'tablesorter' ).realCellIndex += colSpan;
if ( this.rowSpan > 1 ) {
$.data( this, 'tablesorter' ).needResort = true;
}
}
while ( rowspanCells.length ) {
if ( $.data( rowspanCells[ 0 ], 'tablesorter' ).needResort ) {
resortCells();
}
var cell = rowspanCells.shift();
var cellData = $.data( cell, 'tablesorter' );
var rowSpan = cell.rowSpan;
colSpan = cell.colSpan;
spanningRealCellIndex = cellData.realCellIndex;
cell.rowSpan = 1;
var $nextRows = $( cell ).parent().nextAll();
for ( var i = 0; i < rowSpan - 1; i++ ) {
var row = $nextRows[ i ];
if ( !row ) {
// Badly formatted HTML for table.
// Ignore this row, but leave a warning for someone to be able to find this.
// Perhaps in future this could be a wikitext linter rule, or preview warning
// on the edit page.
mw.log.warn( mw.message( 'sort-rowspan-error' ).plain() );
break;
}
var $tds = $( row.cells ).filter( filterfunc );
var $clone = $( cell ).clone();
$clone.data( 'tablesorter', {
realCellIndex: spanningRealCellIndex,
realRowIndex: cellData.realRowIndex + i,
needResort: true
} );
if ( $tds.length ) {
$tds.each( fixTdCellIndex );
$tds.first().before( $clone );
} else {
$nextRows.eq( i ).append( $clone );
}
}
}
}
/**
* Build index to handle colspanned cells in the body.
* Set the cell index for each column in an array,
* so that colspaned cells set multiple in this array.
* columnToCell[collumnIndex] point at the real cell in this row.
*
* @param {jQuery} $table object for a <table>
*/
function manageColspans( $table ) {
var $rows = $table.find( '> tbody > tr' ),
totalRows = $rows.length || 0,
config = $table.data( 'tablesorter' ).config,
columns = config.columns,
columnToCell, cellsInRow, index;
for ( var i = 0; i < totalRows; i++ ) {
var $row = $rows.eq( i );
// if this is a child row, continue to the next row (as buildCache())
// eslint-disable-next-line no-jquery/no-class-state
if ( $row.hasClass( config.cssChildRow ) ) {
// go to the next for loop
continue;
}
columnToCell = [];
cellsInRow = ( $row[ 0 ].cells.length ) || 0; // all cells in this row
index = 0; // real cell index in this row
for ( var j = 0; j < columns; index++ ) {
if ( index === cellsInRow ) {
// Row with cells less than columns: add empty cell
$row.append( '<td>' );
cellsInRow++;
}
for ( var k = 0; k < $row[ 0 ].cells[ index ].colSpan; k++ ) {
columnToCell[ j++ ] = index;
}
}
// Store it in $row
$row.data( 'columnToCell', columnToCell );
}
}
function buildCollation() {
var keys = [];
ts.collationTable = mw.config.get( 'tableSorterCollation' );
ts.collationRegex = null;
if ( ts.collationTable ) {
// Build array of key names
for ( var key in ts.collationTable ) {
keys.push( mw.util.escapeRegExp( key ) );
}
if ( keys.length ) {
ts.collationRegex = new RegExp( keys.join( '|' ), 'ig' );
}
}
if ( window.Intl && Intl.Collator ) {
ts.collator = new Intl.Collator( [
mw.config.get( 'wgPageContentLanguage' ),
mw.config.get( 'wgUserLanguage' )
], {
numeric: true
} );
}
}
function cacheRegexs() {
if ( ts.rgx ) {
return;
}
ts.rgx = {
IPAddress: [
new RegExp( /^\d{1,3}[.]\d{1,3}[.]\d{1,3}[.]\d{1,3}$/ )
],
currency: [
new RegExp( /(^[£$€¥]|[£$€¥]$)/ ),
new RegExp( /[£$€¥]/g )
],
url: [
new RegExp( /^(https?|ftp|file):\/\/$/ ),
new RegExp( /(https?|ftp|file):\/\// )
],
isoDate: [
new RegExp( /^[^-\d]*(-?\d{1,4})-(0\d|1[0-2])(-([0-3]\d))?([T\s]([01]\d|2[0-4]):?(([0-5]\d):?(([0-5]\d|60)([.,]\d{1,3})?)?)?([zZ]|([-+])([01]\d|2[0-3]):?([0-5]\d)?)?)?/ ),
new RegExp( /^[^-\d]*(-?\d{1,4})-?(\d\d)?(-?(\d\d))?([T\s](\d\d):?((\d\d)?:?((\d\d)?([.,]\d{1,3})?)?)?([zZ]|([-+])(\d\d):?(\d\d)?)?)?/ )
],
usLongDate: [
new RegExp( /^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/ )
],
time: [
new RegExp( /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/ )
]
};
}
/**
* Converts sort objects [ { Integer: String }, ... ] to the internally used nested array
* structure [ [ Integer, Integer ], ... ]
*
* @param {Array} sortObjects List of sort objects.
* @return {Array} List of internal sort definitions.
*/
function convertSortList( sortObjects ) {
var sortList = [];
sortObjects.forEach( function ( sortObject ) {
// eslint-disable-next-line no-jquery/no-each-util
$.each( sortObject, function ( columnIndex, order ) {
var orderIndex = ( order === 'desc' ) ? 1 : 0;
sortList.push( [ parseInt( columnIndex, 10 ), orderIndex ] );
} );
} );
return sortList;
}
/* Public scope */
$.tablesorter = {
defaultOptions: {
cssHeader: 'headerSort',
cssAsc: 'headerSortUp',
cssDesc: 'headerSortDown',
cssInitial: '',
cssChildRow: 'expand-child',
sortMultiSortKey: 'shiftKey',
unsortableClass: 'unsortable',
parsers: [],
cancelSelection: true,
sortList: [],
headerList: [],
headerToColumns: [],
columnToHeader: [],
columns: 0
},
dateRegex: [],
monthNames: {},
/**
* @param {jQuery} $tables
* @param {Object} [settings]
* @return {jQuery}
*/
construct: function ( $tables, settings ) {
return $tables.each( function ( i, table ) {
// Declare and cache.
var cache,
$table = $( table ),
firstTime = true;
// Don't construct twice on the same table
if ( $.data( table, 'tablesorter' ) ) {
return;
}
// Quit if no tbody
if ( !table.tBodies ) {
return;
}
if ( !table.tHead ) {
// No thead found. Look for rows with <th>s and
// move them into a <thead> tag or a <tfoot> tag
emulateTHeadAndFoot( $table );
// Still no thead? Then quit
if ( !table.tHead ) {
return;
}
}
// The `sortable` class is used to identify tables which will become sortable
// If not used it will create a FOUC but it should be added since the sortable class
// is responsible for certain crucial style elements. If the class is already present
// this action will be harmless.
$table.addClass( 'jquery-tablesorter sortable' );
// Merge and extend
var config = $.extend( {}, $.tablesorter.defaultOptions, settings );
// Save the settings where they read
$.data( table, 'tablesorter', { config: config } );
// Get the CSS class names, could be done elsewhere
var sortCSS = [ config.cssAsc, config.cssDesc, config.cssInitial ];
// Messages tell the user what the *next* state will be
// so are shifted by one relative to the CSS classes.
var sortMsg = [ mw.msg( 'sort-descending' ), mw.msg( 'sort-initial' ), mw.msg( 'sort-ascending' ) ];
// Build headers
var $headers = buildHeaders( table, sortMsg );
// Grab and process locale settings.
buildTransformTable();
buildDateTable();
// Precaching regexps can bring 10 fold
// performance improvements in some browsers.
cacheRegexs();
function setupForFirstSort() {
var $tfoot, $sortbottoms, $sorttops;
firstTime = false;
// Defer buildCollationTable to first sort. As user and site scripts
// may customize tableSorterCollation but load after $.ready(), other
// scripts may call .tablesorter() before they have done the
// tableSorterCollation customizations.
buildCollation();
// Move .sortbottom rows to the <tfoot> at the bottom of the <table>
$sortbottoms = $table.find( '> tbody > tr.sortbottom' );
if ( $sortbottoms.length ) {
$tfoot = $table.children( 'tfoot' );
if ( $tfoot.length ) {
$tfoot.eq( 0 ).prepend( $sortbottoms );
} else {
$table.append( $( '<tfoot>' ).append( $sortbottoms ) );
}
}
// Move .sorttop rows to the <thead> at the top of the <table>
// <thead> should exist if we got this far
$sorttops = $table.find( '> tbody > tr.sorttop' );
if ( $sorttops.length ) {
$table.children( 'thead' ).append( $sorttops );
}
explodeRowspans( $table );
manageColspans( $table );
// Try to auto detect column type, and store in tables config
config.parsers = buildParserCache( table, $headers );
}
// Apply event handling to headers
// this is too big, perhaps break it out?
$headers.on( 'keypress click', function ( e ) {
if ( e.type === 'click' && e.target.nodeName.toLowerCase() === 'a' ) {
// The user clicked on a link inside a table header.
// Do nothing and let the default link click action continue.
return true;
}
if ( e.type === 'keypress' && e.which !== 13 ) {
// Only handle keypresses on the "Enter" key.
return true;
}
if ( firstTime ) {
setupForFirstSort();
}
// Build the cache for the tbody cells
// to share between calculations for this sort action.
// Re-calculated each time a sort action is performed due to possibility
// that sort values change. Shouldn't be too expensive, but if it becomes
// too slow an event based system should be implemented somehow where
// cells get event .change() and bubbles up to the <table> here
cache = buildCache( table );
var totalRows = ( $table[ 0 ].tBodies[ 0 ] && $table[ 0 ].tBodies[ 0 ].rows.length ) || 0;
if ( totalRows > 0 ) {
var cell = this;
var $cell = $( cell );
var numSortOrders = 3;
// Get current column sort order
$cell.data( {
order: $cell.data( 'count' ) % numSortOrders,
count: $cell.data( 'count' ) + 1
} );
// Get current column index
var columns = config.headerToColumns[ $cell.data( 'headerIndex' ) ];
var newSortList = columns.map( function ( c ) {
return [ c, $cell.data( 'order' ) ];
} );
// Index of first column belonging to this header
var col = columns[ 0 ];
if ( !e[ config.sortMultiSortKey ] ) {
// User only wants to sort on one column set
// Flush the sort list and add new columns
config.sortList = newSortList;
} else {
// Multi column sorting
// It is not possible for one column to belong to multiple headers,
// so this is okay - we don't need to check for every value in the columns array
if ( isValueInArray( col, config.sortList ) ) {
// The user has clicked on an already sorted column.
// Reverse the sorting direction for all tables.
for ( var j = 0; j < config.sortList.length; j++ ) {
var s = config.sortList[ j ];
var o = config.headerList[ config.columnToHeader[ s[ 0 ] ] ];
if ( isValueInArray( s[ 0 ], newSortList ) ) {
$( o ).data( 'count', s[ 1 ] + 1 );
s[ 1 ] = $( o ).data( 'count' ) % numSortOrders;
}
}
} else {
// Add columns to sort list array
config.sortList = config.sortList.concat( newSortList );
}
}
// Reset order/counts of cells not affected by sorting
setHeadersOrder( $headers, config.sortList, config.headerToColumns );
// Set CSS for headers
setHeadersCss( $table[ 0 ], $headers, config.sortList, sortCSS, sortMsg, config.columnToHeader );
appendToTable(
$table[ 0 ], multisort( $table[ 0 ], config.sortList, cache )
);
// stuff i added
for (let i = 1; i < $headers.length + 1; i++) mergeClonedRows($table[ 0 ], i) // try to merge all columns
// Stop normal event by returning false
return false;
}
// Cancel selection
} ).on( 'mousedown', function () {
if ( config.cancelSelection ) {
this.onselectstart = function () {
return false;
};
return false;
}
} );
/**
* Sorts the table. If no sorting is specified by passing a list of sort
* objects, the table is sorted according to the initial sorting order.
* Passing an empty array will reset sorting (basically just reset the headers
* making the table appear unsorted).
*
* @param {Array} [sortList] List of sort objects.
*/
$table.data( 'tablesorter' ).sort = function ( sortList ) {
if ( firstTime ) {
setupForFirstSort();
}
if ( sortList === undefined ) {
sortList = config.sortList;
} else if ( sortList.length > 0 ) {
sortList = convertSortList( sortList );
}
// Set each column's sort count to be able to determine the correct sort
// order when clicking on a header cell the next time
setHeadersOrder( $headers, sortList, config.headerToColumns );
// re-build the cache for the tbody cells
cache = buildCache( table );
// set css for headers
setHeadersCss( table, $headers, sortList, sortCSS, sortMsg, config.columnToHeader );
// sort the table and append it to the dom
appendToTable( table, multisort( table, sortList, cache ) );
};
// sort initially
if ( config.sortList.length > 0 ) {
config.sortList = convertSortList( config.sortList );
$table.data( 'tablesorter' ).sort();
}
} );
},
addParser: function ( parser ) {
if ( !getParserById( parser.id ) ) {
parsers.push( parser );
}
},
formatDigit: function ( s ) {
if ( ts.transformTable !== false ) {
var out = '';
for ( var p = 0; p < s.length; p++ ) {
var c = s.charAt( p );
if ( c in ts.transformTable ) {
out += ts.transformTable[ c ];
} else {
out += c;
}
}
s = out;
}
var i = parseFloat( s.replace( /[, ]/g, '' ).replace( '\u2212', '-' ) );
return isNaN( i ) ? -Infinity : i;
},
formatFloat: function ( s ) {
var i = parseFloat( s );
return isNaN( i ) ? -Infinity : i;
},
formatInt: function ( s ) {
var i = parseInt( s, 10 );
return isNaN( i ) ? -Infinity : i;
},
clearTableBody: function ( table ) {
$( table.tBodies[ 0 ] ).empty();
},
getParser: function ( id ) {
buildTransformTable();
buildDateTable();
cacheRegexs();
buildCollation();
return getParserById( id );
},
getParsers: function () { // for table diagnosis
return parsers;
}
};
// Shortcut
ts = $.tablesorter;
// Register as jQuery prototype method
$.fn.tablesorter = function ( settings ) {
return ts.construct( this, settings );
};
// Add default parsers
ts.addParser( {
id: 'text',
is: function () {
return true;
},
format: function ( s ) {
s = s.trim();
if ( ts.collationRegex ) {
var tsc = ts.collationTable;
s = s.replace( ts.collationRegex, function ( match ) {
var upper = match.toUpperCase(),
lower = match.toLowerCase();
var r;
if ( upper === match && !lower === match ) {
r = tsc[ lower ] ? tsc[ lower ] : tsc[ upper ];
r = r.toUpperCase();
} else {
r = tsc[ lower ];
}
return r;
} );
}
return s;
},
type: 'text'
} );
ts.addParser( {
id: 'IPAddress',
is: function ( s ) {
return ts.rgx.IPAddress[ 0 ].test( s );
},
format: function ( s ) {
var a = s.split( '.' ),
r = '';
for ( var i = 0; i < a.length; i++ ) {
var item = a[ i ];
if ( item.length === 1 ) {
r += '00' + item;
} else if ( item.length === 2 ) {
r += '0' + item;
} else {
r += item;
}
}
return $.tablesorter.formatFloat( r );
},
type: 'numeric'
} );
ts.addParser( {
id: 'currency',
is: function ( s ) {
return ts.rgx.currency[ 0 ].test( s );
},
format: function ( s ) {
return $.tablesorter.formatDigit( s.replace( ts.rgx.currency[ 1 ], '' ) );
},
type: 'numeric'
} );
ts.addParser( {
id: 'url',
is: function ( s ) {
return ts.rgx.url[ 0 ].test( s );
},
format: function ( s ) {
return s.replace( ts.rgx.url[ 1 ], '' ).trim();
},
type: 'text'
} );
ts.addParser( {
id: 'isoDate',
is: function ( s ) {
return ts.rgx.isoDate[ 0 ].test( s );
},
format: function ( s ) {
var match = s.match( ts.rgx.isoDate[ 0 ] );
if ( match === null ) {
// Otherwise a signed number with 1-4 digit is parsed as isoDate
match = s.match( ts.rgx.isoDate[ 1 ] );
}
if ( !match ) {
return -Infinity;
}
var i;
// Month and day
for ( i = 2; i <= 4; i += 2 ) {
if ( !match[ i ] || match[ i ].length === 0 ) {
match[ i ] = 1;
}
}
// Time
for ( i = 6; i <= 15; i++ ) {
if ( !match[ i ] || match[ i ].length === 0 ) {
match[ i ] = '0';
}
}
var ms = parseFloat( match[ 11 ].replace( /,/, '.' ) ) * 1000;
var hOffset = $.tablesorter.formatInt( match[ 13 ] + match[ 14 ] );
var mOffset = $.tablesorter.formatInt( match[ 13 ] + match[ 15 ] );
var isodate = new Date( 0 );
// Because Date constructor changes year 0-99 to 1900-1999, use setUTCFullYear()
isodate.setUTCFullYear( match[ 1 ], match[ 2 ] - 1, match[ 4 ] );
isodate.setUTCHours( match[ 6 ] - hOffset, match[ 8 ] - mOffset, match[ 10 ], ms );
return isodate.getTime();
},
type: 'numeric'
} );
ts.addParser( {
id: 'usLongDate',
is: function ( s ) {
return ts.rgx.usLongDate[ 0 ].test( s );
},
format: function ( s ) {
return $.tablesorter.formatFloat( new Date( s ).getTime() );
},
type: 'numeric'
} );
ts.addParser( {
id: 'date',
is: function ( s ) {
return ( ts.dateRegex[ 0 ].test( s ) || ts.dateRegex[ 1 ].test( s ) || ts.dateRegex[ 2 ].test( s ) );
},
format: function ( s ) {
s = s.toLowerCase().trim();
var match;
if ( ( match = s.match( ts.dateRegex[ 0 ] ) ) !== null ) {
if ( mw.config.get( 'wgDefaultDateFormat' ) === 'mdy' || mw.config.get( 'wgPageContentLanguage' ) === 'en' ) {
s = [ match[ 3 ], match[ 1 ], match[ 2 ] ];
} else if ( mw.config.get( 'wgDefaultDateFormat' ) === 'dmy' ) {
s = [ match[ 3 ], match[ 2 ], match[ 1 ] ];
} else {
// If we get here, we don't know which order the dd-dd-dddd
// date is in. So return something not entirely invalid.
return '99999999';
}
} else if ( ( match = s.match( ts.dateRegex[ 1 ] ) ) !== null ) {
s = [ match[ 3 ], String( ts.monthNames[ match[ 2 ] ] ), match[ 1 ] ];
} else if ( ( match = s.match( ts.dateRegex[ 2 ] ) ) !== null ) {
s = [ match[ 3 ], String( ts.monthNames[ match[ 1 ] ] ), match[ 2 ] ];
} else {
// Should never get here
return '99999999';
}
// Pad Month and Day
if ( s[ 1 ].length === 1 ) {
s[ 1 ] = '0' + s[ 1 ];
}
if ( s[ 2 ].length === 1 ) {
s[ 2 ] = '0' + s[ 2 ];
}
var y;
if ( ( y = parseInt( s[ 0 ], 10 ) ) < 100 ) {
// Guestimate years without centuries
if ( y < 30 ) {
s[ 0 ] = 2000 + y;
} else {
s[ 0 ] = 1900 + y;
}
}
while ( s[ 0 ].length < 4 ) {
s[ 0 ] = '0' + s[ 0 ];
}
return parseInt( s.join( '' ), 10 );
},
type: 'numeric'
} );
ts.addParser( {
id: 'time',
is: function ( s ) {
return ts.rgx.time[ 0 ].test( s );
},
format: function ( s ) {
return $.tablesorter.formatFloat( new Date( '2000/01/01 ' + s ).getTime() );
},
type: 'numeric'
} );
ts.addParser( {
id: 'number',
is: function ( s ) {
return $.tablesorter.numberRegex.test( s.trim() );
},
format: function ( s ) {
return $.tablesorter.formatDigit( s );
},
type: 'numeric'
} );
}() );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment