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