Merge branch 'MDL-73633-master' of git://github.com/mihailges/moodle
[moodle.git] / lib / classes / navigation / views / secondary.php
blob42b3b7aa8462757396b6e09003bf5716066135b8
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 namespace core\navigation\views;
19 use navigation_node;
20 use url_select;
22 /**
23 * Class secondary_navigation_view.
25 * The secondary navigation view is a stripped down tweaked version of the
26 * settings_navigation/navigation
28 * @package core
29 * @category navigation
30 * @copyright 2021 onwards Peter Dias
31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33 class secondary extends view {
34 /** @var string $headertitle The header for this particular menu*/
35 public $headertitle;
37 /** @var int The maximum limit of navigation nodes displayed in the secondary navigation */
38 const MAX_DISPLAYED_NAV_NODES = 5;
40 /** @var navigation_node The course overflow node. */
41 protected $courseoverflownode = null;
43 /**
44 * Defines the default structure for the secondary nav in a course context.
46 * In a course context, we are curating nodes from the settingsnav and navigation objects.
47 * The following mapping construct specifies which object we are fetching it from, the type of the node, the key
48 * and in what order we want the node - defined as per the mockups.
50 * @return array
52 protected function get_default_course_mapping(): array {
53 $nodes = [];
54 $nodes['settings'] = [
55 self::TYPE_CONTAINER => [
56 'coursereports' => 3,
57 'questionbank' => 4,
59 self::TYPE_SETTING => [
60 'editsettings' => 0,
61 'review' => 1.1,
62 'manageinstances' => 1.2,
63 'groups' => 1.3,
64 'override' => 1.4,
65 'roles' => 1.5,
66 'permissions' => 1.6,
67 'otherusers' => 1.7,
68 'gradebooksetup' => 2.1,
69 'outcomes' => 2.2,
70 'coursecompletion' => 6,
71 'coursebadges' => 7.1,
72 'newbadge' => 7.2,
73 'filtermanagement' => 9,
74 'unenrolself' => 10,
75 'coursetags' => 11,
76 'download' => 12,
77 'contextlocking' => 13,
80 $nodes['navigation'] = [
81 self::TYPE_CONTAINER => [
82 'participants' => 1,
84 self::TYPE_SETTING => [
85 'grades' => 2,
86 'badgesview' => 7,
87 'competencies' => 8,
89 self::TYPE_CUSTOM => [
90 'contentbank' => 5,
94 return $nodes;
97 /**
98 * Defines the default structure for the secondary nav in a module context.
100 * In a module context, we are curating nodes from the settingsnav object.
101 * The following mapping construct specifies the type of the node, the key
102 * and in what order we want the node - defined as per the mockups.
104 * @return array
106 protected function get_default_module_mapping(): array {
107 return [
108 self::TYPE_SETTING => [
109 'modedit' => 1,
110 "mod_{$this->page->activityname}_useroverrides" => 3, // Overrides are module specific.
111 "mod_{$this->page->activityname}_groupoverrides" => 4,
112 'roleassign' => 5,
113 'filtermanage' => 6,
114 'roleoverride' => 7,
115 'rolecheck' => 7.1,
116 'logreport' => 8,
117 'backup' => 9,
118 'restore' => 10,
119 'competencybreakdown' => 11,
121 self::TYPE_CUSTOM => [
122 'advgrading' => 2,
123 'contentbank' => 12,
129 * Defines the default structure for the secondary nav in a category context.
131 * In a category context, we are curating nodes from the settingsnav object.
132 * The following mapping construct specifies the type of the node, the key
133 * and in what order we want the node - defined as per the mockups.
135 * @return array
137 protected function get_default_category_mapping(): array {
138 return [];
142 * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.
144 * @return array
146 protected function get_default_course_more_menu_nodes(): array {
147 return [];
151 * Define the keys of the module secondary nav nodes that should be forced into the "more" menu by default.
153 * @return array
155 protected function get_default_module_more_menu_nodes(): array {
156 return ['roleoverride', 'rolecheck', 'logreport', 'roleassign', 'filtermanage', 'backup', 'restore',
157 'competencybreakdown', "mod_{$this->page->activityname}_useroverrides",
158 "mod_{$this->page->activityname}_groupoverrides"];
162 * Define the keys of the admin secondary nav nodes that should be forced into the "more" menu by default.
164 * @return array
166 protected function get_default_admin_more_menu_nodes(): array {
167 return [];
171 * Initialise the view based navigation based on the current context.
173 * As part of the initial restructure, the secondary nav is only considered for the following pages:
174 * 1 - Site admin settings
175 * 2 - Course page - Does not include front_page which has the same context.
176 * 3 - Module page
178 public function initialise(): void {
179 global $SITE;
181 if (during_initial_install() || $this->initialised) {
182 return;
184 $this->id = 'secondary_navigation';
185 $context = $this->context;
186 $this->headertitle = get_string('menu');
187 $defaultmoremenunodes = [];
188 $maxdisplayednodes = self::MAX_DISPLAYED_NAV_NODES;
190 switch ($context->contextlevel) {
191 case CONTEXT_COURSE:
192 $this->headertitle = get_string('courseheader');
193 $this->load_course_navigation();
194 $defaultmoremenunodes = $this->get_default_course_more_menu_nodes();
195 break;
196 case CONTEXT_MODULE:
197 $this->headertitle = get_string('activityheader');
198 $this->load_module_navigation();
199 $defaultmoremenunodes = $this->get_default_module_more_menu_nodes();
200 break;
201 case CONTEXT_COURSECAT:
202 $this->headertitle = get_string('categoryheader');
203 $this->load_category_navigation();
204 break;
205 case CONTEXT_SYSTEM:
206 $this->headertitle = get_string('homeheader');
207 $this->load_admin_navigation();
208 // If the site administration navigation was generated after load_admin_navigation().
209 if ($this->has_children()) {
210 // Do not explicitly limit the number of navigation nodes displayed in the site administration
211 // navigation menu.
212 $maxdisplayednodes = null;
214 $defaultmoremenunodes = $this->get_default_admin_more_menu_nodes();
215 break;
218 $this->remove_unwanted_nodes();
220 // Don't need to show anything if only the view node is available. Remove it.
221 if ($this->children->count() == 1) {
222 $this->children->remove('modulepage');
224 // Force certain navigation nodes to be displayed in the "more" menu.
225 $this->force_nodes_into_more_menu($defaultmoremenunodes, $maxdisplayednodes);
226 // Search and set the active node.
227 $this->scan_for_active_node($this);
228 $this->initialised = true;
232 * Returns a node with the action being from the first found child node that has an action (Recursive).
234 * @param navigation_node $node The part of the node tree we are checking.
235 * @param navigation_node $basenode The very first node to be used for the return.
236 * @return navigation_node|null
238 protected function get_node_with_first_action(navigation_node $node, navigation_node $basenode): ?navigation_node {
239 $newnode = null;
240 if (!$node->has_children()) {
241 return null;
244 // Find the first child with an action and update the main node.
245 foreach ($node->children as $child) {
246 if ($child->has_action()) {
247 $newnode = $basenode;
248 $newnode->action = $child->action;
249 return $newnode;
252 if (is_null($newnode)) {
253 // Check for children and go again.
254 foreach ($node->children as $child) {
255 if ($child->has_children()) {
256 $newnode = $this->get_node_with_first_action($child, $basenode);
258 if (!is_null($newnode)) {
259 return $newnode;
264 return null;
268 * Some nodes are containers only with no action. If this container has an action then nothing is done. If it does not have
269 * an action then a search is done through the children looking for the first node that has an action. This action is then given
270 * to the parent node that is initially provided as a parameter.
272 * @param navigation_node $node The navigation node that we want to ensure has an action tied to it.
273 * @return navigation_node The node intact with an action to use.
275 protected function get_first_action_for_node(navigation_node $node): ?navigation_node {
276 // If the node does not have children OR has an action no further processing needed.
277 $newnode = null;
278 if ($node->has_children()) {
279 if (!$node->has_action()) {
280 // We want to find the first child with an action.
281 // We want to check all children on this level before going further down.
282 // Note that new node gets changed here.
283 $newnode = $this->get_node_with_first_action($node, $node);
284 } else {
285 $newnode = $node;
288 return $newnode;
292 * Returns a list of all expected nodes in the course administration.
294 * @return array An array of keys for navigation nodes in the course administration.
296 protected function get_expected_course_admin_nodes(): array {
297 $expectednodes = [];
298 foreach ($this->get_default_course_mapping()['settings'] as $value) {
299 foreach ($value as $nodekey => $notused) {
300 $expectednodes[] = $nodekey;
303 foreach ($this->get_default_course_mapping()['navigation'] as $value) {
304 foreach ($value as $nodekey => $notused) {
305 $expectednodes[] = $nodekey;
308 $othernodes = ['users', 'gradeadmin', 'coursereports', 'coursebadges'];
309 $leftovercourseadminnodes = ['backup', 'restore', 'import', 'copy', 'reset'];
310 $expectednodes = array_merge($expectednodes, $othernodes);
311 $expectednodes = array_merge($expectednodes, $leftovercourseadminnodes);
312 return $expectednodes;
316 * Load the course secondary navigation. Since we are sourcing all the info from existing objects that already do
317 * the relevant checks, we don't do it again here.
319 protected function load_course_navigation(): void {
320 $course = $this->page->course;
321 // Initialise the main navigation and settings nav.
322 // It is important that this is done before we try anything.
323 $settingsnav = $this->page->settingsnav;
324 $navigation = $this->page->navigation;
326 $url = new \moodle_url('/course/view.php', ['id' => $course->id]);
327 $this->add(get_string('course'), $url, self::TYPE_COURSE, null, 'coursehome');
329 $nodes = $this->get_default_course_mapping();
330 $nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings'] ?? []);
331 $nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation'] ?? []);
332 $this->add_ordered_nodes($nodesordered);
334 // Try to get any custom nodes defined by a user which may include containers.
335 $expectedcourseadmin = $this->get_expected_course_admin_nodes();
337 foreach ($settingsnav->children as $value) {
338 if ($value->key == 'courseadmin') {
339 foreach ($value->children as $other) {
340 if (array_search($other->key, $expectedcourseadmin) === false) {
341 $othernode = $this->get_first_action_for_node($other);
342 // Get the first node and check whether it's been added already.
343 if ($othernode && !$this->get($othernode->key)) {
344 $this->add_node($othernode);
345 } else {
346 $this->add_node($other);
353 $coursecontext = \context_course::instance($course->id);
354 if (has_capability('moodle/course:update', $coursecontext)) {
355 $overflownode = $this->get_course_overflow_nodes();
356 if (is_null($overflownode)) {
357 return;
359 $actionnode = $this->get_first_action_for_node($overflownode);
360 // All additional nodes will be available under the 'Course reuse' page.
361 $text = get_string('coursereuse');
362 $this->add($text, $actionnode->action, null, null, 'courseadmin', new \pix_icon('t/edit', $text));
367 * Gets the overflow navigation nodes for the course administration category.
369 * @return navigation_node The course overflow nodes.
371 protected function get_course_overflow_nodes(): ?navigation_node {
372 global $SITE;
374 // This gets called twice on some pages, and so trying to create this navigation node twice results in no children being
375 // present the second time this is called.
376 if (isset($this->courseoverflownode)) {
377 return $this->courseoverflownode;
380 // Start with getting the base node for the front page or the course.
381 $node = null;
382 if ($this->page->course == $SITE->id) {
383 $node = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
384 } else {
385 $node = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
387 $coursesettings = $node ? $node->get_children_key_list() : [];
388 $thissettings = $this->get_children_key_list();
389 $diff = array_diff($coursesettings, $thissettings);
391 // Remove our specific created elements (user - participants, badges - coursebadges, grades - gradebooksetup,
392 // grades - outcomes).
393 $shortdiff = array_filter($diff, function($value) {
394 return !($value == 'users' || $value == 'coursebadges' || $value == 'gradebooksetup' ||
395 $value == 'outcomes');
398 // Permissions may be in play here that ultimately will show no overflow.
399 if (empty($shortdiff)) {
400 return null;
403 $firstitem = array_shift($shortdiff);
404 $navnode = $node->get($firstitem);
405 foreach ($shortdiff as $key) {
406 $courseadminnodes = $node->get($key);
407 if ($courseadminnodes) {
408 if ($courseadminnodes->parent->key == $node->key) {
409 $navnode->add_node($courseadminnodes);
413 $this->courseoverflownode = $navnode;
414 return $navnode;
419 * Recursively looks for a match to the current page url.
421 * @param navigation_node $node The node to look through.
422 * @return navigation_node|null The node that matches this page's url.
424 protected function nodes_match_current_url(navigation_node $node): ?navigation_node {
425 $pagenode = $this->page->url;
426 if ($node->has_action()) {
427 // Check this node first.
428 if ($node->action->compare($pagenode)) {
429 return $node;
432 if ($node->has_children()) {
433 foreach ($node->children as $child) {
434 $result = $this->nodes_match_current_url($child);
435 if ($result) {
436 return $result;
440 return null;
444 * Returns a url_select object with overflow navigation nodes.
445 * This looks to see if the current page is within the course administration, or some other page that requires an overflow
446 * select object.
448 * @return url_select|null The overflow menu data.
450 public function get_overflow_menu_data(): ?url_select {
452 if (!$this->page->get_navigation_overflow_state()) {
453 return null;
456 $activenode = $this->find_active_node();
457 $incourseadmin = false;
459 if (!$activenode) {
460 // Could be in the course admin section.
461 $courseadmin = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
462 if (!$courseadmin) {
463 return null;
466 $activenode = $courseadmin->find_active_node();
467 if (!$activenode) {
468 return null;
470 $incourseadmin = true;
473 if ($activenode->key == 'courseadmin' || $incourseadmin) {
474 $courseoverflownode = $this->get_course_overflow_nodes();
475 if (is_null($courseoverflownode)) {
476 return null;
478 $menuarray = static::create_menu_element([$courseoverflownode]);
479 if ($activenode->key != 'courseadmin') {
480 $inmenu = false;
481 foreach ($menuarray as $key => $value) {
482 if ($this->page->url->out(false) == $key) {
483 $inmenu = true;
486 if (!$inmenu) {
487 return null;
490 $menuselect = new url_select($menuarray, $this->page->url, null);
491 $menuselect->set_label(get_string('browsecourseadminindex', 'course'), ['class' => 'sr-only']);
492 return $menuselect;
493 } else {
494 return $this->get_other_overflow_menu_data($activenode);
499 * Gets overflow menu data for third party plugin settings.
501 * @param navigation_node $activenode The node to gather the children for to put into the overflow menu.
502 * @return url_select|null The overflow menu in a url_select object.
504 protected function get_other_overflow_menu_data(navigation_node $activenode): ?url_select {
505 if (!$activenode->has_action()) {
506 return null;
509 if (!$activenode->has_children()) {
510 return null;
513 // If the setting is extending the course navigation then the page being redirected to should be in the course context.
514 // It was decided on the issue that put this code here that plugins that extend the course navigation should have the pages
515 // that are redirected to, be in the course context or module context depending on which callback was used.
516 // Third part plugins were checked to see if any existing plugins had settings in a system context and none were found.
517 // The request of third party developers is to keep their settings within the specified context.
518 if ($this->page->context->contextlevel != CONTEXT_COURSE && $this->page->context->contextlevel != CONTEXT_MODULE) {
519 return null;
522 // These areas have their own code to retrieve added plugin navigation nodes.
523 if ($activenode->key == 'coursehome' || $activenode->key == 'questionbank' || $activenode->key == 'coursereports') {
524 return null;
527 $menunode = $this->page->settingsnav->find($activenode->key, null);
529 if (!$menunode instanceof navigation_node) {
530 return null;
532 // Loop through all children and try and find a match to the current url.
533 $matchednode = $this->nodes_match_current_url($menunode);
534 if (is_null($matchednode)) {
535 return null;
537 if (!isset($menunode) || !$menunode->has_children()) {
538 return null;
540 $selectdata = static::create_menu_element([$menunode], false);
541 $urlselect = new url_select($selectdata, $matchednode->action->out(false), null);
542 $urlselect->set_label(get_string('browsesettingindex', 'course'), ['class' => 'sr-only']);
543 return $urlselect;
547 * Get the module's secondary navigation. This is based on settings_nav and would include plugin nodes added via
548 * '_extend_settings_navigation'.
549 * It populates the tree based on the nav mockup
551 * If nodes change, we will have to explicitly call the callback again.
553 protected function load_module_navigation(): void {
554 $settingsnav = $this->page->settingsnav;
555 $mainnode = $settingsnav->find('modulesettings', self::TYPE_SETTING);
556 $nodes = $this->get_default_module_mapping();
558 if ($mainnode) {
559 $url = new \moodle_url('/mod/' . $this->page->activityname . '/view.php', ['id' => $this->page->cm->id]);
560 $setactive = $url->compare($this->page->url, URL_MATCH_BASE);
561 $node = $this->add(get_string('modulename', $this->page->activityname), $url, null, null, 'modulepage');
562 if ($setactive) {
563 $node->make_active();
565 // Add the initial nodes.
566 $nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
567 $this->add_ordered_nodes($nodesordered);
569 // We have finished inserting the initial structure.
570 // Populate the menu with the rest of the nodes available.
571 $this->load_remaining_nodes($mainnode, $nodes);
576 * Load the course category navigation.
578 protected function load_category_navigation(): void {
579 $settingsnav = $this->page->settingsnav;
580 $mainnode = $settingsnav->find('categorysettings', self::TYPE_CONTAINER);
581 $nodes = $this->get_default_category_mapping();
583 if ($mainnode) {
584 $url = new \moodle_url('/course/index.php', ['categoryid' => $this->context->instanceid]);
585 $this->add($this->context->get_context_name(), $url, self::TYPE_CONTAINER, null, 'categorymain');
587 // Add the initial nodes.
588 $nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
589 $this->add_ordered_nodes($nodesordered);
591 // We have finished inserting the initial structure.
592 // Populate the menu with the rest of the nodes available.
593 $this->load_remaining_nodes($mainnode, $nodes);
598 * Load the site admin navigation
600 protected function load_admin_navigation(): void {
601 global $PAGE, $SITE;
603 $settingsnav = $this->page->settingsnav;
604 $node = $settingsnav->find('root', self::TYPE_SITE_ADMIN);
605 // We need to know if we are on the main site admin search page. Here the navigation between tabs are done via
606 // anchors and page reload doesn't happen. On every nested admin settings page, the secondary nav needs to
607 // exist as links with anchors appended in order to redirect back to the admin search page and the corresponding
608 // tab. Note this value refers to being present on the page itself, before a search has been performed.
609 $isadminsearchpage = $PAGE->url->compare(new \moodle_url('/admin/search.php', ['query' => '']), URL_MATCH_PARAMS);
610 if ($node) {
611 $siteadminnode = $this->add(get_string('general'), "#link$node->key", null, null, 'siteadminnode');
612 if ($isadminsearchpage) {
613 $siteadminnode->action = false;
614 $siteadminnode->tab = "#link$node->key";
615 } else {
616 $siteadminnode->action = new \moodle_url("/admin/search.php", [], "link$node->key");
618 foreach ($node->children as $child) {
619 if ($child->display && !$child->is_short_branch()) {
620 // Mimic the current boost behaviour and pass down anchors for the tabs.
621 if ($isadminsearchpage) {
622 $child->action = false;
623 $child->tab = "#link$child->key";
624 } else {
625 $child->action = new \moodle_url("/admin/search.php", [], "link$child->key");
627 $this->add_node(clone $child);
628 } else {
629 $siteadminnode->add_node(clone $child);
632 } else if ($this->page->course->id == $SITE->id) {
633 $this->load_course_navigation();
638 * Adds the indexed nodes to the current view. The key should indicate it's position in the tree. Any sub nodes
639 * needs to be numbered appropriately, e.g. 3.1 would make the identified node be listed under #3 node.
641 * @param array $nodes An array of navigation nodes to be added.
643 protected function add_ordered_nodes(array $nodes): void {
644 ksort($nodes);
645 foreach ($nodes as $key => $node) {
646 // If the key is a string then we are assuming this is a nested element.
647 if (is_string($key)) {
648 $parentnode = $nodes[floor($key)] ?? null;
649 if ($parentnode) {
650 $parentnode->add_node(clone $node);
652 } else {
653 $this->add_node(clone $node);
659 * Find the remaining nodes that need to be loaded into secondary based on the current context
661 * @param navigation_node $completenode The original node that we are sourcing information from
662 * @param array $nodesmap The map used to populate secondary nav in the given context
664 protected function load_remaining_nodes(navigation_node $completenode, array $nodesmap): void {
665 $flattenednodes = [];
666 foreach ($nodesmap as $nodecontainer) {
667 $flattenednodes = array_merge(array_keys($nodecontainer), $flattenednodes);
670 $populatedkeys = $this->get_children_key_list();
671 $existingkeys = $completenode->get_children_key_list();
672 $leftover = array_diff($existingkeys, $populatedkeys);
673 foreach ($leftover as $key) {
674 if (!in_array($key, $flattenednodes) && $leftovernode = $completenode->get($key)) {
675 // Check for nodes with children and potentially no action to direct to.
676 if ($leftovernode->has_children()) {
677 $leftovernode = $this->get_first_action_for_node($leftovernode);
680 // Confirm we have a valid object to add.
681 if ($leftovernode) {
682 $this->add_node(clone $leftovernode);
689 * Force certain secondary navigation nodes to be displayed in the "more" menu.
691 * @param array $defaultmoremenunodes Array with navigation node keys of the pre-defined nodes that
692 * should be added into the "more" menu by default
693 * @param int|null $maxdisplayednodes The maximum limit of navigation nodes displayed in the secondary navigation
695 protected function force_nodes_into_more_menu(array $defaultmoremenunodes = [], ?int $maxdisplayednodes = null) {
696 // Counter of the navigation nodes that are initially displayed in the secondary nav
697 // (excludes the nodes from the "more" menu).
698 $displayednodescount = 0;
699 foreach ($this->children as $child) {
700 // Skip if the navigation node has been already forced into the "more" menu.
701 if ($child->forceintomoremenu) {
702 continue;
704 // If the navigation node is in the pre-defined list of nodes that should be added by default in the
705 // "more" menu or the maximum limit of displayed navigation nodes has been reached (if defined).
706 if (in_array($child->key, $defaultmoremenunodes) ||
707 (!is_null($maxdisplayednodes) && $displayednodescount >= $maxdisplayednodes)) {
708 // Force the node and its children into the "more" menu.
709 $child->set_force_into_more_menu(true);
710 continue;
712 $displayednodescount++;
717 * Remove navigation nodes that should not be displayed in the secondary navigation.
719 protected function remove_unwanted_nodes() {
720 foreach ($this->children as $child) {
721 if (!$child->showinsecondarynavigation) {
722 $child->remove();
728 * Takes the given navigation nodes and searches for children and formats it all into an array in a format to be used by a
729 * url_select element.
731 * @param navigation_node[] $navigationnodes Navigation nodes to format into a menu.
732 * @param bool $forceheadings Whether the returned array should be forced to use headings.
733 * @return array|null A url select element for navigating through the navigation nodes.
735 public static function create_menu_element(array $navigationnodes, bool $forceheadings = false): ?array {
736 if (empty($navigationnodes)) {
737 return null;
740 // If one item, do we put this into a url_select?
741 if (count($navigationnodes) < 2) {
742 // Check if there are children.
743 $navnode = array_shift($navigationnodes);
744 $menudata = [];
745 if (!$navnode->has_children()) {
746 // Just one item.
747 if (!$navnode->has_action()) {
748 return null;
750 $menudata[$navnode->action->out(false)] = static::format_node_text($navnode);
751 } else {
752 if (static::does_menu_need_headings($navnode) || $forceheadings) {
753 // Let's do headings.
754 $menudata = static::get_headings_nav_array($navnode);
755 } else {
756 // Simple flat nav.
757 $menudata = static::get_flat_nav_array($navnode);
760 return $menudata;
761 } else {
762 // We have more than one navigation node to handle. Put each node in it's own heading.
763 $menudata = [];
764 $titledata = [];
765 foreach ($navigationnodes as $navigationnode) {
766 if ($navigationnode->has_children()) {
767 $menuarray = [];
768 // Add a heading and flatten out everything else.
769 if ($navigationnode->has_action()) {
770 $menuarray[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =
771 static::format_node_text($navigationnode);
772 $menuarray[static::format_node_text($navigationnode)] += static::get_whole_tree_flat($navigationnode);
773 } else {
774 $menuarray[static::format_node_text($navigationnode)] = static::get_whole_tree_flat($navigationnode);
777 $titledata += $menuarray;
778 } else {
779 // Add with no heading.
780 if (!$navigationnode->has_action()) {
781 return null;
783 $menudata[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);
786 $menudata += [$titledata];
787 return $menudata;
792 * Recursively goes through the provided navigation node and returns a flat version.
794 * @param navigation_node $navigationnode The navigationnode.
795 * @return array The whole tree flat.
797 protected static function get_whole_tree_flat(navigation_node $navigationnode): array {
798 $nodes = [];
799 foreach ($navigationnode->children as $child) {
800 if ($child->has_action()) {
801 $nodes[$child->action->out()] = $child->text;
803 if ($child->has_children()) {
804 $childnodes = static::get_whole_tree_flat($child);
805 $nodes = array_merge($nodes, $childnodes);
808 return $nodes;
812 * Checks to see if the provided navigation node has children and determines if we want headings for a url select element.
814 * @param navigation_node $navigationnode The navigation node we are checking.
815 * @return bool Whether we want headings or not.
817 protected static function does_menu_need_headings(navigation_node $navigationnode): bool {
818 if (!$navigationnode->has_children()) {
819 return false;
821 foreach ($navigationnode->children as $child) {
822 if ($child->has_children()) {
823 return true;
826 return false;
830 * Takes the navigation node and returns it in a flat fashion. This is not recursive.
832 * @param navigation_node $navigationnode The navigation node that we want to format into an array in a flat structure.
833 * @return array The flat navigation array.
835 protected static function get_flat_nav_array(navigation_node $navigationnode): array {
836 $menuarray = [];
837 if ($navigationnode->has_action()) {
838 $menuarray[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);
841 foreach ($navigationnode->children as $child) {
842 if ($child->has_action()) {
843 $menuarray[$child->action->out(false)] = static::format_node_text($child);
846 return $menuarray;
850 * For any navigation node that we have determined needs headings we return a more tree like array structure.
852 * @param navigation_node $navigationnode The navigation node to use for the formatted array structure.
853 * @return array The headings navigation array structure.
855 protected static function get_headings_nav_array(navigation_node $navigationnode): array {
856 $menublock = [];
857 // We know that this single node has headings, so grab this for the first heading.
858 $firstheading = [];
859 if ($navigationnode->has_action()) {
860 $firstheading[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =
861 static::format_node_text($navigationnode);
862 $firstheading[static::format_node_text($navigationnode)] += static::get_more_child_nodes($navigationnode, $menublock);
863 } else {
864 $firstheading[static::format_node_text($navigationnode)] = static::get_more_child_nodes($navigationnode, $menublock);
866 return [$firstheading + $menublock];
870 * Recursively goes and gets all children nodes.
872 * @param navigation_node $node The node to get the children of.
873 * @param array $menublock Used to put all child nodes in its own container.
874 * @return array The additional child nodes.
876 protected static function get_more_child_nodes(navigation_node $node, array &$menublock): array {
877 $nodes = [];
878 foreach ($node->children as $child) {
879 if (!$child->has_children()) {
880 if (!$child->has_action()) {
881 continue;
883 $nodes[$child->action->out(false)] = static::format_node_text($child);
884 } else {
885 $newarray = [];
886 if ($child->has_action()) {
887 $newarray[static::format_node_text($child)][$child->action->out(false)] = static::format_node_text($child);
888 $newarray[static::format_node_text($child)] += static::get_more_child_nodes($child, $menublock);
889 } else {
890 $newarray[static::format_node_text($child)] = static::get_more_child_nodes($child, $menublock);
892 $menublock += $newarray;
895 return $nodes;
899 * Returns the navigation node text in a string.
901 * @param navigation_node $navigationnode The navigationnode to return the text string of.
902 * @return string The navigation node text string.
904 protected static function format_node_text(navigation_node $navigationnode): string {
905 return (is_a($navigationnode->text, 'lang_string')) ? $navigationnode->text->out() : $navigationnode->text;