From afadb5cbeacb2b849737c55f73ff9350d879a4de Mon Sep 17 00:00:00 2001 From: Stefan-Alexander Scholz Date: Thu, 17 Aug 2023 21:33:53 +0200 Subject: [PATCH] Feature: Smart menus (#300) --- CHANGES.md | 5 + README.md | 4 + amd/build/fontawesome-popover.min.js | 11 + amd/build/fontawesome-popover.min.js.map | 1 + amd/build/smartmenu.min.js | 10 + amd/build/smartmenu.min.js.map | 1 + amd/src/fontawesome-popover.js | 217 +++ amd/src/smartmenu.js | 213 +++ classes/cache/loader.php | 60 + classes/eventobservers.php | 134 +- classes/form/smartmenu_form.php | 241 +++ classes/form/smartmenu_item_form.php | 350 +++++ classes/output/navigation/primary.php | 233 +++ classes/smartmenu.php | 971 ++++++++++++ classes/smartmenu_item.php | 1339 +++++++++++++++++ classes/table/smartmenus_items.php | 275 ++++ classes/table/smartmenus_menus.php | 251 +++ db/caches.php | 14 + db/events.php | 45 + db/install.xml | 70 + db/upgrade.php | 88 ++ form/element-colorpicker.php | 99 ++ lang/en/theme_boost_union.php | 172 ++- layout/columns2.php | 10 +- layout/drawers.php | 8 +- layout/includes/smartmenus.php | 30 + lib.php | 65 + scss/boost_union/post.scss | 1142 +++++++++++++- settings.php | 12 +- smartmenus/edit.php | 110 ++ smartmenus/edit_items.php | 143 ++ smartmenus/items.php | 172 +++ smartmenus/menulib.php | 655 ++++++++ smartmenus/menus.php | 147 ++ templates/core/moremenu.mustache | 103 ++ templates/core/moremenu.mustache.upstream | 71 + templates/core/moremenu_children.mustache | 131 ++ .../core/moremenu_children.mustache.upstream | 107 ++ .../core/user_action_menu_items.mustache | 93 ++ .../user_action_menu_items.mustache.upstream | 84 ++ .../user_action_menu_submenu_items.mustache | 70 + ...ction_menu_submenu_items.mustache.upstream | 49 + templates/core/user_menu.mustache | 115 ++ templates/core/user_menu.mustache.upstream | 108 ++ .../fontawesome-iconpicker-popover.mustache | 54 + .../primary-drawer-mobile-child.mustache | 66 + .../smartmenus-cardmenu-children.mustache | 114 ++ templates/theme_boost/columns2.mustache | 4 +- templates/theme_boost/drawers.mustache | 7 +- templates/theme_boost/footer.mustache | 11 + templates/theme_boost/navbar.mustache | 15 +- .../primary-drawer-mobile.mustache | 86 ++ .../primary-drawer-mobile.mustache.upstream | 97 ++ ...hat_theme_boost_union_behat_smartmenus.php | 257 ++++ .../theme_boost_union_menu_rules.feature | 244 +++ .../theme_boost_union_menuitem_rules.feature | 235 +++ ...ost_union_menuitems_dynamiccourses.feature | 281 ++++ ...union_menuitemsettings_application.feature | 225 +++ ..._union_menuitemsettings_management.feature | 147 ++ ...ost_union_menusettings_application.feature | 260 ++++ ...oost_union_menusettings_management.feature | 130 ++ version.php | 2 +- 62 files changed, 10423 insertions(+), 41 deletions(-) create mode 100644 amd/build/fontawesome-popover.min.js create mode 100644 amd/build/fontawesome-popover.min.js.map create mode 100644 amd/build/smartmenu.min.js create mode 100644 amd/build/smartmenu.min.js.map create mode 100644 amd/src/fontawesome-popover.js create mode 100644 amd/src/smartmenu.js create mode 100644 classes/cache/loader.php create mode 100644 classes/form/smartmenu_form.php create mode 100644 classes/form/smartmenu_item_form.php create mode 100644 classes/output/navigation/primary.php create mode 100644 classes/smartmenu.php create mode 100644 classes/smartmenu_item.php create mode 100644 classes/table/smartmenus_items.php create mode 100644 classes/table/smartmenus_menus.php create mode 100644 form/element-colorpicker.php create mode 100644 layout/includes/smartmenus.php create mode 100644 smartmenus/edit.php create mode 100644 smartmenus/edit_items.php create mode 100644 smartmenus/items.php create mode 100644 smartmenus/menulib.php create mode 100644 smartmenus/menus.php create mode 100644 templates/core/moremenu.mustache create mode 100644 templates/core/moremenu.mustache.upstream create mode 100644 templates/core/moremenu_children.mustache create mode 100644 templates/core/moremenu_children.mustache.upstream create mode 100644 templates/core/user_action_menu_items.mustache create mode 100644 templates/core/user_action_menu_items.mustache.upstream create mode 100644 templates/core/user_action_menu_submenu_items.mustache create mode 100644 templates/core/user_action_menu_submenu_items.mustache.upstream create mode 100644 templates/core/user_menu.mustache create mode 100644 templates/core/user_menu.mustache.upstream create mode 100644 templates/fontawesome-iconpicker-popover.mustache create mode 100644 templates/primary-drawer-mobile-child.mustache create mode 100644 templates/smartmenus-cardmenu-children.mustache create mode 100644 templates/theme_boost/primary-drawer-mobile.mustache create mode 100644 templates/theme_boost/primary-drawer-mobile.mustache.upstream create mode 100644 tests/behat/behat_theme_boost_union_behat_smartmenus.php create mode 100644 tests/behat/theme_boost_union_menu_rules.feature create mode 100644 tests/behat/theme_boost_union_menuitem_rules.feature create mode 100644 tests/behat/theme_boost_union_menuitems_dynamiccourses.feature create mode 100644 tests/behat/theme_boost_union_menuitemsettings_application.feature create mode 100644 tests/behat/theme_boost_union_menuitemsettings_management.feature create mode 100644 tests/behat/theme_boost_union_menusettings_application.feature create mode 100644 tests/behat/theme_boost_union_menusettings_management.feature diff --git a/CHANGES.md b/CHANGES.md index 3de86a89bbe..d91277d9e14 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,11 @@ moodle-theme_boost_union Changes ------- +### Unreleased + +* 2023-08-13 - Improvement: Make the fonts in the dark navbar variants always fully white (not only when hovered) to improve the contrast. +* 2023-08-13 - Feature: Smart menus, solves #137 + ### v4.1-r9 * 2023-07-24 - Improvement: Only add load OffCanvas module when offcanvas region is enabled, solves #343. diff --git a/README.md b/README.md index 912d2949895..3eca6be961d 100644 --- a/README.md +++ b/README.md @@ -431,6 +431,10 @@ With this setting a hint will appear in the course header if the course is visib Boost Union's flavours offer a possibility to override particular Moodle look & feel settings in particular contexts. On this page, you can create and manage flavours. +### Settings page "Smart menus" + +Smart menus allow site administrators to create customizable menus that can be placed in different locations on the site, such as the site main menu, bottom mobile menu, and user menu. The menus can be configured to display different types of content, including links to other pages or resources, category links, or user profile links. On this page, you can create and manage smart menus. + Capabilities ------------ diff --git a/amd/build/fontawesome-popover.min.js b/amd/build/fontawesome-popover.min.js new file mode 100644 index 00000000000..938e48b8045 --- /dev/null +++ b/amd/build/fontawesome-popover.min.js @@ -0,0 +1,11 @@ +/** + * Theme Boost Union - JS code which shows all fontawesome icons in a popover. + * + * @module theme_boost_union/fontawesome-popover + * @copyright 2023 bdecent GmbH + * @copyright based on code from theme_boost\footer-popover by Bas Brands. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define("theme_boost_union/fontawesome-popover",["jquery","theme_boost/popover","core/fragment"],(function($,popover,Fragment){const SELECTORS_PICKERCONTAINER=".fontawesome-iconpicker-popover";var contextID;let pickerIsShown=!1;var SELECTBOX;const iconPicker=target=>{if(null==(SELECTBOX=document.querySelector(target)))return;(target=>{var input=document.createElement("input");input.setAttribute("type","text"),input.classList.add("fontawesome-autocomplete"),input.classList.add("form-control"),input.setAttribute("name","iconsearch"),""!=SELECTBOX.value&&(input.value=null!==SELECTBOX.querySelector("option[selected]")?SELECTBOX.querySelector("option[selected]").text:"");var wrapper=document.createElement("div");wrapper.classList.add("fontawesome-picker-container"),wrapper.append(input),document.querySelector(target).style.display="none",document.querySelector(target).parentNode.append(wrapper)})(target);const pickerInput=document.querySelector(target).parentNode.querySelector("input.fontawesome-autocomplete");null!=pickerInput?(Fragment.loadFragment("theme_boost_union","icons_list",contextID,{}).then((function(html){$(pickerInput).popover({content:html,html:!0,placement:"bottom",customClass:"fontawesome-picker",trigger:"click"}),$(pickerInput).on("inserted.bs.popover",(function(){document.querySelector(".fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions").querySelectorAll("li").forEach((li=>{li.addEventListener("click",(e=>{var target=e.target.closest("li"),value=target.getAttribute("aria-value"),label=target.getAttribute("aria-label");pickerInput.value=label,SELECTBOX.value=value||0,$(pickerInput).popover("hide")}))}))}))})).catch(),document.addEventListener("click",(e=>{pickerIsShown&&!e.target.closest(SELECTORS_PICKERCONTAINER)&&$(pickerInput).popover("hide")}),!0),document.addEventListener("keydown",(e=>{pickerIsShown&&"Escape"===e.key&&($(pickerInput).popover("hide"),pickerInput.focus())})),document.addEventListener("focus",(e=>{pickerIsShown&&!e.target.closest(SELECTORS_PICKERCONTAINER)&&$(pickerInput).popover("hide")}),!0),$(pickerInput).on("shown.bs.popover",(()=>{if(pickerIsShown=!0,""!=pickerInput.value){var iconSuggestion=document.querySelector(".fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions");null!==iconSuggestion.querySelector('li[aria-label="'+pickerInput.value+'"]')&&(iconSuggestion.querySelectorAll("li").forEach((li=>li.classList.remove("selected"))),iconSuggestion.querySelector('li[aria-label="'+pickerInput.value+'"]').classList.add("selected"))}})),$(pickerInput).on("hide.bs.popover",(()=>{pickerIsShown=!1})),pickerInput.addEventListener("keyup",(function(e){(target=>{var filter=target.value.toLowerCase();SELECTBOX.value=filter||0;var ul=document.querySelector(".fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions");if(null!=ul)for(var li=ul.querySelectorAll("li"),i=0;iiconPicker(target)),1e3)};return{init:(target,contextid)=>{contextID=contextid,iconPicker(target)}}})); + +//# sourceMappingURL=fontawesome-popover.min.js.map \ No newline at end of file diff --git a/amd/build/fontawesome-popover.min.js.map b/amd/build/fontawesome-popover.min.js.map new file mode 100644 index 00000000000..d26b02334d4 --- /dev/null +++ b/amd/build/fontawesome-popover.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fontawesome-popover.min.js","sources":["../src/fontawesome-popover.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 * Theme Boost Union - JS code which shows all fontawesome icons in a popover.\n *\n * @module theme_boost_union/fontawesome-popover\n * @copyright 2023 bdecent GmbH \n * @copyright based on code from theme_boost\\footer-popover by Bas Brands.\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'theme_boost/popover', 'core/fragment'], function($, popover, Fragment) {\n\n const SELECTORS = {\n PICKERCONTAINER: '.fontawesome-iconpicker-popover',\n PICKERCONTENT: '[data-region=\"icons-list\"]',\n };\n\n var contextID;\n\n let pickerIsShown = false;\n\n var SELECTBOX;\n\n /**\n * Get the icon list for popover.\n *\n * @returns {String} HTML string\n * @private\n */\n const getIconList = () => {\n return Fragment.loadFragment('theme_boost_union', 'icons_list', contextID, {});\n };\n\n /**\n * Filter the icons in the list with values which the user entered in the search input.\n * Given input will contain the text in both aria-value and aria-label.\n * Ex. \"core:t\\document\" is aria-value and \"fa-document\" is aria-label.\n *\n * @param {Element} target\n */\n const filterIcons = (target) => {\n var filter = target.value.toLowerCase();\n SELECTBOX.value = filter || 0;\n var ul = document.querySelector('.fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions');\n if (ul === undefined || ul === null) {\n return;\n }\n var li = ul.querySelectorAll('li');\n\n for (var i = 0; i < li.length; i++) {\n var value = li[i].getAttribute('aria-value');\n var label = li[i].getAttribute('aria-label');\n if (!value.toLowerCase().includes(filter) && !label.toLowerCase().includes(filter)) {\n li[i].style.display = \"none\";\n } else {\n li[i].style.display = \"inline-block\";\n }\n }\n };\n\n /**\n * Creates input element and append the element into the target element's parent node.\n * User is able to search icons using this input field.\n *\n * @param {String} target Element Selector.\n */\n const createElements = (target) => {\n\n var input = document.createElement('input');\n input.setAttribute('type', 'text');\n input.classList.add('fontawesome-autocomplete');\n input.classList.add('form-control');\n input.setAttribute('name', 'iconsearch');\n\n if (SELECTBOX.value != '') {\n input.value = SELECTBOX.querySelector('option[selected]') !== null\n ? SELECTBOX.querySelector('option[selected]').text : '';\n }\n\n var wrapper = document.createElement('div');\n wrapper.classList.add(\"fontawesome-picker-container\");\n wrapper.append(input);\n\n document.querySelector(target).style.display = 'none';\n document.querySelector(target).parentNode.append(wrapper);\n };\n\n /**\n * Update the target with fontawesome iconpicker.\n *\n * Create picker input field for search icons insert to DOM, fetch the icons list and setup the popover with icons content.\n * Display the popover when the icon search input field is focused or clicked. This way user can view the list of icons and\n * search icons. When the icon is selected, same icon in the select element will be selected.\n *\n * @param {String} target Element Selector.\n */\n const iconPicker = (target) => {\n\n SELECTBOX = document.querySelector(target);\n\n if (SELECTBOX === undefined || SELECTBOX === null) {\n return;\n }\n\n // Create input element and insert for search icons and hide the current target select box.\n createElements(target);\n\n // Parent of the target element.\n var selectBoxParent = document.querySelector(target).parentNode;\n\n // Input element for search icons, appended in createElements method.\n const pickerInput = selectBoxParent.querySelector(\"input.fontawesome-autocomplete\");\n\n // Check the search input created and inserted in DOM.\n if (pickerInput === undefined || pickerInput === null) {\n setTimeout(() => iconPicker(target), 1000);\n return;\n }\n\n // Fetch the icons list and setup popover with icons list.\n getIconList().then(function(html) {\n\n $(pickerInput).popover({\n content: html,\n html: true,\n placement: 'bottom',\n customClass: 'fontawesome-picker',\n trigger: 'click'\n });\n\n // Event observer when the popover is inserted in DOM, create event listner for each icon in icons list.\n // Icon is clicked, set the icon aria-value as value for select box.\n // Set the icon label to value of autocomplete picker.\n $(pickerInput).on('inserted.bs.popover', function() {\n var ul = document.querySelector('.fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions');\n ul.querySelectorAll('li').forEach((li) => {\n li.addEventListener('click', (e) => {\n var target = e.target.closest('li');\n var value = target.getAttribute('aria-value');\n var label = target.getAttribute('aria-label');\n pickerInput.value = label;\n SELECTBOX.value = value || 0;\n $(pickerInput).popover('hide');\n });\n });\n });\n return;\n }).catch();\n\n document.addEventListener('click', e => {\n if (pickerIsShown && !e.target.closest(SELECTORS.PICKERCONTAINER)) {\n $(pickerInput).popover('hide');\n }\n },\n true);\n\n document.addEventListener('keydown', e => {\n if (pickerIsShown && e.key === 'Escape') {\n $(pickerInput).popover('hide');\n pickerInput.focus();\n }\n });\n\n document.addEventListener('focus', e => {\n if (pickerIsShown && !e.target.closest(SELECTORS.PICKERCONTAINER)) {\n $(pickerInput).popover('hide');\n }\n },\n true);\n\n $(pickerInput).on('shown.bs.popover', () => {\n pickerIsShown = true;\n // Add class to selected icon, helps to differentiate.\n if (pickerInput.value != '') {\n var iconSuggestion = document.querySelector('.fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions');\n if (iconSuggestion.querySelector('li[aria-label=\"' + pickerInput.value + '\"]') !== null) {\n // Remove selected class.\n iconSuggestion.querySelectorAll('li').forEach((li) =>\n li.classList.remove('selected'));\n // Assign selected class for new.\n iconSuggestion.querySelector('li[aria-label=\"' + pickerInput.value + '\"]').classList.add('selected');\n }\n }\n });\n\n $(pickerInput).on('hide.bs.popover', () => {\n pickerIsShown = false;\n });\n\n pickerInput.addEventListener('keyup', function(e) {\n filterIcons(e.target);\n });\n\n };\n\n return {\n init: (target, contextid) => {\n contextID = contextid;\n iconPicker(target);\n }\n\n };\n});\n"],"names":["define","$","popover","Fragment","SELECTORS","contextID","pickerIsShown","SELECTBOX","iconPicker","target","document","querySelector","input","createElement","setAttribute","classList","add","value","text","wrapper","append","style","display","parentNode","createElements","pickerInput","loadFragment","then","html","content","placement","customClass","trigger","on","querySelectorAll","forEach","li","addEventListener","e","closest","getAttribute","label","catch","key","focus","iconSuggestion","remove","filter","toLowerCase","ul","i","length","includes","filterIcons","setTimeout","init","contextid"],"mappings":";;;;;;;;AAwBAA,+CAAO,CAAC,SAAU,sBAAuB,kBAAkB,SAASC,EAAGC,QAASC,gBAEtEC,0BACe,sCAIjBC,cAEAC,eAAgB,MAEhBC,gBA2EEC,WAAcC,YAIZF,OAFJA,UAAYG,SAASC,cAAcF,gBAhCfA,CAAAA,aAEhBG,MAAQF,SAASG,cAAc,SACnCD,MAAME,aAAa,OAAQ,QAC3BF,MAAMG,UAAUC,IAAI,4BACpBJ,MAAMG,UAAUC,IAAI,gBACpBJ,MAAME,aAAa,OAAQ,cAEJ,IAAnBP,UAAUU,QACVL,MAAMK,MAAwD,OAAhDV,UAAUI,cAAc,oBAChCJ,UAAUI,cAAc,oBAAoBO,KAAO,QAGzDC,QAAUT,SAASG,cAAc,OACrCM,QAAQJ,UAAUC,IAAI,gCACtBG,QAAQC,OAAOR,OAEfF,SAASC,cAAcF,QAAQY,MAAMC,QAAU,OAC/CZ,SAASC,cAAcF,QAAQc,WAAWH,OAAOD,UAqBjDK,CAAef,cAMTgB,YAHgBf,SAASC,cAAcF,QAAQc,WAGjBZ,cAAc,kCAG9Cc,MAAAA,aApFGtB,SAASuB,aAAa,oBAAqB,aAAcrB,UAAW,IA0F7DsB,MAAK,SAASC,MAExB3B,EAAEwB,aAAavB,QAAQ,CACnB2B,QAASD,KACTA,MAAM,EACNE,UAAW,SACXC,YAAa,qBACbC,QAAS,UAMb/B,EAAEwB,aAAaQ,GAAG,uBAAuB,WAC5BvB,SAASC,cAAc,mEAC7BuB,iBAAiB,MAAMC,SAASC,KAC/BA,GAAGC,iBAAiB,SAAUC,QACtB7B,OAAS6B,EAAE7B,OAAO8B,QAAQ,MAC1BtB,MAAQR,OAAO+B,aAAa,cAC5BC,MAAQhC,OAAO+B,aAAa,cAChCf,YAAYR,MAAQwB,MACpBlC,UAAUU,MAAQA,OAAS,EAC3BhB,EAAEwB,aAAavB,QAAQ,oBAKpCwC,QAEHhC,SAAS2B,iBAAiB,SAASC,IAC3BhC,gBAAkBgC,EAAE7B,OAAO8B,QAAQnC,4BACnCH,EAAEwB,aAAavB,QAAQ,WAG/B,GAEAQ,SAAS2B,iBAAiB,WAAWC,IAC7BhC,eAA2B,WAAVgC,EAAEK,MACnB1C,EAAEwB,aAAavB,QAAQ,QACvBuB,YAAYmB,YAIpBlC,SAAS2B,iBAAiB,SAASC,IAC3BhC,gBAAkBgC,EAAE7B,OAAO8B,QAAQnC,4BACnCH,EAAEwB,aAAavB,QAAQ,WAG/B,GAEAD,EAAEwB,aAAaQ,GAAG,oBAAoB,QAClC3B,eAAgB,EAES,IAArBmB,YAAYR,MAAa,KACrB4B,eAAiBnC,SAASC,cAAc,mEACuC,OAA/EkC,eAAelC,cAAc,kBAAoBc,YAAYR,MAAQ,QAErE4B,eAAeX,iBAAiB,MAAMC,SAASC,IACvCA,GAAGrB,UAAU+B,OAAO,cAE5BD,eAAelC,cAAc,kBAAoBc,YAAYR,MAAQ,MAAMF,UAAUC,IAAI,iBAKrGf,EAAEwB,aAAaQ,GAAG,mBAAmB,KACjC3B,eAAgB,KAGpBmB,YAAYY,iBAAiB,SAAS,SAASC,GArJ9B7B,CAAAA,aACbsC,OAAStC,OAAOQ,MAAM+B,cAC1BzC,UAAUU,MAAQ8B,QAAU,MACxBE,GAAKvC,SAASC,cAAc,sEAC5BsC,MAAAA,WAGAb,GAAKa,GAAGf,iBAAiB,MAEpBgB,EAAI,EAAGA,EAAId,GAAGe,OAAQD,IAAK,KAC5BjC,MAAQmB,GAAGc,GAAGV,aAAa,cAC3BC,MAAQL,GAAGc,GAAGV,aAAa,cAC1BvB,MAAM+B,cAAcI,SAASL,SAAYN,MAAMO,cAAcI,SAASL,QAGvEX,GAAGc,GAAG7B,MAAMC,QAAU,eAFtBc,GAAGc,GAAG7B,MAAMC,QAAU,SAyI1B+B,CAAYf,EAAE7B,YA3Ed6C,YAAW,IAAM9C,WAAWC,SAAS,YAgFtC,CACH8C,KAAM,CAAC9C,OAAQ+C,aACXnD,UAAYmD,UACZhD,WAAWC"} \ No newline at end of file diff --git a/amd/build/smartmenu.min.js b/amd/build/smartmenu.min.js new file mode 100644 index 00000000000..6eca61394f2 --- /dev/null +++ b/amd/build/smartmenu.min.js @@ -0,0 +1,10 @@ +/** + * Theme Boost Union - JS for smart menu to realize the third level submenu support. + * + * @module theme_boost_union/smartmenu + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define("theme_boost_union/smartmenu",["jquery","core/moremenu"],(function($){const hideSubmenus=target=>{var visibleMenu=document.querySelectorAll("nav.moremenu .dropdown-submenu.show");null!==visibleMenu&&visibleMenu.forEach((el=>{el!=target&&el.classList.remove("show")}))},moveOutMoreMenu=navMenu=>{if(null!==navMenu){var outMenus=navMenu.querySelectorAll(".dropdownmoremenu .force-menu-out"),menuslist=[];if(null!==outMenus){outMenus.forEach((menu=>{menu.querySelector("a").classList.remove("dropdown-item"),menu.querySelector("a").classList.add("nav-link"),menuslist.push(menu),menu.parentNode.removeChild(menu)}));var length=menuslist.length,newPosition=navMenu.children.length-1-length||0;menuslist.forEach((menu=>navMenu.insertBefore(menu,navMenu.children[newPosition]))),window.dispatchEvent(new Event("resize"))}}};return{init:()=>{(()=>{var submenu=document.querySelectorAll("nav.moremenu .dropdown-submenu");null!==submenu&&submenu.forEach((item=>{item.addEventListener("click",(e=>{var target=e.currentTarget;hideSubmenus(target),target.classList.toggle("show"),e.stopPropagation()}))})),$(document).on("hidden.bs.dropdown",(e=>{var submenus=e.relatedTarget.parentNode.querySelectorAll(".dropdown-submenu.show");null!==submenus&&submenus.forEach((e=>e.classList.remove("show")))})),document.addEventListener("click",(e=>{var dropdown=e.target.closest(".dropdownmoremenu"),subMenu=e.target.closest(".dropdown-submenu");dropdown&&null!==subMenu&&(dropdown.querySelectorAll(".dropdown-submenu.show").forEach((menu=>{menu.classList.remove("show")})),subMenu.classList.toggle("show"));var dropdownMenu=e.target.parentNode.classList.contains("dropdown");dropdown&&dropdownMenu&&dropdown.querySelectorAll(".dropdown-menu.show").forEach((menu=>{menu!=e.target.closest(".dropdown-menu")&&menu.classList.remove("show")}))}),!0);var helpIcon=document.querySelectorAll(".moremenu .dropdown .menu-helpicon");null!==helpIcon&&helpIcon.forEach((icon=>{icon.addEventListener("click",(e=>{e.stopPropagation()}))}))})(),(()=>{var cards=document.querySelectorAll(".card-dropdown.card-overflow-no-wrap");if(null!==cards){var scrollStart,scrollMoved;let startPos,scrollPos;cards.forEach((card=>{var scrollElement=card.querySelector(".dropdown-menu");scrollElement.addEventListener("mousedown",(e=>{scrollStart=!0;var target=e.currentTarget.querySelector(".card-block-wrapper");startPos=e.pageX,scrollPos=target.scrollLeft})),scrollElement.addEventListener("mousemove",(e=>{if(e.preventDefault(),!scrollStart)return;scrollMoved=!0;var target=e.currentTarget.querySelector(".card-block-wrapper");const scroll=e.pageX-startPos;target.scrollLeft=scrollPos-scroll})),scrollElement.addEventListener("click",(e=>{scrollMoved&&(e.preventDefault(),scrollMoved=!1),e.stopPropagation()})),scrollElement.addEventListener("mouseleave",(()=>{scrollStart=!1,scrollMoved=!1})),scrollElement.addEventListener("mouseup",(()=>{scrollStart=!1}))}))}})(),(()=>{var primaryNav=document.querySelector(".primary-navigation ul.more-nav");moveOutMoreMenu(primaryNav);var menuBar=document.querySelector("nav.menubar ul.more-nav");moveOutMoreMenu(menuBar)})()}}})); + +//# sourceMappingURL=smartmenu.min.js.map \ No newline at end of file diff --git a/amd/build/smartmenu.min.js.map b/amd/build/smartmenu.min.js.map new file mode 100644 index 00000000000..6877d2a276f --- /dev/null +++ b/amd/build/smartmenu.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"smartmenu.min.js","sources":["../src/smartmenu.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 * Theme Boost Union - JS for smart menu to realize the third level submenu support.\n *\n * @module theme_boost_union/smartmenu\n * @copyright 2023 bdecent GmbH \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([\"jquery\", \"core/moremenu\"], function($) {\n /**\n * Implement the second level of submenu support.\n * Find the submenus inside the dropdown, add an event listener for click event which - on the click - shows the submenu list.\n */\n const addSubmenu = () => {\n // Fetch the list of submenus from moremenu.\n var submenu = document.querySelectorAll('nav.moremenu .dropdown-submenu');\n if (submenu !== null) {\n submenu.forEach((item) => {\n // Add event listener to show the submenu on click.\n item.addEventListener('click', (e) => {\n var target = e.currentTarget;\n // Hide the shown menu.\n hideSubmenus(target);\n target.classList.toggle('show');\n // Prevent hiding the parent menu.\n e.stopPropagation();\n });\n });\n }\n\n // Hide the submenus when its parent dropdown is hidden.\n $(document).on('hidden.bs.dropdown', e => {\n var target = e.relatedTarget.parentNode;\n var submenus = target.querySelectorAll('.dropdown-submenu.show');\n if (submenus !== null) {\n submenus.forEach((e) => e.classList.remove('show'));\n }\n });\n\n // Provide the third level menu support inside the more menu.\n // StopPropagation used in the toggledropdown method on Moremenu.js, It prevents the opening of the third level menus.\n // Used the document delegation method to fetch the click on moremenu and submenu.\n document.addEventListener('click', (e) => {\n var dropdown = e.target.closest('.dropdownmoremenu');\n var subMenu = e.target.closest('.dropdown-submenu');\n if (dropdown && subMenu !== null) {\n // Hide the previously opend submenus. before open the new one.\n dropdown.querySelectorAll('.dropdown-submenu.show').forEach((menu) => {\n menu.classList.remove('show');\n });\n subMenu.classList.toggle('show');\n }\n\n // Hide the opened menus before open the other menus.\n var dropdownMenu = e.target.parentNode.classList.contains('dropdown');\n if (dropdown && dropdownMenu) {\n dropdown.querySelectorAll('.dropdown-menu.show').forEach((menu) => {\n // Hide the opened menus in more menu.\n if (menu != e.target.closest('.dropdown-menu')) {\n menu.classList.remove('show');\n }\n });\n }\n\n }, true);\n\n // Prevent the closing of dropdown during the click on help icon.\n var helpIcon = document.querySelectorAll('.moremenu .dropdown .menu-helpicon');\n if (helpIcon !== null) {\n helpIcon.forEach((icon) => {\n icon.addEventListener('click', (e) => {\n e.stopPropagation();\n });\n });\n }\n };\n\n /**\n * Hide visible submenus before display new submenu.\n *\n * @param {Selector} target\n */\n const hideSubmenus = (target) => {\n var visibleMenu = document.querySelectorAll('nav.moremenu .dropdown-submenu.show');\n if (visibleMenu !== null) {\n visibleMenu.forEach((el) => {\n if (el != target) {\n el.classList.remove('show');\n }\n });\n }\n };\n\n /**\n * Make the no wrapped card menus scroll using swipe or drag.\n */\n const cardScroll = () => {\n var cards = document.querySelectorAll('.card-dropdown.card-overflow-no-wrap');\n if (cards !== null) {\n var scrollStart; // Verify the mouse is clicked and still in click not released.\n var scrollMoved; // Prevent the click on scrolling.\n let startPos, scrollPos;\n\n cards.forEach((card) => {\n var scrollElement = card.querySelector('.dropdown-menu');\n\n scrollElement.addEventListener('mousedown', (e) => {\n scrollStart = true;\n var target = e.currentTarget.querySelector('.card-block-wrapper');\n startPos = e.pageX;\n scrollPos = target.scrollLeft;\n });\n\n scrollElement.addEventListener('mousemove', (e) => {\n e.preventDefault();\n if (!scrollStart) {\n return;\n }\n scrollMoved = true;\n var target = e.currentTarget.querySelector('.card-block-wrapper');\n const scroll = e.pageX - startPos;\n target.scrollLeft = scrollPos - scroll;\n });\n\n scrollElement.addEventListener('click', (e) => {\n if (scrollMoved) {\n e.preventDefault();\n scrollMoved = false;\n }\n e.stopPropagation();\n });\n scrollElement.addEventListener('mouseleave', () => {\n scrollStart = false;\n scrollMoved = false;\n });\n scrollElement.addEventListener('mouseup', () => {\n scrollStart = false;\n });\n });\n }\n };\n\n /**\n * Move the menubar and primary navigation menu items from more menu.\n */\n const autoCollapse = () => {\n var primaryNav = document.querySelector('.primary-navigation ul.more-nav');\n moveOutMoreMenu(primaryNav);\n\n var menuBar = document.querySelector('nav.menubar ul.more-nav');\n moveOutMoreMenu(menuBar);\n };\n\n /**\n * Move the items from more menu, items which is set to force outside more menu.\n * Remove those items from more menu and insert the menu before the last normal item.\n * Find the length and children's length to insert the out menus in that positions.\n * Rerun the more menu it will more the other normal menus into more menu to fix the alignmenu issue.\n *\n * @param {HTMLElement} navMenu The navbar container.\n */\n const moveOutMoreMenu = (navMenu) => {\n\n if (navMenu === null) {\n return;\n }\n\n var outMenus = navMenu.querySelectorAll('.dropdownmoremenu .force-menu-out');\n var menuslist = [];\n\n if (outMenus === null) {\n return;\n }\n\n outMenus.forEach((menu) => {\n menu.querySelector('a').classList.remove('dropdown-item');\n menu.querySelector('a').classList.add('nav-link');\n\n menuslist.push(menu);\n menu.parentNode.removeChild(menu);\n });\n // Find the length and children's length to insert the out menus in that positions.\n var length = menuslist.length;\n var navLength = navMenu.children.length - 1; // Remove more menu.\n var newPosition = navLength - length || 0;\n // Insert the stored menus before the more menu.\n menuslist.forEach((menu) => navMenu.insertBefore(menu, navMenu.children[newPosition]));\n window.dispatchEvent(new Event('resize')); // Dispatch the resize event to create more menu.\n };\n\n return {\n init: () => {\n addSubmenu();\n cardScroll();\n autoCollapse();\n }\n };\n});\n"],"names":["define","$","hideSubmenus","target","visibleMenu","document","querySelectorAll","forEach","el","classList","remove","moveOutMoreMenu","navMenu","outMenus","menuslist","menu","querySelector","add","push","parentNode","removeChild","length","newPosition","children","insertBefore","window","dispatchEvent","Event","init","submenu","item","addEventListener","e","currentTarget","toggle","stopPropagation","on","submenus","relatedTarget","dropdown","closest","subMenu","dropdownMenu","contains","helpIcon","icon","addSubmenu","cards","scrollStart","scrollMoved","startPos","scrollPos","card","scrollElement","pageX","scrollLeft","preventDefault","scroll","cardScroll","primaryNav","menuBar","autoCollapse"],"mappings":";;;;;;;AAuBAA,qCAAO,CAAC,SAAU,kBAAkB,SAASC,SA0EnCC,aAAgBC,aACdC,YAAcC,SAASC,iBAAiB,uCACxB,OAAhBF,aACAA,YAAYG,SAASC,KACbA,IAAML,QACNK,GAAGC,UAAUC,OAAO,YA0E9BC,gBAAmBC,aAEL,OAAZA,aAIAC,SAAWD,QAAQN,iBAAiB,qCACpCQ,UAAY,MAEC,OAAbD,UAIJA,SAASN,SAASQ,OACdA,KAAKC,cAAc,KAAKP,UAAUC,OAAO,iBACzCK,KAAKC,cAAc,KAAKP,UAAUQ,IAAI,YAEtCH,UAAUI,KAAKH,MACfA,KAAKI,WAAWC,YAAYL,aAG5BM,OAASP,UAAUO,OAEnBC,YADYV,QAAQW,SAASF,OAAS,EACZA,QAAU,EAExCP,UAAUP,SAASQ,MAASH,QAAQY,aAAaT,KAAMH,QAAQW,SAASD,gBACxEG,OAAOC,cAAc,IAAIC,MAAM,oBAG5B,CACHC,KAAM,KAlLS,UAEXC,QAAUxB,SAASC,iBAAiB,kCACxB,OAAZuB,SACAA,QAAQtB,SAASuB,OAEbA,KAAKC,iBAAiB,SAAUC,QACxB7B,OAAS6B,EAAEC,cAEf/B,aAAaC,QACbA,OAAOM,UAAUyB,OAAO,QAExBF,EAAEG,wBAMdlC,EAAEI,UAAU+B,GAAG,sBAAsBJ,QAE7BK,SADSL,EAAEM,cAAcnB,WACPb,iBAAiB,0BACtB,OAAb+B,UACAA,SAAS9B,SAASyB,GAAMA,EAAEvB,UAAUC,OAAO,aAOnDL,SAAS0B,iBAAiB,SAAUC,QAC5BO,SAAWP,EAAE7B,OAAOqC,QAAQ,qBAC5BC,QAAUT,EAAE7B,OAAOqC,QAAQ,qBAC3BD,UAAwB,OAAZE,UAEZF,SAASjC,iBAAiB,0BAA0BC,SAASQ,OACzDA,KAAKN,UAAUC,OAAO,WAE1B+B,QAAQhC,UAAUyB,OAAO,aAIzBQ,aAAeV,EAAE7B,OAAOgB,WAAWV,UAAUkC,SAAS,YACtDJ,UAAYG,cACZH,SAASjC,iBAAiB,uBAAuBC,SAASQ,OAElDA,MAAQiB,EAAE7B,OAAOqC,QAAQ,mBACzBzB,KAAKN,UAAUC,OAAO,cAKnC,OAGCkC,SAAWvC,SAASC,iBAAiB,sCACxB,OAAbsC,UACAA,SAASrC,SAASsC,OACdA,KAAKd,iBAAiB,SAAUC,IAC5BA,EAAEG,yBAyHVW,GAhGW,UACXC,MAAQ1C,SAASC,iBAAiB,2CACxB,OAAVyC,MAAgB,KACZC,YACAC,gBACAC,SAAUC,UAEdJ,MAAMxC,SAAS6C,WACPC,cAAgBD,KAAKpC,cAAc,kBAEvCqC,cAActB,iBAAiB,aAAcC,IACzCgB,aAAc,MACV7C,OAAS6B,EAAEC,cAAcjB,cAAc,uBAC3CkC,SAAWlB,EAAEsB,MACbH,UAAYhD,OAAOoD,cAGvBF,cAActB,iBAAiB,aAAcC,OACzCA,EAAEwB,kBACGR,mBAGLC,aAAc,MACV9C,OAAS6B,EAAEC,cAAcjB,cAAc,6BACrCyC,OAASzB,EAAEsB,MAAQJ,SACzB/C,OAAOoD,WAAaJ,UAAYM,UAGpCJ,cAActB,iBAAiB,SAAUC,IACjCiB,cACAjB,EAAEwB,iBACFP,aAAc,GAElBjB,EAAEG,qBAENkB,cAActB,iBAAiB,cAAc,KACzCiB,aAAc,EACdC,aAAc,KAElBI,cAActB,iBAAiB,WAAW,KACtCiB,aAAc,UAyDtBU,GAhDa,UACbC,WAAatD,SAASW,cAAc,mCACxCL,gBAAgBgD,gBAEZC,QAAUvD,SAASW,cAAc,2BACrCL,gBAAgBiD,UA4CZC"} \ No newline at end of file diff --git a/amd/src/fontawesome-popover.js b/amd/src/fontawesome-popover.js new file mode 100644 index 00000000000..3627d352971 --- /dev/null +++ b/amd/src/fontawesome-popover.js @@ -0,0 +1,217 @@ +// 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 . + +/** + * Theme Boost Union - JS code which shows all fontawesome icons in a popover. + * + * @module theme_boost_union/fontawesome-popover + * @copyright 2023 bdecent GmbH + * @copyright based on code from theme_boost\footer-popover by Bas Brands. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['jquery', 'theme_boost/popover', 'core/fragment'], function($, popover, Fragment) { + + const SELECTORS = { + PICKERCONTAINER: '.fontawesome-iconpicker-popover', + PICKERCONTENT: '[data-region="icons-list"]', + }; + + var contextID; + + let pickerIsShown = false; + + var SELECTBOX; + + /** + * Get the icon list for popover. + * + * @returns {String} HTML string + * @private + */ + const getIconList = () => { + return Fragment.loadFragment('theme_boost_union', 'icons_list', contextID, {}); + }; + + /** + * Filter the icons in the list with values which the user entered in the search input. + * Given input will contain the text in both aria-value and aria-label. + * Ex. "core:t\document" is aria-value and "fa-document" is aria-label. + * + * @param {Element} target + */ + const filterIcons = (target) => { + var filter = target.value.toLowerCase(); + SELECTBOX.value = filter || 0; + var ul = document.querySelector('.fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions'); + if (ul === undefined || ul === null) { + return; + } + var li = ul.querySelectorAll('li'); + + for (var i = 0; i < li.length; i++) { + var value = li[i].getAttribute('aria-value'); + var label = li[i].getAttribute('aria-label'); + if (!value.toLowerCase().includes(filter) && !label.toLowerCase().includes(filter)) { + li[i].style.display = "none"; + } else { + li[i].style.display = "inline-block"; + } + } + }; + + /** + * Creates input element and append the element into the target element's parent node. + * User is able to search icons using this input field. + * + * @param {String} target Element Selector. + */ + const createElements = (target) => { + + var input = document.createElement('input'); + input.setAttribute('type', 'text'); + input.classList.add('fontawesome-autocomplete'); + input.classList.add('form-control'); + input.setAttribute('name', 'iconsearch'); + + if (SELECTBOX.value != '') { + input.value = SELECTBOX.querySelector('option[selected]') !== null + ? SELECTBOX.querySelector('option[selected]').text : ''; + } + + var wrapper = document.createElement('div'); + wrapper.classList.add("fontawesome-picker-container"); + wrapper.append(input); + + document.querySelector(target).style.display = 'none'; + document.querySelector(target).parentNode.append(wrapper); + }; + + /** + * Update the target with fontawesome iconpicker. + * + * Create picker input field for search icons insert to DOM, fetch the icons list and setup the popover with icons content. + * Display the popover when the icon search input field is focused or clicked. This way user can view the list of icons and + * search icons. When the icon is selected, same icon in the select element will be selected. + * + * @param {String} target Element Selector. + */ + const iconPicker = (target) => { + + SELECTBOX = document.querySelector(target); + + if (SELECTBOX === undefined || SELECTBOX === null) { + return; + } + + // Create input element and insert for search icons and hide the current target select box. + createElements(target); + + // Parent of the target element. + var selectBoxParent = document.querySelector(target).parentNode; + + // Input element for search icons, appended in createElements method. + const pickerInput = selectBoxParent.querySelector("input.fontawesome-autocomplete"); + + // Check the search input created and inserted in DOM. + if (pickerInput === undefined || pickerInput === null) { + setTimeout(() => iconPicker(target), 1000); + return; + } + + // Fetch the icons list and setup popover with icons list. + getIconList().then(function(html) { + + $(pickerInput).popover({ + content: html, + html: true, + placement: 'bottom', + customClass: 'fontawesome-picker', + trigger: 'click' + }); + + // Event observer when the popover is inserted in DOM, create event listner for each icon in icons list. + // Icon is clicked, set the icon aria-value as value for select box. + // Set the icon label to value of autocomplete picker. + $(pickerInput).on('inserted.bs.popover', function() { + var ul = document.querySelector('.fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions'); + ul.querySelectorAll('li').forEach((li) => { + li.addEventListener('click', (e) => { + var target = e.target.closest('li'); + var value = target.getAttribute('aria-value'); + var label = target.getAttribute('aria-label'); + pickerInput.value = label; + SELECTBOX.value = value || 0; + $(pickerInput).popover('hide'); + }); + }); + }); + return; + }).catch(); + + document.addEventListener('click', e => { + if (pickerIsShown && !e.target.closest(SELECTORS.PICKERCONTAINER)) { + $(pickerInput).popover('hide'); + } + }, + true); + + document.addEventListener('keydown', e => { + if (pickerIsShown && e.key === 'Escape') { + $(pickerInput).popover('hide'); + pickerInput.focus(); + } + }); + + document.addEventListener('focus', e => { + if (pickerIsShown && !e.target.closest(SELECTORS.PICKERCONTAINER)) { + $(pickerInput).popover('hide'); + } + }, + true); + + $(pickerInput).on('shown.bs.popover', () => { + pickerIsShown = true; + // Add class to selected icon, helps to differentiate. + if (pickerInput.value != '') { + var iconSuggestion = document.querySelector('.fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions'); + if (iconSuggestion.querySelector('li[aria-label="' + pickerInput.value + '"]') !== null) { + // Remove selected class. + iconSuggestion.querySelectorAll('li').forEach((li) => + li.classList.remove('selected')); + // Assign selected class for new. + iconSuggestion.querySelector('li[aria-label="' + pickerInput.value + '"]').classList.add('selected'); + } + } + }); + + $(pickerInput).on('hide.bs.popover', () => { + pickerIsShown = false; + }); + + pickerInput.addEventListener('keyup', function(e) { + filterIcons(e.target); + }); + + }; + + return { + init: (target, contextid) => { + contextID = contextid; + iconPicker(target); + } + + }; +}); diff --git a/amd/src/smartmenu.js b/amd/src/smartmenu.js new file mode 100644 index 00000000000..892b105dc9b --- /dev/null +++ b/amd/src/smartmenu.js @@ -0,0 +1,213 @@ +// 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 . + +/** + * Theme Boost Union - JS for smart menu to realize the third level submenu support. + * + * @module theme_boost_union/smartmenu + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(["jquery", "core/moremenu"], function($) { + /** + * Implement the second level of submenu support. + * Find the submenus inside the dropdown, add an event listener for click event which - on the click - shows the submenu list. + */ + const addSubmenu = () => { + // Fetch the list of submenus from moremenu. + var submenu = document.querySelectorAll('nav.moremenu .dropdown-submenu'); + if (submenu !== null) { + submenu.forEach((item) => { + // Add event listener to show the submenu on click. + item.addEventListener('click', (e) => { + var target = e.currentTarget; + // Hide the shown menu. + hideSubmenus(target); + target.classList.toggle('show'); + // Prevent hiding the parent menu. + e.stopPropagation(); + }); + }); + } + + // Hide the submenus when its parent dropdown is hidden. + $(document).on('hidden.bs.dropdown', e => { + var target = e.relatedTarget.parentNode; + var submenus = target.querySelectorAll('.dropdown-submenu.show'); + if (submenus !== null) { + submenus.forEach((e) => e.classList.remove('show')); + } + }); + + // Provide the third level menu support inside the more menu. + // StopPropagation used in the toggledropdown method on Moremenu.js, It prevents the opening of the third level menus. + // Used the document delegation method to fetch the click on moremenu and submenu. + document.addEventListener('click', (e) => { + var dropdown = e.target.closest('.dropdownmoremenu'); + var subMenu = e.target.closest('.dropdown-submenu'); + if (dropdown && subMenu !== null) { + // Hide the previously opend submenus. before open the new one. + dropdown.querySelectorAll('.dropdown-submenu.show').forEach((menu) => { + menu.classList.remove('show'); + }); + subMenu.classList.toggle('show'); + } + + // Hide the opened menus before open the other menus. + var dropdownMenu = e.target.parentNode.classList.contains('dropdown'); + if (dropdown && dropdownMenu) { + dropdown.querySelectorAll('.dropdown-menu.show').forEach((menu) => { + // Hide the opened menus in more menu. + if (menu != e.target.closest('.dropdown-menu')) { + menu.classList.remove('show'); + } + }); + } + + }, true); + + // Prevent the closing of dropdown during the click on help icon. + var helpIcon = document.querySelectorAll('.moremenu .dropdown .menu-helpicon'); + if (helpIcon !== null) { + helpIcon.forEach((icon) => { + icon.addEventListener('click', (e) => { + e.stopPropagation(); + }); + }); + } + }; + + /** + * Hide visible submenus before display new submenu. + * + * @param {Selector} target + */ + const hideSubmenus = (target) => { + var visibleMenu = document.querySelectorAll('nav.moremenu .dropdown-submenu.show'); + if (visibleMenu !== null) { + visibleMenu.forEach((el) => { + if (el != target) { + el.classList.remove('show'); + } + }); + } + }; + + /** + * Make the no wrapped card menus scroll using swipe or drag. + */ + const cardScroll = () => { + var cards = document.querySelectorAll('.card-dropdown.card-overflow-no-wrap'); + if (cards !== null) { + var scrollStart; // Verify the mouse is clicked and still in click not released. + var scrollMoved; // Prevent the click on scrolling. + let startPos, scrollPos; + + cards.forEach((card) => { + var scrollElement = card.querySelector('.dropdown-menu'); + + scrollElement.addEventListener('mousedown', (e) => { + scrollStart = true; + var target = e.currentTarget.querySelector('.card-block-wrapper'); + startPos = e.pageX; + scrollPos = target.scrollLeft; + }); + + scrollElement.addEventListener('mousemove', (e) => { + e.preventDefault(); + if (!scrollStart) { + return; + } + scrollMoved = true; + var target = e.currentTarget.querySelector('.card-block-wrapper'); + const scroll = e.pageX - startPos; + target.scrollLeft = scrollPos - scroll; + }); + + scrollElement.addEventListener('click', (e) => { + if (scrollMoved) { + e.preventDefault(); + scrollMoved = false; + } + e.stopPropagation(); + }); + scrollElement.addEventListener('mouseleave', () => { + scrollStart = false; + scrollMoved = false; + }); + scrollElement.addEventListener('mouseup', () => { + scrollStart = false; + }); + }); + } + }; + + /** + * Move the menubar and primary navigation menu items from more menu. + */ + const autoCollapse = () => { + var primaryNav = document.querySelector('.primary-navigation ul.more-nav'); + moveOutMoreMenu(primaryNav); + + var menuBar = document.querySelector('nav.menubar ul.more-nav'); + moveOutMoreMenu(menuBar); + }; + + /** + * Move the items from more menu, items which is set to force outside more menu. + * Remove those items from more menu and insert the menu before the last normal item. + * Find the length and children's length to insert the out menus in that positions. + * Rerun the more menu it will more the other normal menus into more menu to fix the alignmenu issue. + * + * @param {HTMLElement} navMenu The navbar container. + */ + const moveOutMoreMenu = (navMenu) => { + + if (navMenu === null) { + return; + } + + var outMenus = navMenu.querySelectorAll('.dropdownmoremenu .force-menu-out'); + var menuslist = []; + + if (outMenus === null) { + return; + } + + outMenus.forEach((menu) => { + menu.querySelector('a').classList.remove('dropdown-item'); + menu.querySelector('a').classList.add('nav-link'); + + menuslist.push(menu); + menu.parentNode.removeChild(menu); + }); + // Find the length and children's length to insert the out menus in that positions. + var length = menuslist.length; + var navLength = navMenu.children.length - 1; // Remove more menu. + var newPosition = navLength - length || 0; + // Insert the stored menus before the more menu. + menuslist.forEach((menu) => navMenu.insertBefore(menu, navMenu.children[newPosition])); + window.dispatchEvent(new Event('resize')); // Dispatch the resize event to create more menu. + }; + + return { + init: () => { + addSubmenu(); + cardScroll(); + autoCollapse(); + } + }; +}); diff --git a/classes/cache/loader.php b/classes/cache/loader.php new file mode 100644 index 00000000000..21a093b4afa --- /dev/null +++ b/classes/cache/loader.php @@ -0,0 +1,60 @@ +. + +/** + * Theme Boost Union - Custom cache loader for the smart menus. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union\cache; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/cache/classes/loaders.php'); + +/** + * Custom cache loader to handle the smart menus and items deletion. + */ +class loader extends \cache_application { + + /** + * Delete the cached menus or menu items for all of its users. + * + * Fetch the cache store, generate the keys with menu or item id and keyword of user cache. + * Get the list of cached files by their filename, filenames are stored in the format of "menuid/itemid_u_ userid". + * Generate the key with menu/item id and the keyword of "_u" to get list of all users cache file for this menu/item. + * + * Delete all the files using delete_many method. + * + * @param int $id ID of the menu or item. + * @return void + */ + public function delete_menu($id) { + $store = $this->get_store(); + $prefix = $id .'_u'; + if ($list = $store->find_by_prefix($prefix)) { + + $keys = array_map(function($key) { + $key = current(explode('-', $key)); + return $key; + }, $list); + $this->delete_many($keys); + } + } +} diff --git a/classes/eventobservers.php b/classes/eventobservers.php index 4494523ff7e..a07d09d1368 100644 --- a/classes/eventobservers.php +++ b/classes/eventobservers.php @@ -36,7 +36,7 @@ class eventobservers { /** * Cohort deleted event observer. * - * @param \core\event\base $event The event. + * @param \core\event\base $event The event that triggered the handler. */ public static function cohort_deleted(\core\event\base $event) { global $CFG; @@ -51,12 +51,19 @@ public static function cohort_deleted(\core\event\base $event) { // of the current user and not for all users. \cache_helper::purge_by_event('theme_boost_union_cohort_deleted'); } + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Deletion this cohort may result in a menu change for its users. + // Verify if any of the menus used this cohort in restriction rules and, if yes, purge the menus cache. + \smartmenu_helper::purge_cache_deleted_cohort($event->objectid); } /** * Cohort member added event observer. * - * @param \core\event\base $event The event. + * @param \core\event\base $event The event that triggered the handler. */ public static function cohort_member_added(\core\event\base $event) { global $CFG; @@ -72,12 +79,19 @@ public static function cohort_member_added(\core\event\base $event) { // This way, we avoid that the flavours cache is purged unnecessarily for all users. set_user_preference('theme_boost_union_flavours_purgesessioncache', true, $event->relateduserid); } + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Adding users to this cohort may result in a menu change for these users. + // Verify if any of the menus used this cohort in restriction rules and, if yes, purge the menus cache for this user. + \smartmenu_helper::purge_cache_session_cohort($event->objectid, $event->relateduserid); } /** * Cohort member removed event observer. * - * @param \core\event\base $event The event. + * @param \core\event\base $event The event that triggered the handler. */ public static function cohort_member_removed(\core\event\base $event) { global $CFG; @@ -93,5 +107,119 @@ public static function cohort_member_removed(\core\event\base $event) { // This way, we avoid that the flavours cache is purged unnecessarily for all users. set_user_preference('theme_boost_union_flavours_purgesessioncache', true, $event->relateduserid); } + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Removing users from this cohort may result in a menu change for these users. + // Verify if any of the menus used this cohort in restriction rules and, if yes, purge the menus cache for this user. + \smartmenu_helper::purge_cache_session_cohort($event->objectid, $event->relateduserid); + } + + /** + * Event observer for when a role is assigned to a user. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function role_assigned(\core\event\base $event) { + global $CFG; + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Purge the cached menus for the user with the assigned role. + \smartmenu_helper::purge_cache_session_roles($event->objectid, $event->relateduserid); + } + + /** + * Event observer for when a role is unassigned from a user. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function role_unassigned(\core\event\base $event) { + global $CFG; + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Purge the cached menus for the user with the unassigned role. + \smartmenu_helper::purge_cache_session_roles($event->objectid, $event->relateduserid); + } + + /** + * Event observer for when a role is deleted. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function role_deleted(\core\event\base $event) { + global $CFG; + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Purge the cached menus for all users with the deleted role. + \smartmenu_helper::purge_cache_deleted_roles($event->objectid); + } + + /** + * Event observer for when a course is updated. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function course_updated(\core\event\base $event) { + global $CFG; + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Purge all the dynamic course items cache. + \smartmenu_helper::purge_cache_dynamic_courseitems(); + + return true; + } + + /** + * Event observer for when a category is updated or deleted. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function category_updated(\core\event\base $event) { + global $CFG; + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Clear the cache of menu when the course updated. + \smartmenu_helper::purge_cache_dynamic_courseitems(); + } + + /** + * Event observer for when a course or module completion is updated. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function completion_updated(\core\event\base $event) { + global $CFG; + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Clear the cache of menu when the course/module completion updated for user. + \smartmenu_helper::set_user_purgecache($event->relateduserid); + } + + /** + * Event observer for when a user profile is updated. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function user_updated(\core\event\base $event) { + global $CFG; + + // Require smart menus library. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + + // Clear the cache of menu when the course/module completion updated for user. + \smartmenu_helper::set_user_purgecache($event->relateduserid); } } diff --git a/classes/form/smartmenu_form.php b/classes/form/smartmenu_form.php new file mode 100644 index 00000000000..faedea087ba --- /dev/null +++ b/classes/form/smartmenu_form.php @@ -0,0 +1,241 @@ +. + +/** + * Theme Boost Union - Smart menu edit form + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union\form; + +defined('MOODLE_INTERNAL') || die(); + +// Require forms library. +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot.'/cohort/lib.php'); + +use theme_boost_union\smartmenu; + +/** + * Form for editing or adding a smart menu item. + */ +class smartmenu_form extends \moodleform { + + /** + * Define smartmenu form elements. + * + * @return void + */ + public function definition() { + $mform = $this->_form; + + $mform->addElement('hidden', 'id', 0); + $mform->setType('id', PARAM_INT); + + // General section. + $mform->addElement('header', 'generalsection', get_string('smartmenusgeneralsection', 'theme_boost_union')); + + // Add the Title field (required). + $mform->addElement('text', 'title', get_string('smartmenusmenutitle', 'theme_boost_union')); + $mform->setType('title', PARAM_TEXT); + $mform->addRule('title', get_string('error'), 'required'); + $mform->addHelpButton('title', 'smartmenusmenutitle', 'theme_boost_union'); + + // Add the Description field (optional). + $mform->addElement('editor', 'description', get_string('description')); + $mform->addHelpButton('description', 'smartmenusdescription', 'theme_boost_union'); + + // Add the Show Description field (required). + $options = array( + smartmenu::DESC_NEVER => get_string('never', 'core'), + smartmenu::DESC_ABOVE => get_string('smartmenusabove', 'theme_boost_union'), + smartmenu::DESC_BELOW => get_string('smartmenusbelow', 'theme_boost_union'), + smartmenu::DESC_HELP => get_string('help', 'core') + ); + $mform->addElement('select', 'showdesc', get_string('smartmenusshowdescription', 'theme_boost_union'), $options); + + // Add the Location field, Locations the menu will be shown. + $types = \theme_boost_union\smartmenu::get_locations(); + $location = $mform->addElement('autocomplete', 'location', get_string('smartmenuslocation', 'theme_boost_union'), $types); + $mform->addHelpButton('location', 'smartmenuslocation', 'theme_boost_union'); + $location->setMultiple(true); + + // Add the Type field (required), card, list. + $types = \theme_boost_union\smartmenu::get_types(); + $mform->addElement('select', 'type', get_string('smartmenustypes', 'theme_boost_union'), $types); + $mform->addHelpButton('type', 'smartmenustypes', 'theme_boost_union'); + + // Advanced settings options. Settings for advanced users / special use cases. + $mform->addElement('header', 'advanced_settings', get_string('smartmenusadvancedsettings', 'theme_boost_union')); + + // Display options mode. + $displayoptions = [ + smartmenu::MODE_SUBMENU => get_string('smartmenussubmenu', 'theme_boost_union'), + smartmenu::MODE_INLINE => get_string('smartmenusinline', 'theme_boost_union'), + ]; + $mform->addElement('select', 'mode', get_string('smartmenusmode', 'theme_boost_union'), $displayoptions); + $mform->addHelpButton('mode', 'smartmenusmenumode', 'theme_boost_union'); + + // CSS Class. + $mform->addElement('text', 'cssclass', get_string('smartmenuscssclass', 'theme_boost_union')); + $mform->addHelpButton('cssclass', 'smartmenuscssclass', 'theme_boost_union'); + $mform->setType('cssclass', PARAM_TEXT); + + // More menu behavior. + $moremenu = array( + smartmenu::MOREMENU_DEFAULT => get_string('default', 'core'), + smartmenu::MOREMENU_INTO => get_string('smartmenusforcedintomoremenu', 'theme_boost_union'), + smartmenu::MOREMENU_OUTSIDE => get_string('smartmenusforcedoutsideofmoremenu', 'theme_boost_union') + ); + $mform->addElement('select', 'moremenubehavior', get_string('smartmenusmoremenubehavior', 'theme_boost_union'), $moremenu); + $mform->addHelpButton('moremenubehavior', 'smartmenusmoremenubehavior', 'theme_boost_union'); + + // Card appearance options. + $mform->addElement('header', 'card_appearance', get_string('smartmenuscardappearance', 'theme_boost_union')); + + // Size. + $sizeoptions = array( + smartmenu::TINY => get_string('smartmenustiny', 'theme_boost_union').' (50px)', + smartmenu::SMALL => get_string('smartmenussmall', 'theme_boost_union').' (100px)', + smartmenu::MEDIUM => get_string('smartmenusmedium', 'theme_boost_union').' (150px)', + smartmenu::LARGE => get_string('smartmenuslarge', 'theme_boost_union').' (200px)' + ); + $mform->addElement('select', 'cardsize', get_string('smartmenuscardsize', 'theme_boost_union'), $sizeoptions); + $mform->disabledIf('cardsize', 'type', 0); + $mform->addHelpButton('cardsize', 'smartmenuscardsize', 'theme_boost_union'); + + // Form. + $formoptions = array( + smartmenu::SQUARE => get_string('smartmenussquare', 'theme_boost_union') . ' (1/1)', + smartmenu::PORTRAIT => get_string('smartmenusportrait', 'theme_boost_union') . ' (2/3)', + smartmenu::LANDSCAPE => get_string('smartmenuslandscape', 'theme_boost_union') . ' (3/2)', + smartmenu::FULLWIDTH => get_string('smartmenusfullwidth', 'theme_boost_union') + ); + $mform->addElement('select', 'cardform', get_string('smartmenuscardform', 'theme_boost_union'), $formoptions); + $mform->disabledIf('cardform', 'type', 'neq', smartmenu::TYPE_CARD); + $mform->addHelpButton('cardform', 'smartmenuscardform', 'theme_boost_union'); + + // Overflow behavior. + $overflow = array( + smartmenu::NOWRAP => get_string('smartmenusnowrap', 'theme_boost_union'), + smartmenu::WRAP => get_string('smartmenuswrap', 'theme_boost_union') + ); + $mform->addElement('select', 'overflowbehavior', get_string('smartmenusoverflowbehavior', 'theme_boost_union'), $overflow); + $mform->disabledIf('overflowbehavior', 'type', 'neq', smartmenu::TYPE_CARD); + $mform->addHelpButton('overflowbehavior', 'smartmenusoverflowbehavior', 'theme_boost_union'); + + // Access rule by roles. + $mform->addElement('header', 'accessbyroles', get_string('smartmenusaccessbyroles', 'theme_boost_union')); + + // Access based on the user roles. + $roleoptions = role_get_names(\context_system::instance()); + $roles = []; + foreach ($roleoptions as $role) { + $roles[$role->id] = $role->localname; + } + $roles = $mform->addElement('autocomplete', 'roles', get_string('smartmenusbyrole', 'theme_boost_union'), $roles); + $mform->addHelpButton('roles', 'smartmenusbyrole', 'theme_boost_union'); + $roles->setMultiple(true); + + $rolecontext = [ + smartmenu::ANYCONTEXT => get_string('any'), + smartmenu::SYSTEMCONTEXT => get_string('coresystem'), + ]; + $mform->addElement('select', 'rolecontext', get_string('smartmenusrolecontext', 'theme_boost_union'), $rolecontext); + $mform->addHelpButton('rolecontext', 'smartmenusrolecontext', 'theme_boost_union'); + + // Access rule by cohorts. + $mform->addElement('header', 'accessbycohorts', get_string('smartmenusaccessbycohorts', 'theme_boost_union')); + + // Cohorts based access. + $cohortslist = \cohort_get_all_cohorts(); + $cohorts = $cohortslist['cohorts']; + if ($cohorts) { + array_walk($cohorts, function(&$value) { + $value = $value->name; + }); + } + + $cohort = $mform->addElement('autocomplete', 'cohorts', get_string('smartmenusbycohort', 'theme_boost_union'), $cohorts); + $cohort->setMultiple(true); + $mform->addHelpButton('cohorts', 'smartmenusbycohort', 'theme_boost_union'); + + $rolecontext = [ + smartmenu::ANY => get_string('any'), + smartmenu::ALL => get_string('all'), + ]; + $mform->addElement('select', 'operator', get_string('smartmenusoperator', 'theme_boost_union'), $rolecontext); + $mform->addHelpButton('operator', 'smartmenusoperator', 'theme_boost_union'); + + // Access rule by languages. + $mform->addElement('header', 'accessbylanguage', get_string('smartmenusaccessbylanguage', 'theme_boost_union')); + + // Languages based access. + $languages = get_string_manager()->get_list_of_translations(); + $langoptions = array(); + foreach ($languages as $key => $lang) { + $langoptions[$key] = $lang; + } + $language = $mform->addElement('autocomplete', 'languages', + get_string('smartmenusbylanguage', 'theme_boost_union'), $langoptions); + $language->setMultiple(true); + $mform->addHelpButton('languages', 'smartmenusbylanguage', 'theme_boost_union'); + + // Access rule by languages. + $mform->addElement('header', 'accessbydateselector', get_string('smartmenusaccessbydateselector', 'theme_boost_union')); + + $mform->addElement('date_time_selector', 'start_date', + get_string('smartmenusfrom', 'theme_boost_union'), array('optional' => true)); + $mform->addHelpButton('start_date', 'smartmenusfrom', 'theme_boost_union'); + + $mform->addElement('date_time_selector', 'end_date', + get_string('smartmenusdurationuntil', 'theme_boost_union'), array('optional' => true)); + $mform->addHelpButton('end_date', 'smartmenusdurationuntil', 'theme_boost_union'); + + // Add the Submit button. + // When two elements we need a group. + $buttonar = array(); + $classar = array('class' => 'form-submit'); + $buttonar[] = &$mform->createElement('submit', 'saveandreturn', get_string('savechangesandreturn'), $classar); + $buttonar[] = &$mform->createElement('submit', 'saveanddisplay', + get_string('smartmenussavechangesandconfigure', 'theme_boost_union'), $classar); + $buttonar[] = &$mform->createElement('cancel'); + $mform->addGroup($buttonar, 'buttonar', '', array(' '), false); + $mform->closeHeaderBefore('buttonar'); + } + + /** + * Validates form data. Verify the card form size and overflow behaviour is selected if menu type is card. + * + * @param array $data Array containing form data. + * @param array $files Array containing uploaded files. + * @return array Array of errors, if any. + */ + public function validation($data, $files) { + $errors = []; + // Verify the URL field is not empty if the item type is static. + if ($data['type'] == smartmenu::TYPE_CARD && empty($data['cardform'])) { + $errors['cardform'] = get_string('required'); + } + if ($data['type'] == smartmenu::TYPE_CARD && empty($data['overflowbehavior'])) { + $errors['overflowbehavior'] = get_string('required'); + } + return $errors; + } +} diff --git a/classes/form/smartmenu_item_form.php b/classes/form/smartmenu_item_form.php new file mode 100644 index 00000000000..f68755e7c93 --- /dev/null +++ b/classes/form/smartmenu_item_form.php @@ -0,0 +1,350 @@ +. + +/** + * Theme Boost Union - Smart menu item edit form + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union\form; + +defined('MOODLE_INTERNAL') || die(); + +// Require forms library. +require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->dirroot.'/cohort/lib.php'); + +use theme_boost_union\smartmenu_item as menuitem; +use theme_boost_union\smartmenu; + +/** + * Smart menu items edit form. + */ +class smartmenu_item_form extends \moodleform { + + /** + * Menu item create form elements defined. + * + * @return void + */ + public function definition() { + global $DB, $PAGE, $CFG; + + $mform = $this->_form; + + // Current edit item id, null for new items. + $mform->addElement('hidden', 'id', 0); + $mform->setType('id', PARAM_INT); + + // Menu id. + $mform->addElement('hidden', 'menu', 0); + $mform->setType('menu', PARAM_INT); + $menuid = (isset($this->_customdata['menu'])) ? $this->_customdata['menu'] : 0; + $mform->setDefault('menu', $menuid); + + require_once($CFG->dirroot.'/theme/boost_union/form/element-colorpicker.php'); + \MoodleQuickForm::registerElementType( + 'theme_boost_union_colorpicker', + $CFG->dirroot.'/theme/boost_union/form/element-colorpicker.php', + 'moodlequickform_themeboostunion_colorpicker' + ); + + // General section. + $mform->addElement('header', 'general', get_string('general', 'core')); + + // Menu item Title. + $mform->addElement('text', 'title', get_string('smartmenustitle', 'theme_boost_union')); + $mform->setType('title', PARAM_TEXT); + $mform->addRule('title', null, 'required'); + $mform->addHelpButton('title', 'smartmenustitle', 'theme_boost_union'); + + // Type of the menu item. Display as heading or static content or dynamic courses. + $types = menuitem::get_types(); + $mform->addElement('select', 'type', get_string('smartmenustype', 'theme_boost_union'), $types); + $mform->setType('type', PARAM_INT); + $mform->addRule('type', null, 'required'); + $mform->addHelpButton('type', 'smartmenustype', 'theme_boost_union'); + + $mform->addElement('text', 'url', get_string('smartmenusurl', 'theme_boost_union')); + $mform->setType('url', PARAM_URL); + $mform->hideIf('url', 'type', 'neq', menuitem::TYPESTATIC); + $mform->addHelpButton('url', 'smartmenusurl', 'theme_boost_union'); + + // List of categories selector. + $categories = \core_course_category::make_categories_list(); + $cate = $mform->addElement('autocomplete', 'category', get_string('smartmenuscategory', 'theme_boost_union'), $categories); + $mform->setType('category', PARAM_INT); + $mform->hideIf('category', 'type', 'neq', menuitem::TYPEDYNAMIC); + $cate->setMultiple(true); + $mform->addHelpButton('category', 'smartmenuscategory', 'theme_boost_union'); + + // Get list of roles used in course context. + $roles = get_roles_for_contextlevels(CONTEXT_COURSE); + list($insql, $inparams) = $DB->get_in_or_equal(array_values($roles)); + $roles = $DB->get_records_sql("SELECT * FROM {role} WHERE id $insql", $inparams); + $enrolmentroles = role_fix_names($roles, null, ROLENAME_ALIAS, true); + + $role = $mform->addElement('autocomplete', 'enrolmentrole', + get_string('smartmenusenrolmentrole', 'theme_boost_union'), $enrolmentroles); + $mform->setType('enrolmentrole', PARAM_INT); + $mform->hideIf('enrolmentrole', 'type', 'neq', menuitem::TYPEDYNAMIC); + $role->setMultiple(true); + $mform->addHelpButton('enrolmentrole', 'smartmenusenrolmentrole', 'theme_boost_union'); + + $completionstatuses = array( + menuitem::COMPLETION_ENROLLED => get_string('smartmenusenrolled', 'theme_boost_union'), + menuitem::COMPLETION_INPROGRESS => get_string('inprogress', 'completion'), + menuitem::COMPLETION_COMPLETED => get_string('completed', 'completion'), + ); + $completion = $mform->addElement('autocomplete', 'completionstatus', + get_string('smartmenuscompletionstatus', 'theme_boost_union'), $completionstatuses); + $mform->setType('completionstatus', PARAM_INT); + $mform->hideIf('completionstatus', 'type', 'neq', menuitem::TYPEDYNAMIC); + $completion->setMultiple(true); + $mform->addHelpButton('completionstatus', 'smartmenuscompletionstatus', 'theme_boost_union'); + + $ranges = array( + menuitem::RANGE_PAST => get_string('smartmenuspast', 'theme_boost_union'), + menuitem::RANGE_PRESENT => get_string('smartmenuspresent', 'theme_boost_union'), + menuitem::RANGE_FUTURE => get_string('smartmenusfuture', 'theme_boost_union'), + ); + $range = $mform->addElement('autocomplete', 'daterange', get_string('smartmenusdaterange', 'theme_boost_union'), $ranges); + $mform->setType('daterange', PARAM_INT); + $mform->hideIf('daterange', 'type', 'neq', menuitem::TYPEDYNAMIC); + $range->setMultiple(true); + $mform->addHelpButton('daterange', 'smartmenusdaterange', 'theme_boost_union'); + + // Load the custom course fields form elements. + // Using custom fields to setup the conditions. + menuitem::load_custom_field_config($mform); + + // Appearance section. + $mform->addElement('header', 'appearance_header', get_string('smartmenusappearanceheader', 'theme_boost_union')); + $mform->addElement('static', 'appearanceheader_desc', get_string('smartmenusappearanceheader_desc', 'theme_boost_union')); + + // Display field value option. + $displayfields = [ + menuitem::FIELD_FULLNAME => get_string('smartmenusfullname', 'theme_boost_union'), + menuitem::FIELD_SHORTNAME => get_string('smartmenusshortname', 'theme_boost_union'), + ]; + $mform->addElement('select', 'displayfield', get_string('smartmenusdisplayfield', 'theme_boost_union'), $displayfields); + $mform->hideIf('displayfield', 'type', 'neq', menuitem::TYPEDYNAMIC); + $mform->addHelpButton('displayfield', 'smartmenusdisplayfield', 'theme_boost_union'); + + // Number of charaters to display in menu item title. + $mform->addElement('text', 'textcount', get_string('smartmenustextcount', 'theme_boost_union')); + $mform->setType('textcount', PARAM_INT); + $mform->hideIf('textcount', 'type', 'neq', menuitem::TYPEDYNAMIC); + $mform->addHelpButton('textcount', 'smartmenustextcount', 'theme_boost_union'); + + // Display options field. + $displayoptions = [ + menuitem::MODE_INLINE => get_string('smartmenusinline', 'theme_boost_union'), + menuitem::MODE_SUBMENU => get_string('smartmenussubmenu', 'theme_boost_union'), + ]; + $mform->addElement('select', 'mode', get_string('smartmenusmode', 'theme_boost_union'), $displayoptions); + $mform->addHelpButton('mode', 'smartmenusmode', 'theme_boost_union'); + + // Menu Icon. + $options = []; + $theme = \theme_config::load($PAGE->theme->name); + $faiconsystem = \core\output\icon_system_fontawesome::instance($theme->get_icon_system()); + $iconlist = $faiconsystem->get_core_icon_map(); + array_unshift($iconlist, ''); + // Icon element. + $icons = $mform->addElement('select', 'menuicon', get_string('icon', 'core'), $iconlist); + $icons->setMultiple(false); + $mform->setType('menuicon', PARAM_TEXT); + $mform->addHelpButton('menuicon', 'smartmenusmenuicon', 'theme_boost_union'); + + // Include the fontawesome icon picker for menu icon select. + $contextid = \context_system::instance()->id; + $PAGE->requires->js_call_amd('theme_boost_union/fontawesome-popover', 'init', ['#id_menuicon', $contextid]); + + // Display options field. + $displayoptions = [ + menuitem::DISPLAY_SHOWTITLEICON => get_string('smartmenusshowtitleicon', 'theme_boost_union'), + menuitem::DISPLAY_HIDETITLE => get_string('smartmenushidetitle', 'theme_boost_union'), + menuitem::DISPLAY_HIDETITLEMOBILE => get_string('smartmenushidetitlemobile', 'theme_boost_union') + ]; + $mform->addElement('select', 'display', get_string('smartmenusdisplayoptions', 'theme_boost_union'), $displayoptions); + $mform->addHelpButton('display', 'smartmenusdisplayoptions', 'theme_boost_union'); + + // Tooltip field. + $mform->addElement('text', 'tooltip', get_string('smartmenustooltip', 'theme_boost_union')); + $mform->setType('tooltip', PARAM_TEXT); + $mform->addHelpButton('tooltip', 'smartmenustooltip', 'theme_boost_union'); + + // Order field. + $mform->addElement('text', 'sortorder', get_string('smartmenusorder', 'theme_boost_union')); + $mform->setType('sortorder', PARAM_INT); + $mform->addRule('sortorder', get_string('required'), 'required'); + $mform->addRule('sortorder', get_string('err_numeric', 'form'), 'numeric'); + $mform->addHelpButton('sortorder', 'smartmenusorder', 'theme_boost_union'); + + if (isset($this->_customdata['nextorder'])) { + $mform->setDefault('sortorder', $this->_customdata['nextorder']); + } + + // Target field. + $targetoptions = [ + menuitem::TARGET_SAME => get_string('tilelinktargetsetting_samewindow', 'theme_boost_union'), + menuitem::TARGET_NEW => get_string('tilelinktargetsetting_newtab', 'theme_boost_union') + ]; + $mform->addElement('select', 'target', get_string('smartmenustarget', 'theme_boost_union'), $targetoptions); + $mform->addHelpButton('target', 'smartmenustarget', 'theme_boost_union'); + + // CSS class field. + $mform->addElement('text', 'cssclass', get_string('smartmenuscssclass', 'theme_boost_union')); + $mform->setType('cssclass', PARAM_TEXT); + $mform->addHelpButton('cssclass', 'smartmenuscssclass', 'theme_boost_union'); + + // Responsive fields. + $group = []; + // Hide in Desktop. + $group[] = $mform->createElement('advcheckbox', 'desktop', + get_string('smartmenusresponsivedesktop', 'theme_boost_union'), null, ['group' => 1]); + // Hide in Tablet. + $group[] = $mform->createElement('advcheckbox', 'tablet', + get_string('smartmenusresponsivetablet', 'theme_boost_union'), null, ['group' => 1]); + // Hide in mobile. + $group[] = $mform->createElement('advcheckbox', 'mobile', + get_string('smartmenusresponsivemobile', 'theme_boost_union'), null, ['group' => 1]); + $mform->addGroup($group, 'responsive', get_string('smartmenusresponsive', 'theme_boost_union'), '', false); + // Select all controller. + $this->add_checkbox_controller(1); + + // Appearance section. + $mform->addElement('header', 'appearance_card', get_string('smartmenusappearancecard', 'theme_boost_union')); + $mform->addElement('static', 'appearancecard_desc', get_string('smartmenusappearancecard_desc', 'theme_boost_union')); + + // Appearance options for cards. + $options = menuitem::image_fileoptions(); + $mform->addElement('filemanager', 'image', get_string('smartmenusimage', 'theme_boost_union'), null, $options); + $mform->addHelpButton('image', 'smartmenusimage', 'theme_boost_union'); + + $textpositionoptions = array( + menuitem::POSITION_BELOW => get_string('smartmenusbelowimage', 'theme_boost_union'), + menuitem::POSITION_OVERLAYTOP => get_string('smartmenusoverlaytop', 'theme_boost_union'), + menuitem::POSITION_OVERLAYBOTTOM => get_string('smartmenusoverlaybottom', 'theme_boost_union') + ); + $mform->addElement('select', 'textposition', + get_string('smartmenustextposition', 'theme_boost_union'), $textpositionoptions); + $mform->setDefault('textposition', 'theme_boost_union'); + $mform->addHelpButton('textposition', 'smartmenustextposition', 'theme_boost_union'); + + // Text color. + $mform->addElement('theme_boost_union_colorpicker', 'textcolor', get_string('smartmenustextcolor', 'theme_boost_union')); + $mform->addHelpButton('textcolor', 'smartmenustextcolor', 'theme_boost_union'); + $mform->setType('textcolor', PARAM_TEXT); + + // Background color. + $mform->addElement('theme_boost_union_colorpicker', 'backgroundcolor', + get_string('smartmenusbackgroundcolor', 'theme_boost_union')); + $mform->addHelpButton('backgroundcolor', 'smartmenusbackgroundcolor', 'theme_boost_union'); + $mform->setType('backgroundcolor', PARAM_TEXT); + + // Access rule by roles. + $mform->addElement('header', 'accessbyroles', get_string('smartmenusaccessbyroles', 'theme_boost_union')); + + // Access based on the user roles. + $roleoptions = role_get_names(\context_system::instance()); + $roles = []; + foreach ($roleoptions as $role) { + $roles[$role->id] = $role->localname; + } + $roles = $mform->addElement('autocomplete', 'roles', get_string('smartmenusbyrole', 'theme_boost_union'), $roles); + $mform->addHelpButton('roles', 'smartmenusbyrole', 'theme_boost_union'); + $roles->setMultiple(true); + + $rolecontext = [ + smartmenu::ANYCONTEXT => get_string('any'), + smartmenu::SYSTEMCONTEXT => get_string('coresystem'), + ]; + $mform->addElement('select', 'rolecontext', get_string('smartmenusrolecontext', 'theme_boost_union'), $rolecontext); + $mform->addHelpButton('rolecontext', 'smartmenusrolecontext', 'theme_boost_union'); + + // Access rule by cohorts. + $mform->addElement('header', 'accessbycohorts', get_string('smartmenusaccessbycohorts', 'theme_boost_union')); + + // Cohorts based access. + $cohorts = \cohort_get_all_cohorts(); + $cohortoptions = $cohorts['cohorts']; + if ($cohortoptions) { + array_walk($cohortoptions, function(&$value) { + $value = $value->name; + }); + } + $cohort = $mform->addElement('autocomplete', 'cohorts', + get_string('smartmenusbycohort', 'theme_boost_union'), $cohortoptions); + + $mform->addHelpButton('cohorts', 'smartmenusbycohort', 'theme_boost_union'); + $cohort->setMultiple(true); + + $operator = [ + smartmenu::ANY => get_string('any'), + smartmenu::ALL => get_string('all'), + ]; + $mform->addElement('select', 'operator', get_string('smartmenusoperator', 'theme_boost_union'), $operator); + $mform->addHelpButton('operator', 'smartmenusoperator', 'theme_boost_union'); + + // Access rule by languages. + $mform->addElement('header', 'accessbylanguage', get_string('smartmenusaccessbylanguage', 'theme_boost_union')); + + // Languages based access. + $languages = get_string_manager()->get_list_of_translations(); + $langoptions = array(); + foreach ($languages as $key => $lang) { + $langoptions[$key] = $lang; + } + $language = $mform->addElement('autocomplete', 'languages', + get_string('smartmenusbylanguage', 'theme_boost_union'), $langoptions); + $language->setMultiple(true); + $mform->addHelpButton('languages', 'smartmenusbylanguage', 'theme_boost_union'); + + // Access rule by dates. + $mform->addElement('header', 'accessbydateselector', get_string('smartmenusaccessbydateselector', 'theme_boost_union')); + // Prevent the menu display until the start date is reached. + $mform->addElement('date_time_selector', 'start_date', + get_string('smartmenusfrom', 'theme_boost_union'), array('optional' => true)); + $mform->addHelpButton('start_date', 'smartmenusfrom', 'theme_boost_union'); + // Hide the item, if the end date is reached. + $mform->addElement('date_time_selector', 'end_date', + get_string('smartmenusdurationuntil', 'theme_boost_union'), array('optional' => true)); + $mform->addHelpButton('end_date', 'smartmenusdurationuntil', 'theme_boost_union'); + + $this->add_action_buttons(); + } + + /** + * Validate the user input data. Verified the URL input filled if the item type is static. + * + * @param array $data + * @param array $files + * @return void + */ + public function validation($data, $files) { + $errors = []; + // Verify the URL field is not empty if the item type is static. + if ($data['type'] == menuitem::TYPESTATIC && empty($data['url'])) { + $errors['url'] = get_string('required'); + } + return $errors; + } +} diff --git a/classes/output/navigation/primary.php b/classes/output/navigation/primary.php new file mode 100644 index 00000000000..6f199d70150 --- /dev/null +++ b/classes/output/navigation/primary.php @@ -0,0 +1,233 @@ +. + +/** + * Theme Boost Union - Primary navigation render. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union\output\navigation; + +use renderable; +use renderer_base; +use templatable; +use custom_menu; +use \theme_boost_union\smartmenu; + +/** + * Primary navigation renderable. + * + * This file combines primary nav, custom menu, lang menu and + * usermenu into a standardized format for the frontend. + * + * This renderer is copied and modified from /lib/classes/navigation/output/primary.php + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @copyright based on code 2021 onwards Peter Dias + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class primary extends \core\navigation\output\primary { + + /** @var moodle_page $page the moodle page that the navigation belongs to */ + private $page = null; + + /** + * primary constructor. + * @param \moodle_page $page + */ + public function __construct($page) { + $this->page = $page; + parent::__construct($page); + } + + /** + * Combine the various menus into a standardized output. + * + * Modifications compared to the original function: + * * Build the smart menus and its items as navigation nodes. + * * Generate the nodes for different locations based on the menus locations. + * * Combine the smart menus nodes with core primary menus. + * + * @param renderer_base|null $output + * @return array + */ + public function export_for_template(?renderer_base $output = null): array { + global $DB; + + // Create smart menu cache. + $cache = \cache::make('theme_boost_union', 'smartmenus'); + + // Check if the smart menus are already there in the cache. + if (!$cache->get(smartmenu::CACHE_MENUSLIST)) { + // If the smart menu feature is not installed at all, use the parent function. + // This will help to avoid hickups during a theme upgrade. + $dbman = $DB->get_manager(); + if (!$dbman->table_exists('theme_boost_union_menus')) { + return parent::export_for_template($output); + } + } + + if (!$output) { + $output = $this->page->get_renderer('core'); + } + + // Generate the menus and its items into nodes. + $smartmenus = smartmenu::build_smartmenu(); + // Smartmenus not created, then fallback to core navigation. + if (empty($smartmenus)) { + return parent::export_for_template($output); + } + + // Get the menus for the main menu. + $mainmenu = smartmenu::get_menus_forlocation(smartmenu::LOCATION_MAIN, $smartmenus); + + // Separate the menus for the menubar. + $menubarmenus = smartmenu::get_menus_forlocation(smartmenu::LOCATION_MENU, $smartmenus); + + // Separate the menus for the user menus. + $locationusermenus = smartmenu::get_menus_forlocation(smartmenu::LOCATION_USER, $smartmenus); + + // Separate the menus for the bottom menu. + $locationbottom = smartmenu::get_menus_forlocation(smartmenu::LOCATION_BOTTOM, $smartmenus); + + // Merge the smart menu nodes which contain the main menu location with the primary and custom menu nodes. + $menudata = array_merge($this->get_primary_nav(), $this->get_custom_menu($output), $mainmenu); + $moremenu = new \core\navigation\output\more_menu((object) $menudata, 'navbar-nav', false); + + // Menubar. + // Items of menus only added in the menubar. + // Removed the menu nodes from menubar, each item will be displayed as menu in menubar. + if (!empty($menubarmenus)) { + $menubarmoremenu = new \core\navigation\output\more_menu((object) $menubarmenus, 'navbar-nav-menu-bar', false); + } + + // Bottom bar. + // Include the menu navigation menus to the mobile menu when the bottom bar doesn't have any menus. + $mobileprimarynav = (!empty($locationbottom)) + ? array_merge($this->get_primary_nav(), $this->get_custom_menu($output), $locationbottom) + : $mobileprimarynav = $menudata; + + if (!empty($mobileprimarynav)) { + $bottombar = new \core\navigation\output\more_menu((object) $mobileprimarynav, 'navbar-nav-bottom-bar', false); + $bottombardata = $bottombar->export_for_template($output); + $bottombardata['drawer'] = (!empty($locationbottom)) ? true : false; + } + + // Usermenu. + // Merge the smartmenu nodes which contains the location for user menu, with the default core user menu nodes. + $languagemenu = new \core\output\language_menu($this->page); + $usermenu = $this->get_user_menu($output); + $this->build_usermenus($usermenu, $locationusermenus); + + return [ + 'mobileprimarynav' => $mobileprimarynav, + 'moremenu' => $moremenu->export_for_template($output), + 'menubar' => isset($menubarmoremenu) ? $menubarmoremenu->export_for_template($output) : false, + 'lang' => !isloggedin() || isguestuser() ? $languagemenu->export_for_template($output) : [], + 'user' => $usermenu ?? [], + 'bottombar' => $bottombardata ?? false + ]; + } + + /** + * Attach the smart menus to the user menu which has location selected for user menu. + * Separate the children items and attach those items to submenus element in user menu. + * Add the menus in items element in user menu. + * + * User menu and its submenus are connected using submenuid. Added submenuid for submenu items if that has children. + * Add all the items before logout menu. Removed the logout menu, then add the items into user menu items, + * once all items are added, separator included before logout + * if any smart menus are included then added the logout menu to menu items. + * + * @param array $usermenu + * @param array $menus + * @return void + */ + public function build_usermenus(&$usermenu, $menus) { + + if (empty($menus)) { + return []; + } + + $logout = !empty($usermenu['items']) ? array_pop($usermenu['items']) : ''; + foreach ($menus as $menu) { + // Menu with empty childrens. + if (!isset($menu->children)) { + $usermenu['items'][] = $menu; + continue; + } + + // Menu with children, split the children and push them into submenus. + if (isset($menu->submenuid)) { + $children = $menu->children; + + // Update the dividers item type. + array_walk($children, function(&$value) use (&$usermenu, $menu) { + if (isset($value['divider'])) { + $value['itemtype'] = 'divider'; + $value['link'] = ''; + } + + // Children is submenu item, add third level submenu. + // Only three levels is available, therefore implemented in a static way, in case wants to use multiple levels. + // Convert this into separate function make dynamic. + if (!empty($value['children'])) { + $uniqueid = uniqid(); + $value['submenuid'] = $uniqueid; + + $submenu = [ + 'id' => $uniqueid, + 'returnid' => $menu->submenuid, // Return the third level submenus back to its parent section. + 'title' => $value['title'], + 'items' => $value['children'], + ]; + + // Insert the third level children into submenus. + $usermenu['submenus'][] = (object) $submenu; + + unset($value['children']); + } + }); + + $submenu = [ + 'id' => $menu->submenuid, + 'title' => $menu->title, + 'items' => $children + ]; + $usermenu['items'][] = $menu; + $usermenu['submenus'][] = (object) $submenu; + } + } + + // Include the divider after smart menus items to make difference from logout. + $divider = [ + 'title' => '####', + 'itemtype' => 'divider', + 'divider' => 1, + 'link' => '' + ]; + array_push($usermenu['items'], $divider); + + // Update the logout menu at end of menus. + if (!empty($logout)) { + array_push($usermenu['items'], $logout); + } + } +} diff --git a/classes/smartmenu.php b/classes/smartmenu.php new file mode 100644 index 00000000000..4ff63a64c94 --- /dev/null +++ b/classes/smartmenu.php @@ -0,0 +1,971 @@ +. + +/** + * Menu controller for managing menus and menu items. Build menu for different locations. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union; + +defined('MOODLE_INTERNAL') || die(); + +use custom_menu; +use context_system; +use moodle_exception; +use core\navigation\views\primary; +use cache; +use cache_helper; +use smartmenu_helper; + +require_once($CFG->dirroot.'/theme/boost_union/smartmenus/menulib.php'); + +/** + * The menu controller handles actions related to managing menus. + * + * This controller provides methods for listing available menus, creating new menus, + * updating existing menus, deleting menus, and sorting the order of menus. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class smartmenu { + + /** + * The unique identifier for the menu. + * + * @var int + */ + public $id; + + /** + * The name of the menu. + * + * @var \stdclass + */ + public $menu; + + /** + * The helper object for managing smart menu restrict access rules. + * + * @var \smartmenu_helper + */ + public $helper; + + /** + * The cache object for caching menu data. + * + * @var \cache + */ + public $cache; + + /** + * Constants that define different locations where the menu can appear. + * Displayed in Main menu. + * + * @var int + */ + public const LOCATION_MAIN = 1; + + /** + * Displayed in Menu bar. Top of the main menu. + * + * @var int + */ + public const LOCATION_MENU = 2; + + /** + * Display in Usermenu. + * + * @var int + */ + public const LOCATION_USER = 3; + + /** + * Display in the Bottom mobile bar. + * + * @var int + */ + public const LOCATION_BOTTOM = 4; + + /** + * The description text is never displayed. + * @var int + */ + public const DESC_NEVER = 0; + + /** + * The description text is displayed above the menu item. + * @var int + */ + public const DESC_ABOVE = 1; + + /** + * The description text is displayed below the menu item. + * @var int + */ + public const DESC_BELOW = 2; + + /** + * The description text is displayed in a help tooltip when the user hovers over the menu item. + * @var int + */ + public const DESC_HELP = 3; + + /** + * Constants for the type of smart menu display + * Displays menu items in a list format. + * + * @var int + */ + public const TYPE_LIST = 0; + + /** + * Displays menu items in a card format. + * @var int + */ + public const TYPE_CARD = 1; + + /** + * Roles based on Any context + * @var int + */ + public const ANYCONTEXT = 1; + + /** + * Roles based on System context + * @var int + */ + public const SYSTEMCONTEXT = 2; + + /** + * Constants for access rules all matching + * @var int + */ + public const ALL = 1; + + /** + * Constants for access rule any of matching + * @var int + */ + public const ANY = 2; + + /** + * Class constants for specifying image dimensions + */ + + /** + * Square (1/1) dimensions + * @var int + */ + public const SQUARE = 1; + + /** + * Portrait (2/3) dimensions + * @var int + */ + public const PORTRAIT = 2; + + /** + * Landscape (3/2) dimensions + * @var int + */ + public const LANDSCAPE = 3; + + /** + * Full width dimensions + * @var int + */ + public const FULLWIDTH = 4; + + /** + * Tiny size option. + * @var int + */ + public const TINY = 1; + + /** + * Small - Card size option . + * @var int + */ + public const SMALL = 2; + + /** + * Medium size option for card. + * @var int + */ + public const MEDIUM = 3; + + /** + * Large size option for card. + * @var int + */ + public const LARGE = 4; + + /** + * Constants for controlling the display of the "More" menu. + * Default position (below main menu) + * @var int + */ + public const MOREMENU_DEFAULT = 0; + + /** + * Position into the main menu. + * @var int + */ + public const MOREMENU_INTO = 1; + + /** + * Position outside of the main menu. + * @var int + */ + public const MOREMENU_OUTSIDE = 2; + + /** + * Flag to indicate the overflow behavior of card, should be wrapped. + * @var int + */ + public const WRAP = 1; + + /** + * Flag to indicate the overflow behavior of card, should not be wrapped. + * @var int + */ + public const NOWRAP = 2; + + /** + * Display the menuitems as menu. + * @var int + */ + public const MODE_INLINE = 2; + + /** + * Display the menuitems as submenu. + * @var int + */ + public const MODE_SUBMENU = 1; + + /** + * Cache key for the menus list. + */ + public const CACHE_MENUSLIST = 'menuslist'; + + /** + * Create an instance of the smartmenu class from the given menu ID or menu object/array. + * + * @param int|stdclass $menu + * @return smartmenu + */ + public static function instance($menu) { + + if (is_scalar($menu)) { + $menu = self::get_menu($menu); + } + + if (!is_array($menu) && !is_object($menu)) { + throw new moodle_exception('menuformatnotcorrect', 'theme_boost_union'); + } + return new self($menu); + } + + /** + * SmartMenu constructor. + * + * @param mixed $menu Menu data. + * @throws moodle_exception If menu format is not correct. + */ + public function __construct($menu) { + $this->id = $menu->id; + $this->menu = self::update_menu_valuesformat($menu); + $this->helper = new \smartmenu_helper($this->menu); + // Cache for menu. + $this->cache = cache::make('theme_boost_union', 'smartmenus'); + } + + /** + * Find the menu's visibility, it only consider the menus own visiblity + * Please use the $this->menu->visible to check only menus visiblity. + * + * @return bool + */ + public function is_visible() { + // Verify the menus access rules for current user. + return $this->menu->visible && $this->helper->verify_access_restrictions($this->menu) ? true : false; + } + + /** + * Delete the current menu and all its associated items from the database. + * + * @return bool True if the deletion is successful, false otherwise. + */ + public function delete_menu() { + global $DB; + if ($DB->delete_records('theme_boost_union_menus', ['id' => $this->id])) { + // Delete all its items. + $DB->delete_records('theme_boost_union_menuitems', ['menu' => $this->id]); + // Purge the menus cache. + $this->cache->delete_menu($this->id); + // Delete the cached menus list. + $this->cache->delete(self::CACHE_MENUSLIST); + + return true; + } + return false; + } + + /** + * Move the current menu upwards in the list of menus, fetch the previous menu and move the menu to previous menu position. + * + * @return bool true if the menu was moved successfully, false otherwise. + */ + public function move_upward() { + global $DB; + // Current menu position. + $currentposition = $this->menu->sortorder; + // Confirm is moving upward is possible. + if ($currentposition > 1) { + $menus = $DB->get_records('theme_boost_union_menus', ['sortorder' => $currentposition - 1]); + if (empty($menus)) { + return false; + } + $prevmenu = current($menus); + // Update the menu position to upwards. + $DB->set_field('theme_boost_union_menus', 'sortorder', $prevmenu->sortorder, ['id' => $this->id]); + // Set the prevmenu position to down. + $DB->set_field('theme_boost_union_menus', 'sortorder', $currentposition, ['id' => $prevmenu->id]); + // Purge the menus list, need to recreate in new order. + $this->cache->delete(self::CACHE_MENUSLIST); + + return true; + } + return false; + } + + /** + * Move the current menu downwards in the list of menus, fetch the next menu and move the menu to next menu position. + * + * @return bool true if the menu was moved successfully, false otherwise. + */ + public function move_downward() { + global $DB; + $currentposition = $this->menu->sortorder; + // Confirm is moving downward is possible. + if ($currentposition < self::get_menuscount()) { + $menus = $DB->get_records('theme_boost_union_menus', ['sortorder' => $currentposition + 1]); + if (empty($menus)) { + return false; + } + $nextmenu = current($menus); + // Update the menu position to down. + $DB->set_field('theme_boost_union_menus', 'sortorder', $nextmenu->sortorder, ['id' => $this->id]); + // Set the nextmenu position to up. + $DB->set_field('theme_boost_union_menus', 'sortorder', $currentposition, ['id' => $nextmenu->id]); + // Purge the menus list, need to recreate in new order. + $this->cache->delete(self::CACHE_MENUSLIST); + + return true; + } + return false; + } + + /** + * Duplicates the current menu, along with all its menu items. + * + * @return bool True on success, false on failure. + * @throws moodle_exception If the menu format is not correct. + */ + public function duplicate() { + + // Get list of items associated with its current menu. + $items = $this->get_menu_items(); + // Clone the menu. + $record = $this->menu; + // Remove the id to create the menu as new menu. + $record->id = 0; + + // Send the current menu without id to manage_instance will insert the menu as new menu. + $duplicateid = self::manage_instance($record); + + if (!empty($items)) { + // Duplicate the items related to menu. + foreach ($items as $item) { + $item = \theme_boost_union\smartmenu_item::instance($item->id)->item; + $item->id = 0; + // New menu id as menu for duplicate item. + $item->menu = $duplicateid; + // Create the item. + \theme_boost_union\smartmenu_item::manage_instance($item); + } + } + // Success message for duplicated menu. + \core\notification::success(get_string('smartmenusmenuduplicated', 'theme_boost_union')); + + // New menu added, Recreate the menuslist in cache. + $this->cache->delete(self::CACHE_MENUSLIST); + + return true; + } + + /** + * Updates the "visible" field of the current menu and deletes it from the cache. + * + * @param bool $visible The new value for the "visible" field. + * @return bool True if the update was successful, false otherwise. + */ + public function update_visible(bool $visible) { + // Delete the current menu from cache. + $this->cache->delete_menu($this->id); + // Purge the menus list from cache, recreates it on next page load. + $this->cache->delete(self::CACHE_MENUSLIST); + + return $this->update_field('visible', $visible, ['id' => $this->id]); + } + + /** + * Updates a field of the current menu with the given key and value. + * + * @param string $key The key of the field to update. + * @param mixed $value The new value of the field. + * @return bool|int Returns true on success, or false on failure. it also deletes the current menu from cache. + */ + public function update_field($key, $value) { + global $DB; + + $result = $DB->set_field('theme_boost_union_menus', $key, $value, ['id' => $this->id]); + + // Delete the current menu from cache. + $this->cache->delete_menu($this->id); + + return $result; + } + + /** + * Get the HTML class for the card form size. + * + * @return string HTML class for the card form size. + */ + protected function get_cardform() { + + $options = [ + self::SQUARE => 'square', self::PORTRAIT => 'portrait', self::LANDSCAPE => 'landscape', self::FULLWIDTH => 'fullwidth' + ]; + + return isset($options[$this->menu->cardform]) ? 'card-form-'.$options[$this->menu->cardform] : ''; + } + + /** + * Get the HTML class for the card size. + * + * @return string HTML class for the card size. + */ + protected function get_cardsize() { + + $options = [ + self::TINY => 'tiny', self::SMALL => 'small', self::MEDIUM => 'medium', self::LARGE => 'large' + ]; + + return isset($options[$this->menu->cardsize]) ? 'card-size-' . $options[$this->menu->cardsize] : ''; + } + + /** + * Get the HTML class for the card overflow behaviour. + * + * @return string HTML class for the card overflow behaviour. + */ + public function get_cardwrap() { + + $options = [ + self::WRAP => 'wrap', self::NOWRAP => 'no-wrap' + ]; + + return isset($options[$this->menu->overflowbehavior]) ? 'card-overflow-' . $options[$this->menu->overflowbehavior] : ''; + } + + + /** + * Fetch the list of menuitems assosciated with current menu. + * + * @return array|false List of menu items or false if no items found. + */ + public function get_menu_items() { + global $DB; + + $sql = "SELECT mi.* FROM {theme_boost_union_menuitems} mi + LEFT JOIN {theme_boost_union_menus} mn ON mn.id = mi.menu + WHERE mi.menu=:id + ORDER BY mi.sortorder ASC"; + + $params = [ + 'id' => $this->menu->id + ]; + $items = $DB->get_records_sql($sql, $params); + + return $items; + } + + /** + * This method is responsible for building the menu and its associated menu items. + * It first checks whether the menu is visible or not. If the menu is not visible, it returns false. + * + * Next, the method checks the cache for the menu and its menu items. If the cache has the data, it returns the cached data. + * Otherwise, it builds the menu and its items from scratch. + * + * The method build node from menu, These node array include the menu's classes, title, URL, text, and key. + * It also sets up some additional properties like itemtype, submenuid, card, forceintomoremenu, + * and haschildren based on the menu's properties. + * + * Then fetches the menu items for the menu and builds them one by one. + * It checks the type of the item, whether it is static or dynamic, and processes it accordingly. + * If the menu has any child items, the method sets up a children array for the menu node and adds the child items to it. + * It then sets the haschildren property to true. + * + * Finally, the processed menu node and its child items are stored in the cache, and the method returns the node. + * + * @param bool $resetcache True means remove the cache and build. Useful to session based menus and items purge. + * @return false|object Returns false if the menu is not visible or a menu object otherwise. + */ + public function build($resetcache=false) { + global $OUTPUT, $USER; + static $itemcache; + + if (empty($itemcache)) { + $itemcache = cache::make('theme_boost_union', 'smartmenu_items'); + } + // Make cachekey using combine the menu and current user. + $cachekey = "{$this->menu->id}_u_{$USER->id}"; + + // Purge the cached menus data if the menu date restrictions are reached or passed. + smartmenu_helper::purge_cache_date_reached($this->cache, $this->menu, 'menulastcheckdate'); + + // Get the menu and its menu items from cache. + $menuitems = []; + $nodes = $this->cache->get($cachekey); + if (!empty($nodes)) { + // List of menu items added to this menu. + $menuitems = $nodes->menuitems ?? []; + + } else { + // Set flag to store the menu data to cache. + $storecache = true; + + if (!$this->is_visible()) { + return false; + } + + $this->menu->classes[] = $this->get_cardform(); // Html class for the card form size, Potrait, Square, landscape. + $this->menu->classes[] = $this->get_cardsize(); // HTML class for the card Size, tiny, small, medium, large. + $this->menu->classes[] = $this->get_cardwrap(); // HtML class for the card overflow behaviour. + $this->menu->classes[] = $this->menu->cssclass;// Custom class selector for menu. + $this->menu->classes[] = ($this->menu->moremenubehavior == self::MOREMENU_OUTSIDE) ? "force-menu-out" : ''; + + // Card type menus doesn't supports inline menus. + // Mode is submenu or not set anything then create the menuitems as submenu. + // Otherwise add the menu items directoly as menu. + if ($this->menu->mode != self::MODE_INLINE || $this->menu->type == self::TYPE_CARD) { + + $nodes = (object) [ + 'menudata' => $this->menu, + 'title' => $this->menu->title, + 'url' => null, + 'text' => $this->menu->title, + 'key' => $this->menu->id, + 'submenulink' => 1, + 'itemtype' => 'submenu-link', // Used in user menus, to identify the menu type, "link" for submmenus. + 'submenuid' => uniqid(), // Menu has user menu location, then the submenu id is manatory for submenus. + 'card' => ($this->menu->type == self::TYPE_CARD) ? true : false, + 'forceintomoremenu' => ($this->menu->moremenubehavior == self::MOREMENU_INTO) ? true : false, + 'haschildren' => 0, + 'sort' => uniqid() // Support third level menu. + ]; + + // Add the description data to nodes. Inline mode menus not supports the menu. + if ($this->menu->showdesc != self::DESC_NEVER) { + $description = format_text($this->menu->description['text'], FORMAT_HTML); + $nodes->helptext = $description; + $nodes->abovehelptext = ($this->menu->showdesc == self::DESC_ABOVE) ? true : false; + $nodes->belowhelptext = ($this->menu->showdesc == self::DESC_BELOW) ? true : false; + // Add selector class in dropdown element for style. + $this->menu->classes[] = ($nodes->abovehelptext) ? 'dropdown-description-above' : ''; + $this->menu->classes[] = ($nodes->belowhelptext) ? 'dropdown-description-below' : ''; + + // Show the description as helpicon. + if ($this->menu->showdesc == self::DESC_HELP) { + $alt = get_string('description'); + $data = [ + 'text' => $description, + 'alt' => $alt, + 'icon' => (new \pix_icon('help', $alt, 'core', ['class' => 'iconhelp']))->export_for_template($OUTPUT), + 'ltr' => !right_to_left() + ]; + $nodes->helpicon = $OUTPUT->render_from_template('core/help_icon', $data); + } + + } + // Menu is set to inline, items classes are loadded in this variable menuclasses in template. + $nodes->menuclasses = $this->menu->classes; // Menus classes. + } + } + // Menus not exists in cache, then build the menu and menu items. + // Get list of its items. + $menuitems = $menuitems ?: $this->get_menu_items(); + + if (!empty($menuitems)) { + + $builditems = []; + foreach ($menuitems as $item) { + // Need to purge the items for user, remove the cache before build. + if ($resetcache) { + // Purge the items cache for this user. + $cachekey = "{$item->id}_u_{$USER->id}"; + $itemcache->delete($cachekey); + } + // Build the item based on restrict rules and its type like static, dynamic. + $item = \theme_boost_union\smartmenu_item::instance($item, $this->menu)->build(); + // Merge the dynamic course items as single item. + $builditems = (!empty($item)) ? array_merge($builditems, $item) : $builditems; + } + + if (isset($nodes) && !empty($nodes)) { + // Setup the childrens to parent menu node. + $nodes->haschildren = (count($builditems) > 0) ? true : false; + $nodes->children = $builditems; + } else { + // If menu is inline mode, then it items are displayed directly in menus. + // Set the menuitems as separate menu node in cache. + // Remove dividers from inline menus. + $builditems = array_filter($builditems, function($item) { + // Remove the item is divider. + return !isset($item['divider']) || !$item['divider']; + }); + + array_walk($builditems, function(&$item) { + // Make the dynamic courses as top menu for user menus dropdown. if menu mode is inline. + if ($item['haschildren']) { + // Below elements are used to separate the submenus and links for usermenu. + $item['itemtype'] = 'submenu-link'; + $item['submenulink'] = 1; + $item['link'] = 0; + $item['submenuid'] = uniqid(); + } + }); + + $nodes = $builditems; + } + } + + // Set the processed menus node and its children item nodes in Cache. + if (isset($nodes) && isset($storecache)) { + $nodescache = clone (object) $nodes; + // Remove the children data from cache before store. + unset($nodescache->children); + $nodescache->menuitems = $menuitems; + $this->cache->set($cachekey, $nodescache); + } + // Remove the menu items list from nodes. it doesn't need to build the smartmenus. + if (isset($nodes->menuitems)) { + // Remove the menu items list from nodes, it doesn't need anymore. + unset($nodes->menuitems); + } + + return $nodes ?? false; + } + + /** + * Retrieves the list of menus that are assigned to the specified location. + * + * @param string $location The location to retrieve menus for. + * @param array $menus The list of all available menus. + * @return array An array of menus that are assigned to the specified location. + */ + public static function get_menus_forlocation($location, $menus) { + + if (empty($menus) || $location == '') { + return []; + } + // Get the menu location from topmenus list. + // Locations from itemdata is not accurate, to fix this need to remove the all of items cache for the updated menu. + // Instead of delete item caches, get locations from cached menus list. + $topmenus = smartmenu_helper::get_menu_cache()->get(self::CACHE_MENUSLIST); + + $menulocation = []; + foreach ($menus as $menu) { + + $menu = (object) $menu; + + if (isset($menu->menudata->location)) { + $menulocation = $menu->menudata->location; + } else if (isset($menu->itemdata->menu) && isset($topmenus[$menu->itemdata->menu])) { // Inline menus. + $menulocation = json_decode($topmenus[$menu->itemdata->menu]->location); + } + + if (!isset($menulocation) || empty($menulocation)) { + continue; + } + // The menu contians the specified location. then store the menu for this location. + if (in_array($location, $menulocation)) { + $result[] = $menu; + $menulocation = []; // Reset the menu location for verify next menu. + } + } + + return $result ?? []; + } + + /** + * Fetches a menu record from the database by ID and returns it as an object with convert the json values to array. + * + * @param int $id The ID of the menu to fetch. + * @return stdClass|false Returns an object if the menu is found, false otherwise. + */ + public static function get_menu($id) { + global $DB; + + // Verfiy and Fetch menu record from DB. + if ($record = $DB->get_record('theme_boost_union_menus', ['id' => $id])) { + + // Decode the multiple option select elements values to array. + $record = self::update_menu_valuesformat($record); + + return $record; + } else { + throw new moodle_exception('error:smartmenusmenunotfound', 'theme_boost_union'); + } + return false; + } + + /** + * Update the menu values from json to array. + * + * @param stdclass $menu Record data of the menu. + * @return stdclass Converted menu data. + */ + public static function update_menu_valuesformat($menu) { + + // Verify the format is already updated. + if (!is_scalar($menu->location)) { + return $menu; + } + + $menu->description = [ + 'text' => $menu->description, + 'format' => $menu->description_format + ]; + // Decode the multiple option select elements values to array. + $menu->location = json_decode($menu->location); + $menu->roles = json_decode($menu->roles); + $menu->cohorts = json_decode($menu->cohorts); + $menu->languages = json_decode($menu->languages); + $menu->mode = $menu->mode ?? self::MODE_SUBMENU; // Submenu is default menu mode. + + return $menu; + } + + /** + * Returns the count of all menus in the database. + * + * @return int The number of menus. + */ + public static function get_menuscount() { + global $DB; + return $DB->count_records('theme_boost_union_menus', []); + } + + /** + * Get the last menu from records based on the sordorder. + * + * @return stdClass An object representing the last menu, or an empty object if no menus exist. + */ + public static function get_lastmenu() { + global $DB; + $records = $DB->get_records_sql('SELECT * FROM {theme_boost_union_menus} ORDER BY sortorder DESC', [], 0, 1); + return (object) (!empty($records) ? current($records) : []); + } + + /** + * Get all available smart menu locations. + * Menu will be displayed in Main menu, Menu bar (above the main menu), User menu, Bottom menu. + * + * @return array An array with all available locations. + */ + public static function get_locations() { + // List of locations where same menu can be used in multiple places. + $locations = array( + self::LOCATION_MAIN => get_string('smartmenuslocationmain', 'theme_boost_union'), + self::LOCATION_MENU => get_string('smartmenuslocationmenu', 'theme_boost_union'), + self::LOCATION_USER => get_string('smartmenuslocationuser', 'theme_boost_union'), + self::LOCATION_BOTTOM => get_string('smartmenuslocationbottom', 'theme_boost_union') + ); + + return $locations; + } + + /** + * Fetch the user readable name for a specific location of the current menu. + * + * @param int $location The type ID. + * @return string|false The localized name of the location, or false if the type is invalid. + */ + public static function get_location($location) { + $locations = self::get_locations(); + return $locations[$location] ?? false; + } + + /** + * Get all available smart menu types. Either card or list. + * + * @return array An array with all available types, where key is the type id and value is the localized type name. + */ + public static function get_types() { + $types = array( + self::TYPE_LIST => get_string('smartmenustypeslist', 'theme_boost_union'), + self::TYPE_CARD => get_string('smartmenustypescard', 'theme_boost_union') + ); + + return $types; + } + + /** + * Get the localized name for a specific type. + * + * @param int $type The type ID. + * @return string|false The localized name of the type, or false if the type is invalid. + */ + public static function get_type($type) { + $types = self::get_types(); + return $types[$type] ?? false; + } + + /** + * Insert or update the menu instance to DB. Convert the multiple options select elements to json. + * setup menu order after insert. + * + * Delete the current menu cache after updated the menu. + * + * @param stdclass $formdata + * @return int The menu ID. + */ + public static function manage_instance($formdata) { + global $DB; + + $record = $formdata; + + $record->description_format = $formdata->description['format']; + $record->description = $formdata->description['text']; + + // Encode the multiple value elements into json to store. + $record->location = json_encode($formdata->location); + $record->roles = json_encode($formdata->roles); + $record->cohorts = json_encode($formdata->cohorts); + $record->languages = json_encode($formdata->languages); + + $cache = cache::make('theme_boost_union', 'smartmenus'); + + $transaction = $DB->start_delegated_transaction(); + + if (isset($formdata->id) && $DB->record_exists('theme_boost_union_menus', ['id' => $formdata->id])) { + $menuid = $formdata->id; + + $DB->update_record('theme_boost_union_menus', $record); + // Clear the current menu caches. Update may cause changes in the menus list. + // Delete the menu cache for all users. + $cache->delete_menu($menuid); + // Menu updated, recreate the menuslist. + $cache->delete(self::CACHE_MENUSLIST); + // Show the edited success notification. + \core\notification::success(get_string('smartmenusupdatesuccess', 'theme_boost_union')); + } else { + // Setup the menu order. + $lastmenu = self::get_lastmenu(); + $record->sortorder = isset($lastmenu->sortorder) ? $lastmenu->sortorder + 1 : 1; + $menuid = $DB->insert_record('theme_boost_union_menus', $record); + // New menu added, recreate the menuslist. + $cache->delete(self::CACHE_MENUSLIST); + // Show the menu inserted success notification. + \core\notification::success(get_string('smartmenusinsertsuccess', 'theme_boost_union')); + } + + // Allow to update the DB changes to Database. + $transaction->allow_commit(); + + return $menuid; + } + + /** + * Retrieve all top level menus. + * + * @return array|false An array of top level menus or false if no menu found. + */ + public static function get_menus() { + global $DB; + + $topmenus = $DB->get_records('theme_boost_union_menus', [], 'sortorder ASC'); + return $topmenus; + } + + /** + * Initialize the build of smart menus, Fetch the list of menus and init the build for each menu. + * + * @return array An array of SmartMenu nodes. + */ + public static function build_smartmenu() { + global $USER; + + $nodes = []; + + $cache = cache::make('theme_boost_union', 'smartmenus'); + // Fetch the list of menus from cache. + $topmenus = $cache->get(self::CACHE_MENUSLIST); + // Get top level menus, store the menus to cache. + if (empty($topmenus)) { + $topmenus = self::get_menus(); + $cache->set(self::CACHE_MENUSLIST, $topmenus); + } + + if (empty($topmenus)) { + // Still menus are not created. + return false; + } + + // Test the flag to purge the cache is set for this user. + $removecache = (get_user_preferences('theme_boost_union_menu_purgesessioncache', false) == true); + + foreach ($topmenus as $menu) { + // Need to purge the menus for user, remove the cache before build. + if ($removecache) { + // Purge the menu cache for this user. + $cachekey = "{$menu->id}_u_{$USER->id}"; + $cache->delete($cachekey); + } + + if ($node = self::instance($menu)->build($removecache)) { + if (isset($node->menudata)) { + $nodes[] = $node; + } else { + $nodes = array_merge($nodes, array_values((array) $node)); + } + } + } + + // Menus are purged in the build method when needed, then clear the user preference of purge cache. + \smartmenu_helper::clear_user_cachepreferencemenu(); + + return $nodes; + } +} diff --git a/classes/smartmenu_item.php b/classes/smartmenu_item.php new file mode 100644 index 00000000000..63bf282d2bf --- /dev/null +++ b/classes/smartmenu_item.php @@ -0,0 +1,1339 @@ +. + +/** + * Item controller for managing menu items. Build the item as node to attach as submenu. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union; + +defined('MOODLE_INTERNAL') || die(); + +use context_system; +use html_writer; +use smartmenu_helper; +use stdClass; +use cache; +use \core_course\external\course_summary_exporter; + +require_once($CFG->dirroot.'/theme/boost_union/smartmenus/menulib.php'); + +/** + * The item controller handles actions related to managing items. + * + * This controller provides methods for listing available menus, creating new items, + * updating existing items, deleting items, and sorting the order of items. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class smartmenu_item { + + /** + * Representing a heading type for a menu item. + * @var int + */ + const TYPEHEADING = 0; + + /** + * Represents the type of a static element. + * @var int + */ + const TYPESTATIC = 1; + + /** + * Represents the type of a dynamic element. + * @var int + */ + const TYPEDYNAMIC = 2; + + /** + * Represents the completion status of an item where the status is 'enrolled'. + * @var int + */ + const COMPLETION_ENROLLED = 1; + + /** + * Represents the completion status of an item where the status is 'inprogress'. + * @var int + */ + const COMPLETION_INPROGRESS = 2; + + /** + * Represents the completion status of an item where the status is 'completed'. + * @var int + */ + const COMPLETION_COMPLETED = 3; + + /** + * Represents the range of an items where the range is past. + * @var int + */ + const RANGE_PAST = 1; + + /** + * Represents the range of an items where the range is preset. + * @var int + */ + const RANGE_PRESENT = 2; + + /** + * Represents the range of an items where the range is future. + * @var int + */ + const RANGE_FUTURE = 3; + + /** + * Hide the item title an all viewport. + * @var int + */ + const DISPLAY_HIDETITLE = 0; + + /** + * Hide the item title in mobile viewport. + * @var int + */ + const DISPLAY_HIDETITLEMOBILE = 1; + + /** + * Display the title with icon. + * @var int + */ + const DISPLAY_SHOWTITLEICON = 2; + + /** + * Open the item link in same tab. + * @var int + */ + const TARGET_SAME = 0; + + /** + * Open the item link in new tab on browser. + * @var int + */ + const TARGET_NEW = 1; + + /** + * Show the item title in below of the card image. + * @var int + */ + const POSITION_BELOW = 0; + + /** + * Show the item title in top of the card overlay. + * @var int + */ + const POSITION_OVERLAYTOP = 1; + + /** + * Show the item title in bottom of the card overlay. + * @var int + */ + const POSITION_OVERLAYBOTTOM = 2; + + /** + * Display the dynamic items as second level submenu of the menu. + * @var int + */ + const MODE_SUBMENU = 2; + + /** + * Display the dynamic items as each item of the menu. + * @var int + */ + const MODE_INLINE = 1; + + /** + * Opacity for the Card background layout in overlay text positions. + * @var int + */ + const BACKGROUND_OPACITY = 5; + + /** + * Display the course shortname as title in menu for dynamic menu item. + * + * @var int + */ + const FIELD_SHORTNAME = 1; + + /** + * Display the course fullname as title in menu for dynamic menu item. + * @var int + */ + const FIELD_FULLNAME = 0; + + /** + * The ID of the menu item. + * @var int + */ + public $id; + + /** + * The record of the current item. + * @var \stdclass + */ + public $item; + + /** + * The menu to which this menu item belongs. + * @var \stdclass + */ + public $menu; + + /** + * The helper object for this menu item. + * @var \smartmenu_helper + */ + public $helper; + + /** + * The cache object for this menu item. + * @var cache + */ + public $cache; + + /** + * The menu cache object for this menu item. + * @var object + */ + public $menucache; + + /** + * Create a new instance of this class. + * + * @param int|stdclass $item The ID of the item to retrieve or record data of item. + * @param stdclass|null $menu Data of the menu the item belongs to. + * @return smartmenu_item A new instance of this class. + */ + public static function instance($item, $menu=null) { + return new self($item, $menu); + } + + /** + * Menu item constructor, Retrive the item data, Create smartmenu_helper for this item, + * Creates the cache instance for item and its menu. + * + * @param int|stdclass $item Record or id of the menu. + * @param stdclass|null $menu Menu data belongs to this item, it fetch the menus data if empty. + */ + public function __construct($item, $menu=null) { + + if (is_scalar($item)) { + $item = $this->get_item($item); + } + + // Item ID. + $this->id = $item->id; + + // Format the item values. + $this->item = $this->update_item_valuesformat($item); + + // Verify the item data is object or array, otherwise throws an exeception. + if (!is_array($item) && !is_object($item)) { + throw new \moodle_exception('error:smartmenusmenuitemnotfound', 'theme_boost_union'); + } + + // Menu data, the current item belongs to. + $this->menu = $menu ?: smartmenu::get_menu($this->item->menu); + + // Smartmenu helper to verify the access rules. + $this->helper = new smartmenu_helper($this->item); + + // Cache instance for the items. + $this->cache = cache::make('theme_boost_union', 'smartmenu_items'); + + // Menus cache instance. + // Purge the menu related to the item, when the item is updated, created and sorted. + $this->menucache = cache::make('theme_boost_union', 'smartmenus'); + } + + /** + * Deletes the cache for the current item and the cached data of the current item's menu. + * + * @return void + */ + public function delete_cache() { + // Remove cache of current item for all users. + $this->cache->delete_menu($this->item->id); + // Delete the cached data of current items menu. + $this->menucache->delete_menu($this->item->menu); + } + + /** + * Fetches a item record from the database by ID and returns it as an object with convert the json values to array. + * + * @param int $itemid Id of the item. + * @return \stdclass Menu record if found or false. + * @throws \moodle_exception When menu is not found. + */ + public function get_item($itemid=null) { + global $DB; + + // Verfiy and Fetch menu record from DB. + if ($record = $DB->get_record('theme_boost_union_menuitems', ['id' => $itemid ?: $this->id ])) { + + // Decode the multiple option select elements values to array. + return $this->update_item_valuesformat($record); + + } else { + throw new \moodle_exception('error:smartmenusmenuitemnotfound', 'theme_boost_union'); + } + + return false; + } + + /** + * Updated the items values format, Some the values like category and other restriction options are stored as json. + * Convert the json values to array. + * + * @param stdclass $itemdata + * @return stdclass Items data in updated format. + */ + public function update_item_valuesformat($itemdata) { + // Verify the format is already updated. + if (!is_scalar($itemdata->category)) { + return $itemdata; + } + $itemdata->category = json_decode($itemdata->category) ?: []; + $itemdata->enrolmentrole = json_decode($itemdata->enrolmentrole) ?: []; + $itemdata->completionstatus = json_decode($itemdata->completionstatus) ?: []; + $itemdata->daterange = json_decode($itemdata->daterange) ?: []; + + // Restrict access rules. + $itemdata->roles = json_decode($itemdata->roles) ?: []; + $itemdata->cohorts = json_decode($itemdata->cohorts) ?: []; + $itemdata->languages = json_decode($itemdata->languages) ?: []; + + // Seperate the customfields. + $customfields = json_decode($itemdata->customfields) ?: []; + foreach ($customfields as $field => $value) { + $itemdata->{'customfield_'.$field} = $value; + } + + return $itemdata; + } + + /** + * Delete the current item's menu from the database. + * + * @return bool True if the deletion is successful, false otherwise. + */ + public function delete_menuitem() { + global $DB; + + if ($DB->delete_records('theme_boost_union_menuitems', ['id' => $this->id])) { + // Reorder the items. + $this->reorder_items(); + // Delete the cache. + $this->delete_cache(); + + return true; + } + return false; + } + + /** + * Move the menu items order to upwards. + * Find the previous item and set the previous item order as order for current item. + * + * @return bool true if the item was moved successfully, false otherwise + */ + public function move_upward() { + global $DB; + + $currentposition = $this->item->sortorder; + // Confirm is moving upward is possible. + if ($currentposition > 1) { + // Find the previous item. + $sql = 'SELECT * FROM {theme_boost_union_menuitems} WHERE sortorder < :pos AND menu = :menu ORDER BY sortorder ASC'; + $previtems = $DB->get_records_sql($sql, [ + 'pos' => $currentposition, + 'menu' => $this->item->menu + ]); + + if (empty($previtems)) { + return false; + } + + $previtem = end($previtems); + + // Update the menu position to upwards. + $DB->set_field('theme_boost_union_menuitems', 'sortorder', $previtem->sortorder, [ + 'id' => $this->id, + 'menu' => $this->item->menu + ]); + // Set the prevmenu position to down. + $DB->set_field('theme_boost_union_menuitems', 'sortorder', $currentposition, [ + 'id' => $previtem->id, + 'menu' => $this->item->menu + ]); + + // Difference between two items is more than 1 then reorder the items. + if (($currentposition - $previtem->sortorder) > 1) { + $this->reorder_items(); + } + // Delete the menu cache, recreate the menu with updated items order. + $this->delete_cache(); + + return true; + } + return false; + } + + /** + * Move the menu items order to downwards. + * Find the next item and set the next item order as order for current item. + * + * @return bool true if the item was moved successfully, false otherwise + */ + public function move_downward() { + global $DB; + $currentposition = $this->item->sortorder; + // Find the previous item. + $sql = 'SELECT * FROM {theme_boost_union_menuitems} WHERE sortorder > :pos AND menu = :menu ORDER BY sortorder ASC'; + $nextitems = $DB->get_records_sql($sql, [ + 'pos' => $currentposition, + 'menu' => $this->item->menu + ]); + + if (empty($nextitems)) { + return false; + } + + $nextitem = current($nextitems); // First item in the list. + // Update the menu position to down. + $DB->set_field('theme_boost_union_menuitems', 'sortorder', $nextitem->sortorder, [ + 'id' => $this->id, + 'menu' => $this->item->menu + ]); + // Set the prevmenu position to up. + $DB->set_field('theme_boost_union_menuitems', 'sortorder', $currentposition, [ + 'id' => $nextitem->id, + 'menu' => $this->item->menu + ]); + + // Difference between two items is more than 1 then reorder the items. + if (($nextitem->sortorder - $currentposition) > 1) { + $this->reorder_items(); + } + + // Delete the menu cache, recreate the menu with updated items order. + $this->delete_cache(); + + return true; + + } + + /** + * Reorder the items sort order, if the last menu order is not same as count of records. + * We should update the order of items. + * + * @return void + */ + public function reorder_items() { + global $DB; + + $sql = 'SELECT * FROM {theme_boost_union_menuitems} WHERE menu=:menu ORDER BY sortorder ASC'; + $records = $DB->get_records_sql($sql, ['menu' => $this->item->menu]); + + if (empty($records)) { + return false; + } + + // The last item order is not same as count of records. + if (end($records)->sortorder != count($records)) { + // Update the sortorder from lower order. + $i = 1; + foreach ($records as $itemid => $item) { + $DB->set_field('theme_boost_union_menuitems', 'sortorder', $i, ['id' => $item->id]); + $i++; + } + } + } + + /** + * Get the last item from the current menu`s list of items. + * + * @param int $menuid + * @return stdclass An object representing the last item, or an empty object if no menus exist. + */ + public static function get_lastitem($menuid) { + global $DB; + + $sql = 'SELECT * FROM {theme_boost_union_menuitems} WHERE menu=:menu ORDER BY sortorder DESC'; + $records = $DB->get_records_sql($sql, ['menu' => $menuid], 0, 1); + return (object) (!empty($records) ? current($records) : []); + } + /** + * Duplicate the current item, clone the current item object and remove the id from item then + * send to manage_instance method to create as new item. + * + * @return void + */ + public function duplicate() { + $record = $this->item; + $record->id = 0; + // Create instance. + if (self::manage_instance($record)) { + \core\notification::success(get_string('smartmenusmenuitemduplicated', 'theme_boost_union')); + } + } + + /** + * Updates a field of the current item with the given key and value. + * + * @param string $key The key of the field to update. + * @param mixed $value The new value of the field. + * @return bool|int Returns true on success, or false on failure. it also deletes the current menu from cache. + */ + public function update_field($key, $value) { + global $DB; + // Delete the cached current item and menu of this item. + $this->delete_cache(); + + return $DB->set_field('theme_boost_union_menuitems', $key, $value, ['id' => $this->id]); + } + + /** + * Returns the URL of the image associated with the given item ID, + * or a placeholder image URL if no image is associated with the item. + * + * @param int $itemid The ID of the item. + * @return string The URL of the image associated with the item. + */ + public function get_itemimage($itemid) { + global $OUTPUT, $SITE; + + $fs = get_file_storage(); + $contextid = \context_system::instance()->id; + $files = $fs->get_area_files($contextid, 'theme_boost_union', 'smartmenus_itemimage', $itemid, '', false); + if (!empty($files)) { + // Get the first file. + $file = reset($files); + + $url = \moodle_url::make_pluginfile_url( + $file->get_contextid(), + $file->get_component(), + $file->get_filearea(), + $file->get_itemid(), + $file->get_filepath(), + $file->get_filename(), + false + ); + } + $placeholderimage = $OUTPUT->get_generated_image_for_id($SITE->id); + return $url ?? $placeholderimage; + } + + /** + * Returns the URL of the image associated with the given course ID, + * or a placeholder image URL if no image is associated with the course. + * + * @param stdclass $course The course record + * @return string The URL of the image associated with the item. + */ + public function get_course_image($course) { + + $courseimage = course_summary_exporter::get_course_image($course); + if (!$courseimage && $this->menu->type == smartmenu::TYPE_CARD) { + // Course image not available, then check current parent item image, + // If found then use the image otherwise generate the Custom image. + $courseimage = $this->get_itemimage($this->item->id); + } + return $courseimage; + } + + /** + * Generate a node data for a heading item. + * + * @return array The node data. + */ + protected function generate_heading() { + + return $this->generate_node_data( + $this->item->title, // Title. + '#', // URL. + null, // Default key. + $this->item->tooltip, // Tooltip. + 'heading' + ); + } + + /** + * Generate the item as static menu item, Send the custom URL to moodle_url to make this work with relative URL. + * + * @return string + */ + protected function generate_static_item() { + + $staticurl = new \moodle_url($this->item->url); + + return $this->generate_node_data( + $this->item->title, // Title. + $staticurl, // URL. + null, // Default key. + $this->item->tooltip, // Tooltip. + ); + } + + /** + * Generate the dynamic courses based on the conditions of categories, enrollmentrole, + * daterange (Past, Present, Future), and customfields. + * + * Generate the fetched course records as each nodes. + * + * @return stdclass + */ + protected function generate_dynamic_item() { + global $DB; + + // Prevent the item if the item mode is submenu and the menu is card. + if ($this->item->mode == self::MODE_SUBMENU && $this->menu->type == smartmenu::TYPE_CARD) { + return []; + } + $query = (object) [ + 'select' => ['c.*'], + 'join' => [], + 'where' => ["c.visible > 0"], + 'params' => [], + ]; + + // Courses from categories. + $this->get_categories_sql($query); + + // Enrolment role. + $this->get_enrollmentrole_sql($query); + + // Completion status based condition query. + $this->get_completionstatus_sql($query); + + // Daterange based courses filter. + $this->get_daterange_sql($query); + + // Custom field based courses filter. + $this->get_customfield_sql($query); + + // Build the queries. + $select = implode(',', array_filter($query->select)); + $join = implode('', array_filter($query->join)); + $where = implode(' AND ', array_filter($query->where)); + $params = array_merge($query->params); + + $sql = " SELECT $select FROM {course} c $join"; + $sql .= $where ? " WHERE $where " : ''; + + // Sort the courses in ascending order by its display field. + $sql .= ($this->item->displayfield == self::FIELD_SHORTNAME) ? " ORDER BY c.shortname ASC " : " ORDER BY c.fullname ASC "; + + // Fetch the course records based on the sql. + $records = $DB->get_records_sql($sql, $params); + + if (empty($records)) { + return []; + } + + $items = []; + // Build the items data into nodes. + foreach ($records as $record) { + $url = new \moodle_url('/course/view.php', ['id' => $record->id]); + $rkey = 'item-'.$this->item->id.'-dynamic-'.$record->id; + // Get the course image from overview files. + $itemimage = $this->get_course_image($record); + // Generate the navigation node for this course and add the node to items list. + $coursename = ($this->item->displayfield == self::FIELD_SHORTNAME) ? $record->shortname : $record->fullname; + // Short the course text name. used custom end (2) dots instead of three dots to display more words from coursenames. + $coursename = ($this->item->textcount) ? $this->shorten_words($coursename, $this->item->textcount) : $coursename; + + $items[] = $this->generate_node_data($coursename, $url, $rkey, null, 'link', false, [], $itemimage); + } + + // Submenu only contains the title as separate node. + if ($this->item->mode == self::MODE_SUBMENU) { + $haschildren = (count($items) > 0 ) ? true : false; + $submenu[] = $this->generate_node_data( + $this->item->title, // Title. + '', // URL. + null, // Default key. + $this->item->tooltip, // Tooltip. + 'submenu', // Item type. + $haschildren, // Has children. + $items // Children. + ); + return $submenu; + } + return $items; + } + + /** + * Given some text and an ideal length, this function truncates the text based on words count. + * + * @param string $text text to be shortened + * @param int $count Length of the words + * @return string $text shortened string + */ + protected function shorten_words($text, $count) { + if (str_word_count($text, 0) > $count) { + $words = str_word_count($text, 2); // Find the position of last word. + $positions = array_keys($words); + $text = trim(substr($text, 0, $positions[$count])).'..'; + } + return $text; + } + + + /** + * Adds category filter to the SQL query. + * + * @param stdclass $query the database query to modify + * @return bool true if categories filter was applied, false otherwise + */ + protected function get_categories_sql(&$query) { + global $DB; + + if (empty($this->item->category)) { + return false; + } + + list($insql, $inparams) = $DB->get_in_or_equal($this->item->category, SQL_PARAMS_NAMED, 'cg'); + $query->where[] = "c.category $insql"; + $query->params += $inparams; + } + + /** + * Prepare the SQL query to get the enrolments based role. + * Get user role assignments in the context, roleid should same as selected. + * + * @param stdclass $query the database query to modify + * @return void + */ + protected function get_enrollmentrole_sql(&$query) { + global $DB, $USER; + + if (empty($this->item->enrolmentrole)) { + return false; + } + + $roles = $this->item->enrolmentrole; + [$rsql, $rparams] = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'roles'); + + $query->where[] = "c.id IN (SELECT ctx.instanceid + FROM {role_assignments} ra + JOIN {context} ctx ON ctx.id = ra.contextid AND ctx.contextlevel = " . CONTEXT_COURSE . " + WHERE ra.userid = :ruserid AND ra.roleid $rsql)"; + + $query->params += $rparams + ['ruserid' => $USER->id]; + } + + /** + * Build condition query for course completion status, + * It uses course_completion conditions, it has conditions Enrolled, Inprogress, Completed. + * + * Enrolled means user should have active enrollment, not completed any course completion enabled activity modules. + * Inprogress means user should complete any of the activity enabled for course completion condition. + * Completed Means user should completed the course. + * + * Query find the progress by calculate the completions of activity which enabled for course completions. + * + * @param [type] $query + * @return void + */ + protected function get_completionstatus_sql(&$query) { + global $DB, $USER; + + if (empty($this->item->completionstatus)) { + return false; + } + + $status = $this->item->completionstatus; + // Convert the selected completion status to insql. + $list = []; + foreach ($status as $condition) { + switch($condition) { + case self::COMPLETION_INPROGRESS: + $list[] = 'inprogress'; + break; + case self::COMPLETION_COMPLETED: + $list[] = 'completed'; + break; + case self::COMPLETION_ENROLLED: + $list[] = 'enrolled'; + break; + } + } + + list($insql, $inparam) = $DB->get_in_or_equal($list, SQL_PARAMS_NAMED, 'csts'); + + $sql = "SELECT ue.courseid FROM ( + SELECT + CASE WHEN cc.timecompleted > 0 THEN 'completed' + WHEN cc.timestarted > 0 THEN 'inprogress' + ELSE 'enrolled' + END AS status, + e.courseid AS courseid + FROM {user_enrolments} ue + LEFT JOIN {enrol} e ON ue.enrolid = e.id + LEFT JOIN {course_completions} cc ON cc.course = e.courseid AND ue.userid = cc.userid + WHERE ue.userid = :fueuserid AND ue.status <= 0 + AND (ue.timestart = 0 OR ue.timestart <= :timestart) + AND (ue.timeend = 0 OR ue.timeend > :timeend) + ) ue WHERE ue.status $insql"; + + $query->where[] = " c.id IN ($sql) "; + $query->params += ['fueuserid' => $USER->id, 'timestart' => time(), 'timeend' => time()] + $inparam; + } + + /** + * Generates the SQL statement for the date range condition. Range is based on the course startdate and enddate. + * + * @param stdclass $query The database query object. + * @return bool Returns false if the item's date range is empty. + */ + protected function get_daterange_sql(&$query) { + global $DB, $USER; + + if (empty($this->item->daterange)) { + return false; + } + + $dates = $this->item->daterange; + + $sql = []; + $params = []; + foreach ($dates as $key => $date) { + switch ($date) { + case self::RANGE_PAST: + $sql[] = "c.enddate <> 0 AND c.enddate < :now_$key"; + $params += ['now_'.$key => time()]; + break; + case self::RANGE_PRESENT: + $sql[] = "(c.startdate < :startdate_$key AND ( c.enddate = 0 OR c.enddate > :enddate_$key) )"; + $params += ['enddate_'.$key => time(), 'startdate_'.$key => time()]; + break; + case self::RANGE_FUTURE: + $sql[] = "c.startdate > :now_$key"; + $params += ['now_'.$key => time()]; + break; + } + } + + $query->where[] = $sql ? '('.implode(' OR ', $sql).')' : ''; + $query->params += $params; + } + + /** + * Generates the SQL statement for the custom field condition. + * It creates a condition query to fetch the course which has the same value mentioned in the item customfield conditions. + * + * @param stdclass $query The database query object. + * @return bool Returns false if the item's date range is empty. + */ + protected function get_customfield_sql(&$query) { + global $DB; + + if (empty($this->item->customfields)) { + return false; + } + + $customfields = $this->item->customfields ? json_decode($this->item->customfields) : []; + + $i = 0; // Multiple fields unique id for values. + $params = []; + $sql = []; + + foreach ($customfields as $shortname => $value) { + // Filter the null, autocomplete fields dont displayed the empty value, user cannot able to remove the null fields. + // Therfore remove the 0 or empty values from condition values. + if (is_array($value)) { + $value = array_filter($value, function($v) { + return $v != 0; + }); + } + + if ($value == '' || $value === 0 || empty($value)) { + continue; + } + + // Select from multiple values for a custom field. + if (is_array($value)) { + list($insql, $inparams) = $DB->get_in_or_equal($value, SQL_PARAMS_NAMED, 'val_'.$i); + $where = "cd.value $insql"; + $params += $inparams; + } else { + $where = "cd.value=:value_$i"; + $params += ["value_$i" => $value]; + } + + $i++; + $sql[] = " + c.id IN ( + SELECT instanceid FROM {customfield_data} cd + JOIN {customfield_field} cf ON cd.fieldid = cf.id AND cf.shortname = :shortname_$i + WHERE $where + )"; + $params += ["shortname_$i" => $shortname]; + } + + $query->where[] = $sql ? implode(' AND ', $sql) : ''; + $query->params += $params; + } + + /** + * Defines a build method that generates the HTML markup for a menu item. + * + * First, it checks if the menu item is cached and returns it if found. + * If not, it verifies if the user has access to the menu item and checks for any access restrictions. + * + * It then adds custom CSS classes and hides the menu item based on specific viewport sizes. + * It also adds classes for the item title placement on the card. + * + * Next, it checks the type of the menu item and generates the array markup based on the type: + * If the type is static, it generates a static item using the generate_static_item method and returns it as an array. + * If the type is dynamic, it generates a dynamic item using the generate_dynamic_item method and returns it as an array. + * If the type is heading or not set, it generates a heading using the generate_heading method and returns it as an array. + * It then sets the item's classes and saves the items cache. + * Finally, it deletes the menu cache and returns the generated HTML markup as an array. + * + * @return false|array Returns false if the menu is not visible or a item array otherwise. + */ + public function build() { + global $USER; + + smartmenu_helper::purge_cache_date_reached($this->cache, $this->item, 'itemlastcheckdate'); + + $cachekey = "{$this->item->id}_u_{$USER->id}"; + if ($result = $this->cache->get($cachekey)) { + return $result; + } + + // Verify the restriction rules. + if (empty($this->item) || !$this->helper->verify_access_restrictions()) { + return false; + } + + // Add custom css class. + $class[] = $this->item->cssclass; + // Add classes for hide items in specific viewport. + $class[] = $this->item->desktop ? 'd-lg-none' : 'd-lg-inline-flex'; + $class[] = $this->item->tablet ? 'd-md-none' : 'd-md-inline-flex'; + $class[] = $this->item->mobile ? 'd-none' : 'd-inline-flex'; + + // Add classes for item title placement on card. + $class[] = $this->get_textposition_class(); + // Menu item class. + $types = [self::TYPESTATIC => 'static', self::TYPEDYNAMIC => 'dynamic', self::TYPEHEADING => 'heading']; + $class[] = 'menu-item-'.($types[$this->item->type] ?? ''); + // Add classes to item data. + $this->item->classes = $class; + // Load the location of menu, used to collect menus for locations in menu inline mode. + $this->item->location = $this->menu->location; + + // Convert the item background color hexcode into rgba with opacity. Used in the overlay style. + $this->convert_background_code(); + + switch ($this->item->type): + + case self::TYPESTATIC: + $static = $this->generate_static_item(); + $result = [$static]; // Return the result as recursive array for merge with dynamic items. + $type = 'static'; + break; + + case self::TYPEDYNAMIC: + $result = $this->generate_dynamic_item(); + $type = 'dynamic'; + break; + + case self::TYPEHEADING: + default: + $heading = $this->generate_heading(); + $result = [$heading]; // Return the result as recursive array useful to merge with dynamic items. + $type = 'heading'; + + endswitch; + + // Save the items cache. + $this->cache->set($cachekey, $result); + + return $result; + } + + /** + * Generate node data for dynamic menu item. + * + * Formats the title and adds an icon if it is specified in the menu item. + * It then creates an array containing all the necessary data for the node such as + * the item data, URL, key, text, title, whether or not it has children, item image, and item type. + * + * If the node has children, the function adds an array of child nodes to the data array. + * If the menu item's target is set to open in a new window, the function adds an 'attributes' element + * to the data array with the target set to '__blank'. + * + * Lastly, if the title is a series of '#' characters, the function adds a 'divider' element to the data array. + * + * @param string $title The title of the item. + * @param string $url The URL of the item. + * @param string|null $key The unique key of the item, defaults to 'item-' followed by the item ID. + * @param string|null $tooltip The tooltip text of the item. + * @param string $itemtype The type of the item, defaults to 'link'. + * @param int $haschildren Whether the item has children or not, defaults to 0. + * @param array $children An array of child nodes, defaults to an empty array. + * @param string $itemimage Card image url for item. + * + * @return array An associative array of node data for the item. + */ + public function generate_node_data($title, $url, $key=null, $tooltip=null, + $itemtype='link', $haschildren=0, $children=[], $itemimage='') { + + global $OUTPUT; + + $title = $titleorg = format_string($title); + // Icon not shown in moodle 4.x, added the icon with text. + if ($this->item->menuicon) { + $icon = explode(':', $this->item->menuicon); + $iconstr = isset($icon[1]) ? $icon[1] : 'moodle'; + $component = isset($icon[0]) ? $icon[0] : ''; + // Render the pix icon. + $icon = $OUTPUT->pix_icon($iconstr, $this->item->title, $component); + + switch ($this->item->display) { + + case self::DISPLAY_SHOWTITLEICON: + $title = $icon . $title; + break; + case self::DISPLAY_HIDETITLE: + $title = $icon; + break; + case self::DISPLAY_HIDETITLEMOBILE: + $title = $icon . html_writer::tag('label', $title, ['class' => 'd-none d-sm-inline-block']); + break; + } + } + + // Generate dynamic image for empty image cards. + if (empty($itemimage) && $this->menu->type == smartmenu::TYPE_CARD) { + $itemimage = $this->get_itemimage($this->item->id); + } + + $data = [ + 'itemdata' => $this->item, + 'menuclasses' => $this->item->classes, // If menu is inline, need to add the item custom class in dropdown. + 'location' => $this->menu->location, + 'url' => $url ?: 'javascript:void(0)', + 'key' => $key != null ? $key : 'item-'.$this->item->id, + 'text' => $title, + 'icontitle' => $title, // Title with icon. + 'title' => $titleorg, + 'tooltip' => $tooltip ? format_string($tooltip) : '', + 'haschildren' => $haschildren, + 'itemimage' => $itemimage, + 'itemtype' => 'link', + 'link' => 1, + 'sort' => uniqid() // Support third level menu. + ]; + + if ($haschildren && !empty($children)) { + $data['children'] = $children; + } + + if ($this->item->target == self::TARGET_NEW && $url != '') { + $data['attributes'] = array([ + 'name' => 'target', + 'value' => '__blank' + ]); + } + + if (preg_match("/^#+$/", format_string($title))) { + // In main menu divider is separate property. + // For lang menu divider is mentioned in itemtype. + // Updated the item type in the build_user_menu in primary navigation class method. + $data['divider'] = true; + } + return $data; + } + + /** + * Get the class of item title text position for card layout. + * + * @return string|null An html class of the text position. + */ + public function get_textposition_class() { + switch ($this->item->textposition) { + case self::POSITION_OVERLAYBOTTOM: + $class = 'card-text-overlay-bottom'; + break; + case self::POSITION_OVERLAYTOP; + $class = 'card-text-overlay-top'; + break; + default: + $class = 'card-text-below'; + break; + } + return $class ?? ''; + } + + /** + * Convert the card item background color hexa code into rgba(). + * It includes the opacity with color code and convert it into rgba. + * + * @return void + */ + public function convert_background_code() { + // Verify the menu style is card, and is the text position is overly bottom or overlay top. + if ($this->menu->type != smartmenu::TYPE_CARD || $this->item->textposition == self::POSITION_BELOW) { + return false; + } + + // Attach the opacity into bg color and convert the item bgcolor hexa code into rgba. + $background = smartmenu_helper::color_get_rgba($this->item->backgroundcolor, self::BACKGROUND_OPACITY); + $this->item->backgroundcolor = $background; + } + + /** + * Get count of available menus. + * + * @param int $menuid Count the items under the given menu. + * @return int The number of items. + */ + public static function get_itemscount($menuid) { + global $DB; + return $DB->count_records('theme_boost_union_menuitems', ['menu' => $menuid]); + } + + /** + * Load the course custom fields mform elements to create/edit menu item form. + * It helps to setup the conditions based on custom field values when the menu item type is dynamic courses. + * + * @param \MoodleQuickForm $mform + * @return void + */ + public static function load_custom_field_config(&$mform) { + global $PAGE; + + $coursehandler = \core_course\customfield\course_handler::create(); + foreach ($coursehandler->get_fields() as $field) { + $shortname = $field->get('shortname'); + $fieldid = $field->get('id'); + $field = \core_customfield\field_controller::create($fieldid); + $data = \core_customfield\api::get_instance_fields_data([$fieldid => $field], 0); + if (isset($data[$fieldid])) { + $data = $data[$fieldid]; + $data->instance_form_definition($mform); + $elem = $mform->getElement("customfield_".$shortname); + // Remove the rules for custom fields. + if (isset($mform->_rules["customfield_".$shortname])) { + unset($mform->_rules["customfield_".$shortname]); + } + // Remove the custom fields from required sections. + if (($key = array_search("customfield_".$shortname, $mform->_required)) !== false) { + unset($mform->_required[$key]); + } + // By default, ensure that no values are pre-set in the form as defaults. + $default = (isset($mform->_types["customfield_".$shortname]) + && $mform->_types["customfield_".$shortname]) == 'int' ? 0 : ''; + + $mform->setDefault("customfield_".$shortname, $default); + // Change the password fields type to text, then admin can view the password field as text field. + if ($elem->_type == 'password') { + $elem->_type = 'text'; + } + + // Make the select fields to select multiple. + if ("select" == $field->get('type') || "semester" == $field->get('type')) { + $elem->setMultiple(true); + $mform->setDefault("customfield_".$shortname, 0); + } + + $mform->hideif("customfield_".$shortname, 'type', 'neq', self::TYPEDYNAMIC); + } + } + + $PAGE->requires->js_amd_inline('require(["core/form-autocomplete", "core/str"], function(Auto, Str) { + // List of custom fields. + var dropdowns = document.querySelectorAll("div[data-fieldtype=select] [id^=id_customfield_]"); + // Fetch no-selection string. + Str.get_string("noselection", "form").then((noSelection) => { + dropdowns.forEach((elem) => { + elem.classList.add("custom-select"); + // Change the field type to autcomplete, it fix the suggestion box alignment. + elem.parentNode.setAttribute("data-fieldtype", "autocomplete"); + Auto.enhance(elem, "", false, "", false, true, noSelection); + }); + }); + })'); + } + + /** + * Get an array of available types for the item. + * + * @param int|null $type Optional. The specific type to retrieve. Defaults to null. + * @return array|string An array of types if $type is null, or a string with the name of the specific type. + */ + public static function get_types(int $type=null) { + $types = array( + self::TYPEHEADING => get_string('heading', 'editor'), + self::TYPESTATIC => get_string('smartmenusstatic', 'theme_boost_union'), + self::TYPEDYNAMIC => get_string('smartmenusdynamiccourses', 'theme_boost_union'), + ); + + return ($type !== null && isset($types[$type])) ? $types[$type] : $types; + } + + /** + * Returns the display options for the menu items. + * + * @param int|null $option The display option to retrieve. If null, returns all display options. + * @return array|string The array of display options if $option is null, or the display option string if $option is set. + * @throws coding_exception if $option is set but invalid. + */ + public static function get_display_options(int $option=null) { + $displayoptions = [ + self::DISPLAY_SHOWTITLEICON => get_string('smartmenusshowtitleicon', 'theme_boost_union'), + self::DISPLAY_HIDETITLE => get_string('smartmenushidetitle', 'theme_boost_union'), + self::DISPLAY_HIDETITLEMOBILE => get_string('smartmenushidetitlemobile', 'theme_boost_union') + ]; + + return ($option !== null && isset($displayoptions[$option])) ? $displayoptions[$option] : $displayoptions; + } + + /** + * Insert or update the menu instance to DB. Convert the multiple options select elements to json. + * setup menu path after insert/update. + * + * Update the other items order when the current item order is updated. + * Increase the sortorder to next for all items + * + * + * @param stdclass $formdata + * @return bool + */ + public static function manage_instance($formdata) { + global $DB; + + $record = $formdata; + + // Convert the multiple valueable item types to JSON. + $record->category = json_encode($formdata->category); + $record->enrolmentrole = json_encode($formdata->enrolmentrole); + $record->completionstatus = json_encode($formdata->completionstatus); + $record->daterange = json_encode($formdata->daterange); + + $coursehandler = \core_course\customfield\course_handler::create(); + $customfields = []; + foreach ($coursehandler->get_fields() as $field) { + $shortname = $field->get('shortname'); + $customfields[$shortname] = $record->{'customfield_'.$shortname} ?? ''; + } + $record->customfields = json_encode($customfields); + + // Update the multiple values to JSON format. + $record->roles = json_encode($formdata->roles); + $record->cohorts = json_encode($formdata->cohorts); + $record->languages = json_encode($formdata->languages); + + // Enable the responsive viewports. + $record->desktop = ($record->desktop) ?? 0; + $record->tablet = ($record->tablet) ?? 0; + $record->mobile = ($record->mobile) ?? 0; + + $transaction = $DB->start_delegated_transaction(); + + // Cache for menus. + $menucache = cache::make('theme_boost_union', 'smartmenus'); + // Cache for menu items. + $cache = cache::make('theme_boost_union', 'smartmenu_items'); + + if (isset($formdata->id) && $oldrecord = $DB->get_record('theme_boost_union_menuitems', ['id' => $formdata->id])) { + $itemid = $formdata->id; + + $DB->update_record('theme_boost_union_menuitems', $record); + + // Increase or decrease the order of items between previous and current order of this item. + if ($oldrecord->sortorder != $record->sortorder) { + $operation = ($oldrecord->sortorder < $record->sortorder) + ? 'SET sortorder=sortorder-1 WHERE (sortorder between :oldorder AND :neworder)' + : 'SET sortorder=sortorder+1 WHERE (sortorder between :neworder AND :oldorder)'; + + $decrease = "UPDATE {theme_boost_union_menuitems} $operation AND id != :item AND menu=:menuid"; + // Used the execute method, couldn't found any defined function to update records using sql. + $DB->execute($decrease, [ + 'oldorder' => $oldrecord->sortorder, + 'neworder' => $record->sortorder, + 'item' => $formdata->id, + 'menuid' => $formdata->menu + ]); + } + + // Delete the cached data of its menu. Menu will recreate with this item. + $menucache->delete_menu($formdata->menu); + // Purge the current item cache for all users. + $cache->delete_menu($formdata->id); + + // Show the edited success notification. + \core\notification::success(get_string('smartmenusupdatesuccess', 'theme_boost_union')); + } else { + $record->sortorder = $record->sortorder ?: 1; + $itemid = $DB->insert_record('theme_boost_union_menuitems', $record); + // Setup the order for item. + $sql = "UPDATE {theme_boost_union_menuitems} + SET sortorder = sortorder + 1 + WHERE sortorder >= :sortorder AND id != :item AND menu=:menuid"; + + $DB->execute($sql, ['sortorder' => $record->sortorder, 'item' => $itemid, 'menuid' => $record->menu]); + // Show the menu inserted success notification. + \core\notification::success(get_string('smartmenusinsertsuccess', 'theme_boost_union')); + + // Delete the cached data of its menu. Menu will recreate with this item. + $menucache->delete_menu($formdata->menu); + } + + // Save the item image files to the file directory. + if (isset($record->image)) { + $draftitemid = file_get_submitted_draft_itemid('itemimage'); + file_save_draft_area_files( + $record->image, + context_system::instance()->id, + 'theme_boost_union', + 'smartmenus_itemimage', + $itemid, + self::image_fileoptions() + ); + } + + $transaction->allow_commit(); + + return true; + } + + /** + * Get file options for selecting a single web image file. + * + * @return array An array of file options. + */ + public static function image_fileoptions() { + + return [ + 'subdirs' => 0, + 'maxfiles' => 1, + 'accepted_types' => 'web_image', + ]; + } + +} diff --git a/classes/table/smartmenus_items.php b/classes/table/smartmenus_items.php new file mode 100644 index 00000000000..67b6588cceb --- /dev/null +++ b/classes/table/smartmenus_items.php @@ -0,0 +1,275 @@ +. + +/** + * Table to list the items for menu. Display the items access rules and it type. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union\table; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/lib/tablelib.php'); + +use moodle_url; +use html_writer; +use smartmenu_helper; + +/** + * List of items available in the menu. + */ +class smartmenus_items extends \table_sql { + + /** + * Setup and Render the menus table. + * + * @param int $pagesize Size of page for paginated displayed table. + * @param bool $useinitialsbar Whether to use the initials bar which will only be used if there is a fullname column defined. + * @param string $downloadhelpbutton + */ + public function out($pagesize, $useinitialsbar, $downloadhelpbutton = '') { + $columns = ['title', 'type', 'restrictions', 'sortorder']; + + $headers = [ + get_string('smartmenustitle', 'theme_boost_union'), + get_string('smartmenustypes', 'theme_boost_union'), + get_string('smartmenusrestriction', 'theme_boost_union'), + get_string('actions'), + ]; + + $this->define_columns($columns); + $this->define_headers($headers); + + // Remove sorting for some fields. + $this->sortable(false, 'sortorder', SORT_ASC); + + // Do not make the table collapsible. + $this->collapsible(false); + + $this->guess_base_url(); + // Table name for TESTING. + $this->set_attribute('id', 'smartmenus_item'); + + parent::out($pagesize, $useinitialsbar, $downloadhelpbutton); + } + + /** + * Guess the base url for the participants table. + */ + public function guess_base_url(): void { + $menu = required_param('menu', PARAM_INT); + $this->baseurl = new moodle_url('/theme/boost_union/smartmenus/items.php', ['menu' => $menu]); + } + + /** + * Set the sql query to fetch smart menus list. + * + * @param int $pagesize Size of page for paginated displayed table. + * @param boolean $useinitialsbar Whether to use the initials bar which will only be used if there is a fullname column defined. + * @return void + */ + public function query_db($pagesize, $useinitialsbar = true) { + // Fetch all avialable records from smart menu table. + $this->set_sql('*', '{theme_boost_union_menuitems}', 'menu=:menuid', ['menuid' => $this->uniqueid]); + parent::query_db($pagesize, $useinitialsbar); + } + + /** + * Display the item type, whether is static, dynamic, or heading. + * + * @param stdclass $row Data of the item. + * @return string HTML content to show the item type. + */ + public function col_type($row) { + $type = \theme_boost_union\smartmenu_item::get_types($row->type); + return html_writer::tag('span', $type, ['class' => 'badge badge-primary']); + } + + /** + * Display the access rules configured in menu. Fetch the user readable names for roles, cohorts, lanauges and dates. + * + * @param object $row + * @return string $html + */ + public function col_restrictions($row) { + global $DB; + + $rules = []; + + if ($row->roles != '' && !empty(json_decode($row->roles))) { + $roles = json_decode($row->roles); + $rolelist = $DB->get_records_list('role', 'id', $roles); + $rolenames = role_fix_names($rolelist); + array_walk($rolenames, function(&$value) { + $value = html_writer::tag('span', $value->localname, ['class' => 'badge badge-primary']); + }); + + $rules[] = [ + 'name' => get_string('smartmenusbyrole', 'theme_boost_union'), + 'value' => implode(' ', $rolenames) + ]; + } + + if ($row->cohorts != '' && !empty(json_decode($row->cohorts))) { + $cohorts = json_decode($row->cohorts); + $cohortlist = $DB->get_records_list('cohort', 'id', $cohorts); + + array_walk($cohortlist, function(&$value) { + $value = html_writer::tag('span', $value->name, ['class' => 'badge badge-primary']); + }); + $rules[] = [ + 'name' => get_string('smartmenusbycohort', 'theme_boost_union'), + 'value' => implode(' ', $cohortlist) + ]; + } + + if ($row->languages != '' && !empty(json_decode($row->languages))) { + // Get user readable name for selected lanauges. + $languages = json_decode($row->languages); + $options = get_string_manager()->get_list_of_translations(); + $list = []; + foreach ($languages as $lang) { + if (isset($options[$lang])) { + $list[] = html_writer::tag('span', $options[$lang], ['class' => 'badge badge-primary']); + } + } + $rules[] = [ + 'name' => get_string('smartmenusbylanguage', 'theme_boost_union'), + 'value' => implode(' ', $list) + ]; + } + + if ($row->start_date) { + $rules[] = [ + 'name' => get_string('smartmenusfrom', 'theme_boost_union'), + 'value' => userdate($row->start_date, get_string('strftimedate', 'core_langconfig') ) + ]; + + } + if ($row->end_date) { + $rules[] = [ + 'name' => get_string('smartmenusdurationuntil', 'theme_boost_union'), + 'value' => userdate($row->end_date, get_string('strftimedate', 'core_langconfig') ) + ]; + + } + + $html = ''; + foreach ($rules as $rule) { + $html .= html_writer::tag('li', html_writer::tag('label', $rule['name']) . $rule['value']); + } + return $html ? html_writer::tag('ul', $html) : get_string('smartmenusnorestrict', 'theme_boost_union'); + } + + /** + * Actions Column, which contains the options to update the menuitem visibility, Update the menu, delete, duplicate, sort. + * Used sortorder column as actions column, if not mention the sortorder column in columns order doesn't works based sortorder. + * @param \stdclass $row + * @return string + */ + public function col_sortorder($row) { + global $OUTPUT; + + $baseurl = new \moodle_url('/theme/boost_union/smartmenus/items.php', [ + 'id' => $row->id, + 'sesskey' => \sesskey() + ]); + $actions = array(); + + // Show/Hide. + if ($row->visible) { + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'hide')), + 'icon' => new \pix_icon('t/hide', \get_string('hide')), + 'attributes' => array('data-action' => 'hide', 'class' => 'action-hide') + ); + } else { + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'show')), + 'icon' => new \pix_icon('t/show', \get_string('show')), + 'attributes' => array('data-action' => 'show', 'class' => 'action-show') + ); + } + // Edit. + $actions[] = array( + 'url' => new moodle_url('/theme/boost_union/smartmenus/edit_items.php', [ + 'id' => $row->id, + 'sesskey' => sesskey() + ]), + 'icon' => new \pix_icon('t/edit', \get_string('edit')), + 'attributes' => array('class' => 'action-edit') + ); + + // Make the menu item duplicate. + $actions[] = array( + 'url' => new \moodle_url($baseurl, ['action' => 'copy']), + 'icon' => new \pix_icon('t/copy', \get_string('smartmenuscopyitem', 'theme_boost_union')), + 'attributes' => array('class' => 'action-copy') + ); + + // Delete. + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'delete')), + 'icon' => new \pix_icon('t/delete', \get_string('delete')), + 'attributes' => array('class' => 'action-delete'), + 'action' => new \confirm_action(get_string('smartmenusdeleteconfirmitem', 'theme_boost_union')) + ); + + // Move up/down. + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'moveup')), + 'icon' => new \pix_icon('t/up', \get_string('moveup')), + 'attributes' => array('data-action' => 'moveup', 'class' => 'action-moveup') + ); + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'movedown')), + 'icon' => new \pix_icon('t/down', \get_string('movedown')), + 'attributes' => array('data-action' => 'movedown', 'class' => 'action-movedown') + ); + + $actionshtml = array(); + foreach ($actions as $action) { + $action['attributes']['role'] = 'button'; + $actionshtml[] = $OUTPUT->action_icon( + $action['url'], + $action['icon'], + ($action['action'] ?? null), + $action['attributes'] + ); + } + return html_writer::span(join('', $actionshtml), 'menu-item-actions item-actions mr-0'); + } + + /** + * Override the default "Nothing to display" message when no menus available. + * + * @return void + */ + public function print_nothing_to_display() { + global $OUTPUT; + + // Show notification as html element. + $notification = new \core\output\notification( + get_string('smartmenusitemsnothingtodisplay', 'theme_boost_union'), + \core\output\notification::NOTIFY_INFO); + $notification->set_show_closebutton(false); + echo $OUTPUT->render($notification); + } +} diff --git a/classes/table/smartmenus_menus.php b/classes/table/smartmenus_menus.php new file mode 100644 index 00000000000..4d0d7d2678a --- /dev/null +++ b/classes/table/smartmenus_menus.php @@ -0,0 +1,251 @@ +. + +/** + * Table to list the menus. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union\table; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/lib/tablelib.php'); + +use moodle_url; +use html_writer; +use theme_boost_union\smartmenu as menuinstance; + +/** + * List of Smart menus table. + */ +class smartmenus_menus extends \table_sql { + + /** + * Setup and Render the menus table. + * + * @param int $pagesize Size of page for paginated displayed table. + * @param bool $useinitialsbar Whether to use the initials bar which will only be used if there is a fullname column defined. + * @param string $downloadhelpbutton + */ + public function out($pagesize, $useinitialsbar, $downloadhelpbutton = '') { + + // Define table headers and columns. + $columns = ['title', 'description', 'location', 'type', 'sortorder']; + $headers = [ + get_string('smartmenustitle', 'theme_boost_union'), + get_string('smartmenusdescription', 'theme_boost_union'), + get_string('smartmenuslocation', 'theme_boost_union'), + get_string('smartmenustypes', 'theme_boost_union'), + get_string('actions'), + ]; + + $this->define_columns($columns); + $this->define_headers($headers); + + // Remove sorting for some fields. + $this->sortable(false, 'sortorder', SORT_ASC); + + // Do not make the table collapsible. + $this->collapsible(false); + + $this->set_attribute('id', 'smartmenus'); + + $this->guess_base_url(); + + parent::out($pagesize, $useinitialsbar, $downloadhelpbutton); + } + + /** + * Guess the base url for the menu items table. + */ + public function guess_base_url(): void { + $this->baseurl = new moodle_url('/theme/boost_union/smartmenus/menus.php'); + } + + /** + * Set the sql query to fetch smart menus list. + * + * @param int $pagesize Size of page for paginated displayed table. + * @param boolean $useinitialsbar Whether to use the initials bar which will only be used if there is a fullname column defined. + * @return void + */ + public function query_db($pagesize, $useinitialsbar = true) { + // Fetch all avialable records from smart menu table. + $this->set_sql('*', '{theme_boost_union_menus}', '1=1'); + + parent::query_db($pagesize, $useinitialsbar); + } + + /** + * Show the menu title in the list, render the description based on the show description value "Above, below and help". + * Default help_icon method only works with string identifiers, rendered the help icon from template directly. + * + * @param object $row + * @return void + */ + public function col_title($row) { + global $OUTPUT; + $title = html_writer::tag('h6', $row->title, ['class' => 'menu-title']); + return $title; + } + + /** + * Display the menus description. + * + * @param stdclass $row + * @return void + */ + public function col_description($row) { + + $description = format_text($row->description, FORMAT_HTML); + $description = html_to_text($description); + + return $description; + } + + /** + * Display the locations of menu. Convert the languge shortname to user readable name. + * + * @param stdClass $row The row object containing the location information. + * @return string The HTML code to display the location information as a list of badges. + */ + public function col_location($row) { + $locations = json_decode($row->location); + return (!empty($locations)) ? implode(' ', array_map(function($value) { + $location = \theme_boost_union\smartmenu::get_location($value); + return html_writer::tag('span', $location, ['class' => 'badge badge-primary']); + }, $locations)) : ""; + } + + /** + * Display the "type" of column for a row in the item table. + * + * @param object $row The database row representing the Smart Menu item. + * @return string The HTML representation of the "type" column for the given row. + */ + public function col_type($row) { + $type = \theme_boost_union\smartmenu::get_type($row->type); + return html_writer::tag('span', $type, ['class' => 'badge badge-primary']); + } + + /** + * Actions Column, which contains the options to update the menuitem visibility, Update the menu, delete, duplicate, sort. + * Used sortorder column as actions column, if not mention the sortorder column in columns order doesn't works based sortorder. + * @param \stdclass $row + * @return string + */ + public function col_sortorder($row) { + global $OUTPUT; + + $baseurl = new \moodle_url('/theme/boost_union/smartmenus/menus.php', [ + 'id' => $row->id, + 'sesskey' => \sesskey() + ]); + $actions = array(); + + // Show/Hide. + if ($row->visible) { + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'hidemenu')), + 'icon' => new \pix_icon('t/hide', \get_string('hide')), + 'attributes' => array('data-action' => 'hide', 'class' => 'action-hide') + ); + } else { + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'showmenu')), + 'icon' => new \pix_icon('t/show', \get_string('show')), + 'attributes' => array('data-action' => 'show', 'class' => 'action-show') + ); + } + + // Edit. + $actions[] = array( + 'url' => new moodle_url('/theme/boost_union/smartmenus/edit.php', [ + 'id' => $row->id, + 'sesskey' => sesskey() + ]), + 'icon' => new \pix_icon('t/edit', \get_string('edit')), + 'attributes' => array('class' => 'action-edit') + ); + + // Make the menu duplicate. + $actions[] = array( + 'url' => new \moodle_url($baseurl, ['action' => 'copy']), + 'icon' => new \pix_icon('t/copy', \get_string('smartmenuscopymenu', 'theme_boost_union')), + 'attributes' => array('class' => 'action-copy') + ); + + // List of items. + $itemsurl = new \moodle_url('/theme/boost_union/smartmenus/items.php', ['menu' => $row->id]); + $actions[] = array( + 'url' => $itemsurl, + 'icon' => new \pix_icon('e/bullet_list', \get_string('list')), + 'attributes' => array('class' => 'action-list-items') + ); + + // Delete. + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'delete')), + 'icon' => new \pix_icon('t/delete', \get_string('delete')), + 'attributes' => array('class' => 'action-delete'), + 'action' => new \confirm_action(get_string('smartmenusdeleteconfirmmenu', 'theme_boost_union')) + ); + + // Move up/down. + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'moveup')), + 'icon' => new \pix_icon('t/up', \get_string('moveup')), + 'attributes' => array('data-action' => 'moveup', 'class' => 'action-moveup') + ); + $actions[] = array( + 'url' => new \moodle_url($baseurl, array('action' => 'movedown')), + 'icon' => new \pix_icon('t/down', \get_string('movedown')), + 'attributes' => array('data-action' => 'movedown', 'class' => 'action-movedown') + ); + + $actionshtml = array(); + foreach ($actions as $action) { + $action['attributes']['role'] = 'button'; + $actionshtml[] = $OUTPUT->action_icon( + $action['url'], + $action['icon'], + ($action['action'] ?? null), + $action['attributes'] + ); + } + return html_writer::span(join('', $actionshtml), 'menu-item-actions item-actions mr-0'); + } + + /** + * Override the default "Nothing to display" message when no menus available. + * + * @return void + */ + public function print_nothing_to_display() { + global $OUTPUT; + + // Show notification as html element. + $notification = new \core\output\notification( + get_string('smartmenusmenusnothingtodisplay', 'theme_boost_union'), + \core\output\notification::NOTIFY_INFO); + $notification->set_show_closebutton(false); + echo $OUTPUT->render($notification); + } +} diff --git a/db/caches.php b/db/caches.php index 89e18458f82..40868acfc9f 100644 --- a/db/caches.php +++ b/db/caches.php @@ -67,5 +67,19 @@ 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, + ), + // This cache stores the smart menus. + 'smartmenus' => array( + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => false, + 'overrideclass' => '\theme_boost_union\cache\loader', + ), + // This cache stores the smart menus' menu items. + 'smartmenu_items' => array( + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => false, + 'overrideclass' => '\theme_boost_union\cache\loader', ) ); diff --git a/db/events.php b/db/events.php index b25a33e93ef..271bdce6c32 100644 --- a/db/events.php +++ b/db/events.php @@ -37,4 +37,49 @@ 'eventname' => '\core\event\cohort_member_removed', 'callback' => '\theme_boost_union\eventobservers::cohort_member_removed' ), + array( + 'eventname' => 'core\event\role_assigned', + 'callback' => '\theme_boost_union\eventobservers::role_assigned' + ), + array( + 'eventname' => 'core\event\role_deleted', + 'callback' => '\theme_boost_union\eventobservers::role_deleted' + ), + array( + 'eventname' => 'core\event\role_unassigned', + 'callback' => '\theme_boost_union\eventobservers::role_unassigned' + ), + array( + 'eventname' => 'core\event\user_updated', + 'callback' => '\theme_boost_union\eventobservers::user_updated' + ), + array( + 'eventname' => 'core\event\course_created', + 'callback' => '\theme_boost_union\eventobservers::course_updated' + ), + array( + 'eventname' => 'core\event\course_completion_updated', + 'callback' => '\theme_boost_union\eventobservers::completion_updated' + ), + array( + 'eventname' => 'core\event\course_module_completion_updated', + 'callback' => '\theme_boost_union\eventobservers::completion_updated' + ), + array( + 'eventname' => 'core\event\course_updated', + 'callback' => '\theme_boost_union\eventobservers::course_updated' + ), + array( + 'eventname' => 'core\event\course_deleted', + 'callback' => '\theme_boost_union\eventobservers::course_updated' + ), + array( + 'eventname' => 'core\event\course_category_deleted', + 'callback' => '\theme_boost_union\eventobservers::category_updated' + ), + array( + 'eventname' => 'core\event\course_category_updated', + 'callback' => '\theme_boost_union\eventobservers::category_updated' + ), + ); diff --git a/db/install.xml b/db/install.xml index ddb4c48b1e4..ba223184b66 100644 --- a/db/install.xml +++ b/db/install.xml @@ -26,5 +26,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/db/upgrade.php b/db/upgrade.php index 0a82cd9e16c..9e35af80c48 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -136,5 +136,93 @@ function xmldb_theme_boost_union_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2022080922, 'theme', 'boost_union'); } + if ($oldversion < 2023010517) { + + // Define table theme_boost_union_menus to be created. + $table = new xmldb_table('theme_boost_union_menus'); + + // Adding fields to table theme_boost_union_menus. + $table->add_field('id', XMLDB_TYPE_INTEGER, '18', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE); + $table->add_field('title', XMLDB_TYPE_CHAR, '255', null, null); + $table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null, 'title'); + $table->add_field('description_format', XMLDB_TYPE_INTEGER, 9, null, null, null, null, 'description'); + $table->add_field('showdesc', XMLDB_TYPE_INTEGER, '9', null, null, null, null, 'description_format'); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '18', XMLDB_UNSIGNED, null, null, null, 'showdesc'); + $table->add_field('location', XMLDB_TYPE_CHAR, '50', null, XMLDB_NOTNULL, null, null, 'sortorder'); + $table->add_field('type', XMLDB_TYPE_INTEGER, '9', null, XMLDB_NOTNULL, null, '1', 'location'); + $table->add_field('mode', XMLDB_TYPE_INTEGER, '9', null, null, null, '1', 'type'); + $table->add_field('cssclass', XMLDB_TYPE_CHAR, '50', null, null, null, null, 'mode'); + $table->add_field('moremenubehavior', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, null, null, null, 'cssclass'); + $table->add_field('cardsize', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, null, null, null, 'moremenubehavior'); + $table->add_field('cardform', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, null, null, null, 'cardsize'); + $table->add_field('overflowbehavior', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, null, null, null, 'cardform'); + $table->add_field('roles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'overflowbehavior'); + $table->add_field('rolecontext', XMLDB_TYPE_INTEGER, '9', null, null, null, null, 'roles'); + $table->add_field('cohorts', XMLDB_TYPE_TEXT, null, null, null, null, null, 'rolecontext'); + $table->add_field('operator', XMLDB_TYPE_INTEGER, '9', null, null, null, null, 'cohorts'); + $table->add_field('languages', XMLDB_TYPE_TEXT, null, null, null, null, null, 'operator'); + $table->add_field('start_date', XMLDB_TYPE_INTEGER, '18', null, null, null, null, 'languages'); + $table->add_field('end_date', XMLDB_TYPE_INTEGER, '18', null, null, null, null, 'start_date'); + $table->add_field('visible', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '1', 'end_date'); + + // Adding keys to table theme_boost_union_menus. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for theme_boost_union_menus. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table theme_boost_union_menuitems to be created. + $table = new xmldb_table('theme_boost_union_menuitems'); + + // Adding fields to table theme_boost_union_menuitems. + $table->add_field('id', XMLDB_TYPE_INTEGER, '18', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE); + $table->add_field('title', XMLDB_TYPE_CHAR, '255', null, null); + $table->add_field('menu', XMLDB_TYPE_INTEGER, '18', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, 'title'); + $table->add_field('type', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, null, null, '1', 'menu'); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '18', XMLDB_UNSIGNED, null, null, null, 'type'); + $table->add_field('url', XMLDB_TYPE_TEXT, null, null, null, null, null, 'sortorder'); + $table->add_field('category', XMLDB_TYPE_TEXT, null, null, null, null, null, 'url'); + $table->add_field('enrolmentrole', XMLDB_TYPE_TEXT, null, null, null, null, null, 'category'); + $table->add_field('completionstatus', XMLDB_TYPE_CHAR, '20', null, null, null, null, 'enrolmentrole'); + $table->add_field('daterange', XMLDB_TYPE_CHAR, '20', null, null, null, null, 'completionstatus'); + $table->add_field('customfields', XMLDB_TYPE_TEXT, null, null, null, null, null, 'daterange'); + $table->add_field('displayfield', XMLDB_TYPE_INTEGER, '9', null, null, null, null, 'customfields'); + $table->add_field('textcount', XMLDB_TYPE_INTEGER, '9', null, null, null, null, 'displayfield'); + $table->add_field('mode', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, 'textcount'); + $table->add_field('menuicon', XMLDB_TYPE_CHAR, '50', null, null, null, null, 'mode'); + $table->add_field('display', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, null, null, null, 'menuicon'); + $table->add_field('tooltip', XMLDB_TYPE_CHAR, '255', null, null); + $table->add_field('target', XMLDB_TYPE_INTEGER, '9', null, null, null, null, 'tooltip'); + $table->add_field('cssclass', XMLDB_TYPE_CHAR, '50', null, null, null, null, 'target'); + $table->add_field('textposition', XMLDB_TYPE_INTEGER, '9', null, null, null, '0', 'cssclass'); + $table->add_field('textcolor', XMLDB_TYPE_CHAR, '10', null, null, null, null, 'textposition'); + $table->add_field('backgroundcolor', XMLDB_TYPE_CHAR, '10', null, null, null, null, 'textcolor'); + $table->add_field('desktop', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, null, null, '1', 'backgroundcolor'); + $table->add_field('tablet', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, null, null, '1', 'desktop'); + $table->add_field('mobile', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, null, null, '1', 'tablet'); + $table->add_field('roles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'mobile'); + $table->add_field('rolecontext', XMLDB_TYPE_INTEGER, '9', null, null, null, '1', 'roles'); + $table->add_field('cohorts', XMLDB_TYPE_TEXT, null, null, null, null, null, 'rolecontext'); + $table->add_field('operator', XMLDB_TYPE_INTEGER, '9', null, null, null, '1', 'cohorts'); + $table->add_field('languages', XMLDB_TYPE_TEXT, null, null, null, null, null, 'operator'); + $table->add_field('start_date', XMLDB_TYPE_INTEGER, '18', null, null, null, null, 'languages'); + $table->add_field('end_date', XMLDB_TYPE_INTEGER, '18', null, null, null, null, 'start_date'); + $table->add_field('visible', XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '1', 'end_date'); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '18', null, null, null, null, 'visible'); + + // Adding keys to table theme_boost_union_menuitems. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Conditionally launch create table for theme_boost_union_menuitems. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Boost_union savepoint reached. + upgrade_plugin_savepoint(true, 2023010517, 'theme', 'boost_union'); + } + return true; } diff --git a/form/element-colorpicker.php b/form/element-colorpicker.php new file mode 100644 index 00000000000..dd95bfe90a4 --- /dev/null +++ b/form/element-colorpicker.php @@ -0,0 +1,99 @@ +. + +/** + * Theme Boost Union Login - Form element for color picker + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once('HTML/QuickForm/input.php'); +require_once($CFG->dirroot.'/lib/form/templatable_form_element.php'); +require_once($CFG->dirroot.'/lib/form/text.php'); + +/** + * Form element for color picker + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class moodlequickform_themeboostunion_colorpicker extends MoodleQuickForm_text implements templatable { + + use templatable_form_element { + export_for_template as export_for_template_base; + } + + /** + * Constructor. + * + * @param string $elementname (optional) Name of the text field. + * @param string $elementlabel (optional) Text field label. + * @param string $attributes (optional) Either a typical HTML attribute string or an associative array. + */ + public function __construct($elementname=null, $elementlabel=null, $attributes=null) { + parent::__construct($elementname, $elementlabel, $attributes); + $this->setType('text'); + + // Add a CSS class for styling the color picker. + $class = $this->getAttribute('class'); + if (empty($class)) { + $class = ''; + } + $this->updateAttributes(array('class' => $class.' theme_boost_union-form-colour-picker ')); + } + + /** + * Export for template. + * + * @param renderer_base $output + * @return array|stdClass + */ + public function export_for_template(renderer_base $output) { + global $PAGE; + + // Compose template context for the mform element. + $context = $this->export_for_template_base($output); + + // Build loading icon. + $icon = new pix_icon('i/loading', get_string('loading', 'admin'), 'moodle', ['class' => 'loadingicon']); + $icondata = $icon->export_for_template($output); + $iconoutput = $output->render_from_template('core/pix_icon', $icondata); + + // Get ID of the element. + $id = $this->getAttribute('id'); + + // Add JS to append the color picker div before the element and initiate the color picker utility method. + $PAGE->requires->js_amd_inline(" + var element = document.getElementById('$id'); + var pickerDiv = document.createElement('div'); + pickerDiv.classList.add('admin_colourpicker', 'clearfix'); + pickerDiv.innerHTML = '$iconoutput'; // Add loading icon. + element.parentNode.prepend(pickerDiv); + element.parentNode.style.flexDirection = 'column'; // Helps to align the config text when + // theme_boost_union is not the active theme. + + // Init color picker utility. + M.util.init_colour_picker(Y, '$id'); + "); + + return $context; + } +} diff --git a/lang/en/theme_boost_union.php b/lang/en/theme_boost_union.php index def0aac79b0..294b394cad2 100644 --- a/lang/en/theme_boost_union.php +++ b/lang/en/theme_boost_union.php @@ -353,7 +353,8 @@ $string['primarynavigationheading'] = 'Primary navigation'; // ... ... Settings: Hide nodes in primary navigation. $string['hidenodesprimarynavigationsetting'] = 'Hide nodes in primary navigation'; -$string['hidenodesprimarynavigationsetting_desc'] = 'With this setting, you can hide one or multiple nodes from the primary navigation.'; +$string['hidenodesprimarynavigationsetting_desc'] = 'With this setting, you can hide one or multiple nodes from the primary navigation.

+Please note: Here, you can just remove navigation nodes. But if you want to add custom navigation nodes, please consider using Boost Union\'s smart menu functionality.'; // ... Section: Breadcrumbs. $string['breadcrumbsheading'] = 'Breadcrumbs'; // ... ... Setting: Course category breadcrumb. @@ -711,6 +712,175 @@ $string['flavourstitle'] = 'Title'; $string['flavourstitle_help'] = 'The flavour\'s title is just used internally to allow you to document a particular flavour in the list of flavours.'; +// Settings: Smart menus page. +$string['smartmenus'] = 'Smart menus'; +$string['error:smartmenusmenuitemnotfound'] = 'Smart menu item not found'; +$string['error:smartmenusmenunotfound'] = 'Smart menu not found'; +$string['smartmenus_desc'] = '

Smart menus allow site administrators to create customizable menus that can be placed in different locations on the site, such as the site main menu, bottom mobile menu, and user menu. The menus can be configured to display different types of content, including links to other pages or resources, category links, or user profile links.

Site administrators can create a new menu and specify the menu items, and display settings. The administrator can also choose where the menu will be displayed on the site and whether it should be visible to all users or only to certain user roles.

'; +$string['smartmenusabove'] = 'Above'; +$string['smartmenusaccessbycohorts'] = 'Restrict visibility by cohorts'; +$string['smartmenusaccessbydateselector'] = 'Restrict visibility by date'; +$string['smartmenusaccessbylanguage'] = 'Restrict visibility by language'; +$string['smartmenusaccessbyroles'] = 'Restrict visibility by roles'; +$string['smartmenusaccessrules'] = 'Access rules'; +$string['smartmenusaddnewdynamicitem'] = 'Add new dynamic item'; +$string['smartmenusaddnewheadingitem'] = 'Add new heading item'; +$string['smartmenusaddnewitem'] = 'Add new menu item'; +$string['smartmenusaddnewstaticitem'] = 'Add new static item'; +$string['smartmenusadvancedsettings'] = 'Advanced settings'; +$string['smartmenusallroles'] = 'All roles'; +$string['smartmenusanycohort'] = 'Any cohort'; +$string['smartmenusanylanguage'] = 'Any language'; +$string['smartmenusappearancecard'] = 'Card appearance'; +$string['smartmenusappearancecard_desc'] = 'Use these settings to customize the look and feel of the smart menu item when using cards'; +$string['smartmenusappearanceheader'] = 'Appearance'; +$string['smartmenusappearanceheader_desc'] = 'Use these settings to customize the look and feel of the smart menu item'; +$string['smartmenusbackgroundcolor'] = 'Background color'; +$string['smartmenusbackgroundcolor_help'] = 'Select the background color for the menu item'; +$string['smartmenusbelow'] = 'Below'; +$string['smartmenusbelowimage'] = 'Below image'; +$string['smartmenusbycohort'] = 'By cohort'; +$string['smartmenusbycohort_help'] = 'Restrict the visibility based on the user\'s cohorts.'; +$string['smartmenusbylanguage'] = 'By language'; +$string['smartmenusbylanguage_help'] = 'Restrict the visibility based on the user\'s language'; +$string['smartmenusbyrole'] = 'By role'; +$string['smartmenusbyrole_help'] = 'Restrict the visibility based on the user\'s roles.'; +$string['smartmenuscardappearance'] = 'Card appearance'; +$string['smartmenuscardform'] = 'Card form'; +$string['smartmenuscardform_help'] = 'Select the form of the card for card-style menus, choosing between square, portrait, landscape or fullwidth'; +$string['smartmenuscardsize'] = 'Card size'; +$string['smartmenuscardsize_help'] = 'Set the size of the card for card-style menus, choosing between tiny, small, medium, or large'; +$string['smartmenuscategory'] = 'Category'; +$string['smartmenuscategory_help'] = 'Select a category to display its courses as menu items'; +$string['smartmenuscompletionstatus'] = 'Completion status'; +$string['smartmenuscompletionstatus_help'] = 'For dynamic menu items, you can display a list of courses based on the user\'s completion status in the course. If you select "In progress" as the completion status, the dynamic list of courses will only contain courses that the current user is currently working on'; +$string['smartmenuscopyitem'] = 'Copy menu item'; +$string['smartmenuscopymenu'] = 'Copy menu and its items'; +$string['smartmenuscreate'] = 'Create menu'; +$string['smartmenuscreateitem'] = 'Create menu item'; +$string['smartmenuscreatemenu'] = 'Create new smart menu'; +$string['smartmenuscreatemenuitem'] = 'Create smart menu item'; +$string['smartmenuscssclass'] = 'CSS classes'; +$string['smartmenuscssclass_help'] = 'Enter a CSS class for the menu item. This can be used to apply custom styling to the menu item'; +$string['smartmenusdaterange'] = 'Date range'; +$string['smartmenusdaterange_help'] = 'Select the date range for which the menu item should be visible. The menu item will only be displayed to users during this period'; +$string['smartmenusdefault'] = 'Default'; +$string['smartmenusdeleteconfirmitem'] = 'Are you sure you want to delete this menu item from the smart menu?'; +$string['smartmenusdeleteconfirmmenu'] = 'Are you sure you want to delete this menu from the smart menus?'; +$string['smartmenusdescription'] = 'Description'; +$string['smartmenusdescription_help'] = 'The description of the menu'; +$string['smartmenusdisplayfield'] = 'Select name field'; +$string['smartmenusdisplayfield_help'] = 'This setting allows you to choose the field to display as the course name. Select one of the following options:
  • Short Name: Displays the short name of the course
  • Full Name: Displays the full name of the course
'; +$string['smartmenusdisplayoptions'] = 'Display options'; +$string['smartmenusdisplayoptions_help'] = 'Choose how you want the menu item to be displayed. You can display both the title and icon, hide the title, or hide the title only on mobile devices'; +$string['smartmenusdurationuntil'] = 'Until'; +$string['smartmenusdurationuntil_help'] = 'Hide this menu / menu item after the given date is reached'; +$string['smartmenusdynamiccourses'] = 'Dynamic courses'; +$string['smartmenusedit'] = 'Edit menu'; +$string['smartmenusedititem'] = 'Edit menu item'; +$string['smartmenuseditmenu'] = 'Edit smart menu'; +$string['smartmenuseditmenuitem'] = 'Edit smart menu item'; +$string['smartmenusenrolled'] = 'Enrolled'; +$string['smartmenusenrolmentrole'] = 'Enrolment role'; +$string['smartmenusenrolmentrole_help'] = 'Select the role for which the menu item should be visible. The menu item will only be displayed to users who have this role'; +$string['smartmenusexperimental'] = 'Please note: The smart menus functionality is fully usable in the current state of implementation, but has to be considered as experimental due to the large amount of setting combinations which still might trigger unexpected issues. Against this background, please test your smart menus with your individual menu settings thoroughly. If you encounter any issues with smart menus, please report them on Github with clear steps to reproduce.'; +$string['smartmenusforcedintomoremenu'] = 'Forced into more menu'; +$string['smartmenusforcedoutsideofmoremenu'] = 'Forced out side of more menu'; +$string['smartmenusfrom'] = 'From'; +$string['smartmenusfrom_help'] = 'Hide this menu / menu item before the given date is reached'; +$string['smartmenusfullname'] = 'Full name'; +$string['smartmenusfullwidth'] = 'Full width'; +$string['smartmenusfuture'] = 'Future'; +$string['smartmenusgeneralsection'] = 'General sections'; +$string['smartmenushelp'] = 'Help'; +$string['smartmenushidetitle'] = 'Hide title'; +$string['smartmenushidetitlemobile'] = 'Hide title in mobile'; +$string['smartmenusimage'] = 'Image'; +$string['smartmenusimage_help'] = 'Select an image to display next to the menu item. The item image will be used for card type menus and will be displayed as the card image for the item'; +$string['smartmenusinline'] = 'Inline'; +$string['smartmenusinsertsuccess'] = 'Smart menu created successfully'; +$string['smartmenusitemsnothingtodisplay'] = 'There aren\'t any items added to this smart menu yet. Please add an item to this menu.'; +$string['smartmenuslandscape'] = 'Landscape'; +$string['smartmenuslarge'] = 'Large'; +$string['smartmenuslocation'] = 'Locations'; +$string['smartmenuslocation_help'] = '

Select the location(s) where you want the menu to appear on the page:

  • The main navigation is at the top of the page where Moodle core shows the Home, Dashboard, My courses and Site administration navigation items already.
  • The menu bar is located above the main navigation, at the top of the page.
  • The user menu can be accessed by clicking on the user avatar in the navigation bar.
  • The bottom bar is placed at the bottom of the screen and can be used to implement a thumb navigation for easy access to important areas, such as the dashboard, the my courses page or the home page.

Please note that upon enabling the bottom bar, the hamburger icon will be replaced by your site\'s logo, because users can reach the main navigation then using the bottom bar.

'; +$string['smartmenuslocationbottom'] = 'Bottom bar'; +$string['smartmenuslocationmain'] = 'Main navigation'; +$string['smartmenuslocationmenu'] = 'Menu bar'; +$string['smartmenuslocationuser'] = 'User Menu'; +$string['smartmenusmedium'] = 'Medium'; +$string['smartmenusmenudeleted'] = 'Smart menu deleted successfully'; +$string['smartmenusmenuduplicated'] = 'Menu and its menu items duplicated successfully'; +$string['smartmenusmenuicon'] = 'Icon'; +$string['smartmenusmenuicon_help'] = 'Select an icon to display with the menu item title'; +$string['smartmenusmenuitemduplicated'] = 'Menu item duplicated successfully'; +$string['smartmenusmenuitems'] = 'Menu items'; +$string['smartmenusmenumode'] = 'Menu mode'; +$string['smartmenusmenumode_help'] = '

Select the mode how the menu\'s items should be displayed.

  • Submenu: The menu items will be displayed as a submenu with the menu\'s title as parent node. This is the default option.
  • Inline: The menu items will be displayed directly in the navigation, one after another. Please note that this option is not supported for card type menus.
'; +$string['smartmenusmenus'] = 'Menus'; +$string['smartmenusmenusnothingtodisplay'] = 'There aren\'t any smart menus created yet. Please create your first smart menu to get things going.'; +$string['smartmenusmenutitle'] = 'Title'; +$string['smartmenusmenutitle_help'] = 'The title of the menu. This will be used as the label of the parent node of this menu.'; +$string['smartmenusmode'] = 'Mode'; +$string['smartmenusmode_help'] = 'Select the mode for which the menu item should be visible.
  • Inline: the item is displayed as a regular menu item within the menu. This is the default mode for static and heading menu items.
  • Submenu: the item is displayed as a submenu item, which can be expanded or collapsed by clicking on the parent item. This mode is useful for dynamic menu items, where courses are displayed as submenu items for this menu item. The title of this menu item is used as the text for the submenu item.
'; +$string['smartmenusmoremenubehavior'] = 'More menu behavior'; +$string['smartmenusmoremenubehavior_help'] = 'Choose whether to display the menu when there are too many items to fit in the menu or not. If enabled, excess items will be moved inside the "more" menu. You can choose whether to force the menus to display inside the "more" menu or outside of it.'; +$string['smartmenusnever'] = 'Never'; +$string['smartmenusnorestrict'] = 'Not restricted'; +$string['smartmenusnowrap'] = 'No wrap'; +$string['smartmenusoperator'] = 'Operator'; +$string['smartmenusoperator_help'] = 'Select the operator for the cohort condition (Any or All)'; +$string['smartmenusorder'] = 'Order'; +$string['smartmenusorder_help'] = 'Rearrange the position of item'; +$string['smartmenusoverflowbehavior'] = 'Overflow behavior'; +$string['smartmenusoverflowbehavior_help'] = 'Choose how the menu should behave when it overflows its container, either by showing a scrollbar or wrap the overflowing items'; +$string['smartmenusoverlaybottom'] = 'Bottom overlay'; +$string['smartmenusoverlaytop'] = 'Top overlay'; +$string['smartmenuspast'] = 'Past'; +$string['smartmenusportrait'] = 'Portrait'; +$string['smartmenuspresent'] = 'Present'; +$string['smartmenusresponsive'] = 'Responsive'; +$string['smartmenusresponsivedesktop'] = 'Desktop'; +$string['smartmenusresponsivemobile'] = 'Mobile'; +$string['smartmenusresponsivetablet'] = 'Tablet'; +$string['smartmenusrestriction'] = 'Access rules'; +$string['smartmenusrolecontext'] = 'Context'; +$string['smartmenusrolecontext_help'] = 'Select the context for which the user\'s role should be checked (Any context or system context only)'; +$string['smartmenussavechangesandconfigure'] = 'Save and configure items'; +$string['smartmenussettings'] = 'Smart menu settings'; +$string['smartmenusshortname'] = 'Short name'; +$string['smartmenusshowdescription'] = 'Show description'; +$string['smartmenusshowtitleicon'] = 'Show title icon'; +$string['smartmenussmall'] = 'Small'; +$string['smartmenussortorder_help'] = 'Enter a sort order for the menu item. Menu items are displayed in ascending order based on their sort order'; +$string['smartmenussquare'] = 'Square'; +$string['smartmenusstatic'] = 'Static'; +$string['smartmenussubmenu'] = 'Submenu'; +$string['smartmenustarget'] = 'Target'; +$string['smartmenustarget_help'] = 'Select the target for the link. The menu item will open in this target when clicked (e.g. in a new window or tab).'; +$string['smartmenustextcolor'] = 'Text color'; +$string['smartmenustextcolor_help'] = 'Select the color for the menu item text'; +$string['smartmenustextcount'] = 'Number of words'; +$string['smartmenustextcount_help'] = 'Specify the maximum number of words to be displayed for the course title in the menu'; +$string['smartmenustextposition'] = 'Text position'; +$string['smartmenustextposition_help'] = 'Select the position of the menu item text in relation to the icon or image. Options include top overlay, bottom overlay, and below image.
  • Top overlay: Displays the item title over the overlay and at the top of the section.
  • Bottom overlay: Displays the item title over the overlay and at the bottom of the section.
  • Below image: Displays the item title below the card image.
'; +$string['smartmenustiny'] = 'Tiny'; +$string['smartmenustitle'] = 'Title'; +$string['smartmenustitle_help'] = 'Enter the title for the menu item. If you want to display a separator in the menu, use hash signs (###) as title and choose Heading as type.'; +$string['smartmenustooltip'] = 'Tooltip'; +$string['smartmenustooltip_help'] = 'Enter a tooltip to display when the user hovers over the menu item'; +$string['smartmenustop'] = 'Top'; +$string['smartmenustype'] = 'Type'; +$string['smartmenustype_help'] = '

Select the type of menu item you want to create, choosing between static, heading and dynamic courses.

  • Static: A static menu item is simply a link to a fixed URL that does not change.
  • Heading: A heading menu item is used to group related menu items together under a common heading. It does not have a link and is not clickable.
  • Dynamic courses: A dynamic courses menu item is used to display a list of courses based on certain criteria, such as category, enrollment role, completion status, date range, etc. The content displayed in a dynamic courses menu item will update automatically as the criteria changes.
'; +$string['smartmenustypes'] = 'Types'; +$string['smartmenustypes_help'] = 'Select the type of menu you want to create, choosing between card and list'; +$string['smartmenustypescard'] = 'Card'; +$string['smartmenustypeslist'] = 'List'; +$string['smartmenusupdatesuccess'] = 'Smart menu updated successfully'; +$string['smartmenusurl'] = 'Menu item URL'; +$string['smartmenusurl_help'] = 'Enter the URL for the menu item. This is the link that will be followed when the menu item is clicked'; +$string['smartmenuswrap'] = 'Wrap'; + // Privacy API. $string['privacy:metadata'] = 'The Boost Union theme does not store any personal data about any user.'; diff --git a/layout/columns2.php b/layout/columns2.php index 30cb393ea8e..c08cb03e353 100644 --- a/layout/columns2.php +++ b/layout/columns2.php @@ -28,6 +28,7 @@ * * Include static pages * * Include Jvascript disabled hint * * Include info banners + * * Include smart menus * * @package theme_boost_union * @copyright 2022 Luca Bösch, BFH Bern University of Applied Sciences luca.boesch@bfh.ch @@ -68,7 +69,9 @@ } } -$primary = new core\navigation\output\primary($PAGE); +// Load the navigation from boost_union primary navigation, the extended version of core primary navigation. +// It includes the smart menus and menu items, for multiple locations. +$primary = new theme_boost_union\output\navigation\primary($PAGE); $renderer = $PAGE->get_renderer('core'); $primarymenu = $primary->export_for_template($renderer); $buildregionmainsettings = !$PAGE->include_region_main_settings_in_header_actions() && !$PAGE->has_secondary_navigation(); @@ -93,7 +96,7 @@ 'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu), 'headercontent' => $headercontent, 'overflow' => $overflow, - 'addblockbutton' => $addblockbutton, + 'addblockbutton' => $addblockbutton ]; // Include the template content for the course related hints. @@ -120,5 +123,8 @@ // Include the template content for the navbar styling. require_once(__DIR__ . '/includes/navbar.php'); +// Include the template content for the smart menus. +require_once(__DIR__ . '/includes/smartmenus.php'); + // Render columns2.mustache from theme_boost (which is overridden in theme_boost_union). echo $OUTPUT->render_from_template('theme_boost/columns2', $templatecontext); diff --git a/layout/drawers.php b/layout/drawers.php index b20a9cca0ba..ff0192bb9dc 100644 --- a/layout/drawers.php +++ b/layout/drawers.php @@ -31,6 +31,7 @@ * * Include info banners * * Include additional block regions * * Handle admin setting for right-hand block drawer of site home + * * Include smart menus * * @package theme_boost_union * @copyright 2022 Luca Bösch, BFH Bern University of Applied Sciences luca.boesch@bfh.ch @@ -124,7 +125,9 @@ } } -$primary = new core\navigation\output\primary($PAGE); +// Load the navigation from boost_union primary navigation, the extended version of core primary navigation. +// It includes the smart menus and menu items, for multiple locations. +$primary = new theme_boost_union\output\navigation\primary($PAGE); $renderer = $PAGE->get_renderer('core'); $primarymenu = $primary->export_for_template($renderer); $buildregionmainsettings = !$PAGE->include_region_main_settings_in_header_actions() && !$PAGE->has_secondary_navigation(); @@ -188,5 +191,8 @@ require_once(__DIR__ . '/includes/advertisementtiles.php'); } +// Include the template content for the smart menus. +require_once(__DIR__ . '/includes/smartmenus.php'); + // Render drawers.mustache from theme_boost (which is overridden in theme_boost_union). echo $OUTPUT->render_from_template('theme_boost/drawers', $templatecontext); diff --git a/layout/includes/smartmenus.php b/layout/includes/smartmenus.php new file mode 100644 index 00000000000..9beaf938cb6 --- /dev/null +++ b/layout/includes/smartmenus.php @@ -0,0 +1,30 @@ +. + +/** + * Theme Boost Union - smart menus include. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// Add smart menu elements to template context. +// TODO Add switch if enabled. +$templatecontext['menubar'] = $primarymenu['menubar'] ?? []; +$templatecontext['bottombar'] = $primarymenu['bottombar'] ?? []; diff --git a/lib.php b/lib.php index 7ed82d47420..3be45887cf1 100644 --- a/lib.php +++ b/lib.php @@ -481,6 +481,20 @@ function theme_boost_union_pluginfile($course, $cm, $context, $filearea, $args, // Send stored file (and cache it for 90 days, similar to other static assets within Moodle). send_stored_file($file, DAYSECS * 90, 0, $forcedownload, $options); + // Serve the files from the smart menu card images. + } else if ($filearea === 'smartmenus_itemimage' && $context->contextlevel === CONTEXT_SYSTEM) { + // Get file storage. + $fs = get_file_storage(); + + // Get the file from the filestorage. + $file = $fs->get_file($context->id, 'theme_boost_union', $filearea, $args[0], '/', $args[1]); + if (!$file) { + send_file_not_found(); + } + + // Send stored file (and cache it for 90 days, similar to other static assets within Moodle). + send_stored_file($file, DAYSECS * 90, 0, $forcedownload, $options); + } else { send_file_not_found(); } @@ -517,3 +531,54 @@ function theme_boost_union_before_standard_html_head() { // Return an empty string to keep the caller happy. return $html; } + +/** + * Fetches the list of icons and creates an icon suggestion list to be sent to a fragment. + * + * @param array $args An array of arguments. + * @return string The rendered HTML of the icon suggestion list. + */ +function theme_boost_union_output_fragment_icons_list($args) { + global $OUTPUT, $PAGE; + + // Proceed only if a context was given as argument. + if ($args['context']) { + // Initialize rendered icon list. + $icons = []; + + // Load the theme config. + $theme = \theme_config::load($PAGE->theme->name); + + // Get the FA system. + $faiconsystem = \core\output\icon_system_fontawesome::instance($theme->get_icon_system()); + + // Get the icon list. + $iconlist = $faiconsystem->get_core_icon_map(); + + // Add an empty element to the beginning of the icon list. + array_unshift($iconlist, ''); + + // Iterate over the icons. + foreach ($iconlist as $iconkey => $icontxt) { + // Split the component from the icon key. + $icon = explode(':', $iconkey); + + // Pick the icon key. + $iconstr = isset($icon[1]) ? $icon[1] : 'moodle'; + + // Pick the component. + $component = isset($icon[0]) ? $icon[0] : ''; + + // Render the pix icon. + $icon = new \pix_icon($iconstr, "", $component); + $icons[] = [ + 'icon' => $faiconsystem->render_pix_icon($OUTPUT, $icon), + 'value' => $iconkey, + 'label' => $icontxt + ]; + } + + // Return the rendered icon list. + return $OUTPUT->render_from_template('theme_boost_union/fontawesome-iconpicker-popover', ['options' => $icons]); + } +} diff --git a/scss/boost_union/post.scss b/scss/boost_union/post.scss index 91bdf3bbf0e..7d943046659 100644 --- a/scss/boost_union/post.scss +++ b/scss/boost_union/post.scss @@ -28,22 +28,19 @@ /* Remove the white border at the bottom of the navbar as it looks odd with a dark navbar. */ border-bottom: none; - /* Change the color of the site name in the navbar to light grey and to white when active. */ + /* Change the color of the site name in the navbar to white. */ .navbar-brand { - color: #c8c8c8; - } - .navbar-brand:hover { color: $white; } - /* Change the color of the navigation items in the navbar to light grey, to white when active + /* Change the color of the navigation items in the navbar to white and to black when hovered (as Moodle core will then show a white background). This has to use !important as Moodle core already uses !important for some of its hover states. */ .nav-link { - color: #c8c8c8; + color: $white; } .nav-link.active { - color: $white; + border-bottom-color: $white; } .nav-link:hover, .nav-link:focus { @@ -58,16 +55,20 @@ } /* Change the color of the icons in the navbar as well as the toggle icon in the usermenu - to a light grey and to white when hovered. + to white. This has to use !important as Moodle core already uses !important for the icon colors. */ .nav-link .icon, .nav-link a .icon, .usermenu .dropdown-toggle { - color: #c8c8c8 !important; /* stylelint-disable-line declaration-no-important */ + color: $white !important; /* stylelint-disable-line declaration-no-important */ + } + + /* Change the color of the icons in the dropdown menu to a dark grey and to white when hovered. + This has to use !important as Moodle core already uses !important for the icon colors. */ + .dropdown-menu .dropdown-item .icon { + color: #1d2125 !important; /* stylelint-disable-line declaration-no-important */ } - .nav-link:hover .icon, - .nav-link:hover a .icon, - .usermenu .dropdown-toggle:hover { + .dropdown-menu .dropdown-item:hover .icon { color: $white !important; /* stylelint-disable-line declaration-no-important */ } @@ -77,13 +78,10 @@ color: #6a737b !important; /* stylelint-disable-line declaration-no-important */ } - /* Change the color of the edit switch label in the navbar to light grey and to white when hovered. + /* Change the color of the edit switch label in the navbar to white. This has to use !important as Moodle core would change the color to text-primary as soon as editing is on. */ .editmode-switch-form, .editmode-switch-form label { - color: #c8c8c8 !important; /* stylelint-disable-line declaration-no-important */ - } - .editmode-switch-form label:hover { color: $white !important; /* stylelint-disable-line declaration-no-important */ } @@ -104,12 +102,9 @@ color: #fff; } - /* Change the color of the login link in the navbar to a light grey and to white when hovered. */ + /* Change the color of the login link in the navbar to white. */ .login, .login a { - color: #c8c8c8; - } - .login a:hover { color: $white; } @@ -119,12 +114,9 @@ color: #343a40 !important; /* stylelint-disable-line declaration-no-important */ } - /* Change the color of the language selector link in the navbar to a light grey and to white when hovered. */ + /* Change the color of the language selector link in the navbar to white. */ .langmenu .dropdown .dropdown-toggle { - color: #c8c8c8; - &:hover { - color: $white; - } + color: $white; } } @@ -823,7 +815,7 @@ body.limitedwidth .theme-block-region-footer-coursecontentwidth { background-color: $white; /* Change horizontal margin and padding to - to make the footnote work with background images. + make the footnote work with background images. We do not need any additional right padding as there isn't a back-to-top button nor a footer button on the login page on this screen size. */ margin-left: 0; @@ -913,10 +905,1106 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } +/*======================================= + * Settings: Smart menus. + ======================================*/ + +/*--------------------------------------- + * Header menu + --------------------------------------*/ + +.navbar { + &.fixed-top { + /* Added height and background color for the top header, + z-index is added to show the top header dropdown menu above the header block */ + &.boost-union-menubar { /* boost-union-menubar class is added for the top header */ + height: 42px; + border-bottom: 0; + background-color: #f8f9fa; + z-index: 1031; + /* The position of the side drawer has been adjusted slightly below to accommodate the top header. */ + ~ .drawer.drawer-left { + top: 0; + } + ~ .drawer.drawer-right { + top: 101px; + } + /* The position of the main page has been adjusted slightly below to accommodate the top header. */ + ~ #page.drawers { + margin-top: 100px; + /* The position of the drawer button to toggle has been adjusted slightly + below to accommodate the top header. + .drawer-toggles .drawer-toggler { + top: 110px; + }*/ + } + .moremenu { + .more-nav { + li { + /* The card dropdown menu link position is changed to static to show the card dropdown menu + in full width */ + &.card-dropdown { + position: static; + } + &.nav-item { + &:first-child { + /* The position of the dropdown menu should not extend beyond the visible area of + the screen */ + .dropdown-menu { + left: 0; + right: auto; + } + } + } + a { + &:not(.active) { + border-bottom: 0; + padding: 10px; + } + > * { + cursor: pointer; + } + /* Background color added in the menubar menu link on hover */ + &.nav-link:hover { + background-color: #f4f4f4; + } + } + /* The positions of the dropdown menu added */ + .dropdown-menu { + left: auto; + right: 0; + li { + width: 100%; + } + } + /* Removed the padding in the card dropdown overlay*/ + &.dropdown.card-dropdown { + .card-block-wrapper .card-block a { + padding: 0; + } + } + /* Added max-height and overflow auto for scollbar when the card item reach the dropdown + height */ + &.dropdownmoremenu .dropdown-menu .dropdown-menu { + max-height: 300px; + overflow-y: auto; + } + } + } + } + + .navbar { + /* Margin top has been added to the Header block to ensure that it does not conflict with the top + header. */ + &.fixed-top { + margin-top: 40px; + /* The position of the card dropdown block has been set to static, + so it will always appear under the card menu link. */ + .moremenu .dropdown.card-dropdown { + position: static; + .dropdown-menu { + overflow-y: auto; + } + &:not(.card-overflow-no-wrap) .dropdown-menu { + max-height: calc(100vh - 300px); + } + } + } + } + } + /* The width is added to show the navigation drawer in full page and + position of the navigation drawer has been adjusted to open the drawer from below. */ + ~ .drawer-bottom.drawer { + width: 100%; + top: 0; + padding-bottom: 101px; + visibility: hidden; + /* Visible on show when the drawer toggle "More" link is clicked */ + &.show { + visibility: visible; + } + } + /* The position of the main page has been adjusted slightly below to accommodate the top header. */ + ~ #page.drawers { + margin-top: 60px; + } + .moremenu .more-nav li.card-dropdown { + /* The position of the card dropdown block has been set to static, + so it will always appear under the card menu link. */ + position: static; + /* Card appearance - Card form style */ + /* Card form style - Square */ + &.card-form-square { + /* Minimun and maximum width is added when the Card size "Tiny" with card form "Square" */ + &.card-size-tiny .dropdown-menu .card-block { + min-width: 90px; + max-width: 90px; + } + /* Minimun and maximum width is added when the Card size "Small" with card form "Square" */ + &.card-size-small .dropdown-menu .card-block { + min-width: 140px; + max-width: 140px; + } + /* Minimun and maximum width is added when the Card size "Medium" with card form "Square" */ + &.card-size-medium .dropdown-menu .card-block { + min-width: 190px; + max-width: 190px; + } + /* Minimun and maximum width is added when the Card size "Large" with card form "Square" */ + &.card-size-large .dropdown-menu .card-block { + min-width: 240px; + max-width: 240px; + } + } + /* Card form style - Portrait */ + &.card-form-portrait { + /* Minimun and maximum width is added when the Card size "Tiny" with card form "portrait" */ + &.card-size-tiny .dropdown-menu .card-block { + min-width: 60px; + max-width: 60px; + } + /* Minimun and maximum width is added when the Card size "Small" with card form "portrait" */ + &.card-size-small .dropdown-menu .card-block { + min-width: 94px; + max-width: 94px; + } + /* Minimun and maximum width is added when the Card size "Medium" with card form "portrait" */ + &.card-size-medium .dropdown-menu .card-block { + min-width: 127px; + max-width: 127px; + } + /* Minimun and maximum width is added when the Card size "Large" with card form "portrait" */ + &.card-size-large .dropdown-menu .card-block { + min-width: 160px; + max-width: 160px; + } + } + /* Card form style - Landscape */ + &.card-form-landscape { + /* Minimun and maximum width is added when the Card size "Tiny" with card form "Landscape" */ + &.card-size-tiny .dropdown-menu .card-block { + min-width: 135px; + max-width: 135px; + } + /* Minimun and maximum width is added when the Card size "Small" with card form "Landscape" */ + &.card-size-small .dropdown-menu .card-block { + min-width: 210px; + max-width: 210px; + } + /* Minimun and maximum width is added when the Card size "Medium" with card form "Landscape" */ + &.card-size-medium .dropdown-menu .card-block { + min-width: 285px; + max-width: 285px; + } + /* Minimun and maximum width is added when the Card size "Large" with card form "Landscape" */ + &.card-size-large .dropdown-menu .card-block { + min-width: 360px; + max-width: 360px; + } + } + /* Card form style - Fullwidth */ + /* Full Width is added when the Card size "Tiny, Small, Medium and Large" with card form "Full width" */ + &.card-form-fullwidth { + &.card-size-tiny, + &.card-size-small, + &.card-size-medium, + &.card-size-large { + .dropdown-menu .card-block { + min-width: 100%; + width: 100%; + } + } + /* Card size "large" has margin right none because the size of the card is full width of the screen */ + &.card-overflow-wrap.card-size-large { + .dropdown-menu .card-block { + margin-right: 0; + } + } + } + } + /* Minimum width is adde for the dropdown, overlay y axis is visible to show the sub dropdown menus*/ + > div:not(.navbar-nav) .dropdown:not(.card-dropdown) .dropdown-menu { + min-width: 200px; + overflow-y: visible; + } + /* The icon tag aligned top at the menu link */ + .usermenu .dropdown-menu .submenu .items a.dropdown-item { + align-items: baseline; + } + } + .moremenu { + .more-nav li.nav-item { + /* Menu link icon aligned center */ + a { + align-items: center; + label { + margin-bottom: 0; + } + } + /* The style below added when the menu link is under the "More" option in the responsive alignment */ + /* The full width style is added for the menu link inside the "More" menu */ + &.dropdownmoremenu .dropdown-menu li.nav-item.dropdown { + width: 100%; + .dropdown-toggle { + /* Header dropdown toggle menu link color added */ + &:hover, + &:focus, + &:active { + color: $white; + } + } + /* Header dropdown menu border added and position added not to conflict + with the screen in the responsive alignment */ + .dropdown-menu { + border: 1px solid rgba(0, 0, 0, 0.15); + padding: 0.5rem 0; + position: absolute; + top: 0; + left: auto; + right: 100%; + /* Removed the background color for the menu link of the submenu link*/ + .dropdown-item { + &:not(:hover):not(:focus):not(:active) { + background: none; + } + } + } + &.card-dropdown { + .dropdown-menu { + min-width: 420px; + max-height: calc(100vh - 120px); + text-align: center; + } + &.card-form-fullwidth { + .dropdown-menu .card-block-wrapper { + padding: 0 15px; + } + } + } + } + } + .dropdown { + /* Top margin removed and overlay y axis is visible to show the dropdowm menu without the scrollbar */ + .dropdown-menu { + overflow-y: visible; + margin-top: 0; + /* Menu description font size, padding, border-radius and background color added */ + .menu-description { + font-size: 12px; + padding: 0.25rem 1.5rem; + border-radius: 5px; + background-color: #eee; + /* Removed the margin bottom for the content to avoid unwanted space below the description */ + p { + margin-bottom: 0; + } + } + /* Padding and Position added for the "help icon" in menu */ + .menu-helpicon { + text-align: center; + padding-right: 10px; + margin-bottom: 10px; + /* Margin removed in the menu description help icon in the menu */ + i { + margin: 0; + } + } + /* The font size and opacity reduced when the Menu link is selected as "Heading" option in the + settings */ + .dropdown-item { + /* Dropdown menu link alignment at top */ + align-items: baseline; + /* The "Tick" icon removed when the menu link is clicked */ + &:before { + display: none; + } + &.menu-item-heading { + font-size: 14px; + opacity: .6; + /* Menu link icon font size when the menu link as "Heading" and "icon" option selected in the + settings */ + i.icon { + font-size: 14px; + } + } + } + /* The below styles are header dropdown submenu */ + .dropdown-submenu { + /* Position relative submenu link to control the arrow icon when it has submenus */ + > a.dropdown-item { + position: relative; + /* Header dropdown submenu */ + &:before { + content: ''; + } + /* The arrow icon and its postion at the right side is added when the header menu which has + dropdown */ + &:after { + content: '\f105'; + font-family: fontawesome; + line-height: 10px; + position: absolute; + top: 40%; + right: 10px; + } + } + /* The submenu dropdown is hidden before the menu is clicked */ + ul { + display: none; + } + /* The position relative is added for the dropdown menu */ + &.show { + position: relative; + /* The dropdown submenu style is added for the header menu */ + /* Height added to control the menu with scrollbard, border, border radius, background color, + padding and its position styles are added and display block is added to show the submenu when + the dropdown menu is clicked */ + ul { + max-height: 250px; + list-style: none; + border: 1px solid $border-color; + border-radius: 0.5rem; + background-color: #fff; + padding: .5rem 0; + margin-left: 1px; + overflow-y: auto; + display: block; + position: absolute; + top: 0; + left: 100%; + /* The full width of the dropdown submenu link */ + li { + width: 100%; + /* The height and padding is added for the header dropdown submenu links */ + a.nav-link { + width: 100%; + height: auto; + padding: 0.1rem 1.5rem; + } + } + } + } + } + } + /* The top padding is removed in the dropdown menu when menu description is added in the top */ + &.dropdown-description-above .dropdown-menu { + padding-top: 0; + /* When menu description is added in the top the menu description bottom border radius is removed */ + .menu-description { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } + /* The bottom padding is removed in the dropdown menu when menu description is added in the below */ + &.dropdown-description-below .dropdown-menu { + padding-bottom: 0; + /* When menu description is added in the bottom the menu description top border radius is removed */ + .menu-description { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + /* Card layout - The below styles are header dropdown Card style menus */ + &.card-dropdown { + /* The header card dropdown menu width, padding, removed border radius and top margin, added + white background color and it's position styles */ + .dropdown-menu { + width: 100%; + border-radius: 0; + background-color: #f0efef; + padding: 30px; + margin-top: 0; + position: absolute; + top: 100%; + left: 0; + /* The header card dropdown menu items styles */ + /* Margin added to the card block right & bottom and vertical align added to + align the card block center to each other */ + .card-block { + margin-right: 15px; + margin-bottom: 15px; + display: inline-block; + vertical-align: top; + flex-direction: column; + /* The image parent div added for the card dropdown items */ + .img-block { + width: 100%; + height: 200px; + /* The image size added for the card dropdown items */ + img { + width: 100%; + height: 100%; + object-fit: cover; + } + > a { + padding: 0; + } + } + /* The card dropdown items content block width, padding, background color + and aligned center styles added */ + .content-block { + width: 100%; + padding: 10px; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + /* The margin added to the heading in the content block for the card dropdown items */ + h5 { + width: 100%; + margin-bottom: 0; + overflow: hidden; + /* The font style, color, paddign added to the link of the heading + in the content block for the card dropdown items */ + a { + font-size: 14px; + color: $white; + font-weight: bold; + padding: 0; + /* Background removed for the link of the heading on hover + in the content block for the card dropdown items */ + &:hover, + &:focus, + &:active { + background: none; + } + } + } + /* The line height added and "Go" button block aligned right side + in the content block for the card dropdown items */ + .btn-block { + width: auto; + line-height: 10px; + text-align: right; + margin-left: 5px; + a { + /* The color and padding added for the "Go" button in + the content block of the card dropdown items */ + color: #fff; + padding: 0; + &:hover, + &:focus, + &:active { + /* Background removed for the link of the button on + hover in the content block for the card dropdown items */ + background: none; + } + } + } + } + /* The below style is added when the Menu link as "Heading" in "card layout" Menu settings */ + &.menu-item-heading { + /* The Card menu as "Heading" option content block style */ + .content-block { + /* The Card menu content heading menu link font-size and opacity added */ + h5 a { + font-size: 16px; + opacity: .6; + /* The Card menu content heading menu link icon font-size added */ + i { + font-size: 16px; + } + } + /* The Card Menu as "Heading" option content block "Go" button link is hidden */ + .btn-block { + display: none; + } + } + } + /* The "Text position" of the menu link is selected as "Top overlay" + or "Below overlay" in the "Card Appearance" settings */ + &.card-text-overlay-top, + &.card-text-overlay-bottom { + /* The position relative to the card dropdown */ + position: relative; + /* The border, background, box-shadow, outline are removed for the card overlay link and + the width, height, position is added to redirect to course for the card top overlay & + card bottom overlay */ + > a { + width: 100%; + height: 100%; + border: 0; + background: none; + box-shadow: none; + outline: none; + position: absolute; + top: 0; + left: 0; + z-index: 1; + } + /* The width and position of the menu link at the top of the card block in the card + dropdown menu */ + .content-block { + width: 100%; + height: 100%; + align-items: flex-start; + position: absolute; + top: 0; + left: 0; + } + } + /* The "Text position" of the menu link bottom + of the card block in the "Below overlay" option */ + &.card-text-overlay-bottom { + .content-block { + align-items: flex-end; + } + } + } + } + /* Menu description aligned center and margin top & bottom added in the Card drowpdown menu */ + &.dropdown-description-above, + &.dropdown-description-below { + .dropdown-menu .menu-description { + text-align: center; + margin: 10px 0; + } + } + /* Card appearance - Card size */ + /* Height of the Card size - Tiny */ + &.card-size-tiny .dropdown-menu .card-block .img-block { + height: 50px; + } + /* Height of the Card size - Small */ + &.card-size-small .dropdown-menu .card-block .img-block { + height: 100px; + } + /* Height of the Card size - Medium */ + &.card-size-medium .dropdown-menu .card-block .img-block { + height: 150px; + } + /* Height of the Card size - large */ + &.card-size-large .dropdown-menu .card-block .img-block { + height: 200px; + } + /* Card Overflow behaviour - No Wrap */ + /* Displayed the card menu as flex, nowrap and horizontal scroll bar to show the dropdown menu in a + single row */ + &.card-overflow-no-wrap.show .dropdown-menu { + overflow-x: auto; + display: flex; + flex-wrap: nowrap; + flex-direction: column; + /* Scrollbar */ + /* Width and height of the scrollbar */ + ::-webkit-scrollbar { + width: 12px; + height: 10px; + } + /* Background color for the scrollbar on hover */ + ::-webkit-scrollbar-thumb:hover { + background-color: #495057; + } + /* Border, border radius and background color for the bar in the scrollbar */ + ::-webkit-scrollbar-thumb { + background-color: #6a737b; + border-radius: 20px; + border: 2px solid #f8f9fa; + } + /* Background for the scrollbar */ + ::-webkit-scrollbar-track { + background: #f8f9fa; + } + /* End of Scrollbar */ + .card-block-wrapper { + display: flex; + overflow-y: auto; + } + } + /* Card size - "Large" with the "Text positions" */ + /* Increased the height of the card block in the "Text Position" when the card size Large */ + &.card-size-large .dropdown-menu .card-block { + &.card-text-overlay-top, + &.card-text-overlay-bottom { + > a, + .img-block, + .content-block { + height: calc(200px + 39px); + } + } + } + /* Card size - "Medium" with the "Text positions" */ + /* Increased the height of the card block in the "Text Position" when the card size Medium */ + &.card-size-medium .dropdown-menu .card-block { + &.card-text-overlay-top, + &.card-text-overlay-bottom { + > a, + .img-block, + .content-block { + height: calc(150px + 39px); + } + } + } + /* Card size - "Small" with the "Text positions" */ + /* Increased the height of the card block in the "Text Position" when the card size Small */ + &.card-size-small .dropdown-menu .card-block { + &.card-text-overlay-top, + &.card-text-overlay-bottom { + > a, + .img-block, + .content-block { + height: calc(100px + 39px); + } + } + } + /* Card size - "Tiny" with the "Text positions" */ + /* Increased the height of the card block in the "Text Position" when the card size Tiny */ + &.card-size-tiny .dropdown-menu .card-block { + &.card-text-overlay-top, + &.card-text-overlay-bottom { + > a, + .img-block, + .content-block { + height: calc(50px + 39px); + } + } + } + } + } + } +} +.drawer-primary .drawercontent .list-group .list-group-item { + align-items: center; +} + +/* The nav drawer position to the top in the responsive alignment */ +@include media-breakpoint-down(md) { + .navbar.fixed-top.boost-union-menubar ~ .drawer { + top: 40px; + &.drawer-right { + top: 0; + } + } +} + +/* The nav drawer position to the top in the responsive alignment */ +@include media-breakpoint-up(md) { + .navbar.fixed-top.boost-union-menubar ~ #page.drawers .drawer-toggles .drawer-toggler { + top: 110px; + } +} + +@include media-breakpoint-down(sm) { + .navbar.fixed-top { + /* The header z-index is increased to show at top when the footer bottom menu is added */ + &.smartmenu-bottom-navigation { + z-index: 1050; + } + /* The Header menubar z-index is increased to show at top in responsive */ + &.boost-union-menubar { + &.smartmenu-bottom-navigation { + z-index: 1051; + } + ~ .drawer.drawer-right { + z-index: 1052; + } + /* When the menu in the footer bottom is clicked, its navigation drawer + position appears below the header. */ + ~ .drawer-bottom.drawer { + top: 101px; + } + } + /* When the menu in the footer bottom is clicked, its navigation drawer + position appears below the header when the header menubar is disabled. */ + ~ .drawer-bottom.drawer { + top: 61px; + /* The nav drawer header(close button) is hidden is responsive */ + .drawerheader { + display: none; + } + /* The nav drawer content font size, weight and padding is added*/ + .drawercontent .menu-title { + font-size: 18px; + font-weight: bold; + padding: 15px; + } + } + /* When the menu in the left adn right drawer toggle button is clicked, its navigation drawer + position appears below the header when the header menubar is disabled. */ + ~ .drawer.drawer-left, + ~ .drawer.drawer-right { + max-width: 315px; + padding-bottom: 0; + /* top: 61px; */ + } + /*~ #page.drawers { + The position of the drawer button to toggle has been adjusted slightly + below on the course view page to accommodate the top header. + .drawer-toggles .drawer-left-toggle, + .drawer-toggles .drawer-right-toggle { + top: calc(60px + 0.7rem); + } + }*/ + } +} + + +/*--------------------------------------- + * Footer menu + --------------------------------------*/ + +#page-wrapper #page { + #page-footer { + .navbar { + /* Footer bottom menu */ + /* Padding space removed around the bottom menu block, box-shadow and z-index + to view the bottom menu at the top added*/ + &.boost-union-bottom-menu { + padding: 0; + box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.1); + z-index: 1050; + .bottom-navigation .more-nav { + > li { + /* The footer bottom dropdown menu icon and text are displayed in separate rows. */ + &.dropdown > a { + flex-direction: column; + /* The footer bottom dropdown menu icon margin is removed. */ + i { + margin: 0 0 3px; + } + } + /* The footer bottom menu item border bottom is removed when it's not active. */ + > a { + &.dropdown-toggle { + padding-right: 25px; + } + &:not(.active) { + border-bottom: 0; + flex-wrap: wrap; + flex-direction: column; + justify-content: center; + position: relative; + i { + margin: 0; + } + &:after { + margin-left: 5px; + position: absolute; + right: 0; + } + } + } + } + li { + &:first-child, + &:nth-child(2), + &:nth-child(3) { + &.dropdown .dropdown-menu .dropdown-submenu ul { + left: 100%; + right: auto; + } + } + &.dropdown { + /* Footer bottom dropdown menu width, height and removed margin, position to stay the + dropup menu */ + .dropdown-menu { + width: auto; + margin-top: 0; + margin-bottom: 0; + top: auto; + bottom: 100%; + left: auto; + right: 0; + /* The footer bottom dropdown submenu min-width, max-height, overflow and position + to show the menus above the footer */ + .dropdown-submenu ul { + min-width: 150px; + max-height: calc(100vh - 300px); + overflow: auto; + left: auto; + right: 100%; + bottom: 0; + top: auto; + } + } + /* Footer bottom dropdown style when the menus are in the "More" option in the responsive + alignment */ + &.dropdownmoremenu { + display: flex; + align-items: center; + /* The footer bottom dropdown menu icon and text are displayed in separate rows. */ + > a.dropdown-toggle { + flex-direction: column; + /* Added ellipsis fontawesome icon, and diplay block to for the dropdown more + menu when the smart menus are not added in the Footer menu */ + &:before { + content: '\f142'; + font-family: fontawesome; + } + /* The arrow icon after the More menu link text is hidden when the smart menus are + not added */ + &:after { + display: none; + } + } + /* Footer bottom dropdown toggle button height, font-size, color, align center, + removed border radius, padding and added margin left & right style */ + button.navbar-toggler { + height: 100%; + font-size: calc(0.90375rem + 0.045vw); + color: $primary; + text-align: center; + border-radius: 0; + padding-top: 0; + padding-bottom: 0; + margin: 0 10px; + /* Footer bottom dropdown toggle button baclground added on hover */ + &:hover { + font-size: calc(0.90375rem + 0.045vw); + background: #f8f9fa; + } + /* More menu toggle button icon margin remove in the mobile view */ + i { + margin: -5px auto 5px; + display: block; + } + } + } + } + /* Footer bottom menu card-dropdown */ + /* The card dropdown Position is changed to static, so the dropdown menu will be in + fullscreen */ + &.card-dropdown { + position: static; + /* Card appearance - Card form style */ + /* Card form style - Square */ + &.card-form-square { + /* Minimun and maximum width is added when the Card size "Tiny" with card form + "Square" */ + &.card-size-tiny .dropdown-menu .card-block { + min-width: 90px; + max-width: 90px; + } + /* Minimun and maximum width is added when the Card size "Small" with card form + "Square" */ + &.card-size-small .dropdown-menu .card-block { + min-width: 140px; + max-width: 140px; + } + /* Minimun and maximum width is added when the Card size "Medium" with card form + "Square" */ + &.card-size-medium .dropdown-menu .card-block { + min-width: 190px; + max-width: 190px; + } + /* Minimun and maximum width is added when the Card size "Large" with card form + "Square" */ + &.card-size-large .dropdown-menu .card-block { + min-width: 240px; + max-width: 240px; + } + } + /* Card form style - Portrait */ + &.card-form-portrait { + /* Minimun and maximum width is added when the Card size "Tiny" with card form + "portrait" */ + &.card-size-tiny .dropdown-menu .card-block { + min-width: 60px; + max-width: 60px; + } + /* Minimun and maximum width is added when the Card size "Small" with card form + "portrait" */ + &.card-size-small .dropdown-menu .card-block { + min-width: 94px; + max-width: 94px; + } + /* Minimun and maximum width is added when the Card size "Medium" with card form + "portrait" */ + &.card-size-medium .dropdown-menu .card-block { + min-width: 127px; + max-width: 127px; + } + /* Minimun and maximum width is added when the Card size "Large" with card form + "portrait" */ + &.card-size-large .dropdown-menu .card-block { + min-width: 160px; + max-width: 160px; + } + } + /* Card form style - Landscape */ + &.card-form-landscape { + /* Minimun and maximum width is added when the Card size "Tiny" with card form + "Landscape" */ + &.card-size-tiny .dropdown-menu .card-block { + min-width: 135px; + max-width: 135px; + } + /* Minimun and maximum width is added when the Card size "Small" with card form + "Landscape" */ + &.card-size-small .dropdown-menu .card-block { + min-width: 210px; + max-width: 210px; + } + /* Minimun and maximum width is added when the Card size "Medium" with card form + "Landscape" */ + &.card-size-medium .dropdown-menu .card-block { + min-width: 285px; + max-width: 285px; + } + /* Minimun and maximum width is added when the Card size "Large" with card form + "Landscape" */ + &.card-size-large .dropdown-menu .card-block { + min-width: 360px; + max-width: 360px; + } + } + /* Card form style - Fullwidth */ + /* Full Width is added when the Card size "Tiny, Small, Medium and Large" + with card form "Full width" */ + &.card-form-fullwidth { + &.card-size-tiny, + &.card-size-small, + &.card-size-medium, + &.card-size-large { + .dropdown-menu .card-block { + min-width: 100%; + width: 100%; + } + } + /* Card size "large" has margin right none because the size of the card is full + width of the screen */ + &.card-overflow-wrap.card-size-large { + .dropdown-menu .card-block { + margin-right: 0; + } + } + } + /* The card dropdown menu width is 100%, so the dropdown menu will not overflow the + screen */ + .dropdown-menu { + width: 100%; + max-height: calc(100vh - 300px); + overflow-y: auto; + } + } + /* The position of the footer dropdown menu should not extend beyond the visible area of + the screen */ + &:first-child.dropdown .dropdown-menu { + left: 0; + right: auto; + } + } + } + } + } + } +} +/* End of Footer Menu */ + +@include media-breakpoint-down(sm) { + #page-wrapper #page { + &.footer-bottom-menu { + /* Increases the space at the bottom for the footer popover, as the bottom menu bar might hide it. */ + .btn-footer-popover { + bottom: 4rem; + } + } + } +} + +@include media-breakpoint-down(sm) { + /* The padding for the footer to visible the footer bottom menu because it is in the position style */ + #page-wrapper #page .footer-bottom-menu { + padding-bottom: 75px; + } + /* The nav drawer width is extend to full screen in the mobile view */ + body .drawer.show { + max-width: none; + width: 100%; + } + .drawer-primary .drawercontent .list-group .list-group-item { + /* The padding left is added to the nav drawer menu dropdown menu in the mobile view */ + .list-group-item-action { + padding-left: 32px; + align-items: baseline; + } + /* The margin left is added to the dropdown submenu icon for the nav drawer menu in the mobile view */ + .list-group-item .list-group-item-action i { + margin-left: 25px; + } + } +} + +/*======================================= + * General styling + ======================================*/ + +/*--------------------------------------- + * Form element: Fontawesome picker + --------------------------------------*/ + +.fontawesome-picker { + /* Padding removed in the fontawesome picker popover. */ + .popover-body { + padding: 0; + + /* Width, height, padding added, margin + removed and text aligned centered in the popover fontawesome icon picker. */ + .fontawesome-icon-suggestions { + width: 250px; + height: 350px; + list-style: none; + text-align: center; + overflow: auto; + padding: 10px 2px 2px 10px; + margin: 0; + + /* Width, height, line height, border, border radius and margin + added for the icon item in the popover fontawesome icon picker */ + li { + width: 40px; + height: 40px; + line-height: 40px; + border-radius: 5px; + border: 1px solid #eee; + margin: 0 10px 10px 0; + display: inline-block; + cursor: pointer; + + /* Background color added on hover and selected for the font icon item. */ + &:hover, + &.selected { + background-color: #f4f4f4; + } + + /* Width, height, font size added and margin removed for the Font icon. */ + .icon { + width: 14px; + height: 14px; + font-size: 16px; + margin: 0; + } + } + } + } +} + + +/*--------------------------------------- + * Form element: Color picker + --------------------------------------*/ + +/* Display the color picker box and the text field as column. */ +.theme_boost_union-form-colour-picker .felement { + flex-direction: column; +} + + /*======================================= * Supporting third-party plugins ======================================*/ +/*--------------------------------------- + * Learning Tools + --------------------------------------*/ + +/* On smaller screens. */ +@include media-breakpoint-down(sm) { + /* Move the learning tools floating button up as soon as the bottom menu is shown. */ + #page.footer-bottom-menu .learningtools-action-info .floating-button { + bottom: 4rem; + } +} + + /*--------------------------------------- * Dash Pro --------------------------------------*/ @@ -972,7 +2060,7 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } /* On not-so-large screens. */ - @include media-breakpoint-down(lg) { /* This means less than 300px per outside block region. */ + @include media-breakpoint-down(lg) { /* This means less than 992px per outside block region. */ /* Stack content vertically. */ .main-inner-outside-nextmaincontent { display: flex; diff --git a/settings.php b/settings.php index 7cfcfe45146..7c27d974200 100644 --- a/settings.php +++ b/settings.php @@ -85,6 +85,14 @@ new moodle_url('/theme/boost_union/flavours/overview.php'), 'theme/boost_union:configure'); $ADMIN->add('theme_boost_union', $flavourspage); + + // Create Smart Menus settings page as external page. + // (and allow users with the theme/boost_union:configure capability to access it). + $smartmenuspage = new admin_externalpage('theme_boost_union_smartmenus', + get_string('smartmenus', 'theme_boost_union', null, true), + new moodle_url('/theme/boost_union/smartmenus/menus.php'), + 'theme/boost_union:configure'); + $ADMIN->add('theme_boost_union', $smartmenuspage); } // Create full settings page structure. @@ -1067,7 +1075,9 @@ // Setting: Hide nodes in primary navigation. $name = 'theme_boost_union/hidenodesprimarynavigation'; $title = get_string('hidenodesprimarynavigationsetting', 'theme_boost_union', null, true); - $description = get_string('hidenodesprimarynavigationsetting_desc', 'theme_boost_union', null, true); + $smartmenuurl = new moodle_url('/theme/boost_union/smartmenus/menus.php'); + $description = get_string('hidenodesprimarynavigationsetting_desc', 'theme_boost_union', + array('url' => $smartmenuurl), true); $setting = new admin_setting_configmulticheckbox($name, $title, $description, array(), $hidenodesoptions); $setting->set_updatedcallback('theme_reset_all_caches'); $tab->add($setting); diff --git a/smartmenus/edit.php b/smartmenus/edit.php new file mode 100644 index 00000000000..0d729e10a0e --- /dev/null +++ b/smartmenus/edit.php @@ -0,0 +1,110 @@ +. + +/** + * Theme boost union - Edit menu. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Require config. +require(__DIR__.'/../../../config.php'); + +// Require plugin libraries. +require_once($CFG->dirroot.'/theme/boost_union/locallib.php'); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +// Get parameters. +$id = optional_param('id', null, PARAM_INT); + +// Get system context. +$context = context_system::instance(); + +// Access checks. +require_login(); +require_sesskey(); +require_capability('theme/boost_union:configure', $context); + +// Prepare the page. +$PAGE->set_context($context); +$PAGE->set_url(new moodle_url('/theme/boost_union/smartmenus/edit.php', array('id' => $id, 'sesskey' => sesskey()))); +$PAGE->set_cacheable(false); +$PAGE->navbar->add(get_string('themes', 'core'), new moodle_url('/admin/category.php', array('category' => 'themes'))); +$PAGE->navbar->add(get_string('pluginname', 'theme_boost_union'), new moodle_url('/admin/category.php', + array('category' => 'theme_boost_union'))); +$PAGE->navbar->add(get_string('smartmenus', 'theme_boost_union'), new moodle_url('/theme/boost_union/smartmenus/menus.php')); +$PAGE->set_title(theme_boost_union_get_externaladminpage_title(get_string('smartmenus', 'theme_boost_union'))); +if ($id !== null && $id > 0) { + $PAGE->set_heading(get_string('smartmenuseditmenu', 'theme_boost_union')); + $PAGE->navbar->add(get_string('smartmenusedit', 'theme_boost_union')); +} else { + $PAGE->set_heading(get_string('smartmenuscreatemenu', 'theme_boost_union')); + $PAGE->navbar->add(get_string('smartmenuscreate', 'theme_boost_union')); +} + +// Init form. +$form = new \theme_boost_union\form\smartmenu_form(null, array('id' => $id)); + +// If the form was submitted. +if ($data = $form->get_data()) { + // Handle form results. + $menuid = theme_boost_union\smartmenu::manage_instance($data); + + // After the menu data was saved, let's redirect to configure items for this menu. + if (isset($data->saveanddisplay) && $data->saveanddisplay) { + redirect(new moodle_url('/theme/boost_union/smartmenus/items.php', array('menu' => $menuid))); + + // Otherwise. + } else { + // Redirect to menu list. + redirect(new moodle_url('/theme/boost_union/smartmenus/menus.php')); + } + + // Otherwise if the form was cancelled. +} else if ($form->is_cancelled()) { + // Redirect to menu list. + redirect(new moodle_url('/theme/boost_union/smartmenus/menus.php')); +} + +// If a menu ID is given. +if ($id !== null && $id > 0) { + // Fetch the data for the menu. + if ($record = theme_boost_union\smartmenu::get_menu($id)) { + // Set the menu data to the menu edit form. + $form->set_data($record); + + // If the menu is not available. + } else { + // Add a notification to the page. + \core\notification::error(get_string('error:smartmenusmenunotfound', 'theme_boost_union')); + + // Redirect to menu list (where the notification is shown). + redirect(new moodle_url('/theme/boost_union/smartmenus/menus.php')); + } +} + +// Start page output. +echo $OUTPUT->header(); + +// Show form. +echo $form->display(); + +// Finish page output. +echo $OUTPUT->footer(); diff --git a/smartmenus/edit_items.php b/smartmenus/edit_items.php new file mode 100644 index 00000000000..5ce0bc82f62 --- /dev/null +++ b/smartmenus/edit_items.php @@ -0,0 +1,143 @@ +. + +/** + * Theme boost union - Edit menu items. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Require config. +require(__DIR__.'/../../../config.php'); + +// Require plugin libraries. +require_once($CFG->dirroot.'/theme/boost_union/locallib.php'); + +// Require file library. +require_once($CFG->dirroot.'/lib/filelib.php'); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +// Get parameters. +$id = optional_param('id', null, PARAM_INT); +$menuid = optional_param('menu', null, PARAM_INT); +$action = optional_param('action', null, PARAM_ALPHAEXT); + +// Get system context. +$context = context_system::instance(); + +// Access checks. +require_login(); +require_sesskey(); +require_capability('theme/boost_union:configure', $context); + +// Verify the existence of the menu and menu item. +if ($menuid == null && $id !== null) { + // Verify the menu item exists. Get the menu from item id. + $item = $DB->get_record('theme_boost_union_menuitems', ['id' => $id]); + if (!$menu = $DB->get_record('theme_boost_union_menus', ['id' => $item->menu])) { + throw new moodle_exception('error:smartmenusmenuitemnotfound', 'theme_boost_union'); + } +} else { + // Verify the menu exists. + $menu = $DB->get_record('theme_boost_union_menus', ['id' => $menuid]); + if (!$menu) { + throw new moodle_exception('error:smartmenusmenunotfound', 'theme_boost_union'); + } +} + +// Prepare the page. +$PAGE->set_context($context); +$PAGE->set_url(new moodle_url('/theme/boost_union/smartmenus/edit_items.php', array('menu' => $menu->id, 'sesskey' => sesskey()))); +$PAGE->navbar->add(get_string('themes', 'core'), new moodle_url('/admin/category.php', array('category' => 'themes'))); +$PAGE->navbar->add(get_string('pluginname', 'theme_boost_union'), new moodle_url('/admin/category.php', + array('category' => 'theme_boost_union'))); +$PAGE->navbar->add(get_string('smartmenus', 'theme_boost_union'), new moodle_url('/theme/boost_union/smartmenus/menus.php')); +$PAGE->navbar->add(get_string('smartmenusmenuitems', 'theme_boost_union'), new moodle_url('/theme/boost_union/smartmenus/items.php', + array('menu' => $menu->id))); +$PAGE->set_title(theme_boost_union_get_externaladminpage_title(get_string('smartmenus', 'theme_boost_union'))); +if ($menuid == null && $id !== null) { + $PAGE->set_heading(get_string('smartmenuseditmenuitem', 'theme_boost_union')); + $PAGE->navbar->add(get_string('smartmenusedititem', 'theme_boost_union')); +} else { + $PAGE->set_heading(get_string('smartmenuscreatemenuitem', 'theme_boost_union')); + $PAGE->navbar->add(get_string('smartmenuscreateitem', 'theme_boost_union')); +} + +// Prepare draft item id for the form. +$draftitemid = file_get_submitted_draft_itemid('image'); +file_prepare_draft_area($draftitemid, $context->id, 'theme_boost_union', 'smartmenus_itemimage', $id, + theme_boost_union\smartmenu_item::image_fileoptions() +); + +// Prepare the next menu item id based on the last item ID (if no menu items exist yet, the last item is 0). +$lastmenu = theme_boost_union\smartmenu_item::get_lastitem($menu->id); +$nextorder = isset($lastmenu->sortorder) ? $lastmenu->sortorder + 1 : 1; + +// Init form. +$formurl = new moodle_url('/theme/boost_union/smartmenus/edit_items.php', array('menu' => $menu->id, 'sesskey' => sesskey())); +$form = new \theme_boost_union\form\smartmenu_item_form($formurl->out(false), array( + 'id' => $id, + 'menu' => $menu->id, + 'nextorder' => $nextorder, +)); + +// If the form was submitted. +if ($data = $form->get_data()) { + // Handle form results. + $result = theme_boost_union\smartmenu_item::manage_instance($data); + if ($result) { + // Redirect to menu item list. + redirect(new moodle_url('/theme/boost_union/smartmenus/items.php', array('menu' => $menu->id))); + } + + // Otherwise if the form was cancelled. +} else if ($form->is_cancelled()) { + // Redirect to menu item list. + redirect(new moodle_url('/theme/boost_union/smartmenus/items.php', array('menu' => $menu->id))); +} + +// If a menu item ID is given. +if ($id !== null && $id > 0) { + // Fetch the data for the menu item. + if ($record = theme_boost_union\smartmenu_item::instance($id)->get_item()) { + // Get an unused draft item id which will be used for this form. + $record->image = $draftitemid; + + // Set the menu data to the menu edit form. + $form->set_data($record); + + // If the menu item is not available. + } else { + // Add a notification to the page. + \core\notification::error(get_string('error:smartmenusmenuitemnotfound', 'theme_boost_union')); + + // Redirect to menu item list (where the notification is shown). + redirect(new moodle_url('/theme/boost_union/smartmenus/items.php', array('menu' => $menu->id))); + } +} + +// Start page output. +echo $OUTPUT->header(); + +// Show form. +echo $form->display(); + +// Finish page output. +echo $OUTPUT->footer(); diff --git a/smartmenus/items.php b/smartmenus/items.php new file mode 100644 index 00000000000..b13c3752a7a --- /dev/null +++ b/smartmenus/items.php @@ -0,0 +1,172 @@ +. + +/** + * Theme Boost Union - Menu items page. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Require config. +require(__DIR__.'/../../../config.php'); + +// Require plugin libraries. +require_once($CFG->dirroot. '/theme/boost_union/smartmenus/menulib.php'); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +// Get parameters. +$action = optional_param('action', null, PARAM_ALPHAEXT); +$menuid = optional_param('menu', null, PARAM_INT); +$id = optional_param('id', null, PARAM_INT); + +// Verify the existence of the menu and menu item. +if ($menuid == null && $id !== null) { + // Verify the menu item exists. Get the menu from item id. + $item = $DB->get_record('theme_boost_union_menuitems', ['id' => $id]); + if (!$menu = $DB->get_record('theme_boost_union_menus', ['id' => $item->menu])) { + throw new moodle_exception('error:smartmenusmenuitemnotfound', 'theme_boost_union'); + } +} else { + // Verify the menu exists. + $menu = $DB->get_record('theme_boost_union_menus', ['id' => $menuid]); + if (!$menu) { + throw new moodle_exception('error:smartmenusmenunotfound', 'theme_boost_union'); + } +} + +// Compose the page URL. +$pageurl = new moodle_url('/theme/boost_union/smartmenus/items.php', ['menu' => $menu->id]); + +// Get system context. +$context = context_system::instance(); + +// Access checks. +require_login(); +require_capability('theme/boost_union:configure', $context); + +// Prepare the page (to make sure that all necessary information is already set even if we just handle the actions as a start). +$PAGE->set_context($context); +$PAGE->set_url($pageurl); +$PAGE->set_cacheable(false); +$PAGE->navbar->add(get_string('themes', 'core'), new moodle_url('/admin/category.php', array('category' => 'themes'))); +$PAGE->navbar->add(get_string('pluginname', 'theme_boost_union'), new moodle_url('/admin/category.php', + array('category' => 'theme_boost_union'))); +$PAGE->navbar->add(get_string('smartmenus', 'theme_boost_union'), new moodle_url('/theme/boost_union/smartmenus/menus.php')); +$PAGE->navbar->add(get_string('smartmenusmenuitems', 'theme_boost_union'), new moodle_url('/theme/boost_union/smartmenus/items.php', + array('menu' => $menu->id))); + +// Process actions. +if ($action !== null && confirm_sesskey()) { + // Every action is based on a menu, thus the menu ID param has to exist. + $id = required_param('id', PARAM_INT); + + // Create menu instance. Actions are performed in smartmenu instance. + $item = new theme_boost_union\smartmenu_item($id); + + $transaction = $DB->start_delegated_transaction(); + + // Perform the requested action. + switch ($action) { + // Triggered action is delete, then init the deletion of menu. + case 'delete': + // Delete the menu. + if ($item->delete_menuitem()) { + // Notification to user for menu deleted success. + \core\notification::success(get_string('smartmenusmenudeleted', 'theme_boost_union')); + } + break; + // Move the menu order to down. + case "movedown": + // Move the item downwards. + $item->move_downward(); + break; + case "moveup": + // Move the item upwards. + $item->move_upward(); + break; + case "copy": + // Duplicate the item. + $item->duplicate(); + break; + case "hide": + // Hide the item from menu. + $item->update_field('visible', false); + break; + case "show": + // Show the item in menu. + $item->update_field('visible', true); + break; + } + + // Allow to update the changes to database. + $transaction->allow_commit(); + + // Redirect to the items page to remove the params from the URL. + redirect($pageurl); +} + +// Further prepare the page. +$PAGE->set_title(theme_boost_union_get_externaladminpage_title(get_string('smartmenus', 'theme_boost_union'))); +$PAGE->set_heading(theme_boost_union_get_externaladminpage_heading()); + +// Build smart menu items table. +$table = new theme_boost_union\table\smartmenus_items($menu->id); +$table->define_baseurl($PAGE->url); + +// Start page output. +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('smartmenus', 'theme_boost_union')); +if (isset($menu->title)) { + $menuheading = $menu->title; + $settingstitle = get_string('smartmenussettings', 'theme_boost_union'); + $settingsurl = new moodle_url('/theme/boost_union/smartmenus/edit.php', array('id' => $menuid, 'sesskey' => sesskey())); + $menuheading .= html_writer::link($settingsurl, + $OUTPUT->pix_icon('t/edit', $settingstitle, 'moodle', array('class' => 'ml-2'))); + echo $OUTPUT->heading($menuheading, 4); +} + +// Prepare 'Create smart menu item' buttons. +$createbutton = $OUTPUT->box_start(); +$createbutton .= $OUTPUT->single_button( + new \moodle_url('/theme/boost_union/smartmenus/edit_items.php', ['menu' => $menuid, 'sesskey' => sesskey()]), + get_string('smartmenusaddnewitem', 'theme_boost_union'), 'get'); +$createbutton .= $OUTPUT->box_end(); + +// If there aren't any smart menu items yet. +$countitems = $DB->count_records('theme_boost_union_menuitems'); +if ($countitems < 1) { + // Show the table, which, since it is empty, falls back to the + // "There aren't any items added to this smart menu yet. Please add an item to this menu." notice. + $table->out(0, true); + + // And then show the button. + echo $createbutton; + + // Otherwise. +} else { + // Show the button. + echo $createbutton; + + // And then show the table. + $table->out(50, true); +} + +// Finish page output. +echo $OUTPUT->footer(); diff --git a/smartmenus/menulib.php b/smartmenus/menulib.php new file mode 100644 index 00000000000..fc926d2a30e --- /dev/null +++ b/smartmenus/menulib.php @@ -0,0 +1,655 @@ +. + +/** + * Theme Boost Union - Smart menu Helper. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot. '/theme/boost_union/locallib.php'); + +use theme_boost_union\smartmenu; +use theme_boost_union\smartmenu_item; + +/** + * Smartmenu helper which contains the methods to verify the access rules for menu and its items. + * + * It contains the method to purge caches of menu and items for different events. + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class smartmenu_helper { + + /** + * Data record of the item or menu. + * + * @var stdclass + */ + public $data; + + /** + * Current loggedin user id. + * + * @var int + */ + public $userid; + + /** + * Smartmenu helper - Constructor. + * + * This class mainly verifies the access rules of the menu or item are passed for the current user. + * Use the item DB record data if want to verify for item, use menu data if want to verify menu. + * + * @param stdclass $data the item DB record data if want to verify for item, use menu data if want to verify menu. + */ + public function __construct($data) { + global $USER; + + $this->data = $data; + // Verify for the current user. + $this->userid = $USER->id; + } + + /** + * Find the user visibility access. + * + * Genreate and join the multiple restrictions conditions based Queries and fetch the records using the query, + * User will have access if records found otherwise user will restricted. + * + * @return bool Returns true if access rules are passed, otherfise its false. + */ + public function verify_access_restrictions() { + global $DB; + + if (!$this->data->visible) { + return false; + } + + $query = (object) [ + 'select' => ['u.*'], + 'join' => [], + 'where' => [], + 'params' => ['userid' => $this->userid], + ]; + + // Restriction by roles. + $this->restriction_byroles($query); + + // Restriction by cohorts. + $this->restriction_bycohorts($query); + + // REstriction by lanuages. + $this->restriction_bylanguage($query); + + // Restriction by date. Menu configured date is not started or already ended then hide the menu. + if (!$this->restriction_bydate()) { + return false; + } + + // Join the query object. + $select = implode(',', array_filter($query->select)); + $join = implode('', array_filter($query->join)); + $where = implode(' AND ', array_filter($query->where)); + $params = array_merge($query->params); + + // Show the menus and item, if it doesn't contain any restrictions. + if (empty($where)) { + return true; + } + + // Build the query from query object from different restrictions. + $sql = " SELECT $select FROM {user} u $join"; + $sql .= $where ? " WHERE u.id=:userid AND $where " : ' WHERE u.id=:userid '; + + $records = $DB->get_records_sql($sql, $params); + // Records found user will have access otherwise restrict the user to view the menu or menu item. + return count($records) > 0 ? true : false; + } + + /** + * Generate the queries to verify the user has selected role. + * + * @param stdclass $query Array which contains elements for DB conditions, selectors and params. + * @return void + */ + public function restriction_byroles(&$query) { + global $DB; + + $roles = $this->data->roles; + // Roles not mentioned then stop the role check. + if ($roles == '' || empty($roles)) { + return true; + } + + list($insql, $inparam) = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'rl'); + + $contextsql = ($this->data->rolecontext == smartmenu::SYSTEMCONTEXT) + ? ' AND contextid=:systemcontext ' : ''; + + $query->where[] = " u.id IN (SELECT userid FROM {role_assignments} WHERE roleid $insql AND userid=:rluserid $contextsql)"; + $params = [ + 'rluserid' => $this->userid, + 'systemcontext' => context_system::instance()->id + ]; + $query->params += array_merge($params, $inparam); + + } + + /** + * The purpose of this function is to check if a user is assigned to one or more cohorts that are specified in a menu. + * For the operator "ALL" it gets the count of records and verfiy the records count is same as count of selected cohorts. + * + * @param stdclass $query Array which contains elements for DB conditions, selectors and params. + * @return void + */ + public function restriction_bycohorts(&$query) { + global $DB; + + $cohorts = $this->data->cohorts; + + if ($cohorts == '' || empty($cohorts)) { + return true; + } + // Build insql to confirm the user cohort is available in the configured cohort. + list($insql, $inparam) = $DB->get_in_or_equal($cohorts, SQL_PARAMS_NAMED, 'ch'); + + // If operator is all then check the count of user assigned cohorts, + // Confirm the count is same as configured menu/items cohorts count. + $condition = ($this->data->operator == smartmenu::ALL) ? " GROUP BY cm.userid HAVING COUNT(DISTINCT c.id) = :chcount" : ''; + + $sql = " JOIN (SELECT count(*) AS member FROM {cohort_members} cm + JOIN {cohort} c ON cm.cohortid = c.id + WHERE c.id $insql AND cm.userid=:chuserid $condition) ch ON true"; + + $params = ['chuserid' => $this->userid, 'chcount' => count($cohorts)] + $inparam; + $query->params += $params; + $query->join[] = $sql; + $query->where[] = ' ch.member <> 0 '; + } + + /** + * Generate the queries and params to verify the user has the language which is contained in the current data. + * + * @param stdclass $query Array which contains elements for DB conditions, selectors and params. + * @return void + */ + public function restriction_bylanguage(&$query) { + global $DB; + + $languages = $this->data->languages; + if (empty($languages)) { + return true; + } + list($insql, $inparam) = $DB->get_in_or_equal($languages, SQL_PARAMS_NAMED, 'la'); + + if (!empty($inparam)) { + $query->where[] = "u.lang $insql"; + $query->params += $inparam; + } + } + + /** + * It checks the current item or menu data contained access rules based on start or end date. + * + * Start date is configured and the date is reached user has access otherwise it hide the node. + * Or the end date is cconfigured and the date is passed it will hide the node from menu. + * + * @return bool True if the date is reached and not passed if configured, otherwise it false. + */ + public function restriction_bydate() { + + $startdate = $this->data->start_date; + $enddate = $this->data->end_date; + // Check any of the start date or end date is configured. + if (empty($startdate) && empty($enddate)) { + return true; + } + + $date = new \DateTime("now", \core_date::get_user_timezone_object()); + $today = $date->getTimestamp(); + + // Verify the started date is reached. + if (!empty($startdate) && $startdate > $today) { + return false; + } + + // If the menu startdate reached or no start date, then check the enddate is reached. + if (!empty($enddate) && $enddate < $today) { + return false; + } + // Menu is configured between the start and end date. + return true; + } + + /** + * Fetch the list of menus which is used the triggered ID in the access rules for the given method. + * + * Find the menus which contains the given ID in the access rule (Role or cohorts). + * + * @param int $id ID of the triggered method, Role or cohort id. + * @param string $method Field to find, Role or Cohort. + * @return array + */ + public static function find_condition_used_menus($id, $method='cohorts') { + global $DB; + + $like = $DB->sql_like($method, ':value'); + $sql = "SELECT * FROM {theme_boost_union_menus} WHERE $like"; + $params = ['value' => '%"'.$id.'"%']; + + $records = $DB->get_records_sql($sql, $params); + + return $records; + } + + /** + * Fetch the list of items which is used the triggered ID in the access rules for the given method. + * + * Find the items which contains the given ID in the access rule (Role or cohorts). + * + * @param int $id ID of the triggered method, Role or cohort id. + * @param string $method Field to find, Role or Cohort. + * @return array + */ + public static function find_condition_used_menuitems($id, $method='cohorts') { + global $DB; + + $like = $DB->sql_like($method, ':value'); + $sql = "SELECT * FROM {theme_boost_union_menuitems} WHERE $like"; + $params = ['value' => '%"'.$id.'"%']; + + $records = $DB->get_records_sql($sql, $params); + return $records; + } + + /** + * Generate the cache helper for smart menu. + * + * @return cache + */ + public static function get_menu_cache() { + static $cache; + + if (empty($cache)) { + $cache = cache::make('theme_boost_union', 'smartmenus'); + } + + return $cache; + } + + /** + * Generate the cache helper for smart menu item. + * + * @return cache + */ + public static function get_item_cache() { + static $cache; + + if (empty($cache)) { + $cache = cache::make('theme_boost_union', 'smartmenu_items'); + } + + return $cache; + } + + + /** + * Purge the cache for menu and menu items when the cohort is deleted. + * + * It verify any of the menus are used the cohort in the access rules. if records found it will purge the cache of the menus. + * It run the same verification and purge cache for items. Then it remove the cohort from rules. + * + * @param int $cohortid Deleted cohort id. + * @return void + */ + public static function purge_cache_deleted_cohort($cohortid) { + + $records = self::find_condition_used_menus($cohortid); + + if (!empty($records)) { + // Remove the deleted cohort from rules if used in menus restriction. + self::remove_deleted_condition_menu($records, $cohortid); + } + + $records = self::find_condition_used_menuitems($cohortid); + if (!empty($records)) { + // Remove the deleted cohort from menu item rules if used in menuitems restriction. + self::remove_deleted_condition_menuitems($records, $cohortid); + } + } + + /** + * Clear the smartmenu and menu items stored cache for the menus which is used the given role in restriction condition. + * Remove the deleted role from menu restrictions. + * + * @param [type] $roleid + * @return void + */ + public static function purge_cache_deleted_roles($roleid) { + + $records = self::find_condition_used_menus($roleid, 'roles'); + + if (!empty($records)) { + // Remove the deleted role from menu restrictions. + self::remove_deleted_condition_menu($records, $roleid, 'roles'); + } + + $records = self::find_condition_used_menuitems($roleid, 'roles'); + if (!empty($records)) { + // Remove the deleted role from menu item restrictions. + self::remove_deleted_condition_menuitems($records, $roleid, 'roles'); + } + + } + + /** + * Remove the deleted conditions from menu data. + * If the role or cohort is deleted this method will remove the role from the access rules if setup in the menus. + * + * Get the menus which is used the deleted role or cohort in access rules, + * then remove the id from that method and set the updated data for the method related field. + * + * @param stclass $menus List of menus need to purge from cache. + * @param int $id ID of the deleted role or cohort. + * @param string $method Role or cohort which is triggered the purge. + * @return void + */ + public static function remove_deleted_condition_menu($menus, $id, $method='cohorts') { + global $DB; + + if ($menus) { + foreach ($menus as $menu) { + if (isset($menu->$method)) { + $value = json_decode($menu->$method); + if (($key = array_search($id, $value)) !== false) { + unset($value[$key]); + $updated = json_encode($value); + $DB->set_field('theme_boost_union_menus', $method, $updated, ['id' => $menu->id]); + + // Purge the cache of this menu. + self::purge_menu_cache($menu->id); + } + } + } + } + } + + /** + * Remove the deleted conditions from menu items data. + * If the role or cohort is deleted this method will remove the role from the access rules if setup in the menu items. + * + * Get the items which is used the deleted role or cohort in access rules, + * then remove the id from that method and set the updated data for the method related field. + * + * @param stclass $menuitems List of menuitems need to purge from cache + * @param int $id ID of the deleted role or cohort. + * @param string $method Role or cohort which is triggered the purge. + * @return void + */ + public static function remove_deleted_condition_menuitems($menuitems, $id, $method='cohorts') { + global $DB; + + if ($menuitems) { + foreach ($menuitems as $item) { + if (isset($item->$method)) { + $value = json_decode($item->$method); + if (($key = array_search($id, $value)) !== false) { + unset($value[$key]); + $updated = json_encode($value); + $DB->set_field('theme_boost_union_menuitems', $method, $updated, ['id' => $item->id]); + // Purge the cache of this item and its menu. + self::purge_menu_cache($item->menu); + self::purge_item_cache($item->id); + } + } + } + } + } + + /** + * Purge the cache for menu and menu items when the user is assigned or removed from the cohort. + * + * It sets the user preferences to trigger the cache purging when the menu is fetched for the user. + * + * The actual cache purging is performed in build method in smartmenu + * that checks the user preferences and purges the cache accordingly. + * + * @param int $cohortid Affected cohort id. + * @param int $userid Affected user id. + * + * @return void + */ + public static function purge_cache_session_cohort(int $cohortid, int $userid) { + + if ($menus = self::find_condition_used_menus($cohortid)) { + // Remove the menus cache for the user. + $menus = array_column($menus, 'id'); + array_walk($menus, ['self', 'remove_user_cachemenu'], $userid); + } + + if ($items = self::find_condition_used_menuitems($cohortid)) { + // Get the list of menus related to the items. + $menus = array_unique(array_column($items, 'menu')); + $items = array_column($items, 'id'); + // Remove the menus and item cache for the user. + array_walk($menus, ['self', 'remove_user_cachemenu'], $userid); + array_walk($items, ['self', 'remove_user_cacheitem'], $userid); + + } + } + + /** + * Purge the given user cache of menu and items which are configured with the affected role. + * + * Find the list of menus and items configured with the given role and delete the menu from users cache. + * Items are configured with this role, then method removes the items menu cache too. + * + * @param int $roleid Removed role id. + * @param int $userid Releated user who is affected by this event. + * @return void + */ + public static function purge_cache_session_roles(int $roleid, int $userid) { + + if ($menus = self::find_condition_used_menus($roleid, 'roles')) { + // Remove the menus cache for the user. + $menus = array_column($menus, 'id'); + array_walk($menus, ['self', 'remove_user_cachemenu'], $userid); + } + + if ($items = self::find_condition_used_menuitems($roleid, 'roles')) { + // Get the list of menus related to the items. + $menus = array_unique(array_column($items, 'menu')); + $items = array_column($items, 'id'); + // Remove the menus and item cache for the user. + array_walk($menus, ['self', 'remove_user_cachemenu'], $userid); + array_walk($items, ['self', 'remove_user_cacheitem'], $userid); + } + } + + /** + * Purge the cache of dynamic course items. + * + * @return void + */ + public static function purge_cache_dynamic_courseitems() { + global $DB; + // Fetch list of menuitems, configured with the event categoryid. + if ($items = $DB->get_records('theme_boost_union_menuitems', ['type' => smartmenu_item::TYPEDYNAMIC])) { + // List of items to purge. + $items = array_column($items, 'id'); + // Remove the menus items for the user. + array_walk($items, ['self', 'purge_item_cache']); + } + } + + /** + * Deletes the cached menu data for the particular user. + * Fetch the cache instacne, genreate the key combine with menuid and userid, then delete the menu cahce. + * + * @param int $menuid ID of the menu. + * @param int $key + * @param int $userid ID of the user to purge menu cache. + * @return void + */ + public static function remove_user_cachemenu($menuid, $key, $userid) { + // Fetch the cache helper for menu. + $cache = self::get_menu_cache(); + // Create key to remove this menu cache for given user and delete. + $cachekey = "{$menuid}_u_{$userid}"; + $cache->delete($cachekey); + } + + /** + * Deletes the cached menu item data for the particular user. + * Fetch the cache instacne, genreate the key combine with itemid and userid, then delete the item from cache. + * + * @param int $itemid ID of the item. + * @param int $key + * @param int $userid ID of the user to purge menu cache. + * @return void + */ + public static function remove_user_cacheitem($itemid, $key, $userid) { + // Fetch the cache helper for item. + $cache = self::get_item_cache(); + // Create key to remove this items cache for given user and delete. + $cachekey = "{$itemid}_u_{$userid}"; + $cache->delete($cachekey); + } + + /** + * Purges the cached menu data if the menu has a start or end date restriction, and that restriction has been reached or passed. + * + * If the menu has a start date and that date is earlier than the current date, + * and the last check date is earlier than the startdate, then the cached menu data is cleared from the cache + * and the last check date is updated to the current date. + * + * Similarly, if the menu has an end date and that date is earlier than the current date, + * and the last check date is earlier than or equal to the end date, + * then the cached menu data is cleared from the cache and the last check date is updated to the current date. + * + * @param \cache_store $cache The cache object. + * @param object $data The menu data object. + * @param string $key The cache key. + * + * @return void + */ + public static function purge_cache_date_reached($cache, $data, $key) { + + // Check is the cached menu has setup date restriction, then check is the time reached or ended. + $lastcheckdate = $cache->get($key); + $date = new \DateTime("now", \core_date::get_user_timezone_object()); + $today = $date->getTimestamp(); + + // Menu start date reached and last cache date is less than today, then clear the cache and set last check date. + // Verify the lastcheckdate is prevents setup cache again even the cache was stored after the startdate reached. + if ($data->start_date != '' && $data->start_date < $today && $lastcheckdate < $data->start_date) { + $cache->delete_menu($data->id); + $cache->set($key, $today); + } + + // Menu end date is gone and last cache date is less than today, then clear the cache and set last check date. + // Verify the lastcheckdate is prevents setup cache again even the cache was stored after the enddate reached. + if ($data->end_date != '' && $data->end_date < $today && $lastcheckdate <= $data->end_date) { + $cache->delete_menu($data->id); + $cache->set($key, $today); + } + } + + /** + * Remove the specific menu cache for all the users. + * + * @param int $menuid Menu ID to purge. + * @return void + */ + protected static function purge_menu_cache($menuid) { + $cache = self::get_menu_cache(); + $cache->delete_menu($menuid); + } + + /** + * Remove the specific item cache for all the users. + * + * @param int $itemid Item ID to purge. + * @return void + */ + protected static function purge_item_cache($itemid) { + $cache = self::get_item_cache(); + $cache->delete_menu($itemid); + } + + /** + * Sets the user preferences to trigger the cache purging when the menu is fetched for the user. + * The actual cache purging is performed in build method in smartmenu + * that checks the user preferences and purges the cache accordingly. + * + * @param int $userid + * @return void + */ + public static function set_user_purgecache($userid) { + // Clear all the menu and item caches for this user. + set_user_preference('theme_boost_union_menu_purgesessioncache', true, $userid); + } + + /** + * Reset the preference of user to clear the session cache for menu items. + * @return void + */ + public static function clear_user_cachepreferencemenu() { + global $USER; + set_user_preference('theme_boost_union_menu_purgesessioncache', false, $USER); + } + + /** + * Function returns the rgb format with the combination of passed color hex and opacity. + * Used in the item background color or card layout. + * + * @param string $hexa Color code #ffffff + * @param int $opacity Opacity need to add for color, if 5 then opacity is 0.5 + * @return string + */ + public static function color_get_rgba($hexa, $opacity) { + if (!empty($hexa)) { + list($r, $g, $b) = sscanf($hexa, "#%02x%02x%02x"); + if ($opacity == '') { + $opacity = 0.0; + } else { + $opacity = $opacity / 10; + } + return "rgba($r, $g, $b, $opacity)"; + } + } + + /** + * Returns the list of lanuages available in LMS. + * + * @return array + */ + public static function get_lanuage_options() { + $languages = get_string_manager()->get_list_of_translations(); + $langoptions = array(); + foreach ($languages as $key => $lang) { + $langoptions[$key] = $lang; + } + return $langoptions; + } +} diff --git a/smartmenus/menus.php b/smartmenus/menus.php new file mode 100644 index 00000000000..df0bb50673a --- /dev/null +++ b/smartmenus/menus.php @@ -0,0 +1,147 @@ +. + +/** + * Theme Boost Union - Menu overview page + * + * @package theme_boost_union + * @copyright 2023 bdecent GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Require config. +require(__DIR__.'/../../../config.php'); + +// Require plugin libraries. +require_once($CFG->dirroot. '/theme/boost_union/smartmenus/menulib.php'); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +// Get parameters. +$action = optional_param('action', null, PARAM_ALPHAEXT); +$menuid = optional_param('id', null, PARAM_INT); + +// Get system context. +$context = context_system::instance(); + +// Access checks. +admin_externalpage_setup('theme_boost_union_smartmenus'); + +// Prepare the page (to make sure that all necessary information is already set even if we just handle the actions as a start). +$PAGE->set_context($context); +$PAGE->set_url(new moodle_url('/theme/boost_union/smartmenus/menus.php')); +$PAGE->set_cacheable(false); + +// Process actions. +if ($action !== null && confirm_sesskey()) { + // Every action is based on a menu, thus the menu ID param has to exist. + $menuid = required_param('id', PARAM_INT); + + // Create menu instance. Actions are performed in smartmenu instance. + $menu = theme_boost_union\smartmenu::instance($menuid); + + $transaction = $DB->start_delegated_transaction(); + + // Perform the requested action. + switch ($action) { + // Triggered action is delete, then init the deletion of menu. + case 'delete': + // Delete the menu. + if ($menu->delete_menu()) { + // Notification to user for menu deleted success. + \core\notification::success(get_string('smartmenusmenudeleted', 'theme_boost_union')); + } + break; + // Move the menu order to down. + case "movedown": + // Move the menu downwards. + $menu->move_downward(); + break; + case "moveup": + // Move the menu upwards. + $menu->move_upward(); + break; + case "copy": + // Duplicate the menu and it items. + $menu->duplicate(); + break; + case "hidemenu": + // Disable the menu visibility. + $menu->update_visible(false); + break; + case "showmenu": + // Enable the menu. + $menu->update_visible(true); + break; + } + + // Allow to update the changes to database. + $transaction->allow_commit(); + + // Redirect to the same page. + redirect($PAGE->url); +} + +// Further prepare the page. +$PAGE->set_title(theme_boost_union_get_externaladminpage_title(get_string('smartmenus', 'theme_boost_union'))); +$PAGE->set_heading(theme_boost_union_get_externaladminpage_heading()); + +// Build smart menus table. +$table = new theme_boost_union\table\smartmenus_menus($context->id); +$table->define_baseurl($PAGE->url); + +// Start page output. +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('smartmenus', 'theme_boost_union')); + +// Show smart menus description. +echo get_string('smartmenus_desc', 'theme_boost_union'); + +// Add experimental warning. +$experimentalnotification = new \core\output\notification(get_string('smartmenusexperimental', 'theme_boost_union'), + \core\output\notification::NOTIFY_WARNING); +$experimentalnotification->set_show_closebutton(false); +echo $OUTPUT->render($experimentalnotification); + +// Prepare 'Create smart menu' button. +$createbutton = $OUTPUT->box_start(); +$createbutton .= $OUTPUT->single_button( + new \moodle_url('/theme/boost_union/smartmenus/edit.php', array('sesskey' => sesskey())), + get_string('smartmenuscreatemenu', 'theme_boost_union'), 'get'); +$createbutton .= $OUTPUT->box_end(); + +// If there aren't any smart menus yet. +$countmenus = $DB->count_records('theme_boost_union_menus'); +if ($countmenus < 1) { + // Show the table, which, since it is empty, falls back to the + // "There aren't any smart menus created yet. Please create your first smart menu to get things going." notice. + $table->out(0, true); + + // And then show the button. + echo $createbutton; + + // Otherwise. +} else { + // Show the button. + echo $createbutton; + + // And then show the table. + $table->out(50, true); +} + +// Finish page output. +echo $OUTPUT->footer(); diff --git a/templates/core/moremenu.mustache b/templates/core/moremenu.mustache new file mode 100644 index 00000000000..ed350ae1846 --- /dev/null +++ b/templates/core/moremenu.mustache @@ -0,0 +1,103 @@ +{{! + 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/moremenu + + The More menu. + + Example context (json): + { + "nodecollection": { + "children": [ + { + "text": "Home", + "action": "/index.php?redirect=0", + "active": "true" + }, + { + "text": "Dashboard", + "action": "/my" + }, + { + "text": "Courses", + "action": "/course" + }, + { + "text": "Site Administration", + "action": "/admin/search.php" + } + ] + }, + "moremenuid": "614c104dbacfa" + } +}} +{{! + This template is a modified version of core/moremenu + + Modifications compared to the original template: + * Add card style menu. +}} + +{{#js}} + require(['core/moremenu'], function(moremenu) { + moremenu(document.querySelector('#moremenu-{{moremenuid}}-{{navbarstyle}}')); + }); +{{/js}} diff --git a/templates/core/moremenu.mustache.upstream b/templates/core/moremenu.mustache.upstream new file mode 100644 index 00000000000..1da4a578521 --- /dev/null +++ b/templates/core/moremenu.mustache.upstream @@ -0,0 +1,71 @@ +{{! + 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/moremenu + + The More menu. + + Example context (json): + { + "nodecollection": { + "children": [ + { + "text": "Home", + "action": "/index.php?redirect=0", + "active": "true" + }, + { + "text": "Dashboard", + "action": "/my" + }, + { + "text": "Courses", + "action": "/course" + }, + { + "text": "Site Administration", + "action": "/admin/search.php" + } + ] + }, + "moremenuid": "614c104dbacfa" + } +}} + +{{#js}} + require(['core/moremenu'], function(moremenu) { + moremenu(document.querySelector('#moremenu-{{moremenuid}}-{{navbarstyle}}')); + }); +{{/js}} diff --git a/templates/core/moremenu_children.mustache b/templates/core/moremenu_children.mustache new file mode 100644 index 00000000000..4460eb57901 --- /dev/null +++ b/templates/core/moremenu_children.mustache @@ -0,0 +1,131 @@ +{{! + 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/moremenu_children + + The More menu children + + Example context (json): + { + "divider": "", + "haschildren": "", + "moremenuid": "614c104dbacfa", + "text": "Moodle community", + "children": "", + "title": "Moodle community", + "url": "https://moodle.org" + } +}} +{{! + This template is a modified version of core/moremenu + + Modifications compared to the original template: + * Added submenu support + * Added smartmenus styles, icons +}} +{{#haschildren}} + +{{/haschildren}} +{{^haschildren}} + +{{/haschildren}} diff --git a/templates/core/moremenu_children.mustache.upstream b/templates/core/moremenu_children.mustache.upstream new file mode 100644 index 00000000000..bed6e9d8cd9 --- /dev/null +++ b/templates/core/moremenu_children.mustache.upstream @@ -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 . +}} +{{! + @template core/moremenu_children + + The More menu children + + Example context (json): + { + "divider": "", + "haschildren": "", + "moremenuid": "614c104dbacfa", + "text": "Moodle community", + "children": "", + "title": "Moodle community", + "url": "https://moodle.org" + } +}} +{{#haschildren}} + +{{/haschildren}} +{{^haschildren}} + +{{/haschildren}} diff --git a/templates/core/user_action_menu_items.mustache b/templates/core/user_action_menu_items.mustache new file mode 100644 index 00000000000..8ba2cb2e4b9 --- /dev/null +++ b/templates/core/user_action_menu_items.mustache @@ -0,0 +1,93 @@ +{{! + 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_items + + Template for user action menu items. + + Context variables required for this template: + * items - The different items to be rendered + * link - If a link is provided render it. + * title - The text to be shown for the link. + * 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): + { + "items": [ + { + "link": { + "title": "Github user", + "url": "https://raw.githubusercontent.com/", + "pixicon": "t/dashboard", + "imgsrc": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png" + }, + "divider": 1 + }, + { + "submenulink": { + "title": "Title", + "submenuid": "86cebd87", + "pixicon": "t/dashboard", + "imgsrc": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png" + }, + "divider": 1 + } + ] + } +}} +{{! + This template is a modified version of core/user_menu + + Modifications compared to the original template: + * Add menu classes to items + * Add icons to items +}} +{{#items}} + {{#link}} + + {{#pixicon}} + {{#pix}}{{pixicon}}{{/pix}} + {{/pixicon}} + {{^pixicon}} + {{#imgsrc}}{{/imgsrc}} + {{/pixicon}} + {{#icontitle}}{{{icontitle}}}{{/icontitle}} + {{^icontitle}}{{title}}{{/icontitle}} + + {{/link}} + {{#submenulink}} + + {{#pixicon}} + {{#pix}}{{pixicon}}{{/pix}} + {{/pixicon}} + {{^pixicon}} + {{#imgsrc}}{{/imgsrc}} + {{/pixicon}} + {{{title}}} + + {{/submenulink}} + {{#divider}}{{/divider}} +{{/items}} diff --git a/templates/core/user_action_menu_items.mustache.upstream b/templates/core/user_action_menu_items.mustache.upstream new file mode 100644 index 00000000000..29a177973f3 --- /dev/null +++ b/templates/core/user_action_menu_items.mustache.upstream @@ -0,0 +1,84 @@ +{{! + 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_items + + Template for user action menu items. + + Context variables required for this template: + * items - The different items to be rendered + * link - If a link is provided render it. + * title - The text to be shown for the link. + * 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): + { + "items": [ + { + "link": { + "title": "Github user", + "url": "https://raw.githubusercontent.com/", + "pixicon": "t/dashboard", + "imgsrc": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png" + }, + "divider": 1 + }, + { + "submenulink": { + "title": "Title", + "submenuid": "86cebd87", + "pixicon": "t/dashboard", + "imgsrc": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png" + }, + "divider": 1 + } + ] + } +}} +{{#items}} + {{#link}} + + {{#pixicon}} + {{#pix}}{{pixicon}}{{/pix}} + {{/pixicon}} + {{^pixicon}} + {{#imgsrc}}{{/imgsrc}} + {{/pixicon}} + {{title}} + + {{/link}} + {{#submenulink}} + + {{#pixicon}} + {{#pix}}{{pixicon}}{{/pix}} + {{/pixicon}} + {{^pixicon}} + {{#imgsrc}}{{/imgsrc}} + {{/pixicon}} + {{title}} + + {{/submenulink}} + {{#divider}}{{/divider}} +{{/items}} diff --git a/templates/core/user_action_menu_submenu_items.mustache b/templates/core/user_action_menu_submenu_items.mustache new file mode 100644 index 00000000000..74a7f06d5e9 --- /dev/null +++ b/templates/core/user_action_menu_submenu_items.mustache @@ -0,0 +1,70 @@ +{{! + 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 + } + } + } +}} +{{! + This template is a modified version of core/user_action_menu_submenus + + Modifications compared to the original template: + * Include icon support. Changed the text variable to support html. + * Include thirdlevel submenu support. Haschildren based menu item added. +}} +{{#items}} + {{#haschildren}} + + {{#pixicon}} + {{#pix}}{{pixicon}}{{/pix}} + {{/pixicon}} + {{^pixicon}} + {{#imgsrc}}{{/imgsrc}} + {{/pixicon}} + {{{text}}} + + {{/haschildren}} + + {{^haschildren}} + {{#link}} + + {{{text}}} + + {{/link}} + {{/haschildren}} +{{/items}} diff --git a/templates/core/user_action_menu_submenu_items.mustache.upstream b/templates/core/user_action_menu_submenu_items.mustache.upstream new file mode 100644 index 00000000000..35a25e3eeea --- /dev/null +++ b/templates/core/user_action_menu_submenu_items.mustache.upstream @@ -0,0 +1,49 @@ +{{! + 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/templates/core/user_menu.mustache b/templates/core/user_menu.mustache new file mode 100644 index 00000000000..8c8c26dc257 --- /dev/null +++ b/templates/core/user_menu.mustache @@ -0,0 +1,115 @@ +{{! + 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_menu + + Action link template. + + Context variables required for this template: + * unauthenticateduseruser - (Optional) Items to be displayed if an an unautheticated user is accessing the site + * content - The content to be displayed in the header. + * url - The login url + * items - Array of user menu items used in user_action_menu_items. Required if the above not provided. + * metadata - Array of additional metadata to be displayed in the dropdown button. + * 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): + { + "unauthenticateduser": { + "content": "You are not logged in", + "url": "https://yourmoodlesite/login/index.php" + }, + "items": [], + "metadata": [], + "avatardata": [], + "userfullname": "Admin User", + "submenus": [ + { + "id": "86cebd87", + "title": "Submenu title", + "items": [] + } + ] + } +}} +{{! + This template is a modified version of core/user_menu + + Modifications compared to the original template: + * Include submenus custom return id, helps to get back to the submenus parent from third level child. + * Support icons in a user menu item +}} +
+ {{#unauthenticateduser}} + + {{/unauthenticateduser}} + {{^unauthenticateduser}} + + {{/unauthenticateduser}} +
+{{#js}} + require(['core/usermenu'], function(UserMenu) { + UserMenu.init(); + }); +{{/js}} diff --git a/templates/core/user_menu.mustache.upstream b/templates/core/user_menu.mustache.upstream new file mode 100644 index 00000000000..309c531e42f --- /dev/null +++ b/templates/core/user_menu.mustache.upstream @@ -0,0 +1,108 @@ +{{! + 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_menu + + Action link template. + + Context variables required for this template: + * unauthenticateduseruser - (Optional) Items to be displayed if an an unautheticated user is accessing the site + * content - The content to be displayed in the header. + * url - The login url + * items - Array of user menu items used in user_action_menu_items. Required if the above not provided. + * metadata - Array of additional metadata to be displayed in the dropdown button. + * 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): + { + "unauthenticateduser": { + "content": "You are not logged in", + "url": "https://yourmoodlesite/login/index.php" + }, + "items": [], + "metadata": [], + "avatardata": [], + "userfullname": "Admin User", + "submenus": [ + { + "id": "86cebd87", + "title": "Submenu title", + "items": [] + } + ] + } +}} +
+ {{#unauthenticateduser}} + + {{/unauthenticateduser}} + {{^unauthenticateduser}} + + {{/unauthenticateduser}} +
+{{#js}} + require(['core/usermenu'], function(UserMenu) { + UserMenu.init(); + }); +{{/js}} diff --git a/templates/fontawesome-iconpicker-popover.mustache b/templates/fontawesome-iconpicker-popover.mustache new file mode 100644 index 00000000000..65287e78979 --- /dev/null +++ b/templates/fontawesome-iconpicker-popover.mustache @@ -0,0 +1,54 @@ +{{! + 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 theme_boost_union/fontawesome-iconpicker-popover + + Boost Union template for the list of valid fontawesome icons. + It is based on core/form_autocomplete_suggestions. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * options - List of icons with label and value fields. + + Example context (json): + { + "options": [ + { + "label": "fa-book", + "value": "core:book", + "icon": "" + }, + { + "label": "fa-info-circle", + "value": "core:docs", + "icon": "" + } + ] + } +}} +
+ +
diff --git a/templates/primary-drawer-mobile-child.mustache b/templates/primary-drawer-mobile-child.mustache new file mode 100644 index 00000000000..e902fc1840a --- /dev/null +++ b/templates/primary-drawer-mobile-child.mustache @@ -0,0 +1,66 @@ +{{! + 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 theme_boost_union/primary-drawer-mobile-child + + This template renders the mobile version of the top navbar submenus in a drawer. + + Example context (json): + { + "text": "Dashboard", + "url": "/my", + "isactive": "true" + } +}} + + + diff --git a/templates/smartmenus-cardmenu-children.mustache b/templates/smartmenus-cardmenu-children.mustache new file mode 100644 index 00000000000..eff0135eb42 --- /dev/null +++ b/templates/smartmenus-cardmenu-children.mustache @@ -0,0 +1,114 @@ +{{! + 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 theme_boost_union/smartmenus-cardmenu-children + + The smart menus card menu children. + This is a modified version of core/moremenu_children. + + Example context (json): + { + "divider": "", + "haschildren": "", + "moremenuid": "614c104dbacfa", + "text": "Moodle community", + "children": "", + "title": "Moodle community", + "url": "https://moodle.org" + } +}} +{{#haschildren}} + +{{/haschildren}} +{{^haschildren}} + +{{/haschildren}} diff --git a/templates/theme_boost/columns2.mustache b/templates/theme_boost/columns2.mustache index 3a902091de0..c56c8af744b 100644 --- a/templates/theme_boost/columns2.mustache +++ b/templates/theme_boost/columns2.mustache @@ -57,6 +57,7 @@ * Include theme_boost_union/navbar template instead of theme_boost/navbar * Added the possibility to show course related hints. * Added the possibility to show info banners. + * Added smartmenu js to support third level submenus. }} {{> theme_boost/head }} @@ -121,8 +122,9 @@ {{#js}} M.util.js_pending('theme_boost/loader'); -require(['theme_boost/loader', 'theme_boost/drawer'], function(Loader, Drawer) { +require(['theme_boost/loader', 'theme_boost/drawer', 'theme_boost_union/smartmenu'], function(Loader, Drawer, SmartMenu) { Drawer.init(); M.util.js_complete('theme_boost/loader'); + SmartMenu.init(); }); {{/js}} diff --git a/templates/theme_boost/drawers.mustache b/templates/theme_boost/drawers.mustache index 77b3b996eca..c6af6606f01 100644 --- a/templates/theme_boost/drawers.mustache +++ b/templates/theme_boost/drawers.mustache @@ -60,6 +60,8 @@ * Added the possibility to show info banners. * Include theme_boost_union/advertisementtiles template * Added additional block regions (including JS for offcanvas region) + * Added smartmenu js to support third level submenus. + * Added footer-bottom-menu class to #page element as soon as the bottom menu is shown }} {{> theme_boost/head }} @@ -119,7 +121,7 @@ {{/regions.offcanvas.hasblocks}} {{/userisediting}} -
+