PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.22
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.22
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / js / tablesorter / jquery.tablesorter.widgets.js

jquery.tablesorter.widgets.js in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 3.2.22, at js/tablesorter/jquery.tablesorter.widgets.js

3,178 lines 123.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /*! tablesorter (FORK) - updated 2023-01-27 (v2.31.3)*/
2 /* Includes widgets ( storage,uitheme,columns,filter,stickyHeaders,resizable,saveSort ) */
3 (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) {
4 /*! Widget: storage - updated 2018-03-18 (v2.30.0) */
5 /*global JSON:false */
6 ;(function ($, window, document) {
7 'use strict';
8
9 var ts = $.tablesorter || {};
10
11 // update defaults for validator; these values must be falsy!
12 $.extend(true, ts.defaults, {
13 fixedUrl: '',
14 widgetOptions: {
15 storage_fixedUrl: '',
16 storage_group: '',
17 storage_page: '',
18 storage_storageType: '',
19 storage_tableId: '',
20 storage_useSessionStorage: ''
21 }
22 });
23
24 // *** Store data in local storage, with a cookie fallback ***
25 /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json)
26 if you need it, then include https://github.com/douglascrockford/JSON-js
27
28 $.parseJSON is not available is jQuery versions older than 1.4.1, using older
29 versions will only allow storing information for one page at a time
30
31 // *** Save data (JSON format only) ***
32 // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid
33 var val = { "mywidget" : "data1" }; // valid JSON uses double quotes
34 // $.tablesorter.storage(table, key, val);
35 $.tablesorter.storage(table, 'tablesorter-mywidget', val);
36
37 // *** Get data: $.tablesorter.storage(table, key); ***
38 v = $.tablesorter.storage(table, 'tablesorter-mywidget');
39 // val may be empty, so also check for your data
40 val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : '';
41 alert(val); // 'data1' if saved, or '' if not
42 */
43 ts.storage = function(table, key, value, options) {
44 table = $(table)[0];
45 var cookieIndex, cookies, date,
46 hasStorage = false,
47 values = {},
48 c = table.config,
49 wo = c && c.widgetOptions,
50 debug = ts.debug(c, 'storage'),
51 storageType = (
52 ( options && options.storageType ) || ( wo && wo.storage_storageType )
53 ).toString().charAt(0).toLowerCase(),
54 // deprecating "useSessionStorage"; any storageType setting overrides it
55 session = storageType ? '' :
56 ( options && options.useSessionStorage ) || ( wo && wo.storage_useSessionStorage ),
57 $table = $(table),
58 // id from (1) options ID, (2) table 'data-table-group' attribute, (3) widgetOptions.storage_tableId,
59 // (4) table ID, then (5) table index
60 id = options && options.id ||
61 $table.attr( options && options.group || wo && wo.storage_group || 'data-table-group') ||
62 wo && wo.storage_tableId || table.id || $('.tablesorter').index( $table ),
63 // url from (1) options url, (2) table 'data-table-page' attribute, (3) widgetOptions.storage_fixedUrl,
64 // (4) table.config.fixedUrl (deprecated), then (5) window location path
65 url = options && options.url ||
66 $table.attr(options && options.page || wo && wo.storage_page || 'data-table-page') ||
67 wo && wo.storage_fixedUrl || c && c.fixedUrl || window.location.pathname;
68
69 // skip if using cookies
70 if (storageType !== 'c') {
71 storageType = (storageType === 's' || session) ? 'sessionStorage' : 'localStorage';
72 // https://gist.github.com/paulirish/5558557
73 if (storageType in window) {
74 try {
75 window[storageType].setItem('_tmptest', 'temp');
76 hasStorage = true;
77 window[storageType].removeItem('_tmptest');
78 } catch (error) {
79 console.warn( storageType + ' is not supported in this browser' );
80 }
81 }
82 }
83 if (debug) {
84 console.log('Storage >> Using', hasStorage ? storageType : 'cookies');
85 }
86 // *** get value ***
87 if (hasStorage) {
88 values = JSON.parse( window[storageType][key] || 'null' ) || {};
89 } else {
90 // old browser, using cookies
91 cookies = document.cookie.split(/[;\s|=]/);
92 // add one to get from the key to the value
93 cookieIndex = $.inArray(key, cookies) + 1;
94 values = (cookieIndex !== 0) ? JSON.parse(cookies[cookieIndex] || 'null') || {} : {};
95 }
96
97 // allow value to be an empty string too
98 if (typeof value !== 'undefined' && window.JSON && JSON.hasOwnProperty('stringify')) {
99 // add unique identifiers = url pathname > table ID/index on page > data
100 if (!values[url]) {
101 values[url] = {};
102 }
103 values[url][id] = value;
104 // *** set value ***
105 if (hasStorage) {
106 window[storageType][key] = JSON.stringify(values);
107 } else {
108 date = new Date();
109 date.setTime(date.getTime() + (31536e+6)); // 365 days
110 document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g, '\"') + '; expires=' + date.toGMTString() + '; path=/';
111 }
112 } else {
113 return values && values[url] ? values[url][id] : '';
114 }
115 };
116
117 })(jQuery, window, document);
118
119 /*! Widget: uitheme - updated 2018-03-18 (v2.30.0) */
120 ;(function ($) {
121 'use strict';
122 var ts = $.tablesorter || {};
123
124 ts.themes = {
125 'bootstrap' : {
126 table : 'table table-bordered table-striped',
127 caption : 'caption',
128 // header class names
129 header : 'bootstrap-header', // give the header a gradient background (theme.bootstrap_2.css)
130 sortNone : '',
131 sortAsc : '',
132 sortDesc : '',
133 active : '', // applied when column is sorted
134 hover : '', // custom css required - a defined bootstrap style may not override other classes
135 // icon class names
136 icons : '', // add 'bootstrap-icon-white' to make them white; this icon class is added to the <i> in the header
137 iconSortNone : 'bootstrap-icon-unsorted', // class name added to icon when column is not sorted
138 iconSortAsc : 'glyphicon glyphicon-chevron-up', // class name added to icon when column has ascending sort
139 iconSortDesc : 'glyphicon glyphicon-chevron-down', // class name added to icon when column has descending sort
140 filterRow : '', // filter row class
141 footerRow : '',
142 footerCells : '',
143 even : '', // even row zebra striping
144 odd : '' // odd row zebra striping
145 },
146 'jui' : {
147 table : 'ui-widget ui-widget-content ui-corner-all', // table classes
148 caption : 'ui-widget-content',
149 // header class names
150 header : 'ui-widget-header ui-corner-all ui-state-default', // header classes
151 sortNone : '',
152 sortAsc : '',
153 sortDesc : '',
154 active : 'ui-state-active', // applied when column is sorted
155 hover : 'ui-state-hover', // hover class
156 // icon class names
157 icons : 'ui-icon', // icon class added to the <i> in the header
158 iconSortNone : 'ui-icon-carat-2-n-s ui-icon-caret-2-n-s', // class name added to icon when column is not sorted
159 iconSortAsc : 'ui-icon-carat-1-n ui-icon-caret-1-n', // class name added to icon when column has ascending sort
160 iconSortDesc : 'ui-icon-carat-1-s ui-icon-caret-1-s', // class name added to icon when column has descending sort
161 filterRow : '',
162 footerRow : '',
163 footerCells : '',
164 even : 'ui-widget-content', // even row zebra striping
165 odd : 'ui-state-default' // odd row zebra striping
166 }
167 };
168
169 $.extend(ts.css, {
170 wrapper : 'tablesorter-wrapper' // ui theme & resizable
171 });
172
173 ts.addWidget({
174 id: 'uitheme',
175 priority: 10,
176 format: function(table, c, wo) {
177 var i, tmp, hdr, icon, time, $header, $icon, $tfoot, $h, oldtheme, oldremove, oldIconRmv, hasOldTheme,
178 themesAll = ts.themes,
179 $table = c.$table.add( $( c.namespace + '_extra_table' ) ),
180 $headers = c.$headers.add( $( c.namespace + '_extra_headers' ) ),
181 theme = c.theme || 'jui',
182 themes = themesAll[theme] || {},
183 remove = [ themes.sortNone, themes.sortDesc, themes.sortAsc, themes.active ].join( ' ' ).trim(),
184 iconRmv = [ themes.iconSortNone, themes.iconSortDesc, themes.iconSortAsc ].join( ' ' ).trim(),
185 debug = ts.debug(c, 'uitheme');
186 if (debug) { time = new Date(); }
187 // initialization code - run once
188 if (!$table.hasClass('tablesorter-' + theme) || c.theme !== c.appliedTheme || !wo.uitheme_applied) {
189 wo.uitheme_applied = true;
190 oldtheme = themesAll[c.appliedTheme] || {};
191 hasOldTheme = !$.isEmptyObject(oldtheme);
192 oldremove = hasOldTheme ? [ oldtheme.sortNone, oldtheme.sortDesc, oldtheme.sortAsc, oldtheme.active ].join( ' ' ) : '';
193 oldIconRmv = hasOldTheme ? [ oldtheme.iconSortNone, oldtheme.iconSortDesc, oldtheme.iconSortAsc ].join( ' ' ) : '';
194 if (hasOldTheme) {
195 wo.zebra[0] = ' ' + wo.zebra[0].replace(' ' + oldtheme.even, '').trim();
196 wo.zebra[1] = ' ' + wo.zebra[1].replace(' ' + oldtheme.odd, '').trim();
197 c.$tbodies.children().removeClass( [ oldtheme.even, oldtheme.odd ].join(' ') );
198 }
199 // update zebra stripes
200 if (themes.even) { wo.zebra[0] += ' ' + themes.even; }
201 if (themes.odd) { wo.zebra[1] += ' ' + themes.odd; }
202 // add caption style
203 $table.children('caption')
204 .removeClass(oldtheme.caption || '')
205 .addClass(themes.caption);
206 // add table/footer class names
207 $tfoot = $table
208 // remove other selected themes
209 .removeClass( (c.appliedTheme ? 'tablesorter-' + (c.appliedTheme || '') : '') + ' ' + (oldtheme.table || '') )
210 .addClass('tablesorter-' + theme + ' ' + (themes.table || '')) // add theme widget class name
211 .children('tfoot');
212 c.appliedTheme = c.theme;
213
214 if ($tfoot.length) {
215 $tfoot
216 // if oldtheme.footerRow or oldtheme.footerCells are undefined, all class names are removed
217 .children('tr').removeClass(oldtheme.footerRow || '').addClass(themes.footerRow)
218 .children('th, td').removeClass(oldtheme.footerCells || '').addClass(themes.footerCells);
219 }
220 // update header classes
221 $headers
222 .removeClass( (hasOldTheme ? [ oldtheme.header, oldtheme.hover, oldremove ].join(' ') : '') || '' )
223 .addClass(themes.header)
224 .not('.sorter-false')
225 .off('mouseenter.tsuitheme mouseleave.tsuitheme')
226 .on('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) {
227 // toggleClass with switch added in jQuery 1.3
228 $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover || '');
229 });
230
231 $headers.each(function() {
232 var $this = $(this);
233 if (!$this.find('.' + ts.css.wrapper).length) {
234 // Firefox needs this inner div to position the icon & resizer correctly
235 $this.wrapInner('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>');
236 }
237 });
238 if (c.cssIcon) {
239 // if c.cssIcon is '', then no <i> is added to the header
240 $headers
241 .find('.' + ts.css.icon)
242 .removeClass(hasOldTheme ? [ oldtheme.icons, oldIconRmv ].join(' ') : '')
243 .addClass(themes.icons || '');
244 }
245 // filter widget initializes after uitheme
246 if (ts.hasWidget( c.table, 'filter' )) {
247 tmp = function() {
248 $table.children('thead').children('.' + ts.css.filterRow)
249 .removeClass(hasOldTheme ? oldtheme.filterRow || '' : '')
250 .addClass(themes.filterRow || '');
251 };
252 if (wo.filter_initialized) {
253 tmp();
254 } else {
255 $table.one('filterInit', function() {
256 tmp();
257 });
258 }
259 }
260 }
261 for (i = 0; i < c.columns; i++) {
262 $header = c.$headers
263 .add($(c.namespace + '_extra_headers'))
264 .not('.sorter-false')
265 .filter('[data-column="' + i + '"]');
266 $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $();
267 $h = $headers.not('.sorter-false').filter('[data-column="' + i + '"]:last');
268 if ($h.length) {
269 $header.removeClass(remove);
270 $icon.removeClass(iconRmv);
271 if ($h[0].sortDisabled) {
272 // no sort arrows for disabled columns!
273 $icon.removeClass(themes.icons || '');
274 } else {
275 hdr = themes.sortNone;
276 icon = themes.iconSortNone;
277 if ($h.hasClass(ts.css.sortAsc)) {
278 hdr = [ themes.sortAsc, themes.active ].join(' ');
279 icon = themes.iconSortAsc;
280 } else if ($h.hasClass(ts.css.sortDesc)) {
281 hdr = [ themes.sortDesc, themes.active ].join(' ');
282 icon = themes.iconSortDesc;
283 }
284 $header.addClass(hdr);
285 $icon.addClass(icon || '');
286 }
287 }
288 }
289 if (debug) {
290 console.log('uitheme >> Applied ' + theme + ' theme' + ts.benchmark(time));
291 }
292 },
293 remove: function(table, c, wo, refreshing) {
294 if (!wo.uitheme_applied) { return; }
295 var $table = c.$table,
296 theme = c.appliedTheme || 'jui',
297 themes = ts.themes[ theme ] || ts.themes.jui,
298 $headers = $table.children('thead').children(),
299 remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc,
300 iconRmv = themes.iconSortNone + ' ' + themes.iconSortDesc + ' ' + themes.iconSortAsc;
301 $table.removeClass('tablesorter-' + theme + ' ' + themes.table);
302 wo.uitheme_applied = false;
303 if (refreshing) { return; }
304 $table.find(ts.css.header).removeClass(themes.header);
305 $headers
306 .off('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover
307 .removeClass(themes.hover + ' ' + remove + ' ' + themes.active)
308 .filter('.' + ts.css.filterRow)
309 .removeClass(themes.filterRow);
310 $headers.find('.' + ts.css.icon).removeClass(themes.icons + ' ' + iconRmv);
311 }
312 });
313
314 })(jQuery);
315
316 /*! Widget: columns - updated 5/24/2017 (v2.28.11) */
317 ;(function ($) {
318 'use strict';
319 var ts = $.tablesorter || {};
320
321 ts.addWidget({
322 id: 'columns',
323 priority: 65,
324 options : {
325 columns : [ 'primary', 'secondary', 'tertiary' ]
326 },
327 format: function(table, c, wo) {
328 var $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx,
329 $table = c.$table,
330 $tbodies = c.$tbodies,
331 sortList = c.sortList,
332 len = sortList.length,
333 // removed c.widgetColumns support
334 css = wo && wo.columns || [ 'primary', 'secondary', 'tertiary' ],
335 last = css.length - 1;
336 remove = css.join(' ');
337 // check if there is a sort (on initialization there may not be one)
338 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
339 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody
340 $rows = $tbody.children('tr');
341 // loop through the visible rows
342 $rows.each(function() {
343 $row = $(this);
344 if (this.style.display !== 'none') {
345 // remove all columns class names
346 $cells = $row.children().removeClass(remove);
347 // add appropriate column class names
348 if (sortList && sortList[0]) {
349 // primary sort column class
350 $cells.eq(sortList[0][0]).addClass(css[0]);
351 if (len > 1) {
352 for (indx = 1; indx < len; indx++) {
353 // secondary, tertiary, etc sort column classes
354 $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] );
355 }
356 }
357 }
358 }
359 });
360 ts.processTbody(table, $tbody, false);
361 }
362 // add classes to thead and tfoot
363 rows = wo.columns_thead !== false ? [ 'thead tr' ] : [];
364 if (wo.columns_tfoot !== false) {
365 rows.push('tfoot tr');
366 }
367 if (rows.length) {
368 $rows = $table.find( rows.join(',') ).children().removeClass(remove);
369 if (len) {
370 for (indx = 0; indx < len; indx++) {
371 // add primary. secondary, tertiary, etc sort column classes
372 $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]);
373 }
374 }
375 }
376 },
377 remove: function(table, c, wo) {
378 var tbodyIndex, $tbody,
379 $tbodies = c.$tbodies,
380 remove = (wo.columns || [ 'primary', 'secondary', 'tertiary' ]).join(' ');
381 c.$headers.removeClass(remove);
382 c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove);
383 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
384 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
385 $tbody.children('tr').each(function() {
386 $(this).children().removeClass(remove);
387 });
388 ts.processTbody(table, $tbody, false); // restore tbody
389 }
390 }
391 });
392
393 })(jQuery);
394
395 /*! Widget: filter - updated 2018-03-18 (v2.30.0) *//*
396 * Requires tablesorter v2.8+ and jQuery 1.7+
397 * by Rob Garrison
398 */
399 ;( function ( $ ) {
400 'use strict';
401 var tsf, tsfRegex,
402 ts = $.tablesorter || {},
403 tscss = ts.css,
404 tskeyCodes = ts.keyCodes;
405
406 $.extend( tscss, {
407 filterRow : 'tablesorter-filter-row',
408 filter : 'tablesorter-filter',
409 filterDisabled : 'disabled',
410 filterRowHide : 'hideme'
411 });
412
413 $.extend( tskeyCodes, {
414 backSpace : 8,
415 escape : 27,
416 space : 32,
417 left : 37,
418 down : 40
419 });
420
421 ts.addWidget({
422 id: 'filter',
423 priority: 50,
424 options : {
425 filter_cellFilter : '', // css class name added to the filter cell ( string or array )
426 filter_childRows : false, // if true, filter includes child row content in the search
427 filter_childByColumn : false, // ( filter_childRows must be true ) if true = search child rows by column; false = search all child row text grouped
428 filter_childWithSibs : true, // if true, include matching child row siblings
429 filter_columnAnyMatch: true, // if true, allows using '#:{query}' in AnyMatch searches ( column:query )
430 filter_columnFilters : true, // if true, a filter will be added to the top of each table column
431 filter_cssFilter : '', // css class name added to the filter row & each input in the row ( tablesorter-filter is ALWAYS added )
432 filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value
433 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.
434 filter_excludeFilter : {}, // filters to exclude, per column
435 filter_external : '', // jQuery selector string ( or jQuery object ) of external filters
436 filter_filteredRow : 'filtered', // class added to filtered rows; define in css with "display:none" to hide the filtered-out rows
437 filter_filterLabel : 'Filter "{{label}}" column by...', // Aria-label added to filter input/select; see #1495
438 filter_formatter : null, // add custom filter elements to the filter row
439 filter_functions : null, // add custom filter functions using this option
440 filter_hideEmpty : true, // hide filter row when table is empty
441 filter_hideFilters : false, // collapse filter row when mouse leaves the area
442 filter_ignoreCase : true, // if true, make all searches case-insensitive
443 filter_liveSearch : true, // if true, search column content while the user types ( with a delay )
444 filter_matchType : { 'input': 'exact', 'select': 'exact' }, // global query settings ('exact' or 'match'); overridden by "filter-match" or "filter-exact" class
445 filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available ( visible ) options within the drop down
446 filter_placeholder : { search : '', select : '' }, // default placeholder text ( overridden by any header 'data-placeholder' setting )
447 filter_reset : null, // jQuery selector string of an element used to reset the filters
448 filter_resetOnEsc : true, // Reset filter input when the user presses escape - normalized across browsers
449 filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters
450 filter_searchDelay : 300, // typing delay in milliseconds before starting a search
451 filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true
452 filter_selectSource : null, // include a function to return an array of values to be added to the column filter select
453 filter_selectSourceSeparator : '|', // filter_selectSource array text left of the separator is added to the option value, right into the option text
454 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.
455 filter_startsWith : false, // if true, filter start from the beginning of the cell contents
456 filter_useParsedData : false // filter all data using parsed content
457 },
458 format: function( table, c, wo ) {
459 if ( !c.$table.hasClass( 'hasFilters' ) ) {
460 tsf.init( table, c, wo );
461 }
462 },
463 remove: function( table, c, wo, refreshing ) {
464 var tbodyIndex, $tbody,
465 $table = c.$table,
466 $tbodies = c.$tbodies,
467 events = (
468 'addRows updateCell update updateRows updateComplete appendCache filterReset ' +
469 'filterAndSortReset filterFomatterUpdate filterEnd search stickyHeadersInit '
470 ).split( ' ' ).join( c.namespace + 'filter ' );
471 $table
472 .removeClass( 'hasFilters' )
473 // add filter namespace to all BUT search
474 .off( events.replace( ts.regex.spaces, ' ' ) )
475 // remove the filter row even if refreshing, because the column might have been moved
476 .find( '.' + tscss.filterRow ).remove();
477 wo.filter_initialized = false;
478 if ( refreshing ) { return; }
479 for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
480 $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody
481 $tbody.children().removeClass( wo.filter_filteredRow ).show();
482 ts.processTbody( table, $tbody, false ); // restore tbody
483 }
484 if ( wo.filter_reset ) {
485 $( document ).off( 'click' + c.namespace + 'filter', wo.filter_reset );
486 }
487 }
488 });
489
490 tsf = ts.filter = {
491
492 // regex used in filter 'check' functions - not for general use and not documented
493 regex: {
494 regex : /^\/((?:\\\/|[^\/])+)\/([migyu]{0,5})?$/, // regex to test for regex
495 child : /tablesorter-childRow/, // child row class name; this gets updated in the script
496 filtered : /filtered/, // filtered (hidden) row class name; updated in the script
497 type : /undefined|number/, // check type
498 exact : /(^[\"\'=]+)|([\"\'=]+$)/g, // exact match (allow '==')
499 operators : /[<>=]/g, // replace operators
500 query : '(q|query)', // replace filter queries
501 wild01 : /\?/g, // wild card match 0 or 1
502 wild0More : /\*/g, // wild care match 0 or more
503 quote : /\"/g,
504 isNeg1 : /(>=?\s*-\d)/,
505 isNeg2 : /(<=?\s*\d)/
506 },
507 // function( c, data ) { }
508 // c = table.config
509 // data.$row = jQuery object of the row currently being processed
510 // data.$cells = jQuery object of all cells within the current row
511 // data.filters = array of filters for all columns ( some may be undefined )
512 // data.filter = filter for the current column
513 // data.iFilter = same as data.filter, except lowercase ( if wo.filter_ignoreCase is true )
514 // data.exact = table cell text ( or parsed data if column parser enabled; may be a number & not a string )
515 // data.iExact = same as data.exact, except lowercase ( if wo.filter_ignoreCase is true; may be a number & not a string )
516 // data.cache = table cell text from cache, so it has been parsed ( & in all lower case if c.ignoreCase is true )
517 // data.cacheArray = An array of parsed content from each table cell in the row being processed
518 // data.index = column index; table = table element ( DOM )
519 // data.parsed = array ( by column ) of boolean values ( from filter_useParsedData or 'filter-parsed' class )
520 types: {
521 or : function( c, data, vars ) {
522 // look for "|", but not if it is inside of a regular expression
523 if ( ( tsfRegex.orTest.test( data.iFilter ) || tsfRegex.orSplit.test( data.filter ) ) &&
524 // this test for regex has potential to slow down the overall search
525 !tsfRegex.regex.test( data.filter ) ) {
526 var indx, filterMatched, query, regex,
527 // duplicate data but split filter
528 data2 = $.extend( {}, data ),
529 filter = data.filter.split( tsfRegex.orSplit ),
530 iFilter = data.iFilter.split( tsfRegex.orSplit ),
531 len = filter.length;
532 for ( indx = 0; indx < len; indx++ ) {
533 data2.nestedFilters = true;
534 data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' );
535 data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' );
536 query = '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')';
537 try {
538 // use try/catch, because query may not be a valid regex if "|" is contained within a partial regex search,
539 // e.g "/(Alex|Aar" -> Uncaught SyntaxError: Invalid regular expression: /(/(Alex)/: Unterminated group
540 regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' );
541 // filterMatched = data2.filter === '' && indx > 0 ? true
542 // look for an exact match with the 'or' unless the 'filter-match' class is found
543 filterMatched = regex.test( data2.exact ) || tsf.processTypes( c, data2, vars );
544 if ( filterMatched ) {
545 return filterMatched;
546 }
547 } catch ( error ) {
548 return null;
549 }
550 }
551 // may be null from processing types
552 return filterMatched || false;
553 }
554 return null;
555 },
556 // Look for an AND or && operator ( logical and )
557 and : function( c, data, vars ) {
558 if ( tsfRegex.andTest.test( data.filter ) ) {
559 var indx, filterMatched, result, query, regex,
560 // duplicate data but split filter
561 data2 = $.extend( {}, data ),
562 filter = data.filter.split( tsfRegex.andSplit ),
563 iFilter = data.iFilter.split( tsfRegex.andSplit ),
564 len = filter.length;
565 for ( indx = 0; indx < len; indx++ ) {
566 data2.nestedFilters = true;
567 data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' );
568 data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' );
569 query = ( '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')' )
570 // replace wild cards since /(a*)/i will match anything
571 .replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' );
572 try {
573 // use try/catch just in case RegExp is invalid
574 regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' );
575 // look for an exact match with the 'and' unless the 'filter-match' class is found
576 result = ( regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ) );
577 if ( indx === 0 ) {
578 filterMatched = result;
579 } else {
580 filterMatched = filterMatched && result;
581 }
582 } catch ( error ) {
583 return null;
584 }
585 }
586 // may be null from processing types
587 return filterMatched || false;
588 }
589 return null;
590 },
591 // Look for regex
592 regex: function( c, data ) {
593 if ( tsfRegex.regex.test( data.filter ) ) {
594 var matches,
595 // cache regex per column for optimal speed
596 regex = data.filter_regexCache[ data.index ] || tsfRegex.regex.exec( data.filter ),
597 isRegex = regex instanceof RegExp;
598 try {
599 if ( !isRegex ) {
600 // force case insensitive search if ignoreCase option set?
601 // if ( c.ignoreCase && !regex[2] ) { regex[2] = 'i'; }
602 data.filter_regexCache[ data.index ] = regex = new RegExp( regex[1], regex[2] );
603 }
604 matches = regex.test( data.exact );
605 } catch ( error ) {
606 matches = false;
607 }
608 return matches;
609 }
610 return null;
611 },
612 // Look for operators >, >=, < or <=
613 operators: function( c, data ) {
614 // ignore empty strings... because '' < 10 is true
615 if ( tsfRegex.operTest.test( data.iFilter ) && data.iExact !== '' ) {
616 var cachedValue, result, txt,
617 table = c.table,
618 parsed = data.parsed[ data.index ],
619 query = ts.formatFloat( data.iFilter.replace( tsfRegex.operators, '' ), table ),
620 parser = c.parsers[ data.index ] || {},
621 savedSearch = query;
622 // parse filter value in case we're comparing numbers ( dates )
623 if ( parsed || parser.type === 'numeric' ) {
624 txt = '' + data.iFilter.replace( tsfRegex.operators, '' ).trim();
625 result = tsf.parseFilter( c, txt, data, true );
626 query = ( typeof result === 'number' && result !== '' && !isNaN( result ) ) ? result : query;
627 }
628 // iExact may be numeric - see issue #149;
629 // check if cached is defined, because sometimes j goes out of range? ( numeric columns )
630 if ( ( parsed || parser.type === 'numeric' ) && !isNaN( query ) &&
631 typeof data.cache !== 'undefined' ) {
632 cachedValue = data.cache;
633 } else {
634 txt = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact;
635 cachedValue = ts.formatFloat( txt, table );
636 }
637 if ( tsfRegex.gtTest.test( data.iFilter ) ) {
638 result = tsfRegex.gteTest.test( data.iFilter ) ? cachedValue >= query : cachedValue > query;
639 } else if ( tsfRegex.ltTest.test( data.iFilter ) ) {
640 result = tsfRegex.lteTest.test( data.iFilter ) ? cachedValue <= query : cachedValue < query;
641 }
642 // keep showing all rows if nothing follows the operator
643 if ( !result && savedSearch === '' ) {
644 result = true;
645 }
646 return result;
647 }
648 return null;
649 },
650 // Look for a not match
651 notMatch: function( c, data ) {
652 if ( tsfRegex.notTest.test( data.iFilter ) ) {
653 var indx,
654 txt = data.iFilter.replace( '!', '' ),
655 filter = tsf.parseFilter( c, txt, data ) || '';
656 if ( tsfRegex.exact.test( filter ) ) {
657 // look for exact not matches - see #628
658 filter = filter.replace( tsfRegex.exact, '' );
659 return filter === '' ? true : filter.trim() !== data.iExact;
660 } else {
661 indx = data.iExact.search( filter.trim() );
662 return filter === '' ? true :
663 // return true if not found
664 data.anyMatch ? indx < 0 :
665 // return false if found
666 !( c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 0 );
667 }
668 }
669 return null;
670 },
671 // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric
672 exact: function( c, data ) {
673 /*jshint eqeqeq:false */
674 if ( tsfRegex.exact.test( data.iFilter ) ) {
675 var txt = data.iFilter.replace( tsfRegex.exact, '' ),
676 filter = tsf.parseFilter( c, txt, data ) || '';
677 // eslint-disable-next-line eqeqeq
678 return data.anyMatch ? $.inArray( filter, data.rowArray ) >= 0 : filter == data.iExact;
679 }
680 return null;
681 },
682 // Look for a range ( using ' to ' or ' - ' ) - see issue #166; thanks matzhu!
683 range : function( c, data ) {
684 if ( tsfRegex.toTest.test( data.iFilter ) ) {
685 var result, tmp, range1, range2,
686 table = c.table,
687 index = data.index,
688 parsed = data.parsed[index],
689 // make sure the dash is for a range and not indicating a negative number
690 query = data.iFilter.split( tsfRegex.toSplit );
691
692 tmp = query[0].replace( ts.regex.nondigit, '' ) || '';
693 range1 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table );
694 tmp = query[1].replace( ts.regex.nondigit, '' ) || '';
695 range2 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table );
696 // parse filter value in case we're comparing numbers ( dates )
697 if ( parsed || c.parsers[ index ].type === 'numeric' ) {
698 result = c.parsers[ index ].format( '' + query[0], table, c.$headers.eq( index ), index );
699 range1 = ( result !== '' && !isNaN( result ) ) ? result : range1;
700 result = c.parsers[ index ].format( '' + query[1], table, c.$headers.eq( index ), index );
701 range2 = ( result !== '' && !isNaN( result ) ) ? result : range2;
702 }
703 if ( ( parsed || c.parsers[ index ].type === 'numeric' ) && !isNaN( range1 ) && !isNaN( range2 ) ) {
704 result = data.cache;
705 } else {
706 tmp = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact;
707 result = ts.formatFloat( tmp, table );
708 }
709 if ( range1 > range2 ) {
710 tmp = range1; range1 = range2; range2 = tmp; // swap
711 }
712 return ( result >= range1 && result <= range2 ) || ( range1 === '' || range2 === '' );
713 }
714 return null;
715 },
716 // Look for wild card: ? = single, * = multiple, or | = logical OR
717 wild : function( c, data ) {
718 if ( tsfRegex.wildOrTest.test( data.iFilter ) ) {
719 var query = '' + ( tsf.parseFilter( c, data.iFilter, data ) || '' );
720 // look for an exact match with the 'or' unless the 'filter-match' class is found
721 if ( !tsfRegex.wildTest.test( query ) && data.nestedFilters ) {
722 query = data.isMatch ? query : '^(' + query + ')$';
723 }
724 // parsing the filter may not work properly when using wildcards =/
725 try {
726 return new RegExp(
727 query.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ),
728 c.widgetOptions.filter_ignoreCase ? 'i' : ''
729 )
730 .test( data.exact );
731 } catch ( error ) {
732 return null;
733 }
734 }
735 return null;
736 },
737 // fuzzy text search; modified from https://github.com/mattyork/fuzzy ( MIT license )
738 fuzzy: function( c, data ) {
739 if ( tsfRegex.fuzzyTest.test( data.iFilter ) ) {
740 var indx,
741 patternIndx = 0,
742 len = data.iExact.length,
743 txt = data.iFilter.slice( 1 ),
744 pattern = tsf.parseFilter( c, txt, data ) || '';
745 for ( indx = 0; indx < len; indx++ ) {
746 if ( data.iExact[ indx ] === pattern[ patternIndx ] ) {
747 patternIndx += 1;
748 }
749 }
750 return patternIndx === pattern.length;
751 }
752 return null;
753 }
754 },
755 init: function( table ) {
756 // filter language options
757 ts.language = $.extend( true, {}, {
758 to : 'to',
759 or : 'or',
760 and : 'and'
761 }, ts.language );
762
763 var options, string, txt, $header, column, val, fxn, noSelect,
764 c = table.config,
765 wo = c.widgetOptions,
766 processStr = function(prefix, str, suffix) {
767 str = str.trim();
768 // don't include prefix/suffix if str is empty
769 return str === '' ? '' : (prefix || '') + str + (suffix || '');
770 };
771 c.$table.addClass( 'hasFilters' );
772 c.lastSearch = [];
773
774 // define timers so using clearTimeout won't cause an undefined error
775 wo.filter_searchTimer = null;
776 wo.filter_initTimer = null;
777 wo.filter_formatterCount = 0;
778 wo.filter_formatterInit = [];
779 wo.filter_anyColumnSelector = '[data-column="all"],[data-column="any"]';
780 wo.filter_multipleColumnSelector = '[data-column*="-"],[data-column*=","]';
781
782 val = '\\{' + tsfRegex.query + '\\}';
783 $.extend( tsfRegex, {
784 child : new RegExp( c.cssChildRow ),
785 filtered : new RegExp( wo.filter_filteredRow ),
786 alreadyFiltered : new RegExp( '(\\s+(-' + processStr('|', ts.language.or) + processStr('|', ts.language.to) + ')\\s+)', 'i' ),
787 toTest : new RegExp( '\\s+(-' + processStr('|', ts.language.to) + ')\\s+', 'i' ),
788 toSplit : new RegExp( '(?:\\s+(?:-' + processStr('|', ts.language.to) + ')\\s+)', 'gi' ),
789 andTest : new RegExp( '\\s+(' + processStr('', ts.language.and, '|') + '&&)\\s+', 'i' ),
790 andSplit : new RegExp( '(?:\\s+(?:' + processStr('', ts.language.and, '|') + '&&)\\s+)', 'gi' ),
791 orTest : new RegExp( '(\\|' + processStr('|\\s+', ts.language.or, '\\s+') + ')', 'i' ),
792 orSplit : new RegExp( '(?:\\|' + processStr('|\\s+(?:', ts.language.or, ')\\s+') + ')', 'gi' ),
793 iQuery : new RegExp( val, 'i' ),
794 igQuery : new RegExp( val, 'ig' ),
795 operTest : /^[<>]=?/,
796 gtTest : />/,
797 gteTest : />=/,
798 ltTest : /</,
799 lteTest : /<=/,
800 notTest : /^\!/,
801 wildOrTest : /[\?\*\|]/,
802 wildTest : /\?\*/,
803 fuzzyTest : /^~/,
804 exactTest : /[=\"\|!]/
805 });
806
807 // don't build filter row if columnFilters is false or all columns are set to 'filter-false'
808 // see issue #156
809 val = c.$headers.filter( '.filter-false, .parser-false' ).length;
810 if ( wo.filter_columnFilters !== false && val !== c.$headers.length ) {
811 // build filter row
812 tsf.buildRow( table, c, wo );
813 }
814
815 txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset ' +
816 'filterAndSortReset filterResetSaved filterEnd search '.split( ' ' ).join( c.namespace + 'filter ' );
817 c.$table.on( txt, function( event, filter ) {
818 val = wo.filter_hideEmpty &&
819 $.isEmptyObject( c.cache ) &&
820 !( c.delayInit && event.type === 'appendCache' );
821 // hide filter row using the 'filtered' class name
822 c.$table.find( '.' + tscss.filterRow ).toggleClass( wo.filter_filteredRow, val ); // fixes #450
823 if ( !/(search|filter)/.test( event.type ) ) {
824 event.stopPropagation();
825 tsf.buildDefault( table, true );
826 }
827 // Add filterAndSortReset - see #1361
828 if ( event.type === 'filterReset' || event.type === 'filterAndSortReset' ) {
829 c.$table.find( '.' + tscss.filter ).add( wo.filter_$externalFilters ).val( '' );
830 if ( event.type === 'filterAndSortReset' ) {
831 ts.sortReset( this.config, function() {
832 tsf.searching( table, [] );
833 });
834 } else {
835 tsf.searching( table, [] );
836 }
837 } else if ( event.type === 'filterResetSaved' ) {
838 ts.storage( table, 'tablesorter-filters', '' );
839 } else if ( event.type === 'filterEnd' ) {
840 tsf.buildDefault( table, true );
841 } else {
842 // send false argument to force a new search; otherwise if the filter hasn't changed,
843 // it will return
844 filter = event.type === 'search' ? filter :
845 event.type === 'updateComplete' ? c.$table.data( 'lastSearch' ) : '';
846 if ( /(update|add)/.test( event.type ) && event.type !== 'updateComplete' ) {
847 // force a new search since content has changed
848 c.lastCombinedFilter = null;
849 c.lastSearch = [];
850 // update filterFormatters after update (& small delay) - Fixes #1237
851 setTimeout(function() {
852 c.$table.triggerHandler( 'filterFomatterUpdate' );
853 }, 100);
854 }
855 // pass true ( skipFirst ) to prevent the tablesorter.setFilters function from skipping the first
856 // input ensures all inputs are updated when a search is triggered on the table
857 // $( 'table' ).trigger( 'search', [...] );
858 tsf.searching( table, filter, true );
859 }
860 return false;
861 });
862
863 // reset button/link
864 if ( wo.filter_reset ) {
865 if ( wo.filter_reset instanceof $ ) {
866 // reset contains a jQuery object, bind to it
867 wo.filter_reset.click( function() {
868 c.$table.triggerHandler( 'filterReset' );
869 });
870 } else if ( $( wo.filter_reset ).length ) {
871 // reset is a jQuery selector, use event delegation
872 $( document )
873 .off( 'click' + c.namespace + 'filter', wo.filter_reset )
874 .on( 'click' + c.namespace + 'filter', wo.filter_reset, function() {
875 // trigger a reset event, so other functions ( filter_formatter ) know when to reset
876 c.$table.triggerHandler( 'filterReset' );
877 });
878 }
879 }
880 if ( wo.filter_functions ) {
881 for ( column = 0; column < c.columns; column++ ) {
882 fxn = ts.getColumnData( table, wo.filter_functions, column );
883 if ( fxn ) {
884 // remove 'filter-select' from header otherwise the options added here are replaced with
885 // all options
886 $header = c.$headerIndexed[ column ].removeClass( 'filter-select' );
887 // don't build select if 'filter-false' or 'parser-false' set
888 noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) );
889 options = '';
890 if ( fxn === true && noSelect ) {
891 tsf.buildSelect( table, column );
892 } else if ( typeof fxn === 'object' && noSelect ) {
893 // add custom drop down list
894 for ( string in fxn ) {
895 if ( typeof string === 'string' ) {
896 options += options === '' ?
897 '<option value="">' +
898 ( $header.data( 'placeholder' ) ||
899 $header.attr( 'data-placeholder' ) ||
900 wo.filter_placeholder.select ||
901 ''
902 ) +
903 '</option>' : '';
904 val = string;
905 txt = string;
906 if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) {
907 val = string.split( wo.filter_selectSourceSeparator );
908 txt = val[1];
909 val = val[0];
910 }
911 options += '<option ' +
912 ( txt === val ? '' : 'data-function-name="' + string + '" ' ) +
913 'value="' + val + '">' + txt + '</option>';
914 }
915 }
916 c.$table
917 .find( 'thead' )
918 .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' )
919 .append( options );
920 txt = wo.filter_selectSource;
921 fxn = typeof txt === 'function' ? true : ts.getColumnData( table, txt, column );
922 if ( fxn ) {
923 // updating so the extra options are appended
924 tsf.buildSelect( c.table, column, '', true, $header.hasClass( wo.filter_onlyAvail ) );
925 }
926 }
927 }
928 }
929 }
930 // not really updating, but if the column has both the 'filter-select' class &
931 // filter_functions set to true, it would append the same options twice.
932 tsf.buildDefault( table, true );
933
934 tsf.bindSearch( table, c.$table.find( '.' + tscss.filter ), true );
935 if ( wo.filter_external ) {
936 tsf.bindSearch( table, wo.filter_external );
937 }
938
939 if ( wo.filter_hideFilters ) {
940 tsf.hideFilters( c );
941 }
942
943 // show processing icon
944 if ( c.showProcessing ) {
945 txt = 'filterStart filterEnd '.split( ' ' ).join( c.namespace + 'filter-sp ' );
946 c.$table
947 .off( txt.replace( ts.regex.spaces, ' ' ) )
948 .on( txt, function( event, columns ) {
949 // only add processing to certain columns to all columns
950 $header = ( columns ) ?
951 c.$table
952 .find( '.' + tscss.header )
953 .filter( '[data-column]' )
954 .filter( function() {
955 return columns[ $( this ).data( 'column' ) ] !== '';
956 }) : '';
957 ts.isProcessing( table, event.type === 'filterStart', columns ? $header : '' );
958 });
959 }
960
961 // set filtered rows count ( intially unfiltered )
962 c.filteredRows = c.totalRows;
963
964 // add default values
965 txt = 'tablesorter-initialized pagerBeforeInitialized '.split( ' ' ).join( c.namespace + 'filter ' );
966 c.$table
967 .off( txt.replace( ts.regex.spaces, ' ' ) )
968 .on( txt, function() {
969 tsf.completeInit( this );
970 });
971 // if filter widget is added after pager has initialized; then set filter init flag
972 if ( c.pager && c.pager.initialized && !wo.filter_initialized ) {
973 c.$table.triggerHandler( 'filterFomatterUpdate' );
974 setTimeout( function() {
975 tsf.filterInitComplete( c );
976 }, 100 );
977 } else if ( !wo.filter_initialized ) {
978 tsf.completeInit( table );
979 }
980 },
981 completeInit: function( table ) {
982 // redefine 'c' & 'wo' so they update properly inside this callback
983 var c = table.config,
984 wo = c.widgetOptions,
985 filters = tsf.setDefaults( table, c, wo ) || [];
986 if ( filters.length ) {
987 // prevent delayInit from triggering a cache build if filters are empty
988 if ( !( c.delayInit && filters.join( '' ) === '' ) ) {
989 ts.setFilters( table, filters, true );
990 }
991 }
992 c.$table.triggerHandler( 'filterFomatterUpdate' );
993 // trigger init after setTimeout to prevent multiple filterStart/End/Init triggers
994 setTimeout( function() {
995 if ( !wo.filter_initialized ) {
996 tsf.filterInitComplete( c );
997 }
998 }, 100 );
999 },
1000
1001 // $cell parameter, but not the config, is passed to the filter_formatters,
1002 // so we have to work with it instead
1003 formatterUpdated: function( $cell, column ) {
1004 // prevent error if $cell is undefined - see #1056
1005 var $table = $cell && $cell.closest( 'table' );
1006 var config = $table.length && $table[0].config,
1007 wo = config && config.widgetOptions;
1008 if ( wo && !wo.filter_initialized ) {
1009 // add updates by column since this function
1010 // may be called numerous times before initialization
1011 wo.filter_formatterInit[ column ] = 1;
1012 }
1013 },
1014 filterInitComplete: function( c ) {
1015 var indx, len,
1016 wo = c.widgetOptions,
1017 count = 0,
1018 completed = function() {
1019 wo.filter_initialized = true;
1020 // update lastSearch - it gets cleared often
1021 c.lastSearch = c.$table.data( 'lastSearch' );
1022 c.$table.triggerHandler( 'filterInit', c );
1023 tsf.findRows( c.table, c.lastSearch || [] );
1024 if (ts.debug(c, 'filter')) {
1025 console.log('Filter >> Widget initialized');
1026 }
1027 };
1028 if ( $.isEmptyObject( wo.filter_formatter ) ) {
1029 completed();
1030 } else {
1031 len = wo.filter_formatterInit.length;
1032 for ( indx = 0; indx < len; indx++ ) {
1033 if ( wo.filter_formatterInit[ indx ] === 1 ) {
1034 count++;
1035 }
1036 }
1037 clearTimeout( wo.filter_initTimer );
1038 if ( !wo.filter_initialized && count === wo.filter_formatterCount ) {
1039 // filter widget initialized
1040 completed();
1041 } else if ( !wo.filter_initialized ) {
1042 // fall back in case a filter_formatter doesn't call
1043 // $.tablesorter.filter.formatterUpdated( $cell, column ), and the count is off
1044 wo.filter_initTimer = setTimeout( function() {
1045 completed();
1046 }, 500 );
1047 }
1048 }
1049 },
1050 // encode or decode filters for storage; see #1026
1051 processFilters: function( filters, encode ) {
1052 var indx,
1053 // fixes #1237; previously returning an encoded "filters" value
1054 result = [],
1055 mode = encode ? encodeURIComponent : decodeURIComponent,
1056 len = filters.length;
1057 for ( indx = 0; indx < len; indx++ ) {
1058 if ( filters[ indx ] ) {
1059 result[ indx ] = mode( filters[ indx ] );
1060 }
1061 }
1062 return result;
1063 },
1064 setDefaults: function( table, c, wo ) {
1065 var isArray, saved, indx, col, $filters,
1066 // get current ( default ) filters
1067 filters = ts.getFilters( table ) || [];
1068 if ( wo.filter_saveFilters && ts.storage ) {
1069 saved = ts.storage( table, 'tablesorter-filters' ) || [];
1070 isArray = Array.isArray( saved );
1071 // make sure we're not just getting an empty array
1072 if ( !( isArray && saved.join( '' ) === '' || !isArray ) ) {
1073 filters = tsf.processFilters( saved );
1074 }
1075 }
1076 // if no filters saved, then check default settings
1077 if ( filters.join( '' ) === '' ) {
1078 // allow adding default setting to external filters
1079 $filters = c.$headers.add( wo.filter_$externalFilters )
1080 .filter( '[' + wo.filter_defaultAttrib + ']' );
1081 for ( indx = 0; indx <= c.columns; indx++ ) {
1082 // include data-column='all' external filters
1083 col = indx === c.columns ? 'all' : indx;
1084 filters[ indx ] = $filters
1085 .filter( '[data-column="' + col + '"]' )
1086 .attr( wo.filter_defaultAttrib ) || filters[indx] || '';
1087 }
1088 }
1089 c.$table.data( 'lastSearch', filters );
1090 return filters;
1091 },
1092 parseFilter: function( c, filter, data, parsed ) {
1093 return parsed || data.parsed[ data.index ] ?
1094 c.parsers[ data.index ].format( filter, c.table, [], data.index ) :
1095 filter;
1096 },
1097 buildRow: function( table, c, wo ) {
1098 var $filter, col, column, $header, makeSelect, disabled, name, ffxn, tmp,
1099 // c.columns defined in computeThIndexes()
1100 cellFilter = wo.filter_cellFilter,
1101 columns = c.columns,
1102 arry = Array.isArray( cellFilter ),
1103 buildFilter = '<tr role="search" class="' + tscss.filterRow + ' ' + c.cssIgnoreRow + '">';
1104 for ( column = 0; column < columns; column++ ) {
1105 if ( c.$headerIndexed[ column ].length ) {
1106 // account for entire column set with colspan. See #1047
1107 tmp = c.$headerIndexed[ column ] && c.$headerIndexed[ column ][0].colSpan || 0;
1108 if ( tmp > 1 ) {
1109 buildFilter += '<td data-column="' + column + '-' + ( column + tmp - 1 ) + '" colspan="' + tmp + '"';
1110 } else {
1111 buildFilter += '<td data-column="' + column + '"';
1112 }
1113 if ( arry ) {
1114 buildFilter += ( cellFilter[ column ] ? ' class="' + cellFilter[ column ] + '"' : '' );
1115 } else {
1116 buildFilter += ( cellFilter !== '' ? ' class="' + cellFilter + '"' : '' );
1117 }
1118 buildFilter += '></td>';
1119 }
1120 }
1121 c.$filters = $( buildFilter += '</tr>' )
1122 .appendTo( c.$table.children( 'thead' ).eq( 0 ) )
1123 .children( 'td' );
1124 // build each filter input
1125 for ( column = 0; column < columns; column++ ) {
1126 disabled = false;
1127 // assuming last cell of a column is the main column
1128 $header = c.$headerIndexed[ column ];
1129 if ( $header && $header.length ) {
1130 // $filter = c.$filters.filter( '[data-column="' + column + '"]' );
1131 $filter = tsf.getColumnElm( c, c.$filters, column );
1132 ffxn = ts.getColumnData( table, wo.filter_functions, column );
1133 makeSelect = ( wo.filter_functions && ffxn && typeof ffxn !== 'function' ) ||
1134 $header.hasClass( 'filter-select' );
1135 // get data from jQuery data, metadata, headers option or header class name
1136 col = ts.getColumnData( table, c.headers, column );
1137 disabled = ts.getData( $header[0], col, 'filter' ) === 'false' ||
1138 ts.getData( $header[0], col, 'parser' ) === 'false';
1139
1140 if ( makeSelect ) {
1141 buildFilter = $( '<select>' ).appendTo( $filter );
1142 } else {
1143 ffxn = ts.getColumnData( table, wo.filter_formatter, column );
1144 if ( ffxn ) {
1145 wo.filter_formatterCount++;
1146 buildFilter = ffxn( $filter, column );
1147 // no element returned, so lets go find it
1148 if ( buildFilter && buildFilter.length === 0 ) {
1149 buildFilter = $filter.children( 'input' );
1150 }
1151 // element not in DOM, so lets attach it
1152 if ( buildFilter && ( buildFilter.parent().length === 0 ||
1153 ( buildFilter.parent().length && buildFilter.parent()[0] !== $filter[0] ) ) ) {
1154 $filter.append( buildFilter );
1155 }
1156 } else {
1157 buildFilter = $( '<input type="search">' ).appendTo( $filter );
1158 }
1159 if ( buildFilter ) {
1160 tmp = $header.data( 'placeholder' ) ||
1161 $header.attr( 'data-placeholder' ) ||
1162 wo.filter_placeholder.search || '';
1163 buildFilter.attr( 'placeholder', tmp );
1164 }
1165 }
1166 if ( buildFilter ) {
1167 // add filter class name
1168 name = ( Array.isArray( wo.filter_cssFilter ) ?
1169 ( typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '' ) :
1170 wo.filter_cssFilter ) || '';
1171 // copy data-column from table cell (it will include colspan)
1172 buildFilter.addClass( tscss.filter + ' ' + name );
1173 name = wo.filter_filterLabel;
1174 tmp = name.match(/{{([^}]+?)}}/g);
1175 if (!tmp) {
1176 tmp = [ '{{label}}' ];
1177 }
1178 $.each(tmp, function(indx, attr) {
1179 var regex = new RegExp(attr, 'g'),
1180 data = $header.attr('data-' + attr.replace(/{{|}}/g, '')),
1181 text = typeof data === 'undefined' ? $header.text() : data;
1182 name = name.replace( regex, text.trim() );
1183 });
1184 buildFilter.attr({
1185 'data-column': $filter.attr( 'data-column' ),
1186 'aria-label': name
1187 });
1188 if ( disabled ) {
1189 buildFilter.attr( 'placeholder', '' ).addClass( tscss.filterDisabled )[0].disabled = true;
1190 }
1191 }
1192 }
1193 }
1194 },
1195 bindSearch: function( table, $el, internal ) {
1196 table = $( table )[0];
1197 $el = $( $el ); // allow passing a selector string
1198 if ( !$el.length ) { return; }
1199 var tmp,
1200 c = table.config,
1201 wo = c.widgetOptions,
1202 namespace = c.namespace + 'filter',
1203 $ext = wo.filter_$externalFilters;
1204 if ( internal !== true ) {
1205 // save anyMatch element
1206 tmp = wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector;
1207 wo.filter_$anyMatch = $el.filter( tmp );
1208 if ( $ext && $ext.length ) {
1209 wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el );
1210 } else {
1211 wo.filter_$externalFilters = $el;
1212 }
1213 // update values ( external filters added after table initialization )
1214 ts.setFilters( table, c.$table.data( 'lastSearch' ) || [], internal === false );
1215 }
1216 // unbind events
1217 tmp = ( 'keypress keyup keydown search change input '.split( ' ' ).join( namespace + ' ' ) );
1218 $el
1219 // use data attribute instead of jQuery data since the head is cloned without including
1220 // the data/binding
1221 .attr( 'data-lastSearchTime', new Date().getTime() )
1222 .off( tmp.replace( ts.regex.spaces, ' ' ) )
1223 .on( 'keydown' + namespace, function( event ) {
1224 if ( event.which === tskeyCodes.escape && !table.config.widgetOptions.filter_resetOnEsc ) {
1225 // prevent keypress event
1226 return false;
1227 }
1228 })
1229 .on( 'keyup' + namespace, function( event ) {
1230 wo = table.config.widgetOptions; // make sure "wo" isn't cached
1231 var column = parseInt( $( this ).attr( 'data-column' ), 10 ),
1232 liveSearch = typeof wo.filter_liveSearch === 'boolean' ? wo.filter_liveSearch :
1233 ts.getColumnData( table, wo.filter_liveSearch, column );
1234 if ( typeof liveSearch === 'undefined' ) {
1235 liveSearch = wo.filter_liveSearch.fallback || false;
1236 }
1237 $( this ).attr( 'data-lastSearchTime', new Date().getTime() );
1238 // emulate what webkit does.... escape clears the filter
1239 if ( event.which === tskeyCodes.escape ) {
1240 // make sure to restore the last value on escape
1241 this.value = wo.filter_resetOnEsc ? '' : c.lastSearch[column];
1242 // don't return if the search value is empty ( all rows need to be revealed )
1243 } else if ( this.value !== '' && (
1244 // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace
1245 ( typeof liveSearch === 'number' && this.value.length < liveSearch ) ||
1246 // let return & backspace continue on, but ignore arrows & non-valid characters
1247 ( event.which !== tskeyCodes.enter && event.which !== tskeyCodes.backSpace &&
1248 ( event.which < tskeyCodes.space || ( event.which >= tskeyCodes.left && event.which <= tskeyCodes.down ) ) ) ) ) {
1249 return;
1250 // live search
1251 } else if ( liveSearch === false ) {
1252 if ( this.value !== '' && event.which !== tskeyCodes.enter ) {
1253 return;
1254 }
1255 }
1256 // change event = no delay; last true flag tells getFilters to skip newest timed input
1257 tsf.searching( table, true, true, column );
1258 })
1259 // include change for select - fixes #473
1260 .on( 'search change keypress input blur '.split( ' ' ).join( namespace + ' ' ), function( event ) {
1261 // don't get cached data, in case data-column changes dynamically
1262 var column = parseInt( $( this ).attr( 'data-column' ), 10 ),
1263 eventType = event.type,
1264 liveSearch = typeof wo.filter_liveSearch === 'boolean' ?
1265 wo.filter_liveSearch :
1266 ts.getColumnData( table, wo.filter_liveSearch, column );
1267 if ( table.config.widgetOptions.filter_initialized &&
1268 // immediate search if user presses enter
1269 ( event.which === tskeyCodes.enter ||
1270 // immediate search if a "search" or "blur" is triggered on the input
1271 ( eventType === 'search' || eventType === 'blur' ) ||
1272 // change & input events must be ignored if liveSearch !== true
1273 ( eventType === 'change' || eventType === 'input' ) &&
1274 // prevent search if liveSearch is a number
1275 ( liveSearch === true || liveSearch !== true && event.target.nodeName !== 'INPUT' ) &&
1276 // don't allow 'change' or 'input' event to process if the input value
1277 // is the same - fixes #685
1278 this.value !== c.lastSearch[column]
1279 )
1280 ) {
1281 event.preventDefault();
1282 // init search with no delay
1283 $( this ).attr( 'data-lastSearchTime', new Date().getTime() );
1284 tsf.searching( table, eventType !== 'keypress' || event.which === tskeyCodes.enter, true, column );
1285 }
1286 });
1287 },
1288 searching: function( table, filter, skipFirst, column ) {
1289 var liveSearch,
1290 wo = table.config.widgetOptions;
1291 if (typeof column === 'undefined') {
1292 // no delay
1293 liveSearch = false;
1294 } else {
1295 liveSearch = typeof wo.filter_liveSearch === 'boolean' ?
1296 wo.filter_liveSearch :
1297 // get column setting, or set to fallback value, or default to false
1298 ts.getColumnData( table, wo.filter_liveSearch, column );
1299 if ( typeof liveSearch === 'undefined' ) {
1300 liveSearch = wo.filter_liveSearch.fallback || false;
1301 }
1302 }
1303 clearTimeout( wo.filter_searchTimer );
1304 if ( typeof filter === 'undefined' || filter === true ) {
1305 // delay filtering
1306 wo.filter_searchTimer = setTimeout( function() {
1307 tsf.checkFilters( table, filter, skipFirst );
1308 }, liveSearch ? wo.filter_searchDelay : 10 );
1309 } else {
1310 // skip delay
1311 tsf.checkFilters( table, filter, skipFirst );
1312 }
1313 },
1314 equalFilters: function (c, filter1, filter2) {
1315 var indx,
1316 f1 = [],
1317 f2 = [],
1318 len = c.columns + 1; // add one to include anyMatch filter
1319 filter1 = Array.isArray(filter1) ? filter1 : [];
1320 filter2 = Array.isArray(filter2) ? filter2 : [];
1321 for (indx = 0; indx < len; indx++) {
1322 f1[indx] = filter1[indx] || '';
1323 f2[indx] = filter2[indx] || '';
1324 }
1325 return f1.join(',') === f2.join(',');
1326 },
1327 checkFilters: function( table, filter, skipFirst ) {
1328 var c = table.config,
1329 wo = c.widgetOptions,
1330 filterArray = Array.isArray( filter ),
1331 filters = ( filterArray ) ? filter : ts.getFilters( table, true ),
1332 currentFilters = filters || []; // current filter values
1333 // prevent errors if delay init is set
1334 if ( $.isEmptyObject( c.cache ) ) {
1335 // update cache if delayInit set & pager has initialized ( after user initiates a search )
1336 if ( c.delayInit && ( !c.pager || c.pager && c.pager.initialized ) ) {
1337 ts.updateCache( c, function() {
1338 tsf.checkFilters( table, false, skipFirst );
1339 });
1340 }
1341 return;
1342 }
1343 // add filter array back into inputs
1344 if ( filterArray ) {
1345 ts.setFilters( table, filters, false, skipFirst !== true );
1346 if ( !wo.filter_initialized ) {
1347 c.lastSearch = [];
1348 c.lastCombinedFilter = '';
1349 }
1350 }
1351 if ( wo.filter_hideFilters ) {
1352 // show/hide filter row as needed
1353 c.$table
1354 .find( '.' + tscss.filterRow )
1355 .triggerHandler( tsf.hideFiltersCheck( c ) ? 'mouseleave' : 'mouseenter' );
1356 }
1357 // return if the last search is the same; but filter === false when updating the search
1358 // see example-widget-filter.html filter toggle buttons
1359 if ( tsf.equalFilters(c, c.lastSearch, currentFilters) ) {
1360 if ( filter !== false ) {
1361 return;
1362 } else {
1363 // force filter refresh
1364 c.lastCombinedFilter = '';
1365 c.lastSearch = [];
1366 }
1367 }
1368 // define filter inside it is false
1369 filters = filters || [];
1370 // convert filters to strings - see #1070
1371 filters = Array.prototype.map ?
1372 filters.map( String ) :
1373 // for IE8 & older browsers - maybe not the best method
1374 filters.join( '\ufffd' ).split( '\ufffd' );
1375
1376 if ( wo.filter_initialized ) {
1377 c.$table.triggerHandler( 'filterStart', [ filters ] );
1378 }
1379 if ( c.showProcessing ) {
1380 // give it time for the processing icon to kick in
1381 setTimeout( function() {
1382 tsf.findRows( table, filters, currentFilters );
1383 return false;
1384 }, 30 );
1385 } else {
1386 tsf.findRows( table, filters, currentFilters );
1387 return false;
1388 }
1389 },
1390 hideFiltersCheck: function( c ) {
1391 if (typeof c.widgetOptions.filter_hideFilters === 'function') {
1392 var val = c.widgetOptions.filter_hideFilters( c );
1393 if (typeof val === 'boolean') {
1394 return val;
1395 }
1396 }
1397 return ts.getFilters( c.$table ).join( '' ) === '';
1398 },
1399 hideFilters: function( c, $table ) {
1400 var timer;
1401 ( $table || c.$table )
1402 .find( '.' + tscss.filterRow )
1403 .addClass( tscss.filterRowHide )
1404 .on( 'mouseenter mouseleave', function( e ) {
1405 // save event object - http://bugs.jquery.com/ticket/12140
1406 var event = e,
1407 $row = $( this );
1408 clearTimeout( timer );
1409 timer = setTimeout( function() {
1410 if ( /enter|over/.test( event.type ) ) {
1411 $row.removeClass( tscss.filterRowHide );
1412 } else {
1413 // don't hide if input has focus
1414 // $( ':focus' ) needs jQuery 1.6+
1415 if ( $( document.activeElement ).closest( 'tr' )[0] !== $row[0] ) {
1416 // don't hide row if any filter has a value
1417 $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) );
1418 }
1419 }
1420 }, 200 );
1421 })
1422 .find( 'input, select' ).on( 'focus blur', function( e ) {
1423 var event = e,
1424 $row = $( this ).closest( 'tr' );
1425 clearTimeout( timer );
1426 timer = setTimeout( function() {
1427 clearTimeout( timer );
1428 // don't hide row if any filter has a value
1429 $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) && event.type !== 'focus' );
1430 }, 200 );
1431 });
1432 },
1433 defaultFilter: function( filter, mask ) {
1434 if ( filter === '' ) { return filter; }
1435 var regex = tsfRegex.iQuery,
1436 maskLen = mask.match( tsfRegex.igQuery ).length,
1437 query = maskLen > 1 ? filter.trim().split( /\s/ ) : [ filter.trim() ],
1438 len = query.length - 1,
1439 indx = 0,
1440 val = mask;
1441 if ( len < 1 && maskLen > 1 ) {
1442 // only one 'word' in query but mask has >1 slots
1443 query[1] = query[0];
1444 }
1445 // replace all {query} with query words...
1446 // if query = 'Bob', then convert mask from '!{query}' to '!Bob'
1447 // if query = 'Bob Joe Frank', then convert mask '{q} OR {q}' to 'Bob OR Joe OR Frank'
1448 while ( regex.test( val ) ) {
1449 val = val.replace( regex, query[indx++] || '' );
1450 if ( regex.test( val ) && indx < len && ( query[indx] || '' ) !== '' ) {
1451 val = mask.replace( regex, val );
1452 }
1453 }
1454 return val;
1455 },
1456 getLatestSearch: function( $input ) {
1457 if ( $input ) {
1458 return $input.sort( function( a, b ) {
1459 return $( b ).attr( 'data-lastSearchTime' ) - $( a ).attr( 'data-lastSearchTime' );
1460 });
1461 }
1462 return $input || $();
1463 },
1464 findRange: function( c, val, ignoreRanges ) {
1465 // look for multiple columns '1-3,4-6,8' in data-column
1466 var temp, ranges, range, start, end, singles, i, indx, len,
1467 columns = [];
1468 if ( /^[0-9]+$/.test( val ) ) {
1469 // always return an array
1470 return [ parseInt( val, 10 ) ];
1471 }
1472 // process column range
1473 if ( !ignoreRanges && /-/.test( val ) ) {
1474 ranges = val.match( /(\d+)\s*-\s*(\d+)/g );
1475 len = ranges ? ranges.length : 0;
1476 for ( indx = 0; indx < len; indx++ ) {
1477 range = ranges[indx].split( /\s*-\s*/ );
1478 start = parseInt( range[0], 10 ) || 0;
1479 end = parseInt( range[1], 10 ) || ( c.columns - 1 );
1480 if ( start > end ) {
1481 temp = start; start = end; end = temp; // swap
1482 }
1483 if ( end >= c.columns ) {
1484 end = c.columns - 1;
1485 }
1486 for ( ; start <= end; start++ ) {
1487 columns[ columns.length ] = start;
1488 }
1489 // remove processed range from val
1490 val = val.replace( ranges[ indx ], '' );
1491 }
1492 }
1493 // process single columns
1494 if ( !ignoreRanges && /,/.test( val ) ) {
1495 singles = val.split( /\s*,\s*/ );
1496 len = singles.length;
1497 for ( i = 0; i < len; i++ ) {
1498 if ( singles[ i ] !== '' ) {
1499 indx = parseInt( singles[ i ], 10 );
1500 if ( indx < c.columns ) {
1501 columns[ columns.length ] = indx;
1502 }
1503 }
1504 }
1505 }
1506 // return all columns
1507 if ( !columns.length ) {
1508 for ( indx = 0; indx < c.columns; indx++ ) {
1509 columns[ columns.length ] = indx;
1510 }
1511 }
1512 return columns;
1513 },
1514 getColumnElm: function( c, $elements, column ) {
1515 // data-column may contain multiple columns '1-3,5-6,8'
1516 // replaces: c.$filters.filter( '[data-column="' + column + '"]' );
1517 return $elements.filter( function() {
1518 var cols = tsf.findRange( c, $( this ).attr( 'data-column' ) );
1519 return $.inArray( column, cols ) > -1;
1520 });
1521 },
1522 multipleColumns: function( c, $input ) {
1523 // look for multiple columns '1-3,4-6,8' in data-column
1524 var wo = c.widgetOptions,
1525 // only target 'all' column inputs on initialization
1526 // & don't target 'all' column inputs if they don't exist
1527 targets = wo.filter_initialized || !$input.filter( wo.filter_anyColumnSelector ).length,
1528 val = ( tsf.getLatestSearch( $input ).attr( 'data-column' ) || '' ).trim();
1529 return tsf.findRange( c, val, !targets );
1530 },
1531 processTypes: function( c, data, vars ) {
1532 var ffxn,
1533 filterMatched = null,
1534 matches = null;
1535 for ( ffxn in tsf.types ) {
1536 if ( $.inArray( ffxn, vars.excludeMatch ) < 0 && matches === null ) {
1537 matches = tsf.types[ffxn]( c, data, vars );
1538 if ( matches !== null ) {
1539 data.matchedOn = ffxn;
1540 filterMatched = matches;
1541 }
1542 }
1543 }
1544 return filterMatched;
1545 },
1546 matchType: function( c, columnIndex ) {
1547 var isMatch,
1548 wo = c.widgetOptions,
1549 $el = c.$headerIndexed[ columnIndex ];
1550 // filter-exact > filter-match > filter_matchType for type
1551 if ( $el.hasClass( 'filter-exact' ) ) {
1552 isMatch = false;
1553 } else if ( $el.hasClass( 'filter-match' ) ) {
1554 isMatch = true;
1555 } else {
1556 // filter-select is not applied when filter_functions are used, so look for a select
1557 if ( wo.filter_columnFilters ) {
1558 $el = c.$filters
1559 .find( '.' + tscss.filter )
1560 .add( wo.filter_$externalFilters )
1561 .filter( '[data-column="' + columnIndex + '"]' );
1562 } else if ( wo.filter_$externalFilters ) {
1563 $el = wo.filter_$externalFilters.filter( '[data-column="' + columnIndex + '"]' );
1564 }
1565 isMatch = $el.length ?
1566 c.widgetOptions.filter_matchType[ ( $el[ 0 ].nodeName || '' ).toLowerCase() ] === 'match' :
1567 // default to exact, if no inputs found
1568 false;
1569 }
1570 return isMatch;
1571 },
1572 processRow: function( c, data, vars ) {
1573 var result, filterMatched,
1574 fxn, ffxn, txt,
1575 wo = c.widgetOptions,
1576 showRow = true,
1577 hasAnyMatchInput = wo.filter_$anyMatch && wo.filter_$anyMatch.length,
1578
1579 // if wo.filter_$anyMatch data-column attribute is changed dynamically
1580 // we don't want to do an "anyMatch" search on one column using data
1581 // for the entire row - see #998
1582 columnIndex = wo.filter_$anyMatch && wo.filter_$anyMatch.length ?
1583 // look for multiple columns '1-3,4-6,8'
1584 tsf.multipleColumns( c, wo.filter_$anyMatch ) :
1585 [];
1586 data.$cells = data.$row.children();
1587 data.matchedOn = null;
1588 if ( data.anyMatchFlag && columnIndex.length > 1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) {
1589 data.anyMatch = true;
1590 data.isMatch = true;
1591 data.rowArray = data.$cells.map( function( i ) {
1592 if ( $.inArray( i, columnIndex ) > -1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) {
1593 if ( data.parsed[ i ] ) {
1594 txt = data.cacheArray[ i ];
1595 } else {
1596 txt = data.rawArray[ i ];
1597 txt = wo.filter_ignoreCase ? txt.toLowerCase().trim() : txt.trim();
1598 if ( c.sortLocaleCompare ) {
1599 txt = ts.replaceAccents( txt );
1600 }
1601 }
1602 return txt;
1603 }
1604 }).get();
1605 data.filter = data.anyMatchFilter;
1606 data.iFilter = data.iAnyMatchFilter;
1607 data.exact = data.rowArray.join( ' ' );
1608 data.iExact = wo.filter_ignoreCase ? data.exact.toLowerCase() : data.exact;
1609 data.cache = data.cacheArray.slice( 0, -1 ).join( ' ' );
1610 vars.excludeMatch = vars.noAnyMatch;
1611 filterMatched = tsf.processTypes( c, data, vars );
1612 if ( filterMatched !== null ) {
1613 showRow = filterMatched;
1614 } else {
1615 if ( wo.filter_startsWith ) {
1616 showRow = false;
1617 // data.rowArray may not contain all columns
1618 columnIndex = Math.min( c.columns, data.rowArray.length );
1619 while ( !showRow && columnIndex > 0 ) {
1620 columnIndex--;
1621 showRow = showRow || data.rowArray[ columnIndex ].indexOf( data.iFilter ) === 0;
1622 }
1623 } else {
1624 showRow = ( data.iExact + data.childRowText ).indexOf( data.iFilter ) >= 0;
1625 }
1626 }
1627 data.anyMatch = false;
1628 // no other filters to process
1629 if ( data.filters.join( '' ) === data.filter ) {
1630 return showRow;
1631 }
1632 }
1633
1634 for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) {
1635 data.filter = data.filters[ columnIndex ];
1636 data.index = columnIndex;
1637
1638 // filter types to exclude, per column
1639 vars.excludeMatch = vars.excludeFilter[ columnIndex ];
1640
1641 // ignore if filter is empty or disabled
1642 if ( data.filter ) {
1643 data.cache = data.cacheArray[ columnIndex ];
1644 result = data.parsed[ columnIndex ] ? data.cache : data.rawArray[ columnIndex ] || '';
1645 data.exact = c.sortLocaleCompare ? ts.replaceAccents( result ) : result; // issue #405
1646 data.iExact = !tsfRegex.type.test( typeof data.exact ) && wo.filter_ignoreCase ?
1647 data.exact.toLowerCase() : data.exact;
1648 data.isMatch = tsf.matchType( c, columnIndex );
1649
1650 result = showRow; // if showRow is true, show that row
1651
1652 // in case select filter option has a different value vs text 'a - z|A through Z'
1653 ffxn = wo.filter_columnFilters ?
1654 c.$filters.add( wo.filter_$externalFilters )
1655 .filter( '[data-column="' + columnIndex + '"]' )
1656 .find( 'select option:selected' )
1657 .attr( 'data-function-name' ) || '' : '';
1658 // replace accents - see #357
1659 if ( c.sortLocaleCompare ) {
1660 data.filter = ts.replaceAccents( data.filter );
1661 }
1662
1663 // replace column specific default filters - see #1088
1664 if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultColFilter[ columnIndex ] ) ) {
1665 data.filter = tsf.defaultFilter( data.filter, vars.defaultColFilter[ columnIndex ] );
1666 }
1667
1668 // data.iFilter = case insensitive ( if wo.filter_ignoreCase is true ),
1669 // data.filter = case sensitive
1670 data.iFilter = wo.filter_ignoreCase ? ( data.filter || '' ).toLowerCase() : data.filter;
1671 fxn = vars.functions[ columnIndex ];
1672 filterMatched = null;
1673 if ( fxn ) {
1674 if ( typeof fxn === 'function' ) {
1675 // filter callback( exact cell content, parser normalized content,
1676 // filter input value, column index, jQuery row object )
1677 filterMatched = fxn( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data );
1678 } else if ( typeof fxn[ ffxn || data.filter ] === 'function' ) {
1679 // selector option function
1680 txt = ffxn || data.filter;
1681 filterMatched =
1682 fxn[ txt ]( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data );
1683 }
1684 }
1685 if ( filterMatched === null ) {
1686 // cycle through the different filters
1687 // filters return a boolean or null if nothing matches
1688 filterMatched = tsf.processTypes( c, data, vars );
1689 // select with exact match; ignore "and" or "or" within the text; fixes #1486
1690 txt = fxn === true && (data.matchedOn === 'and' || data.matchedOn === 'or');
1691 if ( filterMatched !== null && !txt) {
1692 result = filterMatched;
1693 // Look for match, and add child row data for matching
1694 } else {
1695 // check fxn (filter-select in header) after filter types are checked
1696 // without this, the filter + jQuery UI selectmenu demo was breaking
1697 if ( fxn === true ) {
1698 // default selector uses exact match unless 'filter-match' class is found
1699 result = data.isMatch ?
1700 // data.iExact may be a number
1701 ( '' + data.iExact ).search( data.iFilter ) >= 0 :
1702 data.filter === data.exact;
1703 } else {
1704 txt = ( data.iExact + data.childRowText ).indexOf( tsf.parseFilter( c, data.iFilter, data ) );
1705 result = ( ( !wo.filter_startsWith && txt >= 0 ) || ( wo.filter_startsWith && txt === 0 ) );
1706 }
1707 }
1708 } else {
1709 result = filterMatched;
1710 }
1711 showRow = ( result ) ? showRow : false;
1712 }
1713 }
1714 return showRow;
1715 },
1716 findRows: function( table, filters, currentFilters ) {
1717 if (
1718 tsf.equalFilters(table.config, table.config.lastSearch, currentFilters) ||
1719 !table.config.widgetOptions.filter_initialized
1720 ) {
1721 return;
1722 }
1723 var len, norm_rows, rowData, $rows, $row, rowIndex, tbodyIndex, $tbody, columnIndex,
1724 isChild, childRow, lastSearch, showRow, showParent, time, val, indx,
1725 notFiltered, searchFiltered, query, injected, res, id, txt,
1726 storedFilters = $.extend( [], filters ),
1727 c = table.config,
1728 wo = c.widgetOptions,
1729 debug = ts.debug(c, 'filter'),
1730 // data object passed to filters; anyMatch is a flag for the filters
1731 data = {
1732 anyMatch: false,
1733 filters: filters,
1734 // regex filter type cache
1735 filter_regexCache : []
1736 },
1737 vars = {
1738 // anyMatch really screws up with these types of filters
1739 noAnyMatch: [ 'range', 'operators' ],
1740 // cache filter variables that use ts.getColumnData in the main loop
1741 functions : [],
1742 excludeFilter : [],
1743 defaultColFilter : [],
1744 defaultAnyFilter : ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || ''
1745 };
1746 // parse columns after formatter, in case the class is added at that point
1747 data.parsed = [];
1748 for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) {
1749 data.parsed[ columnIndex ] = wo.filter_useParsedData ||
1750 // parser has a "parsed" parameter
1751 ( c.parsers && c.parsers[ columnIndex ] && c.parsers[ columnIndex ].parsed ||
1752 // getData may not return 'parsed' if other 'filter-' class names exist
1753 // ( e.g. <th class="filter-select filter-parsed"> )
1754 ts.getData && ts.getData( c.$headerIndexed[ columnIndex ],
1755 ts.getColumnData( table, c.headers, columnIndex ), 'filter' ) === 'parsed' ||
1756 c.$headerIndexed[ columnIndex ].hasClass( 'filter-parsed' ) );
1757
1758 vars.functions[ columnIndex ] =
1759 ts.getColumnData( table, wo.filter_functions, columnIndex ) ||
1760 c.$headerIndexed[ columnIndex ].hasClass( 'filter-select' );
1761 vars.defaultColFilter[ columnIndex ] =
1762 ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || '';
1763 vars.excludeFilter[ columnIndex ] =
1764 ( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split( /\s+/ );
1765 }
1766
1767 if ( debug ) {
1768 console.log( 'Filter >> Starting filter widget search', filters );
1769 time = new Date();
1770 }
1771 // filtered rows count
1772 c.filteredRows = 0;
1773 c.totalRows = 0;
1774 currentFilters = ( storedFilters || [] );
1775
1776 for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) {
1777 $tbody = ts.processTbody( table, c.$tbodies.eq( tbodyIndex ), true );
1778 // skip child rows & widget added ( removable ) rows - fixes #448 thanks to @hempel!
1779 // $rows = $tbody.children( 'tr' ).not( c.selectorRemove );
1780 columnIndex = c.columns;
1781 // convert stored rows into a jQuery object
1782 norm_rows = c.cache[ tbodyIndex ].normalized;
1783 $rows = $( $.map( norm_rows, function( el ) {
1784 return el[ columnIndex ].$row.get();
1785 }) );
1786
1787 if ( currentFilters.join('') === '' || wo.filter_serversideFiltering ) {
1788 $rows
1789 .removeClass( wo.filter_filteredRow )
1790 .not( '.' + c.cssChildRow )
1791 .css( 'display', '' );
1792 } else {
1793 // filter out child rows
1794 $rows = $rows.not( '.' + c.cssChildRow );
1795 len = $rows.length;
1796
1797 if ( ( wo.filter_$anyMatch && wo.filter_$anyMatch.length ) ||
1798 typeof filters[c.columns] !== 'undefined' ) {
1799 data.anyMatchFlag = true;
1800 data.anyMatchFilter = '' + (
1801 filters[ c.columns ] ||
1802 wo.filter_$anyMatch && tsf.getLatestSearch( wo.filter_$anyMatch ).val() ||
1803 ''
1804 );
1805 if ( wo.filter_columnAnyMatch ) {
1806 // specific columns search
1807 query = data.anyMatchFilter.split( tsfRegex.andSplit );
1808 injected = false;
1809 for ( indx = 0; indx < query.length; indx++ ) {
1810 res = query[ indx ].split( ':' );
1811 if ( res.length > 1 ) {
1812 // make the column a one-based index ( non-developers start counting from one :P )
1813 if ( isNaN( res[0] ) ) {
1814 $.each( c.headerContent, function( i, txt ) {
1815 // multiple matches are possible
1816 if ( txt.toLowerCase().indexOf( res[0] ) > -1 ) {
1817 id = i;
1818 filters[ id ] = res[1];
1819 }
1820 });
1821 } else {
1822 id = parseInt( res[0], 10 ) - 1;
1823 }
1824 if ( id >= 0 && id < c.columns ) { // if id is an integer
1825 filters[ id ] = res[1];
1826 query.splice( indx, 1 );
1827 indx--;
1828 injected = true;
1829 }
1830 }
1831 }
1832 if ( injected ) {
1833 data.anyMatchFilter = query.join( ' && ' );
1834 }
1835 }
1836 }
1837
1838 // optimize searching only through already filtered rows - see #313
1839 searchFiltered = wo.filter_searchFiltered;
1840 lastSearch = c.lastSearch || c.$table.data( 'lastSearch' ) || [];
1841 if ( searchFiltered ) {
1842 // cycle through all filters; include last ( columnIndex + 1 = match any column ). Fixes #669
1843 for ( indx = 0; indx < columnIndex + 1; indx++ ) {
1844 val = filters[indx] || '';
1845 // break out of loop if we've already determined not to search filtered rows
1846 if ( !searchFiltered ) { indx = columnIndex; }
1847 // search already filtered rows if...
1848 searchFiltered = searchFiltered && lastSearch.length &&
1849 // there are no changes from beginning of filter
1850 val.indexOf( lastSearch[indx] || '' ) === 0 &&
1851 // if there is NOT a logical 'or', or range ( 'to' or '-' ) in the string
1852 !tsfRegex.alreadyFiltered.test( val ) &&
1853 // if we are not doing exact matches, using '|' ( logical or ) or not '!'
1854 !tsfRegex.exactTest.test( val ) &&
1855 // don't search only filtered if the value is negative
1856 // ( '> -10' => '> -100' will ignore hidden rows )
1857 !( tsfRegex.isNeg1.test( val ) || tsfRegex.isNeg2.test( val ) ) &&
1858 // if filtering using a select without a 'filter-match' class ( exact match ) - fixes #593
1859 !( val !== '' && c.$filters && c.$filters.filter( '[data-column="' + indx + '"]' ).find( 'select' ).length &&
1860 !tsf.matchType( c, indx ) );
1861 }
1862 }
1863 notFiltered = $rows.not( '.' + wo.filter_filteredRow ).length;
1864 // can't search when all rows are hidden - this happens when looking for exact matches
1865 if ( searchFiltered && notFiltered === 0 ) { searchFiltered = false; }
1866 if ( debug ) {
1867 console.log( 'Filter >> Searching through ' +
1868 ( searchFiltered && notFiltered < len ? notFiltered : 'all' ) + ' rows' );
1869 }
1870 if ( data.anyMatchFlag ) {
1871 if ( c.sortLocaleCompare ) {
1872 // replace accents
1873 data.anyMatchFilter = ts.replaceAccents( data.anyMatchFilter );
1874 }
1875 if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultAnyFilter ) ) {
1876 data.anyMatchFilter = tsf.defaultFilter( data.anyMatchFilter, vars.defaultAnyFilter );
1877 // clear search filtered flag because default filters are not saved to the last search
1878 searchFiltered = false;
1879 }
1880 // make iAnyMatchFilter lowercase unless both filter widget & core ignoreCase options are true
1881 // when c.ignoreCase is true, the cache contains all lower case data
1882 data.iAnyMatchFilter = !( wo.filter_ignoreCase && c.ignoreCase ) ?
1883 data.anyMatchFilter :
1884 data.anyMatchFilter.toLowerCase();
1885 }
1886
1887 // loop through the rows
1888 for ( rowIndex = 0; rowIndex < len; rowIndex++ ) {
1889
1890 txt = $rows[ rowIndex ].className;
1891 // the first row can never be a child row
1892 isChild = rowIndex && tsfRegex.child.test( txt );
1893 // skip child rows & already filtered rows
1894 if ( isChild || ( searchFiltered && tsfRegex.filtered.test( txt ) ) ) {
1895 continue;
1896 }
1897
1898 data.$row = $rows.eq( rowIndex );
1899 data.rowIndex = rowIndex;
1900 data.cacheArray = norm_rows[ rowIndex ];
1901 rowData = data.cacheArray[ c.columns ];
1902 data.rawArray = rowData.raw;
1903 data.childRowText = '';
1904
1905 if ( !wo.filter_childByColumn ) {
1906 txt = '';
1907 // child row cached text
1908 childRow = rowData.child;
1909 // so, if 'table.config.widgetOptions.filter_childRows' is true and there is
1910 // a match anywhere in the child row, then it will make the row visible
1911 // checked here so the option can be changed dynamically
1912 for ( indx = 0; indx < childRow.length; indx++ ) {
1913 txt += ' ' + childRow[indx].join( ' ' ) || '';
1914 }
1915 data.childRowText = wo.filter_childRows ?
1916 ( wo.filter_ignoreCase ? txt.toLowerCase() : txt ) :
1917 '';
1918 }
1919
1920 showRow = false;
1921 showParent = tsf.processRow( c, data, vars );
1922 $row = rowData.$row;
1923
1924 // don't pass reference to val
1925 val = showParent ? true : false;
1926 childRow = rowData.$row.filter( ':gt(0)' );
1927 if ( wo.filter_childRows && childRow.length ) {
1928 if ( wo.filter_childByColumn ) {
1929 if ( !wo.filter_childWithSibs ) {
1930 // hide all child rows
1931 childRow.addClass( wo.filter_filteredRow );
1932 // if only showing resulting child row, only include parent
1933 $row = $row.eq( 0 );
1934 }
1935 // cycle through each child row
1936 for ( indx = 0; indx < childRow.length; indx++ ) {
1937 data.$row = childRow.eq( indx );
1938 data.cacheArray = rowData.child[ indx ];
1939 data.rawArray = data.cacheArray;
1940 val = tsf.processRow( c, data, vars );
1941 // use OR comparison on child rows
1942 showRow = showRow || val;
1943 if ( !wo.filter_childWithSibs && val ) {
1944 childRow.eq( indx ).removeClass( wo.filter_filteredRow );
1945 }
1946 }
1947 }
1948 // keep parent row match even if no child matches... see #1020
1949 showRow = showRow || showParent;
1950 } else {
1951 showRow = val;
1952 }
1953 $row
1954 .toggleClass( wo.filter_filteredRow, !showRow )[0]
1955 .display = showRow ? '' : 'none';
1956 }
1957 }
1958 c.filteredRows += $rows.not( '.' + wo.filter_filteredRow ).length;
1959 c.totalRows += $rows.length;
1960 ts.processTbody( table, $tbody, false );
1961 }
1962 // lastCombinedFilter is no longer used internally
1963 c.lastCombinedFilter = storedFilters.join(''); // save last search
1964 // don't save 'filters' directly since it may have altered ( AnyMatch column searches )
1965 c.lastSearch = storedFilters;
1966 c.$table.data( 'lastSearch', storedFilters );
1967 if ( wo.filter_saveFilters && ts.storage ) {
1968 ts.storage( table, 'tablesorter-filters', tsf.processFilters( storedFilters, true ) );
1969 }
1970 if ( debug ) {
1971 console.log( 'Filter >> Completed search' + ts.benchmark(time) );
1972 }
1973 if ( wo.filter_initialized ) {
1974 c.$table.triggerHandler( 'filterBeforeEnd', c );
1975 c.$table.triggerHandler( 'filterEnd', c );
1976 }
1977 setTimeout( function() {
1978 ts.applyWidget( c.table ); // make sure zebra widget is applied
1979 }, 0 );
1980 },
1981 getOptionSource: function( table, column, onlyAvail ) {
1982 table = $( table )[0];
1983 var c = table.config,
1984 wo = c.widgetOptions,
1985 arry = false,
1986 source = wo.filter_selectSource,
1987 last = c.$table.data( 'lastSearch' ) || [],
1988 fxn = typeof source === 'function' ? true : ts.getColumnData( table, source, column );
1989
1990 if ( onlyAvail && last[column] !== '' ) {
1991 onlyAvail = false;
1992 }
1993
1994 // filter select source option
1995 if ( fxn === true ) {
1996 // OVERALL source
1997 arry = source( table, column, onlyAvail );
1998 } else if ( fxn instanceof $ || ( $.type( fxn ) === 'string' && fxn.indexOf( '</option>' ) >= 0 ) ) {
1999 // selectSource is a jQuery object or string of options
2000 return fxn;
2001 } else if ( Array.isArray( fxn ) ) {
2002 arry = fxn;
2003 } else if ( $.type( source ) === 'object' && fxn ) {
2004 // custom select source function for a SPECIFIC COLUMN
2005 arry = fxn( table, column, onlyAvail );
2006 // abort - updating the selects from an external method
2007 if (arry === null) {
2008 return null;
2009 }
2010 }
2011 if ( arry === false ) {
2012 // fall back to original method
2013 arry = tsf.getOptions( table, column, onlyAvail );
2014 }
2015
2016 return tsf.processOptions( table, column, arry );
2017
2018 },
2019 processOptions: function( table, column, arry ) {
2020 if ( !Array.isArray( arry ) ) {
2021 return false;
2022 }
2023 table = $( table )[0];
2024 var cts, txt, indx, len, parsedTxt, str,
2025 c = table.config,
2026 validColumn = typeof column !== 'undefined' && column !== null && column >= 0 && column < c.columns,
2027 direction = validColumn ? c.$headerIndexed[ column ].hasClass( 'filter-select-sort-desc' ) : false,
2028 parsed = [];
2029 // get unique elements and sort the list
2030 // if $.tablesorter.sortText exists ( not in the original tablesorter ),
2031 // then natural sort the list otherwise use a basic sort
2032 arry = $.grep( arry, function( value, indx ) {
2033 if ( value.text ) {
2034 return true;
2035 }
2036 return $.inArray( value, arry ) === indx;
2037 });
2038 if ( validColumn && c.$headerIndexed[ column ].hasClass( 'filter-select-nosort' ) ) {
2039 // unsorted select options
2040 return arry;
2041 } else {
2042 len = arry.length;
2043 // parse select option values
2044 for ( indx = 0; indx < len; indx++ ) {
2045 txt = arry[ indx ];
2046 // check for object
2047 str = txt.text ? txt.text : txt;
2048 // sortNatural breaks if you don't pass it strings
2049 parsedTxt = ( validColumn && c.parsers && c.parsers.length &&
2050 c.parsers[ column ].format( str, table, [], column ) || str ).toString();
2051 parsedTxt = c.widgetOptions.filter_ignoreCase ? parsedTxt.toLowerCase() : parsedTxt;
2052 // parse array data using set column parser; this DOES NOT pass the original
2053 // table cell to the parser format function
2054 if ( txt.text ) {
2055 txt.parsed = parsedTxt;
2056 parsed[ parsed.length ] = txt;
2057 } else {
2058 parsed[ parsed.length ] = {
2059 text : txt,
2060 // check parser length - fixes #934
2061 parsed : parsedTxt
2062 };
2063 }
2064 }
2065 // sort parsed select options
2066 cts = c.textSorter || '';
2067 parsed.sort( function( a, b ) {
2068 var x = direction ? b.parsed : a.parsed,
2069 y = direction ? a.parsed : b.parsed;
2070 if ( validColumn && typeof cts === 'function' ) {
2071 // custom OVERALL text sorter
2072 return cts( x, y, true, column, table );
2073 } else if ( validColumn && typeof cts === 'object' && cts.hasOwnProperty( column ) ) {
2074 // custom text sorter for a SPECIFIC COLUMN
2075 return cts[column]( x, y, true, column, table );
2076 } else if ( ts.sortNatural ) {
2077 // fall back to natural sort
2078 return ts.sortNatural( x, y );
2079 }
2080 // using an older version! do a basic sort
2081 return true;
2082 });
2083 // rebuild arry from sorted parsed data
2084 arry = [];
2085 len = parsed.length;
2086 for ( indx = 0; indx < len; indx++ ) {
2087 arry[ arry.length ] = parsed[indx];
2088 }
2089 return arry;
2090 }
2091 },
2092 getOptions: function( table, column, onlyAvail ) {
2093 table = $( table )[0];
2094 var rowIndex, tbodyIndex, len, row, cache, indx, child, childLen,
2095 c = table.config,
2096 wo = c.widgetOptions,
2097 arry = [];
2098 for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) {
2099 cache = c.cache[tbodyIndex];
2100 len = c.cache[tbodyIndex].normalized.length;
2101 // loop through the rows
2102 for ( rowIndex = 0; rowIndex < len; rowIndex++ ) {
2103 // get cached row from cache.row ( old ) or row data object
2104 // ( new; last item in normalized array )
2105 row = cache.row ?
2106 cache.row[ rowIndex ] :
2107 cache.normalized[ rowIndex ][ c.columns ].$row[0];
2108 // check if has class filtered
2109 if ( onlyAvail && row.className.match( wo.filter_filteredRow ) ) {
2110 continue;
2111 }
2112 // get non-normalized cell content
2113 if ( wo.filter_useParsedData ||
2114 c.parsers[column].parsed ||
2115 c.$headerIndexed[column].hasClass( 'filter-parsed' ) ) {
2116 arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ column ];
2117 // child row parsed data
2118 if ( wo.filter_childRows && wo.filter_childByColumn ) {
2119 childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length - 1;
2120 for ( indx = 0; indx < childLen; indx++ ) {
2121 arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ c.columns ].child[ indx ][ column ];
2122 }
2123 }
2124 } else {
2125 // get raw cached data instead of content directly from the cells
2126 arry[ arry.length ] = cache.normalized[ rowIndex ][ c.columns ].raw[ column ];
2127 // child row unparsed data
2128 if ( wo.filter_childRows && wo.filter_childByColumn ) {
2129 childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length;
2130 for ( indx = 1; indx < childLen; indx++ ) {
2131 child = cache.normalized[ rowIndex ][ c.columns ].$row.eq( indx ).children().eq( column );
2132 arry[ arry.length ] = '' + ts.getElementText( c, child, column );
2133 }
2134 }
2135 }
2136 }
2137 }
2138 return arry;
2139 },
2140 buildSelect: function( table, column, arry, updating, onlyAvail ) {
2141 table = $( table )[0];
2142 column = parseInt( column, 10 );
2143 if ( !table.config.cache || $.isEmptyObject( table.config.cache ) ) {
2144 return;
2145 }
2146
2147 var indx, val, txt, t, $filters, $filter, option,
2148 c = table.config,
2149 wo = c.widgetOptions,
2150 node = c.$headerIndexed[ column ],
2151 // t.data( 'placeholder' ) won't work in jQuery older than 1.4.3
2152 options = '<option value="">' +
2153 ( node.data( 'placeholder' ) ||
2154 node.attr( 'data-placeholder' ) ||
2155 wo.filter_placeholder.select || ''
2156 ) + '</option>',
2157 // Get curent filter value
2158 currentValue = c.$table
2159 .find( 'thead' )
2160 .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' )
2161 .val();
2162
2163 // nothing included in arry ( external source ), so get the options from
2164 // filter_selectSource or column data
2165 if ( typeof arry === 'undefined' || arry === '' ) {
2166 arry = tsf.getOptionSource( table, column, onlyAvail );
2167 // abort, selects are updated by an external method
2168 if (arry === null) {
2169 return;
2170 }
2171 }
2172
2173 if ( Array.isArray( arry ) ) {
2174 // build option list
2175 for ( indx = 0; indx < arry.length; indx++ ) {
2176 option = arry[ indx ];
2177 if ( option.text ) {
2178 // OBJECT!! add data-function-name in case the value is set in filter_functions
2179 option['data-function-name'] = typeof option.value === 'undefined' ? option.text : option.value;
2180
2181 // support jQuery < v1.8, otherwise the below code could be shortened to
2182 // options += $( '<option>', option )[ 0 ].outerHTML;
2183 options += '<option';
2184 for ( val in option ) {
2185 if ( option.hasOwnProperty( val ) && val !== 'text' ) {
2186 options += ' ' + val + '="' + option[ val ].replace( tsfRegex.quote, '&quot;' ) + '"';
2187 }
2188 }
2189 if ( !option.value ) {
2190 options += ' value="' + option.text.replace( tsfRegex.quote, '&quot;' ) + '"';
2191 }
2192 options += '>' + option.text.replace( tsfRegex.quote, '&quot;' ) + '</option>';
2193 // above code is needed in jQuery < v1.8
2194
2195 // make sure we don't turn an object into a string (objects without a "text" property)
2196 } else if ( '' + option !== '[object Object]' ) {
2197 txt = option = ( '' + option ).replace( tsfRegex.quote, '&quot;' );
2198 val = txt;
2199 // allow including a symbol in the selectSource array
2200 // 'a-z|A through Z' so that 'a-z' becomes the option value
2201 // and 'A through Z' becomes the option text
2202 if ( txt.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) {
2203 t = txt.split( wo.filter_selectSourceSeparator );
2204 val = t[0];
2205 txt = t[1];
2206 }
2207 // replace quotes - fixes #242 & ignore empty strings
2208 // see http://stackoverflow.com/q/14990971/145346
2209 options += option !== '' ?
2210 '<option ' +
2211 ( val === txt ? '' : 'data-function-name="' + option + '" ' ) +
2212 'value="' + val + '">' + txt +
2213 '</option>' : '';
2214 }
2215 }
2216 // clear arry so it doesn't get appended twice
2217 arry = [];
2218 }
2219
2220 // update all selects in the same column ( clone thead in sticky headers &
2221 // any external selects ) - fixes 473
2222 $filters = ( c.$filters ? c.$filters : c.$table.children( 'thead' ) )
2223 .find( '.' + tscss.filter );
2224 if ( wo.filter_$externalFilters ) {
2225 $filters = $filters && $filters.length ?
2226 $filters.add( wo.filter_$externalFilters ) :
2227 wo.filter_$externalFilters;
2228 }
2229 $filter = $filters.filter( 'select[data-column="' + column + '"]' );
2230
2231 // make sure there is a select there!
2232 if ( $filter.length ) {
2233 $filter[ updating ? 'html' : 'append' ]( options );
2234 if ( !Array.isArray( arry ) ) {
2235 // append options if arry is provided externally as a string or jQuery object
2236 // options ( default value ) was already added
2237 $filter.append( arry ).val( currentValue );
2238 }
2239 $filter.val( currentValue );
2240 }
2241 },
2242 buildDefault: function( table, updating ) {
2243 var columnIndex, $header, noSelect,
2244 c = table.config,
2245 wo = c.widgetOptions,
2246 columns = c.columns;
2247 // build default select dropdown
2248 for ( columnIndex = 0; columnIndex < columns; columnIndex++ ) {
2249 $header = c.$headerIndexed[columnIndex];
2250 noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) );
2251 // look for the filter-select class; build/update it if found
2252 if ( ( $header.hasClass( 'filter-select' ) ||
2253 ts.getColumnData( table, wo.filter_functions, columnIndex ) === true ) && noSelect ) {
2254 tsf.buildSelect( table, columnIndex, '', updating, $header.hasClass( wo.filter_onlyAvail ) );
2255 }
2256 }
2257 }
2258 };
2259
2260 // filter regex variable
2261 tsfRegex = tsf.regex;
2262
2263 ts.getFilters = function( table, getRaw, setFilters, skipFirst ) {
2264 var i, $filters, $column, cols,
2265 filters = [],
2266 c = table ? $( table )[0].config : '',
2267 wo = c ? c.widgetOptions : '';
2268 if ( ( getRaw !== true && wo && !wo.filter_columnFilters ) ||
2269 // setFilters called, but last search is exactly the same as the current
2270 // fixes issue #733 & #903 where calling update causes the input values to reset
2271 ( Array.isArray(setFilters) && tsf.equalFilters(c, setFilters, c.lastSearch) )
2272 ) {
2273 return $( table ).data( 'lastSearch' ) || [];
2274 }
2275 if ( c ) {
2276 if ( c.$filters ) {
2277 $filters = c.$filters.find( '.' + tscss.filter );
2278 }
2279 if ( wo.filter_$externalFilters ) {
2280 $filters = $filters && $filters.length ?
2281 $filters.add( wo.filter_$externalFilters ) :
2282 wo.filter_$externalFilters;
2283 }
2284 if ( $filters && $filters.length ) {
2285 filters = setFilters || [];
2286 for ( i = 0; i < c.columns + 1; i++ ) {
2287 cols = ( i === c.columns ?
2288 // 'all' columns can now include a range or set of columms ( data-column='0-2,4,6-7' )
2289 wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector :
2290 '[data-column="' + i + '"]' );
2291 $column = $filters.filter( cols );
2292 if ( $column.length ) {
2293 // move the latest search to the first slot in the array
2294 $column = tsf.getLatestSearch( $column );
2295 if ( Array.isArray( setFilters ) ) {
2296 // skip first ( latest input ) to maintain cursor position while typing
2297 if ( skipFirst && $column.length > 1 ) {
2298 $column = $column.slice( 1 );
2299 }
2300 if ( i === c.columns ) {
2301 // prevent data-column='all' from filling data-column='0,1' ( etc )
2302 cols = $column.filter( wo.filter_anyColumnSelector );
2303 $column = cols.length ? cols : $column;
2304 }
2305 $column
2306 .val( setFilters[ i ] )
2307 // must include a namespace here; but not c.namespace + 'filter'?
2308 .trigger( 'change' + c.namespace );
2309 } else {
2310 filters[i] = $column.val() || '';
2311 // don't change the first... it will move the cursor
2312 if ( i === c.columns ) {
2313 // don't update range columns from 'all' setting
2314 $column
2315 .slice( 1 )
2316 .filter( '[data-column*="' + $column.attr( 'data-column' ) + '"]' )
2317 .val( filters[ i ] );
2318 } else {
2319 $column
2320 .slice( 1 )
2321 .val( filters[ i ] );
2322 }
2323 }
2324 // save any match input dynamically
2325 if ( i === c.columns && $column.length ) {
2326 wo.filter_$anyMatch = $column;
2327 }
2328 }
2329 }
2330 }
2331 }
2332 return filters;
2333 };
2334
2335 ts.setFilters = function( table, filter, apply, skipFirst ) {
2336 var c = table ? $( table )[0].config : '',
2337 valid = ts.getFilters( table, true, filter, skipFirst );
2338 // default apply to "true"
2339 if ( typeof apply === 'undefined' ) {
2340 apply = true;
2341 }
2342 if ( c && apply ) {
2343 // ensure new set filters are applied, even if the search is the same
2344 c.lastCombinedFilter = null;
2345 c.lastSearch = [];
2346 tsf.searching( c.table, filter, skipFirst );
2347 c.$table.triggerHandler( 'filterFomatterUpdate' );
2348 }
2349 return valid.length !== 0;
2350 };
2351
2352 })( jQuery );
2353
2354 /*! Widget: stickyHeaders - updated 9/27/2017 (v2.29.0) *//*
2355 * Requires tablesorter v2.8+ and jQuery 1.4.3+
2356 * by Rob Garrison
2357 */
2358 ;(function ($, window) {
2359 'use strict';
2360 var ts = $.tablesorter || {};
2361
2362 $.extend(ts.css, {
2363 sticky : 'tablesorter-stickyHeader', // stickyHeader
2364 stickyVis : 'tablesorter-sticky-visible',
2365 stickyHide: 'tablesorter-sticky-hidden',
2366 stickyWrap: 'tablesorter-sticky-wrapper'
2367 });
2368
2369 // Add a resize event to table headers
2370 ts.addHeaderResizeEvent = function(table, disable, settings) {
2371 table = $(table)[0]; // make sure we're using a dom element
2372 if ( !table.config ) { return; }
2373 var defaults = {
2374 timer : 250
2375 },
2376 options = $.extend({}, defaults, settings),
2377 c = table.config,
2378 wo = c.widgetOptions,
2379 checkSizes = function( triggerEvent ) {
2380 var index, headers, $header, sizes, width, height,
2381 len = c.$headers.length;
2382 wo.resize_flag = true;
2383 headers = [];
2384 for ( index = 0; index < len; index++ ) {
2385 $header = c.$headers.eq( index );
2386 sizes = $header.data( 'savedSizes' ) || [ 0, 0 ]; // fixes #394
2387 width = $header[0].offsetWidth;
2388 height = $header[0].offsetHeight;
2389 if ( width !== sizes[0] || height !== sizes[1] ) {
2390 $header.data( 'savedSizes', [ width, height ] );
2391 headers.push( $header[0] );
2392 }
2393 }
2394 if ( headers.length && triggerEvent !== false ) {
2395 c.$table.triggerHandler( 'resize', [ headers ] );
2396 }
2397 wo.resize_flag = false;
2398 };
2399 clearInterval(wo.resize_timer);
2400 if (disable) {
2401 wo.resize_flag = false;
2402 return false;
2403 }
2404 checkSizes( false );
2405 wo.resize_timer = setInterval(function() {
2406 if (wo.resize_flag) { return; }
2407 checkSizes();
2408 }, options.timer);
2409 };
2410
2411 function getStickyOffset(c, wo) {
2412 var $el = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : [];
2413 return $el.length ?
2414 $el.height() || 0 :
2415 parseInt(wo.stickyHeaders_offset, 10) || 0;
2416 }
2417
2418 // Sticky headers based on this awesome article:
2419 // http://css-tricks.com/13465-persistent-headers/
2420 // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech
2421 // **************************
2422 ts.addWidget({
2423 id: 'stickyHeaders',
2424 priority: 54, // sticky widget must be initialized after the filter & before pager widget!
2425 options: {
2426 stickyHeaders : '', // extra class name added to the sticky header row
2427 stickyHeaders_appendTo : null, // jQuery selector or object to phycially attach the sticky headers
2428 stickyHeaders_attachTo : null, // jQuery selector or object to attach scroll listener to (overridden by xScroll & yScroll settings)
2429 stickyHeaders_xScroll : null, // jQuery selector or object to monitor horizontal scroll position (defaults: xScroll > attachTo > window)
2430 stickyHeaders_yScroll : null, // jQuery selector or object to monitor vertical scroll position (defaults: yScroll > attachTo > window)
2431 stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element
2432 stickyHeaders_filteredToTop: true, // scroll table top into view after filtering
2433 stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists
2434 stickyHeaders_addResizeEvent : true, // trigger 'resize' event on headers
2435 stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header
2436 stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs
2437 },
2438 format: function(table, c, wo) {
2439 // filter widget doesn't initialize on an empty table. Fixes #449
2440 if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) {
2441 return;
2442 }
2443 var index, len, $t,
2444 $table = c.$table,
2445 // add position: relative to attach element, hopefully it won't cause trouble.
2446 $attach = $(wo.stickyHeaders_attachTo || wo.stickyHeaders_appendTo),
2447 namespace = c.namespace + 'stickyheaders ',
2448 // element to watch for the scroll event
2449 $yScroll = $(wo.stickyHeaders_yScroll || wo.stickyHeaders_attachTo || window),
2450 $xScroll = $(wo.stickyHeaders_xScroll || wo.stickyHeaders_attachTo || window),
2451 $thead = $table.children('thead:first'),
2452 $header = $thead.children('tr').not('.sticky-false').children(),
2453 $tfoot = $table.children('tfoot'),
2454 stickyOffset = getStickyOffset(c, wo),
2455 // is this table nested? If so, find parent sticky header wrapper (div, not table)
2456 $nestedSticky = $table.parent().closest('.' + ts.css.table).hasClass('hasStickyHeaders') ?
2457 $table.parent().closest('table.tablesorter')[0].config.widgetOptions.$sticky.parent() : [],
2458 nestedStickyTop = $nestedSticky.length ? $nestedSticky.height() : 0,
2459 // clone table, then wrap to make sticky header
2460 $stickyTable = wo.$sticky = $table.clone()
2461 .addClass('containsStickyHeaders ' + ts.css.sticky + ' ' + wo.stickyHeaders + ' ' + c.namespace.slice(1) + '_extra_table' )
2462 .wrap('<div class="' + ts.css.stickyWrap + '">'),
2463 $stickyWrap = $stickyTable.parent()
2464 .addClass(ts.css.stickyHide)
2465 .css({
2466 position : $attach.length ? 'absolute' : 'fixed',
2467 padding : parseInt( $stickyTable.parent().parent().css('padding-left'), 10 ),
2468 top : stickyOffset + nestedStickyTop,
2469 left : 0,
2470 visibility : 'hidden',
2471 zIndex : wo.stickyHeaders_zIndex || 2
2472 }),
2473 $stickyThead = $stickyTable.children('thead:first'),
2474 $stickyCells,
2475 laststate = '',
2476 setWidth = function($orig, $clone) {
2477 var index, width, border, $cell, $this,
2478 $cells = $orig.filter(':visible'),
2479 len = $cells.length;
2480 for ( index = 0; index < len; index++ ) {
2481 $cell = $clone.filter(':visible').eq(index);
2482 $this = $cells.eq(index);
2483 // code from https://github.com/jmosbech/StickyTableHeaders
2484 if ($this.css('box-sizing') === 'border-box') {
2485 width = $this.outerWidth();
2486 } else {
2487 if ($cell.css('border-collapse') === 'collapse') {
2488 if (window.getComputedStyle) {
2489 width = parseFloat( window.getComputedStyle($this[0], null).width );
2490 } else {
2491 // ie8 only
2492 border = parseFloat( $this.css('border-width') );
2493 width = $this.outerWidth() - parseFloat( $this.css('padding-left') ) - parseFloat( $this.css('padding-right') ) - border;
2494 }
2495 } else {
2496 width = $this.width();
2497 }
2498 }
2499 $cell.css({
2500 'width': width,
2501 'min-width': width,
2502 'max-width': width
2503 });
2504 }
2505 },
2506 getLeftPosition = function(yWindow) {
2507 if (yWindow === false && $nestedSticky.length) {
2508 return $table.position().left;
2509 }
2510 return $attach.length ?
2511 parseInt($attach.css('padding-left'), 10) || 0 :
2512 $table.offset().left - parseInt($table.css('margin-left'), 10) - $(window).scrollLeft();
2513 },
2514 resizeHeader = function() {
2515 $stickyWrap.css({
2516 left : getLeftPosition(),
2517 width: $table.outerWidth()
2518 });
2519 setWidth( $table, $stickyTable );
2520 setWidth( $header, $stickyCells );
2521 },
2522 scrollSticky = function( resizing ) {
2523 if (!$table.is(':visible')) { return; } // fixes #278
2524 // Detect nested tables - fixes #724
2525 nestedStickyTop = $nestedSticky.length ? $nestedSticky.offset().top - $yScroll.scrollTop() + $nestedSticky.height() : 0;
2526 var tmp,
2527 offset = $table.offset(),
2528 stickyOffset = getStickyOffset(c, wo),
2529 yWindow = $.isWindow( $yScroll[0] ), // $.isWindow needs jQuery 1.4.3
2530 yScroll = yWindow ?
2531 $yScroll.scrollTop() :
2532 // use parent sticky position if nested AND inside of a scrollable element - see #1512
2533 $nestedSticky.length ? parseInt($nestedSticky[0].style.top, 10) : $yScroll.offset().top,
2534 attachTop = $attach.length ? yScroll : $yScroll.scrollTop(),
2535 captionHeight = wo.stickyHeaders_includeCaption ? 0 : $table.children( 'caption' ).height() || 0,
2536 scrollTop = attachTop + stickyOffset + nestedStickyTop - captionHeight,
2537 tableHeight = $table.height() - ($stickyWrap.height() + ($tfoot.height() || 0)) - captionHeight,
2538 isVisible = ( scrollTop > offset.top ) && ( scrollTop < offset.top + tableHeight ) ? 'visible' : 'hidden',
2539 state = isVisible === 'visible' ? ts.css.stickyVis : ts.css.stickyHide,
2540 needsUpdating = !$stickyWrap.hasClass( state ),
2541 cssSettings = { visibility : isVisible };
2542 if ($attach.length) {
2543 // attached sticky headers always need updating
2544 needsUpdating = true;
2545 cssSettings.top = yWindow ? scrollTop - $attach.offset().top : $attach.scrollTop();
2546 }
2547 // adjust when scrolling horizontally - fixes issue #143
2548 tmp = getLeftPosition(yWindow);
2549 if (tmp !== parseInt($stickyWrap.css('left'), 10)) {
2550 needsUpdating = true;
2551 cssSettings.left = tmp;
2552 }
2553 cssSettings.top = ( cssSettings.top || 0 ) +
2554 // If nested AND inside of a scrollable element, only add parent sticky height
2555 (!yWindow && $nestedSticky.length ? $nestedSticky.height() : stickyOffset + nestedStickyTop);
2556 if (needsUpdating) {
2557 $stickyWrap
2558 .removeClass( ts.css.stickyVis + ' ' + ts.css.stickyHide )
2559 .addClass( state )
2560 .css(cssSettings);
2561 }
2562 if (isVisible !== laststate || resizing) {
2563 // make sure the column widths match
2564 resizeHeader();
2565 laststate = isVisible;
2566 }
2567 };
2568 // only add a position relative if a position isn't already defined
2569 if ($attach.length && !$attach.css('position')) {
2570 $attach.css('position', 'relative');
2571 }
2572 // fix clone ID, if it exists - fixes #271
2573 if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; }
2574 // clear out cloned table, except for sticky header
2575 // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing
2576 $stickyTable.find('> thead:gt(0), tr.sticky-false').hide();
2577 $stickyTable.find('> tbody, > tfoot').remove();
2578 $stickyTable.find('caption').toggle(wo.stickyHeaders_includeCaption);
2579 // issue #172 - find td/th in sticky header
2580 $stickyCells = $stickyThead.children().children();
2581 $stickyTable.css({ height:0, width:0, margin: 0 });
2582 // remove resizable block
2583 $stickyCells.find('.' + ts.css.resizer).remove();
2584 // update sticky header class names to match real header after sorting
2585 $table
2586 .addClass('hasStickyHeaders')
2587 .on('pagerComplete' + namespace, function() {
2588 resizeHeader();
2589 });
2590
2591 ts.bindEvents(table, $stickyThead.children().children('.' + ts.css.header));
2592
2593 if (wo.stickyHeaders_appendTo) {
2594 $(wo.stickyHeaders_appendTo).append( $stickyWrap );
2595 } else {
2596 // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned.
2597 $table.after( $stickyWrap );
2598 }
2599
2600 // onRenderHeader is defined, we need to do something about it (fixes #641)
2601 if (c.onRenderHeader) {
2602 $t = $stickyThead.children('tr').children();
2603 len = $t.length;
2604 for ( index = 0; index < len; index++ ) {
2605 // send second parameter
2606 c.onRenderHeader.apply( $t.eq( index ), [ index, c, $stickyTable ] );
2607 }
2608 }
2609 // make it sticky!
2610 $xScroll.add($yScroll)
2611 .off( ('scroll resize '.split(' ').join( namespace )).replace(/\s+/g, ' ') )
2612 .on('scroll resize '.split(' ').join( namespace ), function( event ) {
2613 scrollSticky( event.type === 'resize' );
2614 });
2615 c.$table
2616 .off('stickyHeadersUpdate' + namespace)
2617 .on('stickyHeadersUpdate' + namespace, function() {
2618 scrollSticky( true );
2619 });
2620
2621 if (wo.stickyHeaders_addResizeEvent) {
2622 ts.addHeaderResizeEvent(table);
2623 }
2624
2625 // look for filter widget
2626 if ($table.hasClass('hasFilters') && wo.filter_columnFilters) {
2627 // scroll table into view after filtering, if sticky header is active - #482
2628 $table.on('filterEnd' + namespace, function() {
2629 // $(':focus') needs jQuery 1.6+
2630 var $td = $(document.activeElement).closest('td'),
2631 column = $td.parent().children().index($td);
2632 // only scroll if sticky header is active
2633 if ($stickyWrap.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) {
2634 // scroll to original table (not sticky clone)
2635 window.scrollTo(0, $table.position().top);
2636 // give same input/select focus; check if c.$filters exists; fixes #594
2637 if (column >= 0 && c.$filters) {
2638 c.$filters.eq(column).find('a, select, input').filter(':visible').focus();
2639 }
2640 }
2641 });
2642 ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) );
2643 // support hideFilters
2644 if (wo.filter_hideFilters) {
2645 ts.filter.hideFilters(c, $stickyTable);
2646 }
2647 }
2648
2649 // resize table (Firefox)
2650 if (wo.stickyHeaders_addResizeEvent) {
2651 $table.on('resize' + c.namespace + 'stickyheaders', function() {
2652 resizeHeader();
2653 });
2654 }
2655
2656 // make sure sticky is visible if page is partially scrolled
2657 scrollSticky( true );
2658 $table.triggerHandler('stickyHeadersInit');
2659
2660 },
2661 remove: function(table, c, wo) {
2662 var namespace = c.namespace + 'stickyheaders ';
2663 c.$table
2664 .removeClass('hasStickyHeaders')
2665 .off( ('pagerComplete resize filterEnd stickyHeadersUpdate '.split(' ').join(namespace)).replace(/\s+/g, ' ') )
2666 .next('.' + ts.css.stickyWrap).remove();
2667 if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table
2668 $(window)
2669 .add(wo.stickyHeaders_xScroll)
2670 .add(wo.stickyHeaders_yScroll)
2671 .add(wo.stickyHeaders_attachTo)
2672 .off( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') );
2673 ts.addHeaderResizeEvent(table, true);
2674 }
2675 });
2676
2677 })(jQuery, window);
2678
2679 /*! Widget: resizable - updated 2018-03-26 (v2.30.2) */
2680 /*jshint browser:true, jquery:true, unused:false */
2681 ;(function ($, window) {
2682 'use strict';
2683 var ts = $.tablesorter || {};
2684
2685 $.extend(ts.css, {
2686 resizableContainer : 'tablesorter-resizable-container',
2687 resizableHandle : 'tablesorter-resizable-handle',
2688 resizableNoSelect : 'tablesorter-disableSelection',
2689 resizableStorage : 'tablesorter-resizable'
2690 });
2691
2692 // Add extra scroller css
2693 $(function() {
2694 var s = '<style>' +
2695 'body.' + ts.css.resizableNoSelect + ' { -ms-user-select: none; -moz-user-select: -moz-none;' +
2696 '-khtml-user-select: none; -webkit-user-select: none; user-select: none; }' +
2697 '.' + ts.css.resizableContainer + ' { position: relative; height: 1px; }' +
2698 // make handle z-index > than stickyHeader z-index, so the handle stays above sticky header
2699 '.' + ts.css.resizableHandle + ' { position: absolute; display: inline-block; width: 8px;' +
2700 'top: 1px; cursor: ew-resize; z-index: 3; user-select: none; -moz-user-select: none; }' +
2701 '</style>';
2702 $('head').append(s);
2703 });
2704
2705 ts.resizable = {
2706 init : function( c, wo ) {
2707 if ( c.$table.hasClass( 'hasResizable' ) ) { return; }
2708 c.$table.addClass( 'hasResizable' );
2709
2710 var noResize, $header, column, storedSizes, tmp,
2711 $table = c.$table,
2712 $parent = $table.parent(),
2713 marginTop = parseInt( $table.css( 'margin-top' ), 10 ),
2714
2715 // internal variables
2716 vars = wo.resizable_vars = {
2717 useStorage : ts.storage && wo.resizable !== false,
2718 $wrap : $parent,
2719 mouseXPosition : 0,
2720 $target : null,
2721 $next : null,
2722 overflow : $parent.css('overflow') === 'auto' ||
2723 $parent.css('overflow') === 'scroll' ||
2724 $parent.css('overflow-x') === 'auto' ||
2725 $parent.css('overflow-x') === 'scroll',
2726 storedSizes : []
2727 };
2728
2729 // set default widths
2730 ts.resizableReset( c.table, true );
2731
2732 // now get measurements!
2733 vars.tableWidth = $table.width();
2734 // attempt to autodetect
2735 vars.fullWidth = Math.abs( $parent.width() - vars.tableWidth ) < 20;
2736
2737 /*
2738 // Hacky method to determine if table width is set to 'auto'
2739 // http://stackoverflow.com/a/20892048/145346
2740 if ( !vars.fullWidth ) {
2741 tmp = $table.width();
2742 $header = $table.wrap('<span>').parent(); // temp variable
2743 storedSizes = parseInt( $table.css( 'margin-left' ), 10 ) || 0;
2744 $table.css( 'margin-left', storedSizes + 50 );
2745 vars.tableWidth = $header.width() > tmp ? 'auto' : tmp;
2746 $table.css( 'margin-left', storedSizes ? storedSizes : '' );
2747 $header = null;
2748 $table.unwrap('<span>');
2749 }
2750 */
2751
2752 if ( vars.useStorage && vars.overflow ) {
2753 // save table width
2754 ts.storage( c.table, 'tablesorter-table-original-css-width', vars.tableWidth );
2755 tmp = ts.storage( c.table, 'tablesorter-table-resized-width' ) || 'auto';
2756 ts.resizable.setWidth( $table, tmp, true );
2757 }
2758 wo.resizable_vars.storedSizes = storedSizes = ( vars.useStorage ?
2759 ts.storage( c.table, ts.css.resizableStorage ) :
2760 [] ) || [];
2761 ts.resizable.setWidths( c, wo, storedSizes );
2762 ts.resizable.updateStoredSizes( c, wo );
2763
2764 wo.$resizable_container = $( '<div class="' + ts.css.resizableContainer + '">' )
2765 .css({ top : marginTop })
2766 .insertBefore( $table );
2767 // add container
2768 for ( column = 0; column < c.columns; column++ ) {
2769 $header = c.$headerIndexed[ column ];
2770 tmp = ts.getColumnData( c.table, c.headers, column );
2771 noResize = ts.getData( $header, tmp, 'resizable' ) === 'false';
2772 if ( !noResize ) {
2773 $( '<div class="' + ts.css.resizableHandle + '">' )
2774 .appendTo( wo.$resizable_container )
2775 .attr({
2776 'data-column' : column,
2777 'unselectable' : 'on'
2778 })
2779 .data( 'header', $header )
2780 .on( 'selectstart', false );
2781 }
2782 }
2783 ts.resizable.bindings( c, wo );
2784 },
2785
2786 updateStoredSizes : function( c, wo ) {
2787 var column, $header,
2788 len = c.columns,
2789 vars = wo.resizable_vars;
2790 vars.storedSizes = [];
2791 for ( column = 0; column < len; column++ ) {
2792 $header = c.$headerIndexed[ column ];
2793 vars.storedSizes[ column ] = $header.is(':visible') ? $header.width() : 0;
2794 }
2795 },
2796
2797 setWidth : function( $el, width, overflow ) {
2798 // overflow tables need min & max width set as well
2799 $el.css({
2800 'width' : width,
2801 'min-width' : overflow ? width : '',
2802 'max-width' : overflow ? width : ''
2803 });
2804 },
2805
2806 setWidths : function( c, wo, storedSizes ) {
2807 var column, $temp,
2808 vars = wo.resizable_vars,
2809 $extra = $( c.namespace + '_extra_headers' ),
2810 $col = c.$table.children( 'colgroup' ).children( 'col' );
2811 storedSizes = storedSizes || vars.storedSizes || [];
2812 // process only if table ID or url match
2813 if ( storedSizes.length ) {
2814 for ( column = 0; column < c.columns; column++ ) {
2815 // set saved resizable widths
2816 ts.resizable.setWidth( c.$headerIndexed[ column ], storedSizes[ column ], vars.overflow );
2817 if ( $extra.length ) {
2818 // stickyHeaders needs to modify min & max width as well
2819 $temp = $extra.eq( column ).add( $col.eq( column ) );
2820 ts.resizable.setWidth( $temp, storedSizes[ column ], vars.overflow );
2821 }
2822 }
2823 $temp = $( c.namespace + '_extra_table' );
2824 if ( $temp.length && !ts.hasWidget( c.table, 'scroller' ) ) {
2825 ts.resizable.setWidth( $temp, c.$table.outerWidth(), vars.overflow );
2826 }
2827 }
2828 },
2829
2830 setHandlePosition : function( c, wo ) {
2831 var startPosition,
2832 tableHeight = c.$table.height(),
2833 $handles = wo.$resizable_container.children(),
2834 handleCenter = Math.floor( $handles.width() / 2 );
2835
2836 if ( ts.hasWidget( c.table, 'scroller' ) ) {
2837 tableHeight = 0;
2838 c.$table.closest( '.' + ts.css.scrollerWrap ).children().each(function() {
2839 var $this = $(this);
2840 // center table has a max-height set
2841 tableHeight += $this.filter('[style*="height"]').length ? $this.height() : $this.children('table').height();
2842 });
2843 }
2844
2845 if ( !wo.resizable_includeFooter && c.$table.children('tfoot').length ) {
2846 tableHeight -= c.$table.children('tfoot').height();
2847 }
2848 // subtract out table left position from resizable handles. Fixes #864
2849 // jQuery v3.3.0+ appears to include the start position with the $header.position().left; see #1544
2850 startPosition = parseFloat($.fn.jquery) >= 3.3 ? 0 : c.$table.position().left;
2851 $handles.each( function() {
2852 var $this = $(this),
2853 column = parseInt( $this.attr( 'data-column' ), 10 ),
2854 columns = c.columns - 1,
2855 $header = $this.data( 'header' );
2856 if ( !$header ) { return; } // see #859
2857 if (
2858 !$header.is(':visible') ||
2859 ( !wo.resizable_addLastColumn && ts.resizable.checkVisibleColumns(c, column) )
2860 ) {
2861 $this.hide();
2862 } else if ( column < columns || column === columns && wo.resizable_addLastColumn ) {
2863 $this.css({
2864 display: 'inline-block',
2865 height : tableHeight,
2866 left : $header.position().left - startPosition + $header.outerWidth() - handleCenter
2867 });
2868 }
2869 });
2870 },
2871
2872 // Fixes #1485
2873 checkVisibleColumns: function( c, column ) {
2874 var i,
2875 len = 0;
2876 for ( i = column + 1; i < c.columns; i++ ) {
2877 len += c.$headerIndexed[i].is( ':visible' ) ? 1 : 0;
2878 }
2879 return len === 0;
2880 },
2881
2882 // prevent text selection while dragging resize bar
2883 toggleTextSelection : function( c, wo, toggle ) {
2884 var namespace = c.namespace + 'tsresize';
2885 wo.resizable_vars.disabled = toggle;
2886 $( 'body' ).toggleClass( ts.css.resizableNoSelect, toggle );
2887 if ( toggle ) {
2888 $( 'body' )
2889 .attr( 'unselectable', 'on' )
2890 .on( 'selectstart' + namespace, false );
2891 } else {
2892 $( 'body' )
2893 .removeAttr( 'unselectable' )
2894 .off( 'selectstart' + namespace );
2895 }
2896 },
2897
2898 bindings : function( c, wo ) {
2899 var namespace = c.namespace + 'tsresize';
2900 wo.$resizable_container.children().on( 'mousedown', function( event ) {
2901 // save header cell and mouse position
2902 var column,
2903 vars = wo.resizable_vars,
2904 $extras = $( c.namespace + '_extra_headers' ),
2905 $header = $( event.target ).data( 'header' );
2906
2907 column = parseInt( $header.attr( 'data-column' ), 10 );
2908 vars.$target = $header = $header.add( $extras.filter('[data-column="' + column + '"]') );
2909 vars.target = column;
2910
2911 // if table is not as wide as it's parent, then resize the table
2912 vars.$next = event.shiftKey || wo.resizable_targetLast ?
2913 $header.parent().children().not( '.resizable-false' ).filter( ':last' ) :
2914 $header.nextAll( ':not(.resizable-false)' ).eq( 0 );
2915
2916 column = parseInt( vars.$next.attr( 'data-column' ), 10 );
2917 vars.$next = vars.$next.add( $extras.filter('[data-column="' + column + '"]') );
2918 vars.next = column;
2919
2920 vars.mouseXPosition = event.pageX;
2921 ts.resizable.updateStoredSizes( c, wo );
2922 ts.resizable.toggleTextSelection(c, wo, true );
2923 });
2924
2925 $( document )
2926 .on( 'mousemove' + namespace, function( event ) {
2927 var vars = wo.resizable_vars;
2928 // ignore mousemove if no mousedown
2929 if ( !vars.disabled || vars.mouseXPosition === 0 || !vars.$target ) { return; }
2930 if ( wo.resizable_throttle ) {
2931 clearTimeout( vars.timer );
2932 vars.timer = setTimeout( function() {
2933 ts.resizable.mouseMove( c, wo, event );
2934 }, isNaN( wo.resizable_throttle ) ? 5 : wo.resizable_throttle );
2935 } else {
2936 ts.resizable.mouseMove( c, wo, event );
2937 }
2938 })
2939 .on( 'mouseup' + namespace, function() {
2940 if (!wo.resizable_vars.disabled) { return; }
2941 ts.resizable.toggleTextSelection( c, wo, false );
2942 ts.resizable.stopResize( c, wo );
2943 ts.resizable.setHandlePosition( c, wo );
2944 });
2945
2946 // resizeEnd event triggered by scroller widget
2947 $( window ).on( 'resize' + namespace + ' resizeEnd' + namespace, function() {
2948 ts.resizable.setHandlePosition( c, wo );
2949 });
2950
2951 // right click to reset columns to default widths
2952 c.$table
2953 .on( 'columnUpdate pagerComplete resizableUpdate '.split( ' ' ).join( namespace + ' ' ), function() {
2954 ts.resizable.setHandlePosition( c, wo );
2955 })
2956 .on( 'resizableReset' + namespace, function() {
2957 ts.resizableReset( c.table );
2958 })
2959 .find( 'thead:first' )
2960 .add( $( c.namespace + '_extra_table' ).find( 'thead:first' ) )
2961 .on( 'contextmenu' + namespace, function() {
2962 // $.isEmptyObject() needs jQuery 1.4+; allow right click if already reset
2963 var allowClick = wo.resizable_vars.storedSizes.length === 0;
2964 ts.resizableReset( c.table );
2965 ts.resizable.setHandlePosition( c, wo );
2966 wo.resizable_vars.storedSizes = [];
2967 return allowClick;
2968 });
2969
2970 },
2971
2972 mouseMove : function( c, wo, event ) {
2973 if ( wo.resizable_vars.mouseXPosition === 0 || !wo.resizable_vars.$target ) { return; }
2974 // resize columns
2975 var column,
2976 total = 0,
2977 vars = wo.resizable_vars,
2978 $next = vars.$next,
2979 tar = vars.storedSizes[ vars.target ],
2980 leftEdge = event.pageX - vars.mouseXPosition;
2981 if ( vars.overflow ) {
2982 if ( tar + leftEdge > 0 ) {
2983 vars.storedSizes[ vars.target ] += leftEdge;
2984 ts.resizable.setWidth( vars.$target, vars.storedSizes[ vars.target ], true );
2985 // update the entire table width
2986 for ( column = 0; column < c.columns; column++ ) {
2987 total += vars.storedSizes[ column ];
2988 }
2989 ts.resizable.setWidth( c.$table.add( $( c.namespace + '_extra_table' ) ), total );
2990 }
2991 if ( !$next.length ) {
2992 // if expanding right-most column, scroll the wrapper
2993 vars.$wrap[0].scrollLeft = c.$table.width();
2994 }
2995 } else if ( vars.fullWidth ) {
2996 vars.storedSizes[ vars.target ] += leftEdge;
2997 vars.storedSizes[ vars.next ] -= leftEdge;
2998 ts.resizable.setWidths( c, wo );
2999 } else {
3000 vars.storedSizes[ vars.target ] += leftEdge;
3001 ts.resizable.setWidths( c, wo );
3002 }
3003 vars.mouseXPosition = event.pageX;
3004 // dynamically update sticky header widths
3005 c.$table.triggerHandler('stickyHeadersUpdate');
3006 },
3007
3008 stopResize : function( c, wo ) {
3009 var vars = wo.resizable_vars;
3010 ts.resizable.updateStoredSizes( c, wo );
3011 if ( vars.useStorage ) {
3012 // save all column widths
3013 ts.storage( c.table, ts.css.resizableStorage, vars.storedSizes );
3014 ts.storage( c.table, 'tablesorter-table-resized-width', c.$table.width() );
3015 }
3016 vars.mouseXPosition = 0;
3017 vars.$target = vars.$next = null;
3018 // will update stickyHeaders, just in case, see #912
3019 c.$table.triggerHandler('stickyHeadersUpdate');
3020 c.$table.triggerHandler('resizableComplete');
3021 }
3022 };
3023
3024 // this widget saves the column widths if
3025 // $.tablesorter.storage function is included
3026 // **************************
3027 ts.addWidget({
3028 id: 'resizable',
3029 priority: 40,
3030 options: {
3031 resizable : true, // save column widths to storage
3032 resizable_addLastColumn : false,
3033 resizable_includeFooter: true,
3034 resizable_widths : [],
3035 resizable_throttle : false, // set to true (5ms) or any number 0-10 range
3036 resizable_targetLast : false
3037 },
3038 init: function(table, thisWidget, c, wo) {
3039 ts.resizable.init( c, wo );
3040 },
3041 format: function( table, c, wo ) {
3042 ts.resizable.setHandlePosition( c, wo );
3043 },
3044 remove: function( table, c, wo, refreshing ) {
3045 if (wo.$resizable_container) {
3046 var namespace = c.namespace + 'tsresize';
3047 c.$table.add( $( c.namespace + '_extra_table' ) )
3048 .removeClass('hasResizable')
3049 .children( 'thead' )
3050 .off( 'contextmenu' + namespace );
3051
3052 wo.$resizable_container.remove();
3053 ts.resizable.toggleTextSelection( c, wo, false );
3054 ts.resizableReset( table, refreshing );
3055 $( document ).off( 'mousemove' + namespace + ' mouseup' + namespace );
3056 }
3057 }
3058 });
3059
3060 ts.resizableReset = function( table, refreshing ) {
3061 $( table ).each(function() {
3062 var index, $t,
3063 c = this.config,
3064 wo = c && c.widgetOptions,
3065 vars = wo.resizable_vars;
3066 if ( table && c && c.$headerIndexed.length ) {
3067 // restore the initial table width
3068 if ( vars.overflow && vars.tableWidth ) {
3069 ts.resizable.setWidth( c.$table, vars.tableWidth, true );
3070 if ( vars.useStorage ) {
3071 ts.storage( table, 'tablesorter-table-resized-width', vars.tableWidth );
3072 }
3073 }
3074 for ( index = 0; index < c.columns; index++ ) {
3075 $t = c.$headerIndexed[ index ];
3076 if ( wo.resizable_widths && wo.resizable_widths[ index ] ) {
3077 ts.resizable.setWidth( $t, wo.resizable_widths[ index ], vars.overflow );
3078 } else if ( !$t.hasClass( 'resizable-false' ) ) {
3079 // don't clear the width of any column that is not resizable
3080 ts.resizable.setWidth( $t, '', vars.overflow );
3081 }
3082 }
3083
3084 // reset stickyHeader widths
3085 c.$table.triggerHandler( 'stickyHeadersUpdate' );
3086 if ( ts.storage && !refreshing ) {
3087 ts.storage( this, ts.css.resizableStorage, [] );
3088 }
3089 }
3090 });
3091 };
3092
3093 })( jQuery, window );
3094
3095 /*! Widget: saveSort - updated 2018-03-19 (v2.30.1) *//*
3096 * Requires tablesorter v2.16+
3097 * by Rob Garrison
3098 */
3099 ;(function ($) {
3100 'use strict';
3101 var ts = $.tablesorter || {};
3102
3103 function getStoredSortList(c) {
3104 var stored = ts.storage( c.table, 'tablesorter-savesort' );
3105 return (stored && stored.hasOwnProperty('sortList') && Array.isArray(stored.sortList)) ? stored.sortList : [];
3106 }
3107
3108 function sortListChanged(c, sortList) {
3109 return (sortList || getStoredSortList(c)).join(',') !== c.sortList.join(',');
3110 }
3111
3112 // this widget saves the last sort only if the
3113 // saveSort widget option is true AND the
3114 // $.tablesorter.storage function is included
3115 // **************************
3116 ts.addWidget({
3117 id: 'saveSort',
3118 priority: 20,
3119 options: {
3120 saveSort : true
3121 },
3122 init: function(table, thisWidget, c, wo) {
3123 // run widget format before all other widgets are applied to the table
3124 thisWidget.format(table, c, wo, true);
3125 },
3126 format: function(table, c, wo, init) {
3127 var time,
3128 $table = c.$table,
3129 saveSort = wo.saveSort !== false, // make saveSort active/inactive; default to true
3130 sortList = { 'sortList' : c.sortList },
3131 debug = ts.debug(c, 'saveSort');
3132 if (debug) {
3133 time = new Date();
3134 }
3135 if ($table.hasClass('hasSaveSort')) {
3136 if (saveSort && table.hasInitialized && ts.storage && sortListChanged(c)) {
3137 ts.storage( table, 'tablesorter-savesort', sortList );
3138 if (debug) {
3139 console.log('saveSort >> Saving last sort: ' + c.sortList + ts.benchmark(time));
3140 }
3141 }
3142 } else {
3143 // set table sort on initial run of the widget
3144 $table.addClass('hasSaveSort');
3145 sortList = '';
3146 // get data
3147 if (ts.storage) {
3148 sortList = getStoredSortList(c);
3149 if (debug) {
3150 console.log('saveSort >> Last sort loaded: "' + sortList + '"' + ts.benchmark(time));
3151 }
3152 $table.on('saveSortReset', function(event) {
3153 event.stopPropagation();
3154 ts.storage( table, 'tablesorter-savesort', '' );
3155 });
3156 }
3157 // init is true when widget init is run, this will run this widget before all other widgets have initialized
3158 // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice.
3159 if (init && sortList && sortList.length > 0) {
3160 c.sortList = sortList;
3161 } else if (table.hasInitialized && sortList && sortList.length > 0) {
3162 // update sort change
3163 if (sortListChanged(c, sortList)) {
3164 ts.sortOn(c, sortList);
3165 }
3166 }
3167 }
3168 },
3169 remove: function(table, c) {
3170 c.$table.removeClass('hasSaveSort');
3171 // clear storage
3172 if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); }
3173 }
3174 });
3175
3176 })(jQuery);
3177 return jQuery.tablesorter;}));
3178