From 60282f0787864c67e125258bd7b76aef57577aa1 Mon Sep 17 00:00:00 2001 From: Rob Garrison Date: Wed, 9 Dec 2015 12:34:48 -0600 Subject: [PATCH] Core & Filter: Add duplicateSpan option Core: - Added `duplicateSpan` option (default is `true`). - Renamed `$.tablesorter.formatSortingOrder` to `$.tablesorter.getOrder`. - Include `table` in console.error if an issue is encountered during initialization. - Clean up warning when no parser is found for given data. - Fix `config.sortVars` js error for non-existent header cells. - Added unit tests. - Added "example-colspan.html" demo. Filter: - Filters that span multiple columns now have the correct data-column set. - Consolidated code that parsed data-column ranges into `findRange` function. - Added unit tests --- docs/example-colspan.html | 183 ++++++++++++++++++++++++++++++++++++ docs/index.html | 29 +++++- js/jquery.tablesorter.js | 49 +++++++--- js/widgets/widget-filter.js | 52 ++++++---- test.html | 24 +++++ testing/testing-widgets.js | 25 ++++- testing/testing.js | 39 ++++++++ 7 files changed, 363 insertions(+), 38 deletions(-) create mode 100644 docs/example-colspan.html diff --git a/docs/example-colspan.html b/docs/example-colspan.html new file mode 100644 index 00000000..2c14b32e --- /dev/null +++ b/docs/example-colspan.html @@ -0,0 +1,183 @@ + + + + + jQuery plugin: Tablesorter 2.0 - Sorting & Filtering with Colspans + + + + + + + + + + + + + + + + + + + + + + + +
+ +

+ NOTE! +

+

+ +

Demo

+
    +
  • Sort Column + (toggle sort direction) - There is no method to use the UI to sort the second column because it has no header; use "sorton" instead. +
  • +
  • Search: + + + + , + then toggle duplicateSpan : . +
  • +
  • Searching the first two columns : +
      +
    • Search using column 0 (zero):
      + (nothing visible in column filter)
      + (search second column, nothing visible in filter) +
    • +
    • Search using column 6 (used by "all" filter):
      + (search both index columns)
      + (only search "Group" column)
      + (search second column) +
    • +
    +
  • +
+ +Search: + + + + +

The reason for this issue is that the filter input in the index column has this setting: +data-column="0-1", and it has not yet been worked out how to properly target that input.
+ It is still being investigated as to why the search using the button targeting column 6 and the "all" input have different results (Enter "4" in the input and 4 rows will appear in the result, then click on the "4" to search both index columns - one less row). +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Index (colspan 2)Products
Product IDNumericAnimalsUrl
IndexProduct IDNumericAnimalsUrl
Group 16abc 9155Lionhttp://www.nytimes.com/
Group 41abc 1237Ox http://www.yahoo.com
Group 12zyx 1 957 Koala http://www.mit.edu/
Group 05abc 256Elephanthttp://www.wikipedia.org/
Group 30abc 12317 Koalahttp://www.google.com
Group 28zyx 910Girafeehttp://www.facebook.com
Group 13zyx 4 767Bisonhttp://www.whitehouse.gov/
Group 24abc 113Chimphttp://www.ucla.edu/
Group 47ABC 10 87Zebrahttp://www.google.com
Group 39zyx 120Koalahttp://www.nasa.gov/
+ +

Javascript

+
+

+	
+

CSS

+
+

+	
+

HTML

+
+

+	
+ +
+ + + + diff --git a/docs/index.html b/docs/index.html index e3ef4341..f83bf99c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -340,11 +340,12 @@
  • Sorting Accented Characters (sortLocaleCompare; v2.24; languages).
  • Sort table using a link outside the table (external link; v2.17.0).
  • Attach child rows (rows that sort with their parent row) (v2.15.12).
  • -
  • Use child rows + filter widget (v2.22.0)
  • +
  • Use child rows + filter widget (v2.22.0).
  • Sorting with Multiple Tbodies (v2.2).
  • Sorting Across Multiple Columns (v2.3).
  • Show a processing icon during sorting/filtering (v2.4).
  • Delay table initialization (delayInit).
  • +
  • Sort & filter with colspans (duplicateSpan; v2.24.7).
  • @@ -462,9 +463,9 @@
  • basic (v2.0.18; v2.23.5).
  • external option (match any column) (v2.13.3; v2.22.0).
  • external inputs (v2.14; v2.18.0).
  • -
  • custom (v2.3.6; v2.22.0).
  • -
  • custom searches (v2.17.5; v2.22.0).
  • -
  • custom search (example #2) (v2.19.1; v2.24.6).
  • +
  • custom filter functions (v2.3.6; v2.22.0).
  • +
  • custom search types (v2.17.5; v2.22.0).
  • +
  • custom search type (example #2: date range) (v2.19.1; v2.24.6).
  • child rows (v2.23.4).
  • formatter: jQuery UI widgets and HTML5 Elements (v2.7.7; v2.17.5).
  • formatter: select2 (v2.16.0; v2.21.3).
  • @@ -583,6 +584,26 @@ + + + Boolean + true + Any colspan cells in the tbody may have its content duplicated in the cache for each spanned column (v2.24.7). +
    +

    If true, the cache will contain duplicated cell contents for every column the colspan includes. This makes it easier to sort & filter columns because a cell spanning all columns will only work with one parser. If false, the contents of cells that are spanned will be set to an empty string.

    +
    // this row: <tr><td colspan="3">foo</td><td>bar</td></tr> results in this row cache:
    +[ 'foo', 'foo', 'foo', 'bar' ] // if duplicateSpan = true
    +[ 'foo', '',    '',    'bar' ] // if duplicateSpan = false
    + *NOTE* + +
    + + Example + + String diff --git a/js/jquery.tablesorter.js b/js/jquery.tablesorter.js index 727ec3b6..bcd6d6e4 100644 --- a/js/jquery.tablesorter.js +++ b/js/jquery.tablesorter.js @@ -62,6 +62,7 @@ emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero, emptyMax, emptyMin stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero + duplicateSpan : true, // colspan cells in the tbody will have duplicated content in the cache for each spanned column textExtraction : 'basic', // text extraction method/function - function( node, table, cellIndex ){} textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in default textExtraction function) textSorter : null, // choose overall or specific column sorter function( a, b, direction, table, columnIndex ) [alt: ts.sortText] @@ -209,7 +210,7 @@ if ( table.hasInitialized ) { console.warn( 'Stopping initialization. Tablesorter has already been initialized' ); } else { - console.error( 'Stopping initialization! No table, thead or tbody' ); + console.error( 'Stopping initialization! No table, thead or tbody', table ); } } return; @@ -561,7 +562,7 @@ // this may get updated numerous times if there are multiple rows c.sortVars[ column ] = { count : -1, // set to -1 because clicking on the header automatically adds one - order: ts.formatSortingOrder( tmp ) ? + order: ts.getOrder( tmp ) ? [ 1, 0, 2 ] : // desc, asc, unsorted [ 0, 1, 2 ], // asc, desc, unsorted lockedOrder : false @@ -569,7 +570,7 @@ tmp = ts.getData( $elem, configHeaders, 'lockedOrder' ) || false; if ( typeof tmp !== 'undefined' && tmp !== false ) { c.sortVars[ column ].lockedOrder = true; - c.sortVars[ column ].order = ts.formatSortingOrder( tmp ) ? [ 1, 1, 1 ] : [ 0, 0, 0 ]; + c.sortVars[ column ].order = ts.getOrder( tmp ) ? [ 1, 1, 1 ] : [ 0, 0, 0 ]; } // add cell to headerList c.headerList[ index ] = elem; @@ -692,6 +693,12 @@ if ( span > 0 ) { colIndex += span; max += span; + while ( span + 1 > 0 ) { + // set colspan columns to use the same parsers & extractors + list.parsers[ colIndex - span ] = parser; + list.extractors[ colIndex - span ] = extractor; + span--; + } } } colIndex++; @@ -834,7 +841,7 @@ buildCache : function( c, callback, $tbodies ) { var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row, cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData, - colMax, span, cacheIndex, max, len, + colMax, span, cacheIndex, hasParser, max, len, index, table = c.table, parsers = c.parsers; // update tbody variable @@ -909,22 +916,31 @@ max = c.columns; for ( colIndex = 0; colIndex < max; ++colIndex ) { cell = $row[ 0 ].cells[ colIndex ]; - if ( typeof parsers[ cacheIndex ] === 'undefined' ) { - if ( c.debug ) { - console.warn( 'No parser found for column ' + colIndex + '; cell:', cell, 'does it have a header?' ); + if ( cell && cacheIndex < c.columns ) { + hasParser = typeof parsers[ cacheIndex ] !== 'undefined'; + if ( !hasParser && c.debug ) { + console.warn( 'No parser found for row: ' + rowIndex + ', column: ' + colIndex + + '; cell containing: "' + $(cell).text() + '"; does it have a header?' ); } - } else if ( cell ) { val = ts.getElementText( c, cell, cacheIndex ); rowData.raw[ cacheIndex ] = val; // save original row text + // save raw column text even if there is no parser set txt = ts.getParsedText( c, cell, cacheIndex, val ); cols[ cacheIndex ] = txt; - if ( ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) { + if ( hasParser && ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) { // determine column max value (ignore sign) colMax[ cacheIndex ] = Math.max( Math.abs( txt ) || 0, colMax[ cacheIndex ] || 0 ); } // allow colSpan in tbody span = cell.colSpan - 1; if ( span > 0 ) { + index = 0; + while ( index <= span ) { + // duplicate text (or not) to spanned columns + rowData.raw[ cacheIndex + index ] = c.duplicateSpan || index === 0 ? val : ''; + cols[ cacheIndex + index ] = c.duplicateSpan || index === 0 ? val : ''; + index++; + } cacheIndex += span; max += span; } @@ -953,7 +969,7 @@ if ( !val[ 'row: ' + cacheIndex ] ) { val[ 'row: ' + cacheIndex ] = {}; } - val[ 'row: ' + cacheIndex ][ c.headerContent[ colIndex ] ] = + val[ 'row: ' + cacheIndex ][ c.$headerIndexed[ colIndex ].text() ] = c.cache[ 0 ].normalized[ cacheIndex ][ colIndex ]; } } @@ -1054,7 +1070,7 @@ col = parseInt( $el.attr( 'data-column' ), 10 ), end = col + c.$headers[ i ].colSpan; for ( ; col < end; col++ ) { - include = include ? ts.isValueInArray( col, c.sortList ) > -1 : false; + include = include ? include || ts.isValueInArray( col, c.sortList ) > -1 : false; } return include; }); @@ -1159,6 +1175,14 @@ col = parseInt( val[ 0 ], 10 ); // prevents error if sorton array is wrong if ( col < c.columns ) { + + // set order if not already defined - due to colspan header without associated header cell + // adding this check prevents a javascript error + if ( !c.sortVars[ col ].order ) { + order = c.sortVars[ col ].order = ts.getOrder( c.sortInitialOrder ) ? [ 1, 0, 2 ] : [ 0, 1, 2 ]; + c.sortVars[ col ].count = 0; + } + order = c.sortVars[ col ].order; dir = ( '' + val[ 1 ] ).match( /^(1|d|s|o|n)/ ); dir = dir ? dir[ 0 ] : ''; @@ -1437,6 +1461,7 @@ ts.initSort( c, cell, event ); }, 50 ); } + var arry, indx, headerIndx, dir, temp, tmp, $header, notMultiSort = !event[ c.sortMultiSortKey ], table = c.table, @@ -1708,7 +1733,7 @@ return ( parsers && parsers[ column ] ) ? parsers[ column ].type || '' : ''; }, - formatSortingOrder : function( val ) { + getOrder : function( val ) { // look for 'd' in 'desc' order; return true return ( /^d/i.test( val ) || val === 1 ); }, diff --git a/js/widgets/widget-filter.js b/js/widgets/widget-filter.js index a1f412b8..2d360dae 100644 --- a/js/widgets/widget-filter.js +++ b/js/widgets/widget-filter.js @@ -208,7 +208,7 @@ table = c.table, parsed = data.parsed[ data.index ], query = ts.formatFloat( data.iFilter.replace( tsfRegex.operators, '' ), table ), - parser = c.parsers[ data.index ], + parser = c.parsers[ data.index ] || {}, savedSearch = query; // parse filter value in case we're comparing numbers ( dates ) if ( parsed || parser.type === 'numeric' ) { @@ -629,7 +629,7 @@ for ( indx = 0; indx <= c.columns; indx++ ) { // include data-column='all' external filters col = indx === c.columns ? 'all' : indx; - filters[indx] = $filters + filters[ indx ] = $filters .filter( '[data-column="' + col + '"]' ) .attr( wo.filter_defaultAttrib ) || filters[indx] || ''; } @@ -651,11 +651,12 @@ buildFilter = ''; for ( column = 0; column < columns; column++ ) { if ( c.$headerIndexed[ column ].length ) { - buildFilter += ' 1 ) { - buildFilter += ' colspan="' + tmp + '"'; + buildFilter += ' -1; + }); + }, + multipleColumns: function( c, $input ) { + // look for multiple columns '1-3,4-6,8' in data-column + var wo = c.widgetOptions, + // only target 'all' column inputs on initialization + // & don't target 'all' column inputs if they don't exist + targets = wo.filter_initialized || !$input.filter( wo.filter_anyColumnSelector ).length, + val = $.trim( tsf.getLatestSearch( $input ).attr( 'data-column' ) || '' ); + return tsf.findRange( c, val, !targets ); + }, processTypes: function( c, data, vars ) { var ffxn, filterMatched = null, diff --git a/test.html b/test.html index 904d407d..e721f560 100644 --- a/test.html +++ b/test.html @@ -127,6 +127,30 @@ + + + + + + + + + + + + + + + + + + + + + + +
    IndexSort All Columns
    Product IDNumericAnimalsUrl
    G16a9155Lnytimes
    G12z1 957 K mit
    G30a1317 Kgoogle
    G28z910Gfacebook
    G13z24 67Bwhitehouse
    G47 A1087Zgoogle
    G39z120K nasa
    +
    diff --git a/testing/testing-widgets.js b/testing/testing-widgets.js index 05ab1917..5f416125 100644 --- a/testing/testing-widgets.js +++ b/testing/testing-widgets.js @@ -141,12 +141,29 @@ jQuery(function($){ assert.cacheCompare( this.table, 3, [ 12, 18, 13, 18 ], 'starting filter value on age column', true ); }); + QUnit.test( 'Filter column range', function(assert) { + expect(10); + var range = $.tablesorter.filter.findRange, + c = { columns: 10 }; // psuedo table.config + + assert.deepEqual( range( c, '6' ), [ 6 ], '6' ); + assert.deepEqual( range( c, '5, 6' ), [ 5,6 ], '5, 6' ); + assert.deepEqual( range( c, '5 - 6' ), [ 5,6 ], '5 - 6' ); + assert.deepEqual( range( c, '1-3,5-6,8' ), [ 1,2,3,5,6,8 ], '1-3,5-6,8' ); + assert.deepEqual( range( c, '6- 3, 2,4' ), [ 3,4,5,6,2,4 ], '6- 3,2,4 (dupes included)' ); + assert.deepEqual( range( c, '-1-3, 11' ), [ 1,2,3 ], '-1-3, 11 (negative & out of range ignored)' ); + assert.deepEqual( range( c, '8-12' ), [ 8,9 ], '8-12 (not out of range)' ); + assert.deepEqual( range( c, 'all' ), [ 0,1,2,3,4,5,6,7,8,9 ], 'all' ); + assert.deepEqual( range( c, 'any-text' ), [ 0,1,2,3,4,5,6,7,8,9 ], 'text with dash -> all columns' ); + assert.deepEqual( range( c, 'a-b-c,100' ), [ 0,1,2,3,4,5,6,7,8,9 ], 'text with dashes & commas -> all columns' ); + }); + QUnit.test( 'Filter searches', function(assert) { var ts = this.ts, - c = this.c, - wo = this.wo, - $table = this.$table, - table = this.table; + c = this.c, + wo = this.wo, + $table = this.$table, + table = this.table; expect(33); return QUnit.SequentialRunner( diff --git a/testing/testing.js b/testing/testing.js index b7487e6d..6df2fada 100644 --- a/testing/testing.js +++ b/testing/testing.js @@ -143,11 +143,13 @@ jQuery(function($){ $table3 = $('#table3'), $table4 = $('#table4'), $table5 = $('#table5'), // empty table + $table6 = $('#table6'), // colspan table table1 = $table1[0], table2 = $table2[0], table3 = $table3[0], table4 = $table4[0], table5 = $table5[0], + table6 = $table6[0], th0 = $table1.find('th')[0], // first table header cell init = false, sortIndx = 0, @@ -210,6 +212,7 @@ jQuery(function($){ }); $table5.tablesorter(); + $table6.tablesorter(); QUnit.module('core'); /************************************************ @@ -610,6 +613,42 @@ jQuery(function($){ }); + + QUnit.test( 'colspan parsing', function(assert) { + assert.expect(2); + + t = [ + 'g1', '6', 'a9', 155, 'l', 'nytimes', + 'g1', '2', 'z1 957 K mit', 'z1 957 K mit', 'z1 957 K mit', 'z1 957 K mit', // colspan 4 + 'g3', '0', 'a13', '17 K', '17 K', 'google', + 'g2', '8', 'z9', 10, 'g', 'facebook', + 'g1', '3', 'z24 67', 'z24 67', 'b', 'whitehouse', + 'g4', '7 A10', '7 A10', 87, 'z', 'google', + 'g3', '9', 'z12', 0, 'K nasa', 'K nasa' + ]; + assert.cacheCompare( table6,'all', t, 'colspans in tbody (duplicateSpan:true)' ); + + $('#testblock').html('' + + '' + + '' + + '' + + '' + + '' + + '
    1234
    123
    yz
    abc
    ') + .find('table') + .tablesorter({ + headers : { '*' : { sorter: 'text' } }, + duplicateSpan: false + }); + t = [ + '1', '2', '', '3', + 'y', '', '', 'z', + 'a', 'b', 'c', '' + ]; + assert.cacheCompare( $('#testblock table')[0], 'all', t, 'colspans not duplicated in cache (duplicateSpan:false)' ); + + }); + QUnit.test( 'sorton methods', function(assert) { assert.expect(6);