Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace 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 node.
  77              $this->remove($this->page->course->category, \breadcrumb_navigation_node::TYPE_CATEGORY);
  78              // Remove the course breadcrumb node.
  79              $this->remove($this->page->course->id, \breadcrumb_navigation_node::TYPE_COURSE);
  80              // Remove the navbar nodes that already exist in the secondary navigation menu.
  81              $this->remove_items_that_exist_in_navigation($PAGE->secondarynav);
  82  
  83              switch ($this->page->pagetype) {
  84                  case 'group-groupings':
  85                  case 'group-grouping':
  86                  case 'group-overview':
  87                  case 'group-assign':
  88                      // Remove the 'Groups' navbar node in the Groupings, Grouping, group Overview and Assign pages.
  89                      $this->remove('groups');
  90                  case 'backup-backup':
  91                  case 'backup-restorefile':
  92                  case 'backup-copy':
  93                  case 'course-reset':
  94                      // Remove the 'Import' navbar node in the Backup, Restore, Copy course and Reset pages.
  95                      $this->remove('import');
  96                  case 'course-user':
  97                      $this->remove('mygrades');
  98                      $this->remove('grades');
  99              }
 100          }
 101  
 102          // Remove 'My courses' if we are in the module context.
 103          if ($this->page->context->contextlevel == CONTEXT_MODULE) {
 104              $this->remove('mycourses');
 105              $this->remove('courses');
 106              // Remove the course category breadcrumb node.
 107              $this->remove($this->page->course->category, \breadcrumb_navigation_node::TYPE_CATEGORY);
 108              $courseformat = course_get_format($this->page->course)->get_course();
 109              // Section items can be only removed if a course layout (coursedisplay) is not explicitly set in the
 110              // given course format or the set course layout is not 'One section per page'.
 111              $removesections = !isset($courseformat->coursedisplay) ||
 112                  $courseformat->coursedisplay != COURSE_DISPLAY_MULTIPAGE;
 113              if ($removesections) {
 114                  // If the course sections are removed, we need to add the anchor of current section to the Course.
 115                  $coursenode = $this->get_item($this->page->course->id);
 116                  if (!is_null($coursenode) && $this->page->cm->sectionnum !== null) {
 117                      $coursenode->action = course_get_format($this->page->course)->get_view_url($this->page->cm->sectionnum);
 118                  }
 119              }
 120          }
 121  
 122          if ($this->page->context->contextlevel == CONTEXT_SYSTEM) {
 123              // Remove the navbar nodes that already exist in the secondary navigation menu.
 124              $this->remove_items_that_exist_in_navigation($PAGE->secondarynav);
 125          }
 126  
 127          // Set the designated one path for courses.
 128          $mycoursesnode = $this->get_item('mycourses');
 129          if (!is_null($mycoursesnode)) {
 130              $url = new \moodle_url('/my/courses.php');
 131              $mycoursesnode->action = $url;
 132              $mycoursesnode->text = get_string('mycourses');
 133          }
 134  
 135          $this->remove_no_link_items($removesections);
 136  
 137          // Don't display the navbar if there is only one item. Apparently this is bad UX design.
 138          if ($this->item_count() <= 1) {
 139              $this->clear_items();
 140              return;
 141          }
 142  
 143          // Make sure that the last item is not a link. Not sure if this is always a good idea.
 144          $this->remove_last_item_action();
 145      }
 146  
 147      /**
 148       * Get all the boostnavbaritem elements.
 149       *
 150       * @return boostnavbaritem[] Boost navbar items.
 151       */
 152      public function get_items(): array {
 153          return $this->items;
 154      }
 155  
 156      /**
 157       * Removes all navigation items out of this boost navbar
 158       */
 159      protected function clear_items(): void {
 160          $this->items = [];
 161      }
 162  
 163      /**
 164       * Retrieve a single navbar item.
 165       *
 166       * @param  string|int $key The identifier of the navbar item to return.
 167       * @return \breadcrumb_navigation_node|null The navbar item.
 168       */
 169      protected function get_item($key): ?\breadcrumb_navigation_node {
 170          foreach ($this->items as $item) {
 171              if ($key === $item->key) {
 172                  return $item;
 173              }
 174          }
 175          return null;
 176      }
 177  
 178      /**
 179       * Counts all of the navbar items.
 180       *
 181       * @return int How many navbar items there are.
 182       */
 183      protected function item_count(): int {
 184          return count($this->items);
 185      }
 186  
 187      /**
 188       * Remove a boostnavbaritem from the boost navbar.
 189       *
 190       * @param  string|int $itemkey An identifier for the boostnavbaritem
 191       * @param  int|null $itemtype An additional type identifier for the boostnavbaritem (optional)
 192       */
 193      protected function remove($itemkey, ?int $itemtype = null): void {
 194  
 195          $itemfound = false;
 196          foreach ($this->items as $key => $item) {
 197              if ($item->key === $itemkey) {
 198                  // If a type identifier is also specified, check whether the type of the breadcrumb item matches the
 199                  // specified type. Skip if types to not match.
 200                  if (!is_null($itemtype) && $item->type !== $itemtype) {
 201                      continue;
 202                  }
 203                  unset($this->items[$key]);
 204                  $itemfound = true;
 205                  break;
 206              }
 207          }
 208          if (!$itemfound) {
 209              return;
 210          }
 211  
 212          $itemcount = $this->item_count();
 213          if ($itemcount <= 0) {
 214              return;
 215          }
 216  
 217          $this->items = array_values($this->items);
 218          // Set the last item to last item if it is not.
 219          $lastitem = $this->items[$itemcount - 1];
 220          if (!$lastitem->is_last()) {
 221              $lastitem->set_last(true);
 222          }
 223      }
 224  
 225      /**
 226       * Removes the action from the last item of the boostnavbaritem.
 227       */
 228      protected function remove_last_item_action(): void {
 229          $item = end($this->items);
 230          $item->action = null;
 231          reset($this->items);
 232      }
 233  
 234      /**
 235       * Returns the second last navbar item. This is for use in the mobile view where we are showing just the second
 236       * last item in the breadcrumb navbar.
 237       *
 238       * @return breakcrumb_navigation_node|null The second last navigation node.
 239       */
 240      public function get_penultimate_item(): ?\breadcrumb_navigation_node {
 241          $number = $this->item_count() - 2;
 242          return ($number >= 0) ? $this->items[$number] : null;
 243      }
 244  
 245      /**
 246       * Remove items that have no actions associated with them and optionally remove items that are sections.
 247       *
 248       * The only exception is the last item in the list which may not have a link but needs to be displayed.
 249       *
 250       * @param bool $removesections Whether section items should be also removed (only applies when they have an action)
 251       */
 252      protected function remove_no_link_items(bool $removesections = true): void {
 253          foreach ($this->items as $key => $value) {
 254              if (!$value->is_last() &&
 255                      (!$value->has_action() || ($value->type == \navigation_node::TYPE_SECTION && $removesections))) {
 256                  unset($this->items[$key]);
 257              }
 258          }
 259          $this->items = array_values($this->items);
 260      }
 261  
 262      /**
 263       * Remove breadcrumb items that already exist in a given navigation view.
 264       *
 265       * This method removes the breadcrumb items that have a text => action match in a given navigation view
 266       * (primary or secondary).
 267       *
 268       * @param view $navigationview The navigation view object.
 269       */
 270      protected function remove_items_that_exist_in_navigation(view $navigationview): void {
 271          // Loop through the navigation view items and create a 'text' => 'action' array which will be later used
 272          // to compare whether any of the breadcrumb items matches these pairs.
 273          $navigationviewitems = [];
 274          foreach ($navigationview->children as $child) {
 275              list($childtext, $childaction) = $this->get_node_text_and_action($child);
 276              if ($childaction) {
 277                  $navigationviewitems[$childtext] = $childaction;
 278              }
 279          }
 280          // Loop through the breadcrumb items and if the item's 'text' and 'action' values matches with any of the
 281          // existing navigation view items, remove it from the breadcrumbs.
 282          foreach ($this->items as $item) {
 283              list($itemtext, $itemaction) = $this->get_node_text_and_action($item);
 284              if ($itemaction) {
 285                  if (array_key_exists($itemtext, $navigationviewitems) &&
 286                          $navigationviewitems[$itemtext] === $itemaction) {
 287                      $this->remove($item->key);
 288                  }
 289              }
 290          }
 291      }
 292  
 293      /**
 294       * Remove duplicate breadcrumb items.
 295       *
 296       * This method looks for breadcrumb items that have identical text and action values and removes the first item.
 297       */
 298      protected function remove_duplicate_items(): void {
 299          $taken = [];
 300          // Reverse the order of the items before filtering so that the first occurrence is removed instead of the last.
 301          $filtereditems = array_values(array_filter(array_reverse($this->items), function($item) use (&$taken) {
 302              list($itemtext, $itemaction) = $this->get_node_text_and_action($item);
 303              if ($itemaction) {
 304                  if (array_key_exists($itemtext, $taken) && $taken[$itemtext] === $itemaction) {
 305                      return false;
 306                  }
 307                  $taken[$itemtext] = $itemaction;
 308              }
 309              return true;
 310          }));
 311          // Reverse back the order.
 312          $this->items = array_reverse($filtereditems);
 313      }
 314  
 315      /**
 316       * Helper function that returns an array of the text and the outputted action url (if exists) for a given
 317       * navigation node.
 318       *
 319       * @param navigation_node $node The navigation node object.
 320       * @return array
 321       */
 322      protected function get_node_text_and_action(navigation_node $node): array {
 323          $text = $node->text instanceof lang_string ? $node->text->out() : $node->text;
 324          $action = null;
 325          if ($node->has_action()) {
 326              if ($node->action instanceof moodle_url) {
 327                  $action = $node->action->out();
 328              } else if ($node->action instanceof action_link) {
 329                  $action = $node->action->url->out();
 330              } else {
 331                  $action = $node->action;
 332              }
 333          }
 334          return [$text, $action];
 335      }
 336  }