ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK5\ƕzzviews/secondary.phpnu[. 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' => 4, 'questionbank' => 5, ], 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' => 7, 'coursebadges' => 8.1, 'newbadge' => 8.2, 'filtermanagement' => 10, 'unenrolself' => 11, 'coursetags' => 12, 'download' => 13, 'contextlocking' => 14, ], ]; $nodes['navigation'] = [ self::TYPE_CONTAINER => [ 'participants' => 1, 'courseoverview' => 3, ], self::TYPE_SETTING => [ 'grades' => 2, 'badgesview' => 8, 'competencies' => 9, 'communication' => 15, ], self::TYPE_CUSTOM => [ 'contentbank' => 6, '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) ); } // Allow plugins to add nodes to the secondary navigation. $hook = new \core\hook\navigation\secondary_extend($this); \core\di::get(\core\hook\manager::class)->dispatch($hook); } /** * 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; $activeleafnode = $this->page->settingsnav->find_active_node(); $parentnode = $activeleafnode->parent ?? null; if ($issingleactivitycourse && $parentnode && $parentnode->key === 'quiz_report') { $activenode = $parentnode; } 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' => 'visually-hidden']); 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' => 'visually-hidden']); 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'); } } } } PKX\ekk styles.cssnu[.block_navigation .block_tree .depth_1 > .tree_item.branch { padding-left: 0; background-image: none; } .block_navigation .block_tree .depth_1 > ul { margin: 0; } .block_navigation .block_tree ul { margin-left: 18px; } .block_navigation .block_tree p.hasicon { text-indent: -21px; padding-left: 21px; } .block_navigation .block_tree p.hasicon img { width: 16px; height: 16px; margin-top: 3px; margin-right: 5px; vertical-align: top; } .block_navigation .block_tree p.hasicon.visibleifjs { display: block; } .block_navigation .block_tree .tree_item { cursor: pointer; padding-left: 0; margin: 3px 0; background-position: 0 50%; background-repeat: no-repeat; word-wrap: break-word; } .block_navigation .block_tree .tree_item.branch { padding-left: 21px; } .block_navigation .block_tree .active_tree_node { font-weight: bold; } .block_navigation .block_tree [aria-expanded="true"] { background-image: url('[[pix:t/expanded]]'); } .block_navigation .block_tree [aria-expanded="false"] { background-image: url('[[pix:t/collapsed]]'); } .block_navigation .block_tree [aria-expanded="true"].emptybranch { background-image: url('[[pix:t/collapsed_empty]]'); } .block_navigation .block_tree [aria-expanded="false"] p.loading { background-image: url('[[pix:i/loading_small]]'); } /*rtl:raw: .block_navigation .block_tree [aria-expanded="false"] {background-image: url('[[pix:t/collapsed_rtl]]');} .block_navigation .block_tree [aria-expanded="true"].emptybranch {background-image: url('[[pix:t/collapsed_empty_rtl]]');} .block_navigation .block_tree [aria-expanded="false"].loading {background-image: url('[[pix:i/loading_small]]');} */ .block_navigation .block_tree [aria-hidden="false"] { display: block; } .block_navigation .block_tree [aria-hidden="true"]:not(.icon) { display: none; } PKX\ׂf!f! renderer.phpnu[. /** * Outputs the navigation tree. * * @since Moodle 2.0 * @package block_navigation * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Renderer for block navigation * * @package block_navigation * @category navigation * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class block_navigation_renderer extends plugin_renderer_base { /** * Returns the content of the navigation tree. * * @param global_navigation $navigation * @param int $expansionlimit * @param array $options * @return string $content */ public function navigation_tree(global_navigation $navigation, $expansionlimit, array $options = array()) { $navigation->add_class('navigation_node'); $navigationattrs = array( 'class' => 'block_tree list', 'role' => 'tree', 'data-ajax-loader' => 'block_navigation/nav_loader'); $content = $this->navigation_node(array($navigation), $navigationattrs, $expansionlimit, $options); if (isset($navigation->id) && !is_numeric($navigation->id) && !empty($content)) { $content = $this->output->box($content, 'block_tree_box', $navigation->id); } return $content; } /** * Produces a navigation node for the navigation tree * * @param navigation_node[] $items * @param array $attrs * @param int $expansionlimit * @param array $options * @param int $depth * @return string */ protected function navigation_node($items, $attrs=array(), $expansionlimit=null, array $options = array(), $depth=1) { // Exit if empty, we don't want an empty ul element. if (count($items) === 0) { return ''; } // Turn our navigation items into list items. $lis = array(); // Set the number to be static for unique id's. static $number = 0; $htmlidprefix = html_writer::random_id(); foreach ($items as $item) { $number++; if (!$item->display && !$item->contains_active_node()) { continue; } $isexpandable = (empty($expansionlimit) || ($item->type > navigation_node::TYPE_ACTIVITY || $item->type < $expansionlimit) || ($item->contains_active_node() && $item->children->count() > 0)); // Skip elements which have no content and no action - no point in showing them if (!$isexpandable && empty($item->action)) { continue; } $id = $item->id ? $item->id : html_writer::random_id(); $content = $item->get_content(); $title = $item->get_title(); $ulattr = ['id' => $id . '_group', 'role' => 'group']; $liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth], 'role' => 'treeitem']; $pattr = ['class' => ['tree_item']]; $pattr += !empty($item->id) ? ['id' => $item->id] : []; $isbranch = $isexpandable && ($item->children->count() > 0 || ($item->has_children() && (isloggedin() || $item->type <= navigation_node::TYPE_CATEGORY))); $hasicon = ((!$isbranch || $item->type == navigation_node::TYPE_ACTIVITY || $item->type == navigation_node::TYPE_RESOURCE) && $item->icon instanceof renderable); $icon = ''; if ($hasicon) { $liattr['class'][] = 'item_with_icon'; $pattr['class'][] = 'hasicon'; $icon = $this->output->render($item->icon); // Because an icon is being used we're going to wrap the actual content in a span. // This will allow designers to create columns for the content, as we've done in styles.css. $content = $icon . html_writer::span($content, 'item-content-wrap'); } if ($item->helpbutton !== null) { $content = trim($item->helpbutton).html_writer::tag('span', $content, array('class'=>'clearhelpbutton')); } if (empty($content)) { continue; } $nodetextid = $htmlidprefix . '_label_' . $depth . '_' . $number; $attributes = array('tabindex' => '-1', 'id' => $nodetextid); if ($title !== '') { $attributes['title'] = $title; } if ($item->hidden) { $attributes['class'] = 'dimmed_text'; } if (is_string($item->action) || empty($item->action) || (($item->type === navigation_node::TYPE_CATEGORY || $item->type === navigation_node::TYPE_MY_CATEGORY) && empty($options['linkcategories']))) { $content = html_writer::tag('span', $content, $attributes); } else if ($item->action instanceof action_link) { //TODO: to be replaced with something else $link = $item->action; $link->text = $icon.html_writer::span($link->text, 'item-content-wrap'); $link->attributes = array_merge($link->attributes, $attributes); $content = $this->output->render($link); } else if ($item->action instanceof moodle_url) { $content = html_writer::link($item->action, $content, $attributes); } if ($isbranch) { $ariaexpanded = $item->has_children() && (!$item->forceopen || $item->collapse); $pattr['class'][] = 'branch'; $liattr['class'][] = 'contains_branch'; $liattr += ['aria-expanded' => $ariaexpanded ? "false" : "true"]; if ($item->requiresajaxloading) { $liattr += [ 'data-requires-ajax' => 'true', 'data-loaded' => 'false', 'data-node-id' => $item->id, 'data-node-key' => $item->key, 'data-node-type' => $item->type ]; } else { $liattr += ['aria-owns' => $id . '_group']; } } if ($item->isactive === true) { $liattr['class'][] = 'current_branch'; } if (!empty($item->classes) && count($item->classes)>0) { $pattr['class'] = array_merge($pattr['class'], $item->classes); } $liattr['class'] = join(' ', $liattr['class']); $pattr['class'] = join(' ', $pattr['class']); $liattr += $depth == 1 ? ['data-collapsible' => 'false'] : []; if (isset($liattr['aria-expanded']) && $liattr['aria-expanded'] === 'false') { $ulattr += ['aria-hidden' => 'true']; } // Create the structure. $content = html_writer::tag('p', $content, $pattr); if ($isexpandable) { $content .= $this->navigation_node($item->children, $ulattr, $expansionlimit, $options, $depth + 1); } if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) { $content = html_writer::empty_tag('hr') . $content; } $liattr['aria-labelledby'] = $nodetextid; $content = html_writer::tag('li', $content, $liattr); $lis[] = $content; } if (count($lis) === 0) { // There is still a chance, despite having items, that nothing had content and no list items were created. return ''; } // We used to separate using new lines, however we don't do that now, instead we'll save a few chars. // The source is complex already anyway. return html_writer::tag('ul', implode('', $lis), $attrs); } } PKX\l db/access.phpnu[. /** * Navigation block caps. * * @package block_navigation * @copyright Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'block/navigation:myaddinstance' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => array( 'user' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/my:manageblocks' ), 'block/navigation:addinstance' => array( 'riskbitmask' => RISK_SPAM | RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_BLOCK, 'archetypes' => array( 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/site:manageblocks' ), ); PKX\{o< db/upgrade.phpnu[. /** * This file keeps track of upgrades to the navigation block * * Sometimes, changes between versions involve alterations to database structures * and other major things that may break installations. * * The upgrade function in this file will attempt to perform all the necessary * actions to upgrade your older installation to the current version. * * If there's something it cannot do itself, it will tell you what you need to do. * * The commands in here will all be database-neutral, using the methods of * database_manager class * * Please do not forget to use upgrade_set_timeout() * before any action that may take longer time to finish. * * @since Moodle 2.0 * @package block_navigation * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * As of the implementation of this block and the general navigation code * in Moodle 2.0 the body of immediate upgrade work for this block and * settings is done in core upgrade {@see lib/db/upgrade.php} * * There were several reasons that they were put there and not here, both becuase * the process for the two blocks was very similar and because the upgrade process * was complex due to us wanting to remvoe the outmoded blocks that this * block was going to replace. * * @param int $oldversion * @param object $block */ function xmldb_block_navigation_upgrade($oldversion, $block) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } PKX\ylang/en/block_navigation.phpnu[. /** * This file contains language strings used in the global navigation block * * @since Moodle 2.0 * @package block_navigation * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['everything'] = 'Everything'; $string['courses'] = 'Categories and courses'; $string['coursestructures'] = 'Categories, courses, and course structures'; $string['courseactivities'] = 'Categories, courses, and course activities'; $string['enabledockdesc'] = 'Allow the user to dock this block'; $string['expansionlimit'] = 'Generate navigation for the following'; $string['linkcategoriesdesc'] = 'Display categories as links'; $string['navigation:addinstance'] = 'Add a new navigation block'; $string['navigation:myaddinstance'] = 'Add a new navigation block to Dashboard'; $string['pluginname'] = 'Navigation'; $string['trimmode'] = 'Trim mode'; $string['trimmoderight'] = 'Trim characters from the right'; $string['trimmodeleft'] = 'Trim characters from the left'; $string['trimmodecenter'] = 'Trim characters from the center'; $string['trimlength'] = 'How many characters to trim to'; $string['privacy:metadata'] = 'The Navigation block only shows data stored in other locations.'; PKX\p˻> > edit_form.phpnu[. /** * Form for editing global navigation instances. * * @since Moodle 2.0 * @package block_navigation * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Form for editing global navigation instances. * * @package block_navigation * @category navigation * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class block_navigation_edit_form extends block_edit_form { /** * @param MoodleQuickForm $mform */ protected function specific_definition($mform) { global $CFG; $mform->addElement('header', 'configheader', get_string('blocksettings', 'block')); $mods = array('enabledock'=>'yes', 'linkcategories'=>'no'); $yesnooptions = array('yes'=>get_string('yes'), 'no'=>get_string('no')); foreach ($mods as $modname=>$default) { $mform->addElement('select', 'config_'.$modname, get_string($modname.'desc', $this->block->blockname), $yesnooptions); $mform->setDefault('config_'.$modname, $default); } $options = array( block_navigation::TRIM_RIGHT => get_string('trimmoderight', $this->block->blockname), block_navigation::TRIM_LEFT => get_string('trimmodeleft', $this->block->blockname), block_navigation::TRIM_CENTER => get_string('trimmodecenter', $this->block->blockname) ); $mform->addElement('select', 'config_trimmode', get_string('trimmode', $this->block->blockname), $options); $mform->setType('config_trimmode', PARAM_INT); $mform->addElement('text', 'config_trimlength', get_string('trimlength', $this->block->blockname)); $mform->setDefault('config_trimlength', 50); $mform->setType('config_trimlength', PARAM_INT); $options = array( 0 => get_string('everything', $this->block->blockname), global_navigation::TYPE_COURSE => get_string('courses', $this->block->blockname), global_navigation::TYPE_SECTION => get_string('coursestructures', $this->block->blockname), global_navigation::TYPE_ACTIVITY => get_string('courseactivities', $this->block->blockname) ); $mform->addElement('select', 'config_expansionlimit', get_string('expansionlimit', $this->block->blockname), $options); $mform->setType('config_expansionlimit', PARAM_INT); } }PKX\# version.phpnu[. /** * Version details * * @package block_navigation * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'block_navigation'; // Full name of the plugin (used for diagnostics) PKX\ classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for block_navigation. * * @package block_navigation * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace block_navigation\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for block_navigation implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PKX\řamd/src/navblock.jsnu[// 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 . /** * Load the navigation tree javascript. * * @module block_navigation/navblock * @copyright 2015 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {notifyBlockContentUpdated} from 'core_block/events'; import Tree from 'core/tree'; /** * Initialise the navblock javascript for the specified block instance. * * @method * @param {Number} instanceId */ export const init = instanceId => { const navTree = new Tree(".block_navigation .block_tree"); const blockNode = document.querySelector(`[data-instance-id="${instanceId}"]`); /** * The method to call when then the navtree finishes expanding a group. * * @method finishExpandingGroup * @param {Object} item * @fires event:blockContentUpdated */ navTree.finishExpandingGroup = item => { Tree.prototype.finishExpandingGroup.call(navTree, item); notifyBlockContentUpdated(blockNode); }; /** * The method to call whe then the navtree collapses a group * * @method collapseGroup * @param {Object} item * @fires event:blockContentUpdated */ navTree.collapseGroup = item => { Tree.prototype.collapseGroup.call(navTree, item); notifyBlockContentUpdated(blockNode); }; }; PKX\f[ɟamd/src/nav_loader.jsnu[// 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 . /** * Load the nav tree items via ajax and render the response. * * @module block_navigation/nav_loader * @copyright 2015 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'], function($, ajax, config, renderer) { var URL = config.wwwroot + '/lib/ajax/getnavbranch.php'; /** * Get the block instance id. * * @function getBlockInstanceId * @param {Element} element * @returns {String} the instance id */ function getBlockInstanceId(element) { return element.closest('[data-block]').attr('data-instanceid'); } return { load: function(element) { element = $(element); var promise = $.Deferred(); var data = { elementid: element.attr('data-node-id'), id: element.attr('data-node-key'), type: element.attr('data-node-type'), sesskey: config.sesskey, instance: getBlockInstanceId(element) }; var settings = { type: 'POST', dataType: 'json', data: data }; $.ajax(URL, settings).done(function(nodes) { renderer.render(element, nodes); promise.resolve(); }); return promise; } }; }); PKX\XYamd/src/site_admin_loader.jsnu[// 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 . /** * Load the site admin nav tree via ajax and render the response. * * @module block_navigation/site_admin_loader * @copyright 2015 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'], function($, ajax, config, renderer) { var SITE_ADMIN_NODE_TYPE = 71; var URL = config.wwwroot + '/lib/ajax/getsiteadminbranch.php'; return { load: function(element) { element = $(element); var promise = $.Deferred(); var data = { type: SITE_ADMIN_NODE_TYPE, sesskey: config.sesskey }; var settings = { type: 'POST', dataType: 'json', data: data }; $.ajax(URL, settings).done(function(nodes) { renderer.render(element, nodes); promise.resolve(); }); return promise; } }; }); PKX\4!amd/src/ajax_response_renderer.jsnu[// 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 . /** * Parse the response from the navblock ajax page and render the correct DOM * structure for the tree from it. * * @module block_navigation/ajax_response_renderer * @copyright 2015 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/templates', 'core/notification', 'core/url', 'core/aria', ], function( $, Templates, Notification, Url, Aria ) { // Mappings for the different types of nodes coming from the navigation. // Copied from lib/navigationlib.php navigation_node constants. var NODETYPE = { // @type int Activity (course module) = 40. ACTIVITY: 40, // @type int Resource (course module = 50. RESOURCE: 50, }; /** * Build DOM. * * @method buildDOM * @param {Object} rootElement the root element of DOM. * @param {object} nodes jquery object representing the nodes to be build. */ function buildDOM(rootElement, nodes) { var ul = $('
    '); ul.attr('role', 'group'); Aria.hide(ul); $.each(nodes, function(index, node) { if (typeof node !== 'object') { return; } var li = $('
  • '); var p = $('

    '); var id = node.id || node.key + '_tree_item'; var icon = null; var isBranch = (node.expandable || node.haschildren) ? true : false; li.attr('role', 'treeitem'); p.addClass('tree_item'); p.attr('id', id); // Negative tab index to allow it to receive focus. p.attr('tabindex', '-1'); if (node.requiresajaxloading) { li.attr('data-requires-ajax', true); li.attr('data-node-id', node.id); li.attr('data-node-key', node.key); li.attr('data-node-type', node.type); } if (isBranch) { li.addClass('collapsed contains_branch'); li.attr('aria-expanded', false); p.addClass('branch'); } var eleToAddIcon = null; if (node.link) { var link = $(''); eleToAddIcon = link; link.append('' + node.name + ''); if (node.hidden) { link.addClass('dimmed'); } p.append(link); } else { var span = $(''); eleToAddIcon = span; span.append('' + node.name + ''); if (node.hidden) { span.addClass('dimmed'); } p.append(span); } if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) { li.addClass('item_with_icon'); p.addClass('hasicon'); if (node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE) { icon = $(''); icon.attr('alt', node.icon.alt); icon.attr('title', node.icon.title); icon.attr('src', Url.imageUrl(node.icon.pix, node.icon.component)); $.each(node.icon.classes, function(index, className) { icon.addClass(className); }); eleToAddIcon.prepend(icon); } else { if (node.icon.component == 'moodle') { node.icon.component = 'core'; } Templates.renderPix(node.icon.pix, node.icon.component, node.icon.title).then(function(html) { // Prepend. eleToAddIcon.prepend(html); return; }).catch(Notification.exception); } } li.append(p); ul.append(li); if (node.children && node.children.length) { buildDOM(li, node.children); } else if (isBranch && !node.requiresajaxloading) { li.removeClass('contains_branch'); p.addClass('emptybranch'); } }); rootElement.append(ul); var id = rootElement.attr('id') + '_group'; ul.attr('id', id); rootElement.attr('aria-owns', id); rootElement.attr('role', 'treeitem'); } return { render: function(element, nodes) { // The first element of the response is the existing node so we start with processing the children. if (nodes.children && nodes.children.length) { buildDOM(element, nodes.children); var item = element.children("[role='treeitem']").first(); var group = element.find('#' + item.attr('aria-owns')); item.attr('aria-expanded', true); Aria.unhide(group); } else { if (element.hasClass('contains_branch')) { element.removeClass('contains_branch'); element.addClass('emptybranch'); } } } }; }); PKX\L  "amd/build/site_admin_loader.min.jsnu[/** * Load the site admin nav tree via ajax and render the response. * * @module block_navigation/site_admin_loader * @copyright 2015 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("block_navigation/site_admin_loader",["jquery","core/ajax","core/config","block_navigation/ajax_response_renderer"],(function($,ajax,config,renderer){var URL=config.wwwroot+"/lib/ajax/getsiteadminbranch.php";return{load:function(element){element=$(element);var promise=$.Deferred(),settings={type:"POST",dataType:"json",data:{type:71,sesskey:config.sesskey}};return $.ajax(URL,settings).done((function(nodes){renderer.render(element,nodes),promise.resolve()})),promise}}})); //# sourceMappingURL=site_admin_loader.min.js.mapPKX\&T amd/build/navblock.min.js.mapnu[{"version":3,"file":"navblock.min.js","sources":["../src/navblock.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Load the navigation tree javascript.\n *\n * @module block_navigation/navblock\n * @copyright 2015 John Okely \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {notifyBlockContentUpdated} from 'core_block/events';\nimport Tree from 'core/tree';\n\n/**\n * Initialise the navblock javascript for the specified block instance.\n *\n * @method\n * @param {Number} instanceId\n */\nexport const init = instanceId => {\n const navTree = new Tree(\".block_navigation .block_tree\");\n const blockNode = document.querySelector(`[data-instance-id=\"${instanceId}\"]`);\n\n /**\n * The method to call when then the navtree finishes expanding a group.\n *\n * @method finishExpandingGroup\n * @param {Object} item\n * @fires event:blockContentUpdated\n */\n navTree.finishExpandingGroup = item => {\n Tree.prototype.finishExpandingGroup.call(navTree, item);\n notifyBlockContentUpdated(blockNode);\n };\n\n /**\n * The method to call whe then the navtree collapses a group\n *\n * @method collapseGroup\n * @param {Object} item\n * @fires event:blockContentUpdated\n */\n navTree.collapseGroup = item => {\n Tree.prototype.collapseGroup.call(navTree, item);\n notifyBlockContentUpdated(blockNode);\n };\n};\n"],"names":["instanceId","navTree","Tree","blockNode","document","querySelector","finishExpandingGroup","item","prototype","call","collapseGroup"],"mappings":";;;;;;;oJA+BoBA,mBACVC,QAAU,IAAIC,cAAK,iCACnBC,UAAYC,SAASC,2CAAoCL,kBAS/DC,QAAQK,qBAAuBC,qBACtBC,UAAUF,qBAAqBG,KAAKR,QAASM,4CACxBJ,YAU9BF,QAAQS,cAAgBH,qBACfC,UAAUE,cAAcD,KAAKR,QAASM,4CACjBJ"}PKX\ 'amd/build/ajax_response_renderer.min.jsnu[/** * Parse the response from the navblock ajax page and render the correct DOM * structure for the tree from it. * * @module block_navigation/ajax_response_renderer * @copyright 2015 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("block_navigation/ajax_response_renderer",["jquery","core/templates","core/notification","core/url","core/aria"],(function($,Templates,Notification,Url,Aria){var NODETYPE_ACTIVITY=40,NODETYPE_RESOURCE=50;function buildDOM(rootElement,nodes){var ul=$("
      ");ul.attr("role","group"),Aria.hide(ul),$.each(nodes,(function(index,node){if("object"==typeof node){var li=$("
    • "),p=$("

      "),id=node.id||node.key+"_tree_item",icon=null,isBranch=!(!node.expandable&&!node.haschildren);li.attr("role","treeitem"),p.addClass("tree_item"),p.attr("id",id),p.attr("tabindex","-1"),node.requiresajaxloading&&(li.attr("data-requires-ajax",!0),li.attr("data-node-id",node.id),li.attr("data-node-key",node.key),li.attr("data-node-type",node.type)),isBranch&&(li.addClass("collapsed contains_branch"),li.attr("aria-expanded",!1),p.addClass("branch"));var eleToAddIcon=null;if(node.link){var link=$('');eleToAddIcon=link,link.append(''+node.name+""),node.hidden&&link.addClass("dimmed"),p.append(link)}else{var span=$("");eleToAddIcon=span,span.append(''+node.name+""),node.hidden&&span.addClass("dimmed"),p.append(span)}!node.icon||isBranch&&node.type!==NODETYPE_ACTIVITY&&node.type!==NODETYPE_RESOURCE||(li.addClass("item_with_icon"),p.addClass("hasicon"),node.type===NODETYPE_ACTIVITY||node.type===NODETYPE_RESOURCE?((icon=$("")).attr("alt",node.icon.alt),icon.attr("title",node.icon.title),icon.attr("src",Url.imageUrl(node.icon.pix,node.icon.component)),$.each(node.icon.classes,(function(index,className){icon.addClass(className)})),eleToAddIcon.prepend(icon)):("moodle"==node.icon.component&&(node.icon.component="core"),Templates.renderPix(node.icon.pix,node.icon.component,node.icon.title).then((function(html){eleToAddIcon.prepend(html)})).catch(Notification.exception))),li.append(p),ul.append(li),node.children&&node.children.length?buildDOM(li,node.children):isBranch&&!node.requiresajaxloading&&(li.removeClass("contains_branch"),p.addClass("emptybranch"))}})),rootElement.append(ul);var id=rootElement.attr("id")+"_group";ul.attr("id",id),rootElement.attr("aria-owns",id),rootElement.attr("role","treeitem")}return{render:function(element,nodes){if(nodes.children&&nodes.children.length){buildDOM(element,nodes.children);var item=element.children("[role='treeitem']").first(),group=element.find("#"+item.attr("aria-owns"));item.attr("aria-expanded",!0),Aria.unhide(group)}else element.hasClass("contains_branch")&&(element.removeClass("contains_branch"),element.addClass("emptybranch"))}}})); //# sourceMappingURL=ajax_response_renderer.min.js.mapPKX\c%O#O#+amd/build/ajax_response_renderer.min.js.mapnu[{"version":3,"file":"ajax_response_renderer.min.js","sources":["../src/ajax_response_renderer.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Parse the response from the navblock ajax page and render the correct DOM\n * structure for the tree from it.\n *\n * @module block_navigation/ajax_response_renderer\n * @copyright 2015 John Okely \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([\n 'jquery',\n 'core/templates',\n 'core/notification',\n 'core/url',\n 'core/aria',\n], function(\n $,\n Templates,\n Notification,\n Url,\n Aria\n) {\n\n // Mappings for the different types of nodes coming from the navigation.\n // Copied from lib/navigationlib.php navigation_node constants.\n var NODETYPE = {\n // @type int Activity (course module) = 40.\n ACTIVITY: 40,\n // @type int Resource (course module = 50.\n RESOURCE: 50,\n };\n\n /**\n * Build DOM.\n *\n * @method buildDOM\n * @param {Object} rootElement the root element of DOM.\n * @param {object} nodes jquery object representing the nodes to be build.\n */\n function buildDOM(rootElement, nodes) {\n var ul = $('
        ');\n ul.attr('role', 'group');\n Aria.hide(ul);\n\n $.each(nodes, function(index, node) {\n if (typeof node !== 'object') {\n return;\n }\n\n var li = $('
      • ');\n var p = $('

        ');\n var id = node.id || node.key + '_tree_item';\n var icon = null;\n var isBranch = (node.expandable || node.haschildren) ? true : false;\n\n li.attr('role', 'treeitem');\n p.addClass('tree_item');\n p.attr('id', id);\n // Negative tab index to allow it to receive focus.\n p.attr('tabindex', '-1');\n\n if (node.requiresajaxloading) {\n li.attr('data-requires-ajax', true);\n li.attr('data-node-id', node.id);\n li.attr('data-node-key', node.key);\n li.attr('data-node-type', node.type);\n }\n\n if (isBranch) {\n li.addClass('collapsed contains_branch');\n li.attr('aria-expanded', false);\n p.addClass('branch');\n }\n\n var eleToAddIcon = null;\n if (node.link) {\n var link = $('');\n\n eleToAddIcon = link;\n link.append('' + node.name + '');\n\n if (node.hidden) {\n link.addClass('dimmed');\n }\n\n p.append(link);\n } else {\n var span = $('');\n\n eleToAddIcon = span;\n span.append('' + node.name + '');\n\n if (node.hidden) {\n span.addClass('dimmed');\n }\n\n p.append(span);\n }\n\n if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) {\n li.addClass('item_with_icon');\n p.addClass('hasicon');\n\n if (node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE) {\n icon = $('');\n icon.attr('alt', node.icon.alt);\n icon.attr('title', node.icon.title);\n icon.attr('src', Url.imageUrl(node.icon.pix, node.icon.component));\n $.each(node.icon.classes, function(index, className) {\n icon.addClass(className);\n });\n eleToAddIcon.prepend(icon);\n } else {\n if (node.icon.component == 'moodle') {\n node.icon.component = 'core';\n }\n Templates.renderPix(node.icon.pix, node.icon.component, node.icon.title).then(function(html) {\n // Prepend.\n eleToAddIcon.prepend(html);\n return;\n }).catch(Notification.exception);\n }\n }\n\n li.append(p);\n ul.append(li);\n\n if (node.children && node.children.length) {\n buildDOM(li, node.children);\n } else if (isBranch && !node.requiresajaxloading) {\n li.removeClass('contains_branch');\n p.addClass('emptybranch');\n }\n });\n\n rootElement.append(ul);\n var id = rootElement.attr('id') + '_group';\n ul.attr('id', id);\n rootElement.attr('aria-owns', id);\n rootElement.attr('role', 'treeitem');\n }\n\n return {\n render: function(element, nodes) {\n // The first element of the response is the existing node so we start with processing the children.\n if (nodes.children && nodes.children.length) {\n buildDOM(element, nodes.children);\n\n var item = element.children(\"[role='treeitem']\").first();\n var group = element.find('#' + item.attr('aria-owns'));\n\n item.attr('aria-expanded', true);\n Aria.unhide(group);\n } else {\n if (element.hasClass('contains_branch')) {\n element.removeClass('contains_branch');\n element.addClass('emptybranch');\n }\n }\n }\n };\n});\n"],"names":["define","$","Templates","Notification","Url","Aria","NODETYPE","buildDOM","rootElement","nodes","ul","attr","hide","each","index","node","li","p","id","key","icon","isBranch","expandable","haschildren","addClass","requiresajaxloading","type","eleToAddIcon","link","title","append","name","hidden","span","alt","imageUrl","pix","component","classes","className","prepend","renderPix","then","html","catch","exception","children","length","removeClass","render","element","item","first","group","find","unhide","hasClass"],"mappings":";;;;;;;;AAuBAA,iDAAO,CACH,SACA,iBACA,oBACA,WACA,cACD,SACCC,EACAC,UACAC,aACAC,IACAC,UAKIC,kBAEU,GAFVA,kBAIU,YAULC,SAASC,YAAaC,WACvBC,GAAKT,EAAE,aACXS,GAAGC,KAAK,OAAQ,SAChBN,KAAKO,KAAKF,IAEVT,EAAEY,KAAKJ,OAAO,SAASK,MAAOC,SACN,iBAATA,UAIPC,GAAKf,EAAE,aACPgB,EAAIhB,EAAE,WACNiB,GAAKH,KAAKG,IAAMH,KAAKI,IAAM,aAC3BC,KAAO,KACPC,YAAYN,KAAKO,aAAcP,KAAKQ,aAExCP,GAAGL,KAAK,OAAQ,YAChBM,EAAEO,SAAS,aACXP,EAAEN,KAAK,KAAMO,IAEbD,EAAEN,KAAK,WAAY,MAEfI,KAAKU,sBACLT,GAAGL,KAAK,sBAAsB,GAC9BK,GAAGL,KAAK,eAAgBI,KAAKG,IAC7BF,GAAGL,KAAK,gBAAiBI,KAAKI,KAC9BH,GAAGL,KAAK,iBAAkBI,KAAKW,OAG/BL,WACAL,GAAGQ,SAAS,6BACZR,GAAGL,KAAK,iBAAiB,GACzBM,EAAEO,SAAS,eAGXG,aAAe,QACfZ,KAAKa,KAAM,KACPA,KAAO3B,EAAE,aAAec,KAAKc,MAAQ,WAAad,KAAKa,KAAO,UAElED,aAAeC,KACfA,KAAKE,OAAO,mCAAqCf,KAAKgB,KAAO,WAEzDhB,KAAKiB,QACLJ,KAAKJ,SAAS,UAGlBP,EAAEa,OAAOF,UACN,KACCK,KAAOhC,EAAE,iBAEb0B,aAAeM,KACfA,KAAKH,OAAO,mCAAqCf,KAAKgB,KAAO,WAEzDhB,KAAKiB,QACLC,KAAKT,SAAS,UAGlBP,EAAEa,OAAOG,OAGTlB,KAAKK,MAAUC,UAAYN,KAAKW,OAASpB,mBAAqBS,KAAKW,OAASpB,oBAC5EU,GAAGQ,SAAS,kBACZP,EAAEO,SAAS,WAEPT,KAAKW,OAASpB,mBAAqBS,KAAKW,OAASpB,oBACjDc,KAAOnB,EAAE,WACJU,KAAK,MAAOI,KAAKK,KAAKc,KAC3Bd,KAAKT,KAAK,QAASI,KAAKK,KAAKS,OAC7BT,KAAKT,KAAK,MAAOP,IAAI+B,SAASpB,KAAKK,KAAKgB,IAAKrB,KAAKK,KAAKiB,YACvDpC,EAAEY,KAAKE,KAAKK,KAAKkB,SAAS,SAASxB,MAAOyB,WACtCnB,KAAKI,SAASe,cAElBZ,aAAaa,QAAQpB,QAEM,UAAvBL,KAAKK,KAAKiB,YACVtB,KAAKK,KAAKiB,UAAY,QAE1BnC,UAAUuC,UAAU1B,KAAKK,KAAKgB,IAAKrB,KAAKK,KAAKiB,UAAWtB,KAAKK,KAAKS,OAAOa,MAAK,SAASC,MAEnFhB,aAAaa,QAAQG,SAEtBC,MAAMzC,aAAa0C,aAI9B7B,GAAGc,OAAOb,GACVP,GAAGoB,OAAOd,IAEND,KAAK+B,UAAY/B,KAAK+B,SAASC,OAC/BxC,SAASS,GAAID,KAAK+B,UACXzB,WAAaN,KAAKU,sBACzBT,GAAGgC,YAAY,mBACf/B,EAAEO,SAAS,oBAInBhB,YAAYsB,OAAOpB,QACfQ,GAAKV,YAAYG,KAAK,MAAQ,SAClCD,GAAGC,KAAK,KAAMO,IACdV,YAAYG,KAAK,YAAaO,IAC9BV,YAAYG,KAAK,OAAQ,kBAGtB,CACHsC,OAAQ,SAASC,QAASzC,UAElBA,MAAMqC,UAAYrC,MAAMqC,SAASC,OAAQ,CACzCxC,SAAS2C,QAASzC,MAAMqC,cAEpBK,KAAOD,QAAQJ,SAAS,qBAAqBM,QAC7CC,MAAQH,QAAQI,KAAK,IAAMH,KAAKxC,KAAK,cAEzCwC,KAAKxC,KAAK,iBAAiB,GAC3BN,KAAKkD,OAAOF,YAERH,QAAQM,SAAS,qBACjBN,QAAQF,YAAY,mBACpBE,QAAQ1B,SAAS"}PKX\Ϳr amd/build/nav_loader.min.js.mapnu[{"version":3,"file":"nav_loader.min.js","sources":["../src/nav_loader.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Load the nav tree items via ajax and render the response.\n *\n * @module block_navigation/nav_loader\n * @copyright 2015 John Okely \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'],\n function($, ajax, config, renderer) {\n var URL = config.wwwroot + '/lib/ajax/getnavbranch.php';\n\n /**\n * Get the block instance id.\n *\n * @function getBlockInstanceId\n * @param {Element} element\n * @returns {String} the instance id\n */\n function getBlockInstanceId(element) {\n return element.closest('[data-block]').attr('data-instanceid');\n }\n\n return {\n load: function(element) {\n element = $(element);\n var promise = $.Deferred();\n var data = {\n elementid: element.attr('data-node-id'),\n id: element.attr('data-node-key'),\n type: element.attr('data-node-type'),\n sesskey: config.sesskey,\n instance: getBlockInstanceId(element)\n };\n var settings = {\n type: 'POST',\n dataType: 'json',\n data: data\n };\n\n $.ajax(URL, settings).done(function(nodes) {\n renderer.render(element, nodes);\n promise.resolve();\n });\n\n return promise;\n }\n };\n});\n"],"names":["define","$","ajax","config","renderer","URL","wwwroot","getBlockInstanceId","element","closest","attr","load","promise","Deferred","settings","type","dataType","data","elementid","id","sesskey","instance","done","nodes","render","resolve"],"mappings":";;;;;;;AAsBAA,qCAAO,CAAC,SAAU,YAAa,cAAe,4CAC1C,SAASC,EAAGC,KAAMC,OAAQC,cAClBC,IAAMF,OAAOG,QAAU,sCASlBC,mBAAmBC,gBACjBA,QAAQC,QAAQ,gBAAgBC,KAAK,yBAG7C,CACHC,KAAM,SAASH,SACXA,QAAUP,EAAEO,aACRI,QAAUX,EAAEY,WAQZC,SAAW,CACXC,KAAM,OACNC,SAAU,OACVC,KAVO,CACPC,UAAWV,QAAQE,KAAK,gBACxBS,GAAIX,QAAQE,KAAK,iBACjBK,KAAMP,QAAQE,KAAK,kBACnBU,QAASjB,OAAOiB,QAChBC,SAAUd,mBAAmBC,kBAQjCP,EAAEC,KAAKG,IAAKS,UAAUQ,MAAK,SAASC,OAChCnB,SAASoB,OAAOhB,QAASe,OACzBX,QAAQa,aAGLb"}PKX\xamd/build/navblock.min.jsnu[define("block_navigation/navblock",["exports","core_block/events","core/tree"],(function(_exports,_events,_tree){var obj; /** * Load the navigation tree javascript. * * @module block_navigation/navblock * @copyright 2015 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_tree=(obj=_tree)&&obj.__esModule?obj:{default:obj};_exports.init=instanceId=>{const navTree=new _tree.default(".block_navigation .block_tree"),blockNode=document.querySelector('[data-instance-id="'.concat(instanceId,'"]'));navTree.finishExpandingGroup=item=>{_tree.default.prototype.finishExpandingGroup.call(navTree,item),(0,_events.notifyBlockContentUpdated)(blockNode)},navTree.collapseGroup=item=>{_tree.default.prototype.collapseGroup.call(navTree,item),(0,_events.notifyBlockContentUpdated)(blockNode)}}})); //# sourceMappingURL=navblock.min.js.mapPKX\{i &amd/build/site_admin_loader.min.js.mapnu[{"version":3,"file":"site_admin_loader.min.js","sources":["../src/site_admin_loader.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Load the site admin nav tree via ajax and render the response.\n *\n * @module block_navigation/site_admin_loader\n * @copyright 2015 John Okely \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'],\n function($, ajax, config, renderer) {\n\n var SITE_ADMIN_NODE_TYPE = 71;\n var URL = config.wwwroot + '/lib/ajax/getsiteadminbranch.php';\n\n return {\n load: function(element) {\n element = $(element);\n var promise = $.Deferred();\n var data = {\n type: SITE_ADMIN_NODE_TYPE,\n sesskey: config.sesskey\n };\n var settings = {\n type: 'POST',\n dataType: 'json',\n data: data\n };\n\n $.ajax(URL, settings).done(function(nodes) {\n renderer.render(element, nodes);\n promise.resolve();\n });\n\n return promise;\n }\n };\n});\n"],"names":["define","$","ajax","config","renderer","URL","wwwroot","load","element","promise","Deferred","settings","type","dataType","data","sesskey","done","nodes","render","resolve"],"mappings":";;;;;;;AAsBAA,4CAAO,CAAC,SAAU,YAAa,cAAe,4CACtC,SAASC,EAAGC,KAAMC,OAAQC,cAG1BC,IAAMF,OAAOG,QAAU,yCAEpB,CACHC,KAAM,SAASC,SACXA,QAAUP,EAAEO,aACRC,QAAUR,EAAES,WAKZC,SAAW,CACXC,KAAM,OACNC,SAAU,OACVC,KAPO,CACPF,KARe,GASfG,QAASZ,OAAOY,iBAQpBd,EAAEC,KAAKG,IAAKM,UAAUK,MAAK,SAASC,OAChCb,SAASc,OAAOV,QAASS,OACzBR,QAAQU,aAGLV"}PKX\1-amd/build/nav_loader.min.jsnu[/** * Load the nav tree items via ajax and render the response. * * @module block_navigation/nav_loader * @copyright 2015 John Okely * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("block_navigation/nav_loader",["jquery","core/ajax","core/config","block_navigation/ajax_response_renderer"],(function($,ajax,config,renderer){var URL=config.wwwroot+"/lib/ajax/getnavbranch.php";function getBlockInstanceId(element){return element.closest("[data-block]").attr("data-instanceid")}return{load:function(element){element=$(element);var promise=$.Deferred(),settings={type:"POST",dataType:"json",data:{elementid:element.attr("data-node-id"),id:element.attr("data-node-key"),type:element.attr("data-node-type"),sesskey:config.sesskey,instance:getBlockInstanceId(element)}};return $.ajax(URL,settings).done((function(nodes){renderer.render(element,nodes),promise.resolve()})),promise}}})); //# sourceMappingURL=nav_loader.min.js.mapPKX\h=.tests/behat/adminnistration_navigation.featurenu[@block @block_navigation Feature: Test that admin can see related nodes in Administration block In order to manage As an admin I need to be able to see related nodes in Administration block Background: Given the following "categories" exist: | name | category | idnumber | visible | | cat1 | 0 | cat1 | 1 | And the following "courses" exist: | fullname | shortname | category | visible | | Course 1 | c1 | cat1 | 1 | And the following config values are set as admin: | unaddableblocks | | theme_boost | And I log in as "admin" And I am on site homepage And I turn editing mode on And I add the "Administration" block if not present And I configure the "Administration" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" @javascript Scenario: As admin I must not see question related nodes in Administration. Given the following "activities" exist: | activity | name | intro | course | idnumber | | quiz | Quiz 1 | Quiz 1 for testing the Add menu | c1 | quiz1 | And I am on "Course 1" course homepage Then I should see "Question bank" And I should not see "Questions" And I am on the "Quiz 1" "mod_quiz > view" page And "Question bank" "link" should exist And "Questions" "link" should exist And "Categories" "link" should exist PKX\8c'#tests/behat/view_my_courses.featurenu[@block @block_navigation Feature: View my courses in navigation block In order to navigate to my courses As a student I need my courses displayed in the navigation block Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "categories" exist: | name | category | idnumber | | cat1 | 0 | cat1 | | cat2 | 0 | cat2 | | cat3 | 0 | cat3 | | cat31 | cat3 | cat31 | | cat32 | cat3 | cat32 | | cat33 | cat3 | cat33 | And the following "courses" exist: | fullname | shortname | category | | Course1 | c1 | cat1 | | Course2 | c2 | cat2 | | Course31 | c31 | cat31 | | Course32 | c32 | cat32 | | Course331| c331 | cat33 | | Course332| c332 | cat33 | And the following "course enrolments" exist: | user | course | role | | student1 | c1 | student | | student1 | c31 | student | | student1 | c331 | student | And I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" And I log out @javascript Scenario: The plain list of enrolled courses is shown Given the following config values are set as admin: | navshowmycoursecategories | 0 | And I log in as "student1" When I click on "Dashboard" "link" in the "Navigation" "block" Then I should not see "cat1" in the "Navigation" "block" And I should not see "cat2" in the "Navigation" "block" And I should see "c1" in the "Navigation" "block" And I should see "c31" in the "Navigation" "block" And I should see "c331" in the "Navigation" "block" And I should not see "c2" in the "Navigation" "block" And I should not see "c32" in the "Navigation" "block" And I should not see "c332" in the "Navigation" "block" @javascript Scenario: The nested list of enrolled courses is shown Given the following config values are set as admin: | navshowmycoursecategories | 1 | And I log in as "student1" When I click on "Dashboard" "link" in the "Navigation" "block" Then I should see "cat1" in the "Navigation" "block" And I should see "cat3" in the "Navigation" "block" And I should not see "cat2" in the "Navigation" "block" And I expand "cat3" node And I should see "cat31" in the "Navigation" "block" And I should see "cat33" in the "Navigation" "block" And I should not see "cat32" in the "Navigation" "block" And I expand "cat31" node And I should see "c31" in the "Navigation" "block" And I expand "cat33" node And I should see "c331" in the "Navigation" "block" And I should not see "c332" in the "Navigation" "block" @javascript Scenario: I can expand categories and courses as guest Given the following config values are set as admin: | navshowmycoursecategories | 1 | | navshowallcourses | 1 | And I expand "Courses" node And I should see "cat1" in the "Navigation" "block" And I should see "cat2" in the "Navigation" "block" And I should see "cat3" in the "Navigation" "block" And I should not see "cat31" in the "Navigation" "block" And I should not see "cat32" in the "Navigation" "block" And I should not see "cat331" in the "Navigation" "block" And I should not see "c1" in the "Navigation" "block" And I should not see "c2" in the "Navigation" "block" And I should not see "c31" in the "Navigation" "block" And I should not see "c32" in the "Navigation" "block" When I expand "cat3" node And I expand "cat31" node And I expand "cat1" node Then I should see "cat1" in the "Navigation" "block" And I should see "cat2" in the "Navigation" "block" And I should see "cat3" in the "Navigation" "block" And I should see "cat31" in the "Navigation" "block" And I should see "cat32" in the "Navigation" "block" And I should not see "cat331" in the "Navigation" "block" And I should see "c1" in the "Navigation" "block" And I should not see "c2" in the "Navigation" "block" And I should see "c31" in the "Navigation" "block" And I should not see "c32" in the "Navigation" "block" @javascript Scenario: The course limit setting is applied in the navigation block Given the following config values are set as admin: | navcourselimit | 2 | And I log in as "student1" And I should see "More..." in the "Navigation" "block" When I click on "More..." "link" in the "Navigation" "block" Then I should see "My courses" in the "page-header" "region" @javascript Scenario: The dashboard node is not displayed in the navigation block when it is disabled Given the following config values are set as admin: | enabledashboard | 0 | When I log in as "student1" Then I should not see "Dashboard" in the "Navigation" "block" # Re-enable dashboard and check then it's displayed in the navigation block. And the following config values are set as admin: | enabledashboard | 1 | And I reload the page And I should see "Dashboard" in the "Navigation" "block" PKX\x %tests/behat/participants_link.featurenu[@block @block_navigation Feature: Displaying the link to the Participants page In order to see the course / site participants As a student / admin respectively I need a link to the Participants page be displayed (but only if I can access that page) Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | One | student1@example.com | | student2 | Student | Two | student2@example.com | And the following "courses" exist: | fullname | shortname | | Course1 | C1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" And I log out @javascript Scenario: Course participants link is displayed to enrolled students after expanding the course node When I log in as "student1" And I expand "C1" node Then "Participants" "link" should exist in the "Navigation" "block" And I click on "Participants" "link" in the "Navigation" "block" And I should see "Participants" And "Student One" "link" should exist And "Student Two" "link" should not exist @javascript Scenario: Course participants link is displayed to users depending on role permissions settings Given the following "activities" exist: | activity | course | name | | forum | C1 | Test forum name | And I am on the "Course1" "enrolment methods" page logged in as admin And I click on "Edit" "link" in the "Guest access" "table_row" And I set the following fields to these values: | Allow guest access | Yes | And I press "Save changes" When I am on the "Course1" course page logged in as guest Then I should not see "Participants" in the "Navigation" "block" And I am on the "Test forum name" "forum activity" page And I should not see "Participants" in the "Navigation" "block" And I log out And the following "role capability" exists: | role | guest | | moodle/course:viewparticipants | allow | And I am on the "Course1" course page logged in as guest And I should see "Participants" in the "Navigation" "block" And I am on the "Test forum name" "forum activity" page And I should see "Participants" in the "Navigation" "block" Scenario: Site participants link is displayed to admins When I log in as "admin" Then "Participants" "link" should exist in the "Navigation" "block" And I click on "Participants" "link" in the "Navigation" "block" And I should see "Participants" And "Student One" "link" should exist And "Student Two" "link" should exist @javascript Scenario: Site participants link is not displayed to students (MDL-55667) Given I log in as "admin" And I set the following administration settings values: | defaultfrontpageroleid | Student (student) | And I log out When I log in as "student2" And I expand "Site pages" node Then "Participants" "link" should not exist in the "Navigation" "block" PKX\?$$'tests/behat/expand_courses_node.featurenu[@block @block_navigation Feature: Expand the courses nodes within the navigation block In order to navigate the site As an anonymous user, a guest, a student, and an admin I need to expand the courses node in the navigation block and check the display of courses and categories. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "categories" exist: | name | category | idnumber | visible | | cat1 | 0 | cat1 | 1 | | cat2 | 0 | cat2 | 1 | | cat21 | cat2 | cat21 | 1 | | cat211 | cat21 | cat211 | 1 | | cat3 | 0 | cat3 | 0 | And the following "courses" exist: | fullname | shortname | category | visible | | Course 1 | c1 | cat1 | 1 | | Course 2 | c2 | cat2 | 1 | | Course 3 | c3 | cat21 | 1 | | Course 4 | c4 | cat211 | 1 | | Course 5 | c5 | cat211 | 0 | | Course 6 | c6 | cat211 | 0 | | Course 7 | c7 | cat3 | 1 | | Course 8 | c8 | cat3 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | c1 | teacher | | teacher1 | c3 | teacher | | teacher1 | c5 | teacher | | student1 | c1 | student | | student1 | c2 | student | | student1 | c4 | student | And the following config values are set as admin: | navshowallcourses | 1 | And I log in as "admin" And I am on site homepage And I turn editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" And I turn editing mode off And I am on the "Course 2" "enrolment methods" page And I click on "Edit" "link" in the "Guest access" "table_row" And I set the following fields to these values: | Allow guest access | Yes | And I press "Save changes" And I log out @javascript Scenario: As an anonymous user I expand the courses node to see courses. When I should see "You are not logged in." in the ".logininfo" "css_element" And I should see "Home" in the "Navigation" "block" And I should see "Courses" in the "Navigation" "block" And I expand "Courses" node And I should see "cat1" in the "Navigation" "block" And I should see "cat2" in the "Navigation" "block" And I should not see "cat3" in the "Navigation" "block" And I expand "cat1" node And I expand "cat2" node And I should see "cat21" in the "Navigation" "block" And I expand "cat21" node And I should see "cat211" in the "Navigation" "block" And I expand "cat211" node Then I should see "c1" in the "Navigation" "block" And I should see "c2" in the "Navigation" "block" And I should see "c3" in the "Navigation" "block" And I should see "c4" in the "Navigation" "block" And I should not see "c5" in the "Navigation" "block" And I should not see "c6" in the "Navigation" "block" And navigation node "c1" should not be expandable And navigation node "c2" should not be expandable And navigation node "c3" should not be expandable And navigation node "c4" should not be expandable @javascript Scenario: As the admin user I expand the courses and category nodes to see courses. When I log in as "admin" And I am on site homepage And I should see "Site home" in the "Navigation" "block" And I should see "Courses" in the "Navigation" "block" And I expand "Courses" node And I should see "cat1" in the "Navigation" "block" And I should see "cat2" in the "Navigation" "block" And I should see "cat3" in the "Navigation" "block" And I expand "cat1" node And I expand "cat2" node And I expand "cat3" node And I should see "cat21" in the "Navigation" "block" And I expand "cat21" node And I should see "cat211" in the "Navigation" "block" And I expand "cat211" node Then I should see "c1" in the "Navigation" "block" And I should see "c2" in the "Navigation" "block" And I should see "c3" in the "Navigation" "block" And I should see "c4" in the "Navigation" "block" And I should see "c5" in the "Navigation" "block" And I should see "c6" in the "Navigation" "block" And I should see "c7" in the "Navigation" "block" And I should see "c8" in the "Navigation" "block" And navigation node "c1" should be expandable And navigation node "c2" should be expandable And navigation node "c3" should be expandable And navigation node "c4" should be expandable And navigation node "c5" should be expandable And navigation node "c6" should be expandable And navigation node "c7" should be expandable And navigation node "c8" should be expandable @javascript Scenario: As teacher1 I expand the courses and category nodes to see courses. When I log in as "teacher1" And I am on site homepage And I should see "Site home" in the "Navigation" "block" And I should see "Courses" in the "Navigation" "block" And I expand "Courses" node And I should see "cat1" in the "Navigation" "block" And I should see "cat2" in the "Navigation" "block" And I should not see "cat3" in the "Navigation" "block" And I expand "cat1" node And I expand "cat2" node And I should see "cat21" in the "Navigation" "block" And I expand "cat21" node And I should see "cat211" in the "Navigation" "block" And I expand "cat211" node Then I should see "c1" in the "Navigation" "block" And I should see "c2" in the "Navigation" "block" And I should see "c3" in the "Navigation" "block" And I should see "c4" in the "Navigation" "block" And I should see "c5" in the "Navigation" "block" And I should not see "c6" in the "Navigation" "block" And I should not see "c7" in the "Navigation" "block" And I should not see "c8" in the "Navigation" "block" And navigation node "c1" should be expandable And navigation node "c2" should be expandable And navigation node "c3" should be expandable And navigation node "c4" should not be expandable And navigation node "c5" should be expandable @javascript Scenario: As student1 I expand the courses and category nodes to see courses. When I log in as "student1" And I am on site homepage And I should see "Site home" in the "Navigation" "block" And I should see "Courses" in the "Navigation" "block" And I expand "Courses" node And I should see "cat1" in the "Navigation" "block" And I should see "cat2" in the "Navigation" "block" And I should not see "cat3" in the "Navigation" "block" And I expand "cat1" node And I expand "cat2" node And I should see "cat21" in the "Navigation" "block" And I expand "cat21" node And I should see "cat211" in the "Navigation" "block" And I expand "cat211" node Then I should see "c1" in the "Navigation" "block" And I should see "c2" in the "Navigation" "block" And I should see "c3" in the "Navigation" "block" And I should see "c4" in the "Navigation" "block" And I should not see "c5" in the "Navigation" "block" And I should not see "c6" in the "Navigation" "block" And I should not see "c7" in the "Navigation" "block" And I should not see "c8" in the "Navigation" "block" And navigation node "c1" should be expandable And navigation node "c2" should be expandable And navigation node "c3" should not be expandable And navigation node "c4" should be expandable @javascript Scenario: As guest I expand the courses and category nodes to see courses. When I log in as "guest" And I am on site homepage And I should see "Site home" in the "Navigation" "block" And I should see "Courses" in the "Navigation" "block" And I expand "Courses" node And I should see "cat1" in the "Navigation" "block" And I should see "cat2" in the "Navigation" "block" And I should not see "cat3" in the "Navigation" "block" And I expand "cat1" node And I expand "cat2" node And I should see "cat21" in the "Navigation" "block" And I expand "cat21" node And I should see "cat211" in the "Navigation" "block" And I expand "cat211" node Then I should see "c1" in the "Navigation" "block" And I should see "c2" in the "Navigation" "block" And I should see "c3" in the "Navigation" "block" And I should see "c4" in the "Navigation" "block" And I should not see "c5" in the "Navigation" "block" And I should not see "c6" in the "Navigation" "block" And I should not see "c7" in the "Navigation" "block" And I should not see "c8" in the "Navigation" "block" And navigation node "c1" should not be expandable And navigation node "c2" should be expandable And navigation node "c3" should not be expandable And navigation node "c4" should not be expandable PKX\J Q1Q1block_navigation.phpnu[. /** * This file contains classes used to manage the navigation structures in Moodle * and was introduced as part of the changes occuring in Moodle 2.0 * * @since Moodle 2.0 * @package block_navigation * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * The global navigation tree block class * * Used to produce the global navigation block new to Moodle 2.0 * * @package block_navigation * @category navigation * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class block_navigation extends block_base { /** @var int This allows for multiple navigation trees */ public static $navcount; /** @var string The name of the block */ public $blockname = null; /** @var bool A switch to indicate whether content has been generated or not. */ protected $contentgenerated = false; /** @var bool|null variable for checking if the block is docked*/ protected $docked = null; /** @var int Trim characters from the right */ const TRIM_RIGHT = 1; /** @var int Trim characters from the left */ const TRIM_LEFT = 2; /** @var int Trim characters from the center */ const TRIM_CENTER = 3; /** * Set the initial properties for the block */ function init() { $this->blockname = get_class($this); $this->title = get_string('pluginname', $this->blockname); } /** * All multiple instances of this block * @return bool Returns false */ function instance_allow_multiple() { return false; } /** * Set the applicable formats for this block to all * @return array */ function applicable_formats() { return array('all' => true); } /** * Allow the user to configure a block instance * @return bool Returns true */ function instance_allow_config() { return true; } /** * The navigation block cannot be hidden by default as it is integral to * the navigation of Moodle. * * @return false */ function instance_can_be_hidden() { return false; } /** * Find out if an instance can be docked. * * @return bool true or false depending on whether the instance can be docked or not. */ function instance_can_be_docked() { return (parent::instance_can_be_docked() && (empty($this->config->enabledock) || $this->config->enabledock=='yes')); } /** * Gets Javascript that may be required for navigation */ function get_required_javascript() { parent::get_required_javascript(); $arguments = array( 'instanceid' => $this->instance->id ); $this->page->requires->string_for_js('viewallcourses', 'moodle'); $this->page->requires->js_call_amd('block_navigation/navblock', 'init', $arguments); } /** * Gets the content for this block by grabbing it from $this->page * * @return object $this->content */ function get_content() { global $CFG; // First check if we have already generated, don't waste cycles if ($this->contentgenerated === true) { return $this->content; } // JS for navigation moved to the standard theme, the code will probably have to depend on the actual page structure // $this->page->requires->js('/lib/javascript-navigation.js'); // Navcount is used to allow us to have multiple trees although I dont' know why // you would want two trees the same block_navigation::$navcount++; // Check if this block has been docked if ($this->docked === null) { $this->docked = get_user_preferences('nav_in_tab_panel_globalnav'.block_navigation::$navcount, 0); } // Check if there is a param to change the docked state if ($this->docked && optional_param('undock', null, PARAM_INT)==$this->instance->id) { unset_user_preference('nav_in_tab_panel_globalnav'.block_navigation::$navcount); $url = $this->page->url; $url->remove_params(array('undock')); redirect($url); } else if (!$this->docked && optional_param('dock', null, PARAM_INT)==$this->instance->id) { set_user_preferences(array('nav_in_tab_panel_globalnav'.block_navigation::$navcount=>1)); $url = $this->page->url; $url->remove_params(array('dock')); redirect($url); } $trimmode = self::TRIM_RIGHT; $trimlength = 50; if (!empty($this->config->trimmode)) { $trimmode = (int)$this->config->trimmode; } if (!empty($this->config->trimlength)) { $trimlength = (int)$this->config->trimlength; } // Get the navigation object or don't display the block if none provided. if (!$navigation = $this->get_navigation()) { return null; } $expansionlimit = null; if (!empty($this->config->expansionlimit)) { $expansionlimit = $this->config->expansionlimit; $navigation->set_expansion_limit($this->config->expansionlimit); } $this->trim($navigation, $trimmode, $trimlength, ceil($trimlength/2)); // Get the expandable items so we can pass them to JS $expandable = array(); $navigation->find_expandable($expandable); if ($expansionlimit) { foreach ($expandable as $key=>$node) { if ($node['type'] > $expansionlimit && !($expansionlimit == navigation_node::TYPE_COURSE && $node['type'] == $expansionlimit && $node['branchid'] == SITEID)) { unset($expandable[$key]); } } } $limit = 20; if (!empty($CFG->navcourselimit)) { $limit = $CFG->navcourselimit; } $expansionlimit = 0; if (!empty($this->config->expansionlimit)) { $expansionlimit = $this->config->expansionlimit; } $options = array(); $options['linkcategories'] = (!empty($this->config->linkcategories) && $this->config->linkcategories == 'yes'); // Grab the items to display $renderer = $this->page->get_renderer($this->blockname); $this->content = new stdClass(); $this->content->text = $renderer->navigation_tree($navigation, $expansionlimit, $options); // Set content generated to true so that we know it has been done $this->contentgenerated = true; return $this->content; } /** * Returns the navigation * * @return navigation_node The navigation object to display */ protected function get_navigation() { // Initialise (only actually happens if it hasn't already been done yet) $this->page->navigation->initialise(); return clone($this->page->navigation); } /** * Returns the attributes to set for this block * * This function returns an array of HTML attributes for this block including * the defaults. * {@link block_tree::html_attributes()} is used to get the default arguments * and then we check whether the user has enabled hover expansion and add the * appropriate hover class if it has. * * @return array An array of HTML attributes */ public function html_attributes() { $attributes = parent::html_attributes(); if (!empty($this->config->enablehoverexpansion) && $this->config->enablehoverexpansion == 'yes') { $attributes['class'] .= ' block_js_expansion'; } return $attributes; } /** * Trims the text and shorttext properties of this node and optionally * all of its children. * * @param navigation_node $node * @param int $mode One of navigation_node::TRIM_* * @param int $long The length to trim text to * @param int $short The length to trim shorttext to * @param bool $recurse Recurse all children */ public function trim(navigation_node $node, $mode=1, $long=50, $short=25, $recurse=true) { switch ($mode) { case self::TRIM_RIGHT : if (core_text::strlen($node->text)>($long+3)) { // Truncate the text to $long characters $node->text = $this->trim_right($node->text, $long); } if (is_string($node->shorttext) && core_text::strlen($node->shorttext)>($short+3)) { // Truncate the shorttext $node->shorttext = $this->trim_right($node->shorttext, $short); } break; case self::TRIM_LEFT : if (core_text::strlen($node->text)>($long+3)) { // Truncate the text to $long characters $node->text = $this->trim_left($node->text, $long); } if (is_string($node->shorttext) && core_text::strlen($node->shorttext)>($short+3)) { // Truncate the shorttext $node->shorttext = $this->trim_left($node->shorttext, $short); } break; case self::TRIM_CENTER : if (core_text::strlen($node->text)>($long+3)) { // Truncate the text to $long characters $node->text = $this->trim_center($node->text, $long); } if (is_string($node->shorttext) && core_text::strlen($node->shorttext)>($short+3)) { // Truncate the shorttext $node->shorttext = $this->trim_center($node->shorttext, $short); } break; } if ($recurse && $node->children->count()) { foreach ($node->children as &$child) { $this->trim($child, $mode, $long, $short, true); } } } /** * Truncate a string from the left * @param string $string The string to truncate * @param int $length The length to truncate to * @return string The truncated string */ protected function trim_left($string, $length) { return '...'.core_text::substr($string, core_text::strlen($string)-$length, $length); } /** * Truncate a string from the right * @param string $string The string to truncate * @param int $length The length to truncate to * @return string The truncated string */ protected function trim_right($string, $length) { return core_text::substr($string, 0, $length).'...'; } /** * Truncate a string in the center * @param string $string The string to truncate * @param int $length The length to truncate to * @return string The truncated string */ protected function trim_center($string, $length) { $trimlength = ceil($length/2); $start = core_text::substr($string, 0, $trimlength); $end = core_text::substr($string, core_text::strlen($string)-$trimlength); $string = $start.'...'.$end; return $string; } /** * Returns the role that best describes the navigation block... 'navigation' * * @return string 'navigation' */ public function get_aria_role() { return 'navigation'; } /** * Return the plugin config settings for external functions. * * @return stdClass the configs for both the block instance and plugin * @since Moodle 3.8 */ public function get_config_for_external() { // Return all settings for all users since it is safe (no private keys, etc..). $configs = !empty($this->config) ? $this->config : new stdClass(); return (object) [ 'instance' => $configs, 'plugin' => new stdClass(), ]; } } PKz\C$0p!!views/primary.phpnu[. namespace core\navigation\views; use navigation_node; /** * Class primary. * * The primary navigation view is a combination of few components - navigation, output->navbar, * * @package core * @category navigation * @copyright 2021 onwards Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class primary extends view { /** * Initialise the primary navigation node */ public function initialise(): void { global $CFG; if (during_initial_install() || $this->initialised) { return; } $this->id = 'primary_navigation'; $showhomenode = empty($this->page->theme->removedprimarynavitems) || !in_array('home', $this->page->theme->removedprimarynavitems); // We do not need to change the text for the home/dashboard depending on the set homepage. if ($showhomenode) { $sitehome = $this->add(get_string('home'), new \moodle_url('/'), self::TYPE_SYSTEM, null, 'home', new \pix_icon('i/home', '')); } if (isloggedin() && !isguestuser()) { $homepage = get_home_page(); if ($homepage == HOMEPAGE_MY || $homepage == HOMEPAGE_MYCOURSES) { // We need to stop automatic redirection. if ($showhomenode) { $sitehome->action->param('redirect', '0'); } } // Add the dashboard link. $showmyhomenode = !empty($CFG->enabledashboard) && (empty($this->page->theme->removedprimarynavitems) || !in_array('myhome', $this->page->theme->removedprimarynavitems)); if ($showmyhomenode) { $this->add(get_string('myhome'), new \moodle_url('/my/'), self::TYPE_SETTING, null, 'myhome', new \pix_icon('i/dashboard', '')); } // Add the mycourses link. $showcoursesnode = empty($this->page->theme->removedprimarynavitems) || !in_array('courses', $this->page->theme->removedprimarynavitems); if ($showcoursesnode) { $this->add(get_string('mycourses'), new \moodle_url('/my/courses.php'), self::TYPE_ROOTNODE, null, 'mycourses'); } } // Add the calendar link only for guest users. if (isguestuser()) { $this->add(get_string('calendar', 'calendar'), new \moodle_url('/calendar/view.php?view=month'), self::TYPE_ROOTNODE, null, 'calendar'); } $showsiteadminnode = empty($this->page->theme->removedprimarynavitems) || !in_array('siteadminnode', $this->page->theme->removedprimarynavitems); if ($showsiteadminnode && $node = $this->get_site_admin_node()) { // We don't need everything from the node just the initial link. $this->add($node->text, $node->action(), self::TYPE_SITE_ADMIN, null, 'siteadminnode', $node->icon); } // Allow plugins to add nodes to the primary navigation. $hook = new \core\hook\navigation\primary_extend($this); \core\di::get(\core\hook\manager::class)->dispatch($hook); // Search and set the active node. $this->set_active_node(); $this->initialised = true; } /** * Get the site admin node if available. * * @return navigation_node|null */ private function get_site_admin_node(): ?navigation_node { // Add the site admin node. We are using the settingsnav so as to avoid rechecking permissions again. $settingsnav = $this->page->settingsnav; $node = $settingsnav->find('siteadministration', self::TYPE_SITE_ADMIN); if (!$node) { // Try again. This node can exist with 2 different keys. $node = $settingsnav->find('root', self::TYPE_SITE_ADMIN); } return $node ?: null; } /** * Find and set the active node. Initially searches based on URL/explicitly set active node. * If nothing is found, it checks the following: * - If the node is a site page, set 'Home' as active * - If within a course context, set 'My courses' as active * - If within a course category context, set 'Site Admin' (if available) else set 'Home' * - Else if available set site admin as active * - Fallback, set 'Home' as active */ private function set_active_node(): void { global $SITE; $activenode = $this->search_and_set_active_node($this); // If we haven't found an active node based on the standard search. Follow the criteria above. if (!$activenode) { $children = $this->get_children_key_list(); $navactivenode = $this->page->navigation->find_active_node(); $activekey = 'home'; if (isset($navactivenode->parent) && $navactivenode->parent->text == get_string('sitepages')) { $activekey = 'home'; } else if (in_array($this->context->contextlevel, [CONTEXT_COURSE, CONTEXT_MODULE])) { if ($this->page->course->id != $SITE->id) { $activekey = 'courses'; } } else if (in_array('siteadminnode', $children) && $node = $this->get_site_admin_node()) { if ($this->context->contextlevel == CONTEXT_COURSECAT || $node->search_for_active_node(URL_MATCH_EXACT)) { $activekey = 'siteadminnode'; } } else if (in_array('calendar', $children) && $node = $this->find('calendar', self::TYPE_ROOTNODE)) { if ($node->search_for_active_node(URL_MATCH_BASE)) { $activekey = 'calendar'; } } if ($activekey && $activenode = $this->find($activekey, null)) { $activenode->make_active(); } } } /** * Searches all children for the matching active node * * This method recursively traverse through the node tree to * find the node to activate/highlight: * 1. If the user had set primary node key to highlight, it * tries to match this key with the node(s). Hence it would * travel all the nodes. * 2. If no primary key is provided by the dev, then it would * check for the active node set in the tree. * * @param navigation_node $node * @param array $actionnodes navigation nodes array to set active and inactive. * @return navigation_node|null */ private function search_and_set_active_node(navigation_node $node, array &$actionnodes = []): ?navigation_node { global $PAGE; $activekey = $PAGE->get_primary_activate_tab(); if ($activekey) { if ($node->key && ($activekey === $node->key)) { return $node; } } else if ($node->check_if_active()) { return $node; } foreach ($node->children as $child) { $outcome = $this->search_and_set_active_node($child, $actionnodes); if ($outcome !== null) { $outcome->make_active(); $actionnodes['active'] = $outcome; if ($activekey === null) { return $actionnodes['active']; } } else { // If the child is active then make it inactive. if ($child->isactive) { $actionnodes['set_inactive'][] = $child; } } } // If we have successfully found an active node then reset any other nodes to inactive. if (isset($actionnodes['set_inactive']) && isset($actionnodes['active'])) { foreach ($actionnodes['set_inactive'] as $inactivenode) { $inactivenode->make_inactive(); } $actionnodes['set_inactive'] = []; } return ($actionnodes['active'] ?? null); } } PKz\VDBBviews/view.phpnu[. namespace core\navigation\views; use navigation_node; use navigation_node_collection; /** * Class view. * * The base view class which expands on the navigation_node, * * @package core * @category navigation * @copyright 2021 onwards Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class view extends navigation_node { /** @var \stdClass $context the current context */ protected $context; /** @var \moodle_page $page the moodle page that the navigation belongs to */ protected $page; /** @var bool $initialised A switch to see if the navigation node is initialised */ protected $initialised = false; /** @var navigation_node $activenode A string identifier for the active node*/ public $activenode; /** * Function to initialise the respective view * @return void */ abstract public function initialise(): void; /** * navigation constructor. * @param \moodle_page $page */ public function __construct(\moodle_page $page) { global $FULLME; if (during_initial_install()) { return false; } $this->page = $page; $this->context = $this->page->context; $this->children = new navigation_node_collection(); } /** * Get the leaf nodes for the nav view * * @param navigation_node $source The settingsnav OR navigation object * @param array $nodes An array of nodes to fetch from the source which specifies the node type and final order * @return array $nodesordered The fetched nodes ordered based on final specification. */ protected function get_leaf_nodes(navigation_node $source, array $nodes): array { $nodesordered = []; foreach ($nodes as $type => $leaves) { foreach ($leaves as $leaf => $location) { if ($node = $source->find($leaf, $type)) { $nodesordered["$location"] = $nodesordered["$location"] ?? $node; } } } return $nodesordered; } /** * Scan the given node for the active node. It starts first with a strict search and then switches to a base search if * required. * * @param navigation_node $node The node to scan. * @return navigation_node|null The active node or null. */ protected function scan_for_active_node(navigation_node $node): ?navigation_node { $result = $this->active_node_scan($node); if (!is_null($result)) { return $result; } else { return $this->active_node_scan($node, URL_MATCH_BASE); } } /** * This function recursively scans nodes until it finds the active node or there * are no more nodes. We are using a custom implementation here to adjust the strictness * and also because we need the parent node and not the specific child node in the new views. * e.g. Structure for site admin, * SecondaryNav * - Site Admin * - Users * - User policies * - Courses * In the above example, if we are on the 'User Policies' page, the active node should be 'Users' * * @param navigation_node $node * @param int $strictness How stict to be with the scan for the active node. * @return navigation_node|null */ protected function active_node_scan(navigation_node $node, int $strictness = URL_MATCH_EXACT): ?navigation_node { $result = null; $activekey = $this->page->get_secondary_active_tab(); if ($activekey) { if ($node->key && $activekey === $node->key) { return $node; } } else if ($node->check_if_active($strictness)) { return $node; // No need to continue, exit function. } foreach ($node->children as $child) { if ($this->active_node_scan($child, $strictness)) { // If node is one of the new views then set the active node to the child. if (!$node instanceof view) { $node->make_active(); $result = $node; } else { $child->make_active(); $this->activenode = $child; $result = $child; } // If the secondary active tab not set then just return the result (fallback). if ($activekey === null) { return $result; } } else { // Make sure to reset the active state. $child->make_inactive(); } } return $result; } } PKz\k99output/primary.phpnu[. namespace core\navigation\output; use renderable; use renderer_base; use templatable; use custom_menu; use filter_manager; /** * Primary navigation renderable * * This file combines primary nav, custom menu, lang menu and * usermenu into a standardized format for the frontend * * @package core * @category navigation * @copyright 2021 onwards Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class primary implements renderable, templatable { /** @var \moodle_page $page the moodle page that the navigation belongs to */ private $page = null; /** * primary constructor. * @param \moodle_page $page */ public function __construct($page) { $this->page = $page; } /** * Combine the various menus into a standardized output. * * @param renderer_base|null $output * @return array */ public function export_for_template(?renderer_base $output = null): array { if (!$output) { $output = $this->page->get_renderer('core'); } $menudata = (object) $this->merge_primary_and_custom($this->get_primary_nav(), $this->get_custom_menu($output)); $moremenu = new \core\navigation\output\more_menu($menudata, 'navbar-nav', false); $mobileprimarynav = $this->merge_primary_and_custom($this->get_primary_nav(), $this->get_custom_menu($output), true); $languagemenu = new \core\output\language_menu($this->page); return [ 'mobileprimarynav' => $mobileprimarynav, 'moremenu' => $moremenu->export_for_template($output), 'lang' => !isloggedin() || isguestuser() ? $languagemenu->export_for_template($output) : [], 'user' => $this->get_user_menu($output), ]; } /** * Get the primary nav object and standardize the output * * @param \navigation_node|null $parent used for nested nodes, by default the primarynav node * @return array */ protected function get_primary_nav($parent = null): array { if ($parent === null) { $parent = $this->page->primarynav; } $nodes = []; foreach ($parent->children as $node) { $children = $this->get_primary_nav($node); $activechildren = array_filter($children, function($child) { return !empty($child['isactive']); }); if ($node->preceedwithhr && count($nodes) && empty($nodes[count($nodes) - 1]['divider'])) { $nodes[] = ['divider' => true]; } $nodes[] = [ 'title' => $node->get_title(), 'url' => $node->action(), 'text' => $node->text, 'icon' => $node->icon, 'isactive' => $node->isactive || !empty($activechildren), 'key' => $node->key, 'sort' => $node->key, 'children' => $children, 'haschildren' => !empty($children) ? 1 : 0, ]; } return $nodes; } /** * Custom menu items reside on the same level as the original nodes. * Fetch and convert the nodes to a standardised array. * * @param renderer_base $output * @return array */ protected function get_custom_menu(renderer_base $output): array { global $CFG; // Early return if a custom menu does not exists. if (empty($CFG->custommenuitems)) { return []; } $custommenuitems = $CFG->custommenuitems; // If filtering of the primary custom menu is enabled, apply only the string filters. if (!empty($CFG->navfilter && !empty($CFG->stringfilters))) { // Apply filters that are enabled for Content and Headings. $filtermanager = \filter_manager::instance(); $custommenuitems = $filtermanager->filter_string($custommenuitems, \context_system::instance()); } $currentlang = current_language(); $custommenunodes = custom_menu::convert_text_to_menu_nodes($custommenuitems, $currentlang); $nodes = []; foreach ($custommenunodes as $node) { $nodes[] = $node->export_for_template($output); } return $nodes; } /** * When defining custom menu items, the active flag is not obvserved correctly. Therefore, the merge of the primary * and custom navigation must be handled a bit smarter. Change the "isactive" flag of the nodes (this may set by * default in the primary nav nodes but is entirely missing in the custom nav nodes). * Set the $expandedmenu argument to true when the menu for the mobile template is build. * * @param array $primary * @param array $custom * @param bool $expandedmenu * @return array */ protected function merge_primary_and_custom(array $primary, array $custom, bool $expandedmenu = false): array { if (empty($custom)) { return $primary; // No custom nav, nothing to merge. } // Remember the amount of primary nodes and whether we changed the active flag in the custom menu nodes. $primarylen = count($primary); $changed = false; foreach (array_keys($custom) as $i) { if (!$changed) { if ($this->flag_active_nodes($custom[$i], $expandedmenu)) { $changed = true; } } $primary[] = $custom[$i]; } // In case some custom node is active, mark all primary nav elements as inactive. if ($changed) { for ($i = 0; $i < $primarylen; $i++) { $primary[$i]['isactive'] = false; } } return $primary; } /** * Recursive checks if any of the children is active. If that's the case this node (the parent) is active as * well. If the node has no children, check if the node itself is active. Use pass by reference for the node * object because we actively change/set the "isactive" flag inside the method and this needs to be kept at the * callers side. * Set $expandedmenu to true, if the mobile menu is done, in this case the active flag gets the node that is * actually active, while the parent hierarchy of the active node gets the flag isopen. * * @param object $node * @param bool $expandedmenu * @return bool */ protected function flag_active_nodes(object $node, bool $expandedmenu = false): bool { global $FULLME; $active = false; foreach (array_keys($node->children ?? []) as $c) { if ($this->flag_active_nodes($node->children[$c], $expandedmenu)) { $active = true; } } // One of the children is active, so this node (the parent) is active as well. if ($active) { if ($expandedmenu) { $node->isopen = true; } else { $node->isactive = true; } return true; } // By default, the menu item node to check is not active. $node->isactive = false; // Check if the node url matches the called url. The node url may omit the trailing index.php, therefore check // this as well. if (empty($node->url)) { // Current menu node has no url set, so it can't be active. return false; } $nodeurl = parse_url($node->url); $current = parse_url($FULLME ?? ''); $pathmatches = false; // Check for same host names before comparing the path. $currenthost = array_key_exists('host', $current) ? strtolower($current['host']) : ''; $nodehost = array_key_exists('host', $nodeurl) ? strtolower($nodeurl['host']) : ''; if ($currenthost !== $nodehost) { return false; } // Exact match of the path of node and current url. $nodepath = $nodeurl['path'] ?? '/'; $currentpath = $current['path'] ?? '/'; if ($nodepath === $currentpath) { $pathmatches = true; } // The current url may be trailed by a index.php, otherwise it's the same as the node path. if (!$pathmatches && $nodepath . 'index.php' === $currentpath) { $pathmatches = true; } // No path did match, so the node can't be active. if (!$pathmatches) { return false; } // We are here because the path matches, so now look at the query string. $nodequery = $nodeurl['query'] ?? ''; $currentquery = $current['query'] ?? ''; // If the node has no query string defined, then the patch match is sufficient. if (empty($nodeurl['query'])) { $node->isactive = true; return true; } // If the node contains a query string then also the current url must match this query. if ($nodequery === $currentquery) { $node->isactive = true; } return $node->isactive; } /** * Get/Generate the user menu. * * This is leveraging the data from user_get_user_navigation_info and the logic in $OUTPUT->user_menu() * * @param renderer_base $output * @return array */ public function get_user_menu(renderer_base $output): array { global $CFG, $USER, $PAGE; require_once($CFG->dirroot . '/user/lib.php'); $usermenudata = []; $submenusdata = []; $info = user_get_user_navigation_info($USER, $PAGE); if (isset($info->unauthenticateduser)) { $info->unauthenticateduser['content'] = get_string($info->unauthenticateduser['content']); $info->unauthenticateduser['url'] = get_login_url(); return (array) $info; } // Gather all the avatar data to be displayed in the user menu. $usermenudata['avatardata'][] = [ 'content' => $info->metadata['useravatar'], 'classes' => 'current' ]; $usermenudata['userfullname'] = $info->metadata['realuserfullname'] ?? $info->metadata['userfullname']; // Logged in as someone else. if ($info->metadata['asotheruser']) { $usermenudata['avatardata'][] = [ 'content' => $info->metadata['realuseravatar'], 'classes' => 'realuser' ]; $usermenudata['metadata'][] = [ 'content' => get_string('loggedinas', 'moodle', $info->metadata['userfullname']), 'classes' => 'viewingas' ]; } // Gather all the meta data to be displayed in the user menu. $metadata = [ 'asotherrole' => [ 'value' => 'rolename', 'class' => 'role role-##GENERATEDCLASS##', ], 'userloginfail' => [ 'value' => 'userloginfail', 'class' => 'loginfailures', ], 'asmnetuser' => [ 'value' => 'mnetidprovidername', 'class' => 'mnet mnet-##GENERATEDCLASS##', ], ]; foreach ($metadata as $key => $value) { if (!empty($info->metadata[$key])) { $content = $info->metadata[$value['value']] ?? ''; $generatedclass = strtolower(preg_replace('#[ ]+#', '-', trim($content))); $customclass = str_replace('##GENERATEDCLASS##', $generatedclass, ($value['class'] ?? '')); $usermenudata['metadata'][] = [ 'content' => $content, 'classes' => $customclass ]; } } $modifiedarray = array_map(function($value) { $value->divider = $value->itemtype == 'divider'; $value->link = $value->itemtype == 'link'; if (isset($value->pix) && !empty($value->pix)) { $value->pixicon = $value->pix; unset($value->pix); } return $value; }, $info->navitems); // Include the language menu as a submenu within the user menu. $languagemenu = new \core\output\language_menu($this->page); $langmenu = $languagemenu->export_for_template($output); if (!empty($langmenu)) { $languageitems = $langmenu['items']; // If there are available languages, generate the data for the the language selector submenu. if (!empty($languageitems)) { $langsubmenuid = uniqid(); // Generate the data for the link to language selector submenu. $language = (object) [ 'itemtype' => 'submenu-link', 'submenuid' => $langsubmenuid, 'title' => get_string('language'), 'divider' => false, 'submenulink' => true, ]; // Place the link before the 'Log out' menu item which is either the last item in the menu or // second to last when 'Switch roles' is available. $menuposition = count($modifiedarray) - 1; if (has_capability('moodle/role:switchroles', $PAGE->context)) { $menuposition = count($modifiedarray) - 2; } array_splice($modifiedarray, $menuposition, 0, [$language]); // Generate the data for the language selector submenu. $submenusdata[] = (object)[ 'id' => $langsubmenuid, 'title' => get_string('languageselector'), 'items' => $languageitems, ]; } } // Add divider before the last item. $modifiedarray[count($modifiedarray) - 2]->divider = true; $usermenudata['items'] = $modifiedarray; $usermenudata['submenus'] = array_values($submenusdata); return $usermenudata; } } PKz\ output/more_menu.phpnu[. namespace core\navigation\output; use renderable; use renderer_base; use templatable; use custom_menu; /** * more menu navigation renderable * * @package core * @category navigation * @copyright 2021 onwards Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class more_menu implements renderable, templatable { protected $content; protected $navbarstyle; protected $haschildren; protected $istablist; /** * Constructor for this class. * * @param object $content Navigation objects. * @param string $navbarstyle class name. * @param bool $haschildren The content has children. * @param bool $istablist When true, the more menu should be rendered and behave with a tablist ARIA role. * If false, it's rendered with a menubar ARIA role. Defaults to false. */ public function __construct(object $content, string $navbarstyle, bool $haschildren = true, bool $istablist = false) { $this->content = $content; $this->navbarstyle = $navbarstyle; $this->haschildren = $haschildren; $this->istablist = $istablist; } /** * Return data for rendering a template. * * @param renderer_base $output The output * @return array Data for rendering a template */ public function export_for_template(renderer_base $output): array { $data = [ 'navbarstyle' => $this->navbarstyle, 'istablist' => $this->istablist, ]; if ($this->haschildren) { // The node collection doesn't have anything to render so exit now. if (!isset($this->content->children) || count($this->content->children) == 0) { return []; } // Find all nodes that have children and are defined to show the children in a submenu. // For each of these nodes we would like to display a dropdown menu and in order to achieve that // (as required by the template) we need to set the node's property 'moremenuid' to a new unique value and // 'haschildren' to true. foreach ($this->content->children as &$item) { if ($item->showchildreninsubmenu && isset($this->content->children) && count($this->content->children) > 0) { $item->moremenuid = uniqid(); $item->haschildren = true; } } $data['nodecollection'] = $this->content; } else { $data['nodearray'] = (array) $this->content; // If there is no node array to render then return an empty array. if (empty($data['nodearray'])) { return []; } } $data['moremenuid'] = uniqid(); return $data; } } PK5\ƕzzviews/secondary.phpnu[PKX\ekk styles.cssnu[PKX\ׂf!f! brenderer.phpnu[PKX\l db/access.phpnu[PKX\{o< Vdb/upgrade.phpnu[PKX\ylang/en/block_navigation.phpnu[PKX\p˻> >  edit_form.phpnu[PKX\# eversion.phpnu[PKX\ Aclasses/privacy/provider.phpnu[PKX\ř}#amd/src/navblock.jsnu[PKX\f[ɟ+amd/src/nav_loader.jsnu[PKX\XYu4amd/src/site_admin_loader.jsnu[PKX\4!;amd/src/ajax_response_renderer.jsnu[PKX\L  "Samd/build/site_admin_loader.min.jsnu[PKX\&T CWamd/build/navblock.min.js.mapnu[PKX\ 'Xaamd/build/ajax_response_renderer.min.jsnu[PKX\c%O#O#+smamd/build/ajax_response_renderer.min.js.mapnu[PKX\Ϳr amd/build/nav_loader.min.js.mapnu[PKX\xwamd/build/navblock.min.jsnu[PKX\{i &amd/build/site_admin_loader.min.js.mapnu[PKX\1-kamd/build/nav_loader.min.jsnu[PKX\h=.tests/behat/adminnistration_navigation.featurenu[PKX\8c'#tests/behat/view_my_courses.featurenu[PKX\x %;tests/behat/participants_link.featurenu[PKX\?$$'atests/behat/expand_courses_node.featurenu[PKX\J Q1Q1block_navigation.phpnu[PKz\C$0p!!J1views/primary.phpnu[PKz\VDBBzSviews/view.phpnu[PKz\k99houtput/primary.phpnu[PKz\ Ƣoutput/more_menu.phpnu[PKy