1 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
17 * Javascript for showing/hiding pages of content.
19 * @module core/paged_content_pages
20 * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 'core/paged_content_events'
38 ROOT: '[data-region="page-container"]',
39 PAGE_REGION: '[data-region="paged-content-page"]',
40 ACTIVE_PAGE_REGION: '[data-region="paged-content-page"].active'
44 PAGING_CONTENT_ITEM: 'core/paged_content_page',
45 LOADING: 'core/overlay_loading'
49 * Find a page by the number.
51 * @param {object} root The root element.
52 * @param {Number} pageNumber The number of the page to be found.
53 * @returns {jQuery} The page.
55 var findPage = function(root, pageNumber) {
56 return root.find('[data-page="' + pageNumber + '"]');
60 * Show the loading spinner until the returned deferred is resolved by the
63 * @param {object} root The root element.
64 * @returns {promise} The page.
66 var startLoading = function(root) {
67 var deferred = $.Deferred();
69 Templates.render(TEMPLATES.LOADING, {visible: true})
70 .then(function(html) {
71 var loadingSpinner = $(html);
72 // Put this in a timer to give the calling code 100 milliseconds
73 // to render the content before we show the loading spinner. This
74 // helps prevent a loading icon flicker on close to instant
76 var timerId = setTimeout(function() {
77 root.css('position', 'relative');
78 loadingSpinner.appendTo(root);
81 deferred.always(function() {
82 clearTimeout(timerId);
83 // Remove the loading spinner when our deferred is resolved
84 // by the calling code.
85 loadingSpinner.remove();
86 root.css('position', '');
92 .fail(Notification.exception);
98 * Render the result of the page promise in a paged content page.
100 * This function returns a promise that is resolved with the new paged content
103 * @param {object} root The root element.
104 * @param {promise} pagePromise The promise resolved with HTML and JS to render in the page.
105 * @param {int} pageNumber The page number.
106 * @returns {promise} The page.
108 var renderPagePromise = function(root, pagePromise, pageNumber) {
109 var deferred = $.Deferred();
110 pagePromise.then(function(html, pageJS) {
111 // When we get the contents to be rendered we can pass it in as the
112 // content for a new page.
113 Templates.render(TEMPLATES.PAGING_CONTENT_ITEM, {
117 .then(function(html) {
118 // Make sure the JS we got from the page promise is being added
119 // to the page when we render the page.
120 Templates.appendNodeContents(root, html, pageJS);
121 var page = findPage(root, pageNumber);
122 deferred.resolve(page);
125 .fail(function(exception) {
126 deferred.reject(exception);
128 .fail(Notification.exception);
132 .fail(function(exception) {
133 deferred.reject(exception);
136 .fail(Notification.exception);
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.
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:
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.
164 * If the renderPagesContentCallback is not provided then it is assumed that
165 * all pages have been rendered prior to initialising this module.
167 * @param {object} root The root element.
168 * @param {Number} pagesData The data for which pages need to be visible.
169 * @param {function} renderPagesContentCallback Render pages content.
171 var showPages = function(root, pagesData, renderPagesContentCallback) {
172 var existingPages = [];
173 var newPageData = [];
174 var newPagesPromise = $.Deferred();
176 // Check which of the pages being requests have previously been rendered
177 // so that we only ask for new pages to be rendered by the callback.
178 pagesData.forEach(function(pageData) {
179 var pageNumber = pageData.pageNumber;
180 var existingPage = findPage(root, pageNumber);
181 if (existingPage.length) {
182 existingPages.push(existingPage);
184 newPageData.push(pageData);
188 if (newPageData.length && typeof renderPagesContentCallback === 'function') {
189 // If we have pages we haven't previously seen then ask the client code
190 // to render them for us by calling the callback.
191 var promises = renderPagesContentCallback(newPageData);
192 // After the client has finished rendering each of the pages being asked
193 // for then begin our rendering process to put that content into paged
195 var renderPagePromises = promises.map(function(promise, index) {
196 // Create our promise for when our rendering will be completed.
197 return renderPagePromise(root, promise, newPageData[index].pageNumber);
199 // After each of our rendering promises have been completed then we can
200 // give all of the new pages to the next bit of code for handling.
201 $.when.apply($, renderPagePromises)
203 var newPages = Array.prototype.slice.call(arguments);
204 // Resolve the promise with the list of newly rendered pages.
205 newPagesPromise.resolve(newPages);
208 .fail(function(exception) {
209 newPagesPromise.reject(exception);
212 .fail(Notification.exception);
214 // If there aren't any pages to load then immediately resolve the promise.
215 newPagesPromise.resolve([]);
218 var loadingPromise = startLoading(root);
219 newPagesPromise.then(function(newPages) {
220 // Once all of the new pages have been created then add them to any
221 // existing pages we have.
222 var pagesToShow = existingPages.concat(newPages);
223 // Hide all existing pages.
224 root.find(SELECTORS.PAGE_REGION).addClass('hidden');
225 // Show each of the pages that were requested.
226 pagesToShow.forEach(function(page) {
227 page.removeClass('hidden');
232 .fail(Notification.exception)
234 loadingPromise.resolve();
239 * Initialise the module to listen for SHOW_PAGES events and render the
240 * appropriate pages using the provided renderPagesContentCallback function.
242 * The renderPagesContentCallback is provided a list of data to know which
244 * E.g. the data to load 2 pages might look like:
258 * The renderPagesContentCallback should return an array of promises, one for
259 * each page in the pages data, that is resolved with the HTML and JS for that page.
261 * If the renderPagesContentCallback is not provided then it is assumed that
262 * all pages have been rendered prior to initialising this module.
264 * The event element is the element to listen for the paged content events on.
266 * @param {object} root The root element.
267 * @param {object} eventElement The element to listen for events on.
268 * @param {function} renderPagesContentCallback Render pages content.
270 var init = function(root, eventElement, renderPagesContentCallback) {
272 eventElement = $(eventElement);
274 eventElement.on(PagedContentEvents.SHOW_PAGES, function(e, pagesData) {
275 showPages(root, pagesData, renderPagesContentCallback);
281 rootSelector: SELECTORS.ROOT,