Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

{"version":3,"file":"menu_navigation.min.js","sources":["../src/menu_navigation.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Keyboard initialization for a given html node.\n *\n * @module     core/menu_navigation\n * @copyright  2021 Moodle\n * @author     Mathew May <mathew.solutions>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SELECTORS = {\n    'menuitem': '[role=\"menuitem\"]',\n    'tab': '[role=\"tab\"]',\n    'dropdowntoggle': '[data-toggle=\"dropdown\"]',\n    'primarymenuitemactive': '.primary-navigation .dropdown-item[aria-current=\"true\"]',\n};\n\nlet openDropdownNode = null;\n\n/**\n * Small helper function to check if a given node is null or not.\n *\n * @param {HTMLElement|null} item The node that we want to compare.\n * @param {HTMLElement} fallback Either the first node or final node that can be focused on.\n * @return {HTMLElement}\n */\nconst clickErrorHandler = (item, fallback) => {\n    if (item !== null) {\n        return item;\n    } else {\n        return fallback;\n    }\n};\n\n/**\n * Control classes etc of the selected dropdown item and its' parent <a>\n *\n * @param {HTMLElement} src The node within the dropdown the user selected.\n */\nconst menuItemHelper = src => {\n    let parent;\n\n    // Do not apply any actions if the selected dropdown item is explicitly instructing to not display an active state.\n    if (src.dataset.disableactive) {\n        return;\n    }\n    // Handling for dropdown escapes.\n    // A bulk of the handling is already done by aria.js just add polish.\n    if (src.classList.contains('dropdown-item')) {\n        parent = src.closest('.dropdown-menu');\n        const dropDownToggle = document.getElementById(parent.getAttribute('aria-labelledby'));\n        dropDownToggle.classList.add('active');\n        dropDownToggle.setAttribute('tabindex', 0);\n    } else if (src.matches(`${SELECTORS.tab},${SELECTORS.menuitem}`) && !src.matches(SELECTORS.dropdowntoggle)) {\n        parent = src.parentElement.parentElement.querySelector('.dropdown-menu');\n    } else {\n        return;\n    }\n    // Remove active class from any other dropdown elements.\n    Array.prototype.forEach.call(parent.children, node => {\n        const menuItem = node.querySelector(SELECTORS.menuitem);\n        if (menuItem !== null) {\n            menuItem.classList.remove('active');\n            // Remove aria selection state.\n            menuItem.removeAttribute('aria-current');\n        }\n    });\n    // Set the applicable element's selection state.\n    if (src.getAttribute('role') === 'menuitem') {\n        src.setAttribute('aria-current', 'true');\n    }\n};\n\n/**\n * Check if there are sub items in a dropdown menu. There can be one element active only. That is usually controlled\n * by the server. However, when you click, the newly clicked item gets the active state as well. This is no problem\n * because the user leaves the page and a new page load happens. When the user hits the back button, the old page dom\n * is restored from the cache, with both menu items active. If there is such a case, we need to uncheck the item that\n * was clicked when leaving this page.\n * Make sure that this function is applied in the main menu only. The gradebook may contain drop down menus as well\n * were more than one item can be flagged as active.\n */\nconst dropDownMenuActiveCheck = function() {\n    const items = document.querySelectorAll(SELECTORS.primarymenuitemactive);\n    // Do the check only, if there is more than one subitem active.\n    if (items !== null && items.length > 1) {\n        items.forEach(function(e) {\n            // Get the link target from the href attribute and compare it with the current url in the browser.\n            const href = e.getAttribute('href');\n            const windowHref = window.location.href || '';\n            const windowPath = window.location.pathname || '';\n            if (href !== windowHref && href !== windowPath\n                && href !== windowHref + '/index.php' && href !== windowPath + 'index.php') {\n                e.classList.remove('active');\n                e.removeAttribute('aria-current');\n            }\n        });\n    }\n};\n\n/**\n * Defined keyboard event handling so we can remove listeners on nodes on resize etc.\n *\n * @param {event} e The triggering element and key presses etc.\n */\nconst keyboardListenerEvents = e => {\n    const src = e.srcElement;\n    const firstNode = e.currentTarget.firstElementChild;\n    const lastNode = findUsableLastNode(e.currentTarget);\n\n    // Handling for dropdown escapes.\n    // A bulk of the handling is already done by aria.js just add polish.\n    if (src.classList.contains('dropdown-item')) {\n        if (e.key == 'ArrowRight' ||\n            e.key == 'ArrowLeft') {\n            e.preventDefault();\n            if (openDropdownNode !== null) {\n                openDropdownNode.parentElement.click();\n            }\n        }\n        if (e.key == ' ' ||\n            e.key == 'Enter') {\n            e.preventDefault();\n\n            menuItemHelper(src);\n\n            if (!src.parentElement.classList.contains('dropdown')) {\n                src.click();\n            }\n        }\n    } else {\n        const rtl = window.right_to_left();\n        const arrowNext = rtl ? 'ArrowLeft' : 'ArrowRight';\n        const arrowPrevious = rtl ? 'ArrowRight' : 'ArrowLeft';\n\n        if (src.getAttribute('role') === 'menuitem') {\n            // When not rendered within a dropdown menu, handle keyboard navigation if the element is rendered as a menu item.\n            if (e.key == arrowNext) {\n                e.preventDefault();\n                setFocusNext(src, firstNode);\n            }\n            if (e.key == arrowPrevious) {\n                e.preventDefault();\n                setFocusPrev(src, lastNode);\n            }\n            // Let aria.js handle the dropdowns.\n            if (e.key == 'ArrowUp' ||\n                e.key == 'ArrowDown') {\n                openDropdownNode = src;\n                e.preventDefault();\n            }\n            if (e.key == 'Home') {\n                e.preventDefault();\n                setFocusHomeEnd(firstNode);\n            }\n            if (e.key == 'End') {\n                e.preventDefault();\n                setFocusHomeEnd(lastNode);\n            }\n        }\n\n        if (e.key == ' ' ||\n            e.key == 'Enter') {\n            e.preventDefault();\n            // Aria.js handles dropdowns etc.\n            if (!src.parentElement.classList.contains('dropdown')) {\n                src.click();\n            }\n        }\n    }\n};\n\n/**\n * Defined click event handling so we can remove listeners on nodes on resize etc.\n *\n * @param {event} e The triggering element and key presses etc.\n */\nconst clickListenerEvents = e => {\n    const src = e.srcElement;\n    menuItemHelper(src);\n};\n\n/**\n * The initial entry point that a given module can pass a HTMLElement.\n *\n * @param {HTMLElement} elementRoot The menu to add handlers upon.\n */\nexport default elementRoot => {\n    // Remove any and all instances of old listeners on the passed element.\n    elementRoot.removeEventListener('keydown', keyboardListenerEvents);\n    elementRoot.removeEventListener('click', clickListenerEvents);\n    // (Re)apply our event listeners to the passed element.\n    elementRoot.addEventListener('keydown', keyboardListenerEvents);\n    elementRoot.addEventListener('click', clickListenerEvents);\n};\n\n// We need this triggered only when the user hits the back button.\nwindow.addEventListener('pageshow', dropDownMenuActiveCheck);\n\n/**\n * Handle the focusing to the next element in the dropdown.\n *\n * @param {HTMLElement|null} currentNode The node that we want to take action on.\n * @param {HTMLElement} firstNode The backup node to focus as a last resort.\n */\nconst setFocusNext = (currentNode, firstNode) => {\n    const listElement = currentNode.parentElement;\n    const nextListItem = ((el) => {\n        do {\n            el = el.nextElementSibling;\n        } while (el && !el.offsetHeight); // We only work with the visible tabs.\n        return el;\n    })(listElement);\n    const nodeToSelect = clickErrorHandler(nextListItem, firstNode);\n    const parent = listElement.parentElement;\n    const isTabList = parent.getAttribute('role') === 'tablist';\n    const itemSelector = isTabList ? SELECTORS.tab : SELECTORS.menuitem;\n    const menuItem = nodeToSelect.querySelector(itemSelector);\n    menuItem.focus();\n};\n\n/**\n * Handle the focusing to the previous element in the dropdown.\n *\n * @param {HTMLElement|null} currentNode The node that we want to take action on.\n * @param {HTMLElement} lastNode The backup node to focus as a last resort.\n */\nconst setFocusPrev = (currentNode, lastNode) => {\n    const listElement = currentNode.parentElement;\n    const nextListItem = ((el) => {\n        do {\n            el = el.previousElementSibling;\n        } while (el && !el.offsetHeight); // We only work with the visible tabs.\n        return el;\n    })(listElement);\n    const nodeToSelect = clickErrorHandler(nextListItem, lastNode);\n    const parent = listElement.parentElement;\n    const isTabList = parent.getAttribute('role') === 'tablist';\n    const itemSelector = isTabList ? SELECTORS.tab : SELECTORS.menuitem;\n    const menuItem = nodeToSelect.querySelector(itemSelector);\n    menuItem.focus();\n};\n\n/**\n * Focus on either the start or end of a nav list.\n *\n * @param {HTMLElement} node The element to focus on.\n */\nconst setFocusHomeEnd = node => {\n    node.querySelector(SELECTORS.menuitem).focus();\n};\n\n/**\n * We need to look within the menu to find a last node we can add focus to.\n *\n * @param {HTMLElement} elementRoot Menu to find a final child node within.\n * @return {HTMLElement}\n */\nconst findUsableLastNode = elementRoot => {\n    const lastNode = elementRoot.lastElementChild;\n\n    // An example is the more menu existing but hidden on the page for the time being.\n    if (!lastNode.classList.contains('d-none')) {\n        return elementRoot.lastElementChild;\n    } else {\n        // Cast the HTMLCollection & reverse it.\n        const extractedNodes = Array.prototype.map.call(elementRoot.children, node => {\n            return node;\n        }).reverse();\n\n        // Get rid of any nodes we can not set focus on.\n        const nodesToUse = extractedNodes.filter((node => {\n            if (!node.classList.contains('d-none')) {\n                return node;\n            }\n        }));\n\n        // If we find no elements we can set focus on, fall back to the absolute first element.\n        if (nodesToUse.length !== 0) {\n            return nodesToUse[0];\n        } else {\n            return elementRoot.firstElementChild;\n        }\n    }\n};\n"],"names":["SELECTORS","openDropdownNode","clickErrorHandler","item","fallback","menuItemHelper","src","parent","dataset","disableactive","classList","contains","closest","dropDownToggle","document","getElementById","getAttribute","add","setAttribute","matches","parentElement","querySelector","Array","prototype","forEach","call","children","node","menuItem","remove","removeAttribute","keyboardListenerEvents","e","srcElement","firstNode","currentTarget","firstElementChild","lastNode","findUsableLastNode","key","preventDefault","click","rtl","window","right_to_left","arrowNext","arrowPrevious","setFocusNext","setFocusPrev","setFocusHomeEnd","clickListenerEvents","elementRoot","removeEventListener","addEventListener","items","querySelectorAll","length","href","windowHref","location","windowPath","pathname","currentNode","listElement","nextListItem","el","nextElementSibling","offsetHeight","nodeToSelect","itemSelector","focus","previousElementSibling","lastElementChild","nodesToUse","map","reverse","filter"],"mappings":"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"}