PageSourceSearch

https://casinopulse.club/src/frontend/assets/js/plugins/jquery.tablesorter.js

js casinopulse.club collected 2026-10-09 23:25:40 UTC 124,687 bytes, 3,023 lines download raw bytes

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

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