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