From 413b9219bfa2504ba1b41a68dbe632840b549b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikel=20Mart=C3=ADn?= Date: Mon, 20 Jan 2025 14:13:05 +0100 Subject: [PATCH] MDL-75669 output: Fix subpanels for BS5 - Fix subpanels positioning using data-bs-popper static - Fix subpanels and dialogs event listener to use Boostrap EventHandler. Doing that will avoid Bootstrap from stop propagating the event to the subpanels and dialogs listeners. --- lib/amd/build/local/action_menu/subpanel.min.js | 4 ++-- lib/amd/build/local/action_menu/subpanel.min.js.map | 2 +- lib/amd/build/local/dropdown/dialog.min.js | 4 ++-- lib/amd/build/local/dropdown/dialog.min.js.map | 2 +- lib/amd/build/local/dropdown/status.min.js | 2 +- lib/amd/build/local/dropdown/status.min.js.map | 2 +- lib/amd/src/local/action_menu/subpanel.js | 6 +++++- lib/amd/src/local/dropdown/dialog.js | 9 +++++++-- lib/amd/src/local/dropdown/status.js | 1 + lib/templates/local/action_menu/subpanel.mustache | 2 +- 10 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/amd/build/local/action_menu/subpanel.min.js b/lib/amd/build/local/action_menu/subpanel.min.js index 2ef8659b5e3..ba7954565d1 100644 --- a/lib/amd/build/local/action_menu/subpanel.min.js +++ b/lib/amd/build/local/action_menu/subpanel.min.js @@ -1,10 +1,10 @@ -define("core/local/action_menu/subpanel",["exports","core/utils","core/pagehelpers","core/pending","core/aria"],(function(_exports,_utils,_pagehelpers,_pending,_aria){var obj; +define("core/local/action_menu/subpanel",["exports","core/utils","core/pagehelpers","core/pending","core/aria","theme_boost/bootstrap/dom/event-handler"],(function(_exports,_utils,_pagehelpers,_pending,_aria,_eventHandler){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Action menu subpanel JS controls. * * @module core/local/action_menu/subpanel * @copyright 2023 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const Selectors_mainMenu='[role="menu"]',Selectors_dropdownRight=".dropdown-menu-end",Selectors_subPanel=".dropdown-subpanel",Selectors_subPanelMenuItem=".dropdown-subpanel > .dropdown-item",Selectors_subPanelContent=".dropdown-subpanel > .dropdown-menu",Selectors_drawer='[data-region="fixed-drawer"]',Selectors_blockColumn=".blockcolumn",Selectors_columnLeft=".columnleft",Classes_dropRight="dropend",Classes_dropLeft="dropstart",Classes_dropDown="dropdown",Classes_forceLeft="downleft",Classes_contentDisplayed="content-displayed",BootstrapEvents_hideDropdown="hidden.bs.dropdown";let initialized=!1;const updateAllPanelsPosition=()=>{document.querySelectorAll(Selectors_subPanel).forEach((dropdown=>{new SubPanel(dropdown).updatePosition()}))};class SubPanel{constructor(element){this.element=element,this.menuItem=element.querySelector(Selectors_subPanelMenuItem),this.panelContent=element.querySelector(Selectors_subPanelContent),this.showPreviewOnFocus=!0}init(){this.element.dataset.subPanelInitialized||(this.updatePosition(),this.element.addEventListener("focusin",this._mainElementFocusInHandler.bind(this)),this.menuItem.addEventListener("click",this._menuItemClickHandler.bind(this)),this.menuItem.addEventListener("keydown",this._menuItemKeyHandler.bind(this)),(0,_pagehelpers.isBehatSite)()||(this.menuItem.addEventListener("mouseover",this._menuItemHoverHandler.bind(this)),this.menuItem.addEventListener("mouseout",this._menuItemHoverOutHandler.bind(this))),this.panelContent.addEventListener("keydown",this._panelContentKeyHandler.bind(this)),this.element.dataset.subPanelInitialized=!0)}_needSmallSpaceBehaviour(){return(0,_pagehelpers.isExtraSmall)()||null!==this.element.closest(Selectors_drawer)||null!==this.element.closest(Selectors_blockColumn)}_needDropdownRight(){return null===this.element.closest(Selectors_columnLeft)&&null!==this.element.closest(Selectors_dropdownRight)}_mainElementFocusInHandler(){!this._needSmallSpaceBehaviour()&&this.showPreviewOnFocus?this.setVisibility(!0):this.showPreviewOnFocus=!0}_menuItemClickHandler(event){event.stopPropagation(),event.preventDefault(),this._needSmallSpaceBehaviour()&&this.setVisibility(!this.getVisibility())}_menuItemHoverHandler(){this._needSmallSpaceBehaviour()||this.setVisibility(!0)}_menuItemHoverOutHandler(){this._needSmallSpaceBehaviour()||this._hideOtherSubPanels()}_menuItemKeyHandler(event){if("ArrowUp"===event.key||"ArrowDown"===event.key&&!this._needSmallSpaceBehaviour())return void this.setVisibility(!1);let focusPanel=!1;("ArrowRight"===event.key||"ArrowLeft"===event.key||"Tab"===event.key&&!event.shiftKey)&&(focusPanel=!0),"Enter"!==event.key&&" "!==event.key||(focusPanel=!0),"ArrowDown"===event.key&&this._needSmallSpaceBehaviour()&&this.getVisibility()&&(focusPanel=!0),focusPanel&&(event.stopPropagation(),event.preventDefault(),this.setVisibility(!0),this._focusPanelContent())}_panelContentKeyHandler(event){const canLoop=!this._needSmallSpaceBehaviour();let isBrowsingSubPanel=!1,newFocus=null;"ArrowRight"!==event.key&&"ArrowLeft"!==event.key||(newFocus=this.menuItem),("Escape"===event.key||"Tab"===event.key&&event.shiftKey)&&(newFocus=this.menuItem,this.setVisibility(!1),this.showPreviewOnFocus=!1),"ArrowUp"===event.key&&(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"ArrowDown"===event.key&&(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"Home"===event.key&&(newFocus=(0,_pagehelpers.firstFocusableElement)(this.panelContent),isBrowsingSubPanel=!0),"End"===event.key&&(newFocus=(0,_pagehelpers.lastFocusableElement)(this.panelContent),isBrowsingSubPanel=!0),null===newFocus&&isBrowsingSubPanel&&!canLoop&&(newFocus=this.menuItem),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/action_menu/subpanel:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panelContent);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisibility(visible){visible&&this._hideOtherSubPanels(),!visible&&this.getVisibility&&(0,_aria.hide)(this.panelContent),visible&&!this.getVisibility&&(0,_aria.unhide)(this.panelContent),this.menuItem.setAttribute("aria-expanded",visible?"true":"false"),this.panelContent.classList.toggle("show",visible),this.element.classList.toggle(Classes_contentDisplayed,visible)}_hideOtherSubPanels(){this.element.closest(Selectors_mainMenu).querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);if(dropdownSubPanel===this.element)return;new SubPanel(dropdownSubPanel).setVisibility(!1)}))}getVisibility(){return"true"===this.menuItem.getAttribute("aria-expanded")}updatePosition(){const dropdownRight=this._needDropdownRight();if(this._needSmallSpaceBehaviour())return this.element.classList.remove(Classes_dropRight),this.element.classList.remove(Classes_dropLeft),this.element.classList.add(Classes_dropDown),void this.element.classList.toggle(Classes_forceLeft,dropdownRight);this.element.classList.remove(Classes_dropDown),this.element.classList.remove(Classes_forceLeft),this.element.classList.toggle(Classes_dropRight,!dropdownRight),this.element.classList.toggle(Classes_dropLeft,dropdownRight)}}_exports.init=selector=>{initialized||(document.addEventListener(BootstrapEvents_hideDropdown,(()=>{document.querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);new SubPanel(dropdownSubPanel).setVisibility(!1)}))})),window.addEventListener("resize",(0,_utils.debounce)(updateAllPanelsPosition,400)),initialized=!0);const subMenu=document.querySelector(selector);if(!subMenu)throw new Error("Sub panel element not found: ".concat(selector));new SubPanel(subMenu).init()}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),_eventHandler=_interopRequireDefault(_eventHandler);const Selectors_mainMenu='[role="menu"]',Selectors_dropdownRight=".dropdown-menu-end",Selectors_subPanel=".dropdown-subpanel",Selectors_subPanelMenuItem=".dropdown-subpanel > .dropdown-item",Selectors_subPanelContent=".dropdown-subpanel > .dropdown-menu",Selectors_drawer='[data-region="fixed-drawer"]',Selectors_blockColumn=".blockcolumn",Selectors_columnLeft=".columnleft",Classes_dropRight="dropend",Classes_dropLeft="dropstart",Classes_dropDown="dropdown",Classes_forceLeft="downleft",Classes_contentDisplayed="content-displayed",BootstrapEvents_hideDropdown="hidden.bs.dropdown";let initialized=!1;const updateAllPanelsPosition=()=>{document.querySelectorAll(Selectors_subPanel).forEach((dropdown=>{new SubPanel(dropdown).updatePosition()}))};class SubPanel{constructor(element){this.element=element,this.menuItem=element.querySelector(Selectors_subPanelMenuItem),this.panelContent=element.querySelector(Selectors_subPanelContent),this.showPreviewOnFocus=!0}init(){if(this.element.dataset.subPanelInitialized)return;this.updatePosition(),this.element.addEventListener("focusin",this._mainElementFocusInHandler.bind(this)),this.menuItem.addEventListener("click",this._menuItemClickHandler.bind(this));const subpanelMenuItemSelector="#".concat(this.element.id).concat(Selectors_subPanelMenuItem);_eventHandler.default.on(document,"keydown",subpanelMenuItemSelector,this._menuItemKeyHandler.bind(this)),(0,_pagehelpers.isBehatSite)()||(this.menuItem.addEventListener("mouseover",this._menuItemHoverHandler.bind(this)),this.menuItem.addEventListener("mouseout",this._menuItemHoverOutHandler.bind(this))),this.panelContent.addEventListener("keydown",this._panelContentKeyHandler.bind(this)),this.element.dataset.subPanelInitialized=!0}_needSmallSpaceBehaviour(){return(0,_pagehelpers.isExtraSmall)()||null!==this.element.closest(Selectors_drawer)||null!==this.element.closest(Selectors_blockColumn)}_needDropdownRight(){return null===this.element.closest(Selectors_columnLeft)&&null!==this.element.closest(Selectors_dropdownRight)}_mainElementFocusInHandler(){!this._needSmallSpaceBehaviour()&&this.showPreviewOnFocus?this.setVisibility(!0):this.showPreviewOnFocus=!0}_menuItemClickHandler(event){event.stopPropagation(),event.preventDefault(),this._needSmallSpaceBehaviour()&&this.setVisibility(!this.getVisibility())}_menuItemHoverHandler(){this._needSmallSpaceBehaviour()||this.setVisibility(!0)}_menuItemHoverOutHandler(){this._needSmallSpaceBehaviour()||this._hideOtherSubPanels()}_menuItemKeyHandler(event){if("ArrowUp"===event.key||"ArrowDown"===event.key&&!this._needSmallSpaceBehaviour())return void this.setVisibility(!1);let focusPanel=!1;("ArrowRight"===event.key||"ArrowLeft"===event.key||"Tab"===event.key&&!event.shiftKey)&&(focusPanel=!0),"Enter"!==event.key&&" "!==event.key||(focusPanel=!0),"ArrowDown"===event.key&&this._needSmallSpaceBehaviour()&&this.getVisibility()&&(focusPanel=!0),focusPanel&&(event.stopPropagation(),event.preventDefault(),this.setVisibility(!0),this._focusPanelContent())}_panelContentKeyHandler(event){const canLoop=!this._needSmallSpaceBehaviour();let isBrowsingSubPanel=!1,newFocus=null;"ArrowRight"!==event.key&&"ArrowLeft"!==event.key||(newFocus=this.menuItem),("Escape"===event.key||"Tab"===event.key&&event.shiftKey)&&(newFocus=this.menuItem,this.setVisibility(!1),this.showPreviewOnFocus=!1),"ArrowUp"===event.key&&(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"ArrowDown"===event.key&&(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"Home"===event.key&&(newFocus=(0,_pagehelpers.firstFocusableElement)(this.panelContent),isBrowsingSubPanel=!0),"End"===event.key&&(newFocus=(0,_pagehelpers.lastFocusableElement)(this.panelContent),isBrowsingSubPanel=!0),null===newFocus&&isBrowsingSubPanel&&!canLoop&&(newFocus=this.menuItem),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/action_menu/subpanel:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panelContent);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisibility(visible){visible&&this._hideOtherSubPanels(),!visible&&this.getVisibility&&(0,_aria.hide)(this.panelContent),visible&&!this.getVisibility&&(0,_aria.unhide)(this.panelContent),this.menuItem.setAttribute("aria-expanded",visible?"true":"false"),this.panelContent.classList.toggle("show",visible),this.element.classList.toggle(Classes_contentDisplayed,visible)}_hideOtherSubPanels(){this.element.closest(Selectors_mainMenu).querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);if(dropdownSubPanel===this.element)return;new SubPanel(dropdownSubPanel).setVisibility(!1)}))}getVisibility(){return"true"===this.menuItem.getAttribute("aria-expanded")}updatePosition(){const dropdownRight=this._needDropdownRight();if(this._needSmallSpaceBehaviour())return this.element.classList.remove(Classes_dropRight),this.element.classList.remove(Classes_dropLeft),this.element.classList.add(Classes_dropDown),void this.element.classList.toggle(Classes_forceLeft,dropdownRight);this.element.classList.remove(Classes_dropDown),this.element.classList.remove(Classes_forceLeft),this.element.classList.toggle(Classes_dropRight,!dropdownRight),this.element.classList.toggle(Classes_dropLeft,dropdownRight)}}_exports.init=selector=>{initialized||(document.addEventListener(BootstrapEvents_hideDropdown,(()=>{document.querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);new SubPanel(dropdownSubPanel).setVisibility(!1)}))})),window.addEventListener("resize",(0,_utils.debounce)(updateAllPanelsPosition,400)),initialized=!0);const subMenu=document.querySelector(selector);if(!subMenu)throw new Error("Sub panel element not found: ".concat(selector));new SubPanel(subMenu).init()}})); //# sourceMappingURL=subpanel.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/action_menu/subpanel.min.js.map b/lib/amd/build/local/action_menu/subpanel.min.js.map index 55d2911812f..ff14def10ef 100644 --- a/lib/amd/build/local/action_menu/subpanel.min.js.map +++ b/lib/amd/build/local/action_menu/subpanel.min.js.map @@ -1 +1 @@ -{"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 .\n\n/**\n * Action menu subpanel JS controls.\n *\n * @module core/local/action_menu/subpanel\n * @copyright 2023 Mikel Martín \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';\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 this.menuItem.addEventListener('keydown', 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","_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":";;;;;;;kJAsCMA,mBACQ,gBADRA,wBAEa,qBAFbA,mBAGQ,qBAHRA,2BAIgB,sCAJhBA,0BAKe,sCALfA,iBAOM,+BAPNA,sBASW,eATXA,qBAUU,cAGVC,kBACS,UADTA,iBAEQ,YAFRA,iBAGQ,WAHRA,kBAIS,WAJTA,yBAKgB,oBAGhBC,6BACY,yBAGdC,aAAc,QA0BZC,wBAA0B,KAC5BC,SAASC,iBAAiBN,oBAAoBO,SAAQC,WACjC,IAAIC,SAASD,UACrBE,2BAQXD,SAKFE,YAAYC,cACHA,QAAUA,aACVC,SAAWD,QAAQE,cAAcd,iCACjCe,aAAeH,QAAQE,cAAcd,gCASrCgB,oBAAqB,EAQ9BC,OACQC,KAAKN,QAAQO,QAAQC,2BAIpBV,sBAGAE,QAAQS,iBAAiB,UAAWH,KAAKI,2BAA2BC,KAAKL,YAEzEL,SAASQ,iBAAiB,QAASH,KAAKM,sBAAsBD,KAAKL,YACnEL,SAASQ,iBAAiB,UAAWH,KAAKO,oBAAoBF,KAAKL,QACnE,qCAGIL,SAASQ,iBAAiB,YAAaH,KAAKQ,sBAAsBH,KAAKL,YACvEL,SAASQ,iBAAiB,WAAYH,KAAKS,yBAAyBJ,KAAKL,aAG7EH,aAAaM,iBAAiB,UAAWH,KAAKU,wBAAwBL,KAAKL,YAE3EN,QAAQO,QAAQC,qBAAsB,GAa/CS,kCACW,gCACwC,OAA3CX,KAAKN,QAAQkB,QAAQ9B,mBAC2B,OAAhDkB,KAAKN,QAAQkB,QAAQ9B,uBAY7B+B,4BACuD,OAA/Cb,KAAKN,QAAQkB,QAAQ9B,uBAGgC,OAAlDkB,KAAKN,QAAQkB,QAAQ9B,yBAMhCsB,8BACQJ,KAAKW,4BAA+BX,KAAKF,wBAMxCgB,eAAc,QAHVhB,oBAAqB,EAUlCQ,sBAAsBS,OAGlBA,MAAMC,kBACND,MAAME,iBACFjB,KAAKW,iCACAG,eAAed,KAAKkB,iBAQjCV,wBACQR,KAAKW,iCAGJG,eAAc,GAOvBL,2BACQT,KAAKW,iCAGJQ,sBAQTZ,oBAAoBQ,UAEE,YAAdA,MAAMK,KAAoC,cAAdL,MAAMK,MAAwBpB,KAAKW,4CAC1DG,eAAc,OAKnBO,YAAa,GAEC,eAAdN,MAAMK,KAAsC,cAAdL,MAAMK,KAAsC,QAAdL,MAAMK,MAAkBL,MAAMO,YAC1FD,YAAa,GAEE,UAAdN,MAAMK,KAAiC,MAAdL,MAAMK,MAChCC,YAAa,GAGC,cAAdN,MAAMK,KAAuBpB,KAAKW,4BAA8BX,KAAKkB,kBACrEG,YAAa,GAEbA,aACAN,MAAMC,kBACND,MAAME,sBACDH,eAAc,QACdS,sBAUbb,wBAAwBK,aAGdS,SAAWxB,KAAKW,+BAClBc,oBAAqB,EACrBC,SAAW,KACG,eAAdX,MAAMK,KAAsC,cAAdL,MAAMK,MACpCM,SAAW1B,KAAKL,WAIF,WAAdoB,MAAMK,KAAmC,QAAdL,MAAMK,KAAiBL,MAAMO,YACxDI,SAAW1B,KAAKL,cACXmB,eAAc,QACdhB,oBAAqB,GAEZ,YAAdiB,MAAMK,MACNM,UAAW,yCAAyB1B,KAAKH,aAAc2B,SACvDC,oBAAqB,GAEP,cAAdV,MAAMK,MACNM,UAAW,qCAAqB1B,KAAKH,aAAc2B,SACnDC,oBAAqB,GAEP,SAAdV,MAAMK,MACNM,UAAW,sCAAsB1B,KAAKH,cACtC4B,oBAAqB,GAEP,QAAdV,MAAMK,MACNM,UAAW,qCAAqB1B,KAAKH,cACrC4B,oBAAqB,GAIR,OAAbC,UAAqBD,qBAAuBD,UAC5CE,SAAW1B,KAAKL,UAEH,OAAb+B,WACAX,MAAMC,kBACND,MAAME,iBACNS,SAASC,SAQjBJ,2BACUK,eAAiB,IAAIC,iBAAQ,0CAGnCC,YAAW,WACDC,gBAAiB,sCAAsB/B,KAAKH,cAC9CkC,gBACAA,eAAeJ,QAEnBC,eAAeI,YAChB,KAOPlB,cAAcmB,SACNA,cACKd,uBAGJc,SAAWjC,KAAKkB,8BACZlB,KAAKH,cAEVoC,UAAYjC,KAAKkB,gCACVlB,KAAKH,mBAEXF,SAASuC,aAAa,gBAAiBD,QAAU,OAAS,cAC1DpC,aAAasC,UAAUC,OAAO,OAAQH,cACtCvC,QAAQyC,UAAUC,OAAOrD,yBAA0BkD,SAO5Dd,sBACqBnB,KAAKN,QAAQkB,QAAQ9B,oBAC7BM,2BAAoBN,oCAAkCO,SAAQgD,wBAC7DC,iBAAmBD,gBAAgBzB,QAAQ9B,uBAC7CwD,mBAAqBtC,KAAKN,eAGb,IAAIH,SAAS+C,kBACrBxB,eAAc,MAQ/BI,sBAC2D,SAAhDlB,KAAKL,SAAS4C,aAAa,iBAMtC/C,uBACUgD,cAAgBxC,KAAKa,wBACvBb,KAAKW,uCACAjB,QAAQyC,UAAUM,OAAO1D,wBACzBW,QAAQyC,UAAUM,OAAO1D,uBACzBW,QAAQyC,UAAUO,IAAI3D,4BACtBW,QAAQyC,UAAUC,OAAOrD,kBAAmByD,oBAGhD9C,QAAQyC,UAAUM,OAAO1D,uBACzBW,QAAQyC,UAAUM,OAAO1D,wBACzBW,QAAQyC,UAAUC,OAAOrD,mBAAoByD,oBAC7C9C,QAAQyC,UAAUC,OAAOrD,iBAAkByD,8BAUnCG,WA1Ub1D,cAIJE,SAASgB,iBAAiBnB,8BAA8B,KACpDG,SAASC,2BAAoBN,oCAAkCO,SAAQgD,wBAC7DC,iBAAmBD,gBAAgBzB,QAAQ9B,oBAChC,IAAIS,SAAS+C,kBACrBxB,eAAc,SAI/B8B,OAAOzC,iBAAiB,UAAU,mBAASjB,wBAAyB,MAEpED,aAAc,SA8TR4D,QAAU1D,SAASS,cAAc+C,cAClCE,cACK,IAAIC,6CAAsCH,WAEnC,IAAIpD,SAASsD,SACrB9C"} \ No newline at end of file +{"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 .\n\n/**\n * Action menu subpanel JS controls.\n *\n * @module core/local/action_menu/subpanel\n * @copyright 2023 Mikel Martín \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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"} \ No newline at end of file diff --git a/lib/amd/build/local/dropdown/dialog.min.js b/lib/amd/build/local/dropdown/dialog.min.js index 281a0829628..3c3825490dd 100644 --- a/lib/amd/build/local/dropdown/dialog.min.js +++ b/lib/amd/build/local/dropdown/dialog.min.js @@ -1,10 +1,10 @@ -define("core/local/dropdown/dialog",["exports","theme_boost/bootstrap/dropdown","core/pagehelpers","core/pending"],(function(_exports,_dropdown,_pagehelpers,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("core/local/dropdown/dialog",["exports","theme_boost/bootstrap/dropdown","core/pagehelpers","core/pending","theme_boost/bootstrap/dom/event-handler"],(function(_exports,_dropdown,_pagehelpers,_pending,_eventHandler){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Dropdown status JS controls. * * @module core/local/dropdown/dialog * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.getDropdownDialog=_exports.DropdownDialog=void 0,_dropdown=_interopRequireDefault(_dropdown),_pending=_interopRequireDefault(_pending);const Selectors_dropdownButton='[data-for="dropdowndialog_button"]',Selectors_dropdownDialog='[data-for="dropdowndialog_dialog"]';class DropdownDialog{constructor(element){this.element=element,this.button=element.querySelector(Selectors_dropdownButton),this.panel=element.querySelector(Selectors_dropdownDialog)}init(){this.element.dataset.dropdownDialogInitialized||(this.button.addEventListener("keydown",this._buttonKeyHandler.bind(this)),this.panel.addEventListener("keydown",this._contentKeyHandler.bind(this)),this.element.dataset.dropdownDialogInitialized=!0)}_buttonKeyHandler(event){if("ArrowUp"===event.key||"ArrowLeft"===event.key)return event.stopPropagation(),event.preventDefault(),void this.setVisible(!1);"ArrowDown"!==event.key&&"ArrowRight"!==event.key||(event.stopPropagation(),event.preventDefault(),this.setVisible(!0),this._focusPanelContent())}_contentKeyHandler(event){let newFocus=null;"End"===event.key&&(newFocus=(0,_pagehelpers.lastFocusableElement)(this.panel)),"Home"===event.key&&(newFocus=(0,_pagehelpers.firstFocusableElement)(this.panel)),"ArrowUp"!==event.key&&"ArrowLeft"!==event.key||(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panel,!1),newFocus||(newFocus=this.button)),"ArrowDown"!==event.key&&"ArrowRight"!==event.key||(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panel,!1)),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/dropdown/dialog:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panel);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisible(visible){visible!==this.isVisible()&&_dropdown.default.getOrCreateInstance(this.button).toggle()}isVisible(){return"true"===this.button.getAttribute("aria-expanded")}setButtonContent(content){this.button.innerHTML=content}setButtonDisabled(disabled){disabled?this.button.setAttribute("disabled","disabled"):this.button.removeAttribute("disabled")}getElement(){return this.element}}_exports.DropdownDialog=DropdownDialog;const getDropdownDialog=selector=>{const dropdownElement=document.querySelector(selector);return dropdownElement?new DropdownDialog(dropdownElement):null};_exports.getDropdownDialog=getDropdownDialog;_exports.init=selector=>{const dropdown=getDropdownDialog(selector);if(!dropdown)throw new Error("Dopdown dialog element not found: ".concat(selector));dropdown.init()}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.getDropdownDialog=_exports.DropdownDialog=void 0,_dropdown=_interopRequireDefault(_dropdown),_pending=_interopRequireDefault(_pending),_eventHandler=_interopRequireDefault(_eventHandler);const Selectors_dropdownButton='[data-for="dropdowndialog_button"]',Selectors_dropdownDialog='[data-for="dropdowndialog_dialog"]';class DropdownDialog{constructor(element){this.element=element,this.button=element.querySelector(Selectors_dropdownButton),this.panel=element.querySelector(Selectors_dropdownDialog)}init(){if(this.element.dataset.dropdownDialogInitialized)return;const dialogButtonSelector="#".concat(this.element.id," ").concat(Selectors_dropdownButton);_eventHandler.default.on(document,"keydown",dialogButtonSelector,this._buttonKeyHandler.bind(this));const dialogSelector="#".concat(this.element.id," ").concat(Selectors_dropdownDialog);_eventHandler.default.on(document,"keydown",dialogSelector,this._contentKeyHandler.bind(this)),this.element.dataset.dropdownDialogInitialized=!0}_buttonKeyHandler(event){if("ArrowUp"===event.key||"ArrowLeft"===event.key)return event.stopPropagation(),event.preventDefault(),void this.setVisible(!1);"ArrowDown"!==event.key&&"ArrowRight"!==event.key||(event.stopPropagation(),event.preventDefault(),this.setVisible(!0),this._focusPanelContent())}_contentKeyHandler(event){let newFocus=null;"End"===event.key&&(newFocus=(0,_pagehelpers.lastFocusableElement)(this.panel)),"Home"===event.key&&(newFocus=(0,_pagehelpers.firstFocusableElement)(this.panel)),"ArrowUp"!==event.key&&"ArrowLeft"!==event.key||(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panel,!1),newFocus||(newFocus=this.button)),"ArrowDown"!==event.key&&"ArrowRight"!==event.key||(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panel,!1)),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/dropdown/dialog:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panel);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisible(visible){visible!==this.isVisible()&&_dropdown.default.getOrCreateInstance(this.button).toggle()}isVisible(){return"true"===this.button.getAttribute("aria-expanded")}setButtonContent(content){this.button.innerHTML=content}setButtonDisabled(disabled){disabled?this.button.setAttribute("disabled","disabled"):this.button.removeAttribute("disabled")}getElement(){return this.element}}_exports.DropdownDialog=DropdownDialog;const getDropdownDialog=selector=>{const dropdownElement=document.querySelector(selector);return dropdownElement?new DropdownDialog(dropdownElement):null};_exports.getDropdownDialog=getDropdownDialog;_exports.init=selector=>{const dropdown=getDropdownDialog(selector);if(!dropdown)throw new Error("Dopdown dialog element not found: ".concat(selector));dropdown.init()}})); //# sourceMappingURL=dialog.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/dropdown/dialog.min.js.map b/lib/amd/build/local/dropdown/dialog.min.js.map index a7f7705689d..ae3ccf98e6b 100644 --- a/lib/amd/build/local/dropdown/dialog.min.js.map +++ b/lib/amd/build/local/dropdown/dialog.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dialog.min.js","sources":["../../../src/local/dropdown/dialog.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 .\n\n/**\n * Dropdown status JS controls.\n *\n * @module core/local/dropdown/dialog\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Dropdown from 'theme_boost/bootstrap/dropdown';\nimport {\n firstFocusableElement,\n lastFocusableElement,\n previousFocusableElement,\n nextFocusableElement,\n} from 'core/pagehelpers';\nimport Pending from 'core/pending';\n\nconst Selectors = {\n dropdownButton: '[data-for=\"dropdowndialog_button\"]',\n dropdownDialog: '[data-for=\"dropdowndialog_dialog\"]',\n};\n\n/**\n * Dropdown dialog class.\n * @private\n */\nexport class DropdownDialog {\n /**\n * Constructor.\n * @param {HTMLElement} element The element to initialize.\n */\n constructor(element) {\n this.element = element;\n this.button = element.querySelector(Selectors.dropdownButton);\n this.panel = element.querySelector(Selectors.dropdownDialog);\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.dropdownDialogInitialized) {\n return;\n }\n\n // Menu Item events.\n this.button.addEventListener('keydown', this._buttonKeyHandler.bind(this));\n // Subpanel content events.\n this.panel.addEventListener('keydown', this._contentKeyHandler.bind(this));\n\n this.element.dataset.dropdownDialogInitialized = true;\n }\n\n /**\n * Dropdown button key handler.\n * @param {Event} event\n * @private\n */\n _buttonKeyHandler(event) {\n if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {\n event.stopPropagation();\n event.preventDefault();\n this.setVisible(false);\n return;\n }\n\n if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {\n event.stopPropagation();\n event.preventDefault();\n this.setVisible(true);\n this._focusPanelContent();\n }\n }\n\n /**\n * Sub panel content key handler.\n * @param {Event} event\n * @private\n */\n _contentKeyHandler(event) {\n let newFocus = null;\n\n if (event.key === 'End') {\n newFocus = lastFocusableElement(this.panel);\n }\n if (event.key === 'Home') {\n newFocus = firstFocusableElement(this.panel);\n }\n if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {\n newFocus = previousFocusableElement(this.panel, false);\n if (!newFocus) {\n newFocus = this.button;\n }\n }\n if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {\n newFocus = nextFocusableElement(this.panel, false);\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/dropdown/dialog: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.panel);\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 setVisible(visible) {\n if (visible === this.isVisible()) {\n return;\n }\n Dropdown.getOrCreateInstance(this.button).toggle();\n }\n\n /**\n * Get the visibility of a subpanel.\n * @returns {Boolean} true if the subpanel is visible.\n */\n isVisible() {\n return this.button.getAttribute('aria-expanded') === 'true';\n }\n\n /**\n * Set the content of the button.\n * @param {String} content\n */\n setButtonContent(content) {\n this.button.innerHTML = content;\n }\n\n /**\n * Set the disabled state of the button.\n * @param {Boolean} disabled\n */\n setButtonDisabled(disabled) {\n if (disabled) {\n this.button.setAttribute('disabled', 'disabled');\n } else {\n this.button.removeAttribute('disabled');\n }\n }\n\n /**\n * Return the main dropdown HTML element.\n * @returns {HTMLElement} The element.\n */\n getElement() {\n return this.element;\n }\n}\n\n/**\n * Get the dropdown dialog instance from a selector.\n * @param {string} selector The query selector to init.\n * @returns {DropdownDialog|null} The dropdown dialog instance if any.\n */\nexport const getDropdownDialog = (selector) => {\n const dropdownElement = document.querySelector(selector);\n if (!dropdownElement) {\n return null;\n }\n return new DropdownDialog(dropdownElement);\n};\n\n/**\n * Initialize module.\n *\n * @method\n * @param {string} selector The query selector to init.\n */\nexport const init = (selector) => {\n const dropdown = getDropdownDialog(selector);\n if (!dropdown) {\n throw new Error(`Dopdown dialog element not found: ${selector}`);\n }\n dropdown.init();\n};\n"],"names":["Selectors","DropdownDialog","constructor","element","button","querySelector","panel","init","this","dataset","dropdownDialogInitialized","addEventListener","_buttonKeyHandler","bind","_contentKeyHandler","event","key","stopPropagation","preventDefault","setVisible","_focusPanelContent","newFocus","focus","pendingPromise","Pending","setTimeout","firstFocusable","resolve","visible","isVisible","getOrCreateInstance","toggle","getAttribute","setButtonContent","content","innerHTML","setButtonDisabled","disabled","setAttribute","removeAttribute","getElement","getDropdownDialog","selector","dropdownElement","document","dropdown","Error"],"mappings":";;;;;;;iOAgCMA,yBACc,qCADdA,yBAEc,2CAOPC,eAKTC,YAAYC,cACHA,QAAUA,aACVC,OAASD,QAAQE,cAAcL,+BAC/BM,MAAQH,QAAQE,cAAcL,0BAQvCO,OACQC,KAAKL,QAAQM,QAAQC,iCAKpBN,OAAOO,iBAAiB,UAAWH,KAAKI,kBAAkBC,KAAKL,YAE/DF,MAAMK,iBAAiB,UAAWH,KAAKM,mBAAmBD,KAAKL,YAE/DL,QAAQM,QAAQC,2BAA4B,GAQrDE,kBAAkBG,UACI,YAAdA,MAAMC,KAAmC,cAAdD,MAAMC,WACjCD,MAAME,kBACNF,MAAMG,2BACDC,YAAW,GAIF,cAAdJ,MAAMC,KAAqC,eAAdD,MAAMC,MACnCD,MAAME,kBACNF,MAAMG,sBACDC,YAAW,QACXC,sBASbN,mBAAmBC,WACXM,SAAW,KAEG,QAAdN,MAAMC,MACNK,UAAW,qCAAqBb,KAAKF,QAEvB,SAAdS,MAAMC,MACNK,UAAW,sCAAsBb,KAAKF,QAExB,YAAdS,MAAMC,KAAmC,cAAdD,MAAMC,MACjCK,UAAW,yCAAyBb,KAAKF,OAAO,GAC3Ce,WACDA,SAAWb,KAAKJ,SAGN,cAAdW,MAAMC,KAAqC,eAAdD,MAAMC,MACnCK,UAAW,qCAAqBb,KAAKF,OAAO,IAE/B,OAAbe,WACAN,MAAME,kBACNF,MAAMG,iBACNG,SAASC,SAQjBF,2BACUG,eAAiB,IAAIC,iBAAQ,qCAGnCC,YAAW,WACDC,gBAAiB,sCAAsBlB,KAAKF,OAC9CoB,gBACAA,eAAeJ,QAEnBC,eAAeI,YAChB,KAOPR,WAAWS,SACHA,UAAYpB,KAAKqB,+BAGZC,oBAAoBtB,KAAKJ,QAAQ2B,SAO9CF,kBACyD,SAA9CrB,KAAKJ,OAAO4B,aAAa,iBAOpCC,iBAAiBC,cACR9B,OAAO+B,UAAYD,QAO5BE,kBAAkBC,UACVA,cACKjC,OAAOkC,aAAa,WAAY,iBAEhClC,OAAOmC,gBAAgB,YAQpCC,oBACWhC,KAAKL,sDASPsC,kBAAqBC,iBACxBC,gBAAkBC,SAASvC,cAAcqC,iBAC1CC,gBAGE,IAAI1C,eAAe0C,iBAFf,iEAWMD,iBACXG,SAAWJ,kBAAkBC,cAC9BG,eACK,IAAIC,kDAA2CJ,WAEzDG,SAAStC"} \ No newline at end of file +{"version":3,"file":"dialog.min.js","sources":["../../../src/local/dropdown/dialog.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 .\n\n/**\n * Dropdown status JS controls.\n *\n * @module core/local/dropdown/dialog\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Dropdown from 'theme_boost/bootstrap/dropdown';\nimport {\n firstFocusableElement,\n lastFocusableElement,\n previousFocusableElement,\n nextFocusableElement,\n} from 'core/pagehelpers';\nimport Pending from 'core/pending';\nimport EventHandler from 'theme_boost/bootstrap/dom/event-handler';\n\nconst Selectors = {\n dropdownButton: '[data-for=\"dropdowndialog_button\"]',\n dropdownDialog: '[data-for=\"dropdowndialog_dialog\"]',\n};\n\n/**\n * Dropdown dialog class.\n * @private\n */\nexport class DropdownDialog {\n /**\n * Constructor.\n * @param {HTMLElement} element The element to initialize.\n */\n constructor(element) {\n this.element = element;\n this.button = element.querySelector(Selectors.dropdownButton);\n this.panel = element.querySelector(Selectors.dropdownDialog);\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.dropdownDialogInitialized) {\n return;\n }\n\n // Use the Bootstrap key handler for the Dropdown button key handler.\n // This will avoid Boostrap Dropdown handler to prevent the propagation to the dialog.\n // Menu Item events.\n const dialogButtonSelector = `#${this.element.id} ${Selectors.dropdownButton}`;\n EventHandler.on(document, 'keydown', dialogButtonSelector, this._buttonKeyHandler.bind(this));\n // Subpanel content events.\n const dialogSelector = `#${this.element.id} ${Selectors.dropdownDialog}`;\n EventHandler.on(document, 'keydown', dialogSelector, this._contentKeyHandler.bind(this));\n\n this.element.dataset.dropdownDialogInitialized = true;\n }\n\n /**\n * Dropdown button key handler.\n * @param {Event} event\n * @private\n */\n _buttonKeyHandler(event) {\n if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {\n event.stopPropagation();\n event.preventDefault();\n this.setVisible(false);\n return;\n }\n\n if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {\n event.stopPropagation();\n event.preventDefault();\n this.setVisible(true);\n this._focusPanelContent();\n }\n }\n\n /**\n * Sub panel content key handler.\n * @param {Event} event\n * @private\n */\n _contentKeyHandler(event) {\n let newFocus = null;\n\n if (event.key === 'End') {\n newFocus = lastFocusableElement(this.panel);\n }\n if (event.key === 'Home') {\n newFocus = firstFocusableElement(this.panel);\n }\n if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {\n newFocus = previousFocusableElement(this.panel, false);\n if (!newFocus) {\n newFocus = this.button;\n }\n }\n if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {\n newFocus = nextFocusableElement(this.panel, false);\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/dropdown/dialog: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.panel);\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 setVisible(visible) {\n if (visible === this.isVisible()) {\n return;\n }\n Dropdown.getOrCreateInstance(this.button).toggle();\n }\n\n /**\n * Get the visibility of a subpanel.\n * @returns {Boolean} true if the subpanel is visible.\n */\n isVisible() {\n return this.button.getAttribute('aria-expanded') === 'true';\n }\n\n /**\n * Set the content of the button.\n * @param {String} content\n */\n setButtonContent(content) {\n this.button.innerHTML = content;\n }\n\n /**\n * Set the disabled state of the button.\n * @param {Boolean} disabled\n */\n setButtonDisabled(disabled) {\n if (disabled) {\n this.button.setAttribute('disabled', 'disabled');\n } else {\n this.button.removeAttribute('disabled');\n }\n }\n\n /**\n * Return the main dropdown HTML element.\n * @returns {HTMLElement} The element.\n */\n getElement() {\n return this.element;\n }\n}\n\n/**\n * Get the dropdown dialog instance from a selector.\n * @param {string} selector The query selector to init.\n * @returns {DropdownDialog|null} The dropdown dialog instance if any.\n */\nexport const getDropdownDialog = (selector) => {\n const dropdownElement = document.querySelector(selector);\n if (!dropdownElement) {\n return null;\n }\n return new DropdownDialog(dropdownElement);\n};\n\n/**\n * Initialize module.\n *\n * @method\n * @param {string} selector The query selector to init.\n */\nexport const init = (selector) => {\n const dropdown = getDropdownDialog(selector);\n if (!dropdown) {\n throw new Error(`Dopdown dialog element not found: ${selector}`);\n }\n dropdown.init();\n};\n"],"names":["Selectors","DropdownDialog","constructor","element","button","querySelector","panel","init","this","dataset","dropdownDialogInitialized","dialogButtonSelector","id","on","document","_buttonKeyHandler","bind","dialogSelector","_contentKeyHandler","event","key","stopPropagation","preventDefault","setVisible","_focusPanelContent","newFocus","focus","pendingPromise","Pending","setTimeout","firstFocusable","resolve","visible","isVisible","getOrCreateInstance","toggle","getAttribute","setButtonContent","content","innerHTML","setButtonDisabled","disabled","setAttribute","removeAttribute","getElement","getDropdownDialog","selector","dropdownElement","dropdown","Error"],"mappings":";;;;;;;qRAiCMA,yBACc,qCADdA,yBAEc,2CAOPC,eAKTC,YAAYC,cACHA,QAAUA,aACVC,OAASD,QAAQE,cAAcL,+BAC/BM,MAAQH,QAAQE,cAAcL,0BAQvCO,UACQC,KAAKL,QAAQM,QAAQC,uCAOnBC,gCAA2BH,KAAKL,QAAQS,eAAMZ,gDACvCa,GAAGC,SAAU,UAAWH,qBAAsBH,KAAKO,kBAAkBC,KAAKR,aAEjFS,0BAAqBT,KAAKL,QAAQS,eAAMZ,gDACjCa,GAAGC,SAAU,UAAWG,eAAgBT,KAAKU,mBAAmBF,KAAKR,YAE7EL,QAAQM,QAAQC,2BAA4B,EAQrDK,kBAAkBI,UACI,YAAdA,MAAMC,KAAmC,cAAdD,MAAMC,WACjCD,MAAME,kBACNF,MAAMG,2BACDC,YAAW,GAIF,cAAdJ,MAAMC,KAAqC,eAAdD,MAAMC,MACnCD,MAAME,kBACNF,MAAMG,sBACDC,YAAW,QACXC,sBASbN,mBAAmBC,WACXM,SAAW,KAEG,QAAdN,MAAMC,MACNK,UAAW,qCAAqBjB,KAAKF,QAEvB,SAAda,MAAMC,MACNK,UAAW,sCAAsBjB,KAAKF,QAExB,YAAda,MAAMC,KAAmC,cAAdD,MAAMC,MACjCK,UAAW,yCAAyBjB,KAAKF,OAAO,GAC3CmB,WACDA,SAAWjB,KAAKJ,SAGN,cAAde,MAAMC,KAAqC,eAAdD,MAAMC,MACnCK,UAAW,qCAAqBjB,KAAKF,OAAO,IAE/B,OAAbmB,WACAN,MAAME,kBACNF,MAAMG,iBACNG,SAASC,SAQjBF,2BACUG,eAAiB,IAAIC,iBAAQ,qCAGnCC,YAAW,WACDC,gBAAiB,sCAAsBtB,KAAKF,OAC9CwB,gBACAA,eAAeJ,QAEnBC,eAAeI,YAChB,KAOPR,WAAWS,SACHA,UAAYxB,KAAKyB,+BAGZC,oBAAoB1B,KAAKJ,QAAQ+B,SAO9CF,kBACyD,SAA9CzB,KAAKJ,OAAOgC,aAAa,iBAOpCC,iBAAiBC,cACRlC,OAAOmC,UAAYD,QAO5BE,kBAAkBC,UACVA,cACKrC,OAAOsC,aAAa,WAAY,iBAEhCtC,OAAOuC,gBAAgB,YAQpCC,oBACWpC,KAAKL,sDASP0C,kBAAqBC,iBACxBC,gBAAkBjC,SAAST,cAAcyC,iBAC1CC,gBAGE,IAAI9C,eAAe8C,iBAFf,iEAWMD,iBACXE,SAAWH,kBAAkBC,cAC9BE,eACK,IAAIC,kDAA2CH,WAEzDE,SAASzC"} \ No newline at end of file diff --git a/lib/amd/build/local/dropdown/status.min.js b/lib/amd/build/local/dropdown/status.min.js index bd0b50cbb52..c2556faa15c 100644 --- a/lib/amd/build/local/dropdown/status.min.js +++ b/lib/amd/build/local/dropdown/status.min.js @@ -11,6 +11,6 @@ define("core/local/dropdown/status",["exports","core/local/dropdown/dialog"],(fu * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const Selectors_checkedIcon='[data-for="checkedIcon"]',Selectors_option='[role="option"]',Selectors_optionItem="[data-optionnumber]",Selectors_optionIcon=".option-icon",Selectors_selectedOption='[role="option"][aria-selected="true"]',Selectors_uncheckedIcon='[data-for="uncheckedIcon"]',Classes_selected="selected",Classes_disabled="disabled",Classes_hidden="d-none";class DropdownStatus extends _dialog.DropdownDialog{constructor(element){super(element),this.buttonSync="true"==element.dataset.buttonSync,this.updateStatus="true"==element.dataset.updateStatus}init(){super.init(),this.element.dataset.dropdownStatusInitialized||(this.panel.addEventListener("click",this._contentClickHandler.bind(this)),"true"==this.element.dataset.buttonSync&&this.setButtonSyncEnabled(!0),"true"==this.element.dataset.updateStatus&&this.setUpdateStatusEnabled(!0),this.element.dataset.dropdownStatusInitialized=!0)}_contentClickHandler(event){const option=event.target.closest(Selectors_option);option&&"true"!==option.getAttribute("aria-disabled")&&"true"!==option.getAttribute("aria-selected")&&this.isUpdateStatusEnabled()&&this.setSelectedValue(option.dataset.value)}setSelectedValue(value){const selected=this.panel.querySelector(Selectors_selectedOption);if(selected&&selected.dataset.value===value)return;selected&&this._updateOptionChecked(selected,!1);const option=this.panel.querySelector("".concat(Selectors_option,'[data-value="').concat(value,'"]'));option&&this._updateOptionChecked(option,!0),this.isButtonSyncEnabled()&&this.syncButtonText(),this.element.dispatchEvent(new Event("change"))}_updateOptionChecked(option,checked){option.setAttribute("aria-selected",checked.toString()),option.classList.toggle(Classes_selected,checked),option.classList.toggle(Classes_disabled,checked);const optionItem=option.closest(Selectors_optionItem);optionItem&&this._updateOptionItemChecked(optionItem,checked),checked?this.element.dataset.value=option.dataset.value:this.element.dataset.value===option.dataset.value&&delete this.element.dataset.value}_updateOptionItemChecked(optionItem,checked){var _optionItem$dataset$s;const selectedClasses=null!==(_optionItem$dataset$s=optionItem.dataset.selectedClasses)&&void 0!==_optionItem$dataset$s?_optionItem$dataset$s:Classes_selected;for(const selectedClass of selectedClasses.split(" "))optionItem.classList.toggle(selectedClass,checked);checked?optionItem.dataset.selected=checked:null==optionItem||delete optionItem.dataset.selected;const checkedIcon=optionItem.querySelector(Selectors_checkedIcon);checkedIcon&&checkedIcon.classList.toggle(Classes_hidden,!checked);const uncheckedIcon=optionItem.querySelector(Selectors_uncheckedIcon);uncheckedIcon&&uncheckedIcon.classList.toggle(Classes_hidden,checked)}getSelectedValue(){var _selected$dataset$val;const selected=this.panel.querySelector(Selectors_selectedOption);return null!==(_selected$dataset$val=null==selected?void 0:selected.dataset.value)&&void 0!==_selected$dataset$val?_selected$dataset$val:null}setButtonSyncEnabled(value){value?this.element.dataset.buttonSync="true":delete this.element.dataset.buttonSync,value&&this.syncButtonText()}isButtonSyncEnabled(){return"true"==this.element.dataset.buttonSync}syncButtonText(){const selected=this.panel.querySelector(Selectors_selectedOption);if(!selected)return;let newText=selected.textContent;const optionIcon=this._getOptionIcon(selected);optionIcon&&(newText=optionIcon.innerHTML+newText),this.button.innerHTML=newText}setUpdateStatusEnabled(value){value?this.element.dataset.updateStatus="true":delete this.element.dataset.updateStatus}isUpdateStatusEnabled(){return"true"==this.element.dataset.updateStatus}_getOptionIcon(option){const optionItem=option.closest(Selectors_optionItem);return optionItem?optionItem.querySelector(Selectors_optionIcon):null}}_exports.DropdownStatus=DropdownStatus;const getDropdownStatus=selector=>{const dropdownElement=document.querySelector(selector);return dropdownElement?new DropdownStatus(dropdownElement):null};_exports.getDropdownStatus=getDropdownStatus;_exports.init=selector=>{const dropdown=getDropdownStatus(selector);if(!dropdown)throw new Error("Dopdown status element not found: ".concat(selector));dropdown.init()}})); +const Selectors_checkedIcon='[data-for="checkedIcon"]',Selectors_option='[role="option"]',Selectors_optionItem="[data-optionnumber]",Selectors_optionIcon=".option-icon",Selectors_selectedOption='[role="option"][aria-selected="true"]',Selectors_uncheckedIcon='[data-for="uncheckedIcon"]',Classes_selected="selected",Classes_disabled="disabled",Classes_hidden="d-none";class DropdownStatus extends _dialog.DropdownDialog{constructor(element){super(element),this.buttonSync="true"==element.dataset.buttonSync,this.updateStatus="true"==element.dataset.updateStatus}init(){super.init(),this.element.dataset.dropdownStatusInitialized||(this.panel.addEventListener("click",this._contentClickHandler.bind(this)),"true"==this.element.dataset.buttonSync&&this.setButtonSyncEnabled(!0),"true"==this.element.dataset.updateStatus&&this.setUpdateStatusEnabled(!0),this.element.dataset.dropdownStatusInitialized=!0)}_contentClickHandler(event){const option=event.target.closest(Selectors_option);option&&"true"!==option.getAttribute("aria-disabled")&&"true"!==option.getAttribute("aria-selected")&&this.isUpdateStatusEnabled()&&(this.setSelectedValue(option.dataset.value),this.button.focus())}setSelectedValue(value){const selected=this.panel.querySelector(Selectors_selectedOption);if(selected&&selected.dataset.value===value)return;selected&&this._updateOptionChecked(selected,!1);const option=this.panel.querySelector("".concat(Selectors_option,'[data-value="').concat(value,'"]'));option&&this._updateOptionChecked(option,!0),this.isButtonSyncEnabled()&&this.syncButtonText(),this.element.dispatchEvent(new Event("change"))}_updateOptionChecked(option,checked){option.setAttribute("aria-selected",checked.toString()),option.classList.toggle(Classes_selected,checked),option.classList.toggle(Classes_disabled,checked);const optionItem=option.closest(Selectors_optionItem);optionItem&&this._updateOptionItemChecked(optionItem,checked),checked?this.element.dataset.value=option.dataset.value:this.element.dataset.value===option.dataset.value&&delete this.element.dataset.value}_updateOptionItemChecked(optionItem,checked){var _optionItem$dataset$s;const selectedClasses=null!==(_optionItem$dataset$s=optionItem.dataset.selectedClasses)&&void 0!==_optionItem$dataset$s?_optionItem$dataset$s:Classes_selected;for(const selectedClass of selectedClasses.split(" "))optionItem.classList.toggle(selectedClass,checked);checked?optionItem.dataset.selected=checked:null==optionItem||delete optionItem.dataset.selected;const checkedIcon=optionItem.querySelector(Selectors_checkedIcon);checkedIcon&&checkedIcon.classList.toggle(Classes_hidden,!checked);const uncheckedIcon=optionItem.querySelector(Selectors_uncheckedIcon);uncheckedIcon&&uncheckedIcon.classList.toggle(Classes_hidden,checked)}getSelectedValue(){var _selected$dataset$val;const selected=this.panel.querySelector(Selectors_selectedOption);return null!==(_selected$dataset$val=null==selected?void 0:selected.dataset.value)&&void 0!==_selected$dataset$val?_selected$dataset$val:null}setButtonSyncEnabled(value){value?this.element.dataset.buttonSync="true":delete this.element.dataset.buttonSync,value&&this.syncButtonText()}isButtonSyncEnabled(){return"true"==this.element.dataset.buttonSync}syncButtonText(){const selected=this.panel.querySelector(Selectors_selectedOption);if(!selected)return;let newText=selected.textContent;const optionIcon=this._getOptionIcon(selected);optionIcon&&(newText=optionIcon.innerHTML+newText),this.button.innerHTML=newText}setUpdateStatusEnabled(value){value?this.element.dataset.updateStatus="true":delete this.element.dataset.updateStatus}isUpdateStatusEnabled(){return"true"==this.element.dataset.updateStatus}_getOptionIcon(option){const optionItem=option.closest(Selectors_optionItem);return optionItem?optionItem.querySelector(Selectors_optionIcon):null}}_exports.DropdownStatus=DropdownStatus;const getDropdownStatus=selector=>{const dropdownElement=document.querySelector(selector);return dropdownElement?new DropdownStatus(dropdownElement):null};_exports.getDropdownStatus=getDropdownStatus;_exports.init=selector=>{const dropdown=getDropdownStatus(selector);if(!dropdown)throw new Error("Dopdown status element not found: ".concat(selector));dropdown.init()}})); //# sourceMappingURL=status.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/dropdown/status.min.js.map b/lib/amd/build/local/dropdown/status.min.js.map index 3b4e616baff..802dfab23a3 100644 --- a/lib/amd/build/local/dropdown/status.min.js.map +++ b/lib/amd/build/local/dropdown/status.min.js.map @@ -1 +1 @@ -{"version":3,"file":"status.min.js","sources":["../../../src/local/dropdown/status.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 .\n\n/**\n * Dropdown status JS controls.\n *\n * The status controls enable extra configurarions for the dropdown like:\n * - Sync the button text with the selected option.\n * - Update the status of the button when the selected option changes. This will\n * trigger a \"change\" event when the status changes.\n *\n * @module core/local/dropdown/status\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {DropdownDialog} from 'core/local/dropdown/dialog';\n\nconst Selectors = {\n checkedIcon: '[data-for=\"checkedIcon\"]',\n option: '[role=\"option\"]',\n optionItem: '[data-optionnumber]',\n optionIcon: '.option-icon',\n selectedOption: '[role=\"option\"][aria-selected=\"true\"]',\n uncheckedIcon: '[data-for=\"uncheckedIcon\"]',\n};\n\nconst Classes = {\n selected: 'selected',\n disabled: 'disabled',\n hidden: 'd-none',\n};\n\n/**\n * Dropdown dialog class.\n * @private\n */\nexport class DropdownStatus extends DropdownDialog {\n /**\n * Constructor.\n * @param {HTMLElement} element The element to initialize.\n */\n constructor(element) {\n super(element);\n this.buttonSync = element.dataset.buttonSync == 'true';\n this.updateStatus = element.dataset.updateStatus == '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 * @private\n */\n init() {\n super.init();\n\n if (this.element.dataset.dropdownStatusInitialized) {\n return;\n }\n\n this.panel.addEventListener('click', this._contentClickHandler.bind(this));\n\n if (this.element.dataset.buttonSync == 'true') {\n this.setButtonSyncEnabled(true);\n }\n if (this.element.dataset.updateStatus == 'true') {\n this.setUpdateStatusEnabled(true);\n }\n\n this.element.dataset.dropdownStatusInitialized = true;\n }\n\n /**\n * Handle click events on the status content.\n * @param {Event} event The event.\n * @private\n */\n _contentClickHandler(event) {\n const option = event.target.closest(Selectors.option);\n if (!option) {\n return;\n }\n if (option.getAttribute('aria-disabled') === 'true') {\n return;\n }\n if (option.getAttribute('aria-selected') === 'true') {\n return;\n }\n if (this.isUpdateStatusEnabled()) {\n this.setSelectedValue(option.dataset.value);\n }\n }\n\n /**\n * Sets the selected value.\n * @param {string} value The value to set.\n */\n setSelectedValue(value) {\n const selected = this.panel.querySelector(Selectors.selectedOption);\n if (selected && selected.dataset.value === value) {\n return;\n }\n if (selected) {\n this._updateOptionChecked(selected, false);\n }\n const option = this.panel.querySelector(`${Selectors.option}[data-value=\"${value}\"]`);\n if (option) {\n this._updateOptionChecked(option, true);\n }\n if (this.isButtonSyncEnabled()) {\n this.syncButtonText();\n }\n // Emit standard radio button event with the selected option.\n this.element.dispatchEvent(new Event('change'));\n }\n\n /**\n * Update the option checked content.\n * @private\n * @param {HTMLElement} option the option element to set\n * @param {Boolean} checked the new checked value\n */\n _updateOptionChecked(option, checked) {\n option.setAttribute('aria-selected', checked.toString());\n option.classList.toggle(Classes.selected, checked);\n option.classList.toggle(Classes.disabled, checked);\n\n const optionItem = option.closest(Selectors.optionItem);\n if (optionItem) {\n this._updateOptionItemChecked(optionItem, checked);\n }\n\n if (checked) {\n this.element.dataset.value = option.dataset.value;\n } else if (this.element.dataset.value === option.dataset.value) {\n delete this.element.dataset.value;\n }\n }\n\n /**\n * Update the option item checked content.\n * @private\n * @param {HTMLElement} optionItem\n * @param {Boolean} checked\n */\n _updateOptionItemChecked(optionItem, checked) {\n const selectedClasses = optionItem.dataset.selectedClasses ?? Classes.selected;\n for (const selectedClass of selectedClasses.split(' ')) {\n optionItem.classList.toggle(selectedClass, checked);\n }\n if (checked) {\n optionItem.dataset.selected = checked;\n } else {\n delete optionItem?.dataset.selected;\n }\n const checkedIcon = optionItem.querySelector(Selectors.checkedIcon);\n if (checkedIcon) {\n checkedIcon.classList.toggle(Classes.hidden, !checked);\n }\n const uncheckedIcon = optionItem.querySelector(Selectors.uncheckedIcon);\n if (uncheckedIcon) {\n uncheckedIcon.classList.toggle(Classes.hidden, checked);\n }\n }\n\n\n /**\n * Return the selected value.\n * @returns {string|null} The selected value.\n */\n getSelectedValue() {\n const selected = this.panel.querySelector(Selectors.selectedOption);\n return selected?.dataset.value ?? null;\n }\n\n /**\n * Set the button sync value.\n *\n * If the sync is enabled, the button text will show the selected option.\n *\n * @param {Boolean} value The value to set.\n */\n setButtonSyncEnabled(value) {\n if (value) {\n this.element.dataset.buttonSync = 'true';\n } else {\n delete this.element.dataset.buttonSync;\n }\n if (value) {\n this.syncButtonText();\n }\n }\n\n /**\n * Return if the button sync is enabled.\n * @returns {Boolean} The button sync value.\n */\n isButtonSyncEnabled() {\n return this.element.dataset.buttonSync == 'true';\n }\n\n /**\n * Sync the button text with the selected option.\n */\n syncButtonText() {\n const selected = this.panel.querySelector(Selectors.selectedOption);\n if (!selected) {\n return;\n }\n let newText = selected.textContent;\n const optionIcon = this._getOptionIcon(selected);\n if (optionIcon) {\n newText = optionIcon.innerHTML + newText;\n }\n this.button.innerHTML = newText;\n }\n\n /**\n * Set the update status value.\n *\n * @param {Boolean} value The value to set.\n */\n setUpdateStatusEnabled(value) {\n if (value) {\n this.element.dataset.updateStatus = 'true';\n } else {\n delete this.element.dataset.updateStatus;\n }\n }\n\n /**\n * Return if the update status is enabled.\n * @returns {Boolean} The update status value.\n */\n isUpdateStatusEnabled() {\n return this.element.dataset.updateStatus == 'true';\n }\n\n _getOptionIcon(option) {\n const optionItem = option.closest(Selectors.optionItem);\n if (!optionItem) {\n return null;\n }\n return optionItem.querySelector(Selectors.optionIcon);\n }\n\n}\n\n/**\n * Get the dropdown dialog instance form a selector.\n * @param {string} selector The query selector to init.\n * @returns {DropdownStatus|null} The dropdown dialog instance if any.\n */\nexport const getDropdownStatus = (selector) => {\n const dropdownElement = document.querySelector(selector);\n if (!dropdownElement) {\n return null;\n }\n return new DropdownStatus(dropdownElement);\n};\n\n/**\n * Initialize module.\n *\n * @method\n * @param {string} selector The query selector to init.\n */\nexport const init = (selector) => {\n const dropdown = getDropdownStatus(selector);\n if (!dropdown) {\n throw new Error(`Dopdown status element not found: ${selector}`);\n }\n dropdown.init();\n};\n"],"names":["Selectors","Classes","DropdownStatus","DropdownDialog","constructor","element","buttonSync","dataset","updateStatus","init","this","dropdownStatusInitialized","panel","addEventListener","_contentClickHandler","bind","setButtonSyncEnabled","setUpdateStatusEnabled","event","option","target","closest","getAttribute","isUpdateStatusEnabled","setSelectedValue","value","selected","querySelector","_updateOptionChecked","isButtonSyncEnabled","syncButtonText","dispatchEvent","Event","checked","setAttribute","toString","classList","toggle","optionItem","_updateOptionItemChecked","selectedClasses","selectedClass","split","checkedIcon","uncheckedIcon","getSelectedValue","newText","textContent","optionIcon","_getOptionIcon","innerHTML","button","getDropdownStatus","selector","dropdownElement","document","dropdown","Error"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"status.min.js","sources":["../../../src/local/dropdown/status.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 .\n\n/**\n * Dropdown status JS controls.\n *\n * The status controls enable extra configurarions for the dropdown like:\n * - Sync the button text with the selected option.\n * - Update the status of the button when the selected option changes. This will\n * trigger a \"change\" event when the status changes.\n *\n * @module core/local/dropdown/status\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {DropdownDialog} from 'core/local/dropdown/dialog';\n\nconst Selectors = {\n checkedIcon: '[data-for=\"checkedIcon\"]',\n option: '[role=\"option\"]',\n optionItem: '[data-optionnumber]',\n optionIcon: '.option-icon',\n selectedOption: '[role=\"option\"][aria-selected=\"true\"]',\n uncheckedIcon: '[data-for=\"uncheckedIcon\"]',\n};\n\nconst Classes = {\n selected: 'selected',\n disabled: 'disabled',\n hidden: 'd-none',\n};\n\n/**\n * Dropdown dialog class.\n * @private\n */\nexport class DropdownStatus extends DropdownDialog {\n /**\n * Constructor.\n * @param {HTMLElement} element The element to initialize.\n */\n constructor(element) {\n super(element);\n this.buttonSync = element.dataset.buttonSync == 'true';\n this.updateStatus = element.dataset.updateStatus == '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 * @private\n */\n init() {\n super.init();\n\n if (this.element.dataset.dropdownStatusInitialized) {\n return;\n }\n\n this.panel.addEventListener('click', this._contentClickHandler.bind(this));\n\n if (this.element.dataset.buttonSync == 'true') {\n this.setButtonSyncEnabled(true);\n }\n if (this.element.dataset.updateStatus == 'true') {\n this.setUpdateStatusEnabled(true);\n }\n\n this.element.dataset.dropdownStatusInitialized = true;\n }\n\n /**\n * Handle click events on the status content.\n * @param {Event} event The event.\n * @private\n */\n _contentClickHandler(event) {\n const option = event.target.closest(Selectors.option);\n if (!option) {\n return;\n }\n if (option.getAttribute('aria-disabled') === 'true') {\n return;\n }\n if (option.getAttribute('aria-selected') === 'true') {\n return;\n }\n if (this.isUpdateStatusEnabled()) {\n this.setSelectedValue(option.dataset.value);\n this.button.focus();\n }\n }\n\n /**\n * Sets the selected value.\n * @param {string} value The value to set.\n */\n setSelectedValue(value) {\n const selected = this.panel.querySelector(Selectors.selectedOption);\n if (selected && selected.dataset.value === value) {\n return;\n }\n if (selected) {\n this._updateOptionChecked(selected, false);\n }\n const option = this.panel.querySelector(`${Selectors.option}[data-value=\"${value}\"]`);\n if (option) {\n this._updateOptionChecked(option, true);\n }\n if (this.isButtonSyncEnabled()) {\n this.syncButtonText();\n }\n // Emit standard radio button event with the selected option.\n this.element.dispatchEvent(new Event('change'));\n }\n\n /**\n * Update the option checked content.\n * @private\n * @param {HTMLElement} option the option element to set\n * @param {Boolean} checked the new checked value\n */\n _updateOptionChecked(option, checked) {\n option.setAttribute('aria-selected', checked.toString());\n option.classList.toggle(Classes.selected, checked);\n option.classList.toggle(Classes.disabled, checked);\n\n const optionItem = option.closest(Selectors.optionItem);\n if (optionItem) {\n this._updateOptionItemChecked(optionItem, checked);\n }\n\n if (checked) {\n this.element.dataset.value = option.dataset.value;\n } else if (this.element.dataset.value === option.dataset.value) {\n delete this.element.dataset.value;\n }\n }\n\n /**\n * Update the option item checked content.\n * @private\n * @param {HTMLElement} optionItem\n * @param {Boolean} checked\n */\n _updateOptionItemChecked(optionItem, checked) {\n const selectedClasses = optionItem.dataset.selectedClasses ?? Classes.selected;\n for (const selectedClass of selectedClasses.split(' ')) {\n optionItem.classList.toggle(selectedClass, checked);\n }\n if (checked) {\n optionItem.dataset.selected = checked;\n } else {\n delete optionItem?.dataset.selected;\n }\n const checkedIcon = optionItem.querySelector(Selectors.checkedIcon);\n if (checkedIcon) {\n checkedIcon.classList.toggle(Classes.hidden, !checked);\n }\n const uncheckedIcon = optionItem.querySelector(Selectors.uncheckedIcon);\n if (uncheckedIcon) {\n uncheckedIcon.classList.toggle(Classes.hidden, checked);\n }\n }\n\n\n /**\n * Return the selected value.\n * @returns {string|null} The selected value.\n */\n getSelectedValue() {\n const selected = this.panel.querySelector(Selectors.selectedOption);\n return selected?.dataset.value ?? null;\n }\n\n /**\n * Set the button sync value.\n *\n * If the sync is enabled, the button text will show the selected option.\n *\n * @param {Boolean} value The value to set.\n */\n setButtonSyncEnabled(value) {\n if (value) {\n this.element.dataset.buttonSync = 'true';\n } else {\n delete this.element.dataset.buttonSync;\n }\n if (value) {\n this.syncButtonText();\n }\n }\n\n /**\n * Return if the button sync is enabled.\n * @returns {Boolean} The button sync value.\n */\n isButtonSyncEnabled() {\n return this.element.dataset.buttonSync == 'true';\n }\n\n /**\n * Sync the button text with the selected option.\n */\n syncButtonText() {\n const selected = this.panel.querySelector(Selectors.selectedOption);\n if (!selected) {\n return;\n }\n let newText = selected.textContent;\n const optionIcon = this._getOptionIcon(selected);\n if (optionIcon) {\n newText = optionIcon.innerHTML + newText;\n }\n this.button.innerHTML = newText;\n }\n\n /**\n * Set the update status value.\n *\n * @param {Boolean} value The value to set.\n */\n setUpdateStatusEnabled(value) {\n if (value) {\n this.element.dataset.updateStatus = 'true';\n } else {\n delete this.element.dataset.updateStatus;\n }\n }\n\n /**\n * Return if the update status is enabled.\n * @returns {Boolean} The update status value.\n */\n isUpdateStatusEnabled() {\n return this.element.dataset.updateStatus == 'true';\n }\n\n _getOptionIcon(option) {\n const optionItem = option.closest(Selectors.optionItem);\n if (!optionItem) {\n return null;\n }\n return optionItem.querySelector(Selectors.optionIcon);\n }\n\n}\n\n/**\n * Get the dropdown dialog instance form a selector.\n * @param {string} selector The query selector to init.\n * @returns {DropdownStatus|null} The dropdown dialog instance if any.\n */\nexport const getDropdownStatus = (selector) => {\n const dropdownElement = document.querySelector(selector);\n if (!dropdownElement) {\n return null;\n }\n return new DropdownStatus(dropdownElement);\n};\n\n/**\n * Initialize module.\n *\n * @method\n * @param {string} selector The query selector to init.\n */\nexport const init = (selector) => {\n const dropdown = getDropdownStatus(selector);\n if (!dropdown) {\n throw new Error(`Dopdown status element not found: ${selector}`);\n }\n dropdown.init();\n};\n"],"names":["Selectors","Classes","DropdownStatus","DropdownDialog","constructor","element","buttonSync","dataset","updateStatus","init","this","dropdownStatusInitialized","panel","addEventListener","_contentClickHandler","bind","setButtonSyncEnabled","setUpdateStatusEnabled","event","option","target","closest","getAttribute","isUpdateStatusEnabled","setSelectedValue","value","button","focus","selected","querySelector","_updateOptionChecked","isButtonSyncEnabled","syncButtonText","dispatchEvent","Event","checked","setAttribute","toString","classList","toggle","optionItem","_updateOptionItemChecked","selectedClasses","selectedClass","split","checkedIcon","uncheckedIcon","getSelectedValue","newText","textContent","optionIcon","_getOptionIcon","innerHTML","getDropdownStatus","selector","dropdownElement","document","dropdown","Error"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/src/local/action_menu/subpanel.js b/lib/amd/src/local/action_menu/subpanel.js index fc839b19a8f..cf15061f406 100644 --- a/lib/amd/src/local/action_menu/subpanel.js +++ b/lib/amd/src/local/action_menu/subpanel.js @@ -35,6 +35,7 @@ import { hide, unhide, } from 'core/aria'; +import EventHandler from 'theme_boost/bootstrap/dom/event-handler'; const Selectors = { mainMenu: '[role="menu"]', @@ -134,7 +135,10 @@ class SubPanel { this.element.addEventListener('focusin', this._mainElementFocusInHandler.bind(this)); // Menu Item events. this.menuItem.addEventListener('click', this._menuItemClickHandler.bind(this)); - this.menuItem.addEventListener('keydown', this._menuItemKeyHandler.bind(this)); + // Use the Bootstrap key handler for the menu item key handler. + // This will avoid Boostrap Dropdown handler to prevent the propagation to the subpanel. + const subpanelMenuItemSelector = `#${this.element.id}${Selectors.subPanelMenuItem}`; + EventHandler.on(document, 'keydown', subpanelMenuItemSelector, this._menuItemKeyHandler.bind(this)); if (!isBehatSite()) { // Behat in Chrome usually move the mouse over the page when trying clicking a subpanel element. // If the menu has more than one subpanel this could cause closing the subpanel by mistake. diff --git a/lib/amd/src/local/dropdown/dialog.js b/lib/amd/src/local/dropdown/dialog.js index 6b5f282c11e..302537bd7a5 100644 --- a/lib/amd/src/local/dropdown/dialog.js +++ b/lib/amd/src/local/dropdown/dialog.js @@ -29,6 +29,7 @@ import { nextFocusableElement, } from 'core/pagehelpers'; import Pending from 'core/pending'; +import EventHandler from 'theme_boost/bootstrap/dom/event-handler'; const Selectors = { dropdownButton: '[data-for="dropdowndialog_button"]', @@ -60,10 +61,14 @@ export class DropdownDialog { return; } + // Use the Bootstrap key handler for the Dropdown button key handler. + // This will avoid Boostrap Dropdown handler to prevent the propagation to the dialog. // Menu Item events. - this.button.addEventListener('keydown', this._buttonKeyHandler.bind(this)); + const dialogButtonSelector = `#${this.element.id} ${Selectors.dropdownButton}`; + EventHandler.on(document, 'keydown', dialogButtonSelector, this._buttonKeyHandler.bind(this)); // Subpanel content events. - this.panel.addEventListener('keydown', this._contentKeyHandler.bind(this)); + const dialogSelector = `#${this.element.id} ${Selectors.dropdownDialog}`; + EventHandler.on(document, 'keydown', dialogSelector, this._contentKeyHandler.bind(this)); this.element.dataset.dropdownDialogInitialized = true; } diff --git a/lib/amd/src/local/dropdown/status.js b/lib/amd/src/local/dropdown/status.js index d23e57da508..bbc2d9964cf 100644 --- a/lib/amd/src/local/dropdown/status.js +++ b/lib/amd/src/local/dropdown/status.js @@ -101,6 +101,7 @@ export class DropdownStatus extends DropdownDialog { } if (this.isUpdateStatusEnabled()) { this.setSelectedValue(option.dataset.value); + this.button.focus(); } } diff --git a/lib/templates/local/action_menu/subpanel.mustache b/lib/templates/local/action_menu/subpanel.mustache index 92d253d7a9d..838d4baab7b 100644 --- a/lib/templates/local/action_menu/subpanel.mustache +++ b/lib/templates/local/action_menu/subpanel.mustache @@ -57,7 +57,7 @@ {{/itemicon}} {{{text}}} -