mirror of
https://github.com/Mottie/tablesorter.git
synced 2024-11-15 23:54:22 +00:00
20d87d933e
Only reset the lastSearch and lastCombinedFilter variables if the filters were equal before. We don't need to "force filter refresh" if the filters were not equal anyways.
3185 lines
124 KiB
JavaScript
3185 lines
124 KiB
JavaScript
/*** This file is dynamically generated ***
|
|
█████▄ ▄████▄ █████▄ ▄████▄ ██████ ███████▄ ▄████▄ █████▄ ██ ██████ ██ ██
|
|
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██▄▄ ██▄▄██
|
|
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀▀▀██
|
|
█████▀ ▀████▀ ██ ██ ▀████▀ ██ ██ ██ ██ ▀████▀ █████▀ ██ ██ █████▀
|
|
*/
|
|
/*! tablesorter (FORK) - updated 2018-11-20 (v2.31.1)*/
|
|
/* Includes widgets ( storage,uitheme,columns,filter,stickyHeaders,resizable,saveSort ) */
|
|
(function(factory){if (typeof define === 'function' && define.amd){define(['jquery'], factory);} else if (typeof module === 'object' && typeof module.exports === 'object'){module.exports = factory(require('jquery'));} else {factory(jQuery);}}(function(jQuery) {
|
|
/*! Widget: storage - updated 2018-03-18 (v2.30.0) */
|
|
/*global JSON:false */
|
|
;(function ($, window, document) {
|
|
'use strict';
|
|
|
|
var ts = $.tablesorter || {};
|
|
|
|
// update defaults for validator; these values must be falsy!
|
|
$.extend(true, ts.defaults, {
|
|
fixedUrl: '',
|
|
widgetOptions: {
|
|
storage_fixedUrl: '',
|
|
storage_group: '',
|
|
storage_page: '',
|
|
storage_storageType: '',
|
|
storage_tableId: '',
|
|
storage_useSessionStorage: ''
|
|
}
|
|
});
|
|
|
|
// *** Store data in local storage, with a cookie fallback ***
|
|
/* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json)
|
|
if you need it, then include https://github.com/douglascrockford/JSON-js
|
|
|
|
$.parseJSON is not available is jQuery versions older than 1.4.1, using older
|
|
versions will only allow storing information for one page at a time
|
|
|
|
// *** Save data (JSON format only) ***
|
|
// val must be valid JSON... use http://jsonlint.com/ to ensure it is valid
|
|
var val = { "mywidget" : "data1" }; // valid JSON uses double quotes
|
|
// $.tablesorter.storage(table, key, val);
|
|
$.tablesorter.storage(table, 'tablesorter-mywidget', val);
|
|
|
|
// *** Get data: $.tablesorter.storage(table, key); ***
|
|
v = $.tablesorter.storage(table, 'tablesorter-mywidget');
|
|
// val may be empty, so also check for your data
|
|
val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : '';
|
|
alert(val); // 'data1' if saved, or '' if not
|
|
*/
|
|
ts.storage = function(table, key, value, options) {
|
|
table = $(table)[0];
|
|
var cookieIndex, cookies, date,
|
|
hasStorage = false,
|
|
values = {},
|
|
c = table.config,
|
|
wo = c && c.widgetOptions,
|
|
debug = ts.debug(c, 'storage'),
|
|
storageType = (
|
|
( options && options.storageType ) || ( wo && wo.storage_storageType )
|
|
).toString().charAt(0).toLowerCase(),
|
|
// deprecating "useSessionStorage"; any storageType setting overrides it
|
|
session = storageType ? '' :
|
|
( options && options.useSessionStorage ) || ( wo && wo.storage_useSessionStorage ),
|
|
$table = $(table),
|
|
// id from (1) options ID, (2) table 'data-table-group' attribute, (3) widgetOptions.storage_tableId,
|
|
// (4) table ID, then (5) table index
|
|
id = options && options.id ||
|
|
$table.attr( options && options.group || wo && wo.storage_group || 'data-table-group') ||
|
|
wo && wo.storage_tableId || table.id || $('.tablesorter').index( $table ),
|
|
// url from (1) options url, (2) table 'data-table-page' attribute, (3) widgetOptions.storage_fixedUrl,
|
|
// (4) table.config.fixedUrl (deprecated), then (5) window location path
|
|
url = options && options.url ||
|
|
$table.attr(options && options.page || wo && wo.storage_page || 'data-table-page') ||
|
|
wo && wo.storage_fixedUrl || c && c.fixedUrl || window.location.pathname;
|
|
|
|
// skip if using cookies
|
|
if (storageType !== 'c') {
|
|
storageType = (storageType === 's' || session) ? 'sessionStorage' : 'localStorage';
|
|
// https://gist.github.com/paulirish/5558557
|
|
if (storageType in window) {
|
|
try {
|
|
window[storageType].setItem('_tmptest', 'temp');
|
|
hasStorage = true;
|
|
window[storageType].removeItem('_tmptest');
|
|
} catch (error) {
|
|
console.warn( storageType + ' is not supported in this browser' );
|
|
}
|
|
}
|
|
}
|
|
if (debug) {
|
|
console.log('Storage >> Using', hasStorage ? storageType : 'cookies');
|
|
}
|
|
// *** get value ***
|
|
if ($.parseJSON) {
|
|
if (hasStorage) {
|
|
values = $.parseJSON( window[storageType][key] || 'null' ) || {};
|
|
} else {
|
|
// old browser, using cookies
|
|
cookies = document.cookie.split(/[;\s|=]/);
|
|
// add one to get from the key to the value
|
|
cookieIndex = $.inArray(key, cookies) + 1;
|
|
values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || 'null') || {} : {};
|
|
}
|
|
}
|
|
// allow value to be an empty string too
|
|
if (typeof value !== 'undefined' && window.JSON && JSON.hasOwnProperty('stringify')) {
|
|
// add unique identifiers = url pathname > table ID/index on page > data
|
|
if (!values[url]) {
|
|
values[url] = {};
|
|
}
|
|
values[url][id] = value;
|
|
// *** set value ***
|
|
if (hasStorage) {
|
|
window[storageType][key] = JSON.stringify(values);
|
|
} else {
|
|
date = new Date();
|
|
date.setTime(date.getTime() + (31536e+6)); // 365 days
|
|
document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g, '\"') + '; expires=' + date.toGMTString() + '; path=/';
|
|
}
|
|
} else {
|
|
return values && values[url] ? values[url][id] : '';
|
|
}
|
|
};
|
|
|
|
})(jQuery, window, document);
|
|
|
|
/*! Widget: uitheme - updated 2018-03-18 (v2.30.0) */
|
|
;(function ($) {
|
|
'use strict';
|
|
var ts = $.tablesorter || {};
|
|
|
|
ts.themes = {
|
|
'bootstrap' : {
|
|
table : 'table table-bordered table-striped',
|
|
caption : 'caption',
|
|
// header class names
|
|
header : 'bootstrap-header', // give the header a gradient background (theme.bootstrap_2.css)
|
|
sortNone : '',
|
|
sortAsc : '',
|
|
sortDesc : '',
|
|
active : '', // applied when column is sorted
|
|
hover : '', // custom css required - a defined bootstrap style may not override other classes
|
|
// icon class names
|
|
icons : '', // add 'bootstrap-icon-white' to make them white; this icon class is added to the <i> in the header
|
|
iconSortNone : 'bootstrap-icon-unsorted', // class name added to icon when column is not sorted
|
|
iconSortAsc : 'glyphicon glyphicon-chevron-up', // class name added to icon when column has ascending sort
|
|
iconSortDesc : 'glyphicon glyphicon-chevron-down', // class name added to icon when column has descending sort
|
|
filterRow : '', // filter row class
|
|
footerRow : '',
|
|
footerCells : '',
|
|
even : '', // even row zebra striping
|
|
odd : '' // odd row zebra striping
|
|
},
|
|
'jui' : {
|
|
table : 'ui-widget ui-widget-content ui-corner-all', // table classes
|
|
caption : 'ui-widget-content',
|
|
// header class names
|
|
header : 'ui-widget-header ui-corner-all ui-state-default', // header classes
|
|
sortNone : '',
|
|
sortAsc : '',
|
|
sortDesc : '',
|
|
active : 'ui-state-active', // applied when column is sorted
|
|
hover : 'ui-state-hover', // hover class
|
|
// icon class names
|
|
icons : 'ui-icon', // icon class added to the <i> in the header
|
|
iconSortNone : 'ui-icon-carat-2-n-s ui-icon-caret-2-n-s', // class name added to icon when column is not sorted
|
|
iconSortAsc : 'ui-icon-carat-1-n ui-icon-caret-1-n', // class name added to icon when column has ascending sort
|
|
iconSortDesc : 'ui-icon-carat-1-s ui-icon-caret-1-s', // class name added to icon when column has descending sort
|
|
filterRow : '',
|
|
footerRow : '',
|
|
footerCells : '',
|
|
even : 'ui-widget-content', // even row zebra striping
|
|
odd : 'ui-state-default' // odd row zebra striping
|
|
}
|
|
};
|
|
|
|
$.extend(ts.css, {
|
|
wrapper : 'tablesorter-wrapper' // ui theme & resizable
|
|
});
|
|
|
|
ts.addWidget({
|
|
id: 'uitheme',
|
|
priority: 10,
|
|
format: function(table, c, wo) {
|
|
var i, tmp, hdr, icon, time, $header, $icon, $tfoot, $h, oldtheme, oldremove, oldIconRmv, hasOldTheme,
|
|
themesAll = ts.themes,
|
|
$table = c.$table.add( $( c.namespace + '_extra_table' ) ),
|
|
$headers = c.$headers.add( $( c.namespace + '_extra_headers' ) ),
|
|
theme = c.theme || 'jui',
|
|
themes = themesAll[theme] || {},
|
|
remove = $.trim( [ themes.sortNone, themes.sortDesc, themes.sortAsc, themes.active ].join( ' ' ) ),
|
|
iconRmv = $.trim( [ themes.iconSortNone, themes.iconSortDesc, themes.iconSortAsc ].join( ' ' ) ),
|
|
debug = ts.debug(c, 'uitheme');
|
|
if (debug) { time = new Date(); }
|
|
// initialization code - run once
|
|
if (!$table.hasClass('tablesorter-' + theme) || c.theme !== c.appliedTheme || !wo.uitheme_applied) {
|
|
wo.uitheme_applied = true;
|
|
oldtheme = themesAll[c.appliedTheme] || {};
|
|
hasOldTheme = !$.isEmptyObject(oldtheme);
|
|
oldremove = hasOldTheme ? [ oldtheme.sortNone, oldtheme.sortDesc, oldtheme.sortAsc, oldtheme.active ].join( ' ' ) : '';
|
|
oldIconRmv = hasOldTheme ? [ oldtheme.iconSortNone, oldtheme.iconSortDesc, oldtheme.iconSortAsc ].join( ' ' ) : '';
|
|
if (hasOldTheme) {
|
|
wo.zebra[0] = $.trim( ' ' + wo.zebra[0].replace(' ' + oldtheme.even, '') );
|
|
wo.zebra[1] = $.trim( ' ' + wo.zebra[1].replace(' ' + oldtheme.odd, '') );
|
|
c.$tbodies.children().removeClass( [ oldtheme.even, oldtheme.odd ].join(' ') );
|
|
}
|
|
// update zebra stripes
|
|
if (themes.even) { wo.zebra[0] += ' ' + themes.even; }
|
|
if (themes.odd) { wo.zebra[1] += ' ' + themes.odd; }
|
|
// add caption style
|
|
$table.children('caption')
|
|
.removeClass(oldtheme.caption || '')
|
|
.addClass(themes.caption);
|
|
// add table/footer class names
|
|
$tfoot = $table
|
|
// remove other selected themes
|
|
.removeClass( (c.appliedTheme ? 'tablesorter-' + (c.appliedTheme || '') : '') + ' ' + (oldtheme.table || '') )
|
|
.addClass('tablesorter-' + theme + ' ' + (themes.table || '')) // add theme widget class name
|
|
.children('tfoot');
|
|
c.appliedTheme = c.theme;
|
|
|
|
if ($tfoot.length) {
|
|
$tfoot
|
|
// if oldtheme.footerRow or oldtheme.footerCells are undefined, all class names are removed
|
|
.children('tr').removeClass(oldtheme.footerRow || '').addClass(themes.footerRow)
|
|
.children('th, td').removeClass(oldtheme.footerCells || '').addClass(themes.footerCells);
|
|
}
|
|
// update header classes
|
|
$headers
|
|
.removeClass( (hasOldTheme ? [ oldtheme.header, oldtheme.hover, oldremove ].join(' ') : '') || '' )
|
|
.addClass(themes.header)
|
|
.not('.sorter-false')
|
|
.unbind('mouseenter.tsuitheme mouseleave.tsuitheme')
|
|
.bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) {
|
|
// toggleClass with switch added in jQuery 1.3
|
|
$(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover || '');
|
|
});
|
|
|
|
$headers.each(function() {
|
|
var $this = $(this);
|
|
if (!$this.find('.' + ts.css.wrapper).length) {
|
|
// Firefox needs this inner div to position the icon & resizer correctly
|
|
$this.wrapInner('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>');
|
|
}
|
|
});
|
|
if (c.cssIcon) {
|
|
// if c.cssIcon is '', then no <i> is added to the header
|
|
$headers
|
|
.find('.' + ts.css.icon)
|
|
.removeClass(hasOldTheme ? [ oldtheme.icons, oldIconRmv ].join(' ') : '')
|
|
.addClass(themes.icons || '');
|
|
}
|
|
// filter widget initializes after uitheme
|
|
if (ts.hasWidget( c.table, 'filter' )) {
|
|
tmp = function() {
|
|
$table.children('thead').children('.' + ts.css.filterRow)
|
|
.removeClass(hasOldTheme ? oldtheme.filterRow || '' : '')
|
|
.addClass(themes.filterRow || '');
|
|
};
|
|
if (wo.filter_initialized) {
|
|
tmp();
|
|
} else {
|
|
$table.one('filterInit', function() {
|
|
tmp();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
for (i = 0; i < c.columns; i++) {
|
|
$header = c.$headers
|
|
.add($(c.namespace + '_extra_headers'))
|
|
.not('.sorter-false')
|
|
.filter('[data-column="' + i + '"]');
|
|
$icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $();
|
|
$h = $headers.not('.sorter-false').filter('[data-column="' + i + '"]:last');
|
|
if ($h.length) {
|
|
$header.removeClass(remove);
|
|
$icon.removeClass(iconRmv);
|
|
if ($h[0].sortDisabled) {
|
|
// no sort arrows for disabled columns!
|
|
$icon.removeClass(themes.icons || '');
|
|
} else {
|
|
hdr = themes.sortNone;
|
|
icon = themes.iconSortNone;
|
|
if ($h.hasClass(ts.css.sortAsc)) {
|
|
hdr = [ themes.sortAsc, themes.active ].join(' ');
|
|
icon = themes.iconSortAsc;
|
|
} else if ($h.hasClass(ts.css.sortDesc)) {
|
|
hdr = [ themes.sortDesc, themes.active ].join(' ');
|
|
icon = themes.iconSortDesc;
|
|
}
|
|
$header.addClass(hdr);
|
|
$icon.addClass(icon || '');
|
|
}
|
|
}
|
|
}
|
|
if (debug) {
|
|
console.log('uitheme >> Applied ' + theme + ' theme' + ts.benchmark(time));
|
|
}
|
|
},
|
|
remove: function(table, c, wo, refreshing) {
|
|
if (!wo.uitheme_applied) { return; }
|
|
var $table = c.$table,
|
|
theme = c.appliedTheme || 'jui',
|
|
themes = ts.themes[ theme ] || ts.themes.jui,
|
|
$headers = $table.children('thead').children(),
|
|
remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc,
|
|
iconRmv = themes.iconSortNone + ' ' + themes.iconSortDesc + ' ' + themes.iconSortAsc;
|
|
$table.removeClass('tablesorter-' + theme + ' ' + themes.table);
|
|
wo.uitheme_applied = false;
|
|
if (refreshing) { return; }
|
|
$table.find(ts.css.header).removeClass(themes.header);
|
|
$headers
|
|
.unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover
|
|
.removeClass(themes.hover + ' ' + remove + ' ' + themes.active)
|
|
.filter('.' + ts.css.filterRow)
|
|
.removeClass(themes.filterRow);
|
|
$headers.find('.' + ts.css.icon).removeClass(themes.icons + ' ' + iconRmv);
|
|
}
|
|
});
|
|
|
|
})(jQuery);
|
|
|
|
/*! Widget: columns - updated 5/24/2017 (v2.28.11) */
|
|
;(function ($) {
|
|
'use strict';
|
|
var ts = $.tablesorter || {};
|
|
|
|
ts.addWidget({
|
|
id: 'columns',
|
|
priority: 65,
|
|
options : {
|
|
columns : [ 'primary', 'secondary', 'tertiary' ]
|
|
},
|
|
format: function(table, c, wo) {
|
|
var $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx,
|
|
$table = c.$table,
|
|
$tbodies = c.$tbodies,
|
|
sortList = c.sortList,
|
|
len = sortList.length,
|
|
// removed c.widgetColumns support
|
|
css = wo && wo.columns || [ 'primary', 'secondary', 'tertiary' ],
|
|
last = css.length - 1;
|
|
remove = css.join(' ');
|
|
// check if there is a sort (on initialization there may not be one)
|
|
for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
|
|
$tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody
|
|
$rows = $tbody.children('tr');
|
|
// loop through the visible rows
|
|
$rows.each(function() {
|
|
$row = $(this);
|
|
if (this.style.display !== 'none') {
|
|
// remove all columns class names
|
|
$cells = $row.children().removeClass(remove);
|
|
// add appropriate column class names
|
|
if (sortList && sortList[0]) {
|
|
// primary sort column class
|
|
$cells.eq(sortList[0][0]).addClass(css[0]);
|
|
if (len > 1) {
|
|
for (indx = 1; indx < len; indx++) {
|
|
// secondary, tertiary, etc sort column classes
|
|
$cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
ts.processTbody(table, $tbody, false);
|
|
}
|
|
// add classes to thead and tfoot
|
|
rows = wo.columns_thead !== false ? [ 'thead tr' ] : [];
|
|
if (wo.columns_tfoot !== false) {
|
|
rows.push('tfoot tr');
|
|
}
|
|
if (rows.length) {
|
|
$rows = $table.find( rows.join(',') ).children().removeClass(remove);
|
|
if (len) {
|
|
for (indx = 0; indx < len; indx++) {
|
|
// add primary. secondary, tertiary, etc sort column classes
|
|
$rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
remove: function(table, c, wo) {
|
|
var tbodyIndex, $tbody,
|
|
$tbodies = c.$tbodies,
|
|
remove = (wo.columns || [ 'primary', 'secondary', 'tertiary' ]).join(' ');
|
|
c.$headers.removeClass(remove);
|
|
c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove);
|
|
for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
|
|
$tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
|
|
$tbody.children('tr').each(function() {
|
|
$(this).children().removeClass(remove);
|
|
});
|
|
ts.processTbody(table, $tbody, false); // restore tbody
|
|
}
|
|
}
|
|
});
|
|
|
|
})(jQuery);
|
|
|
|
/*! Widget: filter - updated 2018-03-18 (v2.30.0) *//*
|
|
* Requires tablesorter v2.8+ and jQuery 1.7+
|
|
* by Rob Garrison
|
|
*/
|
|
;( function ( $ ) {
|
|
'use strict';
|
|
var tsf, tsfRegex,
|
|
ts = $.tablesorter || {},
|
|
tscss = ts.css,
|
|
tskeyCodes = ts.keyCodes;
|
|
|
|
$.extend( tscss, {
|
|
filterRow : 'tablesorter-filter-row',
|
|
filter : 'tablesorter-filter',
|
|
filterDisabled : 'disabled',
|
|
filterRowHide : 'hideme'
|
|
});
|
|
|
|
$.extend( tskeyCodes, {
|
|
backSpace : 8,
|
|
escape : 27,
|
|
space : 32,
|
|
left : 37,
|
|
down : 40
|
|
});
|
|
|
|
ts.addWidget({
|
|
id: 'filter',
|
|
priority: 50,
|
|
options : {
|
|
filter_cellFilter : '', // css class name added to the filter cell ( string or array )
|
|
filter_childRows : false, // if true, filter includes child row content in the search
|
|
filter_childByColumn : false, // ( filter_childRows must be true ) if true = search child rows by column; false = search all child row text grouped
|
|
filter_childWithSibs : true, // if true, include matching child row siblings
|
|
filter_columnAnyMatch: true, // if true, allows using '#:{query}' in AnyMatch searches ( column:query )
|
|
filter_columnFilters : true, // if true, a filter will be added to the top of each table column
|
|
filter_cssFilter : '', // css class name added to the filter row & each input in the row ( tablesorter-filter is ALWAYS added )
|
|
filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value
|
|
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; define in css with "display:none" to hide the filtered-out rows
|
|
filter_filterLabel : 'Filter "{{label}}" column by...', // Aria-label added to filter input/select; see #1495
|
|
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_matchType : { 'input': 'exact', 'select': 'exact' }, // global query settings ('exact' or 'match'); overridden by "filter-match" or "filter-exact" class
|
|
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_resetOnEsc : true, // Reset filter input when the user presses escape - normalized across browsers
|
|
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_selectSourceSeparator : '|', // filter_selectSource array text left of the separator is added to the option value, right into the option text
|
|
filter_serversideFiltering : false, // if true, must perform server-side filtering b/c client-side filtering is disabled, but the ui and events will still be used.
|
|
filter_startsWith : false, // if true, filter start from the beginning of the cell contents
|
|
filter_useParsedData : false // filter all data using parsed content
|
|
},
|
|
format: function( table, c, wo ) {
|
|
if ( !c.$table.hasClass( 'hasFilters' ) ) {
|
|
tsf.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 ' +
|
|
'filterAndSortReset filterFomatterUpdate filterEnd search stickyHeadersInit '
|
|
).split( ' ' ).join( c.namespace + 'filter ' );
|
|
$table
|
|
.removeClass( 'hasFilters' )
|
|
// add filter namespace to all BUT search
|
|
.unbind( events.replace( ts.regex.spaces, ' ' ) )
|
|
// remove the filter row even if refreshing, because the column might have been moved
|
|
.find( '.' + tscss.filterRow ).remove();
|
|
wo.filter_initialized = false;
|
|
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' + c.namespace + 'filter' );
|
|
}
|
|
}
|
|
});
|
|
|
|
tsf = ts.filter = {
|
|
|
|
// regex used in filter 'check' functions - not for general use and not documented
|
|
regex: {
|
|
regex : /^\/((?:\\\/|[^\/])+)\/([migyu]{0,5})?$/, // 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 '==')
|
|
operators : /[<>=]/g, // replace operators
|
|
query : '(q|query)', // replace filter queries
|
|
wild01 : /\?/g, // wild card match 0 or 1
|
|
wild0More : /\*/g, // wild care match 0 or more
|
|
quote : /\"/g,
|
|
isNeg1 : /(>=?\s*-\d)/,
|
|
isNeg2 : /(<=?\s*\d)/
|
|
},
|
|
// function( c, data ) { }
|
|
// c = table.config
|
|
// data.$row = jQuery object of the row currently being processed
|
|
// data.$cells = jQuery object of all cells within the current row
|
|
// data.filters = array of filters for all columns ( some may be undefined )
|
|
// data.filter = filter for the current column
|
|
// data.iFilter = same as data.filter, except lowercase ( if wo.filter_ignoreCase is true )
|
|
// data.exact = table cell text ( or parsed data if column parser enabled; may be a number & not a string )
|
|
// data.iExact = same as data.exact, except lowercase ( if wo.filter_ignoreCase is true; may be a number & not a string )
|
|
// data.cache = table cell text from cache, so it has been parsed ( & in all lower case if c.ignoreCase is true )
|
|
// data.cacheArray = An array of parsed content from each table cell in the row being processed
|
|
// data.index = column index; table = table element ( DOM )
|
|
// data.parsed = array ( by column ) of boolean values ( from filter_useParsedData or 'filter-parsed' class )
|
|
types: {
|
|
or : function( c, data, vars ) {
|
|
// look for "|", but not if it is inside of a regular expression
|
|
if ( ( tsfRegex.orTest.test( data.iFilter ) || tsfRegex.orSplit.test( data.filter ) ) &&
|
|
// this test for regex has potential to slow down the overall search
|
|
!tsfRegex.regex.test( data.filter ) ) {
|
|
var indx, filterMatched, query, regex,
|
|
// duplicate data but split filter
|
|
data2 = $.extend( {}, data ),
|
|
filter = data.filter.split( tsfRegex.orSplit ),
|
|
iFilter = data.iFilter.split( tsfRegex.orSplit ),
|
|
len = filter.length;
|
|
for ( indx = 0; indx < len; indx++ ) {
|
|
data2.nestedFilters = true;
|
|
data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' );
|
|
data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' );
|
|
query = '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')';
|
|
try {
|
|
// use try/catch, because query may not be a valid regex if "|" is contained within a partial regex search,
|
|
// e.g "/(Alex|Aar" -> Uncaught SyntaxError: Invalid regular expression: /(/(Alex)/: Unterminated group
|
|
regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' );
|
|
// filterMatched = data2.filter === '' && indx > 0 ? true
|
|
// look for an exact match with the 'or' unless the 'filter-match' class is found
|
|
filterMatched = regex.test( data2.exact ) || tsf.processTypes( c, data2, vars );
|
|
if ( filterMatched ) {
|
|
return filterMatched;
|
|
}
|
|
} catch ( error ) {
|
|
return null;
|
|
}
|
|
}
|
|
// may be null from processing types
|
|
return filterMatched || false;
|
|
}
|
|
return null;
|
|
},
|
|
// Look for an AND or && operator ( logical and )
|
|
and : function( c, data, vars ) {
|
|
if ( tsfRegex.andTest.test( data.filter ) ) {
|
|
var indx, filterMatched, result, query, regex,
|
|
// duplicate data but split filter
|
|
data2 = $.extend( {}, data ),
|
|
filter = data.filter.split( tsfRegex.andSplit ),
|
|
iFilter = data.iFilter.split( tsfRegex.andSplit ),
|
|
len = filter.length;
|
|
for ( indx = 0; indx < len; indx++ ) {
|
|
data2.nestedFilters = true;
|
|
data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' );
|
|
data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' );
|
|
query = ( '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')' )
|
|
// replace wild cards since /(a*)/i will match anything
|
|
.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' );
|
|
try {
|
|
// use try/catch just in case RegExp is invalid
|
|
regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' );
|
|
// look for an exact match with the 'and' unless the 'filter-match' class is found
|
|
result = ( regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ) );
|
|
if ( indx === 0 ) {
|
|
filterMatched = result;
|
|
} else {
|
|
filterMatched = filterMatched && result;
|
|
}
|
|
} catch ( error ) {
|
|
return null;
|
|
}
|
|
}
|
|
// may be null from processing types
|
|
return filterMatched || false;
|
|
}
|
|
return null;
|
|
},
|
|
// Look for regex
|
|
regex: function( c, data ) {
|
|
if ( tsfRegex.regex.test( data.filter ) ) {
|
|
var matches,
|
|
// cache regex per column for optimal speed
|
|
regex = data.filter_regexCache[ data.index ] || tsfRegex.regex.exec( data.filter ),
|
|
isRegex = regex instanceof RegExp;
|
|
try {
|
|
if ( !isRegex ) {
|
|
// force case insensitive search if ignoreCase option set?
|
|
// if ( c.ignoreCase && !regex[2] ) { regex[2] = 'i'; }
|
|
data.filter_regexCache[ data.index ] = regex = new RegExp( regex[1], regex[2] );
|
|
}
|
|
matches = regex.test( data.exact );
|
|
} catch ( error ) {
|
|
matches = false;
|
|
}
|
|
return matches;
|
|
}
|
|
return null;
|
|
},
|
|
// Look for operators >, >=, < or <=
|
|
operators: function( c, data ) {
|
|
// ignore empty strings... because '' < 10 is true
|
|
if ( tsfRegex.operTest.test( data.iFilter ) && data.iExact !== '' ) {
|
|
var cachedValue, result, txt,
|
|
table = c.table,
|
|
parsed = data.parsed[ data.index ],
|
|
query = ts.formatFloat( data.iFilter.replace( tsfRegex.operators, '' ), table ),
|
|
parser = c.parsers[ data.index ] || {},
|
|
savedSearch = query;
|
|
// parse filter value in case we're comparing numbers ( dates )
|
|
if ( parsed || parser.type === 'numeric' ) {
|
|
txt = $.trim( '' + data.iFilter.replace( tsfRegex.operators, '' ) );
|
|
result = tsf.parseFilter( c, txt, data, 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 )
|
|
if ( ( parsed || parser.type === 'numeric' ) && !isNaN( query ) &&
|
|
typeof data.cache !== 'undefined' ) {
|
|
cachedValue = data.cache;
|
|
} else {
|
|
txt = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact;
|
|
cachedValue = ts.formatFloat( txt, table );
|
|
}
|
|
if ( tsfRegex.gtTest.test( data.iFilter ) ) {
|
|
result = tsfRegex.gteTest.test( data.iFilter ) ? cachedValue >= query : cachedValue > query;
|
|
} else if ( tsfRegex.ltTest.test( data.iFilter ) ) {
|
|
result = tsfRegex.lteTest.test( data.iFilter ) ? cachedValue <= query : cachedValue < query;
|
|
}
|
|
// keep showing all rows if nothing follows the operator
|
|
if ( !result && savedSearch === '' ) {
|
|
result = true;
|
|
}
|
|
return result;
|
|
}
|
|
return null;
|
|
},
|
|
// Look for a not match
|
|
notMatch: function( c, data ) {
|
|
if ( tsfRegex.notTest.test( data.iFilter ) ) {
|
|
var indx,
|
|
txt = data.iFilter.replace( '!', '' ),
|
|
filter = tsf.parseFilter( c, txt, data ) || '';
|
|
if ( tsfRegex.exact.test( filter ) ) {
|
|
// look for exact not matches - see #628
|
|
filter = filter.replace( tsfRegex.exact, '' );
|
|
return filter === '' ? true : $.trim( filter ) !== data.iExact;
|
|
} else {
|
|
indx = data.iExact.search( $.trim( filter ) );
|
|
return filter === '' ? true :
|
|
// return true if not found
|
|
data.anyMatch ? indx < 0 :
|
|
// return false if found
|
|
!( c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 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 ( tsfRegex.exact.test( data.iFilter ) ) {
|
|
var txt = data.iFilter.replace( tsfRegex.exact, '' ),
|
|
filter = tsf.parseFilter( c, txt, data ) || '';
|
|
// eslint-disable-next-line eqeqeq
|
|
return data.anyMatch ? $.inArray( filter, data.rowArray ) >= 0 : filter == data.iExact;
|
|
}
|
|
return null;
|
|
},
|
|
// Look for a range ( using ' to ' or ' - ' ) - see issue #166; thanks matzhu!
|
|
range : function( c, data ) {
|
|
if ( tsfRegex.toTest.test( data.iFilter ) ) {
|
|
var result, tmp, range1, range2,
|
|
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( tsfRegex.toSplit );
|
|
|
|
tmp = query[0].replace( ts.regex.nondigit, '' ) || '';
|
|
range1 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table );
|
|
tmp = query[1].replace( ts.regex.nondigit, '' ) || '';
|
|
range2 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), 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;
|
|
}
|
|
if ( ( parsed || c.parsers[ index ].type === 'numeric' ) && !isNaN( range1 ) && !isNaN( range2 ) ) {
|
|
result = data.cache;
|
|
} else {
|
|
tmp = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact;
|
|
result = ts.formatFloat( tmp, 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 ( tsfRegex.wildOrTest.test( data.iFilter ) ) {
|
|
var query = '' + ( tsf.parseFilter( c, data.iFilter, data ) || '' );
|
|
// look for an exact match with the 'or' unless the 'filter-match' class is found
|
|
if ( !tsfRegex.wildTest.test( query ) && data.nestedFilters ) {
|
|
query = data.isMatch ? query : '^(' + query + ')$';
|
|
}
|
|
// parsing the filter may not work properly when using wildcards =/
|
|
try {
|
|
return new RegExp(
|
|
query.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ),
|
|
c.widgetOptions.filter_ignoreCase ? 'i' : ''
|
|
)
|
|
.test( data.exact );
|
|
} catch ( error ) {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
// fuzzy text search; modified from https://github.com/mattyork/fuzzy ( MIT license )
|
|
fuzzy: function( c, data ) {
|
|
if ( tsfRegex.fuzzyTest.test( data.iFilter ) ) {
|
|
var indx,
|
|
patternIndx = 0,
|
|
len = data.iExact.length,
|
|
txt = data.iFilter.slice( 1 ),
|
|
pattern = tsf.parseFilter( c, txt, data ) || '';
|
|
for ( indx = 0; indx < len; indx++ ) {
|
|
if ( data.iExact[ indx ] === pattern[ patternIndx ] ) {
|
|
patternIndx += 1;
|
|
}
|
|
}
|
|
return patternIndx === pattern.length;
|
|
}
|
|
return null;
|
|
}
|
|
},
|
|
init: function( table ) {
|
|
// filter language options
|
|
ts.language = $.extend( true, {}, {
|
|
to : 'to',
|
|
or : 'or',
|
|
and : 'and'
|
|
}, ts.language );
|
|
|
|
var options, string, txt, $header, column, val, fxn, noSelect,
|
|
c = table.config,
|
|
wo = c.widgetOptions,
|
|
processStr = function(prefix, str, suffix) {
|
|
str = str.trim();
|
|
// don't include prefix/suffix if str is empty
|
|
return str === '' ? '' : (prefix || '') + str + (suffix || '');
|
|
};
|
|
c.$table.addClass( 'hasFilters' );
|
|
c.lastSearch = [];
|
|
|
|
// define timers so using clearTimeout won't cause an undefined error
|
|
wo.filter_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*=","]';
|
|
|
|
val = '\\{' + tsfRegex.query + '\\}';
|
|
$.extend( tsfRegex, {
|
|
child : new RegExp( c.cssChildRow ),
|
|
filtered : new RegExp( wo.filter_filteredRow ),
|
|
alreadyFiltered : new RegExp( '(\\s+(-' + processStr('|', ts.language.or) + processStr('|', ts.language.to) + ')\\s+)', 'i' ),
|
|
toTest : new RegExp( '\\s+(-' + processStr('|', ts.language.to) + ')\\s+', 'i' ),
|
|
toSplit : new RegExp( '(?:\\s+(?:-' + processStr('|', ts.language.to) + ')\\s+)', 'gi' ),
|
|
andTest : new RegExp( '\\s+(' + processStr('', ts.language.and, '|') + '&&)\\s+', 'i' ),
|
|
andSplit : new RegExp( '(?:\\s+(?:' + processStr('', ts.language.and, '|') + '&&)\\s+)', 'gi' ),
|
|
orTest : new RegExp( '(\\|' + processStr('|\\s+', ts.language.or, '\\s+') + ')', 'i' ),
|
|
orSplit : new RegExp( '(?:\\|' + processStr('|\\s+(?:', ts.language.or, ')\\s+') + ')', 'gi' ),
|
|
iQuery : new RegExp( val, 'i' ),
|
|
igQuery : new RegExp( val, 'ig' ),
|
|
operTest : /^[<>]=?/,
|
|
gtTest : />/,
|
|
gteTest : />=/,
|
|
ltTest : /</,
|
|
lteTest : /<=/,
|
|
notTest : /^\!/,
|
|
wildOrTest : /[\?\*\|]/,
|
|
wildTest : /\?\*/,
|
|
fuzzyTest : /^~/,
|
|
exactTest : /[=\"\|!]/
|
|
});
|
|
|
|
// don't build filter row if columnFilters is false or all columns are set to 'filter-false'
|
|
// see issue #156
|
|
val = c.$headers.filter( '.filter-false, .parser-false' ).length;
|
|
if ( wo.filter_columnFilters !== false && val !== c.$headers.length ) {
|
|
// build filter row
|
|
tsf.buildRow( table, c, wo );
|
|
}
|
|
|
|
txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset ' +
|
|
'filterAndSortReset filterResetSaved 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( '.' + tscss.filterRow ).toggleClass( wo.filter_filteredRow, val ); // fixes #450
|
|
if ( !/(search|filter)/.test( event.type ) ) {
|
|
event.stopPropagation();
|
|
tsf.buildDefault( table, true );
|
|
}
|
|
// Add filterAndSortReset - see #1361
|
|
if ( event.type === 'filterReset' || event.type === 'filterAndSortReset' ) {
|
|
c.$table.find( '.' + tscss.filter ).add( wo.filter_$externalFilters ).val( '' );
|
|
if ( event.type === 'filterAndSortReset' ) {
|
|
ts.sortReset( this.config, function() {
|
|
tsf.searching( table, [] );
|
|
});
|
|
} else {
|
|
tsf.searching( table, [] );
|
|
}
|
|
} else if ( event.type === 'filterResetSaved' ) {
|
|
ts.storage( table, 'tablesorter-filters', '' );
|
|
} else if ( event.type === 'filterEnd' ) {
|
|
tsf.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 = [];
|
|
// update filterFormatters after update (& small delay) - Fixes #1237
|
|
setTimeout(function() {
|
|
c.$table.triggerHandler( 'filterFomatterUpdate' );
|
|
}, 100);
|
|
}
|
|
// 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', [...] );
|
|
tsf.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.triggerHandler( 'filterReset' );
|
|
});
|
|
} else if ( $( wo.filter_reset ).length ) {
|
|
// reset is a jQuery selector, use event delegation
|
|
$( document )
|
|
.undelegate( wo.filter_reset, 'click' + c.namespace + 'filter' )
|
|
.delegate( wo.filter_reset, 'click' + c.namespace + 'filter', function() {
|
|
// trigger a reset event, so other functions ( filter_formatter ) know when to reset
|
|
c.$table.triggerHandler( '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 ) {
|
|
tsf.buildSelect( table, column );
|
|
} else if ( typeof fxn === 'object' && noSelect ) {
|
|
// add custom drop down list
|
|
for ( string in fxn ) {
|
|
if ( typeof string === 'string' ) {
|
|
options += options === '' ?
|
|
'<option value="">' +
|
|
( $header.data( 'placeholder' ) ||
|
|
$header.attr( 'data-placeholder' ) ||
|
|
wo.filter_placeholder.select ||
|
|
''
|
|
) +
|
|
'</option>' : '';
|
|
val = string;
|
|
txt = string;
|
|
if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) {
|
|
val = string.split( wo.filter_selectSourceSeparator );
|
|
txt = val[1];
|
|
val = val[0];
|
|
}
|
|
options += '<option ' +
|
|
( txt === val ? '' : 'data-function-name="' + string + '" ' ) +
|
|
'value="' + val + '">' + txt + '</option>';
|
|
}
|
|
}
|
|
c.$table
|
|
.find( 'thead' )
|
|
.find( 'select.' + tscss.filter + '[data-column="' + column + '"]' )
|
|
.append( options );
|
|
txt = wo.filter_selectSource;
|
|
fxn = typeof txt === 'function' ? true : ts.getColumnData( table, txt, column );
|
|
if ( fxn ) {
|
|
// updating so the extra options are appended
|
|
tsf.buildSelect( c.table, column, '', true, $header.hasClass( wo.filter_onlyAvail ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 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.
|
|
tsf.buildDefault( table, true );
|
|
|
|
tsf.bindSearch( table, c.$table.find( '.' + tscss.filter ), true );
|
|
if ( wo.filter_external ) {
|
|
tsf.bindSearch( table, wo.filter_external );
|
|
}
|
|
|
|
if ( wo.filter_hideFilters ) {
|
|
tsf.hideFilters( c );
|
|
}
|
|
|
|
// show processing icon
|
|
if ( c.showProcessing ) {
|
|
txt = 'filterStart filterEnd '.split( ' ' ).join( c.namespace + 'filter-sp ' );
|
|
c.$table
|
|
.unbind( txt.replace( ts.regex.spaces, ' ' ) )
|
|
.bind( txt, function( event, columns ) {
|
|
// only add processing to certain columns to all columns
|
|
$header = ( columns ) ?
|
|
c.$table
|
|
.find( '.' + tscss.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
|
|
txt = 'tablesorter-initialized pagerBeforeInitialized '.split( ' ' ).join( c.namespace + 'filter ' );
|
|
c.$table
|
|
.unbind( txt.replace( ts.regex.spaces, ' ' ) )
|
|
.bind( txt, function() {
|
|
tsf.completeInit( this );
|
|
});
|
|
// 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.triggerHandler( 'filterFomatterUpdate' );
|
|
setTimeout( function() {
|
|
tsf.filterInitComplete( c );
|
|
}, 100 );
|
|
} else if ( !wo.filter_initialized ) {
|
|
tsf.completeInit( table );
|
|
}
|
|
},
|
|
completeInit: function( table ) {
|
|
// redefine 'c' & 'wo' so they update properly inside this callback
|
|
var c = table.config,
|
|
wo = c.widgetOptions,
|
|
filters = tsf.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.triggerHandler( 'filterFomatterUpdate' );
|
|
// trigger init after setTimeout to prevent multiple filterStart/End/Init triggers
|
|
setTimeout( function() {
|
|
if ( !wo.filter_initialized ) {
|
|
tsf.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 ) {
|
|
// prevent error if $cell is undefined - see #1056
|
|
var $table = $cell && $cell.closest( 'table' );
|
|
var config = $table.length && $table[0].config,
|
|
wo = config && config.widgetOptions;
|
|
if ( wo && !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;
|
|
// update lastSearch - it gets cleared often
|
|
c.lastSearch = c.$table.data( 'lastSearch' );
|
|
c.$table.triggerHandler( 'filterInit', c );
|
|
tsf.findRows( c.table, c.lastSearch || [] );
|
|
if (ts.debug(c, 'filter')) {
|
|
console.log('Filter >> Widget initialized');
|
|
}
|
|
};
|
|
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 );
|
|
}
|
|
}
|
|
},
|
|
// encode or decode filters for storage; see #1026
|
|
processFilters: function( filters, encode ) {
|
|
var indx,
|
|
// fixes #1237; previously returning an encoded "filters" value
|
|
result = [],
|
|
mode = encode ? encodeURIComponent : decodeURIComponent,
|
|
len = filters.length;
|
|
for ( indx = 0; indx < len; indx++ ) {
|
|
if ( filters[ indx ] ) {
|
|
result[ indx ] = mode( filters[ indx ] );
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
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 = tsf.processFilters( 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, data, parsed ) {
|
|
return parsed || data.parsed[ data.index ] ?
|
|
c.parsers[ data.index ].format( filter, c.table, [], data.index ) :
|
|
filter;
|
|
},
|
|
buildRow: function( table, c, wo ) {
|
|
var $filter, col, column, $header, makeSelect, disabled, name, ffxn, tmp,
|
|
// c.columns defined in computeThIndexes()
|
|
cellFilter = wo.filter_cellFilter,
|
|
columns = c.columns,
|
|
arry = $.isArray( cellFilter ),
|
|
buildFilter = '<tr role="search" class="' + tscss.filterRow + ' ' + c.cssIgnoreRow + '">';
|
|
for ( column = 0; column < columns; column++ ) {
|
|
if ( c.$headerIndexed[ column ].length ) {
|
|
// account for entire column set with colspan. See #1047
|
|
tmp = c.$headerIndexed[ column ] && c.$headerIndexed[ column ][0].colSpan || 0;
|
|
if ( tmp > 1 ) {
|
|
buildFilter += '<td data-column="' + column + '-' + ( column + tmp - 1 ) + '" colspan="' + tmp + '"';
|
|
} else {
|
|
buildFilter += '<td data-column="' + column + '"';
|
|
}
|
|
if ( arry ) {
|
|
buildFilter += ( cellFilter[ column ] ? ' class="' + cellFilter[ column ] + '"' : '' );
|
|
} else {
|
|
buildFilter += ( cellFilter !== '' ? ' class="' + cellFilter + '"' : '' );
|
|
}
|
|
buildFilter += '></td>';
|
|
}
|
|
}
|
|
c.$filters = $( buildFilter += '</tr>' )
|
|
.appendTo( c.$table.children( 'thead' ).eq( 0 ) )
|
|
.children( '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 ];
|
|
if ( $header && $header.length ) {
|
|
// $filter = c.$filters.filter( '[data-column="' + column + '"]' );
|
|
$filter = tsf.getColumnElm( c, c.$filters, column );
|
|
ffxn = ts.getColumnData( table, wo.filter_functions, column );
|
|
makeSelect = ( 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 ( makeSelect ) {
|
|
buildFilter = $( '<select>' ).appendTo( $filter );
|
|
} else {
|
|
ffxn = ts.getColumnData( table, wo.filter_formatter, column );
|
|
if ( ffxn ) {
|
|
wo.filter_formatterCount++;
|
|
buildFilter = ffxn( $filter, column );
|
|
// no element returned, so lets go find it
|
|
if ( buildFilter && buildFilter.length === 0 ) {
|
|
buildFilter = $filter.children( 'input' );
|
|
}
|
|
// element not in DOM, so lets attach it
|
|
if ( buildFilter && ( buildFilter.parent().length === 0 ||
|
|
( buildFilter.parent().length && buildFilter.parent()[0] !== $filter[0] ) ) ) {
|
|
$filter.append( buildFilter );
|
|
}
|
|
} else {
|
|
buildFilter = $( '<input type="search">' ).appendTo( $filter );
|
|
}
|
|
if ( buildFilter ) {
|
|
tmp = $header.data( 'placeholder' ) ||
|
|
$header.attr( 'data-placeholder' ) ||
|
|
wo.filter_placeholder.search || '';
|
|
buildFilter.attr( 'placeholder', tmp );
|
|
}
|
|
}
|
|
if ( buildFilter ) {
|
|
// add filter class name
|
|
name = ( $.isArray( wo.filter_cssFilter ) ?
|
|
( typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '' ) :
|
|
wo.filter_cssFilter ) || '';
|
|
// copy data-column from table cell (it will include colspan)
|
|
buildFilter.addClass( tscss.filter + ' ' + name );
|
|
name = wo.filter_filterLabel;
|
|
tmp = name.match(/{{([^}]+?)}}/g);
|
|
if (!tmp) {
|
|
tmp = [ '{{label}}' ];
|
|
}
|
|
$.each(tmp, function(indx, attr) {
|
|
var regex = new RegExp(attr, 'g'),
|
|
data = $header.attr('data-' + attr.replace(/{{|}}/g, '')),
|
|
text = typeof data === 'undefined' ? $header.text() : data;
|
|
name = name.replace( regex, $.trim( text ) );
|
|
});
|
|
buildFilter.attr({
|
|
'data-column': $filter.attr( 'data-column' ),
|
|
'aria-label': name
|
|
});
|
|
if ( disabled ) {
|
|
buildFilter.attr( 'placeholder', '' ).addClass( tscss.filterDisabled )[0].disabled = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
bindSearch: function( table, $el, internal ) {
|
|
table = $( table )[0];
|
|
$el = $( $el ); // allow passing a selector string
|
|
if ( !$el.length ) { return; }
|
|
var tmp,
|
|
c = table.config,
|
|
wo = c.widgetOptions,
|
|
namespace = c.namespace + 'filter',
|
|
$ext = wo.filter_$externalFilters;
|
|
if ( internal !== true ) {
|
|
// save anyMatch element
|
|
tmp = wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector;
|
|
wo.filter_$anyMatch = $el.filter( tmp );
|
|
if ( $ext && $ext.length ) {
|
|
wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el );
|
|
} else {
|
|
wo.filter_$externalFilters = $el;
|
|
}
|
|
// update values ( external filters added after table initialization )
|
|
ts.setFilters( table, c.$table.data( 'lastSearch' ) || [], internal === false );
|
|
}
|
|
// unbind events
|
|
tmp = ( 'keypress keyup keydown search change input '.split( ' ' ).join( namespace + ' ' ) );
|
|
$el
|
|
// use data attribute instead of jQuery data since the head is cloned without including
|
|
// the data/binding
|
|
.attr( 'data-lastSearchTime', new Date().getTime() )
|
|
.unbind( tmp.replace( ts.regex.spaces, ' ' ) )
|
|
.bind( 'keydown' + namespace, function( event ) {
|
|
if ( event.which === tskeyCodes.escape && !table.config.widgetOptions.filter_resetOnEsc ) {
|
|
// prevent keypress event
|
|
return false;
|
|
}
|
|
})
|
|
.bind( 'keyup' + namespace, function( event ) {
|
|
wo = table.config.widgetOptions; // make sure "wo" isn't cached
|
|
var column = parseInt( $( this ).attr( 'data-column' ), 10 ),
|
|
liveSearch = typeof wo.filter_liveSearch === 'boolean' ? wo.filter_liveSearch :
|
|
ts.getColumnData( table, wo.filter_liveSearch, column );
|
|
if ( typeof liveSearch === 'undefined' ) {
|
|
liveSearch = wo.filter_liveSearch.fallback || false;
|
|
}
|
|
$( this ).attr( 'data-lastSearchTime', new Date().getTime() );
|
|
// emulate what webkit does.... escape clears the filter
|
|
if ( event.which === tskeyCodes.escape ) {
|
|
// make sure to restore the last value on escape
|
|
this.value = wo.filter_resetOnEsc ? '' : c.lastSearch[column];
|
|
// don't return if the search value is empty ( all rows need to be revealed )
|
|
} else if ( this.value !== '' && (
|
|
// liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace
|
|
( typeof liveSearch === 'number' && this.value.length < liveSearch ) ||
|
|
// let return & backspace continue on, but ignore arrows & non-valid characters
|
|
( event.which !== tskeyCodes.enter && event.which !== tskeyCodes.backSpace &&
|
|
( event.which < tskeyCodes.space || ( event.which >= tskeyCodes.left && event.which <= tskeyCodes.down ) ) ) ) ) {
|
|
return;
|
|
// live search
|
|
} else if ( liveSearch === false ) {
|
|
if ( this.value !== '' && event.which !== tskeyCodes.enter ) {
|
|
return;
|
|
}
|
|
}
|
|
// change event = no delay; last true flag tells getFilters to skip newest timed input
|
|
tsf.searching( table, true, true, column );
|
|
})
|
|
// include change for select - fixes #473
|
|
.bind( 'search change keypress input blur '.split( ' ' ).join( namespace + ' ' ), function( event ) {
|
|
// don't get cached data, in case data-column changes dynamically
|
|
var column = parseInt( $( this ).attr( 'data-column' ), 10 ),
|
|
eventType = event.type,
|
|
liveSearch = typeof wo.filter_liveSearch === 'boolean' ?
|
|
wo.filter_liveSearch :
|
|
ts.getColumnData( table, wo.filter_liveSearch, column );
|
|
if ( table.config.widgetOptions.filter_initialized &&
|
|
// immediate search if user presses enter
|
|
( event.which === tskeyCodes.enter ||
|
|
// immediate search if a "search" or "blur" is triggered on the input
|
|
( eventType === 'search' || eventType === 'blur' ) ||
|
|
// change & input events must be ignored if liveSearch !== true
|
|
( eventType === 'change' || eventType === 'input' ) &&
|
|
// prevent search if liveSearch is a number
|
|
( liveSearch === true || liveSearch !== true && event.target.nodeName !== 'INPUT' ) &&
|
|
// don't allow 'change' or 'input' event to process if the input value
|
|
// is the same - fixes #685
|
|
this.value !== c.lastSearch[column]
|
|
)
|
|
) {
|
|
event.preventDefault();
|
|
// init search with no delay
|
|
$( this ).attr( 'data-lastSearchTime', new Date().getTime() );
|
|
tsf.searching( table, eventType !== 'keypress' || event.which === tskeyCodes.enter, true, column );
|
|
}
|
|
});
|
|
},
|
|
searching: function( table, filter, skipFirst, column ) {
|
|
var liveSearch,
|
|
wo = table.config.widgetOptions;
|
|
if (typeof column === 'undefined') {
|
|
// no delay
|
|
liveSearch = false;
|
|
} else {
|
|
liveSearch = typeof wo.filter_liveSearch === 'boolean' ?
|
|
wo.filter_liveSearch :
|
|
// get column setting, or set to fallback value, or default to false
|
|
ts.getColumnData( table, wo.filter_liveSearch, column );
|
|
if ( typeof liveSearch === 'undefined' ) {
|
|
liveSearch = wo.filter_liveSearch.fallback || false;
|
|
}
|
|
}
|
|
clearTimeout( wo.filter_searchTimer );
|
|
if ( typeof filter === 'undefined' || filter === true ) {
|
|
// delay filtering
|
|
wo.filter_searchTimer = setTimeout( function() {
|
|
tsf.checkFilters( table, filter, skipFirst );
|
|
}, liveSearch ? wo.filter_searchDelay : 10 );
|
|
} else {
|
|
// skip delay
|
|
tsf.checkFilters( table, filter, skipFirst );
|
|
}
|
|
},
|
|
equalFilters: function (c, filter1, filter2) {
|
|
var indx,
|
|
f1 = [],
|
|
f2 = [],
|
|
len = c.columns + 1; // add one to include anyMatch filter
|
|
filter1 = $.isArray(filter1) ? filter1 : [];
|
|
filter2 = $.isArray(filter2) ? filter2 : [];
|
|
for (indx = 0; indx < len; indx++) {
|
|
f1[indx] = filter1[indx] || '';
|
|
f2[indx] = filter2[indx] || '';
|
|
}
|
|
return f1.join(',') === f2.join(',');
|
|
},
|
|
checkFilters: function( table, filter, skipFirst ) {
|
|
var c = table.config,
|
|
wo = c.widgetOptions,
|
|
filterArray = $.isArray( filter ),
|
|
filters = ( filterArray ) ? filter : ts.getFilters( table, true ),
|
|
currentFilters = filters || []; // current filter values
|
|
// prevent errors if delay init is set
|
|
if ( $.isEmptyObject( c.cache ) ) {
|
|
// update cache if delayInit set & pager has initialized ( after user initiates a search )
|
|
if ( c.delayInit && ( !c.pager || c.pager && c.pager.initialized ) ) {
|
|
ts.updateCache( c, function() {
|
|
tsf.checkFilters( table, false, skipFirst );
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
// add filter array back into inputs
|
|
if ( filterArray ) {
|
|
ts.setFilters( table, filters, false, skipFirst !== true );
|
|
if ( !wo.filter_initialized ) {
|
|
c.lastSearch = [];
|
|
c.lastCombinedFilter = '';
|
|
}
|
|
}
|
|
if ( wo.filter_hideFilters ) {
|
|
// show/hide filter row as needed
|
|
c.$table
|
|
.find( '.' + tscss.filterRow )
|
|
.triggerHandler( tsf.hideFiltersCheck( c ) ? 'mouseleave' : 'mouseenter' );
|
|
}
|
|
// return if the last search is the same; but filter === false when updating the search
|
|
// see example-widget-filter.html filter toggle buttons
|
|
if ( tsf.equalFilters(c, c.lastSearch, currentFilters) ) {
|
|
if ( filter !== false ) {
|
|
return;
|
|
} else {
|
|
// force filter refresh
|
|
c.lastCombinedFilter = '';
|
|
c.lastSearch = [];
|
|
}
|
|
}
|
|
// define filter inside it is false
|
|
filters = filters || [];
|
|
// convert filters to strings - see #1070
|
|
filters = Array.prototype.map ?
|
|
filters.map( String ) :
|
|
// for IE8 & older browsers - maybe not the best method
|
|
filters.join( '\ufffd' ).split( '\ufffd' );
|
|
|
|
if ( wo.filter_initialized ) {
|
|
c.$table.triggerHandler( 'filterStart', [ filters ] );
|
|
}
|
|
if ( c.showProcessing ) {
|
|
// give it time for the processing icon to kick in
|
|
setTimeout( function() {
|
|
tsf.findRows( table, filters, currentFilters );
|
|
return false;
|
|
}, 30 );
|
|
} else {
|
|
tsf.findRows( table, filters, currentFilters );
|
|
return false;
|
|
}
|
|
},
|
|
hideFiltersCheck: function( c ) {
|
|
if (typeof c.widgetOptions.filter_hideFilters === 'function') {
|
|
var val = c.widgetOptions.filter_hideFilters( c );
|
|
if (typeof val === 'boolean') {
|
|
return val;
|
|
}
|
|
}
|
|
return ts.getFilters( c.$table ).join( '' ) === '';
|
|
},
|
|
hideFilters: function( c, $table ) {
|
|
var timer;
|
|
( $table || c.$table )
|
|
.find( '.' + tscss.filterRow )
|
|
.addClass( tscss.filterRowHide )
|
|
.bind( 'mouseenter mouseleave', function( e ) {
|
|
// save event object - http://bugs.jquery.com/ticket/12140
|
|
var event = e,
|
|
$row = $( this );
|
|
clearTimeout( timer );
|
|
timer = setTimeout( function() {
|
|
if ( /enter|over/.test( event.type ) ) {
|
|
$row.removeClass( tscss.filterRowHide );
|
|
} else {
|
|
// don't hide if input has focus
|
|
// $( ':focus' ) needs jQuery 1.6+
|
|
if ( $( document.activeElement ).closest( 'tr' )[0] !== $row[0] ) {
|
|
// don't hide row if any filter has a value
|
|
$row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) );
|
|
}
|
|
}
|
|
}, 200 );
|
|
})
|
|
.find( 'input, select' ).bind( 'focus blur', function( e ) {
|
|
var event = e,
|
|
$row = $( this ).closest( 'tr' );
|
|
clearTimeout( timer );
|
|
timer = setTimeout( function() {
|
|
clearTimeout( timer );
|
|
// don't hide row if any filter has a value
|
|
$row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) && event.type !== 'focus' );
|
|
}, 200 );
|
|
});
|
|
},
|
|
defaultFilter: function( filter, mask ) {
|
|
if ( filter === '' ) { return filter; }
|
|
var regex = tsfRegex.iQuery,
|
|
maskLen = mask.match( tsfRegex.igQuery ).length,
|
|
query = maskLen > 1 ? $.trim( filter ).split( /\s/ ) : [ $.trim( filter ) ],
|
|
len = query.length - 1,
|
|
indx = 0,
|
|
val = mask;
|
|
if ( len < 1 && maskLen > 1 ) {
|
|
// only one 'word' in query but mask has >1 slots
|
|
query[1] = query[0];
|
|
}
|
|
// replace all {query} with query words...
|
|
// if query = 'Bob', then convert mask from '!{query}' to '!Bob'
|
|
// if query = 'Bob Joe Frank', then convert mask '{q} OR {q}' to 'Bob OR Joe OR Frank'
|
|
while ( regex.test( val ) ) {
|
|
val = val.replace( regex, query[indx++] || '' );
|
|
if ( regex.test( val ) && indx < len && ( query[indx] || '' ) !== '' ) {
|
|
val = mask.replace( regex, val );
|
|
}
|
|
}
|
|
return val;
|
|
},
|
|
getLatestSearch: function( $input ) {
|
|
if ( $input ) {
|
|
return $input.sort( function( a, b ) {
|
|
return $( b ).attr( 'data-lastSearchTime' ) - $( a ).attr( 'data-lastSearchTime' );
|
|
});
|
|
}
|
|
return $input || $();
|
|
},
|
|
findRange: function( c, val, ignoreRanges ) {
|
|
// look for multiple columns '1-3,4-6,8' in data-column
|
|
var temp, ranges, range, start, end, singles, i, indx, len,
|
|
columns = [];
|
|
if ( /^[0-9]+$/.test( val ) ) {
|
|
// always return an array
|
|
return [ parseInt( val, 10 ) ];
|
|
}
|
|
// process column range
|
|
if ( !ignoreRanges && /-/.test( val ) ) {
|
|
ranges = val.match( /(\d+)\s*-\s*(\d+)/g );
|
|
len = ranges ? ranges.length : 0;
|
|
for ( indx = 0; indx < len; indx++ ) {
|
|
range = ranges[indx].split( /\s*-\s*/ );
|
|
start = parseInt( range[0], 10 ) || 0;
|
|
end = parseInt( range[1], 10 ) || ( c.columns - 1 );
|
|
if ( start > end ) {
|
|
temp = start; start = end; end = temp; // swap
|
|
}
|
|
if ( end >= c.columns ) {
|
|
end = c.columns - 1;
|
|
}
|
|
for ( ; start <= end; start++ ) {
|
|
columns[ columns.length ] = start;
|
|
}
|
|
// remove processed range from val
|
|
val = val.replace( ranges[ indx ], '' );
|
|
}
|
|
}
|
|
// process single columns
|
|
if ( !ignoreRanges && /,/.test( val ) ) {
|
|
singles = val.split( /\s*,\s*/ );
|
|
len = singles.length;
|
|
for ( i = 0; i < len; i++ ) {
|
|
if ( singles[ i ] !== '' ) {
|
|
indx = parseInt( singles[ i ], 10 );
|
|
if ( indx < c.columns ) {
|
|
columns[ columns.length ] = indx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// return all columns
|
|
if ( !columns.length ) {
|
|
for ( indx = 0; indx < c.columns; indx++ ) {
|
|
columns[ columns.length ] = indx;
|
|
}
|
|
}
|
|
return columns;
|
|
},
|
|
getColumnElm: function( c, $elements, column ) {
|
|
// data-column may contain multiple columns '1-3,5-6,8'
|
|
// replaces: c.$filters.filter( '[data-column="' + column + '"]' );
|
|
return $elements.filter( function() {
|
|
var cols = tsf.findRange( c, $( this ).attr( 'data-column' ) );
|
|
return $.inArray( column, cols ) > -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,
|
|
matches = null;
|
|
for ( ffxn in tsf.types ) {
|
|
if ( $.inArray( ffxn, vars.excludeMatch ) < 0 && matches === null ) {
|
|
matches = tsf.types[ffxn]( c, data, vars );
|
|
if ( matches !== null ) {
|
|
data.matchedOn = ffxn;
|
|
filterMatched = matches;
|
|
}
|
|
}
|
|
}
|
|
return filterMatched;
|
|
},
|
|
matchType: function( c, columnIndex ) {
|
|
var isMatch,
|
|
wo = c.widgetOptions,
|
|
$el = c.$headerIndexed[ columnIndex ];
|
|
// filter-exact > filter-match > filter_matchType for type
|
|
if ( $el.hasClass( 'filter-exact' ) ) {
|
|
isMatch = false;
|
|
} else if ( $el.hasClass( 'filter-match' ) ) {
|
|
isMatch = true;
|
|
} else {
|
|
// filter-select is not applied when filter_functions are used, so look for a select
|
|
if ( wo.filter_columnFilters ) {
|
|
$el = c.$filters
|
|
.find( '.' + tscss.filter )
|
|
.add( wo.filter_$externalFilters )
|
|
.filter( '[data-column="' + columnIndex + '"]' );
|
|
} else if ( wo.filter_$externalFilters ) {
|
|
$el = wo.filter_$externalFilters.filter( '[data-column="' + columnIndex + '"]' );
|
|
}
|
|
isMatch = $el.length ?
|
|
c.widgetOptions.filter_matchType[ ( $el[ 0 ].nodeName || '' ).toLowerCase() ] === 'match' :
|
|
// default to exact, if no inputs found
|
|
false;
|
|
}
|
|
return isMatch;
|
|
},
|
|
processRow: function( c, data, vars ) {
|
|
var result, filterMatched,
|
|
fxn, ffxn, txt,
|
|
wo = c.widgetOptions,
|
|
showRow = true,
|
|
hasAnyMatchInput = wo.filter_$anyMatch && wo.filter_$anyMatch.length,
|
|
|
|
// if wo.filter_$anyMatch data-column attribute is changed dynamically
|
|
// we don't want to do an "anyMatch" search on one column using data
|
|
// for the entire row - see #998
|
|
columnIndex = wo.filter_$anyMatch && wo.filter_$anyMatch.length ?
|
|
// look for multiple columns '1-3,4-6,8'
|
|
tsf.multipleColumns( c, wo.filter_$anyMatch ) :
|
|
[];
|
|
data.$cells = data.$row.children();
|
|
data.matchedOn = null;
|
|
if ( data.anyMatchFlag && columnIndex.length > 1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) {
|
|
data.anyMatch = true;
|
|
data.isMatch = true;
|
|
data.rowArray = data.$cells.map( function( i ) {
|
|
if ( $.inArray( i, columnIndex ) > -1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) {
|
|
if ( data.parsed[ i ] ) {
|
|
txt = data.cacheArray[ i ];
|
|
} else {
|
|
txt = data.rawArray[ i ];
|
|
txt = $.trim( wo.filter_ignoreCase ? txt.toLowerCase() : txt );
|
|
if ( c.sortLocaleCompare ) {
|
|
txt = ts.replaceAccents( txt );
|
|
}
|
|
}
|
|
return txt;
|
|
}
|
|
}).get();
|
|
data.filter = data.anyMatchFilter;
|
|
data.iFilter = data.iAnyMatchFilter;
|
|
data.exact = data.rowArray.join( ' ' );
|
|
data.iExact = wo.filter_ignoreCase ? data.exact.toLowerCase() : data.exact;
|
|
data.cache = data.cacheArray.slice( 0, -1 ).join( ' ' );
|
|
vars.excludeMatch = vars.noAnyMatch;
|
|
filterMatched = tsf.processTypes( c, data, vars );
|
|
if ( filterMatched !== null ) {
|
|
showRow = filterMatched;
|
|
} else {
|
|
if ( wo.filter_startsWith ) {
|
|
showRow = false;
|
|
// data.rowArray may not contain all columns
|
|
columnIndex = Math.min( c.columns, data.rowArray.length );
|
|
while ( !showRow && columnIndex > 0 ) {
|
|
columnIndex--;
|
|
showRow = showRow || data.rowArray[ columnIndex ].indexOf( data.iFilter ) === 0;
|
|
}
|
|
} else {
|
|
showRow = ( data.iExact + data.childRowText ).indexOf( data.iFilter ) >= 0;
|
|
}
|
|
}
|
|
data.anyMatch = false;
|
|
// no other filters to process
|
|
if ( data.filters.join( '' ) === data.filter ) {
|
|
return showRow;
|
|
}
|
|
}
|
|
|
|
for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) {
|
|
data.filter = data.filters[ columnIndex ];
|
|
data.index = columnIndex;
|
|
|
|
// filter types to exclude, per column
|
|
vars.excludeMatch = vars.excludeFilter[ columnIndex ];
|
|
|
|
// ignore if filter is empty or disabled
|
|
if ( data.filter ) {
|
|
data.cache = data.cacheArray[ columnIndex ];
|
|
result = data.parsed[ columnIndex ] ? data.cache : data.rawArray[ columnIndex ] || '';
|
|
data.exact = c.sortLocaleCompare ? ts.replaceAccents( result ) : result; // issue #405
|
|
data.iExact = !tsfRegex.type.test( typeof data.exact ) && wo.filter_ignoreCase ?
|
|
data.exact.toLowerCase() : data.exact;
|
|
data.isMatch = tsf.matchType( c, columnIndex );
|
|
|
|
result = showRow; // if showRow is true, show that row
|
|
|
|
// in case select filter option has a different value vs text 'a - z|A through Z'
|
|
ffxn = wo.filter_columnFilters ?
|
|
c.$filters.add( wo.filter_$externalFilters )
|
|
.filter( '[data-column="' + columnIndex + '"]' )
|
|
.find( 'select option:selected' )
|
|
.attr( 'data-function-name' ) || '' : '';
|
|
// replace accents - see #357
|
|
if ( c.sortLocaleCompare ) {
|
|
data.filter = ts.replaceAccents( data.filter );
|
|
}
|
|
|
|
// replace column specific default filters - see #1088
|
|
if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultColFilter[ columnIndex ] ) ) {
|
|
data.filter = tsf.defaultFilter( data.filter, vars.defaultColFilter[ columnIndex ] );
|
|
}
|
|
|
|
// data.iFilter = case insensitive ( if wo.filter_ignoreCase is true ),
|
|
// data.filter = case sensitive
|
|
data.iFilter = wo.filter_ignoreCase ? ( data.filter || '' ).toLowerCase() : data.filter;
|
|
fxn = vars.functions[ columnIndex ];
|
|
filterMatched = null;
|
|
if ( fxn ) {
|
|
if ( typeof fxn === 'function' ) {
|
|
// filter callback( exact cell content, parser normalized content,
|
|
// filter input value, column index, jQuery row object )
|
|
filterMatched = fxn( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data );
|
|
} else if ( typeof fxn[ ffxn || data.filter ] === 'function' ) {
|
|
// selector option function
|
|
txt = ffxn || data.filter;
|
|
filterMatched =
|
|
fxn[ txt ]( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data );
|
|
}
|
|
}
|
|
if ( filterMatched === null ) {
|
|
// cycle through the different filters
|
|
// filters return a boolean or null if nothing matches
|
|
filterMatched = tsf.processTypes( c, data, vars );
|
|
// select with exact match; ignore "and" or "or" within the text; fixes #1486
|
|
txt = fxn === true && (data.matchedOn === 'and' || data.matchedOn === 'or');
|
|
if ( filterMatched !== null && !txt) {
|
|
result = filterMatched;
|
|
// Look for match, and add child row data for matching
|
|
} else {
|
|
// check fxn (filter-select in header) after filter types are checked
|
|
// without this, the filter + jQuery UI selectmenu demo was breaking
|
|
if ( fxn === true ) {
|
|
// default selector uses exact match unless 'filter-match' class is found
|
|
result = data.isMatch ?
|
|
// data.iExact may be a number
|
|
( '' + data.iExact ).search( data.iFilter ) >= 0 :
|
|
data.filter === data.exact;
|
|
} else {
|
|
txt = ( data.iExact + data.childRowText ).indexOf( tsf.parseFilter( c, data.iFilter, data ) );
|
|
result = ( ( !wo.filter_startsWith && txt >= 0 ) || ( wo.filter_startsWith && txt === 0 ) );
|
|
}
|
|
}
|
|
} else {
|
|
result = filterMatched;
|
|
}
|
|
showRow = ( result ) ? showRow : false;
|
|
}
|
|
}
|
|
return showRow;
|
|
},
|
|
findRows: function( table, filters, currentFilters ) {
|
|
if (
|
|
tsf.equalFilters(table.config, table.config.lastSearch, currentFilters) ||
|
|
!table.config.widgetOptions.filter_initialized
|
|
) {
|
|
return;
|
|
}
|
|
var len, norm_rows, rowData, $rows, $row, rowIndex, tbodyIndex, $tbody, columnIndex,
|
|
isChild, childRow, lastSearch, showRow, showParent, time, val, indx,
|
|
notFiltered, searchFiltered, query, injected, res, id, txt,
|
|
storedFilters = $.extend( [], filters ),
|
|
c = table.config,
|
|
wo = c.widgetOptions,
|
|
debug = ts.debug(c, 'filter'),
|
|
// data object passed to filters; anyMatch is a flag for the filters
|
|
data = {
|
|
anyMatch: false,
|
|
filters: filters,
|
|
// regex filter type cache
|
|
filter_regexCache : []
|
|
},
|
|
vars = {
|
|
// anyMatch really screws up with these types of filters
|
|
noAnyMatch: [ 'range', 'operators' ],
|
|
// cache filter variables that use ts.getColumnData in the main loop
|
|
functions : [],
|
|
excludeFilter : [],
|
|
defaultColFilter : [],
|
|
defaultAnyFilter : ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || ''
|
|
};
|
|
// parse columns after formatter, in case the class is added at that point
|
|
data.parsed = [];
|
|
for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) {
|
|
data.parsed[ columnIndex ] = wo.filter_useParsedData ||
|
|
// parser has a "parsed" parameter
|
|
( c.parsers && c.parsers[ columnIndex ] && c.parsers[ columnIndex ].parsed ||
|
|
// getData may not return 'parsed' if other 'filter-' class names exist
|
|
// ( e.g. <th class="filter-select filter-parsed"> )
|
|
ts.getData && ts.getData( c.$headerIndexed[ columnIndex ],
|
|
ts.getColumnData( table, c.headers, columnIndex ), 'filter' ) === 'parsed' ||
|
|
c.$headerIndexed[ columnIndex ].hasClass( 'filter-parsed' ) );
|
|
|
|
vars.functions[ columnIndex ] =
|
|
ts.getColumnData( table, wo.filter_functions, columnIndex ) ||
|
|
c.$headerIndexed[ columnIndex ].hasClass( 'filter-select' );
|
|
vars.defaultColFilter[ columnIndex ] =
|
|
ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || '';
|
|
vars.excludeFilter[ columnIndex ] =
|
|
( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split( /\s+/ );
|
|
}
|
|
|
|
if ( debug ) {
|
|
console.log( 'Filter >> Starting filter widget search', filters );
|
|
time = new Date();
|
|
}
|
|
// filtered rows count
|
|
c.filteredRows = 0;
|
|
c.totalRows = 0;
|
|
currentFilters = ( storedFilters || [] );
|
|
|
|
for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) {
|
|
$tbody = ts.processTbody( table, c.$tbodies.eq( tbodyIndex ), true );
|
|
// skip child rows & widget added ( removable ) rows - fixes #448 thanks to @hempel!
|
|
// $rows = $tbody.children( 'tr' ).not( c.selectorRemove );
|
|
columnIndex = c.columns;
|
|
// convert stored rows into a jQuery object
|
|
norm_rows = c.cache[ tbodyIndex ].normalized;
|
|
$rows = $( $.map( norm_rows, function( el ) {
|
|
return el[ columnIndex ].$row.get();
|
|
}) );
|
|
|
|
if ( currentFilters.join('') === '' || wo.filter_serversideFiltering ) {
|
|
$rows
|
|
.removeClass( wo.filter_filteredRow )
|
|
.not( '.' + c.cssChildRow )
|
|
.css( 'display', '' );
|
|
} else {
|
|
// filter out child rows
|
|
$rows = $rows.not( '.' + c.cssChildRow );
|
|
len = $rows.length;
|
|
|
|
if ( ( wo.filter_$anyMatch && wo.filter_$anyMatch.length ) ||
|
|
typeof filters[c.columns] !== 'undefined' ) {
|
|
data.anyMatchFlag = true;
|
|
data.anyMatchFilter = '' + (
|
|
filters[ c.columns ] ||
|
|
wo.filter_$anyMatch && tsf.getLatestSearch( wo.filter_$anyMatch ).val() ||
|
|
''
|
|
);
|
|
if ( wo.filter_columnAnyMatch ) {
|
|
// specific columns search
|
|
query = data.anyMatchFilter.split( tsfRegex.andSplit );
|
|
injected = false;
|
|
for ( indx = 0; indx < query.length; indx++ ) {
|
|
res = query[ indx ].split( ':' );
|
|
if ( res.length > 1 ) {
|
|
// make the column a one-based index ( non-developers start counting from one :P )
|
|
if ( isNaN( res[0] ) ) {
|
|
$.each( c.headerContent, function( i, txt ) {
|
|
// multiple matches are possible
|
|
if ( txt.toLowerCase().indexOf( res[0] ) > -1 ) {
|
|
id = i;
|
|
filters[ id ] = res[1];
|
|
}
|
|
});
|
|
} else {
|
|
id = parseInt( res[0], 10 ) - 1;
|
|
}
|
|
if ( id >= 0 && id < c.columns ) { // if id is an integer
|
|
filters[ id ] = res[1];
|
|
query.splice( indx, 1 );
|
|
indx--;
|
|
injected = true;
|
|
}
|
|
}
|
|
}
|
|
if ( injected ) {
|
|
data.anyMatchFilter = query.join( ' && ' );
|
|
}
|
|
}
|
|
}
|
|
|
|
// optimize searching only through already filtered rows - see #313
|
|
searchFiltered = wo.filter_searchFiltered;
|
|
lastSearch = c.lastSearch || c.$table.data( 'lastSearch' ) || [];
|
|
if ( searchFiltered ) {
|
|
// cycle through all filters; include last ( columnIndex + 1 = match any column ). Fixes #669
|
|
for ( indx = 0; indx < columnIndex + 1; 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...
|
|
searchFiltered = searchFiltered && lastSearch.length &&
|
|
// there are no changes from beginning of filter
|
|
val.indexOf( lastSearch[indx] || '' ) === 0 &&
|
|
// if there is NOT a logical 'or', or range ( 'to' or '-' ) in the string
|
|
!tsfRegex.alreadyFiltered.test( val ) &&
|
|
// if we are not doing exact matches, using '|' ( logical or ) or not '!'
|
|
!tsfRegex.exactTest.test( val ) &&
|
|
// don't search only filtered if the value is negative
|
|
// ( '> -10' => '> -100' will ignore hidden rows )
|
|
!( tsfRegex.isNeg1.test( val ) || tsfRegex.isNeg2.test( val ) ) &&
|
|
// if filtering using a select without a 'filter-match' class ( exact match ) - fixes #593
|
|
!( val !== '' && c.$filters && c.$filters.filter( '[data-column="' + indx + '"]' ).find( 'select' ).length &&
|
|
!tsf.matchType( c, indx ) );
|
|
}
|
|
}
|
|
notFiltered = $rows.not( '.' + wo.filter_filteredRow ).length;
|
|
// can't search when all rows are hidden - this happens when looking for exact matches
|
|
if ( searchFiltered && notFiltered === 0 ) { searchFiltered = false; }
|
|
if ( debug ) {
|
|
console.log( 'Filter >> Searching through ' +
|
|
( searchFiltered && notFiltered < len ? notFiltered : 'all' ) + ' rows' );
|
|
}
|
|
if ( data.anyMatchFlag ) {
|
|
if ( c.sortLocaleCompare ) {
|
|
// replace accents
|
|
data.anyMatchFilter = ts.replaceAccents( data.anyMatchFilter );
|
|
}
|
|
if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultAnyFilter ) ) {
|
|
data.anyMatchFilter = tsf.defaultFilter( data.anyMatchFilter, vars.defaultAnyFilter );
|
|
// clear search filtered flag because default filters are not saved to the last search
|
|
searchFiltered = false;
|
|
}
|
|
// make iAnyMatchFilter lowercase unless both filter widget & core ignoreCase options are true
|
|
// when c.ignoreCase is true, the cache contains all lower case data
|
|
data.iAnyMatchFilter = !( wo.filter_ignoreCase && c.ignoreCase ) ?
|
|
data.anyMatchFilter :
|
|
data.anyMatchFilter.toLowerCase();
|
|
}
|
|
|
|
// loop through the rows
|
|
for ( rowIndex = 0; rowIndex < len; rowIndex++ ) {
|
|
|
|
txt = $rows[ rowIndex ].className;
|
|
// the first row can never be a child row
|
|
isChild = rowIndex && tsfRegex.child.test( txt );
|
|
// skip child rows & already filtered rows
|
|
if ( isChild || ( searchFiltered && tsfRegex.filtered.test( txt ) ) ) {
|
|
continue;
|
|
}
|
|
|
|
data.$row = $rows.eq( rowIndex );
|
|
data.rowIndex = rowIndex;
|
|
data.cacheArray = norm_rows[ rowIndex ];
|
|
rowData = data.cacheArray[ c.columns ];
|
|
data.rawArray = rowData.raw;
|
|
data.childRowText = '';
|
|
|
|
if ( !wo.filter_childByColumn ) {
|
|
txt = '';
|
|
// child row cached text
|
|
childRow = rowData.child;
|
|
// so, if 'table.config.widgetOptions.filter_childRows' is true and there is
|
|
// a match anywhere in the child row, then it will make the row visible
|
|
// checked here so the option can be changed dynamically
|
|
for ( indx = 0; indx < childRow.length; indx++ ) {
|
|
txt += ' ' + childRow[indx].join( ' ' ) || '';
|
|
}
|
|
data.childRowText = wo.filter_childRows ?
|
|
( wo.filter_ignoreCase ? txt.toLowerCase() : txt ) :
|
|
'';
|
|
}
|
|
|
|
showRow = false;
|
|
showParent = tsf.processRow( c, data, vars );
|
|
$row = rowData.$row;
|
|
|
|
// don't pass reference to val
|
|
val = showParent ? true : false;
|
|
childRow = rowData.$row.filter( ':gt(0)' );
|
|
if ( wo.filter_childRows && childRow.length ) {
|
|
if ( wo.filter_childByColumn ) {
|
|
if ( !wo.filter_childWithSibs ) {
|
|
// hide all child rows
|
|
childRow.addClass( wo.filter_filteredRow );
|
|
// if only showing resulting child row, only include parent
|
|
$row = $row.eq( 0 );
|
|
}
|
|
// cycle through each child row
|
|
for ( indx = 0; indx < childRow.length; indx++ ) {
|
|
data.$row = childRow.eq( indx );
|
|
data.cacheArray = rowData.child[ indx ];
|
|
data.rawArray = data.cacheArray;
|
|
val = tsf.processRow( c, data, vars );
|
|
// use OR comparison on child rows
|
|
showRow = showRow || val;
|
|
if ( !wo.filter_childWithSibs && val ) {
|
|
childRow.eq( indx ).removeClass( wo.filter_filteredRow );
|
|
}
|
|
}
|
|
}
|
|
// keep parent row match even if no child matches... see #1020
|
|
showRow = showRow || showParent;
|
|
} else {
|
|
showRow = val;
|
|
}
|
|
$row
|
|
.toggleClass( wo.filter_filteredRow, !showRow )[0]
|
|
.display = showRow ? '' : 'none';
|
|
}
|
|
}
|
|
c.filteredRows += $rows.not( '.' + wo.filter_filteredRow ).length;
|
|
c.totalRows += $rows.length;
|
|
ts.processTbody( table, $tbody, false );
|
|
}
|
|
// lastCombinedFilter is no longer used internally
|
|
c.lastCombinedFilter = storedFilters.join(''); // save last search
|
|
// don't save 'filters' directly since it may have altered ( AnyMatch column searches )
|
|
c.lastSearch = storedFilters;
|
|
c.$table.data( 'lastSearch', storedFilters );
|
|
if ( wo.filter_saveFilters && ts.storage ) {
|
|
ts.storage( table, 'tablesorter-filters', tsf.processFilters( storedFilters, true ) );
|
|
}
|
|
if ( debug ) {
|
|
console.log( 'Filter >> Completed search' + ts.benchmark(time) );
|
|
}
|
|
if ( wo.filter_initialized ) {
|
|
c.$table.triggerHandler( 'filterBeforeEnd', c );
|
|
c.$table.triggerHandler( 'filterEnd', c );
|
|
}
|
|
setTimeout( function() {
|
|
ts.applyWidget( c.table ); // make sure zebra widget is applied
|
|
}, 0 );
|
|
},
|
|
getOptionSource: function( table, column, onlyAvail ) {
|
|
table = $( table )[0];
|
|
var c = table.config,
|
|
wo = c.widgetOptions,
|
|
arry = false,
|
|
source = wo.filter_selectSource,
|
|
last = c.$table.data( 'lastSearch' ) || [],
|
|
fxn = typeof source === 'function' ? true : ts.getColumnData( table, source, column );
|
|
|
|
if ( onlyAvail && last[column] !== '' ) {
|
|
onlyAvail = false;
|
|
}
|
|
|
|
// filter select source option
|
|
if ( fxn === true ) {
|
|
// OVERALL source
|
|
arry = source( table, column, onlyAvail );
|
|
} else if ( fxn instanceof $ || ( $.type( fxn ) === 'string' && fxn.indexOf( '</option>' ) >= 0 ) ) {
|
|
// selectSource is a jQuery object or string of options
|
|
return fxn;
|
|
} else if ( $.isArray( fxn ) ) {
|
|
arry = fxn;
|
|
} else if ( $.type( source ) === 'object' && fxn ) {
|
|
// custom select source function for a SPECIFIC COLUMN
|
|
arry = fxn( table, column, onlyAvail );
|
|
// abort - updating the selects from an external method
|
|
if (arry === null) {
|
|
return null;
|
|
}
|
|
}
|
|
if ( arry === false ) {
|
|
// fall back to original method
|
|
arry = tsf.getOptions( table, column, onlyAvail );
|
|
}
|
|
|
|
return tsf.processOptions( table, column, arry );
|
|
|
|
},
|
|
processOptions: function( table, column, arry ) {
|
|
if ( !$.isArray( arry ) ) {
|
|
return false;
|
|
}
|
|
table = $( table )[0];
|
|
var cts, txt, indx, len, parsedTxt, str,
|
|
c = table.config,
|
|
validColumn = typeof column !== 'undefined' && column !== null && column >= 0 && column < c.columns,
|
|
direction = validColumn ? c.$headerIndexed[ column ].hasClass( 'filter-select-sort-desc' ) : false,
|
|
parsed = [];
|
|
// get unique elements and sort the list
|
|
// if $.tablesorter.sortText exists ( not in the original tablesorter ),
|
|
// then natural sort the list otherwise use a basic sort
|
|
arry = $.grep( arry, function( value, indx ) {
|
|
if ( value.text ) {
|
|
return true;
|
|
}
|
|
return $.inArray( value, arry ) === indx;
|
|
});
|
|
if ( validColumn && c.$headerIndexed[ column ].hasClass( 'filter-select-nosort' ) ) {
|
|
// unsorted select options
|
|
return arry;
|
|
} else {
|
|
len = arry.length;
|
|
// parse select option values
|
|
for ( indx = 0; indx < len; indx++ ) {
|
|
txt = arry[ indx ];
|
|
// check for object
|
|
str = txt.text ? txt.text : txt;
|
|
// sortNatural breaks if you don't pass it strings
|
|
parsedTxt = ( validColumn && c.parsers && c.parsers.length &&
|
|
c.parsers[ column ].format( str, table, [], column ) || str ).toString();
|
|
parsedTxt = c.widgetOptions.filter_ignoreCase ? parsedTxt.toLowerCase() : parsedTxt;
|
|
// parse array data using set column parser; this DOES NOT pass the original
|
|
// table cell to the parser format function
|
|
if ( txt.text ) {
|
|
txt.parsed = parsedTxt;
|
|
parsed[ parsed.length ] = txt;
|
|
} else {
|
|
parsed[ parsed.length ] = {
|
|
text : txt,
|
|
// check parser length - fixes #934
|
|
parsed : parsedTxt
|
|
};
|
|
}
|
|
}
|
|
// sort parsed select options
|
|
cts = c.textSorter || '';
|
|
parsed.sort( function( a, b ) {
|
|
var x = direction ? b.parsed : a.parsed,
|
|
y = direction ? a.parsed : b.parsed;
|
|
if ( validColumn && typeof cts === 'function' ) {
|
|
// custom OVERALL text sorter
|
|
return cts( x, y, true, column, table );
|
|
} else if ( validColumn && typeof cts === 'object' && cts.hasOwnProperty( column ) ) {
|
|
// custom text sorter for a SPECIFIC COLUMN
|
|
return cts[column]( x, y, true, column, table );
|
|
} else if ( ts.sortNatural ) {
|
|
// fall back to natural sort
|
|
return ts.sortNatural( x, y );
|
|
}
|
|
// using an older version! do a basic sort
|
|
return true;
|
|
});
|
|
// rebuild arry from sorted parsed data
|
|
arry = [];
|
|
len = parsed.length;
|
|
for ( indx = 0; indx < len; indx++ ) {
|
|
arry[ arry.length ] = parsed[indx];
|
|
}
|
|
return arry;
|
|
}
|
|
},
|
|
getOptions: function( table, column, onlyAvail ) {
|
|
table = $( table )[0];
|
|
var rowIndex, tbodyIndex, len, row, cache, indx, child, childLen,
|
|
c = table.config,
|
|
wo = c.widgetOptions,
|
|
arry = [];
|
|
for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) {
|
|
cache = c.cache[tbodyIndex];
|
|
len = c.cache[tbodyIndex].normalized.length;
|
|
// loop through the rows
|
|
for ( rowIndex = 0; rowIndex < len; rowIndex++ ) {
|
|
// get cached row from cache.row ( old ) or row data object
|
|
// ( new; last item in normalized array )
|
|
row = cache.row ?
|
|
cache.row[ rowIndex ] :
|
|
cache.normalized[ rowIndex ][ c.columns ].$row[0];
|
|
// check if has class filtered
|
|
if ( onlyAvail && row.className.match( wo.filter_filteredRow ) ) {
|
|
continue;
|
|
}
|
|
// get non-normalized cell content
|
|
if ( wo.filter_useParsedData ||
|
|
c.parsers[column].parsed ||
|
|
c.$headerIndexed[column].hasClass( 'filter-parsed' ) ) {
|
|
arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ column ];
|
|
// child row parsed data
|
|
if ( wo.filter_childRows && wo.filter_childByColumn ) {
|
|
childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length - 1;
|
|
for ( indx = 0; indx < childLen; indx++ ) {
|
|
arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ c.columns ].child[ indx ][ column ];
|
|
}
|
|
}
|
|
} else {
|
|
// get raw cached data instead of content directly from the cells
|
|
arry[ arry.length ] = cache.normalized[ rowIndex ][ c.columns ].raw[ column ];
|
|
// child row unparsed data
|
|
if ( wo.filter_childRows && wo.filter_childByColumn ) {
|
|
childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length;
|
|
for ( indx = 1; indx < childLen; indx++ ) {
|
|
child = cache.normalized[ rowIndex ][ c.columns ].$row.eq( indx ).children().eq( column );
|
|
arry[ arry.length ] = '' + ts.getElementText( c, child, column );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return arry;
|
|
},
|
|
buildSelect: function( table, column, arry, updating, onlyAvail ) {
|
|
table = $( table )[0];
|
|
column = parseInt( column, 10 );
|
|
if ( !table.config.cache || $.isEmptyObject( table.config.cache ) ) {
|
|
return;
|
|
}
|
|
|
|
var indx, val, txt, t, $filters, $filter, option,
|
|
c = table.config,
|
|
wo = c.widgetOptions,
|
|
node = c.$headerIndexed[ column ],
|
|
// t.data( 'placeholder' ) won't work in jQuery older than 1.4.3
|
|
options = '<option value="">' +
|
|
( node.data( 'placeholder' ) ||
|
|
node.attr( 'data-placeholder' ) ||
|
|
wo.filter_placeholder.select || ''
|
|
) + '</option>',
|
|
// Get curent filter value
|
|
currentValue = c.$table
|
|
.find( 'thead' )
|
|
.find( 'select.' + tscss.filter + '[data-column="' + column + '"]' )
|
|
.val();
|
|
|
|
// nothing included in arry ( external source ), so get the options from
|
|
// filter_selectSource or column data
|
|
if ( typeof arry === 'undefined' || arry === '' ) {
|
|
arry = tsf.getOptionSource( table, column, onlyAvail );
|
|
// abort, selects are updated by an external method
|
|
if (arry === null) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ( $.isArray( arry ) ) {
|
|
// build option list
|
|
for ( indx = 0; indx < arry.length; indx++ ) {
|
|
option = arry[ indx ];
|
|
if ( option.text ) {
|
|
// OBJECT!! add data-function-name in case the value is set in filter_functions
|
|
option['data-function-name'] = typeof option.value === 'undefined' ? option.text : option.value;
|
|
|
|
// support jQuery < v1.8, otherwise the below code could be shortened to
|
|
// options += $( '<option>', option )[ 0 ].outerHTML;
|
|
options += '<option';
|
|
for ( val in option ) {
|
|
if ( option.hasOwnProperty( val ) && val !== 'text' ) {
|
|
options += ' ' + val + '="' + option[ val ].replace( tsfRegex.quote, '"' ) + '"';
|
|
}
|
|
}
|
|
if ( !option.value ) {
|
|
options += ' value="' + option.text.replace( tsfRegex.quote, '"' ) + '"';
|
|
}
|
|
options += '>' + option.text.replace( tsfRegex.quote, '"' ) + '</option>';
|
|
// above code is needed in jQuery < v1.8
|
|
|
|
// make sure we don't turn an object into a string (objects without a "text" property)
|
|
} else if ( '' + option !== '[object Object]' ) {
|
|
txt = option = ( '' + option ).replace( tsfRegex.quote, '"' );
|
|
val = txt;
|
|
// allow including a symbol in the selectSource array
|
|
// 'a-z|A through Z' so that 'a-z' becomes the option value
|
|
// and 'A through Z' becomes the option text
|
|
if ( txt.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) {
|
|
t = txt.split( wo.filter_selectSourceSeparator );
|
|
val = t[0];
|
|
txt = t[1];
|
|
}
|
|
// replace quotes - fixes #242 & ignore empty strings
|
|
// see http://stackoverflow.com/q/14990971/145346
|
|
options += option !== '' ?
|
|
'<option ' +
|
|
( val === txt ? '' : 'data-function-name="' + option + '" ' ) +
|
|
'value="' + val + '">' + txt +
|
|
'</option>' : '';
|
|
}
|
|
}
|
|
// clear arry so it doesn't get appended twice
|
|
arry = [];
|
|
}
|
|
|
|
// update all selects in the same column ( clone thead in sticky headers &
|
|
// any external selects ) - fixes 473
|
|
$filters = ( c.$filters ? c.$filters : c.$table.children( 'thead' ) )
|
|
.find( '.' + tscss.filter );
|
|
if ( wo.filter_$externalFilters ) {
|
|
$filters = $filters && $filters.length ?
|
|
$filters.add( wo.filter_$externalFilters ) :
|
|
wo.filter_$externalFilters;
|
|
}
|
|
$filter = $filters.filter( 'select[data-column="' + column + '"]' );
|
|
|
|
// make sure there is a select there!
|
|
if ( $filter.length ) {
|
|
$filter[ updating ? 'html' : 'append' ]( options );
|
|
if ( !$.isArray( arry ) ) {
|
|
// append options if arry is provided externally as a string or jQuery object
|
|
// options ( default value ) was already added
|
|
$filter.append( arry ).val( currentValue );
|
|
}
|
|
$filter.val( currentValue );
|
|
}
|
|
},
|
|
buildDefault: function( table, updating ) {
|
|
var columnIndex, $header, noSelect,
|
|
c = table.config,
|
|
wo = c.widgetOptions,
|
|
columns = c.columns;
|
|
// build default select dropdown
|
|
for ( columnIndex = 0; columnIndex < columns; columnIndex++ ) {
|
|
$header = c.$headerIndexed[columnIndex];
|
|
noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) );
|
|
// look for the filter-select class; build/update it if found
|
|
if ( ( $header.hasClass( 'filter-select' ) ||
|
|
ts.getColumnData( table, wo.filter_functions, columnIndex ) === true ) && noSelect ) {
|
|
tsf.buildSelect( table, columnIndex, '', updating, $header.hasClass( wo.filter_onlyAvail ) );
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// filter regex variable
|
|
tsfRegex = tsf.regex;
|
|
|
|
ts.getFilters = function( table, getRaw, setFilters, skipFirst ) {
|
|
var i, $filters, $column, cols,
|
|
filters = [],
|
|
c = table ? $( table )[0].config : '',
|
|
wo = c ? c.widgetOptions : '';
|
|
if ( ( getRaw !== true && wo && !wo.filter_columnFilters ) ||
|
|
// setFilters called, but last search is exactly the same as the current
|
|
// fixes issue #733 & #903 where calling update causes the input values to reset
|
|
( $.isArray(setFilters) && tsf.equalFilters(c, setFilters, c.lastSearch) )
|
|
) {
|
|
return $( table ).data( 'lastSearch' ) || [];
|
|
}
|
|
if ( c ) {
|
|
if ( c.$filters ) {
|
|
$filters = c.$filters.find( '.' + tscss.filter );
|
|
}
|
|
if ( wo.filter_$externalFilters ) {
|
|
$filters = $filters && $filters.length ?
|
|
$filters.add( wo.filter_$externalFilters ) :
|
|
wo.filter_$externalFilters;
|
|
}
|
|
if ( $filters && $filters.length ) {
|
|
filters = setFilters || [];
|
|
for ( i = 0; i < c.columns + 1; i++ ) {
|
|
cols = ( i === c.columns ?
|
|
// 'all' columns can now include a range or set of columms ( data-column='0-2,4,6-7' )
|
|
wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector :
|
|
'[data-column="' + i + '"]' );
|
|
$column = $filters.filter( cols );
|
|
if ( $column.length ) {
|
|
// move the latest search to the first slot in the array
|
|
$column = tsf.getLatestSearch( $column );
|
|
if ( $.isArray( setFilters ) ) {
|
|
// skip first ( latest input ) to maintain cursor position while typing
|
|
if ( skipFirst && $column.length > 1 ) {
|
|
$column = $column.slice( 1 );
|
|
}
|
|
if ( i === c.columns ) {
|
|
// prevent data-column='all' from filling data-column='0,1' ( etc )
|
|
cols = $column.filter( wo.filter_anyColumnSelector );
|
|
$column = cols.length ? cols : $column;
|
|
}
|
|
$column
|
|
.val( setFilters[ i ] )
|
|
// must include a namespace here; but not c.namespace + 'filter'?
|
|
.trigger( 'change' + c.namespace );
|
|
} else {
|
|
filters[i] = $column.val() || '';
|
|
// don't change the first... it will move the cursor
|
|
if ( i === c.columns ) {
|
|
// don't update range columns from 'all' setting
|
|
$column
|
|
.slice( 1 )
|
|
.filter( '[data-column*="' + $column.attr( 'data-column' ) + '"]' )
|
|
.val( filters[ i ] );
|
|
} else {
|
|
$column
|
|
.slice( 1 )
|
|
.val( filters[ i ] );
|
|
}
|
|
}
|
|
// save any match input dynamically
|
|
if ( i === c.columns && $column.length ) {
|
|
wo.filter_$anyMatch = $column;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return filters;
|
|
};
|
|
|
|
ts.setFilters = function( table, filter, apply, skipFirst ) {
|
|
var c = table ? $( table )[0].config : '',
|
|
valid = ts.getFilters( table, true, filter, skipFirst );
|
|
// default apply to "true"
|
|
if ( typeof apply === 'undefined' ) {
|
|
apply = true;
|
|
}
|
|
if ( c && apply ) {
|
|
// ensure new set filters are applied, even if the search is the same
|
|
c.lastCombinedFilter = null;
|
|
c.lastSearch = [];
|
|
tsf.searching( c.table, filter, skipFirst );
|
|
c.$table.triggerHandler( 'filterFomatterUpdate' );
|
|
}
|
|
return valid.length !== 0;
|
|
};
|
|
|
|
})( jQuery );
|
|
|
|
/*! Widget: stickyHeaders - updated 9/27/2017 (v2.29.0) *//*
|
|
* Requires tablesorter v2.8+ and jQuery 1.4.3+
|
|
* by Rob Garrison
|
|
*/
|
|
;(function ($, window) {
|
|
'use strict';
|
|
var ts = $.tablesorter || {};
|
|
|
|
$.extend(ts.css, {
|
|
sticky : 'tablesorter-stickyHeader', // stickyHeader
|
|
stickyVis : 'tablesorter-sticky-visible',
|
|
stickyHide: 'tablesorter-sticky-hidden',
|
|
stickyWrap: 'tablesorter-sticky-wrapper'
|
|
});
|
|
|
|
// Add a resize event to table headers
|
|
ts.addHeaderResizeEvent = function(table, disable, settings) {
|
|
table = $(table)[0]; // make sure we're using a dom element
|
|
if ( !table.config ) { return; }
|
|
var defaults = {
|
|
timer : 250
|
|
},
|
|
options = $.extend({}, defaults, settings),
|
|
c = table.config,
|
|
wo = c.widgetOptions,
|
|
checkSizes = function( triggerEvent ) {
|
|
var index, headers, $header, sizes, width, height,
|
|
len = c.$headers.length;
|
|
wo.resize_flag = true;
|
|
headers = [];
|
|
for ( index = 0; index < len; index++ ) {
|
|
$header = c.$headers.eq( index );
|
|
sizes = $header.data( 'savedSizes' ) || [ 0, 0 ]; // fixes #394
|
|
width = $header[0].offsetWidth;
|
|
height = $header[0].offsetHeight;
|
|
if ( width !== sizes[0] || height !== sizes[1] ) {
|
|
$header.data( 'savedSizes', [ width, height ] );
|
|
headers.push( $header[0] );
|
|
}
|
|
}
|
|
if ( headers.length && triggerEvent !== false ) {
|
|
c.$table.triggerHandler( 'resize', [ headers ] );
|
|
}
|
|
wo.resize_flag = false;
|
|
};
|
|
clearInterval(wo.resize_timer);
|
|
if (disable) {
|
|
wo.resize_flag = false;
|
|
return false;
|
|
}
|
|
checkSizes( false );
|
|
wo.resize_timer = setInterval(function() {
|
|
if (wo.resize_flag) { return; }
|
|
checkSizes();
|
|
}, options.timer);
|
|
};
|
|
|
|
function getStickyOffset(c, wo) {
|
|
var $el = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : [];
|
|
return $el.length ?
|
|
$el.height() || 0 :
|
|
parseInt(wo.stickyHeaders_offset, 10) || 0;
|
|
}
|
|
|
|
// Sticky headers based on this awesome article:
|
|
// http://css-tricks.com/13465-persistent-headers/
|
|
// and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech
|
|
// **************************
|
|
ts.addWidget({
|
|
id: 'stickyHeaders',
|
|
priority: 54, // sticky widget must be initialized after the filter & before pager widget!
|
|
options: {
|
|
stickyHeaders : '', // extra class name added to the sticky header row
|
|
stickyHeaders_appendTo : null, // jQuery selector or object to phycially attach the sticky headers
|
|
stickyHeaders_attachTo : null, // jQuery selector or object to attach scroll listener to (overridden by xScroll & yScroll settings)
|
|
stickyHeaders_xScroll : null, // jQuery selector or object to monitor horizontal scroll position (defaults: xScroll > attachTo > window)
|
|
stickyHeaders_yScroll : null, // jQuery selector or object to monitor vertical scroll position (defaults: yScroll > attachTo > window)
|
|
stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element
|
|
stickyHeaders_filteredToTop: true, // scroll table top into view after filtering
|
|
stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists
|
|
stickyHeaders_addResizeEvent : true, // trigger 'resize' event on headers
|
|
stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header
|
|
stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs
|
|
},
|
|
format: function(table, c, wo) {
|
|
// filter widget doesn't initialize on an empty table. Fixes #449
|
|
if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) {
|
|
return;
|
|
}
|
|
var index, len, $t,
|
|
$table = c.$table,
|
|
// add position: relative to attach element, hopefully it won't cause trouble.
|
|
$attach = $(wo.stickyHeaders_attachTo || wo.stickyHeaders_appendTo),
|
|
namespace = c.namespace + 'stickyheaders ',
|
|
// element to watch for the scroll event
|
|
$yScroll = $(wo.stickyHeaders_yScroll || wo.stickyHeaders_attachTo || window),
|
|
$xScroll = $(wo.stickyHeaders_xScroll || wo.stickyHeaders_attachTo || window),
|
|
$thead = $table.children('thead:first'),
|
|
$header = $thead.children('tr').not('.sticky-false').children(),
|
|
$tfoot = $table.children('tfoot'),
|
|
stickyOffset = getStickyOffset(c, wo),
|
|
// is this table nested? If so, find parent sticky header wrapper (div, not table)
|
|
$nestedSticky = $table.parent().closest('.' + ts.css.table).hasClass('hasStickyHeaders') ?
|
|
$table.parent().closest('table.tablesorter')[0].config.widgetOptions.$sticky.parent() : [],
|
|
nestedStickyTop = $nestedSticky.length ? $nestedSticky.height() : 0,
|
|
// clone table, then wrap to make sticky header
|
|
$stickyTable = wo.$sticky = $table.clone()
|
|
.addClass('containsStickyHeaders ' + ts.css.sticky + ' ' + wo.stickyHeaders + ' ' + c.namespace.slice(1) + '_extra_table' )
|
|
.wrap('<div class="' + ts.css.stickyWrap + '">'),
|
|
$stickyWrap = $stickyTable.parent()
|
|
.addClass(ts.css.stickyHide)
|
|
.css({
|
|
position : $attach.length ? 'absolute' : 'fixed',
|
|
padding : parseInt( $stickyTable.parent().parent().css('padding-left'), 10 ),
|
|
top : stickyOffset + nestedStickyTop,
|
|
left : 0,
|
|
visibility : 'hidden',
|
|
zIndex : wo.stickyHeaders_zIndex || 2
|
|
}),
|
|
$stickyThead = $stickyTable.children('thead:first'),
|
|
$stickyCells,
|
|
laststate = '',
|
|
setWidth = function($orig, $clone) {
|
|
var index, width, border, $cell, $this,
|
|
$cells = $orig.filter(':visible'),
|
|
len = $cells.length;
|
|
for ( index = 0; index < len; index++ ) {
|
|
$cell = $clone.filter(':visible').eq(index);
|
|
$this = $cells.eq(index);
|
|
// code from https://github.com/jmosbech/StickyTableHeaders
|
|
if ($this.css('box-sizing') === 'border-box') {
|
|
width = $this.outerWidth();
|
|
} else {
|
|
if ($cell.css('border-collapse') === 'collapse') {
|
|
if (window.getComputedStyle) {
|
|
width = parseFloat( window.getComputedStyle($this[0], null).width );
|
|
} else {
|
|
// ie8 only
|
|
border = parseFloat( $this.css('border-width') );
|
|
width = $this.outerWidth() - parseFloat( $this.css('padding-left') ) - parseFloat( $this.css('padding-right') ) - border;
|
|
}
|
|
} else {
|
|
width = $this.width();
|
|
}
|
|
}
|
|
$cell.css({
|
|
'width': width,
|
|
'min-width': width,
|
|
'max-width': width
|
|
});
|
|
}
|
|
},
|
|
getLeftPosition = function(yWindow) {
|
|
if (yWindow === false && $nestedSticky.length) {
|
|
return $table.position().left;
|
|
}
|
|
return $attach.length ?
|
|
parseInt($attach.css('padding-left'), 10) || 0 :
|
|
$table.offset().left - parseInt($table.css('margin-left'), 10) - $(window).scrollLeft();
|
|
},
|
|
resizeHeader = function() {
|
|
$stickyWrap.css({
|
|
left : getLeftPosition(),
|
|
width: $table.outerWidth()
|
|
});
|
|
setWidth( $table, $stickyTable );
|
|
setWidth( $header, $stickyCells );
|
|
},
|
|
scrollSticky = function( resizing ) {
|
|
if (!$table.is(':visible')) { return; } // fixes #278
|
|
// Detect nested tables - fixes #724
|
|
nestedStickyTop = $nestedSticky.length ? $nestedSticky.offset().top - $yScroll.scrollTop() + $nestedSticky.height() : 0;
|
|
var tmp,
|
|
offset = $table.offset(),
|
|
stickyOffset = getStickyOffset(c, wo),
|
|
yWindow = $.isWindow( $yScroll[0] ), // $.isWindow needs jQuery 1.4.3
|
|
yScroll = yWindow ?
|
|
$yScroll.scrollTop() :
|
|
// use parent sticky position if nested AND inside of a scrollable element - see #1512
|
|
$nestedSticky.length ? parseInt($nestedSticky[0].style.top, 10) : $yScroll.offset().top,
|
|
attachTop = $attach.length ? yScroll : $yScroll.scrollTop(),
|
|
captionHeight = wo.stickyHeaders_includeCaption ? 0 : $table.children( 'caption' ).height() || 0,
|
|
scrollTop = attachTop + stickyOffset + nestedStickyTop - captionHeight,
|
|
tableHeight = $table.height() - ($stickyWrap.height() + ($tfoot.height() || 0)) - captionHeight,
|
|
isVisible = ( scrollTop > offset.top ) && ( scrollTop < offset.top + tableHeight ) ? 'visible' : 'hidden',
|
|
state = isVisible === 'visible' ? ts.css.stickyVis : ts.css.stickyHide,
|
|
needsUpdating = !$stickyWrap.hasClass( state ),
|
|
cssSettings = { visibility : isVisible };
|
|
if ($attach.length) {
|
|
// attached sticky headers always need updating
|
|
needsUpdating = true;
|
|
cssSettings.top = yWindow ? scrollTop - $attach.offset().top : $attach.scrollTop();
|
|
}
|
|
// adjust when scrolling horizontally - fixes issue #143
|
|
tmp = getLeftPosition(yWindow);
|
|
if (tmp !== parseInt($stickyWrap.css('left'), 10)) {
|
|
needsUpdating = true;
|
|
cssSettings.left = tmp;
|
|
}
|
|
cssSettings.top = ( cssSettings.top || 0 ) +
|
|
// If nested AND inside of a scrollable element, only add parent sticky height
|
|
(!yWindow && $nestedSticky.length ? $nestedSticky.height() : stickyOffset + nestedStickyTop);
|
|
if (needsUpdating) {
|
|
$stickyWrap
|
|
.removeClass( ts.css.stickyVis + ' ' + ts.css.stickyHide )
|
|
.addClass( state )
|
|
.css(cssSettings);
|
|
}
|
|
if (isVisible !== laststate || resizing) {
|
|
// make sure the column widths match
|
|
resizeHeader();
|
|
laststate = isVisible;
|
|
}
|
|
};
|
|
// only add a position relative if a position isn't already defined
|
|
if ($attach.length && !$attach.css('position')) {
|
|
$attach.css('position', 'relative');
|
|
}
|
|
// fix clone ID, if it exists - fixes #271
|
|
if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; }
|
|
// clear out cloned table, except for sticky header
|
|
// include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing
|
|
$stickyTable.find('> thead:gt(0), tr.sticky-false').hide();
|
|
$stickyTable.find('> tbody, > tfoot').remove();
|
|
$stickyTable.find('caption').toggle(wo.stickyHeaders_includeCaption);
|
|
// issue #172 - find td/th in sticky header
|
|
$stickyCells = $stickyThead.children().children();
|
|
$stickyTable.css({ height:0, width:0, margin: 0 });
|
|
// remove resizable block
|
|
$stickyCells.find('.' + ts.css.resizer).remove();
|
|
// update sticky header class names to match real header after sorting
|
|
$table
|
|
.addClass('hasStickyHeaders')
|
|
.bind('pagerComplete' + namespace, function() {
|
|
resizeHeader();
|
|
});
|
|
|
|
ts.bindEvents(table, $stickyThead.children().children('.' + ts.css.header));
|
|
|
|
if (wo.stickyHeaders_appendTo) {
|
|
$(wo.stickyHeaders_appendTo).append( $stickyWrap );
|
|
} else {
|
|
// add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned.
|
|
$table.after( $stickyWrap );
|
|
}
|
|
|
|
// onRenderHeader is defined, we need to do something about it (fixes #641)
|
|
if (c.onRenderHeader) {
|
|
$t = $stickyThead.children('tr').children();
|
|
len = $t.length;
|
|
for ( index = 0; index < len; index++ ) {
|
|
// send second parameter
|
|
c.onRenderHeader.apply( $t.eq( index ), [ index, c, $stickyTable ] );
|
|
}
|
|
}
|
|
// make it sticky!
|
|
$xScroll.add($yScroll)
|
|
.unbind( ('scroll resize '.split(' ').join( namespace )).replace(/\s+/g, ' ') )
|
|
.bind('scroll resize '.split(' ').join( namespace ), function( event ) {
|
|
scrollSticky( event.type === 'resize' );
|
|
});
|
|
c.$table
|
|
.unbind('stickyHeadersUpdate' + namespace)
|
|
.bind('stickyHeadersUpdate' + namespace, function() {
|
|
scrollSticky( true );
|
|
});
|
|
|
|
if (wo.stickyHeaders_addResizeEvent) {
|
|
ts.addHeaderResizeEvent(table);
|
|
}
|
|
|
|
// look for filter widget
|
|
if ($table.hasClass('hasFilters') && wo.filter_columnFilters) {
|
|
// scroll table into view after filtering, if sticky header is active - #482
|
|
$table.bind('filterEnd' + namespace, function() {
|
|
// $(':focus') needs jQuery 1.6+
|
|
var $td = $(document.activeElement).closest('td'),
|
|
column = $td.parent().children().index($td);
|
|
// only scroll if sticky header is active
|
|
if ($stickyWrap.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) {
|
|
// scroll to original table (not sticky clone)
|
|
window.scrollTo(0, $table.position().top);
|
|
// give same input/select focus; check if c.$filters exists; fixes #594
|
|
if (column >= 0 && c.$filters) {
|
|
c.$filters.eq(column).find('a, select, input').filter(':visible').focus();
|
|
}
|
|
}
|
|
});
|
|
ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) );
|
|
// support hideFilters
|
|
if (wo.filter_hideFilters) {
|
|
ts.filter.hideFilters(c, $stickyTable);
|
|
}
|
|
}
|
|
|
|
// resize table (Firefox)
|
|
if (wo.stickyHeaders_addResizeEvent) {
|
|
$table.bind('resize' + c.namespace + 'stickyheaders', function() {
|
|
resizeHeader();
|
|
});
|
|
}
|
|
|
|
// make sure sticky is visible if page is partially scrolled
|
|
scrollSticky( true );
|
|
$table.triggerHandler('stickyHeadersInit');
|
|
|
|
},
|
|
remove: function(table, c, wo) {
|
|
var namespace = c.namespace + 'stickyheaders ';
|
|
c.$table
|
|
.removeClass('hasStickyHeaders')
|
|
.unbind( ('pagerComplete resize filterEnd stickyHeadersUpdate '.split(' ').join(namespace)).replace(/\s+/g, ' ') )
|
|
.next('.' + ts.css.stickyWrap).remove();
|
|
if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table
|
|
$(window)
|
|
.add(wo.stickyHeaders_xScroll)
|
|
.add(wo.stickyHeaders_yScroll)
|
|
.add(wo.stickyHeaders_attachTo)
|
|
.unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') );
|
|
ts.addHeaderResizeEvent(table, true);
|
|
}
|
|
});
|
|
|
|
})(jQuery, window);
|
|
|
|
/*! Widget: resizable - updated 2018-03-26 (v2.30.2) */
|
|
/*jshint browser:true, jquery:true, unused:false */
|
|
;(function ($, window) {
|
|
'use strict';
|
|
var ts = $.tablesorter || {};
|
|
|
|
$.extend(ts.css, {
|
|
resizableContainer : 'tablesorter-resizable-container',
|
|
resizableHandle : 'tablesorter-resizable-handle',
|
|
resizableNoSelect : 'tablesorter-disableSelection',
|
|
resizableStorage : 'tablesorter-resizable'
|
|
});
|
|
|
|
// Add extra scroller css
|
|
$(function() {
|
|
var s = '<style>' +
|
|
'body.' + ts.css.resizableNoSelect + ' { -ms-user-select: none; -moz-user-select: -moz-none;' +
|
|
'-khtml-user-select: none; -webkit-user-select: none; user-select: none; }' +
|
|
'.' + ts.css.resizableContainer + ' { position: relative; height: 1px; }' +
|
|
// make handle z-index > than stickyHeader z-index, so the handle stays above sticky header
|
|
'.' + ts.css.resizableHandle + ' { position: absolute; display: inline-block; width: 8px;' +
|
|
'top: 1px; cursor: ew-resize; z-index: 3; user-select: none; -moz-user-select: none; }' +
|
|
'</style>';
|
|
$('head').append(s);
|
|
});
|
|
|
|
ts.resizable = {
|
|
init : function( c, wo ) {
|
|
if ( c.$table.hasClass( 'hasResizable' ) ) { return; }
|
|
c.$table.addClass( 'hasResizable' );
|
|
|
|
var noResize, $header, column, storedSizes, tmp,
|
|
$table = c.$table,
|
|
$parent = $table.parent(),
|
|
marginTop = parseInt( $table.css( 'margin-top' ), 10 ),
|
|
|
|
// internal variables
|
|
vars = wo.resizable_vars = {
|
|
useStorage : ts.storage && wo.resizable !== false,
|
|
$wrap : $parent,
|
|
mouseXPosition : 0,
|
|
$target : null,
|
|
$next : null,
|
|
overflow : $parent.css('overflow') === 'auto' ||
|
|
$parent.css('overflow') === 'scroll' ||
|
|
$parent.css('overflow-x') === 'auto' ||
|
|
$parent.css('overflow-x') === 'scroll',
|
|
storedSizes : []
|
|
};
|
|
|
|
// set default widths
|
|
ts.resizableReset( c.table, true );
|
|
|
|
// now get measurements!
|
|
vars.tableWidth = $table.width();
|
|
// attempt to autodetect
|
|
vars.fullWidth = Math.abs( $parent.width() - vars.tableWidth ) < 20;
|
|
|
|
/*
|
|
// Hacky method to determine if table width is set to 'auto'
|
|
// http://stackoverflow.com/a/20892048/145346
|
|
if ( !vars.fullWidth ) {
|
|
tmp = $table.width();
|
|
$header = $table.wrap('<span>').parent(); // temp variable
|
|
storedSizes = parseInt( $table.css( 'margin-left' ), 10 ) || 0;
|
|
$table.css( 'margin-left', storedSizes + 50 );
|
|
vars.tableWidth = $header.width() > tmp ? 'auto' : tmp;
|
|
$table.css( 'margin-left', storedSizes ? storedSizes : '' );
|
|
$header = null;
|
|
$table.unwrap('<span>');
|
|
}
|
|
*/
|
|
|
|
if ( vars.useStorage && vars.overflow ) {
|
|
// save table width
|
|
ts.storage( c.table, 'tablesorter-table-original-css-width', vars.tableWidth );
|
|
tmp = ts.storage( c.table, 'tablesorter-table-resized-width' ) || 'auto';
|
|
ts.resizable.setWidth( $table, tmp, true );
|
|
}
|
|
wo.resizable_vars.storedSizes = storedSizes = ( vars.useStorage ?
|
|
ts.storage( c.table, ts.css.resizableStorage ) :
|
|
[] ) || [];
|
|
ts.resizable.setWidths( c, wo, storedSizes );
|
|
ts.resizable.updateStoredSizes( c, wo );
|
|
|
|
wo.$resizable_container = $( '<div class="' + ts.css.resizableContainer + '">' )
|
|
.css({ top : marginTop })
|
|
.insertBefore( $table );
|
|
// add container
|
|
for ( column = 0; column < c.columns; column++ ) {
|
|
$header = c.$headerIndexed[ column ];
|
|
tmp = ts.getColumnData( c.table, c.headers, column );
|
|
noResize = ts.getData( $header, tmp, 'resizable' ) === 'false';
|
|
if ( !noResize ) {
|
|
$( '<div class="' + ts.css.resizableHandle + '">' )
|
|
.appendTo( wo.$resizable_container )
|
|
.attr({
|
|
'data-column' : column,
|
|
'unselectable' : 'on'
|
|
})
|
|
.data( 'header', $header )
|
|
.bind( 'selectstart', false );
|
|
}
|
|
}
|
|
ts.resizable.bindings( c, wo );
|
|
},
|
|
|
|
updateStoredSizes : function( c, wo ) {
|
|
var column, $header,
|
|
len = c.columns,
|
|
vars = wo.resizable_vars;
|
|
vars.storedSizes = [];
|
|
for ( column = 0; column < len; column++ ) {
|
|
$header = c.$headerIndexed[ column ];
|
|
vars.storedSizes[ column ] = $header.is(':visible') ? $header.width() : 0;
|
|
}
|
|
},
|
|
|
|
setWidth : function( $el, width, overflow ) {
|
|
// overflow tables need min & max width set as well
|
|
$el.css({
|
|
'width' : width,
|
|
'min-width' : overflow ? width : '',
|
|
'max-width' : overflow ? width : ''
|
|
});
|
|
},
|
|
|
|
setWidths : function( c, wo, storedSizes ) {
|
|
var column, $temp,
|
|
vars = wo.resizable_vars,
|
|
$extra = $( c.namespace + '_extra_headers' ),
|
|
$col = c.$table.children( 'colgroup' ).children( 'col' );
|
|
storedSizes = storedSizes || vars.storedSizes || [];
|
|
// process only if table ID or url match
|
|
if ( storedSizes.length ) {
|
|
for ( column = 0; column < c.columns; column++ ) {
|
|
// set saved resizable widths
|
|
ts.resizable.setWidth( c.$headerIndexed[ column ], storedSizes[ column ], vars.overflow );
|
|
if ( $extra.length ) {
|
|
// stickyHeaders needs to modify min & max width as well
|
|
$temp = $extra.eq( column ).add( $col.eq( column ) );
|
|
ts.resizable.setWidth( $temp, storedSizes[ column ], vars.overflow );
|
|
}
|
|
}
|
|
$temp = $( c.namespace + '_extra_table' );
|
|
if ( $temp.length && !ts.hasWidget( c.table, 'scroller' ) ) {
|
|
ts.resizable.setWidth( $temp, c.$table.outerWidth(), vars.overflow );
|
|
}
|
|
}
|
|
},
|
|
|
|
setHandlePosition : function( c, wo ) {
|
|
var startPosition,
|
|
tableHeight = c.$table.height(),
|
|
$handles = wo.$resizable_container.children(),
|
|
handleCenter = Math.floor( $handles.width() / 2 );
|
|
|
|
if ( ts.hasWidget( c.table, 'scroller' ) ) {
|
|
tableHeight = 0;
|
|
c.$table.closest( '.' + ts.css.scrollerWrap ).children().each(function() {
|
|
var $this = $(this);
|
|
// center table has a max-height set
|
|
tableHeight += $this.filter('[style*="height"]').length ? $this.height() : $this.children('table').height();
|
|
});
|
|
}
|
|
|
|
if ( !wo.resizable_includeFooter && c.$table.children('tfoot').length ) {
|
|
tableHeight -= c.$table.children('tfoot').height();
|
|
}
|
|
// subtract out table left position from resizable handles. Fixes #864
|
|
// jQuery v3.3.0+ appears to include the start position with the $header.position().left; see #1544
|
|
startPosition = parseFloat($.fn.jquery) >= 3.3 ? 0 : c.$table.position().left;
|
|
$handles.each( function() {
|
|
var $this = $(this),
|
|
column = parseInt( $this.attr( 'data-column' ), 10 ),
|
|
columns = c.columns - 1,
|
|
$header = $this.data( 'header' );
|
|
if ( !$header ) { return; } // see #859
|
|
if (
|
|
!$header.is(':visible') ||
|
|
( !wo.resizable_addLastColumn && ts.resizable.checkVisibleColumns(c, column) )
|
|
) {
|
|
$this.hide();
|
|
} else if ( column < columns || column === columns && wo.resizable_addLastColumn ) {
|
|
$this.css({
|
|
display: 'inline-block',
|
|
height : tableHeight,
|
|
left : $header.position().left - startPosition + $header.outerWidth() - handleCenter
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
// Fixes #1485
|
|
checkVisibleColumns: function( c, column ) {
|
|
var i,
|
|
len = 0;
|
|
for ( i = column + 1; i < c.columns; i++ ) {
|
|
len += c.$headerIndexed[i].is( ':visible' ) ? 1 : 0;
|
|
}
|
|
return len === 0;
|
|
},
|
|
|
|
// prevent text selection while dragging resize bar
|
|
toggleTextSelection : function( c, wo, toggle ) {
|
|
var namespace = c.namespace + 'tsresize';
|
|
wo.resizable_vars.disabled = toggle;
|
|
$( 'body' ).toggleClass( ts.css.resizableNoSelect, toggle );
|
|
if ( toggle ) {
|
|
$( 'body' )
|
|
.attr( 'unselectable', 'on' )
|
|
.bind( 'selectstart' + namespace, false );
|
|
} else {
|
|
$( 'body' )
|
|
.removeAttr( 'unselectable' )
|
|
.unbind( 'selectstart' + namespace );
|
|
}
|
|
},
|
|
|
|
bindings : function( c, wo ) {
|
|
var namespace = c.namespace + 'tsresize';
|
|
wo.$resizable_container.children().bind( 'mousedown', function( event ) {
|
|
// save header cell and mouse position
|
|
var column,
|
|
vars = wo.resizable_vars,
|
|
$extras = $( c.namespace + '_extra_headers' ),
|
|
$header = $( event.target ).data( 'header' );
|
|
|
|
column = parseInt( $header.attr( 'data-column' ), 10 );
|
|
vars.$target = $header = $header.add( $extras.filter('[data-column="' + column + '"]') );
|
|
vars.target = column;
|
|
|
|
// if table is not as wide as it's parent, then resize the table
|
|
vars.$next = event.shiftKey || wo.resizable_targetLast ?
|
|
$header.parent().children().not( '.resizable-false' ).filter( ':last' ) :
|
|
$header.nextAll( ':not(.resizable-false)' ).eq( 0 );
|
|
|
|
column = parseInt( vars.$next.attr( 'data-column' ), 10 );
|
|
vars.$next = vars.$next.add( $extras.filter('[data-column="' + column + '"]') );
|
|
vars.next = column;
|
|
|
|
vars.mouseXPosition = event.pageX;
|
|
ts.resizable.updateStoredSizes( c, wo );
|
|
ts.resizable.toggleTextSelection(c, wo, true );
|
|
});
|
|
|
|
$( document )
|
|
.bind( 'mousemove' + namespace, function( event ) {
|
|
var vars = wo.resizable_vars;
|
|
// ignore mousemove if no mousedown
|
|
if ( !vars.disabled || vars.mouseXPosition === 0 || !vars.$target ) { return; }
|
|
if ( wo.resizable_throttle ) {
|
|
clearTimeout( vars.timer );
|
|
vars.timer = setTimeout( function() {
|
|
ts.resizable.mouseMove( c, wo, event );
|
|
}, isNaN( wo.resizable_throttle ) ? 5 : wo.resizable_throttle );
|
|
} else {
|
|
ts.resizable.mouseMove( c, wo, event );
|
|
}
|
|
})
|
|
.bind( 'mouseup' + namespace, function() {
|
|
if (!wo.resizable_vars.disabled) { return; }
|
|
ts.resizable.toggleTextSelection( c, wo, false );
|
|
ts.resizable.stopResize( c, wo );
|
|
ts.resizable.setHandlePosition( c, wo );
|
|
});
|
|
|
|
// resizeEnd event triggered by scroller widget
|
|
$( window ).bind( 'resize' + namespace + ' resizeEnd' + namespace, function() {
|
|
ts.resizable.setHandlePosition( c, wo );
|
|
});
|
|
|
|
// right click to reset columns to default widths
|
|
c.$table
|
|
.bind( 'columnUpdate pagerComplete resizableUpdate '.split( ' ' ).join( namespace + ' ' ), function() {
|
|
ts.resizable.setHandlePosition( c, wo );
|
|
})
|
|
.bind( 'resizableReset' + namespace, function() {
|
|
ts.resizableReset( c.table );
|
|
})
|
|
.find( 'thead:first' )
|
|
.add( $( c.namespace + '_extra_table' ).find( 'thead:first' ) )
|
|
.bind( 'contextmenu' + namespace, function() {
|
|
// $.isEmptyObject() needs jQuery 1.4+; allow right click if already reset
|
|
var allowClick = wo.resizable_vars.storedSizes.length === 0;
|
|
ts.resizableReset( c.table );
|
|
ts.resizable.setHandlePosition( c, wo );
|
|
wo.resizable_vars.storedSizes = [];
|
|
return allowClick;
|
|
});
|
|
|
|
},
|
|
|
|
mouseMove : function( c, wo, event ) {
|
|
if ( wo.resizable_vars.mouseXPosition === 0 || !wo.resizable_vars.$target ) { return; }
|
|
// resize columns
|
|
var column,
|
|
total = 0,
|
|
vars = wo.resizable_vars,
|
|
$next = vars.$next,
|
|
tar = vars.storedSizes[ vars.target ],
|
|
leftEdge = event.pageX - vars.mouseXPosition;
|
|
if ( vars.overflow ) {
|
|
if ( tar + leftEdge > 0 ) {
|
|
vars.storedSizes[ vars.target ] += leftEdge;
|
|
ts.resizable.setWidth( vars.$target, vars.storedSizes[ vars.target ], true );
|
|
// update the entire table width
|
|
for ( column = 0; column < c.columns; column++ ) {
|
|
total += vars.storedSizes[ column ];
|
|
}
|
|
ts.resizable.setWidth( c.$table.add( $( c.namespace + '_extra_table' ) ), total );
|
|
}
|
|
if ( !$next.length ) {
|
|
// if expanding right-most column, scroll the wrapper
|
|
vars.$wrap[0].scrollLeft = c.$table.width();
|
|
}
|
|
} else if ( vars.fullWidth ) {
|
|
vars.storedSizes[ vars.target ] += leftEdge;
|
|
vars.storedSizes[ vars.next ] -= leftEdge;
|
|
ts.resizable.setWidths( c, wo );
|
|
} else {
|
|
vars.storedSizes[ vars.target ] += leftEdge;
|
|
ts.resizable.setWidths( c, wo );
|
|
}
|
|
vars.mouseXPosition = event.pageX;
|
|
// dynamically update sticky header widths
|
|
c.$table.triggerHandler('stickyHeadersUpdate');
|
|
},
|
|
|
|
stopResize : function( c, wo ) {
|
|
var vars = wo.resizable_vars;
|
|
ts.resizable.updateStoredSizes( c, wo );
|
|
if ( vars.useStorage ) {
|
|
// save all column widths
|
|
ts.storage( c.table, ts.css.resizableStorage, vars.storedSizes );
|
|
ts.storage( c.table, 'tablesorter-table-resized-width', c.$table.width() );
|
|
}
|
|
vars.mouseXPosition = 0;
|
|
vars.$target = vars.$next = null;
|
|
// will update stickyHeaders, just in case, see #912
|
|
c.$table.triggerHandler('stickyHeadersUpdate');
|
|
c.$table.triggerHandler('resizableComplete');
|
|
}
|
|
};
|
|
|
|
// this widget saves the column widths if
|
|
// $.tablesorter.storage function is included
|
|
// **************************
|
|
ts.addWidget({
|
|
id: 'resizable',
|
|
priority: 40,
|
|
options: {
|
|
resizable : true, // save column widths to storage
|
|
resizable_addLastColumn : false,
|
|
resizable_includeFooter: true,
|
|
resizable_widths : [],
|
|
resizable_throttle : false, // set to true (5ms) or any number 0-10 range
|
|
resizable_targetLast : false
|
|
},
|
|
init: function(table, thisWidget, c, wo) {
|
|
ts.resizable.init( c, wo );
|
|
},
|
|
format: function( table, c, wo ) {
|
|
ts.resizable.setHandlePosition( c, wo );
|
|
},
|
|
remove: function( table, c, wo, refreshing ) {
|
|
if (wo.$resizable_container) {
|
|
var namespace = c.namespace + 'tsresize';
|
|
c.$table.add( $( c.namespace + '_extra_table' ) )
|
|
.removeClass('hasResizable')
|
|
.children( 'thead' )
|
|
.unbind( 'contextmenu' + namespace );
|
|
|
|
wo.$resizable_container.remove();
|
|
ts.resizable.toggleTextSelection( c, wo, false );
|
|
ts.resizableReset( table, refreshing );
|
|
$( document ).unbind( 'mousemove' + namespace + ' mouseup' + namespace );
|
|
}
|
|
}
|
|
});
|
|
|
|
ts.resizableReset = function( table, refreshing ) {
|
|
$( table ).each(function() {
|
|
var index, $t,
|
|
c = this.config,
|
|
wo = c && c.widgetOptions,
|
|
vars = wo.resizable_vars;
|
|
if ( table && c && c.$headerIndexed.length ) {
|
|
// restore the initial table width
|
|
if ( vars.overflow && vars.tableWidth ) {
|
|
ts.resizable.setWidth( c.$table, vars.tableWidth, true );
|
|
if ( vars.useStorage ) {
|
|
ts.storage( table, 'tablesorter-table-resized-width', vars.tableWidth );
|
|
}
|
|
}
|
|
for ( index = 0; index < c.columns; index++ ) {
|
|
$t = c.$headerIndexed[ index ];
|
|
if ( wo.resizable_widths && wo.resizable_widths[ index ] ) {
|
|
ts.resizable.setWidth( $t, wo.resizable_widths[ index ], vars.overflow );
|
|
} else if ( !$t.hasClass( 'resizable-false' ) ) {
|
|
// don't clear the width of any column that is not resizable
|
|
ts.resizable.setWidth( $t, '', vars.overflow );
|
|
}
|
|
}
|
|
|
|
// reset stickyHeader widths
|
|
c.$table.triggerHandler( 'stickyHeadersUpdate' );
|
|
if ( ts.storage && !refreshing ) {
|
|
ts.storage( this, ts.css.resizableStorage, [] );
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
})( jQuery, window );
|
|
|
|
/*! Widget: saveSort - updated 2018-03-19 (v2.30.1) *//*
|
|
* Requires tablesorter v2.16+
|
|
* by Rob Garrison
|
|
*/
|
|
;(function ($) {
|
|
'use strict';
|
|
var ts = $.tablesorter || {};
|
|
|
|
function getStoredSortList(c) {
|
|
var stored = ts.storage( c.table, 'tablesorter-savesort' );
|
|
return (stored && stored.hasOwnProperty('sortList') && $.isArray(stored.sortList)) ? stored.sortList : [];
|
|
}
|
|
|
|
function sortListChanged(c, sortList) {
|
|
return (sortList || getStoredSortList(c)).join(',') !== c.sortList.join(',');
|
|
}
|
|
|
|
// this widget saves the last sort only if the
|
|
// saveSort widget option is true AND the
|
|
// $.tablesorter.storage function is included
|
|
// **************************
|
|
ts.addWidget({
|
|
id: 'saveSort',
|
|
priority: 20,
|
|
options: {
|
|
saveSort : true
|
|
},
|
|
init: function(table, thisWidget, c, wo) {
|
|
// run widget format before all other widgets are applied to the table
|
|
thisWidget.format(table, c, wo, true);
|
|
},
|
|
format: function(table, c, wo, init) {
|
|
var time,
|
|
$table = c.$table,
|
|
saveSort = wo.saveSort !== false, // make saveSort active/inactive; default to true
|
|
sortList = { 'sortList' : c.sortList },
|
|
debug = ts.debug(c, 'saveSort');
|
|
if (debug) {
|
|
time = new Date();
|
|
}
|
|
if ($table.hasClass('hasSaveSort')) {
|
|
if (saveSort && table.hasInitialized && ts.storage && sortListChanged(c)) {
|
|
ts.storage( table, 'tablesorter-savesort', sortList );
|
|
if (debug) {
|
|
console.log('saveSort >> Saving last sort: ' + c.sortList + ts.benchmark(time));
|
|
}
|
|
}
|
|
} else {
|
|
// set table sort on initial run of the widget
|
|
$table.addClass('hasSaveSort');
|
|
sortList = '';
|
|
// get data
|
|
if (ts.storage) {
|
|
sortList = getStoredSortList(c);
|
|
if (debug) {
|
|
console.log('saveSort >> Last sort loaded: "' + sortList + '"' + ts.benchmark(time));
|
|
}
|
|
$table.bind('saveSortReset', function(event) {
|
|
event.stopPropagation();
|
|
ts.storage( table, 'tablesorter-savesort', '' );
|
|
});
|
|
}
|
|
// init is true when widget init is run, this will run this widget before all other widgets have initialized
|
|
// this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice.
|
|
if (init && sortList && sortList.length > 0) {
|
|
c.sortList = sortList;
|
|
} else if (table.hasInitialized && sortList && sortList.length > 0) {
|
|
// update sort change
|
|
if (sortListChanged(c, sortList)) {
|
|
ts.sortOn(c, sortList);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
remove: function(table, c) {
|
|
c.$table.removeClass('hasSaveSort');
|
|
// clear storage
|
|
if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); }
|
|
}
|
|
});
|
|
|
|
})(jQuery);
|
|
return jQuery.tablesorter;}));
|