1*bd500cd2Sbeccabroek/** 2*bd500cd2Sbeccabroek * dirPagination - AngularJS module for paginating (almost) anything. 3*bd500cd2Sbeccabroek * https://github.com/michaelbromley/angularUtils 4*bd500cd2Sbeccabroek * 5*bd500cd2Sbeccabroek * 6*bd500cd2Sbeccabroek * Credits 7*bd500cd2Sbeccabroek * ======= 8*bd500cd2Sbeccabroek * 9*bd500cd2Sbeccabroek * Daniel Tabuenca: 10*bd500cd2Sbeccabroek * https://groups.google.com/d/msg/angular/an9QpzqIYiM/r8v-3W1X5vcJ for the idea 11*bd500cd2Sbeccabroek * on how to dynamically invoke the ng-repeat directive. 12*bd500cd2Sbeccabroek * 13*bd500cd2Sbeccabroek * I borrowed a couple of lines and a few attribute names from the AngularUI 14*bd500cd2Sbeccabroek * Bootstrap project: 15*bd500cd2Sbeccabroek * https://github.com/angular-ui/bootstrap/blob/master/src/pagination/pagination.js 16*bd500cd2Sbeccabroek * 17*bd500cd2Sbeccabroek * Copyright 2014 Michael Bromley <michael@michaelbromley.co.uk> 18*bd500cd2Sbeccabroek */ 19*bd500cd2Sbeccabroek 20*bd500cd2Sbeccabroek(function() { 21*bd500cd2Sbeccabroek 22*bd500cd2Sbeccabroek/** 23*bd500cd2Sbeccabroek * Config 24*bd500cd2Sbeccabroek */ 25*bd500cd2Sbeccabroekvar moduleName = 'app.common.directives.dirPagination'; 26*bd500cd2Sbeccabroekvar DEFAULT_ID = '__default'; 27*bd500cd2Sbeccabroek 28*bd500cd2Sbeccabroek/** 29*bd500cd2Sbeccabroek * Module 30*bd500cd2Sbeccabroek */ 31*bd500cd2Sbeccabroekangular.module(moduleName, []) 32*bd500cd2Sbeccabroek .directive( 33*bd500cd2Sbeccabroek 'dirPaginate', 34*bd500cd2Sbeccabroek ['$compile', '$parse', 'paginationService', dirPaginateDirective]) 35*bd500cd2Sbeccabroek .directive('dirPaginateNoCompile', noCompileDirective) 36*bd500cd2Sbeccabroek .directive( 37*bd500cd2Sbeccabroek 'dirPaginationControls', 38*bd500cd2Sbeccabroek [ 39*bd500cd2Sbeccabroek 'paginationService', 'paginationTemplate', 40*bd500cd2Sbeccabroek dirPaginationControlsDirective 41*bd500cd2Sbeccabroek ]) 42*bd500cd2Sbeccabroek .filter('itemsPerPage', ['paginationService', itemsPerPageFilter]) 43*bd500cd2Sbeccabroek .service('paginationService', paginationService) 44*bd500cd2Sbeccabroek .provider('paginationTemplate', paginationTemplateProvider) 45*bd500cd2Sbeccabroek .run(['$templateCache', dirPaginationControlsTemplateInstaller]); 46*bd500cd2Sbeccabroek 47*bd500cd2Sbeccabroekfunction dirPaginateDirective($compile, $parse, paginationService) { 48*bd500cd2Sbeccabroek return { 49*bd500cd2Sbeccabroek terminal: true, 50*bd500cd2Sbeccabroek multiElement: true, 51*bd500cd2Sbeccabroek priority: 100, 52*bd500cd2Sbeccabroek compile: dirPaginationCompileFn 53*bd500cd2Sbeccabroek }; 54*bd500cd2Sbeccabroek 55*bd500cd2Sbeccabroek function dirPaginationCompileFn(tElement, tAttrs) { 56*bd500cd2Sbeccabroek var expression = tAttrs.dirPaginate; 57*bd500cd2Sbeccabroek // regex taken directly from 58*bd500cd2Sbeccabroek // https://github.com/angular/angular.js/blob/v1.4.x/src/ng/directive/ngRepeat.js#L339 59*bd500cd2Sbeccabroek var match = expression.match( 60*bd500cd2Sbeccabroek /^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); 61*bd500cd2Sbeccabroek 62*bd500cd2Sbeccabroek var filterPattern = 63*bd500cd2Sbeccabroek /\|\s*itemsPerPage\s*:\s*(.*\(\s*\w*\)|([^\)]*?(?=\s+as\s+))|[^\)]*)/; 64*bd500cd2Sbeccabroek if (match[2].match(filterPattern) === null) { 65*bd500cd2Sbeccabroek throw 'pagination directive: the \'itemsPerPage\' filter must be set.'; 66*bd500cd2Sbeccabroek } 67*bd500cd2Sbeccabroek var itemsPerPageFilterRemoved = match[2].replace(filterPattern, ''); 68*bd500cd2Sbeccabroek var collectionGetter = $parse(itemsPerPageFilterRemoved); 69*bd500cd2Sbeccabroek 70*bd500cd2Sbeccabroek addNoCompileAttributes(tElement); 71*bd500cd2Sbeccabroek 72*bd500cd2Sbeccabroek // If any value is specified for paginationId, we register the un-evaluated 73*bd500cd2Sbeccabroek // expression at this stage for the benefit of any dir-pagination-controls 74*bd500cd2Sbeccabroek // directives that may be looking for this ID. 75*bd500cd2Sbeccabroek var rawId = tAttrs.paginationId || DEFAULT_ID; 76*bd500cd2Sbeccabroek paginationService.registerInstance(rawId); 77*bd500cd2Sbeccabroek 78*bd500cd2Sbeccabroek return function dirPaginationLinkFn(scope, element, attrs) { 79*bd500cd2Sbeccabroek // Now that we have access to the `scope` we can interpolate any 80*bd500cd2Sbeccabroek // expression given in the paginationId attribute and potentially register 81*bd500cd2Sbeccabroek // a new ID if it evaluates to a different value than the rawId. 82*bd500cd2Sbeccabroek var paginationId = 83*bd500cd2Sbeccabroek $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID; 84*bd500cd2Sbeccabroek 85*bd500cd2Sbeccabroek // (TODO: this seems sound, but I'm reverting as many bug reports followed 86*bd500cd2Sbeccabroek // it's introduction in 0.11.0. Needs more investigation.) In case rawId 87*bd500cd2Sbeccabroek // != paginationId we deregister using rawId for the sake of general 88*bd500cd2Sbeccabroek // cleanliness before registering using paginationId 89*bd500cd2Sbeccabroek // paginationService.deregisterInstance(rawId); 90*bd500cd2Sbeccabroek paginationService.registerInstance(paginationId); 91*bd500cd2Sbeccabroek 92*bd500cd2Sbeccabroek var repeatExpression = getRepeatExpression(expression, paginationId); 93*bd500cd2Sbeccabroek addNgRepeatToElement(element, attrs, repeatExpression); 94*bd500cd2Sbeccabroek 95*bd500cd2Sbeccabroek removeTemporaryAttributes(element); 96*bd500cd2Sbeccabroek var compiled = $compile(element); 97*bd500cd2Sbeccabroek 98*bd500cd2Sbeccabroek var currentPageGetter = 99*bd500cd2Sbeccabroek makeCurrentPageGetterFn(scope, attrs, paginationId); 100*bd500cd2Sbeccabroek paginationService.setCurrentPageParser( 101*bd500cd2Sbeccabroek paginationId, currentPageGetter, scope); 102*bd500cd2Sbeccabroek 103*bd500cd2Sbeccabroek if (typeof attrs.totalItems !== 'undefined') { 104*bd500cd2Sbeccabroek paginationService.setAsyncModeTrue(paginationId); 105*bd500cd2Sbeccabroek scope.$watch( 106*bd500cd2Sbeccabroek function() { 107*bd500cd2Sbeccabroek return $parse(attrs.totalItems)(scope); 108*bd500cd2Sbeccabroek }, 109*bd500cd2Sbeccabroek function(result) { 110*bd500cd2Sbeccabroek if (0 <= result) { 111*bd500cd2Sbeccabroek paginationService.setCollectionLength(paginationId, result); 112*bd500cd2Sbeccabroek } 113*bd500cd2Sbeccabroek }); 114*bd500cd2Sbeccabroek } else { 115*bd500cd2Sbeccabroek paginationService.setAsyncModeFalse(paginationId); 116*bd500cd2Sbeccabroek scope.$watchCollection( 117*bd500cd2Sbeccabroek function() { 118*bd500cd2Sbeccabroek return collectionGetter(scope); 119*bd500cd2Sbeccabroek }, 120*bd500cd2Sbeccabroek function(collection) { 121*bd500cd2Sbeccabroek if (collection) { 122*bd500cd2Sbeccabroek var collectionLength = (collection instanceof Array) ? 123*bd500cd2Sbeccabroek collection.length : 124*bd500cd2Sbeccabroek Object.keys(collection).length; 125*bd500cd2Sbeccabroek paginationService.setCollectionLength( 126*bd500cd2Sbeccabroek paginationId, collectionLength); 127*bd500cd2Sbeccabroek } 128*bd500cd2Sbeccabroek }); 129*bd500cd2Sbeccabroek } 130*bd500cd2Sbeccabroek 131*bd500cd2Sbeccabroek // Delegate to the link function returned by the new compilation of the 132*bd500cd2Sbeccabroek // ng-repeat 133*bd500cd2Sbeccabroek compiled(scope); 134*bd500cd2Sbeccabroek 135*bd500cd2Sbeccabroek // (TODO: Reverting this due to many bug reports in v 0.11.0. Needs 136*bd500cd2Sbeccabroek // investigation as the principle is sound) When the scope is destroyed, 137*bd500cd2Sbeccabroek // we make sure to remove the reference to it in paginationService so that 138*bd500cd2Sbeccabroek // it can be properly garbage collected scope.$on('$destroy', function 139*bd500cd2Sbeccabroek // destroyDirPagination() { 140*bd500cd2Sbeccabroek // paginationService.deregisterInstance(paginationId); 141*bd500cd2Sbeccabroek // }); 142*bd500cd2Sbeccabroek }; 143*bd500cd2Sbeccabroek } 144*bd500cd2Sbeccabroek 145*bd500cd2Sbeccabroek /** 146*bd500cd2Sbeccabroek * If a pagination id has been specified, we need to check that it is present 147*bd500cd2Sbeccabroek * as the second argument passed to the itemsPerPage filter. If it is not 148*bd500cd2Sbeccabroek * there, we add it and return the modified expression. 149*bd500cd2Sbeccabroek * 150*bd500cd2Sbeccabroek * @param expression 151*bd500cd2Sbeccabroek * @param paginationId 152*bd500cd2Sbeccabroek * @returns {*} 153*bd500cd2Sbeccabroek */ 154*bd500cd2Sbeccabroek function getRepeatExpression(expression, paginationId) { 155*bd500cd2Sbeccabroek var repeatExpression, 156*bd500cd2Sbeccabroek idDefinedInFilter = 157*bd500cd2Sbeccabroek !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/); 158*bd500cd2Sbeccabroek 159*bd500cd2Sbeccabroek if (paginationId !== DEFAULT_ID && !idDefinedInFilter) { 160*bd500cd2Sbeccabroek repeatExpression = expression.replace( 161*bd500cd2Sbeccabroek /(\|\s*itemsPerPage\s*:\s*[^|\s]*)/, '$1 : \'' + paginationId + '\''); 162*bd500cd2Sbeccabroek } else { 163*bd500cd2Sbeccabroek repeatExpression = expression; 164*bd500cd2Sbeccabroek } 165*bd500cd2Sbeccabroek 166*bd500cd2Sbeccabroek return repeatExpression; 167*bd500cd2Sbeccabroek } 168*bd500cd2Sbeccabroek 169*bd500cd2Sbeccabroek /** 170*bd500cd2Sbeccabroek * Adds the ng-repeat directive to the element. In the case of multi-element 171*bd500cd2Sbeccabroek * (-start, -end) it adds the appropriate multi-element ng-repeat to the first 172*bd500cd2Sbeccabroek * and last element in the range. 173*bd500cd2Sbeccabroek * @param element 174*bd500cd2Sbeccabroek * @param attrs 175*bd500cd2Sbeccabroek * @param repeatExpression 176*bd500cd2Sbeccabroek */ 177*bd500cd2Sbeccabroek function addNgRepeatToElement(element, attrs, repeatExpression) { 178*bd500cd2Sbeccabroek if (element[0].hasAttribute('dir-paginate-start') || 179*bd500cd2Sbeccabroek element[0].hasAttribute('data-dir-paginate-start')) { 180*bd500cd2Sbeccabroek // using multiElement mode (dir-paginate-start, dir-paginate-end) 181*bd500cd2Sbeccabroek attrs.$set('ngRepeatStart', repeatExpression); 182*bd500cd2Sbeccabroek element.eq(element.length - 1).attr('ng-repeat-end', true); 183*bd500cd2Sbeccabroek } else { 184*bd500cd2Sbeccabroek attrs.$set('ngRepeat', repeatExpression); 185*bd500cd2Sbeccabroek } 186*bd500cd2Sbeccabroek } 187*bd500cd2Sbeccabroek 188*bd500cd2Sbeccabroek /** 189*bd500cd2Sbeccabroek * Adds the dir-paginate-no-compile directive to each element in the tElement 190*bd500cd2Sbeccabroek * range. 191*bd500cd2Sbeccabroek * @param tElement 192*bd500cd2Sbeccabroek */ 193*bd500cd2Sbeccabroek function addNoCompileAttributes(tElement) { 194*bd500cd2Sbeccabroek angular.forEach(tElement, function(el) { 195*bd500cd2Sbeccabroek if (el.nodeType === 1) { 196*bd500cd2Sbeccabroek angular.element(el).attr('dir-paginate-no-compile', true); 197*bd500cd2Sbeccabroek } 198*bd500cd2Sbeccabroek }); 199*bd500cd2Sbeccabroek } 200*bd500cd2Sbeccabroek 201*bd500cd2Sbeccabroek /** 202*bd500cd2Sbeccabroek * Removes the variations on dir-paginate (data-, -start, -end) and the 203*bd500cd2Sbeccabroek * dir-paginate-no-compile directives. 204*bd500cd2Sbeccabroek * @param element 205*bd500cd2Sbeccabroek */ 206*bd500cd2Sbeccabroek function removeTemporaryAttributes(element) { 207*bd500cd2Sbeccabroek angular.forEach(element, function(el) { 208*bd500cd2Sbeccabroek if (el.nodeType === 1) { 209*bd500cd2Sbeccabroek angular.element(el).removeAttr('dir-paginate-no-compile'); 210*bd500cd2Sbeccabroek } 211*bd500cd2Sbeccabroek }); 212*bd500cd2Sbeccabroek element.eq(0) 213*bd500cd2Sbeccabroek .removeAttr('dir-paginate-start') 214*bd500cd2Sbeccabroek .removeAttr('dir-paginate') 215*bd500cd2Sbeccabroek .removeAttr('data-dir-paginate-start') 216*bd500cd2Sbeccabroek .removeAttr('data-dir-paginate'); 217*bd500cd2Sbeccabroek element.eq(element.length - 1) 218*bd500cd2Sbeccabroek .removeAttr('dir-paginate-end') 219*bd500cd2Sbeccabroek .removeAttr('data-dir-paginate-end'); 220*bd500cd2Sbeccabroek } 221*bd500cd2Sbeccabroek 222*bd500cd2Sbeccabroek /** 223*bd500cd2Sbeccabroek * Creates a getter function for the current-page attribute, using the 224*bd500cd2Sbeccabroek * expression provided or a default value if no current-page expression was 225*bd500cd2Sbeccabroek * specified. 226*bd500cd2Sbeccabroek * 227*bd500cd2Sbeccabroek * @param scope 228*bd500cd2Sbeccabroek * @param attrs 229*bd500cd2Sbeccabroek * @param paginationId 230*bd500cd2Sbeccabroek * @returns {*} 231*bd500cd2Sbeccabroek */ 232*bd500cd2Sbeccabroek function makeCurrentPageGetterFn(scope, attrs, paginationId) { 233*bd500cd2Sbeccabroek var currentPageGetter; 234*bd500cd2Sbeccabroek if (attrs.currentPage) { 235*bd500cd2Sbeccabroek currentPageGetter = $parse(attrs.currentPage); 236*bd500cd2Sbeccabroek } else { 237*bd500cd2Sbeccabroek // If the current-page attribute was not set, we'll make our own. 238*bd500cd2Sbeccabroek // Replace any non-alphanumeric characters which might confuse 239*bd500cd2Sbeccabroek // the $parse service and give unexpected results. 240*bd500cd2Sbeccabroek // See https://github.com/michaelbromley/angularUtils/issues/233 241*bd500cd2Sbeccabroek var defaultCurrentPage = 242*bd500cd2Sbeccabroek (paginationId + '__currentPage').replace(/\W/g, '_'); 243*bd500cd2Sbeccabroek scope[defaultCurrentPage] = 1; 244*bd500cd2Sbeccabroek currentPageGetter = $parse(defaultCurrentPage); 245*bd500cd2Sbeccabroek } 246*bd500cd2Sbeccabroek return currentPageGetter; 247*bd500cd2Sbeccabroek } 248*bd500cd2Sbeccabroek} 249*bd500cd2Sbeccabroek 250*bd500cd2Sbeccabroek/** 251*bd500cd2Sbeccabroek * This is a helper directive that allows correct compilation when in 252*bd500cd2Sbeccabroek * multi-element mode (ie dir-paginate-start, dir-paginate-end). It is 253*bd500cd2Sbeccabroek * dynamically added to all elements in the dir-paginate compile function, and 254*bd500cd2Sbeccabroek * it prevents further compilation of any inner directives. It is then removed 255*bd500cd2Sbeccabroek * in the link function, and all inner directives are then manually compiled. 256*bd500cd2Sbeccabroek */ 257*bd500cd2Sbeccabroekfunction noCompileDirective() { 258*bd500cd2Sbeccabroek return {priority: 5000, terminal: true}; 259*bd500cd2Sbeccabroek} 260*bd500cd2Sbeccabroek 261*bd500cd2Sbeccabroekfunction dirPaginationControlsTemplateInstaller($templateCache) { 262*bd500cd2Sbeccabroek $templateCache.put( 263*bd500cd2Sbeccabroek 'app.common.directives.dirPagination.template', 264*bd500cd2Sbeccabroek '<ul class="pagination" ng-if="1 < pages.length || !autoHide"><li ng-if="boundaryLinks" ng-class="{ disabled : pagination.current == 1 }"><a href="" ng-click="setCurrent(1)">«</a></li><li ng-if="directionLinks" ng-class="{ disabled : pagination.current == 1 }"><a href="" ng-click="setCurrent(pagination.current - 1)">‹</a></li><li ng-repeat="pageNumber in pages track by tracker(pageNumber, $index)" ng-class="{ active : pagination.current == pageNumber, disabled : pageNumber == \'...\' || ( ! autoHide && pages.length === 1 ) }"><a href="" ng-click="setCurrent(pageNumber)">{{ pageNumber }}</a></li><li ng-if="directionLinks" ng-class="{ disabled : pagination.current == pagination.last }"><a href="" ng-click="setCurrent(pagination.current + 1)">›</a></li><li ng-if="boundaryLinks" ng-class="{ disabled : pagination.current == pagination.last }"><a href="" ng-click="setCurrent(pagination.last)">»</a></li></ul>'); 265*bd500cd2Sbeccabroek} 266*bd500cd2Sbeccabroek 267*bd500cd2Sbeccabroekfunction dirPaginationControlsDirective(paginationService, paginationTemplate) { 268*bd500cd2Sbeccabroek var numberRegex = /^\d+$/; 269*bd500cd2Sbeccabroek 270*bd500cd2Sbeccabroek var DDO = { 271*bd500cd2Sbeccabroek restrict: 'AE', 272*bd500cd2Sbeccabroek scope: 273*bd500cd2Sbeccabroek {maxSize: '=?', onPageChange: '&?', paginationId: '=?', autoHide: '=?'}, 274*bd500cd2Sbeccabroek link: dirPaginationControlsLinkFn 275*bd500cd2Sbeccabroek }; 276*bd500cd2Sbeccabroek 277*bd500cd2Sbeccabroek // We need to check the paginationTemplate service to see whether a template 278*bd500cd2Sbeccabroek // path or string has been specified, and add the `template` or `templateUrl` 279*bd500cd2Sbeccabroek // property to the DDO as appropriate. The order of priority to decide which 280*bd500cd2Sbeccabroek // template to use is (highest priority first): 281*bd500cd2Sbeccabroek // 1. paginationTemplate.getString() 282*bd500cd2Sbeccabroek // 2. attrs.templateUrl 283*bd500cd2Sbeccabroek // 3. paginationTemplate.getPath() 284*bd500cd2Sbeccabroek var templateString = paginationTemplate.getString(); 285*bd500cd2Sbeccabroek if (templateString !== undefined) { 286*bd500cd2Sbeccabroek DDO.template = templateString; 287*bd500cd2Sbeccabroek } else { 288*bd500cd2Sbeccabroek DDO.templateUrl = function(elem, attrs) { 289*bd500cd2Sbeccabroek return attrs.templateUrl || paginationTemplate.getPath(); 290*bd500cd2Sbeccabroek }; 291*bd500cd2Sbeccabroek } 292*bd500cd2Sbeccabroek return DDO; 293*bd500cd2Sbeccabroek 294*bd500cd2Sbeccabroek function dirPaginationControlsLinkFn(scope, element, attrs) { 295*bd500cd2Sbeccabroek // rawId is the un-interpolated value of the pagination-id attribute. This 296*bd500cd2Sbeccabroek // is only important when the corresponding dir-paginate directive has not 297*bd500cd2Sbeccabroek // yet been linked (e.g. if it is inside an ng-if block), and in that case 298*bd500cd2Sbeccabroek // it prevents this controls directive from assuming that there is no 299*bd500cd2Sbeccabroek // corresponding dir-paginate directive and wrongly throwing an exception. 300*bd500cd2Sbeccabroek var rawId = attrs.paginationId || DEFAULT_ID; 301*bd500cd2Sbeccabroek var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID; 302*bd500cd2Sbeccabroek 303*bd500cd2Sbeccabroek if (!paginationService.isRegistered(paginationId) && 304*bd500cd2Sbeccabroek !paginationService.isRegistered(rawId)) { 305*bd500cd2Sbeccabroek var idMessage = 306*bd500cd2Sbeccabroek (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' '; 307*bd500cd2Sbeccabroek if (window.console) { 308*bd500cd2Sbeccabroek console.warn( 309*bd500cd2Sbeccabroek 'Pagination directive: the pagination controls' + idMessage + 310*bd500cd2Sbeccabroek 'cannot be used without the corresponding pagination directive, which was not found at link time.'); 311*bd500cd2Sbeccabroek } 312*bd500cd2Sbeccabroek } 313*bd500cd2Sbeccabroek 314*bd500cd2Sbeccabroek if (!scope.maxSize) { 315*bd500cd2Sbeccabroek scope.maxSize = 9; 316*bd500cd2Sbeccabroek } 317*bd500cd2Sbeccabroek scope.autoHide = scope.autoHide === undefined ? true : scope.autoHide; 318*bd500cd2Sbeccabroek scope.directionLinks = angular.isDefined(attrs.directionLinks) ? 319*bd500cd2Sbeccabroek scope.$parent.$eval(attrs.directionLinks) : 320*bd500cd2Sbeccabroek true; 321*bd500cd2Sbeccabroek scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? 322*bd500cd2Sbeccabroek scope.$parent.$eval(attrs.boundaryLinks) : 323*bd500cd2Sbeccabroek false; 324*bd500cd2Sbeccabroek 325*bd500cd2Sbeccabroek var paginationRange = Math.max(scope.maxSize, 5); 326*bd500cd2Sbeccabroek scope.pages = []; 327*bd500cd2Sbeccabroek scope.pagination = {last: 1, current: 1}; 328*bd500cd2Sbeccabroek scope.range = {lower: 1, upper: 1, total: 1}; 329*bd500cd2Sbeccabroek 330*bd500cd2Sbeccabroek scope.$watch('maxSize', function(val) { 331*bd500cd2Sbeccabroek if (val) { 332*bd500cd2Sbeccabroek paginationRange = Math.max(scope.maxSize, 5); 333*bd500cd2Sbeccabroek generatePagination(); 334*bd500cd2Sbeccabroek } 335*bd500cd2Sbeccabroek }); 336*bd500cd2Sbeccabroek 337*bd500cd2Sbeccabroek scope.$watch( 338*bd500cd2Sbeccabroek function() { 339*bd500cd2Sbeccabroek if (paginationService.isRegistered(paginationId)) { 340*bd500cd2Sbeccabroek return (paginationService.getCollectionLength(paginationId) + 1) * 341*bd500cd2Sbeccabroek paginationService.getItemsPerPage(paginationId); 342*bd500cd2Sbeccabroek } 343*bd500cd2Sbeccabroek }, 344*bd500cd2Sbeccabroek function(length) { 345*bd500cd2Sbeccabroek if (0 < length) { 346*bd500cd2Sbeccabroek generatePagination(); 347*bd500cd2Sbeccabroek } 348*bd500cd2Sbeccabroek }); 349*bd500cd2Sbeccabroek 350*bd500cd2Sbeccabroek scope.$watch( 351*bd500cd2Sbeccabroek function() { 352*bd500cd2Sbeccabroek if (paginationService.isRegistered(paginationId)) { 353*bd500cd2Sbeccabroek return (paginationService.getItemsPerPage(paginationId)); 354*bd500cd2Sbeccabroek } 355*bd500cd2Sbeccabroek }, 356*bd500cd2Sbeccabroek function(current, previous) { 357*bd500cd2Sbeccabroek if (current != previous && typeof previous !== 'undefined') { 358*bd500cd2Sbeccabroek goToPage(scope.pagination.current); 359*bd500cd2Sbeccabroek } 360*bd500cd2Sbeccabroek }); 361*bd500cd2Sbeccabroek 362*bd500cd2Sbeccabroek scope.$watch( 363*bd500cd2Sbeccabroek function() { 364*bd500cd2Sbeccabroek if (paginationService.isRegistered(paginationId)) { 365*bd500cd2Sbeccabroek return paginationService.getCurrentPage(paginationId); 366*bd500cd2Sbeccabroek } 367*bd500cd2Sbeccabroek }, 368*bd500cd2Sbeccabroek function(currentPage, previousPage) { 369*bd500cd2Sbeccabroek if (currentPage != previousPage) { 370*bd500cd2Sbeccabroek goToPage(currentPage); 371*bd500cd2Sbeccabroek } 372*bd500cd2Sbeccabroek }); 373*bd500cd2Sbeccabroek 374*bd500cd2Sbeccabroek scope.setCurrent = function(num) { 375*bd500cd2Sbeccabroek if (paginationService.isRegistered(paginationId) && 376*bd500cd2Sbeccabroek isValidPageNumber(num)) { 377*bd500cd2Sbeccabroek num = parseInt(num, 10); 378*bd500cd2Sbeccabroek paginationService.setCurrentPage(paginationId, num); 379*bd500cd2Sbeccabroek } 380*bd500cd2Sbeccabroek }; 381*bd500cd2Sbeccabroek 382*bd500cd2Sbeccabroek /** 383*bd500cd2Sbeccabroek * Custom "track by" function which allows for duplicate "..." entries on 384*bd500cd2Sbeccabroek * long lists, yet fixes the problem of wrongly-highlighted links which 385*bd500cd2Sbeccabroek * happens when using "track by $index" - see 386*bd500cd2Sbeccabroek * https://github.com/michaelbromley/angularUtils/issues/153 387*bd500cd2Sbeccabroek * @param id 388*bd500cd2Sbeccabroek * @param index 389*bd500cd2Sbeccabroek * @returns {string} 390*bd500cd2Sbeccabroek */ 391*bd500cd2Sbeccabroek scope.tracker = function(id, index) { 392*bd500cd2Sbeccabroek return id + '_' + index; 393*bd500cd2Sbeccabroek }; 394*bd500cd2Sbeccabroek 395*bd500cd2Sbeccabroek function goToPage(num) { 396*bd500cd2Sbeccabroek if (paginationService.isRegistered(paginationId) && 397*bd500cd2Sbeccabroek isValidPageNumber(num)) { 398*bd500cd2Sbeccabroek var oldPageNumber = scope.pagination.current; 399*bd500cd2Sbeccabroek 400*bd500cd2Sbeccabroek scope.pages = generatePagesArray( 401*bd500cd2Sbeccabroek num, paginationService.getCollectionLength(paginationId), 402*bd500cd2Sbeccabroek paginationService.getItemsPerPage(paginationId), paginationRange); 403*bd500cd2Sbeccabroek scope.pagination.current = num; 404*bd500cd2Sbeccabroek updateRangeValues(); 405*bd500cd2Sbeccabroek 406*bd500cd2Sbeccabroek // if a callback has been set, then call it with the page number as the 407*bd500cd2Sbeccabroek // first argument and the previous page number as a second argument 408*bd500cd2Sbeccabroek if (scope.onPageChange) { 409*bd500cd2Sbeccabroek scope.onPageChange( 410*bd500cd2Sbeccabroek {newPageNumber: num, oldPageNumber: oldPageNumber}); 411*bd500cd2Sbeccabroek } 412*bd500cd2Sbeccabroek } 413*bd500cd2Sbeccabroek } 414*bd500cd2Sbeccabroek 415*bd500cd2Sbeccabroek function generatePagination() { 416*bd500cd2Sbeccabroek if (paginationService.isRegistered(paginationId)) { 417*bd500cd2Sbeccabroek var page = 418*bd500cd2Sbeccabroek parseInt(paginationService.getCurrentPage(paginationId)) || 1; 419*bd500cd2Sbeccabroek scope.pages = generatePagesArray( 420*bd500cd2Sbeccabroek page, paginationService.getCollectionLength(paginationId), 421*bd500cd2Sbeccabroek paginationService.getItemsPerPage(paginationId), paginationRange); 422*bd500cd2Sbeccabroek scope.pagination.current = page; 423*bd500cd2Sbeccabroek scope.pagination.last = scope.pages[scope.pages.length - 1]; 424*bd500cd2Sbeccabroek if (scope.pagination.last < scope.pagination.current) { 425*bd500cd2Sbeccabroek scope.setCurrent(scope.pagination.last); 426*bd500cd2Sbeccabroek } else { 427*bd500cd2Sbeccabroek updateRangeValues(); 428*bd500cd2Sbeccabroek } 429*bd500cd2Sbeccabroek } 430*bd500cd2Sbeccabroek } 431*bd500cd2Sbeccabroek 432*bd500cd2Sbeccabroek /** 433*bd500cd2Sbeccabroek * This function updates the values (lower, upper, total) of the 434*bd500cd2Sbeccabroek * `scope.range` object, which can be used in the pagination template to 435*bd500cd2Sbeccabroek * display the current page range, e.g. "showing 21 - 40 of 144 results"; 436*bd500cd2Sbeccabroek */ 437*bd500cd2Sbeccabroek function updateRangeValues() { 438*bd500cd2Sbeccabroek if (paginationService.isRegistered(paginationId)) { 439*bd500cd2Sbeccabroek var currentPage = paginationService.getCurrentPage(paginationId), 440*bd500cd2Sbeccabroek itemsPerPage = paginationService.getItemsPerPage(paginationId), 441*bd500cd2Sbeccabroek totalItems = paginationService.getCollectionLength(paginationId); 442*bd500cd2Sbeccabroek 443*bd500cd2Sbeccabroek scope.range.lower = (currentPage - 1) * itemsPerPage + 1; 444*bd500cd2Sbeccabroek scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems); 445*bd500cd2Sbeccabroek scope.range.total = totalItems; 446*bd500cd2Sbeccabroek } 447*bd500cd2Sbeccabroek } 448*bd500cd2Sbeccabroek function isValidPageNumber(num) { 449*bd500cd2Sbeccabroek return ( 450*bd500cd2Sbeccabroek numberRegex.test(num) && (0 < num && num <= scope.pagination.last)); 451*bd500cd2Sbeccabroek } 452*bd500cd2Sbeccabroek } 453*bd500cd2Sbeccabroek 454*bd500cd2Sbeccabroek /** 455*bd500cd2Sbeccabroek * Generate an array of page numbers (or the '...' string) which is used in an 456*bd500cd2Sbeccabroek * ng-repeat to generate the links used in pagination 457*bd500cd2Sbeccabroek * 458*bd500cd2Sbeccabroek * @param currentPage 459*bd500cd2Sbeccabroek * @param rowsPerPage 460*bd500cd2Sbeccabroek * @param paginationRange 461*bd500cd2Sbeccabroek * @param collectionLength 462*bd500cd2Sbeccabroek * @returns {Array} 463*bd500cd2Sbeccabroek */ 464*bd500cd2Sbeccabroek function generatePagesArray( 465*bd500cd2Sbeccabroek currentPage, collectionLength, rowsPerPage, paginationRange) { 466*bd500cd2Sbeccabroek var pages = []; 467*bd500cd2Sbeccabroek var totalPages = Math.ceil(collectionLength / rowsPerPage); 468*bd500cd2Sbeccabroek var halfWay = Math.ceil(paginationRange / 2); 469*bd500cd2Sbeccabroek var position; 470*bd500cd2Sbeccabroek 471*bd500cd2Sbeccabroek if (currentPage <= halfWay) { 472*bd500cd2Sbeccabroek position = 'start'; 473*bd500cd2Sbeccabroek } else if (totalPages - halfWay < currentPage) { 474*bd500cd2Sbeccabroek position = 'end'; 475*bd500cd2Sbeccabroek } else { 476*bd500cd2Sbeccabroek position = 'middle'; 477*bd500cd2Sbeccabroek } 478*bd500cd2Sbeccabroek 479*bd500cd2Sbeccabroek var ellipsesNeeded = paginationRange < totalPages; 480*bd500cd2Sbeccabroek var i = 1; 481*bd500cd2Sbeccabroek while (i <= totalPages && i <= paginationRange) { 482*bd500cd2Sbeccabroek var pageNumber = 483*bd500cd2Sbeccabroek calculatePageNumber(i, currentPage, paginationRange, totalPages); 484*bd500cd2Sbeccabroek 485*bd500cd2Sbeccabroek var openingEllipsesNeeded = 486*bd500cd2Sbeccabroek (i === 2 && (position === 'middle' || position === 'end')); 487*bd500cd2Sbeccabroek var closingEllipsesNeeded = 488*bd500cd2Sbeccabroek (i === paginationRange - 1 && 489*bd500cd2Sbeccabroek (position === 'middle' || position === 'start')); 490*bd500cd2Sbeccabroek if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) { 491*bd500cd2Sbeccabroek pages.push('...'); 492*bd500cd2Sbeccabroek } else { 493*bd500cd2Sbeccabroek pages.push(pageNumber); 494*bd500cd2Sbeccabroek } 495*bd500cd2Sbeccabroek i++; 496*bd500cd2Sbeccabroek } 497*bd500cd2Sbeccabroek return pages; 498*bd500cd2Sbeccabroek } 499*bd500cd2Sbeccabroek 500*bd500cd2Sbeccabroek /** 501*bd500cd2Sbeccabroek * Given the position in the sequence of pagination links [i], figure out what 502*bd500cd2Sbeccabroek * page number corresponds to that position. 503*bd500cd2Sbeccabroek * 504*bd500cd2Sbeccabroek * @param i 505*bd500cd2Sbeccabroek * @param currentPage 506*bd500cd2Sbeccabroek * @param paginationRange 507*bd500cd2Sbeccabroek * @param totalPages 508*bd500cd2Sbeccabroek * @returns {*} 509*bd500cd2Sbeccabroek */ 510*bd500cd2Sbeccabroek function calculatePageNumber(i, currentPage, paginationRange, totalPages) { 511*bd500cd2Sbeccabroek var halfWay = Math.ceil(paginationRange / 2); 512*bd500cd2Sbeccabroek if (i === paginationRange) { 513*bd500cd2Sbeccabroek return totalPages; 514*bd500cd2Sbeccabroek } else if (i === 1) { 515*bd500cd2Sbeccabroek return i; 516*bd500cd2Sbeccabroek } else if (paginationRange < totalPages) { 517*bd500cd2Sbeccabroek if (totalPages - halfWay < currentPage) { 518*bd500cd2Sbeccabroek return totalPages - paginationRange + i; 519*bd500cd2Sbeccabroek } else if (halfWay < currentPage) { 520*bd500cd2Sbeccabroek return currentPage - halfWay + i; 521*bd500cd2Sbeccabroek } else { 522*bd500cd2Sbeccabroek return i; 523*bd500cd2Sbeccabroek } 524*bd500cd2Sbeccabroek } else { 525*bd500cd2Sbeccabroek return i; 526*bd500cd2Sbeccabroek } 527*bd500cd2Sbeccabroek } 528*bd500cd2Sbeccabroek} 529*bd500cd2Sbeccabroek 530*bd500cd2Sbeccabroek/** 531*bd500cd2Sbeccabroek * This filter slices the collection into pages based on the current page number 532*bd500cd2Sbeccabroek * and number of items per page. 533*bd500cd2Sbeccabroek * @param paginationService 534*bd500cd2Sbeccabroek * @returns {Function} 535*bd500cd2Sbeccabroek */ 536*bd500cd2Sbeccabroekfunction itemsPerPageFilter(paginationService) { 537*bd500cd2Sbeccabroek return function(collection, itemsPerPage, paginationId) { 538*bd500cd2Sbeccabroek if (typeof (paginationId) === 'undefined') { 539*bd500cd2Sbeccabroek paginationId = DEFAULT_ID; 540*bd500cd2Sbeccabroek } 541*bd500cd2Sbeccabroek if (!paginationService.isRegistered(paginationId)) { 542*bd500cd2Sbeccabroek throw 'pagination directive: the itemsPerPage id argument (id: ' + 543*bd500cd2Sbeccabroek paginationId + ') does not match a registered pagination-id.'; 544*bd500cd2Sbeccabroek } 545*bd500cd2Sbeccabroek var end; 546*bd500cd2Sbeccabroek var start; 547*bd500cd2Sbeccabroek if (angular.isObject(collection)) { 548*bd500cd2Sbeccabroek itemsPerPage = parseInt(itemsPerPage) || 9999999999; 549*bd500cd2Sbeccabroek if (paginationService.isAsyncMode(paginationId)) { 550*bd500cd2Sbeccabroek start = 0; 551*bd500cd2Sbeccabroek } else { 552*bd500cd2Sbeccabroek start = 553*bd500cd2Sbeccabroek (paginationService.getCurrentPage(paginationId) - 1) * itemsPerPage; 554*bd500cd2Sbeccabroek } 555*bd500cd2Sbeccabroek end = start + itemsPerPage; 556*bd500cd2Sbeccabroek paginationService.setItemsPerPage(paginationId, itemsPerPage); 557*bd500cd2Sbeccabroek 558*bd500cd2Sbeccabroek if (collection instanceof Array) { 559*bd500cd2Sbeccabroek // the array just needs to be sliced 560*bd500cd2Sbeccabroek return collection.slice(start, end); 561*bd500cd2Sbeccabroek } else { 562*bd500cd2Sbeccabroek // in the case of an object, we need to get an array of keys, slice 563*bd500cd2Sbeccabroek // that, then map back to the original object. 564*bd500cd2Sbeccabroek var slicedObject = {}; 565*bd500cd2Sbeccabroek angular.forEach(keys(collection).slice(start, end), function(key) { 566*bd500cd2Sbeccabroek slicedObject[key] = collection[key]; 567*bd500cd2Sbeccabroek }); 568*bd500cd2Sbeccabroek return slicedObject; 569*bd500cd2Sbeccabroek } 570*bd500cd2Sbeccabroek } else { 571*bd500cd2Sbeccabroek return collection; 572*bd500cd2Sbeccabroek } 573*bd500cd2Sbeccabroek }; 574*bd500cd2Sbeccabroek} 575*bd500cd2Sbeccabroek 576*bd500cd2Sbeccabroek/** 577*bd500cd2Sbeccabroek * Shim for the Object.keys() method which does not exist in IE < 9 578*bd500cd2Sbeccabroek * @param obj 579*bd500cd2Sbeccabroek * @returns {Array} 580*bd500cd2Sbeccabroek */ 581*bd500cd2Sbeccabroekfunction keys(obj) { 582*bd500cd2Sbeccabroek if (!Object.keys) { 583*bd500cd2Sbeccabroek var objKeys = []; 584*bd500cd2Sbeccabroek for (var i in obj) { 585*bd500cd2Sbeccabroek if (obj.hasOwnProperty(i)) { 586*bd500cd2Sbeccabroek objKeys.push(i); 587*bd500cd2Sbeccabroek } 588*bd500cd2Sbeccabroek } 589*bd500cd2Sbeccabroek return objKeys; 590*bd500cd2Sbeccabroek } else { 591*bd500cd2Sbeccabroek return Object.keys(obj); 592*bd500cd2Sbeccabroek } 593*bd500cd2Sbeccabroek} 594*bd500cd2Sbeccabroek 595*bd500cd2Sbeccabroek/** 596*bd500cd2Sbeccabroek * This service allows the various parts of the module to communicate and stay 597*bd500cd2Sbeccabroek * in sync. 598*bd500cd2Sbeccabroek */ 599*bd500cd2Sbeccabroekfunction paginationService() { 600*bd500cd2Sbeccabroek var instances = {}; 601*bd500cd2Sbeccabroek var lastRegisteredInstance; 602*bd500cd2Sbeccabroek 603*bd500cd2Sbeccabroek this.registerInstance = function(instanceId) { 604*bd500cd2Sbeccabroek if (typeof instances[instanceId] === 'undefined') { 605*bd500cd2Sbeccabroek instances[instanceId] = {asyncMode: false}; 606*bd500cd2Sbeccabroek lastRegisteredInstance = instanceId; 607*bd500cd2Sbeccabroek } 608*bd500cd2Sbeccabroek }; 609*bd500cd2Sbeccabroek 610*bd500cd2Sbeccabroek this.deregisterInstance = function(instanceId) { 611*bd500cd2Sbeccabroek delete instances[instanceId]; 612*bd500cd2Sbeccabroek }; 613*bd500cd2Sbeccabroek 614*bd500cd2Sbeccabroek this.isRegistered = function(instanceId) { 615*bd500cd2Sbeccabroek return (typeof instances[instanceId] !== 'undefined'); 616*bd500cd2Sbeccabroek }; 617*bd500cd2Sbeccabroek 618*bd500cd2Sbeccabroek this.getLastInstanceId = function() { 619*bd500cd2Sbeccabroek return lastRegisteredInstance; 620*bd500cd2Sbeccabroek }; 621*bd500cd2Sbeccabroek 622*bd500cd2Sbeccabroek this.setCurrentPageParser = function(instanceId, val, scope) { 623*bd500cd2Sbeccabroek instances[instanceId].currentPageParser = val; 624*bd500cd2Sbeccabroek instances[instanceId].context = scope; 625*bd500cd2Sbeccabroek }; 626*bd500cd2Sbeccabroek this.setCurrentPage = function(instanceId, val) { 627*bd500cd2Sbeccabroek instances[instanceId].currentPageParser.assign( 628*bd500cd2Sbeccabroek instances[instanceId].context, val); 629*bd500cd2Sbeccabroek }; 630*bd500cd2Sbeccabroek this.getCurrentPage = function(instanceId) { 631*bd500cd2Sbeccabroek var parser = instances[instanceId].currentPageParser; 632*bd500cd2Sbeccabroek return parser ? parser(instances[instanceId].context) : 1; 633*bd500cd2Sbeccabroek }; 634*bd500cd2Sbeccabroek 635*bd500cd2Sbeccabroek this.setItemsPerPage = function(instanceId, val) { 636*bd500cd2Sbeccabroek instances[instanceId].itemsPerPage = val; 637*bd500cd2Sbeccabroek }; 638*bd500cd2Sbeccabroek this.getItemsPerPage = function(instanceId) { 639*bd500cd2Sbeccabroek return instances[instanceId].itemsPerPage; 640*bd500cd2Sbeccabroek }; 641*bd500cd2Sbeccabroek 642*bd500cd2Sbeccabroek this.setCollectionLength = function(instanceId, val) { 643*bd500cd2Sbeccabroek instances[instanceId].collectionLength = val; 644*bd500cd2Sbeccabroek }; 645*bd500cd2Sbeccabroek this.getCollectionLength = function(instanceId) { 646*bd500cd2Sbeccabroek return instances[instanceId].collectionLength; 647*bd500cd2Sbeccabroek }; 648*bd500cd2Sbeccabroek 649*bd500cd2Sbeccabroek this.setAsyncModeTrue = function(instanceId) { 650*bd500cd2Sbeccabroek instances[instanceId].asyncMode = true; 651*bd500cd2Sbeccabroek }; 652*bd500cd2Sbeccabroek 653*bd500cd2Sbeccabroek this.setAsyncModeFalse = function(instanceId) { 654*bd500cd2Sbeccabroek instances[instanceId].asyncMode = false; 655*bd500cd2Sbeccabroek }; 656*bd500cd2Sbeccabroek 657*bd500cd2Sbeccabroek this.isAsyncMode = function(instanceId) { 658*bd500cd2Sbeccabroek return instances[instanceId].asyncMode; 659*bd500cd2Sbeccabroek }; 660*bd500cd2Sbeccabroek} 661*bd500cd2Sbeccabroek 662*bd500cd2Sbeccabroek/** 663*bd500cd2Sbeccabroek * This provider allows global configuration of the template path used by the 664*bd500cd2Sbeccabroek * dir-pagination-controls directive. 665*bd500cd2Sbeccabroek */ 666*bd500cd2Sbeccabroekfunction paginationTemplateProvider() { 667*bd500cd2Sbeccabroek var templatePath = 'app.common.directives.dirPagination.template'; 668*bd500cd2Sbeccabroek var templateString; 669*bd500cd2Sbeccabroek 670*bd500cd2Sbeccabroek /** 671*bd500cd2Sbeccabroek * Set a templateUrl to be used by all instances of <dir-pagination-controls> 672*bd500cd2Sbeccabroek * @param {String} path 673*bd500cd2Sbeccabroek */ 674*bd500cd2Sbeccabroek this.setPath = function(path) { 675*bd500cd2Sbeccabroek templatePath = path; 676*bd500cd2Sbeccabroek }; 677*bd500cd2Sbeccabroek 678*bd500cd2Sbeccabroek /** 679*bd500cd2Sbeccabroek * Set a string of HTML to be used as a template by all instances 680*bd500cd2Sbeccabroek * of <dir-pagination-controls>. If both a path *and* a string have been set, 681*bd500cd2Sbeccabroek * the string takes precedence. 682*bd500cd2Sbeccabroek * @param {String} str 683*bd500cd2Sbeccabroek */ 684*bd500cd2Sbeccabroek this.setString = function(str) { 685*bd500cd2Sbeccabroek templateString = str; 686*bd500cd2Sbeccabroek }; 687*bd500cd2Sbeccabroek 688*bd500cd2Sbeccabroek this.$get = function() { 689*bd500cd2Sbeccabroek return { 690*bd500cd2Sbeccabroek getPath: function() { 691*bd500cd2Sbeccabroek return templatePath; 692*bd500cd2Sbeccabroek }, 693*bd500cd2Sbeccabroek getString: function() { 694*bd500cd2Sbeccabroek return templateString; 695*bd500cd2Sbeccabroek } 696*bd500cd2Sbeccabroek }; 697*bd500cd2Sbeccabroek }; 698*bd500cd2Sbeccabroek} 699*bd500cd2Sbeccabroek})(); 700