Proyectos de Subversion Moodle

Rev

Rev 1 | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |

{"version":3,"file":"subpanel.min.js","sources":["../../../src/local/action_menu/subpanel.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 * Action menu subpanel JS controls.\n *\n * @module      core/local/action_menu/subpanel\n * @copyright   2023 Mikel Martín <mikel@moodle.com>\n * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {debounce} from 'core/utils';\nimport {\n    isBehatSite,\n    isExtraSmall,\n    firstFocusableElement,\n    lastFocusableElement,\n    previousFocusableElement,\n    nextFocusableElement,\n} from 'core/pagehelpers';\nimport Pending from 'core/pending';\nimport {\n    hide,\n    unhide,\n} from 'core/aria';\nimport EventHandler from 'theme_boost/bootstrap/dom/event-handler';\n\nconst Selectors = {\n    mainMenu: '[role=\"menu\"]',\n    dropdownRight: '.dropdown-menu-end',\n    subPanel: '.dropdown-subpanel',\n    subPanelMenuItem: '.dropdown-subpanel > .dropdown-item',\n    subPanelContent: '.dropdown-subpanel > .dropdown-menu',\n    // Drawer selector.\n    drawer: '[data-region=\"fixed-drawer\"]',\n    // Lateral blocks columns selectors.\n    blockColumn: '.blockcolumn',\n    columnLeft: '.columnleft',\n};\n\nconst Classes = {\n    dropRight: 'dropend',\n    dropLeft: 'dropstart',\n    dropDown: 'dropdown',\n    forceLeft: 'downleft',\n    contentDisplayed: 'content-displayed',\n};\n\nconst BootstrapEvents = {\n    hideDropdown: 'hidden.bs.dropdown',\n};\n\nlet initialized = false;\n\n/**\n * Initialize all delegated events into the page.\n */\nconst initPageEvents = () => {\n    if (initialized) {\n        return;\n    }\n    // Hide all subpanels when hiding a dropdown.\n    document.addEventListener(BootstrapEvents.hideDropdown, () => {\n        document.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n            const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n            const subPanel = new SubPanel(dropdownSubPanel);\n            subPanel.setVisibility(false);\n        });\n    });\n\n    window.addEventListener('resize', debounce(updateAllPanelsPosition, 400));\n\n    initialized = true;\n};\n\n/**\n * Update all the panels position.\n */\nconst updateAllPanelsPosition = () => {\n    document.querySelectorAll(Selectors.subPanel).forEach(dropdown => {\n        const subpanel = new SubPanel(dropdown);\n        subpanel.updatePosition();\n    });\n};\n\n/**\n * Subpanel class.\n * @private\n */\nclass SubPanel {\n    /**\n     * Constructor.\n     * @param {HTMLElement} element The element to initialize.\n     */\n    constructor(element) {\n        this.element = element;\n        this.menuItem = element.querySelector(Selectors.subPanelMenuItem);\n        this.panelContent = element.querySelector(Selectors.subPanelContent);\n        /**\n         * Enable preview when the menu item has focus.\n         *\n         * This is disabled when the user press ESC or shift+TAB to force closing\n         *\n         * @type {Boolean}\n         * @private\n         */\n        this.showPreviewOnFocus = true;\n    }\n\n    /**\n     * Initialize the subpanel element.\n     *\n     * This method adds the event listeners to the subpanel and the position classes.\n     */\n    init() {\n        if (this.element.dataset.subPanelInitialized) {\n            return;\n        }\n\n        this.updatePosition();\n\n        // Full element events.\n        this.element.addEventListener('focusin', this._mainElementFocusInHandler.bind(this));\n        // Menu Item events.\n        this.menuItem.addEventListener('click', this._menuItemClickHandler.bind(this));\n        // Use the Bootstrap key handler for the menu item key handler.\n        // This will avoid Boostrap Dropdown handler to prevent the propagation to the subpanel.\n        const subpanelMenuItemSelector = `#${this.element.id}${Selectors.subPanelMenuItem}`;\n        EventHandler.on(document, 'keydown', subpanelMenuItemSelector, this._menuItemKeyHandler.bind(this));\n        if (!isBehatSite()) {\n            // Behat in Chrome usually move the mouse over the page when trying clicking a subpanel element.\n            // If the menu has more than one subpanel this could cause closing the subpanel by mistake.\n            this.menuItem.addEventListener('mouseover', this._menuItemHoverHandler.bind(this));\n            this.menuItem.addEventListener('mouseout', this._menuItemHoverOutHandler.bind(this));\n        }\n        // Subpanel content events.\n        this.panelContent.addEventListener('keydown', this._panelContentKeyHandler.bind(this));\n\n        this.element.dataset.subPanelInitialized = true;\n    }\n\n    /**\n     * Checks if the subpanel has enough space.\n     *\n     * In general there are two scenarios were the subpanel must be interacted differently:\n     * - Extra small screens: The subpanel is displayed below the menu item.\n     * - Drawer: The subpanel is displayed one of the drawers.\n     * - Block columns: for classic based themes.\n     *\n     * @returns {Boolean} true if the subpanel should be displayed in small screens.\n     */\n    _needSmallSpaceBehaviour() {\n        return isExtraSmall() ||\n            this.element.closest(Selectors.drawer) !== null ||\n            this.element.closest(Selectors.blockColumn) !== null;\n    }\n\n    /**\n     * Check if the subpanel should be displayed on the right.\n     *\n     * This is defined by the drop right boostrap class. However, if the menu is\n     * displayed in a block column on the right, the subpanel should be forced\n     * to the right.\n     *\n     * @returns {Boolean} true if the subpanel should be displayed on the right.\n     */\n    _needDropdownRight() {\n        if (this.element.closest(Selectors.columnLeft) !== null) {\n            return false;\n        }\n        return this.element.closest(Selectors.dropdownRight) !== null;\n    }\n\n    /**\n     * Main element focus in handler.\n     */\n    _mainElementFocusInHandler() {\n        if (this._needSmallSpaceBehaviour() || !this.showPreviewOnFocus) {\n            // Preview is disabled when the user press ESC or shift+TAB to force closing\n            // but if the continue navigating with keyboard the preview is enabled again.\n            this.showPreviewOnFocus = true;\n            return;\n        }\n        this.setVisibility(true);\n    }\n\n    /**\n     * Menu item click handler.\n     * @param {Event} event\n     */\n    _menuItemClickHandler(event) {\n        // Avoid dropdowns being closed after clicking a subemnu.\n        // This won't be needed with BS5 (data-bs-auto-close handles it).\n        event.stopPropagation();\n        event.preventDefault();\n        if (this._needSmallSpaceBehaviour()) {\n            this.setVisibility(!this.getVisibility());\n        }\n    }\n\n    /**\n     * Menu item hover handler.\n     * @private\n     */\n    _menuItemHoverHandler() {\n        if (this._needSmallSpaceBehaviour()) {\n            return;\n        }\n        this.setVisibility(true);\n    }\n\n    /**\n     * Menu item hover out handler.\n     * @private\n     */\n    _menuItemHoverOutHandler() {\n        if (this._needSmallSpaceBehaviour()) {\n            return;\n        }\n        this._hideOtherSubPanels();\n    }\n\n    /**\n     * Menu item key handler.\n     * @param {Event} event\n     * @private\n     */\n    _menuItemKeyHandler(event) {\n        // In small sizes te down key will focus on the panel.\n        if (event.key === 'ArrowUp' || (event.key === 'ArrowDown' && !this._needSmallSpaceBehaviour())) {\n            this.setVisibility(false);\n            return;\n        }\n\n        // Keys to move focus to the panel.\n        let focusPanel = false;\n\n        if (event.key === 'ArrowRight' || event.key === 'ArrowLeft' || (event.key === 'Tab' && !event.shiftKey)) {\n            focusPanel = true;\n        }\n        if ((event.key === 'Enter' || event.key === ' ')) {\n            focusPanel = true;\n        }\n        // In extra small screen the panel is shown below the item.\n        if (event.key === 'ArrowDown' && this._needSmallSpaceBehaviour() && this.getVisibility()) {\n            focusPanel = true;\n        }\n        if (focusPanel) {\n            event.stopPropagation();\n            event.preventDefault();\n            this.setVisibility(true);\n            this._focusPanelContent();\n        }\n\n    }\n\n    /**\n     * Sub panel content key handler.\n     * @param {Event} event\n     * @private\n     */\n    _panelContentKeyHandler(event) {\n        // In extra small devices the panel is displayed under the menu item\n        // so the arrow up/down switch between subpanel and the menu item.\n        const canLoop = !this._needSmallSpaceBehaviour();\n        let isBrowsingSubPanel = false;\n        let newFocus = null;\n        if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n            newFocus = this.menuItem;\n        }\n        // Acording to WCAG Esc and Tab are similar to arrow navigation but they\n        // force the subpanel to be closed.\n        if (event.key === 'Escape' || (event.key === 'Tab' && event.shiftKey)) {\n            newFocus = this.menuItem;\n            this.setVisibility(false);\n            this.showPreviewOnFocus = false;\n        }\n        if (event.key === 'ArrowUp') {\n            newFocus = previousFocusableElement(this.panelContent, canLoop);\n            isBrowsingSubPanel = true;\n        }\n        if (event.key === 'ArrowDown') {\n            newFocus = nextFocusableElement(this.panelContent, canLoop);\n            isBrowsingSubPanel = true;\n        }\n        if (event.key === 'Home') {\n            newFocus = firstFocusableElement(this.panelContent);\n            isBrowsingSubPanel = true;\n        }\n        if (event.key === 'End') {\n            newFocus = lastFocusableElement(this.panelContent);\n            isBrowsingSubPanel = true;\n        }\n        // If the user cannot loop and arrive to the start/end of the subpanel\n        // we focus on the menu item.\n        if (newFocus === null && isBrowsingSubPanel && !canLoop) {\n            newFocus = this.menuItem;\n        }\n        if (newFocus !== null) {\n            event.stopPropagation();\n            event.preventDefault();\n            newFocus.focus();\n        }\n    }\n\n    /**\n     * Focus on the first focusable element of the subpanel.\n     * @private\n     */\n    _focusPanelContent() {\n        const pendingPromise = new Pending('core/action_menu/subpanel:focuscontent');\n        // Some Bootstrap events are triggered after the click event.\n        // To prevent this from affecting the focus we wait a bit.\n        setTimeout(() => {\n            const firstFocusable = firstFocusableElement(this.panelContent);\n            if (firstFocusable) {\n                firstFocusable.focus();\n            }\n            pendingPromise.resolve();\n        }, 100);\n    }\n\n    /**\n     * Set the visibility of a subpanel.\n     * @param {Boolean} visible true if the subpanel should be visible.\n     */\n    setVisibility(visible) {\n        if (visible) {\n            this._hideOtherSubPanels();\n        }\n        // Aria hidden/unhidden can alter the focus, we only want to do it when needed.\n        if (!visible && this.getVisibility) {\n            hide(this.panelContent);\n        }\n        if (visible && !this.getVisibility) {\n            unhide(this.panelContent);\n        }\n        this.menuItem.setAttribute('aria-expanded', visible ? 'true' : 'false');\n        this.panelContent.classList.toggle('show', visible);\n        this.element.classList.toggle(Classes.contentDisplayed, visible);\n    }\n\n    /**\n     * Hide all other subpanels in the parent menu.\n     * @private\n     */\n    _hideOtherSubPanels() {\n        const dropdown = this.element.closest(Selectors.mainMenu);\n        dropdown.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n            const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n            if (dropdownSubPanel === this.element) {\n                return;\n            }\n            const subPanel = new SubPanel(dropdownSubPanel);\n            subPanel.setVisibility(false);\n        });\n    }\n\n    /**\n     * Get the visibility of a subpanel.\n     * @returns {Boolean} true if the subpanel is visible.\n     */\n    getVisibility() {\n        return this.menuItem.getAttribute('aria-expanded') === 'true';\n    }\n\n    /**\n     * Update the panels position depending on the screen size and panel position.\n     */\n    updatePosition() {\n        const dropdownRight = this._needDropdownRight();\n        if (this._needSmallSpaceBehaviour()) {\n            this.element.classList.remove(Classes.dropRight);\n            this.element.classList.remove(Classes.dropLeft);\n            this.element.classList.add(Classes.dropDown);\n            this.element.classList.toggle(Classes.forceLeft, dropdownRight);\n            return;\n        }\n        this.element.classList.remove(Classes.dropDown);\n        this.element.classList.remove(Classes.forceLeft);\n        this.element.classList.toggle(Classes.dropRight, !dropdownRight);\n        this.element.classList.toggle(Classes.dropLeft, dropdownRight);\n    }\n}\n\n/**\n * Initialise module for given report\n *\n * @method\n * @param {string} selector The query selector to init.\n */\nexport const init = (selector) => {\n    initPageEvents();\n    const subMenu = document.querySelector(selector);\n    if (!subMenu) {\n        throw new Error(`Sub panel element not found: ${selector}`);\n    }\n    const subPanel = new SubPanel(subMenu);\n    subPanel.init();\n};\n"],"names":["Selectors","Classes","BootstrapEvents","initialized","updateAllPanelsPosition","document","querySelectorAll","forEach","dropdown","SubPanel","updatePosition","constructor","element","menuItem","querySelector","panelContent","showPreviewOnFocus","init","this","dataset","subPanelInitialized","addEventListener","_mainElementFocusInHandler","bind","_menuItemClickHandler","subpanelMenuItemSelector","id","on","_menuItemKeyHandler","_menuItemHoverHandler","_menuItemHoverOutHandler","_panelContentKeyHandler","_needSmallSpaceBehaviour","closest","_needDropdownRight","setVisibility","event","stopPropagation","preventDefault","getVisibility","_hideOtherSubPanels","key","focusPanel","shiftKey","_focusPanelContent","canLoop","isBrowsingSubPanel","newFocus","focus","pendingPromise","Pending","setTimeout","firstFocusable","resolve","visible","setAttribute","classList","toggle","visibleSubPanel","dropdownSubPanel","getAttribute","dropdownRight","remove","add","selector","window","subMenu","Error"],"mappings":"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"}