jquery-dynatable-rails 0.3.1 → 0.3.2

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: 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);