PageRenderTime 422ms CodeModel.GetById 121ms app.highlight 184ms RepoModel.GetById 97ms app.codeStats 1ms

/js/lib/backbone.paginator.js

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