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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +17 -0
- data/app/assets/javascripts/dynatable_builder.js +34 -0
- data/lib/dynatable_builder.rb +8 -0
- data/lib/dynatable_builder/builder.rb +104 -0
- data/lib/dynatable_builder/engine.rb +9 -0
- data/lib/dynatable_builder/search.rb +63 -0
- data/lib/dynatable_builder/table.rb +74 -0
- data/lib/dynatable_builder/version.rb +3 -0
- data/lib/dynatable_builder/view_helpers.rb +86 -0
- data/lib/generators/dynatable_generator.rb +39 -0
- data/lib/generators/templates/index.html.erb +18 -0
- data/lib/generators/templates/index.html.haml +14 -0
- data/lib/generators/templates/table.rb +35 -0
- data/vendor/assets/javascripts/jquery.dynatable.js +1733 -0
- data/vendor/assets/stylesheets/jquery.dynatable.css +72 -0
- metadata +200 -0
@@ -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'> ▲</span>");
|
1048
|
+
};
|
1049
|
+
|
1050
|
+
this.appendArrowDown = function($link) {
|
1051
|
+
this.removeArrow($link);
|
1052
|
+
$link.append("<span class='dynatable-arrow'> ▼</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">…</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);
|