├── .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 |
2 | ## Angular Directive to Paginate Anything
3 | [](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 | Name
74 | Description
75 | Access
76 |
77 |
78 |
79 |
80 | url
81 | url of endpoint which returns a JSON array
82 | Read/write. Changing it will reset to the first page.
83 |
84 |
85 | url-params
86 | map of strings or objects which will be turned to ?key1=value1&key2=value2 after the url
87 | Read/write. Changing it will reset to the first page.
88 |
89 |
90 | headers
91 | additional headers to send during request
92 | Write-only.
93 |
94 |
95 | page
96 | the currently active page
97 | Read/write. Writing changes pages. Zero-based.
98 |
99 |
100 | per-page
101 | (default=`50`) Max number of elements per page
102 | Read/write. The server may choose to send fewer items though.
103 |
104 |
105 | per-page-presets
106 | Array of suggestions for per-page. Adjusts depending on server limits
107 | Read/write.
108 |
109 |
110 | auto-presets
111 | (default=`true`) Overrides per-page presets and client-limit to quantized values 1,2,5,10,25,50...
112 | Read/write.
113 |
114 |
115 | client-limit
116 | (default=`250`) Biggest page size the directive will show. Server response may be smaller.
117 | Read/write.
118 |
119 |
120 | link-group-size
121 | (default=`3`) Number of elements surrounding current page.
122 | Read/write.
123 |
124 |
125 | num-items
126 | Total items reported by server for the collection
127 | Read-only.
128 |
129 |
130 | num-pages
131 | num-items / per-page
132 | Read-only.
133 |
134 |
135 | server-limit
136 | Maximum results the server will send (Infinity if not yet detected)
137 | Read-only.
138 |
139 |
140 | range-from
141 | Position of first item in currently loaded range
142 | Read-only.
143 |
144 |
145 | range-to
146 | Position of last item in currently loaded range
147 | Read-only.
148 |
149 |
150 | reload-page
151 | If set to true, the current page is reloaded.
152 | Write-only.
153 |
154 |
155 | size
156 | Twitter bootstrap sizing `sm`, `md` (default), or `lg` for the navigation elements.
157 | Write-only.
158 |
159 |
160 | passive
161 | If using more than one pagination control set this to 'true' on all but the first.
162 | Write-only.
163 |
164 |
165 | transform-response
166 | Function that will get called once the http response has returned. See Angular's $https documentation for more information.
167 | Read/write. Changing it will reset to the first page.
168 |
169 |
170 | method
171 | Type of request method. Can be either GET or POST. Default is GET.
172 | Read/write.
173 |
174 |
175 | post-data
176 | An array of data to be sent when method is set to POST.
177 | Read/write.
178 |
179 |
180 | load-fn
181 | A callback function to perform the request. Gets the http config as parameter and must return a promise.
182 | Write-only.
183 |
184 |
185 |
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 |
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",'')}]);
--------------------------------------------------------------------------------
/dist/paginate-anything.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
24 |
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 |
24 |
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 |
--------------------------------------------------------------------------------