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