Pagination.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. define(["../_StoreMixin", "dojo/_base/declare", "dojo/_base/array", "dojo/_base/lang", "dojo/_base/Deferred",
  2. "dojo/on", "dojo/query", "dojo/string", "dojo/has", "put-selector/put", "dojo/i18n!./nls/pagination",
  3. "dojo/_base/sniff", "xstyle/css!../css/extensions/Pagination.css"],
  4. function(_StoreMixin, declare, arrayUtil, lang, Deferred, on, query, string, has, put, i18n){
  5. function cleanupContent(grid){
  6. // Remove any currently-rendered rows, or noDataMessage
  7. if(grid.noDataNode){
  8. put(grid.noDataNode, "!");
  9. delete grid.noDataNode;
  10. }else{
  11. grid.cleanup();
  12. }
  13. grid.contentNode.innerHTML = "";
  14. }
  15. function cleanupLoading(grid){
  16. if(grid.loadingNode){
  17. put(grid.loadingNode, "!");
  18. delete grid.loadingNode;
  19. }else if(grid._oldPageNodes){
  20. // If cleaning up after a load w/ showLoadingMessage: false,
  21. // be careful to only clean up rows from the old page, not the new one
  22. for(var id in grid._oldPageNodes){
  23. grid.removeRow(grid._oldPageNodes[id]);
  24. }
  25. delete grid._oldPageNodes;
  26. // Also remove the observer from the previous page, if there is one
  27. if(grid._oldPageObserver){
  28. grid._oldPageObserver.cancel();
  29. grid._numObservers--;
  30. delete grid._oldPageObserver;
  31. }
  32. }
  33. delete grid._isLoading;
  34. }
  35. return declare(_StoreMixin, {
  36. // summary:
  37. // An extension for adding discrete pagination to a List or Grid.
  38. // rowsPerPage: Number
  39. // Number of rows (items) to show on a given page.
  40. rowsPerPage: 10,
  41. // pagingTextBox: Boolean
  42. // Indicates whether or not to show a textbox for paging.
  43. pagingTextBox: false,
  44. // previousNextArrows: Boolean
  45. // Indicates whether or not to show the previous and next arrow links.
  46. previousNextArrows: true,
  47. // firstLastArrows: Boolean
  48. // Indicates whether or not to show the first and last arrow links.
  49. firstLastArrows: false,
  50. // pagingLinks: Number
  51. // The number of page links to show on each side of the current page
  52. // Set to 0 (or false) to disable page links.
  53. pagingLinks: 2,
  54. // pageSizeOptions: Array[Number]
  55. // This provides options for different page sizes in a drop-down.
  56. // If it is empty (default), no page size drop-down will be displayed.
  57. pageSizeOptions: null,
  58. // showLoadingMessage: Boolean
  59. // If true, clears previous data and displays loading node when requesting
  60. // another page; if false, leaves previous data in place until new data
  61. // arrives, then replaces it immediately.
  62. showLoadingMessage: true,
  63. // i18nPagination: Object
  64. // This object contains all of the internationalized strings as
  65. // key/value pairs.
  66. i18nPagination: i18n,
  67. showFooter: true,
  68. _currentPage: 1,
  69. _total: 0,
  70. buildRendering: function(){
  71. this.inherited(arguments);
  72. // add pagination to footer
  73. var grid = this,
  74. paginationNode = this.paginationNode =
  75. put(this.footerNode, "div.dgrid-pagination"),
  76. statusNode = this.paginationStatusNode =
  77. put(paginationNode, "div.dgrid-status"),
  78. i18n = this.i18nPagination,
  79. navigationNode,
  80. node,
  81. i;
  82. statusNode.tabIndex = 0;
  83. // Initialize UI based on pageSizeOptions and rowsPerPage
  84. this._updatePaginationSizeSelect();
  85. this._updateRowsPerPageOption();
  86. // initialize some content into paginationStatusNode, to ensure
  87. // accurate results on initial resize call
  88. statusNode.innerHTML = string.substitute(i18n.status,
  89. { start: 1, end: 1, total: 0 });
  90. navigationNode = this.paginationNavigationNode =
  91. put(paginationNode, "div.dgrid-navigation");
  92. if(this.firstLastArrows){
  93. // create a first-page link
  94. node = this.paginationFirstNode =
  95. put(navigationNode, "span.dgrid-first.dgrid-page-link", "«");
  96. node.setAttribute("aria-label", i18n.gotoFirst);
  97. node.tabIndex = 0;
  98. }
  99. if(this.previousNextArrows){
  100. // create a previous link
  101. node = this.paginationPreviousNode =
  102. put(navigationNode, "span.dgrid-previous.dgrid-page-link", "‹");
  103. node.setAttribute("aria-label", i18n.gotoPrev);
  104. node.tabIndex = 0;
  105. }
  106. this.paginationLinksNode = put(navigationNode, "span.dgrid-pagination-links");
  107. if(this.previousNextArrows){
  108. // create a next link
  109. node = this.paginationNextNode =
  110. put(navigationNode, "span.dgrid-next.dgrid-page-link", "›");
  111. node.setAttribute("aria-label", i18n.gotoNext);
  112. node.tabIndex = 0;
  113. }
  114. if(this.firstLastArrows){
  115. // create a last-page link
  116. node = this.paginationLastNode =
  117. put(navigationNode, "span.dgrid-last.dgrid-page-link", "»");
  118. node.setAttribute("aria-label", i18n.gotoLast);
  119. node.tabIndex = 0;
  120. }
  121. this._listeners.push(on(navigationNode, ".dgrid-page-link:click,.dgrid-page-link:keydown", function(event){
  122. // For keyboard events, only respond to enter
  123. if(event.type === "keydown" && event.keyCode !== 13){
  124. return;
  125. }
  126. var cls = this.className,
  127. curr, max;
  128. if(grid._isLoading || cls.indexOf("dgrid-page-disabled") > -1){
  129. return;
  130. }
  131. curr = grid._currentPage;
  132. max = Math.ceil(grid._total / grid.rowsPerPage);
  133. // determine navigation target based on clicked link's class
  134. if(this === grid.paginationPreviousNode){
  135. grid.gotoPage(curr - 1);
  136. }else if(this === grid.paginationNextNode){
  137. grid.gotoPage(curr + 1);
  138. }else if(this === grid.paginationFirstNode){
  139. grid.gotoPage(1);
  140. }else if(this === grid.paginationLastNode){
  141. grid.gotoPage(max);
  142. }else if(cls === "dgrid-page-link"){
  143. grid.gotoPage(+this.innerHTML, true); // the innerHTML has the page number
  144. }
  145. }));
  146. },
  147. destroy: function(){
  148. this.inherited(arguments);
  149. if(this._pagingTextBoxHandle){
  150. this._pagingTextBoxHandle.remove();
  151. }
  152. },
  153. _updatePaginationSizeSelect: function(){
  154. // summary:
  155. // Creates or repopulates the pagination size selector based on
  156. // the values in pageSizeOptions. Called from buildRendering
  157. // and _setPageSizeOptions.
  158. var pageSizeOptions = this.pageSizeOptions,
  159. paginationSizeSelect = this.paginationSizeSelect,
  160. handle;
  161. if(pageSizeOptions && pageSizeOptions.length){
  162. if(!paginationSizeSelect){
  163. // First time setting page options; create the select
  164. paginationSizeSelect = this.paginationSizeSelect =
  165. put(this.paginationNode, "select.dgrid-page-size");
  166. handle = this._paginationSizeChangeHandle =
  167. on(paginationSizeSelect, "change", lang.hitch(this, function(){
  168. this.set("rowsPerPage", +this.paginationSizeSelect.value);
  169. }));
  170. this._listeners.push(handle);
  171. }
  172. // Repopulate options
  173. paginationSizeSelect.options.length = 0;
  174. for(i = 0; i < pageSizeOptions.length; i++){
  175. put(paginationSizeSelect, "option", pageSizeOptions[i], {
  176. value: pageSizeOptions[i],
  177. selected: this.rowsPerPage === pageSizeOptions[i]
  178. });
  179. }
  180. // Ensure current rowsPerPage value is in options
  181. this._updateRowsPerPageOption();
  182. }else if(!(pageSizeOptions && pageSizeOptions.length) && paginationSizeSelect){
  183. // pageSizeOptions was removed; remove/unhook the drop-down
  184. put(paginationSizeSelect, "!");
  185. this.paginationSizeSelect = null;
  186. this._paginationSizeChangeHandle.remove();
  187. }
  188. },
  189. _setPageSizeOptions: function(pageSizeOptions){
  190. this.pageSizeOptions = pageSizeOptions && pageSizeOptions.sort(function(a, b){
  191. return a - b;
  192. });
  193. this._updatePaginationSizeSelect();
  194. },
  195. _updateRowsPerPageOption: function(){
  196. // summary:
  197. // Ensures that an option for rowsPerPage's value exists in the
  198. // paginationSizeSelect drop-down (if one is rendered).
  199. // Called from buildRendering and _setRowsPerPage.
  200. var rowsPerPage = this.rowsPerPage,
  201. pageSizeOptions = this.pageSizeOptions,
  202. paginationSizeSelect = this.paginationSizeSelect;
  203. if(paginationSizeSelect){
  204. if(arrayUtil.indexOf(pageSizeOptions, rowsPerPage) < 0){
  205. this._setPageSizeOptions(pageSizeOptions.concat([rowsPerPage]));
  206. }else{
  207. paginationSizeSelect.value = "" + rowsPerPage;
  208. }
  209. }
  210. },
  211. _setRowsPerPage: function(rowsPerPage){
  212. this.rowsPerPage = rowsPerPage;
  213. this._updateRowsPerPageOption();
  214. this.gotoPage(1);
  215. },
  216. _updateNavigation: function(focusLink){
  217. // summary:
  218. // Update status and navigation controls based on total count from query
  219. var grid = this,
  220. i18n = this.i18nPagination,
  221. linksNode = this.paginationLinksNode,
  222. currentPage = this._currentPage,
  223. pagingLinks = this.pagingLinks,
  224. paginationNavigationNode = this.paginationNavigationNode,
  225. end = Math.ceil(this._total / this.rowsPerPage),
  226. pagingTextBoxHandle = this._pagingTextBoxHandle;
  227. function pageLink(page, addSpace){
  228. var link;
  229. if(grid.pagingTextBox && page == currentPage && end > 1){
  230. // use a paging text box if enabled instead of just a number
  231. link = put(linksNode, 'input.dgrid-page-input[type=text][value=$]', currentPage);
  232. link.setAttribute("aria-label", i18n.jumpPage);
  233. grid._pagingTextBoxHandle = on(link, "change", function(){
  234. var value = +this.value;
  235. if(!isNaN(value) && value > 0 && value <= end){
  236. grid.gotoPage(+this.value, true);
  237. }
  238. });
  239. }else{
  240. // normal link
  241. link = put(linksNode,
  242. 'span' + (page == currentPage ? '.dgrid-page-disabled' : '') + '.dgrid-page-link',
  243. page + (addSpace ? " " : ""));
  244. link.setAttribute("aria-label", i18n.gotoPage);
  245. link.tabIndex = 0;
  246. }
  247. if(page == currentPage && focusLink){
  248. // focus on it if we are supposed to retain the focus
  249. link.focus();
  250. }
  251. }
  252. if(pagingTextBoxHandle){ pagingTextBoxHandle.remove(); }
  253. linksNode.innerHTML = "";
  254. query(".dgrid-first, .dgrid-previous", paginationNavigationNode).forEach(function(link){
  255. put(link, (currentPage == 1 ? "." : "!") + "dgrid-page-disabled");
  256. });
  257. query(".dgrid-last, .dgrid-next", paginationNavigationNode).forEach(function(link){
  258. put(link, (currentPage >= end ? "." : "!") + "dgrid-page-disabled");
  259. });
  260. if(pagingLinks && end > 0){
  261. // always include the first page (back to the beginning)
  262. pageLink(1, true);
  263. var start = currentPage - pagingLinks;
  264. if(start > 2) {
  265. // visual indication of skipped page links
  266. put(linksNode, "span.dgrid-page-skip", "...");
  267. }else{
  268. start = 2;
  269. }
  270. // now iterate through all the page links we should show
  271. for(var i = start; i < Math.min(currentPage + pagingLinks + 1, end); i++){
  272. pageLink(i, true);
  273. }
  274. if(currentPage + pagingLinks + 1 < end){
  275. put(linksNode, "span.dgrid-page-skip", "...");
  276. }
  277. // last link
  278. if(end > 1){
  279. pageLink(end);
  280. }
  281. }else if(grid.pagingTextBox){
  282. // The pageLink function is also used to create the paging textbox.
  283. pageLink(currentPage);
  284. }
  285. },
  286. refresh: function(){
  287. var self = this;
  288. this.inherited(arguments);
  289. if(!this.store){
  290. console.warn("Pagination requires a store to operate.");
  291. return;
  292. }
  293. // Reset to first page and return promise from gotoPage
  294. return this.gotoPage(1).then(function(results){
  295. // Emit on a separate turn to enable event to be used consistently for
  296. // initial render, regardless of whether the backing store is async
  297. setTimeout(function() {
  298. on.emit(self.domNode, "dgrid-refresh-complete", {
  299. bubbles: true,
  300. cancelable: false,
  301. grid: self,
  302. results: results // QueryResults object (may be a wrapped promise)
  303. });
  304. }, 0);
  305. return results;
  306. });
  307. },
  308. _onNotification: function(rows){
  309. if(rows.length !== this._rowsOnPage){
  310. // Refresh the current page to maintain correct number of rows on page
  311. this.gotoPage(this._currentPage);
  312. }
  313. },
  314. renderArray: function(results, beforeNode){
  315. var grid = this,
  316. rows = this.inherited(arguments);
  317. // Make sure _lastCollection is cleared (due to logic in List)
  318. this._lastCollection = null;
  319. if(!beforeNode){
  320. if(this._topLevelRequest){
  321. // Cancel previous async request that didn't finish
  322. this._topLevelRequest.cancel();
  323. delete this._topLevelRequest;
  324. }
  325. if (typeof results.cancel === "function") {
  326. // Store reference to new async request in progress
  327. this._topLevelRequest = results;
  328. }
  329. Deferred.when(results, function(){
  330. if(grid._topLevelRequest){
  331. // Remove reference to request now that it's finished
  332. delete grid._topLevelRequest;
  333. }
  334. });
  335. }
  336. return rows;
  337. },
  338. insertRow: function(){
  339. var oldNodes = this._oldPageNodes,
  340. row = this.inherited(arguments);
  341. if(oldNodes && row === oldNodes[row.id]){
  342. // If the previous row was reused, avoid removing it in cleanup
  343. delete oldNodes[row.id];
  344. }
  345. return row;
  346. },
  347. gotoPage: function(page, focusLink){
  348. // summary:
  349. // Loads the given page. Note that page numbers start at 1.
  350. var grid = this,
  351. dfd = new Deferred();
  352. var result = this._trackError(function(){
  353. var count = grid.rowsPerPage,
  354. start = (page - 1) * count,
  355. options = lang.mixin(grid.get("queryOptions"), {
  356. start: start,
  357. count: count
  358. // current sort is also included by get("queryOptions")
  359. }),
  360. results,
  361. contentNode = grid.contentNode,
  362. loadingNode,
  363. oldNodes,
  364. children,
  365. i,
  366. len;
  367. if(grid.showLoadingMessage){
  368. cleanupContent(grid);
  369. loadingNode = grid.loadingNode = put(contentNode, "div.dgrid-loading");
  370. loadingNode.innerHTML = grid.loadingMessage;
  371. }else{
  372. // Reference nodes to be cleared later, rather than now;
  373. // iterate manually since IE < 9 doesn't like slicing HTMLCollections
  374. grid._oldPageNodes = oldNodes = {};
  375. children = contentNode.children;
  376. for(i = 0, len = children.length; i < len; i++){
  377. oldNodes[children[i].id] = children[i];
  378. }
  379. // Also reference the current page's observer (if any)
  380. grid._oldPageObserver = grid.observers.pop();
  381. }
  382. // set flag to deactivate pagination event handlers until loaded
  383. grid._isLoading = true;
  384. // Run new query and pass it into renderArray
  385. results = grid.store.query(grid.query, options);
  386. Deferred.when(grid.renderArray(results, null, options), function(rows){
  387. cleanupLoading(grid);
  388. // Reset scroll Y-position now that new page is loaded.
  389. grid.scrollTo({ y: 0 });
  390. Deferred.when(results.total, function(total){
  391. if(!total){
  392. if(grid.noDataNode){
  393. put(grid.noDataNode, "!");
  394. delete grid.noDataNode;
  395. }
  396. // If there are no results, display the no data message.
  397. grid.noDataNode = put(grid.contentNode, "div.dgrid-no-data");
  398. grid.noDataNode.innerHTML = grid.noDataMessage;
  399. }
  400. // Update status text based on now-current page and total.
  401. grid.paginationStatusNode.innerHTML = string.substitute(grid.i18nPagination.status, {
  402. start: Math.min(start + 1, total),
  403. end: Math.min(total, start + count),
  404. total: total
  405. });
  406. grid._total = total;
  407. grid._currentPage = page;
  408. grid._rowsOnPage = rows.length;
  409. // It's especially important that _updateNavigation is called only
  410. // after renderArray is resolved as well (to prevent jumping).
  411. grid._updateNavigation(focusLink);
  412. });
  413. if (has("ie") < 7 || (has("ie") && has("quirks"))) {
  414. // call resize in old IE in case grid is set to height: auto
  415. grid.resize();
  416. }
  417. dfd.resolve(results);
  418. }, function(error){
  419. cleanupLoading(grid);
  420. dfd.reject(error);
  421. });
  422. return dfd.promise;
  423. });
  424. if (!result) {
  425. // A synchronous error occurred; reject the promise.
  426. dfd.reject();
  427. }
  428. var self = this;
  429. dfd.promise.then(function (results) {
  430. // Emit on a separate turn to enable event to be used consistently for
  431. // initial render, regardless of whether the backing store is async
  432. setTimeout(function () {
  433. on.emit(self.domNode, "dgrid-page-complete", {
  434. bubbles: true,
  435. cancelable: false,
  436. grid: self,
  437. page: page,
  438. results: results // QueryResults object (may be a wrapped promise)
  439. });
  440. }, 0);
  441. return results;
  442. });
  443. return dfd.promise;
  444. }
  445. });
  446. });