1 // This file is part of Moodle - http://moodle.org/
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.
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/>.
17 * Javascript to enhance the paged content paging bar.
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
25 'core/custom_interaction_events',
26 'core/paged_content_events',
39 ROOT: '[data-region="paging-bar"]',
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"]',
53 * Get the page element by number.
55 * @param {object} root The root element.
56 * @param {Number} pageNumber The page number.
59 var getPageByNumber = function(root, pageNumber) {
60 return root.find(SELECTORS.PAGE_ITEM + '[data-page-number="' + pageNumber + '"]');
64 * Get the next button element.
66 * @param {object} root The root element.
69 var getNextButton = function(root) {
70 return root.find(SELECTORS.NEXT_BUTTON);
74 * Set the last page number after which no more pages
77 * @param {object} root The root element.
78 * @param {Number} number Page number.
80 var setLastPageNumber = function(root, number) {
81 root.attr('data-last-page-number', number);
85 * Get the last page number.
87 * @param {object} root The root element.
90 var getLastPageNumber = function(root) {
91 return parseInt(root.attr('data-last-page-number'), 10);
95 * Get the active page number.
97 * @param {object} root The root element.
98 * @returns {Number} The page number
100 var getActivePageNumber = function(root) {
101 return parseInt(root.attr('data-active-page-number'), 10);
105 * Set the active page number.
107 * @param {object} root The root element.
108 * @param {Number} number Page number.
110 var setActivePageNumber = function(root, number) {
111 root.attr('data-active-page-number', number);
115 * Check if there is an active page number.
117 * @param {object} root The root element.
120 var hasActivePageNumber = function(root) {
121 var number = getActivePageNumber(root);
122 return !isNaN(number) && number != 0;
126 * Get the page number for a given page.
128 * @param {object} root The root element.
129 * @param {object} page The page element.
130 * @returns {Number} The page number
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
136 return parseInt(page.attr('data-page-number'), 10);
140 var activePageNumber = null;
142 switch (page.attr('data-control')) {
148 pageNumber = getLastPageNumber(root);
152 activePageNumber = getActivePageNumber(root);
153 var lastPage = getLastPageNumber(root);
155 pageNumber = activePageNumber + 1;
156 } else if (activePageNumber && activePageNumber < lastPage) {
157 pageNumber = activePageNumber + 1;
159 pageNumber = lastPage;
164 activePageNumber = getActivePageNumber(root);
165 if (activePageNumber && activePageNumber > 1) {
166 pageNumber = activePageNumber - 1;
177 // Make sure we return an int not a string.
178 return parseInt(pageNumber, 10);
182 * Get the limit of items for each page.
184 * @param {object} root The root element.
187 var getLimit = function(root) {
188 return parseInt(root.attr('data-items-per-page'), 10);
192 * Set the limit of items for each page.
194 * @param {object} root The root element.
195 * @param {Number} limit Items per page limit.
197 var setLimit = function(root, limit) {
198 root.attr('data-items-per-page', limit);
202 * Show the paging bar.
204 * @param {object} root The root element.
206 var show = function(root) {
207 root.removeClass('hidden');
211 * Hide the paging bar.
213 * @param {object} root The root element.
215 var hide = function(root) {
216 root.addClass('hidden');
220 * Disable the next and last buttons in the paging bar.
222 * @param {object} root The root element.
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);
235 * Enable the next and last buttons in the paging bar.
237 * @param {object} root The root element.
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');
250 * Disable the previous and first buttons in the paging bar.
252 * @param {object} root The root element.
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);
265 * Adjusts the size of the paging bar and hides unnecessary pages.
267 * @param {object} root The root element.
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;
291 show(beginningDotsButton);
294 hide(beginningDotsButton);
296 if (maxpage < lastPageNumber) {
297 show(endingDotsButton);
300 hide(endingDotsButton);
302 dotsButtons.addClass('disabled');
303 dotsButtons.attr('aria-disabled', true);
307 pages.each(function(index, page) {
309 if ((index + 1) >= minpage && (index + 1) <= maxpage) {
320 * Enable the previous and first buttons in the paging bar.
322 * @param {object} root The root element.
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');
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
339 * @param {object} root The root element.
340 * @return {array} First element is the key, second is the component.
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();
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
355 * @param {object} root The root element.
356 * @return {array} First element is the key, second is the component.
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();
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).
370 * Sets the active page number to be the last page found with
371 * an "active" class (if any).
373 * Sets the last page number.
375 * @param {object} root The root element.
376 * @param {jQuery} items A jQuery list of items.
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;
385 item.attr('data-page-number', pageNumber);
388 if (item.hasClass('active')) {
389 setActivePageNumber(root, pageNumber);
393 setLastPageNumber(root, lastPageNumber);
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.
400 * @param {object} root The root element.
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
409 var stringRequests = pageItems.map(function(index, page) {
411 var pageNumber = getPageNumber(root, page);
413 if (pageNumber === activePageNumber) {
415 key: activePageAriaLabelComponents[0],
416 component: activePageAriaLabelComponents[1],
421 key: pageAriaLabelComponents[0],
422 component: pageAriaLabelComponents[1],
428 Str.get_strings(stringRequests).then(function(strings) {
429 pageItems.each(function(index, page) {
431 var string = strings[index];
432 page.attr('aria-label', string);
433 page.find(SELECTORS.PAGE_LINK).attr('aria-label', string);
439 // No need to interrupt the page if we can't load the aria lang strings.
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
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.
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;
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);
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);
476 enableNextControlButtons(root);
479 if (pageNumber > 1) {
480 enablePreviousControlButtons(root);
482 disablePreviousControlButtons(root);
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
490 PubSub.publish(id + PagedContentEvents.SHOW_PAGES, [{
491 pageNumber: pageNumber,
498 * Add event listeners for interactions with the paging bar as well as listening
499 * for custom paged content events.
501 * Each event will trigger different logic to update parts of the paging bar's
504 * @param {object} root The root element.
505 * @param {string} id A uniqie id for this instance.
507 var registerEventListeners = function(root, id) {
508 var ignoreControlWhileLoading = root.attr('data-ignore-control-while-loading');
511 if (ignoreControlWhileLoading == "") {
512 // Default to ignoring control while loading if not specified.
513 ignoreControlWhileLoading = true;
516 CustomEvents.define(root, [
517 CustomEvents.events.activate
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.
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);
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
541 PubSub.subscribe(id + PagedContentEvents.ALL_ITEMS_LOADED, function(pageNumber) {
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);
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.
556 disableNextControlButtons(root);
557 disablePreviousControlButtons(root);
560 disableNextControlButtons(root);
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.
570 // This is triggered when the paging limit is modified.
571 PubSub.subscribe(id + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT, function(limit) {
573 setLimit(root, limit);
574 setLastPageNumber(root, 0);
575 setActivePageNumber(root, 0);
577 // Reload the data from page 1 again.
578 showPage(root, 1, id);
583 * Initialise the paging bar.
584 * @param {object} root The root element.
585 * @param {string} id A uniqie id for this instance.
587 var init = function(root, id) {
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
598 getPageByNumber(root, activePageNumber).click();
599 if (activePageNumber == 1) {
600 // If the first page is active then disable the previous buttons.
601 disablePreviousControlButtons(root);
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();
609 adjustPagingBarSize(root);
614 disableNextControlButtons: disableNextControlButtons,
615 enableNextControlButtons: enableNextControlButtons,
616 disablePreviousControlButtons: disablePreviousControlButtons,
617 enablePreviousControlButtons: enablePreviousControlButtons,
619 rootSelector: SELECTORS.ROOT,