/ajax/libs/backbone.paginator/0.8/backbone.paginator.js
JavaScript | 1050 lines | 695 code | 188 blank | 167 comment | 163 complexity | d95877dc0b0f20821ba60db3cfd7d176 MD5 | raw file
1/*! backbone.paginator - v0.8.0 - 6/14/2013
2* http://github.com/addyosmani/backbone.paginator
3* Copyright (c) 2013 Addy Osmani; Licensed MIT */
4/*globals Backbone:true, _:true, jQuery:true*/
5Backbone.Paginator = (function ( Backbone, _, $ ) {
6 "use strict";
7
8
9 var bbVer = _.map(Backbone.VERSION.split('.'), function(digit) {
10 return parseInt(digit, 10);
11 });
12
13 var Paginator = {};
14 Paginator.version = "0.8.0";
15
16 // @name: clientPager
17 //
18 // @tagline: Paginator for client-side data
19 //
20 // @description:
21 // This paginator is responsible for providing pagination
22 // and sort capabilities for a single payload of data
23 // we wish to paginate by the UI for easier browsering.
24 //
25 Paginator.clientPager = Backbone.Collection.extend({
26
27 // DEFAULTS FOR SORTING & FILTERING
28 useDiacriticsPlugin: true, // use diacritics plugin if available
29 useLevenshteinPlugin: true, // use levenshtein plugin if available
30 sortColumn: "",
31 sortDirection: "desc",
32 lastSortColumn: "",
33 fieldFilterRules: [],
34 lastFieldFilterRules: [],
35 filterFields: "",
36 filterExpression: "",
37 lastFilterExpression: "",
38
39 //DEFAULT PAGINATOR UI VALUES
40 defaults_ui: {
41 firstPage: 0,
42 currentPage: 1,
43 perPage: 5,
44 totalPages: 10,
45 pagesInRange: 4
46 },
47
48 // Default values used when sorting and/or filtering.
49 initialize: function(){
50 //LISTEN FOR ADD & REMOVE EVENTS THEN REMOVE MODELS FROM ORGINAL MODELS
51 this.on('add', this.addModel, this);
52 this.on('remove', this.removeModel, this);
53
54 // SET DEFAULT VALUES (ALLOWS YOU TO POPULATE PAGINATOR MAUNALLY)
55 this.setDefaults();
56 },
57
58
59 setDefaults: function() {
60 // SET DEFAULT UI SETTINGS
61 var options = _.defaults(this.paginator_ui, this.defaults_ui);
62
63 //UPDATE GLOBAL UI SETTINGS
64 _.defaults(this, options);
65 },
66
67 addModel: function(model) {
68 this.origModels.push(model);
69 },
70
71 removeModel: function(model) {
72 var index = _.indexOf(this.origModels, model);
73
74 this.origModels.splice(index, 1);
75 },
76
77 sync: function ( method, model, options ) {
78 var self = this;
79
80 // SET DEFAULT VALUES
81 this.setDefaults();
82
83 // Some values could be functions, let's make sure
84 // to change their scope too and run them
85 var queryAttributes = {};
86 _.each(_.result(self, "server_api"), function(value, key){
87 if( _.isFunction(value) ) {
88 value = _.bind(value, self);
89 value = value();
90 }
91 queryAttributes[key] = value;
92 });
93
94 var queryOptions = _.clone(self.paginator_core);
95 _.each(queryOptions, function(value, key){
96 if( _.isFunction(value) ) {
97 value = _.bind(value, self);
98 value = value();
99 }
100 queryOptions[key] = value;
101 });
102
103 // Create default values if no others are specified
104 queryOptions = _.defaults(queryOptions, {
105 timeout: 25000,
106 cache: false,
107 type: 'GET',
108 dataType: 'jsonp'
109 });
110
111 queryOptions = _.extend(queryOptions, {
112 data: decodeURIComponent($.param(queryAttributes)),
113 processData: false,
114 url: _.result(queryOptions, 'url')
115 }, options);
116
117 var promiseSuccessFormat = !(bbVer[0] === 0 &&
118 bbVer[1] === 9 &&
119 bbVer[2] === 10);
120
121 var success = queryOptions.success;
122 queryOptions.success = function ( resp, status, xhr ) {
123 if ( success ) {
124 // This is to keep compatibility with Backbone 0.9.10
125 if (promiseSuccessFormat) {
126 success( resp, status, xhr );
127 } else {
128 success( model, resp, queryOptions );
129 }
130 }
131 if ( model && model.trigger ) {
132 model.trigger( 'sync', model, resp, queryOptions );
133 }
134 };
135
136 var error = queryOptions.error;
137 queryOptions.error = function ( xhr ) {
138 if ( error ) {
139 error( model, xhr, queryOptions );
140 }
141 if ( model && model.trigger ) {
142 model.trigger( 'error', model, xhr, queryOptions );
143 }
144 };
145
146 var xhr = queryOptions.xhr = Backbone.ajax( queryOptions );
147 if ( model && model.trigger ) {
148 model.trigger('request', model, xhr, queryOptions);
149 }
150 return xhr;
151 },
152
153 nextPage: function (options) {
154 if(this.currentPage < this.information.totalPages) {
155 this.currentPage = ++this.currentPage;
156 this.pager(options);
157 }
158 },
159
160 previousPage: function (options) {
161 if(this.currentPage > 1) {
162 this.currentPage = --this.currentPage;
163 this.pager(options);
164 }
165 },
166
167 goTo: function ( page, options ) {
168 if(page !== undefined){
169 this.currentPage = parseInt(page, 10);
170 this.pager(options);
171 }
172 },
173
174 howManyPer: function ( perPage ) {
175 if(perPage !== undefined){
176 var lastPerPage = this.perPage;
177 this.perPage = parseInt(perPage, 10);
178 this.currentPage = Math.ceil( ( lastPerPage * ( this.currentPage - 1 ) + 1 ) / perPage);
179 this.pager();
180 }
181 },
182
183
184 // setSort is used to sort the current model. After
185 // passing 'column', which is the model's field you want
186 // to filter and 'direction', which is the direction
187 // desired for the ordering ('asc' or 'desc'), pager()
188 // and info() will be called automatically.
189 setSort: function ( column, direction ) {
190 if(column !== undefined && direction !== undefined){
191 this.lastSortColumn = this.sortColumn;
192 this.sortColumn = column;
193 this.sortDirection = direction;
194 this.pager();
195 this.info();
196 }
197 },
198
199 // setFieldFilter is used to filter each value of each model
200 // according to `rules` that you pass as argument.
201 // Example: You have a collection of books with 'release year' and 'author'.
202 // You can filter only the books that were released between 1999 and 2003
203 // And then you can add another `rule` that will filter those books only to
204 // authors who's name start with 'A'.
205 setFieldFilter: function ( fieldFilterRules ) {
206 if( !_.isEmpty( fieldFilterRules ) ) {
207 this.lastFieldFilterRules = this.fieldFilterRules;
208 this.fieldFilterRules = fieldFilterRules;
209 this.pager();
210 this.info();
211 // if all the filters are removed, we should save the last filter
212 // and then let the list reset to it's original state.
213 } else {
214 this.lastFieldFilterRules = this.fieldFilterRules;
215 this.fieldFilterRules = '';
216 this.pager();
217 this.info();
218 }
219 },
220
221 // doFakeFieldFilter can be used to get the number of models that will remain
222 // after calling setFieldFilter with a filter rule(s)
223 doFakeFieldFilter: function ( rules ) {
224 if( !_.isEmpty( rules ) ) {
225 var testModels = this.origModels;
226 if (testModels === undefined) {
227 testModels = this.models;
228 }
229
230 testModels = this._fieldFilter(testModels, rules);
231
232 // To comply with current behavior, also filter by any previously defined setFilter rules.
233 if ( this.filterExpression !== "" ) {
234 testModels = this._filter(testModels, this.filterFields, this.filterExpression);
235 }
236
237 // Return size
238 return testModels.length;
239 }
240
241 },
242
243 // setFilter is used to filter the current model. After
244 // passing 'fields', which can be a string referring to
245 // the model's field, an array of strings representing
246 // each of the model's fields or an object with the name
247 // of the model's field(s) and comparing options (see docs)
248 // you wish to filter by and
249 // 'filter', which is the word or words you wish to
250 // filter by, pager() and info() will be called automatically.
251 setFilter: function ( fields, filter ) {
252 if( fields !== undefined && filter !== undefined ){
253 this.filterFields = fields;
254 this.lastFilterExpression = this.filterExpression;
255 this.filterExpression = filter;
256 this.pager();
257 this.info();
258 }
259 },
260
261 // doFakeFilter can be used to get the number of models that will
262 // remain after calling setFilter with a `fields` and `filter` args.
263 doFakeFilter: function ( fields, filter ) {
264 if( fields !== undefined && filter !== undefined ){
265 var testModels = this.origModels;
266 if (testModels === undefined) {
267 testModels = this.models;
268 }
269
270 // To comply with current behavior, first filter by any previously defined setFieldFilter rules.
271 if ( !_.isEmpty( this.fieldFilterRules ) ) {
272 testModels = this._fieldFilter(testModels, this.fieldFilterRules);
273 }
274
275 testModels = this._filter(testModels, fields, filter);
276
277 // Return size
278 return testModels.length;
279 }
280 },
281
282
283 // pager is used to sort, filter and show the data
284 // you expect the library to display.
285 pager: function (options) {
286 var self = this,
287 disp = this.perPage,
288 start = (self.currentPage - 1) * disp,
289 stop = start + disp;
290 // Saving the original models collection is important
291 // as we could need to sort or filter, and we don't want
292 // to loose the data we fetched from the server.
293 if (self.origModels === undefined) {
294 self.origModels = self.models;
295 }
296
297 self.models = self.origModels.slice();
298
299 // Check if sorting was set using setSort.
300 if ( this.sortColumn !== "" ) {
301 self.models = self._sort(self.models, this.sortColumn, this.sortDirection);
302 }
303
304 // Check if field-filtering was set using setFieldFilter
305 if ( !_.isEmpty( this.fieldFilterRules ) ) {
306 self.models = self._fieldFilter(self.models, this.fieldFilterRules);
307 }
308
309 // Check if filtering was set using setFilter.
310 if ( this.filterExpression !== "" ) {
311 self.models = self._filter(self.models, this.filterFields, this.filterExpression);
312 }
313
314 // If the sorting or the filtering was changed go to the first page
315 if ( this.lastSortColumn !== this.sortColumn || this.lastFilterExpression !== this.filterExpression || !_.isEqual(this.fieldFilterRules, this.lastFieldFilterRules) ) {
316 start = 0;
317 stop = start + disp;
318 self.currentPage = 1;
319
320 this.lastSortColumn = this.sortColumn;
321 this.lastFieldFilterRules = this.fieldFilterRules;
322 this.lastFilterExpression = this.filterExpression;
323 }
324
325 // We need to save the sorted and filtered models collection
326 // because we'll use that sorted and filtered collection in info().
327 self.sortedAndFilteredModels = self.models.slice();
328 self.info();
329 self.reset(self.models.slice(start, stop));
330
331 // This is somewhat of a hack to get all the nextPage, prevPage, and goTo methods
332 // to work with a success callback (as in the requestPager). Realistically there is no failure case here,
333 // but maybe we could catch exception and trigger a failure callback?
334 _.result(options, 'success');
335 },
336
337 // The actual place where the collection is sorted.
338 // Check setSort for arguments explicacion.
339 _sort: function ( models, sort, direction ) {
340 models = models.sort(function (a, b) {
341 var ac = a.get(sort),
342 bc = b.get(sort);
343
344 if ( _.isUndefined(ac) || _.isUndefined(bc) || ac === null || bc === null ) {
345 return 0;
346 } else {
347 /* Make sure that both ac and bc are lowercase strings.
348 * .toString() first so we don't have to worry if ac or bc
349 * have other String-only methods.
350 */
351 ac = ac.toString().toLowerCase();
352 bc = bc.toString().toLowerCase();
353 }
354
355 if (direction === 'desc') {
356
357 // We need to know if there aren't any non-number characters
358 // and that there are numbers-only characters and maybe a dot
359 // if we have a float.
360 // Oh, also a '-' for negative numbers!
361 if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) &&
362 (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))){
363
364 if( (ac - 0) < (bc - 0) ) {
365 return 1;
366 }
367 if( (ac - 0) > (bc - 0) ) {
368 return -1;
369 }
370 } else {
371 if (ac < bc) {
372 return 1;
373 }
374 if (ac > bc) {
375 return -1;
376 }
377 }
378
379 } else {
380
381 //Same as the regexp check in the 'if' part.
382 if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) &&
383 (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))){
384 if( (ac - 0) < (bc - 0) ) {
385 return -1;
386 }
387 if( (ac - 0) > (bc - 0) ) {
388 return 1;
389 }
390 } else {
391 if (ac < bc) {
392 return -1;
393 }
394 if (ac > bc) {
395 return 1;
396 }
397 }
398
399 }
400
401 if (a.cid && b.cid){
402 var aId = a.cid,
403 bId = b.cid;
404
405 if (aId < bId) {
406 return -1;
407 }
408 if (aId > bId) {
409 return 1;
410 }
411 }
412
413 return 0;
414 });
415
416 return models;
417 },
418
419 // The actual place where the collection is field-filtered.
420 // Check setFieldFilter for arguments explicacion.
421 _fieldFilter: function( models, rules ) {
422
423 // Check if there are any rules
424 if ( _.isEmpty(rules) ) {
425 return models;
426 }
427
428 var filteredModels = [];
429
430 // Iterate over each rule
431 _.each(models, function(model){
432
433 var should_push = true;
434
435 // Apply each rule to each model in the collection
436 _.each(rules, function(rule){
437
438 // Don't go inside the switch if we're already sure that the model won't be included in the results
439 if( !should_push ){
440 return false;
441 }
442
443 should_push = false;
444
445 // The field's value will be passed to a custom function, which should
446 // return true (if model should be included) or false (model should be ignored)
447 if(rule.type === "function"){
448 var f = _.wrap(rule.value, function(func){
449 return func( model.get(rule.field) );
450 });
451 if( f() ){
452 should_push = true;
453 }
454
455 // The field's value is required to be non-empty
456 }else if(rule.type === "required"){
457 if( !_.isEmpty( model.get(rule.field).toString() ) ) {
458 should_push = true;
459 }
460
461 // The field's value is required to be greater tan N (numbers only)
462 }else if(rule.type === "min"){
463 if( !_.isNaN( Number( model.get(rule.field) ) ) &&
464 !_.isNaN( Number( rule.value ) ) &&
465 Number( model.get(rule.field) ) >= Number( rule.value ) ) {
466 should_push = true;
467 }
468
469 // The field's value is required to be smaller tan N (numbers only)
470 }else if(rule.type === "max"){
471 if( !_.isNaN( Number( model.get(rule.field) ) ) &&
472 !_.isNaN( Number( rule.value ) ) &&
473 Number( model.get(rule.field) ) <= Number( rule.value ) ) {
474 should_push = true;
475 }
476
477 // The field's value is required to be between N and M (numbers only)
478 }else if(rule.type === "range"){
479 if( !_.isNaN( Number( model.get(rule.field) ) ) &&
480 _.isObject( rule.value ) &&
481 !_.isNaN( Number( rule.value.min ) ) &&
482 !_.isNaN( Number( rule.value.max ) ) &&
483 Number( model.get(rule.field) ) >= Number( rule.value.min ) &&
484 Number( model.get(rule.field) ) <= Number( rule.value.max ) ) {
485 should_push = true;
486 }
487
488 // The field's value is required to be more than N chars long
489 }else if(rule.type === "minLength"){
490 if( model.get(rule.field).toString().length >= rule.value ) {
491 should_push = true;
492 }
493
494 // The field's value is required to be no more than N chars long
495 }else if(rule.type === "maxLength"){
496 if( model.get(rule.field).toString().length <= rule.value ) {
497 should_push = true;
498 }
499
500 // The field's value is required to be more than N chars long and no more than M chars long
501 }else if(rule.type === "rangeLength"){
502 if( _.isObject( rule.value ) &&
503 !_.isNaN( Number( rule.value.min ) ) &&
504 !_.isNaN( Number( rule.value.max ) ) &&
505 model.get(rule.field).toString().length >= rule.value.min &&
506 model.get(rule.field).toString().length <= rule.value.max ) {
507 should_push = true;
508 }
509
510 // The field's value is required to be equal to one of the values in rules.value
511 }else if(rule.type === "oneOf"){
512 if( _.isArray( rule.value ) &&
513 _.include( rule.value, model.get(rule.field) ) ) {
514 should_push = true;
515 }
516
517 // The field's value is required to be equal to the value in rules.value
518 }else if(rule.type === "equalTo"){
519 if( rule.value === model.get(rule.field) ) {
520 should_push = true;
521 }
522
523 }else if(rule.type === "containsAllOf"){
524 if( _.isArray( rule.value ) &&
525 _.isArray(model.get(rule.field)) &&
526 _.intersection( rule.value, model.get(rule.field)).length === rule.value.length) {
527 should_push = true;
528 }
529
530 // The field's value is required to match the regular expression
531 }else if(rule.type === "pattern"){
532 if( model.get(rule.field).toString().match(rule.value) ) {
533 should_push = true;
534 }
535
536 //Unknown type
537 }else{
538 should_push = false;
539 }
540
541 });
542
543 if( should_push ){
544 filteredModels.push(model);
545 }
546
547 });
548
549 return filteredModels;
550 },
551
552 // The actual place where the collection is filtered.
553 // Check setFilter for arguments explicacion.
554 _filter: function ( models, fields, filter ) {
555
556 // For example, if you had a data model containing cars like { color: '', description: '', hp: '' },
557 // your fields was set to ['color', 'description', 'hp'] and your filter was set
558 // to "Black Mustang 300", the word "Black" will match all the cars that have black color, then
559 // "Mustang" in the description and then the HP in the 'hp' field.
560 // NOTE: "Black Musta 300" will return the same as "Black Mustang 300"
561
562 // We accept fields to be a string, an array or an object
563 // but if string or array is passed we need to convert it
564 // to an object.
565
566 var self = this;
567
568 var obj_fields = {};
569
570 if( _.isString( fields ) ) {
571 obj_fields[fields] = {cmp_method: 'regexp'};
572 }else if( _.isArray( fields ) ) {
573 _.each(fields, function(field){
574 obj_fields[field] = {cmp_method: 'regexp'};
575 });
576 }else{
577 _.each(fields, function( cmp_opts, field ) {
578 obj_fields[field] = _.defaults(cmp_opts, { cmp_method: 'regexp' });
579 });
580 }
581
582 fields = obj_fields;
583
584 //Remove diacritic characters if diacritic plugin is loaded
585 if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
586 filter = Backbone.Paginator.removeDiacritics(filter);
587 }
588
589 // 'filter' can be only a string.
590 // If 'filter' is string we need to convert it to
591 // a regular expression.
592 // For example, if 'filter' is 'black dog' we need
593 // to find every single word, remove duplicated ones (if any)
594 // and transform the result to '(black|dog)'
595 if( filter === '' || !_.isString(filter) ) {
596 return models;
597 } else {
598 var words = _.map(filter.match(/\w+/ig), function(element) { return element.toLowerCase(); });
599 var pattern = "(" + _.uniq(words).join("|") + ")";
600 var regexp = new RegExp(pattern, "igm");
601 }
602
603 var filteredModels = [];
604
605 // We need to iterate over each model
606 _.each( models, function( model ) {
607
608 var matchesPerModel = [];
609
610 // and over each field of each model
611 _.each( fields, function( cmp_opts, field ) {
612
613 var value = model.get( field );
614
615 if( value ) {
616
617 // The regular expression we created earlier let's us detect if a
618 // given string contains each and all of the words in the regular expression
619 // or not, but in both cases match() will return an array containing all
620 // the words it matched.
621 var matchesPerField = [];
622
623 if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
624 value = Backbone.Paginator.removeDiacritics(value.toString());
625 }else{
626 value = value.toString();
627 }
628
629 // Levenshtein cmp
630 if( cmp_opts.cmp_method === 'levenshtein' && _.has(Backbone.Paginator, 'levenshtein') && self.useLevenshteinPlugin ) {
631 var distance = Backbone.Paginator.levenshtein(value, filter);
632
633 _.defaults(cmp_opts, { max_distance: 0 });
634
635 if( distance <= cmp_opts.max_distance ) {
636 matchesPerField = _.uniq(words);
637 }
638
639 // Default (RegExp) cmp
640 }else{
641 matchesPerField = value.match( regexp );
642 }
643
644 matchesPerField = _.map(matchesPerField, function(match) {
645 return match.toString().toLowerCase();
646 });
647
648 _.each(matchesPerField, function(match){
649 matchesPerModel.push(match);
650 });
651
652 }
653
654 });
655
656 // We just need to check if the returned array contains all the words in our
657 // regex, and if it does, it means that we have a match, so we should save it.
658 matchesPerModel = _.uniq( _.without(matchesPerModel, "") );
659
660 if( _.isEmpty( _.difference(words, matchesPerModel) ) ) {
661 filteredModels.push(model);
662 }
663
664 });
665
666 return filteredModels;
667 },
668
669 // You shouldn't need to call info() as this method is used to
670 // calculate internal data as first/prev/next/last page...
671 info: function () {
672 var self = this,
673 info = {},
674 totalRecords = (self.sortedAndFilteredModels) ? self.sortedAndFilteredModels.length : self.length,
675 totalPages = Math.ceil(totalRecords / self.perPage);
676
677 info = {
678 totalUnfilteredRecords: self.origModels.length,
679 totalRecords: totalRecords,
680 currentPage: self.currentPage,
681 perPage: this.perPage,
682 totalPages: totalPages,
683 lastPage: totalPages,
684 previous: false,
685 next: false,
686 startRecord: totalRecords === 0 ? 0 : (self.currentPage - 1) * this.perPage + 1,
687 endRecord: Math.min(totalRecords, self.currentPage * this.perPage)
688 };
689
690 if (self.currentPage > 1) {
691 info.previous = self.currentPage - 1;
692 }
693
694 if (self.currentPage < info.totalPages) {
695 info.next = self.currentPage + 1;
696 }
697
698 info.pageSet = self.setPagination(info);
699
700 self.information = info;
701 return info;
702 },
703
704
705 // setPagination also is an internal function that shouldn't be called directly.
706 // It will create an array containing the pages right before and right after the
707 // actual page.
708 setPagination: function ( info ) {
709
710 var pages = [], i = 0, l = 0;
711
712 // How many adjacent pages should be shown on each side?
713 var ADJACENTx2 = this.pagesInRange * 2,
714 LASTPAGE = Math.ceil(info.totalRecords / info.perPage);
715
716 if (LASTPAGE > 1) {
717
718 // not enough pages to bother breaking it up
719 if (LASTPAGE <= (1 + ADJACENTx2)) {
720 for (i = 1, l = LASTPAGE; i <= l; i++) {
721 pages.push(i);
722 }
723 }
724
725 // enough pages to hide some
726 else {
727
728 //close to beginning; only hide later pages
729 if (info.currentPage <= (this.pagesInRange + 1)) {
730 for (i = 1, l = 2 + ADJACENTx2; i < l; i++) {
731 pages.push(i);
732 }
733 }
734
735 // in middle; hide some front and some back
736 else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) {
737 for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) {
738 pages.push(i);
739 }
740 }
741
742 // close to end; only hide early pages
743 else {
744 for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) {
745 pages.push(i);
746 }
747 }
748 }
749
750 }
751
752 return pages;
753
754 },
755
756 bootstrap: function(options) {
757 _.extend(this, options);
758 this.goTo(1);
759 this.info();
760 return this;
761 }
762
763 });
764
765 // function aliasing
766 Paginator.clientPager.prototype.prevPage = Paginator.clientPager.prototype.previousPage;
767
768 // Helper function to generate rejected Deferred
769 var reject = function () {
770 var response = new $.Deferred();
771 response.reject();
772 return response.promise();
773 };
774
775 // @name: requestPager
776 //
777 // Paginator for server-side data being requested from a backend/API
778 //
779 // @description:
780 // This paginator is responsible for providing pagination
781 // and sort capabilities for requests to a server-side
782 // data service (e.g an API)
783 //
784 Paginator.requestPager = Backbone.Collection.extend({
785
786 sync: function ( method, model, options ) {
787
788 var self = this;
789
790 self.setDefaults();
791
792 // Some values could be functions, let's make sure
793 // to change their scope too and run them
794 var queryAttributes = {};
795 _.each(_.result(self, "server_api"), function(value, key){
796 if( _.isFunction(value) ) {
797 value = _.bind(value, self);
798 value = value();
799 }
800 queryAttributes[key] = value;
801 });
802
803 var queryOptions = _.clone(self.paginator_core);
804 _.each(queryOptions, function(value, key){
805 if( _.isFunction(value) ) {
806 value = _.bind(value, self);
807 value = value();
808 }
809 queryOptions[key] = value;
810 });
811
812 // Create default values if no others are specified
813 queryOptions = _.defaults(queryOptions, {
814 timeout: 25000,
815 cache: false,
816 type: 'GET',
817 dataType: 'jsonp'
818 });
819
820 // Allows the passing in of {data: {foo: 'bar'}} at request time to overwrite server_api defaults
821 if( options.data ){
822 options.data = decodeURIComponent($.param(_.extend(queryAttributes,options.data)));
823 }else{
824 options.data = decodeURIComponent($.param(queryAttributes));
825 }
826
827 queryOptions = _.extend(queryOptions, {
828 data: decodeURIComponent($.param(queryAttributes)),
829 processData: false,
830 url: _.result(queryOptions, 'url')
831 }, options);
832
833 var promiseSuccessFormat = !(bbVer[0] === 0 &&
834 bbVer[1] === 9 &&
835 bbVer[2] === 10);
836
837 var success = queryOptions.success;
838 queryOptions.success = function ( resp, status, xhr ) {
839
840 if ( success ) {
841 // This is to keep compatibility with Backbone 0.9.10
842 if (promiseSuccessFormat) {
843 success( resp, status, xhr );
844 } else {
845 success( model, resp, queryOptions );
846 }
847 }
848 if (bbVer[0] < 1 && model && model.trigger ) {
849 model.trigger( 'sync', model, resp, queryOptions );
850 }
851 };
852
853 var error = queryOptions.error;
854 queryOptions.error = function ( xhr ) {
855 if ( error ) {
856 error( xhr );
857 }
858 if ( model && model.trigger ) {
859 model.trigger( 'error', model, xhr, queryOptions );
860 }
861 };
862
863 var xhr = queryOptions.xhr = Backbone.ajax( queryOptions );
864 if ( model && model.trigger ) {
865 model.trigger('request', model, xhr, queryOptions);
866 }
867 return xhr;
868 },
869
870 setDefaults: function() {
871 var self = this;
872
873 // Create default values if no others are specified
874 _.defaults(self.paginator_ui, {
875 firstPage: 0,
876 currentPage: 1,
877 perPage: 5,
878 totalPages: 10,
879 pagesInRange: 4
880 });
881
882 // Change scope of 'paginator_ui' object values
883 _.each(self.paginator_ui, function(value, key) {
884 if (_.isUndefined(self[key])) {
885 self[key] = self.paginator_ui[key];
886 }
887 });
888 },
889
890 requestNextPage: function ( options ) {
891 if ( this.currentPage !== undefined ) {
892 this.currentPage += 1;
893 return this.pager( options );
894 } else {
895 return reject();
896 }
897 },
898
899 requestPreviousPage: function ( options ) {
900 if ( this.currentPage !== undefined ) {
901 this.currentPage -= 1;
902 return this.pager( options );
903 } else {
904 return reject();
905 }
906 },
907
908 updateOrder: function ( column, options ) {
909 if (column !== undefined) {
910 this.sortField = column;
911 return this.pager( options );
912 } else {
913 return reject();
914 }
915 },
916
917 goTo: function ( page, options ) {
918 if ( page !== undefined ) {
919 this.currentPage = parseInt(page, 10);
920 return this.pager( options );
921 } else {
922 return reject();
923 }
924 },
925
926 howManyPer: function ( count, options ) {
927 if ( count !== undefined ) {
928 this.currentPage = this.firstPage;
929 this.perPage = count;
930 return this.pager( options );
931 } else {
932 return reject();
933 }
934 },
935
936 info: function () {
937
938 var info = {
939 // If parse() method is implemented and totalRecords is set to the length
940 // of the records returned, make it available. Else, default it to 0
941 totalRecords: this.totalRecords || 0,
942
943 currentPage: this.currentPage,
944 firstPage: this.firstPage,
945 totalPages: Math.ceil(this.totalRecords / this.perPage),
946 lastPage: this.totalPages, // should use totalPages in template
947 perPage: this.perPage,
948 previous:false,
949 next:false
950 };
951
952 if (this.currentPage > 1) {
953 info.previous = this.currentPage - 1;
954 }
955
956 if (this.currentPage < info.totalPages) {
957 info.next = this.currentPage + 1;
958 }
959
960 // left around for backwards compatibility
961 info.hasNext = info.next;
962 info.hasPrevious = info.next;
963
964 info.pageSet = this.setPagination(info);
965
966 this.information = info;
967 return info;
968 },
969
970 setPagination: function ( info ) {
971
972 var pages = [], i = 0, l = 0;
973
974 // How many adjacent pages should be shown on each side?
975 var ADJACENTx2 = this.pagesInRange * 2,
976 LASTPAGE = Math.ceil(info.totalRecords / info.perPage);
977
978 if (LASTPAGE > 1) {
979
980 // not enough pages to bother breaking it up
981 if (LASTPAGE <= (1 + ADJACENTx2)) {
982 for (i = 1, l = LASTPAGE; i <= l; i++) {
983 pages.push(i);
984 }
985 }
986
987 // enough pages to hide some
988 else {
989
990 //close to beginning; only hide later pages
991 if (info.currentPage <= (this.pagesInRange + 1)) {
992 for (i = 1, l = 2 + ADJACENTx2; i < l; i++) {
993 pages.push(i);
994 }
995 }
996
997 // in middle; hide some front and some back
998 else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) {
999 for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) {
1000 pages.push(i);
1001 }
1002 }
1003
1004 // close to end; only hide early pages
1005 else {
1006 for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) {
1007 pages.push(i);
1008 }
1009 }
1010 }
1011
1012 }
1013
1014 return pages;
1015
1016 },
1017
1018 // fetches the latest results from the server
1019 pager: function ( options ) {
1020 if ( !_.isObject(options) ) {
1021 options = {};
1022 }
1023 return this.fetch( options );
1024 },
1025
1026 url: function(){
1027 // Expose url parameter enclosed in this.paginator_core.url to properly
1028 // extend Collection and allow Collection CRUD
1029 if(this.paginator_core !== undefined && this.paginator_core.url !== undefined){
1030 return this.paginator_core.url;
1031 } else {
1032 return null;
1033 }
1034 },
1035
1036 bootstrap: function(options) {
1037 _.extend(this, options);
1038 this.setDefaults();
1039 this.info();
1040 return this;
1041 }
1042 });
1043
1044 // function aliasing
1045 Paginator.requestPager.prototype.nextPage = Paginator.requestPager.prototype.requestNextPage;
1046 Paginator.requestPager.prototype.prevPage = Paginator.requestPager.prototype.requestPreviousPage;
1047
1048 return Paginator;
1049
1050}( Backbone, _, jQuery ));