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