lib/amd/src/paged_content_pages.js

  1. // This file is part of Moodle - http://moodle.org/
  2. //
  3. // Moodle is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. //
  8. // Moodle is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  15. /**
  16. * Javascript for showing/hiding pages of content.
  17. *
  18. * @module core/paged_content_pages
  19. * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. define(
  23. [
  24. 'jquery',
  25. 'core/templates',
  26. 'core/notification',
  27. 'core/pubsub',
  28. 'core/paged_content_events',
  29. 'core/pending',
  30. ],
  31. function(
  32. $,
  33. Templates,
  34. Notification,
  35. PubSub,
  36. PagedContentEvents,
  37. Pending
  38. ) {
  39. var SELECTORS = {
  40. ROOT: '[data-region="page-container"]',
  41. PAGE_REGION: '[data-region="paged-content-page"]',
  42. ACTIVE_PAGE_REGION: '[data-region="paged-content-page"].active'
  43. };
  44. var TEMPLATES = {
  45. PAGING_CONTENT_ITEM: 'core/paged_content_page',
  46. LOADING: 'core/overlay_loading'
  47. };
  48. var PRELOADING_GRACE_PERIOD = 300;
  49. /**
  50. * Find a page by the number.
  51. *
  52. * @param {object} root The root element.
  53. * @param {Number} pageNumber The number of the page to be found.
  54. * @returns {jQuery} The page.
  55. */
  56. var findPage = function(root, pageNumber) {
  57. return root.find('[data-page="' + pageNumber + '"]');
  58. };
  59. /**
  60. * Show the loading spinner until the returned deferred is resolved by the
  61. * calling code.
  62. *
  63. * The loading spinner is only rendered after a short grace period to avoid
  64. * having it flash up briefly in the interface.
  65. *
  66. * @param {object} root The root element.
  67. * @returns {promise} The page.
  68. */
  69. var startLoading = function(root) {
  70. var deferred = $.Deferred();
  71. root.attr('aria-busy', true);
  72. var pendingPromise = new Pending('core/paged_content_pages:startLoading');
  73. Templates.render(TEMPLATES.LOADING, {visible: true})
  74. .then(function(html) {
  75. var loadingSpinner = $(html);
  76. // Put this in a timer to give the calling code 300 milliseconds
  77. // to render the content before we show the loading spinner. This
  78. // helps prevent a loading icon flicker on close to instant
  79. // rendering.
  80. var timerId = setTimeout(function() {
  81. root.css('position', 'relative');
  82. loadingSpinner.appendTo(root);
  83. }, PRELOADING_GRACE_PERIOD);
  84. deferred.always(function() {
  85. clearTimeout(timerId);
  86. // Remove the loading spinner when our deferred is resolved
  87. // by the calling code.
  88. loadingSpinner.remove();
  89. root.css('position', '');
  90. root.removeAttr('aria-busy');
  91. pendingPromise.resolve();
  92. return;
  93. });
  94. return;
  95. })
  96. .fail(Notification.exception);
  97. return deferred;
  98. };
  99. /**
  100. * Render the result of the page promise in a paged content page.
  101. *
  102. * This function returns a promise that is resolved with the new paged content
  103. * page.
  104. *
  105. * @param {object} root The root element.
  106. * @param {promise} pagePromise The promise resolved with HTML and JS to render in the page.
  107. * @param {Number} pageNumber The page number.
  108. * @returns {promise} The page.
  109. */
  110. var renderPagePromise = function(root, pagePromise, pageNumber) {
  111. var deferred = $.Deferred();
  112. pagePromise.then(function(html, pageJS) {
  113. pageJS = pageJS || '';
  114. // When we get the contents to be rendered we can pass it in as the
  115. // content for a new page.
  116. Templates.render(TEMPLATES.PAGING_CONTENT_ITEM, {
  117. page: pageNumber,
  118. content: html
  119. })
  120. .then(function(html) {
  121. // Make sure the JS we got from the page promise is being added
  122. // to the page when we render the page.
  123. Templates.appendNodeContents(root, html, pageJS);
  124. var page = findPage(root, pageNumber);
  125. deferred.resolve(page);
  126. return;
  127. })
  128. .fail(function(exception) {
  129. deferred.reject(exception);
  130. })
  131. .fail(Notification.exception);
  132. return;
  133. })
  134. .fail(function(exception) {
  135. deferred.reject(exception);
  136. return;
  137. })
  138. .fail(Notification.exception);
  139. return deferred.promise();
  140. };
  141. /**
  142. * Make one or more pages visible based on the SHOW_PAGES event. The show
  143. * pages event provides data containing which pages should be shown as well
  144. * as the limit and offset values for loading the items for each of those pages.
  145. *
  146. * The renderPagesContentCallback is provided this list of data to know which
  147. * pages to load. E.g. the data to load 2 pages might look like:
  148. * [
  149. * {
  150. * pageNumber: 1,
  151. * limit: 5,
  152. * offset: 0
  153. * },
  154. * {
  155. * pageNumber: 2,
  156. * limit: 5,
  157. * offset: 5
  158. * }
  159. * ]
  160. *
  161. * The renderPagesContentCallback should return an array of promises, one for
  162. * each page in the pages data, that is resolved with the HTML and JS for that page.
  163. *
  164. * If the renderPagesContentCallback is not provided then it is assumed that
  165. * all pages have been rendered prior to initialising this module.
  166. *
  167. * This function triggers the PAGES_SHOWN event after the pages have been rendered.
  168. *
  169. * @param {object} root The root element.
  170. * @param {Number} pagesData The data for which pages need to be visible.
  171. * @param {string} id A unique id for this instance.
  172. * @param {function} renderPagesContentCallback Render pages content.
  173. */
  174. var showPages = function(root, pagesData, id, renderPagesContentCallback) {
  175. var pendingPromise = new Pending('core/paged_content_pages:showPages');
  176. var existingPages = [];
  177. var newPageData = [];
  178. var newPagesPromise = $.Deferred();
  179. var shownewpage = true;
  180. // Check which of the pages being requests have previously been rendered
  181. // so that we only ask for new pages to be rendered by the callback.
  182. pagesData.forEach(function(pageData) {
  183. var pageNumber = pageData.pageNumber;
  184. var existingPage = findPage(root, pageNumber);
  185. if (existingPage.length) {
  186. existingPages.push(existingPage);
  187. } else {
  188. newPageData.push(pageData);
  189. }
  190. });
  191. if (newPageData.length && typeof renderPagesContentCallback === 'function') {
  192. // If we have pages we haven't previously seen then ask the client code
  193. // to render them for us by calling the callback.
  194. var promises = renderPagesContentCallback(newPageData, {
  195. allItemsLoaded: function(lastPageNumber) {
  196. PubSub.publish(id + PagedContentEvents.ALL_ITEMS_LOADED, lastPageNumber);
  197. }
  198. });
  199. // After the client has finished rendering each of the pages being asked
  200. // for then begin our rendering process to put that content into paged
  201. // content pages.
  202. var renderPagePromises = promises.map(function(promise, index) {
  203. // Create our promise for when our rendering will be completed.
  204. return renderPagePromise(root, promise, newPageData[index].pageNumber);
  205. });
  206. // After each of our rendering promises have been completed then we can
  207. // give all of the new pages to the next bit of code for handling.
  208. $.when.apply($, renderPagePromises)
  209. .then(function() {
  210. var newPages = Array.prototype.slice.call(arguments);
  211. // Resolve the promise with the list of newly rendered pages.
  212. newPagesPromise.resolve(newPages);
  213. return;
  214. })
  215. .fail(function(exception) {
  216. newPagesPromise.reject(exception);
  217. return;
  218. })
  219. .fail(Notification.exception);
  220. } else {
  221. // If there aren't any pages to load then immediately resolve the promise.
  222. newPagesPromise.resolve([]);
  223. }
  224. var loadingPromise = startLoading(root);
  225. newPagesPromise.then(function(newPages) {
  226. // Once all of the new pages have been created then add them to any
  227. // existing pages we have.
  228. var pagesToShow = existingPages.concat(newPages);
  229. // Hide all existing pages.
  230. root.find(SELECTORS.PAGE_REGION).addClass('hidden');
  231. // Show each of the pages that were requested.;
  232. pagesToShow.forEach(function(page) {
  233. if (shownewpage) {
  234. page.removeClass('hidden');
  235. }
  236. });
  237. return;
  238. })
  239. .then(function() {
  240. // Let everything else know we've displayed the pages.
  241. PubSub.publish(id + PagedContentEvents.PAGES_SHOWN, pagesData);
  242. return;
  243. })
  244. .fail(Notification.exception)
  245. .always(function() {
  246. loadingPromise.resolve();
  247. pendingPromise.resolve();
  248. })
  249. .catch();
  250. };
  251. /**
  252. * Initialise the module to listen for SHOW_PAGES events and render the
  253. * appropriate pages using the provided renderPagesContentCallback function.
  254. *
  255. * The renderPagesContentCallback is provided a list of data to know which
  256. * pages to load.
  257. * E.g. the data to load 2 pages might look like:
  258. * [
  259. * {
  260. * pageNumber: 1,
  261. * limit: 5,
  262. * offset: 0
  263. * },
  264. * {
  265. * pageNumber: 2,
  266. * limit: 5,
  267. * offset: 5
  268. * }
  269. * ]
  270. *
  271. * The renderPagesContentCallback should return an array of promises, one for
  272. * each page in the pages data, that is resolved with the HTML and JS for that page.
  273. *
  274. * If the renderPagesContentCallback is not provided then it is assumed that
  275. * all pages have been rendered prior to initialising this module.
  276. *
  277. * The event element is the element to listen for the paged content events on.
  278. *
  279. * @param {object} root The root element.
  280. * @param {string} id A unique id for this instance.
  281. * @param {function} renderPagesContentCallback Render pages content.
  282. */
  283. var init = function(root, id, renderPagesContentCallback) {
  284. root = $(root);
  285. PubSub.subscribe(id + PagedContentEvents.SHOW_PAGES, function(pagesData) {
  286. showPages(root, pagesData, id, renderPagesContentCallback);
  287. });
  288. PubSub.subscribe(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, function() {
  289. // If the items per page limit was changed then we need to clear our content
  290. // the load new values based on the new limit.
  291. root.empty();
  292. });
  293. };
  294. return {
  295. init: init,
  296. rootSelector: SELECTORS.ROOT,
  297. };
  298. });