jquery.tablesorter: Fix explodeRowspans
authorBrad Jorsch <bjorsch@wikimedia.org>
Mon, 19 Nov 2012 14:47:11 +0000 (09:47 -0500)
committerOri Livneh <ori@wikimedia.org>
Mon, 18 Mar 2013 03:58:12 +0000 (20:58 -0700)
$.tablesorter's explodeRowspans works ok for simple tables, but row
headers or unusual spanning structures will confuse it.

This rewrite makes it more robust.

Bug: 41889
Change-Id: Icb674f7eece053435ca9525d45709579df14cc74

RELEASE-NOTES-1.21
resources/jquery/jquery.tablesorter.js
tests/qunit/suites/resources/jquery/jquery.tablesorter.test.js

index 2e3cf72..295eb7b 100644 (file)
@@ -204,6 +204,7 @@ production.
   script in includes/DefaultSettings.php
 * (bug 45143) jquery.badge: Treat non-Latin variants of zero as zero as well.
 * (bug 46151) mwdocgen.php should not ignore exit code of doxygen command.
+* (bug 41889) Fix $.tablesorter rowspan exploding for complex cases.
 
 === API changes in 1.21 ===
 * prop=revisions can now report the contentmodel and contentformat.
index 8bf1f61..e08c9aa 100644 (file)
 
        }
 
+       /**
+        * Replace all rowspanned cells in the body with clones in each row, so sorting
+        * need not worry about them.
+        *
+        * @param $table jQuery object for a <table>
+        */
        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();
+               var 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;
+                       var l = this.cells.length;
+                       for ( var i = 0; i < l; i++ ) {
+                               this.cells[i].realCellIndex = col;
+                               this.cells[i].realRowIndex = this.rowIndex;
+                               col += this.cells[i].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 ret = a.realCellIndex - b.realCellIndex;
+                               if ( !ret ) {
+                                       ret = a.realRowIndex - b.realRowIndex;
+                               }
+                               return ret;
+                       } );
+                       $.each( rowspanCells, function () {
+                               this.needResort = false;
+                       } );
+               }
+               resortCells();
+
+               var spanningRealCellIndex, rowSpan, colSpan;
+               function filterfunc() {
+                       return this.realCellIndex >= spanningRealCellIndex;
+               }
+
+               function fixTdCellIndex() {
+                       this.realCellIndex += colSpan;
+                       if ( this.rowSpan > 1 ) {
+                               this.needResort = true;
+                       }
+               }
+
+               while ( rowspanCells.length ) {
+                       if ( rowspanCells[0].needResort ) {
+                               resortCells();
+                       }
+
+                       var cell = rowspanCells.shift();
+                       rowSpan = cell.rowSpan;
+                       colSpan = cell.colSpan;
+                       spanningRealCellIndex = cell.realCellIndex;
+                       cell.rowSpan = 1;
+                       var $nextRows = $( 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() );
+                               var $tds = $( $nextRows[i].cells ).filter( filterfunc );
+                               var $clone = $( cell ).clone();
+                               $clone[0].realCellIndex = spanningRealCellIndex;
+                               if ( $tds.length ) {
+                                       $tds.each( fixTdCellIndex );
+                                       $tds.first().before( $clone );
                                } else {
-                                       td.eq( this.cellIndex - 1 ).after( cell.clone() );
+                                       $nextRows.eq( i ).append( $clone );
                                }
                        }
-               });
+               }
        }
 
        function buildCollationTable() {
index deff5b0..307b044 100644 (file)
                } );
        }
 
+       /**
+        * Run a table test by building a table with the given HTML,
+        * running some callback on it, then checking the results.
+        *
+        * @param {String} msg text to pass on to qunit for the comparison
+        * @param {String} HTML to make the table
+        * @param {String[][]} expected rows/cols to compare against at end
+        * @param {function($table)} callback something to do with the table before we compare
+        */
+       function tableTestHTML( msg, html, expected, callback ) {
+               QUnit.test( msg, 1, function ( assert ) {
+                       var $table = $( html );
+
+                       // Give caller a chance to set up sorting and manipulate the table.
+                       if ( callback ) {
+                               callback( $table );
+                       } else {
+                               $table.tablesorter();
+                               $table.find( '#sortme' ).click();
+                       }
+
+                       // Table sorting is done synchronously; if it ever needs to change back
+                       // to asynchronous, we'll need a timeout or a callback here.
+                       var extracted = tableExtract( $table );
+                       assert.deepEqual( extracted, expected, msg );
+               } );
+       }
+
        function reversed( arr ) {
                // Clone array
                var arr2 = arr.slice( 0 );
                        'Applied correct sorting order'
                );
        } );
+
+       // bug 41889 - exploding rowspans in more complex cases
+       tableTestHTML(
+               'Rowspan exploding with row headers',
+               '<table class="sortable">' +
+                       '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th></tr></thead>' +
+                       '<tbody>' +
+                       '<tr><td>1</td><th rowspan="2">foo</th><td rowspan="2">bar</td><td>baz</td></tr>' +
+                       '<tr><td>2</td><td>baz</td></tr>' +
+                       '</tbody></table>',
+               [
+                       [ '1', 'foo', 'bar', 'baz' ],
+                       [ '2', 'foo', 'bar', 'baz' ]
+               ]
+       );
+
+       tableTestHTML(
+               'Rowspan exploding with colspanned cells',
+               '<table class="sortable">' +
+                       '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th></tr></thead>' +
+                       '<tbody>' +
+                       '<tr><td>1</td><td>foo</td><td>bar</td><td rowspan="2">baz</td></tr>' +
+                       '<tr><td>2</td><td colspan="2">foobar</td></tr>' +
+                       '</tbody></table>',
+               [
+                       [ '1', 'foo', 'bar', 'baz' ],
+                       [ '2', 'foobar', 'baz' ]
+               ]
+       );
+
+       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>' +
+                       '<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>' +
+                       '</tbody></table>',
+               [
+                       [ '1', 'foo', 'bar', 'baz', 'quux' ],
+                       [ '2', 'foobar', 'baz', 'quux' ]
+               ]
+       );
+
+       tableTestHTML(
+               'Rowspan exploding with rightmost rows spanning most',
+               '<table class="sortable">' +
+                       '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th></tr></thead>' +
+                       '<tbody>' +
+                       '<tr><td>1</td><td rowspan="2">foo</td><td rowspan="4">bar</td></tr>' +
+                       '<tr><td>2</td></tr>' +
+                       '<tr><td>3</td><td rowspan="2">foo</td></tr>' +
+                       '<tr><td>4</td></tr>' +
+                       '</tbody></table>',
+               [
+                       [ '1', 'foo', 'bar' ],
+                       [ '2', 'foo', 'bar' ],
+                       [ '3', 'foo', 'bar' ],
+                       [ '4', 'foo', 'bar' ]
+               ]
+       );
+
+       tableTestHTML(
+               'Rowspan exploding with rightmost rows spanning most (2)',
+               '<table class="sortable">' +
+                       '<thead><tr><th id="sortme">n</th><th>foo</th><th>bar</th><th>baz</th></tr></thead>' +
+                       '<tbody>' +
+                       '<tr><td>1</td><td rowspan="2">foo</td><td rowspan="4">bar</td><td>baz</td></tr>' +
+                       '<tr><td>2</td><td>baz</td></tr>' +
+                       '<tr><td>3</td><td rowspan="2">foo</td><td>baz</td></tr>' +
+                       '<tr><td>4</td><td>baz</td></tr>' +
+                       '</tbody></table>',
+               [
+                       [ '1', 'foo', 'bar', 'baz' ],
+                       [ '2', 'foo', 'bar', 'baz' ],
+                       [ '3', 'foo', 'bar', 'baz' ],
+                       [ '4', 'foo', 'bar', 'baz' ]
+               ]
+       );
+
+       tableTestHTML(
+               'Rowspan exploding with row-and-colspanned cells',
+               '<table class="sortable">' +
+                       '<thead><tr><th id="sortme">n</th><th>foo1</th><th>foo2</th><th>bar</th><th>baz</th></tr></thead>' +
+                       '<tbody>' +
+                       '<tr><td>1</td><td rowspan="2">foo1</td><td rowspan="2">foo2</td><td rowspan="4">bar</td><td>baz</td></tr>' +
+                       '<tr><td>2</td><td>baz</td></tr>' +
+                       '<tr><td>3</td><td colspan="2" rowspan="2">foo</td><td>baz</td></tr>' +
+                       '<tr><td>4</td><td>baz</td></tr>' +
+                       '</tbody></table>',
+               [
+                       [ '1', 'foo1', 'foo2', 'bar', 'baz' ],
+                       [ '2', 'foo1', 'foo2', 'bar', 'baz' ],
+                       [ '3', 'foo', 'bar', 'baz' ],
+                       [ '4', 'foo', 'bar', 'baz' ]
+               ]
+       );
+
+       tableTestHTML(
+               'Rowspan exploding with uneven rowspan layout',
+               '<table class="sortable">' +
+                       '<thead><tr><th id="sortme">n</th><th>foo1</th><th>foo2</th><th>foo3</th><th>bar</th><th>baz</th></tr></thead>' +
+                       '<tbody>' +
+                       '<tr><td>1</td><td rowspan="2">foo1</td><td rowspan="2">foo2</td><td rowspan="2">foo3</td><td>bar</td><td>baz</td></tr>' +
+                       '<tr><td>2</td><td rowspan="3">bar</td><td>baz</td></tr>' +
+                       '<tr><td>3</td><td rowspan="2">foo1</td><td rowspan="2">foo2</td><td rowspan="2">foo3</td><td>baz</td></tr>' +
+                       '<tr><td>4</td><td>baz</td></tr>' +
+                       '</tbody></table>',
+               [
+                       [ '1', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ],
+                       [ '2', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ],
+                       [ '3', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ],
+                       [ '4', 'foo1', 'foo2', 'foo3', 'bar', 'baz' ]
+               ]
+       );
+
 }( jQuery, mediaWiki ) );