jquery-tablesorter-rails4 0.0.1

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