From 56642ea3969419f9fa1b2a5ab19b3dd8fe066b3c Mon Sep 17 00:00:00 2001 From: Scotty Waggoner Date: Sun, 19 Apr 2015 00:21:29 -0700 Subject: [PATCH] feat(pagination): add force-ellipses option and boundaryLinkNumbers - Adds force-ellipsis option for adding ellipses to pagination - Add boundary-link-numbers option for allowance of the first and last page numbers to always be shown Closes #2924 Closes #3064 Closes #3565 --- .gitignore | 1 + src/pagination/docs/demo.html | 19 ++- src/pagination/docs/readme.md | 8 ++ src/pagination/pagination.js | 40 +++++- src/pagination/test/pagination.spec.js | 181 +++++++++++++++++++++++-- 5 files changed, 223 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index e378263e09..6716155984 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ lib-cov *.swp *.swo .DS_Store +.idea pids logs diff --git a/src/pagination/docs/demo.html b/src/pagination/docs/demo.html index 16ec3ac447..fa26e2e07c 100644 --- a/src/pagination/docs/demo.html +++ b/src/pagination/docs/demo.html @@ -7,13 +7,22 @@

Default

The selected page no: {{currentPage}}
+
+

Limit the maximum visible buttons

+
rotate defaulted to true:
+ +
rotate defaulted to true and force-ellipses set to true:
+ +
rotate set to false:
+ +
boundary-link-numbers set to true and rotate defaulted to true:
+ +
boundary-link-numbers set to true and rotate set to false:
+ +
Page: {{bigCurrentPage}} / {{numPages}}
+

Pager

-
-

Limit the maximum visible buttons

- - -
Page: {{bigCurrentPage}} / {{numPages}}
diff --git a/src/pagination/docs/readme.md b/src/pagination/docs/readme.md index 07c89d5ed3..74f483179c 100644 --- a/src/pagination/docs/readme.md +++ b/src/pagination/docs/readme.md @@ -36,6 +36,10 @@ Settings can be provided as attributes in the `` or globally con * `rotate` _(Defaults: true)_ : Whether to keep current page in the middle of the visible ones. + + * `force-ellipses` + _(Defaults: false)_ : + Also displays ellipses when `rotate` is true and `max-size` is smaller than the number of pages. * `direction-links` _(Default: true)_ : @@ -60,6 +64,10 @@ Settings can be provided as attributes in the `` or globally con * `last-text` _(Default: 'Last')_ : Text for Last button. + + * `boundary-link-numbers` + _(Default: false)_ : + Whether to always display the first and last page numbers. If `max-size` is smaller than the number of pages, then the first and last page numbers are still shown with ellipses in-between as necessary. NOTE: `max-size` refers to the center of the range. This option may add up to 2 more numbers on each side of the displayed range for the end value and what would be an ellipsis but is replaced by a number because it is sequential. * `template-url` _(Default: 'template/pagination/pagination.html')_ : diff --git a/src/pagination/pagination.js b/src/pagination/pagination.js index 90606eef73..6ae49a2a04 100644 --- a/src/pagination/pagination.js +++ b/src/pagination/pagination.js @@ -24,7 +24,7 @@ angular.module('ui.bootstrap.pagination', []) $scope.$watch('totalItems', function(newTotal, oldTotal) { if (angular.isDefined(newTotal) || newTotal !== oldTotal) { - $scope.totalPages = self.calculateTotalPages(); + $scope.totalPages = self.calculateTotalPages(); updatePage(); } }); @@ -80,12 +80,14 @@ angular.module('ui.bootstrap.pagination', []) .constant('uibPaginationConfig', { itemsPerPage: 10, boundaryLinks: false, + boundaryLinkNumbers: false, directionLinks: true, firstText: 'First', previousText: 'Previous', nextText: 'Next', lastText: 'Last', - rotate: true + rotate: true, + forceEllipses: false }) .directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, paginationConfig) { @@ -115,7 +117,9 @@ angular.module('ui.bootstrap.pagination', []) // Setup configuration parameters var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, - rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; + rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate, + forceEllipses = angular.isDefined(attrs.forceEllipses) ? scope.$parent.$eval(attrs.forceEllipses) : paginationConfig.forceEllipses, + boundaryLinkNumbers = angular.isDefined(attrs.boundaryLinkNumbers) ? scope.$parent.$eval(attrs.boundaryLinkNumbers) : paginationConfig.boundaryLinkNumbers; scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; @@ -148,7 +152,7 @@ angular.module('ui.bootstrap.pagination', []) if (isMaxSized) { if (rotate) { // Current page is displayed in the middle of the visible ones - startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); + startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1); endPage = startPage + maxSize - 1; // Adjust if limit is exceeded @@ -158,7 +162,7 @@ angular.module('ui.bootstrap.pagination', []) } } else { // Visible pages are paginated with maxSize - startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; + startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1; // Adjust last page if limit is exceeded endPage = Math.min(startPage + maxSize - 1, totalPages); @@ -172,16 +176,38 @@ angular.module('ui.bootstrap.pagination', []) } // Add links to move between page sets - if ( isMaxSized && maxSize > 0 ) { - if ( startPage > 1 ) { + if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) { + if (startPage > 1) { + if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning var previousPageSet = makePage(startPage - 1, '...', false); pages.unshift(previousPageSet); } + if (boundaryLinkNumbers) { + if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential + var secondPageLink = makePage(2, '2', false); + pages.unshift(secondPageLink); + } + //add the first page + var firstPageLink = makePage(1, '1', false); + pages.unshift(firstPageLink); + } + } if (endPage < totalPages) { + if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end var nextPageSet = makePage(endPage + 1, '...', false); pages.push(nextPageSet); } + if (boundaryLinkNumbers) { + if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential + var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false); + pages.push(secondToLastPageLink); + } + //add the last page + var lastPageLink = makePage(totalPages, totalPages, false); + pages.push(lastPageLink); + } + } } return pages; } diff --git a/src/pagination/test/pagination.spec.js b/src/pagination/test/pagination.spec.js index 292807f8ec..d94184d2ba 100644 --- a/src/pagination/test/pagination.spec.js +++ b/src/pagination/test/pagination.spec.js @@ -24,9 +24,9 @@ describe('pagination directive', function() { } // Returns a comma-separated string that represents the pager, like: "Prev, 1, 2, 3, Next" - function getPaginationAsText(){ + function getPaginationAsText() { var len = getPaginationBarSize(), outItems = []; - for(var i = 0; i < len; i++){ + for (var i = 0; i < len; i++) { outItems.push(getPaginationEl(i).text()); } return outItems.join(', '); @@ -290,8 +290,8 @@ describe('pagination directive', function() { $rootScope.$digest(); }); - it('contains maxsize + 3 li elements', function() { - expect(getPaginationBarSize()).toBe($rootScope.maxSize + 3); + it('contains maxsize + 2 li elements', function() { + expect(getPaginationBarSize()).toBe($rootScope.maxSize + 2); expect(getPaginationEl(0).text()).toBe('Previous'); expect(getPaginationEl(-1).text()).toBe('Next'); }); @@ -309,8 +309,8 @@ describe('pagination directive', function() { clickPaginationEl(-1); expect($rootScope.currentPage).toBe(7); - expect(getPaginationEl(4)).toHaveClass('active'); - expect(getPaginationEl(4).text()).toBe(''+$rootScope.currentPage); + expect(getPaginationEl(3)).toHaveClass('active'); + expect(getPaginationEl(3).text()).toBe(''+$rootScope.currentPage); }); it('shows the page number in middle after the prev link is clicked', function() { @@ -318,14 +318,14 @@ describe('pagination directive', function() { clickPaginationEl(0); expect($rootScope.currentPage).toBe(6); - expect(getPaginationEl(4)).toHaveClass('active'); - expect(getPaginationEl(4).text()).toBe(''+$rootScope.currentPage); + expect(getPaginationEl(3)).toHaveClass('active'); + expect(getPaginationEl(3).text()).toBe(''+$rootScope.currentPage); }); it('changes pagination bar size when max-size value changed', function() { $rootScope.maxSize = 7; $rootScope.$digest(); - expect(getPaginationBarSize()).toBe(10); + expect(getPaginationBarSize()).toBe(9); }); it('sets the pagination bar size to num-pages, if max-size is greater than num-pages ', function() { @@ -360,6 +360,47 @@ describe('pagination directive', function() { element.remove(); }); + }); + + describe('with `force-ellipses` option', function() { + beforeEach(function() { + $rootScope.total = 98; // 10 pages + $rootScope.currentPage = 3; + $rootScope.maxSize = 5; + element = $compile('')($rootScope); + $rootScope.$digest(); + }); + + it('contains maxsize + 3 li elements', function() { + expect(getPaginationBarSize()).toBe($rootScope.maxSize + 3); + expect(getPaginationEl(0).text()).toBe('Previous'); + expect(getPaginationEl(-1).text()).toBe('Next'); + expect(getPaginationEl(-2).text()).toBe('...'); + }); + + it('shows the page number in middle after the next link is clicked', function() { + updateCurrentPage(6); + clickPaginationEl(-1); + + expect($rootScope.currentPage).toBe(7); + expect(getPaginationEl(4)).toHaveClass('active'); + expect(getPaginationEl(4).text()).toBe(''+$rootScope.currentPage); + }); + + it('shows the page number in middle after the prev link is clicked', function() { + updateCurrentPage(7); + clickPaginationEl(0); + + expect($rootScope.currentPage).toBe(6); + expect(getPaginationEl(4)).toHaveClass('active'); + expect(getPaginationEl(4).text()).toBe(''+$rootScope.currentPage); + }); + + it('changes pagination bar size when max-size value changed', function() { + $rootScope.maxSize = 7; + $rootScope.$digest(); + expect(getPaginationBarSize()).toBe(10); + }); it('should display an ellipsis on the right if the last displayed page\'s number is less than the last page', function() { updateCurrentPage(1); @@ -369,20 +410,112 @@ describe('pagination directive', function() { it('should display an ellipsis on the left if the first displayed page\'s number is greater than 1', function() { updateCurrentPage(10); expect(getPaginationAsText()).toBe('Previous, ..., 6, 7, 8, 9, 10, Next'); - }); + }); it('should display both ellipsis\' if the displayed range is in the middle', function() { updateCurrentPage(5); expect(getPaginationAsText()).toBe('Previous, ..., 3, 4, 5, 6, 7, ..., Next'); }); - it('should not display any ellipsis\' if the number of pages >= maxsize', function() { + it('should not display any ellipses if the number of pages >= maxsize', function() { $rootScope.maxSize = 10; $rootScope.$digest(); expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Next'); }); }); + describe('with `boundary-link-numbers` option', function() { + beforeEach(function() { + $rootScope.total = 98; // 10 pages + $rootScope.currentPage = 3; + $rootScope.maxSize = 5; + element = $compile('')($rootScope); + $rootScope.$digest(); + }); + + it('contains maxsize + 4 li elements', function() { + expect(getPaginationBarSize()).toBe($rootScope.maxSize + 4); + expect(getPaginationEl(0).text()).toBe('Previous'); + expect(getPaginationEl(-1).text()).toBe('Next'); + expect(getPaginationEl(-2).text()).toBe('10'); + expect(getPaginationEl(-3).text()).toBe('...'); + }); + + it('shows the page number in middle after the next link is clicked', function() { + updateCurrentPage(6); + clickPaginationEl(-1); + + expect($rootScope.currentPage).toBe(7); + expect(getPaginationEl(5)).toHaveClass('active'); + expect(getPaginationEl(5).text()).toBe(''+$rootScope.currentPage); + }); + + it('shows the page number in middle after the prev link is clicked', function() { + updateCurrentPage(7); + clickPaginationEl(0); + + expect($rootScope.currentPage).toBe(6); + expect(getPaginationEl(5)).toHaveClass('active'); + expect(getPaginationEl(5).text()).toBe(''+$rootScope.currentPage); + }); + + it('changes pagination bar size when max-size value changed', function() { + $rootScope.maxSize = 7; + $rootScope.$digest(); + expect(getPaginationBarSize()).toBe(11); + }); + + it('should display an ellipsis on the right if the last displayed page\'s number is less than the last page', function() { + updateCurrentPage(1); + expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, ..., 10, Next'); + }); + + it('should display an ellipsis on the left if the first displayed page\'s number is greater than 1', function() { + updateCurrentPage(10); + expect(getPaginationAsText()).toBe('Previous, 1, ..., 6, 7, 8, 9, 10, Next'); + }); + + it('should display both ellipses if the displayed range is in the middle', function() { + $rootScope.maxSize = 3; + $rootScope.$digest(); + updateCurrentPage(6); + expect(getPaginationAsText()).toBe('Previous, 1, ..., 5, 6, 7, ..., 10, Next'); + }); + + it('should not display any ellipses if the number of pages >= maxsize', function() { + $rootScope.maxSize = 10; + $rootScope.$digest(); + expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Next'); + }); + + it('should not display an ellipsis on the left if the start page is 2', function() { + updateCurrentPage(4); + expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, ..., 10, Next'); + }); + + it('should not display an ellipsis on the left if the start page is 3', function() { + updateCurrentPage(5); + expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, ..., 10, Next'); + }); + + it('should not display an ellipsis on the right if the end page is totalPages - 1', function() { + updateCurrentPage(7); + expect(getPaginationAsText()).toBe('Previous, 1, ..., 5, 6, 7, 8, 9, 10, Next'); + }); + + it('should not display an ellipsis on the right if the end page is totalPages - 2', function() { + updateCurrentPage(6); + expect(getPaginationAsText()).toBe('Previous, 1, ..., 4, 5, 6, 7, 8, 9, 10, Next'); + }); + + it('should not display any ellipses if the number of pages <= maxsize + 4 and current page is in center', function() { + $rootScope.total = 88; // 9 pages + $rootScope.$digest(); + updateCurrentPage(5); + expect(getPaginationAsText()).toBe('Previous, 1, 2, 3, 4, 5, 6, 7, 8, 9, Next'); + }); + }); + describe('with `max-size` option & no `rotate`', function() { beforeEach(function() { $rootScope.total = 115; // 12 pages @@ -549,7 +682,7 @@ describe('pagination directive', function() { expect(linkEl).not.toHaveFocus(); element.remove(); - }); + }); it('should blur the "last" link after it has been clicked', function() { body.append(element); @@ -648,7 +781,7 @@ describe('pagination directive', function() { }); }); - describe('`num-pages`', function () { + describe('`num-pages`', function() { beforeEach(function() { $rootScope.numpg = null; element = $compile('')($rootScope); @@ -716,9 +849,29 @@ describe('pagination directive', function() { element = $compile('')($rootScope); $rootScope.$digest(); - // Should contain 2 nav buttons, 2 pages, and 2 ellipsis, since the currentPage defaults to 3, which is in the middle + expect(getPaginationBarSize()).toBe(4); + }); + + it('should take forceEllipses defaults into account', function () { + paginationConfig.forceEllipses = true; + element = $compile('')($rootScope); + $rootScope.$digest(); + + // Should contain 2 nav buttons, 2 pages, and 2 ellipsis since the currentPage defaults to 3, which is in the middle expect(getPaginationBarSize()).toBe(6); }); + + it('should take boundaryLinkNumbers defaults into account', function () { + paginationConfig.boundaryLinkNumbers = true; + $rootScope.total = 88; // 9 pages + $rootScope.currentPage = 5; + element = $compile('')($rootScope); + $rootScope.$digest(); + + // Should contain 2 nav buttons, 2 pages, 2 ellipsis, and 2 extra end numbers since the currentPage is in the middle + expect(getPaginationBarSize()).toBe(9); + expect(getPaginationAsText()).toBe('Previous, 1, ..., 4, 5, 6, ..., 9, Next'); + }); }); describe('override configuration from attributes', function() {