tableling-rails 0.0.21 → 0.0.22

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