/ajax/libs/backbone.paginator/0.7/backbone.paginator.js
JavaScript | 1043 lines | 691 code | 186 blank | 166 comment | 160 complexity | 64c99e6e022220a54241a03ce509710b MD5 | raw file
1/*! backbone.paginator - v0.7.0 - 3/25/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 var Paginator = {};
9 Paginator.version = "0.7.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 promiseSuccessFormat = !(parseInt(bbVer[0], 10) === 0 &&
114 parseInt(bbVer[1], 10) === 9 &&
115 parseInt(bbVer[2], 10) === 10);
116
117 var success = queryOptions.success;
118 queryOptions.success = function ( resp, status, xhr ) {
119 if ( success ) {
120 // This is to keep compatibility with Backbone 0.9.10
121 if (promiseSuccessFormat) {
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 if( (ac - 0) < (bc - 0) ) {
361 return 1;
362 }
363 if( (ac - 0) > (bc - 0) ) {
364 return -1;
365 }
366 } else {
367 if (ac < bc) {
368 return 1;
369 }
370 if (ac > bc) {
371 return -1;
372 }
373 }
374
375 } else {
376
377 //Same as the regexp check in the 'if' part.
378 if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) &&
379 (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))){
380 if( (ac - 0) < (bc - 0) ) {
381 return -1;
382 }
383 if( (ac - 0) > (bc - 0) ) {
384 return 1;
385 }
386 } else {
387 if (ac < bc) {
388 return -1;
389 }
390 if (ac > bc) {
391 return 1;
392 }
393 }
394
395 }
396
397 if (a.cid && b.cid){
398 var aId = a.cid,
399 bId = b.cid;
400
401 if (aId < bId) {
402 return -1;
403 }
404 if (aId > bId) {
405 return 1;
406 }
407 }
408
409 return 0;
410 });
411
412 return models;
413 },
414
415 // The actual place where the collection is field-filtered.
416 // Check setFieldFilter for arguments explicacion.
417 _fieldFilter: function( models, rules ) {
418
419 // Check if there are any rules
420 if ( _.isEmpty(rules) ) {
421 return models;
422 }
423
424 var filteredModels = [];
425
426 // Iterate over each rule
427 _.each(models, function(model){
428
429 var should_push = true;
430
431 // Apply each rule to each model in the collection
432 _.each(rules, function(rule){
433
434 // Don't go inside the switch if we're already sure that the model won't be included in the results
435 if( !should_push ){
436 return false;
437 }
438
439 should_push = false;
440
441 // The field's value will be passed to a custom function, which should
442 // return true (if model should be included) or false (model should be ignored)
443 if(rule.type === "function"){
444 var f = _.wrap(rule.value, function(func){
445 return func( model.get(rule.field) );
446 });
447 if( f() ){
448 should_push = true;
449 }
450
451 // The field's value is required to be non-empty
452 }else if(rule.type === "required"){
453 if( !_.isEmpty( model.get(rule.field).toString() ) ) {
454 should_push = true;
455 }
456
457 // The field's value is required to be greater tan N (numbers only)
458 }else if(rule.type === "min"){
459 if( !_.isNaN( Number( model.get(rule.field) ) ) &&
460 !_.isNaN( Number( rule.value ) ) &&
461 Number( model.get(rule.field) ) >= Number( rule.value ) ) {
462 should_push = true;
463 }
464
465 // The field's value is required to be smaller tan N (numbers only)
466 }else if(rule.type === "max"){
467 if( !_.isNaN( Number( model.get(rule.field) ) ) &&
468 !_.isNaN( Number( rule.value ) ) &&
469 Number( model.get(rule.field) ) <= Number( rule.value ) ) {
470 should_push = true;
471 }
472
473 // The field's value is required to be between N and M (numbers only)
474 }else if(rule.type === "range"){
475 if( !_.isNaN( Number( model.get(rule.field) ) ) &&
476 _.isObject( rule.value ) &&
477 !_.isNaN( Number( rule.value.min ) ) &&
478 !_.isNaN( Number( rule.value.max ) ) &&
479 Number( model.get(rule.field) ) >= Number( rule.value.min ) &&
480 Number( model.get(rule.field) ) <= Number( rule.value.max ) ) {
481 should_push = true;
482 }
483
484 // The field's value is required to be more than N chars long
485 }else if(rule.type === "minLength"){
486 if( model.get(rule.field).toString().length >= rule.value ) {
487 should_push = true;
488 }
489
490 // The field's value is required to be no more than N chars long
491 }else if(rule.type === "maxLength"){
492 if( model.get(rule.field).toString().length <= rule.value ) {
493 should_push = true;
494 }
495
496 // The field's value is required to be more than N chars long and no more than M chars long
497 }else if(rule.type === "rangeLength"){
498 if( _.isObject( rule.value ) &&
499 !_.isNaN( Number( rule.value.min ) ) &&
500 !_.isNaN( Number( rule.value.max ) ) &&
501 model.get(rule.field).toString().length >= rule.value.min &&
502 model.get(rule.field).toString().length <= rule.value.max ) {
503 should_push = true;
504 }
505
506 // The field's value is required to be equal to one of the values in rules.value
507 }else if(rule.type === "oneOf"){
508 if( _.isArray( rule.value ) &&
509 _.include( rule.value, model.get(rule.field) ) ) {
510 should_push = true;
511 }
512
513 // The field's value is required to be equal to the value in rules.value
514 }else if(rule.type === "equalTo"){
515 if( rule.value === model.get(rule.field) ) {
516 should_push = true;
517 }
518
519 }else if(rule.type === "containsAllOf"){
520 if( _.isArray( rule.value ) &&
521 _.isArray(model.get(rule.field)) &&
522 _.intersection( rule.value, model.get(rule.field)).length === rule.value.length) {
523 should_push = true;
524 }
525
526 // The field's value is required to match the regular expression
527 }else if(rule.type === "pattern"){
528 if( model.get(rule.field).toString().match(rule.value) ) {
529 should_push = true;
530 }
531
532 //Unknown type
533 }else{
534 should_push = false;
535 }
536
537 });
538
539 if( should_push ){
540 filteredModels.push(model);
541 }
542
543 });
544
545 return filteredModels;
546 },
547
548 // The actual place where the collection is filtered.
549 // Check setFilter for arguments explicacion.
550 _filter: function ( models, fields, filter ) {
551
552 // For example, if you had a data model containing cars like { color: '', description: '', hp: '' },
553 // your fields was set to ['color', 'description', 'hp'] and your filter was set
554 // to "Black Mustang 300", the word "Black" will match all the cars that have black color, then
555 // "Mustang" in the description and then the HP in the 'hp' field.
556 // NOTE: "Black Musta 300" will return the same as "Black Mustang 300"
557
558 // We accept fields to be a string, an array or an object
559 // but if string or array is passed we need to convert it
560 // to an object.
561
562 var self = this;
563
564 var obj_fields = {};
565
566 if( _.isString( fields ) ) {
567 obj_fields[fields] = {cmp_method: 'regexp'};
568 }else if( _.isArray( fields ) ) {
569 _.each(fields, function(field){
570 obj_fields[field] = {cmp_method: 'regexp'};
571 });
572 }else{
573 _.each(fields, function( cmp_opts, field ) {
574 obj_fields[field] = _.defaults(cmp_opts, { cmp_method: 'regexp' });
575 });
576 }
577
578 fields = obj_fields;
579
580 //Remove diacritic characters if diacritic plugin is loaded
581 if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
582 filter = Backbone.Paginator.removeDiacritics(filter);
583 }
584
585 // 'filter' can be only a string.
586 // If 'filter' is string we need to convert it to
587 // a regular expression.
588 // For example, if 'filter' is 'black dog' we need
589 // to find every single word, remove duplicated ones (if any)
590 // and transform the result to '(black|dog)'
591 if( filter === '' || !_.isString(filter) ) {
592 return models;
593 } else {
594 var words = _.map(filter.match(/\w+/ig), function(element) { return element.toLowerCase(); });
595 var pattern = "(" + _.uniq(words).join("|") + ")";
596 var regexp = new RegExp(pattern, "igm");
597 }
598
599 var filteredModels = [];
600
601 // We need to iterate over each model
602 _.each( models, function( model ) {
603
604 var matchesPerModel = [];
605
606 // and over each field of each model
607 _.each( fields, function( cmp_opts, field ) {
608
609 var value = model.get( field );
610
611 if( value ) {
612
613 // The regular expression we created earlier let's us detect if a
614 // given string contains each and all of the words in the regular expression
615 // or not, but in both cases match() will return an array containing all
616 // the words it matched.
617 var matchesPerField = [];
618
619 if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){
620 value = Backbone.Paginator.removeDiacritics(value.toString());
621 }else{
622 value = value.toString();
623 }
624
625 // Levenshtein cmp
626 if( cmp_opts.cmp_method === 'levenshtein' && _.has(Backbone.Paginator, 'levenshtein') && self.useLevenshteinPlugin ) {
627 var distance = Backbone.Paginator.levenshtein(value, filter);
628
629 _.defaults(cmp_opts, { max_distance: 0 });
630
631 if( distance <= cmp_opts.max_distance ) {
632 matchesPerField = _.uniq(words);
633 }
634
635 // Default (RegExp) cmp
636 }else{
637 matchesPerField = value.match( regexp );
638 }
639
640 matchesPerField = _.map(matchesPerField, function(match) {
641 return match.toString().toLowerCase();
642 });
643
644 _.each(matchesPerField, function(match){
645 matchesPerModel.push(match);
646 });
647
648 }
649
650 });
651
652 // We just need to check if the returned array contains all the words in our
653 // regex, and if it does, it means that we have a match, so we should save it.
654 matchesPerModel = _.uniq( _.without(matchesPerModel, "") );
655
656 if( _.isEmpty( _.difference(words, matchesPerModel) ) ) {
657 filteredModels.push(model);
658 }
659
660 });
661
662 return filteredModels;
663 },
664
665 // You shouldn't need to call info() as this method is used to
666 // calculate internal data as first/prev/next/last page...
667 info: function () {
668 var self = this,
669 info = {},
670 totalRecords = (self.sortedAndFilteredModels) ? self.sortedAndFilteredModels.length : self.length,
671 totalPages = Math.ceil(totalRecords / self.perPage);
672
673 info = {
674 totalUnfilteredRecords: self.origModels.length,
675 totalRecords: totalRecords,
676 currentPage: self.currentPage,
677 perPage: this.perPage,
678 totalPages: totalPages,
679 lastPage: totalPages,
680 previous: false,
681 next: false,
682 startRecord: totalRecords === 0 ? 0 : (self.currentPage - 1) * this.perPage + 1,
683 endRecord: Math.min(totalRecords, self.currentPage * this.perPage)
684 };
685
686 if (self.currentPage > 1) {
687 info.previous = self.currentPage - 1;
688 }
689
690 if (self.currentPage < info.totalPages) {
691 info.next = self.currentPage + 1;
692 }
693
694 info.pageSet = self.setPagination(info);
695
696 self.information = info;
697 return info;
698 },
699
700
701 // setPagination also is an internal function that shouldn't be called directly.
702 // It will create an array containing the pages right before and right after the
703 // actual page.
704 setPagination: function ( info ) {
705
706 var pages = [], i = 0, l = 0;
707
708 // How many adjacent pages should be shown on each side?
709 var ADJACENTx2 = this.pagesInRange * 2,
710 LASTPAGE = Math.ceil(info.totalRecords / info.perPage);
711
712 if (LASTPAGE > 1) {
713
714 // not enough pages to bother breaking it up
715 if (LASTPAGE <= (1 + ADJACENTx2)) {
716 for (i = 1, l = LASTPAGE; i <= l; i++) {
717 pages.push(i);
718 }
719 }
720
721 // enough pages to hide some
722 else {
723
724 //close to beginning; only hide later pages
725 if (info.currentPage <= (this.pagesInRange + 1)) {
726 for (i = 1, l = 2 + ADJACENTx2; i < l; i++) {
727 pages.push(i);
728 }
729 }
730
731 // in middle; hide some front and some back
732 else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) {
733 for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) {
734 pages.push(i);
735 }
736 }
737
738 // close to end; only hide early pages
739 else {
740 for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) {
741 pages.push(i);
742 }
743 }
744 }
745
746 }
747
748 return pages;
749
750 },
751
752 bootstrap: function(options) {
753 _.extend(this, options);
754 this.goTo(1);
755 this.info();
756 return this;
757 }
758
759 });
760
761 // function aliasing
762 Paginator.clientPager.prototype.prevPage = Paginator.clientPager.prototype.previousPage;
763
764 // @name: requestPager
765 //
766 // Paginator for server-side data being requested from a backend/API
767 //
768 // @description:
769 // This paginator is responsible for providing pagination
770 // and sort capabilities for requests to a server-side
771 // data service (e.g an API)
772 //
773 Paginator.requestPager = Backbone.Collection.extend({
774
775 sync: function ( method, model, options ) {
776
777 var self = this;
778
779 self.setDefaults();
780
781 // Some values could be functions, let's make sure
782 // to change their scope too and run them
783 var queryAttributes = {};
784 _.each(_.result(self, "server_api"), function(value, key){
785 if( _.isFunction(value) ) {
786 value = _.bind(value, self);
787 value = value();
788 }
789 queryAttributes[key] = value;
790 });
791
792 var queryOptions = _.clone(self.paginator_core);
793 _.each(queryOptions, function(value, key){
794 if( _.isFunction(value) ) {
795 value = _.bind(value, self);
796 value = value();
797 }
798 queryOptions[key] = value;
799 });
800
801 // Create default values if no others are specified
802 queryOptions = _.defaults(queryOptions, {
803 timeout: 25000,
804 cache: false,
805 type: 'GET',
806 dataType: 'jsonp'
807 });
808
809 // Allows the passing in of {data: {foo: 'bar'}} at request time to overwrite server_api defaults
810 if( options.data ){
811 options.data = decodeURIComponent($.param(_.extend(queryAttributes,options.data)));
812 }else{
813 options.data = decodeURIComponent($.param(queryAttributes));
814 }
815
816 queryOptions = _.extend(queryOptions, {
817 data: decodeURIComponent($.param(queryAttributes)),
818 processData: false,
819 url: _.result(queryOptions, 'url')
820 }, options);
821
822 var bbVer = Backbone.VERSION.split('.');
823 var promiseSuccessFormat = !(parseInt(bbVer[0], 10) === 0 &&
824 parseInt(bbVer[1], 10) === 9 &&
825 parseInt(bbVer[2], 10) === 10);
826
827 var success = queryOptions.success;
828 queryOptions.success = function ( resp, status, xhr ) {
829
830 if ( success ) {
831 // This is to keep compatibility with Backbone 0.9.10
832 if (promiseSuccessFormat) {
833 success( resp, status, xhr );
834 } else {
835 success( model, resp, queryOptions );
836 }
837 }
838 if ( model && model.trigger ) {
839 model.trigger( 'sync', model, resp, queryOptions );
840 }
841 };
842
843 var error = queryOptions.error;
844 queryOptions.error = function ( xhr ) {
845 if ( error ) {
846 error( model, xhr, queryOptions );
847 }
848 if ( model && model.trigger ) {
849 model.trigger( 'error', model, xhr, queryOptions );
850 }
851 };
852
853 var xhr = queryOptions.xhr = $.ajax( queryOptions );
854 if ( model && model.trigger ) {
855 model.trigger('request', model, xhr, queryOptions);
856 }
857 return xhr;
858 },
859
860 setDefaults: function() {
861 var self = this;
862
863 // Create default values if no others are specified
864 _.defaults(self.paginator_ui, {
865 firstPage: 0,
866 currentPage: 1,
867 perPage: 5,
868 totalPages: 10,
869 pagesInRange: 4
870 });
871
872 // Change scope of 'paginator_ui' object values
873 _.each(self.paginator_ui, function(value, key) {
874 if (_.isUndefined(self[key])) {
875 self[key] = self.paginator_ui[key];
876 }
877 });
878 },
879
880 requestNextPage: function ( options ) {
881 if ( this.currentPage !== undefined ) {
882 this.currentPage += 1;
883 return this.pager( options );
884 } else {
885 var response = new $.Deferred();
886 response.reject();
887 return response.promise();
888 }
889 },
890
891 requestPreviousPage: function ( options ) {
892 if ( this.currentPage !== undefined ) {
893 this.currentPage -= 1;
894 return this.pager( options );
895 } else {
896 var response = new $.Deferred();
897 response.reject();
898 return response.promise();
899 }
900 },
901
902 updateOrder: function ( column ) {
903 if (column !== undefined) {
904 this.sortField = column;
905 this.pager();
906 }
907
908 },
909
910 goTo: function ( page, options ) {
911 if ( page !== undefined ) {
912 this.currentPage = parseInt(page, 10);
913 return this.pager( options );
914 } else {
915 var response = new $.Deferred();
916 response.reject();
917 return response.promise();
918 }
919 },
920
921 howManyPer: function ( count ) {
922 if( count !== undefined ){
923 this.currentPage = this.firstPage;
924 this.perPage = count;
925 this.pager();
926 }
927 },
928
929 info: function () {
930
931 var info = {
932 // If parse() method is implemented and totalRecords is set to the length
933 // of the records returned, make it available. Else, default it to 0
934 totalRecords: this.totalRecords || 0,
935
936 currentPage: this.currentPage,
937 firstPage: this.firstPage,
938 totalPages: Math.ceil(this.totalRecords / this.perPage),
939 lastPage: this.totalPages, // should use totalPages in template
940 perPage: this.perPage,
941 previous:false,
942 next:false
943 };
944
945 if (this.currentPage > 1) {
946 info.previous = this.currentPage - 1;
947 }
948
949 if (this.currentPage < info.totalPages) {
950 info.next = this.currentPage + 1;
951 }
952
953 // left around for backwards compatibility
954 info.hasNext = info.next;
955 info.hasPrevious = info.next;
956
957 info.pageSet = this.setPagination(info);
958
959 this.information = info;
960 return info;
961 },
962
963 setPagination: function ( info ) {
964
965 var pages = [], i = 0, l = 0;
966
967 // How many adjacent pages should be shown on each side?
968 var ADJACENTx2 = this.pagesInRange * 2,
969 LASTPAGE = Math.ceil(info.totalRecords / info.perPage);
970
971 if (LASTPAGE > 1) {
972
973 // not enough pages to bother breaking it up
974 if (LASTPAGE <= (1 + ADJACENTx2)) {
975 for (i = 1, l = LASTPAGE; i <= l; i++) {
976 pages.push(i);
977 }
978 }
979
980 // enough pages to hide some
981 else {
982
983 //close to beginning; only hide later pages
984 if (info.currentPage <= (this.pagesInRange + 1)) {
985 for (i = 1, l = 2 + ADJACENTx2; i < l; i++) {
986 pages.push(i);
987 }
988 }
989
990 // in middle; hide some front and some back
991 else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) {
992 for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) {
993 pages.push(i);
994 }
995 }
996
997 // close to end; only hide early pages
998 else {
999 for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) {
1000 pages.push(i);
1001 }
1002 }
1003 }
1004
1005 }
1006
1007 return pages;
1008
1009 },
1010
1011 // fetches the latest results from the server
1012 pager: function ( options ) {
1013 if ( !_.isObject(options) ) {
1014 options = {};
1015 }
1016 return this.fetch( options );
1017 },
1018
1019 url: function(){
1020 // Expose url parameter enclosed in this.paginator_core.url to properly
1021 // extend Collection and allow Collection CRUD
1022 if(this.paginator_core !== undefined && this.paginator_core.url !== undefined){
1023 return this.paginator_core.url;
1024 } else {
1025 return null;
1026 }
1027 },
1028
1029 bootstrap: function(options) {
1030 _.extend(this, options);
1031 this.setDefaults();
1032 this.info();
1033 return this;
1034 }
1035 });
1036
1037 // function aliasing
1038 Paginator.requestPager.prototype.nextPage = Paginator.requestPager.prototype.requestNextPage;
1039 Paginator.requestPager.prototype.prevPage = Paginator.requestPager.prototype.requestPreviousPage;
1040
1041 return Paginator;
1042
1043}( Backbone, _, jQuery ));