[ Index ]

PHP Cross Reference of GlotPress

title

Body

[close]

/assets/js/vendor/ -> jquery.tablesorter.js (source)

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


Generated: Thu Nov 26 01:02:01 2020 Cross-referenced by PHPXref 0.7.1