Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 400 and 402]

   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 theme_boost;
  18  
  19  use core\navigation\views\view;
  20  use navigation_node;
  21  use moodle_url;
  22  use action_link;
  23  use lang_string;
  24  
  25  /**
  26   * Creates a navbar for boost that allows easy control of the navbar items.
  27   *
  28   * @package    theme_boost
  29   * @copyright  2021 Adrian Greeve <adrian@moodle.com>
  30   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   */
  32  class boostnavbar implements \renderable {
  33  
  34      /** @var array The individual items of the navbar. */
  35      protected $items = [];
  36      /** @var moodle_page The current moodle page. */
  37      protected $page;
  38  
  39      /**
  40       * Takes a navbar object and picks the necessary parts for display.
  41       *
  42       * @param \moodle_page $page The current moodle page.
  43       */
  44      public function __construct(\moodle_page $page) {
  45          $this->page = $page;
  46          foreach ($this->page->navbar->get_items() as $item) {
  47              $this->items[] = $item;
  48          }
  49          $this->prepare_nodes_for_boost();
  50      }
  51  
  52      /**
  53       * Prepares the navigation nodes for use with boost.
  54       */
  55      protected function prepare_nodes_for_boost(): void {
  56          global $PAGE;
  57  
  58          // Remove the navbar nodes that already exist in the primary navigation menu.
  59          $this->remove_items_that_exist_in_navigation($PAGE->primarynav);
  60  
  61          // Defines whether section items with an action should be removed by default.
  62          $removesections = true;
  63  
  64          if ($this->page->context->contextlevel == CONTEXT_COURSECAT) {
  65              // Remove the 'Permissions' navbar node in the Check permissions page.
  66              if ($this->page->pagetype === 'admin-roles-check') {
  67                  $this->remove('permissions');
  68              }
  69          }
  70          if ($this->page->context->contextlevel == CONTEXT_COURSE) {
  71              // Remove any duplicate navbar nodes.
  72              $this->remove_duplicate_items();
  73              // Remove 'My courses' and 'Courses' if we are in the course context.
  74              $this->remove('mycourses');
  75              $this->remove('courses');
  76              // Remove the course category breadcrumb nodes.
  77              foreach ($this->items as $key => $item) {
  78                  // Remove if it is a course category breadcrumb node.
  79                  $this->remove($item->key, \breadcrumb_navigation_node::TYPE_CATEGORY);
  80              }
  81              // Remove the course breadcrumb node.
  82              $this->remove($this->page->course->id, \breadcrumb_navigation_node::TYPE_COURSE);
  83              // Remove the navbar nodes that already exist in the secondary navigation menu.
  84              $this->remove_items_that_exist_in_navigation($PAGE->secondarynav);
  85  
  86              switch ($this->page->pagetype) {
  87                  case 'group-groupings':
  88                  case 'group-grouping':
  89                  case 'group-overview':
  90                  case 'group-assign':
  91                      // Remove the 'Groups' navbar node in the Groupings, Grouping, group Overview and Assign pages.
  92                      $this->remove('groups');
  93                  case 'backup-backup':
  94                  case 'backup-restorefile':
  95                  case 'backup-copy':
  96                  case 'course-reset':
  97                      // Remove the 'Import' navbar node in the Backup, Restore, Copy course and Reset pages.
  98                      $this->remove('import');
  99                  case 'course-user':
 100                      $this->remove('mygrades');
 101                      $this->remove('grades');
 102              }
 103          }
 104  
 105          // Remove 'My courses' if we are in the module context.
 106          if ($this->page->context->contextlevel == CONTEXT_MODULE) {
 107              $this->remove('mycourses');
 108              $this->remove('courses');
 109              // Remove the course category breadcrumb nodes.
 110              foreach ($this->items as $key => $item) {
 111                  // Remove if it is a course category breadcrumb node.
 112                  $this->remove($item->key, \breadcrumb_navigation_node::TYPE_CATEGORY);
 113              }
 114              $courseformat = course_get_format($this->page->course)->get_course();
 115              // Section items can be only removed if a course layout (coursedisplay) is not explicitly set in the
 116              // given course format or the set course layout is not 'One section per page'.
 117              $removesections = !isset($courseformat->coursedisplay) ||
 118                  $courseformat->coursedisplay != COURSE_DISPLAY_MULTIPAGE;
 119              if ($removesections) {
 120                  // If the course sections are removed, we need to add the anchor of current section to the Course.
 121                  $coursenode = $this->get_item($this->page->course->id);
 122                  if (!is_null($coursenode) && $this->page->cm->sectionnum !== null) {
 123                      $coursenode->action = course_get_format($this->page->course)->get_view_url($this->page->cm->sectionnum);
 124                  }
 125              }
 126          }
 127  
 128          if ($this->page->context->contextlevel == CONTEXT_SYSTEM) {
 129              // Remove the navbar nodes that already exist in the secondary navigation menu.
 130              $this->remove_items_that_exist_in_navigation($PAGE->secondarynav);
 131          }
 132  
 133          // Set the designated one path for courses.
 134          $mycoursesnode = $this->get_item('mycourses');
 135          if (!is_null($mycoursesnode)) {
 136              $url = new \moodle_url('/my/courses.php');
 137              $mycoursesnode->action = $url;
 138              $mycoursesnode->text = get_string('mycourses');
 139          }
 140  
 141          $this->remove_no_link_items($removesections);
 142  
 143          // Don't display the navbar if there is only one item. Apparently this is bad UX design.
 144          if ($this->item_count() <= 1) {
 145              $this->clear_items();
 146              return;
 147          }
 148  
 149          // Make sure that the last item is not a link. Not sure if this is always a good idea.
 150          $this->remove_last_item_action();
 151      }
 152  
 153      /**
 154       * Get all the boostnavbaritem elements.
 155       *
 156       * @return boostnavbaritem[] Boost navbar items.
 157       */
 158      public function get_items(): array {
 159          return $this->items;
 160      }
 161  
 162      /**
 163       * Removes all navigation items out of this boost navbar
 164       */
 165      protected function clear_items(): void {
 166          $this->items = [];
 167      }
 168  
 169      /**
 170       * Retrieve a single navbar item.
 171       *
 172       * @param  string|int $key The identifier of the navbar item to return.
 173       * @return \breadcrumb_navigation_node|null The navbar item.
 174       */
 175      protected function get_item($key): ?\breadcrumb_navigation_node {
 176          foreach ($this->items as $item) {
 177              if ($key === $item->key) {
 178                  return $item;
 179              }
 180          }
 181          return null;
 182      }
 183  
 184      /**
 185       * Counts all of the navbar items.
 186       *
 187       * @return int How many navbar items there are.
 188       */
 189      protected function item_count(): int {
 190          return count($this->items);
 191      }
 192  
 193      /**
 194       * Remove a boostnavbaritem from the boost navbar.
 195       *
 196       * @param  string|int $itemkey An identifier for the boostnavbaritem
 197       * @param  int|null $itemtype An additional type identifier for the boostnavbaritem (optional)
 198       */
 199      protected function remove($itemkey, ?int $itemtype = null): void {
 200  
 201          $itemfound = false;
 202          foreach ($this->items as $key => $item) {
 203              if ($item->key === $itemkey) {
 204                  // If a type identifier is also specified, check whether the type of the breadcrumb item matches the
 205                  // specified type. Skip if types to not match.
 206                  if (!is_null($itemtype) && $item->type !== $itemtype) {
 207                      continue;
 208                  }
 209                  unset($this->items[$key]);
 210                  $itemfound = true;
 211                  break;
 212              }
 213          }
 214          if (!$itemfound) {
 215              return;
 216          }
 217  
 218          $itemcount = $this->item_count();
 219          if ($itemcount <= 0) {
 220              return;
 221          }
 222  
 223          $this->items = array_values($this->items);
 224          // Set the last item to last item if it is not.
 225          $lastitem = $this->items[$itemcount - 1];
 226          if (!$lastitem->is_last()) {
 227              $lastitem->set_last(true);
 228          }
 229      }
 230  
 231      /**
 232       * Removes the action from the last item of the boostnavbaritem.
 233       */
 234      protected function remove_last_item_action(): void {
 235          $item = end($this->items);
 236          $item->action = null;
 237          reset($this->items);
 238      }
 239  
 240      /**
 241       * Returns the second last navbar item. This is for use in the mobile view where we are showing just the second
 242       * last item in the breadcrumb navbar.
 243       *
 244       * @return breakcrumb_navigation_node|null The second last navigation node.
 245       */
 246      public function get_penultimate_item(): ?\breadcrumb_navigation_node {
 247          $number = $this->item_count() - 2;
 248          return ($number >= 0) ? $this->items[$number] : null;
 249      }
 250  
 251      /**
 252       * Remove items that have no actions associated with them and optionally remove items that are sections.
 253       *
 254       * The only exception is the last item in the list which may not have a link but needs to be displayed.
 255       *
 256       * @param bool $removesections Whether section items should be also removed (only applies when they have an action)
 257       */
 258      protected function remove_no_link_items(bool $removesections = true): void {
 259          foreach ($this->items as $key => $value) {
 260              if (!$value->is_last() &&
 261                      (!$value->has_action() || ($value->type == \navigation_node::TYPE_SECTION && $removesections))) {
 262                  unset($this->items[$key]);
 263              }
 264          }
 265          $this->items = array_values($this->items);
 266      }
 267  
 268      /**
 269       * Remove breadcrumb items that already exist in a given navigation view.
 270       *
 271       * This method removes the breadcrumb items that have a text => action match in a given navigation view
 272       * (primary or secondary).
 273       *
 274       * @param view $navigationview The navigation view object.
 275       */
 276      protected function remove_items_that_exist_in_navigation(view $navigationview): void {
 277          // Loop through the navigation view items and create a 'text' => 'action' array which will be later used
 278          // to compare whether any of the breadcrumb items matches these pairs.
 279          $navigationviewitems = [];
 280          foreach ($navigationview->children as $child) {
 281              list($childtext, $childaction) = $this->get_node_text_and_action($child);
 282              if ($childaction) {
 283                  $navigationviewitems[$childtext] = $childaction;
 284              }
 285          }
 286          // Loop through the breadcrumb items and if the item's 'text' and 'action' values matches with any of the
 287          // existing navigation view items, remove it from the breadcrumbs.
 288          foreach ($this->items as $item) {
 289              list($itemtext, $itemaction) = $this->get_node_text_and_action($item);
 290              if ($itemaction) {
 291                  if (array_key_exists($itemtext, $navigationviewitems) &&
 292                          $navigationviewitems[$itemtext] === $itemaction) {
 293                      $this->remove($item->key);
 294                  }
 295              }
 296          }
 297      }
 298  
 299      /**
 300       * Remove duplicate breadcrumb items.
 301       *
 302       * This method looks for breadcrumb items that have identical text and action values and removes the first item.
 303       */
 304      protected function remove_duplicate_items(): void {
 305          $taken = [];
 306          // Reverse the order of the items before filtering so that the first occurrence is removed instead of the last.
 307          $filtereditems = array_values(array_filter(array_reverse($this->items), function($item) use (&$taken) {
 308              list($itemtext, $itemaction) = $this->get_node_text_and_action($item);
 309              if ($itemaction) {
 310                  if (array_key_exists($itemtext, $taken) && $taken[$itemtext] === $itemaction) {
 311                      return false;
 312                  }
 313                  $taken[$itemtext] = $itemaction;
 314              }
 315              return true;
 316          }));
 317          // Reverse back the order.
 318          $this->items = array_reverse($filtereditems);
 319      }
 320  
 321      /**
 322       * Helper function that returns an array of the text and the outputted action url (if exists) for a given
 323       * navigation node.
 324       *
 325       * @param navigation_node $node The navigation node object.
 326       * @return array
 327       */
 328      protected function get_node_text_and_action(navigation_node $node): array {
 329          $text = $node->text instanceof lang_string ? $node->text->out() : $node->text;
 330          $action = null;
 331          if ($node->has_action()) {
 332              if ($node->action instanceof moodle_url) {
 333                  $action = $node->action->out();
 334              } else if ($node->action instanceof action_link) {
 335                  $action = $node->action->url->out();
 336              } else {
 337                  $action = $node->action;
 338              }
 339          }
 340          return [$text, $action];
 341      }
 342  }