tableling-rails 0.0.23 → 0.0.24

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Tableling v0.0.23
2
+ * Tableling v0.0.24
3
3
  * Copyright (c) 2012-2014 Simon Oulevay (Alpha Hydrae) <hydrae.alpha@gmail.com>
4
4
  * Distributed under MIT license
5
5
  * https://github.com/AlphaHydrae/tableling
@@ -7,729 +7,752 @@
7
7
  Backbone.Tableling = Tableling = (function(Backbone, _, $){
8
8
 
9
9
  var Tableling = {
10
- version : "0.0.23"
10
+ version : "0.0.24"
11
11
  };
12
12
 
13
- // Tableling
14
- // ---------
15
- //
16
- // A tableling table is a Marionette layout which fetches data
17
- // from a Backbone collection. It is controlled with an EventAggregator.
18
- Tableling.Table = Backbone.Marionette.Layout.extend({
19
-
20
- className: 'tableling',
21
-
22
- // Default table options can be overriden by subclasses.
23
- config : {
24
- page : 1
25
- },
26
-
27
- initialize : function(options) {
28
- options = options || {};
29
-
30
- this.collection = options.collection;
31
-
32
- // Table options can also be overriden for each instance at construction.
33
- this.config = _.extend(_.clone(this.config || {}), _.result(options, 'config') || {});
34
-
35
- // We use an event aggregator to manage the layout and its components.
36
- // You can use your own by passing a `vent` option.
37
- this.vent = options.vent || new Backbone.Wreqr.EventAggregator();
38
-
39
- this.fetchOptions = _.extend(_.clone(this.fetchOptions || {}), _.result(options, 'fetchOptions') || {});
40
- this.autoUpdate = typeof(options.autoUpdate) != 'undefined' ? options.autoUpdate : true;
41
-
42
- // Components should trigger the `table:update` event to update
43
- // the table (e.g. change page size, sort) and fetch the new data.
44
- this.vent.on('table:update', this.onUpdate, this);
45
-
46
- this.on('item:rendered', this.setup, this);
47
- },
48
-
49
- // Called once rendering is complete. By default, it updates the table.
50
- setup : function() {
51
- this.ventTrigger('table:setup', this.config);
52
- if (this.autoUpdate) {
53
- this.ventTrigger('table:update');
54
- }
55
- },
56
-
57
- // Subclasses must return the Backbone.Collection used to fetch data.
58
- getCollection : function() {
59
- return this.collection;
60
- },
61
-
62
- // ### Refreshing the table
63
- update : function(config, options) {
64
- this.ventTrigger('table:update', config, options);
65
- },
66
-
67
- onUpdate : function(config, options) {
68
-
69
- _.each(config || {}, _.bind(this.updateValue, this));
70
-
71
- // Set the `refresh` option to false to update the table configuration
72
- // without refreshing.
73
- if (!options || typeof(options.refresh) == 'undefined' || options.refresh) {
74
- this.refresh();
75
- }
76
- },
77
-
78
- updateValue : function(value, key) {
79
- if (value && value.toString().length) {
80
- this.config[key] = value;
81
- } else {
82
- // Blank values are deleted to avoid sending them in ajax requests.
83
- delete this.config[key];
84
- }
85
- },
86
-
87
- refresh : function() {
88
-
89
- // You can provide `fetchOptions` to add properties to the
90
- // fetch request.
91
- //
92
- // var MyTable = Tableling.Table.extend({
93
- // fetchOptions : {
94
- // type : 'POST' // fetch data with POST
95
- // }
96
- // });
97
- //
98
- // // You can also override for each instance.
99
- // new MyTable({
100
- // fetchOptions : {
101
- // type : 'GET'
102
- // }
103
- // });
104
- var options = _.clone(this.fetchOptions);
105
- options.data = this.requestData();
106
- options.success = _.bind(this.processResponse, this);
107
- options.reset = true;
108
-
109
- // `table:refreshing` is triggered every time new data is being fetched.
110
- // The first argument is the request data.
111
- this.ventTrigger('table:refreshing', options.data);
112
-
113
- this.getCollection().fetch(options);
114
- },
115
-
116
- // ### Request
117
- requestData : function() {
118
- return this.config;
119
- },
120
-
121
- // ### Response
122
- processResponse : function(collection, response) {
123
-
124
- this.config.length = collection.length;
125
-
126
- // Tableling expects the response from a fetch to have a `total` property
127
- // which is the total number of items (not just in the current page).
128
- this.config.total = response.total;
129
-
130
- // The server may override the `page` property, for example if the
131
- // requested page was outside the range of available pages.
132
- if (response.page) {
133
- this.config.page = response.page;
134
- }
135
-
136
- // `tableling:refreshed` is triggered after every refresh. The first argument
137
- // is the current table configuration with the following additional meta data:
138
- //
139
- // * `total` - the total number of items
140
- // * `length` - the number of items in the current page
141
- this.ventTrigger('table:refreshed', this.config);
142
- },
143
-
144
- // Triggers an event in the event aggregator. If `Tableling.debug` is set, it also
145
- // logs the event and its arguments.
146
- ventTrigger : function() {
147
-
148
- var args = Array.prototype.slice.call(arguments);
149
- if (Tableling.debug) {
150
- console.log(_.first(args) + ' - ' + JSON.stringify(args.slice(1)));
151
- }
152
-
153
- this.vent.trigger.apply(this.vent, args);
13
+ // Tableling
14
+ // ---------
15
+ //
16
+ // A tableling table is a Marionette layout which fetches data
17
+ // from a Backbone collection. It is controlled with an EventAggregator.
18
+ Tableling.Table = Backbone.Marionette.Layout.extend({
19
+
20
+ className: 'tableling',
21
+
22
+ // Default table options can be overriden by subclasses.
23
+ config: {
24
+ page: 1
25
+ },
26
+
27
+ initialize: function(options) {
28
+ options = options || {};
29
+
30
+ this.collection = options.collection;
31
+
32
+ // Table options can also be overriden for each instance at construction.
33
+ this.config = _.extend(_.clone(this.config || {}), _.result(options, 'config') || {});
34
+
35
+ // We use an event aggregator to manage the layout and its components.
36
+ // You can use your own by passing a `vent` option.
37
+ this.vent = options.vent || new Backbone.Wreqr.EventAggregator();
38
+
39
+ this.fetchOptions = _.extend(_.clone(this.fetchOptions || {}), _.result(options, 'fetchOptions') || {});
40
+
41
+ if (typeof(options.autoUpdate) != 'undefined') {
42
+ this.autoUpdate = options.autoUpdate;
154
43
  }
155
- });
156
-
157
- // Tableling.Collection
158
- // --------------------
159
- //
160
- // Tableling expects fetch responses to have a `total` property in addition
161
- // to the model data. You can extend this Backbone.Collection subclass which
162
- // expects the following response format:
163
- //
164
- // {
165
- // "total": 12,
166
- // "data": [
167
- // { /* ... model data ... */ },
168
- // { /* ... model data ... */ }
169
- // ]
170
- // }
171
- Tableling.Collection = Backbone.Collection.extend({
172
-
173
- parse : function(response) {
174
- return response.data;
44
+
45
+ if (typeof(this.autoUpdate) == 'undefined') {
46
+ this.autoUpdate = true;
175
47
  }
176
- });
177
-
178
- // Implementations
179
- // ---------------
180
- //
181
- // <a href="tableling.bootstrap.html">tableling.bootstrap</a> provides views styled
182
- // with [Twitter Bootstrap](http://twitter.github.com/bootstrap/) classes.
183
-
184
- // Tableling.Modular
185
- // -----------------
186
- //
187
- // Tableling subclass which splits functionality into *modules*
188
- // and handles rendering.
189
- Tableling.Modular = Tableling.Table.extend({
190
-
191
- // The list of module names must be specified by subclasses.
192
- modules : [],
193
-
194
- // Modules are set up after rendering, before refreshing.
195
- setup : function() {
196
-
197
- this.moduleViews = {};
198
- _.each(this.modules, _.bind(this.setupModule, this));
199
-
200
- Tableling.Table.prototype.setup.call(this);
201
- },
202
-
203
- // ### Modules
204
- // Each module is identified by a name, for example `pageSize`.
205
- setupModule : function(name) {
206
-
207
- // The layout must have a region named after the module, e.g. `pageSizeRegion`.
208
- var region = name + 'Region';
209
-
210
- // It must have a view class, e.g. `pageSizeView`, which will be shown into
211
- // the region.
212
- var viewClass = this[name + 'View'];
213
-
214
- // When instantiated, the view class will be passed the event
215
- // aggregator as the `vent` option. Additional options can be
216
- // given named after the view class, e.g. `pageSizeViewOptions`.
217
- var options = _.extend(this.getModuleOptions(name), { vent: this.vent });
218
-
219
- var view = new viewClass(options);
220
-
221
- // Module view instances are stored by name in the `moduleViews` property
222
- // for future reference.
223
- this.moduleViews[name] = view;
224
-
225
- this[region].show(view);
226
- return view;
227
- },
228
-
229
- // By default the collection is the one given at construction.
230
- // Otherwise, a modular table expects a `table` module which
231
- // should have a collection (e.g. a Marionette CompositeView or
232
- // CollectionView). If your subclass does not have either, it
233
- // should override this method to return the Backbone.Collection
234
- // used to fetch table data.
235
- getCollection : function() {
236
- return this.collection || (this.moduleViews && this.moduleViews.table ? this.moduleViews.table.collection : undefined);
237
- },
238
-
239
- getModuleOptions : function(name) {
240
- var options = this[name + 'ViewOptions'] || {};
241
- options = typeof(options) == 'function' ? options.call(this) : options;
242
- return name == 'table' ? _.defaults(options, { collection : this.collection }) : options;
48
+
49
+ // Components should trigger the `table:update` event to update
50
+ // the table (e.g. change page size, sort) and fetch the new data.
51
+ this.vent.on('table:update', this.onUpdate, this);
52
+
53
+ this.on('item:rendered', this.setup, this);
54
+
55
+ if (typeof(this.initializeTable) == 'function') {
56
+ this.initializeTable(options);
243
57
  }
244
- });
245
-
246
- // ### Example
247
- // This is how a `PageSizeView` module might be registered in a subclass:
248
- //
249
- // var MyTable = Tableling.Modular.extend({
250
- //
251
- // modules : [ 'pageSize' ],
252
- //
253
- // pageSizeView : PageSizeView,
254
- // pageSizeViewOptions : {
255
- // itemView : PageSizeItem
256
- // },
257
- //
258
- // regions : {
259
- // pageSizeRegion : '.pageSize'
260
- // }
261
- // });
262
-
263
- // Tableling.Module
264
- // ----------------
265
- //
266
- // A module is an item view that is automatically bound to the table's
267
- // event aggregator.
268
- Tableling.Module = Backbone.Marionette.ItemView.extend({
269
-
270
- i18n : {},
271
- templateHelpers : function() {
272
- return this.i18n;
273
- },
274
-
275
- initialize : function(options) {
276
-
277
- this.vent = options.vent;
278
-
279
- // The `setup` method of the view is called when the table
280
- // is first set up.
281
- this.vent.on('table:setup', this.setup, this);
282
-
283
- // The `refresh` method of the view is called every time the table
284
- // is refreshed.
285
- this.vent.on('table:refreshed', this.refresh, this);
286
-
287
- this.i18n = _.clone(options.i18n || this.i18n);
288
- },
289
-
290
- // Call `update` to trigger an update of the table.
291
- update : function() {
292
- this.vent.trigger('table:update', this.config());
293
- },
294
-
295
- // Implementations should override this to set initial values.
296
- setup : function(config) {
297
- },
298
-
299
- // Implementations should override this to stay up to date with
300
- // the table state.
301
- refresh : function(config) {
302
- },
303
-
304
- // New table configuration to be sent on updates. For example,
305
- // a page size view might update the `pageSize` property.
306
- config : function() {
307
- return {};
58
+ },
59
+
60
+ // Called once rendering is complete. By default, it updates the table.
61
+ setup: function() {
62
+ this.ventTrigger('table:setup', this.config);
63
+ if (this.autoUpdate) {
64
+ this.ventTrigger('table:update');
308
65
  }
309
- });
310
-
311
- // Tableling.FieldModule
312
- // ---------------------
313
- //
314
- // A basic module with a single form field. It comes with sensible
315
- // defaults and only requires a `name` and a `template` parameter.
316
- Tableling.FieldModule = Tableling.Module.extend({
317
-
318
- // TODO: check name
319
-
320
- initialize : function(options) {
321
-
322
- Tableling.Module.prototype.initialize.call(this, options);
323
-
324
- if (!this.ui) {
325
- this.ui = {};
326
- }
327
- // The name attribute of the form field is the same as the
328
- // module's, e.g. `pageSize`.
329
- this.ui.field = '[name="' + this.name + '"]';
330
-
331
- if (!this.events) {
332
- this.events = {};
333
- }
334
- this.events.submit = 'onSubmit';
335
- this.events['change [name="' + this.name + '"]'] = 'update';
336
- },
337
-
338
- setup : function(config) {
339
- this.setupValue(config[this.name]);
340
- this.vent.trigger('table:update', this.config(), { refresh : false });
341
- },
342
-
343
- setupValue : function(value) {
344
- this.ui.field.val(value);
345
- },
346
-
347
- // The table property updated is the one with the same name as the module.
348
- config : function() {
349
- var config = {};
350
- config[this.name] = this.ui.field.val();
351
- return config;
352
- },
353
-
354
- onSubmit : function(e) {
355
- e.preventDefault();
356
- return false;
66
+ },
67
+
68
+ // Subclasses must return the Backbone.Collection used to fetch data.
69
+ getCollection: function() {
70
+ return this.collection;
71
+ },
72
+
73
+ // ### Refreshing the table
74
+ update: function(config, options) {
75
+ this.ventTrigger('table:update', config, options);
76
+ },
77
+
78
+ onUpdate: function(config, options) {
79
+
80
+ _.each(config || {}, _.bind(this.updateValue, this));
81
+
82
+ // Set the `refresh` option to false to update the table configuration
83
+ // without refreshing.
84
+ if (!options || typeof(options.refresh) == 'undefined' || options.refresh) {
85
+ this.refresh();
357
86
  }
358
- });
359
-
360
- // This is how a `PageSizeView` module might be implemented:
361
- //
362
- // var html = '<input type="text" name="pageSize" />';
363
- //
364
- // var PageSizeView = Tableling.FieldModule.extend({
365
- // name : 'pageSize'
366
- // template : _.template(html)
367
- // });
368
- //
369
- // When the value of the input field changes, the event aggregator will
370
- // receive a `tableling:update` event with the `pageSize` property set
371
- // to that value.
372
-
373
- Tableling.Plain = {};
374
-
375
- Tableling.Plain.Table = Tableling.Modular.extend({
376
-
377
- className: 'tableling',
378
- modules : [ 'table', 'pageSize', 'quickSearch', 'info', 'page' ],
379
- template : _.template('<div class="header"><div class="pageSize" /><div class="quickSearch" /></div><div class="table" /><div class="footer"><div class="info" /><div class="page" /></div>'),
380
-
381
- regions : {
382
- tableRegion : '.table',
383
- pageSizeRegion : '.pageSize',
384
- quickSearchRegion : '.quickSearch',
385
- infoRegion : '.info',
386
- pageRegion : '.page'
87
+ },
88
+
89
+ updateValue: function(value, key) {
90
+ if (value && value.toString().length) {
91
+ this.config[key] = value;
92
+ } else {
93
+ // Blank values are deleted to avoid sending them in ajax requests.
94
+ delete this.config[key];
387
95
  }
388
- });
389
-
390
- Tableling.Plain.TableView = Backbone.Marionette.CompositeView.extend({
391
-
392
- events : {
393
- 'click thead th.sorting' : 'updateSort',
394
- 'click thead th.sorting-asc' : 'updateSort',
395
- 'click thead th.sorting-desc' : 'updateSort'
396
- },
397
-
398
- initialize : function(options) {
399
- // TODO: add auto-sort
400
- this.vent = options.vent;
401
- this.sort = [];
402
- this.vent.on('table:setup', this.setSort, this);
403
- this.vent.on('table:refreshed', this.setSort, this);
404
- },
405
-
406
- updateSort : function(ev) {
407
-
408
- var el = $(ev.currentTarget);
409
- if (!(el.hasClass('sorting') || el.hasClass('sorting-asc') || el.hasClass('sorting-desc'))) {
410
- return;
411
- }
412
-
413
- var field = this.fieldName(el);
414
-
415
- if (ev.shiftKey || this.sort.length == 1) {
416
-
417
- var index = -1;
418
- _.find(this.sort, function(item, i) {
419
- if (item.split(' ')[0] == field) {
420
- index = i;
421
- }
422
- });
423
-
424
- if (index >= 0) {
425
-
426
- var parts = this.sort[index].split(' ');
427
- this.sort[index] = parts[0] + ' ' + (parts[1] == 'asc' ? 'desc' : 'asc');
428
- this.showSort();
429
- return this.vent.trigger('table:update', this.config());
430
- }
431
- }
432
-
433
- if (!ev.shiftKey) {
434
- this.sort.length = 0;
435
- }
436
-
437
- this.sort.push(field + ' asc');
438
-
439
- this.showSort();
440
-
441
- this.vent.trigger('table:update', this.config());
442
- },
443
-
444
- setSort : function(config) {
445
- if (config && config.sort) {
446
- this.sort = config.sort.slice(0);
447
- this.showSort();
448
- }
449
- },
450
-
451
- showSort : function() {
452
-
453
- this.$el.find('thead th.sorting, thead th.sorting-asc, thead th.sorting-desc').removeClass('sorting sorting-asc sorting-desc').addClass('sorting');
454
-
455
- for (var i = 0; i < this.sort.length; i++) {
456
-
457
- var parts = this.sort[i].split(' ');
458
- var name = parts[0];
459
- var direction = parts[1];
460
-
461
- field = this.$el.find('thead [data-field="' + name + '"]');
462
- if (!field.length) {
463
- field = this.$el.find('thead th:contains("' + name + '")');
464
- }
465
-
466
- if (field.length) {
467
- field.removeClass('sorting').addClass(direction == 'desc' ? 'sorting-desc' : 'sorting-asc');
468
- }
469
- }
470
- },
471
-
472
- config : function() {
473
- return {
474
- page : 1,
475
- sort : this.sortConfig()
476
- };
477
- },
478
-
479
- sortConfig : function() {
480
- return this.sort.length ? this.sort : null;
481
- },
482
-
483
- fieldName : function(el) {
484
- return el.data('field') || el.text();
96
+ },
97
+
98
+ refresh: function() {
99
+
100
+ // You can provide `fetchOptions` to add properties to the
101
+ // fetch request.
102
+ //
103
+ // var MyTable = Tableling.Table.extend({
104
+ // fetchOptions: {
105
+ // type: 'POST' // fetch data with POST
106
+ // }
107
+ // });
108
+ //
109
+ // // You can also override for each instance.
110
+ // new MyTable({
111
+ // fetchOptions: {
112
+ // type: 'GET'
113
+ // }
114
+ // });
115
+ var options = _.clone(this.fetchOptions);
116
+ options.data = this.requestData();
117
+ options.success = _.bind(this.processResponse, this);
118
+ options.reset = true;
119
+
120
+ // `table:refreshing` is triggered every time new data is being fetched.
121
+ // The first argument is the request data.
122
+ this.ventTrigger('table:refreshing', options.data);
123
+
124
+ this.getCollection().fetch(options);
125
+ },
126
+
127
+ // ### Request
128
+ requestData: function() {
129
+ return this.config;
130
+ },
131
+
132
+ // ### Response
133
+ processResponse: function(collection, response) {
134
+
135
+ this.config.length = collection.length;
136
+
137
+ // Tableling expects the response from a fetch to have a `total` property
138
+ // which is the total number of items (not just in the current page).
139
+ this.config.total = response.total;
140
+
141
+ // The server may override the `page` property, for example if the
142
+ // requested page was outside the range of available pages.
143
+ if (response.page) {
144
+ this.config.page = response.page;
485
145
  }
486
- });
487
-
488
- Tableling.Plain.PageSizeView = Tableling.Plain.Table.prototype.pageSizeView = Tableling.FieldModule.extend({
489
-
490
- // TODO: update current page intelligently
491
- name : 'pageSize',
492
- template : function(data) {
493
- return _.template('<select name="pageSize" /> <%- entries %>', data);
494
- },
495
-
496
- i18n : {
497
- entries : 'entries per page'
498
- },
499
- sizes : [ 10, 15, 20, 25, 50 ],
500
-
501
- ui : {
502
- field : 'select'
503
- },
504
-
505
- initialize : function(options) {
506
- Tableling.FieldModule.prototype.initialize.call(this, options);
507
- this.sizes = _.clone(options.sizes || this.sizes);
508
- },
509
-
510
- onRender : function() {
511
- this.ui.field.empty();
512
- _.each(this.sizes, _.bind(this.addSize, this));
513
- },
514
-
515
- addSize : function(size) {
516
- $('<option />').text(size).appendTo(this.ui.field);
517
- },
518
-
519
- setupValue : function(value) {
520
- if (value) {
521
- Tableling.FieldModule.prototype.setupValue.apply(this, Array.prototype.slice.call(arguments));
522
- }
523
- },
524
-
525
- config : function() {
526
- var config = Tableling.FieldModule.prototype.config.call(this);
527
- config.page = 1;
528
- return config;
146
+
147
+ // `tableling:refreshed` is triggered after every refresh. The first argument
148
+ // is the current table configuration with the following additional meta data:
149
+ //
150
+ // * `total` - the total number of items
151
+ // * `length` - the number of items in the current page
152
+ this.ventTrigger('table:refreshed', this.config);
153
+ },
154
+
155
+ // Triggers an event in the event aggregator. If `Tableling.debug` is set, it also
156
+ // logs the event and its arguments.
157
+ ventTrigger: function() {
158
+
159
+ var args = Array.prototype.slice.call(arguments);
160
+ if (Tableling.debug) {
161
+ console.log(_.first(args) + ' - ' + JSON.stringify(args.slice(1)));
529
162
  }
530
- });
531
-
532
- Tableling.Plain.QuickSearchView = Tableling.Plain.Table.prototype.quickSearchView = Tableling.FieldModule.extend({
533
-
534
- name : 'quickSearch',
535
- template : function(data) {
536
- return _.template('<input type="text" name="quickSearch" placeholder="<%- quickSearch %>" />', data);
537
- },
538
-
539
- i18n : {
540
- quickSearch : 'Quick search...'
541
- },
542
-
543
- config : function() {
544
- var config = Tableling.FieldModule.prototype.config.call(this);
545
- config.page = 1;
546
- return config;
163
+
164
+ this.vent.trigger.apply(this.vent, args);
165
+ }
166
+ });
167
+
168
+ // Tableling.Collection
169
+ // --------------------
170
+ //
171
+ // Tableling expects fetch responses to have a `total` property in addition
172
+ // to the model data. You can extend this Backbone.Collection subclass which
173
+ // expects the following response format:
174
+ //
175
+ // {
176
+ // "total": 12,
177
+ // "data": [
178
+ // { /* ... model data ... */ },
179
+ // { /* ... model data ... */ }
180
+ // ]
181
+ // }
182
+ Tableling.Collection = Backbone.Collection.extend({
183
+
184
+ parse: function(response) {
185
+ return response.data;
186
+ }
187
+ });
188
+
189
+ // Implementations
190
+ // ---------------
191
+ //
192
+ // <a href="tableling.bootstrap.html">tableling.bootstrap</a> provides views styled
193
+ // with [Twitter Bootstrap](http://twitter.github.com/bootstrap/) classes.
194
+
195
+ // Tableling.Modular
196
+ // -----------------
197
+ //
198
+ // Tableling subclass which splits functionality into *modules*
199
+ // and handles rendering.
200
+ Tableling.Modular = Tableling.Table.extend({
201
+
202
+ // The list of module names must be specified by subclasses.
203
+ modules: [],
204
+
205
+ // Modules are set up after rendering, before refreshing.
206
+ setup: function() {
207
+
208
+ this.moduleViews = {};
209
+ _.each(this.modules, this.setupModule, this);
210
+
211
+ Tableling.Table.prototype.setup.call(this);
212
+ },
213
+
214
+ // ### Modules
215
+ // Each module is identified by a name, for example `pageSize`.
216
+ setupModule: function(name) {
217
+
218
+ // The layout must have a region named after the module, e.g. `pageSizeRegion`.
219
+ var region = name + 'Region';
220
+
221
+ // It must have a view class, e.g. `pageSizeView`, which will be shown into
222
+ // the region.
223
+ var viewClass = this[name + 'View'];
224
+
225
+ // When instantiated, the view class will be passed the event
226
+ // aggregator as the `vent` option. Additional options can be
227
+ // given named after the view class, e.g. `pageSizeViewOptions`.
228
+ var options = _.extend(this.getModuleOptions(name), { vent: this.vent });
229
+
230
+ // The collection is also passed to view classes.
231
+ _.defaults(options, { collection: this.getCollection() });
232
+
233
+ var view = new viewClass(options);
234
+
235
+ // Module view instances are stored by name in the `moduleViews` property
236
+ // for future reference.
237
+ this.moduleViews[name] = view;
238
+
239
+ this[region].show(view);
240
+ return view;
241
+ },
242
+
243
+ // By default the collection is the one given at construction.
244
+ // Otherwise, a modular table expects a `table` module which
245
+ // should have a collection (e.g. a Marionette CompositeView or
246
+ // CollectionView). If your subclass does not have either, it
247
+ // should override this method to return the Backbone.Collection
248
+ // used to fetch table data.
249
+ getCollection: function() {
250
+ return this.collection || (this.moduleViews && this.moduleViews.table ? this.moduleViews.table.collection : undefined);
251
+ },
252
+
253
+ getModuleOptions: function(name) {
254
+ return _.result(this, name + 'ViewOptions') || {};
255
+ }
256
+ });
257
+
258
+ // ### Example
259
+ // This is how a `PageSizeView` module might be registered in a subclass:
260
+ //
261
+ // var MyTable = Tableling.Modular.extend({
262
+ //
263
+ // modules: [ 'pageSize' ],
264
+ //
265
+ // pageSizeView: PageSizeView,
266
+ // pageSizeViewOptions: {
267
+ // itemView: PageSizeItem
268
+ // },
269
+ //
270
+ // regions: {
271
+ // pageSizeRegion: '.pageSize'
272
+ // }
273
+ // });
274
+
275
+ // Tableling.Module
276
+ // ----------------
277
+ //
278
+ // A module is an item view that is automatically bound to the table's
279
+ // event aggregator.
280
+ Tableling.Module = Backbone.Marionette.ItemView.extend({
281
+
282
+ i18n: {},
283
+ templateHelpers: function() {
284
+ return this.i18n;
285
+ },
286
+
287
+ initialize: function(options) {
288
+
289
+ this.vent = options.vent;
290
+
291
+ // The `setup` method of the view is called when the table
292
+ // is first set up.
293
+ this.vent.on('table:setup', this.setup, this);
294
+
295
+ // The `refresh` method of the view is called every time the table
296
+ // is refreshed.
297
+ this.vent.on('table:refreshed', this.refresh, this);
298
+
299
+ this.i18n = _.clone(options.i18n || this.i18n);
300
+
301
+ if (typeof(this.initializeModule) == 'function') {
302
+ this.initializeModule(options);
547
303
  }
548
- });
549
-
550
- Tableling.Plain.InfoView = Tableling.Plain.Table.prototype.infoView = Tableling.Module.extend({
551
-
552
- template : function(data) {
553
- return _.template(data.template, {
554
- first : '<span class="first">0</span>',
555
- last : '<span class="last">0</span>',
556
- total : '<span class="total">0</span>'
304
+ },
305
+
306
+ // Call `update` to trigger an update of the table.
307
+ update: function() {
308
+ this.vent.trigger('table:update', this.config());
309
+ },
310
+
311
+ // Implementations should override this to set initial values.
312
+ setup: function(config) {
313
+ },
314
+
315
+ // Implementations should override this to stay up to date with
316
+ // the table state.
317
+ refresh: function(config) {
318
+ },
319
+
320
+ // New table configuration to be sent on updates. For example,
321
+ // a page size view might update the `pageSize` property.
322
+ config: function() {
323
+ return {};
324
+ }
325
+ });
326
+
327
+ // Tableling.FieldModule
328
+ // ---------------------
329
+ //
330
+ // A basic module with a single form field. It comes with sensible
331
+ // defaults and only requires a `name` and a `template` parameter.
332
+ Tableling.FieldModule = Tableling.Module.extend({
333
+
334
+ initialize: function(options) {
335
+ if (!_.isString(this.name)) {
336
+ throw new Error("Tableling module must have a name property.");
337
+ }
338
+
339
+ if (!this.ui) {
340
+ this.ui = {};
341
+ }
342
+ // The name attribute of the form field is the same as the
343
+ // module's, e.g. `pageSize`.
344
+ this.ui.field = '[name="' + this.name + '"]';
345
+
346
+ if (!this.events) {
347
+ this.events = {};
348
+ }
349
+ this.events.submit = 'onSubmit';
350
+ this.events['change [name="' + this.name + '"]'] = 'update';
351
+
352
+ Tableling.Module.prototype.initialize.call(this, options);
353
+ },
354
+
355
+ setup: function(config) {
356
+ this.setupValue(config[this.name]);
357
+ this.vent.trigger('table:update', this.config(), { refresh: false });
358
+ },
359
+
360
+ setupValue: function(value) {
361
+ this.ui.field.val(value);
362
+ },
363
+
364
+ // The table property updated is the one with the same name as the module.
365
+ config: function() {
366
+ var config = {};
367
+ config[this.name] = this.ui.field.val();
368
+ return config;
369
+ },
370
+
371
+ onSubmit: function(e) {
372
+ e.preventDefault();
373
+ return false;
374
+ }
375
+ });
376
+
377
+ // This is how a `PageSizeView` module might be implemented:
378
+ //
379
+ // var html = '<input type="text" name="pageSize" />';
380
+ //
381
+ // var PageSizeView = Tableling.FieldModule.extend({
382
+ // name: 'pageSize'
383
+ // template: _.template(html)
384
+ // });
385
+ //
386
+ // When the value of the input field changes, the event aggregator will
387
+ // receive a `tableling:update` event with the `pageSize` property set
388
+ // to that value.
389
+
390
+ Tableling.Plain = {};
391
+
392
+ Tableling.Plain.Table = Tableling.Modular.extend({
393
+
394
+ className: 'tableling',
395
+ modules: [ 'table', 'pageSize', 'quickSearch', 'info', 'page' ],
396
+ template: _.template('<div class="header"><div class="pageSize" /><div class="quickSearch" /></div><div class="table" /><div class="footer"><div class="info" /><div class="page" /></div>'),
397
+
398
+ regions: {
399
+ tableRegion: '.table',
400
+ pageSizeRegion: '.pageSize',
401
+ quickSearchRegion: '.quickSearch',
402
+ infoRegion: '.info',
403
+ pageRegion: '.page'
404
+ }
405
+ });
406
+
407
+ // TODO: make table view a module
408
+ Tableling.Plain.TableView = Backbone.Marionette.CompositeView.extend({
409
+
410
+ moduleEvents: {
411
+ 'click thead th.sorting': 'updateSort',
412
+ 'click thead th.sorting-asc': 'updateSort',
413
+ 'click thead th.sorting-desc': 'updateSort'
414
+ },
415
+
416
+ // TODO: add auto-sort
417
+ initialize: function(options) {
418
+
419
+ this.vent = options.vent;
420
+ this.sort = [];
421
+ this.vent.on('table:setup', this.setSort, this);
422
+ this.vent.on('table:refreshed', this.setSort, this);
423
+ this.events = _.extend({}, this.events || {}, this.moduleEvents);
424
+
425
+ if (typeof(this.initializeModule) == 'function') {
426
+ this.initializeModule(options);
427
+ }
428
+ },
429
+
430
+ updateSort: function(ev) {
431
+
432
+ var el = $(ev.currentTarget);
433
+ if (!(el.hasClass('sorting') || el.hasClass('sorting-asc') || el.hasClass('sorting-desc'))) {
434
+ return;
435
+ }
436
+
437
+ var field = this.fieldName(el);
438
+
439
+ if (ev.shiftKey || this.sort.length == 1) {
440
+
441
+ var index = -1;
442
+ _.find(this.sort, function(item, i) {
443
+ if (item.split(' ')[0] == field) {
444
+ index = i;
445
+ }
557
446
  });
558
- },
559
-
560
- i18n : {
561
- template : 'Showing <%= first %> to <%= last %> of <%= total %> entries'
562
- },
563
-
564
- ui : {
565
- first: '.first',
566
- last: '.last',
567
- total: '.total'
568
- },
569
-
570
- refresh : function(data) {
571
- if (data) {
572
- this.ui.first.text(this.firstRecord(data));
573
- this.ui.last.text(this.lastRecord(data));
574
- this.ui.total.text(data.total);
447
+
448
+ if (index >= 0) {
449
+
450
+ var parts = this.sort[index].split(' ');
451
+ this.sort[index] = parts[0] + ' ' + (parts[1] == 'asc' ? 'desc' : 'asc');
452
+ this.showSort();
453
+ return this.vent.trigger('table:update', this.config());
575
454
  }
576
- },
577
-
578
- firstRecord : function(data) {
579
- return data.length ? ((data.page || 1) - 1) * data.pageSize + 1 : 0;
580
- },
581
-
582
- lastRecord : function(data) {
583
- return data.length ? this.firstRecord(data) + data.length - 1 : 0;
584
455
  }
585
- });
586
-
587
- Tableling.Plain.PageView = Tableling.Plain.Table.prototype.pageView = Tableling.Module.extend({
456
+
457
+ if (!ev.shiftKey) {
458
+ this.sort.length = 0;
459
+ }
460
+
461
+ this.sort.push(field + ' asc');
462
+
463
+ this.showSort();
464
+
465
+ this.vent.trigger('table:update', this.config());
466
+ },
467
+
468
+ setSort: function(config) {
469
+ if (config && config.sort) {
470
+ this.sort = config.sort.slice(0);
471
+ this.showSort();
472
+ }
473
+ },
474
+
475
+ showSort: function() {
476
+
477
+ this.$el.find('thead th.sorting, thead th.sorting-asc, thead th.sorting-desc').removeClass('sorting sorting-asc sorting-desc').addClass('sorting');
478
+
479
+ for (var i = 0; i < this.sort.length; i++) {
480
+
481
+ var parts = this.sort[i].split(' ');
482
+ var name = parts[0];
483
+ var direction = parts[1];
588
484
 
589
- template : _.template('<ul class="pagination"><li class="first"><a href="#">&lt;&lt;</a></li><li class="previous"><a href="#">&lt;</a></li><li class="next"><a href="#">&gt;</a></li><li class="last"><a href="#">&gt;&gt;</a></li></ul>'),
590
- pageTemplate : _.template('<li class="page"><a href="#"><%- number %></a></li>'),
591
-
592
- ui : {
593
- first : '.first',
594
- previous : '.previous',
595
- next : '.next',
596
- last : '.last'
597
- },
598
-
599
- events : {
600
- 'click .first:not(.disabled)' : 'goToFirstPage',
601
- 'click .previous:not(.disabled)' : 'goToPreviousPage',
602
- 'click .page:not(.disabled)' : 'goToPage',
603
- 'click .next:not(.disabled)' : 'goToNextPage',
604
- 'click .last:not(.disabled)' : 'goToLastPage'
605
- },
606
-
607
- refresh : function(data) {
608
- this.$el.find('.page').remove();
609
- if (!data || !data.length) {
610
- this.ui.first.addClass('disabled');
611
- this.ui.previous.addClass('disabled');
612
- this.ui.next.addClass('disabled');
613
- this.ui.last.addClass('disabled');
614
- } else {
615
- this.data = data;
616
- this.enable(this.ui.first, this.getPage(data) > 1);
617
- this.enable(this.ui.previous, this.getPage(data) > 1);
618
- this.setupPages();
619
- this.enable(this.ui.next, this.getPage(data) < this.numberOfPages(data));
620
- this.enable(this.ui.last, this.getPage(data) < this.numberOfPages(data));
621
- }
622
- },
623
-
624
- setupPages : function() {
625
-
626
- var page = this.getPage(this.data);
627
- var total = this.numberOfPages();
628
-
629
- var first = page - 2;
630
- if (total - first < 4) {
631
- first = total - 4;
485
+ field = this.$el.find('thead [data-field="' + name + '"]');
486
+ if (!field.length) {
487
+ field = this.$el.find('thead th:contains("' + name + '")');
632
488
  }
633
-
634
- if (first < 1) {
635
- first = 1;
636
- }
637
-
638
- var n = 5;
639
- if (first + n - 1 > total) {
640
- n = total - first + 1;
641
- }
642
-
643
- _.times(n, function(i) {
644
- $(this.pageTemplate({ number : first + i })).insertBefore(this.ui.next);
645
- }, this);
646
-
647
- var i = page - first;
648
- this.$el.find('.page').slice(i, i + 1).addClass('disabled');
649
- },
650
-
651
- enable : function(el, enabled) {
652
- el.removeClass('disabled');
653
- if (!enabled) {
654
- el.addClass('disabled');
489
+
490
+ if (field.length) {
491
+ field.removeClass('sorting').addClass(direction == 'desc' ? 'sorting-desc' : 'sorting-asc');
655
492
  }
656
- },
657
-
658
- numberOfPages : function() {
659
- return Math.ceil(this.data.total / this.data.pageSize);
660
- },
661
-
662
- goToFirstPage : function(e) {
663
- e.preventDefault();
664
- this.goToPageNumber(1);
665
- },
666
-
667
- goToPreviousPage : function(e) {
668
- e.preventDefault();
669
- this.goToPageNumber(this.getPage(this.data) - 1);
670
- },
671
-
672
- goToPage : function(e) {
673
- e.preventDefault();
674
- this.goToPageNumber(parseInt($(e.target).text(), 10));
675
- },
676
-
677
- goToNextPage : function(e) {
678
- e.preventDefault();
679
- this.goToPageNumber(this.getPage(this.data) + 1);
680
- },
681
-
682
- goToLastPage : function(e) {
683
- e.preventDefault();
684
- this.goToPageNumber(this.numberOfPages());
685
- },
686
-
687
- goToPageNumber : function(n) {
688
- this.vent.trigger('table:update', { page : n });
689
- },
690
-
691
- getPage : function(data) {
692
- return data.page || 1;
693
493
  }
694
- });
695
-
696
- Tableling.Bootstrap = {};
697
-
698
- Tableling.Bootstrap.Table = Tableling.Plain.Table.extend({
699
- template : _.template('<div class="header clearfix"><div class="pageSize pull-left" /><div class="quickSearch pull-right" /></div><div class="table" /><div class="footer clearfix"><div class="info pull-left" /><div class="page pull-right" /></div>')
700
- });
701
-
702
- Tableling.Bootstrap.TableView = Tableling.Plain.TableView.extend({});
703
-
704
- Tableling.Bootstrap.PageSizeView = Tableling.Bootstrap.Table.prototype.pageSizeView = Tableling.Plain.PageSizeView.extend({
705
-
706
- tagName : 'form',
707
- className : 'form-inline',
708
- attributes : {
709
- role : 'form'
710
- },
711
- template : function(data) {
712
- return _.template('<div class="formGroup"><select name="pageSize" class="form-control"><option>5</option><option>10</option><option>15</option></select> <%- entries %></div>', data);
494
+ },
495
+
496
+ config: function() {
497
+ return {
498
+ page: 1,
499
+ sort: this.sortConfig()
500
+ };
501
+ },
502
+
503
+ sortConfig: function() {
504
+ return this.sort.length ? this.sort : null;
505
+ },
506
+
507
+ fieldName: function(el) {
508
+ return el.data('field') || el.text();
509
+ }
510
+ });
511
+
512
+ Tableling.Plain.PageSizeView = Tableling.Plain.Table.prototype.pageSizeView = Tableling.FieldModule.extend({
513
+
514
+ // TODO: update current page intelligently
515
+ name: 'pageSize',
516
+ template: function(data) {
517
+ return _.template('<select name="pageSize" /> <%- entries %>', data);
518
+ },
519
+
520
+ i18n: {
521
+ entries: 'entries per page'
522
+ },
523
+ sizes: [ 10, 15, 20, 25, 50 ],
524
+
525
+ ui: {
526
+ field: 'select'
527
+ },
528
+
529
+ initialize: function(options) {
530
+ this.sizes = _.clone(options.sizes || this.sizes);
531
+ Tableling.FieldModule.prototype.initialize.call(this, options);
532
+ },
533
+
534
+ onRender: function() {
535
+ this.ui.field.empty();
536
+ _.each(this.sizes, _.bind(this.addSize, this));
537
+ },
538
+
539
+ addSize: function(size) {
540
+ $('<option />').text(size).appendTo(this.ui.field);
541
+ },
542
+
543
+ setupValue: function(value) {
544
+ if (value) {
545
+ Tableling.FieldModule.prototype.setupValue.apply(this, Array.prototype.slice.call(arguments));
713
546
  }
714
- });
715
-
716
- Tableling.Bootstrap.QuickSearchView = Tableling.Bootstrap.Table.prototype.quickSearchView = Tableling.Plain.QuickSearchView.extend({
717
-
718
- tagName : 'form',
719
- className : 'form-inline',
720
- attributes : {
721
- role : 'form'
722
- },
723
- template : function(data) {
724
- return _.template('<div class="formGroup"><input type="text" name="quickSearch" class="form-control" placeholder="<%- quickSearch %>" /></div>', data);
547
+ },
548
+
549
+ config: function() {
550
+ var config = Tableling.FieldModule.prototype.config.call(this);
551
+ config.page = 1;
552
+ return config;
553
+ }
554
+ });
555
+
556
+ Tableling.Plain.QuickSearchView = Tableling.Plain.Table.prototype.quickSearchView = Tableling.FieldModule.extend({
557
+
558
+ name: 'quickSearch',
559
+ template: function(data) {
560
+ return _.template('<input type="text" name="quickSearch" placeholder="<%- quickSearch %>" />', data);
561
+ },
562
+
563
+ i18n: {
564
+ quickSearch: 'Quick search...'
565
+ },
566
+
567
+ config: function() {
568
+ var config = Tableling.FieldModule.prototype.config.call(this);
569
+ config.page = 1;
570
+ return config;
571
+ }
572
+ });
573
+
574
+ Tableling.Plain.InfoView = Tableling.Plain.Table.prototype.infoView = Tableling.Module.extend({
575
+
576
+ template: function(data) {
577
+ return _.template(data.template, {
578
+ first: '<span class="first">0</span>',
579
+ last: '<span class="last">0</span>',
580
+ total: '<span class="total">0</span>'
581
+ });
582
+ },
583
+
584
+ i18n: {
585
+ template: 'Showing <%= first %> to <%= last %> of <%= total %> entries'
586
+ },
587
+
588
+ ui: {
589
+ first: '.first',
590
+ last: '.last',
591
+ total: '.total'
592
+ },
593
+
594
+ refresh: function(data) {
595
+ if (data) {
596
+ this.ui.first.text(this.firstRecord(data));
597
+ this.ui.last.text(this.lastRecord(data));
598
+ this.ui.total.text(data.total);
599
+ }
600
+ },
601
+
602
+ firstRecord: function(data) {
603
+ return data.length ? ((data.page || 1) - 1) * data.pageSize + 1 : 0;
604
+ },
605
+
606
+ lastRecord: function(data) {
607
+ return data.length ? this.firstRecord(data) + data.length - 1 : 0;
608
+ }
609
+ });
610
+
611
+ Tableling.Plain.PageView = Tableling.Plain.Table.prototype.pageView = Tableling.Module.extend({
612
+
613
+ template: _.template('<ul class="pagination"><li class="first"><a href="#">&lt;&lt;</a></li><li class="previous"><a href="#">&lt;</a></li><li class="next"><a href="#">&gt;</a></li><li class="last"><a href="#">&gt;&gt;</a></li></ul>'),
614
+ pageTemplate: _.template('<li class="page"><a href="#"><%- number %></a></li>'),
615
+
616
+ ui: {
617
+ first: '.first',
618
+ previous: '.previous',
619
+ next: '.next',
620
+ last: '.last'
621
+ },
622
+
623
+ events: {
624
+ 'click .first:not(.disabled)': 'goToFirstPage',
625
+ 'click .previous:not(.disabled)': 'goToPreviousPage',
626
+ 'click .page:not(.disabled)': 'goToPage',
627
+ 'click .next:not(.disabled)': 'goToNextPage',
628
+ 'click .last:not(.disabled)': 'goToLastPage'
629
+ },
630
+
631
+ refresh: function(data) {
632
+ this.$el.find('.page').remove();
633
+ if (!data || !data.length) {
634
+ this.ui.first.addClass('disabled');
635
+ this.ui.previous.addClass('disabled');
636
+ this.ui.next.addClass('disabled');
637
+ this.ui.last.addClass('disabled');
638
+ } else {
639
+ this.data = data;
640
+ this.enable(this.ui.first, this.getPage(data) > 1);
641
+ this.enable(this.ui.previous, this.getPage(data) > 1);
642
+ this.setupPages();
643
+ this.enable(this.ui.next, this.getPage(data) < this.numberOfPages(data));
644
+ this.enable(this.ui.last, this.getPage(data) < this.numberOfPages(data));
645
+ }
646
+ },
647
+
648
+ setupPages: function() {
649
+
650
+ var page = this.getPage(this.data);
651
+ var total = this.numberOfPages();
652
+
653
+ var first = page - 2;
654
+ if (total - first < 4) {
655
+ first = total - 4;
656
+ }
657
+
658
+ if (first < 1) {
659
+ first = 1;
725
660
  }
726
- });
727
-
728
- Tableling.Bootstrap.InfoView = Tableling.Bootstrap.Table.prototype.infoView = Tableling.Plain.InfoView.extend({});
729
-
730
- Tableling.Bootstrap.PageView = Tableling.Bootstrap.Table.prototype.pageView = Tableling.Plain.PageView.extend({});
731
-
661
+
662
+ var n = 5;
663
+ if (first + n - 1 > total) {
664
+ n = total - first + 1;
665
+ }
666
+
667
+ _.times(n, function(i) {
668
+ $(this.pageTemplate({ number: first + i })).insertBefore(this.ui.next);
669
+ }, this);
670
+
671
+ var i = page - first;
672
+ this.$el.find('.page').slice(i, i + 1).addClass('disabled');
673
+ },
674
+
675
+ enable: function(el, enabled) {
676
+ el.removeClass('disabled');
677
+ if (!enabled) {
678
+ el.addClass('disabled');
679
+ }
680
+ },
681
+
682
+ numberOfPages: function() {
683
+ return Math.ceil(this.data.total / this.data.pageSize);
684
+ },
685
+
686
+ goToFirstPage: function(e) {
687
+ e.preventDefault();
688
+ this.goToPageNumber(1);
689
+ },
690
+
691
+ goToPreviousPage: function(e) {
692
+ e.preventDefault();
693
+ this.goToPageNumber(this.getPage(this.data) - 1);
694
+ },
695
+
696
+ goToPage: function(e) {
697
+ e.preventDefault();
698
+ this.goToPageNumber(parseInt($(e.target).text(), 10));
699
+ },
700
+
701
+ goToNextPage: function(e) {
702
+ e.preventDefault();
703
+ this.goToPageNumber(this.getPage(this.data) + 1);
704
+ },
705
+
706
+ goToLastPage: function(e) {
707
+ e.preventDefault();
708
+ this.goToPageNumber(this.numberOfPages());
709
+ },
710
+
711
+ goToPageNumber: function(n) {
712
+ this.vent.trigger('table:update', { page: n });
713
+ },
714
+
715
+ getPage: function(data) {
716
+ return data.page || 1;
717
+ }
718
+ });
719
+
720
+ Tableling.Bootstrap = {};
721
+
722
+ Tableling.Bootstrap.Table = Tableling.Plain.Table.extend({
723
+ template: _.template('<div class="header clearfix"><div class="pageSize pull-left" /><div class="quickSearch pull-right" /></div><div class="table" /><div class="footer clearfix"><div class="info pull-left" /><div class="page pull-right" /></div>')
724
+ });
725
+
726
+ Tableling.Bootstrap.TableView = Tableling.Plain.TableView.extend({});
727
+
728
+ Tableling.Bootstrap.PageSizeView = Tableling.Bootstrap.Table.prototype.pageSizeView = Tableling.Plain.PageSizeView.extend({
729
+
730
+ tagName: 'form',
731
+ className: 'form-inline',
732
+ attributes: {
733
+ role: 'form'
734
+ },
735
+ template: function(data) {
736
+ return _.template('<div class="formGroup"><select name="pageSize" class="form-control"><option>5</option><option>10</option><option>15</option></select> <%- entries %></div>', data);
737
+ }
738
+ });
739
+
740
+ Tableling.Bootstrap.QuickSearchView = Tableling.Bootstrap.Table.prototype.quickSearchView = Tableling.Plain.QuickSearchView.extend({
741
+
742
+ tagName: 'form',
743
+ className: 'form-inline',
744
+ attributes: {
745
+ role: 'form'
746
+ },
747
+ template: function(data) {
748
+ return _.template('<div class="formGroup"><input type="text" name="quickSearch" class="form-control" placeholder="<%- quickSearch %>" /></div>', data);
749
+ }
750
+ });
751
+
752
+ Tableling.Bootstrap.InfoView = Tableling.Bootstrap.Table.prototype.infoView = Tableling.Plain.InfoView.extend({});
753
+
754
+ Tableling.Bootstrap.PageView = Tableling.Bootstrap.Table.prototype.pageView = Tableling.Plain.PageView.extend({});
732
755
 
733
756
  return Tableling;
734
757
 
735
- })(Backbone, _, $ || window.jQuery || window.Zepto || window.ender);
758
+ })(Backbone, _, $ || window.jQuery || window.Zepto || window.ender);