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 to enhance the paged content paging bar.
18
 *
19
 * @module     core/paged_content_paging_bar
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
    'jquery',
25
    'core/custom_interaction_events',
26
    'core/paged_content_events',
27
    'core/str',
28
    'core/pubsub',
29
    'core/pending',
30
],
31
function(
32
    $,
33
    CustomEvents,
34
    PagedContentEvents,
35
    Str,
36
    PubSub,
37
    Pending
38
) {
39
 
40
    var SELECTORS = {
41
        ROOT: '[data-region="paging-bar"]',
42
        PAGE: '[data-page]',
43
        PAGE_ITEM: '[data-region="page-item"]',
44
        PAGE_LINK: '[data-region="page-link"]',
45
        FIRST_BUTTON: '[data-control="first"]',
46
        LAST_BUTTON: '[data-control="last"]',
47
        NEXT_BUTTON: '[data-control="next"]',
48
        PREVIOUS_BUTTON: '[data-control="previous"]',
49
        DOTS_BUTTONS: '[data-dots]',
50
        BEGINNING_DOTS_BUTTON: '[data-dots="beginning"]',
51
        ENDING_DOTS_BUTTON: '[data-dots="ending"]',
52
    };
53
 
54
    /**
55
     * Get the page element by number.
56
     *
57
     * @param {object} root The root element.
58
     * @param {Number} pageNumber The page number.
59
     * @return {jQuery}
60
     */
61
    var getPageByNumber = function(root, pageNumber) {
62
        return root.find(SELECTORS.PAGE_ITEM + '[data-page-number="' + pageNumber + '"]');
63
    };
64
 
65
    /**
66
     * Get the next button element.
67
     *
68
     * @param {object} root The root element.
69
     * @return {jQuery}
70
     */
71
    var getNextButton = function(root) {
72
        return root.find(SELECTORS.NEXT_BUTTON);
73
    };
74
 
75
    /**
76
     * Set the last page number after which no more pages
77
     * should be loaded.
78
     *
79
     * @param {object} root The root element.
80
     * @param {Number} number Page number.
81
     */
82
    var setLastPageNumber = function(root, number) {
83
        root.attr('data-last-page-number', number);
84
    };
85
 
86
    /**
87
     * Get the last page number.
88
     *
89
     * @param {object} root The root element.
90
     * @return {Number}
91
     */
92
    var getLastPageNumber = function(root) {
93
        return parseInt(root.attr('data-last-page-number'), 10);
94
    };
95
 
96
    /**
97
     * Get the active page number.
98
     *
99
     * @param {object} root The root element.
100
     * @returns {Number} The page number
101
     */
102
    var getActivePageNumber = function(root) {
103
        return parseInt(root.attr('data-active-page-number'), 10);
104
    };
105
 
106
    /**
107
     * Set the active page number.
108
     *
109
     * @param {object} root The root element.
110
     * @param {Number} number Page number.
111
     */
112
    var setActivePageNumber = function(root, number) {
113
        root.attr('data-active-page-number', number);
114
    };
115
 
116
    /**
117
     * Check if there is an active page number.
118
     *
119
     * @param {object} root The root element.
120
     * @returns {bool}
121
     */
122
    var hasActivePageNumber = function(root) {
123
        var number = getActivePageNumber(root);
124
        return !isNaN(number) && number != 0;
125
    };
126
 
127
    /**
128
     * Get the page number for a given page.
129
     *
130
     * @param {object} root The root element.
131
     * @param {object} page The page element.
132
     * @returns {Number} The page number
133
     */
134
    var getPageNumber = function(root, page) {
135
        if (page.attr('data-page') != undefined) {
136
            // If it's an actual page then we can just use the page number
137
            // attribute.
138
            return parseInt(page.attr('data-page-number'), 10);
139
        }
140
 
141
        var pageNumber = 1;
142
        var activePageNumber = null;
143
 
144
        switch (page.attr('data-control')) {
145
            case 'first':
146
                pageNumber = 1;
147
                break;
148
 
149
            case 'last':
150
                pageNumber = getLastPageNumber(root);
151
                break;
152
 
153
            case 'next':
154
                activePageNumber = getActivePageNumber(root);
155
                var lastPage = getLastPageNumber(root);
156
                if (!lastPage) {
157
                    pageNumber = activePageNumber + 1;
158
                } else if (activePageNumber && activePageNumber < lastPage) {
159
                    pageNumber = activePageNumber + 1;
160
                } else {
161
                    pageNumber = lastPage;
162
                }
163
                break;
164
 
165
            case 'previous':
166
                activePageNumber = getActivePageNumber(root);
167
                if (activePageNumber && activePageNumber > 1) {
168
                    pageNumber = activePageNumber - 1;
169
                } else {
170
                    pageNumber = 1;
171
                }
172
                break;
173
 
174
            default:
175
                pageNumber = 1;
176
                break;
177
        }
178
 
179
        // Make sure we return an int not a string.
180
        return parseInt(pageNumber, 10);
181
    };
182
 
183
    /**
184
     * Get the limit of items for each page.
185
     *
186
     * @param {object} root The root element.
187
     * @returns {Number}
188
     */
189
    var getLimit = function(root) {
190
        return parseInt(root.attr('data-items-per-page'), 10);
191
    };
192
 
193
    /**
194
     * Set the limit of items for each page.
195
     *
196
     * @param {object} root The root element.
197
     * @param {Number} limit Items per page limit.
198
     */
199
    var setLimit = function(root, limit) {
200
        root.attr('data-items-per-page', limit);
201
    };
202
 
203
    /**
204
     * Show the paging bar.
205
     *
206
     * @param {object} root The root element.
207
     */
208
    var show = function(root) {
209
        root.removeClass('hidden');
210
    };
211
 
212
    /**
213
     * Hide the paging bar.
214
     *
215
     * @param {object} root The root element.
216
     */
217
    var hide = function(root) {
218
        root.addClass('hidden');
219
    };
220
 
221
    /**
222
     * Disable the next and last buttons in the paging bar.
223
     *
224
     * @param {object} root The root element.
225
     */
226
    var disableNextControlButtons = function(root) {
227
        var nextButton = root.find(SELECTORS.NEXT_BUTTON);
228
        var lastButton = root.find(SELECTORS.LAST_BUTTON);
229
 
230
        nextButton.addClass('disabled');
231
        nextButton.attr('aria-disabled', true);
232
        lastButton.addClass('disabled');
233
        lastButton.attr('aria-disabled', true);
234
    };
235
 
236
    /**
237
     * Enable the next and last buttons in the paging bar.
238
     *
239
     * @param {object} root The root element.
240
     */
241
    var enableNextControlButtons = function(root) {
242
        var nextButton = root.find(SELECTORS.NEXT_BUTTON);
243
        var lastButton = root.find(SELECTORS.LAST_BUTTON);
244
 
245
        nextButton.removeClass('disabled');
246
        nextButton.removeAttr('aria-disabled');
247
        lastButton.removeClass('disabled');
248
        lastButton.removeAttr('aria-disabled');
249
    };
250
 
251
    /**
252
     * Disable the previous and first buttons in the paging bar.
253
     *
254
     * @param {object} root The root element.
255
     */
256
    var disablePreviousControlButtons = function(root) {
257
        var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);
258
        var firstButton = root.find(SELECTORS.FIRST_BUTTON);
259
 
260
        previousButton.addClass('disabled');
261
        previousButton.attr('aria-disabled', true);
262
        firstButton.addClass('disabled');
263
        firstButton.attr('aria-disabled', true);
264
    };
265
 
266
    /**
267
     * Adjusts the size of the paging bar and hides unnecessary pages.
268
     *
269
     * @param {object} root The root element.
270
     */
271
    var adjustPagingBarSize = function(root) {
272
        var activePageNumber = getActivePageNumber(root);
273
        var lastPageNumber = getLastPageNumber(root);
274
 
275
        var dotsButtons = root.find(SELECTORS.DOTS_BUTTONS);
276
        var beginningDotsButton = root.find(SELECTORS.BEGINNING_DOTS_BUTTON);
277
        var endingDotsButton = root.find(SELECTORS.ENDING_DOTS_BUTTON);
278
 
279
        var pages = root.find(SELECTORS.PAGE);
280
        var barSize = parseInt(root.attr('data-bar-size'), 10);
281
 
282
        if (barSize && lastPageNumber > barSize) {
283
 
284
            var minpage = Math.max(activePageNumber - Math.round(barSize / 2), 1);
285
            var maxpage = minpage + barSize - 1;
286
 
287
            if (maxpage >= lastPageNumber) {
288
                maxpage = lastPageNumber;
289
                minpage = maxpage - barSize + 1;
290
            }
291
 
292
            if (minpage > 1) {
293
                show(beginningDotsButton);
294
                minpage++;
295
            } else {
296
                hide(beginningDotsButton);
297
            }
298
            if (maxpage < lastPageNumber) {
299
                show(endingDotsButton);
300
                maxpage--;
301
            } else {
302
                hide(endingDotsButton);
303
            }
304
            dotsButtons.addClass('disabled');
305
            dotsButtons.attr('aria-disabled', true);
306
 
307
            hide(pages);
308
 
309
            pages.each(function(index, page) {
310
                page = $(page);
311
                if ((index + 1) >= minpage && (index + 1) <= maxpage) {
312
                    show(page);
313
                }
314
            });
315
 
316
        } else {
317
            hide(dotsButtons);
318
        }
319
    };
320
 
321
    /**
322
     * Enable the previous and first buttons in the paging bar.
323
     *
324
     * @param {object} root The root element.
325
     */
326
    var enablePreviousControlButtons = function(root) {
327
        var previousButton = root.find(SELECTORS.PREVIOUS_BUTTON);
328
        var firstButton = root.find(SELECTORS.FIRST_BUTTON);
329
 
330
        previousButton.removeClass('disabled');
331
        previousButton.removeAttr('aria-disabled');
332
        firstButton.removeClass('disabled');
333
        firstButton.removeAttr('aria-disabled');
334
    };
335
 
336
    /**
337
     * Get the components for a get_string request for the aria-label
338
     * on a page. The value is a comma separated string of key and
339
     * component.
340
     *
341
     * @param {object} root The root element.
342
     * @return {array} First element is the key, second is the component.
343
     */
344
    var getPageAriaLabelComponents = function(root) {
345
        var componentString = root.attr('data-aria-label-components-pagination-item');
346
        var components = componentString.split(',').map(function(component) {
347
            return component.trim();
348
        });
349
        return components;
350
    };
351
 
352
    /**
353
     * Get the components for a get_string request for the aria-label
354
     * on an active page. The value is a comma separated string of key and
355
     * component.
356
     *
357
     * @param {object} root The root element.
358
     * @return {array} First element is the key, second is the component.
359
     */
360
    var getActivePageAriaLabelComponents = function(root) {
361
        var componentString = root.attr('data-aria-label-components-pagination-active-item');
362
        var components = componentString.split(',').map(function(component) {
363
            return component.trim();
364
        });
365
        return components;
366
    };
367
 
368
    /**
369
     * Set page numbers on each of the given items. Page numbers are set
370
     * from 1..n (where n is the number of items).
371
     *
372
     * Sets the active page number to be the last page found with
373
     * an "active" class (if any).
374
     *
375
     * Sets the last page number.
376
     *
377
     * @param {object} root The root element.
378
     * @param {jQuery} items A jQuery list of items.
379
     */
380
    var generatePageNumbers = function(root, items) {
381
        var lastPageNumber = 0;
382
        setActivePageNumber(root, 0);
383
 
384
        items.each(function(index, item) {
385
            var pageNumber = index + 1;
386
            item = $(item);
387
            item.attr('data-page-number', pageNumber);
388
            lastPageNumber++;
389
 
390
            if (item.hasClass('active')) {
391
                setActivePageNumber(root, pageNumber);
392
            }
393
        });
394
 
395
        setLastPageNumber(root, lastPageNumber);
396
    };
397
 
398
    /**
399
     * Set the aria-labels on each of the page items in the paging bar.
400
     * This includes the next, previous, first, and last items.
401
     *
402
     * @param {object} root The root element.
403
     */
404
    var generateAriaLabels = function(root) {
405
        var pageAriaLabelComponents = getPageAriaLabelComponents(root);
406
        var activePageAriaLabelComponents = getActivePageAriaLabelComponents(root);
407
        var activePageNumber = getActivePageNumber(root);
408
        var pageItems = root.find(SELECTORS.PAGE_ITEM);
409
        // We want to request all of the strings at once rather than
410
        // one at a time.
411
        var stringRequests = pageItems.toArray().map(function(index, page) {
412
            page = $(page);
413
            var pageNumber = getPageNumber(root, page);
414
 
415
            if (pageNumber === activePageNumber) {
416
                return {
417
                    key: activePageAriaLabelComponents[0],
418
                    component: activePageAriaLabelComponents[1],
419
                    param: pageNumber
420
                };
421
            } else {
422
                return {
423
                    key: pageAriaLabelComponents[0],
424
                    component: pageAriaLabelComponents[1],
425
                    param: pageNumber
426
                };
427
            }
428
        });
429
 
430
        Str.get_strings(stringRequests).then(function(strings) {
431
            pageItems.each(function(index, page) {
432
                page = $(page);
433
                var string = strings[index];
434
                page.attr('aria-label', string);
435
                page.find(SELECTORS.PAGE_LINK).attr('aria-label', string);
436
            });
437
 
438
            return strings;
439
        })
440
        .catch(function() {
441
            // No need to interrupt the page if we can't load the aria lang strings.
442
            return;
443
        });
444
    };
445
 
446
    /**
447
     * Make the paging bar item for the given page number visible and fire
448
     * the SHOW_PAGES paged content event to tell any listening content to
449
     * update.
450
     *
451
     * @param {object} root The root element.
452
     * @param {Number} pageNumber The number for the page to show.
453
     * @param {string} id A uniqie id for this instance.
454
     */
455
    var showPage = function(root, pageNumber, id) {
456
        var pendingPromise = new Pending('core/paged_content_paging_bar:showPage');
457
        var lastPageNumber = getLastPageNumber(root);
458
        var isSamePage = pageNumber == getActivePageNumber(root);
459
        var limit = getLimit(root);
460
        var offset = (pageNumber - 1) * limit;
461
 
462
        if (!isSamePage) {
463
            // We only need to toggle the active class if the user didn't click
464
            // on the already active page.
465
            root.find(SELECTORS.PAGE_ITEM).removeClass('active').removeAttr('aria-current');
466
            var page = getPageByNumber(root, pageNumber);
467
            page.addClass('active');
468
            page.attr('aria-current', true);
469
            setActivePageNumber(root, pageNumber);
470
 
471
            adjustPagingBarSize(root);
472
        }
473
 
474
        // Make sure the control buttons are disabled as the user navigates
475
        // to either end of the limits.
476
        if (lastPageNumber && pageNumber >= lastPageNumber) {
477
            disableNextControlButtons(root);
478
        } else {
479
            enableNextControlButtons(root);
480
        }
481
 
482
        if (pageNumber > 1) {
483
            enablePreviousControlButtons(root);
484
        } else {
485
            disablePreviousControlButtons(root);
486
        }
487
 
488
        generateAriaLabels(root);
489
 
490
        // This event requires a payload that contains a list of all pages that
491
        // were activated. In the case of the paging bar we only show one page at
492
        // a time.
493
        PubSub.publish(id + PagedContentEvents.SHOW_PAGES, [{
494
            pageNumber: pageNumber,
495
            limit: limit,
496
            offset: offset
497
        }]);
498
 
499
        pendingPromise.resolve();
500
    };
501
 
502
    /**
503
     * Add event listeners for interactions with the paging bar as well as listening
504
     * for custom paged content events.
505
     *
506
     * Each event will trigger different logic to update parts of the paging bar's
507
     * display.
508
     *
509
     * @param {object} root The root element.
510
     * @param {string} id A uniqie id for this instance.
511
     */
512
    var registerEventListeners = function(root, id) {
513
        var ignoreControlWhileLoading = root.attr('data-ignore-control-while-loading');
514
        var loading = false;
515
 
516
        if (ignoreControlWhileLoading == "") {
517
            // Default to ignoring control while loading if not specified.
518
            ignoreControlWhileLoading = true;
519
        }
520
 
521
        CustomEvents.define(root, [
522
            CustomEvents.events.activate
523
        ]);
524
 
525
        root.on(CustomEvents.events.activate, SELECTORS.PAGE_ITEM, function(e, data) {
526
            data.originalEvent.preventDefault();
527
            data.originalEvent.stopPropagation();
528
 
529
            if (ignoreControlWhileLoading && loading) {
530
                // Do nothing if configured to ignore control while loading.
531
                return;
532
            }
533
 
534
            var page = $(e.target).closest(SELECTORS.PAGE_ITEM);
535
 
536
            if (!page.hasClass('disabled')) {
537
                var pageNumber = getPageNumber(root, page);
538
                showPage(root, pageNumber, id);
539
                loading = true;
540
            }
541
        });
542
 
543
        // This event is fired when all of the items have been loaded. Typically used
544
        // in an "infinite" pages context when we don't know the exact number of pages
545
        // ahead of time.
546
        PubSub.subscribe(id + PagedContentEvents.ALL_ITEMS_LOADED, function(pageNumber) {
547
            loading = false;
548
            var currentLastPage = getLastPageNumber(root);
549
 
550
            if (!currentLastPage || pageNumber < currentLastPage) {
551
                // Somehow the value we've got saved is higher than the new
552
                // value we just received. Perhaps events came out of order.
553
                // In any case, save the lowest value.
554
                setLastPageNumber(root, pageNumber);
555
            }
556
 
557
            if (pageNumber === 1 && root.attr('data-hide-control-on-single-page')) {
558
                // If all items were loaded on the first page then we can hide
559
                // the paging bar because there are no other pages to load.
560
                hide(root);
561
                disableNextControlButtons(root);
562
                disablePreviousControlButtons(root);
563
            } else {
564
                show(root);
565
                disableNextControlButtons(root);
566
            }
567
        });
568
 
569
        // This event is fired after all of the requested pages have been rendered.
570
        PubSub.subscribe(id + PagedContentEvents.PAGES_SHOWN, function() {
571
            // All pages have been shown so turn off the loading flag.
572
            loading = false;
573
        });
574
 
575
        // This is triggered when the paging limit is modified.
576
        PubSub.subscribe(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, function(limit) {
577
            // Update the limit.
578
            setLimit(root, limit);
579
            setLastPageNumber(root, 0);
580
            setActivePageNumber(root, 0);
581
            show(root);
582
            // Reload the data from page 1 again.
583
            showPage(root, 1, id);
584
        });
585
    };
586
 
587
    /**
588
     * Initialise the paging bar.
589
     * @param {object} root The root element.
590
     * @param {string} id A uniqie id for this instance.
591
     */
592
    var init = function(root, id) {
593
        root = $(root);
594
        var pages = root.find(SELECTORS.PAGE);
595
        generatePageNumbers(root, pages);
596
        registerEventListeners(root, id);
597
 
598
        if (hasActivePageNumber(root)) {
599
            var activePageNumber = getActivePageNumber(root);
600
            // If the the paging bar was rendered with an active page selected
601
            // then make sure we fired off the event to tell the content page to
602
            // show.
603
            getPageByNumber(root, activePageNumber).click();
604
            if (activePageNumber == 1) {
605
                // If the first page is active then disable the previous buttons.
606
                disablePreviousControlButtons(root);
607
            }
608
        } else {
609
            // There was no active page number so load the first page using
610
            // the next button. This allows the infinite pagination to work.
611
            getNextButton(root).click();
612
        }
613
 
614
        adjustPagingBarSize(root);
615
    };
616
 
617
    return {
618
        init: init,
619
        disableNextControlButtons: disableNextControlButtons,
620
        enableNextControlButtons: enableNextControlButtons,
621
        disablePreviousControlButtons: disablePreviousControlButtons,
622
        enablePreviousControlButtons: enablePreviousControlButtons,
623
        showPage: showPage,
624
        rootSelector: SELECTORS.ROOT,
625
    };
626
});