flashgrid 3.1.1 → 3.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cd3ec69037f078c806099265f08b726c4766efeb
4
- data.tar.gz: d9e3590033a4b4f4e909c575ac9eb05a9cfcb96a
3
+ metadata.gz: bb3a156f4c57b1754c9a5eea31e98e51e2e33595
4
+ data.tar.gz: 79cf307756645eab4d161e2dd8f649c57425e192
5
5
  SHA512:
6
- metadata.gz: 4fb17d8e10229a9516035e230c321a9b59a5a4c45dc2ff332f1c7fe848ed500a921ae5ecd5a4b71464ef5ea9cc30707a7f46d1d3cba6f260ef4af948b70fbbb0
7
- data.tar.gz: 90e019644e24b3f892068fed043f9f78b7e6e30448c4a21b716b8c1243be7f7caf69c370c5a28006b1d9c578def9ebc34ff8cfd99a0819b15ad583f4d7f6fa97
6
+ metadata.gz: 7e3cace4a3b745814f27f56b9784426e27ceacd20785f2e893f628ca945ca4a3f23726e56974f3a2baccfe27fd912f07cdb24c09e4eac70ace6c99b405e8aa4a
7
+ data.tar.gz: 4b1fb8d296c3348aa2a516c366f110b2c01d7f3c4d3728c46a722d313a9fa92991f920ddcabf081966caefc9283b84a4500e1deb8e2780744fc182769b57db72
data/README.md CHANGED
@@ -83,6 +83,7 @@ Add the JS files you want to include:
83
83
  //= require scrollspy.js
84
84
  //= require switch.js
85
85
  //= require tab.js
86
+ //= require tablespy.js
86
87
  //= require time_picker.js
87
88
  //= require tooltip.js
88
89
  //= require transition.js
@@ -1,3 +1,3 @@
1
1
  module Flashgrid
2
- VERSION = "3.1.1"
2
+ VERSION = "3.2.0"
3
3
  end
@@ -29,17 +29,21 @@
29
29
  Collapse.prototype.show = function () {
30
30
  if (this.transitioning || this.$element.hasClass('in')) return
31
31
 
32
+ var activesData
33
+ var actives = this.$parent && this.$parent.find('> .panel').children('.in, .collapsing')
34
+
35
+ if (actives && actives.length) {
36
+ activesData = actives.data('bs.collapse')
37
+ if (activesData && activesData.transitioning) return
38
+ }
39
+
32
40
  var startEvent = $.Event('show.bs.collapse')
33
41
  this.$element.trigger(startEvent)
34
42
  if (startEvent.isDefaultPrevented()) return
35
43
 
36
- var actives = this.$parent && this.$parent.find('> .panel').children('.in, .collapsing')
37
-
38
44
  if (actives && actives.length) {
39
- var hasData = actives.data('bs.collapse')
40
- if (hasData && hasData.transitioning) return
41
45
  Plugin.call(actives, 'hide')
42
- hasData || actives.data('bs.collapse', null)
46
+ activesData || actives.data('bs.collapse', null)
43
47
  }
44
48
 
45
49
  var dimension = this.dimension()
@@ -0,0 +1,1883 @@
1
+ !(function($) {
2
+ "use strict";
3
+ $.extend({
4
+ /*jshint supernew:true */
5
+ tablespy: new function() {
6
+
7
+ var ts = this;
8
+
9
+ ts.version = "2.17.8";
10
+
11
+ ts.parsers = [];
12
+ ts.widgets = [];
13
+ ts.defaults = {
14
+
15
+ // *** appearance
16
+ theme : 'default', // adds tablespy-{theme} to the table for styling
17
+ widthFixed : false, // adds colgroup to fix widths of columns
18
+ showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered.
19
+
20
+ headerTemplate : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = <i/> (class from cssIcon)
21
+ onRenderTemplate : null, // function(index, template){ return template; }, (template is a string)
22
+ onRenderHeader : null, // function(index){}, (nothing to return)
23
+
24
+ // *** functionality
25
+ cancelSelection : true, // prevent text selection in the header
26
+ tabIndex : true, // add tabindex to header for keyboard accessibility
27
+ dateFormat : 'mmddyyyy', // other options: "ddmmyyy" or "yyyymmdd"
28
+ sortMultiSortKey : 'shiftKey', // key used to select additional columns
29
+ sortResetKey : 'ctrlKey', // key used to remove sorting on a column
30
+ usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89"
31
+ delayInit : false, // if false, the parsed table contents will not update until the first sort
32
+ serverSideSorting: false, // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used.
33
+
34
+ // *** sort options
35
+ headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
36
+ ignoreCase : true, // ignore case while sorting
37
+ sortForce : null, // column(s) first sorted; always applied
38
+ sortList : [], // Initial sort order; applied initially; updated when manually sorted
39
+ sortAppend : null, // column(s) sorted last; always applied
40
+ sortStable : false, // when sorting two rows with exactly the same content, the original sort order is maintained
41
+
42
+ sortInitialOrder : 'asc', // sort direction on first click
43
+ sortLocaleCompare: false, // replace equivalent character (accented characters)
44
+ sortReset : false, // third click on the header will reset column to default - unsorted
45
+ sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns
46
+
47
+ emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero
48
+ stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero
49
+ textExtraction : 'basic', // text extraction method/function - function(node, table, cellIndex){}
50
+ textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in textExtraction function)
51
+ textSorter : null, // choose overall or specific column sorter function(a, b, direction, table, columnIndex) [alt: ts.sortText]
52
+ numberSorter : null, // choose overall numeric sorter function(a, b, direction, maxColumnValue)
53
+
54
+ // *** widget options
55
+ widgets: [], // method to add widgets, e.g. widgets: ['zebra']
56
+ widgetOptions : {
57
+ zebra : [ 'even', 'odd' ] // zebra widget alternating row class names
58
+ },
59
+ initWidgets : true, // apply widgets on tablespy initialization
60
+
61
+ // *** callbacks
62
+ initialized : null, // function(table){},
63
+
64
+ // *** extra css class names
65
+ tableClass : '',
66
+ cssAsc : '',
67
+ cssDesc : '',
68
+ cssNone : '',
69
+ cssHeader : '',
70
+ cssHeaderRow : '',
71
+ cssProcessing : '', // processing icon applied to header during sort/filter
72
+
73
+ cssChildRow : 'tablespy-childRow', // class name indiciating that a row is to be attached to the its parent
74
+ cssIcon : 'tablespy-icon', // if this class exists, a <i> will be added to the header automatically
75
+ cssInfoBlock : 'tablespy-infoOnly', // don't sort tbody with this class name (only one class name allowed here!)
76
+
77
+ // *** selectors
78
+ selectorHeaders : '> thead th, > thead td',
79
+ selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort
80
+ selectorRemove : '.remove-me',
81
+
82
+ // *** advanced
83
+ debug : false,
84
+
85
+ // *** Internal variables
86
+ headerList: [],
87
+ empties: {},
88
+ strings: {},
89
+ parsers: []
90
+
91
+ // deprecated; but retained for backwards compatibility
92
+ // widgetZebra: { css: ["even", "odd"] }
93
+
94
+ };
95
+
96
+ // internal css classes - these will ALWAYS be added to
97
+ // the table and MUST only contain one class name - fixes #381
98
+ ts.css = {
99
+ table : 'tablespy',
100
+ cssHasChild: 'tablespy-hasChildRow',
101
+ childRow : 'tablespy-childRow',
102
+ header : 'tablespy-header',
103
+ headerRow : 'tablespy-headerRow',
104
+ headerIn : 'tablespy-header-inner',
105
+ icon : 'tablespy-icon',
106
+ info : 'tablespy-infoOnly',
107
+ processing : 'tablespy-processing',
108
+ sortAsc : 'tablespy-headerAsc sort-asc',
109
+ sortDesc : 'tablespy-headerDesc sort-desc',
110
+ sortNone : 'tablespy-headerUnSorted sort-none'
111
+ };
112
+
113
+ // labels applied to sortable headers for accessibility (aria) support
114
+ ts.language = {
115
+ sortAsc : 'Ascending sort applied, ',
116
+ sortDesc : 'Descending sort applied, ',
117
+ sortNone : 'No sort applied, ',
118
+ nextAsc : 'activate to apply an ascending sort',
119
+ nextDesc : 'activate to apply a descending sort',
120
+ nextNone : 'activate to remove the sort'
121
+ };
122
+
123
+ /* debuging utils */
124
+ function log() {
125
+ var a = arguments[0],
126
+ s = arguments.length > 1 ? Array.prototype.slice.call(arguments) : a;
127
+ if (typeof console !== "undefined" && typeof console.log !== "undefined") {
128
+ console[ /error/i.test(a) ? 'error' : /warn/i.test(a) ? 'warn' : 'log' ](s);
129
+ } else {
130
+ alert(s);
131
+ }
132
+ }
133
+
134
+ function benchmark(s, d) {
135
+ log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)");
136
+ }
137
+
138
+ ts.log = log;
139
+ ts.benchmark = benchmark;
140
+
141
+ // $.isEmptyObject from jQuery v1.4
142
+ function isEmptyObject(obj) {
143
+ /*jshint forin: false */
144
+ for (var name in obj) {
145
+ return false;
146
+ }
147
+ return true;
148
+ }
149
+
150
+ function getElementText(table, node, cellIndex) {
151
+ if (!node) { return ""; }
152
+ var te, c = table.config,
153
+ t = c.textExtraction || '',
154
+ text = "";
155
+ if (t === "basic") {
156
+ // check data-attribute first
157
+ text = $(node).attr(c.textAttribute) || node.textContent || node.innerText || $(node).text() || "";
158
+ } else {
159
+ if (typeof(t) === "function") {
160
+ text = t(node, table, cellIndex);
161
+ } else if (typeof (te = ts.getColumnData( table, t, cellIndex )) === 'function') {
162
+ text = te(node, table, cellIndex);
163
+ } else {
164
+ // previous "simple" method
165
+ text = node.textContent || node.innerText || $(node).text() || "";
166
+ }
167
+ }
168
+ return $.trim(text);
169
+ }
170
+
171
+ function detectParserForColumn(table, rows, rowIndex, cellIndex) {
172
+ var cur,
173
+ i = ts.parsers.length,
174
+ node = false,
175
+ nodeValue = '',
176
+ keepLooking = true;
177
+ while (nodeValue === '' && keepLooking) {
178
+ rowIndex++;
179
+ if (rows[rowIndex]) {
180
+ node = rows[rowIndex].cells[cellIndex];
181
+ nodeValue = getElementText(table, node, cellIndex);
182
+ if (table.config.debug) {
183
+ log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': "' + nodeValue + '"');
184
+ }
185
+ } else {
186
+ keepLooking = false;
187
+ }
188
+ }
189
+ while (--i >= 0) {
190
+ cur = ts.parsers[i];
191
+ // ignore the default text parser because it will always be true
192
+ if (cur && cur.id !== 'text' && cur.is && cur.is(nodeValue, table, node)) {
193
+ return cur;
194
+ }
195
+ }
196
+ // nothing found, return the generic parser (text)
197
+ return ts.getParserById('text');
198
+ }
199
+
200
+ function buildParserCache(table) {
201
+ var c = table.config,
202
+ // update table bodies in case we start with an empty table
203
+ tb = c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')'),
204
+ rows, list, l, i, h, ch, np, p, e, time,
205
+ j = 0,
206
+ parsersDebug = "",
207
+ len = tb.length;
208
+ if ( len === 0) {
209
+ return c.debug ? log('Warning: *Empty table!* Not building a parser cache') : '';
210
+ } else if (c.debug) {
211
+ time = new Date();
212
+ log('Detecting parsers for each column');
213
+ }
214
+ list = {
215
+ extractors: [],
216
+ parsers: []
217
+ };
218
+ while (j < len) {
219
+ rows = tb[j].rows;
220
+ if (rows[j]) {
221
+ l = c.columns; // rows[j].cells.length;
222
+ for (i = 0; i < l; i++) {
223
+ h = c.$headers.filter('[data-column="' + i + '"]:last');
224
+ // get column indexed table cell
225
+ ch = ts.getColumnData( table, c.headers, i );
226
+ // get column parser/extractor
227
+ e = ts.getParserById( ts.getData(h, ch, 'extractor') );
228
+ p = ts.getParserById( ts.getData(h, ch, 'sorter') );
229
+ np = ts.getData(h, ch, 'parser') === 'false';
230
+ // empty cells behaviour - keeping emptyToBottom for backwards compatibility
231
+ c.empties[i] = ( ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase();
232
+ // text strings behaviour in numerical sorts
233
+ c.strings[i] = ( ts.getData(h, ch, 'string') || c.stringTo || 'max' ).toLowerCase();
234
+ if (np) {
235
+ p = ts.getParserById('no-parser');
236
+ }
237
+ if (!e) {
238
+ // For now, maybe detect someday
239
+ e = false;
240
+ }
241
+ if (!p) {
242
+ p = detectParserForColumn(table, rows, -1, i);
243
+ }
244
+ if (c.debug) {
245
+ parsersDebug += "column:" + i + "; extractor:" + e.id + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n";
246
+ }
247
+ list.parsers[i] = p;
248
+ list.extractors[i] = e;
249
+ }
250
+ }
251
+ j += (list.parsers.length) ? len : 1;
252
+ }
253
+ if (c.debug) {
254
+ log(parsersDebug ? parsersDebug : "No parsers detected");
255
+ benchmark("Completed detecting parsers", time);
256
+ }
257
+ c.parsers = list.parsers;
258
+ c.extractors = list.extractors;
259
+ }
260
+
261
+ /* utils */
262
+ function buildCache(table) {
263
+ var cc, t, tx, v, i, j, k, $row, rows, cols, cacheTime,
264
+ totalRows, rowData, colMax,
265
+ c = table.config,
266
+ $tb = c.$table.children('tbody'),
267
+ extractors = c.extractors,
268
+ parsers = c.parsers;
269
+ c.cache = {};
270
+ c.totalRows = 0;
271
+ // if no parsers found, return - it's an empty table.
272
+ if (!parsers) {
273
+ return c.debug ? log('Warning: *Empty table!* Not building a cache') : '';
274
+ }
275
+ if (c.debug) {
276
+ cacheTime = new Date();
277
+ }
278
+ // processing icon
279
+ if (c.showProcessing) {
280
+ ts.isProcessing(table, true);
281
+ }
282
+ for (k = 0; k < $tb.length; k++) {
283
+ colMax = []; // column max value per tbody
284
+ cc = c.cache[k] = {
285
+ normalized: [] // array of normalized row data; last entry contains "rowData" above
286
+ // colMax: # // added at the end
287
+ };
288
+
289
+ // ignore tbodies with class name from c.cssInfoBlock
290
+ if (!$tb.eq(k).hasClass(c.cssInfoBlock)) {
291
+ totalRows = ($tb[k] && $tb[k].rows.length) || 0;
292
+ for (i = 0; i < totalRows; ++i) {
293
+ rowData = {
294
+ // order: original row order #
295
+ // $row : jQuery Object[]
296
+ child: [] // child row text (filter widget)
297
+ };
298
+ /** Add the table data to main data array */
299
+ $row = $($tb[k].rows[i]);
300
+ rows = [ new Array(c.columns) ];
301
+ cols = [];
302
+ // if this is a child row, add it to the last row's children and continue to the next row
303
+ // ignore child row class, if it is the first row
304
+ if ($row.hasClass(c.cssChildRow) && i !== 0) {
305
+ t = cc.normalized.length - 1;
306
+ cc.normalized[t][c.columns].$row = cc.normalized[t][c.columns].$row.add($row);
307
+ // add "hasChild" class name to parent row
308
+ if (!$row.prev().hasClass(c.cssChildRow)) {
309
+ $row.prev().addClass(ts.css.cssHasChild);
310
+ }
311
+ // save child row content (un-parsed!)
312
+ rowData.child[t] = $.trim( $row[0].textContent || $row[0].innerText || $row.text() || "" );
313
+ // go to the next for loop
314
+ continue;
315
+ }
316
+ rowData.$row = $row;
317
+ rowData.order = i; // add original row position to rowCache
318
+ for (j = 0; j < c.columns; ++j) {
319
+ if (typeof parsers[j] === 'undefined') {
320
+ if (c.debug) {
321
+ log('No parser found for cell:', $row[0].cells[j], 'does it have a header?');
322
+ }
323
+ continue;
324
+ }
325
+ t = getElementText(table, $row[0].cells[j], j);
326
+ // do extract before parsing if there is one
327
+ if (typeof extractors[j].id === 'undefined') {
328
+ tx = t;
329
+ } else {
330
+ tx = extractors[j].format(t, table, $row[0].cells[j], j);
331
+ }
332
+ // allow parsing if the string is empty, previously parsing would change it to zero,
333
+ // in case the parser needs to extract data from the table cell attributes
334
+ v = parsers[j].id === 'no-parser' ? '' : parsers[j].format(tx, table, $row[0].cells[j], j);
335
+ cols.push( c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v );
336
+ if ((parsers[j].type || '').toLowerCase() === "numeric") {
337
+ // determine column max value (ignore sign)
338
+ colMax[j] = Math.max(Math.abs(v) || 0, colMax[j] || 0);
339
+ }
340
+ }
341
+ // ensure rowData is always in the same location (after the last column)
342
+ cols[c.columns] = rowData;
343
+ cc.normalized.push(cols);
344
+ }
345
+ cc.colMax = colMax;
346
+ // total up rows, not including child rows
347
+ c.totalRows += cc.normalized.length;
348
+ }
349
+ }
350
+ if (c.showProcessing) {
351
+ ts.isProcessing(table); // remove processing icon
352
+ }
353
+ if (c.debug) {
354
+ benchmark("Building cache for " + totalRows + " rows", cacheTime);
355
+ }
356
+ }
357
+
358
+ // init flag (true) used by pager plugin to prevent widget application
359
+ function appendToTable(table, init) {
360
+ var c = table.config,
361
+ wo = c.widgetOptions,
362
+ b = table.tBodies,
363
+ rows = [],
364
+ cc = c.cache,
365
+ n, totalRows, $bk, $tb,
366
+ i, k, appendTime;
367
+ // empty table - fixes #206/#346
368
+ if (isEmptyObject(cc)) {
369
+ // run pager appender in case the table was just emptied
370
+ return c.appender ? c.appender(table, rows) :
371
+ table.isUpdating ? c.$table.trigger("updateComplete", table) : ''; // Fixes #532
372
+ }
373
+ if (c.debug) {
374
+ appendTime = new Date();
375
+ }
376
+ for (k = 0; k < b.length; k++) {
377
+ $bk = $(b[k]);
378
+ if ($bk.length && !$bk.hasClass(c.cssInfoBlock)) {
379
+ // get tbody
380
+ $tb = ts.processTbody(table, $bk, true);
381
+ n = cc[k].normalized;
382
+ totalRows = n.length;
383
+ for (i = 0; i < totalRows; i++) {
384
+ rows.push(n[i][c.columns].$row);
385
+ // removeRows used by the pager plugin; don't render if using ajax - fixes #411
386
+ if (!c.appender || (c.pager && (!c.pager.removeRows || !wo.pager_removeRows) && !c.pager.ajax)) {
387
+ $tb.append(n[i][c.columns].$row);
388
+ }
389
+ }
390
+ // restore tbody
391
+ ts.processTbody(table, $tb, false);
392
+ }
393
+ }
394
+ if (c.appender) {
395
+ c.appender(table, rows);
396
+ }
397
+ if (c.debug) {
398
+ benchmark("Rebuilt table", appendTime);
399
+ }
400
+ // apply table widgets; but not before ajax completes
401
+ if (!init && !c.appender) { ts.applyWidget(table); }
402
+ if (table.isUpdating) {
403
+ c.$table.trigger("updateComplete", table);
404
+ }
405
+ }
406
+
407
+ function formatSortingOrder(v) {
408
+ // look for "d" in "desc" order; return true
409
+ return (/^d/i.test(v) || v === 1);
410
+ }
411
+
412
+ function buildHeaders(table) {
413
+ var ch, $t,
414
+ h, i, t, lock, time,
415
+ c = table.config;
416
+ c.headerList = [];
417
+ c.headerContent = [];
418
+ if (c.debug) {
419
+ time = new Date();
420
+ }
421
+ // children tr in tfoot - see issue #196 & #547
422
+ c.columns = ts.computeColumnIndex( c.$table.children('thead, tfoot').children('tr') );
423
+ // add icon if cssIcon option exists
424
+ i = c.cssIcon ? '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' : '';
425
+ // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683
426
+ c.$headers = $(table).find(c.selectorHeaders).each(function(index) {
427
+ $t = $(this);
428
+ // make sure to get header cell & not column indexed cell
429
+ ch = ts.getColumnData( table, c.headers, index, true );
430
+ // save original header content
431
+ c.headerContent[index] = $(this).html();
432
+ // if headerTemplate is empty, don't reformat the header cell
433
+ if ( c.headerTemplate !== '' ) {
434
+ // set up header template
435
+ t = c.headerTemplate.replace(/\{content\}/g, $(this).html()).replace(/\{icon\}/g, i);
436
+ if (c.onRenderTemplate) {
437
+ h = c.onRenderTemplate.apply($t, [index, t]);
438
+ if (h && typeof h === 'string') { t = h; } // only change t if something is returned
439
+ }
440
+ $(this).html('<div class="' + ts.css.headerIn + '">' + t + '</div>'); // faster than wrapInner
441
+ }
442
+ if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); }
443
+ this.column = parseInt( $(this).attr('data-column'), 10);
444
+ this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2];
445
+ this.count = -1; // set to -1 because clicking on the header automatically adds one
446
+ this.lockedOrder = false;
447
+ lock = ts.getData($t, ch, 'lockedOrder') || false;
448
+ if (typeof lock !== 'undefined' && lock !== false) {
449
+ this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0];
450
+ }
451
+ $t.addClass(ts.css.header + ' ' + c.cssHeader);
452
+ // add cell to headerList
453
+ c.headerList[index] = this;
454
+ // add to parent in case there are multiple rows
455
+ $t.parent().addClass(ts.css.headerRow + ' ' + c.cssHeaderRow).attr('role', 'row');
456
+ // allow keyboard cursor to focus on element
457
+ if (c.tabIndex) { $t.attr("tabindex", 0); }
458
+ }).attr({
459
+ scope: 'col',
460
+ role : 'columnheader'
461
+ });
462
+ // enable/disable sorting
463
+ updateHeader(table);
464
+ if (c.debug) {
465
+ benchmark("Built headers:", time);
466
+ log(c.$headers);
467
+ }
468
+ }
469
+
470
+ function commonUpdate(table, resort, callback) {
471
+ var c = table.config;
472
+ // remove rows/elements before update
473
+ c.$table.find(c.selectorRemove).remove();
474
+ // rebuild parsers
475
+ buildParserCache(table);
476
+ // rebuild the cache map
477
+ buildCache(table);
478
+ checkResort(c.$table, resort, callback);
479
+ }
480
+
481
+ function updateHeader(table) {
482
+ var s, $th, col,
483
+ c = table.config;
484
+ c.$headers.each(function(index, th){
485
+ $th = $(th);
486
+ col = ts.getColumnData( table, c.headers, index, true );
487
+ // add "sorter-false" class if "parser-false" is set
488
+ s = ts.getData( th, col, 'sorter' ) === 'false' || ts.getData( th, col, 'parser' ) === 'false';
489
+ th.sortDisabled = s;
490
+ $th[ s ? 'addClass' : 'removeClass' ]('sorter-false').attr('aria-disabled', '' + s);
491
+ // aria-controls - requires table ID
492
+ if (table.id) {
493
+ if (s) {
494
+ $th.removeAttr('aria-controls');
495
+ } else {
496
+ $th.attr('aria-controls', table.id);
497
+ }
498
+ }
499
+ });
500
+ }
501
+
502
+ function setHeadersCss(table) {
503
+ var f, i, j,
504
+ c = table.config,
505
+ list = c.sortList,
506
+ len = list.length,
507
+ none = ts.css.sortNone + ' ' + c.cssNone,
508
+ css = [ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc],
509
+ aria = ['ascending', 'descending'],
510
+ // find the footer
511
+ $t = $(table).find('tfoot tr').children().add(c.$extraHeaders).removeClass(css.join(' '));
512
+ // remove all header information
513
+ c.$headers
514
+ .removeClass(css.join(' '))
515
+ .addClass(none).attr('aria-sort', 'none');
516
+ for (i = 0; i < len; i++) {
517
+ // direction = 2 means reset!
518
+ if (list[i][1] !== 2) {
519
+ // multicolumn sorting updating - choose the :last in case there are nested columns
520
+ f = c.$headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (len === 1 ? ':last' : '') );
521
+ if (f.length) {
522
+ for (j = 0; j < f.length; j++) {
523
+ if (!f[j].sortDisabled) {
524
+ f.eq(j).removeClass(none).addClass(css[list[i][1]]).attr('aria-sort', aria[list[i][1]]);
525
+ }
526
+ }
527
+ // add sorted class to footer & extra headers, if they exist
528
+ if ($t.length) {
529
+ $t.filter('[data-column="' + list[i][0] + '"]').removeClass(none).addClass(css[list[i][1]]);
530
+ }
531
+ }
532
+ }
533
+ }
534
+ // add verbose aria labels
535
+ c.$headers.not('.sorter-false').each(function(){
536
+ var $this = $(this),
537
+ nextSort = this.order[(this.count + 1) % (c.sortReset ? 3 : 2)],
538
+ txt = $this.text() + ': ' +
539
+ ts.language[ $this.hasClass(ts.css.sortAsc) ? 'sortAsc' : $this.hasClass(ts.css.sortDesc) ? 'sortDesc' : 'sortNone' ] +
540
+ ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ];
541
+ $this.attr('aria-label', txt );
542
+ });
543
+ }
544
+
545
+ // automatically add col group, and column sizes if set
546
+ function fixColumnWidth(table) {
547
+ var colgroup, overallWidth,
548
+ c = table.config;
549
+ if (c.widthFixed && c.$table.find('colgroup').length === 0) {
550
+ colgroup = $('<colgroup>');
551
+ overallWidth = $(table).width();
552
+ // only add col for visible columns - fixes #371
553
+ $(table.tBodies).not('.' + c.cssInfoBlock).find("tr:first").children(":visible").each(function() {
554
+ colgroup.append($('<col>').css('width', parseInt(($(this).width()/overallWidth)*1000, 10)/10 + '%'));
555
+ });
556
+ c.$table.prepend(colgroup);
557
+ }
558
+ }
559
+
560
+ function updateHeaderSortCount(table, list) {
561
+ var s, t, o, col, primary,
562
+ c = table.config,
563
+ sl = list || c.sortList;
564
+ c.sortList = [];
565
+ $.each(sl, function(i,v){
566
+ // ensure all sortList values are numeric - fixes #127
567
+ col = parseInt(v[0], 10);
568
+ // make sure header exists
569
+ o = c.$headers.filter('[data-column="' + col + '"]:last')[0];
570
+ if (o) { // prevents error if sorton array is wrong
571
+ // o.count = o.count + 1;
572
+ t = ('' + v[1]).match(/^(1|d|s|o|n)/);
573
+ t = t ? t[0] : '';
574
+ // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext
575
+ switch(t) {
576
+ case '1': case 'd': // descending
577
+ t = 1;
578
+ break;
579
+ case 's': // same direction (as primary column)
580
+ // if primary sort is set to "s", make it ascending
581
+ t = primary || 0;
582
+ break;
583
+ case 'o':
584
+ s = o.order[(primary || 0) % (c.sortReset ? 3 : 2)];
585
+ // opposite of primary column; but resets if primary resets
586
+ t = s === 0 ? 1 : s === 1 ? 0 : 2;
587
+ break;
588
+ case 'n':
589
+ o.count = o.count + 1;
590
+ t = o.order[(o.count) % (c.sortReset ? 3 : 2)];
591
+ break;
592
+ default: // ascending
593
+ t = 0;
594
+ break;
595
+ }
596
+ primary = i === 0 ? t : primary;
597
+ s = [ col, parseInt(t, 10) || 0 ];
598
+ c.sortList.push(s);
599
+ t = $.inArray(s[1], o.order); // fixes issue #167
600
+ o.count = t >= 0 ? t : s[1] % (c.sortReset ? 3 : 2);
601
+ }
602
+ });
603
+ }
604
+
605
+ function getCachedSortType(parsers, i) {
606
+ return (parsers && parsers[i]) ? parsers[i].type || '' : '';
607
+ }
608
+
609
+ function initSort(table, cell, event){
610
+ if (table.isUpdating) {
611
+ // let any updates complete before initializing a sort
612
+ return setTimeout(function(){ initSort(table, cell, event); }, 50);
613
+ }
614
+ var arry, indx, col, order, s,
615
+ c = table.config,
616
+ key = !event[c.sortMultiSortKey],
617
+ $table = c.$table;
618
+ // Only call sortStart if sorting is enabled
619
+ $table.trigger("sortStart", table);
620
+ // get current column sort order
621
+ cell.count = event[c.sortResetKey] ? 2 : (cell.count + 1) % (c.sortReset ? 3 : 2);
622
+ // reset all sorts on non-current column - issue #30
623
+ if (c.sortRestart) {
624
+ indx = cell;
625
+ c.$headers.each(function() {
626
+ // only reset counts on columns that weren't just clicked on and if not included in a multisort
627
+ if (this !== indx && (key || !$(this).is('.' + ts.css.sortDesc + ',.' + ts.css.sortAsc))) {
628
+ this.count = -1;
629
+ }
630
+ });
631
+ }
632
+ // get current column index
633
+ indx = cell.column;
634
+ // user only wants to sort on one column
635
+ if (key) {
636
+ // flush the sort list
637
+ c.sortList = [];
638
+ if (c.sortForce !== null) {
639
+ arry = c.sortForce;
640
+ for (col = 0; col < arry.length; col++) {
641
+ if (arry[col][0] !== indx) {
642
+ c.sortList.push(arry[col]);
643
+ }
644
+ }
645
+ }
646
+ // add column to sort list
647
+ order = cell.order[cell.count];
648
+ if (order < 2) {
649
+ c.sortList.push([indx, order]);
650
+ // add other columns if header spans across multiple
651
+ if (cell.colSpan > 1) {
652
+ for (col = 1; col < cell.colSpan; col++) {
653
+ c.sortList.push([indx + col, order]);
654
+ }
655
+ }
656
+ }
657
+ // multi column sorting
658
+ } else {
659
+ // get rid of the sortAppend before adding more - fixes issue #115 & #523
660
+ if (c.sortAppend && c.sortList.length > 1) {
661
+ for (col = 0; col < c.sortAppend.length; col++) {
662
+ s = ts.isValueInArray(c.sortAppend[col][0], c.sortList);
663
+ if (s >= 0) {
664
+ c.sortList.splice(s,1);
665
+ }
666
+ }
667
+ }
668
+ // the user has clicked on an already sorted column
669
+ if (ts.isValueInArray(indx, c.sortList) >= 0) {
670
+ // reverse the sorting direction
671
+ for (col = 0; col < c.sortList.length; col++) {
672
+ s = c.sortList[col];
673
+ order = c.$headers.filter('[data-column="' + s[0] + '"]:last')[0];
674
+ if (s[0] === indx) {
675
+ // order.count seems to be incorrect when compared to cell.count
676
+ s[1] = order.order[cell.count];
677
+ if (s[1] === 2) {
678
+ c.sortList.splice(col,1);
679
+ order.count = -1;
680
+ }
681
+ }
682
+ }
683
+ } else {
684
+ // add column to sort list array
685
+ order = cell.order[cell.count];
686
+ if (order < 2) {
687
+ c.sortList.push([indx, order]);
688
+ // add other columns if header spans across multiple
689
+ if (cell.colSpan > 1) {
690
+ for (col = 1; col < cell.colSpan; col++) {
691
+ c.sortList.push([indx + col, order]);
692
+ }
693
+ }
694
+ }
695
+ }
696
+ }
697
+ if (c.sortAppend !== null) {
698
+ arry = c.sortAppend;
699
+ for (col = 0; col < arry.length; col++) {
700
+ if (arry[col][0] !== indx) {
701
+ c.sortList.push(arry[col]);
702
+ }
703
+ }
704
+ }
705
+ // sortBegin event triggered immediately before the sort
706
+ $table.trigger("sortBegin", table);
707
+ // setTimeout needed so the processing icon shows up
708
+ setTimeout(function(){
709
+ // set css for headers
710
+ setHeadersCss(table);
711
+ multisort(table);
712
+ appendToTable(table);
713
+ $table.trigger("sortEnd", table);
714
+ }, 1);
715
+ }
716
+
717
+ // sort multiple columns
718
+ function multisort(table) { /*jshint loopfunc:true */
719
+ var i, k, num, col, sortTime, colMax,
720
+ cache, order, sort, x, y,
721
+ dir = 0,
722
+ c = table.config,
723
+ cts = c.textSorter || '',
724
+ sortList = c.sortList,
725
+ l = sortList.length,
726
+ bl = table.tBodies.length;
727
+ if (c.serverSideSorting || isEmptyObject(c.cache)) { // empty table - fixes #206/#346
728
+ return;
729
+ }
730
+ if (c.debug) { sortTime = new Date(); }
731
+ for (k = 0; k < bl; k++) {
732
+ colMax = c.cache[k].colMax;
733
+ cache = c.cache[k].normalized;
734
+
735
+ cache.sort(function(a, b) {
736
+ // cache is undefined here in IE, so don't use it!
737
+ for (i = 0; i < l; i++) {
738
+ col = sortList[i][0];
739
+ order = sortList[i][1];
740
+ // sort direction, true = asc, false = desc
741
+ dir = order === 0;
742
+
743
+ if (c.sortStable && a[col] === b[col] && l === 1) {
744
+ return a[c.columns].order - b[c.columns].order;
745
+ }
746
+
747
+ // fallback to natural sort since it is more robust
748
+ num = /n/i.test(getCachedSortType(c.parsers, col));
749
+ if (num && c.strings[col]) {
750
+ // sort strings in numerical columns
751
+ if (typeof (c.string[c.strings[col]]) === 'boolean') {
752
+ num = (dir ? 1 : -1) * (c.string[c.strings[col]] ? -1 : 1);
753
+ } else {
754
+ num = (c.strings[col]) ? c.string[c.strings[col]] || 0 : 0;
755
+ }
756
+ // fall back to built-in numeric sort
757
+ // var sort = $.tablespy["sort" + s](table, a[c], b[c], c, colMax[c], dir);
758
+ sort = c.numberSorter ? c.numberSorter(a[col], b[col], dir, colMax[col], table) :
759
+ ts[ 'sortNumeric' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], num, colMax[col], col, table);
760
+ } else {
761
+ // set a & b depending on sort direction
762
+ x = dir ? a : b;
763
+ y = dir ? b : a;
764
+ // text sort function
765
+ if (typeof(cts) === 'function') {
766
+ // custom OVERALL text sorter
767
+ sort = cts(x[col], y[col], dir, col, table);
768
+ } else if (typeof(cts) === 'object' && cts.hasOwnProperty(col)) {
769
+ // custom text sorter for a SPECIFIC COLUMN
770
+ sort = cts[col](x[col], y[col], dir, col, table);
771
+ } else {
772
+ // fall back to natural sort
773
+ sort = ts[ 'sortNatural' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], col, table, c);
774
+ }
775
+ }
776
+ if (sort) { return sort; }
777
+ }
778
+ return a[c.columns].order - b[c.columns].order;
779
+ });
780
+ }
781
+ if (c.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time", sortTime); }
782
+ }
783
+
784
+ function resortComplete($table, callback){
785
+ var table = $table[0];
786
+ if (table.isUpdating) {
787
+ $table.trigger('updateComplete', table);
788
+ }
789
+ if ($.isFunction(callback)) {
790
+ callback($table[0]);
791
+ }
792
+ }
793
+
794
+ function checkResort($table, flag, callback) {
795
+ var sl = $table[0].config.sortList;
796
+ // don't try to resort if the table is still processing
797
+ // this will catch spamming of the updateCell method
798
+ if (flag !== false && !$table[0].isProcessing && sl.length) {
799
+ $table.trigger("sorton", [sl, function(){
800
+ resortComplete($table, callback);
801
+ }, true]);
802
+ } else {
803
+ resortComplete($table, callback);
804
+ ts.applyWidget($table[0], false);
805
+ }
806
+ }
807
+
808
+ function bindMethods(table){
809
+ var c = table.config,
810
+ $table = c.$table;
811
+ // apply easy methods that trigger bound events
812
+ $table
813
+ .unbind('sortReset update updateRows updateCell updateAll addRows updateComplete sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave '.split(' ').join(c.namespace + ' '))
814
+ .bind("sortReset" + c.namespace, function(e, callback){
815
+ e.stopPropagation();
816
+ c.sortList = [];
817
+ setHeadersCss(table);
818
+ multisort(table);
819
+ appendToTable(table);
820
+ if ($.isFunction(callback)) {
821
+ callback(table);
822
+ }
823
+ })
824
+ .bind("updateAll" + c.namespace, function(e, resort, callback){
825
+ e.stopPropagation();
826
+ table.isUpdating = true;
827
+ ts.refreshWidgets(table, true, true);
828
+ ts.restoreHeaders(table);
829
+ buildHeaders(table);
830
+ ts.bindEvents(table, c.$headers, true);
831
+ bindMethods(table);
832
+ commonUpdate(table, resort, callback);
833
+ })
834
+ .bind("update" + c.namespace + " updateRows" + c.namespace, function(e, resort, callback) {
835
+ e.stopPropagation();
836
+ table.isUpdating = true;
837
+ // update sorting (if enabled/disabled)
838
+ updateHeader(table);
839
+ commonUpdate(table, resort, callback);
840
+ })
841
+ .bind("updateCell" + c.namespace, function(e, cell, resort, callback) {
842
+ e.stopPropagation();
843
+ table.isUpdating = true;
844
+ $table.find(c.selectorRemove).remove();
845
+ // get position from the dom
846
+ var v, t, row, icell,
847
+ $tb = $table.find('tbody'),
848
+ $cell = $(cell),
849
+ // update cache - format: function(s, table, cell, cellIndex)
850
+ // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr');
851
+ tbdy = $tb.index( $.fn.closest ? $cell.closest('tbody') : $cell.parents('tbody').filter(':first') ),
852
+ $row = $.fn.closest ? $cell.closest('tr') : $cell.parents('tr').filter(':first');
853
+ cell = $cell[0]; // in case cell is a jQuery object
854
+ // tbody may not exist if update is initialized while tbody is removed for processing
855
+ if ($tb.length && tbdy >= 0) {
856
+ row = $tb.eq(tbdy).find('tr').index( $row );
857
+ icell = $cell.index();
858
+ c.cache[tbdy].normalized[row][c.columns].$row = $row;
859
+ if (typeof c.extractors[icell].id === 'undefined') {
860
+ t = getElementText(table, cell, icell);
861
+ } else {
862
+ t = c.extractors[icell].format( getElementText(table, cell, icell), table, cell, icell );
863
+ }
864
+ v = c.parsers[icell].id === 'no-parser' ? '' :
865
+ c.parsers[icell].format( t, table, cell, icell );
866
+ c.cache[tbdy].normalized[row][icell] = c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v;
867
+ if ((c.parsers[icell].type || '').toLowerCase() === "numeric") {
868
+ // update column max value (ignore sign)
869
+ c.cache[tbdy].colMax[icell] = Math.max(Math.abs(v) || 0, c.cache[tbdy].colMax[icell] || 0);
870
+ }
871
+ checkResort($table, resort, callback);
872
+ }
873
+ })
874
+ .bind("addRows" + c.namespace, function(e, $row, resort, callback) {
875
+ e.stopPropagation();
876
+ table.isUpdating = true;
877
+ if (isEmptyObject(c.cache)) {
878
+ // empty table, do an update instead - fixes #450
879
+ updateHeader(table);
880
+ commonUpdate(table, resort, callback);
881
+ } else {
882
+ $row = $($row).attr('role', 'row'); // make sure we're using a jQuery object
883
+ var i, j, l, t, v, rowData, cells,
884
+ rows = $row.filter('tr').length,
885
+ tbdy = $table.find('tbody').index( $row.parents('tbody').filter(':first') );
886
+ // fixes adding rows to an empty table - see issue #179
887
+ if (!(c.parsers && c.parsers.length)) {
888
+ buildParserCache(table);
889
+ }
890
+ // add each row
891
+ for (i = 0; i < rows; i++) {
892
+ l = $row[i].cells.length;
893
+ cells = [];
894
+ rowData = {
895
+ child: [],
896
+ $row : $row.eq(i),
897
+ order: c.cache[tbdy].normalized.length
898
+ };
899
+ // add each cell
900
+ for (j = 0; j < l; j++) {
901
+ if (typeof c.extractors[j].id === 'undefined') {
902
+ t = getElementText(table, $row[i].cells[j], j);
903
+ } else {
904
+ t = c.extractors[j].format( getElementText(table, $row[i].cells[j], j), table, $row[i].cells[j], j );
905
+ }
906
+ v = c.parsers[j].id === 'no-parser' ? '' :
907
+ c.parsers[j].format( t, table, $row[i].cells[j], j );
908
+ cells[j] = c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v;
909
+ if ((c.parsers[j].type || '').toLowerCase() === "numeric") {
910
+ // update column max value (ignore sign)
911
+ c.cache[tbdy].colMax[j] = Math.max(Math.abs(cells[j]) || 0, c.cache[tbdy].colMax[j] || 0);
912
+ }
913
+ }
914
+ // add the row data to the end
915
+ cells.push(rowData);
916
+ // update cache
917
+ c.cache[tbdy].normalized.push(cells);
918
+ }
919
+ // resort using current settings
920
+ checkResort($table, resort, callback);
921
+ }
922
+ })
923
+ .bind("updateComplete" + c.namespace, function(){
924
+ table.isUpdating = false;
925
+ })
926
+ .bind("sorton" + c.namespace, function(e, list, callback, init) {
927
+ var c = table.config;
928
+ e.stopPropagation();
929
+ $table.trigger("sortStart", this);
930
+ // update header count index
931
+ updateHeaderSortCount(table, list);
932
+ // set css for headers
933
+ setHeadersCss(table);
934
+ // fixes #346
935
+ if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); }
936
+ $table.trigger("sortBegin", this);
937
+ // sort the table and append it to the dom
938
+ multisort(table);
939
+ appendToTable(table, init);
940
+ $table.trigger("sortEnd", this);
941
+ ts.applyWidget(table);
942
+ if ($.isFunction(callback)) {
943
+ callback(table);
944
+ }
945
+ })
946
+ .bind("appendCache" + c.namespace, function(e, callback, init) {
947
+ e.stopPropagation();
948
+ appendToTable(table, init);
949
+ if ($.isFunction(callback)) {
950
+ callback(table);
951
+ }
952
+ })
953
+ .bind("updateCache" + c.namespace, function(e, callback){
954
+ // rebuild parsers
955
+ if (!(c.parsers && c.parsers.length)) {
956
+ buildParserCache(table);
957
+ }
958
+ // rebuild the cache map
959
+ buildCache(table);
960
+ if ($.isFunction(callback)) {
961
+ callback(table);
962
+ }
963
+ })
964
+ .bind("applyWidgetId" + c.namespace, function(e, id) {
965
+ e.stopPropagation();
966
+ ts.getWidgetById(id).format(table, c, c.widgetOptions);
967
+ })
968
+ .bind("applyWidgets" + c.namespace, function(e, init) {
969
+ e.stopPropagation();
970
+ // apply widgets
971
+ ts.applyWidget(table, init);
972
+ })
973
+ .bind("refreshWidgets" + c.namespace, function(e, all, dontapply){
974
+ e.stopPropagation();
975
+ ts.refreshWidgets(table, all, dontapply);
976
+ })
977
+ .bind("destroy" + c.namespace, function(e, c, cb){
978
+ e.stopPropagation();
979
+ ts.destroy(table, c, cb);
980
+ })
981
+ .bind("resetToLoadState" + c.namespace, function(){
982
+ // remove all widgets
983
+ ts.refreshWidgets(table, true, true);
984
+ // restore original settings; this clears out current settings, but does not clear
985
+ // values saved to storage.
986
+ c = $.extend(true, ts.defaults, c.originalSettings);
987
+ table.hasInitialized = false;
988
+ // setup the entire table again
989
+ ts.setup( table, c );
990
+ });
991
+ }
992
+
993
+ /* public methods */
994
+ ts.construct = function(settings) {
995
+ return this.each(function() {
996
+ var table = this,
997
+ // merge & extend config options
998
+ c = $.extend(true, {}, ts.defaults, settings);
999
+ // save initial settings
1000
+ c.originalSettings = settings;
1001
+ // create a table from data (build table widget)
1002
+ if (!table.hasInitialized && ts.buildTable && this.tagName !== 'TABLE') {
1003
+ // return the table (in case the original target is the table's container)
1004
+ ts.buildTable(table, c);
1005
+ } else {
1006
+ ts.setup(table, c);
1007
+ }
1008
+ });
1009
+ };
1010
+
1011
+ ts.setup = function(table, c) {
1012
+ // if no thead or tbody, or tablespy is already present, quit
1013
+ if (!table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true) {
1014
+ return c.debug ? log('ERROR: stopping initialization! No table, thead, tbody or tablespy has already been initialized') : '';
1015
+ }
1016
+
1017
+ var k = '',
1018
+ $table = $(table),
1019
+ m = $.metadata;
1020
+ // initialization flag
1021
+ table.hasInitialized = false;
1022
+ // table is being processed flag
1023
+ table.isProcessing = true;
1024
+ // make sure to store the config object
1025
+ table.config = c;
1026
+ // save the settings where they read
1027
+ $.data(table, "tablespy", c);
1028
+ if (c.debug) { $.data( table, 'startoveralltimer', new Date()); }
1029
+
1030
+ // removing this in version 3 (only supports jQuery 1.7+)
1031
+ c.supportsDataObject = (function(version) {
1032
+ version[0] = parseInt(version[0], 10);
1033
+ return (version[0] > 1) || (version[0] === 1 && parseInt(version[1], 10) >= 4);
1034
+ })($.fn.jquery.split("."));
1035
+ // digit sort text location; keeping max+/- for backwards compatibility
1036
+ c.string = { 'max': 1, 'min': -1, 'emptymin': 1, 'emptymax': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false };
1037
+ // ensure case insensitivity
1038
+ c.emptyTo = c.emptyTo.toLowerCase();
1039
+ c.stringTo = c.stringTo.toLowerCase();
1040
+ // add table theme class only if there isn't already one there
1041
+ if (!/tablespy\-/.test($table.attr('class'))) {
1042
+ k = (c.theme !== '' ? ' tablespy-' + c.theme : '');
1043
+ }
1044
+ c.table = table;
1045
+ c.$table = $table
1046
+ .addClass(ts.css.table + ' ' + c.tableClass + k)
1047
+ .attr('role', 'grid');
1048
+ c.$headers = $table.find(c.selectorHeaders);
1049
+
1050
+ // give the table a unique id, which will be used in namespace binding
1051
+ if (!c.namespace) {
1052
+ c.namespace = '.tablespy' + Math.random().toString(16).slice(2);
1053
+ } else {
1054
+ // make sure namespace starts with a period & doesn't have weird characters
1055
+ c.namespace = '.' + c.namespace.replace(/\W/g,'');
1056
+ }
1057
+
1058
+ c.$table.children().children('tr').attr('role', 'row');
1059
+ c.$tbodies = $table.children('tbody:not(.' + c.cssInfoBlock + ')').attr({
1060
+ 'aria-live' : 'polite',
1061
+ 'aria-relevant' : 'all'
1062
+ });
1063
+ if (c.$table.find('caption').length) {
1064
+ c.$table.attr('aria-labelledby', 'theCaption');
1065
+ }
1066
+ c.widgetInit = {}; // keep a list of initialized widgets
1067
+ // change textExtraction via data-attribute
1068
+ c.textExtraction = c.$table.attr('data-text-extraction') || c.textExtraction || 'basic';
1069
+ // build headers
1070
+ buildHeaders(table);
1071
+ // fixate columns if the users supplies the fixedWidth option
1072
+ // do this after theme has been applied
1073
+ fixColumnWidth(table);
1074
+ // try to auto detect column type, and store in tables config
1075
+ buildParserCache(table);
1076
+ // start total row count at zero
1077
+ c.totalRows = 0;
1078
+ // build the cache for the tbody cells
1079
+ // delayInit will delay building the cache until the user starts a sort
1080
+ if (!c.delayInit) { buildCache(table); }
1081
+ // bind all header events and methods
1082
+ ts.bindEvents(table, c.$headers, true);
1083
+ bindMethods(table);
1084
+ // get sort list from jQuery data or metadata
1085
+ // in jQuery < 1.4, an error occurs when calling $table.data()
1086
+ if (c.supportsDataObject && typeof $table.data().sortlist !== 'undefined') {
1087
+ c.sortList = $table.data().sortlist;
1088
+ } else if (m && ($table.metadata() && $table.metadata().sortlist)) {
1089
+ c.sortList = $table.metadata().sortlist;
1090
+ }
1091
+ // apply widget init code
1092
+ ts.applyWidget(table, true);
1093
+ // if user has supplied a sort list to constructor
1094
+ if (c.sortList.length > 0) {
1095
+ $table.trigger("sorton", [c.sortList, {}, !c.initWidgets, true]);
1096
+ } else {
1097
+ setHeadersCss(table);
1098
+ if (c.initWidgets) {
1099
+ // apply widget format
1100
+ ts.applyWidget(table, false);
1101
+ }
1102
+ }
1103
+
1104
+ // show processesing icon
1105
+ if (c.showProcessing) {
1106
+ $table
1107
+ .unbind('sortBegin' + c.namespace + ' sortEnd' + c.namespace)
1108
+ .bind('sortBegin' + c.namespace + ' sortEnd' + c.namespace, function(e) {
1109
+ clearTimeout(c.processTimer);
1110
+ ts.isProcessing(table);
1111
+ if (e.type === 'sortBegin') {
1112
+ c.processTimer = setTimeout(function(){
1113
+ ts.isProcessing(table, true);
1114
+ }, 500);
1115
+ }
1116
+ });
1117
+ }
1118
+
1119
+ // initialized
1120
+ table.hasInitialized = true;
1121
+ table.isProcessing = false;
1122
+ if (c.debug) {
1123
+ ts.benchmark("Overall initialization time", $.data( table, 'startoveralltimer'));
1124
+ }
1125
+ $table.trigger('tablespy-initialized', table);
1126
+ if (typeof c.initialized === 'function') { c.initialized(table); }
1127
+ };
1128
+
1129
+ ts.getColumnData = function(table, obj, indx, getCell){
1130
+ if (typeof obj === 'undefined' || obj === null) { return; }
1131
+ table = $(table)[0];
1132
+ var result, $h, k,
1133
+ c = table.config;
1134
+ if (obj[indx]) {
1135
+ return getCell ? obj[indx] : obj[c.$headers.index( c.$headers.filter('[data-column="' + indx + '"]:last') )];
1136
+ }
1137
+ for (k in obj) {
1138
+ if (typeof k === 'string') {
1139
+ if (getCell) {
1140
+ // get header cell
1141
+ $h = c.$headers.eq(indx).filter(k);
1142
+ } else {
1143
+ // get column indexed cell
1144
+ $h = c.$headers.filter('[data-column="' + indx + '"]:last').filter(k);
1145
+ }
1146
+ if ($h.length) {
1147
+ return obj[k];
1148
+ }
1149
+ }
1150
+ }
1151
+ return result;
1152
+ };
1153
+
1154
+ // computeTableHeaderCellIndexes from:
1155
+ // http://www.javascripttoolbox.com/lib/table/examples.php
1156
+ // http://www.javascripttoolbox.com/temp/table_cellindex.html
1157
+ ts.computeColumnIndex = function(trs) {
1158
+ var matrix = [],
1159
+ lookup = {},
1160
+ cols = 0, // determine the number of columns
1161
+ i, j, k, l, $cell, cell, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow;
1162
+ for (i = 0; i < trs.length; i++) {
1163
+ cells = trs[i].cells;
1164
+ for (j = 0; j < cells.length; j++) {
1165
+ cell = cells[j];
1166
+ $cell = $(cell);
1167
+ rowIndex = cell.parentNode.rowIndex;
1168
+ cellId = rowIndex + "-" + $cell.index();
1169
+ rowSpan = cell.rowSpan || 1;
1170
+ colSpan = cell.colSpan || 1;
1171
+ if (typeof(matrix[rowIndex]) === "undefined") {
1172
+ matrix[rowIndex] = [];
1173
+ }
1174
+ // Find first available column in the first row
1175
+ for (k = 0; k < matrix[rowIndex].length + 1; k++) {
1176
+ if (typeof(matrix[rowIndex][k]) === "undefined") {
1177
+ firstAvailCol = k;
1178
+ break;
1179
+ }
1180
+ }
1181
+ lookup[cellId] = firstAvailCol;
1182
+ cols = Math.max(firstAvailCol, cols);
1183
+ // add data-column
1184
+ $cell.attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex
1185
+ for (k = rowIndex; k < rowIndex + rowSpan; k++) {
1186
+ if (typeof(matrix[k]) === "undefined") {
1187
+ matrix[k] = [];
1188
+ }
1189
+ matrixrow = matrix[k];
1190
+ for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
1191
+ matrixrow[l] = "x";
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+ // may not be accurate if # header columns !== # tbody columns
1197
+ return cols + 1; // add one because it's a zero-based index
1198
+ };
1199
+
1200
+ // *** Process table ***
1201
+ // add processing indicator
1202
+ ts.isProcessing = function(table, toggle, $ths) {
1203
+ table = $(table);
1204
+ var c = table[0].config,
1205
+ // default to all headers
1206
+ $h = $ths || table.find('.' + ts.css.header);
1207
+ if (toggle) {
1208
+ // don't use sortList if custom $ths used
1209
+ if (typeof $ths !== 'undefined' && c.sortList.length > 0) {
1210
+ // get headers from the sortList
1211
+ $h = $h.filter(function(){
1212
+ // get data-column from attr to keep compatibility with jQuery 1.2.6
1213
+ return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList) >= 0;
1214
+ });
1215
+ }
1216
+ table.add($h).addClass(ts.css.processing + ' ' + c.cssProcessing);
1217
+ } else {
1218
+ table.add($h).removeClass(ts.css.processing + ' ' + c.cssProcessing);
1219
+ }
1220
+ };
1221
+
1222
+ // detach tbody but save the position
1223
+ // don't use tbody because there are portions that look for a tbody index (updateCell)
1224
+ ts.processTbody = function(table, $tb, getIt){
1225
+ table = $(table)[0];
1226
+ var holdr;
1227
+ if (getIt) {
1228
+ table.isProcessing = true;
1229
+ $tb.before('<span class="tablespy-savemyplace"/>');
1230
+ holdr = ($.fn.detach) ? $tb.detach() : $tb.remove();
1231
+ return holdr;
1232
+ }
1233
+ holdr = $(table).find('span.tablespy-savemyplace');
1234
+ $tb.insertAfter( holdr );
1235
+ holdr.remove();
1236
+ table.isProcessing = false;
1237
+ };
1238
+
1239
+ ts.clearTableBody = function(table) {
1240
+ $(table)[0].config.$tbodies.children().detach();
1241
+ };
1242
+
1243
+ ts.bindEvents = function(table, $headers, core){
1244
+ table = $(table)[0];
1245
+ var downTime,
1246
+ c = table.config;
1247
+ if (core !== true) {
1248
+ c.$extraHeaders = c.$extraHeaders ? c.$extraHeaders.add($headers) : $headers;
1249
+ }
1250
+ // apply event handling to headers and/or additional headers (stickyheaders, scroller, etc)
1251
+ $headers
1252
+ // http://stackoverflow.com/questions/5312849/jquery-find-self;
1253
+ .find(c.selectorSort).add( $headers.filter(c.selectorSort) )
1254
+ .unbind('mousedown mouseup sort keyup '.split(' ').join(c.namespace + ' '))
1255
+ .bind('mousedown mouseup sort keyup '.split(' ').join(c.namespace + ' '), function(e, external) {
1256
+ var cell, type = e.type;
1257
+ // only recognize left clicks or enter
1258
+ if ( ((e.which || e.button) !== 1 && !/sort|keyup/.test(type)) || (type === 'keyup' && e.which !== 13) ) {
1259
+ return;
1260
+ }
1261
+ // ignore long clicks (prevents resizable widget from initializing a sort)
1262
+ if (type === 'mouseup' && external !== true && (new Date().getTime() - downTime > 250)) { return; }
1263
+ // set timer on mousedown
1264
+ if (type === 'mousedown') {
1265
+ downTime = new Date().getTime();
1266
+ return /(input|select|button|textarea)/i.test(e.target.tagName) ? '' : !c.cancelSelection;
1267
+ }
1268
+ if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); }
1269
+ // jQuery v1.2.6 doesn't have closest()
1270
+ cell = $.fn.closest ? $(this).closest('th, td')[0] : /TH|TD/.test(this.tagName) ? this : $(this).parents('th, td')[0];
1271
+ // reference original table headers and find the same cell
1272
+ cell = c.$headers[ $headers.index( cell ) ];
1273
+ if (!cell.sortDisabled) {
1274
+ initSort(table, cell, e);
1275
+ }
1276
+ });
1277
+ if (c.cancelSelection) {
1278
+ // cancel selection
1279
+ $headers
1280
+ .attr('unselectable', 'on')
1281
+ .bind('selectstart', false)
1282
+ .css({
1283
+ 'user-select': 'none',
1284
+ 'MozUserSelect': 'none' // not needed for jQuery 1.8+
1285
+ });
1286
+ }
1287
+ };
1288
+
1289
+ // restore headers
1290
+ ts.restoreHeaders = function(table){
1291
+ var c = $(table)[0].config;
1292
+ // don't use c.$headers here in case header cells were swapped
1293
+ c.$table.find(c.selectorHeaders).each(function(i){
1294
+ // only restore header cells if it is wrapped
1295
+ // because this is also used by the updateAll method
1296
+ if ($(this).find('.' + ts.css.headerIn).length){
1297
+ $(this).html( c.headerContent[i] );
1298
+ }
1299
+ });
1300
+ };
1301
+
1302
+ ts.destroy = function(table, removeClasses, callback){
1303
+ table = $(table)[0];
1304
+ if (!table.hasInitialized) { return; }
1305
+ // remove all widgets
1306
+ ts.refreshWidgets(table, true, true);
1307
+ var $t = $(table), c = table.config,
1308
+ $h = $t.find('thead:first'),
1309
+ $r = $h.find('tr.' + ts.css.headerRow).removeClass(ts.css.headerRow + ' ' + c.cssHeaderRow),
1310
+ $f = $t.find('tfoot:first > tr').children('th, td');
1311
+ if (removeClasses === false && $.inArray('uitheme', c.widgets) >= 0) {
1312
+ // reapply uitheme classes, in case we want to maintain appearance
1313
+ $t.trigger('applyWidgetId', ['uitheme']);
1314
+ $t.trigger('applyWidgetId', ['zebra']);
1315
+ }
1316
+ // remove widget added rows, just in case
1317
+ $h.find('tr').not($r).remove();
1318
+ // disable tablespy
1319
+ $t
1320
+ .removeData('tablespy')
1321
+ .unbind('sortReset update updateAll updateRows updateCell addRows updateComplete sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave keypress sortBegin sortEnd resetToLoadState '.split(' ').join(c.namespace + ' '));
1322
+ c.$headers.add($f)
1323
+ .removeClass( [ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone].join(' ') )
1324
+ .removeAttr('data-column')
1325
+ .removeAttr('aria-label')
1326
+ .attr('aria-disabled', 'true');
1327
+ $r.find(c.selectorSort).unbind('mousedown mouseup keypress '.split(' ').join(c.namespace + ' '));
1328
+ ts.restoreHeaders(table);
1329
+ $t.toggleClass(ts.css.table + ' ' + c.tableClass + ' tablespy-' + c.theme, removeClasses === false);
1330
+ // clear flag in case the plugin is initialized again
1331
+ table.hasInitialized = false;
1332
+ delete table.config.cache;
1333
+ if (typeof callback === 'function') {
1334
+ callback(table);
1335
+ }
1336
+ };
1337
+
1338
+ // *** sort functions ***
1339
+ // regex used in natural sort
1340
+ ts.regex = {
1341
+ chunk : /(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, // chunk/tokenize numbers & letters
1342
+ chunks: /(^\\0|\\0$)/, // replace chunks @ ends
1343
+ hex: /^0x[0-9a-f]+$/i // hex
1344
+ };
1345
+
1346
+ // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed)
1347
+ // this function will only accept strings, or you'll see "TypeError: undefined is not a function"
1348
+ // I could add a = a.toString(); b = b.toString(); but it'll slow down the sort overall
1349
+ ts.sortNatural = function(a, b) {
1350
+ if (a === b) { return 0; }
1351
+ var xN, xD, yN, yD, xF, yF, i, mx,
1352
+ r = ts.regex;
1353
+ // first try and sort Hex codes
1354
+ if (r.hex.test(b)) {
1355
+ xD = parseInt(a.match(r.hex), 16);
1356
+ yD = parseInt(b.match(r.hex), 16);
1357
+ if ( xD < yD ) { return -1; }
1358
+ if ( xD > yD ) { return 1; }
1359
+ }
1360
+ // chunk/tokenize
1361
+ xN = a.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0');
1362
+ yN = b.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0');
1363
+ mx = Math.max(xN.length, yN.length);
1364
+ // natural sorting through split numeric strings and default strings
1365
+ for (i = 0; i < mx; i++) {
1366
+ // find floats not starting with '0', string or 0 if not defined
1367
+ xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0;
1368
+ yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0;
1369
+ // handle numeric vs string comparison - number < string - (Kyle Adams)
1370
+ if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; }
1371
+ // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
1372
+ if (typeof xF !== typeof yF) {
1373
+ xF += '';
1374
+ yF += '';
1375
+ }
1376
+ if (xF < yF) { return -1; }
1377
+ if (xF > yF) { return 1; }
1378
+ }
1379
+ return 0;
1380
+ };
1381
+
1382
+ ts.sortNaturalAsc = function(a, b, col, table, c) {
1383
+ if (a === b) { return 0; }
1384
+ var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1385
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1386
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1387
+ return ts.sortNatural(a, b);
1388
+ };
1389
+
1390
+ ts.sortNaturalDesc = function(a, b, col, table, c) {
1391
+ if (a === b) { return 0; }
1392
+ var e = c.string[ (c.empties[col] || c.emptyTo ) ];
1393
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1394
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1395
+ return ts.sortNatural(b, a);
1396
+ };
1397
+
1398
+ // basic alphabetical sort
1399
+ ts.sortText = function(a, b) {
1400
+ return a > b ? 1 : (a < b ? -1 : 0);
1401
+ };
1402
+
1403
+ // return text string value by adding up ascii value
1404
+ // so the text is somewhat sorted when using a digital sort
1405
+ // this is NOT an alphanumeric sort
1406
+ ts.getTextValue = function(a, num, mx) {
1407
+ if (mx) {
1408
+ // make sure the text value is greater than the max numerical value (mx)
1409
+ var i, l = a ? a.length : 0, n = mx + num;
1410
+ for (i = 0; i < l; i++) {
1411
+ n += a.charCodeAt(i);
1412
+ }
1413
+ return num * n;
1414
+ }
1415
+ return 0;
1416
+ };
1417
+
1418
+ ts.sortNumericAsc = function(a, b, num, mx, col, table) {
1419
+ if (a === b) { return 0; }
1420
+ var c = table.config,
1421
+ e = c.string[ (c.empties[col] || c.emptyTo ) ];
1422
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : -e || -1; }
1423
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : e || 1; }
1424
+ if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1425
+ if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1426
+ return a - b;
1427
+ };
1428
+
1429
+ ts.sortNumericDesc = function(a, b, num, mx, col, table) {
1430
+ if (a === b) { return 0; }
1431
+ var c = table.config,
1432
+ e = c.string[ (c.empties[col] || c.emptyTo ) ];
1433
+ if (a === '' && e !== 0) { return typeof e === 'boolean' ? (e ? -1 : 1) : e || 1; }
1434
+ if (b === '' && e !== 0) { return typeof e === 'boolean' ? (e ? 1 : -1) : -e || -1; }
1435
+ if (isNaN(a)) { a = ts.getTextValue(a, num, mx); }
1436
+ if (isNaN(b)) { b = ts.getTextValue(b, num, mx); }
1437
+ return b - a;
1438
+ };
1439
+
1440
+ ts.sortNumeric = function(a, b) {
1441
+ return a - b;
1442
+ };
1443
+
1444
+ // used when replacing accented characters during sorting
1445
+ ts.characterEquivalents = {
1446
+ "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5", // áàâãäąå
1447
+ "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5", // ÁÀÂÃÄĄÅ
1448
+ "c" : "\u00e7\u0107\u010d", // çćč
1449
+ "C" : "\u00c7\u0106\u010c", // ÇĆČ
1450
+ "e" : "\u00e9\u00e8\u00ea\u00eb\u011b\u0119", // éèêëěę
1451
+ "E" : "\u00c9\u00c8\u00ca\u00cb\u011a\u0118", // ÉÈÊËĚĘ
1452
+ "i" : "\u00ed\u00ec\u0130\u00ee\u00ef\u0131", // íìİîïı
1453
+ "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌİÎÏ
1454
+ "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö
1455
+ "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ
1456
+ "ss": "\u00df", // ß (s sharp)
1457
+ "SS": "\u1e9e", // ẞ (Capital sharp s)
1458
+ "u" : "\u00fa\u00f9\u00fb\u00fc\u016f", // úùûüů
1459
+ "U" : "\u00da\u00d9\u00db\u00dc\u016e" // ÚÙÛÜŮ
1460
+ };
1461
+ ts.replaceAccents = function(s) {
1462
+ var a, acc = '[', eq = ts.characterEquivalents;
1463
+ if (!ts.characterRegex) {
1464
+ ts.characterRegexArray = {};
1465
+ for (a in eq) {
1466
+ if (typeof a === 'string') {
1467
+ acc += eq[a];
1468
+ ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g');
1469
+ }
1470
+ }
1471
+ ts.characterRegex = new RegExp(acc + ']');
1472
+ }
1473
+ if (ts.characterRegex.test(s)) {
1474
+ for (a in eq) {
1475
+ if (typeof a === 'string') {
1476
+ s = s.replace( ts.characterRegexArray[a], a );
1477
+ }
1478
+ }
1479
+ }
1480
+ return s;
1481
+ };
1482
+
1483
+ // *** utilities ***
1484
+ ts.isValueInArray = function(column, arry) {
1485
+ var indx, len = arry.length;
1486
+ for (indx = 0; indx < len; indx++) {
1487
+ if (arry[indx][0] === column) {
1488
+ return indx;
1489
+ }
1490
+ }
1491
+ return -1;
1492
+ };
1493
+
1494
+ ts.addParser = function(parser) {
1495
+ var i, l = ts.parsers.length, a = true;
1496
+ for (i = 0; i < l; i++) {
1497
+ if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) {
1498
+ a = false;
1499
+ }
1500
+ }
1501
+ if (a) {
1502
+ ts.parsers.push(parser);
1503
+ }
1504
+ };
1505
+
1506
+ ts.getParserById = function(name) {
1507
+ /*jshint eqeqeq:false */
1508
+ if (name == 'false') { return false; }
1509
+ var i, l = ts.parsers.length;
1510
+ for (i = 0; i < l; i++) {
1511
+ if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) {
1512
+ return ts.parsers[i];
1513
+ }
1514
+ }
1515
+ return false;
1516
+ };
1517
+
1518
+ ts.addWidget = function(widget) {
1519
+ ts.widgets.push(widget);
1520
+ };
1521
+
1522
+ ts.hasWidget = function(table, name){
1523
+ table = $(table);
1524
+ return table.length && table[0].config && table[0].config.widgetInit[name] || false;
1525
+ };
1526
+
1527
+ ts.getWidgetById = function(name) {
1528
+ var i, w, l = ts.widgets.length;
1529
+ for (i = 0; i < l; i++) {
1530
+ w = ts.widgets[i];
1531
+ if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) {
1532
+ return w;
1533
+ }
1534
+ }
1535
+ };
1536
+
1537
+ ts.applyWidget = function(table, init) {
1538
+ table = $(table)[0]; // in case this is called externally
1539
+ var c = table.config,
1540
+ wo = c.widgetOptions,
1541
+ widgets = [],
1542
+ time, w, wd;
1543
+ // prevent numerous consecutive widget applications
1544
+ if (init !== false && table.hasInitialized && (table.isApplyingWidgets || table.isUpdating)) { return; }
1545
+ if (c.debug) { time = new Date(); }
1546
+ if (c.widgets.length) {
1547
+ table.isApplyingWidgets = true;
1548
+ // ensure unique widget ids
1549
+ c.widgets = $.grep(c.widgets, function(v, k){
1550
+ return $.inArray(v, c.widgets) === k;
1551
+ });
1552
+ // build widget array & add priority as needed
1553
+ $.each(c.widgets || [], function(i,n){
1554
+ wd = ts.getWidgetById(n);
1555
+ if (wd && wd.id) {
1556
+ // set priority to 10 if not defined
1557
+ if (!wd.priority) { wd.priority = 10; }
1558
+ widgets[i] = wd;
1559
+ }
1560
+ });
1561
+ // sort widgets by priority
1562
+ widgets.sort(function(a, b){
1563
+ return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1;
1564
+ });
1565
+ // add/update selected widgets
1566
+ $.each(widgets, function(i,w){
1567
+ if (w) {
1568
+ if (init || !(c.widgetInit[w.id])) {
1569
+ // set init flag first to prevent calling init more than once (e.g. pager)
1570
+ c.widgetInit[w.id] = true;
1571
+ if (w.hasOwnProperty('options')) {
1572
+ wo = table.config.widgetOptions = $.extend( true, {}, w.options, wo );
1573
+ }
1574
+ if (w.hasOwnProperty('init')) {
1575
+ w.init(table, w, c, wo);
1576
+ }
1577
+ }
1578
+ if (!init && w.hasOwnProperty('format')) {
1579
+ w.format(table, c, wo, false);
1580
+ }
1581
+ }
1582
+ });
1583
+ }
1584
+ setTimeout(function(){
1585
+ table.isApplyingWidgets = false;
1586
+ }, 0);
1587
+ if (c.debug) {
1588
+ w = c.widgets.length;
1589
+ benchmark("Completed " + (init === true ? "initializing " : "applying ") + w + " widget" + (w !== 1 ? "s" : ""), time);
1590
+ }
1591
+ };
1592
+
1593
+ ts.refreshWidgets = function(table, doAll, dontapply) {
1594
+ table = $(table)[0]; // see issue #243
1595
+ var i, c = table.config,
1596
+ cw = c.widgets,
1597
+ w = ts.widgets, l = w.length;
1598
+ // remove previous widgets
1599
+ for (i = 0; i < l; i++){
1600
+ if ( w[i] && w[i].id && (doAll || $.inArray( w[i].id, cw ) < 0) ) {
1601
+ if (c.debug) { log( 'Refeshing widgets: Removing "' + w[i].id + '"' ); }
1602
+ // only remove widgets that have been initialized - fixes #442
1603
+ if (w[i].hasOwnProperty('remove') && c.widgetInit[w[i].id]) {
1604
+ w[i].remove(table, c, c.widgetOptions);
1605
+ c.widgetInit[w[i].id] = false;
1606
+ }
1607
+ }
1608
+ }
1609
+ if (dontapply !== true) {
1610
+ ts.applyWidget(table, doAll);
1611
+ }
1612
+ };
1613
+
1614
+ // get sorter, string, empty, etc options for each column from
1615
+ // jQuery data, metadata, header option or header class name ("sorter-false")
1616
+ // priority = jQuery data > meta > headers option > header class name
1617
+ ts.getData = function(h, ch, key) {
1618
+ var val = '', $h = $(h), m, cl;
1619
+ if (!$h.length) { return ''; }
1620
+ m = $.metadata ? $h.metadata() : false;
1621
+ cl = ' ' + ($h.attr('class') || '');
1622
+ if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){
1623
+ // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder"
1624
+ // "data-sort-initial-order" is assigned to "sortInitialOrder"
1625
+ val += $h.data(key) || $h.data(key.toLowerCase());
1626
+ } else if (m && typeof m[key] !== 'undefined') {
1627
+ val += m[key];
1628
+ } else if (ch && typeof ch[key] !== 'undefined') {
1629
+ val += ch[key];
1630
+ } else if (cl !== ' ' && cl.match(' ' + key + '-')) {
1631
+ // include sorter class name "sorter-text", etc; now works with "sorter-my-custom-parser"
1632
+ val = cl.match( new RegExp('\\s' + key + '-([\\w-]+)') )[1] || '';
1633
+ }
1634
+ return $.trim(val);
1635
+ };
1636
+
1637
+ ts.formatFloat = function(s, table) {
1638
+ if (typeof s !== 'string' || s === '') { return s; }
1639
+ // allow using formatFloat without a table; defaults to US number format
1640
+ var i,
1641
+ t = table && table.config ? table.config.usNumberFormat !== false :
1642
+ typeof table !== "undefined" ? table : true;
1643
+ if (t) {
1644
+ // US Format - 1,234,567.89 -> 1234567.89
1645
+ s = s.replace(/,/g,'');
1646
+ } else {
1647
+ // German Format = 1.234.567,89 -> 1234567.89
1648
+ // French Format = 1 234 567,89 -> 1234567.89
1649
+ s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.');
1650
+ }
1651
+ if(/^\s*\([.\d]+\)/.test(s)) {
1652
+ // make (#) into a negative number -> (10) = -10
1653
+ s = s.replace(/^\s*\(([.\d]+)\)/, '-$1');
1654
+ }
1655
+ i = parseFloat(s);
1656
+ // return the text instead of zero
1657
+ return isNaN(i) ? $.trim(s) : i;
1658
+ };
1659
+
1660
+ ts.isDigit = function(s) {
1661
+ // replace all unwanted chars and match
1662
+ return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'"\s]/g, '')) : true;
1663
+ };
1664
+
1665
+ }()
1666
+ });
1667
+
1668
+ // make shortcut
1669
+ var ts = $.tablespy;
1670
+
1671
+ // extend plugin scope
1672
+ $.fn.extend({
1673
+ tablespy: ts.construct
1674
+ });
1675
+
1676
+ // add default parsers
1677
+ ts.addParser({
1678
+ id: 'no-parser',
1679
+ is: function() {
1680
+ return false;
1681
+ },
1682
+ format: function() {
1683
+ return '';
1684
+ },
1685
+ type: 'text'
1686
+ });
1687
+
1688
+ ts.addParser({
1689
+ id: "text",
1690
+ is: function() {
1691
+ return true;
1692
+ },
1693
+ format: function(s, table) {
1694
+ var c = table.config;
1695
+ if (s) {
1696
+ s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s );
1697
+ s = c.sortLocaleCompare ? ts.replaceAccents(s) : s;
1698
+ }
1699
+ return s;
1700
+ },
1701
+ type: "text"
1702
+ });
1703
+
1704
+ ts.addParser({
1705
+ id: "digit",
1706
+ is: function(s) {
1707
+ return ts.isDigit(s);
1708
+ },
1709
+ format: function(s, table) {
1710
+ var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ""), table);
1711
+ return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
1712
+ },
1713
+ type: "numeric"
1714
+ });
1715
+
1716
+ ts.addParser({
1717
+ id: "currency",
1718
+ is: function(s) {
1719
+ return (/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/).test((s || '').replace(/[+\-,. ]/g,'')); // £$€¤¥¢
1720
+ },
1721
+ format: function(s, table) {
1722
+ var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ""), table);
1723
+ return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s;
1724
+ },
1725
+ type: "numeric"
1726
+ });
1727
+
1728
+ ts.addParser({
1729
+ id: "ipAddress",
1730
+ is: function(s) {
1731
+ return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s);
1732
+ },
1733
+ format: function(s, table) {
1734
+ var i, a = s ? s.split(".") : '',
1735
+ r = "",
1736
+ l = a.length;
1737
+ for (i = 0; i < l; i++) {
1738
+ r += ("00" + a[i]).slice(-3);
1739
+ }
1740
+ return s ? ts.formatFloat(r, table) : s;
1741
+ },
1742
+ type: "numeric"
1743
+ });
1744
+
1745
+ ts.addParser({
1746
+ id: "url",
1747
+ is: function(s) {
1748
+ return (/^(https?|ftp|file):\/\//).test(s);
1749
+ },
1750
+ format: function(s) {
1751
+ return s ? $.trim(s.replace(/(https?|ftp|file):\/\//, '')) : s;
1752
+ },
1753
+ parsed : true, // filter widget flag
1754
+ type: "text"
1755
+ });
1756
+
1757
+ ts.addParser({
1758
+ id: "isoDate",
1759
+ is: function(s) {
1760
+ return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/).test(s);
1761
+ },
1762
+ format: function(s, table) {
1763
+ return s ? ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || s) : "", table) : s;
1764
+ },
1765
+ type: "numeric"
1766
+ });
1767
+
1768
+ ts.addParser({
1769
+ id: "percent",
1770
+ is: function(s) {
1771
+ return (/(\d\s*?%|%\s*?\d)/).test(s) && s.length < 15;
1772
+ },
1773
+ format: function(s, table) {
1774
+ return s ? ts.formatFloat(s.replace(/%/g, ""), table) : s;
1775
+ },
1776
+ type: "numeric"
1777
+ });
1778
+
1779
+ ts.addParser({
1780
+ id: "usLongDate",
1781
+ is: function(s) {
1782
+ // two digit years are not allowed cross-browser
1783
+ // Jan 01, 2013 12:34:56 PM or 01 Jan 2013
1784
+ return (/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i).test(s) || (/^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i).test(s);
1785
+ },
1786
+ format: function(s, table) {
1787
+ return s ? ts.formatFloat( (new Date(s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || s), table) : s;
1788
+ },
1789
+ type: "numeric"
1790
+ });
1791
+
1792
+ ts.addParser({
1793
+ id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd"
1794
+ is: function(s) {
1795
+ // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included
1796
+ return (/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/).test((s || '').replace(/\s+/g," ").replace(/[\-.,]/g, "/"));
1797
+ },
1798
+ format: function(s, table, cell, cellIndex) {
1799
+ if (s) {
1800
+ var c = table.config,
1801
+ ci = c.$headers.filter('[data-column=' + cellIndex + ']:last'),
1802
+ format = ci.length && ci[0].dateFormat || ts.getData( ci, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat') || c.dateFormat;
1803
+ s = s.replace(/\s+/g," ").replace(/[\-.,]/g, "/"); // escaped - because JSHint in Firefox was showing it as an error
1804
+ if (format === "mmddyyyy") {
1805
+ s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2");
1806
+ } else if (format === "ddmmyyyy") {
1807
+ s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1");
1808
+ } else if (format === "yyyymmdd") {
1809
+ s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3");
1810
+ }
1811
+ }
1812
+ return s ? ts.formatFloat( (new Date(s).getTime() || s), table) : s;
1813
+ },
1814
+ type: "numeric"
1815
+ });
1816
+
1817
+ ts.addParser({
1818
+ id: "time",
1819
+ is: function(s) {
1820
+ return (/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s);
1821
+ },
1822
+ format: function(s, table) {
1823
+ return s ? ts.formatFloat( (new Date("2000/01/01 " + s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || s), table) : s;
1824
+ },
1825
+ type: "numeric"
1826
+ });
1827
+
1828
+ ts.addParser({
1829
+ id: "metadata",
1830
+ is: function() {
1831
+ return false;
1832
+ },
1833
+ format: function(s, table, cell) {
1834
+ var c = table.config,
1835
+ p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
1836
+ return $(cell).metadata()[p];
1837
+ },
1838
+ type: "numeric"
1839
+ });
1840
+
1841
+ // add default widgets
1842
+ ts.addWidget({
1843
+ id: "zebra",
1844
+ priority: 90,
1845
+ format: function(table, c, wo) {
1846
+ var $tb, $tv, $tr, row, even, time, k,
1847
+ child = new RegExp(c.cssChildRow, 'i'),
1848
+ b = c.$tbodies;
1849
+ if (c.debug) {
1850
+ time = new Date();
1851
+ }
1852
+ for (k = 0; k < b.length; k++ ) {
1853
+ // loop through the visible rows
1854
+ row = 0;
1855
+ $tb = b.eq(k);
1856
+ $tv = $tb.children('tr:visible').not(c.selectorRemove);
1857
+ // revered back to using jQuery each - strangely it's the fastest method
1858
+ /*jshint loopfunc:true */
1859
+ $tv.each(function(){
1860
+ $tr = $(this);
1861
+ // style child rows the same way the parent row was styled
1862
+ if (!child.test(this.className)) { row++; }
1863
+ even = (row % 2 === 0);
1864
+ $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]);
1865
+ });
1866
+ }
1867
+ if (c.debug) {
1868
+ ts.benchmark("Applying Zebra widget", time);
1869
+ }
1870
+ },
1871
+ remove: function(table, c, wo){
1872
+ var k, $tb,
1873
+ b = c.$tbodies,
1874
+ rmv = (wo.zebra || [ "even", "odd" ]).join(' ');
1875
+ for (k = 0; k < b.length; k++ ){
1876
+ $tb = $.tablespy.processTbody(table, b.eq(k), true); // remove tbody
1877
+ $tb.children().removeClass(rmv);
1878
+ $.tablespy.processTbody(table, $tb, false); // restore tbody
1879
+ }
1880
+ }
1881
+ });
1882
+
1883
+ })(jQuery);