jqueryTablesorter

Jquery tablesorter

As of 2018-08-24. See the latest version.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/371532/623264/jqueryTablesorter.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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