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