jquery-tablesorter 1.4.1 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- /*! tableSorter 2.4+ widgets - updated 1/29/2013
1
+ /*! tableSorter 2.8+ widgets - updated 6/4/2013
2
2
  *
3
3
  * Column Styles
4
4
  * Column Filters
@@ -6,15 +6,15 @@
6
6
  * Sticky Header
7
7
  * UI Theme (generalized)
8
8
  * Save Sort
9
- * ["zebra", "uitheme", "stickyHeaders", "filter", "columns"]
9
+ * [ "columns", "filter", "resizable", "stickyHeaders", "uitheme", "saveSort" ]
10
10
  */
11
11
  /*jshint browser:true, jquery:true, unused:false, loopfunc:true */
12
12
  /*global jQuery: false, localStorage: false, navigator: false */
13
13
  ;(function($){
14
14
  "use strict";
15
- $.tablesorter = $.tablesorter || {};
15
+ var ts = $.tablesorter = $.tablesorter || {};
16
16
 
17
- $.tablesorter.themes = {
17
+ ts.themes = {
18
18
  "bootstrap" : {
19
19
  table : 'table table-bordered table-striped',
20
20
  header : 'bootstrap-header', // give the header a gradient background
@@ -66,19 +66,26 @@ $.tablesorter.themes = {
66
66
  val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : '';
67
67
  alert(val); // "data1" if saved, or "" if not
68
68
  */
69
- $.tablesorter.storage = function(table, key, val){
69
+ ts.storage = function(table, key, val){
70
70
  var d, k, ls = false, v = {},
71
71
  id = table.id || $('.tablesorter').index( $(table) ),
72
72
  url = window.location.pathname;
73
- try { ls = !!(localStorage.getItem); } catch(e) {}
73
+ // https://gist.github.com/paulirish/5558557
74
+ if ("localStorage" in window) {
75
+ try {
76
+ window.localStorage.setItem('_tmptest', 'temp');
77
+ ls = true;
78
+ window.localStorage.removeItem('_tmptest');
79
+ } catch(e) {}
80
+ }
74
81
  // *** get val ***
75
82
  if ($.parseJSON){
76
83
  if (ls){
77
- v = $.parseJSON(localStorage[key]) || {};
84
+ v = $.parseJSON(localStorage[key] || '{}');
78
85
  } else {
79
86
  k = document.cookie.split(/[;\s|=]/); // cookie
80
87
  d = $.inArray(key, k) + 1; // add one to get from the key to the value
81
- v = (d !== 0) ? $.parseJSON(k[d]) || {} : {};
88
+ v = (d !== 0) ? $.parseJSON(k[d] || '{}') : {};
82
89
  }
83
90
  }
84
91
  // allow val to be an empty string to
@@ -101,20 +108,61 @@ $.tablesorter.storage = function(table, key, val){
101
108
  }
102
109
  };
103
110
 
111
+ // Add a resize event to table headers
112
+ // **************************
113
+ ts.addHeaderResizeEvent = function(table, disable, options){
114
+ var defaults = {
115
+ timer : 250
116
+ },
117
+ o = $.extend({}, defaults, options),
118
+ c = table.config,
119
+ wo = c.widgetOptions,
120
+ headers,
121
+ checkSizes = function(){
122
+ wo.resize_flag = true;
123
+ headers = [];
124
+ c.$headers.each(function(){
125
+ var d = $.data(this, 'savedSizes'),
126
+ w = this.offsetWidth,
127
+ h = this.offsetHeight;
128
+ if (w !== d[0] || h !== d[1]) {
129
+ $.data(this, 'savedSizes', [ w, h ]);
130
+ headers.push(this);
131
+ }
132
+ });
133
+ if (headers.length) { c.$table.trigger('resize', [ headers ]); }
134
+ wo.resize_flag = false;
135
+ };
136
+ clearInterval(wo.resize_timer);
137
+ if (disable) {
138
+ wo.resize_flag = false;
139
+ return false;
140
+ }
141
+ c.$headers.each(function(){
142
+ $.data(this, 'savedSizes', [ this.offsetWidth, this.offsetHeight ]);
143
+ });
144
+ wo.resize_timer = setInterval(function(){
145
+ if (wo.resize_flag) { return; }
146
+ checkSizes();
147
+ }, o.timer);
148
+ };
149
+
104
150
  // Widget: General UI theme
105
151
  // "uitheme" option in "widgetOptions"
106
152
  // **************************
107
- $.tablesorter.addWidget({
153
+ ts.addWidget({
108
154
  id: "uitheme",
109
- format: function(table){
155
+ priority: 10,
156
+ options: {
157
+ uitheme : 'jui'
158
+ },
159
+ format: function(table, c, wo){
110
160
  var time, klass, $el, $tar,
111
- t = $.tablesorter.themes,
112
- $t = $(table),
113
- c = table.config,
114
- wo = c.widgetOptions,
115
- theme = c.theme !== 'default' ? c.theme : wo.uitheme || 'jui', // default uitheme is 'jui'
161
+ t = ts.themes,
162
+ $t = c.$table,
163
+ theme = c.theme !== 'default' ? c.theme : wo.uitheme || 'jui',
116
164
  o = t[ t[theme] ? theme : t[wo.uitheme] ? wo.uitheme : 'jui'],
117
- $h = $(c.headerList),
165
+ $h = c.$headers,
118
166
  sh = 'tr.' + (wo.stickyHeaders || 'tablesorter-stickyHeader'),
119
167
  rmv = o.sortNone + ' ' + o.sortDesc + ' ' + o.sortAsc;
120
168
  if (c.debug) { time = new Date(); }
@@ -137,10 +185,9 @@ $.tablesorter.addWidget({
137
185
  $h
138
186
  .addClass(o.header)
139
187
  .filter(':not(.sorter-false)')
140
- .hover(function(){
141
- $(this).addClass(o.hover);
142
- }, function(){
143
- $(this).removeClass(o.hover);
188
+ .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(e){
189
+ // toggleClass with switch added in jQuery 1.3
190
+ $(this)[ e.type === 'mouseenter' ? 'addClass' : 'removeClass' ](o.hover);
144
191
  });
145
192
  if (!$h.find('.tablesorter-wrapper').length) {
146
193
  // Firefox needs this inner div to position the resizer correctly
@@ -169,20 +216,20 @@ $.tablesorter.addWidget({
169
216
  }
170
217
  });
171
218
  if (c.debug){
172
- $.tablesorter.benchmark("Applying " + theme + " theme", time);
219
+ ts.benchmark("Applying " + theme + " theme", time);
173
220
  }
174
221
  },
175
222
  remove: function(table, c, wo){
176
- var $t = $(table),
223
+ var $t = c.$table,
177
224
  theme = typeof wo.uitheme === 'object' ? 'jui' : wo.uitheme || 'jui',
178
- o = typeof wo.uitheme === 'object' ? wo.uitheme : $.tablesorter.themes[ $.tablesorter.themes.hasOwnProperty(theme) ? theme : 'jui'],
225
+ o = typeof wo.uitheme === 'object' ? wo.uitheme : ts.themes[ ts.themes.hasOwnProperty(theme) ? theme : 'jui'],
179
226
  $h = $t.children('thead').children(),
180
227
  rmv = o.sortNone + ' ' + o.sortDesc + ' ' + o.sortAsc;
181
228
  $t
182
229
  .removeClass('tablesorter-' + theme + ' ' + o.table)
183
230
  .find(c.cssHeader).removeClass(o.header);
184
231
  $h
185
- .unbind('mouseenter mouseleave') // remove hover
232
+ .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover
186
233
  .removeClass(o.hover + ' ' + rmv + ' ' + o.active)
187
234
  .find('.tablesorter-filter-row').removeClass(o.filterRow);
188
235
  $h.find('.tablesorter-icon').removeClass(o.icons);
@@ -193,17 +240,18 @@ $.tablesorter.addWidget({
193
240
  // "columns", "columns_thead" (true) and
194
241
  // "columns_tfoot" (true) options in "widgetOptions"
195
242
  // **************************
196
- $.tablesorter.addWidget({
243
+ ts.addWidget({
197
244
  id: "columns",
198
- format: function(table){
245
+ priority: 30,
246
+ options : {
247
+ columns : [ "primary", "secondary", "tertiary" ]
248
+ },
249
+ format: function(table, c, wo){
199
250
  var $tb, $tr, $td, $t, time, last, rmv, i, k, l,
200
- $tbl = $(table),
201
- c = table.config,
202
- wo = c.widgetOptions,
251
+ $tbl = c.$table,
203
252
  b = c.$tbodies,
204
253
  list = c.sortList,
205
254
  len = list.length,
206
- css = [ "primary", "secondary", "tertiary" ]; // default options
207
255
  // keep backwards compatibility, for now
208
256
  css = (c.widgetColumns && c.widgetColumns.hasOwnProperty('css')) ? c.widgetColumns.css || css :
209
257
  (wo && wo.hasOwnProperty('columns')) ? wo.columns || css : css;
@@ -214,7 +262,7 @@ $.tablesorter.addWidget({
214
262
  }
215
263
  // check if there is a sort (on initialization there may not be one)
216
264
  for (k = 0; k < b.length; k++ ){
217
- $tb = $.tablesorter.processTbody(table, b.eq(k), true); // detach tbody
265
+ $tb = ts.processTbody(table, b.eq(k), true); // detach tbody
218
266
  $tr = $tb.children('tr');
219
267
  l = $tr.length;
220
268
  // loop through the visible rows
@@ -236,7 +284,7 @@ $.tablesorter.addWidget({
236
284
  }
237
285
  }
238
286
  });
239
- $.tablesorter.processTbody(table, $tb, false);
287
+ ts.processTbody(table, $tb, false);
240
288
  }
241
289
  // add classes to thead and tfoot
242
290
  $tr = wo.columns_thead !== false ? 'thead tr' : '';
@@ -257,83 +305,82 @@ $.tablesorter.addWidget({
257
305
  }
258
306
  }
259
307
  if (c.debug){
260
- $.tablesorter.benchmark("Applying Columns widget", time);
308
+ ts.benchmark("Applying Columns widget", time);
261
309
  }
262
310
  },
263
311
  remove: function(table, c, wo){
264
312
  var k, $tb,
265
313
  b = c.$tbodies,
266
- rmv = (c.widgetOptions.columns || [ "primary", "secondary", "tertiary" ]).join(' ');
314
+ rmv = (wo.columns || [ "primary", "secondary", "tertiary" ]).join(' ');
267
315
  c.$headers.removeClass(rmv);
268
- $(table).children('tfoot').children('tr').children('th, td').removeClass(rmv);
316
+ c.$table.children('tfoot').children('tr').children('th, td').removeClass(rmv);
269
317
  for (k = 0; k < b.length; k++ ){
270
- $tb = $.tablesorter.processTbody(table, b.eq(k), true); // remove tbody
318
+ $tb = ts.processTbody(table, b.eq(k), true); // remove tbody
271
319
  $tb.children('tr').each(function(){
272
320
  $(this).children().removeClass(rmv);
273
321
  });
274
- $.tablesorter.processTbody(table, $tb, false); // restore tbody
322
+ ts.processTbody(table, $tb, false); // restore tbody
275
323
  }
276
324
  }
277
325
  });
278
326
 
279
- /* Widget: filter
280
- widgetOptions:
281
- filter_childRows : false // if true, filter includes child row content in the search
282
- filter_columnFilters : true // if true, a filter will be added to the top of each table column
283
- filter_cssFilter : 'tablesorter-filter' // css class name added to the filter row & each input in the row
284
- filter_functions : null // add custom filter functions using this option
285
- filter_hideFilters : false // collapse filter row when mouse leaves the area
286
- filter_ignoreCase : true // if true, make all searches case-insensitive
287
- filter_reset : null // jQuery selector string of an element used to reset the filters
288
- filter_searchDelay : 300 // typing delay in milliseconds before starting a search
289
- filter_startsWith : false // if true, filter start from the beginning of the cell contents
290
- filter_useParsedData : false // filter all data using parsed content
291
- 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.
292
- **************************/
293
- $.tablesorter.addWidget({
327
+ // Widget: filter
328
+ // **************************
329
+ ts.addWidget({
294
330
  id: "filter",
295
- format: function(table){
296
- if (table.config.parsers && !$(table).hasClass('hasFilters')){
331
+ priority: 50,
332
+ options : {
333
+ filter_childRows : false, // if true, filter includes child row content in the search
334
+ filter_columnFilters : true, // if true, a filter will be added to the top of each table column
335
+ filter_cssFilter : 'tablesorter-filter', // css class name added to the filter row & each input in the row
336
+ filter_filteredRow : 'filtered', // class added to filtered rows; needed by pager plugin
337
+ filter_formatter : null, // add custom filter elements to the filter row
338
+ filter_functions : null, // add custom filter functions using this option
339
+ filter_hideFilters : false, // collapse filter row when mouse leaves the area
340
+ filter_ignoreCase : true, // if true, make all searches case-insensitive
341
+ filter_liveSearch : true, // if true, search column content while the user types (with a delay)
342
+ filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available (visible) options within the drop down
343
+ filter_reset : null, // jQuery selector string of an element used to reset the filters
344
+ filter_searchDelay : 300, // typing delay in milliseconds before starting a search
345
+ filter_startsWith : false, // if true, filter start from the beginning of the cell contents
346
+ filter_useParsedData : false, // filter all data using parsed content
347
+ 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.
348
+ filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value
349
+
350
+ // regex used in filter "check" functions - not for general use and not documented
351
+ filter_regex : {
352
+ "regex" : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex
353
+ "child" : /tablesorter-childRow/, // child row class name; this gets updated in the script
354
+ "filtered" : /filtered/, // filtered (hidden) row class name; updated in the script
355
+ "type" : /undefined|number/, // check type
356
+ "exact" : /(^[\"|\'|=])|([\"|\'|=]$)/g, // exact match
357
+ "nondigit" : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser)
358
+ "operators" : /[<>=]/g // replace operators
359
+ }
360
+ },
361
+ format: function(table, c, wo){
362
+ if (c.parsers && !c.$table.hasClass('hasFilters')){
297
363
  var i, j, k, l, val, ff, x, xi, st, sel, str,
298
364
  ft, ft2, $th, rg, s, t, dis, col,
365
+ fmt = ts.formatFloat,
299
366
  last = '', // save last filter search
300
- ts = $.tablesorter,
301
- c = table.config,
302
- $ths = $(c.headerList),
303
- wo = c.widgetOptions,
304
- css = wo.filter_cssFilter || 'tablesorter-filter',
305
- $t = $(table).addClass('hasFilters'),
306
- b = c.$tbodies,
367
+ $ths = c.$headers,
368
+ css = wo.filter_cssFilter,
369
+ $t = c.$table.addClass('hasFilters'),
370
+ b = $t.find('tbody'),
307
371
  cols = c.parsers.length,
308
- reg = [ // regex used in filter "check" functions
309
- /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // 0 = regex to test for regex
310
- new RegExp(c.cssChildRow), // 1 = child row
311
- /undefined|number/, // 2 = check type
312
- /(^[\"|\'|=])|([\"|\'|=]$)/, // 3 = exact match
313
- /[\"\'=]/g, // 4 = replace exact match flags
314
- /[^\w,. \-()]/g, // 5 = replace non-digits (from digit & currency parser)
315
- /[<>=]/g // 6 = replace operators
316
- ],
317
- parsed = $ths.map(function(i){
318
- return (ts.getData) ? ts.getData($ths.filter('[data-column="' + i + '"]:last'), c.headers[i], 'filter') === 'parsed' : $(this).hasClass('filter-parsed');
319
- }).get(),
320
- time, timer,
372
+ parsed, time, timer,
321
373
 
322
374
  // dig fer gold
323
375
  checkFilters = function(filter){
324
376
  var arry = $.isArray(filter),
325
- $inpts = $t.find('thead').eq(0).children('tr').find('select.' + css + ', input.' + css),
326
- v = (arry) ? filter : $inpts.map(function(){
327
- return $(this).val() || '';
328
- }).get(),
377
+ v = (arry) ? filter : ts.getFilters(table),
329
378
  cv = (v || []).join(''); // combined filter values
330
379
  // add filter array back into inputs
331
380
  if (arry) {
332
- $inpts.each(function(i,el){
333
- $(el).val(filter[i] || '');
334
- });
381
+ ts.setFilters( $t, v );
335
382
  }
336
- if (wo.filter_hideFilters === true){
383
+ if (wo.filter_hideFilters){
337
384
  // show/hide filter row as needed
338
385
  $t.find('.tablesorter-filter-row').trigger( cv === '' ? 'mouseleave' : 'mouseenter' );
339
386
  }
@@ -353,27 +400,36 @@ $.tablesorter.addWidget({
353
400
  }
354
401
  },
355
402
  findRows = function(filter, v, cv){
356
- var $tb, $tr, $td, cr, r, l, ff, time, arry;
403
+ var $tb, $tr, $td, cr, r, l, ff, time, r1, r2, searchFiltered;
357
404
  if (c.debug) { time = new Date(); }
358
-
359
405
  for (k = 0; k < b.length; k++ ){
360
- $tb = $.tablesorter.processTbody(table, b.eq(k), true);
361
- $tr = $tb.children('tr');
406
+ if (b.eq(k).hasClass(c.cssInfoBlock)) { continue; } // ignore info blocks, issue #264
407
+ $tb = ts.processTbody(table, b.eq(k), true);
408
+ $tr = $tb.children('tr:not(.' + c.cssChildRow + ')');
362
409
  l = $tr.length;
363
410
  if (cv === '' || wo.filter_serversideFiltering){
364
- $tr.show().removeClass('filtered');
411
+ $tb.children().show().removeClass(wo.filter_filteredRow);
365
412
  } else {
413
+ // optimize searching only through already filtered rows - see #313
414
+ searchFiltered = true;
415
+ r = $t.data('lastSearch') || [];
416
+ $.each(v, function(i,val){
417
+ // check for changes from beginning of filter; but ignore if there is a logical "or" in the string
418
+ searchFiltered = (val || '').indexOf(r[i] || '') === 0 && searchFiltered && !/(\s+or\s+|\|)/g.test(val || '');
419
+ });
420
+ // can't search when all rows are hidden - this happens when looking for exact matches
421
+ if (searchFiltered && $tr.filter(':visible').length === 0) { searchFiltered = false; }
366
422
  // loop through the rows
367
423
  for (j = 0; j < l; j++){
368
- // skip child rows
369
- if (reg[1].test($tr[j].className)) { continue; }
424
+ r = $tr[j].className;
425
+ // skip child rows & already filtered rows
426
+ if ( wo.filter_regex.child.test(r) || (searchFiltered && wo.filter_regex.filtered.test(r)) ) { continue; }
370
427
  r = true;
371
428
  cr = $tr.eq(j).nextUntil('tr:not(.' + c.cssChildRow + ')');
372
429
  // so, if "table.config.widgetOptions.filter_childRows" is true and there is
373
430
  // a match anywhere in the child row, then it will make the row visible
374
431
  // checked here so the option can be changed dynamically
375
- t = (cr.length && (wo && wo.hasOwnProperty('filter_childRows') &&
376
- typeof wo.filter_childRows !== 'undefined' ? wo.filter_childRows : true)) ? cr.text() : '';
432
+ t = (cr.length && wo.filter_childRows) ? cr.text() : '';
377
433
  t = wo.filter_ignoreCase ? t.toLocaleLowerCase() : t;
378
434
  $td = $tr.eq(j).children('td');
379
435
  for (i = 0; i < cols; i++){
@@ -386,7 +442,7 @@ $.tablesorter.addWidget({
386
442
  // using older or original tablesorter
387
443
  x = $.trim($td.eq(i).text());
388
444
  }
389
- xi = !reg[2].test(typeof x) && wo.filter_ignoreCase ? x.toLocaleLowerCase() : x;
445
+ xi = !wo.filter_regex.type.test(typeof x) && wo.filter_ignoreCase ? x.toLocaleLowerCase() : x;
390
446
  ff = r; // if r is true, show that row
391
447
  // val = case insensitive, v[i] = case sensitive
392
448
  val = wo.filter_ignoreCase ? v[i].toLocaleLowerCase() : v[i];
@@ -402,15 +458,16 @@ $.tablesorter.addWidget({
402
458
  ff = wo.filter_functions[i][v[i]](x, c.cache[k].normalized[j][i], v[i], i);
403
459
  }
404
460
  // Look for regex
405
- } else if (reg[0].test(val)){
406
- rg = reg[0].exec(val);
461
+ } else if (wo.filter_regex.regex.test(val)){
462
+ rg = wo.filter_regex.regex.exec(val);
407
463
  try {
408
464
  ff = new RegExp(rg[1], rg[2]).test(xi);
409
465
  } catch (err){
410
466
  ff = false;
411
467
  }
412
- // Look for quotes or equals to get an exact match
413
- } else if (reg[3].test(val) && xi === val.replace(reg[4], '')){
468
+ // Look for quotes or equals to get an exact match; ignore type since xi could be numeric
469
+ /*jshint eqeqeq:false */
470
+ } else if (val.replace(wo.filter_regex.exact, '') == xi){
414
471
  ff = true;
415
472
  // Look for a not match
416
473
  } else if (/^\!/.test(val)){
@@ -419,14 +476,52 @@ $.tablesorter.addWidget({
419
476
  ff = val === '' ? true : !(wo.filter_startsWith ? s === 0 : s >= 0);
420
477
  // Look for operators >, >=, < or <=
421
478
  } else if (/^[<>]=?/.test(val)){
422
- // xi may be numeric - see issue #149
423
- rg = isNaN(xi) ? $.tablesorter.formatFloat(xi.replace(reg[5], ''), table) : $.tablesorter.formatFloat(xi, table);
424
- s = $.tablesorter.formatFloat(val.replace(reg[5], '').replace(reg[6],''), table);
479
+ s = fmt(val.replace(wo.filter_regex.nondigit, '').replace(wo.filter_regex.operators,''), table);
480
+ // parse filter value in case we're comparing numbers (dates)
481
+ if (parsed[i] || c.parsers[i].type === 'numeric') {
482
+ rg = c.parsers[i].format('' + val.replace(wo.filter_regex.operators,''), table, $ths.eq(i), i);
483
+ s = (rg !== '' && !isNaN(rg)) ? rg : s;
484
+ }
485
+ // xi may be numeric - see issue #149;
486
+ // check if c.cache[k].normalized[j] is defined, because sometimes j goes out of range? (numeric columns)
487
+ rg = ( parsed[i] || c.parsers[i].type === 'numeric' ) && !isNaN(s) && c.cache[k].normalized[j] ? c.cache[k].normalized[j][i] :
488
+ isNaN(xi) ? fmt(xi.replace(wo.filter_regex.nondigit, ''), table) : fmt(xi, table);
425
489
  if (/>/.test(val)) { ff = />=/.test(val) ? rg >= s : rg > s; }
426
490
  if (/</.test(val)) { ff = /<=/.test(val) ? rg <= s : rg < s; }
427
- // Look for wild card: ? = single, or * = multiple
428
- } else if (/[\?|\*]/.test(val)){
429
- ff = new RegExp( val.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(xi);
491
+ if (s === '') { ff = true; } // keep showing all rows if nothing follows the operator
492
+ // Look for an AND or && operator (logical and)
493
+ } else if (/\s+(AND|&&)\s+/g.test(v[i])) {
494
+ s = val.split(/(?:\s+(?:and|&&)\s+)/g);
495
+ ff = xi.search($.trim(s[0])) >= 0;
496
+ r1 = s.length - 1;
497
+ while (ff && r1) {
498
+ ff = ff && xi.search($.trim(s[r1])) >= 0;
499
+ r1--;
500
+ }
501
+ // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu!
502
+ } else if (/\s+(-|to)\s+/.test(val)){
503
+ s = val.split(/(?: - | to )/); // make sure the dash is for a range and not indicating a negative number
504
+ r1 = fmt(s[0].replace(wo.filter_regex.nondigit, ''), table);
505
+ r2 = fmt(s[1].replace(wo.filter_regex.nondigit, ''), table);
506
+ // parse filter value in case we're comparing numbers (dates)
507
+ if (parsed[i] || c.parsers[i].type === 'numeric') {
508
+ rg = c.parsers[i].format('' + s[0], table, $ths.eq(i), i);
509
+ r1 = (rg !== '' && !isNaN(rg)) ? rg : r1;
510
+ rg = c.parsers[i].format('' + s[1], table, $ths.eq(i), i);
511
+ r2 = (rg !== '' && !isNaN(rg)) ? rg : r2;
512
+ }
513
+ rg = ( parsed[i] || c.parsers[i].type === 'numeric' ) && !isNaN(r1) && !isNaN(r2) ? c.cache[k].normalized[j][i] :
514
+ isNaN(xi) ? fmt(xi.replace(wo.filter_regex.nondigit, ''), table) : fmt(xi, table);
515
+ if (r1 > r2) { ff = r1; r1 = r2; r2 = ff; } // swap
516
+ ff = (rg >= r1 && rg <= r2) || (r1 === '' || r2 === '') ? true : false;
517
+ // Look for wild card: ? = single, * = multiple, or | = logical OR
518
+ } else if ( /[\?|\*]/.test(val) || /\s+OR\s+/.test(v[i]) ){
519
+ s = val.replace(/\s+OR\s+/gi,"|");
520
+ // look for an exact match with the "or" unless the "filter-match" class is found
521
+ if (!$ths.filter('[data-column="' + i + '"]:last').hasClass('filter-match') && /\|/.test(s)) {
522
+ s = '^(' + s + ')$';
523
+ }
524
+ ff = new RegExp( s.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(xi);
430
525
  // Look for match, and add child row data for matching
431
526
  } else {
432
527
  x = (xi + t).indexOf(val);
@@ -436,28 +531,32 @@ $.tablesorter.addWidget({
436
531
  }
437
532
  }
438
533
  $tr[j].style.display = (r ? '' : 'none');
439
- $tr.eq(j)[r ? 'removeClass' : 'addClass']('filtered');
534
+ $tr.eq(j)[r ? 'removeClass' : 'addClass'](wo.filter_filteredRow);
440
535
  if (cr.length) { cr[r ? 'show' : 'hide'](); }
441
536
  }
442
537
  }
443
- $.tablesorter.processTbody(table, $tb, false);
538
+ ts.processTbody(table, $tb, false);
444
539
  }
445
-
446
540
  last = cv; // save last search
541
+ $t.data('lastSearch', v);
447
542
  if (c.debug){
448
543
  ts.benchmark("Completed filter widget search", time);
449
544
  }
450
545
  $t.trigger('applyWidgets'); // make sure zebra widget is applied
451
546
  $t.trigger('filterEnd');
452
547
  },
453
- buildSelect = function(i, updating){
454
- var o, arry = [];
548
+ buildSelect = function(i, updating, onlyavail){
549
+ var o, t, arry = [], currentVal;
455
550
  i = parseInt(i, 10);
456
- o = '<option value="">' + ($ths.filter('[data-column="' + i + '"]:last').attr('data-placeholder') || '') + '</option>';
551
+ t = $ths.filter('[data-column="' + i + '"]:last');
552
+ // t.data('placeholder') won't work in jQuery older than 1.4.3
553
+ o = '<option value="">' + (t.data('placeholder') || t.attr('data-placeholder') || '') + '</option>';
457
554
  for (k = 0; k < b.length; k++ ){
458
555
  l = c.cache[k].row.length;
459
556
  // loop through the rows
460
557
  for (j = 0; j < l; j++){
558
+ // check if has class filtered
559
+ if (onlyavail && c.cache[k].row[j][0].className.match(wo.filter_filteredRow)) { continue; }
461
560
  // get non-normalized cell content
462
561
  if (wo.filter_useParsedData){
463
562
  arry.push( '' + c.cache[k].normalized[j][i] );
@@ -474,13 +573,18 @@ $.tablesorter.addWidget({
474
573
  // if $.tablesorter.sortText exists (not in the original tablesorter),
475
574
  // then natural sort the list otherwise use a basic sort
476
575
  arry = $.grep(arry, function(v, k){
477
- return $.inArray(v ,arry) === k;
576
+ return $.inArray(v, arry) === k;
478
577
  });
479
- arry = (ts.sortText) ? arry.sort(function(a,b){ return ts.sortText(table, a, b, i); }) : arry.sort(true);
578
+ arry = (ts.sortText) ? arry.sort(function(a, b){ return ts.sortText(table, a, b, i); }) : arry.sort(true);
579
+
580
+ // Get curent filter value
581
+ currentVal = $t.find('thead').find('select.' + css + '[data-column="' + i + '"]').val();
480
582
 
481
583
  // build option list
482
584
  for (k = 0; k < arry.length; k++){
483
- o += '<option value="' + arry[k] + '">' + arry[k] + '</option>';
585
+ t = arry[k].replace(/\"/g, "&quot;");
586
+ // replace quotes - fixes #242 & ignore empty strings - see http://stackoverflow.com/q/14990971/145346
587
+ o += arry[k] !== '' ? '<option value="' + t + '"' + (currentVal === t ? ' selected="selected"' : '') +'>' + arry[k] + '</option>' : '';
484
588
  }
485
589
  $t.find('thead').find('select.' + css + '[data-column="' + i + '"]')[ updating ? 'html' : 'append' ](o);
486
590
  },
@@ -492,73 +596,114 @@ $.tablesorter.addWidget({
492
596
  if ((t.hasClass('filter-select') || wo.filter_functions && wo.filter_functions[i] === true) && !t.hasClass('filter-false')){
493
597
  if (!wo.filter_functions) { wo.filter_functions = {}; }
494
598
  wo.filter_functions[i] = true; // make sure this select gets processed by filter_functions
495
- buildSelect(i, updating);
599
+ buildSelect(i, updating, t.hasClass(wo.filter_onlyAvail));
496
600
  }
497
601
  }
602
+ },
603
+ searching = function(filter){
604
+ if (typeof filter === 'undefined' || filter === true){
605
+ // delay filtering
606
+ clearTimeout(timer);
607
+ timer = setTimeout(function(){
608
+ checkFilters(filter);
609
+ }, wo.filter_liveSearch ? wo.filter_searchDelay : 10);
610
+ } else {
611
+ // skip delay
612
+ checkFilters(filter);
613
+ }
498
614
  };
499
-
500
615
  if (c.debug){
501
616
  time = new Date();
502
617
  }
503
- wo.filter_ignoreCase = wo.filter_ignoreCase !== false; // set default filter_ignoreCase to true
504
- wo.filter_useParsedData = wo.filter_useParsedData === true; // default is false
618
+ wo.filter_regex.child = new RegExp(c.cssChildRow);
619
+ wo.filter_regex.filtered = new RegExp(wo.filter_filteredRow);
505
620
  // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156
506
621
  if (wo.filter_columnFilters !== false && $ths.filter('.filter-false').length !== $ths.length){
507
- t = '<tr class="tablesorter-filter-row">'; // build filter row
622
+ // build filter row
623
+ t = '<tr class="tablesorter-filter-row">';
624
+ for (i = 0; i < cols; i++){
625
+ t += '<td></td>';
626
+ }
627
+ c.$filters = $(t += '</tr>').appendTo( $t.find('thead').eq(0) ).find('td');
628
+ // build each filter input
508
629
  for (i = 0; i < cols; i++){
509
630
  dis = false;
510
631
  $th = $ths.filter('[data-column="' + i + '"]:last'); // assuming last cell of a column is the main column
511
632
  sel = (wo.filter_functions && wo.filter_functions[i] && typeof wo.filter_functions[i] !== 'function') || $th.hasClass('filter-select');
512
- t += '<td>';
513
- if (sel){
514
- t += '<select data-column="' + i + '" class="' + css;
515
- } else {
516
- t += '<input type="search" placeholder="' + ($th.attr('data-placeholder') || "") + '" data-column="' + i + '" class="' + css;
517
- }
518
633
  // use header option - headers: { 1: { filter: false } } OR add class="filter-false"
519
634
  if (ts.getData){
520
- dis = ts.getData($th[0], c.headers[i], 'filter') === 'false';
521
635
  // get data from jQuery data, metadata, headers option or header class name
522
- t += dis ? ' disabled" disabled' : '"';
636
+ dis = ts.getData($th[0], c.headers[i], 'filter') === 'false';
523
637
  } else {
524
- dis = (c.headers[i] && c.headers[i].hasOwnProperty('filter') && c.headers[i].filter === false) || $th.hasClass('filter-false');
525
638
  // only class names and header options - keep this for compatibility with tablesorter v2.0.5
526
- t += (dis) ? ' disabled" disabled' : '"';
639
+ dis = (c.headers[i] && c.headers[i].hasOwnProperty('filter') && c.headers[i].filter === false) || $th.hasClass('filter-false');
640
+ }
641
+
642
+ if (sel){
643
+ t = $('<select>').appendTo( c.$filters.eq(i) );
644
+ } else {
645
+ if (wo.filter_formatter && $.isFunction(wo.filter_formatter[i])) {
646
+ t = wo.filter_formatter[i]( c.$filters.eq(i), i );
647
+ // no element returned, so lets go find it
648
+ if (t && t.length === 0) { t = c.$filters.eq(i).children('input'); }
649
+ // element not in DOM, so lets attach it
650
+ if (t && (t.parent().length === 0 || (t.parent().length && t.parent()[0] !== c.$filters[i]))) {
651
+ c.$filters.eq(i).append(t);
652
+ }
653
+ } else {
654
+ t = $('<input type="search">').appendTo( c.$filters.eq(i) );
655
+ }
656
+ if (t) {
657
+ t.attr('placeholder', $th.data('placeholder') || $th.attr('data-placeholder') || '');
658
+ }
659
+ }
660
+ if (t) {
661
+ t.addClass(css).attr('data-column', i);
662
+ if (dis) {
663
+ t.addClass('disabled')[0].disabled = true; // disabled!
664
+ }
527
665
  }
528
- t += (sel ? '></select>' : '>') + '</td>';
529
666
  }
530
- $t.find('thead').eq(0).append(t += '</tr>');
531
667
  }
532
668
  $t
533
- // add .tsfilter namespace to all BUT search
534
- .bind('addRows updateCell update appendCache search'.split(' ').join('.tsfilter '), function(e, filter){
535
- if (e.type !== 'search'){
669
+ .bind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join('.tsfilter '), function(e, filter){
670
+ if (!/(search|filterReset|filterEnd)/.test(e.type)){
671
+ e.stopPropagation();
672
+ buildDefault(true);
673
+ }
674
+ if (e.type === 'filterReset') {
675
+ $t.find('.' + css).val('');
676
+ }
677
+ if (e.type === 'filterEnd') {
536
678
  buildDefault(true);
679
+ } else {
680
+ // send false argument to force a new search; otherwise if the filter hasn't changed, it will return
681
+ filter = e.type === 'search' ? filter : e.type === 'updateComplete' ? $t.data('lastSearch') : '';
682
+ searching(filter);
537
683
  }
538
- checkFilters(e.type === 'search' ? filter : '');
539
684
  return false;
540
685
  })
541
686
  .find('input.' + css).bind('keyup search', function(e, filter){
542
- // ignore arrow and meta keys; allow backspace
543
- if ((e.which < 32 && e.which !== 8) || (e.which >= 37 && e.which <=40)) { return; }
544
- // skip delay
545
- if (typeof filter !== 'undefined'){
546
- checkFilters(filter);
547
- return false;
687
+ // emulate what webkit does.... escape clears the filter
688
+ if (e.which === 27) {
689
+ this.value = '';
690
+ // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace
691
+ } else if ( (typeof wo.filter_liveSearch === 'number' && this.value.length < wo.filter_liveSearch && this.value !== '') || ( e.type === 'keyup' &&
692
+ ( (e.which < 32 && e.which !== 8 && wo.filter_liveSearch === true && e.which !== 13) || (e.which >= 37 && e.which <=40) || (e.which !== 13 && wo.filter_liveSearch === false) ) ) ) {
693
+ return;
548
694
  }
549
- // delay filtering
550
- clearTimeout(timer);
551
- timer = setTimeout(function(){
552
- checkFilters();
553
- }, wo.filter_searchDelay || 300);
695
+ searching(filter);
554
696
  });
555
697
 
698
+ // parse columns after formatter, in case the class is added at that point
699
+ parsed = $ths.map(function(i){
700
+ return (ts.getData) ? ts.getData($ths.filter('[data-column="' + i + '"]:last'), c.headers[i], 'filter') === 'parsed' : $(this).hasClass('filter-parsed');
701
+ }).get();
702
+
556
703
  // reset button/link
557
704
  if (wo.filter_reset && $(wo.filter_reset).length){
558
- $(wo.filter_reset).bind('click', function(){
559
- $t.find('.' + css).val('');
560
- checkFilters();
561
- return false;
705
+ $(wo.filter_reset).bind('click.tsfilter', function(){
706
+ $t.trigger('filterReset');
562
707
  });
563
708
  }
564
709
  if (wo.filter_functions){
@@ -573,7 +718,7 @@ $.tablesorter.addWidget({
573
718
  // add custom drop down list
574
719
  for (str in wo.filter_functions[col]){
575
720
  if (typeof str === 'string'){
576
- ff += ff === '' ? '<option value="">' + (t.attr('data-placeholder') || '') + '</option>' : '';
721
+ ff += ff === '' ? '<option value="">' + (t.data('placeholder') || t.attr('data-placeholder') || '') + '</option>' : '';
577
722
  ff += '<option value="' + str + '">' + str + '</option>';
578
723
  }
579
724
  }
@@ -586,11 +731,11 @@ $.tablesorter.addWidget({
586
731
  // it would append the same options twice.
587
732
  buildDefault(true);
588
733
 
589
- $t.find('select.' + css).bind('change search', function(){
590
- checkFilters();
734
+ $t.find('select.' + css).bind('change search', function(e, filter){
735
+ checkFilters(filter);
591
736
  });
592
737
 
593
- if (wo.filter_hideFilters === true){
738
+ if (wo.filter_hideFilters){
594
739
  $t
595
740
  .find('.tablesorter-filter-row')
596
741
  .addClass('hideme')
@@ -607,7 +752,7 @@ $.tablesorter.addWidget({
607
752
  // $(':focus') needs jQuery 1.6+
608
753
  if ($(document.activeElement).closest('tr')[0] !== ft[0]){
609
754
  // get all filter values
610
- all = $t.find('.' + (wo.filter_cssFilter || 'tablesorter-filter')).map(function(){
755
+ all = $t.find('.' + wo.filter_cssFilter).map(function(){
611
756
  return $(this).val() || '';
612
757
  }).get().join('');
613
758
  // don't hide row if any filter has a value
@@ -623,7 +768,7 @@ $.tablesorter.addWidget({
623
768
  clearTimeout(st);
624
769
  st = setTimeout(function(){
625
770
  // don't hide row if any filter has a value
626
- if ($t.find('.' + (wo.filter_cssFilter || 'tablesorter-filter')).map(function(){ return $(this).val() || ''; }).get().join('') === ''){
771
+ if ($t.find('.' + wo.filter_cssFilter).map(function(){ return $(this).val() || ''; }).get().join('') === ''){
627
772
  ft2[ e.type === 'focus' ? 'removeClass' : 'addClass']('hideme');
628
773
  }
629
774
  }, 200);
@@ -632,7 +777,7 @@ $.tablesorter.addWidget({
632
777
 
633
778
  // show processing icon
634
779
  if (c.showProcessing) {
635
- $t.bind('filterStart filterEnd', function(e, v) {
780
+ $t.bind('filterStart.tsfilter filterEnd.tsfilter', function(e, v) {
636
781
  var fc = (v) ? $t.find('.' + c.cssHeader).filter('[data-column]').filter(function(){
637
782
  return v[$(this).data('column')] !== '';
638
783
  }) : '';
@@ -643,103 +788,143 @@ $.tablesorter.addWidget({
643
788
  if (c.debug){
644
789
  ts.benchmark("Applying Filter widget", time);
645
790
  }
791
+ // add default values
792
+ $t.bind('tablesorter-initialized', function(){
793
+ ff = ts.getFilters(table);
794
+ for (i = 0; i < ff.length; i++) {
795
+ ff[i] = $ths.filter('[data-column="' + i + '"]:last').attr(wo.filter_defaultAttrib) || ff[i];
796
+ }
797
+ ts.setFilters(table, ff, true);
798
+ });
646
799
  // filter widget initialized
647
800
  $t.trigger('filterInit');
801
+ checkFilters();
648
802
  }
649
803
  },
650
804
  remove: function(table, c, wo){
651
805
  var k, $tb,
652
- $t = $(table),
806
+ $t = c.$table,
653
807
  b = c.$tbodies;
654
808
  $t
655
809
  .removeClass('hasFilters')
656
810
  // add .tsfilter namespace to all BUT search
657
- .unbind('addRows updateCell update appendCache search'.split(' ').join('.tsfilter'))
811
+ .unbind('addRows updateCell update updateComplete appendCache search filterStart filterEnd '.split(' ').join('.tsfilter '))
658
812
  .find('.tablesorter-filter-row').remove();
659
813
  for (k = 0; k < b.length; k++ ){
660
- $tb = $.tablesorter.processTbody(table, b.eq(k), true); // remove tbody
661
- $tb.children().removeClass('filtered').show();
662
- $.tablesorter.processTbody(table, $tb, false); // restore tbody
814
+ $tb = ts.processTbody(table, b.eq(k), true); // remove tbody
815
+ $tb.children().removeClass(wo.filter_filteredRow).show();
816
+ ts.processTbody(table, $tb, false); // restore tbody
663
817
  }
664
- if (wo.filterreset) { $(wo.filter_reset).unbind('click'); }
818
+ if (wo.filterreset) { $(wo.filter_reset).unbind('click.tsfilter'); }
665
819
  }
666
820
  });
821
+ ts.getFilters = function(table) {
822
+ var c = table ? $(table)[0].config : {};
823
+ if (c && c.widgetOptions && !c.widgetOptions.filter_columnFilters) { return $(table).data('lastSearch'); }
824
+ return c && c.$filters ? c.$filters.find('.' + c.widgetOptions.filter_cssFilter).map(function(i, el) {
825
+ return $(el).val();
826
+ }).get() || [] : false;
827
+ };
828
+ ts.setFilters = function(table, filter, apply) {
829
+ var $t = $(table),
830
+ c = $t.length ? $t[0].config : {},
831
+ valid = c && c.$filters ? c.$filters.find('.' + c.widgetOptions.filter_cssFilter).each(function(i, el) {
832
+ $(el).val(filter[i] || '');
833
+ }).trigger('change.tsfilter') || false : false;
834
+ if (apply) { $t.trigger('search', [filter, false]); }
835
+ return !!valid;
836
+ };
667
837
 
668
838
  // Widget: Sticky headers
669
839
  // based on this awesome article:
670
840
  // http://css-tricks.com/13465-persistent-headers/
671
841
  // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech
672
842
  // **************************
673
- $.tablesorter.addWidget({
843
+ ts.addWidget({
674
844
  id: "stickyHeaders",
675
- format: function(table){
676
- if ($(table).hasClass('hasStickyHeaders')) { return; }
677
- var $table = $(table).addClass('hasStickyHeaders'),
678
- c = table.config,
679
- wo = c.widgetOptions,
680
- win = $(window),
681
- header = $(table).children('thead:first'), //.add( $(table).find('caption') ),
845
+ priority: 60,
846
+ options: {
847
+ stickyHeaders : 'tablesorter-stickyHeader',
848
+ stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element
849
+ stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists
850
+ stickyHeaders_addResizeEvent : true, // trigger "resize" event on headers
851
+ stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header
852
+ stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs
853
+ },
854
+ format: function(table, c, wo){
855
+ if (c.$table.hasClass('hasStickyHeaders')) { return; }
856
+ var $t = c.$table,
857
+ $win = $(window),
858
+ header = $t.children('thead:first'),
682
859
  hdrCells = header.children('tr:not(.sticky-false)').children(),
683
- css = wo.stickyHeaders || 'tablesorter-stickyHeader',
684
860
  innr = '.tablesorter-header-inner',
685
- firstRow = hdrCells.eq(0).parent(),
686
- tfoot = $table.find('tfoot'),
687
- t2 = wo.$sticky = $table.clone(), // clone table, but don't remove id... the table might be styled by css
688
- // clone the entire thead - seems to work in IE8+
689
- stkyHdr = t2.children('thead:first')
690
- .addClass(css)
861
+ tfoot = $t.find('tfoot'),
862
+ filterInputs = '.' + (wo.filter_cssFilter || 'tablesorter-filter'),
863
+ $stickyOffset = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : '',
864
+ stickyOffset = $stickyOffset.length ? $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0,
865
+ stickyzIndex = wo.stickyHeaders_zIndex ? wo.stickyHeaders_zIndex : 2,
866
+ $stickyTable = wo.$sticky = $t.clone()
867
+ .addClass('containsStickyHeaders')
691
868
  .css({
692
- width : header.outerWidth(true),
693
869
  position : 'fixed',
694
870
  margin : 0,
695
- top : 0,
871
+ top : stickyOffset,
696
872
  visibility : 'hidden',
697
- zIndex : 1
873
+ zIndex : stickyzIndex
698
874
  }),
699
- stkyCells = stkyHdr.children('tr:not(.sticky-false)').children(), // issue #172
875
+ stkyHdr = $stickyTable.children('thead:first').addClass(wo.stickyHeaders),
876
+ stkyCells,
700
877
  laststate = '',
701
878
  spacing = 0,
879
+ flag = false,
702
880
  resizeHdr = function(){
881
+ stickyOffset = $stickyOffset.length ? $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0;
703
882
  var bwsr = navigator.userAgent;
704
883
  spacing = 0;
705
884
  // yes, I dislike browser sniffing, but it really is needed here :(
706
885
  // webkit automatically compensates for border spacing
707
- if ($table.css('border-collapse') !== 'collapse' && !/(webkit|msie)/i.test(bwsr)) {
886
+ if ($t.css('border-collapse') !== 'collapse' && !/(webkit|msie)/i.test(bwsr)) {
708
887
  // Firefox & Opera use the border-spacing
709
888
  // update border-spacing here because of demos that switch themes
710
889
  spacing = parseInt(hdrCells.eq(0).css('border-left-width'), 10) * 2;
711
890
  }
712
- stkyHdr.css({
713
- left : header.offset().left - win.scrollLeft() - spacing,
714
- width: header.outerWidth()
891
+ $stickyTable.css({
892
+ left : header.offset().left - $win.scrollLeft() - spacing,
893
+ width: $t.width()
715
894
  });
716
- stkyCells
717
- .each(function(i){
718
- var $h = hdrCells.eq(i);
719
- $(this).css({
720
- width: $h.width() - spacing,
721
- height: $h.height()
722
- });
723
- })
724
- .find(innr).each(function(i){
725
- var hi = hdrCells.eq(i).find(innr),
726
- w = hi.width(); // - ( parseInt(hi.css('padding-left'), 10) + parseInt(hi.css('padding-right'), 10) );
727
- $(this).width(w);
895
+ stkyCells.filter(':visible').each(function(i){
896
+ var $h = hdrCells.filter(':visible').eq(i);
897
+ $(this)
898
+ .css({
899
+ width: $h.width() - spacing,
900
+ height: $h.height()
901
+ })
902
+ .find(innr).width( $h.find(innr).width() );
728
903
  });
729
904
  };
905
+ // fix clone ID, if it exists - fixes #271
906
+ if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; }
730
907
  // clear out cloned table, except for sticky header
731
- t2.find('thead:gt(0),tr.sticky-false,tbody,tfoot,caption').remove();
732
- t2.css({ height:0, width:0, padding:0, margin:0, border:0 });
733
- // remove rows you don't want to be sticky
734
- stkyHdr.find('tr.sticky-false').remove();
908
+ // include caption & filter row (fixes #126 & #249)
909
+ $stickyTable.find('thead:gt(0), tr.sticky-false, tbody, tfoot').remove();
910
+ if (!wo.stickyHeaders_includeCaption) {
911
+ $stickyTable.find('caption').remove();
912
+ }
913
+ // issue #172 - find td/th in sticky header
914
+ stkyCells = stkyHdr.children().children();
915
+ $stickyTable.css({ height:0, width:0, padding:0, margin:0, border:0 });
735
916
  // remove resizable block
736
917
  stkyCells.find('.tablesorter-resizer').remove();
737
918
  // update sticky header class names to match real header after sorting
738
- $table
919
+ $t
920
+ .addClass('hasStickyHeaders')
739
921
  .bind('sortEnd.tsSticky', function(){
740
- hdrCells.each(function(i){
741
- var t = stkyCells.eq(i);
742
- t.attr('class', $(this).attr('class'));
922
+ hdrCells.filter(':visible').each(function(i){
923
+ var t = stkyCells.filter(':visible').eq(i);
924
+ t
925
+ .attr('class', $(this).attr('class'))
926
+ // remove processing icon
927
+ .removeClass(c.cssProcessing);
743
928
  if (c.cssIcon){
744
929
  t
745
930
  .find('.' + c.cssIcon)
@@ -750,54 +935,86 @@ $.tablesorter.addWidget({
750
935
  .bind('pagerComplete.tsSticky', function(){
751
936
  resizeHdr();
752
937
  });
753
- // set sticky header cell width and link clicks to real header
754
- hdrCells.find('*').andSelf().filter(c.selectorSort).each(function(i){
755
- var t = $(this);
756
- stkyCells.eq(i)
938
+ // http://stackoverflow.com/questions/5312849/jquery-find-self;
939
+ hdrCells.find(c.selectorSort).add( c.$headers.filter(c.selectorSort) ).each(function(i){
940
+ var t = $(this),
757
941
  // clicking on sticky will trigger sort
758
- .bind('mouseup', function(e){
942
+ $cell = stkyHdr.children('tr.tablesorter-headerRow').children().eq(i).bind('mouseup', function(e){
759
943
  t.trigger(e, true); // external mouseup flag (click timer is ignored)
760
- })
761
- // prevent sticky header text selection
762
- .bind('mousedown', function(){
763
- this.onselectstart = function(){ return false; };
764
- return false;
765
944
  });
945
+ // prevent sticky header text selection
946
+ if (c.cancelSelection) {
947
+ $cell
948
+ .attr('unselectable', 'on')
949
+ .bind('selectstart', false)
950
+ .css({
951
+ 'user-select': 'none',
952
+ 'MozUserSelect': 'none'
953
+ });
954
+ }
766
955
  });
767
956
  // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned.
768
- $table.after( t2 );
957
+ $t.after( $stickyTable );
769
958
  // make it sticky!
770
- win
771
- .bind('scroll.tsSticky', function(){
772
- var offset = firstRow.offset(),
773
- sTop = win.scrollTop(),
774
- tableHt = $table.height() - (stkyHdr.height() + (tfoot.height() || 0)),
959
+ $win.bind('scroll.tsSticky resize.tsSticky', function(e){
960
+ if (!$t.is(':visible')) { return; } // fixes #278
961
+ var pre = 'tablesorter-sticky-',
962
+ offset = $t.offset(),
963
+ cap = (wo.stickyHeaders_includeCaption ? 0 : $t.find('caption').outerHeight(true)),
964
+ sTop = $win.scrollTop() + stickyOffset - cap,
965
+ tableHt = $t.height() - ($stickyTable.height() + (tfoot.height() || 0)),
775
966
  vis = (sTop > offset.top) && (sTop < offset.top + tableHt) ? 'visible' : 'hidden';
776
- stkyHdr
967
+ $stickyTable
968
+ .removeClass(pre + 'visible ' + pre + 'hidden')
969
+ .addClass(pre + vis)
777
970
  .css({
778
971
  // adjust when scrolling horizontally - fixes issue #143
779
- left : header.offset().left - win.scrollLeft() - spacing,
972
+ left : header.offset().left - $win.scrollLeft() - spacing,
780
973
  visibility : vis
781
974
  });
782
- if (vis !== laststate){
975
+ if (vis !== laststate || e.type === 'resize'){
783
976
  // make sure the column widths match
784
977
  resizeHdr();
785
978
  laststate = vis;
786
979
  }
787
- })
788
- .bind('resize.tsSticky', function(){
789
- resizeHdr();
790
980
  });
981
+ if (wo.stickyHeaders_addResizeEvent) {
982
+ ts.addHeaderResizeEvent(table);
983
+ }
984
+
985
+ // look for filter widget
986
+ $t.bind('filterEnd', function(){
987
+ if (flag) { return; }
988
+ stkyHdr.find('.tablesorter-filter-row').children().each(function(i){
989
+ $(this).find(filterInputs).val( c.$filters.find(filterInputs).eq(i).val() );
990
+ });
991
+ });
992
+ stkyCells.find(filterInputs).bind('keyup search change', function(e){
993
+ // ignore arrow and meta keys; allow backspace
994
+ if ((e.which < 32 && e.which !== 8) || (e.which >= 37 && e.which <=40)) { return; }
995
+ flag = true;
996
+ var $f = $(this), col = $f.attr('data-column');
997
+ c.$filters.find(filterInputs).eq(col)
998
+ .val( $f.val() )
999
+ .trigger('search');
1000
+ setTimeout(function(){
1001
+ flag = false;
1002
+ }, wo.filter_searchDelay);
1003
+ });
1004
+ $t.trigger('stickyHeadersInit');
1005
+
791
1006
  },
792
1007
  remove: function(table, c, wo){
793
- var $t = $(table),
794
- css = wo.stickyHeaders || 'tablesorter-stickyHeader';
795
- $t
1008
+ c.$table
796
1009
  .removeClass('hasStickyHeaders')
797
1010
  .unbind('sortEnd.tsSticky pagerComplete.tsSticky')
798
- .find('.' + css).remove();
799
- if (wo.$sticky) { wo.$sticky.remove(); } // remove cloned thead
800
- $(window).unbind('scroll.tsSticky resize.tsSticky');
1011
+ .find('.' + wo.stickyHeaders).remove();
1012
+ if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table
1013
+ // don't unbind if any table on the page still has stickyheaders applied
1014
+ if (!$('.hasStickyHeaders').length) {
1015
+ $(window).unbind('scroll.tsSticky resize.tsSticky');
1016
+ }
1017
+ ts.addHeaderResizeEvent(table, false);
801
1018
  }
802
1019
  });
803
1020
 
@@ -805,39 +1022,42 @@ $.tablesorter.addWidget({
805
1022
  // this widget saves the column widths if
806
1023
  // $.tablesorter.storage function is included
807
1024
  // **************************
808
- $.tablesorter.addWidget({
1025
+ ts.addWidget({
809
1026
  id: "resizable",
810
- format: function(table){
811
- if ($(table).hasClass('hasResizable')) { return; }
812
- $(table).addClass('hasResizable');
813
- var $t, t, i, j, s, $c, $cols, w, tw,
814
- $tbl = $(table),
815
- c = table.config,
816
- wo = c.widgetOptions,
1027
+ priority: 40,
1028
+ options: {
1029
+ resizable : true,
1030
+ resizable_addLastColumn : false
1031
+ },
1032
+ format: function(table, c, wo){
1033
+ if (c.$table.hasClass('hasResizable')) { return; }
1034
+ c.$table.addClass('hasResizable');
1035
+ var $t, t, i, j, s = {}, $c, $cols, w, tw,
1036
+ $tbl = c.$table,
817
1037
  position = 0,
818
1038
  $target = null,
819
1039
  $next = null,
820
1040
  fullWidth = Math.abs($tbl.parent().width() - $tbl.width()) < 20,
821
1041
  stopResize = function(){
822
- if ($.tablesorter.storage && $target){
1042
+ if (ts.storage && $target){
823
1043
  s[$target.index()] = $target.width();
824
1044
  s[$next.index()] = $next.width();
825
1045
  $target.width( s[$target.index()] );
826
1046
  $next.width( s[$next.index()] );
827
1047
  if (wo.resizable !== false){
828
- $.tablesorter.storage(table, 'tablesorter-resizable', s);
1048
+ ts.storage(table, 'tablesorter-resizable', s);
829
1049
  }
830
1050
  }
831
1051
  position = 0;
832
1052
  $target = $next = null;
833
1053
  $(window).trigger('resize'); // will update stickyHeaders, just in case
834
1054
  };
835
- s = ($.tablesorter.storage && wo.resizable !== false) ? $.tablesorter.storage(table, 'tablesorter-resizable') : {};
1055
+ s = (ts.storage && wo.resizable !== false) ? ts.storage(table, 'tablesorter-resizable') : {};
836
1056
  // process only if table ID or url match
837
1057
  if (s){
838
1058
  for (j in s){
839
- if (!isNaN(j) && j < c.headerList.length){
840
- $(c.headerList[j]).width(s[j]); // set saved resizable widths
1059
+ if (!isNaN(j) && j < c.$headers.length){
1060
+ c.$headers.eq(j).width(s[j]); // set saved resizable widths
841
1061
  }
842
1062
  }
843
1063
  }
@@ -846,7 +1066,7 @@ $.tablesorter.addWidget({
846
1066
  $t.children().each(function(){
847
1067
  t = $(this);
848
1068
  i = t.attr('data-column');
849
- j = $.tablesorter.getData( t, c.headers[i], 'resizable') === "false";
1069
+ j = ts.getData( t, c.headers[i], 'resizable') === "false";
850
1070
  $t.children().filter('[data-column="' + i + '"]').toggleClass('resizable-false', j);
851
1071
  });
852
1072
  // add wrapper inside each cell to allow for positioning of the resizable target block
@@ -856,7 +1076,8 @@ $.tablesorter.addWidget({
856
1076
  // Firefox needs this inner div to position the resizer correctly
857
1077
  $c.wrapInner('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>');
858
1078
  }
859
- $c = $c.slice(0,-1); // don't include the last column of the row
1079
+ // don't include the last column of the row
1080
+ if (!wo.resizable_addLastColumn) { $c = $c.slice(0,-1); }
860
1081
  $cols = $cols ? $cols.add($c) : $c;
861
1082
  });
862
1083
  $cols
@@ -895,12 +1116,12 @@ $.tablesorter.addWidget({
895
1116
  position = e.pageX;
896
1117
  });
897
1118
  $tbl.find('thead:first')
898
- .bind('mouseup.tsresize mouseleave.tsresize', function(e){
1119
+ .bind('mouseup.tsresize mouseleave.tsresize', function(){
899
1120
  stopResize();
900
1121
  })
901
1122
  // right click to reset columns to default widths
902
1123
  .bind('contextmenu.tsresize', function(){
903
- $.tablesorter.resizableReset(table);
1124
+ ts.resizableReset(table);
904
1125
  // $.isEmptyObject() needs jQuery 1.4+
905
1126
  var rtn = $.isEmptyObject ? $.isEmptyObject(s) : s === {}; // allow right click if already reset
906
1127
  s = {};
@@ -908,7 +1129,7 @@ $.tablesorter.addWidget({
908
1129
  });
909
1130
  },
910
1131
  remove: function(table, c, wo){
911
- $(table)
1132
+ c.$table
912
1133
  .removeClass('hasResizable')
913
1134
  .find('thead')
914
1135
  .unbind('mouseup.tsresize mouseleave.tsresize contextmenu.tsresize')
@@ -916,12 +1137,12 @@ $.tablesorter.addWidget({
916
1137
  .unbind('mousemove.tsresize mouseup.tsresize')
917
1138
  // don't remove "tablesorter-wrapper" as uitheme uses it too
918
1139
  .find('.tablesorter-resizer,.tablesorter-resizer-grip').remove();
919
- $.tablesorter.resizableReset(table);
1140
+ ts.resizableReset(table);
920
1141
  }
921
1142
  });
922
- $.tablesorter.resizableReset = function(table){
923
- $(table.config.headerList).filter(':not(.resizable-false)').css('width','');
924
- if ($.tablesorter.storage) { $.tablesorter.storage(table, 'tablesorter-resizable', {}); }
1143
+ ts.resizableReset = function(table){
1144
+ table.config.$headers.filter(':not(.resizable-false)').css('width','');
1145
+ if (ts.storage) { ts.storage(table, 'tablesorter-resizable', {}); }
925
1146
  };
926
1147
 
927
1148
  // Save table sort widget
@@ -929,38 +1150,46 @@ $.tablesorter.resizableReset = function(table){
929
1150
  // saveSort widget option is true AND the
930
1151
  // $.tablesorter.storage function is included
931
1152
  // **************************
932
- $.tablesorter.addWidget({
1153
+ ts.addWidget({
933
1154
  id: 'saveSort',
934
- init: function(table, thisWidget){
1155
+ priority: 20,
1156
+ options: {
1157
+ saveSort : true
1158
+ },
1159
+ init: function(table, thisWidget, c, wo){
935
1160
  // run widget format before all other widgets are applied to the table
936
- thisWidget.format(table, true);
1161
+ thisWidget.format(table, c, wo, true);
937
1162
  },
938
- format: function(table, init){
939
- var sl, time, c = table.config,
940
- wo = c.widgetOptions,
1163
+ format: function(table, c, wo, init){
1164
+ var sl, time,
1165
+ $t = c.$table,
941
1166
  ss = wo.saveSort !== false, // make saveSort active/inactive; default to true
942
1167
  sortList = { "sortList" : c.sortList };
943
1168
  if (c.debug){
944
1169
  time = new Date();
945
1170
  }
946
- if ($(table).hasClass('hasSaveSort')){
947
- if (ss && table.hasInitialized && $.tablesorter.storage){
948
- $.tablesorter.storage( table, 'tablesorter-savesort', sortList );
1171
+ if ($t.hasClass('hasSaveSort')){
1172
+ if (ss && table.hasInitialized && ts.storage){
1173
+ ts.storage( table, 'tablesorter-savesort', sortList );
949
1174
  if (c.debug){
950
- $.tablesorter.benchmark('saveSort widget: Saving last sort: ' + c.sortList, time);
1175
+ ts.benchmark('saveSort widget: Saving last sort: ' + c.sortList, time);
951
1176
  }
952
1177
  }
953
1178
  } else {
954
1179
  // set table sort on initial run of the widget
955
- $(table).addClass('hasSaveSort');
1180
+ $t.addClass('hasSaveSort');
956
1181
  sortList = '';
957
1182
  // get data
958
- if ($.tablesorter.storage){
959
- sl = $.tablesorter.storage( table, 'tablesorter-savesort' );
1183
+ if (ts.storage){
1184
+ sl = ts.storage( table, 'tablesorter-savesort' );
960
1185
  sortList = (sl && sl.hasOwnProperty('sortList') && $.isArray(sl.sortList)) ? sl.sortList : '';
961
1186
  if (c.debug){
962
- $.tablesorter.benchmark('saveSort: Last sort loaded: "' + sortList + '"', time);
1187
+ ts.benchmark('saveSort: Last sort loaded: "' + sortList + '"', time);
963
1188
  }
1189
+ $t.bind('saveSortReset', function(e){
1190
+ e.stopPropagation();
1191
+ ts.storage( table, 'tablesorter-savesort', '' );
1192
+ });
964
1193
  }
965
1194
  // init is true when widget init is run, this will run this widget before all other widgets have initialized
966
1195
  // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice.
@@ -968,13 +1197,13 @@ $.tablesorter.addWidget({
968
1197
  c.sortList = sortList;
969
1198
  } else if (table.hasInitialized && sortList && sortList.length > 0){
970
1199
  // update sort change
971
- $(table).trigger('sorton', [sortList]);
1200
+ $t.trigger('sorton', [sortList]);
972
1201
  }
973
1202
  }
974
1203
  },
975
- remove: function(table, c, wo){
1204
+ remove: function(table){
976
1205
  // clear storage
977
- if ($.tablesorter.storage) { $.tablesorter.storage( table, 'tablesorter-savesort', '' ); }
1206
+ if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); }
978
1207
  }
979
1208
  });
980
1209