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