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