jquery-tablesorter 1.8.0 → 1.8.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: acfcfbdcbf520532b495a3aa784033d4ec287a30
4
- data.tar.gz: 002c5105872cb05cf0d4f7d2f78d027fc8fc7864
3
+ metadata.gz: e6f685058fd350d114a2f272290ad8a7e5c3f7ed
4
+ data.tar.gz: e9de584dc9a659b30b9bd79dec2a863cadf04749
5
5
  SHA512:
6
- metadata.gz: 171363daa41553a03cbca64f9c58bac9810d9c3b4f87509eda0713bee18bc0989ee2beca7b29247987085c379ccd97735382dd3f54f03ed4ffaf9f8df1938815
7
- data.tar.gz: 5c0a43f4f40e32e946ed3e500d9f7012ffffe50803fb6a880a51d4648672435cd5fcde8e3d5abb33abb57cf511d60acac63766db00f678f63015223266f79ed6
6
+ metadata.gz: 49d6f67cce6923d1ac1fb24d651673dc3162608e41dfb70349fb3ec37c61b815f5cd4462887d2f891cdf1a05a90603d09bd4d45289f6e605270590f2cb744958
7
+ data.tar.gz: d8c7d5b3890bea49ed049a17600ef2695a205450e432fac80d72b1835e69eb3c8edb7fddfd4517bef9d47207107186696c1fdee1b95992a10562871d357d21a2
@@ -4,7 +4,7 @@
4
4
 
5
5
  Simple integration of jquery-tablesorter into the asset pipeline.
6
6
 
7
- Current tablesorter version: 2.13.2 (11/2/2013), [documentation]
7
+ Current tablesorter version: 2.13.3 (11/9/2013), [documentation]
8
8
 
9
9
  Any issue associate with the js/css files, please report to [Mottie's fork].
10
10
 
@@ -1,3 +1,3 @@
1
1
  module JqueryTablesorter
2
- VERSION = "1.8.0"
2
+ VERSION = "1.8.1"
3
3
  end
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * tablesorter pager plugin
3
- * updated 10/30/2013
3
+ * updated 11/9/2013
4
4
  */
5
5
  /*jshint browser:true, jquery:true, unused:false */
6
6
  ;(function($) {
@@ -236,6 +236,9 @@
236
236
  $t.find('thead tr.' + p.cssErrorRow).remove(); // Clean up any previous error.
237
237
 
238
238
  if ( exception ) {
239
+ if (c.debug) {
240
+ ts.log('Ajax Error', xhr, exception);
241
+ }
239
242
  $err = $('<tr class="' + p.cssErrorRow + '"><td style="text-align:center;" colspan="' + hl + '">' + (
240
243
  xhr.status === 0 ? 'Not connected, verify Network' :
241
244
  xhr.status === 404 ? 'Requested page not found [404]' :
@@ -272,15 +275,13 @@
272
275
  c.$tbodies.eq(0).empty().append(d);
273
276
  } else if (l) {
274
277
  // build table from array
275
- if ( l > 0 ) {
276
- for ( i = 0; i < l; i++ ) {
277
- tds += '<tr>';
278
- for ( j = 0; j < d[i].length; j++ ) {
279
- // build tbody cells
280
- tds += '<td>' + d[i][j] + '</td>';
281
- }
282
- tds += '</tr>';
278
+ for ( i = 0; i < l; i++ ) {
279
+ tds += '<tr>';
280
+ for ( j = 0; j < d[i].length; j++ ) {
281
+ // build tbody cells
282
+ tds += '<td>' + d[i][j] + '</td>';
283
283
  }
284
+ tds += '</tr>';
284
285
  }
285
286
  // add rows to first tbody
286
287
  c.$tbodies.eq(0).html( tds );
@@ -314,9 +315,15 @@
314
315
  if (c.showProcessing) {
315
316
  ts.isProcessing(table); // remove loading icon
316
317
  }
317
- p.totalPages = Math.ceil( p.totalRows / ( p.size || 10 ) );
318
+ // make sure last pager settings are saved, prevents multiple server side calls with
319
+ // the same parameters
320
+ p.last.totalPages = p.totalPages = Math.ceil( p.totalRows / ( p.size || 10 ) );
321
+ p.last.currentFilters = p.currentFilters;
322
+ p.last.sortList = (c.sortList || []).join(',');
318
323
  updatePageDisplay(table, p);
319
324
  fixHeight(table, p);
325
+ // apply widgets after table has rendered
326
+ $t.trigger('applyWidgets');
320
327
  if (p.initialized) {
321
328
  $t.trigger('pagerChange', p);
322
329
  $t.trigger('updateComplete');
@@ -350,17 +357,21 @@
350
357
  p.oldAjaxSuccess(data);
351
358
  }
352
359
  };
360
+ if (c.debug) {
361
+ ts.log('ajax initialized', p.ajaxObject);
362
+ }
353
363
  $.ajax(p.ajaxObject);
354
364
  }
355
365
  },
356
366
 
357
367
  getAjaxUrl = function(table, p) {
358
- var url = (p.ajaxUrl) ? p.ajaxUrl
368
+ var c = table.config,
369
+ url = (p.ajaxUrl) ? p.ajaxUrl
359
370
  // allow using "{page+1}" in the url string to switch to a non-zero based index
360
371
  .replace(/\{page([\-+]\d+)?\}/, function(s,n){ return p.page + (n ? parseInt(n, 10) : 0); })
361
372
  .replace(/\{size\}/g, p.size) : '',
362
- sl = table.config.sortList,
363
- fl = p.currentFilters || [],
373
+ sl = c.sortList,
374
+ fl = p.currentFilters || $(table).data('lastSearch') || [],
364
375
  sortCol = url.match(/\{\s*sort(?:List)?\s*:\s*(\w*)\s*\}/),
365
376
  filterCol = url.match(/\{\s*filter(?:List)?\s*:\s*(\w*)\s*\}/),
366
377
  arry = [];
@@ -382,10 +393,14 @@
382
393
  });
383
394
  // if the arry is empty, just add the fcol parameter... "&{filterList:fcol}" becomes "&fcol"
384
395
  url = url.replace(/\{\s*filter(?:List)?\s*:\s*(\w*)\s*\}/g, arry.length ? arry.join('&') : filterCol );
396
+ p.currentFilters = fl;
385
397
  }
386
398
  if ( typeof(p.customAjaxUrl) === "function" ) {
387
399
  url = p.customAjaxUrl(table, url);
388
400
  }
401
+ if (c.debug) {
402
+ ts.log('Pager ajax url: ' + url);
403
+ }
389
404
  return url;
390
405
  },
391
406
 
@@ -433,6 +448,9 @@
433
448
  p.totalPages = 1;
434
449
  $(table).addClass('pagerDisabled').find('tr.pagerSavedHeightSpacer').remove();
435
450
  renderTable(table, table.config.rowsCopy, p);
451
+ if (table.config.debug) {
452
+ ts.log('pager disabled');
453
+ }
436
454
  }
437
455
  // disable size selector
438
456
  p.$size.add(p.$goto).each(function(){
@@ -442,17 +460,25 @@
442
460
 
443
461
  moveToPage = function(table, p, flag) {
444
462
  if ( p.isDisabled ) { return; }
445
- var l = p.last,
463
+ var c = table.config,
464
+ l = p.last,
446
465
  pg = Math.min( p.totalPages, p.filteredPages );
447
466
  if ( p.page < 0 ) { p.page = 0; }
448
467
  if ( p.page > ( pg - 1 ) && pg !== 0 ) { p.page = pg - 1; }
449
- // don't allow rendering multiple times on the same page/size/totalpages/filters
450
- if (l.page === p.page && l.size === p.size && l.total === p.totalPages && l.filters === p.currentFilters ) { return; }
468
+ // don't allow rendering multiple times on the same page/size/totalpages/filters/sorts
469
+ if ( l.page === p.page && l.size === p.size && l.totalPages === p.totalPages &&
470
+ (l.currentFilters || []).join(',') === (p.currentFilters || []).join(',') &&
471
+ l.sortList === (c.sortList || []).join(',') ) { return; }
472
+ if (c.debug) {
473
+ ts.log('Pager changing to page ' + p.page);
474
+ }
451
475
  p.last = {
452
476
  page : p.page,
453
477
  size : p.size,
478
+ // fixes #408; modify sortList otherwise it auto-updates
479
+ sortList : (c.sortList || []).join(','),
454
480
  totalPages : p.totalPages,
455
- currentFilters : p.currentFilters
481
+ currentFilters : p.currentFilters || []
456
482
  };
457
483
  if (p.ajax) {
458
484
  getAjax(table, p);
@@ -460,18 +486,18 @@
460
486
  renderTable(table, table.config.rowsCopy, p);
461
487
  }
462
488
  $.data(table, 'pagerLastPage', p.page);
463
- $.data(table, 'pagerUpdateTriggered', true);
464
489
  if (p.initialized && flag !== false) {
465
- $(table).trigger('pageMoved', p);
490
+ c.$table.trigger('pageMoved', p);
491
+ c.$table.trigger('applyWidgets');
466
492
  }
467
493
  },
468
494
 
469
495
  setPageSize = function(table, size, p) {
470
- p.size = size;
471
- p.$size.val(size);
496
+ p.size = size || p.size || 10;
497
+ p.$size.val(p.size);
472
498
  $.data(table, 'pagerLastPage', p.page);
473
499
  $.data(table, 'pagerLastSize', p.size);
474
- p.totalPages = Math.ceil( p.totalRows / ( p.size || 10 ) );
500
+ p.totalPages = Math.ceil( p.totalRows / p.size );
475
501
  moveToPage(table, p);
476
502
  },
477
503
 
@@ -517,14 +543,17 @@
517
543
  p.$goto.removeClass(p.cssDisabled).removeAttr('disabled');
518
544
  p.isDisabled = false;
519
545
  p.page = $.data(table, 'pagerLastPage') || p.page || 0;
520
- p.size = $.data(table, 'pagerLastSize') || parseInt(pg.find('option[selected]').val(), 10) || p.size;
546
+ p.size = $.data(table, 'pagerLastSize') || parseInt(pg.find('option[selected]').val(), 10) || p.size || 10;
521
547
  pg.val(p.size); // set page size
522
- p.totalPages = Math.ceil( Math.min( p.totalPages, p.filteredPages ) / ( p.size || 10 ) );
548
+ p.totalPages = Math.ceil( Math.min( p.totalPages, p.filteredPages ) / p.size );
523
549
  if ( triggered ) {
524
550
  $(table).trigger('update');
525
551
  setPageSize(table, p.size, p);
526
552
  hideRowsSetup(table, p);
527
553
  fixHeight(table, p);
554
+ if (table.config.debug) {
555
+ ts.log('pager enabled');
556
+ }
528
557
  }
529
558
  };
530
559
 
@@ -534,8 +563,8 @@
534
563
  if ( !p.ajax ) {
535
564
  c.rowsCopy = rows;
536
565
  p.totalRows = p.countChildRows ? c.$tbodies.eq(0).children().length : rows.length;
537
- p.size = $.data(table, 'pagerLastSize') || p.size;
538
- p.totalPages = Math.ceil( p.totalRows / ( p.size || 10 ) );
566
+ p.size = $.data(table, 'pagerLastSize') || p.size || 10;
567
+ p.totalPages = Math.ceil( p.totalRows / p.size );
539
568
  renderTable(table, rows, p);
540
569
  }
541
570
  };
@@ -551,34 +580,36 @@
551
580
  $t = c.$table,
552
581
  // added in case the pager is reinitialized after being destroyed.
553
582
  pager = p.$container = $(p.container).addClass('tablesorter-pager').show();
583
+ if (c.debug) {
584
+ ts.log('Pager initializing');
585
+ }
554
586
  p.oldAjaxSuccess = p.oldAjaxSuccess || p.ajaxObject.success;
555
587
  c.appender = $this.appender;
556
-
588
+ if (ts.filter && c.widgets.indexOf('filter') >= 0) {
589
+ // get any default filter settings (data-value attribute) fixes #388
590
+ p.currentFilters = c.$table.data('lastSearch') || ts.filter.setDefaults(table, c, c.widgetOptions) || [];
591
+ // set, but don't apply current filters
592
+ ts.setFilters(table, p.currentFilters, false);
593
+ }
557
594
  if (p.savePages && ts.storage) {
558
595
  t = ts.storage(table, 'tablesorter-pager') || {}; // fixes #387
559
596
  p.page = isNaN(t.page) ? p.page : t.page;
560
597
  p.size = ( isNaN(t.size) ? p.size : t.size ) || 10;
598
+ $.data(table, 'pagerLastSize', p.size);
561
599
  }
562
600
 
563
601
  $t
564
- .unbind('filterStart.pager filterEnd.pager sortEnd.pager disable.pager enable.pager destroy.pager update.pager pageSize.pager')
602
+ .unbind('filterStart filterEnd sortEnd disable enable destroy update pageSize '.split(' ').join('.pager '))
565
603
  .bind('filterStart.pager', function(e, filters) {
566
- $.data(table, 'pagerUpdateTriggered', false);
567
604
  p.currentFilters = filters;
568
605
  })
569
606
  // update pager after filter widget completes
570
- .bind('filterEnd.pager sortEnd.pager', function(e) {
571
- //Prevent infinite event loops from occuring by setting this in all moveToPage calls and catching it here.
572
- if ($.data(table, 'pagerUpdateTriggered')) {
573
- $.data(table, 'pagerUpdateTriggered', false);
574
- return;
575
- }
576
- //only run the server side sorting if it has been enabled
577
- if (e.type === "filterEnd" || (e.type === "sortEnd" && c.serverSideSorting)) {
607
+ .bind('filterEnd.pager sortEnd.pager', function() {
608
+ if (p.initialized) {
578
609
  moveToPage(table, p, false);
610
+ updatePageDisplay(table, p, false);
611
+ fixHeight(table, p);
579
612
  }
580
- updatePageDisplay(table, p, false);
581
- fixHeight(table, p);
582
613
  })
583
614
  .bind('disable.pager', function(e){
584
615
  e.stopPropagation();
@@ -617,6 +648,7 @@
617
648
  pager.find(ctrls.join(','))
618
649
  .unbind('click.pager')
619
650
  .bind('click.pager', function(e){
651
+ e.stopPropagation();
620
652
  var i, $t = $(this), l = ctrls.length;
621
653
  if ( !$t.hasClass(p.cssDisabled) ) {
622
654
  for (i = 0; i < l; i++) {
@@ -626,7 +658,6 @@
626
658
  }
627
659
  }
628
660
  }
629
- return false;
630
661
  });
631
662
 
632
663
  // goto selector
@@ -1,5 +1,5 @@
1
1
  /**!
2
- * TableSorter 2.13.2 - Client-side table sorting with ease!
2
+ * TableSorter 2.13.3 - Client-side table sorting with ease!
3
3
  * @requires jQuery v1.2.6+
4
4
  *
5
5
  * Copyright (c) 2007 Christian Bach
@@ -24,7 +24,7 @@
24
24
 
25
25
  var ts = this;
26
26
 
27
- ts.version = "2.13.2";
27
+ ts.version = "2.13.3";
28
28
 
29
29
  ts.parsers = [];
30
30
  ts.widgets = [];
@@ -122,7 +122,8 @@
122
122
  };
123
123
 
124
124
  /* debuging utils */
125
- function log(s) {
125
+ function log() {
126
+ var s = arguments.length > 1 ? Array.prototype.slice.call(arguments) : arguments[0];
126
127
  if (typeof console !== "undefined" && typeof console.log !== "undefined") {
127
128
  console.log(s);
128
129
  } else {
@@ -201,9 +202,12 @@
201
202
  var c = table.config,
202
203
  // update table bodies in case we start with an empty table
203
204
  tb = c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')'),
204
- rows, list, l, i, h, ch, p, parsersDebug = "";
205
+ rows, list, l, i, h, ch, p, time, parsersDebug = "";
205
206
  if ( tb.length === 0) {
206
207
  return c.debug ? log('*Empty table!* Not building a parser cache') : '';
208
+ } else if (c.debug) {
209
+ time = new Date();
210
+ log('Detecting parsers for each column');
207
211
  }
208
212
  rows = tb[0].rows;
209
213
  if (rows[0]) {
@@ -233,6 +237,7 @@
233
237
  }
234
238
  if (c.debug) {
235
239
  log(parsersDebug);
240
+ benchmark("Completed detecting parsers", time);
236
241
  }
237
242
  c.parsers = list;
238
243
  }
@@ -301,11 +306,12 @@
301
306
  // init flag (true) used by pager plugin to prevent widget application
302
307
  function appendToTable(table, init) {
303
308
  var c = table.config,
304
- b = table.tBodies,
305
- rows = [],
306
- c2 = c.cache,
307
- r, n, totalRows, checkCell, $bk, $tb,
308
- i, j, k, l, pos, appendTime;
309
+ wo = c.widgetOptions,
310
+ b = table.tBodies,
311
+ rows = [],
312
+ c2 = c.cache,
313
+ r, n, totalRows, checkCell, $bk, $tb,
314
+ i, j, k, l, pos, appendTime;
309
315
  if (isEmptyObject(c2)) { return; } // empty table - fixes #206/#346
310
316
  if (c.debug) {
311
317
  appendTime = new Date();
@@ -322,8 +328,8 @@
322
328
  for (i = 0; i < totalRows; i++) {
323
329
  pos = n[i][checkCell];
324
330
  rows.push(r[pos]);
325
- // removeRows used by the pager plugin
326
- if (!c.appender || !c.removeRows) {
331
+ // removeRows used by the pager plugin; don't render if using ajax - fixes #411
332
+ if (!c.appender || (c.pager && (!c.pager.removeRows || !wo.pager_removeRows) && !c.pager.ajax)) {
327
333
  l = r[pos].length;
328
334
  for (j = 0; j < l; j++) {
329
335
  $tb.append(r[pos][j]);
@@ -340,8 +346,8 @@
340
346
  if (c.debug) {
341
347
  benchmark("Rebuilt table", appendTime);
342
348
  }
343
- // apply table widgets
344
- if (!init) { ts.applyWidget(table); }
349
+ // apply table widgets; but not before ajax completes
350
+ if (!init && !c.appender) { ts.applyWidget(table); }
345
351
  // trigger sortend
346
352
  $(table).trigger("sortEnd", table);
347
353
  $(table).trigger("updateComplete", table);
@@ -517,7 +523,7 @@
517
523
  // ensure all sortList values are numeric - fixes #127
518
524
  s = [ parseInt(v[0], 10), parseInt(v[1], 10) ];
519
525
  // make sure header exists
520
- o = c.headerList[s[0]];
526
+ o = c.$headers[s[0]];
521
527
  if (o) { // prevents error if sorton array is wrong
522
528
  c.sortList.push(s);
523
529
  t = $.inArray(s[1], o.order); // fixes issue #167
@@ -584,12 +590,13 @@
584
590
  }
585
591
  // the user has clicked on an already sorted column
586
592
  if (ts.isValueInArray(i, c.sortList)) {
587
- // reverse the sorting direction for all tables
593
+ // reverse the sorting direction
588
594
  for (j = 0; j < c.sortList.length; j++) {
589
595
  s = c.sortList[j];
590
- o = c.headerList[s[0]];
596
+ o = c.$headers[s[0]];
591
597
  if (s[0] === i) {
592
- s[1] = o.order[o.count];
598
+ // o.count seems to be incorrect when compared to cell.count
599
+ s[1] = o.order[cell.count];
593
600
  if (s[1] === 2) {
594
601
  c.sortList.splice(j,1);
595
602
  o.count = -1;
@@ -1,4 +1,4 @@
1
- /*! Filter widget formatter functions - updated 10/30/2013
1
+ /*! Filter widget formatter functions - updated 11/9/2013
2
2
  * requires: tableSorter 2.7.7+ and jQuery 1.4.3+
3
3
  *
4
4
  * uiSpinner (jQuery UI spinner)
@@ -92,7 +92,7 @@ $.tablesorter.filterFormatter = {
92
92
  .val(o.value)
93
93
  .appendTo($cell)
94
94
  .spinner(o)
95
- .bind('change keyup', function(e){
95
+ .bind('change keyup', function(){
96
96
  updateSpinner();
97
97
  });
98
98
 
@@ -113,7 +113,7 @@ $.tablesorter.filterFormatter = {
113
113
  .val(o.value)
114
114
  .appendTo($shcell)
115
115
  .spinner(o)
116
- .bind('change keyup', function(e){
116
+ .bind('change keyup', function(){
117
117
  $cell.find('.spinner').val( this.value );
118
118
  updateSpinner();
119
119
  });
@@ -228,7 +228,7 @@ $.tablesorter.filterFormatter = {
228
228
  .val(o.value)
229
229
  .appendTo($shcell)
230
230
  .slider(o)
231
- .bind('change keyup', function(e){
231
+ .bind('change keyup', function(){
232
232
  $cell.find('.slider').val( this.value );
233
233
  updateSlider();
234
234
  });
@@ -337,7 +337,7 @@ $.tablesorter.filterFormatter = {
337
337
  .val(o.value)
338
338
  .appendTo($shcell)
339
339
  .slider(o)
340
- .bind('change keyup', function(e){
340
+ .bind('change keyup', function(){
341
341
  $cell.find('.range').val( this.value );
342
342
  updateUiRange();
343
343
  });
@@ -630,8 +630,7 @@ $.tablesorter.filterFormatter = {
630
630
  var compare = ( $cell.find('.compare').val() || o.compare);
631
631
  $cell.find('input[type=hidden]')
632
632
  // add equal to the beginning, so we filter exact numbers
633
- .val( !o.addToggle || chkd ? (o.compare ? o.compare : o.exactMatch ? '=' : '') + v : '' )
634
- .val( !o.addToggle || chkd ? compare + v : '' )
633
+ .val( !o.addToggle || chkd ? (compare ? compare : o.exactMatch ? '=' : '') + v : '' )
635
634
  .trigger('search', delayed ? delayed : o.delayed).end()
636
635
  .find('.number').val(v);
637
636
  if ($cell.find('.number').length) {
@@ -732,7 +731,7 @@ $.tablesorter.filterFormatter = {
732
731
  HTML5 range slider
733
732
  \**********************/
734
733
  html5Range : function($cell, indx, def5Range) {
735
- var t, o = $.extend({
734
+ var o = $.extend({
736
735
  value : 0,
737
736
  min : 0,
738
737
  max : 100,
@@ -1,4 +1,4 @@
1
- /*! tableSorter 2.8+ widgets - updated 11/2/2013
1
+ /*! tableSorter 2.8+ widgets - updated 11/9/2013
2
2
  *
3
3
  * Column Styles
4
4
  * Column Filters
@@ -17,6 +17,7 @@ var ts = $.tablesorter = $.tablesorter || {};
17
17
  ts.themes = {
18
18
  "bootstrap" : {
19
19
  table : 'table table-bordered table-striped',
20
+ caption : 'caption',
20
21
  header : 'bootstrap-header', // give the header a gradient background
21
22
  footerRow : '',
22
23
  footerCells: '',
@@ -32,6 +33,7 @@ ts.themes = {
32
33
  },
33
34
  "jui" : {
34
35
  table : 'ui-widget ui-widget-content ui-corner-all', // table classes
36
+ caption : 'ui-widget-content ui-corner-all',
35
37
  header : 'ui-widget-header ui-corner-all ui-state-default', // header classes
36
38
  footerRow : '',
37
39
  footerCells: '',
@@ -167,10 +169,13 @@ ts.addWidget({
167
169
  sh = 'tr.' + (wo.stickyHeaders || 'tablesorter-stickyHeader'),
168
170
  rmv = o.sortNone + ' ' + o.sortDesc + ' ' + o.sortAsc;
169
171
  if (c.debug) { time = new Date(); }
172
+ // initialization code - run once
170
173
  if (!$t.hasClass('tablesorter-' + theme) || c.theme === theme || !table.hasInitialized){
171
174
  // update zebra stripes
172
175
  if (o.even !== '') { wo.zebra[0] += ' ' + o.even; }
173
176
  if (o.odd !== '') { wo.zebra[1] += ' ' + o.odd; }
177
+ // add caption style
178
+ $t.find('caption').addClass(o.caption);
174
179
  // add table/footer class names
175
180
  t = $t
176
181
  // remove other selected themes; use widgetOptions.theme_remove
@@ -191,7 +196,7 @@ ts.addWidget({
191
196
  $(this)[ e.type === 'mouseenter' ? 'addClass' : 'removeClass' ](o.hover);
192
197
  });
193
198
  if (!$h.find('.tablesorter-wrapper').length) {
194
- // Firefox needs this inner div to position the resizer correctly
199
+ // Firefox needs this inner div to position the icon/resizer correctly
195
200
  $h.wrapInner('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>');
196
201
  }
197
202
  if (c.cssIcon){
@@ -327,6 +332,7 @@ ts.addWidget({
327
332
  id: "filter",
328
333
  priority: 50,
329
334
  options : {
335
+ filter_anyMatch : false, // if true overrides default find rows behaviours and if any column matches query it returns that row
330
336
  filter_childRows : false, // if true, filter includes child row content in the search
331
337
  filter_columnFilters : true, // if true, a filter will be added to the top of each table column
332
338
  filter_cssFilter : '', // css class name added to the filter row & each input in the row (tablesorter-filter is ALWAYS added)
@@ -342,504 +348,663 @@ ts.addWidget({
342
348
  filter_startsWith : false, // if true, filter start from the beginning of the cell contents
343
349
  filter_useParsedData : false, // filter all data using parsed content
344
350
  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.
345
- filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value
346
-
347
- // regex used in filter "check" functions - not for general use and not documented
348
- filter_regex : {
349
- "regex" : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex
350
- "child" : /tablesorter-childRow/, // child row class name; this gets updated in the script
351
- "filtered" : /filtered/, // filtered (hidden) row class name; updated in the script
352
- "type" : /undefined|number/, // check type
353
- "exact" : /(^[\"|\'|=]+)|([\"|\'|=]+$)/g, // exact match (allow '==')
354
- "nondigit" : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser)
355
- "operators" : /[<>=]/g // replace operators
351
+ filter_defaultAttrib : 'data-value' // data attribute in the header cell that contains the default filter value
352
+ },
353
+ format: function(table, c, wo) {
354
+ if (!c.$table.hasClass('hasFilters')) {
355
+ if (c.parsers || !c.parsers && wo.filter_serversideFiltering) {
356
+ ts.filter.init(table, c, wo);
357
+ }
356
358
  }
357
359
  },
358
- format: function(table, c, wo){
359
- if (c.$table.hasClass('hasFilters')) { return; }
360
- // allow filter widget to work if it is being used
361
- if (c.parsers || !c.parsers && wo.filter_serversideFiltering){
362
- var i, j, k, l, val, ff, x, xi, st, sel, str,
363
- ft, ft2, $th, rg, s, t, dis, col,
364
- fmt = ts.formatFloat,
365
- last = '', // save last filter search
366
- $ths = c.$headers,
367
- $t = c.$table.addClass('hasFilters'),
368
- b = c.$tbodies,
369
- // c.columns defined in computeThIndexes()
370
- cols = c.columns || c.$headers.filter('th').length,
371
- parsed, time, timer,
360
+ remove: function(table, c, wo) {
361
+ var tbodyIndex, $tbody,
362
+ $table = c.$table,
363
+ $tbodies = c.$tbodies;
364
+ $table
365
+ .removeClass('hasFilters')
366
+ // add .tsfilter namespace to all BUT search
367
+ .unbind('addRows updateCell update updateComplete appendCache search filterStart filterEnd '.split(' ').join('.tsfilter '))
368
+ .find('.tablesorter-filter-row').remove();
369
+ for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
370
+ $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
371
+ $tbody.children().removeClass(wo.filter_filteredRow).show();
372
+ ts.processTbody(table, $tbody, false); // restore tbody
373
+ }
374
+ if (wo.filter_reset) {
375
+ $(document).undelegate(wo.filter_reset, 'click.tsfilter');
376
+ }
377
+ }
378
+ });
379
+
380
+ ts.filter = {
372
381
 
373
- // dig fer gold
374
- checkFilters = function(filter){
375
- var arry = $.isArray(filter),
376
- v = (arry) ? filter : ts.getFilters(table),
377
- cv = (v || []).join(''); // combined filter values
378
- // add filter array back into inputs
379
- if (arry) {
380
- ts.setFilters( $t, v );
382
+ // regex used in filter "check" functions - not for general use and not documented
383
+ regex: {
384
+ regex : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex
385
+ child : /tablesorter-childRow/, // child row class name; this gets updated in the script
386
+ filtered : /filtered/, // filtered (hidden) row class name; updated in the script
387
+ type : /undefined|number/, // check type
388
+ exact : /(^[\"|\'|=]+)|([\"|\'|=]+$)/g, // exact match (allow '==')
389
+ nondigit : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser)
390
+ operators : /[<>=]/g // replace operators
391
+ },
392
+ // function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed )
393
+ // filter = array of filter input values; iFilter = same array, except lowercase
394
+ // exact = table cell text (or parsed data if column parser enabled)
395
+ // iExact = same as exact, except lowercase
396
+ // cached = table cell text from cache, so it has been parsed
397
+ // index = column index; table = table element (DOM)
398
+ // wo = widget options (table.config.widgetOptions)
399
+ // parsed = array (by column) of boolean values (from filter_useParsedData or "filter-parsed" class)
400
+ types: {
401
+ // Look for regex
402
+ regex: function( filter, iFilter, exact, iExact ) {
403
+ if ( ts.filter.regex.regex.test(iFilter) ) {
404
+ var matches,
405
+ regex = ts.filter.regex.regex.exec(iFilter);
406
+ try {
407
+ matches = new RegExp(regex[1], regex[2]).test( iExact );
408
+ } catch (error) {
409
+ matches = false;
381
410
  }
382
- if (wo.filter_hideFilters){
383
- // show/hide filter row as needed
384
- $t.find('.tablesorter-filter-row').trigger( cv === '' ? 'mouseleave' : 'mouseenter' );
411
+ return matches;
412
+ }
413
+ return null;
414
+ },
415
+ // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric
416
+ exact: function( filter, iFilter, exact, iExact ) {
417
+ /*jshint eqeqeq:false */
418
+ if ( iFilter.replace(ts.filter.regex.exact, '') == iExact ) {
419
+ return true;
420
+ }
421
+ return null;
422
+ },
423
+ // Look for a not match
424
+ notMatch: function( filter, iFilter, exact, iExact, cached, index, table, wo ) {
425
+ if ( /^\!/.test(iFilter) ) {
426
+ iFilter = iFilter.replace('!', '');
427
+ var indx = iExact.search( $.trim(iFilter) );
428
+ return iFilter === '' ? true : !(wo.filter_startsWith ? indx === 0 : indx >= 0);
429
+ }
430
+ return null;
431
+ },
432
+ // Look for operators >, >=, < or <=
433
+ operators: function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) {
434
+ if ( /^[<>]=?/.test(iFilter) ) {
435
+ var cachedValue, result,
436
+ c = table.config,
437
+ query = ts.formatFloat( iFilter.replace(ts.filter.regex.operators, ''), table ),
438
+ parser = c.parsers[index],
439
+ savedSearch = query;
440
+ // parse filter value in case we're comparing numbers (dates)
441
+ if (parsed[index] || parser.type === 'numeric') {
442
+ cachedValue = parser.format( '' + iFilter.replace(ts.filter.regex.operators, ''), table, c.$headers.eq(index), index );
443
+ query = ( typeof query === "number" && cachedValue !== '' && !isNaN(cachedValue) ) ? cachedValue : query;
385
444
  }
386
- // return if the last search is the same; but filter === false when updating the search
387
- // see example-widget-filter.html filter toggle buttons
388
- if (last === cv && filter !== false) { return; }
389
- $t.trigger('filterStart', [v]);
390
- if (c.showProcessing) {
391
- // give it time for the processing icon to kick in
392
- setTimeout(function(){
393
- findRows(filter, v, cv);
394
- return false;
395
- }, 30);
396
- } else {
397
- findRows(filter, v, cv);
398
- return false;
445
+ // iExact may be numeric - see issue #149;
446
+ // check if cached is defined, because sometimes j goes out of range? (numeric columns)
447
+ cachedValue = ( parsed[index] || parser.type === 'numeric' ) && !isNaN(query) && cached ? cached :
448
+ isNaN(iExact) ? ts.formatFloat( iExact.replace(ts.filter.regex.nondigit, ''), table) :
449
+ ts.formatFloat( iExact, table );
450
+ if ( />/.test(iFilter) ) { result = />=/.test(iFilter) ? cachedValue >= query : cachedValue > query; }
451
+ if ( /</.test(iFilter) ) { result = /<=/.test(iFilter) ? cachedValue <= query : cachedValue < query; }
452
+ // keep showing all rows if nothing follows the operator
453
+ if ( !result && savedSearch === '' ) { result = true; }
454
+ return result;
455
+ }
456
+ return null;
457
+ },
458
+ // Look for an AND or && operator (logical and)
459
+ and : function( filter, iFilter, exact, iExact ) {
460
+ if ( /\s+(AND|&&)\s+/g.test(filter) ) {
461
+ var query = iFilter.split( /(?:\s+(?:and|&&)\s+)/g ),
462
+ result = iExact.search( $.trim(query[0]) ) >= 0,
463
+ indx = query.length - 1;
464
+ while (result && indx) {
465
+ result = result && iExact.search( $.trim(query[indx]) ) >= 0;
466
+ indx--;
399
467
  }
400
- },
401
- findRows = function(filter, v, cv){
402
- var $tb, $tr, $td, cr, r, l, ff, fr, time, r1, r2, searchFiltered;
403
- if (c.debug) { time = new Date(); }
404
- for (k = 0; k < b.length; k++ ){
405
- if (b.eq(k).hasClass(ts.css.info)) { continue; } // ignore info blocks, issue #264
406
- $tb = ts.processTbody(table, b.eq(k), true);
407
- $tr = $tb.children('tr:not(.' + c.cssChildRow + ')');
408
- l = $tr.length;
409
- if (cv === '' || wo.filter_serversideFiltering){
410
- $tb.children().show().removeClass(wo.filter_filteredRow);
411
- } else {
412
- // optimize searching only through already filtered rows - see #313
413
- searchFiltered = true;
414
- r = $t.data('lastSearch') || [];
415
- $.each(v, function(i,val){
416
- // check for changes from beginning of filter; but ignore if there is a logical "or" in the string
417
- searchFiltered = (val || '').indexOf(r[i] || '') === 0 && searchFiltered && !/(\s+or\s+|\|)/g.test(val || '');
418
- });
419
- // can't search when all rows are hidden - this happens when looking for exact matches
420
- if (searchFiltered && $tr.filter(':visible').length === 0) { searchFiltered = false; }
421
- // loop through the rows
422
- for (j = 0; j < l; j++){
423
- r = $tr[j].className;
424
- // skip child rows & already filtered rows
425
- if ( wo.filter_regex.child.test(r) || (searchFiltered && wo.filter_regex.filtered.test(r)) ) { continue; }
426
- r = true;
427
- cr = $tr.eq(j).nextUntil('tr:not(.' + c.cssChildRow + ')');
428
- // so, if "table.config.widgetOptions.filter_childRows" is true and there is
429
- // a match anywhere in the child row, then it will make the row visible
430
- // checked here so the option can be changed dynamically
431
- t = (cr.length && wo.filter_childRows) ? cr.text() : '';
432
- t = wo.filter_ignoreCase ? t.toLocaleLowerCase() : t;
433
- $td = $tr.eq(j).children('td');
434
- for (i = 0; i < cols; i++){
435
- // ignore if filter is empty or disabled
436
- if (v[i]){
437
- // check if column data should be from the cell or from parsed data
438
- if (wo.filter_useParsedData || parsed[i]){
439
- x = c.cache[k].normalized[j][i];
440
- } else {
441
- // using older or original tablesorter
442
- x = $.trim($td.eq(i).text());
443
- }
444
- xi = !wo.filter_regex.type.test(typeof x) && wo.filter_ignoreCase ? x.toLocaleLowerCase() : x;
445
- ff = r; // if r is true, show that row
446
- // replace accents - see #357
447
- v[i] = c.sortLocaleCompare ? ts.replaceAccents(v[i]) : v[i];
448
- // val = case insensitive, v[i] = case sensitive
449
- val = wo.filter_ignoreCase ? v[i].toLocaleLowerCase() : v[i];
450
- if (wo.filter_functions && wo.filter_functions[i]){
451
- if (wo.filter_functions[i] === true){
452
- // default selector; no "filter-select" class
453
- ff = ($ths.filter('[data-column="' + i + '"]:last').hasClass('filter-match')) ? xi.search(val) >= 0 : v[i] === x;
454
- } else if (typeof wo.filter_functions[i] === 'function'){
455
- // filter callback( exact cell content, parser normalized content, filter input value, column index )
456
- ff = wo.filter_functions[i](x, c.cache[k].normalized[j][i], v[i], i, $tr.eq(j));
457
- } else if (typeof wo.filter_functions[i][v[i]] === 'function'){
458
- // selector option function
459
- ff = wo.filter_functions[i][v[i]](x, c.cache[k].normalized[j][i], v[i], i, $tr.eq(j));
460
- }
461
- // Look for regex
462
- } else if (wo.filter_regex.regex.test(val)){
463
- rg = wo.filter_regex.regex.exec(val);
464
- try {
465
- ff = new RegExp(rg[1], rg[2]).test(xi);
466
- } catch (err){
467
- ff = false;
468
- }
469
- // Look for quotes or equals to get an exact match; ignore type since xi could be numeric
470
- /*jshint eqeqeq:false */
471
- } else if (val.replace(wo.filter_regex.exact, '') == xi){
472
- ff = true;
473
- // Look for a not match
474
- } else if (/^\!/.test(val)){
475
- val = val.replace('!','');
476
- s = xi.search($.trim(val));
477
- ff = val === '' ? true : !(wo.filter_startsWith ? s === 0 : s >= 0);
478
- // Look for operators >, >=, < or <=
479
- } else if (/^[<>]=?/.test(val)){
480
- s = fr = fmt(val.replace(wo.filter_regex.nondigit, '').replace(wo.filter_regex.operators,''), table);
481
- // parse filter value in case we're comparing numbers (dates)
482
- if (parsed[i] || c.parsers[i].type === 'numeric') {
483
- rg = c.parsers[i].format('' + val.replace(wo.filter_regex.operators,''), table, $ths.eq(i), i);
484
- s = (isNaN(s) && rg !== '' && !isNaN(rg)) ? rg : s;
485
- }
486
- // xi may be numeric - see issue #149;
487
- // check if c.cache[k].normalized[j] is defined, because sometimes j goes out of range? (numeric columns)
488
- rg = ( parsed[i] || c.parsers[i].type === 'numeric' ) && !isNaN(s) && c.cache[k].normalized[j] ? c.cache[k].normalized[j][i] :
489
- isNaN(xi) ? fmt(xi.replace(wo.filter_regex.nondigit, ''), table) : fmt(xi, table);
490
- if (/>/.test(val)) { ff = />=/.test(val) ? rg >= s : rg > s; }
491
- if (/</.test(val)) { ff = /<=/.test(val) ? rg <= s : rg < s; }
492
- if (!ff && fr === '') { ff = true; } // keep showing all rows if nothing follows the operator
493
- // Look for an AND or && operator (logical and)
494
- } else if (/\s+(AND|&&)\s+/g.test(v[i])) {
495
- s = val.split(/(?:\s+(?:and|&&)\s+)/g);
496
- ff = xi.search($.trim(s[0])) >= 0;
497
- r1 = s.length - 1;
498
- while (ff && r1) {
499
- ff = ff && xi.search($.trim(s[r1])) >= 0;
500
- r1--;
501
- }
502
- // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu!
503
- } else if (/\s+(-|to)\s+/.test(val)){
504
- s = val.split(/(?: - | to )/); // make sure the dash is for a range and not indicating a negative number
505
- r1 = fmt(s[0].replace(wo.filter_regex.nondigit, ''), table);
506
- r2 = fmt(s[1].replace(wo.filter_regex.nondigit, ''), table);
507
- // parse filter value in case we're comparing numbers (dates)
508
- if (parsed[i] || c.parsers[i].type === 'numeric') {
509
- rg = c.parsers[i].format('' + s[0], table, $ths.eq(i), i);
510
- r1 = (rg !== '' && !isNaN(rg)) ? rg : r1;
511
- rg = c.parsers[i].format('' + s[1], table, $ths.eq(i), i);
512
- r2 = (rg !== '' && !isNaN(rg)) ? rg : r2;
513
- }
514
- rg = ( parsed[i] || c.parsers[i].type === 'numeric' ) && !isNaN(r1) && !isNaN(r2) ? c.cache[k].normalized[j][i] :
515
- isNaN(xi) ? fmt(xi.replace(wo.filter_regex.nondigit, ''), table) : fmt(xi, table);
516
- if (r1 > r2) { ff = r1; r1 = r2; r2 = ff; } // swap
517
- ff = (rg >= r1 && rg <= r2) || (r1 === '' || r2 === '') ? true : false;
518
- // Look for wild card: ? = single, * = multiple, or | = logical OR
519
- } else if ( /[\?|\*]/.test(val) || /\s+OR\s+/.test(v[i]) ){
520
- s = val.replace(/\s+OR\s+/gi,"|");
521
- // look for an exact match with the "or" unless the "filter-match" class is found
522
- if (!$ths.filter('[data-column="' + i + '"]:last').hasClass('filter-match') && /\|/.test(s)) {
523
- s = '^(' + s + ')$';
524
- }
525
- ff = new RegExp( s.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(xi);
526
- // Look for match, and add child row data for matching
527
- } else {
528
- x = (xi + t).indexOf(val);
529
- ff = ( (!wo.filter_startsWith && x >= 0) || (wo.filter_startsWith && x === 0) );
530
- }
531
- r = (ff) ? (r ? true : false) : false;
532
- }
533
- }
534
- $tr[j].style.display = (r ? '' : 'none');
535
- $tr.eq(j)[r ? 'removeClass' : 'addClass'](wo.filter_filteredRow);
536
- if (cr.length) {
537
- if (c.pager && c.pager.countChildRows || wo.pager_countChildRows) {
538
- cr[r ? 'removeClass' : 'addClass'](wo.filter_filteredRow); // see issue #396
539
- }
540
- cr[r ? 'show' : 'hide']();
541
- }
542
- }
468
+ return result;
469
+ }
470
+ return null;
471
+ },
472
+ // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu!
473
+ range : function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) {
474
+ if ( /\s+(-|to)\s+/.test(iFilter) ) {
475
+ var result,
476
+ c = table.config,
477
+ query = iFilter.split(/(?: - | to )/), // make sure the dash is for a range and not indicating a negative number
478
+ range1 = ts.formatFloat(query[0].replace(ts.filter.regex.nondigit, ''), table),
479
+ range2 = ts.formatFloat(query[1].replace(ts.filter.regex.nondigit, ''), table);
480
+ // parse filter value in case we're comparing numbers (dates)
481
+ if (parsed[index] || c.parsers[index].type === 'numeric') {
482
+ result = c.parsers[index].format('' + query[0], table, c.$headers.eq(index), index);
483
+ range1 = (result !== '' && !isNaN(result)) ? result : range1;
484
+ result = c.parsers[index].format('' + query[1], table, c.$headers.eq(index), index);
485
+ range2 = (result !== '' && !isNaN(result)) ? result : range2;
486
+ }
487
+ result = ( parsed[index] || c.parsers[index].type === 'numeric' ) && !isNaN(range1) && !isNaN(range2) ? cached :
488
+ isNaN(iExact) ? ts.formatFloat( iExact.replace(ts.filter.regex.nondigit, ''), table) :
489
+ ts.formatFloat( iExact, table );
490
+ if (range1 > range2) { result = range1; range1 = range2; range2 = result; } // swap
491
+ return (result >= range1 && result <= range2) || (range1 === '' || range2 === '');
492
+ }
493
+ return null;
494
+ },
495
+ // Look for wild card: ? = single, * = multiple, or | = logical OR
496
+ wild : function( filter, iFilter, exact, iExact, cached, index, table ) {
497
+ if ( /[\?|\*]/.test(iFilter) || /\s+OR\s+/.test(filter) ) {
498
+ var c = table.config,
499
+ query = iFilter.replace(/\s+OR\s+/gi,"|");
500
+ // look for an exact match with the "or" unless the "filter-match" class is found
501
+ if (!c.$headers.filter('[data-column="' + index + '"]:last').hasClass('filter-match') && /\|/.test(query)) {
502
+ query = '^(' + query + ')$';
503
+ }
504
+ return new RegExp( query.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(iExact);
505
+ }
506
+ return null;
507
+ },
508
+ // fuzzy text search; modified from https://github.com/mattyork/fuzzy (MIT license)
509
+ fuzzy: function( filter, iFilter, exact, iExact ) {
510
+ if ( /^~/.test(iFilter) ) {
511
+ var indx,
512
+ patternIndx = 0,
513
+ len = iExact.length,
514
+ pattern = iFilter.slice(1);
515
+ for (indx = 0; indx < len; indx++) {
516
+ if (iExact[indx] === pattern[patternIndx]) {
517
+ patternIndx += 1;
543
518
  }
544
- ts.processTbody(table, $tb, false);
545
519
  }
546
- last = cv; // save last search
547
- $t.data('lastSearch', v);
548
- if (c.debug){
549
- ts.benchmark("Completed filter widget search", time);
520
+ if (patternIndx === pattern.length) {
521
+ return true;
550
522
  }
551
- $t.trigger('applyWidgets'); // make sure zebra widget is applied
552
- $t.trigger('filterEnd');
553
- },
554
- buildSelect = function(i, updating, onlyavail){
555
- var o, t, arry = [], currentVal;
556
- i = parseInt(i, 10);
557
- t = $ths.filter('[data-column="' + i + '"]:last');
558
- // t.data('placeholder') won't work in jQuery older than 1.4.3
559
- o = '<option value="">' + (t.data('placeholder') || t.attr('data-placeholder') || '') + '</option>';
560
- for (k = 0; k < b.length; k++ ){
561
- l = c.cache[k].row.length;
562
- // loop through the rows
563
- for (j = 0; j < l; j++){
564
- // check if has class filtered
565
- if (onlyavail && c.cache[k].row[j][0].className.match(wo.filter_filteredRow)) { continue; }
566
- // get non-normalized cell content
567
- if (wo.filter_useParsedData){
568
- arry.push( '' + c.cache[k].normalized[j][i] );
569
- } else {
570
- t = c.cache[k].row[j][0].cells[i];
571
- if (t){
572
- arry.push( $.trim(c.supportsTextContent ? t.textContent : $(t).text()) );
523
+ return false;
524
+ }
525
+ return null;
526
+ }
527
+ },
528
+ init: function(table, c, wo) {
529
+ var options, string, $header, column, filters, time;
530
+ if (c.debug) {
531
+ time = new Date();
532
+ }
533
+ c.$table.addClass('hasFilters');
534
+
535
+ ts.filter.regex.child = new RegExp(c.cssChildRow);
536
+ ts.filter.regex.filtered = new RegExp(wo.filter_filteredRow);
537
+
538
+ // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156
539
+ if (wo.filter_columnFilters !== false && c.$headers.filter('.filter-false').length !== c.$headers.length) {
540
+ // build filter row
541
+ ts.filter.buildRow(table, c, wo);
542
+ }
543
+
544
+ c.$table.bind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join('.tsfilter '), function(event, filter) {
545
+ if ( !/(search|filterReset|filterEnd)/.test(event.type) ) {
546
+ event.stopPropagation();
547
+ ts.filter.buildDefault(table, true);
548
+ }
549
+ if (event.type === 'filterReset') {
550
+ ts.filter.searching(table, []);
551
+ }
552
+ if (event.type === 'filterEnd') {
553
+ ts.filter.buildDefault(table, true);
554
+ } else {
555
+ // send false argument to force a new search; otherwise if the filter hasn't changed, it will return
556
+ filter = event.type === 'search' ? filter : event.type === 'updateComplete' ? c.$table.data('lastSearch') : '';
557
+ ts.filter.searching(table, filter);
558
+ }
559
+ return false;
560
+ });
561
+ ts.filter.bindSearch( table, c.$table.find('input.tablesorter-filter') );
562
+
563
+ // reset button/link
564
+ if (wo.filter_reset) {
565
+ $(document).delegate(wo.filter_reset, 'click.tsfilter', function() {
566
+ ts.filter.searching(table, []);
567
+ });
568
+ }
569
+ if (wo.filter_functions) {
570
+ // column = column # (string)
571
+ for (column in wo.filter_functions) {
572
+ if (wo.filter_functions.hasOwnProperty(column) && typeof column === 'string') {
573
+ $header = c.$headers.filter('[data-column="' + column + '"]:last');
574
+ options = '';
575
+ if (wo.filter_functions[column] === true && !$header.hasClass('filter-false')) {
576
+ ts.filter.buildSelect(column);
577
+ } else if (typeof column === 'string' && !$header.hasClass('filter-false')) {
578
+ // add custom drop down list
579
+ for (string in wo.filter_functions[column]) {
580
+ if (typeof string === 'string') {
581
+ options += options === '' ?
582
+ '<option value="">' + ($header.data('placeholder') || $header.attr('data-placeholder') || '') + '</option>' : '';
583
+ options += '<option value="' + string + '">' + string + '</option>';
573
584
  }
574
585
  }
586
+ c.$table.find('thead').find('select.tablesorter-filter[data-column="' + column + '"]').append(options);
575
587
  }
576
588
  }
589
+ }
590
+ }
591
+ // not really updating, but if the column has both the "filter-select" class & filter_functions set to true,
592
+ // it would append the same options twice.
593
+ ts.filter.buildDefault(table, true);
577
594
 
578
- // get unique elements and sort the list
579
- // if $.tablesorter.sortText exists (not in the original tablesorter),
580
- // then natural sort the list otherwise use a basic sort
581
- arry = $.grep(arry, function(v, k){
582
- return $.inArray(v, arry) === k;
583
- });
584
- arry = (ts.sortNatural) ? arry.sort(function(a, b){ return ts.sortNatural(a, b); }) : arry.sort(true);
595
+ c.$table.find('select.tablesorter-filter').bind('change search', function(event, filter) {
596
+ ts.filter.checkFilters(table, filter);
597
+ });
585
598
 
586
- // Get curent filter value
587
- currentVal = $t.find('thead').find('select.tablesorter-filter[data-column="' + i + '"]').val();
599
+ if (wo.filter_hideFilters) {
600
+ ts.filter.hideFilters(table, c, wo);
601
+ }
588
602
 
589
- // build option list
590
- for (k = 0; k < arry.length; k++){
591
- t = arry[k].replace(/\"/g, "&quot;");
592
- // replace quotes - fixes #242 & ignore empty strings - see http://stackoverflow.com/q/14990971/145346
593
- o += arry[k] !== '' ? '<option value="' + t + '"' + (currentVal === t ? ' selected="selected"' : '') +'>' + arry[k] + '</option>' : '';
594
- }
595
- $t.find('thead').find('select.tablesorter-filter[data-column="' + i + '"]')[ updating ? 'html' : 'append' ](o);
596
- },
597
- buildDefault = function(updating){
598
- // build default select dropdown
599
- for (i = 0; i < cols; i++){
600
- t = $ths.filter('[data-column="' + i + '"]:last');
601
- // look for the filter-select class; build/update it if found
602
- if ((t.hasClass('filter-select') || wo.filter_functions && wo.filter_functions[i] === true) && !t.hasClass('filter-false')){
603
- if (!wo.filter_functions) { wo.filter_functions = {}; }
604
- wo.filter_functions[i] = true; // make sure this select gets processed by filter_functions
605
- buildSelect(i, updating, t.hasClass(wo.filter_onlyAvail));
603
+ // show processing icon
604
+ if (c.showProcessing) {
605
+ c.$table.bind('filterStart.tsfilter filterEnd.tsfilter', function(event, columns) {
606
+ // only add processing to certain columns to all columns
607
+ $header = (columns) ? c.$table.find('.' + ts.css.header).filter('[data-column]').filter(function() {
608
+ return columns[$(this).data('column')] !== '';
609
+ }) : '';
610
+ ts.isProcessing(table, event.type === 'filterStart', columns ? $header : '');
611
+ });
612
+ }
613
+ if (c.debug) {
614
+ ts.benchmark("Applying Filter widget", time);
615
+ }
616
+ // add default values
617
+ c.$table.bind('tablesorter-initialized pagerInitialized', function() {
618
+ filters = ts.filter.setDefaults(table, c, wo) || [];
619
+ if (filters.length) {
620
+ ts.setFilters(table, filters, true);
621
+ }
622
+ });
623
+ // filter widget initialized
624
+ wo.filter_Initialized = true;
625
+ c.$table.trigger('filterInit');
626
+ ts.filter.checkFilters(table);
627
+ },
628
+ setDefaults: function(table, c, wo){
629
+ var indx,
630
+ filters = [],
631
+ columns = c.columns;
632
+ for (indx = 0; indx < columns; indx++) {
633
+ filters[indx] = c.$headers.filter('[data-column="' + indx + '"]:last').attr(wo.filter_defaultAttrib) || filters[indx];
634
+ }
635
+ $(table).data('lastSearch', filters);
636
+ return filters;
637
+ },
638
+ buildRow: function(table, c, wo) {
639
+ var column, $header, buildSelect, disabled,
640
+ // c.columns defined in computeThIndexes()
641
+ columns = c.columns,
642
+ buildFilter = '<tr class="tablesorter-filter-row">';
643
+ for (column = 0; column < columns; column++) {
644
+ buildFilter += '<td></td>';
645
+ }
646
+ c.$filters = $(buildFilter += '</tr>').appendTo( c.$table.find('thead').eq(0) ).find('td');
647
+ // build each filter input
648
+ for (column = 0; column < columns; column++) {
649
+ disabled = false;
650
+ // assuming last cell of a column is the main column
651
+ $header = c.$headers.filter('[data-column="' + column + '"]:last');
652
+ buildSelect = (wo.filter_functions && wo.filter_functions[column] && typeof wo.filter_functions[column] !== 'function') ||
653
+ $header.hasClass('filter-select');
654
+ // get data from jQuery data, metadata, headers option or header class name
655
+ if (ts.getData) {
656
+ // get data from jQuery data, metadata, headers option or header class name
657
+ disabled = ts.getData($header[0], c.headers[column], 'filter') === 'false';
658
+ } else {
659
+ // only class names and header options - keep this for compatibility with tablesorter v2.0.5
660
+ disabled = (c.headers[column] && c.headers[column].hasOwnProperty('filter') && c.headers[column].filter === false) ||
661
+ $header.hasClass('filter-false');
662
+ }
663
+ if (buildSelect) {
664
+ buildFilter = $('<select>').appendTo( c.$filters.eq(column) );
665
+ } else {
666
+ if (wo.filter_formatter && $.isFunction(wo.filter_formatter[column])) {
667
+ buildFilter = wo.filter_formatter[column]( c.$filters.eq(column), column );
668
+ // no element returned, so lets go find it
669
+ if (buildFilter && buildFilter.length === 0) {
670
+ buildFilter = c.$filters.eq(column).children('input');
671
+ }
672
+ // element not in DOM, so lets attach it
673
+ if ( buildFilter && (buildFilter.parent().length === 0 ||
674
+ (buildFilter.parent().length && buildFilter.parent()[0] !== c.$filters[column])) ) {
675
+ c.$filters.eq(column).append(buildFilter);
606
676
  }
607
- }
608
- },
609
- searching = function(filter){
610
- if (typeof filter === 'undefined' || filter === true){
611
- // delay filtering
612
- clearTimeout(timer);
613
- timer = setTimeout(function(){
614
- checkFilters(filter);
615
- }, wo.filter_liveSearch ? wo.filter_searchDelay : 10);
616
677
  } else {
617
- // skip delay
618
- checkFilters(filter);
678
+ buildFilter = $('<input type="search">').appendTo( c.$filters.eq(column) );
679
+ }
680
+ if (buildFilter) {
681
+ buildFilter.attr('placeholder', $header.data('placeholder') || $header.attr('data-placeholder') || '');
619
682
  }
620
- };
621
- if (c.debug){
622
- time = new Date();
623
683
  }
624
- wo.filter_regex.child = new RegExp(c.cssChildRow);
625
- wo.filter_regex.filtered = new RegExp(wo.filter_filteredRow);
626
- // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156
627
-
628
- if (wo.filter_columnFilters !== false && $ths.filter('.filter-false').length !== $ths.length){
629
- // build filter row
630
- t = '<tr class="tablesorter-filter-row">';
631
- for (i = 0; i < cols; i++){
632
- t += '<td></td>';
684
+ if (buildFilter) {
685
+ buildFilter.addClass('tablesorter-filter ' + wo.filter_cssFilter).attr('data-column', column);
686
+ if (disabled) {
687
+ buildFilter.addClass('disabled')[0].disabled = true; // disabled!
633
688
  }
634
- c.$filters = $(t += '</tr>').appendTo( $t.find('thead').eq(0) ).find('td');
635
- // build each filter input
636
- for (i = 0; i < cols; i++){
637
- dis = false;
638
- $th = $ths.filter('[data-column="' + i + '"]:last'); // assuming last cell of a column is the main column
639
- sel = (wo.filter_functions && wo.filter_functions[i] && typeof wo.filter_functions[i] !== 'function') || $th.hasClass('filter-select');
640
- // use header option - headers: { 1: { filter: false } } OR add class="filter-false"
641
- if (ts.getData){
642
- // get data from jQuery data, metadata, headers option or header class name
643
- dis = ts.getData($th[0], c.headers[i], 'filter') === 'false';
644
- } else {
645
- // only class names and header options - keep this for compatibility with tablesorter v2.0.5
646
- dis = (c.headers[i] && c.headers[i].hasOwnProperty('filter') && c.headers[i].filter === false) || $th.hasClass('filter-false');
647
- }
648
-
649
- if (sel){
650
- t = $('<select>').appendTo( c.$filters.eq(i) );
689
+ }
690
+ }
691
+ },
692
+ bindSearch: function(table, $el) {
693
+ table = $(table)[0];
694
+ var external, wo = table.config.widgetOptions;
695
+ $el.bind('keyup search', function(event, filter) {
696
+ // emulate what webkit does.... escape clears the filter
697
+ if (event.which === 27) {
698
+ this.value = '';
699
+ // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace
700
+ } else if ( (typeof wo.filter_liveSearch === 'number' && this.value.length < wo.filter_liveSearch && this.value !== '') ||
701
+ ( event.type === 'keyup' && ( (event.which < 32 && event.which !== 8 && wo.filter_liveSearch === true && event.which !== 13) ||
702
+ ( event.which >= 37 && event.which <= 40 ) || (event.which !== 13 && wo.filter_liveSearch === false) ) ) ) {
703
+ return;
704
+ }
705
+ // external searches won't have a filter parameter, so grab the value
706
+ external = $(this).hasClass('tablesorter-filter') ? filter : [ $(this).val() ];
707
+ ts.filter.searching(table, filter, external);
708
+ });
709
+ },
710
+ checkFilters: function(table, filter) {
711
+ var c = table.config,
712
+ wo = c.widgetOptions,
713
+ filterArray = $.isArray(filter),
714
+ filters = (filterArray) ? filter : ts.getFilters(table),
715
+ combinedFilters = (filters || []).join(''); // combined filter values
716
+ // add filter array back into inputs
717
+ if (filterArray) {
718
+ ts.setFilters( table, filters );
719
+ }
720
+ if (wo.filter_hideFilters) {
721
+ // show/hide filter row as needed
722
+ c.$table.find('.tablesorter-filter-row').trigger( combinedFilters === '' ? 'mouseleave' : 'mouseenter' );
723
+ }
724
+ // return if the last search is the same; but filter === false when updating the search
725
+ // see example-widget-filter.html filter toggle buttons
726
+ if (c.lastCombinedFilter === combinedFilters && filter !== false) { return; }
727
+ c.$table.trigger('filterStart', [filters]);
728
+ if (c.showProcessing) {
729
+ // give it time for the processing icon to kick in
730
+ setTimeout(function() {
731
+ ts.filter.findRows(table, filters, combinedFilters);
732
+ return false;
733
+ }, 30);
734
+ } else {
735
+ ts.filter.findRows(table, filters, combinedFilters);
736
+ return false;
737
+ }
738
+ },
739
+ hideFilters: function(table, c, wo) {
740
+ var $filterRow, $filterRow2, timer;
741
+ c.$table
742
+ .find('.tablesorter-filter-row')
743
+ .addClass('hideme')
744
+ .bind('mouseenter mouseleave', function(e) {
745
+ // save event object - http://bugs.jquery.com/ticket/12140
746
+ var event = e;
747
+ $filterRow = $(this);
748
+ clearTimeout(timer);
749
+ timer = setTimeout(function() {
750
+ if ( /enter|over/.test(event.type) ) {
751
+ $filterRow.removeClass('hideme');
651
752
  } else {
652
- if (wo.filter_formatter && $.isFunction(wo.filter_formatter[i])) {
653
- t = wo.filter_formatter[i]( c.$filters.eq(i), i );
654
- // no element returned, so lets go find it
655
- if (t && t.length === 0) { t = c.$filters.eq(i).children('input'); }
656
- // element not in DOM, so lets attach it
657
- if (t && (t.parent().length === 0 || (t.parent().length && t.parent()[0] !== c.$filters[i]))) {
658
- c.$filters.eq(i).append(t);
753
+ // don't hide if input has focus
754
+ // $(':focus') needs jQuery 1.6+
755
+ if ( $(document.activeElement).closest('tr')[0] !== $filterRow[0] ) {
756
+ // don't hide row if any filter has a value
757
+ if (ts.getFilters(table).join('') === '') {
758
+ $filterRow.addClass('hideme');
659
759
  }
660
- } else {
661
- t = $('<input type="search">').appendTo( c.$filters.eq(i) );
662
- }
663
- if (t) {
664
- t.attr('placeholder', $th.data('placeholder') || $th.attr('data-placeholder') || '');
665
- }
666
- }
667
- if (t) {
668
- t.addClass('tablesorter-filter ' + wo.filter_cssFilter).attr('data-column', i);
669
- if (dis) {
670
- t.addClass('disabled')[0].disabled = true; // disabled!
671
760
  }
672
761
  }
673
- }
674
- }
675
- $t
676
- .bind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join('.tsfilter '), function(e, filter){
677
- if (!/(search|filterReset|filterEnd)/.test(e.type)){
678
- e.stopPropagation();
679
- buildDefault(true);
680
- }
681
- if (e.type === 'filterReset') {
682
- searching([]);
683
- }
684
- if (e.type === 'filterEnd') {
685
- buildDefault(true);
686
- } else {
687
- // send false argument to force a new search; otherwise if the filter hasn't changed, it will return
688
- filter = e.type === 'search' ? filter : e.type === 'updateComplete' ? $t.data('lastSearch') : '';
689
- searching(filter);
690
- }
691
- return false;
762
+ }, 200);
692
763
  })
693
- .find('input.tablesorter-filter').bind('keyup search', function(e, filter){
694
- // emulate what webkit does.... escape clears the filter
695
- if (e.which === 27) {
696
- this.value = '';
697
- // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace
698
- } else if ( (typeof wo.filter_liveSearch === 'number' && this.value.length < wo.filter_liveSearch && this.value !== '') || ( e.type === 'keyup' &&
699
- ( (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) ) ) ) {
700
- return;
701
- }
702
- searching(filter);
764
+ .find('input, select').bind('focus blur', function(e) {
765
+ $filterRow2 = $(this).closest('tr');
766
+ clearTimeout(timer);
767
+ var event = e;
768
+ timer = setTimeout(function() {
769
+ // don't hide row if any filter has a value
770
+ if (ts.getFilters(table).join('') === '') {
771
+ $filterRow2[ event.type === 'focus' ? 'removeClass' : 'addClass']('hideme');
772
+ }
773
+ }, 200);
703
774
  });
704
-
775
+ },
776
+ findRows: function(table, filters, combinedFilters) {
777
+ var cached, len, $rows, rowIndex, tbodyIndex, $tbody, $cells, columnIndex,
778
+ childRow, childRowText, exact, iExact, iFilter, lastSearch, matches, result,
779
+ searchFiltered, filterMatched, showRow, time,
780
+ c = table.config,
781
+ wo = c.widgetOptions,
782
+ columns = c.columns,
783
+ $tbodies = c.$tbodies,
784
+ // anyMatch really screws up with these types of filters
785
+ anyMatchNotAllowedTypes = [ 'range', 'operators' ],
705
786
  // parse columns after formatter, in case the class is added at that point
706
- parsed = $ths.map(function(i){
707
- return (ts.getData) ? ts.getData($ths.filter('[data-column="' + i + '"]:last'), c.headers[i], 'filter') === 'parsed' : $(this).hasClass('filter-parsed');
787
+ parsed = c.$headers.map(function(columnIndex) {
788
+ return (ts.getData) ?
789
+ ts.getData(c.$headers.filter('[data-column="' + columnIndex + '"]:last'), c.headers[columnIndex], 'filter') === 'parsed' :
790
+ $(this).hasClass('filter-parsed');
708
791
  }).get();
709
-
710
- // reset button/link
711
- if (wo.filter_reset){
712
- $(document).delegate(wo.filter_reset, 'click.tsfilter', function(){
713
- $t.trigger('filterReset');
792
+ if (c.debug) { time = new Date(); }
793
+ for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
794
+ if ($tbodies.eq(tbodyIndex).hasClass(ts.css.info)) { continue; } // ignore info blocks, issue #264
795
+ $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true);
796
+ $rows = $tbody.children('tr').not('.' + c.cssChildRow);
797
+ len = $rows.length;
798
+ if (combinedFilters === '' || wo.filter_serversideFiltering) {
799
+ $tbody.children().show().removeClass(wo.filter_filteredRow);
800
+ } else {
801
+ // optimize searching only through already filtered rows - see #313
802
+ searchFiltered = true;
803
+ lastSearch = c.lastSearch || c.$table.data('lastSearch') || [];
804
+ $.each(filters, function(indx, val) {
805
+ // check for changes from beginning of filter; but ignore if there is a logical "or" in the string
806
+ searchFiltered = (val || '').indexOf(lastSearch[indx] || '') === 0 && searchFiltered && !/(\s+or\s+|\|)/g.test(val || '');
714
807
  });
715
- }
716
- if (wo.filter_functions){
717
- // i = column # (string)
718
- for (col in wo.filter_functions){
719
- if (wo.filter_functions.hasOwnProperty(col) && typeof col === 'string'){
720
- t = $ths.filter('[data-column="' + col + '"]:last');
721
- ff = '';
722
- if (wo.filter_functions[col] === true && !t.hasClass('filter-false')){
723
- buildSelect(col);
724
- } else if (typeof col === 'string' && !t.hasClass('filter-false')){
725
- // add custom drop down list
726
- for (str in wo.filter_functions[col]){
727
- if (typeof str === 'string'){
728
- ff += ff === '' ? '<option value="">' + (t.data('placeholder') || t.attr('data-placeholder') || '') + '</option>' : '';
729
- ff += '<option value="' + str + '">' + str + '</option>';
730
- }
808
+ // can't search when all rows are hidden - this happens when looking for exact matches
809
+ if (searchFiltered && $rows.filter(':visible').length === 0) { searchFiltered = false; }
810
+ // loop through the rows
811
+ for (rowIndex = 0; rowIndex < len; rowIndex++) {
812
+ childRow = $rows[rowIndex].className;
813
+ // skip child rows & already filtered rows
814
+ if ( ts.filter.regex.child.test(childRow) || (searchFiltered && ts.filter.regex.filtered.test(childRow)) ) { continue; }
815
+ showRow = true;
816
+ // *** nextAll/nextUntil not supported by Zepto! ***
817
+ childRow = $rows.eq(rowIndex).nextUntil('tr:not(.' + c.cssChildRow + ')');
818
+ // so, if "table.config.widgetOptions.filter_childRows" is true and there is
819
+ // a match anywhere in the child row, then it will make the row visible
820
+ // checked here so the option can be changed dynamically
821
+ childRowText = (childRow.length && wo.filter_childRows) ? childRow.text() : '';
822
+ childRowText = wo.filter_ignoreCase ? childRowText.toLocaleLowerCase() : childRowText;
823
+ $cells = $rows.eq(rowIndex).children('td');
824
+ for (columnIndex = 0; columnIndex < columns; columnIndex++) {
825
+ // ignore if filter is empty or disabled
826
+ if (filters[columnIndex] || wo.filter_anyMatch) {
827
+ cached = c.cache[tbodyIndex].normalized[rowIndex][columnIndex];
828
+ // check if column data should be from the cell or from parsed data
829
+ if (wo.filter_useParsedData || parsed[columnIndex]) {
830
+ exact = cached;
831
+ } else {
832
+ // using older or original tablesorter
833
+ exact = $.trim($cells.eq(columnIndex).text());
834
+ exact = c.sortLocaleCompare ? ts.replaceAccents(exact) : exact; // issue #405
731
835
  }
732
- $t.find('thead').find('select.tablesorter-filter[data-column="' + col + '"]').append(ff);
733
- }
734
- }
735
- }
736
- }
737
- // not really updating, but if the column has both the "filter-select" class & filter_functions set to true,
738
- // it would append the same options twice.
739
- buildDefault(true);
836
+ iExact = !ts.filter.regex.type.test(typeof exact) && wo.filter_ignoreCase ? exact.toLocaleLowerCase() : exact;
837
+ result = showRow; // if showRow is true, show that row
740
838
 
741
- $t.find('select.tablesorter-filter').bind('change search', function(e, filter){
742
- checkFilters(filter);
743
- });
839
+ if (typeof filters[columnIndex] === "undefined" || filters[columnIndex] === null) {
840
+ filters[columnIndex] = wo.filter_anyMatch ? combinedFilters : filters[columnIndex];
841
+ }
744
842
 
745
- if (wo.filter_hideFilters){
746
- $t
747
- .find('.tablesorter-filter-row')
748
- .addClass('hideme')
749
- .bind('mouseenter mouseleave', function(e){
750
- // save event object - http://bugs.jquery.com/ticket/12140
751
- var all, evt = e;
752
- ft = $(this);
753
- clearTimeout(st);
754
- st = setTimeout(function(){
755
- if (/enter|over/.test(evt.type)){
756
- ft.removeClass('hideme');
843
+ // replace accents - see #357
844
+ filters[columnIndex] = c.sortLocaleCompare ? ts.replaceAccents(filters[columnIndex]) : filters[columnIndex];
845
+ // val = case insensitive, filters[columnIndex] = case sensitive
846
+ iFilter = wo.filter_ignoreCase ? filters[columnIndex].toLocaleLowerCase() : filters[columnIndex];
847
+ if (wo.filter_functions && wo.filter_functions[columnIndex]) {
848
+ if (wo.filter_functions[columnIndex] === true) {
849
+ // default selector; no "filter-select" class
850
+ result = (c.$headers.filter('[data-column="' + columnIndex + '"]:last').hasClass('filter-match')) ?
851
+ iExact.search(iFilter) >= 0 : filters[columnIndex] === exact;
852
+ } else if (typeof wo.filter_functions[columnIndex] === 'function') {
853
+ // filter callback( exact cell content, parser normalized content, filter input value, column index, jQuery row object )
854
+ result = wo.filter_functions[columnIndex](exact, cached, filters[columnIndex], columnIndex, $rows.eq(rowIndex));
855
+ } else if (typeof wo.filter_functions[columnIndex][filters[columnIndex]] === 'function') {
856
+ // selector option function
857
+ result = wo.filter_functions[columnIndex][filters[columnIndex]](exact, cached, filters[columnIndex], columnIndex, $rows.eq(rowIndex));
858
+ }
757
859
  } else {
758
- // don't hide if input has focus
759
- // $(':focus') needs jQuery 1.6+
760
- if ($(document.activeElement).closest('tr')[0] !== ft[0]){
761
- // get all filter values
762
- all = ts.getFilters(table).join('');
763
- // don't hide row if any filter has a value
764
- if (all === ''){
765
- ft.addClass('hideme');
860
+ filterMatched = null;
861
+ // cycle through the different filters
862
+ // filters return a boolean or null if nothing matches
863
+ $.each(ts.filter.types, function(type, typeFunction) {
864
+ if (!wo.filter_anyMatch || (wo.filter_anyMatch && anyMatchNotAllowedTypes.indexOf(type) < 0)) {
865
+ matches = typeFunction( filters[columnIndex], iFilter, exact, iExact, cached, columnIndex, table, wo, parsed );
866
+ if (matches !== null) {
867
+ filterMatched = matches;
868
+ return false;
869
+ }
766
870
  }
871
+ });
872
+ if (filterMatched !== null) {
873
+ result = filterMatched;
874
+ // Look for match, and add child row data for matching
875
+ } else {
876
+ exact = (iExact + childRowText).indexOf(iFilter);
877
+ result = ( (!wo.filter_startsWith && exact >= 0) || (wo.filter_startsWith && exact === 0) );
767
878
  }
768
879
  }
769
- }, 200);
770
- })
771
- .find('input, select').bind('focus blur', function(e){
772
- ft2 = $(this).closest('tr');
773
- clearTimeout(st);
774
- st = setTimeout(function(){
775
- // don't hide row if any filter has a value
776
- if (ts.getFilters(table).join('') === ''){
777
- ft2[ e.type === 'focus' ? 'removeClass' : 'addClass']('hideme');
880
+ if (wo.filter_anyMatch) {
881
+ showRow = result;
882
+ if (showRow){
883
+ break;
884
+ }
885
+ } else {
886
+ showRow = (result) ? showRow : false;
778
887
  }
779
- }, 200);
780
- });
888
+ }
889
+ }
890
+ $rows[rowIndex].style.display = (showRow ? '' : 'none');
891
+ $rows.eq(rowIndex)[showRow ? 'removeClass' : 'addClass'](wo.filter_filteredRow);
892
+ if (childRow.length) {
893
+ if (c.pager && c.pager.countChildRows || wo.pager_countChildRows) {
894
+ childRow[showRow ? 'removeClass' : 'addClass'](wo.filter_filteredRow); // see issue #396
895
+ }
896
+ childRow.toggle(showRow);
897
+ }
898
+ }
781
899
  }
782
-
783
- // show processing icon
784
- if (c.showProcessing) {
785
- $t.bind('filterStart.tsfilter filterEnd.tsfilter', function(e, v) {
786
- var fc = (v) ? $t.find('.' + ts.css.header).filter('[data-column]').filter(function(){
787
- return v[$(this).data('column')] !== '';
788
- }) : '';
789
- ts.isProcessing($t[0], e.type === 'filterStart', v ? fc : '');
790
- });
900
+ ts.processTbody(table, $tbody, false);
901
+ }
902
+ c.lastCombinedFilter = combinedFilters; // save last search
903
+ c.lastSearch = filters;
904
+ c.$table.data('lastSearch', filters);
905
+ if (c.debug) {
906
+ ts.benchmark("Completed filter widget search", time);
907
+ }
908
+ c.$table.trigger('applyWidgets'); // make sure zebra widget is applied
909
+ c.$table.trigger('filterEnd');
910
+ },
911
+ buildSelect: function(table, column, updating, onlyavail) {
912
+ column = parseInt(column, 10);
913
+ var indx, rowIndex, tbodyIndex, len, currentValue, txt,
914
+ c = table.config,
915
+ wo = c.widgetOptions,
916
+ $tbodies = c.$tbodies,
917
+ arry = [],
918
+ node = c.$headers.filter('[data-column="' + column + '"]:last'),
919
+ // t.data('placeholder') won't work in jQuery older than 1.4.3
920
+ options = '<option value="">' + ( node.data('placeholder') || node.attr('data-placeholder') || '' ) + '</option>';
921
+ for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
922
+ len = c.cache[tbodyIndex].row.length;
923
+ // loop through the rows
924
+ for (rowIndex = 0; rowIndex < len; rowIndex++) {
925
+ // check if has class filtered
926
+ if (onlyavail && c.cache[tbodyIndex].row[rowIndex][0].className.match(wo.filter_filteredRow)) { continue; }
927
+ // get non-normalized cell content
928
+ if (wo.filter_useParsedData) {
929
+ arry.push( '' + c.cache[tbodyIndex].normalized[rowIndex][column] );
930
+ } else {
931
+ node = c.cache[tbodyIndex].row[rowIndex][0].cells[column];
932
+ if (node) {
933
+ arry.push( $.trim( node.textContent || node.innerText || $(node).text() ) );
934
+ }
935
+ }
791
936
  }
937
+ }
938
+ // get unique elements and sort the list
939
+ // if $.tablesorter.sortText exists (not in the original tablesorter),
940
+ // then natural sort the list otherwise use a basic sort
941
+ arry = $.grep(arry, function(value, indx) {
942
+ return $.inArray(value, arry) === indx;
943
+ });
944
+ arry = (ts.sortNatural) ? arry.sort(function(a, b) { return ts.sortNatural(a, b); }) : arry.sort(true);
792
945
 
793
- if (c.debug){
794
- ts.benchmark("Applying Filter widget", time);
946
+ // Get curent filter value
947
+ currentValue = c.$table.find('thead').find('select.tablesorter-filter[data-column="' + column + '"]').val();
948
+
949
+ // build option list
950
+ for (indx = 0; indx < arry.length; indx++) {
951
+ txt = arry[indx].replace(/\"/g, "&quot;");
952
+ // replace quotes - fixes #242 & ignore empty strings - see http://stackoverflow.com/q/14990971/145346
953
+ options += arry[indx] !== '' ? '<option value="' + txt + '"' + (currentValue === txt ? ' selected="selected"' : '') +
954
+ '>' + arry[indx] + '</option>' : '';
955
+ }
956
+ c.$table.find('thead').find('select.tablesorter-filter[data-column="' + column + '"]')[ updating ? 'html' : 'append' ](options);
957
+ },
958
+ buildDefault: function(table, updating) {
959
+ var columnIndex, $header,
960
+ c = table.config,
961
+ wo = c.widgetOptions,
962
+ columns = c.columns;
963
+ // build default select dropdown
964
+ for (columnIndex = 0; columnIndex < columns; columnIndex++) {
965
+ $header = c.$headers.filter('[data-column="' + columnIndex + '"]:last');
966
+ // look for the filter-select class; build/update it if found
967
+ if (($header.hasClass('filter-select') || wo.filter_functions && wo.filter_functions[columnIndex] === true) &&
968
+ !$header.hasClass('filter-false')) {
969
+ if (!wo.filter_functions) { wo.filter_functions = {}; }
970
+ wo.filter_functions[columnIndex] = true; // make sure this select gets processed by filter_functions
971
+ ts.filter.buildSelect(table, columnIndex, updating, $header.hasClass(wo.filter_onlyAvail));
795
972
  }
796
- // add default values
797
- $t.bind('tablesorter-initialized', function(){
798
- ff = ts.getFilters(table);
799
- // ff is undefined when filter_columnFilters = false
800
- if (ff) {
801
- for (i = 0; i < ff.length; i++) {
802
- ff[i] = $ths.filter('[data-column="' + i + '"]:last').attr(wo.filter_defaultAttrib) || ff[i];
803
- }
804
- ts.setFilters(table, ff, true);
805
- }
806
- });
807
- // filter widget initialized
808
- $t.trigger('filterInit');
809
- checkFilters();
810
973
  }
811
974
  },
812
- remove: function(table, c, wo){
813
- var k, $tb,
814
- $t = c.$table,
815
- b = c.$tbodies;
816
- $t
817
- .removeClass('hasFilters')
818
- // add .tsfilter namespace to all BUT search
819
- .unbind('addRows updateCell update updateComplete appendCache search filterStart filterEnd '.split(' ').join('.tsfilter '))
820
- .find('.tablesorter-filter-row').remove();
821
- for (k = 0; k < b.length; k++ ){
822
- $tb = ts.processTbody(table, b.eq(k), true); // remove tbody
823
- $tb.children().removeClass(wo.filter_filteredRow).show();
824
- ts.processTbody(table, $tb, false); // restore tbody
975
+ searching: function(table, filter, external) {
976
+ if (typeof filter === 'undefined' || filter === true || external) {
977
+ var wo = table.config.widgetOptions;
978
+ // delay filtering
979
+ clearTimeout(wo.searchTimer);
980
+ wo.searchTimer = setTimeout(function() {
981
+ ts.filter.checkFilters(table, external || filter);
982
+ }, wo.filter_liveSearch ? wo.filter_searchDelay : 10);
983
+ } else {
984
+ // skip delay
985
+ ts.filter.checkFilters(table, filter);
825
986
  }
826
- if (wo.filter_reset) { $(document).undelegate(wo.filter_reset, 'click.tsfilter'); }
827
987
  }
828
- });
988
+ };
989
+
829
990
  ts.getFilters = function(table) {
830
991
  var c = table ? $(table)[0].config : {};
831
- if (c && c.widgetOptions && !c.widgetOptions.filter_columnFilters) { return $(table).data('lastSearch'); }
832
- return c && c.$filters ? c.$filters.map(function(i, el) {
992
+ if (c && c.widgetOptions && !c.widgetOptions.filter_columnFilters) {
993
+ // no filter row
994
+ return $(table).data('lastSearch');
995
+ }
996
+ return c && c.$filters ? c.$filters.map(function(indx, el) {
833
997
  return $(el).find('.tablesorter-filter').val() || '';
834
998
  }).get() || [] : false;
835
999
  };
1000
+
836
1001
  ts.setFilters = function(table, filter, apply) {
837
- var $t = $(table),
838
- c = $t.length ? $t[0].config : {},
839
- valid = c && c.$filters ? c.$filters.each(function(i, el) {
840
- $(el).find('.tablesorter-filter').val(filter[i] || '');
841
- }) || false : false;
842
- if (apply) { $t.trigger('search', [filter, false]); }
1002
+ var $table = $(table),
1003
+ c = $table.length ? $table[0].config : {},
1004
+ valid = c && c.$filters ? c.$filters.each(function(indx, el) {
1005
+ $(el).find('.tablesorter-filter').val(filter[indx] || '');
1006
+ }).trigger('change.tsfilter') || false : false;
1007
+ if (apply) { $table.trigger('search', [filter, false]); }
843
1008
  return !!valid;
844
1009
  };
845
1010