PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.5
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.5
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.5, at js/tablesorter/jquery.tablesorter.widgets.js

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