| 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 |  * 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 |  */
 | 
        
           |  |  | 23 |   | 
        
           |  |  | 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
 | 
        
           |  |  | 34 | };
 | 
        
           |  |  | 35 |   | 
        
           |  |  | 36 | const Selectors = {
 | 
        
           |  |  | 37 |     focusable: 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',
 | 
        
           |  |  | 38 | };
 | 
        
           |  |  | 39 |   | 
        
           |  |  | 40 | const Classes = {
 | 
        
           |  |  | 41 |     behatSite: 'behat-site',
 | 
        
           |  |  | 42 | };
 | 
        
           |  |  | 43 |   | 
        
           |  |  | 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);
 | 
        
           |  |  | 50 | };
 | 
        
           |  |  | 51 |   | 
        
           |  |  | 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;
 | 
        
           |  |  | 59 | };
 | 
        
           |  |  | 60 |   | 
        
           |  |  | 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;
 | 
        
           |  |  | 69 | };
 | 
        
           |  |  | 70 |   | 
        
           |  |  | 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;
 | 
        
           |  |  | 79 | };
 | 
        
           |  |  | 80 |   | 
        
           |  |  | 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;
 | 
        
           |  |  | 89 | };
 | 
        
           |  |  | 90 |   | 
        
           |  |  | 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);
 | 
        
           |  |  | 99 | };
 | 
        
           |  |  | 100 |   | 
        
           |  |  | 101 | /**
 | 
        
           |  |  | 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;
 | 
        
           |  |  | 110 | };
 | 
        
           |  |  | 111 |   | 
        
           |  |  | 112 | /**
 | 
        
           |  |  | 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);
 | 
        
           |  |  | 120 | };
 | 
        
           |  |  | 121 |   | 
        
           |  |  | 122 | /**
 | 
        
           |  |  | 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);
 | 
        
           |  |  | 131 | };
 | 
        
           |  |  | 132 |   | 
        
           |  |  | 133 | /**
 | 
        
           |  |  | 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);
 | 
        
           |  |  | 142 | };
 | 
        
           |  |  | 143 |   | 
        
           |  |  | 144 | /**
 | 
        
           |  |  | 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);
 | 
        
           |  |  | 156 |   | 
        
           |  |  | 157 |     if (focusedIndex === -1) {
 | 
        
           |  |  | 158 |         return null;
 | 
        
           |  |  | 159 |     }
 | 
        
           |  |  | 160 |   | 
        
           |  |  | 161 |     const newIndex = focusedIndex + direction;
 | 
        
           |  |  | 162 |   | 
        
           |  |  | 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;
 | 
        
           |  |  | 173 | };
 |