From 125b153636f8591cf0e782569a0d58f3f2ef74d4 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 20 Mar 2015 19:44:29 +0100 Subject: [PATCH] Update to v0.6.0 See release notes --- bower.json | 2 +- dirPagination.js | 501 ++++++++++++++++++++++++++++------------------- 2 files changed, 302 insertions(+), 201 deletions(-) diff --git a/bower.json b/bower.json index ec1888a..630214a 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "angularUtils-pagination", - "version": "0.5.1", + "version": "0.6.0", "homepage": "https://github.com/michaelbromley/angularUtils/tree/master/src/directives/pagination", "authors": [ "Michael Bromley " diff --git a/dirPagination.js b/dirPagination.js index 5aa6efb..552d783 100644 --- a/dirPagination.js +++ b/dirPagination.js @@ -33,97 +33,302 @@ module = angular.module(moduleName, []); } - module.directive('dirPaginate', ['$compile', '$parse', 'paginationService', function($compile, $parse, paginationService) { + module + .directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective]) + .directive('dirPaginateNoCompile', noCompileDirective) + .directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective]) + .filter('itemsPerPage', ['paginationService', itemsPerPageFilter]) + .service('paginationService', paginationService) + .provider('paginationTemplate', paginationTemplateProvider); + + function dirPaginateDirective($compile, $parse, paginationService) { return { terminal: true, multiElement: true, - priority: 5000, // This setting is used in conjunction with the later call to $compile() to prevent infinite recursion of compilation - compile: function dirPaginationCompileFn(tElement, tAttrs){ + compile: dirPaginationCompileFn + }; + + function dirPaginationCompileFn(tElement, tAttrs){ + + var expression = tAttrs.dirPaginate; + // regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211 + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + + var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/; + if (match[2].match(filterPattern) === null) { + throw 'pagination directive: the \'itemsPerPage\' filter must be set.'; + } + var itemsPerPageFilterRemoved = match[2].replace(filterPattern, ''); + var collectionGetter = $parse(itemsPerPageFilterRemoved); + + addNoCompileAttributes(tElement); + + // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any + // dir-pagination-controls directives that may be looking for this ID. + var rawId = tAttrs.paginationId || DEFAULT_ID; + paginationService.registerInstance(rawId); + + return function dirPaginationLinkFn(scope, element, attrs){ + + // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and + // potentially register a new ID if it evaluates to a different value than the rawId. + var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID; + paginationService.registerInstance(paginationId); - var expression = tAttrs.dirPaginate; - // regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211 - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + var repeatExpression = getRepeatExpression(expression, paginationId); + addNgRepeatToElement(element, attrs, repeatExpression); - var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/; - if (match[2].match(filterPattern) === null) { - throw 'pagination directive: the \'itemsPerPage\' filter must be set.'; + removeTemporaryAttributes(element); + var compiled = $compile(element); + + var currentPageGetter = makeCurrentPageGetterFn(scope, attrs, paginationId); + paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope); + + if (typeof attrs.totalItems !== 'undefined') { + paginationService.setAsyncModeTrue(paginationId); + scope.$watch(function() { + return $parse(attrs.totalItems)(scope); + }, function (result) { + if (0 <= result) { + paginationService.setCollectionLength(paginationId, result); + } + }); + } else { + scope.$watchCollection(function() { + return collectionGetter(scope); + }, function(collection) { + if (collection) { + paginationService.setCollectionLength(paginationId, collection.length); + } + }); } - var itemsPerPageFilterRemoved = match[2].replace(filterPattern, ''); - var collectionGetter = $parse(itemsPerPageFilterRemoved); - - // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any - // dir-pagination-controls directives that may be looking for this ID. - var rawId = tAttrs.paginationId || DEFAULT_ID; - paginationService.registerInstance(rawId); - - return function dirPaginationLinkFn(scope, element, attrs){ - - // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and - // potentially register a new ID if it evaluates to a different value than the rawId. - var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID; - paginationService.registerInstance(paginationId); - - var repeatExpression; - var idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/); - if (paginationId !== DEFAULT_ID && !idDefinedInFilter) { - repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:[^|]*)/, "$1 : '" + paginationId + "'"); - } else { - repeatExpression = expression; - } - // Add ng-repeat to the dom element - if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) { - // using multiElement mode (dir-paginate-start, dir-paginate-end) - attrs.$set('ngRepeatStart', repeatExpression); - element.eq(element.length - 1).attr('ng-repeat-end', true); - } else { - attrs.$set('ngRepeat', repeatExpression); - } + // Delegate to the link function returned by the new compilation of the ng-repeat + compiled(scope); + }; + } + + /** + * If a pagination id has been specified, we need to check that it is present as the second argument passed to + * the itemsPerPage filter. If it is not there, we add it and return the modified expression. + * + * @param expression + * @param paginationId + * @returns {*} + */ + function getRepeatExpression(expression, paginationId) { + var repeatExpression, + idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/); - var compiled = $compile(element, false, 5000); // we manually compile the element again, as we have now added ng-repeat. Priority less than 5000 prevents infinite recursion of compiling dirPaginate + if (paginationId !== DEFAULT_ID && !idDefinedInFilter) { + repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:[^|]*)/, "$1 : '" + paginationId + "'"); + } else { + repeatExpression = expression; + } - var currentPageGetter; - if (attrs.currentPage) { - currentPageGetter = $parse(attrs.currentPage); - } else { - // if the current-page attribute was not set, we'll make our own - var defaultCurrentPage = paginationId + '__currentPage'; - scope[defaultCurrentPage] = 1; - currentPageGetter = $parse(defaultCurrentPage); - } - paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope); - - if (typeof attrs.totalItems !== 'undefined') { - paginationService.setAsyncModeTrue(paginationId); - scope.$watch(function() { - return $parse(attrs.totalItems)(scope); - }, function (result) { - if (0 <= result) { - paginationService.setCollectionLength(paginationId, result); - } - }); - } else { - scope.$watchCollection(function() { - return collectionGetter(scope); - }, function(collection) { - if (collection) { - paginationService.setCollectionLength(paginationId, collection.length); - } - }); - } + return repeatExpression; + } - // Delegate to the link function returned by the new compilation of the ng-repeat - compiled(scope); - }; + /** + * Adds the ng-repeat directive to the element. In the case of multi-element (-start, -end) it adds the + * appropriate multi-element ng-repeat to the first and last element in the range. + * @param element + * @param attrs + * @param repeatExpression + */ + function addNgRepeatToElement(element, attrs, repeatExpression) { + if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) { + // using multiElement mode (dir-paginate-start, dir-paginate-end) + attrs.$set('ngRepeatStart', repeatExpression); + element.eq(element.length - 1).attr('ng-repeat-end', true); + } else { + attrs.$set('ngRepeat', repeatExpression); } - }; - }]); + } - module.directive('dirPaginationControls', ['paginationService', 'paginationTemplate', function(paginationService, paginationTemplate) { + /** + * Adds the dir-paginate-no-compile directive to each element in the tElement range. + * @param tElement + */ + function addNoCompileAttributes(tElement) { + angular.forEach(tElement, function(el) { + if (el.nodeType === Node.ELEMENT_NODE) { + angular.element(el).attr('dir-paginate-no-compile', true); + } + }); + } + + /** + * Removes the variations on dir-paginate (data-, -start, -end) and the dir-paginate-no-compile directives. + * @param element + */ + function removeTemporaryAttributes(element) { + angular.forEach(element, function(el) { + if (el.nodeType === Node.ELEMENT_NODE) { + angular.element(el).removeAttr('dir-paginate-no-compile'); + } + }); + element.eq(0).removeAttr('dir-paginate-start').removeAttr('dir-paginate').removeAttr('data-dir-paginate-start').removeAttr('data-dir-paginate'); + element.eq(element.length - 1).removeAttr('dir-paginate-end').removeAttr('data-dir-paginate-end'); + } + + /** + * Creates a getter function for the current-page attribute, using the expression provided or a default value if + * no current-page expression was specified. + * + * @param scope + * @param attrs + * @param paginationId + * @returns {*} + */ + function makeCurrentPageGetterFn(scope, attrs, paginationId) { + var currentPageGetter; + if (attrs.currentPage) { + currentPageGetter = $parse(attrs.currentPage); + } else { + // if the current-page attribute was not set, we'll make our own + var defaultCurrentPage = paginationId + '__currentPage'; + scope[defaultCurrentPage] = 1; + currentPageGetter = $parse(defaultCurrentPage); + } + return currentPageGetter; + } + } + + /** + * This is a helper directive that allows correct compilation when in multi-element mode (ie dir-paginate-start, dir-paginate-end). + * It is dynamically added to all elements in the dir-paginate compile function, and it prevents further compilation of + * any inner directives. It is then removed in the link function, and all inner directives are then manually compiled. + */ + function noCompileDirective() { + return { + priority: 5000, + terminal: true + }; + } + + function dirPaginationControlsDirective(paginationService, paginationTemplate) { var numberRegex = /^\d+$/; + return { + restrict: 'AE', + templateUrl: function(elem, attrs) { + return attrs.templateUrl || paginationTemplate.getPath(); + }, + scope: { + maxSize: '=?', + onPageChange: '&?', + paginationId: '=?' + }, + link: dirPaginationControlsLinkFn + }; + + function dirPaginationControlsLinkFn(scope, element, attrs) { + + // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has + // 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 + // no corresponding dir-paginate directive and wrongly throwing an exception. + var rawId = attrs.paginationId || DEFAULT_ID; + var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID; + + if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) { + var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' '; + throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.'; + } + + if (!scope.maxSize) { scope.maxSize = 9; } + scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true; + scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false; + + var paginationRange = Math.max(scope.maxSize, 5); + scope.pages = []; + scope.pagination = { + last: 1, + current: 1 + }; + scope.range = { + lower: 1, + upper: 1, + total: 1 + }; + + scope.$watch(function() { + return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId); + }, function(length) { + if (0 < length) { + generatePagination(); + } + }); + + scope.$watch(function() { + return (paginationService.getItemsPerPage(paginationId)); + }, function(current, previous) { + if (current != previous && typeof previous !== 'undefined') { + goToPage(scope.pagination.current); + } + }); + + scope.$watch(function() { + return paginationService.getCurrentPage(paginationId); + }, function(currentPage, previousPage) { + if (currentPage != previousPage) { + goToPage(currentPage); + } + }); + + scope.setCurrent = function(num) { + if (isValidPageNumber(num)) { + paginationService.setCurrentPage(paginationId, num); + } + }; + + function goToPage(num) { + if (isValidPageNumber(num)) { + scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); + scope.pagination.current = num; + updateRangeValues(); + + // if a callback has been set, then call it with the page number as an argument + if (scope.onPageChange) { + scope.onPageChange({ newPageNumber : num }); + } + } + } + + function generatePagination() { + var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1; + + scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); + scope.pagination.current = page; + scope.pagination.last = scope.pages[scope.pages.length - 1]; + if (scope.pagination.last < scope.pagination.current) { + scope.setCurrent(scope.pagination.last); + } else { + updateRangeValues(); + } + } + + /** + * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination + * template to display the current page range, e.g. "showing 21 - 40 of 144 results"; + */ + function updateRangeValues() { + var currentPage = paginationService.getCurrentPage(paginationId), + itemsPerPage = paginationService.getItemsPerPage(paginationId), + totalItems = paginationService.getCollectionLength(paginationId); + + scope.range.lower = (currentPage - 1) * itemsPerPage + 1; + scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems); + scope.range.total = totalItems; + } + + function isValidPageNumber(num) { + return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last)); + } + } + /** * Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the * links used in pagination @@ -192,124 +397,14 @@ return i; } } + } - return { - restrict: 'AE', - templateUrl: function(elem, attrs) { - return attrs.templateUrl || paginationTemplate.getPath(); - }, - scope: { - maxSize: '=?', - onPageChange: '&?', - paginationId: '=?' - }, - link: function dirPaginationControlsLinkFn(scope, element, attrs) { - - // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has - // 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 - // no corresponding dir-paginate directive and wrongly throwing an exception. - var rawId = attrs.paginationId || DEFAULT_ID; - var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID; - - if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) { - var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' '; - throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.'; - } - - if (!scope.maxSize) { scope.maxSize = 9; } - scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true; - scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false; - - var paginationRange = Math.max(scope.maxSize, 5); - scope.pages = []; - scope.pagination = { - last: 1, - current: 1 - }; - scope.range = { - lower: 1, - upper: 1, - total: 1 - }; - - scope.$watch(function() { - return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId); - }, function(length) { - if (0 < length) { - generatePagination(); - } - }); - - scope.$watch(function() { - return (paginationService.getItemsPerPage(paginationId)); - }, function(current, previous) { - if (current != previous && typeof previous !== 'undefined') { - goToPage(scope.pagination.current); - } - }); - - scope.$watch(function() { - return paginationService.getCurrentPage(paginationId); - }, function(currentPage, previousPage) { - if (currentPage != previousPage) { - goToPage(currentPage); - } - }); - - scope.setCurrent = function(num) { - if (isValidPageNumber(num)) { - paginationService.setCurrentPage(paginationId, num); - } - }; - - function goToPage(num) { - if (isValidPageNumber(num)) { - scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); - scope.pagination.current = num; - updateRangeValues(); - - // if a callback has been set, then call it with the page number as an argument - if (scope.onPageChange) { - scope.onPageChange({ newPageNumber : num }); - } - } - } - - function generatePagination() { - var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1; - - scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); - scope.pagination.current = page; - scope.pagination.last = scope.pages[scope.pages.length - 1]; - if (scope.pagination.last < scope.pagination.current) { - scope.setCurrent(scope.pagination.last); - } else { - updateRangeValues(); - } - } - - /** - * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination - * template to display the current page range, e.g. "showing 21 - 40 of 144 results"; - */ - function updateRangeValues() { - var currentPage = paginationService.getCurrentPage(paginationId), - itemsPerPage = paginationService.getItemsPerPage(paginationId), - totalItems = paginationService.getCollectionLength(paginationId); - - scope.range.lower = (currentPage - 1) * itemsPerPage + 1; - scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems); - scope.range.total = totalItems; - } - - function isValidPageNumber(num) { - return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last)); - } - } - }; - }]); - - module.filter('itemsPerPage', ['paginationService', function(paginationService) { + /** + * This filter slices the collection into pages based on the current page number and number of items per page. + * @param paginationService + * @returns {Function} + */ + function itemsPerPageFilter(paginationService) { return function(collection, itemsPerPage, paginationId) { if (typeof (paginationId) === 'undefined') { @@ -335,9 +430,12 @@ return collection; } }; - }]); + } - module.service('paginationService', function() { + /** + * This service allows the various parts of the module to communicate and stay in sync. + */ + function paginationService() { var instances = {}; var lastRegisteredInstance; @@ -392,16 +490,19 @@ this.isAsyncMode = function(instanceId) { return instances[instanceId].asyncMode; }; - }); - - module.provider('paginationTemplate', function() { + } + + /** + * This provider allows global configuration of the template path used by the dir-pagination-controls directive. + */ + function paginationTemplateProvider() { var templatePath = 'directives/pagination/dirPagination.tpl.html'; - + this.setPath = function(path) { templatePath = path; }; - + this.$get = function() { return { getPath: function() { @@ -409,5 +510,5 @@ } }; }; - }); + } })(); \ No newline at end of file