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