From c934766061636e94d52cf45eaa5d73afbf70ba92 Mon Sep 17 00:00:00 2001 From: Henning Snater Date: Tue, 4 Sep 2012 16:27:14 +0200 Subject: [PATCH] sort method for jquery.tablesorter the sort method allows programmatic sorting as well as instantly sorting the table when initialising the tablesorter; furthermore, the method can be used to reset sorting (e.g. if rows have been added later via JS); sortEnd event may be used to reapply alternating table row colours or other purposes patch set 2: fixed mentioned issues and added another test patch set 3: addressed all further issues, introduced more obvious way to specify sorting patch set 4: implemented sort method instead of using an event; reprashed whole commit message; introduced 'sortEnd' event patch set 5: fixed white space error patch set 6: Add release notes and rebase Change-Id: Id14862100cd27ebd6980c48dcf497db229c4301f --- RELEASE-NOTES-1.21 | 2 + resources/jquery/jquery.tablesorter.js | 119 ++++++++++++---- .../jquery/jquery.tablesorter.test.js | 131 +++++++++++++++++- 3 files changed, 223 insertions(+), 29 deletions(-) diff --git a/RELEASE-NOTES-1.21 b/RELEASE-NOTES-1.21 index d8f65068d1..e072974d7a 100644 --- a/RELEASE-NOTES-1.21 +++ b/RELEASE-NOTES-1.21 @@ -26,6 +26,8 @@ production. * jQuery UI upgraded from 1.8.23 to 1.8.24. * Added separate fa_sha1 field to filearchive table. This allows sha1 searches with the api in miser mode for deleted files. +* Add initial and programmatic sorting for tablesorter. +* Add the event "sortEnd.tablesorter", triggered after sorting has completed. === Bug fixes in 1.21 === * (bug 40353) SpecialDoubleRedirect should support interwiki redirects. diff --git a/resources/jquery/jquery.tablesorter.js b/resources/jquery/jquery.tablesorter.js index 2940d6f2be..75dd2f1720 100644 --- a/resources/jquery/jquery.tablesorter.js +++ b/resources/jquery/jquery.tablesorter.js @@ -19,6 +19,9 @@ * @example $( 'table' ).tablesorter(); * @desc Create a simple tablesorter interface. * + * @example $( 'table' ).tablesorter( { sortList: [ { 0: 'desc' }, { 1: 'asc' } ] } ); + * @desc Create a tablesorter interface initially sorting on the first and second column. + * * @option String cssHeader ( optional ) A string of the class name to be appended * to sortable tr elements in the thead of the table. Default value: * "header" @@ -44,9 +47,16 @@ * tablesorter should cancel selection of the table headers text. * Default value: true * + * @option Array sortList ( optional ) An array containing objects specifying sorting. + * By passing more than one object, multi-sorting will be applied. Object structure: + * { : } + * Default value: [] + * * @option Boolean debug ( optional ) Boolean flag indicating if tablesorter * should display debuging information usefull for development. * + * @event sortEnd.tablesorter: Triggered as soon as any sorting has been applied. + * * @type jQuery * * @name tablesorter @@ -223,6 +233,8 @@ } table.tBodies[0].appendChild( fragment ); + + $( table ).trigger( 'sortEnd.tablesorter' ); } /** @@ -313,8 +325,8 @@ } function setHeadersCss( table, $headers, list, css, msg ) { - // Remove all header information - $headers.removeClass( css[0] ).removeClass( css[1] ); + // Remove all header information and reset titles to default message + $headers.removeClass( css[0] ).removeClass( css[1] ).attr( 'title', msg[1] ); var h = []; $headers.each( function ( offset ) { @@ -480,6 +492,25 @@ }; } + /** + * Converts sort objects [ { Integer: String }, ... ] to the internally used nested array + * structure [ [ Integer , Integer ], ... ] + * + * @param sortObjects {Array} List of sort objects. + * @return {Array} List of internal sort definitions. + */ + + function convertSortList( sortObjects ) { + var sortList = []; + $.each( sortObjects, function( i, sortObject ) { + $.each ( sortObject, function( columnIndex, order ) { + var orderIndex = ( order === 'desc' ) ? 1 : 0; + sortList.push( [columnIndex, orderIndex] ); + } ); + } ); + return sortList; + } + /* Public scope */ $.tablesorter = { @@ -514,8 +545,7 @@ // Declare and cache. var $document, $headers, cache, config, sortOrder, $table = $( table ), - shiftDown = 0, - firstTime = true; + shiftDown = 0; // Quit if no tbody if ( !table.tBodies ) { @@ -533,6 +563,7 @@ } $table.addClass( "jquery-tablesorter" ); + // FIXME config should probably not be stored in the plain table node // New config object. table.config = {}; @@ -540,7 +571,7 @@ config = $.extend( table.config, $.tablesorter.defaultOptions, settings ); // Save the settings where they read - $.data( table, 'tablesorter', config ); + $.data( table, 'tablesorter', { config: config } ); // Get the CSS class names, could be done else where. var sortCSS = [ config.cssDesc, config.cssAsc ]; @@ -558,36 +589,36 @@ // performance improvements in some browsers. cacheRegexs(); + // 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 ); + + // initially build the cache for the tbody cells (to be able to sort initially) + cache = buildCache( table ); + // Apply event handling to headers // this is too big, perhaps break it out? - $headers.click( function ( e ) { + $headers.filter( ':not(.unsortable)' ).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 // to share between calculations for this sort action. // Re-calculated each time a sort action is performed due to possiblity @@ -655,6 +686,40 @@ 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 sortList {Array} (optional) List of sort objects. + */ + $table.data( 'tablesorter' ).sort = function( sortList ) { + + if ( sortList === undefined ) { + sortList = config.sortList; + } else if ( sortList.length > 0 ) { + sortList = convertSortList( sortList ); + } + + // re-build the cache for the tbody cells + cache = buildCache( table ); + + // set css for headers + setHeadersCss( table, $headers, sortList, sortCSS, sortMsg ); + + // sort the table and append it to the dom + appendToTable( table, multisort( table, sortList, cache ) ); + }; + + // sort initially + if ( config.sortList.length > 0 ) { + explodeRowspans( $table ); + config.sortList = convertSortList( config.sortList ); + $table.data( 'tablesorter' ).sort(); + } + } ); }, diff --git a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js index 16d8170790..291c6b8d5f 100644 --- a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js @@ -107,6 +107,24 @@ var planets = [mercury, venus, earth, mars, jupiter, saturn]; var ascendingName = [earth, jupiter, mars, mercury, saturn, venus]; var ascendingRadius = [mercury, mars, venus, earth, saturn, jupiter]; +tableTest( + 'Basic planet table: sorting initially - ascending by name', + header, + planets, + ascendingName, + function ( $table ) { + $table.tablesorter( { sortList: [ { 0: 'asc' } ] } ); + } +); +tableTest( + 'Basic planet table: sorting initially - descending by radius', + header, + planets, + reversed(ascendingRadius), + function ( $table ) { + $table.tablesorter( { sortList: [ { 1: 'desc' } ] } ); + } +); tableTest( 'Basic planet table: ascending by name', header, @@ -158,6 +176,81 @@ tableTest( } ); +// Sample data set to test multiple column sorting +var header = [ 'column1' , 'column2'], + a1 = [ 'A', '1' ], + a2 = [ 'A', '2' ], + a3 = [ 'A', '3' ], + b1 = [ 'B', '1' ], + b2 = [ 'B', '2' ], + b3 = [ 'B', '3' ]; +var initial = [a2, b3, a1, a3, b2, b1]; +var asc = [a1, a2, a3, b1, b2, b3]; +var descasc = [b1, b2, b3, a1, a2, a3]; + +tableTest( + 'Sorting multiple columns by passing sort list', + header, + initial, + asc, + function ( $table ) { + $table.tablesorter( + { sortList: [ { 0: 'asc' }, { 1: 'asc' } ] } + ); + } +); +tableTest( + 'Sorting multiple columns by programmatically triggering sort()', + header, + initial, + descasc, + function ( $table ) { + $table.tablesorter(); + $table.data( 'tablesorter' ).sort( + [ { 0: 'desc' }, { 1: 'asc' } ] + ); + } +); +tableTest( + 'Reset to initial sorting by triggering sort() without any parameters', + header, + initial, + asc, + function ( $table ) { + $table.tablesorter( + { sortList: [ { 0: 'asc' }, { 1: 'asc' } ] } + ); + $table.data( 'tablesorter' ).sort( + [ { 0: 'desc' }, { 1: 'asc' } ] + ); + $table.data( 'tablesorter' ).sort(); + } +); +QUnit.test( 'Reset sorting making table appear unsorted', 3, function ( assert ) { + var $table = tableCreate( header, initial ); + $table.tablesorter( + { sortList: [ { 0: 'desc' }, { 1: 'asc' } ] } + ); + $table.data( 'tablesorter' ).sort( [] ); + + assert.equal( + $table.find( 'th.headerSortUp' ).length + $table.find( 'th.headerSortDown' ).length, + 0, + 'No sort specific sort classes addign to header cells' + ); + + assert.equal( + $table.find( 'th' ).first().attr( 'title' ), + mw.msg( 'sort-ascending' ), + 'First header cell has default title' + ); + + assert.equal( + $table.find( 'th' ).first().attr( 'title' ), + $table.find( 'th' ).last().attr( 'title' ), + 'Both header cells\' titles match' + ); +} ); // Regression tests! tableTest( @@ -306,7 +399,7 @@ tableTest( planets, planetsRowspan, function ( $table ) { - // Modify the table to have a multiuple-row-spanning cell: + // Modify the table to have a multiple-row-spanning cell: // - Remove 2nd cell of 4th row, and, 2nd cell or 5th row. $table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove(); // - Set rowspan for 2nd cell of 3rd row to 3. @@ -317,13 +410,29 @@ tableTest( $table.find( '.headerSort:eq(0)' ).click(); } ); +tableTest( + 'Basic planet table: same value for multiple rows via rowspan (sorting initially)', + header, + planets, + planetsRowspan, + function ( $table ) { + // Modify the table to have a multiple-row-spanning cell: + // - Remove 2nd cell of 4th row, and, 2nd cell or 5th row. + $table.find( 'tr:eq(3) td:eq(1), tr:eq(4) td:eq(1)' ).remove(); + // - Set rowspan for 2nd cell of 3rd row to 3. + // This covers the removed cell in the 4th and 5th row. + $table.find( 'tr:eq(2) td:eq(1)' ).prop( 'rowspan', '3' ); + + $table.tablesorter( { sortList: [ { 0: 'asc' } ] } ); + } +); tableTest( 'Basic planet table: Same value for multiple rows via rowspan II', header, planets, planetsRowspanII, function ( $table ) { - // Modify the table to have a multiuple-row-spanning cell: + // Modify the table to have a multiple-row-spanning cell: // - Remove 1st cell of 4th row, and, 1st cell or 5th row. $table.find( 'tr:eq(3) td:eq(0), tr:eq(4) td:eq(0)' ).remove(); // - Set rowspan for 1st cell of 3rd row to 3. @@ -414,6 +523,24 @@ tableTest( } ); +QUnit.test( 'Test detection routine', function ( assert ) { + var $table; + $table = $( + '
' + + '' + + '' + + '' + + '' + + '
CAPTION
THEAD
1
text
' + ); + $table.tablesorter(); + + assert.equal( + $table.data( 'tablesorter' ).config.parsers[0].id, + 'number', + 'Correctly detected column content skipping sortbottom' + ); +} ); /** FIXME: the diff output is not very readeable. */ QUnit.test( 'bug 32047 - caption must be before thead', function ( assert ) { -- 2.20.1