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