MDL-61138 javascript: add paged content widget
[moodle.git] / lib / amd / src / paged_content_pages.js
blob4fbbb104622b64ca0649a44d244f0c6cea7aaea2
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.
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/>.
16 /**
17  * Javascript for showing/hiding pages of content.
18  *
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
22  */
23 define(
24     [
25         'jquery',
26         'core/templates',
27         'core/notification',
28         'core/paged_content_events'
29     ],
30     function(
31         $,
32         Templates,
33         Notification,
34         PagedContentEvents
35     ) {
37     var SELECTORS = {
38         ROOT: '[data-region="page-container"]',
39         PAGE_REGION: '[data-region="paged-content-page"]',
40         ACTIVE_PAGE_REGION: '[data-region="paged-content-page"].active'
41     };
43     var TEMPLATES = {
44         PAGING_CONTENT_ITEM: 'core/paged_content_page',
45         LOADING: 'core/overlay_loading'
46     };
48     /**
49      * Find a page by the number.
50      *
51      * @param {object} root The root element.
52      * @param {Number} pageNumber The number of the page to be found.
53      * @returns {jQuery} The page.
54      */
55     var findPage = function(root, pageNumber) {
56         return root.find('[data-page="' + pageNumber + '"]');
57     };
59     /**
60      * Show the loading spinner until the returned deferred is resolved by the
61      * calling code.
62      *
63      * @param {object} root The root element.
64      * @returns {promise} The page.
65      */
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
75                 // rendering.
76                 var timerId = setTimeout(function() {
77                     root.css('position', 'relative');
78                     loadingSpinner.appendTo(root);
79                 }, 100);
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', '');
87                     return;
88                 });
90                 return;
91             })
92             .fail(Notification.exception);
94         return deferred;
95     };
97     /**
98      * Render the result of the page promise in a paged content page.
99      *
100      * This function returns a promise that is resolved with the new paged content
101      * page.
102      *
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.
107      */
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, {
114                 page: pageNumber,
115                 content: html
116             })
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);
123                 return;
124             })
125             .fail(function(exception) {
126                 deferred.reject(exception);
127             })
128             .fail(Notification.exception);
130             return;
131         })
132         .fail(function(exception) {
133             deferred.reject(exception);
134             return;
135         })
136         .fail(Notification.exception);
138         return deferred;
139     };
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      * @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.
170      */
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);
183             } else {
184                 newPageData.push(pageData);
185             }
186         });
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
194             // content pages.
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);
198             });
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)
202                 .then(function() {
203                     var newPages = Array.prototype.slice.call(arguments);
204                     // Resolve the promise with the list of newly rendered pages.
205                     newPagesPromise.resolve(newPages);
206                     return;
207                 })
208                 .fail(function(exception) {
209                     newPagesPromise.reject(exception);
210                     return;
211                 })
212                 .fail(Notification.exception);
213         } else {
214             // If there aren't any pages to load then immediately resolve the promise.
215             newPagesPromise.resolve([]);
216         }
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');
228             });
230             return;
231         })
232         .fail(Notification.exception)
233         .always(function() {
234             loadingPromise.resolve();
235         });
236     };
238     /**
239      * Initialise the module to listen for SHOW_PAGES events and render the
240      * appropriate pages using the provided renderPagesContentCallback function.
241      *
242      * The renderPagesContentCallback is provided a list of data to know which
243      * pages to load.
244      * E.g. the data to load 2 pages might look like:
245      * [
246      *      {
247      *          pageNumber: 1,
248      *          limit: 5,
249      *          offset: 0
250      *      },
251      *      {
252      *          pageNumber: 2,
253      *          limit: 5,
254      *          offset: 5
255      *      }
256      * ]
257      *
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.
260      *
261      * If the renderPagesContentCallback is not provided then it is assumed that
262      * all pages have been rendered prior to initialising this module.
263      *
264      * The event element is the element to listen for the paged content events on.
265      *
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.
269      */
270     var init = function(root, eventElement, renderPagesContentCallback) {
271         root = $(root);
272         eventElement = $(eventElement);
274         eventElement.on(PagedContentEvents.SHOW_PAGES, function(e, pagesData) {
275             showPages(root, pagesData, renderPagesContentCallback);
276         });
277     };
279     return {
280         init: init,
281         rootSelector: SELECTORS.ROOT,
282     };