MatthiasDD has uploaded a new change for review. https://gerrit.wikimedia.org/r/233168
Change subject: jquery.tablesorter: Add ability for cells with colspan in tbody ...................................................................... jquery.tablesorter: Add ability for cells with colspan in tbody * Add manageColspans() to handle colspaned cells in table body. Add config.columns - number of colums with extended colspan, inclusive unsortable columns buildParserCache() iterate for all columns (not only the first body row, Bug: T105731) buildCache() also (Bug: T74534) Rows with not enougth cells get after first click additional empty cells. * Add $.tablesorter.getParsers() for better table diagnosis. fix 3 litle bugs: * Improve multi column sorting with colspan in header (add columnToHeader[..] to s[0] ) * Unsortable headers get after sorting no title tag. ($headers contain only sortable headers) * Parser detection in tables with max. 5 rows and empty cells works now. Bug: T105731 Bug: T74534 Change-Id: I518029616d4c10a48eeaad8e92962f4e580f9413 --- M resources/src/jquery/jquery.tablesorter.js M tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js 2 files changed, 209 insertions(+), 88 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/68/233168/1 diff --git a/resources/src/jquery/jquery.tablesorter.js b/resources/src/jquery/jquery.tablesorter.js index 8efbb1c..c504de8 100644 --- a/resources/src/jquery/jquery.tablesorter.js +++ b/resources/src/jquery/jquery.tablesorter.js @@ -105,18 +105,25 @@ } } - function detectParserForColumn( table, rows, cellIndex ) { + function detectParserForColumn( table, rows, column ) { var l = parsers.length, + cellIndex, nodeValue, // 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 ) { - if ( rows[rowIndex] && rows[rowIndex].cells[cellIndex] ) { - nodeValue = $.trim( getElementSortKey( rows[rowIndex].cells[cellIndex] ) ); + if ( rows[rowIndex] ) { + if ( rowIndex != lastRowIndex) { + lastRowIndex = rowIndex; + cellIndex = $(rows[rowIndex]).data( 'columnToCell' )[column]; + nodeValue = $.trim( getElementSortKey( rows[rowIndex].cells[cellIndex] ) ); + } } else { nodeValue = ''; } @@ -134,13 +141,22 @@ i++; rowIndex = 0; concurrent = 0; + empty = 0; } } else { // Empty cell + empty++; rowIndex++; - if ( rowIndex > rows.length ) { - rowIndex = 0; + if ( rowIndex >= rows.length ) { + if ( 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; } } } @@ -150,24 +166,23 @@ } function buildParserCache( table, $headers ) { - var sortType, cells, len, i, parser, + var sortType, len, j, parser, rows = table.tBodies[0].rows, + config = $(table).data( 'tablesorter' ).config, parsers = []; if ( rows[0] ) { - cells = rows[0].cells; - len = cells.length; - - for ( i = 0; i < len; i++ ) { + len = config.columns; + for ( j = 0; j < len; j++ ) { parser = false; - sortType = $headers.eq( i ).data( 'sortType' ); + sortType = $headers.eq( config.columnToHeader[j] ).data( 'sortType' ); if ( sortType !== undefined ) { parser = getParserById( sortType ); } if ( parser === false ) { - parser = detectParserForColumn( table, rows, i ); + parser = detectParserForColumn( table, rows, j ); } parsers.push( parser ); @@ -181,15 +196,16 @@ function buildCache( table ) { var i, j, $row, cols, totalRows = ( table.tBodies[0] && table.tBodies[0].rows.length ) || 0, - totalCells = ( table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length ) || 0, config = $( table ).data( 'tablesorter' ).config, parsers = config.parsers, + len = parsers.length, + cellIndex, cache = { row: [], normalized: [] }; - for ( i = 0; i < totalRows; ++i ) { + for ( i = 0; i < totalRows; i++ ) { // Add the table data to main data array $row = $( table.tBodies[0].rows[i] ); @@ -205,8 +221,9 @@ cache.row.push( $row ); - for ( j = 0; j < totalCells; ++j ) { - cols.push( parsers[j].format( getElementSortKey( $row[0].cells[j] ), table, $row[0].cells[j] ) ); + for ( j = 0; j < len; j++ ) { + cellIndex = $row.data( 'columnToCell' )[j]; + cols.push( parsers[j].format( getElementSortKey( $row[0].cells[cellIndex] ) ) ); } cols.push( cache.normalized.length ); // add position for rowCache @@ -283,7 +300,7 @@ maxSeen = 0, colspanOffset = 0, columns, - i, + k, $cell, rowspan, colspan, @@ -337,29 +354,17 @@ // 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 - $tableHeaders.each( function ( headerIndex ) { + headerIndex = 0; + $tableHeaders.each( function () { $cell = $( this ); columns = []; - for ( i = 0; i < this.colSpan; i++ ) { - config.columnToHeader[ colspanOffset + i ] = headerIndex; - columns.push( colspanOffset + i ); - } - - config.headerToColumns[ headerIndex ] = columns; - colspanOffset += this.colSpan; - - $cell.data( { - headerIndex: headerIndex, - order: 0, - count: 0 - } ); - + //this data is not used, delete it? if ( $cell.hasClass( config.unsortableClass ) ) { $cell.data( 'sortDisabled', true ); } - if ( !$cell.data( 'sortDisabled' ) ) { + if ( !$cell.hasClass( config.unsortableClass ) ) { $cell .addClass( config.cssHeader ) .prop( 'tabIndex', 0 ) @@ -367,14 +372,32 @@ role: 'columnheader button', title: msg[1] } ); + + for ( 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 //MK + config.headerList[ headerIndex ] = this; + headerIndex++; } - - // add cell to headerList - config.headerList[headerIndex] = this; + + 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; //MK - return $tableHeaders; - + return $tableHeaders.not( '.' + config.unsortableClass ); } /** @@ -551,6 +574,7 @@ var spanningRealCellIndex, rowSpan, colSpan, cell, cellData, i, $tds, $clone, $nextRows, rowspanCells = $table.find( '> tbody > tr > [rowspan]' ).get(); + //rowspanCells = $table.find( '> tbody > tr >' ).not( '[rowspan="1"]' ).get(); //better? // Short circuit if ( !rowspanCells.length ) { @@ -637,6 +661,48 @@ } } + /** + * Colspanned cells in the body would not cloned, + * only the cell index is multipe set in a array for every row, + * columnToCell[collumnIndex] point at the real cell in this row. + * + * @param $table jQuery object for a <table> + */ +function manageColspans( $table ) { + var i, j, k, $row, + $rows = $table.find( '> tbody > tr' ), + totalRows = $rows.length || 0 , + config = $table.data( 'tablesorter' ).config, + columns = config.columns, + columnToCell, cellsInRow, index; + + for ( i = 0; i < totalRows; i++ ) { + + $row = $rows.eq(i); + // if this is a child row, continue to the next row (as buildCache()) + 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 ( j = 0; j < columns; index++) { + if ( index === cellsInRow) { + //row with cells less than columns: add empty cell + $row.append( '<td></td>' ); + cellsInRow++; + } + for (k = 0; k < $row[0].cells[index].colSpan; k++ ) { + columnToCell[j++] = index; + } + } + //Store it in $row + $row.data( 'columnToCell', columnToCell ); + } + } + function buildCollationTable() { ts.collationTable = mw.config.get( 'tableSorterCollation' ); ts.collationRegex = null; @@ -714,12 +780,13 @@ cssChildRow: 'expand-child', sortMultiSortKey: 'shiftKey', unsortableClass: 'unsortable', - parsers: {}, + parsers: [], cancelSelection: true, sortList: [], headerList: [], headerToColumns: [], - columnToHeader: [] + columnToHeader: [], + columns: 0 }, dateRegex: [], @@ -797,6 +864,7 @@ } explodeRowspans( $table ); + manageColspans( $table ); // Try to auto detect column type, and store in tables config config.parsers = buildParserCache( table, $headers ); @@ -804,7 +872,7 @@ // Apply event handling to headers // this is too big, perhaps break it out? - $headers.not( '.' + config.unsortableClass ).on( 'keypress click', function ( e ) { + $headers.on( 'keypress click', function ( e ) { var cell, $cell, columns, newSortList, i, totalRows, j, s, o; @@ -866,7 +934,7 @@ // Reverse the sorting direction for all tables. for ( j = 0; j < config.sortList.length; j++ ) { s = config.sortList[j]; - o = config.headerList[s[0]]; + o = config.headerList[config.columnToHeader[s[0]]]; if ( isValueInArray( s[0], newSortList ) ) { $( o ).data( 'count', s[1] + 1 ); s[1] = $( o ).data( 'count' ) % 2; @@ -937,7 +1005,7 @@ // sort initially if ( config.sortList.length > 0 ) { - setupForFirstSort(); + //setupForFirstSort(); is in sort() config.sortList = convertSortList( config.sortList ); $table.data( 'tablesorter' ).sort(); } @@ -998,6 +1066,10 @@ buildCollationTable(); return getParserById( id ); + }, + + getParsers: function () { //for table diagnosis + return parsers; } }; diff --git a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js index acd98a6..5391ff5 100644 --- a/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js +++ b/tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js @@ -1,24 +1,5 @@ ( function ( $, mw ) { - var header, - - // Data set "simple" - a1 = [ 'A', '1' ], - a2 = [ 'A', '2' ], - a3 = [ 'A', '3' ], - b1 = [ 'B', '1' ], - b2 = [ 'B', '2' ], - b3 = [ 'B', '3' ], - simple = [a2, b3, a1, a3, b2, b1], - simpleAsc = [a1, a2, a3, b1, b2, b3], - simpleDescasc = [b1, b2, b3, a1, a2, a3], - - // Data set "colspan" - aaa1 = [ 'A', 'A', 'A', '1' ], - aab5 = [ 'A', 'A', 'B', '5' ], - abc3 = [ 'A', 'B', 'C', '3' ], - bbc2 = [ 'B', 'B', 'C', '2' ], - caa4 = [ 'C', 'A', 'A', '4' ], - colspanInitial = [ aab5, aaa1, abc3, bbc2, caa4 ], + var header = [ 'Planet', 'Radius (km)'], // Data set "planets" mercury = [ 'Mercury', '2439.7' ], @@ -33,6 +14,26 @@ planetsRowspan, planetsRowspanII, planetsAscNameLegacy, + + // Data set "simple" + a1 = [ 'A', '1' ], + a2 = [ 'A', '2' ], + a3 = [ 'A', '3' ], + b1 = [ 'B', '1' ], + b2 = [ 'B', '2' ], + b3 = [ 'B', '3' ], + simple = [a2, b3, a1, a3, b2, b1], + simpleAsc = [a1, a2, a3, b1, b2, b3], + simpleDescasc = [b1, b2, b3, a1, a2, a3], + + // Data set "colspan" + header4 = [ 'column1a', 'column1b', 'column1c', 'column2' ]; + aaa1 = [ 'A', 'A', 'A', '1' ], + aab5 = [ 'A', 'A', 'B', '5' ], + abc3 = [ 'A', 'B', 'C', '3' ], + bbc2 = [ 'B', 'B', 'C', '2' ], + caa4 = [ 'C', 'A', 'A', '4' ], + colspanInitial = [ aab5, aaa1, abc3, bbc2, caa4 ], // Data set "ipv4" ipv4 = [ @@ -302,7 +303,6 @@ } // Sample data set using planets named and their radius - header = [ 'Planet', 'Radius (km)']; tableTest( 'Basic planet table: sorting initially - ascending by name', @@ -388,9 +388,6 @@ $table.find( '.headerSort:eq(1)' ).click().click(); } ); - - header = [ 'column1', 'column2' ]; - tableTest( 'Sorting multiple columns by passing sort list', header, @@ -500,10 +497,9 @@ } ); // Sorting with colspans - header = [ 'column1a', 'column1b', 'column1c', 'column2' ]; tableTest( 'Sorting with colspanned headers: spanned column', - header, + header4, colspanInitial, [ aaa1, aab5, abc3, bbc2, caa4 ], function ( $table ) { @@ -516,7 +512,7 @@ } ); tableTest( 'Sorting with colspanned headers: sort spanned column twice', - header, + header4, colspanInitial, [ caa4, bbc2, abc3, aab5, aaa1 ], function ( $table ) { @@ -530,7 +526,7 @@ } ); tableTest( 'Sorting with colspanned headers: subsequent column', - header, + header4, colspanInitial, [ aaa1, bbc2, abc3, caa4, aab5 ], function ( $table ) { @@ -543,7 +539,7 @@ } ); tableTest( 'Sorting with colspanned headers: sort subsequent column twice', - header, + header4, colspanInitial, [ aab5, caa4, abc3, bbc2, aaa1 ], function ( $table ) { @@ -557,18 +553,36 @@ } ); - tableTest( - 'Basic planet table: one unsortable column', - header, - planets, - planets, - function ( $table ) { - $table.find( 'tr:eq(0) > th:eq(0)' ).addClass( 'unsortable' ); + QUnit.test( 'Basic planet table: one unsortable column', 3, function ( assert ) { + var $table = tableCreate( header, planets ), + $cell; + $table.find( 'tr:eq(0) > th:eq(0)' ).addClass( 'unsortable' ); - $table.tablesorter(); - $table.find( 'tr:eq(0) > th:eq(0)' ).click(); - } - ); + $table.tablesorter(); + $table.find( 'tr:eq(0) > th:eq(0)' ).click(); + + assert.deepEqual( + tableExtract( $table ), + planets, + 'table not sorted' + ); + + $cell = $table.find( 'tr:eq(0) > th:eq(0)' ); + $table.find( 'tr:eq(0) > th:eq(1)' ).click(); + + assert.equal( + $cell.hasClass('headerSortUp') || $cell.hasClass('headerSortDown'), + false, + 'after sort: no class headerSortUp or headerSortDown' + ); + + assert.equal( + $cell.attr('title'), + undefined, + 'after sort: no title tag added' + ); + + } ); // Regression tests! tableTest( @@ -1262,14 +1276,14 @@ tableTestHTML( 'Rowspan exploding with colspanned cells (2)', '<table class="sortable">' + - '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th><th>quux</th></tr></thead>' + + '<thead><tr><th>n</th><th>foo</th><th>bar</th><th>baz</th><th id="sortme">n2</th></tr></thead>' + '<tbody>' + - '<tr><td>1</td><td>foo</td><td>bar</td><td rowspan="2">baz</td><td>quux</td></tr>' + - '<tr><td>2</td><td colspan="2">foobar</td><td>quux</td></tr>' + + '<tr><td>1</td><td>foo</td><td>bar</td><td rowspan="2">baz</td><td>2</td></tr>' + + '<tr><td>2</td><td colspan="2">foobar</td><td>1</td></tr>' + '</tbody></table>', [ - [ '1', 'foo', 'bar', 'baz', 'quux' ], - [ '2', 'foobar', 'baz', 'quux' ] + [ '2', 'foobar', 'baz', '1' ], + [ '1', 'foo', 'bar', 'baz', '2' ] ] ); @@ -1345,4 +1359,39 @@ ] ); + QUnit.test( 'bug 105731 - incomplete rows in table body', 3, function ( assert ) { + var $table, parsers; + $table = $( + '<table class="sortable">' + + '<tr><th>A</th><th>B</th></tr>' + + '<tr><td>3</td></tr>' + + '<tr><td>1</td><td>2</td></tr>' + + '</table>' + ); + $table.tablesorter(); + $table.find( '.headerSort:eq(0)' ).click(); + //now the first row have 2 columns + $table.find( '.headerSort:eq(1)' ).click(); + + parsers = $table.data( 'tablesorter' ).config.parsers; + + assert.equal( + parsers.length, + 2, + 'detectParserForColumn() detect 2 parsers' + ); + + assert.equal( + parsers[1].id, + 'number', + 'detectParserForColumn() detect parser.id "number" for second column' + ); + + assert.equal( + parsers[1].format( $table.find( 'tbody > tr > td:eq(1)' ).text() ), + 0, + 'empty cell is sorted as number 0' + ); + + } ); }( jQuery, mediaWiki ) ); -- To view, visit https://gerrit.wikimedia.org/r/233168 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I518029616d4c10a48eeaad8e92962f4e580f9413 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: MatthiasDD <matthias...@gmx.de> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits