MDL-77721 navigation: Revert the patch for MDL-75908.

This reverts commit 7b8fa9de865aa7a22eeb25d00fd92d28222a3b71.
This commit is contained in:
Simey Lameze 2023-03-23 15:43:20 +08:00
parent f3bf17cdfb
commit 61f4843fed
6 changed files with 9 additions and 274 deletions

View File

@ -7,6 +7,6 @@ define("core/menu_navigation",["exports"],(function(_exports){Object.definePrope
* @author Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const SELECTORS_menuitem='[role="menuitem"]',SELECTORS_tab='[role="tab"]',SELECTORS_dropdowntoggle='[data-toggle="dropdown"]',SELECTORS_dropdownitemactive='.dropdown-item[aria-current="true"]';let openDropdownNode=null;const clickErrorHandler=(item,fallback)=>null!==item?item:fallback,menuItemHelper=src=>{let parent;if(!src.dataset.disableactive){if(src.classList.contains("dropdown-item")){parent=src.closest(".dropdown-menu");const dropDownToggle=document.getElementById(parent.getAttribute("aria-labelledby"));dropDownToggle.classList.add("active"),dropDownToggle.setAttribute("tabindex",0)}else{if(!src.matches("".concat(SELECTORS_tab,",").concat(SELECTORS_menuitem))||src.matches(SELECTORS_dropdowntoggle))return;parent=src.parentElement.parentElement.querySelector(".dropdown-menu")}Array.prototype.forEach.call(parent.children,(node=>{const menuItem=node.querySelector(SELECTORS_menuitem);null!==menuItem&&(menuItem.classList.remove("active"),menuItem.removeAttribute("aria-current"))})),"menuitem"===src.getAttribute("role")&&src.setAttribute("aria-current","true")}},keyboardListenerEvents=e=>{const src=e.srcElement,firstNode=e.currentTarget.firstElementChild,lastNode=findUsableLastNode(e.currentTarget);if(src.classList.contains("dropdown-item"))"ArrowRight"!=e.key&&"ArrowLeft"!=e.key||(e.preventDefault(),null!==openDropdownNode&&openDropdownNode.parentElement.click())," "!=e.key&&"Enter"!=e.key||(e.preventDefault(),menuItemHelper(src),src.parentElement.classList.contains("dropdown")||src.click());else{const rtl=window.right_to_left(),arrowNext=rtl?"ArrowLeft":"ArrowRight",arrowPrevious=rtl?"ArrowRight":"ArrowLeft";"menuitem"===src.getAttribute("role")&&(e.key==arrowNext&&(e.preventDefault(),setFocusNext(src,firstNode)),e.key==arrowPrevious&&(e.preventDefault(),setFocusPrev(src,lastNode)),"ArrowUp"!=e.key&&"ArrowDown"!=e.key||(openDropdownNode=src,e.preventDefault()),"Home"==e.key&&(e.preventDefault(),setFocusHomeEnd(firstNode)),"End"==e.key&&(e.preventDefault(),setFocusHomeEnd(lastNode)))," "!=e.key&&"Enter"!=e.key||(e.preventDefault(),src.parentElement.classList.contains("dropdown")||src.click())}},clickListenerEvents=e=>{const src=e.srcElement;menuItemHelper(src)};_exports.default=elementRoot=>{elementRoot.removeEventListener("keydown",keyboardListenerEvents),elementRoot.removeEventListener("click",clickListenerEvents),elementRoot.addEventListener("keydown",keyboardListenerEvents),elementRoot.addEventListener("click",clickListenerEvents)},window.addEventListener("pageshow",(function(){const items=document.querySelectorAll(SELECTORS_dropdownitemactive);null!==items&&items.length>1&&items.forEach((function(e){const href=e.getAttribute("href");href!==window.location.href&&href!==window.location.pathname&&href!==window.location.href+"/index.php"&&href!==window.location.pathname+"index.php"&&(e.classList.remove("active"),e.removeAttribute("aria-current"))}))}));const setFocusNext=(currentNode,firstNode)=>{const listElement=currentNode.parentElement,nextListItem=(el=>{do{el=el.nextElementSibling}while(el&&!el.offsetHeight);return el})(listElement),nodeToSelect=clickErrorHandler(nextListItem,firstNode),itemSelector="tablist"===listElement.parentElement.getAttribute("role")?SELECTORS_tab:SELECTORS_menuitem;nodeToSelect.querySelector(itemSelector).focus()},setFocusPrev=(currentNode,lastNode)=>{const listElement=currentNode.parentElement,nextListItem=(el=>{do{el=el.previousElementSibling}while(el&&!el.offsetHeight);return el})(listElement),nodeToSelect=clickErrorHandler(nextListItem,lastNode),itemSelector="tablist"===listElement.parentElement.getAttribute("role")?SELECTORS_tab:SELECTORS_menuitem;nodeToSelect.querySelector(itemSelector).focus()},setFocusHomeEnd=node=>{node.querySelector(SELECTORS_menuitem).focus()},findUsableLastNode=elementRoot=>{if(elementRoot.lastElementChild.classList.contains("d-none")){const nodesToUse=Array.prototype.map.call(elementRoot.children,(node=>node)).reverse().filter((node=>{if(!node.classList.contains("d-none"))return node}));return 0!==nodesToUse.length?nodesToUse[0]:elementRoot.firstElementChild}return elementRoot.lastElementChild};return _exports.default}));
const SELECTORS_menuitem='[role="menuitem"]',SELECTORS_tab='[role="tab"]',SELECTORS_dropdowntoggle='[data-toggle="dropdown"]';let openDropdownNode=null;const clickErrorHandler=(item,fallback)=>null!==item?item:fallback,menuItemHelper=src=>{let parent;if(!src.dataset.disableactive){if(src.classList.contains("dropdown-item")){parent=src.closest(".dropdown-menu");const dropDownToggle=document.getElementById(parent.getAttribute("aria-labelledby"));dropDownToggle.classList.add("active"),dropDownToggle.setAttribute("tabindex",0)}else{if(!src.matches("".concat(SELECTORS_tab,",").concat(SELECTORS_menuitem))||src.matches(SELECTORS_dropdowntoggle))return;parent=src.parentElement.parentElement.querySelector(".dropdown-menu")}Array.prototype.forEach.call(parent.children,(node=>{const menuItem=node.querySelector(SELECTORS_menuitem);null!==menuItem&&(menuItem.classList.remove("active"),menuItem.removeAttribute("aria-current"))})),"menuitem"===src.getAttribute("role")&&src.setAttribute("aria-current","true")}},keyboardListenerEvents=e=>{const src=e.srcElement,firstNode=e.currentTarget.firstElementChild,lastNode=findUsableLastNode(e.currentTarget);if(src.classList.contains("dropdown-item"))"ArrowRight"!=e.key&&"ArrowLeft"!=e.key||(e.preventDefault(),null!==openDropdownNode&&openDropdownNode.parentElement.click())," "!=e.key&&"Enter"!=e.key||(e.preventDefault(),menuItemHelper(src),src.parentElement.classList.contains("dropdown")||src.click());else{const rtl=window.right_to_left(),arrowNext=rtl?"ArrowLeft":"ArrowRight",arrowPrevious=rtl?"ArrowRight":"ArrowLeft";"menuitem"===src.getAttribute("role")&&(e.key==arrowNext&&(e.preventDefault(),setFocusNext(src,firstNode)),e.key==arrowPrevious&&(e.preventDefault(),setFocusPrev(src,lastNode)),"ArrowUp"!=e.key&&"ArrowDown"!=e.key||(openDropdownNode=src,e.preventDefault()),"Home"==e.key&&(e.preventDefault(),setFocusHomeEnd(firstNode)),"End"==e.key&&(e.preventDefault(),setFocusHomeEnd(lastNode)))," "!=e.key&&"Enter"!=e.key||(e.preventDefault(),src.parentElement.classList.contains("dropdown")||src.click())}},clickListenerEvents=e=>{const src=e.srcElement;menuItemHelper(src)};_exports.default=elementRoot=>{elementRoot.removeEventListener("keydown",keyboardListenerEvents),elementRoot.removeEventListener("click",clickListenerEvents),elementRoot.addEventListener("keydown",keyboardListenerEvents),elementRoot.addEventListener("click",clickListenerEvents)};const setFocusNext=(currentNode,firstNode)=>{const listElement=currentNode.parentElement,nextListItem=(el=>{do{el=el.nextElementSibling}while(el&&!el.offsetHeight);return el})(listElement),nodeToSelect=clickErrorHandler(nextListItem,firstNode),itemSelector="tablist"===listElement.parentElement.getAttribute("role")?SELECTORS_tab:SELECTORS_menuitem;nodeToSelect.querySelector(itemSelector).focus()},setFocusPrev=(currentNode,lastNode)=>{const listElement=currentNode.parentElement,nextListItem=(el=>{do{el=el.previousElementSibling}while(el&&!el.offsetHeight);return el})(listElement),nodeToSelect=clickErrorHandler(nextListItem,lastNode),itemSelector="tablist"===listElement.parentElement.getAttribute("role")?SELECTORS_tab:SELECTORS_menuitem;nodeToSelect.querySelector(itemSelector).focus()},setFocusHomeEnd=node=>{node.querySelector(SELECTORS_menuitem).focus()},findUsableLastNode=elementRoot=>{if(elementRoot.lastElementChild.classList.contains("d-none")){const nodesToUse=Array.prototype.map.call(elementRoot.children,(node=>node)).reverse().filter((node=>{if(!node.classList.contains("d-none"))return node}));return 0!==nodesToUse.length?nodesToUse[0]:elementRoot.firstElementChild}return elementRoot.lastElementChild};return _exports.default}));
//# sourceMappingURL=menu_navigation.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,6 @@ const SELECTORS = {
'menuitem': '[role="menuitem"]',
'tab': '[role="tab"]',
'dropdowntoggle': '[data-toggle="dropdown"]',
'dropdownitemactive': '.dropdown-item[aria-current="true"]',
};
let openDropdownNode = null;
@ -85,30 +84,6 @@ const menuItemHelper = src => {
}
};
/**
* Check if there are sub items in a dropdown menu. There can be one element active only. That is usually controlled
* by the server. However, when you click, the newly clicked item gets the active state as well. This is no problem
* because the user leaves the page and a new page load happens. When the user hits the back button, the old page dom
* is restored from the cache, with both menu items active. If there is such a case, we need to uncheck the item that
* was clicked when leaving this page.
*
*/
const dropDownMenuActiveCheck = function() {
const items = document.querySelectorAll(SELECTORS.dropdownitemactive);
// Do the check only, if there is more than one subitem active.
if (items !== null && items.length > 1) {
items.forEach(function(e) {
// Get the link target from the href attribute and compare it with the current url in the browser.
const href = e.getAttribute('href');
if (href !== window.location.href && href !== window.location.pathname
&& href !== window.location.href + '/index.php' && href !== window.location.pathname + 'index.php') {
e.classList.remove('active');
e.removeAttribute('aria-current');
}
});
}
};
/**
* Defined keyboard event handling so we can remove listeners on nodes on resize etc.
*
@ -205,9 +180,6 @@ export default elementRoot => {
elementRoot.addEventListener('click', clickListenerEvents);
};
// We need this triggered only when the user hits the back button.
window.addEventListener('pageshow', dropDownMenuActiveCheck);
/**
* Handle the focusing to the next element in the dropdown.
*

View File

@ -55,9 +55,9 @@ class primary implements renderable, templatable {
$output = $this->page->get_renderer('core');
}
$menudata = (object) $this->merge_primary_and_custom($this->get_primary_nav(), $this->get_custom_menu($output));
$menudata = (object) array_merge($this->get_primary_nav(), $this->get_custom_menu($output));
$moremenu = new \core\navigation\output\more_menu($menudata, 'navbar-nav', false);
$mobileprimarynav = $this->merge_primary_and_custom($this->get_primary_nav(), $this->get_custom_menu($output), true);
$mobileprimarynav = array_merge($this->get_primary_nav(), $this->get_custom_menu($output));
$languagemenu = new \core\output\language_menu($this->page);
@ -116,111 +116,6 @@ class primary implements renderable, templatable {
return $nodes;
}
/**
* When defining custom menu items, the active flag is not obvserved correctly. Therefore, the merge of the primary
* and custom navigation must be handled a bit smarter. Change the "isactive" flag of the nodes (this may set by
* default in the primary nav nodes but is entirely missing in the custom nav nodes).
* Set the $expandedmenu argument to true when the menu for the mobile template is build.
*
* @param array $primary
* @param array $custom
* @param bool $expandedmenu
* @return array
*/
protected function merge_primary_and_custom(array $primary, array $custom, bool $expandedmenu = false): array {
if (empty($custom)) {
return $primary; // No custom nav, nothing to merge.
}
// Remember the amount of primary nodes and whether we changed the active flag in the custom menu nodes.
$primarylen = count($primary);
$changed = false;
foreach (array_keys($custom) as $i) {
if (!$changed) {
if ($this->flag_active_nodes($custom[$i], $expandedmenu)) {
$changed = true;
}
}
$primary[] = $custom[$i];
}
// In case some custom node is active, mark all primary nav elements as inactive.
if ($changed) {
for ($i = 0; $i < $primarylen; $i++) {
$primary[$i]['isactive'] = false;
}
}
return $primary;
}
/**
* Recursive checks if any of the children is active. If that's the case this node (the parent) is active as
* well. If the node has no children, check if the node itself is active. Use pass by reference for the node
* object because we actively change/set the "isactive" flag inside the method and this needs to be kept at the
* callers side.
* Set $expandedmenu to true, if the mobile menu is done, in this case the active flag gets the node that is
* actually active, while the parent hierarchy of the active node gets the flag isopen.
*
* @param object $node
* @param bool $expandedmenu
* @return bool
*/
protected function flag_active_nodes(object $node, bool $expandedmenu = false): bool {
global $FULLME;
$active = false;
foreach (array_keys($node->children ?? []) as $c) {
if ($this->flag_active_nodes($node->children[$c], $expandedmenu)) {
$active = true;
}
}
// One of the children is active, so this node (the parent) is active as well.
if ($active) {
if ($expandedmenu) {
$node->isopen = true;
} else {
$node->isactive = true;
}
return true;
}
// By default, the menu item node to check is not active.
$node->isactive = false;
// Check if the node url matches the called url. The node url may omit the trailing index.php, therefore check
// this as well.
if (empty($node->url)) {
// Current menu node has no url set, so it can't be active.
return false;
}
$nodeurl = parse_url($node->url);
$current = parse_url($FULLME ?? '');
$pathmatches = false;
// Exact match of the path of node and current url.
if ($nodeurl['path'] === $current['path']) {
$pathmatches = true;
}
// The current url may be trailed by a index.php, otherwise it's the same as the node path.
if (!$pathmatches && $nodeurl['path'] . 'index.php' === $current['path']) {
$pathmatches = true;
}
// No path did match, so the node can't be active.
if (!$pathmatches) {
return false;
}
// We are here because the path matches, so now look at the query string.
$nodequery = $nodeurl['query'] ?? '';
$currentquery = $current['query'] ?? '';
// If the node has no query string defined, then the patch match is sufficient.
if (empty($nodeurl['query'])) {
$node->isactive = true;
return true;
}
// If the node contains a query string then also the current url must match this query.
if ($nodequery === $currentquery) {
$node->isactive = true;
}
return $node->isactive;
}
/**
* Get/Generate the user menu.
*

View File

@ -153,17 +153,6 @@ class primary_test extends \advanced_testcase {
* @param array $expected
*/
public function test_get_custom_menu(string $config, array $expected) {
$actual = $this->get_custom_menu($config);
$this->assertEquals($expected, $actual);
}
/**
* Helper method to get the template data for the custommenuitem that is set here via parameter.
* @param string $config
* @return array
* @throws \ReflectionException
*/
protected function get_custom_menu(string $config): array {
global $CFG, $PAGE;
$CFG->custommenuitems = $config;
$output = new primary($PAGE);
@ -182,7 +171,8 @@ class primary_test extends \advanced_testcase {
$actual = $method->invoke($output, $renderer);
$custommenufilter($actual);
return $actual;
$this->assertEquals($expected, $actual);
}
/**
@ -311,126 +301,4 @@ class primary_test extends \advanced_testcase {
]
];
}
/**
* Test the merge_primary_and_custom and the eval_is_active method. Merge primary and custom menu with different
* page urls and check that the correct nodes are active and open, depending on the data for each menu.
*
* @covers \core\navigation\output\primary::merge_primary_and_custom
* @covers \core\navigation\output\primary::flag_active_nodes
* @return void
* @throws \ReflectionException
* @throws \moodle_exception
*/
public function test_merge_primary_and_custom() {
global $PAGE;
$menu = $this->merge_and_render_menus();
$this->assertEquals(4, count(\array_keys($menu)));
$msg = 'No active nodes for page ' . $PAGE->url;
$this->assertEmpty($this->get_menu_item_names_by_type($menu, 'isactive'), $msg);
$this->assertEmpty($this->get_menu_item_names_by_type($menu, 'isopen'), str_replace('active', 'open', $msg));
$msg = 'Active nodes desktop for /course/search.php';
$menu = $this->merge_and_render_menus('/course/search.php');
$isactive = $this->get_menu_item_names_by_type($menu, 'isactive');
$this->assertEquals(['Courses', 'Course search'], $isactive, $msg);
$this->assertEmpty($this->get_menu_item_names_by_type($menu, 'isopem'), str_replace('Active', 'Open', $msg));
$msg = 'Active nodes mobile for /course/search.php';
$menu = $this->merge_and_render_menus('/course/search.php', true);
$isactive = $this->get_menu_item_names_by_type($menu, 'isactive');
$this->assertEquals(['Course search'], $isactive, $msg);
$isopen = $this->get_menu_item_names_by_type($menu, 'isopen');
$this->assertEquals(['Courses'], $isopen, str_replace('Active', 'Open', $msg));
$msg = 'Active nodes desktop for /course/search.php?areaids=core_course-course&q=test';
$menu = $this->merge_and_render_menus('/course/search.php?areaids=core_course-course&q=test');
$isactive = $this->get_menu_item_names_by_type($menu, 'isactive');
$this->assertEquals(['Courses', 'Course search'], $isactive, $msg);
$msg = 'Active nodes desktop for /?theme=boost';
$menu = $this->merge_and_render_menus('/?theme=boost');
$isactive = $this->get_menu_item_names_by_type($menu, 'isactive');
$this->assertEquals(['Theme', 'Boost'], $isactive, $msg);
}
/**
* Internal function to get an array of top menu items from the primary and the custom menu. The latter is defined
* in this function.
* @param string|null $url
* @param bool|null $ismobile
* @return array
* @throws \ReflectionException
* @throws \coding_exception
*/
protected function merge_and_render_menus(?string $url = null, ?bool $ismobile = false): array {
global $PAGE, $FULLME;
if ($url !== null) {
$PAGE->set_url($url);
$FULLME = $PAGE->url->out();
}
$primary = new primary($PAGE);
$method = new ReflectionMethod('core\navigation\output\primary', 'get_primary_nav');
$method->setAccessible(true);
$dataprimary = $method->invoke($primary);
// Take this custom menu that would come from the setting custommenitems.
$custommenuitems = <<< ENDMENU
Theme
-Boost|/?theme=boost
-Classic|/?theme=classic
-Purge Cache|/admin/purgecaches.php
Courses
-All courses|/course/
-Course search|/course/search.php
-###
-FAQ|https://example.org/faq
-My Important Course|/course/view.php?id=4
Mobile app|https://example.org/app|Download our app
ENDMENU;
$datacustom = $this->get_custom_menu($custommenuitems);
$method = new ReflectionMethod('core\navigation\output\primary', 'merge_primary_and_custom');
$method->setAccessible(true);
$menucomplete = $method->invoke($primary, $dataprimary, $datacustom, $ismobile);
return $menucomplete;
}
/**
* Traverse the menu array structure (all nodes recursively) and fetch the node texts from the menu nodes that are
* active/open (determined via param $nodetype that can be "inactive" or "isopen"). The returned array contains a
* list of nade names that match this criterion.
* @param array $menu
* @param string $nodetype
* @return array
*/
protected function get_menu_item_names_by_type(array $menu, string $nodetype): array {
$matchednodes = [];
foreach ($menu as $menuitem) {
// Either the node is an array.
if (is_array($menuitem)) {
if ($menuitem[$nodetype] ?? false) {
$matchednodes[] = $menuitem['text'];
}
// Recursively move through child items.
if (array_key_exists('children', $menuitem) && count($menuitem['children'])) {
$matchednodes = array_merge($matchednodes, $this->get_menu_item_names_by_type($menuitem['children'], $nodetype));
}
} else {
// Otherwise the node is a standard object.
if (isset($menuitem->{$nodetype}) && $menuitem->{$nodetype} === true) {
$matchednodes[] = $menuitem->text;
}
// Recursively move through child items.
if (isset($menuitem->children) && is_array($menuitem->children) && !empty($menuitem->children)) {
$matchednodes = array_merge($matchednodes, $this->get_menu_item_names_by_type($menuitem->children, $nodetype));
}
}
}
return $matchednodes;
}
}

View File

@ -62,7 +62,7 @@
<div class="list-group">
{{#mobileprimarynav}}
{{#haschildren}}
<a id="drop-down-{{sort}}" href="#" class="list-group-item list-group-item-action icons-collapse-expand {{^isopen}}collapsed {{/isopen}}d-flex" data-toggle="collapse" data-target="#drop-down-menu-{{sort}}" aria-expanded="{{#isopen}}true{{/isopen}}{{^isopen}}false{{/isopen}}" aria-controls="drop-down-menu-{{sort}}">
<a id="drop-down-{{sort}}" href="#" class="list-group-item list-group-item-action icons-collapse-expand collapsed d-flex" data-toggle="collapse" data-target="#drop-down-menu-{{sort}}" aria-expanded="false" aria-controls="drop-down-menu-{{sort}}">
{{{text}}}
<span class="ml-auto expanded-icon icon-no-margin mx-2">
{{#pix}} t/expanded, core {{/pix}}
@ -77,10 +77,10 @@
</span>
</span>
</a>
<div class="collapse {{#isopen}}show {{/isopen}}list-group-item p-0 border-0" role="menu" id="drop-down-menu-{{sort}}" aria-labelledby="drop-down-{{sort}}">
<div class="collapse list-group-item p-0 border-0" role="menu" id="drop-down-menu-{{sort}}" aria-labelledby="drop-down-{{sort}}">
{{#children}}
{{^divider}}
<a href="{{{url}}}" class="pl-5 {{^isactive}}bg-light{{/isactive}}{{#isactive}}active{{/isactive}} list-group-item list-group-item-action">{{{text}}}</a>
<a href="{{{url}}}" class="pl-5 bg-light list-group-item list-group-item-action">{{{text}}}</a>
{{/divider}}
{{/children}}
</div>