jquery-tablesorter 1.9.5 → 1.10.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.8+ widgets - updated 12/16/2013 (v2.14.5)
1
+ /*! tableSorter 2.8+ widgets - updated 2/19/2014 (v2.15.0)
2
2
  *
3
3
  * Column Styles
4
4
  * Column Filters
@@ -49,6 +49,16 @@ ts.themes = {
49
49
  }
50
50
  };
51
51
 
52
+ $.extend(ts.css, {
53
+ filterRow : 'tablesorter-filter-row', // filter
54
+ filter : 'tablesorter-filter',
55
+ wrapper : 'tablesorter-wrapper', // ui theme & resizable
56
+ resizer : 'tablesorter-resizer', // resizable
57
+ grip : 'tablesorter-resizer-grip',
58
+ sticky : 'tablesorter-stickyHeader', // stickyHeader
59
+ stickyVis : 'tablesorter-sticky-visible'
60
+ });
61
+
52
62
  // *** Store data in local storage, with a cookie fallback ***
53
63
  /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json)
54
64
  if you need it, then include https://github.com/douglascrockford/JSON-js
@@ -200,16 +210,16 @@ ts.addWidget({
200
210
  // toggleClass with switch added in jQuery 1.3
201
211
  $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover);
202
212
  });
203
- if (!$headers.find('.tablesorter-wrapper').length) {
213
+ if (!$headers.find('.' + ts.css.wrapper).length) {
204
214
  // Firefox needs this inner div to position the resizer correctly
205
- $headers.wrapInner('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>');
215
+ $headers.wrapInner('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>');
206
216
  }
207
217
  if (c.cssIcon) {
208
218
  // if c.cssIcon is '', then no <i> is added to the header
209
219
  $headers.find('.' + ts.css.icon).addClass(themes.icons);
210
220
  }
211
221
  if ($table.hasClass('hasFilters')) {
212
- $headers.find('.tablesorter-filter-row').addClass(themes.filterRow);
222
+ $headers.find('.' + ts.css.filterRow).addClass(themes.filterRow);
213
223
  }
214
224
  }
215
225
  $.each($headers, function() {
@@ -218,7 +228,7 @@ ts.addWidget({
218
228
  if (this.sortDisabled) {
219
229
  // no sort arrows for disabled columns!
220
230
  $header.removeClass(remove);
221
- $icon.removeClass(remove + ' tablesorter-icon ' + themes.icons);
231
+ $icon.removeClass(remove + ' ' + themes.icons);
222
232
  } else {
223
233
  classes = ($header.hasClass(ts.css.sortAsc)) ?
224
234
  themes.sortAsc :
@@ -244,9 +254,9 @@ ts.addWidget({
244
254
  $headers
245
255
  .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover
246
256
  .removeClass(themes.hover + ' ' + remove + ' ' + themes.active)
247
- .find('.tablesorter-filter-row')
257
+ .find('.' + ts.css.filterRow)
248
258
  .removeClass(themes.filterRow);
249
- $headers.find('.tablesorter-icon').removeClass(themes.icons);
259
+ $headers.find('.' + ts.css.icon).removeClass(themes.icons);
250
260
  }
251
261
  });
252
262
 
@@ -338,13 +348,14 @@ ts.addWidget({
338
348
  id: "filter",
339
349
  priority: 50,
340
350
  options : {
341
- filter_anyMatch : false, // if true overrides default find rows behaviours and if any column matches query it returns that row
342
351
  filter_childRows : false, // if true, filter includes child row content in the search
343
352
  filter_columnFilters : true, // if true, a filter will be added to the top of each table column
344
353
  filter_cssFilter : '', // css class name added to the filter row & each input in the row (tablesorter-filter is ALWAYS added)
354
+ filter_external : '', // jQuery selector string (or jQuery object) of external filters
345
355
  filter_filteredRow : 'filtered', // class added to filtered rows; needed by pager plugin
346
356
  filter_formatter : null, // add custom filter elements to the filter row
347
357
  filter_functions : null, // add custom filter functions using this option
358
+ filter_hideEmpty : true, // hide filter row when table is empty
348
359
  filter_hideFilters : false, // collapse filter row when mouse leaves the area
349
360
  filter_ignoreCase : true, // if true, make all searches case-insensitive
350
361
  filter_liveSearch : true, // if true, search column content while the user types (with a delay)
@@ -359,9 +370,7 @@ ts.addWidget({
359
370
  },
360
371
  format: function(table, c, wo) {
361
372
  if (!c.$table.hasClass('hasFilters')) {
362
- if (c.parsers || !c.parsers && wo.filter_serversideFiltering) {
363
- ts.filter.init(table, c, wo);
364
- }
373
+ ts.filter.init(table, c, wo);
365
374
  }
366
375
  },
367
376
  remove: function(table, c, wo) {
@@ -372,7 +381,7 @@ ts.addWidget({
372
381
  .removeClass('hasFilters')
373
382
  // add .tsfilter namespace to all BUT search
374
383
  .unbind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join('.tsfilter '))
375
- .find('.tablesorter-filter-row').remove();
384
+ .find('.' + ts.css.filterRow).remove();
376
385
  for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
377
386
  $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
378
387
  $tbody.children().removeClass(wo.filter_filteredRow).show();
@@ -419,23 +428,6 @@ ts.filter = {
419
428
  }
420
429
  return null;
421
430
  },
422
- // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric
423
- exact: function( filter, iFilter, exact, iExact ) {
424
- /*jshint eqeqeq:false */
425
- if (ts.filter.regex.exact.test(iFilter)) {
426
- return iFilter.replace(ts.filter.regex.exact, '') == iExact;
427
- }
428
- return null;
429
- },
430
- // Look for a not match
431
- notMatch: function( filter, iFilter, exact, iExact, cached, index, table, wo ) {
432
- if ( /^\!/.test(iFilter) ) {
433
- iFilter = iFilter.replace('!', '');
434
- var indx = iExact.search( $.trim(iFilter) );
435
- return iFilter === '' ? true : !(wo.filter_startsWith ? indx === 0 : indx >= 0);
436
- }
437
- return null;
438
- },
439
431
  // Look for operators >, >=, < or <=
440
432
  operators: function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) {
441
433
  if ( /^[<>]=?/.test(iFilter) ) {
@@ -462,6 +454,24 @@ ts.filter = {
462
454
  }
463
455
  return null;
464
456
  },
457
+ // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric
458
+ exact: function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed, rowArray ) {
459
+ /*jshint eqeqeq:false */
460
+ if (ts.filter.regex.exact.test(iFilter)) {
461
+ var fltr = iFilter.replace(ts.filter.regex.exact, '');
462
+ return rowArray ? $.inArray(fltr, rowArray) >= 0 : fltr == iExact;
463
+ }
464
+ return null;
465
+ },
466
+ // Look for a not match
467
+ notMatch: function( filter, iFilter, exact, iExact, cached, index, table, wo ) {
468
+ if ( /^\!/.test(iFilter) ) {
469
+ iFilter = iFilter.replace('!', '');
470
+ var indx = iExact.search( $.trim(iFilter) );
471
+ return iFilter === '' ? true : !(wo.filter_startsWith ? indx === 0 : indx >= 0);
472
+ }
473
+ return null;
474
+ },
465
475
  // Look for an AND or && operator (logical and)
466
476
  and : function( filter, iFilter, exact, iExact ) {
467
477
  if ( /\s+(AND|&&)\s+/g.test(filter) ) {
@@ -500,13 +510,13 @@ ts.filter = {
500
510
  return null;
501
511
  },
502
512
  // Look for wild card: ? = single, * = multiple, or | = logical OR
503
- wild : function( filter, iFilter, exact, iExact, cached, index, table ) {
504
- if ( /[\?|\*]/.test(iFilter) || /\s+OR\s+/.test(filter) ) {
513
+ wild : function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed, rowArray ) {
514
+ if ( /[\?|\*]/.test(iFilter) || /\s+OR\s+/i.test(filter) ) {
505
515
  var c = table.config,
506
516
  query = iFilter.replace(/\s+OR\s+/gi,"|");
507
517
  // look for an exact match with the "or" unless the "filter-match" class is found
508
518
  if (!c.$headers.filter('[data-column="' + index + '"]:last').hasClass('filter-match') && /\|/.test(query)) {
509
- query = '^(' + query + ')$';
519
+ query = $.isArray(rowArray) ? '(' + query + ')' : '^(' + query + ')$';
510
520
  }
511
521
  return new RegExp( query.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(iExact);
512
522
  }
@@ -549,7 +559,8 @@ ts.filter = {
549
559
  }
550
560
 
551
561
  c.$table.bind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join('.tsfilter '), function(event, filter) {
552
- if ( !/(search|filterReset|filterEnd)/.test(event.type) ) {
562
+ c.$table.find('.' + ts.css.filterRow).toggle( !(wo.filter_hideEmpty && $.isEmptyObject(c.cache)) ); // fixes #450
563
+ if ( !/(search|filter)/.test(event.type) ) {
553
564
  event.stopPropagation();
554
565
  ts.filter.buildDefault(table, true);
555
566
  }
@@ -560,15 +571,26 @@ ts.filter = {
560
571
  } else {
561
572
  // send false argument to force a new search; otherwise if the filter hasn't changed, it will return
562
573
  filter = event.type === 'search' ? filter : event.type === 'updateComplete' ? c.$table.data('lastSearch') : '';
563
- ts.filter.searching(table, filter);
574
+ if (/(update|add)/.test(event.type)) {
575
+ // force a new search since content has changed
576
+ c.lastCombinedFilter = null;
577
+ }
578
+ // pass true (dontSkip) to prevent the tablesorter.setFilters function from skipping the first input
579
+ // ensures all inputs are updated when a search is triggered on the table $('table').trigger('search', [...]);
580
+ ts.filter.searching(table, filter, true);
564
581
  }
565
582
  return false;
566
583
  });
567
- ts.filter.bindSearch( table, c.$table.find('input.tablesorter-filter') );
584
+ ts.filter.bindSearch( table, c.$table.find('input.' + ts.css.filter), true );
585
+ if (wo.filter_external) {
586
+ ts.filter.bindSearch( table, wo.filter_external );
587
+ }
568
588
 
569
589
  // reset button/link
570
590
  if (wo.filter_reset) {
571
- $(document).delegate(wo.filter_reset, 'click.tsfilter', function() {
591
+ $(document)
592
+ .undelegate(wo.filter_reset, 'click.tsfilter')
593
+ .delegate(wo.filter_reset, 'click.tsfilter', function() {
572
594
  // trigger a reset event, so other functions (filterFormatter) know when to reset
573
595
  c.$table.trigger('filterReset');
574
596
  });
@@ -590,7 +612,7 @@ ts.filter = {
590
612
  options += '<option value="' + string + '">' + string + '</option>';
591
613
  }
592
614
  }
593
- c.$table.find('thead').find('select.tablesorter-filter[data-column="' + column + '"]').append(options);
615
+ c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]').append(options);
594
616
  }
595
617
  }
596
618
  }
@@ -599,8 +621,8 @@ ts.filter = {
599
621
  // it would append the same options twice.
600
622
  ts.filter.buildDefault(table, true);
601
623
 
602
- c.$table.find('select.tablesorter-filter').bind('change search', function(event, filter) {
603
- ts.filter.checkFilters(table, filter);
624
+ c.$table.find('select.' + ts.css.filter).bind('change search', function(event, filter) {
625
+ ts.filter.checkFilters(table, filter, true);
604
626
  });
605
627
 
606
628
  if (wo.filter_hideFilters) {
@@ -627,36 +649,31 @@ ts.filter = {
627
649
  if (filters.length) {
628
650
  ts.setFilters(table, filters, true);
629
651
  }
652
+ c.$table.trigger('filterFomatterUpdate');
630
653
  ts.filter.checkFilters(table, filters);
631
654
  });
632
655
  // filter widget initialized
633
- wo.filter_Initialized = true;
656
+ wo.filter_initialized = true;
634
657
  c.$table.trigger('filterInit');
635
658
  },
636
659
  setDefaults: function(table, c, wo) {
637
- var indx, isArray,
638
- filters = [],
639
- columns = c.columns;
660
+ var isArray, saved,
661
+ // get current (default) filters
662
+ filters = ts.getFilters(table);
640
663
  if (wo.filter_saveFilters && ts.storage) {
641
- filters = ts.storage( table, 'tablesorter-filters' ) || [];
642
- isArray = $.isArray(filters);
643
- // make sure we're not just saving an empty array
644
- if (isArray && filters.join('') === '' || !isArray ) { filters = []; }
645
- }
646
- // if not filters saved, then check default settings
647
- if (!filters.length) {
648
- for (indx = 0; indx < columns; indx++) {
649
- filters[indx] = c.$headers.filter('[data-column="' + indx + '"]:last').attr(wo.filter_defaultAttrib) || filters[indx];
650
- }
664
+ saved = ts.storage( table, 'tablesorter-filters' ) || [];
665
+ isArray = $.isArray(saved);
666
+ // make sure we're not just getting an empty array
667
+ if ( !(isArray && saved.join('') === '' || !isArray) ) { filters = saved; }
651
668
  }
652
- $(table).data('lastSearch', filters);
669
+ c.$table.data('lastSearch', filters);
653
670
  return filters;
654
671
  },
655
672
  buildRow: function(table, c, wo) {
656
- var column, $header, buildSelect, disabled,
673
+ var column, $header, buildSelect, disabled, name,
657
674
  // c.columns defined in computeThIndexes()
658
675
  columns = c.columns,
659
- buildFilter = '<tr class="tablesorter-filter-row">';
676
+ buildFilter = '<tr class="' + ts.css.filterRow + '">';
660
677
  for (column = 0; column < columns; column++) {
661
678
  buildFilter += '<td></td>';
662
679
  }
@@ -699,19 +716,41 @@ ts.filter = {
699
716
  }
700
717
  }
701
718
  if (buildFilter) {
702
- buildFilter.addClass('tablesorter-filter ' + wo.filter_cssFilter).attr('data-column', column);
719
+ // add filter class name
720
+ name = ( $.isArray(wo.filter_cssFilter) ?
721
+ (typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '') :
722
+ wo.filter_cssFilter ) || '';
723
+ buildFilter.addClass( ts.css.filter + ' ' + name ).attr('data-column', column);
703
724
  if (disabled) {
704
725
  buildFilter.addClass('disabled')[0].disabled = true; // disabled!
705
726
  }
706
727
  }
707
728
  }
708
729
  },
709
- bindSearch: function(table, $el) {
730
+ bindSearch: function(table, $el, internal) {
710
731
  table = $(table)[0];
711
- var external, wo = table.config.widgetOptions;
712
- $el.unbind('keyup search filterReset')
713
- .bind('keyup search', function(event, filter) {
714
- var $this = $(this);
732
+ $el = $($el); // allow passing a selector string
733
+ if (!$el.length) { return; }
734
+ var c = table.config,
735
+ wo = c.widgetOptions,
736
+ $ext = wo.filter_$externalFilters;
737
+ if (internal !== true) {
738
+ // save anyMatch element
739
+ wo.filter_$anyMatch = $el.filter('[data-column="all"]');
740
+ if ($ext && $ext.length) {
741
+ wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el );
742
+ } else {
743
+ wo.filter_$externalFilters = $el;
744
+ }
745
+ // update values (external filters added after table initialization)
746
+ ts.setFilters(table, c.$table.data('lastSearch') || [], internal === false);
747
+ }
748
+ $el
749
+ .data('lastSearchTime', new Date().getTime())
750
+ .unbind('keyup search change')
751
+ // include change for select - fixes #473
752
+ .bind('keyup search change', function(event, filters) {
753
+ $(this).data('lastSearchTime', new Date().getTime());
715
754
  // emulate what webkit does.... escape clears the filter
716
755
  if (event.which === 27) {
717
756
  this.value = '';
@@ -721,23 +760,14 @@ ts.filter = {
721
760
  ( event.which >= 37 && event.which <= 40 ) || (event.which !== 13 && wo.filter_liveSearch === false) ) ) ) {
722
761
  return;
723
762
  }
724
- // external searches won't have a filter parameter, so grab the value
725
- if ($this.hasClass('tablesorter-filter') && !$this.hasClass('tablesorter-external-filter')) {
726
- external = filter;
727
- } else {
728
- external = [];
729
- $el.each(function(){
730
- // target the appropriate column if the external input has a data-column attribute
731
- external[ $(this).data('column') || 0 ] = $(this).val();
732
- });
733
- }
734
- ts.filter.searching(table, filter, external);
735
- })
736
- .bind('filterReset', function(){
763
+ // true flag in getFilters forces obtaining the latest values
764
+ ts.filter.searching( table, filters || ts.getFilters( table, true ), true );
765
+ });
766
+ c.$table.bind('filterReset', function(){
737
767
  $el.val('');
738
768
  });
739
769
  },
740
- checkFilters: function(table, filter) {
770
+ checkFilters: function(table, filter, dontSkip) {
741
771
  var c = table.config,
742
772
  wo = c.widgetOptions,
743
773
  filterArray = $.isArray(filter),
@@ -745,11 +775,11 @@ ts.filter = {
745
775
  combinedFilters = (filters || []).join(''); // combined filter values
746
776
  // add filter array back into inputs
747
777
  if (filterArray) {
748
- ts.setFilters( table, filters );
778
+ ts.setFilters( table, filters, false, dontSkip !== true );
749
779
  }
750
780
  if (wo.filter_hideFilters) {
751
781
  // show/hide filter row as needed
752
- c.$table.find('.tablesorter-filter-row').trigger( combinedFilters === '' ? 'mouseleave' : 'mouseenter' );
782
+ c.$table.find('.' + ts.css.filterRow).trigger( combinedFilters === '' ? 'mouseleave' : 'mouseenter' );
753
783
  }
754
784
  // return if the last search is the same; but filter === false when updating the search
755
785
  // see example-widget-filter.html filter toggle buttons
@@ -774,7 +804,7 @@ ts.filter = {
774
804
  hideFilters: function(table, c) {
775
805
  var $filterRow, $filterRow2, timer;
776
806
  c.$table
777
- .find('.tablesorter-filter-row')
807
+ .find('.' + ts.css.filterRow)
778
808
  .addClass('hideme')
779
809
  .bind('mouseenter mouseleave', function(e) {
780
810
  // save event object - http://bugs.jquery.com/ticket/12140
@@ -810,9 +840,10 @@ ts.filter = {
810
840
  },
811
841
  findRows: function(table, filters, combinedFilters) {
812
842
  if (table.config.lastCombinedFilter === combinedFilters) { return; }
813
- var cached, len, $rows, rowIndex, tbodyIndex, $tbody, $cells, columnIndex,
843
+ var cached, len, $rows, cacheIndex, rowIndex, tbodyIndex, $tbody, $cells, columnIndex,
814
844
  childRow, childRowText, exact, iExact, iFilter, lastSearch, matches, result,
815
845
  searchFiltered, filterMatched, showRow, time,
846
+ anyMatch, iAnyMatch, rowArray, rowText, iRowText, rowCache,
816
847
  c = table.config,
817
848
  wo = c.widgetOptions,
818
849
  columns = c.columns,
@@ -821,19 +852,19 @@ ts.filter = {
821
852
  anyMatchNotAllowedTypes = [ 'range', 'notMatch', 'operators' ],
822
853
  // parse columns after formatter, in case the class is added at that point
823
854
  parsed = c.$headers.map(function(columnIndex) {
824
- return (ts.getData) ?
855
+ return c.parsers[columnIndex].parsed || ( ts.getData ?
825
856
  ts.getData(c.$headers.filter('[data-column="' + columnIndex + '"]:last'), c.headers[columnIndex], 'filter') === 'parsed' :
826
- $(this).hasClass('filter-parsed');
857
+ $(this).hasClass('filter-parsed') );
827
858
  }).get();
828
859
  if (c.debug) { time = new Date(); }
829
860
  for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
830
861
  if ($tbodies.eq(tbodyIndex).hasClass(ts.css.info)) { continue; } // ignore info blocks, issue #264
831
862
  $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true);
832
863
  // skip child rows & widget added (removable) rows - fixes #448 thanks to @hempel!
833
- $rows = $tbody.children('tr').not('.' + c.cssChildRow).not(c.selectorRemove);
864
+ $rows = $tbody.children('tr').not(c.selectorRemove);
834
865
  len = $rows.length;
835
866
  if (combinedFilters === '' || wo.filter_serversideFiltering) {
836
- $tbody.children().show().removeClass(wo.filter_filteredRow);
867
+ $tbody.children().not('.' + c.cssChildRow).show().removeClass(wo.filter_filteredRow);
837
868
  } else {
838
869
  // optimize searching only through already filtered rows - see #313
839
870
  searchFiltered = true;
@@ -844,7 +875,17 @@ ts.filter = {
844
875
  });
845
876
  // can't search when all rows are hidden - this happens when looking for exact matches
846
877
  if (searchFiltered && $rows.filter(':visible').length === 0) { searchFiltered = false; }
878
+ if ((wo.filter_$anyMatch && wo.filter_$anyMatch.length) || filters[c.columns]) {
879
+ anyMatch = wo.filter_$anyMatch && wo.filter_$anyMatch.val() || filters[c.columns] || '';
880
+ if (c.sortLocaleCompare) {
881
+ // replace accents
882
+ anyMatch = ts.replaceAccents(anyMatch);
883
+ }
884
+ iAnyMatch = anyMatch.toLowerCase();
885
+ }
886
+
847
887
  // loop through the rows
888
+ cacheIndex = 0;
848
889
  for (rowIndex = 0; rowIndex < len; rowIndex++) {
849
890
  childRow = $rows[rowIndex].className;
850
891
  // skip child rows & already filtered rows
@@ -858,10 +899,44 @@ ts.filter = {
858
899
  childRowText = (childRow.length && wo.filter_childRows) ? childRow.text() : '';
859
900
  childRowText = wo.filter_ignoreCase ? childRowText.toLocaleLowerCase() : childRowText;
860
901
  $cells = $rows.eq(rowIndex).children('td');
902
+
903
+ if (anyMatch) {
904
+ rowArray = $cells.map(function(i){
905
+ var txt;
906
+ if (parsed[i]) {
907
+ txt = c.cache[tbodyIndex].normalized[cacheIndex][i];
908
+ } else {
909
+ txt = wo.filter_ignoreCase ? $(this).text().toLowerCase() : $(this).text();
910
+ if (c.sortLocaleCompare) {
911
+ txt = ts.replaceAccents(txt);
912
+ }
913
+ }
914
+ return txt;
915
+ }).get();
916
+ rowText = rowArray.join(' ');
917
+ iRowText = rowText.toLowerCase();
918
+ rowCache = c.cache[tbodyIndex].normalized[cacheIndex].join(' ');
919
+ filterMatched = null;
920
+ $.each(ts.filter.types, function(type, typeFunction) {
921
+ if ($.inArray(type, anyMatchNotAllowedTypes) < 0) {
922
+ matches = typeFunction( anyMatch, iAnyMatch, rowText, iRowText, rowCache, columns, table, wo, parsed, rowArray );
923
+ if (matches !== null) {
924
+ filterMatched = matches;
925
+ return false;
926
+ }
927
+ }
928
+ });
929
+ if (filterMatched !== null) {
930
+ showRow = filterMatched;
931
+ } else {
932
+ showRow = (iRowText + childRowText).indexOf(iAnyMatch) >= 0;
933
+ }
934
+ }
935
+
861
936
  for (columnIndex = 0; columnIndex < columns; columnIndex++) {
862
937
  // ignore if filter is empty or disabled
863
- if (filters[columnIndex] || wo.filter_anyMatch) {
864
- cached = c.cache[tbodyIndex].normalized[rowIndex][columnIndex];
938
+ if (filters[columnIndex]) {
939
+ cached = c.cache[tbodyIndex].normalized[cacheIndex][columnIndex];
865
940
  // check if column data should be from the cell or from parsed data
866
941
  if (wo.filter_useParsedData || parsed[columnIndex]) {
867
942
  exact = cached;
@@ -873,14 +948,10 @@ ts.filter = {
873
948
  iExact = !ts.filter.regex.type.test(typeof exact) && wo.filter_ignoreCase ? exact.toLocaleLowerCase() : exact;
874
949
  result = showRow; // if showRow is true, show that row
875
950
 
876
- if (typeof filters[columnIndex] === "undefined" || filters[columnIndex] === null) {
877
- filters[columnIndex] = wo.filter_anyMatch ? combinedFilters : filters[columnIndex];
878
- }
879
-
880
951
  // replace accents - see #357
881
952
  filters[columnIndex] = c.sortLocaleCompare ? ts.replaceAccents(filters[columnIndex]) : filters[columnIndex];
882
953
  // val = case insensitive, filters[columnIndex] = case sensitive
883
- iFilter = wo.filter_ignoreCase ? filters[columnIndex].toLocaleLowerCase() : filters[columnIndex];
954
+ iFilter = wo.filter_ignoreCase ? (filters[columnIndex] || '').toLocaleLowerCase() : filters[columnIndex];
884
955
  if (wo.filter_functions && wo.filter_functions[columnIndex]) {
885
956
  if (wo.filter_functions[columnIndex] === true) {
886
957
  // default selector; no "filter-select" class
@@ -898,12 +969,10 @@ ts.filter = {
898
969
  // cycle through the different filters
899
970
  // filters return a boolean or null if nothing matches
900
971
  $.each(ts.filter.types, function(type, typeFunction) {
901
- if (!wo.filter_anyMatch || (wo.filter_anyMatch && $.inArray(type, anyMatchNotAllowedTypes) < 0)) {
902
- matches = typeFunction( filters[columnIndex], iFilter, exact, iExact, cached, columnIndex, table, wo, parsed );
903
- if (matches !== null) {
904
- filterMatched = matches;
905
- return false;
906
- }
972
+ matches = typeFunction( filters[columnIndex], iFilter, exact, iExact, cached, columnIndex, table, wo, parsed );
973
+ if (matches !== null) {
974
+ filterMatched = matches;
975
+ return false;
907
976
  }
908
977
  });
909
978
  if (filterMatched !== null) {
@@ -914,14 +983,7 @@ ts.filter = {
914
983
  result = ( (!wo.filter_startsWith && exact >= 0) || (wo.filter_startsWith && exact === 0) );
915
984
  }
916
985
  }
917
- if (wo.filter_anyMatch) {
918
- showRow = result;
919
- if (showRow){
920
- break;
921
- }
922
- } else {
923
- showRow = (result) ? showRow : false;
924
- }
986
+ showRow = (result) ? showRow : false;
925
987
  }
926
988
  }
927
989
  $rows[rowIndex].style.display = (showRow ? '' : 'none');
@@ -932,6 +994,7 @@ ts.filter = {
932
994
  }
933
995
  childRow.toggle(showRow);
934
996
  }
997
+ cacheIndex++;
935
998
  }
936
999
  }
937
1000
  ts.processTbody(table, $tbody, false);
@@ -984,7 +1047,7 @@ ts.filter = {
984
1047
  arry = (ts.sortNatural) ? arry.sort(function(a, b) { return ts.sortNatural(a, b); }) : arry.sort(true);
985
1048
 
986
1049
  // Get curent filter value
987
- currentValue = c.$table.find('thead').find('select.tablesorter-filter[data-column="' + column + '"]').val();
1050
+ currentValue = c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]').val();
988
1051
 
989
1052
  // build option list
990
1053
  for (indx = 0; indx < arry.length; indx++) {
@@ -993,7 +1056,7 @@ ts.filter = {
993
1056
  options += arry[indx] !== '' ? '<option value="' + txt + '"' + (currentValue === txt ? ' selected="selected"' : '') +
994
1057
  '>' + arry[indx] + '</option>' : '';
995
1058
  }
996
- c.$table.find('thead').find('select.tablesorter-filter[data-column="' + column + '"]')[ updating ? 'html' : 'append' ](options);
1059
+ c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]')[ updating ? 'html' : 'append' ](options);
997
1060
  },
998
1061
  buildDefault: function(table, updating) {
999
1062
  var columnIndex, $header,
@@ -1012,39 +1075,75 @@ ts.filter = {
1012
1075
  }
1013
1076
  }
1014
1077
  },
1015
- searching: function(table, filter, external) {
1016
- if (typeof filter === 'undefined' || filter === true || external) {
1078
+ searching: function(table, filter, dontSkip) {
1079
+ if (typeof filter === 'undefined' || filter === true) {
1017
1080
  var wo = table.config.widgetOptions;
1018
1081
  // delay filtering
1019
1082
  clearTimeout(wo.searchTimer);
1020
1083
  wo.searchTimer = setTimeout(function() {
1021
- ts.filter.checkFilters(table, external || filter);
1084
+ ts.filter.checkFilters(table, filter, dontSkip );
1022
1085
  }, wo.filter_liveSearch ? wo.filter_searchDelay : 10);
1023
1086
  } else {
1024
1087
  // skip delay
1025
- ts.filter.checkFilters(table, filter);
1088
+ ts.filter.checkFilters(table, filter, dontSkip);
1026
1089
  }
1027
1090
  }
1028
1091
  };
1029
1092
 
1030
- ts.getFilters = function(table) {
1031
- var c = table ? $(table)[0].config : {};
1032
- if (c && c.widgetOptions && !c.widgetOptions.filter_columnFilters) {
1033
- // no filter row
1093
+ ts.getFilters = function(table, getRaw, setFilters, skipFirst) {
1094
+ var i, $filters, $column,
1095
+ filters = false,
1096
+ c = table ? $(table)[0].config : '',
1097
+ wo = table ? c.widgetOptions : '';
1098
+ if (getRaw !== true && wo && !wo.filter_columnFilters) {
1034
1099
  return $(table).data('lastSearch');
1035
1100
  }
1036
- return c && c.$filters ? c.$filters.map(function(indx, el) {
1037
- return $(el).find('.tablesorter-filter').val() || '';
1038
- }).get() || [] : false;
1101
+ if (c) {
1102
+ if (c.$filters) {
1103
+ $filters = c.$filters.find('.' + ts.css.filter);
1104
+ }
1105
+ if (wo.filter_$externalFilters) {
1106
+ $filters = $filters && $filters.length ? $filters.add(wo.filter_$externalFilters) : wo.filter_$externalFilters;
1107
+ }
1108
+ if ($filters && $filters.length) {
1109
+ filters = setFilters || [];
1110
+ for (i = 0; i < c.columns + 1; i++) {
1111
+ $column = $filters.filter('[data-column="' + (i === c.columns ? 'all' : i) + '"]');
1112
+ if ($column.length) {
1113
+ // move the latest search to the first slot in the array
1114
+ $column = $column.sort(function(a, b){
1115
+ return $(a).data('lastSearchTime') <= $(b).data('lastSearchTime');
1116
+ });
1117
+ if ($.isArray(setFilters)) {
1118
+ // skip first (latest input) to maintain cursor position while typing
1119
+ (skipFirst ? $column.slice(1) : $column).val( setFilters[i] ).trigger('change.tsfilter');
1120
+ } else {
1121
+ filters[i] = $column.val() || '';
1122
+ // don't change the first... it will move the cursor
1123
+ $column.slice(1).val( filters[i] );
1124
+ }
1125
+ // save any match input dynamically
1126
+ if (i === c.columns && $column.length) {
1127
+ wo.filter_$anyMatch = $column;
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+ }
1133
+ if (filters.length === 0) {
1134
+ filters = false;
1135
+ }
1136
+ return filters;
1039
1137
  };
1040
1138
 
1041
- ts.setFilters = function(table, filter, apply) {
1042
- var $table = $(table),
1043
- c = $table.length ? $table[0].config : {},
1044
- valid = c && c.$filters ? c.$filters.each(function(indx, el) {
1045
- $(el).find('.tablesorter-filter').val(filter[indx] || '');
1046
- }).trigger('change.tsfilter') || false : false;
1047
- if (apply) { $table.trigger('search', [filter, false]); }
1139
+ ts.setFilters = function(table, filter, apply, skipFirst) {
1140
+ var c = table ? $(table)[0].config : '',
1141
+ valid = ts.getFilters(table, true, filter, skipFirst);
1142
+ if (c && apply) {
1143
+ // ensure new set filters are applied, even if the search is the same
1144
+ c.lastCombinedFilter = null;
1145
+ c.$table.trigger('search', [filter, false]).trigger('filterFomatterUpdate');
1146
+ }
1048
1147
  return !!valid;
1049
1148
  };
1050
1149
 
@@ -1076,9 +1175,8 @@ ts.addWidget({
1076
1175
  $thead = $table.children('thead:first'),
1077
1176
  $win = $attach.length ? $attach : $(window),
1078
1177
  $header = $thead.children('tr').not('.sticky-false').children(),
1079
- innerHeader = '.tablesorter-header-inner',
1178
+ innerHeader = '.' + ts.css.headerIn,
1080
1179
  $tfoot = $table.find('tfoot'),
1081
- filterInputs = '.tablesorter-filter',
1082
1180
  $stickyOffset = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : '',
1083
1181
  stickyOffset = $attach.length ? 0 : $stickyOffset.length ?
1084
1182
  $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0,
@@ -1092,7 +1190,7 @@ ts.addWidget({
1092
1190
  visibility : 'hidden',
1093
1191
  zIndex : wo.stickyHeaders_zIndex ? wo.stickyHeaders_zIndex : 2
1094
1192
  }),
1095
- $stickyThead = $stickyTable.children('thead:first').addClass('tablesorter-stickyHeader ' + wo.stickyHeaders),
1193
+ $stickyThead = $stickyTable.children('thead:first').addClass(ts.css.sticky + ' ' + wo.stickyHeaders),
1096
1194
  $stickyCells,
1097
1195
  laststate = '',
1098
1196
  spacing = 0,
@@ -1108,8 +1206,8 @@ ts.addWidget({
1108
1206
  spacing = parseInt($header.eq(0).css('border-left-width'), 10) * 2;
1109
1207
  }
1110
1208
  $stickyTable.css({
1111
- left : $attach.length ? parseInt($attach.css('padding-left'), 10) +
1112
- parseInt($attach.css('margin-left'), 10) + parseInt($table.css('border-left-width'), 10) :
1209
+ left : $attach.length ? (parseInt($attach.css('padding-left'), 10) || 0) + parseInt(c.$table.css('padding-left'), 10) +
1210
+ parseInt(c.$table.css('margin-left'), 10) + parseInt($table.css('border-left-width'), 10) :
1113
1211
  $thead.offset().left - $win.scrollLeft() - spacing,
1114
1212
  width: $table.width()
1115
1213
  });
@@ -1139,7 +1237,7 @@ ts.addWidget({
1139
1237
  $stickyCells = $stickyThead.children().children();
1140
1238
  $stickyTable.css({ height:0, width:0, padding:0, margin:0, border:0 });
1141
1239
  // remove resizable block
1142
- $stickyCells.find('.tablesorter-resizer').remove();
1240
+ $stickyCells.find('.' + ts.css.resizer).remove();
1143
1241
  // update sticky header class names to match real header after sorting
1144
1242
  $table
1145
1243
  .addClass('hasStickyHeaders')
@@ -1159,27 +1257,11 @@ ts.addWidget({
1159
1257
  .bind('pagerComplete.tsSticky', function() {
1160
1258
  resizeHeader();
1161
1259
  });
1162
- // http://stackoverflow.com/questions/5312849/jquery-find-self;
1163
- $header.find(c.selectorSort).add( c.$headers.filter(c.selectorSort) ).each(function(indx) {
1164
- var $header = $(this),
1165
- // clicking on sticky will trigger sort
1166
- $cell = $stickyThead.children('tr.tablesorter-headerRow').children().eq(indx).bind('mouseup', function(event) {
1167
- $header.trigger(event, true); // external mouseup flag (click timer is ignored)
1168
- });
1169
- // prevent sticky header text selection
1170
- if (c.cancelSelection) {
1171
- $cell
1172
- .attr('unselectable', 'on')
1173
- .bind('selectstart', false)
1174
- .css({
1175
- 'user-select': 'none',
1176
- 'MozUserSelect': 'none'
1177
- });
1178
- }
1179
- });
1260
+
1261
+ ts.bindEvents(table, $stickyThead.children().children('.tablesorter-header'));
1262
+
1180
1263
  // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned.
1181
1264
  $table.after( $stickyTable );
1182
-
1183
1265
  // make it sticky!
1184
1266
  $win.bind('scroll.tsSticky resize.tsSticky', function(event) {
1185
1267
  if (!$table.is(':visible')) { return; } // fixes #278
@@ -1212,17 +1294,22 @@ ts.addWidget({
1212
1294
 
1213
1295
  // look for filter widget
1214
1296
  if ($table.hasClass('hasFilters')) {
1297
+ // scroll table into view after filtering, if sticky header is active - #482
1215
1298
  $table.bind('filterEnd', function() {
1216
1299
  // $(':focus') needs jQuery 1.6+
1217
- if ( $(document.activeElement).closest('thead')[0] !== $stickyThead[0] ) {
1218
- // don't update the stickyheader filter row if it already has focus
1219
- $stickyThead.find('.tablesorter-filter-row').children().each(function(indx) {
1220
- $(this).find(filterInputs).val( c.$filters.find(filterInputs).eq(indx).val() );
1221
- });
1300
+ var $td = $(document.activeElement).closest('td'),
1301
+ column = $td.parent().children().index($td);
1302
+ // only scroll if sticky header is active
1303
+ if ($stickyTable.hasClass(ts.css.stickyVis)) {
1304
+ // scroll to original table (not sticky clone)
1305
+ window.scrollTo(0, $table.position().top);
1306
+ // give same input/select focus
1307
+ if (column >= 0) {
1308
+ c.$filters.eq(column).find('a, select, input').filter(':visible').focus();
1309
+ }
1222
1310
  }
1223
1311
  });
1224
-
1225
- ts.filter.bindSearch( $table, $stickyCells.find('.tablesorter-filter').addClass('tablesorter-external-filter') );
1312
+ ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) );
1226
1313
  }
1227
1314
 
1228
1315
  $table.trigger('stickyHeadersInit');
@@ -1232,7 +1319,7 @@ ts.addWidget({
1232
1319
  c.$table
1233
1320
  .removeClass('hasStickyHeaders')
1234
1321
  .unbind('sortEnd.tsSticky pagerComplete.tsSticky')
1235
- .find('.tablesorter-stickyHeader').remove();
1322
+ .find('.' + ts.css.sticky).remove();
1236
1323
  if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table
1237
1324
  // don't unbind if any table on the page still has stickyheaders applied
1238
1325
  if (!$('.hasStickyHeaders').length) {
@@ -1264,7 +1351,8 @@ ts.addWidget({
1264
1351
  $next = null,
1265
1352
  fullWidth = Math.abs($table.parent().width() - $table.width()) < 20,
1266
1353
  stopResize = function() {
1267
- if (ts.storage && $target) {
1354
+ if (ts.storage && $target && $next) {
1355
+ storedSizes = {};
1268
1356
  storedSizes[$target.index()] = $target.width();
1269
1357
  storedSizes[$next.index()] = $next.width();
1270
1358
  $target.width( storedSizes[$target.index()] );
@@ -1298,9 +1386,9 @@ ts.addWidget({
1298
1386
  // add wrapper inside each cell to allow for positioning of the resizable target block
1299
1387
  $rows.each(function() {
1300
1388
  $column = $(this).children().not('.resizable-false');
1301
- if (!$(this).find('.tablesorter-wrapper').length) {
1389
+ if (!$(this).find('.' + ts.css.wrapper).length) {
1302
1390
  // Firefox needs this inner div to position the resizer correctly
1303
- $column.wrapInner('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>');
1391
+ $column.wrapInner('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>');
1304
1392
  }
1305
1393
  // don't include the last column of the row
1306
1394
  if (!wo.resizable_addLastColumn) { $column = $column.slice(0,-1); }
@@ -1311,8 +1399,8 @@ ts.addWidget({
1311
1399
  var $column = $(this),
1312
1400
  padding = parseInt($column.css('padding-right'), 10) + 10; // 10 is 1/2 of the 20px wide resizer grip
1313
1401
  $column
1314
- .find('.tablesorter-wrapper')
1315
- .append('<div class="tablesorter-resizer" style="cursor:w-resize;position:absolute;z-index:1;right:-' +
1402
+ .find('.' + ts.css.wrapper)
1403
+ .append('<div class="' + ts.css.resizer + '" style="cursor:w-resize;position:absolute;z-index:1;right:-' +
1316
1404
  padding + 'px;top:0;height:100%;width:20px;"></div>');
1317
1405
  })
1318
1406
  .bind('mousemove.tsresize', function(event) {
@@ -1330,7 +1418,7 @@ ts.addWidget({
1330
1418
  .bind('mouseup.tsresize', function() {
1331
1419
  stopResize();
1332
1420
  })
1333
- .find('.tablesorter-resizer,.tablesorter-resizer-grip')
1421
+ .find('.' + ts.css.resizer + ',.' + ts.css.grip)
1334
1422
  .bind('mousedown', function(event) {
1335
1423
  // save header cell and mouse position; closest() not supported by jQuery v1.2.6
1336
1424
  $target = $(event.target).closest('th');
@@ -1361,13 +1449,15 @@ ts.addWidget({
1361
1449
  .children('tr').children()
1362
1450
  .unbind('mousemove.tsresize mouseup.tsresize')
1363
1451
  // don't remove "tablesorter-wrapper" as uitheme uses it too
1364
- .find('.tablesorter-resizer,.tablesorter-resizer-grip').remove();
1452
+ .find('.' + ts.css.resizer + ',.' + ts.css.grip).remove();
1365
1453
  ts.resizableReset(table);
1366
1454
  }
1367
1455
  });
1368
1456
  ts.resizableReset = function(table) {
1369
- table.config.$headers.not('.resizable-false').css('width','');
1370
- if (ts.storage) { ts.storage(table, 'tablesorter-resizable', {}); }
1457
+ $(table).each(function(){
1458
+ this.config.$headers.not('.resizable-false').css('width','');
1459
+ if (ts.storage) { ts.storage(this, 'tablesorter-resizable', {}); }
1460
+ });
1371
1461
  };
1372
1462
 
1373
1463
  // Save table sort widget