Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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
});