dynatable_builder 0.0.1

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