diff --git a/web/skins/bootstrap/includes/functions.php b/web/skins/bootstrap/includes/functions.php
index a36d8c9ee..e197863ba 100644
--- a/web/skins/bootstrap/includes/functions.php
+++ b/web/skins/bootstrap/includes/functions.php
@@ -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 )
}
?>
+
diff --git a/web/skins/bootstrap/js/dirPagination.js b/web/skins/bootstrap/js/dirPagination.js
new file mode 100644
index 000000000..869f128ec
--- /dev/null
+++ b/web/skins/bootstrap/js/dirPagination.js
@@ -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
+ */
+
+(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;
+ }
+ };
+ };
+ });
+})();
\ No newline at end of file
diff --git a/web/skins/bootstrap/js/dirPagination.tpl.html b/web/skins/bootstrap/js/dirPagination.tpl.html
new file mode 100644
index 000000000..250268232
--- /dev/null
+++ b/web/skins/bootstrap/js/dirPagination.tpl.html
@@ -0,0 +1,20 @@
+
+
+