rubycritic 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -1
  3. data/CONTRIBUTING.md +2 -1
  4. data/README.md +1 -0
  5. data/lib/rubycritic/cli/options.rb +17 -1
  6. data/lib/rubycritic/commands/ci.rb +2 -3
  7. data/lib/rubycritic/commands/default.rb +2 -3
  8. data/lib/rubycritic/configuration.rb +4 -1
  9. data/lib/rubycritic/core/analysed_module.rb +19 -0
  10. data/lib/rubycritic/core/location.rb +11 -0
  11. data/lib/rubycritic/core/rating.rb +8 -0
  12. data/lib/rubycritic/core/smell.rb +16 -0
  13. data/lib/rubycritic/{report_generators → generators/html}/assets/javascripts/application.js +0 -0
  14. data/lib/rubycritic/{report_generators → generators/html}/assets/javascripts/highcharts.src-4.0.1.js +0 -0
  15. data/lib/rubycritic/{report_generators → generators/html}/assets/javascripts/jquery-2.1.0.js +0 -0
  16. data/lib/rubycritic/{report_generators → generators/html}/assets/javascripts/jquery.floatThead-v1.2.7.js +0 -0
  17. data/lib/rubycritic/{report_generators → generators/html}/assets/javascripts/jquery.scrollTo-1.4.11.js +0 -0
  18. data/lib/rubycritic/generators/html/assets/javascripts/jquery.tablesorter.js +2089 -0
  19. data/lib/rubycritic/{report_generators → generators/html}/assets/javascripts/jquery.timeago-v1.4.1.js +0 -0
  20. data/lib/rubycritic/{report_generators → generators/html}/assets/javascripts/prettify-4-Mar-2013.js +0 -0
  21. data/lib/rubycritic/{report_generators → generators/html}/assets/stylesheets/application.css +87 -25
  22. data/lib/rubycritic/{report_generators → generators/html}/assets/stylesheets/prettify.custom_theme.css +0 -0
  23. data/lib/rubycritic/generators/html/base.rb +52 -0
  24. data/lib/rubycritic/generators/html/code_file.rb +40 -0
  25. data/lib/rubycritic/generators/html/code_index.rb +26 -0
  26. data/lib/rubycritic/generators/html/line.rb +37 -0
  27. data/lib/rubycritic/generators/html/overview.rb +27 -0
  28. data/lib/rubycritic/generators/html/smells_index.rb +38 -0
  29. data/lib/rubycritic/{report_generators → generators/html}/templates/code_file.html.erb +4 -2
  30. data/lib/rubycritic/{report_generators → generators/html}/templates/code_index.html.erb +8 -4
  31. data/lib/rubycritic/{report_generators → generators/html}/templates/layouts/application.html.erb +12 -8
  32. data/lib/rubycritic/{report_generators → generators/html}/templates/line.html.erb +0 -0
  33. data/lib/rubycritic/{report_generators → generators/html}/templates/overview.html.erb +0 -0
  34. data/lib/rubycritic/{report_generators → generators/html}/templates/smells_index.html.erb +4 -4
  35. data/lib/rubycritic/{report_generators → generators/html}/templates/smelly_line.html.erb +0 -0
  36. data/lib/rubycritic/{report_generators → generators/html}/turbulence.rb +0 -0
  37. data/lib/rubycritic/{report_generators → generators/html}/view_helpers.rb +0 -0
  38. data/lib/rubycritic/generators/html_report.rb +66 -0
  39. data/lib/rubycritic/generators/json/simple.rb +30 -0
  40. data/lib/rubycritic/generators/json_report.rb +23 -0
  41. data/lib/rubycritic/reporter.rb +20 -0
  42. data/lib/rubycritic/version.rb +1 -1
  43. data/test/lib/rubycritic/analysers/churn_test.rb +12 -13
  44. data/test/lib/rubycritic/analysers/complexity_test.rb +9 -5
  45. data/test/lib/rubycritic/analysers/smells/flay_test.rb +27 -20
  46. data/test/lib/rubycritic/analysers/smells/flog_test.rb +17 -15
  47. data/test/lib/rubycritic/analysers/smells/reek_test.rb +2 -2
  48. data/test/lib/rubycritic/report_generators/turbulence_test.rb +1 -1
  49. data/test/lib/rubycritic/report_generators/view_helpers_test.rb +1 -1
  50. data/test/lib/rubycritic/source_control_systems/base_test.rb +2 -2
  51. data/test/lib/rubycritic/source_locator_test.rb +1 -1
  52. data/test/samples/flay/smelly.rb +0 -9
  53. data/test/samples/flay/smelly2.rb +8 -0
  54. metadata +33 -31
  55. data/lib/rubycritic/report_generators/assets/javascripts/jquery.tablesorter-2.0.js +0 -1031
  56. data/lib/rubycritic/report_generators/base.rb +0 -50
  57. data/lib/rubycritic/report_generators/code_file.rb +0 -38
  58. data/lib/rubycritic/report_generators/code_index.rb +0 -24
  59. data/lib/rubycritic/report_generators/current_code_file.rb +0 -17
  60. data/lib/rubycritic/report_generators/line.rb +0 -31
  61. data/lib/rubycritic/report_generators/overview.rb +0 -25
  62. data/lib/rubycritic/report_generators/smells_index.rb +0 -36
  63. data/lib/rubycritic/reporters/base.rb +0 -24
  64. data/lib/rubycritic/reporters/main.rb +0 -51
  65. data/lib/rubycritic/reporters/mini.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 88e1add4a91ef2551d93ccec55b2991033b7c2f0
4
- data.tar.gz: 4d2b5928a03eff96fc1e5b97529e73e9538affa7
3
+ metadata.gz: 5e28af528222741e3d805ba162643f4e7b739c56
4
+ data.tar.gz: 34d5a5bb5c16637c96939d59be91ca3378d9b2fc
5
5
  SHA512:
6
- metadata.gz: 5bd3e6a0658fbdc874d740a3efc1b1b2649d69224218017bef6071447653ab18796b0a86ceddf5fb3199b4904f00655bac7ec70ec4ab6c43b52681896a48c072
7
- data.tar.gz: 449e90f3900e888dd4995135ca4d96a4da595b0d40a7e860d432d20eb358b1354c2ada9fce1a22a4b0a291247489a07d5eda42c75bf1acab2ea2e2238d434d06
6
+ metadata.gz: f0849ed6312b44cae65c95a67e9b48375aa1de23379f3370849182209b8740cb630cf02888cc97ea74fcbe05877a6c3869ed8afdc84f2ff3de586d41a20d4adc
7
+ data.tar.gz: be6ce3d5f7a7132ca37bfc61d7979be2cefd5e88b11c7e566d71ac9219c1b51002a7d0ee46b89c6a54aeb9dd150aa585a8ccd9d7d3d26d3bc2fbb7e9c2524b92
data/CHANGELOG.md CHANGED
@@ -1,4 +1,10 @@
1
- # 1.3.0 2015-02-16
1
+ # 1.4.0 / 2015-03-14
2
+
3
+ * [FEATURE] New report in JSON format. Available by using the new CLI option `-f`
4
+ * [FEATURE] New CLI option `--suppress-ratings` to suppress ratings (by halostatue)
5
+ * [CHANGE] Improve UI, particularly the sortable tables (by crackofdusk)
6
+
7
+ # 1.3.0 / 2015-02-16
2
8
 
3
9
  * [FEATURE] New CLI option `--deduplicate-symlinks` to deduplicate symlinks (by LeeXGreen)
4
10
  * [CHANGE] Update to Reek 1.6.5 (from 1.6.3)
@@ -16,6 +22,7 @@
16
22
  * [FEATURE] Add partial support for Mercurial
17
23
  * [FEATURE] Allow using RubyCritic programatically
18
24
  * [CHANGE] Update to Reek 1.6.0 (from 1.3.8)
25
+ * [BUGFIX] Fix issue #18 - Prevent encoding issues when using Git
19
26
 
20
27
  # 1.1.1 / 2014-07-29
21
28
 
data/CONTRIBUTING.md CHANGED
@@ -59,7 +59,7 @@ If you are experiencing unexpected behavior and, after having read the documenta
59
59
  ```
60
60
 
61
61
  The more information you provide, the easier it will be to track down the issue and fix it.
62
-
62
+ If you have never written a bug report before, or if you want to brush up on your bug reporting skills, read Simon Tatham's essay [How to Report Bugs Effectively].
63
63
 
64
64
  [descriptive commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
65
65
  [Create a pull request]: https://help.github.com/articles/creating-a-pull-request
@@ -68,3 +68,4 @@ If you are experiencing unexpected behavior and, after having read the documenta
68
68
  [Travis]: https://travis-ci.org
69
69
  [issues tracker]: https://github.com/whitesmith/rubycritic/issues
70
70
  [Create a new issue]: https://github.com/whitesmith/rubycritic/issues/new
71
+ [How to Report Bugs Effectively]: http://www.chiark.greenend.org.uk/~sgtatham/bugs.html
data/README.md CHANGED
@@ -92,6 +92,7 @@ $ rubycritic --help
92
92
  | `-p/--path` | Sets the output directory (tmp/rubycritic by default) |
93
93
  | `-m/--mode-ci` | Uses CI mode (faster, but only analyses last commit) |
94
94
  | `--deduplicate-symlinks` | De-duplicate symlinks based on their final target |
95
+ | `--suppress-ratings` | Suppress letter ratings |
95
96
 
96
97
  Alternative Usage Methods
97
98
  -------------------------
@@ -17,6 +17,16 @@ module Rubycritic
17
17
  @root = path
18
18
  end
19
19
 
20
+ opts.on(
21
+ "-f", "--format [FORMAT]",
22
+ [:html, :json],
23
+ "Report smells in the given format:",
24
+ " html (default)",
25
+ " json"
26
+ ) do |format|
27
+ @format = format
28
+ end
29
+
20
30
  opts.on("-m", "--mode-ci", "Use CI mode (faster, but only analyses last commit)") do
21
31
  @mode = :ci
22
32
  end
@@ -25,6 +35,10 @@ module Rubycritic
25
35
  @deduplicate_symlinks = true
26
36
  end
27
37
 
38
+ opts.on("--suppress-ratings", "Suppress letter ratings") do
39
+ @suppress_ratings = true
40
+ end
41
+
28
42
  opts.on_tail("-v", "--version", "Show gem's version") do
29
43
  @mode = :version
30
44
  end
@@ -44,8 +58,10 @@ module Rubycritic
44
58
  {
45
59
  :mode => @mode,
46
60
  :root => @root,
61
+ :format => @format,
47
62
  :deduplicate_symlinks => @deduplicate_symlinks,
48
- :paths => paths
63
+ :paths => paths,
64
+ :suppress_ratings => @suppress_ratings
49
65
  }
50
66
  end
51
67
 
@@ -1,6 +1,6 @@
1
1
  require "rubycritic/source_control_systems/base"
2
2
  require "rubycritic/analysers_runner"
3
- require "rubycritic/reporters/main"
3
+ require "rubycritic/reporter"
4
4
 
5
5
  module Rubycritic
6
6
  module Command
@@ -19,8 +19,7 @@ module Rubycritic
19
19
  end
20
20
 
21
21
  def report(analysed_modules)
22
- report_location = Reporter::Main.new(analysed_modules).generate_report
23
- puts "New critique at #{report_location}"
22
+ Reporter.generate_report(analysed_modules)
24
23
  end
25
24
  end
26
25
  end
@@ -1,7 +1,7 @@
1
1
  require "rubycritic/source_control_systems/base"
2
2
  require "rubycritic/analysers_runner"
3
3
  require "rubycritic/revision_comparator"
4
- require "rubycritic/reporters/main"
4
+ require "rubycritic/reporter"
5
5
 
6
6
  module Rubycritic
7
7
  module Command
@@ -21,8 +21,7 @@ module Rubycritic
21
21
  end
22
22
 
23
23
  def report(analysed_modules)
24
- report_location = Reporter::Main.new(analysed_modules).generate_report
25
- puts "New critique at #{report_location}"
24
+ Reporter.generate_report(analysed_modules)
26
25
  end
27
26
  end
28
27
  end
@@ -1,12 +1,15 @@
1
1
  module Rubycritic
2
2
  class Configuration
3
3
  attr_reader :root
4
- attr_accessor :source_control_system, :mode, :deduplicate_symlinks
4
+ attr_accessor :source_control_system, :mode, :format, :deduplicate_symlinks,
5
+ :suppress_ratings
5
6
 
6
7
  def set(options)
7
8
  self.mode = options[:mode] || :default
8
9
  self.root = options[:root] || "tmp/rubycritic"
10
+ self.format = options[:format] || :html
9
11
  self.deduplicate_symlinks = options[:deduplicate_symlinks] || false
12
+ self.suppress_ratings = options[:suppress_ratings] || false
10
13
  end
11
14
 
12
15
  def root=(path)
@@ -42,6 +42,25 @@ module Rubycritic
42
42
  def smells_at_location(location)
43
43
  smells.select { |smell| smell.at_location?(location) }
44
44
  end
45
+
46
+ def to_h
47
+ {
48
+ :name => name,
49
+ :path => path,
50
+ :smells => smells,
51
+ :churn => churn,
52
+ :committed_at => committed_at,
53
+ :complexity => complexity,
54
+ :duplication => duplication,
55
+ :methods_count => methods_count,
56
+ :cost => cost,
57
+ :rating => rating
58
+ }
59
+ end
60
+
61
+ def to_json(*a)
62
+ to_h.to_json(*a)
63
+ end
45
64
  end
46
65
 
47
66
  end
@@ -18,6 +18,17 @@ module Rubycritic
18
18
  "#{pathname}:#{line}"
19
19
  end
20
20
 
21
+ def to_h
22
+ {
23
+ :path => pathname.to_s,
24
+ :line => line
25
+ }
26
+ end
27
+
28
+ def to_json(*a)
29
+ to_h.to_json(*a)
30
+ end
31
+
21
32
  def ==(other)
22
33
  state == other.state
23
34
  end
@@ -17,6 +17,14 @@ module Rubycritic
17
17
  def to_s
18
18
  @letter
19
19
  end
20
+
21
+ def to_h
22
+ @letter
23
+ end
24
+
25
+ def to_json(*a)
26
+ to_h.to_json(*a)
27
+ end
20
28
  end
21
29
 
22
30
  end
@@ -31,6 +31,22 @@ module Rubycritic
31
31
  "(#{type}) #{context} #{message}"
32
32
  end
33
33
 
34
+ def to_h
35
+ {
36
+ :context => context,
37
+ :cost => cost,
38
+ :locations => locations,
39
+ :message => message,
40
+ :score => score,
41
+ :status => status,
42
+ :type => type
43
+ }
44
+ end
45
+
46
+ def to_json(*a)
47
+ to_h.to_json(*a)
48
+ end
49
+
34
50
  def hash
35
51
  state.hash
36
52
  end
@@ -0,0 +1,2089 @@
1
+ /*! TableSorter (FORK) v2.21.0 *//*
2
+ * Client-side table sorting with ease!
3
+ * @requires jQuery v1.2.6+
4
+ *
5
+ * Copyright (c) 2007 Christian Bach
6
+ * fork maintained by Rob Garrison
7
+ *
8
+ * Examples and docs at: http://tablesorter.com
9
+ * Dual licensed under the MIT and GPL licenses:
10
+ * http://www.opensource.org/licenses/mit-license.php
11
+ * http://www.gnu.org/licenses/gpl.html
12
+ *
13
+ * @type jQuery
14
+ * @name tablesorter (FORK)
15
+ * @cat Plugins/Tablesorter
16
+ * @author Christian Bach - christian.bach@polyester.se
17
+ * @contributor Rob Garrison - https://github.com/Mottie/tablesorter
18
+ */
19
+ /*jshint browser:true, jquery:true, unused:false, expr: true */
20
+ /*global console:false, alert:false, require:false, define:false, module:false */
21
+ (function(factory) {
22
+ if (typeof define === 'function' && define.amd) {
23
+ define(['jquery'], factory);
24
+ } else if (typeof module === 'object' && typeof module.exports === 'object') {
25
+ module.exports = factory(require('jquery'));
26
+ } else {
27
+ factory(jQuery);
28
+ }
29
+ }(function($) {
30
+ 'use strict';
31
+ $.extend({
32
+ /*jshint supernew:true */
33
+ tablesorter: new function() {
34
+
35
+ var ts = this;
36
+
37
+ ts.version = '2.21.0';
38
+
39
+ ts.parsers = [];
40
+ ts.widgets = [];
41
+ ts.defaults = {
42
+
43
+ // *** appearance
44
+ theme : 'default', // adds tablesorter-{theme} to the table for styling
45
+ widthFixed : false, // adds colgroup to fix widths of columns
46
+ showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered.
47
+
48
+ headerTemplate : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = <i/> (class from cssIcon)
49
+ onRenderTemplate : null, // function(index, template){ return template; }, (template is a string)
50
+ onRenderHeader : null, // function(index){}, (nothing to return)
51
+
52
+ // *** functionality
53
+ cancelSelection : true, // prevent text selection in the header
54
+ tabIndex : true, // add tabindex to header for keyboard accessibility
55
+ dateFormat : 'mmddyyyy', // other options: 'ddmmyyy' or 'yyyymmdd'
56
+ sortMultiSortKey : 'shiftKey', // key used to select additional columns
57
+ sortResetKey : 'ctrlKey', // key used to remove sorting on a column
58
+ usNumberFormat : true, // false for German '1.234.567,89' or French '1 234 567,89'
59
+ delayInit : false, // if false, the parsed table contents will not update until the first sort
60
+ serverSideSorting: false, // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used.
61
+ resort : true, // default setting to trigger a resort after an 'update', 'addRows', 'updateCell', etc has completed
62
+
63
+ // *** sort options
64
+ headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
65
+ ignoreCase : true, // ignore case while sorting
66
+ sortForce : null, // column(s) first sorted; always applied
67
+ sortList : [], // Initial sort order; applied initially; updated when manually sorted
68
+ sortAppend : null, // column(s) sorted last; always applied
69
+ sortStable : false, // when sorting two rows with exactly the same content, the original sort order is maintained
70
+
71
+ sortInitialOrder : 'asc', // sort direction on first click
72
+ sortLocaleCompare: false, // replace equivalent character (accented characters)
73
+ sortReset : false, // third click on the header will reset column to default - unsorted
74
+ sortRestart : false, // restart sort to 'sortInitialOrder' when clicking on previously unsorted columns
75
+
76
+ emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero, emptyMax, emptyMin
77
+ stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero
78
+ textExtraction : 'basic', // text extraction method/function - function(node, table, cellIndex){}
79
+ textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in default textExtraction function)
80
+ textSorter : null, // choose overall or specific column sorter function(a, b, direction, table, columnIndex) [alt: ts.sortText]
81
+ numberSorter : null, // choose overall numeric sorter function(a, b, direction, maxColumnValue)
82
+
83
+ // *** widget options
84
+ widgets: [], // method to add widgets, e.g. widgets: ['zebra']
85
+ widgetOptions : {
86
+ zebra : [ 'even', 'odd' ] // zebra widget alternating row class names
87
+ },
88
+ initWidgets : true, // apply widgets on tablesorter initialization
89
+ widgetClass : 'widget-{name}', // table class name template to match to include a widget
90
+
91
+ // *** callbacks
92
+ initialized : null, // function(table){},
93
+
94
+ // *** extra css class names
95
+ tableClass : '',
96
+ cssAsc : '',
97
+ cssDesc : '',
98
+ cssNone : '',
99
+ cssHeader : '',
100
+ cssHeaderRow : '',
101
+ cssProcessing : '', // processing icon applied to header during sort/filter
102
+
103
+ cssChildRow : 'tablesorter-childRow', // class name indiciating that a row is to be attached to the its parent
104
+ cssIcon : 'tablesorter-icon', // if this class does not exist, the {icon} will not be added from the headerTemplate
105
+ cssIconNone : '', // class name added to the icon when there is no column sort
106
+ cssIconAsc : '', // class name added to the icon when the column has an ascending sort
107
+ cssIconDesc : '', // class name added to the icon when the column has a descending sort
108
+ cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!)
109
+ cssNoSort : 'tablesorter-noSort', // class name added to element inside header; clicking on it won't cause a sort
110
+ cssIgnoreRow : 'tablesorter-ignoreRow', // header row to ignore; cells within this row will not be added to c.$headers
111
+
112
+ // *** selectors
113
+ selectorHeaders : '> thead th, > thead td',
114
+ selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort
115
+ selectorRemove : '.remove-me',
116
+
117
+ // *** advanced
118
+ debug : false,
119
+
120
+ // *** Internal variables
121
+ headerList: [],
122
+ empties: {},
123
+ strings: {},
124
+ parsers: []
125
+
126
+ // removed: widgetZebra: { css: ['even', 'odd'] }
127
+
128
+ };
129
+
130
+ // internal css classes - these will ALWAYS be added to
131
+ // the table and MUST only contain one class name - fixes #381
132
+ ts.css = {
133
+ table : 'tablesorter',
134
+ cssHasChild: 'tablesorter-hasChildRow',
135
+ childRow : 'tablesorter-childRow',
136
+ colgroup : 'tablesorter-colgroup',
137
+ header : 'tablesorter-header',
138
+ headerRow : 'tablesorter-headerRow',
139
+ headerIn : 'tablesorter-header-inner',
140
+ icon : 'tablesorter-icon',
141
+ processing : 'tablesorter-processing',
142
+ sortAsc : 'tablesorter-headerAsc',
143
+ sortDesc : 'tablesorter-headerDesc',
144
+ sortNone : 'tablesorter-headerUnSorted'
145
+ };
146
+
147
+ // labels applied to sortable headers for accessibility (aria) support
148
+ ts.language = {
149
+ sortAsc : 'Ascending sort applied, ',
150
+ sortDesc : 'Descending sort applied, ',
151
+ sortNone : 'No sort applied, ',
152
+ nextAsc : 'activate to apply an ascending sort',
153
+ nextDesc : 'activate to apply a descending sort',
154
+ nextNone : 'activate to remove the sort'
155
+ };
156
+
157
+ // These methods can be applied on table.config instance
158
+ ts.instanceMethods = {};
159
+
160
+ /* debuging utils */
161
+ function log() {
162
+ var a = arguments[0],
163
+ s = arguments.length > 1 ? Array.prototype.slice.call(arguments) : a;
164
+ if (typeof console !== 'undefined' && typeof console.log !== 'undefined') {
165
+ console[ /error/i.test(a) ? 'error' : /warn/i.test(a) ? 'warn' : 'log' ](s);
166
+ } else {
167
+ alert(s);
168
+ }
169
+ }
170
+
171
+ function benchmark(s, d) {
172
+ log(s + ' (' + (new Date().getTime() - d.getTime()) + 'ms)');
173
+ }
174
+
175
+ ts.log = log;
176
+ ts.benchmark = benchmark;
177
+
178
+ // $.isEmptyObject from jQuery v1.4
179
+ function isEmptyObject(obj) {
180
+ /*jshint forin: false */
181
+ for (var name in obj) {
182
+ return false;
183
+ }
184
+ return true;
185
+ }
186
+
187
+ ts.getElementText = function(c, node, cellIndex) {
188
+ if (!node) { return ''; }
189
+ var te,
190
+ t = c.textExtraction || '',
191
+ // node could be a jquery object
192
+ // http://jsperf.com/jquery-vs-instanceof-jquery/2
193
+ $node = node.jquery ? node : $(node);
194
+ if (typeof(t) === 'string') {
195
+ // check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow!
196
+ return $.trim( ( t === 'basic' ? $node.attr(c.textAttribute) || node.textContent : node.textContent ) || $node.text() || '' );
197
+ } else {
198
+ if (typeof(t) === 'function') {
199
+ return $.trim( t($node[0], c.table, cellIndex) );
200
+ } else if (typeof (te = ts.getColumnData( c.table, t, cellIndex )) === 'function') {
201
+ return $.trim( te($node[0], c.table, cellIndex) );
202
+ }
203
+ }
204
+ // fallback
205
+ return $.trim( $node[0].textContent || $node.text() || '' );
206
+ };
207
+
208
+ function detectParserForColumn(table, rows, rowIndex, cellIndex) {
209
+ var cur, $node,
210
+ c = table.config,
211
+ i = ts.parsers.length,
212
+ node = false,
213
+ nodeValue = '',
214
+ keepLooking = true;
215
+ while (nodeValue === '' && keepLooking) {
216
+ rowIndex++;
217
+ if (rows[rowIndex]) {
218
+ node = rows[rowIndex].cells[cellIndex];
219
+ nodeValue = ts.getElementText(c, node, cellIndex);
220
+ $node = $(node);
221
+ if (table.config.debug) {
222
+ log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': "' + nodeValue + '"');
223
+ }
224
+ } else {
225
+ keepLooking = false;
226
+ }
227
+ }
228
+ while (--i >= 0) {
229
+ cur = ts.parsers[i];
230
+ // ignore the default text parser because it will always be true
231
+ if (cur && cur.id !== 'text' && cur.is && cur.is(nodeValue, table, node, $node)) {
232
+ return cur;
233
+ }
234
+ }
235
+ // nothing found, return the generic parser (text)
236
+ return ts.getParserById('text');
237
+ }
238
+
239
+ function buildParserCache(table) {
240
+ var c = table.config,
241
+ // update table bodies in case we start with an empty table
242
+ tb = c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')'),
243
+ rows, list, l, i, h, ch, np, p, e, time,
244
+ j = 0,
245
+ parsersDebug = '',
246
+ len = tb.length;
247
+ if ( len === 0) {
248
+ return c.debug ? log('Warning: *Empty table!* Not building a parser cache') : '';
249
+ } else if (c.debug) {
250
+ time = new Date();
251
+ log('Detecting parsers for each column');
252
+ }
253
+ list = {
254
+ extractors: [],
255
+ parsers: []
256
+ };
257
+ while (j < len) {
258
+ rows = tb[j].rows;
259
+ if (rows.length) {
260
+ l = c.columns; // rows[j].cells.length;
261
+ for (i = 0; i < l; i++) {
262
+ h = c.$headerIndexed[i];
263
+ // get column indexed table cell
264
+ ch = ts.getColumnData( table, c.headers, i );
265
+ // get column parser/extractor
266
+ e = ts.getParserById( ts.getData(h, ch, 'extractor') );
267
+ p = ts.getParserById( ts.getData(h, ch, 'sorter') );
268
+ np = ts.getData(h, ch, 'parser') === 'false';
269
+ // empty cells behaviour - keeping emptyToBottom for backwards compatibility
270
+ c.empties[i] = ( ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase();
271
+ // text strings behaviour in numerical sorts
272
+ c.strings[i] = ( ts.getData(h, ch, 'string') || c.stringTo || 'max' ).toLowerCase();
273
+ if (np) {
274
+ p = ts.getParserById('no-parser');
275
+ }
276
+ if (!e) {
277
+ // For now, maybe detect someday
278
+ e = false;
279
+ }
280
+ if (!p) {
281
+ p = detectParserForColumn(table, rows, -1, i);
282
+ }
283
+ if (c.debug) {
284
+ parsersDebug += 'column:' + i + '; extractor:' + e.id + '; parser:' + p.id + '; string:' + c.strings[i] + '; empty: ' + c.empties[i] + '\n';
285
+ }
286
+ list.parsers[i] = p;
287
+ list.extractors[i] = e;
288
+ }
289
+ }
290
+ j += (list.parsers.length) ? len : 1;
291
+ }
292
+ if (c.debug) {
293
+ log(parsersDebug ? parsersDebug : 'No parsers detected');
294
+ benchmark('Completed detecting parsers', time);
295
+ }
296
+ c.parsers = list.parsers;
297
+ c.extractors = list.extractors;
298
+ }
299
+
300
+ /* utils */
301
+ function buildCache(table) {
302
+ var cc, t, tx, v, i, j, k, $row, cols, cacheTime,
303
+ totalRows, rowData, colMax,
304
+ c = table.config,
305
+ $tb = c.$tbodies,
306
+ extractors = c.extractors,
307
+ parsers = c.parsers;
308
+ c.cache = {};
309
+ c.totalRows = 0;
310
+ // if no parsers found, return - it's an empty table.
311
+ if (!parsers) {
312
+ return c.debug ? log('Warning: *Empty table!* Not building a cache') : '';
313
+ }
314
+ if (c.debug) {
315
+ cacheTime = new Date();
316
+ }
317
+ // processing icon
318
+ if (c.showProcessing) {
319
+ ts.isProcessing(table, true);
320
+ }
321
+ for (k = 0; k < $tb.length; k++) {
322
+ colMax = []; // column max value per tbody
323
+ cc = c.cache[k] = {
324
+ normalized: [] // array of normalized row data; last entry contains 'rowData' above
325
+ // colMax: # // added at the end
326
+ };
327
+
328
+ totalRows = ($tb[k] && $tb[k].rows.length) || 0;
329
+ for (i = 0; i < totalRows; ++i) {
330
+ rowData = {
331
+ // order: original row order #
332
+ // $row : jQuery Object[]
333
+ child: [], // child row text (filter widget)
334
+ raw: [] // original row text
335
+ };
336
+ /** Add the table data to main data array */
337
+ $row = $($tb[k].rows[i]);
338
+ cols = [];
339
+ // if this is a child row, add it to the last row's children and continue to the next row
340
+ // ignore child row class, if it is the first row
341
+ if ($row.hasClass(c.cssChildRow) && i !== 0) {
342
+ t = cc.normalized.length - 1;
343
+ cc.normalized[t][c.columns].$row = cc.normalized[t][c.columns].$row.add($row);
344
+ // add 'hasChild' class name to parent row
345
+ if (!$row.prev().hasClass(c.cssChildRow)) {
346
+ $row.prev().addClass(ts.css.cssHasChild);
347
+ }
348
+ // save child row content (un-parsed!)
349
+ rowData.child[t] = $.trim( $row[0].textContent || $row.text() || '' );
350
+ // go to the next for loop
351
+ continue;
352
+ }
353
+ rowData.$row = $row;
354
+ rowData.order = i; // add original row position to rowCache
355
+ for (j = 0; j < c.columns; ++j) {
356
+ if (typeof parsers[j] === 'undefined') {
357
+ if (c.debug) {
358
+ log('No parser found for cell:', $row[0].cells[j], 'does it have a header?');
359
+ }
360
+ continue;
361
+ }
362
+ t = ts.getElementText(c, $row[0].cells[j], j);
363
+ rowData.raw.push(t); // save original row text
364
+ // do extract before parsing if there is one
365
+ if (typeof extractors[j].id === 'undefined') {
366
+ tx = t;
367
+ } else {
368
+ tx = extractors[j].format(t, table, $row[0].cells[j], j);
369
+ }
370
+ // allow parsing if the string is empty, previously parsing would change it to zero,
371
+ // in case the parser needs to extract data from the table cell attributes
372
+ v = parsers[j].id === 'no-parser' ? '' : parsers[j].format(tx, table, $row[0].cells[j], j);
373
+ cols.push( c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v );
374
+ if ((parsers[j].type || '').toLowerCase() === 'numeric') {
375
+ // determine column max value (ignore sign)
376
+ colMax[j] = Math.max(Math.abs(v) || 0, colMax[j] || 0);
377
+ }
378
+ }
379
+ // ensure rowData is always in the same location (after the last column)
380
+ cols[c.columns] = rowData;
381
+ cc.normalized.push(cols);
382
+ }
383
+ cc.colMax = colMax;
384
+ // total up rows, not including child rows
385
+ c.totalRows += cc.normalized.length;
386
+
387
+ }
388
+ if (c.showProcessing) {
389
+ ts.isProcessing(table); // remove processing icon
390
+ }
391
+ if (c.debug) {
392
+ benchmark('Building cache for ' + totalRows + ' rows', cacheTime);
393
+ }
394
+ }
395
+
396
+ // init flag (true) used by pager plugin to prevent widget application
397
+ function appendToTable(table, init) {
398
+ var c = table.config,
399
+ wo = c.widgetOptions,
400
+ $tbodies = c.$tbodies,
401
+ rows = [],
402
+ cc = c.cache,
403
+ n, totalRows, $bk, $tb,
404
+ i, k, appendTime;
405
+ // empty table - fixes #206/#346
406
+ if (isEmptyObject(cc)) {
407
+ // run pager appender in case the table was just emptied
408
+ return c.appender ? c.appender(table, rows) :
409
+ table.isUpdating ? c.$table.trigger('updateComplete', table) : ''; // Fixes #532
410
+ }
411
+ if (c.debug) {
412
+ appendTime = new Date();
413
+ }
414
+ for (k = 0; k < $tbodies.length; k++) {
415
+ $bk = $tbodies.eq(k);
416
+ if ($bk.length) {
417
+ // get tbody
418
+ $tb = ts.processTbody(table, $bk, true);
419
+ n = cc[k].normalized;
420
+ totalRows = n.length;
421
+ for (i = 0; i < totalRows; i++) {
422
+ rows.push(n[i][c.columns].$row);
423
+ // removeRows used by the pager plugin; don't render if using ajax - fixes #411
424
+ if (!c.appender || (c.pager && (!c.pager.removeRows || !wo.pager_removeRows) && !c.pager.ajax)) {
425
+ $tb.append(n[i][c.columns].$row);
426
+ }
427
+ }
428
+ // restore tbody
429
+ ts.processTbody(table, $tb, false);
430
+ }
431
+ }
432
+ if (c.appender) {
433
+ c.appender(table, rows);
434
+ }
435
+ if (c.debug) {
436
+ benchmark('Rebuilt table', appendTime);
437
+ }
438
+ // apply table widgets; but not before ajax completes
439
+ if (!init && !c.appender) { ts.applyWidget(table); }
440
+ if (table.isUpdating) {
441
+ c.$table.trigger('updateComplete', table);
442
+ }
443
+ }
444
+
445
+ function formatSortingOrder(v) {
446
+ // look for 'd' in 'desc' order; return true
447
+ return (/^d/i.test(v) || v === 1);
448
+ }
449
+
450
+ function buildHeaders(table) {
451
+ var ch, $t, h, i, t, lock, time, indx,
452
+ c = table.config;
453
+ c.headerList = [];
454
+ c.headerContent = [];
455
+ if (c.debug) {
456
+ time = new Date();
457
+ }
458
+ // children tr in tfoot - see issue #196 & #547
459
+ c.columns = ts.computeColumnIndex( c.$table.children('thead, tfoot').children('tr') );
460
+ // add icon if cssIcon option exists
461
+ i = c.cssIcon ? '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' : '';
462
+ // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683
463
+ c.$headers = $( $.map( $(table).find(c.selectorHeaders), function(elem, index) {
464
+ $t = $(elem);
465
+ // ignore cell (don't add it to c.$headers) if row has ignoreRow class
466
+ if ($t.parent().hasClass(c.cssIgnoreRow)) { return; }
467
+ // make sure to get header cell & not column indexed cell
468
+ ch = ts.getColumnData( table, c.headers, index, true );
469
+ // save original header content
470
+ c.headerContent[index] = $t.html();
471
+ // if headerTemplate is empty, don't reformat the header cell
472
+ if ( c.headerTemplate !== '' && !$t.find('.' + ts.css.headerIn).length ) {
473
+ // set up header template
474
+ t = c.headerTemplate.replace(/\{content\}/g, $t.html()).replace(/\{icon\}/g, $t.find('.' + ts.css.icon).length ? '' : i);
475
+ if (c.onRenderTemplate) {
476
+ h = c.onRenderTemplate.apply($t, [index, t]);
477
+ if (h && typeof h === 'string') { t = h; } // only change t if something is returned
478
+ }
479
+ $t.html('<div class="' + ts.css.headerIn + '">' + t + '</div>'); // faster than wrapInner
480
+ }
481
+ if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index, c, c.$table]); }
482
+ // *** remove this.column value if no conflicts found
483
+ elem.column = parseInt( $t.attr('data-column'), 10);
484
+ elem.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2];
485
+ elem.count = -1; // set to -1 because clicking on the header automatically adds one
486
+ elem.lockedOrder = false;
487
+ lock = ts.getData($t, ch, 'lockedOrder') || false;
488
+ if (typeof lock !== 'undefined' && lock !== false) {
489
+ elem.order = elem.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0];
490
+ }
491
+ $t.addClass(ts.css.header + ' ' + c.cssHeader);
492
+ // add cell to headerList
493
+ c.headerList[index] = elem;
494
+ // add to parent in case there are multiple rows
495
+ $t.parent().addClass(ts.css.headerRow + ' ' + c.cssHeaderRow).attr('role', 'row');
496
+ // allow keyboard cursor to focus on element
497
+ if (c.tabIndex) { $t.attr('tabindex', 0); }
498
+ return elem;
499
+ }));
500
+ // cache headers per column
501
+ c.$headerIndexed = [];
502
+ for (indx = 0; indx < c.columns; indx++) {
503
+ $t = c.$headers.filter('[data-column="' + indx + '"]');
504
+ // target sortable column cells, unless there are none, then use non-sortable cells
505
+ // .last() added in jQuery 1.4; use .filter(':last') to maintain compatibility with jQuery v1.2.6
506
+ c.$headerIndexed[indx] = $t.not('.sorter-false').length ? $t.not('.sorter-false').filter(':last') : $t.filter(':last');
507
+ }
508
+ $(table).find(c.selectorHeaders).attr({
509
+ scope: 'col',
510
+ role : 'columnheader'
511
+ });
512
+ // enable/disable sorting
513
+ updateHeader(table);
514
+ if (c.debug) {
515
+ benchmark('Built headers:', time);
516
+ log(c.$headers);
517
+ }
518
+ }
519
+
520
+ function commonUpdate(table, resort, callback) {
521
+ var c = table.config;
522
+ // remove rows/elements before update
523
+ c.$table.find(c.selectorRemove).remove();
524
+ // rebuild parsers
525
+ buildParserCache(table);
526
+ // rebuild the cache map
527
+ buildCache(table);
528
+ checkResort(c, resort, callback);
529
+ }
530
+
531
+ function updateHeader(table) {
532
+ var s, $th, col,
533
+ c = table.config;
534
+ c.$headers.each(function(index, th){
535
+ $th = $(th);
536
+ col = ts.getColumnData( table, c.headers, index, true );
537
+ // add 'sorter-false' class if 'parser-false' is set
538
+ s = ts.getData( th, col, 'sorter' ) === 'false' || ts.getData( th, col, 'parser' ) === 'false';
539
+ th.sortDisabled = s;
540
+ $th[ s ? 'addClass' : 'removeClass' ]('sorter-false').attr('aria-disabled', '' + s);
541
+ // aria-controls - requires table ID
542
+ if (table.id) {
543
+ if (s) {
544
+ $th.removeAttr('aria-controls');
545
+ } else {
546
+ $th.attr('aria-controls', table.id);
547
+ }
548
+ }
549
+ });
550
+ }
551
+
552
+ function setHeadersCss(table) {
553
+ var f, i, j,
554
+ c = table.config,
555
+ list = c.sortList,
556
+ len = list.length,
557
+ none = ts.css.sortNone + ' ' + c.cssNone,
558
+ css = [ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc],
559
+ cssIcon = [ c.cssIconAsc, c.cssIconDesc, c.cssIconNone ],
560
+ aria = ['ascending', 'descending'],
561
+ // find the footer
562
+ $t = $(table).find('tfoot tr').children().add(c.$extraHeaders).removeClass(css.join(' '));
563
+ // remove all header information
564
+ c.$headers
565
+ .removeClass(css.join(' '))
566
+ .addClass(none).attr('aria-sort', 'none')
567
+ .find('.' + c.cssIcon)
568
+ .removeClass(cssIcon.join(' '))
569
+ .addClass(cssIcon[2]);
570
+ for (i = 0; i < len; i++) {
571
+ // direction = 2 means reset!
572
+ if (list[i][1] !== 2) {
573
+ // multicolumn sorting updating - choose the :last in case there are nested columns
574
+ f = c.$headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (len === 1 ? ':last' : '') );
575
+ if (f.length) {
576
+ for (j = 0; j < f.length; j++) {
577
+ if (!f[j].sortDisabled) {
578
+ f.eq(j)
579
+ .removeClass(none)
580
+ .addClass(css[list[i][1]])
581
+ .attr('aria-sort', aria[list[i][1]])
582
+ .find('.' + c.cssIcon)
583
+ .removeClass(cssIcon[2])
584
+ .addClass(cssIcon[list[i][1]]);
585
+ }
586
+ }
587
+ // add sorted class to footer & extra headers, if they exist
588
+ if ($t.length) {
589
+ $t.filter('[data-column="' + list[i][0] + '"]').removeClass(none).addClass(css[list[i][1]]);
590
+ }
591
+ }
592
+ }
593
+ }
594
+ // add verbose aria labels
595
+ c.$headers.not('.sorter-false').each(function(){
596
+ var $this = $(this),
597
+ nextSort = this.order[(this.count + 1) % (c.sortReset ? 3 : 2)],
598
+ txt = $.trim( $this.text() ) + ': ' +
599
+ ts.language[ $this.hasClass(ts.css.sortAsc) ? 'sortAsc' : $this.hasClass(ts.css.sortDesc) ? 'sortDesc' : 'sortNone' ] +
600
+ ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ];
601
+ $this.attr('aria-label', txt );
602
+ });
603
+ }
604
+
605
+ function updateHeaderSortCount( table, list ) {
606
+ var col, dir, group, header, indx, primary, temp, val,
607
+ c = table.config,
608
+ sortList = list || c.sortList,
609
+ len = sortList.length;
610
+ c.sortList = [];
611
+ for (indx = 0; indx < len; indx++) {
612
+ val = sortList[indx];
613
+ // ensure all sortList values are numeric - fixes #127
614
+ col = parseInt(val[0], 10);
615
+ // make sure header exists
616
+ header = c.$headerIndexed[col][0];
617
+ if (header) { // prevents error if sorton array is wrong
618
+ // o.count = o.count + 1;
619
+ dir = ('' + val[1]).match(/^(1|d|s|o|n)/);
620
+ dir = dir ? dir[0] : '';
621
+ // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext
622
+ switch(dir) {
623
+ case '1': case 'd': // descending
624
+ dir = 1;
625
+ break;
626
+ case 's': // same direction (as primary column)
627
+ // if primary sort is set to 's', make it ascending
628
+ dir = primary || 0;
629
+ break;
630
+ case 'o':
631
+ temp = header.order[(primary || 0) % (c.sortReset ? 3 : 2)];
632
+ // opposite of primary column; but resets if primary resets
633
+ dir = temp === 0 ? 1 : temp === 1 ? 0 : 2;
634
+ break;
635
+ case 'n':
636
+ header.count = header.count + 1;
637
+ dir = header.order[(header.count) % (c.sortReset ? 3 : 2)];
638
+ break;
639
+ default: // ascending
640
+ dir = 0;
641
+ break;
642
+ }
643
+ primary = indx === 0 ? dir : primary;
644
+ group = [ col, parseInt(dir, 10) || 0 ];
645
+ c.sortList.push(group);
646
+ dir = $.inArray(group[1], header.order); // fixes issue #167
647
+ header.count = dir >= 0 ? dir : group[1] % (c.sortReset ? 3 : 2);
648
+ }
649
+ }
650
+ }
651
+
652
+ function getCachedSortType(parsers, i) {
653
+ return (parsers && parsers[i]) ? parsers[i].type || '' : '';
654
+ }
655
+
656
+ function initSort(table, cell, event){
657
+ if (table.isUpdating) {
658
+ // let any updates complete before initializing a sort
659
+ return setTimeout(function(){ initSort(table, cell, event); }, 50);
660
+ }
661
+ var arry, indx, col, order, s,
662
+ c = table.config,
663
+ key = !event[c.sortMultiSortKey],
664
+ $table = c.$table;
665
+ // Only call sortStart if sorting is enabled
666
+ $table.trigger('sortStart', table);
667
+ // get current column sort order
668
+ cell.count = event[c.sortResetKey] ? 2 : (cell.count + 1) % (c.sortReset ? 3 : 2);
669
+ // reset all sorts on non-current column - issue #30
670
+ if (c.sortRestart) {
671
+ indx = cell;
672
+ c.$headers.each(function() {
673
+ // only reset counts on columns that weren't just clicked on and if not included in a multisort
674
+ if (this !== indx && (key || !$(this).is('.' + ts.css.sortDesc + ',.' + ts.css.sortAsc))) {
675
+ this.count = -1;
676
+ }
677
+ });
678
+ }
679
+ // get current column index
680
+ indx = parseInt( $(cell).attr('data-column'), 10 );
681
+ // user only wants to sort on one column
682
+ if (key) {
683
+ // flush the sort list
684
+ c.sortList = [];
685
+ if (c.sortForce !== null) {
686
+ arry = c.sortForce;
687
+ for (col = 0; col < arry.length; col++) {
688
+ if (arry[col][0] !== indx) {
689
+ c.sortList.push(arry[col]);
690
+ }
691
+ }
692
+ }
693
+ // add column to sort list
694
+ order = cell.order[cell.count];
695
+ if (order < 2) {
696
+ c.sortList.push([indx, order]);
697
+ // add other columns if header spans across multiple
698
+ if (cell.colSpan > 1) {
699
+ for (col = 1; col < cell.colSpan; col++) {
700
+ c.sortList.push([indx + col, order]);
701
+ }
702
+ }
703
+ }
704
+ // multi column sorting
705
+ } else {
706
+ // get rid of the sortAppend before adding more - fixes issue #115 & #523
707
+ if (c.sortAppend && c.sortList.length > 1) {
708
+ for (col = 0; col < c.sortAppend.length; col++) {
709
+ s = ts.isValueInArray(c.sortAppend[col][0], c.sortList);
710
+ if (s >= 0) {
711
+ c.sortList.splice(s,1);
712
+ }
713
+ }
714
+ }
715
+ // the user has clicked on an already sorted column
716
+ if (ts.isValueInArray(indx, c.sortList) >= 0) {
717
+ // reverse the sorting direction
718
+ for (col = 0; col < c.sortList.length; col++) {
719
+ s = c.sortList[col];
720
+ order = c.$headerIndexed[ s[0] ][0];
721
+ if (s[0] === indx) {
722
+ // order.count seems to be incorrect when compared to cell.count
723
+ s[1] = order.order[cell.count];
724
+ if (s[1] === 2) {
725
+ c.sortList.splice(col,1);
726
+ order.count = -1;
727
+ }
728
+ }
729
+ }
730
+ } else {
731
+ // add column to sort list array
732
+ order = cell.order[cell.count];
733
+ if (order < 2) {
734
+ c.sortList.push([indx, order]);
735
+ // add other columns if header spans across multiple
736
+ if (cell.colSpan > 1) {
737
+ for (col = 1; col < cell.colSpan; col++) {
738
+ c.sortList.push([indx + col, order]);
739
+ }
740
+ }
741
+ }
742
+ }
743
+ }
744
+ if (c.sortAppend !== null) {
745
+ arry = c.sortAppend;
746
+ for (col = 0; col < arry.length; col++) {
747
+ if (arry[col][0] !== indx) {
748
+ c.sortList.push(arry[col]);
749
+ }
750
+ }
751
+ }
752
+ // sortBegin event triggered immediately before the sort
753
+ $table.trigger('sortBegin', table);
754
+ // setTimeout needed so the processing icon shows up
755
+ setTimeout(function(){
756
+ // set css for headers
757
+ setHeadersCss(table);
758
+ multisort(table);
759
+ appendToTable(table);
760
+ $table.trigger('sortEnd', table);
761
+ }, 1);
762
+ }
763
+
764
+ // sort multiple columns
765
+ function multisort(table) { /*jshint loopfunc:true */
766
+ var i, k, num, col, sortTime, colMax,
767
+ rows, order, sort, x, y,
768
+ dir = 0,
769
+ c = table.config,
770
+ cts = c.textSorter || '',
771
+ sortList = c.sortList,
772
+ l = sortList.length,
773
+ bl = c.$tbodies.length;
774
+ if (c.serverSideSorting || isEmptyObject(c.cache)) { // empty table - fixes #206/#346
775
+ return;
776
+ }
777
+ if (c.debug) { sortTime = new Date(); }
778
+ for (k = 0; k < bl; k++) {
779
+ colMax = c.cache[k].colMax;
780
+ rows = c.cache[k].normalized;
781
+
782
+ rows.sort(function(a, b) {
783
+ // rows is undefined here in IE, so don't use it!
784
+ for (i = 0; i < l; i++) {
785
+ col = sortList[i][0];
786
+ order = sortList[i][1];
787
+ // sort direction, true = asc, false = desc
788
+ dir = order === 0;
789
+
790
+ if (c.sortStable && a[col] === b[col] && l === 1) {
791
+ return a[c.columns].order - b[c.columns].order;
792
+ }
793
+
794
+ // fallback to natural sort since it is more robust
795
+ num = /n/i.test(getCachedSortType(c.parsers, col));
796
+ if (num && c.strings[col]) {
797
+ // sort strings in numerical columns
798
+ if (typeof (c.string[c.strings[col]]) === 'boolean') {
799
+ num = (dir ? 1 : -1) * (c.string[c.strings[col]] ? -1 : 1);
800
+ } else {
801
+ num = (c.strings[col]) ? c.string[c.strings[col]] || 0 : 0;
802
+ }
803
+ // fall back to built-in numeric sort
804
+ // var sort = $.tablesorter['sort' + s](table, a[c], b[c], c, colMax[c], dir);
805
+ sort = c.numberSorter ? c.numberSorter(a[col], b[col], dir, colMax[col], table) :
806
+ ts[ 'sortNumeric' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], num, colMax[col], col, table);
807
+ } else {
808
+ // set a & b depending on sort direction
809
+ x = dir ? a : b;
810
+ y = dir ? b : a;
811
+ // text sort function
812
+ if (typeof(cts) === 'function') {
813
+ // custom OVERALL text sorter
814
+ sort = cts(x[col], y[col], dir, col, table);
815
+ } else if (typeof(cts) === 'object' && cts.hasOwnProperty(col)) {
816
+ // custom text sorter for a SPECIFIC COLUMN
817
+ sort = cts[col](x[col], y[col], dir, col, table);
818
+ } else {
819
+ // fall back to natural sort
820
+ sort = ts[ 'sortNatural' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], col, table, c);
821
+ }
822
+ }
823
+ if (sort) { return sort; }
824
+ }
825
+ return a[c.columns].order - b[c.columns].order;
826
+ });
827
+ }
828
+ if (c.debug) { benchmark('Sorting on ' + sortList.toString() + ' and dir ' + order + ' time', sortTime); }
829
+ }
830
+
831
+ function resortComplete(c, callback){
832
+ if (c.table.isUpdating) {
833
+ c.$table.trigger('updateComplete', c.table);
834
+ }
835
+ if ($.isFunction(callback)) {
836
+ callback(c.table);
837
+ }
838
+ }
839
+
840
+ function checkResort(c, resort, callback) {
841
+ var sl = $.isArray(resort) ? resort : c.sortList,
842
+ // if no resort parameter is passed, fallback to config.resort (true by default)
843
+ resrt = typeof resort === 'undefined' ? c.resort : resort;
844
+ // don't try to resort if the table is still processing
845
+ // this will catch spamming of the updateCell method
846
+ if (resrt !== false && !c.serverSideSorting && !c.table.isProcessing) {
847
+ if (sl.length) {
848
+ c.$table.trigger('sorton', [sl, function(){
849
+ resortComplete(c, callback);
850
+ }, true]);
851
+ } else {
852
+ c.$table.trigger('sortReset', [function(){
853
+ resortComplete(c, callback);
854
+ ts.applyWidget(c.table, false);
855
+ }]);
856
+ }
857
+ } else {
858
+ resortComplete(c, callback);
859
+ ts.applyWidget(c.table, false);
860
+ }
861
+ }
862
+
863
+ function bindMethods(table){
864
+ var c = table.config,
865
+ $table = c.$table,
866
+ events = ('sortReset update updateRows updateCell updateAll addRows updateComplete sorton appendCache ' +
867
+ 'updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave ').split(' ')
868
+ .join(c.namespace + ' ');
869
+ // apply easy methods that trigger bound events
870
+ $table
871
+ .unbind( events.replace(/\s+/g, ' ') )
872
+ .bind('sortReset' + c.namespace, function(e, callback){
873
+ e.stopPropagation();
874
+ c.sortList = [];
875
+ setHeadersCss(table);
876
+ multisort(table);
877
+ appendToTable(table);
878
+ if ($.isFunction(callback)) {
879
+ callback(table);
880
+ }
881
+ })
882
+ .bind('updateAll' + c.namespace, function(e, resort, callback){
883
+ e.stopPropagation();
884
+ table.isUpdating = true;
885
+ ts.refreshWidgets(table, true, true);
886
+ buildHeaders(table);
887
+ ts.bindEvents(table, c.$headers, true);
888
+ bindMethods(table);
889
+ commonUpdate(table, resort, callback);
890
+ })
891
+ .bind('update' + c.namespace + ' updateRows' + c.namespace, function(e, resort, callback) {
892
+ e.stopPropagation();
893
+ table.isUpdating = true;
894
+ // update sorting (if enabled/disabled)
895
+ updateHeader(table);
896
+ commonUpdate(table, resort, callback);
897
+ })
898
+ .bind('updateCell' + c.namespace, function(e, cell, resort, callback) {
899
+ e.stopPropagation();
900
+ table.isUpdating = true;
901
+ $table.find(c.selectorRemove).remove();
902
+ // get position from the dom
903
+ var v, t, row, icell,
904
+ $tb = c.$tbodies,
905
+ $cell = $(cell),
906
+ // update cache - format: function(s, table, cell, cellIndex)
907
+ // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr');
908
+ tbdy = $tb.index( $.fn.closest ? $cell.closest('tbody') : $cell.parents('tbody').filter(':first') ),
909
+ $row = $.fn.closest ? $cell.closest('tr') : $cell.parents('tr').filter(':first');
910
+ cell = $cell[0]; // in case cell is a jQuery object
911
+ // tbody may not exist if update is initialized while tbody is removed for processing
912
+ if ($tb.length && tbdy >= 0) {
913
+ row = $tb.eq(tbdy).find('tr').index( $row );
914
+ icell = $cell.index();
915
+ c.cache[tbdy].normalized[row][c.columns].$row = $row;
916
+ if (typeof c.extractors[icell].id === 'undefined') {
917
+ t = ts.getElementText(c, cell, icell);
918
+ } else {
919
+ t = c.extractors[icell].format( ts.getElementText(c, cell, icell), table, cell, icell );
920
+ }
921
+ v = c.parsers[icell].id === 'no-parser' ? '' :
922
+ c.parsers[icell].format( t, table, cell, icell );
923
+ c.cache[tbdy].normalized[row][icell] = c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v;
924
+ if ((c.parsers[icell].type || '').toLowerCase() === 'numeric') {
925
+ // update column max value (ignore sign)
926
+ c.cache[tbdy].colMax[icell] = Math.max(Math.abs(v) || 0, c.cache[tbdy].colMax[icell] || 0);
927
+ }
928
+ v = resort !== 'undefined' ? resort : c.resort;
929
+ if (v !== false) {
930
+ // widgets will be reapplied
931
+ checkResort(c, v, callback);
932
+ } else {
933
+ // don't reapply widgets is resort is false, just in case it causes
934
+ // problems with element focus
935
+ if ($.isFunction(callback)) {
936
+ callback(table);
937
+ }
938
+ c.$table.trigger('updateComplete', c.table);
939
+ }
940
+ }
941
+ })
942
+ .bind('addRows' + c.namespace, function(e, $row, resort, callback) {
943
+ e.stopPropagation();
944
+ table.isUpdating = true;
945
+ if (isEmptyObject(c.cache)) {
946
+ // empty table, do an update instead - fixes #450
947
+ updateHeader(table);
948
+ commonUpdate(table, resort, callback);
949
+ } else {
950
+ $row = $($row).attr('role', 'row'); // make sure we're using a jQuery object
951
+ var i, j, l, t, v, rowData, cells,
952
+ rows = $row.filter('tr').length,
953
+ tbdy = c.$tbodies.index( $row.parents('tbody').filter(':first') );
954
+ // fixes adding rows to an empty table - see issue #179
955
+ if (!(c.parsers && c.parsers.length)) {
956
+ buildParserCache(table);
957
+ }
958
+ // add each row
959
+ for (i = 0; i < rows; i++) {
960
+ l = $row[i].cells.length;
961
+ cells = [];
962
+ rowData = {
963
+ child: [],
964
+ $row : $row.eq(i),
965
+ order: c.cache[tbdy].normalized.length
966
+ };
967
+ // add each cell
968
+ for (j = 0; j < l; j++) {
969
+ if (typeof c.extractors[j].id === 'undefined') {
970
+ t = ts.getElementText(c, $row[i].cells[j], j);
971
+ } else {
972
+ t = c.extractors[j].format( ts.getElementText(c, $row[i].cells[j], j), table, $row[i].cells[j], j );
973
+ }
974
+ v = c.parsers[j].id === 'no-parser' ? '' :
975
+ c.parsers[j].format( t, table, $row[i].cells[j], j );
976
+ cells[j] = c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v;
977
+ if ((c.parsers[j].type || '').toLowerCase() === 'numeric') {
978
+ // update column max value (ignore sign)
979
+ c.cache[tbdy].colMax[j] = Math.max(Math.abs(cells[j]) || 0, c.cache[tbdy].colMax[j] || 0);
980
+ }
981
+ }
982
+ // add the row data to the end
983
+ cells.push(rowData);
984
+ // update cache
985
+ c.cache[tbdy].normalized.push(cells);
986
+ }
987
+ // resort using current settings
988
+ checkResort(c, resort, callback);
989
+ }
990
+ })
991
+ .bind('updateComplete' + c.namespace, function(){
992
+ table.isUpdating = false;
993
+ })
994
+ .bind('sorton' + c.namespace, function(e, list, callback, init) {
995
+ var c = table.config;
996
+ e.stopPropagation();
997
+ $table.trigger('sortStart', this);
998
+ // update header count index
999
+ updateHeaderSortCount(table, list);
1000
+ // set css for headers
1001
+ setHeadersCss(table);
1002
+ // fixes #346
1003
+ if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); }
1004
+ $table.trigger('sortBegin', this);
1005
+ // sort the table and append it to the dom
1006
+ multisort(table);
1007
+ appendToTable(table, init);
1008
+ $table.trigger('sortEnd', this);
1009
+ ts.applyWidget(table);
1010
+ if ($.isFunction(callback)) {
1011
+ callback(table);
1012
+ }
1013
+ })
1014
+ .bind('appendCache' + c.namespace, function(e, callback, init) {
1015
+ e.stopPropagation();
1016
+ appendToTable(table, init);
1017
+ if ($.isFunction(callback)) {
1018
+ callback(table);
1019
+ }
1020
+ })
1021
+ .bind('updateCache' + c.namespace, function(e, callback){
1022
+ // rebuild parsers
1023
+ if (!(c.parsers && c.parsers.length)) {
1024
+ buildParserCache(table);
1025
+ }
1026
+ // rebuild the cache map
1027
+ buildCache(table);
1028
+ if ($.isFunction(callback)) {
1029
+ callback(table);
1030
+ }
1031
+ })
1032
+ .bind('applyWidgetId' + c.namespace, function(e, id) {
1033
+ e.stopPropagation();
1034
+ ts.getWidgetById(id).format(table, c, c.widgetOptions);
1035
+ })
1036
+ .bind('applyWidgets' + c.namespace, function(e, init) {
1037
+ e.stopPropagation();
1038
+ // apply widgets
1039
+ ts.applyWidget(table, init);
1040
+ })
1041
+ .bind('refreshWidgets' + c.namespace, function(e, all, dontapply){
1042
+ e.stopPropagation();
1043
+ ts.refreshWidgets(table, all, dontapply);
1044
+ })
1045
+ .bind('destroy' + c.namespace, function(e, c, cb){
1046
+ e.stopPropagation();
1047
+ ts.destroy(table, c, cb);
1048
+ })
1049
+ .bind('resetToLoadState' + c.namespace, function(){
1050
+ // remove all widgets
1051
+ ts.removeWidget(table, true, false);
1052
+ // restore original settings; this clears out current settings, but does not clear
1053
+ // values saved to storage.
1054
+ c = $.extend(true, ts.defaults, c.originalSettings);
1055
+ table.hasInitialized = false;
1056
+ // setup the entire table again
1057
+ ts.setup( table, c );
1058
+ });
1059
+ }
1060
+
1061
+ /* public methods */
1062
+ ts.construct = function(settings) {
1063
+ return this.each(function() {
1064
+ var table = this,
1065
+ // merge & extend config options
1066
+ c = $.extend(true, {}, ts.defaults, settings, ts.instanceMethods);
1067
+ // save initial settings
1068
+ c.originalSettings = settings;
1069
+ // create a table from data (build table widget)
1070
+ if (!table.hasInitialized && ts.buildTable && this.tagName !== 'TABLE') {
1071
+ // return the table (in case the original target is the table's container)
1072
+ ts.buildTable(table, c);
1073
+ } else {
1074
+ ts.setup(table, c);
1075
+ }
1076
+ });
1077
+ };
1078
+
1079
+ ts.setup = function(table, c) {
1080
+ // if no thead or tbody, or tablesorter is already present, quit
1081
+ if (!table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true) {
1082
+ return c.debug ? log('ERROR: stopping initialization! No table, thead, tbody or tablesorter has already been initialized') : '';
1083
+ }
1084
+
1085
+ var k = '',
1086
+ $table = $(table),
1087
+ m = $.metadata;
1088
+ // initialization flag
1089
+ table.hasInitialized = false;
1090
+ // table is being processed flag
1091
+ table.isProcessing = true;
1092
+ // make sure to store the config object
1093
+ table.config = c;
1094
+ // save the settings where they read
1095
+ $.data(table, 'tablesorter', c);
1096
+ if (c.debug) { $.data( table, 'startoveralltimer', new Date()); }
1097
+
1098
+ // removing this in version 3 (only supports jQuery 1.7+)
1099
+ c.supportsDataObject = (function(version) {
1100
+ version[0] = parseInt(version[0], 10);
1101
+ return (version[0] > 1) || (version[0] === 1 && parseInt(version[1], 10) >= 4);
1102
+ })($.fn.jquery.split('.'));
1103
+ // digit sort text location; keeping max+/- for backwards compatibility
1104
+ c.string = { 'max': 1, 'min': -1, 'emptymin': 1, 'emptymax': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false };
1105
+ // ensure case insensitivity
1106
+ c.emptyTo = c.emptyTo.toLowerCase();
1107
+ c.stringTo = c.stringTo.toLowerCase();
1108
+ // add table theme class only if there isn't already one there
1109
+ if (!/tablesorter\-/.test($table.attr('class'))) {
1110
+ k = (c.theme !== '' ? ' tablesorter-' + c.theme : '');
1111
+ }
1112
+ c.table = table;
1113
+ c.$table = $table
1114
+ .addClass(ts.css.table + ' ' + c.tableClass + k)
1115
+ .attr('role', 'grid');
1116
+ c.$headers = $table.find(c.selectorHeaders);
1117
+
1118
+ // give the table a unique id, which will be used in namespace binding
1119
+ if (!c.namespace) {
1120
+ c.namespace = '.tablesorter' + Math.random().toString(16).slice(2);
1121
+ } else {
1122
+ // make sure namespace starts with a period & doesn't have weird characters
1123
+ c.namespace = '.' + c.namespace.replace(/\W/g,'');
1124
+ }
1125
+
1126
+ c.$table.children().children('tr').attr('role', 'row');
1127
+ c.$tbodies = $table.children('tbody:not(.' + c.cssInfoBlock + ')').attr({
1128
+ 'aria-live' : 'polite',
1129
+ 'aria-relevant' : 'all'
1130
+ });
1131
+ if (c.$table.children('caption').length) {
1132
+ k = c.$table.children('caption')[0];
1133
+ if (!k.id) { k.id = c.namespace.slice(1) + 'caption'; }
1134
+ c.$table.attr('aria-labelledby', k.id);
1135
+ }
1136
+ c.widgetInit = {}; // keep a list of initialized widgets
1137
+ // change textExtraction via data-attribute
1138
+ c.textExtraction = c.$table.attr('data-text-extraction') || c.textExtraction || 'basic';
1139
+ // build headers
1140
+ buildHeaders(table);
1141
+ // fixate columns if the users supplies the fixedWidth option
1142
+ // do this after theme has been applied
1143
+ ts.fixColumnWidth(table);
1144
+ // add widget options before parsing (e.g. grouping widget has parser settings)
1145
+ ts.applyWidgetOptions(table, c);
1146
+ // try to auto detect column type, and store in tables config
1147
+ buildParserCache(table);
1148
+ // start total row count at zero
1149
+ c.totalRows = 0;
1150
+ // build the cache for the tbody cells
1151
+ // delayInit will delay building the cache until the user starts a sort
1152
+ if (!c.delayInit) { buildCache(table); }
1153
+ // bind all header events and methods
1154
+ ts.bindEvents(table, c.$headers, true);
1155
+ bindMethods(table);
1156
+ // get sort list from jQuery data or metadata
1157
+ // in jQuery < 1.4, an error occurs when calling $table.data()
1158
+ if (c.supportsDataObject && typeof $table.data().sortlist !== 'undefined') {
1159
+ c.sortList = $table.data().sortlist;
1160
+ } else if (m && ($table.metadata() && $table.metadata().sortlist)) {
1161
+ c.sortList = $table.metadata().sortlist;
1162
+ }
1163
+ // apply widget init code
1164
+ ts.applyWidget(table, true);
1165
+ // if user has supplied a sort list to constructor
1166
+ if (c.sortList.length > 0) {
1167
+ $table.trigger('sorton', [c.sortList, {}, !c.initWidgets, true]);
1168
+ } else {
1169
+ setHeadersCss(table);
1170
+ if (c.initWidgets) {
1171
+ // apply widget format
1172
+ ts.applyWidget(table, false);
1173
+ }
1174
+ }
1175
+
1176
+ // show processesing icon
1177
+ if (c.showProcessing) {
1178
+ $table
1179
+ .unbind('sortBegin' + c.namespace + ' sortEnd' + c.namespace)
1180
+ .bind('sortBegin' + c.namespace + ' sortEnd' + c.namespace, function(e) {
1181
+ clearTimeout(c.processTimer);
1182
+ ts.isProcessing(table);
1183
+ if (e.type === 'sortBegin') {
1184
+ c.processTimer = setTimeout(function(){
1185
+ ts.isProcessing(table, true);
1186
+ }, 500);
1187
+ }
1188
+ });
1189
+ }
1190
+
1191
+ // initialized
1192
+ table.hasInitialized = true;
1193
+ table.isProcessing = false;
1194
+ if (c.debug) {
1195
+ ts.benchmark('Overall initialization time', $.data( table, 'startoveralltimer'));
1196
+ }
1197
+ $table.trigger('tablesorter-initialized', table);
1198
+ if (typeof c.initialized === 'function') { c.initialized(table); }
1199
+ };
1200
+
1201
+ // automatically add a colgroup with col elements set to a percentage width
1202
+ ts.fixColumnWidth = function(table) {
1203
+ table = $(table)[0];
1204
+ var overallWidth, percent,
1205
+ c = table.config,
1206
+ colgroup = c.$table.children('colgroup');
1207
+ // remove plugin-added colgroup, in case we need to refresh the widths
1208
+ if (colgroup.length && colgroup.hasClass(ts.css.colgroup)) {
1209
+ colgroup.remove();
1210
+ }
1211
+ if (c.widthFixed && c.$table.children('colgroup').length === 0) {
1212
+ colgroup = $('<colgroup class="' + ts.css.colgroup + '">');
1213
+ overallWidth = c.$table.width();
1214
+ // only add col for visible columns - fixes #371
1215
+ c.$tbodies.find('tr:first').children(':visible').each(function() {
1216
+ percent = parseInt( ( $(this).width() / overallWidth ) * 1000, 10 ) / 10 + '%';
1217
+ colgroup.append( $('<col>').css('width', percent) );
1218
+ });
1219
+ c.$table.prepend(colgroup);
1220
+ }
1221
+ };
1222
+
1223
+ ts.getColumnData = function(table, obj, indx, getCell, $headers){
1224
+ if (typeof obj === 'undefined' || obj === null) { return; }
1225
+ table = $(table)[0];
1226
+ var $h, k,
1227
+ c = table.config,
1228
+ $cells = ( $headers || c.$headers ),
1229
+ // c.$headerIndexed is not defined initially
1230
+ $cell = c.$headerIndexed && c.$headerIndexed[indx] || $cells.filter('[data-column="' + indx + '"]:last');
1231
+ if (obj[indx]) {
1232
+ return getCell ? obj[indx] : obj[$cells.index( $cell )];
1233
+ }
1234
+ for (k in obj) {
1235
+ if (typeof k === 'string') {
1236
+ $h = $cell
1237
+ // header cell with class/id
1238
+ .filter(k)
1239
+ // find elements within the header cell with cell/id
1240
+ .add( $cell.find(k) );
1241
+ if ($h.length) {
1242
+ return obj[k];
1243
+ }
1244
+ }
1245
+ }
1246
+ return;
1247
+ };
1248
+
1249
+ // computeTableHeaderCellIndexes from:
1250
+ // http://www.javascripttoolbox.com/lib/table/examples.php
1251
+ // http://www.javascripttoolbox.com/temp/table_cellindex.html
1252
+ ts.computeColumnIndex = function(trs) {
1253
+ var matrix = [],
1254
+ lookup = {},
1255
+ cols = 0, // determine the number of columns
1256
+ i, j, k, l, $cell, cell, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow;
1257
+ for (i = 0; i < trs.length; i++) {
1258
+ cells = trs[i].cells;
1259
+ for (j = 0; j < cells.length; j++) {
1260
+ cell = cells[j];
1261
+ $cell = $(cell);
1262
+ rowIndex = cell.parentNode.rowIndex;
1263
+ cellId = rowIndex + '-' + $cell.index();
1264
+ rowSpan = cell.rowSpan || 1;
1265
+ colSpan = cell.colSpan || 1;
1266
+ if (typeof(matrix[rowIndex]) === 'undefined') {
1267
+ matrix[rowIndex] = [];
1268
+ }
1269
+ // Find first available column in the first row
1270
+ for (k = 0; k < matrix[rowIndex].length + 1; k++) {
1271
+ if (typeof(matrix[rowIndex][k]) === 'undefined') {
1272
+ firstAvailCol = k;
1273
+ break;
1274
+ }
1275
+ }
1276
+ lookup[cellId] = firstAvailCol;
1277
+ cols = Math.max(firstAvailCol, cols);
1278
+ // add data-column
1279
+ $cell.attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex
1280
+ for (k = rowIndex; k < rowIndex + rowSpan; k++) {
1281
+ if (typeof(matrix[k]) === 'undefined') {
1282
+ matrix[k] = [];
1283
+ }
1284
+ matrixrow = matrix[k];
1285
+ for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
1286
+ matrixrow[l] = 'x';
1287
+ }
1288
+ }
1289
+ }
1290
+ }
1291
+ // may not be accurate if # header columns !== # tbody columns
1292
+ return cols + 1; // add one because it's a zero-based index
1293
+ };
1294
+
1295
+ // *** Process table ***
1296
+ // add processing indicator
1297
+ ts.isProcessing = function(table, toggle, $ths) {
1298
+ table = $(table);
1299
+ var c = table[0].config,
1300
+ // default to all headers
1301
+ $h = $ths || table.find('.' + ts.css.header);
1302
+ if (toggle) {
1303
+ // don't use sortList if custom $ths used
1304
+ if (typeof $ths !== 'undefined' && c.sortList.length > 0) {
1305
+ // get headers from the sortList
1306
+ $h = $h.filter(function(){
1307
+ // get data-column from attr to keep compatibility with jQuery 1.2.6
1308
+ return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList) >= 0;
1309
+ });
1310
+ }
1311
+ table.add($h).addClass(ts.css.processing + ' ' + c.cssProcessing);
1312
+ } else {
1313
+ table.add($h).removeClass(ts.css.processing + ' ' + c.cssProcessing);
1314
+ }
1315
+ };
1316
+
1317
+ // detach tbody but save the position
1318
+ // don't use tbody because there are portions that look for a tbody index (updateCell)
1319
+ ts.processTbody = function(table, $tb, getIt){
1320
+ table = $(table)[0];
1321
+ var holdr;
1322
+ if (getIt) {
1323
+ table.isProcessing = true;
1324
+ $tb.before('<span class="tablesorter-savemyplace"/>');
1325
+ holdr = ($.fn.detach) ? $tb.detach() : $tb.remove();
1326
+ return holdr;
1327
+ }
1328
+ holdr = $(table).find('span.tablesorter-savemyplace');
1329
+ $tb.insertAfter( holdr );
1330
+ holdr.remove();
1331
+ table.isProcessing = false;
1332
+ };
1333
+
1334
+ ts.clearTableBody = function(table) {
1335
+ $(table)[0].config.$tbodies.children().detach();
1336
+ };
1337
+
1338
+ ts.bindEvents = function(table, $headers, core){
1339
+ table = $(table)[0];
1340
+ var downTime,
1341
+ c = table.config;
1342
+ if (core !== true) {
1343
+ c.$extraHeaders = c.$extraHeaders ? c.$extraHeaders.add($headers) : $headers;
1344
+ }
1345
+ // apply event handling to headers and/or additional headers (stickyheaders, scroller, etc)
1346
+ $headers
1347
+ // http://stackoverflow.com/questions/5312849/jquery-find-self;
1348
+ .find(c.selectorSort).add( $headers.filter(c.selectorSort) )
1349
+ .unbind( ('mousedown mouseup sort keyup '.split(' ').join(c.namespace + ' ')).replace(/\s+/g, ' ') )
1350
+ .bind( 'mousedown mouseup sort keyup '.split(' ').join(c.namespace + ' '), function(e, external) {
1351
+ var cell,
1352
+ $target = $(e.target),
1353
+ type = e.type;
1354
+ // only recognize left clicks or enter
1355
+ if ( ((e.which || e.button) !== 1 && !/sort|keyup/.test(type)) || (type === 'keyup' && e.which !== 13) ) {
1356
+ return;
1357
+ }
1358
+ // ignore long clicks (prevents resizable widget from initializing a sort)
1359
+ if (type === 'mouseup' && external !== true && (new Date().getTime() - downTime > 250)) { return; }
1360
+ // set timer on mousedown
1361
+ if (type === 'mousedown') {
1362
+ downTime = new Date().getTime();
1363
+ return;
1364
+ }
1365
+ cell = $.fn.closest ? $target.closest('td,th') : $target.parents('td,th').filter(':first');
1366
+ // prevent sort being triggered on form elements
1367
+ if ( /(input|select|button|textarea)/i.test(e.target.tagName) ||
1368
+ // nosort class name, or elements within a nosort container
1369
+ $target.hasClass(c.cssNoSort) || $target.parents('.' + c.cssNoSort).length > 0 ||
1370
+ // elements within a button
1371
+ $target.parents('button').length > 0 ) {
1372
+ return !c.cancelSelection;
1373
+ }
1374
+ if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); }
1375
+ // jQuery v1.2.6 doesn't have closest()
1376
+ cell = $.fn.closest ? $(this).closest('th, td')[0] : /TH|TD/.test(this.tagName) ? this : $(this).parents('th, td')[0];
1377
+ // reference original table headers and find the same cell
1378
+ cell = c.$headers[ $headers.index( cell ) ];
1379
+ if (!cell.sortDisabled) {
1380
+ initSort(table, cell, e);
1381
+ }
1382
+ });
1383
+ if (c.cancelSelection) {
1384
+ // cancel selection
1385
+ $headers
1386
+ .attr('unselectable', 'on')
1387
+ .bind('selectstart', false)
1388
+ .css({
1389
+ 'user-select': 'none',
1390
+ 'MozUserSelect': 'none' // not needed for jQuery 1.8+
1391
+ });
1392
+ }
1393
+ };
1394
+
1395
+ // restore headers
1396
+ ts.restoreHeaders = function(table){
1397
+ var $cell,
1398
+ c = $(table)[0].config;
1399
+ // don't use c.$headers here in case header cells were swapped
1400
+ c.$table.find(c.selectorHeaders).each(function(i){
1401
+ $cell = $(this);
1402
+ // only restore header cells if it is wrapped
1403
+ // because this is also used by the updateAll method
1404
+ if ($cell.find('.' + ts.css.headerIn).length){
1405
+ $cell.html( c.headerContent[i] );
1406
+ }
1407
+ });
1408
+ };
1409
+
1410
+ ts.destroy = function(table, removeClasses, callback){
1411
+ table = $(table)[0];
1412
+ if (!table.hasInitialized) { return; }
1413
+ // remove all widgets
1414
+ ts.removeWidget(table, true, false);
1415
+ var events,
1416
+ $t = $(table),
1417
+ c = table.config,
1418
+ $h = $t.find('thead:first'),
1419
+ $r = $h.find('tr.' + ts.css.headerRow).removeClass(ts.css.headerRow + ' ' + c.cssHeaderRow),
1420
+ $f = $t.find('tfoot:first > tr').children('th, td');
1421
+ if (removeClasses === false && $.inArray('uitheme', c.widgets) >= 0) {
1422
+ // reapply uitheme classes, in case we want to maintain appearance
1423
+ $t.trigger('applyWidgetId', ['uitheme']);
1424
+ $t.trigger('applyWidgetId', ['zebra']);
1425
+ }
1426
+ // remove widget added rows, just in case
1427
+ $h.find('tr').not($r).remove();
1428
+ // disable tablesorter
1429
+ events = 'sortReset update updateAll updateRows updateCell addRows updateComplete sorton appendCache updateCache ' +
1430
+ 'applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave keypress sortBegin sortEnd resetToLoadState '.split(' ')
1431
+ .join(c.namespace + ' ');
1432
+ $t
1433
+ .removeData('tablesorter')
1434
+ .unbind( events.replace(/\s+/g, ' ') );
1435
+ c.$headers.add($f)
1436
+ .removeClass( [ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone].join(' ') )
1437
+ .removeAttr('data-column')
1438
+ .removeAttr('aria-label')
1439
+ .attr('aria-disabled', 'true');
1440
+ $r.find(c.selectorSort).unbind( ('mousedown mouseup keypress '.split(' ').join(c.namespace + ' ')).replace(/\s+/g, ' ') );
1441
+ ts.restoreHeaders(table);
1442
+ $t.toggleClass(ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false);
1443
+ // clear flag in case the plugin is initialized again
1444
+ table.hasInitialized = false;
1445
+ delete table.config.cache;
1446
+ if (typeof callback === 'function') {
1447
+ callback(table);
1448
+ }
1449
+ };
1450
+
1451
+ // *** sort functions ***
1452
+ // regex used in natural sort
1453
+ ts.regex = {
1454
+ chunk : /(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, // chunk/tokenize numbers & letters
1455
+ chunks: /(^\\0|\\0$)/, // replace chunks @ ends
1456
+ hex: /^0x[0-9a-f]+$/i // hex
1457
+ };
1458
+
1459
+ // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed)
1460
+ // this function will only accept strings, or you'll see 'TypeError: undefined is not a function'
1461
+ // I could add a = a.toString(); b = b.toString(); but it'll slow down the sort overall
1462
+ ts.sortNatural = function(a, b) {
1463
+ if (a === b) { return 0; }
1464
+ var xN, xD, yN, yD, xF, yF, i, mx,
1465
+ r = ts.regex;
1466
+ // first try and sort Hex codes
1467
+ if (r.hex.test(b)) {
1468
+ xD = parseInt(a.match(r.hex), 16);
1469
+ yD = parseInt(b.match(r.hex), 16);
1470
+ if ( xD < yD ) { return -1; }
1471
+ if ( xD > yD ) { return 1; }
1472
+ }
1473
+ // chunk/tokenize
1474
+ xN = a.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0');
1475
+ yN = b.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0');
1476
+ mx = Math.max(xN.length, yN.length);
1477
+ // natural sorting through split numeric strings and default strings
1478
+ for (i = 0; i < mx; i++) {
1479
+ // find floats not starting with '0', string or 0 if not defined
1480
+ xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0;
1481
+ yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0;
1482
+ // handle numeric vs string comparison - number < string - (Kyle Adams)
1483
+ if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; }
1484
+ // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
1485
+ if (typeof xF !== typeof yF) {
1486
+ xF += '';
1487
+ yF += '';
1488
+ }
1489
+ if (xF < yF) { return -1; }
1490
+ if (xF > yF) { return 1; }
1491
+ }
1492
+ return 0;
1493
+ };
1494
+
1495
+ ts.sortNaturalAsc = function(a, b, col, table, c) {
1496
+ if (a === b) { return 0; }
1497
+ var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1498
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1499
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1500
+ return ts.sortNatural(a, b);
1501
+ };
1502
+
1503
+ ts.sortNaturalDesc = function(a, b, col, table, c) {
1504
+ if (a === b) { return 0; }
1505
+ var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1506
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1507
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1508
+ return ts.sortNatural(b, a);
1509
+ };
1510
+
1511
+ // basic alphabetical sort
1512
+ ts.sortText = function(a, b) {
1513
+ return a > b ? 1 : (a < b ? -1 : 0);
1514
+ };
1515
+
1516
+ // return text string value by adding up ascii value
1517
+ // so the text is somewhat sorted when using a digital sort
1518
+ // this is NOT an alphanumeric sort
1519
+ ts.getTextValue = function(a, num, mx) {
1520
+ if (mx) {
1521
+ // make sure the text value is greater than the max numerical value (mx)
1522
+ var i, l = a ? a.length : 0, n = mx + num;
1523
+ for (i = 0; i < l; i++) {
1524
+ n += a.charCodeAt(i);
1525
+ }
1526
+ return num * n;
1527
+ }
1528
+ return 0;
1529
+ };
1530
+
1531
+ ts.sortNumericAsc = function(a, b, num, mx, col, table) {
1532
+ if (a === b) { return 0; }
1533
+ var c = table.config,
1534
+ e = c.string[ (c.empties[col] || c.emptyTo ) ];
1535
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1536
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1537
+ if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1538
+ if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1539
+ return a - b;
1540
+ };
1541
+
1542
+ ts.sortNumericDesc = function(a, b, num, mx, col, table) {
1543
+ if (a === b) { return 0; }
1544
+ var c = table.config,
1545
+ e = c.string[ (c.empties[col] || c.emptyTo ) ];
1546
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1547
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1548
+ if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1549
+ if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1550
+ return b - a;
1551
+ };
1552
+
1553
+ ts.sortNumeric = function(a, b) {
1554
+ return a - b;
1555
+ };
1556
+
1557
+ // used when replacing accented characters during sorting
1558
+ ts.characterEquivalents = {
1559
+ 'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãäąå
1560
+ 'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄÅ
1561
+ 'c' : '\u00e7\u0107\u010d', // çćč
1562
+ 'C' : '\u00c7\u0106\u010c', // ÇĆČ
1563
+ 'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę
1564
+ 'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ
1565
+ 'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı
1566
+ 'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ
1567
+ 'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6', // óòôõö
1568
+ 'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6', // ÓÒÔÕÖ
1569
+ 'ss': '\u00df', // ß (s sharp)
1570
+ 'SS': '\u1e9e', // ẞ (Capital sharp s)
1571
+ 'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů
1572
+ 'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ
1573
+ };
1574
+ ts.replaceAccents = function(s) {
1575
+ var a, acc = '[', eq = ts.characterEquivalents;
1576
+ if (!ts.characterRegex) {
1577
+ ts.characterRegexArray = {};
1578
+ for (a in eq) {
1579
+ if (typeof a === 'string') {
1580
+ acc += eq[a];
1581
+ ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g');
1582
+ }
1583
+ }
1584
+ ts.characterRegex = new RegExp(acc + ']');
1585
+ }
1586
+ if (ts.characterRegex.test(s)) {
1587
+ for (a in eq) {
1588
+ if (typeof a === 'string') {
1589
+ s = s.replace( ts.characterRegexArray[a], a );
1590
+ }
1591
+ }
1592
+ }
1593
+ return s;
1594
+ };
1595
+
1596
+ // *** utilities ***
1597
+ ts.isValueInArray = function(column, arry) {
1598
+ var indx, len = arry.length;
1599
+ for (indx = 0; indx < len; indx++) {
1600
+ if (arry[indx][0] === column) {
1601
+ return indx;
1602
+ }
1603
+ }
1604
+ return -1;
1605
+ };
1606
+
1607
+ ts.addParser = function(parser) {
1608
+ var i, l = ts.parsers.length, a = true;
1609
+ for (i = 0; i < l; i++) {
1610
+ if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) {
1611
+ a = false;
1612
+ }
1613
+ }
1614
+ if (a) {
1615
+ ts.parsers.push(parser);
1616
+ }
1617
+ };
1618
+
1619
+ // Use it to add a set of methods to table.config which will be available for all tables.
1620
+ // This should be done before table initialization
1621
+ ts.addInstanceMethods = function(methods) {
1622
+ $.extend(ts.instanceMethods, methods);
1623
+ };
1624
+
1625
+ ts.getParserById = function(name) {
1626
+ /*jshint eqeqeq:false */
1627
+ if (name == 'false') { return false; }
1628
+ var i, l = ts.parsers.length;
1629
+ for (i = 0; i < l; i++) {
1630
+ if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) {
1631
+ return ts.parsers[i];
1632
+ }
1633
+ }
1634
+ return false;
1635
+ };
1636
+
1637
+ ts.addWidget = function(widget) {
1638
+ ts.widgets.push(widget);
1639
+ };
1640
+
1641
+ ts.hasWidget = function(table, name){
1642
+ table = $(table);
1643
+ return table.length && table[0].config && table[0].config.widgetInit[name] || false;
1644
+ };
1645
+
1646
+ ts.getWidgetById = function(name) {
1647
+ var i, w, l = ts.widgets.length;
1648
+ for (i = 0; i < l; i++) {
1649
+ w = ts.widgets[i];
1650
+ if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) {
1651
+ return w;
1652
+ }
1653
+ }
1654
+ };
1655
+
1656
+ ts.applyWidgetOptions = function( table, c ){
1657
+ var indx, widget,
1658
+ len = c.widgets.length,
1659
+ wo = c.widgetOptions;
1660
+ if (len) {
1661
+ for (indx = 0; indx < len; indx++) {
1662
+ widget = ts.getWidgetById( c.widgets[indx] );
1663
+ if ( widget && 'options' in widget ) {
1664
+ wo = table.config.widgetOptions = $.extend( true, {}, widget.options, wo );
1665
+ }
1666
+ }
1667
+ }
1668
+ };
1669
+
1670
+ ts.applyWidget = function(table, init, callback) {
1671
+ table = $(table)[0]; // in case this is called externally
1672
+ var indx, len, name,
1673
+ c = table.config,
1674
+ wo = c.widgetOptions,
1675
+ tableClass = ' ' + c.table.className + ' ',
1676
+ widgets = [],
1677
+ time, time2, w, wd;
1678
+ // prevent numerous consecutive widget applications
1679
+ if (init !== false && table.hasInitialized && (table.isApplyingWidgets || table.isUpdating)) { return; }
1680
+ if (c.debug) { time = new Date(); }
1681
+ // look for widgets to apply from in table class
1682
+ // stop using \b otherwise this matches 'ui-widget-content' & adds 'content' widget
1683
+ wd = new RegExp( '\\s' + c.widgetClass.replace( /\{name\}/i, '([\\w-]+)' )+ '\\s', 'g' );
1684
+ if ( tableClass.match( wd ) ) {
1685
+ // extract out the widget id from the table class (widget id's can include dashes)
1686
+ w = tableClass.match( wd );
1687
+ if ( w ) {
1688
+ len = w.length;
1689
+ for (indx = 0; indx < len; indx++) {
1690
+ c.widgets.push( w[indx].replace( wd, '$1' ) );
1691
+ }
1692
+ }
1693
+ }
1694
+ if (c.widgets.length) {
1695
+ table.isApplyingWidgets = true;
1696
+ // ensure unique widget ids
1697
+ c.widgets = $.grep(c.widgets, function(v, k){
1698
+ return $.inArray(v, c.widgets) === k;
1699
+ });
1700
+ name = c.widgets || [];
1701
+ len = name.length;
1702
+ // build widget array & add priority as needed
1703
+ for (indx = 0; indx < len; indx++) {
1704
+ wd = ts.getWidgetById(name[indx]);
1705
+ if (wd && wd.id) {
1706
+ // set priority to 10 if not defined
1707
+ if (!wd.priority) { wd.priority = 10; }
1708
+ widgets[indx] = wd;
1709
+ }
1710
+ }
1711
+ // sort widgets by priority
1712
+ widgets.sort(function(a, b){
1713
+ return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1;
1714
+ });
1715
+ // add/update selected widgets
1716
+ len = widgets.length;
1717
+ for (indx = 0; indx < len; indx++) {
1718
+ if (widgets[indx]) {
1719
+ if ( init || !( c.widgetInit[ widgets[indx].id ] ) ) {
1720
+ // set init flag first to prevent calling init more than once (e.g. pager)
1721
+ c.widgetInit[ widgets[indx].id ] = true;
1722
+ if (table.hasInitialized) {
1723
+ // don't reapply widget options on tablesorter init
1724
+ ts.applyWidgetOptions( table, c );
1725
+ }
1726
+ if ( 'init' in widgets[indx] ) {
1727
+ if (c.debug) { time2 = new Date(); }
1728
+ widgets[indx].init(table, widgets[indx], c, wo);
1729
+ if (c.debug) { ts.benchmark('Initializing ' + widgets[indx].id + ' widget', time2); }
1730
+ }
1731
+ }
1732
+ if ( !init && 'format' in widgets[indx] ) {
1733
+ if (c.debug) { time2 = new Date(); }
1734
+ widgets[indx].format(table, c, wo, false);
1735
+ if (c.debug) { ts.benchmark( ( init ? 'Initializing ' : 'Applying ' ) + widgets[indx].id + ' widget', time2); }
1736
+ }
1737
+ }
1738
+ }
1739
+ // callback executed on init only
1740
+ if (!init && typeof callback === 'function') {
1741
+ callback(table);
1742
+ }
1743
+ }
1744
+ setTimeout(function(){
1745
+ table.isApplyingWidgets = false;
1746
+ $.data(table, 'lastWidgetApplication', new Date());
1747
+ }, 0);
1748
+ if (c.debug) {
1749
+ w = c.widgets.length;
1750
+ benchmark('Completed ' + (init === true ? 'initializing ' : 'applying ') + w + ' widget' + (w !== 1 ? 's' : ''), time);
1751
+ }
1752
+ };
1753
+
1754
+ ts.removeWidget = function(table, name, refreshing){
1755
+ table = $(table)[0];
1756
+ var i, widget, indx, len,
1757
+ c = table.config;
1758
+ // if name === true, add all widgets from $.tablesorter.widgets
1759
+ if (name === true) {
1760
+ name = [];
1761
+ len = ts.widgets.length;
1762
+ for (indx = 0; indx < len; indx++) {
1763
+ widget = ts.widgets[indx];
1764
+ if (widget && widget.id) {
1765
+ name.push( widget.id );
1766
+ }
1767
+ }
1768
+ } else {
1769
+ // name can be either an array of widgets names,
1770
+ // or a space/comma separated list of widget names
1771
+ name = ( $.isArray(name) ? name.join(',') : name || '' ).toLowerCase().split( /[\s,]+/ );
1772
+ }
1773
+ len = name.length;
1774
+ for (i = 0; i < len; i++) {
1775
+ widget = ts.getWidgetById(name[i]);
1776
+ indx = $.inArray( name[i], c.widgets );
1777
+ if ( widget && 'remove' in widget ) {
1778
+ if (c.debug && indx >= 0) { log( 'Removing "' + name[i] + '" widget' ); }
1779
+ widget.remove(table, c, c.widgetOptions, refreshing);
1780
+ c.widgetInit[ name[i] ] = false;
1781
+ }
1782
+ // don't remove the widget from config.widget if refreshing
1783
+ if (indx >= 0 && refreshing !== true) {
1784
+ c.widgets.splice( indx, 1 );
1785
+ }
1786
+ }
1787
+ };
1788
+
1789
+ ts.refreshWidgets = function(table, doAll, dontapply) {
1790
+ table = $(table)[0]; // see issue #243
1791
+ var indx,
1792
+ c = table.config,
1793
+ cw = c.widgets,
1794
+ widgets = ts.widgets,
1795
+ len = widgets.length,
1796
+ list = [],
1797
+ callback = function(table){
1798
+ $(table).trigger('refreshComplete');
1799
+ };
1800
+ // remove widgets not defined in config.widgets, unless doAll is true
1801
+ for (indx = 0; indx < len; indx++) {
1802
+ if (widgets[indx] && widgets[indx].id && (doAll || $.inArray( widgets[indx].id, cw ) < 0)) {
1803
+ list.push( widgets[indx].id );
1804
+ }
1805
+ }
1806
+ ts.removeWidget( table, list.join(','), true );
1807
+ if (dontapply !== true) {
1808
+ // call widget init if
1809
+ ts.applyWidget(table, doAll || false, callback );
1810
+ if (doAll) {
1811
+ // apply widget format
1812
+ ts.applyWidget(table, false, callback);
1813
+ }
1814
+ } else {
1815
+ callback(table);
1816
+ }
1817
+ };
1818
+
1819
+ // get sorter, string, empty, etc options for each column from
1820
+ // jQuery data, metadata, header option or header class name ('sorter-false')
1821
+ // priority = jQuery data > meta > headers option > header class name
1822
+ ts.getData = function(h, ch, key) {
1823
+ var val = '', $h = $(h), m, cl;
1824
+ if (!$h.length) { return ''; }
1825
+ m = $.metadata ? $h.metadata() : false;
1826
+ cl = ' ' + ($h.attr('class') || '');
1827
+ if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){
1828
+ // 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder'
1829
+ // 'data-sort-initial-order' is assigned to 'sortInitialOrder'
1830
+ val += $h.data(key) || $h.data(key.toLowerCase());
1831
+ } else if (m && typeof m[key] !== 'undefined') {
1832
+ val += m[key];
1833
+ } else if (ch && typeof ch[key] !== 'undefined') {
1834
+ val += ch[key];
1835
+ } else if (cl !== ' ' && cl.match(' ' + key + '-')) {
1836
+ // include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser'
1837
+ val = cl.match( new RegExp('\\s' + key + '-([\\w-]+)') )[1] || '';
1838
+ }
1839
+ return $.trim(val);
1840
+ };
1841
+
1842
+ ts.formatFloat = function(s, table) {
1843
+ if (typeof s !== 'string' || s === '') { return s; }
1844
+ // allow using formatFloat without a table; defaults to US number format
1845
+ var i,
1846
+ t = table && table.config ? table.config.usNumberFormat !== false :
1847
+ typeof table !== 'undefined' ? table : true;
1848
+ if (t) {
1849
+ // US Format - 1,234,567.89 -> 1234567.89
1850
+ s = s.replace(/,/g,'');
1851
+ } else {
1852
+ // German Format = 1.234.567,89 -> 1234567.89
1853
+ // French Format = 1 234 567,89 -> 1234567.89
1854
+ s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.');
1855
+ }
1856
+ if(/^\s*\([.\d]+\)/.test(s)) {
1857
+ // make (#) into a negative number -> (10) = -10
1858
+ s = s.replace(/^\s*\(([.\d]+)\)/, '-$1');
1859
+ }
1860
+ i = parseFloat(s);
1861
+ // return the text instead of zero
1862
+ return isNaN(i) ? $.trim(s) : i;
1863
+ };
1864
+
1865
+ ts.isDigit = function(s) {
1866
+ // replace all unwanted chars and match
1867
+ return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'"\s]/g, '')) : true;
1868
+ };
1869
+
1870
+ }()
1871
+ });
1872
+
1873
+ // make shortcut
1874
+ var ts = $.tablesorter;
1875
+
1876
+ // extend plugin scope
1877
+ $.fn.extend({
1878
+ tablesorter: ts.construct
1879
+ });
1880
+
1881
+ // add default parsers
1882
+ ts.addParser({
1883
+ id: 'no-parser',
1884
+ is: function() {
1885
+ return false;
1886
+ },
1887
+ format: function() {
1888
+ return '';
1889
+ },
1890
+ type: 'text'
1891
+ });
1892
+
1893
+ ts.addParser({
1894
+ id: 'text',
1895
+ is: function() {
1896
+ return true;
1897
+ },
1898
+ format: function(s, table) {
1899
+ var c = table.config;
1900
+ if (s) {
1901
+ s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s );
1902
+ s = c.sortLocaleCompare ? ts.replaceAccents(s) : s;
1903
+ }
1904
+ return s;
1905
+ },
1906
+ type: 'text'
1907
+ });
1908
+
1909
+ ts.addParser({
1910
+ id: 'digit',
1911
+ is: function(s) {
1912
+ return ts.isDigit(s);
1913
+ },
1914
+ format: function(s, table) {
1915
+ var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ''), table);
1916
+ return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
1917
+ },
1918
+ type: 'numeric'
1919
+ });
1920
+
1921
+ ts.addParser({
1922
+ id: 'currency',
1923
+ is: function(s) {
1924
+ return (/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/).test((s || '').replace(/[+\-,. ]/g,'')); // £$€¤¥¢
1925
+ },
1926
+ format: function(s, table) {
1927
+ var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ''), table);
1928
+ return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
1929
+ },
1930
+ type: 'numeric'
1931
+ });
1932
+
1933
+ ts.addParser({
1934
+ id: 'url',
1935
+ is: function(s) {
1936
+ return (/^(https?|ftp|file):\/\//).test(s);
1937
+ },
1938
+ format: function(s) {
1939
+ return s ? $.trim(s.replace(/(https?|ftp|file):\/\//, '')) : s;
1940
+ },
1941
+ parsed : true, // filter widget flag
1942
+ type: 'text'
1943
+ });
1944
+
1945
+ ts.addParser({
1946
+ id: 'isoDate',
1947
+ is: function(s) {
1948
+ return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/).test(s);
1949
+ },
1950
+ format: function(s, table) {
1951
+ var date = s ? new Date( s.replace(/-/g, '/') ) : s;
1952
+ return date instanceof Date && isFinite(date) ? date.getTime() : s;
1953
+ },
1954
+ type: 'numeric'
1955
+ });
1956
+
1957
+ ts.addParser({
1958
+ id: 'percent',
1959
+ is: function(s) {
1960
+ return (/(\d\s*?%|%\s*?\d)/).test(s) && s.length < 15;
1961
+ },
1962
+ format: function(s, table) {
1963
+ return s ? ts.formatFloat(s.replace(/%/g, ''), table) : s;
1964
+ },
1965
+ type: 'numeric'
1966
+ });
1967
+
1968
+ // added image parser to core v2.17.9
1969
+ ts.addParser({
1970
+ id: 'image',
1971
+ is: function(s, table, node, $node){
1972
+ return $node.find('img').length > 0;
1973
+ },
1974
+ format: function(s, table, cell) {
1975
+ return $(cell).find('img').attr(table.config.imgAttr || 'alt') || s;
1976
+ },
1977
+ parsed : true, // filter widget flag
1978
+ type: 'text'
1979
+ });
1980
+
1981
+ ts.addParser({
1982
+ id: 'usLongDate',
1983
+ is: function(s) {
1984
+ // two digit years are not allowed cross-browser
1985
+ // Jan 01, 2013 12:34:56 PM or 01 Jan 2013
1986
+ return (/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i).test(s) || (/^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i).test(s);
1987
+ },
1988
+ format: function(s, table) {
1989
+ var date = s ? new Date( s.replace(/(\S)([AP]M)$/i, '$1 $2') ) : s;
1990
+ return date instanceof Date && isFinite(date) ? date.getTime() : s;
1991
+ },
1992
+ type: 'numeric'
1993
+ });
1994
+
1995
+ ts.addParser({
1996
+ id: 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd'
1997
+ is: function(s) {
1998
+ // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included
1999
+ return (/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/).test((s || '').replace(/\s+/g,' ').replace(/[\-.,]/g, '/'));
2000
+ },
2001
+ format: function(s, table, cell, cellIndex) {
2002
+ if (s) {
2003
+ var date, d,
2004
+ c = table.config,
2005
+ ci = c.$headerIndexed[ cellIndex ],
2006
+ format = ci.length && ci[0].dateFormat || ts.getData( ci, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat') || c.dateFormat;
2007
+ d = s.replace(/\s+/g, ' ').replace(/[\-.,]/g, '/'); // escaped - because JSHint in Firefox was showing it as an error
2008
+ if (format === 'mmddyyyy') {
2009
+ d = d.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, '$3/$1/$2');
2010
+ } else if (format === 'ddmmyyyy') {
2011
+ d = d.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, '$3/$2/$1');
2012
+ } else if (format === 'yyyymmdd') {
2013
+ d = d.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, '$1/$2/$3');
2014
+ }
2015
+ date = new Date(d);
2016
+ return date instanceof Date && isFinite(date) ? date.getTime() : s;
2017
+ }
2018
+ return s;
2019
+ },
2020
+ type: 'numeric'
2021
+ });
2022
+
2023
+ ts.addParser({
2024
+ id: 'time',
2025
+ is: function(s) {
2026
+ return (/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s);
2027
+ },
2028
+ format: function(s, table) {
2029
+ var date = s ? new Date( '2000/01/01 ' + s.replace(/(\S)([AP]M)$/i, '$1 $2') ) : s;
2030
+ return date instanceof Date && isFinite(date) ? date.getTime() : s;
2031
+ },
2032
+ type: 'numeric'
2033
+ });
2034
+
2035
+ ts.addParser({
2036
+ id: 'metadata',
2037
+ is: function() {
2038
+ return false;
2039
+ },
2040
+ format: function(s, table, cell) {
2041
+ var c = table.config,
2042
+ p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
2043
+ return $(cell).metadata()[p];
2044
+ },
2045
+ type: 'numeric'
2046
+ });
2047
+
2048
+ // add default widgets
2049
+ ts.addWidget({
2050
+ id: 'zebra',
2051
+ priority: 90,
2052
+ format: function(table, c, wo) {
2053
+ var $tb, $tv, $tr, row, even, time, k,
2054
+ child = new RegExp(c.cssChildRow, 'i'),
2055
+ b = c.$tbodies;
2056
+ if (c.debug) {
2057
+ time = new Date();
2058
+ }
2059
+ for (k = 0; k < b.length; k++ ) {
2060
+ // loop through the visible rows
2061
+ row = 0;
2062
+ $tb = b.eq(k);
2063
+ $tv = $tb.children('tr:visible').not(c.selectorRemove);
2064
+ // revered back to using jQuery each - strangely it's the fastest method
2065
+ /*jshint loopfunc:true */
2066
+ $tv.each(function(){
2067
+ $tr = $(this);
2068
+ // style child rows the same way the parent row was styled
2069
+ if (!child.test(this.className)) { row++; }
2070
+ even = (row % 2 === 0);
2071
+ $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]);
2072
+ });
2073
+ }
2074
+ },
2075
+ remove: function(table, c, wo, refreshing){
2076
+ if (refreshing) { return; }
2077
+ var k, $tb,
2078
+ b = c.$tbodies,
2079
+ rmv = (wo.zebra || [ 'even', 'odd' ]).join(' ');
2080
+ for (k = 0; k < b.length; k++ ){
2081
+ $tb = ts.processTbody(table, b.eq(k), true); // remove tbody
2082
+ $tb.children().removeClass(rmv);
2083
+ ts.processTbody(table, $tb, false); // restore tbody
2084
+ }
2085
+ }
2086
+ });
2087
+
2088
+ return ts;
2089
+ }));