nano-lazar 1.1.0

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