Created
January 22, 2015 12:43
-
-
Save michaelbromley/ec0f433fecf23ba20528 to your computer and use it in GitHub Desktop.
dirPagination: Fix for issue 92
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is a fix I have made for https://github.com/michaelbromley/angularUtils/issues/92 | |
// If you ran into this issue, try this out. I am looking for feedback before I push to the repo. | |
// It's a bit hacky but the tests pass and so far looks okay to me... | |
/** | |
* dirPagination - AngularJS module for paginating (almost) anything. | |
* | |
* | |
* Credits | |
* ======= | |
* | |
* Daniel Tabuenca: https://groups.google.com/d/msg/angular/an9QpzqIYiM/r8v-3W1X5vcJ | |
* for the idea on how to dynamically invoke the ng-repeat directive. | |
* | |
* I borrowed a couple of lines and a few attribute names from the AngularUI Bootstrap project: | |
* https://github.com/angular-ui/bootstrap/blob/master/src/pagination/pagination.js | |
* | |
* Copyright 2014 Michael Bromley <[email protected]> | |
*/ | |
(function() { | |
/** | |
* Config | |
*/ | |
var moduleName = 'angularUtils.directives.dirPagination'; | |
var DEFAULT_ID = '__default'; | |
/** | |
* Module | |
*/ | |
var module; | |
try { | |
module = angular.module(moduleName); | |
} catch(err) { | |
// named module does not exist, so create one | |
module = angular.module(moduleName, []); | |
} | |
module.directive('dirPaginate', ['$compile', '$parse', '$timeout', 'paginationService', function($compile, $parse, $timeout, 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){ | |
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); | |
// 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); | |
var multiElementMode = tElement[0].hasAttribute('dir-paginate-start') || tElement[0].hasAttribute('data-dir-paginate-start'); | |
var nestedNgRepeat = false; | |
if (multiElementMode) { | |
// When an ng-repeat is used in multiElement mode, and that ng-repeat is *not* contained | |
// within the first element of the range, it does not get compiled correctly. See https://github.com/michaelbromley/angularUtils/issues/92 | |
// This code detects this specific case and is used at the end of the link function to trigger a work-around. | |
for (var i = 1; i < tElement.length; i++) { | |
if (tElement[i].nodeType === 1 && tElement[i].innerHTML.indexOf('ng-repeat') !== -1) { | |
nestedNgRepeat = true; | |
} | |
} | |
} | |
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 (multiElementMode) { | |
// 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); | |
} | |
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 | |
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); | |
} | |
}); | |
} | |
// Delegate to the link function returned by the new compilation of the ng-repeat | |
compiled(scope); | |
// Work-around for the case where an ng-repeat is nested in a multi-element node and does not get displayed correctly on first load. | |
// This disgusting but effective hack just forces a switch of the current page, forcing the nested ng-repeat to render correctly. | |
if (multiElementMode && nestedNgRepeat) { | |
var currentPage = paginationService.getCurrentPage(paginationId); | |
paginationService.setCurrentPage(paginationId, 0); | |
$timeout(function() { | |
paginationService.setCurrentPage(paginationId, currentPage); | |
}); | |
} | |
}; | |
} | |
}; | |
}]); | |
module.directive('dirPaginationControls', ['paginationService', 'paginationTemplate', function(paginationService, paginationTemplate) { | |
var numberRegex = /^\d+$/; | |
/** | |
* Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the | |
* links used in pagination | |
* | |
* @param currentPage | |
* @param rowsPerPage | |
* @param paginationRange | |
* @param collectionLength | |
* @returns {Array} | |
*/ | |
function generatePagesArray(currentPage, collectionLength, rowsPerPage, paginationRange) { | |
var pages = []; | |
var totalPages = Math.ceil(collectionLength / rowsPerPage); | |
var halfWay = Math.ceil(paginationRange / 2); | |
var position; | |
if (currentPage <= halfWay) { | |
position = 'start'; | |
} else if (totalPages - halfWay < currentPage) { | |
position = 'end'; | |
} else { | |
position = 'middle'; | |
} | |
var ellipsesNeeded = paginationRange < totalPages; | |
var i = 1; | |
while (i <= totalPages && i <= paginationRange) { | |
var pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages); | |
var openingEllipsesNeeded = (i === 2 && (position === 'middle' || position === 'end')); | |
var closingEllipsesNeeded = (i === paginationRange - 1 && (position === 'middle' || position === 'start')); | |
if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) { | |
pages.push('...'); | |
} else { | |
pages.push(pageNumber); | |
} | |
i ++; | |
} | |
return pages; | |
} | |
/** | |
* Given the position in the sequence of pagination links [i], figure out what page number corresponds to that position. | |
* | |
* @param i | |
* @param currentPage | |
* @param paginationRange | |
* @param totalPages | |
* @returns {*} | |
*/ | |
function calculatePageNumber(i, currentPage, paginationRange, totalPages) { | |
var halfWay = Math.ceil(paginationRange/2); | |
if (i === paginationRange) { | |
return totalPages; | |
} else if (i === 1) { | |
return i; | |
} else if (paginationRange < totalPages) { | |
if (totalPages - halfWay < currentPage) { | |
return totalPages - paginationRange + i; | |
} else if (halfWay < currentPage) { | |
return currentPage - halfWay + i; | |
} else { | |
return i; | |
} | |
} else { | |
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) { | |
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) { | |
return function(collection, itemsPerPage, paginationId) { | |
if (typeof (paginationId) === 'undefined') { | |
paginationId = DEFAULT_ID; | |
} | |
if (!paginationService.isRegistered(paginationId)) { | |
throw 'pagination directive: the itemsPerPage id argument (id: ' + paginationId + ') does not match a registered pagination-id.'; | |
} | |
var end; | |
var start; | |
if (collection instanceof Array) { | |
itemsPerPage = parseInt(itemsPerPage) || 9999999999; | |
if (paginationService.isAsyncMode(paginationId)) { | |
start = 0; | |
} else { | |
start = (paginationService.getCurrentPage(paginationId) - 1) * itemsPerPage; | |
} | |
end = start + itemsPerPage; | |
paginationService.setItemsPerPage(paginationId, itemsPerPage); | |
return collection.slice(start, end); | |
} else { | |
return collection; | |
} | |
}; | |
}]); | |
module.service('paginationService', function() { | |
var instances = {}; | |
var lastRegisteredInstance; | |
this.registerInstance = function(instanceId) { | |
if (typeof instances[instanceId] === 'undefined') { | |
instances[instanceId] = { | |
asyncMode: false | |
}; | |
lastRegisteredInstance = instanceId; | |
} | |
}; | |
this.isRegistered = function(instanceId) { | |
return (typeof instances[instanceId] !== 'undefined'); | |
}; | |
this.getLastInstanceId = function() { | |
return lastRegisteredInstance; | |
}; | |
this.setCurrentPageParser = function(instanceId, val, scope) { | |
instances[instanceId].currentPageParser = val; | |
instances[instanceId].context = scope; | |
}; | |
this.setCurrentPage = function(instanceId, val) { | |
instances[instanceId].currentPageParser.assign(instances[instanceId].context, val); | |
}; | |
this.getCurrentPage = function(instanceId) { | |
var parser = instances[instanceId].currentPageParser; | |
return parser ? parser(instances[instanceId].context) : 1; | |
}; | |
this.setItemsPerPage = function(instanceId, val) { | |
instances[instanceId].itemsPerPage = val; | |
}; | |
this.getItemsPerPage = function(instanceId) { | |
return instances[instanceId].itemsPerPage; | |
}; | |
this.setCollectionLength = function(instanceId, val) { | |
instances[instanceId].collectionLength = val; | |
}; | |
this.getCollectionLength = function(instanceId) { | |
return instances[instanceId].collectionLength; | |
}; | |
this.setAsyncModeTrue = function(instanceId) { | |
instances[instanceId].asyncMode = true; | |
}; | |
this.isAsyncMode = function(instanceId) { | |
return instances[instanceId].asyncMode; | |
}; | |
}); | |
module.provider('paginationTemplate', function() { | |
var templatePath = 'directives/pagination/dirPagination.tpl.html'; | |
this.setPath = function(path) { | |
templatePath = path; | |
}; | |
this.$get = function() { | |
return { | |
getPath: function() { | |
return templatePath; | |
} | |
}; | |
}; | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment