1/*! tableSorter 2.16+ widgets - updated 9/15/2014 (v2.17.8) 2 * 3 * Column Styles 4 * Column Filters 5 * Column Resizing 6 * Sticky Header 7 * UI Theme (generalized) 8 * Save Sort 9 * [ "columns", "filter", "resizable", "stickyHeaders", "uitheme", "saveSort" ] 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 caption : 'caption', 21 header : 'bootstrap-header', // give the header a gradient background 22 footerRow : '', 23 footerCells: '', 24 icons : '', // add "icon-white" to make them white; this icon class is added to the <i> in the header 25 sortNone : 'bootstrap-icon-unsorted', 26 sortAsc : 'icon-chevron-up glyphicon glyphicon-chevron-up', 27 sortDesc : 'icon-chevron-down glyphicon glyphicon-chevron-down', 28 active : '', // applied when column is sorted 29 hover : '', // use custom css here - bootstrap class may not override it 30 filterRow : '', // filter row class 31 even : '', // even row zebra striping 32 odd : '' // odd row zebra striping 33 }, 34 "jui" : { 35 table : 'ui-widget ui-widget-content ui-corner-all', // table classes 36 caption : 'ui-widget-content ui-corner-all', 37 header : 'ui-widget-header ui-corner-all ui-state-default', // header classes 38 footerRow : '', 39 footerCells: '', 40 icons : 'ui-icon', // icon class added to the <i> in the header 41 sortNone : 'ui-icon-carat-2-n-s', 42 sortAsc : 'ui-icon-carat-1-n', 43 sortDesc : 'ui-icon-carat-1-s', 44 active : 'ui-state-active', // applied when column is sorted 45 hover : 'ui-state-hover', // hover class 46 filterRow : '', 47 even : 'ui-widget-content', // even row zebra striping 48 odd : 'ui-state-default' // odd row zebra striping 49 } 50}; 51 52$.extend(ts.css, { 53 filterRow : 'tablesorter-filter-row', // filter 54 filter : 'tablesorter-filter', 55 wrapper : 'tablesorter-wrapper', // ui theme & resizable 56 resizer : 'tablesorter-resizer', // resizable 57 sticky : 'tablesorter-stickyHeader', // stickyHeader 58 stickyVis : 'tablesorter-sticky-visible' 59}); 60 61// *** Store data in local storage, with a cookie fallback *** 62/* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json) 63 if you need it, then include https://github.com/douglascrockford/JSON-js 64 65 $.parseJSON is not available is jQuery versions older than 1.4.1, using older 66 versions will only allow storing information for one page at a time 67 68 // *** Save data (JSON format only) *** 69 // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid 70 var val = { "mywidget" : "data1" }; // valid JSON uses double quotes 71 // $.tablesorter.storage(table, key, val); 72 $.tablesorter.storage(table, 'tablesorter-mywidget', val); 73 74 // *** Get data: $.tablesorter.storage(table, key); *** 75 v = $.tablesorter.storage(table, 'tablesorter-mywidget'); 76 // val may be empty, so also check for your data 77 val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : ''; 78 alert(val); // "data1" if saved, or "" if not 79*/ 80ts.storage = function(table, key, value, options) { 81 table = $(table)[0]; 82 var cookieIndex, cookies, date, 83 hasLocalStorage = false, 84 values = {}, 85 c = table.config, 86 $table = $(table), 87 id = options && options.id || $table.attr(options && options.group || 88 'data-table-group') || table.id || $('.tablesorter').index( $table ), 89 url = options && options.url || $table.attr(options && options.page || 90 'data-table-page') || c && c.fixedUrl || window.location.pathname; 91 // https://gist.github.com/paulirish/5558557 92 if ("localStorage" in window) { 93 try { 94 window.localStorage.setItem('_tmptest', 'temp'); 95 hasLocalStorage = true; 96 window.localStorage.removeItem('_tmptest'); 97 } catch(error) {} 98 } 99 // *** get value *** 100 if ($.parseJSON) { 101 if (hasLocalStorage) { 102 values = $.parseJSON(localStorage[key] || '{}'); 103 } else { 104 // old browser, using cookies 105 cookies = document.cookie.split(/[;\s|=]/); 106 // add one to get from the key to the value 107 cookieIndex = $.inArray(key, cookies) + 1; 108 values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || '{}') : {}; 109 } 110 } 111 // allow value to be an empty string too 112 if ((value || value === '') && window.JSON && JSON.hasOwnProperty('stringify')) { 113 // add unique identifiers = url pathname > table ID/index on page > data 114 if (!values[url]) { 115 values[url] = {}; 116 } 117 values[url][id] = value;
118 // *** set value *** 119 if (hasLocalStorage) { 120 localStorage[key] = JSON.stringify(values); 121 } else { 122 date = new Date(); 123 date.setTime(date.getTime() + (31536e+6)); // 365 days 124 document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g,'\"') + '; expires=' + date.toGMTString() + '; path=/'; 125 } 126 } else { 127 return values && values[url] ? values[url][id] : ''; 128 } 129}; 130 131// Add a resize event to table headers 132// ************************** 133ts.addHeaderResizeEvent = function(table, disable, settings) { 134 table = $(table)[0]; // make sure we're usig a dom element 135 var headers, 136 defaults = { 137 timer : 250 138 }, 139 options = $.extend({}, defaults, settings), 140 c = table.config, 141 wo = c.widgetOptions, 142 checkSizes = function(triggerEvent) { 143 wo.resize_flag = true; 144 headers = []; 145 c.$headers.each(function() { 146 var $header = $(this), 147 sizes = $header.data('savedSizes') || [0,0], // fixes #394 148 width = this.offsetWidth, 149 height = this.offsetHeight; 150 if (width !== sizes[0] || height !== sizes[1]) { 151 $header.data('savedSizes', [ width, height ]); 152 headers.push(this); 153 } 154 }); 155 if (headers.length && triggerEvent !== false) { 156 c.$table.trigger('resize', [ headers ]); 157 } 158 wo.resize_flag = false; 159 }; 160 checkSizes(false); 161 clearInterval(wo.resize_timer); 162 if (disable) { 163 wo.resize_flag = false; 164 return false; 165 } 166 wo.resize_timer = setInterval(function() { 167 if (wo.resize_flag) { return; } 168 checkSizes(); 169 }, options.timer); 170}; 171 172// Widget: General UI theme 173// "uitheme" option in "widgetOptions" 174// ************************** 175ts.addWidget({ 176 id: "uitheme", 177 priority: 10, 178 format: function(table, c, wo) { 179 var i, time, classes, $header, $icon, $tfoot, $h, 180 themesAll = ts.themes, 181 $table = c.$table, 182 $headers = c.$headers, 183 theme = c.theme || 'jui', 184 themes = themesAll[theme] || themesAll.jui, 185 remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc; 186 if (c.debug) { time = new Date(); } 187 // initialization code - run once 188 if (!$table.hasClass('tablesorter-' + theme) || c.theme === theme || !table.hasInitialized) { 189 // update zebra stripes 190 if (themes.even !== '') { wo.zebra[0] += ' ' + themes.even; } 191 if (themes.odd !== '') { wo.zebra[1] += ' ' + themes.odd; } 192 // add caption style 193 $table.find('caption').addClass(themes.caption); 194 // add table/footer class names 195 $tfoot = $table 196 // remove other selected themes 197 .removeClass( c.theme === '' ? '' : 'tablesorter-' + c.theme ) 198 .addClass('tablesorter-' + theme + ' ' + themes.table) // add theme widget class name 199 .find('tfoot'); 200 if ($tfoot.length) { 201 $tfoot 202 .find('tr').addClass(themes.footerRow) 203 .children('th, td').addClass(themes.footerCells); 204 } 205 // update header classes 206 $headers 207 .addClass(themes.header) 208 .not('.sorter-false') 209 .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) { 210 // toggleClass with switch added in jQuery 1.3 211 $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover); 212 }); 213 if (!$headers.find('.' + ts.css.wrapper).length) { 214 // Firefox needs this inner div to position the resizer correctly 215 $headers.wrapInner('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>'); 216 } 217 if (c.cssIcon) { 218 // if c.cssIcon is '', then no <i> is added to the header 219 $headers.find('.' + ts.css.icon).addClass(themes.icons); 220 } 221 if ($table.hasClass('hasFilters')) { 222 $headers.find('.' + ts.css.filterRow).addClass(themes.filterRow); 223 } 224 } 225 for (i = 0; i < c.columns; i++) { 226 $header = c.$headers.add(c.$extraHeaders).filter('[data-column="' + i + '"]'); 227 $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $header; 228 $h = c.$headers.filter('[data-column="' + i + '"]:last'); 229 if ($h.length) { 230 if ($h[0].sortDisabled) { 231 // no sort arrows for disabled columns! 232 $header.removeClass(remove); 233 $icon.removeClass(remove + ' ' + themes.icons); 234 } else { 235 classes = ($header.hasClass(ts.css.sortAsc)) ? 236 themes.sortAsc :
vendor: 7,751 bytes, lines 237-407
237 ($header.hasClass(ts.css.sortDesc)) ? themes.sortDesc : 238 $header.hasClass(ts.css.header) ? themes.sortNone : ''; 239 $header[classes === themes.sortNone ? 'removeClass' : 'addClass'](themes.active); 240 $icon.removeClass(remove).addClass(classes); 241 } 242 } 243 } 244 if (c.debug) { 245 ts.benchmark("Applying " + theme + " theme", time); 246 } 247 }, 248 remove: function(table, c) { 249 var $table = c.$table, 250 theme = c.theme || 'jui', 251 themes = ts.themes[ theme ] || ts.themes.jui, 252 $headers = $table.children('thead').children(), 253 remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc; 254 $table 255 .removeClass('tablesorter-' + theme + ' ' + themes.table) 256 .find(ts.css.header).removeClass(themes.header); 257 $headers 258 .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover 259 .removeClass(themes.hover + ' ' + remove + ' ' + themes.active) 260 .find('.' + ts.css.filterRow) 261 .removeClass(themes.filterRow); 262 $headers.find('.' + ts.css.icon).removeClass(themes.icons); 263 } 264}); 265 266// Widget: Column styles 267// "columns", "columns_thead" (true) and 268// "columns_tfoot" (true) options in "widgetOptions" 269// ************************** 270ts.addWidget({ 271 id: "columns", 272 priority: 30, 273 options : { 274 columns : [ "primary", "secondary", "tertiary" ] 275 }, 276 format: function(table, c, wo) { 277 var time, $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx, 278 $table = c.$table, 279 $tbodies = c.$tbodies, 280 sortList = c.sortList, 281 len = sortList.length, 282 // removed c.widgetColumns support 283 css = wo && wo.columns || [ "primary", "secondary", "tertiary" ], 284 last = css.length - 1; 285 remove = css.join(' '); 286 if (c.debug) { 287 time = new Date(); 288 } 289 // check if there is a sort (on initialization there may not be one) 290 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 291 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody 292 $rows = $tbody.children('tr'); 293 // loop through the visible rows 294 $rows.each(function() { 295 $row = $(this); 296 if (this.style.display !== 'none') { 297 // remove all columns class names 298 $cells = $row.children().removeClass(remove); 299 // add appropriate column class names 300 if (sortList && sortList[0]) { 301 // primary sort column class 302 $cells.eq(sortList[0][0]).addClass(css[0]); 303 if (len > 1) { 304 for (indx = 1; indx < len; indx++) { 305 // secondary, tertiary, etc sort column classes 306 $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] ); 307 } 308 } 309 } 310 } 311 }); 312 ts.processTbody(table, $tbody, false); 313 } 314 // add classes to thead and tfoot 315 rows = wo.columns_thead !== false ? ['thead tr'] : []; 316 if (wo.columns_tfoot !== false) { 317 rows.push('tfoot tr'); 318 } 319 if (rows.length) { 320 $rows = $table.find( rows.join(',') ).children().removeClass(remove); 321 if (len) { 322 for (indx = 0; indx < len; indx++) { 323 // add primary. secondary, tertiary, etc sort column classes 324 $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]); 325 } 326 } 327 } 328 if (c.debug) { 329 ts.benchmark("Applying Columns widget", time); 330 } 331 }, 332 remove: function(table, c, wo) { 333 var tbodyIndex, $tbody, 334 $tbodies = c.$tbodies, 335 remove = (wo.columns || [ "primary", "secondary", "tertiary" ]).join(' '); 336 c.$headers.removeClass(remove); 337 c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove); 338 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 339 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody 340 $tbody.children('tr').each(function() { 341 $(this).children().removeClass(remove); 342 }); 343 ts.processTbody(table, $tbody, false); // restore tbody 344 } 345 } 346}); 347 348// Widget: filter 349// ************************** 350ts.addWidget({ 351 id: "filter", 352 priority: 50, 353 options : { 354 filter_childRows : false, // if true, filter includes child row content in the search 355 filter_columnFilters : true, // if true, a filter will be added to the top of each table column 356 filter_cssFilter : '', // css class name added to the filter row & each input in the row (tablesorter-filter is ALWAYS added) 357 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. 358 filter_excludeFilter : {}, // filters to exclude, per column 359 filter_external : '', // jQuery selector string (or jQuery object) of external filters 360 filter_filteredRow : 'filtered', // class added to filtered rows; needed by pager plugin 361 filter_formatter : null, // add custom filter elements to the filter row 362 filter_functions : null, // add custom filter functions using this option 363 filter_hideEmpty : true, // hide filter row when table is empty 364 filter_hideFilters : false, // collapse filter row when mouse leaves the area 365 filter_ignoreCase : true, // if true, make all searches case-insensitive 366 filter_liveSearch : true, // if true, search column content while the user types (with a delay) 367 filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available (visible) options within the drop down 368 filter_placeholder : { search : '', select : '' }, // default placeholder text (overridden by any header "data-placeholder" setting) 369 filter_reset : null, // jQuery selector string of an element used to reset the filters 370 filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters 371 filter_searchDelay : 300, // typing delay in milliseconds before starting a search 372 filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true 373 filter_selectSource : null, // include a function to return an array of values to be added to the column filter select 374 filter_startsWith : false, // if true, filter start from the beginning of the cell contents 375 filter_useParsedData : false, // filter all data using parsed content 376 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. 377 filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value 378 filter_selectSourceSeparator : '|' // filter_selectSource array text left of the separator is added to the option value, right into the option text 379 }, 380 format: function(table, c, wo) { 381 if (!c.$table.hasClass('hasFilters')) { 382 ts.filter.init(table, c, wo); 383 } 384 }, 385 remove: function(table, c, wo) { 386 var tbodyIndex, $tbody, 387 $table = c.$table, 388 $tbodies = c.$tbodies; 389 $table 390 .removeClass('hasFilters') 391 // add .tsfilter namespace to all BUT search 392 .unbind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join(c.namespace + 'filter ')) 393 .find('.' + ts.css.filterRow).remove(); 394 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 395 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody 396 $tbody.children().removeClass(wo.filter_filteredRow).show(); 397 ts.processTbody(table, $tbody, false); // restore tbody 398 } 399 if (wo.filter_reset) { 400 $(document).undelegate(wo.filter_reset, 'click.tsfilter'); 401 } 402 } 403}); 404 405ts.filter = { 406 407 // regex used in filter "check" functions - not for general use and n
407ot documented 408 regex: { 409 regex : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex 410 child : /tablesorter-childRow/, // child row class name; this gets updated in the script 411 filtered : /filtered/, // filtered (hidden) row class name; updated in the script 412 type : /undefined|number/, // check type 413 exact : /(^[\"|\'|=]+)|([\"|\'|=]+$)/g, // exact match (allow '==') 414 nondigit : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser) 415 operators : /[<>=]/g, // replace operators 416 query : '(q|query)' // replace filter queries 417 }, 418 // function( c, data ) { } 419 // c = table.config 420 // data.filter = array of filter input values; data.iFilter = same array, except lowercase 421 // data.exact = table cell text (or parsed data if column parser enabled) 422 // data.iExact = same as data.exact, except lowercase 423 // data.cache = table cell text from cache, so it has been parsed 424 // data.index = column index; table = table element (DOM) 425 // data.parsed = array (by column) of boolean values (from filter_useParsedData or "filter-parsed" class) 426 types: { 427 // Look for regex 428 regex: function( c, data ) { 429 if ( ts.filter.regex.regex.test(data.iFilter) ) { 430 var matches, 431 regex = ts.filter.regex.regex.exec(data.iFilter); 432 try { 433 matches = new RegExp(regex[1], regex[2]).test( data.iExact ); 434 } catch (error) { 435 matches = false; 436 } 437 return matches; 438 } 439 return null; 440 }, 441 // Look for operators >, >=, < or <= 442 operators: function( c, data ) { 443 if ( /^[<>]=?/.test(data.iFilter) ) { 444 var cachedValue, result, 445 table = c.table, 446 index = data.index, 447 parsed = data.parsed[index], 448 query = ts.formatFloat( data.iFilter.replace(ts.filter.regex.operators, ''), table ), 449 parser = c.parsers[index], 450 savedSearch = query; 451 // parse filter value in case we're comparing numbers (dates) 452 if (parsed || parser.type === 'numeric') { 453 result = ts.filter.parseFilter(c, $.trim('' + data.iFilter.replace(ts.filter.regex.operators, '')), index, parsed, true); 454 query = ( typeof result === "number" && result !== '' && !isNaN(result) ) ? result : query; 455 } 456 457 // iExact may be numeric - see issue #149; 458 // check if cached is defined, because sometimes j goes out of range? (numeric columns) 459 cachedValue = ( parsed || parser.type === 'numeric' ) && !isNaN(query) && typeof data.cache !== 'undefined' ? data.cache : 460 isNaN(data.iExact) ? ts.formatFloat( data.iExact.replace(ts.filter.regex.nondigit, ''), table) : 461 ts.formatFloat( data.iExact, table ); 462 463 if ( />/.test(data.iFilter) ) { result = />=/.test(data.iFilter) ? cachedValue >= query : cachedValue > query; } 464 if ( /</.test(data.iFilter) ) { result = /<=/.test(data.iFilter) ? cachedValue <= query : cachedValue < query; } 465 // keep showing all rows if nothing follows the operator 466 if ( !result && savedSearch === '' ) { result = true; } 467 return result; 468 } 469 return null; 470 }, 471 // Look for a not match 472 notMatch: function( c, data ) { 473 if ( /^\!/.test(data.iFilter) ) { 474 var indx, 475 filter = ts.filter.parseFilter(c, data.iFilter.replace('!', ''), data.index, data.parsed[data.index]); 476 if (ts.filter.regex.exact.test(filter)) { 477 // look for exact not matches - see #628 478 filter = filter.replace(ts.filter.regex.exact, ''); 479 return filter === '' ? true : $.trim(filter) !== data.iExact; 480 } else { 481 indx = data.iExact.search( $.trim(filter) ); 482 return filter === '' ? true : !(c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 0); 483 } 484 } 485 return null; 486 }, 487 // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric 488 exact: function( c, data ) { 489 /*jshint eqeqeq:false */ 490 if (ts.filter.regex.exact.test(data.iFilter)) { 491 var filter = ts.filter.parseFilter(c, data.iFilter.replace(ts.filter.regex.exact, ''), data.index, data.parsed[data.index]); 492 return data.anyMatch ? $.inArray(filter, data.rowArray) >= 0 : filter == data.iExact; 493 } 494 return null; 495 }, 496 // Look for an AND or && operator (logical and) 497 and : function( c, data ) { 498 if ( ts.filter.regex.andTest.test(data.filter) ) { 499 var index = data.index, 500 parsed = data.parsed[index], 501 query = data.iFilter.split( ts.filter.regex.andSplit ), 502 result = data.iExact.search( $.trim( ts.filter.parseFilter(c, query[0], index, parsed) ) ) >= 0, 503 indx = query.length - 1; 504 while (result && indx) { 505 result = result && data.iExact.search( $.trim( ts.filter.parseFilter(c, query[indx], index, parsed) ) ) >= 0; 506 indx--; 507 } 508 return result; 509 } 510 return null; 511 }, 512 // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu! 513 range : function( c, data ) { 514 if ( ts.filter.regex.toTest.test(data.iFilter) ) { 515 var result, tmp, 516 table = c.table, 517 index = data.index, 518 parsed = data.parsed[index], 519 // make sure the dash is for a range and not indicating a negative number 520 query = data.iFilter.split( ts.filter.regex.toSplit ), 521 range1 = ts.formatFloat( ts.filter.parseFilter(c, query[0].replace(ts.filter.regex.nondigit, ''), index, parsed), table ),
522 range2 = ts.formatFloat( ts.filter.parseFilter(c, query[1].replace(ts.filter.regex.nondigit, ''), index, parsed), table ); 523 // parse filter value in case we're comparing numbers (dates) 524 if (parsed || c.parsers[index].type === 'numeric') { 525 result = c.parsers[index].format('' + query[0], table, c.$headers.eq(index), index); 526 range1 = (result !== '' && !isNaN(result)) ? result : range1; 527 result = c.parsers[index].format('' + query[1], table, c.$headers.eq(index), index); 528 range2 = (result !== '' && !isNaN(result)) ? result : range2; 529 } 530 result = ( parsed || c.parsers[index].type === 'numeric' ) && !isNaN(range1) && !isNaN(range2) ? data.cache : 531 isNaN(data.iExact) ? ts.formatFloat( data.iExact.replace(ts.filter.regex.nondigit, ''), table) : 532 ts.formatFloat( data.iExact, table ); 533 if (range1 > range2) { tmp = range1; range1 = range2; range2 = tmp; } // swap 534 return (result >= range1 && result <= range2) || (range1 === '' || range2 === ''); 535 } 536 return null; 537 }, 538 // Look for wild card: ? = single, * = multiple, or | = logical OR 539 wild : function( c, data ) { 540 if ( /[\?|\*]/.test(data.iFilter) || ts.filter.regex.orReplace.test(data.filter) ) { 541 var index = data.index, 542 parsed = data.parsed[index], 543 query = ts.filter.parseFilter(c, data.iFilter.replace(ts.filter.regex.orReplace, "|"), index, parsed); 544 // look for an exact match with the "or" unless the "filter-match" class is found 545 if (!c.$headers.filter('[data-column="' + index + '"]:last').hasClass('filter-match') && /\|/.test(query)) { 546 query = data.anyMatch && $.isArray(data.rowArray) ? '(' + query + ')' : '^(' + query + ')$'; 547 } 548 // parsing the filter may not work properly when using wildcards =/ 549 return new RegExp( query.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(data.iExact); 550 } 551 return null; 552 }, 553 // fuzzy text search; modified from https://github.com/mattyork/fuzzy (MIT license) 554 fuzzy: function( c, data ) { 555 if ( /^~/.test(data.iFilter) ) { 556 var indx, 557 patternIndx = 0, 558 len = data.iExact.length, 559 pattern = ts.filter.parseFilter(c, data.iFilter.slice(1), data.index, data.parsed[data.index]); 560 for (indx = 0; indx < len; indx++) { 561 if (data.iExact[indx] === pattern[patternIndx]) { 562 patternIndx += 1; 563 } 564 } 565 if (patternIndx === pattern.length) { 566 return true; 567 } 568 return false; 569 } 570 return null; 571 } 572 }, 573 init: function(table, c, wo) { 574 // filter language options 575 ts.language = $.extend(true, {}, { 576 to : 'to', 577 or : 'or', 578 and : 'and' 579 }, ts.language); 580 581 var options, string, txt, $header, column, filters, val, time, fxn, noSelect, 582 regex = ts.filter.regex; 583 if (c.debug) { 584 time = new Date(); 585 } 586 c.$table.addClass('hasFilters'); 587 588 // define timers so using clearTimeout won't cause an undefined error 589 wo.searchTimer = null; 590 wo.filter_initTimer = null; 591 wo.filter_formatterCount = 0; 592 wo.filter_formatterInit = []; 593 wo.filter_initializing = true; 594 595 txt = '\\{' + ts.filter.regex.query + '\\}'; 596 $.extend( regex, { 597 child : new RegExp(c.cssChildRow), 598 filtered : new RegExp(wo.filter_filteredRow), 599 alreadyFiltered : new RegExp('(\\s+(' + ts.language.or + '|-|' + ts.language.to + ')\\s+)', 'i'), 600 toTest : new RegExp('\\s+(-|' + ts.language.to + ')\\s+', 'i'), 601 toSplit : new RegExp('(?:\\s+(?:-|' + ts.language.to + ')\\s+)' ,'gi'), 602 andTest : new RegExp('\\s+(' + ts.language.and + '|&&)\\s+', 'i'), 603 andSplit : new RegExp('(?:\\s+(?:' + ts.language.and + '|&&)\\s+)', 'gi'), 604 orReplace : new RegExp('\\s+(' + ts.language.or + ')\\s+', 'gi'), 605 iQuery : new RegExp(txt, 'i'), 606 igQuery : new RegExp(txt, 'ig') 607 }); 608 609 // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156 610 if (wo.filter_columnFilters !== false && c.$headers.filter('.filter-false, .parser-false').length !== c.$headers.length) { 611 // build filter row 612 ts.filter.buildRow(table, c, wo); 613 } 614 615 c.$table.bind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join(c.namespace + 'filter '), function(event, filter) { 616 c.$table.find('.' + ts.css.filterRow).toggle( !(wo.filter_hideEmpty && $.isEmptyObject(c.cache) && !(c.delayInit && event.type === 'appendCache')) ); // fixes #450 617 if ( !/(search|filter)/.test(event.type) ) { 618 event.stopPropagation(); 619 ts.filter.buildDefault(table, true); 620 } 621 if (event.type === 'filterReset') { 622 c.$table.find('.' + ts.css.filter).add(wo.filter_$externalFilters).val(''); 623 ts.filter.searching(table, []); 624 } else if (event.type === 'filterEnd') { 625 ts.filter.buildDefault(table, true); 626 } else { 627 // send false argument to force a new search; otherwise if the filter hasn't changed, it will return 628 filter = event.type === 'search' ? filter : event.type === 'updateComplete' ? c.$table.data('lastSearch') : ''; 629 if (/(update|add)/.test(event.type) && event.type !== "updateComplete") { 630 // force a new search since content has changed 631 c.lastCombinedFilter = null; 632 c.lastSearch = []; 633 } 634 // pass true (skipFirst) to prevent the tablesorter.setFilters function from skipping the first input 635 // ensures all inputs are updated when a search is triggered on the table $('table').trigger('search', [...]); 636 ts.filter.searching(table, filter, true); 637 } 638 return false; 639 }); 640 641 // reset button/link 642 if (wo.filter_reset) { 643 if (wo.filter_reset instanceof $) { 644 // reset contains a jQuery object, bind to it 645 wo.filter_reset.click(function(){ 646 c.$table.trigger('filterReset'); 647 }); 648 } else if ($(wo.filter_reset).length) { 649 // reset is a jQuery selector, use event delegation 650 $(document) 651 .undelegate(wo.filter_reset, 'click.tsfilter') 652 .delegate(wo.filter_reset, 'click.tsfilter', function() { 653 // trigger a reset event, so other functions (filterFormatter) know when to reset 654 c.$table.trigger('filterReset'); 655 }); 656 } 657 } 658 if (wo.filter_functions) { 659 for (column = 0; column < c.columns; column++) { 660 fxn = ts.getColumnData( table, wo.filter_functions, column ); 661 if (fxn) { 662 // remove "filter-select" from header otherwise the options added here are replaced with all options 663 $header = c.$headers.filter('[data-column="' + column + '"]:last').removeClass('filter-select'); 664 // don't build select if "filter-false" or "parser-false" set 665 noSelect = !($header.hasClass('filter-false') || $header.hasClass('parser-false')); 666 options = ''; 667 if ( fxn === true && noSelect ) { 668 ts.filter.buildSelect(table, column); 669 } else if ( typeof fxn === 'object' && noSelect ) { 670 // add custom drop down list 671 for (string in fxn) { 672 if (typeof string === 'string') { 673 options += options === '' ? 674 '<option value="">' + ($header.data('placeholder') || $header.attr('data-placeholder') || wo.filter_placeholder.select || '') + '</option>' : ''; 675 val = string; 676 txt = string; 677 if (string.indexOf(wo.filter_selectSourceSeparator) >= 0) { 678 val = string.split(wo.filter_selectSourceSeparator); 679 txt = val[1]; 680 val = val[0]; 681 } 682 options += '<option ' + (txt === val ? '' : 'data-function-name="' + string + '" ') + 'value="' + val + '">' + txt + '</option>'; 683 } 684 } 685 c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]').append(options); 686 } 687 } 688 } 689 } 690 // not really updating, but if the column has both the "filter-select" class & filter_functions set to true, 691 // it would append the same options twice. 692 ts.filter.buildDefault(table, true); 693 694 ts.filter.bindSearch( table, c.$table.find('.' + ts.css.filter), true ); 695 if (wo.filter_external) { 696 ts.filter.bindSearch( table, wo.filter_external ); 697 } 698 699 if (wo.filter_hideFilters) { 700 ts.filter.hideFilters(table, c); 701 } 702 703 // show processing icon 704 if (c.showProcessing) { 705 c.$table.bind('filterStart' + c.namespace + 'filter filterEnd' + c.namespace + 'filter', function(event, columns) { 706 // only add processing to certain columns to all columns 707 $header = (columns) ? c.$table.find('.' + ts.css.header).filter('[data-column]').filter(function() { 708 return columns[$(this).data('column')] !== ''; 709 }) : ''; 710 ts.isProcessing(table, event.type === 'filterStart', columns ? $header : ''); 711 }); 712 } 713 714 // set filtered rows count (intially unfiltered) 715 c.filteredRows = c.totalRows; 716 717 if (c.debug) { 718 ts.benchmark("Applying Filter widget", time); 719 } 720 // add default values 721 c.$table.bind('tablesorter-initialized pagerInitialized', function() { 722 // redefine "wo" as it does not update properly inside this callback 723 var wo = this.config.widgetOptions; 724 filters = ts.filter.setDefaults(table, c, wo) || []; 725 if (filters.length) { 726 // prevent delayInit from triggering a cache build if filters are empty 727 if ( !(c.delayInit && filters.join('') === '') ) { 728 ts.setFilters(table, filters, true); 729 } 730 } 731 c.$table.trigger('filterFomatterUpdate'); 732 // trigger init after setTimeout to prevent multiple filterStart/End/Init triggers 733 setTimeout(function(){ 734 if (!wo.filter_initialized) { 735 ts.filter.filterInitComplete(c); 736 } 737 }, 100); 738 }); 739 // if filter widget is added after pager has initialized; then set filter init flag 740 if (c.pager && c.pager.initialized && !wo.filter_initialized) { 741 c.$table.trigger('filterFomatterUpdate'); 742 setTimeout(function(){ 743 ts.filter.filterInitComplete(c); 744 }, 100); 745 } 746 }, 747 // $cell parameter, but not the config, is passed to the 748 // filter_formatters, so we have to work with it instead 749 formatterUpdated: function($cell, column) { 750 var wo = $cell.closest('table')[0].config.widgetOptions; 751 if (!wo.filter_initialized) { 752 // add updates by column since this function 753 // may be called numerous times before initialization 754 wo.filter_formatterInit[column] = 1; 755 } 756 }, 757 filterInitComplete: function(c){ 758 var wo = c.widgetOptions, 759 count = 0, 760 completed = function(){ 761 wo.filter_initialized = true; 762 wo.filter_initializing = false; 763 ts.filter.findRows(c.table, c.$table.data('lastSearch'), null); 764 c.$table.trigger('filterInit', c); 765 }; 766 $.each( wo.filter_formatterInit, function(i, val) { 767 if (val === 1) { 768 count++; 769 } 770 }); 771 clearTimeout(wo.filter_initTimer); 772 if (!wo.filter_initialized && count === wo.filter_formatterCount) { 773 // filter widget initialized 774 completed(); 775 } else if (!wo.filter_initialized) { 776 // fall back in case a filter_formatter doesn't call 777 // $.tablesorter.filter.formatterUpdated($cell, column), and the count is off 778 wo.filter_initTimer = setTimeout(function(){ 779 completed(); 780 }, 500); 781 } 782 }, 783 784 setDefaults: function(table, c, wo) { 785 var isArray, saved, indx, 786 // get current (default) filters 787 filters = ts.getFilters(table) || []; 788 if (wo.filter_saveFilters && ts.storage) { 789 saved = ts.storage( table, 'tablesorter-filters' ) || []; 790 isArray = $.isArray(saved); 791 // make sure we're not just getting an empty array 792 if ( !(isArray && saved.join('') === '' || !isArray) ) { filters = saved; } 793 } 794 // if no filters saved, then check default settings 795 if (filters.join('') === '') { 796 for (indx = 0; indx < c.columns; indx++) { 797 filters[indx] = c.$headers.filter('[data-column="' + indx + '"]:last').attr(wo.filter_defaultAttrib) || filters[indx]; 798 } 799 } 800 c.$table.data('lastSearch', filters); 801 return filters; 802 }, 803 parseFilter: function(c, filter, column, parsed, forceParse){ 804 return forceParse || parsed ? 805 c.parsers[column].format( filter, c.table, [], column ) : 806 filter; 807 }, 808 buildRow: function(table, c, wo) { 809 var col, column, $header, buildSelect, disabled, name, ffxn, 810 // c.columns defined in computeThIndexes() 811 columns = c.columns, 812 buildFilter = '<tr role="row" class="' + ts.css.filterRow + '">'; 813 for (column = 0; column < columns; column++) { 814 buildFilter += '<td></td>'; 815 } 816 c.$filters = $(buildFilter += '</tr>').appendTo( c.$table.children('thead').eq(0) ).find('td'); 817 // build each filter input 818 for (column = 0; column < columns; column++) { 819 disabled = false; 820 // assuming last cell of a column is the main column 821 $header = c.$headers.filter('[data-column="' + column + '"]:last');
822 ffxn = ts.getColumnData( table, wo.filter_functions, column ); 823 buildSelect = (wo.filter_functions && ffxn && typeof ffxn !== "function" ) || 824 $header.hasClass('filter-select'); 825 // get data from jQuery data, metadata, headers option or header class name 826 col = ts.getColumnData( table, c.headers, column ); 827 disabled = ts.getData($header[0], col, 'filter') === 'false' || ts.getData($header[0], col, 'parser') === 'false'; 828 829 if (buildSelect) { 830 buildFilter = $('<select>').appendTo( c.$filters.eq(column) ); 831 } else { 832 ffxn = ts.getColumnData( table, wo.filter_formatter, column ); 833 if (ffxn) { 834 wo.filter_formatterCount++; 835 buildFilter = ffxn( c.$filters.eq(column), column ); 836 // no element returned, so lets go find it 837 if (buildFilter && buildFilter.length === 0) { 838 buildFilter = c.$filters.eq(column).children('input'); 839 } 840 // element not in DOM, so lets attach it 841 if ( buildFilter && (buildFilter.parent().length === 0 || 842 (buildFilter.parent().length && buildFilter.parent()[0] !== c.$filters[column])) ) { 843 c.$filters.eq(column).append(buildFilter); 844 } 845 } else { 846 buildFilter = $('<input type="search">').appendTo( c.$filters.eq(column) ); 847 } 848 if (buildFilter) { 849 buildFilter.attr('placeholder', $header.data('placeholder') || $header.attr('data-placeholder') || wo.filter_placeholder.search || ''); 850 } 851 } 852 if (buildFilter) { 853 // add filter class name 854 name = ( $.isArray(wo.filter_cssFilter) ? 855 (typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '') : 856 wo.filter_cssFilter ) || ''; 857 buildFilter.addClass( ts.css.filter + ' ' + name ).attr('data-column', column); 858 if (disabled) { 859 buildFilter.attr('placeholder', '').addClass('disabled')[0].disabled = true; // disabled! 860 } 861 } 862 } 863 }, 864 bindSearch: function(table, $el, internal) { 865 table = $(table)[0]; 866 $el = $($el); // allow passing a selector string 867 if (!$el.length) { return; } 868 var c = table.config, 869 wo = c.widgetOptions, 870 $ext = wo.filter_$externalFilters; 871 if (internal !== true) { 872 // save anyMatch element 873 wo.filter_$anyMatch = $el.filter('[data-column="all"]'); 874 if ($ext && $ext.length) { 875 wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el ); 876 } else { 877 wo.filter_$externalFilters = $el; 878 } 879 // update values (external filters added after table initialization) 880 ts.setFilters(table, c.$table.data('lastSearch') || [], internal === false); 881 } 882 $el 883 // use data attribute instead of jQuery data since the head is cloned without including the data/binding 884 .attr('data-lastSearchTime', new Date().getTime()) 885 .unbind('keypress keyup search change '.split(' ').join(c.namespace + 'filter ')) 886 // include change for select - fixes #473 887 .bind('keyup' + c.namespace + 'filter', function(event) { 888 $(this).attr('data-lastSearchTime', new Date().getTime()); 889 // emulate what webkit does.... escape clears the filter 890 if (event.which === 27) { 891 this.value = ''; 892 // live search 893 } else if ( wo.filter_liveSearch === false ) { 894 return; 895 // don't return if the search value is empty (all rows need to be revealed) 896 } else if ( this.value !== '' && ( 897 // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace 898 ( typeof wo.filter_liveSearch === 'number' && this.value.length < wo.filter_liveSearch ) || 899 // let return & backspace continue on, but ignore arrows & non-valid characters 900 ( event.which !== 13 && event.which !== 8 && ( event.which < 32 || (event.which >= 37 && event.which <= 40) ) ) ) ) { 901 return; 902 } 903 // change event = no delay; last true flag tells getFilters to skip newest timed input 904 ts.filter.searching( table, true, true ); 905 }) 906 .bind('search change keypress '.split(' ').join(c.namespace + 'filter '), function(event){ 907 var column = $(this).data('column'); 908 // don't allow "change" event to process if the input value is the same - fixes #685 909 if (event.which === 13 || event.type === 'search' || event.type === 'change' && this.value !== c.lastSearch[column]) { 910 event.preventDefault(); 911 // init search with no delay 912 $(this).attr('data-lastSearchTime', new Date().getTime()); 913 ts.filter.searching( table, false, true ); 914 } 915 }); 916 }, 917 searching: function(table, filter, skipFirst) { 918 var wo = table.config.widgetOptions; 919 clearTimeout(wo.searchTimer); 920 if (typeof filter === 'undefined' || filter === true) { 921 // delay filtering 922 wo.searchTimer = setTimeout(function() { 923 ts.filter.checkFilters(table, filter, skipFirst ); 924 }, wo.filter_liveSearch ? wo.filter_searchDelay : 10); 925 } else { 926 // skip delay 927 ts.filter.checkFilters(table, filter, skipFirst); 928 } 929 }, 930 checkFilters: function(table, filter, skipFirst) { 931 var c = table.config, 932 wo = c.widgetOptions, 933 filterArray = $.isArray(filter), 934 filters = (filterArray) ? filter : ts.getFilters(table, true), 935 combinedFilters = (filters || []).join('');
935 // combined filter values 936 // prevent errors if delay init is set 937 if ($.isEmptyObject(c.cache)) { 938 // update cache if delayInit set & pager has initialized (after user initiates a search) 939 if (c.delayInit && c.pager && c.pager.initialized) { 940 c.$table.trigger('updateCache', [function(){ 941 ts.filter.checkFilters(table, false, skipFirst); 942 }] ); 943 } 944 return; 945 } 946 // add filter array back into inputs 947 if (filterArray) { 948 ts.setFilters( table, filters, false, skipFirst !== true ); 949 if (!wo.filter_initialized) { c.lastCombinedFilter = ''; } 950 } 951 if (wo.filter_hideFilters) { 952 // show/hide filter row as needed 953 c.$table.find('.' + ts.css.filterRow).trigger( combinedFilters === '' ? 'mouseleave' : 'mouseenter' ); 954 } 955 // return if the last search is the same; but filter === false when updating the search 956 // see example-widget-filter.html filter toggle buttons 957 if (c.lastCombinedFilter === combinedFilters && filter !== false) { 958 return; 959 } else if (filter === false) { 960 // force filter refresh 961 c.lastCombinedFilter = null; 962 c.lastSearch = []; 963 } 964 if (wo.filter_initialized) { c.$table.trigger('filterStart', [filters]); } 965 if (c.showProcessing) { 966 // give it time for the processing icon 967 setTimeout(function() { 968 ts.filter.findRows(table, filters, combinedFilters); 969 return false; 970 }, 30); 971 } else { 972 ts.filter.findRows(table, filters, combinedFilters); 973 return false; 974 } 975 }, 976 hideFilters: function(table, c) { 977 var $filterRow, $filterRow2, timer; 978 $(table) 979 .find('.' + ts.css.filterRow) 980 .addClass('hideme') 981 .bind('mouseenter mouseleave', function(e) { 982 // save event object - http://bugs.jquery.com/ticket/12140 983 var event = e; 984 $filterRow = $(this); 985 clearTimeout(timer); 986 timer = setTimeout(function() { 987 if ( /enter|over/.test(event.type) ) { 988 $filterRow.removeClass('hideme'); 989 } else { 990 // don't hide if input has focus 991 // $(':focus') needs jQuery 1.6+ 992 if ( $(document.activeElement).closest('tr')[0] !== $filterRow[0] ) { 993 // don't hide row if any filter has a value 994 if (c.lastCombinedFilter === '') { 995 $filterRow.addClass('hideme'); 996 } 997 } 998 } 999 }, 200); 1000 }) 1001 .find('input, select').bind('focus blur', function(e) { 1002 $filterRow2 = $(this).closest('tr'); 1003 clearTimeout(timer); 1004 var event = e; 1005 timer = setTimeout(function() { 1006 // don't hide row if any filter has a value 1007 if (ts.getFilters(c.$table).join('') === '') { 1008 $filterRow2[ event.type === 'focus' ? 'removeClass' : 'addClass']('hideme'); 1009 } 1010 }, 200); 1011 }); 1012 }, 1013 defaultFilter: function(filter, mask){ 1014 if (filter === '') { return filter; } 1015 var regex = ts.filter.regex.iQuery, 1016 maskLen = mask.match( ts.filter.regex.igQuery ).length, 1017 query = maskLen > 1 ? $.trim(filter).split(/\s/) : [ $.trim(filter) ], 1018 len = query.length - 1, 1019 indx = 0, 1020 val = mask; 1021 if ( len < 1 && maskLen > 1 ) { 1022 // only one "word" in query but mask has >1 slots 1023 query[1] = query[0]; 1024 } 1025 // replace all {query} with query words... 1026 // if query = "Bob", then convert mask from "!{query}" to "!Bob" 1027 // if query = "Bob Joe Frank", then convert mask "{q} OR {q}" to "Bob OR Joe OR Frank" 1028 while (regex.test(val)) { 1029 val = val.replace(regex, query[indx++] || ''); 1030 if (regex.test(val) && indx < len && (query[indx] || '') !== '') { 1031 val = mask.replace(regex, val); 1032 } 1033 } 1034 return val; 1035 }, 1036 findRows: function(table, filters, combinedFilters) { 1037 if (table.config.lastCombinedFilter === combinedFilters || table.config.widgetOptions.filter_initializing) { return; } 1038 var len, $rows, rowIndex, tbodyIndex, $tbody, $cells, $cell, columnIndex, 1039 childRow, lastSearch, hasSelect, matches, result, showRow, time, val, indx, 1040 notFiltered, searchFiltered, filterMatched, excludeMatch, fxn, ffxn, 1041 regex = ts.filter.regex, 1042 c = table.config, 1043 wo = c.widgetOptions, 1044 $tbodies = c.$table.children('tbody'), // target all tbodies #568 1045 // data object passed to filters; anyMatch is a flag for the filters 1046 data = { anyMatch: false }, 1047 // anyMatch really screws up with these types of filters 1048 noAnyMatch = [ 'range', 'notMatch', 'operators' ]; 1049 1050 // parse columns after formatter, in case the class is added at that point 1051 data.parsed = c.$headers.map(function(columnIndex) { 1052 return c.parsers && c.parsers[columnIndex] && c.parsers[columnIndex].parsed || 1053 // getData won't return "parsed" if other "filter-" class names exist (e.g. <th class="filter-select filter-parsed">) 1054 ts.getData && ts.getData(c.$headers.filter('[data-column="' + columnIndex + '"]:last'), ts.getColumnData( table, c.headers, columnIndex ), 'filter') === 'parsed' || 1055 $(this).hasClass('filter-parsed'); 1056 }).get(); 1057 1058 if (c.debug) { time = new Date(); }
1059 // filtered rows count 1060 c.filteredRows = 0; 1061 c.totalRows = 0; 1062 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 1063 if ($tbodies.eq(tbodyIndex).hasClass(c.cssInfoBlock || ts.css.info)) { continue; } // ignore info blocks, issue #264 1064 $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); 1065 // skip child rows & widget added (removable) rows - fixes #448 thanks to @hempel! 1066 // $rows = $tbody.children('tr').not(c.selectorRemove); 1067 columnIndex = c.columns; 1068 // convert stored rows into a jQuery object 1069 $rows = $( $.map(c.cache[tbodyIndex].normalized, function(el){ return el[columnIndex].$row.get(); }) ); 1070 1071 if (combinedFilters === '' || wo.filter_serversideFiltering) { 1072 $rows.removeClass(wo.filter_filteredRow).not('.' + c.cssChildRow).show(); 1073 } else { 1074 // filter out child rows 1075 $rows = $rows.not('.' + c.cssChildRow); 1076 len = $rows.length; 1077 // optimize searching only through already filtered rows - see #313 1078 searchFiltered = wo.filter_searchFiltered; 1079 lastSearch = c.lastSearch || c.$table.data('lastSearch') || []; 1080 if (searchFiltered) { 1081 // cycle through all filters; include last (columnIndex + 1 = match any column). Fixes #669 1082 for (indx = 0; indx < columnIndex + 1; indx++) { 1083 val = filters[indx] || ''; 1084 // break out of loop if we've already determined not to search filtered rows 1085 if (!searchFiltered) { indx = columnIndex; } 1086 // search already filtered rows if... 1087 searchFiltered = searchFiltered && lastSearch.length && 1088 // there are no changes from beginning of filter 1089 val.indexOf(lastSearch[indx] || '') === 0 && 1090 // if there is NOT a logical "or", or range ("to" or "-") in the string 1091 !regex.alreadyFiltered.test(val) && 1092 // if we are not doing exact matches, using "|" (logical or) or not "!" 1093 !/[=\"\|!]/.test(val) && 1094 // don't search only filtered if the value is negative ('> -10' => '> -100' will ignore hidden rows) 1095 !(/(>=?\s*-\d)/.test(val) || /(<=?\s*\d)/.test(val)) && 1096 // if filtering using a select without a "filter-match" class (exact match) - fixes #593 1097 !( val !== '' && c.$filters && c.$filters.eq(indx).find('select').length && !c.$headers.filter('[data-column="' + indx + '"]:last').hasClass('filter-match') ); 1098 } 1099 } 1100 notFiltered = $rows.not('.' + wo.filter_filteredRow).length; 1101 // can't search when all rows are hidden - this happens when looking for exact matches 1102 if (searchFiltered && notFiltered === 0) { searchFiltered = false; } 1103 if (c.debug) { 1104 ts.log( "Searching through " + ( searchFiltered && notFiltered < len ? notFiltered : "all" ) + " rows" ); 1105 } 1106 if ((wo.filter_$anyMatch && wo.filter_$anyMatch.length) || filters[c.columns]) { 1107 data.anyMatchFlag = true; 1108 data.anyMatchFilter = wo.filter_$anyMatch && wo.filter_$anyMatch.val() || filters[c.columns] || ''; 1109 if (c.sortLocaleCompare) { 1110 // replace accents 1111 data.anyMatchFilter = ts.replaceAccents(data.anyMatchFilter); 1112 } 1113 if (wo.filter_defaultFilter && regex.iQuery.test( ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || '')) { 1114 data.anyMatchFilter = ts.filter.defaultFilter( data.anyMatchFilter, ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) ); 1115 // clear search filtered flag because default filters are not saved to the last search 1116 searchFiltered = false; 1117 } 1118 data.iAnyMatchFilter = data.anyMatchFilter; 1119 } 1120 1121 // loop through the rows 1122 for (rowIndex = 0; rowIndex < len; rowIndex++) { 1123 1124 data.cacheArray = c.cache[tbodyIndex].normalized[rowIndex]; 1125 1126 childRow = $rows[rowIndex].className; 1127 // skip child rows & already filtered rows 1128 if ( regex.child.test(childRow) || (searchFiltered && regex.filtered.test(childRow)) ) { continue; } 1129 showRow = true; 1130 // *** nextAll/nextUntil not supported by Zepto! *** 1131 childRow = $rows.eq(rowIndex).nextUntil('tr:not(.' + c.cssChildRow + ')'); 1132 // so, if "table.config.widgetOptions.filter_childRows" is true and there is 1133 // a match anywhere in the child row, then it will make the row visible 1134 // checked here so the option can be changed dynamically 1135 data.childRowText = (childRow.length && wo.filter_childRows) ? childRow.text() : ''; 1136 data.childRowText = wo.filter_ignoreCase ? data.childRowText.toLocaleLowerCase() : data.childRowText; 1137 $cells = $rows.eq(rowIndex).children();
1138 1139 if (data.anyMatchFlag) { 1140 data.anyMatch = true; 1141 data.rowArray = $cells.map(function(i){ 1142 var txt; 1143 if (data.parsed[i]) { 1144 txt = data.cacheArray[i]; 1145 } else { 1146 txt = wo.filter_ignoreCase ? $(this).text().toLowerCase() : $(this).text(); 1147 if (c.sortLocaleCompare) { 1148 txt = ts.replaceAccents(txt); 1149 } 1150 } 1151 return txt; 1152 }).get(); 1153 data.filter = data.anyMatchFilter; 1154 data.iFilter = data.iAnyMatchFilter; 1155 data.exact = data.rowArray.join(' '); 1156 data.iExact = data.exact.toLowerCase(); 1157 data.cache = data.cacheArray.slice(0,-1).join(' '); 1158 filterMatched = null; 1159 $.each(ts.filter.types, function(type, typeFunction) { 1160 if ($.inArray(type, noAnyMatch) < 0) { 1161 matches = typeFunction( c, data ); 1162 if (matches !== null) { 1163 filterMatched = matches; 1164 return false; 1165 } 1166 } 1167 }); 1168 if (filterMatched !== null) { 1169 showRow = filterMatched; 1170 } else { 1171 if (wo.filter_startsWith) { 1172 showRow = false; 1173 columnIndex = c.columns; 1174 while (!showRow && columnIndex > 0) { 1175 columnIndex--; 1176 showRow = showRow || data.rowArray[columnIndex].indexOf(data.iFilter) === 0; 1177 } 1178 } else { 1179 showRow = (data.iExact + data.childRowText).indexOf(data.iFilter) >= 0; 1180 } 1181 } 1182 data.anyMatch = false; 1183 } 1184 1185 for (columnIndex = 0; columnIndex < c.columns; columnIndex++) { 1186 data.filter = filters[columnIndex]; 1187 data.index = columnIndex; 1188 1189 // filter types to exclude, per column 1190 excludeMatch = ( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split(/\s+/); 1191 1192 // ignore if filter is empty or disabled 1193 if (data.filter) { 1194 data.cache = data.cacheArray[columnIndex]; 1195 // check if column data should be from the cell or from parsed data 1196 if (wo.filter_useParsedData || data.parsed[columnIndex]) { 1197 data.exact = data.cache; 1198 } else { 1199 // using older or original tablesorter 1200 data.exact = $.trim( $cells.eq(columnIndex).text() ); 1201 data.exact = c.sortLocaleCompare ? ts.replaceAccents(data.exact) : data.exact; // issue #405 1202 } 1203 data.iExact = !regex.type.test(typeof data.exact) && wo.filter_ignoreCase ? data.exact.toLocaleLowerCase() : data.exact; 1204 result = showRow; // if showRow is true, show that row 1205 1206 // in case select filter option has a different value vs text "a - z|A through Z" 1207 ffxn = wo.filter_columnFilters ? 1208 c.$filters.add(c.$externalFilters).filter('[data-column="'+ columnIndex + '"]').find('select option:selected').attr('data-function-name') || '' : ''; 1209 1210 // replace accents - see #357 1211 data.filter = c.sortLocaleCompare ? ts.replaceAccents(data.filter) : data.filter; 1212 1213 val = true; 1214 if (wo.filter_defaultFilter && regex.iQuery.test( ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || '')) { 1215 data.filter = ts.filter.defaultFilter( data.filter, ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) ); 1216 // val is used to indicate that a filter select is using a default filter; so we override the exact & partial matches 1217 val = false; 1218 } 1219 // data.iFilter = case insensitive, data.filter = case sensitive 1220 data.iFilter = wo.filter_ignoreCase ? (data.filter || '').toLocaleLowerCase() : data.filter; 1221 fxn = ts.getColumnData( table, wo.filter_functions, columnIndex ); 1222 $cell = c.$headers.filter('[data-column="' + columnIndex + '"]:last'); 1223 hasSelect = $cell.hasClass('filter-select'); 1224 if ( fxn || ( hasSelect && val ) ) { 1225 if (fxn === true || hasSelect) { 1226 // default selector uses exact match unless "filter-match" class is found 1227 result = ($cell.hasClass('filter-match')) ? data.iExact.search(data.iFilter) >= 0 : data.filter === data.exact; 1228 } else if (typeof fxn === 'function') { 1229 // filter callback( exact cell content, parser normalized content, filter input value, column index, jQuery row object ) 1230 result = fxn(data.exact, data.cache, data.filter, columnIndex, $rows.eq(rowIndex)); 1231 } else if (typeof fxn[ffxn || data.filter] === 'function') { 1232 // selector option function 1233 result = fxn[ffxn || data.filter](data.exact, data.cache, data.filter, columnIndex, $rows.eq(rowIndex)); 1234 } 1235 } else { 1236 filterMatched = null; 1237 // cycle through the different filters 1238 // filters return a boolean or null if nothing matches 1239 $.each(ts.filter.types, function(type, typeFunction) { 1240 if ($.inArray(type, excludeMatch) < 0) { 1241 matches = typeFunction( c, data ); 1242 if (matches !== null) { 1243 filterMatched = matches; 1244 return false; 1245 } 1246 } 1247 });
1248 if (filterMatched !== null) { 1249 result = filterMatched; 1250 // Look for match, and add child row data for matching 1251 } else { 1252 data.exact = (data.iExact + data.childRowText).indexOf( ts.filter.parseFilter(c, data.iFilter, columnIndex, data.parsed[columnIndex]) ); 1253 result = ( (!wo.filter_startsWith && data.exact >= 0) || (wo.filter_startsWith && data.exact === 0) ); 1254 } 1255 } 1256 showRow = (result) ? showRow : false; 1257 } 1258 } 1259 $rows.eq(rowIndex) 1260 .toggle(showRow) 1261 .toggleClass(wo.filter_filteredRow, !showRow); 1262 if (childRow.length) { 1263 childRow.toggleClass(wo.filter_filteredRow, !showRow); 1264 } 1265 } 1266 } 1267 c.filteredRows += $rows.not('.' + wo.filter_filteredRow).length; 1268 c.totalRows += $rows.length; 1269 ts.processTbody(table, $tbody, false); 1270 } 1271 c.lastCombinedFilter = combinedFilters; // save last search 1272 c.lastSearch = filters; 1273 c.$table.data('lastSearch', filters); 1274 if (wo.filter_saveFilters && ts.storage) { 1275 ts.storage( table, 'tablesorter-filters', filters ); 1276 } 1277 if (c.debug) { 1278 ts.benchmark("Completed filter widget search", time); 1279 } 1280 if (wo.filter_initialized) { c.$table.trigger('filterEnd', c ); } 1281 setTimeout(function(){ 1282 c.$table.trigger('applyWidgets'); // make sure zebra widget is applied 1283 }, 0); 1284 }, 1285 getOptionSource: function(table, column, onlyAvail) { 1286 var cts, 1287 c = table.config, 1288 wo = c.widgetOptions, 1289 parsed = [], 1290 arry = false, 1291 source = wo.filter_selectSource, 1292 last = c.$table.data('lastSearch') || [], 1293 fxn = $.isFunction(source) ? true : ts.getColumnData( table, source, column ); 1294 1295 if (onlyAvail && last[column] !== '') { 1296 onlyAvail = false; 1297 } 1298 1299 // filter select source option 1300 if (fxn === true) { 1301 // OVERALL source 1302 arry = source(table, column, onlyAvail); 1303 } else if ( fxn instanceof $ || ($.type(fxn) === 'string' && fxn.indexOf('</option>') >= 0) ) { 1304 // selectSource is a jQuery object or string of options 1305 return fxn; 1306 } else if ($.isArray(fxn)) { 1307 arry = fxn; 1308 } else if ($.type(source) === 'object' && fxn) { 1309 // custom select source function for a SPECIFIC COLUMN 1310 arry = fxn(table, column, onlyAvail); 1311 } 1312 if (arry === false) { 1313 // fall back to original method 1314 arry = ts.filter.getOptions(table, column, onlyAvail); 1315 } 1316 1317 // get unique elements and sort the list 1318 // if $.tablesorter.sortText exists (not in the original tablesorter), 1319 // then natural sort the list otherwise use a basic sort 1320 arry = $.grep(arry, function(value, indx) { 1321 return $.inArray(value, arry) === indx; 1322 }); 1323 1324 if (c.$headers.filter('[data-column="' + column + '"]:last').hasClass('filter-select-nosort')) { 1325 // unsorted select options 1326 return arry; 1327 } else { 1328 // parse select option values 1329 $.each(arry, function(i, v){ 1330 // parse array data using set column parser; this DOES NOT pass the original 1331 // table cell to the parser format function 1332 parsed.push({ t : v, p : c.parsers && c.parsers[column].format( v, table, [], column ) }); 1333 }); 1334 1335 // sort parsed select options 1336 cts = c.textSorter || ''; 1337 parsed.sort(function(a, b){ 1338 // sortNatural breaks if you don't pass it strings 1339 var x = a.p.toString(), y = b.p.toString(); 1340 if ($.isFunction(cts)) { 1341 // custom OVERALL text sorter 1342 return cts(x, y, true, column, table); 1343 } else if (typeof(cts) === 'object' && cts.hasOwnProperty(column)) { 1344 // custom text sorter for a SPECIFIC COLUMN 1345 return cts[column](x, y, true, column, table); 1346 } else if (ts.sortNatural) { 1347 // fall back to natural sort 1348 return ts.sortNatural(x, y); 1349 } 1350 // using an older version! do a basic sort 1351 return true; 1352 }); 1353 // rebuild arry from sorted parsed data 1354 arry = []; 1355 $.each(parsed, function(i, v){ 1356 arry.push(v.t); 1357 }); 1358 return arry; 1359 } 1360 }, 1361 getOptions: function(table, column, onlyAvail) { 1362 var rowIndex, tbodyIndex, len, row, cache, cell, 1363 c = table.config, 1364 wo = c.widgetOptions, 1365 $tbodies = c.$table.children('tbody'), 1366 arry = []; 1367 for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { 1368 if (!$tbodies.eq(tbodyIndex).hasClass(c.cssInfoBlock)) { 1369 cache = c.cache[tbodyIndex]; 1370 len = c.cache[tbodyIndex].normalized.length; 1371 // loop through the rows 1372 for (rowIndex = 0; rowIndex < len; rowIndex++) { 1373 // get cached row from cache.row (old) or row data object (new; last item in normalized array)
1374 row = cache.row ? cache.row[rowIndex] : cache.normalized[rowIndex][c.columns].$row[0]; 1375 // check if has class filtered 1376 if (onlyAvail && row.className.match(wo.filter_filteredRow)) { continue; } 1377 // get non-normalized cell content 1378 if (wo.filter_useParsedData || c.parsers[column].parsed || c.$headers.filter('[data-column="' + column + '"]:last').hasClass('filter-parsed')) { 1379 arry.push( '' + cache.normalized[rowIndex][column] ); 1380 } else { 1381 cell = row.cells[column]; 1382 if (cell) { 1383 arry.push( $.trim( cell.textContent || cell.innerText || $(cell).text() ) ); 1384 } 1385 } 1386 } 1387 } 1388 } 1389 return arry; 1390 }, 1391 buildSelect: function(table, column, arry, updating, onlyAvail) { 1392 table = $(table)[0]; 1393 column = parseInt(column, 10); 1394 if (!table.config.cache || $.isEmptyObject(table.config.cache)) { return; } 1395 var indx, val, txt, t, $filters, $filter, 1396 c = table.config, 1397 wo = c.widgetOptions, 1398 node = c.$headers.filter('[data-column="' + column + '"]:last'), 1399 // t.data('placeholder') won't work in jQuery older than 1.4.3 1400 options = '<option value="">' + ( node.data('placeholder') || node.attr('data-placeholder') || wo.filter_placeholder.select || '' ) + '</option>', 1401 // Get curent filter value 1402 currentValue = c.$table.find('thead').find('select.' + ts.css.filter + '[data-column="' + column + '"]').val(); 1403 // nothing included in arry (external source), so get the options from filter_selectSource or column data 1404 if (typeof arry === 'undefined' || arry === '') { 1405 arry = ts.filter.getOptionSource(table, column, onlyAvail); 1406 } 1407 1408 if ($.isArray(arry)) { 1409 // build option list 1410 for (indx = 0; indx < arry.length; indx++) { 1411 txt = arry[indx] = ('' + arry[indx]).replace(/\"/g, """); 1412 val = txt; 1413 // allow including a symbol in the selectSource array 1414 // "a-z|A through Z" so that "a-z" becomes the option value 1415 // and "A through Z" becomes the option text 1416 if (txt.indexOf(wo.filter_selectSourceSeparator) >= 0) { 1417 t = txt.split(wo.filter_selectSourceSeparator); 1418 val = t[0]; 1419 txt = t[1]; 1420 } 1421 // replace quotes - fixes #242 & ignore empty strings - see http://stackoverflow.com/q/14990971/145346 1422 options += arry[indx] !== '' ? '<option ' + (val === txt ? '' : 'data-function-name="' + arry[indx] + '" ') + 'value="' + val + '">' + txt + '</option>' : ''; 1423 } 1424 // clear arry so it doesn't get appended twice 1425 arry = []; 1426 } 1427 1428 // update all selects in the same column (clone thead in sticky headers & any external selects) - fixes 473 1429 $filters = ( c.$filters ? c.$filters : c.$table.children('thead') ).find('.' + ts.css.filter); 1430 if (wo.filter_$externalFilters) { 1431 $filters = $filters && $filters.length ? $filters.add(wo.filter_$externalFilters) : wo.filter_$externalFilters; 1432 } 1433 $filter = $filters.filter('select[data-column="' + column + '"]'); 1434 1435 // make sure there is a select there! 1436 if ($filter.length) { 1437 $filter[ updating ? 'html' : 'append' ](options); 1438 if (!$.isArray(arry)) { 1439 // append options if arry is provided externally as a string or jQuery object 1440 // options (default value) was already added 1441 $filter.append(arry).val(currentValue); 1442 } 1443 $filter.val(currentValue); 1444 } 1445 }, 1446 buildDefault: function(table, updating) { 1447 var columnIndex, $header, noSelect, 1448 c = table.config, 1449 wo = c.widgetOptions, 1450 columns = c.columns; 1451 // build default select dropdown 1452 for (columnIndex = 0; columnIndex < columns; columnIndex++) { 1453 $header = c.$headers.filter('[data-column="' + columnIndex + '"]:last'); 1454 noSelect = !($header.hasClass('filter-false') || $header.hasClass('parser-false')); 1455 // look for the filter-select class; build/update it if found 1456 if (($header.hasClass('filter-select') || ts.getColumnData( table, wo.filter_functions, columnIndex ) === true) && noSelect) { 1457 ts.filter.buildSelect(table, columnIndex, '', updating, $header.hasClass(wo.filter_onlyAvail)); 1458 } 1459 } 1460 } 1461}; 1462 1463ts.getFilters = function(table, getRaw, setFilters, skipFirst) { 1464 var i, $filters, $column, 1465 filters = false, 1466 c = table ? $(table)[0].config : '', 1467 wo = c ? c.widgetOptions : ''; 1468 if (getRaw !== true && wo && !wo.filter_columnFilters) { 1469 return $(table).data('lastSearch'); 1470 } 1471 if (c) { 1472 if (c.$filters) { 1473 $filters = c.$filters.find('.' + ts.css.filter); 1474 } 1475 if (wo.filter_$externalFilters) { 1476 $filters = $filters && $filters.length ? $filters.add(wo.filter_$externalFilters) : w
1476o.filter_$externalFilters; 1477 } 1478 if ($filters && $filters.length) { 1479 filters = setFilters || []; 1480 for (i = 0; i < c.columns + 1; i++) { 1481 $column = $filters.filter('[data-column="' + (i === c.columns ? 'all' : i) + '"]'); 1482 if ($column.length) { 1483 // move the latest search to the first slot in the array 1484 $column = $column.sort(function(a, b){ 1485 return $(b).attr('data-lastSearchTime') - $(a).attr('data-lastSearchTime'); 1486 }); 1487 if ($.isArray(setFilters)) { 1488 // skip first (latest input) to maintain cursor position while typing 1489 (skipFirst ? $column.slice(1) : $column).val( setFilters[i] ).trigger('change.tsfilter'); 1490 } else { 1491 filters[i] = $column.val() || ''; 1492 // don't change the first... it will move the cursor 1493 $column.slice(1).val( filters[i] ); 1494 } 1495 // save any match input dynamically 1496 if (i === c.columns && $column.length) { 1497 wo.filter_$anyMatch = $column; 1498 } 1499 } 1500 } 1501 } 1502 } 1503 if (filters.length === 0) { 1504 filters = false; 1505 } 1506 return filters; 1507}; 1508 1509ts.setFilters = function(table, filter, apply, skipFirst) { 1510 var c = table ? $(table)[0].config : '', 1511 valid = ts.getFilters(table, true, filter, skipFirst); 1512 if (c && apply) { 1513 // ensure new set filters are applied, even if the search is the same 1514 c.lastCombinedFilter = null; 1515 c.lastSearch = []; 1516 ts.filter.searching(c.$table[0], filter, skipFirst); 1517 c.$table.trigger('filterFomatterUpdate'); 1518 } 1519 return !!valid; 1520}; 1521 1522// Widget: Sticky headers 1523// based on this awesome article: 1524// http://css-tricks.com/13465-persistent-headers/ 1525// and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech 1526// ************************** 1527ts.addWidget({ 1528 id: "stickyHeaders", 1529 priority: 60, // sticky widget must be initialized after the filter widget! 1530 options: { 1531 stickyHeaders : '', // extra class name added to the sticky header row 1532 stickyHeaders_attachTo : null, // jQuery selector or object to attach sticky header to 1533 stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element 1534 stickyHeaders_filteredToTop: true, // scroll table top into view after filtering 1535 stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists 1536 stickyHeaders_addResizeEvent : true, // trigger "resize" event on headers 1537 stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header 1538 stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs 1539 }, 1540 format: function(table, c, wo) { 1541 // filter widget doesn't initialize on an empty table. Fixes #449 1542 if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) { 1543 return; 1544 } 1545 var $table = c.$table, 1546 $attach = $(wo.stickyHeaders_attachTo || 'window'), 1547 $thead = $table.children('thead:first'), 1548 $win = $attach.length ? $attach : $(window), 1549 $header = $thead.children('tr').not('.sticky-false').children(), 1550 innerHeader = '.' + ts.css.headerIn, 1551 $tfoot = $table.find('tfoot'), 1552 $stickyOffset = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : '', 1553 stickyOffset = $attach.length ? 0 : $stickyOffset.length ? 1554 $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0, 1555 $stickyTable = wo.$sticky = $table.clone() 1556 .addClass('containsStickyHeaders') 1557 .css({ 1558 position : $attach.length ? 'absolute' : 'fixed', 1559 margin : 0, 1560 top : stickyOffset, 1561 left : 0, 1562 visibility : 'hidden', 1563 zIndex : wo.stickyHeaders_zIndex ? wo.stickyHeaders_zIndex : 2 1564 }), 1565 $stickyThead = $stickyTable.children('thead:first').addClass(ts.css.sticky + ' ' + wo.stickyHeaders), 1566 $stickyCells, 1567 laststate = '', 1568 spacing = 0, 1569 nonwkie = $table.css('border-collapse') !== 'collapse' && !/(webkit|msie)/i.test(navigator.userAgent), 1570 resizeHeader = function() { 1571 stickyOffset = $stickyOffset.length ? $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0; 1572 spacing = 0; 1573 // webkit automatically compensates for border spacing 1574 if (nonwkie) { 1575 // Firefox & Opera use the border-spacing 1576 // update border-spacing here because of demos that switch themes 1577 spacing = parseInt($header.eq(0).css('border-left-width'), 10) * 2; 1578 } 1579 $stickyTable.css({ 1580 left : $attach.length ? (parseInt($attach.css('padding-left'), 10) || 0) + parseInt(c.$table.css('padding-left'), 10) + 1581 parseInt(c.$table.css('margin-left'), 10) + parseInt($table.css('border-left-width'), 10) : 1582 $thead.offset().left - $win.scrollLeft() - spacing,
1583 width: $table.width() 1584 }); 1585 $stickyCells.filter(':visible').each(function(i) { 1586 var $cell = $header.filter(':visible').eq(i), 1587 // stuff, to make columns line up in Firefox 1588 offset = nonwkie && $(this).attr('data-column') === ( '' + parseInt(c.columns/2, 10) ) ? 1 : 0; 1589 $(this) 1590 .css({ width: $cell.width() - spacing }) 1591 .find(innerHeader).width( $cell.find(innerHeader).width() - offset ); 1592 }); 1593 }; 1594 // fix clone ID, if it exists - fixes #271 1595 if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; } 1596 // clear out cloned table, except for sticky header 1597 // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing 1598 $stickyTable.find('thead:gt(0), tr.sticky-false').hide(); 1599 $stickyTable.find('tbody, tfoot').remove(); 1600 if (!wo.stickyHeaders_includeCaption) { 1601 $stickyTable.find('caption').remove(); 1602 } else { 1603 $stickyTable.find('caption').css( 'margin-left', '-1px' ); 1604 } 1605 // issue #172 - find td/th in sticky header 1606 $stickyCells = $stickyThead.children().children(); 1607 $stickyTable.css({ height:0, width:0, padding:0, margin:0, border:0 }); 1608 // remove resizable block 1609 $stickyCells.find('.' + ts.css.resizer).remove(); 1610 // update sticky header class names to match real header after sorting 1611 $table 1612 .addClass('hasStickyHeaders') 1613 .bind('pagerComplete.tsSticky', function() { 1614 resizeHeader(); 1615 }); 1616 1617 ts.bindEvents(table, $stickyThead.children().children('.tablesorter-header')); 1618 1619 // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. 1620 $table.after( $stickyTable ); 1621 $win.bind('scroll.tsSticky resize.tsSticky', function(event) { 1622 if (!$table.is(':visible')) { return; } // fixes #278 1623 var prefix = 'tablesorter-sticky-', 1624 offset = $table.offset(), 1625 captionHeight = (wo.stickyHeaders_includeCaption ? 0 : $table.find('caption').outerHeight(true)), 1626 scrollTop = ($attach.length ? $attach.offset().top : $win.scrollTop()) + stickyOffset - captionHeight, 1627 tableHeight = $table.height() - ($stickyTable.height() + ($tfoot.height() || 0)), 1628 isVisible = (scrollTop > offset.top) && (scrollTop < offset.top + tableHeight) ? 'visible' : 'hidden', 1629 cssSettings = { visibility : isVisible }; 1630 if ($attach.length) { 1631 cssSettings.top = $attach.scrollTop(); 1632 } else { 1633 // adjust when scrolling horizontally - fixes issue #143 1634 cssSettings.left = $thead.offset().left - $win.scrollLeft() - spacing; 1635 } 1636 $stickyTable 1637 .removeClass(prefix + 'visible ' + prefix + 'hidden') 1638 .addClass(prefix + isVisible) 1639 .css(cssSettings); 1640 if (isVisible !== laststate || event.type === 'resize') { 1641 // make sure the column widths match 1642 resizeHeader(); 1643 laststate = isVisible; 1644 } 1645 }); 1646 if (wo.stickyHeaders_addResizeEvent) { 1647 ts.addHeaderResizeEvent(table); 1648 } 1649 1650 // look for filter widget 1651 if ($table.hasClass('hasFilters')) { 1652 // scroll table into view after filtering, if sticky header is active - #482 1653 $table.bind('filterEnd', function() { 1654 // $(':focus') needs jQuery 1.6+ 1655 var $td = $(document.activeElement).closest('td'), 1656 column = $td.parent().children().index($td); 1657 // only scroll if sticky header is active 1658 if ($stickyTable.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) { 1659 // scroll to original table (not sticky clone) 1660 window.scrollTo(0, $table.position().top); 1661 // give same input/select focus; check if c.$filters exists; fixes #594 1662 if (column >= 0 && c.$filters) { 1663 c.$filters.eq(column).find('a, select, input').filter(':visible').focus(); 1664 } 1665 } 1666 }); 1667 ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) ); 1668 // support hideFilters 1669 if (wo.filter_hideFilters) { 1670 ts.filter.hideFilters($stickyTable, c); 1671 } 1672 } 1673 1674 $table.trigger('stickyHeadersInit'); 1675 1676 }, 1677 remove: function(table, c, wo) { 1678 c.$table 1679 .removeClass('hasStickyHeaders') 1680 .unbind('pagerComplete.tsSticky') 1681 .find('.' + ts.css.sticky).remove(); 1682 if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table 1683 // don't unbind if any table on the page still has stickyheaders applied 1684 if (!$('.hasStickyHeaders').length) { 1685 $(window).unbind('scroll.tsSticky resize.tsSticky'); 1686 } 1687 ts.addHeaderResizeEvent(table, false); 1688 } 1689}); 1690 1691// Add Column resizing widget 1692// this widget saves the column widths if 1693// $.tablesorter.storage function is included 1694// ************************** 1695ts.addWidget({ 1696 id: "resizable", 1697 priority: 40, 1698 options: { 1699 resizable : true, 1700 resizable_addLastColumn : false, 1701 resizable_widths : [], 1702 resizable_throttle : false // set to true (5ms) or any number 0-10 range 1703 }, 1704 format: function(table, c, wo) { 1705 if (c.$table.hasClass('hasResizable')) { return; } 1706 c.$table.addClass('hasResizable'); 1707 ts.resizableReset(table, true); // set default widths 1708 var $rows, $columns, $column, column, timer, 1709 storedSizes = {}, 1710 $table = c.$table, 1711 mouseXPosition = 0, 1712 $target = null, 1713 $next = null,
1714 fullWidth = Math.abs($table.parent().width() - $table.width()) < 20, 1715 mouseMove = function(event){ 1716 if (mouseXPosition === 0 || !$target) { return; } 1717 // resize columns 1718 var leftEdge = event.pageX - mouseXPosition, 1719 targetWidth = $target.width(); 1720 $target.width( targetWidth + leftEdge ); 1721 if ($target.width() !== targetWidth && fullWidth) { 1722 $next.width( $next.width() - leftEdge ); 1723 } 1724 mouseXPosition = event.pageX; 1725 }, 1726 stopResize = function() { 1727 if (ts.storage && $target && $next) { 1728 storedSizes = {}; 1729 storedSizes[$target.index()] = $target.width(); 1730 storedSizes[$next.index()] = $next.width(); 1731 $target.width( storedSizes[$target.index()] ); 1732 $next.width( storedSizes[$next.index()] ); 1733 if (wo.resizable !== false) { 1734 // save all column widths 1735 ts.storage(table, 'tablesorter-resizable', c.$headers.map(function(){ return $(this).width(); }).get() ); 1736 } 1737 } 1738 mouseXPosition = 0; 1739 $target = $next = null; 1740 $(window).trigger('resize'); // will update stickyHeaders, just in case 1741 }; 1742 storedSizes = (ts.storage && wo.resizable !== false) ? ts.storage(table, 'tablesorter-resizable') : {}; 1743 // process only if table ID or url match 1744 if (storedSizes) { 1745 for (column in storedSizes) { 1746 if (!isNaN(column) && column < c.$headers.length) { 1747 c.$headers.eq(column).width(storedSizes[column]); // set saved resizable widths 1748 } 1749 } 1750 } 1751 $rows = $table.children('thead:first').children('tr'); 1752 // add resizable-false class name to headers (across rows as needed) 1753 $rows.children().each(function() { 1754 var canResize, 1755 $column = $(this); 1756 column = $column.attr('data-column'); 1757 canResize = ts.getData( $column, ts.getColumnData( table, c.headers, column ), 'resizable') === "false"; 1758 $rows.children().filter('[data-column="' + column + '"]')[canResize ? 'addClass' : 'removeClass']('resizable-false'); 1759 }); 1760 // add wrapper inside each cell to allow for positioning of the resizable target block 1761 $rows.each(function() { 1762 $column = $(this).children().not('.resizable-false'); 1763 if (!$(this).find('.' + ts.css.wrapper).length) { 1764 // Firefox needs this inner div to position the resizer correctly 1765 $column.wrapInner('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>'); 1766 } 1767 // don't include the last column of the row 1768 if (!wo.resizable_addLastColumn) { $column = $column.slice(0,-1); } 1769 $columns = $columns ? $columns.add($column) : $column; 1770 }); 1771 $columns 1772 .each(function() { 1773 var $column = $(this), 1774 padding = parseInt($column.css('padding-right'), 10) + 10; // 10 is 1/2 of the 20px wide resizer 1775 $column 1776 .find('.' + ts.css.wrapper) 1777 .append('<div class="' + ts.css.resizer + '" style="cursor:w-resize;position:absolute;z-index:1;right:-' + 1778 padding + 'px;top:0;height:100%;width:20px;"></div>'); 1779 }) 1780 .find('.' + ts.css.resizer) 1781 .bind('mousedown', function(event) { 1782 // save header cell and mouse position 1783 $target = $(event.target).closest('th'); 1784 var $header = c.$headers.filter('[data-column="' + $target.attr('data-column') + '"]'); 1785 if ($header.length > 1) { $target = $target.add($header); } 1786 // if table is not as wide as it's parent, then resize the table 1787 $next = event.shiftKey ? $target.parent().find('th').not('.resizable-false').filter(':last') : $target.nextAll(':not(.resizable-false)').eq(0); 1788 mouseXPosition = event.pageX; 1789 }); 1790 $(document) 1791 .bind('mousemove.tsresize', function(event) { 1792 // ignore mousemove if no mousedown 1793 if (mouseXPosition === 0 || !$target) { return; } 1794 if (wo.resizable_throttle) { 1795 clearTimeout(timer); 1796 timer = setTimeout(function(){ 1797 mouseMove(event); 1798 }, isNaN(wo.resizable_throttle) ? 5 : wo.resizable_throttle ); 1799 } else { 1800 mouseMove(event); 1801 } 1802 }) 1803 .bind('mouseup.tsresize', function() { 1804 stopResize(); 1805 }); 1806 1807 // right click to reset columns to default widths 1808 $table.find('thead:first').bind('contextmenu.tsresize', function() { 1809 ts.resizableReset(table); 1810 // $.isEmptyObject() needs jQuery 1.4+; allow right click if already reset 1811 var allowClick = $.isEmptyObject ? $.isEmptyObject(storedSizes) : true; 1812 storedSizes = {}; 1813 return allowClick; 1814 }); 1815 }, 1816 remove: function(table, c) { 1817 c.$table 1818 .removeClass('hasResizable') 1819 .children('thead') 1820 .unbind('mouseup.tsresize mouseleave.tsresize contextmenu.tsresize') 1821 .children('tr').children() 1822 .unbind('mousemove.tsresize mouseup.tsresize') 1823 // don't remove "tablesorter-wrapper" as uitheme uses it too 1824 .find('.' + ts.css.resizer).remove(); 1825 ts.resizableReset(table); 1826 } 1827}); 1828ts.resizableReset = function(table, nosave) { 1829 $(table).each(function(){ 1830 var $t, 1831 c = this.config, 1832 wo = c && c.widgetOptions; 1833 if (table && c) { 1834 c.$headers.each(function(i){ 1835 $t = $(this); 1836 if (wo.resizable_widths[i]) { 1837 $t.css('width', wo.resizable_widths[i]); 1838 } else if (!$t.hasClass('resizable-false')) { 1839 // don't clear the width of any column that is not resizable 1840 $t.css('width',''); 1841 } 1842 }); 1843 if (ts.storage && !nosave) { ts.storage(this, 'tablesorter-resizable', {}); } 1844 } 1845 }); 1846}; 1847 1848// Save table sort widget 1849// this widget saves the last sort only if the 1850// saveSort widget option is true AND the 1851// $.tablesorter.storage function is included 1852// ************************** 1853ts.addWidget({ 1854 id: 'saveSort', 1855 priority: 20, 1856 options: { 1857 saveSort : true 1858 }, 1859 init: function(table, thisWidget, c, wo) { 1860 // run widget format before all other widgets are applied to the table 1861 thisWidget.format(table, c, wo, true); 1862 }, 1863 format: function(table, c, wo, init) { 1864 var stored, time, 1865 $table = c.$table, 1866 saveSort = wo.saveSort !== false, // make saveSort active/inactive; default to true
1867 sortList = { "sortList" : c.sortList }; 1868 if (c.debug) { 1869 time = new Date(); 1870 } 1871 if ($table.hasClass('hasSaveSort')) { 1872 if (saveSort && table.hasInitialized && ts.storage) { 1873 ts.storage( table, 'tablesorter-savesort', sortList ); 1874 if (c.debug) { 1875 ts.benchmark('saveSort widget: Saving last sort: ' + c.sortList, time); 1876 } 1877 } 1878 } else { 1879 // set table sort on initial run of the widget 1880 $table.addClass('hasSaveSort'); 1881 sortList = ''; 1882 // get data 1883 if (ts.storage) { 1884 stored = ts.storage( table, 'tablesorter-savesort' ); 1885 sortList = (stored && stored.hasOwnProperty('sortList') && $.isArray(stored.sortList)) ? stored.sortList : ''; 1886 if (c.debug) { 1887 ts.benchmark('saveSort: Last sort loaded: "' + sortList + '"', time); 1888 } 1889 $table.bind('saveSortReset', function(event) { 1890 event.stopPropagation(); 1891 ts.storage( table, 'tablesorter-savesort', '' ); 1892 }); 1893 } 1894 // init is true when widget init is run, this will run this widget before all other widgets have initialized 1895 // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice. 1896 if (init && sortList && sortList.length > 0) { 1897 c.sortList = sortList; 1898 } else if (table.hasInitialized && sortList && sortList.length > 0) { 1899 // update sort change 1900 $table.trigger('sorton', [sortList]); 1901 } 1902 } 1903 }, 1904 remove: function(table) { 1905 // clear storage 1906 if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); } 1907 } 1908}); 1909 1910})(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.