jenkins-bot has submitted this change and it was merged.

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.
* Clear unused header data 'sortDisabled' .
* 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, 212 insertions(+), 93 deletions(-)

Approvals:
  TheDJ: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/resources/src/jquery/jquery.tablesorter.js 
b/resources/src/jquery/jquery.tablesorter.js
index 23474c0..eaa138b 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,22 @@
        }
 
        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 +195,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 +220,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
@@ -249,7 +265,7 @@
         * 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 jQuery object for a <table>
+        * @param {jQuery} $table object for a <table>
         */
        function emulateTHeadAndFoot( $table ) {
                var $thead, $tfoot, i, len,
@@ -294,12 +310,13 @@
                        maxSeen = 0,
                        colspanOffset = 0,
                        columns,
-                       i,
+                       k,
                        $cell,
                        rowspan,
                        colspan,
                        headerCount,
                        longestTR,
+                       headerIndex,
                        exploded,
                        $tableHeaders = $( [] ),
                        $tableRows = $( 'thead:eq(0) > tr', table );
@@ -348,29 +365,15 @@
 
                // 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 ) {
+               config.columnToHeader = [];
+               config.headerToColumns = [];
+               config.headerList = [];
+               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
-                       } );
-
-                       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 )
@@ -378,14 +381,33 @@
                                                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
+                               config.headerList[ headerIndex ] = this;
+                               headerIndex++;
                        }
 
-                       // add cell to headerList
-                       config.headerList[ headerIndex ] = this;
+                       colspanOffset += this.colSpan;
                } );
 
-               return $tableHeaders;
+               // 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 ) {
@@ -638,6 +660,49 @@
                }
        }
 
+       /**
+        * 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 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>' );
+                                       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;
@@ -715,12 +780,13 @@
                                cssChildRow: 'expand-child',
                                sortMultiSortKey: 'shiftKey',
                                unsortableClass: 'unsortable',
-                               parsers: {},
+                               parsers: [],
                                cancelSelection: true,
                                sortList: [],
                                headerList: [],
                                headerToColumns: [],
-                               columnToHeader: []
+                               columnToHeader: [],
+                               columns: 0
                        },
 
                        dateRegex: [],
@@ -798,6 +864,7 @@
                                                }
 
                                                explodeRowspans( $table );
+                                               manageColspans( $table );
 
                                                // Try to auto detect column 
type, and store in tables config
                                                config.parsers = 
buildParserCache( table, $headers );
@@ -805,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;
@@ -834,7 +901,7 @@
                                                cache = buildCache( table );
 
                                                totalRows = ( $table[ 0 
].tBodies[ 0 ] && $table[ 0 ].tBodies[ 0 ].rows.length ) || 0;
-                                               if ( !table.sortDisabled && 
totalRows > 0 ) {
+                                               if ( totalRows > 0 ) {
                                                        cell = this;
                                                        $cell = $( cell );
 
@@ -867,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;
@@ -938,7 +1005,6 @@
 
                                        // sort initially
                                        if ( config.sortList.length > 0 ) {
-                                               setupForFirstSort();
                                                config.sortList = 
convertSortList( config.sortList );
                                                $table.data( 'tablesorter' 
).sort();
                                        }
@@ -999,6 +1065,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 759322a..d4d0ce1 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: merged
Gerrit-Change-Id: I518029616d4c10a48eeaad8e92962f4e580f9413
Gerrit-PatchSet: 7
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: MatthiasDD <matthias...@gmx.de>
Gerrit-Reviewer: Edokter <er...@darcoury.nl>
Gerrit-Reviewer: Jack Phoenix <j...@countervandalism.net>
Gerrit-Reviewer: Krinkle <krinklem...@gmail.com>
Gerrit-Reviewer: MatthiasDD <matthias...@gmx.de>
Gerrit-Reviewer: TheDJ <hartman.w...@gmail.com>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to