jquery-tablesorter 1.16.3 → 1.16.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/jquery-tablesorter/version.rb +1 -1
  4. data/vendor/assets/javascripts/jquery-tablesorter/extras/semver-mod.js +277 -86
  5. data/vendor/assets/javascripts/jquery-tablesorter/extras/semver.js +272 -81
  6. data/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.combined.js +4483 -0
  7. data/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.js +45 -13
  8. data/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.widgets.js +10 -11
  9. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-extract.js +9 -16
  10. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-iso8601.js +2 -2
  11. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-month.js +3 -4
  12. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-range.js +2 -5
  13. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-two-digit-year.js +2 -3
  14. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-weekday.js +2 -3
  15. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date.js +6 -10
  16. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-duration.js +1 -1
  17. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-feet-inch-fraction.js +1 -1
  18. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-file-type.js +1 -1
  19. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-ignore-articles.js +1 -1
  20. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-image.js +2 -3
  21. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-input-select.js +1 -1
  22. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-metric.js +1 -1
  23. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-named-numbers.js +1 -1
  24. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-network.js +9 -6
  25. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-roman.js +1 -1
  26. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-alignChar.js +2 -2
  27. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-chart.js +1 -1
  28. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-columnSelector.js +1 -1
  29. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-cssStickyHeaders.js +1 -1
  30. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-editable.js +1 -1
  31. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-html5.js +1 -1
  32. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-jui.js +1 -1
  33. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-select2.js +1 -1
  34. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-type-insideRange.js +1 -4
  35. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-formatter.js +1 -1
  36. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-grouping.js +1 -1
  37. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-headerTitles.js +1 -1
  38. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-math.js +1 -1
  39. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-output.js +1 -1
  40. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-print.js +1 -1
  41. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-reflow.js +1 -1
  42. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-repeatheaders.js +1 -1
  43. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-staticRow.js +1 -1
  44. metadata +3 -2
@@ -0,0 +1,4483 @@
1
+ /*** This file is dynamically generated ***
2
+ █████▄ ▄████▄ █████▄ ▄████▄ ██████ ███████▄ ▄████▄ █████▄ ██ ██████ ██ ██
3
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
4
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀▀▀▀██
5
+ █████▀ ▀████▀ ██ ██ ▀████▀ ██ ██ ██ ██ ▀████▀ █████▀ ██ ██ █████▀
6
+ */
7
+ /*! tablesorter (FORK) - updated 03-30-2015 (v2.21.4)*/
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
+ /*! TableSorter (FORK) v2.21.4 *//*
20
+ * Client-side table sorting with ease!
21
+ * @requires jQuery v1.2.6+
22
+ *
23
+ * Copyright (c) 2007 Christian Bach
24
+ * fork maintained by Rob Garrison
25
+ *
26
+ * Examples and docs at: http://tablesorter.com
27
+ * Dual licensed under the MIT and GPL licenses:
28
+ * http://www.opensource.org/licenses/mit-license.php
29
+ * http://www.gnu.org/licenses/gpl.html
30
+ *
31
+ * @type jQuery
32
+ * @name tablesorter (FORK)
33
+ * @cat Plugins/Tablesorter
34
+ * @author Christian Bach - christian.bach@polyester.se
35
+ * @contributor Rob Garrison - https://github.com/Mottie/tablesorter
36
+ */
37
+ /*jshint browser:true, jquery:true, unused:false, expr: true */
38
+ /*global console:false, alert:false, require:false, define:false, module:false */
39
+ ;(function($){
40
+ 'use strict';
41
+ $.extend({
42
+ /*jshint supernew:true */
43
+ tablesorter: new function() {
44
+
45
+ var ts = this;
46
+
47
+ ts.version = '2.21.4';
48
+
49
+ ts.parsers = [];
50
+ ts.widgets = [];
51
+ ts.defaults = {
52
+
53
+ // *** appearance
54
+ theme : 'default', // adds tablesorter-{theme} to the table for styling
55
+ widthFixed : false, // adds colgroup to fix widths of columns
56
+ showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered.
57
+
58
+ headerTemplate : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = <i/> (class from cssIcon)
59
+ onRenderTemplate : null, // function(index, template){ return template; }, (template is a string)
60
+ onRenderHeader : null, // function(index){}, (nothing to return)
61
+
62
+ // *** functionality
63
+ cancelSelection : true, // prevent text selection in the header
64
+ tabIndex : true, // add tabindex to header for keyboard accessibility
65
+ dateFormat : 'mmddyyyy', // other options: 'ddmmyyy' or 'yyyymmdd'
66
+ sortMultiSortKey : 'shiftKey', // key used to select additional columns
67
+ sortResetKey : 'ctrlKey', // key used to remove sorting on a column
68
+ usNumberFormat : true, // false for German '1.234.567,89' or French '1 234 567,89'
69
+ delayInit : false, // if false, the parsed table contents will not update until the first sort
70
+ serverSideSorting: false, // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used.
71
+ resort : true, // default setting to trigger a resort after an 'update', 'addRows', 'updateCell', etc has completed
72
+
73
+ // *** sort options
74
+ headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
75
+ ignoreCase : true, // ignore case while sorting
76
+ sortForce : null, // column(s) first sorted; always applied
77
+ sortList : [], // Initial sort order; applied initially; updated when manually sorted
78
+ sortAppend : null, // column(s) sorted last; always applied
79
+ sortStable : false, // when sorting two rows with exactly the same content, the original sort order is maintained
80
+
81
+ sortInitialOrder : 'asc', // sort direction on first click
82
+ sortLocaleCompare: false, // replace equivalent character (accented characters)
83
+ sortReset : false, // third click on the header will reset column to default - unsorted
84
+ sortRestart : false, // restart sort to 'sortInitialOrder' when clicking on previously unsorted columns
85
+
86
+ emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero, emptyMax, emptyMin
87
+ stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero
88
+ textExtraction : 'basic', // text extraction method/function - function(node, table, cellIndex){}
89
+ textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in default textExtraction function)
90
+ textSorter : null, // choose overall or specific column sorter function(a, b, direction, table, columnIndex) [alt: ts.sortText]
91
+ numberSorter : null, // choose overall numeric sorter function(a, b, direction, maxColumnValue)
92
+
93
+ // *** widget options
94
+ widgets: [], // method to add widgets, e.g. widgets: ['zebra']
95
+ widgetOptions : {
96
+ zebra : [ 'even', 'odd' ] // zebra widget alternating row class names
97
+ },
98
+ initWidgets : true, // apply widgets on tablesorter initialization
99
+ widgetClass : 'widget-{name}', // table class name template to match to include a widget
100
+
101
+ // *** callbacks
102
+ initialized : null, // function(table){},
103
+
104
+ // *** extra css class names
105
+ tableClass : '',
106
+ cssAsc : '',
107
+ cssDesc : '',
108
+ cssNone : '',
109
+ cssHeader : '',
110
+ cssHeaderRow : '',
111
+ cssProcessing : '', // processing icon applied to header during sort/filter
112
+
113
+ cssChildRow : 'tablesorter-childRow', // class name indiciating that a row is to be attached to the its parent
114
+ cssIcon : 'tablesorter-icon', // if this class does not exist, the {icon} will not be added from the headerTemplate
115
+ cssIconNone : '', // class name added to the icon when there is no column sort
116
+ cssIconAsc : '', // class name added to the icon when the column has an ascending sort
117
+ cssIconDesc : '', // class name added to the icon when the column has a descending sort
118
+ cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!)
119
+ cssNoSort : 'tablesorter-noSort', // class name added to element inside header; clicking on it won't cause a sort
120
+ cssIgnoreRow : 'tablesorter-ignoreRow', // header row to ignore; cells within this row will not be added to c.$headers
121
+
122
+ // *** selectors
123
+ selectorHeaders : '> thead th, > thead td',
124
+ selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort
125
+ selectorRemove : '.remove-me',
126
+
127
+ // *** advanced
128
+ debug : false,
129
+
130
+ // *** Internal variables
131
+ headerList: [],
132
+ empties: {},
133
+ strings: {},
134
+ parsers: []
135
+
136
+ // removed: widgetZebra: { css: ['even', 'odd'] }
137
+
138
+ };
139
+
140
+ // internal css classes - these will ALWAYS be added to
141
+ // the table and MUST only contain one class name - fixes #381
142
+ ts.css = {
143
+ table : 'tablesorter',
144
+ cssHasChild: 'tablesorter-hasChildRow',
145
+ childRow : 'tablesorter-childRow',
146
+ colgroup : 'tablesorter-colgroup',
147
+ header : 'tablesorter-header',
148
+ headerRow : 'tablesorter-headerRow',
149
+ headerIn : 'tablesorter-header-inner',
150
+ icon : 'tablesorter-icon',
151
+ processing : 'tablesorter-processing',
152
+ sortAsc : 'tablesorter-headerAsc',
153
+ sortDesc : 'tablesorter-headerDesc',
154
+ sortNone : 'tablesorter-headerUnSorted'
155
+ };
156
+
157
+ // labels applied to sortable headers for accessibility (aria) support
158
+ ts.language = {
159
+ sortAsc : 'Ascending sort applied, ',
160
+ sortDesc : 'Descending sort applied, ',
161
+ sortNone : 'No sort applied, ',
162
+ nextAsc : 'activate to apply an ascending sort',
163
+ nextDesc : 'activate to apply a descending sort',
164
+ nextNone : 'activate to remove the sort'
165
+ };
166
+
167
+ // These methods can be applied on table.config instance
168
+ ts.instanceMethods = {};
169
+
170
+ /* debuging utils */
171
+ function log() {
172
+ var a = arguments[0],
173
+ s = arguments.length > 1 ? Array.prototype.slice.call(arguments) : a;
174
+ if (typeof console !== 'undefined' && typeof console.log !== 'undefined') {
175
+ console[ /error/i.test(a) ? 'error' : /warn/i.test(a) ? 'warn' : 'log' ](s);
176
+ } else {
177
+ alert(s);
178
+ }
179
+ }
180
+
181
+ function benchmark(s, d) {
182
+ log(s + ' (' + (new Date().getTime() - d.getTime()) + 'ms)');
183
+ }
184
+
185
+ ts.log = log;
186
+ ts.benchmark = benchmark;
187
+
188
+ // $.isEmptyObject from jQuery v1.4
189
+ function isEmptyObject(obj) {
190
+ /*jshint forin: false */
191
+ for (var name in obj) {
192
+ return false;
193
+ }
194
+ return true;
195
+ }
196
+
197
+ ts.getElementText = function(c, node, cellIndex) {
198
+ if (!node) { return ''; }
199
+ var te,
200
+ t = c.textExtraction || '',
201
+ // node could be a jquery object
202
+ // http://jsperf.com/jquery-vs-instanceof-jquery/2
203
+ $node = node.jquery ? node : $(node);
204
+ if (typeof(t) === 'string') {
205
+ // check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow!
206
+ return $.trim( ( t === 'basic' ? $node.attr(c.textAttribute) || node.textContent : node.textContent ) || $node.text() || '' );
207
+ } else {
208
+ if (typeof(t) === 'function') {
209
+ return $.trim( t($node[0], c.table, cellIndex) );
210
+ } else if (typeof (te = ts.getColumnData( c.table, t, cellIndex )) === 'function') {
211
+ return $.trim( te($node[0], c.table, cellIndex) );
212
+ }
213
+ }
214
+ // fallback
215
+ return $.trim( $node[0].textContent || $node.text() || '' );
216
+ };
217
+
218
+ function detectParserForColumn(table, rows, rowIndex, cellIndex) {
219
+ var cur, $node,
220
+ c = table.config,
221
+ i = ts.parsers.length,
222
+ node = false,
223
+ nodeValue = '',
224
+ keepLooking = true;
225
+ while (nodeValue === '' && keepLooking) {
226
+ rowIndex++;
227
+ if (rows[rowIndex]) {
228
+ node = rows[rowIndex].cells[cellIndex];
229
+ nodeValue = ts.getElementText(c, node, cellIndex);
230
+ $node = $(node);
231
+ if (table.config.debug) {
232
+ log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': "' + nodeValue + '"');
233
+ }
234
+ } else {
235
+ keepLooking = false;
236
+ }
237
+ }
238
+ while (--i >= 0) {
239
+ cur = ts.parsers[i];
240
+ // ignore the default text parser because it will always be true
241
+ if (cur && cur.id !== 'text' && cur.is && cur.is(nodeValue, table, node, $node)) {
242
+ return cur;
243
+ }
244
+ }
245
+ // nothing found, return the generic parser (text)
246
+ return ts.getParserById('text');
247
+ }
248
+
249
+ function buildParserCache(table) {
250
+ var c = table.config,
251
+ // update table bodies in case we start with an empty table
252
+ tb = c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')'),
253
+ rows, list, l, i, h, ch, np, p, e, time,
254
+ j = 0,
255
+ parsersDebug = '',
256
+ len = tb.length;
257
+ if ( len === 0) {
258
+ return c.debug ? log('Warning: *Empty table!* Not building a parser cache') : '';
259
+ } else if (c.debug) {
260
+ time = new Date();
261
+ log('Detecting parsers for each column');
262
+ }
263
+ list = {
264
+ extractors: [],
265
+ parsers: []
266
+ };
267
+ while (j < len) {
268
+ rows = tb[j].rows;
269
+ if (rows.length) {
270
+ l = c.columns; // rows[j].cells.length;
271
+ for (i = 0; i < l; i++) {
272
+ h = c.$headerIndexed[i];
273
+ // get column indexed table cell
274
+ ch = ts.getColumnData( table, c.headers, i );
275
+ // get column parser/extractor
276
+ e = ts.getParserById( ts.getData(h, ch, 'extractor') );
277
+ p = ts.getParserById( ts.getData(h, ch, 'sorter') );
278
+ np = ts.getData(h, ch, 'parser') === 'false';
279
+ // empty cells behaviour - keeping emptyToBottom for backwards compatibility
280
+ c.empties[i] = ( ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase();
281
+ // text strings behaviour in numerical sorts
282
+ c.strings[i] = ( ts.getData(h, ch, 'string') || c.stringTo || 'max' ).toLowerCase();
283
+ if (np) {
284
+ p = ts.getParserById('no-parser');
285
+ }
286
+ if (!e) {
287
+ // For now, maybe detect someday
288
+ e = false;
289
+ }
290
+ if (!p) {
291
+ p = detectParserForColumn(table, rows, -1, i);
292
+ }
293
+ if (c.debug) {
294
+ parsersDebug += 'column:' + i + '; extractor:' + e.id + '; parser:' + p.id + '; string:' + c.strings[i] + '; empty: ' + c.empties[i] + '\n';
295
+ }
296
+ list.parsers[i] = p;
297
+ list.extractors[i] = e;
298
+ }
299
+ }
300
+ j += (list.parsers.length) ? len : 1;
301
+ }
302
+ if (c.debug) {
303
+ log(parsersDebug ? parsersDebug : 'No parsers detected');
304
+ benchmark('Completed detecting parsers', time);
305
+ }
306
+ c.parsers = list.parsers;
307
+ c.extractors = list.extractors;
308
+ }
309
+
310
+ /* utils */
311
+ function buildCache(table) {
312
+ var cc, t, tx, v, i, j, k, $row, cols, cacheTime,
313
+ totalRows, rowData, colMax,
314
+ c = table.config,
315
+ $tb = c.$tbodies,
316
+ extractors = c.extractors,
317
+ parsers = c.parsers;
318
+ c.cache = {};
319
+ c.totalRows = 0;
320
+ // if no parsers found, return - it's an empty table.
321
+ if (!parsers) {
322
+ return c.debug ? log('Warning: *Empty table!* Not building a cache') : '';
323
+ }
324
+ if (c.debug) {
325
+ cacheTime = new Date();
326
+ }
327
+ // processing icon
328
+ if (c.showProcessing) {
329
+ ts.isProcessing(table, true);
330
+ }
331
+ for (k = 0; k < $tb.length; k++) {
332
+ colMax = []; // column max value per tbody
333
+ cc = c.cache[k] = {
334
+ normalized: [] // array of normalized row data; last entry contains 'rowData' above
335
+ // colMax: # // added at the end
336
+ };
337
+
338
+ totalRows = ($tb[k] && $tb[k].rows.length) || 0;
339
+ for (i = 0; i < totalRows; ++i) {
340
+ rowData = {
341
+ // order: original row order #
342
+ // $row : jQuery Object[]
343
+ child: [], // child row text (filter widget)
344
+ raw: [] // original row text
345
+ };
346
+ /** Add the table data to main data array */
347
+ $row = $($tb[k].rows[i]);
348
+ cols = [];
349
+ // if this is a child row, add it to the last row's children and continue to the next row
350
+ // ignore child row class, if it is the first row
351
+ if ($row.hasClass(c.cssChildRow) && i !== 0) {
352
+ t = cc.normalized.length - 1;
353
+ cc.normalized[t][c.columns].$row = cc.normalized[t][c.columns].$row.add($row);
354
+ // add 'hasChild' class name to parent row
355
+ if (!$row.prev().hasClass(c.cssChildRow)) {
356
+ $row.prev().addClass(ts.css.cssHasChild);
357
+ }
358
+ // save child row content (un-parsed!)
359
+ rowData.child[t] = $.trim( $row[0].textContent || $row.text() || '' );
360
+ // go to the next for loop
361
+ continue;
362
+ }
363
+ rowData.$row = $row;
364
+ rowData.order = i; // add original row position to rowCache
365
+ for (j = 0; j < c.columns; ++j) {
366
+ if (typeof parsers[j] === 'undefined') {
367
+ if (c.debug) {
368
+ log('No parser found for cell:', $row[0].cells[j], 'does it have a header?');
369
+ }
370
+ continue;
371
+ }
372
+ t = ts.getElementText(c, $row[0].cells[j], j);
373
+ rowData.raw.push(t); // save original row text
374
+ // do extract before parsing if there is one
375
+ if (typeof extractors[j].id === 'undefined') {
376
+ tx = t;
377
+ } else {
378
+ tx = extractors[j].format(t, table, $row[0].cells[j], j);
379
+ }
380
+ // allow parsing if the string is empty, previously parsing would change it to zero,
381
+ // in case the parser needs to extract data from the table cell attributes
382
+ v = parsers[j].id === 'no-parser' ? '' : parsers[j].format(tx, table, $row[0].cells[j], j);
383
+ cols.push( c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v );
384
+ if ((parsers[j].type || '').toLowerCase() === 'numeric') {
385
+ // determine column max value (ignore sign)
386
+ colMax[j] = Math.max(Math.abs(v) || 0, colMax[j] || 0);
387
+ }
388
+ }
389
+ // ensure rowData is always in the same location (after the last column)
390
+ cols[c.columns] = rowData;
391
+ cc.normalized.push(cols);
392
+ }
393
+ cc.colMax = colMax;
394
+ // total up rows, not including child rows
395
+ c.totalRows += cc.normalized.length;
396
+
397
+ }
398
+ if (c.showProcessing) {
399
+ ts.isProcessing(table); // remove processing icon
400
+ }
401
+ if (c.debug) {
402
+ benchmark('Building cache for ' + totalRows + ' rows', cacheTime);
403
+ }
404
+ }
405
+
406
+ // init flag (true) used by pager plugin to prevent widget application
407
+ function appendToTable(table, init) {
408
+ var c = table.config,
409
+ wo = c.widgetOptions,
410
+ $tbodies = c.$tbodies,
411
+ rows = [],
412
+ cc = c.cache,
413
+ n, totalRows, $bk, $tb,
414
+ i, k, appendTime;
415
+ // empty table - fixes #206/#346
416
+ if (isEmptyObject(cc)) {
417
+ // run pager appender in case the table was just emptied
418
+ return c.appender ? c.appender(table, rows) :
419
+ table.isUpdating ? c.$table.trigger('updateComplete', table) : ''; // Fixes #532
420
+ }
421
+ if (c.debug) {
422
+ appendTime = new Date();
423
+ }
424
+ for (k = 0; k < $tbodies.length; k++) {
425
+ $bk = $tbodies.eq(k);
426
+ if ($bk.length) {
427
+ // get tbody
428
+ $tb = ts.processTbody(table, $bk, true);
429
+ n = cc[k].normalized;
430
+ totalRows = n.length;
431
+ for (i = 0; i < totalRows; i++) {
432
+ rows.push(n[i][c.columns].$row);
433
+ // removeRows used by the pager plugin; don't render if using ajax - fixes #411
434
+ if (!c.appender || (c.pager && (!c.pager.removeRows || !wo.pager_removeRows) && !c.pager.ajax)) {
435
+ $tb.append(n[i][c.columns].$row);
436
+ }
437
+ }
438
+ // restore tbody
439
+ ts.processTbody(table, $tb, false);
440
+ }
441
+ }
442
+ if (c.appender) {
443
+ c.appender(table, rows);
444
+ }
445
+ if (c.debug) {
446
+ benchmark('Rebuilt table', appendTime);
447
+ }
448
+ // apply table widgets; but not before ajax completes
449
+ if (!init && !c.appender) { ts.applyWidget(table); }
450
+ if (table.isUpdating) {
451
+ c.$table.trigger('updateComplete', table);
452
+ }
453
+ }
454
+
455
+ function formatSortingOrder(v) {
456
+ // look for 'd' in 'desc' order; return true
457
+ return (/^d/i.test(v) || v === 1);
458
+ }
459
+
460
+ function buildHeaders(table) {
461
+ var ch, $t, h, i, t, lock, time, indx,
462
+ c = table.config;
463
+ c.headerList = [];
464
+ c.headerContent = [];
465
+ if (c.debug) {
466
+ time = new Date();
467
+ }
468
+ // children tr in tfoot - see issue #196 & #547
469
+ c.columns = ts.computeColumnIndex( c.$table.children('thead, tfoot').children('tr') );
470
+ // add icon if cssIcon option exists
471
+ i = c.cssIcon ? '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' : '';
472
+ // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683
473
+ c.$headers = $( $.map( $(table).find(c.selectorHeaders), function(elem, index) {
474
+ $t = $(elem);
475
+ // ignore cell (don't add it to c.$headers) if row has ignoreRow class
476
+ if ($t.parent().hasClass(c.cssIgnoreRow)) { return; }
477
+ // make sure to get header cell & not column indexed cell
478
+ ch = ts.getColumnData( table, c.headers, index, true );
479
+ // save original header content
480
+ c.headerContent[index] = $t.html();
481
+ // if headerTemplate is empty, don't reformat the header cell
482
+ if ( c.headerTemplate !== '' && !$t.find('.' + ts.css.headerIn).length ) {
483
+ // set up header template
484
+ t = c.headerTemplate.replace(/\{content\}/g, $t.html()).replace(/\{icon\}/g, $t.find('.' + ts.css.icon).length ? '' : i);
485
+ if (c.onRenderTemplate) {
486
+ h = c.onRenderTemplate.apply($t, [index, t]);
487
+ if (h && typeof h === 'string') { t = h; } // only change t if something is returned
488
+ }
489
+ $t.html('<div class="' + ts.css.headerIn + '">' + t + '</div>'); // faster than wrapInner
490
+ }
491
+ if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index, c, c.$table]); }
492
+ // *** remove this.column value if no conflicts found
493
+ elem.column = parseInt( $t.attr('data-column'), 10);
494
+ elem.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2];
495
+ elem.count = -1; // set to -1 because clicking on the header automatically adds one
496
+ elem.lockedOrder = false;
497
+ lock = ts.getData($t, ch, 'lockedOrder') || false;
498
+ if (typeof lock !== 'undefined' && lock !== false) {
499
+ elem.order = elem.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0];
500
+ }
501
+ $t.addClass(ts.css.header + ' ' + c.cssHeader);
502
+ // add cell to headerList
503
+ c.headerList[index] = elem;
504
+ // add to parent in case there are multiple rows
505
+ $t.parent().addClass(ts.css.headerRow + ' ' + c.cssHeaderRow).attr('role', 'row');
506
+ // allow keyboard cursor to focus on element
507
+ if (c.tabIndex) { $t.attr('tabindex', 0); }
508
+ return elem;
509
+ }));
510
+ // cache headers per column
511
+ c.$headerIndexed = [];
512
+ for (indx = 0; indx < c.columns; indx++) {
513
+ $t = c.$headers.filter('[data-column="' + indx + '"]');
514
+ // target sortable column cells, unless there are none, then use non-sortable cells
515
+ // .last() added in jQuery 1.4; use .filter(':last') to maintain compatibility with jQuery v1.2.6
516
+ c.$headerIndexed[indx] = $t.not('.sorter-false').length ? $t.not('.sorter-false').filter(':last') : $t.filter(':last');
517
+ }
518
+ $(table).find(c.selectorHeaders).attr({
519
+ scope: 'col',
520
+ role : 'columnheader'
521
+ });
522
+ // enable/disable sorting
523
+ updateHeader(table);
524
+ if (c.debug) {
525
+ benchmark('Built headers:', time);
526
+ log(c.$headers);
527
+ }
528
+ }
529
+
530
+ function commonUpdate(table, resort, callback) {
531
+ var c = table.config;
532
+ // remove rows/elements before update
533
+ c.$table.find(c.selectorRemove).remove();
534
+ // rebuild parsers
535
+ buildParserCache(table);
536
+ // rebuild the cache map
537
+ buildCache(table);
538
+ checkResort(c, resort, callback);
539
+ }
540
+
541
+ function updateHeader(table) {
542
+ var s, $th, col,
543
+ c = table.config;
544
+ c.$headers.each(function(index, th){
545
+ $th = $(th);
546
+ col = ts.getColumnData( table, c.headers, index, true );
547
+ // add 'sorter-false' class if 'parser-false' is set
548
+ s = ts.getData( th, col, 'sorter' ) === 'false' || ts.getData( th, col, 'parser' ) === 'false';
549
+ th.sortDisabled = s;
550
+ $th[ s ? 'addClass' : 'removeClass' ]('sorter-false').attr('aria-disabled', '' + s);
551
+ // aria-controls - requires table ID
552
+ if (table.id) {
553
+ if (s) {
554
+ $th.removeAttr('aria-controls');
555
+ } else {
556
+ $th.attr('aria-controls', table.id);
557
+ }
558
+ }
559
+ });
560
+ }
561
+
562
+ function setHeadersCss(table) {
563
+ var f, i, j,
564
+ c = table.config,
565
+ list = c.sortList,
566
+ len = list.length,
567
+ none = ts.css.sortNone + ' ' + c.cssNone,
568
+ css = [ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc],
569
+ cssIcon = [ c.cssIconAsc, c.cssIconDesc, c.cssIconNone ],
570
+ aria = ['ascending', 'descending'],
571
+ // find the footer
572
+ $t = $(table).find('tfoot tr').children()
573
+ .add( $( c.namespace + '_extra_headers' ) )
574
+ .removeClass( css.join( ' ' ) );
575
+ // remove all header information
576
+ c.$headers
577
+ .removeClass(css.join(' '))
578
+ .addClass(none).attr('aria-sort', 'none')
579
+ .find('.' + ts.css.icon)
580
+ .removeClass(cssIcon.join(' '))
581
+ .addClass(cssIcon[2]);
582
+ for (i = 0; i < len; i++) {
583
+ // direction = 2 means reset!
584
+ if (list[i][1] !== 2) {
585
+ // multicolumn sorting updating - choose the :last in case there are nested columns
586
+ f = c.$headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (len === 1 ? ':last' : '') );
587
+ if (f.length) {
588
+ for (j = 0; j < f.length; j++) {
589
+ if (!f[j].sortDisabled) {
590
+ f.eq(j)
591
+ .removeClass(none)
592
+ .addClass(css[list[i][1]])
593
+ .attr('aria-sort', aria[list[i][1]])
594
+ .find('.' + ts.css.icon)
595
+ .removeClass(cssIcon[2])
596
+ .addClass(cssIcon[list[i][1]]);
597
+ }
598
+ }
599
+ // add sorted class to footer & extra headers, if they exist
600
+ if ($t.length) {
601
+ $t.filter('[data-column="' + list[i][0] + '"]').removeClass(none).addClass(css[list[i][1]]);
602
+ }
603
+ }
604
+ }
605
+ }
606
+ // add verbose aria labels
607
+ c.$headers.not('.sorter-false').each(function(){
608
+ var $this = $(this),
609
+ nextSort = this.order[(this.count + 1) % (c.sortReset ? 3 : 2)],
610
+ txt = $.trim( $this.text() ) + ': ' +
611
+ ts.language[ $this.hasClass(ts.css.sortAsc) ? 'sortAsc' : $this.hasClass(ts.css.sortDesc) ? 'sortDesc' : 'sortNone' ] +
612
+ ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ];
613
+ $this.attr('aria-label', txt );
614
+ });
615
+ }
616
+
617
+ function updateHeaderSortCount( table, list ) {
618
+ var col, dir, group, header, indx, primary, temp, val,
619
+ c = table.config,
620
+ sortList = list || c.sortList,
621
+ len = sortList.length;
622
+ c.sortList = [];
623
+ for (indx = 0; indx < len; indx++) {
624
+ val = sortList[indx];
625
+ // ensure all sortList values are numeric - fixes #127
626
+ col = parseInt(val[0], 10);
627
+ // make sure header exists
628
+ header = c.$headerIndexed[col][0];
629
+ if (header) { // prevents error if sorton array is wrong
630
+ // o.count = o.count + 1;
631
+ dir = ('' + val[1]).match(/^(1|d|s|o|n)/);
632
+ dir = dir ? dir[0] : '';
633
+ // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext
634
+ switch(dir) {
635
+ case '1': case 'd': // descending
636
+ dir = 1;
637
+ break;
638
+ case 's': // same direction (as primary column)
639
+ // if primary sort is set to 's', make it ascending
640
+ dir = primary || 0;
641
+ break;
642
+ case 'o':
643
+ temp = header.order[(primary || 0) % (c.sortReset ? 3 : 2)];
644
+ // opposite of primary column; but resets if primary resets
645
+ dir = temp === 0 ? 1 : temp === 1 ? 0 : 2;
646
+ break;
647
+ case 'n':
648
+ header.count = header.count + 1;
649
+ dir = header.order[(header.count) % (c.sortReset ? 3 : 2)];
650
+ break;
651
+ default: // ascending
652
+ dir = 0;
653
+ break;
654
+ }
655
+ primary = indx === 0 ? dir : primary;
656
+ group = [ col, parseInt(dir, 10) || 0 ];
657
+ c.sortList.push(group);
658
+ dir = $.inArray(group[1], header.order); // fixes issue #167
659
+ header.count = dir >= 0 ? dir : group[1] % (c.sortReset ? 3 : 2);
660
+ }
661
+ }
662
+ }
663
+
664
+ function getCachedSortType(parsers, i) {
665
+ return (parsers && parsers[i]) ? parsers[i].type || '' : '';
666
+ }
667
+
668
+ function initSort(table, cell, event){
669
+ if (table.isUpdating) {
670
+ // let any updates complete before initializing a sort
671
+ return setTimeout(function(){ initSort(table, cell, event); }, 50);
672
+ }
673
+ var arry, indx, col, order, s,
674
+ c = table.config,
675
+ key = !event[c.sortMultiSortKey],
676
+ $table = c.$table;
677
+ // Only call sortStart if sorting is enabled
678
+ $table.trigger('sortStart', table);
679
+ // get current column sort order
680
+ cell.count = event[c.sortResetKey] ? 2 : (cell.count + 1) % (c.sortReset ? 3 : 2);
681
+ // reset all sorts on non-current column - issue #30
682
+ if (c.sortRestart) {
683
+ indx = cell;
684
+ c.$headers.each(function() {
685
+ // only reset counts on columns that weren't just clicked on and if not included in a multisort
686
+ if (this !== indx && (key || !$(this).is('.' + ts.css.sortDesc + ',.' + ts.css.sortAsc))) {
687
+ this.count = -1;
688
+ }
689
+ });
690
+ }
691
+ // get current column index
692
+ indx = parseInt( $(cell).attr('data-column'), 10 );
693
+ // user only wants to sort on one column
694
+ if (key) {
695
+ // flush the sort list
696
+ c.sortList = [];
697
+ if (c.sortForce !== null) {
698
+ arry = c.sortForce;
699
+ for (col = 0; col < arry.length; col++) {
700
+ if (arry[col][0] !== indx) {
701
+ c.sortList.push(arry[col]);
702
+ }
703
+ }
704
+ }
705
+ // add column to sort list
706
+ order = cell.order[cell.count];
707
+ if (order < 2) {
708
+ c.sortList.push([indx, order]);
709
+ // add other columns if header spans across multiple
710
+ if (cell.colSpan > 1) {
711
+ for (col = 1; col < cell.colSpan; col++) {
712
+ c.sortList.push([indx + col, order]);
713
+ }
714
+ }
715
+ }
716
+ // multi column sorting
717
+ } else {
718
+ // get rid of the sortAppend before adding more - fixes issue #115 & #523
719
+ if (c.sortAppend && c.sortList.length > 1) {
720
+ for (col = 0; col < c.sortAppend.length; col++) {
721
+ s = ts.isValueInArray(c.sortAppend[col][0], c.sortList);
722
+ if (s >= 0) {
723
+ c.sortList.splice(s,1);
724
+ }
725
+ }
726
+ }
727
+ // the user has clicked on an already sorted column
728
+ if (ts.isValueInArray(indx, c.sortList) >= 0) {
729
+ // reverse the sorting direction
730
+ for (col = 0; col < c.sortList.length; col++) {
731
+ s = c.sortList[col];
732
+ order = c.$headerIndexed[ s[0] ][0];
733
+ if (s[0] === indx) {
734
+ // order.count seems to be incorrect when compared to cell.count
735
+ s[1] = order.order[cell.count];
736
+ if (s[1] === 2) {
737
+ c.sortList.splice(col,1);
738
+ order.count = -1;
739
+ }
740
+ }
741
+ }
742
+ } else {
743
+ // add column to sort list array
744
+ order = cell.order[cell.count];
745
+ if (order < 2) {
746
+ c.sortList.push([indx, order]);
747
+ // add other columns if header spans across multiple
748
+ if (cell.colSpan > 1) {
749
+ for (col = 1; col < cell.colSpan; col++) {
750
+ c.sortList.push([indx + col, order]);
751
+ }
752
+ }
753
+ }
754
+ }
755
+ }
756
+ if (c.sortAppend !== null) {
757
+ arry = c.sortAppend;
758
+ for (col = 0; col < arry.length; col++) {
759
+ if (arry[col][0] !== indx) {
760
+ c.sortList.push(arry[col]);
761
+ }
762
+ }
763
+ }
764
+ // sortBegin event triggered immediately before the sort
765
+ $table.trigger('sortBegin', table);
766
+ // setTimeout needed so the processing icon shows up
767
+ setTimeout(function(){
768
+ // set css for headers
769
+ setHeadersCss(table);
770
+ multisort(table);
771
+ appendToTable(table);
772
+ $table.trigger('sortEnd', table);
773
+ }, 1);
774
+ }
775
+
776
+ // sort multiple columns
777
+ function multisort(table) { /*jshint loopfunc:true */
778
+ var i, k, num, col, sortTime, colMax,
779
+ rows, order, sort, x, y,
780
+ dir = 0,
781
+ c = table.config,
782
+ cts = c.textSorter || '',
783
+ sortList = c.sortList,
784
+ l = sortList.length,
785
+ bl = c.$tbodies.length;
786
+ if (c.serverSideSorting || isEmptyObject(c.cache)) { // empty table - fixes #206/#346
787
+ return;
788
+ }
789
+ if (c.debug) { sortTime = new Date(); }
790
+ for (k = 0; k < bl; k++) {
791
+ colMax = c.cache[k].colMax;
792
+ rows = c.cache[k].normalized;
793
+
794
+ rows.sort(function(a, b) {
795
+ // rows is undefined here in IE, so don't use it!
796
+ for (i = 0; i < l; i++) {
797
+ col = sortList[i][0];
798
+ order = sortList[i][1];
799
+ // sort direction, true = asc, false = desc
800
+ dir = order === 0;
801
+
802
+ if (c.sortStable && a[col] === b[col] && l === 1) {
803
+ return a[c.columns].order - b[c.columns].order;
804
+ }
805
+
806
+ // fallback to natural sort since it is more robust
807
+ num = /n/i.test(getCachedSortType(c.parsers, col));
808
+ if (num && c.strings[col]) {
809
+ // sort strings in numerical columns
810
+ if (typeof (c.string[c.strings[col]]) === 'boolean') {
811
+ num = (dir ? 1 : -1) * (c.string[c.strings[col]] ? -1 : 1);
812
+ } else {
813
+ num = (c.strings[col]) ? c.string[c.strings[col]] || 0 : 0;
814
+ }
815
+ // fall back to built-in numeric sort
816
+ // var sort = $.tablesorter['sort' + s](table, a[c], b[c], c, colMax[c], dir);
817
+ sort = c.numberSorter ? c.numberSorter(a[col], b[col], dir, colMax[col], table) :
818
+ ts[ 'sortNumeric' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], num, colMax[col], col, table);
819
+ } else {
820
+ // set a & b depending on sort direction
821
+ x = dir ? a : b;
822
+ y = dir ? b : a;
823
+ // text sort function
824
+ if (typeof(cts) === 'function') {
825
+ // custom OVERALL text sorter
826
+ sort = cts(x[col], y[col], dir, col, table);
827
+ } else if (typeof(cts) === 'object' && cts.hasOwnProperty(col)) {
828
+ // custom text sorter for a SPECIFIC COLUMN
829
+ sort = cts[col](x[col], y[col], dir, col, table);
830
+ } else {
831
+ // fall back to natural sort
832
+ sort = ts[ 'sortNatural' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], col, table, c);
833
+ }
834
+ }
835
+ if (sort) { return sort; }
836
+ }
837
+ return a[c.columns].order - b[c.columns].order;
838
+ });
839
+ }
840
+ if (c.debug) { benchmark('Sorting on ' + sortList.toString() + ' and dir ' + order + ' time', sortTime); }
841
+ }
842
+
843
+ function resortComplete(c, callback){
844
+ if (c.table.isUpdating) {
845
+ c.$table.trigger('updateComplete', c.table);
846
+ }
847
+ if ($.isFunction(callback)) {
848
+ callback(c.table);
849
+ }
850
+ }
851
+
852
+ function checkResort(c, resort, callback) {
853
+ var sl = $.isArray(resort) ? resort : c.sortList,
854
+ // if no resort parameter is passed, fallback to config.resort (true by default)
855
+ resrt = typeof resort === 'undefined' ? c.resort : resort;
856
+ // don't try to resort if the table is still processing
857
+ // this will catch spamming of the updateCell method
858
+ if (resrt !== false && !c.serverSideSorting && !c.table.isProcessing) {
859
+ if (sl.length) {
860
+ c.$table.trigger('sorton', [sl, function(){
861
+ resortComplete(c, callback);
862
+ }, true]);
863
+ } else {
864
+ c.$table.trigger('sortReset', [function(){
865
+ resortComplete(c, callback);
866
+ ts.applyWidget(c.table, false);
867
+ }]);
868
+ }
869
+ } else {
870
+ resortComplete(c, callback);
871
+ ts.applyWidget(c.table, false);
872
+ }
873
+ }
874
+
875
+ function bindMethods(table){
876
+ var c = table.config,
877
+ $table = c.$table,
878
+ events = ('sortReset update updateRows updateCell updateAll addRows updateComplete sorton appendCache ' +
879
+ 'updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave ').split(' ')
880
+ .join(c.namespace + ' ');
881
+ // apply easy methods that trigger bound events
882
+ $table
883
+ .unbind( events.replace(/\s+/g, ' ') )
884
+ .bind('sortReset' + c.namespace, function(e, callback){
885
+ e.stopPropagation();
886
+ c.sortList = [];
887
+ setHeadersCss(table);
888
+ multisort(table);
889
+ appendToTable(table);
890
+ if ($.isFunction(callback)) {
891
+ callback(table);
892
+ }
893
+ })
894
+ .bind('updateAll' + c.namespace, function(e, resort, callback){
895
+ e.stopPropagation();
896
+ table.isUpdating = true;
897
+ ts.refreshWidgets(table, true, true);
898
+ buildHeaders(table);
899
+ ts.bindEvents(table, c.$headers, true);
900
+ bindMethods(table);
901
+ commonUpdate(table, resort, callback);
902
+ })
903
+ .bind('update' + c.namespace + ' updateRows' + c.namespace, function(e, resort, callback) {
904
+ e.stopPropagation();
905
+ table.isUpdating = true;
906
+ // update sorting (if enabled/disabled)
907
+ updateHeader(table);
908
+ commonUpdate(table, resort, callback);
909
+ })
910
+ .bind('updateCell' + c.namespace, function(e, cell, resort, callback) {
911
+ e.stopPropagation();
912
+ table.isUpdating = true;
913
+ $table.find(c.selectorRemove).remove();
914
+ // get position from the dom
915
+ var v, t, row, icell,
916
+ $tb = c.$tbodies,
917
+ $cell = $(cell),
918
+ // update cache - format: function(s, table, cell, cellIndex)
919
+ // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr');
920
+ tbdy = $tb.index( $.fn.closest ? $cell.closest('tbody') : $cell.parents('tbody').filter(':first') ),
921
+ $row = $.fn.closest ? $cell.closest('tr') : $cell.parents('tr').filter(':first');
922
+ cell = $cell[0]; // in case cell is a jQuery object
923
+ // tbody may not exist if update is initialized while tbody is removed for processing
924
+ if ($tb.length && tbdy >= 0) {
925
+ row = $tb.eq(tbdy).find('tr').index( $row );
926
+ icell = $cell.index();
927
+ c.cache[tbdy].normalized[row][c.columns].$row = $row;
928
+ if (typeof c.extractors[icell].id === 'undefined') {
929
+ t = ts.getElementText(c, cell, icell);
930
+ } else {
931
+ t = c.extractors[icell].format( ts.getElementText(c, cell, icell), table, cell, icell );
932
+ }
933
+ v = c.parsers[icell].id === 'no-parser' ? '' :
934
+ c.parsers[icell].format( t, table, cell, icell );
935
+ c.cache[tbdy].normalized[row][icell] = c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v;
936
+ if ((c.parsers[icell].type || '').toLowerCase() === 'numeric') {
937
+ // update column max value (ignore sign)
938
+ c.cache[tbdy].colMax[icell] = Math.max(Math.abs(v) || 0, c.cache[tbdy].colMax[icell] || 0);
939
+ }
940
+ v = resort !== 'undefined' ? resort : c.resort;
941
+ if (v !== false) {
942
+ // widgets will be reapplied
943
+ checkResort(c, v, callback);
944
+ } else {
945
+ // don't reapply widgets is resort is false, just in case it causes
946
+ // problems with element focus
947
+ if ($.isFunction(callback)) {
948
+ callback(table);
949
+ }
950
+ c.$table.trigger('updateComplete', c.table);
951
+ }
952
+ }
953
+ })
954
+ .bind('addRows' + c.namespace, function(e, $row, resort, callback) {
955
+ e.stopPropagation();
956
+ table.isUpdating = true;
957
+ if (isEmptyObject(c.cache)) {
958
+ // empty table, do an update instead - fixes #450
959
+ updateHeader(table);
960
+ commonUpdate(table, resort, callback);
961
+ } else {
962
+ $row = $($row).attr('role', 'row'); // make sure we're using a jQuery object
963
+ var i, j, l, t, v, rowData, cells,
964
+ rows = $row.filter('tr').length,
965
+ tbdy = c.$tbodies.index( $row.parents('tbody').filter(':first') );
966
+ // fixes adding rows to an empty table - see issue #179
967
+ if (!(c.parsers && c.parsers.length)) {
968
+ buildParserCache(table);
969
+ }
970
+ // add each row
971
+ for (i = 0; i < rows; i++) {
972
+ l = $row[i].cells.length;
973
+ cells = [];
974
+ rowData = {
975
+ child: [],
976
+ $row : $row.eq(i),
977
+ order: c.cache[tbdy].normalized.length
978
+ };
979
+ // add each cell
980
+ for (j = 0; j < l; j++) {
981
+ if (typeof c.extractors[j].id === 'undefined') {
982
+ t = ts.getElementText(c, $row[i].cells[j], j);
983
+ } else {
984
+ t = c.extractors[j].format( ts.getElementText(c, $row[i].cells[j], j), table, $row[i].cells[j], j );
985
+ }
986
+ v = c.parsers[j].id === 'no-parser' ? '' :
987
+ c.parsers[j].format( t, table, $row[i].cells[j], j );
988
+ cells[j] = c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v;
989
+ if ((c.parsers[j].type || '').toLowerCase() === 'numeric') {
990
+ // update column max value (ignore sign)
991
+ c.cache[tbdy].colMax[j] = Math.max(Math.abs(cells[j]) || 0, c.cache[tbdy].colMax[j] || 0);
992
+ }
993
+ }
994
+ // add the row data to the end
995
+ cells.push(rowData);
996
+ // update cache
997
+ c.cache[tbdy].normalized.push(cells);
998
+ }
999
+ // resort using current settings
1000
+ checkResort(c, resort, callback);
1001
+ }
1002
+ })
1003
+ .bind('updateComplete' + c.namespace, function(){
1004
+ table.isUpdating = false;
1005
+ })
1006
+ .bind('sorton' + c.namespace, function(e, list, callback, init) {
1007
+ var c = table.config;
1008
+ e.stopPropagation();
1009
+ $table.trigger('sortStart', this);
1010
+ // update header count index
1011
+ updateHeaderSortCount(table, list);
1012
+ // set css for headers
1013
+ setHeadersCss(table);
1014
+ // fixes #346
1015
+ if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); }
1016
+ $table.trigger('sortBegin', this);
1017
+ // sort the table and append it to the dom
1018
+ multisort(table);
1019
+ appendToTable(table, init);
1020
+ $table.trigger('sortEnd', this);
1021
+ ts.applyWidget(table);
1022
+ if ($.isFunction(callback)) {
1023
+ callback(table);
1024
+ }
1025
+ })
1026
+ .bind('appendCache' + c.namespace, function(e, callback, init) {
1027
+ e.stopPropagation();
1028
+ appendToTable(table, init);
1029
+ if ($.isFunction(callback)) {
1030
+ callback(table);
1031
+ }
1032
+ })
1033
+ .bind('updateCache' + c.namespace, function(e, callback){
1034
+ // rebuild parsers
1035
+ if (!(c.parsers && c.parsers.length)) {
1036
+ buildParserCache(table);
1037
+ }
1038
+ // rebuild the cache map
1039
+ buildCache(table);
1040
+ if ($.isFunction(callback)) {
1041
+ callback(table);
1042
+ }
1043
+ })
1044
+ .bind('applyWidgetId' + c.namespace, function(e, id) {
1045
+ e.stopPropagation();
1046
+ ts.getWidgetById(id).format(table, c, c.widgetOptions);
1047
+ })
1048
+ .bind('applyWidgets' + c.namespace, function(e, init) {
1049
+ e.stopPropagation();
1050
+ // apply widgets
1051
+ ts.applyWidget(table, init);
1052
+ })
1053
+ .bind('refreshWidgets' + c.namespace, function(e, all, dontapply){
1054
+ e.stopPropagation();
1055
+ ts.refreshWidgets(table, all, dontapply);
1056
+ })
1057
+ .bind('destroy' + c.namespace, function(e, c, cb){
1058
+ e.stopPropagation();
1059
+ ts.destroy(table, c, cb);
1060
+ })
1061
+ .bind('resetToLoadState' + c.namespace, function(){
1062
+ // remove all widgets
1063
+ ts.removeWidget(table, true, false);
1064
+ // restore original settings; this clears out current settings, but does not clear
1065
+ // values saved to storage.
1066
+ c = $.extend(true, ts.defaults, c.originalSettings);
1067
+ table.hasInitialized = false;
1068
+ // setup the entire table again
1069
+ ts.setup( table, c );
1070
+ });
1071
+ }
1072
+
1073
+ /* public methods */
1074
+ ts.construct = function(settings) {
1075
+ return this.each(function() {
1076
+ var table = this,
1077
+ // merge & extend config options
1078
+ c = $.extend(true, {}, ts.defaults, settings, ts.instanceMethods);
1079
+ // save initial settings
1080
+ c.originalSettings = settings;
1081
+ // create a table from data (build table widget)
1082
+ if (!table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE') {
1083
+ // return the table (in case the original target is the table's container)
1084
+ ts.buildTable(table, c);
1085
+ } else {
1086
+ ts.setup(table, c);
1087
+ }
1088
+ });
1089
+ };
1090
+
1091
+ ts.setup = function(table, c) {
1092
+ // if no thead or tbody, or tablesorter is already present, quit
1093
+ if (!table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true) {
1094
+ return c.debug ? log('ERROR: stopping initialization! No table, thead, tbody or tablesorter has already been initialized') : '';
1095
+ }
1096
+
1097
+ var k = '',
1098
+ $table = $(table),
1099
+ m = $.metadata;
1100
+ // initialization flag
1101
+ table.hasInitialized = false;
1102
+ // table is being processed flag
1103
+ table.isProcessing = true;
1104
+ // make sure to store the config object
1105
+ table.config = c;
1106
+ // save the settings where they read
1107
+ $.data(table, 'tablesorter', c);
1108
+ if (c.debug) { $.data( table, 'startoveralltimer', new Date()); }
1109
+
1110
+ // removing this in version 3 (only supports jQuery 1.7+)
1111
+ c.supportsDataObject = (function(version) {
1112
+ version[0] = parseInt(version[0], 10);
1113
+ return (version[0] > 1) || (version[0] === 1 && parseInt(version[1], 10) >= 4);
1114
+ })($.fn.jquery.split('.'));
1115
+ // digit sort text location; keeping max+/- for backwards compatibility
1116
+ c.string = { 'max': 1, 'min': -1, 'emptymin': 1, 'emptymax': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false };
1117
+ // ensure case insensitivity
1118
+ c.emptyTo = c.emptyTo.toLowerCase();
1119
+ c.stringTo = c.stringTo.toLowerCase();
1120
+ // add table theme class only if there isn't already one there
1121
+ if (!/tablesorter\-/.test($table.attr('class'))) {
1122
+ k = (c.theme !== '' ? ' tablesorter-' + c.theme : '');
1123
+ }
1124
+ c.table = table;
1125
+ c.$table = $table
1126
+ .addClass(ts.css.table + ' ' + c.tableClass + k)
1127
+ .attr('role', 'grid');
1128
+ c.$headers = $table.find(c.selectorHeaders);
1129
+
1130
+ // give the table a unique id, which will be used in namespace binding
1131
+ if (!c.namespace) {
1132
+ c.namespace = '.tablesorter' + Math.random().toString(16).slice(2);
1133
+ } else {
1134
+ // make sure namespace starts with a period & doesn't have weird characters
1135
+ c.namespace = '.' + c.namespace.replace(/\W/g,'');
1136
+ }
1137
+
1138
+ c.$table.children().children('tr').attr('role', 'row');
1139
+ c.$tbodies = $table.children('tbody:not(.' + c.cssInfoBlock + ')').attr({
1140
+ 'aria-live' : 'polite',
1141
+ 'aria-relevant' : 'all'
1142
+ });
1143
+ if (c.$table.children('caption').length) {
1144
+ k = c.$table.children('caption')[0];
1145
+ if (!k.id) { k.id = c.namespace.slice(1) + 'caption'; }
1146
+ c.$table.attr('aria-labelledby', k.id);
1147
+ }
1148
+ c.widgetInit = {}; // keep a list of initialized widgets
1149
+ // change textExtraction via data-attribute
1150
+ c.textExtraction = c.$table.attr('data-text-extraction') || c.textExtraction || 'basic';
1151
+ // build headers
1152
+ buildHeaders(table);
1153
+ // fixate columns if the users supplies the fixedWidth option
1154
+ // do this after theme has been applied
1155
+ ts.fixColumnWidth(table);
1156
+ // add widget options before parsing (e.g. grouping widget has parser settings)
1157
+ ts.applyWidgetOptions(table, c);
1158
+ // try to auto detect column type, and store in tables config
1159
+ buildParserCache(table);
1160
+ // start total row count at zero
1161
+ c.totalRows = 0;
1162
+ // build the cache for the tbody cells
1163
+ // delayInit will delay building the cache until the user starts a sort
1164
+ if (!c.delayInit) { buildCache(table); }
1165
+ // bind all header events and methods
1166
+ ts.bindEvents(table, c.$headers, true);
1167
+ bindMethods(table);
1168
+ // get sort list from jQuery data or metadata
1169
+ // in jQuery < 1.4, an error occurs when calling $table.data()
1170
+ if (c.supportsDataObject && typeof $table.data().sortlist !== 'undefined') {
1171
+ c.sortList = $table.data().sortlist;
1172
+ } else if (m && ($table.metadata() && $table.metadata().sortlist)) {
1173
+ c.sortList = $table.metadata().sortlist;
1174
+ }
1175
+ // apply widget init code
1176
+ ts.applyWidget(table, true);
1177
+ // if user has supplied a sort list to constructor
1178
+ if (c.sortList.length > 0) {
1179
+ $table.trigger('sorton', [c.sortList, {}, !c.initWidgets, true]);
1180
+ } else {
1181
+ setHeadersCss(table);
1182
+ if (c.initWidgets) {
1183
+ // apply widget format
1184
+ ts.applyWidget(table, false);
1185
+ }
1186
+ }
1187
+
1188
+ // show processesing icon
1189
+ if (c.showProcessing) {
1190
+ $table
1191
+ .unbind('sortBegin' + c.namespace + ' sortEnd' + c.namespace)
1192
+ .bind('sortBegin' + c.namespace + ' sortEnd' + c.namespace, function(e) {
1193
+ clearTimeout(c.processTimer);
1194
+ ts.isProcessing(table);
1195
+ if (e.type === 'sortBegin') {
1196
+ c.processTimer = setTimeout(function(){
1197
+ ts.isProcessing(table, true);
1198
+ }, 500);
1199
+ }
1200
+ });
1201
+ }
1202
+
1203
+ // initialized
1204
+ table.hasInitialized = true;
1205
+ table.isProcessing = false;
1206
+ if (c.debug) {
1207
+ ts.benchmark('Overall initialization time', $.data( table, 'startoveralltimer'));
1208
+ }
1209
+ $table.trigger('tablesorter-initialized', table);
1210
+ if (typeof c.initialized === 'function') { c.initialized(table); }
1211
+ };
1212
+
1213
+ // automatically add a colgroup with col elements set to a percentage width
1214
+ ts.fixColumnWidth = function(table) {
1215
+ table = $(table)[0];
1216
+ var overallWidth, percent,
1217
+ c = table.config,
1218
+ colgroup = c.$table.children('colgroup');
1219
+ // remove plugin-added colgroup, in case we need to refresh the widths
1220
+ if (colgroup.length && colgroup.hasClass(ts.css.colgroup)) {
1221
+ colgroup.remove();
1222
+ }
1223
+ if (c.widthFixed && c.$table.children('colgroup').length === 0) {
1224
+ colgroup = $('<colgroup class="' + ts.css.colgroup + '">');
1225
+ overallWidth = c.$table.width();
1226
+ // only add col for visible columns - fixes #371
1227
+ c.$tbodies.find('tr:first').children(':visible').each(function() {
1228
+ percent = parseInt( ( $(this).width() / overallWidth ) * 1000, 10 ) / 10 + '%';
1229
+ colgroup.append( $('<col>').css('width', percent) );
1230
+ });
1231
+ c.$table.prepend(colgroup);
1232
+ }
1233
+ };
1234
+
1235
+ ts.getColumnData = function(table, obj, indx, getCell, $headers){
1236
+ if (typeof obj === 'undefined' || obj === null) { return; }
1237
+ table = $(table)[0];
1238
+ var $h, k,
1239
+ c = table.config,
1240
+ $cells = ( $headers || c.$headers ),
1241
+ // c.$headerIndexed is not defined initially
1242
+ $cell = c.$headerIndexed && c.$headerIndexed[indx] || $cells.filter('[data-column="' + indx + '"]:last');
1243
+ if (obj[indx]) {
1244
+ return getCell ? obj[indx] : obj[$cells.index( $cell )];
1245
+ }
1246
+ for (k in obj) {
1247
+ if (typeof k === 'string') {
1248
+ $h = $cell
1249
+ // header cell with class/id
1250
+ .filter(k)
1251
+ // find elements within the header cell with cell/id
1252
+ .add( $cell.find(k) );
1253
+ if ($h.length) {
1254
+ return obj[k];
1255
+ }
1256
+ }
1257
+ }
1258
+ return;
1259
+ };
1260
+
1261
+ // computeTableHeaderCellIndexes from:
1262
+ // http://www.javascripttoolbox.com/lib/table/examples.php
1263
+ // http://www.javascripttoolbox.com/temp/table_cellindex.html
1264
+ ts.computeColumnIndex = function(trs) {
1265
+ var matrix = [],
1266
+ lookup = {},
1267
+ i, j, k, l, $cell, cell, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow;
1268
+ for (i = 0; i < trs.length; i++) {
1269
+ cells = trs[i].cells;
1270
+ for (j = 0; j < cells.length; j++) {
1271
+ cell = cells[j];
1272
+ $cell = $(cell);
1273
+ rowIndex = cell.parentNode.rowIndex;
1274
+ cellId = rowIndex + '-' + $cell.index();
1275
+ rowSpan = cell.rowSpan || 1;
1276
+ colSpan = cell.colSpan || 1;
1277
+ if (typeof(matrix[rowIndex]) === 'undefined') {
1278
+ matrix[rowIndex] = [];
1279
+ }
1280
+ // Find first available column in the first row
1281
+ for (k = 0; k < matrix[rowIndex].length + 1; k++) {
1282
+ if (typeof(matrix[rowIndex][k]) === 'undefined') {
1283
+ firstAvailCol = k;
1284
+ break;
1285
+ }
1286
+ }
1287
+ lookup[cellId] = firstAvailCol;
1288
+ // add data-column
1289
+ $cell.attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex
1290
+ for (k = rowIndex; k < rowIndex + rowSpan; k++) {
1291
+ if (typeof(matrix[k]) === 'undefined') {
1292
+ matrix[k] = [];
1293
+ }
1294
+ matrixrow = matrix[k];
1295
+ for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
1296
+ matrixrow[l] = 'x';
1297
+ }
1298
+ }
1299
+ }
1300
+ }
1301
+ return matrixrow.length;
1302
+ };
1303
+
1304
+ // *** Process table ***
1305
+ // add processing indicator
1306
+ ts.isProcessing = function(table, toggle, $ths) {
1307
+ table = $(table);
1308
+ var c = table[0].config,
1309
+ // default to all headers
1310
+ $h = $ths || table.find('.' + ts.css.header);
1311
+ if (toggle) {
1312
+ // don't use sortList if custom $ths used
1313
+ if (typeof $ths !== 'undefined' && c.sortList.length > 0) {
1314
+ // get headers from the sortList
1315
+ $h = $h.filter(function(){
1316
+ // get data-column from attr to keep compatibility with jQuery 1.2.6
1317
+ return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList) >= 0;
1318
+ });
1319
+ }
1320
+ table.add($h).addClass(ts.css.processing + ' ' + c.cssProcessing);
1321
+ } else {
1322
+ table.add($h).removeClass(ts.css.processing + ' ' + c.cssProcessing);
1323
+ }
1324
+ };
1325
+
1326
+ // detach tbody but save the position
1327
+ // don't use tbody because there are portions that look for a tbody index (updateCell)
1328
+ ts.processTbody = function(table, $tb, getIt){
1329
+ table = $(table)[0];
1330
+ var holdr;
1331
+ if (getIt) {
1332
+ table.isProcessing = true;
1333
+ $tb.before('<span class="tablesorter-savemyplace"/>');
1334
+ holdr = ($.fn.detach) ? $tb.detach() : $tb.remove();
1335
+ return holdr;
1336
+ }
1337
+ holdr = $(table).find('span.tablesorter-savemyplace');
1338
+ $tb.insertAfter( holdr );
1339
+ holdr.remove();
1340
+ table.isProcessing = false;
1341
+ };
1342
+
1343
+ ts.clearTableBody = function(table) {
1344
+ $(table)[0].config.$tbodies.children().detach();
1345
+ };
1346
+
1347
+ ts.bindEvents = function(table, $headers, core){
1348
+ table = $(table)[0];
1349
+ var t, downTarget = null,
1350
+ c = table.config;
1351
+ if (core !== true) {
1352
+ $headers.addClass( c.namespace.slice(1) + '_extra_headers' );
1353
+ t = $.fn.closest ? $headers.closest('table')[0] : $headers.parents('table')[0];
1354
+ if (t && t.nodeName === 'TABLE' && t !== table) {
1355
+ $(t).addClass( c.namespace.slice(1) + '_extra_table' );
1356
+ }
1357
+ }
1358
+ // apply event handling to headers and/or additional headers (stickyheaders, scroller, etc)
1359
+ $headers
1360
+ // http://stackoverflow.com/questions/5312849/jquery-find-self;
1361
+ .find(c.selectorSort).add( $headers.filter(c.selectorSort) )
1362
+ .unbind( ('mousedown mouseup click sort keyup '.split(' ').join(c.namespace + ' ')).replace(/\s+/g, ' ') )
1363
+ .bind( 'mousedown mouseup click sort keyup '.split(' ').join(c.namespace + ' '), function(e, external) {
1364
+ var cell,
1365
+ $target = $(e.target),
1366
+ type = e.type;
1367
+ // only recognize left clicks
1368
+ if ( ( ( e.which || e.button ) !== 1 && !/sort|keyup|click/.test(type) ) ||
1369
+ // allow pressing enter
1370
+ ( type === 'keyup' && e.which !== 13 ) ||
1371
+ // allow triggering a click event (e.which is undefined) & ignore physical clicks
1372
+ ( type === 'click' && typeof e.which !== 'undefined' ) ) {
1373
+ return;
1374
+ }
1375
+ // ignore mouseup if mousedown wasn't on the same target
1376
+ if ( type === 'mouseup' && downTarget !== e.target && external !== true ) { return; }
1377
+ // set timer on mousedown
1378
+ if ( type === 'mousedown' ) {
1379
+ downTarget = e.target;
1380
+ return;
1381
+ }
1382
+ downTarget = null;
1383
+ // prevent sort being triggered on form elements
1384
+ if ( /(input|select|button|textarea)/i.test(e.target.nodeName) ||
1385
+ // nosort class name, or elements within a nosort container
1386
+ $target.hasClass(c.cssNoSort) || $target.parents('.' + c.cssNoSort).length > 0 ||
1387
+ // elements within a button
1388
+ $target.parents('button').length > 0 ) {
1389
+ return !c.cancelSelection;
1390
+ }
1391
+ if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); }
1392
+ // jQuery v1.2.6 doesn't have closest()
1393
+ cell = $.fn.closest ? $(this).closest('th, td')[0] : /TH|TD/.test(this.nodeName) ? this : $(this).parents('th, td')[0];
1394
+ // reference original table headers and find the same cell
1395
+ cell = c.$headers[ $headers.index( cell ) ];
1396
+ if (!cell.sortDisabled) {
1397
+ initSort(table, cell, e);
1398
+ }
1399
+ });
1400
+ if (c.cancelSelection) {
1401
+ // cancel selection
1402
+ $headers
1403
+ .attr('unselectable', 'on')
1404
+ .bind('selectstart', false)
1405
+ .css({
1406
+ 'user-select': 'none',
1407
+ 'MozUserSelect': 'none' // not needed for jQuery 1.8+
1408
+ });
1409
+ }
1410
+ };
1411
+
1412
+ // restore headers
1413
+ ts.restoreHeaders = function(table){
1414
+ var $cell,
1415
+ c = $(table)[0].config;
1416
+ // don't use c.$headers here in case header cells were swapped
1417
+ c.$table.find(c.selectorHeaders).each(function(i){
1418
+ $cell = $(this);
1419
+ // only restore header cells if it is wrapped
1420
+ // because this is also used by the updateAll method
1421
+ if ($cell.find('.' + ts.css.headerIn).length){
1422
+ $cell.html( c.headerContent[i] );
1423
+ }
1424
+ });
1425
+ };
1426
+
1427
+ ts.destroy = function(table, removeClasses, callback){
1428
+ table = $(table)[0];
1429
+ if (!table.hasInitialized) { return; }
1430
+ // remove all widgets
1431
+ ts.removeWidget(table, true, false);
1432
+ var events,
1433
+ $t = $(table),
1434
+ c = table.config,
1435
+ $h = $t.find('thead:first'),
1436
+ $r = $h.find('tr.' + ts.css.headerRow).removeClass(ts.css.headerRow + ' ' + c.cssHeaderRow),
1437
+ $f = $t.find('tfoot:first > tr').children('th, td');
1438
+ if (removeClasses === false && $.inArray('uitheme', c.widgets) >= 0) {
1439
+ // reapply uitheme classes, in case we want to maintain appearance
1440
+ $t.trigger('applyWidgetId', ['uitheme']);
1441
+ $t.trigger('applyWidgetId', ['zebra']);
1442
+ }
1443
+ // remove widget added rows, just in case
1444
+ $h.find('tr').not($r).remove();
1445
+ // disable tablesorter
1446
+ events = 'sortReset update updateAll updateRows updateCell addRows updateComplete sorton appendCache updateCache ' +
1447
+ 'applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave keypress sortBegin sortEnd resetToLoadState '.split(' ')
1448
+ .join(c.namespace + ' ');
1449
+ $t
1450
+ .removeData('tablesorter')
1451
+ .unbind( events.replace(/\s+/g, ' ') );
1452
+ c.$headers.add($f)
1453
+ .removeClass( [ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone].join(' ') )
1454
+ .removeAttr('data-column')
1455
+ .removeAttr('aria-label')
1456
+ .attr('aria-disabled', 'true');
1457
+ $r.find(c.selectorSort).unbind( ('mousedown mouseup keypress '.split(' ').join(c.namespace + ' ')).replace(/\s+/g, ' ') );
1458
+ ts.restoreHeaders(table);
1459
+ $t.toggleClass(ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false);
1460
+ // clear flag in case the plugin is initialized again
1461
+ table.hasInitialized = false;
1462
+ delete table.config.cache;
1463
+ if (typeof callback === 'function') {
1464
+ callback(table);
1465
+ }
1466
+ };
1467
+
1468
+ // *** sort functions ***
1469
+ // regex used in natural sort
1470
+ ts.regex = {
1471
+ chunk : /(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, // chunk/tokenize numbers & letters
1472
+ chunks: /(^\\0|\\0$)/, // replace chunks @ ends
1473
+ hex: /^0x[0-9a-f]+$/i // hex
1474
+ };
1475
+
1476
+ // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed)
1477
+ // this function will only accept strings, or you'll see 'TypeError: undefined is not a function'
1478
+ // I could add a = a.toString(); b = b.toString(); but it'll slow down the sort overall
1479
+ ts.sortNatural = function(a, b) {
1480
+ if (a === b) { return 0; }
1481
+ var xN, xD, yN, yD, xF, yF, i, mx,
1482
+ r = ts.regex;
1483
+ // first try and sort Hex codes
1484
+ if (r.hex.test(b)) {
1485
+ xD = parseInt(a.match(r.hex), 16);
1486
+ yD = parseInt(b.match(r.hex), 16);
1487
+ if ( xD < yD ) { return -1; }
1488
+ if ( xD > yD ) { return 1; }
1489
+ }
1490
+ // chunk/tokenize
1491
+ xN = a.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0');
1492
+ yN = b.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0');
1493
+ mx = Math.max(xN.length, yN.length);
1494
+ // natural sorting through split numeric strings and default strings
1495
+ for (i = 0; i < mx; i++) {
1496
+ // find floats not starting with '0', string or 0 if not defined
1497
+ xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0;
1498
+ yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0;
1499
+ // handle numeric vs string comparison - number < string - (Kyle Adams)
1500
+ if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; }
1501
+ // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
1502
+ if (typeof xF !== typeof yF) {
1503
+ xF += '';
1504
+ yF += '';
1505
+ }
1506
+ if (xF < yF) { return -1; }
1507
+ if (xF > yF) { return 1; }
1508
+ }
1509
+ return 0;
1510
+ };
1511
+
1512
+ ts.sortNaturalAsc = function(a, b, col, table, c) {
1513
+ if (a === b) { return 0; }
1514
+ var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1515
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1516
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1517
+ return ts.sortNatural(a, b);
1518
+ };
1519
+
1520
+ ts.sortNaturalDesc = function(a, b, col, table, c) {
1521
+ if (a === b) { return 0; }
1522
+ var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1523
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1524
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1525
+ return ts.sortNatural(b, a);
1526
+ };
1527
+
1528
+ // basic alphabetical sort
1529
+ ts.sortText = function(a, b) {
1530
+ return a > b ? 1 : (a < b ? -1 : 0);
1531
+ };
1532
+
1533
+ // return text string value by adding up ascii value
1534
+ // so the text is somewhat sorted when using a digital sort
1535
+ // this is NOT an alphanumeric sort
1536
+ ts.getTextValue = function(a, num, mx) {
1537
+ if (mx) {
1538
+ // make sure the text value is greater than the max numerical value (mx)
1539
+ var i, l = a ? a.length : 0, n = mx + num;
1540
+ for (i = 0; i < l; i++) {
1541
+ n += a.charCodeAt(i);
1542
+ }
1543
+ return num * n;
1544
+ }
1545
+ return 0;
1546
+ };
1547
+
1548
+ ts.sortNumericAsc = function(a, b, num, mx, col, table) {
1549
+ if (a === b) { return 0; }
1550
+ var c = table.config,
1551
+ e = c.string[ (c.empties[col] || c.emptyTo ) ];
1552
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1553
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1554
+ if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1555
+ if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1556
+ return a - b;
1557
+ };
1558
+
1559
+ ts.sortNumericDesc = function(a, b, num, mx, col, table) {
1560
+ if (a === b) { return 0; }
1561
+ var c = table.config,
1562
+ e = c.string[ (c.empties[col] || c.emptyTo ) ];
1563
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1564
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1565
+ if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1566
+ if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1567
+ return b - a;
1568
+ };
1569
+
1570
+ ts.sortNumeric = function(a, b) {
1571
+ return a - b;
1572
+ };
1573
+
1574
+ // used when replacing accented characters during sorting
1575
+ ts.characterEquivalents = {
1576
+ 'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãäąå
1577
+ 'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄÅ
1578
+ 'c' : '\u00e7\u0107\u010d', // çćč
1579
+ 'C' : '\u00c7\u0106\u010c', // ÇĆČ
1580
+ 'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę
1581
+ 'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ
1582
+ 'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı
1583
+ 'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ
1584
+ 'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6', // óòôõö
1585
+ 'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6', // ÓÒÔÕÖ
1586
+ 'ss': '\u00df', // ß (s sharp)
1587
+ 'SS': '\u1e9e', // ẞ (Capital sharp s)
1588
+ 'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů
1589
+ 'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ
1590
+ };
1591
+ ts.replaceAccents = function(s) {
1592
+ var a, acc = '[', eq = ts.characterEquivalents;
1593
+ if (!ts.characterRegex) {
1594
+ ts.characterRegexArray = {};
1595
+ for (a in eq) {
1596
+ if (typeof a === 'string') {
1597
+ acc += eq[a];
1598
+ ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g');
1599
+ }
1600
+ }
1601
+ ts.characterRegex = new RegExp(acc + ']');
1602
+ }
1603
+ if (ts.characterRegex.test(s)) {
1604
+ for (a in eq) {
1605
+ if (typeof a === 'string') {
1606
+ s = s.replace( ts.characterRegexArray[a], a );
1607
+ }
1608
+ }
1609
+ }
1610
+ return s;
1611
+ };
1612
+
1613
+ // *** utilities ***
1614
+ ts.isValueInArray = function(column, arry) {
1615
+ var indx, len = arry.length;
1616
+ for (indx = 0; indx < len; indx++) {
1617
+ if (arry[indx][0] === column) {
1618
+ return indx;
1619
+ }
1620
+ }
1621
+ return -1;
1622
+ };
1623
+
1624
+ ts.addParser = function(parser) {
1625
+ var i, l = ts.parsers.length, a = true;
1626
+ for (i = 0; i < l; i++) {
1627
+ if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) {
1628
+ a = false;
1629
+ }
1630
+ }
1631
+ if (a) {
1632
+ ts.parsers.push(parser);
1633
+ }
1634
+ };
1635
+
1636
+ // Use it to add a set of methods to table.config which will be available for all tables.
1637
+ // This should be done before table initialization
1638
+ ts.addInstanceMethods = function(methods) {
1639
+ $.extend(ts.instanceMethods, methods);
1640
+ };
1641
+
1642
+ ts.getParserById = function(name) {
1643
+ /*jshint eqeqeq:false */
1644
+ if (name == 'false') { return false; }
1645
+ var i, l = ts.parsers.length;
1646
+ for (i = 0; i < l; i++) {
1647
+ if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) {
1648
+ return ts.parsers[i];
1649
+ }
1650
+ }
1651
+ return false;
1652
+ };
1653
+
1654
+ ts.addWidget = function(widget) {
1655
+ ts.widgets.push(widget);
1656
+ };
1657
+
1658
+ ts.hasWidget = function(table, name){
1659
+ table = $(table);
1660
+ return table.length && table[0].config && table[0].config.widgetInit[name] || false;
1661
+ };
1662
+
1663
+ ts.getWidgetById = function(name) {
1664
+ var i, w, l = ts.widgets.length;
1665
+ for (i = 0; i < l; i++) {
1666
+ w = ts.widgets[i];
1667
+ if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) {
1668
+ return w;
1669
+ }
1670
+ }
1671
+ };
1672
+
1673
+ ts.applyWidgetOptions = function( table, c ){
1674
+ var indx, widget,
1675
+ len = c.widgets.length,
1676
+ wo = c.widgetOptions;
1677
+ if (len) {
1678
+ for (indx = 0; indx < len; indx++) {
1679
+ widget = ts.getWidgetById( c.widgets[indx] );
1680
+ if ( widget && 'options' in widget ) {
1681
+ wo = table.config.widgetOptions = $.extend( true, {}, widget.options, wo );
1682
+ }
1683
+ }
1684
+ }
1685
+ };
1686
+
1687
+ ts.applyWidget = function(table, init, callback) {
1688
+ table = $(table)[0]; // in case this is called externally
1689
+ var indx, len, name,
1690
+ c = table.config,
1691
+ wo = c.widgetOptions,
1692
+ tableClass = ' ' + c.table.className + ' ',
1693
+ widgets = [],
1694
+ time, time2, w, wd;
1695
+ // prevent numerous consecutive widget applications
1696
+ if (init !== false && table.hasInitialized && (table.isApplyingWidgets || table.isUpdating)) { return; }
1697
+ if (c.debug) { time = new Date(); }
1698
+ // look for widgets to apply from in table class
1699
+ // stop using \b otherwise this matches 'ui-widget-content' & adds 'content' widget
1700
+ wd = new RegExp( '\\s' + c.widgetClass.replace( /\{name\}/i, '([\\w-]+)' )+ '\\s', 'g' );
1701
+ if ( tableClass.match( wd ) ) {
1702
+ // extract out the widget id from the table class (widget id's can include dashes)
1703
+ w = tableClass.match( wd );
1704
+ if ( w ) {
1705
+ len = w.length;
1706
+ for (indx = 0; indx < len; indx++) {
1707
+ c.widgets.push( w[indx].replace( wd, '$1' ) );
1708
+ }
1709
+ }
1710
+ }
1711
+ if (c.widgets.length) {
1712
+ table.isApplyingWidgets = true;
1713
+ // ensure unique widget ids
1714
+ c.widgets = $.grep(c.widgets, function(v, k){
1715
+ return $.inArray(v, c.widgets) === k;
1716
+ });
1717
+ name = c.widgets || [];
1718
+ len = name.length;
1719
+ // build widget array & add priority as needed
1720
+ for (indx = 0; indx < len; indx++) {
1721
+ wd = ts.getWidgetById(name[indx]);
1722
+ if (wd && wd.id) {
1723
+ // set priority to 10 if not defined
1724
+ if (!wd.priority) { wd.priority = 10; }
1725
+ widgets[indx] = wd;
1726
+ }
1727
+ }
1728
+ // sort widgets by priority
1729
+ widgets.sort(function(a, b){
1730
+ return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1;
1731
+ });
1732
+ // add/update selected widgets
1733
+ len = widgets.length;
1734
+ for (indx = 0; indx < len; indx++) {
1735
+ if (widgets[indx]) {
1736
+ if ( init || !( c.widgetInit[ widgets[indx].id ] ) ) {
1737
+ // set init flag first to prevent calling init more than once (e.g. pager)
1738
+ c.widgetInit[ widgets[indx].id ] = true;
1739
+ if (table.hasInitialized) {
1740
+ // don't reapply widget options on tablesorter init
1741
+ ts.applyWidgetOptions( table, c );
1742
+ }
1743
+ if ( 'init' in widgets[indx] ) {
1744
+ if (c.debug) { time2 = new Date(); }
1745
+ widgets[indx].init(table, widgets[indx], c, wo);
1746
+ if (c.debug) { ts.benchmark('Initializing ' + widgets[indx].id + ' widget', time2); }
1747
+ }
1748
+ }
1749
+ if ( !init && 'format' in widgets[indx] ) {
1750
+ if (c.debug) { time2 = new Date(); }
1751
+ widgets[indx].format(table, c, wo, false);
1752
+ if (c.debug) { ts.benchmark( ( init ? 'Initializing ' : 'Applying ' ) + widgets[indx].id + ' widget', time2); }
1753
+ }
1754
+ }
1755
+ }
1756
+ // callback executed on init only
1757
+ if (!init && typeof callback === 'function') {
1758
+ callback(table);
1759
+ }
1760
+ }
1761
+ setTimeout(function(){
1762
+ table.isApplyingWidgets = false;
1763
+ $.data(table, 'lastWidgetApplication', new Date());
1764
+ }, 0);
1765
+ if (c.debug) {
1766
+ w = c.widgets.length;
1767
+ benchmark('Completed ' + (init === true ? 'initializing ' : 'applying ') + w + ' widget' + (w !== 1 ? 's' : ''), time);
1768
+ }
1769
+ };
1770
+
1771
+ ts.removeWidget = function(table, name, refreshing){
1772
+ table = $(table)[0];
1773
+ var i, widget, indx, len,
1774
+ c = table.config;
1775
+ // if name === true, add all widgets from $.tablesorter.widgets
1776
+ if (name === true) {
1777
+ name = [];
1778
+ len = ts.widgets.length;
1779
+ for (indx = 0; indx < len; indx++) {
1780
+ widget = ts.widgets[indx];
1781
+ if (widget && widget.id) {
1782
+ name.push( widget.id );
1783
+ }
1784
+ }
1785
+ } else {
1786
+ // name can be either an array of widgets names,
1787
+ // or a space/comma separated list of widget names
1788
+ name = ( $.isArray(name) ? name.join(',') : name || '' ).toLowerCase().split( /[\s,]+/ );
1789
+ }
1790
+ len = name.length;
1791
+ for (i = 0; i < len; i++) {
1792
+ widget = ts.getWidgetById(name[i]);
1793
+ indx = $.inArray( name[i], c.widgets );
1794
+ if ( widget && 'remove' in widget ) {
1795
+ if (c.debug && indx >= 0) { log( 'Removing "' + name[i] + '" widget' ); }
1796
+ widget.remove(table, c, c.widgetOptions, refreshing);
1797
+ c.widgetInit[ name[i] ] = false;
1798
+ }
1799
+ // don't remove the widget from config.widget if refreshing
1800
+ if (indx >= 0 && refreshing !== true) {
1801
+ c.widgets.splice( indx, 1 );
1802
+ }
1803
+ }
1804
+ };
1805
+
1806
+ ts.refreshWidgets = function(table, doAll, dontapply) {
1807
+ table = $(table)[0]; // see issue #243
1808
+ var indx,
1809
+ c = table.config,
1810
+ cw = c.widgets,
1811
+ widgets = ts.widgets,
1812
+ len = widgets.length,
1813
+ list = [],
1814
+ callback = function(table){
1815
+ $(table).trigger('refreshComplete');
1816
+ };
1817
+ // remove widgets not defined in config.widgets, unless doAll is true
1818
+ for (indx = 0; indx < len; indx++) {
1819
+ if (widgets[indx] && widgets[indx].id && (doAll || $.inArray( widgets[indx].id, cw ) < 0)) {
1820
+ list.push( widgets[indx].id );
1821
+ }
1822
+ }
1823
+ ts.removeWidget( table, list.join(','), true );
1824
+ if (dontapply !== true) {
1825
+ // call widget init if
1826
+ ts.applyWidget(table, doAll || false, callback );
1827
+ if (doAll) {
1828
+ // apply widget format
1829
+ ts.applyWidget(table, false, callback);
1830
+ }
1831
+ } else {
1832
+ callback(table);
1833
+ }
1834
+ };
1835
+
1836
+ ts.getColumnText = function( table, column, callback ) {
1837
+ table = $( table )[0];
1838
+ var tbodyIndex, rowIndex, cache, row, tbodyLen, rowLen, raw, parsed, $cell, result,
1839
+ hasCallback = typeof callback === 'function',
1840
+ allColumns = column === 'all',
1841
+ data = { raw : [], parsed: [], $cell: [] },
1842
+ c = table.config;
1843
+ if ( !isEmptyObject( c ) ) {
1844
+ tbodyLen = c.$tbodies.length;
1845
+ for ( tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++ ) {
1846
+ cache = c.cache[ tbodyIndex ].normalized;
1847
+ rowLen = cache.length;
1848
+ for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) {
1849
+ result = true;
1850
+ row = cache[ rowIndex ];
1851
+ parsed = ( allColumns ) ? row.slice(0, c.columns) : row[ column ];
1852
+ row = row[ c.columns ];
1853
+ raw = ( allColumns ) ? row.raw : row.raw[ column ];
1854
+ $cell = ( allColumns ) ? row.$row.children() : row.$row.children().eq( column );
1855
+ if ( hasCallback ) {
1856
+ result = callback({
1857
+ tbodyIndex: tbodyIndex,
1858
+ rowIndex: rowIndex,
1859
+ parsed: parsed,
1860
+ raw: raw,
1861
+ $row: row.$row,
1862
+ $cell: $cell
1863
+ });
1864
+ }
1865
+ if ( result !== false ) {
1866
+ data.parsed.push( parsed );
1867
+ data.raw.push( raw );
1868
+ data.$cell.push( $cell );
1869
+ }
1870
+ }
1871
+ }
1872
+ // return everything
1873
+ return data;
1874
+ }
1875
+ };
1876
+
1877
+ // get sorter, string, empty, etc options for each column from
1878
+ // jQuery data, metadata, header option or header class name ('sorter-false')
1879
+ // priority = jQuery data > meta > headers option > header class name
1880
+ ts.getData = function(h, ch, key) {
1881
+ var val = '', $h = $(h), m, cl;
1882
+ if (!$h.length) { return ''; }
1883
+ m = $.metadata ? $h.metadata() : false;
1884
+ cl = ' ' + ($h.attr('class') || '');
1885
+ if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){
1886
+ // 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder'
1887
+ // 'data-sort-initial-order' is assigned to 'sortInitialOrder'
1888
+ val += $h.data(key) || $h.data(key.toLowerCase());
1889
+ } else if (m && typeof m[key] !== 'undefined') {
1890
+ val += m[key];
1891
+ } else if (ch && typeof ch[key] !== 'undefined') {
1892
+ val += ch[key];
1893
+ } else if (cl !== ' ' && cl.match(' ' + key + '-')) {
1894
+ // include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser'
1895
+ val = cl.match( new RegExp('\\s' + key + '-([\\w-]+)') )[1] || '';
1896
+ }
1897
+ return $.trim(val);
1898
+ };
1899
+
1900
+ ts.formatFloat = function(s, table) {
1901
+ if (typeof s !== 'string' || s === '') { return s; }
1902
+ // allow using formatFloat without a table; defaults to US number format
1903
+ var i,
1904
+ t = table && table.config ? table.config.usNumberFormat !== false :
1905
+ typeof table !== 'undefined' ? table : true;
1906
+ if (t) {
1907
+ // US Format - 1,234,567.89 -> 1234567.89
1908
+ s = s.replace(/,/g,'');
1909
+ } else {
1910
+ // German Format = 1.234.567,89 -> 1234567.89
1911
+ // French Format = 1 234 567,89 -> 1234567.89
1912
+ s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.');
1913
+ }
1914
+ if(/^\s*\([.\d]+\)/.test(s)) {
1915
+ // make (#) into a negative number -> (10) = -10
1916
+ s = s.replace(/^\s*\(([.\d]+)\)/, '-$1');
1917
+ }
1918
+ i = parseFloat(s);
1919
+ // return the text instead of zero
1920
+ return isNaN(i) ? $.trim(s) : i;
1921
+ };
1922
+
1923
+ ts.isDigit = function(s) {
1924
+ // replace all unwanted chars and match
1925
+ return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'"\s]/g, '')) : true;
1926
+ };
1927
+
1928
+ }()
1929
+ });
1930
+
1931
+ // make shortcut
1932
+ var ts = $.tablesorter;
1933
+
1934
+ // extend plugin scope
1935
+ $.fn.extend({
1936
+ tablesorter: ts.construct
1937
+ });
1938
+
1939
+ // add default parsers
1940
+ ts.addParser({
1941
+ id: 'no-parser',
1942
+ is: function() {
1943
+ return false;
1944
+ },
1945
+ format: function() {
1946
+ return '';
1947
+ },
1948
+ type: 'text'
1949
+ });
1950
+
1951
+ ts.addParser({
1952
+ id: 'text',
1953
+ is: function() {
1954
+ return true;
1955
+ },
1956
+ format: function(s, table) {
1957
+ var c = table.config;
1958
+ if (s) {
1959
+ s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s );
1960
+ s = c.sortLocaleCompare ? ts.replaceAccents(s) : s;
1961
+ }
1962
+ return s;
1963
+ },
1964
+ type: 'text'
1965
+ });
1966
+
1967
+ ts.addParser({
1968
+ id: 'digit',
1969
+ is: function(s) {
1970
+ return ts.isDigit(s);
1971
+ },
1972
+ format: function(s, table) {
1973
+ var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ''), table);
1974
+ return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
1975
+ },
1976
+ type: 'numeric'
1977
+ });
1978
+
1979
+ ts.addParser({
1980
+ id: 'currency',
1981
+ is: function(s) {
1982
+ return (/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/).test((s || '').replace(/[+\-,. ]/g,'')); // £$€¤¥¢
1983
+ },
1984
+ format: function(s, table) {
1985
+ var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ''), table);
1986
+ return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
1987
+ },
1988
+ type: 'numeric'
1989
+ });
1990
+
1991
+ ts.addParser({
1992
+ id: 'url',
1993
+ is: function(s) {
1994
+ return (/^(https?|ftp|file):\/\//).test(s);
1995
+ },
1996
+ format: function(s) {
1997
+ return s ? $.trim(s.replace(/(https?|ftp|file):\/\//, '')) : s;
1998
+ },
1999
+ parsed : true, // filter widget flag
2000
+ type: 'text'
2001
+ });
2002
+
2003
+ ts.addParser({
2004
+ id: 'isoDate',
2005
+ is: function(s) {
2006
+ return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/).test(s);
2007
+ },
2008
+ format: function(s, table) {
2009
+ var date = s ? new Date( s.replace(/-/g, '/') ) : s;
2010
+ return date instanceof Date && isFinite(date) ? date.getTime() : s;
2011
+ },
2012
+ type: 'numeric'
2013
+ });
2014
+
2015
+ ts.addParser({
2016
+ id: 'percent',
2017
+ is: function(s) {
2018
+ return (/(\d\s*?%|%\s*?\d)/).test(s) && s.length < 15;
2019
+ },
2020
+ format: function(s, table) {
2021
+ return s ? ts.formatFloat(s.replace(/%/g, ''), table) : s;
2022
+ },
2023
+ type: 'numeric'
2024
+ });
2025
+
2026
+ // added image parser to core v2.17.9
2027
+ ts.addParser({
2028
+ id: 'image',
2029
+ is: function(s, table, node, $node){
2030
+ return $node.find('img').length > 0;
2031
+ },
2032
+ format: function(s, table, cell) {
2033
+ return $(cell).find('img').attr(table.config.imgAttr || 'alt') || s;
2034
+ },
2035
+ parsed : true, // filter widget flag
2036
+ type: 'text'
2037
+ });
2038
+
2039
+ ts.addParser({
2040
+ id: 'usLongDate',
2041
+ is: function(s) {
2042
+ // two digit years are not allowed cross-browser
2043
+ // Jan 01, 2013 12:34:56 PM or 01 Jan 2013
2044
+ return (/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i).test(s) || (/^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i).test(s);
2045
+ },
2046
+ format: function(s, table) {
2047
+ var date = s ? new Date( s.replace(/(\S)([AP]M)$/i, '$1 $2') ) : s;
2048
+ return date instanceof Date && isFinite(date) ? date.getTime() : s;
2049
+ },
2050
+ type: 'numeric'
2051
+ });
2052
+
2053
+ ts.addParser({
2054
+ id: 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd'
2055
+ is: function(s) {
2056
+ // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included
2057
+ return (/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/).test((s || '').replace(/\s+/g,' ').replace(/[\-.,]/g, '/'));
2058
+ },
2059
+ format: function(s, table, cell, cellIndex) {
2060
+ if (s) {
2061
+ var date, d,
2062
+ c = table.config,
2063
+ ci = c.$headerIndexed[ cellIndex ],
2064
+ format = ci.length && ci[0].dateFormat || ts.getData( ci, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat') || c.dateFormat;
2065
+ d = s.replace(/\s+/g, ' ').replace(/[\-.,]/g, '/'); // escaped - because JSHint in Firefox was showing it as an error
2066
+ if (format === 'mmddyyyy') {
2067
+ d = d.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, '$3/$1/$2');
2068
+ } else if (format === 'ddmmyyyy') {
2069
+ d = d.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, '$3/$2/$1');
2070
+ } else if (format === 'yyyymmdd') {
2071
+ d = d.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, '$1/$2/$3');
2072
+ }
2073
+ date = new Date(d);
2074
+ return date instanceof Date && isFinite(date) ? date.getTime() : s;
2075
+ }
2076
+ return s;
2077
+ },
2078
+ type: 'numeric'
2079
+ });
2080
+
2081
+ ts.addParser({
2082
+ id: 'time',
2083
+ is: function(s) {
2084
+ return (/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s);
2085
+ },
2086
+ format: function(s, table) {
2087
+ var date = s ? new Date( '2000/01/01 ' + s.replace(/(\S)([AP]M)$/i, '$1 $2') ) : s;
2088
+ return date instanceof Date && isFinite(date) ? date.getTime() : s;
2089
+ },
2090
+ type: 'numeric'
2091
+ });
2092
+
2093
+ ts.addParser({
2094
+ id: 'metadata',
2095
+ is: function() {
2096
+ return false;
2097
+ },
2098
+ format: function(s, table, cell) {
2099
+ var c = table.config,
2100
+ p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
2101
+ return $(cell).metadata()[p];
2102
+ },
2103
+ type: 'numeric'
2104
+ });
2105
+
2106
+ // add default widgets
2107
+ ts.addWidget({
2108
+ id: 'zebra',
2109
+ priority: 90,
2110
+ format: function(table, c, wo) {
2111
+ var $tb, $tv, $tr, row, even, time, k,
2112
+ child = new RegExp(c.cssChildRow, 'i'),
2113
+ b = c.$tbodies.add( $( c.namespace + '_extra_table' ).children( 'tbody' ) );
2114
+ if (c.debug) {
2115
+ time = new Date();
2116
+ }
2117
+ for (k = 0; k < b.length; k++ ) {
2118
+ // loop through the visible rows
2119
+ row = 0;
2120
+ $tb = b.eq(k);
2121
+ $tv = $tb.children('tr:visible').not(c.selectorRemove);
2122
+ // revered back to using jQuery each - strangely it's the fastest method
2123
+ /*jshint loopfunc:true */
2124
+ $tv.each(function(){
2125
+ $tr = $(this);
2126
+ // style child rows the same way the parent row was styled
2127
+ if (!child.test(this.className)) { row++; }
2128
+ even = (row % 2 === 0);
2129
+ $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]);
2130
+ });
2131
+ }
2132
+ },
2133
+ remove: function(table, c, wo, refreshing){
2134
+ if (refreshing) { return; }
2135
+ var k, $tb,
2136
+ b = c.$tbodies,
2137
+ rmv = (wo.zebra || [ 'even', 'odd' ]).join(' ');
2138
+ for (k = 0; k < b.length; k++ ){
2139
+ $tb = ts.processTbody(table, b.eq(k), true); // remove tbody
2140
+ $tb.children().removeClass(rmv);
2141
+ ts.processTbody(table, $tb, false); // restore tbody
2142
+ }
2143
+ }
2144
+ });
2145
+
2146
+ })(jQuery);
2147
+
2148
+ /*! Widget: storage - updated 3/26/2015 (v2.21.3) */
2149
+ ;(function ($, window, document) {
2150
+ 'use strict';
2151
+
2152
+ var ts = $.tablesorter = $.tablesorter || {};
2153
+ // *** Store data in local storage, with a cookie fallback ***
2154
+ /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json)
2155
+ if you need it, then include https://github.com/douglascrockford/JSON-js
2156
+
2157
+ $.parseJSON is not available is jQuery versions older than 1.4.1, using older
2158
+ versions will only allow storing information for one page at a time
2159
+
2160
+ // *** Save data (JSON format only) ***
2161
+ // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid
2162
+ var val = { "mywidget" : "data1" }; // valid JSON uses double quotes
2163
+ // $.tablesorter.storage(table, key, val);
2164
+ $.tablesorter.storage(table, 'tablesorter-mywidget', val);
2165
+
2166
+ // *** Get data: $.tablesorter.storage(table, key); ***
2167
+ v = $.tablesorter.storage(table, 'tablesorter-mywidget');
2168
+ // val may be empty, so also check for your data
2169
+ val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : '';
2170
+ alert(val); // "data1" if saved, or "" if not
2171
+ */
2172
+ ts.storage = function(table, key, value, options) {
2173
+ table = $(table)[0];
2174
+ var cookieIndex, cookies, date,
2175
+ hasStorage = false,
2176
+ values = {},
2177
+ c = table.config,
2178
+ wo = c && c.widgetOptions,
2179
+ storageType = ( options && options.useSessionStorage ) || ( wo && wo.storage_useSessionStorage ) ?
2180
+ 'sessionStorage' : 'localStorage',
2181
+ $table = $(table),
2182
+ // id from (1) options ID, (2) table "data-table-group" attribute, (3) widgetOptions.storage_tableId,
2183
+ // (4) table ID, then (5) table index
2184
+ id = options && options.id ||
2185
+ $table.attr( options && options.group || wo && wo.storage_group || 'data-table-group') ||
2186
+ wo && wo.storage_tableId || table.id || $('.tablesorter').index( $table ),
2187
+ // url from (1) options url, (2) table "data-table-page" attribute, (3) widgetOptions.storage_fixedUrl,
2188
+ // (4) table.config.fixedUrl (deprecated), then (5) window location path
2189
+ url = options && options.url ||
2190
+ $table.attr(options && options.page || wo && wo.storage_page || 'data-table-page') ||
2191
+ wo && wo.storage_fixedUrl || c && c.fixedUrl || window.location.pathname;
2192
+ // https://gist.github.com/paulirish/5558557
2193
+ if (storageType in window) {
2194
+ try {
2195
+ window[storageType].setItem('_tmptest', 'temp');
2196
+ hasStorage = true;
2197
+ window[storageType].removeItem('_tmptest');
2198
+ } catch(error) {
2199
+ if (c && c.debug) {
2200
+ ts.log( storageType + ' is not supported in this browser' );
2201
+ }
2202
+ }
2203
+ }
2204
+ // *** get value ***
2205
+ if ($.parseJSON) {
2206
+ if (hasStorage) {
2207
+ values = $.parseJSON( window[storageType][key] || 'null' ) || {};
2208
+ } else {
2209
+ // old browser, using cookies
2210
+ cookies = document.cookie.split(/[;\s|=]/);
2211
+ // add one to get from the key to the value
2212
+ cookieIndex = $.inArray(key, cookies) + 1;
2213
+ values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || 'null') || {} : {};
2214
+ }
2215
+ }
2216
+ // allow value to be an empty string too
2217
+ if ((value || value === '') && window.JSON && JSON.hasOwnProperty('stringify')) {
2218
+ // add unique identifiers = url pathname > table ID/index on page > data
2219
+ if (!values[url]) {
2220
+ values[url] = {};
2221
+ }
2222
+ values[url][id] = value;
2223
+ // *** set value ***
2224
+ if (hasStorage) {
2225
+ window[storageType][key] = JSON.stringify(values);
2226
+ } else {
2227
+ date = new Date();
2228
+ date.setTime(date.getTime() + (31536e+6)); // 365 days
2229
+ document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g,'\"') + '; expires=' + date.toGMTString() + '; path=/';
2230
+ }
2231
+ } else {
2232
+ return values && values[url] ? values[url][id] : '';
2233
+ }
2234
+ };
2235
+
2236
+ })(jQuery, window, document);
2237
+
2238
+ /*! Widget: uitheme - updated 3/26/2015 (v2.21.3) */
2239
+ ;(function ($) {
2240
+ 'use strict';
2241
+ var ts = $.tablesorter = $.tablesorter || {};
2242
+
2243
+ ts.themes = {
2244
+ 'bootstrap' : {
2245
+ table : 'table table-bordered table-striped',
2246
+ caption : 'caption',
2247
+ // header class names
2248
+ header : 'bootstrap-header', // give the header a gradient background (theme.bootstrap_2.css)
2249
+ sortNone : '',
2250
+ sortAsc : '',
2251
+ sortDesc : '',
2252
+ active : '', // applied when column is sorted
2253
+ hover : '', // custom css required - a defined bootstrap style may not override other classes
2254
+ // icon class names
2255
+ icons : '', // add "icon-white" to make them white; this icon class is added to the <i> in the header
2256
+ iconSortNone : 'bootstrap-icon-unsorted', // class name added to icon when column is not sorted
2257
+ iconSortAsc : 'icon-chevron-up glyphicon glyphicon-chevron-up', // class name added to icon when column has ascending sort
2258
+ iconSortDesc : 'icon-chevron-down glyphicon glyphicon-chevron-down', // class name added to icon when column has descending sort
2259
+ filterRow : '', // filter row class
2260
+ footerRow : '',
2261
+ footerCells : '',
2262
+ even : '', // even row zebra striping
2263
+ odd : '' // odd row zebra striping
2264
+ },
2265
+ 'jui' : {
2266
+ table : 'ui-widget ui-widget-content ui-corner-all', // table classes
2267
+ caption : 'ui-widget-content',
2268
+ // header class names
2269
+ header : 'ui-widget-header ui-corner-all ui-state-default', // header classes
2270
+ sortNone : '',
2271
+ sortAsc : '',
2272
+ sortDesc : '',
2273
+ active : 'ui-state-active', // applied when column is sorted
2274
+ hover : 'ui-state-hover', // hover class
2275
+ // icon class names
2276
+ icons : 'ui-icon', // icon class added to the <i> in the header
2277
+ iconSortNone : 'ui-icon-carat-2-n-s', // class name added to icon when column is not sorted
2278
+ iconSortAsc : 'ui-icon-carat-1-n', // class name added to icon when column has ascending sort
2279
+ iconSortDesc : 'ui-icon-carat-1-s', // class name added to icon when column has descending sort
2280
+ filterRow : '',
2281
+ footerRow : '',
2282
+ footerCells : '',
2283
+ even : 'ui-widget-content', // even row zebra striping
2284
+ odd : 'ui-state-default' // odd row zebra striping
2285
+ }
2286
+ };
2287
+
2288
+ $.extend(ts.css, {
2289
+ wrapper : 'tablesorter-wrapper' // ui theme & resizable
2290
+ });
2291
+
2292
+ ts.addWidget({
2293
+ id: "uitheme",
2294
+ priority: 10,
2295
+ format: function(table, c, wo) {
2296
+ var i, hdr, icon, time, $header, $icon, $tfoot, $h, oldtheme, oldremove, oldIconRmv, hasOldTheme,
2297
+ themesAll = ts.themes,
2298
+ $table = c.$table.add( $( c.namespace + '_extra_table' ) ),
2299
+ $headers = c.$headers.add( $( c.namespace + '_extra_headers' ) ),
2300
+ theme = c.theme || 'jui',
2301
+ themes = themesAll[theme] || {},
2302
+ remove = $.trim( [ themes.sortNone, themes.sortDesc, themes.sortAsc, themes.active ].join( ' ' ) ),
2303
+ iconRmv = $.trim( [ themes.iconSortNone, themes.iconSortDesc, themes.iconSortAsc ].join( ' ' ) );
2304
+ if (c.debug) { time = new Date(); }
2305
+ // initialization code - run once
2306
+ if (!$table.hasClass('tablesorter-' + theme) || c.theme !== c.appliedTheme || !wo.uitheme_applied) {
2307
+ wo.uitheme_applied = true;
2308
+ oldtheme = themesAll[c.appliedTheme] || {};
2309
+ hasOldTheme = !$.isEmptyObject(oldtheme);
2310
+ oldremove = hasOldTheme ? [ oldtheme.sortNone, oldtheme.sortDesc, oldtheme.sortAsc, oldtheme.active ].join( ' ' ) : '';
2311
+ oldIconRmv = hasOldTheme ? [ oldtheme.iconSortNone, oldtheme.iconSortDesc, oldtheme.iconSortAsc ].join( ' ' ) : '';
2312
+ if (hasOldTheme) {
2313
+ wo.zebra[0] = $.trim( ' ' + wo.zebra[0].replace(' ' + oldtheme.even, '') );
2314
+ wo.zebra[1] = $.trim( ' ' + wo.zebra[1].replace(' ' + oldtheme.odd, '') );
2315
+ c.$tbodies.children().removeClass( [oldtheme.even, oldtheme.odd].join(' ') );
2316
+ }
2317
+ // update zebra stripes
2318
+ if (themes.even) { wo.zebra[0] += ' ' + themes.even; }
2319
+ if (themes.odd) { wo.zebra[1] += ' ' + themes.odd; }
2320
+ // add caption style
2321
+ $table.children('caption')
2322
+ .removeClass(oldtheme.caption || '')
2323
+ .addClass(themes.caption);
2324
+ // add table/footer class names
2325
+ $tfoot = $table
2326
+ // remove other selected themes
2327
+ .removeClass( (c.appliedTheme ? 'tablesorter-' + (c.appliedTheme || '') : '') + ' ' + (oldtheme.table || '') )
2328
+ .addClass('tablesorter-' + theme + ' ' + (themes.table || '')) // add theme widget class name
2329
+ .children('tfoot');
2330
+ c.appliedTheme = c.theme;
2331
+
2332
+ if ($tfoot.length) {
2333
+ $tfoot
2334
+ // if oldtheme.footerRow or oldtheme.footerCells are undefined, all class names are removed
2335
+ .children('tr').removeClass(oldtheme.footerRow || '').addClass(themes.footerRow)
2336
+ .children('th, td').removeClass(oldtheme.footerCells || '').addClass(themes.footerCells);
2337
+ }
2338
+ // update header classes
2339
+ $headers
2340
+ .removeClass( (hasOldTheme ? [oldtheme.header, oldtheme.hover, oldremove].join(' ') : '') || '' )
2341
+ .addClass(themes.header)
2342
+ .not('.sorter-false')
2343
+ .unbind('mouseenter.tsuitheme mouseleave.tsuitheme')
2344
+ .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) {
2345
+ // toggleClass with switch added in jQuery 1.3
2346
+ $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover || '');
2347
+ });
2348
+
2349
+ $headers.each(function(){
2350
+ var $this = $(this);
2351
+ if (!$this.find('.' + ts.css.wrapper).length) {
2352
+ // Firefox needs this inner div to position the icon & resizer correctly
2353
+ $this.wrapInner('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>');
2354
+ }
2355
+ });
2356
+ if (c.cssIcon) {
2357
+ // if c.cssIcon is '', then no <i> is added to the header
2358
+ $headers
2359
+ .find('.' + ts.css.icon)
2360
+ .removeClass(hasOldTheme ? [oldtheme.icons, oldIconRmv].join(' ') : '')
2361
+ .addClass(themes.icons || '');
2362
+ }
2363
+ if ($table.hasClass('hasFilters')) {
2364
+ $table.children('thead').children('.' + ts.css.filterRow)
2365
+ .removeClass(hasOldTheme ? oldtheme.filterRow || '' : '')
2366
+ .addClass(themes.filterRow || '');
2367
+ }
2368
+ }
2369
+ for (i = 0; i < c.columns; i++) {
2370
+ $header = c.$headers
2371
+ .add($(c.namespace + '_extra_headers'))
2372
+ .not('.sorter-false')
2373
+ .filter('[data-column="' + i + '"]');
2374
+ $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $();
2375
+ $h = $headers.not('.sorter-false').filter('[data-column="' + i + '"]:last');
2376
+ if ($h.length) {
2377
+ $header.removeClass(remove);
2378
+ $icon.removeClass(iconRmv);
2379
+ if ($h[0].sortDisabled) {
2380
+ // no sort arrows for disabled columns!
2381
+ $icon.removeClass(themes.icons || '');
2382
+ } else {
2383
+ hdr = themes.sortNone;
2384
+ icon = themes.iconSortNone;
2385
+ if ($h.hasClass(ts.css.sortAsc)) {
2386
+ hdr = [themes.sortAsc, themes.active].join(' ');
2387
+ icon = themes.iconSortAsc;
2388
+ } else if ($h.hasClass(ts.css.sortDesc)) {
2389
+ hdr = [themes.sortDesc, themes.active].join(' ');
2390
+ icon = themes.iconSortDesc;
2391
+ }
2392
+ $header.addClass(hdr);
2393
+ $icon.addClass(icon || '');
2394
+ }
2395
+ }
2396
+ }
2397
+ if (c.debug) {
2398
+ ts.benchmark("Applying " + theme + " theme", time);
2399
+ }
2400
+ },
2401
+ remove: function(table, c, wo, refreshing) {
2402
+ if (!wo.uitheme_applied) { return; }
2403
+ var $table = c.$table,
2404
+ theme = c.appliedTheme || 'jui',
2405
+ themes = ts.themes[ theme ] || ts.themes.jui,
2406
+ $headers = $table.children('thead').children(),
2407
+ remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc,
2408
+ iconRmv = themes.iconSortNone + ' ' + themes.iconSortDesc + ' ' + themes.iconSortAsc;
2409
+ $table.removeClass('tablesorter-' + theme + ' ' + themes.table);
2410
+ wo.uitheme_applied = false;
2411
+ if (refreshing) { return; }
2412
+ $table.find(ts.css.header).removeClass(themes.header);
2413
+ $headers
2414
+ .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover
2415
+ .removeClass(themes.hover + ' ' + remove + ' ' + themes.active)
2416
+ .filter('.' + ts.css.filterRow)
2417
+ .removeClass(themes.filterRow);
2418
+ $headers.find('.' + ts.css.icon).removeClass(themes.icons + ' ' + iconRmv);
2419
+ }
2420
+ });
2421
+
2422
+ })(jQuery);
2423
+
2424
+ /*! Widget: columns */
2425
+ ;(function ($) {
2426
+ 'use strict';
2427
+ var ts = $.tablesorter = $.tablesorter || {};
2428
+
2429
+ ts.addWidget({
2430
+ id: "columns",
2431
+ priority: 30,
2432
+ options : {
2433
+ columns : [ "primary", "secondary", "tertiary" ]
2434
+ },
2435
+ format: function(table, c, wo) {
2436
+ var $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx,
2437
+ $table = c.$table,
2438
+ $tbodies = c.$tbodies,
2439
+ sortList = c.sortList,
2440
+ len = sortList.length,
2441
+ // removed c.widgetColumns support
2442
+ css = wo && wo.columns || [ "primary", "secondary", "tertiary" ],
2443
+ last = css.length - 1;
2444
+ remove = css.join(' ');
2445
+ // check if there is a sort (on initialization there may not be one)
2446
+ for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
2447
+ $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody
2448
+ $rows = $tbody.children('tr');
2449
+ // loop through the visible rows
2450
+ $rows.each(function() {
2451
+ $row = $(this);
2452
+ if (this.style.display !== 'none') {
2453
+ // remove all columns class names
2454
+ $cells = $row.children().removeClass(remove);
2455
+ // add appropriate column class names
2456
+ if (sortList && sortList[0]) {
2457
+ // primary sort column class
2458
+ $cells.eq(sortList[0][0]).addClass(css[0]);
2459
+ if (len > 1) {
2460
+ for (indx = 1; indx < len; indx++) {
2461
+ // secondary, tertiary, etc sort column classes
2462
+ $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] );
2463
+ }
2464
+ }
2465
+ }
2466
+ }
2467
+ });
2468
+ ts.processTbody(table, $tbody, false);
2469
+ }
2470
+ // add classes to thead and tfoot
2471
+ rows = wo.columns_thead !== false ? ['thead tr'] : [];
2472
+ if (wo.columns_tfoot !== false) {
2473
+ rows.push('tfoot tr');
2474
+ }
2475
+ if (rows.length) {
2476
+ $rows = $table.find( rows.join(',') ).children().removeClass(remove);
2477
+ if (len) {
2478
+ for (indx = 0; indx < len; indx++) {
2479
+ // add primary. secondary, tertiary, etc sort column classes
2480
+ $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]);
2481
+ }
2482
+ }
2483
+ }
2484
+ },
2485
+ remove: function(table, c, wo) {
2486
+ var tbodyIndex, $tbody,
2487
+ $tbodies = c.$tbodies,
2488
+ remove = (wo.columns || [ "primary", "secondary", "tertiary" ]).join(' ');
2489
+ c.$headers.removeClass(remove);
2490
+ c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove);
2491
+ for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
2492
+ $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
2493
+ $tbody.children('tr').each(function() {
2494
+ $(this).children().removeClass(remove);
2495
+ });
2496
+ ts.processTbody(table, $tbody, false); // restore tbody
2497
+ }
2498
+ }
2499
+ });
2500
+
2501
+ })(jQuery);
2502
+
2503
+ /*! Widget: filter - updated 3/26/2015 (v2.21.3) *//*
2504
+ * Requires tablesorter v2.8+ and jQuery 1.7+
2505
+ * by Rob Garrison
2506
+ */
2507
+ ;(function ($) {
2508
+ 'use strict';
2509
+ var ts = $.tablesorter = $.tablesorter || {},
2510
+ tscss = ts.css;
2511
+
2512
+ $.extend(tscss, {
2513
+ filterRow : 'tablesorter-filter-row',
2514
+ filter : 'tablesorter-filter',
2515
+ filterDisabled : 'disabled',
2516
+ filterRowHide : 'hideme'
2517
+ });
2518
+
2519
+ ts.addWidget({
2520
+ id: "filter",
2521
+ priority: 50,
2522
+ options : {
2523
+ filter_childRows : false, // if true, filter includes child row content in the search
2524
+ filter_columnFilters : true, // if true, a filter will be added to the top of each table column
2525
+ filter_columnAnyMatch: true, // if true, allows using "#:{query}" in AnyMatch searches (column:query)
2526
+ filter_cellFilter : '', // css class name added to the filter cell (string or array)
2527
+ filter_cssFilter : '', // css class name added to the filter row & each input in the row (tablesorter-filter is ALWAYS added)
2528
+ 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.
2529
+ filter_excludeFilter : {}, // filters to exclude, per column
2530
+ filter_external : '', // jQuery selector string (or jQuery object) of external filters
2531
+ filter_filteredRow : 'filtered', // class added to filtered rows; needed by pager plugin
2532
+ filter_formatter : null, // add custom filter elements to the filter row
2533
+ filter_functions : null, // add custom filter functions using this option
2534
+ filter_hideEmpty : true, // hide filter row when table is empty
2535
+ filter_hideFilters : false, // collapse filter row when mouse leaves the area
2536
+ filter_ignoreCase : true, // if true, make all searches case-insensitive
2537
+ filter_liveSearch : true, // if true, search column content while the user types (with a delay)
2538
+ filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available (visible) options within the drop down
2539
+ filter_placeholder : { search : '', select : '' }, // default placeholder text (overridden by any header "data-placeholder" setting)
2540
+ filter_reset : null, // jQuery selector string of an element used to reset the filters
2541
+ filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters
2542
+ filter_searchDelay : 300, // typing delay in milliseconds before starting a search
2543
+ filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true
2544
+ filter_selectSource : null, // include a function to return an array of values to be added to the column filter select
2545
+ filter_startsWith : false, // if true, filter start from the beginning of the cell contents
2546
+ filter_useParsedData : false, // filter all data using parsed content
2547
+ filter_serversideFiltering : false, // if true, server-side filtering should be performed because client-side filtering will be disabled, but the ui and events will still be used.
2548
+ filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value
2549
+ filter_selectSourceSeparator : '|' // filter_selectSource array text left of the separator is added to the option value, right into the option text
2550
+ },
2551
+ format: function(table, c, wo) {
2552
+ if (!c.$table.hasClass('hasFilters')) {
2553
+ ts.filter.init(table, c, wo);
2554
+ }
2555
+ },
2556
+ remove: function(table, c, wo, refreshing) {
2557
+ var tbodyIndex, $tbody,
2558
+ $table = c.$table,
2559
+ $tbodies = c.$tbodies,
2560
+ events = 'addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join(c.namespace + 'filter ');
2561
+ $table
2562
+ .removeClass('hasFilters')
2563
+ // add .tsfilter namespace to all BUT search
2564
+ .unbind( events.replace(/\s+/g, ' ') )
2565
+ // remove the filter row even if refreshing, because the column might have been moved
2566
+ .find('.' + tscss.filterRow).remove();
2567
+ if (refreshing) { return; }
2568
+ for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
2569
+ $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
2570
+ $tbody.children().removeClass(wo.filter_filteredRow).show();
2571
+ ts.processTbody(table, $tbody, false); // restore tbody
2572
+ }
2573
+ if (wo.filter_reset) {
2574
+ $(document).undelegate(wo.filter_reset, 'click.tsfilter');
2575
+ }
2576
+ }
2577
+ });
2578
+
2579
+ ts.filter = {
2580
+
2581
+ // regex used in filter "check" functions - not for general use and not documented
2582
+ regex: {
2583
+ regex : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex
2584
+ child : /tablesorter-childRow/, // child row class name; this gets updated in the script
2585
+ filtered : /filtered/, // filtered (hidden) row class name; updated in the script
2586
+ type : /undefined|number/, // check type
2587
+ exact : /(^[\"\'=]+)|([\"\'=]+$)/g, // exact match (allow '==')
2588
+ nondigit : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser)
2589
+ operators : /[<>=]/g, // replace operators
2590
+ query : '(q|query)' // replace filter queries
2591
+ },
2592
+ // function( c, data ) { }
2593
+ // c = table.config
2594
+ // data.filter = array of filter input values;
2595
+ // data.iFilter = same array, except lowercase (if wo.filter_ignoreCase is true)
2596
+ // data.exact = table cell text (or parsed data if column parser enabled)
2597
+ // data.iExact = same as data.exact, except lowercase (if wo.filter_ignoreCase is true)
2598
+ // data.cache = table cell text from cache, so it has been parsed (& in all lower case if config.ignoreCase is true)
2599
+ // data.index = column index; table = table element (DOM)
2600
+ // data.parsed = array (by column) of boolean values (from filter_useParsedData or "filter-parsed" class)
2601
+ types: {
2602
+ // Look for regex
2603
+ regex: function( c, data ) {
2604
+ if ( ts.filter.regex.regex.test(data.iFilter) ) {
2605
+ var matches,
2606
+ regex = ts.filter.regex.regex.exec(data.iFilter);
2607
+ try {
2608
+ matches = new RegExp(regex[1], regex[2]).test( data.iExact );
2609
+ } catch (error) {
2610
+ matches = false;
2611
+ }
2612
+ return matches;
2613
+ }
2614
+ return null;
2615
+ },
2616
+ // Look for operators >, >=, < or <=
2617
+ operators: function( c, data ) {
2618
+ if ( /^[<>]=?/.test(data.iFilter) ) {
2619
+ var cachedValue, result,
2620
+ table = c.table,
2621
+ index = data.index,
2622
+ parsed = data.parsed[index],
2623
+ query = ts.formatFloat( data.iFilter.replace(ts.filter.regex.operators, ''), table ),
2624
+ parser = c.parsers[index],
2625
+ savedSearch = query;
2626
+ // parse filter value in case we're comparing numbers (dates)
2627
+ if (parsed || parser.type === 'numeric') {
2628
+ result = ts.filter.parseFilter(c, $.trim('' + data.iFilter.replace(ts.filter.regex.operators, '')), index, parsed, true);
2629
+ query = ( typeof result === "number" && result !== '' && !isNaN(result) ) ? result : query;
2630
+ }
2631
+
2632
+ // iExact may be numeric - see issue #149;
2633
+ // check if cached is defined, because sometimes j goes out of range? (numeric columns)
2634
+ cachedValue = ( parsed || parser.type === 'numeric' ) && !isNaN(query) && typeof data.cache !== 'undefined' ? data.cache :
2635
+ isNaN(data.iExact) ? ts.formatFloat( data.iExact.replace(ts.filter.regex.nondigit, ''), table) :
2636
+ ts.formatFloat( data.iExact, table );
2637
+
2638
+ if ( />/.test(data.iFilter) ) { result = />=/.test(data.iFilter) ? cachedValue >= query : cachedValue > query; }
2639
+ if ( /</.test(data.iFilter) ) { result = /<=/.test(data.iFilter) ? cachedValue <= query : cachedValue < query; }
2640
+ // keep showing all rows if nothing follows the operator
2641
+ if ( !result && savedSearch === '' ) { result = true; }
2642
+ return result;
2643
+ }
2644
+ return null;
2645
+ },
2646
+ // Look for a not match
2647
+ notMatch: function( c, data ) {
2648
+ if ( /^\!/.test(data.iFilter) ) {
2649
+ var indx,
2650
+ filter = ts.filter.parseFilter(c, data.iFilter.replace('!', ''), data.index, data.parsed[data.index]) || '';
2651
+ if (ts.filter.regex.exact.test(filter)) {
2652
+ // look for exact not matches - see #628
2653
+ filter = filter.replace(ts.filter.regex.exact, '');
2654
+ return filter === '' ? true : $.trim(filter) !== data.iExact;
2655
+ } else {
2656
+ indx = data.iExact.search( $.trim(filter) );
2657
+ return filter === '' ? true : !(c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 0);
2658
+ }
2659
+ }
2660
+ return null;
2661
+ },
2662
+ // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric
2663
+ exact: function( c, data ) {
2664
+ /*jshint eqeqeq:false */
2665
+ if (ts.filter.regex.exact.test(data.iFilter)) {
2666
+ var filter = ts.filter.parseFilter(c, data.iFilter.replace(ts.filter.regex.exact, ''), data.index, data.parsed[data.index]) || '';
2667
+ return data.anyMatch ? $.inArray(filter, data.rowArray) >= 0 : filter == data.iExact;
2668
+ }
2669
+ return null;
2670
+ },
2671
+ // Look for an AND or && operator (logical and)
2672
+ and : function( c, data ) {
2673
+ if ( ts.filter.regex.andTest.test(data.filter) ) {
2674
+ var index = data.index,
2675
+ parsed = data.parsed[index],
2676
+ query = data.iFilter.split( ts.filter.regex.andSplit ),
2677
+ result = data.iExact.search( $.trim( ts.filter.parseFilter(c, query[0], index, parsed) ) ) >= 0,
2678
+ indx = query.length - 1;
2679
+ while (result && indx) {
2680
+ result = result && data.iExact.search( $.trim( ts.filter.parseFilter(c, query[indx], index, parsed) ) ) >= 0;
2681
+ indx--;
2682
+ }
2683
+ return result;
2684
+ }
2685
+ return null;
2686
+ },
2687
+ // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu!
2688
+ range : function( c, data ) {
2689
+ if ( ts.filter.regex.toTest.test(data.iFilter) ) {
2690
+ var result, tmp,
2691
+ table = c.table,
2692
+ index = data.index,
2693
+ parsed = data.parsed[index],
2694
+ // make sure the dash is for a range and not indicating a negative number
2695
+ query = data.iFilter.split( ts.filter.regex.toSplit ),
2696
+ range1 = ts.formatFloat( ts.filter.parseFilter(c, query[0].replace(ts.filter.regex.nondigit, '') || '', index, parsed), table ),
2697
+ range2 = ts.formatFloat( ts.filter.parseFilter(c, query[1].replace(ts.filter.regex.nondigit, '') || '', index, parsed), table );
2698
+ // parse filter value in case we're comparing numbers (dates)
2699
+ if (parsed || c.parsers[index].type === 'numeric') {
2700
+ result = c.parsers[index].format('' + query[0], table, c.$headers.eq(index), index);
2701
+ range1 = (result !== '' && !isNaN(result)) ? result : range1;
2702
+ result = c.parsers[index].format('' + query[1], table, c.$headers.eq(index), index);
2703
+ range2 = (result !== '' && !isNaN(result)) ? result : range2;
2704
+ }
2705
+ result = ( parsed || c.parsers[index].type === 'numeric' ) && !isNaN(range1) && !isNaN(range2) ? data.cache :
2706
+ isNaN(data.iExact) ? ts.formatFloat( data.iExact.replace(ts.filter.regex.nondigit, ''), table) :
2707
+ ts.formatFloat( data.iExact, table );
2708
+ if (range1 > range2) { tmp = range1; range1 = range2; range2 = tmp; } // swap
2709
+ return (result >= range1 && result <= range2) || (range1 === '' || range2 === '');
2710
+ }
2711
+ return null;
2712
+ },
2713
+ // Look for wild card: ? = single, * = multiple, or | = logical OR
2714
+ wild : function( c, data ) {
2715
+ if ( /[\?\*\|]/.test(data.iFilter) || ts.filter.regex.orReplace.test(data.filter) ) {
2716
+ var index = data.index,
2717
+ parsed = data.parsed[index],
2718
+ query = ts.filter.parseFilter(c, data.iFilter.replace(ts.filter.regex.orReplace, "|"), index, parsed) || '';
2719
+ // look for an exact match with the "or" unless the "filter-match" class is found
2720
+ if (!c.$headerIndexed[index].hasClass('filter-match') && /\|/.test(query)) {
2721
+ // show all results while using filter match. Fixes #727
2722
+ if (query[ query.length - 1 ] === '|') { query += '*'; }
2723
+ query = data.anyMatch && $.isArray(data.rowArray) ? '(' + query + ')' : '^(' + query + ')$';
2724
+ }
2725
+ // parsing the filter may not work properly when using wildcards =/
2726
+ return new RegExp( query.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(data.iExact);
2727
+ }
2728
+ return null;
2729
+ },
2730
+ // fuzzy text search; modified from https://github.com/mattyork/fuzzy (MIT license)
2731
+ fuzzy: function( c, data ) {
2732
+ if ( /^~/.test(data.iFilter) ) {
2733
+ var indx,
2734
+ patternIndx = 0,
2735
+ len = data.iExact.length,
2736
+ pattern = ts.filter.parseFilter(c, data.iFilter.slice(1), data.index, data.parsed[data.index]) || '';
2737
+ for (indx = 0; indx < len; indx++) {
2738
+ if (data.iExact[indx] === pattern[patternIndx]) {
2739
+ patternIndx += 1;
2740
+ }
2741
+ }
2742
+ if (patternIndx === pattern.length) {
2743
+ return true;
2744
+ }
2745
+ return false;
2746
+ }
2747
+ return null;
2748
+ }
2749
+ },
2750
+ init: function(table, c, wo) {
2751
+ // filter language options
2752
+ ts.language = $.extend(true, {}, {
2753
+ to : 'to',
2754
+ or : 'or',
2755
+ and : 'and'
2756
+ }, ts.language);
2757
+
2758
+ var options, string, txt, $header, column, filters, val, fxn, noSelect,
2759
+ regex = ts.filter.regex;
2760
+ c.$table.addClass('hasFilters');
2761
+
2762
+ // define timers so using clearTimeout won't cause an undefined error
2763
+ wo.searchTimer = null;
2764
+ wo.filter_initTimer = null;
2765
+ wo.filter_formatterCount = 0;
2766
+ wo.filter_formatterInit = [];
2767
+ wo.filter_anyColumnSelector = '[data-column="all"],[data-column="any"]';
2768
+ wo.filter_multipleColumnSelector = '[data-column*="-"],[data-column*=","]';
2769
+
2770
+ txt = '\\{' + ts.filter.regex.query + '\\}';
2771
+ $.extend( regex, {
2772
+ child : new RegExp(c.cssChildRow),
2773
+ filtered : new RegExp(wo.filter_filteredRow),
2774
+ alreadyFiltered : new RegExp('(\\s+(' + ts.language.or + '|-|' + ts.language.to + ')\\s+)', 'i'),
2775
+ toTest : new RegExp('\\s+(-|' + ts.language.to + ')\\s+', 'i'),
2776
+ toSplit : new RegExp('(?:\\s+(?:-|' + ts.language.to + ')\\s+)' ,'gi'),
2777
+ andTest : new RegExp('\\s+(' + ts.language.and + '|&&)\\s+', 'i'),
2778
+ andSplit : new RegExp('(?:\\s+(?:' + ts.language.and + '|&&)\\s+)', 'gi'),
2779
+ orReplace : new RegExp('\\s+(' + ts.language.or + ')\\s+', 'gi'),
2780
+ iQuery : new RegExp(txt, 'i'),
2781
+ igQuery : new RegExp(txt, 'ig')
2782
+ });
2783
+
2784
+ // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156
2785
+ if (wo.filter_columnFilters !== false && c.$headers.filter('.filter-false, .parser-false').length !== c.$headers.length) {
2786
+ // build filter row
2787
+ ts.filter.buildRow(table, c, wo);
2788
+ }
2789
+
2790
+ txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join(c.namespace + 'filter ');
2791
+ c.$table.bind( txt, function(event, filter) {
2792
+ val = (wo.filter_hideEmpty && $.isEmptyObject(c.cache) && !(c.delayInit && event.type === 'appendCache'));
2793
+ // hide filter row using the "filtered" class name
2794
+ c.$table.find('.' + tscss.filterRow).toggleClass(wo.filter_filteredRow, val ); // fixes #450
2795
+ if ( !/(search|filter)/.test(event.type) ) {
2796
+ event.stopPropagation();
2797
+ ts.filter.buildDefault(table, true);
2798
+ }
2799
+ if (event.type === 'filterReset') {
2800
+ c.$table.find('.' + tscss.filter).add(wo.filter_$externalFilters).val('');
2801
+ ts.filter.searching(table, []);
2802
+ } else if (event.type === 'filterEnd') {
2803
+ ts.filter.buildDefault(table, true);
2804
+ } else {
2805
+ // send false argument to force a new search; otherwise if the filter hasn't changed, it will return
2806
+ filter = event.type === 'search' ? filter : event.type === 'updateComplete' ? c.$table.data('lastSearch') : '';
2807
+ if (/(update|add)/.test(event.type) && event.type !== "updateComplete") {
2808
+ // force a new search since content has changed
2809
+ c.lastCombinedFilter = null;
2810
+ c.lastSearch = [];
2811
+ }
2812
+ // pass true (skipFirst) to prevent the tablesorter.setFilters function from skipping the first input
2813
+ // ensures all inputs are updated when a search is triggered on the table $('table').trigger('search', [...]);
2814
+ ts.filter.searching(table, filter, true);
2815
+ }
2816
+ return false;
2817
+ });
2818
+
2819
+ // reset button/link
2820
+ if (wo.filter_reset) {
2821
+ if (wo.filter_reset instanceof $) {
2822
+ // reset contains a jQuery object, bind to it
2823
+ wo.filter_reset.click(function(){
2824
+ c.$table.trigger('filterReset');
2825
+ });
2826
+ } else if ($(wo.filter_reset).length) {
2827
+ // reset is a jQuery selector, use event delegation
2828
+ $(document)
2829
+ .undelegate(wo.filter_reset, 'click.tsfilter')
2830
+ .delegate(wo.filter_reset, 'click.tsfilter', function() {
2831
+ // trigger a reset event, so other functions (filter_formatter) know when to reset
2832
+ c.$table.trigger('filterReset');
2833
+ });
2834
+ }
2835
+ }
2836
+ if (wo.filter_functions) {
2837
+ for (column = 0; column < c.columns; column++) {
2838
+ fxn = ts.getColumnData( table, wo.filter_functions, column );
2839
+ if (fxn) {
2840
+ // remove "filter-select" from header otherwise the options added here are replaced with all options
2841
+ $header = c.$headerIndexed[column].removeClass('filter-select');
2842
+ // don't build select if "filter-false" or "parser-false" set
2843
+ noSelect = !($header.hasClass('filter-false') || $header.hasClass('parser-false'));
2844
+ options = '';
2845
+ if ( fxn === true && noSelect ) {
2846
+ ts.filter.buildSelect(table, column);
2847
+ } else if ( typeof fxn === 'object' && noSelect ) {
2848
+ // add custom drop down list
2849
+ for (string in fxn) {
2850
+ if (typeof string === 'string') {
2851
+ options += options === '' ?
2852
+ '<option value="">' + ($header.data('placeholder') || $header.attr('data-placeholder') || wo.filter_placeholder.select || '') + '</option>' : '';
2853
+ val = string;
2854
+ txt = string;
2855
+ if (string.indexOf(wo.filter_selectSourceSeparator) >= 0) {
2856
+ val = string.split(wo.filter_selectSourceSeparator);
2857
+ txt = val[1];
2858
+ val = val[0];
2859
+ }
2860
+ options += '<option ' + (txt === val ? '' : 'data-function-name="' + string + '" ') + 'value="' + val + '">' + txt + '</option>';
2861
+ }
2862
+ }
2863
+ c.$table.find('thead').find('select.' + tscss.filter + '[data-column="' + column + '"]').append(options);
2864
+ }
2865
+ }
2866
+ }
2867
+ }
2868
+ // not really updating, but if the column has both the "filter-select" class & filter_functions set to true,
2869
+ // it would append the same options twice.
2870
+ ts.filter.buildDefault(table, true);
2871
+
2872
+ ts.filter.bindSearch( table, c.$table.find('.' + tscss.filter), true );
2873
+ if (wo.filter_external) {
2874
+ ts.filter.bindSearch( table, wo.filter_external );
2875
+ }
2876
+
2877
+ if (wo.filter_hideFilters) {
2878
+ ts.filter.hideFilters(table, c);
2879
+ }
2880
+
2881
+ // show processing icon
2882
+ if (c.showProcessing) {
2883
+ c.$table
2884
+ .unbind( ('filterStart filterEnd '.split(' ').join(c.namespace + 'filter ')).replace(/\s+/g, ' ') )
2885
+ .bind( 'filterStart filterEnd '.split(' ').join(c.namespace + 'filter '), function(event, columns) {
2886
+ // only add processing to certain columns to all columns
2887
+ $header = (columns) ? c.$table.find('.' + tscss.header).filter('[data-column]').filter(function() {
2888
+ return columns[$(this).data('column')] !== '';
2889
+ }) : '';
2890
+ ts.isProcessing(table, event.type === 'filterStart', columns ? $header : '');
2891
+ });
2892
+ }
2893
+
2894
+ // set filtered rows count (intially unfiltered)
2895
+ c.filteredRows = c.totalRows;
2896
+
2897
+ // add default values
2898
+ c.$table
2899
+ .unbind( ('tablesorter-initialized pagerBeforeInitialized '.split(' ').join(c.namespace + 'filter ')).replace(/\s+/g, ' ') )
2900
+ .bind( 'tablesorter-initialized pagerBeforeInitialized '.split(' ').join(c.namespace + 'filter '), function() {
2901
+ // redefine "wo" as it does not update properly inside this callback
2902
+ var wo = this.config.widgetOptions;
2903
+ filters = ts.filter.setDefaults(table, c, wo) || [];
2904
+ if (filters.length) {
2905
+ // prevent delayInit from triggering a cache build if filters are empty
2906
+ if ( !(c.delayInit && filters.join('') === '') ) {
2907
+ ts.setFilters(table, filters, true);
2908
+ }
2909
+ }
2910
+ c.$table.trigger('filterFomatterUpdate');
2911
+ // trigger init after setTimeout to prevent multiple filterStart/End/Init triggers
2912
+ setTimeout(function(){
2913
+ if (!wo.filter_initialized) {
2914
+ ts.filter.filterInitComplete(c);
2915
+ }
2916
+ }, 100);
2917
+ });
2918
+ // if filter widget is added after pager has initialized; then set filter init flag
2919
+ if (c.pager && c.pager.initialized && !wo.filter_initialized) {
2920
+ c.$table.trigger('filterFomatterUpdate');
2921
+ setTimeout(function(){
2922
+ ts.filter.filterInitComplete(c);
2923
+ }, 100);
2924
+ }
2925
+ },
2926
+ // $cell parameter, but not the config, is passed to the
2927
+ // filter_formatters, so we have to work with it instead
2928
+ formatterUpdated: function($cell, column) {
2929
+ var wo = $cell.closest('table')[0].config.widgetOptions;
2930
+ if (!wo.filter_initialized) {
2931
+ // add updates by column since this function
2932
+ // may be called numerous times before initialization
2933
+ wo.filter_formatterInit[column] = 1;
2934
+ }
2935
+ },
2936
+ filterInitComplete: function(c){
2937
+ var indx, len,
2938
+ wo = c.widgetOptions,
2939
+ count = 0,
2940
+ completed = function(){
2941
+ wo.filter_initialized = true;
2942
+ c.$table.trigger('filterInit', c);
2943
+ ts.filter.findRows(c.table, c.$table.data('lastSearch') || []);
2944
+ };
2945
+ if ( $.isEmptyObject( wo.filter_formatter ) ) {
2946
+ completed();
2947
+ } else {
2948
+ len = wo.filter_formatterInit.length;
2949
+ for (indx = 0; indx < len; indx++) {
2950
+ if (wo.filter_formatterInit[indx] === 1) {
2951
+ count++;
2952
+ }
2953
+ }
2954
+ clearTimeout(wo.filter_initTimer);
2955
+ if (!wo.filter_initialized && count === wo.filter_formatterCount) {
2956
+ // filter widget initialized
2957
+ completed();
2958
+ } else if (!wo.filter_initialized) {
2959
+ // fall back in case a filter_formatter doesn't call
2960
+ // $.tablesorter.filter.formatterUpdated($cell, column), and the count is off
2961
+ wo.filter_initTimer = setTimeout(function(){
2962
+ completed();
2963
+ }, 500);
2964
+ }
2965
+ }
2966
+ },
2967
+
2968
+ setDefaults: function(table, c, wo) {
2969
+ var isArray, saved, indx, col, $filters,
2970
+ // get current (default) filters
2971
+ filters = ts.getFilters(table) || [];
2972
+ if (wo.filter_saveFilters && ts.storage) {
2973
+ saved = ts.storage( table, 'tablesorter-filters' ) || [];
2974
+ isArray = $.isArray(saved);
2975
+ // make sure we're not just getting an empty array
2976
+ if ( !(isArray && saved.join('') === '' || !isArray) ) { filters = saved; }
2977
+ }
2978
+ // if no filters saved, then check default settings
2979
+ if (filters.join('') === '') {
2980
+ // allow adding default setting to external filters
2981
+ $filters = c.$headers.add( wo.filter_$externalFilters ).filter('[' + wo.filter_defaultAttrib + ']');
2982
+ for (indx = 0; indx <= c.columns; indx++) {
2983
+ // include data-column="all" external filters
2984
+ col = indx === c.columns ? 'all' : indx;
2985
+ filters[indx] = $filters.filter('[data-column="' + col + '"]').attr(wo.filter_defaultAttrib) || filters[indx] || '';
2986
+ }
2987
+ }
2988
+ c.$table.data('lastSearch', filters);
2989
+ return filters;
2990
+ },
2991
+ parseFilter: function(c, filter, column, parsed, forceParse){
2992
+ return forceParse || parsed ?
2993
+ c.parsers[column].format( filter, c.table, [], column ) :
2994
+ filter;
2995
+ },
2996
+ buildRow: function(table, c, wo) {
2997
+ var col, column, $header, buildSelect, disabled, name, ffxn,
2998
+ // c.columns defined in computeThIndexes()
2999
+ columns = c.columns,
3000
+ arry = $.isArray(wo.filter_cellFilter),
3001
+ buildFilter = '<tr role="row" class="' + tscss.filterRow + ' ' + c.cssIgnoreRow + '">';
3002
+ for (column = 0; column < columns; column++) {
3003
+ if (arry) {
3004
+ buildFilter += '<td' + ( wo.filter_cellFilter[column] ? ' class="' + wo.filter_cellFilter[column] + '"' : '' ) + '></td>';
3005
+ } else {
3006
+ buildFilter += '<td' + ( wo.filter_cellFilter !== '' ? ' class="' + wo.filter_cellFilter + '"' : '' ) + '></td>';
3007
+ }
3008
+ }
3009
+ c.$filters = $(buildFilter += '</tr>').appendTo( c.$table.children('thead').eq(0) ).find('td');
3010
+ // build each filter input
3011
+ for (column = 0; column < columns; column++) {
3012
+ disabled = false;
3013
+ // assuming last cell of a column is the main column
3014
+ $header = c.$headerIndexed[column];
3015
+ ffxn = ts.getColumnData( table, wo.filter_functions, column );
3016
+ buildSelect = (wo.filter_functions && ffxn && typeof ffxn !== "function" ) ||
3017
+ $header.hasClass('filter-select');
3018
+ // get data from jQuery data, metadata, headers option or header class name
3019
+ col = ts.getColumnData( table, c.headers, column );
3020
+ disabled = ts.getData($header[0], col, 'filter') === 'false' || ts.getData($header[0], col, 'parser') === 'false';
3021
+
3022
+ if (buildSelect) {
3023
+ buildFilter = $('<select>').appendTo( c.$filters.eq(column) );
3024
+ } else {
3025
+ ffxn = ts.getColumnData( table, wo.filter_formatter, column );
3026
+ if (ffxn) {
3027
+ wo.filter_formatterCount++;
3028
+ buildFilter = ffxn( c.$filters.eq(column), column );
3029
+ // no element returned, so lets go find it
3030
+ if (buildFilter && buildFilter.length === 0) {
3031
+ buildFilter = c.$filters.eq(column).children('input');
3032
+ }
3033
+ // element not in DOM, so lets attach it
3034
+ if ( buildFilter && (buildFilter.parent().length === 0 ||
3035
+ (buildFilter.parent().length && buildFilter.parent()[0] !== c.$filters[column])) ) {
3036
+ c.$filters.eq(column).append(buildFilter);
3037
+ }
3038
+ } else {
3039
+ buildFilter = $('<input type="search">').appendTo( c.$filters.eq(column) );
3040
+ }
3041
+ if (buildFilter) {
3042
+ buildFilter.attr('placeholder', $header.data('placeholder') || $header.attr('data-placeholder') || wo.filter_placeholder.search || '');
3043
+ }
3044
+ }
3045
+ if (buildFilter) {
3046
+ // add filter class name
3047
+ name = ( $.isArray(wo.filter_cssFilter) ?
3048
+ (typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '') :
3049
+ wo.filter_cssFilter ) || '';
3050
+ buildFilter.addClass( tscss.filter + ' ' + name ).attr('data-column', column);
3051
+ if (disabled) {
3052
+ buildFilter.attr('placeholder', '').addClass(tscss.filterDisabled)[0].disabled = true; // disabled!
3053
+ }
3054
+ }
3055
+ }
3056
+ },
3057
+ bindSearch: function(table, $el, internal) {
3058
+ table = $(table)[0];
3059
+ $el = $($el); // allow passing a selector string
3060
+ if (!$el.length) { return; }
3061
+ var c = table.config,
3062
+ wo = c.widgetOptions,
3063
+ $ext = wo.filter_$externalFilters;
3064
+ if (internal !== true) {
3065
+ // save anyMatch element
3066
+ wo.filter_$anyMatch = $el.filter(wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector);
3067
+ if ($ext && $ext.length) {
3068
+ wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el );
3069
+ } else {
3070
+ wo.filter_$externalFilters = $el;
3071
+ }
3072
+ // update values (external filters added after table initialization)
3073
+ ts.setFilters(table, c.$table.data('lastSearch') || [], internal === false);
3074
+ }
3075
+ $el
3076
+ // use data attribute instead of jQuery data since the head is cloned without including the data/binding
3077
+ .attr('data-lastSearchTime', new Date().getTime())
3078
+ .unbind( ('keypress keyup search change '.split(' ').join(c.namespace + 'filter ')).replace(/\s+/g, ' ') )
3079
+ // include change for select - fixes #473
3080
+ .bind('keyup' + c.namespace + 'filter', function(event) {
3081
+ $(this).attr('data-lastSearchTime', new Date().getTime());
3082
+ // emulate what webkit does.... escape clears the filter
3083
+ if (event.which === 27) {
3084
+ this.value = '';
3085
+ // live search
3086
+ } else if ( wo.filter_liveSearch === false ) {
3087
+ return;
3088
+ // don't return if the search value is empty (all rows need to be revealed)
3089
+ } else if ( this.value !== '' && (
3090
+ // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace
3091
+ ( typeof wo.filter_liveSearch === 'number' && this.value.length < wo.filter_liveSearch ) ||
3092
+ // let return & backspace continue on, but ignore arrows & non-valid characters
3093
+ ( event.which !== 13 && event.which !== 8 && ( event.which < 32 || (event.which >= 37 && event.which <= 40) ) ) ) ) {
3094
+ return;
3095
+ }
3096
+ // change event = no delay; last true flag tells getFilters to skip newest timed input
3097
+ ts.filter.searching( table, true, true );
3098
+ })
3099
+ .bind( 'search change keypress '.split(' ').join(c.namespace + 'filter '), function(event){
3100
+ var column = $(this).data('column');
3101
+ // don't allow "change" event to process if the input value is the same - fixes #685
3102
+ if (event.which === 13 || event.type === 'search' || event.type === 'change' && this.value !== c.lastSearch[column]) {
3103
+ event.preventDefault();
3104
+ // init search with no delay
3105
+ $(this).attr('data-lastSearchTime', new Date().getTime());
3106
+ ts.filter.searching( table, false, true );
3107
+ }
3108
+ });
3109
+ },
3110
+ searching: function(table, filter, skipFirst) {
3111
+ var wo = table.config.widgetOptions;
3112
+ clearTimeout(wo.searchTimer);
3113
+ if (typeof filter === 'undefined' || filter === true) {
3114
+ // delay filtering
3115
+ wo.searchTimer = setTimeout(function() {
3116
+ ts.filter.checkFilters(table, filter, skipFirst );
3117
+ }, wo.filter_liveSearch ? wo.filter_searchDelay : 10);
3118
+ } else {
3119
+ // skip delay
3120
+ ts.filter.checkFilters(table, filter, skipFirst);
3121
+ }
3122
+ },
3123
+ checkFilters: function(table, filter, skipFirst) {
3124
+ var c = table.config,
3125
+ wo = c.widgetOptions,
3126
+ filterArray = $.isArray(filter),
3127
+ filters = (filterArray) ? filter : ts.getFilters(table, true),
3128
+ combinedFilters = (filters || []).join(''); // combined filter values
3129
+ // prevent errors if delay init is set
3130
+ if ($.isEmptyObject(c.cache)) {
3131
+ // update cache if delayInit set & pager has initialized (after user initiates a search)
3132
+ if (c.delayInit && c.pager && c.pager.initialized) {
3133
+ c.$table.trigger('updateCache', [function(){
3134
+ ts.filter.checkFilters(table, false, skipFirst);
3135
+ }] );
3136
+ }
3137
+ return;
3138
+ }
3139
+ // add filter array back into inputs
3140
+ if (filterArray) {
3141
+ ts.setFilters( table, filters, false, skipFirst !== true );
3142
+ if (!wo.filter_initialized) { c.lastCombinedFilter = ''; }
3143
+ }
3144
+ if (wo.filter_hideFilters) {
3145
+ // show/hide filter row as needed
3146
+ c.$table.find('.' + tscss.filterRow).trigger( combinedFilters === '' ? 'mouseleave' : 'mouseenter' );
3147
+ }
3148
+ // return if the last search is the same; but filter === false when updating the search
3149
+ // see example-widget-filter.html filter toggle buttons
3150
+ if (c.lastCombinedFilter === combinedFilters && filter !== false) {
3151
+ return;
3152
+ } else if (filter === false) {
3153
+ // force filter refresh
3154
+ c.lastCombinedFilter = null;
3155
+ c.lastSearch = [];
3156
+ }
3157
+ if (wo.filter_initialized) { c.$table.trigger('filterStart', [filters]); }
3158
+ if (c.showProcessing) {
3159
+ // give it time for the processing icon to kick in
3160
+ setTimeout(function() {
3161
+ ts.filter.findRows(table, filters, combinedFilters);
3162
+ return false;
3163
+ }, 30);
3164
+ } else {
3165
+ ts.filter.findRows(table, filters, combinedFilters);
3166
+ return false;
3167
+ }
3168
+ },
3169
+ hideFilters: function(table, c) {
3170
+ var $filterRow, $filterRow2, timer;
3171
+ $(table)
3172
+ .find('.' + tscss.filterRow)
3173
+ .addClass(tscss.filterRowHide)
3174
+ .bind('mouseenter mouseleave', function(e) {
3175
+ // save event object - http://bugs.jquery.com/ticket/12140
3176
+ var event = e;
3177
+ $filterRow = $(this);
3178
+ clearTimeout(timer);
3179
+ timer = setTimeout(function() {
3180
+ if ( /enter|over/.test(event.type) ) {
3181
+ $filterRow.removeClass(tscss.filterRowHide);
3182
+ } else {
3183
+ // don't hide if input has focus
3184
+ // $(':focus') needs jQuery 1.6+
3185
+ if ( $(document.activeElement).closest('tr')[0] !== $filterRow[0] ) {
3186
+ // don't hide row if any filter has a value
3187
+ if (c.lastCombinedFilter === '') {
3188
+ $filterRow.addClass(tscss.filterRowHide);
3189
+ }
3190
+ }
3191
+ }
3192
+ }, 200);
3193
+ })
3194
+ .find('input, select').bind('focus blur', function(e) {
3195
+ $filterRow2 = $(this).closest('tr');
3196
+ clearTimeout(timer);
3197
+ var event = e;
3198
+ timer = setTimeout(function() {
3199
+ // don't hide row if any filter has a value
3200
+ if (ts.getFilters(c.$table).join('') === '') {
3201
+ $filterRow2[ event.type === 'focus' ? 'removeClass' : 'addClass'](tscss.filterRowHide);
3202
+ }
3203
+ }, 200);
3204
+ });
3205
+ },
3206
+ defaultFilter: function(filter, mask){
3207
+ if (filter === '') { return filter; }
3208
+ var regex = ts.filter.regex.iQuery,
3209
+ maskLen = mask.match( ts.filter.regex.igQuery ).length,
3210
+ query = maskLen > 1 ? $.trim(filter).split(/\s/) : [ $.trim(filter) ],
3211
+ len = query.length - 1,
3212
+ indx = 0,
3213
+ val = mask;
3214
+ if ( len < 1 && maskLen > 1 ) {
3215
+ // only one "word" in query but mask has >1 slots
3216
+ query[1] = query[0];
3217
+ }
3218
+ // replace all {query} with query words...
3219
+ // if query = "Bob", then convert mask from "!{query}" to "!Bob"
3220
+ // if query = "Bob Joe Frank", then convert mask "{q} OR {q}" to "Bob OR Joe OR Frank"
3221
+ while (regex.test(val)) {
3222
+ val = val.replace(regex, query[indx++] || '');
3223
+ if (regex.test(val) && indx < len && (query[indx] || '') !== '') {
3224
+ val = mask.replace(regex, val);
3225
+ }
3226
+ }
3227
+ return val;
3228
+ },
3229
+ getLatestSearch: function( $input ) {
3230
+ if ($input) {
3231
+ return $input.sort(function(a, b) {
3232
+ return $(b).attr('data-lastSearchTime') - $(a).attr('data-lastSearchTime');
3233
+ });
3234
+ }
3235
+ return $();
3236
+ },
3237
+ multipleColumns: function( c, $input ) {
3238
+ // look for multiple columns "1-3,4-6,8" in data-column
3239
+ var temp, ranges, range, start, end, singles, i, indx, len,
3240
+ wo = c.widgetOptions,
3241
+ // only target "all" column inputs on initialization
3242
+ // & don't target "all" column inputs if they don't exist
3243
+ targets = wo.filter_initialized || !$input.filter(wo.filter_anyColumnSelector).length,
3244
+ columns = [],
3245
+ val = $.trim( ts.filter.getLatestSearch( $input ).attr('data-column') || '' );
3246
+ // process column range
3247
+ if ( targets && /-/.test( val ) ) {
3248
+ ranges = val.match( /(\d+)\s*-\s*(\d+)/g );
3249
+ len = ranges.length;
3250
+ for (indx = 0; indx < len; indx++) {
3251
+ range = ranges[indx].split( /\s*-\s*/ );
3252
+ start = parseInt( range[0], 10 ) || 0;
3253
+ end = parseInt( range[1], 10 ) || ( c.columns - 1 );
3254
+ if ( start > end ) { temp = start; start = end; end = temp; } // swap
3255
+ if ( end >= c.columns ) { end = c.columns - 1; }
3256
+ for ( ; start <= end; start++ ) {
3257
+ columns.push(start);
3258
+ }
3259
+ // remove processed range from val
3260
+ val = val.replace( ranges[indx], '' );
3261
+ }
3262
+ }
3263
+ // process single columns
3264
+ if ( targets && /,/.test( val ) ) {
3265
+ singles = val.split( /\s*,\s*/ );
3266
+ len = singles.length;
3267
+ for (i = 0; i < len; i++) {
3268
+ if (singles[i] !== '') {
3269
+ indx = parseInt( singles[i], 10 );
3270
+ if ( indx < c.columns ) {
3271
+ columns.push( indx );
3272
+ }
3273
+ }
3274
+ }
3275
+ }
3276
+ // return all columns
3277
+ if (!columns.length) {
3278
+ for ( indx = 0; indx < c.columns; indx++ ) {
3279
+ columns.push( indx );
3280
+ }
3281
+ }
3282
+ return columns;
3283
+ },
3284
+ findRows: function(table, filters, combinedFilters) {
3285
+ if (table.config.lastCombinedFilter === combinedFilters || !table.config.widgetOptions.filter_initialized) { return; }
3286
+ var len, norm_rows, $rows, rowIndex, tbodyIndex, $tbody, $cells, $cell, columnIndex,
3287
+ childRow, lastSearch, hasSelect, matches, result, showRow, time, val, indx,
3288
+ notFiltered, searchFiltered, filterMatched, excludeMatch, fxn, ffxn,
3289
+ query, injected, res, id,
3290
+ regex = ts.filter.regex,
3291
+ c = table.config,
3292
+ wo = c.widgetOptions,
3293
+ // data object passed to filters; anyMatch is a flag for the filters
3294
+ data = { anyMatch: false },
3295
+ // anyMatch really screws up with these types of filters
3296
+ noAnyMatch = [ 'range', 'notMatch', 'operators' ];
3297
+
3298
+ // parse columns after formatter, in case the class is added at that point
3299
+ data.parsed = c.$headers.map(function(columnIndex) {
3300
+ return c.parsers && c.parsers[columnIndex] && c.parsers[columnIndex].parsed ||
3301
+ // getData won't return "parsed" if other "filter-" class names exist (e.g. <th class="filter-select filter-parsed">)
3302
+ ts.getData && ts.getData(c.$headerIndexed[columnIndex], ts.getColumnData( table, c.headers, columnIndex ), 'filter') === 'parsed' ||
3303
+ $(this).hasClass('filter-parsed');
3304
+ }).get();
3305
+
3306
+ if (c.debug) {
3307
+ ts.log('Filter: Starting filter widget search', filters);
3308
+ time = new Date();
3309
+ }
3310
+ // filtered rows count
3311
+ c.filteredRows = 0;
3312
+ c.totalRows = 0;
3313
+ // combindedFilters are undefined on init
3314
+ combinedFilters = (filters || []).join('');
3315
+
3316
+ for (tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) {
3317
+ $tbody = ts.processTbody(table, c.$tbodies.eq(tbodyIndex), true);
3318
+ // skip child rows & widget added (removable) rows - fixes #448 thanks to @hempel!
3319
+ // $rows = $tbody.children('tr').not(c.selectorRemove);
3320
+ columnIndex = c.columns;
3321
+ // convert stored rows into a jQuery object
3322
+ norm_rows = c.cache[tbodyIndex].normalized;
3323
+ $rows = $( $.map(norm_rows, function(el){ return el[columnIndex].$row.get(); }) );
3324
+
3325
+ if (combinedFilters === '' || wo.filter_serversideFiltering) {
3326
+ $rows.removeClass(wo.filter_filteredRow).not('.' + c.cssChildRow).css('display', '');
3327
+ } else {
3328
+ // filter out child rows
3329
+ $rows = $rows.not('.' + c.cssChildRow);
3330
+ len = $rows.length;
3331
+
3332
+ if ( (wo.filter_$anyMatch && wo.filter_$anyMatch.length) || typeof filters[c.columns] !== 'undefined' ) {
3333
+ data.anyMatchFlag = true;
3334
+ data.anyMatchFilter = wo.filter_$anyMatch && ts.filter.getLatestSearch( wo.filter_$anyMatch ).val() || ( '' + filters[c.columns] ) || '';
3335
+ if (wo.filter_columnAnyMatch) {
3336
+ // specific columns search
3337
+ query = data.anyMatchFilter.split( ts.filter.regex.andSplit );
3338
+ injected = false;
3339
+ for (indx = 0; indx < query.length; indx++) {
3340
+ res = query[indx].split(':');
3341
+ if ( res.length > 1 ) {
3342
+ // make the column a one-based index ( non-developers start counting from one :P )
3343
+ id = parseInt( res[0], 10 ) - 1;
3344
+ if ( id >= 0 && id < c.columns ) { // if id is an integer
3345
+ filters[id] = res[1];
3346
+ query.splice(indx, 1);
3347
+ indx--;
3348
+ injected = true;
3349
+ }
3350
+ }
3351
+ }
3352
+ if (injected) {
3353
+ data.anyMatchFilter = query.join(' && ');
3354
+ }
3355
+ }
3356
+ }
3357
+
3358
+ // optimize searching only through already filtered rows - see #313
3359
+ searchFiltered = wo.filter_searchFiltered;
3360
+ lastSearch = c.lastSearch || c.$table.data('lastSearch') || [];
3361
+ if (searchFiltered) {
3362
+ // cycle through all filters; include last (columnIndex + 1 = match any column). Fixes #669
3363
+ for (indx = 0; indx < columnIndex + 1; indx++) {
3364
+ val = filters[indx] || '';
3365
+ // break out of loop if we've already determined not to search filtered rows
3366
+ if (!searchFiltered) { indx = columnIndex; }
3367
+ // search already filtered rows if...
3368
+ searchFiltered = searchFiltered && lastSearch.length &&
3369
+ // there are no changes from beginning of filter
3370
+ val.indexOf(lastSearch[indx] || '') === 0 &&
3371
+ // if there is NOT a logical "or", or range ("to" or "-") in the string
3372
+ !regex.alreadyFiltered.test(val) &&
3373
+ // if we are not doing exact matches, using "|" (logical or) or not "!"
3374
+ !/[=\"\|!]/.test(val) &&
3375
+ // don't search only filtered if the value is negative ('> -10' => '> -100' will ignore hidden rows)
3376
+ !(/(>=?\s*-\d)/.test(val) || /(<=?\s*\d)/.test(val)) &&
3377
+ // if filtering using a select without a "filter-match" class (exact match) - fixes #593
3378
+ !( val !== '' && c.$filters && c.$filters.eq(indx).find('select').length && !c.$headerIndexed[indx].hasClass('filter-match') );
3379
+ }
3380
+ }
3381
+ notFiltered = $rows.not('.' + wo.filter_filteredRow).length;
3382
+ // can't search when all rows are hidden - this happens when looking for exact matches
3383
+ if (searchFiltered && notFiltered === 0) { searchFiltered = false; }
3384
+ if (c.debug) {
3385
+ ts.log( 'Filter: Searching through ' + ( searchFiltered && notFiltered < len ? notFiltered : 'all' ) + ' rows' );
3386
+ }
3387
+ if (data.anyMatchFlag) {
3388
+ if (c.sortLocaleCompare) {
3389
+ // replace accents
3390
+ data.anyMatchFilter = ts.replaceAccents(data.anyMatchFilter);
3391
+ }
3392
+ if (wo.filter_defaultFilter && regex.iQuery.test( ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || '')) {
3393
+ data.anyMatchFilter = ts.filter.defaultFilter( data.anyMatchFilter, ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) );
3394
+ // clear search filtered flag because default filters are not saved to the last search
3395
+ searchFiltered = false;
3396
+ }
3397
+ // make iAnyMatchFilter lowercase unless both filter widget & core ignoreCase options are true
3398
+ // when c.ignoreCase is true, the cache contains all lower case data
3399
+ data.iAnyMatchFilter = !(wo.filter_ignoreCase && c.ignoreCase) ? data.anyMatchFilter : data.anyMatchFilter.toLocaleLowerCase();
3400
+ }
3401
+
3402
+ // loop through the rows
3403
+ for (rowIndex = 0; rowIndex < len; rowIndex++) {
3404
+
3405
+ data.cacheArray = norm_rows[rowIndex];
3406
+
3407
+ childRow = $rows[rowIndex].className;
3408
+ // skip child rows & already filtered rows
3409
+ if ( regex.child.test(childRow) || (searchFiltered && regex.filtered.test(childRow)) ) { continue; }
3410
+ showRow = true;
3411
+ // *** nextAll/nextUntil not supported by Zepto! ***
3412
+ childRow = $rows.eq(rowIndex).nextUntil('tr:not(.' + c.cssChildRow + ')');
3413
+ // so, if "table.config.widgetOptions.filter_childRows" is true and there is
3414
+ // a match anywhere in the child row, then it will make the row visible
3415
+ // checked here so the option can be changed dynamically
3416
+ data.childRowText = (childRow.length && wo.filter_childRows) ? childRow.text() : '';
3417
+ data.childRowText = wo.filter_ignoreCase ? data.childRowText.toLocaleLowerCase() : data.childRowText;
3418
+ $cells = $rows.eq(rowIndex).children();
3419
+ if (data.anyMatchFlag) {
3420
+ // look for multiple columns "1-3,4-6,8"
3421
+ columnIndex = ts.filter.multipleColumns( c, wo.filter_$anyMatch );
3422
+ data.anyMatch = true;
3423
+ data.rowArray = $cells.map(function(i){
3424
+ if ( $.inArray(i, columnIndex) > -1 ) {
3425
+ var txt;
3426
+ if (data.parsed[i]) {
3427
+ txt = data.cacheArray[i];
3428
+ } else {
3429
+ txt = this ? this.getAttribute( c.textAttribute ) || this.textContent || $(this).text() : '';
3430
+ txt = $.trim( wo.filter_ignoreCase ? txt.toLowerCase() : txt );
3431
+ if (c.sortLocaleCompare) {
3432
+ txt = ts.replaceAccents(txt);
3433
+ }
3434
+ }
3435
+ return txt;
3436
+ }
3437
+ }).get();
3438
+ data.filter = data.anyMatchFilter;
3439
+ data.iFilter = data.iAnyMatchFilter;
3440
+ data.exact = data.rowArray.join(' ');
3441
+ data.iExact = wo.filter_ignoreCase ? data.exact.toLowerCase() : data.exact;
3442
+ data.cache = data.cacheArray.slice(0,-1).join(' ');
3443
+ filterMatched = null;
3444
+ $.each(ts.filter.types, function(type, typeFunction) {
3445
+ if ($.inArray(type, noAnyMatch) < 0) {
3446
+ matches = typeFunction( c, data );
3447
+ if (matches !== null) {
3448
+ filterMatched = matches;
3449
+ return false;
3450
+ }
3451
+ }
3452
+ });
3453
+ if (filterMatched !== null) {
3454
+ showRow = filterMatched;
3455
+ } else {
3456
+ if (wo.filter_startsWith) {
3457
+ showRow = false;
3458
+ columnIndex = c.columns;
3459
+ while (!showRow && columnIndex > 0) {
3460
+ columnIndex--;
3461
+ showRow = showRow || data.rowArray[columnIndex].indexOf(data.iFilter) === 0;
3462
+ }
3463
+ } else {
3464
+ showRow = (data.iExact + data.childRowText).indexOf(data.iFilter) >= 0;
3465
+ }
3466
+ }
3467
+ data.anyMatch = false;
3468
+ }
3469
+
3470
+ for (columnIndex = 0; columnIndex < c.columns; columnIndex++) {
3471
+ data.filter = filters[columnIndex];
3472
+ data.index = columnIndex;
3473
+
3474
+ // filter types to exclude, per column
3475
+ excludeMatch = ( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split(/\s+/);
3476
+
3477
+ // ignore if filter is empty or disabled
3478
+ if (data.filter) {
3479
+ data.cache = data.cacheArray[columnIndex];
3480
+ // check if column data should be from the cell or from parsed data
3481
+ if (wo.filter_useParsedData || data.parsed[columnIndex]) {
3482
+ data.exact = data.cache;
3483
+ } else {
3484
+ val = $cells[columnIndex];
3485
+ result = val ? $.trim( val.getAttribute( c.textAttribute ) || val.textContent || $cells.eq(columnIndex).text() ) : '';
3486
+ data.exact = c.sortLocaleCompare ? ts.replaceAccents(result) : result; // issue #405
3487
+ }
3488
+ data.iExact = !regex.type.test(typeof data.exact) && wo.filter_ignoreCase ? data.exact.toLocaleLowerCase() : data.exact;
3489
+ result = showRow; // if showRow is true, show that row
3490
+
3491
+ // in case select filter option has a different value vs text "a - z|A through Z"
3492
+ ffxn = wo.filter_columnFilters ?
3493
+ c.$filters.add(c.$externalFilters).filter('[data-column="'+ columnIndex + '"]').find('select option:selected').attr('data-function-name') || '' : '';
3494
+ // replace accents - see #357
3495
+ if (c.sortLocaleCompare) {
3496
+ data.filter = ts.replaceAccents(data.filter);
3497
+ }
3498
+
3499
+ val = true;
3500
+ if (wo.filter_defaultFilter && regex.iQuery.test( ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || '')) {
3501
+ data.filter = ts.filter.defaultFilter( data.filter, ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) );
3502
+ // val is used to indicate that a filter select is using a default filter; so we override the exact & partial matches
3503
+ val = false;
3504
+ }
3505
+ // data.iFilter = case insensitive (if wo.filter_ignoreCase is true), data.filter = case sensitive
3506
+ data.iFilter = wo.filter_ignoreCase ? (data.filter || '').toLocaleLowerCase() : data.filter;
3507
+ fxn = ts.getColumnData( table, wo.filter_functions, columnIndex );
3508
+ $cell = c.$headerIndexed[columnIndex];
3509
+ hasSelect = $cell.hasClass('filter-select');
3510
+ if ( fxn || ( hasSelect && val ) ) {
3511
+ if (fxn === true || hasSelect) {
3512
+ // default selector uses exact match unless "filter-match" class is found
3513
+ result = ($cell.hasClass('filter-match')) ? data.iExact.search(data.iFilter) >= 0 : data.filter === data.exact;
3514
+ } else if (typeof fxn === 'function') {
3515
+ // filter callback( exact cell content, parser normalized content, filter input value, column index, jQuery row object )
3516
+ result = fxn(data.exact, data.cache, data.filter, columnIndex, $rows.eq(rowIndex), c);
3517
+ } else if (typeof fxn[ffxn || data.filter] === 'function') {
3518
+ // selector option function
3519
+ result = fxn[ffxn || data.filter](data.exact, data.cache, data.filter, columnIndex, $rows.eq(rowIndex), c);
3520
+ }
3521
+ } else {
3522
+ filterMatched = null;
3523
+ // cycle through the different filters
3524
+ // filters return a boolean or null if nothing matches
3525
+ $.each(ts.filter.types, function(type, typeFunction) {
3526
+ if ($.inArray(type, excludeMatch) < 0) {
3527
+ matches = typeFunction( c, data );
3528
+ if (matches !== null) {
3529
+ filterMatched = matches;
3530
+ return false;
3531
+ }
3532
+ }
3533
+ });
3534
+ if (filterMatched !== null) {
3535
+ result = filterMatched;
3536
+ // Look for match, and add child row data for matching
3537
+ } else {
3538
+ data.exact = (data.iExact + data.childRowText).indexOf( ts.filter.parseFilter(c, data.iFilter, columnIndex, data.parsed[columnIndex]) );
3539
+ result = ( (!wo.filter_startsWith && data.exact >= 0) || (wo.filter_startsWith && data.exact === 0) );
3540
+ }
3541
+ }
3542
+ showRow = (result) ? showRow : false;
3543
+ }
3544
+ }
3545
+ $rows.eq(rowIndex)
3546
+ .toggleClass(wo.filter_filteredRow, !showRow)[0]
3547
+ .display = showRow ? '' : 'none';
3548
+ if (childRow.length) {
3549
+ childRow.toggleClass(wo.filter_filteredRow, !showRow);
3550
+ }
3551
+ }
3552
+ }
3553
+ c.filteredRows += $rows.not('.' + wo.filter_filteredRow).length;
3554
+ c.totalRows += $rows.length;
3555
+ ts.processTbody(table, $tbody, false);
3556
+ }
3557
+ c.lastCombinedFilter = combinedFilters; // save last search
3558
+ c.lastSearch = filters;
3559
+ c.$table.data('lastSearch', filters);
3560
+ if (wo.filter_saveFilters && ts.storage) {
3561
+ ts.storage( table, 'tablesorter-filters', filters );
3562
+ }
3563
+ if (c.debug) {
3564
+ ts.benchmark("Completed filter widget search", time);
3565
+ }
3566
+ if (wo.filter_initialized) { c.$table.trigger('filterEnd', c ); }
3567
+ setTimeout(function(){
3568
+ c.$table.trigger('applyWidgets'); // make sure zebra widget is applied
3569
+ }, 0);
3570
+ },
3571
+ getOptionSource: function(table, column, onlyAvail) {
3572
+ table = $(table)[0];
3573
+ var cts, indx, len,
3574
+ c = table.config,
3575
+ wo = c.widgetOptions,
3576
+ parsed = [],
3577
+ arry = false,
3578
+ source = wo.filter_selectSource,
3579
+ last = c.$table.data('lastSearch') || [],
3580
+ fxn = $.isFunction(source) ? true : ts.getColumnData( table, source, column );
3581
+
3582
+ if (onlyAvail && last[column] !== '') {
3583
+ onlyAvail = false;
3584
+ }
3585
+
3586
+ // filter select source option
3587
+ if (fxn === true) {
3588
+ // OVERALL source
3589
+ arry = source(table, column, onlyAvail);
3590
+ } else if ( fxn instanceof $ || ($.type(fxn) === 'string' && fxn.indexOf('</option>') >= 0) ) {
3591
+ // selectSource is a jQuery object or string of options
3592
+ return fxn;
3593
+ } else if ($.isArray(fxn)) {
3594
+ arry = fxn;
3595
+ } else if ($.type(source) === 'object' && fxn) {
3596
+ // custom select source function for a SPECIFIC COLUMN
3597
+ arry = fxn(table, column, onlyAvail);
3598
+ }
3599
+ if (arry === false) {
3600
+ // fall back to original method
3601
+ arry = ts.filter.getOptions(table, column, onlyAvail);
3602
+ }
3603
+
3604
+ // get unique elements and sort the list
3605
+ // if $.tablesorter.sortText exists (not in the original tablesorter),
3606
+ // then natural sort the list otherwise use a basic sort
3607
+ arry = $.grep(arry, function(value, indx) {
3608
+ return $.inArray(value, arry) === indx;
3609
+ });
3610
+
3611
+ if (c.$headerIndexed[column].hasClass('filter-select-nosort')) {
3612
+ // unsorted select options
3613
+ return arry;
3614
+ } else {
3615
+ len = arry.length;
3616
+ // parse select option values
3617
+ for (indx = 0; indx < len; indx++) {
3618
+ // parse array data using set column parser; this DOES NOT pass the original
3619
+ // table cell to the parser format function
3620
+ parsed.push({ t : arry[indx], p : c.parsers && c.parsers[column].format( arry[indx], table, [], column ) });
3621
+ }
3622
+
3623
+ // sort parsed select options
3624
+ cts = c.textSorter || '';
3625
+ parsed.sort(function(a, b){
3626
+ // sortNatural breaks if you don't pass it strings
3627
+ var x = a.p.toString(), y = b.p.toString();
3628
+ if ($.isFunction(cts)) {
3629
+ // custom OVERALL text sorter
3630
+ return cts(x, y, true, column, table);
3631
+ } else if (typeof(cts) === 'object' && cts.hasOwnProperty(column)) {
3632
+ // custom text sorter for a SPECIFIC COLUMN
3633
+ return cts[column](x, y, true, column, table);
3634
+ } else if (ts.sortNatural) {
3635
+ // fall back to natural sort
3636
+ return ts.sortNatural(x, y);
3637
+ }
3638
+ // using an older version! do a basic sort
3639
+ return true;
3640
+ });
3641
+ // rebuild arry from sorted parsed data
3642
+ arry = [];
3643
+ len = parsed.length;
3644
+ for (indx = 0; indx < len; indx++) {
3645
+ arry.push( parsed[indx].t );
3646
+ }
3647
+ return arry;
3648
+ }
3649
+ },
3650
+ getOptions: function(table, column, onlyAvail) {
3651
+ table = $(table)[0];
3652
+ var rowIndex, tbodyIndex, len, row, cache, cell,
3653
+ c = table.config,
3654
+ wo = c.widgetOptions,
3655
+ arry = [];
3656
+ for (tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) {
3657
+ cache = c.cache[tbodyIndex];
3658
+ len = c.cache[tbodyIndex].normalized.length;
3659
+ // loop through the rows
3660
+ for (rowIndex = 0; rowIndex < len; rowIndex++) {
3661
+ // get cached row from cache.row (old) or row data object (new; last item in normalized array)
3662
+ row = cache.row ? cache.row[rowIndex] : cache.normalized[rowIndex][c.columns].$row[0];
3663
+ // check if has class filtered
3664
+ if (onlyAvail && row.className.match(wo.filter_filteredRow)) { continue; }
3665
+ // get non-normalized cell content
3666
+ if (wo.filter_useParsedData || c.parsers[column].parsed || c.$headerIndexed[column].hasClass('filter-parsed')) {
3667
+ arry.push( '' + cache.normalized[rowIndex][column] );
3668
+ } else {
3669
+ cell = row.cells[column];
3670
+ if (cell) {
3671
+ arry.push( $.trim( cell.getAttribute( c.textAttribute ) || cell.textContent || $(cell).text() ) );
3672
+ }
3673
+ }
3674
+ }
3675
+ }
3676
+ return arry;
3677
+ },
3678
+ buildSelect: function(table, column, arry, updating, onlyAvail) {
3679
+ table = $(table)[0];
3680
+ column = parseInt(column, 10);
3681
+ if (!table.config.cache || $.isEmptyObject(table.config.cache)) { return; }
3682
+ var indx, val, txt, t, $filters, $filter,
3683
+ c = table.config,
3684
+ wo = c.widgetOptions,
3685
+ node = c.$headerIndexed[column],
3686
+ // t.data('placeholder') won't work in jQuery older than 1.4.3
3687
+ options = '<option value="">' + ( node.data('placeholder') || node.attr('data-placeholder') || wo.filter_placeholder.select || '' ) + '</option>',
3688
+ // Get curent filter value
3689
+ currentValue = c.$table.find('thead').find('select.' + tscss.filter + '[data-column="' + column + '"]').val();
3690
+ // nothing included in arry (external source), so get the options from filter_selectSource or column data
3691
+ if (typeof arry === 'undefined' || arry === '') {
3692
+ arry = ts.filter.getOptionSource(table, column, onlyAvail);
3693
+ }
3694
+
3695
+ if ($.isArray(arry)) {
3696
+ // build option list
3697
+ for (indx = 0; indx < arry.length; indx++) {
3698
+ txt = arry[indx] = ('' + arry[indx]).replace(/\"/g, "&quot;");
3699
+ val = txt;
3700
+ // allow including a symbol in the selectSource array
3701
+ // "a-z|A through Z" so that "a-z" becomes the option value
3702
+ // and "A through Z" becomes the option text
3703
+ if (txt.indexOf(wo.filter_selectSourceSeparator) >= 0) {
3704
+ t = txt.split(wo.filter_selectSourceSeparator);
3705
+ val = t[0];
3706
+ txt = t[1];
3707
+ }
3708
+ // replace quotes - fixes #242 & ignore empty strings - see http://stackoverflow.com/q/14990971/145346
3709
+ options += arry[indx] !== '' ? '<option ' + (val === txt ? '' : 'data-function-name="' + arry[indx] + '" ') + 'value="' + val + '">' + txt + '</option>' : '';
3710
+ }
3711
+ // clear arry so it doesn't get appended twice
3712
+ arry = [];
3713
+ }
3714
+
3715
+ // update all selects in the same column (clone thead in sticky headers & any external selects) - fixes 473
3716
+ $filters = ( c.$filters ? c.$filters : c.$table.children('thead') ).find('.' + tscss.filter);
3717
+ if (wo.filter_$externalFilters) {
3718
+ $filters = $filters && $filters.length ? $filters.add(wo.filter_$externalFilters) : wo.filter_$externalFilters;
3719
+ }
3720
+ $filter = $filters.filter('select[data-column="' + column + '"]');
3721
+
3722
+ // make sure there is a select there!
3723
+ if ($filter.length) {
3724
+ $filter[ updating ? 'html' : 'append' ](options);
3725
+ if (!$.isArray(arry)) {
3726
+ // append options if arry is provided externally as a string or jQuery object
3727
+ // options (default value) was already added
3728
+ $filter.append(arry).val(currentValue);
3729
+ }
3730
+ $filter.val(currentValue);
3731
+ }
3732
+ },
3733
+ buildDefault: function(table, updating) {
3734
+ var columnIndex, $header, noSelect,
3735
+ c = table.config,
3736
+ wo = c.widgetOptions,
3737
+ columns = c.columns;
3738
+ // build default select dropdown
3739
+ for (columnIndex = 0; columnIndex < columns; columnIndex++) {
3740
+ $header = c.$headerIndexed[columnIndex];
3741
+ noSelect = !($header.hasClass('filter-false') || $header.hasClass('parser-false'));
3742
+ // look for the filter-select class; build/update it if found
3743
+ if (($header.hasClass('filter-select') || ts.getColumnData( table, wo.filter_functions, columnIndex ) === true) && noSelect) {
3744
+ ts.filter.buildSelect(table, columnIndex, '', updating, $header.hasClass(wo.filter_onlyAvail));
3745
+ }
3746
+ }
3747
+ }
3748
+ };
3749
+
3750
+ ts.getFilters = function(table, getRaw, setFilters, skipFirst) {
3751
+ var i, $filters, $column, cols,
3752
+ filters = false,
3753
+ c = table ? $(table)[0].config : '',
3754
+ wo = c ? c.widgetOptions : '';
3755
+ if (getRaw !== true && wo && !wo.filter_columnFilters) {
3756
+ return $(table).data('lastSearch');
3757
+ }
3758
+ if (c) {
3759
+ if (c.$filters) {
3760
+ $filters = c.$filters.find('.' + tscss.filter);
3761
+ }
3762
+ if (wo.filter_$externalFilters) {
3763
+ $filters = $filters && $filters.length ? $filters.add(wo.filter_$externalFilters) : wo.filter_$externalFilters;
3764
+ }
3765
+ if ($filters && $filters.length) {
3766
+ filters = setFilters || [];
3767
+ for (i = 0; i < c.columns + 1; i++) {
3768
+ cols = ( i === c.columns ?
3769
+ // "all" columns can now include a range or set of columms (data-column="0-2,4,6-7")
3770
+ wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector :
3771
+ '[data-column="' + i + '"]' );
3772
+ $column = $filters.filter(cols);
3773
+ if ($column.length) {
3774
+ // move the latest search to the first slot in the array
3775
+ $column = ts.filter.getLatestSearch( $column );
3776
+ if ($.isArray(setFilters)) {
3777
+ // skip first (latest input) to maintain cursor position while typing
3778
+ if (skipFirst) { $column.slice(1); }
3779
+ if (i === c.columns) {
3780
+ // prevent data-column="all" from filling data-column="0,1" (etc)
3781
+ cols = $column.filter(wo.filter_anyColumnSelector);
3782
+ $column = cols.length ? cols : $column;
3783
+ }
3784
+ $column
3785
+ .val( setFilters[i] )
3786
+ .trigger('change.tsfilter');
3787
+ } else {
3788
+ filters[i] = $column.val() || '';
3789
+ // don't change the first... it will move the cursor
3790
+ if (i === c.columns) {
3791
+ // don't update range columns from "all" setting
3792
+ $column.slice(1).filter('[data-column*="' + $column.attr('data-column') + '"]').val( filters[i] );
3793
+ } else {
3794
+ $column.slice(1).val( filters[i] );
3795
+ }
3796
+ }
3797
+ // save any match input dynamically
3798
+ if (i === c.columns && $column.length) {
3799
+ wo.filter_$anyMatch = $column;
3800
+ }
3801
+ }
3802
+ }
3803
+ }
3804
+ }
3805
+ if (filters.length === 0) {
3806
+ filters = false;
3807
+ }
3808
+ return filters;
3809
+ };
3810
+
3811
+ ts.setFilters = function(table, filter, apply, skipFirst) {
3812
+ var c = table ? $(table)[0].config : '',
3813
+ valid = ts.getFilters(table, true, filter, skipFirst);
3814
+ if (c && apply) {
3815
+ // ensure new set filters are applied, even if the search is the same
3816
+ c.lastCombinedFilter = null;
3817
+ c.lastSearch = [];
3818
+ ts.filter.searching(c.table, filter, skipFirst);
3819
+ c.$table.trigger('filterFomatterUpdate');
3820
+ }
3821
+ return !!valid;
3822
+ };
3823
+
3824
+ })(jQuery);
3825
+
3826
+ /*! Widget: stickyHeaders - updated 3/26/2015 (v2.21.3) *//*
3827
+ * Requires tablesorter v2.8+ and jQuery 1.4.3+
3828
+ * by Rob Garrison
3829
+ */
3830
+ ;(function ($, window) {
3831
+ 'use strict';
3832
+ var ts = $.tablesorter = $.tablesorter || {};
3833
+
3834
+ $.extend(ts.css, {
3835
+ sticky : 'tablesorter-stickyHeader', // stickyHeader
3836
+ stickyVis : 'tablesorter-sticky-visible',
3837
+ stickyHide: 'tablesorter-sticky-hidden',
3838
+ stickyWrap: 'tablesorter-sticky-wrapper'
3839
+ });
3840
+
3841
+ // Add a resize event to table headers
3842
+ ts.addHeaderResizeEvent = function(table, disable, settings) {
3843
+ table = $(table)[0]; // make sure we're using a dom element
3844
+ var headers,
3845
+ defaults = {
3846
+ timer : 250
3847
+ },
3848
+ options = $.extend({}, defaults, settings),
3849
+ c = table.config,
3850
+ wo = c.widgetOptions,
3851
+ checkSizes = function(triggerEvent) {
3852
+ wo.resize_flag = true;
3853
+ headers = [];
3854
+ c.$headers.each(function() {
3855
+ var $header = $(this),
3856
+ sizes = $header.data('savedSizes') || [0,0], // fixes #394
3857
+ width = this.offsetWidth,
3858
+ height = this.offsetHeight;
3859
+ if (width !== sizes[0] || height !== sizes[1]) {
3860
+ $header.data('savedSizes', [ width, height ]);
3861
+ headers.push(this);
3862
+ }
3863
+ });
3864
+ if (headers.length && triggerEvent !== false) {
3865
+ c.$table.trigger('resize', [ headers ]);
3866
+ }
3867
+ wo.resize_flag = false;
3868
+ };
3869
+ checkSizes(false);
3870
+ clearInterval(wo.resize_timer);
3871
+ if (disable) {
3872
+ wo.resize_flag = false;
3873
+ return false;
3874
+ }
3875
+ wo.resize_timer = setInterval(function() {
3876
+ if (wo.resize_flag) { return; }
3877
+ checkSizes();
3878
+ }, options.timer);
3879
+ };
3880
+
3881
+ // Sticky headers based on this awesome article:
3882
+ // http://css-tricks.com/13465-persistent-headers/
3883
+ // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech
3884
+ // **************************
3885
+ ts.addWidget({
3886
+ id: "stickyHeaders",
3887
+ priority: 60, // sticky widget must be initialized after the filter widget!
3888
+ options: {
3889
+ stickyHeaders : '', // extra class name added to the sticky header row
3890
+ stickyHeaders_attachTo : null, // jQuery selector or object to attach sticky header to
3891
+ stickyHeaders_xScroll : null, // jQuery selector or object to monitor horizontal scroll position (defaults: xScroll > attachTo > window)
3892
+ stickyHeaders_yScroll : null, // jQuery selector or object to monitor vertical scroll position (defaults: yScroll > attachTo > window)
3893
+ stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element
3894
+ stickyHeaders_filteredToTop: true, // scroll table top into view after filtering
3895
+ stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists
3896
+ stickyHeaders_addResizeEvent : true, // trigger "resize" event on headers
3897
+ stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header
3898
+ stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs
3899
+ },
3900
+ format: function(table, c, wo) {
3901
+ // filter widget doesn't initialize on an empty table. Fixes #449
3902
+ if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) {
3903
+ return;
3904
+ }
3905
+ var $table = c.$table,
3906
+ // add position: relative to attach element, hopefully it won't cause trouble.
3907
+ $attach = $(wo.stickyHeaders_attachTo),
3908
+ namespace = c.namespace + 'stickyheaders ',
3909
+ // element to watch for the scroll event
3910
+ $yScroll = $(wo.stickyHeaders_yScroll || wo.stickyHeaders_attachTo || window),
3911
+ $xScroll = $(wo.stickyHeaders_xScroll || wo.stickyHeaders_attachTo || window),
3912
+ $thead = $table.children('thead:first'),
3913
+ $header = $thead.children('tr').not('.sticky-false').children(),
3914
+ $tfoot = $table.children('tfoot'),
3915
+ $stickyOffset = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : '',
3916
+ stickyOffset = $stickyOffset.length ? $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0,
3917
+ // is this table nested? If so, find parent sticky header wrapper (div, not table)
3918
+ $nestedSticky = $table.parent().closest('.' + ts.css.table).hasClass('hasStickyHeaders') ?
3919
+ $table.parent().closest('table.tablesorter')[0].config.widgetOptions.$sticky.parent() : [],
3920
+ nestedStickyTop = $nestedSticky.length ? $nestedSticky.height() : 0,
3921
+ // clone table, then wrap to make sticky header
3922
+ $stickyTable = wo.$sticky = $table.clone()
3923
+ .addClass('containsStickyHeaders ' + ts.css.sticky + ' ' + wo.stickyHeaders + ' ' + c.namespace.slice(1) + '_extra_table' )
3924
+ .wrap('<div class="' + ts.css.stickyWrap + '">'),
3925
+ $stickyWrap = $stickyTable.parent()
3926
+ .addClass(ts.css.stickyHide)
3927
+ .css({
3928
+ position : $attach.length ? 'absolute' : 'fixed',
3929
+ padding : parseInt( $stickyTable.parent().parent().css('padding-left'), 10 ),
3930
+ top : stickyOffset + nestedStickyTop,
3931
+ left : 0,
3932
+ visibility : 'hidden',
3933
+ zIndex : wo.stickyHeaders_zIndex || 2
3934
+ }),
3935
+ $stickyThead = $stickyTable.children('thead:first'),
3936
+ $stickyCells,
3937
+ laststate = '',
3938
+ spacing = 0,
3939
+ setWidth = function($orig, $clone){
3940
+ $orig.filter(':visible').each(function(i) {
3941
+ var width, border,
3942
+ $cell = $clone.filter(':visible').eq(i),
3943
+ $this = $(this);
3944
+ // code from https://github.com/jmosbech/StickyTableHeaders
3945
+ if ($this.css('box-sizing') === 'border-box') {
3946
+ width = $this.outerWidth();
3947
+ } else {
3948
+ if ($cell.css('border-collapse') === 'collapse') {
3949
+ if (window.getComputedStyle) {
3950
+ width = parseFloat( window.getComputedStyle(this, null).width );
3951
+ } else {
3952
+ // ie8 only
3953
+ border = parseFloat( $this.css('border-width') );
3954
+ width = $this.outerWidth() - parseFloat( $this.css('padding-left') ) - parseFloat( $this.css('padding-right') ) - border;
3955
+ }
3956
+ } else {
3957
+ width = $this.width();
3958
+ }
3959
+ }
3960
+ $cell.css({
3961
+ 'min-width': width,
3962
+ 'max-width': width
3963
+ });
3964
+ });
3965
+ },
3966
+ resizeHeader = function() {
3967
+ stickyOffset = $stickyOffset.length ? $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0;
3968
+ spacing = 0;
3969
+ $stickyWrap.css({
3970
+ left : $attach.length ? parseInt($attach.css('padding-left'), 10) || 0 :
3971
+ $table.offset().left - parseInt($table.css('margin-left'), 10) - $xScroll.scrollLeft() - spacing,
3972
+ width: $table.outerWidth()
3973
+ });
3974
+ setWidth( $table, $stickyTable );
3975
+ setWidth( $header, $stickyCells );
3976
+ };
3977
+ // only add a position relative if a position isn't already defined
3978
+ if ($attach.length && !$attach.css('position')) {
3979
+ $attach.css('position', 'relative');
3980
+ }
3981
+ // fix clone ID, if it exists - fixes #271
3982
+ if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; }
3983
+ // clear out cloned table, except for sticky header
3984
+ // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing
3985
+ $stickyTable.find('thead:gt(0), tr.sticky-false').hide();
3986
+ $stickyTable.find('tbody, tfoot').remove();
3987
+ $stickyTable.find('caption').toggle(wo.stickyHeaders_includeCaption);
3988
+ // issue #172 - find td/th in sticky header
3989
+ $stickyCells = $stickyThead.children().children();
3990
+ $stickyTable.css({ height:0, width:0, margin: 0 });
3991
+ // remove resizable block
3992
+ $stickyCells.find('.' + ts.css.resizer).remove();
3993
+ // update sticky header class names to match real header after sorting
3994
+ $table
3995
+ .addClass('hasStickyHeaders')
3996
+ .bind('pagerComplete' + namespace, function() {
3997
+ resizeHeader();
3998
+ });
3999
+
4000
+ ts.bindEvents(table, $stickyThead.children().children('.' + ts.css.header));
4001
+
4002
+ // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned.
4003
+ $table.after( $stickyWrap );
4004
+
4005
+ // onRenderHeader is defined, we need to do something about it (fixes #641)
4006
+ if (c.onRenderHeader) {
4007
+ $stickyThead.children('tr').children().each(function(index){
4008
+ // send second parameter
4009
+ c.onRenderHeader.apply( $(this), [ index, c, $stickyTable ] );
4010
+ });
4011
+ }
4012
+
4013
+ // make it sticky!
4014
+ $xScroll.add($yScroll)
4015
+ .unbind( ('scroll resize '.split(' ').join( namespace )).replace(/\s+/g, ' ') )
4016
+ .bind('scroll resize '.split(' ').join( namespace ), function(event) {
4017
+ if (!$table.is(':visible')) { return; } // fixes #278
4018
+ // Detect nested tables - fixes #724
4019
+ nestedStickyTop = $nestedSticky.length ? $nestedSticky.offset().top - $yScroll.scrollTop() + $nestedSticky.height() : 0;
4020
+ var offset = $table.offset(),
4021
+ yWindow = $.isWindow( $yScroll[0] ), // $.isWindow needs jQuery 1.4.3
4022
+ xWindow = $.isWindow( $xScroll[0] ),
4023
+ // scrollTop = ( $attach.length ? $attach.offset().top : $yScroll.scrollTop() ) + stickyOffset + nestedStickyTop,
4024
+ scrollTop = ( $attach.length ? ( yWindow ? $yScroll.scrollTop() : $yScroll.offset().top ) : $yScroll.scrollTop() ) + stickyOffset + nestedStickyTop,
4025
+ tableHeight = $table.height() - ($stickyWrap.height() + ($tfoot.height() || 0)),
4026
+ isVisible = ( scrollTop > offset.top ) && ( scrollTop < offset.top + tableHeight ) ? 'visible' : 'hidden',
4027
+ cssSettings = { visibility : isVisible };
4028
+
4029
+ if ($attach.length) {
4030
+ cssSettings.top = yWindow ? scrollTop - $attach.offset().top : $attach.scrollTop();
4031
+ }
4032
+ if (xWindow) {
4033
+ // adjust when scrolling horizontally - fixes issue #143
4034
+ cssSettings.left = $table.offset().left - parseInt($table.css('margin-left'), 10) - $xScroll.scrollLeft() - spacing;
4035
+ }
4036
+ if ($nestedSticky.length) {
4037
+ cssSettings.top = ( cssSettings.top || 0 ) + stickyOffset + nestedStickyTop;
4038
+ }
4039
+ $stickyWrap
4040
+ .removeClass( ts.css.stickyVis + ' ' + ts.css.stickyHide )
4041
+ .addClass( isVisible === 'visible' ? ts.css.stickyVis : ts.css.stickyHide )
4042
+ .css(cssSettings);
4043
+ if (isVisible !== laststate || event.type === 'resize') {
4044
+ // make sure the column widths match
4045
+ resizeHeader();
4046
+ laststate = isVisible;
4047
+ }
4048
+ });
4049
+ if (wo.stickyHeaders_addResizeEvent) {
4050
+ ts.addHeaderResizeEvent(table);
4051
+ }
4052
+
4053
+ // look for filter widget
4054
+ if ($table.hasClass('hasFilters') && wo.filter_columnFilters) {
4055
+ // scroll table into view after filtering, if sticky header is active - #482
4056
+ $table.bind('filterEnd' + namespace, function() {
4057
+ // $(':focus') needs jQuery 1.6+
4058
+ var $td = $(document.activeElement).closest('td'),
4059
+ column = $td.parent().children().index($td);
4060
+ // only scroll if sticky header is active
4061
+ if ($stickyWrap.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) {
4062
+ // scroll to original table (not sticky clone)
4063
+ window.scrollTo(0, $table.position().top);
4064
+ // give same input/select focus; check if c.$filters exists; fixes #594
4065
+ if (column >= 0 && c.$filters) {
4066
+ c.$filters.eq(column).find('a, select, input').filter(':visible').focus();
4067
+ }
4068
+ }
4069
+ });
4070
+ ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) );
4071
+ // support hideFilters
4072
+ if (wo.filter_hideFilters) {
4073
+ ts.filter.hideFilters($stickyTable, c);
4074
+ }
4075
+ }
4076
+
4077
+ $table.trigger('stickyHeadersInit');
4078
+
4079
+ },
4080
+ remove: function(table, c, wo) {
4081
+ var namespace = c.namespace + 'stickyheaders ';
4082
+ c.$table
4083
+ .removeClass('hasStickyHeaders')
4084
+ .unbind( ('pagerComplete filterEnd '.split(' ').join(namespace)).replace(/\s+/g, ' ') )
4085
+ .next('.' + ts.css.stickyWrap).remove();
4086
+ if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table
4087
+ $(window)
4088
+ .add(wo.stickyHeaders_xScroll)
4089
+ .add(wo.stickyHeaders_yScroll)
4090
+ .add(wo.stickyHeaders_attachTo)
4091
+ .unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') );
4092
+ ts.addHeaderResizeEvent(table, false);
4093
+ }
4094
+ });
4095
+
4096
+ })(jQuery, window);
4097
+
4098
+ /*! Widget: resizable - updated 3/26/2015 (v2.21.3) */
4099
+ ;(function ($, window) {
4100
+ 'use strict';
4101
+ var ts = $.tablesorter = $.tablesorter || {};
4102
+
4103
+ $.extend(ts.css, {
4104
+ resizableContainer : 'tablesorter-resizable-container',
4105
+ resizableHandle : 'tablesorter-resizable-handle',
4106
+ resizableNoSelect : 'tablesorter-disableSelection',
4107
+ resizableStorage : 'tablesorter-resizable'
4108
+ });
4109
+
4110
+ // Add extra scroller css
4111
+ $(function(){
4112
+ var s = '<style>' +
4113
+ 'body.' + ts.css.resizableNoSelect + ' { -ms-user-select: none; -moz-user-select: -moz-none;' +
4114
+ '-khtml-user-select: none; -webkit-user-select: none; user-select: none; }' +
4115
+ '.' + ts.css.resizableContainer + ' { position: relative; height: 1px; }' +
4116
+ // make handle z-index > than stickyHeader z-index, so the handle stays above sticky header
4117
+ '.' + ts.css.resizableHandle + ' { position: absolute; display: inline-block; width: 8px; top: 1px;' +
4118
+ 'cursor: ew-resize; z-index: 3; user-select: none; -moz-user-select: none; }' +
4119
+ '</style>';
4120
+ $(s).appendTo('body');
4121
+ });
4122
+
4123
+ ts.resizable = {
4124
+ init : function( c, wo ) {
4125
+ if ( c.$table.hasClass( 'hasResizable' ) ) { return; }
4126
+ c.$table.addClass( 'hasResizable' );
4127
+ ts.resizableReset( c.table, true ); // set default widths
4128
+
4129
+ // internal variables
4130
+ wo.resizable_ = {
4131
+ $wrap : c.$table.parent(),
4132
+ mouseXPosition : 0,
4133
+ $target : null,
4134
+ $next : null,
4135
+ overflow : c.$table.parent().css('overflow') === 'auto',
4136
+ fullWidth : Math.abs(c.$table.parent().width() - c.$table.width()) < 20,
4137
+ storedSizes : []
4138
+ };
4139
+
4140
+ var noResize, $header, column, storedSizes,
4141
+ marginTop = parseInt( c.$table.css( 'margin-top' ), 10 );
4142
+
4143
+ wo.resizable_.storedSizes = storedSizes = ( ( ts.storage && wo.resizable !== false ) ?
4144
+ ts.storage( c.table, ts.css.resizableStorage ) :
4145
+ [] ) || [];
4146
+ ts.resizable.setWidths( c, wo, storedSizes );
4147
+
4148
+ wo.$resizable_container = $( '<div class="' + ts.css.resizableContainer + '">' )
4149
+ .css({ top : marginTop })
4150
+ .insertBefore( c.$table );
4151
+ // add container
4152
+ for ( column = 0; column < c.columns; column++ ) {
4153
+ $header = c.$headerIndexed[ column ];
4154
+ noResize = ts.getData( $header, ts.getColumnData( c.table, c.headers, column ), 'resizable' ) === 'false';
4155
+ if ( !noResize ) {
4156
+ $( '<div class="' + ts.css.resizableHandle + '">' )
4157
+ .appendTo( wo.$resizable_container )
4158
+ .attr({
4159
+ 'data-column' : column,
4160
+ 'unselectable' : 'on'
4161
+ })
4162
+ .data( 'header', $header )
4163
+ .bind( 'selectstart', false );
4164
+ }
4165
+ }
4166
+ c.$table.one('tablesorter-initialized', function() {
4167
+ ts.resizable.setHandlePosition( c, wo );
4168
+ ts.resizable.bindings( this.config, this.config.widgetOptions );
4169
+ });
4170
+ },
4171
+
4172
+ setWidth : function( $el, width ) {
4173
+ $el.css({
4174
+ 'width' : width,
4175
+ 'min-width' : '',
4176
+ 'max-width' : ''
4177
+ });
4178
+ },
4179
+
4180
+ setWidths : function( c, wo, storedSizes ) {
4181
+ var column,
4182
+ $extra = $( c.namespace + '_extra_headers' ),
4183
+ $col = c.$table.children( 'colgroup' ).children( 'col' );
4184
+ storedSizes = storedSizes || wo.resizable_.storedSizes || [];
4185
+ // process only if table ID or url match
4186
+ if ( storedSizes.length ) {
4187
+ for ( column = 0; column < c.columns; column++ ) {
4188
+ // set saved resizable widths
4189
+ c.$headers.eq( column ).width( storedSizes[ column ] );
4190
+ if ( $extra.length ) {
4191
+ // stickyHeaders needs to modify min & max width as well
4192
+ ts.resizable.setWidth( $extra.eq( column ).add( $col.eq( column ) ), storedSizes[ column ] );
4193
+ }
4194
+ }
4195
+ if ( $( c.namespace + '_extra_table' ).length && !ts.hasWidget( c.table, 'scroller' ) ) {
4196
+ ts.resizable.setWidth( $( c.namespace + '_extra_table' ), c.$table.outerWidth() );
4197
+ }
4198
+ }
4199
+ },
4200
+
4201
+ setHandlePosition : function( c, wo ) {
4202
+ var tableWidth = c.$table.outerWidth(),
4203
+ hasScroller = ts.hasWidget( c.table, 'scroller' ),
4204
+ tableHeight = c.$table.height(),
4205
+ $handles = wo.$resizable_container.children(),
4206
+ handleCenter = Math.floor( $handles.width() / 2 - parseFloat( c.$headers.css( 'border-right-width' ) ) * 2 );
4207
+
4208
+ if ( hasScroller ) {
4209
+ tableHeight = 0;
4210
+ c.$table.closest( '.' + ts.css.scrollerWrap ).children().each(function(){
4211
+ var $this = $(this);
4212
+ // center table has a max-height set
4213
+ tableHeight += $this.filter('[style*="height"]').length ? $this.height() : $this.children('table').height();
4214
+ });
4215
+ }
4216
+ $handles.each( function() {
4217
+ var $this = $(this),
4218
+ column = parseInt( $this.attr( 'data-column' ), 10 ),
4219
+ columns = c.columns - 1,
4220
+ $header = $this.data( 'header' );
4221
+ if ( column < columns || column === columns && wo.resizable_addLastColumn ) {
4222
+ $this.css({
4223
+ height : tableHeight,
4224
+ left : $header.position().left + $header.width() - handleCenter
4225
+ });
4226
+ }
4227
+ });
4228
+ },
4229
+
4230
+ // prevent text selection while dragging resize bar
4231
+ toggleTextSelection : function( c, toggle ) {
4232
+ var namespace = c.namespace + 'tsresize';
4233
+ c.widgetOptions.resizable_.disabled = toggle;
4234
+ $( 'body' ).toggleClass( ts.css.resizableNoSelect, toggle );
4235
+ if ( toggle ) {
4236
+ $( 'body' )
4237
+ .attr( 'unselectable', 'on' )
4238
+ .bind( 'selectstart' + namespace, false );
4239
+ } else {
4240
+ $( 'body' )
4241
+ .removeAttr( 'unselectable' )
4242
+ .unbind( 'selectstart' + namespace );
4243
+ }
4244
+ },
4245
+
4246
+ bindings : function( c, wo ) {
4247
+ var namespace = c.namespace + 'tsresize';
4248
+ wo.$resizable_container.children().bind( 'mousedown', function( event ) {
4249
+ // save header cell and mouse position
4250
+ var column,
4251
+ vars = wo.resizable_,
4252
+ $extras = $( c.namespace + '_extra_headers' ),
4253
+ $header = $( event.target ).data( 'header' );
4254
+
4255
+ column = parseInt( $header.attr( 'data-column' ), 10 );
4256
+ vars.$target = $header = $header.add( $extras.filter('[data-column="' + column + '"]') );
4257
+ vars.target = column;
4258
+
4259
+ // if table is not as wide as it's parent, then resize the table
4260
+ vars.$next = event.shiftKey || wo.resizable_targetLast ?
4261
+ $header.parent().children().not( '.resizable-false' ).filter( ':last' ) :
4262
+ $header.nextAll( ':not(.resizable-false)' ).eq( 0 );
4263
+
4264
+ column = parseInt( vars.$next.attr( 'data-column' ), 10 );
4265
+ vars.$next = vars.$next.add( $extras.filter('[data-column="' + column + '"]') );
4266
+ vars.next = column;
4267
+
4268
+ vars.mouseXPosition = event.pageX;
4269
+ vars.storedSizes = c.$headers.map(function(){ return $(this).width(); }).get();
4270
+ ts.resizable.toggleTextSelection( c, true );
4271
+ });
4272
+
4273
+ $( document )
4274
+ .bind( 'mousemove' + namespace, function( event ) {
4275
+ var vars = wo.resizable_;
4276
+ // ignore mousemove if no mousedown
4277
+ if ( !vars.disabled || vars.mouseXPosition === 0 || !vars.$target ) { return; }
4278
+ if ( wo.resizable_throttle ) {
4279
+ clearTimeout( vars.timer );
4280
+ vars.timer = setTimeout( function() {
4281
+ ts.resizable.mouseMove( c, wo, event );
4282
+ }, isNaN( wo.resizable_throttle ) ? 5 : wo.resizable_throttle );
4283
+ } else {
4284
+ ts.resizable.mouseMove( c, wo, event );
4285
+ }
4286
+ })
4287
+ .bind( 'mouseup' + namespace, function() {
4288
+ if (!wo.resizable_.disabled) { return; }
4289
+ ts.resizable.toggleTextSelection( c, false );
4290
+ ts.resizable.stopResize( c, wo );
4291
+ ts.resizable.setHandlePosition( c, wo );
4292
+ });
4293
+
4294
+ // resizeEnd event triggered by scroller widget
4295
+ $( window ).bind( 'resize' + namespace + ' resizeEnd' + namespace, function() {
4296
+ ts.resizable.setHandlePosition( c, wo );
4297
+ });
4298
+
4299
+ // right click to reset columns to default widths
4300
+ c.$table.find( 'thead:first' ).add( $( c.namespace + '_extra_table' ).find( 'thead:first' ) )
4301
+ .bind( 'contextmenu' + namespace, function() {
4302
+ // $.isEmptyObject() needs jQuery 1.4+; allow right click if already reset
4303
+ var allowClick = wo.resizable_.storedSizes.length === 0;
4304
+ ts.resizableReset( c.table );
4305
+ ts.resizable.setHandlePosition( c, wo );
4306
+ wo.resizable_.storedSizes = [];
4307
+ return allowClick;
4308
+ });
4309
+
4310
+ },
4311
+
4312
+ mouseMove : function( c, wo, event ) {
4313
+ if ( wo.resizable_.mouseXPosition === 0 || !wo.resizable_.$target ) { return; }
4314
+ // resize columns
4315
+ var vars = wo.resizable_,
4316
+ $target = vars.$target,
4317
+ $next = vars.$next,
4318
+ leftEdge = event.pageX - vars.mouseXPosition,
4319
+ targetWidth = $target.width();
4320
+ if ( vars.fullWidth ) {
4321
+ vars.storedSizes[ vars.target ] += leftEdge;
4322
+ vars.storedSizes[ vars.next ] -= leftEdge;
4323
+ ts.resizable.setWidths( c, wo );
4324
+
4325
+ } else if ( vars.overflow ) {
4326
+ c.$table.add( $( c.namespace + '_extra_table' ) ).width(function(i, w){
4327
+ return w + leftEdge;
4328
+ });
4329
+ if ( !$next.length ) {
4330
+ // if expanding right-most column, scroll the wrapper
4331
+ vars.$wrap[0].scrollLeft = c.$table.width();
4332
+ }
4333
+ } else {
4334
+ vars.storedSizes[ vars.target ] += leftEdge;
4335
+ ts.resizable.setWidths( c, wo );
4336
+ }
4337
+ vars.mouseXPosition = event.pageX;
4338
+ },
4339
+
4340
+ stopResize : function( c, wo ) {
4341
+ var vars = wo.resizable_;
4342
+ vars.storedSizes = [];
4343
+ if ( ts.storage ) {
4344
+ vars.storedSizes = c.$headers.map(function(){ return $(this).width(); }).get();
4345
+ if ( wo.resizable !== false ) {
4346
+ // save all column widths
4347
+ ts.storage( c.table, ts.css.resizableStorage, vars.storedSizes );
4348
+ }
4349
+ }
4350
+ vars.mouseXPosition = 0;
4351
+ vars.$target = vars.$next = null;
4352
+ $(window).trigger('resize'); // will update stickyHeaders, just in case
4353
+ }
4354
+ };
4355
+
4356
+ // this widget saves the column widths if
4357
+ // $.tablesorter.storage function is included
4358
+ // **************************
4359
+ ts.addWidget({
4360
+ id: "resizable",
4361
+ priority: 40,
4362
+ options: {
4363
+ resizable : true,
4364
+ resizable_addLastColumn : false,
4365
+ resizable_widths : [],
4366
+ resizable_throttle : false, // set to true (5ms) or any number 0-10 range
4367
+ resizable_targetLast : false
4368
+ },
4369
+ init: function(table, thisWidget, c, wo) {
4370
+ ts.resizable.init( c, wo );
4371
+ },
4372
+ remove: function( table, c, wo ) {
4373
+ if (wo.$resizable_container) {
4374
+ var namespace = c.namespace + 'tsresize';
4375
+ c.$table.add( $( c.namespace + '_extra_table' ) )
4376
+ .removeClass('hasResizable')
4377
+ .children( 'thead' ).unbind( 'contextmenu' + namespace );
4378
+
4379
+ wo.$resizable_container.remove();
4380
+ ts.resizable.toggleTextSelection( c, false );
4381
+ ts.resizableReset( table );
4382
+ $( document ).unbind( 'mousemove' + namespace + ' mouseup' + namespace );
4383
+ }
4384
+ }
4385
+ });
4386
+
4387
+ ts.resizableReset = function( table, nosave ) {
4388
+ $( table ).each(function(){
4389
+ var $t,
4390
+ c = this.config,
4391
+ wo = c && c.widgetOptions;
4392
+ if ( table && c ) {
4393
+ c.$headers.each( function( i ) {
4394
+ $t = $(this);
4395
+ if ( wo.resizable_widths && wo.resizable_widths[ i ] ) {
4396
+ $t.css( 'width', wo.resizable_widths[ i ] );
4397
+ } else if ( !$t.hasClass( 'resizable-false' ) ) {
4398
+ // don't clear the width of any column that is not resizable
4399
+ $t.css( 'width', '' );
4400
+ }
4401
+ });
4402
+ // reset stickyHeader widths
4403
+ $( window ).trigger( 'resize' );
4404
+ if ( ts.storage && !nosave ) {
4405
+ ts.storage( this, ts.css.resizableStorage, {} );
4406
+ }
4407
+ }
4408
+ });
4409
+ };
4410
+
4411
+ })( jQuery, window );
4412
+
4413
+ /*! Widget: saveSort */
4414
+ ;(function ($) {
4415
+ 'use strict';
4416
+ var ts = $.tablesorter = $.tablesorter || {};
4417
+
4418
+ // this widget saves the last sort only if the
4419
+ // saveSort widget option is true AND the
4420
+ // $.tablesorter.storage function is included
4421
+ // **************************
4422
+ ts.addWidget({
4423
+ id: 'saveSort',
4424
+ priority: 20,
4425
+ options: {
4426
+ saveSort : true
4427
+ },
4428
+ init: function(table, thisWidget, c, wo) {
4429
+ // run widget format before all other widgets are applied to the table
4430
+ thisWidget.format(table, c, wo, true);
4431
+ },
4432
+ format: function(table, c, wo, init) {
4433
+ var stored, time,
4434
+ $table = c.$table,
4435
+ saveSort = wo.saveSort !== false, // make saveSort active/inactive; default to true
4436
+ sortList = { "sortList" : c.sortList };
4437
+ if (c.debug) {
4438
+ time = new Date();
4439
+ }
4440
+ if ($table.hasClass('hasSaveSort')) {
4441
+ if (saveSort && table.hasInitialized && ts.storage) {
4442
+ ts.storage( table, 'tablesorter-savesort', sortList );
4443
+ if (c.debug) {
4444
+ ts.benchmark('saveSort widget: Saving last sort: ' + c.sortList, time);
4445
+ }
4446
+ }
4447
+ } else {
4448
+ // set table sort on initial run of the widget
4449
+ $table.addClass('hasSaveSort');
4450
+ sortList = '';
4451
+ // get data
4452
+ if (ts.storage) {
4453
+ stored = ts.storage( table, 'tablesorter-savesort' );
4454
+ sortList = (stored && stored.hasOwnProperty('sortList') && $.isArray(stored.sortList)) ? stored.sortList : '';
4455
+ if (c.debug) {
4456
+ ts.benchmark('saveSort: Last sort loaded: "' + sortList + '"', time);
4457
+ }
4458
+ $table.bind('saveSortReset', function(event) {
4459
+ event.stopPropagation();
4460
+ ts.storage( table, 'tablesorter-savesort', '' );
4461
+ });
4462
+ }
4463
+ // init is true when widget init is run, this will run this widget before all other widgets have initialized
4464
+ // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice.
4465
+ if (init && sortList && sortList.length > 0) {
4466
+ c.sortList = sortList;
4467
+ } else if (table.hasInitialized && sortList && sortList.length > 0) {
4468
+ // update sort change
4469
+ $table.trigger('sorton', [sortList]);
4470
+ }
4471
+ }
4472
+ },
4473
+ remove: function(table, c) {
4474
+ c.$table.removeClass('hasSaveSort');
4475
+ // clear storage
4476
+ if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); }
4477
+ }
4478
+ });
4479
+
4480
+ })(jQuery);
4481
+
4482
+ return $.tablesorter;
4483
+ }));