PageRenderTime 34ms CodeModel.GetById 1ms app.highlight 28ms RepoModel.GetById 0ms app.codeStats 0ms

/ajax/libs/backbone.paginator/0.8/backbone.paginator.js

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