/*! Widget: filter - updated 3/5/2015 (v2.21.0) *//*
* Requires tablesorter v2.8+ and jQuery 1.7+
* by Rob Garrison
*/
;(function ($) {
'use strict';
var ts = $.tablesorter = $.tablesorter || {};
$.extend(ts.css, {
filterRow : 'tablesorter-filter-row',
filter : 'tablesorter-filter'
});
ts.addWidget({
id: "filter",
priority: 50,
options : {
filter_childRows : false, // if true, filter includes child row content in the search
filter_columnFilters : true, // if true, a filter will be added to the top of each table column
filter_columnAnyMatch: true, // if true, allows using "#:{query}" in AnyMatch searches (column:query)
filter_cellFilter : '', // css class name added to the filter cell (string or array)
filter_cssFilter : '', // css class name added to the filter row & each input in the row (tablesorter-filter is ALWAYS added)
filter_defaultFilter : {}, // add a default column filter type "~{query}" to make fuzzy searches default; "{q1} AND {q2}" to make all searches use a logical AND.
filter_excludeFilter : {}, // filters to exclude, per column
filter_external : '', // jQuery selector string (or jQuery object) of external filters
filter_filteredRow : 'filtered', // class added to filtered rows; needed by pager plugin
filter_formatter : null, // add custom filter elements to the filter row
filter_functions : null, // add custom filter functions using this option
filter_hideEmpty : true, // hide filter row when table is empty
filter_hideFilters : false, // collapse filter row when mouse leaves the area
filter_ignoreCase : true, // if true, make all searches case-insensitive
filter_liveSearch : true, // if true, search column content while the user types (with a delay)
filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available (visible) options within the drop down
filter_placeholder : { search : '', select : '' }, // default placeholder text (overridden by any header "data-placeholder" setting)
filter_reset : null, // jQuery selector string of an element used to reset the filters
filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters
filter_searchDelay : 300, // typing delay in milliseconds before starting a search
filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true
filter_selectSource : null, // include a function to return an array of values to be added to the column filter select
filter_startsWith : false, // if true, filter start from the beginning of the cell contents
filter_useParsedData : false, // filter all data using parsed content
filter_serversideFiltering : false, // if true, server-side filtering should be performed because client-side filtering will be disabled, but the ui and events will still be used.
filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value
filter_selectSourceSeparator : '|' // filter_selectSource array text left of the separator is added to the option value, right into the option text
},
format: function(table, c, wo) {
if (!c.$table.hasClass('hasFilters')) {
ts.filter.init(table, c, wo);
}
},
remove: function(table, c, wo, refreshing) {
var tbodyIndex, $tbody,
$table = c.$table,
$tbodies = c.$tbodies,
events = 'addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join(c.namespace + 'filter ');
$table
.removeClass('hasFilters')
// add .tsfilter namespace to all BUT search
.unbind( events.replace(/\s+/g, ' ') )
// remove the filter row even if refreshing, because the column might have been moved
.find('.' + ts.css.filterRow).remove();
if (refreshing) { return; }
for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
$tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
$tbody.children().removeClass(wo.filter_filteredRow).show();
ts.processTbody(table, $tbody, false); // restore tbody
}
if (wo.filter_reset) {
$(document).undelegate(wo.filter_reset, 'click.tsfilter');
}
}
});
ts.filter = {
// regex used in filter "check" functions - not for general use and not documented
regex: {
regex : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex
child : /tablesorter-childRow/, // child row class name; this gets updated in the script
filtered : /filtered/, // filtered (hidden) row class name; updated in the script
type : /undefined|number/, // check type
exact : /(^[\"\'=]+)|([\"\'=]+$)/g, // exact match (allow '==')
nondigit : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser)
operators : /[<>=]/g, // replace operators
query : '(q|query)' // replace filter queries
},
// function( c, data ) { }
// c = table.config
// data.filter = array of filter input values;
// data.iFilter = same array, except lowercase (if wo.filter_ignoreCase is true)
// data.exact = table cell text (or parsed data if column parser enabled)
// data.iExact = same as data.exact, except lowercase (if wo.filter_ignoreCase is true)
// data.cache = table cell text from cache, so it has been parsed (& in all lower case if config.ignoreCase is true)
// data.index = column index; table = table element (DOM)
// data.parsed = array (by column) of boolean values (from filter_useParsedData or "filter-parsed" class)
types: {
// Look for regex
regex: function( c, data ) {
if ( ts.filter.regex.regex.test(data.iFilter) ) {
var matches,
regex = ts.filter.regex.regex.exec(data.iFilter);
try {
matches = new RegExp(regex[1], regex[2]).test( data.iExact );
} catch (error) {
matches = false;
}
return matches;
}
return null;
},
// Look for operators >, >=, < or <=
operators: function( c, data ) {
if ( /^[<>]=?/.test(data.iFilter) ) {
var cachedValue, result,
table = c.table,
index = data.index,
parsed = data.parsed[index],
query = ts.formatFloat( data.iFilter.replace(ts.filter.regex.operators, ''), table ),
parser = c.parsers[index],
savedSearch = query;
// parse filter value in case we're comparing numbers (dates)
if (parsed || parser.type === 'numeric') {
result = ts.filter.parseFilter(c, $.trim('' + data.iFilter.replace(ts.filter.regex.operators, '')), index, parsed, true);
query = ( typeof result === "number" && result !== '' && !isNaN(result) ) ? result : query;
}
// iExact may be numeric - see issue #149;
// check if cached is defined, because sometimes j goes out of range? (numeric columns)
cachedValue = ( parsed || parser.type === 'numeric' ) && !isNaN(query) && typeof data.cache !== 'undefined' ? data.cache :
isNaN(data.iExact) ? ts.formatFloat( data.iExact.replace(ts.filter.regex.nondigit, ''), table) :
ts.formatFloat( data.iExact, table );
if ( />/.test(data.iFilter) ) { result = />=/.test(data.iFilter) ? cachedValue >= query : cachedValue > query; }
if ( /= 0);
}
}
return null;
},
// Look for quotes or equals to get an exact match; ignore type since iExact could be numeric
exact: function( c, data ) {
/*jshint eqeqeq:false */
if (ts.filter.regex.exact.test(data.iFilter)) {
var filter = ts.filter.parseFilter(c, data.iFilter.replace(ts.filter.regex.exact, ''), data.index, data.parsed[data.index]) || '';
return data.anyMatch ? $.inArray(filter, data.rowArray) >= 0 : filter == data.iExact;
}
return null;
},
// Look for an AND or && operator (logical and)
and : function( c, data ) {
if ( ts.filter.regex.andTest.test(data.filter) ) {
var index = data.index,
parsed = data.parsed[index],
query = data.iFilter.split( ts.filter.regex.andSplit ),
result = data.iExact.search( $.trim( ts.filter.parseFilter(c, query[0], index, parsed) ) ) >= 0,
indx = query.length - 1;
while (result && indx) {
result = result && data.iExact.search( $.trim( ts.filter.parseFilter(c, query[indx], index, parsed) ) ) >= 0;
indx--;
}
return result;
}
return null;
},
// Look for a range (using " to " or " - ") - see issue #166; thanks matzhu!
range : function( c, data ) {
if ( ts.filter.regex.toTest.test(data.iFilter) ) {
var result, tmp,
table = c.table,
index = data.index,
parsed = data.parsed[index],
// make sure the dash is for a range and not indicating a negative number
query = data.iFilter.split( ts.filter.regex.toSplit ),
range1 = ts.formatFloat( ts.filter.parseFilter(c, query[0].replace(ts.filter.regex.nondigit, '') || '', index, parsed), table ),
range2 = ts.formatFloat( ts.filter.parseFilter(c, query[1].replace(ts.filter.regex.nondigit, '') || '', index, parsed), table );
// parse filter value in case we're comparing numbers (dates)
if (parsed || c.parsers[index].type === 'numeric') {
result = c.parsers[index].format('' + query[0], table, c.$headers.eq(index), index);
range1 = (result !== '' && !isNaN(result)) ? result : range1;
result = c.parsers[index].format('' + query[1], table, c.$headers.eq(index), index);
range2 = (result !== '' && !isNaN(result)) ? result : range2;
}
result = ( parsed || c.parsers[index].type === 'numeric' ) && !isNaN(range1) && !isNaN(range2) ? data.cache :
isNaN(data.iExact) ? ts.formatFloat( data.iExact.replace(ts.filter.regex.nondigit, ''), table) :
ts.formatFloat( data.iExact, table );
if (range1 > range2) { tmp = range1; range1 = range2; range2 = tmp; } // swap
return (result >= range1 && result <= range2) || (range1 === '' || range2 === '');
}
return null;
},
// Look for wild card: ? = single, * = multiple, or | = logical OR
wild : function( c, data ) {
if ( /[\?\*\|]/.test(data.iFilter) || ts.filter.regex.orReplace.test(data.filter) ) {
var index = data.index,
parsed = data.parsed[index],
query = ts.filter.parseFilter(c, data.iFilter.replace(ts.filter.regex.orReplace, "|"), index, parsed) || '';
// look for an exact match with the "or" unless the "filter-match" class is found
if (!c.$headerIndexed[index].hasClass('filter-match') && /\|/.test(query)) {
// show all results while using filter match. Fixes #727
if (query[ query.length - 1 ] === '|') { query += '*'; }
query = data.anyMatch && $.isArray(data.rowArray) ? '(' + query + ')' : '^(' + query + ')$';
}
// parsing the filter may not work properly when using wildcards =/
return new RegExp( query.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(data.iExact);
}
return null;
},
// fuzzy text search; modified from https://github.com/mattyork/fuzzy (MIT license)
fuzzy: function( c, data ) {
if ( /^~/.test(data.iFilter) ) {
var indx,
patternIndx = 0,
len = data.iExact.length,
pattern = ts.filter.parseFilter(c, data.iFilter.slice(1), data.index, data.parsed[data.index]) || '';
for (indx = 0; indx < len; indx++) {
if (data.iExact[indx] === pattern[patternIndx]) {
patternIndx += 1;
}
}
if (patternIndx === pattern.length) {
return true;
}
return false;
}
return null;
}
},
init: function(table, c, wo) {
// filter language options
ts.language = $.extend(true, {}, {
to : 'to',
or : 'or',
and : 'and'
}, ts.language);
var options, string, txt, $header, column, filters, val, fxn, noSelect,
regex = ts.filter.regex;
c.$table.addClass('hasFilters');
// define timers so using clearTimeout won't cause an undefined error
wo.searchTimer = null;
wo.filter_initTimer = null;
wo.filter_formatterCount = 0;
wo.filter_formatterInit = [];
wo.filter_anyColumnSelector = '[data-column="all"],[data-column="any"]';
wo.filter_multipleColumnSelector = '[data-column*="-"],[data-column*=","]';
txt = '\\{' + ts.filter.regex.query + '\\}';
$.extend( regex, {
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'),
iQuery : new RegExp(txt, 'i'),
igQuery : new RegExp(txt, 'ig')
});
// 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, .parser-false').length !== c.$headers.length) {
// build filter row
ts.filter.buildRow(table, c, wo);
}
txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join(c.namespace + 'filter ');
c.$table.bind( txt, function(event, filter) {
val = (wo.filter_hideEmpty && $.isEmptyObject(c.cache) && !(c.delayInit && event.type === 'appendCache'));
// hide filter row using the "filtered" class name
c.$table.find('.' + ts.css.filterRow).toggleClass(wo.filter_filteredRow, val ); // fixes #450
if ( !/(search|filter)/.test(event.type) ) {
event.stopPropagation();
ts.filter.buildDefault(table, true);
}
if (event.type === 'filterReset') {
c.$table.find('.' + ts.css.filter).add(wo.filter_$externalFilters).val('');
ts.filter.searching(table, []);
} else if (event.type === 'filterEnd') {
ts.filter.buildDefault(table, true);
} else {
// send false argument to force a new search; otherwise if the filter hasn't changed, it will return
filter = event.type === 'search' ? filter : event.type === 'updateComplete' ? c.$table.data('lastSearch') : '';
if (/(update|add)/.test(event.type) && event.type !== "updateComplete") {
// force a new search since content has changed
c.lastCombinedFilter = null;
c.lastSearch = [];
}
// 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', [...]);
ts.filter.searching(table, filter, true);
}
return false;
});
// reset button/link
if (wo.filter_reset) {
if (wo.filter_reset instanceof $) {
// reset contains a jQuery object, bind to it
wo.filter_reset.click(function(){
c.$table.trigger('filterReset');
});
} else if ($(wo.filter_reset).length) {
// reset is a jQuery selector, use event delegation
$(document)
.undelegate(wo.filter_reset, 'click.tsfilter')
.delegate(wo.filter_reset, 'click.tsfilter', function() {
// trigger a reset event, so other functions (filter_formatter) know when to reset
c.$table.trigger('filterReset');
});
}
}
if (wo.filter_functions) {
for (column = 0; column < c.columns; column++) {
fxn = ts.getColumnData( table, wo.filter_functions, column );
if (fxn) {
// remove "filter-select" from header otherwise the options added here are replaced with all options
$header = c.$headerIndexed[column].removeClass('filter-select');
// don't build select if "filter-false" or "parser-false" set
noSelect = !($header.hasClass('filter-false') || $header.hasClass('parser-false'));
options = '';
if ( fxn === true && noSelect ) {
ts.filter.buildSelect(table, column);
} else if ( typeof fxn === 'object' && noSelect ) {
// add custom drop down list
for (string in fxn) {
if (typeof string === 'string') {
options += options === '' ?
'' : '';
val = string;
txt = string;
if (string.indexOf(wo.filter_selectSourceSeparator) >= 0) {
val = string.split(wo.filter_selectSourceSeparator);
txt = val[1];
val = val[0];
}
options += '';
}
}
c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]').append(options);
}
}
}
}
// not really updating, but if the column has both the "filter-select" class & filter_functions set to true,
// it would append the same options twice.
ts.filter.buildDefault(table, true);
ts.filter.bindSearch( table, c.$table.find('.' + ts.css.filter), true );
if (wo.filter_external) {
ts.filter.bindSearch( table, wo.filter_external );
}
if (wo.filter_hideFilters) {
ts.filter.hideFilters(table, c);
}
// show processing icon
if (c.showProcessing) {
c.$table
.unbind( ('filterStart filterEnd '.split(' ').join(c.namespace + 'filter ')).replace(/\s+/g, ' ') )
.bind( 'filterStart filterEnd '.split(' ').join(c.namespace + 'filter '), function(event, columns) {
// only add processing to certain columns to all columns
$header = (columns) ? c.$table.find('.' + ts.css.header).filter('[data-column]').filter(function() {
return columns[$(this).data('column')] !== '';
}) : '';
ts.isProcessing(table, event.type === 'filterStart', columns ? $header : '');
});
}
// set filtered rows count (intially unfiltered)
c.filteredRows = c.totalRows;
// add default values
c.$table
.unbind( ('tablesorter-initialized pagerBeforeInitialized '.split(' ').join(c.namespace + 'filter ')).replace(/\s+/g, ' ') )
.bind( 'tablesorter-initialized pagerBeforeInitialized '.split(' ').join(c.namespace + 'filter '), function() {
// redefine "wo" as it does not update properly inside this callback
var wo = this.config.widgetOptions;
filters = ts.filter.setDefaults(table, c, wo) || [];
if (filters.length) {
// prevent delayInit from triggering a cache build if filters are empty
if ( !(c.delayInit && filters.join('') === '') ) {
ts.setFilters(table, filters, true);
}
}
c.$table.trigger('filterFomatterUpdate');
// trigger init after setTimeout to prevent multiple filterStart/End/Init triggers
setTimeout(function(){
if (!wo.filter_initialized) {
ts.filter.filterInitComplete(c);
}
}, 100);
});
// if filter widget is added after pager has initialized; then set filter init flag
if (c.pager && c.pager.initialized && !wo.filter_initialized) {
c.$table.trigger('filterFomatterUpdate');
setTimeout(function(){
ts.filter.filterInitComplete(c);
}, 100);
}
},
// $cell parameter, but not the config, is passed to the
// filter_formatters, so we have to work with it instead
formatterUpdated: function($cell, column) {
var wo = $cell.closest('table')[0].config.widgetOptions;
if (!wo.filter_initialized) {
// add updates by column since this function
// may be called numerous times before initialization
wo.filter_formatterInit[column] = 1;
}
},
filterInitComplete: function(c){
var indx, len,
wo = c.widgetOptions,
count = 0,
completed = function(){
wo.filter_initialized = true;
c.$table.trigger('filterInit', c);
ts.filter.findRows(c.table, c.$table.data('lastSearch') || []);
};
if ( $.isEmptyObject( wo.filter_formatter ) ) {
completed();
} else {
len = wo.filter_formatterInit.length;
for (indx = 0; indx < len; indx++) {
if (wo.filter_formatterInit[indx] === 1) {
count++;
}
}
clearTimeout(wo.filter_initTimer);
if (!wo.filter_initialized && count === wo.filter_formatterCount) {
// filter widget initialized
completed();
} else if (!wo.filter_initialized) {
// fall back in case a filter_formatter doesn't call
// $.tablesorter.filter.formatterUpdated($cell, column), and the count is off
wo.filter_initTimer = setTimeout(function(){
completed();
}, 500);
}
}
},
setDefaults: function(table, c, wo) {
var isArray, saved, indx, col, $filters,
// get current (default) filters
filters = ts.getFilters(table) || [];
if (wo.filter_saveFilters && ts.storage) {
saved = ts.storage( table, 'tablesorter-filters' ) || [];
isArray = $.isArray(saved);
// make sure we're not just getting an empty array
if ( !(isArray && saved.join('') === '' || !isArray) ) { filters = saved; }
}
// if no filters saved, then check default settings
if (filters.join('') === '') {
// allow adding default setting to external filters
$filters = c.$headers.add( wo.filter_$externalFilters ).filter('[' + wo.filter_defaultAttrib + ']');
for (indx = 0; indx <= c.columns; indx++) {
// include data-column="all" external filters
col = indx === c.columns ? 'all' : indx;
filters[indx] = $filters.filter('[data-column="' + col + '"]').attr(wo.filter_defaultAttrib) || filters[indx] || '';
}
}
c.$table.data('lastSearch', filters);
return filters;
},
parseFilter: function(c, filter, column, parsed, forceParse){
return forceParse || parsed ?
c.parsers[column].format( filter, c.table, [], column ) :
filter;
},
buildRow: function(table, c, wo) {
var col, column, $header, buildSelect, disabled, name, ffxn,
// c.columns defined in computeThIndexes()
columns = c.columns,
arry = $.isArray(wo.filter_cellFilter),
buildFilter = '
';
for (column = 0; column < columns; column++) {
if (arry) {
buildFilter += '
';
} else {
buildFilter += '
';
}
}
c.$filters = $(buildFilter += '
').appendTo( c.$table.children('thead').eq(0) ).find('td');
// build each filter input
for (column = 0; column < columns; column++) {
disabled = false;
// assuming last cell of a column is the main column
$header = c.$headerIndexed[column];
ffxn = ts.getColumnData( table, wo.filter_functions, column );
buildSelect = (wo.filter_functions && ffxn && typeof ffxn !== "function" ) ||
$header.hasClass('filter-select');
// get data from jQuery data, metadata, headers option or header class name
col = ts.getColumnData( table, c.headers, column );
disabled = ts.getData($header[0], col, 'filter') === 'false' || ts.getData($header[0], col, 'parser') === 'false';
if (buildSelect) {
buildFilter = $('