Add pagination directive to angular frontend
Big thanks to @michaelbromley for his work on this. Source can be be found at https://github.com/michaelbromley/angularUtils/tree/master/src/directives/pagination
This commit is contained in:
parent
a2cd6e1434
commit
ad8bbea2bc
|
@ -37,6 +37,7 @@ function xhtmlHeaders( $file, $title )
|
|||
$viewCssPhpFile = getSkinFile( 'views/css/'.$basename.'.css.php' );
|
||||
$viewJsFile = getSkinFile( 'views/js/'.$basename.'.js' );
|
||||
$viewJsPhpFile = getSkinFile( 'views/js/'.$basename.'.js.php' );
|
||||
$dirPagination = getSkinFile( 'js/dirPagination.js' );
|
||||
|
||||
extract( $GLOBALS, EXTR_OVERWRITE );
|
||||
?>
|
||||
|
@ -75,6 +76,7 @@ function xhtmlHeaders( $file, $title )
|
|||
}
|
||||
?>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js"></script>
|
||||
<script src="<?= $dirPagination ?>"></script>
|
||||
<script type="text/javascript" src="<?= $ChartJsFile ?>"></script>
|
||||
<script src="<?= getSkinFile('js/app.js'); ?>"></script>
|
||||
<script src="<?= getSkinFile('js/controllers.js'); ?>"></script>
|
||||
|
|
|
@ -0,0 +1,413 @@
|
|||
/**
|
||||
* 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 <michael@michaelbromley.co.uk>
|
||||
*/
|
||||
|
||||
(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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,20 @@
|
|||
<div class="text-center">
|
||||
<ul class="pagination" ng-if="1 < pages.length">
|
||||
<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 $index" ng-class="{ active : pagination.current == pageNumber, disabled : pageNumber == '...' }">
|
||||
<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>
|
||||
</div>
|
Loading…
Reference in New Issue