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