jquery-dynatable-rails 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a70a9990943bbe77b4470160766681599a4ff486
4
- data.tar.gz: f045ccf4e84129ddfe4da3393de47e8c02b99768
3
+ metadata.gz: b6e2fa52f5c8abbb77cada9fe2b076b82d071908
4
+ data.tar.gz: f8ef2f1d0139ade6d88e07c7f806c12d504c4b20
5
5
  SHA512:
6
- metadata.gz: 4e2fad37ba5c03f940f62338392a6d825437469038141bd744faf28d1bee042c7eb302fa28bc55badc48f111343324802b8f6a2e40e0aea88a8430ccc27d655b
7
- data.tar.gz: a65a16201b8ff5912dc2691cad6e11cb7a594e9a2c3d47730d881602905a603ab9d059acaeb7654c37b5058443679a951a69db67af18bced24d546d616365445
6
+ metadata.gz: e23d4686145ee95fa3ae64cbd1c41952b0554be2fca8c2e6f75febc6f873141181f1024f490f0b681fcac6974810d3110c022e1213c2c31a7ca552a9d60edaa9
7
+ data.tar.gz: 23fba218fb0113acfebc5192ef1dd0465210ba783b36e966d4541329d6ff9a9f770598d39a5c3e583e4b5ad62f6b0e6182cbedf69ac5932bc4c2eac22c4a6502
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Jquery::Dynatable::Rails
2
2
 
3
- TODO: Write a gem description
3
+ This gem packages the jQuery [Dynatable](https://github.com/alfajango/jquery-dynatable)
4
+ plugin for easy use with the Rails 3.1+ asset pipleine.
5
+
6
+ Dynatable is a funner, semantic, interactive table plugin using jQuery,
7
+ HTML5, and JSON. And it's not just for tables.
4
8
 
5
9
  ## Installation
6
10
 
@@ -18,11 +22,23 @@ Or install it yourself as:
18
22
 
19
23
  ## Usage
20
24
 
21
- TODO: Write usage instructions here
25
+ /= require 'jquery.dynatable'
26
+
27
+
28
+ ## Thanks
29
+ Thanks the authors for making the [Dynatable](https://github.com/alfajango/jquery-dynatable) plugin for jQuery:
30
+
31
+ Steve Schwartz -
32
+ [JangoSteve on Github](https://github.com/JangoSteve),
33
+ [@jangosteve on Twitter](https://twitter.com/jangosteve)
34
+
35
+ ![Alfa Jango logo](https://s3.amazonaws.com/s3.alfajango.com/github-readmes/AlfaJango_Logo_Black_noname-tiny.png)
36
+ [Alfa Jango Open Source](http://os.alfajango.com) -
37
+ [@alfajango on Twitter](https://twitter.com/alfajango)
22
38
 
23
39
  ## Contributing
24
40
 
25
- 1. Fork it ( http://github.com/<my-github-username>/jquery-dynatable-rails/fork )
41
+ 1. Fork it ( http://github.com/ricardodovalle/jquery-dynatable-rails/fork )
26
42
  2. Create your feature branch (`git checkout -b my-new-feature`)
27
43
  3. Commit your changes (`git commit -am 'Add some feature'`)
28
44
  4. Push to the branch (`git push origin my-new-feature`)
@@ -20,8 +20,10 @@ Gem::Specification.new do |spec|
20
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.add_development_dependency 'bundler', '~> 1.5'
24
- spec.add_development_dependency 'rake', '~> 0'
23
+ spec.add_development_dependency 'bundler'
24
+ spec.add_development_dependency 'rake'
25
+
26
+ spec.add_dependency 'jquery-rails'
27
+ spec.add_dependency "railties", ">= 3.2", "< 5.0"
25
28
 
26
- spec.add_dependency 'jquery-rails', '~> 0'
27
29
  end
@@ -3,7 +3,8 @@ require "jquery/dynatable/rails/version"
3
3
  module Jquery
4
4
  module Dynatable
5
5
  module Rails
6
- # Your code goes here...
6
+ class Engine < ::Rails::Engine
7
+ end
7
8
  end
8
9
  end
9
10
  end
@@ -1,7 +1,7 @@
1
1
  module Jquery
2
2
  module Dynatable
3
3
  module Rails
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.2"
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,1681 @@
1
+ /*
2
+ * jQuery Dynatable plugin 0.3.1
3
+ *
4
+ * Copyright (c) 2014 Steve Schwartz (JangoSteve)
5
+ *
6
+ * Dual licensed under the AGPL and Proprietary licenses:
7
+ * http://www.dynatable.com/license/
8
+ *
9
+ * Date: Tue Jan 02 2014
10
+ */
11
+ //
12
+
13
+ (function($) {
14
+ var defaults,
15
+ mergeSettings,
16
+ dt,
17
+ Model,
18
+ modelPrototypes = {
19
+ dom: Dom,
20
+ domColumns: DomColumns,
21
+ records: Records,
22
+ recordsCount: RecordsCount,
23
+ processingIndicator: ProcessingIndicator,
24
+ state: State,
25
+ sorts: Sorts,
26
+ sortsHeaders: SortsHeaders,
27
+ queries: Queries,
28
+ inputsSearch: InputsSearch,
29
+ paginationPage: PaginationPage,
30
+ paginationPerPage: PaginationPerPage,
31
+ paginationLinks: PaginationLinks
32
+ },
33
+ utility,
34
+ build,
35
+ processAll,
36
+ initModel,
37
+ defaultRowWriter,
38
+ defaultCellWriter,
39
+ defaultAttributeWriter,
40
+ defaultAttributeReader;
41
+
42
+ //-----------------------------------------------------------------
43
+ // Cached plugin global defaults
44
+ //-----------------------------------------------------------------
45
+
46
+ defaults = {
47
+ features: {
48
+ paginate: true,
49
+ sort: true,
50
+ pushState: true,
51
+ search: true,
52
+ recordCount: true,
53
+ perPageSelect: true
54
+ },
55
+ table: {
56
+ defaultColumnIdStyle: 'camelCase',
57
+ columns: null,
58
+ headRowSelector: 'thead tr', // or e.g. tr:first-child
59
+ bodyRowSelector: 'tbody tr',
60
+ headRowClass: null
61
+ },
62
+ inputs: {
63
+ queries: null,
64
+ sorts: null,
65
+ multisort: ['ctrlKey', 'shiftKey', 'metaKey'],
66
+ page: null,
67
+ queryEvent: 'blur change',
68
+ recordCountTarget: null,
69
+ recordCountPlacement: 'after',
70
+ paginationLinkTarget: null,
71
+ paginationLinkPlacement: 'after',
72
+ paginationClass: 'dynatable-pagination-links',
73
+ paginationLinkClass: 'dynatable-page-link',
74
+ paginationPrevClass: 'dynatable-page-prev',
75
+ paginationNextClass: 'dynatable-page-next',
76
+ paginationActiveClass: 'dynatable-active-page',
77
+ paginationDisabledClass: 'dynatable-disabled-page',
78
+ paginationPrev: 'Previous',
79
+ paginationNext: 'Next',
80
+ paginationGap: [1,2,2,1],
81
+ searchTarget: null,
82
+ searchPlacement: 'before',
83
+ perPageTarget: null,
84
+ perPagePlacement: 'before',
85
+ perPageText: 'Show: ',
86
+ recordCountText: 'Showing ',
87
+ processingText: 'Processing...'
88
+ },
89
+ dataset: {
90
+ ajax: false,
91
+ ajaxUrl: null,
92
+ ajaxCache: null,
93
+ ajaxOnLoad: false,
94
+ ajaxMethod: 'GET',
95
+ ajaxDataType: 'json',
96
+ totalRecordCount: null,
97
+ queries: {},
98
+ queryRecordCount: null,
99
+ page: null,
100
+ perPageDefault: 10,
101
+ perPageOptions: [10,20,50,100],
102
+ sorts: {},
103
+ sortsKeys: null,
104
+ sortTypes: {},
105
+ records: null
106
+ },
107
+ writers: {
108
+ _rowWriter: defaultRowWriter,
109
+ _cellWriter: defaultCellWriter,
110
+ _attributeWriter: defaultAttributeWriter
111
+ },
112
+ readers: {
113
+ _rowReader: null,
114
+ _attributeReader: defaultAttributeReader
115
+ },
116
+ params: {
117
+ dynatable: 'dynatable',
118
+ queries: 'queries',
119
+ sorts: 'sorts',
120
+ page: 'page',
121
+ perPage: 'perPage',
122
+ offset: 'offset',
123
+ records: 'records',
124
+ record: null,
125
+ queryRecordCount: 'queryRecordCount',
126
+ totalRecordCount: 'totalRecordCount'
127
+ }
128
+ };
129
+
130
+ //-----------------------------------------------------------------
131
+ // Each dynatable instance inherits from this,
132
+ // set properties specific to instance
133
+ //-----------------------------------------------------------------
134
+
135
+ dt = {
136
+ init: function(element, options) {
137
+ this.settings = mergeSettings(options);
138
+ this.element = element;
139
+ this.$element = $(element);
140
+
141
+ // All the setup that doesn't require element or options
142
+ build.call(this);
143
+
144
+ return this;
145
+ },
146
+
147
+ process: function(skipPushState) {
148
+ processAll.call(this, skipPushState);
149
+ }
150
+ };
151
+
152
+ //-----------------------------------------------------------------
153
+ // Cached plugin global functions
154
+ //-----------------------------------------------------------------
155
+
156
+ mergeSettings = function(options) {
157
+ var newOptions = $.extend(true, {}, defaults, options);
158
+
159
+ // TODO: figure out a better way to do this.
160
+ // Doing `extend(true)` causes any elements that are arrays
161
+ // to merge the default and options arrays instead of overriding the defaults.
162
+ if (options) {
163
+ if (options.inputs) {
164
+ if (options.inputs.multisort) {
165
+ newOptions.inputs.multisort = options.inputs.multisort;
166
+ }
167
+ if (options.inputs.paginationGap) {
168
+ newOptions.inputs.paginationGap = options.inputs.paginationGap;
169
+ }
170
+ }
171
+ if (options.dataset && options.dataset.perPageOptions) {
172
+ newOptions.dataset.perPageOptions = options.dataset.perPageOptions;
173
+ }
174
+ }
175
+
176
+ return newOptions;
177
+ };
178
+
179
+ build = function() {
180
+ this.$element.trigger('dynatable:preinit', this);
181
+
182
+ for (model in modelPrototypes) {
183
+ if (modelPrototypes.hasOwnProperty(model)) {
184
+ var modelInstance = this[model] = new modelPrototypes[model](this, this.settings);
185
+ if (modelInstance.initOnLoad()) {
186
+ modelInstance.init();
187
+ }
188
+ }
189
+ }
190
+
191
+ this.$element.trigger('dynatable:init', this);
192
+
193
+ if (!this.settings.dataset.ajax || (this.settings.dataset.ajax && this.settings.dataset.ajaxOnLoad) || this.settings.features.paginate) {
194
+ this.process();
195
+ }
196
+ };
197
+
198
+ processAll = function(skipPushState) {
199
+ var data = {};
200
+
201
+ this.$element.trigger('dynatable:beforeProcess', data);
202
+
203
+ if (!$.isEmptyObject(this.settings.dataset.queries)) { data[this.settings.params.queries] = this.settings.dataset.queries; }
204
+ // TODO: Wrap this in a try/rescue block to hide the processing indicator and indicate something went wrong if error
205
+ this.processingIndicator.show();
206
+
207
+ if (this.settings.features.sort && !$.isEmptyObject(this.settings.dataset.sorts)) { data[this.settings.params.sorts] = this.settings.dataset.sorts; }
208
+ if (this.settings.features.paginate && this.settings.dataset.page) {
209
+ var page = this.settings.dataset.page,
210
+ perPage = this.settings.dataset.perPage;
211
+ data[this.settings.params.page] = page;
212
+ data[this.settings.params.perPage] = perPage;
213
+ data[this.settings.params.offset] = (page - 1) * perPage;
214
+ }
215
+ if (this.settings.dataset.ajaxData) { $.extend(data, this.settings.dataset.ajaxData); }
216
+
217
+ // If ajax, sends query to ajaxUrl with queries and sorts serialized and appended in ajax data
218
+ // otherwise, executes queries and sorts on in-page data
219
+ if (this.settings.dataset.ajax) {
220
+ var _this = this;
221
+ var options = {
222
+ type: _this.settings.dataset.ajaxMethod,
223
+ dataType: _this.settings.dataset.ajaxDataType,
224
+ data: data,
225
+ error: function(xhr, error) {
226
+ },
227
+ success: function(response) {
228
+ _this.$element.trigger('dynatable:ajax:success', response);
229
+ // Merge ajax results and meta-data into dynatables cached data
230
+ _this.records.updateFromJson(response);
231
+ // update table with new records
232
+ _this.dom.update();
233
+
234
+ if (!skipPushState && _this.state.initOnLoad()) {
235
+ _this.state.push(data);
236
+ }
237
+ },
238
+ complete: function() {
239
+ _this.processingIndicator.hide();
240
+ }
241
+ };
242
+ // Do not pass url to `ajax` options if blank
243
+ if (this.settings.dataset.ajaxUrl) {
244
+ options.url = this.settings.dataset.ajaxUrl;
245
+
246
+ // If ajaxUrl is blank, then we're using the current page URL,
247
+ // we need to strip out any query, sort, or page data controlled by dynatable
248
+ // that may have been in URL when page loaded, so that it doesn't conflict with
249
+ // what's passed in with the data ajax parameter
250
+ } else {
251
+ options.url = utility.refreshQueryString(window.location.href, {}, this.settings);
252
+ }
253
+ if (this.settings.dataset.ajaxCache !== null) { options.cache = this.settings.dataset.ajaxCache; }
254
+
255
+ $.ajax(options);
256
+ } else {
257
+ this.records.resetOriginal();
258
+ this.queries.run();
259
+ if (this.settings.features.sort) {
260
+ this.records.sort();
261
+ }
262
+ if (this.settings.features.paginate) {
263
+ this.records.paginate();
264
+ }
265
+ this.dom.update();
266
+ this.processingIndicator.hide();
267
+
268
+ if (!skipPushState && this.state.initOnLoad()) {
269
+ this.state.push(data);
270
+ }
271
+ }
272
+ this.$element.trigger('dynatable:afterProcess', data);
273
+ };
274
+
275
+ function defaultRowWriter(rowIndex, record, columns, cellWriter) {
276
+ var tr = '';
277
+
278
+ // grab the record's attribute for each column
279
+ for (var i = 0, len = columns.length; i < len; i++) {
280
+ tr += cellWriter(columns[i], record);
281
+ }
282
+
283
+ return '<tr>' + tr + '</tr>';
284
+ };
285
+
286
+ function defaultCellWriter(column, record) {
287
+ var html = column.attributeWriter(record),
288
+ td = '<td';
289
+
290
+ if (column.hidden || column.textAlign) {
291
+ td += ' style="';
292
+
293
+ // keep cells for hidden column headers hidden
294
+ if (column.hidden) {
295
+ td += 'display: none;';
296
+ }
297
+
298
+ // keep cells aligned as their column headers are aligned
299
+ if (column.textAlign) {
300
+ td += 'text-align: ' + column.textAlign + ';';
301
+ }
302
+
303
+ td += '"';
304
+ }
305
+
306
+ return td + '>' + html + '</td>';
307
+ };
308
+
309
+ function defaultAttributeWriter(record) {
310
+ // `this` is the column object in settings.columns
311
+ // TODO: automatically convert common types, such as arrays and objects, to string
312
+ return record[this.id];
313
+ };
314
+
315
+ function defaultAttributeReader(cell, record) {
316
+ return $(cell).html();
317
+ };
318
+
319
+ //-----------------------------------------------------------------
320
+ // Dynatable object model prototype
321
+ // (all object models get these default functions)
322
+ //-----------------------------------------------------------------
323
+
324
+ Model = {
325
+ initOnLoad: function() {
326
+ return true;
327
+ },
328
+
329
+ init: function() {}
330
+ };
331
+
332
+ for (model in modelPrototypes) {
333
+ if (modelPrototypes.hasOwnProperty(model)) {
334
+ var modelPrototype = modelPrototypes[model];
335
+ modelPrototype.prototype = Model;
336
+ }
337
+ }
338
+
339
+ //-----------------------------------------------------------------
340
+ // Dynatable object models
341
+ //-----------------------------------------------------------------
342
+
343
+ function Dom(obj, settings) {
344
+ var _this = this;
345
+
346
+ // update table contents with new records array
347
+ // from query (whether ajax or not)
348
+ this.update = function() {
349
+ var rows = '',
350
+ columns = settings.table.columns,
351
+ rowWriter = settings.writers._rowWriter,
352
+ cellWriter = settings.writers._cellWriter;
353
+
354
+ obj.$element.trigger('dynatable:beforeUpdate', rows);
355
+
356
+ // loop through records
357
+ for (var i = 0, len = settings.dataset.records.length; i < len; i++) {
358
+ var record = settings.dataset.records[i],
359
+ tr = rowWriter(i, record, columns, cellWriter);
360
+ rows += tr;
361
+ }
362
+
363
+ // Appended dynatable interactive elements
364
+ if (settings.features.recordCount) {
365
+ $('#dynatable-record-count-' + obj.element.id).replaceWith(obj.recordsCount.create());
366
+ }
367
+ if (settings.features.paginate) {
368
+ $('#dynatable-pagination-links-' + obj.element.id).replaceWith(obj.paginationLinks.create());
369
+ if (settings.features.perPageSelect) {
370
+ $('#dynatable-per-page-' + obj.element.id).val(parseInt(settings.dataset.perPage));
371
+ }
372
+ }
373
+
374
+ // Sort headers functionality
375
+ if (settings.features.sort && columns) {
376
+ obj.sortsHeaders.removeAllArrows();
377
+ for (var i = 0, len = columns.length; i < len; i++) {
378
+ var column = columns[i],
379
+ sortedByColumn = utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; }),
380
+ value = settings.dataset.sorts[column.sorts[0]];
381
+
382
+ if (sortedByColumn) {
383
+ obj.$element.find('[data-dynatable-column="' + column.id + '"]').find('.dynatable-sort-header').each(function(){
384
+ if (value == 1) {
385
+ obj.sortsHeaders.appendArrowUp($(this));
386
+ } else {
387
+ obj.sortsHeaders.appendArrowDown($(this));
388
+ }
389
+ });
390
+ }
391
+ }
392
+ }
393
+
394
+ // Query search functionality
395
+ if (settings.inputs.queries || settings.features.search) {
396
+ var allQueries = settings.inputs.queries || $();
397
+ if (settings.features.search) {
398
+ allQueries = allQueries.add('#dynatable-query-search-' + obj.element.id);
399
+ }
400
+
401
+ allQueries.each(function() {
402
+ var $this = $(this),
403
+ q = settings.dataset.queries[$this.data('dynatable-query')];
404
+ $this.val(q || '');
405
+ });
406
+ }
407
+
408
+ obj.$element.find(settings.table.bodyRowSelector).remove();
409
+ obj.$element.append(rows);
410
+
411
+ obj.$element.trigger('dynatable:afterUpdate', rows);
412
+ };
413
+ };
414
+
415
+ function DomColumns(obj, settings) {
416
+ var _this = this;
417
+
418
+ this.initOnLoad = function() {
419
+ return obj.$element.is('table');
420
+ };
421
+
422
+ this.init = function() {
423
+ settings.table.columns = [];
424
+ this.getFromTable();
425
+ };
426
+
427
+ // initialize table[columns] array
428
+ this.getFromTable = function() {
429
+ var $columns = obj.$element.find(settings.table.headRowSelector).children('th,td');
430
+ if ($columns.length) {
431
+ $columns.each(function(index){
432
+ _this.add($(this), index, true);
433
+ });
434
+ } else {
435
+ return $.error("Couldn't find any columns headers in '" + settings.table.headRowSelector + " th,td'. If your header row is different, specify the selector in the table: headRowSelector option.");
436
+ }
437
+ };
438
+
439
+ this.add = function($column, position, skipAppend, skipUpdate) {
440
+ var columns = settings.table.columns,
441
+ label = $column.text(),
442
+ id = $column.data('dynatable-column') || utility.normalizeText(label, settings.table.defaultColumnIdStyle),
443
+ dataSorts = $column.data('dynatable-sorts'),
444
+ sorts = dataSorts ? $.map(dataSorts.split(','), function(text) { return $.trim(text); }) : [id];
445
+
446
+ // If the column id is blank, generate an id for it
447
+ if ( !id ) {
448
+ this.generate($column);
449
+ id = $column.data('dynatable-column');
450
+ }
451
+ // Add column data to plugin instance
452
+ columns.splice(position, 0, {
453
+ index: position,
454
+ label: label,
455
+ id: id,
456
+ attributeWriter: settings.writers[id] || settings.writers._attributeWriter,
457
+ attributeReader: settings.readers[id] || settings.readers._attributeReader,
458
+ sorts: sorts,
459
+ hidden: $column.css('display') === 'none',
460
+ textAlign: $column.css('text-align')
461
+ });
462
+
463
+ // Modify header cell
464
+ $column
465
+ .attr('data-dynatable-column', id)
466
+ .addClass('dynatable-head');
467
+ if (settings.table.headRowClass) { $column.addClass(settings.table.headRowClass); }
468
+
469
+ // Append column header to table
470
+ if (!skipAppend) {
471
+ var domPosition = position + 1,
472
+ $sibling = obj.$element.find(settings.table.headRowSelector)
473
+ .children('th:nth-child(' + domPosition + '),td:nth-child(' + domPosition + ')').first(),
474
+ columnsAfter = columns.slice(position + 1, columns.length);
475
+
476
+ if ($sibling.length) {
477
+ $sibling.before($column);
478
+ // sibling column doesn't yet exist (maybe this is the last column in the header row)
479
+ } else {
480
+ obj.$element.find(settings.table.headRowSelector).append($column);
481
+ }
482
+
483
+ obj.sortsHeaders.attachOne($column.get());
484
+
485
+ // increment the index of all columns after this one that was just inserted
486
+ if (columnsAfter.length) {
487
+ for (var i = 0, len = columnsAfter.length; i < len; i++) {
488
+ columnsAfter[i].index += 1;
489
+ }
490
+ }
491
+
492
+ if (!skipUpdate) {
493
+ obj.dom.update();
494
+ }
495
+ }
496
+
497
+ return dt;
498
+ };
499
+
500
+ this.remove = function(columnIndexOrId) {
501
+ var columns = settings.table.columns,
502
+ length = columns.length;
503
+
504
+ if (typeof(columnIndexOrId) === "number") {
505
+ var column = columns[columnIndexOrId];
506
+ this.removeFromTable(column.id);
507
+ this.removeFromArray(columnIndexOrId);
508
+ } else {
509
+ // Traverse columns array in reverse order so that subsequent indices
510
+ // don't get messed up when we delete an item from the array in an iteration
511
+ for (var i = columns.length - 1; i >= 0; i--) {
512
+ var column = columns[i];
513
+
514
+ if (column.id === columnIndexOrId) {
515
+ this.removeFromTable(columnIndexOrId);
516
+ this.removeFromArray(i);
517
+ }
518
+ }
519
+ }
520
+
521
+ obj.dom.update();
522
+ };
523
+
524
+ this.removeFromTable = function(columnId) {
525
+ obj.$element.find(settings.table.headRowSelector).children('[data-dynatable-column="' + columnId + '"]').first()
526
+ .remove();
527
+ };
528
+
529
+ this.removeFromArray = function(index) {
530
+ var columns = settings.table.columns,
531
+ adjustColumns;
532
+ columns.splice(index, 1);
533
+ adjustColumns = columns.slice(index, columns.length);
534
+ for (var i = 0, len = adjustColumns.length; i < len; i++) {
535
+ adjustColumns[i].index -= 1;
536
+ }
537
+ };
538
+
539
+ this.generate = function($cell) {
540
+ var cell = $cell === undefined ? $('<th></th>') : $cell;
541
+ return this.attachGeneratedAttributes(cell);
542
+ };
543
+
544
+ this.attachGeneratedAttributes = function($cell) {
545
+ // Use increment to create unique column name that is the same each time the page is reloaded,
546
+ // in order to avoid errors with mismatched attribute names when loading cached `dataset.records` array
547
+ var increment = obj.$element.find(settings.table.headRowSelector).children('th[data-dynatable-generated]').length;
548
+ return $cell
549
+ .attr('data-dynatable-column', 'dynatable-generated-' + increment) //+ utility.randomHash(),
550
+ .attr('data-dynatable-no-sort', 'true')
551
+ .attr('data-dynatable-generated', increment);
552
+ };
553
+ };
554
+
555
+ function Records(obj, settings) {
556
+ var _this = this;
557
+
558
+ this.initOnLoad = function() {
559
+ return !settings.dataset.ajax;
560
+ };
561
+
562
+ this.init = function() {
563
+ if (settings.dataset.records === null) {
564
+ settings.dataset.records = this.getFromTable();
565
+
566
+ if (!settings.dataset.queryRecordCount) {
567
+ settings.dataset.queryRecordCount = this.count();
568
+ }
569
+
570
+ if (!settings.dataset.totalRecordCount){
571
+ settings.dataset.totalRecordCount = settings.dataset.queryRecordCount;
572
+ }
573
+ }
574
+
575
+ // Create cache of original full recordset (unpaginated and unqueried)
576
+ settings.dataset.originalRecords = $.extend(true, [], settings.dataset.records);
577
+ };
578
+
579
+ // merge ajax response json with cached data including
580
+ // meta-data and records
581
+ this.updateFromJson = function(data) {
582
+ var records;
583
+ if (settings.params.records === "_root") {
584
+ records = data;
585
+ } else if (settings.params.records in data) {
586
+ records = data[settings.params.records];
587
+ }
588
+ if (settings.params.record) {
589
+ var len = records.length - 1;
590
+ for (var i = 0; i < len; i++) {
591
+ records[i] = records[i][settings.params.record];
592
+ }
593
+ }
594
+ if (settings.params.queryRecordCount in data) {
595
+ settings.dataset.queryRecordCount = data[settings.params.queryRecordCount];
596
+ }
597
+ if (settings.params.totalRecordCount in data) {
598
+ settings.dataset.totalRecordCount = data[settings.params.totalRecordCount];
599
+ }
600
+ settings.dataset.records = records;
601
+ };
602
+
603
+ // For really advanced sorting,
604
+ // see http://james.padolsey.com/javascript/sorting-elements-with-jquery/
605
+ this.sort = function() {
606
+ var sort = [].sort,
607
+ sorts = settings.dataset.sorts,
608
+ sortsKeys = settings.dataset.sortsKeys,
609
+ sortTypes = settings.dataset.sortTypes;
610
+
611
+ var sortFunction = function(a, b) {
612
+ var comparison;
613
+ if ($.isEmptyObject(sorts)) {
614
+ comparison = obj.sorts.functions['originalPlacement'](a, b);
615
+ } else {
616
+ for (var i = 0, len = sortsKeys.length; i < len; i++) {
617
+ var attr = sortsKeys[i],
618
+ direction = sorts[attr],
619
+ sortType = sortTypes[attr] || obj.sorts.guessType(a, b, attr);
620
+ comparison = obj.sorts.functions[sortType](a, b, attr, direction);
621
+ // Don't need to sort any further unless this sort is a tie between a and b,
622
+ // so break the for loop unless tied
623
+ if (comparison !== 0) { break; }
624
+ }
625
+ }
626
+ return comparison;
627
+ }
628
+
629
+ return sort.call(settings.dataset.records, sortFunction);
630
+ };
631
+
632
+ this.paginate = function() {
633
+ var bounds = this.pageBounds(),
634
+ first = bounds[0], last = bounds[1];
635
+ settings.dataset.records = settings.dataset.records.slice(first, last);
636
+ };
637
+
638
+ this.resetOriginal = function() {
639
+ settings.dataset.records = settings.dataset.originalRecords || [];
640
+ };
641
+
642
+ this.pageBounds = function() {
643
+ var page = settings.dataset.page || 1,
644
+ first = (page - 1) * settings.dataset.perPage,
645
+ last = Math.min(first + settings.dataset.perPage, settings.dataset.queryRecordCount);
646
+ return [first,last];
647
+ };
648
+
649
+ // get initial recordset to populate table
650
+ // if ajax, call ajaxUrl
651
+ // otherwise, initialize from in-table records
652
+ this.getFromTable = function() {
653
+ var records = [],
654
+ columns = settings.table.columns,
655
+ tableRecords = obj.$element.find(settings.table.bodyRowSelector);
656
+
657
+ tableRecords.each(function(index){
658
+ var record = {};
659
+ record['dynatable-original-index'] = index;
660
+ $(this).find('th,td').each(function(index) {
661
+ if (columns[index] === undefined) {
662
+ // Header cell didn't exist for this column, so let's generate and append
663
+ // a new header cell with a randomly generated name (so we can store and
664
+ // retrieve the contents of this column for each record)
665
+ obj.domColumns.add(obj.domColumns.generate(), columns.length, false, true); // don't skipAppend, do skipUpdate
666
+ }
667
+ var value = columns[index].attributeReader(this, record),
668
+ attr = columns[index].id;
669
+
670
+ // If value from table is HTML, let's get and cache the text equivalent for
671
+ // the default string sorting, since it rarely makes sense for sort headers
672
+ // to sort based on HTML tags.
673
+ if (typeof(value) === "string" && value.match(/\s*\<.+\>/)) {
674
+ if (! record['dynatable-sortable-text']) {
675
+ record['dynatable-sortable-text'] = {};
676
+ }
677
+ record['dynatable-sortable-text'][attr] = $.trim($('<div></div>').html(value).text());
678
+ }
679
+
680
+ record[attr] = value;
681
+ });
682
+ // Allow configuration function which alters record based on attributes of
683
+ // table row (e.g. from html5 data- attributes)
684
+ if (typeof(settings.readers._rowReader) === "function") {
685
+ settings.readers._rowReader(index, this, record);
686
+ }
687
+ records.push(record);
688
+ });
689
+ return records; // 1st row is header
690
+ };
691
+
692
+ // count records from table
693
+ this.count = function() {
694
+ return settings.dataset.records.length;
695
+ };
696
+ };
697
+
698
+ function RecordsCount(obj, settings) {
699
+ this.initOnLoad = function() {
700
+ return settings.features.recordCount;
701
+ };
702
+
703
+ this.init = function() {
704
+ this.attach();
705
+ };
706
+
707
+ this.create = function() {
708
+ var recordsShown = obj.records.count(),
709
+ recordsQueryCount = settings.dataset.queryRecordCount,
710
+ recordsTotal = settings.dataset.totalRecordCount,
711
+ text = settings.inputs.recordCountText,
712
+ collection_name = settings.params.records;
713
+
714
+ if (recordsShown < recordsQueryCount && settings.features.paginate) {
715
+ var bounds = obj.records.pageBounds();
716
+ text += "<span class='dynatable-record-bounds'>" + (bounds[0] + 1) + " to " + bounds[1] + "</span> of ";
717
+ } else if (recordsShown === recordsQueryCount && settings.features.paginate) {
718
+ text += recordsShown + " of ";
719
+ }
720
+ text += recordsQueryCount + " " + collection_name;
721
+ if (recordsQueryCount < recordsTotal) {
722
+ text += " (filtered from " + recordsTotal + " total records)";
723
+ }
724
+
725
+ return $('<span></span>', {
726
+ id: 'dynatable-record-count-' + obj.element.id,
727
+ 'class': 'dynatable-record-count',
728
+ html: text
729
+ });
730
+ };
731
+
732
+ this.attach = function() {
733
+ var $target = settings.inputs.recordCountTarget ? $(settings.inputs.recordCountTarget) : obj.$element;
734
+ $target[settings.inputs.recordCountPlacement](this.create());
735
+ };
736
+ };
737
+
738
+ function ProcessingIndicator(obj, settings) {
739
+ this.init = function() {
740
+ this.attach();
741
+ };
742
+
743
+ this.create = function() {
744
+ var $processing = $('<div></div>', {
745
+ html: '<span>' + settings.inputs.processingText + '</span>',
746
+ id: 'dynatable-processing-' + obj.element.id,
747
+ 'class': 'dynatable-processing',
748
+ style: 'position: absolute; display: none;'
749
+ });
750
+
751
+ return $processing;
752
+ };
753
+
754
+ this.position = function() {
755
+ var $processing = $('#dynatable-processing-' + obj.element.id),
756
+ $span = $processing.children('span'),
757
+ spanHeight = $span.outerHeight(),
758
+ spanWidth = $span.outerWidth(),
759
+ $covered = obj.$element,
760
+ offset = $covered.offset(),
761
+ height = $covered.outerHeight(), width = $covered.outerWidth();
762
+
763
+ $processing
764
+ .offset({left: offset.left, top: offset.top})
765
+ .width(width)
766
+ .height(height)
767
+ $span
768
+ .offset({left: offset.left + ( (width - spanWidth) / 2 ), top: offset.top + ( (height - spanHeight) / 2 )});
769
+
770
+ return $processing;
771
+ };
772
+
773
+ this.attach = function() {
774
+ obj.$element.before(this.create());
775
+ };
776
+
777
+ this.show = function() {
778
+ $('#dynatable-processing-' + obj.element.id).show();
779
+ this.position();
780
+ };
781
+
782
+ this.hide = function() {
783
+ $('#dynatable-processing-' + obj.element.id).hide();
784
+ };
785
+ };
786
+
787
+ function State(obj, settings) {
788
+ this.initOnLoad = function() {
789
+ // Check if pushState option is true, and if browser supports it
790
+ return settings.features.pushState && history.pushState;
791
+ };
792
+
793
+ this.init = function() {
794
+ window.onpopstate = function(event) {
795
+ if (event.state && event.state.dynatable) {
796
+ obj.state.pop(event);
797
+ }
798
+ }
799
+ };
800
+
801
+ this.push = function(data) {
802
+ var urlString = window.location.search,
803
+ urlOptions,
804
+ path,
805
+ params,
806
+ hash,
807
+ newParams,
808
+ cacheStr,
809
+ cache,
810
+ // replaceState on initial load, then pushState after that
811
+ firstPush = !(window.history.state && window.history.state.dynatable),
812
+ pushFunction = firstPush ? 'replaceState' : 'pushState';
813
+
814
+ if (urlString && /^\?/.test(urlString)) { urlString = urlString.substring(1); }
815
+ $.extend(urlOptions, data);
816
+
817
+ params = utility.refreshQueryString(urlString, data, settings);
818
+ if (params) { params = '?' + params; }
819
+ hash = window.location.hash;
820
+ path = window.location.pathname;
821
+
822
+ obj.$element.trigger('dynatable:push', data);
823
+
824
+ cache = { dynatable: { dataset: settings.dataset } };
825
+ if (!firstPush) { cache.dynatable.scrollTop = $(window).scrollTop(); }
826
+ cacheStr = JSON.stringify(cache);
827
+
828
+ // Mozilla has a 640k char limit on what can be stored in pushState.
829
+ // See "limit" in https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method
830
+ // and "dataStr.length" in http://wine.git.sourceforge.net/git/gitweb.cgi?p=wine/wine-gecko;a=patch;h=43a11bdddc5fc1ff102278a120be66a7b90afe28
831
+ //
832
+ // Likewise, other browsers may have varying (undocumented) limits.
833
+ // Also, Firefox's limit can be changed in about:config as browser.history.maxStateObjectSize
834
+ // Since we don't know what the actual limit will be in any given situation, we'll just try caching and rescue
835
+ // any exceptions by retrying pushState without caching the records.
836
+ //
837
+ // I have absolutely no idea why perPageOptions suddenly becomes an array-like object instead of an array,
838
+ // but just recently, this started throwing an error if I don't convert it:
839
+ // 'Uncaught Error: DATA_CLONE_ERR: DOM Exception 25'
840
+ cache.dynatable.dataset.perPageOptions = $.makeArray(cache.dynatable.dataset.perPageOptions);
841
+
842
+ try {
843
+ window.history[pushFunction](cache, "Dynatable state", path + params + hash);
844
+ } catch(error) {
845
+ // Make cached records = null, so that `pop` will rerun process to retrieve records
846
+ cache.dynatable.dataset.records = null;
847
+ window.history[pushFunction](cache, "Dynatable state", path + params + hash);
848
+ }
849
+ };
850
+
851
+ this.pop = function(event) {
852
+ var data = event.state.dynatable;
853
+ settings.dataset = data.dataset;
854
+
855
+ if (data.scrollTop) { $(window).scrollTop(data.scrollTop); }
856
+
857
+ // If dataset.records is cached from pushState
858
+ if ( data.dataset.records ) {
859
+ obj.dom.update();
860
+ } else {
861
+ obj.process(true);
862
+ }
863
+ };
864
+ };
865
+
866
+ function Sorts(obj, settings) {
867
+ this.initOnLoad = function() {
868
+ return settings.features.sort;
869
+ };
870
+
871
+ this.init = function() {
872
+ var sortsUrl = window.location.search.match(new RegExp(settings.params.sorts + '[^&=]*=[^&]*', 'g'));
873
+ settings.dataset.sorts = sortsUrl ? utility.deserialize(sortsUrl)[settings.params.sorts] : {};
874
+ settings.dataset.sortsKeys = sortsUrl ? utility.keysFromObject(settings.dataset.sorts) : [];
875
+ };
876
+
877
+ this.add = function(attr, direction) {
878
+ var sortsKeys = settings.dataset.sortsKeys,
879
+ index = $.inArray(attr, sortsKeys);
880
+ settings.dataset.sorts[attr] = direction;
881
+ if (index === -1) { sortsKeys.push(attr); }
882
+ return dt;
883
+ };
884
+
885
+ this.remove = function(attr) {
886
+ var sortsKeys = settings.dataset.sortsKeys,
887
+ index = $.inArray(attr, sortsKeys);
888
+ delete settings.dataset.sorts[attr];
889
+ if (index !== -1) { sortsKeys.splice(index, 1); }
890
+ return dt;
891
+ };
892
+
893
+ this.clear = function() {
894
+ settings.dataset.sorts = {};
895
+ settings.dataset.sortsKeys.length = 0;
896
+ };
897
+
898
+ // Try to intelligently guess which sort function to use
899
+ // based on the type of attribute values.
900
+ // Consider using something more robust than `typeof` (http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/)
901
+ this.guessType = function(a, b, attr) {
902
+ var types = {
903
+ string: 'string',
904
+ number: 'number',
905
+ 'boolean': 'number',
906
+ object: 'number' // dates and null values are also objects, this works...
907
+ },
908
+ attrType = a[attr] ? typeof(a[attr]) : typeof(b[attr]),
909
+ type = types[attrType] || 'number';
910
+ return type;
911
+ };
912
+
913
+ // Built-in sort functions
914
+ // (the most common use-cases I could think of)
915
+ this.functions = {
916
+ number: function(a, b, attr, direction) {
917
+ return a[attr] === b[attr] ? 0 : (direction > 0 ? a[attr] - b[attr] : b[attr] - a[attr]);
918
+ },
919
+ string: function(a, b, attr, direction) {
920
+ var aAttr = (a['dynatable-sortable-text'] && a['dynatable-sortable-text'][attr]) ? a['dynatable-sortable-text'][attr] : a[attr],
921
+ bAttr = (b['dynatable-sortable-text'] && b['dynatable-sortable-text'][attr]) ? b['dynatable-sortable-text'][attr] : b[attr],
922
+ comparison;
923
+ aAttr = aAttr.toLowerCase();
924
+ bAttr = bAttr.toLowerCase();
925
+ comparison = aAttr === bAttr ? 0 : (direction > 0 ? aAttr > bAttr : bAttr > aAttr);
926
+ // force false boolean value to -1, true to 1, and tie to 0
927
+ return comparison === false ? -1 : (comparison - 0);
928
+ },
929
+ originalPlacement: function(a, b) {
930
+ return a['dynatable-original-index'] - b['dynatable-original-index'];
931
+ }
932
+ };
933
+ };
934
+
935
+ // turn table headers into links which add sort to sorts array
936
+ function SortsHeaders(obj, settings) {
937
+ var _this = this;
938
+
939
+ this.initOnLoad = function() {
940
+ return settings.features.sort;
941
+ };
942
+
943
+ this.init = function() {
944
+ this.attach();
945
+ };
946
+
947
+ this.create = function(cell) {
948
+ var $cell = $(cell),
949
+ $link = $('<a></a>', {
950
+ 'class': 'dynatable-sort-header',
951
+ href: '#',
952
+ html: $cell.html()
953
+ }),
954
+ id = $cell.data('dynatable-column'),
955
+ column = utility.findObjectInArray(settings.table.columns, {id: id});
956
+
957
+ $link.bind('click', function(e) {
958
+ _this.toggleSort(e, $link, column);
959
+ obj.process();
960
+
961
+ e.preventDefault();
962
+ });
963
+
964
+ if (this.sortedByColumn($link, column)) {
965
+ if (this.sortedByColumnValue(column) == 1) {
966
+ this.appendArrowUp($link);
967
+ } else {
968
+ this.appendArrowDown($link);
969
+ }
970
+ }
971
+
972
+ return $link;
973
+ };
974
+
975
+ this.removeAll = function() {
976
+ obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
977
+ _this.removeAllArrows();
978
+ _this.removeOne(this);
979
+ });
980
+ };
981
+
982
+ this.removeOne = function(cell) {
983
+ var $cell = $(cell),
984
+ $link = $cell.find('.dynatable-sort-header');
985
+ if ($link.length) {
986
+ var html = $link.html();
987
+ $link.remove();
988
+ $cell.html($cell.html() + html);
989
+ }
990
+ };
991
+
992
+ this.attach = function() {
993
+ obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
994
+ _this.attachOne(this);
995
+ });
996
+ };
997
+
998
+ this.attachOne = function(cell) {
999
+ var $cell = $(cell);
1000
+ if (!$cell.data('dynatable-no-sort')) {
1001
+ $cell.html(this.create(cell));
1002
+ }
1003
+ };
1004
+
1005
+ this.appendArrowUp = function($link) {
1006
+ this.removeArrow($link);
1007
+ $link.append("<span class='dynatable-arrow'> &#9650;</span>");
1008
+ };
1009
+
1010
+ this.appendArrowDown = function($link) {
1011
+ this.removeArrow($link);
1012
+ $link.append("<span class='dynatable-arrow'> &#9660;</span>");
1013
+ };
1014
+
1015
+ this.removeArrow = function($link) {
1016
+ // Not sure why `parent()` is needed, the arrow should be inside the link from `append()` above
1017
+ $link.find('.dynatable-arrow').remove();
1018
+ };
1019
+
1020
+ this.removeAllArrows = function() {
1021
+ obj.$element.find('.dynatable-arrow').remove();
1022
+ };
1023
+
1024
+ this.toggleSort = function(e, $link, column) {
1025
+ var sortedByColumn = this.sortedByColumn($link, column),
1026
+ value = this.sortedByColumnValue(column);
1027
+ // Clear existing sorts unless this is a multisort event
1028
+ if (!settings.inputs.multisort || !utility.anyMatch(e, settings.inputs.multisort, function(evt, key) { return e[key]; })) {
1029
+ this.removeAllArrows();
1030
+ obj.sorts.clear();
1031
+ }
1032
+
1033
+ // If sorts for this column are already set
1034
+ if (sortedByColumn) {
1035
+ // If ascending, then make descending
1036
+ if (value == 1) {
1037
+ for (var i = 0, len = column.sorts.length; i < len; i++) {
1038
+ obj.sorts.add(column.sorts[i], -1);
1039
+ }
1040
+ this.appendArrowDown($link);
1041
+ // If descending, remove sort
1042
+ } else {
1043
+ for (var i = 0, len = column.sorts.length; i < len; i++) {
1044
+ obj.sorts.remove(column.sorts[i]);
1045
+ }
1046
+ this.removeArrow($link);
1047
+ }
1048
+ // Otherwise, if not already set, set to ascending
1049
+ } else {
1050
+ for (var i = 0, len = column.sorts.length; i < len; i++) {
1051
+ obj.sorts.add(column.sorts[i], 1);
1052
+ }
1053
+ this.appendArrowUp($link);
1054
+ }
1055
+ };
1056
+
1057
+ this.sortedByColumn = function($link, column) {
1058
+ return utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; });
1059
+ };
1060
+
1061
+ this.sortedByColumnValue = function(column) {
1062
+ return settings.dataset.sorts[column.sorts[0]];
1063
+ };
1064
+ };
1065
+
1066
+ function Queries(obj, settings) {
1067
+ var _this = this;
1068
+
1069
+ this.initOnLoad = function() {
1070
+ return settings.inputs.queries || settings.features.search;
1071
+ };
1072
+
1073
+ this.init = function() {
1074
+ var queriesUrl = window.location.search.match(new RegExp(settings.params.queries + '[^&=]*=[^&]*', 'g'));
1075
+
1076
+ settings.dataset.queries = queriesUrl ? utility.deserialize(queriesUrl)[settings.params.queries] : {};
1077
+ if (settings.dataset.queries === "") { settings.dataset.queries = {}; }
1078
+
1079
+ if (settings.inputs.queries) {
1080
+ this.setupInputs();
1081
+ }
1082
+ };
1083
+
1084
+ this.add = function(name, value) {
1085
+ // reset to first page since query will change records
1086
+ if (settings.features.paginate) {
1087
+ settings.dataset.page = 1;
1088
+ }
1089
+ settings.dataset.queries[name] = value;
1090
+ return dt;
1091
+ };
1092
+
1093
+ this.remove = function(name) {
1094
+ delete settings.dataset.queries[name];
1095
+ return dt;
1096
+ };
1097
+
1098
+ this.run = function() {
1099
+ for (query in settings.dataset.queries) {
1100
+ if (settings.dataset.queries.hasOwnProperty(query)) {
1101
+ var value = settings.dataset.queries[query];
1102
+ if (_this.functions[query] === undefined) {
1103
+ // Try to lazily evaluate query from column names if not explicitly defined
1104
+ var queryColumn = utility.findObjectInArray(settings.table.columns, {id: query});
1105
+ if (queryColumn) {
1106
+ _this.functions[query] = function(record, queryValue) {
1107
+ return record[query] == queryValue;
1108
+ };
1109
+ } else {
1110
+ $.error("Query named '" + query + "' called, but not defined in queries.functions");
1111
+ continue; // to skip to next query
1112
+ }
1113
+ }
1114
+ // collect all records that return true for query
1115
+ settings.dataset.records = $.map(settings.dataset.records, function(record) {
1116
+ return _this.functions[query](record, value) ? record : null;
1117
+ });
1118
+ }
1119
+ }
1120
+ settings.dataset.queryRecordCount = obj.records.count();
1121
+ };
1122
+
1123
+ // Shortcut for performing simple query from built-in search
1124
+ this.runSearch = function(q) {
1125
+ var origQueries = $.extend({}, settings.dataset.queries);
1126
+ if (q) {
1127
+ this.add('search', q);
1128
+ } else {
1129
+ this.remove('search');
1130
+ }
1131
+ if (!utility.objectsEqual(settings.dataset.queries, origQueries)) {
1132
+ obj.process();
1133
+ }
1134
+ };
1135
+
1136
+ this.setupInputs = function() {
1137
+ settings.inputs.queries.each(function() {
1138
+ var $this = $(this),
1139
+ event = $this.data('dynatable-query-event') || settings.inputs.queryEvent,
1140
+ query = $this.data('dynatable-query') || $this.attr('name') || this.id,
1141
+ queryFunction = function(e) {
1142
+ var q = $(this).val();
1143
+ if (q === "") { q = undefined; }
1144
+ if (q === settings.dataset.queries[query]) { return false; }
1145
+ if (q) {
1146
+ _this.add(query, q);
1147
+ } else {
1148
+ _this.remove(query);
1149
+ }
1150
+ obj.process();
1151
+ e.preventDefault();
1152
+ };
1153
+
1154
+ $this
1155
+ .attr('data-dynatable-query', query)
1156
+ .bind(event, queryFunction)
1157
+ .bind('keypress', function(e) {
1158
+ if (e.which == 13) {
1159
+ queryFunction.call(this, e);
1160
+ }
1161
+ });
1162
+
1163
+ if (settings.dataset.queries[query]) { $this.val(decodeURIComponent(settings.dataset.queries[query])); }
1164
+ });
1165
+ };
1166
+
1167
+ // Query functions for in-page querying
1168
+ // each function should take a record and a value as input
1169
+ // and output true of false as to whether the record is a match or not
1170
+ this.functions = {
1171
+ search: function(record, queryValue) {
1172
+ var contains = false;
1173
+ // Loop through each attribute of record
1174
+ for (attr in record) {
1175
+ if (record.hasOwnProperty(attr)) {
1176
+ var attrValue = record[attr];
1177
+ if (typeof(attrValue) === "string" && attrValue.toLowerCase().indexOf(queryValue.toLowerCase()) !== -1) {
1178
+ contains = true;
1179
+ // Don't need to keep searching attributes once found
1180
+ break;
1181
+ } else {
1182
+ continue;
1183
+ }
1184
+ }
1185
+ }
1186
+ return contains;
1187
+ }
1188
+ };
1189
+ };
1190
+
1191
+ function InputsSearch(obj, settings) {
1192
+ var _this = this;
1193
+
1194
+ this.initOnLoad = function() {
1195
+ return settings.features.search;
1196
+ };
1197
+
1198
+ this.init = function() {
1199
+ this.attach();
1200
+ };
1201
+
1202
+ this.create = function() {
1203
+ var $search = $('<input />', {
1204
+ type: 'search',
1205
+ id: 'dynatable-query-search-' + obj.element.id,
1206
+ 'data-dynatable-query': 'search',
1207
+ value: settings.dataset.queries.search
1208
+ }),
1209
+ $searchSpan = $('<span></span>', {
1210
+ id: 'dynatable-search-' + obj.element.id,
1211
+ 'class': 'dynatable-search',
1212
+ text: 'Search: '
1213
+ }).append($search);
1214
+
1215
+ $search
1216
+ .bind(settings.inputs.queryEvent, function() {
1217
+ obj.queries.runSearch($(this).val());
1218
+ })
1219
+ .bind('keypress', function(e) {
1220
+ if (e.which == 13) {
1221
+ obj.queries.runSearch($(this).val());
1222
+ e.preventDefault();
1223
+ }
1224
+ });
1225
+ return $searchSpan;
1226
+ };
1227
+
1228
+ this.attach = function() {
1229
+ var $target = settings.inputs.searchTarget ? $(settings.inputs.searchTarget) : obj.$element;
1230
+ $target[settings.inputs.searchPlacement](this.create());
1231
+ };
1232
+ };
1233
+
1234
+ // provide a public function for selecting page
1235
+ function PaginationPage(obj, settings) {
1236
+ this.initOnLoad = function() {
1237
+ return settings.features.paginate;
1238
+ };
1239
+
1240
+ this.init = function() {
1241
+ var pageUrl = window.location.search.match(new RegExp(settings.params.page + '=([^&]*)'));
1242
+ // If page is present in URL parameters and pushState is enabled
1243
+ // (meaning that it'd be possible for dynatable to have put the
1244
+ // page parameter in the URL)
1245
+ if (pageUrl && settings.features.pushState) {
1246
+ this.set(pageUrl[1]);
1247
+ } else {
1248
+ this.set(1);
1249
+ }
1250
+ };
1251
+
1252
+ this.set = function(page) {
1253
+ settings.dataset.page = parseInt(page, 10);
1254
+ }
1255
+ };
1256
+
1257
+ function PaginationPerPage(obj, settings) {
1258
+ var _this = this;
1259
+
1260
+ this.initOnLoad = function() {
1261
+ return settings.features.paginate;
1262
+ };
1263
+
1264
+ this.init = function() {
1265
+ var perPageUrl = window.location.search.match(new RegExp(settings.params.perPage + '=([^&]*)'));
1266
+
1267
+ // If perPage is present in URL parameters and pushState is enabled
1268
+ // (meaning that it'd be possible for dynatable to have put the
1269
+ // perPage parameter in the URL)
1270
+ if (perPageUrl && settings.features.pushState) {
1271
+ // Don't reset page to 1 on init, since it might override page
1272
+ // set on init from URL
1273
+ this.set(perPageUrl[1], true);
1274
+ } else {
1275
+ this.set(settings.dataset.perPageDefault, true);
1276
+ }
1277
+
1278
+ if (settings.features.perPageSelect) {
1279
+ this.attach();
1280
+ }
1281
+ };
1282
+
1283
+ this.create = function() {
1284
+ var $select = $('<select>', {
1285
+ id: 'dynatable-per-page-' + obj.element.id,
1286
+ 'class': 'dynatable-per-page-select'
1287
+ });
1288
+
1289
+ for (var i = 0, len = settings.dataset.perPageOptions.length; i < len; i++) {
1290
+ var number = settings.dataset.perPageOptions[i],
1291
+ selected = settings.dataset.perPage == number ? 'selected="selected"' : '';
1292
+ $select.append('<option value="' + number + '" ' + selected + '>' + number + '</option>');
1293
+ }
1294
+
1295
+ $select.bind('change', function(e) {
1296
+ _this.set($(this).val());
1297
+ obj.process();
1298
+ });
1299
+
1300
+ return $('<span />', {
1301
+ 'class': 'dynatable-per-page'
1302
+ }).append("<span class='dynatable-per-page-label'>" + settings.inputs.perPageText + "</span>").append($select);
1303
+ };
1304
+
1305
+ this.attach = function() {
1306
+ var $target = settings.inputs.perPageTarget ? $(settings.inputs.perPageTarget) : obj.$element;
1307
+ $target[settings.inputs.perPagePlacement](this.create());
1308
+ };
1309
+
1310
+ this.set = function(number, skipResetPage) {
1311
+ if (!skipResetPage) { obj.paginationPage.set(1); }
1312
+ settings.dataset.perPage = parseInt(number);
1313
+ };
1314
+ };
1315
+
1316
+ // pagination links which update dataset.page attribute
1317
+ function PaginationLinks(obj, settings) {
1318
+ var _this = this;
1319
+
1320
+ this.initOnLoad = function() {
1321
+ return settings.features.paginate;
1322
+ };
1323
+
1324
+ this.init = function() {
1325
+ this.attach();
1326
+ };
1327
+
1328
+ this.create = function() {
1329
+ var pageLinks = '<ul id="' + 'dynatable-pagination-links-' + obj.element.id + '" class="' + settings.inputs.paginationClass + '">',
1330
+ pageLinkClass = settings.inputs.paginationLinkClass,
1331
+ activePageClass = settings.inputs.paginationActiveClass,
1332
+ disabledPageClass = settings.inputs.paginationDisabledClass,
1333
+ pages = Math.ceil(settings.dataset.queryRecordCount / settings.dataset.perPage),
1334
+ page = settings.dataset.page,
1335
+ breaks = [
1336
+ settings.inputs.paginationGap[0],
1337
+ settings.dataset.page - settings.inputs.paginationGap[1],
1338
+ settings.dataset.page + settings.inputs.paginationGap[2],
1339
+ (pages + 1) - settings.inputs.paginationGap[3]
1340
+ ];
1341
+
1342
+ pageLinks += '<li><span>Pages: </span></li>';
1343
+
1344
+ for (var i = 1; i <= pages; i++) {
1345
+ if ( (i > breaks[0] && i < breaks[1]) || (i > breaks[2] && i < breaks[3])) {
1346
+ // skip to next iteration in loop
1347
+ continue;
1348
+ } else {
1349
+ var li = obj.paginationLinks.buildLink(i, i, pageLinkClass, page == i, activePageClass),
1350
+ breakIndex,
1351
+ nextBreak;
1352
+
1353
+ // If i is not between one of the following
1354
+ // (1 + (settings.paginationGap[0]))
1355
+ // (page - settings.paginationGap[1])
1356
+ // (page + settings.paginationGap[2])
1357
+ // (pages - settings.paginationGap[3])
1358
+ breakIndex = $.inArray(i, breaks);
1359
+ nextBreak = breaks[breakIndex + 1];
1360
+ if (breakIndex > 0 && i !== 1 && nextBreak && nextBreak > (i + 1)) {
1361
+ var ellip = '<li><span class="dynatable-page-break">&hellip;</span></li>';
1362
+ li = breakIndex < 2 ? ellip + li : li + ellip;
1363
+ }
1364
+
1365
+ if (settings.inputs.paginationPrev && i === 1) {
1366
+ var prevLi = obj.paginationLinks.buildLink(page - 1, settings.inputs.paginationPrev, pageLinkClass + ' ' + settings.inputs.paginationPrevClass, page === 1, disabledPageClass);
1367
+ li = prevLi + li;
1368
+ }
1369
+ if (settings.inputs.paginationNext && i === pages) {
1370
+ var nextLi = obj.paginationLinks.buildLink(page + 1, settings.inputs.paginationNext, pageLinkClass + ' ' + settings.inputs.paginationNextClass, page === pages, disabledPageClass);
1371
+ li += nextLi;
1372
+ }
1373
+
1374
+ pageLinks += li;
1375
+ }
1376
+ }
1377
+
1378
+ pageLinks += '</ul>';
1379
+
1380
+ // only bind page handler to non-active and non-disabled page links
1381
+ var selector = '#dynatable-pagination-links-' + obj.element.id + ' a.' + pageLinkClass + ':not(.' + activePageClass + ',.' + disabledPageClass + ')';
1382
+ // kill any existing delegated-bindings so they don't stack up
1383
+ $(document).undelegate(selector, 'click.dynatable');
1384
+ $(document).delegate(selector, 'click.dynatable', function(e) {
1385
+ $this = $(this);
1386
+ $this.closest(settings.inputs.paginationClass).find('.' + activePageClass).removeClass(activePageClass);
1387
+ $this.addClass(activePageClass);
1388
+
1389
+ obj.paginationPage.set($this.data('dynatable-page'));
1390
+ obj.process();
1391
+ e.preventDefault();
1392
+ });
1393
+
1394
+ return pageLinks;
1395
+ };
1396
+
1397
+ this.buildLink = function(page, label, linkClass, conditional, conditionalClass) {
1398
+ var link = '<a data-dynatable-page=' + page + ' class="' + linkClass,
1399
+ li = '<li';
1400
+
1401
+ if (conditional) {
1402
+ link += ' ' + conditionalClass;
1403
+ li += ' class="' + conditionalClass + '"';
1404
+ }
1405
+
1406
+ link += '">' + label + '</a>';
1407
+ li += '>' + link + '</li>';
1408
+
1409
+ return li;
1410
+ };
1411
+
1412
+ this.attach = function() {
1413
+ // append page links *after* delegate-event-binding so it doesn't need to
1414
+ // find and select all page links to bind event
1415
+ var $target = settings.inputs.paginationLinkTarget ? $(settings.inputs.paginationLinkTarget) : obj.$element;
1416
+ $target[settings.inputs.paginationLinkPlacement](obj.paginationLinks.create());
1417
+ };
1418
+ };
1419
+
1420
+ utility = dt.utility = {
1421
+ normalizeText: function(text, style) {
1422
+ text = this.textTransform[style](text);
1423
+ return text;
1424
+ },
1425
+ textTransform: {
1426
+ trimDash: function(text) {
1427
+ return text.replace(/^\s+|\s+$/g, "").replace(/\s+/g, "-");
1428
+ },
1429
+ camelCase: function(text) {
1430
+ text = this.trimDash(text);
1431
+ return text
1432
+ .replace(/(\-[a-zA-Z])/g, function($1){return $1.toUpperCase().replace('-','');})
1433
+ .replace(/([A-Z])([A-Z]+)/g, function($1,$2,$3){return $2 + $3.toLowerCase();})
1434
+ .replace(/^[A-Z]/, function($1){return $1.toLowerCase();});
1435
+ },
1436
+ dashed: function(text) {
1437
+ text = this.trimDash(text);
1438
+ return this.lowercase(text);
1439
+ },
1440
+ underscore: function(text) {
1441
+ text = this.trimDash(text);
1442
+ return this.lowercase(text.replace(/(-)/g, '_'));
1443
+ },
1444
+ lowercase: function(text) {
1445
+ return text.replace(/([A-Z])/g, function($1){return $1.toLowerCase();});
1446
+ }
1447
+ },
1448
+ // Deserialize params in URL to object
1449
+ // see http://stackoverflow.com/questions/1131630/javascript-jquery-param-inverse-function/3401265#3401265
1450
+ deserialize: function(query) {
1451
+ if (!query) return {};
1452
+ // modified to accept an array of partial URL strings
1453
+ if (typeof(query) === "object") { query = query.join('&'); }
1454
+
1455
+ var hash = {},
1456
+ vars = query.split("&");
1457
+
1458
+ for (var i = 0; i < vars.length; i++) {
1459
+ var pair = vars[i].split("="),
1460
+ k = decodeURIComponent(pair[0]),
1461
+ v, m;
1462
+
1463
+ if (!pair[1]) { continue };
1464
+ v = decodeURIComponent(pair[1].replace(/\+/g, ' '));
1465
+
1466
+ // modified to parse multi-level parameters (e.g. "hi[there][dude]=whatsup" => hi: {there: {dude: "whatsup"}})
1467
+ while (m = k.match(/([^&=]+)\[([^&=]+)\]$/)) {
1468
+ var origV = v;
1469
+ k = m[1];
1470
+ v = {};
1471
+
1472
+ // If nested param ends in '][', then the regex above erroneously included half of a trailing '[]',
1473
+ // which indicates the end-value is part of an array
1474
+ if (m[2].substr(m[2].length-2) == '][') { // must use substr for IE to understand it
1475
+ v[m[2].substr(0,m[2].length-2)] = [origV];
1476
+ } else {
1477
+ v[m[2]] = origV;
1478
+ }
1479
+ }
1480
+
1481
+ // If it is the first entry with this name
1482
+ if (typeof hash[k] === "undefined") {
1483
+ if (k.substr(k.length-2) != '[]') { // not end with []. cannot use negative index as IE doesn't understand it
1484
+ hash[k] = v;
1485
+ } else {
1486
+ hash[k] = [v];
1487
+ }
1488
+ // If subsequent entry with this name and not array
1489
+ } else if (typeof hash[k] === "string") {
1490
+ hash[k] = v; // replace it
1491
+ // modified to add support for objects
1492
+ } else if (typeof hash[k] === "object") {
1493
+ hash[k] = $.extend({}, hash[k], v);
1494
+ // If subsequent entry with this name and is array
1495
+ } else {
1496
+ hash[k].push(v);
1497
+ }
1498
+ }
1499
+ return hash;
1500
+ },
1501
+ refreshQueryString: function(urlString, data, settings) {
1502
+ var _this = this,
1503
+ queryString = urlString.split('?'),
1504
+ path = queryString.shift(),
1505
+ urlOptions;
1506
+
1507
+ urlOptions = this.deserialize(urlString);
1508
+
1509
+ // Loop through each dynatable param and update the URL with it
1510
+ for (attr in settings.params) {
1511
+ if (settings.params.hasOwnProperty(attr)) {
1512
+ var label = settings.params[attr];
1513
+ // Skip over parameters matching attributes for disabled features (i.e. leave them untouched),
1514
+ // because if the feature is turned off, then parameter name is a coincidence and it's unrelated to dynatable.
1515
+ if (
1516
+ (!settings.features.sort && attr == "sorts") ||
1517
+ (!settings.features.paginate && _this.anyMatch(attr, ["page", "perPage", "offset"], function(attr, param) { return attr == param; }))
1518
+ ) {
1519
+ continue;
1520
+ }
1521
+
1522
+ // Delete page and offset from url params if on page 1 (default)
1523
+ if ((attr === "page" || attr === "offset") && data["page"] === 1) {
1524
+ if (urlOptions[label]) {
1525
+ delete urlOptions[label];
1526
+ }
1527
+ continue;
1528
+ }
1529
+
1530
+ // Delete perPage from url params if default perPage value
1531
+ if (attr === "perPage" && data[label] == settings.dataset.perPageDefault) {
1532
+ if (urlOptions[label]) {
1533
+ delete urlOptions[label];
1534
+ }
1535
+ continue;
1536
+ }
1537
+
1538
+ // For queries, we're going to handle each possible query parameter individually here instead of
1539
+ // handling the entire queries object below, since we need to make sure that this is a query controlled by dynatable.
1540
+ if (attr == "queries" && data[label]) {
1541
+ var queries = settings.inputs.queries || [],
1542
+ inputQueries = $.makeArray(queries.map(function() { return $(this).attr('name') }));
1543
+
1544
+ if (settings.features.search) { inputQueries.push('search'); }
1545
+
1546
+ for (var i = 0, len = inputQueries.length; i < len; i++) {
1547
+ var attr = inputQueries[i];
1548
+ if (data[label][attr]) {
1549
+ if (typeof urlOptions[label] === 'undefined') { urlOptions[label] = {}; }
1550
+ urlOptions[label][attr] = data[label][attr];
1551
+ } else {
1552
+ delete urlOptions[label][attr];
1553
+ }
1554
+ }
1555
+ continue;
1556
+ }
1557
+
1558
+ // If we haven't returned true by now, then we actually want to update the parameter in the URL
1559
+ if (data[label]) {
1560
+ urlOptions[label] = data[label];
1561
+ } else {
1562
+ delete urlOptions[label];
1563
+ }
1564
+ }
1565
+ }
1566
+ return decodeURI($.param(urlOptions));
1567
+ },
1568
+ // Get array of keys from object
1569
+ // see http://stackoverflow.com/questions/208016/how-to-list-the-properties-of-a-javascript-object/208020#208020
1570
+ keysFromObject: function(obj){
1571
+ var keys = [];
1572
+ for (var key in obj){
1573
+ keys.push(key);
1574
+ }
1575
+ return keys;
1576
+ },
1577
+ // Find an object in an array of objects by attributes.
1578
+ // E.g. find object with {id: 'hi', name: 'there'} in an array of objects
1579
+ findObjectInArray: function(array, objectAttr) {
1580
+ var _this = this,
1581
+ foundObject;
1582
+ for (var i = 0, len = array.length; i < len; i++) {
1583
+ var item = array[i];
1584
+ // For each object in array, test to make sure all attributes in objectAttr match
1585
+ if (_this.allMatch(item, objectAttr, function(item, key, value) { return item[key] == value; })) {
1586
+ foundObject = item;
1587
+ break;
1588
+ }
1589
+ }
1590
+ return foundObject;
1591
+ },
1592
+ // Return true if supplied test function passes for ALL items in an array
1593
+ allMatch: function(item, arrayOrObject, test) {
1594
+ // start off with true result by default
1595
+ var match = true,
1596
+ isArray = $.isArray(arrayOrObject);
1597
+ // Loop through all items in array
1598
+ $.each(arrayOrObject, function(key, value) {
1599
+ var result = isArray ? test(item, value) : test(item, key, value);
1600
+ // If a single item tests false, go ahead and break the array by returning false
1601
+ // and return false as result,
1602
+ // otherwise, continue with next iteration in loop
1603
+ // (if we make it through all iterations without overriding match with false,
1604
+ // then we can return the true result we started with by default)
1605
+ if (!result) { return match = false; }
1606
+ });
1607
+ return match;
1608
+ },
1609
+ // Return true if supplied test function passes for ANY items in an array
1610
+ anyMatch: function(item, arrayOrObject, test) {
1611
+ var match = false,
1612
+ isArray = $.isArray(arrayOrObject);
1613
+
1614
+ $.each(arrayOrObject, function(key, value) {
1615
+ var result = isArray ? test(item, value) : test(item, key, value);
1616
+ if (result) {
1617
+ // As soon as a match is found, set match to true, and return false to stop the `$.each` loop
1618
+ match = true;
1619
+ return false;
1620
+ }
1621
+ });
1622
+ return match;
1623
+ },
1624
+ // Return true if two objects are equal
1625
+ // (i.e. have the same attributes and attribute values)
1626
+ objectsEqual: function(a, b) {
1627
+ for (attr in a) {
1628
+ if (a.hasOwnProperty(attr)) {
1629
+ if (!b.hasOwnProperty(attr) || a[attr] !== b[attr]) {
1630
+ return false;
1631
+ }
1632
+ }
1633
+ }
1634
+ for (attr in b) {
1635
+ if (b.hasOwnProperty(attr) && !a.hasOwnProperty(attr)) {
1636
+ return false;
1637
+ }
1638
+ }
1639
+ return true;
1640
+ },
1641
+ // Taken from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/105074#105074
1642
+ randomHash: function() {
1643
+ return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
1644
+ }
1645
+ };
1646
+
1647
+ //-----------------------------------------------------------------
1648
+ // Build the dynatable plugin
1649
+ //-----------------------------------------------------------------
1650
+
1651
+ // Object.create support test, and fallback for browsers without it
1652
+ if ( typeof Object.create !== "function" ) {
1653
+ Object.create = function (o) {
1654
+ function F() {}
1655
+ F.prototype = o;
1656
+ return new F();
1657
+ };
1658
+ }
1659
+
1660
+ //-----------------------------------------------------------------
1661
+ // Global dynatable plugin setting defaults
1662
+ //-----------------------------------------------------------------
1663
+
1664
+ $.dynatableSetup = function(options) {
1665
+ defaults = mergeSettings(options);
1666
+ };
1667
+
1668
+ // Create dynatable plugin based on a defined object
1669
+ $.dynatable = function( object ) {
1670
+ $.fn['dynatable'] = function( options ) {
1671
+ return this.each(function() {
1672
+ if ( ! $.data( this, 'dynatable' ) ) {
1673
+ $.data( this, 'dynatable', Object.create(object).init(this, options) );
1674
+ }
1675
+ });
1676
+ };
1677
+ };
1678
+
1679
+ $.dynatable(dt);
1680
+
1681
+ })(jQuery);