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