Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]
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. 13 // 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/>. 16 17 namespace core\navigation\views; 18 19 use navigation_node; 20 use url_select; 21 use settings_navigation; 22 23 /** 24 * Class secondary_navigation_view. 25 * 26 * The secondary navigation view is a stripped down tweaked version of the 27 * settings_navigation/navigation 28 * 29 * @package core 30 * @category navigation 31 * @copyright 2021 onwards Peter Dias 32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 */ 34 class secondary extends view { 35 /** @var string $headertitle The header for this particular menu*/ 36 public $headertitle; 37 38 /** @var int The maximum limit of navigation nodes displayed in the secondary navigation */ 39 const MAX_DISPLAYED_NAV_NODES = 5; 40 41 /** @var navigation_node The course overflow node. */ 42 protected $courseoverflownode = null; 43 44 /** @var string The key of the node to set as selected in the course overflow menu, if explicitly set by a page. */ 45 protected $overflowselected = null; 46 47 /** 48 * Defines the default structure for the secondary nav in a course context. 49 * 50 * In a course context, we are curating nodes from the settingsnav and navigation objects. 51 * The following mapping construct specifies which object we are fetching it from, the type of the node, the key 52 * and in what order we want the node - defined as per the mockups. 53 * 54 * @return array 55 */ 56 protected function get_default_course_mapping(): array { 57 $nodes = []; 58 $nodes['settings'] = [ 59 self::TYPE_CONTAINER => [ 60 'coursereports' => 3, 61 'questionbank' => 4, 62 ], 63 self::TYPE_SETTING => [ 64 'editsettings' => 0, 65 'review' => 1.1, 66 'manageinstances' => 1.2, 67 'groups' => 1.3, 68 'override' => 1.4, 69 'roles' => 1.5, 70 'permissions' => 1.6, 71 'otherusers' => 1.7, 72 'gradebooksetup' => 2.1, 73 'outcomes' => 2.2, 74 'coursecompletion' => 6, 75 'coursebadges' => 7.1, 76 'newbadge' => 7.2, 77 'filtermanagement' => 9, 78 'unenrolself' => 10, 79 'coursetags' => 11, 80 'download' => 12, 81 'contextlocking' => 13, 82 ], 83 ]; 84 $nodes['navigation'] = [ 85 self::TYPE_CONTAINER => [ 86 'participants' => 1, 87 ], 88 self::TYPE_SETTING => [ 89 'grades' => 2, 90 'badgesview' => 7, 91 'competencies' => 8, 92 'communication' => 14, 93 ], 94 self::TYPE_CUSTOM => [ 95 'contentbank' => 5, 96 'participants' => 1, // In site home, 'participants' is classified differently. 97 ], 98 ]; 99 100 return $nodes; 101 } 102 103 /** 104 * Defines the default structure for the secondary nav in a module context. 105 * 106 * In a module context, we are curating nodes from the settingsnav object. 107 * The following mapping construct specifies the type of the node, the key 108 * and in what order we want the node - defined as per the mockups. 109 * 110 * @return array 111 */ 112 protected function get_default_module_mapping(): array { 113 return [ 114 self::TYPE_SETTING => [ 115 'modedit' => 1, 116 "mod_{$this->page->activityname}_useroverrides" => 3, // Overrides are module specific. 117 "mod_{$this->page->activityname}_groupoverrides" => 4, 118 'roleassign' => 7.2, 119 'filtermanage' => 6, 120 'roleoverride' => 7, 121 'rolecheck' => 7.1, 122 'logreport' => 8, 123 'backup' => 9, 124 'restore' => 10, 125 'competencybreakdown' => 11, 126 'sendtomoodlenet' => 16, 127 ], 128 self::TYPE_CUSTOM => [ 129 'advgrading' => 2, 130 'contentbank' => 12, 131 ], 132 ]; 133 } 134 135 /** 136 * Defines the default structure for the secondary nav in a category context. 137 * 138 * In a category context, we are curating nodes from the settingsnav object. 139 * The following mapping construct specifies the type of the node, the key 140 * and in what order we want the node - defined as per the mockups. 141 * 142 * @return array 143 */ 144 protected function get_default_category_mapping(): array { 145 return [ 146 self::TYPE_SETTING => [ 147 'edit' => 1, 148 'permissions' => 2, 149 'roles' => 2.1, 150 'rolecheck' => 2.2, 151 ] 152 ]; 153 } 154 155 /** 156 * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default. 157 * 158 * @return array 159 */ 160 protected function get_default_category_more_menu_nodes(): array { 161 return ['addsubcat', 'roles', 'permissions', 'contentbank', 'cohort', 'filters', 'restorecourse']; 162 } 163 /** 164 * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default. 165 * 166 * @return array 167 */ 168 protected function get_default_course_more_menu_nodes(): array { 169 return []; 170 } 171 172 /** 173 * Define the keys of the module secondary nav nodes that should be forced into the "more" menu by default. 174 * 175 * @return array 176 */ 177 protected function get_default_module_more_menu_nodes(): array { 178 return ['roleoverride', 'rolecheck', 'logreport', 'roleassign', 'filtermanage', 'backup', 'restore', 179 'competencybreakdown', "mod_{$this->page->activityname}_useroverrides", 180 "mod_{$this->page->activityname}_groupoverrides"]; 181 } 182 183 /** 184 * Define the keys of the admin secondary nav nodes that should be forced into the "more" menu by default. 185 * 186 * @return array 187 */ 188 protected function get_default_admin_more_menu_nodes(): array { 189 return []; 190 } 191 192 /** 193 * Initialise the view based navigation based on the current context. 194 * 195 * As part of the initial restructure, the secondary nav is only considered for the following pages: 196 * 1 - Site admin settings 197 * 2 - Course page - Does not include front_page which has the same context. 198 * 3 - Module page 199 */ 200 public function initialise(): void { 201 global $SITE; 202 203 if (during_initial_install() || $this->initialised) { 204 return; 205 } 206 $this->id = 'secondary_navigation'; 207 $context = $this->context; 208 $this->headertitle = get_string('menu'); 209 $defaultmoremenunodes = []; 210 $maxdisplayednodes = self::MAX_DISPLAYED_NAV_NODES; 211 212 switch ($context->contextlevel) { 213 case CONTEXT_COURSE: 214 $this->headertitle = get_string('courseheader'); 215 if ($this->page->course->format === 'singleactivity') { 216 $this->load_single_activity_course_navigation(); 217 } else { 218 $this->load_course_navigation(); 219 $defaultmoremenunodes = $this->get_default_course_more_menu_nodes(); 220 } 221 break; 222 case CONTEXT_MODULE: 223 $this->headertitle = get_string('activityheader'); 224 if ($this->page->course->format === 'singleactivity') { 225 $this->load_single_activity_course_navigation(); 226 } else { 227 $this->load_module_navigation($this->page->settingsnav); 228 $defaultmoremenunodes = $this->get_default_module_more_menu_nodes(); 229 } 230 break; 231 case CONTEXT_COURSECAT: 232 $this->headertitle = get_string('categoryheader'); 233 $this->load_category_navigation(); 234 $defaultmoremenunodes = $this->get_default_category_more_menu_nodes(); 235 break; 236 case CONTEXT_SYSTEM: 237 $this->headertitle = get_string('homeheader'); 238 $this->load_admin_navigation(); 239 // If the site administration navigation was generated after load_admin_navigation(). 240 if ($this->has_children()) { 241 // Do not explicitly limit the number of navigation nodes displayed in the site administration 242 // navigation menu. 243 $maxdisplayednodes = null; 244 } 245 $defaultmoremenunodes = $this->get_default_admin_more_menu_nodes(); 246 break; 247 } 248 249 $this->remove_unwanted_nodes($this); 250 251 // Don't need to show anything if only the view node is available. Remove it. 252 if ($this->children->count() == 1) { 253 $this->children->remove('modulepage'); 254 } 255 // Force certain navigation nodes to be displayed in the "more" menu. 256 $this->force_nodes_into_more_menu($defaultmoremenunodes, $maxdisplayednodes); 257 // Search and set the active node. 258 $this->scan_for_active_node($this); 259 $this->initialised = true; 260 } 261 262 /** 263 * Returns a node with the action being from the first found child node that has an action (Recursive). 264 * 265 * @param navigation_node $node The part of the node tree we are checking. 266 * @param navigation_node $basenode The very first node to be used for the return. 267 * @return navigation_node|null 268 */ 269 protected function get_node_with_first_action(navigation_node $node, navigation_node $basenode): ?navigation_node { 270 $newnode = null; 271 if (!$node->has_children()) { 272 return null; 273 } 274 275 // Find the first child with an action and update the main node. 276 foreach ($node->children as $child) { 277 if ($child->has_action()) { 278 $newnode = $basenode; 279 $newnode->action = $child->action; 280 return $newnode; 281 } 282 } 283 if (is_null($newnode)) { 284 // Check for children and go again. 285 foreach ($node->children as $child) { 286 if ($child->has_children()) { 287 $newnode = $this->get_node_with_first_action($child, $basenode); 288 289 if (!is_null($newnode)) { 290 return $newnode; 291 } 292 } 293 } 294 } 295 return null; 296 } 297 298 /** 299 * Some nodes are containers only with no action. If this container has an action then nothing is done. If it does not have 300 * an action then a search is done through the children looking for the first node that has an action. This action is then given 301 * to the parent node that is initially provided as a parameter. 302 * 303 * @param navigation_node $node The navigation node that we want to ensure has an action tied to it. 304 * @return navigation_node The node intact with an action to use. 305 */ 306 protected function get_first_action_for_node(navigation_node $node): ?navigation_node { 307 // If the node does not have children and has no action then no further processing is needed. 308 $newnode = null; 309 if ($node->has_children() && !$node->has_action()) { 310 // We want to find the first child with an action. 311 // We want to check all children on this level before going further down. 312 // Note that new node gets changed here. 313 $newnode = $this->get_node_with_first_action($node, $node); 314 } else if ($node->has_action()) { 315 $newnode = $node; 316 } 317 return $newnode; 318 } 319 320 /** 321 * Recursive call to add all custom navigation nodes to secondary 322 * 323 * @param navigation_node $node The node which should be added to secondary 324 * @param navigation_node $basenode The original parent node 325 * @param navigation_node|null $root The parent node nodes are to be added/removed to. 326 * @param bool $forceadd Whether or not to bypass the external action check and force add all nodes 327 */ 328 protected function add_external_nodes_to_secondary(navigation_node $node, navigation_node $basenode, 329 ?navigation_node $root = null, bool $forceadd = false) { 330 $root = $root ?? $this; 331 // Add the first node. 332 if ($node->has_action() && !$this->get($node->key)) { 333 $root->add_node(clone $node); 334 } 335 336 // If the node has an external action add all children to the secondary navigation. 337 if (!$node->has_internal_action() || $forceadd) { 338 if ($node->has_children()) { 339 foreach ($node->children as $child) { 340 if ($child->has_children()) { 341 $this->add_external_nodes_to_secondary($child, $basenode, $root, true); 342 } else if ($child->has_action() && !$this->get($child->key)) { 343 // Check whether the basenode matches a child's url. 344 // This would have happened in get_first_action_for_node. 345 // In these cases, we prefer the specific child content. 346 if ($basenode->has_action() && $basenode->action()->compare($child->action())) { 347 $root->children->remove($basenode->key, $basenode->type); 348 } 349 $root->add_node(clone $child); 350 } 351 } 352 } 353 } 354 } 355 356 /** 357 * Returns a list of all expected nodes in the course administration. 358 * 359 * @return array An array of keys for navigation nodes in the course administration. 360 */ 361 protected function get_expected_course_admin_nodes(): array { 362 $expectednodes = []; 363 foreach ($this->get_default_course_mapping()['settings'] as $value) { 364 foreach ($value as $nodekey => $notused) { 365 $expectednodes[] = $nodekey; 366 } 367 } 368 foreach ($this->get_default_course_mapping()['navigation'] as $value) { 369 foreach ($value as $nodekey => $notused) { 370 $expectednodes[] = $nodekey; 371 } 372 } 373 $othernodes = ['users', 'gradeadmin', 'coursereports', 'coursebadges']; 374 $leftovercourseadminnodes = ['backup', 'restore', 'import', 'copy', 'reset']; 375 $expectednodes = array_merge($expectednodes, $othernodes); 376 $expectednodes = array_merge($expectednodes, $leftovercourseadminnodes); 377 return $expectednodes; 378 } 379 380 /** 381 * Load the course secondary navigation. Since we are sourcing all the info from existing objects that already do 382 * the relevant checks, we don't do it again here. 383 * 384 * @param navigation_node|null $rootnode The node where the course navigation nodes should be added into as children. 385 * If not explicitly defined, the nodes will be added to the secondary root 386 * node by default. 387 */ 388 protected function load_course_navigation(?navigation_node $rootnode = null): void { 389 global $SITE; 390 391 $rootnode = $rootnode ?? $this; 392 $course = $this->page->course; 393 // Initialise the main navigation and settings nav. 394 // It is important that this is done before we try anything. 395 $settingsnav = $this->page->settingsnav; 396 $navigation = $this->page->navigation; 397 398 if ($course->id == $SITE->id) { 399 $firstnodeidentifier = get_string('home'); // The first node in the site course nav is called 'Home'. 400 $frontpage = $settingsnav->get('frontpage'); // The site course nodes are children of a dedicated 'frontpage' node. 401 $settingsnav = $frontpage ?: $settingsnav; 402 $courseadminnode = $frontpage ?: null; // Custom nodes for the site course are also children of the 'frontpage' node. 403 } else { 404 $firstnodeidentifier = get_string('course'); // Regular courses have a first node called 'Course'. 405 $courseadminnode = $settingsnav->get('courseadmin'); // Custom nodes for regular courses live under 'courseadmin'. 406 } 407 408 // Add the known nodes from settings and navigation. 409 $nodes = $this->get_default_course_mapping(); 410 $nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings'] ?? []); 411 $nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation'] ?? []); 412 $this->add_ordered_nodes($nodesordered, $rootnode); 413 414 // Try to get any custom nodes defined by plugins, which may include containers. 415 if ($courseadminnode) { 416 $expectedcourseadmin = $this->get_expected_course_admin_nodes(); 417 foreach ($courseadminnode->children as $other) { 418 if (array_search($other->key, $expectedcourseadmin, true) === false) { 419 $othernode = $this->get_first_action_for_node($other); 420 $recursivenode = $othernode && !$rootnode->get($othernode->key) ? $othernode : $other; 421 // Get the first node and check whether it's been added already. 422 // Also check if the first node is an external link. If it is, add all children. 423 $this->add_external_nodes_to_secondary($recursivenode, $recursivenode, $rootnode); 424 } 425 } 426 } 427 428 // Move some nodes into a 'course reuse' node. 429 $overflownode = $this->get_course_overflow_nodes($rootnode); 430 if (!is_null($overflownode)) { 431 $actionnode = $this->get_first_action_for_node($overflownode); 432 if ($actionnode) { 433 // All additional nodes will be available under the 'Course reuse' page. 434 $text = get_string('coursereuse'); 435 $rootnode->add($text, $actionnode->action, navigation_node::TYPE_COURSE, null, 'coursereuse', 436 new \pix_icon('t/edit', $text)); 437 } 438 } 439 440 // Add the respective first node, provided there are other nodes included. 441 if (!empty($nodekeys = $rootnode->children->get_key_list())) { 442 $rootnode->add_node( 443 navigation_node::create($firstnodeidentifier, new \moodle_url('/course/view.php', ['id' => $course->id]), 444 self::TYPE_COURSE, null, 'coursehome'), reset($nodekeys) 445 ); 446 } 447 } 448 449 /** 450 * Gets the overflow navigation nodes for the course administration category. 451 * 452 * @param navigation_node|null $rootnode The node from where the course overflow nodes should be obtained. 453 * If not explicitly defined, the nodes will be obtained from the secondary root 454 * node by default. 455 * @return navigation_node The course overflow nodes. 456 */ 457 protected function get_course_overflow_nodes(?navigation_node $rootnode = null): ?navigation_node { 458 global $SITE; 459 460 $rootnode = $rootnode ?? $this; 461 // This gets called twice on some pages, and so trying to create this navigation node twice results in no children being 462 // present the second time this is called. 463 if (isset($this->courseoverflownode)) { 464 return $this->courseoverflownode; 465 } 466 467 // Start with getting the base node for the front page or the course. 468 $node = null; 469 if ($this->page->course->id == $SITE->id) { 470 $node = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING); 471 } else { 472 $node = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE); 473 } 474 $coursesettings = $node ? $node->get_children_key_list() : []; 475 $thissettings = $rootnode->get_children_key_list(); 476 $diff = array_diff($coursesettings, $thissettings); 477 478 // Remove our specific created elements (user - participants, badges - coursebadges, grades - gradebooksetup, 479 // grades - outcomes). 480 $shortdiff = array_filter($diff, function($value) { 481 return !($value == 'users' || $value == 'coursebadges' || $value == 'gradebooksetup' || 482 $value == 'outcomes'); 483 }); 484 485 // Permissions may be in play here that ultimately will show no overflow. 486 if (empty($shortdiff)) { 487 return null; 488 } 489 490 $firstitem = array_shift($shortdiff); 491 $navnode = $node->get($firstitem); 492 foreach ($shortdiff as $key) { 493 $courseadminnodes = $node->get($key); 494 if ($courseadminnodes) { 495 if ($courseadminnodes->parent->key == $node->key) { 496 $navnode->add_node($courseadminnodes); 497 } 498 } 499 } 500 $this->courseoverflownode = $navnode; 501 return $navnode; 502 503 } 504 505 /** 506 * Recursively looks for a match to the current page url. 507 * 508 * @param navigation_node $node The node to look through. 509 * @return navigation_node|null The node that matches this page's url. 510 */ 511 protected function nodes_match_current_url(navigation_node $node): ?navigation_node { 512 $pagenode = $this->page->url; 513 if ($node->has_action()) { 514 // Check this node first. 515 if ($node->action->compare($pagenode)) { 516 return $node; 517 } 518 } 519 if ($node->has_children()) { 520 foreach ($node->children as $child) { 521 $result = $this->nodes_match_current_url($child); 522 if ($result) { 523 return $result; 524 } 525 } 526 } 527 return null; 528 } 529 530 /** 531 * Recursively search a node and its children for a node matching the key string $key. 532 * 533 * @param navigation_node $node the navigation node to check. 534 * @param string $key the key of the node to match. 535 * @return navigation_node|null node if found, otherwise null. 536 */ 537 protected function node_matches_key_string(navigation_node $node, string $key): ?navigation_node { 538 if ($node->has_action()) { 539 // Check this node first. 540 if ($node->key == $key) { 541 return $node; 542 } 543 } 544 if ($node->has_children()) { 545 foreach ($node->children as $child) { 546 $result = $this->node_matches_key_string($child, $key); 547 if ($result) { 548 return $result; 549 } 550 } 551 } 552 return null; 553 } 554 555 /** 556 * Force a specific node in the 'coursereuse' course overflow to be selected, based on the provided node key. 557 * 558 * Normally, the selected node is determined by matching the page URL to the node URL. E.g. The page 'backup/restorefile.php' 559 * will match the "Restore" node which has a registered URL of 'backup/restorefile.php' because the URLs match. 560 * 561 * 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 562 * match the node it needs to reside under. I.e. this permits several pages to 'share' the same overflow node. When the page 563 * knows the PAGE->url won't match the node URL, the page can simply say "I want to match the 'XXX' node". 564 * 565 * E.g. 566 * - The $PAGE->url is 'backup/restore.php' (this page is used during restores but isn't the main landing page for a restore) 567 * - The 'Restore' node in the overflow has a key of 'restore' and will only match 'backup/restorefile.php' by default (the 568 * main restore landing page). 569 * - The backup/restore.php page calls: 570 * $PAGE->secondarynav->set_overflow_selected_node(new moodle_url('restore'); 571 * and when the page is loaded, the 'Restore' node be presented as the selected node. 572 * 573 * @param string $nodekey The string key of the overflow node to match. 574 */ 575 public function set_overflow_selected_node(string $nodekey): void { 576 $this->overflowselected = $nodekey; 577 } 578 579 /** 580 * Returns a url_select object with overflow navigation nodes. 581 * This looks to see if the current page is within the course administration, or some other page that requires an overflow 582 * select object. 583 * 584 * @return url_select|null The overflow menu data. 585 */ 586 public function get_overflow_menu_data(): ?url_select { 587 588 if (!$this->page->get_navigation_overflow_state()) { 589 return null; 590 } 591 592 $issingleactivitycourse = $this->page->course->format === 'singleactivity'; 593 $rootnode = $issingleactivitycourse ? $this->find('course', self::TYPE_COURSE) : $this; 594 $activenode = $this->find_active_node(); 595 $incourseadmin = false; 596 597 if (!$activenode || ($issingleactivitycourse && $activenode->key === 'course')) { 598 // Could be in the course admin section. 599 $courseadmin = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE); 600 if (!$courseadmin) { 601 return null; 602 } 603 604 $activenode = $courseadmin->find_active_node(); 605 if (!$activenode) { 606 return null; 607 } 608 $incourseadmin = true; 609 } 610 611 if ($activenode->key === 'coursereuse' || $incourseadmin) { 612 $courseoverflownode = $this->get_course_overflow_nodes($rootnode); 613 if (is_null($courseoverflownode)) { 614 return null; 615 } 616 if ($incourseadmin) { 617 // Validate whether the active node is part of the expected course overflow nodes. 618 if (($activenode->key !== $courseoverflownode->key) && 619 !$courseoverflownode->find($activenode->key, $activenode->type)) { 620 return null; 621 } 622 } 623 $menuarray = static::create_menu_element([$courseoverflownode]); 624 if ($activenode->key != 'coursereuse') { 625 $inmenu = false; 626 foreach ($menuarray as $key => $value) { 627 if ($this->page->url->out(false) == $key) { 628 $inmenu = true; 629 } 630 } 631 if (!$inmenu) { 632 return null; 633 } 634 } 635 // If the page has explicitly set the overflow node it would like selected, find and use that node. 636 if ($this->overflowselected) { 637 $selectedoverflownode = $this->node_matches_key_string($courseoverflownode, $this->overflowselected); 638 $selectedoverflownodeurl = $selectedoverflownode ? $selectedoverflownode->action->out(false) : null; 639 } 640 641 $menuselect = new url_select($menuarray, $selectedoverflownodeurl ?? $this->page->url, null); 642 $menuselect->set_label(get_string('browsecourseadminindex', 'course'), ['class' => 'sr-only']); 643 return $menuselect; 644 } else { 645 return $this->get_other_overflow_menu_data($activenode); 646 } 647 } 648 649 /** 650 * Gets overflow menu data for third party plugin settings. 651 * 652 * @param navigation_node $activenode The node to gather the children for to put into the overflow menu. 653 * @return url_select|null The overflow menu in a url_select object. 654 */ 655 protected function get_other_overflow_menu_data(navigation_node $activenode): ?url_select { 656 if (!$activenode->has_action()) { 657 return null; 658 } 659 660 if (!$activenode->has_children()) { 661 return null; 662 } 663 664 // If the setting is extending the course navigation then the page being redirected to should be in the course context. 665 // It was decided on the issue that put this code here that plugins that extend the course navigation should have the pages 666 // that are redirected to, be in the course context or module context depending on which callback was used. 667 // Third part plugins were checked to see if any existing plugins had settings in a system context and none were found. 668 // The request of third party developers is to keep their settings within the specified context. 669 if ($this->page->context->contextlevel != CONTEXT_COURSE 670 && $this->page->context->contextlevel != CONTEXT_MODULE 671 && $this->page->context->contextlevel != CONTEXT_COURSECAT) { 672 return null; 673 } 674 675 // These areas have their own code to retrieve added plugin navigation nodes. 676 if ($activenode->key == 'coursehome' || $activenode->key == 'questionbank' || $activenode->key == 'coursereports') { 677 return null; 678 } 679 680 $menunode = $this->page->settingsnav->find($activenode->key, null); 681 682 if (!$menunode instanceof navigation_node) { 683 return null; 684 } 685 // Loop through all children and try and find a match to the current url. 686 $matchednode = $this->nodes_match_current_url($menunode); 687 if (is_null($matchednode)) { 688 return null; 689 } 690 if (!isset($menunode) || !$menunode->has_children()) { 691 return null; 692 } 693 $selectdata = static::create_menu_element([$menunode], false); 694 $urlselect = new url_select($selectdata, $matchednode->action->out(false), null); 695 $urlselect->set_label(get_string('browsesettingindex', 'course'), ['class' => 'sr-only']); 696 return $urlselect; 697 } 698 699 /** 700 * Get the module's secondary navigation. This is based on settings_nav and would include plugin nodes added via 701 * '_extend_settings_navigation'. 702 * It populates the tree based on the nav mockup 703 * 704 * If nodes change, we will have to explicitly call the callback again. 705 * 706 * @param settings_navigation $settingsnav The settings navigation object related to the module page 707 * @param navigation_node|null $rootnode The node where the module navigation nodes should be added into as children. 708 * If not explicitly defined, the nodes will be added to the secondary root 709 * node by default. 710 */ 711 protected function load_module_navigation(settings_navigation $settingsnav, ?navigation_node $rootnode = null): void { 712 $rootnode = $rootnode ?? $this; 713 $mainnode = $settingsnav->find('modulesettings', self::TYPE_SETTING); 714 $nodes = $this->get_default_module_mapping(); 715 716 if ($mainnode) { 717 $url = new \moodle_url('/mod/' . $settingsnav->get_page()->activityname . '/view.php', 718 ['id' => $settingsnav->get_page()->cm->id]); 719 $setactive = $url->compare($settingsnav->get_page()->url, URL_MATCH_BASE); 720 $node = $rootnode->add(get_string('modulename', $settingsnav->get_page()->activityname), $url, 721 null, null, 'modulepage'); 722 if ($setactive) { 723 $node->make_active(); 724 } 725 // Add the initial nodes. 726 $nodesordered = $this->get_leaf_nodes($mainnode, $nodes); 727 $this->add_ordered_nodes($nodesordered, $rootnode); 728 729 // We have finished inserting the initial structure. 730 // Populate the menu with the rest of the nodes available. 731 $this->load_remaining_nodes($mainnode, $nodes, $rootnode); 732 } 733 } 734 735 /** 736 * Load the course category navigation. 737 */ 738 protected function load_category_navigation(): void { 739 $settingsnav = $this->page->settingsnav; 740 $mainnode = $settingsnav->find('categorysettings', self::TYPE_CONTAINER); 741 $nodes = $this->get_default_category_mapping(); 742 743 if ($mainnode) { 744 $url = new \moodle_url('/course/index.php', ['categoryid' => $this->context->instanceid]); 745 $this->add(get_string('category'), $url, self::TYPE_CONTAINER, null, 'categorymain'); 746 747 // Add the initial nodes. 748 $nodesordered = $this->get_leaf_nodes($mainnode, $nodes); 749 $this->add_ordered_nodes($nodesordered); 750 751 // We have finished inserting the initial structure. 752 // Populate the menu with the rest of the nodes available. 753 $this->load_remaining_nodes($mainnode, $nodes); 754 } 755 } 756 757 /** 758 * Load the site admin navigation 759 */ 760 protected function load_admin_navigation(): void { 761 global $PAGE, $SITE; 762 763 $settingsnav = $this->page->settingsnav; 764 $node = $settingsnav->find('root', self::TYPE_SITE_ADMIN); 765 // We need to know if we are on the main site admin search page. Here the navigation between tabs are done via 766 // anchors and page reload doesn't happen. On every nested admin settings page, the secondary nav needs to 767 // exist as links with anchors appended in order to redirect back to the admin search page and the corresponding 768 // tab. Note this value refers to being present on the page itself, before a search has been performed. 769 $isadminsearchpage = $PAGE->url->compare(new \moodle_url('/admin/search.php', ['query' => '']), URL_MATCH_PARAMS); 770 if ($node) { 771 $siteadminnode = $this->add(get_string('general'), "#link$node->key", null, null, 'siteadminnode'); 772 if ($isadminsearchpage) { 773 $siteadminnode->action = false; 774 $siteadminnode->tab = "#link$node->key"; 775 } else { 776 $siteadminnode->action = new \moodle_url("/admin/search.php", [], "link$node->key"); 777 } 778 foreach ($node->children as $child) { 779 if ($child->display && !$child->is_short_branch()) { 780 // Mimic the current boost behaviour and pass down anchors for the tabs. 781 if ($isadminsearchpage) { 782 $child->action = false; 783 $child->tab = "#link$child->key"; 784 } else { 785 $child->action = new \moodle_url("/admin/search.php", [], "link$child->key"); 786 } 787 $this->add_node(clone $child); 788 } else { 789 $siteadminnode->add_node(clone $child); 790 } 791 } 792 } 793 } 794 795 /** 796 * Adds the indexed nodes to the current view or a given node. The key should indicate it's position in the tree. 797 * Any sub nodes needs to be numbered appropriately, e.g. 3.1 would make the identified node be listed under #3 node. 798 * 799 * @param array $nodes An array of navigation nodes to be added. 800 * @param navigation_node|null $rootnode The node where the nodes should be added into as children. If not explicitly 801 * defined, the nodes will be added to the secondary root node by default. 802 */ 803 protected function add_ordered_nodes(array $nodes, ?navigation_node $rootnode = null): void { 804 $rootnode = $rootnode ?? $this; 805 ksort($nodes); 806 foreach ($nodes as $key => $node) { 807 // If the key is a string then we are assuming this is a nested element. 808 if (is_string($key)) { 809 $parentnode = $nodes[floor($key)] ?? null; 810 if ($parentnode) { 811 $parentnode->add_node(clone $node); 812 } 813 } else { 814 $rootnode->add_node(clone $node); 815 } 816 } 817 } 818 819 /** 820 * Find the remaining nodes that need to be loaded into secondary based on the current context or a given node. 821 * 822 * @param navigation_node $completenode The original node that we are sourcing information from 823 * @param array $nodesmap The map used to populate secondary nav in the given context 824 * @param navigation_node|null $rootnode The node where the remaining nodes should be added into as children. If not 825 * explicitly defined, the nodes will be added to the secondary root node by 826 * default. 827 */ 828 protected function load_remaining_nodes(navigation_node $completenode, array $nodesmap, 829 ?navigation_node $rootnode = null): void { 830 $flattenednodes = []; 831 $rootnode = $rootnode ?? $this; 832 foreach ($nodesmap as $nodecontainer) { 833 $flattenednodes = array_merge(array_keys($nodecontainer), $flattenednodes); 834 } 835 836 $populatedkeys = $this->get_children_key_list(); 837 $existingkeys = $completenode->get_children_key_list(); 838 $leftover = array_diff($existingkeys, $populatedkeys); 839 foreach ($leftover as $key) { 840 if (!in_array($key, $flattenednodes, true) && $leftovernode = $completenode->get($key)) { 841 // Check for nodes with children and potentially no action to direct to. 842 if ($leftovernode->has_children()) { 843 $leftovernode = $this->get_first_action_for_node($leftovernode); 844 } 845 846 // We have found the first node with an action. 847 if ($leftovernode) { 848 $this->add_external_nodes_to_secondary($leftovernode, $leftovernode, $rootnode); 849 } 850 } 851 } 852 } 853 854 /** 855 * Force certain secondary navigation nodes to be displayed in the "more" menu. 856 * 857 * @param array $defaultmoremenunodes Array with navigation node keys of the pre-defined nodes that 858 * should be added into the "more" menu by default 859 * @param int|null $maxdisplayednodes The maximum limit of navigation nodes displayed in the secondary navigation 860 */ 861 protected function force_nodes_into_more_menu(array $defaultmoremenunodes = [], ?int $maxdisplayednodes = null) { 862 // Counter of the navigation nodes that are initially displayed in the secondary nav 863 // (excludes the nodes from the "more" menu). 864 $displayednodescount = 0; 865 foreach ($this->children as $child) { 866 // Skip if the navigation node has been already forced into the "more" menu. 867 if ($child->forceintomoremenu) { 868 continue; 869 } 870 // If the navigation node is in the pre-defined list of nodes that should be added by default in the 871 // "more" menu or the maximum limit of displayed navigation nodes has been reached (if defined). 872 if (in_array($child->key, $defaultmoremenunodes) || 873 (!is_null($maxdisplayednodes) && $displayednodescount >= $maxdisplayednodes)) { 874 // Force the node and its children into the "more" menu. 875 $child->set_force_into_more_menu(true); 876 continue; 877 } 878 $displayednodescount++; 879 } 880 } 881 882 /** 883 * Recursively remove navigation nodes that should not be displayed in the secondary navigation. 884 * 885 * @param navigation_node $node The starting navigation node. 886 */ 887 protected function remove_unwanted_nodes(navigation_node $node) { 888 foreach ($node->children as $child) { 889 if (!$child->showinsecondarynavigation) { 890 $child->remove(); 891 continue; 892 } 893 if (!empty($child->children)) { 894 $this->remove_unwanted_nodes($child); 895 } 896 } 897 } 898 899 /** 900 * Takes the given navigation nodes and searches for children and formats it all into an array in a format to be used by a 901 * url_select element. 902 * 903 * @param navigation_node[] $navigationnodes Navigation nodes to format into a menu. 904 * @param bool $forceheadings Whether the returned array should be forced to use headings. 905 * @return array|null A url select element for navigating through the navigation nodes. 906 */ 907 public static function create_menu_element(array $navigationnodes, bool $forceheadings = false): ?array { 908 if (empty($navigationnodes)) { 909 return null; 910 } 911 912 // If one item, do we put this into a url_select? 913 if (count($navigationnodes) < 2) { 914 // Check if there are children. 915 $navnode = array_shift($navigationnodes); 916 $menudata = []; 917 if (!$navnode->has_children()) { 918 // Just one item. 919 if (!$navnode->has_action()) { 920 return null; 921 } 922 $menudata[$navnode->action->out(false)] = static::format_node_text($navnode); 923 } else { 924 if (static::does_menu_need_headings($navnode) || $forceheadings) { 925 // Let's do headings. 926 $menudata = static::get_headings_nav_array($navnode); 927 } else { 928 // Simple flat nav. 929 $menudata = static::get_flat_nav_array($navnode); 930 } 931 } 932 return $menudata; 933 } else { 934 // We have more than one navigation node to handle. Put each node in it's own heading. 935 $menudata = []; 936 $titledata = []; 937 foreach ($navigationnodes as $navigationnode) { 938 if ($navigationnode->has_children()) { 939 $menuarray = []; 940 // Add a heading and flatten out everything else. 941 if ($navigationnode->has_action()) { 942 $menuarray[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] = 943 static::format_node_text($navigationnode); 944 $menuarray[static::format_node_text($navigationnode)] += static::get_whole_tree_flat($navigationnode); 945 } else { 946 $menuarray[static::format_node_text($navigationnode)] = static::get_whole_tree_flat($navigationnode); 947 } 948 949 $titledata += $menuarray; 950 } else { 951 // Add with no heading. 952 if (!$navigationnode->has_action()) { 953 return null; 954 } 955 $menudata[$navigationnode->action->out(false)] = static::format_node_text($navigationnode); 956 } 957 } 958 $menudata += [$titledata]; 959 return $menudata; 960 } 961 } 962 963 /** 964 * Recursively goes through the provided navigation node and returns a flat version. 965 * 966 * @param navigation_node $navigationnode The navigationnode. 967 * @return array The whole tree flat. 968 */ 969 protected static function get_whole_tree_flat(navigation_node $navigationnode): array { 970 $nodes = []; 971 foreach ($navigationnode->children as $child) { 972 if ($child->has_action()) { 973 $nodes[$child->action->out()] = $child->text; 974 } 975 if ($child->has_children()) { 976 $childnodes = static::get_whole_tree_flat($child); 977 $nodes = array_merge($nodes, $childnodes); 978 } 979 } 980 return $nodes; 981 } 982 983 /** 984 * Checks to see if the provided navigation node has children and determines if we want headings for a url select element. 985 * 986 * @param navigation_node $navigationnode The navigation node we are checking. 987 * @return bool Whether we want headings or not. 988 */ 989 protected static function does_menu_need_headings(navigation_node $navigationnode): bool { 990 if (!$navigationnode->has_children()) { 991 return false; 992 } 993 foreach ($navigationnode->children as $child) { 994 if ($child->has_children()) { 995 return true; 996 } 997 } 998 return false; 999 } 1000 1001 /** 1002 * Takes the navigation node and returns it in a flat fashion. This is not recursive. 1003 * 1004 * @param navigation_node $navigationnode The navigation node that we want to format into an array in a flat structure. 1005 * @return array The flat navigation array. 1006 */ 1007 protected static function get_flat_nav_array(navigation_node $navigationnode): array { 1008 $menuarray = []; 1009 if ($navigationnode->has_action()) { 1010 $menuarray[$navigationnode->action->out(false)] = static::format_node_text($navigationnode); 1011 } 1012 1013 foreach ($navigationnode->children as $child) { 1014 if ($child->has_action()) { 1015 $menuarray[$child->action->out(false)] = static::format_node_text($child); 1016 } 1017 } 1018 return $menuarray; 1019 } 1020 1021 /** 1022 * For any navigation node that we have determined needs headings we return a more tree like array structure. 1023 * 1024 * @param navigation_node $navigationnode The navigation node to use for the formatted array structure. 1025 * @return array The headings navigation array structure. 1026 */ 1027 protected static function get_headings_nav_array(navigation_node $navigationnode): array { 1028 $menublock = []; 1029 // We know that this single node has headings, so grab this for the first heading. 1030 $firstheading = []; 1031 if ($navigationnode->has_action()) { 1032 $firstheading[static::format_node_text($navigationnode)][$navigationnode->action->out(false)] = 1033 static::format_node_text($navigationnode); 1034 $firstheading[static::format_node_text($navigationnode)] += static::get_more_child_nodes($navigationnode, $menublock); 1035 } else { 1036 $firstheading[static::format_node_text($navigationnode)] = static::get_more_child_nodes($navigationnode, $menublock); 1037 } 1038 return [$firstheading + $menublock]; 1039 } 1040 1041 /** 1042 * Recursively goes and gets all children nodes. 1043 * 1044 * @param navigation_node $node The node to get the children of. 1045 * @param array $menublock Used to put all child nodes in its own container. 1046 * @return array The additional child nodes. 1047 */ 1048 protected static function get_more_child_nodes(navigation_node $node, array &$menublock): array { 1049 $nodes = []; 1050 foreach ($node->children as $child) { 1051 if (!$child->has_children()) { 1052 if (!$child->has_action()) { 1053 continue; 1054 } 1055 $nodes[$child->action->out(false)] = static::format_node_text($child); 1056 } else { 1057 $newarray = []; 1058 if ($child->has_action()) { 1059 $newarray[static::format_node_text($child)][$child->action->out(false)] = static::format_node_text($child); 1060 $newarray[static::format_node_text($child)] += static::get_more_child_nodes($child, $menublock); 1061 } else { 1062 $newarray[static::format_node_text($child)] = static::get_more_child_nodes($child, $menublock); 1063 } 1064 $menublock += $newarray; 1065 } 1066 } 1067 return $nodes; 1068 } 1069 1070 /** 1071 * Returns the navigation node text in a string. 1072 * 1073 * @param navigation_node $navigationnode The navigationnode to return the text string of. 1074 * @return string The navigation node text string. 1075 */ 1076 protected static function format_node_text(navigation_node $navigationnode): string { 1077 return (is_a($navigationnode->text, 'lang_string')) ? $navigationnode->text->out() : $navigationnode->text; 1078 } 1079 1080 /** 1081 * Load the single activity course secondary navigation. 1082 */ 1083 protected function load_single_activity_course_navigation(): void { 1084 $page = $this->page; 1085 $course = $page->course; 1086 1087 // Create 'Course' navigation node. 1088 $coursesecondarynode = navigation_node::create(get_string('course'), null, self::TYPE_COURSE, null, 'course'); 1089 $this->load_course_navigation($coursesecondarynode); 1090 // Remove the unnecessary 'Course' child node generated in load_course_navigation(). 1091 $coursehomenode = $coursesecondarynode->find('coursehome', self::TYPE_COURSE); 1092 if (!empty($coursehomenode)) { 1093 $coursehomenode->remove(); 1094 } 1095 1096 // Add the 'Course' node to the secondary navigation only if this node has children nodes. 1097 if (count($coursesecondarynode->children) > 0) { 1098 $this->add_node($coursesecondarynode); 1099 // Once all the items have been added to the 'Course' secondary navigation node, set the 'showchildreninsubmenu' 1100 // property to true. This is required to force the template to output these items within a dropdown menu. 1101 $coursesecondarynode->showchildreninsubmenu = true; 1102 } 1103 1104 // Create 'Activity' navigation node. 1105 $activitysecondarynode = navigation_node::create(get_string('activity'), null, self::TYPE_ACTIVITY, null, 'activity'); 1106 1107 // We should display the module related navigation in the course context as well. Therefore, we need to 1108 // re-initialize the page object and manually set the course module to the one that it is currently visible in 1109 // the course in order to obtain the required module settings navigation. 1110 if ($page->context instanceof \context_course) { 1111 $this->page->set_secondary_active_tab($coursesecondarynode->key); 1112 // Get the currently used module in the single activity course. 1113 $module = current(array_filter(get_course_mods($course->id), function ($module) { 1114 return $module->visible == 1; 1115 })); 1116 // If the default module for the single course format has not been set yet, skip displaying the module 1117 // related navigation in the secondary navigation. 1118 if (!$module) { 1119 return; 1120 } 1121 $page = new \moodle_page(); 1122 $page->set_cm($module, $course); 1123 $page->set_url(new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id])); 1124 } 1125 1126 $this->load_module_navigation($page->settingsnav, $activitysecondarynode); 1127 1128 // Add the 'Activity' node to the secondary navigation only if this node has more that one child node. 1129 if (count($activitysecondarynode->children) > 1) { 1130 // Set the 'showchildreninsubmenu' property to true to later output the the module navigation items within 1131 // a dropdown menu. 1132 $activitysecondarynode->showchildreninsubmenu = true; 1133 $this->add_node($activitysecondarynode); 1134 if ($this->context instanceof \context_module) { 1135 $this->page->set_secondary_active_tab($activitysecondarynode->key); 1136 } 1137 } else { // Otherwise, add the 'View activity' node to the secondary navigation. 1138 $viewactivityurl = new \moodle_url('/mod/' . $page->activityname . '/view.php', ['id' => $page->cm->id]); 1139 $this->add(get_string('modulename', $page->activityname), $viewactivityurl, null, null, 'modulepage'); 1140 if ($this->context instanceof \context_module) { 1141 $this->page->set_secondary_active_tab('modulepage'); 1142 } 1143 } 1144 } 1145 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body