├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── dist ├── paginate-anything-tpls.js ├── paginate-anything-tpls.min.js ├── paginate-anything.html ├── paginate-anything.js └── paginate-anything.min.js ├── img ├── link-group-size.png └── paginate-anything-logo.png ├── index.js ├── karma.conf.js ├── package.json ├── src ├── paginate-anything.html ├── paginate-anything.js └── paginate-anything.min.js └── test └── paginate-anything-spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "bower_components" 3 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .DS_Store 4 | tmp 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": true, 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "globals": { "angular": false, "window": false, "define": false, "describe": false, "it": false, "beforeEach": false, "context": false, "expect": false, "spyOn": false } 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - npm install -g grunt-cli 6 | - bower install 7 | script: 8 | - grunt test 9 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 'use strict'; 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | jshint: { 6 | all: [ 7 | './src/paginate-anything.js', '*.json', './test/*.js' 8 | ], 9 | options: { 10 | jshintrc: '.jshintrc' 11 | } 12 | }, 13 | karma: { 14 | travis: { 15 | configFile: 'karma.conf.js', 16 | singleRun: true, 17 | browsers: ['PhantomJS'] 18 | } 19 | }, 20 | bump: { 21 | options: { 22 | files: ['package.json', 'bower.json'], 23 | updateConfigs: ['pkg'], 24 | commitFiles: ['-a'], 25 | pushTo: 'origin' 26 | } 27 | }, 28 | ngtemplates: { 29 | options: { 30 | module: 'bgf.paginateAnything', 31 | htmlmin: { 32 | collapseBooleanAttributes: true, 33 | collapseWhitespace: true, 34 | removeAttributeQuotes: true, 35 | removeComments: true, // Only if you don't use comment directives! 36 | removeEmptyAttributes: true, 37 | removeRedundantAttributes: true, 38 | removeScriptTypeAttributes: true, 39 | removeStyleLinkTypeAttributes: true 40 | } 41 | }, 42 | template: { 43 | src: ['src/*.html'], 44 | dest: 'tmp/templates.js' 45 | }, 46 | }, 47 | concat: { 48 | template: { 49 | options: { 50 | }, 51 | src: ['src/paginate-anything.js', '<%= ngtemplates.template.dest %>'], 52 | dest: 'dist/paginate-anything-tpls.js' 53 | } 54 | }, 55 | copy: { 56 | main : { 57 | files: [ 58 | { 59 | src: ['src/paginate-anything.js'], 60 | dest: 'dist/paginate-anything.js' 61 | } 62 | ] 63 | }, 64 | template : { 65 | files: [ 66 | { 67 | src: ['src/paginate-anything.html'], 68 | dest: 'dist/paginate-anything.html' 69 | } 70 | ] 71 | } 72 | }, 73 | uglify: { 74 | options: { 75 | banner: '// <%= pkg.name %> - v<%= pkg.version %>\n' 76 | }, 77 | dist: { 78 | files: { 79 | 'dist/paginate-anything.min.js': ['dist/paginate-anything.js'], 80 | 'dist/paginate-anything-tpls.min.js': ['dist/paginate-anything-tpls.js'], 81 | } 82 | } 83 | }, 84 | clean: { 85 | temp: { 86 | src: [ 'tmp' ] 87 | } 88 | } 89 | }); 90 | 91 | grunt.loadNpmTasks('grunt-bump'); 92 | grunt.loadNpmTasks('grunt-contrib-clean'); 93 | grunt.loadNpmTasks('grunt-contrib-copy'); 94 | grunt.loadNpmTasks('grunt-contrib-concat'); 95 | grunt.loadNpmTasks('grunt-contrib-jshint'); 96 | grunt.loadNpmTasks('grunt-karma'); 97 | grunt.loadNpmTasks('grunt-contrib-uglify'); 98 | grunt.loadNpmTasks('grunt-angular-templates'); 99 | 100 | grunt.registerTask('default', ['test', 'build']); 101 | grunt.registerTask('test', ['jshint', 'karma:travis']); 102 | grunt.registerTask('build', ['clean','ngtemplates', 'concat', 'copy', 'uglify']); 103 | grunt.registerTask('makeRelease', ['bump-only', 'test', 'build', 'bump-commit']); 104 | }; 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Joe Nelson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Logo 2 | ## Angular Directive to Paginate Anything 3 | [![Build Status](https://travis-ci.org/begriffs/angular-paginate-anything.png?branch=master)](https://travis-ci.org/begriffs/angular-paginate-anything) 4 | 5 | Add server-side pagination to any list or table on the page. This 6 | directive connects a variable of your choice on the local scope with 7 | data provied on a given URL. It provides a pagination user interface 8 | that triggers updates to the variable through paginated AJAX requests. 9 | 10 | Pagination is a distinct concern and should be handled separately from 11 | other app logic. Do it right, do it in one place. Paginate anything! 12 | 13 | ### [DEMO](http://begriffs.github.io/angular-paginate-anything/) 14 | 15 | ### Usage 16 | 17 | Include with bower 18 | 19 | ```sh 20 | bower install angular-paginate-anything 21 | ``` 22 | 23 | The bower package contains files in the ```dist/```directory with the following names: 24 | 25 | - angular-paginate-anything.js 26 | - angular-paginate-anything.min.js 27 | - angular-paginate-anything-tpls.js 28 | - angular-paginate-anything-tpls.min.js 29 | 30 | Files with the ```min``` suffix are minified versions to be used in production. The files with ```-tpls``` in their name have the directive template bundled. If you don't need the default template use the ```angular-paginate-anything.min.js``` file and provide your own template with the ```templateUrl``` attribute. 31 | 32 | Load the javascript and declare your Angular dependency 33 | 34 | ```html 35 | 36 | ``` 37 | 38 | ```js 39 | angular.module('myModule', ['bgf.paginateAnything']); 40 | ``` 41 | 42 | Then in your view 43 | 44 | ```html 45 | 46 | 47 | 50 | 51 | ``` 52 | 53 | The `pagination` directive uses an external template stored in 54 | `tpl/paginate-anything.html`. Host it in a place accessible to 55 | your page and set the `templateUrl` attribute. Note that the `url` 56 | param can be a scope variable as well as a hard-coded string. 57 | 58 | ### Benefits 59 | 60 | * Attaches to anything — ng-repeat, ng-grid, ngTable etc 61 | * Server side pagination scales to large data 62 | * Works with any MIME type through RFC2616 Range headers 63 | * Handles finite or infinite lists 64 | * Negotiates per-page limits with server 65 | * Keeps items in view when changing page size 66 | * Twitter Bootstrap compatible markup 67 | 68 | ### Directive Attributes 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 |
NameDescriptionAccess
urlurl of endpoint which returns a JSON arrayRead/write. Changing it will reset to the first page.
url-paramsmap of strings or objects which will be turned to ?key1=value1&key2=value2 after the urlRead/write. Changing it will reset to the first page.
headersadditional headers to send during requestWrite-only.
pagethe currently active pageRead/write. Writing changes pages. Zero-based.
per-page(default=`50`) Max number of elements per pageRead/write. The server may choose to send fewer items though.
per-page-presetsArray of suggestions for per-page. Adjusts depending on server limitsRead/write.
auto-presets(default=`true`) Overrides per-page presets and client-limit to quantized values 1,2,5,10,25,50...Read/write.
client-limit(default=`250`) Biggest page size the directive will show. Server response may be smaller.Read/write.
link-group-size(default=`3`) Number of elements surrounding current page. illustrationRead/write.
num-itemsTotal items reported by server for the collectionRead-only.
num-pagesnum-items / per-pageRead-only.
server-limitMaximum results the server will send (Infinity if not yet detected)Read-only.
range-fromPosition of first item in currently loaded rangeRead-only.
range-toPosition of last item in currently loaded rangeRead-only.
reload-pageIf set to true, the current page is reloaded.Write-only.
sizeTwitter bootstrap sizing `sm`, `md` (default), or `lg` for the navigation elements.Write-only.
passiveIf using more than one pagination control set this to 'true' on all but the first.Write-only.
transform-responseFunction that will get called once the http response has returned. See Angular's $https documentation for more information.Read/write. Changing it will reset to the first page.
methodType of request method. Can be either GET or POST. Default is GET.Read/write.
post-dataAn array of data to be sent when method is set to POST.Read/write.
load-fnA callback function to perform the request. Gets the http config as parameter and must return a promise.Write-only.
186 | 187 | ### Events 188 | 189 | The directive emits events as pages begin loading (`pagination:loadStart`) 190 | or finish (`pagination:loadPage`) or errors occur (`pagination:error`). 191 | To catch these events do the following: 192 | 193 | ```js 194 | $scope.$on('pagination:loadPage', function (event, status, config) { 195 | // config contains parameters of the page request 196 | console.log(config.url); 197 | // status is the HTTP status of the result 198 | console.log(status); 199 | }); 200 | ``` 201 | 202 | The `pagination:loadStart` is passed the client request rather than 203 | the server response. 204 | 205 | To trigger a reload the `pagination:reload` event can be send: 206 | 207 | ```js 208 | function () { 209 | $scope.$broadcast('pagination:reload'); 210 | } 211 | ``` 212 | 213 | ### How to deal with sorting, filtering and facets? 214 | 215 | Your server is responsible for interpreting URLs to provide these 216 | features. You can connect the `url` attribute of this directive 217 | to a scope variable and adjust the variable with query params and 218 | whatever else your server recognizes. Or you can use the `url-params` 219 | attribute to connect a map of strings or objects which will be 220 | turned to ?key1=value1&key2=value2 after the url. Changing the 221 | `url` or `url-params` causes the pagination to reset to the first 222 | page and maintain page size. 223 | 224 | Example: 225 | ```js 226 | $scope.url = 'api/resources'; 227 | $scope.urlParams = { 228 | key1: "value1", 229 | key2: "value2" 230 | }; 231 | ``` 232 | 233 | Will turn into the URL of the resource that is being requested: `api/resources?key1=value1&key2=value2` 234 | 235 | ### What your server needs to do 236 | 237 | This directive decorates AJAX requests to your server with some 238 | simple, standard headers. You read these headers to determine the 239 | limit and offset of the requested data. Your need to set response 240 | headers to indicate the range returned and the total number of items 241 | in the collection. 242 | 243 | You can write the logic yourself, or use one of the following server 244 | side libraries. 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 |
FrameworkSolution
Ruby on Railsbegriffs/clean_pagination gem
Node.jsnode-paginate-anything module
Express JS from scratch howto
ServiceStack for .NETService Stack .NET howto
ASP.NET Web APIASP.NET Web API howto
275 | 276 | For a reference of a properly configured server, visit 277 | [pagination.begriffs.com](http://pagination.begriffs.com/). 278 | 279 | Here is an example HTTP transaction that requests the first twenty-five 280 | items and a response that provides them and says there are one 281 | hundred total items. 282 | 283 | Request 284 | 285 | ```HTTP 286 | GET /stuff HTTP/1.1 287 | Range-Unit: items 288 | Range: 0-24 289 | ``` 290 | 291 | Response 292 | 293 | ```HTTP 294 | HTTP/1.1 206 Partial Content 295 | Content-Range: 0-24/100 296 | Range-Unit: items 297 | Content-Type: application/json 298 | 299 | [ etc, etc, ... ] 300 | ``` 301 | 302 | In short your server parses the `Range` header to find the zero-based 303 | start and end item. It includes a `Content-Range` header in the 304 | response disclosing the unit and range it chooses to return, along with the 305 | total items after a slash, where total items can be "*" meaning 306 | unknown or infinite. 307 | 308 | When there are zero elements to return your server should send 309 | status code 204 (no content), `Content-Range: */0`, and an empty 310 | body (or `[]` if the endpoint normally returns a JSON array). 311 | 312 | To do all this header stuff you'll need to enable CORS on your server. 313 | In a Rails app you can do this by adding the following to `config/application.rb`: 314 | 315 | ```ruby 316 | config.middleware.use Rack::Cors do 317 | allow do 318 | origins '*' 319 | resource '*', 320 | :headers => :any, 321 | :methods => [:get, :options], 322 | :expose => ['Content-Range', 'Accept-Ranges'] 323 | end 324 | end 325 | ``` 326 | 327 | For a more complete implementation including other appropriate responses 328 | see my [clean_pagination](https://github.com/begriffs/clean_pagination) gem. 329 | 330 | ### Using the load-fn callback 331 | 332 | Instead of having paginate-anything handle the http requests there is the option of using a callback function to perform the requests. This might be helpful e.g. if the data does not come from http endpoints, further processing of the request needs to be done prior to submitting the request or further processing of the response is necessary. 333 | 334 | The callback can be used as follows: 335 | 336 | ```html 337 | 338 | ``` 339 | 340 | ```js 341 | $scope.callback = function (config) { 342 | return $http(config); 343 | } 344 | 345 | // alternatively 346 | $scope.callback = function(config) { 347 | return $q(function(resolve) { 348 | resolve({ 349 | data: ['a', 'b'], 350 | status: 200, 351 | config: {}, 352 | headers: function(headerName) { 353 | // fake Content-Range headers 354 | return '0-1/*'; 355 | } 356 | }); 357 | }); 358 | } 359 | ``` 360 | 361 | ### Further reading 362 | 363 | * [Hypertext Transfer Protocol (HTTP/1.1): Range Requests](http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html) 364 | * [RFC2616 Section 3.12, custom range units](http://www.ietf.org/rfc/rfc2616.txt) 365 | * [Beyond HTTP Header Links](http://blog.begriffs.com/2014/03/beyond-http-header-links.html) 366 | * [Heroku recommends Range headers for pagination](https://github.com/interagent/http-api-design#paginate-with-ranges) 367 | 368 | ### Thanks 369 | 370 | Thanks to [Steve Klabnik](https://twitter.com/steveklabnik) for 371 | discussions about doing hypermedia/HATEOAS right, and to [Rebecca 372 | Wright](https://twitter.com/rebecca_wrights) for reviewing and 373 | improving my original user interface ideas for the paginator. 374 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-paginate-anything", 3 | "version": "4.2.0", 4 | "main": "dist/paginate-anything-tpls.js", 5 | "dependencies": { 6 | "angular": "latest" 7 | }, 8 | "devDependencies": { 9 | "angular-mocks": "latest", 10 | "jasmine-as-promised": "latest" 11 | }, 12 | "ignore": [ 13 | "**/.*", 14 | "**/*.json", 15 | "node_modules", 16 | "bower_components", 17 | "test", 18 | "img", 19 | "src", 20 | "Gruntfile.js", 21 | "package.json", 22 | "karma.conf.js" 23 | ] 24 | } 25 | 26 | -------------------------------------------------------------------------------- /dist/paginate-anything-tpls.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // 1 2 5 10 25 50 100 250 500 etc 5 | function quantizedNumber(i) { 6 | var adjust = [1, 2.5, 5]; 7 | return Math.floor(Math.pow(10, Math.floor(i/3)) * adjust[i % 3]); 8 | } 9 | 10 | // the j such that quantizedNumber(j) is closest to i 11 | function quantizedIndex(i) { 12 | if(i < 1) { return 0; } 13 | var group = Math.floor(Math.log(i) / Math.LN10), 14 | offset = i/(2.5 * Math.pow(10, group)); 15 | if(offset >= 3) { 16 | group++; 17 | offset = 0; 18 | } 19 | return 3*group + Math.round(Math.min(2, offset)); 20 | } 21 | 22 | function quantize(i) { 23 | if(i === Infinity) { return Infinity; } 24 | return quantizedNumber(quantizedIndex(i)); 25 | } 26 | 27 | // don't overwrite default response transforms 28 | function appendTransform(defaults, transform) { 29 | defaults = angular.isArray(defaults) ? defaults : [defaults]; 30 | return (transform) ? defaults.concat(transform) : defaults; 31 | } 32 | 33 | function parseRange(hdr) { 34 | var m = hdr && hdr.match(/^(?:items )?(\d+)-(\d+)\/(\d+|\*)$/); 35 | if(m) { 36 | return { 37 | from: +m[1], 38 | to: +m[2], 39 | total: m[3] === '*' ? Infinity : +m[3] 40 | }; 41 | } else if(hdr === '*/0') { 42 | return { total: 0 }; 43 | } 44 | return null; 45 | } 46 | 47 | function length(range) { 48 | return range.to - range.from + 1; 49 | } 50 | 51 | angular.module('bgf.paginateAnything', []). 52 | 53 | directive('bgfPagination', function () { 54 | var defaultLinkGroupSize = 3, defaultClientLimit = 250, defaultPerPage = 50; 55 | 56 | return { 57 | restrict: 'AE', 58 | scope: { 59 | // required 60 | url: '=', 61 | collection: '=', 62 | 63 | // optional 64 | urlParams: '=?', 65 | headers: '=?', 66 | page: '=?', 67 | perPage: '=?', 68 | perPagePresets: '=?', 69 | autoPresets: '=?', 70 | clientLimit: '=?', 71 | linkGroupSize: '=?', 72 | reloadPage: '=?', 73 | size: '=?', 74 | passive: '@', 75 | transformResponse: '=?', 76 | method: '@', 77 | postData: '=?', 78 | loadFn: '&', 79 | 80 | // directive -> app communication only 81 | numPages: '=?', 82 | numItems: '=?', 83 | serverLimit: '=?', 84 | rangeFrom: '=?', 85 | rangeTo: '=?' 86 | }, 87 | templateUrl: function(element, attr) { 88 | return attr.templateUrl || 'src/paginate-anything.html'; 89 | }, 90 | replace: true, 91 | controller: ['$scope', '$attrs', '$http', function($scope, $attrs, $http) { 92 | 93 | $scope.reloadPage = false; 94 | $scope.serverLimit = Infinity; // it's not known yet 95 | $scope.Math = window.Math; // Math for the template 96 | var useLoadFn = $attrs.loadFn !== undefined; // directive's '&' params are always set, need to determine from $attrs whether to use loadFn 97 | 98 | if(typeof $scope.autoPresets !== 'boolean') { 99 | $scope.autoPresets = true; 100 | } 101 | 102 | var lgs = $scope.linkGroupSize, cl = $scope.clientLimit; 103 | $scope.linkGroupSize = typeof lgs === 'number' ? lgs : defaultLinkGroupSize; 104 | $scope.clientLimit = typeof cl === 'number' ? cl : defaultClientLimit; 105 | 106 | $scope.updatePresets = function () { 107 | if($scope.autoPresets) { 108 | var presets = [], i; 109 | for(i = Math.min(3, quantizedIndex($scope.perPage || defaultPerPage)); 110 | i <= quantizedIndex(Math.min($scope.clientLimit, $scope.serverLimit)); 111 | i++) { 112 | presets.push(quantizedNumber(i)); 113 | } 114 | $scope.perPagePresets = presets; 115 | } else { 116 | $scope.perPagePresets = $scope.perPagePresets.filter( 117 | function (preset) { return preset <= $scope.serverLimit; } 118 | ).concat([$scope.serverLimit]); 119 | } 120 | }; 121 | 122 | $scope.gotoPage = function (i) { 123 | if(i < 0 || i*$scope.perPage >= $scope.numItems) { 124 | return; 125 | } 126 | $scope.page = i; 127 | }; 128 | 129 | $scope.linkGroupFirst = function() { 130 | var rightDebt = Math.max( 0, 131 | $scope.linkGroupSize - ($scope.numPages - 1 - ($scope.page + 2)) 132 | ); 133 | return Math.max( 0, 134 | $scope.page - ($scope.linkGroupSize + rightDebt) 135 | ); 136 | }; 137 | 138 | $scope.linkGroupLast = function() { 139 | var leftDebt = Math.max( 0, 140 | $scope.linkGroupSize - ($scope.page - 2) 141 | ); 142 | return Math.min( $scope.numPages-1, 143 | $scope.page + ($scope.linkGroupSize + leftDebt) 144 | ); 145 | }; 146 | 147 | $scope.isFinite = function() { 148 | return $scope.numPages < Infinity; 149 | }; 150 | 151 | function requestRange(request) { 152 | if($scope.passive === 'true' || !$scope.url && !useLoadFn) { return; } 153 | $scope.$emit('pagination:loadStart', request); 154 | 155 | var config = { 156 | method: $scope.method || 'GET', 157 | url: $scope.url, 158 | params: $scope.urlParams, 159 | data: $scope.postData, 160 | headers: angular.extend( 161 | {}, $scope.headers, 162 | { 'Range-Unit': 'items', Range: [request.from, request.to].join('-') } 163 | ), 164 | transformResponse: appendTransform($http.defaults.transformResponse, $scope.transformResponse) 165 | }; 166 | var responsePromise = useLoadFn ? $scope.loadFn({config: config}) : $http(config); 167 | responsePromise.then(function (rsp) { 168 | var response = parseRange(rsp.headers('Content-Range')); 169 | if(rsp.status === 204 || (response && response.total === 0)) { 170 | $scope.numItems = 0; 171 | $scope.collection = []; 172 | } else { 173 | $scope.numItems = response ? response.total : rsp.data.length; 174 | $scope.collection = rsp.data || []; 175 | } 176 | 177 | if(response) { 178 | $scope.rangeFrom = response.from; 179 | $scope.rangeTo = response.to; 180 | if(length(response) < response.total) { 181 | if( 182 | ( request.to < response.total - 1) || 183 | (response.to < response.total - 1 && response.total < request.to) 184 | ) { 185 | if(!$scope.perPage || length(response) < $scope.perPage) { 186 | if($scope.autoPresets) { 187 | var idx = quantizedIndex(length(response)); 188 | if(quantizedNumber(idx) > length(response)) { 189 | idx--; 190 | } 191 | $scope.serverLimit = quantizedNumber(idx); 192 | } else { 193 | $scope.serverLimit = length(response); 194 | } 195 | $scope.perPage = $scope.Math.min( 196 | $scope.serverLimit, 197 | $scope.clientLimit 198 | ); 199 | } 200 | } 201 | } 202 | } 203 | $scope.numPages = Math.ceil($scope.numItems / ($scope.perPage || defaultPerPage)); 204 | 205 | $scope.$emit('pagination:loadPage', rsp.status, rsp.config); 206 | }, function (rsp) { 207 | $scope.$emit('pagination:error', rsp.status, rsp.config); 208 | }); 209 | } 210 | 211 | $scope.page = $scope.page || 0; 212 | $scope.size = $scope.size || 'md'; 213 | if($scope.autoPresets) { 214 | $scope.updatePresets(); 215 | } 216 | 217 | $scope.$watch('page', function(newPage, oldPage) { 218 | if($scope.passive === 'true') { return; } 219 | 220 | if(newPage !== oldPage) { 221 | if(newPage < 0 || newPage*$scope.perPage >= $scope.numItems) { 222 | return; 223 | } 224 | 225 | var pp = $scope.perPage || defaultPerPage; 226 | 227 | if($scope.autoPresets) { 228 | pp = quantize(pp); 229 | } 230 | 231 | requestRange({ 232 | from: newPage * pp, 233 | to: (newPage+1) * pp - 1 234 | }); 235 | } 236 | }); 237 | 238 | $scope.$watch('perPage', function(newPp, oldPp) { 239 | if($scope.passive === 'true') { return; } 240 | 241 | if(typeof(oldPp) === 'number' && newPp !== oldPp) { 242 | var first = $scope.page * oldPp; 243 | var newPage = Math.floor(first / newPp); 244 | 245 | if($scope.page !== newPage) { 246 | $scope.page = newPage; 247 | } else { 248 | requestRange({ 249 | from: $scope.page * newPp, 250 | to: ($scope.page+1) * newPp - 1 251 | }); 252 | } 253 | } 254 | }); 255 | 256 | $scope.$watch('serverLimit', function(newLimit, oldLimit) { 257 | if($scope.passive === 'true') { return; } 258 | 259 | if(newLimit !== oldLimit) { 260 | $scope.updatePresets(); 261 | } 262 | }); 263 | 264 | $scope.$watch('url', function(newUrl, oldUrl) { 265 | if($scope.passive === 'true') { return; } 266 | 267 | if(newUrl !== oldUrl) { 268 | if($scope.page === 0){ 269 | $scope.reloadPage = true; 270 | } else { 271 | $scope.page = 0; 272 | } 273 | } 274 | }); 275 | 276 | $scope.$watch('urlParams', function(newParams, oldParams) { 277 | if($scope.passive === 'true') { return; } 278 | 279 | if(!angular.equals(newParams, oldParams)) { 280 | if($scope.page === 0){ 281 | $scope.reloadPage = true; 282 | } else { 283 | $scope.page = 0; 284 | } 285 | } 286 | }, true); 287 | 288 | $scope.$watch('headers', function(newHeaders, oldHeaders) { 289 | if($scope.passive === 'true') { return; } 290 | 291 | if(!angular.equals(newHeaders, oldHeaders)) { 292 | if($scope.page === 0){ 293 | $scope.reloadPage = true; 294 | } else { 295 | $scope.page = 0; 296 | } 297 | } 298 | }, true); 299 | 300 | $scope.$watch('reloadPage', function(newVal, oldVal) { 301 | if($scope.passive === 'true') { return; } 302 | 303 | if(newVal === true && oldVal === false) { 304 | var pp = $scope.perPage || defaultPerPage; 305 | $scope.reloadPage = false; 306 | requestRange({ 307 | from: $scope.page * pp, 308 | to: ($scope.page+1) * pp - 1 309 | }); 310 | } 311 | }); 312 | 313 | $scope.$watch('transformResponse', function(newTransform, oldTransform) { 314 | if($scope.passive === 'true') { return; } 315 | if(!newTransform || !oldTransform) { return; } 316 | 317 | // If applying a transform to returned data, it makes sense to start at the first page if changed 318 | // Unfortunately it's not really possible to compare function equality 319 | // In lieu of that, for now we'll compare string representations of them. 320 | if(!angular.equals(newTransform.toString(), oldTransform.toString())) { 321 | if($scope.page === 0){ 322 | $scope.reloadPage = true; 323 | } else { 324 | $scope.page = 0; 325 | } 326 | } 327 | }, true); 328 | 329 | $scope.$on('pagination:reload', function() { 330 | $scope.reloadPage = true; 331 | }); 332 | 333 | var pp = $scope.perPage || defaultPerPage; 334 | 335 | if($scope.autoPresets) { 336 | pp = quantize(pp); 337 | } 338 | 339 | requestRange({ 340 | from: $scope.page * pp, 341 | to: ($scope.page+1) * pp - 1 342 | }); 343 | }] 344 | }; 345 | }). 346 | 347 | filter('makeRange', function() { 348 | // http://stackoverflow.com/a/14932395/3102996 349 | return function(input) { 350 | var lowBound, highBound; 351 | switch (input.length) { 352 | case 1: 353 | lowBound = 0; 354 | highBound = parseInt(input[0], 10) - 1; 355 | break; 356 | case 2: 357 | lowBound = parseInt(input[0], 10); 358 | highBound = parseInt(input[1], 10); 359 | break; 360 | default: 361 | return input; 362 | } 363 | var result = []; 364 | for (var i = lowBound; i <= highBound; i++) { result.push(i); } 365 | return result; 366 | }; 367 | }); 368 | }()); 369 | 370 | angular.module('bgf.paginateAnything').run(['$templateCache', function($templateCache) { 371 | 'use strict'; 372 | 373 | $templateCache.put('src/paginate-anything.html', 374 | "
0 && numPages > 1\">per page
" 375 | ); 376 | 377 | }]); 378 | -------------------------------------------------------------------------------- /dist/paginate-anything-tpls.min.js: -------------------------------------------------------------------------------- 1 | // angular-paginate-anything - v4.2.0 2 | !function(){"use strict";function a(a){var b=[1,2.5,5];return Math.floor(Math.pow(10,Math.floor(a/3))*b[a%3])}function b(a){if(a<1)return 0;var b=Math.floor(Math.log(a)/Math.LN10),c=a/(2.5*Math.pow(10,b));return c>=3&&(b++,c=0),3*b+Math.round(Math.min(2,c))}function c(c){return c===1/0?1/0:a(b(c))}function d(a,b){return a=angular.isArray(a)?a:[a],b?a.concat(b):a}function e(a){var b=a&&a.match(/^(?:items )?(\d+)-(\d+)\/(\d+|\*)$/);return b?{from:+b[1],to:+b[2],total:"*"===b[3]?1/0:+b[3]}:"*/0"===a?{total:0}:null}function f(a){return a.to-a.from+1}angular.module("bgf.paginateAnything",[]).directive("bgfPagination",function(){var g=3,h=250,i=50;return{restrict:"AE",scope:{url:"=",collection:"=",urlParams:"=?",headers:"=?",page:"=?",perPage:"=?",perPagePresets:"=?",autoPresets:"=?",clientLimit:"=?",linkGroupSize:"=?",reloadPage:"=?",size:"=?",passive:"@",transformResponse:"=?",method:"@",postData:"=?",loadFn:"&",numPages:"=?",numItems:"=?",serverLimit:"=?",rangeFrom:"=?",rangeTo:"=?"},templateUrl:function(a,b){return b.templateUrl||"src/paginate-anything.html"},replace:!0,controller:["$scope","$attrs","$http",function(j,k,l){function m(c){if("true"!==j.passive&&(j.url||n)){j.$emit("pagination:loadStart",c);var g={method:j.method||"GET",url:j.url,params:j.urlParams,data:j.postData,headers:angular.extend({},j.headers,{"Range-Unit":"items",Range:[c.from,c.to].join("-")}),transformResponse:d(l.defaults.transformResponse,j.transformResponse)},h=n?j.loadFn({config:g}):l(g);h.then(function(d){var g=e(d.headers("Content-Range"));if(204===d.status||g&&0===g.total?(j.numItems=0,j.collection=[]):(j.numItems=g?g.total:d.data.length,j.collection=d.data||[]),g&&(j.rangeFrom=g.from,j.rangeTo=g.to,f(g)f(g)&&h--,j.serverLimit=a(h)}else j.serverLimit=f(g);j.perPage=j.Math.min(j.serverLimit,j.clientLimit)}j.numPages=Math.ceil(j.numItems/(j.perPage||i)),j.$emit("pagination:loadPage",d.status,d.config)},function(a){j.$emit("pagination:error",a.status,a.config)})}}j.reloadPage=!1,j.serverLimit=1/0,j.Math=window.Math;var n=void 0!==k.loadFn;"boolean"!=typeof j.autoPresets&&(j.autoPresets=!0);var o=j.linkGroupSize,p=j.clientLimit;j.linkGroupSize="number"==typeof o?o:g,j.clientLimit="number"==typeof p?p:h,j.updatePresets=function(){if(j.autoPresets){var c,d=[];for(c=Math.min(3,b(j.perPage||i));c<=b(Math.min(j.clientLimit,j.serverLimit));c++)d.push(a(c));j.perPagePresets=d}else j.perPagePresets=j.perPagePresets.filter(function(a){return a<=j.serverLimit}).concat([j.serverLimit])},j.gotoPage=function(a){a<0||a*j.perPage>=j.numItems||(j.page=a)},j.linkGroupFirst=function(){var a=Math.max(0,j.linkGroupSize-(j.numPages-1-(j.page+2)));return Math.max(0,j.page-(j.linkGroupSize+a))},j.linkGroupLast=function(){var a=Math.max(0,j.linkGroupSize-(j.page-2));return Math.min(j.numPages-1,j.page+(j.linkGroupSize+a))},j.isFinite=function(){return j.numPages<1/0},j.page=j.page||0,j.size=j.size||"md",j.autoPresets&&j.updatePresets(),j.$watch("page",function(a,b){if("true"!==j.passive&&a!==b){if(a<0||a*j.perPage>=j.numItems)return;var d=j.perPage||i;j.autoPresets&&(d=c(d)),m({from:a*d,to:(a+1)*d-1})}}),j.$watch("perPage",function(a,b){if("true"!==j.passive&&"number"==typeof b&&a!==b){var c=j.page*b,d=Math.floor(c/a);j.page!==d?j.page=d:m({from:j.page*a,to:(j.page+1)*a-1})}}),j.$watch("serverLimit",function(a,b){"true"!==j.passive&&a!==b&&j.updatePresets()}),j.$watch("url",function(a,b){"true"!==j.passive&&a!==b&&(0===j.page?j.reloadPage=!0:j.page=0)}),j.$watch("urlParams",function(a,b){"true"!==j.passive&&(angular.equals(a,b)||(0===j.page?j.reloadPage=!0:j.page=0))},!0),j.$watch("headers",function(a,b){"true"!==j.passive&&(angular.equals(a,b)||(0===j.page?j.reloadPage=!0:j.page=0))},!0),j.$watch("reloadPage",function(a,b){if("true"!==j.passive&&a===!0&&b===!1){var c=j.perPage||i;j.reloadPage=!1,m({from:j.page*c,to:(j.page+1)*c-1})}}),j.$watch("transformResponse",function(a,b){"true"!==j.passive&&a&&b&&(angular.equals(a.toString(),b.toString())||(0===j.page?j.reloadPage=!0:j.page=0))},!0),j.$on("pagination:reload",function(){j.reloadPage=!0});var q=j.perPage||i;j.autoPresets&&(q=c(q)),m({from:j.page*q,to:(j.page+1)*q-1})}]}}).filter("makeRange",function(){return function(a){var b,c;switch(a.length){case 1:b=0,c=parseInt(a[0],10)-1;break;case 2:b=parseInt(a[0],10),c=parseInt(a[1],10);break;default:return a}for(var d=[],e=b;e<=c;e++)d.push(e);return d}})}(),angular.module("bgf.paginateAnything").run(["$templateCache",function(a){"use strict";a.put("src/paginate-anything.html",'
per page
')}]); -------------------------------------------------------------------------------- /dist/paginate-anything.html: -------------------------------------------------------------------------------- 1 |
2 | 21 |
22 | 25 | per page 26 |
27 |
28 | -------------------------------------------------------------------------------- /dist/paginate-anything.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // 1 2 5 10 25 50 100 250 500 etc 5 | function quantizedNumber(i) { 6 | var adjust = [1, 2.5, 5]; 7 | return Math.floor(Math.pow(10, Math.floor(i/3)) * adjust[i % 3]); 8 | } 9 | 10 | // the j such that quantizedNumber(j) is closest to i 11 | function quantizedIndex(i) { 12 | if(i < 1) { return 0; } 13 | var group = Math.floor(Math.log(i) / Math.LN10), 14 | offset = i/(2.5 * Math.pow(10, group)); 15 | if(offset >= 3) { 16 | group++; 17 | offset = 0; 18 | } 19 | return 3*group + Math.round(Math.min(2, offset)); 20 | } 21 | 22 | function quantize(i) { 23 | if(i === Infinity) { return Infinity; } 24 | return quantizedNumber(quantizedIndex(i)); 25 | } 26 | 27 | // don't overwrite default response transforms 28 | function appendTransform(defaults, transform) { 29 | defaults = angular.isArray(defaults) ? defaults : [defaults]; 30 | return (transform) ? defaults.concat(transform) : defaults; 31 | } 32 | 33 | function parseRange(hdr) { 34 | var m = hdr && hdr.match(/^(?:items )?(\d+)-(\d+)\/(\d+|\*)$/); 35 | if(m) { 36 | return { 37 | from: +m[1], 38 | to: +m[2], 39 | total: m[3] === '*' ? Infinity : +m[3] 40 | }; 41 | } else if(hdr === '*/0') { 42 | return { total: 0 }; 43 | } 44 | return null; 45 | } 46 | 47 | function length(range) { 48 | return range.to - range.from + 1; 49 | } 50 | 51 | angular.module('bgf.paginateAnything', []). 52 | 53 | directive('bgfPagination', function () { 54 | var defaultLinkGroupSize = 3, defaultClientLimit = 250, defaultPerPage = 50; 55 | 56 | return { 57 | restrict: 'AE', 58 | scope: { 59 | // required 60 | url: '=', 61 | collection: '=', 62 | 63 | // optional 64 | urlParams: '=?', 65 | headers: '=?', 66 | page: '=?', 67 | perPage: '=?', 68 | perPagePresets: '=?', 69 | autoPresets: '=?', 70 | clientLimit: '=?', 71 | linkGroupSize: '=?', 72 | reloadPage: '=?', 73 | size: '=?', 74 | passive: '@', 75 | transformResponse: '=?', 76 | method: '@', 77 | postData: '=?', 78 | loadFn: '&', 79 | 80 | // directive -> app communication only 81 | numPages: '=?', 82 | numItems: '=?', 83 | serverLimit: '=?', 84 | rangeFrom: '=?', 85 | rangeTo: '=?' 86 | }, 87 | templateUrl: function(element, attr) { 88 | return attr.templateUrl || 'src/paginate-anything.html'; 89 | }, 90 | replace: true, 91 | controller: ['$scope', '$attrs', '$http', function($scope, $attrs, $http) { 92 | 93 | $scope.reloadPage = false; 94 | $scope.serverLimit = Infinity; // it's not known yet 95 | $scope.Math = window.Math; // Math for the template 96 | var useLoadFn = $attrs.loadFn !== undefined; // directive's '&' params are always set, need to determine from $attrs whether to use loadFn 97 | 98 | if(typeof $scope.autoPresets !== 'boolean') { 99 | $scope.autoPresets = true; 100 | } 101 | 102 | var lgs = $scope.linkGroupSize, cl = $scope.clientLimit; 103 | $scope.linkGroupSize = typeof lgs === 'number' ? lgs : defaultLinkGroupSize; 104 | $scope.clientLimit = typeof cl === 'number' ? cl : defaultClientLimit; 105 | 106 | $scope.updatePresets = function () { 107 | if($scope.autoPresets) { 108 | var presets = [], i; 109 | for(i = Math.min(3, quantizedIndex($scope.perPage || defaultPerPage)); 110 | i <= quantizedIndex(Math.min($scope.clientLimit, $scope.serverLimit)); 111 | i++) { 112 | presets.push(quantizedNumber(i)); 113 | } 114 | $scope.perPagePresets = presets; 115 | } else { 116 | $scope.perPagePresets = $scope.perPagePresets.filter( 117 | function (preset) { return preset <= $scope.serverLimit; } 118 | ).concat([$scope.serverLimit]); 119 | } 120 | }; 121 | 122 | $scope.gotoPage = function (i) { 123 | if(i < 0 || i*$scope.perPage >= $scope.numItems) { 124 | return; 125 | } 126 | $scope.page = i; 127 | }; 128 | 129 | $scope.linkGroupFirst = function() { 130 | var rightDebt = Math.max( 0, 131 | $scope.linkGroupSize - ($scope.numPages - 1 - ($scope.page + 2)) 132 | ); 133 | return Math.max( 0, 134 | $scope.page - ($scope.linkGroupSize + rightDebt) 135 | ); 136 | }; 137 | 138 | $scope.linkGroupLast = function() { 139 | var leftDebt = Math.max( 0, 140 | $scope.linkGroupSize - ($scope.page - 2) 141 | ); 142 | return Math.min( $scope.numPages-1, 143 | $scope.page + ($scope.linkGroupSize + leftDebt) 144 | ); 145 | }; 146 | 147 | $scope.isFinite = function() { 148 | return $scope.numPages < Infinity; 149 | }; 150 | 151 | function requestRange(request) { 152 | if($scope.passive === 'true' || !$scope.url && !useLoadFn) { return; } 153 | $scope.$emit('pagination:loadStart', request); 154 | 155 | var config = { 156 | method: $scope.method || 'GET', 157 | url: $scope.url, 158 | params: $scope.urlParams, 159 | data: $scope.postData, 160 | headers: angular.extend( 161 | {}, $scope.headers, 162 | { 'Range-Unit': 'items', Range: [request.from, request.to].join('-') } 163 | ), 164 | transformResponse: appendTransform($http.defaults.transformResponse, $scope.transformResponse) 165 | }; 166 | var responsePromise = useLoadFn ? $scope.loadFn({config: config}) : $http(config); 167 | responsePromise.then(function (rsp) { 168 | var response = parseRange(rsp.headers('Content-Range')); 169 | if(rsp.status === 204 || (response && response.total === 0)) { 170 | $scope.numItems = 0; 171 | $scope.collection = []; 172 | } else { 173 | $scope.numItems = response ? response.total : rsp.data.length; 174 | $scope.collection = rsp.data || []; 175 | } 176 | 177 | if(response) { 178 | $scope.rangeFrom = response.from; 179 | $scope.rangeTo = response.to; 180 | if(length(response) < response.total) { 181 | if( 182 | ( request.to < response.total - 1) || 183 | (response.to < response.total - 1 && response.total < request.to) 184 | ) { 185 | if(!$scope.perPage || length(response) < $scope.perPage) { 186 | if($scope.autoPresets) { 187 | var idx = quantizedIndex(length(response)); 188 | if(quantizedNumber(idx) > length(response)) { 189 | idx--; 190 | } 191 | $scope.serverLimit = quantizedNumber(idx); 192 | } else { 193 | $scope.serverLimit = length(response); 194 | } 195 | $scope.perPage = $scope.Math.min( 196 | $scope.serverLimit, 197 | $scope.clientLimit 198 | ); 199 | } 200 | } 201 | } 202 | } 203 | $scope.numPages = Math.ceil($scope.numItems / ($scope.perPage || defaultPerPage)); 204 | 205 | $scope.$emit('pagination:loadPage', rsp.status, rsp.config); 206 | }, function (rsp) { 207 | $scope.$emit('pagination:error', rsp.status, rsp.config); 208 | }); 209 | } 210 | 211 | $scope.page = $scope.page || 0; 212 | $scope.size = $scope.size || 'md'; 213 | if($scope.autoPresets) { 214 | $scope.updatePresets(); 215 | } 216 | 217 | $scope.$watch('page', function(newPage, oldPage) { 218 | if($scope.passive === 'true') { return; } 219 | 220 | if(newPage !== oldPage) { 221 | if(newPage < 0 || newPage*$scope.perPage >= $scope.numItems) { 222 | return; 223 | } 224 | 225 | var pp = $scope.perPage || defaultPerPage; 226 | 227 | if($scope.autoPresets) { 228 | pp = quantize(pp); 229 | } 230 | 231 | requestRange({ 232 | from: newPage * pp, 233 | to: (newPage+1) * pp - 1 234 | }); 235 | } 236 | }); 237 | 238 | $scope.$watch('perPage', function(newPp, oldPp) { 239 | if($scope.passive === 'true') { return; } 240 | 241 | if(typeof(oldPp) === 'number' && newPp !== oldPp) { 242 | var first = $scope.page * oldPp; 243 | var newPage = Math.floor(first / newPp); 244 | 245 | if($scope.page !== newPage) { 246 | $scope.page = newPage; 247 | } else { 248 | requestRange({ 249 | from: $scope.page * newPp, 250 | to: ($scope.page+1) * newPp - 1 251 | }); 252 | } 253 | } 254 | }); 255 | 256 | $scope.$watch('serverLimit', function(newLimit, oldLimit) { 257 | if($scope.passive === 'true') { return; } 258 | 259 | if(newLimit !== oldLimit) { 260 | $scope.updatePresets(); 261 | } 262 | }); 263 | 264 | $scope.$watch('url', function(newUrl, oldUrl) { 265 | if($scope.passive === 'true') { return; } 266 | 267 | if(newUrl !== oldUrl) { 268 | if($scope.page === 0){ 269 | $scope.reloadPage = true; 270 | } else { 271 | $scope.page = 0; 272 | } 273 | } 274 | }); 275 | 276 | $scope.$watch('urlParams', function(newParams, oldParams) { 277 | if($scope.passive === 'true') { return; } 278 | 279 | if(!angular.equals(newParams, oldParams)) { 280 | if($scope.page === 0){ 281 | $scope.reloadPage = true; 282 | } else { 283 | $scope.page = 0; 284 | } 285 | } 286 | }, true); 287 | 288 | $scope.$watch('headers', function(newHeaders, oldHeaders) { 289 | if($scope.passive === 'true') { return; } 290 | 291 | if(!angular.equals(newHeaders, oldHeaders)) { 292 | if($scope.page === 0){ 293 | $scope.reloadPage = true; 294 | } else { 295 | $scope.page = 0; 296 | } 297 | } 298 | }, true); 299 | 300 | $scope.$watch('reloadPage', function(newVal, oldVal) { 301 | if($scope.passive === 'true') { return; } 302 | 303 | if(newVal === true && oldVal === false) { 304 | var pp = $scope.perPage || defaultPerPage; 305 | $scope.reloadPage = false; 306 | requestRange({ 307 | from: $scope.page * pp, 308 | to: ($scope.page+1) * pp - 1 309 | }); 310 | } 311 | }); 312 | 313 | $scope.$watch('transformResponse', function(newTransform, oldTransform) { 314 | if($scope.passive === 'true') { return; } 315 | if(!newTransform || !oldTransform) { return; } 316 | 317 | // If applying a transform to returned data, it makes sense to start at the first page if changed 318 | // Unfortunately it's not really possible to compare function equality 319 | // In lieu of that, for now we'll compare string representations of them. 320 | if(!angular.equals(newTransform.toString(), oldTransform.toString())) { 321 | if($scope.page === 0){ 322 | $scope.reloadPage = true; 323 | } else { 324 | $scope.page = 0; 325 | } 326 | } 327 | }, true); 328 | 329 | $scope.$on('pagination:reload', function() { 330 | $scope.reloadPage = true; 331 | }); 332 | 333 | var pp = $scope.perPage || defaultPerPage; 334 | 335 | if($scope.autoPresets) { 336 | pp = quantize(pp); 337 | } 338 | 339 | requestRange({ 340 | from: $scope.page * pp, 341 | to: ($scope.page+1) * pp - 1 342 | }); 343 | }] 344 | }; 345 | }). 346 | 347 | filter('makeRange', function() { 348 | // http://stackoverflow.com/a/14932395/3102996 349 | return function(input) { 350 | var lowBound, highBound; 351 | switch (input.length) { 352 | case 1: 353 | lowBound = 0; 354 | highBound = parseInt(input[0], 10) - 1; 355 | break; 356 | case 2: 357 | lowBound = parseInt(input[0], 10); 358 | highBound = parseInt(input[1], 10); 359 | break; 360 | default: 361 | return input; 362 | } 363 | var result = []; 364 | for (var i = lowBound; i <= highBound; i++) { result.push(i); } 365 | return result; 366 | }; 367 | }); 368 | }()); 369 | -------------------------------------------------------------------------------- /dist/paginate-anything.min.js: -------------------------------------------------------------------------------- 1 | // angular-paginate-anything - v4.2.0 2 | !function(){"use strict";function a(a){var b=[1,2.5,5];return Math.floor(Math.pow(10,Math.floor(a/3))*b[a%3])}function b(a){if(a<1)return 0;var b=Math.floor(Math.log(a)/Math.LN10),c=a/(2.5*Math.pow(10,b));return c>=3&&(b++,c=0),3*b+Math.round(Math.min(2,c))}function c(c){return c===1/0?1/0:a(b(c))}function d(a,b){return a=angular.isArray(a)?a:[a],b?a.concat(b):a}function e(a){var b=a&&a.match(/^(?:items )?(\d+)-(\d+)\/(\d+|\*)$/);return b?{from:+b[1],to:+b[2],total:"*"===b[3]?1/0:+b[3]}:"*/0"===a?{total:0}:null}function f(a){return a.to-a.from+1}angular.module("bgf.paginateAnything",[]).directive("bgfPagination",function(){var g=3,h=250,i=50;return{restrict:"AE",scope:{url:"=",collection:"=",urlParams:"=?",headers:"=?",page:"=?",perPage:"=?",perPagePresets:"=?",autoPresets:"=?",clientLimit:"=?",linkGroupSize:"=?",reloadPage:"=?",size:"=?",passive:"@",transformResponse:"=?",method:"@",postData:"=?",loadFn:"&",numPages:"=?",numItems:"=?",serverLimit:"=?",rangeFrom:"=?",rangeTo:"=?"},templateUrl:function(a,b){return b.templateUrl||"src/paginate-anything.html"},replace:!0,controller:["$scope","$attrs","$http",function(j,k,l){function m(c){if("true"!==j.passive&&(j.url||n)){j.$emit("pagination:loadStart",c);var g={method:j.method||"GET",url:j.url,params:j.urlParams,data:j.postData,headers:angular.extend({},j.headers,{"Range-Unit":"items",Range:[c.from,c.to].join("-")}),transformResponse:d(l.defaults.transformResponse,j.transformResponse)},h=n?j.loadFn({config:g}):l(g);h.then(function(d){var g=e(d.headers("Content-Range"));if(204===d.status||g&&0===g.total?(j.numItems=0,j.collection=[]):(j.numItems=g?g.total:d.data.length,j.collection=d.data||[]),g&&(j.rangeFrom=g.from,j.rangeTo=g.to,f(g)f(g)&&h--,j.serverLimit=a(h)}else j.serverLimit=f(g);j.perPage=j.Math.min(j.serverLimit,j.clientLimit)}j.numPages=Math.ceil(j.numItems/(j.perPage||i)),j.$emit("pagination:loadPage",d.status,d.config)},function(a){j.$emit("pagination:error",a.status,a.config)})}}j.reloadPage=!1,j.serverLimit=1/0,j.Math=window.Math;var n=void 0!==k.loadFn;"boolean"!=typeof j.autoPresets&&(j.autoPresets=!0);var o=j.linkGroupSize,p=j.clientLimit;j.linkGroupSize="number"==typeof o?o:g,j.clientLimit="number"==typeof p?p:h,j.updatePresets=function(){if(j.autoPresets){var c,d=[];for(c=Math.min(3,b(j.perPage||i));c<=b(Math.min(j.clientLimit,j.serverLimit));c++)d.push(a(c));j.perPagePresets=d}else j.perPagePresets=j.perPagePresets.filter(function(a){return a<=j.serverLimit}).concat([j.serverLimit])},j.gotoPage=function(a){a<0||a*j.perPage>=j.numItems||(j.page=a)},j.linkGroupFirst=function(){var a=Math.max(0,j.linkGroupSize-(j.numPages-1-(j.page+2)));return Math.max(0,j.page-(j.linkGroupSize+a))},j.linkGroupLast=function(){var a=Math.max(0,j.linkGroupSize-(j.page-2));return Math.min(j.numPages-1,j.page+(j.linkGroupSize+a))},j.isFinite=function(){return j.numPages<1/0},j.page=j.page||0,j.size=j.size||"md",j.autoPresets&&j.updatePresets(),j.$watch("page",function(a,b){if("true"!==j.passive&&a!==b){if(a<0||a*j.perPage>=j.numItems)return;var d=j.perPage||i;j.autoPresets&&(d=c(d)),m({from:a*d,to:(a+1)*d-1})}}),j.$watch("perPage",function(a,b){if("true"!==j.passive&&"number"==typeof b&&a!==b){var c=j.page*b,d=Math.floor(c/a);j.page!==d?j.page=d:m({from:j.page*a,to:(j.page+1)*a-1})}}),j.$watch("serverLimit",function(a,b){"true"!==j.passive&&a!==b&&j.updatePresets()}),j.$watch("url",function(a,b){"true"!==j.passive&&a!==b&&(0===j.page?j.reloadPage=!0:j.page=0)}),j.$watch("urlParams",function(a,b){"true"!==j.passive&&(angular.equals(a,b)||(0===j.page?j.reloadPage=!0:j.page=0))},!0),j.$watch("headers",function(a,b){"true"!==j.passive&&(angular.equals(a,b)||(0===j.page?j.reloadPage=!0:j.page=0))},!0),j.$watch("reloadPage",function(a,b){if("true"!==j.passive&&a===!0&&b===!1){var c=j.perPage||i;j.reloadPage=!1,m({from:j.page*c,to:(j.page+1)*c-1})}}),j.$watch("transformResponse",function(a,b){"true"!==j.passive&&a&&b&&(angular.equals(a.toString(),b.toString())||(0===j.page?j.reloadPage=!0:j.page=0))},!0),j.$on("pagination:reload",function(){j.reloadPage=!0});var q=j.perPage||i;j.autoPresets&&(q=c(q)),m({from:j.page*q,to:(j.page+1)*q-1})}]}}).filter("makeRange",function(){return function(a){var b,c;switch(a.length){case 1:b=0,c=parseInt(a[0],10)-1;break;case 2:b=parseInt(a[0],10),c=parseInt(a[1],10);break;default:return a}for(var d=[],e=b;e<=c;e++)d.push(e);return d}})}(); -------------------------------------------------------------------------------- /img/link-group-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/begriffs/angular-paginate-anything/b9e3fddace64b53d8301a09a2490f32ff80ae554/img/link-group-size.png -------------------------------------------------------------------------------- /img/paginate-anything-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/begriffs/angular-paginate-anything/b9e3fddace64b53d8301a09a2490f32ff80ae554/img/paginate-anything-logo.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./dist/paginate-anything-tpls.js'); 2 | module.exports = 'bgf.paginateAnything'; 3 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Jan 15 2014 12:28:49 GMT-0800 (PST) 3 | 4 | module.exports = function(config) { 5 | 'use strict'; 6 | config.set({ 7 | 8 | // base path, that will be used to resolve files and exclude 9 | basePath: '', 10 | 11 | 12 | // frameworks to use 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'bower_components/angular/angular.js', 19 | 'bower_components/angular-mocks/angular-mocks.js', 20 | 'bower_components/jasmine-as-promised/src/jasmine-as-promised.js', 21 | 'src/paginate-anything.js', 22 | 'test/**/*.js', 23 | 24 | 'src/*.html' 25 | ], 26 | 27 | 28 | // populate the angular template cache for tests 29 | preprocessors: { 30 | 'src/*.html': ['ng-html2js'] 31 | }, 32 | 33 | 34 | // test results reporter to use 35 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 36 | reporters: ['progress'], 37 | 38 | 39 | // web server port 40 | port: 9876, 41 | 42 | 43 | // enable / disable colors in the output (reporters and logs) 44 | colors: true, 45 | 46 | 47 | // level of logging 48 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 49 | logLevel: config.LOG_INFO, 50 | 51 | 52 | // enable / disable watching file and executing tests whenever any file changes 53 | autoWatch: false, 54 | 55 | 56 | plugins:[ 57 | 'karma-jasmine', 58 | 'karma-phantomjs-launcher', 59 | 'karma-ng-html2js-preprocessor' 60 | ], 61 | 62 | 63 | // Start these browsers, currently available: 64 | // - Chrome 65 | // - ChromeCanary 66 | // - Firefox 67 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 68 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 69 | // - PhantomJS 70 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 71 | browsers: ['PhantomJS'], 72 | 73 | 74 | // If browser does not capture in given timeout [ms], kill it 75 | captureTimeout: 60000, 76 | 77 | 78 | // Continuous Integration mode 79 | // if true, it capture browsers, run tests and exit 80 | singleRun: false 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-paginate-anything", 3 | "version": "4.2.0", 4 | "description": "Add server-side pagination to any list", 5 | "main": "./index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/begriffs/angular-paginate-anything.git" 9 | }, 10 | "devDependencies": { 11 | "bower": "latest", 12 | "grunt": "latest", 13 | "grunt-angular-templates": "latest", 14 | "grunt-bump": "latest", 15 | "grunt-cli": "latest", 16 | "grunt-contrib-clean": "latest", 17 | "grunt-contrib-concat": "latest", 18 | "grunt-contrib-copy": "latest", 19 | "grunt-contrib-jshint": "latest", 20 | "grunt-contrib-uglify": "latest", 21 | "grunt-contrib-watch": "latest", 22 | "grunt-karma": "latest", 23 | "jasmine-core": "^2.4.1", 24 | "jshint": "latest", 25 | "karma": "latest", 26 | "karma-jasmine": "~0.2.0", 27 | "karma-ng-html2js-preprocessor": "latest", 28 | "karma-phantomjs-launcher": "latest", 29 | "phantomjs-prebuilt": "^2.1.7" 30 | }, 31 | "keywords": [], 32 | "author": { 33 | "name": "Joe Nelson", 34 | "email": "", 35 | "url": "https://github.com/begriffs" 36 | }, 37 | "licenses": [ 38 | { 39 | "type": "MIT" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/paginate-anything.html: -------------------------------------------------------------------------------- 1 |
2 | 21 |
22 | 25 | per page 26 |
27 |
28 | -------------------------------------------------------------------------------- /src/paginate-anything.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // 1 2 5 10 25 50 100 250 500 etc 5 | function quantizedNumber(i) { 6 | var adjust = [1, 2.5, 5]; 7 | return Math.floor(Math.pow(10, Math.floor(i/3)) * adjust[i % 3]); 8 | } 9 | 10 | // the j such that quantizedNumber(j) is closest to i 11 | function quantizedIndex(i) { 12 | if(i < 1) { return 0; } 13 | var group = Math.floor(Math.log(i) / Math.LN10), 14 | offset = i/(2.5 * Math.pow(10, group)); 15 | if(offset >= 3) { 16 | group++; 17 | offset = 0; 18 | } 19 | return 3*group + Math.round(Math.min(2, offset)); 20 | } 21 | 22 | function quantize(i) { 23 | if(i === Infinity) { return Infinity; } 24 | return quantizedNumber(quantizedIndex(i)); 25 | } 26 | 27 | // don't overwrite default response transforms 28 | function appendTransform(defaults, transform) { 29 | defaults = angular.isArray(defaults) ? defaults : [defaults]; 30 | return (transform) ? defaults.concat(transform) : defaults; 31 | } 32 | 33 | function parseRange(hdr) { 34 | var m = hdr && hdr.match(/^(?:items )?(\d+)-(\d+)\/(\d+|\*)$/); 35 | if(m) { 36 | return { 37 | from: +m[1], 38 | to: +m[2], 39 | total: m[3] === '*' ? Infinity : +m[3] 40 | }; 41 | } else if(hdr === '*/0') { 42 | return { total: 0 }; 43 | } 44 | return null; 45 | } 46 | 47 | function length(range) { 48 | return range.to - range.from + 1; 49 | } 50 | 51 | angular.module('bgf.paginateAnything', []). 52 | 53 | directive('bgfPagination', function () { 54 | var defaultLinkGroupSize = 3, defaultClientLimit = 250, defaultPerPage = 50; 55 | 56 | return { 57 | restrict: 'AE', 58 | scope: { 59 | // required 60 | url: '=', 61 | collection: '=', 62 | 63 | // optional 64 | urlParams: '=?', 65 | headers: '=?', 66 | page: '=?', 67 | perPage: '=?', 68 | perPagePresets: '=?', 69 | autoPresets: '=?', 70 | clientLimit: '=?', 71 | linkGroupSize: '=?', 72 | reloadPage: '=?', 73 | size: '=?', 74 | passive: '@', 75 | transformResponse: '=?', 76 | method: '@', 77 | postData: '=?', 78 | loadFn: '&', 79 | 80 | // directive -> app communication only 81 | numPages: '=?', 82 | numItems: '=?', 83 | serverLimit: '=?', 84 | rangeFrom: '=?', 85 | rangeTo: '=?' 86 | }, 87 | templateUrl: function(element, attr) { 88 | return attr.templateUrl || 'src/paginate-anything.html'; 89 | }, 90 | replace: true, 91 | controller: ['$scope', '$attrs', '$http', function($scope, $attrs, $http) { 92 | 93 | $scope.reloadPage = false; 94 | $scope.serverLimit = Infinity; // it's not known yet 95 | $scope.Math = window.Math; // Math for the template 96 | var useLoadFn = $attrs.loadFn !== undefined; // directive's '&' params are always set, need to determine from $attrs whether to use loadFn 97 | 98 | if(typeof $scope.autoPresets !== 'boolean') { 99 | $scope.autoPresets = true; 100 | } 101 | 102 | var lgs = $scope.linkGroupSize, cl = $scope.clientLimit; 103 | $scope.linkGroupSize = typeof lgs === 'number' ? lgs : defaultLinkGroupSize; 104 | $scope.clientLimit = typeof cl === 'number' ? cl : defaultClientLimit; 105 | 106 | $scope.updatePresets = function () { 107 | if($scope.autoPresets) { 108 | var presets = [], i; 109 | for(i = Math.min(3, quantizedIndex($scope.perPage || defaultPerPage)); 110 | i <= quantizedIndex(Math.min($scope.clientLimit, $scope.serverLimit)); 111 | i++) { 112 | presets.push(quantizedNumber(i)); 113 | } 114 | $scope.perPagePresets = presets; 115 | } else { 116 | $scope.perPagePresets = $scope.perPagePresets.filter( 117 | function (preset) { return preset <= $scope.serverLimit; } 118 | ).concat([$scope.serverLimit]); 119 | } 120 | }; 121 | 122 | $scope.gotoPage = function (i) { 123 | if(i < 0 || i*$scope.perPage >= $scope.numItems) { 124 | return; 125 | } 126 | $scope.page = i; 127 | }; 128 | 129 | $scope.linkGroupFirst = function() { 130 | var rightDebt = Math.max( 0, 131 | $scope.linkGroupSize - ($scope.numPages - 1 - ($scope.page + 2)) 132 | ); 133 | return Math.max( 0, 134 | $scope.page - ($scope.linkGroupSize + rightDebt) 135 | ); 136 | }; 137 | 138 | $scope.linkGroupLast = function() { 139 | var leftDebt = Math.max( 0, 140 | $scope.linkGroupSize - ($scope.page - 2) 141 | ); 142 | return Math.min( $scope.numPages-1, 143 | $scope.page + ($scope.linkGroupSize + leftDebt) 144 | ); 145 | }; 146 | 147 | $scope.isFinite = function() { 148 | return $scope.numPages < Infinity; 149 | }; 150 | 151 | function requestRange(request) { 152 | if($scope.passive === 'true' || !$scope.url && !useLoadFn) { return; } 153 | $scope.$emit('pagination:loadStart', request); 154 | 155 | var config = { 156 | method: $scope.method || 'GET', 157 | url: $scope.url, 158 | params: $scope.urlParams, 159 | data: $scope.postData, 160 | headers: angular.extend( 161 | {}, $scope.headers, 162 | { 'Range-Unit': 'items', Range: [request.from, request.to].join('-') } 163 | ), 164 | transformResponse: appendTransform($http.defaults.transformResponse, $scope.transformResponse) 165 | }; 166 | var responsePromise = useLoadFn ? $scope.loadFn({config: config}) : $http(config); 167 | responsePromise.then(function (rsp) { 168 | var response = parseRange(rsp.headers('Content-Range')); 169 | if(rsp.status === 204 || (response && response.total === 0)) { 170 | $scope.numItems = 0; 171 | $scope.collection = []; 172 | } else { 173 | $scope.numItems = response ? response.total : rsp.data.length; 174 | $scope.collection = rsp.data || []; 175 | } 176 | 177 | if(response) { 178 | $scope.rangeFrom = response.from; 179 | $scope.rangeTo = response.to; 180 | if(length(response) < response.total) { 181 | if( 182 | ( request.to < response.total - 1) || 183 | (response.to < response.total - 1 && response.total < request.to) 184 | ) { 185 | if(!$scope.perPage || length(response) < $scope.perPage) { 186 | if($scope.autoPresets) { 187 | var idx = quantizedIndex(length(response)); 188 | if(quantizedNumber(idx) > length(response)) { 189 | idx--; 190 | } 191 | $scope.serverLimit = quantizedNumber(idx); 192 | } else { 193 | $scope.serverLimit = length(response); 194 | } 195 | $scope.perPage = $scope.Math.min( 196 | $scope.serverLimit, 197 | $scope.clientLimit 198 | ); 199 | } 200 | } 201 | } 202 | } 203 | $scope.numPages = Math.ceil($scope.numItems / ($scope.perPage || defaultPerPage)); 204 | 205 | $scope.$emit('pagination:loadPage', rsp.status, rsp.config); 206 | }, function (rsp) { 207 | $scope.$emit('pagination:error', rsp.status, rsp.config); 208 | }); 209 | } 210 | 211 | $scope.page = $scope.page || 0; 212 | $scope.size = $scope.size || 'md'; 213 | if($scope.autoPresets) { 214 | $scope.updatePresets(); 215 | } 216 | 217 | $scope.$watch('page', function(newPage, oldPage) { 218 | if($scope.passive === 'true') { return; } 219 | 220 | if(newPage !== oldPage) { 221 | if(newPage < 0 || newPage*$scope.perPage >= $scope.numItems) { 222 | return; 223 | } 224 | 225 | var pp = $scope.perPage || defaultPerPage; 226 | 227 | if($scope.autoPresets) { 228 | pp = quantize(pp); 229 | } 230 | 231 | requestRange({ 232 | from: newPage * pp, 233 | to: (newPage+1) * pp - 1 234 | }); 235 | } 236 | }); 237 | 238 | $scope.$watch('perPage', function(newPp, oldPp) { 239 | if($scope.passive === 'true') { return; } 240 | 241 | if(typeof(oldPp) === 'number' && newPp !== oldPp) { 242 | var first = $scope.page * oldPp; 243 | var newPage = Math.floor(first / newPp); 244 | 245 | if($scope.page !== newPage) { 246 | $scope.page = newPage; 247 | } else { 248 | requestRange({ 249 | from: $scope.page * newPp, 250 | to: ($scope.page+1) * newPp - 1 251 | }); 252 | } 253 | } 254 | }); 255 | 256 | $scope.$watch('serverLimit', function(newLimit, oldLimit) { 257 | if($scope.passive === 'true') { return; } 258 | 259 | if(newLimit !== oldLimit) { 260 | $scope.updatePresets(); 261 | } 262 | }); 263 | 264 | $scope.$watch('url', function(newUrl, oldUrl) { 265 | if($scope.passive === 'true') { return; } 266 | 267 | if(newUrl !== oldUrl) { 268 | if($scope.page === 0){ 269 | $scope.reloadPage = true; 270 | } else { 271 | $scope.page = 0; 272 | } 273 | } 274 | }); 275 | 276 | $scope.$watch('urlParams', function(newParams, oldParams) { 277 | if($scope.passive === 'true') { return; } 278 | 279 | if(!angular.equals(newParams, oldParams)) { 280 | if($scope.page === 0){ 281 | $scope.reloadPage = true; 282 | } else { 283 | $scope.page = 0; 284 | } 285 | } 286 | }, true); 287 | 288 | $scope.$watch('headers', function(newHeaders, oldHeaders) { 289 | if($scope.passive === 'true') { return; } 290 | 291 | if(!angular.equals(newHeaders, oldHeaders)) { 292 | if($scope.page === 0){ 293 | $scope.reloadPage = true; 294 | } else { 295 | $scope.page = 0; 296 | } 297 | } 298 | }, true); 299 | 300 | $scope.$watch('reloadPage', function(newVal, oldVal) { 301 | if($scope.passive === 'true') { return; } 302 | 303 | if(newVal === true && oldVal === false) { 304 | var pp = $scope.perPage || defaultPerPage; 305 | $scope.reloadPage = false; 306 | requestRange({ 307 | from: $scope.page * pp, 308 | to: ($scope.page+1) * pp - 1 309 | }); 310 | } 311 | }); 312 | 313 | $scope.$watch('transformResponse', function(newTransform, oldTransform) { 314 | if($scope.passive === 'true') { return; } 315 | if(!newTransform || !oldTransform) { return; } 316 | 317 | // If applying a transform to returned data, it makes sense to start at the first page if changed 318 | // Unfortunately it's not really possible to compare function equality 319 | // In lieu of that, for now we'll compare string representations of them. 320 | if(!angular.equals(newTransform.toString(), oldTransform.toString())) { 321 | if($scope.page === 0){ 322 | $scope.reloadPage = true; 323 | } else { 324 | $scope.page = 0; 325 | } 326 | } 327 | }, true); 328 | 329 | $scope.$on('pagination:reload', function() { 330 | $scope.reloadPage = true; 331 | }); 332 | 333 | var pp = $scope.perPage || defaultPerPage; 334 | 335 | if($scope.autoPresets) { 336 | pp = quantize(pp); 337 | } 338 | 339 | requestRange({ 340 | from: $scope.page * pp, 341 | to: ($scope.page+1) * pp - 1 342 | }); 343 | }] 344 | }; 345 | }). 346 | 347 | filter('makeRange', function() { 348 | // http://stackoverflow.com/a/14932395/3102996 349 | return function(input) { 350 | var lowBound, highBound; 351 | switch (input.length) { 352 | case 1: 353 | lowBound = 0; 354 | highBound = parseInt(input[0], 10) - 1; 355 | break; 356 | case 2: 357 | lowBound = parseInt(input[0], 10); 358 | highBound = parseInt(input[1], 10); 359 | break; 360 | default: 361 | return input; 362 | } 363 | var result = []; 364 | for (var i = lowBound; i <= highBound; i++) { result.push(i); } 365 | return result; 366 | }; 367 | }); 368 | }()); 369 | -------------------------------------------------------------------------------- /src/paginate-anything.min.js: -------------------------------------------------------------------------------- 1 | // angular-paginate-anything - v3.1.1 2 | !function(){"use strict";function a(a){var b=[1,2.5,5];return Math.floor(Math.pow(10,Math.floor(a/3))*b[a%3])}function b(a){if(1>a)return 0;var b=Math.floor(Math.log(a)/Math.LN10),c=a/(2.5*Math.pow(10,b));return c>=3&&(b++,c=0),3*b+Math.round(Math.min(2,c))}function c(c){return 1/0===c?1/0:a(b(c))}function d(a){var b=a&&a.match(/^(\d+)-(\d+)\/(\d+|\*)$/);return b?{from:+b[1],to:+b[2],total:"*"===b[3]?1/0:+b[3]}:"*/0"===a?{total:0}:null}function e(a){return a.to-a.from+1}angular.module("bgf.paginateAnything",[]).directive("bgfPagination",function(){var f=3,g=250,h=50;return{restrict:"AE",scope:{url:"=",collection:"=",urlParams:"=?",headers:"=?",page:"=?",perPage:"=?",perPagePresets:"=?",autoPresets:"=?",clientLimit:"=?",linkGroupSize:"=?",reloadPage:"=?",size:"=?",passive:"@",numPages:"=?",numItems:"=?",serverLimit:"=?",rangeFrom:"=?",rangeTo:"=?"},templateUrl:function(a,b){return b.templateUrl||"tpl/paginate-anything.html"},replace:!0,controller:["$scope","$http",function(i,j){function k(c){i.$emit("pagination:loadStart",c),j({method:"GET",url:i.url,params:i.urlParams,headers:angular.extend({},i.headers,{"Range-Unit":"items",Range:[c.from,c.to].join("-")})}).success(function(f,g,j,k){var l=d(j("Content-Range"));if(204===g||l&&0===l.total?(i.numItems=0,i.collection=[]):(i.numItems=l?l.total:f.length,i.collection=f||[]),l&&(i.rangeFrom=l.from,i.rangeTo=l.to,e(l)e(l)&&m--,i.serverLimit=a(m)}else i.serverLimit=e(l);i.perPage=i.Math.min(i.serverLimit,i.clientLimit)}i.numPages=Math.ceil(i.numItems/(i.perPage||h)),i.$emit("pagination:loadPage",g,k)}).error(function(a,b,c,d){i.$emit("pagination:error",b,d)})}i.reloadPage=!1,i.serverLimit=1/0,i.Math=window.Math,"boolean"!=typeof i.autoPresets&&(i.autoPresets=!0);var l=i.linkGroupSize,m=i.clientLimit;if(i.linkGroupSize="number"==typeof l?l:f,i.clientLimit="number"==typeof m?m:g,i.updatePresets=function(){if(i.autoPresets){var c,d=[];for(c=Math.min(3,b(i.perPage||h));c<=b(Math.min(i.clientLimit,i.serverLimit));c++)d.push(a(c));i.perPagePresets=d}else i.perPagePresets=i.perPagePresets.filter(function(a){return a<=i.serverLimit}).concat([i.serverLimit])},i.gotoPage=function(a){i.page=a},i.linkGroupFirst=function(){var a=Math.max(0,i.linkGroupSize-(i.numPages-1-(i.page+2)));return Math.max(0,i.page-(i.linkGroupSize+a))},i.linkGroupLast=function(){var a=Math.max(0,i.linkGroupSize-(i.page-2));return Math.min(i.numPages-1,i.page+(i.linkGroupSize+a))},i.isFinite=function(){return i.numPages<1/0},i.page=i.page||0,i.size=i.size||"md",i.autoPresets&&i.updatePresets(),i.$watch("page",function(a,b){if("true"!==i.passive&&a!==b){if(0>a||a*i.perPage>=i.numItems)return;var d=i.perPage||h;i.autoPresets&&(d=c(d)),k({from:a*d,to:(a+1)*d-1})}}),i.$watch("perPage",function(a,b){if("true"!==i.passive&&"number"==typeof b&&a!==b){var c=i.page*b,d=Math.floor(c/a);i.page!==d?i.page=d:k({from:i.page*a,to:(i.page+1)*a-1})}}),i.$watch("serverLimit",function(a,b){"true"!==i.passive&&a!==b&&i.updatePresets()}),i.$watch("url",function(a,b){"true"!==i.passive&&a!==b&&(0===i.page?i.reloadPage=!0:i.page=0)}),i.$watch("urlParams",function(a,b){"true"!==i.passive&&(angular.equals(a,b)||(0===i.page?i.reloadPage=!0:i.page=0))},!0),i.$watch("headers",function(a,b){"true"!==i.passive&&(angular.equals(a,b)||(0===i.page?i.reloadPage=!0:i.page=0))},!0),i.$watch("reloadPage",function(a,b){"true"!==i.passive&&a===!0&&b===!1&&(i.reloadPage=!1,k({from:i.page*i.perPage,to:(i.page+1)*i.perPage-1}))}),"true"!==i.passive){var n=i.perPage||h;i.autoPresets&&(n=c(n)),k({from:i.page*n,to:(i.page+1)*n-1})}}]}}).filter("makeRange",function(){return function(a){var b,c;switch(a.length){case 1:b=0,c=parseInt(a[0],10)-1;break;case 2:b=parseInt(a[0],10),c=parseInt(a[1],10);break;default:return a}for(var d=[],e=b;c>=e;e++)d.push(e);return d}})}(); -------------------------------------------------------------------------------- /test/paginate-anything-spec.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var $httpBackend, $compile, scope, $q; 5 | beforeEach(function () { 6 | angular.mock.module('bgf.paginateAnything'); 7 | angular.mock.module('src/paginate-anything.html'); 8 | 9 | angular.mock.inject( 10 | ['$httpBackend', '$compile', '$rootScope', '$q', 11 | function (httpBackend, compile, rootScope, q) { 12 | $httpBackend = httpBackend; 13 | $compile = compile; 14 | $q = q; 15 | scope = rootScope.$new(); 16 | }] 17 | ); 18 | }); 19 | 20 | var template = ''; 34 | 35 | function linksShouldBe(elt, ar) { 36 | ar.unshift('«'); 37 | ar.push('»'); 38 | for(var i = 0; i < ar.length; i++) { 39 | expect(elt.find('li').eq(i).text().trim()).toEqual(ar[i]); 40 | } 41 | } 42 | 43 | function finiteStringBackend(s, maxRange) { 44 | maxRange = maxRange || s.length; 45 | 46 | return function(method, url, data, headers) { 47 | var m = headers.Range.match(/^(\d+)-(\d+)$/); 48 | if(m) { 49 | m[1] = +m[1]; 50 | m[2] = +m[2]; 51 | m[2] = Math.min(m[2] + 1, m[1] + maxRange); 52 | return [ 53 | m[2] < s.length ? 206 : 200, 54 | s.slice(m[1], m[2]).split(''), 55 | { 56 | 'Range-Unit': 'items', 57 | 'Content-Range': [m[1], Math.min(s.length, m[2])-1].join('-') + '/' + s.length 58 | } 59 | ]; 60 | } 61 | }; 62 | } 63 | 64 | describe('paginate-anything', function () { 65 | it('does not appear for a non-range-paginated resource', function () { 66 | $httpBackend.expectGET('/items').respond(200, ''); 67 | var elt = $compile(template)(scope); 68 | scope.$digest(); 69 | $httpBackend.flush(); 70 | expect(elt.find('ul').length).toEqual(0); 71 | }); 72 | 73 | it('does not appear for a ranged yet complete resource', function () { 74 | $httpBackend.expectGET('/items').respond(200, 75 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/25' } 76 | ); 77 | var elt = $compile(template)(scope); 78 | scope.$digest(); 79 | $httpBackend.flush(); 80 | expect(elt.find('ul').length).toEqual(0); 81 | }); 82 | 83 | it('appears for a ranged incomplete resource', function () { 84 | $httpBackend.expectGET('/items').respond(206, 85 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/26' } 86 | ); 87 | var elt = $compile(template)(scope); 88 | scope.$digest(); 89 | $httpBackend.flush(); 90 | expect(elt.find('ul').length).toEqual(1); 91 | }); 92 | 93 | it('does not appear after a redraw of the page', function() { 94 | $httpBackend.expectGET('/items').respond(206, 95 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/26' } 96 | ); 97 | var elt = $compile(template)(scope); 98 | scope.$digest(); 99 | $httpBackend.flush(); 100 | expect(elt.find('ul').length).toEqual(1); 101 | 102 | $httpBackend.expectGET('/items?foo=foo').respond(200, ''); 103 | scope.urlParams = {foo: 'foo'}; 104 | scope.$digest(); 105 | $httpBackend.flush(); 106 | expect(elt.find('ul').length).toEqual(0); 107 | }); 108 | 109 | it('starts at page 0', function () { 110 | $httpBackend.expectGET('/items').respond(206, 111 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/26' } 112 | ); 113 | $compile(template)(scope); 114 | scope.$digest(); 115 | $httpBackend.flush(); 116 | expect(scope.page).toEqual(0); 117 | }); 118 | 119 | it('loads empty array for zero-length range responses', function () { 120 | $httpBackend.expectGET('/items').respond(206, 121 | '', { 'Range-Unit': 'items', 'Content-Range': '*/0' } 122 | ); 123 | $compile(template)(scope); 124 | scope.$digest(); 125 | $httpBackend.flush(); 126 | expect(scope.numItems).toEqual(0); 127 | expect(scope.numPages).toEqual(0); 128 | expect(scope.collection).toEqual([]); 129 | }); 130 | 131 | it('loads empty array for 204 no content', function () { 132 | $httpBackend.expectGET('/items').respond(204, ''); 133 | $compile(template)(scope); 134 | scope.$digest(); 135 | $httpBackend.flush(); 136 | expect(scope.numItems).toEqual(0); 137 | expect(scope.numPages).toEqual(0); 138 | expect(scope.collection).toEqual([]); 139 | }); 140 | 141 | it('loads empty array for response lacking in any pagination info', function () { 142 | $httpBackend.expectGET('/items').respond(200, ''); 143 | $compile(template)(scope); 144 | scope.$digest(); 145 | $httpBackend.flush(); 146 | expect(scope.numItems).toEqual(0); 147 | expect(scope.collection).toEqual([]); 148 | }); 149 | 150 | it('knows total pages', function () { 151 | $httpBackend.expectGET('/items').respond(206, 152 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/26' } 153 | ); 154 | $compile(template)(scope); 155 | scope.$digest(); 156 | $httpBackend.flush(); 157 | expect(scope.numPages).toEqual(2); 158 | }); 159 | 160 | it('discovers server range limit when range comes back small', function () { 161 | $httpBackend.expectGET('/items').respond( 162 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz', 2) 163 | ); 164 | $compile(template)(scope); 165 | scope.$digest(); 166 | $httpBackend.flush(); 167 | expect(scope.numPages).toEqual(13); 168 | expect(scope.perPage).toEqual(2); 169 | }); 170 | 171 | it('changing the page on the scope updates the collection', function () { 172 | $httpBackend.whenGET('/items').respond( 173 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz', 20) 174 | ); 175 | $compile(template)(scope); 176 | scope.$digest(); 177 | $httpBackend.flush(); 178 | 179 | scope.page = 1; 180 | scope.$digest(); 181 | $httpBackend.flush(); 182 | 183 | // perPage actually bumps down to 10 because it is a quantized number 184 | expect(scope.collection).toEqual(['k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't']); 185 | }); 186 | 187 | it('uses provided function to retrieve data', function () { 188 | var deferred = $q.defer(); 189 | deferred.resolve({ data: ['a', 'b'], status: 200, config: {}, headers: function() { return '0-1'; } }); 190 | 191 | scope.loadFn = function(config) { 192 | expect(config.headers.Range).toBe('0-49'); 193 | return deferred.promise; 194 | }; 195 | var template = ''; 201 | $compile(template)(scope); 202 | scope.$digest(); 203 | expect(scope.collection).toEqual(['a', 'b']); 204 | $httpBackend.verifyNoOutstandingRequest(); 205 | }); 206 | 207 | it('performs search on pagination:reload event', function () { 208 | scope.page = 0; 209 | scope.perPage = 1; 210 | $compile(template)(scope); 211 | 212 | var get = $httpBackend.whenGET('/items'); 213 | get.respond( 214 | finiteStringBackend('a', 1) 215 | ); 216 | scope.$digest(); 217 | $httpBackend.flush(); 218 | expect(scope.collection).toEqual(['a']); 219 | 220 | get.respond( 221 | finiteStringBackend('b', 1) 222 | ); 223 | scope.$broadcast('pagination:reload'); 224 | scope.$digest(); 225 | $httpBackend.flush(); 226 | expect(scope.collection).toEqual(['b']); 227 | 228 | $httpBackend.verifyNoOutstandingRequest(); 229 | }); 230 | 231 | it('can start on a different page', function () { 232 | scope.perPage = 25; 233 | scope.page = 1; 234 | $httpBackend.whenGET('/items').respond( 235 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz', 20) 236 | ); 237 | $compile(template)(scope); 238 | scope.$digest(); 239 | $httpBackend.flush(); 240 | 241 | expect(scope.collection).toEqual(['z']); 242 | }); 243 | 244 | it('limited range at the end does not trigger resizing perPage', function () { 245 | scope.perPage = 20; 246 | scope.page = 1; 247 | $httpBackend.whenGET('/items').respond( 248 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz', 20) 249 | ); 250 | $compile(template)(scope); 251 | scope.$digest(); 252 | $httpBackend.flush(); 253 | 254 | expect(scope.perPage).toEqual(20); 255 | expect(scope.numPages).toEqual(2); 256 | 257 | scope.page = 0; 258 | scope.$digest(); 259 | $httpBackend.flush(); 260 | 261 | expect(scope.perPage).toEqual(20); 262 | expect(scope.numPages).toEqual(2); 263 | }); 264 | 265 | it('increasing perPage while staying on same page has an effect', function () { 266 | scope.perPage = 4; 267 | $httpBackend.whenGET('/items').respond( 268 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 269 | ); 270 | $compile(template)(scope); 271 | scope.$digest(); 272 | $httpBackend.flush(); 273 | 274 | scope.perPage = 16; 275 | scope.$digest(); 276 | $httpBackend.flush(); 277 | expect(scope.page).toEqual(0); 278 | }); 279 | 280 | it('decreasing perPage keeps the first item on the current page', function () { 281 | scope.perPage = 5; 282 | scope.page = 1; 283 | $httpBackend.whenGET('/items').respond( 284 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 285 | ); 286 | $compile(template)(scope); 287 | scope.$digest(); 288 | $httpBackend.flush(); 289 | 290 | scope.perPage = 1; 291 | scope.$digest(); 292 | $httpBackend.flush(); 293 | expect(scope.collection).toEqual(['f']); 294 | expect(scope.page).toEqual(5); 295 | }); 296 | 297 | it('tiny last page and decreasing perPage preserves the first item', function () { 298 | scope.perPage = 500; 299 | scope.page = 0; 300 | $httpBackend.whenGET('/items').respond( 301 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 302 | ); 303 | $compile(template)(scope); 304 | scope.$digest(); 305 | $httpBackend.flush(); 306 | 307 | scope.perPage = 1; 308 | scope.$digest(); 309 | $httpBackend.flush(); 310 | expect(scope.collection).toEqual(['a']); 311 | expect(scope.page).toEqual(0); 312 | }); 313 | 314 | it('increasing perPage keeps the middle item on the current page', function () { 315 | scope.perPage = 12; 316 | scope.page = 2; 317 | $httpBackend.whenGET('/items').respond( 318 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 319 | ); 320 | $compile(template)(scope); 321 | scope.$digest(); 322 | $httpBackend.flush(); 323 | 324 | scope.perPage = 13; 325 | scope.$digest(); 326 | $httpBackend.flush(); 327 | expect(scope.page).toEqual(1); 328 | }); 329 | 330 | it('changing perPage rounds down for middle item', function () { 331 | scope.perPage = 2; 332 | scope.page = 1; 333 | $httpBackend.whenGET('/items').respond( 334 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 335 | ); 336 | $compile(template)(scope); 337 | scope.$digest(); 338 | $httpBackend.flush(); 339 | 340 | scope.perPage = 1; 341 | scope.$digest(); 342 | $httpBackend.flush(); 343 | expect(scope.collection).toEqual(['c']); 344 | expect(scope.page).toEqual(2); 345 | }); 346 | 347 | it('halving perPage fixes the first item on the current page', function () { 348 | scope.perPage = 4; 349 | scope.page = 1; 350 | $httpBackend.whenGET('/items').respond( 351 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 352 | ); 353 | $compile(template)(scope); 354 | scope.$digest(); 355 | $httpBackend.flush(); 356 | 357 | scope.perPage = 2; 358 | scope.$digest(); 359 | $httpBackend.flush(); 360 | expect(scope.collection).toEqual(['e', 'f']); 361 | expect(scope.page).toEqual(2); 362 | }); 363 | 364 | it('doubling perPage fixes first item on the current page (if already divides larger size)', function () { 365 | scope.perPage = 5; 366 | scope.page = 2; 367 | $httpBackend.whenGET('/items').respond( 368 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 369 | ); 370 | $compile(template)(scope); 371 | scope.$digest(); 372 | $httpBackend.flush(); 373 | 374 | scope.perPage = 10; 375 | scope.$digest(); 376 | $httpBackend.flush(); 377 | expect(scope.collection).toEqual([ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't']); 378 | expect(scope.page).toEqual(1); 379 | }); 380 | 381 | it('doubling perPage does not fix the first item when new size >= total', function () { 382 | scope.perPage = 20; 383 | scope.page = 1; 384 | $httpBackend.whenGET('/items').respond( 385 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 386 | ); 387 | $compile(template)(scope); 388 | scope.$digest(); 389 | $httpBackend.flush(); 390 | 391 | scope.perPage = 40; 392 | scope.$digest(); 393 | $httpBackend.flush(); 394 | expect(scope.page).toEqual(0); 395 | }); 396 | 397 | it('halving perPage doubles numPages', function () { 398 | scope.perPage = 4; 399 | scope.page = 1; 400 | $httpBackend.whenGET('/items').respond( 401 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 402 | ); 403 | $compile(template)(scope); 404 | scope.$digest(); 405 | $httpBackend.flush(); 406 | expect(scope.numPages).toEqual(7); 407 | 408 | scope.perPage = 2; 409 | scope.$digest(); 410 | $httpBackend.flush(); 411 | expect(scope.numPages).toEqual(13); 412 | }); 413 | 414 | it('doubling perPage halves numPages', function () { 415 | scope.perPage = 2; 416 | scope.page = 1; 417 | $httpBackend.whenGET('/items').respond( 418 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 419 | ); 420 | $compile(template)(scope); 421 | scope.$digest(); 422 | $httpBackend.flush(); 423 | expect(scope.numPages).toEqual(13); 424 | 425 | scope.perPage = 4; 426 | scope.$digest(); 427 | $httpBackend.flush(); 428 | expect(scope.numPages).toEqual(7); 429 | }); 430 | 431 | it('perPage >= total makes numPages=1', function () { 432 | scope.perPage = 5; 433 | scope.page = 1; 434 | $httpBackend.whenGET('/items').respond( 435 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 436 | ); 437 | $compile(template)(scope); 438 | scope.$digest(); 439 | $httpBackend.flush(); 440 | expect(scope.numPages).toEqual(6); 441 | 442 | scope.perPage = 50; 443 | scope.$digest(); 444 | $httpBackend.flush(); 445 | expect(scope.numPages).toEqual(1); 446 | }); 447 | 448 | it('keeps page in-bounds when shrinking perPage', function () { 449 | scope.perPage = 10; 450 | scope.page = 2; 451 | $httpBackend.whenGET('/items').respond( 452 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 453 | ); 454 | $compile(template)(scope); 455 | scope.$digest(); 456 | $httpBackend.flush(); 457 | 458 | scope.perPage = 2; 459 | scope.$digest(); 460 | $httpBackend.flush(); 461 | expect(scope.page).toEqual(10); 462 | }); 463 | 464 | it('reloads data when asked explicitly', function () { 465 | $httpBackend.expectGET('/items').respond(200, ''); 466 | $compile(template)(scope); 467 | scope.$digest(); 468 | $httpBackend.flush(); 469 | 470 | $httpBackend.expectGET('/items').respond(200, ''); 471 | scope.reloadPage = true; 472 | scope.$digest(); 473 | $httpBackend.flush(); 474 | }); 475 | 476 | it('reloading sans perPage peram succeeds', function () { 477 | // scope.perPage left intentionally unset 478 | scope.page = 1; 479 | $httpBackend.whenGET('/items').respond( 480 | finiteStringBackend('a') 481 | ); 482 | 483 | $compile(template)(scope); 484 | scope.$digest(); 485 | $httpBackend.flush(); 486 | 487 | // after reloading 488 | scope.reloadPage = true; 489 | scope.$digest(); 490 | $httpBackend.flush(); 491 | }); 492 | 493 | }); 494 | 495 | describe('ui', function () { 496 | it('disables next link on last page', function () { 497 | scope.perPage = 2; 498 | scope.page = 12; 499 | $httpBackend.whenGET('/items').respond( 500 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 501 | ); 502 | var elt = $compile(template)(scope); 503 | scope.$digest(); 504 | $httpBackend.flush(); 505 | 506 | expect(elt.find('ul').eq(0).find('li').eq(-1).hasClass('disabled')).toBe(true); 507 | 508 | // trying to advance further has no effect 509 | scope.page = 13; 510 | scope.$digest(); 511 | $httpBackend.verifyNoOutstandingRequest(); 512 | }); 513 | 514 | it('enables next link on next-to-last page', function () { 515 | scope.perPage = 2; 516 | scope.page = 11; 517 | $httpBackend.whenGET('/items').respond( 518 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 519 | ); 520 | var elt = $compile(template)(scope); 521 | scope.$digest(); 522 | $httpBackend.flush(); 523 | 524 | expect(elt.find('li').eq(-1).hasClass('disabled')).toBe(false); 525 | }); 526 | 527 | it('omits ellipses if possible', function () { 528 | scope.perPage = 5; 529 | $httpBackend.whenGET('/items').respond( 530 | finiteStringBackend('abcdefghijklmno') // 15 total 531 | ); 532 | var elt = $compile(template)(scope); 533 | scope.$digest(); 534 | $httpBackend.flush(); 535 | 536 | linksShouldBe(elt, ['1', '2', '3']); 537 | }); 538 | 539 | it('adds ellipses at end', function () { 540 | scope.perPage = 2; 541 | scope.linkGroupSize = 2; 542 | $httpBackend.whenGET('/items').respond( 543 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 544 | ); 545 | var elt = $compile(template)(scope); 546 | scope.$digest(); 547 | $httpBackend.flush(); 548 | 549 | linksShouldBe(elt, ['1', '2', '3', '4', '5', '6', '7', '…', '13']); 550 | }); 551 | 552 | it('adds ellipses at beginning', function () { 553 | scope.perPage = 2; 554 | scope.linkGroupSize = 2; 555 | scope.page = 11; 556 | $httpBackend.whenGET('/items').respond( 557 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 558 | ); 559 | var elt = $compile(template)(scope); 560 | scope.$digest(); 561 | $httpBackend.flush(); 562 | 563 | linksShouldBe(elt, ['1', '…', '7', '8', '9', '10', '11', '12', '13']); 564 | }); 565 | 566 | it('show final ellipsis when range is close', function () { 567 | scope.perPage = 1; 568 | scope.linkGroupSize = 0; 569 | scope.page = 23; 570 | $httpBackend.whenGET('/items').respond( 571 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 572 | ); 573 | var elt = $compile(template)(scope); 574 | scope.$digest(); 575 | $httpBackend.flush(); 576 | 577 | linksShouldBe(elt, ['1', '…', '24', '…', '26']); 578 | }); 579 | 580 | it('show final page when range is very close', function () { 581 | scope.perPage = 1; 582 | scope.linkGroupSize = 0; 583 | scope.page = 24; 584 | $httpBackend.whenGET('/items').respond( 585 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 586 | ); 587 | var elt = $compile(template)(scope); 588 | scope.$digest(); 589 | $httpBackend.flush(); 590 | 591 | linksShouldBe(elt, ['1', '…', '24', '25', '26']); 592 | }); 593 | 594 | it('show initial ellipsis when range is close', function () { 595 | scope.perPage = 1; 596 | scope.linkGroupSize = 0; 597 | scope.page = 2; 598 | $httpBackend.whenGET('/items').respond( 599 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 600 | ); 601 | var elt = $compile(template)(scope); 602 | scope.$digest(); 603 | $httpBackend.flush(); 604 | 605 | linksShouldBe(elt, ['1', '…', '3', '…', '26']); 606 | }); 607 | 608 | it('show initial page when range is very close', function () { 609 | scope.perPage = 1; 610 | scope.linkGroupSize = 0; 611 | scope.page = 1; 612 | $httpBackend.whenGET('/items').respond( 613 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 614 | ); 615 | var elt = $compile(template)(scope); 616 | scope.$digest(); 617 | $httpBackend.flush(); 618 | 619 | linksShouldBe(elt, ['1', '2', '3', '…', '26']); 620 | }); 621 | 622 | it('adds ellipses on both sides', function () { 623 | scope.linkGroupSize = 2; 624 | scope.perPage = 2; 625 | scope.page = 5; 626 | $httpBackend.whenGET('/items').respond( 627 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 628 | ); 629 | var elt = $compile(template)(scope); 630 | scope.$digest(); 631 | $httpBackend.flush(); 632 | 633 | linksShouldBe(elt, ['1', '…', '4', '5', '6', '7', '8', '…', '13']); 634 | }); 635 | 636 | it('number of links does not change when group is cropped at start', function () { 637 | scope.linkGroupSize = 1; 638 | scope.perPage = 1; 639 | scope.page = 0; 640 | $httpBackend.whenGET('/items').respond( 641 | finiteStringBackend('abcd') 642 | ); 643 | var elt = $compile(template)(scope); 644 | scope.$digest(); 645 | $httpBackend.flush(); 646 | 647 | linksShouldBe(elt, ['1', '2', '3', '4']); 648 | }); 649 | 650 | it('number of links does not change when group is cropped at end', function () { 651 | scope.linkGroupSize = 1; 652 | scope.perPage = 1; 653 | scope.page = 3; 654 | $httpBackend.whenGET('/items').respond( 655 | finiteStringBackend('abcd') 656 | ); 657 | var elt = $compile(template)(scope); 658 | scope.$digest(); 659 | $httpBackend.flush(); 660 | 661 | linksShouldBe(elt, ['1', '2', '3', '4']); 662 | }); 663 | 664 | it('first two pages do not count against padding debt', function () { 665 | scope.linkGroupSize = 1; 666 | scope.perPage = 2; 667 | scope.page = 1; 668 | $httpBackend.whenGET('/items').respond( 669 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 670 | ); 671 | var elt = $compile(template)(scope); 672 | scope.$digest(); 673 | $httpBackend.flush(); 674 | 675 | linksShouldBe(elt, ['1', '2', '3', '4', '5', '…', '13']); 676 | }); 677 | 678 | it('last two pages do not count against padding debt', function () { 679 | scope.linkGroupSize = 1; 680 | scope.perPage = 2; 681 | scope.page = 11; 682 | $httpBackend.whenGET('/items').respond( 683 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 684 | ); 685 | var elt = $compile(template)(scope); 686 | scope.$digest(); 687 | $httpBackend.flush(); 688 | 689 | linksShouldBe(elt, ['1', '…', '9', '10', '11', '12', '13']); 690 | }); 691 | }); 692 | 693 | describe('infinite lists', function () { 694 | it('know total pages', function () { 695 | $httpBackend.expectGET('/items').respond(206, 696 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/*' } 697 | ); 698 | $compile(template)(scope); 699 | scope.$digest(); 700 | $httpBackend.flush(); 701 | expect(scope.numItems).toEqual(Infinity); 702 | expect(scope.numPages).toEqual(Infinity); 703 | }); 704 | 705 | it('do not display a last page button', function () { 706 | scope.linkGroupSize = 0; 707 | scope.perPage = 1000000; 708 | scope.page = 0; 709 | $httpBackend.expectGET('/items').respond(206, 710 | '', { 'Range-Unit': 'items', 'Content-Range': '0-999999/*' } 711 | ); 712 | var elt = $compile(template)(scope); 713 | scope.$digest(); 714 | $httpBackend.flush(); 715 | 716 | linksShouldBe(elt, ['1', '2', '3', '…']); 717 | }); 718 | 719 | it('detects server perPage limit', function () { 720 | scope.perPage = 1000000; 721 | $httpBackend.whenGET('/items').respond(206, 722 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/*' } 723 | ); 724 | $compile(template)(scope); 725 | scope.$digest(); 726 | $httpBackend.flush(); 727 | expect(scope.perPage).toEqual(25); 728 | }); 729 | }); 730 | 731 | describe('per-page presets', function () { 732 | it('goes in quantized number increments', function () { 733 | scope.clientLimit = 200; 734 | $httpBackend.expectGET('/items').respond( 735 | finiteStringBackend('abcdefghijklmnopqrstuvw') // length == 23 736 | ); 737 | $compile(template)(scope); 738 | scope.$digest(); 739 | $httpBackend.flush(); 740 | 741 | expect(scope.perPagePresets).toEqual([10, 25, 50, 100, 250]); 742 | }); 743 | 744 | it('can be set manually', function () { 745 | scope.perPagePresets = [2,3,5,7,11]; 746 | scope.autoPresets = false; 747 | scope.perPage = 5; 748 | $httpBackend.whenGET('/items').respond(206, 749 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/*' } 750 | ); 751 | $compile(template)(scope); 752 | scope.$digest(); 753 | 754 | expect(scope.perPagePresets).toEqual([2,3,5,7,11]); 755 | }); 756 | 757 | it('adjusts for small server limits', function () { 758 | $httpBackend.expectGET('/items').respond( 759 | finiteStringBackend('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz', 46) 760 | ); 761 | $compile(template)(scope); 762 | scope.$digest(); 763 | $httpBackend.flush(); 764 | 765 | expect(scope.perPagePresets).toEqual([10, 25]); 766 | }); 767 | 768 | it('does not adjust values less than server limit when set manually', function () { 769 | scope.autoPresets = false; 770 | scope.perPagePresets = [2,3,5,7,11]; 771 | $httpBackend.expectGET('/items').respond( 772 | finiteStringBackend('abcdefghijk') 773 | ); 774 | $compile(template)(scope); 775 | scope.$digest(); 776 | $httpBackend.flush(); 777 | 778 | expect(scope.perPagePresets).toEqual([2,3,5,7,11]); 779 | }); 780 | 781 | it('truncates values greater than detected server limit', function () { 782 | scope.autoPresets = false; 783 | scope.perPagePresets = [10,20,30,3000]; 784 | $httpBackend.expectGET('/items').respond(206, 785 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/*' } 786 | ); 787 | $compile(template)(scope); 788 | scope.$digest(); 789 | $httpBackend.flush(); 790 | 791 | expect(scope.perPagePresets).toEqual([10,20,25]); 792 | }); 793 | 794 | it('does not adjust if client limit < server limit', function () { 795 | scope.clientLimit = 40; 796 | $httpBackend.expectGET('/items').respond( 797 | finiteStringBackend('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz', 46) 798 | ); 799 | $compile(template)(scope); 800 | scope.$digest(); 801 | $httpBackend.flush(); 802 | 803 | expect(scope.perPagePresets).toEqual([10, 25]); 804 | }); 805 | 806 | it('hides per page select box when empty and no server limit detected', function () { 807 | scope.autoPresets = false; 808 | scope.perPagePresets = []; 809 | $httpBackend.expectGET('/items').respond(206, 810 | '', { 'Range-Unit': 'items', 'Content-Range': '0-999999/1000000' } 811 | ); 812 | var elt = $compile(template)(scope); 813 | scope.$digest(); 814 | $httpBackend.flush(); 815 | expect(elt.find('select').length).toEqual(0); 816 | }); 817 | }); 818 | 819 | describe('filtering', function () { 820 | var template = ''; 826 | 827 | it('reloads data from page 0 when url changes', function () { 828 | $httpBackend.whenGET('/letters').respond(finiteStringBackend('abcd')); 829 | $httpBackend.whenGET('/numbers').respond(finiteStringBackend('1234')); 830 | scope.url = '/letters'; 831 | scope.perPage = 2; 832 | scope.page = 1; 833 | 834 | $compile(template)(scope); 835 | 836 | scope.$digest(); 837 | $httpBackend.flush(); 838 | 839 | scope.url = '/numbers'; 840 | scope.$digest(); 841 | $httpBackend.flush(); 842 | 843 | expect(scope.page).toEqual(0); 844 | expect(scope.collection).toEqual(['1', '2']); 845 | }); 846 | 847 | it('reloads data from page 0 when urlParams changes', function () { 848 | $httpBackend.expectGET('/letters?foo=bar').respond(finiteStringBackend('abcd')); 849 | scope.url = '/letters'; 850 | scope.urlParams = { foo: 'bar' }; 851 | scope.perPage = 2; 852 | scope.page = 1; 853 | 854 | $compile(template)(scope); 855 | 856 | scope.$digest(); 857 | $httpBackend.flush(); 858 | 859 | $httpBackend.expectGET('/letters?foo=baz').respond(finiteStringBackend('abcd')); 860 | scope.urlParams = { foo: 'baz' }; 861 | scope.$digest(); 862 | $httpBackend.flush(); 863 | 864 | expect(scope.page).toEqual(0); 865 | }); 866 | 867 | it('passes extra headers by request', function () { 868 | $httpBackend.expectGET( 869 | '/items', 870 | { 871 | 'foo':'bar', 872 | 'Range-Unit':'items', 873 | 'Range':'0-49', 874 | 'Accept':'application/json, text/plain, */*' 875 | } 876 | ).respond(206, '', 877 | { 'Range-Unit': 'items', 'Content-Range': '0-49/200' } 878 | ); 879 | scope.url = '/items'; 880 | scope.headers = { foo: 'bar' }; 881 | $compile(template)(scope); 882 | 883 | scope.$digest(); 884 | $httpBackend.flush(); 885 | }); 886 | 887 | it('reloads data from page 0 when headers change', function () { 888 | $httpBackend.expectGET('/letters').respond(finiteStringBackend('abcd')); 889 | scope.url = '/letters'; 890 | scope.headers = { foo: 'bar' }; 891 | scope.perPage = 2; 892 | scope.page = 1; 893 | 894 | $compile(template)(scope); 895 | 896 | scope.$digest(); 897 | $httpBackend.flush(); 898 | 899 | $httpBackend.expectGET('/letters').respond(finiteStringBackend('abcd')); 900 | scope.headers = { foo: 'baz' }; 901 | scope.$digest(); 902 | $httpBackend.flush(); 903 | 904 | expect(scope.page).toEqual(0); 905 | }); 906 | 907 | it('reloads data when url changes even if already at page 0', function () { 908 | $httpBackend.whenGET('/letters').respond(finiteStringBackend('abcd')); 909 | $httpBackend.whenGET('/numbers').respond(finiteStringBackend('1234')); 910 | scope.url = '/letters'; 911 | scope.perPage = 2; 912 | scope.page = 0; 913 | 914 | $compile(template)(scope); 915 | 916 | scope.$digest(); 917 | $httpBackend.flush(); 918 | 919 | scope.url = '/numbers'; 920 | scope.$digest(); 921 | $httpBackend.flush(); 922 | 923 | expect(scope.page).toEqual(0); 924 | expect(scope.collection).toEqual(['1', '2']); 925 | }); 926 | }); 927 | 928 | describe('response transformations', function() { 929 | var template = ''; 934 | 935 | it('should perform admirably', function () { 936 | $httpBackend.whenGET('/letters').respond(finiteStringBackend('abcd')); 937 | scope.url = '/letters'; 938 | scope.perPage = 2; 939 | scope.page = 0; 940 | scope.transformResponse = function (data) { return data; }; 941 | 942 | $compile(template)(scope); 943 | 944 | scope.$digest(); 945 | $httpBackend.flush(); 946 | 947 | expect(scope.page).toEqual(0); 948 | expect(scope.collection).toEqual(['a', 'b']); 949 | }); 950 | 951 | it('should transform data accordingly', function () { 952 | $httpBackend.whenGET('/letters').respond(finiteStringBackend('abcd')); 953 | scope.url = '/letters'; 954 | scope.perPage = 2; 955 | scope.page = 0; 956 | scope.transformResponse = function (oldData) { 957 | var newData = []; 958 | angular.forEach(oldData, function (d) { 959 | newData.push(d.toUpperCase()); 960 | }); 961 | return newData; 962 | }; 963 | 964 | $compile(template)(scope); 965 | 966 | scope.$digest(); 967 | $httpBackend.flush(); 968 | 969 | expect(scope.page).toEqual(0); 970 | expect(scope.collection).toEqual(['A', 'B']); 971 | }); 972 | 973 | it('reloads data from page 0 when transformResponse changes', function () { 974 | $httpBackend.expectGET('/letters').respond(finiteStringBackend('abcd')); 975 | scope.url = '/letters'; 976 | scope.perPage = 2; 977 | scope.page = 1; 978 | scope.transformResponse = function (foo) { return foo; }; 979 | 980 | $compile(template)(scope); 981 | 982 | scope.$digest(); 983 | $httpBackend.flush(); 984 | 985 | $httpBackend.expectGET('/letters').respond(finiteStringBackend('abcd')); 986 | scope.transformResponse = function (baz) { return baz; }; 987 | scope.$digest(); 988 | $httpBackend.flush(); 989 | 990 | expect(scope.page).toEqual(0); 991 | }); 992 | 993 | 994 | }); 995 | 996 | describe('passive mode', function () { 997 | var template = '' + ''; 1004 | 1005 | // TODO: This test is not effective because the problem only appears 1006 | // in Firefox, but even the karma-firefox-launcher did not catch it 1007 | // for some reason. 1008 | it('prevents duplicate loading', function () { 1009 | $httpBackend.expectGET('/items').respond(206, 1010 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/26' } 1011 | ); 1012 | $compile(template)(scope); 1013 | scope.$digest(); 1014 | $httpBackend.flush(1); 1015 | $httpBackend.verifyNoOutstandingRequest(); 1016 | }); 1017 | }); 1018 | 1019 | describe('events', function () { 1020 | var template = ''; 1024 | 1025 | it('occur for loaded page', function (done) { 1026 | $httpBackend.expectGET('/items').respond(206, 1027 | '', { 'Range-Unit': 'items', 'Content-Range': '0-24/26' } 1028 | ); 1029 | $compile(template)(scope); 1030 | 1031 | scope.$on('pagination:loadPage', function(e, status, config) { 1032 | expect(config.url).toBe('/items'); 1033 | expect(status).toBe(206); 1034 | done(); 1035 | }); 1036 | scope.$digest(); 1037 | $httpBackend.flush(); 1038 | }); 1039 | 1040 | it('occur for errors', function (done) { 1041 | $httpBackend.expectGET('/items').respond(500, '', { }); 1042 | $compile(template)(scope); 1043 | 1044 | scope.$on('pagination:error', function(e, status, config) { 1045 | expect(config.url).toBe('/items'); 1046 | expect(status).toBe(500); 1047 | done(); 1048 | }); 1049 | scope.$digest(); 1050 | $httpBackend.flush(); 1051 | }); 1052 | }); 1053 | 1054 | describe('currently loaded range', function () { 1055 | var template = ''; 1061 | 1062 | it('stays up to date', function () { 1063 | $httpBackend.whenGET('/items').respond( 1064 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 1065 | ); 1066 | scope.perPage = 5; 1067 | 1068 | scope.page = 0; 1069 | $compile(template)(scope); 1070 | scope.$digest(); 1071 | $httpBackend.flush(); 1072 | 1073 | expect(scope.rangeFrom).toEqual(0); 1074 | expect(scope.rangeTo).toEqual(4); 1075 | 1076 | scope.page = 1; 1077 | scope.$digest(); 1078 | $httpBackend.flush(); 1079 | expect(scope.rangeFrom).toEqual(5); 1080 | expect(scope.rangeTo).toEqual(9); 1081 | }); 1082 | 1083 | it('is left unspecified when no results are returned', function () { 1084 | $httpBackend.expectGET('/items').respond(204, ''); 1085 | scope.perPage = 5; 1086 | scope.page = 0; 1087 | $compile(template)(scope); 1088 | scope.$digest(); 1089 | $httpBackend.flush(); 1090 | 1091 | expect(scope.rangeFrom).toEqual(undefined); 1092 | expect(scope.rangeTo).toEqual(undefined); 1093 | }); 1094 | }); 1095 | 1096 | describe('on page load', function () { 1097 | var template = ''; 1105 | 1106 | it('quantizes items per-page by default', function () { 1107 | $httpBackend.whenGET('/items').respond( 1108 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 1109 | ); 1110 | 1111 | scope.perPage = 20; 1112 | 1113 | scope.page = 0; 1114 | $compile(template)(scope); 1115 | scope.$digest(); 1116 | $httpBackend.flush(); 1117 | 1118 | expect(scope.rangeFrom).toEqual(0); 1119 | expect(scope.rangeTo).toEqual(24); 1120 | }); 1121 | 1122 | it('doesn\'t quantize items per-page if auto-presets is set to false', function () { 1123 | $httpBackend.whenGET('/items').respond( 1124 | finiteStringBackend('abcdefghijklmnopqrstuvwxyz') 1125 | ); 1126 | 1127 | scope.autoPresets = false; 1128 | scope.perPage = 20; 1129 | 1130 | scope.page = 0; 1131 | $compile(template)(scope); 1132 | scope.$digest(); 1133 | $httpBackend.flush(); 1134 | 1135 | expect(scope.rangeFrom).toEqual(0); 1136 | expect(scope.rangeTo).toEqual(19); 1137 | }); 1138 | }); 1139 | describe('blank url', function () { 1140 | var template = ''; 1144 | it('should not make an http request', function () { 1145 | $compile(template)(scope); 1146 | scope.$digest(); 1147 | $httpBackend.verifyNoOutstandingRequest(); 1148 | }); 1149 | }); 1150 | }()); 1151 | --------------------------------------------------------------------------------