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

/ajax/libs/backbone.paginator/0.6/backbone.paginator.js

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