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