1/*! tableSorter 2.8+ widgets - updated 4/13/2013 2 * 3 * Column Styles 4 * Column Filters 5 * Column Resizing 6 * Sticky Header 7 * UI Theme (generalized) 8 * Save Sort 9 * ["zebra", "uitheme", "stickyHeaders", "filter", "columns"] 10 */ 11/*jshint browser:true, jquery:true, unused:false, loopfunc:true */ 12/*global jQuery: false, localStorage: false, navigator: false */ 13;(function($){ 14"use strict"; 15var ts = $.tablesorter = $.tablesorter || {}; 16 17ts.themes = { 18 "bootstrap" : { 19 table : 'table table-bordered table-striped', 20 header : 'bootstrap-header', // give the header a gradient background 21 footerRow : '', 22 footerCells: '', 23 icons : '', // add "icon-white" to make them white; this icon class is added to the <i> in the header 24 sortNone : 'bootstrap-icon-unsorted', 25 sortAsc : 'icon-chevron-up', 26 sortDesc : 'icon-chevron-down', 27 active : '', // applied when column is sorted 28 hover : '', // use custom css here - bootstrap class may not override it 29 filterRow : '', // filter row class 30 even : '', // even row zebra striping 31 odd : '' // odd row zebra striping 32 }, 33 "jui" : { 34 table : 'ui-widget ui-widget-content ui-corner-all', // table classes 35 header : 'ui-widget-header ui-corner-all ui-state-default', // header classes 36 footerRow : '', 37 footerCells: '', 38 icons : 'ui-icon', // icon class added to the <i> in the header 39 sortNone : 'ui-icon-carat-2-n-s', 40 sortAsc : 'ui-icon-carat-1-n', 41 sortDesc : 'ui-icon-carat-1-s', 42 active : 'ui-state-active', // applied when column is sorted 43 hover : 'ui-state-hover', // hover class 44 filterRow : '', 45 even : 'ui-widget-content', // even row zebra striping 46 odd : 'ui-state-default' // odd row zebra striping 47 } 48}; 49 50// *** Store data in local storage, with a cookie fallback *** 51/* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json) 52 if you need it, then include https://github.com/douglascrockford/JSON-js 53 54 $.parseJSON is not available is jQuery versions older than 1.4.1, using older 55 versions will only allow storing information for one page at a time 56 57 // *** Save data (JSON format only) *** 58 // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid 59 var val = { "mywidget" : "data1" }; // valid JSON uses double quotes 60 // $.tablesorter.storage(table, key, val); 61 $.tablesorter.storage(table, 'tablesorter-mywidget', val); 62 63 // *** Get data: $.tablesorter.storage(table, key); *** 64 v = $.tablesorter.storage(table, 'tablesorter-mywidget'); 65 // val may be empty, so also check for your data 66 val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : ''; 67 alert(val); // "data1" if saved, or "" if not 68*/ 69ts.storage = function(table, key, val){ 70 var d, k, ls = false, v = {}, 71 id = table.id || $('.tablesorter').index( $(table) ), 72 url = window.location.pathname; 73 try { ls = !!(localStorage.getItem); } catch(e) {} 74 // *** get val *** 75 if ($.parseJSON){ 76 if (ls){ 77 v = $.parseJSON(localStorage[key] || '{}'); 78 } else { 79 k = document.cookie.split(/[;\s|=]/); // cookie 80 d = $.inArray(key, k) + 1; // add one to get from the key to the value 81 v = (d !== 0) ? $.parseJSON(k[d] || '{}') : {}; 82 } 83 } 84 // allow val to be an empty string to 85 if ((val || val === '') && window.JSON && JSON.hasOwnProperty('stringify')){ 86 // add unique identifiers = url pathname > table ID/index on page > data 87 if (!v[url]) { 88 v[url] = {}; 89 } 90 v[url][id] = val; 91 // *** set val *** 92 if (ls){ 93 localStorage[key] = JSON.stringify(v); 94 } else { 95 d = new Date(); 96 d.setTime(d.getTime() + (31536e+6)); // 365 days 97 document.cookie = key + '=' + (JSON.stringify(v)).replace(/\"/g,'\"') + '; expires=' + d.toGMTString() + '; path=/'; 98 } 99 } else { 100 return v && v[url] ? v[url][id] : {}; 101 } 102}; 103 104// Widget: General UI theme 105// "uitheme" option in "widgetOptions" 106// ************************** 107ts.addWidget({ 108 id: "uitheme", 109 priority: 10, 110 options: { 111 uitheme : 'jui' 112 }, 113 format: function(table, c, wo){ 114 var time, klass, $el, $tar, 115 t = ts.themes, 116 $t = c.$table, 117 theme = c.theme !== 'default' ? c.theme : wo.uitheme || 'jui', 118 o = t[ t[theme] ? theme : t[wo.uitheme] ? wo.uitheme : 'jui'], 119 $h = c.$headers, 120 sh = 'tr.' + (wo.stickyHeaders || 'tablesorter-stickyHeader'),
121 rmv = o.sortNone + ' ' + o.sortDesc + ' ' + o.sortAsc; 122 if (c.debug) { time = new Date(); } 123 if (!$t.hasClass('tablesorter-' + theme) || c.theme === theme || !table.hasInitialized){ 124 // update zebra stripes 125 if (o.even !== '') { wo.zebra[0] += ' ' + o.even; } 126 if (o.odd !== '') { wo.zebra[1] += ' ' + o.odd; } 127 // add table/footer class names 128 t = $t 129 // remove other selected themes; use widgetOptions.theme_remove 130 .removeClass( c.theme === '' ? '' : 'tablesorter-' + c.theme ) 131 .addClass('tablesorter-' + theme + ' ' + o.table) // add theme widget class name 132 .find('tfoot'); 133 if (t.length) { 134 t 135 .find('tr').addClass(o.footerRow) 136 .children('th, td').addClass(o.footerCells); 137 } 138 // update header classes 139 $h 140 .addClass(o.header) 141 .filter(':not(.sorter-false)') 142 .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(e){ 143 // toggleClass with switch added in jQuery 1.3 144 $(this)[ e.type === 'mouseenter' ? 'addClass' : 'removeClass' ](o.hover); 145 }); 146 if (!$h.find('.tablesorter-wrapper').length) { 147 // Firefox needs this inner div to position the resizer correctly 148 $h.wrapInner('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>'); 149 } 150 if (c.cssIcon){ 151 // if c.cssIcon is '', then no <i> is added to the header 152 $h.find('.' + c.cssIcon).addClass(o.icons); 153 } 154 if ($t.hasClass('hasFilters')){ 155 $h.find('.tablesorter-filter-row').addClass(o.filterRow); 156 } 157 } 158 $.each($h, function(i){ 159 $el = $(this); 160 $tar = (c.cssIcon) ? $el.find('.' + c.cssIcon) : $el; 161 if (this.sortDisabled){ 162 // no sort arrows for disabled columns! 163 $el.removeClass(rmv); 164 $tar.removeClass(rmv + ' tablesorter-icon ' + o.icons); 165 } else { 166 t = ($t.hasClass('hasStickyHeaders')) ? $t.find(sh).find('th').eq(i).add($el) : $el; 167 klass = ($el.hasClass(c.cssAsc)) ? o.sortAsc : ($el.hasClass(c.cssDesc)) ? o.sortDesc : $el.hasClass(c.cssHeader) ? o.sortNone : ''; 168 $el[klass === o.sortNone ? 'removeClass' : 'addClass'](o.active); 169 $tar.removeClass(rmv).addClass(klass); 170 } 171 }); 172 if (c.debug){ 173 ts.benchmark("Applying " + theme + " theme", time); 174 } 175 }, 176 remove: function(table, c, wo){ 177 var $t = c.$table, 178 theme = typeof wo.uitheme === 'object' ? 'jui' : wo.uitheme || 'jui', 179 o = typeof wo.uitheme === 'object' ? wo.uitheme : ts.themes[ ts.themes.hasOwnProperty(theme) ? theme : 'jui'], 180 $h = $t.children('thead').children(), 181 rmv = o.sortNone + ' ' + o.sortDesc + ' ' + o.sortAsc; 182 $t 183 .removeClass('tablesorter-' + theme + ' ' + o.table) 184 .find(c.cssHeader).removeClass(o.header); 185 $h 186 .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover 187 .removeClass(o.hover + ' ' + rmv + ' ' + o.active) 188 .find('.tablesorter-filter-row').removeClass(o.filterRow); 189 $h.find('.tablesorter-icon').removeClass(o.icons); 190 } 191}); 192 193// Widget: Column styles 194// "columns", "columns_thead" (true) and 195// "columns_tfoot" (true) options in "widgetOptions" 196// ************************** 197ts.addWidget({ 198 id: "columns", 199 priority: 30, 200 options : { 201 columns : [ "primary", "secondary", "tertiary" ] 202 }, 203 format: function(table, c, wo){ 204 var $tb, $tr, $td, $t, time, last, rmv, i, k, l, 205 $tbl = c.$table, 206 b = c.$tbodies, 207 list = c.sortList, 208 len = list.length, 209 // keep backwards compatibility, for now 210 css = (c.widgetColumns && c.widgetColumns.hasOwnProperty('css')) ? c.widgetColumns.css || css : 211 (wo && wo.hasOwnProperty('columns')) ? wo.columns || css : css; 212 last = css.length-1; 213 rmv = css.join(' '); 214 if (c.debug){ 215 time = new Date(); 216 } 217 // check if there is a sort (on initialization there may not be one) 218 for (k = 0; k < b.length; k++ ){ 219 $tb = ts.processTbody(table, b.eq(k), true); // detach tbody 220 $tr = $tb.children('tr'); 221 l = $tr.length; 222 // loop through the visible rows 223 $tr.each(function(){ 224 $t = $(this); 225 if (this.style.display !== 'none'){ 226 // remove all columns class names 227 $td = $t.children().removeClass(rmv); 228 // add appropriate column class names 229 if (list && list[0]){ 230 // primary sort column class 231 $td.eq(list[0][0]).addClass(css[0]); 232 if (len > 1){ 233 for (i = 1; i < len; i++){ 234 // secondary, tertiary, etc sort column classes 235 $td.eq(list[i][0]).addClass( css[i] || css[last] ); 236 } 237 } 238 } 239 } 240 }); 241 ts.processTbody(table, $tb, false); 242 } 243 // add classes to thead and tfoot 244 $tr = wo.columns_thead !== false ? 'thead tr' : '';
245 if (wo.columns_tfoot !== false) { 246 $tr += ($tr === '' ? '' : ',') + 'tfoot tr'; 247 } 248 if ($tr.length) { 249 $t = $tbl.find($tr).children().removeClass(rmv); 250 if (list && list[0]){ 251 // primary sort column class 252 $t.filter('[data-column="' + list[0][0] + '"]').addClass(css[0]); 253 if (len > 1){ 254 for (i = 1; i < len; i++){ 255 // secondary, tertiary, etc sort column classes 256 $t.filter('[data-column="' + list[i][0] + '"]').addClass(css[i] || css[last]); 257 } 258 } 259 } 260 } 261 if (c.debug){ 262 ts.benchmark("Applying Columns widget", time); 263 } 264 }, 265 remove: function(table, c, wo){ 266 var k, $tb, 267 b = c.$tbodies, 268 rmv = (wo.columns || [ "primary", "secondary", "tertiary" ]).join(' '); 269 c.$headers.removeClass(rmv); 270 c.$table.children('tfoot').children('tr').children('th, td').removeClass(rmv); 271 for (k = 0; k < b.length; k++ ){ 272 $tb = ts.processTbody(table, b.eq(k), true); // remove tbody 273 $tb.children('tr').each(function(){ 274 $(this).children().removeClass(rmv); 275 }); 276 ts.processTbody(table, $tb, false); // restore tbody 277 } 278 } 279}); 280 281// Widget: filter 282// ************************** 283ts.addWidget({ 284 id: "filter", 285 priority: 50, 286 options : { 287 filter_childRows : false, // if true, filter includes child row content in the search 288 filter_columnFilters : true, // if true, a filter will be added to the top of each table column 289 filter_cssFilter : 'tablesorter-filter', // css class name added to the filter row & each input in the row 290 filter_formatter : null, // add custom filter elements to the filter row 291 filter_functions : null, // add custom filter functions using this option 292 filter_hideFilters : false, // collapse filter row when mouse leaves the area 293 filter_ignoreCase : true, // if true, make all searches case-insensitive 294 filter_liveSearch : true, // if true, search column content while the user types (with a delay) 295 filter_reset : null, // jQuery selector string of an element used to reset the filters 296 filter_searchDelay : 300, // typing delay in milliseconds before starting a search 297 filter_startsWith : false, // if true, filter start from the beginning of the cell contents 298 filter_useParsedData : false, // filter all data using parsed content 299 filter_serversideFiltering : false, // if true, server-side filtering should be performed because client-side filtering will be disabled, but the ui and events will still be used. 300 301 // regex used in filter "check" functions - not for general use and not documented 302 filter_regex : { 303 "regex" : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex 304 "child" : /tablesorter-childRow/, // child row class name; this gets updated in the script 305 "type" : /undefined|number/, // check type 306 "exact" : /(^[\"|\'|=])|([\"|\'|=]$)/g, // exact match 307 "nondigit" : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser) 308 "operators" : /[<>=]/g // replace operators 309 } 310 }, 311 format: function(table, c, wo){ 312 if (c.parsers && !c.$table.hasClass('hasFilters')){ 313 var i, j, k, l, val, ff, x, xi, st, sel, str, 314 ft, ft2, $th, rg, s, t, dis, col, 315 fmt = ts.formatFloat, 316 last = '', // save last filter search 317 $ths = c.$headers, 318 css = wo.filter_cssFilter, 319 $t = c.$table.addClass('hasFilters'), 320 b = $t.find('tbody'), 321 cols = c.parsers.length, 322 parsed, time, timer, 323 324 // dig fer gold 325 checkFilters = function(filter){ 326 var arry = $.isArray(filter), 327 v = (arry) ? filter : ts.getFilters(table), 328 cv = (v || []).join('');
328 // combined filter values 329 // add filter array back into inputs 330 if (arry) { 331 c.$filters.each(function(i,el){ 332 $(el).val(filter[i] || ''); 333 }); 334 } 335 if (wo.filter_hideFilters){ 336 // show/hide filter row as needed 337 $t.find('.tablesorter-filter-row').trigger( cv === '' ? 'mouseleave' : 'mouseenter' ); 338 } 339 // return if the last search is the same; but filter === false when updating the search 340 // see example-widget-filter.html filter toggle buttons 341 if (last === cv && filter !== false) { return; } 342 $t.trigger('filterStart', [v]); 343 if (c.showProcessing) { 344 // give it time for the processing icon to kick in 345 setTimeout(function(){ 346 findRows(filter, v, cv); 347 return false; 348 }, 30); 349 } else { 350 findRows(filter, v, cv); 351 return false; 352 } 353 }, 354 findRows = function(filter, v, cv){ 355 var $tb, $tr, $td, cr, r, l, ff, time, r1, r2; 356 if (c.debug) { time = new Date(); } 357 358 for (k = 0; k < b.length; k++ ){ 359 if (b.eq(k).hasClass(c.cssInfoBlock)) { continue; } // ignore info blocks, issue #264 360 $tb = ts.processTbody(table, b.eq(k), true); 361 $tr = $tb.children('tr'); 362 l = $tr.length; 363 if (cv === '' || wo.filter_serversideFiltering){ 364 $tr.show().removeClass('filtered'); 365 } else { 366 // loop through the rows 367 for (j = 0; j < l; j++){ 368 // skip child rows 369 if (wo.filter_regex.child.test($tr[j].className)) { continue; } 370 r = true; 371 cr = $tr.eq(j).nextUntil('tr:not(.' + c.cssChildRow + ')'); 372 // so, if "table.config.widgetOptions.filter_childRows" is true and there is 373 // a match anywhere in the child row, then it will make the row visible 374 // checked here so the option can be changed dynamically 375 t = (cr.length && wo.filter_childRows) ? cr.text() : ''; 376 t = wo.filter_ignoreCase ? t.toLocaleLowerCase() : t; 377 $td = $tr.eq(j).children('td'); 378 for (i = 0; i < cols; i++){ 379 // ignore if filter is empty or disabled 380 if (v[i]){ 381 // check if column data should be from the cell or from parsed data 382 if (wo.filter_useParsedData || parsed[i]){ 383 x = c.cache[k].normalized[j][i]; 384 } else { 385 // using older or original tablesorter 386 x = $.trim($td.eq(i).text()); 387 } 388 xi = !wo.filter_regex.type.test(typeof x) && wo.filter_ignoreCase ? x.toLocaleLowerCase() : x; 389 ff = r; // if r is true, show that row 390 // val = case insensitive, v[i] = case sensitive 391 val = wo.filter_ignoreCase ? v[i].toLocaleLowerCase() : v[i]; 392 if (wo.filter_functions && wo.filter_functions[i]){ 393 if (wo.filter_functions[i] === true){ 394 // default selector; no "filter-select" class 395 ff = ($ths.filter('[data-column="' + i + '"]:last').hasClass('filter-match')) ? xi.search(val) >= 0 : v[i] === x; 396 } else if (typeof wo.filter_functions[i] === 'function'){ 397 // filter callback( exact cell content, parser normalized content, filter input value, column index ) 398 ff = wo.filter_functions[i](x, c.cache[k].normalized[j][i], v[i], i); 399 } else if (typeof wo.filter_functions[i][v[i]] === 'function'){ 400 // selector option function 401 ff = wo.filter_functions[i][v[i]](x, c.cache[k].normalized[j][i], v[i], i); 402 } 403 // Look for regex 404 } else if (wo.filter_regex.regex.test(val)){ 405 rg = wo.filter_regex.regex.exec(val); 406 try { 407 ff = new RegExp(rg[1], rg[2]).test(xi); 408 } catch (err){ 409 ff = false; 410 } 411 // Look for quotes or equals to get an exact match; ignore type since xi could be numeric 412 /*jshint eqeqeq:false */ 413 } else if (val.replace(wo.filter_regex.exact, '') == xi){ 414 ff = true; 415 // Look for a not match 416 } else if (/^\!/.test(val)){ 417 val = val.replace('!',''); 418 s = xi.search($.trim(val)); 419 ff = val === '' ? true : !(wo.filter_startsWith ? s === 0 : s >= 0); 420 // Look for operators >, >=, < or <= 421 } else if (/^[<>]=?/.test(val)){ 422 // xi may be numeric - see issue #149 423 rg = isNaN(xi) ? fmt(xi.replace(wo.filter_regex.nondigit, ''), table) : fmt(xi, table); 424 s = fmt(val.replace(wo.filter_regex.nondigit, '').replace(wo.filter_regex.operators,''), table); 425 if (/>/.test(val)) { ff = />=/.test(val) ? rg >= s : rg > s; } 426 if (/</.test(val)) { ff = /<=/.test(val) ? rg <= s : rg < s; } 427 if (s === '') { ff = true; } // keep showing all rows if nothing follows the operator
428 // Look for an AND or && operator (logical and) 429 } else if (/\s+(AND|&&)\s+/g.test(v[i])) { 430 s = val.split(/(?:\s+(?:and|&&)\s+)/g); 431 ff = xi.search($.trim(s[0])) >= 0; 432 r1 = s.length - 1; 433 while (ff && r1) { 434 ff = ff && xi.search($.trim(s[r1])) >= 0; 435 r1--; 436 } 437 // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu! 438 } else if (/\s+(-|to)\s+/.test(val)){ 439 rg = isNaN(xi) ? fmt(xi.replace(wo.filter_regex.nondigit, ''), table) : fmt(xi, table); 440 s = val.split(/(?: - | to )/); // make sure the dash is for a range and not indicating a negative number 441 r1 = fmt(s[0].replace(wo.filter_regex.nondigit, ''), table); 442 r2 = fmt(s[1].replace(wo.filter_regex.nondigit, ''), table); 443 if (r1 > r2) { ff = r1; r1 = r2; r2 = ff; } // swap 444 ff = (rg >= r1 && rg <= r2) || (r1 === '' || r2 === '') ? true : false; 445 // Look for wild card: ? = single, * = multiple, or | = logical OR 446 } else if ( /[\?|\*]/.test(val) || /\s+OR\s+/.test(v[i]) ){ 447 ff = new RegExp( val.replace(/\s+or\s+/gi,"|").replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(xi); 448 // Look for match, and add child row data for matching 449 } else { 450 x = (xi + t).indexOf(val); 451 ff = ( (!wo.filter_startsWith && x >= 0) || (wo.filter_startsWith && x === 0) ); 452 } 453 r = (ff) ? (r ? true : false) : false; 454 } 455 } 456 $tr[j].style.display = (r ? '' : 'none'); 457 $tr.eq(j)[r ? 'removeClass' : 'addClass']('filtered'); 458 if (cr.length) { cr[r ? 'show' : 'hide'](); } 459 } 460 } 461 ts.processTbody(table, $tb, false); 462 } 463 464 last = cv; // save last search 465 $t.data('lastSearch', v); 466 if (c.debug){ 467 ts.benchmark("Completed filter widget search", time); 468 } 469 $t.trigger('applyWidgets'); // make sure zebra widget is applied 470 $t.trigger('filterEnd'); 471 }, 472 buildSelect = function(i, updating){ 473 var o, arry = []; 474 i = parseInt(i, 10); 475 o = '<option value="">' + ($ths.filter('[data-column="' + i + '"]:last').attr('data-placeholder') || '') + '</option>'; 476 for (k = 0; k < b.length; k++ ){ 477 l = c.cache[k].row.length; 478 // loop through the rows 479 for (j = 0; j < l; j++){ 480 // get non-normalized cell content 481 if (wo.filter_useParsedData){ 482 arry.push( '' + c.cache[k].normalized[j][i] ); 483 } else { 484 t = c.cache[k].row[j][0].cells[i]; 485 if (t){ 486 arry.push( $.trim(c.supportsTextContent ? t.textContent : $(t).text()) ); 487 } 488 } 489 } 490 } 491 492 // get unique elements and sort the list 493 // if $.tablesorter.sortText exists (not in the original tablesorter), 494 // then natural sort the list otherwise use a basic sort 495 arry = $.grep(arry, function(v, k){ 496 return $.inArray(v, arry) === k; 497 }); 498 arry = (ts.sortText) ? arry.sort(function(a, b){ return ts.sortText(table, a, b, i); }) : arry.sort(true); 499 500 // build option list 501 for (k = 0; k < arry.length; k++){ 502 // replace quotes - fixes #242 & ignore empty strings - see http://stackoverflow.com/q/14990971/145346 503 o += arry[k] !== '' ? '<option value="' + arry[k].replace(/\"/g, """) + '">' + arry[k] + '</option>' : ''; 504 } 505 $t.find('thead').find('select.' + css + '[data-column="' + i + '"]')[ updating ? 'html' : 'append' ](o); 506 }, 507 buildDefault = function(updating){ 508 // build default select dropdown 509 for (i = 0; i < cols; i++){ 510 t = $ths.filter('[data-column="' + i + '"]:last'); 511 // look for the filter-select class; build/update it if found 512 if ((t.hasClass('filter-select') || wo.filter_functions && wo.filter_functions[i] === true) && !t.hasClass('filter-false')){ 513 if (!wo.filter_functions) { wo.filter_functions = {}; } 514 wo.filter_functions[i] = true; // make sure this select gets processed by filter_functions 515 buildSelect(i, updating); 516 } 517 } 518 }; 519 if (c.debug){ 520 time = new Date(); 521 } 522 wo.filter_regex.child = new RegExp(c.cssChildRow);
523 // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156 524 if (wo.filter_columnFilters !== false && $ths.filter('.filter-false').length !== $ths.length){ 525 // build filter row 526 t = '<tr class="tablesorter-filter-row">'; 527 for (i = 0; i < cols; i++){ 528 t += '<td></td>'; 529 } 530 c.$filters = $(t += '</tr>').appendTo( $t.find('thead').eq(0) ).find('td'); 531 // build each filter input 532 for (i = 0; i < cols; i++){ 533 dis = false; 534 $th = $ths.filter('[data-column="' + i + '"]:last'); // assuming last cell of a column is the main column 535 sel = (wo.filter_functions && wo.filter_functions[i] && typeof wo.filter_functions[i] !== 'function') || $th.hasClass('filter-select'); 536 // use header option - headers: { 1: { filter: false } } OR add class="filter-false" 537 if (ts.getData){ 538 // get data from jQuery data, metadata, headers option or header class name 539 dis = ts.getData($th[0], c.headers[i], 'filter') === 'false'; 540 } else { 541 // only class names and header options - keep this for compatibility with tablesorter v2.0.5 542 dis = (c.headers[i] && c.headers[i].hasOwnProperty('filter') && c.headers[i].filter === false) || $th.hasClass('filter-false'); 543 } 544 545 if (sel){ 546 t = $('<select>').appendTo( c.$filters.eq(i) ); 547 } else { 548 if (wo.filter_formatter && $.isFunction(wo.filter_formatter[i])) { 549 t = wo.filter_formatter[i]( c.$filters.eq(i), i ); 550 // no element returned, so lets go find it 551 if (t && t.length === 0) { t = c.$filters.eq(i).children('input'); } 552 // element not in DOM, so lets attach it 553 if (t && (t.parent().length === 0 || (t.parent().length && t.parent()[0] !== c.$filters[i]))) { 554 c.$filters.eq(i).append(t); 555 } 556 } else { 557 t = $('<input type="search">').appendTo( c.$filters.eq(i) ); 558 } 559 if (t) { 560 t.attr('placeholder', $th.attr('data-placeholder') || ''); 561 } 562 } 563 if (t) { 564 t.addClass(css).attr('data-column', i); 565 if (dis) { 566 t.addClass('disabled')[0].disabled = true; // disabled! 567 } 568 } 569 } 570 } 571 $t 572 .bind('addRows updateCell update updateRows updateComplete appendCache filterReset search '.split(' ').join('.tsfilter '), function(e, filter){ 573 if (!/(search|filterReset)/.test(e.type)){ 574 e.stopPropagation(); 575 buildDefault(true); 576 } 577 if (e.type === 'filterReset') { 578 $t.find('.' + css).val(''); 579 } 580 // send false argument to force a new search; otherwise if the filter hasn't changed, it will return 581 filter = e.type === 'search' ? filter : e.type === 'updateComplete' ? $t.data('lastSearch') : ''; 582 checkFilters(filter); 583 return false; 584 }) 585 .find('input.' + css).bind('keyup search', function(e, filter){ 586 // ignore arrow and meta keys; allow backspace 587 if (e.type === 'keyup' && ((e.which < 32 && e.which !== 8) || (e.which >= 37 && e.which <=40) || (e.which !== 13 && !wo.filter_liveSearch))) { return; } 588 // skip delay 589 if (typeof filter === 'undefined' || filter === false){ 590 checkFilters(); 591 } else { 592 // delay filtering 593 clearTimeout(timer); 594 timer = setTimeout(function(){ 595 checkFilters(filter); 596 }, wo.filter_searchDelay); 597 return false; 598 } 599 }); 600 601 // parse columns after formatter, in case the class is added at that point 602 parsed = $ths.map(function(i){ 603 return (ts.getData) ? ts.getData($ths.filter('[data-column="' + i + '"]:last'), c.headers[i], 'filter') === 'parsed' : $(this).hasClass('filter-parsed'); 604 }).get(); 605 606 // reset button/link 607 if (wo.filter_reset && $(wo.filter_reset).length){ 608 $(wo.filter_reset).bind('click.tsfilter', function(){ 609 $t.trigger('filterReset'); 610 }); 611 } 612 if (wo.filter_functions){ 613 // i = column # (string) 614 for (col in wo.filter_functions){ 615 if (wo.filter_functions.hasOwnProperty(col) && typeof col === 'string'){ 616 t = $ths.filter('[data-column="' + col + '"]:last'); 617 ff = ''; 618 if (wo.filter_functions[col] === true && !t.hasClass('filter-false')){ 619 buildSelect(col); 620 } else if (typeof col === 'string' && !t.hasClass('filter-false')){ 621 // add custom drop down list 622 for (str in wo.filter_functions[col]){ 623 if (typeof str === 'string'){ 624 ff += ff === '' ? '<option value="">' + (t.attr('data-placeholder') || '') + '</option>' : ''; 625 ff += '<option value="' + str + '">' + str + '</option>'; 626 } 627 } 628 $t.find('thead').find('select.' + css + '[data-column="' + col + '"]').append(ff); 629 } 630 } 631 } 632 } 633 // not really updating, but if the column has both the "filter-select" class & filter_functions set to true, 634 // it would append the same options twice. 635 buildDefault(true); 636 637 $t.find('select.' + css).bind('change search', function(e, filter){ 638 checkFilters(filter); 639 }); 640 641 if (wo.filter_hideFilters){ 642 $t 643 .find('.tablesorter-filter-row') 644 .addClass('hideme') 645 .bind('mouseenter mouseleave', function(e){ 646 // save event object - http://bugs.jquery.com/ticket/12140 647 var all, evt = e; 648 ft = $(this); 649 clearTimeout(st); 650 st = setTimeout(function(){ 651 if (/enter|over/.test(evt.type)){ 652 ft.removeClass('hideme'); 653 } else { 654 // don't hide if input has focus 655 // $(':focus') needs jQuery 1.6+ 656 if ($(document.activeElement).closest('tr')[0] !== ft[0]){ 657 // get all filter values 658 all = $t.find('.' + wo.filter_cssFilter).map(function(){ 659 return $(this).val() || ''; 660 }).get().join(''); 661 // don't hide row if any filter has a value 662 if (all === ''){ 663 ft.addClass('hideme'); 664 } 665 } 666 } 667 }, 200); 668 }) 669 .find('input, select').bind('focus blur', function(e){ 670 ft2 = $(this).closest('tr'); 671 clearTimeout(st); 672 st = setTimeout(function(){ 673 // don't hide row if any filter has a value 674 if ($t.find('.' + wo.filter_cssFilter).map(function(){ return $(this).val() || ''; }).get().join('') === ''){ 675 ft2[ e.type === 'focus' ? 'removeClass' : 'addClass']('hideme'); 676 } 677 }, 200); 678 }); 679 } 680 681 // show processing icon 682 if (c.showProcessing) { 683 $t.bind('filterStart.tsfilter filterEnd.tsfilter', function(e, v) { 684 var fc = (v) ? $t.find('.' + c.cssHeader).filter('[data-column]').filter(function(){ 685 return v[$(this).data('column')] !== ''; 686 }) : ''; 687 ts.isProcessing($t[0], e.type === 'filterStart', v ? fc : ''); 688 }); 689 } 690 691 if (c.debug){ 692 ts.benchmark("Applying Filter widget", time); 693 } 694 // filter widget initialized 695 $t.trigger('filterInit'); 696 checkFilters(); 697 } 698 }, 699 remove: function(table, c, wo){ 700 var k, $tb, 701 $t = c.$table, 702 b = c.$tbodies; 703 $t 704 .removeClass('hasFilters') 705 // add .tsfilter namespace to all BUT search 706 .unbind('addRows updateCell update updateComplete appendCache search f
706ilterStart filterEnd '.split(' ').join('.tsfilter ')) 707 .find('.tablesorter-filter-row').remove(); 708 for (k = 0; k < b.length; k++ ){ 709 $tb = ts.processTbody(table, b.eq(k), true); // remove tbody 710 $tb.children().removeClass('filtered').show(); 711 ts.processTbody(table, $tb, false); // restore tbody 712 } 713 if (wo.filterreset) { $(wo.filter_reset).unbind('click.tsfilter'); } 714 } 715}); 716ts.getFilters = function(table) { 717 var c = table ? $(table)[0].config : {}; 718 return c && c.$filters ? c.$filters.find('.' + c.widgetOptions.filter_cssFilter).map(function(i, el) { 719 return $(el).val(); 720 }).get() || [] : false; 721}; 722ts.setFilters = function(table, filter, apply) { 723 var $t = $(table), 724 c = $t.length ? $t[0].config : {}, 725 valid = c && c.$filters ? c.$filters.find('.' + c.widgetOptions.filter_cssFilter).each(function(i, el) { 726 $(el).val(filter[i] || ''); 727 }) || false : false; 728 if (valid && apply) { $t.trigger('search'); } 729 return !!valid; 730}; 731 732// Widget: Sticky headers 733// based on this awesome article: 734// http://css-tricks.com/13465-persistent-headers/ 735// and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech 736// ************************** 737ts.addWidget({ 738 id: "stickyHeaders", 739 priority: 60, 740 options: { 741 stickyHeaders: 'tablesorter-stickyHeader', 742 stickyHeaders_cloneId: '-sticky' // added to table ID, if it exists 743 }, 744 format: function(table, c, wo){ 745 if (c.$table.hasClass('hasStickyHeaders')) { return; } 746 var $t = c.$table, 747 win = $(window), 748 header = $t.children('thead:first'), 749 hdrCells = header.children('tr:not(.sticky-false)').children(), 750 innr = '.tablesorter-header-inner', 751 tfoot = $t.find('tfoot'), 752 filterInputs = 'input, select', 753 t2 = wo.$sticky = $t.clone() 754 .addClass('containsStickyHeaders') 755 .css({ 756 position : 'fixed', 757 margin : 0, 758 top : 0, 759 visibility : 'hidden', 760 zIndex : 1 761 }), 762 stkyHdr = t2.children('thead:first').addClass(wo.stickyHeaders), 763 stkyCells, 764 laststate = '', 765 spacing = 0, 766 flag = false, 767 resizeHdr = function(){ 768 var bwsr = navigator.userAgent; 769 spacing = 0; 770 // yes, I dislike browser sniffing, but it really is needed here :( 771 // webkit automatically compensates for border spacing 772 if ($t.css('border-collapse') !== 'collapse' && !/(webkit|msie)/i.test(bwsr)) { 773 // Firefox & Opera use the border-spacing 774 // update border-spacing here because of demos that switch themes 775 spacing = parseInt(hdrCells.eq(0).css('border-left-width'), 10) * 2; 776 } 777 t2.css({ 778 left : header.offset().left - win.scrollLeft() - spacing, 779 width: $t.width() 780 }); 781 stkyCells.filter(':visible').each(function(i){ 782 var $h = hdrCells.filter(':visible').eq(i); 783 $(this) 784 .css({ 785 width: $h.width() - spacing, 786 height: $h.height() 787 }) 788 .find(innr).width( $h.find(innr).width() ); 789 }); 790 }; 791 // fix clone ID, if it exists - fixes #271 792 if (t2.attr('id')) { t2[0].id += wo.stickyHeaders_cloneId; } 793 // clear out cloned table, except for sticky header 794 // include caption & filter row (fixes #126 & #249) 795 t2.find('thead:gt(0), tr.sticky-false, tbody, tfoot').remove(); 796 // issue #172 - find td/th in sticky header 797 stkyCells = stkyHdr.children().children(); 798 t2.css({ height:0, width:0, padding:0, margin:0, border:0 }); 799 // remove resizable block 800 stkyCells.find('.tablesorter-resizer').remove(); 801 // update sticky header class names to match real header after sorting 802 $t 803 .addClass('hasStickyHeaders') 804 .bind('sortEnd.tsSticky', function(){ 805 hdrCells.filter(':visible').each(function(i){ 806 var t = stkyCells.filter(':visible').eq(i); 807 t 808 .attr('class', $(this).attr('class')) 809 // remove processing icon 810 .removeClass(c.cssProcessing); 811 if (c.cssIcon){ 812 t 813 .find('.' + c.cssIcon) 814 .attr('class', $(this).find('.' + c.cssIcon).attr('class')); 815 } 816 }); 817 }) 818 .bind('pagerComplete.tsSticky', function(){ 819 resizeHdr(); 820 }); 821 // http://stackoverflow.com/questions/5312849/jquery-find-self; 822 hdrCells.find(c.selectorSort).add( c.$headers.filter(c.selectorSort) ).each(function(i){ 823 var t = $(this); 824 stkyHdr.children('tr.tablesorter-headerRow').children().eq(i) 825 // clicking on sticky will trigger sort 826 .bind('mouseup', function(e){ 827 t.trigger(e, true); // external mouseup flag (click timer is ignored) 828 }) 829 // prevent sticky header text selection 830 .bind('mousedown', function(){ 831 this.onselectstart = function(){ return false; }; 832 return false;
833 }); 834 }); 835 // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. 836 $t.after( t2 ); 837 // make it sticky! 838 win 839 .bind('scroll.tsSticky resize.tsSticky', function(e){ 840 if (!$t.is(':visible')) { return; } // fixes #278 841 var pre = 'tablesorter-sticky-', 842 offset = $t.offset(), 843 sTop = win.scrollTop(), 844 tableHt = $t.height() - (t2.height() + (tfoot.height() || 0)), 845 vis = (sTop > offset.top) && (sTop < offset.top + tableHt) ? 'visible' : 'hidden'; 846 t2 847 .removeClass(pre + 'visible ' + pre + 'hidden') 848 .addClass(pre + vis) 849 .css({ 850 // adjust when scrolling horizontally - fixes issue #143 851 left : header.offset().left - win.scrollLeft() - spacing, 852 visibility : vis 853 }); 854 if (vis !== laststate || e.type === 'resize'){ 855 // make sure the column widths match 856 resizeHdr(); 857 laststate = vis; 858 } 859 }); 860 861 // look for filter widget 862 $t.bind('filterEnd', function(){ 863 if (flag) { return; } 864 stkyHdr.find('.tablesorter-filter-row').children().each(function(i){ 865 $(this).find(filterInputs).val( c.$filters.find(filterInputs).eq(i).val() ); 866 }); 867 }); 868 stkyCells.find(filterInputs).bind('keyup search', function(e){ 869 // ignore arrow and meta keys; allow backspace 870 if ((e.which < 32 && e.which !== 8) || (e.which >= 37 && e.which <=40)) { return; } 871 flag = true; 872 var $f = $(this), col = $f.attr('data-column'); 873 c.$filters.find(filterInputs).eq(col) 874 .val( $f.val() ) 875 .trigger('search'); 876 setTimeout(function(){ 877 flag = false; 878 }, wo.filter_searchDelay); 879 }); 880 881 }, 882 remove: function(table, c, wo){ 883 c.$table 884 .removeClass('hasStickyHeaders') 885 .unbind('sortEnd.tsSticky pagerComplete.tsSticky') 886 .find('.' + wo.stickyHeaders).remove(); 887 if (wo.$sticky) { wo.$sticky.remove(); } // remove cloned thead 888 $(window).unbind('scroll.tsSticky resize.tsSticky'); 889 } 890}); 891 892// Add Column resizing widget 893// this widget saves the column widths if 894// $.tablesorter.storage function is included 895// ************************** 896ts.addWidget({ 897 id: "resizable", 898 priority: 40, 899 options: { 900 resizable : true, 901 resizable_addLastColumn : false 902 }, 903 format: function(table, c, wo){ 904 if (c.$table.hasClass('hasResizable')) { return; } 905 c.$table.addClass('hasResizable'); 906 var $t, t, i, j, s = {}, $c, $cols, w, tw, 907 $tbl = c.$table, 908 position = 0, 909 $target = null, 910 $next = null, 911 fullWidth = Math.abs($tbl.parent().width() - $tbl.width()) < 20, 912 stopResize = function(){ 913 if (ts.storage && $target){ 914 s[$target.index()] = $target.width(); 915 s[$next.index()] = $next.width(); 916 $target.width( s[$target.index()] ); 917 $next.width( s[$next.index()] ); 918 if (wo.resizable !== false){ 919 ts.storage(table, 'tablesorter-resizable', s); 920 } 921 } 922 position = 0; 923 $target = $next = null; 924 $(window).trigger('resize'); // will update stickyHeaders, just in case 925 }; 926 s = (ts.storage && wo.resizable !== false) ? ts.storage(table, 'tablesorter-resizable') : {}; 927 // process only if table ID or url match 928 if (s){ 929 for (j in s){ 930 if (!isNaN(j) && j < c.$headers.length){ 931 c.$headers.eq(j).width(s[j]); // set saved resizable widths 932 } 933 } 934 } 935 $t = $tbl.children('thead:first').children('tr'); 936 // add resizable-false class name to headers (across rows as needed) 937 $t.children().each(function(){ 938 t = $(this); 939 i = t.attr('data-column'); 940 j = ts.getData( t, c.headers[i], 'resizable') === "false"; 941 $t.children().filter('[data-column="' + i + '"]').toggleClass('resizable-false', j); 942 }); 943 // add wrapper inside each cell to allow for positioning of the resizable target block 944 $t.each(function(){ 945 $c = $(this).children(':not(.resizable-false)'); 946 if (!$(this).find('.tablesorter-wrapper').length) { 947 // Firefox needs this inner div to position the resizer correctly 948 $c.wrapInner('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>'); 949 } 950 // don't include the last column of the row 951 if (!wo.resizable_addLastColumn) { $c = $c.slice(0,-1); } 952 $cols = $cols ? $cols.add($c) : $c; 953 }); 954 $cols 955 .each(function(){ 956 $t = $(this); 957 j = parseInt($t.css('padding-right'), 10) + 10; // 8 is 1/2 of the 16px wide resizer grip 958 t = '<div class="tablesorter-resizer" style="cursor:w-resize;position:absolute;z-index:1;right:-' + j + 959 'px;top:0;height:100%;width:20px;"></div>'; 960 $t 961 .find('.tablesorter-wrapper') 962 .append(t); 963 }) 964 .bind('mousemove.tsresize', function(e){ 965 // ignore mousemove if no mousedown 966 if (position === 0 || !$target) { return; } 967 // resize columns 968 w = e.pageX - position; 969 tw = $target.width(); 970 $target.width( tw + w ); 971 if ($target.width() !== tw && fullWidth){ 972 $next.width( $next.width() - w ); 973 } 974 position = e.pageX; 975 }) 976 .bind('mouseup.tsresize', function(){ 977 stopResize(); 978 }) 979 .find('.tablesorter-resizer,.tablesorter-resizer-grip') 980 .bind('mousedown', function(e){ 981 // save header cell and mouse position; closest() not supported by jQuery v1.2.6 982 $target = $(e.target).closest('th'); 983 t = c.$headers.filter('[data-column="' + $target.attr('data-column') + '"]'); 984 if (t.length > 1) { $target = $target.add(t); } 985 // if table is not as wide as it's parent, then resize the table 986 $next = e.shiftKey ? $target.parent().find('th:not(.resizable-false)').filter(':last') : $target.nextAll(':not(.resizable-false)').eq(0); 987 position = e.pageX; 988 }); 989 $tbl.find('thead:first') 990 .bind('mouseup.tsresize mouseleave.tsresize', function(){ 991 stopResize(); 992 }) 993 // right click to reset columns to default widths 994 .bind('contextmenu.tsresize', function(){ 995 ts.resizableReset(table); 996 // $.isEmptyObject() needs jQuery 1.4+ 997 var rtn = $.isEmptyObject ? $.isEmptyObject(s) : s === {}; // allow right click if already reset 998 s = {}; 999 return rtn; 1000 }); 1001 }, 1002 remove: function(table, c, wo){ 1003 c.$table 1004 .removeClass('hasResizable') 1005 .find('thead') 1006 .unbind('mouseup.tsresize mouseleave.tsresize contextmenu.tsresize') 1007 .find('tr').children() 1008 .unbind('mousemove.tsresize mouseup.tsresize') 1009 // don't remove "tablesorter-wrapper" as uitheme uses it too 1010 .find('.tablesorter-resizer,.tablesorter-resizer-grip').remove(); 1011 ts.resizableReset(table); 1012 } 1013}); 1014ts.resizableReset = function(table){ 1015 table.config.$headers.filter(':not(.resizable-false)').css('width',''); 1016 if (ts.storage) { ts.storage(table, 'tablesorter-resizable', {}); } 1017}; 1018 1019// Save table sort widget 1020// this widget saves the last sort only if the 1021// saveSort widget option is true AND the 1022// $.tablesorter.storage function is included 1023// ************************** 1024ts.addWidget({ 1025 id: 'saveSort', 1026 priority: 20, 1027 options: { 1028 saveSort : true 1029 }, 1030 init: function(table, thisWidget, c, wo){ 1031 // run widget format before all other widgets are applied to the table 1032 thisWidget.format(table, c, wo, true); 1033 }, 1034 format: function(table, c, wo, init){ 1035 var sl, time, 1036 $t = c.$table, 1037 ss = wo.saveSort !== false, // make saveSort active/inactive; default to true
1038 sortList = { "sortList" : c.sortList }; 1039 if (c.debug){ 1040 time = new Date(); 1041 } 1042 if ($t.hasClass('hasSaveSort')){ 1043 if (ss && table.hasInitialized && ts.storage){ 1044 ts.storage( table, 'tablesorter-savesort', sortList ); 1045 if (c.debug){ 1046 ts.benchmark('saveSort widget: Saving last sort: ' + c.sortList, time); 1047 } 1048 } 1049 } else { 1050 // set table sort on initial run of the widget 1051 $t.addClass('hasSaveSort'); 1052 sortList = ''; 1053 // get data 1054 if (ts.storage){ 1055 sl = ts.storage( table, 'tablesorter-savesort' ); 1056 sortList = (sl && sl.hasOwnProperty('sortList') && $.isArray(sl.sortList)) ? sl.sortList : ''; 1057 if (c.debug){ 1058 ts.benchmark('saveSort: Last sort loaded: "' + sortList + '"', time); 1059 } 1060 $t.bind('saveSortReset', function(e){ 1061 e.stopPropagation(); 1062 ts.storage( table, 'tablesorter-savesort', '' ); 1063 }); 1064 } 1065 // init is true when widget init is run, this will run this widget before all other widgets have initialized 1066 // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice. 1067 if (init && sortList && sortList.length > 0){ 1068 c.sortList = sortList; 1069 } else if (table.hasInitialized && sortList && sortList.length > 0){ 1070 // update sort change 1071 $t.trigger('sorton', [sortList]); 1072 } 1073 } 1074 }, 1075 remove: function(table){ 1076 // clear storage 1077 if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); } 1078 } 1079}); 1080 1081})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.