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