diff --git a/lang/en/moodle.php b/lang/en/moodle.php
index 90266b87ea785..850cdb820d2a5 100644
--- a/lang/en/moodle.php
+++ b/lang/en/moodle.php
@@ -1142,6 +1142,7 @@
$string['langrtl'] = 'Language direction right-to-left';
$string['language'] = 'Language';
$string['languagegood'] = 'This language pack is up-to-date! :-)';
+$string['languageselector'] = 'Language selector';
$string['last'] = 'Last';
$string['lastaccess'] = 'Last access';
$string['lastcourseaccess'] = 'Last access to course';
@@ -2217,6 +2218,7 @@
$string['userfiles'] = 'User files';
$string['userlist'] = 'User list';
$string['usermenu'] = 'User menu';
+$string['usermenugoback'] = 'Go back to user menu';
$string['username'] = 'Username';
$string['usernameemail'] = 'Username / email';
$string['usernameemailmatch'] = 'The username and email address do not relate to the same user';
diff --git a/lib/amd/build/usermenu.min.js b/lib/amd/build/usermenu.min.js
new file mode 100644
index 0000000000000..37ae9b5dc5c59
--- /dev/null
+++ b/lib/amd/build/usermenu.min.js
@@ -0,0 +1,2 @@
+define ("core/usermenu",["exports","jquery"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c={userMenu:".usermenu",userMenuCarousel:".usermenu #usermenu-carousel",userMenuCarouselItem:".usermenu #usermenu-carousel .carousel-item",userMenuCarouselItemActive:".usermenu #usermenu-carousel .carousel-item.active",userMenuCarouselNavigationLink:".usermenu #usermenu-carousel .carousel-navigation-link"},d=function(){var a=document.querySelector(c.userMenu);(0,b.default)(c.userMenu).on("shown.bs.dropdown",function(){var b=document.querySelector(c.userMenuCarouselItemActive);b.focus();a.querySelectorAll(c.userMenuCarouselItem).forEach(function(a){if(!a.classList.contains("active")){a.style.width=b.offsetWidth+"px";a.style.height=b.offsetHeight+"px"}})});a.addEventListener("click",function(d){if(d.target.matches(c.userMenuCarouselNavigationLink)){d.stopPropagation();var e=d.target.dataset.carouselTargetId,f=a.querySelector("#"+e),g=Array.from(f.parentNode.children).indexOf(f);(0,b.default)(c.userMenuCarousel).carousel(g)}});(0,b.default)(c.userMenu).on("hide.bs.dropdown",function(){(0,b.default)(c.userMenuCarousel).carousel(0)});(0,b.default)(c.userMenuCarousel).on("slid.bs.carousel",function(){var b=a.querySelector(c.userMenuCarouselItemActive);b.focus()})};a.default={init:function init(){d()}};return a.default});
+//# sourceMappingURL=usermenu.min.js.map
diff --git a/lib/amd/build/usermenu.min.js.map b/lib/amd/build/usermenu.min.js.map
new file mode 100644
index 0000000000000..5776c2247eaa3
--- /dev/null
+++ b/lib/amd/build/usermenu.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../src/usermenu.js"],"names":["selectors","userMenu","userMenuCarousel","userMenuCarouselItem","userMenuCarouselItemActive","userMenuCarouselNavigationLink","registerEventListeners","document","querySelector","on","activeCarouselItem","focus","querySelectorAll","forEach","element","classList","contains","style","width","offsetWidth","height","offsetHeight","addEventListener","e","target","matches","stopPropagation","targetedCarouselItemId","dataset","carouselTargetId","targetedCarouselItem","index","Array","from","parentNode","children","indexOf","carousel","init"],"mappings":"0IAyBA,uD,GAKMA,CAAAA,CAAS,CAAG,CACdC,QAAQ,CAAE,WADI,CAEdC,gBAAgB,CAAE,8BAFJ,CAGdC,oBAAoB,CAAE,6CAHR,CAIdC,0BAA0B,CAAE,oDAJd,CAKdC,8BAA8B,CAAE,wDALlB,C,CAWZC,CAAsB,CAAG,UAAM,CACjC,GAAML,CAAAA,CAAQ,CAAGM,QAAQ,CAACC,aAAT,CAAuBR,CAAS,CAACC,QAAjC,CAAjB,CAGA,cAAED,CAAS,CAACC,QAAZ,EAAsBQ,EAAtB,CAAyB,mBAAzB,CAA8C,UAAM,CAChD,GAAMC,CAAAA,CAAkB,CAAGH,QAAQ,CAACC,aAAT,CAAuBR,CAAS,CAACI,0BAAjC,CAA3B,CAEAM,CAAkB,CAACC,KAAnB,GAEAV,CAAQ,CAACW,gBAAT,CAA0BZ,CAAS,CAACG,oBAApC,EAA0DU,OAA1D,CAAkE,SAAAC,CAAO,CAAI,CAKzE,GAAI,CAACA,CAAO,CAACC,SAAR,CAAkBC,QAAlB,CAA2B,QAA3B,CAAL,CAA2C,CACvCF,CAAO,CAACG,KAAR,CAAcC,KAAd,CAAsBR,CAAkB,CAACS,WAAnB,CAAiC,IAAvD,CACAL,CAAO,CAACG,KAAR,CAAcG,MAAd,CAAuBV,CAAkB,CAACW,YAAnB,CAAkC,IAC5D,CACJ,CATD,CAUH,CAfD,EAkBApB,CAAQ,CAACqB,gBAAT,CAA0B,OAA1B,CAAmC,SAACC,CAAD,CAAO,CAGtC,GAAIA,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBzB,CAAS,CAACK,8BAA3B,CAAJ,CAAgE,CAK5DkB,CAAC,CAACG,eAAF,GAL4D,GAOtDC,CAAAA,CAAsB,CAAGJ,CAAC,CAACC,MAAF,CAASI,OAAT,CAAiBC,gBAPY,CAQtDC,CAAoB,CAAG7B,CAAQ,CAACO,aAAT,CAAuB,IAAMmB,CAA7B,CAR+B,CAUtDI,CAAK,CAAGC,KAAK,CAACC,IAAN,CAAWH,CAAoB,CAACI,UAArB,CAAgCC,QAA3C,EAAqDC,OAArD,CAA6DN,CAA7D,CAV8C,CAY5D,cAAE9B,CAAS,CAACE,gBAAZ,EAA8BmC,QAA9B,CAAuCN,CAAvC,CACH,CACJ,CAjBD,EAoBA,cAAE/B,CAAS,CAACC,QAAZ,EAAsBQ,EAAtB,CAAyB,kBAAzB,CAA6C,UAAM,CAG/C,cAAET,CAAS,CAACE,gBAAZ,EAA8BmC,QAA9B,CAAuC,CAAvC,CACH,CAJD,EAOA,cAAErC,CAAS,CAACE,gBAAZ,EAA8BO,EAA9B,CAAiC,kBAAjC,CAAqD,UAAM,CACvD,GAAMC,CAAAA,CAAkB,CAAGT,CAAQ,CAACO,aAAT,CAAuBR,CAAS,CAACI,0BAAjC,CAA3B,CAEAM,CAAkB,CAACC,KAAnB,EACH,CAJD,CAKH,C,WASc,CACX2B,IAAI,CALK,QAAPA,CAAAA,IAAO,EAAM,CACfhC,CAAsB,EACzB,CAEc,C","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 * Initializes and handles events in the user menu.\n *\n * @module core/usermenu\n * @package core\n * @copyright 2021 Moodle\n * @author Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\n/**\n * User menu constants.\n */\nconst selectors = {\n userMenu: '.usermenu',\n userMenuCarousel: '.usermenu #usermenu-carousel',\n userMenuCarouselItem: '.usermenu #usermenu-carousel .carousel-item',\n userMenuCarouselItemActive: '.usermenu #usermenu-carousel .carousel-item.active',\n userMenuCarouselNavigationLink: '.usermenu #usermenu-carousel .carousel-navigation-link',\n};\n\n/**\n * Register event listeners.\n */\nconst registerEventListeners = () => {\n const userMenu = document.querySelector(selectors.userMenu);\n\n // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).\n $(selectors.userMenu).on('shown.bs.dropdown', () => {\n const activeCarouselItem = document.querySelector(selectors.userMenuCarouselItemActive);\n // Set the focus on the active carousel item.\n activeCarouselItem.focus();\n\n userMenu.querySelectorAll(selectors.userMenuCarouselItem).forEach(element => {\n // Resize all non-active carousel items to match the height and width of the current active (main)\n // carousel item to avoid sizing inconsistencies. This has to be done once the dropdown menu is fully\n // displayed ('shown.bs.dropdown') as the offsetWidth and offsetHeight cannot be obtained when the\n // element is hidden.\n if (!element.classList.contains('active')) {\n element.style.width = activeCarouselItem.offsetWidth + 'px';\n element.style.height = activeCarouselItem.offsetHeight + 'px';\n }\n });\n });\n\n // Handle click events in the user menu.\n userMenu.addEventListener('click', (e) => {\n\n // Handle click event on the carousel navigation (control) links in the user menu.\n if (e.target.matches(selectors.userMenuCarouselNavigationLink)) {\n // By default the user menu dropdown element closes on a click event. This behaviour is not desirable\n // as we need to be able to navigate through the carousel items (submenus of the user menu) within the\n // user menu. Therefore, we need to prevent the propagation of this event and then manually call the\n // carousel transition.\n e.stopPropagation();\n // The id of the targeted carousel item.\n const targetedCarouselItemId = e.target.dataset.carouselTargetId;\n const targetedCarouselItem = userMenu.querySelector('#' + targetedCarouselItemId);\n // Get the position (index) of the targeted carousel item within the parent container element.\n const index = Array.from(targetedCarouselItem.parentNode.children).indexOf(targetedCarouselItem);\n // Navigate to the targeted carousel item.\n $(selectors.userMenuCarousel).carousel(index);\n }\n });\n\n // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).\n $(selectors.userMenu).on('hide.bs.dropdown', () => {\n // Reset the state once the user menu dropdown is closed and return back to the first (main) carousel item\n // if necessary.\n $(selectors.userMenuCarousel).carousel(0);\n });\n\n // Handle the 'slid.bs.carousel' event (Fired when the carousel has completed its slide transition).\n $(selectors.userMenuCarousel).on('slid.bs.carousel', () => {\n const activeCarouselItem = userMenu.querySelector(selectors.userMenuCarouselItemActive);\n // Set the focus on the newly activated carousel item.\n activeCarouselItem.focus();\n });\n};\n\n/**\n * Initialize the user menu.\n */\nconst init = () => {\n registerEventListeners();\n};\n\nexport default {\n init: init,\n};\n"],"file":"usermenu.min.js"}
\ No newline at end of file
diff --git a/lib/amd/src/usermenu.js b/lib/amd/src/usermenu.js
new file mode 100644
index 0000000000000..98b970e4563be
--- /dev/null
+++ b/lib/amd/src/usermenu.js
@@ -0,0 +1,107 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Initializes and handles events in the user menu.
+ *
+ * @module core/usermenu
+ * @package core
+ * @copyright 2021 Moodle
+ * @author Mihail Geshoski
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+
+/**
+ * User menu constants.
+ */
+const selectors = {
+ userMenu: '.usermenu',
+ userMenuCarousel: '.usermenu #usermenu-carousel',
+ userMenuCarouselItem: '.usermenu #usermenu-carousel .carousel-item',
+ userMenuCarouselItemActive: '.usermenu #usermenu-carousel .carousel-item.active',
+ userMenuCarouselNavigationLink: '.usermenu #usermenu-carousel .carousel-navigation-link',
+};
+
+/**
+ * Register event listeners.
+ */
+const registerEventListeners = () => {
+ const userMenu = document.querySelector(selectors.userMenu);
+
+ // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).
+ $(selectors.userMenu).on('shown.bs.dropdown', () => {
+ const activeCarouselItem = document.querySelector(selectors.userMenuCarouselItemActive);
+ // Set the focus on the active carousel item.
+ activeCarouselItem.focus();
+
+ userMenu.querySelectorAll(selectors.userMenuCarouselItem).forEach(element => {
+ // Resize all non-active carousel items to match the height and width of the current active (main)
+ // carousel item to avoid sizing inconsistencies. This has to be done once the dropdown menu is fully
+ // displayed ('shown.bs.dropdown') as the offsetWidth and offsetHeight cannot be obtained when the
+ // element is hidden.
+ if (!element.classList.contains('active')) {
+ element.style.width = activeCarouselItem.offsetWidth + 'px';
+ element.style.height = activeCarouselItem.offsetHeight + 'px';
+ }
+ });
+ });
+
+ // Handle click events in the user menu.
+ userMenu.addEventListener('click', (e) => {
+
+ // Handle click event on the carousel navigation (control) links in the user menu.
+ if (e.target.matches(selectors.userMenuCarouselNavigationLink)) {
+ // By default the user menu dropdown element closes on a click event. This behaviour is not desirable
+ // as we need to be able to navigate through the carousel items (submenus of the user menu) within the
+ // user menu. Therefore, we need to prevent the propagation of this event and then manually call the
+ // carousel transition.
+ e.stopPropagation();
+ // The id of the targeted carousel item.
+ const targetedCarouselItemId = e.target.dataset.carouselTargetId;
+ const targetedCarouselItem = userMenu.querySelector('#' + targetedCarouselItemId);
+ // Get the position (index) of the targeted carousel item within the parent container element.
+ const index = Array.from(targetedCarouselItem.parentNode.children).indexOf(targetedCarouselItem);
+ // Navigate to the targeted carousel item.
+ $(selectors.userMenuCarousel).carousel(index);
+ }
+ });
+
+ // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).
+ $(selectors.userMenu).on('hide.bs.dropdown', () => {
+ // Reset the state once the user menu dropdown is closed and return back to the first (main) carousel item
+ // if necessary.
+ $(selectors.userMenuCarousel).carousel(0);
+ });
+
+ // Handle the 'slid.bs.carousel' event (Fired when the carousel has completed its slide transition).
+ $(selectors.userMenuCarousel).on('slid.bs.carousel', () => {
+ const activeCarouselItem = userMenu.querySelector(selectors.userMenuCarouselItemActive);
+ // Set the focus on the newly activated carousel item.
+ activeCarouselItem.focus();
+ });
+};
+
+/**
+ * Initialize the user menu.
+ */
+const init = () => {
+ registerEventListeners();
+};
+
+export default {
+ init: init,
+};
diff --git a/lib/templates/user_action_menu_items.mustache b/lib/templates/user_action_menu_items.mustache
index 0a89d1da45157..8f7e4ce704045 100644
--- a/lib/templates/user_action_menu_items.mustache
+++ b/lib/templates/user_action_menu_items.mustache
@@ -26,6 +26,11 @@
* url - The href for the link.
* pixicon - (Optional) The Moodle icon to use
* imgsrc - (Optional) If provided, uses this as source for an image tag. Note: pixicon is preferred.
+ * submenulink - If a submenu link is provided render it.
+ * submenuid - The id of the targeted submenu.
+ * title - The text to be shown for the link.
+ * pixicon - (Optional) The Moodle icon to use.
+ * imgsrc - (Optional) If provided, uses this as source for an image tag. Note: pixicon is preferred.
* divider - Whether a divider is to be displayed or not
Example context (json):
@@ -40,6 +45,15 @@
},
"divider": 1
},
+ {
+ "submenulink": {
+ "title": "Title",
+ "submenuid": "86cebd87",
+ "pixicon": "t/dashboard",
+ "imgsrc": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png"
+ },
+ "divider": 1
+ }
]
}
}}
@@ -55,5 +69,16 @@
{{title}}
{{/link}}
+ {{#submenulink}}
+
+ {{#pixicon}}
+ {{#pix}}{{pixicon}}{{/pix}}
+ {{/pixicon}}
+ {{^pixicon}}
+ {{#imgsrc}}{{/imgsrc}}
+ {{/pixicon}}
+ {{title}}
+
+ {{/submenulink}}
{{#divider}}{{/divider}}
{{/items}}
diff --git a/lib/templates/user_action_menu_submenu_items.mustache b/lib/templates/user_action_menu_submenu_items.mustache
new file mode 100644
index 0000000000000..53cf768dfcf4b
--- /dev/null
+++ b/lib/templates/user_action_menu_submenu_items.mustache
@@ -0,0 +1,48 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/user_action_menu_submenus
+
+ Template for the submenus in the user action menu.
+
+ Context variables required for this template:
+ * items - The submenu items
+ * link - If a link is provided render it.
+ * title - The title added to the link.
+ * text - The text to be shown for the link.
+ * url - The href for the link.
+ * isactive - (Optional) Whether the item is currently active (has been selected).
+
+ Example context (json):
+ {
+ "items": {
+ "link": {
+ "title": "Submenu item 1",
+ "text": "Submenu item 1",
+ "url": "https://example.com/",
+ "isactive": 0
+ }
+ }
+ }
+}}
+{{#items}}
+ {{#link}}
+
+ {{text}}
+
+ {{/link}}
+{{/items}}
diff --git a/lib/templates/user_menu.mustache b/lib/templates/user_menu.mustache
index b8197515ca6eb..671934a2552a7 100644
--- a/lib/templates/user_menu.mustache
+++ b/lib/templates/user_menu.mustache
@@ -28,6 +28,10 @@
* avatardata - Array of avatars to be displayed. Usually only the current user's avatar. If viewing as another user,
includes that user's avatar.
* userfullname - The name of the logged in user
+ * submenus - Array of submenus within the user menu.
+ * id - The id of the submenu.
+ * title - The title of the submenu.
+ * items - Array of the submenu items used in core/user_action_menu_submenu_items.
Example context (json):
{
@@ -38,12 +42,19 @@
"items": [],
"metadata": [],
"avatardata": [],
- "userfullname": "Admin User"
+ "userfullname": "Admin User",
+ "submenus": [
+ {
+ "id": "86cebd87",
+ "title": "Submenu title",
+ "items": []
+ }
+ ]
}
}}