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