jquery-tablesorter 1.18.5 → 1.19.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/jquery-tablesorter/version.rb +1 -1
  4. data/vendor/assets/javascripts/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.js +68 -46
  5. data/vendor/assets/javascripts/jquery-tablesorter/extras/jquery.dragtable.mod.js +3 -3
  6. data/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.combined.js +2383 -2100
  7. data/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.js +2298 -2039
  8. data/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.widgets.js +90 -66
  9. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-month.js +45 -20
  10. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-weekday.js +78 -20
  11. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-globalize.js +37 -15
  12. data/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-input-select.js +4 -4
  13. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-chart.js +2 -2
  14. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-columnSelector.js +122 -30
  15. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-editable.js +8 -6
  16. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter.js +79 -58
  17. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-grouping.js +209 -128
  18. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-headerTitles.js +5 -4
  19. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-lazyload.js +367 -0
  20. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-math.js +81 -35
  21. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-output.js +3 -3
  22. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-pager.js +79 -53
  23. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-print.js +25 -14
  24. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-saveSort.js +5 -2
  25. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-scroller.js +11 -7
  26. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-sort2Hash.js +149 -50
  27. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-sortTbodies.js +7 -7
  28. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-staticRow.js +2 -2
  29. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-stickyHeaders.js +2 -2
  30. data/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-view.js +192 -0
  31. metadata +4 -2
@@ -1,4 +1,4 @@
1
- /*! TableSorter (FORK) v2.23.5 *//*
1
+ /*! TableSorter (FORK) v2.24.2 *//*
2
2
  * Client-side table sorting with ease!
3
3
  * @requires jQuery v1.2.6+
4
4
  *
@@ -17,2159 +17,2392 @@
17
17
  * @contributor Rob Garrison - https://github.com/Mottie/tablesorter
18
18
  */
19
19
  /*jshint browser:true, jquery:true, unused:false, expr: true */
20
- ;(function($){
20
+ ;( function( $ ) {
21
21
  'use strict';
22
- $.extend({
23
- /*jshint supernew:true */
24
- tablesorter: new function() {
25
-
26
- var ts = this;
27
-
28
- ts.version = '2.23.5';
29
-
30
- ts.parsers = [];
31
- ts.widgets = [];
32
- ts.defaults = {
33
-
34
- // *** appearance
35
- theme : 'default', // adds tablesorter-{theme} to the table for styling
36
- widthFixed : false, // adds colgroup to fix widths of columns
37
- showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered.
38
-
39
- headerTemplate : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = <i/> (class from cssIcon)
40
- onRenderTemplate : null, // function(index, template){ return template; }, (template is a string)
41
- onRenderHeader : null, // function(index){}, (nothing to return)
42
-
43
- // *** functionality
44
- cancelSelection : true, // prevent text selection in the header
45
- tabIndex : true, // add tabindex to header for keyboard accessibility
46
- dateFormat : 'mmddyyyy', // other options: 'ddmmyyy' or 'yyyymmdd'
47
- sortMultiSortKey : 'shiftKey', // key used to select additional columns
48
- sortResetKey : 'ctrlKey', // key used to remove sorting on a column
49
- usNumberFormat : true, // false for German '1.234.567,89' or French '1 234 567,89'
50
- delayInit : false, // if false, the parsed table contents will not update until the first sort
51
- 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.
52
- resort : true, // default setting to trigger a resort after an 'update', 'addRows', 'updateCell', etc has completed
53
-
54
- // *** sort options
55
- headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
56
- ignoreCase : true, // ignore case while sorting
57
- sortForce : null, // column(s) first sorted; always applied
58
- sortList : [], // Initial sort order; applied initially; updated when manually sorted
59
- sortAppend : null, // column(s) sorted last; always applied
60
- sortStable : false, // when sorting two rows with exactly the same content, the original sort order is maintained
61
-
62
- sortInitialOrder : 'asc', // sort direction on first click
63
- sortLocaleCompare: false, // replace equivalent character (accented characters)
64
- sortReset : false, // third click on the header will reset column to default - unsorted
65
- sortRestart : false, // restart sort to 'sortInitialOrder' when clicking on previously unsorted columns
66
-
67
- emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero, emptyMax, emptyMin
68
- stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero
69
- textExtraction : 'basic', // text extraction method/function - function(node, table, cellIndex){}
70
- textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in default textExtraction function)
71
- textSorter : null, // choose overall or specific column sorter function(a, b, direction, table, columnIndex) [alt: ts.sortText]
72
- numberSorter : null, // choose overall numeric sorter function(a, b, direction, maxColumnValue)
73
-
74
- // *** widget options
75
- widgets: [], // method to add widgets, e.g. widgets: ['zebra']
76
- widgetOptions : {
77
- zebra : [ 'even', 'odd' ] // zebra widget alternating row class names
78
- },
79
- initWidgets : true, // apply widgets on tablesorter initialization
80
- widgetClass : 'widget-{name}', // table class name template to match to include a widget
81
-
82
- // *** callbacks
83
- initialized : null, // function(table){},
84
-
85
- // *** extra css class names
86
- tableClass : '',
87
- cssAsc : '',
88
- cssDesc : '',
89
- cssNone : '',
90
- cssHeader : '',
91
- cssHeaderRow : '',
92
- cssProcessing : '', // processing icon applied to header during sort/filter
93
-
94
- cssChildRow : 'tablesorter-childRow', // class name indiciating that a row is to be attached to the its parent
95
- cssIcon : 'tablesorter-icon', // if this class does not exist, the {icon} will not be added from the headerTemplate
96
- cssIconNone : '', // class name added to the icon when there is no column sort
97
- cssIconAsc : '', // class name added to the icon when the column has an ascending sort
98
- cssIconDesc : '', // class name added to the icon when the column has a descending sort
99
- cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!)
100
- cssNoSort : 'tablesorter-noSort', // class name added to element inside header; clicking on it won't cause a sort
101
- cssIgnoreRow : 'tablesorter-ignoreRow', // header row to ignore; cells within this row will not be added to c.$headers
102
-
103
- // *** events
104
- pointerClick : 'click',
105
- pointerDown : 'mousedown',
106
- pointerUp : 'mouseup',
107
-
108
- // *** selectors
109
- selectorHeaders : '> thead th, > thead td',
110
- selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort
111
- selectorRemove : '.remove-me',
112
-
113
- // *** advanced
114
- debug : false,
115
-
116
- // *** Internal variables
117
- headerList: [],
118
- empties: {},
119
- strings: {},
120
- parsers: []
22
+ var ts = $.tablesorter = {
23
+
24
+ version : '2.24.2',
25
+
26
+ parsers : [],
27
+ widgets : [],
28
+ defaults : {
29
+
30
+ // *** appearance
31
+ theme : 'default', // adds tablesorter-{theme} to the table for styling
32
+ widthFixed : false, // adds colgroup to fix widths of columns
33
+ showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered.
34
+
35
+ headerTemplate : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = <i/> // class from cssIcon
36
+ onRenderTemplate : null, // function( index, template ){ return template; }, // template is a string
37
+ onRenderHeader : null, // function( index ){}, // nothing to return
38
+
39
+ // *** functionality
40
+ cancelSelection : true, // prevent text selection in the header
41
+ tabIndex : true, // add tabindex to header for keyboard accessibility
42
+ dateFormat : 'mmddyyyy', // other options: 'ddmmyyy' or 'yyyymmdd'
43
+ sortMultiSortKey : 'shiftKey', // key used to select additional columns
44
+ sortResetKey : 'ctrlKey', // key used to remove sorting on a column
45
+ usNumberFormat : true, // false for German '1.234.567,89' or French '1 234 567,89'
46
+ delayInit : false, // if false, the parsed table contents will not update until the first sort
47
+ 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.
48
+ resort : true, // default setting to trigger a resort after an 'update', 'addRows', 'updateCell', etc has completed
49
+
50
+ // *** sort options
51
+ headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
52
+ ignoreCase : true, // ignore case while sorting
53
+ sortForce : null, // column(s) first sorted; always applied
54
+ sortList : [], // Initial sort order; applied initially; updated when manually sorted
55
+ sortAppend : null, // column(s) sorted last; always applied
56
+ sortStable : false, // when sorting two rows with exactly the same content, the original sort order is maintained
57
+
58
+ sortInitialOrder : 'asc', // sort direction on first click
59
+ sortLocaleCompare: false, // replace equivalent character (accented characters)
60
+ sortReset : false, // third click on the header will reset column to default - unsorted
61
+ sortRestart : false, // restart sort to 'sortInitialOrder' when clicking on previously unsorted columns
62
+
63
+ emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero, emptyMax, emptyMin
64
+ stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero
65
+ textExtraction : 'basic', // text extraction method/function - function( node, table, cellIndex ){}
66
+ textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in default textExtraction function)
67
+ textSorter : null, // choose overall or specific column sorter function( a, b, direction, table, columnIndex ) [alt: ts.sortText]
68
+ numberSorter : null, // choose overall numeric sorter function( a, b, direction, maxColumnValue )
69
+
70
+ // *** widget options
71
+ widgets: [], // method to add widgets, e.g. widgets: ['zebra']
72
+ widgetOptions : {
73
+ zebra : [ 'even', 'odd' ] // zebra widget alternating row class names
74
+ },
75
+ initWidgets : true, // apply widgets on tablesorter initialization
76
+ widgetClass : 'widget-{name}', // table class name template to match to include a widget
77
+
78
+ // *** callbacks
79
+ initialized : null, // function( table ){},
80
+
81
+ // *** extra css class names
82
+ tableClass : '',
83
+ cssAsc : '',
84
+ cssDesc : '',
85
+ cssNone : '',
86
+ cssHeader : '',
87
+ cssHeaderRow : '',
88
+ cssProcessing : '', // processing icon applied to header during sort/filter
89
+
90
+ cssChildRow : 'tablesorter-childRow', // class name indiciating that a row is to be attached to the its parent
91
+ cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!)
92
+ cssNoSort : 'tablesorter-noSort', // class name added to element inside header; clicking on it won't cause a sort
93
+ cssIgnoreRow : 'tablesorter-ignoreRow', // header row to ignore; cells within this row will not be added to c.$headers
94
+
95
+ cssIcon : 'tablesorter-icon', // if this class does not exist, the {icon} will not be added from the headerTemplate
96
+ cssIconNone : '', // class name added to the icon when there is no column sort
97
+ cssIconAsc : '', // class name added to the icon when the column has an ascending sort
98
+ cssIconDesc : '', // class name added to the icon when the column has a descending sort
99
+
100
+ // *** events
101
+ pointerClick : 'click',
102
+ pointerDown : 'mousedown',
103
+ pointerUp : 'mouseup',
104
+
105
+ // *** selectors
106
+ selectorHeaders : '> thead th, > thead td',
107
+ selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort
108
+ selectorRemove : '.remove-me',
109
+
110
+ // *** advanced
111
+ debug : false,
112
+
113
+ // *** Internal variables
114
+ headerList: [],
115
+ empties: {},
116
+ strings: {},
117
+ parsers: []
118
+
119
+ // removed: widgetZebra: { css: ['even', 'odd'] }
121
120
 
122
- // removed: widgetZebra: { css: ['even', 'odd'] }
121
+ },
123
122
 
124
- };
123
+ // internal css classes - these will ALWAYS be added to
124
+ // the table and MUST only contain one class name - fixes #381
125
+ css : {
126
+ table : 'tablesorter',
127
+ cssHasChild: 'tablesorter-hasChildRow',
128
+ childRow : 'tablesorter-childRow',
129
+ colgroup : 'tablesorter-colgroup',
130
+ header : 'tablesorter-header',
131
+ headerRow : 'tablesorter-headerRow',
132
+ headerIn : 'tablesorter-header-inner',
133
+ icon : 'tablesorter-icon',
134
+ processing : 'tablesorter-processing',
135
+ sortAsc : 'tablesorter-headerAsc',
136
+ sortDesc : 'tablesorter-headerDesc',
137
+ sortNone : 'tablesorter-headerUnSorted'
138
+ },
125
139
 
126
- // internal css classes - these will ALWAYS be added to
127
- // the table and MUST only contain one class name - fixes #381
128
- ts.css = {
129
- table : 'tablesorter',
130
- cssHasChild: 'tablesorter-hasChildRow',
131
- childRow : 'tablesorter-childRow',
132
- colgroup : 'tablesorter-colgroup',
133
- header : 'tablesorter-header',
134
- headerRow : 'tablesorter-headerRow',
135
- headerIn : 'tablesorter-header-inner',
136
- icon : 'tablesorter-icon',
137
- processing : 'tablesorter-processing',
138
- sortAsc : 'tablesorter-headerAsc',
139
- sortDesc : 'tablesorter-headerDesc',
140
- sortNone : 'tablesorter-headerUnSorted'
141
- };
140
+ // labels applied to sortable headers for accessibility (aria) support
141
+ language : {
142
+ sortAsc : 'Ascending sort applied, ',
143
+ sortDesc : 'Descending sort applied, ',
144
+ sortNone : 'No sort applied, ',
145
+ nextAsc : 'activate to apply an ascending sort',
146
+ nextDesc : 'activate to apply a descending sort',
147
+ nextNone : 'activate to remove the sort'
148
+ },
142
149
 
143
- // labels applied to sortable headers for accessibility (aria) support
144
- ts.language = {
145
- sortAsc : 'Ascending sort applied, ',
146
- sortDesc : 'Descending sort applied, ',
147
- sortNone : 'No sort applied, ',
148
- nextAsc : 'activate to apply an ascending sort',
149
- nextDesc : 'activate to apply a descending sort',
150
- nextNone : 'activate to remove the sort'
151
- };
150
+ regex : {
151
+ templateContent : /\{content\}/g,
152
+ templateIcon : /\{icon\}/g,
153
+ templateName : /\{name\}/i,
154
+ spaces : /\s+/g,
155
+ nonWord : /\W/g,
156
+ formElements : /(input|select|button|textarea)/i,
152
157
 
153
- ts.regex = {
154
- templateContent : /\{content\}/g,
155
- templateIcon : /\{icon\}/g,
156
- templateName : /\{name\}/i,
157
- spaces : /\s+/g,
158
- nonWord : /\W/g,
159
- formElements : /(input|select|button|textarea)/i
160
- };
158
+ // *** sort functions ***
159
+ // regex used in natural sort
160
+ // chunk/tokenize numbers & letters
161
+ chunk : /(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi,
162
+ // replace chunks @ ends
163
+ chunks : /(^\\0|\\0$)/,
164
+ hex : /^0x[0-9a-f]+$/i,
165
+
166
+ // *** formatFloat ***
167
+ comma : /,/g,
168
+ digitNonUS : /[\s|\.]/g,
169
+ digitNegativeTest : /^\s*\([.\d]+\)/,
170
+ digitNegativeReplace : /^\s*\(([.\d]+)\)/,
171
+
172
+ // *** isDigit ***
173
+ digitTest : /^[\-+(]?\d+[)]?$/,
174
+ digitReplace : /[,.'"\s]/g
161
175
 
162
- // These methods can be applied on table.config instance
163
- ts.instanceMethods = {};
176
+ },
164
177
 
165
- // $.isEmptyObject from jQuery v1.4
166
- ts.isEmptyObject = function( obj ) {
167
- /*jshint forin: false */
168
- for ( var name in obj ) {
169
- return false;
170
- }
171
- return true;
172
- };
178
+ // digit sort text location; keeping max+/- for backwards compatibility
179
+ string : {
180
+ max : 1,
181
+ min : -1,
182
+ emptymin : 1,
183
+ emptymax : -1,
184
+ zero : 0,
185
+ none : 0,
186
+ 'null' : 0,
187
+ top : true,
188
+ bottom : false
189
+ },
173
190
 
174
- ts.getElementText = function(c, node, cellIndex) {
175
- if (!node) { return ''; }
176
- var te,
177
- t = c.textExtraction || '',
178
- // node could be a jquery object
179
- // http://jsperf.com/jquery-vs-instanceof-jquery/2
180
- $node = node.jquery ? node : $(node);
181
- if (typeof t === 'string') {
182
- // check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow!
183
- // http://www.kellegous.com/j/2013/02/27/innertext-vs-textcontent/
184
- if ( t === 'basic' && typeof ( te = $node.attr(c.textAttribute) ) !== 'undefined' ) {
185
- return $.trim( te );
186
- }
187
- return $.trim( node.textContent || $node.text() );
188
- } else {
189
- if (typeof t === 'function') {
190
- return $.trim( t($node[0], c.table, cellIndex) );
191
- } else if (typeof (te = ts.getColumnData( c.table, t, cellIndex )) === 'function') {
192
- return $.trim( te($node[0], c.table, cellIndex) );
193
- }
194
- }
195
- // fallback
196
- return $.trim( $node[0].textContent || $node.text() );
197
- };
191
+ // These methods can be applied on table.config instance
192
+ instanceMethods : {},
198
193
 
199
- function detectParserForColumn(c, rows, rowIndex, cellIndex) {
200
- var cur, $node,
201
- i = ts.parsers.length,
202
- node = false,
203
- nodeValue = '',
204
- keepLooking = true;
205
- while (nodeValue === '' && keepLooking) {
206
- rowIndex++;
207
- if (rows[rowIndex]) {
208
- node = rows[rowIndex].cells[cellIndex];
209
- nodeValue = ts.getElementText(c, node, cellIndex);
210
- $node = $(node);
211
- if (c.debug) {
212
- console.log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': "' + nodeValue + '"');
213
- }
194
+ /*
195
+ ▄█████ ██████ ██████ ██ ██ █████▄
196
+ ▀█▄ ██▄▄ ██ ██ ██ ██▄▄██
197
+ ▀█▄ ██▀▀ ██ ██ ██ ██▀▀▀
198
+ █████▀ ██████ ██ ▀████▀ ██
199
+ */
200
+
201
+ setup : function( table, c ) {
202
+ // if no thead or tbody, or tablesorter is already present, quit
203
+ if ( !table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true ) {
204
+ if ( c.debug ) {
205
+ if ( table.hasInitialized ) {
206
+ console.warn( 'Stopping initialization. Tablesorter has already been initialized' );
214
207
  } else {
215
- keepLooking = false;
208
+ console.error( 'Stopping initialization! No table, thead or tbody' );
216
209
  }
217
210
  }
218
- while (--i >= 0) {
219
- cur = ts.parsers[i];
220
- // ignore the default text parser because it will always be true
221
- if (cur && cur.id !== 'text' && cur.is && cur.is(nodeValue, c.table, node, $node)) {
222
- return cur;
223
- }
224
- }
225
- // nothing found, return the generic parser (text)
226
- return ts.getParserById('text');
211
+ return;
212
+ }
213
+
214
+ var tmp = '',
215
+ $table = $( table ),
216
+ meta = $.metadata;
217
+ // initialization flag
218
+ table.hasInitialized = false;
219
+ // table is being processed flag
220
+ table.isProcessing = true;
221
+ // make sure to store the config object
222
+ table.config = c;
223
+ // save the settings where they read
224
+ $.data( table, 'tablesorter', c );
225
+ if ( c.debug ) {
226
+ console[ console.group ? 'group' : 'log' ]( 'Initializing tablesorter' );
227
+ $.data( table, 'startoveralltimer', new Date() );
228
+ }
229
+
230
+ // removing this in version 3 (only supports jQuery 1.7+)
231
+ c.supportsDataObject = ( function( version ) {
232
+ version[ 0 ] = parseInt( version[ 0 ], 10 );
233
+ return ( version[ 0 ] > 1 ) || ( version[ 0 ] === 1 && parseInt( version[ 1 ], 10 ) >= 4 );
234
+ })( $.fn.jquery.split( '.' ) );
235
+ // ensure case insensitivity
236
+ c.emptyTo = c.emptyTo.toLowerCase();
237
+ c.stringTo = c.stringTo.toLowerCase();
238
+ c.last = { sortList : [], clickedIndex : -1 };
239
+ // add table theme class only if there isn't already one there
240
+ if ( !/tablesorter\-/.test( $table.attr( 'class' ) ) ) {
241
+ tmp = ( c.theme !== '' ? ' tablesorter-' + c.theme : '' );
242
+ }
243
+ c.table = table;
244
+ c.$table = $table
245
+ .addClass( ts.css.table + ' ' + c.tableClass + tmp )
246
+ .attr( 'role', 'grid' );
247
+ c.$headers = $table.find( c.selectorHeaders );
248
+
249
+ // give the table a unique id, which will be used in namespace binding
250
+ if ( !c.namespace ) {
251
+ c.namespace = '.tablesorter' + Math.random().toString( 16 ).slice( 2 );
252
+ } else {
253
+ // make sure namespace starts with a period & doesn't have weird characters
254
+ c.namespace = '.' + c.namespace.replace( ts.regex.nonWord, '' );
227
255
  }
228
256
 
229
- // centralized function to extract/parse cell contents
230
- ts.getParsedText = function( c, cell, colIndex, txt ) {
231
- if ( typeof txt === 'undefined' ) {
232
- txt = ts.getElementText( c, cell, colIndex );
257
+ c.$table.children().children( 'tr' ).attr( 'role', 'row' );
258
+ c.$tbodies = $table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ).attr({
259
+ 'aria-live' : 'polite',
260
+ 'aria-relevant' : 'all'
261
+ });
262
+ if ( c.$table.children( 'caption' ).length ) {
263
+ tmp = c.$table.children( 'caption' )[ 0 ];
264
+ if ( !tmp.id ) { tmp.id = c.namespace.slice( 1 ) + 'caption'; }
265
+ c.$table.attr( 'aria-labelledby', tmp.id );
266
+ }
267
+ c.widgetInit = {}; // keep a list of initialized widgets
268
+ // change textExtraction via data-attribute
269
+ c.textExtraction = c.$table.attr( 'data-text-extraction' ) || c.textExtraction || 'basic';
270
+ // build headers
271
+ ts.buildHeaders( c );
272
+ // fixate columns if the users supplies the fixedWidth option
273
+ // do this after theme has been applied
274
+ ts.fixColumnWidth( table );
275
+ // add widgets from class name
276
+ ts.addWidgetFromClass( table );
277
+ // add widget options before parsing (e.g. grouping widget has parser settings)
278
+ ts.applyWidgetOptions( table );
279
+ // try to auto detect column type, and store in tables config
280
+ ts.setupParsers( c );
281
+ // start total row count at zero
282
+ c.totalRows = 0;
283
+ // build the cache for the tbody cells
284
+ // delayInit will delay building the cache until the user starts a sort
285
+ if ( !c.delayInit ) { ts.buildCache( c ); }
286
+ // bind all header events and methods
287
+ ts.bindEvents( table, c.$headers, true );
288
+ ts.bindMethods( c );
289
+ // get sort list from jQuery data or metadata
290
+ // in jQuery < 1.4, an error occurs when calling $table.data()
291
+ if ( c.supportsDataObject && typeof $table.data().sortlist !== 'undefined' ) {
292
+ c.sortList = $table.data().sortlist;
293
+ } else if ( meta && ( $table.metadata() && $table.metadata().sortlist ) ) {
294
+ c.sortList = $table.metadata().sortlist;
295
+ }
296
+ // apply widget init code
297
+ ts.applyWidget( table, true );
298
+ // if user has supplied a sort list to constructor
299
+ if ( c.sortList.length > 0 ) {
300
+ ts.sortOn( c, c.sortList, {}, !c.initWidgets );
301
+ } else {
302
+ ts.setHeadersCss( c );
303
+ if ( c.initWidgets ) {
304
+ // apply widget format
305
+ ts.applyWidget( table, false );
233
306
  }
234
- // if no parser, make sure to return the txt
235
- var val = '' + txt,
236
- parser = c.parsers[ colIndex ],
237
- extractor = c.extractors[ colIndex ];
238
- if ( parser ) {
239
- // do extract before parsing, if there is one
240
- if ( extractor && typeof extractor.format === 'function' ) {
241
- txt = extractor.format( txt, c.table, cell, colIndex );
242
- }
243
- // allow parsing if the string is empty, previously parsing would change it to zero,
244
- // in case the parser needs to extract data from the table cell attributes
245
- val = parser.id === 'no-parser' ? '' :
246
- // make sure txt is a string (extractor may have converted it)
247
- parser.format( '' + txt, c.table, cell, colIndex );
248
- if ( c.ignoreCase && typeof val === 'string' ) {
249
- val = val.toLowerCase();
307
+ }
308
+
309
+ // show processesing icon
310
+ if ( c.showProcessing ) {
311
+ $table
312
+ .unbind( 'sortBegin' + c.namespace + ' sortEnd' + c.namespace )
313
+ .bind( 'sortBegin' + c.namespace + ' sortEnd' + c.namespace, function( e ) {
314
+ clearTimeout( c.processTimer );
315
+ ts.isProcessing( table );
316
+ if ( e.type === 'sortBegin' ) {
317
+ c.processTimer = setTimeout( function() {
318
+ ts.isProcessing( table, true );
319
+ }, 500 );
250
320
  }
321
+ });
322
+ }
323
+
324
+ // initialized
325
+ table.hasInitialized = true;
326
+ table.isProcessing = false;
327
+ if ( c.debug ) {
328
+ console.log( 'Overall initialization time: ' + ts.benchmark( $.data( table, 'startoveralltimer' ) ) );
329
+ if ( c.debug && console.groupEnd ) { console.groupEnd(); }
330
+ }
331
+ $table.trigger( 'tablesorter-initialized', table );
332
+ if ( typeof c.initialized === 'function' ) {
333
+ c.initialized( table );
334
+ }
335
+ },
336
+
337
+ bindMethods : function( c ) {
338
+ var $table = c.$table,
339
+ namespace = c.namespace,
340
+ events = ( 'sortReset update updateRows updateAll updateHeaders addRows updateCell updateComplete ' +
341
+ 'sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup ' +
342
+ 'mouseleave ' ).split( ' ' )
343
+ .join( namespace + ' ' );
344
+ // apply easy methods that trigger bound events
345
+ $table
346
+ .unbind( events.replace( ts.regex.spaces, ' ' ) )
347
+ .bind( 'sortReset' + namespace, function( e, callback ) {
348
+ e.stopPropagation();
349
+ // using this.config to ensure functions are getting a non-cached version of the config
350
+ ts.sortReset( this.config, callback );
351
+ })
352
+ .bind( 'updateAll' + namespace, function( e, resort, callback ) {
353
+ e.stopPropagation();
354
+ ts.updateAll( this.config, resort, callback );
355
+ })
356
+ .bind( 'update' + namespace + ' updateRows' + namespace, function( e, resort, callback ) {
357
+ e.stopPropagation();
358
+ ts.update( this.config, resort, callback );
359
+ })
360
+ .bind( 'updateHeaders' + namespace, function( e, callback ) {
361
+ e.stopPropagation();
362
+ ts.updateHeaders( this.config, callback );
363
+ })
364
+ .bind( 'updateCell' + namespace, function( e, cell, resort, callback ) {
365
+ e.stopPropagation();
366
+ ts.updateCell( this.config, cell, resort, callback );
367
+ })
368
+ .bind( 'addRows' + namespace, function( e, $row, resort, callback ) {
369
+ e.stopPropagation();
370
+ ts.addRows( this.config, $row, resort, callback );
371
+ })
372
+ .bind( 'updateComplete' + namespace, function() {
373
+ this.isUpdating = false;
374
+ })
375
+ .bind( 'sorton' + namespace, function( e, list, callback, init ) {
376
+ e.stopPropagation();
377
+ ts.sortOn( this.config, list, callback, init );
378
+ })
379
+ .bind( 'appendCache' + namespace, function( e, callback, init ) {
380
+ e.stopPropagation();
381
+ ts.appendCache( this.config, init );
382
+ if ( $.isFunction( callback ) ) {
383
+ callback( this );
384
+ }
385
+ })
386
+ // $tbodies variable is used by the tbody sorting widget
387
+ .bind( 'updateCache' + namespace, function( e, callback, $tbodies ) {
388
+ e.stopPropagation();
389
+ ts.updateCache( this.config, callback, $tbodies );
390
+ })
391
+ .bind( 'applyWidgetId' + namespace, function( e, id ) {
392
+ e.stopPropagation();
393
+ ts.getWidgetById( id ).format( this, this.config, this.config.widgetOptions );
394
+ })
395
+ .bind( 'applyWidgets' + namespace, function( e, init ) {
396
+ e.stopPropagation();
397
+ // apply widgets
398
+ ts.applyWidget( this, init );
399
+ })
400
+ .bind( 'refreshWidgets' + namespace, function( e, all, dontapply ) {
401
+ e.stopPropagation();
402
+ ts.refreshWidgets( this, all, dontapply );
403
+ })
404
+ .bind( 'destroy' + namespace, function( e, removeClasses, callback ) {
405
+ e.stopPropagation();
406
+ ts.destroy( this, removeClasses, callback );
407
+ })
408
+ .bind( 'resetToLoadState' + namespace, function( e ) {
409
+ e.stopPropagation();
410
+ // remove all widgets
411
+ ts.removeWidget( this, true, false );
412
+ // restore original settings; this clears out current settings, but does not clear
413
+ // values saved to storage.
414
+ c = $.extend( true, ts.defaults, c.originalSettings );
415
+ this.hasInitialized = false;
416
+ // setup the entire table again
417
+ ts.setup( this, c );
418
+ });
419
+ },
420
+
421
+ bindEvents : function( table, $headers, core ) {
422
+ table = $( table )[ 0 ];
423
+ var tmp,
424
+ c = table.config,
425
+ namespace = c.namespace,
426
+ downTarget = null;
427
+ if ( core !== true ) {
428
+ $headers.addClass( namespace.slice( 1 ) + '_extra_headers' );
429
+ tmp = $.fn.closest ? $headers.closest( 'table' )[ 0 ] : $headers.parents( 'table' )[ 0 ];
430
+ if ( tmp && tmp.nodeName === 'TABLE' && tmp !== table ) {
431
+ $( tmp ).addClass( namespace.slice( 1 ) + '_extra_table' );
251
432
  }
252
- return val;
253
- };
433
+ }
434
+ tmp = ( c.pointerDown + ' ' + c.pointerUp + ' ' + c.pointerClick + ' sort keyup ' )
435
+ .replace( ts.regex.spaces, ' ' )
436
+ .split( ' ' )
437
+ .join( namespace + ' ' );
438
+ // apply event handling to headers and/or additional headers (stickyheaders, scroller, etc)
439
+ $headers
440
+ // http://stackoverflow.com/questions/5312849/jquery-find-self;
441
+ .find( c.selectorSort )
442
+ .add( $headers.filter( c.selectorSort ) )
443
+ .unbind( tmp )
444
+ .bind( tmp, function( e, external ) {
445
+ var $cell, cell, temp,
446
+ $target = $( e.target ),
447
+ // wrap event type in spaces, so the match doesn't trigger on inner words
448
+ type = ' ' + e.type + ' ';
449
+ // only recognize left clicks
450
+ if ( ( ( e.which || e.button ) !== 1 && !type.match( ' ' + c.pointerClick + ' | sort | keyup ' ) ) ||
451
+ // allow pressing enter
452
+ ( type === ' keyup ' && e.which !== 13 ) ||
453
+ // allow triggering a click event (e.which is undefined) & ignore physical clicks
454
+ ( type.match( ' ' + c.pointerClick + ' ' ) && typeof e.which !== 'undefined' ) ) {
455
+ return;
456
+ }
457
+ // ignore mouseup if mousedown wasn't on the same target
458
+ if ( type.match( ' ' + c.pointerUp + ' ' ) && downTarget !== e.target && external !== true ) {
459
+ return;
460
+ }
461
+ // set target on mousedown
462
+ if ( type.match( ' ' + c.pointerDown + ' ' ) ) {
463
+ downTarget = e.target;
464
+ // preventDefault needed or jQuery v1.3.2 and older throws an
465
+ // "Uncaught TypeError: handler.apply is not a function" error
466
+ temp = $target.jquery.split( '.' );
467
+ if ( temp[ 0 ] === '1' && temp[ 1 ] < 4 ) { e.preventDefault(); }
468
+ return;
469
+ }
470
+ downTarget = null;
471
+ // prevent sort being triggered on form elements
472
+ if ( ts.regex.formElements.test( e.target.nodeName ) ||
473
+ // nosort class name, or elements within a nosort container
474
+ $target.hasClass( c.cssNoSort ) || $target.parents( '.' + c.cssNoSort ).length > 0 ||
475
+ // elements within a button
476
+ $target.parents( 'button' ).length > 0 ) {
477
+ return !c.cancelSelection;
478
+ }
479
+ if ( c.delayInit && ts.isEmptyObject( c.cache ) ) {
480
+ ts.buildCache( c );
481
+ }
482
+ // jQuery v1.2.6 doesn't have closest()
483
+ $cell = $.fn.closest ? $( this ).closest( 'th, td' ) :
484
+ /TH|TD/.test( this.nodeName ) ? $( this ) : $( this ).parents( 'th, td' );
485
+ // reference original table headers and find the same cell
486
+ // don't use $headers or IE8 throws an error - see #987
487
+ temp = $headers.index( $cell );
488
+ c.last.clickedIndex = ( temp < 0 ) ? $cell.attr( 'data-column' ) : temp;
489
+ // use column index if $headers is undefined
490
+ cell = c.$headers[ c.last.clickedIndex ];
491
+ if ( cell && !cell.sortDisabled ) {
492
+ ts.initSort( c, cell, e );
493
+ }
494
+ });
495
+ if ( c.cancelSelection ) {
496
+ // cancel selection
497
+ $headers
498
+ .attr( 'unselectable', 'on' )
499
+ .bind( 'selectstart', false )
500
+ .css({
501
+ 'user-select' : 'none',
502
+ 'MozUserSelect' : 'none' // not needed for jQuery 1.8+
503
+ });
504
+ }
505
+ },
254
506
 
255
- function buildParserCache( c, $tbodies ) {
256
- var rows, list, l, i, h, ch, np, p, e, time, tb, len,
257
- table = c.table,
258
- j = 0,
259
- debug = {};
260
- // update table bodies in case we start with an empty table
261
- c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')');
262
- tb = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies;
263
- len = tb.length;
264
- if ( len === 0) {
265
- return c.debug ? console.warn('Warning: *Empty table!* Not building a parser cache') : '';
266
- } else if (c.debug) {
267
- time = new Date();
268
- console[ console.group ? 'group' : 'log' ]('Detecting parsers for each column');
269
- }
270
- list = {
271
- extractors: [],
272
- parsers: []
507
+ buildHeaders : function( c ) {
508
+ var $temp, icon, timer, indx;
509
+ c.headerList = [];
510
+ c.headerContent = [];
511
+ c.sortVars = [];
512
+ if ( c.debug ) {
513
+ timer = new Date();
514
+ }
515
+ // children tr in tfoot - see issue #196 & #547
516
+ c.columns = ts.computeColumnIndex( c.$table.children( 'thead, tfoot' ).children( 'tr' ) );
517
+ // add icon if cssIcon option exists
518
+ icon = c.cssIcon ?
519
+ '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' :
520
+ '';
521
+ // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683
522
+ c.$headers = $( $.map( c.$table.find( c.selectorHeaders ), function( elem, index ) {
523
+ var configHeaders, header, column, template, tmp,
524
+ $elem = $( elem );
525
+ // ignore cell (don't add it to c.$headers) if row has ignoreRow class
526
+ if ( $elem.parent().hasClass( c.cssIgnoreRow ) ) { return; }
527
+ // make sure to get header cell & not column indexed cell
528
+ configHeaders = ts.getColumnData( c.table, c.headers, index, true );
529
+ // save original header content
530
+ c.headerContent[ index ] = $elem.html();
531
+ // if headerTemplate is empty, don't reformat the header cell
532
+ if ( c.headerTemplate !== '' && !$elem.find( '.' + ts.css.headerIn ).length ) {
533
+ // set up header template
534
+ template = c.headerTemplate
535
+ .replace( ts.regex.templateContent, $elem.html() )
536
+ .replace( ts.regex.templateIcon, $elem.find( '.' + ts.css.icon ).length ? '' : icon );
537
+ if ( c.onRenderTemplate ) {
538
+ header = c.onRenderTemplate.apply( $elem, [ index, template ] );
539
+ // only change t if something is returned
540
+ if ( header && typeof header === 'string' ) {
541
+ template = header;
542
+ }
543
+ }
544
+ $elem.html( '<div class="' + ts.css.headerIn + '">' + template + '</div>' ); // faster than wrapInner
545
+ }
546
+ if ( c.onRenderHeader ) {
547
+ c.onRenderHeader.apply( $elem, [ index, c, c.$table ] );
548
+ }
549
+ column = parseInt( $elem.attr( 'data-column' ), 10 );
550
+ elem.column = column;
551
+ tmp = ts.getData( $elem, configHeaders, 'sortInitialOrder' ) || c.sortInitialOrder;
552
+ // this may get updated numerous times if there are multiple rows
553
+ c.sortVars[ column ] = {
554
+ count : -1, // set to -1 because clicking on the header automatically adds one
555
+ order: ts.formatSortingOrder( tmp ) ?
556
+ [ 1, 0, 2 ] : // desc, asc, unsorted
557
+ [ 0, 1, 2 ], // asc, desc, unsorted
558
+ lockedOrder : false
273
559
  };
274
- while (j < len) {
275
- rows = tb[j].rows;
276
- if (rows.length) {
277
- l = c.columns; // rows[j].cells.length;
278
- for (i = 0; i < l; i++) {
279
- h = c.$headerIndexed[i];
560
+ tmp = ts.getData( $elem, configHeaders, 'lockedOrder' ) || false;
561
+ if ( typeof tmp !== 'undefined' && tmp !== false ) {
562
+ c.sortVars[ column ].lockedOrder = true;
563
+ c.sortVars[ column ].order = ts.formatSortingOrder( tmp ) ? [ 1, 1, 1 ] : [ 0, 0, 0 ];
564
+ }
565
+ // add cell to headerList
566
+ c.headerList[ index ] = elem;
567
+ // add to parent in case there are multiple rows
568
+ $elem
569
+ .addClass( ts.css.header + ' ' + c.cssHeader )
570
+ .parent()
571
+ .addClass( ts.css.headerRow + ' ' + c.cssHeaderRow )
572
+ .attr( 'role', 'row' );
573
+ // allow keyboard cursor to focus on element
574
+ if ( c.tabIndex ) {
575
+ $elem.attr( 'tabindex', 0 );
576
+ }
577
+ return elem;
578
+ }) );
579
+ // cache headers per column
580
+ c.$headerIndexed = [];
581
+ for ( indx = 0; indx < c.columns; indx++ ) {
582
+ // colspan in header making a column undefined
583
+ if ( ts.isEmptyObject( c.sortVars[ indx ] ) ) {
584
+ c.sortVars[ indx ] = {};
585
+ }
586
+ $temp = c.$headers.filter( '[data-column="' + indx + '"]' );
587
+ // target sortable column cells, unless there are none, then use non-sortable cells
588
+ // .last() added in jQuery 1.4; use .filter(':last') to maintain compatibility with jQuery v1.2.6
589
+ c.$headerIndexed[ indx ] = $temp.length ?
590
+ $temp.not( '.sorter-false' ).length ?
591
+ $temp.not( '.sorter-false' ).filter( ':last' ) :
592
+ $temp.filter( ':last' ) :
593
+ $();
594
+ }
595
+ c.$table.find( c.selectorHeaders ).attr({
596
+ scope: 'col',
597
+ role : 'columnheader'
598
+ });
599
+ // enable/disable sorting
600
+ ts.updateHeader( c );
601
+ if ( c.debug ) {
602
+ console.log( 'Built headers:' + ts.benchmark( timer ) );
603
+ console.log( c.$headers );
604
+ }
605
+ },
606
+
607
+ // Use it to add a set of methods to table.config which will be available for all tables.
608
+ // This should be done before table initialization
609
+ addInstanceMethods : function( methods ) {
610
+ $.extend( ts.instanceMethods, methods );
611
+ },
612
+
613
+ /*
614
+ █████▄ ▄████▄ █████▄ ▄█████ ██████ █████▄ ▄█████
615
+ ██▄▄██ ██▄▄██ ██▄▄██ ▀█▄ ██▄▄ ██▄▄██ ▀█▄
616
+ ██▀▀▀ ██▀▀██ ██▀██ ▀█▄ ██▀▀ ██▀██ ▀█▄
617
+ ██ ██ ██ ██ ██ █████▀ ██████ ██ ██ █████▀
618
+ */
619
+ setupParsers : function( c, $tbodies ) {
620
+ var rows, list, span, max, colIndex, indx, header, configHeaders,
621
+ noParser, parser, extractor, time, tbody, len,
622
+ table = c.table,
623
+ tbodyIndex = 0,
624
+ debug = {};
625
+ // update table bodies in case we start with an empty table
626
+ c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' );
627
+ tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies;
628
+ len = tbody.length;
629
+ if ( len === 0 ) {
630
+ return c.debug ? console.warn( 'Warning: *Empty table!* Not building a parser cache' ) : '';
631
+ } else if ( c.debug ) {
632
+ time = new Date();
633
+ console[ console.group ? 'group' : 'log' ]( 'Detecting parsers for each column' );
634
+ }
635
+ list = {
636
+ extractors: [],
637
+ parsers: []
638
+ };
639
+ while ( tbodyIndex < len ) {
640
+ rows = tbody[ tbodyIndex ].rows;
641
+ if ( rows.length ) {
642
+ colIndex = 0;
643
+ max = c.columns;
644
+ for ( indx = 0; indx < max; indx++ ) {
645
+ header = c.$headerIndexed[ colIndex ];
646
+ if ( header && header.length ) {
280
647
  // get column indexed table cell
281
- ch = ts.getColumnData( table, c.headers, i );
648
+ configHeaders = ts.getColumnData( table, c.headers, colIndex );
282
649
  // get column parser/extractor
283
- e = ts.getParserById( ts.getData(h, ch, 'extractor') );
284
- p = ts.getParserById( ts.getData(h, ch, 'sorter') );
285
- np = ts.getData(h, ch, 'parser') === 'false';
650
+ extractor = ts.getParserById( ts.getData( header, configHeaders, 'extractor' ) );
651
+ parser = ts.getParserById( ts.getData( header, configHeaders, 'sorter' ) );
652
+ noParser = ts.getData( header, configHeaders, 'parser' ) === 'false';
286
653
  // empty cells behaviour - keeping emptyToBottom for backwards compatibility
287
- c.empties[i] = ( ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase();
654
+ c.empties[colIndex] = (
655
+ ts.getData( header, configHeaders, 'empty' ) ||
656
+ c.emptyTo || ( c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase();
288
657
  // text strings behaviour in numerical sorts
289
- c.strings[i] = ( ts.getData(h, ch, 'string') || c.stringTo || 'max' ).toLowerCase();
290
- if (np) {
291
- p = ts.getParserById('no-parser');
658
+ c.strings[colIndex] = (
659
+ ts.getData( header, configHeaders, 'string' ) ||
660
+ c.stringTo ||
661
+ 'max' ).toLowerCase();
662
+ if ( noParser ) {
663
+ parser = ts.getParserById( 'no-parser' );
292
664
  }
293
- if (!e) {
665
+ if ( !extractor ) {
294
666
  // For now, maybe detect someday
295
- e = false;
667
+ extractor = false;
296
668
  }
297
- if (!p) {
298
- p = detectParserForColumn(c, rows, -1, i);
669
+ if ( !parser ) {
670
+ parser = ts.detectParserForColumn( c, rows, -1, colIndex );
299
671
  }
300
- if (c.debug) {
301
- debug[ '(' + i + ') ' + h.text() ] = {
302
- parser : p.id,
303
- extractor : e ? e.id : 'none',
304
- string : c.strings[i],
305
- empty : c.empties[i]
672
+ if ( c.debug ) {
673
+ debug[ '(' + colIndex + ') ' + header.text() ] = {
674
+ parser : parser.id,
675
+ extractor : extractor ? extractor.id : 'none',
676
+ string : c.strings[ colIndex ],
677
+ empty : c.empties[ colIndex ]
306
678
  };
307
679
  }
308
- list.parsers[i] = p;
309
- list.extractors[i] = e;
310
- }
311
- }
312
- j += (list.parsers.length) ? len : 1;
313
- }
314
- if ( c.debug ) {
315
- if ( !ts.isEmptyObject( debug ) ) {
316
- console[ console.table ? 'table' : 'log' ]( debug );
317
- } else {
318
- console.warn( ' No parsers detected!' );
319
- }
320
- console.log( 'Completed detecting parsers' + ts.benchmark( time ) );
321
- if ( console.groupEnd ) { console.groupEnd(); }
322
- }
323
- c.parsers = list.parsers;
324
- c.extractors = list.extractors;
325
- }
326
-
327
- /* utils */
328
- function buildCache(table, callback, $tbodies) {
329
- var cc, t, v, i, j, k, $tb, $row, cols, cacheTime,
330
- totalRows, rowData, prevRowData, colMax,
331
- c = table.config,
332
- parsers = c.parsers;
333
- // update tbody variable
334
- c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')');
335
- $tb = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies,
336
- c.cache = {};
337
- c.totalRows = 0;
338
- // if no parsers found, return - it's an empty table.
339
- if (!parsers) {
340
- return c.debug ? console.warn('Warning: *Empty table!* Not building a cache') : '';
341
- }
342
- if (c.debug) {
343
- cacheTime = new Date();
344
- }
345
- // processing icon
346
- if (c.showProcessing) {
347
- ts.isProcessing(table, true);
348
- }
349
- for (k = 0; k < $tb.length; k++) {
350
- colMax = []; // column max value per tbody
351
- cc = c.cache[k] = {
352
- normalized: [] // array of normalized row data; last entry contains 'rowData' above
353
- // colMax: # // added at the end
354
- };
355
-
356
- totalRows = ($tb[k] && $tb[k].rows.length) || 0;
357
- for (i = 0; i < totalRows; ++i) {
358
- rowData = {
359
- // order: original row order #
360
- // $row : jQuery Object[]
361
- child: [], // child row text (filter widget)
362
- raw: [] // original row text
363
- };
364
- /** Add the table data to main data array */
365
- $row = $( $tb[ k ].rows[ i ] );
366
- cols = [];
367
- // if this is a child row, add it to the last row's children and continue to the next row
368
- // ignore child row class, if it is the first row
369
- if ( $row.hasClass( c.cssChildRow ) && i !== 0 ) {
370
- t = cc.normalized.length - 1;
371
- prevRowData = cc.normalized[ t ][ c.columns ];
372
- prevRowData.$row = prevRowData.$row.add( $row );
373
- // add 'hasChild' class name to parent row
374
- if ( !$row.prev().hasClass( c.cssChildRow ) ) {
375
- $row.prev().addClass( ts.css.cssHasChild );
376
- }
377
- // save child row content (un-parsed!)
378
- v = $row.children( 'th, td' );
379
- t = prevRowData.child.length;
380
- prevRowData.child[ t ] = [];
381
- // child row content does not account for colspans/rowspans; so indexing may be off
382
- for ( j = 0; j < c.columns; j++ ) {
383
- prevRowData.child[ t ][ j ] = ts.getParsedText( c, v[ j ], j );
384
- }
385
- // go to the next for loop
386
- continue;
387
- }
388
- rowData.$row = $row;
389
- rowData.order = i; // add original row position to rowCache
390
- for ( j = 0; j < c.columns; ++j ) {
391
- if (typeof parsers[ j ] === 'undefined') {
392
- if ( c.debug ) {
393
- console.warn( 'No parser found for cell:', $row[ 0 ].cells[ j ], 'does it have a header?' );
394
- }
395
- continue;
396
- }
397
- t = ts.getElementText( c, $row[ 0 ].cells[j], j );
398
- rowData.raw.push( t ); // save original row text
399
- v = ts.getParsedText( c, $row[ 0 ].cells[ j ], j, t );
400
- cols.push( v );
401
- if ( ( parsers[ j ].type || '' ).toLowerCase() === 'numeric' ) {
402
- // determine column max value (ignore sign)
403
- colMax[ j ] = Math.max( Math.abs( v ) || 0, colMax[ j ] || 0 );
680
+ list.parsers[ colIndex ] = parser;
681
+ list.extractors[ colIndex ] = extractor;
682
+ span = header[ 0 ].colSpan - 1;
683
+ if ( span > 0 ) {
684
+ colIndex += span;
685
+ max += span;
404
686
  }
405
687
  }
406
- // ensure rowData is always in the same location (after the last column)
407
- cols[ c.columns ] = rowData;
408
- cc.normalized.push( cols );
688
+ colIndex++;
409
689
  }
410
- cc.colMax = colMax;
411
- // total up rows, not including child rows
412
- c.totalRows += cc.normalized.length;
413
-
414
690
  }
415
- if ( c.showProcessing ) {
416
- ts.isProcessing( table ); // remove processing icon
417
- }
418
- if ( c.debug ) {
419
- console.log( 'Building cache for ' + totalRows + ' rows' + ts.benchmark( cacheTime ) );
420
- }
421
- if ( $.isFunction( callback ) ) {
422
- callback( table );
691
+ tbodyIndex += ( list.parsers.length ) ? len : 1;
692
+ }
693
+ if ( c.debug ) {
694
+ if ( !ts.isEmptyObject( debug ) ) {
695
+ console[ console.table ? 'table' : 'log' ]( debug );
696
+ } else {
697
+ console.warn( ' No parsers detected!' );
423
698
  }
699
+ console.log( 'Completed detecting parsers' + ts.benchmark( time ) );
700
+ if ( console.groupEnd ) { console.groupEnd(); }
424
701
  }
702
+ c.parsers = list.parsers;
703
+ c.extractors = list.extractors;
704
+ },
425
705
 
426
- function formatSortingOrder(v) {
427
- // look for 'd' in 'desc' order; return true
428
- return (/^d/i.test(v) || v === 1);
429
- }
430
-
431
- function buildHeaders( c ) {
432
- var ch, $t, h, i, t, lock, time, indx;
433
- c.headerList = [];
434
- c.headerContent = [];
435
- if (c.debug) {
436
- time = new Date();
437
- }
438
- // children tr in tfoot - see issue #196 & #547
439
- c.columns = ts.computeColumnIndex( c.$table.children('thead, tfoot').children('tr') );
440
- // add icon if cssIcon option exists
441
- i = c.cssIcon ? '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' : '';
442
- // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683
443
- c.$headers = $( $.map( c.$table.find(c.selectorHeaders), function(elem, index) {
444
- $t = $(elem);
445
- // ignore cell (don't add it to c.$headers) if row has ignoreRow class
446
- if ($t.parent().hasClass(c.cssIgnoreRow)) { return; }
447
- // make sure to get header cell & not column indexed cell
448
- ch = ts.getColumnData( c.table, c.headers, index, true );
449
- // save original header content
450
- c.headerContent[index] = $t.html();
451
- // if headerTemplate is empty, don't reformat the header cell
452
- if ( c.headerTemplate !== '' && !$t.find('.' + ts.css.headerIn).length ) {
453
- // set up header template
454
- t = c.headerTemplate
455
- .replace(ts.regex.templateContent, $t.html())
456
- .replace(ts.regex.templateIcon, $t.find('.' + ts.css.icon).length ? '' : i);
457
- if (c.onRenderTemplate) {
458
- h = c.onRenderTemplate.apply( $t, [ index, t ] );
459
- if (h && typeof h === 'string') { t = h; } // only change t if something is returned
460
- }
461
- $t.html('<div class="' + ts.css.headerIn + '">' + t + '</div>'); // faster than wrapInner
462
- }
463
- if (c.onRenderHeader) { c.onRenderHeader.apply( $t, [ index, c, c.$table ] ); }
464
- // *** remove this.column value if no conflicts found
465
- elem.column = parseInt( $t.attr('data-column'), 10);
466
- elem.order = formatSortingOrder( ts.getData( $t, ch, 'sortInitialOrder' ) || c.sortInitialOrder ) ?
467
- [ 1, 0, 2 ] : // desc, asc, unsorted
468
- [ 0, 1, 2 ]; // asc, desc, unsorted
469
- elem.count = -1; // set to -1 because clicking on the header automatically adds one
470
- elem.lockedOrder = false;
471
- lock = ts.getData($t, ch, 'lockedOrder') || false;
472
- if (typeof lock !== 'undefined' && lock !== false) {
473
- elem.order = elem.lockedOrder = formatSortingOrder(lock) ? [ 1, 1, 1 ] : [ 0, 0, 0 ];
474
- }
475
- $t.addClass(ts.css.header + ' ' + c.cssHeader);
476
- // add cell to headerList
477
- c.headerList[index] = elem;
478
- // add to parent in case there are multiple rows
479
- $t.parent().addClass(ts.css.headerRow + ' ' + c.cssHeaderRow).attr('role', 'row');
480
- // allow keyboard cursor to focus on element
481
- if (c.tabIndex) { $t.attr('tabindex', 0); }
482
- return elem;
483
- }));
484
- // cache headers per column
485
- c.$headerIndexed = [];
486
- for (indx = 0; indx < c.columns; indx++) {
487
- $t = c.$headers.filter('[data-column="' + indx + '"]');
488
- // target sortable column cells, unless there are none, then use non-sortable cells
489
- // .last() added in jQuery 1.4; use .filter(':last') to maintain compatibility with jQuery v1.2.6
490
- c.$headerIndexed[indx] = $t.not('.sorter-false').length ? $t.not('.sorter-false').filter(':last') : $t.filter(':last');
491
- }
492
- c.$table.find(c.selectorHeaders).attr({
493
- scope: 'col',
494
- role : 'columnheader'
495
- });
496
- // enable/disable sorting
497
- updateHeader(c.table);
498
- if (c.debug) {
499
- console.log( 'Built headers:' + ts.benchmark( time ) );
500
- console.log( c.$headers );
706
+ addParser : function( parser ) {
707
+ var indx,
708
+ len = ts.parsers.length,
709
+ add = true;
710
+ for ( indx = 0; indx < len; indx++ ) {
711
+ if ( ts.parsers[ indx ].id.toLowerCase() === parser.id.toLowerCase() ) {
712
+ add = false;
501
713
  }
502
714
  }
503
-
504
- function commonUpdate(table, resort, callback) {
505
- var c = table.config;
506
- // remove rows/elements before update
507
- c.$table.find(c.selectorRemove).remove();
508
- // rebuild parsers
509
- buildParserCache(c);
510
- // rebuild the cache map
511
- buildCache(table);
512
- checkResort(c, resort, callback);
715
+ if ( add ) {
716
+ ts.parsers.push( parser );
513
717
  }
718
+ },
514
719
 
515
- function updateHeader(table) {
516
- var index, isDisabled, $th, col,
517
- c = table.config,
518
- len = c.$headers.length;
519
- for ( index = 0; index < len; index++ ) {
520
- $th = c.$headers.eq( index );
521
- col = ts.getColumnData( table, c.headers, index, true );
522
- // add 'sorter-false' class if 'parser-false' is set
523
- isDisabled = ts.getData( $th, col, 'sorter' ) === 'false' || ts.getData( $th, col, 'parser' ) === 'false';
524
- $th[0].sortDisabled = isDisabled;
525
- $th[ isDisabled ? 'addClass' : 'removeClass' ]( 'sorter-false' ).attr( 'aria-disabled', '' + isDisabled );
526
- // disable tab index on disabled cells
527
- if ( c.tabIndex ) {
528
- if ( isDisabled ) {
529
- $th.removeAttr( 'tabindex' );
530
- } else {
531
- $th.attr( 'tabindex', '0' );
532
- }
533
- }
534
- // aria-controls - requires table ID
535
- if (table.id) {
536
- if ( isDisabled ) {
537
- $th.removeAttr('aria-controls');
538
- } else {
539
- $th.attr('aria-controls', table.id);
540
- }
541
- }
720
+ getParserById : function( name ) {
721
+ /*jshint eqeqeq:false */
722
+ if ( name == 'false' ) { return false; }
723
+ var indx,
724
+ len = ts.parsers.length;
725
+ for ( indx = 0; indx < len; indx++ ) {
726
+ if ( ts.parsers[ indx ].id.toLowerCase() === ( name.toString() ).toLowerCase() ) {
727
+ return ts.parsers[ indx ];
542
728
  }
543
729
  }
730
+ return false;
731
+ },
544
732
 
545
- function setHeadersCss(table) {
546
- var f, h, i, j, $headers, $h, nextSort, txt,
547
- c = table.config,
548
- list = c.sortList,
549
- len = list.length,
550
- none = ts.css.sortNone + ' ' + c.cssNone,
551
- css = [ ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc ],
552
- cssIcon = [ c.cssIconAsc, c.cssIconDesc, c.cssIconNone ],
553
- aria = [ 'ascending', 'descending' ],
554
- // find the footer
555
- $t = $(table).find('tfoot tr').children()
556
- .add( $( c.namespace + '_extra_headers' ) )
557
- .removeClass( css.join( ' ' ) );
558
- // remove all header information
559
- c.$headers
560
- .removeClass(css.join(' '))
561
- .addClass(none).attr('aria-sort', 'none')
562
- .find('.' + ts.css.icon)
563
- .removeClass(cssIcon.join(' '))
564
- .addClass(cssIcon[2]);
565
- for (i = 0; i < len; i++) {
566
- // direction = 2 means reset!
567
- if (list[i][1] !== 2) {
568
- // multicolumn sorting updating - see #1005
569
- f = c.lastClickedIndex > 0 ? c.$headers.filter(':gt(' + ( c.lastClickedIndex - 1 ) + ')') : c.$headers;
570
- // choose the :last in case there are nested columns
571
- f = f.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (len === 1 ? ':last' : '') );
572
- if (f.length) {
573
- for (j = 0; j < f.length; j++) {
574
- if (!f[j].sortDisabled) {
575
- f.eq(j)
576
- .removeClass(none)
577
- .addClass(css[list[i][1]])
578
- .attr('aria-sort', aria[list[i][1]])
579
- .find('.' + ts.css.icon)
580
- .removeClass(cssIcon[2])
581
- .addClass(cssIcon[list[i][1]]);
582
- }
583
- }
584
- // add sorted class to footer & extra headers, if they exist
585
- if ($t.length) {
586
- $t.filter('[data-column="' + list[i][0] + '"]').removeClass(none).addClass(css[list[i][1]]);
587
- }
588
- }
733
+ detectParserForColumn : function( c, rows, rowIndex, cellIndex ) {
734
+ var cur, $node,
735
+ indx = ts.parsers.length,
736
+ node = false,
737
+ nodeValue = '',
738
+ keepLooking = true;
739
+ while ( nodeValue === '' && keepLooking ) {
740
+ rowIndex++;
741
+ if ( rows[ rowIndex ] ) {
742
+ node = rows[ rowIndex ].cells[ cellIndex ];
743
+ nodeValue = ts.getElementText( c, node, cellIndex );
744
+ $node = $( node );
745
+ if ( c.debug ) {
746
+ console.log( 'Checking if value was empty on row ' + rowIndex + ', column: ' +
747
+ cellIndex + ': "' + nodeValue + '"' );
589
748
  }
749
+ } else {
750
+ keepLooking = false;
590
751
  }
591
- // add verbose aria labels
592
- len = c.$headers.length;
593
- $headers = c.$headers.not('.sorter-false');
594
- for ( i = 0; i < len; i++ ) {
595
- $h = $headers.eq( i );
596
- if ( $h.length ) {
597
- h = $headers[ i ];
598
- nextSort = h.order[ ( h.count + 1 ) % ( c.sortReset ? 3 : 2 ) ],
599
- txt = $.trim( $h.text() ) + ': ' +
600
- ts.language[ $h.hasClass( ts.css.sortAsc ) ? 'sortAsc' : $h.hasClass( ts.css.sortDesc ) ? 'sortDesc' : 'sortNone' ] +
601
- ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ];
602
- $h.attr( 'aria-label', txt );
603
- }
752
+ }
753
+ while ( --indx >= 0 ) {
754
+ cur = ts.parsers[ indx ];
755
+ // ignore the default text parser because it will always be true
756
+ if ( cur && cur.id !== 'text' && cur.is && cur.is( nodeValue, c.table, node, $node ) ) {
757
+ return cur;
604
758
  }
605
759
  }
760
+ // nothing found, return the generic parser (text)
761
+ return ts.getParserById( 'text' );
762
+ },
606
763
 
607
- function updateHeaderSortCount( table, list ) {
608
- var col, dir, group, header, indx, primary, temp, val,
609
- c = table.config,
610
- sortList = list || c.sortList,
611
- len = sortList.length;
612
- c.sortList = [];
613
- for (indx = 0; indx < len; indx++) {
614
- val = sortList[indx];
615
- // ensure all sortList values are numeric - fixes #127
616
- col = parseInt(val[0], 10);
617
- // prevents error if sorton array is wrong
618
- if ( col < c.columns && c.$headerIndexed[col] ) {
619
- // make sure header exists
620
- header = c.$headerIndexed[col][0];
621
- // o.count = o.count + 1;
622
- dir = ('' + val[1]).match(/^(1|d|s|o|n)/);
623
- dir = dir ? dir[0] : '';
624
- // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext
625
- switch (dir) {
626
- case '1': case 'd': // descending
627
- dir = 1;
628
- break;
629
- case 's': // same direction (as primary column)
630
- // if primary sort is set to 's', make it ascending
631
- dir = primary || 0;
632
- break;
633
- case 'o':
634
- temp = header.order[(primary || 0) % (c.sortReset ? 3 : 2)];
635
- // opposite of primary column; but resets if primary resets
636
- dir = temp === 0 ? 1 : temp === 1 ? 0 : 2;
637
- break;
638
- case 'n':
639
- header.count = header.count + 1;
640
- dir = header.order[(header.count) % (c.sortReset ? 3 : 2)];
641
- break;
642
- default: // ascending
643
- dir = 0;
644
- break;
645
- }
646
- primary = indx === 0 ? dir : primary;
647
- group = [ col, parseInt(dir, 10) || 0 ];
648
- c.sortList.push(group);
649
- dir = $.inArray(group[1], header.order); // fixes issue #167
650
- header.count = dir >= 0 ? dir : group[1] % (c.sortReset ? 3 : 2);
651
- }
764
+ getElementText : function( c, node, cellIndex ) {
765
+ if ( !node ) { return ''; }
766
+ var tmp,
767
+ extract = c.textExtraction || '',
768
+ // node could be a jquery object
769
+ // http://jsperf.com/jquery-vs-instanceof-jquery/2
770
+ $node = node.jquery ? node : $( node );
771
+ if ( typeof extract === 'string' ) {
772
+ // check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow!
773
+ // http://www.kellegous.com/j/2013/02/27/innertext-vs-textcontent/
774
+ if ( extract === 'basic' && typeof ( tmp = $node.attr( c.textAttribute ) ) !== 'undefined' ) {
775
+ return $.trim( tmp );
776
+ }
777
+ return $.trim( node.textContent || $node.text() );
778
+ } else {
779
+ if ( typeof extract === 'function' ) {
780
+ return $.trim( extract( $node[ 0 ], c.table, cellIndex ) );
781
+ } else if ( typeof ( tmp = ts.getColumnData( c.table, extract, cellIndex ) ) === 'function' ) {
782
+ return $.trim( tmp( $node[ 0 ], c.table, cellIndex ) );
652
783
  }
653
784
  }
785
+ // fallback
786
+ return $.trim( $node[ 0 ].textContent || $node.text() );
787
+ },
654
788
 
655
- function getCachedSortType(parsers, i) {
656
- return (parsers && parsers[i]) ? parsers[i].type || '' : '';
657
- }
658
-
659
- function initSort(table, cell, event){
660
- if (table.isUpdating) {
661
- // let any updates complete before initializing a sort
662
- return setTimeout(function(){ initSort(table, cell, event); }, 50);
663
- }
664
- var arry, indx, i, col, order, s, $header,
665
- c = table.config,
666
- key = !event[c.sortMultiSortKey],
667
- $table = c.$table,
668
- len = c.$headers.length;
669
- // Only call sortStart if sorting is enabled
670
- $table.trigger('sortStart', table);
671
- // get current column sort order
672
- cell.count = event[c.sortResetKey] ? 2 : (cell.count + 1) % (c.sortReset ? 3 : 2);
673
- // reset all sorts on non-current column - issue #30
674
- if (c.sortRestart) {
675
- indx = cell;
676
- for ( i = 0; i < len; i++ ) {
677
- $header = c.$headers.eq( i );
678
- // only reset counts on columns that weren't just clicked on and if not included in a multisort
679
- if ( $header[0] !== indx && ( key || !$header.is('.' + ts.css.sortDesc + ',.' + ts.css.sortAsc) ) ) {
680
- $header[0].count = -1;
681
- }
682
- }
789
+ // centralized function to extract/parse cell contents
790
+ getParsedText : function( c, cell, colIndex, txt ) {
791
+ if ( typeof txt === 'undefined' ) {
792
+ txt = ts.getElementText( c, cell, colIndex );
793
+ }
794
+ // if no parser, make sure to return the txt
795
+ var val = '' + txt,
796
+ parser = c.parsers[ colIndex ],
797
+ extractor = c.extractors[ colIndex ];
798
+ if ( parser ) {
799
+ // do extract before parsing, if there is one
800
+ if ( extractor && typeof extractor.format === 'function' ) {
801
+ txt = extractor.format( txt, c.table, cell, colIndex );
802
+ }
803
+ // allow parsing if the string is empty, previously parsing would change it to zero,
804
+ // in case the parser needs to extract data from the table cell attributes
805
+ val = parser.id === 'no-parser' ? '' :
806
+ // make sure txt is a string (extractor may have converted it)
807
+ parser.format( '' + txt, c.table, cell, colIndex );
808
+ if ( c.ignoreCase && typeof val === 'string' ) {
809
+ val = val.toLowerCase();
683
810
  }
684
- // get current column index
685
- indx = parseInt( $(cell).attr('data-column'), 10 );
686
- // user only wants to sort on one column
687
- if (key) {
688
- // flush the sort list
689
- c.sortList = [];
690
- if (c.sortForce !== null) {
691
- arry = c.sortForce;
692
- for (col = 0; col < arry.length; col++) {
693
- if (arry[col][0] !== indx) {
694
- c.sortList.push(arry[col]);
695
- }
811
+ }
812
+ return val;
813
+ },
814
+
815
+ /*
816
+ ▄████▄ ▄████▄ ▄████▄ ██ ██ ██████
817
+ ██ ▀▀ ██▄▄██ ██ ▀▀ ██▄▄██ ██▄▄
818
+ ██ ▄▄ ██▀▀██ ██ ▄▄ ██▀▀██ ██▀▀
819
+ ▀████▀ ██ ██ ▀████▀ ██ ██ ██████
820
+ */
821
+ buildCache : function( c, callback, $tbodies ) {
822
+ var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row,
823
+ cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData,
824
+ colMax, span, cacheIndex, max, len,
825
+ table = c.table,
826
+ parsers = c.parsers;
827
+ // update tbody variable
828
+ c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' );
829
+ $tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies,
830
+ c.cache = {};
831
+ c.totalRows = 0;
832
+ // if no parsers found, return - it's an empty table.
833
+ if ( !parsers ) {
834
+ return c.debug ? console.warn( 'Warning: *Empty table!* Not building a cache' ) : '';
835
+ }
836
+ if ( c.debug ) {
837
+ cacheTime = new Date();
838
+ }
839
+ // processing icon
840
+ if ( c.showProcessing ) {
841
+ ts.isProcessing( table, true );
842
+ }
843
+ for ( tbodyIndex = 0; tbodyIndex < $tbody.length; tbodyIndex++ ) {
844
+ colMax = []; // column max value per tbody
845
+ cache = c.cache[ tbodyIndex ] = {
846
+ normalized: [] // array of normalized row data; last entry contains 'rowData' above
847
+ // colMax: # // added at the end
848
+ };
849
+
850
+ totalRows = ( $tbody[ tbodyIndex ] && $tbody[ tbodyIndex ].rows.length ) || 0;
851
+ for ( rowIndex = 0; rowIndex < totalRows; ++rowIndex ) {
852
+ rowData = {
853
+ // order: original row order #
854
+ // $row : jQuery Object[]
855
+ child: [], // child row text (filter widget)
856
+ raw: [] // original row text
857
+ };
858
+ /** Add the table data to main data array */
859
+ $row = $( $tbody[ tbodyIndex ].rows[ rowIndex ] );
860
+ cols = [];
861
+ // if this is a child row, add it to the last row's children and continue to the next row
862
+ // ignore child row class, if it is the first row
863
+ if ( $row.hasClass( c.cssChildRow ) && rowIndex !== 0 ) {
864
+ len = cache.normalized.length - 1;
865
+ prevRowData = cache.normalized[ len ][ c.columns ];
866
+ prevRowData.$row = prevRowData.$row.add( $row );
867
+ // add 'hasChild' class name to parent row
868
+ if ( !$row.prev().hasClass( c.cssChildRow ) ) {
869
+ $row.prev().addClass( ts.css.cssHasChild );
696
870
  }
697
- }
698
- // add column to sort list
699
- order = cell.order[cell.count];
700
- if (order < 2) {
701
- c.sortList.push([ indx, order ]);
702
- // add other columns if header spans across multiple
703
- if (cell.colSpan > 1) {
704
- for (col = 1; col < cell.colSpan; col++) {
705
- c.sortList.push([ indx + col, order ]);
871
+ // save child row content (un-parsed!)
872
+ $cells = $row.children( 'th, td' );
873
+ len = prevRowData.child.length;
874
+ prevRowData.child[ len ] = [];
875
+ // child row content does not account for colspans/rowspans; so indexing may be off
876
+ cacheIndex = 0;
877
+ max = c.columns;
878
+ for ( colIndex = 0; colIndex < max; colIndex++ ) {
879
+ cell = $cells[ colIndex ];
880
+ if ( cell ) {
881
+ prevRowData.child[ len ][ colIndex ] = ts.getParsedText( c, cell, colIndex );
882
+ span = $cells[ colIndex ].colSpan - 1;
883
+ if ( span > 0 ) {
884
+ cacheIndex += span;
885
+ max += span;
886
+ }
706
887
  }
888
+ cacheIndex++;
707
889
  }
708
- }
709
- // multi column sorting
710
- } else {
711
- // get rid of the sortAppend before adding more - fixes issue #115 & #523
712
- if (c.sortAppend && c.sortList.length > 1) {
713
- for (col = 0; col < c.sortAppend.length; col++) {
714
- s = ts.isValueInArray(c.sortAppend[col][0], c.sortList);
715
- if (s >= 0) {
716
- c.sortList.splice(s, 1);
890
+ // go to the next for loop
891
+ continue;
892
+ }
893
+ rowData.$row = $row;
894
+ rowData.order = rowIndex; // add original row position to rowCache
895
+ cacheIndex = 0;
896
+ max = c.columns;
897
+ for ( colIndex = 0; colIndex < max; ++colIndex ) {
898
+ cell = $row[ 0 ].cells[ colIndex ];
899
+ if ( typeof parsers[ cacheIndex ] === 'undefined' ) {
900
+ if ( c.debug ) {
901
+ console.warn( 'No parser found for column ' + colIndex + '; cell:', cell, 'does it have a header?' );
717
902
  }
718
- }
719
- }
720
- // the user has clicked on an already sorted column
721
- if (ts.isValueInArray(indx, c.sortList) >= 0) {
722
- // reverse the sorting direction
723
- for (col = 0; col < c.sortList.length; col++) {
724
- s = c.sortList[col];
725
- order = c.$headerIndexed[ s[0] ][0];
726
- if (s[0] === indx) {
727
- // order.count seems to be incorrect when compared to cell.count
728
- s[1] = order.order[cell.count];
729
- if (s[1] === 2) {
730
- c.sortList.splice(col, 1);
731
- order.count = -1;
732
- }
903
+ } else if ( cell ) {
904
+ val = ts.getElementText( c, cell, cacheIndex );
905
+ rowData.raw[ cacheIndex ] = val; // save original row text
906
+ txt = ts.getParsedText( c, cell, cacheIndex, val );
907
+ cols[ cacheIndex ] = txt;
908
+ if ( ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) {
909
+ // determine column max value (ignore sign)
910
+ colMax[ cacheIndex ] = Math.max( Math.abs( txt ) || 0, colMax[ cacheIndex ] || 0 );
733
911
  }
734
- }
735
- } else {
736
- // add column to sort list array
737
- order = cell.order[cell.count];
738
- if (order < 2) {
739
- c.sortList.push([ indx, order ]);
740
- // add other columns if header spans across multiple
741
- if (cell.colSpan > 1) {
742
- for (col = 1; col < cell.colSpan; col++) {
743
- c.sortList.push([ indx + col, order ]);
744
- }
912
+ // allow colSpan in tbody
913
+ span = cell.colSpan - 1;
914
+ if ( span > 0 ) {
915
+ cacheIndex += span;
916
+ max += span;
745
917
  }
746
918
  }
919
+ cacheIndex++;
747
920
  }
921
+ // ensure rowData is always in the same location (after the last column)
922
+ cols[ c.columns ] = rowData;
923
+ cache.normalized.push( cols );
748
924
  }
749
- if (c.sortAppend !== null) {
750
- arry = c.sortAppend;
751
- for (col = 0; col < arry.length; col++) {
752
- if (arry[col][0] !== indx) {
753
- c.sortList.push(arry[col]);
925
+ cache.colMax = colMax;
926
+ // total up rows, not including child rows
927
+ c.totalRows += cache.normalized.length;
928
+
929
+ }
930
+ if ( c.showProcessing ) {
931
+ ts.isProcessing( table ); // remove processing icon
932
+ }
933
+ if ( c.debug ) {
934
+ console.log( 'Building cache for ' + totalRows + ' rows' + ts.benchmark( cacheTime ) );
935
+ }
936
+ if ( $.isFunction( callback ) ) {
937
+ callback( table );
938
+ }
939
+ },
940
+
941
+ getColumnText : function( table, column, callback, rowFilter ) {
942
+ table = $( table )[0];
943
+ var tbodyIndex, rowIndex, cache, row, tbodyLen, rowLen, raw, parsed, $cell, result,
944
+ hasCallback = typeof callback === 'function',
945
+ allColumns = column === 'all',
946
+ data = { raw : [], parsed: [], $cell: [] },
947
+ c = table.config;
948
+ if ( ts.isEmptyObject( c ) ) {
949
+ if ( c.debug ) {
950
+ console.warn( 'No cache found - aborting getColumnText function!' );
951
+ }
952
+ } else {
953
+ tbodyLen = c.$tbodies.length;
954
+ for ( tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++ ) {
955
+ cache = c.cache[ tbodyIndex ].normalized;
956
+ rowLen = cache.length;
957
+ for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) {
958
+ row = cache[ rowIndex ];
959
+ if ( rowFilter && !row[ c.columns ].$row.is( rowFilter ) ) {
960
+ continue;
961
+ }
962
+ result = true;
963
+ parsed = ( allColumns ) ? row.slice( 0, c.columns ) : row[ column ];
964
+ row = row[ c.columns ];
965
+ raw = ( allColumns ) ? row.raw : row.raw[ column ];
966
+ $cell = ( allColumns ) ? row.$row.children() : row.$row.children().eq( column );
967
+ if ( hasCallback ) {
968
+ result = callback({
969
+ tbodyIndex : tbodyIndex,
970
+ rowIndex : rowIndex,
971
+ parsed : parsed,
972
+ raw : raw,
973
+ $row : row.$row,
974
+ $cell : $cell
975
+ });
976
+ }
977
+ if ( result !== false ) {
978
+ data.parsed.push( parsed );
979
+ data.raw.push( raw );
980
+ data.$cell.push( $cell );
754
981
  }
755
982
  }
756
983
  }
757
- // sortBegin event triggered immediately before the sort
758
- $table.trigger('sortBegin', table);
759
- // setTimeout needed so the processing icon shows up
760
- setTimeout(function(){
761
- // set css for headers
762
- setHeadersCss(table);
763
- multisort(table);
764
- ts.appendCache( c );
765
- $table.trigger('sortEnd', table);
766
- }, 1);
767
- }
768
-
769
- // sort multiple columns
770
- function multisort(table) { /*jshint loopfunc:true */
771
- var i, k, num, col, sortTime, colMax,
772
- rows, order, sort, x, y,
773
- dir = 0,
774
- c = table.config,
775
- cts = c.textSorter || '',
776
- sortList = c.sortList,
777
- l = sortList.length,
778
- bl = c.$tbodies.length;
779
- if (c.serverSideSorting || ts.isEmptyObject(c.cache)) { // empty table - fixes #206/#346
780
- return;
781
- }
782
- if (c.debug) { sortTime = new Date(); }
783
- for (k = 0; k < bl; k++) {
784
- colMax = c.cache[k].colMax;
785
- rows = c.cache[k].normalized;
786
-
787
- rows.sort(function(a, b) {
788
- // rows is undefined here in IE, so don't use it!
789
- for (i = 0; i < l; i++) {
790
- col = sortList[i][0];
791
- order = sortList[i][1];
792
- // sort direction, true = asc, false = desc
793
- dir = order === 0;
794
-
795
- if (c.sortStable && a[col] === b[col] && l === 1) {
796
- return a[c.columns].order - b[c.columns].order;
797
- }
984
+ // return everything
985
+ return data;
986
+ }
987
+ },
798
988
 
799
- // fallback to natural sort since it is more robust
800
- num = /n/i.test(getCachedSortType(c.parsers, col));
801
- if (num && c.strings[col]) {
802
- // sort strings in numerical columns
803
- if (typeof (c.string[c.strings[col]]) === 'boolean') {
804
- num = (dir ? 1 : -1) * (c.string[c.strings[col]] ? -1 : 1);
805
- } else {
806
- num = (c.strings[col]) ? c.string[c.strings[col]] || 0 : 0;
807
- }
808
- // fall back to built-in numeric sort
809
- // var sort = $.tablesorter['sort' + s]( a[c], b[c], dir, colMax[c], table);
810
- sort = c.numberSorter ? c.numberSorter(a[col], b[col], dir, colMax[col], table) :
811
- ts[ 'sortNumeric' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], num, colMax[col], col, table);
812
- } else {
813
- // set a & b depending on sort direction
814
- x = dir ? a : b;
815
- y = dir ? b : a;
816
- // text sort function
817
- if (typeof cts === 'function') {
818
- // custom OVERALL text sorter
819
- sort = cts(x[col], y[col], dir, col, table);
820
- } else if (typeof cts === 'object' && cts.hasOwnProperty(col)) {
821
- // custom text sorter for a SPECIFIC COLUMN
822
- sort = cts[col](x[col], y[col], dir, col, table);
823
- } else {
824
- // fall back to natural sort
825
- sort = ts[ 'sortNatural' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], col, table, c);
826
- }
827
- }
828
- if (sort) { return sort; }
989
+ /*
990
+ ██ ██ █████▄ █████▄ ▄████▄ ██████ ██████
991
+ ██ ██ ██▄▄██ ██ ██ ██▄▄██ ██ ██▄▄
992
+ ██ ██ ██▀▀▀ ██ ██ ██▀▀██ ██ ██▀▀
993
+ ▀████▀ ██ █████▀ ██ ██ ██ ██████
994
+ */
995
+ setHeadersCss : function( c ) {
996
+ var $sorted, header, indx, column, $header, nextSort, txt, tmp,
997
+ list = c.sortList,
998
+ len = list.length,
999
+ none = ts.css.sortNone + ' ' + c.cssNone,
1000
+ css = [ ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc ],
1001
+ cssIcon = [ c.cssIconAsc, c.cssIconDesc, c.cssIconNone ],
1002
+ aria = [ 'ascending', 'descending' ],
1003
+ // find the footer
1004
+ $headers = c.$table
1005
+ .find( 'tfoot tr' )
1006
+ .children()
1007
+ .add( $( c.namespace + '_extra_headers' ) )
1008
+ .removeClass( css.join( ' ' ) );
1009
+ // remove all header information
1010
+ c.$headers
1011
+ .removeClass( css.join( ' ' ) )
1012
+ .addClass( none )
1013
+ .attr( 'aria-sort', 'none' )
1014
+ .find( '.' + ts.css.icon )
1015
+ .removeClass( cssIcon.join( ' ' ) )
1016
+ .addClass( cssIcon[ 2 ] );
1017
+ for ( indx = 0; indx < len; indx++ ) {
1018
+ // direction = 2 means reset!
1019
+ if ( list[ indx ][ 1 ] !== 2 ) {
1020
+ // multicolumn sorting updating - see #1005
1021
+ // .not(function(){}) needs jQuery 1.4
1022
+ $sorted = c.$headers.filter( function( i, el ) {
1023
+ // only include headers that are in the sortList (this includes colspans)
1024
+ var include = true,
1025
+ $el = $( el ),
1026
+ col = parseInt( $el.attr( 'data-column' ), 10 ),
1027
+ end = col + el.colSpan;
1028
+ for ( ; col < end; col++ ) {
1029
+ include = include ? ts.isValueInArray( col, c.sortList ) > -1 : false;
829
1030
  }
830
- return a[c.columns].order - b[c.columns].order;
1031
+ return include;
831
1032
  });
832
- }
833
- if (c.debug) {
834
- console.log( 'Sorting on ' + sortList.toString() + ' and dir ' + order + ' time' + ts.benchmark(sortTime) );
835
- }
836
- }
837
1033
 
838
- function resortComplete(c, callback){
839
- if (c.table.isUpdating) {
840
- c.$table.trigger('updateComplete', c.table);
1034
+ // choose the :last in case there are nested columns
1035
+ $sorted = $sorted
1036
+ .not( '.sorter-false' )
1037
+ .filter( '[data-column="' + list[ indx ][ 0 ] + '"]' + ( len === 1 ? ':last' : '' ) );
1038
+ if ( $sorted.length ) {
1039
+ for ( column = 0; column < $sorted.length; column++ ) {
1040
+ if ( !$sorted[ column ].sortDisabled ) {
1041
+ $sorted
1042
+ .eq( column )
1043
+ .removeClass( none )
1044
+ .addClass( css[ list[ indx ][ 1 ] ] )
1045
+ .attr( 'aria-sort', aria[ list[ indx ][ 1 ] ] )
1046
+ .find( '.' + ts.css.icon )
1047
+ .removeClass( cssIcon[ 2 ] )
1048
+ .addClass( cssIcon[ list[ indx ][ 1 ] ] );
1049
+ }
1050
+ }
1051
+ // add sorted class to footer & extra headers, if they exist
1052
+ if ( $headers.length ) {
1053
+ $headers
1054
+ .filter( '[data-column="' + list[ indx ][ 0 ] + '"]' )
1055
+ .removeClass( none )
1056
+ .addClass( css[ list[ indx ][ 1 ] ] );
1057
+ }
1058
+ }
841
1059
  }
842
- if ($.isFunction(callback)) {
843
- callback(c.table);
1060
+ }
1061
+ // add verbose aria labels
1062
+ len = c.$headers.length;
1063
+ $headers = c.$headers.not( '.sorter-false' );
1064
+ for ( indx = 0; indx < len; indx++ ) {
1065
+ $header = $headers.eq( indx );
1066
+ if ( $header.length ) {
1067
+ header = $headers[ indx ];
1068
+ column = parseInt( $header.attr( 'data-column' ), 10 );
1069
+ nextSort = c.sortVars[ column ].order[ ( c.sortVars[ column ].count + 1 ) % ( c.sortReset ? 3 : 2 ) ];
1070
+ tmp = $header.hasClass( ts.css.sortAsc ) ?
1071
+ 'sortAsc' :
1072
+ $header.hasClass( ts.css.sortDesc ) ? 'sortDesc' : 'sortNone';
1073
+ txt = $.trim( $header.text() ) + ': ' +
1074
+ ts.language[ tmp ] +
1075
+ ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ];
1076
+ $header.attr( 'aria-label', txt );
844
1077
  }
845
1078
  }
1079
+ },
846
1080
 
847
- function checkResort(c, resort, callback) {
848
- var sl = $.isArray(resort) ? resort : c.sortList,
849
- // if no resort parameter is passed, fallback to config.resort (true by default)
850
- resrt = typeof resort === 'undefined' ? c.resort : resort;
851
- // don't try to resort if the table is still processing
852
- // this will catch spamming of the updateCell method
853
- if (resrt !== false && !c.serverSideSorting && !c.table.isProcessing) {
854
- if (sl.length) {
855
- c.$table.trigger('sorton', [ sl, function(){
856
- resortComplete(c, callback);
857
- }, true ]);
1081
+ updateHeader : function( c ) {
1082
+ var index, isDisabled, $th, col,
1083
+ table = c.table,
1084
+ len = c.$headers.length;
1085
+ for ( index = 0; index < len; index++ ) {
1086
+ $th = c.$headers.eq( index );
1087
+ col = ts.getColumnData( table, c.headers, index, true );
1088
+ // add 'sorter-false' class if 'parser-false' is set
1089
+ isDisabled = ts.getData( $th, col, 'sorter' ) === 'false' || ts.getData( $th, col, 'parser' ) === 'false';
1090
+ $th[ 0 ].sortDisabled = isDisabled;
1091
+ $th[ isDisabled ? 'addClass' : 'removeClass' ]( 'sorter-false' ).attr( 'aria-disabled', '' + isDisabled );
1092
+ // disable tab index on disabled cells
1093
+ if ( c.tabIndex ) {
1094
+ if ( isDisabled ) {
1095
+ $th.removeAttr( 'tabindex' );
858
1096
  } else {
859
- c.$table.trigger('sortReset', [ function(){
860
- resortComplete(c, callback);
861
- ts.applyWidget(c.table, false);
862
- } ]);
1097
+ $th.attr( 'tabindex', '0' );
1098
+ }
1099
+ }
1100
+ // aria-controls - requires table ID
1101
+ if ( table.id ) {
1102
+ if ( isDisabled ) {
1103
+ $th.removeAttr( 'aria-controls' );
1104
+ } else {
1105
+ $th.attr( 'aria-controls', table.id );
863
1106
  }
864
- } else {
865
- resortComplete(c, callback);
866
- ts.applyWidget(c.table, false);
867
1107
  }
868
1108
  }
1109
+ },
869
1110
 
870
- function bindMethods( table ){
871
- var c = table.config,
872
- $table = c.$table,
873
- events = ( 'sortReset update updateRows updateAll updateHeaders addRows updateCell updateComplete ' +
874
- 'sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup ' +
875
- 'mouseleave ' ).split( ' ' )
876
- .join( c.namespace + ' ' );
877
- // apply easy methods that trigger bound events
878
- $table
879
- .unbind( events.replace( ts.regex.spaces, ' ' ) )
880
- .bind( 'sortReset' + c.namespace, function( e, callback ) {
881
- e.stopPropagation();
882
- // using this.config to ensure functions are getting a non-cached version of the config
883
- ts.sortReset( this.config, callback );
884
- })
885
- .bind( 'updateAll' + c.namespace, function( e, resort, callback ) {
886
- e.stopPropagation();
887
- ts.updateAll( this.config, resort, callback );
888
- })
889
- .bind( 'update' + c.namespace + ' updateRows' + c.namespace, function( e, resort, callback ) {
890
- e.stopPropagation();
891
- ts.update( this.config, resort, callback );
892
- })
893
- .bind( 'updateHeaders' + c.namespace, function( e, callback ) {
894
- e.stopPropagation();
895
- ts.updateHeaders( this.config, callback );
896
- })
897
- .bind( 'updateCell' + c.namespace, function(e, cell, resort, callback ) {
898
- e.stopPropagation();
899
- ts.updateCell( this.config, cell, resort, callback );
900
- })
901
- .bind( 'addRows' + c.namespace, function(e, $row, resort, callback) {
902
- e.stopPropagation();
903
- ts.addRows( this.config, $row, resort, callback );
904
- })
905
- .bind( 'updateComplete' + c.namespace, function() {
906
- table.isUpdating = false;
907
- })
908
- .bind( 'sorton' + c.namespace, function( e, list, callback, init ) {
909
- e.stopPropagation();
910
- ts.sortOn( this.config, list, callback, init );
911
- })
912
- .bind( 'appendCache' + c.namespace, function( e, callback, init ) {
913
- e.stopPropagation();
914
- ts.appendCache( this.config, init );
915
- if ( $.isFunction( callback ) ) {
916
- callback( table );
917
- }
918
- })
919
- // $tbodies variable is used by the tbody sorting widget
920
- .bind( 'updateCache' + c.namespace, function( e, callback, $tbodies ){
921
- e.stopPropagation();
922
- ts.updateCache( this.config, callback, $tbodies );
923
- })
924
- .bind( 'applyWidgetId' + c.namespace, function( e, id ) {
925
- e.stopPropagation();
926
- ts.getWidgetById( id ).format( table, this.config, this.config.widgetOptions );
927
- })
928
- .bind( 'applyWidgets' + c.namespace, function( e, init ) {
929
- e.stopPropagation();
930
- // apply widgets
931
- ts.applyWidget( table, init );
932
- })
933
- .bind( 'refreshWidgets' + c.namespace, function( e, all, dontapply ) {
934
- e.stopPropagation();
935
- ts.refreshWidgets( table, all, dontapply );
936
- })
937
- .bind( 'destroy' + c.namespace, function( e, removeClasses, callback ) {
938
- e.stopPropagation();
939
- ts.destroy( table, removeClasses, callback );
940
- })
941
- .bind( 'resetToLoadState' + c.namespace, function( e ) {
942
- e.stopPropagation();
943
- // remove all widgets
944
- ts.removeWidget( table, true, false );
945
- // restore original settings; this clears out current settings, but does not clear
946
- // values saved to storage.
947
- c = $.extend( true, ts.defaults, c.originalSettings );
948
- table.hasInitialized = false;
949
- // setup the entire table again
950
- ts.setup( table, c );
951
- });
1111
+ updateHeaderSortCount : function( c, list ) {
1112
+ var col, dir, group, indx, primary, temp, val, order,
1113
+ sortList = list || c.sortList,
1114
+ len = sortList.length;
1115
+ c.sortList = [];
1116
+ for ( indx = 0; indx < len; indx++ ) {
1117
+ val = sortList[ indx ];
1118
+ // ensure all sortList values are numeric - fixes #127
1119
+ col = parseInt( val[ 0 ], 10 );
1120
+ // prevents error if sorton array is wrong
1121
+ if ( col < c.columns ) {
1122
+ order = c.sortVars[ col ].order;
1123
+ dir = ( '' + val[ 1 ] ).match( /^(1|d|s|o|n)/ );
1124
+ dir = dir ? dir[ 0 ] : '';
1125
+ // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext
1126
+ switch ( dir ) {
1127
+ case '1' : case 'd' : // descending
1128
+ dir = 1;
1129
+ break;
1130
+ case 's' : // same direction (as primary column)
1131
+ // if primary sort is set to 's', make it ascending
1132
+ dir = primary || 0;
1133
+ break;
1134
+ case 'o' :
1135
+ temp = order[ ( primary || 0 ) % ( c.sortReset ? 3 : 2 ) ];
1136
+ // opposite of primary column; but resets if primary resets
1137
+ dir = temp === 0 ? 1 : temp === 1 ? 0 : 2;
1138
+ break;
1139
+ case 'n' :
1140
+ dir = order[ ( ++c.sortVars[ col ].count ) % ( c.sortReset ? 3 : 2 ) ];
1141
+ break;
1142
+ default : // ascending
1143
+ dir = 0;
1144
+ break;
1145
+ }
1146
+ primary = indx === 0 ? dir : primary;
1147
+ group = [ col, parseInt( dir, 10 ) || 0 ];
1148
+ c.sortList.push( group );
1149
+ dir = $.inArray( group[ 1 ], order ); // fixes issue #167
1150
+ c.sortVars[ col ].count = dir >= 0 ? dir : group[ 1 ] % ( c.sortReset ? 3 : 2 );
1151
+ }
952
1152
  }
1153
+ },
953
1154
 
954
- /* public methods */
955
- ts.construct = function(settings) {
956
- return this.each(function() {
957
- var table = this,
958
- // merge & extend config options
959
- c = $.extend(true, {}, ts.defaults, settings, ts.instanceMethods);
960
- // save initial settings
961
- c.originalSettings = settings;
962
- // create a table from data (build table widget)
963
- if (!table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE') {
964
- // return the table (in case the original target is the table's container)
965
- ts.buildTable(table, c);
966
- } else {
967
- ts.setup(table, c);
968
- }
969
- });
970
- };
1155
+ updateAll : function( c, resort, callback ) {
1156
+ var table = c.table;
1157
+ table.isUpdating = true;
1158
+ ts.refreshWidgets( table, true, true );
1159
+ ts.buildHeaders( c );
1160
+ ts.bindEvents( table, c.$headers, true );
1161
+ ts.bindMethods( c );
1162
+ ts.commonUpdate( c, resort, callback );
1163
+ },
971
1164
 
972
- ts.setup = function(table, c) {
973
- // if no thead or tbody, or tablesorter is already present, quit
974
- if (!table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true) {
975
- if ( c.debug ) {
976
- if ( table.hasInitialized ) {
977
- console.warn( 'Stopping initialization. Tablesorter has already been initialized' );
1165
+ update : function( c, resort, callback ) {
1166
+ var table = c.table;
1167
+ table.isUpdating = true;
1168
+ // update sorting (if enabled/disabled)
1169
+ ts.updateHeader( c );
1170
+ ts.commonUpdate( c, resort, callback );
1171
+ },
1172
+
1173
+ // simple header update - see #989
1174
+ updateHeaders : function( c, callback ) {
1175
+ c.table.isUpdating = true;
1176
+ ts.buildHeaders( c );
1177
+ ts.bindEvents( c.table, c.$headers, true );
1178
+ ts.resortComplete( c, callback );
1179
+ },
1180
+
1181
+ updateCell : function( c, cell, resort, callback ) {
1182
+ c.table.isUpdating = true;
1183
+ c.$table.find( c.selectorRemove ).remove();
1184
+ // get position from the dom
1185
+ var tmp, indx, row, icell, cache, len,
1186
+ $tbodies = c.$tbodies,
1187
+ $cell = $( cell ),
1188
+ // update cache - format: function( s, table, cell, cellIndex )
1189
+ // no closest in jQuery v1.2.6
1190
+ tbodyIndex = $tbodies
1191
+ .index( $.fn.closest ? $cell.closest( 'tbody' ) : $cell.parents( 'tbody' ).filter( ':first' ) ),
1192
+ tbcache = c.cache[ tbodyIndex ],
1193
+ $row = $.fn.closest ? $cell.closest( 'tr' ) : $cell.parents( 'tr' ).filter( ':first' );
1194
+ cell = $cell[ 0 ]; // in case cell is a jQuery object
1195
+ // tbody may not exist if update is initialized while tbody is removed for processing
1196
+ if ( $tbodies.length && tbodyIndex >= 0 ) {
1197
+ row = $tbodies.eq( tbodyIndex ).find( 'tr' ).index( $row );
1198
+ cache = tbcache.normalized[ row ];
1199
+ len = $row[ 0 ].cells.length;
1200
+ if ( len !== c.columns ) {
1201
+ // colspan in here somewhere!
1202
+ icell = 0;
1203
+ tmp = false;
1204
+ for ( indx = 0; indx < len; indx++ ) {
1205
+ if ( !tmp && $row[ 0 ].cells[ indx ] !== cell ) {
1206
+ icell += $row[ 0 ].cells[ indx ].colSpan;
978
1207
  } else {
979
- console.error( 'Stopping initialization! No table, thead or tbody' );
1208
+ tmp = true;
980
1209
  }
981
1210
  }
982
- return;
983
- }
984
-
985
- var k = '',
986
- $table = $(table),
987
- m = $.metadata;
988
- // initialization flag
989
- table.hasInitialized = false;
990
- // table is being processed flag
991
- table.isProcessing = true;
992
- // make sure to store the config object
993
- table.config = c;
994
- // save the settings where they read
995
- $.data(table, 'tablesorter', c);
996
- if (c.debug) {
997
- console[ console.group ? 'group' : 'log' ]( 'Initializing tablesorter' );
998
- $.data( table, 'startoveralltimer', new Date());
999
- }
1000
-
1001
- // removing this in version 3 (only supports jQuery 1.7+)
1002
- c.supportsDataObject = (function(version) {
1003
- version[0] = parseInt(version[0], 10);
1004
- return (version[0] > 1) || (version[0] === 1 && parseInt(version[1], 10) >= 4);
1005
- })($.fn.jquery.split('.'));
1006
- // digit sort text location; keeping max+/- for backwards compatibility
1007
- c.string = { 'max': 1, 'min': -1, 'emptymin': 1, 'emptymax': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false };
1008
- // ensure case insensitivity
1009
- c.emptyTo = c.emptyTo.toLowerCase();
1010
- c.stringTo = c.stringTo.toLowerCase();
1011
- // add table theme class only if there isn't already one there
1012
- if (!/tablesorter\-/.test($table.attr('class'))) {
1013
- k = (c.theme !== '' ? ' tablesorter-' + c.theme : '');
1014
- }
1015
- c.table = table;
1016
- c.$table = $table
1017
- .addClass(ts.css.table + ' ' + c.tableClass + k)
1018
- .attr('role', 'grid');
1019
- c.$headers = $table.find(c.selectorHeaders);
1020
-
1021
- // give the table a unique id, which will be used in namespace binding
1022
- if (!c.namespace) {
1023
- c.namespace = '.tablesorter' + Math.random().toString(16).slice(2);
1024
1211
  } else {
1025
- // make sure namespace starts with a period & doesn't have weird characters
1026
- c.namespace = '.' + c.namespace.replace(ts.regex.nonWord, '');
1212
+ icell = $cell.index();
1027
1213
  }
1028
-
1029
- c.$table.children().children('tr').attr('role', 'row');
1030
- c.$tbodies = $table.children('tbody:not(.' + c.cssInfoBlock + ')').attr({
1031
- 'aria-live' : 'polite',
1032
- 'aria-relevant' : 'all'
1033
- });
1034
- if (c.$table.children('caption').length) {
1035
- k = c.$table.children('caption')[0];
1036
- if (!k.id) { k.id = c.namespace.slice(1) + 'caption'; }
1037
- c.$table.attr('aria-labelledby', k.id);
1038
- }
1039
- c.widgetInit = {}; // keep a list of initialized widgets
1040
- // change textExtraction via data-attribute
1041
- c.textExtraction = c.$table.attr('data-text-extraction') || c.textExtraction || 'basic';
1042
- // build headers
1043
- buildHeaders( c );
1044
- // fixate columns if the users supplies the fixedWidth option
1045
- // do this after theme has been applied
1046
- ts.fixColumnWidth(table);
1047
- // add widget options before parsing (e.g. grouping widget has parser settings)
1048
- ts.applyWidgetOptions(table, c);
1049
- // try to auto detect column type, and store in tables config
1050
- buildParserCache(c);
1051
- // start total row count at zero
1052
- c.totalRows = 0;
1053
- // build the cache for the tbody cells
1054
- // delayInit will delay building the cache until the user starts a sort
1055
- if (!c.delayInit) { buildCache(table); }
1056
- // bind all header events and methods
1057
- ts.bindEvents(table, c.$headers, true);
1058
- bindMethods(table);
1059
- // get sort list from jQuery data or metadata
1060
- // in jQuery < 1.4, an error occurs when calling $table.data()
1061
- if (c.supportsDataObject && typeof $table.data().sortlist !== 'undefined') {
1062
- c.sortList = $table.data().sortlist;
1063
- } else if (m && ($table.metadata() && $table.metadata().sortlist)) {
1064
- c.sortList = $table.metadata().sortlist;
1065
- }
1066
- // apply widget init code
1067
- ts.applyWidget(table, true);
1068
- // if user has supplied a sort list to constructor
1069
- if (c.sortList.length > 0) {
1070
- $table.trigger('sorton', [ c.sortList, {}, !c.initWidgets, true ]);
1214
+ tmp = ts.getElementText( c, cell, icell ); // raw
1215
+ cache[ c.columns ].raw[ icell ] = tmp;
1216
+ tmp = ts.getParsedText( c, cell, icell, tmp );
1217
+ cache[ icell ] = tmp; // parsed
1218
+ cache[ c.columns ].$row = $row;
1219
+ if ( ( c.parsers[ icell ].type || '' ).toLowerCase() === 'numeric' ) {
1220
+ // update column max value (ignore sign)
1221
+ tbcache.colMax[ icell ] = Math.max( Math.abs( tmp ) || 0, tbcache.colMax[ icell ] || 0 );
1222
+ }
1223
+ tmp = resort !== 'undefined' ? resort : c.resort;
1224
+ if ( tmp !== false ) {
1225
+ // widgets will be reapplied
1226
+ ts.checkResort( c, tmp, callback );
1071
1227
  } else {
1072
- setHeadersCss(table);
1073
- if (c.initWidgets) {
1074
- // apply widget format
1075
- ts.applyWidget(table, false);
1076
- }
1077
- }
1078
-
1079
- // show processesing icon
1080
- if (c.showProcessing) {
1081
- $table
1082
- .unbind('sortBegin' + c.namespace + ' sortEnd' + c.namespace)
1083
- .bind('sortBegin' + c.namespace + ' sortEnd' + c.namespace, function(e) {
1084
- clearTimeout(c.processTimer);
1085
- ts.isProcessing(table);
1086
- if (e.type === 'sortBegin') {
1087
- c.processTimer = setTimeout(function(){
1088
- ts.isProcessing(table, true);
1089
- }, 500);
1090
- }
1091
- });
1092
- }
1093
-
1094
- // initialized
1095
- table.hasInitialized = true;
1096
- table.isProcessing = false;
1097
- if (c.debug) {
1098
- console.log( 'Overall initialization time: ' + ts.benchmark( $.data( table, 'startoveralltimer') ) );
1099
- if ( c.debug && console.groupEnd ) { console.groupEnd(); }
1100
- }
1101
- $table.trigger('tablesorter-initialized', table);
1102
- if (typeof c.initialized === 'function') { c.initialized(table); }
1103
- };
1104
-
1105
- // automatically add a colgroup with col elements set to a percentage width
1106
- ts.fixColumnWidth = function(table) {
1107
- table = $(table)[0];
1108
- var overallWidth, percent, $tbodies, len, index,
1109
- c = table.config,
1110
- $colgroup = c.$table.children('colgroup');
1111
- // remove plugin-added colgroup, in case we need to refresh the widths
1112
- if ($colgroup.length && $colgroup.hasClass(ts.css.colgroup)) {
1113
- $colgroup.remove();
1114
- }
1115
- if (c.widthFixed && c.$table.children('colgroup').length === 0) {
1116
- $colgroup = $('<colgroup class="' + ts.css.colgroup + '">');
1117
- overallWidth = c.$table.width();
1118
- // only add col for visible columns - fixes #371
1119
- $tbodies = c.$tbodies.find('tr:first').children(':visible'); // .each(function()
1120
- len = $tbodies.length;
1121
- for ( index = 0; index < len; index++ ) {
1122
- percent = parseInt( ( $tbodies.eq( index ).width() / overallWidth ) * 1000, 10 ) / 10 + '%';
1123
- $colgroup.append( $('<col>').css('width', percent) );
1124
- }
1125
- c.$table.prepend($colgroup);
1228
+ // don't reapply widgets is resort is false, just in case it causes
1229
+ // problems with element focus
1230
+ ts.resortComplete( c, callback );
1126
1231
  }
1127
- };
1232
+ }
1233
+ },
1128
1234
 
1129
- ts.getColumnData = function(table, obj, indx, getCell, $headers){
1130
- if (typeof obj === 'undefined' || obj === null) { return; }
1131
- table = $(table)[0];
1132
- var $h, k,
1133
- c = table.config,
1134
- $cells = ( $headers || c.$headers ),
1135
- // c.$headerIndexed is not defined initially
1136
- $cell = c.$headerIndexed && c.$headerIndexed[indx] || $cells.filter('[data-column="' + indx + '"]:last');
1137
- if (obj[indx]) {
1138
- return getCell ? obj[indx] : obj[$cells.index( $cell )];
1139
- }
1140
- for (k in obj) {
1141
- if (typeof k === 'string') {
1142
- $h = $cell
1143
- // header cell with class/id
1144
- .filter(k)
1145
- // find elements within the header cell with cell/id
1146
- .add( $cell.find(k) );
1147
- if ($h.length) {
1148
- return obj[k];
1149
- }
1150
- }
1235
+ addRows : function( c, $row, resort, callback ) {
1236
+ var txt, val, tbodyIndex, rowIndex, rows, cellIndex, len,
1237
+ cacheIndex, rowData, cells, cell, span,
1238
+ // allow passing a row string if only one non-info tbody exists in the table
1239
+ valid = typeof $row === 'string' && c.$tbodies.length === 1 && /<tr/.test( $row || '' ),
1240
+ table = c.table;
1241
+ if ( valid ) {
1242
+ $row = $( $row );
1243
+ c.$tbodies.append( $row );
1244
+ } else if ( !$row ||
1245
+ // row is a jQuery object?
1246
+ !( $row instanceof jQuery ) ||
1247
+ // row contained in the table?
1248
+ ( $.fn.closest ? $row.closest( 'table' )[ 0 ] : $row.parents( 'table' )[ 0 ] ) !== c.table ) {
1249
+ if ( c.debug ) {
1250
+ console.error( 'addRows method requires (1) a jQuery selector reference to rows that have already ' +
1251
+ 'been added to the table, or (2) row HTML string to be added to a table with only one tbody' );
1151
1252
  }
1152
- return;
1153
- };
1154
-
1155
- // computeTableHeaderCellIndexes from:
1156
- // http://www.javascripttoolbox.com/lib/table/examples.php
1157
- // http://www.javascripttoolbox.com/temp/table_cellindex.html
1158
- ts.computeColumnIndex = function(trs) {
1159
- var i, j, k, l, $cell, cell, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol,
1160
- matrix = [],
1161
- matrixrow = [],
1162
- lookup = {};
1163
- for (i = 0; i < trs.length; i++) {
1164
- cells = trs[i].cells;
1165
- for (j = 0; j < cells.length; j++) {
1166
- cell = cells[j];
1167
- $cell = $(cell);
1168
- rowIndex = cell.parentNode.rowIndex;
1169
- cellId = rowIndex + '-' + $cell.index();
1170
- rowSpan = cell.rowSpan || 1;
1171
- colSpan = cell.colSpan || 1;
1172
- if (typeof matrix[rowIndex] === 'undefined') {
1173
- matrix[rowIndex] = [];
1174
- }
1175
- // Find first available column in the first row
1176
- for (k = 0; k < matrix[rowIndex].length + 1; k++) {
1177
- if (typeof matrix[rowIndex][k] === 'undefined') {
1178
- firstAvailCol = k;
1179
- break;
1180
- }
1253
+ return false;
1254
+ }
1255
+ table.isUpdating = true;
1256
+ if ( ts.isEmptyObject( c.cache ) ) {
1257
+ // empty table, do an update instead - fixes #450
1258
+ ts.updateHeader( c );
1259
+ ts.commonUpdate( c, resort, callback );
1260
+ } else {
1261
+ rows = $row.filter( 'tr' ).attr( 'role', 'row' ).length;
1262
+ tbodyIndex = c.$tbodies.index( $row.parents( 'tbody' ).filter( ':first' ) );
1263
+ // fixes adding rows to an empty table - see issue #179
1264
+ if ( !( c.parsers && c.parsers.length ) ) {
1265
+ ts.setupParsers( c );
1266
+ }
1267
+ // add each row
1268
+ for ( rowIndex = 0; rowIndex < rows; rowIndex++ ) {
1269
+ cacheIndex = 0;
1270
+ len = $row[ rowIndex ].cells.length;
1271
+ cells = [];
1272
+ rowData = {
1273
+ child : [],
1274
+ raw : [],
1275
+ $row : $row.eq( rowIndex ),
1276
+ order : c.cache[ tbodyIndex ].normalized.length
1277
+ };
1278
+ // add each cell
1279
+ for ( cellIndex = 0; cellIndex < len; cellIndex++ ) {
1280
+ cell = $row[ rowIndex ].cells[ cellIndex ];
1281
+ txt = ts.getElementText( c, cell, cacheIndex );
1282
+ rowData.raw[ cacheIndex ] = txt;
1283
+ val = ts.getParsedText( c, cell, cacheIndex, txt );
1284
+ cells[ cacheIndex ] = val;
1285
+ if ( ( c.parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) {
1286
+ // update column max value (ignore sign)
1287
+ c.cache[ tbodyIndex ].colMax[ cacheIndex ] =
1288
+ Math.max( Math.abs( val ) || 0, c.cache[ tbodyIndex ].colMax[ cacheIndex ] || 0 );
1181
1289
  }
1182
- lookup[cellId] = firstAvailCol;
1183
- // add data-column
1184
- $cell.attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex
1185
- for (k = rowIndex; k < rowIndex + rowSpan; k++) {
1186
- if (typeof matrix[k] === 'undefined') {
1187
- matrix[k] = [];
1188
- }
1189
- matrixrow = matrix[k];
1190
- for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
1191
- matrixrow[l] = 'x';
1192
- }
1290
+ span = cell.colSpan - 1;
1291
+ if ( span > 0 ) {
1292
+ cacheIndex += span;
1193
1293
  }
1294
+ cacheIndex++;
1194
1295
  }
1296
+ // add the row data to the end
1297
+ cells[ c.columns ] = rowData;
1298
+ // update cache
1299
+ c.cache[ tbodyIndex ].normalized.push( cells );
1195
1300
  }
1196
- return matrixrow.length;
1197
- };
1198
-
1199
- // *** Process table ***
1200
- // add processing indicator
1201
- ts.isProcessing = function( $table, toggle, $ths ) {
1202
- $table = $( $table );
1203
- var c = $table[0].config,
1204
- // default to all headers
1205
- $h = $ths || $table.find('.' + ts.css.header);
1206
- if (toggle) {
1207
- // don't use sortList if custom $ths used
1208
- if (typeof $ths !== 'undefined' && c.sortList.length > 0) {
1209
- // get headers from the sortList
1210
- $h = $h.filter(function(){
1211
- // get data-column from attr to keep compatibility with jQuery 1.2.6
1212
- return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList) >= 0;
1213
- });
1214
- }
1215
- $table.add($h).addClass(ts.css.processing + ' ' + c.cssProcessing);
1216
- } else {
1217
- $table.add($h).removeClass(ts.css.processing + ' ' + c.cssProcessing);
1218
- }
1219
- };
1220
-
1221
- // detach tbody but save the position
1222
- // don't use tbody because there are portions that look for a tbody index (updateCell)
1223
- ts.processTbody = function(table, $tb, getIt){
1224
- table = $(table)[0];
1225
- var holdr;
1226
- if (getIt) {
1227
- table.isProcessing = true;
1228
- $tb.before('<colgroup class="tablesorter-savemyplace"/>');
1229
- holdr = ($.fn.detach) ? $tb.detach() : $tb.remove();
1230
- return holdr;
1231
- }
1232
- holdr = $(table).find('colgroup.tablesorter-savemyplace');
1233
- $tb.insertAfter( holdr );
1234
- holdr.remove();
1235
- table.isProcessing = false;
1236
- };
1301
+ // resort using current settings
1302
+ ts.checkResort( c, resort, callback );
1303
+ }
1304
+ },
1237
1305
 
1238
- ts.clearTableBody = function(table) {
1239
- $(table)[0].config.$tbodies.children().detach();
1240
- };
1306
+ updateCache : function( c, callback, $tbodies ) {
1307
+ // rebuild parsers
1308
+ if ( !( c.parsers && c.parsers.length ) ) {
1309
+ ts.setupParsers( c, $tbodies );
1310
+ }
1311
+ // rebuild the cache map
1312
+ ts.buildCache( c, callback, $tbodies );
1313
+ },
1241
1314
 
1242
- ts.bindEvents = function(table, $headers, core) {
1243
- table = $(table)[0];
1244
- var t, downTarget = null,
1245
- c = table.config;
1246
- if (core !== true) {
1247
- $headers.addClass( c.namespace.slice(1) + '_extra_headers' );
1248
- t = $.fn.closest ? $headers.closest('table')[0] : $headers.parents('table')[0];
1249
- if (t && t.nodeName === 'TABLE' && t !== table) {
1250
- $(t).addClass( c.namespace.slice(1) + '_extra_table' );
1251
- }
1252
- }
1253
- t = ( c.pointerDown + ' ' + c.pointerUp + ' ' + c.pointerClick + ' sort keyup ' )
1254
- .replace(ts.regex.spaces, ' ')
1255
- .split(' ')
1256
- .join(c.namespace + ' ');
1257
- // apply event handling to headers and/or additional headers (stickyheaders, scroller, etc)
1258
- $headers
1259
- // http://stackoverflow.com/questions/5312849/jquery-find-self;
1260
- .find(c.selectorSort).add( $headers.filter(c.selectorSort) )
1261
- .unbind(t)
1262
- .bind(t, function(e, external) {
1263
- var $cell, cell, temp,
1264
- $target = $(e.target),
1265
- // wrap event type in spaces, so the match doesn't trigger on inner words
1266
- type = ' ' + e.type + ' ';
1267
- // only recognize left clicks
1268
- if ( ( ( e.which || e.button ) !== 1 && !type.match( ' ' + c.pointerClick + ' | sort | keyup ' ) ) ||
1269
- // allow pressing enter
1270
- ( type === ' keyup ' && e.which !== 13 ) ||
1271
- // allow triggering a click event (e.which is undefined) & ignore physical clicks
1272
- ( type.match(' ' + c.pointerClick + ' ') && typeof e.which !== 'undefined' ) ) {
1273
- return;
1274
- }
1275
- // ignore mouseup if mousedown wasn't on the same target
1276
- if ( type.match(' ' + c.pointerUp + ' ') && downTarget !== e.target && external !== true ) { return; }
1277
- // set target on mousedown
1278
- if ( type.match(' ' + c.pointerDown + ' ') ) {
1279
- downTarget = e.target;
1280
- // preventDefault needed or jQuery v1.3.2 and older throws an
1281
- // "Uncaught TypeError: handler.apply is not a function" error
1282
- temp = $target.jquery.split( '.' );
1283
- if ( temp[0] === '1' && temp[1] < 4 ) { e.preventDefault(); }
1284
- return;
1285
- }
1286
- downTarget = null;
1287
- // prevent sort being triggered on form elements
1288
- if ( ts.regex.formElements.test(e.target.nodeName) ||
1289
- // nosort class name, or elements within a nosort container
1290
- $target.hasClass(c.cssNoSort) || $target.parents('.' + c.cssNoSort).length > 0 ||
1291
- // elements within a button
1292
- $target.parents('button').length > 0 ) {
1293
- return !c.cancelSelection;
1294
- }
1295
- if (c.delayInit && ts.isEmptyObject(c.cache)) { buildCache(table); }
1296
- // jQuery v1.2.6 doesn't have closest()
1297
- $cell = $.fn.closest ? $(this).closest('th, td') : /TH|TD/.test(this.nodeName) ? $(this) : $(this).parents('th, td');
1298
- // reference original table headers and find the same cell
1299
- // don't use $headers or IE8 throws an error - see #987
1300
- temp = $headers.index( $cell );
1301
- c.lastClickedIndex = ( temp < 0 ) ? $cell.attr('data-column') : temp;
1302
- // use column index if $headers is undefined
1303
- cell = c.$headers[ c.lastClickedIndex ];
1304
- if (cell && !cell.sortDisabled) {
1305
- initSort(table, cell, e);
1315
+ // init flag (true) used by pager plugin to prevent widget application
1316
+ // renamed from appendToTable
1317
+ appendCache : function( c, init ) {
1318
+ var parsed, totalRows, $tbody, $curTbody, rowIndex, tbodyIndex, appendTime,
1319
+ table = c.table,
1320
+ wo = c.widgetOptions,
1321
+ $tbodies = c.$tbodies,
1322
+ rows = [],
1323
+ cache = c.cache;
1324
+ // empty table - fixes #206/#346
1325
+ if ( ts.isEmptyObject( cache ) ) {
1326
+ // run pager appender in case the table was just emptied
1327
+ return c.appender ? c.appender( table, rows ) :
1328
+ table.isUpdating ? c.$table.trigger( 'updateComplete', table ) : ''; // Fixes #532
1329
+ }
1330
+ if ( c.debug ) {
1331
+ appendTime = new Date();
1332
+ }
1333
+ for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
1334
+ $tbody = $tbodies.eq( tbodyIndex );
1335
+ if ( $tbody.length ) {
1336
+ // detach tbody for manipulation
1337
+ $curTbody = ts.processTbody( table, $tbody, true );
1338
+ parsed = cache[ tbodyIndex ].normalized;
1339
+ totalRows = parsed.length;
1340
+ for ( rowIndex = 0; rowIndex < totalRows; rowIndex++ ) {
1341
+ rows.push( parsed[ rowIndex ][ c.columns ].$row );
1342
+ // removeRows used by the pager plugin; don't render if using ajax - fixes #411
1343
+ if ( !c.appender || ( c.pager && ( !c.pager.removeRows || !wo.pager_removeRows ) && !c.pager.ajax ) ) {
1344
+ $curTbody.append( parsed[ rowIndex ][ c.columns ].$row );
1345
+ }
1306
1346
  }
1307
- });
1308
- if (c.cancelSelection) {
1309
- // cancel selection
1310
- $headers
1311
- .attr('unselectable', 'on')
1312
- .bind('selectstart', false)
1313
- .css({
1314
- 'user-select': 'none',
1315
- 'MozUserSelect': 'none' // not needed for jQuery 1.8+
1316
- });
1317
- }
1318
- };
1319
-
1320
- ts.sortReset = function( c, callback ) {
1321
- var table = c.table;
1322
- c.sortList = [];
1323
- setHeadersCss( table );
1324
- multisort( table );
1325
- ts.appendCache( c );
1326
- if ( $.isFunction( callback ) ) {
1327
- callback( table );
1347
+ // restore tbody
1348
+ ts.processTbody( table, $curTbody, false );
1328
1349
  }
1329
- };
1330
-
1331
- ts.updateAll = function( c, resort, callback ) {
1332
- var table = c.table;
1333
- table.isUpdating = true;
1334
- ts.refreshWidgets( table, true, true );
1335
- buildHeaders( c );
1336
- ts.bindEvents( table, c.$headers, true );
1337
- bindMethods( table);
1338
- commonUpdate( table, resort, callback );
1339
- };
1340
-
1341
- ts.update = function( c, resort, callback ) {
1342
- var table = c.table;
1343
- table.isUpdating = true;
1344
- // update sorting (if enabled/disabled)
1345
- updateHeader( table );
1346
- commonUpdate( table, resort, callback );
1347
- };
1350
+ }
1351
+ if ( c.appender ) {
1352
+ c.appender( table, rows );
1353
+ }
1354
+ if ( c.debug ) {
1355
+ console.log( 'Rebuilt table' + ts.benchmark( appendTime ) );
1356
+ }
1357
+ // apply table widgets; but not before ajax completes
1358
+ if ( !init && !c.appender ) {
1359
+ ts.applyWidget( table );
1360
+ }
1361
+ if ( table.isUpdating ) {
1362
+ c.$table.trigger( 'updateComplete', table );
1363
+ }
1364
+ },
1348
1365
 
1349
- // simple header update - see #989
1350
- ts.updateHeaders = function( c, callback ) {
1351
- c.table.isUpdating = true;
1352
- buildHeaders( c );
1353
- ts.bindEvents( c.table, c.$headers, true );
1354
- resortComplete( c, callback );
1355
- };
1366
+ commonUpdate : function( c, resort, callback ) {
1367
+ // remove rows/elements before update
1368
+ c.$table.find( c.selectorRemove ).remove();
1369
+ // rebuild parsers
1370
+ ts.setupParsers( c );
1371
+ // rebuild the cache map
1372
+ ts.buildCache( c );
1373
+ ts.checkResort( c, resort, callback );
1374
+ },
1356
1375
 
1357
- ts.updateCell = function( c, cell, resort, callback ) {
1358
- c.table.isUpdating = true;
1359
- c.$table.find( c.selectorRemove ).remove();
1360
- // get position from the dom
1361
- var t, row, icell, cache,
1376
+ /*
1377
+ ▄█████ ▄████▄ █████▄ ██████ ██ █████▄ ▄████▄
1378
+ ▀█▄ ██ ██ ██▄▄██ ██ ██ ██ ██ ██ ▄▄▄
1379
+ ▀█▄ ██ ██ ██▀██ ██ ██ ██ ██ ██ ▀██
1380
+ █████▀ ▀████▀ ██ ██ ██ ██ ██ ██ ▀████▀
1381
+ */
1382
+ initSort : function( c, cell, event ) {
1383
+ if ( c.table.isUpdating ) {
1384
+ // let any updates complete before initializing a sort
1385
+ return setTimeout( function(){
1386
+ ts.initSort( c, cell, event );
1387
+ }, 50 );
1388
+ }
1389
+ var arry, indx, headerIndx, dir, temp, tmp, $header,
1390
+ notMultiSort = !event[ c.sortMultiSortKey ],
1362
1391
  table = c.table,
1363
- $tb = c.$tbodies,
1364
- $cell = $(cell),
1365
- // update cache - format: function(s, table, cell, cellIndex)
1366
- // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr');
1367
- tbdy = $tb.index( $.fn.closest ? $cell.closest( 'tbody' ) : $cell.parents( 'tbody' ).filter( ':first' ) ),
1368
- tbcache = c.cache[ tbdy ],
1369
- $row = $.fn.closest ? $cell.closest( 'tr' ) : $cell.parents( 'tr' ).filter( ':first' );
1370
- cell = $cell[ 0 ]; // in case cell is a jQuery object
1371
- // tbody may not exist if update is initialized while tbody is removed for processing
1372
- if ( $tb.length && tbdy >= 0 ) {
1373
- row = $tb.eq( tbdy ).find( 'tr' ).index( $row );
1374
- cache = tbcache.normalized[ row ];
1375
- icell = $cell.index();
1376
- t = ts.getParsedText( c, cell, icell );
1377
- cache[ icell ] = t;
1378
- cache[ c.columns ].$row = $row;
1379
- if ( ( c.parsers[ icell ].type || '' ).toLowerCase() === 'numeric' ) {
1380
- // update column max value (ignore sign)
1381
- tbcache.colMax[ icell ] = Math.max( Math.abs( t ) || 0, tbcache.colMax[ icell ] || 0 );
1382
- }
1383
- t = resort !== 'undefined' ? resort : c.resort;
1384
- if ( t !== false ) {
1385
- // widgets will be reapplied
1386
- checkResort( c, t, callback );
1387
- } else {
1388
- // don't reapply widgets is resort is false, just in case it causes
1389
- // problems with element focus
1390
- resortComplete( c, callback );
1392
+ len = c.$headers.length,
1393
+ // get current column index
1394
+ col = parseInt( $( cell ).attr( 'data-column' ), 10 ),
1395
+ order = c.sortVars[ col ].order;
1396
+
1397
+ // Only call sortStart if sorting is enabled
1398
+ c.$table.trigger( 'sortStart', table );
1399
+ // get current column sort order
1400
+ c.sortVars[ col ].count =
1401
+ event[ c.sortResetKey ] ? 2 : ( c.sortVars[ col ].count + 1 ) % ( c.sortReset ? 3 : 2 );
1402
+ // reset all sorts on non-current column - issue #30
1403
+ if ( c.sortRestart ) {
1404
+ tmp = cell;
1405
+ for ( headerIndx = 0; headerIndx < len; headerIndx++ ) {
1406
+ $header = c.$headers.eq( headerIndx );
1407
+ // only reset counts on columns that weren't just clicked on and if not included in a multisort
1408
+ if ( $header[ 0 ] !== tmp &&
1409
+ ( notMultiSort || !$header.is( '.' + ts.css.sortDesc + ',.' + ts.css.sortAsc ) ) ) {
1410
+ c.sortVars[ col ].count = -1;
1391
1411
  }
1392
1412
  }
1393
- };
1394
-
1395
- ts.addRows = function( c, $row, resort, callback ) {
1396
- var i, j, l, rowData, cells, rows, tbdy,
1397
- // allow passing a row string if only one non-info tbody exists in the table
1398
- valid = typeof $row === 'string' && c.$tbodies.length === 1 && /<tr/.test( $row || '' ),
1399
- table = c.table;
1400
- if ( valid ) {
1401
- $row = $( $row );
1402
- c.$tbodies.append( $row );
1403
- } else if ( !$row ||
1404
- // row is a jQuery object?
1405
- !( $row instanceof jQuery ) ||
1406
- // row contained in the table?
1407
- ( $.fn.closest ? $row.closest( 'table' )[ 0 ] : $row.parents( 'table' )[ 0 ] ) !== c.table ) {
1408
- if ( c.debug ) {
1409
- console.error( 'addRows method requires (1) a jQuery selector reference to rows that have already ' +
1410
- 'been added to the table, or (2) row HTML string to be added to a table with only one tbody' );
1413
+ }
1414
+ // user only wants to sort on one column
1415
+ if ( notMultiSort ) {
1416
+ // flush the sort list
1417
+ c.sortList = [];
1418
+ c.last.sortList = [];
1419
+ if ( c.sortForce !== null ) {
1420
+ arry = c.sortForce;
1421
+ for ( indx = 0; indx < arry.length; indx++ ) {
1422
+ if ( arry[ indx ][ 0 ] !== col ) {
1423
+ c.sortList.push( arry[ indx ] );
1424
+ }
1411
1425
  }
1412
- return false;
1413
1426
  }
1414
- table.isUpdating = true;
1415
- if ( ts.isEmptyObject( c.cache ) ) {
1416
- // empty table, do an update instead - fixes #450
1417
- updateHeader( table );
1418
- commonUpdate( table, resort, callback );
1419
- } else {
1420
- rows = $row.filter( 'tr' ).attr( 'role', 'row' ).length;
1421
- tbdy = c.$tbodies.index( $row.parents( 'tbody' ).filter( ':first' ) );
1422
- // fixes adding rows to an empty table - see issue #179
1423
- if ( !( c.parsers && c.parsers.length ) ) {
1424
- buildParserCache( c );
1427
+ // add column to sort list
1428
+ dir = order[ c.sortVars[ col ].count ];
1429
+ if ( dir < 2 ) {
1430
+ c.sortList.push( [ col, dir ] );
1431
+ // add other columns if header spans across multiple
1432
+ if ( cell.colSpan > 1 ) {
1433
+ for ( indx = 1; indx < cell.colSpan; indx++ ) {
1434
+ c.sortList.push( [ col + indx, dir ] );
1435
+ // update count on columns in colSpan
1436
+ c.sortVars[ col + indx ].count = $.inArray( dir, order );
1437
+ }
1425
1438
  }
1426
- // add each row
1427
- for ( i = 0; i < rows; i++ ) {
1428
- l = $row[ i ].cells.length;
1429
- cells = [];
1430
- rowData = {
1431
- child: [],
1432
- $row : $row.eq( i ),
1433
- order: c.cache[ tbdy ].normalized.length
1434
- };
1435
- // add each cell
1436
- for ( j = 0; j < l; j++ ) {
1437
- cells[ j ] = ts.getParsedText( c, $row[ i ].cells[ j ], j );
1438
- if ( ( c.parsers[ j ].type || '' ).toLowerCase() === 'numeric' ) {
1439
- // update column max value (ignore sign)
1440
- c.cache[ tbdy ].colMax[ j ] = Math.max( Math.abs( cells[ j ] ) || 0, c.cache[ tbdy ].colMax[ j ] || 0 );
1439
+ }
1440
+ // multi column sorting
1441
+ } else {
1442
+ // get rid of the sortAppend before adding more - fixes issue #115 & #523
1443
+ c.sortList = $.extend( [], c.last.sortList );
1444
+
1445
+ // the user has clicked on an already sorted column
1446
+ if ( ts.isValueInArray( col, c.sortList ) >= 0 ) {
1447
+ // reverse the sorting direction
1448
+ for ( indx = 0; indx < c.sortList.length; indx++ ) {
1449
+ tmp = c.sortList[ indx ];
1450
+ if ( tmp[ 0 ] === col ) {
1451
+ // order.count seems to be incorrect when compared to cell.count
1452
+ tmp[ 1 ] = order[ c.sortVars[ col ].count ];
1453
+ if ( tmp[1] === 2 ) {
1454
+ c.sortList.splice( indx, 1 );
1455
+ c.sortVars[ col ].count = -1;
1441
1456
  }
1442
1457
  }
1443
- // add the row data to the end
1444
- cells.push( rowData );
1445
- // update cache
1446
- c.cache[ tbdy ].normalized.push( cells );
1447
1458
  }
1448
- // resort using current settings
1449
- checkResort( c, resort, callback );
1450
- }
1451
- };
1452
-
1453
- ts.updateCache = function( c, callback, $tbodies ) {
1454
- // rebuild parsers
1455
- if ( !( c.parsers && c.parsers.length ) ) {
1456
- buildParserCache( c, $tbodies );
1457
- }
1458
- // rebuild the cache map
1459
- buildCache( c.table, callback, $tbodies );
1460
- };
1461
-
1462
- // init flag (true) used by pager plugin to prevent widget application
1463
- // renamed from appendToTable
1464
- ts.appendCache = function( c, init ) {
1465
- var n, totalRows, $bk, $tb, i, k, appendTime,
1466
- table = c.table,
1467
- wo = c.widgetOptions,
1468
- $tbodies = c.$tbodies,
1469
- rows = [],
1470
- cc = c.cache;
1471
- // empty table - fixes #206/#346
1472
- if ( ts.isEmptyObject( cc ) ) {
1473
- // run pager appender in case the table was just emptied
1474
- return c.appender ? c.appender( table, rows ) :
1475
- table.isUpdating ? c.$table.trigger( 'updateComplete', table ) : ''; // Fixes #532
1476
- }
1477
- if ( c.debug ) {
1478
- appendTime = new Date();
1479
- }
1480
- for ( k = 0; k < $tbodies.length; k++ ) {
1481
- $bk = $tbodies.eq( k );
1482
- if ( $bk.length ) {
1483
- // get tbody
1484
- $tb = ts.processTbody( table, $bk, true );
1485
- n = cc[ k ].normalized;
1486
- totalRows = n.length;
1487
- for ( i = 0; i < totalRows; i++ ) {
1488
- rows.push( n[ i ][ c.columns ].$row );
1489
- // removeRows used by the pager plugin; don't render if using ajax - fixes #411
1490
- if ( !c.appender || ( c.pager && ( !c.pager.removeRows || !wo.pager_removeRows ) && !c.pager.ajax ) ) {
1491
- $tb.append( n[ i ][ c.columns ].$row );
1459
+ } else {
1460
+ // add column to sort list array
1461
+ dir = order[ c.sortVars[ col ].count ];
1462
+ if ( dir < 2 ) {
1463
+ c.sortList.push( [ col, dir ] );
1464
+ // add other columns if header spans across multiple
1465
+ if ( cell.colSpan > 1 ) {
1466
+ for ( indx = 1; indx < cell.colSpan; indx++ ) {
1467
+ c.sortList.push( [ col + indx, dir ] );
1468
+ // update count on columns in colSpan
1469
+ c.sortVars[ col + indx ].count = $.inArray( dir, order );
1492
1470
  }
1493
1471
  }
1494
- // restore tbody
1495
- ts.processTbody( table, $tb, false );
1496
1472
  }
1497
1473
  }
1498
- if ( c.appender ) {
1499
- c.appender( table, rows );
1500
- }
1501
- if ( c.debug ) {
1502
- console.log( 'Rebuilt table' + ts.benchmark( appendTime ) );
1503
- }
1504
- // apply table widgets; but not before ajax completes
1505
- if ( !init && !c.appender ) { ts.applyWidget( table ); }
1506
- if ( table.isUpdating ) {
1507
- c.$table.trigger( 'updateComplete', table );
1474
+ }
1475
+ // save sort before applying sortAppend
1476
+ c.last.sortList = $.extend( [], c.sortList );
1477
+ if ( c.sortList.length && c.sortAppend ) {
1478
+ arry = $.isArray( c.sortAppend ) ? c.sortAppend : c.sortAppend[ c.sortList[ 0 ][ 0 ] ];
1479
+ if ( !ts.isEmptyObject( arry ) ) {
1480
+ for ( indx = 0; indx < arry.length; indx++ ) {
1481
+ if ( arry[ indx ][ 0 ] !== col && ts.isValueInArray( arry[ indx ][ 0 ], c.sortList ) < 0 ) {
1482
+ dir = arry[ indx ][ 1 ];
1483
+ temp = ( '' + dir ).match( /^(a|d|s|o|n)/ );
1484
+ if ( temp ) {
1485
+ tmp = c.sortList[ 0 ][ 1 ];
1486
+ switch ( temp[ 0 ] ) {
1487
+ case 'd' :
1488
+ dir = 1;
1489
+ break;
1490
+ case 's' :
1491
+ dir = tmp;
1492
+ break;
1493
+ case 'o' :
1494
+ dir = tmp === 0 ? 1 : 0;
1495
+ break;
1496
+ case 'n' :
1497
+ dir = ( tmp + 1 ) % ( c.sortReset ? 3 : 2 );
1498
+ break;
1499
+ default:
1500
+ dir = 0;
1501
+ break;
1502
+ }
1503
+ }
1504
+ c.sortList.push( [ arry[ indx ][ 0 ], dir ] );
1505
+ }
1506
+ }
1508
1507
  }
1509
- };
1510
-
1511
- ts.sortOn = function( c, list, callback, init ) {
1512
- var table = c.table;
1513
- c.$table.trigger( 'sortStart', table );
1514
- // update header count index
1515
- updateHeaderSortCount( table, list );
1508
+ }
1509
+ // sortBegin event triggered immediately before the sort
1510
+ c.$table.trigger( 'sortBegin', table );
1511
+ // setTimeout needed so the processing icon shows up
1512
+ setTimeout( function() {
1516
1513
  // set css for headers
1517
- setHeadersCss( table );
1518
- // fixes #346
1519
- if ( c.delayInit && ts.isEmptyObject( c.cache ) ) {
1520
- buildCache( table );
1521
- }
1522
- c.$table.trigger( 'sortBegin', table );
1523
- // sort the table and append it to the dom
1524
- multisort( table );
1525
- ts.appendCache( c, init );
1514
+ ts.setHeadersCss( c );
1515
+ ts.multisort( c );
1516
+ ts.appendCache( c );
1526
1517
  c.$table.trigger( 'sortEnd', table );
1527
- ts.applyWidget( table );
1528
- if ( $.isFunction( callback ) ) {
1529
- callback( table );
1530
- }
1531
- };
1518
+ }, 1 );
1519
+ },
1532
1520
 
1533
- // restore headers
1534
- ts.restoreHeaders = function(table){
1535
- var index, $cell,
1536
- c = $(table)[0].config,
1537
- $headers = c.$table.find( c.selectorHeaders ),
1538
- len = $headers.length;
1539
- // don't use c.$headers here in case header cells were swapped
1540
- for ( index = 0; index < len; index++ ) {
1541
- // c.$table.find(c.selectorHeaders).each(function(i){
1542
- $cell = $headers.eq( index );
1543
- // only restore header cells if it is wrapped
1544
- // because this is also used by the updateAll method
1545
- if ( $cell.find( '.' + ts.css.headerIn ).length ) {
1546
- $cell.html( c.headerContent[ index ] );
1521
+ // sort multiple columns
1522
+ multisort : function( c ) { /*jshint loopfunc:true */
1523
+ var tbodyIndex, sortTime, colMax, rows,
1524
+ table = c.table,
1525
+ dir = 0,
1526
+ textSorter = c.textSorter || '',
1527
+ sortList = c.sortList,
1528
+ sortLen = sortList.length,
1529
+ len = c.$tbodies.length;
1530
+ if ( c.serverSideSorting || ts.isEmptyObject( c.cache ) ) {
1531
+ // empty table - fixes #206/#346
1532
+ return;
1533
+ }
1534
+ if ( c.debug ) { sortTime = new Date(); }
1535
+ for ( tbodyIndex = 0; tbodyIndex < len; tbodyIndex++ ) {
1536
+ colMax = c.cache[ tbodyIndex ].colMax;
1537
+ rows = c.cache[ tbodyIndex ].normalized;
1538
+
1539
+ rows.sort( function( a, b ) {
1540
+ var sortIndex, num, col, order, sort, x, y;
1541
+ // rows is undefined here in IE, so don't use it!
1542
+ for ( sortIndex = 0; sortIndex < sortLen; sortIndex++ ) {
1543
+ col = sortList[ sortIndex ][ 0 ];
1544
+ order = sortList[ sortIndex ][ 1 ];
1545
+ // sort direction, true = asc, false = desc
1546
+ dir = order === 0;
1547
+
1548
+ if ( c.sortStable && a[ col ] === b[ col ] && sortLen === 1 ) {
1549
+ return a[ c.columns ].order - b[ c.columns ].order;
1550
+ }
1551
+
1552
+ // fallback to natural sort since it is more robust
1553
+ num = /n/i.test( ts.getSortType( c.parsers, col ) );
1554
+ if ( num && c.strings[ col ] ) {
1555
+ // sort strings in numerical columns
1556
+ if ( typeof ( ts.string[ c.strings[ col ] ] ) === 'boolean' ) {
1557
+ num = ( dir ? 1 : -1 ) * ( ts.string[ c.strings[ col ] ] ? -1 : 1 );
1558
+ } else {
1559
+ num = ( c.strings[ col ] ) ? ts.string[ c.strings[ col ] ] || 0 : 0;
1560
+ }
1561
+ // fall back to built-in numeric sort
1562
+ // var sort = $.tablesorter['sort' + s]( a[col], b[col], dir, colMax[col], table );
1563
+ sort = c.numberSorter ? c.numberSorter( a[ col ], b[ col ], dir, colMax[ col ], table ) :
1564
+ ts[ 'sortNumeric' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ], b[ col ], num, colMax[ col ], col, c );
1565
+ } else {
1566
+ // set a & b depending on sort direction
1567
+ x = dir ? a : b;
1568
+ y = dir ? b : a;
1569
+ // text sort function
1570
+ if ( typeof textSorter === 'function' ) {
1571
+ // custom OVERALL text sorter
1572
+ sort = textSorter( x[ col ], y[ col ], dir, col, table );
1573
+ } else if ( typeof textSorter === 'object' && textSorter.hasOwnProperty( col ) ) {
1574
+ // custom text sorter for a SPECIFIC COLUMN
1575
+ sort = textSorter[ col ]( x[ col ], y[ col ], dir, col, table );
1576
+ } else {
1577
+ // fall back to natural sort
1578
+ sort = ts[ 'sortNatural' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ], b[ col ], col, c );
1579
+ }
1580
+ }
1581
+ if ( sort ) { return sort; }
1547
1582
  }
1548
- }
1549
- };
1583
+ return a[ c.columns ].order - b[ c.columns ].order;
1584
+ });
1585
+ }
1586
+ if ( c.debug ) {
1587
+ console.log( 'Applying sort ' + sortList.toString() + ts.benchmark( sortTime ) );
1588
+ }
1589
+ },
1550
1590
 
1551
- ts.destroy = function(table, removeClasses, callback){
1552
- table = $(table)[0];
1553
- if (!table.hasInitialized) { return; }
1554
- // remove all widgets
1555
- ts.removeWidget(table, true, false);
1556
- var events,
1557
- $t = $(table),
1558
- c = table.config,
1559
- debug = c.debug,
1560
- $h = $t.find('thead:first'),
1561
- $r = $h.find('tr.' + ts.css.headerRow).removeClass(ts.css.headerRow + ' ' + c.cssHeaderRow),
1562
- $f = $t.find('tfoot:first > tr').children('th, td');
1563
- if (removeClasses === false && $.inArray('uitheme', c.widgets) >= 0) {
1564
- // reapply uitheme classes, in case we want to maintain appearance
1565
- $t.trigger('applyWidgetId', [ 'uitheme' ]);
1566
- $t.trigger('applyWidgetId', [ 'zebra' ]);
1567
- }
1568
- // remove widget added rows, just in case
1569
- $h.find('tr').not($r).remove();
1570
- // disable tablesorter
1571
- events = 'sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton ' +
1572
- 'appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave keypress ' +
1573
- 'sortBegin sortEnd resetToLoadState '.split(' ')
1574
- .join(c.namespace + ' ');
1575
- $t
1576
- .removeData('tablesorter')
1577
- .unbind( events.replace(ts.regex.spaces, ' ') );
1578
- c.$headers.add($f)
1579
- .removeClass( [ ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone ].join(' ') )
1580
- .removeAttr('data-column')
1581
- .removeAttr('aria-label')
1582
- .attr('aria-disabled', 'true');
1583
- $r.find(c.selectorSort).unbind( ('mousedown mouseup keypress '.split(' ').join(c.namespace + ' ')).replace(ts.regex.spaces, ' ') );
1584
- ts.restoreHeaders(table);
1585
- $t.toggleClass(ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false);
1586
- // clear flag in case the plugin is initialized again
1587
- table.hasInitialized = false;
1588
- delete table.config.cache;
1589
- if (typeof callback === 'function') {
1590
- callback(table);
1591
- }
1592
- if (debug) {
1593
- console.log( 'tablesorter has been removed' );
1594
- }
1595
- };
1591
+ resortComplete : function( c, callback ) {
1592
+ if ( c.table.isUpdating ) {
1593
+ c.$table.trigger( 'updateComplete', c.table );
1594
+ }
1595
+ if ( $.isFunction( callback ) ) {
1596
+ callback( c.table );
1597
+ }
1598
+ },
1596
1599
 
1597
- // *** sort functions ***
1598
- // regex used in natural sort
1599
- ts.regex.chunk = /(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi; // chunk/tokenize numbers & letters
1600
- ts.regex.chunks = /(^\\0|\\0$)/; // replace chunks @ ends
1601
- ts.regex.hex = /^0x[0-9a-f]+$/i; // hex
1602
-
1603
- // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed)
1604
- // this function will only accept strings, or you'll see 'TypeError: undefined is not a function'
1605
- // I could add a = a.toString(); b = b.toString(); but it'll slow down the sort overall
1606
- ts.sortNatural = function(a, b) {
1607
- if (a === b) { return 0; }
1608
- var xN, xD, yN, yD, xF, yF, i, mx,
1609
- r = ts.regex;
1610
- // first try and sort Hex codes
1611
- if (r.hex.test(b)) {
1612
- xD = parseInt(a.match(r.hex), 16);
1613
- yD = parseInt(b.match(r.hex), 16);
1614
- if ( xD < yD ) { return -1; }
1615
- if ( xD > yD ) { return 1; }
1616
- }
1617
- // chunk/tokenize
1618
- xN = a.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0');
1619
- yN = b.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0');
1620
- mx = Math.max(xN.length, yN.length);
1621
- // natural sorting through split numeric strings and default strings
1622
- for (i = 0; i < mx; i++) {
1623
- // find floats not starting with '0', string or 0 if not defined
1624
- xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0;
1625
- yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0;
1626
- // handle numeric vs string comparison - number < string - (Kyle Adams)
1627
- if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; }
1628
- // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
1629
- if (typeof xF !== typeof yF) {
1630
- xF += '';
1631
- yF += '';
1632
- }
1633
- if (xF < yF) { return -1; }
1634
- if (xF > yF) { return 1; }
1635
- }
1636
- return 0;
1637
- };
1600
+ checkResort : function( c, resort, callback ) {
1601
+ var sortList = $.isArray( resort ) ? resort : c.sortList,
1602
+ // if no resort parameter is passed, fallback to config.resort (true by default)
1603
+ resrt = typeof resort === 'undefined' ? c.resort : resort;
1604
+ // don't try to resort if the table is still processing
1605
+ // this will catch spamming of the updateCell method
1606
+ if ( resrt !== false && !c.serverSideSorting && !c.table.isProcessing ) {
1607
+ if ( sortList.length ) {
1608
+ ts.sortOn( c, sortList, function() {
1609
+ ts.resortComplete( c, callback );
1610
+ }, true );
1611
+ } else {
1612
+ ts.sortReset( c, function() {
1613
+ ts.resortComplete( c, callback );
1614
+ ts.applyWidget( c.table, false );
1615
+ } );
1616
+ }
1617
+ } else {
1618
+ ts.resortComplete( c, callback );
1619
+ ts.applyWidget( c.table, false );
1620
+ }
1621
+ },
1638
1622
 
1639
- ts.sortNaturalAsc = function(a, b, col, table, c) {
1640
- if (a === b) { return 0; }
1641
- var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1642
- if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1643
- if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1644
- return ts.sortNatural(a, b);
1645
- };
1623
+ sortOn : function( c, list, callback, init ) {
1624
+ var table = c.table;
1625
+ c.$table.trigger( 'sortStart', table );
1626
+ // update header count index
1627
+ ts.updateHeaderSortCount( c, list );
1628
+ // set css for headers
1629
+ ts.setHeadersCss( c );
1630
+ // fixes #346
1631
+ if ( c.delayInit && ts.isEmptyObject( c.cache ) ) {
1632
+ ts.buildCache( c );
1633
+ }
1634
+ c.$table.trigger( 'sortBegin', table );
1635
+ // sort the table and append it to the dom
1636
+ ts.multisort( c );
1637
+ ts.appendCache( c, init );
1638
+ c.$table.trigger( 'sortEnd', table );
1639
+ ts.applyWidget( table );
1640
+ if ( $.isFunction( callback ) ) {
1641
+ callback( table );
1642
+ }
1643
+ },
1646
1644
 
1647
- ts.sortNaturalDesc = function(a, b, col, table, c) {
1648
- if (a === b) { return 0; }
1649
- var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1650
- if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1651
- if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1652
- return ts.sortNatural(b, a);
1653
- };
1645
+ sortReset : function( c, callback ) {
1646
+ c.sortList = [];
1647
+ ts.setHeadersCss( c );
1648
+ ts.multisort( c );
1649
+ ts.appendCache( c );
1650
+ if ( $.isFunction( callback ) ) {
1651
+ callback( c.table );
1652
+ }
1653
+ },
1654
1654
 
1655
- // basic alphabetical sort
1656
- ts.sortText = function(a, b) {
1657
- return a > b ? 1 : (a < b ? -1 : 0);
1658
- };
1655
+ getSortType : function( parsers, column ) {
1656
+ return ( parsers && parsers[ column ] ) ? parsers[ column ].type || '' : '';
1657
+ },
1659
1658
 
1660
- // return text string value by adding up ascii value
1661
- // so the text is somewhat sorted when using a digital sort
1662
- // this is NOT an alphanumeric sort
1663
- ts.getTextValue = function(a, num, mx) {
1664
- if (mx) {
1665
- // make sure the text value is greater than the max numerical value (mx)
1666
- var i, l = a ? a.length : 0, n = mx + num;
1667
- for (i = 0; i < l; i++) {
1668
- n += a.charCodeAt(i);
1669
- }
1670
- return num * n;
1671
- }
1672
- return 0;
1673
- };
1659
+ formatSortingOrder : function( val ) {
1660
+ // look for 'd' in 'desc' order; return true
1661
+ return ( /^d/i.test( val ) || val === 1 );
1662
+ },
1674
1663
 
1675
- ts.sortNumericAsc = function(a, b, num, mx, col, table) {
1676
- if (a === b) { return 0; }
1677
- var c = table.config,
1678
- e = c.string[ (c.empties[col] || c.emptyTo ) ];
1679
- if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1680
- if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1681
- if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1682
- if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1683
- return a - b;
1684
- };
1664
+ // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed)
1665
+ // this function will only accept strings, or you'll see 'TypeError: undefined is not a function'
1666
+ // I could add a = a.toString(); b = b.toString(); but it'll slow down the sort overall
1667
+ sortNatural : function( a, b ) {
1668
+ if ( a === b ) { return 0; }
1669
+ var aNum, bNum, aFloat, bFloat, indx, max,
1670
+ regex = ts.regex;
1671
+ // first try and sort Hex codes
1672
+ if ( regex.hex.test( b ) ) {
1673
+ aNum = parseInt( a.match( regex.hex ), 16 );
1674
+ bNum = parseInt( b.match( regex.hex ), 16 );
1675
+ if ( aNum < bNum ) { return -1; }
1676
+ if ( aNum > bNum ) { return 1; }
1677
+ }
1678
+ // chunk/tokenize
1679
+ aNum = a.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' );
1680
+ bNum = b.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' );
1681
+ max = Math.max( aNum.length, bNum.length );
1682
+ // natural sorting through split numeric strings and default strings
1683
+ for ( indx = 0; indx < max; indx++ ) {
1684
+ // find floats not starting with '0', string or 0 if not defined
1685
+ aFloat = isNaN( aNum[ indx ] ) ? aNum[ indx ] || 0 : parseFloat( aNum[ indx ] ) || 0;
1686
+ bFloat = isNaN( bNum[ indx ] ) ? bNum[ indx ] || 0 : parseFloat( bNum[ indx ] ) || 0;
1687
+ // handle numeric vs string comparison - number < string - (Kyle Adams)
1688
+ if ( isNaN( aFloat ) !== isNaN( bFloat ) ) { return isNaN( aFloat ) ? 1 : -1; }
1689
+ // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
1690
+ if ( typeof aFloat !== typeof bFloat ) {
1691
+ aFloat += '';
1692
+ bFloat += '';
1693
+ }
1694
+ if ( aFloat < bFloat ) { return -1; }
1695
+ if ( aFloat > bFloat ) { return 1; }
1696
+ }
1697
+ return 0;
1698
+ },
1685
1699
 
1686
- ts.sortNumericDesc = function(a, b, num, mx, col, table) {
1687
- if (a === b) { return 0; }
1688
- var c = table.config,
1689
- e = c.string[ (c.empties[col] || c.emptyTo ) ];
1690
- if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1691
- if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1692
- if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1693
- if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1694
- return b - a;
1695
- };
1700
+ sortNaturalAsc : function( a, b, col, c ) {
1701
+ if ( a === b ) { return 0; }
1702
+ var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
1703
+ if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; }
1704
+ if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; }
1705
+ return ts.sortNatural( a, b );
1706
+ },
1696
1707
 
1697
- ts.sortNumeric = function(a, b) {
1698
- return a - b;
1699
- };
1708
+ sortNaturalDesc : function( a, b, col, c ) {
1709
+ if ( a === b ) { return 0; }
1710
+ var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
1711
+ if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; }
1712
+ if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; }
1713
+ return ts.sortNatural( b, a );
1714
+ },
1700
1715
 
1701
- // used when replacing accented characters during sorting
1702
- ts.characterEquivalents = {
1703
- 'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãäąå
1704
- 'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄÅ
1705
- 'c' : '\u00e7\u0107\u010d', // çćč
1706
- 'C' : '\u00c7\u0106\u010c', // ÇĆČ
1707
- 'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę
1708
- 'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ
1709
- 'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı
1710
- 'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ
1711
- 'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6\u014d', // óòôõöō
1712
- 'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÓÒÔÕÖŌ
1713
- 'ss': '\u00df', // ß (s sharp)
1714
- 'SS': '\u1e9e', // ẞ (Capital sharp s)
1715
- 'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů
1716
- 'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ
1717
- };
1718
- ts.replaceAccents = function(s) {
1719
- var a, acc = '[', eq = ts.characterEquivalents;
1720
- if (!ts.characterRegex) {
1721
- ts.characterRegexArray = {};
1722
- for (a in eq) {
1723
- if (typeof a === 'string') {
1724
- acc += eq[a];
1725
- ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g');
1726
- }
1727
- }
1728
- ts.characterRegex = new RegExp(acc + ']');
1729
- }
1730
- if (ts.characterRegex.test(s)) {
1731
- for (a in eq) {
1732
- if (typeof a === 'string') {
1733
- s = s.replace( ts.characterRegexArray[a], a );
1734
- }
1735
- }
1736
- }
1737
- return s;
1738
- };
1716
+ // basic alphabetical sort
1717
+ sortText : function( a, b ) {
1718
+ return a > b ? 1 : ( a < b ? -1 : 0 );
1719
+ },
1739
1720
 
1740
- // *** utilities ***
1741
- ts.isValueInArray = function( column, arry ) {
1721
+ // return text string value by adding up ascii value
1722
+ // so the text is somewhat sorted when using a digital sort
1723
+ // this is NOT an alphanumeric sort
1724
+ getTextValue : function( val, num, max ) {
1725
+ if ( max ) {
1726
+ // make sure the text value is greater than the max numerical value (max)
1742
1727
  var indx,
1743
- len = arry && arry.length || 0;
1728
+ len = val ? val.length : 0,
1729
+ n = max + num;
1744
1730
  for ( indx = 0; indx < len; indx++ ) {
1745
- if ( arry[ indx ][ 0 ] === column ) {
1746
- return indx;
1747
- }
1731
+ n += val.charCodeAt( indx );
1748
1732
  }
1749
- return -1;
1750
- };
1733
+ return num * n;
1734
+ }
1735
+ return 0;
1736
+ },
1751
1737
 
1752
- ts.addParser = function(parser) {
1753
- var i, l = ts.parsers.length, a = true;
1754
- for (i = 0; i < l; i++) {
1755
- if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) {
1756
- a = false;
1757
- }
1758
- }
1759
- if (a) {
1760
- ts.parsers.push(parser);
1761
- }
1762
- };
1738
+ sortNumericAsc : function( a, b, num, max, col, c ) {
1739
+ if ( a === b ) { return 0; }
1740
+ var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
1741
+ if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; }
1742
+ if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; }
1743
+ if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); }
1744
+ if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); }
1745
+ return a - b;
1746
+ },
1763
1747
 
1764
- // Use it to add a set of methods to table.config which will be available for all tables.
1765
- // This should be done before table initialization
1766
- ts.addInstanceMethods = function(methods) {
1767
- $.extend(ts.instanceMethods, methods);
1768
- };
1748
+ sortNumericDesc : function( a, b, num, max, col, c ) {
1749
+ if ( a === b ) { return 0; }
1750
+ var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
1751
+ if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; }
1752
+ if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; }
1753
+ if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); }
1754
+ if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); }
1755
+ return b - a;
1756
+ },
1769
1757
 
1770
- ts.getParserById = function(name) {
1771
- /*jshint eqeqeq:false */
1772
- if (name == 'false') { return false; }
1773
- var i, l = ts.parsers.length;
1774
- for (i = 0; i < l; i++) {
1775
- if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) {
1776
- return ts.parsers[i];
1777
- }
1778
- }
1779
- return false;
1780
- };
1758
+ sortNumeric : function( a, b ) {
1759
+ return a - b;
1760
+ },
1781
1761
 
1782
- ts.addWidget = function(widget) {
1783
- ts.widgets.push(widget);
1784
- };
1762
+ /*
1763
+ ██ ██ ██ ██ █████▄ ▄████▄ ██████ ██████ ▄█████
1764
+ ██ ██ ██ ██ ██ ██ ██ ▄▄▄ ██▄▄ ██ ▀█▄
1765
+ ██ ██ ██ ██ ██ ██ ██ ▀██ ██▀▀ ██ ▀█▄
1766
+ ███████▀ ██ █████▀ ▀████▀ ██████ ██ █████▀
1767
+ */
1768
+ addWidget : function( widget ) {
1769
+ ts.widgets.push( widget );
1770
+ },
1785
1771
 
1786
- ts.hasWidget = function( $table, name ) {
1787
- $table = $( $table );
1788
- return $table.length && $table[0].config && $table[0].config.widgetInit[name] || false;
1789
- };
1772
+ hasWidget : function( $table, name ) {
1773
+ $table = $( $table );
1774
+ return $table.length && $table[ 0 ].config && $table[ 0 ].config.widgetInit[ name ] || false;
1775
+ },
1790
1776
 
1791
- ts.getWidgetById = function(name) {
1792
- var i, w, l = ts.widgets.length;
1793
- for (i = 0; i < l; i++) {
1794
- w = ts.widgets[i];
1795
- if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) {
1796
- return w;
1797
- }
1777
+ getWidgetById : function( name ) {
1778
+ var indx, widget,
1779
+ len = ts.widgets.length;
1780
+ for ( indx = 0; indx < len; indx++ ) {
1781
+ widget = ts.widgets[ indx ];
1782
+ if ( widget && widget.id && widget.id.toLowerCase() === name.toLowerCase() ) {
1783
+ return widget;
1798
1784
  }
1799
- };
1785
+ }
1786
+ },
1800
1787
 
1801
- ts.applyWidgetOptions = function( table, c ){
1802
- var indx, widget,
1803
- len = c.widgets.length;
1804
- if (len) {
1805
- for (indx = 0; indx < len; indx++) {
1806
- widget = ts.getWidgetById( c.widgets[indx] );
1807
- if ( widget && 'options' in widget ) {
1808
- c.widgetOptions = $.extend( true, {}, widget.options, c.widgetOptions );
1809
- }
1788
+ applyWidgetOptions : function( table ) {
1789
+ var indx, widget,
1790
+ c = table.config,
1791
+ len = c.widgets.length;
1792
+ if ( len ) {
1793
+ for ( indx = 0; indx < len; indx++ ) {
1794
+ widget = ts.getWidgetById( c.widgets[ indx ] );
1795
+ if ( widget && widget.options ) {
1796
+ c.widgetOptions = $.extend( true, {}, widget.options, c.widgetOptions );
1810
1797
  }
1811
1798
  }
1812
- };
1799
+ }
1800
+ },
1813
1801
 
1814
- ts.applyWidget = function(table, init, callback) {
1815
- table = $(table)[0]; // in case this is called externally
1816
- var indx, len, names, widget, name, applied,
1817
- c = table.config,
1818
- tableClass = ' ' + c.table.className + ' ',
1819
- widgets = [],
1820
- time, time2, w, wd;
1821
- // prevent numerous consecutive widget applications
1822
- if (init !== false && table.hasInitialized && (table.isApplyingWidgets || table.isUpdating)) { return; }
1823
- if (c.debug) { time = new Date(); }
1824
- // look for widgets to apply from in table class
1802
+ addWidgetFromClass : function( table ) {
1803
+ var len, indx,
1804
+ c = table.config,
1805
+ // look for widgets to apply from table class
1825
1806
  // stop using \b otherwise this matches 'ui-widget-content' & adds 'content' widget
1826
- wd = new RegExp( '\\s' + c.widgetClass.replace( ts.regex.templateName, '([\\w-]+)' ) + '\\s', 'g' );
1827
- if ( tableClass.match( wd ) ) {
1828
- // extract out the widget id from the table class (widget id's can include dashes)
1829
- w = tableClass.match( wd );
1830
- if ( w ) {
1831
- len = w.length;
1832
- for (indx = 0; indx < len; indx++) {
1833
- c.widgets.push( w[indx].replace( wd, '$1' ) );
1834
- }
1835
- }
1807
+ regex = '\\s' + c.widgetClass.replace( ts.regex.templateName, '([\\w-]+)' ) + '\\s',
1808
+ widgetClass = new RegExp( regex, 'g' ),
1809
+ // extract out the widget id from the table class (widget id's can include dashes)
1810
+ widget = ( ' ' + c.table.className + ' ' ).match( widgetClass );
1811
+ if ( widget ) {
1812
+ len = widget.length;
1813
+ for ( indx = 0; indx < len; indx++ ) {
1814
+ c.widgets.push( widget[ indx ].replace( widgetClass, '$1' ) );
1836
1815
  }
1837
- if (c.widgets.length) {
1838
- table.isApplyingWidgets = true;
1839
- // ensure unique widget ids
1840
- c.widgets = $.grep(c.widgets, function(v, k){
1841
- return $.inArray(v, c.widgets) === k;
1842
- });
1843
- names = c.widgets || [];
1844
- len = names.length;
1845
- // build widget array & add priority as needed
1846
- for (indx = 0; indx < len; indx++) {
1847
- wd = ts.getWidgetById(names[indx]);
1848
- if (wd && wd.id) {
1849
- // set priority to 10 if not defined
1850
- if (!wd.priority) { wd.priority = 10; }
1851
- widgets[indx] = wd;
1852
- }
1853
- }
1854
- // sort widgets by priority
1855
- widgets.sort(function(a, b){
1856
- return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1;
1857
- });
1858
- // add/update selected widgets
1859
- len = widgets.length;
1860
- if (c.debug) {
1861
- console[ console.group ? 'group' : 'log' ]( 'Start ' + ( init ? 'initializing' : 'applying' ) + ' widgets' );
1816
+ }
1817
+ },
1818
+
1819
+ applyWidget : function( table, init, callback ) {
1820
+ table = $( table )[ 0 ]; // in case this is called externally
1821
+ var indx, len, names, widget, name, applied, time, time2,
1822
+ c = table.config,
1823
+ widgets = [];
1824
+ // prevent numerous consecutive widget applications
1825
+ if ( init !== false && table.hasInitialized && ( table.isApplyingWidgets || table.isUpdating ) ) {
1826
+ return;
1827
+ }
1828
+ if ( c.debug ) { time = new Date(); }
1829
+ ts.addWidgetFromClass( table );
1830
+ if ( c.widgets.length ) {
1831
+ table.isApplyingWidgets = true;
1832
+ // ensure unique widget ids
1833
+ c.widgets = $.grep( c.widgets, function( val, index ) {
1834
+ return $.inArray( val, c.widgets ) === index;
1835
+ });
1836
+ names = c.widgets || [];
1837
+ len = names.length;
1838
+ // build widget array & add priority as needed
1839
+ for ( indx = 0; indx < len; indx++ ) {
1840
+ widget = ts.getWidgetById( names[ indx ] );
1841
+ if ( widget && widget.id ) {
1842
+ // set priority to 10 if not defined
1843
+ if ( !widget.priority ) { widget.priority = 10; }
1844
+ widgets[ indx ] = widget;
1862
1845
  }
1863
- for (indx = 0; indx < len; indx++) {
1864
- widget = widgets[indx];
1865
- if (widget) {
1866
- name = widget.id;
1867
- applied = false;
1868
- if (c.debug) { time2 = new Date(); }
1869
-
1870
- if ( init || !( c.widgetInit[ name ] ) ) {
1871
- // set init flag first to prevent calling init more than once (e.g. pager)
1872
- c.widgetInit[ name ] = true;
1873
- if (table.hasInitialized) {
1874
- // don't reapply widget options on tablesorter init
1875
- ts.applyWidgetOptions( table, table.config );
1876
- }
1877
- if ( 'init' in widget ) {
1878
- applied = true;
1879
- if (c.debug) {
1880
- console[ console.group ? 'group' : 'log' ]( 'Initializing ' + name + ' widget' );
1881
- }
1882
- widget.init(table, widget, table.config, table.config.widgetOptions);
1883
- }
1846
+ }
1847
+ // sort widgets by priority
1848
+ widgets.sort( function( a, b ) {
1849
+ return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1;
1850
+ });
1851
+ // add/update selected widgets
1852
+ len = widgets.length;
1853
+ if ( c.debug ) {
1854
+ console[ console.group ? 'group' : 'log' ]( 'Start ' + ( init ? 'initializing' : 'applying' ) + ' widgets' );
1855
+ }
1856
+ for ( indx = 0; indx < len; indx++ ) {
1857
+ widget = widgets[ indx ];
1858
+ if ( widget ) {
1859
+ name = widget.id;
1860
+ applied = false;
1861
+ if ( c.debug ) { time2 = new Date(); }
1862
+
1863
+ if ( init || !( c.widgetInit[ name ] ) ) {
1864
+ // set init flag first to prevent calling init more than once (e.g. pager)
1865
+ c.widgetInit[ name ] = true;
1866
+ if ( table.hasInitialized ) {
1867
+ // don't reapply widget options on tablesorter init
1868
+ ts.applyWidgetOptions( table );
1884
1869
  }
1885
- if ( !init && 'format' in widget ) {
1870
+ if ( typeof widget.init === 'function' ) {
1886
1871
  applied = true;
1887
- if (c.debug) {
1888
- console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' );
1872
+ if ( c.debug ) {
1873
+ console[ console.group ? 'group' : 'log' ]( 'Initializing ' + name + ' widget' );
1889
1874
  }
1890
- widget.format(table, table.config, table.config.widgetOptions, false);
1875
+ widget.init( table, widget, table.config, table.config.widgetOptions );
1891
1876
  }
1892
- if (c.debug) {
1893
- if (applied) {
1894
- console.log( 'Completed ' + ( init ? 'initializing ' : 'applying ' ) + name + ' widget' + ts.benchmark( time2 ) );
1895
- if ( console.groupEnd ) { console.groupEnd(); }
1896
- }
1877
+ }
1878
+ if ( !init && typeof widget.format === 'function' ) {
1879
+ applied = true;
1880
+ if ( c.debug ) {
1881
+ console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' );
1882
+ }
1883
+ widget.format( table, table.config, table.config.widgetOptions, false );
1884
+ }
1885
+ if ( c.debug ) {
1886
+ if ( applied ) {
1887
+ console.log( 'Completed ' + ( init ? 'initializing ' : 'applying ' ) + name + ' widget' + ts.benchmark( time2 ) );
1888
+ if ( console.groupEnd ) { console.groupEnd(); }
1897
1889
  }
1898
1890
  }
1899
1891
  }
1900
- if ( c.debug && console.groupEnd ) { console.groupEnd(); }
1901
- // callback executed on init only
1902
- if (!init && typeof callback === 'function') {
1903
- callback(table);
1892
+ }
1893
+ if ( c.debug && console.groupEnd ) { console.groupEnd(); }
1894
+ // callback executed on init only
1895
+ if ( !init && typeof callback === 'function' ) {
1896
+ callback( table );
1897
+ }
1898
+ }
1899
+ setTimeout( function() {
1900
+ table.isApplyingWidgets = false;
1901
+ $.data( table, 'lastWidgetApplication', new Date() );
1902
+ c.$table.trigger('tablesorter-ready');
1903
+ }, 0 );
1904
+ if ( c.debug ) {
1905
+ widget = c.widgets.length;
1906
+ console.log( 'Completed ' +
1907
+ ( init === true ? 'initializing ' : 'applying ' ) + widget +
1908
+ ' widget' + ( widget !== 1 ? 's' : '' ) + ts.benchmark( time ) );
1909
+ }
1910
+ },
1911
+
1912
+ removeWidget : function( table, name, refreshing ) {
1913
+ table = $( table )[ 0 ];
1914
+ var index, widget, indx, len,
1915
+ c = table.config;
1916
+ // if name === true, add all widgets from $.tablesorter.widgets
1917
+ if ( name === true ) {
1918
+ name = [];
1919
+ len = ts.widgets.length;
1920
+ for ( indx = 0; indx < len; indx++ ) {
1921
+ widget = ts.widgets[ indx ];
1922
+ if ( widget && widget.id ) {
1923
+ name.push( widget.id );
1924
+ }
1925
+ }
1926
+ } else {
1927
+ // name can be either an array of widgets names,
1928
+ // or a space/comma separated list of widget names
1929
+ name = ( $.isArray( name ) ? name.join( ',' ) : name || '' ).toLowerCase().split( /[\s,]+/ );
1930
+ }
1931
+ len = name.length;
1932
+ for ( index = 0; index < len; index++ ) {
1933
+ widget = ts.getWidgetById( name[ index ] );
1934
+ indx = $.inArray( name[ index ], c.widgets );
1935
+ if ( widget && widget.remove ) {
1936
+ if ( c.debug ) {
1937
+ console.log( ( refreshing ? 'Refreshing' : 'Removing' ) + ' "' + name[ index ] + '" widget' );
1904
1938
  }
1939
+ widget.remove( table, c, c.widgetOptions, refreshing );
1940
+ c.widgetInit[ name[ index ] ] = false;
1905
1941
  }
1906
- setTimeout(function(){
1907
- table.isApplyingWidgets = false;
1908
- $.data(table, 'lastWidgetApplication', new Date());
1909
- }, 0);
1910
- if (c.debug) {
1911
- w = c.widgets.length;
1912
- console.log( 'Completed ' + (init === true ? 'initializing ' : 'applying ') + w + ' widget' + (w !== 1 ? 's' : '') + ts.benchmark(time) );
1942
+ // don't remove the widget from config.widget if refreshing
1943
+ if ( indx >= 0 && refreshing !== true ) {
1944
+ c.widgets.splice( indx, 1 );
1913
1945
  }
1914
- };
1946
+ }
1947
+ },
1948
+
1949
+ refreshWidgets : function( table, doAll, dontapply ) {
1950
+ table = $( table )[ 0 ]; // see issue #243
1951
+ var indx, widget,
1952
+ c = table.config,
1953
+ curWidgets = c.widgets,
1954
+ widgets = ts.widgets,
1955
+ len = widgets.length,
1956
+ list = [],
1957
+ callback = function( table ) {
1958
+ $( table ).trigger( 'refreshComplete' );
1959
+ };
1960
+ // remove widgets not defined in config.widgets, unless doAll is true
1961
+ for ( indx = 0; indx < len; indx++ ) {
1962
+ widget = widgets[ indx ];
1963
+ if ( widget && widget.id && ( doAll || $.inArray( widget.id, curWidgets ) < 0 ) ) {
1964
+ list.push( widget.id );
1965
+ }
1966
+ }
1967
+ ts.removeWidget( table, list.join( ',' ), true );
1968
+ if ( dontapply !== true ) {
1969
+ // call widget init if
1970
+ ts.applyWidget( table, doAll || false, callback );
1971
+ if ( doAll ) {
1972
+ // apply widget format
1973
+ ts.applyWidget( table, false, callback );
1974
+ }
1975
+ } else {
1976
+ callback( table );
1977
+ }
1978
+ },
1979
+
1980
+ /*
1981
+ ██ ██ ██████ ██ ██ ██ ██████ ██ ██████ ▄█████
1982
+ ██ ██ ██ ██ ██ ██ ██ ██ ██▄▄ ▀█▄
1983
+ ██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀█▄
1984
+ ▀████▀ ██ ██ ██████ ██ ██ ██ ██████ █████▀
1985
+ */
1986
+ benchmark : function( diff ) {
1987
+ return ( ' ( ' + ( new Date().getTime() - diff.getTime() ) + 'ms )' );
1988
+ },
1989
+ // deprecated ts.log
1990
+ log : function() {
1991
+ console.log( arguments );
1992
+ },
1993
+
1994
+ // $.isEmptyObject from jQuery v1.4
1995
+ isEmptyObject : function( obj ) {
1996
+ /*jshint forin: false */
1997
+ for ( var name in obj ) {
1998
+ return false;
1999
+ }
2000
+ return true;
2001
+ },
2002
+
2003
+ isValueInArray : function( column, arry ) {
2004
+ var indx,
2005
+ len = arry && arry.length || 0;
2006
+ for ( indx = 0; indx < len; indx++ ) {
2007
+ if ( arry[ indx ][ 0 ] === column ) {
2008
+ return indx;
2009
+ }
2010
+ }
2011
+ return -1;
2012
+ },
2013
+
2014
+ formatFloat : function( str, table ) {
2015
+ if ( typeof str !== 'string' || str === '' ) { return str; }
2016
+ // allow using formatFloat without a table; defaults to US number format
2017
+ var num,
2018
+ usFormat = table && table.config ? table.config.usNumberFormat !== false :
2019
+ typeof table !== 'undefined' ? table : true;
2020
+ if ( usFormat ) {
2021
+ // US Format - 1,234,567.89 -> 1234567.89
2022
+ str = str.replace( ts.regex.comma, '' );
2023
+ } else {
2024
+ // German Format = 1.234.567,89 -> 1234567.89
2025
+ // French Format = 1 234 567,89 -> 1234567.89
2026
+ str = str.replace( ts.regex.digitNonUS, '' ).replace( ts.regex.comma, '.' );
2027
+ }
2028
+ if ( ts.regex.digitNegativeTest.test( str ) ) {
2029
+ // make (#) into a negative number -> (10) = -10
2030
+ str = str.replace( ts.regex.digitNegativeReplace, '-$1' );
2031
+ }
2032
+ num = parseFloat( str );
2033
+ // return the text instead of zero
2034
+ return isNaN( num ) ? $.trim( str ) : num;
2035
+ },
2036
+
2037
+ isDigit : function( str ) {
2038
+ // replace all unwanted chars and match
2039
+ return isNaN( str ) ?
2040
+ ts.regex.digitTest.test( str.toString().replace( ts.regex.digitReplace, '' ) ) :
2041
+ str !== '';
2042
+ },
1915
2043
 
1916
- ts.removeWidget = function(table, name, refreshing){
1917
- table = $(table)[0];
1918
- var i, widget, indx, len,
1919
- c = table.config;
1920
- // if name === true, add all widgets from $.tablesorter.widgets
1921
- if (name === true) {
1922
- name = [];
1923
- len = ts.widgets.length;
1924
- for (indx = 0; indx < len; indx++) {
1925
- widget = ts.widgets[indx];
1926
- if (widget && widget.id) {
1927
- name.push( widget.id );
2044
+ // computeTableHeaderCellIndexes from:
2045
+ // http://www.javascripttoolbox.com/lib/table/examples.php
2046
+ // http://www.javascripttoolbox.com/temp/table_cellindex.html
2047
+ computeColumnIndex : function( $rows ) {
2048
+ var i, j, k, l, $cell, cell, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol,
2049
+ matrix = [],
2050
+ matrixrow = [];
2051
+ for ( i = 0; i < $rows.length; i++ ) {
2052
+ cells = $rows[ i ].cells;
2053
+ for ( j = 0; j < cells.length; j++ ) {
2054
+ cell = cells[ j ];
2055
+ $cell = $( cell );
2056
+ rowIndex = cell.parentNode.rowIndex;
2057
+ cellId = rowIndex + '-' + $cell.index();
2058
+ rowSpan = cell.rowSpan || 1;
2059
+ colSpan = cell.colSpan || 1;
2060
+ if ( typeof matrix[ rowIndex ] === 'undefined' ) {
2061
+ matrix[ rowIndex ] = [];
2062
+ }
2063
+ // Find first available column in the first row
2064
+ for ( k = 0; k < matrix[ rowIndex ].length + 1; k++ ) {
2065
+ if ( typeof matrix[ rowIndex ][ k ] === 'undefined' ) {
2066
+ firstAvailCol = k;
2067
+ break;
1928
2068
  }
1929
2069
  }
1930
- } else {
1931
- // name can be either an array of widgets names,
1932
- // or a space/comma separated list of widget names
1933
- name = ( $.isArray(name) ? name.join(',') : name || '' ).toLowerCase().split( /[\s,]+/ );
1934
- }
1935
- len = name.length;
1936
- for (i = 0; i < len; i++) {
1937
- widget = ts.getWidgetById(name[i]);
1938
- indx = $.inArray( name[i], c.widgets );
1939
- if ( widget && 'remove' in widget ) {
1940
- if (c.debug && indx >= 0) { console.log( 'Removing "' + name[i] + '" widget' ); }
1941
- if ( c.debug ) {
1942
- console.log( ( refreshing ? 'Refreshing' : 'Removing' ) + ' "' + name[i] + '" widget' );
1943
- }
1944
- widget.remove(table, c, c.widgetOptions, refreshing);
1945
- c.widgetInit[ name[i] ] = false;
2070
+ // add data-column (setAttribute = IE8+)
2071
+ if ( cell.setAttribute ) {
2072
+ cell.setAttribute( 'data-column', firstAvailCol );
2073
+ } else {
2074
+ $cell.attr( 'data-column', firstAvailCol );
1946
2075
  }
1947
- // don't remove the widget from config.widget if refreshing
1948
- if (indx >= 0 && refreshing !== true) {
1949
- c.widgets.splice( indx, 1 );
2076
+ for ( k = rowIndex; k < rowIndex + rowSpan; k++ ) {
2077
+ if ( typeof matrix[ k ] === 'undefined' ) {
2078
+ matrix[ k ] = [];
2079
+ }
2080
+ matrixrow = matrix[ k ];
2081
+ for ( l = firstAvailCol; l < firstAvailCol + colSpan; l++ ) {
2082
+ matrixrow[ l ] = 'x';
2083
+ }
1950
2084
  }
1951
2085
  }
1952
- };
2086
+ }
2087
+ return matrixrow.length;
2088
+ },
1953
2089
 
1954
- ts.refreshWidgets = function(table, doAll, dontapply) {
1955
- table = $(table)[0]; // see issue #243
1956
- var indx,
1957
- c = table.config,
1958
- cw = c.widgets,
1959
- widgets = ts.widgets,
1960
- len = widgets.length,
1961
- list = [],
1962
- callback = function(table){
1963
- $(table).trigger('refreshComplete');
1964
- };
1965
- // remove widgets not defined in config.widgets, unless doAll is true
1966
- for (indx = 0; indx < len; indx++) {
1967
- if (widgets[indx] && widgets[indx].id && (doAll || $.inArray( widgets[indx].id, cw ) < 0)) {
1968
- list.push( widgets[indx].id );
1969
- }
2090
+ // automatically add a colgroup with col elements set to a percentage width
2091
+ fixColumnWidth : function( table ) {
2092
+ table = $( table )[ 0 ];
2093
+ var overallWidth, percent, $tbodies, len, index,
2094
+ c = table.config,
2095
+ $colgroup = c.$table.children( 'colgroup' );
2096
+ // remove plugin-added colgroup, in case we need to refresh the widths
2097
+ if ( $colgroup.length && $colgroup.hasClass( ts.css.colgroup ) ) {
2098
+ $colgroup.remove();
2099
+ }
2100
+ if ( c.widthFixed && c.$table.children( 'colgroup' ).length === 0 ) {
2101
+ $colgroup = $( '<colgroup class="' + ts.css.colgroup + '">' );
2102
+ overallWidth = c.$table.width();
2103
+ // only add col for visible columns - fixes #371
2104
+ $tbodies = c.$tbodies.find( 'tr:first' ).children( ':visible' );
2105
+ len = $tbodies.length;
2106
+ for ( index = 0; index < len; index++ ) {
2107
+ percent = parseInt( ( $tbodies.eq( index ).width() / overallWidth ) * 1000, 10 ) / 10 + '%';
2108
+ $colgroup.append( $( '<col>' ).css( 'width', percent ) );
1970
2109
  }
1971
- ts.removeWidget( table, list.join(','), true );
1972
- if (dontapply !== true) {
1973
- // call widget init if
1974
- ts.applyWidget(table, doAll || false, callback );
1975
- if (doAll) {
1976
- // apply widget format
1977
- ts.applyWidget(table, false, callback);
2110
+ c.$table.prepend( $colgroup );
2111
+ }
2112
+ },
2113
+
2114
+ // get sorter, string, empty, etc options for each column from
2115
+ // jQuery data, metadata, header option or header class name ('sorter-false')
2116
+ // priority = jQuery data > meta > headers option > header class name
2117
+ getData : function( header, configHeader, key ) {
2118
+ var meta, cl4ss,
2119
+ val = '',
2120
+ $header = $( header );
2121
+ if ( !$header.length ) { return ''; }
2122
+ meta = $.metadata ? $header.metadata() : false;
2123
+ cl4ss = ' ' + ( $header.attr( 'class' ) || '' );
2124
+ if ( typeof $header.data( key ) !== 'undefined' ||
2125
+ typeof $header.data( key.toLowerCase() ) !== 'undefined' ) {
2126
+ // 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder'
2127
+ // 'data-sort-initial-order' is assigned to 'sortInitialOrder'
2128
+ val += $header.data( key ) || $header.data( key.toLowerCase() );
2129
+ } else if ( meta && typeof meta[ key ] !== 'undefined' ) {
2130
+ val += meta[ key ];
2131
+ } else if ( configHeader && typeof configHeader[ key ] !== 'undefined' ) {
2132
+ val += configHeader[ key ];
2133
+ } else if ( cl4ss !== ' ' && cl4ss.match( ' ' + key + '-' ) ) {
2134
+ // include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser'
2135
+ val = cl4ss.match( new RegExp( '\\s' + key + '-([\\w-]+)' ) )[ 1 ] || '';
2136
+ }
2137
+ return $.trim( val );
2138
+ },
2139
+
2140
+ getColumnData : function( table, obj, indx, getCell, $headers ) {
2141
+ if ( typeof obj === 'undefined' || obj === null ) { return; }
2142
+ table = $( table )[ 0 ];
2143
+ var $header, key,
2144
+ c = table.config,
2145
+ $cells = ( $headers || c.$headers ),
2146
+ // c.$headerIndexed is not defined initially
2147
+ $cell = c.$headerIndexed && c.$headerIndexed[ indx ] ||
2148
+ $cells.filter( '[data-column="' + indx + '"]:last' );
2149
+ if ( obj[ indx ] ) {
2150
+ return getCell ? obj[ indx ] : obj[ $cells.index( $cell ) ];
2151
+ }
2152
+ for ( key in obj ) {
2153
+ if ( typeof key === 'string' ) {
2154
+ $header = $cell
2155
+ // header cell with class/id
2156
+ .filter( key )
2157
+ // find elements within the header cell with cell/id
2158
+ .add( $cell.find( key ) );
2159
+ if ( $header.length ) {
2160
+ return obj[ key ];
1978
2161
  }
1979
- } else {
1980
- callback(table);
1981
2162
  }
1982
- };
2163
+ }
2164
+ return;
2165
+ },
1983
2166
 
1984
- ts.getColumnText = function( table, column, callback ) {
1985
- table = $( table )[0];
1986
- var tbodyIndex, rowIndex, cache, row, tbodyLen, rowLen, raw, parsed, $cell, result,
1987
- hasCallback = typeof callback === 'function',
1988
- allColumns = column === 'all',
1989
- data = { raw : [], parsed: [], $cell: [] },
1990
- c = table.config;
1991
- if ( ts.isEmptyObject( c ) ) {
1992
- if ( c.debug ) {
1993
- console.warn( 'No cache found - aborting getColumnText function!' );
1994
- }
1995
- } else {
1996
- tbodyLen = c.$tbodies.length;
1997
- for ( tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++ ) {
1998
- cache = c.cache[ tbodyIndex ].normalized;
1999
- rowLen = cache.length;
2000
- for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) {
2001
- result = true;
2002
- row = cache[ rowIndex ];
2003
- parsed = ( allColumns ) ? row.slice(0, c.columns) : row[ column ];
2004
- row = row[ c.columns ];
2005
- raw = ( allColumns ) ? row.raw : row.raw[ column ];
2006
- $cell = ( allColumns ) ? row.$row.children() : row.$row.children().eq( column );
2007
- if ( hasCallback ) {
2008
- result = callback({
2009
- tbodyIndex: tbodyIndex,
2010
- rowIndex: rowIndex,
2011
- parsed: parsed,
2012
- raw: raw,
2013
- $row: row.$row,
2014
- $cell: $cell
2015
- });
2016
- }
2017
- if ( result !== false ) {
2018
- data.parsed.push( parsed );
2019
- data.raw.push( raw );
2020
- data.$cell.push( $cell );
2021
- }
2022
- }
2023
- }
2024
- // return everything
2025
- return data;
2167
+ // *** Process table ***
2168
+ // add processing indicator
2169
+ isProcessing : function( $table, toggle, $ths ) {
2170
+ $table = $( $table );
2171
+ var c = $table[ 0 ].config,
2172
+ // default to all headers
2173
+ $header = $ths || $table.find( '.' + ts.css.header );
2174
+ if ( toggle ) {
2175
+ // don't use sortList if custom $ths used
2176
+ if ( typeof $ths !== 'undefined' && c.sortList.length > 0 ) {
2177
+ // get headers from the sortList
2178
+ $header = $header.filter( function() {
2179
+ // get data-column from attr to keep compatibility with jQuery 1.2.6
2180
+ return this.sortDisabled ?
2181
+ false :
2182
+ ts.isValueInArray( parseFloat( $( this ).attr( 'data-column' ) ), c.sortList ) >= 0;
2183
+ });
2026
2184
  }
2027
- };
2185
+ $table.add( $header ).addClass( ts.css.processing + ' ' + c.cssProcessing );
2186
+ } else {
2187
+ $table.add( $header ).removeClass( ts.css.processing + ' ' + c.cssProcessing );
2188
+ }
2189
+ },
2028
2190
 
2029
- // get sorter, string, empty, etc options for each column from
2030
- // jQuery data, metadata, header option or header class name ('sorter-false')
2031
- // priority = jQuery data > meta > headers option > header class name
2032
- ts.getData = function(h, ch, key) {
2033
- var val = '', $h = $(h), m, cl;
2034
- if (!$h.length) { return ''; }
2035
- m = $.metadata ? $h.metadata() : false;
2036
- cl = ' ' + ($h.attr('class') || '');
2037
- if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){
2038
- // 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder'
2039
- // 'data-sort-initial-order' is assigned to 'sortInitialOrder'
2040
- val += $h.data(key) || $h.data(key.toLowerCase());
2041
- } else if (m && typeof m[key] !== 'undefined') {
2042
- val += m[key];
2043
- } else if (ch && typeof ch[key] !== 'undefined') {
2044
- val += ch[key];
2045
- } else if (cl !== ' ' && cl.match(' ' + key + '-')) {
2046
- // include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser'
2047
- val = cl.match( new RegExp('\\s' + key + '-([\\w-]+)') )[1] || '';
2048
- }
2049
- return $.trim(val);
2050
- };
2191
+ // detach tbody but save the position
2192
+ // don't use tbody because there are portions that look for a tbody index (updateCell)
2193
+ processTbody : function( table, $tb, getIt ) {
2194
+ table = $( table )[ 0 ];
2195
+ if ( getIt ) {
2196
+ table.isProcessing = true;
2197
+ $tb.before( '<colgroup class="tablesorter-savemyplace"/>' );
2198
+ return $.fn.detach ? $tb.detach() : $tb.remove();
2199
+ }
2200
+ var holdr = $( table ).find( 'colgroup.tablesorter-savemyplace' );
2201
+ $tb.insertAfter( holdr );
2202
+ holdr.remove();
2203
+ table.isProcessing = false;
2204
+ },
2051
2205
 
2052
- ts.regex.comma = /,/g;
2053
- ts.regex.digitNonUS = /[\s|\.]/g;
2054
- ts.regex.digitNegativeTest = /^\s*\([.\d]+\)/;
2055
- ts.regex.digitNegativeReplace = /^\s*\(([.\d]+)\)/;
2056
- ts.formatFloat = function(s, table) {
2057
- if (typeof s !== 'string' || s === '') { return s; }
2058
- // allow using formatFloat without a table; defaults to US number format
2059
- var i,
2060
- t = table && table.config ? table.config.usNumberFormat !== false :
2061
- typeof table !== 'undefined' ? table : true;
2062
- if (t) {
2063
- // US Format - 1,234,567.89 -> 1234567.89
2064
- s = s.replace(ts.regex.comma, '');
2065
- } else {
2066
- // German Format = 1.234.567,89 -> 1234567.89
2067
- // French Format = 1 234 567,89 -> 1234567.89
2068
- s = s.replace(ts.regex.digitNonUS, '').replace(ts.regex.comma, '.');
2206
+ clearTableBody : function( table ) {
2207
+ $( table )[ 0 ].config.$tbodies.children().detach();
2208
+ },
2209
+
2210
+ // used when replacing accented characters during sorting
2211
+ characterEquivalents : {
2212
+ 'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãäąå
2213
+ 'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄÅ
2214
+ 'c' : '\u00e7\u0107\u010d', // çćč
2215
+ 'C' : '\u00c7\u0106\u010c', // ÇĆČ
2216
+ 'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę
2217
+ 'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ
2218
+ 'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı
2219
+ 'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ
2220
+ 'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6\u014d', // óòôõöō
2221
+ 'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÓÒÔÕÖŌ
2222
+ 'ss': '\u00df', // ß (s sharp)
2223
+ 'SS': '\u1e9e', // ẞ (Capital sharp s)
2224
+ 'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů
2225
+ 'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ
2226
+ },
2227
+
2228
+ replaceAccents : function( str ) {
2229
+ var chr,
2230
+ acc = '[',
2231
+ eq = ts.characterEquivalents;
2232
+ if ( !ts.characterRegex ) {
2233
+ ts.characterRegexArray = {};
2234
+ for ( chr in eq ) {
2235
+ if ( typeof chr === 'string' ) {
2236
+ acc += eq[ chr ];
2237
+ ts.characterRegexArray[ chr ] = new RegExp( '[' + eq[ chr ] + ']', 'g' );
2238
+ }
2069
2239
  }
2070
- if (ts.regex.digitNegativeTest.test(s)) {
2071
- // make (#) into a negative number -> (10) = -10
2072
- s = s.replace(ts.regex.digitNegativeReplace, '-$1');
2240
+ ts.characterRegex = new RegExp( acc + ']' );
2241
+ }
2242
+ if ( ts.characterRegex.test( str ) ) {
2243
+ for ( chr in eq ) {
2244
+ if ( typeof chr === 'string' ) {
2245
+ str = str.replace( ts.characterRegexArray[ chr ], chr );
2246
+ }
2073
2247
  }
2074
- i = parseFloat(s);
2075
- // return the text instead of zero
2076
- return isNaN(i) ? $.trim(s) : i;
2077
- };
2248
+ }
2249
+ return str;
2250
+ },
2078
2251
 
2079
- ts.regex.digitTest = /^[\-+(]?\d+[)]?$/;
2080
- ts.regex.digitReplace = /[,.'"\s]/g;
2081
- ts.isDigit = function(s) {
2082
- // replace all unwanted chars and match
2083
- return isNaN(s) ?
2084
- ts.regex.digitTest.test( s.toString().replace( ts.regex.digitReplace, '' ) ) :
2085
- s !== '';
2086
- };
2252
+ // restore headers
2253
+ restoreHeaders : function( table ) {
2254
+ var index, $cell,
2255
+ c = $( table )[ 0 ].config,
2256
+ $headers = c.$table.find( c.selectorHeaders ),
2257
+ len = $headers.length;
2258
+ // don't use c.$headers here in case header cells were swapped
2259
+ for ( index = 0; index < len; index++ ) {
2260
+ $cell = $headers.eq( index );
2261
+ // only restore header cells if it is wrapped
2262
+ // because this is also used by the updateAll method
2263
+ if ( $cell.find( '.' + ts.css.headerIn ).length ) {
2264
+ $cell.html( c.headerContent[ index ] );
2265
+ }
2266
+ }
2267
+ },
2087
2268
 
2088
- }()
2089
- });
2269
+ destroy : function( table, removeClasses, callback ) {
2270
+ table = $( table )[ 0 ];
2271
+ if ( !table.hasInitialized ) { return; }
2272
+ // remove all widgets
2273
+ ts.removeWidget( table, true, false );
2274
+ var events,
2275
+ $t = $( table ),
2276
+ c = table.config,
2277
+ debug = c.debug,
2278
+ $h = $t.find( 'thead:first' ),
2279
+ $r = $h.find( 'tr.' + ts.css.headerRow ).removeClass( ts.css.headerRow + ' ' + c.cssHeaderRow ),
2280
+ $f = $t.find( 'tfoot:first > tr' ).children( 'th, td' );
2281
+ if ( removeClasses === false && $.inArray( 'uitheme', c.widgets ) >= 0 ) {
2282
+ // reapply uitheme classes, in case we want to maintain appearance
2283
+ $t.trigger( 'applyWidgetId', [ 'uitheme' ] );
2284
+ $t.trigger( 'applyWidgetId', [ 'zebra' ] );
2285
+ }
2286
+ // remove widget added rows, just in case
2287
+ $h.find( 'tr' ).not( $r ).remove();
2288
+ // disable tablesorter
2289
+ events = 'sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton ' +
2290
+ 'appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave keypress ' +
2291
+ 'sortBegin sortEnd resetToLoadState '.split( ' ' )
2292
+ .join( c.namespace + ' ' );
2293
+ $t
2294
+ .removeData( 'tablesorter' )
2295
+ .unbind( events.replace( ts.regex.spaces, ' ' ) );
2296
+ c.$headers
2297
+ .add( $f )
2298
+ .removeClass( [ ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone ].join( ' ' ) )
2299
+ .removeAttr( 'data-column' )
2300
+ .removeAttr( 'aria-label' )
2301
+ .attr( 'aria-disabled', 'true' );
2302
+ $r
2303
+ .find( c.selectorSort )
2304
+ .unbind( ( 'mousedown mouseup keypress '.split( ' ' ).join( c.namespace + ' ' ) ).replace( ts.regex.spaces, ' ' ) );
2305
+ ts.restoreHeaders( table );
2306
+ $t.toggleClass( ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false );
2307
+ // clear flag in case the plugin is initialized again
2308
+ table.hasInitialized = false;
2309
+ delete table.config.cache;
2310
+ if ( typeof callback === 'function' ) {
2311
+ callback( table );
2312
+ }
2313
+ if ( debug ) {
2314
+ console.log( 'tablesorter has been removed' );
2315
+ }
2316
+ }
2090
2317
 
2091
- // make shortcut
2092
- var ts = $.tablesorter;
2318
+ };
2093
2319
 
2094
- // extend plugin scope
2095
- $.fn.extend({
2096
- tablesorter: ts.construct
2097
- });
2320
+ $.fn.tablesorter = function( settings ) {
2321
+ return this.each( function() {
2322
+ var table = this,
2323
+ // merge & extend config options
2324
+ c = $.extend( true, {}, ts.defaults, settings, ts.instanceMethods );
2325
+ // save initial settings
2326
+ c.originalSettings = settings;
2327
+ // create a table from data (build table widget)
2328
+ if ( !table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE' ) {
2329
+ // return the table (in case the original target is the table's container)
2330
+ ts.buildTable( table, c );
2331
+ } else {
2332
+ ts.setup( table, c );
2333
+ }
2334
+ });
2335
+ };
2098
2336
 
2099
2337
  // set up debug logs
2100
2338
  if ( !( window.console && window.console.log ) ) {
2339
+ // access $.tablesorter.logs for browsers that don't have a console...
2101
2340
  ts.logs = [];
2102
2341
  /*jshint -W020 */
2103
2342
  console = {};
2104
2343
  console.log = console.warn = console.error = console.table = function() {
2105
- ts.logs.push( [ Date.now(), arguments ] );
2344
+ var arg = arguments.length > 1 ? arguments : arguments[0];
2345
+ ts.logs.push({ date: Date.now(), log: arg });
2106
2346
  };
2107
2347
  }
2108
2348
 
2109
- ts.log = function(){
2110
- console.log( arguments );
2111
- };
2112
- ts.benchmark = function( diff ) {
2113
- return ( ' (' + ( new Date().getTime() - diff.getTime() ) + 'ms)' );
2114
- };
2115
-
2116
2349
  // add default parsers
2117
2350
  ts.addParser({
2118
- id: 'no-parser',
2119
- is: function() {
2351
+ id : 'no-parser',
2352
+ is : function() {
2120
2353
  return false;
2121
2354
  },
2122
- format: function() {
2355
+ format : function() {
2123
2356
  return '';
2124
2357
  },
2125
- type: 'text'
2358
+ type : 'text'
2126
2359
  });
2127
2360
 
2128
2361
  ts.addParser({
2129
- id: 'text',
2130
- is: function() {
2362
+ id : 'text',
2363
+ is : function() {
2131
2364
  return true;
2132
2365
  },
2133
- format: function(s, table) {
2366
+ format : function( str, table ) {
2134
2367
  var c = table.config;
2135
- if (s) {
2136
- s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s );
2137
- s = c.sortLocaleCompare ? ts.replaceAccents(s) : s;
2368
+ if ( str ) {
2369
+ str = $.trim( c.ignoreCase ? str.toLocaleLowerCase() : str );
2370
+ str = c.sortLocaleCompare ? ts.replaceAccents( str ) : str;
2138
2371
  }
2139
- return s;
2372
+ return str;
2140
2373
  },
2141
- type: 'text'
2374
+ type : 'text'
2142
2375
  });
2143
2376
 
2144
2377
  ts.regex.nondigit = /[^\w,. \-()]/g;
2145
2378
  ts.addParser({
2146
- id: 'digit',
2147
- is: function(s) {
2148
- return ts.isDigit(s);
2379
+ id : 'digit',
2380
+ is : function( str ) {
2381
+ return ts.isDigit( str );
2149
2382
  },
2150
- format: function(s, table) {
2151
- var n = ts.formatFloat((s || '').replace(ts.regex.nondigit, ''), table);
2152
- return s && typeof n === 'number' ? n :
2153
- s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
2383
+ format : function( str, table ) {
2384
+ var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table );
2385
+ return str && typeof num === 'number' ? num :
2386
+ str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str;
2154
2387
  },
2155
- type: 'numeric'
2388
+ type : 'numeric'
2156
2389
  });
2157
2390
 
2158
2391
  ts.regex.currencyReplace = /[+\-,. ]/g;
2159
2392
  ts.regex.currencyTest = /^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/;
2160
2393
  ts.addParser({
2161
- id: 'currency',
2162
- is: function(s) {
2163
- s = (s || '').replace(ts.regex.currencyReplace, '');
2394
+ id : 'currency',
2395
+ is : function( str ) {
2396
+ str = ( str || '' ).replace( ts.regex.currencyReplace, '' );
2164
2397
  // test for £$€¤¥¢
2165
- return ts.regex.currencyTest.test(s);
2398
+ return ts.regex.currencyTest.test( str );
2166
2399
  },
2167
- format: function(s, table) {
2168
- var n = ts.formatFloat((s || '').replace(ts.regex.nondigit, ''), table);
2169
- return s && typeof n === 'number' ? n :
2170
- s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
2400
+ format : function( str, table ) {
2401
+ var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table );
2402
+ return str && typeof num === 'number' ? num :
2403
+ str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str;
2171
2404
  },
2172
- type: 'numeric'
2405
+ type : 'numeric'
2173
2406
  });
2174
2407
 
2175
2408
  // too many protocols to add them all https://en.wikipedia.org/wiki/URI_scheme
@@ -2177,72 +2410,72 @@
2177
2410
  ts.regex.urlProtocolTest = /^(https?|ftp|file):\/\//;
2178
2411
  ts.regex.urlProtocolReplace = /(https?|ftp|file):\/\//;
2179
2412
  ts.addParser({
2180
- id: 'url',
2181
- is: function(s) {
2182
- return ts.regex.urlProtocolTest.test(s);
2413
+ id : 'url',
2414
+ is : function( str ) {
2415
+ return ts.regex.urlProtocolTest.test( str );
2183
2416
  },
2184
- format: function(s) {
2185
- return s ? $.trim(s.replace(ts.regex.urlProtocolReplace, '')) : s;
2417
+ format : function( str ) {
2418
+ return str ? $.trim( str.replace( ts.regex.urlProtocolReplace, '' ) ) : str;
2186
2419
  },
2187
2420
  parsed : true, // filter widget flag
2188
- type: 'text'
2421
+ type : 'text'
2189
2422
  });
2190
2423
 
2191
2424
  ts.regex.dash = /-/g;
2192
2425
  ts.regex.isoDate = /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/;
2193
2426
  ts.addParser({
2194
- id: 'isoDate',
2195
- is: function(s) {
2196
- return ts.regex.isoDate.test(s);
2427
+ id : 'isoDate',
2428
+ is : function( str ) {
2429
+ return ts.regex.isoDate.test( str );
2197
2430
  },
2198
- format: function(s, table) {
2199
- var date = s ? new Date( s.replace(ts.regex.dash, '/') ) : s;
2200
- return date instanceof Date && isFinite(date) ? date.getTime() : s;
2431
+ format : function( str, table ) {
2432
+ var date = str ? new Date( str.replace( ts.regex.dash, '/' ) ) : str;
2433
+ return date instanceof Date && isFinite( date ) ? date.getTime() : str;
2201
2434
  },
2202
- type: 'numeric'
2435
+ type : 'numeric'
2203
2436
  });
2204
2437
 
2205
2438
  ts.regex.percent = /%/g;
2206
2439
  ts.regex.percentTest = /(\d\s*?%|%\s*?\d)/;
2207
2440
  ts.addParser({
2208
- id: 'percent',
2209
- is: function(s) {
2210
- return ts.regex.percentTest.test(s) && s.length < 15;
2441
+ id : 'percent',
2442
+ is : function( str ) {
2443
+ return ts.regex.percentTest.test( str ) && str.length < 15;
2211
2444
  },
2212
- format: function(s, table) {
2213
- return s ? ts.formatFloat(s.replace(ts.regex.percent, ''), table) : s;
2445
+ format : function( str, table ) {
2446
+ return str ? ts.formatFloat( str.replace( ts.regex.percent, '' ), table ) : str;
2214
2447
  },
2215
- type: 'numeric'
2448
+ type : 'numeric'
2216
2449
  });
2217
2450
 
2218
2451
  // added image parser to core v2.17.9
2219
2452
  ts.addParser({
2220
- id: 'image',
2221
- is: function(s, table, node, $node){
2222
- return $node.find('img').length > 0;
2453
+ id : 'image',
2454
+ is : function( str, table, node, $node ) {
2455
+ return $node.find( 'img' ).length > 0;
2223
2456
  },
2224
- format: function(s, table, cell) {
2225
- return $(cell).find('img').attr(table.config.imgAttr || 'alt') || s;
2457
+ format : function( str, table, cell ) {
2458
+ return $( cell ).find( 'img' ).attr( table.config.imgAttr || 'alt' ) || str;
2226
2459
  },
2227
2460
  parsed : true, // filter widget flag
2228
- type: 'text'
2461
+ type : 'text'
2229
2462
  });
2230
2463
 
2231
2464
  ts.regex.dateReplace = /(\S)([AP]M)$/i; // used by usLongDate & time parser
2232
2465
  ts.regex.usLongDateTest1 = /^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i;
2233
2466
  ts.regex.usLongDateTest2 = /^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i;
2234
2467
  ts.addParser({
2235
- id: 'usLongDate',
2236
- is: function(s) {
2468
+ id : 'usLongDate',
2469
+ is : function( str ) {
2237
2470
  // two digit years are not allowed cross-browser
2238
2471
  // Jan 01, 2013 12:34:56 PM or 01 Jan 2013
2239
- return ts.regex.usLongDateTest1.test(s) || ts.regex.usLongDateTest2.test(s);
2472
+ return ts.regex.usLongDateTest1.test( str ) || ts.regex.usLongDateTest2.test( str );
2240
2473
  },
2241
- format: function(s, table) {
2242
- var date = s ? new Date( s.replace(ts.regex.dateReplace, '$1 $2') ) : s;
2243
- return date instanceof Date && isFinite(date) ? date.getTime() : s;
2474
+ format : function( str, table ) {
2475
+ var date = str ? new Date( str.replace( ts.regex.dateReplace, '$1 $2' ) ) : str;
2476
+ return date instanceof Date && isFinite( date ) ? date.getTime() : str;
2244
2477
  },
2245
- type: 'numeric'
2478
+ type : 'numeric'
2246
2479
  });
2247
2480
 
2248
2481
  // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included
@@ -2252,100 +2485,126 @@
2252
2485
  // XXY covers MDY & DMY formats
2253
2486
  ts.regex.shortDateXXY = /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/;
2254
2487
  ts.regex.shortDateYMD = /(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/;
2488
+ ts.convertFormat = function( dateString, format ) {
2489
+ dateString = ( dateString || '' )
2490
+ .replace( ts.regex.spaces, ' ' )
2491
+ .replace( ts.regex.shortDateReplace, '/' );
2492
+ if ( format === 'mmddyyyy' ) {
2493
+ dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$1/$2' );
2494
+ } else if ( format === 'ddmmyyyy' ) {
2495
+ dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$2/$1' );
2496
+ } else if ( format === 'yyyymmdd' ) {
2497
+ dateString = dateString.replace( ts.regex.shortDateYMD, '$1/$2/$3' );
2498
+ }
2499
+ var date = new Date( dateString );
2500
+ return date instanceof Date && isFinite( date ) ? date.getTime() : '';
2501
+ };
2502
+
2255
2503
  ts.addParser({
2256
- id: 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd'
2257
- is: function(s) {
2258
- s = (s || '').replace(ts.regex.spaces, ' ').replace(ts.regex.shortDateReplace, '/');
2259
- return ts.regex.shortDateTest.test(s);
2260
- },
2261
- format: function(s, table, cell, cellIndex) {
2262
- if (s) {
2263
- var date, d,
2264
- c = table.config,
2265
- ci = c.$headerIndexed[ cellIndex ],
2266
- format = ci.length && ci[0].dateFormat ||
2267
- ts.getData( ci, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat') ||
2504
+ id : 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd'
2505
+ is : function( str ) {
2506
+ str = ( str || '' ).replace( ts.regex.spaces, ' ' ).replace( ts.regex.shortDateReplace, '/' );
2507
+ return ts.regex.shortDateTest.test( str );
2508
+ },
2509
+ format : function( str, table, cell, cellIndex ) {
2510
+ if ( str ) {
2511
+ var c = table.config,
2512
+ $header = c.$headerIndexed[ cellIndex ],
2513
+ format = $header.length && $header.data( 'dateFormat' ) ||
2514
+ ts.getData( $header, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat' ) ||
2268
2515
  c.dateFormat;
2269
- d = s.replace(ts.regex.spaces, ' ').replace(ts.regex.shortDateReplace, '/');
2270
- if (format === 'mmddyyyy') {
2271
- d = d.replace(ts.regex.shortDateXXY, '$3/$1/$2');
2272
- } else if (format === 'ddmmyyyy') {
2273
- d = d.replace(ts.regex.shortDateXXY, '$3/$2/$1');
2274
- } else if (format === 'yyyymmdd') {
2275
- d = d.replace(ts.regex.shortDateYMD, '$1/$2/$3');
2516
+ // save format because getData can be slow...
2517
+ if ( $header.length ) {
2518
+ $header.data( 'dateFormat', format );
2276
2519
  }
2277
- date = new Date(d);
2278
- return date instanceof Date && isFinite(date) ? date.getTime() : s;
2520
+ return ts.convertFormat( str, format ) || str;
2279
2521
  }
2280
- return s;
2522
+ return str;
2281
2523
  },
2282
- type: 'numeric'
2524
+ type : 'numeric'
2283
2525
  });
2284
2526
 
2285
- ts.regex.timeTest = /^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i;
2527
+ // match 24 hour time & 12 hours time + am/pm - see http://regexr.com/3c3tk
2528
+ ts.regex.timeTest = /^([1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)$/i;
2529
+ ts.regex.timeMatch = /([1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)/i;
2286
2530
  ts.addParser({
2287
- id: 'time',
2288
- is: function(s) {
2289
- return ts.regex.timeTest.test(s);
2531
+ id : 'time',
2532
+ is : function( str ) {
2533
+ return ts.regex.timeTest.test( str );
2290
2534
  },
2291
- format: function(s, table) {
2292
- var date = s ? new Date( '2000/01/01 ' + s.replace(ts.regex.dateReplace, '$1 $2') ) : s;
2293
- return date instanceof Date && isFinite(date) ? date.getTime() : s;
2535
+ format : function( str, table ) {
2536
+ // isolate time... ignore month, day and year
2537
+ var temp,
2538
+ timePart = ( str || '' ).match( ts.regex.timeMatch ),
2539
+ orig = new Date( str ),
2540
+ // no time component? default to 00:00 by leaving it out, but only if str is defined
2541
+ time = str && ( timePart !== null ? timePart[ 0 ] : '00:00 AM' ),
2542
+ date = time ? new Date( '2000/01/01 ' + time.replace( ts.regex.dateReplace, '$1 $2' ) ) : time;
2543
+ if ( date instanceof Date && isFinite( date ) ) {
2544
+ temp = orig instanceof Date && isFinite( orig ) ? orig.getTime() : 0;
2545
+ // if original string was a valid date, add it to the decimal so the column sorts in some kind of order
2546
+ // luckily new Date() ignores the decimals
2547
+ return temp ? parseFloat( date.getTime() + '.' + orig.getTime() ) : date.getTime();
2548
+ }
2549
+ return str;
2294
2550
  },
2295
- type: 'numeric'
2551
+ type : 'numeric'
2296
2552
  });
2297
2553
 
2298
2554
  ts.addParser({
2299
- id: 'metadata',
2300
- is: function() {
2555
+ id : 'metadata',
2556
+ is : function() {
2301
2557
  return false;
2302
2558
  },
2303
- format: function(s, table, cell) {
2559
+ format : function( str, table, cell ) {
2304
2560
  var c = table.config,
2305
- p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
2306
- return $(cell).metadata()[p];
2561
+ p = ( !c.parserMetadataName ) ? 'sortValue' : c.parserMetadataName;
2562
+ return $( cell ).metadata()[ p ];
2307
2563
  },
2308
- type: 'numeric'
2564
+ type : 'numeric'
2309
2565
  });
2310
2566
 
2567
+ /*
2568
+ ██████ ██████ █████▄ █████▄ ▄████▄
2569
+ ▄█▀ ██▄▄ ██▄▄██ ██▄▄██ ██▄▄██
2570
+ ▄█▀ ██▀▀ ██▀▀██ ██▀▀█ ██▀▀██
2571
+ ██████ ██████ █████▀ ██ ██ ██ ██
2572
+ */
2311
2573
  // add default widgets
2312
2574
  ts.addWidget({
2313
- id: 'zebra',
2314
- priority: 90,
2315
- format: function(table, c, wo) {
2316
- var $tv, $tr, row, even, time, k, i, len,
2317
- child = new RegExp(c.cssChildRow, 'i'),
2318
- b = c.$tbodies.add( $( c.namespace + '_extra_table' ).children( 'tbody:not(.' + c.cssInfoBlock + ')' ) );
2319
- if (c.debug) {
2320
- time = new Date();
2321
- }
2322
- for (k = 0; k < b.length; k++ ) {
2575
+ id : 'zebra',
2576
+ priority : 90,
2577
+ format : function( table, c, wo ) {
2578
+ var $visibleRows, $row, count, isEven, tbodyIndex, rowIndex, len,
2579
+ child = new RegExp( c.cssChildRow, 'i' ),
2580
+ $tbodies = c.$tbodies.add( $( c.namespace + '_extra_table' ).children( 'tbody:not(.' + c.cssInfoBlock + ')' ) );
2581
+ for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
2323
2582
  // loop through the visible rows
2324
- row = 0;
2325
- $tv = b.eq( k ).children( 'tr:visible' ).not( c.selectorRemove );
2326
- len = $tv.length;
2327
- for ( i = 0; i < len; i++ ) {
2328
- $tr = $tv.eq( i );
2583
+ count = 0;
2584
+ $visibleRows = $tbodies.eq( tbodyIndex ).children( 'tr:visible' ).not( c.selectorRemove );
2585
+ len = $visibleRows.length;
2586
+ for ( rowIndex = 0; rowIndex < len; rowIndex++ ) {
2587
+ $row = $visibleRows.eq( rowIndex );
2329
2588
  // style child rows the same way the parent row was styled
2330
- if ( !child.test( $tr[0].className ) ) { row++; }
2331
- even = ( row % 2 === 0 );
2332
- $tr
2333
- .removeClass( wo.zebra[ even ? 1 : 0 ] )
2334
- .addClass( wo.zebra[ even ? 0 : 1 ] );
2589
+ if ( !child.test( $row[ 0 ].className ) ) { count++; }
2590
+ isEven = ( count % 2 === 0 );
2591
+ $row
2592
+ .removeClass( wo.zebra[ isEven ? 1 : 0 ] )
2593
+ .addClass( wo.zebra[ isEven ? 0 : 1 ] );
2335
2594
  }
2336
2595
  }
2337
2596
  },
2338
- remove: function(table, c, wo, refreshing){
2339
- if (refreshing) { return; }
2340
- var k, $tb,
2341
- b = c.$tbodies,
2342
- rmv = (wo.zebra || [ 'even', 'odd' ]).join(' ');
2343
- for (k = 0; k < b.length; k++ ){
2344
- $tb = ts.processTbody(table, b.eq(k), true); // remove tbody
2345
- $tb.children().removeClass(rmv);
2346
- ts.processTbody(table, $tb, false); // restore tbody
2597
+ remove : function( table, c, wo, refreshing ) {
2598
+ if ( refreshing ) { return; }
2599
+ var tbodyIndex, $tbody,
2600
+ $tbodies = c.$tbodies,
2601
+ toRemove = ( wo.zebra || [ 'even', 'odd' ] ).join( ' ' );
2602
+ for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ){
2603
+ $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody
2604
+ $tbody.children().removeClass( toRemove );
2605
+ ts.processTbody( table, $tbody, false ); // restore tbody
2347
2606
  }
2348
2607
  }
2349
2608
  });
2350
2609
 
2351
- })(jQuery);
2610
+ })( jQuery );