Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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