on-demand release 3.6dev+
[moodle.git] / lib / amd / src / paged_content_factory.js
CommitLineData
4ab09853
RW
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 * 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 */
23define(
24[
25 'jquery',
26 'core/templates',
27 'core/notification',
fd955097
P
28 'core/paged_content',
29 'core/paged_content_events',
30 'core/pubsub'
4ab09853
RW
31],
32function(
33 $,
34 Templates,
35 Notification,
fd955097
P
36 PagedContent,
37 PagedContentEvents,
38 PubSub
4ab09853
RW
39) {
40 var TEMPLATES = {
41 PAGED_CONTENT: 'core/paged_content'
42 };
43
2c13ae01
RW
44 var DEFAULT = {
45 ITEMS_PER_PAGE_SINGLE: 25,
46 ITEMS_PER_PAGE_ARRAY: [25, 50, 100, 0],
47 MAX_PAGES: 3
48 };
49
4ab09853 50 /**
2c13ae01
RW
51 * Get the default context to render the paged content mustache
52 * template.
4ab09853 53 *
2c13ae01 54 * @return {object}
4ab09853 55 */
2c13ae01
RW
56 var getDefaultTemplateContext = function() {
57 return {
58 pagingbar: false,
59 pagingdropdown: false,
60 skipjs: true,
61 ignorecontrolwhileloading: true,
62 controlplacementbottom: false
4ab09853 63 };
2c13ae01
RW
64 };
65
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 };
82
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;
93
94 if (numberOfItems > 0) {
95 var partial = numberOfItems % itemsPerPage;
96
97 if (partial) {
98 numberOfItems -= partial;
99 numberOfPages = (numberOfItems / itemsPerPage) + 1;
100 } else {
101 numberOfPages = numberOfItems / itemsPerPage;
102 }
103 }
104
105 return numberOfPages;
106 };
107
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 }
120
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 }
126
127 var context = getDefaultPagingBarTemplateContext();
128 context.itemsperpage = itemsPerPage;
129 var numberOfPages = calculateNumberOfPages(numberOfItems, itemsPerPage);
4ab09853
RW
130
131 for (var i = 1; i <= numberOfPages; i++) {
132 var page = {
133 number: i,
134 page: "" + i,
135 };
136
137 // Make the first page active by default.
138 if (i === 1) {
139 page.active = true;
140 }
141
142 context.pages.push(page);
143 }
144
145 return context;
146 };
147
148 /**
2c13ae01
RW
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 });
181
182 var activeItems = context.filter(function(item) {
183 return item.active;
184 });
185
186 // Default the first item to active if one hasn't been specified.
187 if (!activeItems.length) {
188 context[0].active = true;
189 }
190
191 return context;
192 } else {
193 return itemsPerPage;
194 }
195 };
196
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 }
208
209 var context = getDefaultPagingBarTemplateContext();
210 context.itemsperpage = buildItemsPerPagePagingBarContext(itemsPerPage);
211 context.showitemsperpageselector = $.isArray(itemsPerPage);
212
213 return context;
214 };
215
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 };
231
232 /**
233 * Build the context to render the paging dropdown template based on the number
4ab09853
RW
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 *
2c13ae01
RW
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 *
4ab09853 243 * For example:
4ab09853
RW
244 * Items per page = 25
245 * Would render a dropdown will 4 options:
246 * 25
247 * 50
248 * 100
249 * All
250 *
2c13ae01 251 * @param {Number} itemsPerPage How many items will be shown per page.
4ab09853
RW
252 * @param {object} config Configuration options provided by the client.
253 * @return {object} The template context.
254 */
2c13ae01
RW
255 var buildPagingDropdownTemplateContext = function(itemsPerPage, config) {
256 if (itemsPerPage === null) {
257 itemsPerPage = DEFAULT.ITEMS_PER_PAGE_SINGLE;
258 }
259
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 }
267
4ab09853
RW
268 var context = {
269 options: []
270 };
271
272 var totalItems = 0;
273 var lastIncrease = 0;
2c13ae01 274 var maxPages = DEFAULT.MAX_PAGES;
4ab09853
RW
275
276 if (config.hasOwnProperty('maxPages')) {
277 maxPages = config.maxPages;
278 }
279
280 for (var i = 1; i <= maxPages; i++) {
281 var itemCount = 0;
282
283 if (i <= 2) {
284 itemCount = itemsPerPage;
285 lastIncrease = itemsPerPage;
286 } else {
287 lastIncrease = lastIncrease * 2;
288 itemCount = lastIncrease;
289 }
290
291 totalItems += itemCount;
292 var option = {
293 itemcount: itemCount,
294 content: totalItems
295 };
296
297 // Make the first option active by default.
298 if (i === 1) {
299 option.active = true;
300 }
301
302 context.options.push(option);
303 }
304
305 return context;
306 };
307
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 *
2c13ae01
RW
315 * @param {int|null} numberOfItems Total number of items.
316 * @param {int|null|array} itemsPerPage How many items will be shown per page.
4ab09853
RW
317 * @param {object} config Configuration options provided by the client.
318 * @return {object} The template context.
319 */
2c13ae01
RW
320 var buildTemplateContext = function(numberOfItems, itemsPerPage, config) {
321 var context = getDefaultTemplateContext();
322
323 if (config.hasOwnProperty('ignoreControlWhileLoading')) {
324 context.ignorecontrolwhileloading = config.ignoreControlWhileLoading;
325 }
326
327 if (config.hasOwnProperty('controlPlacementBottom')) {
328 context.controlplacementbottom = config.controlPlacementBottom;
329 }
330
331 if (config.hasOwnProperty('hideControlOnSinglePage')) {
332 context.hidecontrolonsinglepage = config.hideControlOnSinglePage;
333 }
334
335 if (config.hasOwnProperty('ariaLabels')) {
336 context.arialabels = config.ariaLabels;
337 }
4ab09853
RW
338
339 if (config.hasOwnProperty('dropdown') && config.dropdown) {
2c13ae01 340 context.pagingdropdown = buildPagingDropdownTemplateContext(itemsPerPage, config);
4ab09853 341 } else {
2c13ae01 342 context.pagingbar = buildPagingBarTemplateContext(numberOfItems, itemsPerPage);
4ab09853
RW
343 }
344
345 return context;
346 };
347
348 /**
2c13ae01
RW
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).
4ab09853 351 *
2c13ae01
RW
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.
4ab09853 367 */
2c13ae01
RW
368 var create = function(renderPagesContentCallback, config) {
369 return createWithTotalAndLimit(null, null, renderPagesContentCallback, config);
370 };
4ab09853 371
2c13ae01
RW
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 */
fd955097
P
393 var createWithLimit = function(itemsPerPage, renderPagesContentCallback, config) {
394 return createWithTotalAndLimit(null, itemsPerPage, renderPagesContentCallback, config);
4ab09853
RW
395 };
396
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).
2c13ae01
RW
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)
4ab09853 412 *
2c13ae01
RW
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.
4ab09853
RW
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 */
fd955097 419 var createWithTotalAndLimit = function(numberOfItems, itemsPerPage, renderPagesContentCallback, config) {
2c13ae01 420 config = config || {};
4ab09853
RW
421
422 var deferred = $.Deferred();
2c13ae01 423 var templateContext = buildTemplateContext(numberOfItems, itemsPerPage, config);
4ab09853
RW
424
425 Templates.render(TEMPLATES.PAGED_CONTENT, templateContext)
426 .then(function(html, js) {
427 html = $(html);
428
429 var container = html;
4ab09853 430
fd955097 431 PagedContent.init(container, renderPagesContentCallback);
4ab09853
RW
432
433 deferred.resolve(html, js);
434 return;
435 })
436 .fail(function(exception) {
437 deferred.reject(exception);
438 })
439 .fail(Notification.exception);
440
2c13ae01 441 return deferred.promise();
4ab09853
RW
442 };
443
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).
2c13ae01
RW
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)
4ab09853
RW
460 *
461 * @param {array} contentItems The list of items to paginate.
2c13ae01 462 * @param {Number} itemsPerPage How many items will be shown per page.
4ab09853
RW
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 }
471
472 var numberOfItems = contentItems.length;
2c13ae01 473 return createWithTotalAndLimit(numberOfItems, itemsPerPage, function(pagesData) {
4ab09853
RW
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 });
481
482 return renderContentCallback(contentToRender);
483 }, config);
484 };
485
fd955097
P
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 };
496
4ab09853 497 return {
2c13ae01
RW
498 create: create,
499 createWithLimit: createWithLimit,
500 createWithTotalAndLimit: createWithTotalAndLimit,
501 createFromStaticList: createFromStaticList,
502 // Backwards compatibility just in case anyone was using this.
fd955097
P
503 createFromAjax: createWithTotalAndLimit,
504 resetLastPageNumber: resetLastPageNumber
4ab09853
RW
505 };
506});