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