@atlassian/aui 9.4.6-0c8cd15d6 → 9.5.0

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