vendor: 6,376 bytes, lines 1-143
1/*** This file is dynamically generated *** 2ââââââ ââââââ ââââââ ââââââ ââââââ ââââââââ ââââââ ââââââ ââ ââââââ ââ ââ 3ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââââ ââââââ 4ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââââ âââââ 5ââââââ ââââââ ââ ââ ââââââ ââ ââ ââ ââ ââââââ ââââââ ââ ââ ââââââ 6*/ 7/*! tablesorter (FORK) - updated 2020-03-03 (v2.31.3)*/ 8/* Includes widgets ( storage,uitheme,columns,filter,stickyHeaders,resizable,saveSort ) */ 9(function(factory){if (typeof define === 'function' && define.amd){define(['jquery'], factory);} else if (typeof module === 'object' && typeof module.exports === 'object'){module.exports = factory(require('jquery'));} else {factory(jQuery);}}(function(jQuery) { 10/*! Widget: storage - updated 2018-03-18 (v2.30.0) */ 11/*global JSON:false */ 12;(function ($, window, document) { 13 'use strict'; 14 15 var ts = $.tablesorter || {}; 16 17 // update defaults for validator; these values must be falsy! 18 $.extend(true, ts.defaults, { 19 fixedUrl: '', 20 widgetOptions: { 21 storage_fixedUrl: '', 22 storage_group: '', 23 storage_page: '', 24 storage_storageType: '', 25 storage_tableId: '', 26 storage_useSessionStorage: '' 27 } 28 }); 29 30 // *** Store data in local storage, with a cookie fallback *** 31 /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json) 32 if you need it, then include https://github.com/douglascrockford/JSON-js 33 34 $.parseJSON is not available is jQuery versions older than 1.4.1, using older 35 versions will only allow storing information for one page at a time 36 37 // *** Save data (JSON format only) *** 38 // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid 39 var val = { "mywidget" : "data1" }; // valid JSON uses double quotes 40 // $.tablesorter.storage(table, key, val); 41 $.tablesorter.storage(table, 'tablesorter-mywidget', val); 42 43 // *** Get data: $.tablesorter.storage(table, key); *** 44 v = $.tablesorter.storage(table, 'tablesorter-mywidget'); 45 // val may be empty, so also check for your data 46 val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : ''; 47 alert(val); // 'data1' if saved, or '' if not 48 */ 49 ts.storage = function(table, key, value, options) { 50 table = $(table)[0]; 51 var cookieIndex, cookies, date, 52 hasStorage = false, 53 values = {}, 54 c = table.config, 55 wo = c && c.widgetOptions, 56 debug = ts.debug(c, 'storage'), 57 storageType = ( 58 ( options && options.storageType ) || ( wo && wo.storage_storageType ) 59 ).toString().charAt(0).toLowerCase(), 60 // deprecating "useSessionStorage"; any storageType setting overrides it 61 session = storageType ? '' : 62 ( options && options.useSessionStorage ) || ( wo && wo.storage_useSessionStorage ), 63 $table = $(table), 64 // id from (1) options ID, (2) table 'data-table-group' attribute, (3) widgetOptions.storage_tableId, 65 // (4) table ID, then (5) table index 66 id = options && options.id || 67 $table.attr( options && options.group || wo && wo.storage_group || 'data-table-group') || 68 wo && wo.storage_tableId || table.id || $('.tablesorter').index( $table ), 69 // url from (1) options url, (2) table 'data-table-page' attribute, (3) widgetOptions.storage_fixedUrl, 70 // (4) table.config.fixedUrl (deprecated), then (5) window location path 71 url = options && options.url || 72 $table.attr(options && options.page || wo && wo.storage_page || 'data-table-page') || 73 wo && wo.storage_fixedUrl || c && c.fixedUrl || window.location.pathname; 74 75 // skip if using cookies 76 if (storageType !== 'c') { 77 storageType = (storageType === 's' || session) ? 'sessionStorage' : 'localStorage'; 78 // https://gist.github.com/paulirish/5558557 79 if (storageType in window) { 80 try { 81 window[storageType].setItem('_tmptest', 'temp'); 82 hasStorage = true; 83 window[storageType].removeItem('_tmptest'); 84 } catch (error) { 85 console.warn( storageType + ' is not supported in this browser' ); 86 } 87 } 88 } 89 if (debug) { 90 console.log('Storage >> Using', hasStorage ? storageType : 'cookies'); 91 } 92 // *** get value *** 93 if ($.parseJSON) { 94 if (hasStorage) { 95 values = $.parseJSON( window[storageType][key] || 'null' ) || {}; 96 } else { 97 // old browser, using cookies 98 cookies = document.cookie.split(/[;\s|=]/); 99 // add one to get from the key to the value 100 cookieIndex = $.inArray(key, cookies) + 1; 101 values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || 'null') || {} : {}; 102 } 103 } 104 // allow value to be an empty string too 105 if (typeof value !== 'undefined' && window.JSON && JSON.hasOwnProperty('stringify')) { 106 // add unique identifiers = url pathname > table ID/index on page > data 107 if (!values[url]) { 108 values[url] = {}; 109 } 110 values[url][id] = value; 111 // *** set value *** 112 if (hasStorage) { 113 window[storageType][key] = JSON.stringify(values); 114 } else { 115 date = new Date(); 116 date.setTime(date.getTime() + (31536e+6)); // 365 days 117 document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g, '\"') + '; expires=' + date.toGMTString() + '; path=/'; 118 } 119 } else { 120 return values && values[url] ? values[url][id] : ''; 121 } 122 }; 123 124})(jQuery, window, document); 125 126/*! Widget: uitheme - updated 2018-03-18 (v2.30.0) */ 127;(function ($) { 128 'use strict'; 129 var ts = $.tablesorter || {}; 130 131 ts.themes = { 132 'bootstrap' : { 133 table : 'table table-bordered table-striped', 134 caption : 'caption', 135 // header class names 136 header : 'bootstrap-header', // give the header a gradient background (theme.bootstrap_2.css) 137 sortNone : '', 138 sortAsc : '', 139 sortDesc : '', 140 active : '', // applied when column is sorted 141 hover : '', // custom css required - a defined bootstrap style may not override other classes 142 // icon class names 143 icons : '', // add 'bootstrap-icon-white' to make them white; this icon class is added to the <i> in the header
vendor: 120,735 bytes, lines 144-3184
144 iconSortNone : 'bootstrap-icon-unsorted', // class name added to icon when column is not sorted 145 iconSortAsc : 'glyphicon glyphicon-chevron-up', // class name added to icon when column has ascending sort 146 iconSortDesc : 'glyphicon glyphicon-chevron-down', // class name added to icon when column has descending sort 147 filterRow : '', // filter row class 148 footerRow : '', 149 footerCells : '', 150 even : '', // even row zebra striping 151 odd : '' // odd row zebra striping 152 }, 153 'jui' : { 154 table : 'ui-widget ui-widget-content ui-corner-all', // table classes 155 caption : 'ui-widget-content', 156 // header class names 157 header : 'ui-widget-header ui-corner-all ui-state-default', // header classes 158 sortNone : '', 159 sortAsc : '', 160 sortDesc : '', 161 active : 'ui-state-active', // applied when column is sorted 162 hover : 'ui-state-hover', // hover class 163 // icon class names 164 icons : 'ui-icon', // icon class added to the <i> in the header 165 iconSortNone : 'ui-icon-carat-2-n-s ui-icon-caret-2-n-s', // class name added to icon when column is not sorted 166 iconSortAsc : 'ui-icon-carat-1-n ui-icon-caret-1-n', // class name added to icon when column has ascending sort 167 iconSortDesc : 'ui-icon-carat-1-s ui-icon-caret-1-s', // class name added to icon when column has descending sort 168 filterRow : '', 169 footerRow : '', 170 footerCells : '', 171 even : 'ui-widget-content', // even row zebra striping 172 odd : 'ui-state-default' // odd row zebra striping 173 } 174 }; 175 176 $.extend(ts.css, { 177 wrapper : 'tablesorter-wrapper' // ui theme & resizable 178 }); 179 180 ts.addWidget({ 181 id: 'uitheme', 182 priority: 10, 183 format: function(table, c, wo) { 184 var i, tmp, hdr, icon, time, $header, $icon, $tfoot, $h, oldtheme, oldremove, oldIconRmv, hasOldTheme, 185 themesAll = ts.themes, 186 $table = c.$table.add( $( c.namespace + '_extra_table' ) ), 187 $headers = c.$headers.add( $( c.namespace + '_extra_headers' ) ), 188 theme = c.theme || 'jui', 189 themes = themesAll[theme] || {}, 190 remove = $.trim( [ themes.sortNone, themes.sortDesc, themes.sortAsc, themes.active ].join( ' ' ) ), 191 iconRmv = $.trim( [ themes.iconSortNone, themes.iconSortDesc, themes.iconSortAsc ].join( ' ' ) ), 192 debug = ts.debug(c, 'uitheme'); 193 if (debug) { time = new Date(); } 194 // initialization code - run once 195 if (!$table.hasClass('tablesorter-' + theme) || c.theme !== c.appliedTheme || !wo.uitheme_applied) { 196 wo.uitheme_applied = true; 197 oldtheme = themesAll[c.appliedTheme] || {}; 198 hasOldTheme = !$.isEmptyObject(oldtheme); 199 oldremove = hasOldTheme ? [ oldtheme.sortNone, oldtheme.sortDesc, oldtheme.sortAsc, oldtheme.active ].join( ' ' ) : ''; 200 oldIconRmv = hasOldTheme ? [ oldtheme.iconSortNone, oldtheme.iconSortDesc, oldtheme.iconSortAsc ].join( ' ' ) : ''; 201 if (hasOldTheme) { 202 wo.zebra[0] = $.trim( ' ' + wo.zebra[0].replace(' ' + oldtheme.even, '') ); 203 wo.zebra[1] = $.trim( ' ' + wo.zebra[1].replace(' ' + oldtheme.odd, '') ); 204 c.$tbodies.children().removeClass( [ oldtheme.even, oldtheme.odd ].join(' ') ); 205 } 206 // update zebra stripes 207 if (themes.even) { wo.zebra[0] += ' ' + themes.even; } 208 if (themes.odd) { wo.zebra[1] += ' ' + themes.odd; } 209 // add caption style 210 $table.children('caption') 211 .removeClass(oldtheme.caption || '') 212 .addClass(themes.caption); 213 // add table/footer class names 214 $tfoot = $table 215 // remove other selected themes 216 .removeClass( (c.appliedTheme ? 'tablesorter-' + (c.appliedTheme || '') : '') + ' ' + (oldtheme.table || '') ) 217 .addClass('tablesorter-' + theme + ' ' + (themes.table || '')) // add theme widget class name 218 .children('tfoot'); 219 c.appliedTheme = c.theme; 220 221 if ($tfoot.length) { 222 $tfoot 223 // if oldtheme.footerRow or oldtheme.footerCells are undefined, all class names are removed 224 .children('tr').removeClass(oldtheme.footerRow || '').addClass(themes.footerRow) 225 .children('th, td').removeClass(oldtheme.footerCells || '').addClass(themes.footerCells); 226 } 227 // update header classes 228 $headers 229 .removeClass( (hasOldTheme ? [ oldtheme.header, oldtheme.hover, oldremove ].join(' ') : '') || '' ) 230 .addClass(themes.header) 231 .not('.sorter-false') 232 .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') 233 .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) { 234 // toggleClass with switch added in jQuery 1.3 235 $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover || ''); 236 }); 237 238 $headers.each(function() { 239 var $this = $(this); 240 if (!$this.find('.' + ts.css.wrapper).length) { 241 // Firefox needs this inner div to position the icon & resizer correctly 242 $this.wrapInner('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>'); 243 } 244 }); 245 if (c.cssIcon) { 246 // if c.cssIcon is '', then no <i> is added to the header 247 $headers 248 .find('.' + ts.css.icon) 249 .removeClass(hasOldTheme ? [ oldtheme.icons, oldIconRmv ].join(' ') : '') 250 .addClass(themes.icons || ''); 251 } 252 // filter widget initializes after uitheme 253 if (ts.hasWidget( c.table, 'filter' )) { 254 tmp = function() { 255 $table.children('thead').children('.' + ts.css.filterRow) 256 .removeClass(hasOldTheme ? oldtheme.filterRow || '' : '') 257 .addClass(themes.filterRow || ''); 258 }; 259 if (wo.filter_initialized) { 260 tmp(); 261 } else { 262 $table.one('filterInit', function() { 263 tmp(); 264 }); 265 } 266 } 267 } 268 for (i = 0; i < c.columns; i++) { 269 $header = c.$headers 270 .add($(c.namespace + '_extra_headers')) 271 .not('.sorter-false') 272 .filter('[data-column="' + i + '"]'); 273 $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $(); 274 $h = $headers.not('.sorter-false').filter('[data-column="' + i + '"]:last'); 275 if ($h.length) { 276 $header.removeClass(remove); 277 $icon.removeClass(iconRmv); 278 if ($h[0].sortDisabled) { 279 // no sort arrows for disabled columns! 280 $icon.removeClass(themes.icons || ''); 281 } else { 282 hdr = themes.sortNone; 283 icon = themes.iconSortNone; 284 if ($h.hasClass(ts.css.sortAsc)) { 285 hdr = [ themes.sortAsc, themes.active ].join(' '); 286 icon = themes.iconSortAsc; 287 } else if ($h.hasClass(ts.css.sortDesc)) { 288 hdr = [ themes.sortDesc, themes.active ].join(' '); 289 icon = themes.iconSortDesc; 290 } 291 $header.addClass(hdr); 292 $icon.addClass(icon || ''); 293 } 294 } 295 } 296 if (debug) { 297 console.log('uitheme >> Applied ' + theme + ' theme' + ts.benchmark(time)); 298 } 299 }, 300 remove: function(table, c, wo, refreshing) { 301 if (!wo.uitheme_applied) { return; } 302 var $table = c.$table, 303 theme = c.appliedTheme || 'jui', 304 themes = ts.themes[ theme ] || ts.themes.jui, 305 $headers = $table.children('thead').children(), 306 remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc, 307 iconRmv = themes.iconSortNone + ' ' + themes.iconSortDesc + ' ' + themes.iconSortAsc; 308 $table.removeClass('tablesorter-' + theme + ' ' + themes.table); 309 wo.uitheme_applied = false; 310 if (refreshing) { return; } 311 $table.find(ts.css.header).removeClass(themes.header); 312 $headers 313 .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover 314 .removeClass(themes.hover + ' ' + remove + ' ' + themes.active) 315 .filter('.' + ts.css.filterRow) 316 .removeClass(themes.filterRow); 317 $headers.find('.' + ts.css.icon).removeClass(themes.icons + ' ' + iconRmv); 318 } 319 }); 320 321})(jQuery); 322 323/*! Widget: columns - updated 5/24/2017 (v2.28.11) */ 324;(function ($) { 325 'use strict'; 326 var ts = $.tablesorter || {}; 327 328 ts.addWidget({ 329 id: 'columns', 330 priority: 65, 331 options : { 332 columns : [ 'primary', 'secondary', 'tertiary' ] 333 }, 334 format: function(table, c, wo) { 335 var $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx, 336 $table = c.$table, 337 $tbodies = c.$tbodies, 338 sortList = c.sortList, 339 len = sortList.length, 340 // removed c.widgetColumns support 341 css = wo && wo.columns || [ 'primary', 'secondary', 'tertiary' ], 342 last = css.length - 1; 343 remove = css.join(' '); 344 // check if there is a sort (on initialization there may not be one) 345 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 346 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody 347 $rows = $tbody.children('tr'); 348 // loop through the visible rows 349 $rows.each(function() { 350 $row = $(this); 351 if (this.style.display !== 'none') { 352 // remove all columns class names 353 $cells = $row.children().removeClass(remove); 354 // add appropriate column class names 355 if (sortList && sortList[0]) { 356 // primary sort column class 357 $cells.eq(sortList[0][0]).addClass(css[0]); 358 if (len > 1) { 359 for (indx = 1; indx < len; indx++) { 360 // secondary, tertiary, etc sort column classes 361 $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] ); 362 } 363 } 364 } 365 } 366 }); 367 ts.processTbody(table, $tbody, false); 368 } 369 // add classes to thead and tfoot 370 rows = wo.columns_thead !== false ? [ 'thead tr' ] : []; 371 if (wo.columns_tfoot !== false) { 372 rows.push('tfoot tr'); 373 } 374 if (rows.length) { 375 $rows = $table.find( rows.join(',') ).children().removeClass(remove); 376 if (len) { 377 for (indx = 0; indx < len; indx++) { 378 // add primary. secondary, tertiary, etc sort column classes 379 $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]); 380 } 381 } 382 } 383 }, 384 remove: function(table, c, wo) { 385 var tbodyIndex, $tbody, 386 $tbodies = c.$tbodies, 387 remove = (wo.columns || [ 'primary', 'secondary', 'tertiary' ]).join(' '); 388 c.$headers.removeClass(remove); 389 c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove); 390 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 391 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody 392 $tbody.children('tr').each(function() { 393 $(this).children().removeClass(remove); 394 }); 395 ts.processTbody(table, $tbody, false); // restore tbody 396 } 397 } 398 }); 399 400})(jQuery); 401 402/*! Widget: filter - updated 2018-03-18 (v2.30.0) *//* 403 * Requires tablesorter v2.8+ and jQuery 1.7+ 404 * by Rob Garrison 405 */ 406;( function ( $ ) { 407 'use strict'; 408 var tsf, tsfRegex, 409 ts = $.tablesorter || {}, 410 tscss = ts.css, 411 tskeyCodes = ts.keyCodes; 412 413 $.extend( tscss, { 414 filterRow : 'tablesorter-filter-row', 415 filter : 'tablesorter-filter', 416 filterDisabled : 'disabled', 417 filterRowHide : 'hideme' 418 }); 419 420 $.extend( tskeyCodes, { 421 backSpace : 8, 422 escape : 27, 423 space : 32, 424 left : 37, 425 down : 40 426 }); 427 428 ts.addWidget({ 429 id: 'filter', 430 priority: 50, 431 options : { 432 filter_cellFilter : '', // css class name added to the filter cell ( string or array ) 433 filter_childRows : false, // if true, filter includes child row content in the search 434 filter_childByColumn : false, // ( filter_childRows must be true ) if true = search child rows by column; false = search all child row text grouped 435 filter_childWithSibs : true, // if true, include matching child row siblings 436 filter_columnAnyMatch: true, // if true, allows using '#:{query}' in AnyMatch searches ( column:query ) 437 filter_columnFilters : true, // if true, a filter will be added to the top of each table column 438 filter_cssFilter : '', // css class name added to the filter row & each input in the row ( tablesorter-filter is ALWAYS added ) 439 filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value 440 filter_defaultFilter : {}, // add a default column filter type '~{query}' to make fuzzy searches default; '{q1} AND {q2}' to make all searches use a logical AND. 441 filter_excludeFilter : {}, // filters to exclude, per column 442 filter_external : '', // jQuery selector string ( or jQuery object ) of external filters 443 filter_filteredRow : 'filtered', // class added to filtered rows; define in css with "display:none" to hide the filtered-out rows 444 filter_filterLabel : 'Filter "{{label}}" column by...', // Aria-label added to filter input/select; see #1495 445 filter_formatter : null, // add custom filter elements to the filter row 446 filter_functions : null, // add custom filter functions using this option 447 filter_hideEmpty : true, // hide filter row when table is empty 448 filter_hideFilters : false, // collapse filter row when mouse leaves the area 449 filter_ignoreCase : true, // if true, make all searches case-insensitive 450 filter_liveSearch : true, // if true, search column content while the user types ( with a delay ) 451 filter_matchType : { 'input': 'exact', 'select': 'exact' }, // global query settings ('exact' or 'match'); overridden by "filter-match" or "filter-exact" class 452 filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available ( visible ) options within the drop down 453 filter_placeholder : { search : '', select : '' }, // default placeholder text ( overridden by any header 'data-placeholder' setting ) 454 filter_reset : null, // jQuery selector string of an element used to reset the filters 455 filter_resetOnEsc : true, // Reset filter input when the user presses escape - normalized across browsers 456 filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters 457 filter_searchDelay : 300, // typing delay in milliseconds before starting a search 458 filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true 459 filter_selectSource : null, // include a function to return an array of values to be added to the column filter select 460 filter_selectSourceSeparator : '|', // filter_selectSource array text left of the separator is added to the option value, right into the option text 461 filter_serversideFiltering : false, // if true, must perform server-side filtering b/c client-side filtering is disabled, but the ui and events will still be used. 462 filter_startsWith : false, // if true, filter start from the beginning of the cell contents 463 filter_useParsedData : false // filter all data using parsed content 464 }, 465 format: function( table, c, wo ) { 466 if ( !c.$table.hasClass( 'hasFilters' ) ) { 467 tsf.init( table, c, wo ); 468 } 469 }, 470 remove: function( table, c, wo, refreshing ) { 471 var tbodyIndex, $tbody, 472 $table = c.$table, 473 $tbodies = c.$tbodies, 474 events = ( 475 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + 476 'filterAndSortReset filterFomatterUpdate filterEnd search stickyHeadersInit ' 477 ).split( ' ' ).join( c.namespace + 'filter ' ); 478 $table 479 .removeClass( 'hasFilters' ) 480 // add filter namespace to all BUT search 481 .unbind( events.replace( ts.regex.spaces, ' ' ) ) 482 // remove the filter row even if refreshing, because the column might have been moved 483 .find( '.' + tscss.filterRow ).remove(); 484 wo.filter_initialized = false; 485 if ( refreshing ) { return; } 486 for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 487 $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody 488 $tbody.children().removeClass( wo.filter_filteredRow ).show(); 489 ts.processTbody( table, $tbody, false ); // restore tbody 490 } 491 if ( wo.filter_reset ) { 492 $( document ).undelegate( wo.filter_reset, 'click' + c.namespace + 'filter' ); 493 } 494 } 495 }); 496 497 tsf = ts.filter = { 498 499 // regex used in filter 'check' functions - not for general use and not documented 500 regex: { 501 regex : /^\/((?:\\\/|[^\/])+)\/([migyu]{0,5})?$/, // regex to test for regex 502 child : /tablesorter-childRow/, // child row class name; this gets updated in the script 503 filtered : /filtered/, // filtered (hidden) row class name; updated in the script 504 type : /undefined|number/, // check type 505 exact : /(^[\"\'=]+)|([\"\'=]+$)/g, // exact match (allow '==') 506 operators : /[<>=]/g, // replace operators 507 query : '(q|query)', // replace filter queries 508 wild01 : /\?/g, // wild card match 0 or 1 509 wild0More : /\*/g, // wild care match 0 or more 510 quote : /\"/g, 511 isNeg1 : /(>=?\s*-\d)/, 512 isNeg2 : /(<=?\s*\d)/ 513 }, 514 // function( c, data ) { } 515 // c = table.config 516 // data.$row = jQuery object of the row currently being processed 517 // data.$cells = jQuery object of all cells within the current row 518 // data.filters = array of filters for all columns ( some may be undefined ) 519 // data.filter = filter for the current column 520 // data.iFilter = same as data.filter, except lowercase ( if wo.filter_ignoreCase is true ) 521 // data.exact = table cell text ( or parsed data if column parser enabled; may be a number & not a string ) 522 // data.iExact = same as data.exact, except lowercase ( if wo.filter_ignoreCase is true; may be a number & not a string ) 523 // data.cache = table cell text from cache, so it has been parsed ( & in all lower case if c.ignoreCase is true ) 524 // data.cacheArray = An array of parsed content from each table cell in the row being processed 525 // data.index = column index; table = table element ( DOM ) 526 // data.parsed = array ( by column ) of boolean values ( from filter_useParsedData or 'filter-parsed' class ) 527 types: { 528 or : function( c, data, vars ) { 529 // look for "|", but not if it is inside of a regular expression 530 if ( ( tsfRegex.orTest.test( data.iFilter ) || tsfRegex.orSplit.test( data.filter ) ) && 531 // this test for regex has potential to slow down the overall search 532 !tsfRegex.regex.test( data.filter ) ) { 533 var indx, filterMatched, query, regex, 534 // duplicate data but split filter 535 data2 = $.extend( {}, data ), 536 filter = data.filter.split( tsfRegex.orSplit ), 537 iFilter = data.iFilter.split( tsfRegex.orSplit ), 538 len = filter.length; 539 for ( indx = 0; indx < len; indx++ ) { 540 data2.nestedFilters = true; 541 data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); 542 data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); 543 query = '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')'; 544 try { 545 // use try/catch, because query may not be a valid regex if "|" is contained within a partial regex search, 546 // e.g "/(Alex|Aar" -> Uncaught SyntaxError: Invalid regular expression: /(/(Alex)/: Unterminated group 547 regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); 548 // filterMatched = data2.filter === '' && indx > 0 ? true 549 // look for an exact match with the 'or' unless the 'filter-match' class is found 550 filterMatched = regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ); 551 if ( filterMatched ) { 552 return filterMatched; 553 } 554 } catch ( error ) { 555 return null; 556 } 557 } 558 // may be null from processing types 559 return filterMatched || false; 560 } 561 return null; 562 }, 563 // Look for an AND or && operator ( logical and ) 564 and : function( c, data, vars ) { 565 if ( tsfRegex.andTest.test( data.filter ) ) { 566 var indx, filterMatched, result, query, regex, 567 // duplicate data but split filter 568 data2 = $.extend( {}, data ), 569 filter = data.filter.split( tsfRegex.andSplit ), 570 iFilter = data.iFilter.split( tsfRegex.andSplit ), 571 len = filter.length; 572 for ( indx = 0; indx < len; indx++ ) { 573 data2.nestedFilters = true; 574 data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); 575 data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); 576 query = ( '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')' ) 577 // replace wild cards since /(a*)/i will match anything 578 .replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ); 579 try { 580 // use try/catch just in case RegExp is invalid 581 regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); 582 // look for an exact match with the 'and' unless the 'filter-match' class is found 583 result = ( regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ) ); 584 if ( indx === 0 ) { 585 filterMatched = result; 586 } else { 587 filterMatched = filterMatched && result; 588 } 589 } catch ( error ) { 590 return null; 591 } 592 } 593 // may be null from processing types 594 return filterMatched || false; 595 } 596 return null; 597 }, 598 // Look for regex 599 regex: function( c, data ) { 600 if ( tsfRegex.regex.test( data.filter ) ) { 601 var matches, 602 // cache regex per column for optimal speed 603 regex = data.filter_regexCache[ data.index ] || tsfRegex.regex.exec( data.filter ), 604 isRegex = regex instanceof RegExp; 605 try { 606 if ( !isRegex ) { 607 // force case insensitive search if ignoreCase option set? 608 // if ( c.ignoreCase && !regex[2] ) { regex[2] = 'i'; } 609 data.filter_regexCache[ data.index ] = regex = new RegExp( regex[1], regex[2] ); 610 } 611 matches = regex.test( data.exact ); 612 } catch ( error ) { 613 matches = false; 614 } 615 return matches; 616 } 617 return null; 618 }, 619 // Look for operators >, >=, < or <= 620 operators: function( c, data ) { 621 // ignore empty strings... because '' < 10 is true 622 if ( tsfRegex.operTest.test( data.iFilter ) && data.iExact !== '' ) { 623 var cachedValue, result, txt, 624 table = c.table, 625 parsed = data.parsed[ data.index ], 626 query = ts.formatFloat( data.iFilter.replace( tsfRegex.operators, '' ), table ), 627 parser = c.parsers[ data.index ] || {}, 628 savedSearch = query; 629 // parse filter value in case we're comparing numbers ( dates ) 630 if ( parsed || parser.type === 'numeric' ) { 631 txt = $.trim( '' + data.iFilter.replace( tsfRegex.operators, '' ) ); 632 result = tsf.parseFilter( c, txt, data, true ); 633 query = ( typeof result === 'number' && result !== '' && !isNaN( result ) ) ? result : query; 634 } 635 // iExact may be numeric - see issue #149; 636 // check if cached is defined, because sometimes j goes out of range? ( numeric columns ) 637 if ( ( parsed || parser.type === 'numeric' ) && !isNaN( query ) && 638 typeof data.cache !== 'undefined' ) { 639 cachedValue = data.cache; 640 } else { 641 txt = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; 642 cachedValue = ts.formatFloat( txt, table ); 643 } 644 if ( tsfRegex.gtTest.test( data.iFilter ) ) { 645 result = tsfRegex.gteTest.test( data.iFilter ) ? cachedValue >= query : cachedValue > query; 646 } else if ( tsfRegex.ltTest.test( data.iFilter ) ) { 647 result = tsfRegex.lteTest.test( data.iFilter ) ? cachedValue <= query : cachedValue < query; 648 } 649 // keep showing all rows if nothing follows the operator 650 if ( !result && savedSearch === '' ) { 651 result = true; 652 } 653 return result; 654 } 655 return null; 656 }, 657 // Look for a not match 658 notMatch: function( c, data ) { 659 if ( tsfRegex.notTest.test( data.iFilter ) ) { 660 var indx, 661 txt = data.iFilter.replace( '!', '' ), 662 filter = tsf.parseFilter( c, txt, data ) || ''; 663 if ( tsfRegex.exact.test( filter ) ) { 664 // look for exact not matches - see #628 665 filter = filter.replace( tsfRegex.exact, '' ); 666 return filter === '' ? true : $.trim( filter ) !== data.iExact; 667 } else { 668 indx = data.iExact.search( $.trim( filter ) ); 669 return filter === '' ? true : 670 // return true if not found 671 data.anyMatch ? indx < 0 : 672 // return false if found 673 !( c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 0 ); 674 } 675 } 676 return null; 677 }, 678 // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric 679 exact: function( c, data ) { 680 /*jshint eqeqeq:false */ 681 if ( tsfRegex.exact.test( data.iFilter ) ) { 682 var txt = data.iFilter.replace( tsfRegex.exact, '' ), 683 filter = tsf.parseFilter( c, txt, data ) || ''; 684 // eslint-disable-next-line eqeqeq 685 return data.anyMatch ? $.inArray( filter, data.rowArray ) >= 0 : filter == data.iExact; 686 } 687 return null; 688 }, 689 // Look for a range ( using ' to ' or ' - ' ) - see issue #166; thanks matzhu! 690 range : function( c, data ) { 691 if ( tsfRegex.toTest.test( data.iFilter ) ) { 692 var result, tmp, range1, range2, 693 table = c.table, 694 index = data.index, 695 parsed = data.parsed[index], 696 // make sure the dash is for a range and not indicating a negative number 697 query = data.iFilter.split( tsfRegex.toSplit ); 698 699 tmp = query[0].replace( ts.regex.nondigit, '' ) || ''; 700 range1 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); 701 tmp = query[1].replace( ts.regex.nondigit, '' ) || ''; 702 range2 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); 703 // parse filter value in case we're comparing numbers ( dates ) 704 if ( parsed || c.parsers[ index ].type === 'numeric' ) { 705 result = c.parsers[ index ].format( '' + query[0], table, c.$headers.eq( index ), index ); 706 range1 = ( result !== '' && !isNaN( result ) ) ? result : range1; 707 result = c.parsers[ index ].format( '' + query[1], table, c.$headers.eq( index ), index ); 708 range2 = ( result !== '' && !isNaN( result ) ) ? result : range2; 709 } 710 if ( ( parsed || c.parsers[ index ].type === 'numeric' ) && !isNaN( range1 ) && !isNaN( range2 ) ) { 711 result = data.cache; 712 } else { 713 tmp = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; 714 result = ts.formatFloat( tmp, table ); 715 } 716 if ( range1 > range2 ) { 717 tmp = range1; range1 = range2; range2 = tmp; // swap 718 } 719 return ( result >= range1 && result <= range2 ) || ( range1 === '' || range2 === '' ); 720 } 721 return null; 722 }, 723 // Look for wild card: ? = single, * = multiple, or | = logical OR 724 wild : function( c, data ) { 725 if ( tsfRegex.wildOrTest.test( data.iFilter ) ) { 726 var query = '' + ( tsf.parseFilter( c, data.iFilter, data ) || '' ); 727 // look for an exact match with the 'or' unless the 'filter-match' class is found 728 if ( !tsfRegex.wildTest.test( query ) && data.nestedFilters ) { 729 query = data.isMatch ? query : '^(' + query + ')$'; 730 } 731 // parsing the filter may not work properly when using wildcards =/ 732 try { 733 return new RegExp( 734 query.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ), 735 c.widgetOptions.filter_ignoreCase ? 'i' : '' 736 ) 737 .test( data.exact ); 738 } catch ( error ) { 739 return null; 740 } 741 } 742 return null; 743 }, 744 // fuzzy text search; modified from https://github.com/mattyork/fuzzy ( MIT license ) 745 fuzzy: function( c, data ) { 746 if ( tsfRegex.fuzzyTest.test( data.iFilter ) ) { 747 var indx, 748 patternIndx = 0, 749 len = data.iExact.length, 750 txt = data.iFilter.slice( 1 ), 751 pattern = tsf.parseFilter( c, txt, data ) || ''; 752 for ( indx = 0; indx < len; indx++ ) { 753 if ( data.iExact[ indx ] === pattern[ patternIndx ] ) { 754 patternIndx += 1; 755 } 756 } 757 return patternIndx === pattern.length; 758 } 759 return null; 760 } 761 }, 762 init: function( table ) { 763 // filter language options 764 ts.language = $.extend( true, {}, { 765 to : 'to', 766 or : 'or', 767 and : 'and' 768 }, ts.language ); 769 770 var options, string, txt, $header, column, val, fxn, noSelect, 771 c = table.config, 772 wo = c.widgetOptions, 773 processStr = function(prefix, str, suffix) { 774 str = str.trim(); 775 // don't include prefix/suffix if str is empty 776 return str === '' ? '' : (prefix || '') + str + (suffix || ''); 777 }; 778 c.$table.addClass( 'hasFilters' ); 779 c.lastSearch = []; 780 781 // define timers so using clearTimeout won't cause an undefined error 782 wo.filter_searchTimer = null; 783 wo.filter_initTimer = null; 784 wo.filter_formatterCount = 0; 785 wo.filter_formatterInit = []; 786 wo.filter_anyColumnSelector = '[data-column="all"],[data-column="any"]'; 787 wo.filter_multipleColumnSelector = '[data-column*="-"],[data-column*=","]'; 788 789 val = '\\{' + tsfRegex.query + '\\}'; 790 $.extend( tsfRegex, { 791 child : new RegExp( c.cssChildRow ), 792 filtered : new RegExp( wo.filter_filteredRow ), 793 alreadyFiltered : new RegExp( '(\\s+(-' + processStr('|', ts.language.or) + processStr('|', ts.language.to) + ')\\s+)', 'i' ), 794 toTest : new RegExp( '\\s+(-' + processStr('|', ts.language.to) + ')\\s+', 'i' ), 795 toSplit : new RegExp( '(?:\\s+(?:-' + processStr('|', ts.language.to) + ')\\s+)', 'gi' ), 796 andTest : new RegExp( '\\s+(' + processStr('', ts.language.and, '|') + '&&)\\s+', 'i' ), 797 andSplit : new RegExp( '(?:\\s+(?:' + processStr('', ts.language.and, '|') + '&&)\\s+)', 'gi' ), 798 orTest : new RegExp( '(\\|' + processStr('|\\s+', ts.language.or, '\\s+') + ')', 'i' ), 799 orSplit : new RegExp( '(?:\\|' + processStr('|\\s+(?:', ts.language.or, ')\\s+') + ')', 'gi' ), 800 iQuery : new RegExp( val, 'i' ), 801 igQuery : new RegExp( val, 'ig' ), 802 operTest : /^[<>]=?/, 803 gtTest : />/, 804 gteTest : />=/, 805 ltTest : /</, 806 lteTest : /<=/, 807 notTest : /^\!/, 808 wildOrTest : /[\?\*\|]/, 809 wildTest : /\?\*/, 810 fuzzyTest : /^~/, 811 exactTest : /[=\"\|!]/ 812 }); 813 814 // don't build filter row if columnFilters is false or all columns are set to 'filter-false' 815 // see issue #156 816 val = c.$headers.filter( '.filter-false, .parser-false' ).length; 817 if ( wo.filter_columnFilters !== false && val !== c.$headers.length ) { 818 // build filter row 819 tsf.buildRow( table, c, wo ); 820 } 821 822 txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + 823 'filterAndSortReset filterResetSaved filterEnd search '.split( ' ' ).join( c.namespace + 'filter ' ); 824 c.$table.bind( txt, function( event, filter ) { 825 val = wo.filter_hideEmpty && 826 $.isEmptyObject( c.cache ) && 827 !( c.delayInit && event.type === 'appendCache' ); 828 // hide filter row using the 'filtered' class name 829 c.$table.find( '.' + tscss.filterRow ).toggleClass( wo.filter_filteredRow, val ); // fixes #450 830 if ( !/(search|filter)/.test( event.type ) ) { 831 event.stopPropagation(); 832 tsf.buildDefault( table, true ); 833 } 834 // Add filterAndSortReset - see #1361 835 if ( event.type === 'filterReset' || event.type === 'filterAndSortReset' ) { 836 c.$table.find( '.' + tscss.filter ).add( wo.filter_$externalFilters ).val( '' ); 837 if ( event.type === 'filterAndSortReset' ) { 838 ts.sortReset( this.config, function() { 839 tsf.searching( table, [] ); 840 }); 841 } else { 842 tsf.searching( table, [] ); 843 } 844 } else if ( event.type === 'filterResetSaved' ) { 845 ts.storage( table, 'tablesorter-filters', '' ); 846 } else if ( event.type === 'filterEnd' ) { 847 tsf.buildDefault( table, true ); 848 } else { 849 // send false argument to force a new search; otherwise if the filter hasn't changed, 850 // it will return 851 filter = event.type === 'search' ? filter : 852 event.type === 'updateComplete' ? c.$table.data( 'lastSearch' ) : ''; 853 if ( /(update|add)/.test( event.type ) && event.type !== 'updateComplete' ) { 854 // force a new search since content has changed 855 c.lastCombinedFilter = null; 856 c.lastSearch = []; 857 // update filterFormatters after update (& small delay) - Fixes #1237 858 setTimeout(function() { 859 c.$table.triggerHandler( 'filterFomatterUpdate' ); 860 }, 100); 861 } 862 // pass true ( skipFirst ) to prevent the tablesorter.setFilters function from skipping the first 863 // input ensures all inputs are updated when a search is triggered on the table 864 // $( 'table' ).trigger( 'search', [...] ); 865 tsf.searching( table, filter, true ); 866 } 867 return false; 868 }); 869 870 // reset button/link 871 if ( wo.filter_reset ) { 872 if ( wo.filter_reset instanceof $ ) { 873 // reset contains a jQuery object, bind to it 874 wo.filter_reset.click( function() { 875 c.$table.triggerHandler( 'filterReset' ); 876 }); 877 } else if ( $( wo.filter_reset ).length ) { 878 // reset is a jQuery selector, use event delegation 879 $( document ) 880 .undelegate( wo.filter_reset, 'click' + c.namespace + 'filter' ) 881 .delegate( wo.filter_reset, 'click' + c.namespace + 'filter', function() { 882 // trigger a reset event, so other functions ( filter_formatter ) know when to reset 883 c.$table.triggerHandler( 'filterReset' ); 884 }); 885 } 886 } 887 if ( wo.filter_functions ) { 888 for ( column = 0; column < c.columns; column++ ) { 889 fxn = ts.getColumnData( table, wo.filter_functions, column ); 890 if ( fxn ) { 891 // remove 'filter-select' from header otherwise the options added here are replaced with 892 // all options 893 $header = c.$headerIndexed[ column ].removeClass( 'filter-select' ); 894 // don't build select if 'filter-false' or 'parser-false' set 895 noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) ); 896 options = ''; 897 if ( fxn === true && noSelect ) { 898 tsf.buildSelect( table, column ); 899 } else if ( typeof fxn === 'object' && noSelect ) { 900 // add custom drop down list 901 for ( string in fxn ) { 902 if ( typeof string === 'string' ) { 903 options += options === '' ? 904 '<option value="">' + 905 ( $header.data( 'placeholder' ) || 906 $header.attr( 'data-placeholder' ) || 907 wo.filter_placeholder.select || 908 '' 909 ) + 910 '</option>' : ''; 911 val = string; 912 txt = string; 913 if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { 914 val = string.split( wo.filter_selectSourceSeparator ); 915 txt = val[1]; 916 val = val[0]; 917 } 918 options += '<option ' + 919 ( txt === val ? '' : 'data-function-name="' + string + '" ' ) + 920 'value="' + val + '">' + txt + '</option>'; 921 } 922 } 923 c.$table 924 .find( 'thead' ) 925 .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) 926 .append( options ); 927 txt = wo.filter_selectSource; 928 fxn = typeof txt === 'function' ? true : ts.getColumnData( table, txt, column ); 929 if ( fxn ) { 930 // updating so the extra options are appended 931 tsf.buildSelect( c.table, column, '', true, $header.hasClass( wo.filter_onlyAvail ) ); 932 } 933 } 934 } 935 } 936 } 937 // not really updating, but if the column has both the 'filter-select' class & 938 // filter_functions set to true, it would append the same options twice. 939 tsf.buildDefault( table, true ); 940 941 tsf.bindSearch( table, c.$table.find( '.' + tscss.filter ), true ); 942 if ( wo.filter_external ) { 943 tsf.bindSearch( table, wo.filter_external ); 944 } 945 946 if ( wo.filter_hideFilters ) { 947 tsf.hideFilters( c ); 948 } 949 950 // show processing icon 951 if ( c.showProcessing ) { 952 txt = 'filterStart filterEnd '.split( ' ' ).join( c.namespace + 'filter-sp ' ); 953 c.$table 954 .unbind( txt.replace( ts.regex.spaces, ' ' ) ) 955 .bind( txt, function( event, columns ) { 956 // only add processing to certain columns to all columns 957 $header = ( columns ) ? 958 c.$table 959 .find( '.' + tscss.header ) 960 .filter( '[data-column]' ) 961 .filter( function() { 962 return columns[ $( this ).data( 'column' ) ] !== ''; 963 }) : ''; 964 ts.isProcessing( table, event.type === 'filterStart', columns ? $header : '' ); 965 }); 966 } 967 968 // set filtered rows count ( intially unfiltered ) 969 c.filteredRows = c.totalRows; 970 971 // add default values 972 txt = 'tablesorter-initialized pagerBeforeInitialized '.split( ' ' ).join( c.namespace + 'filter ' ); 973 c.$table 974 .unbind( txt.replace( ts.regex.spaces, ' ' ) ) 975 .bind( txt, function() { 976 tsf.completeInit( this ); 977 }); 978 // if filter widget is added after pager has initialized; then set filter init flag 979 if ( c.pager && c.pager.initialized && !wo.filter_initialized ) { 980 c.$table.triggerHandler( 'filterFomatterUpdate' ); 981 setTimeout( function() { 982 tsf.filterInitComplete( c ); 983 }, 100 ); 984 } else if ( !wo.filter_initialized ) { 985 tsf.completeInit( table ); 986 } 987 }, 988 completeInit: function( table ) { 989 // redefine 'c' & 'wo' so they update properly inside this callback 990 var c = table.config, 991 wo = c.widgetOptions, 992 filters = tsf.setDefaults( table, c, wo ) || []; 993 if ( filters.length ) { 994 // prevent delayInit from triggering a cache build if filters are empty 995 if ( !( c.delayInit && filters.join( '' ) === '' ) ) { 996 ts.setFilters( table, filters, true ); 997 } 998 } 999 c.$table.triggerHandler( 'filterFomatterUpdate' ); 1000 // trigger init after setTimeout to prevent multiple filterStart/End/Init triggers 1001 setTimeout( function() { 1002 if ( !wo.filter_initialized ) { 1003 tsf.filterInitComplete( c ); 1004 } 1005 }, 100 ); 1006 }, 1007 1008 // $cell parameter, but not the config, is passed to the filter_formatters, 1009 // so we have to work with it instead 1010 formatterUpdated: function( $cell, column ) { 1011 // prevent error if $cell is undefined - see #1056 1012 var $table = $cell && $cell.closest( 'table' ); 1013 var config = $table.length && $table[0].config, 1014 wo = config && config.widgetOptions; 1015 if ( wo && !wo.filter_initialized ) { 1016 // add updates by column since this function 1017 // may be called numerous times before initialization 1018 wo.filter_formatterInit[ column ] = 1; 1019 } 1020 }, 1021 filterInitComplete: function( c ) { 1022 var indx, len, 1023 wo = c.widgetOptions, 1024 count = 0, 1025 completed = function() { 1026 wo.filter_initialized = true; 1027 // update lastSearch - it gets cleared often 1028 c.lastSearch = c.$table.data( 'lastSearch' ); 1029 c.$table.triggerHandler( 'filterInit', c ); 1030 tsf.findRows( c.table, c.lastSearch || [] ); 1031 if (ts.debug(c, 'filter')) { 1032 console.log('Filter >> Widget initialized'); 1033 } 1034 }; 1035 if ( $.isEmptyObject( wo.filter_formatter ) ) { 1036 completed(); 1037 } else { 1038 len = wo.filter_formatterInit.length; 1039 for ( indx = 0; indx < len; indx++ ) { 1040 if ( wo.filter_formatterInit[ indx ] === 1 ) { 1041 count++; 1042 } 1043 } 1044 clearTimeout( wo.filter_initTimer ); 1045 if ( !wo.filter_initialized && count === wo.filter_formatterCount ) { 1046 // filter widget initialized 1047 completed(); 1048 } else if ( !wo.filter_initialized ) { 1049 // fall back in case a filter_formatter doesn't call 1050 // $.tablesorter.filter.formatterUpdated( $cell, column ), and the count is off 1051 wo.filter_initTimer = setTimeout( function() { 1052 completed(); 1053 }, 500 ); 1054 } 1055 } 1056 }, 1057 // encode or decode filters for storage; see #1026 1058 processFilters: function( filters, encode ) { 1059 var indx, 1060 // fixes #1237; previously returning an encoded "filters" value 1061 result = [], 1062 mode = encode ? encodeURIComponent : decodeURIComponent, 1063 len = filters.length; 1064 for ( indx = 0; indx < len; indx++ ) { 1065 if ( filters[ indx ] ) { 1066 result[ indx ] = mode( filters[ indx ] ); 1067 } 1068 } 1069 return result; 1070 }, 1071 setDefaults: function( table, c, wo ) { 1072 var isArray, saved, indx, col, $filters, 1073 // get current ( default ) filters 1074 filters = ts.getFilters( table ) || []; 1075 if ( wo.filter_saveFilters && ts.storage ) { 1076 saved = ts.storage( table, 'tablesorter-filters' ) || []; 1077 isArray = $.isArray( saved ); 1078 // make sure we're not just getting an empty array 1079 if ( !( isArray && saved.join( '' ) === '' || !isArray ) ) { 1080 filters = tsf.processFilters( saved ); 1081 } 1082 } 1083 // if no filters saved, then check default settings 1084 if ( filters.join( '' ) === '' ) { 1085 // allow adding default setting to external filters 1086 $filters = c.$headers.add( wo.filter_$externalFilters ) 1087 .filter( '[' + wo.filter_defaultAttrib + ']' ); 1088 for ( indx = 0; indx <= c.columns; indx++ ) { 1089 // include data-column='all' external filters 1090 col = indx === c.columns ? 'all' : indx; 1091 filters[ indx ] = $filters 1092 .filter( '[data-column="' + col + '"]' ) 1093 .attr( wo.filter_defaultAttrib ) || filters[indx] || ''; 1094 } 1095 } 1096 c.$table.data( 'lastSearch', filters ); 1097 return filters; 1098 }, 1099 parseFilter: function( c, filter, data, parsed ) { 1100 return parsed || data.parsed[ data.index ] ? 1101 c.parsers[ data.index ].format( filter, c.table, [], data.index ) : 1102 filter; 1103 }, 1104 buildRow: function( table, c, wo ) { 1105 var $filter, col, column, $header, makeSelect, disabled, name, ffxn, tmp, 1106 // c.columns defined in computeThIndexes() 1107 cellFilter = wo.filter_cellFilter, 1108 columns = c.columns, 1109 arry = $.isArray( cellFilter ), 1110 buildFilter = '<tr role="search" class="' + tscss.filterRow + ' ' + c.cssIgnoreRow + '">'; 1111 for ( column = 0; column < columns; column++ ) { 1112 if ( c.$headerIndexed[ column ].length ) { 1113 // account for entire column set with colspan. See #1047 1114 tmp = c.$headerIndexed[ column ] && c.$headerIndexed[ column ][0].colSpan || 0; 1115 if ( tmp > 1 ) { 1116 buildFilter += '<td data-column="' + column + '-' + ( column + tmp - 1 ) + '" colspan="' + tmp + '"'; 1117 } else { 1118 buildFilter += '<td data-column="' + column + '"'; 1119 } 1120 if ( arry ) { 1121 buildFilter += ( cellFilter[ column ] ? ' class="' + cellFilter[ column ] + '"' : '' ); 1122 } else { 1123 buildFilter += ( cellFilter !== '' ? ' class="' + cellFilter + '"' : '' ); 1124 } 1125 buildFilter += '></td>'; 1126 } 1127 } 1128 c.$filters = $( buildFilter += '</tr>' ) 1129 .appendTo( c.$table.children( 'thead' ).eq( 0 ) ) 1130 .children( 'td' ); 1131 // build each filter input 1132 for ( column = 0; column < columns; column++ ) { 1133 disabled = false; 1134 // assuming last cell of a column is the main column 1135 $header = c.$headerIndexed[ column ]; 1136 if ( $header && $header.length ) { 1137 // $filter = c.$filters.filter( '[data-column="' + column + '"]' ); 1138 $filter = tsf.getColumnElm( c, c.$filters, column ); 1139 ffxn = ts.getColumnData( table, wo.filter_functions, column ); 1140 makeSelect = ( wo.filter_functions && ffxn && typeof ffxn !== 'function' ) || 1141 $header.hasClass( 'filter-select' ); 1142 // get data from jQuery data, metadata, headers option or header class name 1143 col = ts.getColumnData( table, c.headers, column ); 1144 disabled = ts.getData( $header[0], col, 'filter' ) === 'false' || 1145 ts.getData( $header[0], col, 'parser' ) === 'false'; 1146 1147 if ( makeSelect ) { 1148 buildFilter = $( '<select>' ).appendTo( $filter ); 1149 } else { 1150 ffxn = ts.getColumnData( table, wo.filter_formatter, column ); 1151 if ( ffxn ) { 1152 wo.filter_formatterCount++; 1153 buildFilter = ffxn( $filter, column ); 1154 // no element returned, so lets go find it 1155 if ( buildFilter && buildFilter.length === 0 ) { 1156 buildFilter = $filter.children( 'input' ); 1157 } 1158 // element not in DOM, so lets attach it 1159 if ( buildFilter && ( buildFilter.parent().length === 0 || 1160 ( buildFilter.parent().length && buildFilter.parent()[0] !== $filter[0] ) ) ) { 1161 $filter.append( buildFilter ); 1162 } 1163 } else { 1164 buildFilter = $( '<input type="search">' ).appendTo( $filter ); 1165 } 1166 if ( buildFilter ) { 1167 tmp = $header.data( 'placeholder' ) || 1168 $header.attr( 'data-placeholder' ) || 1169 wo.filter_placeholder.search || ''; 1170 buildFilter.attr( 'placeholder', tmp ); 1171 } 1172 } 1173 if ( buildFilter ) { 1174 // add filter class name 1175 name = ( $.isArray( wo.filter_cssFilter ) ? 1176 ( typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '' ) : 1177 wo.filter_cssFilter ) || ''; 1178 // copy data-column from table cell (it will include colspan) 1179 buildFilter.addClass( tscss.filter + ' ' + name ); 1180 name = wo.filter_filterLabel; 1181 tmp = name.match(/{{([^}]+?)}}/g); 1182 if (!tmp) { 1183 tmp = [ '{{label}}' ]; 1184 } 1185 $.each(tmp, function(indx, attr) { 1186 var regex = new RegExp(attr, 'g'), 1187 data = $header.attr('data-' + attr.replace(/{{|}}/g, '')), 1188 text = typeof data === 'undefined' ? $header.text() : data; 1189 name = name.replace( regex, $.trim( text ) ); 1190 }); 1191 buildFilter.attr({ 1192 'data-column': $filter.attr( 'data-column' ), 1193 'aria-label': name 1194 }); 1195 if ( disabled ) { 1196 buildFilter.attr( 'placeholder', '' ).addClass( tscss.filterDisabled )[0].disabled = true; 1197 } 1198 } 1199 } 1200 } 1201 }, 1202 bindSearch: function( table, $el, internal ) { 1203 table = $( table )[0]; 1204 $el = $( $el ); // allow passing a selector string 1205 if ( !$el.length ) { return; } 1206 var tmp, 1207 c = table.config, 1208 wo = c.widgetOptions, 1209 namespace = c.namespace + 'filter', 1210 $ext = wo.filter_$externalFilters; 1211 if ( internal !== true ) { 1212 // save anyMatch element 1213 tmp = wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector; 1214 wo.filter_$anyMatch = $el.filter( tmp ); 1215 if ( $ext && $ext.length ) { 1216 wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el ); 1217 } else { 1218 wo.filter_$externalFilters = $el; 1219 } 1220 // update values ( external filters added after table initialization ) 1221 ts.setFilters( table, c.$table.data( 'lastSearch' ) || [], internal === false ); 1222 } 1223 // unbind events 1224 tmp = ( 'keypress keyup keydown search change input '.split( ' ' ).join( namespace + ' ' ) ); 1225 $el 1226 // use data attribute instead of jQuery data since the head is cloned without including 1227 // the data/binding 1228 .attr( 'data-lastSearchTime', new Date().getTime() ) 1229 .unbind( tmp.replace( ts.regex.spaces, ' ' ) ) 1230 .bind( 'keydown' + namespace, function( event ) { 1231 if ( event.which === tskeyCodes.escape && !table.config.widgetOptions.filter_resetOnEsc ) { 1232 // prevent keypress event 1233 return false; 1234 } 1235 }) 1236 .bind( 'keyup' + namespace, function( event ) { 1237 wo = table.config.widgetOptions; // make sure "wo" isn't cached 1238 var column = parseInt( $( this ).attr( 'data-column' ), 10 ), 1239 liveSearch = typeof wo.filter_liveSearch === 'boolean' ? wo.filter_liveSearch : 1240 ts.getColumnData( table, wo.filter_liveSearch, column ); 1241 if ( typeof liveSearch === 'undefined' ) { 1242 liveSearch = wo.filter_liveSearch.fallback || false; 1243 } 1244 $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); 1245 // emulate what webkit does.... escape clears the filter 1246 if ( event.which === tskeyCodes.escape ) { 1247 // make sure to restore the last value on escape 1248 this.value = wo.filter_resetOnEsc ? '' : c.lastSearch[column]; 1249 // don't return if the search value is empty ( all rows need to be revealed ) 1250 } else if ( this.value !== '' && ( 1251 // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace 1252 ( typeof liveSearch === 'number' && this.value.length < liveSearch ) || 1253 // let return & backspace continue on, but ignore arrows & non-valid characters 1254 ( event.which !== tskeyCodes.enter && event.which !== tskeyCodes.backSpace && 1255 ( event.which < tskeyCodes.space || ( event.which >= tskeyCodes.left && event.which <= tskeyCodes.down ) ) ) ) ) { 1256 return; 1257 // live search 1258 } else if ( liveSearch === false ) { 1259 if ( this.value !== '' && event.which !== tskeyCodes.enter ) { 1260 return; 1261 } 1262 } 1263 // change event = no delay; last true flag tells getFilters to skip newest timed input 1264 tsf.searching( table, true, true, column ); 1265 }) 1266 // include change for select - fixes #473 1267 .bind( 'search change keypress input blur '.split( ' ' ).join( namespace + ' ' ), function( event ) { 1268 // don't get cached data, in case data-column changes dynamically 1269 var column = parseInt( $( this ).attr( 'data-column' ), 10 ), 1270 eventType = event.type, 1271 liveSearch = typeof wo.filter_liveSearch === 'boolean' ? 1272 wo.filter_liveSearch : 1273 ts.getColumnData( table, wo.filter_liveSearch, column ); 1274 if ( table.config.widgetOptions.filter_initialized && 1275 // immediate search if user presses enter 1276 ( event.which === tskeyCodes.enter || 1277 // immediate search if a "search" or "blur" is triggered on the input 1278 ( eventType === 'search' || eventType === 'blur' ) || 1279 // change & input events must be ignored if liveSearch !== true 1280 ( eventType === 'change' || eventType === 'input' ) && 1281 // prevent search if liveSearch is a number 1282 ( liveSearch === true || liveSearch !== true && event.target.nodeName !== 'INPUT' ) && 1283 // don't allow 'change' or 'input' event to process if the input value 1284 // is the same - fixes #685 1285 this.value !== c.lastSearch[column] 1286 ) 1287 ) { 1288 event.preventDefault(); 1289 // init search with no delay 1290 $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); 1291 tsf.searching( table, eventType !== 'keypress' || event.which === tskeyCodes.enter, true, column ); 1292 } 1293 }); 1294 }, 1295 searching: function( table, filter, skipFirst, column ) { 1296 var liveSearch, 1297 wo = table.config.widgetOptions; 1298 if (typeof column === 'undefined') { 1299 // no delay 1300 liveSearch = false; 1301 } else { 1302 liveSearch = typeof wo.filter_liveSearch === 'boolean' ? 1303 wo.filter_liveSearch : 1304 // get column setting, or set to fallback value, or default to false 1305 ts.getColumnData( table, wo.filter_liveSearch, column ); 1306 if ( typeof liveSearch === 'undefined' ) { 1307 liveSearch = wo.filter_liveSearch.fallback || false; 1308 } 1309 } 1310 clearTimeout( wo.filter_searchTimer ); 1311 if ( typeof filter === 'undefined' || filter === true ) { 1312 // delay filtering 1313 wo.filter_searchTimer = setTimeout( function() { 1314 tsf.checkFilters( table, filter, skipFirst ); 1315 }, liveSearch ? wo.filter_searchDelay : 10 ); 1316 } else { 1317 // skip delay 1318 tsf.checkFilters( table, filter, skipFirst ); 1319 } 1320 }, 1321 equalFilters: function (c, filter1, filter2) { 1322 var indx, 1323 f1 = [], 1324 f2 = [], 1325 len = c.columns + 1; // add one to include anyMatch filter 1326 filter1 = $.isArray(filter1) ? filter1 : []; 1327 filter2 = $.isArray(filter2) ? filter2 : []; 1328 for (indx = 0; indx < len; indx++) { 1329 f1[indx] = filter1[indx] || ''; 1330 f2[indx] = filter2[indx] || ''; 1331 } 1332 return f1.join(',') === f2.join(','); 1333 }, 1334 checkFilters: function( table, filter, skipFirst ) { 1335 var c = table.config, 1336 wo = c.widgetOptions, 1337 filterArray = $.isArray( filter ), 1338 filters = ( filterArray ) ? filter : ts.getFilters( table, true ), 1339 currentFilters = filters || []; // current filter values 1340 // prevent errors if delay init is set 1341 if ( $.isEmptyObject( c.cache ) ) { 1342 // update cache if delayInit set & pager has initialized ( after user initiates a search ) 1343 if ( c.delayInit && ( !c.pager || c.pager && c.pager.initialized ) ) { 1344 ts.updateCache( c, function() { 1345 tsf.checkFilters( table, false, skipFirst ); 1346 }); 1347 } 1348 return; 1349 } 1350 // add filter array back into inputs 1351 if ( filterArray ) { 1352 ts.setFilters( table, filters, false, skipFirst !== true ); 1353 if ( !wo.filter_initialized ) { 1354 c.lastSearch = []; 1355 c.lastCombinedFilter = ''; 1356 } 1357 } 1358 if ( wo.filter_hideFilters ) { 1359 // show/hide filter row as needed 1360 c.$table 1361 .find( '.' + tscss.filterRow ) 1362 .triggerHandler( tsf.hideFiltersCheck( c ) ? 'mouseleave' : 'mouseenter' ); 1363 } 1364 // return if the last search is the same; but filter === false when updating the search 1365 // see example-widget-filter.html filter toggle buttons 1366 if ( tsf.equalFilters(c, c.lastSearch, currentFilters) ) { 1367 if ( filter !== false ) { 1368 return; 1369 } else { 1370 // force filter refresh 1371 c.lastCombinedFilter = ''; 1372 c.lastSearch = []; 1373 } 1374 } 1375 // define filter inside it is false 1376 filters = filters || []; 1377 // convert filters to strings - see #1070 1378 filters = Array.prototype.map ? 1379 filters.map( String ) : 1380 // for IE8 & older browsers - maybe not the best method 1381 filters.join( '\ufffd' ).split( '\ufffd' ); 1382 1383 if ( wo.filter_initialized ) { 1384 c.$table.triggerHandler( 'filterStart', [ filters ] ); 1385 } 1386 if ( c.showProcessing ) { 1387 // give it time for the processing icon to kick in 1388 setTimeout( function() { 1389 tsf.findRows( table, filters, currentFilters ); 1390 return false; 1391 }, 30 ); 1392 } else { 1393 tsf.findRows( table, filters, currentFilters ); 1394 return false; 1395 } 1396 }, 1397 hideFiltersCheck: function( c ) { 1398 if (typeof c.widgetOptions.filter_hideFilters === 'function') { 1399 var val = c.widgetOptions.filter_hideFilters( c ); 1400 if (typeof val === 'boolean') { 1401 return val; 1402 } 1403 } 1404 return ts.getFilters( c.$table ).join( '' ) === ''; 1405 }, 1406 hideFilters: function( c, $table ) { 1407 var timer; 1408 ( $table || c.$table ) 1409 .find( '.' + tscss.filterRow ) 1410 .addClass( tscss.filterRowHide ) 1411 .bind( 'mouseenter mouseleave', function( e ) { 1412 // save event object - http://bugs.jquery.com/ticket/12140 1413 var event = e, 1414 $row = $( this ); 1415 clearTimeout( timer ); 1416 timer = setTimeout( function() { 1417 if ( /enter|over/.test( event.type ) ) { 1418 $row.removeClass( tscss.filterRowHide ); 1419 } else { 1420 // don't hide if input has focus 1421 // $( ':focus' ) needs jQuery 1.6+ 1422 if ( $( document.activeElement ).closest( 'tr' )[0] !== $row[0] ) { 1423 // don't hide row if any filter has a value 1424 $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) ); 1425 } 1426 } 1427 }, 200 ); 1428 }) 1429 .find( 'input, select' ).bind( 'focus blur', function( e ) { 1430 var event = e, 1431 $row = $( this ).closest( 'tr' ); 1432 clearTimeout( timer ); 1433 timer = setTimeout( function() { 1434 clearTimeout( timer ); 1435 // don't hide row if any filter has a value 1436 $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) && event.type !== 'focus' ); 1437 }, 200 ); 1438 }); 1439 }, 1440 defaultFilter: function( filter, mask ) { 1441 if ( filter === '' ) { return filter; } 1442 var regex = tsfRegex.iQuery, 1443 maskLen = mask.match( tsfRegex.igQuery ).length, 1444 query = maskLen > 1 ? $.trim( filter ).split( /\s/ ) : [ $.trim( filter ) ], 1445 len = query.length - 1, 1446 indx = 0, 1447 val = mask; 1448 if ( len < 1 && maskLen > 1 ) { 1449 // only one 'word' in query but mask has >1 slots 1450 query[1] = query[0]; 1451 } 1452 // replace all {query} with query words... 1453 // if query = 'Bob', then convert mask from '!{query}' to '!Bob' 1454 // if query = 'Bob Joe Frank', then convert mask '{q} OR {q}' to 'Bob OR Joe OR Frank' 1455 while ( regex.test( val ) ) { 1456 val = val.replace( regex, query[indx++] || '' ); 1457 if ( regex.test( val ) && indx < len && ( query[indx] || '' ) !== '' ) { 1458 val = mask.replace( regex, val ); 1459 } 1460 } 1461 return val; 1462 }, 1463 getLatestSearch: function( $input ) { 1464 if ( $input ) { 1465 return $input.sort( function( a, b ) { 1466 return $( b ).attr( 'data-lastSearchTime' ) - $( a ).attr( 'data-lastSearchTime' ); 1467 }); 1468 } 1469 return $input || $(); 1470 }, 1471 findRange: function( c, val, ignoreRanges ) { 1472 // look for multiple columns '1-3,4-6,8' in data-column 1473 var temp, ranges, range, start, end, singles, i, indx, len, 1474 columns = []; 1475 if ( /^[0-9]+$/.test( val ) ) { 1476 // always return an array 1477 return [ parseInt( val, 10 ) ]; 1478 } 1479 // process column range 1480 if ( !ignoreRanges && /-/.test( val ) ) { 1481 ranges = val.match( /(\d+)\s*-\s*(\d+)/g ); 1482 len = ranges ? ranges.length : 0; 1483 for ( indx = 0; indx < len; indx++ ) { 1484 range = ranges[indx].split( /\s*-\s*/ ); 1485 start = parseInt( range[0], 10 ) || 0; 1486 end = parseInt( range[1], 10 ) || ( c.columns - 1 ); 1487 if ( start > end ) { 1488 temp = start; start = end; end = temp; // swap 1489 } 1490 if ( end >= c.columns ) { 1491 end = c.columns - 1; 1492 } 1493 for ( ; start <= end; start++ ) { 1494 columns[ columns.length ] = start; 1495 } 1496 // remove processed range from val 1497 val = val.replace( ranges[ indx ], '' ); 1498 } 1499 } 1500 // process single columns 1501 if ( !ignoreRanges && /,/.test( val ) ) { 1502 singles = val.split( /\s*,\s*/ ); 1503 len = singles.length; 1504 for ( i = 0; i < len; i++ ) { 1505 if ( singles[ i ] !== '' ) { 1506 indx = parseInt( singles[ i ], 10 ); 1507 if ( indx < c.columns ) { 1508 columns[ columns.length ] = indx; 1509 } 1510 } 1511 } 1512 } 1513 // return all columns 1514 if ( !columns.length ) { 1515 for ( indx = 0; indx < c.columns; indx++ ) { 1516 columns[ columns.length ] = indx; 1517 } 1518 } 1519 return columns; 1520 }, 1521 getColumnElm: function( c, $elements, column ) { 1522 // data-column may contain multiple columns '1-3,5-6,8' 1523 // replaces: c.$filters.filter( '[data-column="' + column + '"]' ); 1524 return $elements.filter( function() { 1525 var cols = tsf.findRange( c, $( this ).attr( 'data-column' ) ); 1526 return $.inArray( column, cols ) > -1; 1527 }); 1528 }, 1529 multipleColumns: function( c, $input ) { 1530 // look for multiple columns '1-3,4-6,8' in data-column 1531 var wo = c.widgetOptions, 1532 // only target 'all' column inputs on initialization 1533 // & don't target 'all' column inputs if they don't exist 1534 targets = wo.filter_initialized || !$input.filter( wo.filter_anyColumnSelector ).length, 1535 val = $.trim( tsf.getLatestSearch( $input ).attr( 'data-column' ) || '' ); 1536 return tsf.findRange( c, val, !targets ); 1537 }, 1538 processTypes: function( c, data, vars ) { 1539 var ffxn, 1540 filterMatched = null, 1541 matches = null; 1542 for ( ffxn in tsf.types ) { 1543 if ( $.inArray( ffxn, vars.excludeMatch ) < 0 && matches === null ) { 1544 matches = tsf.types[ffxn]( c, data, vars ); 1545 if ( matches !== null ) { 1546 data.matchedOn = ffxn; 1547 filterMatched = matches; 1548 } 1549 } 1550 } 1551 return filterMatched; 1552 }, 1553 matchType: function( c, columnIndex ) { 1554 var isMatch, 1555 wo = c.widgetOptions, 1556 $el = c.$headerIndexed[ columnIndex ]; 1557 // filter-exact > filter-match > filter_matchType for type 1558 if ( $el.hasClass( 'filter-exact' ) ) { 1559 isMatch = false; 1560 } else if ( $el.hasClass( 'filter-match' ) ) { 1561 isMatch = true; 1562 } else { 1563 // filter-select is not applied when filter_functions are used, so look for a select 1564 if ( wo.filter_columnFilters ) { 1565 $el = c.$filters 1566 .find( '.' + tscss.filter ) 1567 .add( wo.filter_$externalFilters ) 1568 .filter( '[data-column="' + columnIndex + '"]' ); 1569 } else if ( wo.filter_$externalFilters ) { 1570 $el = wo.filter_$externalFilters.filter( '[data-column="' + columnIndex + '"]' ); 1571 } 1572 isMatch = $el.length ? 1573 c.widgetOptions.filter_matchType[ ( $el[ 0 ].nodeName || '' ).toLowerCase() ] === 'match' : 1574 // default to exact, if no inputs found 1575 false; 1576 } 1577 return isMatch; 1578 }, 1579 processRow: function( c, data, vars ) { 1580 var result, filterMatched, 1581 fxn, ffxn, txt, 1582 wo = c.widgetOptions, 1583 showRow = true, 1584 hasAnyMatchInput = wo.filter_$anyMatch && wo.filter_$anyMatch.length, 1585 1586 // if wo.filter_$anyMatch data-column attribute is changed dynamically 1587 // we don't want to do an "anyMatch" search on one column using data 1588 // for the entire row - see #998 1589 columnIndex = wo.filter_$anyMatch && wo.filter_$anyMatch.length ? 1590 // look for multiple columns '1-3,4-6,8' 1591 tsf.multipleColumns( c, wo.filter_$anyMatch ) : 1592 []; 1593 data.$cells = data.$row.children(); 1594 data.matchedOn = null; 1595 if ( data.anyMatchFlag && columnIndex.length > 1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { 1596 data.anyMatch = true; 1597 data.isMatch = true; 1598 data.rowArray = data.$cells.map( function( i ) { 1599 if ( $.inArray( i, columnIndex ) > -1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { 1600 if ( data.parsed[ i ] ) { 1601 txt = data.cacheArray[ i ]; 1602 } else { 1603 txt = data.rawArray[ i ]; 1604 txt = $.trim( wo.filter_ignoreCase ? txt.toLowerCase() : txt ); 1605 if ( c.sortLocaleCompare ) { 1606 txt = ts.replaceAccents( txt ); 1607 } 1608 } 1609 return txt; 1610 } 1611 }).get(); 1612 data.filter = data.anyMatchFilter; 1613 data.iFilter = data.iAnyMatchFilter; 1614 data.exact = data.rowArray.join( ' ' ); 1615 data.iExact = wo.filter_ignoreCase ? data.exact.toLowerCase() : data.exact; 1616 data.cache = data.cacheArray.slice( 0, -1 ).join( ' ' ); 1617 vars.excludeMatch = vars.noAnyMatch; 1618 filterMatched = tsf.processTypes( c, data, vars ); 1619 if ( filterMatched !== null ) { 1620 showRow = filterMatched; 1621 } else { 1622 if ( wo.filter_startsWith ) { 1623 showRow = false; 1624 // data.rowArray may not contain all columns 1625 columnIndex = Math.min( c.columns, data.rowArray.length ); 1626 while ( !showRow && columnIndex > 0 ) { 1627 columnIndex--; 1628 showRow = showRow || data.rowArray[ columnIndex ].indexOf( data.iFilter ) === 0; 1629 } 1630 } else { 1631 showRow = ( data.iExact + data.childRowText ).indexOf( data.iFilter ) >= 0; 1632 } 1633 } 1634 data.anyMatch = false; 1635 // no other filters to process 1636 if ( data.filters.join( '' ) === data.filter ) { 1637 return showRow; 1638 } 1639 } 1640 1641 for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { 1642 data.filter = data.filters[ columnIndex ]; 1643 data.index = columnIndex; 1644 1645 // filter types to exclude, per column 1646 vars.excludeMatch = vars.excludeFilter[ columnIndex ]; 1647 1648 // ignore if filter is empty or disabled 1649 if ( data.filter ) { 1650 data.cache = data.cacheArray[ columnIndex ]; 1651 result = data.parsed[ columnIndex ] ? data.cache : data.rawArray[ columnIndex ] || ''; 1652 data.exact = c.sortLocaleCompare ? ts.replaceAccents( result ) : result; // issue #405 1653 data.iExact = !tsfRegex.type.test( typeof data.exact ) && wo.filter_ignoreCase ? 1654 data.exact.toLowerCase() : data.exact; 1655 data.isMatch = tsf.matchType( c, columnIndex ); 1656 1657 result = showRow; // if showRow is true, show that row 1658 1659 // in case select filter option has a different value vs text 'a - z|A through Z' 1660 ffxn = wo.filter_columnFilters ? 1661 c.$filters.add( wo.filter_$externalFilters ) 1662 .filter( '[data-column="' + columnIndex + '"]' ) 1663 .find( 'select option:selected' ) 1664 .attr( 'data-function-name' ) || '' : ''; 1665 // replace accents - see #357 1666 if ( c.sortLocaleCompare ) { 1667 data.filter = ts.replaceAccents( data.filter ); 1668 } 1669 1670 // replace column specific default filters - see #1088 1671 if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultColFilter[ columnIndex ] ) ) { 1672 data.filter = tsf.defaultFilter( data.filter, vars.defaultColFilter[ columnIndex ] ); 1673 } 1674 1675 // data.iFilter = case insensitive ( if wo.filter_ignoreCase is true ), 1676 // data.filter = case sensitive 1677 data.iFilter = wo.filter_ignoreCase ? ( data.filter || '' ).toLowerCase() : data.filter; 1678 fxn = vars.functions[ columnIndex ]; 1679 filterMatched = null; 1680 if ( fxn ) { 1681 if ( typeof fxn === 'function' ) { 1682 // filter callback( exact cell content, parser normalized content, 1683 // filter input value, column index, jQuery row object ) 1684 filterMatched = fxn( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); 1685 } else if ( typeof fxn[ ffxn || data.filter ] === 'function' ) { 1686 // selector option function 1687 txt = ffxn || data.filter; 1688 filterMatched = 1689 fxn[ txt ]( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); 1690 } 1691 } 1692 if ( filterMatched === null ) { 1693 // cycle through the different filters 1694 // filters return a boolean or null if nothing matches 1695 filterMatched = tsf.processTypes( c, data, vars ); 1696 // select with exact match; ignore "and" or "or" within the text; fixes #1486 1697 txt = fxn === true && (data.matchedOn === 'and' || data.matchedOn === 'or'); 1698 if ( filterMatched !== null && !txt) { 1699 result = filterMatched; 1700 // Look for match, and add child row data for matching 1701 } else { 1702 // check fxn (filter-select in header) after filter types are checked 1703 // without this, the filter + jQuery UI selectmenu demo was breaking 1704 if ( fxn === true ) { 1705 // default selector uses exact match unless 'filter-match' class is found 1706 result = data.isMatch ? 1707 // data.iExact may be a number 1708 ( '' + data.iExact ).search( data.iFilter ) >= 0 : 1709 data.filter === data.exact; 1710 } else { 1711 txt = ( data.iExact + data.childRowText ).indexOf( tsf.parseFilter( c, data.iFilter, data ) ); 1712 result = ( ( !wo.filter_startsWith && txt >= 0 ) || ( wo.filter_startsWith && txt === 0 ) ); 1713 } 1714 } 1715 } else { 1716 result = filterMatched; 1717 } 1718 showRow = ( result ) ? showRow : false; 1719 } 1720 } 1721 return showRow; 1722 }, 1723 findRows: function( table, filters, currentFilters ) { 1724 if ( 1725 tsf.equalFilters(table.config, table.config.lastSearch, currentFilters) || 1726 !table.config.widgetOptions.filter_initialized 1727 ) { 1728 return; 1729 } 1730 var len, norm_rows, rowData, $rows, $row, rowIndex, tbodyIndex, $tbody, columnIndex, 1731 isChild, childRow, lastSearch, showRow, showParent, time, val, indx, 1732 notFiltered, searchFiltered, query, injected, res, id, txt, 1733 storedFilters = $.extend( [], filters ), 1734 c = table.config, 1735 wo = c.widgetOptions, 1736 debug = ts.debug(c, 'filter'), 1737 // data object passed to filters; anyMatch is a flag for the filters 1738 data = { 1739 anyMatch: false, 1740 filters: filters, 1741 // regex filter type cache 1742 filter_regexCache : [] 1743 }, 1744 vars = { 1745 // anyMatch really screws up with these types of filters 1746 noAnyMatch: [ 'range', 'operators' ], 1747 // cache filter variables that use ts.getColumnData in the main loop 1748 functions : [], 1749 excludeFilter : [], 1750 defaultColFilter : [], 1751 defaultAnyFilter : ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || '' 1752 }; 1753 // parse columns after formatter, in case the class is added at that point 1754 data.parsed = []; 1755 for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { 1756 data.parsed[ columnIndex ] = wo.filter_useParsedData || 1757 // parser has a "parsed" parameter 1758 ( c.parsers && c.parsers[ columnIndex ] && c.parsers[ columnIndex ].parsed || 1759 // getData may not return 'parsed' if other 'filter-' class names exist 1760 // ( e.g. <th class="filter-select filter-parsed"> ) 1761 ts.getData && ts.getData( c.$headerIndexed[ columnIndex ], 1762 ts.getColumnData( table, c.headers, columnIndex ), 'filter' ) === 'parsed' || 1763 c.$headerIndexed[ columnIndex ].hasClass( 'filter-parsed' ) ); 1764 1765 vars.functions[ columnIndex ] = 1766 ts.getColumnData( table, wo.filter_functions, columnIndex ) || 1767 c.$headerIndexed[ columnIndex ].hasClass( 'filter-select' ); 1768 vars.defaultColFilter[ columnIndex ] = 1769 ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || ''; 1770 vars.excludeFilter[ columnIndex ] = 1771 ( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split( /\s+/ ); 1772 } 1773 1774 if ( debug ) { 1775 console.log( 'Filter >> Starting filter widget search', filters ); 1776 time = new Date(); 1777 } 1778 // filtered rows count 1779 c.filteredRows = 0; 1780 c.totalRows = 0; 1781 currentFilters = ( storedFilters || [] ); 1782 1783 for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { 1784 $tbody = ts.processTbody( table, c.$tbodies.eq( tbodyIndex ), true ); 1785 // skip child rows & widget added ( removable ) rows - fixes #448 thanks to @hempel! 1786 // $rows = $tbody.children( 'tr' ).not( c.selectorRemove ); 1787 columnIndex = c.columns; 1788 // convert stored rows into a jQuery object 1789 norm_rows = c.cache[ tbodyIndex ].normalized; 1790 $rows = $( $.map( norm_rows, function( el ) { 1791 return el[ columnIndex ].$row.get(); 1792 }) ); 1793 1794 if ( currentFilters.join('') === '' || wo.filter_serversideFiltering ) { 1795 $rows 1796 .removeClass( wo.filter_filteredRow ) 1797 .not( '.' + c.cssChildRow ) 1798 .css( 'display', '' ); 1799 } else { 1800 // filter out child rows 1801 $rows = $rows.not( '.' + c.cssChildRow ); 1802 len = $rows.length; 1803 1804 if ( ( wo.filter_$anyMatch && wo.filter_$anyMatch.length ) || 1805 typeof filters[c.columns] !== 'undefined' ) { 1806 data.anyMatchFlag = true; 1807 data.anyMatchFilter = '' + ( 1808 filters[ c.columns ] || 1809 wo.filter_$anyMatch && tsf.getLatestSearch( wo.filter_$anyMatch ).val() || 1810 '' 1811 ); 1812 if ( wo.filter_columnAnyMatch ) { 1813 // specific columns search 1814 query = data.anyMatchFilter.split( tsfRegex.andSplit ); 1815 injected = false; 1816 for ( indx = 0; indx < query.length; indx++ ) { 1817 res = query[ indx ].split( ':' ); 1818 if ( res.length > 1 ) { 1819 // make the column a one-based index ( non-developers start counting from one :P ) 1820 if ( isNaN( res[0] ) ) { 1821 $.each( c.headerContent, function( i, txt ) { 1822 // multiple matches are possible 1823 if ( txt.toLowerCase().indexOf( res[0] ) > -1 ) { 1824 id = i; 1825 filters[ id ] = res[1]; 1826 } 1827 }); 1828 } else { 1829 id = parseInt( res[0], 10 ) - 1; 1830 } 1831 if ( id >= 0 && id < c.columns ) { // if id is an integer 1832 filters[ id ] = res[1]; 1833 query.splice( indx, 1 ); 1834 indx--; 1835 injected = true; 1836 } 1837 } 1838 } 1839 if ( injected ) { 1840 data.anyMatchFilter = query.join( ' && ' ); 1841 } 1842 } 1843 } 1844 1845 // optimize searching only through already filtered rows - see #313 1846 searchFiltered = wo.filter_searchFiltered; 1847 lastSearch = c.lastSearch || c.$table.data( 'lastSearch' ) || []; 1848 if ( searchFiltered ) { 1849 // cycle through all filters; include last ( columnIndex + 1 = match any column ). Fixes #669 1850 for ( indx = 0; indx < columnIndex + 1; indx++ ) { 1851 val = filters[indx] || ''; 1852 // break out of loop if we've already determined not to search filtered rows 1853 if ( !searchFiltered ) { indx = columnIndex; } 1854 // search already filtered rows if... 1855 searchFiltered = searchFiltered && lastSearch.length && 1856 // there are no changes from beginning of filter 1857 val.indexOf( lastSearch[indx] || '' ) === 0 && 1858 // if there is NOT a logical 'or', or range ( 'to' or '-' ) in the string 1859 !tsfRegex.alreadyFiltered.test( val ) && 1860 // if we are not doing exact matches, using '|' ( logical or ) or not '!' 1861 !tsfRegex.exactTest.test( val ) && 1862 // don't search only filtered if the value is negative 1863 // ( '> -10' => '> -100' will ignore hidden rows ) 1864 !( tsfRegex.isNeg1.test( val ) || tsfRegex.isNeg2.test( val ) ) && 1865 // if filtering using a select without a 'filter-match' class ( exact match ) - fixes #593 1866 !( val !== '' && c.$filters && c.$filters.filter( '[data-column="' + indx + '"]' ).find( 'select' ).length && 1867 !tsf.matchType( c, indx ) ); 1868 } 1869 } 1870 notFiltered = $rows.not( '.' + wo.filter_filteredRow ).length; 1871 // can't search when all rows are hidden - this happens when looking for exact matches 1872 if ( searchFiltered && notFiltered === 0 ) { searchFiltered = false; } 1873 if ( debug ) { 1874 console.log( 'Filter >> Searching through ' + 1875 ( searchFiltered && notFiltered < len ? notFiltered : 'all' ) + ' rows' ); 1876 } 1877 if ( data.anyMatchFlag ) { 1878 if ( c.sortLocaleCompare ) { 1879 // replace accents 1880 data.anyMatchFilter = ts.replaceAccents( data.anyMatchFilter ); 1881 } 1882 if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultAnyFilter ) ) { 1883 data.anyMatchFilter = tsf.defaultFilter( data.anyMatchFilter, vars.defaultAnyFilter ); 1884 // clear search filtered flag because default filters are not saved to the last search 1885 searchFiltered = false; 1886 } 1887 // make iAnyMatchFilter lowercase unless both filter widget & core ignoreCase options are true 1888 // when c.ignoreCase is true, the cache contains all lower case data 1889 data.iAnyMatchFilter = !( wo.filter_ignoreCase && c.ignoreCase ) ? 1890 data.anyMatchFilter : 1891 data.anyMatchFilter.toLowerCase(); 1892 } 1893 1894 // loop through the rows 1895 for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { 1896 1897 txt = $rows[ rowIndex ].className; 1898 // the first row can never be a child row 1899 isChild = rowIndex && tsfRegex.child.test( txt ); 1900 // skip child rows & already filtered rows 1901 if ( isChild || ( searchFiltered && tsfRegex.filtered.test( txt ) ) ) { 1902 continue; 1903 } 1904 1905 data.$row = $rows.eq( rowIndex ); 1906 data.rowIndex = rowIndex; 1907 data.cacheArray = norm_rows[ rowIndex ]; 1908 rowData = data.cacheArray[ c.columns ]; 1909 data.rawArray = rowData.raw; 1910 data.childRowText = ''; 1911 1912 if ( !wo.filter_childByColumn ) { 1913 txt = ''; 1914 // child row cached text 1915 childRow = rowData.child; 1916 // so, if 'table.config.widgetOptions.filter_childRows' is true and there is 1917 // a match anywhere in the child row, then it will make the row visible 1918 // checked here so the option can be changed dynamically 1919 for ( indx = 0; indx < childRow.length; indx++ ) { 1920 txt += ' ' + childRow[indx].join( ' ' ) || ''; 1921 } 1922 data.childRowText = wo.filter_childRows ? 1923 ( wo.filter_ignoreCase ? txt.toLowerCase() : txt ) : 1924 ''; 1925 } 1926 1927 showRow = false; 1928 showParent = tsf.processRow( c, data, vars ); 1929 $row = rowData.$row; 1930 1931 // don't pass reference to val 1932 val = showParent ? true : false; 1933 childRow = rowData.$row.filter( ':gt(0)' ); 1934 if ( wo.filter_childRows && childRow.length ) { 1935 if ( wo.filter_childByColumn ) { 1936 if ( !wo.filter_childWithSibs ) { 1937 // hide all child rows 1938 childRow.addClass( wo.filter_filteredRow ); 1939 // if only showing resulting child row, only include parent 1940 $row = $row.eq( 0 ); 1941 } 1942 // cycle through each child row 1943 for ( indx = 0; indx < childRow.length; indx++ ) { 1944 data.$row = childRow.eq( indx ); 1945 data.cacheArray = rowData.child[ indx ]; 1946 data.rawArray = data.cacheArray; 1947 val = tsf.processRow( c, data, vars ); 1948 // use OR comparison on child rows 1949 showRow = showRow || val; 1950 if ( !wo.filter_childWithSibs && val ) { 1951 childRow.eq( indx ).removeClass( wo.filter_filteredRow ); 1952 } 1953 } 1954 } 1955 // keep parent row match even if no child matches... see #1020 1956 showRow = showRow || showParent; 1957 } else { 1958 showRow = val; 1959 } 1960 $row 1961 .toggleClass( wo.filter_filteredRow, !showRow )[0] 1962 .display = showRow ? '' : 'none'; 1963 } 1964 } 1965 c.filteredRows += $rows.not( '.' + wo.filter_filteredRow ).length; 1966 c.totalRows += $rows.length; 1967 ts.processTbody( table, $tbody, false ); 1968 } 1969 // lastCombinedFilter is no longer used internally 1970 c.lastCombinedFilter = storedFilters.join(''); // save last search 1971 // don't save 'filters' directly since it may have altered ( AnyMatch column searches ) 1972 c.lastSearch = storedFilters; 1973 c.$table.data( 'lastSearch', storedFilters ); 1974 if ( wo.filter_saveFilters && ts.storage ) { 1975 ts.storage( table, 'tablesorter-filters', tsf.processFilters( storedFilters, true ) ); 1976 } 1977 if ( debug ) { 1978 console.log( 'Filter >> Completed search' + ts.benchmark(time) ); 1979 } 1980 if ( wo.filter_initialized ) { 1981 c.$table.triggerHandler( 'filterBeforeEnd', c ); 1982 c.$table.triggerHandler( 'filterEnd', c ); 1983 } 1984 setTimeout( function() { 1985 ts.applyWidget( c.table ); // make sure zebra widget is applied 1986 }, 0 ); 1987 }, 1988 getOptionSource: function( table, column, onlyAvail ) { 1989 table = $( table )[0]; 1990 var c = table.config, 1991 wo = c.widgetOptions, 1992 arry = false, 1993 source = wo.filter_selectSource, 1994 last = c.$table.data( 'lastSearch' ) || [], 1995 fxn = typeof source === 'function' ? true : ts.getColumnData( table, source, column ); 1996 1997 if ( onlyAvail && last[column] !== '' ) { 1998 onlyAvail = false; 1999 } 2000 2001 // filter select source option 2002 if ( fxn === true ) { 2003 // OVERALL source 2004 arry = source( table, column, onlyAvail ); 2005 } else if ( fxn instanceof $ || ( $.type( fxn ) === 'string' && fxn.indexOf( '</option>' ) >= 0 ) ) { 2006 // selectSource is a jQuery object or string of options 2007 return fxn; 2008 } else if ( $.isArray( fxn ) ) { 2009 arry = fxn; 2010 } else if ( $.type( source ) === 'object' && fxn ) { 2011 // custom select source function for a SPECIFIC COLUMN 2012 arry = fxn( table, column, onlyAvail ); 2013 // abort - updating the selects from an external method 2014 if (arry === null) { 2015 return null; 2016 } 2017 } 2018 if ( arry === false ) { 2019 // fall back to original method 2020 arry = tsf.getOptions( table, column, onlyAvail ); 2021 } 2022 2023 return tsf.processOptions( table, column, arry ); 2024 2025 }, 2026 processOptions: function( table, column, arry ) { 2027 if ( !$.isArray( arry ) ) { 2028 return false; 2029 } 2030 table = $( table )[0]; 2031 var cts, txt, indx, len, parsedTxt, str, 2032 c = table.config, 2033 validColumn = typeof column !== 'undefined' && column !== null && column >= 0 && column < c.columns, 2034 direction = validColumn ? c.$headerIndexed[ column ].hasClass( 'filter-select-sort-desc' ) : false, 2035 parsed = []; 2036 // get unique elements and sort the list 2037 // if $.tablesorter.sortText exists ( not in the original tablesorter ), 2038 // then natural sort the list otherwise use a basic sort 2039 arry = $.grep( arry, function( value, indx ) { 2040 if ( value.text ) { 2041 return true; 2042 } 2043 return $.inArray( value, arry ) === indx; 2044 }); 2045 if ( validColumn && c.$headerIndexed[ column ].hasClass( 'filter-select-nosort' ) ) { 2046 // unsorted select options 2047 return arry; 2048 } else { 2049 len = arry.length; 2050 // parse select option values 2051 for ( indx = 0; indx < len; indx++ ) { 2052 txt = arry[ indx ]; 2053 // check for object 2054 str = txt.text ? txt.text : txt; 2055 // sortNatural breaks if you don't pass it strings 2056 parsedTxt = ( validColumn && c.parsers && c.parsers.length && 2057 c.parsers[ column ].format( str, table, [], column ) || str ).toString(); 2058 parsedTxt = c.widgetOptions.filter_ignoreCase ? parsedTxt.toLowerCase() : parsedTxt; 2059 // parse array data using set column parser; this DOES NOT pass the original 2060 // table cell to the parser format function 2061 if ( txt.text ) { 2062 txt.parsed = parsedTxt; 2063 parsed[ parsed.length ] = txt; 2064 } else { 2065 parsed[ parsed.length ] = { 2066 text : txt, 2067 // check parser length - fixes #934 2068 parsed : parsedTxt 2069 }; 2070 } 2071 } 2072 // sort parsed select options 2073 cts = c.textSorter || ''; 2074 parsed.sort( function( a, b ) { 2075 var x = direction ? b.parsed : a.parsed, 2076 y = direction ? a.parsed : b.parsed; 2077 if ( validColumn && typeof cts === 'function' ) { 2078 // custom OVERALL text sorter 2079 return cts( x, y, true, column, table ); 2080 } else if ( validColumn && typeof cts === 'object' && cts.hasOwnProperty( column ) ) { 2081 // custom text sorter for a SPECIFIC COLUMN 2082 return cts[column]( x, y, true, column, table ); 2083 } else if ( ts.sortNatural ) { 2084 // fall back to natural sort 2085 return ts.sortNatural( x, y ); 2086 } 2087 // using an older version! do a basic sort 2088 return true; 2089 }); 2090 // rebuild arry from sorted parsed data 2091 arry = []; 2092 len = parsed.length; 2093 for ( indx = 0; indx < len; indx++ ) { 2094 arry[ arry.length ] = parsed[indx]; 2095 } 2096 return arry; 2097 } 2098 }, 2099 getOptions: function( table, column, onlyAvail ) { 2100 table = $( table )[0]; 2101 var rowIndex, tbodyIndex, len, row, cache, indx, child, childLen, 2102 c = table.config, 2103 wo = c.widgetOptions, 2104 arry = []; 2105 for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { 2106 cache = c.cache[tbodyIndex]; 2107 len = c.cache[tbodyIndex].normalized.length; 2108 // loop through the rows 2109 for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { 2110 // get cached row from cache.row ( old ) or row data object 2111 // ( new; last item in normalized array ) 2112 row = cache.row ? 2113 cache.row[ rowIndex ] : 2114 cache.normalized[ rowIndex ][ c.columns ].$row[0]; 2115 // check if has class filtered 2116 if ( onlyAvail && row.className.match( wo.filter_filteredRow ) ) { 2117 continue; 2118 } 2119 // get non-normalized cell content 2120 if ( wo.filter_useParsedData || 2121 c.parsers[column].parsed || 2122 c.$headerIndexed[column].hasClass( 'filter-parsed' ) ) { 2123 arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ column ]; 2124 // child row parsed data 2125 if ( wo.filter_childRows && wo.filter_childByColumn ) { 2126 childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length - 1; 2127 for ( indx = 0; indx < childLen; indx++ ) { 2128 arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ c.columns ].child[ indx ][ column ]; 2129 } 2130 } 2131 } else { 2132 // get raw cached data instead of content directly from the cells 2133 arry[ arry.length ] = cache.normalized[ rowIndex ][ c.columns ].raw[ column ]; 2134 // child row unparsed data 2135 if ( wo.filter_childRows && wo.filter_childByColumn ) { 2136 childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length; 2137 for ( indx = 1; indx < childLen; indx++ ) { 2138 child = cache.normalized[ rowIndex ][ c.columns ].$row.eq( indx ).children().eq( column ); 2139 arry[ arry.length ] = '' + ts.getElementText( c, child, column ); 2140 } 2141 } 2142 } 2143 } 2144 } 2145 return arry; 2146 }, 2147 buildSelect: function( table, column, arry, updating, onlyAvail ) { 2148 table = $( table )[0]; 2149 column = parseInt( column, 10 ); 2150 if ( !table.config.cache || $.isEmptyObject( table.config.cache ) ) { 2151 return; 2152 } 2153 2154 var indx, val, txt, t, $filters, $filter, option, 2155 c = table.config, 2156 wo = c.widgetOptions, 2157 node = c.$headerIndexed[ column ], 2158 // t.data( 'placeholder' ) won't work in jQuery older than 1.4.3 2159 options = '<option value="">' + 2160 ( node.data( 'placeholder' ) || 2161 node.attr( 'data-placeholder' ) || 2162 wo.filter_placeholder.select || '' 2163 ) + '</option>', 2164 // Get curent filter value 2165 currentValue = c.$table 2166 .find( 'thead' ) 2167 .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) 2168 .val(); 2169 2170 // nothing included in arry ( external source ), so get the options from 2171 // filter_selectSource or column data 2172 if ( typeof arry === 'undefined' || arry === '' ) { 2173 arry = tsf.getOptionSource( table, column, onlyAvail ); 2174 // abort, selects are updated by an external method 2175 if (arry === null) { 2176 return; 2177 } 2178 } 2179 2180 if ( $.isArray( arry ) ) { 2181 // build option list 2182 for ( indx = 0; indx < arry.length; indx++ ) { 2183 option = arry[ indx ]; 2184 if ( option.text ) { 2185 // OBJECT!! add data-function-name in case the value is set in filter_functions 2186 option['data-function-name'] = typeof option.value === 'undefined' ? option.text : option.value; 2187 2188 // support jQuery < v1.8, otherwise the below code could be shortened to 2189 // options += $( '<option>', option )[ 0 ].outerHTML; 2190 options += '<option'; 2191 for ( val in option ) { 2192 if ( option.hasOwnProperty( val ) && val !== 'text' ) { 2193 options += ' ' + val + '="' + option[ val ].replace( tsfRegex.quote, '"' ) + '"'; 2194 } 2195 } 2196 if ( !option.value ) { 2197 options += ' value="' + option.text.replace( tsfRegex.quote, '"' ) + '"'; 2198 } 2199 options += '>' + option.text.replace( tsfRegex.quote, '"' ) + '</option>'; 2200 // above code is needed in jQuery < v1.8 2201 2202 // make sure we don't turn an object into a string (objects without a "text" property) 2203 } else if ( '' + option !== '[object Object]' ) { 2204 txt = option = ( '' + option ).replace( tsfRegex.quote, '"' ); 2205 val = txt; 2206 // allow including a symbol in the selectSource array 2207 // 'a-z|A through Z' so that 'a-z' becomes the option value 2208 // and 'A through Z' becomes the option text 2209 if ( txt.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { 2210 t = txt.split( wo.filter_selectSourceSeparator ); 2211 val = t[0]; 2212 txt = t[1]; 2213 } 2214 // replace quotes - fixes #242 & ignore empty strings 2215 // see http://stackoverflow.com/q/14990971/145346 2216 options += option !== '' ? 2217 '<option ' + 2218 ( val === txt ? '' : 'data-function-name="' + option + '" ' ) + 2219 'value="' + val + '">' + txt + 2220 '</option>' : ''; 2221 } 2222 } 2223 // clear arry so it doesn't get appended twice 2224 arry = []; 2225 } 2226 2227 // update all selects in the same column ( clone thead in sticky headers & 2228 // any external selects ) - fixes 473 2229 $filters = ( c.$filters ? c.$filters : c.$table.children( 'thead' ) ) 2230 .find( '.' + tscss.filter ); 2231 if ( wo.filter_$externalFilters ) { 2232 $filters = $filters && $filters.length ? 2233 $filters.add( wo.filter_$externalFilters ) : 2234 wo.filter_$externalFilters; 2235 } 2236 $filter = $filters.filter( 'select[data-column="' + column + '"]' ); 2237 2238 // make sure there is a select there! 2239 if ( $filter.length ) { 2240 $filter[ updating ? 'html' : 'append' ]( options ); 2241 if ( !$.isArray( arry ) ) { 2242 // append options if arry is provided externally as a string or jQuery object 2243 // options ( default value ) was already added 2244 $filter.append( arry ).val( currentValue ); 2245 } 2246 $filter.val( currentValue ); 2247 } 2248 }, 2249 buildDefault: function( table, updating ) { 2250 var columnIndex, $header, noSelect, 2251 c = table.config, 2252 wo = c.widgetOptions, 2253 columns = c.columns; 2254 // build default select dropdown 2255 for ( columnIndex = 0; columnIndex < columns; columnIndex++ ) { 2256 $header = c.$headerIndexed[columnIndex]; 2257 noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) ); 2258 // look for the filter-select class; build/update it if found 2259 if ( ( $header.hasClass( 'filter-select' ) || 2260 ts.getColumnData( table, wo.filter_functions, columnIndex ) === true ) && noSelect ) { 2261 tsf.buildSelect( table, columnIndex, '', updating, $header.hasClass( wo.filter_onlyAvail ) ); 2262 } 2263 } 2264 } 2265 }; 2266 2267 // filter regex variable 2268 tsfRegex = tsf.regex; 2269 2270 ts.getFilters = function( table, getRaw, setFilters, skipFirst ) { 2271 var i, $filters, $column, cols, 2272 filters = [], 2273 c = table ? $( table )[0].config : '', 2274 wo = c ? c.widgetOptions : ''; 2275 if ( ( getRaw !== true && wo && !wo.filter_columnFilters ) || 2276 // setFilters called, but last search is exactly the same as the current 2277 // fixes issue #733 & #903 where calling update causes the input values to reset 2278 ( $.isArray(setFilters) && tsf.equalFilters(c, setFilters, c.lastSearch) ) 2279 ) { 2280 return $( table ).data( 'lastSearch' ) || []; 2281 } 2282 if ( c ) { 2283 if ( c.$filters ) { 2284 $filters = c.$filters.find( '.' + tscss.filter ); 2285 } 2286 if ( wo.filter_$externalFilters ) { 2287 $filters = $filters && $filters.length ? 2288 $filters.add( wo.filter_$externalFilters ) : 2289 wo.filter_$externalFilters; 2290 } 2291 if ( $filters && $filters.length ) { 2292 filters = setFilters || []; 2293 for ( i = 0; i < c.columns + 1; i++ ) { 2294 cols = ( i === c.columns ? 2295 // 'all' columns can now include a range or set of columms ( data-column='0-2,4,6-7' ) 2296 wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector : 2297 '[data-column="' + i + '"]' ); 2298 $column = $filters.filter( cols ); 2299 if ( $column.length ) { 2300 // move the latest search to the first slot in the array 2301 $column = tsf.getLatestSearch( $column ); 2302 if ( $.isArray( setFilters ) ) { 2303 // skip first ( latest input ) to maintain cursor position while typing 2304 if ( skipFirst && $column.length > 1 ) { 2305 $column = $column.slice( 1 ); 2306 } 2307 if ( i === c.columns ) { 2308 // prevent data-column='all' from filling data-column='0,1' ( etc ) 2309 cols = $column.filter( wo.filter_anyColumnSelector ); 2310 $column = cols.length ? cols : $column; 2311 } 2312 $column 2313 .val( setFilters[ i ] ) 2314 // must include a namespace here; but not c.namespace + 'filter'? 2315 .trigger( 'change' + c.namespace ); 2316 } else { 2317 filters[i] = $column.val() || ''; 2318 // don't change the first... it will move the cursor 2319 if ( i === c.columns ) { 2320 // don't update range columns from 'all' setting 2321 $column 2322 .slice( 1 ) 2323 .filter( '[data-column*="' + $column.attr( 'data-column' ) + '"]' ) 2324 .val( filters[ i ] ); 2325 } else { 2326 $column 2327 .slice( 1 ) 2328 .val( filters[ i ] ); 2329 } 2330 } 2331 // save any match input dynamically 2332 if ( i === c.columns && $column.length ) { 2333 wo.filter_$anyMatch = $column; 2334 } 2335 } 2336 } 2337 } 2338 } 2339 return filters; 2340 }; 2341 2342 ts.setFilters = function( table, filter, apply, skipFirst ) { 2343 var c = table ? $( table )[0].config : '', 2344 valid = ts.getFilters( table, true, filter, skipFirst ); 2345 // default apply to "true" 2346 if ( typeof apply === 'undefined' ) { 2347 apply = true; 2348 } 2349 if ( c && apply ) { 2350 // ensure new set filters are applied, even if the search is the same 2351 c.lastCombinedFilter = null; 2352 c.lastSearch = []; 2353 tsf.searching( c.table, filter, skipFirst ); 2354 c.$table.triggerHandler( 'filterFomatterUpdate' ); 2355 } 2356 return valid.length !== 0; 2357 }; 2358 2359})( jQuery ); 2360 2361/*! Widget: stickyHeaders - updated 9/27/2017 (v2.29.0) *//* 2362 * Requires tablesorter v2.8+ and jQuery 1.4.3+ 2363 * by Rob Garrison 2364 */ 2365;(function ($, window) { 2366 'use strict'; 2367 var ts = $.tablesorter || {}; 2368 2369 $.extend(ts.css, { 2370 sticky : 'tablesorter-stickyHeader', // stickyHeader 2371 stickyVis : 'tablesorter-sticky-visible', 2372 stickyHide: 'tablesorter-sticky-hidden', 2373 stickyWrap: 'tablesorter-sticky-wrapper' 2374 }); 2375 2376 // Add a resize event to table headers 2377 ts.addHeaderResizeEvent = function(table, disable, settings) { 2378 table = $(table)[0]; // make sure we're using a dom element 2379 if ( !table.config ) { return; } 2380 var defaults = { 2381 timer : 250 2382 }, 2383 options = $.extend({}, defaults, settings), 2384 c = table.config, 2385 wo = c.widgetOptions, 2386 checkSizes = function( triggerEvent ) { 2387 var index, headers, $header, sizes, width, height, 2388 len = c.$headers.length; 2389 wo.resize_flag = true; 2390 headers = []; 2391 for ( index = 0; index < len; index++ ) { 2392 $header = c.$headers.eq( index ); 2393 sizes = $header.data( 'savedSizes' ) || [ 0, 0 ]; // fixes #394 2394 width = $header[0].offsetWidth; 2395 height = $header[0].offsetHeight; 2396 if ( width !== sizes[0] || height !== sizes[1] ) { 2397 $header.data( 'savedSizes', [ width, height ] ); 2398 headers.push( $header[0] ); 2399 } 2400 } 2401 if ( headers.length && triggerEvent !== false ) { 2402 c.$table.triggerHandler( 'resize', [ headers ] ); 2403 } 2404 wo.resize_flag = false; 2405 }; 2406 clearInterval(wo.resize_timer); 2407 if (disable) { 2408 wo.resize_flag = false; 2409 return false; 2410 } 2411 checkSizes( false ); 2412 wo.resize_timer = setInterval(function() { 2413 if (wo.resize_flag) { return; } 2414 checkSizes(); 2415 }, options.timer); 2416 }; 2417 2418 function getStickyOffset(c, wo) { 2419 var $el = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : []; 2420 return $el.length ? 2421 $el.height() || 0 : 2422 parseInt(wo.stickyHeaders_offset, 10) || 0; 2423 } 2424 2425 // Sticky headers based on this awesome article: 2426 // http://css-tricks.com/13465-persistent-headers/ 2427 // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech 2428 // ************************** 2429 ts.addWidget({ 2430 id: 'stickyHeaders', 2431 priority: 54, // sticky widget must be initialized after the filter & before pager widget! 2432 options: { 2433 stickyHeaders : '', // extra class name added to the sticky header row 2434 stickyHeaders_appendTo : null, // jQuery selector or object to phycially attach the sticky headers 2435 stickyHeaders_attachTo : null, // jQuery selector or object to attach scroll listener to (overridden by xScroll & yScroll settings) 2436 stickyHeaders_xScroll : null, // jQuery selector or object to monitor horizontal scroll position (defaults: xScroll > attachTo > window) 2437 stickyHeaders_yScroll : null, // jQuery selector or object to monitor vertical scroll position (defaults: yScroll > attachTo > window) 2438 stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element 2439 stickyHeaders_filteredToTop: true, // scroll table top into view after filtering 2440 stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists 2441 stickyHeaders_addResizeEvent : true, // trigger 'resize' event on headers 2442 stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header 2443 stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs 2444 }, 2445 format: function(table, c, wo) { 2446 // filter widget doesn't initialize on an empty table. Fixes #449 2447 if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) { 2448 return; 2449 } 2450 var index, len, $t, 2451 $table = c.$table, 2452 // add position: relative to attach element, hopefully it won't cause trouble. 2453 $attach = $(wo.stickyHeaders_attachTo || wo.stickyHeaders_appendTo), 2454 namespace = c.namespace + 'stickyheaders ', 2455 // element to watch for the scroll event 2456 $yScroll = $(wo.stickyHeaders_yScroll || wo.stickyHeaders_attachTo || window), 2457 $xScroll = $(wo.stickyHeaders_xScroll || wo.stickyHeaders_attachTo || window), 2458 $thead = $table.children('thead:first'), 2459 $header = $thead.children('tr').not('.sticky-false').children(), 2460 $tfoot = $table.children('tfoot'), 2461 stickyOffset = getStickyOffset(c, wo), 2462 // is this table nested? If so, find parent sticky header wrapper (div, not table) 2463 $nestedSticky = $table.parent().closest('.' + ts.css.table).hasClass('hasStickyHeaders') ? 2464 $table.parent().closest('table.tablesorter')[0].config.widgetOptions.$sticky.parent() : [], 2465 nestedStickyTop = $nestedSticky.length ? $nestedSticky.height() : 0, 2466 // clone table, then wrap to make sticky header 2467 $stickyTable = wo.$sticky = $table.clone() 2468 .addClass('containsStickyHeaders ' + ts.css.sticky + ' ' + wo.stickyHeaders + ' ' + c.namespace.slice(1) + '_extra_table' ) 2469 .wrap('<div class="' + ts.css.stickyWrap + '">'), 2470 $stickyWrap = $stickyTable.parent() 2471 .addClass(ts.css.stickyHide) 2472 .css({ 2473 position : $attach.length ? 'absolute' : 'fixed', 2474 padding : parseInt( $stickyTable.parent().parent().css('padding-left'), 10 ), 2475 top : stickyOffset + nestedStickyTop, 2476 left : 0, 2477 visibility : 'hidden', 2478 zIndex : wo.stickyHeaders_zIndex || 2 2479 }), 2480 $stickyThead = $stickyTable.children('thead:first'), 2481 $stickyCells, 2482 laststate = '', 2483 setWidth = function($orig, $clone) { 2484 var index, width, border, $cell, $this, 2485 $cells = $orig.filter(':visible'), 2486 len = $cells.length; 2487 for ( index = 0; index < len; index++ ) { 2488 $cell = $clone.filter(':visible').eq(index); 2489 $this = $cells.eq(index); 2490 // code from https://github.com/jmosbech/StickyTableHeaders 2491 if ($this.css('box-sizing') === 'border-box') { 2492 width = $this.outerWidth(); 2493 } else { 2494 if ($cell.css('border-collapse') === 'collapse') { 2495 if (window.getComputedStyle) { 2496 width = parseFloat( window.getComputedStyle($this[0], null).width ); 2497 } else { 2498 // ie8 only 2499 border = parseFloat( $this.css('border-width') ); 2500 width = $this.outerWidth() - parseFloat( $this.css('padding-left') ) - parseFloat( $this.css('padding-right') ) - border; 2501 } 2502 } else { 2503 width = $this.width(); 2504 } 2505 } 2506 $cell.css({ 2507 'width': width, 2508 'min-width': width, 2509 'max-width': width 2510 }); 2511 } 2512 }, 2513 getLeftPosition = function(yWindow) { 2514 if (yWindow === false && $nestedSticky.length) { 2515 return $table.position().left; 2516 } 2517 return $attach.length ? 2518 parseInt($attach.css('padding-left'), 10) || 0 : 2519 $table.offset().left - parseInt($table.css('margin-left'), 10) - $(window).scrollLeft(); 2520 }, 2521 resizeHeader = function() { 2522 $stickyWrap.css({ 2523 left : getLeftPosition(), 2524 width: $table.outerWidth() 2525 }); 2526 setWidth( $table, $stickyTable ); 2527 setWidth( $header, $stickyCells ); 2528 }, 2529 scrollSticky = function( resizing ) { 2530 if (!$table.is(':visible')) { return; } // fixes #278 2531 // Detect nested tables - fixes #724 2532 nestedStickyTop = $nestedSticky.length ? $nestedSticky.offset().top - $yScroll.scrollTop() + $nestedSticky.height() : 0; 2533 var tmp, 2534 offset = $table.offset(), 2535 stickyOffset = getStickyOffset(c, wo), 2536 yWindow = $.isWindow( $yScroll[0] ), // $.isWindow needs jQuery 1.4.3 2537 yScroll = yWindow ? 2538 $yScroll.scrollTop() : 2539 // use parent sticky position if nested AND inside of a scrollable element - see #1512 2540 $nestedSticky.length ? parseInt($nestedSticky[0].style.top, 10) : $yScroll.offset().top, 2541 attachTop = $attach.length ? yScroll : $yScroll.scrollTop(), 2542 captionHeight = wo.stickyHeaders_includeCaption ? 0 : $table.children( 'caption' ).height() || 0, 2543 scrollTop = attachTop + stickyOffset + nestedStickyTop - captionHeight, 2544 tableHeight = $table.height() - ($stickyWrap.height() + ($tfoot.height() || 0)) - captionHeight, 2545 isVisible = ( scrollTop > offset.top ) && ( scrollTop < offset.top + tableHeight ) ? 'visible' : 'hidden', 2546 state = isVisible === 'visible' ? ts.css.stickyVis : ts.css.stickyHide, 2547 needsUpdating = !$stickyWrap.hasClass( state ), 2548 cssSettings = { visibility : isVisible }; 2549 if ($attach.length) { 2550 // attached sticky headers always need updating 2551 needsUpdating = true; 2552 cssSettings.top = yWindow ? scrollTop - $attach.offset().top : $attach.scrollTop(); 2553 } 2554 // adjust when scrolling horizontally - fixes issue #143 2555 tmp = getLeftPosition(yWindow); 2556 if (tmp !== parseInt($stickyWrap.css('left'), 10)) { 2557 needsUpdating = true; 2558 cssSettings.left = tmp; 2559 } 2560 cssSettings.top = ( cssSettings.top || 0 ) + 2561 // If nested AND inside of a scrollable element, only add parent sticky height 2562 (!yWindow && $nestedSticky.length ? $nestedSticky.height() : stickyOffset + nestedStickyTop); 2563 if (needsUpdating) { 2564 $stickyWrap 2565 .removeClass( ts.css.stickyVis + ' ' + ts.css.stickyHide ) 2566 .addClass( state ) 2567 .css(cssSettings); 2568 } 2569 if (isVisible !== laststate || resizing) { 2570 // make sure the column widths match 2571 resizeHeader(); 2572 laststate = isVisible; 2573 } 2574 }; 2575 // only add a position relative if a position isn't already defined 2576 if ($attach.length && !$attach.css('position')) { 2577 $attach.css('position', 'relative'); 2578 } 2579 // fix clone ID, if it exists - fixes #271 2580 if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; } 2581 // clear out cloned table, except for sticky header 2582 // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing 2583 $stickyTable.find('> thead:gt(0), tr.sticky-false').hide(); 2584 $stickyTable.find('> tbody, > tfoot').remove(); 2585 $stickyTable.find('caption').toggle(wo.stickyHeaders_includeCaption); 2586 // issue #172 - find td/th in sticky header 2587 $stickyCells = $stickyThead.children().children(); 2588 $stickyTable.css({ height:0, width:0, margin: 0 }); 2589 // remove resizable block 2590 $stickyCells.find('.' + ts.css.resizer).remove(); 2591 // update sticky header class names to match real header after sorting 2592 $table 2593 .addClass('hasStickyHeaders') 2594 .bind('pagerComplete' + namespace, function() { 2595 resizeHeader(); 2596 }); 2597 2598 ts.bindEvents(table, $stickyThead.children().children('.' + ts.css.header)); 2599 2600 if (wo.stickyHeaders_appendTo) { 2601 $(wo.stickyHeaders_appendTo).append( $stickyWrap ); 2602 } else { 2603 // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. 2604 $table.after( $stickyWrap ); 2605 } 2606 2607 // onRenderHeader is defined, we need to do something about it (fixes #641) 2608 if (c.onRenderHeader) { 2609 $t = $stickyThead.children('tr').children(); 2610 len = $t.length; 2611 for ( index = 0; index < len; index++ ) { 2612 // send second parameter 2613 c.onRenderHeader.apply( $t.eq( index ), [ index, c, $stickyTable ] ); 2614 } 2615 } 2616 // make it sticky! 2617 $xScroll.add($yScroll) 2618 .unbind( ('scroll resize '.split(' ').join( namespace )).replace(/\s+/g, ' ') ) 2619 .bind('scroll resize '.split(' ').join( namespace ), function( event ) { 2620 scrollSticky( event.type === 'resize' ); 2621 }); 2622 c.$table 2623 .unbind('stickyHeadersUpdate' + namespace) 2624 .bind('stickyHeadersUpdate' + namespace, function() { 2625 scrollSticky( true ); 2626 }); 2627 2628 if (wo.stickyHeaders_addResizeEvent) { 2629 ts.addHeaderResizeEvent(table); 2630 } 2631 2632 // look for filter widget 2633 if ($table.hasClass('hasFilters') && wo.filter_columnFilters) { 2634 // scroll table into view after filtering, if sticky header is active - #482 2635 $table.bind('filterEnd' + namespace, function() { 2636 // $(':focus') needs jQuery 1.6+ 2637 var $td = $(document.activeElement).closest('td'), 2638 column = $td.parent().children().index($td); 2639 // only scroll if sticky header is active 2640 if ($stickyWrap.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) { 2641 // scroll to original table (not sticky clone) 2642 window.scrollTo(0, $table.position().top); 2643 // give same input/select focus; check if c.$filters exists; fixes #594 2644 if (column >= 0 && c.$filters) { 2645 c.$filters.eq(column).find('a, select, input').filter(':visible').focus(); 2646 } 2647 } 2648 }); 2649 ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) ); 2650 // support hideFilters 2651 if (wo.filter_hideFilters) { 2652 ts.filter.hideFilters(c, $stickyTable); 2653 } 2654 } 2655 2656 // resize table (Firefox) 2657 if (wo.stickyHeaders_addResizeEvent) { 2658 $table.bind('resize' + c.namespace + 'stickyheaders', function() { 2659 resizeHeader(); 2660 }); 2661 } 2662 2663 // make sure sticky is visible if page is partially scrolled 2664 scrollSticky( true ); 2665 $table.triggerHandler('stickyHeadersInit'); 2666 2667 }, 2668 remove: function(table, c, wo) { 2669 var namespace = c.namespace + 'stickyheaders '; 2670 c.$table 2671 .removeClass('hasStickyHeaders') 2672 .unbind( ('pagerComplete resize filterEnd stickyHeadersUpdate '.split(' ').join(namespace)).replace(/\s+/g, ' ') ) 2673 .next('.' + ts.css.stickyWrap).remove(); 2674 if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table 2675 $(window) 2676 .add(wo.stickyHeaders_xScroll) 2677 .add(wo.stickyHeaders_yScroll) 2678 .add(wo.stickyHeaders_attachTo) 2679 .unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') ); 2680 ts.addHeaderResizeEvent(table, true); 2681 } 2682 }); 2683 2684})(jQuery, window); 2685 2686/*! Widget: resizable - updated 2018-03-26 (v2.30.2) */ 2687/*jshint browser:true, jquery:true, unused:false */ 2688;(function ($, window) { 2689 'use strict'; 2690 var ts = $.tablesorter || {}; 2691 2692 $.extend(ts.css, { 2693 resizableContainer : 'tablesorter-resizable-container', 2694 resizableHandle : 'tablesorter-resizable-handle', 2695 resizableNoSelect : 'tablesorter-disableSelection', 2696 resizableStorage : 'tablesorter-resizable' 2697 }); 2698 2699 // Add extra scroller css 2700 $(function() { 2701 var s = '<style>' + 2702 'body.' + ts.css.resizableNoSelect + ' { -ms-user-select: none; -moz-user-select: -moz-none;' + 2703 '-khtml-user-select: none; -webkit-user-select: none; user-select: none; }' + 2704 '.' + ts.css.resizableContainer + ' { position: relative; height: 1px; }' + 2705 // make handle z-index > than stickyHeader z-index, so the handle stays above sticky header 2706 '.' + ts.css.resizableHandle + ' { position: absolute; display: inline-block; width: 8px;' + 2707 'top: 1px; cursor: ew-resize; z-index: 3; user-select: none; -moz-user-select: none; }' + 2708 '</style>'; 2709 $('head').append(s); 2710 }); 2711 2712 ts.resizable = { 2713 init : function( c, wo ) { 2714 if ( c.$table.hasClass( 'hasResizable' ) ) { return; } 2715 c.$table.addClass( 'hasResizable' ); 2716 2717 var noResize, $header, column, storedSizes, tmp, 2718 $table = c.$table, 2719 $parent = $table.parent(), 2720 marginTop = parseInt( $table.css( 'margin-top' ), 10 ), 2721 2722 // internal variables 2723 vars = wo.resizable_vars = { 2724 useStorage : ts.storage && wo.resizable !== false, 2725 $wrap : $parent, 2726 mouseXPosition : 0, 2727 $target : null, 2728 $next : null, 2729 overflow : $parent.css('overflow') === 'auto' || 2730 $parent.css('overflow') === 'scroll' || 2731 $parent.css('overflow-x') === 'auto' || 2732 $parent.css('overflow-x') === 'scroll', 2733 storedSizes : [] 2734 }; 2735 2736 // set default widths 2737 ts.resizableReset( c.table, true ); 2738 2739 // now get measurements! 2740 vars.tableWidth = $table.width(); 2741 // attempt to autodetect 2742 vars.fullWidth = Math.abs( $parent.width() - vars.tableWidth ) < 20; 2743 2744 /* 2745 // Hacky method to determine if table width is set to 'auto' 2746 // http://stackoverflow.com/a/20892048/145346 2747 if ( !vars.fullWidth ) { 2748 tmp = $table.width(); 2749 $header = $table.wrap('<span>').parent(); // temp variable 2750 storedSizes = parseInt( $table.css( 'margin-left' ), 10 ) || 0; 2751 $table.css( 'margin-left', storedSizes + 50 ); 2752 vars.tableWidth = $header.width() > tmp ? 'auto' : tmp; 2753 $table.css( 'margin-left', storedSizes ? storedSizes : '' ); 2754 $header = null; 2755 $table.unwrap('<span>'); 2756 } 2757 */ 2758 2759 if ( vars.useStorage && vars.overflow ) { 2760 // save table width 2761 ts.storage( c.table, 'tablesorter-table-original-css-width', vars.tableWidth ); 2762 tmp = ts.storage( c.table, 'tablesorter-table-resized-width' ) || 'auto'; 2763 ts.resizable.setWidth( $table, tmp, true ); 2764 } 2765 wo.resizable_vars.storedSizes = storedSizes = ( vars.useStorage ? 2766 ts.storage( c.table, ts.css.resizableStorage ) : 2767 [] ) || []; 2768 ts.resizable.setWidths( c, wo, storedSizes ); 2769 ts.resizable.updateStoredSizes( c, wo ); 2770 2771 wo.$resizable_container = $( '<div class="' + ts.css.resizableContainer + '">' ) 2772 .css({ top : marginTop }) 2773 .insertBefore( $table ); 2774 // add container 2775 for ( column = 0; column < c.columns; column++ ) { 2776 $header = c.$headerIndexed[ column ]; 2777 tmp = ts.getColumnData( c.table, c.headers, column ); 2778 noResize = ts.getData( $header, tmp, 'resizable' ) === 'false'; 2779 if ( !noResize ) { 2780 $( '<div class="' + ts.css.resizableHandle + '">' ) 2781 .appendTo( wo.$resizable_container ) 2782 .attr({ 2783 'data-column' : column, 2784 'unselectable' : 'on' 2785 }) 2786 .data( 'header', $header ) 2787 .bind( 'selectstart', false ); 2788 } 2789 } 2790 ts.resizable.bindings( c, wo ); 2791 }, 2792 2793 updateStoredSizes : function( c, wo ) { 2794 var column, $header, 2795 len = c.columns, 2796 vars = wo.resizable_vars; 2797 vars.storedSizes = []; 2798 for ( column = 0; column < len; column++ ) { 2799 $header = c.$headerIndexed[ column ]; 2800 vars.storedSizes[ column ] = $header.is(':visible') ? $header.width() : 0; 2801 } 2802 }, 2803 2804 setWidth : function( $el, width, overflow ) { 2805 // overflow tables need min & max width set as well 2806 $el.css({ 2807 'width' : width, 2808 'min-width' : overflow ? width : '', 2809 'max-width' : overflow ? width : '' 2810 }); 2811 }, 2812 2813 setWidths : function( c, wo, storedSizes ) { 2814 var column, $temp, 2815 vars = wo.resizable_vars, 2816 $extra = $( c.namespace + '_extra_headers' ), 2817 $col = c.$table.children( 'colgroup' ).children( 'col' ); 2818 storedSizes = storedSizes || vars.storedSizes || []; 2819 // process only if table ID or url match 2820 if ( storedSizes.length ) { 2821 for ( column = 0; column < c.columns; column++ ) { 2822 // set saved resizable widths 2823 ts.resizable.setWidth( c.$headerIndexed[ column ], storedSizes[ column ], vars.overflow ); 2824 if ( $extra.length ) { 2825 // stickyHeaders needs to modify min & max width as well 2826 $temp = $extra.eq( column ).add( $col.eq( column ) ); 2827 ts.resizable.setWidth( $temp, storedSizes[ column ], vars.overflow ); 2828 } 2829 } 2830 $temp = $( c.namespace + '_extra_table' ); 2831 if ( $temp.length && !ts.hasWidget( c.table, 'scroller' ) ) { 2832 ts.resizable.setWidth( $temp, c.$table.outerWidth(), vars.overflow ); 2833 } 2834 } 2835 }, 2836 2837 setHandlePosition : function( c, wo ) { 2838 var startPosition, 2839 tableHeight = c.$table.height(), 2840 $handles = wo.$resizable_container.children(), 2841 handleCenter = Math.floor( $handles.width() / 2 ); 2842 2843 if ( ts.hasWidget( c.table, 'scroller' ) ) { 2844 tableHeight = 0; 2845 c.$table.closest( '.' + ts.css.scrollerWrap ).children().each(function() { 2846 var $this = $(this); 2847 // center table has a max-height set 2848 tableHeight += $this.filter('[style*="height"]').length ? $this.height() : $this.children('table').height(); 2849 }); 2850 } 2851 2852 if ( !wo.resizable_includeFooter && c.$table.children('tfoot').length ) { 2853 tableHeight -= c.$table.children('tfoot').height(); 2854 } 2855 // subtract out table left position from resizable handles. Fixes #864 2856 // jQuery v3.3.0+ appears to include the start position with the $header.position().left; see #1544 2857 startPosition = parseFloat($.fn.jquery) >= 3.3 ? 0 : c.$table.position().left; 2858 $handles.each( function() { 2859 var $this = $(this), 2860 column = parseInt( $this.attr( 'data-column' ), 10 ), 2861 columns = c.columns - 1, 2862 $header = $this.data( 'header' ); 2863 if ( !$header ) { return; } // see #859 2864 if ( 2865 !$header.is(':visible') || 2866 ( !wo.resizable_addLastColumn && ts.resizable.checkVisibleColumns(c, column) ) 2867 ) { 2868 $this.hide(); 2869 } else if ( column < columns || column === columns && wo.resizable_addLastColumn ) { 2870 $this.css({ 2871 display: 'inline-block', 2872 height : tableHeight, 2873 left : $header.position().left - startPosition + $header.outerWidth() - handleCenter 2874 }); 2875 } 2876 }); 2877 }, 2878 2879 // Fixes #1485 2880 checkVisibleColumns: function( c, column ) { 2881 var i, 2882 len = 0; 2883 for ( i = column + 1; i < c.columns; i++ ) { 2884 len += c.$headerIndexed[i].is( ':visible' ) ? 1 : 0; 2885 } 2886 return len === 0; 2887 }, 2888 2889 // prevent text selection while dragging resize bar 2890 toggleTextSelection : function( c, wo, toggle ) { 2891 var namespace = c.namespace + 'tsresize'; 2892 wo.resizable_vars.disabled = toggle; 2893 $( 'body' ).toggleClass( ts.css.resizableNoSelect, toggle ); 2894 if ( toggle ) { 2895 $( 'body' ) 2896 .attr( 'unselectable', 'on' ) 2897 .bind( 'selectstart' + namespace, false ); 2898 } else { 2899 $( 'body' ) 2900 .removeAttr( 'unselectable' ) 2901 .unbind( 'selectstart' + namespace ); 2902 } 2903 }, 2904 2905 bindings : function( c, wo ) { 2906 var namespace = c.namespace + 'tsresize'; 2907 wo.$resizable_container.children().bind( 'mousedown', function( event ) { 2908 // save header cell and mouse position 2909 var column, 2910 vars = wo.resizable_vars, 2911 $extras = $( c.namespace + '_extra_headers' ), 2912 $header = $( event.target ).data( 'header' ); 2913 2914 column = parseInt( $header.attr( 'data-column' ), 10 ); 2915 vars.$target = $header = $header.add( $extras.filter('[data-column="' + column + '"]') ); 2916 vars.target = column; 2917 2918 // if table is not as wide as it's parent, then resize the table 2919 vars.$next = event.shiftKey || wo.resizable_targetLast ? 2920 $header.parent().children().not( '.resizable-false' ).filter( ':last' ) : 2921 $header.nextAll( ':not(.resizable-false)' ).eq( 0 ); 2922 2923 column = parseInt( vars.$next.attr( 'data-column' ), 10 ); 2924 vars.$next = vars.$next.add( $extras.filter('[data-column="' + column + '"]') ); 2925 vars.next = column; 2926 2927 vars.mouseXPosition = event.pageX; 2928 ts.resizable.updateStoredSizes( c, wo ); 2929 ts.resizable.toggleTextSelection(c, wo, true ); 2930 }); 2931 2932 $( document ) 2933 .bind( 'mousemove' + namespace, function( event ) { 2934 var vars = wo.resizable_vars; 2935 // ignore mousemove if no mousedown 2936 if ( !vars.disabled || vars.mouseXPosition === 0 || !vars.$target ) { return; } 2937 if ( wo.resizable_throttle ) { 2938 clearTimeout( vars.timer ); 2939 vars.timer = setTimeout( function() { 2940 ts.resizable.mouseMove( c, wo, event ); 2941 }, isNaN( wo.resizable_throttle ) ? 5 : wo.resizable_throttle ); 2942 } else { 2943 ts.resizable.mouseMove( c, wo, event ); 2944 } 2945 }) 2946 .bind( 'mouseup' + namespace, function() { 2947 if (!wo.resizable_vars.disabled) { return; } 2948 ts.resizable.toggleTextSelection( c, wo, false ); 2949 ts.resizable.stopResize( c, wo ); 2950 ts.resizable.setHandlePosition( c, wo ); 2951 }); 2952 2953 // resizeEnd event triggered by scroller widget 2954 $( window ).bind( 'resize' + namespace + ' resizeEnd' + namespace, function() { 2955 ts.resizable.setHandlePosition( c, wo ); 2956 }); 2957 2958 // right click to reset columns to default widths 2959 c.$table 2960 .bind( 'columnUpdate pagerComplete resizableUpdate '.split( ' ' ).join( namespace + ' ' ), function() { 2961 ts.resizable.setHandlePosition( c, wo ); 2962 }) 2963 .bind( 'resizableReset' + namespace, function() { 2964 ts.resizableReset( c.table ); 2965 }) 2966 .find( 'thead:first' ) 2967 .add( $( c.namespace + '_extra_table' ).find( 'thead:first' ) ) 2968 .bind( 'contextmenu' + namespace, function() { 2969 // $.isEmptyObject() needs jQuery 1.4+; allow right click if already reset 2970 var allowClick = wo.resizable_vars.storedSizes.length === 0; 2971 ts.resizableReset( c.table ); 2972 ts.resizable.setHandlePosition( c, wo ); 2973 wo.resizable_vars.storedSizes = []; 2974 return allowClick; 2975 }); 2976 2977 }, 2978 2979 mouseMove : function( c, wo, event ) { 2980 if ( wo.resizable_vars.mouseXPosition === 0 || !wo.resizable_vars.$target ) { return; } 2981 // resize columns 2982 var column, 2983 total = 0, 2984 vars = wo.resizable_vars, 2985 $next = vars.$next, 2986 tar = vars.storedSizes[ vars.target ], 2987 leftEdge = event.pageX - vars.mouseXPosition; 2988 if ( vars.overflow ) { 2989 if ( tar + leftEdge > 0 ) { 2990 vars.storedSizes[ vars.target ] += leftEdge; 2991 ts.resizable.setWidth( vars.$target, vars.storedSizes[ vars.target ], true ); 2992 // update the entire table width 2993 for ( column = 0; column < c.columns; column++ ) { 2994 total += vars.storedSizes[ column ]; 2995 } 2996 ts.resizable.setWidth( c.$table.add( $( c.namespace + '_extra_table' ) ), total ); 2997 } 2998 if ( !$next.length ) { 2999 // if expanding right-most column, scroll the wrapper 3000 vars.$wrap[0].scrollLeft = c.$table.width(); 3001 } 3002 } else if ( vars.fullWidth ) { 3003 vars.storedSizes[ vars.target ] += leftEdge; 3004 vars.storedSizes[ vars.next ] -= leftEdge; 3005 ts.resizable.setWidths( c, wo ); 3006 } else { 3007 vars.storedSizes[ vars.target ] += leftEdge; 3008 ts.resizable.setWidths( c, wo ); 3009 } 3010 vars.mouseXPosition = event.pageX; 3011 // dynamically update sticky header widths 3012 c.$table.triggerHandler('stickyHeadersUpdate'); 3013 }, 3014 3015 stopResize : function( c, wo ) { 3016 var vars = wo.resizable_vars; 3017 ts.resizable.updateStoredSizes( c, wo ); 3018 if ( vars.useStorage ) { 3019 // save all column widths 3020 ts.storage( c.table, ts.css.resizableStorage, vars.storedSizes ); 3021 ts.storage( c.table, 'tablesorter-table-resized-width', c.$table.width() ); 3022 } 3023 vars.mouseXPosition = 0; 3024 vars.$target = vars.$next = null; 3025 // will update stickyHeaders, just in case, see #912 3026 c.$table.triggerHandler('stickyHeadersUpdate'); 3027 c.$table.triggerHandler('resizableComplete'); 3028 } 3029 }; 3030 3031 // this widget saves the column widths if 3032 // $.tablesorter.storage function is included 3033 // ************************** 3034 ts.addWidget({ 3035 id: 'resizable', 3036 priority: 40, 3037 options: { 3038 resizable : true, // save column widths to storage 3039 resizable_addLastColumn : false, 3040 resizable_includeFooter: true, 3041 resizable_widths : [], 3042 resizable_throttle : false, // set to true (5ms) or any number 0-10 range 3043 resizable_targetLast : false 3044 }, 3045 init: function(table, thisWidget, c, wo) { 3046 ts.resizable.init( c, wo ); 3047 }, 3048 format: function( table, c, wo ) { 3049 ts.resizable.setHandlePosition( c, wo ); 3050 }, 3051 remove: function( table, c, wo, refreshing ) { 3052 if (wo.$resizable_container) { 3053 var namespace = c.namespace + 'tsresize'; 3054 c.$table.add( $( c.namespace + '_extra_table' ) ) 3055 .removeClass('hasResizable') 3056 .children( 'thead' ) 3057 .unbind( 'contextmenu' + namespace ); 3058 3059 wo.$resizable_container.remove(); 3060 ts.resizable.toggleTextSelection( c, wo, false ); 3061 ts.resizableReset( table, refreshing ); 3062 $( document ).unbind( 'mousemove' + namespace + ' mouseup' + namespace ); 3063 } 3064 } 3065 }); 3066 3067 ts.resizableReset = function( table, refreshing ) { 3068 $( table ).each(function() { 3069 var index, $t, 3070 c = this.config, 3071 wo = c && c.widgetOptions, 3072 vars = wo.resizable_vars; 3073 if ( table && c && c.$headerIndexed.length ) { 3074 // restore the initial table width 3075 if ( vars.overflow && vars.tableWidth ) { 3076 ts.resizable.setWidth( c.$table, vars.tableWidth, true ); 3077 if ( vars.useStorage ) { 3078 ts.storage( table, 'tablesorter-table-resized-width', vars.tableWidth ); 3079 } 3080 } 3081 for ( index = 0; index < c.columns; index++ ) { 3082 $t = c.$headerIndexed[ index ]; 3083 if ( wo.resizable_widths && wo.resizable_widths[ index ] ) { 3084 ts.resizable.setWidth( $t, wo.resizable_widths[ index ], vars.overflow ); 3085 } else if ( !$t.hasClass( 'resizable-false' ) ) { 3086 // don't clear the width of any column that is not resizable 3087 ts.resizable.setWidth( $t, '', vars.overflow ); 3088 } 3089 } 3090 3091 // reset stickyHeader widths 3092 c.$table.triggerHandler( 'stickyHeadersUpdate' ); 3093 if ( ts.storage && !refreshing ) { 3094 ts.storage( this, ts.css.resizableStorage, [] ); 3095 } 3096 } 3097 }); 3098 }; 3099 3100})( jQuery, window ); 3101 3102/*! Widget: saveSort - updated 2018-03-19 (v2.30.1) *//* 3103* Requires tablesorter v2.16+ 3104* by Rob Garrison 3105*/ 3106;(function ($) { 3107 'use strict'; 3108 var ts = $.tablesorter || {}; 3109 3110 function getStoredSortList(c) { 3111 var stored = ts.storage( c.table, 'tablesorter-savesort' ); 3112 return (stored && stored.hasOwnProperty('sortList') && $.isArray(stored.sortList)) ? stored.sortList : []; 3113 } 3114 3115 function sortListChanged(c, sortList) { 3116 return (sortList || getStoredSortList(c)).join(',') !== c.sortList.join(','); 3117 } 3118 3119 // this widget saves the last sort only if the 3120 // saveSort widget option is true AND the 3121 // $.tablesorter.storage function is included 3122 // ************************** 3123 ts.addWidget({ 3124 id: 'saveSort', 3125 priority: 20, 3126 options: { 3127 saveSort : true 3128 }, 3129 init: function(table, thisWidget, c, wo) { 3130 // run widget format before all other widgets are applied to the table 3131 thisWidget.format(table, c, wo, true); 3132 }, 3133 format: function(table, c, wo, init) { 3134 var time, 3135 $table = c.$table, 3136 saveSort = wo.saveSort !== false, // make saveSort active/inactive; default to true 3137 sortList = { 'sortList' : c.sortList }, 3138 debug = ts.debug(c, 'saveSort'); 3139 if (debug) { 3140 time = new Date(); 3141 } 3142 if ($table.hasClass('hasSaveSort')) { 3143 if (saveSort && table.hasInitialized && ts.storage && sortListChanged(c)) { 3144 ts.storage( table, 'tablesorter-savesort', sortList ); 3145 if (debug) { 3146 console.log('saveSort >> Saving last sort: ' + c.sortList + ts.benchmark(time)); 3147 } 3148 } 3149 } else { 3150 // set table sort on initial run of the widget 3151 $table.addClass('hasSaveSort'); 3152 sortList = ''; 3153 // get data 3154 if (ts.storage) { 3155 sortList = getStoredSortList(c); 3156 if (debug) { 3157 console.log('saveSort >> Last sort loaded: "' + sortList + '"' + ts.benchmark(time)); 3158 } 3159 $table.bind('saveSortReset', function(event) { 3160 event.stopPropagation(); 3161 ts.storage( table, 'tablesorter-savesort', '' ); 3162 }); 3163 } 3164 // init is true when widget init is run, this will run this widget before all other widgets have initialized 3165 // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice. 3166 if (init && sortList && sortList.length > 0) { 3167 c.sortList = sortList; 3168 } else if (table.hasInitialized && sortList && sortList.length > 0) { 3169 // update sort change 3170 if (sortListChanged(c, sortList)) { 3171 ts.sortOn(c, sortList); 3172 } 3173 } 3174 } 3175 }, 3176 remove: function(table, c) { 3177 c.$table.removeClass('hasSaveSort'); 3178 // clear storage 3179 if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); } 3180 } 3181 }); 3182 3183})(jQuery); 3184return jQuery.tablesorter;}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.