jquery-tablesorter 1.14.1 → 1.15.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/jquery-tablesorter/version.rb +1 -1
  4. data/vendor/assets/javascripts/jquery-tablesorter.js +1 -1
  5. data/vendor/assets/javascripts/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.js +5 -9
  6. data/vendor/assets/javascripts/jquery-tablesorter/extras/jquery.dragtable.mod.js +1 -4
  7. data/vendor/assets/javascripts/jquery-tablesorter/{jquery.metadata.js → extras/jquery.metadata.js} +1 -0
  8. data/vendor/assets/javascripts/jquery-tablesorter/extras/jquery.quicksearch.js +8 -4
  9. data/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.js +116 -107
  10. data/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.widgets.js +232 -171
  11. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-file-type.js +1 -1
  12. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-input-select.js +1 -1
  13. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-chart.js +2 -3
  14. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-columns.js +78 -0
  15. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-cssStickyHeaders.js +14 -12
  16. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-editable.js +4 -4
  17. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-html5.js +429 -0
  18. data/vendor/assets/javascripts/jquery-tablesorter/{jquery.tablesorter.widgets-filter-formatter.js → widgets/widget-filter-formatter-jui.js} +1 -381
  19. data/vendor/assets/javascripts/jquery-tablesorter/{jquery.tablesorter.widgets-filter-formatter-select2.js → widgets/widget-filter-formatter-select2.js} +0 -0
  20. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter.js +1307 -0
  21. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-formatter.js +3 -3
  22. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-grouping.js +4 -4
  23. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-math.js +3 -3
  24. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-pager.js +4 -8
  25. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-resizable.js +176 -0
  26. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-saveSort.js +68 -0
  27. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-staticRow.js +3 -3
  28. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-stickyHeaders.js +269 -0
  29. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-storage.js +76 -0
  30. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-uitheme.js +184 -0
  31. data/vendor/assets/stylesheets/jquery-tablesorter/theme.black-ice.css +1 -1
  32. data/vendor/assets/stylesheets/jquery-tablesorter/theme.blue.css +1 -1
  33. data/vendor/assets/stylesheets/jquery-tablesorter/theme.dark.css +1 -1
  34. data/vendor/assets/stylesheets/jquery-tablesorter/theme.default.css +1 -1
  35. data/vendor/assets/stylesheets/jquery-tablesorter/theme.dropbox.css +1 -1
  36. data/vendor/assets/stylesheets/jquery-tablesorter/theme.green.css +1 -1
  37. data/vendor/assets/stylesheets/jquery-tablesorter/theme.grey.css +1 -1
  38. data/vendor/assets/stylesheets/jquery-tablesorter/theme.ice.css +1 -1
  39. data/vendor/assets/stylesheets/jquery-tablesorter/theme.jui.css +1 -1
  40. data/vendor/assets/stylesheets/jquery-tablesorter/theme.metro-dark.css +1 -1
  41. metadata +15 -7
@@ -1,4 +1,4 @@
1
- /*! Filter widget formatter functions - updated 7/17/2014 (v2.17.5)
1
+ /*! Filter widget formatter jQuery UI functions *//* updated 7/17/2014 (v2.17.5)
2
2
  * requires: tableSorter (FORK) 2.15+ and jQuery 1.4.3+
3
3
  *
4
4
  * uiSpinner (jQuery UI spinner)
@@ -6,9 +6,6 @@
6
6
  * uiRange (jQuery UI range slider)
7
7
  * uiDateCompare (jQuery UI datepicker; 1 input)
8
8
  * uiDatepicker (jQuery UI datepicker; 2 inputs, filter range)
9
- * html5Number (spinner)
10
- * html5Range (slider)
11
- * html5Color (color)
12
9
  */
13
10
  /*jshint browser:true, jquery:true, unused:false */
14
11
  /*global jQuery: false */
@@ -761,383 +758,6 @@ tsff = ts.filterFormatter = {
761
758
 
762
759
  // return the hidden input so the filter widget has a reference to it
763
760
  return $input.val( o.from ? ( o.to ? o.from + ' - ' + o.to : '>=' + o.from ) : (o.to ? '<=' + o.to : '') );
764
- },
765
-
766
- /**********************\
767
- HTML5 Number (spinner)
768
- \**********************/
769
- html5Number : function($cell, indx, def5Num) {
770
- var t, o = $.extend({
771
- value : 0,
772
- min : 0,
773
- max : 100,
774
- step : 1,
775
- delayed : true,
776
- disabled : false,
777
- addToggle : false,
778
- exactMatch : false,
779
- cellText : '',
780
- compare : '',
781
- skipTest: false
782
- }, def5Num),
783
-
784
- $input,
785
- // test browser for HTML5 range support
786
- $number = $('<input type="number" style="visibility:hidden;" value="test">').appendTo($cell),
787
- // test if HTML5 number is supported - from Modernizr
788
- numberSupported = o.skipTest || $number.attr('type') === 'number' && $number.val() !== 'test',
789
- $shcell = [],
790
- c = $cell.closest('table')[0].config,
791
-
792
- updateNumber = function(delayed, notrigger){
793
- var chkd = o.addToggle ? $cell.find('.toggle').is(':checked') : true,
794
- v = $cell.find('.number').val(),
795
- compare = ($.isArray(o.compare) ? $cell.find(compareSelect).val() || o.compare[ o.selected || 0] : o.compare) || '',
796
- searchType = c.$table[0].hasInitialized ? (delayed ? delayed : o.delayed) || '' : true;
797
- $input
798
- // add equal to the beginning, so we filter exact numbers
799
- .val( !o.addToggle || chkd ? (compare ? compare : o.exactMatch ? '=' : '') + v : '' )
800
- .trigger( notrigger ? '' : 'search', searchType ).end()
801
- .find('.number').val(v);
802
- if ($cell.find('.number').length) {
803
- $cell.find('.number')[0].disabled = (o.disabled || !chkd);
804
- }
805
- // update sticky header cell
806
- if ($shcell.length) {
807
- $shcell.find('.number').val(v)[0].disabled = (o.disabled || !chkd);
808
- $shcell.find(compareSelect).val(compare);
809
- if (o.addToggle) {
810
- $shcell.find('.toggle')[0].checked = chkd;
811
- }
812
- }
813
- };
814
- $number.remove();
815
-
816
- if (numberSupported) {
817
- t = o.addToggle ? '<div class="button"><input id="html5button' + indx + '" type="checkbox" class="toggle" />' +
818
- '<label for="html5button' + indx + '"></label></div>' : '';
819
- t += '<input class="number" type="number" min="' + o.min + '" max="' + o.max + '" value="' +
820
- o.value + '" step="' + o.step + '" />';
821
- // add HTML5 number (spinner)
822
- $cell
823
- .append(t + '<input type="hidden" />')
824
- .find('.toggle, .number').bind('change', function(){
825
- updateNumber();
826
- })
827
- .closest('thead').find('th[data-column=' + indx + ']')
828
- .addClass('filter-parsed') // get exact numbers from column
829
- // on reset
830
- .closest('table').bind('filterReset', function(){
831
- if ($.isArray(o.compare)) {
832
- $cell.add($shcell).find(compareSelect).val( o.compare[ o.selected || 0 ] );
833
- }
834
- // turn off the toggle checkbox
835
- if (o.addToggle) {
836
- $cell.find('.toggle')[0].checked = false;
837
- if ($shcell.length) {
838
- $shcell.find('.toggle')[0].checked = false;
839
- }
840
- }
841
- $cell.find('.number').val( o.value );
842
- setTimeout(function(){
843
- updateNumber();
844
- }, 0);
845
- });
846
- $input = $cell.find('input[type=hidden]').bind('change', function(){
847
- $cell.find('.number').val( this.value );
848
- updateNumber();
849
- });
850
-
851
- // update slider from hidden input, in case of saved filters
852
- c.$table.bind('filterFomatterUpdate', function(){
853
- var val = tsff.updateCompare($cell, $input, o)[0] || o.value;
854
- $cell.find('.number').val( ((val || '') + '').replace(/[><=]/g,'') );
855
- updateNumber(false, true);
856
- ts.filter.formatterUpdated($cell, indx);
857
- });
858
-
859
- if (o.compare) {
860
- // add compare select
861
- tsff.addCompare($cell, indx, o);
862
- $cell.find(compareSelect).bind('change', function(){
863
- updateNumber();
864
- });
865
- }
866
-
867
- // has sticky headers?
868
- c.$table.bind('stickyHeadersInit', function(){
869
- $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx).empty();
870
- $shcell
871
- .append(t)
872
- .find('.toggle, .number').bind('change', function(){
873
- $cell.find('.number').val( $(this).val() );
874
- updateNumber();
875
- });
876
-
877
- if (o.compare) {
878
- // add compare select
879
- tsff.addCompare($shcell, indx, o);
880
- $shcell.find(compareSelect).bind('change', function(){
881
- $cell.find(compareSelect).val( $(this).val() );
882
- updateNumber();
883
- });
884
- }
885
-
886
- updateNumber();
887
- });
888
-
889
- updateNumber();
890
-
891
- }
892
-
893
- return numberSupported ? $cell.find('input[type="hidden"]') : $('<input type="search">');
894
- },
895
-
896
- /**********************\
897
- HTML5 range slider
898
- \**********************/
899
- html5Range : function($cell, indx, def5Range) {
900
- var o = $.extend({
901
- value : 0,
902
- min : 0,
903
- max : 100,
904
- step : 1,
905
- delayed : true,
906
- valueToHeader : true,
907
- exactMatch : true,
908
- cellText : '',
909
- compare : '',
910
- allText : 'all',
911
- skipTest : false
912
- }, def5Range),
913
-
914
- $input,
915
- // test browser for HTML5 range support
916
- $range = $('<input type="range" style="visibility:hidden;" value="test">').appendTo($cell),
917
- // test if HTML5 range is supported - from Modernizr (but I left out the method to detect in Safari 2-4)
918
- // see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/inputtypes.js
919
- rangeSupported = o.skipTest || $range.attr('type') === 'range' && $range.val() !== 'test',
920
- $shcell = [],
921
- c = $cell.closest('table')[0].config,
922
-
923
- updateRange = function(v, delayed, notrigger){
924
- /*jshint eqeqeq:false */
925
- // hidden input changes may include compare symbols
926
- v = ( typeof v === "undefined" ? $input.val() : v ).toString().replace(/[<>=]/g,'') || o.value;
927
- var compare = ($.isArray(o.compare) ? $cell.find(compareSelect).val() || o.compare[ o.selected || 0] : o.compare) || '',
928
- t = ' (' + (compare ? compare + v : v == o.min ? o.allText : v) + ')',
929
- searchType = c.$table[0].hasInitialized ? (delayed ? delayed : o.delayed) || '' : true;
930
- $cell.find('input[type=hidden]')
931
- // add equal to the beginning, so we filter exact numbers
932
- .val( ( compare ? compare + v : ( v == o.min ? '' : ( o.exactMatch ? '=' : '' ) + v ) ) )
933
- //( val == o.min ? '' : val + (o.exactMatch ? '=' : ''))
934
- .trigger( notrigger ? '' : 'search', searchType ).end()
935
- .find('.range').val(v);
936
- // or add current value to the header cell, if desired
937
- $cell.closest('thead').find('th[data-column=' + indx + ']').find('.curvalue').html(t);
938
- // update sticky header cell
939
- if ($shcell.length) {
940
- $shcell
941
- .find('.range').val(v).end()
942
- .find(compareSelect).val( compare );
943
- $shcell.closest('thead').find('th[data-column=' + indx + ']').find('.curvalue').html(t);
944
- }
945
- };
946
- $range.remove();
947
-
948
- if (rangeSupported) {
949
- // add HTML5 range
950
- $cell
951
- .html('<input type="hidden"><input class="range" type="range" min="' + o.min + '" max="' + o.max + '" value="' + o.value + '" />')
952
- .closest('thead').find('th[data-column=' + indx + ']')
953
- .addClass('filter-parsed') // get exact numbers from column
954
- // add span to header for the current slider value
955
- .find('.tablesorter-header-inner').append('<span class="curvalue" />');
956
- // hidden filter update namespace trigger by filter widget
957
- $input = $cell.find('input[type=hidden]').bind('change' + c.namespace + 'filter', function(){
958
- /*jshint eqeqeq:false */
959
- var v = this.value,
960
- compare = ($.isArray(o.compare) ? $cell.find(compareSelect).val() || o.compare[ o.selected || 0] : o.compare) || '';
961
- if (v !== this.lastValue) {
962
- this.lastValue = ( compare ? compare + v : ( v == o.min ? '' : ( o.exactMatch ? '=' : '' ) + v ) );
963
- this.value = this.lastValue;
964
- updateRange( v );
965
- }
966
- });
967
-
968
- $cell.find('.range').bind('change', function(){
969
- updateRange( this.value );
970
- });
971
-
972
- // update spinner from hidden input, in case of saved filters
973
- c.$table.bind('filterFomatterUpdate', function(){
974
- var val = tsff.updateCompare($cell, $input, o)[0];
975
- $cell.find('.range').val( val );
976
- updateRange(val, false, true);
977
- ts.filter.formatterUpdated($cell, indx);
978
- });
979
-
980
- if (o.compare) {
981
- // add compare select
982
- tsff.addCompare($cell, indx, o);
983
- $cell.find(compareSelect).bind('change', function(){
984
- updateRange();
985
- });
986
- }
987
-
988
- // has sticky headers?
989
- c.$table.bind('stickyHeadersInit', function(){
990
- $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx).empty();
991
- $shcell
992
- .html('<input class="range" type="range" min="' + o.min + '" max="' + o.max + '" value="' + o.value + '" />')
993
- .find('.range').bind('change', function(){
994
- updateRange( $shcell.find('.range').val() );
995
- });
996
- updateRange();
997
-
998
- if (o.compare) {
999
- // add compare select
1000
- tsff.addCompare($shcell, indx, o);
1001
- $shcell.find(compareSelect).bind('change', function(){
1002
- $cell.find(compareSelect).val( $(this).val() );
1003
- updateRange();
1004
- });
1005
- }
1006
-
1007
- });
1008
-
1009
- // on reset
1010
- $cell.closest('table').bind('filterReset', function(){
1011
- if ($.isArray(o.compare)) {
1012
- $cell.add($shcell).find(compareSelect).val( o.compare[ o.selected || 0 ] );
1013
- }
1014
- setTimeout(function(){
1015
- updateRange(o.value, false, true);
1016
- }, 0);
1017
- });
1018
- updateRange();
1019
-
1020
- }
1021
-
1022
- return rangeSupported ? $cell.find('input[type="hidden"]') : $('<input type="search">');
1023
- },
1024
-
1025
- /**********************\
1026
- HTML5 Color picker
1027
- \**********************/
1028
- html5Color: function($cell, indx, defColor) {
1029
- var t, o = $.extend({
1030
- value : '#000000',
1031
- disabled : false,
1032
- addToggle : true,
1033
- exactMatch : true,
1034
- valueToHeader : false,
1035
- skipTest : false
1036
- }, defColor),
1037
- $input,
1038
- // Add a hidden input to hold the range values
1039
- $color = $('<input type="color" style="visibility:hidden;" value="test">').appendTo($cell),
1040
- // test if HTML5 color is supported - from Modernizr
1041
- colorSupported = o.skipTest || $color.attr('type') === 'color' && $color.val() !== 'test',
1042
- $shcell = [],
1043
- c = $cell.closest('table')[0].config,
1044
-
1045
- updateColor = function(v, notrigger){
1046
- v = ( typeof v === "undefined" ? $input.val() : v ).toString().replace('=','') || o.value;
1047
- var chkd = true,
1048
- t = ' (' + v + ')';
1049
- if (o.addToggle) {
1050
- chkd = $cell.find('.toggle').is(':checked');
1051
- }
1052
- if ($cell.find('.colorpicker').length) {
1053
- $cell.find('.colorpicker').val(v)[0].disabled = (o.disabled || !chkd);
1054
- }
1055
-
1056
- $input
1057
- .val( chkd ? v + (o.exactMatch ? '=' : '') : '' )
1058
- .trigger( !c.$table[0].hasInitialized || notrigger ? '' : 'search' );
1059
- if (o.valueToHeader) {
1060
- // add current color to the header cell
1061
- $cell.closest('thead').find('th[data-column=' + indx + ']').find('.curcolor').html(t);
1062
- } else {
1063
- // current color to span in cell
1064
- $cell.find('.currentColor').html(t);
1065
- }
1066
-
1067
- // update sticky header cell
1068
- if ($shcell.length) {
1069
- $shcell.find('.colorpicker').val(v)[0].disabled = (o.disabled || !chkd);
1070
- if (o.addToggle) {
1071
- $shcell.find('.toggle')[0].checked = chkd;
1072
- }
1073
- if (o.valueToHeader) {
1074
- // add current color to the header cell
1075
- $shcell.closest('thead').find('th[data-column=' + indx + ']').find('.curcolor').html(t);
1076
- } else {
1077
- // current color to span in cell
1078
- $shcell.find('.currentColor').html(t);
1079
- }
1080
- }
1081
- };
1082
- $color.remove();
1083
-
1084
- if (colorSupported) {
1085
- t = '' + indx + Math.round(Math.random() * 100);
1086
- // add HTML5 color picker
1087
- t = '<div class="color-controls-wrapper">' +
1088
- (o.addToggle ? '<div class="button"><input id="colorbutton' + t + '" type="checkbox" class="toggle" /><label for="colorbutton' +
1089
- t + '"></label></div>' : '') +
1090
- '<input type="hidden"><input class="colorpicker" type="color" />' +
1091
- (o.valueToHeader ? '' : '<span class="currentColor">(#000000)</span>') + '</div>';
1092
- $cell.html(t);
1093
- // add span to header for the current color value - only works if the line in the updateColor() function is also un-commented out
1094
- if (o.valueToHeader) {
1095
- $cell.closest('thead').find('th[data-column=' + indx + ']').find('.tablesorter-header-inner').append('<span class="curcolor" />');
1096
- }
1097
-
1098
- $cell.find('.toggle, .colorpicker').bind('change', function(){
1099
- updateColor( $cell.find('.colorpicker').val() );
1100
- });
1101
-
1102
- // hidden filter update namespace trigger by filter widget
1103
- $input = $cell.find('input[type=hidden]').bind('change' + c.namespace + 'filter', function(){
1104
- updateColor( this.value );
1105
- });
1106
-
1107
- // update slider from hidden input, in case of saved filters
1108
- c.$table.bind('filterFomatterUpdate', function(){
1109
- updateColor( $input.val(), true );
1110
- ts.filter.formatterUpdated($cell, indx);
1111
- });
1112
-
1113
- // on reset
1114
- $cell.closest('table').bind('filterReset', function(){
1115
- // just turn off the colorpicker
1116
- if (o.addToggle) {
1117
- $cell.find('.toggle')[0].checked = false;
1118
- }
1119
- // delay needed because default color needs to be set in the filter
1120
- // there is no compare option here, so if addToggle = false,
1121
- // default color is #000000 (even with no value set)
1122
- setTimeout(function(){
1123
- updateColor();
1124
- }, 0);
1125
- });
1126
-
1127
- // has sticky headers?
1128
- c.$table.bind('stickyHeadersInit', function(){
1129
- $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx);
1130
- $shcell
1131
- .html(t)
1132
- .find('.toggle, .colorpicker').bind('change', function(){
1133
- updateColor( $shcell.find('.colorpicker').val() );
1134
- });
1135
- updateColor( $shcell.find('.colorpicker').val() );
1136
- });
1137
-
1138
- updateColor( o.value );
1139
- }
1140
- return colorSupported ? $cell.find('input[type="hidden"]') : $('<input type="search">');
1141
761
  }
1142
762
 
1143
763
  };
@@ -0,0 +1,1307 @@
1
+ /*! Widget: filter */
2
+ ;(function ($) {
3
+ 'use strict';
4
+ var ts = $.tablesorter = $.tablesorter || {};
5
+
6
+ $.extend(ts.css, {
7
+ filterRow : 'tablesorter-filter-row',
8
+ filter : 'tablesorter-filter'
9
+ });
10
+
11
+ ts.addWidget({
12
+ id: "filter",
13
+ priority: 50,
14
+ options : {
15
+ filter_childRows : false, // if true, filter includes child row content in the search
16
+ filter_columnFilters : true, // if true, a filter will be added to the top of each table column
17
+ filter_columnAnyMatch: true, // if true, allows using "#:{query}" in AnyMatch searches (column:query)
18
+ filter_cellFilter : '', // css class name added to the filter cell (string or array)
19
+ filter_cssFilter : '', // css class name added to the filter row & each input in the row (tablesorter-filter is ALWAYS added)
20
+ filter_defaultFilter : {}, // add a default column filter type "~{query}" to make fuzzy searches default; "{q1} AND {q2}" to make all searches use a logical AND.
21
+ filter_excludeFilter : {}, // filters to exclude, per column
22
+ filter_external : '', // jQuery selector string (or jQuery object) of external filters
23
+ filter_filteredRow : 'filtered', // class added to filtered rows; needed by pager plugin
24
+ filter_formatter : null, // add custom filter elements to the filter row
25
+ filter_functions : null, // add custom filter functions using this option
26
+ filter_hideEmpty : true, // hide filter row when table is empty
27
+ filter_hideFilters : false, // collapse filter row when mouse leaves the area
28
+ filter_ignoreCase : true, // if true, make all searches case-insensitive
29
+ filter_liveSearch : true, // if true, search column content while the user types (with a delay)
30
+ filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available (visible) options within the drop down
31
+ filter_placeholder : { search : '', select : '' }, // default placeholder text (overridden by any header "data-placeholder" setting)
32
+ filter_reset : null, // jQuery selector string of an element used to reset the filters
33
+ filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters
34
+ filter_searchDelay : 300, // typing delay in milliseconds before starting a search
35
+ filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true
36
+ filter_selectSource : null, // include a function to return an array of values to be added to the column filter select
37
+ filter_startsWith : false, // if true, filter start from the beginning of the cell contents
38
+ filter_useParsedData : false, // filter all data using parsed content
39
+ 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.
40
+ filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value
41
+ filter_selectSourceSeparator : '|' // filter_selectSource array text left of the separator is added to the option value, right into the option text
42
+ },
43
+ format: function(table, c, wo) {
44
+ if (!c.$table.hasClass('hasFilters')) {
45
+ ts.filter.init(table, c, wo);
46
+ }
47
+ },
48
+ remove: function(table, c, wo, refreshing) {
49
+ var tbodyIndex, $tbody,
50
+ $table = c.$table,
51
+ $tbodies = c.$tbodies,
52
+ events = 'addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join(c.namespace + 'filter ');
53
+ $table
54
+ .removeClass('hasFilters')
55
+ // add .tsfilter namespace to all BUT search
56
+ .unbind( events.replace(/\s+/g, ' ') )
57
+ // remove the filter row even if refreshing, because the column might have been moved
58
+ .find('.' + ts.css.filterRow).remove();
59
+ if (refreshing) { return; }
60
+ for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
61
+ $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
62
+ $tbody.children().removeClass(wo.filter_filteredRow).show();
63
+ ts.processTbody(table, $tbody, false); // restore tbody
64
+ }
65
+ if (wo.filter_reset) {
66
+ $(document).undelegate(wo.filter_reset, 'click.tsfilter');
67
+ }
68
+ }
69
+ });
70
+
71
+ ts.filter = {
72
+
73
+ // regex used in filter "check" functions - not for general use and not documented
74
+ regex: {
75
+ regex : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex
76
+ child : /tablesorter-childRow/, // child row class name; this gets updated in the script
77
+ filtered : /filtered/, // filtered (hidden) row class name; updated in the script
78
+ type : /undefined|number/, // check type
79
+ exact : /(^[\"\'=]+)|([\"\'=]+$)/g, // exact match (allow '==')
80
+ nondigit : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser)
81
+ operators : /[<>=]/g, // replace operators
82
+ query : '(q|query)' // replace filter queries
83
+ },
84
+ // function( c, data ) { }
85
+ // c = table.config
86
+ // data.filter = array of filter input values;
87
+ // data.iFilter = same array, except lowercase (if wo.filter_ignoreCase is true)
88
+ // data.exact = table cell text (or parsed data if column parser enabled)
89
+ // data.iExact = same as data.exact, except lowercase (if wo.filter_ignoreCase is true)
90
+ // data.cache = table cell text from cache, so it has been parsed (& in all lower case if config.ignoreCase is true)
91
+ // data.index = column index; table = table element (DOM)
92
+ // data.parsed = array (by column) of boolean values (from filter_useParsedData or "filter-parsed" class)
93
+ types: {
94
+ // Look for regex
95
+ regex: function( c, data ) {
96
+ if ( ts.filter.regex.regex.test(data.iFilter) ) {
97
+ var matches,
98
+ regex = ts.filter.regex.regex.exec(data.iFilter);
99
+ try {
100
+ matches = new RegExp(regex[1], regex[2]).test( data.iExact );
101
+ } catch (error) {
102
+ matches = false;
103
+ }
104
+ return matches;
105
+ }
106
+ return null;
107
+ },
108
+ // Look for operators >, >=, < or <=
109
+ operators: function( c, data ) {
110
+ if ( /^[<>]=?/.test(data.iFilter) ) {
111
+ var cachedValue, result,
112
+ table = c.table,
113
+ index = data.index,
114
+ parsed = data.parsed[index],
115
+ query = ts.formatFloat( data.iFilter.replace(ts.filter.regex.operators, ''), table ),
116
+ parser = c.parsers[index],
117
+ savedSearch = query;
118
+ // parse filter value in case we're comparing numbers (dates)
119
+ if (parsed || parser.type === 'numeric') {
120
+ result = ts.filter.parseFilter(c, $.trim('' + data.iFilter.replace(ts.filter.regex.operators, '')), index, parsed, true);
121
+ query = ( typeof result === "number" && result !== '' && !isNaN(result) ) ? result : query;
122
+ }
123
+
124
+ // iExact may be numeric - see issue #149;
125
+ // check if cached is defined, because sometimes j goes out of range? (numeric columns)
126
+ cachedValue = ( parsed || parser.type === 'numeric' ) && !isNaN(query) && typeof data.cache !== 'undefined' ? data.cache :
127
+ isNaN(data.iExact) ? ts.formatFloat( data.iExact.replace(ts.filter.regex.nondigit, ''), table) :
128
+ ts.formatFloat( data.iExact, table );
129
+
130
+ if ( />/.test(data.iFilter) ) { result = />=/.test(data.iFilter) ? cachedValue >= query : cachedValue > query; }
131
+ if ( /</.test(data.iFilter) ) { result = /<=/.test(data.iFilter) ? cachedValue <= query : cachedValue < query; }
132
+ // keep showing all rows if nothing follows the operator
133
+ if ( !result && savedSearch === '' ) { result = true; }
134
+ return result;
135
+ }
136
+ return null;
137
+ },
138
+ // Look for a not match
139
+ notMatch: function( c, data ) {
140
+ if ( /^\!/.test(data.iFilter) ) {
141
+ var indx,
142
+ filter = ts.filter.parseFilter(c, data.iFilter.replace('!', ''), data.index, data.parsed[data.index]);
143
+ if (ts.filter.regex.exact.test(filter)) {
144
+ // look for exact not matches - see #628
145
+ filter = filter.replace(ts.filter.regex.exact, '');
146
+ return filter === '' ? true : $.trim(filter) !== data.iExact;
147
+ } else {
148
+ indx = data.iExact.search( $.trim(filter) );
149
+ return filter === '' ? true : !(c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 0);
150
+ }
151
+ }
152
+ return null;
153
+ },
154
+ // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric
155
+ exact: function( c, data ) {
156
+ /*jshint eqeqeq:false */
157
+ if (ts.filter.regex.exact.test(data.iFilter)) {
158
+ var filter = ts.filter.parseFilter(c, data.iFilter.replace(ts.filter.regex.exact, ''), data.index, data.parsed[data.index]);
159
+ return data.anyMatch ? $.inArray(filter, data.rowArray) >= 0 : filter == data.iExact;
160
+ }
161
+ return null;
162
+ },
163
+ // Look for an AND or && operator (logical and)
164
+ and : function( c, data ) {
165
+ if ( ts.filter.regex.andTest.test(data.filter) ) {
166
+ var index = data.index,
167
+ parsed = data.parsed[index],
168
+ query = data.iFilter.split( ts.filter.regex.andSplit ),
169
+ result = data.iExact.search( $.trim( ts.filter.parseFilter(c, query[0], index, parsed) ) ) >= 0,
170
+ indx = query.length - 1;
171
+ while (result && indx) {
172
+ result = result && data.iExact.search( $.trim( ts.filter.parseFilter(c, query[indx], index, parsed) ) ) >= 0;
173
+ indx--;
174
+ }
175
+ return result;
176
+ }
177
+ return null;
178
+ },
179
+ // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu!
180
+ range : function( c, data ) {
181
+ if ( ts.filter.regex.toTest.test(data.iFilter) ) {
182
+ var result, tmp,
183
+ table = c.table,
184
+ index = data.index,
185
+ parsed = data.parsed[index],
186
+ // make sure the dash is for a range and not indicating a negative number
187
+ query = data.iFilter.split( ts.filter.regex.toSplit ),
188
+ range1 = ts.formatFloat( ts.filter.parseFilter(c, query[0].replace(ts.filter.regex.nondigit, ''), index, parsed), table ),
189
+ range2 = ts.formatFloat( ts.filter.parseFilter(c, query[1].replace(ts.filter.regex.nondigit, ''), index, parsed), table );
190
+ // parse filter value in case we're comparing numbers (dates)
191
+ if (parsed || c.parsers[index].type === 'numeric') {
192
+ result = c.parsers[index].format('' + query[0], table, c.$headers.eq(index), index);
193
+ range1 = (result !== '' && !isNaN(result)) ? result : range1;
194
+ result = c.parsers[index].format('' + query[1], table, c.$headers.eq(index), index);
195
+ range2 = (result !== '' && !isNaN(result)) ? result : range2;
196
+ }
197
+ result = ( parsed || c.parsers[index].type === 'numeric' ) && !isNaN(range1) && !isNaN(range2) ? data.cache :
198
+ isNaN(data.iExact) ? ts.formatFloat( data.iExact.replace(ts.filter.regex.nondigit, ''), table) :
199
+ ts.formatFloat( data.iExact, table );
200
+ if (range1 > range2) { tmp = range1; range1 = range2; range2 = tmp; } // swap
201
+ return (result >= range1 && result <= range2) || (range1 === '' || range2 === '');
202
+ }
203
+ return null;
204
+ },
205
+ // Look for wild card: ? = single, * = multiple, or | = logical OR
206
+ wild : function( c, data ) {
207
+ if ( /[\?\*\|]/.test(data.iFilter) || ts.filter.regex.orReplace.test(data.filter) ) {
208
+ var index = data.index,
209
+ parsed = data.parsed[index],
210
+ query = ts.filter.parseFilter(c, data.iFilter.replace(ts.filter.regex.orReplace, "|"), index, parsed);
211
+ // look for an exact match with the "or" unless the "filter-match" class is found
212
+ if (!c.$headers.filter('[data-column="' + index + '"]:last').hasClass('filter-match') && /\|/.test(query)) {
213
+ // show all results while using filter match. Fixes #727
214
+ if (query[ query.length - 1 ] === '|') { query += '*'; }
215
+ query = data.anyMatch && $.isArray(data.rowArray) ? '(' + query + ')' : '^(' + query + ')$';
216
+ }
217
+ // parsing the filter may not work properly when using wildcards =/
218
+ return new RegExp( query.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(data.iExact);
219
+ }
220
+ return null;
221
+ },
222
+ // fuzzy text search; modified from https://github.com/mattyork/fuzzy (MIT license)
223
+ fuzzy: function( c, data ) {
224
+ if ( /^~/.test(data.iFilter) ) {
225
+ var indx,
226
+ patternIndx = 0,
227
+ len = data.iExact.length,
228
+ pattern = ts.filter.parseFilter(c, data.iFilter.slice(1), data.index, data.parsed[data.index]);
229
+ for (indx = 0; indx < len; indx++) {
230
+ if (data.iExact[indx] === pattern[patternIndx]) {
231
+ patternIndx += 1;
232
+ }
233
+ }
234
+ if (patternIndx === pattern.length) {
235
+ return true;
236
+ }
237
+ return false;
238
+ }
239
+ return null;
240
+ }
241
+ },
242
+ init: function(table, c, wo) {
243
+ // filter language options
244
+ ts.language = $.extend(true, {}, {
245
+ to : 'to',
246
+ or : 'or',
247
+ and : 'and'
248
+ }, ts.language);
249
+
250
+ var options, string, txt, $header, column, filters, val, fxn, noSelect,
251
+ regex = ts.filter.regex;
252
+ c.$table.addClass('hasFilters');
253
+
254
+ // define timers so using clearTimeout won't cause an undefined error
255
+ wo.searchTimer = null;
256
+ wo.filter_initTimer = null;
257
+ wo.filter_formatterCount = 0;
258
+ wo.filter_formatterInit = [];
259
+ wo.filter_anyColumnSelector = '[data-column="all"],[data-column="any"]';
260
+ wo.filter_multipleColumnSelector = '[data-column*="-"],[data-column*=","]';
261
+
262
+ txt = '\\{' + ts.filter.regex.query + '\\}';
263
+ $.extend( regex, {
264
+ child : new RegExp(c.cssChildRow),
265
+ filtered : new RegExp(wo.filter_filteredRow),
266
+ alreadyFiltered : new RegExp('(\\s+(' + ts.language.or + '|-|' + ts.language.to + ')\\s+)', 'i'),
267
+ toTest : new RegExp('\\s+(-|' + ts.language.to + ')\\s+', 'i'),
268
+ toSplit : new RegExp('(?:\\s+(?:-|' + ts.language.to + ')\\s+)' ,'gi'),
269
+ andTest : new RegExp('\\s+(' + ts.language.and + '|&&)\\s+', 'i'),
270
+ andSplit : new RegExp('(?:\\s+(?:' + ts.language.and + '|&&)\\s+)', 'gi'),
271
+ orReplace : new RegExp('\\s+(' + ts.language.or + ')\\s+', 'gi'),
272
+ iQuery : new RegExp(txt, 'i'),
273
+ igQuery : new RegExp(txt, 'ig')
274
+ });
275
+
276
+ // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156
277
+ if (wo.filter_columnFilters !== false && c.$headers.filter('.filter-false, .parser-false').length !== c.$headers.length) {
278
+ // build filter row
279
+ ts.filter.buildRow(table, c, wo);
280
+ }
281
+
282
+ txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join(c.namespace + 'filter ');
283
+ c.$table.bind( txt, function(event, filter) {
284
+ val = (wo.filter_hideEmpty && $.isEmptyObject(c.cache) && !(c.delayInit && event.type === 'appendCache'));
285
+ // hide filter row using the "filtered" class name
286
+ c.$table.find('.' + ts.css.filterRow).toggleClass(wo.filter_filteredRow, val ); // fixes #450
287
+ if ( !/(search|filter)/.test(event.type) ) {
288
+ event.stopPropagation();
289
+ ts.filter.buildDefault(table, true);
290
+ }
291
+ if (event.type === 'filterReset') {
292
+ c.$table.find('.' + ts.css.filter).add(wo.filter_$externalFilters).val('');
293
+ ts.filter.searching(table, []);
294
+ } else if (event.type === 'filterEnd') {
295
+ ts.filter.buildDefault(table, true);
296
+ } else {
297
+ // send false argument to force a new search; otherwise if the filter hasn't changed, it will return
298
+ filter = event.type === 'search' ? filter : event.type === 'updateComplete' ? c.$table.data('lastSearch') : '';
299
+ if (/(update|add)/.test(event.type) && event.type !== "updateComplete") {
300
+ // force a new search since content has changed
301
+ c.lastCombinedFilter = null;
302
+ c.lastSearch = [];
303
+ }
304
+ // pass true (skipFirst) to prevent the tablesorter.setFilters function from skipping the first input
305
+ // ensures all inputs are updated when a search is triggered on the table $('table').trigger('search', [...]);
306
+ ts.filter.searching(table, filter, true);
307
+ }
308
+ return false;
309
+ });
310
+
311
+ // reset button/link
312
+ if (wo.filter_reset) {
313
+ if (wo.filter_reset instanceof $) {
314
+ // reset contains a jQuery object, bind to it
315
+ wo.filter_reset.click(function(){
316
+ c.$table.trigger('filterReset');
317
+ });
318
+ } else if ($(wo.filter_reset).length) {
319
+ // reset is a jQuery selector, use event delegation
320
+ $(document)
321
+ .undelegate(wo.filter_reset, 'click.tsfilter')
322
+ .delegate(wo.filter_reset, 'click.tsfilter', function() {
323
+ // trigger a reset event, so other functions (filter_formatter) know when to reset
324
+ c.$table.trigger('filterReset');
325
+ });
326
+ }
327
+ }
328
+ if (wo.filter_functions) {
329
+ for (column = 0; column < c.columns; column++) {
330
+ fxn = ts.getColumnData( table, wo.filter_functions, column );
331
+ if (fxn) {
332
+ // remove "filter-select" from header otherwise the options added here are replaced with all options
333
+ $header = c.$headers.filter('[data-column="' + column + '"]:last').removeClass('filter-select');
334
+ // don't build select if "filter-false" or "parser-false" set
335
+ noSelect = !($header.hasClass('filter-false') || $header.hasClass('parser-false'));
336
+ options = '';
337
+ if ( fxn === true && noSelect ) {
338
+ ts.filter.buildSelect(table, column);
339
+ } else if ( typeof fxn === 'object' && noSelect ) {
340
+ // add custom drop down list
341
+ for (string in fxn) {
342
+ if (typeof string === 'string') {
343
+ options += options === '' ?
344
+ '<option value="">' + ($header.data('placeholder') || $header.attr('data-placeholder') || wo.filter_placeholder.select || '') + '</option>' : '';
345
+ val = string;
346
+ txt = string;
347
+ if (string.indexOf(wo.filter_selectSourceSeparator) >= 0) {
348
+ val = string.split(wo.filter_selectSourceSeparator);
349
+ txt = val[1];
350
+ val = val[0];
351
+ }
352
+ options += '<option ' + (txt === val ? '' : 'data-function-name="' + string + '" ') + 'value="' + val + '">' + txt + '</option>';
353
+ }
354
+ }
355
+ c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]').append(options);
356
+ }
357
+ }
358
+ }
359
+ }
360
+ // not really updating, but if the column has both the "filter-select" class & filter_functions set to true,
361
+ // it would append the same options twice.
362
+ ts.filter.buildDefault(table, true);
363
+
364
+ ts.filter.bindSearch( table, c.$table.find('.' + ts.css.filter), true );
365
+ if (wo.filter_external) {
366
+ ts.filter.bindSearch( table, wo.filter_external );
367
+ }
368
+
369
+ if (wo.filter_hideFilters) {
370
+ ts.filter.hideFilters(table, c);
371
+ }
372
+
373
+ // show processing icon
374
+ if (c.showProcessing) {
375
+ c.$table
376
+ .unbind( ('filterStart filterEnd '.split(' ').join(c.namespace + 'filter ')).replace(/\s+/g, ' ') )
377
+ .bind( 'filterStart filterEnd '.split(' ').join(c.namespace + 'filter '), function(event, columns) {
378
+ // only add processing to certain columns to all columns
379
+ $header = (columns) ? c.$table.find('.' + ts.css.header).filter('[data-column]').filter(function() {
380
+ return columns[$(this).data('column')] !== '';
381
+ }) : '';
382
+ ts.isProcessing(table, event.type === 'filterStart', columns ? $header : '');
383
+ });
384
+ }
385
+
386
+ // set filtered rows count (intially unfiltered)
387
+ c.filteredRows = c.totalRows;
388
+
389
+ // add default values
390
+ c.$table
391
+ .unbind( ('tablesorter-initialized pagerBeforeInitialized '.split(' ').join(c.namespace + 'filter ')).replace(/\s+/g, ' ') )
392
+ .bind( 'tablesorter-initialized pagerBeforeInitialized '.split(' ').join(c.namespace + 'filter '), function() {
393
+ // redefine "wo" as it does not update properly inside this callback
394
+ var wo = this.config.widgetOptions;
395
+ filters = ts.filter.setDefaults(table, c, wo) || [];
396
+ if (filters.length) {
397
+ // prevent delayInit from triggering a cache build if filters are empty
398
+ if ( !(c.delayInit && filters.join('') === '') ) {
399
+ ts.setFilters(table, filters, true);
400
+ }
401
+ }
402
+ c.$table.trigger('filterFomatterUpdate');
403
+ // trigger init after setTimeout to prevent multiple filterStart/End/Init triggers
404
+ setTimeout(function(){
405
+ if (!wo.filter_initialized) {
406
+ ts.filter.filterInitComplete(c);
407
+ }
408
+ }, 100);
409
+ });
410
+ // if filter widget is added after pager has initialized; then set filter init flag
411
+ if (c.pager && c.pager.initialized && !wo.filter_initialized) {
412
+ c.$table.trigger('filterFomatterUpdate');
413
+ setTimeout(function(){
414
+ ts.filter.filterInitComplete(c);
415
+ }, 100);
416
+ }
417
+ },
418
+ // $cell parameter, but not the config, is passed to the
419
+ // filter_formatters, so we have to work with it instead
420
+ formatterUpdated: function($cell, column) {
421
+ var wo = $cell.closest('table')[0].config.widgetOptions;
422
+ if (!wo.filter_initialized) {
423
+ // add updates by column since this function
424
+ // may be called numerous times before initialization
425
+ wo.filter_formatterInit[column] = 1;
426
+ }
427
+ },
428
+ filterInitComplete: function(c){
429
+ var wo = c.widgetOptions,
430
+ count = 0,
431
+ completed = function(){
432
+ wo.filter_initialized = true;
433
+ c.$table.trigger('filterInit', c);
434
+ ts.filter.findRows(c.table, c.$table.data('lastSearch') || []);
435
+ };
436
+ if ( $.isEmptyObject( wo.filter_formatter ) ) {
437
+ completed();
438
+ } else {
439
+ $.each( wo.filter_formatterInit, function(i, val) {
440
+ if (val === 1) {
441
+ count++;
442
+ }
443
+ });
444
+ clearTimeout(wo.filter_initTimer);
445
+ if (!wo.filter_initialized && count === wo.filter_formatterCount) {
446
+ // filter widget initialized
447
+ completed();
448
+ } else if (!wo.filter_initialized) {
449
+ // fall back in case a filter_formatter doesn't call
450
+ // $.tablesorter.filter.formatterUpdated($cell, column), and the count is off
451
+ wo.filter_initTimer = setTimeout(function(){
452
+ completed();
453
+ }, 500);
454
+ }
455
+ }
456
+ },
457
+
458
+ setDefaults: function(table, c, wo) {
459
+ var isArray, saved, indx,
460
+ // get current (default) filters
461
+ filters = ts.getFilters(table) || [];
462
+ if (wo.filter_saveFilters && ts.storage) {
463
+ saved = ts.storage( table, 'tablesorter-filters' ) || [];
464
+ isArray = $.isArray(saved);
465
+ // make sure we're not just getting an empty array
466
+ if ( !(isArray && saved.join('') === '' || !isArray) ) { filters = saved; }
467
+ }
468
+ // if no filters saved, then check default settings
469
+ if (filters.join('') === '') {
470
+ for (indx = 0; indx < c.columns; indx++) {
471
+ filters[indx] = c.$headers.filter('[data-column="' + indx + '"]:last').attr(wo.filter_defaultAttrib) || filters[indx];
472
+ }
473
+ }
474
+ c.$table.data('lastSearch', filters);
475
+ return filters;
476
+ },
477
+ parseFilter: function(c, filter, column, parsed, forceParse){
478
+ return forceParse || parsed ?
479
+ c.parsers[column].format( filter, c.table, [], column ) :
480
+ filter;
481
+ },
482
+ buildRow: function(table, c, wo) {
483
+ var col, column, $header, buildSelect, disabled, name, ffxn,
484
+ // c.columns defined in computeThIndexes()
485
+ columns = c.columns,
486
+ arry = $.isArray(wo.filter_cellFilter),
487
+ buildFilter = '<tr role="row" class="' + ts.css.filterRow + '">';
488
+ for (column = 0; column < columns; column++) {
489
+ if (arry) {
490
+ buildFilter += '<td' + ( wo.filter_cellFilter[column] ? ' class="' + wo.filter_cellFilter[column] + '"' : '' ) + '></td>';
491
+ } else {
492
+ buildFilter += '<td' + ( wo.filter_cellFilter !== '' ? ' class="' + wo.filter_cellFilter + '"' : '' ) + '></td>';
493
+ }
494
+ }
495
+ c.$filters = $(buildFilter += '</tr>').appendTo( c.$table.children('thead').eq(0) ).find('td');
496
+ // build each filter input
497
+ for (column = 0; column < columns; column++) {
498
+ disabled = false;
499
+ // assuming last cell of a column is the main column
500
+ $header = c.$headers.filter('[data-column="' + column + '"]:last');
501
+ ffxn = ts.getColumnData( table, wo.filter_functions, column );
502
+ buildSelect = (wo.filter_functions && ffxn && typeof ffxn !== "function" ) ||
503
+ $header.hasClass('filter-select');
504
+ // get data from jQuery data, metadata, headers option or header class name
505
+ col = ts.getColumnData( table, c.headers, column );
506
+ disabled = ts.getData($header[0], col, 'filter') === 'false' || ts.getData($header[0], col, 'parser') === 'false';
507
+
508
+ if (buildSelect) {
509
+ buildFilter = $('<select>').appendTo( c.$filters.eq(column) );
510
+ } else {
511
+ ffxn = ts.getColumnData( table, wo.filter_formatter, column );
512
+ if (ffxn) {
513
+ wo.filter_formatterCount++;
514
+ buildFilter = ffxn( c.$filters.eq(column), column );
515
+ // no element returned, so lets go find it
516
+ if (buildFilter && buildFilter.length === 0) {
517
+ buildFilter = c.$filters.eq(column).children('input');
518
+ }
519
+ // element not in DOM, so lets attach it
520
+ if ( buildFilter && (buildFilter.parent().length === 0 ||
521
+ (buildFilter.parent().length && buildFilter.parent()[0] !== c.$filters[column])) ) {
522
+ c.$filters.eq(column).append(buildFilter);
523
+ }
524
+ } else {
525
+ buildFilter = $('<input type="search">').appendTo( c.$filters.eq(column) );
526
+ }
527
+ if (buildFilter) {
528
+ buildFilter.attr('placeholder', $header.data('placeholder') || $header.attr('data-placeholder') || wo.filter_placeholder.search || '');
529
+ }
530
+ }
531
+ if (buildFilter) {
532
+ // add filter class name
533
+ name = ( $.isArray(wo.filter_cssFilter) ?
534
+ (typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '') :
535
+ wo.filter_cssFilter ) || '';
536
+ buildFilter.addClass( ts.css.filter + ' ' + name ).attr('data-column', column);
537
+ if (disabled) {
538
+ buildFilter.attr('placeholder', '').addClass('disabled')[0].disabled = true; // disabled!
539
+ }
540
+ }
541
+ }
542
+ },
543
+ bindSearch: function(table, $el, internal) {
544
+ table = $(table)[0];
545
+ $el = $($el); // allow passing a selector string
546
+ if (!$el.length) { return; }
547
+ var c = table.config,
548
+ wo = c.widgetOptions,
549
+ $ext = wo.filter_$externalFilters;
550
+ if (internal !== true) {
551
+ // save anyMatch element
552
+ wo.filter_$anyMatch = $el.filter(wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector);
553
+ if ($ext && $ext.length) {
554
+ wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el );
555
+ } else {
556
+ wo.filter_$externalFilters = $el;
557
+ }
558
+ // update values (external filters added after table initialization)
559
+ ts.setFilters(table, c.$table.data('lastSearch') || [], internal === false);
560
+ }
561
+ $el
562
+ // use data attribute instead of jQuery data since the head is cloned without including the data/binding
563
+ .attr('data-lastSearchTime', new Date().getTime())
564
+ .unbind( ('keypress keyup search change '.split(' ').join(c.namespace + 'filter ')).replace(/\s+/g, ' ') )
565
+ // include change for select - fixes #473
566
+ .bind('keyup' + c.namespace + 'filter', function(event) {
567
+ $(this).attr('data-lastSearchTime', new Date().getTime());
568
+ // emulate what webkit does.... escape clears the filter
569
+ if (event.which === 27) {
570
+ this.value = '';
571
+ // live search
572
+ } else if ( wo.filter_liveSearch === false ) {
573
+ return;
574
+ // don't return if the search value is empty (all rows need to be revealed)
575
+ } else if ( this.value !== '' && (
576
+ // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace
577
+ ( typeof wo.filter_liveSearch === 'number' && this.value.length < wo.filter_liveSearch ) ||
578
+ // let return & backspace continue on, but ignore arrows & non-valid characters
579
+ ( event.which !== 13 && event.which !== 8 && ( event.which < 32 || (event.which >= 37 && event.which <= 40) ) ) ) ) {
580
+ return;
581
+ }
582
+ // change event = no delay; last true flag tells getFilters to skip newest timed input
583
+ ts.filter.searching( table, true, true );
584
+ })
585
+ .bind( 'search change keypress '.split(' ').join(c.namespace + 'filter '), function(event){
586
+ var column = $(this).data('column');
587
+ // don't allow "change" event to process if the input value is the same - fixes #685
588
+ if (event.which === 13 || event.type === 'search' || event.type === 'change' && this.value !== c.lastSearch[column]) {
589
+ event.preventDefault();
590
+ // init search with no delay
591
+ $(this).attr('data-lastSearchTime', new Date().getTime());
592
+ ts.filter.searching( table, false, true );
593
+ }
594
+ });
595
+ },
596
+ searching: function(table, filter, skipFirst) {
597
+ var wo = table.config.widgetOptions;
598
+ clearTimeout(wo.searchTimer);
599
+ if (typeof filter === 'undefined' || filter === true) {
600
+ // delay filtering
601
+ wo.searchTimer = setTimeout(function() {
602
+ ts.filter.checkFilters(table, filter, skipFirst );
603
+ }, wo.filter_liveSearch ? wo.filter_searchDelay : 10);
604
+ } else {
605
+ // skip delay
606
+ ts.filter.checkFilters(table, filter, skipFirst);
607
+ }
608
+ },
609
+ checkFilters: function(table, filter, skipFirst) {
610
+ var c = table.config,
611
+ wo = c.widgetOptions,
612
+ filterArray = $.isArray(filter),
613
+ filters = (filterArray) ? filter : ts.getFilters(table, true),
614
+ combinedFilters = (filters || []).join(''); // combined filter values
615
+ // prevent errors if delay init is set
616
+ if ($.isEmptyObject(c.cache)) {
617
+ // update cache if delayInit set & pager has initialized (after user initiates a search)
618
+ if (c.delayInit && c.pager && c.pager.initialized) {
619
+ c.$table.trigger('updateCache', [function(){
620
+ ts.filter.checkFilters(table, false, skipFirst);
621
+ }] );
622
+ }
623
+ return;
624
+ }
625
+ // add filter array back into inputs
626
+ if (filterArray) {
627
+ ts.setFilters( table, filters, false, skipFirst !== true );
628
+ if (!wo.filter_initialized) { c.lastCombinedFilter = ''; }
629
+ }
630
+ if (wo.filter_hideFilters) {
631
+ // show/hide filter row as needed
632
+ c.$table.find('.' + ts.css.filterRow).trigger( combinedFilters === '' ? 'mouseleave' : 'mouseenter' );
633
+ }
634
+ // return if the last search is the same; but filter === false when updating the search
635
+ // see example-widget-filter.html filter toggle buttons
636
+ if (c.lastCombinedFilter === combinedFilters && filter !== false) {
637
+ return;
638
+ } else if (filter === false) {
639
+ // force filter refresh
640
+ c.lastCombinedFilter = null;
641
+ c.lastSearch = [];
642
+ }
643
+ if (wo.filter_initialized) { c.$table.trigger('filterStart', [filters]); }
644
+ if (c.showProcessing) {
645
+ // give it time for the processing icon to kick in
646
+ setTimeout(function() {
647
+ ts.filter.findRows(table, filters, combinedFilters);
648
+ return false;
649
+ }, 30);
650
+ } else {
651
+ ts.filter.findRows(table, filters, combinedFilters);
652
+ return false;
653
+ }
654
+ },
655
+ hideFilters: function(table, c) {
656
+ var $filterRow, $filterRow2, timer;
657
+ $(table)
658
+ .find('.' + ts.css.filterRow)
659
+ .addClass('hideme')
660
+ .bind('mouseenter mouseleave', function(e) {
661
+ // save event object - http://bugs.jquery.com/ticket/12140
662
+ var event = e;
663
+ $filterRow = $(this);
664
+ clearTimeout(timer);
665
+ timer = setTimeout(function() {
666
+ if ( /enter|over/.test(event.type) ) {
667
+ $filterRow.removeClass('hideme');
668
+ } else {
669
+ // don't hide if input has focus
670
+ // $(':focus') needs jQuery 1.6+
671
+ if ( $(document.activeElement).closest('tr')[0] !== $filterRow[0] ) {
672
+ // don't hide row if any filter has a value
673
+ if (c.lastCombinedFilter === '') {
674
+ $filterRow.addClass('hideme');
675
+ }
676
+ }
677
+ }
678
+ }, 200);
679
+ })
680
+ .find('input, select').bind('focus blur', function(e) {
681
+ $filterRow2 = $(this).closest('tr');
682
+ clearTimeout(timer);
683
+ var event = e;
684
+ timer = setTimeout(function() {
685
+ // don't hide row if any filter has a value
686
+ if (ts.getFilters(c.$table).join('') === '') {
687
+ $filterRow2[ event.type === 'focus' ? 'removeClass' : 'addClass']('hideme');
688
+ }
689
+ }, 200);
690
+ });
691
+ },
692
+ defaultFilter: function(filter, mask){
693
+ if (filter === '') { return filter; }
694
+ var regex = ts.filter.regex.iQuery,
695
+ maskLen = mask.match( ts.filter.regex.igQuery ).length,
696
+ query = maskLen > 1 ? $.trim(filter).split(/\s/) : [ $.trim(filter) ],
697
+ len = query.length - 1,
698
+ indx = 0,
699
+ val = mask;
700
+ if ( len < 1 && maskLen > 1 ) {
701
+ // only one "word" in query but mask has >1 slots
702
+ query[1] = query[0];
703
+ }
704
+ // replace all {query} with query words...
705
+ // if query = "Bob", then convert mask from "!{query}" to "!Bob"
706
+ // if query = "Bob Joe Frank", then convert mask "{q} OR {q}" to "Bob OR Joe OR Frank"
707
+ while (regex.test(val)) {
708
+ val = val.replace(regex, query[indx++] || '');
709
+ if (regex.test(val) && indx < len && (query[indx] || '') !== '') {
710
+ val = mask.replace(regex, val);
711
+ }
712
+ }
713
+ return val;
714
+ },
715
+ getLatestSearch: function( $input ) {
716
+ if ($input) {
717
+ return $input.sort(function(a, b) {
718
+ return $(b).attr('data-lastSearchTime') - $(a).attr('data-lastSearchTime');
719
+ });
720
+ }
721
+ return $();
722
+ },
723
+ multipleColumns: function( c, $input ) {
724
+ // look for multiple columns "1-3,4-6,8" in data-column
725
+ var ranges, singles, indx,
726
+ wo = c.widgetOptions,
727
+ // only target "all" column inputs on initialization
728
+ // & don't target "all" column inputs if they don't exist
729
+ targets = wo.filter_initialized || !$input.filter(wo.filter_anyColumnSelector).length,
730
+ columns = [],
731
+ val = $.trim( ts.filter.getLatestSearch( $input ).attr('data-column') || '' );
732
+ // process column range
733
+ if ( targets && /-/.test( val ) ) {
734
+ ranges = val.match( /(\d+)\s*-\s*(\d+)/g );
735
+ $.each(ranges, function(i,v){
736
+ var t,
737
+ range = v.split( /\s*-\s*/ ),
738
+ start = parseInt( range[0], 10 ) || 0,
739
+ end = parseInt( range[1], 10 ) || ( c.columns - 1 );
740
+ if ( start > end ) { t = start; start = end; end = t; } // swap
741
+ if ( end >= c.columns ) { end = c.columns - 1; }
742
+ for ( ; start <= end; start++ ) {
743
+ columns.push(start);
744
+ }
745
+ // remove processed range from val
746
+ val = val.replace( v, '' );
747
+ });
748
+ }
749
+ // process single columns
750
+ if ( targets && /,/.test( val ) ) {
751
+ singles = val.split( /\s*,\s*/ );
752
+ $.each( singles, function(i,v) {
753
+ if (v !== '') {
754
+ indx = parseInt( v, 10 );
755
+ if ( indx < c.columns ) {
756
+ columns.push( indx );
757
+ }
758
+ }
759
+ });
760
+ }
761
+ // return all columns
762
+ if (!columns.length) {
763
+ for ( indx = 0; indx < c.columns; indx++ ) {
764
+ columns.push( indx );
765
+ }
766
+ }
767
+ return columns;
768
+ },
769
+ findRows: function(table, filters, combinedFilters) {
770
+ if (table.config.lastCombinedFilter === combinedFilters || !table.config.widgetOptions.filter_initialized) { return; }
771
+ var len, norm_rows, $rows, rowIndex, tbodyIndex, $tbody, $cells, $cell, columnIndex,
772
+ childRow, lastSearch, hasSelect, matches, result, showRow, time, val, indx,
773
+ notFiltered, searchFiltered, filterMatched, excludeMatch, fxn, ffxn,
774
+ query, injected, res, id,
775
+ regex = ts.filter.regex,
776
+ c = table.config,
777
+ wo = c.widgetOptions,
778
+ // data object passed to filters; anyMatch is a flag for the filters
779
+ data = { anyMatch: false },
780
+ // anyMatch really screws up with these types of filters
781
+ noAnyMatch = [ 'range', 'notMatch', 'operators' ];
782
+
783
+ // parse columns after formatter, in case the class is added at that point
784
+ data.parsed = c.$headers.map(function(columnIndex) {
785
+ return c.parsers && c.parsers[columnIndex] && c.parsers[columnIndex].parsed ||
786
+ // getData won't return "parsed" if other "filter-" class names exist (e.g. <th class="filter-select filter-parsed">)
787
+ ts.getData && ts.getData(c.$headers.filter('[data-column="' + columnIndex + '"]:last'), ts.getColumnData( table, c.headers, columnIndex ), 'filter') === 'parsed' ||
788
+ $(this).hasClass('filter-parsed');
789
+ }).get();
790
+
791
+ if (c.debug) {
792
+ ts.log('Starting filter widget search', filters);
793
+ time = new Date();
794
+ }
795
+ // filtered rows count
796
+ c.filteredRows = 0;
797
+ c.totalRows = 0;
798
+ // combindedFilters are undefined on init
799
+ combinedFilters = (filters || []).join('');
800
+
801
+ for (tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) {
802
+ $tbody = ts.processTbody(table, c.$tbodies.eq(tbodyIndex), true);
803
+ // skip child rows & widget added (removable) rows - fixes #448 thanks to @hempel!
804
+ // $rows = $tbody.children('tr').not(c.selectorRemove);
805
+ columnIndex = c.columns;
806
+ // convert stored rows into a jQuery object
807
+ norm_rows = c.cache[tbodyIndex].normalized;
808
+ $rows = $( $.map(norm_rows, function(el){ return el[columnIndex].$row.get(); }) );
809
+
810
+ if (combinedFilters === '' || wo.filter_serversideFiltering) {
811
+ $rows.removeClass(wo.filter_filteredRow).not('.' + c.cssChildRow).show();
812
+ } else {
813
+ // filter out child rows
814
+ $rows = $rows.not('.' + c.cssChildRow);
815
+ len = $rows.length;
816
+
817
+ if ( (wo.filter_$anyMatch && wo.filter_$anyMatch.length) || typeof filters[c.columns] !== 'undefined' ) {
818
+ data.anyMatchFlag = true;
819
+ data.anyMatchFilter = wo.filter_$anyMatch && ts.filter.getLatestSearch( wo.filter_$anyMatch ).val() || ( '' + filters[c.columns] ) || '';
820
+ if (wo.filter_columnAnyMatch) {
821
+ // specific columns search
822
+ query = data.anyMatchFilter.split( ts.filter.regex.andSplit );
823
+ injected = false;
824
+ for (indx = 0; indx < query.length; indx++) {
825
+ res = query[indx].split(':');
826
+ if ( res.length > 1 ) {
827
+ // make the column a one-based index ( non-developers start counting from one :P )
828
+ id = parseInt( res[0], 10 ) - 1;
829
+ if ( id >= 0 && id < c.columns ) { // if id is an integer
830
+ filters[id] = res[1];
831
+ query.splice(indx, 1);
832
+ indx--;
833
+ injected = true;
834
+ }
835
+ }
836
+ }
837
+ if (injected) {
838
+ data.anyMatchFilter = query.join(' && ');
839
+ }
840
+ }
841
+ }
842
+
843
+ // optimize searching only through already filtered rows - see #313
844
+ searchFiltered = wo.filter_searchFiltered;
845
+ lastSearch = c.lastSearch || c.$table.data('lastSearch') || [];
846
+ if (searchFiltered) {
847
+ // cycle through all filters; include last (columnIndex + 1 = match any column). Fixes #669
848
+ for (indx = 0; indx < columnIndex + 1; indx++) {
849
+ val = filters[indx] || '';
850
+ // break out of loop if we've already determined not to search filtered rows
851
+ if (!searchFiltered) { indx = columnIndex; }
852
+ // search already filtered rows if...
853
+ searchFiltered = searchFiltered && lastSearch.length &&
854
+ // there are no changes from beginning of filter
855
+ val.indexOf(lastSearch[indx] || '') === 0 &&
856
+ // if there is NOT a logical "or", or range ("to" or "-") in the string
857
+ !regex.alreadyFiltered.test(val) &&
858
+ // if we are not doing exact matches, using "|" (logical or) or not "!"
859
+ !/[=\"\|!]/.test(val) &&
860
+ // don't search only filtered if the value is negative ('> -10' => '> -100' will ignore hidden rows)
861
+ !(/(>=?\s*-\d)/.test(val) || /(<=?\s*\d)/.test(val)) &&
862
+ // if filtering using a select without a "filter-match" class (exact match) - fixes #593
863
+ !( val !== '' && c.$filters && c.$filters.eq(indx).find('select').length && !c.$headers.filter('[data-column="' + indx + '"]:last').hasClass('filter-match') );
864
+ }
865
+ }
866
+ notFiltered = $rows.not('.' + wo.filter_filteredRow).length;
867
+ // can't search when all rows are hidden - this happens when looking for exact matches
868
+ if (searchFiltered && notFiltered === 0) { searchFiltered = false; }
869
+ if (c.debug) {
870
+ ts.log( "Searching through " + ( searchFiltered && notFiltered < len ? notFiltered : "all" ) + " rows" );
871
+ }
872
+ if (data.anyMatchFlag) {
873
+ if (c.sortLocaleCompare) {
874
+ // replace accents
875
+ data.anyMatchFilter = ts.replaceAccents(data.anyMatchFilter);
876
+ }
877
+ if (wo.filter_defaultFilter && regex.iQuery.test( ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || '')) {
878
+ data.anyMatchFilter = ts.filter.defaultFilter( data.anyMatchFilter, ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) );
879
+ // clear search filtered flag because default filters are not saved to the last search
880
+ searchFiltered = false;
881
+ }
882
+ // make iAnyMatchFilter lowercase unless both filter widget & core ignoreCase options are true
883
+ // when c.ignoreCase is true, the cache contains all lower case data
884
+ data.iAnyMatchFilter = !(wo.filter_ignoreCase && c.ignoreCase) ? data.anyMatchFilter : data.anyMatchFilter.toLocaleLowerCase();
885
+ }
886
+
887
+ // loop through the rows
888
+ for (rowIndex = 0; rowIndex < len; rowIndex++) {
889
+
890
+ data.cacheArray = norm_rows[rowIndex];
891
+
892
+ childRow = $rows[rowIndex].className;
893
+ // skip child rows & already filtered rows
894
+ if ( regex.child.test(childRow) || (searchFiltered && regex.filtered.test(childRow)) ) { continue; }
895
+ showRow = true;
896
+ // *** nextAll/nextUntil not supported by Zepto! ***
897
+ childRow = $rows.eq(rowIndex).nextUntil('tr:not(.' + c.cssChildRow + ')');
898
+ // so, if "table.config.widgetOptions.filter_childRows" is true and there is
899
+ // a match anywhere in the child row, then it will make the row visible
900
+ // checked here so the option can be changed dynamically
901
+ data.childRowText = (childRow.length && wo.filter_childRows) ? childRow.text() : '';
902
+ data.childRowText = wo.filter_ignoreCase ? data.childRowText.toLocaleLowerCase() : data.childRowText;
903
+ $cells = $rows.eq(rowIndex).children();
904
+ if (data.anyMatchFlag) {
905
+ // look for multiple columns "1-3,4-6,8"
906
+ columnIndex = ts.filter.multipleColumns( c, wo.filter_$anyMatch );
907
+ data.anyMatch = true;
908
+ data.rowArray = $cells.map(function(i){
909
+ if ( $.inArray(i, columnIndex) > -1 ) {
910
+ var txt;
911
+ if (data.parsed[i]) {
912
+ txt = data.cacheArray[i];
913
+ } else {
914
+ txt = this ? this.getAttribute( c.textAttribute ) || this.textContent || $(this).text() : '';
915
+ txt = $.trim( wo.filter_ignoreCase ? txt.toLowerCase() : txt );
916
+ if (c.sortLocaleCompare) {
917
+ txt = ts.replaceAccents(txt);
918
+ }
919
+ }
920
+ return txt;
921
+ }
922
+ }).get();
923
+ data.filter = data.anyMatchFilter;
924
+ data.iFilter = data.iAnyMatchFilter;
925
+ data.exact = data.rowArray.join(' ');
926
+ data.iExact = wo.filter_ignoreCase ? data.exact.toLowerCase() : data.exact;
927
+ data.cache = data.cacheArray.slice(0,-1).join(' ');
928
+ filterMatched = null;
929
+ $.each(ts.filter.types, function(type, typeFunction) {
930
+ if ($.inArray(type, noAnyMatch) < 0) {
931
+ matches = typeFunction( c, data );
932
+ if (matches !== null) {
933
+ filterMatched = matches;
934
+ return false;
935
+ }
936
+ }
937
+ });
938
+ if (filterMatched !== null) {
939
+ showRow = filterMatched;
940
+ } else {
941
+ if (wo.filter_startsWith) {
942
+ showRow = false;
943
+ columnIndex = c.columns;
944
+ while (!showRow && columnIndex > 0) {
945
+ columnIndex--;
946
+ showRow = showRow || data.rowArray[columnIndex].indexOf(data.iFilter) === 0;
947
+ }
948
+ } else {
949
+ showRow = (data.iExact + data.childRowText).indexOf(data.iFilter) >= 0;
950
+ }
951
+ }
952
+ data.anyMatch = false;
953
+ }
954
+
955
+ for (columnIndex = 0; columnIndex < c.columns; columnIndex++) {
956
+ data.filter = filters[columnIndex];
957
+ data.index = columnIndex;
958
+
959
+ // filter types to exclude, per column
960
+ excludeMatch = ( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split(/\s+/);
961
+
962
+ // ignore if filter is empty or disabled
963
+ if (data.filter) {
964
+ data.cache = data.cacheArray[columnIndex];
965
+ // check if column data should be from the cell or from parsed data
966
+ if (wo.filter_useParsedData || data.parsed[columnIndex]) {
967
+ data.exact = data.cache;
968
+ } else {
969
+ val = $cells[columnIndex];
970
+ result = val ? $.trim( val.getAttribute( c.textAttribute ) || val.textContent || $cells.eq(columnIndex).text() ) : '';
971
+ data.exact = c.sortLocaleCompare ? ts.replaceAccents(result) : result; // issue #405
972
+ }
973
+ data.iExact = !regex.type.test(typeof data.exact) && wo.filter_ignoreCase ? data.exact.toLocaleLowerCase() : data.exact;
974
+ result = showRow; // if showRow is true, show that row
975
+
976
+ // in case select filter option has a different value vs text "a - z|A through Z"
977
+ ffxn = wo.filter_columnFilters ?
978
+ c.$filters.add(c.$externalFilters).filter('[data-column="'+ columnIndex + '"]').find('select option:selected').attr('data-function-name') || '' : '';
979
+ // replace accents - see #357
980
+ if (c.sortLocaleCompare) {
981
+ data.filter = ts.replaceAccents(data.filter);
982
+ }
983
+
984
+ val = true;
985
+ if (wo.filter_defaultFilter && regex.iQuery.test( ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || '')) {
986
+ data.filter = ts.filter.defaultFilter( data.filter, ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) );
987
+ // val is used to indicate that a filter select is using a default filter; so we override the exact & partial matches
988
+ val = false;
989
+ }
990
+ // data.iFilter = case insensitive (if wo.filter_ignoreCase is true), data.filter = case sensitive
991
+ data.iFilter = wo.filter_ignoreCase ? (data.filter || '').toLocaleLowerCase() : data.filter;
992
+ fxn = ts.getColumnData( table, wo.filter_functions, columnIndex );
993
+ $cell = c.$headers.filter('[data-column="' + columnIndex + '"]:last');
994
+ hasSelect = $cell.hasClass('filter-select');
995
+ if ( fxn || ( hasSelect && val ) ) {
996
+ if (fxn === true || hasSelect) {
997
+ // default selector uses exact match unless "filter-match" class is found
998
+ result = ($cell.hasClass('filter-match')) ? data.iExact.search(data.iFilter) >= 0 : data.filter === data.exact;
999
+ } else if (typeof fxn === 'function') {
1000
+ // filter callback( exact cell content, parser normalized content, filter input value, column index, jQuery row object )
1001
+ result = fxn(data.exact, data.cache, data.filter, columnIndex, $rows.eq(rowIndex));
1002
+ } else if (typeof fxn[ffxn || data.filter] === 'function') {
1003
+ // selector option function
1004
+ result = fxn[ffxn || data.filter](data.exact, data.cache, data.filter, columnIndex, $rows.eq(rowIndex));
1005
+ }
1006
+ } else {
1007
+ filterMatched = null;
1008
+ // cycle through the different filters
1009
+ // filters return a boolean or null if nothing matches
1010
+ $.each(ts.filter.types, function(type, typeFunction) {
1011
+ if ($.inArray(type, excludeMatch) < 0) {
1012
+ matches = typeFunction( c, data );
1013
+ if (matches !== null) {
1014
+ filterMatched = matches;
1015
+ return false;
1016
+ }
1017
+ }
1018
+ });
1019
+ if (filterMatched !== null) {
1020
+ result = filterMatched;
1021
+ // Look for match, and add child row data for matching
1022
+ } else {
1023
+ data.exact = (data.iExact + data.childRowText).indexOf( ts.filter.parseFilter(c, data.iFilter, columnIndex, data.parsed[columnIndex]) );
1024
+ result = ( (!wo.filter_startsWith && data.exact >= 0) || (wo.filter_startsWith && data.exact === 0) );
1025
+ }
1026
+ }
1027
+ showRow = (result) ? showRow : false;
1028
+ }
1029
+ }
1030
+ $rows.eq(rowIndex)
1031
+ .toggle(showRow)
1032
+ .toggleClass(wo.filter_filteredRow, !showRow);
1033
+ if (childRow.length) {
1034
+ childRow.toggleClass(wo.filter_filteredRow, !showRow);
1035
+ }
1036
+ }
1037
+ }
1038
+ c.filteredRows += $rows.not('.' + wo.filter_filteredRow).length;
1039
+ c.totalRows += $rows.length;
1040
+ ts.processTbody(table, $tbody, false);
1041
+ }
1042
+ c.lastCombinedFilter = combinedFilters; // save last search
1043
+ c.lastSearch = filters;
1044
+ c.$table.data('lastSearch', filters);
1045
+ if (wo.filter_saveFilters && ts.storage) {
1046
+ ts.storage( table, 'tablesorter-filters', filters );
1047
+ }
1048
+ if (c.debug) {
1049
+ ts.benchmark("Completed filter widget search", time);
1050
+ }
1051
+ if (wo.filter_initialized) { c.$table.trigger('filterEnd', c ); }
1052
+ setTimeout(function(){
1053
+ c.$table.trigger('applyWidgets'); // make sure zebra widget is applied
1054
+ }, 0);
1055
+ },
1056
+ getOptionSource: function(table, column, onlyAvail) {
1057
+ table = $(table)[0];
1058
+ var cts,
1059
+ c = table.config,
1060
+ wo = c.widgetOptions,
1061
+ parsed = [],
1062
+ arry = false,
1063
+ source = wo.filter_selectSource,
1064
+ last = c.$table.data('lastSearch') || [],
1065
+ fxn = $.isFunction(source) ? true : ts.getColumnData( table, source, column );
1066
+
1067
+ if (onlyAvail && last[column] !== '') {
1068
+ onlyAvail = false;
1069
+ }
1070
+
1071
+ // filter select source option
1072
+ if (fxn === true) {
1073
+ // OVERALL source
1074
+ arry = source(table, column, onlyAvail);
1075
+ } else if ( fxn instanceof $ || ($.type(fxn) === 'string' && fxn.indexOf('</option>') >= 0) ) {
1076
+ // selectSource is a jQuery object or string of options
1077
+ return fxn;
1078
+ } else if ($.isArray(fxn)) {
1079
+ arry = fxn;
1080
+ } else if ($.type(source) === 'object' && fxn) {
1081
+ // custom select source function for a SPECIFIC COLUMN
1082
+ arry = fxn(table, column, onlyAvail);
1083
+ }
1084
+ if (arry === false) {
1085
+ // fall back to original method
1086
+ arry = ts.filter.getOptions(table, column, onlyAvail);
1087
+ }
1088
+
1089
+ // get unique elements and sort the list
1090
+ // if $.tablesorter.sortText exists (not in the original tablesorter),
1091
+ // then natural sort the list otherwise use a basic sort
1092
+ arry = $.grep(arry, function(value, indx) {
1093
+ return $.inArray(value, arry) === indx;
1094
+ });
1095
+
1096
+ if (c.$headers.filter('[data-column="' + column + '"]:last').hasClass('filter-select-nosort')) {
1097
+ // unsorted select options
1098
+ return arry;
1099
+ } else {
1100
+ // parse select option values
1101
+ $.each(arry, function(i, v){
1102
+ // parse array data using set column parser; this DOES NOT pass the original
1103
+ // table cell to the parser format function
1104
+ parsed.push({ t : v, p : c.parsers && c.parsers[column].format( v, table, [], column ) });
1105
+ });
1106
+
1107
+ // sort parsed select options
1108
+ cts = c.textSorter || '';
1109
+ parsed.sort(function(a, b){
1110
+ // sortNatural breaks if you don't pass it strings
1111
+ var x = a.p.toString(), y = b.p.toString();
1112
+ if ($.isFunction(cts)) {
1113
+ // custom OVERALL text sorter
1114
+ return cts(x, y, true, column, table);
1115
+ } else if (typeof(cts) === 'object' && cts.hasOwnProperty(column)) {
1116
+ // custom text sorter for a SPECIFIC COLUMN
1117
+ return cts[column](x, y, true, column, table);
1118
+ } else if (ts.sortNatural) {
1119
+ // fall back to natural sort
1120
+ return ts.sortNatural(x, y);
1121
+ }
1122
+ // using an older version! do a basic sort
1123
+ return true;
1124
+ });
1125
+ // rebuild arry from sorted parsed data
1126
+ arry = [];
1127
+ $.each(parsed, function(i, v){
1128
+ arry.push(v.t);
1129
+ });
1130
+ return arry;
1131
+ }
1132
+ },
1133
+ getOptions: function(table, column, onlyAvail) {
1134
+ table = $(table)[0];
1135
+ var rowIndex, tbodyIndex, len, row, cache, cell,
1136
+ c = table.config,
1137
+ wo = c.widgetOptions,
1138
+ arry = [];
1139
+ for (tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) {
1140
+ cache = c.cache[tbodyIndex];
1141
+ len = c.cache[tbodyIndex].normalized.length;
1142
+ // loop through the rows
1143
+ for (rowIndex = 0; rowIndex < len; rowIndex++) {
1144
+ // get cached row from cache.row (old) or row data object (new; last item in normalized array)
1145
+ row = cache.row ? cache.row[rowIndex] : cache.normalized[rowIndex][c.columns].$row[0];
1146
+ // check if has class filtered
1147
+ if (onlyAvail && row.className.match(wo.filter_filteredRow)) { continue; }
1148
+ // get non-normalized cell content
1149
+ if (wo.filter_useParsedData || c.parsers[column].parsed || c.$headers.filter('[data-column="' + column + '"]:last').hasClass('filter-parsed')) {
1150
+ arry.push( '' + cache.normalized[rowIndex][column] );
1151
+ } else {
1152
+ cell = row.cells[column];
1153
+ if (cell) {
1154
+ arry.push( $.trim( cell.getAttribute( c.textAttribute ) || cell.textContent || $(cell).text() ) );
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+ return arry;
1160
+ },
1161
+ buildSelect: function(table, column, arry, updating, onlyAvail) {
1162
+ table = $(table)[0];
1163
+ column = parseInt(column, 10);
1164
+ if (!table.config.cache || $.isEmptyObject(table.config.cache)) { return; }
1165
+ var indx, val, txt, t, $filters, $filter,
1166
+ c = table.config,
1167
+ wo = c.widgetOptions,
1168
+ node = c.$headers.filter('[data-column="' + column + '"]:last'),
1169
+ // t.data('placeholder') won't work in jQuery older than 1.4.3
1170
+ options = '<option value="">' + ( node.data('placeholder') || node.attr('data-placeholder') || wo.filter_placeholder.select || '' ) + '</option>',
1171
+ // Get curent filter value
1172
+ currentValue = c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]').val();
1173
+ // nothing included in arry (external source), so get the options from filter_selectSource or column data
1174
+ if (typeof arry === 'undefined' || arry === '') {
1175
+ arry = ts.filter.getOptionSource(table, column, onlyAvail);
1176
+ }
1177
+
1178
+ if ($.isArray(arry)) {
1179
+ // build option list
1180
+ for (indx = 0; indx < arry.length; indx++) {
1181
+ txt = arry[indx] = ('' + arry[indx]).replace(/\"/g, "&quot;");
1182
+ val = txt;
1183
+ // allow including a symbol in the selectSource array
1184
+ // "a-z|A through Z" so that "a-z" becomes the option value
1185
+ // and "A through Z" becomes the option text
1186
+ if (txt.indexOf(wo.filter_selectSourceSeparator) >= 0) {
1187
+ t = txt.split(wo.filter_selectSourceSeparator);
1188
+ val = t[0];
1189
+ txt = t[1];
1190
+ }
1191
+ // replace quotes - fixes #242 & ignore empty strings - see http://stackoverflow.com/q/14990971/145346
1192
+ options += arry[indx] !== '' ? '<option ' + (val === txt ? '' : 'data-function-name="' + arry[indx] + '" ') + 'value="' + val + '">' + txt + '</option>' : '';
1193
+ }
1194
+ // clear arry so it doesn't get appended twice
1195
+ arry = [];
1196
+ }
1197
+
1198
+ // update all selects in the same column (clone thead in sticky headers & any external selects) - fixes 473
1199
+ $filters = ( c.$filters ? c.$filters : c.$table.children('thead') ).find('.' + ts.css.filter);
1200
+ if (wo.filter_$externalFilters) {
1201
+ $filters = $filters && $filters.length ? $filters.add(wo.filter_$externalFilters) : wo.filter_$externalFilters;
1202
+ }
1203
+ $filter = $filters.filter('select[data-column="' + column + '"]');
1204
+
1205
+ // make sure there is a select there!
1206
+ if ($filter.length) {
1207
+ $filter[ updating ? 'html' : 'append' ](options);
1208
+ if (!$.isArray(arry)) {
1209
+ // append options if arry is provided externally as a string or jQuery object
1210
+ // options (default value) was already added
1211
+ $filter.append(arry).val(currentValue);
1212
+ }
1213
+ $filter.val(currentValue);
1214
+ }
1215
+ },
1216
+ buildDefault: function(table, updating) {
1217
+ var columnIndex, $header, noSelect,
1218
+ c = table.config,
1219
+ wo = c.widgetOptions,
1220
+ columns = c.columns;
1221
+ // build default select dropdown
1222
+ for (columnIndex = 0; columnIndex < columns; columnIndex++) {
1223
+ $header = c.$headers.filter('[data-column="' + columnIndex + '"]:last');
1224
+ noSelect = !($header.hasClass('filter-false') || $header.hasClass('parser-false'));
1225
+ // look for the filter-select class; build/update it if found
1226
+ if (($header.hasClass('filter-select') || ts.getColumnData( table, wo.filter_functions, columnIndex ) === true) && noSelect) {
1227
+ ts.filter.buildSelect(table, columnIndex, '', updating, $header.hasClass(wo.filter_onlyAvail));
1228
+ }
1229
+ }
1230
+ }
1231
+ };
1232
+
1233
+ ts.getFilters = function(table, getRaw, setFilters, skipFirst) {
1234
+ var i, $filters, $column, cols,
1235
+ filters = false,
1236
+ c = table ? $(table)[0].config : '',
1237
+ wo = c ? c.widgetOptions : '';
1238
+ if (getRaw !== true && wo && !wo.filter_columnFilters) {
1239
+ return $(table).data('lastSearch');
1240
+ }
1241
+ if (c) {
1242
+ if (c.$filters) {
1243
+ $filters = c.$filters.find('.' + ts.css.filter);
1244
+ }
1245
+ if (wo.filter_$externalFilters) {
1246
+ $filters = $filters && $filters.length ? $filters.add(wo.filter_$externalFilters) : wo.filter_$externalFilters;
1247
+ }
1248
+ if ($filters && $filters.length) {
1249
+ filters = setFilters || [];
1250
+ for (i = 0; i < c.columns + 1; i++) {
1251
+ cols = ( i === c.columns ?
1252
+ // "all" columns can now include a range or set of columms (data-column="0-2,4,6-7")
1253
+ wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector :
1254
+ '[data-column="' + i + '"]' );
1255
+ $column = $filters.filter(cols);
1256
+ if ($column.length) {
1257
+ // move the latest search to the first slot in the array
1258
+ $column = ts.filter.getLatestSearch( $column );
1259
+ if ($.isArray(setFilters)) {
1260
+ // skip first (latest input) to maintain cursor position while typing
1261
+ if (skipFirst) { $column.slice(1); }
1262
+ if (i === c.columns) {
1263
+ // prevent data-column="all" from filling data-column="0,1" (etc)
1264
+ cols = $column.filter(wo.filter_anyColumnSelector);
1265
+ $column = cols.length ? cols : $column;
1266
+ }
1267
+ $column
1268
+ .val( setFilters[i] )
1269
+ .trigger('change.tsfilter');
1270
+ } else {
1271
+ filters[i] = $column.val() || '';
1272
+ // don't change the first... it will move the cursor
1273
+ if (i === c.columns) {
1274
+ // don't update range columns from "all" setting
1275
+ $column.slice(1).filter('[data-column*="' + $column.attr('data-column') + '"]').val( filters[i] );
1276
+ } else {
1277
+ $column.slice(1).val( filters[i] );
1278
+ }
1279
+ }
1280
+ // save any match input dynamically
1281
+ if (i === c.columns && $column.length) {
1282
+ wo.filter_$anyMatch = $column;
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+ }
1288
+ if (filters.length === 0) {
1289
+ filters = false;
1290
+ }
1291
+ return filters;
1292
+ };
1293
+
1294
+ ts.setFilters = function(table, filter, apply, skipFirst) {
1295
+ var c = table ? $(table)[0].config : '',
1296
+ valid = ts.getFilters(table, true, filter, skipFirst);
1297
+ if (c && apply) {
1298
+ // ensure new set filters are applied, even if the search is the same
1299
+ c.lastCombinedFilter = null;
1300
+ c.lastSearch = [];
1301
+ ts.filter.searching(c.table, filter, skipFirst);
1302
+ c.$table.trigger('filterFomatterUpdate');
1303
+ }
1304
+ return !!valid;
1305
+ };
1306
+
1307
+ })(jQuery);