Filter: range, not matches & negative values now search all rows. Add filter language option. Fixes #602

This commit is contained in:
Mottie 2014-05-02 13:50:57 -05:00
parent c586a329d6
commit 962108db35
3 changed files with 73 additions and 29 deletions

View File

@ -35,7 +35,7 @@
<script id="js">$(function() { <script id="js">$(function() {
// call the tablesorter plugin // call the tablesorter plugin
$("table.tablesorter").tablesorter({ $("#table").tablesorter({
theme: 'blue', theme: 'blue',
// hidden filter input/selects will resize the columns, so try to minimize the change // hidden filter input/selects will resize the columns, so try to minimize the change
@ -134,7 +134,7 @@
filters[col] = txt; filters[col] = txt;
// using "table.hasFilters" here to make sure we aren't targetting a sticky header // using "table.hasFilters" here to make sure we aren't targetting a sticky header
$.tablesorter.setFilters( $('table.hasFilters'), filters, true ); // new v2.9 $.tablesorter.setFilters( $('#table'), filters, true ); // new v2.9
/** old method (prior to tablsorter v2.9 *** /** old method (prior to tablsorter v2.9 ***
var filters = $('table.tablesorter').find('input.tablesorter-filter'); var filters = $('table.tablesorter').find('input.tablesorter-filter');
@ -162,7 +162,7 @@ $(function(){
// *** widgetfilter_startsWith toggle button *** // *** widgetfilter_startsWith toggle button ***
$('button.toggle').click(function(){ $('button.toggle').click(function(){
var c = $('table.tablesorter')[0].config, var c = $('#table')[0].config,
$t = $(this), $t = $(this),
// toggle the boolean // toggle the boolean
fsw = !c.widgetOptions.filter_startsWith, fsw = !c.widgetOptions.filter_startsWith,
@ -178,7 +178,7 @@ $(function(){
$t.find('span').html( $t.hasClass('filter-match') ? '' : ' No' ); $t.find('span').html( $t.hasClass('filter-match') ? '' : ' No' );
} }
// update search after option change; add false to trigger to skip search delay // update search after option change; add false to trigger to skip search delay
$('table.tablesorter').trigger('search', false); c.$table.trigger('search', false);
return false; return false;
}); });
@ -557,6 +557,24 @@ $(".search").bind('search keyup', function (e) {
<div class="inner"> <div class="inner">
<p>Moved to the wiki pages - <a href="https://github.com/Mottie/tablesorter/wiki/Change3">filter change log</a>. <p>Moved to the wiki pages - <a href="https://github.com/Mottie/tablesorter/wiki/Change3">filter change log</a>.
</div> </div>
<h3><a href="#">Localization</a></h3>
<div class="inner">
<p>You can now change the language of the searches used within the filter widget. For example, to change the localization to French, do the following:
<pre class="prettyprint lang-js">// add French support
$.extend($.tablesorter.language, {
to : 'à',
or : 'ou',
and : 'et'
});</pre>If you want to support multiple languages, separate the language variables with a vertical bar (<code>|</code>, <kbd>Shift</kbd> + <kbd>&#92;</kbd>):
<pre class="prettyprint lang-js">// add French & Spanish support
$.extend($.tablesorter.language, {
to : 'à|a',
or : 'ou|o',
and : 'et|y'
});</pre><span class="label label-info">Note</span> These changes still require the user to enter spaces in the filter to perform the search, e.g. <code>1 à 10</code> (shows rows with numbers between 1 and 10).
</div>
</div> </div>
<h1>Demo</h1> <h1>Demo</h1>
@ -568,7 +586,7 @@ $(".search").bind('search keyup', function (e) {
<div id="demo">Search <button type="button" data-filter-column="5" data-filter-text="2?%">2?%</button> in the Discount column<br> <div id="demo">Search <button type="button" data-filter-column="5" data-filter-text="2?%">2?%</button> in the Discount column<br>
<button type="button" class="reset">Reset</button> <!-- targeted by the "filter_reset" option --> <button type="button" class="reset">Reset</button> <!-- targeted by the "filter_reset" option -->
<table class="tablesorter"> <table id="table" class="tablesorter">
<thead> <thead>
<tr> <tr>
<!-- you can also add a placeholder using script; $('.tablesorter th:eq(0)').data('placeholder', 'hello') --> <!-- you can also add a placeholder using script; $('.tablesorter th:eq(0)').data('placeholder', 'hello') -->

View File

@ -462,9 +462,9 @@
<li><span class="results">&dagger;</span> <a href="example-widget-columns.html">Columns Highlight widget</a> (v2.0.17)</li> <li><span class="results">&dagger;</span> <a href="example-widget-columns.html">Columns Highlight widget</a> (v2.0.17)</li>
<li><span class="label label-info">Beta</span> <a href="example-widget-column-selector.html">Column Selector widget</a> (<span class="version">v2.15</span>; <span class="version updated">v2.15.7</span>).</li> <li><span class="label label-info">Beta</span> <a href="example-widget-column-selector.html">Column Selector widget</a> (<span class="version">v2.15</span>; <span class="version updated">v2.15.7</span>).</li>
<li><a href="example-widget-editable.html">Content Editable widget</a> (v2.9; <span class="version updated">v2.13.2</span>).</li> <li><a href="example-widget-editable.html">Content Editable widget</a> (v2.9; <span class="version updated">v2.13.2</span>).</li>
<li><span class="results">&dagger;</span> Filter Widget (<span class="version updated">v2.16.2</span>): <li><span class="results">&dagger;</span> Filter Widget (<span class="version updated">v2.16.3</span>):
<ul> <ul>
<li><a href="example-widget-filter.html">basic</a> (v2.0.18; <span class="version updated">v2.15</span>)</li> <li><a href="example-widget-filter.html">basic</a> (v2.0.18; <span class="version updated">v2.16.3</span>)</li>
<li><a href="example-widget-filter-any-match.html">external option (match any column)</a> (<span class="version">v2.13.3</span>; <span class="version updated">v2.15</span>)</li> <li><a href="example-widget-filter-any-match.html">external option (match any column)</a> (<span class="version">v2.13.3</span>; <span class="version updated">v2.15</span>)</li>
<li><a href="example-widget-filter-external-inputs.html">external inputs</a> (<span class="version">v2.14</span>; <span class="version updated">v2.15</span>)</li> <li><a href="example-widget-filter-external-inputs.html">external inputs</a> (<span class="version">v2.14</span>; <span class="version updated">v2.15</span>)</li>
<li><a href="example-widget-filter-custom.html">custom</a> (v2.3.6; <span class="version updated">v2.10.1</span>)</li> <li><a href="example-widget-filter-custom.html">custom</a> (v2.3.6; <span class="version updated">v2.10.1</span>)</li>

View File

@ -446,7 +446,7 @@ ts.filter = {
// iExact may be numeric - see issue #149; // iExact may be numeric - see issue #149;
// check if cached is defined, because sometimes j goes out of range? (numeric columns) // check if cached is defined, because sometimes j goes out of range? (numeric columns)
cachedValue = ( parsed[index] || parser.type === 'numeric' )&& !isNaN(query) && typeof cached !== 'undefined' ? cached : cachedValue = ( parsed[index] || parser.type === 'numeric' ) && !isNaN(query) && typeof cached !== 'undefined' ? cached :
isNaN(iExact) ? ts.formatFloat( iExact.replace(ts.filter.regex.nondigit, ''), table) : isNaN(iExact) ? ts.formatFloat( iExact.replace(ts.filter.regex.nondigit, ''), table) :
ts.formatFloat( iExact, table ); ts.formatFloat( iExact, table );
@ -478,8 +478,8 @@ ts.filter = {
}, },
// Look for an AND or && operator (logical and) // Look for an AND or && operator (logical and)
and : function( filter, iFilter, exact, iExact ) { and : function( filter, iFilter, exact, iExact ) {
if ( /\s+(AND|&&)\s+/g.test(filter) ) { if ( ts.filter.regex.andTest.test(filter) ) {
var query = iFilter.split( /(?:\s+(?:and|&&)\s+)/g ), var query = iFilter.split( ts.filter.regex.andSplit ),
result = iExact.search( $.trim(query[0]) ) >= 0, result = iExact.search( $.trim(query[0]) ) >= 0,
indx = query.length - 1; indx = query.length - 1;
while (result && indx) { while (result && indx) {
@ -492,10 +492,11 @@ ts.filter = {
}, },
// Look for a range (using " to " or " - ") - see issue #166; thanks matzhu! // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu!
range : function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) { range : function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) {
if ( /\s+(-|to)\s+/.test(iFilter) ) { if ( ts.filter.regex.toTest.test(iFilter) ) {
var result, tmp, var result, tmp,
c = table.config, c = table.config,
query = iFilter.split(/(?: - | to )/), // make sure the dash is for a range and not indicating a negative number // make sure the dash is for a range and not indicating a negative number
query = iFilter.split( ts.filter.regex.toSplit ),
range1 = ts.formatFloat(query[0].replace(ts.filter.regex.nondigit, ''), table), range1 = ts.formatFloat(query[0].replace(ts.filter.regex.nondigit, ''), table),
range2 = ts.formatFloat(query[1].replace(ts.filter.regex.nondigit, ''), table); range2 = ts.formatFloat(query[1].replace(ts.filter.regex.nondigit, ''), table);
// parse filter value in case we're comparing numbers (dates) // parse filter value in case we're comparing numbers (dates)
@ -515,9 +516,9 @@ ts.filter = {
}, },
// Look for wild card: ? = single, * = multiple, or | = logical OR // Look for wild card: ? = single, * = multiple, or | = logical OR
wild : function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed, rowArray ) { wild : function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed, rowArray ) {
if ( /[\?|\*]/.test(iFilter) || /\s+OR\s+/i.test(filter) ) { if ( /[\?|\*]/.test(iFilter) || ts.filter.regex.orReplace.test(filter) ) {
var c = table.config, var c = table.config,
query = iFilter.replace(/\s+OR\s+/gi,"|"); query = iFilter.replace(ts.filter.regex.orReplace, "|");
// look for an exact match with the "or" unless the "filter-match" class is found // look for an exact match with the "or" unless the "filter-match" class is found
if (!c.$headers.filter('[data-column="' + index + '"]:last').hasClass('filter-match') && /\|/.test(query)) { if (!c.$headers.filter('[data-column="' + index + '"]:last').hasClass('filter-match') && /\|/.test(query)) {
query = $.isArray(rowArray) ? '(' + query + ')' : '^(' + query + ')$'; query = $.isArray(rowArray) ? '(' + query + ')' : '^(' + query + ')$';
@ -547,14 +548,30 @@ ts.filter = {
} }
}, },
init: function(table, c, wo) { init: function(table, c, wo) {
var options, string, $header, column, filters, time; // filter language options
ts.language = $.extend(true, {}, {
to : 'to',
or : 'or',
and : 'and'
}, ts.language);
var options, string, $header, column, filters, time,
regex = ts.filter.regex;
if (c.debug) { if (c.debug) {
time = new Date(); time = new Date();
} }
c.$table.addClass('hasFilters'); c.$table.addClass('hasFilters');
ts.filter.regex.child = new RegExp(c.cssChildRow); $.extend( regex, {
ts.filter.regex.filtered = new RegExp(wo.filter_filteredRow); child : new RegExp(c.cssChildRow),
filtered : new RegExp(wo.filter_filteredRow),
alreadyFiltered : new RegExp('(\\s+(' + ts.language.or + '|-|' + ts.language.to + ')\\s+)', 'i'),
toTest : new RegExp('\\s+(-|' + ts.language.to + ')\\s+', 'i'),
toSplit : new RegExp('(?:\\s+(?:-|' + ts.language.to + ')\\s+)' ,'gi'),
andTest : new RegExp('\\s+(' + ts.language.and + '|&&)\\s+', 'i'),
andSplit : new RegExp('(?:\\s+(?:' + ts.language.and + '|&&)\\s+)', 'gi'),
orReplace : new RegExp('\\s+(' + ts.language.or + ')\\s+', 'gi')
});
// don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156 // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156
if (wo.filter_columnFilters !== false && c.$headers.filter('.filter-false').length !== c.$headers.length) { if (wo.filter_columnFilters !== false && c.$headers.filter('.filter-false').length !== c.$headers.length) {
@ -578,6 +595,7 @@ ts.filter = {
if (/(update|add)/.test(event.type) && event.type !== "updateComplete") { if (/(update|add)/.test(event.type) && event.type !== "updateComplete") {
// force a new search since content has changed // force a new search since content has changed
c.lastCombinedFilter = null; c.lastCombinedFilter = null;
c.lastSearch = [];
} }
// pass true (skipFirst) to prevent the tablesorter.setFilters function from skipping the first input // pass true (skipFirst) to prevent the tablesorter.setFilters function from skipping the first input
// ensures all inputs are updated when a search is triggered on the table $('table').trigger('search', [...]); // ensures all inputs are updated when a search is triggered on the table $('table').trigger('search', [...]);
@ -804,6 +822,7 @@ ts.filter = {
} else if (filter === false) { } else if (filter === false) {
// force filter refresh // force filter refresh
c.lastCombinedFilter = null; c.lastCombinedFilter = null;
c.lastSearch = [];
} }
c.$table.trigger('filterStart', [filters]); c.$table.trigger('filterStart', [filters]);
if (c.showProcessing) { if (c.showProcessing) {
@ -858,8 +877,9 @@ ts.filter = {
if (table.config.lastCombinedFilter === combinedFilters) { return; } if (table.config.lastCombinedFilter === combinedFilters) { return; }
var cached, len, $rows, rowIndex, tbodyIndex, $tbody, $cells, columnIndex, var cached, len, $rows, rowIndex, tbodyIndex, $tbody, $cells, columnIndex,
childRow, childRowText, exact, iExact, iFilter, lastSearch, matches, result, childRow, childRowText, exact, iExact, iFilter, lastSearch, matches, result,
notFiltered, searchFiltered, filterMatched, showRow, time, notFiltered, searchFiltered, filterMatched, showRow, time, val, indx,
anyMatch, iAnyMatch, rowArray, rowText, iRowText, rowCache, anyMatch, iAnyMatch, rowArray, rowText, iRowText, rowCache,
regex = ts.filter.regex,
c = table.config, c = table.config,
wo = c.widgetOptions, wo = c.widgetOptions,
columns = c.columns, columns = c.columns,
@ -889,18 +909,23 @@ ts.filter = {
// optimize searching only through already filtered rows - see #313 // optimize searching only through already filtered rows - see #313
searchFiltered = true; searchFiltered = true;
lastSearch = c.lastSearch || c.$table.data('lastSearch') || []; lastSearch = c.lastSearch || c.$table.data('lastSearch') || [];
$.each(filters, function(indx, val) { for (indx = 0; indx < columnIndex; indx++) {
val = filters[indx] || '';
// break out of loop if we've already determined not to search filtered rows
if (!searchFiltered) { indx = columnIndex; }
// search already filtered rows if... // search already filtered rows if...
searchFiltered = searchFiltered && searchFiltered = searchFiltered && lastSearch.length &&
// there are changes from beginning of filter // there are no changes from beginning of filter
(val || '').indexOf(lastSearch[indx]) === 0 && val.indexOf(lastSearch[indx] || '') === 0 &&
// if there is not a logical "or" in the string // if there is NOT a logical "or", or range ("to" or "-") in the string
!/(\s+or\s+|\|)/g.test(val || '') && !regex.alreadyFiltered.test(val) &&
// if we are not doing exact matches // if we are not doing exact matches, using "|" (logical or) or not "!"
!/[=\"]/.test(lastSearch[indx]) && !/[=\"\|!]/.test(val) &&
// don't search only filtered if the value is negative ('> -10' => '> -100' will ignore hidden rows)
!(/(>=?\s*-\d)/.test(val) || /(<=?\s*\d)/.test(val)) &&
// if filtering using a select without a "filter-match" class (exact match) - fixes #593 // if filtering using a select without a "filter-match" class (exact match) - fixes #593
!( val !== '' && wo.filter_functions && wo.filter_functions[indx] === true && !c.$headers.filter('[data-column="' + indx + '"]:last').hasClass('filter-match') ); !( val !== '' && wo.filter_functions && wo.filter_functions[indx] === true && !c.$headers.filter('[data-column="' + indx + '"]:last').hasClass('filter-match') );
}); }
notFiltered = $rows.not('.' + wo.filter_filteredRow).length; notFiltered = $rows.not('.' + wo.filter_filteredRow).length;
// can't search when all rows are hidden - this happens when looking for exact matches // can't search when all rows are hidden - this happens when looking for exact matches
if (searchFiltered && notFiltered === 0) { searchFiltered = false; } if (searchFiltered && notFiltered === 0) { searchFiltered = false; }
@ -919,7 +944,7 @@ ts.filter = {
for (rowIndex = 0; rowIndex < len; rowIndex++) { for (rowIndex = 0; rowIndex < len; rowIndex++) {
childRow = $rows[rowIndex].className; childRow = $rows[rowIndex].className;
// skip child rows & already filtered rows // skip child rows & already filtered rows
if ( ts.filter.regex.child.test(childRow) || (searchFiltered && ts.filter.regex.filtered.test(childRow)) ) { continue; } if ( regex.child.test(childRow) || (searchFiltered && regex.filtered.test(childRow)) ) { continue; }
showRow = true; showRow = true;
// *** nextAll/nextUntil not supported by Zepto! *** // *** nextAll/nextUntil not supported by Zepto! ***
childRow = $rows.eq(rowIndex).nextUntil('tr:not(.' + c.cssChildRow + ')'); childRow = $rows.eq(rowIndex).nextUntil('tr:not(.' + c.cssChildRow + ')');
@ -975,7 +1000,7 @@ ts.filter = {
exact = $.trim($cells.eq(columnIndex).text()); exact = $.trim($cells.eq(columnIndex).text());
exact = c.sortLocaleCompare ? ts.replaceAccents(exact) : exact; // issue #405 exact = c.sortLocaleCompare ? ts.replaceAccents(exact) : exact; // issue #405
} }
iExact = !ts.filter.regex.type.test(typeof exact) && wo.filter_ignoreCase ? exact.toLocaleLowerCase() : exact; iExact = !regex.type.test(typeof exact) && wo.filter_ignoreCase ? exact.toLocaleLowerCase() : exact;
result = showRow; // if showRow is true, show that row result = showRow; // if showRow is true, show that row
// replace accents - see #357 // replace accents - see #357
@ -1246,6 +1271,7 @@ ts.setFilters = function(table, filter, apply, skipFirst) {
if (c && apply) { if (c && apply) {
// ensure new set filters are applied, even if the search is the same // ensure new set filters are applied, even if the search is the same
c.lastCombinedFilter = null; c.lastCombinedFilter = null;
c.lastSearch = [];
ts.filter.searching(c.$table[0], filter, skipFirst); ts.filter.searching(c.$table[0], filter, skipFirst);
c.$table.trigger('filterFomatterUpdate'); c.$table.trigger('filterFomatterUpdate');
} }