MDL-70208 theme_boost: frontend for primary navigation

- Part of: MDL-69588
This commit is contained in:
Bas Brands
2021-08-23 17:46:39 +08:00
committed by Mathew May
parent 19fd786270
commit da67b468fa
16 changed files with 441 additions and 9 deletions
+1
View File
@@ -1352,6 +1352,7 @@ $string['movefilestohere'] = 'Move files to here';
$string['movefull'] = 'Move {$a} to this location';
$string['movehere'] = 'Move to here';
$string['moveleft'] = 'Move left';
$string['moremenu'] = 'More';
$string['moveright'] = 'Move right';
$string['movesection'] = 'Move section {$a}';
$string['moveselectedcategoriesto'] = 'Move selected categories to';
+2
View File
@@ -0,0 +1,2 @@
define ("core/moremenu",["exports","jquery"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c={regions:{moredropdown:"[data-region=\"moredropdown\"]",morebutton:"[data-region=\"morebutton\"]"},classes:{dropdownitem:"dropdown-item",dropdownmoremenu:"dropdownmoremenu",dropdowntoggle:"dropdown-toggle",hidden:"d-none",active:"active",nav:"nav",navlink:"nav-link",observed:"observed"},attributes:{menu:"[role=\"menu\"]"}},d=6,e=function(a){var b=a.parentNode.offsetHeight+1,f=a.querySelector(c.regions.moredropdown),g=a.querySelector(c.regions.morebutton),h=a.querySelector("."+c.classes.dropdowntoggle);if(a.offsetHeight>b||a.children.length>d){g.classList.remove(c.classes.hidden);var i=Array.from(a.children).reverse();i.forEach(function(e){if(!e.classList.contains(c.classes.dropdownmoremenu)){if(a.offsetHeight>b||a.children.length>d){var g=a.removeChild(e),i=g.querySelector("."+c.classes.navlink);if(i&&!i.hasAttribute("role")){i.setAttribute("role","menuitem")}if(i.classList.contains(c.classes.active)){h.classList.add(c.classes.active)}i.classList.remove(c.classes.navlink);i.classList.add(c.classes.dropdownitem);f.prepend(g)}}})}else{if("children"in f){var j=Array.from(f.children);j.forEach(function(e){if(a.offsetHeight<b&&a.children.length<d){var i=f.removeChild(e),j=i.querySelector("."+c.classes.dropdownitem);if(j){var k=j.getAttribute("role");if("menuitem"===k){j.removeAttribute("role")}}if(j.classList.contains(c.classes.active)){h.classList.remove(c.classes.active)}j.classList.remove(c.classes.dropdownitem);j.classList.add(c.classes.navlink);a.insertBefore(i,g)}});if(0===j.length){g.classList.add(c.classes.hidden)}}if(a.offsetHeight>b){e(a)}}a.parentNode.classList.add(c.classes.observed)},f=function(a){e(a);window.addEventListener("resize",function(){e(a)});var d=function(a){var b=a.target.parentNode.querySelector(c.attributes.menu);if(b){b.classList.toggle("show")}a.stopPropagation()};(0,b.default)("."+c.classes.dropdownmoremenu).on("show.bs.dropdown",function(){var b=a.querySelector(c.regions.moredropdown);b.querySelectorAll(".dropdown").forEach(function(a){a.removeEventListener("click",d,!0);a.addEventListener("click",d,!0)})})};a.default=f;return a.default});
//# sourceMappingURL=moremenu.min.js.map
File diff suppressed because one or more lines are too long
+174
View File
@@ -0,0 +1,174 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Moves wrapping navigation items into a more menu.
*
* @module core/moremenu
* @package core
* @copyright 2021 Moodle
* @author Bas Brands <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
/**
* Moremenu selectors.
*/
const Selectors = {
regions: {
moredropdown: '[data-region="moredropdown"]',
morebutton: '[data-region="morebutton"]'
},
classes: {
dropdownitem: 'dropdown-item',
dropdownmoremenu: 'dropdownmoremenu',
dropdowntoggle: 'dropdown-toggle',
hidden: 'd-none',
active: 'active',
nav: 'nav',
navlink: 'nav-link',
observed: 'observed',
},
attributes: {
menu: '[role="menu"]'
}
};
const maxMenuItems = 6;
/**
* Auto Collapse navigation items that wrap into a dropdown menu.
*
* @param {HTMLElement} menu The navbar container.
*/
const autoCollapse = menu => {
const maxHeight = menu.parentNode.offsetHeight + 1;
const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
const moreButton = menu.querySelector(Selectors.regions.morebutton);
const dropdownToggle = menu.querySelector('.' + Selectors.classes.dropdowntoggle);
// If the menuitems wrap and the menu height is larger than the height of the
// parent. Or if the number if menuitems is larger than the maximum menu items
// allowed then start pushing navlinks into the moreDropdown.
if (menu.offsetHeight > maxHeight || menu.children.length > maxMenuItems) {
moreButton.classList.remove(Selectors.classes.hidden);
const menuNodes = Array.from(menu.children).reverse();
menuNodes.forEach(item => {
if (!item.classList.contains(Selectors.classes.dropdownmoremenu)) {
// After moving the menuitems into the moreDropdown check again
// if the menuheight is still larger then the height of the parent or if the
// menu still has more items than maxMenuItems.
if (menu.offsetHeight > maxHeight || menu.children.length > maxMenuItems) {
const lastNode = menu.removeChild(item);
const navLink = lastNode.querySelector('.' + Selectors.classes.navlink);
if (navLink && !navLink.hasAttribute('role')) {
// Adding the menuitem role so the dropdown includes the
// Accessibility improvements from theme/boost/amd/src/aria.js
navLink.setAttribute('role', 'menuitem');
}
// If there are navLinks that contain an active link in the moreDropdown
// make the dropdownToggle in the moreButton active.
if (navLink.classList.contains(Selectors.classes.active)) {
dropdownToggle.classList.add(Selectors.classes.active);
}
// Change the styling of the navLink to a dropdownitem and push it into
// the moreDropdown.
navLink.classList.remove(Selectors.classes.navlink);
navLink.classList.add(Selectors.classes.dropdownitem);
moreDropdown.prepend(lastNode);
}
}
});
} else {
// If the the menu height is smaller than the height of the parent and there are
// less than the maximum items in the menu, then try returning navlinks to the menu.
if ('children' in moreDropdown) {
const menuNodes = Array.from(moreDropdown.children);
menuNodes.forEach(item => {
if (menu.offsetHeight < maxHeight && menu.children.length < maxMenuItems) {
const lastNode = moreDropdown.removeChild(item);
const navLink = lastNode.querySelector('.' + Selectors.classes.dropdownitem);
if (navLink) {
const currentAttribute = navLink.getAttribute('role');
if (currentAttribute === 'menuitem') {
navLink.removeAttribute('role');
}
}
// Stop displaying the active state on the dropdownToggle if
// the active navlink is removed.
if (navLink.classList.contains(Selectors.classes.active)) {
dropdownToggle.classList.remove(Selectors.classes.active);
}
navLink.classList.remove(Selectors.classes.dropdownitem);
navLink.classList.add(Selectors.classes.navlink);
menu.insertBefore(lastNode, moreButton);
}
});
// If there are no more menuNodes in the dropdown we can hide the moreButton.
if (menuNodes.length === 0) {
moreButton.classList.add(Selectors.classes.hidden);
}
}
if (menu.offsetHeight > maxHeight) {
autoCollapse(menu);
}
}
menu.parentNode.classList.add(Selectors.classes.observed);
};
/**
* Initialise the more menus.
*
* @param {HTMLElement} menu The navbar moremenu.
*/
export default menu => {
autoCollapse(menu);
// When the screen size changes make sure the menu still fits.
window.addEventListener('resize', () => {
autoCollapse(menu);
});
const toggledropdown = e => {
const innerMenu = e.target.parentNode.querySelector(Selectors.attributes.menu);
if (innerMenu) {
innerMenu.classList.toggle('show');
}
e.stopPropagation();
};
// If there are dropdowns in the MoreMenu, add a new
// eventlistener to show the contents on click and prevent the
// moreMenu from closing.
$('.' + Selectors.classes.dropdownmoremenu).on('show.bs.dropdown', function() {
const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
moreDropdown.querySelectorAll('.dropdown').forEach((dropdown) => {
dropdown.removeEventListener('click', toggledropdown, true);
dropdown.addEventListener('click', toggledropdown, true);
});
});
};
+14
View File
@@ -3808,6 +3808,20 @@ EOD;
return $content;
}
/**
* Renders a navigation bar into a "more menu" navigation bar
*
* @param array $content
* @param string $navbarstyle navbar-nav or nav-tabs
* @return string
*/
public function more_menu($content, $navbarstyle) {
return $this->render_from_template('core/moremenu', (object) [
'nodearray' => $content,
'navbarstyle' => $navbarstyle
]);
}
/**
* Renders theme links for switching between default and other themes.
*
+66
View File
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core/moremenu
The More menu.
Example context (json):
{
"nodecollection": {
"children": [
{
"text": "Home",
"action": "/index.php?redirect=0",
"active": "true"
},
{
"text": "Dashboard",
"action": "/my"
},
{
"text": "Courses",
"action": "/course"
},
{
"text": "Site Administration",
"action": "/admin/search.php"
}
]
}
}
}}
<nav class="moremenu">
<ul id="moremenu-{{ uniqid }}" class="nav {{navbarstyle}}">
{{#nodearray}}
{{> core/moremenu_children}}
{{/nodearray}}
<li class="nav-item dropdown dropdownmoremenu d-none" data-region="morebutton">
<a class="dropdown-toggle nav-link" href="#" id="moremenu-dropdown{{ uniqid }}" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{#str}}moremenu, core{{/str}}
</a>
<ul class="dropdown-menu dropdown-menu-right" data-region="moredropdown" aria-labelledby="moremenu-dropdown{{ uniqid }}" role="menu">
</ul>
</li>
</ul>
</nav>
{{#js}}
require(['core/moremenu'], function(moremenu) {
var moreMenu = document.querySelector('#moremenu-{{ uniqid }}');
moremenu(moreMenu);
});
{{/js}}
+56
View File
@@ -0,0 +1,56 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core/moremenu_children
The More menu children
Example context (json):
{
"divider": "",
"haschildren": "",
"uniqid": "Unique string",
"text": "Moodle community",
"children": "",
"title": "Moodle community",
"url": "https://moodle.org"
}
}}
{{#haschildren}}
<li class="dropdown nav-item">
<a class="dropdown-toggle nav-link" id="drop-down-{{uniqid}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" href="#" {{#title}}title="{{{title}}}"{{/title}} aria-controls="drop-down-menu-{{uniqid}}">
{{{text}}}
</a>
<div class="dropdown-menu" role="menu" id="drop-down-menu-{{uniqid}}" aria-labelledby="drop-down-{{uniqid}}">
{{#children}}
{{^divider}}
<a class="dropdown-item" role="menuitem" href="{{{url}}}" {{#title}}title="{{{title}}}"{{/title}}>{{{text}}}</a>
{{/divider}}
{{#divider}}
<div class="dropdown-divider" role="presentation"></div>
{{/divider}}
{{/children}}
</div>
</li>
{{/haschildren}}
{{^haschildren}}
<li class="nav-item">
<a class="nav-link {{#isactive}}active{{/isactive}} {{#classes}}{{.}} {{/classes}}" href="{{{url}}}" role="menuitem" {{#isactive}}aria-current="true"{{/isactive}}>
{{{text}}}
</a>
</li>
{{/haschildren}}
+7 -3
View File
@@ -42,6 +42,11 @@ $hasblocks = strpos($blockshtml, 'data-block=') !== false;
$buildregionmainsettings = !$PAGE->include_region_main_settings_in_header_actions();
// If the settings menu will be included in the header then don't add it here.
$regionmainsettingsmenu = $buildregionmainsettings ? $OUTPUT->region_main_settings_menu() : false;
$primary = new core\navigation\output\primary($PAGE);
$renderer = $PAGE->get_renderer('core');
$primarymenu = $primary->export_for_template($renderer);
$templatecontext = [
'sitename' => format_string($SITE->shortname, true, ['context' => context_course::instance(SITEID), "escape" => false]),
'output' => $OUTPUT,
@@ -50,11 +55,10 @@ $templatecontext = [
'bodyattributes' => $bodyattributes,
'navdraweropen' => $navdraweropen,
'regionmainsettingsmenu' => $regionmainsettingsmenu,
'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu)
'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu),
'primarymoremenu' => $OUTPUT->more_menu(array_merge($primarymenu['primary'], $primarymenu['custom']), 'navbar-nav'),
];
$nav = $PAGE->flatnav;
$templatecontext['flatnavigation'] = $nav;
$templatecontext['firstcollectionlabel'] = $nav->get_collectionlabel();
echo $OUTPUT->render_from_template('theme_boost/columns2', $templatecontext);
+2
View File
@@ -44,3 +44,5 @@ $breadcrumb-divider-rtl: "◀" !default;
@import "moodle/toasts";
@import "moodle/navbar";
@import "moodle/reportbuilder";
@import "moodle/moremenu";
@import "moodle/primarynavigation";
+2
View File
@@ -378,10 +378,12 @@ img.resize {
.action-menu .dropdown-toggle {
text-decoration: none;
display: inline-block;
}
.action-menu {
white-space: nowrap;
display: inline;
}
.block img.resize {
+36
View File
@@ -0,0 +1,36 @@
.moremenu {
opacity: 0;
height: $moremenu-height;
&.observed {
opacity: 1;
}
.nav-link {
height: $moremenu-height;
display: flex;
align-items: center;
}
// Styling for dropdown menus inside the MoreButton.
.dropdownmoremenu > .dropdown-menu {
& > .dropdown-item {
padding: 0;
}
.dropdown-menu {
position: static;
padding: 0;
border: 0;
&.show {
display: block;
}
.dropdown-item {
background-color: $gray-100;
@include hover-focus() {
color: $dropdown-link-hover-color;
@include gradient-bg($dropdown-link-active-bg);
}
}
.dropdown-divider {
display: none;
}
}
}
}
@@ -0,0 +1,8 @@
.navbar.fixed-top {
.moremenu {
height: $navbar-height;
.nav-link {
height: $navbar-height;
}
}
}
+2
View File
@@ -27,3 +27,5 @@ $course-content-maxwidth: 800px;
$box-shadow-drawer-left: -0.25rem .25rem .8rem rgba($black, .025) !default;
$box-shadow-drawer-right: 0 .25rem .8rem rgba($black, .025) !default;
$moremenu-height: 40px !default;
+34 -2
View File
@@ -9987,10 +9987,12 @@ img.resize {
width: 1em; }
.action-menu .dropdown-toggle {
text-decoration: none; }
text-decoration: none;
display: inline-block; }
.action-menu {
white-space: nowrap; }
white-space: nowrap;
display: inline; }
.block img.resize {
height: 0.9em;
@@ -20036,6 +20038,36 @@ div.editor_atto_toolbar button .icon {
text-overflow: clip;
word-break: break-all; }
.moremenu {
opacity: 0;
height: 40px; }
.moremenu.observed {
opacity: 1; }
.moremenu .nav-link {
height: 40px;
display: flex;
align-items: center; }
.moremenu .dropdownmoremenu > .dropdown-menu > .dropdown-item {
padding: 0; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu {
position: static;
padding: 0;
border: 0; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu.show {
display: block; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-item {
background-color: #f8f9fa; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-item:hover, .moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-item:focus {
color: #fff;
background-color: #0f6fc5; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-divider {
display: none; }
.navbar.fixed-top .moremenu {
height: 50px; }
.navbar.fixed-top .moremenu .nav-link {
height: 50px; }
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; }
+2 -2
View File
@@ -53,9 +53,9 @@
{{{ sitename }}}
</a>
{{{primarymoremenu}}}
<ul class="navbar-nav d-none d-md-flex my-1 px-1">
<!-- custom_menu -->
{{{ output.custom_menu }}}
<!-- page_heading_menu -->
{{{ output.page_heading_menu }}}
</ul>
+34 -2
View File
@@ -10199,10 +10199,12 @@ img.resize {
width: 1em; }
.action-menu .dropdown-toggle {
text-decoration: none; }
text-decoration: none;
display: inline-block; }
.action-menu {
white-space: nowrap; }
white-space: nowrap;
display: inline; }
.block img.resize {
height: 0.9em;
@@ -20227,6 +20229,36 @@ div.editor_atto_toolbar button .icon {
text-overflow: clip;
word-break: break-all; }
.moremenu {
opacity: 0;
height: 40px; }
.moremenu.observed {
opacity: 1; }
.moremenu .nav-link {
height: 40px;
display: flex;
align-items: center; }
.moremenu .dropdownmoremenu > .dropdown-menu > .dropdown-item {
padding: 0; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu {
position: static;
padding: 0;
border: 0; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu.show {
display: block; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-item {
background-color: #f8f9fa; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-item:hover, .moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-item:focus {
color: #fff;
background-color: #0f6fc5; }
.moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-divider {
display: none; }
.navbar.fixed-top .moremenu {
height: 50px; }
.navbar.fixed-top .moremenu .nav-link {
height: 50px; }
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; }