MediaWiki:Common.js/tablesorter2.js

/* * Duplicate of https://github.com/Wikia/app/blob/dev/resources/jquery/jquery.tablesorter.js with additional functionality * allows table cache to be regenerated dynamically, e.g. if table contents is changed via JS * use $(table).trigger('updateSortableCache'); to do so * to use this, use the 'sortable2' class instead of 'sortable' * * Other functionality is the same, see http://community.wikia.com/wiki/Help:Sorting for help * * Other changes: all uses of 'tablesorter' replaced with 'tablesorter2' * Added hook to load the sorter (this is in another script in the repo) */

( function( $, mw ) {

/* Local scope */t

var	ts, parsers = [];

/* Parser utility functions */

function getParserById( name ) { var len = parsers.length; for ( var i = 0; i < len; i++ ) { if ( parsers[i].id.toLowerCase === name.toLowerCase ) { return parsers[i]; }       }        return false; }

function getElementText( node ) { var $node = $( node ), data = $node.attr( 'data-sort-value' ); if ( data !== undefined ) { return data; } else { return $node.text; }   }

function getTextFromRowAndCellIndex( rows, rowIndex, cellIndex ) { if ( rows[rowIndex] && rows[rowIndex].cells[cellIndex] ) { return $.trim( getElementText( rows[rowIndex].cells[cellIndex] ) ); } else { return ''; }   }

function detectParserForColumn( table, rows, cellIndex ) { var	l = parsers.length, nodeValue, // Start with 1 because 0 is the fallback parser i = 1, rowIndex = 0, concurrent = 0, needed = ( rows.length > 4 ) ? 5 : rows.length;

while( i < l ) { nodeValue = getTextFromRowAndCellIndex( rows, rowIndex, cellIndex ); if ( nodeValue !== '') { if ( parsers[i].is( nodeValue, table ) ) { concurrent++; rowIndex++; if ( concurrent >= needed ) { // Confirmed the parser for multiple cells, let's return it                       return parsers[i]; }               } else { // Check next parser, reset rows i++; rowIndex = 0; concurrent = 0; }           } else { // Empty cell rowIndex++; if ( rowIndex > rows.length ) { rowIndex = 0; i++; }           }        }

// 0 is always the generic parser (text) return parsers[0]; }

function buildParserCache( table, $headers ) { var	rows = table.tBodies[0].rows, sortType, parsers = [];

if ( rows[0] ) {

var	cells = rows[0].cells, len = cells.length, i, parser;

for ( i = 0; i < len; i++ ) { parser = false; sortType = $headers.eq( i ).data( 'sort-type' ); if ( sortType !== undefined ) { parser = getParserById( sortType ); }

if ( parser === false ) { parser = detectParserForColumn( table, rows, i ); }

parsers.push( parser ); }       }        return parsers; }

/* Other utility functions */

function buildCache( table ) { var	totalRows = ( table.tBodies[0] && table.tBodies[0].rows.length ) || 0, totalCells = ( table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length ) || 0, parsers = table.config.parsers, 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] ), cols = [];

// if this is a child row, add it to the last row's children and // continue to the next row if ( $row.hasClass( table.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 );

for ( var j = 0; j < totalCells; ++j ) { cols.push( parsers[j].format( getElementText( $row[0].cells[j] ), table, $row[0].cells[j] ) ); }

cols.push( cache.normalized.length ); // add position for rowCache 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 ); }

/**	 * Find all header rows in a thead-less table and put them in a tag. * This only treats a row as a header row if it contains only s (no 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 $table jQuery object for a 	 */ function emulateTHeadAndFoot( $table ) { var $rows = $table.find( '> tbody > tr' ); if( !$table.get(0).tHead ) { var $thead = $( ' ' ); $rows.each( function {               if ( $(this).children( 'td' ).length > 0 ) {                    // This row contains a, 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 = $( ' ' ); var len = $rows.length; for ( var i = len-1; i >= 0; i-- ) { if( $( $rows[i] ).children( 'td' ).length > 0 ){ break; }               $tfoot.prepend( $( $rows[i] )); }           $table.append( $tfoot ); }   }

function buildHeaders( table, msg ) { var	maxSeen = 0, longest, realCellIndex = 0, $tableHeaders = $( 'thead:eq(0) > tr', table ); if ( $tableHeaders.length > 1 ) { $tableHeaders.each( function {               if ( this.cells.length > maxSeen ) {                    maxSeen = this.cells.length;                    longest = this;                }            }); $tableHeaders = $( longest ); }       $tableHeaders = $tableHeaders.children( 'th' ).each( function( index ) {            this.column = realCellIndex;

var colspan = this.colspan; colspan = colspan ? parseInt( colspan, 10 ) : 1; realCellIndex += colspan;

this.order = 0; this.count = 0;

var thisCached = $( this );

if ( thisCached.is( '.unsortable' ) ) { this.sortDisabled = true; }

if ( !this.sortDisabled ) {

var $th = thisCached.addClass( table.config.cssHeader ).attr( 'title', msg[1] ) /* Wikia change - check whether arrows already exist before appending */ .each( function {                   if( !thisCached.find( "div .chevron" ).length ) {                        thisCached.append( '    ' );                    }                } ); /* Wikia change end */

}

// add cell to headerList table.config.headerList[index] = this; } );

return $tableHeaders;

}

function isValueInArray( v, a ) { var l = a.length; for ( var i = 0; i < l; i++ ) { if ( a[i][0] == v ) { return true; }       }        return false; }

function setHeadersCss( table, $headers, list, css, msg ) { // Remove all header information $headers.removeClass( css[0] ).removeClass( css[1] );

var h = []; $headers.each( function( offset ) {           if ( !this.sortDisabled ) {                h[this.column] = $( this );            }        } );

var l = list.length; for ( var i = 0; i < l; i++ ) { h[ list[i][0] ].addClass( css[ list[i][1] ] ).attr( 'title', msg[ list[i][1] ] ); }   }

function sortText( a, b ) { return ( (a < b) ? false : ((a > b) ? true : 0) ); }

function sortTextDesc( a, b ) { return ( (b < a) ? false : ((b > a) ? true : 0) ); }

function checkSorting( array1, array2, sortList ) { var col, fn, ret; for ( var i = 0, len = sortList.length; i < len; i++ ) { col = sortList[i][0]; fn = ( sortList[i][1] ) ? sortTextDesc : sortText; ret = fn.call( this, array1[col], array2[col] ); if ( ret !== 0 ) { return ret; }       }        return ret; }

// Merge sort algorithm // Based on http://en.literateprograms.org/Merge_sort_(JavaScript) function mergeSortHelper( array, begin, beginRight, end, sortList ) { for (begin < beginRight; ++begin ) { if ( checkSorting( array[begin], array[beginRight], sortList ) ) { var v = array[begin]; array[begin] = array[beginRight]; var begin2 = beginRight; while ( begin2 + 1 < end && checkSorting( v, array[begin2 + 1], sortList ) ) { var tmp = array[begin2]; array[begin2] = array[begin2 + 1]; array[begin2 + 1] = tmp; ++begin2; }               array[begin2] = v;            } }   }

function mergeSort(array, begin, end, sortList) { var size = end - begin; if ( size < 2 ) { return; }

var beginRight = begin + Math.floor(size / 2);

mergeSort( array, begin, beginRight, sortList ); mergeSort( array, beginRight, end, sortList ); mergeSortHelper( array, begin, beginRight, end, sortList ); }

function multisort( table, sortList, cache ) { var i = sortList.length; mergeSort( cache.normalized, 0, cache.normalized.length, sortList );

return cache; }

function buildTransformTable { var digits = '0123456789,.'.split( '' ); var separatorTransformTable = mw.config.get( 'wgSeparatorTransformTable' ); var 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 regex for number identification for ( var i = 0; i < ascii.length; i++ ) { ts.transformTable[localised[i]] = ascii[i]; digits.push( $.escapeRE( 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 = 1; i < 13; i++ ) { ts.monthNames[0][i] = mw.config.get( 'wgMonthNames' )[i].toLowerCase; ts.monthNames[1][i] = mw.config.get( 'wgMonthNamesShort' )[i].toLowerCase.replace( '.', '' ); regex.push( $.escapeRE( ts.monthNames[0][i] ) ); regex.push( $.escapeRE( ts.monthNames[1][i] ) ); }

// 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' );

}

function explodeRowspans( $table ) { // Split multi row cells into multiple cells with the same content $table.find( '> tbody > tr > [rowspan]' ).each(function {           var rowSpan = this.rowSpan;            this.rowSpan = 1;            var cell = $( this );            var next = cell.parent.nextAll;            for ( var i = 0; i < rowSpan - 1; i++ ) {                var td = next.eq( i ).children( 'td' );                if ( !td.length ) {                    next.eq( i ).append( cell.clone );                } else if ( this.cellIndex === 0 ) {                    td.eq( this.cellIndex ).before( cell.clone );                } else {                    td.eq( this.cellIndex - 1 ).after( cell.clone );                }            }        }); }

function buildCollationTable { ts.collationTable = mw.config.get( 'tableSorterCollation' ); ts.collationRegex = null; if ( ts.collationTable ) { var keys = [];

// Build array of key names for ( var key in ts.collationTable ) { if ( ts.collationTable.hasOwnProperty(key) ) { //to be safe keys.push(key); }           }            if (keys.length) { ts.collationRegex = new RegExp( '[' + keys.join( '' ) + ']', 'ig' ); }       }    }

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{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/) ],           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)))$/) ]       };    }

/* Public scope */

$.tablesorter2 = {

defaultOptions: { cssHeader: 'headerSort', cssAsc: 'headerSortUp', cssDesc: 'headerSortDown', cssChildRow: 'expand-child', sortInitialOrder: 'asc', sortMultiSortKey: 'shiftKey', sortLocaleCompare: false, parsers: {}, widgets: [], headers: {}, cancelSelection: true, sortList: [], headerList: [], selectorHeaders: 'thead tr:eq(0) th', debug: false, complete: function {} // wikia change - react to sorting completion BAC-718 },

dateRegex: [], monthNames: [],

/**			 * @param $tables {jQuery} * @param settings {Object} (optional) */       construct: function( $tables, settings ) { return $tables.each( function( i, table ) {               // Declare and cache.                var	$document, $headers, cache, config, sortOrder,                    $table = $( table ),                    shiftDown = 0,                    firstTime = true;

// Quit if already initialized if ( $table.hasClass('jquery-tablesorter2') ) { return; }               // Quit if no tbody if ( !table.tBodies ) { return; }               if ( !table.tHead ) { // No thead found. Look for rows with s and // move them into a tag or a tag emulateTHeadAndFoot( $table );

// Still no thead? Then quit if ( !table.tHead ) { return; }               }                $table.addClass( "jquery-tablesorter2" );

// New config object. table.config = {};

// Merge and extend. config = $.extend( table.config, $.tablesorter2.defaultOptions, settings );

// Save the settings where they read $.data( table, 'tablesorter2', config );

// Get the CSS class names, could be done else where. var sortCSS = [ config.cssDesc, config.cssAsc ]; var sortMsg = [ mw.msg( 'sort-descending' ), mw.msg( 'sort-ascending' ) ];

// Build headers $headers = buildHeaders( table, sortMsg );

// Grab and process locale settings buildTransformTable; buildDateTable; buildCollationTable;

// Precaching regexps can bring 10 fold // performance improvements in some browsers. cacheRegexs;

// Apply event handling to headers // this is too big, perhaps break it out? $headers.click( function( e ) {                   if ( 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 ( firstTime ) { firstTime = false;

// Legacy fix of .sortbottoms // Wrap them inside inside a tfoot (because that's what they actually want to be) & // and put the at the end of the var $sortbottoms = $table.find( '> tbody > tr.sortbottom' ); if ( $sortbottoms.length ) { var $tfoot = $table.children( 'tfoot' ); if ( $tfoot.length ) { $tfoot.eq(0).prepend( $sortbottoms ); } else { $table.append( $( ' ' ).append( $sortbottoms ) ); }                       }

explodeRowspans( $table ); // try to auto detect column type, and store in tables config table.config.parsers = buildParserCache( table, $headers ); // build the cache for the tbody cells cache = buildCache( table ); }                   var totalRows = ( $table[0].tBodies[0] && $table[0].tBodies[0].rows.length ) || 0; if ( !table.sortDisabled && totalRows > 0 ) {

// Cache jQuery object var $cell = $( this );

// Get current column index var i = this.column;

// Get current column sort order this.order = this.count % 2; this.count++;

// User only wants to sort on one column if ( !e[config.sortMultiSortKey] ) { // Flush the sort list config.sortList = []; // Add column to sort list config.sortList.push( [i, this.order] );

// Multi column sorting } else { // The user has clicked on an already sorted column. if ( isValueInArray( i, config.sortList ) ) { // Reverse the sorting direction for all tables. for ( var j = 0; j < config.sortList.length; j++ ) { var s = config.sortList[j], o = config.headerList[s[0]]; if ( s[0] == i ) { o.count = s[1]; o.count++; s[1] = o.count % 2; }                               }                            } else { // Add column to sort list array config.sortList.push( [i, this.order] ); }                       }

// Set CSS for headers setHeadersCss( $table[0], $headers, config.sortList, sortCSS, sortMsg ); appendToTable(                           $table[0], multisort( $table[0], config.sortList, cache )                        );

config.complete; // wikia change - react to sorting completion BAC-718

// Stop normal event by returning false return false; }

// Cancel selection } ).mousedown( function { if ( config.cancelSelection ) { this.onselectstart = function { return false; };                       return false; }               } );

$table.on('updateSortableCache', function {                    cache = buildCache(table);                });

} );       },

addParser: function( parser ) { var	l = parsers.length, a = true; for ( var i = 0; i < l; i++ ) { if ( parsers[i].id.toLowerCase == parser.id.toLowerCase ) { a = false; }           }            if ( a ) { parsers.push( parser ); }       },

formatDigit: function( s ) { if ( ts.transformTable !== false ) { var	out = '', c;               for ( var p = 0; p < s.length; p++ ) { 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)) ? 0 : i;       },

formatFloat: function( s ) { var i = parseFloat(s); return ( isNaN(i)) ? 0 : i;       },

formatInt: function( s ) { var i = parseInt( s, 10 ); return ( isNaN(i)) ? 0 : i;       },

clearTableBody: function( table ) { if ( $.browser.msie ) { var empty = function( el ) { while ( el.firstChild ) { el.removeChild( el.firstChild ); }               };                empty( table.tBodies[0] ); } else { table.tBodies[0].innerHTML = ''; }       }    };

// Shortcut ts = $.tablesorter2;

// Register as jQuery prototype method $.fn.tablesorter2 = function( settings ) { return ts.construct( this, settings ); };

// Add default parsers ts.addParser( {       id: 'text',        is: function( s ) {            return true;        },        format: function( s ) {            s = $.trim( s.toLowerCase );            if ( ts.collationRegex ) {                var tsc = ts.collationTable;                s = s.replace( ts.collationRegex, function( match ) { var r = tsc[match] ? tsc[match] : tsc[match.toUpperCase]; return r.toLowerCase; } );           }            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 = '',                l = a.length;            for ( var i = 0; i < l; i++ ) {                var item = a[i];                if ( item.length == 1 ) {                    r += '00' + item;                } else if ( item.length == 2 ) {                    r += '0' + item;                } else {                    r += item;                }            }            return $.tablesorter2.formatFloat(r);        },        type: 'numeric'    } );

ts.addParser( {       id: 'currency',        is: function( s ) {            return ts.rgx.currency[0].test(s);        },        format: function( s ) {            return $.tablesorter2.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 $.trim( s.replace( ts.rgx.url[1], '' ) );        },        type: 'text'    } );

ts.addParser( {       id: 'isoDate',        is: function( s ) {            return ts.rgx.isoDate[0].test(s);        },        format: function( s ) {            return $.tablesorter2.formatFloat((s !== '') ? new Date(s.replace( new RegExp( /-/g), '/')).getTime : '0' );       },        type: 'numeric'    } );

ts.addParser( {       id: 'usLongDate',        is: function( s ) {            return ts.rgx.usLongDate[0].test(s);        },        format: function( s ) {            return $.tablesorter2.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, table ) {            s = $.trim( s.toLowerCase );

for ( var i = 1, j = 0; i < 13 && j < 2; i++ ) { s = s.replace( ts.monthNames[j][i], i ); if ( i == 12 ) { j++; i = 0; }           }

s = s.replace( /[\-\.\,' ]/g, '/' );

// Replace double slashes s = s.replace( /\/\//g, '/' ); s = s.replace( /\/\//g, '/' ); s = s.split( '/' );

// Pad Month and Day if ( s[0] && s[0].length == 1 ) { s[0] = '0' + s[0]; }           if ( s[1] && s[1].length == 1 ) { s[1] = '0' + s[1]; }           var y;

if ( !s[2] ) { // Fix yearless dates s[2] = 2000; } else if ( ( y = parseInt( s[2], 10) ) < 100 ) { // Guestimate years without centuries if ( y < 30 ) { s[2] = 2000 + y;               } else { s[2] = 1900 + y;               } }           // Resort array depending on preferences if ( mw.config.get( 'wgDefaultDateFormat' ) == 'mdy' || mw.config.get( 'wgContentLanguage' ) == 'en' ) { s.push( s.shift ); s.push( s.shift ); } else if ( mw.config.get( 'wgDefaultDateFormat' ) == 'dmy' ) { var d = s.shift; s.push( s.shift ); s.push(d); }           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 $.tablesorter2.formatFloat( new Date( '2000/01/01 ' + s ).getTime );        },        type: 'numeric'    } );

ts.addParser( {       id: 'number',        is: function( s, table ) {            return $.tablesorter2.numberRegex.test( $.trim( s ));        },        format: function( s ) {            return $.tablesorter2.formatDigit(s);        },        type: 'numeric'    } );

$(function {        $('table.sortable2').tablesorter2;    }); } )( window.jQuery, window.mw );