dirPagination.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. /**
  2. * dirPagination - AngularJS module for paginating (almost) anything.
  3. *
  4. *
  5. * Credits
  6. * =======
  7. *
  8. * Daniel Tabuenca: https://groups.google.com/d/msg/angular/an9QpzqIYiM/r8v-3W1X5vcJ
  9. * for the idea on how to dynamically invoke the ng-repeat directive.
  10. *
  11. * I borrowed a couple of lines and a few attribute names from the AngularUI Bootstrap project:
  12. * https://github.com/angular-ui/bootstrap/blob/master/src/pagination/pagination.js
  13. *
  14. * Copyright 2014 Michael Bromley <michael@michaelbromley.co.uk>
  15. */
  16. (function() {
  17. /**
  18. * Config
  19. */
  20. var moduleName = 'angularUtils.directives.dirPagination';
  21. var DEFAULT_ID = '__default';
  22. /**
  23. * Module
  24. */
  25. angular.module(moduleName, [])
  26. .directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective])
  27. .directive('dirPaginateNoCompile', noCompileDirective)
  28. .directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective])
  29. .filter('itemsPerPage', ['paginationService', itemsPerPageFilter])
  30. .service('paginationService', paginationService)
  31. .provider('paginationTemplate', paginationTemplateProvider)
  32. .run(['$templateCache',dirPaginationControlsTemplateInstaller]);
  33. function dirPaginateDirective($compile, $parse, paginationService) {
  34. return {
  35. terminal: true,
  36. multiElement: true,
  37. priority: 100,
  38. compile: dirPaginationCompileFn
  39. };
  40. function dirPaginationCompileFn(tElement, tAttrs){
  41. var expression = tAttrs.dirPaginate;
  42. // regex taken directly from https://github.com/angular/angular.js/blob/v1.4.x/src/ng/directive/ngRepeat.js#L339
  43. var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
  44. var filterPattern = /\|\s*itemsPerPage\s*:\s*(.*\(\s*\w*\)|([^\)]*?(?=\s+as\s+))|[^\)]*)/;
  45. if (match[2].match(filterPattern) === null) {
  46. throw 'pagination directive: the \'itemsPerPage\' filter must be set.';
  47. }
  48. var itemsPerPageFilterRemoved = match[2].replace(filterPattern, '');
  49. var collectionGetter = $parse(itemsPerPageFilterRemoved);
  50. addNoCompileAttributes(tElement);
  51. // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any
  52. // dir-pagination-controls directives that may be looking for this ID.
  53. var rawId = tAttrs.paginationId || DEFAULT_ID;
  54. paginationService.registerInstance(rawId);
  55. return function dirPaginationLinkFn(scope, element, attrs){
  56. // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and
  57. // potentially register a new ID if it evaluates to a different value than the rawId.
  58. var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID;
  59. paginationService.registerInstance(paginationId);
  60. var repeatExpression = getRepeatExpression(expression, paginationId);
  61. addNgRepeatToElement(element, attrs, repeatExpression);
  62. removeTemporaryAttributes(element);
  63. var compiled = $compile(element);
  64. var currentPageGetter = makeCurrentPageGetterFn(scope, attrs, paginationId);
  65. paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope);
  66. if (typeof attrs.totalItems !== 'undefined') {
  67. paginationService.setAsyncModeTrue(paginationId);
  68. scope.$watch(function() {
  69. return $parse(attrs.totalItems)(scope);
  70. }, function (result) {
  71. if (0 <= result) {
  72. paginationService.setCollectionLength(paginationId, result);
  73. }
  74. });
  75. } else {
  76. scope.$watchCollection(function() {
  77. return collectionGetter(scope);
  78. }, function(collection) {
  79. if (collection) {
  80. paginationService.setCollectionLength(paginationId, collection.length);
  81. }
  82. });
  83. }
  84. // Delegate to the link function returned by the new compilation of the ng-repeat
  85. compiled(scope);
  86. };
  87. }
  88. /**
  89. * If a pagination id has been specified, we need to check that it is present as the second argument passed to
  90. * the itemsPerPage filter. If it is not there, we add it and return the modified expression.
  91. *
  92. * @param expression
  93. * @param paginationId
  94. * @returns {*}
  95. */
  96. function getRepeatExpression(expression, paginationId) {
  97. var repeatExpression,
  98. idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/);
  99. if (paginationId !== DEFAULT_ID && !idDefinedInFilter) {
  100. repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:\s*[^|\s]*)/, "$1 : '" + paginationId + "'");
  101. } else {
  102. repeatExpression = expression;
  103. }
  104. return repeatExpression;
  105. }
  106. /**
  107. * Adds the ng-repeat directive to the element. In the case of multi-element (-start, -end) it adds the
  108. * appropriate multi-element ng-repeat to the first and last element in the range.
  109. * @param element
  110. * @param attrs
  111. * @param repeatExpression
  112. */
  113. function addNgRepeatToElement(element, attrs, repeatExpression) {
  114. if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) {
  115. // using multiElement mode (dir-paginate-start, dir-paginate-end)
  116. attrs.$set('ngRepeatStart', repeatExpression);
  117. element.eq(element.length - 1).attr('ng-repeat-end', true);
  118. } else {
  119. attrs.$set('ngRepeat', repeatExpression);
  120. }
  121. }
  122. /**
  123. * Adds the dir-paginate-no-compile directive to each element in the tElement range.
  124. * @param tElement
  125. */
  126. function addNoCompileAttributes(tElement) {
  127. angular.forEach(tElement, function(el) {
  128. if (el.nodeType === 1) {
  129. angular.element(el).attr('dir-paginate-no-compile', true);
  130. }
  131. });
  132. }
  133. /**
  134. * Removes the variations on dir-paginate (data-, -start, -end) and the dir-paginate-no-compile directives.
  135. * @param element
  136. */
  137. function removeTemporaryAttributes(element) {
  138. angular.forEach(element, function(el) {
  139. if (el.nodeType === 1) {
  140. angular.element(el).removeAttr('dir-paginate-no-compile');
  141. }
  142. });
  143. element.eq(0).removeAttr('dir-paginate-start').removeAttr('dir-paginate').removeAttr('data-dir-paginate-start').removeAttr('data-dir-paginate');
  144. element.eq(element.length - 1).removeAttr('dir-paginate-end').removeAttr('data-dir-paginate-end');
  145. }
  146. /**
  147. * Creates a getter function for the current-page attribute, using the expression provided or a default value if
  148. * no current-page expression was specified.
  149. *
  150. * @param scope
  151. * @param attrs
  152. * @param paginationId
  153. * @returns {*}
  154. */
  155. function makeCurrentPageGetterFn(scope, attrs, paginationId) {
  156. var currentPageGetter;
  157. if (attrs.currentPage) {
  158. currentPageGetter = $parse(attrs.currentPage);
  159. } else {
  160. // If the current-page attribute was not set, we'll make our own.
  161. // Replace any non-alphanumeric characters which might confuse
  162. // the $parse service and give unexpected results.
  163. // See https://github.com/michaelbromley/angularUtils/issues/233
  164. var defaultCurrentPage = (paginationId + '__currentPage').replace(/\W/g, '_');
  165. scope[defaultCurrentPage] = 1;
  166. currentPageGetter = $parse(defaultCurrentPage);
  167. }
  168. return currentPageGetter;
  169. }
  170. }
  171. /**
  172. * This is a helper directive that allows correct compilation when in multi-element mode (ie dir-paginate-start, dir-paginate-end).
  173. * It is dynamically added to all elements in the dir-paginate compile function, and it prevents further compilation of
  174. * any inner directives. It is then removed in the link function, and all inner directives are then manually compiled.
  175. */
  176. function noCompileDirective() {
  177. return {
  178. priority: 5000,
  179. terminal: true
  180. };
  181. }
  182. function dirPaginationControlsTemplateInstaller($templateCache) {
  183. $templateCache.put('angularUtils.directives.dirPagination.template', '<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)">&laquo;</a></li><li ng-if="directionLinks" ng-class="{ disabled : pagination.current == 1 }"><a href="" ng-click="setCurrent(pagination.current - 1)">&lsaquo;</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)">&rsaquo;</a></li><li ng-if="boundaryLinks" ng-class="{ disabled : pagination.current == pagination.last }"><a href="" ng-click="setCurrent(pagination.last)">&raquo;</a></li></ul>');
  184. }
  185. function dirPaginationControlsDirective(paginationService, paginationTemplate) {
  186. var numberRegex = /^\d+$/;
  187. return {
  188. restrict: 'AE',
  189. templateUrl: function(elem, attrs) {
  190. return attrs.templateUrl || paginationTemplate.getPath();
  191. },
  192. scope: {
  193. maxSize: '=?',
  194. onPageChange: '&?',
  195. paginationId: '=?',
  196. autoHide: '=?'
  197. },
  198. link: dirPaginationControlsLinkFn
  199. };
  200. function dirPaginationControlsLinkFn(scope, element, attrs) {
  201. // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has
  202. // 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
  203. // no corresponding dir-paginate directive and wrongly throwing an exception.
  204. var rawId = attrs.paginationId || DEFAULT_ID;
  205. var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID;
  206. if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) {
  207. var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' ';
  208. console.warn('Pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive, which was not found at link time.');
  209. }
  210. if (!scope.maxSize) { scope.maxSize = 9; }
  211. scope.autoHide = scope.autoHide === undefined ? true : scope.autoHide;
  212. scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true;
  213. scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false;
  214. var paginationRange = Math.max(scope.maxSize, 5);
  215. scope.pages = [];
  216. scope.pagination = {
  217. last: 1,
  218. current: 1
  219. };
  220. scope.range = {
  221. lower: 1,
  222. upper: 1,
  223. total: 1
  224. };
  225. scope.$watch(function() {
  226. if (paginationService.isRegistered(paginationId)) {
  227. return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId);
  228. }
  229. }, function(length) {
  230. if (0 < length) {
  231. generatePagination();
  232. }
  233. });
  234. scope.$watch(function() {
  235. if (paginationService.isRegistered(paginationId)) {
  236. return (paginationService.getItemsPerPage(paginationId));
  237. }
  238. }, function(current, previous) {
  239. if (current != previous && typeof previous !== 'undefined') {
  240. goToPage(scope.pagination.current);
  241. }
  242. });
  243. scope.$watch(function() {
  244. if (paginationService.isRegistered(paginationId)) {
  245. return paginationService.getCurrentPage(paginationId);
  246. }
  247. }, function(currentPage, previousPage) {
  248. if (currentPage != previousPage) {
  249. goToPage(currentPage);
  250. }
  251. });
  252. scope.setCurrent = function(num) {
  253. if (paginationService.isRegistered(paginationId) && isValidPageNumber(num)) {
  254. num = parseInt(num, 10);
  255. paginationService.setCurrentPage(paginationId, num);
  256. }
  257. };
  258. /**
  259. * Custom "track by" function which allows for duplicate "..." entries on long lists,
  260. * yet fixes the problem of wrongly-highlighted links which happens when using
  261. * "track by $index" - see https://github.com/michaelbromley/angularUtils/issues/153
  262. * @param id
  263. * @param index
  264. * @returns {string}
  265. */
  266. scope.tracker = function(id, index) {
  267. return id + '_' + index;
  268. };
  269. function goToPage(num) {
  270. if (paginationService.isRegistered(paginationId) && isValidPageNumber(num)) {
  271. scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
  272. scope.pagination.current = num;
  273. updateRangeValues();
  274. // if a callback has been set, then call it with the page number as an argument
  275. if (scope.onPageChange) {
  276. scope.onPageChange({ newPageNumber : num });
  277. }
  278. }
  279. }
  280. function generatePagination() {
  281. if (paginationService.isRegistered(paginationId)) {
  282. var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1;
  283. scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
  284. scope.pagination.current = page;
  285. scope.pagination.last = scope.pages[scope.pages.length - 1];
  286. if (scope.pagination.last < scope.pagination.current) {
  287. scope.setCurrent(scope.pagination.last);
  288. } else {
  289. updateRangeValues();
  290. }
  291. }
  292. }
  293. /**
  294. * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination
  295. * template to display the current page range, e.g. "showing 21 - 40 of 144 results";
  296. */
  297. function updateRangeValues() {
  298. if (paginationService.isRegistered(paginationId)) {
  299. var currentPage = paginationService.getCurrentPage(paginationId),
  300. itemsPerPage = paginationService.getItemsPerPage(paginationId),
  301. totalItems = paginationService.getCollectionLength(paginationId);
  302. scope.range.lower = (currentPage - 1) * itemsPerPage + 1;
  303. scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems);
  304. scope.range.total = totalItems;
  305. }
  306. }
  307. function isValidPageNumber(num) {
  308. return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last));
  309. }
  310. }
  311. /**
  312. * Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the
  313. * links used in pagination
  314. *
  315. * @param currentPage
  316. * @param rowsPerPage
  317. * @param paginationRange
  318. * @param collectionLength
  319. * @returns {Array}
  320. */
  321. function generatePagesArray(currentPage, collectionLength, rowsPerPage, paginationRange) {
  322. var pages = [];
  323. var totalPages = Math.ceil(collectionLength / rowsPerPage);
  324. var halfWay = Math.ceil(paginationRange / 2);
  325. var position;
  326. if (currentPage <= halfWay) {
  327. position = 'start';
  328. } else if (totalPages - halfWay < currentPage) {
  329. position = 'end';
  330. } else {
  331. position = 'middle';
  332. }
  333. var ellipsesNeeded = paginationRange < totalPages;
  334. var i = 1;
  335. while (i <= totalPages && i <= paginationRange) {
  336. var pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages);
  337. var openingEllipsesNeeded = (i === 2 && (position === 'middle' || position === 'end'));
  338. var closingEllipsesNeeded = (i === paginationRange - 1 && (position === 'middle' || position === 'start'));
  339. if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
  340. pages.push('...');
  341. } else {
  342. pages.push(pageNumber);
  343. }
  344. i ++;
  345. }
  346. return pages;
  347. }
  348. /**
  349. * Given the position in the sequence of pagination links [i], figure out what page number corresponds to that position.
  350. *
  351. * @param i
  352. * @param currentPage
  353. * @param paginationRange
  354. * @param totalPages
  355. * @returns {*}
  356. */
  357. function calculatePageNumber(i, currentPage, paginationRange, totalPages) {
  358. var halfWay = Math.ceil(paginationRange/2);
  359. if (i === paginationRange) {
  360. return totalPages;
  361. } else if (i === 1) {
  362. return i;
  363. } else if (paginationRange < totalPages) {
  364. if (totalPages - halfWay < currentPage) {
  365. return totalPages - paginationRange + i;
  366. } else if (halfWay < currentPage) {
  367. return currentPage - halfWay + i;
  368. } else {
  369. return i;
  370. }
  371. } else {
  372. return i;
  373. }
  374. }
  375. }
  376. /**
  377. * This filter slices the collection into pages based on the current page number and number of items per page.
  378. * @param paginationService
  379. * @returns {Function}
  380. */
  381. function itemsPerPageFilter(paginationService) {
  382. return function(collection, itemsPerPage, paginationId) {
  383. if (typeof (paginationId) === 'undefined') {
  384. paginationId = DEFAULT_ID;
  385. }
  386. if (!paginationService.isRegistered(paginationId)) {
  387. throw 'pagination directive: the itemsPerPage id argument (id: ' + paginationId + ') does not match a registered pagination-id.';
  388. }
  389. var end;
  390. var start;
  391. if (angular.isObject(collection)) {
  392. itemsPerPage = parseInt(itemsPerPage) || 9999999999;
  393. if (paginationService.isAsyncMode(paginationId)) {
  394. start = 0;
  395. } else {
  396. start = (paginationService.getCurrentPage(paginationId) - 1) * itemsPerPage;
  397. }
  398. end = start + itemsPerPage;
  399. paginationService.setItemsPerPage(paginationId, itemsPerPage);
  400. if (collection instanceof Array) {
  401. // the array just needs to be sliced
  402. return collection.slice(start, end);
  403. } else {
  404. // in the case of an object, we need to get an array of keys, slice that, then map back to
  405. // the original object.
  406. var slicedObject = {};
  407. angular.forEach(keys(collection).slice(start, end), function(key) {
  408. slicedObject[key] = collection[key];
  409. });
  410. return slicedObject;
  411. }
  412. } else {
  413. return collection;
  414. }
  415. };
  416. }
  417. /**
  418. * Shim for the Object.keys() method which does not exist in IE < 9
  419. * @param obj
  420. * @returns {Array}
  421. */
  422. function keys(obj) {
  423. if (!Object.keys) {
  424. var objKeys = [];
  425. for (var i in obj) {
  426. if (obj.hasOwnProperty(i)) {
  427. objKeys.push(i);
  428. }
  429. }
  430. return objKeys;
  431. } else {
  432. return Object.keys(obj);
  433. }
  434. }
  435. /**
  436. * This service allows the various parts of the module to communicate and stay in sync.
  437. */
  438. function paginationService() {
  439. var instances = {};
  440. var lastRegisteredInstance;
  441. this.registerInstance = function(instanceId) {
  442. if (typeof instances[instanceId] === 'undefined') {
  443. instances[instanceId] = {
  444. asyncMode: false
  445. };
  446. lastRegisteredInstance = instanceId;
  447. }
  448. };
  449. this.isRegistered = function(instanceId) {
  450. return (typeof instances[instanceId] !== 'undefined');
  451. };
  452. this.getLastInstanceId = function() {
  453. return lastRegisteredInstance;
  454. };
  455. this.setCurrentPageParser = function(instanceId, val, scope) {
  456. instances[instanceId].currentPageParser = val;
  457. instances[instanceId].context = scope;
  458. };
  459. this.setCurrentPage = function(instanceId, val) {
  460. instances[instanceId].currentPageParser.assign(instances[instanceId].context, val);
  461. };
  462. this.getCurrentPage = function(instanceId) {
  463. var parser = instances[instanceId].currentPageParser;
  464. return parser ? parser(instances[instanceId].context) : 1;
  465. };
  466. this.setItemsPerPage = function(instanceId, val) {
  467. instances[instanceId].itemsPerPage = val;
  468. };
  469. this.getItemsPerPage = function(instanceId) {
  470. return instances[instanceId].itemsPerPage;
  471. };
  472. this.setCollectionLength = function(instanceId, val) {
  473. instances[instanceId].collectionLength = val;
  474. };
  475. this.getCollectionLength = function(instanceId) {
  476. return instances[instanceId].collectionLength;
  477. };
  478. this.setAsyncModeTrue = function(instanceId) {
  479. instances[instanceId].asyncMode = true;
  480. };
  481. this.isAsyncMode = function(instanceId) {
  482. return instances[instanceId].asyncMode;
  483. };
  484. }
  485. /**
  486. * This provider allows global configuration of the template path used by the dir-pagination-controls directive.
  487. */
  488. function paginationTemplateProvider() {
  489. var templatePath = 'angularUtils.directives.dirPagination.template';
  490. this.setPath = function(path) {
  491. templatePath = path;
  492. };
  493. this.$get = function() {
  494. return {
  495. getPath: function() {
  496. return templatePath;
  497. }
  498. };
  499. };
  500. }
  501. })();