PageRenderTime 176ms CodeModel.GetById 15ms app.highlight 133ms RepoModel.GetById 1ms app.codeStats 0ms

/ajax/libs/backbone.paginator/0.7/backbone.paginator.js

https://gitlab.com/alidz1982/cdnjs
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 ));