1/** 2 * dirPagination - AngularJS module for paginating (almost) anything. 3 * 4 * 5 * Credits 6 * ======= 7 * 8 * Daniel Tabuenca: https://groups.google.com/d/msg/angular/an9QpzqIYiM/r8v-3W1X5vcJ 9 * for the idea on how to dynamically invoke the ng-repeat directive. 10 * 11 * I borrowed a couple of lines and a few attribute names from the AngularUI Bootstrap project: 12 * https://github.com/angular-ui/bootstrap/blob/master/src/pagination/pagination.js 13 * 14 * Copyright 2014 Michael Bromley <[email protected]> 15 */ 16 17(function() { 18 19 var moduleName = 'dirPagination'; 20 var DEFAULT_ID = '__default'; 21 22 /** Module */ 23 var module; 24 try { 25 module = angular.module(moduleName); 26 } catch(err) { 27 module = angular.module(moduleName, []); // named module does not exist, so create one 28 } 29 30 module 31 .directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective]) 32 .directive('dirPaginateNoCompile', noCompileDirective) 33 .directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective]) 34 .filter('itemsPerPage', ['paginationService', itemsPerPageFilter]) 35 .service('paginationService', paginationService) 36 .provider('paginationTemplate', paginationTemplateProvider) 37 .run(['$templateCache',dirPaginationControlsTemplateInstaller]); 38 39 function dirPaginateDirective($compile, $parse, paginationService) { 40 41 return { 42 terminal: true, 43 multiElement: true, 44 compile: dirPaginationCompileFn 45 }; 46
47 function dirPaginationCompileFn(tElement, tAttrs){ 48 49 var expression = tAttrs.dirPaginate; 50 // regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211 51 var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); 52 53 var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/; 54 if (match[2].match(filterPattern) === null) { 55 throw 'pagination directive: the \'itemsPerPage\' filter must be set.'; 56 } 57 var itemsPerPageFilterRemoved = match[2].replace(filterPattern, ''); 58 var collectionGetter = $parse(itemsPerPageFilterRemoved); 59 60 addNoCompileAttributes(tElement); 61 62 // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any 63 // dir-pagination-controls directives that may be looking for this ID. 64 var rawId = tAttrs.paginationId || DEFAULT_ID; 65 paginationService.registerInstance(rawId); 66 67 return function dirPaginationLinkFn(scope, element, attrs){ 68 69 // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and 70 // potentially register a new ID if it evaluates to a different value than the rawId. 71 var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID; 72 paginationService.registerInstance(paginationId); 73 74 var repeatExpression = getRepeatExpression(expression, paginationId); 75 addNgRepeatToElement(element, attrs, repeatExpression); 76 77 removeTemporaryAttributes(element); 78 var compiled = $compile(element); 79 80 var currentPageGetter = makeCurrentPageGetterFn(scope, attrs, paginationId); 81 paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope); 82 83 if (typeof attrs.totalItems !== 'undefined') { 84 paginationService.setAsyncModeTrue(paginationId); 85 scope.$watch(function() { 86 return $parse(attrs.totalItems)(scope); 87 }, function (result) { 88 if (0 <= result) { 89 paginationService.setCollectionLength(paginationId, result); 90 } 91 }); 92 } else { 93 scope.$watchCollection(function() { 94 return collectionGetter(scope); 95 }, function(collection) { 96 if (collection) { 97 paginationService.setCollectionLength(paginationId, collection.length); 98 } 99 }); 100 } 101 102 // Delegate to the link function returned by the new compilation of the ng-repeat 103 compiled(scope); 104 }; 105 } 106 107 /** 108 * If a pagination id has been specified, we need to check that it is present as the second argument passed to 109 * the itemsPerPage filter. If it is not there, we add it and return the modified expression. 110 * 111 * @param expression 112 * @param paginationId 113 * @returns {*} 114 */ 115 function getRepeatExpression(expression, paginationId) { 116 var repeatExpression, 117 idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/); 118 119 if (paginationId !== DEFAULT_ID && !idDefinedInFilter) { 120 repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:[^|]*)/, "$1 : '" + paginationId + "'"); 121 } else { 122 repeatExpression = expression; 123 } 124 125 return repeatExpression; 126 } 127 128 /** 129 * Adds the ng-repeat directive to the element. In the case of multi-element (-start, -end) it adds the 130 * appropriate multi-element ng-repeat to the first and last element in the range. 131 * @param element 132 * @param attrs 133 * @param repeatExpression 134 */ 135 function addNgRepeatToElement(element, attrs, repeatExpression) { 136 if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) { 137 // using multiElement mode (dir-paginate-start, dir-paginate-end) 138 attrs.$set('ngRepeatStart', repeatExpression); 139 element.eq(element.length - 1).attr('ng-repeat-end', true); 140 } else { 141 attrs.$set('ngRepeat', repeatExpression); 142 } 143 } 144 145 /** 146 * Adds the dir-paginate-no-compile directive to each element in the tElement range. 147 * @param tElement 148 */ 149 function addNoCompileAttributes(tElement) { 150 angular.forEach(tElement, function(el) {
151 if (el.nodeType === Node.ELEMENT_NODE) { 152 angular.element(el).attr('dir-paginate-no-compile', true); 153 } 154 }); 155 } 156 157 /** 158 * Removes the variations on dir-paginate (data-, -start, -end) and the dir-paginate-no-compile directives. 159 * @param element 160 */ 161 function removeTemporaryAttributes(element) { 162 angular.forEach(element, function(el) { 163 if (el.nodeType === Node.ELEMENT_NODE) { 164 angular.element(el).removeAttr('dir-paginate-no-compile'); 165 } 166 }); 167 element.eq(0).removeAttr('dir-paginate-start').removeAttr('dir-paginate').removeAttr('data-dir-paginate-start').removeAttr('data-dir-paginate'); 168 element.eq(element.length - 1).removeAttr('dir-paginate-end').removeAttr('data-dir-paginate-end'); 169 } 170 171 /** 172 * Creates a getter function for the current-page attribute, using the expression provided or a default value if 173 * no current-page expression was specified. 174 * 175 * @param scope 176 * @param attrs 177 * @param paginationId 178 * @returns {*} 179 */ 180 function makeCurrentPageGetterFn(scope, attrs, paginationId) { 181 var currentPageGetter; 182 if (attrs.currentPage) { 183 currentPageGetter = $parse(attrs.currentPage); 184 } else { 185 // if the current-page attribute was not set, we'll make our own 186 var defaultCurrentPage = paginationId + '__currentPage'; 187 scope[defaultCurrentPage] = 1; 188 currentPageGetter = $parse(defaultCurrentPage); 189 } 190 return currentPageGetter; 191 } 192 } 193 194 /** 195 * This is a helper directive that allows correct compilation when in multi-element mode (ie dir-paginate-start, dir-paginate-end). 196 * It is dynamically added to all elements in the dir-paginate compile function, and it prevents further compilation of 197 * any inner directives. It is then removed in the link function, and all inner directives are then manually compiled. 198 */ 199 function noCompileDirective() { 200 return { 201 priority: 5000, 202 terminal: true 203 }; 204 } 205 206 function dirPaginationControlsTemplateInstaller($templateCache) { 207 $templateCache.put('dirPagination.template', '<ul class="pagination" ng-if="1 < pages.length"><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 $index" ng-class="{ active : pagination.current == pageNumber, disabled : pageNumber == \'...\' }"><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>'); 208 } 209 210 function dirPaginationControlsDirective(paginationService, paginationTemplate) { 211 212 var numberRegex = /^\d+$/; 213 214 return { 215 restrict: 'AE', 216 templateUrl: function(elem, attrs) { 217 return attrs.templateUrl || paginationTemplate.getPath(); 218 }, 219 scope: { 220 maxSize: '=?', 221 onPageChange: '&?', 222 paginationId: '=?' 223 }, 224 link: dirPaginationControlsLinkFn 225 }; 226 227 function dirPaginationControlsLinkFn(scope, element, attrs) { 228 229 // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has 230 // not yet been linked (e.g. if it is inside an ng-if block), and in that case it prevents this controls directive from assuming that there is 231 // no corresponding dir-paginate directive and wrongly throwing an exception. 232 var rawId = attrs.paginationId || DEFAULT_ID; 233 var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID; 234 235 if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) { 236 var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' '; 237 throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.'; 238 } 239 240 if (!scope.maxSize) { scope.maxSize = 9; } 241 scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true; 242 scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false; 243 244 var paginationRange = Math.max(scope.maxSize, 5); 245 scope.pages = []; 246 scope.pagination = { 247 last: 1, 248 current: 1 249 }; 250 scope.range = { 251 lower: 1, 252 upper: 1, 253 total: 1 254 }; 255 256 scope.$watch(function() { 257 return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId); 258 }, function(length) { 259 if (0 < length) { 260 generatePagination(); 261 } 262 }); 263 264 scope.$watch(function() { 265 return (paginationService.getItemsPerPage(paginationId)); 266 }, function(current, previous) { 267 if (current != previous && typeof previous !== 'undefined') { 268 goToPage(scope.pagination.current); 269 } 270 }); 271 272 scope.$watch(function() { 273 return paginationService.getCurrentPage(paginationId); 274 }, function(currentPage, previousPage) { 275 if (currentPage != previousPage) { 276 goToPage(currentPage); 277 } 278 }); 279 280 scope.setCurrent = function(num) { 281 if (isValidPageNumber(num)) { 282 num = parseInt(num, 10); 283 paginationService.setCurrentPage(paginationId, num); 284 } 285 }; 286 287 function goToPage(num) { 288 if (isValidPageNumber(num)) { 289 scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); 290 scope.pagination.current = num; 291 updateRangeValues();
292 293 // if a callback has been set, then call it with the page number as an argument 294 if (scope.onPageChange) { 295 scope.onPageChange({ newPageNumber : num }); 296 } 297 } 298 } 299 300 function generatePagination() { 301 var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1; 302 303 scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); 304 scope.pagination.current = page; 305 scope.pagination.last = scope.pages[scope.pages.length - 1]; 306 if (scope.pagination.last < scope.pagination.current) { 307 scope.setCurrent(scope.pagination.last); 308 } else { 309 updateRangeValues(); 310 } 311 } 312 313 /** 314 * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination 315 * template to display the current page range, e.g. "showing 21 - 40 of 144 results"; 316 */ 317 function updateRangeValues() { 318 var currentPage = paginationService.getCurrentPage(paginationId), 319 itemsPerPage = paginationService.getItemsPerPage(paginationId), 320 totalItems = paginationService.getCollectionLength(paginationId); 321 322 scope.range.lower = (currentPage - 1) * itemsPerPage + 1; 323 scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems); 324 scope.range.total = totalItems; 325 } 326 327 function isValidPageNumber(num) { 328 return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last)); 329 } 330 } 331 332 /** 333 * Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the 334 * links used in pagination 335 * 336 * @param currentPage 337 * @param rowsPerPage 338 * @param paginationRange 339 * @param collectionLength 340 * @returns {Array} 341 */ 342 function generatePagesArray(currentPage, collectionLength, rowsPerPage, paginationRange) { 343 var pages = []; 344 var totalPages = Math.ceil(collectionLength / rowsPerPage); 345 var halfWay = Math.ceil(paginationRange / 2); 346 var position; 347 348 if (currentPage <= halfWay) { 349 position = 'start'; 350 } else if (totalPages - halfWay < currentPage) { 351 position = 'end'; 352 } else { 353 position = 'middle'; 354 } 355 356 var ellipsesNeeded = paginationRange < totalPages; 357 var i = 1; 358 while (i <= totalPages && i <= paginationRange) { 359 var pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages); 360 361 var openingEllipsesNeeded = (i === 2 && (position === 'middle' || position === 'end')); 362 var closingEllipsesNeeded = (i === paginationRange - 1 && (position === 'middle' || position === 'start')); 363 if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) { 364 pages.push('...'); 365 } else { 366 pages.push(pageNumber); 367 } 368 i ++; 369 } 370 return pages; 371 } 372 373 /** 374 * Given the position in the sequence of pagination links [i], figure out what page number corresponds to that position. 375 * 376 * @param i 377 * @param currentPage 378 * @param paginationRange 379 * @param totalPages 380 * @returns {*} 381 */ 382 function calculatePageNumber(i, currentPage, paginationRange, totalPages) { 383 var halfWay = Math.ceil(paginationRange/2); 384 if (i === paginationRange) { 385 return totalPages; 386 } else if (i === 1) { 387 return i; 388 } else if (paginationRange < totalPages) { 389 if (totalPages - halfWay < currentPage) { 390 return totalPages - paginationRange + i; 391 } else if (halfWay < currentPage) { 392 return currentPage - halfWay + i; 393 } else { 394 return i; 395 } 396 } else { 397 return i; 398 } 399 } 400 } 401 402 /** 403 * This filter slices the collection into pages based on the current page number and number of items per page. 404 * @param paginationService 405 * @returns {Function} 406 */ 407 function itemsPerPageFilter(paginationService) { 408 409 return function(collection, itemsPerPage, paginationId) { 410 if (typeof (paginationId) === 'undefined') { 411 paginationId = DEFAULT_ID; 412 } 413 if (!paginationService.isRegistered(paginationId)) { 414 throw 'pagination directive: the itemsPerPage id argument (id: ' + paginationId + ') does not match a registered pagination-id.'; 415 } 416 var end; 417 var start; 418 if (collection instanceof Array) { 419 itemsPerPage = parseInt(itemsPerPage) || 9999999999; 420 if (paginationService.isAsyncMode(paginationId)) { 421 start = 0; 422 } else { 423 start = (paginationService.getCurrentPage(paginationId) - 1) * itemsPerPage; 424 } 425 end = start + itemsPerPage; 426 paginationService.setItemsPerPage(paginationId, itemsPerPage); 427 428 return collection.slice(start, end); 429 } else { 430 return collection; 431 } 432 }; 433 } 434 435 /** 436 * This service allows the various parts of the module to communicate and stay in sync. 437 */ 438 function paginationService() { 439 440 var instances = {}; 441 var lastRegisteredInstance; 442 443 this.registerInstance = function(instanceId) { 444 if (typeof instances[instanceId] === 'undefined') { 445 instances[instanceId] = { 446 asyncMode: false 447 }; 448 lastRegisteredInstance = instanceId; 449 } 450 }; 451 452 this.isRegistered = function(instanceId) { 453 return (typeof instances[instanceId] !== 'undefined'); 454 }; 455 456 this.getLastInstanceId = function() { 457 return lastRegisteredInstance; 458 }; 459 460 this.setCurrentPageParser = function(instanceId, val, scope) { 461 instances[instanceId].currentPageParser = val; 462 instances[instanceId].context = scope; 463 }; 464 this.setCurrentPage = function(instanceId, val) { 465 instances[instanceId].currentPageParser.assign(instances[instanceId].context, val); 466 }; 467 this.getCurrentPage = function(instanceId) { 468 var parser = instances[instanceId].currentPageParser; 469 return parser ? parser(instances[instanceId].context) : 1; 470 }; 471 472 this.setItemsPerPage = function(instanceId, val) { 473 instances[instanceId].itemsPerPage = val; 474 }; 475 this.getItemsPerPage = function(instanceId) { 476 return instances[instanceId].itemsPerPage; 477 }; 478 479 this.setCollectionLength = function(instanceId, val) { 480 instances[instanceId].collectionLength = val; 481 }; 482 this.getCollectionLength = function(instanceId) { 483 return instances[instanceId].collectionLength; 484 }; 485 486 this.setAsyncModeTrue = function(instanceId) { 487 instances[instanceId].asyncMode = true; 488 }; 489 490 this.isAsyncMode = function(instanceId) { 491 return instances[instanceId].asyncMode; 492 }; 493 } 494 495 /** 496 * This provider allows global configuration of the template path used by the dir-pagination-controls directive. 497 */ 498 function paginationTemplateProvider() { 499 500 var templatePath = 'dirPagination.template'; 501 502 this.setPath = function(path) { 503 templatePath = path; 504 }; 505 506 this.$get = function() { 507 return { 508 getPath: function() { 509 return templatePath; 510 } 511 }; 512 }; 513 } 514})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.