PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.21
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.21
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.21, at js/tablesorter/jquery.tablesorter.js

2,925 lines 105.9 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></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 .off( 'sortBegin' + c.namespace + ' sortEnd' + c.namespace )
338 .on( '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 .off( events.replace( ts.regex.spaces, ' ' ) )
372 .on( '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 .on( 'updateAll' + namespace, function( e, resort, callback ) {
388 e.stopPropagation();
389 ts.updateAll( this.config, resort, callback );
390 })
391 .on( 'update' + namespace + ' updateRows' + namespace, function( e, resort, callback ) {
392 e.stopPropagation();
393 ts.update( this.config, resort, callback );
394 })
395 .on( 'updateHeaders' + namespace, function( e, callback ) {
396 e.stopPropagation();
397 ts.updateHeaders( this.config, callback );
398 })
399 .on( 'updateCell' + namespace, function( e, cell, resort, callback ) {
400 e.stopPropagation();
401 ts.updateCell( this.config, cell, resort, callback );
402 })
403 .on( 'addRows' + namespace, function( e, $row, resort, callback ) {
404 e.stopPropagation();
405 ts.addRows( this.config, $row, resort, callback );
406 })
407 .on( 'updateComplete' + namespace, function() {
408 this.isUpdating = false;
409 })
410 .on( 'sorton' + namespace, function( e, list, callback, init ) {
411 e.stopPropagation();
412 ts.sortOn( this.config, list, callback, init );
413 })
414 .on( 'appendCache' + namespace, function( e, callback, init ) {
415 e.stopPropagation();
416 ts.appendCache( this.config, init );
417 if ( typeof callback === 'function' ) {
418 callback( this );
419 }
420 })
421 // $tbodies variable is used by the tbody sorting widget
422 .on( 'updateCache' + namespace, function( e, callback, $tbodies ) {
423 e.stopPropagation();
424 ts.updateCache( this.config, callback, $tbodies );
425 })
426 .on( 'applyWidgetId' + namespace, function( e, id ) {
427 e.stopPropagation();
428 ts.applyWidgetId( this, id );
429 })
430 .on( 'applyWidgets' + namespace, function( e, callback ) {
431 e.stopPropagation();
432 // apply widgets (false = not initializing)
433 ts.applyWidget( this, false, callback );
434 })
435 .on( 'refreshWidgets' + namespace, function( e, all, dontapply ) {
436 e.stopPropagation();
437 ts.refreshWidgets( this, all, dontapply );
438 })
439 .on( 'removeWidget' + namespace, function( e, name, refreshing ) {
440 e.stopPropagation();
441 ts.removeWidget( this, name, refreshing );
442 })
443 .on( 'destroy' + namespace, function( e, removeClasses, callback ) {
444 e.stopPropagation();
445 ts.destroy( this, removeClasses, callback );
446 })
447 .on( '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 .off( tmp )
485 .on( 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 .on( '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 elementText = '';
830 if ( typeof extract === 'string' ) {
831 // check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow!
832 // http://www.kellegous.com/j/2013/02/27/innertext-vs-textcontent/
833 if ( extract === 'basic' && typeof ( tmp = $node.attr( c.textAttribute ) ) !== 'undefined' ) {
834 return typeof tmp === 'string' ? tmp.trim() : tmp;
835 }
836 return (node.textContent || $node.text()).trim();
837 } else {
838 if ( typeof extract === 'function' ) {
839 elementText = extract( $node[ 0 ], c.table, cellIndex );
840 return typeof elementText === 'string' ? elementText.trim() : elementText;
841 } else if ( typeof ( tmp = ts.getColumnData( c.table, extract, cellIndex ) ) === 'function' ) {
842 elementText = tmp( $node[ 0 ], c.table, cellIndex );
843 return typeof elementText === 'string' ? elementText.trim() : elementText;
844 }
845 }
846 // fallback
847 return ($node[ 0 ].textContent || $node.text()).trim();
848 },
849
850 // centralized function to extract/parse cell contents
851 getParsedText : function( c, cell, colIndex, txt ) {
852 if ( typeof txt === 'undefined' ) {
853 txt = ts.getElementText( c, cell, colIndex );
854 }
855 // if no parser, make sure to return the txt
856 var val = '' + txt,
857 parser = c.parsers[ colIndex ],
858 extractor = c.extractors[ colIndex ];
859 if ( parser ) {
860 // do extract before parsing, if there is one
861 if ( extractor && typeof extractor.format === 'function' ) {
862 txt = extractor.format( txt, c.table, cell, colIndex );
863 }
864 // allow parsing if the string is empty, previously parsing would change it to zero,
865 // in case the parser needs to extract data from the table cell attributes
866 val = parser.id === 'no-parser' ? '' :
867 // make sure txt is a string (extractor may have converted it)
868 parser.format( '' + txt, c.table, cell, colIndex );
869 if ( c.ignoreCase && typeof val === 'string' ) {
870 val = val.toLowerCase();
871 }
872 }
873 return val;
874 },
875
876 /*
877 ▄████▄ ▄████▄ ▄████▄ ██ ██ ██████
878 ██ ▀▀ ██▄▄██ ██ ▀▀ ██▄▄██ ██▄▄
879 ██ ▄▄ ██▀▀██ ██ ▄▄ ██▀▀██ ██▀▀
880 ▀████▀ ██ ██ ▀████▀ ██ ██ ██████
881 */
882 buildCache : function( c, callback, $tbodies ) {
883 var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row,
884 cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData,
885 colMax, span, cacheIndex, hasParser, max, len, index,
886 table = c.table,
887 parsers = c.parsers,
888 debug = ts.debug(c, 'core');
889 // update tbody variable
890 c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' );
891 $tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies,
892 c.cache = {};
893 c.totalRows = 0;
894 // if no parsers found, return - it's an empty table.
895 if ( !parsers ) {
896 return debug ? console.warn( 'Warning: *Empty table!* Not building a cache' ) : '';
897 }
898 if ( debug ) {
899 cacheTime = new Date();
900 }
901 // processing icon
902 if ( c.showProcessing ) {
903 ts.isProcessing( table, true );
904 }
905 for ( tbodyIndex = 0; tbodyIndex < $tbody.length; tbodyIndex++ ) {
906 colMax = []; // column max value per tbody
907 cache = c.cache[ tbodyIndex ] = {
908 normalized: [] // array of normalized row data; last entry contains 'rowData' above
909 // colMax: # // added at the end
910 };
911
912 totalRows = ( $tbody[ tbodyIndex ] && $tbody[ tbodyIndex ].rows.length ) || 0;
913 for ( rowIndex = 0; rowIndex < totalRows; ++rowIndex ) {
914 rowData = {
915 // order: original row order #
916 // $row : jQuery Object[]
917 child: [], // child row text (filter widget)
918 raw: [] // original row text
919 };
920 /** Add the table data to main data array */
921 $row = $( $tbody[ tbodyIndex ].rows[ rowIndex ] );
922 cols = [];
923 // ignore "remove-me" rows
924 if ( $row.hasClass( c.selectorRemove.slice(1) ) ) {
925 continue;
926 }
927 // if this is a child row, add it to the last row's children and continue to the next row
928 // ignore child row class, if it is the first row
929 if ( $row.hasClass( c.cssChildRow ) && rowIndex !== 0 ) {
930 len = cache.normalized.length - 1;
931 prevRowData = cache.normalized[ len ][ c.columns ];
932 prevRowData.$row = prevRowData.$row.add( $row );
933 // add 'hasChild' class name to parent row
934 if ( !$row.prev().hasClass( c.cssChildRow ) ) {
935 $row.prev().addClass( ts.css.cssHasChild );
936 }
937 // save child row content (un-parsed!)
938 $cells = $row.children( 'th, td' );
939 len = prevRowData.child.length;
940 prevRowData.child[ len ] = [];
941 // child row content does not account for colspans/rowspans; so indexing may be off
942 cacheIndex = 0;
943 max = c.columns;
944 for ( colIndex = 0; colIndex < max; colIndex++ ) {
945 cell = $cells[ colIndex ];
946 if ( cell ) {
947 prevRowData.child[ len ][ colIndex ] = ts.getParsedText( c, cell, colIndex );
948 span = $cells[ colIndex ].colSpan - 1;
949 if ( span > 0 ) {
950 cacheIndex += span;
951 max += span;
952 }
953 }
954 cacheIndex++;
955 }
956 // go to the next for loop
957 continue;
958 }
959 rowData.$row = $row;
960 rowData.order = rowIndex; // add original row position to rowCache
961 cacheIndex = 0;
962 max = c.columns;
963 for ( colIndex = 0; colIndex < max; ++colIndex ) {
964 cell = $row[ 0 ].cells[ colIndex ];
965 if ( cell && cacheIndex < c.columns ) {
966 hasParser = typeof parsers[ cacheIndex ] !== 'undefined';
967 if ( !hasParser && debug ) {
968 console.warn( 'No parser found for row: ' + rowIndex + ', column: ' + colIndex +
969 '; cell containing: "' + $(cell).text() + '"; does it have a header?' );
970 }
971 val = ts.getElementText( c, cell, cacheIndex );
972 rowData.raw[ cacheIndex ] = val; // save original row text
973 // save raw column text even if there is no parser set
974 txt = ts.getParsedText( c, cell, cacheIndex, val );
975 cols[ cacheIndex ] = txt;
976 if ( hasParser && ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) {
977 // determine column max value (ignore sign)
978 colMax[ cacheIndex ] = Math.max( Math.abs( txt ) || 0, colMax[ cacheIndex ] || 0 );
979 }
980 // allow colSpan in tbody
981 span = cell.colSpan - 1;
982 if ( span > 0 ) {
983 index = 0;
984 while ( index <= span ) {
985 // duplicate text (or not) to spanned columns
986 // instead of setting duplicate span to empty string, use textExtraction to try to get a value
987 // see http://stackoverflow.com/q/36449711/145346
988 txt = c.duplicateSpan || index === 0 ?
989 txt :
990 typeof c.textExtraction !== 'string' ?
991 ts.getElementText( c, cell, cacheIndex + index ) || '' :
992 '';
993 rowData.raw[ cacheIndex + index ] = txt;
994 cols[ cacheIndex + index ] = txt;
995 index++;
996 }
997 cacheIndex += span;
998 max += span;
999 }
1000 }
1001 cacheIndex++;
1002 }
1003 // ensure rowData is always in the same location (after the last column)
1004 cols[ c.columns ] = rowData;
1005 cache.normalized[ cache.normalized.length ] = cols;
1006 }
1007 cache.colMax = colMax;
1008 // total up rows, not including child rows
1009 c.totalRows += cache.normalized.length;
1010
1011 }
1012 if ( c.showProcessing ) {
1013 ts.isProcessing( table ); // remove processing icon
1014 }
1015 if ( debug ) {
1016 len = Math.min( 5, c.cache[ 0 ].normalized.length );
1017 console[ console.group ? 'group' : 'log' ]( 'Building cache for ' + c.totalRows +
1018 ' rows (showing ' + len + ' rows in log) and ' + c.columns + ' columns' +
1019 ts.benchmark( cacheTime ) );
1020 val = {};
1021 for ( colIndex = 0; colIndex < c.columns; colIndex++ ) {
1022 for ( cacheIndex = 0; cacheIndex < len; cacheIndex++ ) {
1023 if ( !val[ 'row: ' + cacheIndex ] ) {
1024 val[ 'row: ' + cacheIndex ] = {};
1025 }
1026 val[ 'row: ' + cacheIndex ][ c.$headerIndexed[ colIndex ].text() ] =
1027 c.cache[ 0 ].normalized[ cacheIndex ][ colIndex ];
1028 }
1029 }
1030 console[ console.table ? 'table' : 'log' ]( val );
1031 if ( console.groupEnd ) { console.groupEnd(); }
1032 }
1033 if ( typeof callback === 'function' ) {
1034 callback( table );
1035 }
1036 },
1037
1038 getColumnText : function( table, column, callback, rowFilter ) {
1039 table = $( table )[0];
1040 var tbodyIndex, rowIndex, cache, row, tbodyLen, rowLen, raw, parsed, $cell, result,
1041 hasCallback = typeof callback === 'function',
1042 allColumns = column === 'all',
1043 data = { raw : [], parsed: [], $cell: [] },
1044 c = table.config;
1045 if ( ts.isEmptyObject( c ) ) {
1046 if ( ts.debug(c, 'core') ) {
1047 console.warn( 'No cache found - aborting getColumnText function!' );
1048 }
1049 } else {
1050 tbodyLen = c.$tbodies.length;
1051 for ( tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++ ) {
1052 cache = c.cache[ tbodyIndex ].normalized;
1053 rowLen = cache.length;
1054 for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) {
1055 row = cache[ rowIndex ];
1056 if ( rowFilter && !row[ c.columns ].$row.is( rowFilter ) ) {
1057 continue;
1058 }
1059 result = true;
1060 parsed = ( allColumns ) ? row.slice( 0, c.columns ) : row[ column ];
1061 row = row[ c.columns ];
1062 raw = ( allColumns ) ? row.raw : row.raw[ column ];
1063 $cell = ( allColumns ) ? row.$row.children() : row.$row.children().eq( column );
1064 if ( hasCallback ) {
1065 result = callback({
1066 tbodyIndex : tbodyIndex,
1067 rowIndex : rowIndex,
1068 parsed : parsed,
1069 raw : raw,
1070 $row : row.$row,
1071 $cell : $cell
1072 });
1073 }
1074 if ( result !== false ) {
1075 data.parsed[ data.parsed.length ] = parsed;
1076 data.raw[ data.raw.length ] = raw;
1077 data.$cell[ data.$cell.length ] = $cell;
1078 }
1079 }
1080 }
1081 // return everything
1082 return data;
1083 }
1084 },
1085
1086 /*
1087 ██ ██ █████▄ █████▄ ▄████▄ ██████ ██████
1088 ██ ██ ██▄▄██ ██ ██ ██▄▄██ ██ ██▄▄
1089 ██ ██ ██▀▀▀ ██ ██ ██▀▀██ ██ ██▀▀
1090 ▀████▀ ██ █████▀ ██ ██ ██ ██████
1091 */
1092 setHeadersCss : function( c ) {
1093 var indx, column,
1094 list = c.sortList,
1095 len = list.length,
1096 none = ts.css.sortNone + ' ' + c.cssNone,
1097 css = [ ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc ],
1098 cssIcon = [ c.cssIconAsc, c.cssIconDesc, c.cssIconNone ],
1099 aria = [ 'ascending', 'descending' ],
1100 updateColumnSort = function($el, index) {
1101 $el
1102 .removeClass( none )
1103 .addClass( css[ index ] )
1104 .attr( 'aria-sort', aria[ index ] )
1105 .find( '.' + ts.css.icon )
1106 .removeClass( cssIcon[ 2 ] )
1107 .addClass( cssIcon[ index ] );
1108 },
1109 // find the footer
1110 $extras = c.$table
1111 .find( 'tfoot tr' )
1112 .children( 'td, th' )
1113 .add( $( c.namespace + '_extra_headers' ) )
1114 .removeClass( css.join( ' ' ) ),
1115 // remove all header information
1116 $sorted = c.$headers
1117 .add( $( 'thead ' + c.namespace + '_extra_headers' ) )
1118 .removeClass( css.join( ' ' ) )
1119 .addClass( none )
1120 .attr( 'aria-sort', 'none' )
1121 .find( '.' + ts.css.icon )
1122 .removeClass( cssIcon.join( ' ' ) )
1123 .end();
1124 // add css none to all sortable headers
1125 $sorted
1126 .not( '.sorter-false' )
1127 .find( '.' + ts.css.icon )
1128 .addClass( cssIcon[ 2 ] );
1129 // add disabled css icon class
1130 if ( c.cssIconDisabled ) {
1131 $sorted
1132 .filter( '.sorter-false' )
1133 .find( '.' + ts.css.icon )
1134 .addClass( c.cssIconDisabled );
1135 }
1136 for ( indx = 0; indx < len; indx++ ) {
1137 // direction = 2 means reset!
1138 if ( list[ indx ][ 1 ] !== 2 ) {
1139 // multicolumn sorting updating - see #1005
1140 // .not(function() {}) needs jQuery 1.4
1141 // filter(function(i, el) {}) <- el is undefined in jQuery v1.2.6
1142 $sorted = c.$headers.filter( function( i ) {
1143 // only include headers that are in the sortList (this includes colspans)
1144 var include = true,
1145 $el = c.$headers.eq( i ),
1146 col = parseInt( $el.attr( 'data-column' ), 10 ),
1147 end = col + ts.getClosest( $el, 'th, td' )[0].colSpan;
1148 for ( ; col < end; col++ ) {
1149 include = include ? include || ts.isValueInArray( col, c.sortList ) > -1 : false;
1150 }
1151 return include;
1152 });
1153
1154 // choose the :last in case there are nested columns
1155 $sorted = $sorted
1156 .not( '.sorter-false' )
1157 .filter( '[data-column="' + list[ indx ][ 0 ] + '"]' + ( len === 1 ? ':last' : '' ) );
1158 if ( $sorted.length ) {
1159 for ( column = 0; column < $sorted.length; column++ ) {
1160 if ( !$sorted[ column ].sortDisabled ) {
1161 updateColumnSort( $sorted.eq( column ), list[ indx ][ 1 ] );
1162 }
1163 }
1164 }
1165 // add sorted class to footer & extra headers, if they exist
1166 if ( $extras.length ) {
1167 updateColumnSort( $extras.filter( '[data-column="' + list[ indx ][ 0 ] + '"]' ), list[ indx ][ 1 ] );
1168 }
1169 }
1170 }
1171 // add verbose aria labels
1172 len = c.$headers.length;
1173 for ( indx = 0; indx < len; indx++ ) {
1174 ts.setColumnAriaLabel( c, c.$headers.eq( indx ) );
1175 }
1176 },
1177
1178 getClosest : function( $el, selector ) {
1179 // jQuery v1.2.6 doesn't have closest()
1180 if ( $.fn.closest ) {
1181 return $el.closest( selector );
1182 }
1183 return $el.is( selector ) ?
1184 $el :
1185 $el.parents( selector ).filter( ':first' );
1186 },
1187
1188 // nextSort (optional), lets you disable next sort text
1189 setColumnAriaLabel : function( c, $header, nextSort ) {
1190 if ( $header.length ) {
1191 var column = parseInt( $header.attr( 'data-column' ), 10 ),
1192 vars = c.sortVars[ column ],
1193 tmp = $header.hasClass( ts.css.sortAsc ) ?
1194 'sortAsc' :
1195 $header.hasClass( ts.css.sortDesc ) ? 'sortDesc' : 'sortNone',
1196 txt = $header.text().trim() + ': ' + ts.language[ tmp ];
1197 if ( $header.hasClass( 'sorter-false' ) || nextSort === false ) {
1198 txt += ts.language.sortDisabled;
1199 } else {
1200 tmp = ( vars.count + 1 ) % vars.order.length;
1201 nextSort = vars.order[ tmp ];
1202 // if nextSort
1203 txt += ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ];
1204 }
1205 $header.attr( 'aria-label', txt );
1206 if (vars.sortedBy) {
1207 $header.attr( 'data-sortedBy', vars.sortedBy );
1208 } else {
1209 $header.removeAttr('data-sortedBy');
1210 }
1211 }
1212 },
1213
1214 updateHeader : function( c ) {
1215 var index, isDisabled, $header, col,
1216 table = c.table,
1217 len = c.$headers.length;
1218 for ( index = 0; index < len; index++ ) {
1219 $header = c.$headers.eq( index );
1220 col = ts.getColumnData( table, c.headers, index, true );
1221 // add 'sorter-false' class if 'parser-false' is set
1222 isDisabled = ts.getData( $header, col, 'sorter' ) === 'false' || ts.getData( $header, col, 'parser' ) === 'false';
1223 ts.setColumnSort( c, $header, isDisabled );
1224 }
1225 },
1226
1227 setColumnSort : function( c, $header, isDisabled ) {
1228 var id = c.table.id;
1229 $header[ 0 ].sortDisabled = isDisabled;
1230 $header[ isDisabled ? 'addClass' : 'removeClass' ]( 'sorter-false' )
1231 .attr( 'aria-disabled', '' + isDisabled );
1232 // disable tab index on disabled cells
1233 if ( c.tabIndex ) {
1234 if ( isDisabled ) {
1235 $header.removeAttr( 'tabindex' );
1236 } else {
1237 $header.attr( 'tabindex', '0' );
1238 }
1239 }
1240 // aria-controls - requires table ID
1241 if ( id ) {
1242 if ( isDisabled ) {
1243 $header.removeAttr( 'aria-controls' );
1244 } else {
1245 $header.attr( 'aria-controls', id );
1246 }
1247 }
1248 },
1249
1250 updateHeaderSortCount : function( c, list ) {
1251 var col, dir, group, indx, primary, temp, val, order,
1252 sortList = list || c.sortList,
1253 len = sortList.length;
1254 c.sortList = [];
1255 for ( indx = 0; indx < len; indx++ ) {
1256 val = sortList[ indx ];
1257 // ensure all sortList values are numeric - fixes #127
1258 col = parseInt( val[ 0 ], 10 );
1259 // prevents error if sorton array is wrong
1260 if ( col < c.columns ) {
1261
1262 // set order if not already defined - due to colspan header without associated header cell
1263 // adding this check prevents a javascript error
1264 if ( !c.sortVars[ col ].order ) {
1265 if ( ts.getOrder( c.sortInitialOrder ) ) {
1266 order = c.sortReset ? [ 1, 0, 2 ] : [ 1, 0 ];
1267 } else {
1268 order = c.sortReset ? [ 0, 1, 2 ] : [ 0, 1 ];
1269 }
1270 c.sortVars[ col ].order = order;
1271 c.sortVars[ col ].count = 0;
1272 }
1273
1274 order = c.sortVars[ col ].order;
1275 dir = ( '' + val[ 1 ] ).match( /^(1|d|s|o|n)/ );
1276 dir = dir ? dir[ 0 ] : '';
1277 // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext
1278 switch ( dir ) {
1279 case '1' : case 'd' : // descending
1280 dir = 1;
1281 break;
1282 case 's' : // same direction (as primary column)
1283 // if primary sort is set to 's', make it ascending
1284 dir = primary || 0;
1285 break;
1286 case 'o' :
1287 temp = order[ ( primary || 0 ) % order.length ];
1288 // opposite of primary column; but resets if primary resets
1289 dir = temp === 0 ? 1 : temp === 1 ? 0 : 2;
1290 break;
1291 case 'n' :
1292 dir = order[ ( ++c.sortVars[ col ].count ) % order.length ];
1293 break;
1294 default : // ascending
1295 dir = 0;
1296 break;
1297 }
1298 primary = indx === 0 ? dir : primary;
1299 group = [ col, parseInt( dir, 10 ) || 0 ];
1300 c.sortList[ c.sortList.length ] = group;
1301 dir = $.inArray( group[ 1 ], order ); // fixes issue #167
1302 c.sortVars[ col ].count = dir >= 0 ? dir : group[ 1 ] % order.length;
1303 }
1304 }
1305 },
1306
1307 updateAll : function( c, resort, callback ) {
1308 var table = c.table;
1309 table.isUpdating = true;
1310 ts.refreshWidgets( table, true, true );
1311 ts.buildHeaders( c );
1312 ts.bindEvents( table, c.$headers, true );
1313 ts.bindMethods( c );
1314 ts.commonUpdate( c, resort, callback );
1315 },
1316
1317 update : function( c, resort, callback ) {
1318 var table = c.table;
1319 table.isUpdating = true;
1320 // update sorting (if enabled/disabled)
1321 ts.updateHeader( c );
1322 ts.commonUpdate( c, resort, callback );
1323 },
1324
1325 // simple header update - see #989
1326 updateHeaders : function( c, callback ) {
1327 c.table.isUpdating = true;
1328 ts.buildHeaders( c );
1329 ts.bindEvents( c.table, c.$headers, true );
1330 ts.resortComplete( c, callback );
1331 },
1332
1333 updateCell : function( c, cell, resort, callback ) {
1334 // updateCell for child rows is a mess - we'll ignore them for now
1335 // eventually I'll break out the "update" row cache code to make everything consistent
1336 if ( $( cell ).closest( 'tr' ).hasClass( c.cssChildRow ) ) {
1337 console.warn('Tablesorter Warning! "updateCell" for child row content has been disabled, use "update" instead');
1338 return;
1339 }
1340 if ( ts.isEmptyObject( c.cache ) ) {
1341 // empty table, do an update instead - fixes #1099
1342 ts.updateHeader( c );
1343 ts.commonUpdate( c, resort, callback );
1344 return;
1345 }
1346 c.table.isUpdating = true;
1347 c.$table.find( c.selectorRemove ).remove();
1348 // get position from the dom
1349 var tmp, indx, row, icell, cache, len,
1350 $tbodies = c.$tbodies,
1351 $cell = $( cell ),
1352 // update cache - format: function( s, table, cell, cellIndex )
1353 // no closest in jQuery v1.2.6
1354 tbodyIndex = $tbodies.index( ts.getClosest( $cell, 'tbody' ) ),
1355 tbcache = c.cache[ tbodyIndex ],
1356 $row = ts.getClosest( $cell, 'tr' );
1357 cell = $cell[ 0 ]; // in case cell is a jQuery object
1358 // tbody may not exist if update is initialized while tbody is removed for processing
1359 if ( $tbodies.length && tbodyIndex >= 0 ) {
1360 row = $tbodies.eq( tbodyIndex ).find( 'tr' ).not( '.' + c.cssChildRow ).index( $row );
1361 cache = tbcache.normalized[ row ];
1362 len = $row[ 0 ].cells.length;
1363 if ( len !== c.columns ) {
1364 // colspan in here somewhere!
1365 icell = 0;
1366 tmp = false;
1367 for ( indx = 0; indx < len; indx++ ) {
1368 if ( !tmp && $row[ 0 ].cells[ indx ] !== cell ) {
1369 icell += $row[ 0 ].cells[ indx ].colSpan;
1370 } else {
1371 tmp = true;
1372 }
1373 }
1374 } else {
1375 icell = $cell.index();
1376 }
1377 tmp = ts.getElementText( c, cell, icell ); // raw
1378 cache[ c.columns ].raw[ icell ] = tmp;
1379 tmp = ts.getParsedText( c, cell, icell, tmp );
1380 cache[ icell ] = tmp; // parsed
1381 if ( ( c.parsers[ icell ].type || '' ).toLowerCase() === 'numeric' ) {
1382 // update column max value (ignore sign)
1383 tbcache.colMax[ icell ] = Math.max( Math.abs( tmp ) || 0, tbcache.colMax[ icell ] || 0 );
1384 }
1385 tmp = resort !== 'undefined' ? resort : c.resort;
1386 if ( tmp !== false ) {
1387 // widgets will be reapplied
1388 ts.checkResort( c, tmp, callback );
1389 } else {
1390 // don't reapply widgets is resort is false, just in case it causes
1391 // problems with element focus
1392 ts.resortComplete( c, callback );
1393 }
1394 } else {
1395 if ( ts.debug(c, 'core') ) {
1396 console.error( 'updateCell aborted, tbody missing or not within the indicated table' );
1397 }
1398 c.table.isUpdating = false;
1399 }
1400 },
1401
1402 addRows : function( c, $row, resort, callback ) {
1403 var txt, val, tbodyIndex, rowIndex, rows, cellIndex, len, order,
1404 cacheIndex, rowData, cells, cell, span,
1405 // allow passing a row string if only one non-info tbody exists in the table
1406 valid = typeof $row === 'string' && c.$tbodies.length === 1 && /<tr/.test( $row || '' ),
1407 table = c.table;
1408 if ( valid ) {
1409 $row = $( $row );
1410 c.$tbodies.append( $row );
1411 } else if (
1412 !$row ||
1413 // row is a jQuery object?
1414 !( $row instanceof $ ) ||
1415 // row contained in the table?
1416 ( ts.getClosest( $row, 'table' )[ 0 ] !== c.table )
1417 ) {
1418 if ( ts.debug(c, 'core') ) {
1419 console.error( 'addRows method requires (1) a jQuery selector reference to rows that have already ' +
1420 'been added to the table, or (2) row HTML string to be added to a table with only one tbody' );
1421 }
1422 return false;
1423 }
1424 table.isUpdating = true;
1425 if ( ts.isEmptyObject( c.cache ) ) {
1426 // empty table, do an update instead - fixes #450
1427 ts.updateHeader( c );
1428 ts.commonUpdate( c, resort, callback );
1429 } else {
1430 rows = $row.filter( 'tr' ).attr( 'role', 'row' ).length;
1431 tbodyIndex = c.$tbodies.index( $row.parents( 'tbody' ).filter( ':first' ) );
1432 // fixes adding rows to an empty table - see issue #179
1433 if ( !( c.parsers && c.parsers.length ) ) {
1434 ts.setupParsers( c );
1435 }
1436 // add each row
1437 for ( rowIndex = 0; rowIndex < rows; rowIndex++ ) {
1438 cacheIndex = 0;
1439 len = $row[ rowIndex ].cells.length;
1440 order = c.cache[ tbodyIndex ].normalized.length;
1441 cells = [];
1442 rowData = {
1443 child : [],
1444 raw : [],
1445 $row : $row.eq( rowIndex ),
1446 order : order
1447 };
1448 // add each cell
1449 for ( cellIndex = 0; cellIndex < len; cellIndex++ ) {
1450 cell = $row[ rowIndex ].cells[ cellIndex ];
1451 txt = ts.getElementText( c, cell, cacheIndex );
1452 rowData.raw[ cacheIndex ] = txt;
1453 val = ts.getParsedText( c, cell, cacheIndex, txt );
1454 cells[ cacheIndex ] = val;
1455 if ( ( c.parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) {
1456 // update column max value (ignore sign)
1457 c.cache[ tbodyIndex ].colMax[ cacheIndex ] =
1458 Math.max( Math.abs( val ) || 0, c.cache[ tbodyIndex ].colMax[ cacheIndex ] || 0 );
1459 }
1460 span = cell.colSpan - 1;
1461 if ( span > 0 ) {
1462 cacheIndex += span;
1463 }
1464 cacheIndex++;
1465 }
1466 // add the row data to the end
1467 cells[ c.columns ] = rowData;
1468 // update cache
1469 c.cache[ tbodyIndex ].normalized[ order ] = cells;
1470 }
1471 // resort using current settings
1472 ts.checkResort( c, resort, callback );
1473 }
1474 },
1475
1476 updateCache : function( c, callback, $tbodies ) {
1477 // rebuild parsers
1478 if ( !( c.parsers && c.parsers.length ) ) {
1479 ts.setupParsers( c, $tbodies );
1480 }
1481 // rebuild the cache map
1482 ts.buildCache( c, callback, $tbodies );
1483 },
1484
1485 // init flag (true) used by pager plugin to prevent widget application
1486 // renamed from appendToTable
1487 appendCache : function( c, init ) {
1488 var parsed, totalRows, $tbody, $curTbody, rowIndex, tbodyIndex, appendTime,
1489 table = c.table,
1490 $tbodies = c.$tbodies,
1491 rows = [],
1492 cache = c.cache;
1493 // empty table - fixes #206/#346
1494 if ( ts.isEmptyObject( cache ) ) {
1495 // run pager appender in case the table was just emptied
1496 return c.appender ? c.appender( table, rows ) :
1497 table.isUpdating ? c.$table.triggerHandler( 'updateComplete', table ) : ''; // Fixes #532
1498 }
1499 if ( ts.debug(c, 'core') ) {
1500 appendTime = new Date();
1501 }
1502 for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
1503 $tbody = $tbodies.eq( tbodyIndex );
1504 if ( $tbody.length ) {
1505 // detach tbody for manipulation
1506 $curTbody = ts.processTbody( table, $tbody, true );
1507 parsed = cache[ tbodyIndex ].normalized;
1508 totalRows = parsed.length;
1509 for ( rowIndex = 0; rowIndex < totalRows; rowIndex++ ) {
1510 rows[rows.length] = parsed[ rowIndex ][ c.columns ].$row;
1511 // removeRows used by the pager plugin; don't render if using ajax - fixes #411
1512 if ( !c.appender || ( c.pager && !c.pager.removeRows && !c.pager.ajax ) ) {
1513 $curTbody.append( parsed[ rowIndex ][ c.columns ].$row );
1514 }
1515 }
1516 // restore tbody
1517 ts.processTbody( table, $curTbody, false );
1518 }
1519 }
1520 if ( c.appender ) {
1521 c.appender( table, rows );
1522 }
1523 if ( ts.debug(c, 'core') ) {
1524 console.log( 'Rebuilt table' + ts.benchmark( appendTime ) );
1525 }
1526 // apply table widgets; but not before ajax completes
1527 if ( !init && !c.appender ) {
1528 ts.applyWidget( table );
1529 }
1530 if ( table.isUpdating ) {
1531 c.$table.triggerHandler( 'updateComplete', table );
1532 }
1533 },
1534
1535 commonUpdate : function( c, resort, callback ) {
1536 // remove rows/elements before update
1537 c.$table.find( c.selectorRemove ).remove();
1538 // rebuild parsers
1539 ts.setupParsers( c );
1540 // rebuild the cache map
1541 ts.buildCache( c );
1542 ts.checkResort( c, resort, callback );
1543 },
1544
1545 /*
1546 ▄█████ ▄████▄ █████▄ ██████ ██ █████▄ ▄████▄
1547 ▀█▄ ██ ██ ██▄▄██ ██ ██ ██ ██ ██ ▄▄▄
1548 ▀█▄ ██ ██ ██▀██ ██ ██ ██ ██ ██ ▀██
1549 █████▀ ▀████▀ ██ ██ ██ ██ ██ ██ ▀████▀
1550 */
1551 initSort : function( c, cell, event ) {
1552 if ( c.table.isUpdating ) {
1553 // let any updates complete before initializing a sort
1554 return setTimeout( function() {
1555 ts.initSort( c, cell, event );
1556 }, 50 );
1557 }
1558
1559 var arry, indx, headerIndx, dir, temp, tmp, $header,
1560 notMultiSort = !event[ c.sortMultiSortKey ],
1561 table = c.table,
1562 len = c.$headers.length,
1563 th = ts.getClosest( $( cell ), 'th, td' ),
1564 col = parseInt( th.attr( 'data-column' ), 10 ),
1565 sortedBy = event.type === 'mouseup' ? 'user' : event.type,
1566 order = c.sortVars[ col ].order;
1567 th = th[0];
1568 // Only call sortStart if sorting is enabled
1569 c.$table.triggerHandler( 'sortStart', table );
1570 // get current column sort order
1571 tmp = ( c.sortVars[ col ].count + 1 ) % order.length;
1572 c.sortVars[ col ].count = event[ c.sortResetKey ] ? 2 : tmp;
1573 // reset all sorts on non-current column - issue #30
1574 if ( c.sortRestart ) {
1575 for ( headerIndx = 0; headerIndx < len; headerIndx++ ) {
1576 $header = c.$headers.eq( headerIndx );
1577 tmp = parseInt( $header.attr( 'data-column' ), 10 );
1578 // only reset counts on columns that weren't just clicked on and if not included in a multisort
1579 if ( col !== tmp && ( notMultiSort || $header.hasClass( ts.css.sortNone ) ) ) {
1580 c.sortVars[ tmp ].count = -1;
1581 }
1582 }
1583 }
1584 // user only wants to sort on one column
1585 if ( notMultiSort ) {
1586 $.each( c.sortVars, function( i ) {
1587 c.sortVars[ i ].sortedBy = '';
1588 });
1589 // flush the sort list
1590 c.sortList = [];
1591 c.last.sortList = [];
1592 if ( c.sortForce !== null ) {
1593 arry = c.sortForce;
1594 for ( indx = 0; indx < arry.length; indx++ ) {
1595 if ( arry[ indx ][ 0 ] !== col ) {
1596 c.sortList[ c.sortList.length ] = arry[ indx ];
1597 c.sortVars[ arry[ indx ][ 0 ] ].sortedBy = 'sortForce';
1598 }
1599 }
1600 }
1601 // add column to sort list
1602 dir = order[ c.sortVars[ col ].count ];
1603 if ( dir < 2 ) {
1604 c.sortList[ c.sortList.length ] = [ col, dir ];
1605 c.sortVars[ col ].sortedBy = sortedBy;
1606 // add other columns if header spans across multiple
1607 if ( th.colSpan > 1 ) {
1608 for ( indx = 1; indx < th.colSpan; indx++ ) {
1609 c.sortList[ c.sortList.length ] = [ col + indx, dir ];
1610 // update count on columns in colSpan
1611 c.sortVars[ col + indx ].count = $.inArray( dir, order );
1612 c.sortVars[ col + indx ].sortedBy = sortedBy;
1613 }
1614 }
1615 }
1616 // multi column sorting
1617 } else {
1618 // get rid of the sortAppend before adding more - fixes issue #115 & #523
1619 c.sortList = $.extend( [], c.last.sortList );
1620
1621 // the user has clicked on an already sorted column
1622 if ( ts.isValueInArray( col, c.sortList ) >= 0 ) {
1623 // reverse the sorting direction
1624 c.sortVars[ col ].sortedBy = sortedBy;
1625 for ( indx = 0; indx < c.sortList.length; indx++ ) {
1626 tmp = c.sortList[ indx ];
1627 if ( tmp[ 0 ] === col ) {
1628 // order.count seems to be incorrect when compared to cell.count
1629 tmp[ 1 ] = order[ c.sortVars[ col ].count ];
1630 if ( tmp[1] === 2 ) {
1631 c.sortList.splice( indx, 1 );
1632 c.sortVars[ col ].count = -1;
1633 }
1634 }
1635 }
1636 } else {
1637 // add column to sort list array
1638 dir = order[ c.sortVars[ col ].count ];
1639 c.sortVars[ col ].sortedBy = sortedBy;
1640 if ( dir < 2 ) {
1641 c.sortList[ c.sortList.length ] = [ col, dir ];
1642 // add other columns if header spans across multiple
1643 if ( th.colSpan > 1 ) {
1644 for ( indx = 1; indx < th.colSpan; indx++ ) {
1645 c.sortList[ c.sortList.length ] = [ col + indx, dir ];
1646 // update count on columns in colSpan
1647 c.sortVars[ col + indx ].count = $.inArray( dir, order );
1648 c.sortVars[ col + indx ].sortedBy = sortedBy;
1649 }
1650 }
1651 }
1652 }
1653 }
1654 // save sort before applying sortAppend
1655 c.last.sortList = $.extend( [], c.sortList );
1656 if ( c.sortList.length && c.sortAppend ) {
1657 arry = Array.isArray( c.sortAppend ) ? c.sortAppend : c.sortAppend[ c.sortList[ 0 ][ 0 ] ];
1658 if ( !ts.isEmptyObject( arry ) ) {
1659 for ( indx = 0; indx < arry.length; indx++ ) {
1660 if ( arry[ indx ][ 0 ] !== col && ts.isValueInArray( arry[ indx ][ 0 ], c.sortList ) < 0 ) {
1661 dir = arry[ indx ][ 1 ];
1662 temp = ( '' + dir ).match( /^(a|d|s|o|n)/ );
1663 if ( temp ) {
1664 tmp = c.sortList[ 0 ][ 1 ];
1665 switch ( temp[ 0 ] ) {
1666 case 'd' :
1667 dir = 1;
1668 break;
1669 case 's' :
1670 dir = tmp;
1671 break;
1672 case 'o' :
1673 dir = tmp === 0 ? 1 : 0;
1674 break;
1675 case 'n' :
1676 dir = ( tmp + 1 ) % order.length;
1677 break;
1678 default:
1679 dir = 0;
1680 break;
1681 }
1682 }
1683 c.sortList[ c.sortList.length ] = [ arry[ indx ][ 0 ], dir ];
1684 c.sortVars[ arry[ indx ][ 0 ] ].sortedBy = 'sortAppend';
1685 }
1686 }
1687 }
1688 }
1689 // sortBegin event triggered immediately before the sort
1690 c.$table.triggerHandler( 'sortBegin', table );
1691 // setTimeout needed so the processing icon shows up
1692 setTimeout( function() {
1693 // set css for headers
1694 ts.setHeadersCss( c );
1695 ts.multisort( c );
1696 ts.appendCache( c );
1697 c.$table.triggerHandler( 'sortBeforeEnd', table );
1698 c.$table.triggerHandler( 'sortEnd', table );
1699 }, 1 );
1700 },
1701
1702 // sort multiple columns
1703 multisort : function( c ) { /*jshint loopfunc:true */
1704 var tbodyIndex, sortTime, colMax, rows, tmp,
1705 table = c.table,
1706 sorter = [],
1707 dir = 0,
1708 textSorter = c.textSorter || '',
1709 sortList = c.sortList,
1710 sortLen = sortList.length,
1711 len = c.$tbodies.length;
1712 if ( c.serverSideSorting || ts.isEmptyObject( c.cache ) ) {
1713 // empty table - fixes #206/#346
1714 return;
1715 }
1716 if ( ts.debug(c, 'core') ) { sortTime = new Date(); }
1717 // cache textSorter to optimize speed
1718 if ( typeof textSorter === 'object' ) {
1719 colMax = c.columns;
1720 while ( colMax-- ) {
1721 tmp = ts.getColumnData( table, textSorter, colMax );
1722 if ( typeof tmp === 'function' ) {
1723 sorter[ colMax ] = tmp;
1724 }
1725 }
1726 }
1727 for ( tbodyIndex = 0; tbodyIndex < len; tbodyIndex++ ) {
1728 colMax = c.cache[ tbodyIndex ].colMax;
1729 rows = c.cache[ tbodyIndex ].normalized;
1730
1731 rows.sort( function( a, b ) {
1732 var sortIndex, num, col, order, sort, x, y;
1733 // rows is undefined here in IE, so don't use it!
1734 for ( sortIndex = 0; sortIndex < sortLen; sortIndex++ ) {
1735 col = sortList[ sortIndex ][ 0 ];
1736 order = sortList[ sortIndex ][ 1 ];
1737 // sort direction, true = asc, false = desc
1738 dir = order === 0;
1739
1740 if ( c.sortStable && a[ col ] === b[ col ] && sortLen === 1 ) {
1741 return a[ c.columns ].order - b[ c.columns ].order;
1742 }
1743
1744 // fallback to natural sort since it is more robust
1745 num = /n/i.test( ts.getSortType( c.parsers, col ) );
1746 if ( num && c.strings[ col ] ) {
1747 // sort strings in numerical columns
1748 if ( typeof ( ts.string[ c.strings[ col ] ] ) === 'boolean' ) {
1749 num = ( dir ? 1 : -1 ) * ( ts.string[ c.strings[ col ] ] ? -1 : 1 );
1750 } else {
1751 num = ( c.strings[ col ] ) ? ts.string[ c.strings[ col ] ] || 0 : 0;
1752 }
1753 // fall back to built-in numeric sort
1754 // var sort = $.tablesorter['sort' + s]( a[col], b[col], dir, colMax[col], table );
1755 sort = c.numberSorter ? c.numberSorter( a[ col ], b[ col ], dir, colMax[ col ], table ) :
1756 ts[ 'sortNumeric' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ], b[ col ], num, colMax[ col ], col, c );
1757 } else {
1758 // set a & b depending on sort direction
1759 x = dir ? a : b;
1760 y = dir ? b : a;
1761 // text sort function
1762 if ( typeof textSorter === 'function' ) {
1763 // custom OVERALL text sorter
1764 sort = textSorter( x[ col ], y[ col ], dir, col, table );
1765 } else if ( typeof sorter[ col ] === 'function' ) {
1766 // custom text sorter for a SPECIFIC COLUMN
1767 sort = sorter[ col ]( x[ col ], y[ col ], dir, col, table );
1768 } else {
1769 // fall back to natural sort
1770 sort = ts[ 'sortNatural' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ] || '', b[ col ] || '', col, c );
1771 }
1772 }
1773 if ( sort ) { return sort; }
1774 }
1775 return a[ c.columns ].order - b[ c.columns ].order;
1776 });
1777 }
1778 if ( ts.debug(c, 'core') ) {
1779 console.log( 'Applying sort ' + sortList.toString() + ts.benchmark( sortTime ) );
1780 }
1781 },
1782
1783 resortComplete : function( c, callback ) {
1784 if ( c.table.isUpdating ) {
1785 c.$table.triggerHandler( 'updateComplete', c.table );
1786 }
1787 if ( typeof callback === 'function' ) {
1788 callback( c.table );
1789 }
1790 },
1791
1792 checkResort : function( c, resort, callback ) {
1793 var sortList = Array.isArray( resort ) ? resort : c.sortList,
1794 // if no resort parameter is passed, fallback to config.resort (true by default)
1795 resrt = typeof resort === 'undefined' ? c.resort : resort;
1796 // don't try to resort if the table is still processing
1797 // this will catch spamming of the updateCell method
1798 if ( resrt !== false && !c.serverSideSorting && !c.table.isProcessing ) {
1799 if ( sortList.length ) {
1800 ts.sortOn( c, sortList, function() {
1801 ts.resortComplete( c, callback );
1802 }, true );
1803 } else {
1804 ts.sortReset( c, function() {
1805 ts.resortComplete( c, callback );
1806 ts.applyWidget( c.table, false );
1807 } );
1808 }
1809 } else {
1810 ts.resortComplete( c, callback );
1811 ts.applyWidget( c.table, false );
1812 }
1813 },
1814
1815 sortOn : function( c, list, callback, init ) {
1816 var indx,
1817 table = c.table;
1818 c.$table.triggerHandler( 'sortStart', table );
1819 for (indx = 0; indx < c.columns; indx++) {
1820 c.sortVars[ indx ].sortedBy = ts.isValueInArray( indx, list ) > -1 ? 'sorton' : '';
1821 }
1822 // update header count index
1823 ts.updateHeaderSortCount( c, list );
1824 // set css for headers
1825 ts.setHeadersCss( c );
1826 // fixes #346
1827 if ( c.delayInit && ts.isEmptyObject( c.cache ) ) {
1828 ts.buildCache( c );
1829 }
1830 c.$table.triggerHandler( 'sortBegin', table );
1831 // sort the table and append it to the dom
1832 ts.multisort( c );
1833 ts.appendCache( c, init );
1834 c.$table.triggerHandler( 'sortBeforeEnd', table );
1835 c.$table.triggerHandler( 'sortEnd', table );
1836 ts.applyWidget( table );
1837 if ( typeof callback === 'function' ) {
1838 callback( table );
1839 }
1840 },
1841
1842 sortReset : function( c, callback ) {
1843 c.sortList = [];
1844 var indx;
1845 for (indx = 0; indx < c.columns; indx++) {
1846 c.sortVars[ indx ].count = -1;
1847 c.sortVars[ indx ].sortedBy = '';
1848 }
1849 ts.setHeadersCss( c );
1850 ts.multisort( c );
1851 ts.appendCache( c );
1852 if ( typeof callback === 'function' ) {
1853 callback( c.table );
1854 }
1855 },
1856
1857 getSortType : function( parsers, column ) {
1858 return ( parsers && parsers[ column ] ) ? parsers[ column ].type || '' : '';
1859 },
1860
1861 getOrder : function( val ) {
1862 // look for 'd' in 'desc' order; return true
1863 return ( /^d/i.test( val ) || val === 1 );
1864 },
1865
1866 // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed)
1867 sortNatural : function( a, b ) {
1868 if ( a === b ) { return 0; }
1869 a = ( a || '' ).toString();
1870 b = ( b || '' ).toString();
1871 var aNum, bNum, aFloat, bFloat, indx, max,
1872 regex = ts.regex;
1873 // first try and sort Hex codes
1874 if ( regex.hex.test( b ) ) {
1875 aNum = parseInt( a.match( regex.hex ), 16 );
1876 bNum = parseInt( b.match( regex.hex ), 16 );
1877 if ( aNum < bNum ) { return -1; }
1878 if ( aNum > bNum ) { return 1; }
1879 }
1880 // chunk/tokenize
1881 aNum = a.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' );
1882 bNum = b.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' );
1883 max = Math.max( aNum.length, bNum.length );
1884 // natural sorting through split numeric strings and default strings
1885 for ( indx = 0; indx < max; indx++ ) {
1886 // find floats not starting with '0', string or 0 if not defined
1887 aFloat = isNaN( aNum[ indx ] ) ? aNum[ indx ] || 0 : parseFloat( aNum[ indx ] ) || 0;
1888 bFloat = isNaN( bNum[ indx ] ) ? bNum[ indx ] || 0 : parseFloat( bNum[ indx ] ) || 0;
1889 // handle numeric vs string comparison - number < string - (Kyle Adams)
1890 if ( isNaN( aFloat ) !== isNaN( bFloat ) ) { return isNaN( aFloat ) ? 1 : -1; }
1891 // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
1892 if ( typeof aFloat !== typeof bFloat ) {
1893 aFloat += '';
1894 bFloat += '';
1895 }
1896 if ( aFloat < bFloat ) { return -1; }
1897 if ( aFloat > bFloat ) { return 1; }
1898 }
1899 return 0;
1900 },
1901
1902 sortNaturalAsc : function( a, b, col, c ) {
1903 if ( a === b ) { return 0; }
1904 var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
1905 if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; }
1906 if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; }
1907 return ts.sortNatural( a, b );
1908 },
1909
1910 sortNaturalDesc : function( a, b, col, c ) {
1911 if ( a === b ) { return 0; }
1912 var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
1913 if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; }
1914 if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; }
1915 return ts.sortNatural( b, a );
1916 },
1917
1918 // basic alphabetical sort
1919 sortText : function( a, b ) {
1920 return a > b ? 1 : ( a < b ? -1 : 0 );
1921 },
1922
1923 // return text string value by adding up ascii value
1924 // so the text is somewhat sorted when using a digital sort
1925 // this is NOT an alphanumeric sort
1926 getTextValue : function( val, num, max ) {
1927 if ( max ) {
1928 // make sure the text value is greater than the max numerical value (max)
1929 var indx,
1930 len = val ? val.length : 0,
1931 n = max + num;
1932 for ( indx = 0; indx < len; indx++ ) {
1933 n += val.charCodeAt( indx );
1934 }
1935 return num * n;
1936 }
1937 return 0;
1938 },
1939
1940 sortNumericAsc : function( a, b, num, max, col, c ) {
1941 if ( a === b ) { return 0; }
1942 var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
1943 if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; }
1944 if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; }
1945 if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); }
1946 if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); }
1947 return a - b;
1948 },
1949
1950 sortNumericDesc : function( a, b, num, max, col, c ) {
1951 if ( a === b ) { return 0; }
1952 var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
1953 if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; }
1954 if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; }
1955 if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); }
1956 if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); }
1957 return b - a;
1958 },
1959
1960 sortNumeric : function( a, b ) {
1961 return a - b;
1962 },
1963
1964 /*
1965 ██ ██ ██ ██ █████▄ ▄████▄ ██████ ██████ ▄█████
1966 ██ ██ ██ ██ ██ ██ ██ ▄▄▄ ██▄▄ ██ ▀█▄
1967 ██ ██ ██ ██ ██ ██ ██ ▀██ ██▀▀ ██ ▀█▄
1968 ███████▀ ██ █████▀ ▀████▀ ██████ ██ █████▀
1969 */
1970 addWidget : function( widget ) {
1971 if ( widget.id && !ts.isEmptyObject( ts.getWidgetById( widget.id ) ) ) {
1972 console.warn( '"' + widget.id + '" widget was loaded more than once!' );
1973 }
1974 ts.widgets[ ts.widgets.length ] = widget;
1975 },
1976
1977 hasWidget : function( $table, name ) {
1978 $table = $( $table );
1979 return $table.length && $table[ 0 ].config && $table[ 0 ].config.widgetInit[ name ] || false;
1980 },
1981
1982 getWidgetById : function( name ) {
1983 var indx, widget,
1984 len = ts.widgets.length;
1985 for ( indx = 0; indx < len; indx++ ) {
1986 widget = ts.widgets[ indx ];
1987 if ( widget && widget.id && widget.id.toLowerCase() === name.toLowerCase() ) {
1988 return widget;
1989 }
1990 }
1991 },
1992
1993 applyWidgetOptions : function( table ) {
1994 var indx, widget, wo,
1995 c = table.config,
1996 len = c.widgets.length;
1997 if ( len ) {
1998 for ( indx = 0; indx < len; indx++ ) {
1999 widget = ts.getWidgetById( c.widgets[ indx ] );
2000 if ( widget && widget.options ) {
2001 wo = $.extend( true, {}, widget.options );
2002 c.widgetOptions = $.extend( true, wo, c.widgetOptions );
2003 // add widgetOptions to defaults for option validator
2004 $.extend( true, ts.defaults.widgetOptions, widget.options );
2005 }
2006 }
2007 }
2008 },
2009
2010 addWidgetFromClass : function( table ) {
2011 var len, indx,
2012 c = table.config,
2013 // look for widgets to apply from table class
2014 // don't match from 'ui-widget-content'; use \S instead of \w to include widgets
2015 // with dashes in the name, e.g. "widget-test-2" extracts out "test-2"
2016 regex = '^' + c.widgetClass.replace( ts.regex.templateName, '(\\S+)+' ) + '$',
2017 widgetClass = new RegExp( regex, 'g' ),
2018 // split up table class (widget id's can include dashes) - stop using match
2019 // otherwise only one widget gets extracted, see #1109
2020 widgets = ( table.className || '' ).split( ts.regex.spaces );
2021 if ( widgets.length ) {
2022 len = widgets.length;
2023 for ( indx = 0; indx < len; indx++ ) {
2024 if ( widgets[ indx ].match( widgetClass ) ) {
2025 c.widgets[ c.widgets.length ] = widgets[ indx ].replace( widgetClass, '$1' );
2026 }
2027 }
2028 }
2029 },
2030
2031 applyWidgetId : function( table, id, init ) {
2032 table = $(table)[0];
2033 var applied, time, name,
2034 c = table.config,
2035 wo = c.widgetOptions,
2036 debug = ts.debug(c, 'core'),
2037 widget = ts.getWidgetById( id );
2038 if ( widget ) {
2039 name = widget.id;
2040 applied = false;
2041 // add widget name to option list so it gets reapplied after sorting, filtering, etc
2042 if ( $.inArray( name, c.widgets ) < 0 ) {
2043 c.widgets[ c.widgets.length ] = name;
2044 }
2045 if ( debug ) { time = new Date(); }
2046
2047 if ( init || !( c.widgetInit[ name ] ) ) {
2048 // set init flag first to prevent calling init more than once (e.g. pager)
2049 c.widgetInit[ name ] = true;
2050 if ( table.hasInitialized ) {
2051 // don't reapply widget options on tablesorter init
2052 ts.applyWidgetOptions( table );
2053 }
2054 if ( typeof widget.init === 'function' ) {
2055 applied = true;
2056 if ( debug ) {
2057 console[ console.group ? 'group' : 'log' ]( 'Initializing ' + name + ' widget' );
2058 }
2059 widget.init( table, widget, c, wo );
2060 }
2061 }
2062 if ( !init && typeof widget.format === 'function' ) {
2063 applied = true;
2064 if ( debug ) {
2065 console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' );
2066 }
2067 widget.format( table, c, wo, false );
2068 }
2069 if ( debug ) {
2070 if ( applied ) {
2071 console.log( 'Completed ' + ( init ? 'initializing ' : 'applying ' ) + name + ' widget' + ts.benchmark( time ) );
2072 if ( console.groupEnd ) { console.groupEnd(); }
2073 }
2074 }
2075 }
2076 },
2077
2078 applyWidget : function( table, init, callback ) {
2079 table = $( table )[ 0 ]; // in case this is called externally
2080 var indx, len, names, widget, time,
2081 c = table.config,
2082 debug = ts.debug(c, 'core'),
2083 widgets = [];
2084 // prevent numerous consecutive widget applications
2085 if ( init !== false && table.hasInitialized && ( table.isApplyingWidgets || table.isUpdating ) ) {
2086 return;
2087 }
2088 if ( debug ) { time = new Date(); }
2089 ts.addWidgetFromClass( table );
2090 // prevent "tablesorter-ready" from firing multiple times in a row
2091 clearTimeout( c.timerReady );
2092 if ( c.widgets.length ) {
2093 table.isApplyingWidgets = true;
2094 // ensure unique widget ids
2095 c.widgets = $.grep( c.widgets, function( val, index ) {
2096 return $.inArray( val, c.widgets ) === index;
2097 });
2098 names = c.widgets || [];
2099 len = names.length;
2100 // build widget array & add priority as needed
2101 for ( indx = 0; indx < len; indx++ ) {
2102 widget = ts.getWidgetById( names[ indx ] );
2103 if ( widget && widget.id ) {
2104 // set priority to 10 if not defined
2105 if ( !widget.priority ) { widget.priority = 10; }
2106 widgets[ indx ] = widget;
2107 } else if ( debug ) {
2108 console.warn( '"' + names[ indx ] + '" was enabled, but the widget code has not been loaded!' );
2109 }
2110 }
2111 // sort widgets by priority
2112 widgets.sort( function( a, b ) {
2113 return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1;
2114 });
2115 // add/update selected widgets
2116 len = widgets.length;
2117 if ( debug ) {
2118 console[ console.group ? 'group' : 'log' ]( 'Start ' + ( init ? 'initializing' : 'applying' ) + ' widgets' );
2119 }
2120 for ( indx = 0; indx < len; indx++ ) {
2121 widget = widgets[ indx ];
2122 if ( widget && widget.id ) {
2123 ts.applyWidgetId( table, widget.id, init );
2124 }
2125 }
2126 if ( debug && console.groupEnd ) { console.groupEnd(); }
2127 }
2128 c.timerReady = setTimeout( function() {
2129 table.isApplyingWidgets = false;
2130 $.data( table, 'lastWidgetApplication', new Date() );
2131 c.$table.triggerHandler( 'tablesorter-ready' );
2132 // callback executed on init only
2133 if ( !init && typeof callback === 'function' ) {
2134 callback( table );
2135 }
2136 if ( debug ) {
2137 widget = c.widgets.length;
2138 console.log( 'Completed ' +
2139 ( init === true ? 'initializing ' : 'applying ' ) + widget +
2140 ' widget' + ( widget !== 1 ? 's' : '' ) + ts.benchmark( time ) );
2141 }
2142 }, 10 );
2143 },
2144
2145 removeWidget : function( table, name, refreshing ) {
2146 table = $( table )[ 0 ];
2147 var index, widget, indx, len,
2148 c = table.config;
2149 // if name === true, add all widgets from $.tablesorter.widgets
2150 if ( name === true ) {
2151 name = [];
2152 len = ts.widgets.length;
2153 for ( indx = 0; indx < len; indx++ ) {
2154 widget = ts.widgets[ indx ];
2155 if ( widget && widget.id ) {
2156 name[ name.length ] = widget.id;
2157 }
2158 }
2159 } else {
2160 // name can be either an array of widgets names,
2161 // or a space/comma separated list of widget names
2162 name = ( Array.isArray( name ) ? name.join( ',' ) : name || '' ).toLowerCase().split( /[\s,]+/ );
2163 }
2164 len = name.length;
2165 for ( index = 0; index < len; index++ ) {
2166 widget = ts.getWidgetById( name[ index ] );
2167 indx = $.inArray( name[ index ], c.widgets );
2168 // don't remove the widget from config.widget if refreshing
2169 if ( indx >= 0 && refreshing !== true ) {
2170 c.widgets.splice( indx, 1 );
2171 }
2172 if ( widget && widget.remove ) {
2173 if ( ts.debug(c, 'core') ) {
2174 console.log( ( refreshing ? 'Refreshing' : 'Removing' ) + ' "' + name[ index ] + '" widget' );
2175 }
2176 widget.remove( table, c, c.widgetOptions, refreshing );
2177 c.widgetInit[ name[ index ] ] = false;
2178 }
2179 }
2180 c.$table.triggerHandler( 'widgetRemoveEnd', table );
2181 },
2182
2183 refreshWidgets : function( table, doAll, dontapply ) {
2184 table = $( table )[ 0 ]; // see issue #243
2185 var indx, widget,
2186 c = table.config,
2187 curWidgets = c.widgets,
2188 widgets = ts.widgets,
2189 len = widgets.length,
2190 list = [],
2191 callback = function( table ) {
2192 $( table ).triggerHandler( 'refreshComplete' );
2193 };
2194 // remove widgets not defined in config.widgets, unless doAll is true
2195 for ( indx = 0; indx < len; indx++ ) {
2196 widget = widgets[ indx ];
2197 if ( widget && widget.id && ( doAll || $.inArray( widget.id, curWidgets ) < 0 ) ) {
2198 list[ list.length ] = widget.id;
2199 }
2200 }
2201 ts.removeWidget( table, list.join( ',' ), true );
2202 if ( dontapply !== true ) {
2203 // call widget init if
2204 ts.applyWidget( table, doAll || false, callback );
2205 if ( doAll ) {
2206 // apply widget format
2207 ts.applyWidget( table, false, callback );
2208 }
2209 } else {
2210 callback( table );
2211 }
2212 },
2213
2214 /*
2215 ██ ██ ██████ ██ ██ ██ ██████ ██ ██████ ▄█████
2216 ██ ██ ██ ██ ██ ██ ██ ██ ██▄▄ ▀█▄
2217 ██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀█▄
2218 ▀████▀ ██ ██ ██████ ██ ██ ██ ██████ █████▀
2219 */
2220 benchmark : function( diff ) {
2221 return ( ' (' + ( new Date().getTime() - diff.getTime() ) + ' ms)' );
2222 },
2223 // deprecated ts.log
2224 log : function() {
2225 console.log( arguments );
2226 },
2227 debug : function(c, name) {
2228 return c && (
2229 c.debug === true ||
2230 typeof c.debug === 'string' && c.debug.indexOf(name) > -1
2231 );
2232 },
2233
2234 // $.isEmptyObject from jQuery v1.4
2235 isEmptyObject : function( obj ) {
2236 /*jshint forin: false */
2237 for ( var name in obj ) {
2238 return false;
2239 }
2240 return true;
2241 },
2242
2243 isValueInArray : function( column, arry ) {
2244 var indx,
2245 len = arry && arry.length || 0;
2246 for ( indx = 0; indx < len; indx++ ) {
2247 if ( arry[ indx ][ 0 ] === column ) {
2248 return indx;
2249 }
2250 }
2251 return -1;
2252 },
2253
2254 formatFloat : function( str, table ) {
2255 if ( typeof str !== 'string' || str === '' ) { return str; }
2256 // allow using formatFloat without a table; defaults to US number format
2257 var num,
2258 usFormat = table && table.config ? table.config.usNumberFormat !== false :
2259 typeof table !== 'undefined' ? table : true;
2260 if ( usFormat ) {
2261 // US Format - 1,234,567.89 -> 1234567.89
2262 str = str.replace( ts.regex.comma, '' );
2263 } else {
2264 // German Format = 1.234.567,89 -> 1234567.89
2265 // French Format = 1 234 567,89 -> 1234567.89
2266 str = str.replace( ts.regex.digitNonUS, '' ).replace( ts.regex.comma, '.' );
2267 }
2268 if ( ts.regex.digitNegativeTest.test( str ) ) {
2269 // make (#) into a negative number -> (10) = -10
2270 str = str.replace( ts.regex.digitNegativeReplace, '-$1' );
2271 }
2272 num = parseFloat( str );
2273 // return the text instead of zero
2274 return isNaN( num ) ? ( typeof str === 'string' ? str.trim() : str ) : num;
2275 },
2276
2277 isDigit : function( str ) {
2278 // replace all unwanted chars and match
2279 return isNaN( str ) ?
2280 ts.regex.digitTest.test( str.toString().replace( ts.regex.digitReplace, '' ) ) :
2281 str !== '';
2282 },
2283
2284 // computeTableHeaderCellIndexes from:
2285 // http://www.javascripttoolbox.com/lib/table/examples.php
2286 // http://www.javascripttoolbox.com/temp/table_cellindex.html
2287 computeColumnIndex : function( $rows, c ) {
2288 var i, j, k, l, cell, cells, rowIndex, rowSpan, colSpan, firstAvailCol,
2289 // total columns has been calculated, use it to set the matrixrow
2290 columns = c && c.columns || 0,
2291 matrix = [],
2292 matrixrow = new Array( columns );
2293 for ( i = 0; i < $rows.length; i++ ) {
2294 cells = $rows[ i ].cells;
2295 for ( j = 0; j < cells.length; j++ ) {
2296 cell = cells[ j ];
2297 rowIndex = i;
2298 rowSpan = cell.rowSpan || 1;
2299 colSpan = cell.colSpan || 1;
2300 if ( typeof matrix[ rowIndex ] === 'undefined' ) {
2301 matrix[ rowIndex ] = [];
2302 }
2303 // Find first available column in the first row
2304 for ( k = 0; k < matrix[ rowIndex ].length + 1; k++ ) {
2305 if ( typeof matrix[ rowIndex ][ k ] === 'undefined' ) {
2306 firstAvailCol = k;
2307 break;
2308 }
2309 }
2310 // jscs:disable disallowEmptyBlocks
2311 if ( columns && cell.cellIndex === firstAvailCol ) {
2312 // don't to anything
2313 } else if ( cell.setAttribute ) {
2314 // jscs:enable disallowEmptyBlocks
2315 // add data-column (setAttribute = IE8+)
2316 cell.setAttribute( 'data-column', firstAvailCol );
2317 } else {
2318 // remove once we drop support for IE7 - 1/12/2016
2319 $( cell ).attr( 'data-column', firstAvailCol );
2320 }
2321 for ( k = rowIndex; k < rowIndex + rowSpan; k++ ) {
2322 if ( typeof matrix[ k ] === 'undefined' ) {
2323 matrix[ k ] = [];
2324 }
2325 matrixrow = matrix[ k ];
2326 for ( l = firstAvailCol; l < firstAvailCol + colSpan; l++ ) {
2327 matrixrow[ l ] = 'x';
2328 }
2329 }
2330 }
2331 }
2332 ts.checkColumnCount($rows, matrix, matrixrow.length);
2333 return matrixrow.length;
2334 },
2335
2336 checkColumnCount : function($rows, matrix, columns) {
2337 // this DOES NOT report any tbody column issues, except for the math and
2338 // and column selector widgets
2339 var i, len,
2340 valid = true,
2341 cells = [];
2342 for ( i = 0; i < matrix.length; i++ ) {
2343 // some matrix entries are undefined when testing the footer because
2344 // it is using the rowIndex property
2345 if ( matrix[i] ) {
2346 len = matrix[i].length;
2347 if ( matrix[i].length !== columns ) {
2348 valid = false;
2349 break;
2350 }
2351 }
2352 }
2353 if ( !valid ) {
2354 $rows.each( function( indx, el ) {
2355 var cell = el.parentElement.nodeName;
2356 if ( cells.indexOf( cell ) < 0 ) {
2357 cells.push( cell );
2358 }
2359 });
2360 console.error(
2361 'Invalid or incorrect number of columns in the ' +
2362 cells.join( ' or ' ) + '; expected ' + columns +
2363 ', but found ' + len + ' columns'
2364 );
2365 }
2366 },
2367
2368 // automatically add a colgroup with col elements set to a percentage width
2369 fixColumnWidth : function( table ) {
2370 table = $( table )[ 0 ];
2371 var overallWidth, percent, $tbodies, len, index,
2372 c = table.config,
2373 $colgroup = c.$table.children( 'colgroup' );
2374 // remove plugin-added colgroup, in case we need to refresh the widths
2375 if ( $colgroup.length && $colgroup.hasClass( ts.css.colgroup ) ) {
2376 $colgroup.remove();
2377 }
2378 if ( c.widthFixed && c.$table.children( 'colgroup' ).length === 0 ) {
2379 $colgroup = $( '<colgroup class="' + ts.css.colgroup + '">' );
2380 overallWidth = c.$table.width();
2381 // only add col for visible columns - fixes #371
2382 $tbodies = c.$tbodies.find( 'tr:first' ).children( ':visible' );
2383 len = $tbodies.length;
2384 for ( index = 0; index < len; index++ ) {
2385 percent = parseInt( ( $tbodies.eq( index ).width() / overallWidth ) * 1000, 10 ) / 10 + '%';
2386 $colgroup.append( $( '<col>' ).css( 'width', percent ) );
2387 }
2388 c.$table.prepend( $colgroup );
2389 }
2390 },
2391
2392 // get sorter, string, empty, etc options for each column from
2393 // jQuery data, metadata, header option or header class name ('sorter-false')
2394 // priority = jQuery data > meta > headers option > header class name
2395 getData : function( header, configHeader, key ) {
2396 var meta, cl4ss,
2397 val = '',
2398 $header = $( header );
2399 if ( !$header.length ) { return ''; }
2400 meta = $.metadata ? $header.metadata() : false;
2401 cl4ss = ' ' + ( $header.attr( 'class' ) || '' );
2402 if ( typeof $header.data( key ) !== 'undefined' ||
2403 typeof $header.data( key.toLowerCase() ) !== 'undefined' ) {
2404 // 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder'
2405 // 'data-sort-initial-order' is assigned to 'sortInitialOrder'
2406 val += $header.data( key ) || $header.data( key.toLowerCase() );
2407 } else if ( meta && typeof meta[ key ] !== 'undefined' ) {
2408 val += meta[ key ];
2409 } else if ( configHeader && typeof configHeader[ key ] !== 'undefined' ) {
2410 val += configHeader[ key ];
2411 } else if ( cl4ss !== ' ' && cl4ss.match( ' ' + key + '-' ) ) {
2412 // include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser'
2413 val = cl4ss.match( new RegExp( '\\s' + key + '-([\\w-]+)' ) )[ 1 ] || '';
2414 }
2415 return typeof val === 'string' ? val.trim() : val;
2416 },
2417
2418 getColumnData : function( table, obj, indx, getCell, $headers ) {
2419 if ( typeof obj !== 'object' || obj === null ) {
2420 return obj;
2421 }
2422 table = $( table )[ 0 ];
2423 var $header, key,
2424 c = table.config,
2425 $cells = ( $headers || c.$headers ),
2426 // c.$headerIndexed is not defined initially
2427 $cell = c.$headerIndexed && c.$headerIndexed[ indx ] ||
2428 $cells.find( '[data-column="' + indx + '"]:last' );
2429 if ( typeof obj[ indx ] !== 'undefined' ) {
2430 return getCell ? obj[ indx ] : obj[ $cells.index( $cell ) ];
2431 }
2432 for ( key in obj ) {
2433 if ( typeof key === 'string' ) {
2434 $header = $cell
2435 // header cell with class/id
2436 .filter( key )
2437 // find elements within the header cell with cell/id
2438 .add( $cell.find( key ) );
2439 if ( $header.length ) {
2440 return obj[ key ];
2441 }
2442 }
2443 }
2444 return;
2445 },
2446
2447 // *** Process table ***
2448 // add processing indicator
2449 isProcessing : function( $table, toggle, $headers ) {
2450 $table = $( $table );
2451 var c = $table[ 0 ].config,
2452 // default to all headers
2453 $header = $headers || $table.find( '.' + ts.css.header );
2454 if ( toggle ) {
2455 // don't use sortList if custom $headers used
2456 if ( typeof $headers !== 'undefined' && c.sortList.length > 0 ) {
2457 // get headers from the sortList
2458 $header = $header.filter( function() {
2459 // get data-column from attr to keep compatibility with jQuery 1.2.6
2460 return this.sortDisabled ?
2461 false :
2462 ts.isValueInArray( parseFloat( $( this ).attr( 'data-column' ) ), c.sortList ) >= 0;
2463 });
2464 }
2465 $table.add( $header ).addClass( ts.css.processing + ' ' + c.cssProcessing );
2466 } else {
2467 $table.add( $header ).removeClass( ts.css.processing + ' ' + c.cssProcessing );
2468 }
2469 },
2470
2471 // detach tbody but save the position
2472 // don't use tbody because there are portions that look for a tbody index (updateCell)
2473 processTbody : function( table, $tb, getIt ) {
2474 table = $( table )[ 0 ];
2475 if ( getIt ) {
2476 table.isProcessing = true;
2477 $tb.before( '<colgroup class="tablesorter-savemyplace"/>' );
2478 return $.fn.detach ? $tb.detach() : $tb.remove();
2479 }
2480 var holdr = $( table ).find( 'colgroup.tablesorter-savemyplace' );
2481 $tb.insertAfter( holdr );
2482 holdr.remove();
2483 table.isProcessing = false;
2484 },
2485
2486 clearTableBody : function( table ) {
2487 $( table )[ 0 ].config.$tbodies.children().detach();
2488 },
2489
2490 // used when replacing accented characters during sorting
2491 characterEquivalents : {
2492 'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãä�
2493 å
2494 'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄ�
2495
2496 'c' : '\u00e7\u0107\u010d', // çćč
2497 'C' : '\u00c7\u0106\u010c', // ÇĆČ
2498 'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę
2499 'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ
2500 'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı
2501 'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ
2502 'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6\u014d', // óòôõöō
2503 'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÓÒÔÕÖŌ
2504 'ss': '\u00df', // ß (s sharp)
2505 'SS': '\u1e9e', // ẞ (Capital sharp s)
2506 'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů
2507 'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ
2508 },
2509
2510 replaceAccents : function( str ) {
2511 var chr,
2512 acc = '[',
2513 eq = ts.characterEquivalents;
2514 if ( !ts.characterRegex ) {
2515 ts.characterRegexArray = {};
2516 for ( chr in eq ) {
2517 if ( typeof chr === 'string' ) {
2518 acc += eq[ chr ];
2519 ts.characterRegexArray[ chr ] = new RegExp( '[' + eq[ chr ] + ']', 'g' );
2520 }
2521 }
2522 ts.characterRegex = new RegExp( acc + ']' );
2523 }
2524 if ( ts.characterRegex.test( str ) ) {
2525 for ( chr in eq ) {
2526 if ( typeof chr === 'string' ) {
2527 str = str.replace( ts.characterRegexArray[ chr ], chr );
2528 }
2529 }
2530 }
2531 return str;
2532 },
2533
2534 validateOptions : function( c ) {
2535 var setting, setting2, typ, timer,
2536 // ignore options containing an array
2537 ignore = 'headers sortForce sortList sortAppend widgets'.split( ' ' ),
2538 orig = c.originalSettings;
2539 if ( orig ) {
2540 if ( ts.debug(c, 'core') ) {
2541 timer = new Date();
2542 }
2543 for ( setting in orig ) {
2544 typ = typeof ts.defaults[setting];
2545 if ( typ === 'undefined' ) {
2546 console.warn( 'Tablesorter Warning! "table.config.' + setting + '" option not recognized' );
2547 } else if ( typ === 'object' ) {
2548 for ( setting2 in orig[setting] ) {
2549 typ = ts.defaults[setting] && typeof ts.defaults[setting][setting2];
2550 if ( $.inArray( setting, ignore ) < 0 && typ === 'undefined' ) {
2551 console.warn( 'Tablesorter Warning! "table.config.' + setting + '.' + setting2 + '" option not recognized' );
2552 }
2553 }
2554 }
2555 }
2556 if ( ts.debug(c, 'core') ) {
2557 console.log( 'validate options time:' + ts.benchmark( timer ) );
2558 }
2559 }
2560 },
2561
2562 // restore headers
2563 restoreHeaders : function( table ) {
2564 var index, $cell,
2565 c = $( table )[ 0 ].config,
2566 $headers = c.$table.find( c.selectorHeaders ),
2567 len = $headers.length;
2568 // don't use c.$headers here in case header cells were swapped
2569 for ( index = 0; index < len; index++ ) {
2570 $cell = $headers.eq( index );
2571 // only restore header cells if it is wrapped
2572 // because this is also used by the updateAll method
2573 if ( $cell.find( '.' + ts.css.headerIn ).length ) {
2574 $cell.html( c.headerContent[ index ] );
2575 }
2576 }
2577 },
2578
2579 destroy : function( table, removeClasses, callback ) {
2580 table = $( table )[ 0 ];
2581 if ( !table.hasInitialized ) { return; }
2582 // remove all widgets
2583 ts.removeWidget( table, true, false );
2584 var events,
2585 $t = $( table ),
2586 c = table.config,
2587 $h = $t.find( 'thead:first' ),
2588 $r = $h.find( 'tr.' + ts.css.headerRow ).removeClass( ts.css.headerRow + ' ' + c.cssHeaderRow ),
2589 $f = $t.find( 'tfoot:first > tr' ).children( 'th, td' );
2590 if ( removeClasses === false && $.inArray( 'uitheme', c.widgets ) >= 0 ) {
2591 // reapply uitheme classes, in case we want to maintain appearance
2592 $t.triggerHandler( 'applyWidgetId', [ 'uitheme' ] );
2593 $t.triggerHandler( 'applyWidgetId', [ 'zebra' ] );
2594 }
2595 // remove widget added rows, just in case
2596 $h.find( 'tr' ).not( $r ).remove();
2597 // disable tablesorter - not using .off( namespace ) because namespacing was
2598 // added in jQuery v1.4.3 - see http://api.jquery.com/event.namespace/
2599 events = 'sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton ' +
2600 'appendCache updateCache applyWidgetId applyWidgets refreshWidgets removeWidget destroy mouseup mouseleave ' +
2601 'keypress sortBegin sortEnd resetToLoadState '.split( ' ' )
2602 .join( c.namespace + ' ' );
2603 $t
2604 .removeData( 'tablesorter' )
2605 .off( events.replace( ts.regex.spaces, ' ' ) );
2606 c.$headers
2607 .add( $f )
2608 .removeClass( [ ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone ].join( ' ' ) )
2609 .removeAttr( 'data-column' )
2610 .removeAttr( 'aria-label' )
2611 .attr( 'aria-disabled', 'true' );
2612 $r
2613 .find( c.selectorSort )
2614 .off( ( 'mousedown mouseup keypress '.split( ' ' ).join( c.namespace + ' ' ) ).replace( ts.regex.spaces, ' ' ) );
2615 ts.restoreHeaders( table );
2616 $t.toggleClass( ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false );
2617 $t.removeClass(c.namespace.slice(1));
2618 // clear flag in case the plugin is initialized again
2619 table.hasInitialized = false;
2620 delete table.config.cache;
2621 if ( typeof callback === 'function' ) {
2622 callback( table );
2623 }
2624 if ( ts.debug(c, 'core') ) {
2625 console.log( 'tablesorter has been removed' );
2626 }
2627 }
2628
2629 };
2630
2631 $.fn.tablesorter = function( settings ) {
2632 return this.each( function() {
2633 var table = this,
2634 // merge & extend config options
2635 c = $.extend( true, {}, ts.defaults, settings, ts.instanceMethods );
2636 // save initial settings
2637 c.originalSettings = settings;
2638 // create a table from data (build table widget)
2639 if ( !table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE' ) {
2640 // return the table (in case the original target is the table's container)
2641 ts.buildTable( table, c );
2642 } else {
2643 ts.setup( table, c );
2644 }
2645 });
2646 };
2647
2648 // set up debug logs
2649 if ( !( window.console && window.console.log ) ) {
2650 // access $.tablesorter.logs for browsers that don't have a console...
2651 ts.logs = [];
2652 /*jshint -W020 */
2653 console = {};
2654 console.log = console.warn = console.error = console.table = function() {
2655 var arg = arguments.length > 1 ? arguments : arguments[0];
2656 ts.logs[ ts.logs.length ] = { date: Date.now(), log: arg };
2657 };
2658 }
2659
2660 // add default parsers
2661 ts.addParser({
2662 id : 'no-parser',
2663 is : function() {
2664 return false;
2665 },
2666 format : function() {
2667 return '';
2668 },
2669 type : 'text'
2670 });
2671
2672 ts.addParser({
2673 id : 'text',
2674 is : function() {
2675 return true;
2676 },
2677 format : function( str, table ) {
2678 var c = table.config;
2679 if ( str ) {
2680 str = c.ignoreCase ? str.toLocaleLowerCase() : str;
2681 str = str.trim();
2682 str = c.sortLocaleCompare ? ts.replaceAccents( str ) : str;
2683 }
2684 return str;
2685 },
2686 type : 'text'
2687 });
2688
2689 ts.regex.nondigit = /[^\w,. \-()]/g;
2690 ts.addParser({
2691 id : 'digit',
2692 is : function( str ) {
2693 return ts.isDigit( str );
2694 },
2695 format : function( str, table ) {
2696 var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table );
2697 var tmp = (str && table.config.ignoreCase ? str.toLocaleLowerCase() : str);
2698 return str && typeof num === 'number' ? num :
2699 str ? (typeof tmp === 'string' ? tmp.trim() : tmp ) : str;
2700 },
2701 type : 'numeric'
2702 });
2703
2704 ts.regex.currencyReplace = /[+\-,. ]/g;
2705 ts.regex.currencyTest = /^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/;
2706 ts.addParser({
2707 id : 'currency',
2708 is : function( str ) {
2709 str = ( str || '' ).replace( ts.regex.currencyReplace, '' );
2710 // test for £$€¤¥¢
2711 return ts.regex.currencyTest.test( str );
2712 },
2713 format : function( str, table ) {
2714 var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table );
2715 var tmp = (str && table.config.ignoreCase ? str.toLocaleLowerCase() : str);
2716 return str && typeof num === 'number' ? num :
2717 str ? ( typeof tmp === 'string' ? tmp.trim() : tmp ) : str;
2718 },
2719 type : 'numeric'
2720 });
2721
2722 // too many protocols to add them all https://en.wikipedia.org/wiki/URI_scheme
2723 // now, this regex can be updated before initialization
2724 ts.regex.urlProtocolTest = /^(https?|ftp|file):\/\//;
2725 ts.regex.urlProtocolReplace = /(https?|ftp|file):\/\/(www\.)?/;
2726 ts.addParser({
2727 id : 'url',
2728 is : function( str ) {
2729 return ts.regex.urlProtocolTest.test( str );
2730 },
2731 format : function( str ) {
2732 return str ? str.replace( ts.regex.urlProtocolReplace, '' ).trim() : str;
2733 },
2734 type : 'text'
2735 });
2736
2737 ts.regex.dash = /-/g;
2738 ts.regex.isoDate = /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/;
2739 ts.addParser({
2740 id : 'isoDate',
2741 is : function( str ) {
2742 return ts.regex.isoDate.test( str );
2743 },
2744 format : function( str ) {
2745 var date = str ? new Date( str.replace( ts.regex.dash, '/' ) ) : str;
2746 return date instanceof Date && isFinite( date ) ? date.getTime() : str;
2747 },
2748 type : 'numeric'
2749 });
2750
2751 ts.regex.percent = /%/g;
2752 ts.regex.percentTest = /(\d\s*?%|%\s*?\d)/;
2753 ts.addParser({
2754 id : 'percent',
2755 is : function( str ) {
2756 return ts.regex.percentTest.test( str ) && str.length < 15;
2757 },
2758 format : function( str, table ) {
2759 return str ? ts.formatFloat( str.replace( ts.regex.percent, '' ), table ) : str;
2760 },
2761 type : 'numeric'
2762 });
2763
2764 // added image parser to core v2.17.9
2765 ts.addParser({
2766 id : 'image',
2767 is : function( str, table, node, $node ) {
2768 return $node.find( 'img' ).length > 0;
2769 },
2770 format : function( str, table, cell ) {
2771 return $( cell ).find( 'img' ).attr( table.config.imgAttr || 'alt' ) || str;
2772 },
2773 parsed : true, // filter widget flag
2774 type : 'text'
2775 });
2776
2777 ts.regex.dateReplace = /(\S)([AP]M)$/i; // used by usLongDate & time parser
2778 ts.regex.usLongDateTest1 = /^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i;
2779 ts.regex.usLongDateTest2 = /^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i;
2780 ts.addParser({
2781 id : 'usLongDate',
2782 is : function( str ) {
2783 // two digit years are not allowed cross-browser
2784 // Jan 01, 2013 12:34:56 PM or 01 Jan 2013
2785 return ts.regex.usLongDateTest1.test( str ) || ts.regex.usLongDateTest2.test( str );
2786 },
2787 format : function( str ) {
2788 var date = str ? new Date( str.replace( ts.regex.dateReplace, '$1 $2' ) ) : str;
2789 return date instanceof Date && isFinite( date ) ? date.getTime() : str;
2790 },
2791 type : 'numeric'
2792 });
2793
2794 // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included
2795 ts.regex.shortDateTest = /(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/;
2796 // escaped "-" because JSHint in Firefox was showing it as an error
2797 ts.regex.shortDateReplace = /[\-.,]/g;
2798 // XXY covers MDY & DMY formats
2799 ts.regex.shortDateXXY = /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/;
2800 ts.regex.shortDateYMD = /(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/;
2801 ts.convertFormat = function( dateString, format ) {
2802 dateString = ( dateString || '' )
2803 .replace( ts.regex.spaces, ' ' )
2804 .replace( ts.regex.shortDateReplace, '/' );
2805 if ( format === 'mmddyyyy' ) {
2806 dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$1/$2' );
2807 } else if ( format === 'ddmmyyyy' ) {
2808 dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$2/$1' );
2809 } else if ( format === 'yyyymmdd' ) {
2810 dateString = dateString.replace( ts.regex.shortDateYMD, '$1/$2/$3' );
2811 }
2812 var date = new Date( dateString );
2813 return date instanceof Date && isFinite( date ) ? date.getTime() : '';
2814 };
2815
2816 ts.addParser({
2817 id : 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd'
2818 is : function( str ) {
2819 str = ( str || '' ).replace( ts.regex.spaces, ' ' ).replace( ts.regex.shortDateReplace, '/' );
2820 return ts.regex.shortDateTest.test( str );
2821 },
2822 format : function( str, table, cell, cellIndex ) {
2823 if ( str ) {
2824 var c = table.config,
2825 $header = c.$headerIndexed[ cellIndex ],
2826 format = $header.length && $header.data( 'dateFormat' ) ||
2827 ts.getData( $header, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat' ) ||
2828 c.dateFormat;
2829 // save format because getData can be slow...
2830 if ( $header.length ) {
2831 $header.data( 'dateFormat', format );
2832 }
2833 return ts.convertFormat( str, format ) || str;
2834 }
2835 return str;
2836 },
2837 type : 'numeric'
2838 });
2839
2840 // match 24 hour time & 12 hours time + am/pm - see http://regexr.com/3c3tk
2841 ts.regex.timeTest = /^(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)$|^((?:[01]\d|[2][0-4]):[0-5]\d)$/i;
2842 ts.regex.timeMatch = /(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)/i;
2843 ts.addParser({
2844 id : 'time',
2845 is : function( str ) {
2846 return ts.regex.timeTest.test( str );
2847 },
2848 format : function( str ) {
2849 // isolate time... ignore month, day and year
2850 var temp,
2851 timePart = ( str || '' ).match( ts.regex.timeMatch ),
2852 orig = new Date( str ),
2853 // no time component? default to 00:00 by leaving it out, but only if str is defined
2854 time = str && ( timePart !== null ? timePart[ 0 ] : '00:00 AM' ),
2855 date = time ? new Date( '2000/01/01 ' + time.replace( ts.regex.dateReplace, '$1 $2' ) ) : time;
2856 if ( date instanceof Date && isFinite( date ) ) {
2857 temp = orig instanceof Date && isFinite( orig ) ? orig.getTime() : 0;
2858 // if original string was a valid date, add it to the decimal so the column sorts in some kind of order
2859 // luckily new Date() ignores the decimals
2860 return temp ? parseFloat( date.getTime() + '.' + orig.getTime() ) : date.getTime();
2861 }
2862 return str;
2863 },
2864 type : 'numeric'
2865 });
2866
2867 ts.addParser({
2868 id : 'metadata',
2869 is : function() {
2870 return false;
2871 },
2872 format : function( str, table, cell ) {
2873 var c = table.config,
2874 p = ( !c.parserMetadataName ) ? 'sortValue' : c.parserMetadataName;
2875 return $( cell ).metadata()[ p ];
2876 },
2877 type : 'numeric'
2878 });
2879
2880 /*
2881 ██████ ██████ █████▄ █████▄ ▄████▄
2882 ▄█▀ ██▄▄ ██▄▄██ ██▄▄██ ██▄▄██
2883 ▄█▀ ██▀▀ ██▀▀██ ██▀▀█ ██▀▀██
2884 ██████ ██████ █████▀ ██ ██ ██ ██
2885 */
2886 // add default widgets
2887 ts.addWidget({
2888 id : 'zebra',
2889 priority : 90,
2890 format : function( table, c, wo ) {
2891 var $visibleRows, $row, count, isEven, tbodyIndex, rowIndex, len,
2892 child = new RegExp( c.cssChildRow, 'i' ),
2893 $tbodies = c.$tbodies.add( $( c.namespace + '_extra_table' ).children( 'tbody:not(.' + c.cssInfoBlock + ')' ) );
2894 for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
2895 // loop through the visible rows
2896 count = 0;
2897 $visibleRows = $tbodies.eq( tbodyIndex ).children( 'tr:visible' ).not( c.selectorRemove );
2898 len = $visibleRows.length;
2899 for ( rowIndex = 0; rowIndex < len; rowIndex++ ) {
2900 $row = $visibleRows.eq( rowIndex );
2901 // style child rows the same way the parent row was styled
2902 if ( !child.test( $row[ 0 ].className ) ) { count++; }
2903 isEven = ( count % 2 === 0 );
2904 $row
2905 .removeClass( wo.zebra[ isEven ? 1 : 0 ] )
2906 .addClass( wo.zebra[ isEven ? 0 : 1 ] );
2907 }
2908 }
2909 },
2910 remove : function( table, c, wo, refreshing ) {
2911 if ( refreshing ) { return; }
2912 var tbodyIndex, $tbody,
2913 $tbodies = c.$tbodies,
2914 toRemove = ( wo.zebra || [ 'even', 'odd' ] ).join( ' ' );
2915 for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
2916 $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody
2917 $tbody.children().removeClass( toRemove );
2918 ts.processTbody( table, $tbody, false ); // restore tbody
2919 }
2920 }
2921 });
2922
2923 })( jQuery );
2924 return jQuery.tablesorter;}));
2925