From e3379a3079bdceeaef522624c3e914c19fb19dc6 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 15 May 2023 21:34:20 +0530 Subject: [PATCH 01/31] Smart menu feature implemented --- amd/build/fontawesome-popover.min.js | 10 + amd/build/fontawesome-popover.min.js.map | 1 + amd/build/scrollspy.min.js.map | 2 +- amd/build/smartmenu.min.js | 10 + amd/build/smartmenu.min.js.map | 1 + amd/src/fontawesome-popover.js | 214 +++ amd/src/smartmenu.js | 189 +++ classes/eventobservers.php | 100 ++ classes/form/smartmenu_form.php | 241 +++ classes/form/smartmenu_item_form.php | 348 +++++ classes/output/navigation/primary.php | 201 +++ classes/smartmenu.php | 880 +++++++++++ classes/smartmenu_item.php | 1298 +++++++++++++++++ classes/table/menuitems.php | 268 ++++ classes/table/smartmenu.php | 247 ++++ db/caches.php | 33 + db/events.php | 32 + db/install.xml | 70 + db/upgrade.php | 88 ++ lang/en/theme_boost_union.php | 212 +++ layout/columns2.php | 7 +- layout/drawers.php | 9 +- lib.php | 41 + scss/boost_union/post.scss | 1051 ++++++++++++- settings.php | 8 + smartmenus/colorpicker.php | 112 ++ smartmenus/edit.php | 100 ++ smartmenus/edit_items.php | 128 ++ smartmenus/items.php | 143 ++ smartmenus/menulib.php | 630 ++++++++ smartmenus/menus.php | 119 ++ templates/cardmenu_children.mustache | 113 ++ .../core/user_action_menu_items.mustache | 86 ++ .../user_action_menu_submenu_items.mustache | 67 + templates/core/user_menu.mustache | 118 ++ templates/element_colorpicker.mustache | 40 + templates/fontawesome_list.mustache | 44 + templates/moremenu.mustache | 97 ++ templates/moremenu_children.mustache | 129 ++ .../primary-drawer-mobile-child.mustache | 66 + templates/primary-drawer-mobile.mustache | 79 + templates/theme_boost/columns2.mustache | 4 +- templates/theme_boost/drawers.mustache | 4 +- templates/theme_boost/footer.mustache | 11 + templates/theme_boost/navbar.mustache | 21 +- ...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 | 224 +++ ..._union_menuitemsettings_management.feature | 147 ++ ...ost_union_menusettings_application.feature | 263 ++++ ...oost_union_menusettings_management.feature | 131 ++ version.php | 2 +- 54 files changed, 9439 insertions(+), 17 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/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/menuitems.php create mode 100644 classes/table/smartmenu.php create mode 100644 smartmenus/colorpicker.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/cardmenu_children.mustache create mode 100644 templates/core/user_action_menu_items.mustache create mode 100644 templates/core/user_action_menu_submenu_items.mustache create mode 100644 templates/core/user_menu.mustache create mode 100644 templates/element_colorpicker.mustache create mode 100644 templates/fontawesome_list.mustache create mode 100644 templates/moremenu.mustache create mode 100644 templates/moremenu_children.mustache create mode 100644 templates/primary-drawer-mobile-child.mustache create mode 100644 templates/primary-drawer-mobile.mustache 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/amd/build/fontawesome-popover.min.js b/amd/build/fontawesome-popover.min.js new file mode 100644 index 00000000000..399a6158466 --- /dev/null +++ b/amd/build/fontawesome-popover.min.js @@ -0,0 +1,10 @@ +/** + * Shows the footer content in a popover. Modified version of theme_boost\footer-popover by Bas Brands + * + * @module theme_boost_union/fontawesome-popover + * @copyright bdecent GmbH 2023 + * @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..8d8b055ceaf --- /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/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Shows the footer content in a popover. Modified version of theme_boost\\footer-popover by Bas Brands\r\n *\r\n * @module theme_boost_union/fontawesome-popover\r\n * @copyright bdecent GmbH 2023\r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\ndefine(['jquery', 'theme_boost/popover', 'core/fragment'], function($, popover, Fragment) {\r\n\r\n const SELECTORS = {\r\n PICKERCONTAINER: '.fontawesome-iconpicker-popover',\r\n PICKERCONTENT: '[data-region=\"icons-list\"]',\r\n };\r\n\r\n var contextID;\r\n\r\n let pickerIsShown = false;\r\n\r\n var SELECTBOX;\r\n\r\n /**\r\n * Get the footer content for popover.\r\n *\r\n * @returns {String} HTML string\r\n * @private\r\n */\r\n const getIconList = () => {\r\n return Fragment.loadFragment('theme_boost_union', 'icons_list', contextID, {});\r\n };\r\n\r\n /**\r\n * Filter the icons in list with values user entered in search input.\r\n * Given input will contain the text in both aria-value and aria-label.\r\n * Ex. \"core:t\\document\" is aira value and \"fa-document\" is aria-label.\r\n *\r\n * @param {Element} target\r\n */\r\n const filterIcons = (target) => {\r\n var filter = target.value.toLowerCase();\r\n SELECTBOX.value = filter || 0;\r\n var ul = document.querySelector('.fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions');\r\n if (ul === undefined || ul === null) {\r\n return;\r\n }\r\n var li = ul.querySelectorAll('li');\r\n\r\n for (var i = 0; i < li.length; i++) {\r\n var value = li[i].getAttribute('aria-value');\r\n var label = li[i].getAttribute('aria-label');\r\n if (!value.toLowerCase().includes(filter) && !label.toLowerCase().includes(filter)) {\r\n li[i].style.display = \"none\";\r\n } else {\r\n li[i].style.display = \"inline-block\";\r\n }\r\n }\r\n };\r\n\r\n /**\r\n * Creates input element and append the element into the target elements parentnode.\r\n * USer can able to search icons using this input field\r\n *\r\n * @param {String} target Element Selector.\r\n */\r\n const createElements = (target) => {\r\n\r\n var input = document.createElement('input');\r\n input.setAttribute('type', 'text');\r\n input.classList.add('fontawesome-autocomplete');\r\n input.classList.add('form-control');\r\n input.setAttribute('name', 'iconsearch');\r\n\r\n if (SELECTBOX.value != '') {\r\n input.value = SELECTBOX.querySelector('option[selected]') !== null\r\n ? SELECTBOX.querySelector('option[selected]').text : '';\r\n }\r\n\r\n var wrapper = document.createElement('div');\r\n wrapper.classList.add(\"fontawesome-picker-container\");\r\n wrapper.append(input);\r\n\r\n document.querySelector(target).style.display = 'none';\r\n document.querySelector(target).parentNode.append(wrapper);\r\n };\r\n\r\n /**\r\n * Update the target with fontawesome iconpicker.\r\n *\r\n * Create picker input field for search icons insert to dom, Fetch the icons list and setup the popover with icons content.\r\n * Display the popover when the icon search input field is focused or clicked. This way user can view the list of icons,\r\n * Search icons. when the icons is selected, same icon in the select element will selected.\r\n *\r\n * @param {String} target Element Selector.\r\n */\r\n const iconPicker = (target) => {\r\n\r\n SELECTBOX = document.querySelector(target);\r\n\r\n if (SELECTBOX === undefined || SELECTBOX === null) {\r\n return;\r\n }\r\n\r\n // Create input element and insert for search icons and hide the current target select box.\r\n createElements(target);\r\n\r\n // Parent of the target element.\r\n var selectBoxParent = document.querySelector(target).parentNode;\r\n // Input element for search icons, appended in createElements method.\r\n const pickerInput = selectBoxParent.querySelector(\"input.fontawesome-autocomplete\");\r\n\r\n // Check the search input created and inserted in dom.\r\n if (pickerInput === undefined || pickerInput === null) {\r\n setTimeout(() => iconPicker(target), 1000);\r\n return;\r\n }\r\n\r\n // Fetch the icons list, and setup popover with iconslist.\r\n getIconList().then(function(html) {\r\n\r\n $(pickerInput).popover({\r\n content: html,\r\n html: true,\r\n placement: 'bottom',\r\n customClass: 'fontawesome-picker',\r\n trigger: 'click'\r\n });\r\n\r\n // Event observer when the popover is inserted in dom, create eventlistner for each icons in icons list.\r\n // Icon is clicked, set the icon aria-value as value for select box.\r\n // Set the icon label to value of autocompletd picker.\r\n $(pickerInput).on('inserted.bs.popover', function() {\r\n var ul = document.querySelector('.fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions');\r\n ul.querySelectorAll('li').forEach((li) => {\r\n li.addEventListener('click', (e) => {\r\n var target = e.target.closest('li');\r\n var value = target.getAttribute('aria-value');\r\n var label = target.getAttribute('aria-label');\r\n pickerInput.value = label;\r\n SELECTBOX.value = value || 0;\r\n $(pickerInput).popover('hide');\r\n });\r\n });\r\n });\r\n return;\r\n }).catch();\r\n\r\n document.addEventListener('click', e => {\r\n if (pickerIsShown && !e.target.closest(SELECTORS.PICKERCONTAINER)) {\r\n $(pickerInput).popover('hide');\r\n }\r\n },\r\n true);\r\n\r\n document.addEventListener('keydown', e => {\r\n if (pickerIsShown && e.key === 'Escape') {\r\n $(pickerInput).popover('hide');\r\n pickerInput.focus();\r\n }\r\n });\r\n\r\n document.addEventListener('focus', e => {\r\n if (pickerIsShown && !e.target.closest(SELECTORS.PICKERCONTAINER)) {\r\n $(pickerInput).popover('hide');\r\n }\r\n },\r\n true);\r\n\r\n $(pickerInput).on('shown.bs.popover', () => {\r\n pickerIsShown = true;\r\n // Add class to selected icon, helps to differenciate.\r\n if (pickerInput.value != '') {\r\n var iconSuggestion = document.querySelector('.fontawesome-iconpicker-popover ul.fontawesome-icon-suggestions');\r\n if (iconSuggestion.querySelector('li[aria-label=\"' + pickerInput.value + '\"]') !== null) {\r\n // Remove selected class.\r\n iconSuggestion.querySelectorAll('li').forEach((li) => li.classList.remove('selected'));\r\n // Assign selected class for new.\r\n iconSuggestion.querySelector('li[aria-label=\"' + pickerInput.value + '\"]').classList.add('selected');\r\n }\r\n }\r\n });\r\n\r\n $(pickerInput).on('hide.bs.popover', () => {\r\n pickerIsShown = false;\r\n });\r\n\r\n pickerInput.addEventListener('keyup', function(e) {\r\n filterIcons(e.target);\r\n });\r\n\r\n };\r\n\r\n return {\r\n init: (target, contextid) => {\r\n contextID = contextid;\r\n iconPicker(target);\r\n }\r\n\r\n };\r\n});\r\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":";;;;;;;AAuBAA,+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,cAKTgB,YAFgBf,SAASC,cAAcF,QAAQc,WAEjBZ,cAAc,kCAG9Cc,MAAAA,aAnFGtB,SAASuB,aAAa,oBAAqB,aAAcrB,UAAW,IAyF7DsB,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,IAAOA,GAAGrB,UAAU+B,OAAO,cAE1ED,eAAelC,cAAc,kBAAoBc,YAAYR,MAAQ,MAAMF,UAAUC,IAAI,iBAKrGf,EAAEwB,aAAaQ,GAAG,mBAAmB,KACjC3B,eAAgB,KAGpBmB,YAAYY,iBAAiB,SAAS,SAASC,GAnJ9B7B,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,SAuI1B+B,CAAYf,EAAE7B,YA1Ed6C,YAAW,IAAM9C,WAAWC,SAAS,YA+EtC,CACH8C,KAAM,CAAC9C,OAAQ+C,aACXnD,UAAYmD,UACZhD,WAAWC"} \ No newline at end of file diff --git a/amd/build/scrollspy.min.js.map b/amd/build/scrollspy.min.js.map index ecb6dd93519..f8b7314c323 100644 --- a/amd/build/scrollspy.min.js.map +++ b/amd/build/scrollspy.min.js.map @@ -1 +1 @@ -{"version":3,"file":"scrollspy.min.js","sources":["../src/scrollspy.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 scroll-spy\n *\n * @module theme_boost_union/scrollspy\n * @copyright 2022 Josha Bartsch \n * @copyright based on code from theme_fordson by Chris Kenniburg.\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Runs once at initial load, and once at editmode-switch toggle.\n * Incase of initial load, checks sessionStorage whether a position was set and jumps to the appropriate position.\n *\n * Incase of a click on the switch, iterates over central elements (selector .section.main), determines element\n * with minimal distance between pixel-toprow of view and pixel-toprow of the element.\n * Writes element ID + distance of view from element into session storage.\n *\n * Saving a reference point + relative distance grants leeway for varying page elements.\n * (See original implementation: https://raw.githubusercontent.com/dbnschools/moodle-theme_fordson/master/javascript/scrollspy.js)\n */\nconst initScrollSpy = () => {\n // Unfortunately the editmode-switch carries no unique ID\n let editToggle = document.querySelector('form.editmode-switch-form');\n\n if (!editToggle) {\n // Do not continue when there is no edit toggle.\n return;\n }\n\n editToggle.addEventListener('click', () => {\n\n window.sessionStorage.setItem('edittoggled', true);\n\n let viewporttop = document.getElementById('page').scrollTop;\n let closest = null;\n let closestoffset = null;\n\n document.querySelectorAll('.section.main').forEach((node) => {\n let thisoffset = node.offsetTop;\n\n if (closest && closest.offsetTop) {\n closestoffset = closest.offsetTop;\n }\n if (closest === null || Math.abs(thisoffset - viewporttop) < Math.abs(closestoffset - viewporttop)) {\n closest = node;\n }\n });\n\n window.sessionStorage.setItem('closestid', closest.id);\n window.sessionStorage.setItem('closestdelta', viewporttop - closest.offsetTop);\n });\n\n let edittoggled = window.sessionStorage.getItem('edittoggled');\n\n if (edittoggled) {\n\n let closestid = window.sessionStorage.getItem('closestid');\n let closestdelta = window.sessionStorage.getItem('closestdelta');\n\n if (closestid && closestdelta) {\n let closest = document.getElementById(closestid);\n let y = closest.offsetTop + parseInt(closestdelta);\n\n document.getElementById('page').scrollTo(0, y);\n }\n\n window.sessionStorage.removeItem('edittoggled');\n window.sessionStorage.removeItem('closestid');\n window.sessionStorage.removeItem('closestdelta');\n }\n};\n\n/**\n * Ensures the passed function will be called after the DOM is ready/loaded:\n * Incase DOM is fully loaded when JS is called, call within next tick.\n * Otherwise sets an eventlistener for DOMEventLoaded\n *\n * @param {*} callback\n */\nconst docReady = (callback) => {\n if (document.readyState === \"complete\" || document.readyState === \"interactive\") {\n setTimeout(callback, 1);\n } else {\n document.addEventListener('DOMContentLoaded', callback);\n }\n};\n\nexport const init = () => {\n docReady(initScrollSpy());\n};\n"],"names":["callback","editToggle","document","querySelector","addEventListener","window","sessionStorage","setItem","viewporttop","getElementById","scrollTop","closest","closestoffset","querySelectorAll","forEach","node","thisoffset","offsetTop","Math","abs","id","getItem","closestid","closestdelta","y","parseInt","scrollTo","removeItem","initScrollSpy","readyState","setTimeout"],"mappings":"gKAsGoB,KARFA,IAAAA,SAAAA,SA3DI,UAEdC,WAAaC,SAASC,cAAc,gCAEnCF,aAKLA,WAAWG,iBAAiB,SAAS,KAEjCC,OAAOC,eAAeC,QAAQ,eAAe,OAEzCC,YAAcN,SAASO,eAAe,QAAQC,UAC9CC,QAAU,KACVC,cAAgB,KAEpBV,SAASW,iBAAiB,iBAAiBC,SAASC,WAC5CC,WAAaD,KAAKE,UAElBN,SAAWA,QAAQM,YACnBL,cAAgBD,QAAQM,YAEZ,OAAZN,SAAoBO,KAAKC,IAAIH,WAAaR,aAAeU,KAAKC,IAAIP,cAAgBJ,gBAClFG,QAAUI,SAIlBV,OAAOC,eAAeC,QAAQ,YAAaI,QAAQS,IACnDf,OAAOC,eAAeC,QAAQ,eAAgBC,YAAcG,QAAQM,cAGtDZ,OAAOC,eAAee,QAAQ,gBAE/B,KAETC,UAAYjB,OAAOC,eAAee,QAAQ,aAC1CE,aAAelB,OAAOC,eAAee,QAAQ,mBAE7CC,WAAaC,aAAc,KAEvBC,EADUtB,SAASO,eAAea,WACtBL,UAAYQ,SAASF,cAErCrB,SAASO,eAAe,QAAQiB,SAAS,EAAGF,GAGhDnB,OAAOC,eAAeqB,WAAW,eACjCtB,OAAOC,eAAeqB,WAAW,aACjCtB,OAAOC,eAAeqB,WAAW,kBAoB5BC,GARmB,aAAxB1B,SAAS2B,YAAqD,gBAAxB3B,SAAS2B,WAC/CC,WAAW9B,SAAU,GAErBE,SAASE,iBAAiB,mBAAoBJ"} \ No newline at end of file +{"version":3,"file":"scrollspy.min.js","sources":["../src/scrollspy.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Theme Boost Union - JS code scroll-spy\r\n *\r\n * @module theme_boost_union/scrollspy\r\n * @copyright 2022 Josha Bartsch \r\n * @copyright based on code from theme_fordson by Chris Kenniburg.\r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\n/**\r\n * Runs once at initial load, and once at editmode-switch toggle.\r\n * Incase of initial load, checks sessionStorage whether a position was set and jumps to the appropriate position.\r\n *\r\n * Incase of a click on the switch, iterates over central elements (selector .section.main), determines element\r\n * with minimal distance between pixel-toprow of view and pixel-toprow of the element.\r\n * Writes element ID + distance of view from element into session storage.\r\n *\r\n * Saving a reference point + relative distance grants leeway for varying page elements.\r\n * (See original implementation: https://raw.githubusercontent.com/dbnschools/moodle-theme_fordson/master/javascript/scrollspy.js)\r\n */\r\nconst initScrollSpy = () => {\r\n // Unfortunately the editmode-switch carries no unique ID\r\n let editToggle = document.querySelector('form.editmode-switch-form');\r\n\r\n if (!editToggle) {\r\n // Do not continue when there is no edit toggle.\r\n return;\r\n }\r\n\r\n editToggle.addEventListener('click', () => {\r\n\r\n window.sessionStorage.setItem('edittoggled', true);\r\n\r\n let viewporttop = document.getElementById('page').scrollTop;\r\n let closest = null;\r\n let closestoffset = null;\r\n\r\n document.querySelectorAll('.section.main').forEach((node) => {\r\n let thisoffset = node.offsetTop;\r\n\r\n if (closest && closest.offsetTop) {\r\n closestoffset = closest.offsetTop;\r\n }\r\n if (closest === null || Math.abs(thisoffset - viewporttop) < Math.abs(closestoffset - viewporttop)) {\r\n closest = node;\r\n }\r\n });\r\n\r\n window.sessionStorage.setItem('closestid', closest.id);\r\n window.sessionStorage.setItem('closestdelta', viewporttop - closest.offsetTop);\r\n });\r\n\r\n let edittoggled = window.sessionStorage.getItem('edittoggled');\r\n\r\n if (edittoggled) {\r\n\r\n let closestid = window.sessionStorage.getItem('closestid');\r\n let closestdelta = window.sessionStorage.getItem('closestdelta');\r\n\r\n if (closestid && closestdelta) {\r\n let closest = document.getElementById(closestid);\r\n let y = closest.offsetTop + parseInt(closestdelta);\r\n\r\n document.getElementById('page').scrollTo(0, y);\r\n }\r\n\r\n window.sessionStorage.removeItem('edittoggled');\r\n window.sessionStorage.removeItem('closestid');\r\n window.sessionStorage.removeItem('closestdelta');\r\n }\r\n};\r\n\r\n/**\r\n * Ensures the passed function will be called after the DOM is ready/loaded:\r\n * Incase DOM is fully loaded when JS is called, call within next tick.\r\n * Otherwise sets an eventlistener for DOMEventLoaded\r\n *\r\n * @param {*} callback\r\n */\r\nconst docReady = (callback) => {\r\n if (document.readyState === \"complete\" || document.readyState === \"interactive\") {\r\n setTimeout(callback, 1);\r\n } else {\r\n document.addEventListener('DOMContentLoaded', callback);\r\n }\r\n};\r\n\r\nexport const init = () => {\r\n docReady(initScrollSpy());\r\n};\r\n"],"names":["callback","editToggle","document","querySelector","addEventListener","window","sessionStorage","setItem","viewporttop","getElementById","scrollTop","closest","closestoffset","querySelectorAll","forEach","node","thisoffset","offsetTop","Math","abs","id","getItem","closestid","closestdelta","y","parseInt","scrollTo","removeItem","initScrollSpy","readyState","setTimeout"],"mappings":"gKAsGoB,KARFA,IAAAA,SAAAA,SA3DI,UAEdC,WAAaC,SAASC,cAAc,gCAEnCF,aAKLA,WAAWG,iBAAiB,SAAS,KAEjCC,OAAOC,eAAeC,QAAQ,eAAe,OAEzCC,YAAcN,SAASO,eAAe,QAAQC,UAC9CC,QAAU,KACVC,cAAgB,KAEpBV,SAASW,iBAAiB,iBAAiBC,SAASC,WAC5CC,WAAaD,KAAKE,UAElBN,SAAWA,QAAQM,YACnBL,cAAgBD,QAAQM,YAEZ,OAAZN,SAAoBO,KAAKC,IAAIH,WAAaR,aAAeU,KAAKC,IAAIP,cAAgBJ,gBAClFG,QAAUI,SAIlBV,OAAOC,eAAeC,QAAQ,YAAaI,QAAQS,IACnDf,OAAOC,eAAeC,QAAQ,eAAgBC,YAAcG,QAAQM,cAGtDZ,OAAOC,eAAee,QAAQ,gBAE/B,KAETC,UAAYjB,OAAOC,eAAee,QAAQ,aAC1CE,aAAelB,OAAOC,eAAee,QAAQ,mBAE7CC,WAAaC,aAAc,KAEvBC,EADUtB,SAASO,eAAea,WACtBL,UAAYQ,SAASF,cAErCrB,SAASO,eAAe,QAAQiB,SAAS,EAAGF,GAGhDnB,OAAOC,eAAeqB,WAAW,eACjCtB,OAAOC,eAAeqB,WAAW,aACjCtB,OAAOC,eAAeqB,WAAW,kBAoB5BC,GARmB,aAAxB1B,SAAS2B,YAAqD,gBAAxB3B,SAAS2B,WAC/CC,WAAW9B,SAAU,GAErBE,SAASE,iBAAiB,mBAAoBJ"} \ 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..6375be8db75 --- /dev/null +++ b/amd/build/smartmenu.min.js @@ -0,0 +1,10 @@ +/** + * Theme Boost Union - JS for smartmenu to make the third level submenu support. + * + * @module theme_boost_union/smartmenu + * @copyright bdecent GmbH 2023 + * @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")))}));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..d8084bacae1 --- /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 smartmenu to make the third level submenu support.\n *\n * @module theme_boost_union/smartmenu\n * @copyright bdecent GmbH 2023\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 event listener for click event, on the click show 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 the hide of parent menu.\n e.stopPropagation();\n });\n\n\n });\n }\n\n // Hide the submenus on hidden of its parent dropdown.\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 // 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 moremenu, items which is set to force outside moremenu.\n * Remove those items from more menu and insert the menu before the last normal item.\n * Find the length and childrens length to insert the out menus in that positions.\n * Rerun the moremenu 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 childrens 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});\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","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,SAiDnCC,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,KAzJS,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,wBAQdlC,EAAEI,UAAU+B,GAAG,sBAAsBJ,QAE7BK,SADSL,EAAEM,cAAcnB,WACPb,iBAAiB,0BACtB,OAAb+B,UACAA,SAAS9B,SAASyB,GAAMA,EAAEvB,UAAUC,OAAO,iBAK/C6B,SAAWlC,SAASC,iBAAiB,sCACxB,OAAbiC,UACAA,SAAShC,SAASiC,OACdA,KAAKT,iBAAiB,SAAUC,IAC5BA,EAAEG,yBAyHVM,GAhGW,UACXC,MAAQrC,SAASC,iBAAiB,2CACxB,OAAVoC,MAAgB,KACZC,YACAC,gBACAC,SAAUC,UAEdJ,MAAMnC,SAASwC,WACPC,cAAgBD,KAAK/B,cAAc,kBAEvCgC,cAAcjB,iBAAiB,aAAcC,IACzCW,aAAc,MACVxC,OAAS6B,EAAEC,cAAcjB,cAAc,uBAC3C6B,SAAWb,EAAEiB,MACbH,UAAY3C,OAAO+C,cAGvBF,cAAcjB,iBAAiB,aAAcC,OACzCA,EAAEmB,kBACGR,mBAGLC,aAAc,MACVzC,OAAS6B,EAAEC,cAAcjB,cAAc,6BACrCoC,OAASpB,EAAEiB,MAAQJ,SACzB1C,OAAO+C,WAAaJ,UAAYM,UAGpCJ,cAAcjB,iBAAiB,SAAUC,IACjCY,cACAZ,EAAEmB,iBACFP,aAAc,GAElBZ,EAAEG,qBAENa,cAAcjB,iBAAiB,cAAc,KACzCY,aAAc,EACdC,aAAc,KAElBI,cAAcjB,iBAAiB,WAAW,KACtCY,aAAc,UAyDtBU,GAhDa,UACbC,WAAajD,SAASW,cAAc,mCACxCL,gBAAgB2C,gBAEZC,QAAUlD,SAASW,cAAc,2BACrCL,gBAAgB4C,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..fc0726196b6 --- /dev/null +++ b/amd/src/fontawesome-popover.js @@ -0,0 +1,214 @@ +// 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 . + +/** + * Shows the footer content in a popover. Modified version of theme_boost\footer-popover by Bas Brands + * + * @module theme_boost_union/fontawesome-popover + * @copyright bdecent GmbH 2023 + * @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 footer content for popover. + * + * @returns {String} HTML string + * @private + */ + const getIconList = () => { + return Fragment.loadFragment('theme_boost_union', 'icons_list', contextID, {}); + }; + + /** + * Filter the icons in list with values user entered in search input. + * Given input will contain the text in both aria-value and aria-label. + * Ex. "core:t\document" is aira 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 elements parentnode. + * USer can 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, + * Search icons. when the icons is selected, same icon in the select element will 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 iconslist. + 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 eventlistner for each icons in icons list. + // Icon is clicked, set the icon aria-value as value for select box. + // Set the icon label to value of autocompletd 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 differenciate. + 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..c0050c07959 --- /dev/null +++ b/amd/src/smartmenu.js @@ -0,0 +1,189 @@ +// 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 smartmenu to make the third level submenu support. + * + * @module theme_boost_union/smartmenu + * @copyright bdecent GmbH 2023 + * @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 event listener for click event, on the click show 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 the hide of parent menu. + e.stopPropagation(); + }); + + + }); + } + + // Hide the submenus on hidden of its parent dropdown. + $(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')); + } + }); + + // 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 moremenu, items which is set to force outside moremenu. + * Remove those items from more menu and insert the menu before the last normal item. + * Find the length and childrens length to insert the out menus in that positions. + * Rerun the moremenu 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 childrens 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/eventobservers.php b/classes/eventobservers.php index 4494523ff7e..c3034c043af 100644 --- a/classes/eventobservers.php +++ b/classes/eventobservers.php @@ -51,6 +51,13 @@ 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'); } + + // Verify any of the menu or menu items uses this cohort for restriction. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + // Deletion of this cohort may result restriction of menus for users. + // Verfiy any of the menus used this deleted cohort in restriction rules, then need to purge the menus cache. + \smartmenu_helper::purge_cache_deleted_cohort($event->objectid); + } /** @@ -72,6 +79,13 @@ 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); } + + // Verify any of the menu or menu items uses this cohort for restriction. + // if uses then update the cache for this user session only. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + // Deletion of this cohort may result restriction of menus for users. + // Verfiy any of the menus used this deleted cohort in restriction, then update the menus cache for this user session. + \smartmenu_helper::purge_cache_session_cohort($event->objectid, $event->relateduserid); } /** @@ -93,5 +107,91 @@ 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); } + + // Verify any of the menu or menu items uses this cohort for restriction. + // if uses then update the cache for this user session only. + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + // Deletion of this cohort may result restriction of menus for users. + // Verfiy any of the menus used this deleted cohort in restriction, then update the menus cache for this user session. + \smartmenu_helper::purge_cache_session_cohort($event->objectid, $event->relateduserid); + + } + + /** + * Event observer for when a user profile is updated. + * Purges the cached menus for the updated user. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function user_updated(\core\event\base $event) { + set_user_preference('theme_boost_union_menu_purgesessioncache', true, $event->relateduserid); + set_user_preference('theme_boost_union_menuitem_purgesessioncache', true, $event->relateduserid); + } + + /** + * Event observer for when a role is assigned to a user. + * Purges the cached menus for the user with the assigned role. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function role_assigned(\core\event\base $event) { + global $CFG; + + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + \smartmenu_helper::purge_cache_session_roles($event->objectid, $event->relateduserid); + } + + /** + * Event observer for when a role is unassigned from a user. + * Purges the cached menus for the user with the unassigned role. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function role_unassigned(\core\event\base $event) { + global $CFG; + + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + \smartmenu_helper::purge_cache_session_roles($event->objectid, $event->relateduserid); + } + + /** + * Event observer for when a role is deleted. + * Purges the cached menus for all users with the deleted role. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function role_deleted(\core\event\base $event) { + global $CFG; + + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + \smartmenu_helper::purge_cache_deleted_roles($event->objectid); + } + + /** + * Event observer for when a course is updated. + * Purges the cached menus related to the course. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function course_updated(\core\event\base $event) { + global $CFG; + + require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + // Clear the cache of menu when the course updated. + \cache_helper::purge_by_event('theme_boost_union_course_updated'); + } + + /** + * Event observer for when a course or module completion is updated. + * Purges the cached menus related to the user. + * + * @param \core\event\base $event The event that triggered the handler. + */ + public static function completion_updated(\core\event\base $event) { + global $CFG; + + 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::purge_all_cache_user_session($event->relateduserid); } } diff --git a/classes/form/smartmenu_form.php b/classes/form/smartmenu_form.php new file mode 100644 index 00000000000..8e20c987ed2 --- /dev/null +++ b/classes/form/smartmenu_form.php @@ -0,0 +1,241 @@ +. + +/** + * Theme Boost Union - Smart menu edit form + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @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('smartmenu:generalsection', 'theme_boost_union')); + + // Add the Title field (required). + $mform->addElement('text', 'title', get_string('smartmenu:menutitle', 'theme_boost_union')); + $mform->setType('title', PARAM_TEXT); + $mform->addRule('title', get_string('error'), 'required'); + $mform->addHelpButton('title', 'smartmenu:menutitle', 'theme_boost_union'); + + // Add the Description field (optional). + $mform->addElement('editor', 'description', get_string('description')); + $mform->addHelpButton('description', 'smartmenu:description', 'theme_boost_union'); + + // Add the Show Description field (required). + $options = array( + smartmenu::DESC_NEVER => get_string('never', 'core'), + smartmenu::DESC_ABOVE => get_string('smartmenu:above', 'theme_boost_union'), + smartmenu::DESC_BELOW => get_string('smartmenu:below', 'theme_boost_union'), + smartmenu::DESC_HELP => get_string('help', 'core') + ); + $mform->addElement('select', 'showdesc', get_string('smartmenu:showdescription', '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('smartmenu:location', 'theme_boost_union'), $types); + $mform->addHelpButton('location', 'smartmenu:location', '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('smartmenu:types', 'theme_boost_union'), $types); + $mform->addHelpButton('type', 'smartmenu:types', 'theme_boost_union'); + + // Advanced settings options. Settings for advanced users / special use cases. + $mform->addElement('header', 'advanced_settings', get_string('smartmenu:advancedsettings', 'theme_boost_union')); + + // Display options mode. + $displayoptions = [ + smartmenu::MODE_SUBMENU => get_string('smartmenu:submenu', 'theme_boost_union'), + smartmenu::MODE_INLINE => get_string('smartmenu:inline', 'theme_boost_union'), + ]; + $mform->addElement('select', 'mode', get_string('smartmenu:mode', 'theme_boost_union'), $displayoptions); + $mform->addHelpButton('mode', 'smartmenu:menumode', 'theme_boost_union'); + + // CSS Class. + $mform->addElement('text', 'cssclass', get_string('smartmenu:cssclass', 'theme_boost_union')); + $mform->addHelpButton('cssclass', 'smartmenu:cssclass', '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('smartmenu:forcedintomoremenu', 'theme_boost_union'), + smartmenu::MOREMENU_OUTSIDE => get_string('smartmenu:forcedoutsideofmoremenu', 'theme_boost_union') + ); + $mform->addElement('select', 'moremenubehavior', get_string('smartmenu:moremenubehavior', 'theme_boost_union'), $moremenu); + $mform->addHelpButton('moremenubehavior', 'smartmenu:moremenubehavior', 'theme_boost_union'); + + // Card appearance options. + $mform->addElement('header', 'card_appearance', get_string('smartmenu:cardappearance', 'theme_boost_union')); + + // Size. + $sizeoptions = array( + smartmenu::TINY => get_string('smartmenu:tiny', 'theme_boost_union').' (50px)', + smartmenu::SMALL => get_string('smartmenu:small', 'theme_boost_union').' (100px)', + smartmenu::MEDIUM => get_string('smartmenu:medium', 'theme_boost_union').' (150px)', + smartmenu::LARGE => get_string('smartmenu:large', 'theme_boost_union').' (200px)' + ); + $mform->addElement('select', 'cardsize', get_string('smartmenu:cardsize', 'theme_boost_union'), $sizeoptions); + $mform->disabledIf('cardsize', 'type', 0); + $mform->addHelpButton('cardsize', 'smartmenu:cardsize', 'theme_boost_union'); + + // Form. + $formoptions = array( + smartmenu::SQUARE => get_string('smartmenu:square', 'theme_boost_union') . ' (1/1)', + smartmenu::PORTRAIT => get_string('smartmenu:portrait', 'theme_boost_union') . ' (2/3)', + smartmenu::LANDSCAPE => get_string('smartmenu:landscape', 'theme_boost_union') . ' (3/2)', + smartmenu::FULLWIDTH => get_string('smartmenu:fullwidth', 'theme_boost_union') + ); + $mform->addElement('select', 'cardform', get_string('smartmenu:cardform', 'theme_boost_union'), $formoptions); + $mform->disabledIf('cardform', 'type', 'neq', smartmenu::TYPE_CARD); + $mform->addHelpButton('cardform', 'smartmenu:cardform', 'theme_boost_union'); + + // Overflow behavior. + $overflow = array( + smartmenu::NOWRAP => get_string('smartmenu:no_wrap', 'theme_boost_union'), + smartmenu::WRAP => get_string('smartmenu:wrap', 'theme_boost_union') + ); + $mform->addElement('select', 'overflowbehavior', get_string('smartmenu:overflowbehavior', 'theme_boost_union'), $overflow); + $mform->disabledIf('overflowbehavior', 'type', 'neq', smartmenu::TYPE_CARD); + $mform->addHelpButton('overflowbehavior', 'smartmenu:overflowbehavior', 'theme_boost_union'); + + // Access rule by roles. + $mform->addElement('header', 'accessbyroles', get_string('smartmenu:accessbyroles', '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('smartmenu:byrole', 'theme_boost_union'), $roles); + $mform->addHelpButton('roles', 'smartmenu:byrole', 'theme_boost_union'); + $roles->setMultiple(true); + + $rolecontext = [ + smartmenu::ANYCONTEXT => get_string('any'), + smartmenu::SYSTEMCONTEXT => get_string('coresystem'), + ]; + $mform->addElement('select', 'rolecontext', get_string('smartmenu:rolecontext', 'theme_boost_union'), $rolecontext); + $mform->addHelpButton('rolecontext', 'smartmenu:rolecontext', 'theme_boost_union'); + + // Access rule by cohorts. + $mform->addElement('header', 'accessbycohorts', get_string('smartmenu:accessbycohorts', '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('smartmenu:bycohort', 'theme_boost_union'), $cohorts); + $cohort->setMultiple(true); + $mform->addHelpButton('cohorts', 'smartmenu:bycohort', 'theme_boost_union'); + + $rolecontext = [ + smartmenu::ANY => get_string('any'), + smartmenu::ALL => get_string('all'), + ]; + $mform->addElement('select', 'operator', get_string('smartmenu:operator', 'theme_boost_union'), $rolecontext); + $mform->addHelpButton('operator', 'smartmenu:operator', 'theme_boost_union'); + + // Access rule by languages. + $mform->addElement('header', 'accessbylanguage', get_string('smartmenu:accessbylanguage', '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('smartmenu:bylanguage', 'theme_boost_union'), $langoptions); + $language->setMultiple(true); + $mform->addHelpButton('languages', 'smartmenu:bylanguage', 'theme_boost_union'); + + // Access rule by languages. + $mform->addElement('header', 'accessbydateselector', get_string('smartmenu:accessbydateselector', 'theme_boost_union')); + + $mform->addElement('date_time_selector', 'start_date', + get_string('smartmenu:from', 'theme_boost_union'), array('optional' => true)); + $mform->addHelpButton('start_date', 'smartmenu:from', 'theme_boost_union'); + + $mform->addElement('date_time_selector', 'end_date', + get_string('smartmenu:durationuntil', 'theme_boost_union'), array('optional' => true)); + $mform->addHelpButton('end_date', 'smartmenu:durationuntil', '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('savechangesandconfigure', '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..aa63c4a30aa --- /dev/null +++ b/classes/form/smartmenu_item_form.php @@ -0,0 +1,348 @@ +. + +/** + * Theme Boost Union - Smart menu item edit form + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @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/smartmenus/colorpicker.php'); + \MoodleQuickForm::registerElementType( + 'theme_boost_unioncolorpicker', + $CFG->dirroot.'/theme/boost_union/smartmenus/colorpicker.php', + 'moodlequickform_boostunioncolorpicker' + ); + + // General section. + $mform->addElement('header', 'general', get_string('general', 'core')); + + // Menu item Title. + $mform->addElement('text', 'title', get_string('smartmenu:title', 'theme_boost_union')); + $mform->setType('title', PARAM_TEXT); + $mform->addRule('title', null, 'required'); + $mform->addHelpButton('title', 'smartmenu:title', '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('smartmenu:type', 'theme_boost_union'), $types); + $mform->setType('type', PARAM_INT); + $mform->addRule('type', null, 'required'); + $mform->addHelpButton('type', 'smartmenu:type', 'theme_boost_union'); + + $mform->addElement('text', 'url', get_string('smartmenu:url', 'theme_boost_union')); + $mform->setType('url', PARAM_URL); + $mform->disabledIf('url', 'type', 'neq', menuitem::TYPESTATIC); + $mform->addHelpButton('url', 'smartmenu:url', 'theme_boost_union'); + + // List of categories selector. + $categories = \core_course_category::make_categories_list(); + $cate = $mform->addElement('autocomplete', 'category', get_string('smartmenu:category', 'theme_boost_union'), $categories); + $mform->setType('category', PARAM_INT); + $mform->hideIf('category', 'type', 'neq', menuitem::TYPEDYNAMIC); + $cate->setMultiple(true); + $mform->addHelpButton('category', 'smartmenu:category', '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('smartmenu:enrolmentrole', 'theme_boost_union'), $enrolmentroles); + $mform->setType('enrolmentrole', PARAM_INT); + $mform->hideIf('enrolmentrole', 'type', 'neq', menuitem::TYPEDYNAMIC); + $role->setMultiple(true); + $mform->addHelpButton('enrolmentrole', 'smartmenu:enrolmentrole', 'theme_boost_union'); + + $completionstatuses = array( + menuitem::COMPLETION_ENROLLED => get_string('smartmenu:enrolled', 'theme_boost_union'), + menuitem::COMPLETION_INPROGRESS => get_string('inprogress', 'completion'), + menuitem::COMPLETION_COMPLETED => get_string('completed', 'completion'), + ); + $completion = $mform->addElement('autocomplete', 'completionstatus', + get_string('smartmenu:completionstatus', 'theme_boost_union'), $completionstatuses); + $mform->setType('completionstatus', PARAM_INT); + $mform->hideIf('completionstatus', 'type', 'neq', menuitem::TYPEDYNAMIC); + $completion->setMultiple(true); + $mform->addHelpButton('completionstatus', 'smartmenu:completionstatus', 'theme_boost_union'); + + $ranges = array( + menuitem::RANGE_PAST => get_string('smartmenu:past', 'theme_boost_union'), + menuitem::RANGE_PRESENT => get_string('smartmenu:present', 'theme_boost_union'), + menuitem::RANGE_FUTURE => get_string('smartmenu:future', 'theme_boost_union'), + ); + $range = $mform->addElement('autocomplete', 'daterange', get_string('smartmenu:daterange', 'theme_boost_union'), $ranges); + $mform->setType('daterange', PARAM_INT); + $mform->hideIf('daterange', 'type', 'neq', menuitem::TYPEDYNAMIC); + $range->setMultiple(true); + $mform->addHelpButton('daterange', 'smartmenu:daterange', '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('appearance', 'core')); + $mform->addElement('static', 'appearanceheader_desc', get_string('smartmenu:appearanceheader_desc', 'theme_boost_union')); + + // Display field value option. + $displayfields = [ + menuitem::FIELD_FULLNAME => get_string('smartmenu:fullname', 'theme_boost_union'), + menuitem::FIELD_SHORTNAME => get_string('smartmenu:shortname', 'theme_boost_union'), + ]; + $mform->addElement('select', 'displayfield', get_string('smartmenu:displayfield', 'theme_boost_union'), $displayfields); + $mform->hideIf('displayfield', 'type', 'neq', menuitem::TYPEDYNAMIC); + $mform->addHelpButton('displayfield', 'smartmenu:displayfield', 'theme_boost_union'); + + // Number of charaters to display in menu item title. + $mform->addElement('text', 'textcount', get_string('smartmenu:textcount', 'theme_boost_union')); + $mform->setType('textcount', PARAM_INT); + $mform->hideIf('textcount', 'type', 'neq', menuitem::TYPEDYNAMIC); + $mform->addHelpButton('textcount', 'smartmenu:textcount', 'theme_boost_union'); + + // Display options field. + $displayoptions = [ + menuitem::MODE_INLINE => get_string('smartmenu:inline', 'theme_boost_union'), + menuitem::MODE_SUBMENU => get_string('smartmenu:submenu', 'theme_boost_union'), + ]; + $mform->addElement('select', 'mode', get_string('smartmenu:mode', 'theme_boost_union'), $displayoptions); + $mform->addHelpButton('mode', 'smartmenu:mode', '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', 'smartmenu:menuicon', 'theme_boost_union'); + + // Include the fontaswesome 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('smartmenu:showtitleicon', 'theme_boost_union'), + menuitem::DISPLAY_HIDETITLE => get_string('smartmenu:hidetitle', 'theme_boost_union'), + menuitem::DISPLAY_HIDETITLEMOBILE => get_string('smartmenu:hidetitlemobile', 'theme_boost_union') + ]; + $mform->addElement('select', 'display', get_string('smartmenu:displayoptions', 'theme_boost_union'), $displayoptions); + $mform->addHelpButton('display', 'smartmenu:displayoptions', 'theme_boost_union'); + + // Tooltip field. + $mform->addElement('text', 'tooltip', get_string('smartmenu:tooltip', 'theme_boost_union')); + $mform->setType('tooltip', PARAM_TEXT); + $mform->addHelpButton('tooltip', 'smartmenu:tooltip', 'theme_boost_union'); + + // Order field. + $mform->addElement('text', 'sortorder', get_string('smartmenu:order', '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', 'smartmenu:order', '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('smartmenu:target', 'theme_boost_union'), $targetoptions); + $mform->addHelpButton('target', 'smartmenu:target', 'theme_boost_union'); + + // CSS class field. + $mform->addElement('text', 'cssclass', get_string('smartmenu:cssclass', 'theme_boost_union')); + $mform->setType('cssclass', PARAM_TEXT); + $mform->addHelpButton('cssclass', 'smartmenu:cssclass', 'theme_boost_union'); + + // Responsive fields. + $group = []; + // Hide in Desktop. + $group[] = $mform->createElement('advcheckbox', 'desktop', + get_string('smartmenu:responsivedesktop', 'theme_boost_union'), null, ['group' => 1]); + // Hide in Tablet. + $group[] = $mform->createElement('advcheckbox', 'tablet', + get_string('smartmenu:responsivetablet', 'theme_boost_union'), null, ['group' => 1]); + // Hide in mobile. + $group[] = $mform->createElement('advcheckbox', 'mobile', + get_string('smartmenu:responsivemobile', 'theme_boost_union'), null, ['group' => 1]); + $mform->addGroup($group, 'responsive', get_string('smartmenu:responsive', 'theme_boost_union'), '', false); + // Select all controller. + $this->add_checkbox_controller(1); + + // Appearance section. + $mform->addElement('header', 'appearance_card', get_string('smartmenu:appearancecard', 'theme_boost_union')); + $mform->addElement('static', 'appearancecard_desc', get_string('smartmenu:appearancecard_desc', 'theme_boost_union')); + + // Appearance options for cards. + $options = menuitem::image_fileoptions(); + $mform->addElement('filemanager', 'image', get_string('smartmenu:image', 'theme_boost_union'), null, $options); + $mform->addHelpButton('image', 'smartmenu:image', 'theme_boost_union'); + + $textpositionoptions = array( + menuitem::POSITION_BELOW => get_string('smartmenu:belowimage', 'theme_boost_union'), + menuitem::POSITION_OVERLAYTOP => get_string('smartmenu:overlaytop', 'theme_boost_union'), + menuitem::POSITION_OVERLAYBOTTOM => get_string('smartmenu:overlaybottom', 'theme_boost_union') + ); + $mform->addElement('select', 'textposition', + get_string('smartmenu:textposition', 'theme_boost_union'), $textpositionoptions); + $mform->setDefault('textposition', 'theme_boost_union'); + $mform->addHelpButton('textposition', 'smartmenu:textposition', 'theme_boost_union'); + + // Text color. + $mform->addElement('theme_boost_unioncolorpicker', 'textcolor', get_string('smartmenu:textcolor', 'theme_boost_union')); + $mform->addHelpButton('textcolor', 'smartmenu:textcolor', 'theme_boost_union'); + + // Background color. + $mform->addElement('theme_boost_unioncolorpicker', 'backgroundcolor', + get_string('smartmenu:backgroundcolor', 'theme_boost_union')); + $mform->addHelpButton('backgroundcolor', 'smartmenu:backgroundcolor', 'theme_boost_union'); + + // Access rule by roles. + $mform->addElement('header', 'accessbyroles', get_string('smartmenu:accessbyroles', '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('smartmenu:byrole', 'theme_boost_union'), $roles); + $mform->addHelpButton('roles', 'smartmenu:byrole', 'theme_boost_union'); + $roles->setMultiple(true); + + $rolecontext = [ + smartmenu::ANYCONTEXT => get_string('any'), + smartmenu::SYSTEMCONTEXT => get_string('coresystem'), + ]; + $mform->addElement('select', 'rolecontext', get_string('smartmenu:rolecontext', 'theme_boost_union'), $rolecontext); + $mform->addHelpButton('rolecontext', 'smartmenu:rolecontext', 'theme_boost_union'); + + // Access rule by cohorts. + $mform->addElement('header', 'accessbycohorts', get_string('smartmenu:accessbycohorts', '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('smartmenu:bycohort', 'theme_boost_union'), $cohortoptions); + + $mform->addHelpButton('cohorts', 'smartmenu:bycohort', 'theme_boost_union'); + $cohort->setMultiple(true); + + $operator = [ + smartmenu::ANY => get_string('any'), + smartmenu::ALL => get_string('all'), + ]; + $mform->addElement('select', 'operator', get_string('smartmenu:operator', 'theme_boost_union'), $operator); + $mform->addHelpButton('operator', 'smartmenu:operator', 'theme_boost_union'); + + // Access rule by languages. + $mform->addElement('header', 'accessbylanguage', get_string('smartmenu:accessbylanguage', '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('smartmenu:bylanguage', 'theme_boost_union'), $langoptions); + $language->setMultiple(true); + $mform->addHelpButton('languages', 'smartmenu:bylanguage', 'theme_boost_union'); + + // Access rule by dates. + $mform->addElement('header', 'accessbydateselector', get_string('smartmenu:accessbydateselector', 'theme_boost_union')); + // Prevent the menu display until the start date is reached. + $mform->addElement('date_time_selector', 'start_date', + get_string('smartmenu:from', 'theme_boost_union'), array('optional' => true)); + $mform->addHelpButton('start_date', 'smartmenu:from', 'theme_boost_union'); + // Hide the item, if the end date is reached. + $mform->addElement('date_time_selector', 'end_date', + get_string('smartmenu:durationuntil', 'theme_boost_union'), array('optional' => true)); + $mform->addHelpButton('end_date', 'smartmenu:durationuntil', '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..ef146faa7d6 --- /dev/null +++ b/classes/output/navigation/primary.php @@ -0,0 +1,201 @@ +. + +/** + * Theme Boost Union - Primary navigation render. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_boost_union\output\navigation; + +use custom_menu; +use \theme_boost_union\smartmenu; +use renderer_base; + +/** + * Extending the \core\navigation\output\primary renderer. + */ +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. + * + * Updated the export_for_template method of \core\navigation\output\primary from lib\classes\navigation\output\primary.php. + * + * Build the smartmenus 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, $PAGE; + + $dbman = $DB->get_manager(); + // Backward support check. + 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(); + + // Get the menus for mainmenu. + $mainmenu = smartmenu::get_menus_forlocation(smartmenu::LOCATION_MAIN, $smartmenus); + + // Separate the menus for menubar. + $menubarmenus = smartmenu::get_menus_forlocation(smartmenu::LOCATION_MENU, $smartmenus); + // Separate the menus for usermenus. + $locationusermenus = smartmenu::get_menus_forlocation(smartmenu::LOCATION_USER, $smartmenus); + // Separate the menus for bottom menu. + $locationbottom = smartmenu::get_menus_forlocation(smartmenu::LOCATION_BOTTOM, $smartmenus); + + // Merge the smartmenu nodes which contians the main menu location with 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 contians the location for usermenu, with the default core usermenu 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 smartmenus to usermenu which has location selected for usermenu. + * Seperate the children items and attach those items to submenus element in usermenu. + * Add the menus in items element in usermenu. + * + * Usermenu and its submenus are connected using submenuid. Added submenuid for submenu items if that has childrens. + * Add all the items before logout menu. Removed the logout menu, then add the items into usermenu items, + * once all items are added, sperator 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 childrens and push them into submenus. + if (isset($menu->submenuid)) { + $children = $menu->children; + // Update the dividers itemtype. + 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 avialble, therfore implemeneted in a static way, incase wants 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 childrens 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..31067b37c40 --- /dev/null +++ b/classes/smartmenu.php @@ -0,0 +1,880 @@ +. + +/** + * Menu controller for managing menus and menu items. Build menu for different locations. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @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 bdecent GmbH 2023 + * @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; + + /** + * 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 = $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. + cache_helper::purge_by_event('theme_boost_union_menus_deleted'); + 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 menu cache. + cache_helper::purge_by_event('theme_boost_union_menus_resorted'); + + 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 menu cache. + cache_helper::purge_by_event('theme_boost_union_menus_resorted'); + + 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('smartmenu:menuduplicated', 'theme_boost_union')); + + 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($this->id); + + 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($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. + * + * @return false|object Returns false if the menu is not visible or a menu object otherwise. + */ + public function build() { + global $OUTPUT; + + if (!$this->is_visible()) { + return false; + } + + // Cache for menu. + $cache = cache::make('theme_boost_union', 'smartmenus'); + + // Purge the cached menus data if the menu date restrictions are reached or passed. + smartmenu_helper::purge_cache_date_reached($cache, $this->menu, 'theme_boost_union_menulastcheckdate'); + + // If the flag to purge the menu cache is set for this user. + if (get_user_preferences('theme_boost_union_menu_purgesessioncache', false) == true) { + // Purge the menu cache for this user. + \cache_helper::purge_by_definition('theme_boost_union', 'smartmenus'); + \smartmenu_helper::clear_user_cachepreferencemenu(); + } + + // Get the menu and its menu items from cache. + if ($nodescache = $cache->get($this->menu->id)) { + return $nodescache; + } + + $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 = $this->get_menu_items(); + if (!empty($menuitems)) { + + $builditems = []; + foreach ($menuitems as $item) { + // Build the item based on restrict rules and its type like static, dynamic. + $item = \theme_boost_union\smartmenu_item::instance($item->id)->build(); + // Merge the dynamic course items as single item. + $builditems = (!empty($item)) ? array_merge($builditems, $item) : $builditems; + } + + if (isset($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)) { + $cache->set($this->menu->id, $nodes); + } + + 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 []; + } + foreach ($menus as $menu) { + + $menu = (object) $menu; + + if (isset($menu->menudata->location)) { + $menulocation = $menu->menudata->location; + } + if (isset($menu->itemdata->location)) { + $menulocation = $menu->itemdata->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; + } + } + + 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])) { + $record->description = [ + 'text' => $record->description, + 'format' => $record->description_format + ]; + // Decode the multiple option select elements values to array. + $record->location = json_decode($record->location); + $record->roles = json_decode($record->roles); + $record->cohorts = json_decode($record->cohorts); + $record->languages = json_decode($record->languages); + $record->mode = $record->mode ?? self::MODE_SUBMENU; // Submenu is default menu mode. + + return $record; + } else { + // TODO: string for menu not found. + throw new moodle_exception('menunotfound', 'theme_boost_union'); + } + return false; + } + + /** + * 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('smartmenu:location:main', 'theme_boost_union'), + self::LOCATION_MENU => get_string('smartmenu:location:menu', 'theme_boost_union'), + self::LOCATION_USER => get_string('smartmenu:location:user', 'theme_boost_union'), + self::LOCATION_BOTTOM => get_string('smartmenu:location:bottom', '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('smartmenu:types:list', 'theme_boost_union'), + self::TYPE_CARD => get_string('smartmenu:types:card', '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 bool + */ + 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); + + $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. + \cache_helper::purge_by_event('theme_boost_union_menus_edited'); + + // Delete the item cached. + \cache_helper::purge_by_event('theme_boost_union_menuitems_edited'); + + // Show the edited success notification. + \core\notification::success(get_string('smartmenu:updatesuccess', '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); + + \cache_helper::purge_by_event('theme_boost_union_menus_created'); + // Show the menu inserted success notification. + \core\notification::success(get_string('smartmenu:insertsuccess', '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() { + $nodes = []; + // Get top level menus. + $topmenus = self::get_menus(); + foreach ($topmenus as $menu) { + if ($node = self::instance($menu->id)->build()) { + if (isset($node->menudata)) { + $nodes[] = $node; + } else { + $nodes = array_merge($nodes, array_values((array) $node)); + } + } + } + return $nodes; + } +} diff --git a/classes/smartmenu_item.php b/classes/smartmenu_item.php new file mode 100644 index 00000000000..e15266bab05 --- /dev/null +++ b/classes/smartmenu_item.php @@ -0,0 +1,1298 @@ +. + +/** + * Item controller for managing menu items. Build the item as node to attach as submenu. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @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 bdecent GmbH 2023 + * @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 $id The ID of the item to retrieve. + * @return smartmenu_item A new instance of this class. + */ + public static function instance($id) { + return new self($id); + } + + /** + * Menu item constructor, Retrive the item data, Create smartmenu_helper for this item, + * Creates the cache instance for item and its menu. + * + * @param int $id Record id of the menu. + */ + public function __construct(int $id) { + + // Item id. + $this->id = $id; + + // Item data. + $this->item = $this->get_item($id); + + // Menu data, the current item belongs to. + $this->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'); + + // Cache instance for the item`s menus. + $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() { + // Delete this item cached. + if ($this->cache->has($this->id)) { + $this->cache->delete($this->id); + } + // Delete the cached data of current items menu. + if ($this->menucache->has($this->item->menu)) { + $this->menucache->delete($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. + * + * @return \stdclass Menu record if found or false. + * @throws \moodle_exception When menu is not found. + */ + public function get_item() { + global $DB; + + // Verfiy and Fetch menu record from DB. + if ($record = $DB->get_record('theme_boost_union_menuitems', ['id' => $this->id])) { + + // Decode the multiple option select elements values to array. + $record->category = json_decode($record->category) ?: []; + $record->enrolmentrole = json_decode($record->enrolmentrole) ?: []; + $record->completionstatus = json_decode($record->completionstatus) ?: []; + $record->daterange = json_decode($record->daterange) ?: []; + + // Restrict access rules. + $record->roles = json_decode($record->roles) ?: []; + $record->cohorts = json_decode($record->cohorts) ?: []; + $record->languages = json_decode($record->languages) ?: []; + + // Seperate the customfields. + $customfields = json_decode($record->customfields) ?: []; + foreach ($customfields as $field => $value) { + $record->{'customfield_'.$field} = $value; + } + return $record; + + } else { + // TODO: string for menu not found. + throw new \moodle_exception('itemnotfound', 'theme_boost_union'); + } + + return false; + } + + /** + * 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 cache and menu cache. + $this->delete_cache(); + + // Purge the menu items cache. + \cache_helper::purge_by_event('theme_boost_union_menuitems_sorted'); + + 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 cache and menu cache. + $this->delete_cache(); + + // Purge the menu items cache. + \cache_helper::purge_by_event('theme_boost_union_menuitems_sorted'); + + 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('smartmenu:menuitemduplicated', '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) { + // 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 " : ''; + + // 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() { + + // If the flag to purge the menuitems cache is set for this user. + if (get_user_preferences('theme_boost_union_menuitem_purgesessioncache', false) == true) { + // Purge the menuitems cache for this user. + \cache_helper::purge_by_definition('theme_boost_union', 'smartmenu_items'); + // Clear the user preference for purge the menuitem cache. + \smartmenu_helper::clear_user_cachepreferenceitem(); + } + + // Cache for menu. + $cache = cache::make('theme_boost_union', 'smartmenu_items'); + + // Purge the cached menus data if the menu date restrictions are reached or passed. + smartmenu_helper::purge_cache_date_reached($cache, $this->item, 'theme_boost_union_menuitemlastcheckdate'); + + // Get the node data for item from cache if it is stored. + if ($result = $cache->get($this->item->id)) { + 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. + $cache->set($this->item->id, $result); + // Delete the cache of items menu, Recreate the menu. + $this->menucache->delete($this->item->menu); + + 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; + } + } + + $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 ?: $this->get_itemimage($this->item->id), + '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); + if ("select" == $field->get('type')) { + $elem = $mform->getElement("customfield_".$shortname); + $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"], function(Auto) { + // List of custom fields. + var dropdowns = document.querySelectorAll("div[data-fieldtype=select] [id^=id_customfield_]"); + dropdowns.forEach((elem) => { + Auto.enhance(elem); + }); + })'); + } + + /** + * 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('smartmenu:static', 'theme_boost_union'), + self::TYPEDYNAMIC => get_string('smartmenu:dynamiccourses', '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('smartmenu:showtitleicon', 'theme_boost_union'), + self::DISPLAY_HIDETITLE => get_string('smartmenu:hidetitle', 'theme_boost_union'), + self::DISPLAY_HIDETITLEMOBILE => get_string('smartmenu:hidetitlemobile', '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 menu. + $menucache = cache::make('theme_boost_union', 'smartmenus'); + + 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 item cached. + \cache_helper::purge_by_event('theme_boost_union_menuitems_edited'); + // Delete the cached data of its menu. + \cache_helper::purge_by_event('theme_boost_union_menus_edited'); + + // Show the edited success notification. + \core\notification::success(get_string('smartmenu:updatesuccess', '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('smartmenu:insertsuccess', 'theme_boost_union')); + + // Delete the cached data of its menu. Menu will recreate with this item. + $menucache->delete($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/menuitems.php b/classes/table/menuitems.php new file mode 100644 index 00000000000..eacd2a1258b --- /dev/null +++ b/classes/table/menuitems.php @@ -0,0 +1,268 @@ +. + +/** + * Table to list the items for menu. Display the items access rules and it type. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @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 menuitems 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('smartmenu:title', 'theme_boost_union'), + get_string('smartmenu:types', 'theme_boost_union'), + get_string('smartmenu:restriction', 'theme_boost_union'), + get_string('action'), + ]; + + $this->define_columns($columns); + $this->define_headers($headers); + + // Remove sorting for some fields. + $this->sortable(false, 'sortorder', SORT_ASC); + + $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('smartmenu:byrole', '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('smartmenu:bycohort', '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('smartmenu:bylanguage', 'theme_boost_union'), + 'value' => implode(' ', $list) + ]; + } + + if ($row->start_date) { + $rules[] = [ + 'name' => get_string('smartmenu:from', 'theme_boost_union'), + 'value' => userdate($row->start_date, get_string('strftimedate', 'core_langconfig') ) + ]; + + } + if ($row->end_date) { + $rules[] = [ + 'name' => get_string('smartmenu:durationuntil', '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('smartmenu:norestrict', '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('smartmenu:copyitem', '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('smartmenu:deleteconfirmitem', '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. + echo $OUTPUT->heading(get_string('itemsnothingtodisplay', 'theme_boost_union')); + } +} diff --git a/classes/table/smartmenu.php b/classes/table/smartmenu.php new file mode 100644 index 00000000000..8860a094827 --- /dev/null +++ b/classes/table/smartmenu.php @@ -0,0 +1,247 @@ +. + +/** + * Table to list the menus. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @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 smartmenu 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('smartmenu:title', 'theme_boost_union'), + get_string('smartmenu:description', 'theme_boost_union'), + get_string('smartmenu:location', 'theme_boost_union'), + get_string('smartmenu:types', 'theme_boost_union'), + get_string('action'), + ]; + + $this->define_columns($columns); + $this->define_headers($headers); + + // Remove sorting for some fields. + $this->sortable(false, 'sortorder', SORT_ASC); + + $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('smartmenu:copymenu', '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('smartmenu:deleteconfirmmenu', '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('menusnothingtodisplay', 'theme_boost_union'), \core\output\notification::NOTIFY_INFO); + + echo $OUTPUT->heading(get_string('menusnothingtodisplay', 'theme_boost_union')); + } +} diff --git a/db/caches.php b/db/caches.php index 89e18458f82..af99f754ed1 100644 --- a/db/caches.php +++ b/db/caches.php @@ -67,5 +67,38 @@ 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true, + ), + // This cache stores the menus and menu items. + 'smartmenus' => array( + 'mode' => cache_store::MODE_SESSION, + 'simplekeys' => true, + 'simpledata' => false, + 'invalidationevents' => array( + 'theme_boost_union_menus_created', + 'theme_boost_union_menus_edited', + 'theme_boost_union_menus_resorted', + 'theme_boost_union_menus_deleted', + 'theme_boost_union_cohort_deleted', + 'theme_boost_union_roles_deleted', + 'theme_boost_union_user_updated', + 'theme_boost_union_course_updated' + + ) + ), + // This cache stores the menus and menu items. + 'smartmenu_items' => array( + 'mode' => cache_store::MODE_SESSION, + 'simplekeys' => true, + 'simpledata' => false, + 'invalidationevents' => array( + 'theme_boost_union_menuitems_created', + 'theme_boost_union_menuitems_edited', + 'theme_boost_union_menuitems_resorted', + 'theme_boost_union_menuitems_deleted', + 'theme_boost_union_cohort_deleted', + 'theme_boost_union_roles_deleted', + 'theme_boost_union_user_updated', + 'theme_boost_union_course_updated' + ) ) ); diff --git a/db/events.php b/db/events.php index b25a33e93ef..74d9db04217 100644 --- a/db/events.php +++ b/db/events.php @@ -37,4 +37,36 @@ '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_updated', + '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' + ) ); diff --git a/db/install.xml b/db/install.xml index ddb4c48b1e4..66c8ffe7bad 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..4ff29202f44 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_TEXT, null, null, null, null, null, 'description'); + $table->add_field('showdesc', XMLDB_TYPE_INTEGER, '2', 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, '2', null, XMLDB_NOTNULL, null, '0', 'location'); + $table->add_field('mode', XMLDB_TYPE_INTEGER, '2', 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, '2', XMLDB_UNSIGNED, null, null, '1', 'cssclass'); + $table->add_field('cardsize', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'moremenubehavior'); + $table->add_field('cardform', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'cardsize'); + $table->add_field('overflowbehavior', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'cardform'); + $table->add_field('roles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'overflowbehavior'); + $table->add_field('rolecontext', XMLDB_TYPE_INTEGER, '2', 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, '2', 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, '2', 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, null, null, null, 'title'); + $table->add_field('type', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'menu'); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '18', XMLDB_UNSIGNED, null, null, null, 'showdesc'); + $table->add_field('url', XMLDB_TYPE_TEXT, null, null, null, null, null, 'title'); + $table->add_field('category', XMLDB_TYPE_TEXT, null, null, null, null, null, 'description'); + $table->add_field('enrolmentrole', XMLDB_TYPE_TEXT, null, null, null, null, null, 'description'); + $table->add_field('completionstatus', XMLDB_TYPE_CHAR, '20', null, null, null, null, 'sortorder'); + $table->add_field('daterange', XMLDB_TYPE_CHAR, '20', null, null, null, null, 'sortorder'); + $table->add_field('customfields', XMLDB_TYPE_TEXT, null, null, null, null, null, 'daterange'); + $table->add_field('displayfield', XMLDB_TYPE_INTEGER, 2, null, null, null, null, 'type'); + $table->add_field('textcount', XMLDB_TYPE_INTEGER, 9, null, null, null, null, 'displayfield'); + $table->add_field('mode', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, null, 'customfields'); + $table->add_field('menuicon', XMLDB_TYPE_CHAR, '50', null, null, null, null, 'mode'); + $table->add_field('display', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, null, 'menuicon'); + $table->add_field('tooltip', XMLDB_TYPE_CHAR, '255', null, null); + $table->add_field('target', XMLDB_TYPE_INTEGER, '2', 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, '2', 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, '2', XMLDB_UNSIGNED, null, null, '1', 'backgroundcolor'); + $table->add_field('tablet', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'desktop'); + $table->add_field('mobile', XMLDB_TYPE_INTEGER, '2', 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, '2', null, XMLDB_NOTNULL, null, '1', 'roles'); + $table->add_field('cohorts', XMLDB_TYPE_TEXT, null, null, null, null, null, 'rolecontext'); + $table->add_field('operator', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, 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, '1', 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/lang/en/theme_boost_union.php b/lang/en/theme_boost_union.php index def0aac79b0..c2d0cdeedc2 100644 --- a/lang/en/theme_boost_union.php +++ b/lang/en/theme_boost_union.php @@ -755,3 +755,215 @@ $string['upgradenotice_2022080922_logocompact'] = 'compact logo'; $string['upgradenotice_2022080922_copied'] = 'The existing {$a} from the Moodle core settings has been copied to the Boost Union {$a} setting during this upgrade. Please double-check the result.'; $string['upgradenotice_2022080922_notcopied'] = 'The {$a} setting within Boost Union is empty now. If you want to use a {$a} within Boost Union from now on, just upload it into the Boost Union {$a} setting later.'; + + +// Smart menus. +// ...Smart menu edit form. +$string['smartmenus'] = 'Smart menus'; +$string['smartmenus_desc'] = '"Smart Menu" allows 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 administrator 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.'; +// Smartmenu: Menu form element strings. +$string['smartmenu:generalsection'] = 'General sections'; +$string['smartmenu:title'] = 'Title'; +$string['smartmenu:showdescription'] = 'Show description'; +$string['smartmenu:description'] = 'Description'; +$string['smartmenu:location'] = 'Locations'; +$string['smartmenu:location:main'] = 'Main navigation'; +$string['smartmenu:location:menu'] = 'Menu bar'; +$string['smartmenu:location:user'] = 'User Menu'; +$string['smartmenu:location:bottom'] = 'Bottom bar'; +$string['smartmenu:types'] = 'Types'; +$string['smartmenu:types:list'] = 'List'; +$string['smartmenu:types:card'] = 'Card'; +$string['smartmenu:top'] = 'Top'; +$string['smartmenu:never'] = 'Never'; +$string['smartmenu:above'] = 'Above'; +$string['smartmenu:below'] = 'Below'; +$string['smartmenu:help'] = 'Help'; +$string['smartmenu:advancedsettings'] = 'Advanced settings'; +$string['smartmenu:cssclass'] = 'CSS classes'; +$string['smartmenu:default'] = 'Default'; +$string['smartmenu:forcedintomoremenu'] = 'Forced into more menu'; +$string['smartmenu:forcedoutsideofmoremenu'] = 'Forced out side of moremenu'; +$string['smartmenu:moremenubehavior'] = 'More menu behavior'; +$string['smartmenu:cardappearance'] = 'Card appearance'; +$string['smartmenu:tiny'] = 'Tiny'; +$string['smartmenu:small'] = 'Small'; +$string['smartmenu:medium'] = 'Medium'; +$string['smartmenu:large'] = 'Large'; +$string['smartmenu:cardsize'] = 'Card size'; +$string['smartmenu:square'] = 'Square'; +$string['smartmenu:portrait'] = 'Portrait'; +$string['smartmenu:landscape'] = 'Landscape'; +$string['smartmenu:fullwidth'] = 'Full width'; +$string['smartmenu:cardform'] = 'Card form'; +$string['smartmenu:no_wrap'] = 'No wrap'; +$string['smartmenu:wrap'] = 'Wrap'; +$string['smartmenu:overflowbehavior'] = 'Overflow behavior'; +$string['smartmenu:accessrules'] = 'Access rules'; +$string['smartmenu:allroles'] = 'All roles'; +$string['smartmenu:byrole'] = 'By role'; +$string['smartmenu:anycohort'] = 'Any cohort'; +$string['smartmenu:bycohort'] = 'By cohort'; +$string['smartmenu:anylanguage'] = 'Any language'; +$string['smartmenu:bylanguage'] = 'By language'; +$string['smartmenu:createmenu'] = 'Create new smart menu'; +$string['smartmenu:insertsuccess'] = 'Smart menu created successfully'; +$string['smartmenu:updatesuccess'] = 'Smart menu updated successfully'; +$string['smartmenu:menudeleted'] = 'Smart menu deleted successfully'; +// Smartmenu: Menu form element strings. +$string['smartmenu:menutitle'] = 'Title'; +$string['smartmenu:description'] = 'Description'; +$string['smartmenu:static'] = 'Static'; +$string['smartmenu:type'] = 'Type'; +$string['smartmenu:dynamiccourses'] = 'Dynamic courses'; +$string['smartmenu:completionstatus'] = 'Completion status'; +$string['smartmenu:enrolmentrole'] = 'Enrolment role'; +$string['smartmenu:enrolled'] = 'Enrolled'; +$string['smartmenu:past'] = 'Past'; +$string['smartmenu:present'] = 'Present'; +$string['smartmenu:future'] = 'Future'; +$string['smartmenu:daterange'] = 'Date range'; +$string['smartmenu:appearanceheader_desc'] = 'Use these settings to customize the look and feel of the smart menu item'; +$string['smartmenu:showtitleicon'] = 'Show title icon'; +$string['smartmenu:hidetitle'] = 'Hide title'; +$string['smartmenu:hidetitlemobile'] = 'Hide title in mobile'; +$string['smartmenu:tooltip'] = 'Tooltip'; +$string['smartmenu:tooltip_help'] = 'Falls back to title if not set'; +$string['smartmenu:target'] = 'Target'; +$string['smartmenu:target_help'] = 'Target help'; +$string['smartmenu:responsivedesktop'] = 'Desktop'; +$string['smartmenu:responsivetablet'] = 'Tablet'; +$string['smartmenu:responsivemobile'] = 'Mobile'; +$string['smartmenu:responsive'] = 'Responsive'; +$string['smartmenu:appearancecard'] = 'Card appearance'; +$string['smartmenu:appearancecard_desc'] = 'Use these settings to customize the look and feel of the smart menu item when using cards'; +$string['smartmenu:belowimage'] = 'Below image'; +$string['smartmenu:overlaytop'] = 'Top overlay'; +$string['smartmenu:overlaybottom'] = 'Bottom overlay'; +$string['smartmenu:textposition'] = 'Text position'; +$string['smartmenu:textcolor'] = 'Text color'; +$string['smartmenu:image'] = 'Image'; +$string['smartmenu:backgroundcolor'] = 'Background color'; +$string['smartmenu:displayoptions'] = 'Display options'; +$string['smartmenu:menus'] = 'Menus'; +$string['smartmenu:menuitems'] = 'Menu items'; + +// Smartmenu: Menu items strings. +$string['smartmenu:backtomenus'] = 'Back to all smart menus'; +$string['smartmenu:settings'] = 'Smart menu settings'; +$string['smartmenu:addnewitem'] = 'Add new item'; +$string['smartmenu:addnewheadingitem'] = 'Add new heading item'; +$string['smartmenu:addnewstaticitem'] = 'Add new static item'; +$string['smartmenu:addnewdynamicitem'] = 'Add new dynamic item'; +// Smartmenu: Menu items form elements text. +$string['smartmenu:menuheading'] = ' "{$a}" - items'; +$string['smartmenu:rolecontext'] = 'Context'; +$string['smartmenu:accessbyroles'] = 'Restrict access by roles'; +$string['smartmenu:accessbycohorts'] = 'Restrict access by cohorts'; +$string['smartmenu:accessbylanguage'] = 'Restrict access by language'; +$string['smartmenu:accessbydateselector'] = 'Restrict access by date'; +$string['smartmenu:operator'] = 'Operator'; +$string['smartmenu:submenu'] = 'Submenu'; +$string['smartmenu:inline'] = 'Inline'; +$string['smartmenu:mode'] = 'Mode'; +$string['smartmenu:menuduplicated'] = 'Menu and its menu items duplicated successfully'; +$string['smartmenu:menuitemduplicated'] = 'Menu item duplicated successfully'; +$string['smartmenu:restriction'] = 'Access rules'; +$string['smartmenu:norestrict'] = 'No restricted'; +$string['savechangesandconfigure'] = 'Save and configure items'; +$string['smartmenussettins'] = 'Smart menu - settings'; +$string['smartmenu:deleteconfirmmenu'] = 'Are you sure you want to delete this menu from the smart menus?'; +$string['smartmenu:deleteconfirmitem'] = 'Are you sure you want to delete this menu item from the smart menus?'; +$string['smartmenu:copyitem'] = 'Copy menu item'; +$string['smartmenu:copymenu'] = 'Copy menu and it items'; +$string['menusnothingtodisplay'] = "There aren't any smart menus are created. Please create your first smart menu."; +$string['itemsnothingtodisplay'] = "There aren't any items are created for this menu. Please add first item to this menu."; +$string['smartmenu:menuicon'] = 'Icon'; +$string['smartmenu:order'] = 'Order'; +$string['smartmenu:from'] = 'From'; +$string['smartmenu:durationuntil'] = 'Until'; +$string['smartmenu:category'] = 'Category'; +$string['smartmenu:url'] = 'Menu URL'; +// ...Smartmenu form elements help text. +$string['smartmenu:description_help'] = 'Description about the menu'; +$string['smartmenu:menutitle_help'] = 'Enter the title for menu.'; +$string['smartmenu:location_help'] = "Select the location where you want the menu to appear on the page. 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 profile 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['smartmenu:types_help'] = 'Select the type of menu you want to create, choosing between card, and list.'; +$string['smartmenu:moremenubehavior_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['smartmenu:cardsize_help'] = 'Set the size of the card for card-style menus. choosing between tiny, small, medium, or large'; +$string['smartmenu:cardform_help'] = 'Select the form of the card for card-style menus, choosing between square, portrait, landscape or fullwidth'; +$string['smartmenu:overflowbehavior_help'] = 'Choose how the menu should behave when it overflows its container, either by showing a scrollbar or wrap the overflowing items.'; +// Help strings for menu item form elements. +$string['smartmenu:title_help'] = "Enter the title for the menu item. Use hash signs (###) to display the menu item as a separator for heading type items."; +$string['smartmenu:type_help'] = "Select the type of menu item you want to create: static, heading, or dynamic.
+
    +
  • 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: A dynamic 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 menu item will update automatically as the criteria changes.
  • +
"; +$string['smartmenu:url_help'] = "Enter the URL for the menu item. This is the link that will be followed when the menu item is clicked."; +$string['smartmenu:enrolmentrole_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['smartmenu:completionstatus_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['smartmenu:daterange_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['smartmenu:mode_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['smartmenu:menuicon_help'] = "Select an icon to display with the menu item title."; +$string['smartmenu:displayoptions_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['smartmenu:tooltip_help'] = "Enter a tooltip to display when the user hovers over the menu item."; +$string['smartmenu:sortorder_help'] = "Enter a sort order for the menu item. Menu items are displayed in ascending order based on their sort order."; +$string['smartmenu:target_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['smartmenu:cssclass_help'] = "Enter a CSS class for the menu item. This can be used to apply custom styling to the menu item."; +$string['smartmenu:image_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['smartmenu:textposition_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['smartmenu:textcolor_help'] = "Select the color for the menu item text."; +$string['smartmenu:backgroundcolor_help'] = "Select the background color for the menu item."; +$string['smartmenu:byrole_help'] = "Select whether to show or hide the menu item based on the user's role."; +$string['smartmenu:rolecontext_help'] = "Select the context for which the user's role should be checked (e.g. any or system context)."; +$string['smartmenu:bycohort_help'] = "Select whether to show or hide the menu item based on the user's cohort."; +$string['smartmenu:operator_help'] = "Select the operator for the cohort condition (e.g. \"Any\" or \"All\")."; +$string['smartmenu:bylanguage_help'] = "Select whether to show or hide the menu item based on the user's language."; +$string['smartmenu:order_help'] = 'Rearrange the position of item'; +$string['smartmenu:from_help'] = 'Select the date to display the menu items to users until the date reached'; +$string['smartmenu:durationuntil_help'] = 'Select the date to hide the menu items to users once the date reached'; +$string['smartmenu:category_help'] = 'Select a category to display its courses as menu items'; +$string['smartmenu:menumode'] = 'Menu mode'; +$string['smartmenu:menumode_help'] = "Select the mode for where the menu should be visible. +
  • +Inline: items in the menu will be displayed as a menu directly in the naivgation. This option did not support for card type menus. +
  • +
  • Submenu: This is default option, items will be display as submenu of this menu.
"; + +$string['smartmenu:shortname'] = 'Short name'; +$string['smartmenu:fullname'] = 'Full name'; +$string['smartmenu:displayfield'] = 'Select name field'; +$string['smartmenu:textcount'] = 'Number of words'; +$string['smartmenu:displayfield_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['smartmenu:textcount_help'] = 'Specify the maximum number of words to be displayed for the course title in the menu.'; diff --git a/layout/columns2.php b/layout/columns2.php index 30cb393ea8e..03c5050ffe3 100644 --- a/layout/columns2.php +++ b/layout/columns2.php @@ -28,6 +28,7 @@ * * Include static pages * * Include Jvascript disabled hint * * Include info banners + * * Load the theme_boost_union/output/navigation/primary instead of core primary navigation. * * @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(); @@ -94,6 +97,8 @@ 'headercontent' => $headercontent, 'overflow' => $overflow, 'addblockbutton' => $addblockbutton, + 'menubar' => $primarymenu['menubar'] ?? [], + 'bottombar' => $primarymenu['bottombar'] ?? [] ]; // Include the template content for the course related hints. diff --git a/layout/drawers.php b/layout/drawers.php index b20a9cca0ba..499701d79b9 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 + * * Load the theme_boost_union/output/navigation/primary instead of core primary navigation. * * @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(); @@ -153,7 +156,9 @@ 'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu), 'overflow' => $overflow, 'headercontent' => $headercontent, - 'addblockbutton' => $addblockbutton + 'addblockbutton' => $addblockbutton, + 'menubar' => $primarymenu['menubar'] ?? [], + 'bottombar' => $primarymenu['bottombar'] ?? [] ]; // Include the template content for the course related hints. diff --git a/lib.php b/lib.php index 7ed82d47420..c76a11a94b7 100644 --- a/lib.php +++ b/lib.php @@ -481,6 +481,16 @@ 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); + } else if ($filearea === 'smartmenus_itemimage' && $context->contextlevel === CONTEXT_SYSTEM) { + // Serve smart menu card image for items. + $fs = get_file_storage(); + + $file = $fs->get_file($context->id, 'theme_boost_union', $filearea, $args[0], '/', $args[1]); + if (!$file) { + send_file_not_found(); + } + send_stored_file($file, DAYSECS * 90, 0, $forcedownload, $options); + } else { send_file_not_found(); } @@ -517,3 +527,34 @@ 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; + + if ($args['context']) { + $data = []; + $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, ''); + foreach ($iconlist as $iconkey => $icontxt) { + $icon = explode(':', $iconkey); + $iconstr = isset($icon[1]) ? $icon[1] : 'moodle'; + $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 $OUTPUT->render_from_template('theme_boost_union/fontawesome_list', ['options' => $icons]); + } +} diff --git a/scss/boost_union/post.scss b/scss/boost_union/post.scss index 91bdf3bbf0e..e88554f201c 100644 --- a/scss/boost_union/post.scss +++ b/scss/boost_union/post.scss @@ -44,6 +44,7 @@ } .nav-link.active { color: $white; + border-bottom-color: $white; } .nav-link:hover, .nav-link:focus { @@ -62,14 +63,10 @@ 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 { + .usermenu .dropdown-toggle, + .dropdown-menu .dropdown-item .icon { color: #c8c8c8 !important; /* stylelint-disable-line declaration-no-important */ } - .nav-link:hover .icon, - .nav-link:hover a .icon, - .usermenu .dropdown-toggle:hover { - color: $white !important; /* stylelint-disable-line declaration-no-important */ - } /* Revert color of the close icon in the search panel in the navbar as this icon is still dark on white within the input form. */ @@ -990,3 +987,1045 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } } } + +/* ======================== + Smartmenu: style. + ========================== */ + +/* 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 */ + +/* 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%; + } + } + &.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; + } + } + /* 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; + /* 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; + 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; + .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 */ +@media (max-width: 991px) { + .navbar.fixed-top.boost-union-menubar ~ .drawer { + top: 40px; + &.drawer-right { + top: 0; + } + } +} + +/* The nav drawer position to the top in the responsive alignment */ +@media (min-width: 768px) { + .navbar.fixed-top.boost-union-menubar ~ #page.drawers .drawer-toggles .drawer-toggler { + top: 110px; + } +} + +@media (max-width: 767px) { + .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); + } + }*/ + } +} + +/* Smart Menu Settings */ +/* Menu item settings page style */ +.menuitem-buttons { + /* Menu settings page - margin for the "Restriction" label in the table block when the restrictions are enabled */ + + .no-overflow table tbody tr td ul li { + label { + margin-right: 10px; + } + } +} + +/* The Menu settings - Title alignment in the table block */ +.main-inner-wrapper .main-inner div[role="main"] { + .no-overflow .generaltable tbody tr td { + /* In the Meu settings page, the Smart menu text in the table as display as inline-block + so the help icon stays near the Smart menu text */ + .menu-title { + display: inline-block; + } + /* Margin left added for the help icon near the Smart menu text in the table */ + .btn-link { + margin-left: 5px; + } + } + /* Added margin bottom for the "Create new Smart menu" button in the Menu settings page */ + > .singlebutton { + margin-bottom: 20px; + } +} +/* End of Smart Menu settings */ + +/* Footer Menu */ +#page-wrapper #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 { + &:not(.active) { + border-bottom: 0; + flex-wrap: wrap; + /* flex-basis: min-content; */ + flex-direction: column; + justify-content: center; + position: relative; + i { + margin-right: 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 */ + +@media (max-width: 767px) { + /* 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; + } + } +} + +/* Increases the space at the bottom for the footer popover, as the bottom menu bar might hide it. */ +.btn-footer-popover { + bottom: 4rem; +} + +/*Learning tools floating button align*/ +.learningtools-action-info .floating-button { + bottom: 4rem; +} + +/** + * Fontawesome icon picker styles. + */ +.fontawesome-picker { + /* Padding removed in the fontawesome picker popover */ + .popover-body { + padding: 0; + /* Width, height, padding added, margin removed and text aligned center 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; + } + } + } + } +} diff --git a/settings.php b/settings.php index 7cfcfe45146..eda459a6416 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. + // (and allow users with the theme/boost_union:configure capability to access it). + $tab = 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', $tab); } // Create full settings page structure. diff --git a/smartmenus/colorpicker.php b/smartmenus/colorpicker.php new file mode 100644 index 00000000000..2c78291542c --- /dev/null +++ b/smartmenus/colorpicker.php @@ -0,0 +1,112 @@ +. + +/** + * File contains definition of class MoodleQuickForm_boostunioncolorpicker + * + * @package theme_boost_union + * @copyright bdecent GmbH 2021 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once("HTML/QuickForm/input.php"); + +/** + * Form element for handling colorpicker + * + * @package theme_boost_union + * @copyright bdecent GmbH 2021 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class moodlequickform_boostunioncolorpicker extends HTML_QuickForm_input { + + /** @var bool if true label will be hidden */ + public $_helpbutton = ''; + + /** @var bool if true label will be hidden */ + public $_hiddenlabel = false; + + /** @var bool Whether to force the display of this element to flow LTR. */ + protected $forceltr = false; + + /** + * Sets label to be hidden + * + * @param bool $hiddenlabel sets if label should be hidden + */ + public function sethiddenlabel($hiddenlabel) { + $this->_hiddenlabel = $hiddenlabel; + } + + /** + * Get force LTR option. + * + * @return bool + */ + public function get_force_ltr() { + return $this->forceltr; + } + + /** + * get html for help button + * + * @return string html for help button + */ + public function gethelpbutton() { + return $this->_helpbutton; + } + + /** + * Force the field to flow left-to-right. + * + * This is useful for fields such as URLs, passwords, settings, etc... + * + * @param bool $value The value to set the option to. + */ + public function set_force_ltr($value) { + $this->forceltr = (bool) $value; + } + + // @codingStandardsIgnoreStart + /** + * Returns HTML for this form element. + * + * @return string + */ + public function toHtml() { + // @codingStandardsIgnoreEnd + global $PAGE, $OUTPUT; + $icon = new pix_icon('i/loading', get_string('loading', 'admin'), 'moodle', ['class' => 'loadingicon']); + $template = (object) [ + 'id' => $this->getAttribute('id'), + 'name' => $this->getAttribute('name'), + 'value' => $this->getAttribute('value'), + 'icon' => $icon->export_for_template($OUTPUT), + 'haspreviewconfig' => '', + 'forceltr' => $this->get_force_ltr(), + 'readonly' => '', + ]; + $colorpicker = $OUTPUT->render_from_template('core_admin/setting_configcolourpicker', $template); + $context = $template; + $context->colorpicker = $colorpicker; + $context->lable = $this->getLabel(); + $context->type = 'colorpicker'; + $PAGE->requires->js_init_call('M.util.init_colour_picker', array($this->getAttribute('id'), '')); + return $OUTPUT->render_from_template('theme_boost_union/element_colorpicker', $context); + } +} diff --git a/smartmenus/edit.php b/smartmenus/edit.php new file mode 100644 index 00000000000..ad816d2b68c --- /dev/null +++ b/smartmenus/edit.php @@ -0,0 +1,100 @@ +. + +/** + * Theme boost union - Edit menu. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Require config. +require(__DIR__.'/../../../config.php'); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +require_sesskey(); + +// Smart menu ID to edit. +$id = optional_param('id', null, PARAM_INT); +$action = optional_param('action', null, PARAM_ALPHAEXT); + +// Extend the features of admin settings. +admin_externalpage_setup('theme_boost_union_smartmenus'); + +// Page values. +$url = new moodle_url('/theme/boost_union/smartmenus/edit.php', ['id' => $id, 'sesskey' => sesskey()]); +$context = context_system::instance(); + +// Setup page values. +$PAGE->set_url($url); +$PAGE->set_context($context); +$PAGE->set_heading(get_string('smartmenus', 'theme_boost_union')); + +$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/edit.php')); + +// Edit smart menus form. +$menuform = new \theme_boost_union\form\smartmenu_form(null, ['id' => $id]); +$overviewurl = new moodle_url('/theme/boost_union/smartmenus/menus.php'); + +if ($formdata = $menuform->get_data()) { + $result = theme_boost_union\smartmenu::manage_instance($formdata); + // After saved the menu data, lets redirect to configure items for this menu. + if (isset($formdata->saveanddisplay) && $formdata->saveanddisplay) { + $itemsurl = new \moodle_url('/theme/boost_union/smartmenus/items.php', ['menu' => $result]); + redirect($itemsurl); + } else { + // Redirect to menus list. + redirect($overviewurl); + } +} else if ($menuform->is_cancelled()) { + redirect($overviewurl); +} + +// Setup the menu to the form, if the form id param available. +if ($id !== null && $id > 0) { + + if ($record = theme_boost_union\smartmenu::get_menu($id)) { + // Set the menu data to the menu edit form. + $menuform->set_data($record); + } else { + // Direct the user to list page with error message, when the requested menu is not available. + \core\notification::error(get_string('smartmenu:recordmissing', 'theme_boost_union')); + redirect($overviewurl); + } +} + +// Page content display started. +echo $OUTPUT->header(); + +// Add the create item and create menu buttons. +if ($id !== null && $id > 0) { + echo smartmenu_helper::theme_boost_union_menuitems_button($id); +} + +// Smart menu heading. +echo $OUTPUT->heading(get_string('smartmenussettins', 'theme_boost_union')); + +// Display the smart menu form for create or edit. +echo $menuform->display(); + +// Footer. +echo $OUTPUT->footer(); diff --git a/smartmenus/edit_items.php b/smartmenus/edit_items.php new file mode 100644 index 00000000000..5c4babc8b65 --- /dev/null +++ b/smartmenus/edit_items.php @@ -0,0 +1,128 @@ +. + +/** + * Theme boost union - Edit menu items. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Require config. +require(__DIR__.'/../../../config.php'); + +require_once($CFG->dirroot.'/lib/filelib.php'); +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); + +require_sesskey(); + +// Menu ID. +$menuid = optional_param('menu', null, PARAM_INT); +// Smart menu ID to edit. +$id = optional_param('id', null, PARAM_INT); +$action = optional_param('action', null, PARAM_ALPHAEXT); + +if ($menuid == null && $id !== null) { + // Verify the menu is exists. + $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('menunotfound', 'theme_boost_union_smartmenus'); + } +} else { + // Verify the menu is exists. + $menu = $DB->get_record('theme_boost_union_menus', ['id' => $menuid]); + if (!$menu) { + throw new moodle_exception('menunotfound', 'theme_boost_union_smartmenus'); + } +} +// Extend the features of admin settings. +admin_externalpage_setup('theme_boost_union_smartmenus'); + +// Page values. +$url = new moodle_url('/theme/boost_union/smartmenus/edit_items.php', ['menu' => $menu->id, 'sesskey' => sesskey()]); +$context = \context_system::instance(); + + +// Setup page values. +$PAGE->set_url($url); +$PAGE->set_context($context); +$PAGE->set_heading(get_string('smartmenus', 'theme_boost_union')); + +$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/edit.php')); + +// Edit smart menus 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() +); + +// List of items overview page. +$overviewurl = new moodle_url('/theme/boost_union/smartmenus/items.php', ['menu' => $menu->id]); + +$lastmenu = theme_boost_union\smartmenu_item::get_lastitem($menu->id); +// Empty items then lastorder is 0. +$nextorder = isset($lastmenu->sortorder) ? $lastmenu->sortorder + 1 : 1; +// Create/edit menu items form instance. included the menu id. +$menuform = new \theme_boost_union\form\smartmenu_item_form($url->out(false), [ + 'id' => $id, + 'menu' => $menu->id, + 'nextorder' => $nextorder, +]); + +// Process the submitted items form data. +if ($formdata = $menuform->get_data()) { + + $result = theme_boost_union\smartmenu_item::manage_instance($formdata); + if ($result) { + redirect($overviewurl); + } +} else if ($menuform->is_cancelled()) { + redirect($overviewurl); +} + +// Setup the menu to the form, if the form id param available. +if ($id !== null && $id > 0) { + + if ($record = theme_boost_union\smartmenu_item::instance($id)->get_item()) { + // Get an unused draft itemid which will be used for this form. + $record->image = $draftitemid; + // Set the menu data to the menu edit form. + $menuform->set_data($record); + } else { + // Direct the user to list page with error message, when the requested menu is not available. + \core\notification::error(get_string('smartmenu:recordmissing', 'theme_boost_union')); + redirect($overviewurl); + } +} + +// Page content display started. +echo $OUTPUT->header(); + +// Display the smart menu form for create or edit. +echo $menuform->display(); + +// Footer. +echo $OUTPUT->footer(); diff --git a/smartmenus/items.php b/smartmenus/items.php new file mode 100644 index 00000000000..bf5c729ba97 --- /dev/null +++ b/smartmenus/items.php @@ -0,0 +1,143 @@ +. + +/** + * List the items table for the menu. + * + * Manage the item Create, Update, Delete actions, sort the order of menus, Duplicate the item. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Require config. +require(__DIR__.'/../../../config.php'); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->dirroot. '/theme/boost_union/smartmenus/menulib.php'); + +// Menu ID. +$menuid = optional_param('menu', null, PARAM_INT); + +// MenuItem ID to edit. +$id = optional_param('id', null, PARAM_INT); +$action = optional_param('action', null, PARAM_ALPHAEXT); + +if ($menuid == null && $id !== null) { + // Verify the menu is 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('menunotfound', 'theme_boost_union_smartmenus'); + } +} else { + // Verify the menu is exists. + $menu = $DB->get_record('theme_boost_union_menus', ['id' => $menuid]); + if (!$menu) { + throw new moodle_exception('menunotfound', 'theme_boost_union_smartmenus'); + } +} + +// Page values. +$url = new moodle_url('/theme/boost_union/smartmenus/items.php', ['menu' => $menu->id]); +$context = \context_system::instance(); + +// Extend the features of admin settings. +admin_externalpage_setup('theme_boost_union_smartmenus'); + +// Setup page values. +$PAGE->set_url($url); +$PAGE->set_context($context); +$PAGE->set_heading(get_string('smartmenus', 'theme_boost_union')); + +// Setup the breadcrums for qick access. +$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/items.php')); + +// Verfiy the user session. +if ($action !== null && confirm_sesskey() && $action != 'preview') { + // Every action is based on menu, Menu id param should 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('smartmenu:menudeleted', 'theme_boost_union')); + + } + break; + // Move the menu order to down. + case "movedown": + // Move the item to down order. Fetch the next item and use its order and change the next item position to upwards. + $item->move_downward(); + break; + case "moveup": + // Move the item to upwards.Fetch the previous item and use its order and change the previous item position to upwards. + $item->move_upward(); + break; + case "copy": + // Duplicate the item. Clone the item instance, removed the id and send to manage_instance. + // It will create the item as newone. + $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; + endswitch; + + // Allow to update the changes to database. + $transaction->allow_commit(); + // End of any action redirect to items page for clear the params from url. + redirect($url); + +} + +// Add buttons to edit and view the menus and menu items. + +// Page content display started. +echo $OUTPUT->header(); +// Smart menu heading. +if (isset($menu->title)) { + echo $OUTPUT->heading(get_string('smartmenu:menuheading', 'theme_boost_union', $menu->title)); +} +// Smart menu description. +echo $OUTPUT->box(get_string('smartmenus_desc', 'theme_boost_union')); + +// Add the create item and create menu buttons. +echo smartmenu_helper::theme_boost_union_menuitems_button($menu->id); + +// Display the smart menu items list table. +$table = new theme_boost_union\table\menuitems($menu->id); +$table->out(10, true); + +// Footer. +echo $OUTPUT->footer(); diff --git a/smartmenus/menulib.php b/smartmenus/menulib.php new file mode 100644 index 00000000000..4cda09723ba --- /dev/null +++ b/smartmenus/menulib.php @@ -0,0 +1,630 @@ +. + +/** + * Theme Boost Union - Smart menu Helper. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @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 bdecent GmbH 2023 + * @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; + } + + /** + * 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)) { + \cache_helper::purge_by_event('theme_boost_union_menus_cohort_deleted'); + // Remove the deleted cohort from rules if used in menus restriction. + self::remove_deleted_condition_menu($cohortid); + } + + $records = self::find_condition_used_menuitems($cohortid); + if (!empty($records)) { + \cache_helper::purge_by_event('theme_boost_union_menus_cohort_deleted'); + foreach ($records as $record) { + // Remove the item and its menu data from cache. + smartmenu_item::instance($record->id)->delete_cache(); + } + // Remove the deleted cohort from menu item rules if used in menuitems restriction. + self::remove_deleted_condition_menuitems($cohortid); + } + } + + /** + * 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 (self::find_condition_used_menus($cohortid)) { + set_user_preference('theme_boost_union_menu_purgesessioncache', true, $userid); + } + + if (self::find_condition_used_menuitems($cohortid)) { + set_user_preference('theme_boost_union_menuitem_purgesessioncache', true, $userid); + } + } + + /** + * 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)) { + \cache_helper::purge_by_event('theme_boost_union_menus_role_deleted'); + + // Remove the deleted role from menu restrictions. + self::remove_deleted_condition_menu($roleid, 'roles'); + } + + $records = self::find_condition_used_menuitems($roleid, 'roles'); + if (!empty($records)) { + // Purge the cache. + \cache_helper::purge_by_event('theme_boost_union_menus_role_deleted'); + foreach ($records as $record) { + // Remove the item and its menu data from cache. + smartmenu_item::instance($record->id)->delete_cache(); + } + self::remove_deleted_condition_menuitems($roleid, 'roles'); + } + + } + + /** + * 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. + * + * Before set the preference, it checks any of the menus or menu items used this role in rules. + * If found then it set the purge in preference. + * + * @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 (self::find_condition_used_menus($roleid, 'roles')) { + set_user_preference('theme_boost_union_menu_purgesessioncache', true, $userid); + } + + if (self::find_condition_used_menuitems($roleid, 'roles')) { + set_user_preference('theme_boost_union_menuitem_purgesessioncache', true, $userid); + } + } + + /** + * 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 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($id, $method='cohorts') { + global $DB; + + if ($menus = self::find_condition_used_menus($id, $method)) { + 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]); + } + } + } + } + } + + /** + * 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 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($id, $method='cohorts') { + global $DB; + + if ($menuitems = self::find_condition_used_menuitems($id, $method)) { + 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]); + } + + } + } + } + } + + /** + * 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 purge_all_cache_user_session($userid) { + // Clear all the menu and item caches for this user. + set_user_preference('theme_boost_union_menu_purgesessioncache', true, $userid); + set_user_preference('theme_boost_union_menuitem_purgesessioncache', true, $userid); + } + + /** + * 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($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($data->id); + $cache->set($key, $today); + } + } + + /** + * Purges the cached data of menu or item for the given event. + * + * @param string $key The key of the event. + * @return void + */ + public static function purge_cache_byevent($key) { + \cache_helper::purge_by_event($key); + } + + /** + * Reset the preference of user to clear the session cache for menu items. + * @return void + */ + public static function clear_user_cachepreferenceitem() { + global $USER; + set_user_preference('theme_boost_union_menuitem_purgesessioncache', false, $USER); + } + + /** + * 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); + } + + /** + * Generate the buttons, displayed on top of the items table. Helps to create new items, backto menus list, edit menu settings. + * + * @param int $menuid Menu id. + * @return string + */ + public static function theme_boost_union_menuitems_button(int $menuid) { + global $OUTPUT; + + $output = html_writer::start_div('menuitem-buttons d-flex align-items-baseline'); + + $output .= html_writer::start_div('left-menu-items mr-auto p-2'); + // Setup create menu button on page. + $caption = get_string('smartmenu:backtomenus', 'theme_boost_union'); + $editurl = new moodle_url('/theme/boost_union/smartmenus/menus.php'); + $output .= $OUTPUT->single_button($editurl, $caption, 'get'); + + // Setup create menu button on page. + $caption = get_string('smartmenu:settings', 'theme_boost_union'); + $editurl = new moodle_url('/theme/boost_union/smartmenus/edit.php', ['id' => $menuid, 'sesskey' => sesskey()]); + $output .= $OUTPUT->single_button($editurl, $caption, 'get'); + + $output .= html_writer::end_div(); // E.O left-menu-items. + + // Right side menus to create items. + $output .= html_writer::start_div('right-menu-items'); + + // Add new item. + $itemscaption = get_string('smartmenu:addnewitem', 'theme_boost_union'); + $itemsurl = new moodle_url( + '/theme/boost_union/smartmenus/edit_items.php', + ['menu' => $menuid, 'sesskey' => sesskey()] + ); + $output .= html_writer::link($itemsurl, $itemscaption); + + $output .= html_writer::end_div(); // E.O Right-menu-items. + + $output .= html_writer::end_div(); // E.O Menuitem buttons. + + return $output; + } + + /** + * Generate the button which is displayed on top of the menus table. Helps to create menu. + * + * @return string The HTML contents to display the create menu button. + */ + public static function theme_boost_union_smartmenu_buttons() { + global $OUTPUT; + + // Setup create menu button on page. + $caption = get_string('smartmenu:createmenu', 'theme_boost_union'); + $editurl = new moodle_url('/theme/boost_union/smartmenus/edit.php', ['sesskey' => sesskey()]); + + // IN Moodle 4.2, primary button param depreceted. + $primary = defined('single_button::BUTTON_PRIMARY') ? single_button::BUTTON_PRIMARY : true; + $button = new single_button($editurl, $caption, 'get', $primary); + $button = $OUTPUT->render($button); + + return $button; + } + + /** + * 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..2980275945d --- /dev/null +++ b/smartmenus/menus.php @@ -0,0 +1,119 @@ +. + +/** + * List the available menus and manage the menu Create, Update, Delete actions, sort the order of menus. + * + * @package theme_boost_union + * @copyright bdecent GmbH 2023 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Require config. +require(__DIR__.'/../../../config.php'); + +// Require admin library. +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->dirroot. '/theme/boost_union/smartmenus/menulib.php'); + +// Smart menu ID to edit. +$id = optional_param('id', null, PARAM_INT); +$action = optional_param('action', null, PARAM_ALPHAEXT); + +// Page values. +$url = new moodle_url('/theme/boost_union/smartmenus/menus.php'); +$context = \context_system::instance(); + +// Extend the features of admin settings. +admin_externalpage_setup('theme_boost_union_smartmenus'); + +// Setup page values. +$PAGE->set_url($url); +$PAGE->set_context($context); +$PAGE->set_heading(theme_boost_union_get_externaladminpage_heading()); + +// Setup the breadcrums for qick access. +$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')); + +// Verfiy the user session. +if ($action !== null && confirm_sesskey()) { + // Every action is based on menu, Menu id param should exist. + $id = required_param('id', PARAM_INT); + // Create menu instance. Actions are performed in smartmenu instance. + $menu = theme_boost_union\smartmenu::instance($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 ($menu->delete_menu()) { + // Notification to user for menu deleted success. + \core\notification::success(get_string('smartmenu:menudeleted', '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; + endswitch; + + // Allow to update the changes to database. + $transaction->allow_commit(); + // End of any action redirect to overview page for clear the params from url. + redirect($url); +} + +// Page content display started. +echo $OUTPUT->header(); + +// Smart menu heading. +echo $OUTPUT->heading(get_string('smartmenus', 'theme_boost_union')); + +// Smart menu description. +echo $OUTPUT->box(get_string('smartmenus_desc', 'theme_boost_union')); + +// Add the create item and create menu buttons. +echo smartmenu_helper::theme_boost_union_smartmenu_buttons(); + +// Menus list table. +$table = new theme_boost_union\table\smartmenu($context->id); +$table->out(10, true); + +// Footer. +echo $OUTPUT->footer(); diff --git a/templates/cardmenu_children.mustache b/templates/cardmenu_children.mustache new file mode 100644 index 00000000000..00766e7ccc3 --- /dev/null +++ b/templates/cardmenu_children.mustache @@ -0,0 +1,113 @@ +{{! + 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/cardmenu_children modified version of 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..7406c38ff8e --- /dev/null +++ b/templates/core/user_action_menu_items.mustache @@ -0,0 +1,86 @@ +{{! + 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}} + {{#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_submenu_items.mustache b/templates/core/user_action_menu_submenu_items.mustache new file mode 100644 index 00000000000..627f131c4cf --- /dev/null +++ b/templates/core/user_action_menu_submenu_items.mustache @@ -0,0 +1,67 @@ +{{! + 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/user_action_menu_submenus modified version of 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 + } + } + } +}} +{{! + * 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_menu.mustache b/templates/core/user_menu.mustache new file mode 100644 index 00000000000..2ca34f64dc0 --- /dev/null +++ b/templates/core/user_menu.mustache @@ -0,0 +1,118 @@ +{{! + 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/user_menu + + This template render the user menus, Modified version of 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": [] + } + ] + } +}} +{{! + * Include submenus custom return id, Helps to get back to the submenus parent from third level child. +}} +
+ {{#unauthenticateduser}} + + {{/unauthenticateduser}} + {{^unauthenticateduser}} + + {{/unauthenticateduser}} +
+{{#js}} + require(['core/usermenu'], function(UserMenu) { + UserMenu.init(); + }); +{{/js}} diff --git a/templates/element_colorpicker.mustache b/templates/element_colorpicker.mustache new file mode 100644 index 00000000000..3a2f8c171df --- /dev/null +++ b/templates/element_colorpicker.mustache @@ -0,0 +1,40 @@ +{{! + 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/element_colorpicker + + Template for the form element wrapper template. + + Example context (json): + { + "label": "Course full name", + "type": "text" + } +}} + +
+
+ +
+
+
+
+ {{{colorpicker}}} +
+
diff --git a/templates/fontawesome_list.mustache b/templates/fontawesome_list.mustache new file mode 100644 index 00000000000..60813576702 --- /dev/null +++ b/templates/fontawesome_list.mustache @@ -0,0 +1,44 @@ +{{! + 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_list based on core/form_autocomplete_suggestions + + Moodle template for the list of valid options in an autocomplate form element. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * suggestionsId The dom id of the current suggestions list. + * options List of options with label and value fields. + + Example context (json): + { "suggestionsId": 1, "options": [ + { "label": "Item label with tags", "value": "5" }, + { "label": "Another item label with tags", "value": "4" } + ]} +}} +
+ +
diff --git a/templates/moremenu.mustache b/templates/moremenu.mustache new file mode 100644 index 00000000000..7473ebe4ccc --- /dev/null +++ b/templates/moremenu.mustache @@ -0,0 +1,97 @@ +{{! + 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_unions/moremenu + + The More menu. This template is modified version core/moremenu + + 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/moremenu_children.mustache b/templates/moremenu_children.mustache new file mode 100644 index 00000000000..b4daa049b90 --- /dev/null +++ b/templates/moremenu_children.mustache @@ -0,0 +1,129 @@ +{{! + 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/moremenu_children + + The More menu children. This template is modified version core/moremenu_children + + Example context (json): + { + "divider": "", + "haschildren": "", + "moremenuid": "614c104dbacfa", + "text": "Moodle community", + "children": "", + "title": "Moodle community", + "url": "https://moodle.org" + } +}} +{{! + * Added submenu support + * Added smartmenus styles, icons +}} +{{#haschildren}} + +{{/haschildren}} +{{^haschildren}} + +{{/haschildren}} 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/primary-drawer-mobile.mustache b/templates/primary-drawer-mobile.mustache new file mode 100644 index 00000000000..f32085517d2 --- /dev/null +++ b/templates/primary-drawer-mobile.mustache @@ -0,0 +1,79 @@ +{{! + 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 + + This template renders the mobile version of the top navbar menu in a drawer. Based on the template from theme_boost_union/primary-drawer-mobile. + + Example context (json): + { + "output": { + "should_display_navbar_logo": true, + "get_compact_logo_url": "http://placekitten.com/50/50" + }, + "mobileprimarynav": [ + { + "text": "Dashboard", + "url": "/my", + "isactive": "true" + }, + { + "text": "Site home", + "url": "/", + "isactive": "false" + }, + { + "text": "My courses", + "url": "/course", + "isactive": "false" + } + ] + } +}} + +{{< theme_boost/drawer }} + {{$id}}theme_boost-drawers-primary{{/id}} + {{$drawerclasses}}drawer {{#bottombar.drawer}} drawer-bottom {{/bottombar.drawer}} {{^bottombar.drawer}} drawer-left {{/bottombar.drawer}} drawer-primary{{/drawerclasses}} + {{$drawercloseonresize}}1{{/drawercloseonresize}} + {{$drawerheading}} + {{# output.should_display_navbar_logo }} + + {{/ output.should_display_navbar_logo }} + {{^ output.should_display_navbar_logo }} + {{{ sitename }}} + {{/ output.should_display_navbar_logo }} + {{/drawerheading}} + {{$drawercontent}} + {{#bottombar.drawer}} + + {{/bottombar.drawer}} +
+ {{#mobileprimarynav}} + {{#haschildren}} + {{> theme_boost_union/primary-drawer-mobile-child }} + {{/haschildren}} + {{^haschildren}} + + {{{text}}} + + {{/haschildren}} + {{/mobileprimarynav}} +
+ {{/drawercontent}} + {{$drawerstate}}show-drawer-primary{{/drawerstate}} +{{/ theme_boost/drawer}} 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..24659178882 100644 --- a/templates/theme_boost/drawers.mustache +++ b/templates/theme_boost/drawers.mustache @@ -60,6 +60,7 @@ * 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. }} {{> theme_boost/head }} @@ -295,9 +296,10 @@ {{#js}} M.util.js_pending('theme_boost/loader'); -require(['theme_boost/loader', 'theme_boost/drawer'{{#regions.offcanvas.hasblocks}},'theme_boost_union/offcanvas'{{/regions.offcanvas.hasblocks}}], function(Loader, Drawer{{#regions.offcanvas.hasblocks}},OffCanvas{{/regions.offcanvas.hasblocks}}) { +require(['theme_boost/loader', 'theme_boost/drawer'{{#regions.offcanvas.hasblocks}},'theme_boost_union/offcanvas'{{/regions.offcanvas.hasblocks}}, 'theme_boost_union/smartmenu'], function(Loader, Drawer{{#regions.offcanvas.hasblocks}},OffCanvas{{/regions.offcanvas.hasblocks}}, SmartMenu) { Drawer.init(); M.util.js_complete('theme_boost/loader'); {{#regions.offcanvas.hasblocks}}OffCanvas.init();{{/regions.offcanvas.hasblocks}} + SmartMenu.init(); }); {{/js}} diff --git a/templates/theme_boost/footer.mustache b/templates/theme_boost/footer.mustache index 1e09b80f124..f94d90a1bbe 100644 --- a/templates/theme_boost/footer.mustache +++ b/templates/theme_boost/footer.mustache @@ -51,6 +51,7 @@ * Added the possibility to show the contact page link. * Added the possibility to show the help page link. * Added the possibility to show the maintenance page link. + * Added the bottom menu bar to show menus in footer for mobile devices. }}
@@ -121,6 +122,16 @@ {{{ output.debug_footer_html }}} + + {{#bottombar.drawer}} + {{#bottombar}} + + {{/bottombar}} + {{/bottombar.drawer}}
{{#js}} require(['theme_boost/footer-popover'], function(FooterPopover) { diff --git a/templates/theme_boost/navbar.mustache b/templates/theme_boost/navbar.mustache index 84dbe9c416e..06cd52b2044 100644 --- a/templates/theme_boost/navbar.mustache +++ b/templates/theme_boost/navbar.mustache @@ -61,15 +61,28 @@ Modifications compared to the original template: * Added the possibility to change navbar color. * Include button for off-canvas region. + * Removed the primary menu drawer hamburger menu, added bottom bar more section will help users to open the nav drawer. + * Display the logo on mobile viewport. (removed d-none, changed d-md-flex to d-flex) + * Added menu bar section. + * Included primary mobile drawer from boost_union. + * Loaded the primary and menubar moremenu from theme. }} - -{{> theme_boost_union/primary-drawer-mobile }} +{{> theme_boost/primary-drawer-mobile }} diff --git a/templates/primary-drawer-mobile.mustache b/templates/theme_boost/primary-drawer-mobile.mustache similarity index 90% rename from templates/primary-drawer-mobile.mustache rename to templates/theme_boost/primary-drawer-mobile.mustache index f32085517d2..78c35e71b5a 100644 --- a/templates/primary-drawer-mobile.mustache +++ b/templates/theme_boost/primary-drawer-mobile.mustache @@ -16,9 +16,9 @@ }} {{! - @template theme_boost_union/primary-drawer-mobile + @template theme_boost/primary-drawer-mobile - This template renders the mobile version of the top navbar menu in a drawer. Based on the template from theme_boost_union/primary-drawer-mobile. + This template renders the mobile version of the top navbar menu in a drawer. Example context (json): { @@ -45,7 +45,14 @@ ] } }} +{{! + This template is a modified version of theme_boost/primary-drawer-mobile + Modifications compared to the original template: + * Add bottom bar. + * Extend the mobile primary navigation. + * Move child node markup into its own template. +}} {{< theme_boost/drawer }} {{$id}}theme_boost-drawers-primary{{/id}} {{$drawerclasses}}drawer {{#bottombar.drawer}} drawer-bottom {{/bottombar.drawer}} {{^bottombar.drawer}} drawer-left {{/bottombar.drawer}} drawer-primary{{/drawerclasses}} diff --git a/templates/theme_boost/primary-drawer-mobile.mustache.upstream b/templates/theme_boost/primary-drawer-mobile.mustache.upstream new file mode 100644 index 00000000000..22bc68f0c24 --- /dev/null +++ b/templates/theme_boost/primary-drawer-mobile.mustache.upstream @@ -0,0 +1,97 @@ +{{! + 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/primary-drawer-mobile + + This template renders the mobile version of the top navbar menu in a drawer. + + Example context (json): + { + "output": { + "should_display_navbar_logo": true, + "get_compact_logo_url": "http://placekitten.com/50/50" + }, + "mobileprimarynav": [ + { + "text": "Dashboard", + "url": "/my", + "isactive": "true" + }, + { + "text": "Site home", + "url": "/", + "isactive": "false" + }, + { + "text": "My courses", + "url": "/course", + "isactive": "false" + } + ] + } +}} + +{{< theme_boost/drawer }} + {{$id}}theme_boost-drawers-primary{{/id}} + {{$drawerclasses}}drawer drawer-left drawer-primary{{/drawerclasses}} + {{$drawercloseonresize}}1{{/drawercloseonresize}} + {{$drawerheading}} + {{# output.should_display_navbar_logo }} + + {{/ output.should_display_navbar_logo }} + {{^ output.should_display_navbar_logo }} + {{{ sitename }}} + {{/ output.should_display_navbar_logo }} + {{/drawerheading}} + {{$drawercontent}} +
+ {{#mobileprimarynav}} + {{#haschildren}} + + + {{/haschildren}} + {{^haschildren}} + + {{{text}}} + + {{/haschildren}} + {{/mobileprimarynav}} +
+ {{/drawercontent}} + {{$drawerstate}}show-drawer-primary{{/drawerstate}} +{{/ theme_boost/drawer}} diff --git a/tests/behat/behat_theme_boost_union_behat_smartmenus.php b/tests/behat/behat_theme_boost_union_behat_smartmenus.php index c56db48cb51..d21bbb7458c 100644 --- a/tests/behat/behat_theme_boost_union_behat_smartmenus.php +++ b/tests/behat/behat_theme_boost_union_behat_smartmenus.php @@ -18,7 +18,7 @@ * Theme Boost Union - Custom Behat rules for smartmenus * * @package theme_boost_union - * @copyright bdecent GmbH 2023 + * @copyright 2023 bdecent GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -30,7 +30,7 @@ * Class behat_theme_boost_union_behat_smartmenus * * @package theme_boost_union - * @copyright bdecent GmbH 2023e> + * @copyright 2023 bdecent GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_theme_boost_union_behat_smartmenus extends behat_base { diff --git a/tests/behat/theme_boost_union_menuitemsettings_application.feature b/tests/behat/theme_boost_union_menuitemsettings_application.feature index cbe6f41af97..006c68ea3e1 100644 --- a/tests/behat/theme_boost_union_menuitemsettings_application.feature +++ b/tests/behat/theme_boost_union_menuitemsettings_application.feature @@ -166,9 +166,9 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" items pag @javascript Scenario: Smartmenuitem: Application - Include the custom css class for menu items Given I set "Quick Links" items with the following fields to these values: - | Title | Resources | - | Type | Static | - | Menu URL | http://moodle.org | + | Title | Resources | + | Type | Static | + | Menu item URL | http://moodle.org | # And I navigate to smartmenu "Quick Links" items And I click on ".action-edit" "css_element" in the "Resources" "table_row" And I expand all fieldsets diff --git a/tests/behat/theme_boost_union_menuitemsettings_management.feature b/tests/behat/theme_boost_union_menuitemsettings_management.feature index afe52b4a059..ea5d4152134 100644 --- a/tests/behat/theme_boost_union_menuitemsettings_management.feature +++ b/tests/behat/theme_boost_union_menuitemsettings_management.feature @@ -18,7 +18,7 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" page, man And I navigate to "Appearance > Themes > Boost Union > Smart menus" in site administration Then I should see "Smart menus" in the "#region-main h2" "css_element" And I click on ".action-list-items" "css_element" in the "Quick Links" "table_row" - And I should see "There aren't any items are created for this menu. Please add first item to this menu." + And I should see "There aren't any items added to this smart menu yet. Please add an item to this menu." And "table" "css_element" should not exist in the "#region-main" "css_element" And "Add new item" "link" should exist in the "#region-main" "css_element" And ".menu-item-actions" "css_element" should not exist in the "#region-main" "css_element" @@ -35,7 +35,7 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" page, man | Title | Info | | Type | Heading | And I click on "Save changes" "button" - Then I should not see "There aren't any items are created for this menu. Please add first item to this menu." + Then I should not see "There aren't any items added to this smart menu yet. Please add an item to this menu." And "table" "css_element" should exist in the "#region-main" "css_element" And I should see "Info" in the "smartmenus_item" "table" And ".menu-item-actions" "css_element" should exist in the "smartmenus_item" "table" @@ -69,14 +69,14 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" page, man | Type | Heading | And ".action-delete" "css_element" should exist in the "smartmenus_item" "table" And I click on ".action-delete" "css_element" in the "Info" "table_row" - Then I should see "Are you sure you want to delete this menu item from the smart menus?" in the ".moodle-dialogue-confirm" "css_element" + Then I should see "Are you sure you want to delete this menu item from the smart menu?" in the ".moodle-dialogue-confirm" "css_element" And I click on "Cancel" "button" in the ".moodle-dialogue-confirm" "css_element" And I should see "Info" in the "smartmenus_item" "table" And I click on ".action-delete" "css_element" in the "Info" "table_row" - Then I should see "Are you sure you want to delete this menu item from the smart menus?" in the ".moodle-dialogue-confirm" "css_element" + Then I should see "Are you sure you want to delete this menu item from the smart menu?" in the ".moodle-dialogue-confirm" "css_element" And I click on "Yes" "button" in the ".moodle-dialogue-confirm" "css_element" Then "smartmenus_item" "table" should not exist - And I should see "There aren't any items are created for this menu. Please add first item to this menu." + And I should see "There aren't any items added to this smart menu yet. Please add an item to this menu." @javascript Scenario: Smartmenus: Management - Duplicate an existing menu items diff --git a/tests/behat/theme_boost_union_menusettings_management.feature b/tests/behat/theme_boost_union_menusettings_management.feature index 4635b0aa569..604feeee96f 100644 --- a/tests/behat/theme_boost_union_menusettings_management.feature +++ b/tests/behat/theme_boost_union_menusettings_management.feature @@ -10,7 +10,7 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" page, man When I log in as "admin" And I navigate to "Appearance > Themes > Boost Union > Smart menus" in site administration Then I should see "Smart menus" in the "#region-main h2" "css_element" - And I should see "There aren't any smart menus are created. Please create your first smart menu." + And I should see "There aren't any smart menus created yet. Please create your first smart menu to get things going." And "table" "css_element" should not exist in the "#region-main" "css_element" And "Create new smart menu" "button" should exist in the "#region-main" "css_element" And ".menu-item-actions" "css_element" should not exist in the "#region-main" "css_element" @@ -27,7 +27,7 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" page, man | Locations | Main | And I click on "Save and return" "button" Then I should see "Smart menus" in the "#region-main h2" "css_element" - And I should not see "There aren't any smart menus are created. Please create your first smart menu." + And I should not see "There aren't any smart menus created yet. Please create your first smart menu to get things going." And "table" "css_element" should exist in the "#region-main" "css_element" And the following should exist in the "smartmenus" table: | Title | Locations | @@ -72,7 +72,7 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" page, man And I click on ".action-delete" "css_element" in the "Links" "table_row" Then I should see "Are you sure you want to delete this menu from the smart menus?" in the ".moodle-dialogue-confirm" "css_element" And I click on "Yes" "button" in the ".moodle-dialogue-confirm" "css_element" - And I should see "There aren't any smart menus are created. Please create your first smart menu." + And I should see "There aren't any smart menus created yet. Please create your first smart menu to get things going." @javascript Scenario: Smartmenus: Management - Duplicate an existing menus From 2f92d9821d1ef878997d161a77b3724284939549 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 24 Jul 2023 13:31:10 +0530 Subject: [PATCH 03/31] Upgrade the smart menu's cache mode to application - Fix UN-201, UN-206 --- classes/cache/loader.php | 60 ++++++++ classes/smartmenu.php | 274 ++++++++++++++++++++++++------------- classes/smartmenu_item.php | 150 +++++++++++--------- db/caches.php | 27 +--- 4 files changed, 325 insertions(+), 186 deletions(-) create mode 100644 classes/cache/loader.php diff --git a/classes/cache/loader.php b/classes/cache/loader.php new file mode 100644 index 00000000000..16ff8b83cc3 --- /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 menuitems 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/smartmenu.php b/classes/smartmenu.php index b02a6c50453..26ce992784b 100644 --- a/classes/smartmenu.php +++ b/classes/smartmenu.php @@ -262,6 +262,11 @@ class smartmenu { */ 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. * @@ -288,7 +293,7 @@ public static function instance($menu) { */ public function __construct($menu) { $this->id = $menu->id; - $this->menu = $menu; + $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'); @@ -342,8 +347,8 @@ public function move_upward() { $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 menu cache. - cache_helper::purge_by_event('theme_boost_union_menus_resorted'); + // Purge the menus list, need to recreate in new order. + $this->cache->delete(self::CACHE_MENUSLIST); return true; } @@ -369,8 +374,8 @@ public function move_downward() { $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 menu cache. - cache_helper::purge_by_event('theme_boost_union_menus_resorted'); + // Purge the menus list, need to recreate in new order. + $this->cache->delete(self::CACHE_MENUSLIST); return true; } @@ -409,6 +414,9 @@ public function duplicate() { // 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; } @@ -420,7 +428,9 @@ public function duplicate() { */ public function update_visible(bool $visible) { // Delete the current menu from cache. - $this->cache->delete($this->id); + $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]); } @@ -438,7 +448,7 @@ public function update_field($key, $value) { $result = $DB->set_field('theme_boost_union_menus', $key, $value, ['id' => $this->id]); // Delete the current menu from cache. - $this->cache->delete($this->id); + $this->cache->delete_menu($this->id); return $result; } @@ -525,93 +535,105 @@ public function get_menu_items() { * * 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() { - global $OUTPUT; + public function build($resetcache=false) { + global $OUTPUT, $USER; + static $itemcache; - if (!$this->is_visible()) { - return false; + if (empty($itemcache)) { + $itemcache = cache::make('theme_boost_union', 'smartmenu_items'); } - - // Cache for menu. - $cache = cache::make('theme_boost_union', 'smartmenus'); + // 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($cache, $this->menu, 'theme_boost_union_menulastcheckdate'); - - // If the flag to purge the menu cache is set for this user. - if (get_user_preferences('theme_boost_union_menu_purgesessioncache', false) == true) { - // Purge the menu cache for this user. - \cache_helper::purge_by_definition('theme_boost_union', 'smartmenus'); - \smartmenu_helper::clear_user_cachepreferencemenu(); - } + smartmenu_helper::purge_cache_date_reached($this->cache, $this->menu, 'menulastcheckdate'); // Get the menu and its menu items from cache. - if ($nodescache = $cache->get($this->menu->id)) { - return $nodescache; - } + $menuitems = []; + if ($nodes = $this->cache->get($cachekey)) { + // List of menu items added to this menu. + $menuitems = $nodes->menuitems ?? []; - $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); - } + } 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. } - // 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 = $this->get_menu_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->id)->build(); + $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; } @@ -643,9 +665,19 @@ public function build() { $nodes = $builditems; } } + // Set the processed menus node and its children item nodes in Cache. - if (isset($nodes)) { - $cache->set($this->menu->id, $nodes); + if (isset($nodes) && isset($storecache)) { + $nodescache = clone $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; @@ -697,16 +729,9 @@ public static function get_menu($id) { // Verfiy and Fetch menu record from DB. if ($record = $DB->get_record('theme_boost_union_menus', ['id' => $id])) { - $record->description = [ - 'text' => $record->description, - 'format' => $record->description_format - ]; + // Decode the multiple option select elements values to array. - $record->location = json_decode($record->location); - $record->roles = json_decode($record->roles); - $record->cohorts = json_decode($record->cohorts); - $record->languages = json_decode($record->languages); - $record->mode = $record->mode ?? self::MODE_SUBMENU; // Submenu is default menu mode. + $record = self::update_menu_valuesformat($record); return $record; } else { @@ -716,6 +741,33 @@ public static function get_menu($id) { 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. * @@ -814,6 +866,8 @@ public static function manage_instance($formdata) { $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])) { @@ -821,11 +875,10 @@ public static function manage_instance($formdata) { $DB->update_record('theme_boost_union_menus', $record); // Clear the current menu caches. Update may cause changes in the menus list. - \cache_helper::purge_by_event('theme_boost_union_menus_edited'); - - // Delete the item cached. - \cache_helper::purge_by_event('theme_boost_union_menuitems_edited'); - + // 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 { @@ -833,8 +886,8 @@ public static function manage_instance($formdata) { $lastmenu = self::get_lastmenu(); $record->sortorder = isset($lastmenu->sortorder) ? $lastmenu->sortorder + 1 : 1; $menuid = $DB->insert_record('theme_boost_union_menus', $record); - - \cache_helper::purge_by_event('theme_boost_union_menus_created'); + // 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')); } @@ -863,11 +916,36 @@ public static function get_menus() { * @return array An array of SmartMenu nodes. */ public static function build_smartmenu() { + global $USER; + $nodes = []; - // Get top level menus. - $topmenus = self::get_menus(); + + $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) { - if ($node = self::instance($menu->id)->build()) { + // 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 { @@ -875,6 +953,10 @@ public static function build_smartmenu() { } } } + + // 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 index 9f3e5a68220..e4a5a1d4727 100644 --- a/classes/smartmenu_item.php +++ b/classes/smartmenu_item.php @@ -219,29 +219,40 @@ class smartmenu_item { /** * Create a new instance of this class. * - * @param int $id The ID of the item to retrieve. + * @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($id) { - return new self($id); + 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 $id Record id of the 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(int $id) { + public function __construct($item, $menu=null) { - // Item id. - $this->id = $id; + if (is_scalar($item)) { + $item = $this->get_item($item); + } + + // Item ID. + $this->id = $item->id; - // Item data. - $this->item = $this->get_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('itemnotfound', 'theme_boost_union'); + } // Menu data, the current item belongs to. - $this->menu = smartmenu::get_menu($this->item->menu); + $this->menu = $menu ?: smartmenu::get_menu($this->item->menu); // Smartmenu helper to verify the access rules. $this->helper = new smartmenu_helper($this->item); @@ -249,7 +260,8 @@ public function __construct(int $id) { // Cache instance for the items. $this->cache = cache::make('theme_boost_union', 'smartmenu_items'); - // Cache instance for the item`s menus. + // 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'); } @@ -259,45 +271,27 @@ public function __construct(int $id) { * @return void */ public function delete_cache() { - // Delete this item cached. - if ($this->cache->has($this->id)) { - $this->cache->delete($this->id); - } + // Remove cache of current item for all users. + $this->cache->delete_menu($this->item->id); // Delete the cached data of current items menu. - if ($this->menucache->has($this->item->menu)) { - $this->menucache->delete($this->item->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() { + 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' => $this->id])) { + if ($record = $DB->get_record('theme_boost_union_menuitems', ['id' => $itemid ?: $this->id ])) { // Decode the multiple option select elements values to array. - $record->category = json_decode($record->category) ?: []; - $record->enrolmentrole = json_decode($record->enrolmentrole) ?: []; - $record->completionstatus = json_decode($record->completionstatus) ?: []; - $record->daterange = json_decode($record->daterange) ?: []; - - // Restrict access rules. - $record->roles = json_decode($record->roles) ?: []; - $record->cohorts = json_decode($record->cohorts) ?: []; - $record->languages = json_decode($record->languages) ?: []; - - // Seperate the customfields. - $customfields = json_decode($record->customfields) ?: []; - foreach ($customfields as $field => $value) { - $record->{'customfield_'.$field} = $value; - } - return $record; + return $this->update_item_valuesformat($record); } else { // TODO: string for menu not found. @@ -307,6 +301,37 @@ public function get_item() { 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. * @@ -366,12 +391,9 @@ public function move_upward() { if (($currentposition - $previtem->sortorder) > 1) { $this->reorder_items(); } - // Delete the cache and menu cache. + // Delete the menu cache, recreate the menu with updated items order. $this->delete_cache(); - // Purge the menu items cache. - \cache_helper::purge_by_event('theme_boost_union_menuitems_sorted'); - return true; } return false; @@ -414,7 +436,7 @@ public function move_downward() { $this->reorder_items(); } - // Delete the cache and menu cache. + // Delete the menu cache, recreate the menu with updated items order. $this->delete_cache(); // Purge the menu items cache. @@ -535,7 +557,7 @@ public function get_itemimage($itemid) { public function get_course_image($course) { $courseimage = course_summary_exporter::get_course_image($course); - if (!$courseimage) { + 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); @@ -893,23 +915,12 @@ protected function get_customfield_sql(&$query) { * @return false|array Returns false if the menu is not visible or a item array otherwise. */ public function build() { + global $USER; - // If the flag to purge the menuitems cache is set for this user. - if (get_user_preferences('theme_boost_union_menuitem_purgesessioncache', false) == true) { - // Purge the menuitems cache for this user. - \cache_helper::purge_by_definition('theme_boost_union', 'smartmenu_items'); - // Clear the user preference for purge the menuitem cache. - \smartmenu_helper::clear_user_cachepreferenceitem(); - } - - // Cache for menu. - $cache = cache::make('theme_boost_union', 'smartmenu_items'); - - // Purge the cached menus data if the menu date restrictions are reached or passed. - smartmenu_helper::purge_cache_date_reached($cache, $this->item, 'theme_boost_union_menuitemlastcheckdate'); + smartmenu_helper::purge_cache_date_reached($this->cache, $this->item, 'itemlastcheckdate'); - // Get the node data for item from cache if it is stored. - if ($result = $cache->get($this->item->id)) { + $cachekey = "{$this->item->id}_u_{$USER->id}"; + if ($result = $this->cache->get($cachekey)) { return $result; } @@ -960,9 +971,7 @@ public function build() { endswitch; // Save the items cache. - $cache->set($this->item->id, $result); - // Delete the cache of items menu, Recreate the menu. - $this->menucache->delete($this->item->menu); + $this->cache->set($cachekey, $result); return $result; } @@ -1019,6 +1028,11 @@ public function generate_node_data($title, $url, $key=null, $tooltip=null, } } + // 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. @@ -1030,7 +1044,7 @@ public function generate_node_data($title, $url, $key=null, $tooltip=null, 'title' => $titleorg, 'tooltip' => $tooltip ? format_string($tooltip) : '', 'haschildren' => $haschildren, - 'itemimage' => $itemimage ?: $this->get_itemimage($this->item->id), + 'itemimage' => $itemimage, 'itemtype' => 'link', 'link' => 1, 'sort' => uniqid() // Support third level menu. @@ -1216,8 +1230,10 @@ public static function manage_instance($formdata) { $transaction = $DB->start_delegated_transaction(); - // Cache for menu. + // 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; @@ -1240,10 +1256,10 @@ public static function manage_instance($formdata) { ]); } - // Delete the item cached. - \cache_helper::purge_by_event('theme_boost_union_menuitems_edited'); - // Delete the cached data of its menu. - \cache_helper::purge_by_event('theme_boost_union_menus_edited'); + // 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')); @@ -1260,7 +1276,7 @@ public static function manage_instance($formdata) { \core\notification::success(get_string('smartmenusinsertsuccess', 'theme_boost_union')); // Delete the cached data of its menu. Menu will recreate with this item. - $menucache->delete($formdata->menu); + $menucache->delete_menu($formdata->menu); } // Save the item image files to the file directory. diff --git a/db/caches.php b/db/caches.php index 80e299ece96..40868acfc9f 100644 --- a/db/caches.php +++ b/db/caches.php @@ -70,35 +70,16 @@ ), // This cache stores the smart menus. 'smartmenus' => array( - 'mode' => cache_store::MODE_SESSION, + 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, 'simpledata' => false, - 'invalidationevents' => array( - 'theme_boost_union_menus_created', - 'theme_boost_union_menus_edited', - 'theme_boost_union_menus_resorted', - 'theme_boost_union_menus_deleted', - 'theme_boost_union_cohort_deleted', - 'theme_boost_union_roles_deleted', - 'theme_boost_union_user_updated', - 'theme_boost_union_course_updated' - - ) + 'overrideclass' => '\theme_boost_union\cache\loader', ), // This cache stores the smart menus' menu items. 'smartmenu_items' => array( - 'mode' => cache_store::MODE_SESSION, + 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, 'simpledata' => false, - 'invalidationevents' => array( - 'theme_boost_union_menuitems_created', - 'theme_boost_union_menuitems_edited', - 'theme_boost_union_menuitems_resorted', - 'theme_boost_union_menuitems_deleted', - 'theme_boost_union_cohort_deleted', - 'theme_boost_union_roles_deleted', - 'theme_boost_union_user_updated', - 'theme_boost_union_course_updated' - ) + 'overrideclass' => '\theme_boost_union\cache\loader', ) ); From fbdb784a9102dfbcdd7b3597affe9794183542b1 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 24 Jul 2023 13:42:21 +0530 Subject: [PATCH 04/31] Fix database scheme inconsistent - UN-207. --- db/install.xml | 44 +++++++++++++++++------------------ db/upgrade.php | 62 +++++++++++++++++++++++++------------------------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/db/install.xml b/db/install.xml index 66c8ffe7bad..ba223184b66 100644 --- a/db/install.xml +++ b/db/install.xml @@ -31,25 +31,25 @@ - - + + - - + + - - - - + + + + - + - + - + @@ -60,7 +60,7 @@ - + @@ -68,28 +68,28 @@ - + - + - + - + - - - + + + - + - + - + diff --git a/db/upgrade.php b/db/upgrade.php index 4ff29202f44..9e35af80c48 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -145,25 +145,25 @@ function xmldb_theme_boost_union_upgrade($oldversion) { $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_TEXT, null, null, null, null, null, 'description'); - $table->add_field('showdesc', XMLDB_TYPE_INTEGER, '2', null, null, null, null, 'description_format'); + $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, '2', null, XMLDB_NOTNULL, null, '0', 'location'); - $table->add_field('mode', XMLDB_TYPE_INTEGER, '2', null, null, null, '1', 'type'); + $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, '2', XMLDB_UNSIGNED, null, null, '1', 'cssclass'); - $table->add_field('cardsize', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'moremenubehavior'); - $table->add_field('cardform', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'cardsize'); - $table->add_field('overflowbehavior', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'cardform'); + $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, '2', null, null, null, '1', 'roles'); + $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, '2', null, null, null, '1', 'cohorts'); + $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, '2', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '1', 'end_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']); @@ -179,37 +179,37 @@ function xmldb_theme_boost_union_upgrade($oldversion) { // 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, null, null, null, 'title'); - $table->add_field('type', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'menu'); - $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '18', XMLDB_UNSIGNED, null, null, null, 'showdesc'); - $table->add_field('url', XMLDB_TYPE_TEXT, null, null, null, null, null, 'title'); - $table->add_field('category', XMLDB_TYPE_TEXT, null, null, null, null, null, 'description'); - $table->add_field('enrolmentrole', XMLDB_TYPE_TEXT, null, null, null, null, null, 'description'); - $table->add_field('completionstatus', XMLDB_TYPE_CHAR, '20', null, null, null, null, 'sortorder'); - $table->add_field('daterange', XMLDB_TYPE_CHAR, '20', null, null, null, null, 'sortorder'); + $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, 2, null, null, null, null, 'type'); - $table->add_field('textcount', XMLDB_TYPE_INTEGER, 9, null, null, null, null, 'displayfield'); - $table->add_field('mode', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, null, 'customfields'); + $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, '2', XMLDB_UNSIGNED, null, null, null, 'menuicon'); + $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, '2', null, null, null, null, 'tooltip'); + $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, '2', null, null, null, '0', 'cssclass'); + $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, '2', XMLDB_UNSIGNED, null, null, '1', 'backgroundcolor'); - $table->add_field('tablet', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'desktop'); - $table->add_field('mobile', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, null, null, '1', 'tablet'); + $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, '2', null, XMLDB_NOTNULL, null, '1', 'roles'); + $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, '2', null, XMLDB_NOTNULL, null, '1', 'cohorts'); + $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, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '1', 'end_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. From dfe16023074f163915676cd319805afc3b67209c Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 24 Jul 2023 13:55:39 +0530 Subject: [PATCH 05/31] Improved purge cache methods for various events. Fix - UN-210 --- classes/eventobservers.php | 66 ++++++-- db/events.php | 23 ++- smartmenus/menulib.php | 298 ++++++++++++++++++++++++++----------- 3 files changed, 280 insertions(+), 107 deletions(-) diff --git a/classes/eventobservers.php b/classes/eventobservers.php index 9d8081df9cf..5e0cf3c8e2c 100644 --- a/classes/eventobservers.php +++ b/classes/eventobservers.php @@ -116,17 +116,6 @@ public static function cohort_member_removed(\core\event\base $event) { \smartmenu_helper::purge_cache_session_cohort($event->objectid, $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) { - // Purge the cached menus for the updated user. - set_user_preference('theme_boost_union_menu_purgesessioncache', true, $event->relateduserid); - set_user_preference('theme_boost_union_menuitem_purgesessioncache', true, $event->relateduserid); - } - /** * Event observer for when a role is assigned to a user. * @@ -183,8 +172,44 @@ public static function course_updated(\core\event\base $event) { // Require smart menus library. require_once($CFG->dirroot . '/theme/boost_union/smartmenus/menulib.php'); + // Course is moved to another category, Event data only contains the newer category. + // Update the cache for new category, added the updated course in that menu item, + // But the previous category of the course still hold this course. + // Therefore need to delete all the items which are configured as dynamic courses. + if (isset($event->other['updatedfields'])) { + if (in_array('category', array_keys($event->other['updatedfields']))) { + // Purge all the dynamic course items cache. + \smartmenu_helper::purge_cache_dynamic_courseitems(); + return true; + } + } + + // Fetch category id of the deleted course from the record snapshot. + // Get category using normal get_course throw course not find error. + if ($event->action == 'deleted') { + $record = $event->get_record_snapshot($event->objecttable, $event->objectid); + if (!empty($record)) { + \smartmenu_helper::purge_cache_updated_category($record->category); + } + return true; + } // Clear the cache of menu when the course updated. - \cache_helper::purge_by_event('theme_boost_union_course_updated'); + \smartmenu_helper::purge_cache_updated_course($event->objectid); + } + + /** + * 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_updated_category($event->objectid); } /** @@ -199,6 +224,21 @@ public static function completion_updated(\core\event\base $event) { 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::purge_all_cache_user_session($event->relateduserid); + \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/db/events.php b/db/events.php index 74d9db04217..271bdce6c32 100644 --- a/db/events.php +++ b/db/events.php @@ -57,10 +57,6 @@ 'eventname' => 'core\event\course_created', 'callback' => '\theme_boost_union\eventobservers::course_updated' ), - array( - 'eventname' => 'core\event\course_updated', - 'callback' => '\theme_boost_union\eventobservers::course_updated' - ), array( 'eventname' => 'core\event\course_completion_updated', 'callback' => '\theme_boost_union\eventobservers::completion_updated' @@ -68,5 +64,22 @@ 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/smartmenus/menulib.php b/smartmenus/menulib.php index ae7aa024b1e..4f86fb4f114 100644 --- a/smartmenus/menulib.php +++ b/smartmenus/menulib.php @@ -282,6 +282,37 @@ public static function find_condition_used_menuitems($id, $method='cohorts') { 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. * @@ -296,44 +327,14 @@ public static function purge_cache_deleted_cohort($cohortid) { $records = self::find_condition_used_menus($cohortid); if (!empty($records)) { - \cache_helper::purge_by_event('theme_boost_union_menus_cohort_deleted'); // Remove the deleted cohort from rules if used in menus restriction. - self::remove_deleted_condition_menu($cohortid); + self::remove_deleted_condition_menu($records, $cohortid); } $records = self::find_condition_used_menuitems($cohortid); if (!empty($records)) { - \cache_helper::purge_by_event('theme_boost_union_menus_cohort_deleted'); - foreach ($records as $record) { - // Remove the item and its menu data from cache. - smartmenu_item::instance($record->id)->delete_cache(); - } // Remove the deleted cohort from menu item rules if used in menuitems restriction. - self::remove_deleted_condition_menuitems($cohortid); - } - } - - /** - * 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 (self::find_condition_used_menus($cohortid)) { - set_user_preference('theme_boost_union_menu_purgesessioncache', true, $userid); - } - - if (self::find_condition_used_menuitems($cohortid)) { - set_user_preference('theme_boost_union_menuitem_purgesessioncache', true, $userid); + self::remove_deleted_condition_menuitems($records, $cohortid); } } @@ -349,49 +350,18 @@ public static function purge_cache_deleted_roles($roleid) { $records = self::find_condition_used_menus($roleid, 'roles'); if (!empty($records)) { - \cache_helper::purge_by_event('theme_boost_union_menus_role_deleted'); - // Remove the deleted role from menu restrictions. - self::remove_deleted_condition_menu($roleid, 'roles'); + self::remove_deleted_condition_menu($records, $roleid, 'roles'); } $records = self::find_condition_used_menuitems($roleid, 'roles'); if (!empty($records)) { - // Purge the cache. - \cache_helper::purge_by_event('theme_boost_union_menus_role_deleted'); - foreach ($records as $record) { - // Remove the item and its menu data from cache. - smartmenu_item::instance($record->id)->delete_cache(); - } - self::remove_deleted_condition_menuitems($roleid, 'roles'); + // Remove the deleted role from menu item restrictions. + self::remove_deleted_condition_menuitems($records, $roleid, 'roles'); } } - /** - * 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. - * - * Before set the preference, it checks any of the menus or menu items used this role in rules. - * If found then it set the purge in preference. - * - * @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 (self::find_condition_used_menus($roleid, 'roles')) { - set_user_preference('theme_boost_union_menu_purgesessioncache', true, $userid); - } - - if (self::find_condition_used_menuitems($roleid, 'roles')) { - set_user_preference('theme_boost_union_menuitem_purgesessioncache', true, $userid); - } - } - /** * 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. @@ -399,22 +369,25 @@ public static function purge_cache_session_roles(int $roleid, int $userid) { * 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($id, $method='cohorts') { + public static function remove_deleted_condition_menu($menus, $id, $method='cohorts') { global $DB; - if ($menus = self::find_condition_used_menus($id, $method)) { + 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); } } } @@ -428,41 +401,172 @@ public static function remove_deleted_condition_menu($id, $method='cohorts') { * 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($id, $method='cohorts') { + public static function remove_deleted_condition_menuitems($menuitems, $id, $method='cohorts') { global $DB; - if ($menuitems = self::find_condition_used_menuitems($id, $method)) { + 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); } - } } } } /** - * Sets the user preferences to trigger the cache purging when the menu is fetched for the user. + * 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 $userid + * @param int $cohortid Affected cohort id. + * @param int $userid Affected user id. + * * @return void */ - public static function purge_all_cache_user_session($userid) { - // Clear all the menu and item caches for this user. - set_user_preference('theme_boost_union_menu_purgesessioncache', true, $userid); - set_user_preference('theme_boost_union_menuitem_purgesessioncache', true, $userid); + 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 items and menus which is configured to use the affected course category. + * + * @param int $courseid + * @return void + */ + public static function purge_cache_updated_course($courseid) { + // Fetch affected course data. + $course = get_course($courseid); + if ($items = self::find_condition_used_menuitems($course->category, 'category')) { + // List of items to purge. + $items = array_column($items, 'id'); + // Remove the menus items for the user. + array_walk($items, ['self', 'purge_item_cache']); + } + } + + /** + * Purge the cache of items and menus which is configured to use the affected course category. + * + * @param int $categoryid + * @return void + */ + public static function purge_cache_updated_category($categoryid) { + // Fetch list of menuitems, configured with the event categoryid. + if ($items = self::find_condition_used_menuitems($categoryid, 'category')) { + // List of items to purge. + $items = array_column($items, 'id'); + // Remove the menus items for the user. + array_walk($items, ['self', 'purge_item_cache']); + } + } + + /** + * 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); } /** @@ -492,35 +596,51 @@ public static function purge_cache_date_reached($cache, $data, $key) { // 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($data->id); + $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($data->id); + $cache->delete_menu($data->id); $cache->set($key, $today); } } /** - * Purges the cached data of menu or item for the given event. + * Remove the specific menu cache for all the users. * - * @param string $key The key of the event. + * @param int $menuid Menu ID to purge. * @return void */ - public static function purge_cache_byevent($key) { - \cache_helper::purge_by_event($key); + protected static function purge_menu_cache($menuid) { + $cache = self::get_menu_cache(); + $cache->delete_menu($menuid); } /** - * Reset the preference of user to clear the session cache for menu items. + * Remove the specific item cache for all the users. + * + * @param int $itemid Item ID to purge. * @return void */ - public static function clear_user_cachepreferenceitem() { - global $USER; - set_user_preference('theme_boost_union_menuitem_purgesessioncache', false, $USER); + 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); } /** From d1040a8890b18ed46a114c86fe655f3c207ea582 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 24 Jul 2023 14:29:55 +0530 Subject: [PATCH 06/31] Improved color picker element, added example for font-awesome icon picker. Fix - UN-213, UN-209 --- form/element-colorpicker.php | 101 +++++------------- scss/boost_union/post.scss | 5 + .../core_form/element-colorpicker.mustache | 89 +++++++++++++++ .../fontawesome-iconpicker-popover.mustache | 14 +-- templates/form-element-colorpicker.mustache | 39 ------- 5 files changed, 130 insertions(+), 118 deletions(-) create mode 100644 templates/core_form/element-colorpicker.mustache delete mode 100644 templates/form-element-colorpicker.mustache diff --git a/form/element-colorpicker.php b/form/element-colorpicker.php index dff03f13a66..68d7956f5ca 100644 --- a/form/element-colorpicker.php +++ b/form/element-colorpicker.php @@ -25,100 +25,57 @@ 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. + * Form element for color picker * * @package theme_boost_union * @copyright bdecent GmbH 2021 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class moodlequickform_themeboostunion_colorpicker extends HTML_QuickForm_input { +class moodlequickform_themeboostunion_colorpicker extends MoodleQuickForm_text implements templatable { - /** @var bool if true label will be hidden */ - public $_helpbutton = ''; - - /** @var bool if true label will be hidden */ - public $_hiddenlabel = false; - - /** @var bool Whether to force the display of this element to flow LTR. */ - protected $forceltr = false; - - /** - * Sets label to be hidden. - * - * @param bool $hiddenlabel sets if label should be hidden. - */ - public function sethiddenlabel($hiddenlabel) { - $this->_hiddenlabel = $hiddenlabel; - } - - /** - * Get force LTR option. - * - * @return bool - */ - public function get_force_ltr() { - return $this->forceltr; - } - - /** - * Get html for help button. - * - * @return string html for help button. - */ - public function gethelpbutton() { - return $this->_helpbutton; + use templatable_form_element { + export_for_template as export_for_template_base; } /** - * Force the field to flow left-to-right. - * - * This is useful for fields such as URLs, passwords, settings, etc... + * constructor * - * @param bool $value The value to set the option to. + * @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 set_force_ltr($value) { - $this->forceltr = (bool) $value; + public function __construct($elementname=null, $elementlabel=null, $attributes=null) { + parent::__construct($elementname, $elementlabel, $attributes); + $this->setType('colorpicker'); + + // Add the class admin_colourpicker. + $class = $this->getAttribute('class'); + if (empty($class)) { + $class = ''; + } + $this->updateAttributes(array('class' => $class . ' union-form-colour-picker ')); } - // @codingStandardsIgnoreStart /** - * Returns HTML for this form element. + * Export for template * - * @return string + * @param renderer_base $output + * @return array|stdClass */ - public function toHtml() { - // @codingStandardsIgnoreEnd - global $PAGE, $OUTPUT; - + public function export_for_template(renderer_base $output) { + global $PAGE; + // Compose template context for 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']); - - // Compose template context for Moodle core admin setting. - $template = (object) [ - 'id' => $this->getAttribute('id'), - 'name' => $this->getAttribute('name'), - 'value' => $this->getAttribute('value'), - 'icon' => $icon->export_for_template($OUTPUT), - 'haspreviewconfig' => '', - 'forceltr' => $this->get_force_ltr(), - 'readonly' => '', - ]; - - // Render color picker from Moodle core admin setting. - $colorpicker = $OUTPUT->render_from_template('core_admin/setting_configcolourpicker', $template); - - // Compose template context for Mform element. - $context = $template; - $context->colorpicker = $colorpicker; - $context->lable = $this->getLabel(); - $context->type = 'colorpicker'; - + $context['icon'] = $icon->export_for_template($output); // Add JS init call to page. $PAGE->requires->js_init_call('M.util.init_colour_picker', array($this->getAttribute('id'), '')); - // Render and return Mform element. - return $OUTPUT->render_from_template('theme_boost_union/form-element-colorpicker', $context); + return $context; } } diff --git a/scss/boost_union/post.scss b/scss/boost_union/post.scss index e88554f201c..455e3938d27 100644 --- a/scss/boost_union/post.scss +++ b/scss/boost_union/post.scss @@ -2029,3 +2029,8 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } } } + +/* Display the color picker box and the text field as column */ +.union-form-colour-picker [data-fieldtype="colorpicker"] { + flex-direction: column; +} diff --git a/templates/core_form/element-colorpicker.mustache b/templates/core_form/element-colorpicker.mustache new file mode 100644 index 00000000000..cd3f90790c5 --- /dev/null +++ b/templates/core_form/element-colorpicker.mustache @@ -0,0 +1,89 @@ +{{! + 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/core_form/element-colorpicker + + Boost Union template for the form element colorpicker template. + + Example context (json): + { + "element": { + "id": "id_textcolor", + "name": "textcolor", + "label": null, + "multiple": null, + "checked": null, + "error": null, + "size": null, + "value": "#0454EB", + "type": "colorpicker", + "hiddenlabel": false, + "frozen": false, + "hardfrozen": false, + "extraclasses": " union-form-colour-picker ", + "attributes": "", + "emptylabel": false, + "iderror": "id_error_textcolor", + "icon": { + "attributes": [ + { + "name": "alt", + "value": "Loading" + }, + { + "name": "title", + "value": "Loading" + }, + { + "name": "src", + "value": "http://moodle.com/theme/image.php/boost_union/core/1690187894/i/loading" + } + ], + "extraclasses": "loadingicon" + }, + "wrapperid": "fitem_id_textcolor" + }, + "label": "Text color", + "text": "", + "required": false, + "advanced": false, + "error": null + } +}} + +{{< core_form/element-template }} + {{$element}} +
+ {{#element.icon}} + {{>core/pix_icon}} + {{/element.icon}} +
+ + {{/element}} +{{/ core_form/element-template }} diff --git a/templates/fontawesome-iconpicker-popover.mustache b/templates/fontawesome-iconpicker-popover.mustache index b45d50a14cd..65287e78979 100644 --- a/templates/fontawesome-iconpicker-popover.mustache +++ b/templates/fontawesome-iconpicker-popover.mustache @@ -27,20 +27,20 @@ * none Context variables required for this template: - * suggestionsId - The dom id of the current suggestions list. - * options - List of options with label and value fields. + * options - List of icons with label and value fields. Example context (json): { - "suggestionsId": 1, "options": [ { - "label": "Item label with tags", - "value": "5" + "label": "fa-book", + "value": "core:book", + "icon": "" }, { - "label": "Another item label with tags", - "value": "4" + "label": "fa-info-circle", + "value": "core:docs", + "icon": "" } ] } diff --git a/templates/form-element-colorpicker.mustache b/templates/form-element-colorpicker.mustache deleted file mode 100644 index a821f1e3cae..00000000000 --- a/templates/form-element-colorpicker.mustache +++ /dev/null @@ -1,39 +0,0 @@ -{{! - 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/form-element-colorpicker - - Boost Union template for the form element colorpicker template. - - Example context (json): - { - "label": "Course full name", - "type": "text" - } -}} -
-
- -
-
-
-
- {{{colorpicker}}} -
-
From a524b2ebf32394614b5e7cf5445563117bc049a7 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 24 Jul 2023 14:41:36 +0530 Subject: [PATCH 07/31] Fix failed behat and improve smart menu check in primary navigation. --- classes/output/navigation/primary.php | 16 ++++++++++++---- ...t_union_menuitemsettings_application.feature | 17 +++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/classes/output/navigation/primary.php b/classes/output/navigation/primary.php index 075845ae8cd..cab0a94d599 100644 --- a/classes/output/navigation/primary.php +++ b/classes/output/navigation/primary.php @@ -71,10 +71,14 @@ public function __construct($page) { public function export_for_template(?renderer_base $output = null): array { global $DB; - // If the smart menu feature is not installed at all, use the parent function. - $dbman = $DB->get_manager(); - if (!$dbman->table_exists('theme_boost_union_menus')) { - return parent::export_for_template($output); + $cache = \cache::make('theme_boost_union', 'smartmenus'); + + if (!$cache->get(smartmenu::CACHE_MENUSLIST)) { + // If the smart menu feature is not installed at all, use the parent function. + $dbman = $DB->get_manager(); + if (!$dbman->table_exists('theme_boost_union_menus')) { + return parent::export_for_template($output); + } } if (!$output) { @@ -83,6 +87,10 @@ public function export_for_template(?renderer_base $output = null): array { // 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); diff --git a/tests/behat/theme_boost_union_menuitemsettings_application.feature b/tests/behat/theme_boost_union_menuitemsettings_application.feature index 006c68ea3e1..5770bea09ab 100644 --- a/tests/behat/theme_boost_union_menuitemsettings_application.feature +++ b/tests/behat/theme_boost_union_menuitemsettings_application.feature @@ -6,9 +6,10 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" items pag Background: Given the following "courses" exist: - | fullname| shortname | category | - | Test course | C1 | 0 | - | Test course2 | C2 | 0 | + | fullname | shortname | category | + | Test course | C1 | 0 | + | Test course2 | C2 | 0 | + | Test course word count | C3 | 0 | And the following "users" exist: | username | firstname | lastname | email | lang | | student1 | student | User 1 | student1@test.com | en | @@ -202,23 +203,23 @@ Feature: Configuring the theme_boost_union plugin on the "Smart menus" items pag | Title | Available courses | | Type | Dynamic courses | | Category | Category 1 | - | Select name field | Short name | + | Select name field | Short name | And I should see "Available courses" in the "smartmenus_item" "table" And I click on "Quick Links" "link" in the ".primary-navigation" "css_element" And I should see "C1" in the ".primary-navigation" "css_element" - And I should not see "Test course" in the ".primary-navigation" "css_element" + And I should not see "Test course word count" in the ".primary-navigation" "css_element" And I navigate to smartmenu "Quick Links" items And I click on ".action-edit" "css_element" in the "Available courses" "table_row" And I set the field "Select name field" to "Full name" And I click on "Save changes" "button" And I click on "Quick Links" "link" in the ".primary-navigation" "css_element" And I should not see "C1" in the ".primary-navigation" "css_element" - And I should see "Test course" in the ".primary-navigation" "css_element" + And I should see "Test course word count" in the ".primary-navigation" "css_element" # Set the words count. And I navigate to smartmenu "Quick Links" items And I click on ".action-edit" "css_element" in the "Available courses" "table_row" - And I set the field "Number of words" to "10" + And I set the field "Number of words" to "2" And I click on "Save changes" "button" And I should see "Available courses" in the "smartmenus_item" "table" And I click on "Quick Links" "link" in the ".primary-navigation" "css_element" - And I should see "Test cou.." in the ".primary-navigation" "css_element" + And I should see "Test course.." in the ".primary-navigation" "css_element" From ff90f8c0adf9ae06620b39c0306d20a5f582ae2a Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Mon, 24 Jul 2023 15:09:02 +0530 Subject: [PATCH 08/31] Fix submenu support in more drop-down menu. --- amd/build/smartmenu.min.js | 2 +- amd/build/smartmenu.min.js.map | 2 +- amd/src/smartmenu.js | 27 +++++++++++++++++++++++++++ scss/boost_union/post.scss | 5 +++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/amd/build/smartmenu.min.js b/amd/build/smartmenu.min.js index 86376772275..6eca61394f2 100644 --- a/amd/build/smartmenu.min.js +++ b/amd/build/smartmenu.min.js @@ -5,6 +5,6 @@ * @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")))}));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)})()}}})); +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 index 08a1381341e..63820d17a40 100644 --- a/amd/build/smartmenu.min.js.map +++ b/amd/build/smartmenu.min.js.map @@ -1 +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 // 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","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,SA+CnCC,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,KAvJS,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,iBAK/C6B,SAAWlC,SAASC,iBAAiB,sCACxB,OAAbiC,UACAA,SAAShC,SAASiC,OACdA,KAAKT,iBAAiB,SAAUC,IAC5BA,EAAEG,yBAyHVM,GAhGW,UACXC,MAAQrC,SAASC,iBAAiB,2CACxB,OAAVoC,MAAgB,KACZC,YACAC,gBACAC,SAAUC,UAEdJ,MAAMnC,SAASwC,WACPC,cAAgBD,KAAK/B,cAAc,kBAEvCgC,cAAcjB,iBAAiB,aAAcC,IACzCW,aAAc,MACVxC,OAAS6B,EAAEC,cAAcjB,cAAc,uBAC3C6B,SAAWb,EAAEiB,MACbH,UAAY3C,OAAO+C,cAGvBF,cAAcjB,iBAAiB,aAAcC,OACzCA,EAAEmB,kBACGR,mBAGLC,aAAc,MACVzC,OAAS6B,EAAEC,cAAcjB,cAAc,6BACrCoC,OAASpB,EAAEiB,MAAQJ,SACzB1C,OAAO+C,WAAaJ,UAAYM,UAGpCJ,cAAcjB,iBAAiB,SAAUC,IACjCY,cACAZ,EAAEmB,iBACFP,aAAc,GAElBZ,EAAEG,qBAENa,cAAcjB,iBAAiB,cAAc,KACzCY,aAAc,EACdC,aAAc,KAElBI,cAAcjB,iBAAiB,WAAW,KACtCY,aAAc,UAyDtBU,GAhDa,UACbC,WAAajD,SAASW,cAAc,mCACxCL,gBAAgB2C,gBAEZC,QAAUlD,SAASW,cAAc,2BACrCL,gBAAgB4C,UA4CZC"} \ No newline at end of file +{"version":3,"file":"smartmenu.min.js","sources":["../src/smartmenu.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Theme Boost Union - JS for smart menu to realize the third level submenu support.\r\n *\r\n * @module theme_boost_union/smartmenu\r\n * @copyright 2023 bdecent GmbH \r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\ndefine([\"jquery\", \"core/moremenu\"], function($) {\r\n /**\r\n * Implement the second level of submenu support.\r\n * Find the submenus inside the dropdown, add an event listener for click event which - on the click - shows the submenu list.\r\n */\r\n const addSubmenu = () => {\r\n // Fetch the list of submenus from moremenu.\r\n var submenu = document.querySelectorAll('nav.moremenu .dropdown-submenu');\r\n if (submenu !== null) {\r\n submenu.forEach((item) => {\r\n // Add event listener to show the submenu on click.\r\n item.addEventListener('click', (e) => {\r\n var target = e.currentTarget;\r\n // Hide the shown menu.\r\n hideSubmenus(target);\r\n target.classList.toggle('show');\r\n // Prevent hiding the parent menu.\r\n e.stopPropagation();\r\n });\r\n });\r\n }\r\n\r\n // Hide the submenus when its parent dropdown is hidden.\r\n $(document).on('hidden.bs.dropdown', e => {\r\n var target = e.relatedTarget.parentNode;\r\n var submenus = target.querySelectorAll('.dropdown-submenu.show');\r\n if (submenus !== null) {\r\n submenus.forEach((e) => e.classList.remove('show'));\r\n }\r\n });\r\n\r\n // Provide the third level menu support inside the more menu.\r\n // StopPropagation used in the toggledropdown method on Moremenu.js, It prevents the opening of the third level menus.\r\n // Used the document delegation method to fetch the click on moremenu and submenu.\r\n document.addEventListener('click', (e) => {\r\n var dropdown = e.target.closest('.dropdownmoremenu');\r\n var subMenu = e.target.closest('.dropdown-submenu');\r\n if (dropdown && subMenu !== null) {\r\n // Hide the previously opend submenus. before open the new one.\r\n dropdown.querySelectorAll('.dropdown-submenu.show').forEach((menu) => {\r\n menu.classList.remove('show');\r\n });\r\n subMenu.classList.toggle('show');\r\n }\r\n\r\n // Hide the opened menus before open the other menus.\r\n var dropdownMenu = e.target.parentNode.classList.contains('dropdown');\r\n if (dropdown && dropdownMenu) {\r\n dropdown.querySelectorAll('.dropdown-menu.show').forEach((menu) => {\r\n // Hide the opened menus in more menu.\r\n if (menu != e.target.closest('.dropdown-menu')) {\r\n menu.classList.remove('show');\r\n }\r\n });\r\n }\r\n\r\n }, true);\r\n\r\n // Prevent the closing of dropdown during the click on help icon.\r\n var helpIcon = document.querySelectorAll('.moremenu .dropdown .menu-helpicon');\r\n if (helpIcon !== null) {\r\n helpIcon.forEach((icon) => {\r\n icon.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n });\r\n });\r\n }\r\n };\r\n\r\n /**\r\n * Hide visible submenus before display new submenu.\r\n *\r\n * @param {Selector} target\r\n */\r\n const hideSubmenus = (target) => {\r\n var visibleMenu = document.querySelectorAll('nav.moremenu .dropdown-submenu.show');\r\n if (visibleMenu !== null) {\r\n visibleMenu.forEach((el) => {\r\n if (el != target) {\r\n el.classList.remove('show');\r\n }\r\n });\r\n }\r\n };\r\n\r\n /**\r\n * Make the no wrapped card menus scroll using swipe or drag.\r\n */\r\n const cardScroll = () => {\r\n var cards = document.querySelectorAll('.card-dropdown.card-overflow-no-wrap');\r\n if (cards !== null) {\r\n var scrollStart; // Verify the mouse is clicked and still in click not released.\r\n var scrollMoved; // Prevent the click on scrolling.\r\n let startPos, scrollPos;\r\n\r\n cards.forEach((card) => {\r\n var scrollElement = card.querySelector('.dropdown-menu');\r\n\r\n scrollElement.addEventListener('mousedown', (e) => {\r\n scrollStart = true;\r\n var target = e.currentTarget.querySelector('.card-block-wrapper');\r\n startPos = e.pageX;\r\n scrollPos = target.scrollLeft;\r\n });\r\n\r\n scrollElement.addEventListener('mousemove', (e) => {\r\n e.preventDefault();\r\n if (!scrollStart) {\r\n return;\r\n }\r\n scrollMoved = true;\r\n var target = e.currentTarget.querySelector('.card-block-wrapper');\r\n const scroll = e.pageX - startPos;\r\n target.scrollLeft = scrollPos - scroll;\r\n });\r\n\r\n scrollElement.addEventListener('click', (e) => {\r\n if (scrollMoved) {\r\n e.preventDefault();\r\n scrollMoved = false;\r\n }\r\n e.stopPropagation();\r\n });\r\n scrollElement.addEventListener('mouseleave', () => {\r\n scrollStart = false;\r\n scrollMoved = false;\r\n });\r\n scrollElement.addEventListener('mouseup', () => {\r\n scrollStart = false;\r\n });\r\n });\r\n }\r\n };\r\n\r\n /**\r\n * Move the menubar and primary navigation menu items from more menu.\r\n */\r\n const autoCollapse = () => {\r\n var primaryNav = document.querySelector('.primary-navigation ul.more-nav');\r\n moveOutMoreMenu(primaryNav);\r\n\r\n var menuBar = document.querySelector('nav.menubar ul.more-nav');\r\n moveOutMoreMenu(menuBar);\r\n };\r\n\r\n /**\r\n * Move the items from more menu, items which is set to force outside more menu.\r\n * Remove those items from more menu and insert the menu before the last normal item.\r\n * Find the length and children's length to insert the out menus in that positions.\r\n * Rerun the more menu it will more the other normal menus into more menu to fix the alignmenu issue.\r\n *\r\n * @param {HTMLElement} navMenu The navbar container.\r\n */\r\n const moveOutMoreMenu = (navMenu) => {\r\n\r\n if (navMenu === null) {\r\n return;\r\n }\r\n\r\n var outMenus = navMenu.querySelectorAll('.dropdownmoremenu .force-menu-out');\r\n var menuslist = [];\r\n\r\n if (outMenus === null) {\r\n return;\r\n }\r\n\r\n outMenus.forEach((menu) => {\r\n menu.querySelector('a').classList.remove('dropdown-item');\r\n menu.querySelector('a').classList.add('nav-link');\r\n\r\n menuslist.push(menu);\r\n menu.parentNode.removeChild(menu);\r\n });\r\n // Find the length and children's length to insert the out menus in that positions.\r\n var length = menuslist.length;\r\n var navLength = navMenu.children.length - 1; // Remove more menu.\r\n var newPosition = navLength - length || 0;\r\n // Insert the stored menus before the more menu.\r\n menuslist.forEach((menu) => navMenu.insertBefore(menu, navMenu.children[newPosition]));\r\n window.dispatchEvent(new Event('resize')); // Dispatch the resize event to create more menu.\r\n };\r\n\r\n return {\r\n init: () => {\r\n addSubmenu();\r\n cardScroll();\r\n autoCollapse();\r\n }\r\n };\r\n});\r\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/smartmenu.js b/amd/src/smartmenu.js index 376e32ac12f..892b105dc9b 100644 --- a/amd/src/smartmenu.js +++ b/amd/src/smartmenu.js @@ -52,6 +52,33 @@ define(["jquery", "core/moremenu"], function($) { } }); + // 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) { diff --git a/scss/boost_union/post.scss b/scss/boost_union/post.scss index 455e3938d27..cbffc74dec9 100644 --- a/scss/boost_union/post.scss +++ b/scss/boost_union/post.scss @@ -1424,6 +1424,9 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { height: 100%; object-fit: cover; } + > a { + padding: 0; + } } /* The card dropdown items content block width, padding, background color and aligned center styles added */ @@ -1437,6 +1440,7 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { 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 { @@ -1459,6 +1463,7 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { 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 */ From e3fe9fd8b8f60a63f6922a0dd9b10f3db1347c9b Mon Sep 17 00:00:00 2001 From: Alexander Bias Date: Tue, 25 Jul 2023 06:42:18 +0200 Subject: [PATCH 09/31] Fix stale AMD Map file --- amd/build/smartmenu.min.js.map | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amd/build/smartmenu.min.js.map b/amd/build/smartmenu.min.js.map index 63820d17a40..6877d2a276f 100644 --- a/amd/build/smartmenu.min.js.map +++ b/amd/build/smartmenu.min.js.map @@ -1 +1 @@ -{"version":3,"file":"smartmenu.min.js","sources":["../src/smartmenu.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\r\n//\r\n// Moodle is free software: you can redistribute it and/or modify\r\n// it under the terms of the GNU General Public License as published by\r\n// the Free Software Foundation, either version 3 of the License, or\r\n// (at your option) any later version.\r\n//\r\n// Moodle is distributed in the hope that it will be useful,\r\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r\n// GNU General Public License for more details.\r\n//\r\n// You should have received a copy of the GNU General Public License\r\n// along with Moodle. If not, see .\r\n\r\n/**\r\n * Theme Boost Union - JS for smart menu to realize the third level submenu support.\r\n *\r\n * @module theme_boost_union/smartmenu\r\n * @copyright 2023 bdecent GmbH \r\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\r\n */\r\n\r\ndefine([\"jquery\", \"core/moremenu\"], function($) {\r\n /**\r\n * Implement the second level of submenu support.\r\n * Find the submenus inside the dropdown, add an event listener for click event which - on the click - shows the submenu list.\r\n */\r\n const addSubmenu = () => {\r\n // Fetch the list of submenus from moremenu.\r\n var submenu = document.querySelectorAll('nav.moremenu .dropdown-submenu');\r\n if (submenu !== null) {\r\n submenu.forEach((item) => {\r\n // Add event listener to show the submenu on click.\r\n item.addEventListener('click', (e) => {\r\n var target = e.currentTarget;\r\n // Hide the shown menu.\r\n hideSubmenus(target);\r\n target.classList.toggle('show');\r\n // Prevent hiding the parent menu.\r\n e.stopPropagation();\r\n });\r\n });\r\n }\r\n\r\n // Hide the submenus when its parent dropdown is hidden.\r\n $(document).on('hidden.bs.dropdown', e => {\r\n var target = e.relatedTarget.parentNode;\r\n var submenus = target.querySelectorAll('.dropdown-submenu.show');\r\n if (submenus !== null) {\r\n submenus.forEach((e) => e.classList.remove('show'));\r\n }\r\n });\r\n\r\n // Provide the third level menu support inside the more menu.\r\n // StopPropagation used in the toggledropdown method on Moremenu.js, It prevents the opening of the third level menus.\r\n // Used the document delegation method to fetch the click on moremenu and submenu.\r\n document.addEventListener('click', (e) => {\r\n var dropdown = e.target.closest('.dropdownmoremenu');\r\n var subMenu = e.target.closest('.dropdown-submenu');\r\n if (dropdown && subMenu !== null) {\r\n // Hide the previously opend submenus. before open the new one.\r\n dropdown.querySelectorAll('.dropdown-submenu.show').forEach((menu) => {\r\n menu.classList.remove('show');\r\n });\r\n subMenu.classList.toggle('show');\r\n }\r\n\r\n // Hide the opened menus before open the other menus.\r\n var dropdownMenu = e.target.parentNode.classList.contains('dropdown');\r\n if (dropdown && dropdownMenu) {\r\n dropdown.querySelectorAll('.dropdown-menu.show').forEach((menu) => {\r\n // Hide the opened menus in more menu.\r\n if (menu != e.target.closest('.dropdown-menu')) {\r\n menu.classList.remove('show');\r\n }\r\n });\r\n }\r\n\r\n }, true);\r\n\r\n // Prevent the closing of dropdown during the click on help icon.\r\n var helpIcon = document.querySelectorAll('.moremenu .dropdown .menu-helpicon');\r\n if (helpIcon !== null) {\r\n helpIcon.forEach((icon) => {\r\n icon.addEventListener('click', (e) => {\r\n e.stopPropagation();\r\n });\r\n });\r\n }\r\n };\r\n\r\n /**\r\n * Hide visible submenus before display new submenu.\r\n *\r\n * @param {Selector} target\r\n */\r\n const hideSubmenus = (target) => {\r\n var visibleMenu = document.querySelectorAll('nav.moremenu .dropdown-submenu.show');\r\n if (visibleMenu !== null) {\r\n visibleMenu.forEach((el) => {\r\n if (el != target) {\r\n el.classList.remove('show');\r\n }\r\n });\r\n }\r\n };\r\n\r\n /**\r\n * Make the no wrapped card menus scroll using swipe or drag.\r\n */\r\n const cardScroll = () => {\r\n var cards = document.querySelectorAll('.card-dropdown.card-overflow-no-wrap');\r\n if (cards !== null) {\r\n var scrollStart; // Verify the mouse is clicked and still in click not released.\r\n var scrollMoved; // Prevent the click on scrolling.\r\n let startPos, scrollPos;\r\n\r\n cards.forEach((card) => {\r\n var scrollElement = card.querySelector('.dropdown-menu');\r\n\r\n scrollElement.addEventListener('mousedown', (e) => {\r\n scrollStart = true;\r\n var target = e.currentTarget.querySelector('.card-block-wrapper');\r\n startPos = e.pageX;\r\n scrollPos = target.scrollLeft;\r\n });\r\n\r\n scrollElement.addEventListener('mousemove', (e) => {\r\n e.preventDefault();\r\n if (!scrollStart) {\r\n return;\r\n }\r\n scrollMoved = true;\r\n var target = e.currentTarget.querySelector('.card-block-wrapper');\r\n const scroll = e.pageX - startPos;\r\n target.scrollLeft = scrollPos - scroll;\r\n });\r\n\r\n scrollElement.addEventListener('click', (e) => {\r\n if (scrollMoved) {\r\n e.preventDefault();\r\n scrollMoved = false;\r\n }\r\n e.stopPropagation();\r\n });\r\n scrollElement.addEventListener('mouseleave', () => {\r\n scrollStart = false;\r\n scrollMoved = false;\r\n });\r\n scrollElement.addEventListener('mouseup', () => {\r\n scrollStart = false;\r\n });\r\n });\r\n }\r\n };\r\n\r\n /**\r\n * Move the menubar and primary navigation menu items from more menu.\r\n */\r\n const autoCollapse = () => {\r\n var primaryNav = document.querySelector('.primary-navigation ul.more-nav');\r\n moveOutMoreMenu(primaryNav);\r\n\r\n var menuBar = document.querySelector('nav.menubar ul.more-nav');\r\n moveOutMoreMenu(menuBar);\r\n };\r\n\r\n /**\r\n * Move the items from more menu, items which is set to force outside more menu.\r\n * Remove those items from more menu and insert the menu before the last normal item.\r\n * Find the length and children's length to insert the out menus in that positions.\r\n * Rerun the more menu it will more the other normal menus into more menu to fix the alignmenu issue.\r\n *\r\n * @param {HTMLElement} navMenu The navbar container.\r\n */\r\n const moveOutMoreMenu = (navMenu) => {\r\n\r\n if (navMenu === null) {\r\n return;\r\n }\r\n\r\n var outMenus = navMenu.querySelectorAll('.dropdownmoremenu .force-menu-out');\r\n var menuslist = [];\r\n\r\n if (outMenus === null) {\r\n return;\r\n }\r\n\r\n outMenus.forEach((menu) => {\r\n menu.querySelector('a').classList.remove('dropdown-item');\r\n menu.querySelector('a').classList.add('nav-link');\r\n\r\n menuslist.push(menu);\r\n menu.parentNode.removeChild(menu);\r\n });\r\n // Find the length and children's length to insert the out menus in that positions.\r\n var length = menuslist.length;\r\n var navLength = navMenu.children.length - 1; // Remove more menu.\r\n var newPosition = navLength - length || 0;\r\n // Insert the stored menus before the more menu.\r\n menuslist.forEach((menu) => navMenu.insertBefore(menu, navMenu.children[newPosition]));\r\n window.dispatchEvent(new Event('resize')); // Dispatch the resize event to create more menu.\r\n };\r\n\r\n return {\r\n init: () => {\r\n addSubmenu();\r\n cardScroll();\r\n autoCollapse();\r\n }\r\n };\r\n});\r\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 +{"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 From 316a5f7684d92aa1d7706985f55faf9ae6d1fa90 Mon Sep 17 00:00:00 2001 From: Alexander Bias Date: Tue, 25 Jul 2023 22:57:23 +0200 Subject: [PATCH 10/31] Review changes --- classes/cache/loader.php | 2 +- classes/output/navigation/primary.php | 3 + form/element-colorpicker.php | 2 +- scss/boost_union/post.scss | 176 ++++++++++++++------------ 4 files changed, 100 insertions(+), 83 deletions(-) diff --git a/classes/cache/loader.php b/classes/cache/loader.php index 16ff8b83cc3..21a093b4afa 100644 --- a/classes/cache/loader.php +++ b/classes/cache/loader.php @@ -34,7 +34,7 @@ class loader extends \cache_application { /** - * Delete the cached menus or menuitems for all of its users. + * 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". diff --git a/classes/output/navigation/primary.php b/classes/output/navigation/primary.php index cab0a94d599..f78cf15cbef 100644 --- a/classes/output/navigation/primary.php +++ b/classes/output/navigation/primary.php @@ -71,10 +71,13 @@ public function __construct($page) { 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); diff --git a/form/element-colorpicker.php b/form/element-colorpicker.php index 68d7956f5ca..07432bbdff6 100644 --- a/form/element-colorpicker.php +++ b/form/element-colorpicker.php @@ -32,7 +32,7 @@ * Form element for color picker * * @package theme_boost_union - * @copyright bdecent GmbH 2021 + * @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 { diff --git a/scss/boost_union/post.scss b/scss/boost_union/post.scss index cbffc74dec9..4961b9b2388 100644 --- a/scss/boost_union/post.scss +++ b/scss/boost_union/post.scss @@ -63,10 +63,23 @@ 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, - .dropdown-menu .dropdown-item .icon { + .usermenu .dropdown-toggle { color: #c8c8c8 !important; /* stylelint-disable-line declaration-no-important */ } + .nav-link:hover .icon, + .nav-link:hover a .icon, + .usermenu .dropdown-toggle:hover { + color: $white !important; /* stylelint-disable-line declaration-no-important */ + } + + /* Change the color of the icons in the dropdown menu to a dark 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 */ + } + .dropdown-menu .dropdown-item:hover .icon { + color: $white !important; /* stylelint-disable-line declaration-no-important */ + } /* Revert color of the close icon in the search panel in the navbar as this icon is still dark on white within the input form. */ @@ -911,87 +924,9 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { /*======================================= - * Supporting third-party plugins + * Settings: Smart menus. ======================================*/ -/*--------------------------------------- - * Dash Pro - --------------------------------------*/ - -/* Style Dash Pro dashboards. */ -.path-local-dash-dashboard #page.drawers { - /* Use flex if one of the block regions next to the main content is used. */ - .main-inner-outside-nextmaincontent { - display: flex; - } - - /* Remove the margin of a Dash block when used in the outside top region in hero width mode. */ - #theme-block-region-outside-top.theme-block-region-outside-herowidth .block_dash, - #theme-block-region-outside-bottom.theme-block-region-outside-herowidth .block_dash, - #theme-block-region-footer.theme-block-region-footer-herowidth .block_dash { - margin: 0; - } - - /* The page header makes alignment nearly impossible, - so let's hide it when block regions next to main content are used. */ - .main-inner-outside-nextmaincontent #page-header { - display: none; - } - - /* Remove the spacing which was there for the header which we have hidden in the previous rule. */ - .main-inner-outside-nextmaincontent .main-inner { - margin: 0; - padding: 0; - } - - /* 25-50-25 layout. */ - .main-inner-outside-left-right > .main-inner { - width: 50%; - max-width: 50%; - } - .main-inner-outside-left-right > section { - width: 25%; - max-width: 25%; - margin-top: calc(0.5rem + 1px); - } - - /* 33-67 or 67-33 layout. */ - .main-inner-outside-right > .main-inner, - .main-inner-outside-left > .main-inner { - width: 67%; - max-width: 67%; - } - .main-inner-outside-left > section, - .main-inner-outside-right > section { - width: 33%; - max-width: 33%; - margin-top: calc(0.5rem + 1px); - } - - /* On not-so-large screens. */ - @include media-breakpoint-down(lg) { /* This means less than 300px per outside block region. */ - /* Stack content vertically. */ - .main-inner-outside-nextmaincontent { - display: flex; - flex-direction: column; - } - - /* Main content first, outside block regions later. */ - .main-inner-outside-nextmaincontent > .main-inner { - order: -2; - } - .main-inner-outside-nextmaincontent > section, - .main-inner-outside-nextmaincontent > .main-inner { - width: 100%; - max-width: 100%; - } - } -} - -/* ======================== - Smartmenu: style. - ========================== */ - /* Scrollbar */ /* Width and height of the scrollbar */ @@ -2039,3 +1974,82 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { .union-form-colour-picker [data-fieldtype="colorpicker"] { flex-direction: column; } + + +/*======================================= + * Supporting third-party plugins + ======================================*/ + +/*--------------------------------------- + * Dash Pro + --------------------------------------*/ + +/* Style Dash Pro dashboards. */ +.path-local-dash-dashboard #page.drawers { + /* Use flex if one of the block regions next to the main content is used. */ + .main-inner-outside-nextmaincontent { + display: flex; + } + + /* Remove the margin of a Dash block when used in the outside top region in hero width mode. */ + #theme-block-region-outside-top.theme-block-region-outside-herowidth .block_dash, + #theme-block-region-outside-bottom.theme-block-region-outside-herowidth .block_dash, + #theme-block-region-footer.theme-block-region-footer-herowidth .block_dash { + margin: 0; + } + + /* The page header makes alignment nearly impossible, + so let's hide it when block regions next to main content are used. */ + .main-inner-outside-nextmaincontent #page-header { + display: none; + } + + /* Remove the spacing which was there for the header which we have hidden in the previous rule. */ + .main-inner-outside-nextmaincontent .main-inner { + margin: 0; + padding: 0; + } + + /* 25-50-25 layout. */ + .main-inner-outside-left-right > .main-inner { + width: 50%; + max-width: 50%; + } + .main-inner-outside-left-right > section { + width: 25%; + max-width: 25%; + margin-top: calc(0.5rem + 1px); + } + + /* 33-67 or 67-33 layout. */ + .main-inner-outside-right > .main-inner, + .main-inner-outside-left > .main-inner { + width: 67%; + max-width: 67%; + } + .main-inner-outside-left > section, + .main-inner-outside-right > section { + width: 33%; + max-width: 33%; + margin-top: calc(0.5rem + 1px); + } + + /* On not-so-large screens. */ + @include media-breakpoint-down(lg) { /* This means less than 300px per outside block region. */ + /* Stack content vertically. */ + .main-inner-outside-nextmaincontent { + display: flex; + flex-direction: column; + } + + /* Main content first, outside block regions later. */ + .main-inner-outside-nextmaincontent > .main-inner { + order: -2; + } + .main-inner-outside-nextmaincontent > section, + .main-inner-outside-nextmaincontent > .main-inner { + width: 100%; + max-width: 100%; + } + } +} From 60d483720227167af32c589dd92077fa2aa796f4 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Wed, 26 Jul 2023 16:11:00 +0530 Subject: [PATCH 11/31] Fix color picker --- classes/form/smartmenu_item_form.php | 2 + form/element-colorpicker.php | 22 ++++- scss/boost_union/post.scss | 2 +- .../core_form/element-colorpicker.mustache | 89 ------------------- 4 files changed, 21 insertions(+), 94 deletions(-) delete mode 100644 templates/core_form/element-colorpicker.mustache diff --git a/classes/form/smartmenu_item_form.php b/classes/form/smartmenu_item_form.php index 191ec72ab73..f68755e7c93 100644 --- a/classes/form/smartmenu_item_form.php +++ b/classes/form/smartmenu_item_form.php @@ -252,11 +252,13 @@ public function definition() { // 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')); diff --git a/form/element-colorpicker.php b/form/element-colorpicker.php index 07432bbdff6..9df6be40865 100644 --- a/form/element-colorpicker.php +++ b/form/element-colorpicker.php @@ -50,7 +50,7 @@ class moodlequickform_themeboostunion_colorpicker extends MoodleQuickForm_text i */ public function __construct($elementname=null, $elementlabel=null, $attributes=null) { parent::__construct($elementname, $elementlabel, $attributes); - $this->setType('colorpicker'); + $this->setType('text'); // Add the class admin_colourpicker. $class = $this->getAttribute('class'); @@ -72,10 +72,24 @@ public function export_for_template(renderer_base $output) { $context = $this->export_for_template_base($output); // Build loading icon. $icon = new pix_icon('i/loading', get_string('loading', 'admin'), 'moodle', ['class' => 'loadingicon']); - $context['icon'] = $icon->export_for_template($output); - // Add JS init call to page. - $PAGE->requires->js_init_call('M.util.init_colour_picker', array($this->getAttribute('id'), '')); + $icondata = $icon->export_for_template($output); + $iconoutput = $output->render_from_template('core/pix_icon', $icondata); + // Id of the element. + $id = $this->getAttribute('id'); + // 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 boost_union is not a default. + + // Init color picker utility. + M.util.init_colour_picker(Y, '$id'); + "); return $context; } + } diff --git a/scss/boost_union/post.scss b/scss/boost_union/post.scss index 4961b9b2388..ab994f19368 100644 --- a/scss/boost_union/post.scss +++ b/scss/boost_union/post.scss @@ -1971,7 +1971,7 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } /* Display the color picker box and the text field as column */ -.union-form-colour-picker [data-fieldtype="colorpicker"] { +.union-form-colour-picker .felement { flex-direction: column; } diff --git a/templates/core_form/element-colorpicker.mustache b/templates/core_form/element-colorpicker.mustache deleted file mode 100644 index cd3f90790c5..00000000000 --- a/templates/core_form/element-colorpicker.mustache +++ /dev/null @@ -1,89 +0,0 @@ -{{! - 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/core_form/element-colorpicker - - Boost Union template for the form element colorpicker template. - - Example context (json): - { - "element": { - "id": "id_textcolor", - "name": "textcolor", - "label": null, - "multiple": null, - "checked": null, - "error": null, - "size": null, - "value": "#0454EB", - "type": "colorpicker", - "hiddenlabel": false, - "frozen": false, - "hardfrozen": false, - "extraclasses": " union-form-colour-picker ", - "attributes": "", - "emptylabel": false, - "iderror": "id_error_textcolor", - "icon": { - "attributes": [ - { - "name": "alt", - "value": "Loading" - }, - { - "name": "title", - "value": "Loading" - }, - { - "name": "src", - "value": "http://moodle.com/theme/image.php/boost_union/core/1690187894/i/loading" - } - ], - "extraclasses": "loadingicon" - }, - "wrapperid": "fitem_id_textcolor" - }, - "label": "Text color", - "text": "", - "required": false, - "advanced": false, - "error": null - } -}} - -{{< core_form/element-template }} - {{$element}} -
- {{#element.icon}} - {{>core/pix_icon}} - {{/element.icon}} -
- - {{/element}} -{{/ core_form/element-template }} From 6cd7e08b24a872022f720c1fda5acc872429180a Mon Sep 17 00:00:00 2001 From: Alexander Bias Date: Wed, 26 Jul 2023 22:00:01 +0200 Subject: [PATCH 12/31] Review changes --- form/element-colorpicker.php | 28 +++++++++------- scss/boost_union/post.scss | 63 ++++++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/form/element-colorpicker.php b/form/element-colorpicker.php index 9df6be40865..dd95bfe90a4 100644 --- a/form/element-colorpicker.php +++ b/form/element-colorpicker.php @@ -42,48 +42,53 @@ class moodlequickform_themeboostunion_colorpicker extends MoodleQuickForm_text i } /** - * constructor + * 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 + * @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 the class admin_colourpicker. + // Add a CSS class for styling the color picker. $class = $this->getAttribute('class'); if (empty($class)) { $class = ''; } - $this->updateAttributes(array('class' => $class . ' union-form-colour-picker ')); + $this->updateAttributes(array('class' => $class.' theme_boost_union-form-colour-picker ')); } /** - * Export for template + * Export for template. * * @param renderer_base $output * @return array|stdClass */ public function export_for_template(renderer_base $output) { global $PAGE; - // Compose template context for Mform element. + + // 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); - // Id of the element. + + // Get ID of the element. $id = $this->getAttribute('id'); - // JS to append the color picker div before the element and initiate the color picker utility method. + + // 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 boost_union is not a default. + 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'); @@ -91,5 +96,4 @@ public function export_for_template(renderer_base $output) { return $context; } - } diff --git a/scss/boost_union/post.scss b/scss/boost_union/post.scss index ab994f19368..109d28e7ee4 100644 --- a/scss/boost_union/post.scss +++ b/scss/boost_union/post.scss @@ -927,7 +927,9 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { * Settings: Smart menus. ======================================*/ -/* Scrollbar */ +/*--------------------------------------- + * Scrollbar + --------------------------------------*/ /* Width and height of the scrollbar */ ::-webkit-scrollbar { @@ -950,7 +952,11 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } /* End of Scrollbar */ -/* Header Menu */ + +/*--------------------------------------- + * Header menu + --------------------------------------*/ + .navbar { &.fixed-top { /* Added Height and background color for the top header, @@ -1636,7 +1642,11 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } } -/* Smart Menu Settings */ + +/*--------------------------------------- + * Smart menu settings + --------------------------------------*/ + /* Menu item settings page style */ .menuitem-buttons { /* Menu settings page - margin for the "Restriction" label in the table block when the restrictions are enabled */ @@ -1665,9 +1675,12 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { margin-bottom: 20px; } } -/* End of Smart Menu settings */ -/* Footer Menu */ + +/*--------------------------------------- + * Footer menu + --------------------------------------*/ + #page-wrapper #page-footer { .navbar { /* Footer bottom menu */ @@ -1916,24 +1929,32 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } } -/* Increases the space at the bottom for the footer popover, as the bottom menu bar might hide it. */ +/* Increases the space at the bottom for the footer popover, as the bottom menu bar might hide it. */ .btn-footer-popover { bottom: 4rem; } -/*Learning tools floating button align*/ +/* Learning tools floating button align. */ .learningtools-action-info .floating-button { bottom: 4rem; } -/** - * Fontawesome icon picker styles. - */ + +/*======================================= + * General styling + ======================================*/ + +/*--------------------------------------- + * Form element: Fontawesome picker + --------------------------------------*/ + .fontawesome-picker { - /* Padding removed in the fontawesome picker popover */ + /* Padding removed in the fontawesome picker popover. */ .popover-body { padding: 0; - /* Width, height, padding added, margin removed and text aligned center in the popover fontawesome icon picker */ + + /* Width, height, padding added, margin + removed and text aligned centered in the popover fontawesome icon picker. */ .fontawesome-icon-suggestions { width: 250px; height: 350px; @@ -1942,8 +1963,9 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { 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 */ + added for the icon item in the popover fontawesome icon picker */ li { width: 40px; height: 40px; @@ -1953,12 +1975,14 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { margin: 0 10px 10px 0; display: inline-block; cursor: pointer; - /* Background color added on hover and selected for the font icon item */ + + /* 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 */ + + /* Width, height, font size added and margin removed for the Font icon. */ .icon { width: 14px; height: 14px; @@ -1970,8 +1994,13 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } } -/* Display the color picker box and the text field as column */ -.union-form-colour-picker .felement { + +/*--------------------------------------- + * 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; } From 6ab676d578f2f56d310e22a6406826ecfc02d3eb Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Thu, 27 Jul 2023 17:55:10 +0530 Subject: [PATCH 13/31] Fix cache on menus deletion --- classes/smartmenu.php | 12 ++++++++---- classes/smartmenu_item.php | 3 --- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/classes/smartmenu.php b/classes/smartmenu.php index 26ce992784b..3adc105faee 100644 --- a/classes/smartmenu.php +++ b/classes/smartmenu.php @@ -321,7 +321,10 @@ public function delete_menu() { // Delete all its items. $DB->delete_records('theme_boost_union_menuitems', ['menu' => $this->id]); // Purge the menus cache. - cache_helper::purge_by_event('theme_boost_union_menus_deleted'); + $this->cache->delete_menu($this->id); + // Delete the cached menus list. + $this->cache->delete(self::CACHE_MENUSLIST); + return true; } return false; @@ -553,7 +556,8 @@ public function build($resetcache=false) { // Get the menu and its menu items from cache. $menuitems = []; - if ($nodes = $this->cache->get($cachekey)) { + $nodes = $this->cache->get($cachekey); + if (!empty($nodes)) { // List of menu items added to this menu. $menuitems = $nodes->menuitems ?? []; @@ -638,7 +642,7 @@ public function build($resetcache=false) { $builditems = (!empty($item)) ? array_merge($builditems, $item) : $builditems; } - if (isset($nodes)) { + if (isset($nodes) && !empty($nodes)) { // Setup the childrens to parent menu node. $nodes->haschildren = (count($builditems) > 0) ? true : false; $nodes->children = $builditems; @@ -668,7 +672,7 @@ public function build($resetcache=false) { // Set the processed menus node and its children item nodes in Cache. if (isset($nodes) && isset($storecache)) { - $nodescache = clone $nodes; + $nodescache = clone (object) $nodes; // Remove the children data from cache before store. unset($nodescache->children); $nodescache->menuitems = $menuitems; diff --git a/classes/smartmenu_item.php b/classes/smartmenu_item.php index e4a5a1d4727..9edfbac7c18 100644 --- a/classes/smartmenu_item.php +++ b/classes/smartmenu_item.php @@ -439,9 +439,6 @@ public function move_downward() { // Delete the menu cache, recreate the menu with updated items order. $this->delete_cache(); - // Purge the menu items cache. - \cache_helper::purge_by_event('theme_boost_union_menuitems_sorted'); - return true; } From 9baf2faf836de1ec0f92ebc8a3cf5938844295c5 Mon Sep 17 00:00:00 2001 From: Prasanna LMSACE Date: Thu, 27 Jul 2023 19:29:32 +0530 Subject: [PATCH 14/31] Unique selector added for footer popover button styles --- scss/boost_union/post.scss | 418 +++++++++++++------------ templates/theme_boost/drawers.mustache | 2 +- 2 files changed, 216 insertions(+), 204 deletions(-) diff --git a/scss/boost_union/post.scss b/scss/boost_union/post.scss index 109d28e7ee4..29612ce785c 100644 --- a/scss/boost_union/post.scss +++ b/scss/boost_union/post.scss @@ -1019,6 +1019,13 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { 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; @@ -1681,224 +1688,234 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { * Footer menu --------------------------------------*/ -#page-wrapper #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 { - &:not(.active) { - border-bottom: 0; - flex-wrap: wrap; - /* flex-basis: min-content; */ + #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; + } + } + #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; - justify-content: center; - position: relative; + /* The footer bottom dropdown menu icon margin is removed. */ i { - margin-right: 0; + margin: 0 0 3px; } - &: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; + /* The footer bottom menu item border bottom is removed when it's not active. */ + > a { + &.dropdown-toggle { + padding-right: 25px; } - } - /* 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 { + &:not(.active) { + border-bottom: 0; + flex-wrap: wrap; 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; + justify-content: center; + position: relative; + i { + margin: 0; } - /* 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; + margin-left: 5px; + position: absolute; + right: 0; } } } } - /* 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; + li { + &:first-child, + &:nth-child(2), + &:nth-child(3) { + &.dropdown .dropdown-menu .dropdown-submenu ul { + left: 100%; + right: auto; } } - /* 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; + &.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; + } } - /* 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; + /* 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; + } + } } } - /* 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; + /* 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; + } } - /* 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 - 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 - 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 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 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; + /* 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 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; } } - /* 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; - } } } } @@ -1929,17 +1946,6 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { } } -/* Increases the space at the bottom for the footer popover, as the bottom menu bar might hide it. */ -.btn-footer-popover { - bottom: 4rem; -} - -/* Learning tools floating button align. */ -.learningtools-action-info .floating-button { - bottom: 4rem; -} - - /*======================================= * General styling ======================================*/ @@ -2009,6 +2015,12 @@ body.pagelayout-login:not(.loginbackgroundimage) #footnote { * Supporting third-party plugins ======================================*/ +/*Learning tools floating button position alignment*/ +#page-wrapper #page.footer-bottom-menu .learningtools-action-info .floating-button { + bottom: 4rem; +} + + /*--------------------------------------- * Dash Pro --------------------------------------*/ diff --git a/templates/theme_boost/drawers.mustache b/templates/theme_boost/drawers.mustache index 24659178882..9108e6d26f1 100644 --- a/templates/theme_boost/drawers.mustache +++ b/templates/theme_boost/drawers.mustache @@ -120,7 +120,7 @@ {{/regions.offcanvas.hasblocks}} {{/userisediting}} -
+