c8eb0a6205ce0140497efef82b25e4435f9a905e
[moodle.git] / lib / amd / src / paged_content_factory.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/>.
16 /**
17  * Factory to create a paged content widget.
18  *
19  * @module     core/paged_content_factory
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',
29     'core/paged_content_events',
30     'core/pubsub'
31 ],
32 function(
33     $,
34     Templates,
35     Notification,
36     PagedContent,
37     PagedContentEvents,
38     PubSub
39 ) {
40     var TEMPLATES = {
41         PAGED_CONTENT: 'core/paged_content'
42     };
44     var DEFAULT = {
45         ITEMS_PER_PAGE_SINGLE: 25,
46         ITEMS_PER_PAGE_ARRAY: [25, 50, 100, 0],
47         MAX_PAGES: 3
48     };
50     /**
51      * Get the default context to render the paged content mustache
52      * template.
53      *
54      * @return {object}
55      */
56     var getDefaultTemplateContext = function() {
57         return {
58             pagingbar: false,
59             pagingdropdown: false,
60             skipjs: true,
61             ignorecontrolwhileloading: true,
62             controlplacementbottom: false
63         };
64     };
66     /**
67      * Get the default context to render the paging bar mustache template.
68      *
69      * @return {object}
70      */
71     var getDefaultPagingBarTemplateContext = function() {
72         return {
73             showitemsperpageselector: false,
74             itemsperpage: 35,
75             previous: true,
76             next: true,
77             activepagenumber: 1,
78             hidecontrolonsinglepage: true,
79             pages: []
80         };
81     };
83     /**
84      * Calculate the number of pages required for the given number of items and
85      * how many of each item should appear on a page.
86      *
87      * @param  {Number} numberOfItems How many items in total.
88      * @param  {Number} itemsPerPage  How many items will be shown per page.
89      * @return {Number} The number of pages required.
90      */
91     var calculateNumberOfPages = function(numberOfItems, itemsPerPage) {
92         var numberOfPages = 1;
94         if (numberOfItems > 0) {
95             var partial = numberOfItems % itemsPerPage;
97             if (partial) {
98                 numberOfItems -= partial;
99                 numberOfPages = (numberOfItems / itemsPerPage) + 1;
100             } else {
101                 numberOfPages = numberOfItems / itemsPerPage;
102             }
103         }
105         return numberOfPages;
106     };
108     /**
109      * Build the context for the paging bar template when we have a known number
110      * of items.
111      *
112      * @param {Number} numberOfItems How many items in total.
113      * @param {Number} itemsPerPage  How many items will be shown per page.
114      * @return {object} Mustache template
115      */
116     var buildPagingBarTemplateContextKnownLength = function(numberOfItems, itemsPerPage) {
117         if (itemsPerPage === null) {
118             itemsPerPage = DEFAULT.ITEMS_PER_PAGE_SINGLE;
119         }
121         if ($.isArray(itemsPerPage)) {
122             // If we're given a total number of pages then we don't support a variable
123             // set of items per page so just use the first one.
124             itemsPerPage = itemsPerPage[0];
125         }
127         var context = getDefaultPagingBarTemplateContext();
128         context.itemsperpage = itemsPerPage;
129         var numberOfPages = calculateNumberOfPages(numberOfItems, itemsPerPage);
131         for (var i = 1; i <= numberOfPages; i++) {
132             var page = {
133                 number: i,
134                 page: "" + i,
135             };
137             // Make the first page active by default.
138             if (i === 1) {
139                 page.active = true;
140             }
142             context.pages.push(page);
143         }
145         return context;
146     };
148     /**
149      * Convert the itemsPerPage value into a format applicable for the mustache template.
150      * The given value can be either a single integer or an array of integers / objects.
151      *
152      * E.g.
153      * In: [5, 10]
154      * out: [{value: 5, active: true}, {value: 10, active: false}]
155      *
156      * In: [5, {value: 10, active: true}]
157      * Out: [{value: 5, active: false}, {value: 10, active: true}]
158      *
159      * In: [{value: 5, active: false}, {value: 10, active: true}]
160      * Out: [{value: 5, active: false}, {value: 10, active: true}]
161      *
162      * @param {int|int[]} itemsPerPage Options for number of items per page.
163      * @return {int|array}
164      */
165     var buildItemsPerPagePagingBarContext = function(itemsPerPage) {
166         if ($.isArray(itemsPerPage)) {
167             // Convert the array into a format accepted by the template.
168             var context = itemsPerPage.map(function(num) {
169                 if (typeof num === 'number') {
170                     // If the item is just a plain number then convert it into
171                     // an object with value and active keys.
172                     return {
173                         value: num,
174                         active: false
175                     };
176                 } else {
177                     // Otherwise we assume the caller has specified things correctly.
178                     return num;
179                 }
180             });
182             var activeItems = context.filter(function(item) {
183                 return item.active;
184             });
186             // Default the first item to active if one hasn't been specified.
187             if (!activeItems.length) {
188                 context[0].active = true;
189             }
191             return context;
192         } else {
193             return itemsPerPage;
194         }
195     };
197     /**
198      * Build the context for the paging bar template when we have an unknown
199      * number of items.
200      *
201      * @param {Number} itemsPerPage  How many items will be shown per page.
202      * @return {object} Mustache template
203      */
204     var buildPagingBarTemplateContextUnknownLength = function(itemsPerPage) {
205         if (itemsPerPage === null) {
206             itemsPerPage = DEFAULT.ITEMS_PER_PAGE_ARRAY;
207         }
209         var context = getDefaultPagingBarTemplateContext();
210         context.itemsperpage = buildItemsPerPagePagingBarContext(itemsPerPage);
211         context.showitemsperpageselector = $.isArray(itemsPerPage);
213         return context;
214     };
216     /**
217      * Build the context to render the paging bar template with based on the number
218      * of pages to show.
219      *
220      * @param  {int|null} numberOfItems How many items are there total.
221      * @param  {int|null} itemsPerPage  How many items will be shown per page.
222      * @return {object} The template context.
223      */
224     var buildPagingBarTemplateContext = function(numberOfItems, itemsPerPage) {
225         if (numberOfItems) {
226             return buildPagingBarTemplateContextKnownLength(numberOfItems, itemsPerPage);
227         } else {
228             return buildPagingBarTemplateContextUnknownLength(itemsPerPage);
229         }
230     };
232     /**
233      * Build the context to render the paging dropdown template based on the number
234      * of pages to show and items per page.
235      *
236      * This control is rendered with a gradual increase of the items per page to
237      * limit the number of pages in the dropdown. Each page will show twice as much
238      * as the previous page (except for the first two pages).
239      *
240      * By default there will only be 4 pages shown (including the "All" option) unless
241      * a different number of pages is defined using the maxPages config value.
242      *
243      * For example:
244      * Items per page = 25
245      * Would render a dropdown will 4 options:
246      * 25
247      * 50
248      * 100
249      * All
250      *
251      * @param  {Number} itemsPerPage  How many items will be shown per page.
252      * @param  {object} config  Configuration options provided by the client.
253      * @return {object} The template context.
254      */
255     var buildPagingDropdownTemplateContext = function(itemsPerPage, config) {
256         if (itemsPerPage === null) {
257             itemsPerPage = DEFAULT.ITEMS_PER_PAGE_SINGLE;
258         }
260         if ($.isArray(itemsPerPage)) {
261             // If we're given an array for the items per page, rather than a number,
262             // then just use that as the options for the dropdown.
263             return {
264                 options: itemsPerPage
265             };
266         }
268         var context = {
269             options: []
270         };
272         var totalItems = 0;
273         var lastIncrease = 0;
274         var maxPages = DEFAULT.MAX_PAGES;
276         if (config.hasOwnProperty('maxPages')) {
277             maxPages = config.maxPages;
278         }
280         for (var i = 1; i <= maxPages; i++) {
281             var itemCount = 0;
283             if (i <= 2) {
284                 itemCount = itemsPerPage;
285                 lastIncrease = itemsPerPage;
286             } else {
287                 lastIncrease = lastIncrease * 2;
288                 itemCount = lastIncrease;
289             }
291             totalItems += itemCount;
292             var option = {
293                 itemcount: itemCount,
294                 content: totalItems
295             };
297             // Make the first option active by default.
298             if (i === 1) {
299                 option.active = true;
300             }
302             context.options.push(option);
303         }
305         return context;
306     };
308     /**
309      * Build the context to render the paged content template with based on the number
310      * of pages to show, items per page, and configuration option.
311      *
312      * By default the code will render a paging bar for the paging controls unless
313      * otherwise specified in the provided config.
314      *
315      * @param  {int|null} numberOfItems Total number of items.
316      * @param  {int|null|array} itemsPerPage  How many items will be shown per page.
317      * @param  {object} config  Configuration options provided by the client.
318      * @return {object} The template context.
319      */
320     var buildTemplateContext = function(numberOfItems, itemsPerPage, config) {
321         var context = getDefaultTemplateContext();
323         if (config.hasOwnProperty('ignoreControlWhileLoading')) {
324             context.ignorecontrolwhileloading = config.ignoreControlWhileLoading;
325         }
327         if (config.hasOwnProperty('controlPlacementBottom')) {
328             context.controlplacementbottom = config.controlPlacementBottom;
329         }
331         if (config.hasOwnProperty('hideControlOnSinglePage')) {
332             context.hidecontrolonsinglepage = config.hideControlOnSinglePage;
333         }
335         if (config.hasOwnProperty('ariaLabels')) {
336             context.arialabels = config.ariaLabels;
337         }
339         if (config.hasOwnProperty('dropdown') && config.dropdown) {
340             context.pagingdropdown = buildPagingDropdownTemplateContext(itemsPerPage, config);
341         } else {
342             context.pagingbar = buildPagingBarTemplateContext(numberOfItems, itemsPerPage);
343         }
345         return context;
346     };
348     /**
349      * Create a paged content widget where the complete list of items is not loaded
350      * up front but will instead be loaded by an ajax request (or similar).
351      *
352      * The client code must provide a callback function which loads and renders the
353      * items for each page. See PagedContent.init for more details.
354      *
355      * The function will return a deferred that is resolved with a jQuery object
356      * for the HTML content and a string for the JavaScript.
357      *
358      * The current list of configuration options available are:
359      *      dropdown {bool} True to render the page control as a dropdown (paging bar is default).
360      *      maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
361      *      ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
362      *      controlPlacementBottom {bool} Render controls under paged content (default to false)
363      *
364      * @param  {function} renderPagesContentCallback  Callback for loading and rendering the items.
365      * @param  {object} config  Configuration options provided by the client.
366      * @return {promise} Resolved with jQuery HTML and string JS.
367      */
368     var create = function(renderPagesContentCallback, config) {
369         return createWithTotalAndLimit(null, null, renderPagesContentCallback, config);
370     };
372     /**
373      * Create a paged content widget where the complete list of items is not loaded
374      * up front but will instead be loaded by an ajax request (or similar).
375      *
376      * The client code must provide a callback function which loads and renders the
377      * items for each page. See PagedContent.init for more details.
378      *
379      * The function will return a deferred that is resolved with a jQuery object
380      * for the HTML content and a string for the JavaScript.
381      *
382      * The current list of configuration options available are:
383      *      dropdown {bool} True to render the page control as a dropdown (paging bar is default).
384      *      maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
385      *      ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
386      *      controlPlacementBottom {bool} Render controls under paged content (default to false)
387      *
388      * @param  {int|array|null} itemsPerPage  How many items will be shown per page.
389      * @param  {function} renderPagesContentCallback  Callback for loading and rendering the items.
390      * @param  {object} config  Configuration options provided by the client.
391      * @return {promise} Resolved with jQuery HTML and string JS.
392      */
393     var createWithLimit = function(itemsPerPage, renderPagesContentCallback, config) {
394         return createWithTotalAndLimit(null, itemsPerPage, renderPagesContentCallback, config);
395     };
397     /**
398      * Create a paged content widget where the complete list of items is not loaded
399      * up front but will instead be loaded by an ajax request (or similar).
400      *
401      * The client code must provide a callback function which loads and renders the
402      * items for each page. See PagedContent.init for more details.
403      *
404      * The function will return a deferred that is resolved with a jQuery object
405      * for the HTML content and a string for the JavaScript.
406      *
407      * The current list of configuration options available are:
408      *      dropdown {bool} True to render the page control as a dropdown (paging bar is default).
409      *      maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
410      *      ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
411      *      controlPlacementBottom {bool} Render controls under paged content (default to false)
412      *
413      * @param  {int|null} numberOfItems How many items are there in total.
414      * @param  {int|array|null} itemsPerPage  How many items will be shown per page.
415      * @param  {function} renderPagesContentCallback  Callback for loading and rendering the items.
416      * @param  {object} config  Configuration options provided by the client.
417      * @return {promise} Resolved with jQuery HTML and string JS.
418      */
419     var createWithTotalAndLimit = function(numberOfItems, itemsPerPage, renderPagesContentCallback, config) {
420         config = config || {};
422         var deferred = $.Deferred();
423         var templateContext = buildTemplateContext(numberOfItems, itemsPerPage, config);
425         Templates.render(TEMPLATES.PAGED_CONTENT, templateContext)
426             .then(function(html, js) {
427                 html = $(html);
429                 var container = html;
431                 PagedContent.init(container, renderPagesContentCallback);
433                 deferred.resolve(html, js);
434                 return;
435             })
436             .fail(function(exception) {
437                 deferred.reject(exception);
438             })
439             .fail(Notification.exception);
441         return deferred.promise();
442     };
444     /**
445      * Create a paged content widget where the complete list of items is loaded
446      * up front.
447      *
448      * The client code must provide a callback function which renders the
449      * items for each page. The callback will be provided with an array where each
450      * value in the array is a the list of items to render for the page.
451      *
452      * The function will return a deferred that is resolved with a jQuery object
453      * for the HTML content and a string for the JavaScript.
454      *
455      * The current list of configuration options available are:
456      *      dropdown {bool} True to render the page control as a dropdown (paging bar is default).
457      *      maxPages {Number} The maximum number of pages to show in the dropdown (only works with dropdown option)
458      *      ignoreControlWhileLoading {bool} Disable the pagination controls while loading a page (default to true)
459      *      controlPlacementBottom {bool} Render controls under paged content (default to false)
460      *
461      * @param  {array} contentItems The list of items to paginate.
462      * @param  {Number} itemsPerPage  How many items will be shown per page.
463      * @param  {function} renderContentCallback  Callback for rendering the items for the page.
464      * @param  {object} config  Configuration options provided by the client.
465      * @return {promise} Resolved with jQuery HTML and string JS.
466      */
467     var createFromStaticList = function(contentItems, itemsPerPage, renderContentCallback, config) {
468         if (typeof config == 'undefined') {
469             config = {};
470         }
472         var numberOfItems = contentItems.length;
473         return createWithTotalAndLimit(numberOfItems, itemsPerPage, function(pagesData) {
474             var contentToRender = [];
475             pagesData.forEach(function(pageData) {
476                 var begin = pageData.offset;
477                 var end = pageData.limit ? begin + pageData.limit : numberOfItems;
478                 var items = contentItems.slice(begin, end);
479                 contentToRender.push(items);
480             });
482             return renderContentCallback(contentToRender);
483         }, config);
484     };
486     /**
487      * Reset the last page number for the generated paged-content
488      * This is used when we need a way to update the last page number outside of the getters callback
489      *
490      * @param {String} id ID of the paged content container
491      * @param {Int} lastPageNumber The last page number
492      */
493     var resetLastPageNumber = function(id, lastPageNumber) {
494         PubSub.publish(id + PagedContentEvents.ALL_ITEMS_LOADED, lastPageNumber);
495     };
497     return {
498         create: create,
499         createWithLimit: createWithLimit,
500         createWithTotalAndLimit: createWithTotalAndLimit,
501         createFromStaticList: createFromStaticList,
502         // Backwards compatibility just in case anyone was using this.
503         createFromAjax: createWithTotalAndLimit,
504         resetLastPageNumber: resetLastPageNumber
505     };
506 });