PageSourceSearch

https://www.lesedc.org/global/vendor/mottie/tablesorter/js/jquery.tablesorter.js

js lesedc.org collected 2026-10-01 14:47:05 UTC 107,814 bytes, 2,914 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.