PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.9
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.9
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / js / tablesorter / jquery.tablesorter.js

jquery.tablesorter.js in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 3.2.9, at js/tablesorter/jquery.tablesorter.js

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