Merge branch 'MDL-81713-main' of https://github.com/junpataleta/moodle
[moodle.git] / lib / amd / src / pagehelpers.js
blob7c5e1896644cb965a7754d19058bbf425efa8415
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  * Page utility helpers.
18  *
19  * @module core/pagehelpers
20  * @copyright  2023 Ferran Recio <ferran@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 /**
25  * Maximum sizes for breakpoints. This needs to correspond with Bootstrap
26  * Breakpoints
27  *
28  * @private
29  */
30 const Sizes = {
31     small: 576,
32     medium: 991,
33     large: 1400
36 const Selectors = {
37     focusable: 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',
40 const Classes = {
41     behatSite: 'behat-site',
44 /**
45  * Check fi the current page is a Behat site.
46  * @returns {boolean} true if the current page is a Behat site.
47  */
48 export const isBehatSite = () => {
49     return document.body.classList.contains(Classes.behatSite);
52 /**
53  * Get the current body width.
54  * @returns {number} the current body width.
55  */
56 export const getCurrentWidth = () => {
57     const DomRect = document.body.getBoundingClientRect();
58     return DomRect.x + DomRect.width;
61 /**
62  * Check if the user uses an extra small size browser.
63  *
64  * @returns {boolean} true if the body is smaller than sizes.small max size.
65  */
66 export const isExtraSmall = () => {
67     const browserWidth = getCurrentWidth();
68     return browserWidth < Sizes.small;
71 /**
72  * Check if the user uses a small size browser.
73  *
74  * @returns {boolean} true if the body is smaller than sizes.medium max size.
75  */
76 export const isSmall = () => {
77     const browserWidth = getCurrentWidth();
78     return browserWidth < Sizes.medium;
81 /**
82  * Check if the user uses a large size browser.
83  *
84  * @returns {boolean} true if the body is smaller than sizes.large max size.
85  */
86 export const isLarge = () => {
87     const browserWidth = getCurrentWidth();
88     return browserWidth >= Sizes.large;
91 /**
92  * Get the first focusable element inside a container.
93  * @param {HTMLElement} [container] Container to search in. Defaults to document.
94  * @returns {HTMLElement|null}
95  */
96 export const firstFocusableElement = (container) => {
97     const containerElement = container || document;
98     return containerElement.querySelector(Selectors.focusable);
102  * Get the last focusable element inside a container.
103  * @param {HTMLElement} [container] Container to search in. Defaults to document.
104  * @returns {HTMLElement|null}
105  */
106 export const lastFocusableElement = (container) => {
107     const containerElement = container || document;
108     const focusableElements = containerElement.querySelectorAll(Selectors.focusable);
109     return focusableElements[focusableElements.length - 1] ?? null;
113  * Get all focusable elements inside a container.
114  * @param {HTMLElement} [container] Container to search in. Defaults to document.
115  * @returns {HTMLElement[]}
116  */
117 export const focusableElements = (container) => {
118     const containerElement = container || document;
119     return containerElement.querySelectorAll(Selectors.focusable);
123  * Get the previous focusable element in a container.
124  * It uses the current focused element to know where to start the search.
125  * @param {HTMLElement} [container] Container to search in. Defaults to document.
126  * @param {Boolean} [loopSelection] Whether to loop selection or not. Default to false.
127  * @returns {HTMLElement|null}
128  */
129 export const previousFocusableElement = (container, loopSelection) => {
130     return getRelativeFocusableElement(container, loopSelection, -1);
134  * Get the next focusable element in a container.
135  * It uses the current focused element to know where to start the search.
136  * @param {HTMLElement} [container] Container to search in. Defaults to document.
137  * @param {Boolean} [loopSelection] Whether to loop selection or not. Default to false.
138  * @returns {HTMLElement|null}
139  */
140 export const nextFocusableElement = (container, loopSelection) => {
141     return getRelativeFocusableElement(container, loopSelection, 1);
145  * Internal function to get the next or previous focusable element.
146  * @param {HTMLElement} [container] Container to search in. Defaults to document.
147  * @param {Boolean} [loopSelection] Whether to loop selection or not.
148  * @param {Number} [direction] Direction to search in. 1 for next, -1 for previous.
149  * @returns {HTMLElement|null}
150  * @private
151  */
152 const getRelativeFocusableElement = (container, loopSelection, direction) => {
153     const focusedElement = document.activeElement;
154     const focusables = [...focusableElements(container)];
155     const focusedIndex = focusables.indexOf(focusedElement);
157     if (focusedIndex === -1) {
158         return null;
159     }
161     const newIndex = focusedIndex + direction;
163     if (focusables[newIndex] !== undefined) {
164         return focusables[newIndex];
165     }
166     if (loopSelection != true) {
167         return null;
168     }
169     if (direction > 0) {
170         return focusables[0] ?? null;
171     }
172     return focusables[focusables.length - 1] ?? null;