vendor: 233,211 bytes, lines 1-6078
1/*! tablesorter (FORK) - updated 2018-05-28 (v2.30.5)*/ 2/* Includes widgets ( storage,uitheme,columns,filter,stickyHeaders,resizable,saveSort ) */ 3(function(factory) { 4 if (typeof define === 'function' && define.amd) { 5 define(['jquery'], factory); 6 } else if (typeof module === 'object' && typeof module.exports === 'object') { 7 module.exports = factory(require('jquery')); 8 } else { 9 factory(jQuery); 10 } 11}(function(jQuery) { 12 13/*! TableSorter (FORK) v2.30.5 *//* 14* Client-side table sorting with ease! 15* @requires jQuery v1.2.6+ 16* 17* Copyright (c) 2007 Christian Bach 18* fork maintained by Rob Garrison 19* 20* Examples and original docs at: http://tablesorter.com 21* Dual licensed under the MIT and GPL licenses: 22* http://www.opensource.org/licenses/mit-license.php 23* http://www.gnu.org/licenses/gpl.html 24* 25* @type jQuery 26* @name tablesorter (FORK) 27* @cat Plugins/Tablesorter 28* @author Christian Bach - [email protected] 29* @contributor Rob Garrison - https://github.com/Mottie/tablesorter 30* @docs (fork) - https://mottie.github.io/tablesorter/docs/ 31*/ 32/*jshint browser:true, jquery:true, unused:false, expr: true */ 33;( function( $ ) { 34 'use strict'; 35 var ts = $.tablesorter = { 36 37 version : '2.30.5', 38 39 parsers : [], 40 widgets : [], 41 defaults : { 42 43 // *** appearance 44 theme : 'default', // adds tablesorter-{theme} to the table for styling 45 widthFixed : false, // adds colgroup to fix widths of columns 46 showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered. 47 48 headerTemplate : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = <i/> // class from cssIcon 49 onRenderTemplate : null, // function( index, template ) { return template; }, // template is a string 50 onRenderHeader : null, // function( index ) {}, // nothing to return 51 52 // *** functionality 53 cancelSelection : true, // prevent text selection in the header 54 tabIndex : true, // add tabindex to header for keyboard accessibility 55 dateFormat : 'mmddyyyy', // other options: 'ddmmyyy' or 'yyyymmdd' 56 sortMultiSortKey : 'shiftKey', // key used to select additional columns 57 sortResetKey : 'ctrlKey', // key used to remove sorting on a column 58 usNumberFormat : true, // false for German '1.234.567,89' or French '1 234 567,89' 59 delayInit : false, // if false, the parsed table contents will not update until the first sort 60 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. 61 resort : true, // default setting to trigger a resort after an 'update', 'addRows', 'updateCell', etc has completed 62 63 // *** sort options 64 headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc. 65 ignoreCase : true, // ignore case while sorting 66 sortForce : null, // column(s) first sorted; always applied 67 sortList : [], // Initial sort order; applied initially; updated when manually sorted 68 sortAppend : null, // column(s) sorted last; always applied 69 sortStable : false, // when sorting two rows with exactly the same content, the original sort order is maintained 70 71 sortInitialOrder : 'asc', // sort direction on first click 72 sortLocaleCompare: false, // replace equivalent character (accented characters) 73 sortReset : false, // third click on the header will reset column to default - unsorted 74 sortRestart : false, // restart sort to 'sortInitialOrder' when clicking on previously unsorted columns 75 76 emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero, emptyMax, emptyMin 77 stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero 78 duplicateSpan : true, // colspan cells in the tbody will have duplicated content in the cache for each spanned column 79 textExtraction : 'basic', // text extraction method/function - function( node, table, cellIndex ) {} 80 textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in default textExtraction function) 81 textSorter : null, // choose overall or specific column sorter function( a, b, direction, table, columnIndex ) [alt: ts.sortText] 82 numberSorter : null, // choose overall numeric sorter function( a, b, direction, maxColumnValue ) 83 84 // *** widget options 85 initWidgets : true, // apply widgets on tablesorter initialization 86 widgetClass : 'widget-{name}', // table class name template to match to include a widget 87 widgets : [], // method to add widgets, e.g. widgets: ['zebra'] 88 widgetOptions : { 89 zebra : [ 'even', 'odd' ] // zebra widget alternating row class names 90 }, 91 92 // *** callbacks 93 initialized : null, // function( table ) {}, 94 95 // *** extra css class names 96 tableClass : '', 97 cssAsc : '', 98 cssDesc : '', 99 cssNone : '', 100 cssHeader : '', 101 cssHeaderRow : '', 102 cssProcessing : '', // processing icon applied to header during sort/filter 103 104 cssChildRow : 'tablesorter-childRow', // class name indiciating that a row is to be attached to its parent 105 cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!) 106 cssNoSort : 'tablesorter-noSort', // class name added to element inside header; clicking on it won't cause a sort 107 cssIgnoreRow : 'tablesorter-ignoreRow',// header row to ignore; cells within this row will not be added to c.$headers 108 109 cssIcon : 'tablesorter-icon', // if this class does not exist, the {icon} will not be added from the headerTemplate 110 cssIconNone : '', // class name added to the icon when there is no column sort 111 cssIconAsc : '', // class name added to the icon when the column has an ascending sort 112 cssIconDesc : '', // class name added to the icon when the column has a descending sort 113 cssIconDisabled : '', // class name added to the icon when the column has a disabled sort 114 115 // *** events 116 pointerClick : 'click', 117 pointerDown : 'mousedown', 118 pointerUp : 'mouseup', 119 120 // *** selectors 121 selectorHeaders : '> thead th, > thead td', 122 selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort 123 selectorRemove : '.remove-me', 124 125 // *** advanced 126 debug : false, 127 128 // *** Internal variables 129 headerList: [], 130 empties: {}, 131 strings: {}, 132 parsers: [], 133 134 // *** parser options for validator; values must be falsy! 135 globalize: 0, 136 imgAttr: 0 137 138 // removed: widgetZebra: { css: ['even', 'odd'] } 139 140 }, 141 142 // internal css classes - these will ALWAYS be added to 143 // the table and MUST only contain one class name - fixes #381 144 css : { 145 table : 'tablesorter', 146 cssHasChild: 'tablesorter-hasChildRow', 147 childRow : 'tablesorter-childRow', 148 colgroup : 'tablesorter-colgroup', 149 header : 'tablesorter-header', 150 headerRow : 'tablesorter-headerRow', 151 headerIn : 'tablesorter-header-inner', 152 icon : 'tablesorter-icon', 153 processing : 'tablesorter-processing', 154 sortAsc : 'tablesorter-headerAsc', 155 sortDesc : 'tablesorter-headerDesc', 156 sortNone : 'tablesorter-headerUnSorted' 157 }, 158 159 // labels applied to sortable headers for accessibility (aria) support 160 language : { 161 sortAsc : 'Ascending sort applied, ', 162 sortDesc : 'Descending sort applied, ', 163 sortNone : 'No sort applied, ', 164 sortDisabled : 'sorting is disabled', 165 nextAsc : 'activate to apply an ascending sort', 166 nextDesc : 'activate to apply a descending sort', 167 nextNone : 'activate to remove the sort' 168 }, 169 170 regex : { 171 templateContent : /\{content\}/g, 172 templateIcon : /\{icon\}/g, 173 templateName : /\{name\}/i, 174 spaces : /\s+/g, 175 nonWord : /\W/g, 176 formElements : /(input|select|button|textarea)/i, 177 178 // *** sort functions *** 179 // regex used in natural sort 180 // chunk/tokenize numbers & letters 181 chunk : /(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, 182 // replace chunks @ ends 183 chunks : /(^\\0|\\0$)/, 184 hex : /^0x[0-9a-f]+$/i, 185 186 // *** formatFloat *** 187 comma : /,/g, 188 digitNonUS : /[\s|\.]/g, 189 digitNegativeTest : /^\s*\([.\d]+\)/, 190 digitNegativeReplace : /^\s*\(([.\d]+)\)/, 191 192 // *** isDigit *** 193 digitTest : /^[\-+(]?\d+[)]?$/, 194 digitReplace : /[,.'"\s]/g 195 196 }, 197 198 // digit sort, text location 199 string : { 200 max : 1, 201 min : -1, 202 emptymin : 1, 203 emptymax : -1, 204 zero : 0, 205 none : 0, 206 'null' : 0, 207 top : true, 208 bottom : false 209 }, 210 211 keyCodes : { 212 enter : 13 213 }, 214 215 // placeholder date parser data (globalize) 216 dates : {}, 217 218 // These methods can be applied on table.config instance 219 instanceMethods : {}, 220 221 /* 222 ââââââ ââââââ ââââââ ââ ââ ââââââ 223 âââ ââââ ââ ââ ââ ââââââ 224 âââ ââââ ââ ââ ââ âââââ 225 ââââââ ââââââ ââ ââââââ ââ 226 */ 227 228 setup : function( table, c ) { 229 // if no thead or tbody, or tablesorter is already present, quit 230 if ( !table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true ) { 231 if ( ts.debug(c, 'core') ) { 232 if ( table.hasInitialized ) { 233 console.warn( 'Stopping initialization. Tablesorter has already been initialized' ); 234 } else { 235 console.error( 'Stopping initialization! No table, thead or tbody', table ); 236 } 237 } 238 return; 239 } 240 241 var tmp = '', 242 $table = $( table ), 243 meta = $.metadata; 244 // initialization flag 245 table.hasInitialized = false; 246 // table is being processed flag 247 table.isProcessing = true; 248 // make sure to store the config object 249 table.config = c; 250 // save the settings where they read 251 $.data( table, 'tablesorter', c ); 252 if ( ts.debug(c, 'core') ) { 253 console[ console.group ? 'group' : 'log' ]( 'Initializing tablesorter v' + ts.version ); 254 $.data( table, 'startoveralltimer', new Date() ); 255 } 256 257 // removing this in version 3 (only supports jQuery 1.7+) 258 c.supportsDataObject = ( function( version ) { 259 version[ 0 ] = parseInt( version[ 0 ], 10 ); 260 return ( version[ 0 ] > 1 ) || ( version[ 0 ] === 1 && parseInt( version[ 1 ], 10 ) >= 4 ); 261 })( $.fn.jquery.split( '.' ) ); 262 // ensure case insensitivity 263 c.emptyTo = c.emptyTo.toLowerCase(); 264 c.stringTo = c.stringTo.toLowerCase(); 265 c.last = { sortList : [], clickedIndex : -1 }; 266 // add table theme class only if there isn't already one there 267 if ( !/tablesorter\-/.test( $table.attr( 'class' ) ) ) { 268 tmp = ( c.theme !== '' ? ' tablesorter-' + c.theme : '' ); 269 } 270 271 // give the table a unique id, which will be used in namespace binding 272 if ( !c.namespace ) { 273 c.namespace = '.tablesorter' + Math.random().toString( 16 ).slice( 2 ); 274 } else { 275 // make sure namespace starts with a period & doesn't have weird characters 276 c.namespace = '.' + c.namespace.replace( ts.regex.nonWord, '' ); 277 } 278 279 c.table = table; 280 c.$table = $table 281 // add namespace to table to allow bindings on extra elements to target 282 // the parent table (e.g. parser-input-select) 283 .addClass( ts.css.table + ' ' + c.tableClass + tmp + ' ' + c.namespace.slice(1) ) 284 .attr( 'role', 'grid' ); 285 c.$headers = $table.find( c.selectorHeaders ); 286 287 c.$table.children().children( 'tr' ).attr( 'role', 'row' ); 288 c.$tbodies = $table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ).attr({ 289 'aria-live' : 'polite', 290 'aria-relevant' : 'all' 291 }); 292 if ( c.$table.children( 'caption' ).length ) { 293 tmp = c.$table.children( 'caption' )[ 0 ]; 294 if ( !tmp.id ) { tmp.id = c.namespace.slice( 1 ) + 'caption'; } 295 c.$table.attr( 'aria-labelledby', tmp.id ); 296 } 297 c.widgetInit = {}; // keep a list of initialized widgets 298 // change textExtraction via data-attribute 299 c.textExtraction = c.$table.attr( 'data-text-extraction' ) || c.textExtraction || 'basic'; 300 // build headers 301 ts.buildHeaders( c ); 302 // fixate columns if the users supplies the fixedWidth option 303 // do this after theme has been applied 304 ts.fixColumnWidth( table ); 305 // add widgets from class name 306 ts.addWidgetFromClass( table ); 307 // add widget options before parsing (e.g. grouping widget has parser settings) 308 ts.applyWidgetOptions( table ); 309 // try to auto detect column type, and store in tables config 310 ts.setupParsers( c ); 311 // start total row count at zero 312 c.totalRows = 0; 313 // only validate options while debugging. See #1528 314 if (c.debug) { 315 ts.validateOptions( c ); 316 } 317 // build the cache for the tbody cells 318 // delayInit will delay building the cache until the user starts a sort 319 if ( !c.delayInit ) { ts.buildCache( c ); } 320 // bind all header events and methods 321 ts.bindEvents( table, c.$headers, true ); 322 ts.bindMethods( c ); 323 // get sort list from jQuery data or metadata 324 // in jQuery < 1.4, an error occurs when calling $table.data() 325 if ( c.supportsDataObject && typeof $table.data().sortlist !== 'undefined' ) { 326 c.sortList = $table.data().sortlist; 327 } else if ( meta && ( $table.metadata() && $table.metadata().sortlist ) ) { 328 c.sortList = $table.metadata().sortlist; 329 } 330 // apply widget init code 331 ts.applyWidget( table, true ); 332 // if user has supplied a sort list to constructor 333 if ( c.sortList.length > 0 ) { 334 ts.sortOn( c, c.sortList, {}, !c.initWidgets ); 335 } else { 336 ts.setHeadersCss( c ); 337 if ( c.initWidgets ) { 338 // apply widget format 339 ts.applyWidget( table, false ); 340 } 341 } 342 343 // show processesing icon 344 if ( c.showProcessing ) { 345 $table 346 .unbind( 'sortBegin' + c.namespace + ' sortEnd' + c.namespace ) 347 .bind( 'sortBegin' + c.namespace + ' sortEnd' + c.namespace, function( e ) { 348 clearTimeout( c.timerProcessing ); 349 ts.isProcessing( table ); 350 if ( e.type === 'sortBegin' ) { 351 c.timerProcessing = setTimeout( function() { 352 ts.isProcessing( table, true ); 353 }, 500 ); 354 } 355 }); 356 } 357 358 // initialized 359 table.hasInitialized = true; 360 table.isProcessing = false; 361 if ( ts.debug(c, 'core') ) { 362 console.log( 'Overall initialization time:' + ts.benchmark( $.data( table, 'startoveralltimer' ) ) ); 363 if ( ts.debug(c, 'core') && console.groupEnd ) { console.groupEnd(); } 364 } 365 $table.triggerHandler( 'tablesorter-initialized', table ); 366 if ( typeof c.initialized === 'function' ) { 367 c.initialized( table ); 368 } 369 }, 370 371 bindMethods : function( c ) { 372 var $table = c.$table, 373 namespace = c.namespace, 374 events = ( 'sortReset update updateRows updateAll updateHeaders addRows updateCell updateComplete ' + 375 'sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup ' + 376 'mouseleave ' ).split( ' ' ) 377 .join( namespace + ' ' ); 378 // apply easy methods that trigger bound events 379 $table 380 .unbind( events.replace( ts.regex.spaces, ' ' ) ) 381 .bind( 'sortReset' + namespace, function( e, callback ) { 382 e.stopPropagation(); 383 // using this.config to ensure functions are getting a non-cached version of the config 384 ts.sortReset( this.config, function( table ) { 385 if (table.isApplyingWidgets) { 386 // multiple triggers in a row... filterReset, then sortReset - see #1361 387 // wait to update widgets 388 setTimeout( function() { 389 ts.applyWidget( table, '', callback ); 390 }, 100 ); 391 } else { 392 ts.applyWidget( table, '', callback ); 393 } 394 }); 395 }) 396 .bind( 'updateAll' + namespace, function( e, resort, callback ) { 397 e.stopPropagation(); 398 ts.updateAll( this.config, resort, callback ); 399 }) 400 .bind( 'update' + namespace + ' updateRows' + namespace, function( e, resort, callback ) { 401 e.stopPropagation(); 402 ts.update( this.config, resort, callback ); 403 }) 404 .bind( 'updateHeaders' + namespace, function( e, callback ) { 405 e.stopPropagation(); 406 ts.updateHeaders( this.config, callback ); 407 }) 408 .bind( 'updateCell' + namespace, function( e, cell, resort, callback ) { 409 e.stopPropagation(); 410 ts.updateCell( this.config, cell, resort, callback ); 411 }) 412 .bind( 'addRows' + namespace, function( e, $row, resort, callback ) { 413 e.stopPropagation(); 414 ts.addRows( this.config, $row, resort, callback ); 415 }) 416 .bind( 'updateComplete' + namespace, function() { 417 this.isUpdating = false; 418 }) 419 .bind( 'sorton' + namespace, function( e, list, callback, init ) { 420 e.stopPropagation(); 421 ts.sortOn( this.config, list, callback, init ); 422 }) 423 .bind( 'appendCache' + namespace, function( e, callback, init ) { 424 e.stopPropagation(); 425 ts.appendCache( this.config, init ); 426 if ( $.isFunction( callback ) ) { 427 callback( this ); 428 } 429 }) 430 // $tbodies variable is used by the tbody sorting widget 431 .bind( 'updateCache' + namespace, function( e, callback, $tbodies ) { 432 e.stopPropagation(); 433 ts.updateCache( this.config, callback, $tbodies ); 434 }) 435 .bind( 'applyWidgetId' + namespace, function( e, id ) { 436 e.stopPropagation(); 437 ts.applyWidgetId( this, id ); 438 }) 439 .bind( 'applyWidgets' + namespace, function( e, callback ) { 440 e.stopPropagation(); 441 // apply widgets (false = not initializing) 442 ts.applyWidget( this, false, callback ); 443 }) 444 .bind( 'refreshWidgets' + namespace, function( e, all, dontapply ) { 445 e.stopPropagation(); 446 ts.refreshWidgets( this, all, dontapply ); 447 }) 448 .bind( 'removeWidget' + namespace, function( e, name, refreshing ) { 449 e.stopPropagation(); 450 ts.removeWidget( this, name, refreshing ); 451 }) 452 .bind( 'destroy' + namespace, function( e, removeClasses, callback ) { 453 e.stopPropagation(); 454 ts.destroy( this, removeClasses, callback ); 455 }) 456 .bind( 'resetToLoadState' + namespace, function( e ) { 457 e.stopPropagation(); 458 // remove all widgets 459 ts.removeWidget( this, true, false ); 460 var tmp = $.extend( true, {}, c.originalSettings ); 461 // restore original settings; this clears out current settings, but does not clear 462 // values saved to storage. 463 c = $.extend( true, {}, ts.defaults, tmp ); 464 c.originalSettings = tmp; 465 this.hasInitialized = false; 466 // setup the entire table again 467 ts.setup( this, c ); 468 }); 469 }, 470 471 bindEvents : function( table, $headers, core ) { 472 table = $( table )[ 0 ]; 473 var tmp, 474 c = table.config, 475 namespace = c.namespace, 476 downTarget = null; 477 if ( core !== true ) { 478 $headers.addClass( namespace.slice( 1 ) + '_extra_headers' ); 479 tmp = ts.getClosest( $headers, 'table' ); 480 if ( tmp.length && tmp[ 0 ].nodeName === 'TABLE' && tmp[ 0 ] !== table ) { 481 $( tmp[ 0 ] ).addClass( namespace.slice( 1 ) + '_extra_table' ); 482 } 483 } 484 tmp = ( c.pointerDown + ' ' + c.pointerUp + ' ' + c.pointerClick + ' sort keyup ' ) 485 .replace( ts.regex.spaces, ' ' ) 486 .split( ' ' ) 487 .join( namespace + ' ' ); 488 // apply event handling to headers and/or additional headers (stickyheaders, scroller, etc) 489 $headers 490 // http://stackoverflow.com/questions/5312849/jquery-find-self; 491 .find( c.selectorSort ) 492 .add( $headers.filter( c.selectorSort ) ) 493 .unbind( tmp ) 494 .bind( tmp, function( e, external ) { 495 var $cell, cell, temp, 496 $target = $( e.target ), 497 // wrap event type in spaces, so the match doesn't trigger on inner words 498 type = ' ' + e.type + ' '; 499 // only recognize left clicks 500 if ( ( ( e.which || e.button ) !== 1 && !type.match( ' ' + c.pointerClick + ' | sort | keyup ' ) ) || 501 // allow pressing enter 502 ( type === ' keyup ' && e.which !== ts.keyCodes.enter ) || 503 // allow triggering a click event (e.which is undefined) & ignore physical clicks 504 ( type.match( ' ' + c.pointerClick + ' ' ) && typeof e.which !== 'undefined' ) ) { 505 return; 506 } 507 // ignore mouseup if mousedown wasn't on the same target 508 if ( type.match( ' ' + c.pointerUp + ' ' ) && downTarget !== e.target && external !== true ) { 509 return; 510 } 511 // set target on mousedown 512 if ( type.match( ' ' + c.pointerDown + ' ' ) ) { 513 downTarget = e.target; 514 // preventDefault needed or jQuery v1.3.2 and older throws an 515 // "Uncaught TypeError: handler.apply is not a function" error 516 temp = $target.jquery.split( '.' ); 517 if ( temp[ 0 ] === '1' && temp[ 1 ] < 4 ) { e.preventDefault(); } 518 return; 519 } 520 downTarget = null; 521 $cell = ts.getClosest( $( this ), '.' + ts.css.header ); 522 // prevent sort being triggered on form elements 523 if ( ts.regex.formElements.test( e.target.nodeName ) || 524 // nosort class name, or elements within a nosort container 525 $target.hasClass( c.cssNoSort ) || $target.parents( '.' + c.cssNoSort ).length > 0 || 526 // disabled cell directly clicked 527 $cell.hasClass( 'sorter-false' ) || 528 // elements within a button 529 $target.parents( 'button' ).length > 0 ) { 530 return !c.cancelSelection; 531 } 532 if ( c.delayInit && ts.isEmptyObject( c.cache ) ) { 533 ts.buildCache( c ); 534 } 535 // use column index from data-attribute or index of current row; fixes #1116 536 c.last.clickedIndex = $cell.attr( 'data-column' ) || $cell.index(); 537 cell = c.$headerIndexed[ c.last.clickedIndex ][0]; 538 if ( cell && !cell.sortDisabled ) { 539 ts.initSort( c, cell, e ); 540 } 541 }); 542 if ( c.cancelSelection ) { 543 // cancel selection 544 $headers 545 .attr( 'unselectable', 'on' ) 546 .bind( 'selectstart', false ) 547 .css({ 548 'user-select' : 'none', 549 'MozUserSelect' : 'none' // not needed for jQuery 1.8+ 550 }); 551 } 552 }, 553 554 buildHeaders : function( c ) { 555 var $temp, icon, timer, indx; 556 c.headerList = []; 557 c.headerContent = []; 558 c.sortVars = []; 559 if ( ts.debug(c, 'core') ) { 560 timer = new Date(); 561 } 562 // children tr in tfoot - see issue #196 & #547 563 // don't pass table.config to computeColumnIndex here - widgets (math) pass it to "quickly" index tbody cells 564 c.columns = ts.computeColumnIndex( c.$table.children( 'thead, tfoot' ).children( 'tr' ) ); 565 // add icon if cssIcon option exists 566 icon = c.cssIcon ? 567 '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' : 568 ''; 569 // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683 570 c.$headers = $( $.map( c.$table.find( c.selectorHeaders ), function( elem, index ) { 571 var configHeaders, header, column, template, tmp, 572 $elem = $( elem ); 573 // ignore cell (don't add it to c.$headers) if row has ignoreRow class 574 if ( ts.getClosest( $elem, 'tr' ).hasClass( c.cssIgnoreRow ) ) { return; } 575 // transfer data-column to element if not th/td - #1459 576 if ( !/(th|td)/i.test( elem.nodeName ) ) { 577 tmp = ts.getClosest( $elem, 'th, td' ); 578 $elem.attr( 'data-column', tmp.attr( 'data-column' ) ); 579 } 580 // make sure to get header cell & not column indexed cell 581 configHeaders = ts.getColumnData( c.table, c.headers, index, true ); 582 // save original header content 583 c.headerContent[ index ] = $elem.html(); 584 // if headerTemplate is empty, don't reformat the header cell 585 if ( c.headerTemplate !== '' && !$elem.find( '.' + ts.css.headerIn ).length ) { 586 // set up header template 587 template = c.headerTemplate 588 .replace( ts.regex.templateContent, $elem.html() ) 589 .replace( ts.regex.templateIcon, $elem.find( '.' + ts.css.icon ).length ? '' : icon ); 590 if ( c.onRenderTemplate ) { 591 header = c.onRenderTemplate.apply( $elem, [ index, template ] ); 592 // only change t if something is returned 593 if ( header && typeof header === 'string' ) { 594 template = header; 595 } 596 } 597 $elem.html( '<div class="' + ts.css.headerIn + '">' + template + '</div>' ); // faster than wrapInner 598 } 599 if ( c.onRenderHeader ) { 600 c.onRenderHeader.apply( $elem, [ index, c, c.$table ] ); 601 } 602 column = parseInt( $elem.attr( 'data-column' ), 10 ); 603 elem.column = column; 604 tmp = ts.getOrder( ts.getData( $elem, configHeaders, 'sortInitialOrder' ) || c.sortInitialOrder ); 605 // this may get updated numerous times if there are multiple rows 606 c.sortVars[ column ] = { 607 count : -1, // set to -1 because clicking on the header automatically adds one 608 order: tmp ? 609 ( c.sortReset ? [ 1, 0, 2 ] : [ 1, 0 ] ) : // desc, asc, unsorted 610 ( c.sortReset ? [ 0, 1, 2 ] : [ 0, 1 ] ), // asc, desc, unsorted 611 lockedOrder : false 612 }; 613 tmp = ts.getData( $elem, configHeaders, 'lockedOrder' ) || false; 614 if ( typeof tmp !== 'undefined' && tmp !== false ) { 615 c.sortVars[ column ].lockedOrder = true; 616 c.sortVars[ column ].order = ts.getOrder( tmp ) ? [ 1, 1 ] : [ 0, 0 ]; 617 } 618 // add cell to headerList 619 c.headerList[ index ] = elem; 620 $elem.addClass( ts.css.header + ' ' + c.cssHeader ); 621 // add to parent in case there are multiple rows 622 ts.getClosest( $elem, 'tr' ) 623 .addClass( ts.css.headerRow + ' ' + c.cssHeaderRow ) 624 .attr( 'role', 'row' ); 625 // allow keyboard cursor to focus on element 626 if ( c.tabIndex ) { 627 $elem.attr( 'tabindex', 0 ); 628 } 629 return elem; 630 }) ); 631 // cache headers per column 632 c.$headerIndexed = []; 633 for ( indx = 0; indx < c.columns; indx++ ) { 634 // colspan in header making a column undefined 635 if ( ts.isEmptyObject( c.sortVars[ indx ] ) ) { 636 c.sortVars[ indx ] = {}; 637 } 638 // Use c.$headers.parent() in case selectorHeaders doesn't point to the th/td 639 $temp = c.$headers.filter( '[data-column="' + indx + '"]' ); 640 // target sortable column cells, unless there are none, then use non-sortable cells 641 // .last() added in jQuery 1.4; use .filter(':last') to maintain compatibility with jQuery v1.2.6 642 c.$headerIndexed[ indx ] = $temp.length ? 643 $temp.not( '.sorter-false' ).length ? 644 $temp.not( '.sorter-false' ).filter( ':last' ) : 645 $temp.filter( ':last' ) : 646 $(); 647 } 648 c.$table.find( c.selectorHeaders ).attr({ 649 scope: 'col', 650 role : 'columnheader' 651 }); 652 // enable/disable sorting 653 ts.updateHeader( c ); 654 if ( ts.debug(c, 'core') ) { 655 console.log( 'Built headers:' + ts.benchmark( timer ) ); 656 console.log( c.$headers ); 657 } 658 }, 659 660 // Use it to add a set of methods to table.config which will be available for all tables. 661 // This should be done before table initialization 662 addInstanceMethods : function( methods ) { 663 $.extend( ts.instanceMethods, methods ); 664 }, 665 666 /* 667 ââââââ ââââââ ââââââ ââââââ ââââââ ââââââ ââââââ 668 ââââââ ââââââ ââââââ âââ ââââ ââââââ âââ 669 âââââ ââââââ âââââ âââ ââââ âââââ âââ 670 ââ ââ ââ ââ ââ ââââââ ââââââ ââ ââ ââââââ 671 */ 672 setupParsers : function( c, $tbodies ) { 673 var rows, list, span, max, colIndex, indx, header, configHeaders, 674 noParser, parser, extractor, time, tbody, len, 675 table = c.table, 676 tbodyIndex = 0, 677 debug = ts.debug(c, 'core'), 678 debugOutput = {}; 679 // update table bodies in case we start with an empty table 680 c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ); 681 tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies; 682 len = tbody.length; 683 if ( len === 0 ) { 684 return debug ? console.warn( 'Warning: *Empty table!* Not building a parser cache' ) : ''; 685 } else if ( debug ) { 686 time = new Date(); 687 console[ console.group ? 'group' : 'log' ]( 'Detecting parsers for each column' ); 688 } 689 list = { 690 extractors: [], 691 parsers: [] 692 }; 693 while ( tbodyIndex < len ) { 694 rows = tbody[ tbodyIndex ].rows; 695 if ( rows.length ) { 696 colIndex = 0; 697 max = c.columns; 698 for ( indx = 0; indx < max; indx++ ) { 699 header = c.$headerIndexed[ colIndex ]; 700 if ( header && header.length ) { 701 // get column indexed table cell; adding true parameter fixes #1362 but 702 // it would break backwards compatibility... 703 configHeaders = ts.getColumnData( table, c.headers, colIndex ); // , true ); 704 // get column parser/extractor 705 extractor = ts.getParserById( ts.getData( header, configHeaders, 'extractor' ) ); 706 parser = ts.getParserById( ts.getData( header, configHeaders, 'sorter' ) ); 707 noParser = ts.getData( header, configHeaders, 'parser' ) === 'false'; 708 // empty cells behaviour - keeping emptyToBottom for backwards compatibility 709 c.empties[colIndex] = ( 710 ts.getData( header, configHeaders, 'empty' ) || 711 c.emptyTo || ( c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase(); 712 // text strings behaviour in numerical sorts 713 c.strings[colIndex] = ( 714 ts.getData( header, configHeaders, 'string' ) || 715 c.stringTo || 716 'max' ).toLowerCase(); 717 if ( noParser ) { 718 parser = ts.getParserById( 'no-parser' ); 719 } 720 if ( !extractor ) { 721 // For now, maybe detect someday 722 extractor = false; 723 } 724 if ( !parser ) { 725 parser = ts.detectParserForColumn( c, rows, -1, colIndex ); 726 } 727 if ( debug ) { 728 debugOutput[ '(' + colIndex + ') ' + header.text() ] = { 729 parser : parser.id, 730 extractor : extractor ? extractor.id : 'none', 731 string : c.strings[ colIndex ], 732 empty : c.empties[ colIndex ] 733 }; 734 } 735 list.parsers[ colIndex ] = parser; 736 list.extractors[ colIndex ] = extractor; 737 span = header[ 0 ].colSpan - 1; 738 if ( span > 0 ) { 739 colIndex += span; 740 max += span; 741 while ( span + 1 > 0 ) { 742 // set colspan columns to use the same parsers & extractors 743 list.parsers[ colIndex - span ] = parser; 744 list.extractors[ colIndex - span ] = extractor; 745 span--; 746 } 747 } 748 } 749 colIndex++; 750 } 751 } 752 tbodyIndex += ( list.parsers.length ) ? len : 1; 753 } 754 if ( debug ) { 755 if ( !ts.isEmptyObject( debugOutput ) ) { 756 console[ console.table ? 'table' : 'log' ]( debugOutput ); 757 } else { 758 console.warn( ' No parsers detected!' ); 759 } 760 console.log( 'Completed detecting parsers' + ts.benchmark( time ) ); 761 if ( console.groupEnd ) { console.groupEnd(); } 762 } 763 c.parsers = list.parsers; 764 c.extractors = list.extractors; 765 }, 766 767 addParser : function( parser ) { 768 var indx, 769 len = ts.parsers.length, 770 add = true; 771 for ( indx = 0; indx < len; indx++ ) { 772 if ( ts.parsers[ indx ].id.toLowerCase() === parser.id.toLowerCase() ) { 773 add = false; 774 } 775 } 776 if ( add ) { 777 ts.parsers[ ts.parsers.length ] = parser; 778 } 779 }, 780 781 getParserById : function( name ) { 782 /*jshint eqeqeq:false */ // eslint-disable-next-line eqeqeq 783 if ( name == 'false' ) { return false; } 784 var indx, 785 len = ts.parsers.length; 786 for ( indx = 0; indx < len; indx++ ) { 787 if ( ts.parsers[ indx ].id.toLowerCase() === ( name.toString() ).toLowerCase() ) { 788 return ts.parsers[ indx ]; 789 } 790 } 791 return false; 792 }, 793 794 detectParserForColumn : function( c, rows, rowIndex, cellIndex ) { 795 var cur, $node, row, 796 indx = ts.parsers.length, 797 node = false, 798 nodeValue = '', 799 debug = ts.debug(c, 'core'), 800 keepLooking = true; 801 while ( nodeValue === '' && keepLooking ) { 802 rowIndex++; 803 row = rows[ rowIndex ]; 804 // stop looking after 50 empty rows 805 if ( row && rowIndex < 50 ) { 806 if ( row.className.indexOf( ts.cssIgnoreRow ) < 0 ) { 807 node = rows[ rowIndex ].cells[ cellIndex ]; 808 nodeValue = ts.getElementText( c, node, cellIndex ); 809 $node = $( node ); 810 if ( debug ) { 811 console.log( 'Checking if value was empty on row ' + rowIndex + ', column: ' + 812 cellIndex + ': "' + nodeValue + '"' ); 813 } 814 } 815 } else { 816 keepLooking = false; 817 } 818 } 819 while ( --indx >= 0 ) { 820 cur = ts.parsers[ indx ]; 821 // ignore the default text parser because it will always be true 822 if ( cur && cur.id !== 'text' && cur.is && cur.is( nodeValue, c.table, node, $node ) ) { 823 return cur; 824 } 825 } 826 // nothing found, return the generic parser (text) 827 return ts.getParserById( 'text' ); 828 }, 829 830 getElementText : function( c, node, cellIndex ) { 831 if ( !node ) { return ''; } 832 var tmp, 833 extract = c.textExtraction || '', 834 // node could be a jquery object 835 // http://jsperf.com/jquery-vs-instanceof-jquery/2 836 $node = node.jquery ? node : $( node ); 837 if ( typeof extract === 'string' ) { 838 // check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow! 839 // http://www.kellegous.com/j/2013/02/27/innertext-vs-textcontent/ 840 if ( extract === 'basic' && typeof ( tmp = $node.attr( c.textAttribute ) ) !== 'undefined' ) { 841 return $.trim( tmp ); 842 } 843 return $.trim( node.textContent || $node.text() ); 844 } else { 845 if ( typeof extract === 'function' ) { 846 return $.trim( extract( $node[ 0 ], c.table, cellIndex ) ); 847 } else if ( typeof ( tmp = ts.getColumnData( c.table, extract, cellIndex ) ) === 'function' ) { 848 return $.trim( tmp( $node[ 0 ], c.table, cellIndex ) ); 849 } 850 } 851 // fallback 852 return $.trim( $node[ 0 ].textContent || $node.text() ); 853 }, 854 855 // centralized function to extract/parse cell contents 856 getParsedText : function( c, cell, colIndex, txt ) { 857 if ( typeof txt === 'undefined' ) { 858 txt = ts.getElementText( c, cell, colIndex ); 859 } 860 // if no parser, make sure to return the txt 861 var val = '' + txt, 862 parser = c.parsers[ colIndex ], 863 extractor = c.extractors[ colIndex ]; 864 if ( parser ) { 865 // do extract before parsing, if there is one 866 if ( extractor && typeof extractor.format === 'function' ) { 867 txt = extractor.format( txt, c.table, cell, colIndex ); 868 } 869 // allow parsing if the string is empty, previously parsing would change it to zero, 870 // in case the parser needs to extract data from the table cell attributes 871 val = parser.id === 'no-parser' ? '' : 872 // make sure txt is a string (extractor may have converted it) 873 parser.format( '' + txt, c.table, cell, colIndex ); 874 if ( c.ignoreCase && typeof val === 'string' ) { 875 val = val.toLowerCase(); 876 } 877 } 878 return val; 879 }, 880 881 /* 882 ââââââ ââââââ ââââââ ââ ââ ââââââ 883 ââ ââ ââââââ ââ ââ ââââââ ââââ 884 ââ ââ ââââââ ââ ââ ââââââ ââââ 885 ââââââ ââ ââ ââââââ ââ ââ ââââââ 886 */ 887 buildCache : function( c, callback, $tbodies ) { 888 var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row, 889 cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData, 890 colMax, span, cacheIndex, hasParser, max, len, index, 891 table = c.table, 892 parsers = c.parsers, 893 debug = ts.debug(c, 'core'); 894 // update tbody variable 895 c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ); 896 $tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies, 897 c.cache = {}; 898 c.totalRows = 0; 899 // if no parsers found, return - it's an empty table. 900 if ( !parsers ) { 901 return debug ? console.warn( 'Warning: *Empty table!* Not building a cache' ) : ''; 902 } 903 if ( debug ) { 904 cacheTime = new Date(); 905 } 906 // processing icon 907 if ( c.showProcessing ) { 908 ts.isProcessing( table, true ); 909 } 910 for ( tbodyIndex = 0; tbodyIndex < $tbody.length; tbodyIndex++ ) { 911 colMax = []; // column max value per tbody 912 cache = c.cache[ tbodyIndex ] = { 913 normalized: [] // array of normalized row data; last entry contains 'rowData' above 914 // colMax: # // added at the end 915 }; 916 917 totalRows = ( $tbody[ tbodyIndex ] && $tbody[ tbodyIndex ].rows.length ) || 0; 918 for ( rowIndex = 0; rowIndex < totalRows; ++rowIndex ) { 919 rowData = { 920 // order: original row order # 921 // $row : jQuery Object[] 922 child: [], // child row text (filter widget) 923 raw: [] // original row text 924 }; 925 /** Add the table data to main data array */ 926 $row = $( $tbody[ tbodyIndex ].rows[ rowIndex ] ); 927 cols = []; 928 // ignore "remove-me" rows 929 if ( $row.hasClass( c.selectorRemove.slice(1) ) ) { 930 continue; 931 } 932 // if this is a child row, add it to the last row's children and continue to the next row 933 // ignore child row class, if it is the first row 934 if ( $row.hasClass( c.cssChildRow ) && rowIndex !== 0 ) { 935 len = cache.normalized.length - 1; 936 prevRowData = cache.normalized[ len ][ c.columns ]; 937 prevRowData.$row = prevRowData.$row.add( $row ); 938 // add 'hasChild' class name to parent row 939 if ( !$row.prev().hasClass( c.cssChildRow ) ) { 940 $row.prev().addClass( ts.css.cssHasChild ); 941 } 942 // save child row content (un-parsed!) 943 $cells = $row.children( 'th, td' ); 944 len = prevRowData.child.length; 945 prevRowData.child[ len ] = []; 946 // child row content does not account for colspans/rowspans; so indexing may be off 947 cacheIndex = 0; 948 max = c.columns; 949 for ( colIndex = 0; colIndex < max; colIndex++ ) { 950 cell = $cells[ colIndex ]; 951 if ( cell ) { 952 prevRowData.child[ len ][ colIndex ] = ts.getParsedText( c, cell, colIndex ); 953 span = $cells[ colIndex ].colSpan - 1; 954 if ( span > 0 ) { 955 cacheIndex += span; 956 max += span; 957 } 958 } 959 cacheIndex++; 960 } 961 // go to the next for loop 962 continue; 963 } 964 rowData.$row = $row; 965 rowData.order = rowIndex; // add original row position to rowCache 966 cacheIndex = 0; 967 max = c.columns; 968 for ( colIndex = 0; colIndex < max; ++colIndex ) { 969 cell = $row[ 0 ].cells[ colIndex ]; 970 if ( cell && cacheIndex < c.columns ) { 971 hasParser = typeof parsers[ cacheIndex ] !== 'undefined'; 972 if ( !hasParser && debug ) { 973 console.warn( 'No parser found for row: ' + rowIndex + ', column: ' + colIndex + 974 '; cell containing: "' + $(cell).text() + '"; does it have a header?' ); 975 } 976 val = ts.getElementText( c, cell, cacheIndex ); 977 rowData.raw[ cacheIndex ] = val; // save original row text 978 // save raw column text even if there is no parser set 979 txt = ts.getParsedText( c, cell, cacheIndex, val ); 980 cols[ cacheIndex ] = txt; 981 if ( hasParser && ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) { 982 // determine column max value (ignore sign) 983 colMax[ cacheIndex ] = Math.max( Math.abs( txt ) || 0, colMax[ cacheIndex ] || 0 ); 984 } 985 // allow colSpan in tbody 986 span = cell.colSpan - 1; 987 if ( span > 0 ) { 988 index = 0; 989 while ( index <= span ) { 990 // duplicate text (or not) to spanned columns 991 // instead of setting duplicate span to empty string, use textExtraction to try to get a value 992 // see http://stackoverflow.com/q/36449711/145346 993 txt = c.duplicateSpan || index === 0 ? 994 val : 995 typeof c.textExtraction !== 'string' ? 996 ts.getElementText( c, cell, cacheIndex + index ) || '' : 997 ''; 998 rowData.raw[ cacheIndex + index ] = txt; 999 cols[ cacheIndex + index ] = txt; 1000 index++; 1001 } 1002 cacheIndex += span; 1003 max += span; 1004 } 1005 } 1006 cacheIndex++; 1007 } 1008 // ensure rowData is always in the same location (after the last column) 1009 cols[ c.columns ] = rowData; 1010 cache.normalized[ cache.normalized.length ] = cols; 1011 } 1012 cache.colMax = colMax; 1013 // total up rows, not including child rows 1014 c.totalRows += cache.normalized.length; 1015 1016 } 1017 if ( c.showProcessing ) { 1018 ts.isProcessing( table ); // remove processing icon 1019 } 1020 if ( debug ) { 1021 len = Math.min( 5, c.cache[ 0 ].normalized.length ); 1022 console[ console.group ? 'group' : 'log' ]( 'Building cache for ' + c.totalRows + 1023 ' rows (showing ' + len + ' rows in log) and ' + c.columns + ' columns' + 1024 ts.benchmark( cacheTime ) ); 1025 val = {}; 1026 for ( colIndex = 0; colIndex < c.columns; colIndex++ ) { 1027 for ( cacheIndex = 0; cacheIndex < len; cacheIndex++ ) { 1028 if ( !val[ 'row: ' + cacheIndex ] ) { 1029 val[ 'row: ' + cacheIndex ] = {}; 1030 } 1031 val[ 'row: ' + cacheIndex ][ c.$headerIndexed[ colIndex ].text() ] = 1032 c.cache[ 0 ].normalized[ cacheIndex ][ colIndex ]; 1033 } 1034 } 1035 console[ console.table ? 'table' : 'log' ]( val ); 1036 if ( console.groupEnd ) { console.groupEnd(); } 1037 } 1038 if ( $.isFunction( callback ) ) { 1039 callback( table ); 1040 } 1041 }, 1042 1043 getColumnText : function( table, column, callback, rowFilter ) { 1044 table = $( table )[0]; 1045 var tbodyIndex, rowIndex, cache, row, tbodyLen, rowLen, raw, parsed, $cell, result, 1046 hasCallback = typeof callback === 'function', 1047 allColumns = column === 'all', 1048 data = { raw : [], parsed: [], $cell: [] }, 1049 c = table.config; 1050 if ( ts.isEmptyObject( c ) ) { 1051 if ( ts.debug(c, 'core') ) { 1052 console.warn( 'No cache found - aborting getColumnText function!' ); 1053 } 1054 } else { 1055 tbodyLen = c.$tbodies.length; 1056 for ( tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++ ) { 1057 cache = c.cache[ tbodyIndex ].normalized; 1058 rowLen = cache.length; 1059 for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) { 1060 row = cache[ rowIndex ]; 1061 if ( rowFilter && !row[ c.columns ].$row.is( rowFilter ) ) { 1062 continue; 1063 } 1064 result = true; 1065 parsed = ( allColumns ) ? row.slice( 0, c.columns ) : row[ column ]; 1066 row = row[ c.columns ]; 1067 raw = ( allColumns ) ? row.raw : row.raw[ column ]; 1068 $cell = ( allColumns ) ? row.$row.children() : row.$row.children().eq( column ); 1069 if ( hasCallback ) { 1070 result = callback({ 1071 tbodyIndex : tbodyIndex, 1072 rowIndex : rowIndex, 1073 parsed : parsed, 1074 raw : raw, 1075 $row : row.$row, 1076 $cell : $cell 1077 }); 1078 } 1079 if ( result !== false ) { 1080 data.parsed[ data.parsed.length ] = parsed; 1081 data.raw[ data.raw.length ] = raw; 1082 data.$cell[ data.$cell.length ] = $cell; 1083 } 1084 } 1085 } 1086 // return everything 1087 return data; 1088 } 1089 }, 1090 1091 /* 1092 ââ ââ ââââââ ââââââ ââââââ ââââââ ââââââ 1093 ââ ââ ââââââ ââ ââ ââââââ ââ ââââ 1094 ââ ââ âââââ ââ ââ ââââââ ââ ââââ 1095 ââââââ ââ ââââââ ââ ââ ââ ââââââ 1096 */ 1097 setHeadersCss : function( c ) { 1098 var indx, column, 1099 list = c.sortList, 1100 len = list.length, 1101 none = ts.css.sortNone + ' ' + c.cssNone, 1102 css = [ ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc ], 1103 cssIcon = [ c.cssIconAsc, c.cssIconDesc, c.cssIconNone ], 1104 aria = [ 'ascending', 'descending' ], 1105 updateColumnSort = function($el, index) { 1106 $el 1107 .removeClass( none ) 1108 .addClass( css[ index ] ) 1109 .attr( 'aria-sort', aria[ index ] ) 1110 .find( '.' + ts.css.icon ) 1111 .removeClass( cssIcon[ 2 ] ) 1112 .addClass( cssIcon[ index ] ); 1113 }, 1114 // find the footer 1115 $extras = c.$table 1116 .find( 'tfoot tr' ) 1117 .children( 'td, th' ) 1118 .add( $( c.namespace + '_extra_headers' ) ) 1119 .removeClass( css.join( ' ' ) ), 1120 // remove all header information 1121 $sorted = c.$headers 1122 .add( $( 'thead ' + c.namespace + '_extra_headers' ) ) 1123 .removeClass( css.join( ' ' ) ) 1124 .addClass( none ) 1125 .attr( 'aria-sort', 'none' ) 1126 .find( '.' + ts.css.icon ) 1127 .removeClass( cssIcon.join( ' ' ) ) 1128 .end(); 1129 // add css none to all sortable headers 1130 $sorted 1131 .not( '.sorter-false' ) 1132 .find( '.' + ts.css.icon ) 1133 .addClass( cssIcon[ 2 ] ); 1134 // add disabled css icon class 1135 if ( c.cssIconDisabled ) { 1136 $sorted 1137 .filter( '.sorter-false' ) 1138 .find( '.' + ts.css.icon ) 1139 .addClass( c.cssIconDisabled ); 1140 } 1141 for ( indx = 0; indx < len; indx++ ) { 1142 // direction = 2 means reset! 1143 if ( list[ indx ][ 1 ] !== 2 ) { 1144 // multicolumn sorting updating - see #1005 1145 // .not(function() {}) needs jQuery 1.4 1146 // filter(function(i, el) {}) <- el is undefined in jQuery v1.2.6 1147 $sorted = c.$headers.filter( function( i ) { 1148 // only include headers that are in the sortList (this includes colspans) 1149 var include = true, 1150 $el = c.$headers.eq( i ), 1151 col = parseInt( $el.attr( 'data-column' ), 10 ), 1152 end = col + ts.getClosest( $el, 'th, td' )[0].colSpan; 1153 for ( ; col < end; col++ ) { 1154 include = include ? include || ts.isValueInArray( col, c.sortList ) > -1 : false; 1155 } 1156 return include; 1157 }); 1158 1159 // choose the :last in case there are nested columns 1160 $sorted = $sorted 1161 .not( '.sorter-false' ) 1162 .filter( '[data-column="' + list[ indx ][ 0 ] + '"]' + ( len === 1 ? ':last' : '' ) ); 1163 if ( $sorted.length ) { 1164 for ( column = 0; column < $sorted.length; column++ ) { 1165 if ( !$sorted[ column ].sortDisabled ) { 1166 updateColumnSort( $sorted.eq( column ), list[ indx ][ 1 ] ); 1167 } 1168 } 1169 } 1170 // add sorted class to footer & extra headers, if they exist 1171 if ( $extras.length ) { 1172 updateColumnSort( $extras.filter( '[data-column="' + list[ indx ][ 0 ] + '"]' ), list[ indx ][ 1 ] ); 1173 } 1174 } 1175 } 1176 // add verbose aria labels 1177 len = c.$headers.length; 1178 for ( indx = 0; indx < len; indx++ ) { 1179 ts.setColumnAriaLabel( c, c.$headers.eq( indx ) ); 1180 } 1181 }, 1182 1183 getClosest : function( $el, selector ) { 1184 // jQuery v1.2.6 doesn't have closest() 1185 if ( $.fn.closest ) { 1186 return $el.closest( selector ); 1187 } 1188 return $el.is( selector ) ? 1189 $el : 1190 $el.parents( selector ).filter( ':first' ); 1191 }, 1192 1193 // nextSort (optional), lets you disable next sort text 1194 setColumnAriaLabel : function( c, $header, nextSort ) { 1195 if ( $header.length ) { 1196 var column = parseInt( $header.attr( 'data-column' ), 10 ), 1197 vars = c.sortVars[ column ], 1198 tmp = $header.hasClass( ts.css.sortAsc ) ? 1199 'sortAsc' : 1200 $header.hasClass( ts.css.sortDesc ) ? 'sortDesc' : 'sortNone', 1201 txt = $.trim( $header.text() ) + ': ' + ts.language[ tmp ]; 1202 if ( $header.hasClass( 'sorter-false' ) || nextSort === false ) { 1203 txt += ts.language.sortDisabled; 1204 } else { 1205 tmp = ( vars.count + 1 ) % vars.order.length; 1206 nextSort = vars.order[ tmp ]; 1207 // if nextSort 1208 txt += ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ]; 1209 } 1210 $header.attr( 'aria-label', txt ); 1211 } 1212 }, 1213 1214 updateHeader : function( c ) { 1215 var index, isDisabled, $header, col, 1216 table = c.table, 1217 len = c.$headers.length; 1218 for ( index = 0; index < len; index++ ) { 1219 $header = c.$headers.eq( index ); 1220 col = ts.getColumnData( table, c.headers, index, true ); 1221 // add 'sorter-false' class if 'parser-false' is set 1222 isDisabled = ts.getData( $header, col, 'sorter' ) === 'false' || ts.getData( $header, col, 'parser' ) === 'false'; 1223 ts.setColumnSort( c, $header, isDisabled ); 1224 } 1225 }, 1226 1227 setColumnSort : function( c, $header, isDisabled ) { 1228 var id = c.table.id; 1229 $header[ 0 ].sortDisabled = isDisabled; 1230 $header[ isDisabled ? 'addClass' : 'removeClass' ]( 'sorter-false' ) 1231 .attr( 'aria-disabled', '' + isDisabled ); 1232 // disable tab index on disabled cells 1233 if ( c.tabIndex ) { 1234 if ( isDisabled ) { 1235 $header.removeAttr( 'tabindex' ); 1236 } else { 1237 $header.attr( 'tabindex', '0' ); 1238 } 1239 } 1240 // aria-controls - requires table ID 1241 if ( id ) { 1242 if ( isDisabled ) { 1243 $header.removeAttr( 'aria-controls' ); 1244 } else { 1245 $header.attr( 'aria-controls', id ); 1246 } 1247 } 1248 }, 1249 1250 updateHeaderSortCount : function( c, list ) { 1251 var col, dir, group, indx, primary, temp, val, order, 1252 sortList = list || c.sortList, 1253 len = sortList.length; 1254 c.sortList = []; 1255 for ( indx = 0; indx < len; indx++ ) { 1256 val = sortList[ indx ]; 1257 // ensure all sortList values are numeric - fixes #127 1258 col = parseInt( val[ 0 ], 10 ); 1259 // prevents error if sorton array is wrong 1260 if ( col < c.columns ) { 1261 1262 // set order if not already defined - due to colspan header without associated header cell 1263 // adding this check prevents a javascript error 1264 if ( !c.sortVars[ col ].order ) { 1265 if ( ts.getOrder( c.sortInitialOrder ) ) { 1266 order = c.sortReset ? [ 1, 0, 2 ] : [ 1, 0 ]; 1267 } else { 1268 order = c.sortReset ? [ 0, 1, 2 ] : [ 0, 1 ]; 1269 } 1270 c.sortVars[ col ].order = order; 1271 c.sortVars[ col ].count = 0; 1272 } 1273 1274 order = c.sortVars[ col ].order; 1275 dir = ( '' + val[ 1 ] ).match( /^(1|d|s|o|n)/ ); 1276 dir = dir ? dir[ 0 ] : ''; 1277 // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext 1278 switch ( dir ) { 1279 case '1' : case 'd' : // descending 1280 dir = 1; 1281 break; 1282 case 's' : // same direction (as primary column) 1283 // if primary sort is set to 's', make it ascending 1284 dir = primary || 0; 1285 break; 1286 case 'o' : 1287 temp = order[ ( primary || 0 ) % order.length ]; 1288 // opposite of primary column; but resets if primary resets 1289 dir = temp === 0 ? 1 : temp === 1 ? 0 : 2; 1290 break; 1291 case 'n' : 1292 dir = order[ ( ++c.sortVars[ col ].count ) % order.length ]; 1293 break; 1294 default : // ascending 1295 dir = 0; 1296 break; 1297 } 1298 primary = indx === 0 ? dir : primary; 1299 group = [ col, parseInt( dir, 10 ) || 0 ]; 1300 c.sortList[ c.sortList.length ] = group; 1301 dir = $.inArray( group[ 1 ], order ); // fixes issue #167 1302 c.sortVars[ col ].count = dir >= 0 ? dir : group[ 1 ] % order.length; 1303 } 1304 } 1305 }, 1306 1307 updateAll : function( c, resort, callback ) { 1308 var table = c.table; 1309 table.isUpdating = true; 1310 ts.refreshWidgets( table, true, true ); 1311 ts.buildHeaders( c ); 1312 ts.bindEvents( table, c.$headers, true ); 1313 ts.bindMethods( c ); 1314 ts.commonUpdate( c, resort, callback ); 1315 }, 1316 1317 update : function( c, resort, callback ) { 1318 var table = c.table; 1319 table.isUpdating = true; 1320 // update sorting (if enabled/disabled) 1321 ts.updateHeader( c ); 1322 ts.commonUpdate( c, resort, callback ); 1323 }, 1324 1325 // simple header update - see #989 1326 updateHeaders : function( c, callback ) { 1327 c.table.isUpdating = true; 1328 ts.buildHeaders( c ); 1329 ts.bindEvents( c.table, c.$headers, true ); 1330 ts.resortComplete( c, callback ); 1331 }, 1332 1333 updateCell : function( c, cell, resort, callback ) { 1334 // updateCell for child rows is a mess - we'll ignore them for now 1335 // eventually I'll break out the "update" row cache code to make everything consistent 1336 if ( $( cell ).closest( 'tr' ).hasClass( c.cssChildRow ) ) { 1337 console.warn('Tablesorter Warning! "updateCell" for child row content has been disabled, use "update" instead'); 1338 return; 1339 } 1340 if ( ts.isEmptyObject( c.cache ) ) { 1341 // empty table, do an update instead - fixes #1099 1342 ts.updateHeader( c ); 1343 ts.commonUpdate( c, resort, callback ); 1344 return; 1345 } 1346 c.table.isUpdating = true; 1347 c.$table.find( c.selectorRemove ).remove(); 1348 // get position from the dom 1349 var tmp, indx, row, icell, cache, len, 1350 $tbodies = c.$tbodies, 1351 $cell = $( cell ), 1352 // update cache - format: function( s, table, cell, cellIndex ) 1353 // no closest in jQuery v1.2.6 1354 tbodyIndex = $tbodies.index( ts.getClosest( $cell, 'tbody' ) ), 1355 tbcache = c.cache[ tbodyIndex ], 1356 $row = ts.getClosest( $cell, 'tr' ); 1357 cell = $cell[ 0 ]; // in case cell is a jQuery object 1358 // tbody may not exist if update is initialized while tbody is removed for processing 1359 if ( $tbodies.length && tbodyIndex >= 0 ) { 1360 row = $tbodies.eq( tbodyIndex ).find( 'tr' ).not( '.' + c.cssChildRow ).index( $row ); 1361 cache = tbcache.normalized[ row ]; 1362 len = $row[ 0 ].cells.length; 1363 if ( len !== c.columns ) { 1364 // colspan in here somewhere! 1365 icell = 0; 1366 tmp = false; 1367 for ( indx = 0; indx < len; indx++ ) { 1368 if ( !tmp && $row[ 0 ].cells[ indx ] !== cell ) { 1369 icell += $row[ 0 ].cells[ indx ].colSpan; 1370 } else { 1371 tmp = true; 1372 } 1373 } 1374 } else { 1375 icell = $cell.index(); 1376 } 1377 tmp = ts.getElementText( c, cell, icell ); // raw 1378 cache[ c.columns ].raw[ icell ] = tmp; 1379 tmp = ts.getParsedText( c, cell, icell, tmp ); 1380 cache[ icell ] = tmp; // parsed 1381 if ( ( c.parsers[ icell ].type || '' ).toLowerCase() === 'numeric' ) { 1382 // update column max value (ignore sign) 1383 tbcache.colMax[ icell ] = Math.max( Math.abs( tmp ) || 0, tbcache.colMax[ icell ] || 0 ); 1384 } 1385 tmp = resort !== 'undefined' ? resort : c.resort; 1386 if ( tmp !== false ) { 1387 // widgets will be reapplied 1388 ts.checkResort( c, tmp, callback ); 1389 } else { 1390 // don't reapply widgets is resort is false, just in case it causes 1391 // problems with element focus 1392 ts.resortComplete( c, callback ); 1393 } 1394 } else { 1395 if ( ts.debug(c, 'core') ) { 1396 console.error( 'updateCell aborted, tbody missing or not within the indicated table' ); 1397 } 1398 c.table.isUpdating = false; 1399 } 1400 }, 1401 1402 addRows : function( c, $row, resort, callback ) { 1403 var txt, val, tbodyIndex, rowIndex, rows, cellIndex, len, order, 1404 cacheIndex, rowData, cells, cell, span, 1405 // allow passing a row string if only one non-info tbody exists in the table 1406 valid = typeof $row === 'string' && c.$tbodies.length === 1 && /<tr/.test( $row || '' ), 1407 table = c.table; 1408 if ( valid ) { 1409 $row = $( $row ); 1410 c.$tbodies.append( $row ); 1411 } else if ( 1412 !$row || 1413 // row is a jQuery object? 1414 !( $row instanceof $ ) || 1415 // row contained in the table? 1416 ( ts.getClosest( $row, 'table' )[ 0 ] !== c.table ) 1417 ) { 1418 if ( ts.debug(c, 'core') ) { 1419 console.error( 'addRows method requires (1) a jQuery selector reference to rows that have already ' + 1420 'been added to the table, or (2) row HTML string to be added to a table with only one tbody' ); 1421 } 1422 return false; 1423 } 1424 table.isUpdating = true; 1425 if ( ts.isEmptyObject( c.cache ) ) { 1426 // empty table, do an update instead - fixes #450 1427 ts.updateHeader( c ); 1428 ts.commonUpdate( c, resort, callback ); 1429 } else { 1430 rows = $row.filter( 'tr' ).attr( 'role', 'row' ).length; 1431 tbodyIndex = c.$tbodies.index( $row.parents( 'tbody' ).filter( ':first' ) ); 1432 // fixes adding rows to an empty table - see issue #179 1433 if ( !( c.parsers && c.parsers.length ) ) { 1434 ts.setupParsers( c ); 1435 } 1436 // add each row 1437 for ( rowIndex = 0; rowIndex < rows; rowIndex++ ) { 1438 cacheIndex = 0; 1439 len = $row[ rowIndex ].cells.length; 1440 order = c.cache[ tbodyIndex ].normalized.length; 1441 cells = []; 1442 rowData = { 1443 child : [], 1444 raw : [], 1445 $row : $row.eq( rowIndex ), 1446 order : order 1447 }; 1448 // add each cell 1449 for ( cellIndex = 0; cellIndex < len; cellIndex++ ) { 1450 cell = $row[ rowIndex ].cells[ cellIndex ]; 1451 txt = ts.getElementText( c, cell, cacheIndex ); 1452 rowData.raw[ cacheIndex ] = txt; 1453 val = ts.getParsedText( c, cell, cacheIndex, txt ); 1454 cells[ cacheIndex ] = val; 1455 if ( ( c.parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) { 1456 // update column max value (ignore sign) 1457 c.cache[ tbodyIndex ].colMax[ cacheIndex ] = 1458 Math.max( Math.abs( val ) || 0, c.cache[ tbodyIndex ].colMax[ cacheIndex ] || 0 ); 1459 } 1460 span = cell.colSpan - 1; 1461 if ( span > 0 ) { 1462 cacheIndex += span; 1463 } 1464 cacheIndex++; 1465 } 1466 // add the row data to the end 1467 cells[ c.columns ] = rowData; 1468 // update cache 1469 c.cache[ tbodyIndex ].normalized[ order ] = cells; 1470 } 1471 // resort using current settings 1472 ts.checkResort( c, resort, callback ); 1473 } 1474 }, 1475 1476 updateCache : function( c, callback, $tbodies ) { 1477 // rebuild parsers 1478 if ( !( c.parsers && c.parsers.length ) ) { 1479 ts.setupParsers( c, $tbodies ); 1480 } 1481 // rebuild the cache map 1482 ts.buildCache( c, callback, $tbodies ); 1483 }, 1484 1485 // init flag (true) used by pager plugin to prevent widget application 1486 // renamed from appendToTable 1487 appendCache : function( c, init ) { 1488 var parsed, totalRows, $tbody, $curTbody, rowIndex, tbodyIndex, appendTime, 1489 table = c.table, 1490 $tbodies = c.$tbodies, 1491 rows = [], 1492 cache = c.cache; 1493 // empty table - fixes #206/#346 1494 if ( ts.isEmptyObject( cache ) ) { 1495 // run pager appender in case the table was just emptied 1496 return c.appender ? c.appender( table, rows ) : 1497 table.isUpdating ? c.$table.triggerHandler( 'updateComplete', table ) : ''; // Fixes #532 1498 } 1499 if ( ts.debug(c, 'core') ) { 1500 appendTime = new Date(); 1501 } 1502 for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 1503 $tbody = $tbodies.eq( tbodyIndex ); 1504 if ( $tbody.length ) { 1505 // detach tbody for manipulation 1506 $curTbody = ts.processTbody( table, $tbody, true ); 1507 parsed = cache[ tbodyIndex ].normalized; 1508 totalRows = parsed.length; 1509 for ( rowIndex = 0; rowIndex < totalRows; rowIndex++ ) { 1510 rows[rows.length] = parsed[ rowIndex ][ c.columns ].$row; 1511 // removeRows used by the pager plugin; don't render if using ajax - fixes #411 1512 if ( !c.appender || ( c.pager && !c.pager.removeRows && !c.pager.ajax ) ) { 1513 $curTbody.append( parsed[ rowIndex ][ c.columns ].$row ); 1514 } 1515 } 1516 // restore tbody 1517 ts.processTbody( table, $curTbody, false ); 1518 } 1519 } 1520 if ( c.appender ) { 1521 c.appender( table, rows ); 1522 } 1523 if ( ts.debug(c, 'core') ) { 1524 console.log( 'Rebuilt table' + ts.benchmark( appendTime ) ); 1525 } 1526 // apply table widgets; but not before ajax completes 1527 if ( !init && !c.appender ) { 1528 ts.applyWidget( table ); 1529 } 1530 if ( table.isUpdating ) { 1531 c.$table.triggerHandler( 'updateComplete', table ); 1532 } 1533 }, 1534 1535 commonUpdate : function( c, resort, callback ) { 1536 // remove rows/elements before update 1537 c.$table.find( c.selectorRemove ).remove(); 1538 // rebuild parsers 1539 ts.setupParsers( c ); 1540 // rebuild the cache map 1541 ts.buildCache( c ); 1542 ts.checkResort( c, resort, callback ); 1543 }, 1544 1545 /* 1546 ââââââ ââââââ ââââââ ââââââ ââ ââââââ ââââââ 1547 âââ ââ ââ ââââââ ââ ââ ââ ââ ââ âââ 1548 âââ ââ ââ âââââ ââ ââ ââ ââ ââ âââ 1549 ââââââ ââââââ ââ ââ ââ ââ ââ ââ ââââââ 1550 */ 1551 initSort : function( c, cell, event ) { 1552 if ( c.table.isUpdating ) { 1553 // let any updates complete before initializing a sort 1554 return setTimeout( function() { 1555 ts.initSort( c, cell, event ); 1556 }, 50 ); 1557 } 1558 1559 var arry, indx, headerIndx, dir, temp, tmp, $header, 1560 notMultiSort = !event[ c.sortMultiSortKey ], 1561 table = c.table, 1562 len = c.$headers.length, 1563 th = ts.getClosest( $( cell ), 'th, td' ), 1564 col = parseInt( th.attr( 'data-column' ), 10 ), 1565 order = c.sortVars[ col ].order; 1566 th = th[0]; 1567 // Only call sortStart if sorting is enabled 1568 c.$table.triggerHandler( 'sortStart', table ); 1569 // get current column sort order 1570 tmp = ( c.sortVars[ col ].count + 1 ) % order.length; 1571 c.sortVars[ col ].count = event[ c.sortResetKey ] ? 2 : tmp; 1572 // reset all sorts on non-current column - issue #30 1573 if ( c.sortRestart ) { 1574 for ( headerIndx = 0; headerIndx < len; headerIndx++ ) { 1575 $header = c.$headers.eq( headerIndx ); 1576 tmp = parseInt( $header.attr( 'data-column' ), 10 ); 1577 // only reset counts on columns that weren't just clicked on and if not included in a multisort 1578 if ( col !== tmp && ( notMultiSort || $header.hasClass( ts.css.sortNone ) ) ) { 1579 c.sortVars[ tmp ].count = -1; 1580 } 1581 } 1582 } 1583 // user only wants to sort on one column 1584 if ( notMultiSort ) { 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 } 1594 } 1595 } 1596 // add column to sort list 1597 dir = order[ c.sortVars[ col ].count ]; 1598 if ( dir < 2 ) { 1599 c.sortList[ c.sortList.length ] = [ col, dir ]; 1600 // add other columns if header spans across multiple 1601 if ( th.colSpan > 1 ) { 1602 for ( indx = 1; indx < th.colSpan; indx++ ) { 1603 c.sortList[ c.sortList.length ] = [ col + indx, dir ]; 1604 // update count on columns in colSpan 1605 c.sortVars[ col + indx ].count = $.inArray( dir, order ); 1606 } 1607 } 1608 } 1609 // multi column sorting 1610 } else { 1611 // get rid of the sortAppend before adding more - fixes issue #115 & #523 1612 c.sortList = $.extend( [], c.last.sortList ); 1613 1614 // the user has clicked on an already sorted column 1615 if ( ts.isValueInArray( col, c.sortList ) >= 0 ) { 1616 // reverse the sorting direction 1617 for ( indx = 0; indx < c.sortList.length; indx++ ) { 1618 tmp = c.sortList[ indx ]; 1619 if ( tmp[ 0 ] === col ) { 1620 // order.count seems to be incorrect when compared to cell.count 1621 tmp[ 1 ] = order[ c.sortVars[ col ].count ]; 1622 if ( tmp[1] === 2 ) { 1623 c.sortList.splice( indx, 1 ); 1624 c.sortVars[ col ].count = -1; 1625 } 1626 } 1627 } 1628 } else { 1629 // add column to sort list array 1630 dir = order[ c.sortVars[ col ].count ]; 1631 if ( dir < 2 ) { 1632 c.sortList[ c.sortList.length ] = [ col, dir ]; 1633 // add other columns if header spans across multiple 1634 if ( th.colSpan > 1 ) { 1635 for ( indx = 1; indx < th.colSpan; indx++ ) { 1636 c.sortList[ c.sortList.length ] = [ col + indx, dir ]; 1637 // update count on columns in colSpan 1638 c.sortVars[ col + indx ].count = $.inArray( dir, order ); 1639 } 1640 } 1641 } 1642 } 1643 } 1644 // save sort before applying sortAppend 1645 c.last.sortList = $.extend( [], c.sortList ); 1646 if ( c.sortList.length && c.sortAppend ) { 1647 arry = $.isArray( c.sortAppend ) ? c.sortAppend : c.sortAppend[ c.sortList[ 0 ][ 0 ] ]; 1648 if ( !ts.isEmptyObject( arry ) ) { 1649 for ( indx = 0; indx < arry.length; indx++ ) { 1650 if ( arry[ indx ][ 0 ] !== col && ts.isValueInArray( arry[ indx ][ 0 ], c.sortList ) < 0 ) { 1651 dir = arry[ indx ][ 1 ]; 1652 temp = ( '' + dir ).match( /^(a|d|s|o|n)/ ); 1653 if ( temp ) { 1654 tmp = c.sortList[ 0 ][ 1 ]; 1655 switch ( temp[ 0 ] ) { 1656 case 'd' : 1657 dir = 1; 1658 break; 1659 case 's' : 1660 dir = tmp; 1661 break; 1662 case 'o' : 1663 dir = tmp === 0 ? 1 : 0; 1664 break; 1665 case 'n' : 1666 dir = ( tmp + 1 ) % order.length; 1667 break; 1668 default: 1669 dir = 0; 1670 break; 1671 } 1672 } 1673 c.sortList[ c.sortList.length ] = [ arry[ indx ][ 0 ], dir ]; 1674 } 1675 } 1676 } 1677 } 1678 // sortBegin event triggered immediately before the sort 1679 c.$table.triggerHandler( 'sortBegin', table ); 1680 // setTimeout needed so the processing icon shows up 1681 setTimeout( function() { 1682 // set css for headers 1683 ts.setHeadersCss( c ); 1684 ts.multisort( c ); 1685 ts.appendCache( c ); 1686 c.$table.triggerHandler( 'sortBeforeEnd', table ); 1687 c.$table.triggerHandler( 'sortEnd', table ); 1688 }, 1 ); 1689 }, 1690 1691 // sort multiple columns 1692 multisort : function( c ) { /*jshint loopfunc:true */ 1693 var tbodyIndex, sortTime, colMax, rows, tmp, 1694 table = c.table, 1695 sorter = [], 1696 dir = 0, 1697 textSorter = c.textSorter || '', 1698 sortList = c.sortList, 1699 sortLen = sortList.length, 1700 len = c.$tbodies.length; 1701 if ( c.serverSideSorting || ts.isEmptyObject( c.cache ) ) { 1702 // empty table - fixes #206/#346 1703 return; 1704 } 1705 if ( ts.debug(c, 'core') ) { sortTime = new Date(); } 1706 // cache textSorter to optimize speed 1707 if ( typeof textSorter === 'object' ) { 1708 colMax = c.columns; 1709 while ( colMax-- ) { 1710 tmp = ts.getColumnData( table, textSorter, colMax ); 1711 if ( typeof tmp === 'function' ) { 1712 sorter[ colMax ] = tmp; 1713 } 1714 } 1715 } 1716 for ( tbodyIndex = 0; tbodyIndex < len; tbodyIndex++ ) { 1717 colMax = c.cache[ tbodyIndex ].colMax; 1718 rows = c.cache[ tbodyIndex ].normalized; 1719 1720 rows.sort( function( a, b ) { 1721 var sortIndex, num, col, order, sort, x, y; 1722 // rows is undefined here in IE, so don't use it! 1723 for ( sortIndex = 0; sortIndex < sortLen; sortIndex++ ) { 1724 col = sortList[ sortIndex ][ 0 ]; 1725 order = sortList[ sortIndex ][ 1 ]; 1726 // sort direction, true = asc, false = desc 1727 dir = order === 0; 1728 1729 if ( c.sortStable && a[ col ] === b[ col ] && sortLen === 1 ) { 1730 return a[ c.columns ].order - b[ c.columns ].order; 1731 } 1732 1733 // fallback to natural sort since it is more robust 1734 num = /n/i.test( ts.getSortType( c.parsers, col ) ); 1735 if ( num && c.strings[ col ] ) { 1736 // sort strings in numerical columns 1737 if ( typeof ( ts.string[ c.strings[ col ] ] ) === 'boolean' ) { 1738 num = ( dir ? 1 : -1 ) * ( ts.string[ c.strings[ col ] ] ? -1 : 1 ); 1739 } else { 1740 num = ( c.strings[ col ] ) ? ts.string[ c.strings[ col ] ] || 0 : 0; 1741 } 1742 // fall back to built-in numeric sort 1743 // var sort = $.tablesorter['sort' + s]( a[col], b[col], dir, colMax[col], table ); 1744 sort = c.numberSorter ? c.numberSorter( a[ col ], b[ col ], dir, colMax[ col ], table ) : 1745 ts[ 'sortNumeric' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ], b[ col ], num, colMax[ col ], col, c ); 1746 } else { 1747 // set a & b depending on sort direction 1748 x = dir ? a : b; 1749 y = dir ? b : a; 1750 // text sort function 1751 if ( typeof textSorter === 'function' ) { 1752 // custom OVERALL text sorter 1753 sort = textSorter( x[ col ], y[ col ], dir, col, table ); 1754 } else if ( typeof sorter[ col ] === 'function' ) { 1755 // custom text sorter for a SPECIFIC COLUMN 1756 sort = sorter[ col ]( x[ col ], y[ col ], dir, col, table ); 1757 } else { 1758 // fall back to natural sort 1759 sort = ts[ 'sortNatural' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ], b[ col ], col, c ); 1760 } 1761 } 1762 if ( sort ) { return sort; } 1763 } 1764 return a[ c.columns ].order - b[ c.columns ].order; 1765 }); 1766 } 1767 if ( ts.debug(c, 'core') ) { 1768 console.log( 'Applying sort ' + sortList.toString() + ts.benchmark( sortTime ) ); 1769 } 1770 }, 1771 1772 resortComplete : function( c, callback ) { 1773 if ( c.table.isUpdating ) { 1774 c.$table.triggerHandler( 'updateComplete', c.table ); 1775 } 1776 if ( $.isFunction( callback ) ) { 1777 callback( c.table ); 1778 } 1779 }, 1780 1781 checkResort : function( c, resort, callback ) { 1782 var sortList = $.isArray( resort ) ? resort : c.sortList, 1783 // if no resort parameter is passed, fallback to config.resort (true by default) 1784 resrt = typeof resort === 'undefined' ? c.resort : resort; 1785 // don't try to resort if the table is still processing 1786 // this will catch spamming of the updateCell method 1787 if ( resrt !== false && !c.serverSideSorting && !c.table.isProcessing ) { 1788 if ( sortList.length ) { 1789 ts.sortOn( c, sortList, function() { 1790 ts.resortComplete( c, callback ); 1791 }, true ); 1792 } else { 1793 ts.sortReset( c, function() { 1794 ts.resortComplete( c, callback ); 1795 ts.applyWidget( c.table, false ); 1796 } ); 1797 } 1798 } else { 1799 ts.resortComplete( c, callback ); 1800 ts.applyWidget( c.table, false ); 1801 } 1802 }, 1803 1804 sortOn : function( c, list, callback, init ) { 1805 var table = c.table; 1806 c.$table.triggerHandler( 'sortStart', table ); 1807 // update header count index 1808 ts.updateHeaderSortCount( c, list ); 1809 // set css for headers 1810 ts.setHeadersCss( c ); 1811 // fixes #346 1812 if ( c.delayInit && ts.isEmptyObject( c.cache ) ) { 1813 ts.buildCache( c ); 1814 } 1815 c.$table.triggerHandler( 'sortBegin', table ); 1816 // sort the table and append it to the dom 1817 ts.multisort( c ); 1818 ts.appendCache( c, init ); 1819 c.$table.triggerHandler( 'sortBeforeEnd', table ); 1820 c.$table.triggerHandler( 'sortEnd', table ); 1821 ts.applyWidget( table ); 1822 if ( $.isFunction( callback ) ) { 1823 callback( table ); 1824 } 1825 }, 1826 1827 sortReset : function( c, callback ) { 1828 c.sortList = []; 1829 ts.setHeadersCss( c ); 1830 ts.multisort( c ); 1831 ts.appendCache( c ); 1832 var indx; 1833 for (indx = 0; indx < c.columns; indx++) { 1834 c.sortVars[ indx ].count = -1; 1835 } 1836 if ( $.isFunction( callback ) ) { 1837 callback( c.table ); 1838 } 1839 }, 1840 1841 getSortType : function( parsers, column ) { 1842 return ( parsers && parsers[ column ] ) ? parsers[ column ].type || '' : ''; 1843 }, 1844 1845 getOrder : function( val ) { 1846 // look for 'd' in 'desc' order; return true 1847 return ( /^d/i.test( val ) || val === 1 ); 1848 }, 1849 1850 // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed) 1851 sortNatural : function( a, b ) { 1852 if ( a === b ) { return 0; } 1853 a = a.toString(); 1854 b = b.toString(); 1855 var aNum, bNum, aFloat, bFloat, indx, max, 1856 regex = ts.regex; 1857 // first try and sort Hex codes 1858 if ( regex.hex.test( b ) ) { 1859 aNum = parseInt( ( a || '' ).match( regex.hex ), 16 ); 1860 bNum = parseInt( ( b || '' ).match( regex.hex ), 16 ); 1861 if ( aNum < bNum ) { return -1; } 1862 if ( aNum > bNum ) { return 1; } 1863 } 1864 // chunk/tokenize 1865 aNum = ( a || '' ).replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' ); 1866 bNum = ( b || '' ).replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' ); 1867 max = Math.max( aNum.length, bNum.length ); 1868 // natural sorting through split numeric strings and default strings 1869 for ( indx = 0; indx < max; indx++ ) { 1870 // find floats not starting with '0', string or 0 if not defined 1871 aFloat = isNaN( aNum[ indx ] ) ? aNum[ indx ] || 0 : parseFloat( aNum[ indx ] ) || 0; 1872 bFloat = isNaN( bNum[ indx ] ) ? bNum[ indx ] || 0 : parseFloat( bNum[ indx ] ) || 0; 1873 // handle numeric vs string comparison - number < string - (Kyle Adams) 1874 if ( isNaN( aFloat ) !== isNaN( bFloat ) ) { return isNaN( aFloat ) ? 1 : -1; } 1875 // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' 1876 if ( typeof aFloat !== typeof bFloat ) { 1877 aFloat += ''; 1878 bFloat += ''; 1879 } 1880 if ( aFloat < bFloat ) { return -1; } 1881 if ( aFloat > bFloat ) { return 1; } 1882 } 1883 return 0; 1884 }, 1885 1886 sortNaturalAsc : function( a, b, col, c ) { 1887 if ( a === b ) { return 0; } 1888 var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; 1889 if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; } 1890 if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; } 1891 return ts.sortNatural( a, b ); 1892 }, 1893 1894 sortNaturalDesc : function( a, b, col, c ) { 1895 if ( a === b ) { return 0; } 1896 var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; 1897 if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; } 1898 if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; } 1899 return ts.sortNatural( b, a ); 1900 }, 1901 1902 // basic alphabetical sort 1903 sortText : function( a, b ) { 1904 return a > b ? 1 : ( a < b ? -1 : 0 ); 1905 }, 1906 1907 // return text string value by adding up ascii value 1908 // so the text is somewhat sorted when using a digital sort 1909 // this is NOT an alphanumeric sort 1910 getTextValue : function( val, num, max ) { 1911 if ( max ) { 1912 // make sure the text value is greater than the max numerical value (max) 1913 var indx, 1914 len = val ? val.length : 0, 1915 n = max + num; 1916 for ( indx = 0; indx < len; indx++ ) { 1917 n += val.charCodeAt( indx ); 1918 } 1919 return num * n; 1920 } 1921 return 0; 1922 }, 1923 1924 sortNumericAsc : function( a, b, num, max, col, c ) { 1925 if ( a === b ) { return 0; } 1926 var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; 1927 if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; } 1928 if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; } 1929 if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); } 1930 if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); } 1931 return a - b; 1932 }, 1933 1934 sortNumericDesc : function( a, b, num, max, col, c ) { 1935 if ( a === b ) { return 0; } 1936 var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; 1937 if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; } 1938 if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; } 1939 if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); } 1940 if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); } 1941 return b - a; 1942 }, 1943 1944 sortNumeric : function( a, b ) { 1945 return a - b; 1946 }, 1947 1948 /* 1949 ââ ââ ââ ââ ââââââ ââââââ ââââââ ââââââ ââââââ 1950 ââ ââ ââ ââ ââ ââ ââ âââ ââââ ââ âââ 1951 ââ ââ ââ ââ ââ ââ ââ âââ ââââ ââ âââ 1952 ââââââââ ââ ââââââ ââââââ ââââââ ââ ââââââ 1953 */ 1954 addWidget : function( widget ) { 1955 if ( widget.id && !ts.isEmptyObject( ts.getWidgetById( widget.id ) ) ) { 1956 console.warn( '"' + widget.id + '" widget was loaded more than once!' ); 1957 } 1958 ts.widgets[ ts.widgets.length ] = widget; 1959 }, 1960 1961 hasWidget : function( $table, name ) { 1962 $table = $( $table ); 1963 return $table.length && $table[ 0 ].config && $table[ 0 ].config.widgetInit[ name ] || false; 1964 }, 1965 1966 getWidgetById : function( name ) { 1967 var indx, widget, 1968 len = ts.widgets.length; 1969 for ( indx = 0; indx < len; indx++ ) { 1970 widget = ts.widgets[ indx ]; 1971 if ( widget && widget.id && widget.id.toLowerCase() === name.toLowerCase() ) { 1972 return widget; 1973 } 1974 } 1975 }, 1976 1977 applyWidgetOptions : function( table ) { 1978 var indx, widget, wo, 1979 c = table.config, 1980 len = c.widgets.length; 1981 if ( len ) { 1982 for ( indx = 0; indx < len; indx++ ) { 1983 widget = ts.getWidgetById( c.widgets[ indx ] ); 1984 if ( widget && widget.options ) { 1985 wo = $.extend( true, {}, widget.options ); 1986 c.widgetOptions = $.extend( true, wo, c.widgetOptions ); 1987 // add widgetOptions to defaults for option validator 1988 $.extend( true, ts.defaults.widgetOptions, widget.options ); 1989 } 1990 } 1991 } 1992 }, 1993 1994 addWidgetFromClass : function( table ) { 1995 var len, indx, 1996 c = table.config, 1997 // look for widgets to apply from table class 1998 // don't match from 'ui-widget-content'; use \S instead of \w to include widgets 1999 // with dashes in the name, e.g. "widget-test-2" extracts out "test-2" 2000 regex = '^' + c.widgetClass.replace( ts.regex.templateName, '(\\S+)+' ) + '$', 2001 widgetClass = new RegExp( regex, 'g' ), 2002 // split up table class (widget id's can include dashes) - stop using match 2003 // otherwise only one widget gets extracted, see #1109 2004 widgets = ( table.className || '' ).split( ts.regex.spaces ); 2005 if ( widgets.length ) { 2006 len = widgets.length; 2007 for ( indx = 0; indx < len; indx++ ) { 2008 if ( widgets[ indx ].match( widgetClass ) ) { 2009 c.widgets[ c.widgets.length ] = widgets[ indx ].replace( widgetClass, '$1' ); 2010 } 2011 } 2012 } 2013 }, 2014 2015 applyWidgetId : function( table, id, init ) { 2016 table = $(table)[0]; 2017 var applied, time, name, 2018 c = table.config, 2019 wo = c.widgetOptions, 2020 debug = ts.debug(c, 'core'), 2021 widget = ts.getWidgetById( id ); 2022 if ( widget ) { 2023 name = widget.id; 2024 applied = false; 2025 // add widget name to option list so it gets reapplied after sorting, filtering, etc 2026 if ( $.inArray( name, c.widgets ) < 0 ) { 2027 c.widgets[ c.widgets.length ] = name; 2028 } 2029 if ( debug ) { time = new Date(); } 2030 2031 if ( init || !( c.widgetInit[ name ] ) ) { 2032 // set init flag first to prevent calling init more than once (e.g. pager) 2033 c.widgetInit[ name ] = true; 2034 if ( table.hasInitialized ) { 2035 // don't reapply widget options on tablesorter init 2036 ts.applyWidgetOptions( table ); 2037 } 2038 if ( typeof widget.init === 'function' ) { 2039 applied = true; 2040 if ( debug ) { 2041 console[ console.group ? 'group' : 'log' ]( 'Initializing ' + name + ' widget' ); 2042 } 2043 widget.init( table, widget, c, wo ); 2044 } 2045 } 2046 if ( !init && typeof widget.format === 'function' ) { 2047 applied = true; 2048 if ( debug ) { 2049 console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' ); 2050 } 2051 widget.format( table, c, wo, false ); 2052 } 2053 if ( debug ) { 2054 if ( applied ) { 2055 console.log( 'Completed ' + ( init ? 'initializing ' : 'applying ' ) + name + ' widget' + ts.benchmark( time ) ); 2056 if ( console.groupEnd ) { console.groupEnd(); } 2057 } 2058 } 2059 } 2060 }, 2061 2062 applyWidget : function( table, init, callback ) { 2063 table = $( table )[ 0 ]; // in case this is called externally 2064 var indx, len, names, widget, time, 2065 c = table.config, 2066 debug = ts.debug(c, 'core'), 2067 widgets = []; 2068 // prevent numerous consecutive widget applications 2069 if ( init !== false && table.hasInitialized && ( table.isApplyingWidgets || table.isUpdating ) ) { 2070 return; 2071 } 2072 if ( debug ) { time = new Date(); } 2073 ts.addWidgetFromClass( table ); 2074 // prevent "tablesorter-ready" from firing multiple times in a row 2075 clearTimeout( c.timerReady ); 2076 if ( c.widgets.length ) { 2077 table.isApplyingWidgets = true; 2078 // ensure unique widget ids 2079 c.widgets = $.grep( c.widgets, function( val, index ) { 2080 return $.inArray( val, c.widgets ) === index; 2081 }); 2082 names = c.widgets || []; 2083 len = names.length; 2084 // build widget array & add priority as needed 2085 for ( indx = 0; indx < len; indx++ ) { 2086 widget = ts.getWidgetById( names[ indx ] ); 2087 if ( widget && widget.id ) { 2088 // set priority to 10 if not defined 2089 if ( !widget.priority ) { widget.priority = 10; } 2090 widgets[ indx ] = widget; 2091 } else if ( debug ) { 2092 console.warn( '"' + names[ indx ] + '" was enabled, but the widget code has not been loaded!' ); 2093 } 2094 } 2095 // sort widgets by priority 2096 widgets.sort( function( a, b ) { 2097 return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1; 2098 }); 2099 // add/update selected widgets 2100 len = widgets.length; 2101 if ( debug ) { 2102 console[ console.group ? 'group' : 'log' ]( 'Start ' + ( init ? 'initializing' : 'applying' ) + ' widgets' ); 2103 } 2104 for ( indx = 0; indx < len; indx++ ) { 2105 widget = widgets[ indx ]; 2106 if ( widget && widget.id ) { 2107 ts.applyWidgetId( table, widget.id, init ); 2108 } 2109 } 2110 if ( debug && console.groupEnd ) { console.groupEnd(); } 2111 } 2112 c.timerReady = setTimeout( function() { 2113 table.isApplyingWidgets = false; 2114 $.data( table, 'lastWidgetApplication', new Date() ); 2115 c.$table.triggerHandler( 'tablesorter-ready' ); 2116 // callback executed on init only 2117 if ( !init && typeof callback === 'function' ) { 2118 callback( table ); 2119 } 2120 if ( debug ) { 2121 widget = c.widgets.length; 2122 console.log( 'Completed ' + 2123 ( init === true ? 'initializing ' : 'applying ' ) + widget + 2124 ' widget' + ( widget !== 1 ? 's' : '' ) + ts.benchmark( time ) ); 2125 } 2126 }, 10 ); 2127 }, 2128 2129 removeWidget : function( table, name, refreshing ) { 2130 table = $( table )[ 0 ]; 2131 var index, widget, indx, len, 2132 c = table.config; 2133 // if name === true, add all widgets from $.tablesorter.widgets 2134 if ( name === true ) { 2135 name = []; 2136 len = ts.widgets.length; 2137 for ( indx = 0; indx < len; indx++ ) { 2138 widget = ts.widgets[ indx ]; 2139 if ( widget && widget.id ) { 2140 name[ name.length ] = widget.id; 2141 } 2142 } 2143 } else { 2144 // name can be either an array of widgets names, 2145 // or a space/comma separated list of widget names 2146 name = ( $.isArray( name ) ? name.join( ',' ) : name || '' ).toLowerCase().split( /[\s,]+/ ); 2147 } 2148 len = name.length; 2149 for ( index = 0; index < len; index++ ) { 2150 widget = ts.getWidgetById( name[ index ] ); 2151 indx = $.inArray( name[ index ], c.widgets ); 2152 // don't remove the widget from config.widget if refreshing 2153 if ( indx >= 0 && refreshing !== true ) { 2154 c.widgets.splice( indx, 1 ); 2155 } 2156 if ( widget && widget.remove ) { 2157 if ( ts.debug(c, 'core') ) { 2158 console.log( ( refreshing ? 'Refreshing' : 'Removing' ) + ' "' + name[ index ] + '" widget' ); 2159 } 2160 widget.remove( table, c, c.widgetOptions, refreshing ); 2161 c.widgetInit[ name[ index ] ] = false; 2162 } 2163 } 2164 c.$table.triggerHandler( 'widgetRemoveEnd', table ); 2165 }, 2166 2167 refreshWidgets : function( table, doAll, dontapply ) { 2168 table = $( table )[ 0 ]; // see issue #243 2169 var indx, widget, 2170 c = table.config, 2171 curWidgets = c.widgets, 2172 widgets = ts.widgets, 2173 len = widgets.length, 2174 list = [], 2175 callback = function( table ) { 2176 $( table ).triggerHandler( 'refreshComplete' ); 2177 }; 2178 // remove widgets not defined in config.widgets, unless doAll is true 2179 for ( indx = 0; indx < len; indx++ ) { 2180 widget = widgets[ indx ]; 2181 if ( widget && widget.id && ( doAll || $.inArray( widget.id, curWidgets ) < 0 ) ) { 2182 list[ list.length ] = widget.id; 2183 } 2184 } 2185 ts.removeWidget( table, list.join( ',' ), true ); 2186 if ( dontapply !== true ) { 2187 // call widget init if 2188 ts.applyWidget( table, doAll || false, callback ); 2189 if ( doAll ) { 2190 // apply widget format 2191 ts.applyWidget( table, false, callback ); 2192 } 2193 } else { 2194 callback( table ); 2195 } 2196 }, 2197 2198 /* 2199 ââ ââ ââââââ ââ ââ ââ ââââââ ââ ââââââ ââââââ 2200 ââ ââ ââ ââ ââ ââ ââ ââ ââââ âââ 2201 ââ ââ ââ ââ ââ ââ ââ ââ ââââ âââ 2202 ââââââ ââ ââ ââââââ ââ ââ ââ ââââââ ââââââ 2203 */ 2204 benchmark : function( diff ) { 2205 return ( ' (' + ( new Date().getTime() - diff.getTime() ) + ' ms)' ); 2206 }, 2207 // deprecated ts.log 2208 log : function() { 2209 console.log( arguments ); 2210 }, 2211 debug : function(c, name) { 2212 return c && ( 2213 c.debug === true || 2214 typeof c.debug === 'string' && c.debug.indexOf(name) > -1 2215 ); 2216 }, 2217 2218 // $.isEmptyObject from jQuery v1.4 2219 isEmptyObject : function( obj ) { 2220 /*jshint forin: false */ 2221 for ( var name in obj ) { 2222 return false; 2223 } 2224 return true; 2225 }, 2226 2227 isValueInArray : function( column, arry ) { 2228 var indx, 2229 len = arry && arry.length || 0; 2230 for ( indx = 0; indx < len; indx++ ) { 2231 if ( arry[ indx ][ 0 ] === column ) { 2232 return indx; 2233 } 2234 } 2235 return -1; 2236 }, 2237 2238 formatFloat : function( str, table ) { 2239 if ( typeof str !== 'string' || str === '' ) { return str; } 2240 // allow using formatFloat without a table; defaults to US number format 2241 var num, 2242 usFormat = table && table.config ? table.config.usNumberFormat !== false : 2243 typeof table !== 'undefined' ? table : true; 2244 if ( usFormat ) { 2245 // US Format - 1,234,567.89 -> 1234567.89 2246 str = str.replace( ts.regex.comma, '' ); 2247 } else { 2248 // German Format = 1.234.567,89 -> 1234567.89 2249 // French Format = 1 234 567,89 -> 1234567.89 2250 str = str.replace( ts.regex.digitNonUS, '' ).replace( ts.regex.comma, '.' ); 2251 } 2252 if ( ts.regex.digitNegativeTest.test( str ) ) { 2253 // make (#) into a negative number -> (10) = -10 2254 str = str.replace( ts.regex.digitNegativeReplace, '-$1' ); 2255 } 2256 num = parseFloat( str ); 2257 // return the text instead of zero 2258 return isNaN( num ) ? $.trim( str ) : num; 2259 }, 2260 2261 isDigit : function( str ) { 2262 // replace all unwanted chars and match 2263 return isNaN( str ) ? 2264 ts.regex.digitTest.test( str.toString().replace( ts.regex.digitReplace, '' ) ) : 2265 str !== ''; 2266 }, 2267 2268 // computeTableHeaderCellIndexes from: 2269 // http://www.javascripttoolbox.com/lib/table/examples.php 2270 // http://www.javascripttoolbox.com/temp/table_cellindex.html 2271 computeColumnIndex : function( $rows, c ) { 2272 var i, j, k, l, cell, cells, rowIndex, rowSpan, colSpan, firstAvailCol, 2273 // total columns has been calculated, use it to set the matrixrow 2274 columns = c && c.columns || 0, 2275 matrix = [], 2276 matrixrow = new Array( columns ); 2277 for ( i = 0; i < $rows.length; i++ ) { 2278 cells = $rows[ i ].cells; 2279 for ( j = 0; j < cells.length; j++ ) { 2280 cell = cells[ j ]; 2281 rowIndex = i; 2282 rowSpan = cell.rowSpan || 1; 2283 colSpan = cell.colSpan || 1; 2284 if ( typeof matrix[ rowIndex ] === 'undefined' ) { 2285 matrix[ rowIndex ] = []; 2286 } 2287 // Find first available column in the first row 2288 for ( k = 0; k < matrix[ rowIndex ].length + 1; k++ ) { 2289 if ( typeof matrix[ rowIndex ][ k ] === 'undefined' ) { 2290 firstAvailCol = k; 2291 break; 2292 } 2293 } 2294 // jscs:disable disallowEmptyBlocks 2295 if ( columns && cell.cellIndex === firstAvailCol ) { 2296 // don't to anything 2297 } else if ( cell.setAttribute ) { 2298 // jscs:enable disallowEmptyBlocks 2299 // add data-column (setAttribute = IE8+) 2300 cell.setAttribute( 'data-column', firstAvailCol ); 2301 } else { 2302 // remove once we drop support for IE7 - 1/12/2016 2303 $( cell ).attr( 'data-column', firstAvailCol ); 2304 } 2305 for ( k = rowIndex; k < rowIndex + rowSpan; k++ ) { 2306 if ( typeof matrix[ k ] === 'undefined' ) { 2307 matrix[ k ] = []; 2308 } 2309 matrixrow = matrix[ k ]; 2310 for ( l = firstAvailCol; l < firstAvailCol + colSpan; l++ ) { 2311 matrixrow[ l ] = 'x'; 2312 } 2313 } 2314 } 2315 } 2316 ts.checkColumnCount($rows, matrix, matrixrow.length); 2317 return matrixrow.length; 2318 }, 2319 2320 checkColumnCount : function($rows, matrix, columns) { 2321 // this DOES NOT report any tbody column issues, except for the math and 2322 // and column selector widgets 2323 var i, len, 2324 valid = true, 2325 cells = []; 2326 for ( i = 0; i < matrix.length; i++ ) { 2327 // some matrix entries are undefined when testing the footer because 2328 // it is using the rowIndex property 2329 if ( matrix[i] ) { 2330 len = matrix[i].length; 2331 if ( matrix[i].length !== columns ) { 2332 valid = false; 2333 break; 2334 } 2335 } 2336 } 2337 if ( !valid ) { 2338 $rows.each( function( indx, el ) { 2339 var cell = el.parentElement.nodeName; 2340 if ( cells.indexOf( cell ) < 0 ) { 2341 cells.push( cell ); 2342 } 2343 }); 2344 console.error( 2345 'Invalid or incorrect number of columns in the ' + 2346 cells.join( ' or ' ) + '; expected ' + columns + 2347 ', but found ' + len + ' columns' 2348 ); 2349 } 2350 }, 2351 2352 // automatically add a colgroup with col elements set to a percentage width 2353 fixColumnWidth : function( table ) { 2354 table = $( table )[ 0 ]; 2355 var overallWidth, percent, $tbodies, len, index, 2356 c = table.config, 2357 $colgroup = c.$table.children( 'colgroup' ); 2358 // remove plugin-added colgroup, in case we need to refresh the widths 2359 if ( $colgroup.length && $colgroup.hasClass( ts.css.colgroup ) ) { 2360 $colgroup.remove(); 2361 } 2362 if ( c.widthFixed && c.$table.children( 'colgroup' ).length === 0 ) { 2363 $colgroup = $( '<colgroup class="' + ts.css.colgroup + '">' ); 2364 overallWidth = c.$table.width(); 2365 // only add col for visible columns - fixes #371 2366 $tbodies = c.$tbodies.find( 'tr:first' ).children( ':visible' ); 2367 len = $tbodies.length; 2368 for ( index = 0; index < len; index++ ) { 2369 percent = parseInt( ( $tbodies.eq( index ).width() / overallWidth ) * 1000, 10 ) / 10 + '%'; 2370 $colgroup.append( $( '<col>' ).css( 'width', percent ) ); 2371 } 2372 c.$table.prepend( $colgroup ); 2373 } 2374 }, 2375 2376 // get sorter, string, empty, etc options for each column from 2377 // jQuery data, metadata, header option or header class name ('sorter-false') 2378 // priority = jQuery data > meta > headers option > header class name 2379 getData : function( header, configHeader, key ) { 2380 var meta, cl4ss, 2381 val = '', 2382 $header = $( header ); 2383 if ( !$header.length ) { return ''; } 2384 meta = $.metadata ? $header.metadata() : false; 2385 cl4ss = ' ' + ( $header.attr( 'class' ) || '' ); 2386 if ( typeof $header.data( key ) !== 'undefined' || 2387 typeof $header.data( key.toLowerCase() ) !== 'undefined' ) { 2388 // 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder' 2389 // 'data-sort-initial-order' is assigned to 'sortInitialOrder' 2390 val += $header.data( key ) || $header.data( key.toLowerCase() ); 2391 } else if ( meta && typeof meta[ key ] !== 'undefined' ) { 2392 val += meta[ key ]; 2393 } else if ( configHeader && typeof configHeader[ key ] !== 'undefined' ) { 2394 val += configHeader[ key ]; 2395 } else if ( cl4ss !== ' ' && cl4ss.match( ' ' + key + '-' ) ) { 2396 // include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser' 2397 val = cl4ss.match( new RegExp( '\\s' + key + '-([\\w-]+)' ) )[ 1 ] || ''; 2398 } 2399 return $.trim( val ); 2400 }, 2401 2402 getColumnData : function( table, obj, indx, getCell, $headers ) { 2403 if ( typeof obj !== 'object' || obj === null ) { 2404 return obj; 2405 } 2406 table = $( table )[ 0 ]; 2407 var $header, key, 2408 c = table.config, 2409 $cells = ( $headers || c.$headers ), 2410 // c.$headerIndexed is not defined initially 2411 $cell = c.$headerIndexed && c.$headerIndexed[ indx ] || 2412 $cells.find( '[data-column="' + indx + '"]:last' ); 2413 if ( typeof obj[ indx ] !== 'undefined' ) { 2414 return getCell ? obj[ indx ] : obj[ $cells.index( $cell ) ]; 2415 } 2416 for ( key in obj ) { 2417 if ( typeof key === 'string' ) { 2418 $header = $cell 2419 // header cell with class/id 2420 .filter( key ) 2421 // find elements within the header cell with cell/id 2422 .add( $cell.find( key ) ); 2423 if ( $header.length ) { 2424 return obj[ key ]; 2425 } 2426 } 2427 } 2428 return; 2429 }, 2430 2431 // *** Process table *** 2432 // add processing indicator 2433 isProcessing : function( $table, toggle, $headers ) { 2434 $table = $( $table ); 2435 var c = $table[ 0 ].config, 2436 // default to all headers 2437 $header = $headers || $table.find( '.' + ts.css.header ); 2438 if ( toggle ) { 2439 // don't use sortList if custom $headers used 2440 if ( typeof $headers !== 'undefined' && c.sortList.length > 0 ) { 2441 // get headers from the sortList 2442 $header = $header.filter( function() { 2443 // get data-column from attr to keep compatibility with jQuery 1.2.6 2444 return this.sortDisabled ? 2445 false : 2446 ts.isValueInArray( parseFloat( $( this ).attr( 'data-column' ) ), c.sortList ) >= 0; 2447 }); 2448 } 2449 $table.add( $header ).addClass( ts.css.processing + ' ' + c.cssProcessing ); 2450 } else { 2451 $table.add( $header ).removeClass( ts.css.processing + ' ' + c.cssProcessing ); 2452 } 2453 }, 2454 2455 // detach tbody but save the position 2456 // don't use tbody because there are portions that look for a tbody index (updateCell) 2457 processTbody : function( table, $tb, getIt ) { 2458 table = $( table )[ 0 ]; 2459 if ( getIt ) { 2460 table.isProcessing = true; 2461 $tb.before( '<colgroup class="tablesorter-savemyplace"/>' ); 2462 return $.fn.detach ? $tb.detach() : $tb.remove(); 2463 } 2464 var holdr = $( table ).find( 'colgroup.tablesorter-savemyplace' ); 2465 $tb.insertAfter( holdr ); 2466 holdr.remove(); 2467 table.isProcessing = false; 2468 }, 2469 2470 clearTableBody : function( table ) { 2471 $( table )[ 0 ].config.$tbodies.children().detach(); 2472 }, 2473 2474 // used when replacing accented characters during sorting 2475 characterEquivalents : { 2476 'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // Ã¡Ã Ã¢Ã£Ã¤Ä Ã¥ 2477 'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÃÃÃÃÃÄà 2478 'c' : '\u00e7\u0107\u010d', // çÄÄ 2479 'C' : '\u00c7\u0106\u010c', // ÃÄÄ 2480 'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëÄÄ 2481 'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÃÃÃÃÄÄ 2482 'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // Ãìİîïı 2483 'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÃÃİÃà 2484 'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6\u014d', // óòôõöŠ2485 'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÃÃÃÃÃÅ 2486 'ss': '\u00df', // à (s sharp) 2487 'SS': '\u1e9e', // Ạ(Capital sharp s) 2488 'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů 2489 'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÃÃÃÃÅ® 2490 }, 2491 2492 replaceAccents : function( str ) { 2493 var chr, 2494 acc = '[', 2495 eq = ts.characterEquivalents; 2496 if ( !ts.characterRegex ) { 2497 ts.characterRegexArray = {}; 2498 for ( chr in eq ) { 2499 if ( typeof chr === 'string' ) { 2500 acc += eq[ chr ]; 2501 ts.characterRegexArray[ chr ] = new RegExp( '[' + eq[ chr ] + ']', 'g' ); 2502 } 2503 } 2504 ts.characterRegex = new RegExp( acc + ']' ); 2505 } 2506 if ( ts.characterRegex.test( str ) ) { 2507 for ( chr in eq ) { 2508 if ( typeof chr === 'string' ) { 2509 str = str.replace( ts.characterRegexArray[ chr ], chr ); 2510 } 2511 } 2512 } 2513 return str; 2514 }, 2515 2516 validateOptions : function( c ) { 2517 var setting, setting2, typ, timer, 2518 // ignore options containing an array 2519 ignore = 'headers sortForce sortList sortAppend widgets'.split( ' ' ), 2520 orig = c.originalSettings; 2521 if ( orig ) { 2522 if ( ts.debug(c, 'core') ) { 2523 timer = new Date(); 2524 } 2525 for ( setting in orig ) { 2526 typ = typeof ts.defaults[setting]; 2527 if ( typ === 'undefined' ) { 2528 console.warn( 'Tablesorter Warning! "table.config.' + setting + '" option not recognized' ); 2529 } else if ( typ === 'object' ) { 2530 for ( setting2 in orig[setting] ) { 2531 typ = ts.defaults[setting] && typeof ts.defaults[setting][setting2]; 2532 if ( $.inArray( setting, ignore ) < 0 && typ === 'undefined' ) { 2533 console.warn( 'Tablesorter Warning! "table.config.' + setting + '.' + setting2 + '" option not recognized' ); 2534 } 2535 } 2536 } 2537 } 2538 if ( ts.debug(c, 'core') ) { 2539 console.log( 'validate options time:' + ts.benchmark( timer ) ); 2540 } 2541 } 2542 }, 2543 2544 // restore headers 2545 restoreHeaders : function( table ) { 2546 var index, $cell, 2547 c = $( table )[ 0 ].config, 2548 $headers = c.$table.find( c.selectorHeaders ), 2549 len = $headers.length; 2550 // don't use c.$headers here in case header cells were swapped 2551 for ( index = 0; index < len; index++ ) { 2552 $cell = $headers.eq( index ); 2553 // only restore header cells if it is wrapped 2554 // because this is also used by the updateAll method 2555 if ( $cell.find( '.' + ts.css.headerIn ).length ) { 2556 $cell.html( c.headerContent[ index ] ); 2557 } 2558 } 2559 }, 2560 2561 destroy : function( table, removeClasses, callback ) { 2562 table = $( table )[ 0 ]; 2563 if ( !table.hasInitialized ) { return; } 2564 // remove all widgets 2565 ts.removeWidget( table, true, false ); 2566 var events, 2567 $t = $( table ), 2568 c = table.config, 2569 $h = $t.find( 'thead:first' ), 2570 $r = $h.find( 'tr.' + ts.css.headerRow ).removeClass( ts.css.headerRow + ' ' + c.cssHeaderRow ), 2571 $f = $t.find( 'tfoot:first > tr' ).children( 'th, td' ); 2572 if ( removeClasses === false && $.inArray( 'uitheme', c.widgets ) >= 0 ) { 2573 // reapply uitheme classes, in case we want to maintain appearance 2574 $t.triggerHandler( 'applyWidgetId', [ 'uitheme' ] ); 2575 $t.triggerHandler( 'applyWidgetId', [ 'zebra' ] ); 2576 } 2577 // remove widget added rows, just in case 2578 $h.find( 'tr' ).not( $r ).remove(); 2579 // disable tablesorter - not using .unbind( namespace ) because namespacing was 2580 // added in jQuery v1.4.3 - see http://api.jquery.com/event.namespace/ 2581 events = 'sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton ' + 2582 'appendCache updateCache applyWidgetId applyWidgets refreshWidgets removeWidget destroy mouseup mouseleave ' + 2583 'keypress sortBegin sortEnd resetToLoadState '.split( ' ' ) 2584 .join( c.namespace + ' ' ); 2585 $t 2586 .removeData( 'tablesorter' ) 2587 .unbind( events.replace( ts.regex.spaces, ' ' ) ); 2588 c.$headers 2589 .add( $f ) 2590 .removeClass( [ ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone ].join( ' ' ) ) 2591 .removeAttr( 'data-column' ) 2592 .removeAttr( 'aria-label' ) 2593 .attr( 'aria-disabled', 'true' ); 2594 $r 2595 .find( c.selectorSort ) 2596 .unbind( ( 'mousedown mouseup keypress '.split( ' ' ).join( c.namespace + ' ' ) ).replace( ts.regex.spaces, ' ' ) ); 2597 ts.restoreHeaders( table ); 2598 $t.toggleClass( ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false ); 2599 $t.removeClass(c.namespace.slice(1)); 2600 // clear flag in case the plugin is initialized again 2601 table.hasInitialized = false; 2602 delete table.config.cache; 2603 if ( typeof callback === 'function' ) { 2604 callback( table ); 2605 } 2606 if ( ts.debug(c, 'core') ) { 2607 console.log( 'tablesorter has been removed' ); 2608 } 2609 } 2610 2611 }; 2612 2613 $.fn.tablesorter = function( settings ) { 2614 return this.each( function() { 2615 var table = this, 2616 // merge & extend config options 2617 c = $.extend( true, {}, ts.defaults, settings, ts.instanceMethods ); 2618 // save initial settings 2619 c.originalSettings = settings; 2620 // create a table from data (build table widget) 2621 if ( !table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE' ) { 2622 // return the table (in case the original target is the table's container) 2623 ts.buildTable( table, c ); 2624 } else { 2625 ts.setup( table, c ); 2626 } 2627 }); 2628 }; 2629 2630 // set up debug logs 2631 if ( !( window.console && window.console.log ) ) { 2632 // access $.tablesorter.logs for browsers that don't have a console... 2633 ts.logs = []; 2634 /*jshint -W020 */ 2635 console = {}; 2636 console.log = console.warn = console.error = console.table = function() { 2637 var arg = arguments.length > 1 ? arguments : arguments[0]; 2638 ts.logs[ ts.logs.length ] = { date: Date.now(), log: arg }; 2639 }; 2640 } 2641 2642 // add default parsers 2643 ts.addParser({ 2644 id : 'no-parser', 2645 is : function() { 2646 return false; 2647 }, 2648 format : function() { 2649 return ''; 2650 }, 2651 type : 'text' 2652 }); 2653 2654 ts.addParser({ 2655 id : 'text', 2656 is : function() { 2657 return true; 2658 }, 2659 format : function( str, table ) { 2660 var c = table.config; 2661 if ( str ) { 2662 str = $.trim( c.ignoreCase ? str.toLocaleLowerCase() : str ); 2663 str = c.sortLocaleCompare ? ts.replaceAccents( str ) : str; 2664 } 2665 return str; 2666 }, 2667 type : 'text' 2668 }); 2669 2670 ts.regex.nondigit = /[^\w,. \-()]/g; 2671 ts.addParser({ 2672 id : 'digit', 2673 is : function( str ) { 2674 return ts.isDigit( str ); 2675 }, 2676 format : function( str, table ) { 2677 var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table ); 2678 return str && typeof num === 'number' ? num : 2679 str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str; 2680 }, 2681 type : 'numeric' 2682 }); 2683 2684 ts.regex.currencyReplace = /[+\-,. ]/g; 2685 ts.regex.currencyTest = /^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/; 2686 ts.addParser({ 2687 id : 'currency', 2688 is : function( str ) { 2689 str = ( str || '' ).replace( ts.regex.currencyReplace, '' ); 2690 // test for £$â¬Â¤Â¥Â¢ 2691 return ts.regex.currencyTest.test( str ); 2692 }, 2693 format : function( str, table ) { 2694 var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table ); 2695 return str && typeof num === 'number' ? num : 2696 str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str; 2697 }, 2698 type : 'numeric' 2699 }); 2700 2701 // too many protocols to add them all https://en.wikipedia.org/wiki/URI_scheme 2702 // now, this regex can be updated before initialization 2703 ts.regex.urlProtocolTest = /^(https?|ftp|file):\/\//; 2704 ts.regex.urlProtocolReplace = /(https?|ftp|file):\/\/(www\.)?/; 2705 ts.addParser({ 2706 id : 'url', 2707 is : function( str ) { 2708 return ts.regex.urlProtocolTest.test( str ); 2709 }, 2710 format : function( str ) { 2711 return str ? $.trim( str.replace( ts.regex.urlProtocolReplace, '' ) ) : str; 2712 }, 2713 type : 'text' 2714 }); 2715 2716 ts.regex.dash = /-/g; 2717 ts.regex.isoDate = /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/; 2718 ts.addParser({ 2719 id : 'isoDate', 2720 is : function( str ) { 2721 return ts.regex.isoDate.test( str ); 2722 }, 2723 format : function( str ) { 2724 var date = str ? new Date( str.replace( ts.regex.dash, '/' ) ) : str; 2725 return date instanceof Date && isFinite( date ) ? date.getTime() : str; 2726 }, 2727 type : 'numeric' 2728 }); 2729 2730 ts.regex.percent = /%/g; 2731 ts.regex.percentTest = /(\d\s*?%|%\s*?\d)/; 2732 ts.addParser({ 2733 id : 'percent', 2734 is : function( str ) { 2735 return ts.regex.percentTest.test( str ) && str.length < 15; 2736 }, 2737 format : function( str, table ) { 2738 return str ? ts.formatFloat( str.replace( ts.regex.percent, '' ), table ) : str; 2739 }, 2740 type : 'numeric' 2741 }); 2742 2743 // added image parser to core v2.17.9 2744 ts.addParser({ 2745 id : 'image', 2746 is : function( str, table, node, $node ) { 2747 return $node.find( 'img' ).length > 0; 2748 }, 2749 format : function( str, table, cell ) { 2750 return $( cell ).find( 'img' ).attr( table.config.imgAttr || 'alt' ) || str; 2751 }, 2752 parsed : true, // filter widget flag 2753 type : 'text' 2754 }); 2755 2756 ts.regex.dateReplace = /(\S)([AP]M)$/i; // used by usLongDate & time parser 2757 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; 2758 ts.regex.usLongDateTest2 = /^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i; 2759 ts.addParser({ 2760 id : 'usLongDate', 2761 is : function( str ) { 2762 // two digit years are not allowed cross-browser 2763 // Jan 01, 2013 12:34:56 PM or 01 Jan 2013 2764 return ts.regex.usLongDateTest1.test( str ) || ts.regex.usLongDateTest2.test( str ); 2765 }, 2766 format : function( str ) { 2767 var date = str ? new Date( str.replace( ts.regex.dateReplace, '$1 $2' ) ) : str; 2768 return date instanceof Date && isFinite( date ) ? date.getTime() : str; 2769 }, 2770 type : 'numeric' 2771 }); 2772 2773 // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included 2774 ts.regex.shortDateTest = /(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/; 2775 // escaped "-" because JSHint in Firefox was showing it as an error 2776 ts.regex.shortDateReplace = /[\-.,]/g; 2777 // XXY covers MDY & DMY formats 2778 ts.regex.shortDateXXY = /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/; 2779 ts.regex.shortDateYMD = /(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/; 2780 ts.convertFormat = function( dateString, format ) { 2781 dateString = ( dateString || '' ) 2782 .replace( ts.regex.spaces, ' ' ) 2783 .replace( ts.regex.shortDateReplace, '/' ); 2784 if ( format === 'mmddyyyy' ) { 2785 dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$1/$2' ); 2786 } else if ( format === 'ddmmyyyy' ) { 2787 dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$2/$1' ); 2788 } else if ( format === 'yyyymmdd' ) { 2789 dateString = dateString.replace( ts.regex.shortDateYMD, '$1/$2/$3' ); 2790 } 2791 var date = new Date( dateString ); 2792 return date instanceof Date && isFinite( date ) ? date.getTime() : ''; 2793 }; 2794 2795 ts.addParser({ 2796 id : 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd' 2797 is : function( str ) { 2798 str = ( str || '' ).replace( ts.regex.spaces, ' ' ).replace( ts.regex.shortDateReplace, '/' ); 2799 return ts.regex.shortDateTest.test( str ); 2800 }, 2801 format : function( str, table, cell, cellIndex ) { 2802 if ( str ) { 2803 var c = table.config, 2804 $header = c.$headerIndexed[ cellIndex ], 2805 format = $header.length && $header.data( 'dateFormat' ) || 2806 ts.getData( $header, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat' ) || 2807 c.dateFormat; 2808 // save format because getData can be slow... 2809 if ( $header.length ) { 2810 $header.data( 'dateFormat', format ); 2811 } 2812 return ts.convertFormat( str, format ) || str; 2813 } 2814 return str; 2815 }, 2816 type : 'numeric' 2817 }); 2818 2819 // match 24 hour time & 12 hours time + am/pm - see http://regexr.com/3c3tk 2820 ts.regex.timeTest = /^(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)$|^((?:[01]\d|[2][0-4]):[0-5]\d)$/i; 2821 ts.regex.timeMatch = /(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)/i; 2822 ts.addParser({ 2823 id : 'time', 2824 is : function( str ) { 2825 return ts.regex.timeTest.test( str ); 2826 }, 2827 format : function( str ) { 2828 // isolate time... ignore month, day and year 2829 var temp, 2830 timePart = ( str || '' ).match( ts.regex.timeMatch ), 2831 orig = new Date( str ), 2832 // no time component? default to 00:00 by leaving it out, but only if str is defined 2833 time = str && ( timePart !== null ? timePart[ 0 ] : '00:00 AM' ), 2834 date = time ? new Date( '2000/01/01 ' + time.replace( ts.regex.dateReplace, '$1 $2' ) ) : time; 2835 if ( date instanceof Date && isFinite( date ) ) { 2836 temp = orig instanceof Date && isFinite( orig ) ? orig.getTime() : 0; 2837 // if original string was a valid date, add it to the decimal so the column sorts in some kind of order 2838 // luckily new Date() ignores the decimals 2839 return temp ? parseFloat( date.getTime() + '.' + orig.getTime() ) : date.getTime(); 2840 } 2841 return str; 2842 }, 2843 type : 'numeric' 2844 }); 2845 2846 ts.addParser({ 2847 id : 'metadata', 2848 is : function() { 2849 return false; 2850 }, 2851 format : function( str, table, cell ) { 2852 var c = table.config, 2853 p = ( !c.parserMetadataName ) ? 'sortValue' : c.parserMetadataName; 2854 return $( cell ).metadata()[ p ]; 2855 }, 2856 type : 'numeric' 2857 }); 2858 2859 /* 2860 ââââââ ââââââ ââââââ ââââââ ââââââ 2861 âââ ââââ ââââââ ââââââ ââââââ 2862 âââ ââââ ââââââ âââââ ââââââ 2863 ââââââ ââââââ ââââââ ââ ââ ââ ââ 2864 */ 2865 // add default widgets 2866 ts.addWidget({ 2867 id : 'zebra', 2868 priority : 90, 2869 format : function( table, c, wo ) { 2870 var $visibleRows, $row, count, isEven, tbodyIndex, rowIndex, len, 2871 child = new RegExp( c.cssChildRow, 'i' ), 2872 $tbodies = c.$tbodies.add( $( c.namespace + '_extra_table' ).children( 'tbody:not(.' + c.cssInfoBlock + ')' ) ); 2873 for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 2874 // loop through the visible rows 2875 count = 0; 2876 $visibleRows = $tbodies.eq( tbodyIndex ).children( 'tr:visible' ).not( c.selectorRemove ); 2877 len = $visibleRows.length; 2878 for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { 2879 $row = $visibleRows.eq( rowIndex ); 2880 // style child rows the same way the parent row was styled 2881 if ( !child.test( $row[ 0 ].className ) ) { count++; } 2882 isEven = ( count % 2 === 0 ); 2883 $row 2884 .removeClass( wo.zebra[ isEven ? 1 : 0 ] ) 2885 .addClass( wo.zebra[ isEven ? 0 : 1 ] ); 2886 } 2887 } 2888 }, 2889 remove : function( table, c, wo, refreshing ) { 2890 if ( refreshing ) { return; } 2891 var tbodyIndex, $tbody, 2892 $tbodies = c.$tbodies, 2893 toRemove = ( wo.zebra || [ 'even', 'odd' ] ).join( ' ' ); 2894 for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 2895 $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody 2896 $tbody.children().removeClass( toRemove ); 2897 ts.processTbody( table, $tbody, false ); // restore tbody 2898 } 2899 } 2900 }); 2901 2902})( jQuery ); 2903 2904/*! Widget: storage - updated 2018-03-18 (v2.30.0) */ 2905/*global JSON:false */ 2906;(function ($, window, document) { 2907 'use strict'; 2908 2909 var ts = $.tablesorter || {}; 2910 2911 // update defaults for validator; these values must be falsy! 2912 $.extend(true, ts.defaults, { 2913 fixedUrl: '', 2914 widgetOptions: { 2915 storage_fixedUrl: '', 2916 storage_group: '', 2917 storage_page: '', 2918 storage_storageType: '', 2919 storage_tableId: '', 2920 storage_useSessionStorage: '' 2921 } 2922 }); 2923 2924 // *** Store data in local storage, with a cookie fallback *** 2925 /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json) 2926 if you need it, then include https://github.com/douglascrockford/JSON-js 2927 2928 $.parseJSON is not available is jQuery versions older than 1.4.1, using older 2929 versions will only allow storing information for one page at a time 2930 2931 // *** Save data (JSON format only) *** 2932 // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid 2933 var val = { "mywidget" : "data1" }; // valid JSON uses double quotes 2934 // $.tablesorter.storage(table, key, val); 2935 $.tablesorter.storage(table, 'tablesorter-mywidget', val); 2936 2937 // *** Get data: $.tablesorter.storage(table, key); *** 2938 v = $.tablesorter.storage(table, 'tablesorter-mywidget'); 2939 // val may be empty, so also check for your data 2940 val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : ''; 2941 alert(val); // 'data1' if saved, or '' if not 2942 */ 2943 ts.storage = function(table, key, value, options) { 2944 table = $(table)[0]; 2945 var cookieIndex, cookies, date, 2946 hasStorage = false, 2947 values = {}, 2948 c = table.config, 2949 wo = c && c.widgetOptions, 2950 debug = ts.debug(c, 'storage'), 2951 storageType = ( 2952 ( options && options.storageType ) || ( wo && wo.storage_storageType ) 2953 ).toString().charAt(0).toLowerCase(), 2954 // deprecating "useSessionStorage"; any storageType setting overrides it 2955 session = storageType ? '' : 2956 ( options && options.useSessionStorage ) || ( wo && wo.storage_useSessionStorage ), 2957 $table = $(table), 2958 // id from (1) options ID, (2) table 'data-table-group' attribute, (3) widgetOptions.storage_tableId, 2959 // (4) table ID, then (5) table index 2960 id = options && options.id || 2961 $table.attr( options && options.group || wo && wo.storage_group || 'data-table-group') || 2962 wo && wo.storage_tableId || table.id || $('.tablesorter').index( $table ), 2963 // url from (1) options url, (2) table 'data-table-page' attribute, (3) widgetOptions.storage_fixedUrl, 2964 // (4) table.config.fixedUrl (deprecated), then (5) window location path 2965 url = options && options.url || 2966 $table.attr(options && options.page || wo && wo.storage_page || 'data-table-page') || 2967 wo && wo.storage_fixedUrl || c && c.fixedUrl || window.location.pathname; 2968 2969 // skip if using cookies 2970 if (storageType !== 'c') { 2971 storageType = (storageType === 's' || session) ? 'sessionStorage' : 'localStorage'; 2972 // https://gist.github.com/paulirish/5558557 2973 if (storageType in window) { 2974 try { 2975 window[storageType].setItem('_tmptest', 'temp'); 2976 hasStorage = true; 2977 window[storageType].removeItem('_tmptest'); 2978 } catch (error) { 2979 console.warn( storageType + ' is not supported in this browser' ); 2980 } 2981 } 2982 } 2983 if (debug) { 2984 console.log('Storage >> Using', hasStorage ? storageType : 'cookies'); 2985 } 2986 // *** get value *** 2987 if ($.parseJSON) { 2988 if (hasStorage) { 2989 values = $.parseJSON( window[storageType][key] || 'null' ) || {}; 2990 } else { 2991 // old browser, using cookies 2992 cookies = document.cookie.split(/[;\s|=]/); 2993 // add one to get from the key to the value 2994 cookieIndex = $.inArray(key, cookies) + 1; 2995 values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || 'null') || {} : {}; 2996 } 2997 } 2998 // allow value to be an empty string too 2999 if (typeof value !== 'undefined' && window.JSON && JSON.hasOwnProperty('stringify')) { 3000 // add unique identifiers = url pathname > table ID/index on page > data 3001 if (!values[url]) { 3002 values[url] = {}; 3003 } 3004 values[url][id] = value; 3005 // *** set value *** 3006 if (hasStorage) { 3007 window[storageType][key] = JSON.stringify(values); 3008 } else { 3009 date = new Date(); 3010 date.setTime(date.getTime() + (31536e+6)); // 365 days 3011 document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g, '\"') + '; expires=' + date.toGMTString() + '; path=/'; 3012 } 3013 } else { 3014 return values && values[url] ? values[url][id] : ''; 3015 } 3016 }; 3017 3018})(jQuery, window, document); 3019 3020/*! Widget: uitheme - updated 2018-03-18 (v2.30.0) */ 3021;(function ($) { 3022 'use strict'; 3023 var ts = $.tablesorter || {}; 3024 3025 ts.themes = { 3026 'bootstrap' : { 3027 table : 'table table-bordered table-striped', 3028 caption : 'caption', 3029 // header class names 3030 header : 'bootstrap-header', // give the header a gradient background (theme.bootstrap_2.css) 3031 sortNone : '', 3032 sortAsc : '', 3033 sortDesc : '', 3034 active : '', // applied when column is sorted 3035 hover : '', // custom css required - a defined bootstrap style may not override other classes 3036 // icon class names 3037 icons : '', // add 'bootstrap-icon-white' to make them white; this icon class is added to the <i> in the header 3038 iconSortNone : 'bootstrap-icon-unsorted', // class name added to icon when column is not sorted 3039 iconSortAsc : 'glyphicon glyphicon-chevron-up', // class name added to icon when column has ascending sort 3040 iconSortDesc : 'glyphicon glyphicon-chevron-down', // class name added to icon when column has descending sort 3041 filterRow : '', // filter row class 3042 footerRow : '', 3043 footerCells : '', 3044 even : '', // even row zebra striping 3045 odd : '' // odd row zebra striping 3046 }, 3047 'jui' : { 3048 table : 'ui-widget ui-widget-content ui-corner-all', // table classes 3049 caption : 'ui-widget-content', 3050 // header class names 3051 header : 'ui-widget-header ui-corner-all ui-state-default', // header classes 3052 sortNone : '', 3053 sortAsc : '', 3054 sortDesc : '', 3055 active : 'ui-state-active', // applied when column is sorted 3056 hover : 'ui-state-hover', // hover class 3057 // icon class names 3058 icons : 'ui-icon', // icon class added to the <i> in the header 3059 iconSortNone : 'ui-icon-carat-2-n-s ui-icon-caret-2-n-s', // class name added to icon when column is not sorted 3060 iconSortAsc : 'ui-icon-carat-1-n ui-icon-caret-1-n', // class name added to icon when column has ascending sort 3061 iconSortDesc : 'ui-icon-carat-1-s ui-icon-caret-1-s', // class name added to icon when column has descending sort 3062 filterRow : '', 3063 footerRow : '', 3064 footerCells : '', 3065 even : 'ui-widget-content', // even row zebra striping 3066 odd : 'ui-state-default' // odd row zebra striping 3067 } 3068 }; 3069 3070 $.extend(ts.css, { 3071 wrapper : 'tablesorter-wrapper' // ui theme & resizable 3072 }); 3073 3074 ts.addWidget({ 3075 id: 'uitheme', 3076 priority: 10, 3077 format: function(table, c, wo) { 3078 var i, tmp, hdr, icon, time, $header, $icon, $tfoot, $h, oldtheme, oldremove, oldIconRmv, hasOldTheme, 3079 themesAll = ts.themes, 3080 $table = c.$table.add( $( c.namespace + '_extra_table' ) ), 3081 $headers = c.$headers.add( $( c.namespace + '_extra_headers' ) ), 3082 theme = c.theme || 'jui', 3083 themes = themesAll[theme] || {}, 3084 remove = $.trim( [ themes.sortNone, themes.sortDesc, themes.sortAsc, themes.active ].join( ' ' ) ), 3085 iconRmv = $.trim( [ themes.iconSortNone, themes.iconSortDesc, themes.iconSortAsc ].join( ' ' ) ), 3086 debug = ts.debug(c, 'uitheme'); 3087 if (debug) { time = new Date(); } 3088 // initialization code - run once 3089 if (!$table.hasClass('tablesorter-' + theme) || c.theme !== c.appliedTheme || !wo.uitheme_applied) { 3090 wo.uitheme_applied = true; 3091 oldtheme = themesAll[c.appliedTheme] || {}; 3092 hasOldTheme = !$.isEmptyObject(oldtheme); 3093 oldremove = hasOldTheme ? [ oldtheme.sortNone, oldtheme.sortDesc, oldtheme.sortAsc, oldtheme.active ].join( ' ' ) : ''; 3094 oldIconRmv = hasOldTheme ? [ oldtheme.iconSortNone, oldtheme.iconSortDesc, oldtheme.iconSortAsc ].join( ' ' ) : ''; 3095 if (hasOldTheme) { 3096 wo.zebra[0] = $.trim( ' ' + wo.zebra[0].replace(' ' + oldtheme.even, '') ); 3097 wo.zebra[1] = $.trim( ' ' + wo.zebra[1].replace(' ' + oldtheme.odd, '') ); 3098 c.$tbodies.children().removeClass( [ oldtheme.even, oldtheme.odd ].join(' ') ); 3099 } 3100 // update zebra stripes 3101 if (themes.even) { wo.zebra[0] += ' ' + themes.even; } 3102 if (themes.odd) { wo.zebra[1] += ' ' + themes.odd; } 3103 // add caption style 3104 $table.children('caption') 3105 .removeClass(oldtheme.caption || '') 3106 .addClass(themes.caption); 3107 // add table/footer class names 3108 $tfoot = $table 3109 // remove other selected themes 3110 .removeClass( (c.appliedTheme ? 'tablesorter-' + (c.appliedTheme || '') : '') + ' ' + (oldtheme.table || '') ) 3111 .addClass('tablesorter-' + theme + ' ' + (themes.table || '')) // add theme widget class name 3112 .children('tfoot'); 3113 c.appliedTheme = c.theme; 3114 3115 if ($tfoot.length) { 3116 $tfoot 3117 // if oldtheme.footerRow or oldtheme.footerCells are undefined, all class names are removed 3118 .children('tr').removeClass(oldtheme.footerRow || '').addClass(themes.footerRow) 3119 .children('th, td').removeClass(oldtheme.footerCells || '').addClass(themes.footerCells); 3120 } 3121 // update header classes 3122 $headers 3123 .removeClass( (hasOldTheme ? [ oldtheme.header, oldtheme.hover, oldremove ].join(' ') : '') || '' ) 3124 .addClass(themes.header) 3125 .not('.sorter-false') 3126 .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') 3127 .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) { 3128 // toggleClass with switch added in jQuery 1.3 3129 $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover || ''); 3130 }); 3131 3132 $headers.each(function() { 3133 var $this = $(this); 3134 if (!$this.find('.' + ts.css.wrapper).length) { 3135 // Firefox needs this inner div to position the icon & resizer correctly 3136 $this.wrapInner('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>'); 3137 } 3138 }); 3139 if (c.cssIcon) { 3140 // if c.cssIcon is '', then no <i> is added to the header 3141 $headers 3142 .find('.' + ts.css.icon) 3143 .removeClass(hasOldTheme ? [ oldtheme.icons, oldIconRmv ].join(' ') : '') 3144 .addClass(themes.icons || ''); 3145 } 3146 // filter widget initializes after uitheme 3147 if (ts.hasWidget( c.table, 'filter' )) { 3148 tmp = function() { 3149 $table.children('thead').children('.' + ts.css.filterRow) 3150 .removeClass(hasOldTheme ? oldtheme.filterRow || '' : '') 3151 .addClass(themes.filterRow || ''); 3152 }; 3153 if (wo.filter_initialized) { 3154 tmp(); 3155 } else { 3156 $table.one('filterInit', function() { 3157 tmp(); 3158 }); 3159 } 3160 } 3161 } 3162 for (i = 0; i < c.columns; i++) { 3163 $header = c.$headers 3164 .add($(c.namespace + '_extra_headers')) 3165 .not('.sorter-false') 3166 .filter('[data-column="' + i + '"]'); 3167 $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $(); 3168 $h = $headers.not('.sorter-false').filter('[data-column="' + i + '"]:last'); 3169 if ($h.length) { 3170 $header.removeClass(remove); 3171 $icon.removeClass(iconRmv); 3172 if ($h[0].sortDisabled) { 3173 // no sort arrows for disabled columns! 3174 $icon.removeClass(themes.icons || ''); 3175 } else { 3176 hdr = themes.sortNone; 3177 icon = themes.iconSortNone; 3178 if ($h.hasClass(ts.css.sortAsc)) { 3179 hdr = [ themes.sortAsc, themes.active ].join(' '); 3180 icon = themes.iconSortAsc; 3181 } else if ($h.hasClass(ts.css.sortDesc)) { 3182 hdr = [ themes.sortDesc, themes.active ].join(' '); 3183 icon = themes.iconSortDesc; 3184 } 3185 $header.addClass(hdr); 3186 $icon.addClass(icon || ''); 3187 } 3188 } 3189 } 3190 if (debug) { 3191 console.log('uitheme >> Applied ' + theme + ' theme' + ts.benchmark(time)); 3192 } 3193 }, 3194 remove: function(table, c, wo, refreshing) { 3195 if (!wo.uitheme_applied) { return; } 3196 var $table = c.$table, 3197 theme = c.appliedTheme || 'jui', 3198 themes = ts.themes[ theme ] || ts.themes.jui, 3199 $headers = $table.children('thead').children(), 3200 remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc, 3201 iconRmv = themes.iconSortNone + ' ' + themes.iconSortDesc + ' ' + themes.iconSortAsc; 3202 $table.removeClass('tablesorter-' + theme + ' ' + themes.table); 3203 wo.uitheme_applied = false; 3204 if (refreshing) { return; } 3205 $table.find(ts.css.header).removeClass(themes.header); 3206 $headers 3207 .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover 3208 .removeClass(themes.hover + ' ' + remove + ' ' + themes.active) 3209 .filter('.' + ts.css.filterRow) 3210 .removeClass(themes.filterRow); 3211 $headers.find('.' + ts.css.icon).removeClass(themes.icons + ' ' + iconRmv); 3212 } 3213 }); 3214 3215})(jQuery); 3216 3217/*! Widget: columns - updated 5/24/2017 (v2.28.11) */ 3218;(function ($) { 3219 'use strict'; 3220 var ts = $.tablesorter || {}; 3221 3222 ts.addWidget({ 3223 id: 'columns', 3224 priority: 65, 3225 options : { 3226 columns : [ 'primary', 'secondary', 'tertiary' ] 3227 }, 3228 format: function(table, c, wo) { 3229 var $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx, 3230 $table = c.$table, 3231 $tbodies = c.$tbodies, 3232 sortList = c.sortList, 3233 len = sortList.length, 3234 // removed c.widgetColumns support 3235 css = wo && wo.columns || [ 'primary', 'secondary', 'tertiary' ], 3236 last = css.length - 1; 3237 remove = css.join(' '); 3238 // check if there is a sort (on initialization there may not be one) 3239 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 3240 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody 3241 $rows = $tbody.children('tr'); 3242 // loop through the visible rows 3243 $rows.each(function() { 3244 $row = $(this); 3245 if (this.style.display !== 'none') { 3246 // remove all columns class names 3247 $cells = $row.children().removeClass(remove); 3248 // add appropriate column class names 3249 if (sortList && sortList[0]) { 3250 // primary sort column class 3251 $cells.eq(sortList[0][0]).addClass(css[0]); 3252 if (len > 1) { 3253 for (indx = 1; indx < len; indx++) { 3254 // secondary, tertiary, etc sort column classes 3255 $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] ); 3256 } 3257 } 3258 } 3259 } 3260 }); 3261 ts.processTbody(table, $tbody, false); 3262 } 3263 // add classes to thead and tfoot 3264 rows = wo.columns_thead !== false ? [ 'thead tr' ] : []; 3265 if (wo.columns_tfoot !== false) { 3266 rows.push('tfoot tr'); 3267 } 3268 if (rows.length) { 3269 $rows = $table.find( rows.join(',') ).children().removeClass(remove); 3270 if (len) { 3271 for (indx = 0; indx < len; indx++) { 3272 // add primary. secondary, tertiary, etc sort column classes 3273 $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]); 3274 } 3275 } 3276 } 3277 }, 3278 remove: function(table, c, wo) { 3279 var tbodyIndex, $tbody, 3280 $tbodies = c.$tbodies, 3281 remove = (wo.columns || [ 'primary', 'secondary', 'tertiary' ]).join(' '); 3282 c.$headers.removeClass(remove); 3283 c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove); 3284 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 3285 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody 3286 $tbody.children('tr').each(function() { 3287 $(this).children().removeClass(remove); 3288 }); 3289 ts.processTbody(table, $tbody, false); // restore tbody 3290 } 3291 } 3292 }); 3293 3294})(jQuery); 3295 3296/*! Widget: filter - updated 2018-03-18 (v2.30.0) *//* 3297 * Requires tablesorter v2.8+ and jQuery 1.7+ 3298 * by Rob Garrison 3299 */ 3300;( function ( $ ) { 3301 'use strict'; 3302 var tsf, tsfRegex, 3303 ts = $.tablesorter || {}, 3304 tscss = ts.css, 3305 tskeyCodes = ts.keyCodes; 3306 3307 $.extend( tscss, { 3308 filterRow : 'tablesorter-filter-row', 3309 filter : 'tablesorter-filter', 3310 filterDisabled : 'disabled', 3311 filterRowHide : 'hideme' 3312 }); 3313 3314 $.extend( tskeyCodes, { 3315 backSpace : 8, 3316 escape : 27, 3317 space : 32, 3318 left : 37, 3319 down : 40 3320 }); 3321 3322 ts.addWidget({ 3323 id: 'filter', 3324 priority: 50, 3325 options : { 3326 filter_cellFilter : '', // css class name added to the filter cell ( string or array ) 3327 filter_childRows : false, // if true, filter includes child row content in the search 3328 filter_childByColumn : false, // ( filter_childRows must be true ) if true = search child rows by column; false = search all child row text grouped 3329 filter_childWithSibs : true, // if true, include matching child row siblings 3330 filter_columnAnyMatch: true, // if true, allows using '#:{query}' in AnyMatch searches ( column:query ) 3331 filter_columnFilters : true, // if true, a filter will be added to the top of each table column 3332 filter_cssFilter : '', // css class name added to the filter row & each input in the row ( tablesorter-filter is ALWAYS added ) 3333 filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value 3334 filter_defaultFilter : {}, // add a default column filter type '~{query}' to make fuzzy searches default; '{q1} AND {q2}' to make all searches use a logical AND. 3335 filter_excludeFilter : {}, // filters to exclude, per column 3336 filter_external : '', // jQuery selector string ( or jQuery object ) of external filters 3337 filter_filteredRow : 'filtered', // class added to filtered rows; define in css with "display:none" to hide the filtered-out rows 3338 filter_filterLabel : 'Filter "{{label}}" column by...', // Aria-label added to filter input/select; see #1495 3339 filter_formatter : null, // add custom filter elements to the filter row 3340 filter_functions : null, // add custom filter functions using this option 3341 filter_hideEmpty : true, // hide filter row when table is empty 3342 filter_hideFilters : false, // collapse filter row when mouse leaves the area 3343 filter_ignoreCase : true, // if true, make all searches case-insensitive 3344 filter_liveSearch : true, // if true, search column content while the user types ( with a delay ) 3345 filter_matchType : { 'input': 'exact', 'select': 'exact' }, // global query settings ('exact' or 'match'); overridden by "filter-match" or "filter-exact" class 3346 filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available ( visible ) options within the drop down 3347 filter_placeholder : { search : '', select : '' }, // default placeholder text ( overridden by any header 'data-placeholder' setting ) 3348 filter_reset : null, // jQuery selector string of an element used to reset the filters 3349 filter_resetOnEsc : true, // Reset filter input when the user presses escape - normalized across browsers 3350 filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters 3351 filter_searchDelay : 300, // typing delay in milliseconds before starting a search 3352 filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true 3353 filter_selectSource : null, // include a function to return an array of values to be added to the column filter select 3354 filter_selectSourceSeparator : '|', // filter_selectSource array text left of the separator is added to the option value, right into the option text 3355 filter_serversideFiltering : false, // if true, must perform server-side filtering b/c client-side filtering is disabled, but the ui and events will still be used. 3356 filter_startsWith : false, // if true, filter start from the beginning of the cell contents 3357 filter_useParsedData : false // filter all data using parsed content 3358 }, 3359 format: function( table, c, wo ) { 3360 if ( !c.$table.hasClass( 'hasFilters' ) ) { 3361 tsf.init( table, c, wo ); 3362 } 3363 }, 3364 remove: function( table, c, wo, refreshing ) { 3365 var tbodyIndex, $tbody, 3366 $table = c.$table, 3367 $tbodies = c.$tbodies, 3368 events = ( 3369 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + 3370 'filterAndSortReset filterFomatterUpdate filterEnd search stickyHeadersInit ' 3371 ).split( ' ' ).join( c.namespace + 'filter ' ); 3372 $table 3373 .removeClass( 'hasFilters' ) 3374 // add filter namespace to all BUT search 3375 .unbind( events.replace( ts.regex.spaces, ' ' ) ) 3376 // remove the filter row even if refreshing, because the column might have been moved 3377 .find( '.' + tscss.filterRow ).remove(); 3378 wo.filter_initialized = false; 3379 if ( refreshing ) { return; } 3380 for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 3381 $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody 3382 $tbody.children().removeClass( wo.filter_filteredRow ).show(); 3383 ts.processTbody( table, $tbody, false ); // restore tbody 3384 } 3385 if ( wo.filter_reset ) { 3386 $( document ).undelegate( wo.filter_reset, 'click' + c.namespace + 'filter' ); 3387 } 3388 } 3389 }); 3390 3391 tsf = ts.filter = { 3392 3393 // regex used in filter 'check' functions - not for general use and not documented 3394 regex: { 3395 regex : /^\/((?:\\\/|[^\/])+)\/([migyu]{0,5})?$/, // regex to test for regex 3396 child : /tablesorter-childRow/, // child row class name; this gets updated in the script 3397 filtered : /filtered/, // filtered (hidden) row class name; updated in the script 3398 type : /undefined|number/, // check type 3399 exact : /(^[\"\'=]+)|([\"\'=]+$)/g, // exact match (allow '==') 3400 operators : /[<>=]/g, // replace operators 3401 query : '(q|query)', // replace filter queries 3402 wild01 : /\?/g, // wild card match 0 or 1 3403 wild0More : /\*/g, // wild care match 0 or more 3404 quote : /\"/g, 3405 isNeg1 : /(>=?\s*-\d)/, 3406 isNeg2 : /(<=?\s*\d)/ 3407 }, 3408 // function( c, data ) { } 3409 // c = table.config 3410 // data.$row = jQuery object of the row currently being processed 3411 // data.$cells = jQuery object of all cells within the current row 3412 // data.filters = array of filters for all columns ( some may be undefined ) 3413 // data.filter = filter for the current column 3414 // data.iFilter = same as data.filter, except lowercase ( if wo.filter_ignoreCase is true ) 3415 // data.exact = table cell text ( or parsed data if column parser enabled; may be a number & not a string ) 3416 // data.iExact = same as data.exact, except lowercase ( if wo.filter_ignoreCase is true; may be a number & not a string ) 3417 // data.cache = table cell text from cache, so it has been parsed ( & in all lower case if c.ignoreCase is true ) 3418 // data.cacheArray = An array of parsed content from each table cell in the row being processed 3419 // data.index = column index; table = table element ( DOM ) 3420 // data.parsed = array ( by column ) of boolean values ( from filter_useParsedData or 'filter-parsed' class ) 3421 types: { 3422 or : function( c, data, vars ) { 3423 // look for "|", but not if it is inside of a regular expression 3424 if ( ( tsfRegex.orTest.test( data.iFilter ) || tsfRegex.orSplit.test( data.filter ) ) && 3425 // this test for regex has potential to slow down the overall search 3426 !tsfRegex.regex.test( data.filter ) ) { 3427 var indx, filterMatched, query, regex, 3428 // duplicate data but split filter 3429 data2 = $.extend( {}, data ), 3430 filter = data.filter.split( tsfRegex.orSplit ), 3431 iFilter = data.iFilter.split( tsfRegex.orSplit ), 3432 len = filter.length; 3433 for ( indx = 0; indx < len; indx++ ) { 3434 data2.nestedFilters = true; 3435 data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); 3436 data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); 3437 query = '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')'; 3438 try { 3439 // use try/catch, because query may not be a valid regex if "|" is contained within a partial regex search, 3440 // e.g "/(Alex|Aar" -> Uncaught SyntaxError: Invalid regular expression: /(/(Alex)/: Unterminated group 3441 regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); 3442 // filterMatched = data2.filter === '' && indx > 0 ? true 3443 // look for an exact match with the 'or' unless the 'filter-match' class is found 3444 filterMatched = regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ); 3445 if ( filterMatched ) { 3446 return filterMatched; 3447 } 3448 } catch ( error ) { 3449 return null; 3450 } 3451 } 3452 // may be null from processing types 3453 return filterMatched || false; 3454 } 3455 return null; 3456 }, 3457 // Look for an AND or && operator ( logical and ) 3458 and : function( c, data, vars ) { 3459 if ( tsfRegex.andTest.test( data.filter ) ) { 3460 var indx, filterMatched, result, query, regex, 3461 // duplicate data but split filter 3462 data2 = $.extend( {}, data ), 3463 filter = data.filter.split( tsfRegex.andSplit ), 3464 iFilter = data.iFilter.split( tsfRegex.andSplit ), 3465 len = filter.length; 3466 for ( indx = 0; indx < len; indx++ ) { 3467 data2.nestedFilters = true; 3468 data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); 3469 data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); 3470 query = ( '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')' ) 3471 // replace wild cards since /(a*)/i will match anything 3472 .replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ); 3473 try { 3474 // use try/catch just in case RegExp is invalid 3475 regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); 3476 // look for an exact match with the 'and' unless the 'filter-match' class is found 3477 result = ( regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ) ); 3478 if ( indx === 0 ) { 3479 filterMatched = result; 3480 } else { 3481 filterMatched = filterMatched && result; 3482 } 3483 } catch ( error ) { 3484 return null; 3485 } 3486 } 3487 // may be null from processing types 3488 return filterMatched || false; 3489 } 3490 return null; 3491 }, 3492 // Look for regex 3493 regex: function( c, data ) { 3494 if ( tsfRegex.regex.test( data.filter ) ) { 3495 var matches, 3496 // cache regex per column for optimal speed 3497 regex = data.filter_regexCache[ data.index ] || tsfRegex.regex.exec( data.filter ), 3498 isRegex = regex instanceof RegExp; 3499 try { 3500 if ( !isRegex ) { 3501 // force case insensitive search if ignoreCase option set? 3502 // if ( c.ignoreCase && !regex[2] ) { regex[2] = 'i'; } 3503 data.filter_regexCache[ data.index ] = regex = new RegExp( regex[1], regex[2] ); 3504 } 3505 matches = regex.test( data.exact ); 3506 } catch ( error ) { 3507 matches = false; 3508 } 3509 return matches; 3510 } 3511 return null; 3512 }, 3513 // Look for operators >, >=, < or <= 3514 operators: function( c, data ) { 3515 // ignore empty strings... because '' < 10 is true 3516 if ( tsfRegex.operTest.test( data.iFilter ) && data.iExact !== '' ) { 3517 var cachedValue, result, txt, 3518 table = c.table, 3519 parsed = data.parsed[ data.index ], 3520 query = ts.formatFloat( data.iFilter.replace( tsfRegex.operators, '' ), table ), 3521 parser = c.parsers[ data.index ] || {}, 3522 savedSearch = query; 3523 // parse filter value in case we're comparing numbers ( dates ) 3524 if ( parsed || parser.type === 'numeric' ) { 3525 txt = $.trim( '' + data.iFilter.replace( tsfRegex.operators, '' ) ); 3526 result = tsf.parseFilter( c, txt, data, true ); 3527 query = ( typeof result === 'number' && result !== '' && !isNaN( result ) ) ? result : query; 3528 } 3529 // iExact may be numeric - see issue #149; 3530 // check if cached is defined, because sometimes j goes out of range? ( numeric columns ) 3531 if ( ( parsed || parser.type === 'numeric' ) && !isNaN( query ) && 3532 typeof data.cache !== 'undefined' ) { 3533 cachedValue = data.cache; 3534 } else { 3535 txt = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; 3536 cachedValue = ts.formatFloat( txt, table ); 3537 } 3538 if ( tsfRegex.gtTest.test( data.iFilter ) ) { 3539 result = tsfRegex.gteTest.test( data.iFilter ) ? cachedValue >= query : cachedValue > query; 3540 } else if ( tsfRegex.ltTest.test( data.iFilter ) ) { 3541 result = tsfRegex.lteTest.test( data.iFilter ) ? cachedValue <= query : cachedValue < query; 3542 } 3543 // keep showing all rows if nothing follows the operator 3544 if ( !result && savedSearch === '' ) { 3545 result = true; 3546 } 3547 return result; 3548 } 3549 return null; 3550 }, 3551 // Look for a not match 3552 notMatch: function( c, data ) { 3553 if ( tsfRegex.notTest.test( data.iFilter ) ) { 3554 var indx, 3555 txt = data.iFilter.replace( '!', '' ), 3556 filter = tsf.parseFilter( c, txt, data ) || ''; 3557 if ( tsfRegex.exact.test( filter ) ) { 3558 // look for exact not matches - see #628 3559 filter = filter.replace( tsfRegex.exact, '' ); 3560 return filter === '' ? true : $.trim( filter ) !== data.iExact; 3561 } else { 3562 indx = data.iExact.search( $.trim( filter ) ); 3563 return filter === '' ? true : 3564 // return true if not found 3565 data.anyMatch ? indx < 0 : 3566 // return false if found 3567 !( c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 0 ); 3568 } 3569 } 3570 return null; 3571 }, 3572 // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric 3573 exact: function( c, data ) { 3574 /*jshint eqeqeq:false */ 3575 if ( tsfRegex.exact.test( data.iFilter ) ) { 3576 var txt = data.iFilter.replace( tsfRegex.exact, '' ), 3577 filter = tsf.parseFilter( c, txt, data ) || ''; 3578 // eslint-disable-next-line eqeqeq 3579 return data.anyMatch ? $.inArray( filter, data.rowArray ) >= 0 : filter == data.iExact; 3580 } 3581 return null; 3582 }, 3583 // Look for a range ( using ' to ' or ' - ' ) - see issue #166; thanks matzhu! 3584 range : function( c, data ) { 3585 if ( tsfRegex.toTest.test( data.iFilter ) ) { 3586 var result, tmp, range1, range2, 3587 table = c.table, 3588 index = data.index, 3589 parsed = data.parsed[index], 3590 // make sure the dash is for a range and not indicating a negative number 3591 query = data.iFilter.split( tsfRegex.toSplit ); 3592 3593 tmp = query[0].replace( ts.regex.nondigit, '' ) || ''; 3594 range1 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); 3595 tmp = query[1].replace( ts.regex.nondigit, '' ) || ''; 3596 range2 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); 3597 // parse filter value in case we're comparing numbers ( dates ) 3598 if ( parsed || c.parsers[ index ].type === 'numeric' ) { 3599 result = c.parsers[ index ].format( '' + query[0], table, c.$headers.eq( index ), index ); 3600 range1 = ( result !== '' && !isNaN( result ) ) ? result : range1; 3601 result = c.parsers[ index ].format( '' + query[1], table, c.$headers.eq( index ), index ); 3602 range2 = ( result !== '' && !isNaN( result ) ) ? result : range2; 3603 } 3604 if ( ( parsed || c.parsers[ index ].type === 'numeric' ) && !isNaN( range1 ) && !isNaN( range2 ) ) { 3605 result = data.cache; 3606 } else { 3607 tmp = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; 3608 result = ts.formatFloat( tmp, table ); 3609 } 3610 if ( range1 > range2 ) { 3611 tmp = range1; range1 = range2; range2 = tmp; // swap 3612 } 3613 return ( result >= range1 && result <= range2 ) || ( range1 === '' || range2 === '' ); 3614 } 3615 return null; 3616 }, 3617 // Look for wild card: ? = single, * = multiple, or | = logical OR 3618 wild : function( c, data ) { 3619 if ( tsfRegex.wildOrTest.test( data.iFilter ) ) { 3620 var query = '' + ( tsf.parseFilter( c, data.iFilter, data ) || '' ); 3621 // look for an exact match with the 'or' unless the 'filter-match' class is found 3622 if ( !tsfRegex.wildTest.test( query ) && data.nestedFilters ) { 3623 query = data.isMatch ? query : '^(' + query + ')$'; 3624 } 3625 // parsing the filter may not work properly when using wildcards =/ 3626 try { 3627 return new RegExp( 3628 query.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ), 3629 c.widgetOptions.filter_ignoreCase ? 'i' : '' 3630 ) 3631 .test( data.exact ); 3632 } catch ( error ) { 3633 return null; 3634 } 3635 } 3636 return null; 3637 }, 3638 // fuzzy text search; modified from https://github.com/mattyork/fuzzy ( MIT license ) 3639 fuzzy: function( c, data ) { 3640 if ( tsfRegex.fuzzyTest.test( data.iFilter ) ) { 3641 var indx, 3642 patternIndx = 0, 3643 len = data.iExact.length, 3644 txt = data.iFilter.slice( 1 ), 3645 pattern = tsf.parseFilter( c, txt, data ) || ''; 3646 for ( indx = 0; indx < len; indx++ ) { 3647 if ( data.iExact[ indx ] === pattern[ patternIndx ] ) { 3648 patternIndx += 1; 3649 } 3650 } 3651 return patternIndx === pattern.length; 3652 } 3653 return null; 3654 } 3655 }, 3656 init: function( table ) { 3657 // filter language options 3658 ts.language = $.extend( true, {}, { 3659 to : 'to', 3660 or : 'or', 3661 and : 'and' 3662 }, ts.language ); 3663 3664 var options, string, txt, $header, column, val, fxn, noSelect, 3665 c = table.config, 3666 wo = c.widgetOptions, 3667 processStr = function(prefix, str, suffix) { 3668 str = str.trim(); 3669 // don't include prefix/suffix if str is empty 3670 return str === '' ? '' : (prefix || '') + str + (suffix || ''); 3671 }; 3672 c.$table.addClass( 'hasFilters' ); 3673 c.lastSearch = []; 3674 3675 // define timers so using clearTimeout won't cause an undefined error 3676 wo.filter_searchTimer = null; 3677 wo.filter_initTimer = null; 3678 wo.filter_formatterCount = 0; 3679 wo.filter_formatterInit = []; 3680 wo.filter_anyColumnSelector = '[data-column="all"],[data-column="any"]'; 3681 wo.filter_multipleColumnSelector = '[data-column*="-"],[data-column*=","]'; 3682 3683 val = '\\{' + tsfRegex.query + '\\}'; 3684 $.extend( tsfRegex, { 3685 child : new RegExp( c.cssChildRow ), 3686 filtered : new RegExp( wo.filter_filteredRow ), 3687 alreadyFiltered : new RegExp( '(\\s+(-' + processStr('|', ts.language.or) + processStr('|', ts.language.to) + ')\\s+)', 'i' ), 3688 toTest : new RegExp( '\\s+(-' + processStr('|', ts.language.to) + ')\\s+', 'i' ), 3689 toSplit : new RegExp( '(?:\\s+(?:-' + processStr('|', ts.language.to) + ')\\s+)', 'gi' ), 3690 andTest : new RegExp( '\\s+(' + processStr('', ts.language.and, '|') + '&&)\\s+', 'i' ), 3691 andSplit : new RegExp( '(?:\\s+(?:' + processStr('', ts.language.and, '|') + '&&)\\s+)', 'gi' ), 3692 orTest : new RegExp( '(\\|' + processStr('|\\s+', ts.language.or, '\\s+') + ')', 'i' ), 3693 orSplit : new RegExp( '(?:\\|' + processStr('|\\s+(?:', ts.language.or, ')\\s+') + ')', 'gi' ), 3694 iQuery : new RegExp( val, 'i' ), 3695 igQuery : new RegExp( val, 'ig' ), 3696 operTest : /^[<>]=?/, 3697 gtTest : />/, 3698 gteTest : />=/, 3699 ltTest : /</, 3700 lteTest : /<=/, 3701 notTest : /^\!/, 3702 wildOrTest : /[\?\*\|]/, 3703 wildTest : /\?\*/, 3704 fuzzyTest : /^~/, 3705 exactTest : /[=\"\|!]/ 3706 }); 3707 3708 // don't build filter row if columnFilters is false or all columns are set to 'filter-false' 3709 // see issue #156 3710 val = c.$headers.filter( '.filter-false, .parser-false' ).length; 3711 if ( wo.filter_columnFilters !== false && val !== c.$headers.length ) { 3712 // build filter row 3713 tsf.buildRow( table, c, wo ); 3714 } 3715 3716 txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + 3717 'filterAndSortReset filterResetSaved filterEnd search '.split( ' ' ).join( c.namespace + 'filter ' ); 3718 c.$table.bind( txt, function( event, filter ) { 3719 val = wo.filter_hideEmpty && 3720 $.isEmptyObject( c.cache ) && 3721 !( c.delayInit && event.type === 'appendCache' ); 3722 // hide filter row using the 'filtered' class name 3723 c.$table.find( '.' + tscss.filterRow ).toggleClass( wo.filter_filteredRow, val ); // fixes #450 3724 if ( !/(search|filter)/.test( event.type ) ) { 3725 event.stopPropagation(); 3726 tsf.buildDefault( table, true ); 3727 } 3728 // Add filterAndSortReset - see #1361 3729 if ( event.type === 'filterReset' || event.type === 'filterAndSortReset' ) { 3730 c.$table.find( '.' + tscss.filter ).add( wo.filter_$externalFilters ).val( '' ); 3731 if ( event.type === 'filterAndSortReset' ) { 3732 ts.sortReset( this.config, function() { 3733 tsf.searching( table, [] ); 3734 }); 3735 } else { 3736 tsf.searching( table, [] ); 3737 } 3738 } else if ( event.type === 'filterResetSaved' ) { 3739 ts.storage( table, 'tablesorter-filters', '' ); 3740 } else if ( event.type === 'filterEnd' ) { 3741 tsf.buildDefault( table, true ); 3742 } else { 3743 // send false argument to force a new search; otherwise if the filter hasn't changed, 3744 // it will return 3745 filter = event.type === 'search' ? filter : 3746 event.type === 'updateComplete' ? c.$table.data( 'lastSearch' ) : ''; 3747 if ( /(update|add)/.test( event.type ) && event.type !== 'updateComplete' ) { 3748 // force a new search since content has changed 3749 c.lastCombinedFilter = null; 3750 c.lastSearch = []; 3751 // update filterFormatters after update (& small delay) - Fixes #1237 3752 setTimeout(function() { 3753 c.$table.triggerHandler( 'filterFomatterUpdate' ); 3754 }, 100); 3755 } 3756 // pass true ( skipFirst ) to prevent the tablesorter.setFilters function from skipping the first 3757 // input ensures all inputs are updated when a search is triggered on the table 3758 // $( 'table' ).trigger( 'search', [...] ); 3759 tsf.searching( table, filter, true ); 3760 } 3761 return false; 3762 }); 3763 3764 // reset button/link 3765 if ( wo.filter_reset ) { 3766 if ( wo.filter_reset instanceof $ ) { 3767 // reset contains a jQuery object, bind to it 3768 wo.filter_reset.click( function() { 3769 c.$table.triggerHandler( 'filterReset' ); 3770 }); 3771 } else if ( $( wo.filter_reset ).length ) { 3772 // reset is a jQuery selector, use event delegation 3773 $( document ) 3774 .undelegate( wo.filter_reset, 'click' + c.namespace + 'filter' ) 3775 .delegate( wo.filter_reset, 'click' + c.namespace + 'filter', function() { 3776 // trigger a reset event, so other functions ( filter_formatter ) know when to reset 3777 c.$table.triggerHandler( 'filterReset' ); 3778 }); 3779 } 3780 } 3781 if ( wo.filter_functions ) { 3782 for ( column = 0; column < c.columns; column++ ) { 3783 fxn = ts.getColumnData( table, wo.filter_functions, column ); 3784 if ( fxn ) { 3785 // remove 'filter-select' from header otherwise the options added here are replaced with 3786 // all options 3787 $header = c.$headerIndexed[ column ].removeClass( 'filter-select' ); 3788 // don't build select if 'filter-false' or 'parser-false' set 3789 noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) ); 3790 options = ''; 3791 if ( fxn === true && noSelect ) { 3792 tsf.buildSelect( table, column ); 3793 } else if ( typeof fxn === 'object' && noSelect ) { 3794 // add custom drop down list 3795 for ( string in fxn ) { 3796 if ( typeof string === 'string' ) { 3797 options += options === '' ? 3798 '<option value="">' + 3799 ( $header.data( 'placeholder' ) || 3800 $header.attr( 'data-placeholder' ) || 3801 wo.filter_placeholder.select || 3802 '' 3803 ) + 3804 '</option>' : ''; 3805 val = string; 3806 txt = string; 3807 if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { 3808 val = string.split( wo.filter_selectSourceSeparator ); 3809 txt = val[1]; 3810 val = val[0]; 3811 } 3812 options += '<option ' + 3813 ( txt === val ? '' : 'data-function-name="' + string + '" ' ) + 3814 'value="' + val + '">' + txt + '</option>'; 3815 } 3816 } 3817 c.$table 3818 .find( 'thead' ) 3819 .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) 3820 .append( options ); 3821 txt = wo.filter_selectSource; 3822 fxn = typeof txt === 'function' ? true : ts.getColumnData( table, txt, column ); 3823 if ( fxn ) { 3824 // updating so the extra options are appended 3825 tsf.buildSelect( c.table, column, '', true, $header.hasClass( wo.filter_onlyAvail ) ); 3826 } 3827 } 3828 } 3829 } 3830 } 3831 // not really updating, but if the column has both the 'filter-select' class & 3832 // filter_functions set to true, it would append the same options twice. 3833 tsf.buildDefault( table, true ); 3834 3835 tsf.bindSearch( table, c.$table.find( '.' + tscss.filter ), true ); 3836 if ( wo.filter_external ) { 3837 tsf.bindSearch( table, wo.filter_external ); 3838 } 3839 3840 if ( wo.filter_hideFilters ) { 3841 tsf.hideFilters( c ); 3842 } 3843 3844 // show processing icon 3845 if ( c.showProcessing ) { 3846 txt = 'filterStart filterEnd '.split( ' ' ).join( c.namespace + 'filter-sp ' ); 3847 c.$table 3848 .unbind( txt.replace( ts.regex.spaces, ' ' ) ) 3849 .bind( txt, function( event, columns ) { 3850 // only add processing to certain columns to all columns 3851 $header = ( columns ) ? 3852 c.$table 3853 .find( '.' + tscss.header ) 3854 .filter( '[data-column]' ) 3855 .filter( function() { 3856 return columns[ $( this ).data( 'column' ) ] !== ''; 3857 }) : ''; 3858 ts.isProcessing( table, event.type === 'filterStart', columns ? $header : '' ); 3859 }); 3860 } 3861 3862 // set filtered rows count ( intially unfiltered ) 3863 c.filteredRows = c.totalRows; 3864 3865 // add default values 3866 txt = 'tablesorter-initialized pagerBeforeInitialized '.split( ' ' ).join( c.namespace + 'filter ' ); 3867 c.$table 3868 .unbind( txt.replace( ts.regex.spaces, ' ' ) ) 3869 .bind( txt, function() { 3870 tsf.completeInit( this ); 3871 }); 3872 // if filter widget is added after pager has initialized; then set filter init flag 3873 if ( c.pager && c.pager.initialized && !wo.filter_initialized ) { 3874 c.$table.triggerHandler( 'filterFomatterUpdate' ); 3875 setTimeout( function() { 3876 tsf.filterInitComplete( c ); 3877 }, 100 ); 3878 } else if ( !wo.filter_initialized ) { 3879 tsf.completeInit( table ); 3880 } 3881 }, 3882 completeInit: function( table ) { 3883 // redefine 'c' & 'wo' so they update properly inside this callback 3884 var c = table.config, 3885 wo = c.widgetOptions, 3886 filters = tsf.setDefaults( table, c, wo ) || []; 3887 if ( filters.length ) { 3888 // prevent delayInit from triggering a cache build if filters are empty 3889 if ( !( c.delayInit && filters.join( '' ) === '' ) ) { 3890 ts.setFilters( table, filters, true ); 3891 } 3892 } 3893 c.$table.triggerHandler( 'filterFomatterUpdate' ); 3894 // trigger init after setTimeout to prevent multiple filterStart/End/Init triggers 3895 setTimeout( function() { 3896 if ( !wo.filter_initialized ) { 3897 tsf.filterInitComplete( c ); 3898 } 3899 }, 100 ); 3900 }, 3901 3902 // $cell parameter, but not the config, is passed to the filter_formatters, 3903 // so we have to work with it instead 3904 formatterUpdated: function( $cell, column ) { 3905 // prevent error if $cell is undefined - see #1056 3906 var $table = $cell && $cell.closest( 'table' ); 3907 var config = $table.length && $table[0].config, 3908 wo = config && config.widgetOptions; 3909 if ( wo && !wo.filter_initialized ) { 3910 // add updates by column since this function 3911 // may be called numerous times before initialization 3912 wo.filter_formatterInit[ column ] = 1; 3913 } 3914 }, 3915 filterInitComplete: function( c ) { 3916 var indx, len, 3917 wo = c.widgetOptions, 3918 count = 0, 3919 completed = function() { 3920 wo.filter_initialized = true; 3921 // update lastSearch - it gets cleared often 3922 c.lastSearch = c.$table.data( 'lastSearch' ); 3923 c.$table.triggerHandler( 'filterInit', c ); 3924 tsf.findRows( c.table, c.lastSearch || [] ); 3925 if (ts.debug(c, 'filter')) { 3926 console.log('Filter >> Widget initialized'); 3927 } 3928 }; 3929 if ( $.isEmptyObject( wo.filter_formatter ) ) { 3930 completed(); 3931 } else { 3932 len = wo.filter_formatterInit.length; 3933 for ( indx = 0; indx < len; indx++ ) { 3934 if ( wo.filter_formatterInit[ indx ] === 1 ) { 3935 count++; 3936 } 3937 } 3938 clearTimeout( wo.filter_initTimer ); 3939 if ( !wo.filter_initialized && count === wo.filter_formatterCount ) { 3940 // filter widget initialized 3941 completed(); 3942 } else if ( !wo.filter_initialized ) { 3943 // fall back in case a filter_formatter doesn't call 3944 // $.tablesorter.filter.formatterUpdated( $cell, column ), and the count is off 3945 wo.filter_initTimer = setTimeout( function() { 3946 completed(); 3947 }, 500 ); 3948 } 3949 } 3950 }, 3951 // encode or decode filters for storage; see #1026 3952 processFilters: function( filters, encode ) { 3953 var indx, 3954 // fixes #1237; previously returning an encoded "filters" value 3955 result = [], 3956 mode = encode ? encodeURIComponent : decodeURIComponent, 3957 len = filters.length; 3958 for ( indx = 0; indx < len; indx++ ) { 3959 if ( filters[ indx ] ) { 3960 result[ indx ] = mode( filters[ indx ] ); 3961 } 3962 } 3963 return result; 3964 }, 3965 setDefaults: function( table, c, wo ) { 3966 var isArray, saved, indx, col, $filters, 3967 // get current ( default ) filters 3968 filters = ts.getFilters( table ) || []; 3969 if ( wo.filter_saveFilters && ts.storage ) { 3970 saved = ts.storage( table, 'tablesorter-filters' ) || []; 3971 isArray = $.isArray( saved ); 3972 // make sure we're not just getting an empty array 3973 if ( !( isArray && saved.join( '' ) === '' || !isArray ) ) { 3974 filters = tsf.processFilters( saved ); 3975 } 3976 } 3977 // if no filters saved, then check default settings 3978 if ( filters.join( '' ) === '' ) { 3979 // allow adding default setting to external filters 3980 $filters = c.$headers.add( wo.filter_$externalFilters ) 3981 .filter( '[' + wo.filter_defaultAttrib + ']' ); 3982 for ( indx = 0; indx <= c.columns; indx++ ) { 3983 // include data-column='all' external filters 3984 col = indx === c.columns ? 'all' : indx; 3985 filters[ indx ] = $filters 3986 .filter( '[data-column="' + col + '"]' ) 3987 .attr( wo.filter_defaultAttrib ) || filters[indx] || ''; 3988 } 3989 } 3990 c.$table.data( 'lastSearch', filters ); 3991 return filters; 3992 }, 3993 parseFilter: function( c, filter, data, parsed ) { 3994 return parsed || data.parsed[ data.index ] ? 3995 c.parsers[ data.index ].format( filter, c.table, [], data.index ) : 3996 filter; 3997 }, 3998 buildRow: function( table, c, wo ) { 3999 var $filter, col, column, $header, makeSelect, disabled, name, ffxn, tmp, 4000 // c.columns defined in computeThIndexes() 4001 cellFilter = wo.filter_cellFilter, 4002 columns = c.columns, 4003 arry = $.isArray( cellFilter ), 4004 buildFilter = '<tr role="search" class="' + tscss.filterRow + ' ' + c.cssIgnoreRow + '">'; 4005 for ( column = 0; column < columns; column++ ) { 4006 if ( c.$headerIndexed[ column ].length ) { 4007 // account for entire column set with colspan. See #1047 4008 tmp = c.$headerIndexed[ column ] && c.$headerIndexed[ column ][0].colSpan || 0; 4009 if ( tmp > 1 ) { 4010 buildFilter += '<td data-column="' + column + '-' + ( column + tmp - 1 ) + '" colspan="' + tmp + '"'; 4011 } else { 4012 buildFilter += '<td data-column="' + column + '"'; 4013 } 4014 if ( arry ) { 4015 buildFilter += ( cellFilter[ column ] ? ' class="' + cellFilter[ column ] + '"' : '' ); 4016 } else { 4017 buildFilter += ( cellFilter !== '' ? ' class="' + cellFilter + '"' : '' ); 4018 } 4019 buildFilter += '></td>'; 4020 } 4021 } 4022 c.$filters = $( buildFilter += '</tr>' ) 4023 .appendTo( c.$table.children( 'thead' ).eq( 0 ) ) 4024 .children( 'td' ); 4025 // build each filter input 4026 for ( column = 0; column < columns; column++ ) { 4027 disabled = false; 4028 // assuming last cell of a column is the main column 4029 $header = c.$headerIndexed[ column ]; 4030 if ( $header && $header.length ) { 4031 // $filter = c.$filters.filter( '[data-column="' + column + '"]' ); 4032 $filter = tsf.getColumnElm( c, c.$filters, column ); 4033 ffxn = ts.getColumnData( table, wo.filter_functions, column ); 4034 makeSelect = ( wo.filter_functions && ffxn && typeof ffxn !== 'function' ) || 4035 $header.hasClass( 'filter-select' ); 4036 // get data from jQuery data, metadata, headers option or header class name 4037 col = ts.getColumnData( table, c.headers, column ); 4038 disabled = ts.getData( $header[0], col, 'filter' ) === 'false' || 4039 ts.getData( $header[0], col, 'parser' ) === 'false'; 4040 4041 if ( makeSelect ) { 4042 buildFilter = $( '<select>' ).appendTo( $filter ); 4043 } else { 4044 ffxn = ts.getColumnData( table, wo.filter_formatter, column ); 4045 if ( ffxn ) { 4046 wo.filter_formatterCount++; 4047 buildFilter = ffxn( $filter, column ); 4048 // no element returned, so lets go find it 4049 if ( buildFilter && buildFilter.length === 0 ) { 4050 buildFilter = $filter.children( 'input' ); 4051 } 4052 // element not in DOM, so lets attach it 4053 if ( buildFilter && ( buildFilter.parent().length === 0 || 4054 ( buildFilter.parent().length && buildFilter.parent()[0] !== $filter[0] ) ) ) { 4055 $filter.append( buildFilter ); 4056 } 4057 } else { 4058 buildFilter = $( '<input type="search">' ).appendTo( $filter ); 4059 } 4060 if ( buildFilter ) { 4061 tmp = $header.data( 'placeholder' ) || 4062 $header.attr( 'data-placeholder' ) || 4063 wo.filter_placeholder.search || ''; 4064 buildFilter.attr( 'placeholder', tmp ); 4065 } 4066 } 4067 if ( buildFilter ) { 4068 // add filter class name 4069 name = ( $.isArray( wo.filter_cssFilter ) ? 4070 ( typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '' ) : 4071 wo.filter_cssFilter ) || ''; 4072 // copy data-column from table cell (it will include colspan) 4073 buildFilter.addClass( tscss.filter + ' ' + name ); 4074 name = wo.filter_filterLabel; 4075 tmp = name.match(/{{([^}]+?)}}/g); 4076 if (!tmp) { 4077 tmp = [ '{{label}}' ]; 4078 } 4079 $.each(tmp, function(indx, attr) { 4080 var regex = new RegExp(attr, 'g'), 4081 data = $header.attr('data-' + attr.replace(/{{|}}/g, '')), 4082 text = typeof data === 'undefined' ? $header.text() : data; 4083 name = name.replace( regex, $.trim( text ) ); 4084 }); 4085 buildFilter.attr({ 4086 'data-column': $filter.attr( 'data-column' ), 4087 'aria-label': name 4088 }); 4089 if ( disabled ) { 4090 buildFilter.attr( 'placeholder', '' ).addClass( tscss.filterDisabled )[0].disabled = true; 4091 } 4092 } 4093 } 4094 } 4095 }, 4096 bindSearch: function( table, $el, internal ) { 4097 table = $( table )[0]; 4098 $el = $( $el ); // allow passing a selector string 4099 if ( !$el.length ) { return; } 4100 var tmp, 4101 c = table.config, 4102 wo = c.widgetOptions, 4103 namespace = c.namespace + 'filter', 4104 $ext = wo.filter_$externalFilters; 4105 if ( internal !== true ) { 4106 // save anyMatch element 4107 tmp = wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector; 4108 wo.filter_$anyMatch = $el.filter( tmp ); 4109 if ( $ext && $ext.length ) { 4110 wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el ); 4111 } else { 4112 wo.filter_$externalFilters = $el; 4113 } 4114 // update values ( external filters added after table initialization ) 4115 ts.setFilters( table, c.$table.data( 'lastSearch' ) || [], internal === false ); 4116 } 4117 // unbind events 4118 tmp = ( 'keypress keyup keydown search change input '.split( ' ' ).join( namespace + ' ' ) ); 4119 $el 4120 // use data attribute instead of jQuery data since the head is cloned without including 4121 // the data/binding 4122 .attr( 'data-lastSearchTime', new Date().getTime() ) 4123 .unbind( tmp.replace( ts.regex.spaces, ' ' ) ) 4124 .bind( 'keydown' + namespace, function( event ) { 4125 if ( event.which === tskeyCodes.escape && !table.config.widgetOptions.filter_resetOnEsc ) { 4126 // prevent keypress event 4127 return false; 4128 } 4129 }) 4130 .bind( 'keyup' + namespace, function( event ) { 4131 wo = table.config.widgetOptions; // make sure "wo" isn't cached 4132 var column = parseInt( $( this ).attr( 'data-column' ), 10 ), 4133 liveSearch = typeof wo.filter_liveSearch === 'boolean' ? wo.filter_liveSearch : 4134 ts.getColumnData( table, wo.filter_liveSearch, column ); 4135 if ( typeof liveSearch === 'undefined' ) { 4136 liveSearch = wo.filter_liveSearch.fallback || false; 4137 } 4138 $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); 4139 // emulate what webkit does.... escape clears the filter 4140 if ( event.which === tskeyCodes.escape ) { 4141 // make sure to restore the last value on escape 4142 this.value = wo.filter_resetOnEsc ? '' : c.lastSearch[column]; 4143 // don't return if the search value is empty ( all rows need to be revealed ) 4144 } else if ( this.value !== '' && ( 4145 // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace 4146 ( typeof liveSearch === 'number' && this.value.length < liveSearch ) || 4147 // let return & backspace continue on, but ignore arrows & non-valid characters 4148 ( event.which !== tskeyCodes.enter && event.which !== tskeyCodes.backSpace && 4149 ( event.which < tskeyCodes.space || ( event.which >= tskeyCodes.left && event.which <= tskeyCodes.down ) ) ) ) ) { 4150 return; 4151 // live search 4152 } else if ( liveSearch === false ) { 4153 if ( this.value !== '' && event.which !== tskeyCodes.enter ) { 4154 return; 4155 } 4156 } 4157 // change event = no delay; last true flag tells getFilters to skip newest timed input 4158 tsf.searching( table, true, true, column ); 4159 }) 4160 // include change for select - fixes #473 4161 .bind( 'search change keypress input blur '.split( ' ' ).join( namespace + ' ' ), function( event ) { 4162 // don't get cached data, in case data-column changes dynamically 4163 var column = parseInt( $( this ).attr( 'data-column' ), 10 ), 4164 eventType = event.type, 4165 liveSearch = typeof wo.filter_liveSearch === 'boolean' ? 4166 wo.filter_liveSearch : 4167 ts.getColumnData( table, wo.filter_liveSearch, column ); 4168 if ( table.config.widgetOptions.filter_initialized && 4169 // immediate search if user presses enter 4170 ( event.which === tskeyCodes.enter || 4171 // immediate search if a "search" or "blur" is triggered on the input 4172 ( eventType === 'search' || eventType === 'blur' ) || 4173 // change & input events must be ignored if liveSearch !== true 4174 ( eventType === 'change' || eventType === 'input' ) && 4175 // prevent search if liveSearch is a number 4176 ( liveSearch === true || liveSearch !== true && event.target.nodeName !== 'INPUT' ) && 4177 // don't allow 'change' or 'input' event to process if the input value 4178 // is the same - fixes #685 4179 this.value !== c.lastSearch[column] 4180 ) 4181 ) { 4182 event.preventDefault(); 4183 // init search with no delay 4184 $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); 4185 tsf.searching( table, eventType !== 'keypress', true, column ); 4186 } 4187 }); 4188 }, 4189 searching: function( table, filter, skipFirst, column ) { 4190 var liveSearch, 4191 wo = table.config.widgetOptions; 4192 if (typeof column === 'undefined') { 4193 // no delay 4194 liveSearch = false; 4195 } else { 4196 liveSearch = typeof wo.filter_liveSearch === 'boolean' ? 4197 wo.filter_liveSearch : 4198 // get column setting, or set to fallback value, or default to false 4199 ts.getColumnData( table, wo.filter_liveSearch, column ); 4200 if ( typeof liveSearch === 'undefined' ) { 4201 liveSearch = wo.filter_liveSearch.fallback || false; 4202 } 4203 } 4204 clearTimeout( wo.filter_searchTimer ); 4205 if ( typeof filter === 'undefined' || filter === true ) { 4206 // delay filtering 4207 wo.filter_searchTimer = setTimeout( function() { 4208 tsf.checkFilters( table, filter, skipFirst ); 4209 }, liveSearch ? wo.filter_searchDelay : 10 ); 4210 } else { 4211 // skip delay 4212 tsf.checkFilters( table, filter, skipFirst ); 4213 } 4214 }, 4215 equalFilters: function (c, filter1, filter2) { 4216 var indx, 4217 f1 = [], 4218 f2 = [], 4219 len = c.columns + 1; // add one to include anyMatch filter 4220 filter1 = $.isArray(filter1) ? filter1 : []; 4221 filter2 = $.isArray(filter2) ? filter2 : []; 4222 for (indx = 0; indx < len; indx++) { 4223 f1[indx] = filter1[indx] || ''; 4224 f2[indx] = filter2[indx] || ''; 4225 } 4226 return f1.join(',') === f2.join(','); 4227 }, 4228 checkFilters: function( table, filter, skipFirst ) { 4229 var c = table.config, 4230 wo = c.widgetOptions, 4231 filterArray = $.isArray( filter ), 4232 filters = ( filterArray ) ? filter : ts.getFilters( table, true ), 4233 currentFilters = filters || []; // current filter values 4234 // prevent errors if delay init is set 4235 if ( $.isEmptyObject( c.cache ) ) { 4236 // update cache if delayInit set & pager has initialized ( after user initiates a search ) 4237 if ( c.delayInit && ( !c.pager || c.pager && c.pager.initialized ) ) { 4238 ts.updateCache( c, function() { 4239 tsf.checkFilters( table, false, skipFirst ); 4240 }); 4241 } 4242 return; 4243 } 4244 // add filter array back into inputs 4245 if ( filterArray ) { 4246 ts.setFilters( table, filters, false, skipFirst !== true ); 4247 if ( !wo.filter_initialized ) { 4248 c.lastSearch = []; 4249 c.lastCombinedFilter = ''; 4250 } 4251 } 4252 if ( wo.filter_hideFilters ) { 4253 // show/hide filter row as needed 4254 c.$table 4255 .find( '.' + tscss.filterRow ) 4256 .triggerHandler( tsf.hideFiltersCheck( c ) ? 'mouseleave' : 'mouseenter' ); 4257 } 4258 // return if the last search is the same; but filter === false when updating the search 4259 // see example-widget-filter.html filter toggle buttons 4260 if ( tsf.equalFilters(c, c.lastSearch, currentFilters) && filter !== false ) { 4261 return; 4262 } else if ( filter === false ) { 4263 // force filter refresh 4264 c.lastCombinedFilter = ''; 4265 c.lastSearch = []; 4266 } 4267 // define filter inside it is false 4268 filters = filters || []; 4269 // convert filters to strings - see #1070 4270 filters = Array.prototype.map ? 4271 filters.map( String ) : 4272 // for IE8 & older browsers - maybe not the best method 4273 filters.join( '\ufffd' ).split( '\ufffd' ); 4274 4275 if ( wo.filter_initialized ) { 4276 c.$table.triggerHandler( 'filterStart', [ filters ] ); 4277 } 4278 if ( c.showProcessing ) { 4279 // give it time for the processing icon to kick in 4280 setTimeout( function() { 4281 tsf.findRows( table, filters, currentFilters ); 4282 return false; 4283 }, 30 ); 4284 } else { 4285 tsf.findRows( table, filters, currentFilters ); 4286 return false; 4287 } 4288 }, 4289 hideFiltersCheck: function( c ) { 4290 if (typeof c.widgetOptions.filter_hideFilters === 'function') { 4291 var val = c.widgetOptions.filter_hideFilters( c ); 4292 if (typeof val === 'boolean') { 4293 return val; 4294 } 4295 } 4296 return ts.getFilters( c.$table ).join( '' ) === ''; 4297 }, 4298 hideFilters: function( c, $table ) { 4299 var timer; 4300 ( $table || c.$table ) 4301 .find( '.' + tscss.filterRow ) 4302 .addClass( tscss.filterRowHide ) 4303 .bind( 'mouseenter mouseleave', function( e ) { 4304 // save event object - http://bugs.jquery.com/ticket/12140 4305 var event = e, 4306 $row = $( this ); 4307 clearTimeout( timer ); 4308 timer = setTimeout( function() { 4309 if ( /enter|over/.test( event.type ) ) { 4310 $row.removeClass( tscss.filterRowHide ); 4311 } else { 4312 // don't hide if input has focus 4313 // $( ':focus' ) needs jQuery 1.6+ 4314 if ( $( document.activeElement ).closest( 'tr' )[0] !== $row[0] ) { 4315 // don't hide row if any filter has a value 4316 $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) ); 4317 } 4318 } 4319 }, 200 ); 4320 }) 4321 .find( 'input, select' ).bind( 'focus blur', function( e ) { 4322 var event = e, 4323 $row = $( this ).closest( 'tr' ); 4324 clearTimeout( timer ); 4325 timer = setTimeout( function() { 4326 clearTimeout( timer ); 4327 // don't hide row if any filter has a value 4328 $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) && event.type !== 'focus' ); 4329 }, 200 ); 4330 }); 4331 }, 4332 defaultFilter: function( filter, mask ) { 4333 if ( filter === '' ) { return filter; } 4334 var regex = tsfRegex.iQuery, 4335 maskLen = mask.match( tsfRegex.igQuery ).length, 4336 query = maskLen > 1 ? $.trim( filter ).split( /\s/ ) : [ $.trim( filter ) ], 4337 len = query.length - 1, 4338 indx = 0, 4339 val = mask; 4340 if ( len < 1 && maskLen > 1 ) { 4341 // only one 'word' in query but mask has >1 slots 4342 query[1] = query[0]; 4343 } 4344 // replace all {query} with query words... 4345 // if query = 'Bob', then convert mask from '!{query}' to '!Bob' 4346 // if query = 'Bob Joe Frank', then convert mask '{q} OR {q}' to 'Bob OR Joe OR Frank' 4347 while ( regex.test( val ) ) { 4348 val = val.replace( regex, query[indx++] || '' ); 4349 if ( regex.test( val ) && indx < len && ( query[indx] || '' ) !== '' ) { 4350 val = mask.replace( regex, val ); 4351 } 4352 } 4353 return val; 4354 }, 4355 getLatestSearch: function( $input ) { 4356 if ( $input ) { 4357 return $input.sort( function( a, b ) { 4358 return $( b ).attr( 'data-lastSearchTime' ) - $( a ).attr( 'data-lastSearchTime' ); 4359 }); 4360 } 4361 return $input || $(); 4362 }, 4363 findRange: function( c, val, ignoreRanges ) { 4364 // look for multiple columns '1-3,4-6,8' in data-column 4365 var temp, ranges, range, start, end, singles, i, indx, len, 4366 columns = []; 4367 if ( /^[0-9]+$/.test( val ) ) { 4368 // always return an array 4369 return [ parseInt( val, 10 ) ]; 4370 } 4371 // process column range 4372 if ( !ignoreRanges && /-/.test( val ) ) { 4373 ranges = val.match( /(\d+)\s*-\s*(\d+)/g ); 4374 len = ranges ? ranges.length : 0; 4375 for ( indx = 0; indx < len; indx++ ) { 4376 range = ranges[indx].split( /\s*-\s*/ ); 4377 start = parseInt( range[0], 10 ) || 0; 4378 end = parseInt( range[1], 10 ) || ( c.columns - 1 ); 4379 if ( start > end ) { 4380 temp = start; start = end; end = temp; // swap 4381 } 4382 if ( end >= c.columns ) { 4383 end = c.columns - 1; 4384 } 4385 for ( ; start <= end; start++ ) { 4386 columns[ columns.length ] = start; 4387 } 4388 // remove processed range from val 4389 val = val.replace( ranges[ indx ], '' ); 4390 } 4391 } 4392 // process single columns 4393 if ( !ignoreRanges && /,/.test( val ) ) { 4394 singles = val.split( /\s*,\s*/ ); 4395 len = singles.length; 4396 for ( i = 0; i < len; i++ ) { 4397 if ( singles[ i ] !== '' ) { 4398 indx = parseInt( singles[ i ], 10 ); 4399 if ( indx < c.columns ) { 4400 columns[ columns.length ] = indx; 4401 } 4402 } 4403 } 4404 } 4405 // return all columns 4406 if ( !columns.length ) { 4407 for ( indx = 0; indx < c.columns; indx++ ) { 4408 columns[ columns.length ] = indx; 4409 } 4410 } 4411 return columns; 4412 }, 4413 getColumnElm: function( c, $elements, column ) { 4414 // data-column may contain multiple columns '1-3,5-6,8' 4415 // replaces: c.$filters.filter( '[data-column="' + column + '"]' ); 4416 return $elements.filter( function() { 4417 var cols = tsf.findRange( c, $( this ).attr( 'data-column' ) ); 4418 return $.inArray( column, cols ) > -1; 4419 }); 4420 }, 4421 multipleColumns: function( c, $input ) { 4422 // look for multiple columns '1-3,4-6,8' in data-column 4423 var wo = c.widgetOptions, 4424 // only target 'all' column inputs on initialization 4425 // & don't target 'all' column inputs if they don't exist 4426 targets = wo.filter_initialized || !$input.filter( wo.filter_anyColumnSelector ).length, 4427 val = $.trim( tsf.getLatestSearch( $input ).attr( 'data-column' ) || '' ); 4428 return tsf.findRange( c, val, !targets ); 4429 }, 4430 processTypes: function( c, data, vars ) { 4431 var ffxn, 4432 filterMatched = null, 4433 matches = null; 4434 for ( ffxn in tsf.types ) { 4435 if ( $.inArray( ffxn, vars.excludeMatch ) < 0 && matches === null ) { 4436 matches = tsf.types[ffxn]( c, data, vars ); 4437 if ( matches !== null ) { 4438 data.matchedOn = ffxn; 4439 filterMatched = matches; 4440 } 4441 } 4442 } 4443 return filterMatched; 4444 }, 4445 matchType: function( c, columnIndex ) { 4446 var isMatch, 4447 wo = c.widgetOptions, 4448 $el = c.$headerIndexed[ columnIndex ]; 4449 // filter-exact > filter-match > filter_matchType for type 4450 if ( $el.hasClass( 'filter-exact' ) ) { 4451 isMatch = false; 4452 } else if ( $el.hasClass( 'filter-match' ) ) { 4453 isMatch = true; 4454 } else { 4455 // filter-select is not applied when filter_functions are used, so look for a select 4456 if ( wo.filter_columnFilters ) { 4457 $el = c.$filters 4458 .find( '.' + tscss.filter ) 4459 .add( wo.filter_$externalFilters ) 4460 .filter( '[data-column="' + columnIndex + '"]' ); 4461 } else if ( wo.filter_$externalFilters ) { 4462 $el = wo.filter_$externalFilters.filter( '[data-column="' + columnIndex + '"]' ); 4463 } 4464 isMatch = $el.length ? 4465 c.widgetOptions.filter_matchType[ ( $el[ 0 ].nodeName || '' ).toLowerCase() ] === 'match' : 4466 // default to exact, if no inputs found 4467 false; 4468 } 4469 return isMatch; 4470 }, 4471 processRow: function( c, data, vars ) { 4472 var result, filterMatched, 4473 fxn, ffxn, txt, 4474 wo = c.widgetOptions, 4475 showRow = true, 4476 hasAnyMatchInput = wo.filter_$anyMatch && wo.filter_$anyMatch.length, 4477 4478 // if wo.filter_$anyMatch data-column attribute is changed dynamically 4479 // we don't want to do an "anyMatch" search on one column using data 4480 // for the entire row - see #998 4481 columnIndex = wo.filter_$anyMatch && wo.filter_$anyMatch.length ? 4482 // look for multiple columns '1-3,4-6,8' 4483 tsf.multipleColumns( c, wo.filter_$anyMatch ) : 4484 []; 4485 data.$cells = data.$row.children(); 4486 data.matchedOn = null; 4487 if ( data.anyMatchFlag && columnIndex.length > 1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { 4488 data.anyMatch = true; 4489 data.isMatch = true; 4490 data.rowArray = data.$cells.map( function( i ) { 4491 if ( $.inArray( i, columnIndex ) > -1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { 4492 if ( data.parsed[ i ] ) { 4493 txt = data.cacheArray[ i ]; 4494 } else { 4495 txt = data.rawArray[ i ]; 4496 txt = $.trim( wo.filter_ignoreCase ? txt.toLowerCase() : txt ); 4497 if ( c.sortLocaleCompare ) { 4498 txt = ts.replaceAccents( txt ); 4499 } 4500 } 4501 return txt; 4502 } 4503 }).get(); 4504 data.filter = data.anyMatchFilter; 4505 data.iFilter = data.iAnyMatchFilter; 4506 data.exact = data.rowArray.join( ' ' ); 4507 data.iExact = wo.filter_ignoreCase ? data.exact.toLowerCase() : data.exact; 4508 data.cache = data.cacheArray.slice( 0, -1 ).join( ' ' ); 4509 vars.excludeMatch = vars.noAnyMatch; 4510 filterMatched = tsf.processTypes( c, data, vars ); 4511 if ( filterMatched !== null ) { 4512 showRow = filterMatched; 4513 } else { 4514 if ( wo.filter_startsWith ) { 4515 showRow = false; 4516 // data.rowArray may not contain all columns 4517 columnIndex = Math.min( c.columns, data.rowArray.length ); 4518 while ( !showRow && columnIndex > 0 ) { 4519 columnIndex--; 4520 showRow = showRow || data.rowArray[ columnIndex ].indexOf( data.iFilter ) === 0; 4521 } 4522 } else { 4523 showRow = ( data.iExact + data.childRowText ).indexOf( data.iFilter ) >= 0; 4524 } 4525 } 4526 data.anyMatch = false; 4527 // no other filters to process 4528 if ( data.filters.join( '' ) === data.filter ) { 4529 return showRow; 4530 } 4531 } 4532 4533 for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { 4534 data.filter = data.filters[ columnIndex ]; 4535 data.index = columnIndex; 4536 4537 // filter types to exclude, per column 4538 vars.excludeMatch = vars.excludeFilter[ columnIndex ]; 4539 4540 // ignore if filter is empty or disabled 4541 if ( data.filter ) { 4542 data.cache = data.cacheArray[ columnIndex ]; 4543 result = data.parsed[ columnIndex ] ? data.cache : data.rawArray[ columnIndex ] || ''; 4544 data.exact = c.sortLocaleCompare ? ts.replaceAccents( result ) : result; // issue #405 4545 data.iExact = !tsfRegex.type.test( typeof data.exact ) && wo.filter_ignoreCase ? 4546 data.exact.toLowerCase() : data.exact; 4547 data.isMatch = tsf.matchType( c, columnIndex ); 4548 4549 result = showRow; // if showRow is true, show that row 4550 4551 // in case select filter option has a different value vs text 'a - z|A through Z' 4552 ffxn = wo.filter_columnFilters ? 4553 c.$filters.add( wo.filter_$externalFilters ) 4554 .filter( '[data-column="' + columnIndex + '"]' ) 4555 .find( 'select option:selected' ) 4556 .attr( 'data-function-name' ) || '' : ''; 4557 // replace accents - see #357 4558 if ( c.sortLocaleCompare ) { 4559 data.filter = ts.replaceAccents( data.filter ); 4560 } 4561 4562 // replace column specific default filters - see #1088 4563 if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultColFilter[ columnIndex ] ) ) { 4564 data.filter = tsf.defaultFilter( data.filter, vars.defaultColFilter[ columnIndex ] ); 4565 } 4566 4567 // data.iFilter = case insensitive ( if wo.filter_ignoreCase is true ), 4568 // data.filter = case sensitive 4569 data.iFilter = wo.filter_ignoreCase ? ( data.filter || '' ).toLowerCase() : data.filter; 4570 fxn = vars.functions[ columnIndex ]; 4571 filterMatched = null; 4572 if ( fxn ) { 4573 if ( typeof fxn === 'function' ) { 4574 // filter callback( exact cell content, parser normalized content, 4575 // filter input value, column index, jQuery row object ) 4576 filterMatched = fxn( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); 4577 } else if ( typeof fxn[ ffxn || data.filter ] === 'function' ) { 4578 // selector option function 4579 txt = ffxn || data.filter; 4580 filterMatched = 4581 fxn[ txt ]( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); 4582 } 4583 } 4584 if ( filterMatched === null ) { 4585 // cycle through the different filters 4586 // filters return a boolean or null if nothing matches 4587 filterMatched = tsf.processTypes( c, data, vars ); 4588 // select with exact match; ignore "and" or "or" within the text; fixes #1486 4589 txt = fxn === true && (data.matchedOn === 'and' || data.matchedOn === 'or'); 4590 if ( filterMatched !== null && !txt) { 4591 result = filterMatched; 4592 // Look for match, and add child row data for matching 4593 } else { 4594 // check fxn (filter-select in header) after filter types are checked 4595 // without this, the filter + jQuery UI selectmenu demo was breaking 4596 if ( fxn === true ) { 4597 // default selector uses exact match unless 'filter-match' class is found 4598 result = data.isMatch ? 4599 // data.iExact may be a number 4600 ( '' + data.iExact ).search( data.iFilter ) >= 0 : 4601 data.filter === data.exact; 4602 } else { 4603 txt = ( data.iExact + data.childRowText ).indexOf( tsf.parseFilter( c, data.iFilter, data ) ); 4604 result = ( ( !wo.filter_startsWith && txt >= 0 ) || ( wo.filter_startsWith && txt === 0 ) ); 4605 } 4606 } 4607 } else { 4608 result = filterMatched; 4609 } 4610 showRow = ( result ) ? showRow : false; 4611 } 4612 } 4613 return showRow; 4614 }, 4615 findRows: function( table, filters, currentFilters ) { 4616 if ( 4617 tsf.equalFilters(table.config, table.config.lastSearch, currentFilters) || 4618 !table.config.widgetOptions.filter_initialized 4619 ) { 4620 return; 4621 } 4622 var len, norm_rows, rowData, $rows, $row, rowIndex, tbodyIndex, $tbody, columnIndex, 4623 isChild, childRow, lastSearch, showRow, showParent, time, val, indx, 4624 notFiltered, searchFiltered, query, injected, res, id, txt, 4625 storedFilters = $.extend( [], filters ), 4626 c = table.config, 4627 wo = c.widgetOptions, 4628 debug = ts.debug(c, 'filter'), 4629 // data object passed to filters; anyMatch is a flag for the filters 4630 data = { 4631 anyMatch: false, 4632 filters: filters, 4633 // regex filter type cache 4634 filter_regexCache : [] 4635 }, 4636 vars = { 4637 // anyMatch really screws up with these types of filters 4638 noAnyMatch: [ 'range', 'operators' ], 4639 // cache filter variables that use ts.getColumnData in the main loop 4640 functions : [], 4641 excludeFilter : [], 4642 defaultColFilter : [], 4643 defaultAnyFilter : ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || '' 4644 }; 4645 // parse columns after formatter, in case the class is added at that point 4646 data.parsed = []; 4647 for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { 4648 data.parsed[ columnIndex ] = wo.filter_useParsedData || 4649 // parser has a "parsed" parameter 4650 ( c.parsers && c.parsers[ columnIndex ] && c.parsers[ columnIndex ].parsed || 4651 // getData may not return 'parsed' if other 'filter-' class names exist 4652 // ( e.g. <th class="filter-select filter-parsed"> ) 4653 ts.getData && ts.getData( c.$headerIndexed[ columnIndex ], 4654 ts.getColumnData( table, c.headers, columnIndex ), 'filter' ) === 'parsed' || 4655 c.$headerIndexed[ columnIndex ].hasClass( 'filter-parsed' ) ); 4656 4657 vars.functions[ columnIndex ] = 4658 ts.getColumnData( table, wo.filter_functions, columnIndex ) || 4659 c.$headerIndexed[ columnIndex ].hasClass( 'filter-select' ); 4660 vars.defaultColFilter[ columnIndex ] = 4661 ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || ''; 4662 vars.excludeFilter[ columnIndex ] = 4663 ( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split( /\s+/ ); 4664 } 4665 4666 if ( debug ) { 4667 console.log( 'Filter >> Starting filter widget search', filters ); 4668 time = new Date(); 4669 } 4670 // filtered rows count 4671 c.filteredRows = 0; 4672 c.totalRows = 0; 4673 currentFilters = ( storedFilters || [] ); 4674 4675 for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { 4676 $tbody = ts.processTbody( table, c.$tbodies.eq( tbodyIndex ), true ); 4677 // skip child rows & widget added ( removable ) rows - fixes #448 thanks to @hempel! 4678 // $rows = $tbody.children( 'tr' ).not( c.selectorRemove ); 4679 columnIndex = c.columns; 4680 // convert stored rows into a jQuery object 4681 norm_rows = c.cache[ tbodyIndex ].normalized; 4682 $rows = $( $.map( norm_rows, function( el ) { 4683 return el[ columnIndex ].$row.get(); 4684 }) ); 4685 4686 if ( currentFilters.join('') === '' || wo.filter_serversideFiltering ) { 4687 $rows 4688 .removeClass( wo.filter_filteredRow ) 4689 .not( '.' + c.cssChildRow ) 4690 .css( 'display', '' ); 4691 } else { 4692 // filter out child rows 4693 $rows = $rows.not( '.' + c.cssChildRow ); 4694 len = $rows.length; 4695 4696 if ( ( wo.filter_$anyMatch && wo.filter_$anyMatch.length ) || 4697 typeof filters[c.columns] !== 'undefined' ) { 4698 data.anyMatchFlag = true; 4699 data.anyMatchFilter = '' + ( 4700 filters[ c.columns ] || 4701 wo.filter_$anyMatch && tsf.getLatestSearch( wo.filter_$anyMatch ).val() || 4702 '' 4703 ); 4704 if ( wo.filter_columnAnyMatch ) { 4705 // specific columns search 4706 query = data.anyMatchFilter.split( tsfRegex.andSplit ); 4707 injected = false; 4708 for ( indx = 0; indx < query.length; indx++ ) { 4709 res = query[ indx ].split( ':' ); 4710 if ( res.length > 1 ) { 4711 // make the column a one-based index ( non-developers start counting from one :P ) 4712 if ( isNaN( res[0] ) ) { 4713 $.each( c.headerContent, function( i, txt ) { 4714 // multiple matches are possible 4715 if ( txt.toLowerCase().indexOf( res[0] ) > -1 ) { 4716 id = i; 4717 filters[ id ] = res[1]; 4718 } 4719 }); 4720 } else { 4721 id = parseInt( res[0], 10 ) - 1; 4722 } 4723 if ( id >= 0 && id < c.columns ) { // if id is an integer 4724 filters[ id ] = res[1]; 4725 query.splice( indx, 1 ); 4726 indx--; 4727 injected = true; 4728 } 4729 } 4730 } 4731 if ( injected ) { 4732 data.anyMatchFilter = query.join( ' && ' ); 4733 } 4734 } 4735 } 4736 4737 // optimize searching only through already filtered rows - see #313 4738 searchFiltered = wo.filter_searchFiltered; 4739 lastSearch = c.lastSearch || c.$table.data( 'lastSearch' ) || []; 4740 if ( searchFiltered ) { 4741 // cycle through all filters; include last ( columnIndex + 1 = match any column ). Fixes #669 4742 for ( indx = 0; indx < columnIndex + 1; indx++ ) { 4743 val = filters[indx] || ''; 4744 // break out of loop if we've already determined not to search filtered rows 4745 if ( !searchFiltered ) { indx = columnIndex; } 4746 // search already filtered rows if... 4747 searchFiltered = searchFiltered && lastSearch.length && 4748 // there are no changes from beginning of filter 4749 val.indexOf( lastSearch[indx] || '' ) === 0 && 4750 // if there is NOT a logical 'or', or range ( 'to' or '-' ) in the string 4751 !tsfRegex.alreadyFiltered.test( val ) && 4752 // if we are not doing exact matches, using '|' ( logical or ) or not '!' 4753 !tsfRegex.exactTest.test( val ) && 4754 // don't search only filtered if the value is negative 4755 // ( '> -10' => '> -100' will ignore hidden rows ) 4756 !( tsfRegex.isNeg1.test( val ) || tsfRegex.isNeg2.test( val ) ) && 4757 // if filtering using a select without a 'filter-match' class ( exact match ) - fixes #593 4758 !( val !== '' && c.$filters && c.$filters.filter( '[data-column="' + indx + '"]' ).find( 'select' ).length && 4759 !tsf.matchType( c, indx ) ); 4760 } 4761 } 4762 notFiltered = $rows.not( '.' + wo.filter_filteredRow ).length; 4763 // can't search when all rows are hidden - this happens when looking for exact matches 4764 if ( searchFiltered && notFiltered === 0 ) { searchFiltered = false; } 4765 if ( debug ) { 4766 console.log( 'Filter >> Searching through ' + 4767 ( searchFiltered && notFiltered < len ? notFiltered : 'all' ) + ' rows' ); 4768 } 4769 if ( data.anyMatchFlag ) { 4770 if ( c.sortLocaleCompare ) { 4771 // replace accents 4772 data.anyMatchFilter = ts.replaceAccents( data.anyMatchFilter ); 4773 } 4774 if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultAnyFilter ) ) { 4775 data.anyMatchFilter = tsf.defaultFilter( data.anyMatchFilter, vars.defaultAnyFilter ); 4776 // clear search filtered flag because default filters are not saved to the last search 4777 searchFiltered = false; 4778 } 4779 // make iAnyMatchFilter lowercase unless both filter widget & core ignoreCase options are true 4780 // when c.ignoreCase is true, the cache contains all lower case data 4781 data.iAnyMatchFilter = !( wo.filter_ignoreCase && c.ignoreCase ) ? 4782 data.anyMatchFilter : 4783 data.anyMatchFilter.toLowerCase(); 4784 } 4785 4786 // loop through the rows 4787 for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { 4788 4789 txt = $rows[ rowIndex ].className; 4790 // the first row can never be a child row 4791 isChild = rowIndex && tsfRegex.child.test( txt ); 4792 // skip child rows & already filtered rows 4793 if ( isChild || ( searchFiltered && tsfRegex.filtered.test( txt ) ) ) { 4794 continue; 4795 } 4796 4797 data.$row = $rows.eq( rowIndex ); 4798 data.rowIndex = rowIndex; 4799 data.cacheArray = norm_rows[ rowIndex ]; 4800 rowData = data.cacheArray[ c.columns ]; 4801 data.rawArray = rowData.raw; 4802 data.childRowText = ''; 4803 4804 if ( !wo.filter_childByColumn ) { 4805 txt = ''; 4806 // child row cached text 4807 childRow = rowData.child; 4808 // so, if 'table.config.widgetOptions.filter_childRows' is true and there is 4809 // a match anywhere in the child row, then it will make the row visible 4810 // checked here so the option can be changed dynamically 4811 for ( indx = 0; indx < childRow.length; indx++ ) { 4812 txt += ' ' + childRow[indx].join( ' ' ) || ''; 4813 } 4814 data.childRowText = wo.filter_childRows ? 4815 ( wo.filter_ignoreCase ? txt.toLowerCase() : txt ) : 4816 ''; 4817 } 4818 4819 showRow = false; 4820 showParent = tsf.processRow( c, data, vars ); 4821 $row = rowData.$row; 4822 4823 // don't pass reference to val 4824 val = showParent ? true : false; 4825 childRow = rowData.$row.filter( ':gt(0)' ); 4826 if ( wo.filter_childRows && childRow.length ) { 4827 if ( wo.filter_childByColumn ) { 4828 if ( !wo.filter_childWithSibs ) { 4829 // hide all child rows 4830 childRow.addClass( wo.filter_filteredRow ); 4831 // if only showing resulting child row, only include parent 4832 $row = $row.eq( 0 ); 4833 } 4834 // cycle through each child row 4835 for ( indx = 0; indx < childRow.length; indx++ ) { 4836 data.$row = childRow.eq( indx ); 4837 data.cacheArray = rowData.child[ indx ]; 4838 data.rawArray = data.cacheArray; 4839 val = tsf.processRow( c, data, vars ); 4840 // use OR comparison on child rows 4841 showRow = showRow || val; 4842 if ( !wo.filter_childWithSibs && val ) { 4843 childRow.eq( indx ).removeClass( wo.filter_filteredRow ); 4844 } 4845 } 4846 } 4847 // keep parent row match even if no child matches... see #1020 4848 showRow = showRow || showParent; 4849 } else { 4850 showRow = val; 4851 } 4852 $row 4853 .toggleClass( wo.filter_filteredRow, !showRow )[0] 4854 .display = showRow ? '' : 'none'; 4855 } 4856 } 4857 c.filteredRows += $rows.not( '.' + wo.filter_filteredRow ).length; 4858 c.totalRows += $rows.length; 4859 ts.processTbody( table, $tbody, false ); 4860 } 4861 // lastCombinedFilter is no longer used internally 4862 c.lastCombinedFilter = storedFilters.join(''); // save last search 4863 // don't save 'filters' directly since it may have altered ( AnyMatch column searches ) 4864 c.lastSearch = storedFilters; 4865 c.$table.data( 'lastSearch', storedFilters ); 4866 if ( wo.filter_saveFilters && ts.storage ) { 4867 ts.storage( table, 'tablesorter-filters', tsf.processFilters( storedFilters, true ) ); 4868 } 4869 if ( debug ) { 4870 console.log( 'Filter >> Completed search' + ts.benchmark(time) ); 4871 } 4872 if ( wo.filter_initialized ) { 4873 c.$table.triggerHandler( 'filterBeforeEnd', c ); 4874 c.$table.triggerHandler( 'filterEnd', c ); 4875 } 4876 setTimeout( function() { 4877 ts.applyWidget( c.table ); // make sure zebra widget is applied 4878 }, 0 ); 4879 }, 4880 getOptionSource: function( table, column, onlyAvail ) { 4881 table = $( table )[0]; 4882 var c = table.config, 4883 wo = c.widgetOptions, 4884 arry = false, 4885 source = wo.filter_selectSource, 4886 last = c.$table.data( 'lastSearch' ) || [], 4887 fxn = typeof source === 'function' ? true : ts.getColumnData( table, source, column ); 4888 4889 if ( onlyAvail && last[column] !== '' ) { 4890 onlyAvail = false; 4891 } 4892 4893 // filter select source option 4894 if ( fxn === true ) { 4895 // OVERALL source 4896 arry = source( table, column, onlyAvail ); 4897 } else if ( fxn instanceof $ || ( $.type( fxn ) === 'string' && fxn.indexOf( '</option>' ) >= 0 ) ) { 4898 // selectSource is a jQuery object or string of options 4899 return fxn; 4900 } else if ( $.isArray( fxn ) ) { 4901 arry = fxn; 4902 } else if ( $.type( source ) === 'object' && fxn ) { 4903 // custom select source function for a SPECIFIC COLUMN 4904 arry = fxn( table, column, onlyAvail ); 4905 // abort - updating the selects from an external method 4906 if (arry === null) { 4907 return null; 4908 } 4909 } 4910 if ( arry === false ) { 4911 // fall back to original method 4912 arry = tsf.getOptions( table, column, onlyAvail ); 4913 } 4914 4915 return tsf.processOptions( table, column, arry ); 4916 4917 }, 4918 processOptions: function( table, column, arry ) { 4919 if ( !$.isArray( arry ) ) { 4920 return false; 4921 } 4922 table = $( table )[0]; 4923 var cts, txt, indx, len, parsedTxt, str, 4924 c = table.config, 4925 validColumn = typeof column !== 'undefined' && column !== null && column >= 0 && column < c.columns, 4926 direction = validColumn ? c.$headerIndexed[ column ].hasClass( 'filter-select-sort-desc' ) : false, 4927 parsed = []; 4928 // get unique elements and sort the list 4929 // if $.tablesorter.sortText exists ( not in the original tablesorter ), 4930 // then natural sort the list otherwise use a basic sort 4931 arry = $.grep( arry, function( value, indx ) { 4932 if ( value.text ) { 4933 return true; 4934 } 4935 return $.inArray( value, arry ) === indx; 4936 }); 4937 if ( validColumn && c.$headerIndexed[ column ].hasClass( 'filter-select-nosort' ) ) { 4938 // unsorted select options 4939 return arry; 4940 } else { 4941 len = arry.length; 4942 // parse select option values 4943 for ( indx = 0; indx < len; indx++ ) { 4944 txt = arry[ indx ]; 4945 // check for object 4946 str = txt.text ? txt.text : txt; 4947 // sortNatural breaks if you don't pass it strings 4948 parsedTxt = ( validColumn && c.parsers && c.parsers.length && 4949 c.parsers[ column ].format( str, table, [], column ) || str ).toString(); 4950 parsedTxt = c.widgetOptions.filter_ignoreCase ? parsedTxt.toLowerCase() : parsedTxt; 4951 // parse array data using set column parser; this DOES NOT pass the original 4952 // table cell to the parser format function 4953 if ( txt.text ) { 4954 txt.parsed = parsedTxt; 4955 parsed[ parsed.length ] = txt; 4956 } else { 4957 parsed[ parsed.length ] = { 4958 text : txt, 4959 // check parser length - fixes #934 4960 parsed : parsedTxt 4961 }; 4962 } 4963 } 4964 // sort parsed select options 4965 cts = c.textSorter || ''; 4966 parsed.sort( function( a, b ) { 4967 var x = direction ? b.parsed : a.parsed, 4968 y = direction ? a.parsed : b.parsed; 4969 if ( validColumn && typeof cts === 'function' ) { 4970 // custom OVERALL text sorter 4971 return cts( x, y, true, column, table ); 4972 } else if ( validColumn && typeof cts === 'object' && cts.hasOwnProperty( column ) ) { 4973 // custom text sorter for a SPECIFIC COLUMN 4974 return cts[column]( x, y, true, column, table ); 4975 } else if ( ts.sortNatural ) { 4976 // fall back to natural sort 4977 return ts.sortNatural( x, y ); 4978 } 4979 // using an older version! do a basic sort 4980 return true; 4981 }); 4982 // rebuild arry from sorted parsed data 4983 arry = []; 4984 len = parsed.length; 4985 for ( indx = 0; indx < len; indx++ ) { 4986 arry[ arry.length ] = parsed[indx]; 4987 } 4988 return arry; 4989 } 4990 }, 4991 getOptions: function( table, column, onlyAvail ) { 4992 table = $( table )[0]; 4993 var rowIndex, tbodyIndex, len, row, cache, indx, child, childLen, 4994 c = table.config, 4995 wo = c.widgetOptions, 4996 arry = []; 4997 for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { 4998 cache = c.cache[tbodyIndex]; 4999 len = c.cache[tbodyIndex].normalized.length; 5000 // loop through the rows 5001 for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { 5002 // get cached row from cache.row ( old ) or row data object 5003 // ( new; last item in normalized array ) 5004 row = cache.row ? 5005 cache.row[ rowIndex ] : 5006 cache.normalized[ rowIndex ][ c.columns ].$row[0]; 5007 // check if has class filtered 5008 if ( onlyAvail && row.className.match( wo.filter_filteredRow ) ) { 5009 continue; 5010 } 5011 // get non-normalized cell content 5012 if ( wo.filter_useParsedData || 5013 c.parsers[column].parsed || 5014 c.$headerIndexed[column].hasClass( 'filter-parsed' ) ) { 5015 arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ column ]; 5016 // child row parsed data 5017 if ( wo.filter_childRows && wo.filter_childByColumn ) { 5018 childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length - 1; 5019 for ( indx = 0; indx < childLen; indx++ ) { 5020 arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ c.columns ].child[ indx ][ column ]; 5021 } 5022 } 5023 } else { 5024 // get raw cached data instead of content directly from the cells 5025 arry[ arry.length ] = cache.normalized[ rowIndex ][ c.columns ].raw[ column ]; 5026 // child row unparsed data 5027 if ( wo.filter_childRows && wo.filter_childByColumn ) { 5028 childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length; 5029 for ( indx = 1; indx < childLen; indx++ ) { 5030 child = cache.normalized[ rowIndex ][ c.columns ].$row.eq( indx ).children().eq( column ); 5031 arry[ arry.length ] = '' + ts.getElementText( c, child, column ); 5032 } 5033 } 5034 } 5035 } 5036 } 5037 return arry; 5038 }, 5039 buildSelect: function( table, column, arry, updating, onlyAvail ) { 5040 table = $( table )[0]; 5041 column = parseInt( column, 10 ); 5042 if ( !table.config.cache || $.isEmptyObject( table.config.cache ) ) { 5043 return; 5044 } 5045 5046 var indx, val, txt, t, $filters, $filter, option, 5047 c = table.config, 5048 wo = c.widgetOptions, 5049 node = c.$headerIndexed[ column ], 5050 // t.data( 'placeholder' ) won't work in jQuery older than 1.4.3 5051 options = '<option value="">' + 5052 ( node.data( 'placeholder' ) || 5053 node.attr( 'data-placeholder' ) || 5054 wo.filter_placeholder.select || '' 5055 ) + '</option>', 5056 // Get curent filter value 5057 currentValue = c.$table 5058 .find( 'thead' ) 5059 .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) 5060 .val(); 5061 5062 // nothing included in arry ( external source ), so get the options from 5063 // filter_selectSource or column data 5064 if ( typeof arry === 'undefined' || arry === '' ) { 5065 arry = tsf.getOptionSource( table, column, onlyAvail ); 5066 // abort, selects are updated by an external method 5067 if (arry === null) { 5068 return; 5069 } 5070 } 5071 5072 if ( $.isArray( arry ) ) { 5073 // build option list 5074 for ( indx = 0; indx < arry.length; indx++ ) { 5075 option = arry[ indx ]; 5076 if ( option.text ) { 5077 // OBJECT!! add data-function-name in case the value is set in filter_functions 5078 option['data-function-name'] = typeof option.value === 'undefined' ? option.text : option.value; 5079 5080 // support jQuery < v1.8, otherwise the below code could be shortened to 5081 // options += $( '<option>', option )[ 0 ].outerHTML; 5082 options += '<option'; 5083 for ( val in option ) { 5084 if ( option.hasOwnProperty( val ) && val !== 'text' ) { 5085 options += ' ' + val + '="' + option[ val ].replace( tsfRegex.quote, '"' ) + '"'; 5086 } 5087 } 5088 if ( !option.value ) { 5089 options += ' value="' + option.text.replace( tsfRegex.quote, '"' ) + '"'; 5090 } 5091 options += '>' + option.text.replace( tsfRegex.quote, '"' ) + '</option>'; 5092 // above code is needed in jQuery < v1.8 5093 5094 // make sure we don't turn an object into a string (objects without a "text" property) 5095 } else if ( '' + option !== '[object Object]' ) { 5096 txt = option = ( '' + option ).replace( tsfRegex.quote, '"' ); 5097 val = txt; 5098 // allow including a symbol in the selectSource array 5099 // 'a-z|A through Z' so that 'a-z' becomes the option value 5100 // and 'A through Z' becomes the option text 5101 if ( txt.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { 5102 t = txt.split( wo.filter_selectSourceSeparator ); 5103 val = t[0]; 5104 txt = t[1]; 5105 } 5106 // replace quotes - fixes #242 & ignore empty strings 5107 // see http://stackoverflow.com/q/14990971/145346 5108 options += option !== '' ? 5109 '<option ' + 5110 ( val === txt ? '' : 'data-function-name="' + option + '" ' ) + 5111 'value="' + val + '">' + txt + 5112 '</option>' : ''; 5113 } 5114 } 5115 // clear arry so it doesn't get appended twice 5116 arry = []; 5117 } 5118 5119 // update all selects in the same column ( clone thead in sticky headers & 5120 // any external selects ) - fixes 473 5121 $filters = ( c.$filters ? c.$filters : c.$table.children( 'thead' ) ) 5122 .find( '.' + tscss.filter ); 5123 if ( wo.filter_$externalFilters ) { 5124 $filters = $filters && $filters.length ? 5125 $filters.add( wo.filter_$externalFilters ) : 5126 wo.filter_$externalFilters; 5127 } 5128 $filter = $filters.filter( 'select[data-column="' + column + '"]' ); 5129 5130 // make sure there is a select there! 5131 if ( $filter.length ) { 5132 $filter[ updating ? 'html' : 'append' ]( options ); 5133 if ( !$.isArray( arry ) ) { 5134 // append options if arry is provided externally as a string or jQuery object 5135 // options ( default value ) was already added 5136 $filter.append( arry ).val( currentValue ); 5137 } 5138 $filter.val( currentValue ); 5139 } 5140 }, 5141 buildDefault: function( table, updating ) { 5142 var columnIndex, $header, noSelect, 5143 c = table.config, 5144 wo = c.widgetOptions, 5145 columns = c.columns; 5146 // build default select dropdown 5147 for ( columnIndex = 0; columnIndex < columns; columnIndex++ ) { 5148 $header = c.$headerIndexed[columnIndex]; 5149 noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) ); 5150 // look for the filter-select class; build/update it if found 5151 if ( ( $header.hasClass( 'filter-select' ) || 5152 ts.getColumnData( table, wo.filter_functions, columnIndex ) === true ) && noSelect ) { 5153 tsf.buildSelect( table, columnIndex, '', updating, $header.hasClass( wo.filter_onlyAvail ) ); 5154 } 5155 } 5156 } 5157 }; 5158 5159 // filter regex variable 5160 tsfRegex = tsf.regex; 5161 5162 ts.getFilters = function( table, getRaw, setFilters, skipFirst ) { 5163 var i, $filters, $column, cols, 5164 filters = [], 5165 c = table ? $( table )[0].config : '', 5166 wo = c ? c.widgetOptions : ''; 5167 if ( ( getRaw !== true && wo && !wo.filter_columnFilters ) || 5168 // setFilters called, but last search is exactly the same as the current 5169 // fixes issue #733 & #903 where calling update causes the input values to reset 5170 ( $.isArray(setFilters) && tsf.equalFilters(c, setFilters, c.lastSearch) ) 5171 ) { 5172 return $( table ).data( 'lastSearch' ) || []; 5173 } 5174 if ( c ) { 5175 if ( c.$filters ) { 5176 $filters = c.$filters.find( '.' + tscss.filter ); 5177 } 5178 if ( wo.filter_$externalFilters ) { 5179 $filters = $filters && $filters.length ? 5180 $filters.add( wo.filter_$externalFilters ) : 5181 wo.filter_$externalFilters; 5182 } 5183 if ( $filters && $filters.length ) { 5184 filters = setFilters || []; 5185 for ( i = 0; i < c.columns + 1; i++ ) { 5186 cols = ( i === c.columns ? 5187 // 'all' columns can now include a range or set of columms ( data-column='0-2,4,6-7' ) 5188 wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector : 5189 '[data-column="' + i + '"]' ); 5190 $column = $filters.filter( cols ); 5191 if ( $column.length ) { 5192 // move the latest search to the first slot in the array 5193 $column = tsf.getLatestSearch( $column ); 5194 if ( $.isArray( setFilters ) ) { 5195 // skip first ( latest input ) to maintain cursor position while typing 5196 if ( skipFirst && $column.length > 1 ) { 5197 $column = $column.slice( 1 ); 5198 } 5199 if ( i === c.columns ) { 5200 // prevent data-column='all' from filling data-column='0,1' ( etc ) 5201 cols = $column.filter( wo.filter_anyColumnSelector ); 5202 $column = cols.length ? cols : $column; 5203 } 5204 $column 5205 .val( setFilters[ i ] ) 5206 // must include a namespace here; but not c.namespace + 'filter'? 5207 .trigger( 'change' + c.namespace ); 5208 } else { 5209 filters[i] = $column.val() || ''; 5210 // don't change the first... it will move the cursor 5211 if ( i === c.columns ) { 5212 // don't update range columns from 'all' setting 5213 $column 5214 .slice( 1 ) 5215 .filter( '[data-column*="' + $column.attr( 'data-column' ) + '"]' ) 5216 .val( filters[ i ] ); 5217 } else { 5218 $column 5219 .slice( 1 ) 5220 .val( filters[ i ] ); 5221 } 5222 } 5223 // save any match input dynamically 5224 if ( i === c.columns && $column.length ) { 5225 wo.filter_$anyMatch = $column; 5226 } 5227 } 5228 } 5229 } 5230 } 5231 return filters; 5232 }; 5233 5234 ts.setFilters = function( table, filter, apply, skipFirst ) { 5235 var c = table ? $( table )[0].config : '', 5236 valid = ts.getFilters( table, true, filter, skipFirst ); 5237 // default apply to "true" 5238 if ( typeof apply === 'undefined' ) { 5239 apply = true; 5240 } 5241 if ( c && apply ) { 5242 // ensure new set filters are applied, even if the search is the same 5243 c.lastCombinedFilter = null; 5244 c.lastSearch = []; 5245 tsf.searching( c.table, filter, skipFirst ); 5246 c.$table.triggerHandler( 'filterFomatterUpdate' ); 5247 } 5248 return valid.length !== 0; 5249 }; 5250 5251})( jQuery ); 5252 5253/*! Widget: stickyHeaders - updated 9/27/2017 (v2.29.0) *//* 5254 * Requires tablesorter v2.8+ and jQuery 1.4.3+ 5255 * by Rob Garrison 5256 */ 5257;(function ($, window) { 5258 'use strict'; 5259 var ts = $.tablesorter || {}; 5260 5261 $.extend(ts.css, { 5262 sticky : 'tablesorter-stickyHeader', // stickyHeader 5263 stickyVis : 'tablesorter-sticky-visible', 5264 stickyHide: 'tablesorter-sticky-hidden', 5265 stickyWrap: 'tablesorter-sticky-wrapper' 5266 }); 5267 5268 // Add a resize event to table headers 5269 ts.addHeaderResizeEvent = function(table, disable, settings) { 5270 table = $(table)[0]; // make sure we're using a dom element 5271 if ( !table.config ) { return; } 5272 var defaults = { 5273 timer : 250 5274 }, 5275 options = $.extend({}, defaults, settings), 5276 c = table.config, 5277 wo = c.widgetOptions, 5278 checkSizes = function( triggerEvent ) { 5279 var index, headers, $header, sizes, width, height, 5280 len = c.$headers.length; 5281 wo.resize_flag = true; 5282 headers = []; 5283 for ( index = 0; index < len; index++ ) { 5284 $header = c.$headers.eq( index ); 5285 sizes = $header.data( 'savedSizes' ) || [ 0, 0 ]; // fixes #394 5286 width = $header[0].offsetWidth; 5287 height = $header[0].offsetHeight; 5288 if ( width !== sizes[0] || height !== sizes[1] ) { 5289 $header.data( 'savedSizes', [ width, height ] ); 5290 headers.push( $header[0] ); 5291 } 5292 } 5293 if ( headers.length && triggerEvent !== false ) { 5294 c.$table.triggerHandler( 'resize', [ headers ] ); 5295 } 5296 wo.resize_flag = false; 5297 }; 5298 clearInterval(wo.resize_timer); 5299 if (disable) { 5300 wo.resize_flag = false; 5301 return false; 5302 } 5303 checkSizes( false ); 5304 wo.resize_timer = setInterval(function() { 5305 if (wo.resize_flag) { return; } 5306 checkSizes(); 5307 }, options.timer); 5308 }; 5309 5310 function getStickyOffset(c, wo) { 5311 var $el = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : []; 5312 return $el.length ? 5313 $el.height() || 0 : 5314 parseInt(wo.stickyHeaders_offset, 10) || 0; 5315 } 5316 5317 // Sticky headers based on this awesome article: 5318 // http://css-tricks.com/13465-persistent-headers/ 5319 // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech 5320 // ************************** 5321 ts.addWidget({ 5322 id: 'stickyHeaders', 5323 priority: 54, // sticky widget must be initialized after the filter & before pager widget! 5324 options: { 5325 stickyHeaders : '', // extra class name added to the sticky header row 5326 stickyHeaders_appendTo : null, // jQuery selector or object to phycially attach the sticky headers 5327 stickyHeaders_attachTo : null, // jQuery selector or object to attach scroll listener to (overridden by xScroll & yScroll settings) 5328 stickyHeaders_xScroll : null, // jQuery selector or object to monitor horizontal scroll position (defaults: xScroll > attachTo > window) 5329 stickyHeaders_yScroll : null, // jQuery selector or object to monitor vertical scroll position (defaults: yScroll > attachTo > window) 5330 stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element 5331 stickyHeaders_filteredToTop: true, // scroll table top into view after filtering 5332 stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists 5333 stickyHeaders_addResizeEvent : true, // trigger 'resize' event on headers 5334 stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header 5335 stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs 5336 }, 5337 format: function(table, c, wo) { 5338 // filter widget doesn't initialize on an empty table. Fixes #449 5339 if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) { 5340 return; 5341 } 5342 var index, len, $t, 5343 $table = c.$table, 5344 // add position: relative to attach element, hopefully it won't cause trouble. 5345 $attach = $(wo.stickyHeaders_attachTo || wo.stickyHeaders_appendTo), 5346 namespace = c.namespace + 'stickyheaders ', 5347 // element to watch for the scroll event 5348 $yScroll = $(wo.stickyHeaders_yScroll || wo.stickyHeaders_attachTo || window), 5349 $xScroll = $(wo.stickyHeaders_xScroll || wo.stickyHeaders_attachTo || window), 5350 $thead = $table.children('thead:first'), 5351 $header = $thead.children('tr').not('.sticky-false').children(), 5352 $tfoot = $table.children('tfoot'), 5353 stickyOffset = getStickyOffset(c, wo), 5354 // is this table nested? If so, find parent sticky header wrapper (div, not table) 5355 $nestedSticky = $table.parent().closest('.' + ts.css.table).hasClass('hasStickyHeaders') ? 5356 $table.parent().closest('table.tablesorter')[0].config.widgetOptions.$sticky.parent() : [], 5357 nestedStickyTop = $nestedSticky.length ? $nestedSticky.height() : 0, 5358 // clone table, then wrap to make sticky header 5359 $stickyTable = wo.$sticky = $table.clone() 5360 .addClass('containsStickyHeaders ' + ts.css.sticky + ' ' + wo.stickyHeaders + ' ' + c.namespace.slice(1) + '_extra_table' ) 5361 .wrap('<div class="' + ts.css.stickyWrap + '">'), 5362 $stickyWrap = $stickyTable.parent() 5363 .addClass(ts.css.stickyHide) 5364 .css({ 5365 position : $attach.length ? 'absolute' : 'fixed', 5366 padding : parseInt( $stickyTable.parent().parent().css('padding-left'), 10 ), 5367 top : stickyOffset + nestedStickyTop, 5368 left : 0, 5369 visibility : 'hidden', 5370 zIndex : wo.stickyHeaders_zIndex || 2 5371 }), 5372 $stickyThead = $stickyTable.children('thead:first'), 5373 $stickyCells, 5374 laststate = '', 5375 setWidth = function($orig, $clone) { 5376 var index, width, border, $cell, $this, 5377 $cells = $orig.filter(':visible'), 5378 len = $cells.length; 5379 for ( index = 0; index < len; index++ ) { 5380 $cell = $clone.filter(':visible').eq(index); 5381 $this = $cells.eq(index); 5382 // code from https://github.com/jmosbech/StickyTableHeaders 5383 if ($this.css('box-sizing') === 'border-box') { 5384 width = $this.outerWidth(); 5385 } else { 5386 if ($cell.css('border-collapse') === 'collapse') { 5387 if (window.getComputedStyle) { 5388 width = parseFloat( window.getComputedStyle($this[0], null).width ); 5389 } else { 5390 // ie8 only 5391 border = parseFloat( $this.css('border-width') ); 5392 width = $this.outerWidth() - parseFloat( $this.css('padding-left') ) - parseFloat( $this.css('padding-right') ) - border; 5393 } 5394 } else { 5395 width = $this.width(); 5396 } 5397 } 5398 $cell.css({ 5399 'width': width, 5400 'min-width': width, 5401 'max-width': width 5402 }); 5403 } 5404 }, 5405 getLeftPosition = function(yWindow) { 5406 if (yWindow === false && $nestedSticky.length) { 5407 return $table.position().left; 5408 } 5409 return $attach.length ? 5410 parseInt($attach.css('padding-left'), 10) || 0 : 5411 $table.offset().left - parseInt($table.css('margin-left'), 10) - $(window).scrollLeft(); 5412 }, 5413 resizeHeader = function() { 5414 $stickyWrap.css({ 5415 left : getLeftPosition(), 5416 width: $table.outerWidth() 5417 }); 5418 setWidth( $table, $stickyTable ); 5419 setWidth( $header, $stickyCells ); 5420 }, 5421 scrollSticky = function( resizing ) { 5422 if (!$table.is(':visible')) { return; } // fixes #278 5423 // Detect nested tables - fixes #724 5424 nestedStickyTop = $nestedSticky.length ? $nestedSticky.offset().top - $yScroll.scrollTop() + $nestedSticky.height() : 0; 5425 var tmp, 5426 offset = $table.offset(), 5427 stickyOffset = getStickyOffset(c, wo), 5428 yWindow = $.isWindow( $yScroll[0] ), // $.isWindow needs jQuery 1.4.3 5429 yScroll = yWindow ? 5430 $yScroll.scrollTop() : 5431 // use parent sticky position if nested AND inside of a scrollable element - see #1512 5432 $nestedSticky.length ? parseInt($nestedSticky[0].style.top, 10) : $yScroll.offset().top, 5433 attachTop = $attach.length ? yScroll : $yScroll.scrollTop(), 5434 captionHeight = wo.stickyHeaders_includeCaption ? 0 : $table.children( 'caption' ).height() || 0, 5435 scrollTop = attachTop + stickyOffset + nestedStickyTop - captionHeight, 5436 tableHeight = $table.height() - ($stickyWrap.height() + ($tfoot.height() || 0)) - captionHeight, 5437 isVisible = ( scrollTop > offset.top ) && ( scrollTop < offset.top + tableHeight ) ? 'visible' : 'hidden', 5438 state = isVisible === 'visible' ? ts.css.stickyVis : ts.css.stickyHide, 5439 needsUpdating = !$stickyWrap.hasClass( state ), 5440 cssSettings = { visibility : isVisible }; 5441 if ($attach.length) { 5442 // attached sticky headers always need updating 5443 needsUpdating = true; 5444 cssSettings.top = yWindow ? scrollTop - $attach.offset().top : $attach.scrollTop(); 5445 } 5446 // adjust when scrolling horizontally - fixes issue #143 5447 tmp = getLeftPosition(yWindow); 5448 if (tmp !== parseInt($stickyWrap.css('left'), 10)) { 5449 needsUpdating = true; 5450 cssSettings.left = tmp; 5451 } 5452 cssSettings.top = ( cssSettings.top || 0 ) + 5453 // If nested AND inside of a scrollable element, only add parent sticky height 5454 (!yWindow && $nestedSticky.length ? $nestedSticky.height() : stickyOffset + nestedStickyTop); 5455 if (needsUpdating) { 5456 $stickyWrap 5457 .removeClass( ts.css.stickyVis + ' ' + ts.css.stickyHide ) 5458 .addClass( state ) 5459 .css(cssSettings); 5460 } 5461 if (isVisible !== laststate || resizing) { 5462 // make sure the column widths match 5463 resizeHeader(); 5464 laststate = isVisible; 5465 } 5466 }; 5467 // only add a position relative if a position isn't already defined 5468 if ($attach.length && !$attach.css('position')) { 5469 $attach.css('position', 'relative'); 5470 } 5471 // fix clone ID, if it exists - fixes #271 5472 if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; } 5473 // clear out cloned table, except for sticky header 5474 // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing 5475 $stickyTable.find('> thead:gt(0), tr.sticky-false').hide(); 5476 $stickyTable.find('> tbody, > tfoot').remove(); 5477 $stickyTable.find('caption').toggle(wo.stickyHeaders_includeCaption); 5478 // issue #172 - find td/th in sticky header 5479 $stickyCells = $stickyThead.children().children(); 5480 $stickyTable.css({ height:0, width:0, margin: 0 }); 5481 // remove resizable block 5482 $stickyCells.find('.' + ts.css.resizer).remove(); 5483 // update sticky header class names to match real header after sorting 5484 $table 5485 .addClass('hasStickyHeaders') 5486 .bind('pagerComplete' + namespace, function() { 5487 resizeHeader(); 5488 }); 5489 5490 ts.bindEvents(table, $stickyThead.children().children('.' + ts.css.header)); 5491 5492 if (wo.stickyHeaders_appendTo) { 5493 $(wo.stickyHeaders_appendTo).append( $stickyWrap ); 5494 } else { 5495 // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. 5496 $table.after( $stickyWrap ); 5497 } 5498 5499 // onRenderHeader is defined, we need to do something about it (fixes #641) 5500 if (c.onRenderHeader) { 5501 $t = $stickyThead.children('tr').children(); 5502 len = $t.length; 5503 for ( index = 0; index < len; index++ ) { 5504 // send second parameter 5505 c.onRenderHeader.apply( $t.eq( index ), [ index, c, $stickyTable ] ); 5506 } 5507 } 5508 // make it sticky! 5509 $xScroll.add($yScroll) 5510 .unbind( ('scroll resize '.split(' ').join( namespace )).replace(/\s+/g, ' ') ) 5511 .bind('scroll resize '.split(' ').join( namespace ), function( event ) { 5512 scrollSticky( event.type === 'resize' ); 5513 }); 5514 c.$table 5515 .unbind('stickyHeadersUpdate' + namespace) 5516 .bind('stickyHeadersUpdate' + namespace, function() { 5517 scrollSticky( true ); 5518 }); 5519 5520 if (wo.stickyHeaders_addResizeEvent) { 5521 ts.addHeaderResizeEvent(table); 5522 } 5523 5524 // look for filter widget 5525 if ($table.hasClass('hasFilters') && wo.filter_columnFilters) { 5526 // scroll table into view after filtering, if sticky header is active - #482 5527 $table.bind('filterEnd' + namespace, function() { 5528 // $(':focus') needs jQuery 1.6+ 5529 var $td = $(document.activeElement).closest('td'), 5530 column = $td.parent().children().index($td); 5531 // only scroll if sticky header is active 5532 if ($stickyWrap.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) { 5533 // scroll to original table (not sticky clone) 5534 window.scrollTo(0, $table.position().top); 5535 // give same input/select focus; check if c.$filters exists; fixes #594 5536 if (column >= 0 && c.$filters) { 5537 c.$filters.eq(column).find('a, select, input').filter(':visible').focus(); 5538 } 5539 } 5540 }); 5541 ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) ); 5542 // support hideFilters 5543 if (wo.filter_hideFilters) { 5544 ts.filter.hideFilters(c, $stickyTable); 5545 } 5546 } 5547 5548 // resize table (Firefox) 5549 if (wo.stickyHeaders_addResizeEvent) { 5550 $table.bind('resize' + c.namespace + 'stickyheaders', function() { 5551 resizeHeader(); 5552 }); 5553 } 5554 5555 // make sure sticky is visible if page is partially scrolled 5556 scrollSticky( true ); 5557 $table.triggerHandler('stickyHeadersInit'); 5558 5559 }, 5560 remove: function(table, c, wo) { 5561 var namespace = c.namespace + 'stickyheaders '; 5562 c.$table 5563 .removeClass('hasStickyHeaders') 5564 .unbind( ('pagerComplete resize filterEnd stickyHeadersUpdate '.split(' ').join(namespace)).replace(/\s+/g, ' ') ) 5565 .next('.' + ts.css.stickyWrap).remove(); 5566 if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table 5567 $(window) 5568 .add(wo.stickyHeaders_xScroll) 5569 .add(wo.stickyHeaders_yScroll) 5570 .add(wo.stickyHeaders_attachTo) 5571 .unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') ); 5572 ts.addHeaderResizeEvent(table, true); 5573 } 5574 }); 5575 5576})(jQuery, window); 5577 5578/*! Widget: resizable - updated 2018-03-26 (v2.30.2) */ 5579/*jshint browser:true, jquery:true, unused:false */ 5580;(function ($, window) { 5581 'use strict'; 5582 var ts = $.tablesorter || {}; 5583 5584 $.extend(ts.css, { 5585 resizableContainer : 'tablesorter-resizable-container', 5586 resizableHandle : 'tablesorter-resizable-handle', 5587 resizableNoSelect : 'tablesorter-disableSelection', 5588 resizableStorage : 'tablesorter-resizable' 5589 }); 5590 5591 // Add extra scroller css 5592 $(function() { 5593 var s = '<style>' + 5594 'body.' + ts.css.resizableNoSelect + ' { -ms-user-select: none; -moz-user-select: -moz-none;' + 5595 '-khtml-user-select: none; -webkit-user-select: none; user-select: none; }' + 5596 '.' + ts.css.resizableContainer + ' { position: relative; height: 1px; }' + 5597 // make handle z-index > than stickyHeader z-index, so the handle stays above sticky header 5598 '.' + ts.css.resizableHandle + ' { position: absolute; display: inline-block; width: 8px;' + 5599 'top: 1px; cursor: ew-resize; z-index: 3; user-select: none; -moz-user-select: none; }' + 5600 '</style>'; 5601 $('head').append(s); 5602 }); 5603 5604 ts.resizable = { 5605 init : function( c, wo ) { 5606 if ( c.$table.hasClass( 'hasResizable' ) ) { return; } 5607 c.$table.addClass( 'hasResizable' ); 5608 5609 var noResize, $header, column, storedSizes, tmp, 5610 $table = c.$table, 5611 $parent = $table.parent(), 5612 marginTop = parseInt( $table.css( 'margin-top' ), 10 ), 5613 5614 // internal variables 5615 vars = wo.resizable_vars = { 5616 useStorage : ts.storage && wo.resizable !== false, 5617 $wrap : $parent, 5618 mouseXPosition : 0, 5619 $target : null, 5620 $next : null, 5621 overflow : $parent.css('overflow') === 'auto' || 5622 $parent.css('overflow') === 'scroll' || 5623 $parent.css('overflow-x') === 'auto' || 5624 $parent.css('overflow-x') === 'scroll', 5625 storedSizes : [] 5626 }; 5627 5628 // set default widths 5629 ts.resizableReset( c.table, true ); 5630 5631 // now get measurements! 5632 vars.tableWidth = $table.width(); 5633 // attempt to autodetect 5634 vars.fullWidth = Math.abs( $parent.width() - vars.tableWidth ) < 20; 5635 5636 /* 5637 // Hacky method to determine if table width is set to 'auto' 5638 // http://stackoverflow.com/a/20892048/145346 5639 if ( !vars.fullWidth ) { 5640 tmp = $table.width(); 5641 $header = $table.wrap('<span>').parent(); // temp variable 5642 storedSizes = parseInt( $table.css( 'margin-left' ), 10 ) || 0; 5643 $table.css( 'margin-left', storedSizes + 50 ); 5644 vars.tableWidth = $header.width() > tmp ? 'auto' : tmp; 5645 $table.css( 'margin-left', storedSizes ? storedSizes : '' ); 5646 $header = null; 5647 $table.unwrap('<span>'); 5648 } 5649 */ 5650 5651 if ( vars.useStorage && vars.overflow ) { 5652 // save table width 5653 ts.storage( c.table, 'tablesorter-table-original-css-width', vars.tableWidth ); 5654 tmp = ts.storage( c.table, 'tablesorter-table-resized-width' ) || 'auto'; 5655 ts.resizable.setWidth( $table, tmp, true ); 5656 } 5657 wo.resizable_vars.storedSizes = storedSizes = ( vars.useStorage ? 5658 ts.storage( c.table, ts.css.resizableStorage ) : 5659 [] ) || []; 5660 ts.resizable.setWidths( c, wo, storedSizes ); 5661 ts.resizable.updateStoredSizes( c, wo ); 5662 5663 wo.$resizable_container = $( '<div class="' + ts.css.resizableContainer + '">' ) 5664 .css({ top : marginTop }) 5665 .insertBefore( $table ); 5666 // add container 5667 for ( column = 0; column < c.columns; column++ ) { 5668 $header = c.$headerIndexed[ column ]; 5669 tmp = ts.getColumnData( c.table, c.headers, column ); 5670 noResize = ts.getData( $header, tmp, 'resizable' ) === 'false'; 5671 if ( !noResize ) { 5672 $( '<div class="' + ts.css.resizableHandle + '">' ) 5673 .appendTo( wo.$resizable_container ) 5674 .attr({ 5675 'data-column' : column, 5676 'unselectable' : 'on' 5677 }) 5678 .data( 'header', $header ) 5679 .bind( 'selectstart', false ); 5680 } 5681 } 5682 ts.resizable.bindings( c, wo ); 5683 }, 5684 5685 updateStoredSizes : function( c, wo ) { 5686 var column, $header, 5687 len = c.columns, 5688 vars = wo.resizable_vars; 5689 vars.storedSizes = []; 5690 for ( column = 0; column < len; column++ ) { 5691 $header = c.$headerIndexed[ column ]; 5692 vars.storedSizes[ column ] = $header.is(':visible') ? $header.width() : 0; 5693 } 5694 }, 5695 5696 setWidth : function( $el, width, overflow ) { 5697 // overflow tables need min & max width set as well 5698 $el.css({ 5699 'width' : width, 5700 'min-width' : overflow ? width : '', 5701 'max-width' : overflow ? width : '' 5702 }); 5703 }, 5704 5705 setWidths : function( c, wo, storedSizes ) { 5706 var column, $temp, 5707 vars = wo.resizable_vars, 5708 $extra = $( c.namespace + '_extra_headers' ), 5709 $col = c.$table.children( 'colgroup' ).children( 'col' ); 5710 storedSizes = storedSizes || vars.storedSizes || []; 5711 // process only if table ID or url match 5712 if ( storedSizes.length ) { 5713 for ( column = 0; column < c.columns; column++ ) { 5714 // set saved resizable widths 5715 ts.resizable.setWidth( c.$headerIndexed[ column ], storedSizes[ column ], vars.overflow ); 5716 if ( $extra.length ) { 5717 // stickyHeaders needs to modify min & max width as well 5718 $temp = $extra.eq( column ).add( $col.eq( column ) ); 5719 ts.resizable.setWidth( $temp, storedSizes[ column ], vars.overflow ); 5720 } 5721 } 5722 $temp = $( c.namespace + '_extra_table' ); 5723 if ( $temp.length && !ts.hasWidget( c.table, 'scroller' ) ) { 5724 ts.resizable.setWidth( $temp, c.$table.outerWidth(), vars.overflow ); 5725 } 5726 } 5727 }, 5728 5729 setHandlePosition : function( c, wo ) { 5730 var startPosition, 5731 tableHeight = c.$table.height(), 5732 $handles = wo.$resizable_container.children(), 5733 handleCenter = Math.floor( $handles.width() / 2 ); 5734 5735 if ( ts.hasWidget( c.table, 'scroller' ) ) { 5736 tableHeight = 0; 5737 c.$table.closest( '.' + ts.css.scrollerWrap ).children().each(function() { 5738 var $this = $(this); 5739 // center table has a max-height set 5740 tableHeight += $this.filter('[style*="height"]').length ? $this.height() : $this.children('table').height(); 5741 }); 5742 } 5743 5744 if ( !wo.resizable_includeFooter && c.$table.children('tfoot').length ) { 5745 tableHeight -= c.$table.children('tfoot').height(); 5746 } 5747 // subtract out table left position from resizable handles. Fixes #864 5748 // jQuery v3.3.0+ appears to include the start position with the $header.position().left; see #1544 5749 startPosition = parseFloat($.fn.jquery) >= 3.3 ? 0 : c.$table.position().left; 5750 $handles.each( function() { 5751 var $this = $(this), 5752 column = parseInt( $this.attr( 'data-column' ), 10 ), 5753 columns = c.columns - 1, 5754 $header = $this.data( 'header' ); 5755 if ( !$header ) { return; } // see #859 5756 if ( 5757 !$header.is(':visible') || 5758 ( !wo.resizable_addLastColumn && ts.resizable.checkVisibleColumns(c, column) ) 5759 ) { 5760 $this.hide(); 5761 } else if ( column < columns || column === columns && wo.resizable_addLastColumn ) { 5762 $this.css({ 5763 display: 'inline-block', 5764 height : tableHeight, 5765 left : $header.position().left - startPosition + $header.outerWidth() - handleCenter 5766 }); 5767 } 5768 }); 5769 }, 5770 5771 // Fixes #1485 5772 checkVisibleColumns: function( c, column ) { 5773 var i, 5774 len = 0; 5775 for ( i = column + 1; i < c.columns; i++ ) { 5776 len += c.$headerIndexed[i].is( ':visible' ) ? 1 : 0; 5777 } 5778 return len === 0; 5779 }, 5780 5781 // prevent text selection while dragging resize bar 5782 toggleTextSelection : function( c, wo, toggle ) { 5783 var namespace = c.namespace + 'tsresize'; 5784 wo.resizable_vars.disabled = toggle; 5785 $( 'body' ).toggleClass( ts.css.resizableNoSelect, toggle ); 5786 if ( toggle ) { 5787 $( 'body' ) 5788 .attr( 'unselectable', 'on' ) 5789 .bind( 'selectstart' + namespace, false ); 5790 } else { 5791 $( 'body' ) 5792 .removeAttr( 'unselectable' ) 5793 .unbind( 'selectstart' + namespace ); 5794 } 5795 }, 5796 5797 bindings : function( c, wo ) { 5798 var namespace = c.namespace + 'tsresize'; 5799 wo.$resizable_container.children().bind( 'mousedown', function( event ) { 5800 // save header cell and mouse position 5801 var column, 5802 vars = wo.resizable_vars, 5803 $extras = $( c.namespace + '_extra_headers' ), 5804 $header = $( event.target ).data( 'header' ); 5805 5806 column = parseInt( $header.attr( 'data-column' ), 10 ); 5807 vars.$target = $header = $header.add( $extras.filter('[data-column="' + column + '"]') ); 5808 vars.target = column; 5809 5810 // if table is not as wide as it's parent, then resize the table 5811 vars.$next = event.shiftKey || wo.resizable_targetLast ? 5812 $header.parent().children().not( '.resizable-false' ).filter( ':last' ) : 5813 $header.nextAll( ':not(.resizable-false)' ).eq( 0 ); 5814 5815 column = parseInt( vars.$next.attr( 'data-column' ), 10 ); 5816 vars.$next = vars.$next.add( $extras.filter('[data-column="' + column + '"]') ); 5817 vars.next = column; 5818 5819 vars.mouseXPosition = event.pageX; 5820 ts.resizable.updateStoredSizes( c, wo ); 5821 ts.resizable.toggleTextSelection(c, wo, true ); 5822 }); 5823 5824 $( document ) 5825 .bind( 'mousemove' + namespace, function( event ) { 5826 var vars = wo.resizable_vars; 5827 // ignore mousemove if no mousedown 5828 if ( !vars.disabled || vars.mouseXPosition === 0 || !vars.$target ) { return; } 5829 if ( wo.resizable_throttle ) { 5830 clearTimeout( vars.timer ); 5831 vars.timer = setTimeout( function() { 5832 ts.resizable.mouseMove( c, wo, event ); 5833 }, isNaN( wo.resizable_throttle ) ? 5 : wo.resizable_throttle ); 5834 } else { 5835 ts.resizable.mouseMove( c, wo, event ); 5836 } 5837 }) 5838 .bind( 'mouseup' + namespace, function() { 5839 if (!wo.resizable_vars.disabled) { return; } 5840 ts.resizable.toggleTextSelection( c, wo, false ); 5841 ts.resizable.stopResize( c, wo ); 5842 ts.resizable.setHandlePosition( c, wo ); 5843 }); 5844 5845 // resizeEnd event triggered by scroller widget 5846 $( window ).bind( 'resize' + namespace + ' resizeEnd' + namespace, function() { 5847 ts.resizable.setHandlePosition( c, wo ); 5848 }); 5849 5850 // right click to reset columns to default widths 5851 c.$table 5852 .bind( 'columnUpdate pagerComplete resizableUpdate '.split( ' ' ).join( namespace + ' ' ), function() { 5853 ts.resizable.setHandlePosition( c, wo ); 5854 }) 5855 .bind( 'resizableReset' + namespace, function() { 5856 ts.resizableReset( c.table ); 5857 }) 5858 .find( 'thead:first' ) 5859 .add( $( c.namespace + '_extra_table' ).find( 'thead:first' ) ) 5860 .bind( 'contextmenu' + namespace, function() { 5861 // $.isEmptyObject() needs jQuery 1.4+; allow right click if already reset 5862 var allowClick = wo.resizable_vars.storedSizes.length === 0; 5863 ts.resizableReset( c.table ); 5864 ts.resizable.setHandlePosition( c, wo ); 5865 wo.resizable_vars.storedSizes = []; 5866 return allowClick; 5867 }); 5868 5869 }, 5870 5871 mouseMove : function( c, wo, event ) { 5872 if ( wo.resizable_vars.mouseXPosition === 0 || !wo.resizable_vars.$target ) { return; } 5873 // resize columns 5874 var column, 5875 total = 0, 5876 vars = wo.resizable_vars, 5877 $next = vars.$next, 5878 tar = vars.storedSizes[ vars.target ], 5879 leftEdge = event.pageX - vars.mouseXPosition; 5880 if ( vars.overflow ) { 5881 if ( tar + leftEdge > 0 ) { 5882 vars.storedSizes[ vars.target ] += leftEdge; 5883 ts.resizable.setWidth( vars.$target, vars.storedSizes[ vars.target ], true ); 5884 // update the entire table width 5885 for ( column = 0; column < c.columns; column++ ) { 5886 total += vars.storedSizes[ column ]; 5887 } 5888 ts.resizable.setWidth( c.$table.add( $( c.namespace + '_extra_table' ) ), total ); 5889 } 5890 if ( !$next.length ) { 5891 // if expanding right-most column, scroll the wrapper 5892 vars.$wrap[0].scrollLeft = c.$table.width(); 5893 } 5894 } else if ( vars.fullWidth ) { 5895 vars.storedSizes[ vars.target ] += leftEdge; 5896 vars.storedSizes[ vars.next ] -= leftEdge; 5897 ts.resizable.setWidths( c, wo ); 5898 } else { 5899 vars.storedSizes[ vars.target ] += leftEdge; 5900 ts.resizable.setWidths( c, wo ); 5901 } 5902 vars.mouseXPosition = event.pageX; 5903 // dynamically update sticky header widths 5904 c.$table.triggerHandler('stickyHeadersUpdate'); 5905 }, 5906 5907 stopResize : function( c, wo ) { 5908 var vars = wo.resizable_vars; 5909 ts.resizable.updateStoredSizes( c, wo ); 5910 if ( vars.useStorage ) { 5911 // save all column widths 5912 ts.storage( c.table, ts.css.resizableStorage, vars.storedSizes ); 5913 ts.storage( c.table, 'tablesorter-table-resized-width', c.$table.width() ); 5914 } 5915 vars.mouseXPosition = 0; 5916 vars.$target = vars.$next = null; 5917 // will update stickyHeaders, just in case, see #912 5918 c.$table.triggerHandler('stickyHeadersUpdate'); 5919 c.$table.triggerHandler('resizableComplete'); 5920 } 5921 }; 5922 5923 // this widget saves the column widths if 5924 // $.tablesorter.storage function is included 5925 // ************************** 5926 ts.addWidget({ 5927 id: 'resizable', 5928 priority: 40, 5929 options: { 5930 resizable : true, // save column widths to storage 5931 resizable_addLastColumn : false, 5932 resizable_includeFooter: true, 5933 resizable_widths : [], 5934 resizable_throttle : false, // set to true (5ms) or any number 0-10 range 5935 resizable_targetLast : false 5936 }, 5937 init: function(table, thisWidget, c, wo) { 5938 ts.resizable.init( c, wo ); 5939 }, 5940 format: function( table, c, wo ) { 5941 ts.resizable.setHandlePosition( c, wo ); 5942 }, 5943 remove: function( table, c, wo, refreshing ) { 5944 if (wo.$resizable_container) { 5945 var namespace = c.namespace + 'tsresize'; 5946 c.$table.add( $( c.namespace + '_extra_table' ) ) 5947 .removeClass('hasResizable') 5948 .children( 'thead' ) 5949 .unbind( 'contextmenu' + namespace ); 5950 5951 wo.$resizable_container.remove(); 5952 ts.resizable.toggleTextSelection( c, wo, false ); 5953 ts.resizableReset( table, refreshing ); 5954 $( document ).unbind( 'mousemove' + namespace + ' mouseup' + namespace ); 5955 } 5956 } 5957 }); 5958 5959 ts.resizableReset = function( table, refreshing ) { 5960 $( table ).each(function() { 5961 var index, $t, 5962 c = this.config, 5963 wo = c && c.widgetOptions, 5964 vars = wo.resizable_vars; 5965 if ( table && c && c.$headerIndexed.length ) { 5966 // restore the initial table width 5967 if ( vars.overflow && vars.tableWidth ) { 5968 ts.resizable.setWidth( c.$table, vars.tableWidth, true ); 5969 if ( vars.useStorage ) { 5970 ts.storage( table, 'tablesorter-table-resized-width', vars.tableWidth ); 5971 } 5972 } 5973 for ( index = 0; index < c.columns; index++ ) { 5974 $t = c.$headerIndexed[ index ]; 5975 if ( wo.resizable_widths && wo.resizable_widths[ index ] ) { 5976 ts.resizable.setWidth( $t, wo.resizable_widths[ index ], vars.overflow ); 5977 } else if ( !$t.hasClass( 'resizable-false' ) ) { 5978 // don't clear the width of any column that is not resizable 5979 ts.resizable.setWidth( $t, '', vars.overflow ); 5980 } 5981 } 5982 5983 // reset stickyHeader widths 5984 c.$table.triggerHandler( 'stickyHeadersUpdate' ); 5985 if ( ts.storage && !refreshing ) { 5986 ts.storage( this, ts.css.resizableStorage, [] ); 5987 } 5988 } 5989 }); 5990 }; 5991 5992})( jQuery, window ); 5993 5994/*! Widget: saveSort - updated 2018-03-19 (v2.30.1) *//* 5995* Requires tablesorter v2.16+ 5996* by Rob Garrison 5997*/ 5998;(function ($) { 5999 'use strict'; 6000 var ts = $.tablesorter || {}; 6001 6002 function getStoredSortList(c) { 6003 var stored = ts.storage( c.table, 'tablesorter-savesort' ); 6004 return (stored && stored.hasOwnProperty('sortList') && $.isArray(stored.sortList)) ? stored.sortList : []; 6005 } 6006 6007 function sortListChanged(c, sortList) { 6008 return (sortList || getStoredSortList(c)).join(',') !== c.sortList.join(','); 6009 } 6010 6011 // this widget saves the last sort only if the 6012 // saveSort widget option is true AND the 6013 // $.tablesorter.storage function is included 6014 // ************************** 6015 ts.addWidget({ 6016 id: 'saveSort', 6017 priority: 20, 6018 options: { 6019 saveSort : true 6020 }, 6021 init: function(table, thisWidget, c, wo) { 6022 // run widget format before all other widgets are applied to the table 6023 thisWidget.format(table, c, wo, true); 6024 }, 6025 format: function(table, c, wo, init) { 6026 var time, 6027 $table = c.$table, 6028 saveSort = wo.saveSort !== false, // make saveSort active/inactive; default to true 6029 sortList = { 'sortList' : c.sortList }, 6030 debug = ts.debug(c, 'saveSort'); 6031 if (debug) { 6032 time = new Date(); 6033 } 6034 if ($table.hasClass('hasSaveSort')) { 6035 if (saveSort && table.hasInitialized && ts.storage && sortListChanged(c)) { 6036 ts.storage( table, 'tablesorter-savesort', sortList ); 6037 if (debug) { 6038 console.log('saveSort >> Saving last sort: ' + c.sortList + ts.benchmark(time)); 6039 } 6040 } 6041 } else { 6042 // set table sort on initial run of the widget 6043 $table.addClass('hasSaveSort'); 6044 sortList = ''; 6045 // get data 6046 if (ts.storage) { 6047 sortList = getStoredSortList(c); 6048 if (debug) { 6049 console.log('saveSort >> Last sort loaded: "' + sortList + '"' + ts.benchmark(time)); 6050 } 6051 $table.bind('saveSortReset', function(event) { 6052 event.stopPropagation(); 6053 ts.storage( table, 'tablesorter-savesort', '' ); 6054 }); 6055 } 6056 // init is true when widget init is run, this will run this widget before all other widgets have initialized 6057 // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice. 6058 if (init && sortList && sortList.length > 0) { 6059 c.sortList = sortList; 6060 } else if (table.hasInitialized && sortList && sortList.length > 0) { 6061 // update sort change 6062 if (sortListChanged(c, sortList)) { 6063 ts.sortOn(c, sortList); 6064 } 6065 } 6066 } 6067 }, 6068 remove: function(table, c) { 6069 c.$table.removeClass('hasSaveSort'); 6070 // clear storage 6071 if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); } 6072 } 6073 }); 6074 6075})(jQuery); 6076 6077return jQuery.tablesorter; 6078}
vendor: 4 bytes, line 6078
6078));
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.