rubycritic 1.3.0 → 1.4.0

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