TableSorter

Client-side table sorting with ease

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greatest.deepsurf.us/scripts/5844/836943/TableSorter.js

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