diff --git a/src/pagination/docs/demo.html b/src/pagination/docs/demo.html index 4629364907..bb7e608676 100644 --- a/src/pagination/docs/demo.html +++ b/src/pagination/docs/demo.html @@ -1,8 +1,9 @@ <div ng-controller="PaginationDemoCtrl"> <pagination num-pages="noOfPages" current-page="currentPage"></pagination> - <pagination num-pages="noOfPages" current-page="currentPage" class="pagination-small"></pagination> - <pagination num-pages="noOfPages" current-page="currentPage" class="pagination-mini"></pagination> + <pagination num-pages="noOfPages" current-page="currentPage" class="pagination-small" previous-text="«" next-text="»"></pagination> + <pagination boundary-links="true" num-pages="noOfPages" current-page="currentPage" max-size="maxSize"></pagination> <pagination num-pages="noOfPages" current-page="currentPage" max-size="maxSize"></pagination> + <pagination direction-links="false" num-pages="noOfPages" current-page="currentPage"></pagination> <button class="btn" ng-click="setPage(3)">Set current page to: 3</button> The selected page no: {{currentPage}} </div> \ No newline at end of file diff --git a/src/pagination/docs/readme.md b/src/pagination/docs/readme.md index 3008a2b67f..b40617116f 100644 --- a/src/pagination/docs/readme.md +++ b/src/pagination/docs/readme.md @@ -1,5 +1,5 @@ A lightweight pagination directive that is focused on ... providing pagination! -It will take care of visualising a pagination bar. Additionally it will make sure that the state (enabled / disabled) of the Previous / Next buttons is maintained correctly. +It will take care of visualising a pagination bar. Additionally it will make sure that the state (enabled / disabled) of the Previous / Next and First / Last buttons (if exist) is maintained correctly. It also provides optional attribute max-size to limit the size of pagination bar. \ No newline at end of file diff --git a/src/pagination/pagination.js b/src/pagination/pagination.js index b66bab5aba..bba5f11ed8 100644 --- a/src/pagination/pagination.js +++ b/src/pagination/pagination.js @@ -1,19 +1,45 @@ angular.module('ui.bootstrap.pagination', []) -.directive('pagination', function() { +.constant('paginationConfig', { + boundaryLinks: false, + directionLinks: true, + firstText: 'First', + previousText: 'Previous', + nextText: 'Next', + lastText: 'Last' +}) + +.directive('pagination', ['paginationConfig', function(paginationConfig) { return { restrict: 'EA', scope: { numPages: '=', currentPage: '=', maxSize: '=', - onSelectPage: '&', - nextText: '@', - previousText: '@' + onSelectPage: '&' }, templateUrl: 'template/pagination/pagination.html', replace: true, - link: function(scope) { + link: function(scope, element, attrs) { + + // Setup configuration parameters + var boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; + var directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$eval(attrs.directionLinks) : paginationConfig.directionLinks; + var firstText = angular.isDefined(attrs.firstText) ? attrs.firstText : paginationConfig.firstText; + var previousText = angular.isDefined(attrs.previousText) ? attrs.previousText : paginationConfig.previousText; + var nextText = angular.isDefined(attrs.nextText) ? attrs.nextText : paginationConfig.nextText; + var lastText = angular.isDefined(attrs.lastText) ? attrs.lastText : paginationConfig.lastText; + + // Create page object used in template + function makePage(number, text, isActive, isDisabled) { + return { + number: number, + text: text, + active: isActive, + disabled: isDisabled + }; + } + scope.$watch('numPages + currentPage + maxSize', function() { scope.pages = []; @@ -29,9 +55,31 @@ angular.module('ui.bootstrap.pagination', []) startPage = startPage - ((startPage + maxSize - 1) - scope.numPages ); } - for(var i=0; i < maxSize && i < scope.numPages ;i++) { - scope.pages.push(startPage + i); + // Add page number links + for (var number = startPage, max = startPage + maxSize; number < max; number++) { + var page = makePage(number, number, scope.isActive(number), false); + scope.pages.push(page); } + + // Add previous & next links + if (directionLinks) { + var previousPage = makePage(scope.currentPage - 1, previousText, false, scope.noPrevious()); + scope.pages.unshift(previousPage); + + var nextPage = makePage(scope.currentPage + 1, nextText, false, scope.noNext()); + scope.pages.push(nextPage); + } + + // Add first & last links + if (boundaryLinks) { + var firstPage = makePage(1, firstText, false, scope.noPrevious()); + scope.pages.unshift(firstPage); + + var lastPage = makePage(scope.numPages, lastText, false, scope.noNext()); + scope.pages.push(lastPage); + } + + if ( scope.currentPage > scope.numPages ) { scope.selectPage(scope.numPages); } @@ -47,22 +95,11 @@ angular.module('ui.bootstrap.pagination', []) }; scope.selectPage = function(page) { - if ( ! scope.isActive(page) ) { + if ( ! scope.isActive(page) && page > 0 && page <= scope.numPages) { scope.currentPage = page; scope.onSelectPage({ page: page }); } }; - - scope.selectPrevious = function() { - if ( !scope.noPrevious() ) { - scope.selectPage(scope.currentPage-1); - } - }; - scope.selectNext = function() { - if ( !scope.noNext() ) { - scope.selectPage(scope.currentPage+1); - } - }; } }; -}); \ No newline at end of file +}]); \ No newline at end of file diff --git a/src/pagination/test/pagination.spec.js b/src/pagination/test/pagination.spec.js index 356168432a..1a49709ce4 100644 --- a/src/pagination/test/pagination.spec.js +++ b/src/pagination/test/pagination.spec.js @@ -1,4 +1,4 @@ -describe('pagination directive', function () { +describe('pagination directive with default configuration', function () { var $rootScope, element; beforeEach(module('ui.bootstrap.pagination')); beforeEach(module('template/pagination/pagination.html')); @@ -192,7 +192,7 @@ describe('pagination directive with max size option', function () { }); -describe('pagination custom', function () { +describe('pagination directive with added first & last links', function () { var $rootScope, element; beforeEach(module('ui.bootstrap.pagination')); beforeEach(module('template/pagination/pagination.html')); @@ -201,12 +201,305 @@ describe('pagination custom', function () { $rootScope = _$rootScope_; $rootScope.numPages = 5; $rootScope.currentPage = 3; - element = $compile('<pagination previous-text="<<" next-text=">>" num-pages="numPages" current-page="currentPage"></pagination>')($rootScope); + element = $compile('<pagination boundary-links="true" num-pages="numPages" current-page="currentPage"></pagination>')($rootScope); $rootScope.$digest(); })); + + it('contains one ul and num-pages + 4 li elements', function() { + expect(element.find('ul').length).toBe(1); + expect(element.find('li').length).toBe(9); + expect(element.find('li').eq(0).text()).toBe('First'); + expect(element.find('li').eq(1).text()).toBe('Previous'); + expect(element.find('li').eq(-2).text()).toBe('Next'); + expect(element.find('li').eq(-1).text()).toBe('Last'); + }); + + it('has first and last li visible & with borders', function() { + var firstLiEl = element.find('li').eq(0); + var lastLiEl = element.find('li').eq(-1); + + expect(firstLiEl.text()).toBe('First'); + expect(firstLiEl.css('display')).not.toBe('none'); + expect(lastLiEl.text()).toBe('Last'); + expect(lastLiEl.css('display')).not.toBe('none'); + }); + + + it('disables the "first" & "previous" link if current-page is 1', function() { + $rootScope.currentPage = 1; + $rootScope.$digest(); + expect(element.find('li').eq(0).hasClass('disabled')).toBe(true); + expect(element.find('li').eq(1).hasClass('disabled')).toBe(true); + }); + + it('disables the "last" & "next" link if current-page is num-pages', function() { + $rootScope.currentPage = 5; + $rootScope.$digest(); + expect(element.find('li').eq(-2).hasClass('disabled')).toBe(true); + expect(element.find('li').eq(-1).hasClass('disabled')).toBe(true); + }); + + + it('changes currentPage if the "first" link is clicked', function() { + var first = element.find('li').eq(0).find('a').eq(0); + first.click(); + $rootScope.$digest(); + expect($rootScope.currentPage).toBe(1); + }); + + it('changes currentPage if the "last" link is clicked', function() { + var last = element.find('li').eq(-1).find('a').eq(0); + last.click(); + $rootScope.$digest(); + expect($rootScope.currentPage).toBe($rootScope.numPages); + }); + + it('does not change the current page on "first" click if already at first page', function() { + var first = element.find('li').eq(0).find('a').eq(0); + $rootScope.currentPage = 1; + $rootScope.$digest(); + first.click(); + $rootScope.$digest(); + expect($rootScope.currentPage).toBe(1); + }); + + it('does not change the current page on "last" click if already at last page', function() { + var last = element.find('li').eq(-1).find('a').eq(0); + $rootScope.currentPage = $rootScope.numPages; + $rootScope.$digest(); + last.click(); + $rootScope.$digest(); + expect($rootScope.currentPage).toBe($rootScope.numPages); + }); + + it('changes "first" & "last" text from attributes', function() { + element = $compile('<pagination boundary-links="true" first-text="<<<" last-text=">>>" num-pages="numPages" current-page="currentPage"></pagination>')($rootScope); + $rootScope.$digest(); + + expect(element.find('li').eq(0).text()).toBe('<<<'); + expect(element.find('li').eq(-1).text()).toBe('>>>'); + }); + + it('changes "previous" & "next" text from attributes', function() { + element = $compile('<pagination boundary-links="true" previous-text="<<" next-text=">>" num-pages="numPages" current-page="currentPage"></pagination>')($rootScope); + $rootScope.$digest(); + + expect(element.find('li').eq(1).text()).toBe('<<'); + expect(element.find('li').eq(-2).text()).toBe('>>'); + }); + +}); + +describe('pagination directive with just number links', function () { + var $rootScope, element; + beforeEach(module('ui.bootstrap.pagination')); + beforeEach(module('template/pagination/pagination.html')); + beforeEach(inject(function(_$compile_, _$rootScope_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + $rootScope.numPages = 5; + $rootScope.currentPage = 3; + element = $compile('<pagination direction-links="false" num-pages="numPages" current-page="currentPage"></pagination>')($rootScope); + $rootScope.$digest(); + })); + + it('has a "pagination" css class', function() { + expect(element.hasClass('pagination')).toBe(true); + }); + + it('contains one ul and num-pages li elements', function() { + expect(element.find('ul').length).toBe(1); + expect(element.find('li').length).toBe(5); + expect(element.find('li').eq(0).text()).toBe('1'); + expect(element.find('li').eq(-1).text()).toBe(''+$rootScope.numPages); + }); + + it('has the number of the page as text in each page item', function() { + var lis = element.find('li'); + for(var i=0; i<$rootScope.numPages;i++) { + expect(lis.eq(i).text()).toEqual(''+(i+1)); + } + }); + + it('sets the current-page to be active', function() { + var currentPageItem = element.find('li').eq($rootScope.currentPage-1); + expect(currentPageItem.hasClass('active')).toBe(true); + }); + + it('does not disable the "1" link if current-page is 1', function() { + $rootScope.currentPage = 1; + $rootScope.$digest(); + var onePageItem = element.find('li').eq(0); + expect(onePageItem.hasClass('disabled')).toBe(false); + expect(onePageItem.hasClass('active')).toBe(true); + }); + + it('does not disable the "numPages" link if current-page is num-pages', function() { + $rootScope.currentPage = 5; + $rootScope.$digest(); + var lastPageItem = element.find('li').eq(-1); + expect(lastPageItem.hasClass('disabled')).toBe(false); + expect(lastPageItem.hasClass('active')).toBe(true); + }); + + it('changes currentPage if a page link is clicked', function() { + var page2 = element.find('li').eq(1).find('a'); + page2.click(); + $rootScope.$digest(); + expect($rootScope.currentPage).toBe(2); + }); + + + it('executes the onSelectPage expression when the current page changes', function() { + $rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler'); + element = $compile('<pagination direction-links="false" num-pages="numPages" current-page="currentPage" on-select-page="selectPageHandler(page)"></pagination>')($rootScope); + $rootScope.$digest(); + var page2 = element.find('li').eq(1).find('a').eq(0); + page2.click(); + $rootScope.$digest(); + expect($rootScope.selectPageHandler).toHaveBeenCalledWith(2); + }); + + it('changes the number of items when numPages changes', function() { + $rootScope.numPages = 8; + $rootScope.$digest(); + expect(element.find('li').length).toBe(8); + expect(element.find('li').eq(0).text()).toBe('1'); + expect(element.find('li').eq(-1).text()).toBe(''+$rootScope.numPages); + }); + + it('sets the current page to the last page if the numPages is changed to less than the current page', function() { + $rootScope.selectPageHandler = jasmine.createSpy('selectPageHandler'); + element = $compile('<pagination direction-links="false" num-pages="numPages" current-page="currentPage" on-select-page="selectPageHandler(page)"></pagination>')($rootScope); + $rootScope.$digest(); + $rootScope.numPages = 2; + $rootScope.$digest(); + expect(element.find('li').length).toBe(2); + expect($rootScope.currentPage).toBe(2); + expect($rootScope.selectPageHandler).toHaveBeenCalledWith(2); + }); +}); + +describe('setting paginationConfig', function() { + var $rootScope, element; + var originalConfig = {}; + beforeEach(module('ui.bootstrap.pagination')); + beforeEach(module('template/pagination/pagination.html')); + beforeEach(inject(function(_$compile_, _$rootScope_, paginationConfig) { + $compile = _$compile_; + $rootScope = _$rootScope_; + $rootScope.numPages = 5; + $rootScope.currentPage = 3; + angular.extend(originalConfig, paginationConfig); + paginationConfig.boundaryLinks = true; + paginationConfig.directionLinks = true; + paginationConfig.firstText = 'FI'; + paginationConfig.previousText = 'PR'; + paginationConfig.nextText = 'NE'; + paginationConfig.lastText = 'LA'; + element = $compile('<pagination num-pages="numPages" current-page="currentPage"></pagination>')($rootScope); + $rootScope.$digest(); + })); + afterEach(inject(function(paginationConfig) { + // return it to the original state + angular.extend(paginationConfig, originalConfig); + })); + + it('should change paging text', function () { + expect(element.find('li').eq(0).text()).toBe('FI'); + expect(element.find('li').eq(1).text()).toBe('PR'); + expect(element.find('li').eq(-2).text()).toBe('NE'); + expect(element.find('li').eq(-1).text()).toBe('LA'); + }); + + it('contains one ul and num-pages + 4 li elements', function() { + expect(element.find('ul').length).toBe(1); + expect(element.find('li').length).toBe(9); + }); + +}); + + +describe('pagination directive with first, last & number links', function () { + var $rootScope, element; + beforeEach(module('ui.bootstrap.pagination')); + beforeEach(module('template/pagination/pagination.html')); + beforeEach(inject(function(_$compile_, _$rootScope_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + $rootScope.numPages = 5; + $rootScope.currentPage = 3; + element = $compile('<pagination boundary-links="true" direction-links="false" num-pages="numPages" current-page="currentPage"></pagination>')($rootScope); + $rootScope.$digest(); + })); + + + it('contains one ul and num-pages + 2 li elements', function() { + expect(element.find('ul').length).toBe(1); + expect(element.find('li').length).toBe(7); + expect(element.find('li').eq(0).text()).toBe('First'); + expect(element.find('li').eq(1).text()).toBe('1'); + expect(element.find('li').eq(-2).text()).toBe(''+$rootScope.numPages); + expect(element.find('li').eq(-1).text()).toBe('Last'); + }); + + + it('disables the "first" & activates "1" link if current-page is 1', function() { + $rootScope.currentPage = 1; + $rootScope.$digest(); + expect(element.find('li').eq(0).hasClass('disabled')).toBe(true); + expect(element.find('li').eq(1).hasClass('disabled')).toBe(false); + expect(element.find('li').eq(1).hasClass('active')).toBe(true); + }); + + it('disables the "last" & "next" link if current-page is num-pages', function() { + $rootScope.currentPage = 5; + $rootScope.$digest(); + expect(element.find('li').eq(-2).hasClass('disabled')).toBe(false); + expect(element.find('li').eq(-2).hasClass('active')).toBe(true); + expect(element.find('li').eq(-1).hasClass('disabled')).toBe(true); + }); + + + it('changes currentPage if the "first" link is clicked', function() { + var first = element.find('li').eq(0).find('a').eq(0); + first.click(); + $rootScope.$digest(); + expect($rootScope.currentPage).toBe(1); + }); + + it('changes currentPage if the "last" link is clicked', function() { + var last = element.find('li').eq(-1).find('a').eq(0); + last.click(); + $rootScope.$digest(); + expect($rootScope.currentPage).toBe($rootScope.numPages); + }); + +}); + +describe('pagination bypass configuration from attributes', function () { + var $rootScope, element; + beforeEach(module('ui.bootstrap.pagination')); + beforeEach(module('template/pagination/pagination.html')); + beforeEach(inject(function(_$compile_, _$rootScope_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + $rootScope.numPages = 5; + $rootScope.currentPage = 3; + element = $compile('<pagination boundary-links="true" first-text="<<" previous-text="<" next-text=">" last-text=">>" num-pages="numPages" current-page="currentPage"></pagination>')($rootScope); + $rootScope.$digest(); + })); + + it('contains one ul and num-pages + 4 li elements', function() { + expect(element.find('ul').length).toBe(1); + expect(element.find('li').length).toBe(9); + }); + it('should change paging text from attribute', function () { expect(element.find('li').eq(0).text()).toBe('<<'); + expect(element.find('li').eq(1).text()).toBe('<'); + expect(element.find('li').eq(-2).text()).toBe('>'); expect(element.find('li').eq(-1).text()).toBe('>>'); }); diff --git a/template/pagination/pagination.html b/template/pagination/pagination.html index 4c0d40baac..dec9c8f7cf 100644 --- a/template/pagination/pagination.html +++ b/template/pagination/pagination.html @@ -1,6 +1,4 @@ <div class="pagination"><ul> - <li ng-class="{disabled: noPrevious()}"><a ng-click="selectPrevious()">{{previousText || 'Previous'}}</a></li> - <li ng-repeat="page in pages" ng-class="{active: isActive(page)}"><a ng-click="selectPage(page)">{{page}}</a></li> - <li ng-class="{disabled: noNext()}"><a ng-click="selectNext()">{{nextText || 'Next'}}</a></li> + <li ng-repeat="page in pages" ng-class="{active: page.active, disabled: page.disabled}"><a ng-click="selectPage(page.number)">{{page.text}}</a></li> </ul> </div>