Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// 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/>.namespace core\navigation\views;use navigation_node;use url_select;use settings_navigation;/*** Class secondary_navigation_view.** The secondary navigation view is a stripped down tweaked version of the* settings_navigation/navigation** @package core* @category navigation* @copyright 2021 onwards Peter Dias* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class secondary extends view {/** @var string $headertitle The header for this particular menu*/public $headertitle;/** @var int The maximum limit of navigation nodes displayed in the secondary navigation */const MAX_DISPLAYED_NAV_NODES = 5;/** @var navigation_node The course overflow node. */protected $courseoverflownode = null;/** @var string The key of the node to set as selected in the course overflow menu, if explicitly set by a page. */protected $overflowselected = null;/*** Defines the default structure for the secondary nav in a course context.** In a course context, we are curating nodes from the settingsnav and navigation objects.* The following mapping construct specifies which object we are fetching it from, the type of the node, the key* and in what order we want the node - defined as per the mockups.** @return array*/protected function get_default_course_mapping(): array {$nodes = [];$nodes['settings'] = [self::TYPE_CONTAINER => ['coursereports' => 3,'questionbank' => 4,],self::TYPE_SETTING => ['editsettings' => 0,'review' => 1.1,'manageinstances' => 1.2,'groups' => 1.3,'override' => 1.4,'roles' => 1.5,'permissions' => 1.6,'otherusers' => 1.7,'gradebooksetup' => 2.1,'outcomes' => 2.2,'coursecompletion' => 6,'coursebadges' => 7.1,'newbadge' => 7.2,'filtermanagement' => 9,'unenrolself' => 10,'coursetags' => 11,'download' => 12,'contextlocking' => 13,],];$nodes['navigation'] = [self::TYPE_CONTAINER => ['participants' => 1,],self::TYPE_SETTING => ['grades' => 2,'badgesview' => 7,'competencies' => 8,'communication' => 14,],self::TYPE_CUSTOM => ['contentbank' => 5,'participants' => 1, // In site home, 'participants' is classified differently.],];return $nodes;}/*** Defines the default structure for the secondary nav in a module context.** In a module context, we are curating nodes from the settingsnav object.* The following mapping construct specifies the type of the node, the key* and in what order we want the node - defined as per the mockups.** @return array*/protected function get_default_module_mapping(): array {return [self::TYPE_SETTING => ['modedit' => 1,"mod_{$this->page->activityname}_useroverrides" => 3, // Overrides are module specific."mod_{$this->page->activityname}_groupoverrides" => 4,'roleassign' => 7.2,'filtermanage' => 6,'roleoverride' => 7,'rolecheck' => 7.1,'logreport' => 8,'backup' => 9,'restore' => 10,'competencybreakdown' => 11,'sendtomoodlenet' => 16,],self::TYPE_CUSTOM => ['advgrading' => 2,'contentbank' => 12,],];}/*** Defines the default structure for the secondary nav in a category context.** In a category context, we are curating nodes from the settingsnav object.* The following mapping construct specifies the type of the node, the key* and in what order we want the node - defined as per the mockups.** @return array*/protected function get_default_category_mapping(): array {return [self::TYPE_SETTING => ['edit' => 1,'permissions' => 2,'roles' => 2.1,'rolecheck' => 2.2,]];}/*** Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.** @return array*/protected function get_default_category_more_menu_nodes(): array {return ['addsubcat', 'roles', 'permissions', 'contentbank', 'cohort', 'filters', 'restorecourse'];}/*** Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.** @return array*/protected function get_default_course_more_menu_nodes(): array {return [];}/*** Define the keys of the module secondary nav nodes that should be forced into the "more" menu by default.** @return array*/protected function get_default_module_more_menu_nodes(): array {return ['roleoverride', 'rolecheck', 'logreport', 'roleassign', 'filtermanage', 'backup', 'restore','competencybreakdown', "mod_{$this->page->activityname}_useroverrides","mod_{$this->page->activityname}_groupoverrides"];}/*** Define the keys of the admin secondary nav nodes that should be forced into the "more" menu by default.** @return array*/protected function get_default_admin_more_menu_nodes(): array {return [];}/*** Initialise the view based navigation based on the current context.** As part of the initial restructure, the secondary nav is only considered for the following pages:* 1 - Site admin settings* 2 - Course page - Does not include front_page which has the same context.* 3 - Module page*/public function initialise(): void {global $SITE;if (during_initial_install() || $this->initialised) {return;}$this->id = 'secondary_navigation';$context = $this->context;$this->headertitle = get_string('menu');$defaultmoremenunodes = [];$maxdisplayednodes = self::MAX_DISPLAYED_NAV_NODES;switch ($context->contextlevel) {case CONTEXT_COURSE:$this->headertitle = get_string('courseheader');if ($this->page->course->format === 'singleactivity') {$this->load_single_activity_course_navigation();} else {$this->load_course_navigation();$defaultmoremenunodes = $this->get_default_course_more_menu_nodes();}break;case CONTEXT_MODULE:$this->headertitle = get_string('activityheader');if ($this->page->course->format === 'singleactivity') {$this->load_single_activity_course_navigation();} else {$this->load_module_navigation($this->page->settingsnav);$defaultmoremenunodes = $this->get_default_module_more_menu_nodes();}break;case CONTEXT_COURSECAT:$this->headertitle = get_string('categoryheader');$this->load_category_navigation();$defaultmoremenunodes = $this->get_default_category_more_menu_nodes();break;case CONTEXT_SYSTEM:$this->headertitle = get_string('homeheader');$this->load_admin_navigation();// If the site administration navigation was generated after load_admin_navigation().if ($this->has_children()) {// Do not explicitly limit the number of navigation nodes displayed in the site administration// navigation menu.$maxdisplayednodes = null;}$defaultmoremenunodes = $this->get_default_admin_more_menu_nodes();break;}$this->remove_unwanted_nodes($this);// Don't need to show anything if only the view node is available. Remove it.if ($this->children->count() == 1) {$this->children->remove('modulepage');}// Force certain navigation nodes to be displayed in the "more" menu.$this->force_nodes_into_more_menu($defaultmoremenunodes, $maxdisplayednodes);// Search and set the active node.$this->scan_for_active_node($this);$this->initialised = true;}/*** Returns a node with the action being from the first found child node that has an action (Recursive).** @param navigation_node $node The part of the node tree we are checking.* @param navigation_node $basenode The very first node to be used for the return.* @return navigation_node|null*/protected function get_node_with_first_action(navigation_node $node, navigation_node $basenode): ?navigation_node {$newnode = null;if (!$node->has_children()) {return null;}// Find the first child with an action and update the main node.foreach ($node->children as $child) {if ($child->has_action()) {$newnode = $basenode;$newnode->action = $child->action;return $newnode;}}if (is_null($newnode)) {// Check for children and go again.foreach ($node->children as $child) {if ($child->has_children()) {$newnode = $this->get_node_with_first_action($child, $basenode);if (!is_null($newnode)) {return $newnode;}}}}return null;}/*** Some nodes are containers only with no action. If this container has an action then nothing is done. If it does not have* an action then a search is done through the children looking for the first node that has an action. This action is then given* to the parent node that is initially provided as a parameter.** @param navigation_node $node The navigation node that we want to ensure has an action tied to it.* @return navigation_node The node intact with an action to use.*/protected function get_first_action_for_node(navigation_node $node): ?navigation_node {// If the node does not have children and has no action then no further processing is needed.$newnode = null;if ($node->has_children() && !$node->has_action()) {// We want to find the first child with an action.// We want to check all children on this level before going further down.// Note that new node gets changed here.$newnode = $this->get_node_with_first_action($node, $node);} else if ($node->has_action()) {$newnode = $node;}return $newnode;}/*** Recursive call to add all custom navigation nodes to secondary** @param navigation_node $node The node which should be added to secondary* @param navigation_node $basenode The original parent node* @param navigation_node|null $root The parent node nodes are to be added/removed to.* @param bool $forceadd Whether or not to bypass the external action check and force add all nodes*/protected function add_external_nodes_to_secondary(navigation_node $node, navigation_node $basenode,?navigation_node $root = null, bool $forceadd = false) {$root = $root ?? $this;// Add the first node.if ($node->has_action() && !$this->get($node->key)) {$root->add_node(clone $node);}// If the node has an external action add all children to the secondary navigation.if (!$node->has_internal_action() || $forceadd) {if ($node->has_children()) {foreach ($node->children as $child) {if ($child->has_children()) {$this->add_external_nodes_to_secondary($child, $basenode, $root, true);} else if ($child->has_action() && !$this->get($child->key)) {// Check whether the basenode matches a child's url.// This would have happened in get_first_action_for_node.// In these cases, we prefer the specific child content.if ($basenode->has_action() && $basenode->action()->compare($child->action())) {$root->children->remove($basenode->key, $basenode->type);}$root->add_node(clone $child);}}}}}/*** Returns a list of all expected nodes in the course administration.** @return array An array of keys for navigation nodes in the course administration.*/protected function get_expected_course_admin_nodes(): array {$expectednodes = [];foreach ($this->get_default_course_mapping()['settings'] as $value) {foreach ($value as $nodekey => $notused) {$expectednodes[] = $nodekey;}}foreach ($this->get_default_course_mapping()['navigation'] as $value) {foreach ($value as $nodekey => $notused) {$expectednodes[] = $nodekey;}}$othernodes = ['users', 'gradeadmin', 'coursereports', 'coursebadges'];$leftovercourseadminnodes = ['backup', 'restore', 'import', 'copy', 'reset'];$expectednodes = array_merge($expectednodes, $othernodes);$expectednodes = array_merge($expectednodes, $leftovercourseadminnodes);return $expectednodes;}/*** Load the course secondary navigation. Since we are sourcing all the info from existing objects that already do* the relevant checks, we don't do it again here.** @param navigation_node|null $rootnode The node where the course navigation nodes should be added into as children.* If not explicitly defined, the nodes will be added to the secondary root* node by default.*/protected function load_course_navigation(?navigation_node $rootnode = null): void {global $SITE;$rootnode = $rootnode ?? $this;$course = $this->page->course;// Initialise the main navigation and settings nav.// It is important that this is done before we try anything.$settingsnav = $this->page->settingsnav;$navigation = $this->page->navigation;if ($course->id == $SITE->id) {$firstnodeidentifier = get_string('home'); // The first node in the site course nav is called 'Home'.$frontpage = $settingsnav->get('frontpage'); // The site course nodes are children of a dedicated 'frontpage' node.$settingsnav = $frontpage ?: $settingsnav;$courseadminnode = $frontpage ?: null; // Custom nodes for the site course are also children of the 'frontpage' node.} else {$firstnodeidentifier = get_string('course'); // Regular courses have a first node called 'Course'.$courseadminnode = $settingsnav->get('courseadmin'); // Custom nodes for regular courses live under 'courseadmin'.}// Add the known nodes from settings and navigation.$nodes = $this->get_default_course_mapping();$nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings'] ?? []);$nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation'] ?? []);$this->add_ordered_nodes($nodesordered, $rootnode);// Try to get any custom nodes defined by plugins, which may include containers.if ($courseadminnode) {$expectedcourseadmin = $this->get_expected_course_admin_nodes();foreach ($courseadminnode->children as $other) {if (array_search($other->key, $expectedcourseadmin, true) === false) {$othernode = $this->get_first_action_for_node($other);$recursivenode = $othernode && !$rootnode->get($othernode->key) ? $othernode : $other;// Get the first node and check whether it's been added already.// Also check if the first node is an external link. If it is, add all children.$this->add_external_nodes_to_secondary($recursivenode, $recursivenode, $rootnode);}}}// Add the respective first node, provided there are other nodes included.if (!empty($nodekeys = $rootnode->children->get_key_list())) {$rootnode->add_node(navigation_node::create($firstnodeidentifier, new \moodle_url('/course/view.php', ['id' => $course->id]),self::TYPE_COURSE, null, 'coursehome'), reset($nodekeys));}}/*** Gets the overflow navigation nodes for the course administration category.** @param navigation_node|null $rootnode The node from where the course overflow nodes should be obtained.* If not explicitly defined, the nodes will be obtained from the secondary root* node by default.* @return navigation_node The course overflow nodes.*/protected function get_course_overflow_nodes(?navigation_node $rootnode = null): ?navigation_node {global $SITE;$rootnode = $rootnode ?? $this;// This gets called twice on some pages, and so trying to create this navigation node twice results in no children being// present the second time this is called.if (isset($this->courseoverflownode)) {return $this->courseoverflownode;}// Start with getting the base node for the front page or the course.$node = null;if ($this->page->course->id == $SITE->id) {$node = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);} else {$node = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);}$coursesettings = $node ? $node->get_children_key_list() : [];$thissettings = $rootnode->get_children_key_list();$diff = array_diff($coursesettings, $thissettings);// Remove our specific created elements (user - participants, badges - coursebadges, grades - gradebooksetup,// grades - outcomes).$shortdiff = array_filter($diff, function($value) {return !($value == 'users' || $value == 'coursebadges' || $value == 'gradebooksetup' ||$value == 'outcomes');});// Permissions may be in play here that ultimately will show no overflow.if (empty($shortdiff)) {return null;}$firstitem = array_shift($shortdiff);$navnode = $node->get($firstitem);foreach ($shortdiff as $key) {$courseadminnodes = $node->get($key);if ($courseadminnodes) {if ($courseadminnodes->parent->key == $node->key) {$navnode->add_node($courseadminnodes);}}}$this->courseoverflownode = $navnode;return $navnode;}/*** Recursively looks for a match to the current page url.** @param navigation_node $node The node to look through.* @return navigation_node|null The node that matches this page's url.*/protected function nodes_match_current_url(navigation_node $node): ?navigation_node {$pagenode = $this->page->url;if ($node->has_action()) {// Check this node first.if ($node->action->compare($pagenode)) {return $node;}}if ($node->has_children()) {foreach ($node->children as $child) {$result = $this->nodes_match_current_url($child);if ($result) {return $result;}}}return null;}/*** Recursively search a node and its children for a node matching the key string $key.** @param navigation_node $node the navigation node to check.* @param string $key the key of the node to match.* @return navigation_node|null node if found, otherwise null.*/protected function node_matches_key_string(navigation_node $node, string $key): ?navigation_node {if ($node->has_action()) {// Check this node first.if ($node->key == $key) {return $node;}}if ($node->has_children()) {foreach ($node->children as $child) {$result = $this->node_matches_key_string($child, $key);if ($result) {return $result;}}}return null;}/*** Force a specific node in the 'coursereuse' course overflow to be selected, based on the provided node key.** Normally, the selected node is determined by matching the page URL to the node URL. E.g. The page 'backup/restorefile.php'* will match the "Restore" node which has a registered URL of 'backup/restorefile.php' because the URLs match.** This method allows a page to choose a specific node to match, which is useful in cases where the page knows its URL won't* match the node it needs to reside under. I.e. this permits several pages to 'share' the same overflow node. When the page* knows the PAGE->url won't match the node URL, the page can simply say "I want to match the 'XXX' node".** E.g.* - The $PAGE->url is 'backup/restore.php' (this page is used during restores but isn't the main landing page for a restore)* - The 'Restore' node in the overflow has a key of 'restore' and will only match 'backup/restorefile.php' by default (the* main restore landing page).* - The backup/restore.php page calls:* $PAGE->secondarynav->set_overflow_selected_node(new moodle_url('restore');* and when the page is loaded, the 'Restore' node be presented as the selected node.** @param string $nodekey The string key of the overflow node to match.*/public function set_overflow_selected_node(string $nodekey): void {$this->overflowselected = $nodekey;}/*** Returns a url_select object with overflow navigation nodes.* This looks to see if the current page is within the course administration, or some other page that requires an overflow* select object.** @return url_select|null The overflow menu data.*/public function get_overflow_menu_data(): ?url_select {if (!$this->page->get_navigation_overflow_state()) {return null;}$issingleactivitycourse = $this->page->course->format === 'singleactivity';$rootnode = $issingleactivitycourse ? $this->find('course', self::TYPE_COURSE) : $this;$activenode = $this->find_active_node();$incourseadmin = false;if (!$activenode || ($issingleactivitycourse && $activenode->key === 'course')) {// Could be in the course admin section.$courseadmin = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);if (!$courseadmin) {return null;}$activenode = $courseadmin->find_active_node();if (!$activenode) {return null;}$incourseadmin = true;}if ($activenode->key === 'coursereuse' || $incourseadmin) {$courseoverflownode = $this->get_course_overflow_nodes($rootnode);if (is_null($courseoverflownode)) {return null;}if ($incourseadmin) {// Validate whether the active node is part of the expected course overflow nodes.if (($activenode->key !== $courseoverflownode->key) &&!$courseoverflownode->find($activenode->key, $activenode->type)) {return null;}}$menuarray = static::create_menu_element([$courseoverflownode]);if ($activenode->key != 'coursereuse') {$inmenu = false;foreach ($menuarray as $key => $value) {if ($this->page->url->out(false) == $key) {$inmenu = true;}}if (!$inmenu) {return null;}}// If the page has explicitly set the overflow node it would like selected, find and use that node.if ($this->overflowselected) {$selectedoverflownode = $this->node_matches_key_string($courseoverflownode, $this->overflowselected);$selectedoverflownodeurl = $selectedoverflownode ? $selectedoverflownode->action->out(false) : null;}$menuselect = new url_select($menuarray, $selectedoverflownodeurl ?? $this->page->url, null);$menuselect->set_label(get_string('browsecourseadminindex', 'course'), ['class' => 'sr-only']);return $menuselect;} else {return $this->get_other_overflow_menu_data($activenode);}}/*** Gets overflow menu data for third party plugin settings.** @param navigation_node $activenode The node to gather the children for to put into the overflow menu.* @return url_select|null The overflow menu in a url_select object.*/protected function get_other_overflow_menu_data(navigation_node $activenode): ?url_select {if (!$activenode->has_action()) {return null;}if (!$activenode->has_children()) {return null;}// If the setting is extending the course navigation then the page being redirected to should be in the course context.// It was decided on the issue that put this code here that plugins that extend the course navigation should have the pages// that are redirected to, be in the course context or module context depending on which callback was used.// Third part plugins were checked to see if any existing plugins had settings in a system context and none were found.// The request of third party developers is to keep their settings within the specified context.if ($this->page->context->contextlevel != CONTEXT_COURSE&& $this->page->context->contextlevel != CONTEXT_MODULE&& $this->page->context->contextlevel != CONTEXT_COURSECAT) {return null;}// These areas have their own code to retrieve added plugin navigation nodes.if ($activenode->key == 'coursehome' || $activenode->key == 'questionbank' || $activenode->key == 'coursereports') {return null;}$menunode = $this->page->settingsnav->find($activenode->key, null);if (!$menunode instanceof navigation_node) {return null;}// Loop through all children and try and find a match to the current url.$matchednode = $this->nodes_match_current_url($menunode);if (is_null($matchednode)) {return null;}if (!isset($menunode) || !$menunode->has_children()) {return null;}$selectdata = static::create_menu_element([$menunode], false);$urlselect = new url_select($selectdata, $matchednode->action->out(false), null);$urlselect->set_label(get_string('browsesettingindex', 'course'), ['class' => 'sr-only']);return $urlselect;}/*** Get the module's secondary navigation. This is based on settings_nav and would include plugin nodes added via* '_extend_settings_navigation'.* It populates the tree based on the nav mockup** If nodes change, we will have to explicitly call the callback again.** @param settings_navigation $settingsnav The settings navigation object related to the module page* @param navigation_node|null $rootnode The node where the module navigation nodes should be added into as children.* If not explicitly defined, the nodes will be added to the secondary root* node by default.*/protected function load_module_navigation(settings_navigation $settingsnav, ?navigation_node $rootnode = null): void {$rootnode = $rootnode ?? $this;$mainnode = $settingsnav->find('modulesettings', self::TYPE_SETTING);$nodes = $this->get_default_module_mapping();if ($mainnode) {$url = new \moodle_url('/mod/' . $settingsnav->get_page()->activityname . '/view.php',['id' => $settingsnav->get_page()->cm->id]);$setactive = $url->compare($settingsnav->get_page()->url, URL_MATCH_BASE);$node = $rootnode->add(get_string('modulename', $settingsnav->get_page()->activityname), $url,null, null, 'modulepage');if ($setactive) {$node->make_active();}// Add the initial nodes.$nodesordered = $this->get_leaf_nodes($mainnode, $nodes);$this->add_ordered_nodes($nodesordered, $rootnode);// We have finished inserting the initial structure.// Populate the menu with the rest of the nodes available.$this->load_remaining_nodes($mainnode, $nodes, $rootnode);}}/*** Load the course category navigation.*/protected function load_category_navigation(): void {$settingsnav = $this->page->settingsnav;$mainnode = $settingsnav->find('categorysettings', self::TYPE_CONTAINER);$nodes = $this->get_default_category_mapping();if ($mainnode) {$url = new \moodle_url('/course/index.php', ['categoryid' => $this->context->instanceid]);$this->add(get_string('category'), $url, self::TYPE_CONTAINER, null, 'categorymain');// Add the initial nodes.$nodesordered = $this->get_leaf_nodes($mainnode, $nodes);$this->add_ordered_nodes($nodesordered);// We have finished inserting the initial structure.// Populate the menu with the rest of the nodes available.$this->load_remaining_nodes($mainnode, $nodes);}}/*** Load the site admin navigation*/protected function load_admin_navigation(): void {global $PAGE, $SITE;$settingsnav = $this->page->settingsnav;$node = $settingsnav->find('root', self::TYPE_SITE_ADMIN);// We need to know if we are on the main site admin search page. Here the navigation between tabs are done via// anchors and page reload doesn't happen. On every nested admin settings page, the secondary nav needs to// exist as links with anchors appended in order to redirect back to the admin search page and the corresponding// tab. Note this value refers to being present on the page itself, before a search has been performed.$isadminsearchpage = $PAGE->url->compare(new \moodle_url('/admin/search.php', ['query' => '']), URL_MATCH_PARAMS);if ($node) {$siteadminnode = $this->add(get_string('general'), "#link$node->key", null, null, 'siteadminnode');if ($isadminsearchpage) {$siteadminnode->action = false;$siteadminnode->tab = "#link$node->key";} else {$siteadminnode->action = new \moodle_url("/admin/search.php", [], "link$node->key");}foreach ($node->children as $child) {if ($child->display && !$child->is_short_branch()) {// Mimic the current boost behaviour and pass down anchors for the tabs.if ($isadminsearchpage) {$child->action = false;$child->tab = "#link$child->key";} else {$child->action = new \moodle_url("/admin/search.php", [], "link$child->key");}$this->add_node(clone $child);} else {$siteadminnode->add_node(clone $child);}}}}/*** Adds the indexed nodes to the current view or a given node. The key should indicate it's position in the tree.* Any sub nodes needs to be numbered appropriately, e.g. 3.1 would make the identified node be listed under #3 node.** @param array $nodes An array of navigation nodes to be added.* @param navigation_node|null $rootnode The node where the nodes should be added into as children. If not explicitly* defined, the nodes will be added to the secondary root node by default.*/protected function add_ordered_nodes(array $nodes, ?navigation_node $rootnode = null): void {$rootnode = $rootnode ?? $this;ksort($nodes);foreach ($nodes as $key => $node) {// If the key is a string then we are assuming this is a nested element.if (is_string($key)) {$parentnode = $nodes[floor($key)] ?? null;if ($parentnode) {$parentnode->add_node(clone $node);}} else {$rootnode->add_node(clone $node);}}}/*** Find the remaining nodes that need to be loaded into secondary based on the current context or a given node.** @param navigation_node $completenode The original node that we are sourcing information from* @param array $nodesmap The map used to populate secondary nav in the given context* @param navigation_node|null $rootnode The node where the remaining nodes should be added into as children. If not* explicitly defined, the nodes will be added to the secondary root node by* default.*/protected function load_remaining_nodes(navigation_node $completenode, array $nodesmap,?navigation_node $rootnode = null): void {$flattenednodes = [];$rootnode = $rootnode ?? $this;foreach ($nodesmap as $nodecontainer) {$flattenednodes = array_merge(array_keys($nodecontainer), $flattenednodes);}$populatedkeys = $this->get_children_key_list();$existingkeys = $completenode->get_children_key_list();$leftover = array_diff($existingkeys, $populatedkeys);foreach ($leftover as $key) {if (!in_array($key, $flattenednodes, true) && $leftovernode = $completenode->get($key)) {// Check for nodes with children and potentially no action to direct to.if ($leftovernode->has_children()) {$leftovernode = $this->get_first_action_for_node($leftovernode);}// We have found the first node with an action.if ($leftovernode) {$this->add_external_nodes_to_secondary($leftovernode, $leftovernode, $rootnode);}}}}/*** Force certain secondary navigation nodes to be displayed in the "more" menu.** @param array $defaultmoremenunodes Array with navigation node keys of the pre-defined nodes that* should be added into the "more" menu by default* @param int|null $maxdisplayednodes The maximum limit of navigation nodes displayed in the secondary navigation*/protected function force_nodes_into_more_menu(array $defaultmoremenunodes = [], ?int $maxdisplayednodes = null) {// Counter of the navigation nodes that are initially displayed in the secondary nav// (excludes the nodes from the "more" menu).$displayednodescount = 0;foreach ($this->children as $child) {// Skip if the navigation node has been already forced into the "more" menu.if ($child->forceintomoremenu) {continue;}// If the navigation node is in the pre-defined list of nodes that should be added by default in the// "more" menu or the maximum limit of displayed navigation nodes has been reached (if defined).if (in_array($child->key, $defaultmoremenunodes) ||(!is_null($maxdisplayednodes) && $displayednodescount >= $maxdisplayednodes)) {// Force the node and its children into the "more" menu.$child->set_force_into_more_menu(true);continue;}$displayednodescount++;}}/*** Recursively remove navigation nodes that should not be displayed in the secondary navigation.** @param navigation_node $node The starting navigation node.*/protected function remove_unwanted_nodes(navigation_node $node) {foreach ($node->children as $child) {if (!$child->showinsecondarynavigation) {$child->remove();continue;}if (!empty($child->children)) {$this->remove_unwanted_nodes($child);}}}/*** Takes the given navigation nodes and searches for children and formats it all into an array in a format to be used by a* url_select element.** @param navigation_node[] $navigationnodes Navigation nodes to format into a menu.* @param bool $forceheadings Whether the returned array should be forced to use headings.* @return array|null A url select element for navigating through the navigation nodes.*/public static function create_menu_element(array $navigationnodes, bool $forceheadings = false): ?array {if (empty($navigationnodes)) {return null;}// If one item, do we put this into a url_select?if (count($navigationnodes) < 2) {// Check if there are children.$navnode = array_shift($navigationnodes);$menudata = [];if (!$navnode->has_children()) {// Just one item.if (!$navnode->has_action()) {return null;}$menudata[$navnode->action->out(false)] = static::format_node_text($navnode);} else {if (static::does_menu_need_headings($navnode) || $forceheadings) {// Let's do headings.$menudata = static::get_headings_nav_array($navnode);} else {// Simple flat nav.$menudata = static::get_flat_nav_array($navnode);}}return $menudata;} else {// We have more than one navigation node to handle. Put each node in it's own heading.$menudata = [];$titledata = [];foreach ($navigationnodes as $navigationnode) {if ($navigationnode->has_children()) {$menuarray = [];// Add a heading and flatten out everything else.if ($navigationnode->has_action()) {$menuarray[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =static::format_node_text($navigationnode);$menuarray[static::format_node_text($navigationnode)] += static::get_whole_tree_flat($navigationnode);} else {$menuarray[static::format_node_text($navigationnode)] = static::get_whole_tree_flat($navigationnode);}$titledata += $menuarray;} else {// Add with no heading.if (!$navigationnode->has_action()) {return null;}$menudata[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);}}$menudata += [$titledata];return $menudata;}}/*** Recursively goes through the provided navigation node and returns a flat version.** @param navigation_node $navigationnode The navigationnode.* @return array The whole tree flat.*/protected static function get_whole_tree_flat(navigation_node $navigationnode): array {$nodes = [];foreach ($navigationnode->children as $child) {if ($child->has_action()) {$nodes[$child->action->out()] = $child->text;}if ($child->has_children()) {$childnodes = static::get_whole_tree_flat($child);$nodes = array_merge($nodes, $childnodes);}}return $nodes;}/*** Checks to see if the provided navigation node has children and determines if we want headings for a url select element.** @param navigation_node $navigationnode The navigation node we are checking.* @return bool Whether we want headings or not.*/protected static function does_menu_need_headings(navigation_node $navigationnode): bool {if (!$navigationnode->has_children()) {return false;}foreach ($navigationnode->children as $child) {if ($child->has_children()) {return true;}}return false;}/*** Takes the navigation node and returns it in a flat fashion. This is not recursive.** @param navigation_node $navigationnode The navigation node that we want to format into an array in a flat structure.* @return array The flat navigation array.*/protected static function get_flat_nav_array(navigation_node $navigationnode): array {$menuarray = [];if ($navigationnode->has_action()) {$menuarray[$navigationnode->action->out(false)] = static::format_node_text($navigationnode);}foreach ($navigationnode->children as $child) {if ($child->has_action()) {$menuarray[$child->action->out(false)] = static::format_node_text($child);}}return $menuarray;}/*** For any navigation node that we have determined needs headings we return a more tree like array structure.** @param navigation_node $navigationnode The navigation node to use for the formatted array structure.* @return array The headings navigation array structure.*/protected static function get_headings_nav_array(navigation_node $navigationnode): array {$menublock = [];// We know that this single node has headings, so grab this for the first heading.$firstheading = [];if ($navigationnode->has_action()) {$firstheading[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] =static::format_node_text($navigationnode);$firstheading[static::format_node_text($navigationnode)] += static::get_more_child_nodes($navigationnode, $menublock);} else {$firstheading[static::format_node_text($navigationnode)] = static::get_more_child_nodes($navigationnode, $menublock);}return [$firstheading + $menublock];}/*** Recursively goes and gets all children nodes.** @param navigation_node $node The node to get the children of.* @param array $menublock Used to put all child nodes in its own container.* @return array The additional child nodes.*/protected static function get_more_child_nodes(navigation_node $node, array &$menublock): array {$nodes = [];foreach ($node->children as $child) {if (!$child->has_children()) {if (!$child->has_action()) {continue;}$nodes[$child->action->out(false)] = static::format_node_text($child);} else {$newarray = [];if ($child->has_action()) {$newarray[static::format_node_text($child)][$child->action->out(false)] = static::format_node_text($child);$newarray[static::format_node_text($child)] += static::get_more_child_nodes($child, $menublock);} else {$newarray[static::format_node_text($child)] = static::get_more_child_nodes($child, $menublock);}$menublock += $newarray;}}return $nodes;}/*** Returns the navigation node text in a string.** @param navigation_node $navigationnode The navigationnode to return the text string of.* @return string The navigation node text string.*/protected static function format_node_text(navigation_node $navigationnode): string {return (is_a($navigationnode->text, 'lang_string')) ? $navigationnode->text->out() : $navigationnode->text;}/*** Load the single activity course secondary navigation.*/protected function load_single_activity_course_navigation(): void {$page = $this->page;$course = $page->course;// Create 'Course' navigation node.$coursesecondarynode = navigation_node::create(get_string('course'), null, self::TYPE_COURSE, null, 'course');$this->load_course_navigation($coursesecondarynode);// Remove the unnecessary 'Course' child node generated in load_course_navigation().$coursehomenode = $coursesecondarynode->find('coursehome', self::TYPE_COURSE);if (!empty($coursehomenode)) {$coursehomenode->remove();}// Add the 'Course' node to the secondary navigation only if this node has children nodes.if (count($coursesecondarynode->children) > 0) {$this->add_node($coursesecondarynode);// Once all the items have been added to the 'Course' secondary navigation node, set the 'showchildreninsubmenu'// property to true. This is required to force the template to output these items within a dropdown menu.$coursesecondarynode->showchildreninsubmenu = true;}// Create 'Activity' navigation node.$activitysecondarynode = navigation_node::create(get_string('activity'), null, self::TYPE_ACTIVITY, null, 'activity');// We should display the module related navigation in the course context as well. Therefore, we need to// re-initialize the page object and manually set the course module to the one that it is currently visible in// the course in order to obtain the required module settings navigation.if ($page->context instanceof \context_course) {$this->page->set_secondary_active_tab($coursesecondarynode->key);// Get the currently used module in the single activity course.$module = current(array_filter(get_course_mods($course->id), function ($module) {return $module->visible == 1;}));// If the default module for the single course format has not been set yet, skip displaying the module// related navigation in the secondary navigation.if (!$module) {return;}$page = new \moodle_page();$page->set_cm($module, $course);$page->set_url(new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]));}$this->load_module_navigation($page->settingsnav, $activitysecondarynode);// Add the 'Activity' node to the secondary navigation only if this node has more that one child node.if (count($activitysecondarynode->children) > 1) {// Set the 'showchildreninsubmenu' property to true to later output the the module navigation items within// a dropdown menu.$activitysecondarynode->showchildreninsubmenu = true;$this->add_node($activitysecondarynode);if ($this->context instanceof \context_module) {$this->page->set_secondary_active_tab($activitysecondarynode->key);}} else { // Otherwise, add the 'View activity' node to the secondary navigation.$viewactivityurl = new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]);$this->add(get_string('modulename', $page->activityname), $viewactivityurl, null, null, 'modulepage');if ($this->context instanceof \context_module) {$this->page->set_secondary_active_tab('modulepage');}}}}