/*! backbone.paginator - v0.8.1 - 7/3/2013 * http://github.com/addyosmani/backbone.paginator * Copyright (c) 2013 Addy Osmani; Licensed MIT */ /*globals Backbone:true, _:true, jQuery:true*/ Backbone.Paginator = (function ( Backbone, _, $ ) { "use strict"; var bbVer = _.map(Backbone.VERSION.split('.'), function(digit) { return parseInt(digit, 10); }); var Paginator = {}; Paginator.version = "0.8.1"; // @name: clientPager // // @tagline: Paginator for client-side data // // @description: // This paginator is responsible for providing pagination // and sort capabilities for a single payload of data // we wish to paginate by the UI for easier browsering. // Paginator.clientPager = Backbone.Collection.extend({ // DEFAULTS FOR SORTING & FILTERING useDiacriticsPlugin: true, // use diacritics plugin if available useLevenshteinPlugin: true, // use levenshtein plugin if available sortColumn: "", sortDirection: "desc", lastSortColumn: "", fieldFilterRules: [], lastFieldFilterRules: [], filterFields: "", filterExpression: "", lastFilterExpression: "", //DEFAULT PAGINATOR UI VALUES defaults_ui: { firstPage: 0, currentPage: 1, perPage: 5, totalPages: 10, pagesInRange: 4 }, // Default values used when sorting and/or filtering. initialize: function(){ //LISTEN FOR ADD & REMOVE EVENTS THEN REMOVE MODELS FROM ORGINAL MODELS this.on('add', this.addModel, this); this.on('remove', this.removeModel, this); // SET DEFAULT VALUES (ALLOWS YOU TO POPULATE PAGINATOR MAUNALLY) this.setDefaults(); }, setDefaults: function() { // SET DEFAULT UI SETTINGS var options = _.defaults(this.paginator_ui, this.defaults_ui); //UPDATE GLOBAL UI SETTINGS _.defaults(this, options); }, addModel: function(model) { this.origModels.push(model); }, removeModel: function(model) { var index = _.indexOf(this.origModels, model); this.origModels.splice(index, 1); }, sync: function ( method, model, options ) { var self = this; // SET DEFAULT VALUES this.setDefaults(); // Some values could be functions, let's make sure // to change their scope too and run them var queryAttributes = {}; _.each(_.result(self, "server_api"), function(value, key){ if( _.isFunction(value) ) { value = _.bind(value, self); value = value(); } queryAttributes[key] = value; }); var queryOptions = _.clone(self.paginator_core); _.each(queryOptions, function(value, key){ if( _.isFunction(value) ) { value = _.bind(value, self); value = value(); } queryOptions[key] = value; }); // Create default values if no others are specified queryOptions = _.defaults(queryOptions, { timeout: 25000, cache: false, type: 'GET', dataType: 'jsonp' }); queryOptions = _.extend(queryOptions, { data: decodeURIComponent($.param(queryAttributes)), processData: false, url: _.result(queryOptions, 'url') }, options); var promiseSuccessFormat = !(bbVer[0] === 0 && bbVer[1] === 9 && bbVer[2] === 10); var success = queryOptions.success; queryOptions.success = function ( resp, status, xhr ) { if ( success ) { // This is to keep compatibility with Backbone 0.9.10 if (promiseSuccessFormat) { success( resp, status, xhr ); } else { success( model, resp, queryOptions ); } } if ( model && model.trigger ) { model.trigger( 'sync', model, resp, queryOptions ); } }; var error = queryOptions.error; queryOptions.error = function ( xhr ) { if ( error ) { error( model, xhr, queryOptions ); } if ( model && model.trigger ) { model.trigger( 'error', model, xhr, queryOptions ); } }; var xhr = queryOptions.xhr = Backbone.ajax( queryOptions ); if ( model && model.trigger ) { model.trigger('request', model, xhr, queryOptions); } return xhr; }, nextPage: function (options) { if(this.currentPage < this.information.totalPages) { this.currentPage = ++this.currentPage; this.pager(options); } }, previousPage: function (options) { if(this.currentPage > 1) { this.currentPage = --this.currentPage; this.pager(options); } }, goTo: function ( page, options ) { if(page !== undefined){ this.currentPage = parseInt(page, 10); this.pager(options); } }, howManyPer: function ( perPage ) { if(perPage !== undefined){ var lastPerPage = this.perPage; this.perPage = parseInt(perPage, 10); this.currentPage = Math.ceil( ( lastPerPage * ( this.currentPage - 1 ) + 1 ) / perPage); this.pager(); } }, // setSort is used to sort the current model. After // passing 'column', which is the model's field you want // to filter and 'direction', which is the direction // desired for the ordering ('asc' or 'desc'), pager() // and info() will be called automatically. setSort: function ( column, direction ) { if(column !== undefined && direction !== undefined){ this.lastSortColumn = this.sortColumn; this.sortColumn = column; this.sortDirection = direction; this.pager(); this.info(); } }, // setFieldFilter is used to filter each value of each model // according to `rules` that you pass as argument. // Example: You have a collection of books with 'release year' and 'author'. // You can filter only the books that were released between 1999 and 2003 // And then you can add another `rule` that will filter those books only to // authors who's name start with 'A'. setFieldFilter: function ( fieldFilterRules ) { if( !_.isEmpty( fieldFilterRules ) ) { this.lastFieldFilterRules = this.fieldFilterRules; this.fieldFilterRules = fieldFilterRules; this.pager(); this.info(); // if all the filters are removed, we should save the last filter // and then let the list reset to it's original state. } else { this.lastFieldFilterRules = this.fieldFilterRules; this.fieldFilterRules = ''; this.pager(); this.info(); } }, // doFakeFieldFilter can be used to get the number of models that will remain // after calling setFieldFilter with a filter rule(s) doFakeFieldFilter: function ( rules ) { if( !_.isEmpty( rules ) ) { var testModels = this.origModels; if (testModels === undefined) { testModels = this.models; } testModels = this._fieldFilter(testModels, rules); // To comply with current behavior, also filter by any previously defined setFilter rules. if ( this.filterExpression !== "" ) { testModels = this._filter(testModels, this.filterFields, this.filterExpression); } // Return size return testModels.length; } }, // setFilter is used to filter the current model. After // passing 'fields', which can be a string referring to // the model's field, an array of strings representing // each of the model's fields or an object with the name // of the model's field(s) and comparing options (see docs) // you wish to filter by and // 'filter', which is the word or words you wish to // filter by, pager() and info() will be called automatically. setFilter: function ( fields, filter ) { if( fields !== undefined && filter !== undefined ){ this.filterFields = fields; this.lastFilterExpression = this.filterExpression; this.filterExpression = filter; this.pager(); this.info(); } }, // doFakeFilter can be used to get the number of models that will // remain after calling setFilter with a `fields` and `filter` args. doFakeFilter: function ( fields, filter ) { if( fields !== undefined && filter !== undefined ){ var testModels = this.origModels; if (testModels === undefined) { testModels = this.models; } // To comply with current behavior, first filter by any previously defined setFieldFilter rules. if ( !_.isEmpty( this.fieldFilterRules ) ) { testModels = this._fieldFilter(testModels, this.fieldFilterRules); } testModels = this._filter(testModels, fields, filter); // Return size return testModels.length; } }, // pager is used to sort, filter and show the data // you expect the library to display. pager: function (options) { var self = this, disp = this.perPage, start = (self.currentPage - 1) * disp, stop = start + disp; // Saving the original models collection is important // as we could need to sort or filter, and we don't want // to loose the data we fetched from the server. if (self.origModels === undefined) { self.origModels = self.models; } self.models = self.origModels.slice(); // Check if sorting was set using setSort. if ( this.sortColumn !== "" ) { self.models = self._sort(self.models, this.sortColumn, this.sortDirection); } // Check if field-filtering was set using setFieldFilter if ( !_.isEmpty( this.fieldFilterRules ) ) { self.models = self._fieldFilter(self.models, this.fieldFilterRules); } // Check if filtering was set using setFilter. if ( this.filterExpression !== "" ) { self.models = self._filter(self.models, this.filterFields, this.filterExpression); } // If the sorting or the filtering was changed go to the first page if ( this.lastSortColumn !== this.sortColumn || this.lastFilterExpression !== this.filterExpression || !_.isEqual(this.fieldFilterRules, this.lastFieldFilterRules) ) { start = 0; stop = start + disp; self.currentPage = 1; this.lastSortColumn = this.sortColumn; this.lastFieldFilterRules = this.fieldFilterRules; this.lastFilterExpression = this.filterExpression; } // We need to save the sorted and filtered models collection // because we'll use that sorted and filtered collection in info(). self.sortedAndFilteredModels = self.models.slice(); self.info(); self.reset(self.models.slice(start, stop)); // This is somewhat of a hack to get all the nextPage, prevPage, and goTo methods // to work with a success callback (as in the requestPager). Realistically there is no failure case here, // but maybe we could catch exception and trigger a failure callback? _.result(options, 'success'); }, // The actual place where the collection is sorted. // Check setSort for arguments explicacion. _sort: function ( models, sort, direction ) { models = models.sort(function (a, b) { var ac = a.get(sort), bc = b.get(sort); if ( _.isUndefined(ac) || _.isUndefined(bc) || ac === null || bc === null ) { return 0; } else { /* Make sure that both ac and bc are lowercase strings. * .toString() first so we don't have to worry if ac or bc * have other String-only methods. */ ac = ac.toString().toLowerCase(); bc = bc.toString().toLowerCase(); } if (direction === 'desc') { // We need to know if there aren't any non-number characters // and that there are numbers-only characters and maybe a dot // if we have a float. // Oh, also a '-' for negative numbers! if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) && (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))){ if( (ac - 0) < (bc - 0) ) { return 1; } if( (ac - 0) > (bc - 0) ) { return -1; } } else { if (ac < bc) { return 1; } if (ac > bc) { return -1; } } } else { //Same as the regexp check in the 'if' part. if((!ac.match(/[^\-\d\.]/) && ac.match(/-?[\d\.]+/)) && (!bc.match(/[^\-\d\.]/) && bc.match(/-?[\d\.]+/))){ if( (ac - 0) < (bc - 0) ) { return -1; } if( (ac - 0) > (bc - 0) ) { return 1; } } else { if (ac < bc) { return -1; } if (ac > bc) { return 1; } } } if (a.cid && b.cid){ var aId = a.cid, bId = b.cid; if (aId < bId) { return -1; } if (aId > bId) { return 1; } } return 0; }); return models; }, // The actual place where the collection is field-filtered. // Check setFieldFilter for arguments explicacion. _fieldFilter: function( models, rules ) { // Check if there are any rules if ( _.isEmpty(rules) ) { return models; } var filteredModels = []; // Iterate over each rule _.each(models, function(model){ var should_push = true; // Apply each rule to each model in the collection _.each(rules, function(rule){ // Don't go inside the switch if we're already sure that the model won't be included in the results if( !should_push ){ return false; } should_push = false; // The field's value will be passed to a custom function, which should // return true (if model should be included) or false (model should be ignored) if(rule.type === "function"){ var f = _.wrap(rule.value, function(func){ return func( model.get(rule.field) ); }); if( f() ){ should_push = true; } // The field's value is required to be non-empty }else if(rule.type === "required"){ if( !_.isEmpty( model.get(rule.field).toString() ) ) { should_push = true; } // The field's value is required to be greater tan N (numbers only) }else if(rule.type === "min"){ if( !_.isNaN( Number( model.get(rule.field) ) ) && !_.isNaN( Number( rule.value ) ) && Number( model.get(rule.field) ) >= Number( rule.value ) ) { should_push = true; } // The field's value is required to be smaller tan N (numbers only) }else if(rule.type === "max"){ if( !_.isNaN( Number( model.get(rule.field) ) ) && !_.isNaN( Number( rule.value ) ) && Number( model.get(rule.field) ) <= Number( rule.value ) ) { should_push = true; } // The field's value is required to be between N and M (numbers only) }else if(rule.type === "range"){ if( !_.isNaN( Number( model.get(rule.field) ) ) && _.isObject( rule.value ) && !_.isNaN( Number( rule.value.min ) ) && !_.isNaN( Number( rule.value.max ) ) && Number( model.get(rule.field) ) >= Number( rule.value.min ) && Number( model.get(rule.field) ) <= Number( rule.value.max ) ) { should_push = true; } // The field's value is required to be more than N chars long }else if(rule.type === "minLength"){ if( model.get(rule.field).toString().length >= rule.value ) { should_push = true; } // The field's value is required to be no more than N chars long }else if(rule.type === "maxLength"){ if( model.get(rule.field).toString().length <= rule.value ) { should_push = true; } // The field's value is required to be more than N chars long and no more than M chars long }else if(rule.type === "rangeLength"){ if( _.isObject( rule.value ) && !_.isNaN( Number( rule.value.min ) ) && !_.isNaN( Number( rule.value.max ) ) && model.get(rule.field).toString().length >= rule.value.min && model.get(rule.field).toString().length <= rule.value.max ) { should_push = true; } // The field's value is required to be equal to one of the values in rules.value }else if(rule.type === "oneOf"){ if( _.isArray( rule.value ) && _.include( rule.value, model.get(rule.field) ) ) { should_push = true; } // The field's value is required to be equal to the value in rules.value }else if(rule.type === "equalTo"){ if( rule.value === model.get(rule.field) ) { should_push = true; } }else if(rule.type === "containsAllOf"){ if( _.isArray( rule.value ) && _.isArray(model.get(rule.field)) && _.intersection( rule.value, model.get(rule.field)).length === rule.value.length) { should_push = true; } // The field's value is required to match the regular expression }else if(rule.type === "pattern"){ if( model.get(rule.field).toString().match(rule.value) ) { should_push = true; } //Unknown type }else{ should_push = false; } }); if( should_push ){ filteredModels.push(model); } }); return filteredModels; }, // The actual place where the collection is filtered. // Check setFilter for arguments explicacion. _filter: function ( models, fields, filter ) { // For example, if you had a data model containing cars like { color: '', description: '', hp: '' }, // your fields was set to ['color', 'description', 'hp'] and your filter was set // to "Black Mustang 300", the word "Black" will match all the cars that have black color, then // "Mustang" in the description and then the HP in the 'hp' field. // NOTE: "Black Musta 300" will return the same as "Black Mustang 300" // We accept fields to be a string, an array or an object // but if string or array is passed we need to convert it // to an object. var self = this; var obj_fields = {}; if( _.isString( fields ) ) { obj_fields[fields] = {cmp_method: 'regexp'}; }else if( _.isArray( fields ) ) { _.each(fields, function(field){ obj_fields[field] = {cmp_method: 'regexp'}; }); }else{ _.each(fields, function( cmp_opts, field ) { obj_fields[field] = _.defaults(cmp_opts, { cmp_method: 'regexp' }); }); } fields = obj_fields; //Remove diacritic characters if diacritic plugin is loaded if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){ filter = Backbone.Paginator.removeDiacritics(filter); } // 'filter' can be only a string. // If 'filter' is string we need to convert it to // a regular expression. // For example, if 'filter' is 'black dog' we need // to find every single word, remove duplicated ones (if any) // and transform the result to '(black|dog)' if( filter === '' || !_.isString(filter) ) { return models; } else { var words = _.map(filter.match(/\w+/ig), function(element) { return element.toLowerCase(); }); var pattern = "(" + _.uniq(words).join("|") + ")"; var regexp = new RegExp(pattern, "igm"); } var filteredModels = []; // We need to iterate over each model _.each( models, function( model ) { var matchesPerModel = []; // and over each field of each model _.each( fields, function( cmp_opts, field ) { var value = model.get( field ); if( value ) { // The regular expression we created earlier let's us detect if a // given string contains each and all of the words in the regular expression // or not, but in both cases match() will return an array containing all // the words it matched. var matchesPerField = []; if( _.has(Backbone.Paginator, 'removeDiacritics') && self.useDiacriticsPlugin ){ value = Backbone.Paginator.removeDiacritics(value.toString()); }else{ value = value.toString(); } // Levenshtein cmp if( cmp_opts.cmp_method === 'levenshtein' && _.has(Backbone.Paginator, 'levenshtein') && self.useLevenshteinPlugin ) { var distance = Backbone.Paginator.levenshtein(value, filter); _.defaults(cmp_opts, { max_distance: 0 }); if( distance <= cmp_opts.max_distance ) { matchesPerField = _.uniq(words); } // Default (RegExp) cmp }else{ matchesPerField = value.match( regexp ); } matchesPerField = _.map(matchesPerField, function(match) { return match.toString().toLowerCase(); }); _.each(matchesPerField, function(match){ matchesPerModel.push(match); }); } }); // We just need to check if the returned array contains all the words in our // regex, and if it does, it means that we have a match, so we should save it. matchesPerModel = _.uniq( _.without(matchesPerModel, "") ); if( _.isEmpty( _.difference(words, matchesPerModel) ) ) { filteredModels.push(model); } }); return filteredModels; }, // You shouldn't need to call info() as this method is used to // calculate internal data as first/prev/next/last page... info: function () { var self = this, info = {}, totalRecords = (self.sortedAndFilteredModels) ? self.sortedAndFilteredModels.length : self.length, totalPages = Math.ceil(totalRecords / self.perPage); info = { totalUnfilteredRecords: self.origModels.length, totalRecords: totalRecords, currentPage: self.currentPage, perPage: this.perPage, totalPages: totalPages, lastPage: totalPages, previous: false, next: false, startRecord: totalRecords === 0 ? 0 : (self.currentPage - 1) * this.perPage + 1, endRecord: Math.min(totalRecords, self.currentPage * this.perPage) }; if (self.currentPage > 1) { info.previous = self.currentPage - 1; } if (self.currentPage < info.totalPages) { info.next = self.currentPage + 1; } info.pageSet = self.setPagination(info); self.information = info; return info; }, // setPagination also is an internal function that shouldn't be called directly. // It will create an array containing the pages right before and right after the // actual page. setPagination: function ( info ) { var pages = [], i = 0, l = 0; // How many adjacent pages should be shown on each side? var ADJACENTx2 = this.pagesInRange * 2, LASTPAGE = Math.ceil(info.totalRecords / info.perPage); if (LASTPAGE > 1) { // not enough pages to bother breaking it up if (LASTPAGE <= (1 + ADJACENTx2)) { for (i = 1, l = LASTPAGE; i <= l; i++) { pages.push(i); } } // enough pages to hide some else { //close to beginning; only hide later pages if (info.currentPage <= (this.pagesInRange + 1)) { for (i = 1, l = 2 + ADJACENTx2; i < l; i++) { pages.push(i); } } // in middle; hide some front and some back else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) { for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) { pages.push(i); } } // close to end; only hide early pages else { for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) { pages.push(i); } } } } return pages; }, bootstrap: function(options) { _.extend(this, options); this.goTo(1); this.info(); return this; } }); // function aliasing Paginator.clientPager.prototype.prevPage = Paginator.clientPager.prototype.previousPage; // Helper function to generate rejected Deferred var reject = function () { var response = new $.Deferred(); response.reject(); return response.promise(); }; // @name: requestPager // // Paginator for server-side data being requested from a backend/API // // @description: // This paginator is responsible for providing pagination // and sort capabilities for requests to a server-side // data service (e.g an API) // Paginator.requestPager = Backbone.Collection.extend({ sync: function ( method, model, options ) { var self = this; self.setDefaults(); // Some values could be functions, let's make sure // to change their scope too and run them var queryAttributes = {}; _.each(_.result(self, "server_api"), function(value, key){ if( _.isFunction(value) ) { value = _.bind(value, self); value = value(); } queryAttributes[key] = value; }); var queryOptions = _.clone(self.paginator_core); _.each(queryOptions, function(value, key){ if( _.isFunction(value) ) { value = _.bind(value, self); value = value(); } queryOptions[key] = value; }); // Create default values if no others are specified queryOptions = _.defaults(queryOptions, { timeout: 25000, cache: false, type: 'GET', dataType: 'jsonp' }); // Allows the passing in of {data: {foo: 'bar'}} at request time to overwrite server_api defaults if( options.data ){ options.data = decodeURIComponent($.param(_.extend(queryAttributes,options.data))); }else{ options.data = decodeURIComponent($.param(queryAttributes)); } queryOptions = _.extend(queryOptions, { data: decodeURIComponent($.param(queryAttributes)), processData: false, url: _.result(queryOptions, 'url') }, options); var promiseSuccessFormat = !(bbVer[0] === 0 && bbVer[1] === 9 && bbVer[2] === 10); var success = queryOptions.success; queryOptions.success = function ( resp, status, xhr ) { if ( success ) { // This is to keep compatibility with Backbone 0.9.10 if (promiseSuccessFormat) { success( resp, status, xhr ); } else { success( model, resp, queryOptions ); } } if (bbVer[0] < 1 && model && model.trigger ) { model.trigger( 'sync', model, resp, queryOptions ); } }; var error = queryOptions.error; queryOptions.error = function ( xhr ) { if ( error ) { error( xhr ); } if ( model && model.trigger ) { model.trigger( 'error', model, xhr, queryOptions ); } }; var xhr = queryOptions.xhr = Backbone.ajax( queryOptions ); if ( model && model.trigger ) { model.trigger('request', model, xhr, queryOptions); } return xhr; }, setDefaults: function() { var self = this; // Create default values if no others are specified _.defaults(self.paginator_ui, { firstPage: 0, currentPage: 1, perPage: 5, totalPages: 10, pagesInRange: 4 }); // Change scope of 'paginator_ui' object values _.each(self.paginator_ui, function(value, key) { if (_.isUndefined(self[key])) { self[key] = self.paginator_ui[key]; } }); }, requestNextPage: function ( options ) { if ( this.currentPage !== undefined ) { this.currentPage += 1; return this.pager( options ); } else { return reject(); } }, requestPreviousPage: function ( options ) { if ( this.currentPage !== undefined ) { this.currentPage -= 1; return this.pager( options ); } else { return reject(); } }, updateOrder: function ( column, options ) { if (column !== undefined) { this.sortField = column; return this.pager( options ); } else { return reject(); } }, goTo: function ( page, options ) { if ( page !== undefined ) { this.currentPage = parseInt(page, 10); return this.pager( options ); } else { return reject(); } }, howManyPer: function ( count, options ) { if ( count !== undefined ) { this.currentPage = this.firstPage; this.perPage = count; return this.pager( options ); } else { return reject(); } }, info: function () { var info = { // If parse() method is implemented and totalRecords is set to the length // of the records returned, make it available. Else, default it to 0 totalRecords: this.totalRecords || 0, currentPage: this.currentPage, firstPage: this.firstPage, totalPages: Math.ceil(this.totalRecords / this.perPage), lastPage: this.totalPages, // should use totalPages in template perPage: this.perPage, previous:false, next:false }; if (this.currentPage > 1) { info.previous = this.currentPage - 1; } if (this.currentPage < info.totalPages) { info.next = this.currentPage + 1; } // left around for backwards compatibility info.hasNext = info.next; info.hasPrevious = info.next; info.pageSet = this.setPagination(info); this.information = info; return info; }, setPagination: function ( info ) { var pages = [], i = 0, l = 0; // How many adjacent pages should be shown on each side? var ADJACENTx2 = this.pagesInRange * 2, LASTPAGE = Math.ceil(info.totalRecords / info.perPage); if (LASTPAGE > 1) { // not enough pages to bother breaking it up if (LASTPAGE <= (1 + ADJACENTx2)) { for (i = 1, l = LASTPAGE; i <= l; i++) { pages.push(i); } } // enough pages to hide some else { //close to beginning; only hide later pages if (info.currentPage <= (this.pagesInRange + 1)) { for (i = 1, l = 2 + ADJACENTx2; i < l; i++) { pages.push(i); } } // in middle; hide some front and some back else if (LASTPAGE - this.pagesInRange > info.currentPage && info.currentPage > this.pagesInRange) { for (i = info.currentPage - this.pagesInRange; i <= info.currentPage + this.pagesInRange; i++) { pages.push(i); } } // close to end; only hide early pages else { for (i = LASTPAGE - ADJACENTx2; i <= LASTPAGE; i++) { pages.push(i); } } } } return pages; }, // fetches the latest results from the server pager: function ( options ) { if ( !_.isObject(options) ) { options = {}; } return this.fetch( options ); }, url: function(){ // Expose url parameter enclosed in this.paginator_core.url to properly // extend Collection and allow Collection CRUD if(this.paginator_core !== undefined && this.paginator_core.url !== undefined){ return this.paginator_core.url; } else { return null; } }, bootstrap: function(options) { _.extend(this, options); this.setDefaults(); this.info(); return this; } }); // function aliasing Paginator.requestPager.prototype.nextPage = Paginator.requestPager.prototype.requestNextPage; Paginator.requestPager.prototype.prevPage = Paginator.requestPager.prototype.requestPreviousPage; return Paginator; }( Backbone, _, jQuery ));