jquery-tablesorter 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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