PageRenderTime 139ms CodeModel.GetById 38ms RepoModel.GetById 0ms app.codeStats 0ms

/client_apps/canvas_quizzes/apps/events/js/mixins/paginated_collection.js

https://gitlab.com/ykazemi/canvas-lms
JavaScript | 205 lines | 205 code | 0 blank | 0 comment | 0 complexity | 2d54db054a3354997cc78c73c9cc48b2 MD5 | raw file
  1. define(function(require) {
  2. var _ = require('lodash');
  3. var find = _.find;
  4. var RE_EXTRACT_LINK = /<([^>]+)>; rel="([^"]+)",?\s*/g;
  5. var RE_EXTRACT_PP = /per_page=(\d+)/;
  6. // Extract pagination meta from a JSON-API payload inside the
  7. // "meta.pagination" set.
  8. var parseJsonApiPagination = function(respMeta, meta) {
  9. if (!meta) meta = {};
  10. meta.perPage = respMeta.per_page;
  11. meta.hasMore = !!respMeta.next;
  12. meta.nextPage = meta.hasMore ? respMeta.page + 1 : undefined;
  13. meta.count = respMeta.count;
  14. return meta;
  15. };
  16. // Extract pagination from the Link header.
  17. //
  18. // Here's a good reference:
  19. // https://developer.github.com/guides/traversing-with-pagination/
  20. var parseLinkPagination = function(linkHeader, meta) {
  21. var match, nextLink, lastLink;
  22. var links = [];
  23. if (!meta) meta = {};
  24. while (match = RE_EXTRACT_LINK.exec(linkHeader)) {
  25. links.push({
  26. rel: match[2],
  27. href: match[1],
  28. page: parseInt(/page=(\d+)/.exec(match[1])[1], 10)
  29. });
  30. }
  31. nextLink = find(links, { rel: 'next' });
  32. lastLink = find(links, { rel: 'last' });
  33. meta.perPage = parseInt((RE_EXTRACT_PP.exec(linkHeader) || [])[1] || 0, 10);
  34. meta.hasMore = !!nextLink;
  35. meta.nextPage = meta.hasMore ? nextLink.page : undefined;
  36. // Link header does not provide us with an accurate count of objects, so
  37. // we'll estimate it if we know how many we get per page, and we know the
  38. // index of the last page:
  39. if (lastLink) {
  40. meta.count = meta.perPage * lastLink.page;
  41. }
  42. return meta;
  43. };
  44. /**
  45. * @class Events.Mixins.PaginatedCollection
  46. * @extends {Backbone.Collection}
  47. *
  48. * Adds support for utilizing JSON-API pagination meta-data to allow fetching
  49. * any page of a paginated API resource, or all pages at once.
  50. *
  51. * Usage example:
  52. *
  53. * var Collection = Backbone.Collection.extend({
  54. * // install the mixin
  55. * constructor: function() {
  56. * PaginatedCollection(this);
  57. * return Backbone.Collection.apply(this, arguments);
  58. * },
  59. *
  60. * url: function() {
  61. * return '/users';
  62. * }
  63. * });
  64. *
  65. * var collection = new Collection();
  66. *
  67. * collection.fetch(); // /users
  68. * collection.length; // 10
  69. *
  70. * collection.fetchNext(); // /users?page=2
  71. * collection.length; // 20
  72. *
  73. * // load all available users in one go:
  74. * // /users?page=1
  75. * // ...
  76. * // /users?page=5
  77. * collection.fetchAll().then(function() {
  78. * collection.length; // 50
  79. * });
  80. */
  81. var Mixin = {
  82. /**
  83. * Fetch the next page, if available.
  84. *
  85. * @param {Object} options
  86. * Normal options you'd pass to Backbone.Collection#fetch().
  87. *
  88. * @param {Number} [options.page]
  89. * If specified, exactly that page will be fetched, otherwise we'll
  90. * use the current cursor (or 1).
  91. *
  92. * @return {Promise}
  93. * Resolves when the page has been loaded and the pagination meta
  94. * parsed.
  95. */
  96. fetchNext: function(options) {
  97. var meta = this._paginationMeta;
  98. if (!options) {
  99. options = {};
  100. }
  101. else if (options.hasOwnProperty('xhr')) {
  102. delete options.xhr;
  103. }
  104. if (!options.data) {
  105. options.data = {};
  106. }
  107. options.data.page = options.page || meta.nextPage;
  108. options.success = function(payload, statusText, xhr) {
  109. var header = xhr.getResponseHeader('Link');
  110. if (payload.meta && payload.meta.pagination) {
  111. parseJsonApiPagination(payload.meta.pagination, meta);
  112. }
  113. else if (header) {
  114. parseLinkPagination(header, meta);
  115. }
  116. this.add(payload, { parse: true /* always parse */ });
  117. }.bind(this);
  118. return this.sync('read', this, options);
  119. },
  120. /**
  121. * @return {Boolean}
  122. * Whether there's more data (that we know of) to pull in from the
  123. * server.
  124. */
  125. canLoadMore: function() {
  126. return !!this._paginationMeta.hasMore;
  127. },
  128. /**
  129. * Fetch all available pages.
  130. *
  131. * @param {Object} options
  132. * Options to pass to #fetchNext. "page" is not allowed here and
  133. * will be ignored if specified.
  134. *
  135. * @return {Promise}
  136. * Resolves when *all* pages have been loaded.
  137. */
  138. fetchAll: function(options) {
  139. var meta = this._paginationMeta;
  140. if (!options) {
  141. options = {};
  142. }
  143. else if (options.hasOwnProperty('page')) {
  144. console.warn(
  145. 'You may not specify a page when fetching all pages. ' +
  146. 'Resetting cursor to 1.'
  147. );
  148. delete options.page;
  149. }
  150. if (options.reset) {
  151. this.reset(null, { silent: true });
  152. }
  153. meta.nextPage = 1;
  154. return (function fetch(collection) {
  155. return collection.fetchNext(options).then(function() {
  156. if (meta.hasMore) {
  157. return fetch(collection);
  158. } else {
  159. return collection;
  160. }
  161. });
  162. })(this);
  163. },
  164. /** @private */
  165. _resetPaginationMeta: function() {
  166. this._paginationMeta = {};
  167. },
  168. };
  169. return function applyMixin(collection) {
  170. collection.fetchNext = Mixin.fetchNext;
  171. collection.fetchAll = Mixin.fetchAll;
  172. collection._resetPaginationMeta = Mixin._resetPaginationMeta;
  173. collection.on('reset', collection._resetPaginationMeta, collection);
  174. collection._resetPaginationMeta();
  175. };
  176. });