Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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  /**
  18   * Classes representing HTML elements, used by $OUTPUT methods
  19   *
  20   * Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML
  21   * for an overview.
  22   *
  23   * @package core
  24   * @category output
  25   * @copyright 2009 Tim Hunt
  26   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  /**
  32   * Interface marking other classes as suitable for renderer_base::render()
  33   *
  34   * @copyright 2010 Petr Skoda (skodak) info@skodak.org
  35   * @package core
  36   * @category output
  37   */
  38  interface renderable {
  39      // intentionally empty
  40  }
  41  
  42  /**
  43   * Interface marking other classes having the ability to export their data for use by templates.
  44   *
  45   * @copyright 2015 Damyon Wiese
  46   * @package core
  47   * @category output
  48   * @since 2.9
  49   */
  50  interface templatable {
  51  
  52      /**
  53       * Function to export the renderer data in a format that is suitable for a
  54       * mustache template. This means:
  55       * 1. No complex types - only stdClass, array, int, string, float, bool
  56       * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks).
  57       *
  58       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
  59       * @return stdClass|array
  60       */
  61      public function export_for_template(renderer_base $output);
  62  }
  63  
  64  /**
  65   * Data structure representing a file picker.
  66   *
  67   * @copyright 2010 Dongsheng Cai
  68   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  69   * @since Moodle 2.0
  70   * @package core
  71   * @category output
  72   */
  73  class file_picker implements renderable {
  74  
  75      /**
  76       * @var stdClass An object containing options for the file picker
  77       */
  78      public $options;
  79  
  80      /**
  81       * Constructs a file picker object.
  82       *
  83       * The following are possible options for the filepicker:
  84       *    - accepted_types  (*)
  85       *    - return_types    (FILE_INTERNAL)
  86       *    - env             (filepicker)
  87       *    - client_id       (uniqid)
  88       *    - itemid          (0)
  89       *    - maxbytes        (-1)
  90       *    - maxfiles        (1)
  91       *    - buttonname      (false)
  92       *
  93       * @param stdClass $options An object containing options for the file picker.
  94       */
  95      public function __construct(stdClass $options) {
  96          global $CFG, $USER, $PAGE;
  97          require_once($CFG->dirroot. '/repository/lib.php');
  98          $defaults = array(
  99              'accepted_types'=>'*',
 100              'return_types'=>FILE_INTERNAL,
 101              'env' => 'filepicker',
 102              'client_id' => uniqid(),
 103              'itemid' => 0,
 104              'maxbytes'=>-1,
 105              'maxfiles'=>1,
 106              'buttonname'=>false
 107          );
 108          foreach ($defaults as $key=>$value) {
 109              if (empty($options->$key)) {
 110                  $options->$key = $value;
 111              }
 112          }
 113  
 114          $options->currentfile = '';
 115          if (!empty($options->itemid)) {
 116              $fs = get_file_storage();
 117              $usercontext = context_user::instance($USER->id);
 118              if (empty($options->filename)) {
 119                  if ($files = $fs->get_area_files($usercontext->id, 'user', 'draft', $options->itemid, 'id DESC', false)) {
 120                      $file = reset($files);
 121                  }
 122              } else {
 123                  $file = $fs->get_file($usercontext->id, 'user', 'draft', $options->itemid, $options->filepath, $options->filename);
 124              }
 125              if (!empty($file)) {
 126                  $options->currentfile = html_writer::link(moodle_url::make_draftfile_url($file->get_itemid(), $file->get_filepath(), $file->get_filename()), $file->get_filename());
 127              }
 128          }
 129  
 130          // initilise options, getting files in root path
 131          $this->options = initialise_filepicker($options);
 132  
 133          // copying other options
 134          foreach ($options as $name=>$value) {
 135              if (!isset($this->options->$name)) {
 136                  $this->options->$name = $value;
 137              }
 138          }
 139      }
 140  }
 141  
 142  /**
 143   * Data structure representing a user picture.
 144   *
 145   * @copyright 2009 Nicolas Connault, 2010 Petr Skoda
 146   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 147   * @since Modle 2.0
 148   * @package core
 149   * @category output
 150   */
 151  class user_picture implements renderable {
 152      /**
 153       * @var stdClass A user object with at least fields all columns specified
 154       * in $fields array constant set.
 155       */
 156      public $user;
 157  
 158      /**
 159       * @var int The course id. Used when constructing the link to the user's
 160       * profile, page course id used if not specified.
 161       */
 162      public $courseid;
 163  
 164      /**
 165       * @var bool Add course profile link to image
 166       */
 167      public $link = true;
 168  
 169      /**
 170       * @var int Size in pixels. Special values are (true/1 = 100px) and (false/0 = 35px) for backward compatibility.
 171       * Recommended values (supporting user initials too): 16, 35, 64 and 100.
 172       */
 173      public $size = 35;
 174  
 175      /**
 176       * @var bool Add non-blank alt-text to the image.
 177       * Default true, set to false when image alt just duplicates text in screenreaders.
 178       */
 179      public $alttext = true;
 180  
 181      /**
 182       * @var bool Whether or not to open the link in a popup window.
 183       */
 184      public $popup = false;
 185  
 186      /**
 187       * @var string Image class attribute
 188       */
 189      public $class = 'userpicture';
 190  
 191      /**
 192       * @var bool Whether to be visible to screen readers.
 193       */
 194      public $visibletoscreenreaders = true;
 195  
 196      /**
 197       * @var bool Whether to include the fullname in the user picture link.
 198       */
 199      public $includefullname = false;
 200  
 201      /**
 202       * @var mixed Include user authentication token. True indicates to generate a token for current user, and integer value
 203       * indicates to generate a token for the user whose id is the value indicated.
 204       */
 205      public $includetoken = false;
 206  
 207      /**
 208       * User picture constructor.
 209       *
 210       * @param stdClass $user user record with at least id, picture, imagealt, firstname and lastname set.
 211       *                 It is recommended to add also contextid of the user for performance reasons.
 212       */
 213      public function __construct(stdClass $user) {
 214          global $DB;
 215  
 216          if (empty($user->id)) {
 217              throw new coding_exception('User id is required when printing user avatar image.');
 218          }
 219  
 220          // only touch the DB if we are missing data and complain loudly...
 221          $needrec = false;
 222          foreach (\core_user\fields::get_picture_fields() as $field) {
 223              if (!property_exists($user, $field)) {
 224                  $needrec = true;
 225                  debugging('Missing '.$field.' property in $user object, this is a performance problem that needs to be fixed by a developer. '
 226                            .'Please use the \core_user\fields API to get the full list of required fields.', DEBUG_DEVELOPER);
 227                  break;
 228              }
 229          }
 230  
 231          if ($needrec) {
 232              $this->user = $DB->get_record('user', array('id' => $user->id),
 233                      implode(',', \core_user\fields::get_picture_fields()), MUST_EXIST);
 234          } else {
 235              $this->user = clone($user);
 236          }
 237      }
 238  
 239      /**
 240       * Returns a list of required user fields, useful when fetching required user info from db.
 241       *
 242       * In some cases we have to fetch the user data together with some other information,
 243       * the idalias is useful there because the id would otherwise override the main
 244       * id of the result record. Please note it has to be converted back to id before rendering.
 245       *
 246       * @param string $tableprefix name of database table prefix in query
 247       * @param array $extrafields extra fields to be included in result (do not include TEXT columns because it would break SELECT DISTINCT in MSSQL and ORACLE)
 248       * @param string $idalias alias of id field
 249       * @param string $fieldprefix prefix to add to all columns in their aliases, does not apply to 'id'
 250       * @return string
 251       * @deprecated since Moodle 3.11 MDL-45242
 252       * @see \core_user\fields
 253       */
 254      public static function fields($tableprefix = '', array $extrafields = NULL, $idalias = 'id', $fieldprefix = '') {
 255          debugging('user_picture::fields() is deprecated. Please use the \core_user\fields API instead.', DEBUG_DEVELOPER);
 256          $userfields = \core_user\fields::for_userpic();
 257          if ($extrafields) {
 258              $userfields->including(...$extrafields);
 259          }
 260          $selects = $userfields->get_sql($tableprefix, false, $fieldprefix, $idalias, false)->selects;
 261          if ($tableprefix === '') {
 262              // If no table alias is specified, don't add {user}. in front of fields.
 263              $selects = str_replace('{user}.', '', $selects);
 264          }
 265          // Maintain legacy behaviour where the field list was done with 'implode' and no spaces.
 266          $selects = str_replace(', ', ',', $selects);
 267          return $selects;
 268      }
 269  
 270      /**
 271       * Extract the aliased user fields from a given record
 272       *
 273       * Given a record that was previously obtained using {@link self::fields()} with aliases,
 274       * this method extracts user related unaliased fields.
 275       *
 276       * @param stdClass $record containing user picture fields
 277       * @param array $extrafields extra fields included in the $record
 278       * @param string $idalias alias of the id field
 279       * @param string $fieldprefix prefix added to all columns in their aliases, does not apply to 'id'
 280       * @return stdClass object with unaliased user fields
 281       */
 282      public static function unalias(stdClass $record, array $extrafields = null, $idalias = 'id', $fieldprefix = '') {
 283  
 284          if (empty($idalias)) {
 285              $idalias = 'id';
 286          }
 287  
 288          $return = new stdClass();
 289  
 290          foreach (\core_user\fields::get_picture_fields() as $field) {
 291              if ($field === 'id') {
 292                  if (property_exists($record, $idalias)) {
 293                      $return->id = $record->{$idalias};
 294                  }
 295              } else {
 296                  if (property_exists($record, $fieldprefix.$field)) {
 297                      $return->{$field} = $record->{$fieldprefix.$field};
 298                  }
 299              }
 300          }
 301          // add extra fields if not already there
 302          if ($extrafields) {
 303              foreach ($extrafields as $e) {
 304                  if ($e === 'id' or property_exists($return, $e)) {
 305                      continue;
 306                  }
 307                  $return->{$e} = $record->{$fieldprefix.$e};
 308              }
 309          }
 310  
 311          return $return;
 312      }
 313  
 314      /**
 315       * Works out the URL for the users picture.
 316       *
 317       * This method is recommended as it avoids costly redirects of user pictures
 318       * if requests are made for non-existent files etc.
 319       *
 320       * @param moodle_page $page
 321       * @param renderer_base $renderer
 322       * @return moodle_url
 323       */
 324      public function get_url(moodle_page $page, renderer_base $renderer = null) {
 325          global $CFG;
 326  
 327          if (is_null($renderer)) {
 328              $renderer = $page->get_renderer('core');
 329          }
 330  
 331          // Sort out the filename and size. Size is only required for the gravatar
 332          // implementation presently.
 333          if (empty($this->size)) {
 334              $filename = 'f2';
 335              $size = 35;
 336          } else if ($this->size === true or $this->size == 1) {
 337              $filename = 'f1';
 338              $size = 100;
 339          } else if ($this->size > 100) {
 340              $filename = 'f3';
 341              $size = (int)$this->size;
 342          } else if ($this->size >= 50) {
 343              $filename = 'f1';
 344              $size = (int)$this->size;
 345          } else {
 346              $filename = 'f2';
 347              $size = (int)$this->size;
 348          }
 349  
 350          $defaulturl = $renderer->image_url('u/'.$filename); // default image
 351  
 352          if ((!empty($CFG->forcelogin) and !isloggedin()) ||
 353              (!empty($CFG->forceloginforprofileimage) && (!isloggedin() || isguestuser()))) {
 354              // Protect images if login required and not logged in;
 355              // also if login is required for profile images and is not logged in or guest
 356              // do not use require_login() because it is expensive and not suitable here anyway.
 357              return $defaulturl;
 358          }
 359  
 360          // First try to detect deleted users - but do not read from database for performance reasons!
 361          if (!empty($this->user->deleted) or strpos($this->user->email, '@') === false) {
 362              // All deleted users should have email replaced by md5 hash,
 363              // all active users are expected to have valid email.
 364              return $defaulturl;
 365          }
 366  
 367          // Did the user upload a picture?
 368          if ($this->user->picture > 0) {
 369              if (!empty($this->user->contextid)) {
 370                  $contextid = $this->user->contextid;
 371              } else {
 372                  $context = context_user::instance($this->user->id, IGNORE_MISSING);
 373                  if (!$context) {
 374                      // This must be an incorrectly deleted user, all other users have context.
 375                      return $defaulturl;
 376                  }
 377                  $contextid = $context->id;
 378              }
 379  
 380              $path = '/';
 381              if (clean_param($page->theme->name, PARAM_THEME) == $page->theme->name) {
 382                  // We append the theme name to the file path if we have it so that
 383                  // in the circumstance that the profile picture is not available
 384                  // when the user actually requests it they still get the profile
 385                  // picture for the correct theme.
 386                  $path .= $page->theme->name.'/';
 387              }
 388              // Set the image URL to the URL for the uploaded file and return.
 389              $url = moodle_url::make_pluginfile_url(
 390                      $contextid, 'user', 'icon', null, $path, $filename, false, $this->includetoken);
 391              $url->param('rev', $this->user->picture);
 392              return $url;
 393          }
 394  
 395          if ($this->user->picture == 0 and !empty($CFG->enablegravatar)) {
 396              // Normalise the size variable to acceptable bounds
 397              if ($size < 1 || $size > 512) {
 398                  $size = 35;
 399              }
 400              // Hash the users email address
 401              $md5 = md5(strtolower(trim($this->user->email)));
 402              // Build a gravatar URL with what we know.
 403  
 404              // Find the best default image URL we can (MDL-35669)
 405              if (empty($CFG->gravatardefaulturl)) {
 406                  $absoluteimagepath = $page->theme->resolve_image_location('u/'.$filename, 'core');
 407                  if (strpos($absoluteimagepath, $CFG->dirroot) === 0) {
 408                      $gravatardefault = $CFG->wwwroot . substr($absoluteimagepath, strlen($CFG->dirroot));
 409                  } else {
 410                      $gravatardefault = $CFG->wwwroot . '/pix/u/' . $filename . '.png';
 411                  }
 412              } else {
 413                  $gravatardefault = $CFG->gravatardefaulturl;
 414              }
 415  
 416              // If the currently requested page is https then we'll return an
 417              // https gravatar page.
 418              if (is_https()) {
 419                  return new moodle_url("https://secure.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
 420              } else {
 421                  return new moodle_url("http://www.gravatar.com/avatar/{$md5}", array('s' => $size, 'd' => $gravatardefault));
 422              }
 423          }
 424  
 425          return $defaulturl;
 426      }
 427  }
 428  
 429  /**
 430   * Data structure representing a help icon.
 431   *
 432   * @copyright 2010 Petr Skoda (info@skodak.org)
 433   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 434   * @since Moodle 2.0
 435   * @package core
 436   * @category output
 437   */
 438  class help_icon implements renderable, templatable {
 439  
 440      /**
 441       * @var string lang pack identifier (without the "_help" suffix),
 442       * both get_string($identifier, $component) and get_string($identifier.'_help', $component)
 443       * must exist.
 444       */
 445      public $identifier;
 446  
 447      /**
 448       * @var string Component name, the same as in get_string()
 449       */
 450      public $component;
 451  
 452      /**
 453       * @var string Extra descriptive text next to the icon
 454       */
 455      public $linktext = null;
 456  
 457      /**
 458       * Constructor
 459       *
 460       * @param string $identifier string for help page title,
 461       *  string with _help suffix is used for the actual help text.
 462       *  string with _link suffix is used to create a link to further info (if it exists)
 463       * @param string $component
 464       */
 465      public function __construct($identifier, $component) {
 466          $this->identifier = $identifier;
 467          $this->component  = $component;
 468      }
 469  
 470      /**
 471       * Verifies that both help strings exists, shows debug warnings if not
 472       */
 473      public function diag_strings() {
 474          $sm = get_string_manager();
 475          if (!$sm->string_exists($this->identifier, $this->component)) {
 476              debugging("Help title string does not exist: [$this->identifier, $this->component]");
 477          }
 478          if (!$sm->string_exists($this->identifier.'_help', $this->component)) {
 479              debugging("Help contents string does not exist: [{$this->identifier}_help, $this->component]");
 480          }
 481      }
 482  
 483      /**
 484       * Export this data so it can be used as the context for a mustache template.
 485       *
 486       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
 487       * @return array
 488       */
 489      public function export_for_template(renderer_base $output) {
 490          global $CFG;
 491  
 492          $title = get_string($this->identifier, $this->component);
 493  
 494          if (empty($this->linktext)) {
 495              $alt = get_string('helpprefix2', '', trim($title, ". \t"));
 496          } else {
 497              $alt = get_string('helpwiththis');
 498          }
 499  
 500          $data = get_formatted_help_string($this->identifier, $this->component, false);
 501  
 502          $data->alt = $alt;
 503          $data->icon = (new pix_icon('help', $alt, 'core', ['class' => 'iconhelp']))->export_for_template($output);
 504          $data->linktext = $this->linktext;
 505          $data->title = get_string('helpprefix2', '', trim($title, ". \t"));
 506  
 507          $options = [
 508              'component' => $this->component,
 509              'identifier' => $this->identifier,
 510              'lang' => current_language()
 511          ];
 512  
 513          // Debugging feature lets you display string identifier and component.
 514          if (isset($CFG->debugstringids) && $CFG->debugstringids && optional_param('strings', 0, PARAM_INT)) {
 515              $options['strings'] = 1;
 516          }
 517  
 518          $data->url = (new moodle_url('/help.php', $options))->out(false);
 519          $data->ltr = !right_to_left();
 520          return $data;
 521      }
 522  }
 523  
 524  
 525  /**
 526   * Data structure representing an icon font.
 527   *
 528   * @copyright 2016 Damyon Wiese
 529   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 530   * @package core
 531   * @category output
 532   */
 533  class pix_icon_font implements templatable {
 534  
 535      /**
 536       * @var pix_icon $pixicon The original icon.
 537       */
 538      private $pixicon = null;
 539  
 540      /**
 541       * @var string $key The mapped key.
 542       */
 543      private $key;
 544  
 545      /**
 546       * @var bool $mapped The icon could not be mapped.
 547       */
 548      private $mapped;
 549  
 550      /**
 551       * Constructor
 552       *
 553       * @param pix_icon $pixicon The original icon
 554       */
 555      public function __construct(pix_icon $pixicon) {
 556          global $PAGE;
 557  
 558          $this->pixicon = $pixicon;
 559          $this->mapped = false;
 560          $iconsystem = \core\output\icon_system::instance();
 561  
 562          $this->key = $iconsystem->remap_icon_name($pixicon->pix, $pixicon->component);
 563          if (!empty($this->key)) {
 564              $this->mapped = true;
 565          }
 566      }
 567  
 568      /**
 569       * Return true if this pix_icon was successfully mapped to an icon font.
 570       *
 571       * @return bool
 572       */
 573      public function is_mapped() {
 574          return $this->mapped;
 575      }
 576  
 577      /**
 578       * Export this data so it can be used as the context for a mustache template.
 579       *
 580       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
 581       * @return array
 582       */
 583      public function export_for_template(renderer_base $output) {
 584  
 585          $pixdata = $this->pixicon->export_for_template($output);
 586  
 587          $title = isset($this->pixicon->attributes['title']) ? $this->pixicon->attributes['title'] : '';
 588          $alt = isset($this->pixicon->attributes['alt']) ? $this->pixicon->attributes['alt'] : '';
 589          if (empty($title)) {
 590              $title = $alt;
 591          }
 592          $data = array(
 593              'extraclasses' => $pixdata['extraclasses'],
 594              'title' => $title,
 595              'alt' => $alt,
 596              'key' => $this->key
 597          );
 598  
 599          return $data;
 600      }
 601  }
 602  
 603  /**
 604   * Data structure representing an icon subtype.
 605   *
 606   * @copyright 2016 Damyon Wiese
 607   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 608   * @package core
 609   * @category output
 610   */
 611  class pix_icon_fontawesome extends pix_icon_font {
 612  
 613  }
 614  
 615  /**
 616   * Data structure representing an icon.
 617   *
 618   * @copyright 2010 Petr Skoda
 619   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 620   * @since Moodle 2.0
 621   * @package core
 622   * @category output
 623   */
 624  class pix_icon implements renderable, templatable {
 625  
 626      /**
 627       * @var string The icon name
 628       */
 629      var $pix;
 630  
 631      /**
 632       * @var string The component the icon belongs to.
 633       */
 634      var $component;
 635  
 636      /**
 637       * @var array An array of attributes to use on the icon
 638       */
 639      var $attributes = array();
 640  
 641      /**
 642       * Constructor
 643       *
 644       * @param string $pix short icon name
 645       * @param string $alt The alt text to use for the icon
 646       * @param string $component component name
 647       * @param array $attributes html attributes
 648       */
 649      public function __construct($pix, $alt, $component='moodle', array $attributes = null) {
 650          global $PAGE;
 651  
 652          $this->pix = $pix;
 653          $this->component  = $component;
 654          $this->attributes = (array)$attributes;
 655  
 656          if (empty($this->attributes['class'])) {
 657              $this->attributes['class'] = '';
 658          }
 659  
 660          // Set an additional class for big icons so that they can be styled properly.
 661          if (substr($pix, 0, 2) === 'b/') {
 662              $this->attributes['class'] .= ' iconsize-big';
 663          }
 664  
 665          // If the alt is empty, don't place it in the attributes, otherwise it will override parent alt text.
 666          if (!is_null($alt)) {
 667              $this->attributes['alt'] = $alt;
 668  
 669              // If there is no title, set it to the attribute.
 670              if (!isset($this->attributes['title'])) {
 671                  $this->attributes['title'] = $this->attributes['alt'];
 672              }
 673          } else {
 674              unset($this->attributes['alt']);
 675          }
 676  
 677          if (empty($this->attributes['title'])) {
 678              // Remove the title attribute if empty, we probably want to use the parent node's title
 679              // and some browsers might overwrite it with an empty title.
 680              unset($this->attributes['title']);
 681          }
 682  
 683          // Hide icons from screen readers that have no alt.
 684          if (empty($this->attributes['alt'])) {
 685              $this->attributes['aria-hidden'] = 'true';
 686          }
 687      }
 688  
 689      /**
 690       * Export this data so it can be used as the context for a mustache template.
 691       *
 692       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
 693       * @return array
 694       */
 695      public function export_for_template(renderer_base $output) {
 696          $attributes = $this->attributes;
 697          $extraclasses = '';
 698  
 699          foreach ($attributes as $key => $item) {
 700              if ($key == 'class') {
 701                  $extraclasses = $item;
 702                  unset($attributes[$key]);
 703                  break;
 704              }
 705          }
 706  
 707          $attributes['src'] = $output->image_url($this->pix, $this->component)->out(false);
 708          $templatecontext = array();
 709          foreach ($attributes as $name => $value) {
 710              $templatecontext[] = array('name' => $name, 'value' => $value);
 711          }
 712          $title = isset($attributes['title']) ? $attributes['title'] : '';
 713          if (empty($title)) {
 714              $title = isset($attributes['alt']) ? $attributes['alt'] : '';
 715          }
 716          $data = array(
 717              'attributes' => $templatecontext,
 718              'extraclasses' => $extraclasses
 719          );
 720  
 721          return $data;
 722      }
 723  
 724      /**
 725       * Much simpler version of export that will produce the data required to render this pix with the
 726       * pix helper in a mustache tag.
 727       *
 728       * @return array
 729       */
 730      public function export_for_pix() {
 731          $title = isset($this->attributes['title']) ? $this->attributes['title'] : '';
 732          if (empty($title)) {
 733              $title = isset($this->attributes['alt']) ? $this->attributes['alt'] : '';
 734          }
 735          return [
 736              'key' => $this->pix,
 737              'component' => $this->component,
 738              'title' => (string) $title,
 739          ];
 740      }
 741  }
 742  
 743  /**
 744   * Data structure representing an activity icon.
 745   *
 746   * The difference is that activity icons will always render with the standard icon system (no font icons).
 747   *
 748   * @copyright 2017 Damyon Wiese
 749   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 750   * @package core
 751   */
 752  class image_icon extends pix_icon {
 753  }
 754  
 755  /**
 756   * Data structure representing an emoticon image
 757   *
 758   * @copyright 2010 David Mudrak
 759   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 760   * @since Moodle 2.0
 761   * @package core
 762   * @category output
 763   */
 764  class pix_emoticon extends pix_icon implements renderable {
 765  
 766      /**
 767       * Constructor
 768       * @param string $pix short icon name
 769       * @param string $alt alternative text
 770       * @param string $component emoticon image provider
 771       * @param array $attributes explicit HTML attributes
 772       */
 773      public function __construct($pix, $alt, $component = 'moodle', array $attributes = array()) {
 774          if (empty($attributes['class'])) {
 775              $attributes['class'] = 'emoticon';
 776          }
 777          parent::__construct($pix, $alt, $component, $attributes);
 778      }
 779  }
 780  
 781  /**
 782   * Data structure representing a simple form with only one button.
 783   *
 784   * @copyright 2009 Petr Skoda
 785   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 786   * @since Moodle 2.0
 787   * @package core
 788   * @category output
 789   */
 790  class single_button implements renderable {
 791  
 792      /**
 793       * @var moodle_url Target url
 794       */
 795      public $url;
 796  
 797      /**
 798       * @var string Button label
 799       */
 800      public $label;
 801  
 802      /**
 803       * @var string Form submit method post or get
 804       */
 805      public $method = 'post';
 806  
 807      /**
 808       * @var string Wrapping div class
 809       */
 810      public $class = 'singlebutton';
 811  
 812      /**
 813       * @var bool True if button is primary button. Used for styling.
 814       */
 815      public $primary = false;
 816  
 817      /**
 818       * @var bool True if button disabled, false if normal
 819       */
 820      public $disabled = false;
 821  
 822      /**
 823       * @var string Button tooltip
 824       */
 825      public $tooltip = null;
 826  
 827      /**
 828       * @var string Form id
 829       */
 830      public $formid;
 831  
 832      /**
 833       * @var array List of attached actions
 834       */
 835      public $actions = array();
 836  
 837      /**
 838       * @var array $params URL Params
 839       */
 840      public $params;
 841  
 842      /**
 843       * @var string Action id
 844       */
 845      public $actionid;
 846  
 847      /**
 848       * @var array
 849       */
 850      protected $attributes = [];
 851  
 852      /**
 853       * Constructor
 854       * @param moodle_url $url
 855       * @param string $label button text
 856       * @param string $method get or post submit method
 857       * @param bool $primary whether this is a primary button, used for styling
 858       * @param array $attributes Attributes for the HTML button tag
 859       */
 860      public function __construct(moodle_url $url, $label, $method='post', $primary=false, $attributes = []) {
 861          $this->url    = clone($url);
 862          $this->label  = $label;
 863          $this->method = $method;
 864          $this->primary = $primary;
 865          $this->attributes = $attributes;
 866      }
 867  
 868      /**
 869       * Shortcut for adding a JS confirm dialog when the button is clicked.
 870       * The message must be a yes/no question.
 871       *
 872       * @param string $confirmmessage The yes/no confirmation question. If "Yes" is clicked, the original action will occur.
 873       */
 874      public function add_confirm_action($confirmmessage) {
 875          $this->add_action(new confirm_action($confirmmessage));
 876      }
 877  
 878      /**
 879       * Add action to the button.
 880       * @param component_action $action
 881       */
 882      public function add_action(component_action $action) {
 883          $this->actions[] = $action;
 884      }
 885  
 886      /**
 887       * Sets an attribute for the HTML button tag.
 888       *
 889       * @param  string $name  The attribute name
 890       * @param  mixed  $value The value
 891       * @return null
 892       */
 893      public function set_attribute($name, $value) {
 894          $this->attributes[$name] = $value;
 895      }
 896  
 897      /**
 898       * Export data.
 899       *
 900       * @param renderer_base $output Renderer.
 901       * @return stdClass
 902       */
 903      public function export_for_template(renderer_base $output) {
 904          $url = $this->method === 'get' ? $this->url->out_omit_querystring(true) : $this->url->out_omit_querystring();
 905  
 906          $data = new stdClass();
 907          $data->id = html_writer::random_id('single_button');
 908          $data->formid = $this->formid;
 909          $data->method = $this->method;
 910          $data->url = $url === '' ? '#' : $url;
 911          $data->label = $this->label;
 912          $data->classes = $this->class;
 913          $data->disabled = $this->disabled;
 914          $data->tooltip = $this->tooltip;
 915          $data->primary = $this->primary;
 916  
 917          $data->attributes = [];
 918          foreach ($this->attributes as $key => $value) {
 919              $data->attributes[] = ['name' => $key, 'value' => $value];
 920          }
 921  
 922          // Form parameters.
 923          $actionurl = new moodle_url($this->url);
 924          if ($this->method === 'post') {
 925              $actionurl->param('sesskey', sesskey());
 926          }
 927          $data->params = $actionurl->export_params_for_template();
 928  
 929          // Button actions.
 930          $actions = $this->actions;
 931          $data->actions = array_map(function($action) use ($output) {
 932              return $action->export_for_template($output);
 933          }, $actions);
 934          $data->hasactions = !empty($data->actions);
 935  
 936          return $data;
 937      }
 938  }
 939  
 940  
 941  /**
 942   * Simple form with just one select field that gets submitted automatically.
 943   *
 944   * If JS not enabled small go button is printed too.
 945   *
 946   * @copyright 2009 Petr Skoda
 947   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 948   * @since Moodle 2.0
 949   * @package core
 950   * @category output
 951   */
 952  class single_select implements renderable, templatable {
 953  
 954      /**
 955       * @var moodle_url Target url - includes hidden fields
 956       */
 957      var $url;
 958  
 959      /**
 960       * @var string Name of the select element.
 961       */
 962      var $name;
 963  
 964      /**
 965       * @var array $options associative array value=>label ex.: array(1=>'One, 2=>Two)
 966       *     it is also possible to specify optgroup as complex label array ex.:
 967       *         array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
 968       *         array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
 969       */
 970      var $options;
 971  
 972      /**
 973       * @var string Selected option
 974       */
 975      var $selected;
 976  
 977      /**
 978       * @var array Nothing selected
 979       */
 980      var $nothing;
 981  
 982      /**
 983       * @var array Extra select field attributes
 984       */
 985      var $attributes = array();
 986  
 987      /**
 988       * @var string Button label
 989       */
 990      var $label = '';
 991  
 992      /**
 993       * @var array Button label's attributes
 994       */
 995      var $labelattributes = array();
 996  
 997      /**
 998       * @var string Form submit method post or get
 999       */
1000      var $method = 'get';
1001  
1002      /**
1003       * @var string Wrapping div class
1004       */
1005      var $class = 'singleselect';
1006  
1007      /**
1008       * @var bool True if button disabled, false if normal
1009       */
1010      var $disabled = false;
1011  
1012      /**
1013       * @var string Button tooltip
1014       */
1015      var $tooltip = null;
1016  
1017      /**
1018       * @var string Form id
1019       */
1020      var $formid = null;
1021  
1022      /**
1023       * @var help_icon The help icon for this element.
1024       */
1025      var $helpicon = null;
1026  
1027      /**
1028       * Constructor
1029       * @param moodle_url $url form action target, includes hidden fields
1030       * @param string $name name of selection field - the changing parameter in url
1031       * @param array $options list of options
1032       * @param string $selected selected element
1033       * @param array $nothing
1034       * @param string $formid
1035       */
1036      public function __construct(moodle_url $url, $name, array $options, $selected = '', $nothing = array('' => 'choosedots'), $formid = null) {
1037          $this->url      = $url;
1038          $this->name     = $name;
1039          $this->options  = $options;
1040          $this->selected = $selected;
1041          $this->nothing  = $nothing;
1042          $this->formid   = $formid;
1043      }
1044  
1045      /**
1046       * Shortcut for adding a JS confirm dialog when the button is clicked.
1047       * The message must be a yes/no question.
1048       *
1049       * @param string $confirmmessage The yes/no confirmation question. If "Yes" is clicked, the original action will occur.
1050       */
1051      public function add_confirm_action($confirmmessage) {
1052          $this->add_action(new component_action('submit', 'M.util.show_confirm_dialog', array('message' => $confirmmessage)));
1053      }
1054  
1055      /**
1056       * Add action to the button.
1057       *
1058       * @param component_action $action
1059       */
1060      public function add_action(component_action $action) {
1061          $this->actions[] = $action;
1062      }
1063  
1064      /**
1065       * Adds help icon.
1066       *
1067       * @deprecated since Moodle 2.0
1068       */
1069      public function set_old_help_icon($helppage, $title, $component = 'moodle') {
1070          throw new coding_exception('set_old_help_icon() can not be used any more, please see set_help_icon().');
1071      }
1072  
1073      /**
1074       * Adds help icon.
1075       *
1076       * @param string $identifier The keyword that defines a help page
1077       * @param string $component
1078       */
1079      public function set_help_icon($identifier, $component = 'moodle') {
1080          $this->helpicon = new help_icon($identifier, $component);
1081      }
1082  
1083      /**
1084       * Sets select's label
1085       *
1086       * @param string $label
1087       * @param array $attributes (optional)
1088       */
1089      public function set_label($label, $attributes = array()) {
1090          $this->label = $label;
1091          $this->labelattributes = $attributes;
1092  
1093      }
1094  
1095      /**
1096       * Export data.
1097       *
1098       * @param renderer_base $output Renderer.
1099       * @return stdClass
1100       */
1101      public function export_for_template(renderer_base $output) {
1102          $attributes = $this->attributes;
1103  
1104          $data = new stdClass();
1105          $data->name = $this->name;
1106          $data->method = $this->method;
1107          $data->action = $this->method === 'get' ? $this->url->out_omit_querystring(true) : $this->url->out_omit_querystring();
1108          $data->classes = $this->class;
1109          $data->label = $this->label;
1110          $data->disabled = $this->disabled;
1111          $data->title = $this->tooltip;
1112          $data->formid = !empty($this->formid) ? $this->formid : html_writer::random_id('single_select_f');
1113          $data->id = !empty($attributes['id']) ? $attributes['id'] : html_writer::random_id('single_select');
1114  
1115          // Select element attributes.
1116          // Unset attributes that are already predefined in the template.
1117          unset($attributes['id']);
1118          unset($attributes['class']);
1119          unset($attributes['name']);
1120          unset($attributes['title']);
1121          unset($attributes['disabled']);
1122  
1123          // Map the attributes.
1124          $data->attributes = array_map(function($key) use ($attributes) {
1125              return ['name' => $key, 'value' => $attributes[$key]];
1126          }, array_keys($attributes));
1127  
1128          // Form parameters.
1129          $actionurl = new moodle_url($this->url);
1130          if ($this->method === 'post') {
1131              $actionurl->param('sesskey', sesskey());
1132          }
1133          $data->params = $actionurl->export_params_for_template();
1134  
1135          // Select options.
1136          $hasnothing = false;
1137          if (is_string($this->nothing) && $this->nothing !== '') {
1138              $nothing = ['' => $this->nothing];
1139              $hasnothing = true;
1140              $nothingkey = '';
1141          } else if (is_array($this->nothing)) {
1142              $nothingvalue = reset($this->nothing);
1143              if ($nothingvalue === 'choose' || $nothingvalue === 'choosedots') {
1144                  $nothing = [key($this->nothing) => get_string('choosedots')];
1145              } else {
1146                  $nothing = $this->nothing;
1147              }
1148              $hasnothing = true;
1149              $nothingkey = key($this->nothing);
1150          }
1151          if ($hasnothing) {
1152              $options = $nothing + $this->options;
1153          } else {
1154              $options = $this->options;
1155          }
1156  
1157          foreach ($options as $value => $name) {
1158              if (is_array($options[$value])) {
1159                  foreach ($options[$value] as $optgroupname => $optgroupvalues) {
1160                      $sublist = [];
1161                      foreach ($optgroupvalues as $optvalue => $optname) {
1162                          $option = [
1163                              'value' => $optvalue,
1164                              'name' => $optname,
1165                              'selected' => strval($this->selected) === strval($optvalue),
1166                          ];
1167  
1168                          if ($hasnothing && $nothingkey === $optvalue) {
1169                              $option['ignore'] = 'data-ignore';
1170                          }
1171  
1172                          $sublist[] = $option;
1173                      }
1174                      $data->options[] = [
1175                          'name' => $optgroupname,
1176                          'optgroup' => true,
1177                          'options' => $sublist
1178                      ];
1179                  }
1180              } else {
1181                  $option = [
1182                      'value' => $value,
1183                      'name' => $options[$value],
1184                      'selected' => strval($this->selected) === strval($value),
1185                      'optgroup' => false
1186                  ];
1187  
1188                  if ($hasnothing && $nothingkey === $value) {
1189                      $option['ignore'] = 'data-ignore';
1190                  }
1191  
1192                  $data->options[] = $option;
1193              }
1194          }
1195  
1196          // Label attributes.
1197          $data->labelattributes = [];
1198          // Unset label attributes that are already in the template.
1199          unset($this->labelattributes['for']);
1200          // Map the label attributes.
1201          foreach ($this->labelattributes as $key => $value) {
1202              $data->labelattributes[] = ['name' => $key, 'value' => $value];
1203          }
1204  
1205          // Help icon.
1206          $data->helpicon = !empty($this->helpicon) ? $this->helpicon->export_for_template($output) : false;
1207  
1208          return $data;
1209      }
1210  }
1211  
1212  /**
1213   * Simple URL selection widget description.
1214   *
1215   * @copyright 2009 Petr Skoda
1216   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1217   * @since Moodle 2.0
1218   * @package core
1219   * @category output
1220   */
1221  class url_select implements renderable, templatable {
1222      /**
1223       * @var array $urls associative array value=>label ex.: array(1=>'One, 2=>Two)
1224       *     it is also possible to specify optgroup as complex label array ex.:
1225       *         array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
1226       *         array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
1227       */
1228      var $urls;
1229  
1230      /**
1231       * @var string Selected option
1232       */
1233      var $selected;
1234  
1235      /**
1236       * @var array Nothing selected
1237       */
1238      var $nothing;
1239  
1240      /**
1241       * @var array Extra select field attributes
1242       */
1243      var $attributes = array();
1244  
1245      /**
1246       * @var string Button label
1247       */
1248      var $label = '';
1249  
1250      /**
1251       * @var array Button label's attributes
1252       */
1253      var $labelattributes = array();
1254  
1255      /**
1256       * @var string Wrapping div class
1257       */
1258      var $class = 'urlselect';
1259  
1260      /**
1261       * @var bool True if button disabled, false if normal
1262       */
1263      var $disabled = false;
1264  
1265      /**
1266       * @var string Button tooltip
1267       */
1268      var $tooltip = null;
1269  
1270      /**
1271       * @var string Form id
1272       */
1273      var $formid = null;
1274  
1275      /**
1276       * @var help_icon The help icon for this element.
1277       */
1278      var $helpicon = null;
1279  
1280      /**
1281       * @var string If set, makes button visible with given name for button
1282       */
1283      var $showbutton = null;
1284  
1285      /**
1286       * Constructor
1287       * @param array $urls list of options
1288       * @param string $selected selected element
1289       * @param array $nothing
1290       * @param string $formid
1291       * @param string $showbutton Set to text of button if it should be visible
1292       *   or null if it should be hidden (hidden version always has text 'go')
1293       */
1294      public function __construct(array $urls, $selected = '', $nothing = array('' => 'choosedots'), $formid = null, $showbutton = null) {
1295          $this->urls       = $urls;
1296          $this->selected   = $selected;
1297          $this->nothing    = $nothing;
1298          $this->formid     = $formid;
1299          $this->showbutton = $showbutton;
1300      }
1301  
1302      /**
1303       * Adds help icon.
1304       *
1305       * @deprecated since Moodle 2.0
1306       */
1307      public function set_old_help_icon($helppage, $title, $component = 'moodle') {
1308          throw new coding_exception('set_old_help_icon() can not be used any more, please see set_help_icon().');
1309      }
1310  
1311      /**
1312       * Adds help icon.
1313       *
1314       * @param string $identifier The keyword that defines a help page
1315       * @param string $component
1316       */
1317      public function set_help_icon($identifier, $component = 'moodle') {
1318          $this->helpicon = new help_icon($identifier, $component);
1319      }
1320  
1321      /**
1322       * Sets select's label
1323       *
1324       * @param string $label
1325       * @param array $attributes (optional)
1326       */
1327      public function set_label($label, $attributes = array()) {
1328          $this->label = $label;
1329          $this->labelattributes = $attributes;
1330      }
1331  
1332      /**
1333       * Clean a URL.
1334       *
1335       * @param string $value The URL.
1336       * @return The cleaned URL.
1337       */
1338      protected function clean_url($value) {
1339          global $CFG;
1340  
1341          if (empty($value)) {
1342              // Nothing.
1343  
1344          } else if (strpos($value, $CFG->wwwroot . '/') === 0) {
1345              $value = str_replace($CFG->wwwroot, '', $value);
1346  
1347          } else if (strpos($value, '/') !== 0) {
1348              debugging("Invalid url_select urls parameter: url '$value' is not local relative url!", DEBUG_DEVELOPER);
1349          }
1350  
1351          return $value;
1352      }
1353  
1354      /**
1355       * Flatten the options for Mustache.
1356       *
1357       * This also cleans the URLs.
1358       *
1359       * @param array $options The options.
1360       * @param array $nothing The nothing option.
1361       * @return array
1362       */
1363      protected function flatten_options($options, $nothing) {
1364          $flattened = [];
1365  
1366          foreach ($options as $value => $option) {
1367              if (is_array($option)) {
1368                  foreach ($option as $groupname => $optoptions) {
1369                      if (!isset($flattened[$groupname])) {
1370                          $flattened[$groupname] = [
1371                              'name' => $groupname,
1372                              'isgroup' => true,
1373                              'options' => []
1374                          ];
1375                      }
1376                      foreach ($optoptions as $optvalue => $optoption) {
1377                          $cleanedvalue = $this->clean_url($optvalue);
1378                          $flattened[$groupname]['options'][$cleanedvalue] = [
1379                              'name' => $optoption,
1380                              'value' => $cleanedvalue,
1381                              'selected' => $this->selected == $optvalue,
1382                          ];
1383                      }
1384                  }
1385  
1386              } else {
1387                  $cleanedvalue = $this->clean_url($value);
1388                  $flattened[$cleanedvalue] = [
1389                      'name' => $option,
1390                      'value' => $cleanedvalue,
1391                      'selected' => $this->selected == $value,
1392                  ];
1393              }
1394          }
1395  
1396          if (!empty($nothing)) {
1397              $value = key($nothing);
1398              $name = reset($nothing);
1399              $flattened = [
1400                  $value => ['name' => $name, 'value' => $value, 'selected' => $this->selected == $value]
1401              ] + $flattened;
1402          }
1403  
1404          // Make non-associative array.
1405          foreach ($flattened as $key => $value) {
1406              if (!empty($value['options'])) {
1407                  $flattened[$key]['options'] = array_values($value['options']);
1408              }
1409          }
1410          $flattened = array_values($flattened);
1411  
1412          return $flattened;
1413      }
1414  
1415      /**
1416       * Export for template.
1417       *
1418       * @param renderer_base $output Renderer.
1419       * @return stdClass
1420       */
1421      public function export_for_template(renderer_base $output) {
1422          $attributes = $this->attributes;
1423  
1424          $data = new stdClass();
1425          $data->formid = !empty($this->formid) ? $this->formid : html_writer::random_id('url_select_f');
1426          $data->classes = $this->class;
1427          $data->label = $this->label;
1428          $data->disabled = $this->disabled;
1429          $data->title = $this->tooltip;
1430          $data->id = !empty($attributes['id']) ? $attributes['id'] : html_writer::random_id('url_select');
1431          $data->sesskey = sesskey();
1432          $data->action = (new moodle_url('/course/jumpto.php'))->out(false);
1433  
1434          // Remove attributes passed as property directly.
1435          unset($attributes['class']);
1436          unset($attributes['id']);
1437          unset($attributes['name']);
1438          unset($attributes['title']);
1439          unset($attributes['disabled']);
1440  
1441          $data->showbutton = $this->showbutton;
1442  
1443          // Select options.
1444          $nothing = false;
1445          if (is_string($this->nothing) && $this->nothing !== '') {
1446              $nothing = ['' => $this->nothing];
1447          } else if (is_array($this->nothing)) {
1448              $nothingvalue = reset($this->nothing);
1449              if ($nothingvalue === 'choose' || $nothingvalue === 'choosedots') {
1450                  $nothing = [key($this->nothing) => get_string('choosedots')];
1451              } else {
1452                  $nothing = $this->nothing;
1453              }
1454          }
1455          $data->options = $this->flatten_options($this->urls, $nothing);
1456  
1457          // Label attributes.
1458          $data->labelattributes = [];
1459          // Unset label attributes that are already in the template.
1460          unset($this->labelattributes['for']);
1461          // Map the label attributes.
1462          foreach ($this->labelattributes as $key => $value) {
1463              $data->labelattributes[] = ['name' => $key, 'value' => $value];
1464          }
1465  
1466          // Help icon.
1467          $data->helpicon = !empty($this->helpicon) ? $this->helpicon->export_for_template($output) : false;
1468  
1469          // Finally all the remaining attributes.
1470          $data->attributes = [];
1471          foreach ($attributes as $key => $value) {
1472              $data->attributes[] = ['name' => $key, 'value' => $value];
1473          }
1474  
1475          return $data;
1476      }
1477  }
1478  
1479  /**
1480   * Data structure describing html link with special action attached.
1481   *
1482   * @copyright 2010 Petr Skoda
1483   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1484   * @since Moodle 2.0
1485   * @package core
1486   * @category output
1487   */
1488  class action_link implements renderable {
1489  
1490      /**
1491       * @var moodle_url Href url
1492       */
1493      public $url;
1494  
1495      /**
1496       * @var string Link text HTML fragment
1497       */
1498      public $text;
1499  
1500      /**
1501       * @var array HTML attributes
1502       */
1503      public $attributes;
1504  
1505      /**
1506       * @var array List of actions attached to link
1507       */
1508      public $actions;
1509  
1510      /**
1511       * @var pix_icon Optional pix icon to render with the link
1512       */
1513      public $icon;
1514  
1515      /**
1516       * Constructor
1517       * @param moodle_url $url
1518       * @param string $text HTML fragment
1519       * @param component_action $action
1520       * @param array $attributes associative array of html link attributes + disabled
1521       * @param pix_icon $icon optional pix_icon to render with the link text
1522       */
1523      public function __construct(moodle_url $url,
1524                                  $text,
1525                                  component_action $action=null,
1526                                  array $attributes=null,
1527                                  pix_icon $icon=null) {
1528          $this->url = clone($url);
1529          $this->text = $text;
1530          if (empty($attributes['id'])) {
1531              $attributes['id'] = html_writer::random_id('action_link');
1532          }
1533          $this->attributes = (array)$attributes;
1534          if ($action) {
1535              $this->add_action($action);
1536          }
1537          $this->icon = $icon;
1538      }
1539  
1540      /**
1541       * Add action to the link.
1542       *
1543       * @param component_action $action
1544       */
1545      public function add_action(component_action $action) {
1546          $this->actions[] = $action;
1547      }
1548  
1549      /**
1550       * Adds a CSS class to this action link object
1551       * @param string $class
1552       */
1553      public function add_class($class) {
1554          if (empty($this->attributes['class'])) {
1555              $this->attributes['class'] = $class;
1556          } else {
1557              $this->attributes['class'] .= ' ' . $class;
1558          }
1559      }
1560  
1561      /**
1562       * Returns true if the specified class has been added to this link.
1563       * @param string $class
1564       * @return bool
1565       */
1566      public function has_class($class) {
1567          return strpos(' ' . $this->attributes['class'] . ' ', ' ' . $class . ' ') !== false;
1568      }
1569  
1570      /**
1571       * Return the rendered HTML for the icon. Useful for rendering action links in a template.
1572       * @return string
1573       */
1574      public function get_icon_html() {
1575          global $OUTPUT;
1576          if (!$this->icon) {
1577              return '';
1578          }
1579          return $OUTPUT->render($this->icon);
1580      }
1581  
1582      /**
1583       * Export for template.
1584       *
1585       * @param renderer_base $output The renderer.
1586       * @return stdClass
1587       */
1588      public function export_for_template(renderer_base $output) {
1589          $data = new stdClass();
1590          $attributes = $this->attributes;
1591  
1592          $data->id = $attributes['id'];
1593          unset($attributes['id']);
1594  
1595          $data->disabled = !empty($attributes['disabled']);
1596          unset($attributes['disabled']);
1597  
1598          $data->text = $this->text instanceof renderable ? $output->render($this->text) : (string) $this->text;
1599          $data->url = $this->url ? $this->url->out(false) : '';
1600          $data->icon = $this->icon ? $this->icon->export_for_pix() : null;
1601          $data->classes = isset($attributes['class']) ? $attributes['class'] : '';
1602          unset($attributes['class']);
1603  
1604          $data->attributes = array_map(function($key, $value) {
1605              return [
1606                  'name' => $key,
1607                  'value' => $value
1608              ];
1609          }, array_keys($attributes), $attributes);
1610  
1611          $data->actions = array_map(function($action) use ($output) {
1612              return $action->export_for_template($output);
1613          }, !empty($this->actions) ? $this->actions : []);
1614          $data->hasactions = !empty($this->actions);
1615  
1616          return $data;
1617      }
1618  }
1619  
1620  /**
1621   * Simple html output class
1622   *
1623   * @copyright 2009 Tim Hunt, 2010 Petr Skoda
1624   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1625   * @since Moodle 2.0
1626   * @package core
1627   * @category output
1628   */
1629  class html_writer {
1630  
1631      /**
1632       * Outputs a tag with attributes and contents
1633       *
1634       * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
1635       * @param string $contents What goes between the opening and closing tags
1636       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1637       * @return string HTML fragment
1638       */
1639      public static function tag($tagname, $contents, array $attributes = null) {
1640          return self::start_tag($tagname, $attributes) . $contents . self::end_tag($tagname);
1641      }
1642  
1643      /**
1644       * Outputs an opening tag with attributes
1645       *
1646       * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
1647       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1648       * @return string HTML fragment
1649       */
1650      public static function start_tag($tagname, array $attributes = null) {
1651          return '<' . $tagname . self::attributes($attributes) . '>';
1652      }
1653  
1654      /**
1655       * Outputs a closing tag
1656       *
1657       * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
1658       * @return string HTML fragment
1659       */
1660      public static function end_tag($tagname) {
1661          return '</' . $tagname . '>';
1662      }
1663  
1664      /**
1665       * Outputs an empty tag with attributes
1666       *
1667       * @param string $tagname The name of tag ('input', 'img', 'br' etc.)
1668       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1669       * @return string HTML fragment
1670       */
1671      public static function empty_tag($tagname, array $attributes = null) {
1672          return '<' . $tagname . self::attributes($attributes) . ' />';
1673      }
1674  
1675      /**
1676       * Outputs a tag, but only if the contents are not empty
1677       *
1678       * @param string $tagname The name of tag ('a', 'img', 'span' etc.)
1679       * @param string $contents What goes between the opening and closing tags
1680       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1681       * @return string HTML fragment
1682       */
1683      public static function nonempty_tag($tagname, $contents, array $attributes = null) {
1684          if ($contents === '' || is_null($contents)) {
1685              return '';
1686          }
1687          return self::tag($tagname, $contents, $attributes);
1688      }
1689  
1690      /**
1691       * Outputs a HTML attribute and value
1692       *
1693       * @param string $name The name of the attribute ('src', 'href', 'class' etc.)
1694       * @param string $value The value of the attribute. The value will be escaped with {@link s()}
1695       * @return string HTML fragment
1696       */
1697      public static function attribute($name, $value) {
1698          if ($value instanceof moodle_url) {
1699              return ' ' . $name . '="' . $value->out() . '"';
1700          }
1701  
1702          // special case, we do not want these in output
1703          if ($value === null) {
1704              return '';
1705          }
1706  
1707          // no sloppy trimming here!
1708          return ' ' . $name . '="' . s($value) . '"';
1709      }
1710  
1711      /**
1712       * Outputs a list of HTML attributes and values
1713       *
1714       * @param array $attributes The tag attributes (array('src' => $url, 'class' => 'class1') etc.)
1715       *       The values will be escaped with {@link s()}
1716       * @return string HTML fragment
1717       */
1718      public static function attributes(array $attributes = null) {
1719          $attributes = (array)$attributes;
1720          $output = '';
1721          foreach ($attributes as $name => $value) {
1722              $output .= self::attribute($name, $value);
1723          }
1724          return $output;
1725      }
1726  
1727      /**
1728       * Generates a simple image tag with attributes.
1729       *
1730       * @param string $src The source of image
1731       * @param string $alt The alternate text for image
1732       * @param array $attributes The tag attributes (array('height' => $max_height, 'class' => 'class1') etc.)
1733       * @return string HTML fragment
1734       */
1735      public static function img($src, $alt, array $attributes = null) {
1736          $attributes = (array)$attributes;
1737          $attributes['src'] = $src;
1738          $attributes['alt'] = $alt;
1739  
1740          return self::empty_tag('img', $attributes);
1741      }
1742  
1743      /**
1744       * Generates random html element id.
1745       *
1746       * @staticvar int $counter
1747       * @staticvar type $uniq
1748       * @param string $base A string fragment that will be included in the random ID.
1749       * @return string A unique ID
1750       */
1751      public static function random_id($base='random') {
1752          static $counter = 0;
1753          static $uniq;
1754  
1755          if (!isset($uniq)) {
1756              $uniq = uniqid();
1757          }
1758  
1759          $counter++;
1760          return $base.$uniq.$counter;
1761      }
1762  
1763      /**
1764       * Generates a simple html link
1765       *
1766       * @param string|moodle_url $url The URL
1767       * @param string $text The text
1768       * @param array $attributes HTML attributes
1769       * @return string HTML fragment
1770       */
1771      public static function link($url, $text, array $attributes = null) {
1772          $attributes = (array)$attributes;
1773          $attributes['href']  = $url;
1774          return self::tag('a', $text, $attributes);
1775      }
1776  
1777      /**
1778       * Generates a simple checkbox with optional label
1779       *
1780       * @param string $name The name of the checkbox
1781       * @param string $value The value of the checkbox
1782       * @param bool $checked Whether the checkbox is checked
1783       * @param string $label The label for the checkbox
1784       * @param array $attributes Any attributes to apply to the checkbox
1785       * @param array $labelattributes Any attributes to apply to the label, if present
1786       * @return string html fragment
1787       */
1788      public static function checkbox($name, $value, $checked = true, $label = '',
1789              array $attributes = null, array $labelattributes = null) {
1790          $attributes = (array) $attributes;
1791          $output = '';
1792  
1793          if ($label !== '' and !is_null($label)) {
1794              if (empty($attributes['id'])) {
1795                  $attributes['id'] = self::random_id('checkbox_');
1796              }
1797          }
1798          $attributes['type']    = 'checkbox';
1799          $attributes['value']   = $value;
1800          $attributes['name']    = $name;
1801          $attributes['checked'] = $checked ? 'checked' : null;
1802  
1803          $output .= self::empty_tag('input', $attributes);
1804  
1805          if ($label !== '' and !is_null($label)) {
1806              $labelattributes = (array) $labelattributes;
1807              $labelattributes['for'] = $attributes['id'];
1808              $output .= self::tag('label', $label, $labelattributes);
1809          }
1810  
1811          return $output;
1812      }
1813  
1814      /**
1815       * Generates a simple select yes/no form field
1816       *
1817       * @param string $name name of select element
1818       * @param bool $selected
1819       * @param array $attributes - html select element attributes
1820       * @return string HTML fragment
1821       */
1822      public static function select_yes_no($name, $selected=true, array $attributes = null) {
1823          $options = array('1'=>get_string('yes'), '0'=>get_string('no'));
1824          return self::select($options, $name, $selected, null, $attributes);
1825      }
1826  
1827      /**
1828       * Generates a simple select form field
1829       *
1830       * Note this function does HTML escaping on the optgroup labels, but not on the choice labels.
1831       *
1832       * @param array $options associative array value=>label ex.:
1833       *                array(1=>'One, 2=>Two)
1834       *              it is also possible to specify optgroup as complex label array ex.:
1835       *                array(array('Odd'=>array(1=>'One', 3=>'Three)), array('Even'=>array(2=>'Two')))
1836       *                array(1=>'One', '--1uniquekey'=>array('More'=>array(2=>'Two', 3=>'Three')))
1837       * @param string $name name of select element
1838       * @param string|array $selected value or array of values depending on multiple attribute
1839       * @param array|bool $nothing add nothing selected option, or false of not added
1840       * @param array $attributes html select element attributes
1841       * @return string HTML fragment
1842       */
1843      public static function select(array $options, $name, $selected = '', $nothing = array('' => 'choosedots'), array $attributes = null) {
1844          $attributes = (array)$attributes;
1845          if (is_array($nothing)) {
1846              foreach ($nothing as $k=>$v) {
1847                  if ($v === 'choose' or $v === 'choosedots') {
1848                      $nothing[$k] = get_string('choosedots');
1849                  }
1850              }
1851              $options = $nothing + $options; // keep keys, do not override
1852  
1853          } else if (is_string($nothing) and $nothing !== '') {
1854              // BC
1855              $options = array(''=>$nothing) + $options;
1856          }
1857  
1858          // we may accept more values if multiple attribute specified
1859          $selected = (array)$selected;
1860          foreach ($selected as $k=>$v) {
1861              $selected[$k] = (string)$v;
1862          }
1863  
1864          if (!isset($attributes['id'])) {
1865              $id = 'menu'.$name;
1866              // name may contaion [], which would make an invalid id. e.g. numeric question type editing form, assignment quickgrading
1867              $id = str_replace('[', '', $id);
1868              $id = str_replace(']', '', $id);
1869              $attributes['id'] = $id;
1870          }
1871  
1872          if (!isset($attributes['class'])) {
1873              $class = 'menu'.$name;
1874              // name may contaion [], which would make an invalid class. e.g. numeric question type editing form, assignment quickgrading
1875              $class = str_replace('[', '', $class);
1876              $class = str_replace(']', '', $class);
1877              $attributes['class'] = $class;
1878          }
1879          $attributes['class'] = 'select custom-select ' . $attributes['class']; // Add 'select' selector always.
1880  
1881          $attributes['name'] = $name;
1882  
1883          if (!empty($attributes['disabled'])) {
1884              $attributes['disabled'] = 'disabled';
1885          } else {
1886              unset($attributes['disabled']);
1887          }
1888  
1889          $output = '';
1890          foreach ($options as $value=>$label) {
1891              if (is_array($label)) {
1892                  // ignore key, it just has to be unique
1893                  $output .= self::select_optgroup(key($label), current($label), $selected);
1894              } else {
1895                  $output .= self::select_option($label, $value, $selected);
1896              }
1897          }
1898          return self::tag('select', $output, $attributes);
1899      }
1900  
1901      /**
1902       * Returns HTML to display a select box option.
1903       *
1904       * @param string $label The label to display as the option.
1905       * @param string|int $value The value the option represents
1906       * @param array $selected An array of selected options
1907       * @return string HTML fragment
1908       */
1909      private static function select_option($label, $value, array $selected) {
1910          $attributes = array();
1911          $value = (string)$value;
1912          if (in_array($value, $selected, true)) {
1913              $attributes['selected'] = 'selected';
1914          }
1915          $attributes['value'] = $value;
1916          return self::tag('option', $label, $attributes);
1917      }
1918  
1919      /**
1920       * Returns HTML to display a select box option group.
1921       *
1922       * @param string $groupname The label to use for the group
1923       * @param array $options The options in the group
1924       * @param array $selected An array of selected values.
1925       * @return string HTML fragment.
1926       */
1927      private static function select_optgroup($groupname, $options, array $selected) {
1928          if (empty($options)) {
1929              return '';
1930          }
1931          $attributes = array('label'=>$groupname);
1932          $output = '';
1933          foreach ($options as $value=>$label) {
1934              $output .= self::select_option($label, $value, $selected);
1935          }
1936          return self::tag('optgroup', $output, $attributes);
1937      }
1938  
1939      /**
1940       * This is a shortcut for making an hour selector menu.
1941       *
1942       * @param string $type The type of selector (years, months, days, hours, minutes)
1943       * @param string $name fieldname
1944       * @param int $currenttime A default timestamp in GMT
1945       * @param int $step minute spacing
1946       * @param array $attributes - html select element attributes
1947       * @return HTML fragment
1948       */
1949      public static function select_time($type, $name, $currenttime = 0, $step = 5, array $attributes = null) {
1950          global $OUTPUT;
1951  
1952          if (!$currenttime) {
1953              $currenttime = time();
1954          }
1955          $calendartype = \core_calendar\type_factory::get_calendar_instance();
1956          $currentdate = $calendartype->timestamp_to_date_array($currenttime);
1957          $userdatetype = $type;
1958          $timeunits = array();
1959  
1960          switch ($type) {
1961              case 'years':
1962                  $timeunits = $calendartype->get_years();
1963                  $userdatetype = 'year';
1964                  break;
1965              case 'months':
1966                  $timeunits = $calendartype->get_months();
1967                  $userdatetype = 'month';
1968                  $currentdate['month'] = (int)$currentdate['mon'];
1969                  break;
1970              case 'days':
1971                  $timeunits = $calendartype->get_days();
1972                  $userdatetype = 'mday';
1973                  break;
1974              case 'hours':
1975                  for ($i=0; $i<=23; $i++) {
1976                      $timeunits[$i] = sprintf("%02d",$i);
1977                  }
1978                  break;
1979              case 'minutes':
1980                  if ($step != 1) {
1981                      $currentdate['minutes'] = ceil($currentdate['minutes']/$step)*$step;
1982                  }
1983  
1984                  for ($i=0; $i<=59; $i+=$step) {
1985                      $timeunits[$i] = sprintf("%02d",$i);
1986                  }
1987                  break;
1988              default:
1989                  throw new coding_exception("Time type $type is not supported by html_writer::select_time().");
1990          }
1991  
1992          $attributes = (array) $attributes;
1993          $data = (object) [
1994              'name' => $name,
1995              'id' => !empty($attributes['id']) ? $attributes['id'] : self::random_id('ts_'),
1996              'label' => get_string(substr($type, 0, -1), 'form'),
1997              'options' => array_map(function($value) use ($timeunits, $currentdate, $userdatetype) {
1998                  return [
1999                      'name' => $timeunits[$value],
2000                      'value' => $value,
2001                      'selected' => $currentdate[$userdatetype] == $value
2002                  ];
2003              }, array_keys($timeunits)),
2004          ];
2005  
2006          unset($attributes['id']);
2007          unset($attributes['name']);
2008          $data->attributes = array_map(function($name) use ($attributes) {
2009              return [
2010                  'name' => $name,
2011                  'value' => $attributes[$name]
2012              ];
2013          }, array_keys($attributes));
2014  
2015          return $OUTPUT->render_from_template('core/select_time', $data);
2016      }
2017  
2018      /**
2019       * Shortcut for quick making of lists
2020       *
2021       * Note: 'list' is a reserved keyword ;-)
2022       *
2023       * @param array $items
2024       * @param array $attributes
2025       * @param string $tag ul or ol
2026       * @return string
2027       */
2028      public static function alist(array $items, array $attributes = null, $tag = 'ul') {
2029          $output = html_writer::start_tag($tag, $attributes)."\n";
2030          foreach ($items as $item) {
2031              $output .= html_writer::tag('li', $item)."\n";
2032          }
2033          $output .= html_writer::end_tag($tag);
2034          return $output;
2035      }
2036  
2037      /**
2038       * Returns hidden input fields created from url parameters.
2039       *
2040       * @param moodle_url $url
2041       * @param array $exclude list of excluded parameters
2042       * @return string HTML fragment
2043       */
2044      public static function input_hidden_params(moodle_url $url, array $exclude = null) {
2045          $exclude = (array)$exclude;
2046          $params = $url->params();
2047          foreach ($exclude as $key) {
2048              unset($params[$key]);
2049          }
2050  
2051          $output = '';
2052          foreach ($params as $key => $value) {
2053              $attributes = array('type'=>'hidden', 'name'=>$key, 'value'=>$value);
2054              $output .= self::empty_tag('input', $attributes)."\n";
2055          }
2056          return $output;
2057      }
2058  
2059      /**
2060       * Generate a script tag containing the the specified code.
2061       *
2062       * @param string $jscode the JavaScript code
2063       * @param moodle_url|string $url optional url of the external script, $code ignored if specified
2064       * @return string HTML, the code wrapped in <script> tags.
2065       */
2066      public static function script($jscode, $url=null) {
2067          if ($jscode) {
2068              return self::tag('script', "\n//<![CDATA[\n$jscode\n//]]>\n") . "\n";
2069  
2070          } else if ($url) {
2071              return self::tag('script', '', ['src' => $url]) . "\n";
2072  
2073          } else {
2074              return '';
2075          }
2076      }
2077  
2078      /**
2079       * Renders HTML table
2080       *
2081       * This method may modify the passed instance by adding some default properties if they are not set yet.
2082       * If this is not what you want, you should make a full clone of your data before passing them to this
2083       * method. In most cases this is not an issue at all so we do not clone by default for performance
2084       * and memory consumption reasons.
2085       *
2086       * @param html_table $table data to be rendered
2087       * @return string HTML code
2088       */
2089      public static function table(html_table $table) {
2090          // prepare table data and populate missing properties with reasonable defaults
2091          if (!empty($table->align)) {
2092              foreach ($table->align as $key => $aa) {
2093                  if ($aa) {
2094                      $table->align[$key] = 'text-align:'. fix_align_rtl($aa) .';';  // Fix for RTL languages
2095                  } else {
2096                      $table->align[$key] = null;
2097                  }
2098              }
2099          }
2100          if (!empty($table->size)) {
2101              foreach ($table->size as $key => $ss) {
2102                  if ($ss) {
2103                      $table->size[$key] = 'width:'. $ss .';';
2104                  } else {
2105                      $table->size[$key] = null;
2106                  }
2107              }
2108          }
2109          if (!empty($table->wrap)) {
2110              foreach ($table->wrap as $key => $ww) {
2111                  if ($ww) {
2112                      $table->wrap[$key] = 'white-space:nowrap;';
2113                  } else {
2114                      $table->wrap[$key] = '';
2115                  }
2116              }
2117          }
2118          if (!empty($table->head)) {
2119              foreach ($table->head as $key => $val) {
2120                  if (!isset($table->align[$key])) {
2121                      $table->align[$key] = null;
2122                  }
2123                  if (!isset($table->size[$key])) {
2124                      $table->size[$key] = null;
2125                  }
2126                  if (!isset($table->wrap[$key])) {
2127                      $table->wrap[$key] = null;
2128                  }
2129  
2130              }
2131          }
2132          if (empty($table->attributes['class'])) {
2133              $table->attributes['class'] = 'generaltable';
2134          }
2135          if (!empty($table->tablealign)) {
2136              $table->attributes['class'] .= ' boxalign' . $table->tablealign;
2137          }
2138  
2139          // explicitly assigned properties override those defined via $table->attributes
2140          $table->attributes['class'] = trim($table->attributes['class']);
2141          $attributes = array_merge($table->attributes, array(
2142                  'id'            => $table->id,
2143                  'width'         => $table->width,
2144                  'summary'       => $table->summary,
2145                  'cellpadding'   => $table->cellpadding,
2146                  'cellspacing'   => $table->cellspacing,
2147              ));
2148          $output = html_writer::start_tag('table', $attributes) . "\n";
2149  
2150          $countcols = 0;
2151  
2152          // Output a caption if present.
2153          if (!empty($table->caption)) {
2154              $captionattributes = array();
2155              if ($table->captionhide) {
2156                  $captionattributes['class'] = 'accesshide';
2157              }
2158              $output .= html_writer::tag(
2159                  'caption',
2160                  $table->caption,
2161                  $captionattributes
2162              );
2163          }
2164  
2165          if (!empty($table->head)) {
2166              $countcols = count($table->head);
2167  
2168              $output .= html_writer::start_tag('thead', array()) . "\n";
2169              $output .= html_writer::start_tag('tr', array()) . "\n";
2170              $keys = array_keys($table->head);
2171              $lastkey = end($keys);
2172  
2173              foreach ($table->head as $key => $heading) {
2174                  // Convert plain string headings into html_table_cell objects
2175                  if (!($heading instanceof html_table_cell)) {
2176                      $headingtext = $heading;
2177                      $heading = new html_table_cell();
2178                      $heading->text = $headingtext;
2179                      $heading->header = true;
2180                  }
2181  
2182                  if ($heading->header !== false) {
2183                      $heading->header = true;
2184                  }
2185  
2186                  $tagtype = 'td';
2187                  if ($heading->header && (string)$heading->text != '') {
2188                      $tagtype = 'th';
2189                  }
2190  
2191                  $heading->attributes['class'] .= ' header c' . $key;
2192                  if (isset($table->headspan[$key]) && $table->headspan[$key] > 1) {
2193                      $heading->colspan = $table->headspan[$key];
2194                      $countcols += $table->headspan[$key] - 1;
2195                  }
2196  
2197                  if ($key == $lastkey) {
2198                      $heading->attributes['class'] .= ' lastcol';
2199                  }
2200                  if (isset($table->colclasses[$key])) {
2201                      $heading->attributes['class'] .= ' ' . $table->colclasses[$key];
2202                  }
2203                  $heading->attributes['class'] = trim($heading->attributes['class']);
2204                  $attributes = array_merge($heading->attributes, [
2205                      'style'     => $table->align[$key] . $table->size[$key] . $heading->style,
2206                      'colspan'   => $heading->colspan,
2207                  ]);
2208  
2209                  if ($tagtype == 'th') {
2210                      $attributes['scope'] = !empty($heading->scope) ? $heading->scope : 'col';
2211                  }
2212  
2213                  $output .= html_writer::tag($tagtype, $heading->text, $attributes) . "\n";
2214              }
2215              $output .= html_writer::end_tag('tr') . "\n";
2216              $output .= html_writer::end_tag('thead') . "\n";
2217  
2218              if (empty($table->data)) {
2219                  // For valid XHTML strict every table must contain either a valid tr
2220                  // or a valid tbody... both of which must contain a valid td
2221                  $output .= html_writer::start_tag('tbody', array('class' => 'empty'));
2222                  $output .= html_writer::tag('tr', html_writer::tag('td', '', array('colspan'=>count($table->head))));
2223                  $output .= html_writer::end_tag('tbody');
2224              }
2225          }
2226  
2227          if (!empty($table->data)) {
2228              $keys       = array_keys($table->data);
2229              $lastrowkey = end($keys);
2230              $output .= html_writer::start_tag('tbody', array());
2231  
2232              foreach ($table->data as $key => $row) {
2233                  if (($row === 'hr') && ($countcols)) {
2234                      $output .= html_writer::tag('td', html_writer::tag('div', '', array('class' => 'tabledivider')), array('colspan' => $countcols));
2235                  } else {
2236                      // Convert array rows to html_table_rows and cell strings to html_table_cell objects
2237                      if (!($row instanceof html_table_row)) {
2238                          $newrow = new html_table_row();
2239  
2240                          foreach ($row as $cell) {
2241                              if (!($cell instanceof html_table_cell)) {
2242                                  $cell = new html_table_cell($cell);
2243                              }
2244                              $newrow->cells[] = $cell;
2245                          }
2246                          $row = $newrow;
2247                      }
2248  
2249                      if (isset($table->rowclasses[$key])) {
2250                          $row->attributes['class'] .= ' ' . $table->rowclasses[$key];
2251                      }
2252  
2253                      if ($key == $lastrowkey) {
2254                          $row->attributes['class'] .= ' lastrow';
2255                      }
2256  
2257                      // Explicitly assigned properties should override those defined in the attributes.
2258                      $row->attributes['class'] = trim($row->attributes['class']);
2259                      $trattributes = array_merge($row->attributes, array(
2260                              'id'            => $row->id,
2261                              'style'         => $row->style,
2262                          ));
2263                      $output .= html_writer::start_tag('tr', $trattributes) . "\n";
2264                      $keys2 = array_keys($row->cells);
2265                      $lastkey = end($keys2);
2266  
2267                      $gotlastkey = false; //flag for sanity checking
2268                      foreach ($row->cells as $key => $cell) {
2269                          if ($gotlastkey) {
2270                              //This should never happen. Why do we have a cell after the last cell?
2271                              mtrace("A cell with key ($key) was found after the last key ($lastkey)");
2272                          }
2273  
2274                          if (!($cell instanceof html_table_cell)) {
2275                              $mycell = new html_table_cell();
2276                              $mycell->text = $cell;
2277                              $cell = $mycell;
2278                          }
2279  
2280                          if (($cell->header === true) && empty($cell->scope)) {
2281                              $cell->scope = 'row';
2282                          }
2283  
2284                          if (isset($table->colclasses[$key])) {
2285                              $cell->attributes['class'] .= ' ' . $table->colclasses[$key];
2286                          }
2287  
2288                          $cell->attributes['class'] .= ' cell c' . $key;
2289                          if ($key == $lastkey) {
2290                              $cell->attributes['class'] .= ' lastcol';
2291                              $gotlastkey = true;
2292                          }
2293                          $tdstyle = '';
2294                          $tdstyle .= isset($table->align[$key]) ? $table->align[$key] : '';
2295                          $tdstyle .= isset($table->size[$key]) ? $table->size[$key] : '';
2296                          $tdstyle .= isset($table->wrap[$key]) ? $table->wrap[$key] : '';
2297                          $cell->attributes['class'] = trim($cell->attributes['class']);
2298                          $tdattributes = array_merge($cell->attributes, array(
2299                                  'style' => $tdstyle . $cell->style,
2300                                  'colspan' => $cell->colspan,
2301                                  'rowspan' => $cell->rowspan,
2302                                  'id' => $cell->id,
2303                                  'abbr' => $cell->abbr,
2304                                  'scope' => $cell->scope,
2305                              ));
2306                          $tagtype = 'td';
2307                          if ($cell->header === true) {
2308                              $tagtype = 'th';
2309                          }
2310                          $output .= html_writer::tag($tagtype, $cell->text, $tdattributes) . "\n";
2311                      }
2312                  }
2313                  $output .= html_writer::end_tag('tr') . "\n";
2314              }
2315              $output .= html_writer::end_tag('tbody') . "\n";
2316          }
2317          $output .= html_writer::end_tag('table') . "\n";
2318  
2319          if ($table->responsive) {
2320              return self::div($output, 'table-responsive');
2321          }
2322  
2323          return $output;
2324      }
2325  
2326      /**
2327       * Renders form element label
2328       *
2329       * By default, the label is suffixed with a label separator defined in the
2330       * current language pack (colon by default in the English lang pack).
2331       * Adding the colon can be explicitly disabled if needed. Label separators
2332       * are put outside the label tag itself so they are not read by
2333       * screenreaders (accessibility).
2334       *
2335       * Parameter $for explicitly associates the label with a form control. When
2336       * set, the value of this attribute must be the same as the value of
2337       * the id attribute of the form control in the same document. When null,
2338       * the label being defined is associated with the control inside the label
2339       * element.
2340       *
2341       * @param string $text content of the label tag
2342       * @param string|null $for id of the element this label is associated with, null for no association
2343       * @param bool $colonize add label separator (colon) to the label text, if it is not there yet
2344       * @param array $attributes to be inserted in the tab, for example array('accesskey' => 'a')
2345       * @return string HTML of the label element
2346       */
2347      public static function label($text, $for, $colonize = true, array $attributes=array()) {
2348          if (!is_null($for)) {
2349              $attributes = array_merge($attributes, array('for' => $for));
2350          }
2351          $text = trim($text ?? '');
2352          $label = self::tag('label', $text, $attributes);
2353  
2354          // TODO MDL-12192 $colonize disabled for now yet
2355          // if (!empty($text) and $colonize) {
2356          //     // the $text may end with the colon already, though it is bad string definition style
2357          //     $colon = get_string('labelsep', 'langconfig');
2358          //     if (!empty($colon)) {
2359          //         $trimmed = trim($colon);
2360          //         if ((substr($text, -strlen($trimmed)) == $trimmed) or (substr($text, -1) == ':')) {
2361          //             //debugging('The label text should not end with colon or other label separator,
2362          //             //           please fix the string definition.', DEBUG_DEVELOPER);
2363          //         } else {
2364          //             $label .= $colon;
2365          //         }
2366          //     }
2367          // }
2368  
2369          return $label;
2370      }
2371  
2372      /**
2373       * Combines a class parameter with other attributes. Aids in code reduction
2374       * because the class parameter is very frequently used.
2375       *
2376       * If the class attribute is specified both in the attributes and in the
2377       * class parameter, the two values are combined with a space between.
2378       *
2379       * @param string $class Optional CSS class (or classes as space-separated list)
2380       * @param array $attributes Optional other attributes as array
2381       * @return array Attributes (or null if still none)
2382       */
2383      private static function add_class($class = '', array $attributes = null) {
2384          if ($class !== '') {
2385              $classattribute = array('class' => $class);
2386              if ($attributes) {
2387                  if (array_key_exists('class', $attributes)) {
2388                      $attributes['class'] = trim($attributes['class'] . ' ' . $class);
2389                  } else {
2390                      $attributes = $classattribute + $attributes;
2391                  }
2392              } else {
2393                  $attributes = $classattribute;
2394              }
2395          }
2396          return $attributes;
2397      }
2398  
2399      /**
2400       * Creates a <div> tag. (Shortcut function.)
2401       *
2402       * @param string $content HTML content of tag
2403       * @param string $class Optional CSS class (or classes as space-separated list)
2404       * @param array $attributes Optional other attributes as array
2405       * @return string HTML code for div
2406       */
2407      public static function div($content, $class = '', array $attributes = null) {
2408          return self::tag('div', $content, self::add_class($class, $attributes));
2409      }
2410  
2411      /**
2412       * Starts a <div> tag. (Shortcut function.)
2413       *
2414       * @param string $class Optional CSS class (or classes as space-separated list)
2415       * @param array $attributes Optional other attributes as array
2416       * @return string HTML code for open div tag
2417       */
2418      public static function start_div($class = '', array $attributes = null) {
2419          return self::start_tag('div', self::add_class($class, $attributes));
2420      }
2421  
2422      /**
2423       * Ends a <div> tag. (Shortcut function.)
2424       *
2425       * @return string HTML code for close div tag
2426       */
2427      public static function end_div() {
2428          return self::end_tag('div');
2429      }
2430  
2431      /**
2432       * Creates a <span> tag. (Shortcut function.)
2433       *
2434       * @param string $content HTML content of tag
2435       * @param string $class Optional CSS class (or classes as space-separated list)
2436       * @param array $attributes Optional other attributes as array
2437       * @return string HTML code for span
2438       */
2439      public static function span($content, $class = '', array $attributes = null) {
2440          return self::tag('span', $content, self::add_class($class, $attributes));
2441      }
2442  
2443      /**
2444       * Starts a <span> tag. (Shortcut function.)
2445       *
2446       * @param string $class Optional CSS class (or classes as space-separated list)
2447       * @param array $attributes Optional other attributes as array
2448       * @return string HTML code for open span tag
2449       */
2450      public static function start_span($class = '', array $attributes = null) {
2451          return self::start_tag('span', self::add_class($class, $attributes));
2452      }
2453  
2454      /**
2455       * Ends a <span> tag. (Shortcut function.)
2456       *
2457       * @return string HTML code for close span tag
2458       */
2459      public static function end_span() {
2460          return self::end_tag('span');
2461      }
2462  }
2463  
2464  /**
2465   * Simple javascript output class
2466   *
2467   * @copyright 2010 Petr Skoda
2468   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2469   * @since Moodle 2.0
2470   * @package core
2471   * @category output
2472   */
2473  class js_writer {
2474  
2475      /**
2476       * Returns javascript code calling the function
2477       *
2478       * @param string $function function name, can be complex like Y.Event.purgeElement
2479       * @param array $arguments parameters
2480       * @param int $delay execution delay in seconds
2481       * @return string JS code fragment
2482       */
2483      public static function function_call($function, array $arguments = null, $delay=0) {
2484          if ($arguments) {
2485              $arguments = array_map('json_encode', convert_to_array($arguments));
2486              $arguments = implode(', ', $arguments);
2487          } else {
2488              $arguments = '';
2489          }
2490          $js = "$function($arguments);";
2491  
2492          if ($delay) {
2493              $delay = $delay * 1000; // in miliseconds
2494              $js = "setTimeout(function() { $js }, $delay);";
2495          }
2496          return $js . "\n";
2497      }
2498  
2499      /**
2500       * Special function which adds Y as first argument of function call.
2501       *
2502       * @param string $function The function to call
2503       * @param array $extraarguments Any arguments to pass to it
2504       * @return string Some JS code
2505       */
2506      public static function function_call_with_Y($function, array $extraarguments = null) {
2507          if ($extraarguments) {
2508              $extraarguments = array_map('json_encode', convert_to_array($extraarguments));
2509              $arguments = 'Y, ' . implode(', ', $extraarguments);
2510          } else {
2511              $arguments = 'Y';
2512          }
2513          return "$function($arguments);\n";
2514      }
2515  
2516      /**
2517       * Returns JavaScript code to initialise a new object
2518       *
2519       * @param string $var If it is null then no var is assigned the new object.
2520       * @param string $class The class to initialise an object for.
2521       * @param array $arguments An array of args to pass to the init method.
2522       * @param array $requirements Any modules required for this class.
2523       * @param int $delay The delay before initialisation. 0 = no delay.
2524       * @return string Some JS code
2525       */
2526      public static function object_init($var, $class, array $arguments = null, array $requirements = null, $delay=0) {
2527          if (is_array($arguments)) {
2528              $arguments = array_map('json_encode', convert_to_array($arguments));
2529              $arguments = implode(', ', $arguments);
2530          }
2531  
2532          if ($var === null) {
2533              $js = "new $class(Y, $arguments);";
2534          } else if (strpos($var, '.')!==false) {
2535              $js = "$var = new $class(Y, $arguments);";
2536          } else {
2537              $js = "var $var = new $class(Y, $arguments);";
2538          }
2539  
2540          if ($delay) {
2541              $delay = $delay * 1000; // in miliseconds
2542              $js = "setTimeout(function() { $js }, $delay);";
2543          }
2544  
2545          if (count($requirements) > 0) {
2546              $requirements = implode("', '", $requirements);
2547              $js = "Y.use('$requirements', function(Y){ $js });";
2548          }
2549          return $js."\n";
2550      }
2551  
2552      /**
2553       * Returns code setting value to variable
2554       *
2555       * @param string $name
2556       * @param mixed $value json serialised value
2557       * @param bool $usevar add var definition, ignored for nested properties
2558       * @return string JS code fragment
2559       */
2560      public static function set_variable($name, $value, $usevar = true) {
2561          $output = '';
2562  
2563          if ($usevar) {
2564              if (strpos($name, '.')) {
2565                  $output .= '';
2566              } else {
2567                  $output .= 'var ';
2568              }
2569          }
2570  
2571          $output .= "$name = ".json_encode($value).";";
2572  
2573          return $output;
2574      }
2575  
2576      /**
2577       * Writes event handler attaching code
2578       *
2579       * @param array|string $selector standard YUI selector for elements, may be
2580       *     array or string, element id is in the form "#idvalue"
2581       * @param string $event A valid DOM event (click, mousedown, change etc.)
2582       * @param string $function The name of the function to call
2583       * @param array $arguments An optional array of argument parameters to pass to the function
2584       * @return string JS code fragment
2585       */
2586      public static function event_handler($selector, $event, $function, array $arguments = null) {
2587          $selector = json_encode($selector);
2588          $output = "Y.on('$event', $function, $selector, null";
2589          if (!empty($arguments)) {
2590              $output .= ', ' . json_encode($arguments);
2591          }
2592          return $output . ");\n";
2593      }
2594  }
2595  
2596  /**
2597   * Holds all the information required to render a <table> by {@link core_renderer::table()}
2598   *
2599   * Example of usage:
2600   * $t = new html_table();
2601   * ... // set various properties of the object $t as described below
2602   * echo html_writer::table($t);
2603   *
2604   * @copyright 2009 David Mudrak <david.mudrak@gmail.com>
2605   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2606   * @since Moodle 2.0
2607   * @package core
2608   * @category output
2609   */
2610  class html_table {
2611  
2612      /**
2613       * @var string Value to use for the id attribute of the table
2614       */
2615      public $id = null;
2616  
2617      /**
2618       * @var array Attributes of HTML attributes for the <table> element
2619       */
2620      public $attributes = array();
2621  
2622      /**
2623       * @var array An array of headings. The n-th array item is used as a heading of the n-th column.
2624       * For more control over the rendering of the headers, an array of html_table_cell objects
2625       * can be passed instead of an array of strings.
2626       *
2627       * Example of usage:
2628       * $t->head = array('Student', 'Grade');
2629       */
2630      public $head;
2631  
2632      /**
2633       * @var array An array that can be used to make a heading span multiple columns.
2634       * In this example, {@link html_table:$data} is supposed to have three columns. For the first two columns,
2635       * the same heading is used. Therefore, {@link html_table::$head} should consist of two items.
2636       *
2637       * Example of usage:
2638       * $t->headspan = array(2,1);
2639       */
2640      public $headspan;
2641  
2642      /**
2643       * @var array An array of column alignments.
2644       * The value is used as CSS 'text-align' property. Therefore, possible
2645       * values are 'left', 'right', 'center' and 'justify'. Specify 'right' or 'left' from the perspective
2646       * of a left-to-right (LTR) language. For RTL, the values are flipped automatically.
2647       *
2648       * Examples of usage:
2649       * $t->align = array(null, 'right');
2650       * or
2651       * $t->align[1] = 'right';
2652       */
2653      public $align;
2654  
2655      /**
2656       * @var array The value is used as CSS 'size' property.
2657       *
2658       * Examples of usage:
2659       * $t->size = array('50%', '50%');
2660       * or
2661       * $t->size[1] = '120px';
2662       */
2663      public $size;
2664  
2665      /**
2666       * @var array An array of wrapping information.
2667       * The only possible value is 'nowrap' that sets the
2668       * CSS property 'white-space' to the value 'nowrap' in the given column.
2669       *
2670       * Example of usage:
2671       * $t->wrap = array(null, 'nowrap');
2672       */
2673      public $wrap;
2674  
2675      /**
2676       * @var array Array of arrays or html_table_row objects containing the data. Alternatively, if you have
2677       * $head specified, the string 'hr' (for horizontal ruler) can be used
2678       * instead of an array of cells data resulting in a divider rendered.
2679       *
2680       * Example of usage with array of arrays:
2681       * $row1 = array('Harry Potter', '76 %');
2682       * $row2 = array('Hermione Granger', '100 %');
2683       * $t->data = array($row1, $row2);
2684       *
2685       * Example with array of html_table_row objects: (used for more fine-grained control)
2686       * $cell1 = new html_table_cell();
2687       * $cell1->text = 'Harry Potter';
2688       * $cell1->colspan = 2;
2689       * $row1 = new html_table_row();
2690       * $row1->cells[] = $cell1;
2691       * $cell2 = new html_table_cell();
2692       * $cell2->text = 'Hermione Granger';
2693       * $cell3 = new html_table_cell();
2694       * $cell3->text = '100 %';
2695       * $row2 = new html_table_row();
2696       * $row2->cells = array($cell2, $cell3);
2697       * $t->data = array($row1, $row2);
2698       */
2699      public $data = [];
2700  
2701      /**
2702       * @deprecated since Moodle 2.0. Styling should be in the CSS.
2703       * @var string Width of the table, percentage of the page preferred.
2704       */
2705      public $width = null;
2706  
2707      /**
2708       * @deprecated since Moodle 2.0. Styling should be in the CSS.
2709       * @var string Alignment for the whole table. Can be 'right', 'left' or 'center' (default).
2710       */
2711      public $tablealign = null;
2712  
2713      /**
2714       * @deprecated since Moodle 2.0. Styling should be in the CSS.
2715       * @var int Padding on each cell, in pixels
2716       */
2717      public $cellpadding = null;
2718  
2719      /**
2720       * @var int Spacing between cells, in pixels
2721       * @deprecated since Moodle 2.0. Styling should be in the CSS.
2722       */
2723      public $cellspacing = null;
2724  
2725      /**
2726       * @var array Array of classes to add to particular rows, space-separated string.
2727       * Class 'lastrow' is added automatically for the last row in the table.
2728       *
2729       * Example of usage:
2730       * $t->rowclasses[9] = 'tenth'
2731       */
2732      public $rowclasses;
2733  
2734      /**
2735       * @var array An array of classes to add to every cell in a particular column,
2736       * space-separated string. Class 'cell' is added automatically by the renderer.
2737       * Classes 'c0' or 'c1' are added automatically for every odd or even column,
2738       * respectively. Class 'lastcol' is added automatically for all last cells
2739       * in a row.
2740       *
2741       * Example of usage:
2742       * $t->colclasses = array(null, 'grade');
2743       */
2744      public $colclasses;
2745  
2746      /**
2747       * @var string Description of the contents for screen readers.
2748       *
2749       * The "summary" attribute on the "table" element is not supported in HTML5.
2750       * Consider describing the structure of the table in a "caption" element or in a "figure" element containing the table;
2751       * or, simplify the structure of the table so that no description is needed.
2752       *
2753       * @deprecated since Moodle 3.9.
2754       */
2755      public $summary;
2756  
2757      /**
2758       * @var string Caption for the table, typically a title.
2759       *
2760       * Example of usage:
2761       * $t->caption = "TV Guide";
2762       */
2763      public $caption;
2764  
2765      /**
2766       * @var bool Whether to hide the table's caption from sighted users.
2767       *
2768       * Example of usage:
2769       * $t->caption = "TV Guide";
2770       * $t->captionhide = true;
2771       */
2772      public $captionhide = false;
2773  
2774      /** @var bool Whether to make the table to be scrolled horizontally with ease. Make table responsive across all viewports. */
2775      public $responsive = true;
2776  
2777      /**
2778       * Constructor
2779       */
2780      public function __construct() {
2781          $this->attributes['class'] = '';
2782      }
2783  }
2784  
2785  /**
2786   * Component representing a table row.
2787   *
2788   * @copyright 2009 Nicolas Connault
2789   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2790   * @since Moodle 2.0
2791   * @package core
2792   * @category output
2793   */
2794  class html_table_row {
2795  
2796      /**
2797       * @var string Value to use for the id attribute of the row.
2798       */
2799      public $id = null;
2800  
2801      /**
2802       * @var array Array of html_table_cell objects
2803       */
2804      public $cells = array();
2805  
2806      /**
2807       * @var string Value to use for the style attribute of the table row
2808       */
2809      public $style = null;
2810  
2811      /**
2812       * @var array Attributes of additional HTML attributes for the <tr> element
2813       */
2814      public $attributes = array();
2815  
2816      /**
2817       * Constructor
2818       * @param array $cells
2819       */
2820      public function __construct(array $cells=null) {
2821          $this->attributes['class'] = '';
2822          $cells = (array)$cells;
2823          foreach ($cells as $cell) {
2824              if ($cell instanceof html_table_cell) {
2825                  $this->cells[] = $cell;
2826              } else {
2827                  $this->cells[] = new html_table_cell($cell);
2828              }
2829          }
2830      }
2831  }
2832  
2833  /**
2834   * Component representing a table cell.
2835   *
2836   * @copyright 2009 Nicolas Connault
2837   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2838   * @since Moodle 2.0
2839   * @package core
2840   * @category output
2841   */
2842  class html_table_cell {
2843  
2844      /**
2845       * @var string Value to use for the id attribute of the cell.
2846       */
2847      public $id = null;
2848  
2849      /**
2850       * @var string The contents of the cell.
2851       */
2852      public $text;
2853  
2854      /**
2855       * @var string Abbreviated version of the contents of the cell.
2856       */
2857      public $abbr = null;
2858  
2859      /**
2860       * @var int Number of columns this cell should span.
2861       */
2862      public $colspan = null;
2863  
2864      /**
2865       * @var int Number of rows this cell should span.
2866       */
2867      public $rowspan = null;
2868  
2869      /**
2870       * @var string Defines a way to associate header cells and data cells in a table.
2871       */
2872      public $scope = null;
2873  
2874      /**
2875       * @var bool Whether or not this cell is a header cell.
2876       */
2877      public $header = null;
2878  
2879      /**
2880       * @var string Value to use for the style attribute of the table cell
2881       */
2882      public $style = null;
2883  
2884      /**
2885       * @var array Attributes of additional HTML attributes for the <td> element
2886       */
2887      public $attributes = array();
2888  
2889      /**
2890       * Constructs a table cell
2891       *
2892       * @param string $text
2893       */
2894      public function __construct($text = null) {
2895          $this->text = $text;
2896          $this->attributes['class'] = '';
2897      }
2898  }
2899  
2900  /**
2901   * Component representing a paging bar.
2902   *
2903   * @copyright 2009 Nicolas Connault
2904   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2905   * @since Moodle 2.0
2906   * @package core
2907   * @category output
2908   */
2909  class paging_bar implements renderable, templatable {
2910  
2911      /**
2912       * @var int The maximum number of pagelinks to display.
2913       */
2914      public $maxdisplay = 18;
2915  
2916      /**
2917       * @var int The total number of entries to be pages through..
2918       */
2919      public $totalcount;
2920  
2921      /**
2922       * @var int The page you are currently viewing.
2923       */
2924      public $page;
2925  
2926      /**
2927       * @var int The number of entries that should be shown per page.
2928       */
2929      public $perpage;
2930  
2931      /**
2932       * @var string|moodle_url If this  is a string then it is the url which will be appended with $pagevar,
2933       * an equals sign and the page number.
2934       * If this is a moodle_url object then the pagevar param will be replaced by
2935       * the page no, for each page.
2936       */
2937      public $baseurl;
2938  
2939      /**
2940       * @var string This is the variable name that you use for the pagenumber in your
2941       * code (ie. 'tablepage', 'blogpage', etc)
2942       */
2943      public $pagevar;
2944  
2945      /**
2946       * @var string A HTML link representing the "previous" page.
2947       */
2948      public $previouslink = null;
2949  
2950      /**
2951       * @var string A HTML link representing the "next" page.
2952       */
2953      public $nextlink = null;
2954  
2955      /**
2956       * @var string A HTML link representing the first page.
2957       */
2958      public $firstlink = null;
2959  
2960      /**
2961       * @var string A HTML link representing the last page.
2962       */
2963      public $lastlink = null;
2964  
2965      /**
2966       * @var array An array of strings. One of them is just a string: the current page
2967       */
2968      public $pagelinks = array();
2969  
2970      /**
2971       * Constructor paging_bar with only the required params.
2972       *
2973       * @param int $totalcount The total number of entries available to be paged through
2974       * @param int $page The page you are currently viewing
2975       * @param int $perpage The number of entries that should be shown per page
2976       * @param string|moodle_url $baseurl url of the current page, the $pagevar parameter is added
2977       * @param string $pagevar name of page parameter that holds the page number
2978       */
2979      public function __construct($totalcount, $page, $perpage, $baseurl, $pagevar = 'page') {
2980          $this->totalcount = $totalcount;
2981          $this->page       = $page;
2982          $this->perpage    = $perpage;
2983          $this->baseurl    = $baseurl;
2984          $this->pagevar    = $pagevar;
2985      }
2986  
2987      /**
2988       * Prepares the paging bar for output.
2989       *
2990       * This method validates the arguments set up for the paging bar and then
2991       * produces fragments of HTML to assist display later on.
2992       *
2993       * @param renderer_base $output
2994       * @param moodle_page $page
2995       * @param string $target
2996       * @throws coding_exception
2997       */
2998      public function prepare(renderer_base $output, moodle_page $page, $target) {
2999          if (!isset($this->totalcount) || is_null($this->totalcount)) {
3000              throw new coding_exception('paging_bar requires a totalcount value.');
3001          }
3002          if (!isset($this->page) || is_null($this->page)) {
3003              throw new coding_exception('paging_bar requires a page value.');
3004          }
3005          if (empty($this->perpage)) {
3006              throw new coding_exception('paging_bar requires a perpage value.');
3007          }
3008          if (empty($this->baseurl)) {
3009              throw new coding_exception('paging_bar requires a baseurl value.');
3010          }
3011  
3012          if ($this->totalcount > $this->perpage) {
3013              $pagenum = $this->page - 1;
3014  
3015              if ($this->page > 0) {
3016                  $this->previouslink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>$pagenum)), get_string('previous'), array('class'=>'previous'));
3017              }
3018  
3019              if ($this->perpage > 0) {
3020                  $lastpage = ceil($this->totalcount / $this->perpage);
3021              } else {
3022                  $lastpage = 1;
3023              }
3024  
3025              if ($this->page > round(($this->maxdisplay/3)*2)) {
3026                  $currpage = $this->page - round($this->maxdisplay/3);
3027  
3028                  $this->firstlink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>0)), '1', array('class'=>'first'));
3029              } else {
3030                  $currpage = 0;
3031              }
3032  
3033              $displaycount = $displaypage = 0;
3034  
3035              while ($displaycount < $this->maxdisplay and $currpage < $lastpage) {
3036                  $displaypage = $currpage + 1;
3037  
3038                  if ($this->page == $currpage) {
3039                      $this->pagelinks[] = html_writer::span($displaypage, 'current-page');
3040                  } else {
3041                      $pagelink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>$currpage)), $displaypage);
3042                      $this->pagelinks[] = $pagelink;
3043                  }
3044  
3045                  $displaycount++;
3046                  $currpage++;
3047              }
3048  
3049              if ($currpage < $lastpage) {
3050                  $lastpageactual = $lastpage - 1;
3051                  $this->lastlink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>$lastpageactual)), $lastpage, array('class'=>'last'));
3052              }
3053  
3054              $pagenum = $this->page + 1;
3055  
3056              if ($pagenum != $lastpage) {
3057                  $this->nextlink = html_writer::link(new moodle_url($this->baseurl, array($this->pagevar=>$pagenum)), get_string('next'), array('class'=>'next'));
3058              }
3059          }
3060      }
3061  
3062      /**
3063       * Export for template.
3064       *
3065       * @param renderer_base $output The renderer.
3066       * @return stdClass
3067       */
3068      public function export_for_template(renderer_base $output) {
3069          $data = new stdClass();
3070          $data->previous = null;
3071          $data->next = null;
3072          $data->first = null;
3073          $data->last = null;
3074          $data->label = get_string('page');
3075          $data->pages = [];
3076          $data->haspages = $this->totalcount > $this->perpage;
3077          $data->pagesize = $this->perpage;
3078  
3079          if (!$data->haspages) {
3080              return $data;
3081          }
3082  
3083          if ($this->page > 0) {
3084              $data->previous = [
3085                  'page' => $this->page,
3086                  'url' => (new moodle_url($this->baseurl, [$this->pagevar => $this->page - 1]))->out(false)
3087              ];
3088          }
3089  
3090          $currpage = 0;
3091          if ($this->page > round(($this->maxdisplay / 3) * 2)) {
3092              $currpage = $this->page - round($this->maxdisplay / 3);
3093              $data->first = [
3094                  'page' => 1,
3095                  'url' => (new moodle_url($this->baseurl, [$this->pagevar => 0]))->out(false)
3096              ];
3097          }
3098  
3099          $lastpage = 1;
3100          if ($this->perpage > 0) {
3101              $lastpage = ceil($this->totalcount / $this->perpage);
3102          }
3103  
3104          $displaycount = 0;
3105          $displaypage = 0;
3106          while ($displaycount < $this->maxdisplay and $currpage < $lastpage) {
3107              $displaypage = $currpage + 1;
3108  
3109              $iscurrent = $this->page == $currpage;
3110              $link = new moodle_url($this->baseurl, [$this->pagevar => $currpage]);
3111  
3112              $data->pages[] = [
3113                  'page' => $displaypage,
3114                  'active' => $iscurrent,
3115                  'url' => $iscurrent ? null : $link->out(false)
3116              ];
3117  
3118              $displaycount++;
3119              $currpage++;
3120          }
3121  
3122          if ($currpage < $lastpage) {
3123              $data->last = [
3124                  'page' => $lastpage,
3125                  'url' => (new moodle_url($this->baseurl, [$this->pagevar => $lastpage - 1]))->out(false)
3126              ];
3127          }
3128  
3129          if ($this->page + 1 != $lastpage) {
3130              $data->next = [
3131                  'page' => $this->page + 2,
3132                  'url' => (new moodle_url($this->baseurl, [$this->pagevar => $this->page + 1]))->out(false)
3133              ];
3134          }
3135  
3136          return $data;
3137      }
3138  }
3139  
3140  /**
3141   * Component representing initials bar.
3142   *
3143   * @copyright 2017 Ilya Tregubov
3144   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3145   * @since Moodle 3.3
3146   * @package core
3147   * @category output
3148   */
3149  class initials_bar implements renderable, templatable {
3150  
3151      /**
3152       * @var string Currently selected letter.
3153       */
3154      public $current;
3155  
3156      /**
3157       * @var string Class name to add to this initial bar.
3158       */
3159      public $class;
3160  
3161      /**
3162       * @var string The name to put in front of this initial bar.
3163       */
3164      public $title;
3165  
3166      /**
3167       * @var string URL parameter name for this initial.
3168       */
3169      public $urlvar;
3170  
3171      /**
3172       * @var string URL object.
3173       */
3174      public $url;
3175  
3176      /**
3177       * @var array An array of letters in the alphabet.
3178       */
3179      public $alpha;
3180  
3181      /**
3182       * Constructor initials_bar with only the required params.
3183       *
3184       * @param string $current the currently selected letter.
3185       * @param string $class class name to add to this initial bar.
3186       * @param string $title the name to put in front of this initial bar.
3187       * @param string $urlvar URL parameter name for this initial.
3188       * @param string $url URL object.
3189       * @param array $alpha of letters in the alphabet.
3190       */
3191      public function __construct($current, $class, $title, $urlvar, $url, $alpha = null) {
3192          $this->current       = $current;
3193          $this->class    = $class;
3194          $this->title    = $title;
3195          $this->urlvar    = $urlvar;
3196          $this->url    = $url;
3197          $this->alpha    = $alpha;
3198      }
3199  
3200      /**
3201       * Export for template.
3202       *
3203       * @param renderer_base $output The renderer.
3204       * @return stdClass
3205       */
3206      public function export_for_template(renderer_base $output) {
3207          $data = new stdClass();
3208  
3209          if ($this->alpha == null) {
3210              $this->alpha = explode(',', get_string('alphabet', 'langconfig'));
3211          }
3212  
3213          if ($this->current == 'all') {
3214              $this->current = '';
3215          }
3216  
3217          // We want to find a letter grouping size which suits the language so
3218          // find the largest group size which is less than 15 chars.
3219          // The choice of 15 chars is the largest number of chars that reasonably
3220          // fits on the smallest supported screen size. By always using a max number
3221          // of groups which is a factor of 2, we always get nice wrapping, and the
3222          // last row is always the shortest.
3223          $groupsize = count($this->alpha);
3224          $groups = 1;
3225          while ($groupsize > 15) {
3226              $groups *= 2;
3227              $groupsize = ceil(count($this->alpha) / $groups);
3228          }
3229  
3230          $groupsizelimit = 0;
3231          $groupnumber = 0;
3232          foreach ($this->alpha as $letter) {
3233              if ($groupsizelimit++ > 0 && $groupsizelimit % $groupsize == 1) {
3234                  $groupnumber++;
3235              }
3236              $groupletter = new stdClass();
3237              $groupletter->name = $letter;
3238              $groupletter->url = $this->url->out(false, array($this->urlvar => $letter));
3239              if ($letter == $this->current) {
3240                  $groupletter->selected = $this->current;
3241              }
3242              if (!isset($data->group[$groupnumber])) {
3243                  $data->group[$groupnumber] = new stdClass();
3244              }
3245              $data->group[$groupnumber]->letter[] = $groupletter;
3246          }
3247  
3248          $data->class = $this->class;
3249          $data->title = $this->title;
3250          $data->url = $this->url->out(false, array($this->urlvar => ''));
3251          $data->current = $this->current;
3252          $data->all = get_string('all');
3253  
3254          return $data;
3255      }
3256  }
3257  
3258  /**
3259   * This class represents how a block appears on a page.
3260   *
3261   * During output, each block instance is asked to return a block_contents object,
3262   * those are then passed to the $OUTPUT->block function for display.
3263   *
3264   * contents should probably be generated using a moodle_block_..._renderer.
3265   *
3266   * Other block-like things that need to appear on the page, for example the
3267   * add new block UI, are also represented as block_contents objects.
3268   *
3269   * @copyright 2009 Tim Hunt
3270   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3271   * @since Moodle 2.0
3272   * @package core
3273   * @category output
3274   */
3275  class block_contents {
3276  
3277      /** Used when the block cannot be collapsed **/
3278      const NOT_HIDEABLE = 0;
3279  
3280      /** Used when the block can be collapsed but currently is not **/
3281      const VISIBLE = 1;
3282  
3283      /** Used when the block has been collapsed **/
3284      const HIDDEN = 2;
3285  
3286      /**
3287       * @var int Used to set $skipid.
3288       */
3289      protected static $idcounter = 1;
3290  
3291      /**
3292       * @var int All the blocks (or things that look like blocks) printed on
3293       * a page are given a unique number that can be used to construct id="" attributes.
3294       * This is set automatically be the {@link prepare()} method.
3295       * Do not try to set it manually.
3296       */
3297      public $skipid;
3298  
3299      /**
3300       * @var int If this is the contents of a real block, this should be set
3301       * to the block_instance.id. Otherwise this should be set to 0.
3302       */
3303      public $blockinstanceid = 0;
3304  
3305      /**
3306       * @var int If this is a real block instance, and there is a corresponding
3307       * block_position.id for the block on this page, this should be set to that id.
3308       * Otherwise it should be 0.
3309       */
3310      public $blockpositionid = 0;
3311  
3312      /**
3313       * @var array An array of attribute => value pairs that are put on the outer div of this
3314       * block. {@link $id} and {@link $classes} attributes should be set separately.
3315       */
3316      public $attributes;
3317  
3318      /**
3319       * @var string The title of this block. If this came from user input, it should already
3320       * have had format_string() processing done on it. This will be output inside
3321       * <h2> tags. Please do not cause invalid XHTML.
3322       */
3323      public $title = '';
3324  
3325      /**
3326       * @var string The label to use when the block does not, or will not have a visible title.
3327       * You should never set this as well as title... it will just be ignored.
3328       */
3329      public $arialabel = '';
3330  
3331      /**
3332       * @var string HTML for the content
3333       */
3334      public $content = '';
3335  
3336      /**
3337       * @var array An alternative to $content, it you want a list of things with optional icons.
3338       */
3339      public $footer = '';
3340  
3341      /**
3342       * @var string Any small print that should appear under the block to explain
3343       * to the teacher about the block, for example 'This is a sticky block that was
3344       * added in the system context.'
3345       */
3346      public $annotation = '';
3347  
3348      /**
3349       * @var int One of the constants NOT_HIDEABLE, VISIBLE, HIDDEN. Whether
3350       * the user can toggle whether this block is visible.
3351       */
3352      public $collapsible = self::NOT_HIDEABLE;
3353  
3354      /**
3355       * Set this to true if the block is dockable.
3356       * @var bool
3357       */
3358      public $dockable = false;
3359  
3360      /**
3361       * @var array A (possibly empty) array of editing controls. Each element of
3362       * this array should be an array('url' => $url, 'icon' => $icon, 'caption' => $caption).
3363       * $icon is the icon name. Fed to $OUTPUT->image_url.
3364       */
3365      public $controls = array();
3366  
3367  
3368      /**
3369       * Create new instance of block content
3370       * @param array $attributes
3371       */
3372      public function __construct(array $attributes = null) {
3373          $this->skipid = self::$idcounter;
3374          self::$idcounter += 1;
3375  
3376          if ($attributes) {
3377              // standard block
3378              $this->attributes = $attributes;
3379          } else {
3380              // simple "fake" blocks used in some modules and "Add new block" block
3381              $this->attributes = array('class'=>'block');
3382          }
3383      }
3384  
3385      /**
3386       * Add html class to block
3387       *
3388       * @param string $class
3389       */
3390      public function add_class($class) {
3391          $this->attributes['class'] .= ' '.$class;
3392      }
3393  
3394      /**
3395       * Check if the block is a fake block.
3396       *
3397       * @return boolean
3398       */
3399      public function is_fake() {
3400          return isset($this->attributes['data-block']) && $this->attributes['data-block'] == '_fake';
3401      }
3402  }
3403  
3404  
3405  /**
3406   * This class represents a target for where a block can go when it is being moved.
3407   *
3408   * This needs to be rendered as a form with the given hidden from fields, and
3409   * clicking anywhere in the form should submit it. The form action should be
3410   * $PAGE->url.
3411   *
3412   * @copyright 2009 Tim Hunt
3413   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3414   * @since Moodle 2.0
3415   * @package core
3416   * @category output
3417   */
3418  class block_move_target {
3419  
3420      /**
3421       * @var moodle_url Move url
3422       */
3423      public $url;
3424  
3425      /**
3426       * Constructor
3427       * @param moodle_url $url
3428       */
3429      public function __construct(moodle_url $url) {
3430          $this->url  = $url;
3431      }
3432  }
3433  
3434  /**
3435   * Custom menu item
3436   *
3437   * This class is used to represent one item within a custom menu that may or may
3438   * not have children.
3439   *
3440   * @copyright 2010 Sam Hemelryk
3441   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3442   * @since Moodle 2.0
3443   * @package core
3444   * @category output
3445   */
3446  class custom_menu_item implements renderable, templatable {
3447  
3448      /**
3449       * @var string The text to show for the item
3450       */
3451      protected $text;
3452  
3453      /**
3454       * @var moodle_url The link to give the icon if it has no children
3455       */
3456      protected $url;
3457  
3458      /**
3459       * @var string A title to apply to the item. By default the text
3460       */
3461      protected $title;
3462  
3463      /**
3464       * @var int A sort order for the item, not necessary if you order things in
3465       * the CFG var.
3466       */
3467      protected $sort;
3468  
3469      /**
3470       * @var custom_menu_item A reference to the parent for this item or NULL if
3471       * it is a top level item
3472       */
3473      protected $parent;
3474  
3475      /**
3476       * @var array A array in which to store children this item has.
3477       */
3478      protected $children = array();
3479  
3480      /**
3481       * @var int A reference to the sort var of the last child that was added
3482       */
3483      protected $lastsort = 0;
3484  
3485      /** @var array Array of other HTML attributes for the custom menu item. */
3486      protected $attributes = [];
3487  
3488      /**
3489       * Constructs the new custom menu item
3490       *
3491       * @param string $text
3492       * @param moodle_url $url A moodle url to apply as the link for this item [Optional]
3493       * @param string $title A title to apply to this item [Optional]
3494       * @param int $sort A sort or to use if we need to sort differently [Optional]
3495       * @param custom_menu_item $parent A reference to the parent custom_menu_item this child
3496       *        belongs to, only if the child has a parent. [Optional]
3497       * @param array $attributes Array of other HTML attributes for the custom menu item.
3498       */
3499      public function __construct($text, moodle_url $url = null, $title = null, $sort = null, custom_menu_item $parent = null,
3500                                  array $attributes = []) {
3501  
3502          // Use class setter method for text to ensure it's always a string type.
3503          $this->set_text($text);
3504  
3505          $this->url = $url;
3506          $this->title = $title;
3507          $this->sort = (int)$sort;
3508          $this->parent = $parent;
3509          $this->attributes = $attributes;
3510      }
3511  
3512      /**
3513       * Adds a custom menu item as a child of this node given its properties.
3514       *
3515       * @param string $text
3516       * @param moodle_url $url
3517       * @param string $title
3518       * @param int $sort
3519       * @param array $attributes Array of other HTML attributes for the custom menu item.
3520       * @return custom_menu_item
3521       */
3522      public function add($text, moodle_url $url = null, $title = null, $sort = null, $attributes = []) {
3523          $key = count($this->children);
3524          if (empty($sort)) {
3525              $sort = $this->lastsort + 1;
3526          }
3527          $this->children[$key] = new custom_menu_item($text, $url, $title, $sort, $this, $attributes);
3528          $this->lastsort = (int)$sort;
3529          return $this->children[$key];
3530      }
3531  
3532      /**
3533       * Removes a custom menu item that is a child or descendant to the current menu.
3534       *
3535       * Returns true if child was found and removed.
3536       *
3537       * @param custom_menu_item $menuitem
3538       * @return bool
3539       */
3540      public function remove_child(custom_menu_item $menuitem) {
3541          $removed = false;
3542          if (($key = array_search($menuitem, $this->children)) !== false) {
3543              unset($this->children[$key]);
3544              $this->children = array_values($this->children);
3545              $removed = true;
3546          } else {
3547              foreach ($this->children as $child) {
3548                  if ($removed = $child->remove_child($menuitem)) {
3549                      break;
3550                  }
3551              }
3552          }
3553          return $removed;
3554      }
3555  
3556      /**
3557       * Returns the text for this item
3558       * @return string
3559       */
3560      public function get_text() {
3561          return $this->text;
3562      }
3563  
3564      /**
3565       * Returns the url for this item
3566       * @return moodle_url
3567       */
3568      public function get_url() {
3569          return $this->url;
3570      }
3571  
3572      /**
3573       * Returns the title for this item
3574       * @return string
3575       */
3576      public function get_title() {
3577          return $this->title;
3578      }
3579  
3580      /**
3581       * Sorts and returns the children for this item
3582       * @return array
3583       */
3584      public function get_children() {
3585          $this->sort();
3586          return $this->children;
3587      }
3588  
3589      /**
3590       * Gets the sort order for this child
3591       * @return int
3592       */
3593      public function get_sort_order() {
3594          return $this->sort;
3595      }
3596  
3597      /**
3598       * Gets the parent this child belong to
3599       * @return custom_menu_item
3600       */
3601      public function get_parent() {
3602          return $this->parent;
3603      }
3604  
3605      /**
3606       * Sorts the children this item has
3607       */
3608      public function sort() {
3609          usort($this->children, array('custom_menu','sort_custom_menu_items'));
3610      }
3611  
3612      /**
3613       * Returns true if this item has any children
3614       * @return bool
3615       */
3616      public function has_children() {
3617          return (count($this->children) > 0);
3618      }
3619  
3620      /**
3621       * Sets the text for the node
3622       * @param string $text
3623       */
3624      public function set_text($text) {
3625          $this->text = (string)$text;
3626      }
3627  
3628      /**
3629       * Sets the title for the node
3630       * @param string $title
3631       */
3632      public function set_title($title) {
3633          $this->title = (string)$title;
3634      }
3635  
3636      /**
3637       * Sets the url for the node
3638       * @param moodle_url $url
3639       */
3640      public function set_url(moodle_url $url) {
3641          $this->url = $url;
3642      }
3643  
3644      /**
3645       * Export this data so it can be used as the context for a mustache template.
3646       *
3647       * @param renderer_base $output Used to do a final render of any components that need to be rendered for export.
3648       * @return array
3649       */
3650      public function export_for_template(renderer_base $output) {
3651          global $CFG;
3652  
3653          require_once($CFG->libdir . '/externallib.php');
3654  
3655          $syscontext = context_system::instance();
3656  
3657          $context = new stdClass();
3658          $context->moremenuid = uniqid();
3659          $context->text = external_format_string($this->text, $syscontext->id);
3660          $context->url = $this->url ? $this->url->out() : null;
3661          // No need for the title if it's the same with text.
3662          if ($this->text !== $this->title) {
3663              // Show the title attribute only if it's different from the text.
3664              $context->title = external_format_string($this->title, $syscontext->id);
3665          }
3666          $context->sort = $this->sort;
3667          if (!empty($this->attributes)) {
3668              $context->attributes = $this->attributes;
3669          }
3670          $context->children = array();
3671          if (preg_match("/^#+$/", $this->text)) {
3672              $context->divider = true;
3673          }
3674          $context->haschildren = !empty($this->children) && (count($this->children) > 0);
3675          foreach ($this->children as $child) {
3676              $child = $child->export_for_template($output);
3677              array_push($context->children, $child);
3678          }
3679  
3680          return $context;
3681      }
3682  }
3683  
3684  /**
3685   * Custom menu class
3686   *
3687   * This class is used to operate a custom menu that can be rendered for the page.
3688   * The custom menu is built using $CFG->custommenuitems and is a structured collection
3689   * of custom_menu_item nodes that can be rendered by the core renderer.
3690   *
3691   * To configure the custom menu:
3692   *     Settings: Administration > Appearance > Themes > Theme settings
3693   *
3694   * @copyright 2010 Sam Hemelryk
3695   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3696   * @since Moodle 2.0
3697   * @package core
3698   * @category output
3699   */
3700  class custom_menu extends custom_menu_item {
3701  
3702      /**
3703       * @var string The language we should render for, null disables multilang support.
3704       */
3705      protected $currentlanguage = null;
3706  
3707      /**
3708       * Creates the custom menu
3709       *
3710       * @param string $definition the menu items definition in syntax required by {@link convert_text_to_menu_nodes()}
3711       * @param string $currentlanguage the current language code, null disables multilang support
3712       */
3713      public function __construct($definition = '', $currentlanguage = null) {
3714          $this->currentlanguage = $currentlanguage;
3715          parent::__construct('root'); // create virtual root element of the menu
3716          if (!empty($definition)) {
3717              $this->override_children(self::convert_text_to_menu_nodes($definition, $currentlanguage));
3718          }
3719      }
3720  
3721      /**
3722       * Overrides the children of this custom menu. Useful when getting children
3723       * from $CFG->custommenuitems
3724       *
3725       * @param array $children
3726       */
3727      public function override_children(array $children) {
3728          $this->children = array();
3729          foreach ($children as $child) {
3730              if ($child instanceof custom_menu_item) {
3731                  $this->children[] = $child;
3732              }
3733          }
3734      }
3735  
3736      /**
3737       * Converts a string into a structured array of custom_menu_items which can
3738       * then be added to a custom menu.
3739       *
3740       * Structure:
3741       *     text|url|title|langs
3742       * The number of hyphens at the start determines the depth of the item. The
3743       * languages are optional, comma separated list of languages the line is for.
3744       *
3745       * Example structure:
3746       *     First level first item|http://www.moodle.com/
3747       *     -Second level first item|http://www.moodle.com/partners/
3748       *     -Second level second item|http://www.moodle.com/hq/
3749       *     --Third level first item|http://www.moodle.com/jobs/
3750       *     -Second level third item|http://www.moodle.com/development/
3751       *     First level second item|http://www.moodle.com/feedback/
3752       *     First level third item
3753       *     English only|http://moodle.com|English only item|en
3754       *     German only|http://moodle.de|Deutsch|de,de_du,de_kids
3755       *
3756       *
3757       * @static
3758       * @param string $text the menu items definition
3759       * @param string $language the language code, null disables multilang support
3760       * @return array
3761       */
3762      public static function convert_text_to_menu_nodes($text, $language = null) {
3763          $root = new custom_menu();
3764          $lastitem = $root;
3765          $lastdepth = 0;
3766          $hiddenitems = array();
3767          $lines = explode("\n", $text);
3768          foreach ($lines as $linenumber => $line) {
3769              $line = trim($line);
3770              if (strlen($line) == 0) {
3771                  continue;
3772              }
3773              // Parse item settings.
3774              $itemtext = null;
3775              $itemurl = null;
3776              $itemtitle = null;
3777              $itemvisible = true;
3778              $settings = explode('|', $line);
3779              foreach ($settings as $i => $setting) {
3780                  $setting = trim($setting);
3781                  if ($setting !== '') {
3782                      switch ($i) {
3783                          case 0: // Menu text.
3784                              $itemtext = ltrim($setting, '-');
3785                              break;
3786                          case 1: // URL.
3787                              try {
3788                                  $itemurl = new moodle_url($setting);
3789                              } catch (moodle_exception $exception) {
3790                                  // We're not actually worried about this, we don't want to mess up the display
3791                                  // just for a wrongly entered URL.
3792                                  $itemurl = null;
3793                              }
3794                              break;
3795                          case 2: // Title attribute.
3796                              $itemtitle = $setting;
3797                              break;
3798                          case 3: // Language.
3799                              if (!empty($language)) {
3800                                  $itemlanguages = array_map('trim', explode(',', $setting));
3801                                  $itemvisible &= in_array($language, $itemlanguages);
3802                              }
3803                              break;
3804                      }
3805                  }
3806              }
3807              // Get depth of new item.
3808              preg_match('/^(\-*)/', $line, $match);
3809              $itemdepth = strlen($match[1]) + 1;
3810              // Find parent item for new item.
3811              while (($lastdepth - $itemdepth) >= 0) {
3812                  $lastitem = $lastitem->get_parent();
3813                  $lastdepth--;
3814              }
3815              $lastitem = $lastitem->add($itemtext, $itemurl, $itemtitle, $linenumber + 1);
3816              $lastdepth++;
3817              if (!$itemvisible) {
3818                  $hiddenitems[] = $lastitem;
3819              }
3820          }
3821          foreach ($hiddenitems as $item) {
3822              $item->parent->remove_child($item);
3823          }
3824          return $root->get_children();
3825      }
3826  
3827      /**
3828       * Sorts two custom menu items
3829       *
3830       * This function is designed to be used with the usort method
3831       *     usort($this->children, array('custom_menu','sort_custom_menu_items'));
3832       *
3833       * @static
3834       * @param custom_menu_item $itema
3835       * @param custom_menu_item $itemb
3836       * @return int
3837       */
3838      public static function sort_custom_menu_items(custom_menu_item $itema, custom_menu_item $itemb) {
3839          $itema = $itema->get_sort_order();
3840          $itemb = $itemb->get_sort_order();
3841          if ($itema == $itemb) {
3842              return 0;
3843          }
3844          return ($itema > $itemb) ? +1 : -1;
3845      }
3846  }
3847  
3848  /**
3849   * Stores one tab
3850   *
3851   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3852   * @package core
3853   */
3854  class tabobject implements renderable, templatable {
3855      /** @var string unique id of the tab in this tree, it is used to find selected and/or inactive tabs */
3856      var $id;
3857      /** @var moodle_url|string link */
3858      var $link;
3859      /** @var string text on the tab */
3860      var $text;
3861      /** @var string title under the link, by defaul equals to text */
3862      var $title;
3863      /** @var bool whether to display a link under the tab name when it's selected */
3864      var $linkedwhenselected = false;
3865      /** @var bool whether the tab is inactive */
3866      var $inactive = false;
3867      /** @var bool indicates that this tab's child is selected */
3868      var $activated = false;
3869      /** @var bool indicates that this tab is selected */
3870      var $selected = false;
3871      /** @var array stores children tabobjects */
3872      var $subtree = array();
3873      /** @var int level of tab in the tree, 0 for root (instance of tabtree), 1 for the first row of tabs */
3874      var $level = 1;
3875  
3876      /**
3877       * Constructor
3878       *
3879       * @param string $id unique id of the tab in this tree, it is used to find selected and/or inactive tabs
3880       * @param string|moodle_url $link
3881       * @param string $text text on the tab
3882       * @param string $title title under the link, by defaul equals to text
3883       * @param bool $linkedwhenselected whether to display a link under the tab name when it's selected
3884       */
3885      public function __construct($id, $link = null, $text = '', $title = '', $linkedwhenselected = false) {
3886          $this->id = $id;
3887          $this->link = $link;
3888          $this->text = $text;
3889          $this->title = $title ? $title : $text;
3890          $this->linkedwhenselected = $linkedwhenselected;
3891      }
3892  
3893      /**
3894       * Travels through tree and finds the tab to mark as selected, all parents are automatically marked as activated
3895       *
3896       * @param string $selected the id of the selected tab (whatever row it's on),
3897       *    if null marks all tabs as unselected
3898       * @return bool whether this tab is selected or contains selected tab in its subtree
3899       */
3900      protected function set_selected($selected) {
3901          if ((string)$selected === (string)$this->id) {
3902              $this->selected = true;
3903              // This tab is selected. No need to travel through subtree.
3904              return true;
3905          }
3906          foreach ($this->subtree as $subitem) {
3907              if ($subitem->set_selected($selected)) {
3908                  // This tab has child that is selected. Mark it as activated. No need to check other children.
3909                  $this->activated = true;
3910                  return true;
3911              }
3912          }
3913          return false;
3914      }
3915  
3916      /**
3917       * Travels through tree and finds a tab with specified id
3918       *
3919       * @param string $id
3920       * @return tabtree|null
3921       */
3922      public function find($id) {
3923          if ((string)$this->id === (string)$id) {
3924              return $this;
3925          }
3926          foreach ($this->subtree as $tab) {
3927              if ($obj = $tab->find($id)) {
3928                  return $obj;
3929              }
3930          }
3931          return null;
3932      }
3933  
3934      /**
3935       * Allows to mark each tab's level in the tree before rendering.
3936       *
3937       * @param int $level
3938       */
3939      protected function set_level($level) {
3940          $this->level = $level;
3941          foreach ($this->subtree as $tab) {
3942              $tab->set_level($level + 1);
3943          }
3944      }
3945  
3946      /**
3947       * Export for template.
3948       *
3949       * @param renderer_base $output Renderer.
3950       * @return object
3951       */
3952      public function export_for_template(renderer_base $output) {
3953          if ($this->inactive || ($this->selected && !$this->linkedwhenselected) || $this->activated) {
3954              $link = null;
3955          } else {
3956              $link = $this->link;
3957          }
3958          $active = $this->activated || $this->selected;
3959  
3960          return (object) [
3961              'id' => $this->id,
3962              'link' => is_object($link) ? $link->out(false) : $link,
3963              'text' => $this->text,
3964              'title' => $this->title,
3965              'inactive' => !$active && $this->inactive,
3966              'active' => $active,
3967              'level' => $this->level,
3968          ];
3969      }
3970  
3971  }
3972  
3973  /**
3974   * Renderable for the main page header.
3975   *
3976   * @package core
3977   * @category output
3978   * @since 2.9
3979   * @copyright 2015 Adrian Greeve <adrian@moodle.com>
3980   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3981   */
3982  class context_header implements renderable {
3983  
3984      /**
3985       * @var string $heading Main heading.
3986       */
3987      public $heading;
3988      /**
3989       * @var int $headinglevel Main heading 'h' tag level.
3990       */
3991      public $headinglevel;
3992      /**
3993       * @var string|null $imagedata HTML code for the picture in the page header.
3994       */
3995      public $imagedata;
3996      /**
3997       * @var array $additionalbuttons Additional buttons for the header e.g. Messaging button for the user header.
3998       *      array elements - title => alternate text for the image, or if no image is available the button text.
3999       *                       url => Link for the button to head to. Should be a moodle_url.
4000       *                       image => location to the image, or name of the image in /pix/t/{image name}.
4001       *                       linkattributes => additional attributes for the <a href> element.
4002       *                       page => page object. Don't include if the image is an external image.
4003       */
4004      public $additionalbuttons;
4005      /**
4006       * @var string $prefix A string that is before the title.
4007       */
4008      public $prefix;
4009  
4010      /**
4011       * Constructor.
4012       *
4013       * @param string $heading Main heading data.
4014       * @param int $headinglevel Main heading 'h' tag level.
4015       * @param string|null $imagedata HTML code for the picture in the page header.
4016       * @param string $additionalbuttons Buttons for the header e.g. Messaging button for the user header.
4017       * @param string $prefix Text that precedes the heading.
4018       */
4019      public function __construct($heading = null, $headinglevel = 1, $imagedata = null, $additionalbuttons = null, $prefix = null) {
4020  
4021          $this->heading = $heading;
4022          $this->headinglevel = $headinglevel;
4023          $this->imagedata = $imagedata;
4024          $this->additionalbuttons = $additionalbuttons;
4025          // If we have buttons then format them.
4026          if (isset($this->additionalbuttons)) {
4027              $this->format_button_images();
4028          }
4029          $this->prefix = $prefix;
4030      }
4031  
4032      /**
4033       * Adds an array element for a formatted image.
4034       */
4035      protected function format_button_images() {
4036  
4037          foreach ($this->additionalbuttons as $buttontype => $button) {
4038              $page = $button['page'];
4039              // If no image is provided then just use the title.
4040              if (!isset($button['image'])) {
4041                  $this->additionalbuttons[$buttontype]['formattedimage'] = $button['title'];
4042              } else {
4043                  // Check to see if this is an internal Moodle icon.
4044                  $internalimage = $page->theme->resolve_image_location('t/' . $button['image'], 'moodle');
4045                  if ($internalimage) {
4046                      $this->additionalbuttons[$buttontype]['formattedimage'] = 't/' . $button['image'];
4047                  } else {
4048                      // Treat as an external image.
4049                      $this->additionalbuttons[$buttontype]['formattedimage'] = $button['image'];
4050                  }
4051              }
4052  
4053              if (isset($button['linkattributes']['class'])) {
4054                  $class = $button['linkattributes']['class'] . ' btn';
4055              } else {
4056                  $class = 'btn';
4057              }
4058              // Add the bootstrap 'btn' class for formatting.
4059              $this->additionalbuttons[$buttontype]['linkattributes'] = array_merge($button['linkattributes'],
4060                      array('class' => $class));
4061          }
4062      }
4063  }
4064  
4065  /**
4066   * Stores tabs list
4067   *
4068   * Example how to print a single line tabs:
4069   * $rows = array(
4070   *    new tabobject(...),
4071   *    new tabobject(...)
4072   * );
4073   * echo $OUTPUT->tabtree($rows, $selectedid);
4074   *
4075   * Multiple row tabs may not look good on some devices but if you want to use them
4076   * you can specify ->subtree for the active tabobject.
4077   *
4078   * @copyright 2013 Marina Glancy
4079   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4080   * @since Moodle 2.5
4081   * @package core
4082   * @category output
4083   */
4084  class tabtree extends tabobject {
4085      /**
4086       * Constuctor
4087       *
4088       * It is highly recommended to call constructor when list of tabs is already
4089       * populated, this way you ensure that selected and inactive tabs are located
4090       * and attribute level is set correctly.
4091       *
4092       * @param array $tabs array of tabs, each of them may have it's own ->subtree
4093       * @param string|null $selected which tab to mark as selected, all parent tabs will
4094       *     automatically be marked as activated
4095       * @param array|string|null $inactive list of ids of inactive tabs, regardless of
4096       *     their level. Note that you can as weel specify tabobject::$inactive for separate instances
4097       */
4098      public function __construct($tabs, $selected = null, $inactive = null) {
4099          $this->subtree = $tabs;
4100          if ($selected !== null) {
4101              $this->set_selected($selected);
4102          }
4103          if ($inactive !== null) {
4104              if (is_array($inactive)) {
4105                  foreach ($inactive as $id) {
4106                      if ($tab = $this->find($id)) {
4107                          $tab->inactive = true;
4108                      }
4109                  }
4110              } else if ($tab = $this->find($inactive)) {
4111                  $tab->inactive = true;
4112              }
4113          }
4114          $this->set_level(0);
4115      }
4116  
4117      /**
4118       * Export for template.
4119       *
4120       * @param renderer_base $output Renderer.
4121       * @return object
4122       */
4123      public function export_for_template(renderer_base $output) {
4124          $tabs = [];
4125          $secondrow = false;
4126  
4127          foreach ($this->subtree as $tab) {
4128              $tabs[] = $tab->export_for_template($output);
4129              if (!empty($tab->subtree) && ($tab->level == 0 || $tab->selected || $tab->activated)) {
4130                  $secondrow = new tabtree($tab->subtree);
4131              }
4132          }
4133  
4134          return (object) [
4135              'tabs' => $tabs,
4136              'secondrow' => $secondrow ? $secondrow->export_for_template($output) : false
4137          ];
4138      }
4139  }
4140  
4141  /**
4142   * An action menu.
4143   *
4144   * This action menu component takes a series of primary and secondary actions.
4145   * The primary actions are displayed permanently and the secondary attributes are displayed within a drop
4146   * down menu.
4147   *
4148   * @package core
4149   * @category output
4150   * @copyright 2013 Sam Hemelryk
4151   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4152   */
4153  class action_menu implements renderable, templatable {
4154  
4155      /**
4156       * Top right alignment.
4157       */
4158      const TL = 1;
4159  
4160      /**
4161       * Top right alignment.
4162       */
4163      const TR = 2;
4164  
4165      /**
4166       * Top right alignment.
4167       */
4168      const BL = 3;
4169  
4170      /**
4171       * Top right alignment.
4172       */
4173      const BR = 4;
4174  
4175      /**
4176       * The instance number. This is unique to this instance of the action menu.
4177       * @var int
4178       */
4179      protected $instance = 0;
4180  
4181      /**
4182       * An array of primary actions. Please use {@link action_menu::add_primary_action()} to add actions.
4183       * @var array
4184       */
4185      protected $primaryactions = array();
4186  
4187      /**
4188       * An array of secondary actions. Please use {@link action_menu::add_secondary_action()} to add actions.
4189       * @var array
4190       */
4191      protected $secondaryactions = array();
4192  
4193      /**
4194       * An array of attributes added to the container of the action menu.
4195       * Initialised with defaults during construction.
4196       * @var array
4197       */
4198      public $attributes = array();
4199      /**
4200       * An array of attributes added to the container of the primary actions.
4201       * Initialised with defaults during construction.
4202       * @var array
4203       */
4204      public $attributesprimary = array();
4205      /**
4206       * An array of attributes added to the container of the secondary actions.
4207       * Initialised with defaults during construction.
4208       * @var array
4209       */
4210      public $attributessecondary = array();
4211  
4212      /**
4213       * The string to use next to the icon for the action icon relating to the secondary (dropdown) menu.
4214       * @var array
4215       */
4216      public $actiontext = null;
4217  
4218      /**
4219       * The string to use for the accessible label for the menu.
4220       * @var array
4221       */
4222      public $actionlabel = null;
4223  
4224      /**
4225       * An icon to use for the toggling the secondary menu (dropdown).
4226       * @var pix_icon
4227       */
4228      public $actionicon;
4229  
4230      /**
4231       * Any text to use for the toggling the secondary menu (dropdown).
4232       * @var string
4233       */
4234      public $menutrigger = '';
4235  
4236      /**
4237       * An array of attributes added to the trigger element of the secondary menu.
4238       * @var array
4239       */
4240      public $triggerattributes = [];
4241  
4242      /**
4243       * Any extra classes for toggling to the secondary menu.
4244       * @var string
4245       */
4246      public $triggerextraclasses = '';
4247  
4248      /**
4249       * Place the action menu before all other actions.
4250       * @var bool
4251       */
4252      public $prioritise = false;
4253  
4254      /**
4255       * Dropdown menu alignment class.
4256       * @var string
4257       */
4258      public $dropdownalignment = '';
4259  
4260      /**
4261       * Constructs the action menu with the given items.
4262       *
4263       * @param array $actions An array of actions (action_menu_link|pix_icon|string).
4264       */
4265      public function __construct(array $actions = array()) {
4266          static $initialised = 0;
4267          $this->instance = $initialised;
4268          $initialised++;
4269  
4270          $this->attributes = array(
4271              'id' => 'action-menu-'.$this->instance,
4272              'class' => 'moodle-actionmenu',
4273              'data-enhance' => 'moodle-core-actionmenu'
4274          );
4275          $this->attributesprimary = array(
4276              'id' => 'action-menu-'.$this->instance.'-menubar',
4277              'class' => 'menubar',
4278          );
4279          $this->attributessecondary = array(
4280              'id' => 'action-menu-'.$this->instance.'-menu',
4281              'class' => 'menu',
4282              'data-rel' => 'menu-content',
4283              'aria-labelledby' => 'action-menu-toggle-'.$this->instance,
4284              'role' => 'menu'
4285          );
4286          $this->dropdownalignment = 'dropdown-menu-right';
4287          foreach ($actions as $action) {
4288              $this->add($action);
4289          }
4290      }
4291  
4292      /**
4293       * Sets the label for the menu trigger.
4294       *
4295       * @param string $label The text
4296       */
4297      public function set_action_label($label) {
4298          $this->actionlabel = $label;
4299      }
4300  
4301      /**
4302       * Sets the menu trigger text.
4303       *
4304       * @param string $trigger The text
4305       * @param string $extraclasses Extra classes to style the secondary menu toggle.
4306       */
4307      public function set_menu_trigger($trigger, $extraclasses = '') {
4308          $this->menutrigger = $trigger;
4309          $this->triggerextraclasses = $extraclasses;
4310      }
4311  
4312      /**
4313       * Return true if there is at least one visible link in the menu.
4314       *
4315       * @return bool
4316       */
4317      public function is_empty() {
4318          return !count($this->primaryactions) && !count($this->secondaryactions);
4319      }
4320  
4321      /**
4322       * Initialises JS required fore the action menu.
4323       * The JS is only required once as it manages all action menu's on the page.
4324       *
4325       * @param moodle_page $page
4326       */
4327      public function initialise_js(moodle_page $page) {
4328          static $initialised = false;
4329          if (!$initialised) {
4330              $page->requires->yui_module('moodle-core-actionmenu', 'M.core.actionmenu.init');
4331              $initialised = true;
4332          }
4333      }
4334  
4335      /**
4336       * Adds an action to this action menu.
4337       *
4338       * @param action_menu_link|pix_icon|string $action
4339       */
4340      public function add($action) {
4341          if ($action instanceof action_link) {
4342              if ($action->primary) {
4343                  $this->add_primary_action($action);
4344              } else {
4345                  $this->add_secondary_action($action);
4346              }
4347          } else if ($action instanceof pix_icon) {
4348              $this->add_primary_action($action);
4349          } else {
4350              $this->add_secondary_action($action);
4351          }
4352      }
4353  
4354      /**
4355       * Adds a primary action to the action menu.
4356       *
4357       * @param action_menu_link|action_link|pix_icon|string $action
4358       */
4359      public function add_primary_action($action) {
4360          if ($action instanceof action_link || $action instanceof pix_icon) {
4361              $action->attributes['role'] = 'menuitem';
4362              $action->attributes['tabindex'] = '-1';
4363              if ($action instanceof action_menu_link) {
4364                  $action->actionmenu = $this;
4365              }
4366          }
4367          $this->primaryactions[] = $action;
4368      }
4369  
4370      /**
4371       * Adds a secondary action to the action menu.
4372       *
4373       * @param action_link|pix_icon|string $action
4374       */
4375      public function add_secondary_action($action) {
4376          if ($action instanceof action_link || $action instanceof pix_icon) {
4377              $action->attributes['role'] = 'menuitem';
4378              $action->attributes['tabindex'] = '-1';
4379              if ($action instanceof action_menu_link) {
4380                  $action->actionmenu = $this;
4381              }
4382          }
4383          $this->secondaryactions[] = $action;
4384      }
4385  
4386      /**
4387       * Returns the primary actions ready to be rendered.
4388       *
4389       * @param core_renderer $output The renderer to use for getting icons.
4390       * @return array
4391       */
4392      public function get_primary_actions(core_renderer $output = null) {
4393          global $OUTPUT;
4394          if ($output === null) {
4395              $output = $OUTPUT;
4396          }
4397          $pixicon = $this->actionicon;
4398          $linkclasses = array('toggle-display');
4399  
4400          $title = '';
4401          if (!empty($this->menutrigger)) {
4402              $pixicon = '<b class="caret"></b>';
4403              $linkclasses[] = 'textmenu';
4404          } else {
4405              $title = new lang_string('actionsmenu', 'moodle');
4406              $this->actionicon = new pix_icon(
4407                  't/edit_menu',
4408                  '',
4409                  'moodle',
4410                  array('class' => 'iconsmall actionmenu', 'title' => '')
4411              );
4412              $pixicon = $this->actionicon;
4413          }
4414          if ($pixicon instanceof renderable) {
4415              $pixicon = $output->render($pixicon);
4416              if ($pixicon instanceof pix_icon && isset($pixicon->attributes['alt'])) {
4417                  $title = $pixicon->attributes['alt'];
4418              }
4419          }
4420          $string = '';
4421          if ($this->actiontext) {
4422              $string = $this->actiontext;
4423          }
4424          $label = '';
4425          if ($this->actionlabel) {
4426              $label = $this->actionlabel;
4427          } else {
4428              $label = $title;
4429          }
4430          $actions = $this->primaryactions;
4431          $attributes = array(
4432              'class' => implode(' ', $linkclasses),
4433              'title' => $title,
4434              'aria-label' => $label,
4435              'id' => 'action-menu-toggle-'.$this->instance,
4436              'role' => 'menuitem',
4437              'tabindex' => '-1',
4438          );
4439          $link = html_writer::link('#', $string . $this->menutrigger . $pixicon, $attributes);
4440          if ($this->prioritise) {
4441              array_unshift($actions, $link);
4442          } else {
4443              $actions[] = $link;
4444          }
4445          return $actions;
4446      }
4447  
4448      /**
4449       * Returns the secondary actions ready to be rendered.
4450       * @return array
4451       */
4452      public function get_secondary_actions() {
4453          return $this->secondaryactions;
4454      }
4455  
4456      /**
4457       * Sets the selector that should be used to find the owning node of this menu.
4458       * @param string $selector A CSS/YUI selector to identify the owner of the menu.
4459       */
4460      public function set_owner_selector($selector) {
4461          $this->attributes['data-owner'] = $selector;
4462      }
4463  
4464      /**
4465       * Sets the alignment of the dialogue in relation to button used to toggle it.
4466       *
4467       * @deprecated since Moodle 4.0
4468       *
4469       * @param int $dialogue One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR.
4470       * @param int $button One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR.
4471       */
4472      public function set_alignment($dialogue, $button) {
4473          debugging('The method action_menu::set_alignment() is deprecated, use action_menu::set_menu_left()', DEBUG_DEVELOPER);
4474          if (isset($this->attributessecondary['data-align'])) {
4475              // We've already got one set, lets remove the old class so as to avoid troubles.
4476              $class = $this->attributessecondary['class'];
4477              $search = 'align-'.$this->attributessecondary['data-align'];
4478              $this->attributessecondary['class'] = str_replace($search, '', $class);
4479          }
4480          $align = $this->get_align_string($dialogue) . '-' . $this->get_align_string($button);
4481          $this->attributessecondary['data-align'] = $align;
4482          $this->attributessecondary['class'] .= ' align-'.$align;
4483      }
4484  
4485      /**
4486       * Returns a string to describe the alignment.
4487       *
4488       * @param int $align One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR.
4489       * @return string
4490       */
4491      protected function get_align_string($align) {
4492          switch ($align) {
4493              case self::TL :
4494                  return 'tl';
4495              case self::TR :
4496                  return 'tr';
4497              case self::BL :
4498                  return 'bl';
4499              case self::BR :
4500                  return 'br';
4501              default :
4502                  return 'tl';
4503          }
4504      }
4505  
4506      /**
4507       * Aligns the left corner of the dropdown.
4508       *
4509       */
4510      public function set_menu_left() {
4511          $this->dropdownalignment = 'dropdown-menu-left';
4512      }
4513  
4514      /**
4515       * Sets a constraint for the dialogue.
4516       *
4517       * The constraint is applied when the dialogue is shown and limits the display of the dialogue to within the
4518       * element the constraint identifies.
4519       *
4520       * This is required whenever the action menu is displayed inside any CSS element with the .no-overflow class
4521       * (flexible_table and any of it's child classes are a likely candidate).
4522       *
4523       * @param string $ancestorselector A snippet of CSS used to identify the ancestor to contrain the dialogue to.
4524       */
4525      public function set_constraint($ancestorselector) {
4526          $this->attributessecondary['data-constraint'] = $ancestorselector;
4527      }
4528  
4529      /**
4530       * Set the overflow constraint boundary of the dropdown menu.
4531       * @see https://getbootstrap.com/docs/4.6/components/dropdowns/#options The 'boundary' option in the Bootstrap documentation
4532       *
4533       * @param string $boundary Accepts the values of 'viewport', 'window', or 'scrollParent'.
4534       * @throws coding_exception
4535       */
4536      public function set_boundary(string $boundary) {
4537          if (!in_array($boundary, ['viewport', 'window', 'scrollParent'])) {
4538              throw new coding_exception("HTMLElement reference boundaries are not supported." .
4539                  "Accepted boundaries are 'viewport', 'window', or 'scrollParent'.", DEBUG_DEVELOPER);
4540          }
4541  
4542          $this->triggerattributes['data-boundary'] = $boundary;
4543      }
4544  
4545      /**
4546       * If you call this method the action menu will be displayed but will not be enhanced.
4547       *
4548       * By not displaying the menu enhanced all items will be displayed in a single row.
4549       *
4550       * @deprecated since Moodle 3.2
4551       */
4552      public function do_not_enhance() {
4553          debugging('The method action_menu::do_not_enhance() is deprecated, use a list of action_icon instead.', DEBUG_DEVELOPER);
4554      }
4555  
4556      /**
4557       * Returns true if this action menu will be enhanced.
4558       *
4559       * @return bool
4560       */
4561      public function will_be_enhanced() {
4562          return isset($this->attributes['data-enhance']);
4563      }
4564  
4565      /**
4566       * Sets nowrap on items. If true menu items should not wrap lines if they are longer than the available space.
4567       *
4568       * This property can be useful when the action menu is displayed within a parent element that is either floated
4569       * or relatively positioned.
4570       * In that situation the width of the menu is determined by the width of the parent element which may not be large
4571       * enough for the menu items without them wrapping.
4572       * This disables the wrapping so that the menu takes on the width of the longest item.
4573       *
4574       * @param bool $value If true nowrap gets set, if false it gets removed. Defaults to true.
4575       */
4576      public function set_nowrap_on_items($value = true) {
4577          $class = 'nowrap-items';
4578          if (!empty($this->attributes['class'])) {
4579              $pos = strpos($this->attributes['class'], $class);
4580              if ($value === true && $pos === false) {
4581                  // The value is true and the class has not been set yet. Add it.
4582                  $this->attributes['class'] .= ' '.$class;
4583              } else if ($value === false && $pos !== false) {
4584                  // The value is false and the class has been set. Remove it.
4585                  $this->attributes['class'] = substr($this->attributes['class'], $pos, strlen($class));
4586              }
4587          } else if ($value) {
4588              // The value is true and the class has not been set yet. Add it.
4589              $this->attributes['class'] = $class;
4590          }
4591      }
4592  
4593      /**
4594       * Add classes to the action menu for an easier styling.
4595       *
4596       * @param string $class The class to add to attributes.
4597       */
4598      public function set_additional_classes(string $class = '') {
4599          if (!empty($this->attributes['class'])) {
4600              $this->attributes['class'] .= " ".$class;
4601          } else {
4602              $this->attributes['class'] = $class;
4603          }
4604      }
4605  
4606      /**
4607       * Export for template.
4608       *
4609       * @param renderer_base $output The renderer.
4610       * @return stdClass
4611       */
4612      public function export_for_template(renderer_base $output) {
4613          $data = new stdClass();
4614          // Assign a role of menubar to this action menu when:
4615          // - it contains 2 or more primary actions; or
4616          // - if it contains a primary action and secondary actions.
4617          if (count($this->primaryactions) > 1 || (!empty($this->primaryactions) && !empty($this->secondaryactions))) {
4618              $this->attributes['role'] = 'menubar';
4619          }
4620          $attributes = $this->attributes;
4621          $attributesprimary = $this->attributesprimary;
4622          $attributessecondary = $this->attributessecondary;
4623  
4624          $data->instance = $this->instance;
4625  
4626          $data->classes = isset($attributes['class']) ? $attributes['class'] : '';
4627          unset($attributes['class']);
4628  
4629          $data->attributes = array_map(function($key, $value) {
4630              return [ 'name' => $key, 'value' => $value ];
4631          }, array_keys($attributes), $attributes);
4632  
4633          $primary = new stdClass();
4634          $primary->title = '';
4635          $primary->prioritise = $this->prioritise;
4636  
4637          $primary->classes = isset($attributesprimary['class']) ? $attributesprimary['class'] : '';
4638          unset($attributesprimary['class']);
4639          $primary->attributes = array_map(function($key, $value) {
4640              return [ 'name' => $key, 'value' => $value ];
4641          }, array_keys($attributesprimary), $attributesprimary);
4642          $primary->triggerattributes = array_map(function($key, $value) {
4643              return [ 'name' => $key, 'value' => $value ];
4644          }, array_keys($this->triggerattributes), $this->triggerattributes);
4645  
4646          $actionicon = $this->actionicon;
4647          if (!empty($this->menutrigger)) {
4648              $primary->menutrigger = $this->menutrigger;
4649              $primary->triggerextraclasses = $this->triggerextraclasses;
4650              if ($this->actionlabel) {
4651                  $primary->title = $this->actionlabel;
4652              } else if ($this->actiontext) {
4653                  $primary->title = $this->actiontext;
4654              } else {
4655                  $primary->title = strip_tags($this->menutrigger);
4656              }
4657          } else {
4658              $primary->title = get_string('actionsmenu');
4659              $iconattributes = ['class' => 'iconsmall actionmenu', 'title' => $primary->title];
4660              $actionicon = new pix_icon('t/edit_menu', '', 'moodle', $iconattributes);
4661          }
4662  
4663          // If the menu trigger is within the menubar, assign a role of menuitem. Otherwise, assign as a button.
4664          $primary->triggerrole = 'button';
4665          if (isset($attributes['role']) && $attributes['role'] === 'menubar') {
4666              $primary->triggerrole = 'menuitem';
4667          }
4668  
4669          if ($actionicon instanceof pix_icon) {
4670              $primary->icon = $actionicon->export_for_pix();
4671              if (!empty($actionicon->attributes['alt'])) {
4672                  $primary->title = $actionicon->attributes['alt'];
4673              }
4674          } else {
4675              $primary->iconraw = $actionicon ? $output->render($actionicon) : '';
4676          }
4677  
4678          $primary->actiontext = $this->actiontext ? (string) $this->actiontext : '';
4679          $primary->items = array_map(function($item) use ($output) {
4680              $data = (object) [];
4681              if ($item instanceof action_menu_link) {
4682                  $data->actionmenulink = $item->export_for_template($output);
4683              } else if ($item instanceof action_menu_filler) {
4684                  $data->actionmenufiller = $item->export_for_template($output);
4685              } else if ($item instanceof action_link) {
4686                  $data->actionlink = $item->export_for_template($output);
4687              } else if ($item instanceof pix_icon) {
4688                  $data->pixicon = $item->export_for_template($output);
4689              } else {
4690                  $data->rawhtml = ($item instanceof renderable) ? $output->render($item) : $item;
4691              }
4692              return $data;
4693          }, $this->primaryactions);
4694  
4695          $secondary = new stdClass();
4696          $secondary->classes = isset($attributessecondary['class']) ? $attributessecondary['class'] : '';
4697          unset($attributessecondary['class']);
4698          $secondary->attributes = array_map(function($key, $value) {
4699              return [ 'name' => $key, 'value' => $value ];
4700          }, array_keys($attributessecondary), $attributessecondary);
4701          $secondary->items = array_map(function($item) use ($output) {
4702              $data = (object) [];
4703              if ($item instanceof action_menu_link) {
4704                  $data->actionmenulink = $item->export_for_template($output);
4705              } else if ($item instanceof action_menu_filler) {
4706                  $data->actionmenufiller = $item->export_for_template($output);
4707              } else if ($item instanceof action_link) {
4708                  $data->actionlink = $item->export_for_template($output);
4709              } else if ($item instanceof pix_icon) {
4710                  $data->pixicon = $item->export_for_template($output);
4711              } else {
4712                  $data->rawhtml = ($item instanceof renderable) ? $output->render($item) : $item;
4713              }
4714              return $data;
4715          }, $this->secondaryactions);
4716  
4717          $data->primary = $primary;
4718          $data->secondary = $secondary;
4719          $data->dropdownalignment = $this->dropdownalignment;
4720  
4721          return $data;
4722      }
4723  
4724  }
4725  
4726  /**
4727   * An action menu filler
4728   *
4729   * @package core
4730   * @category output
4731   * @copyright 2013 Andrew Nicols
4732   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4733   */
4734  class action_menu_filler extends action_link implements renderable {
4735  
4736      /**
4737       * True if this is a primary action. False if not.
4738       * @var bool
4739       */
4740      public $primary = true;
4741  
4742      /**
4743       * Constructs the object.
4744       */
4745      public function __construct() {
4746          $this->attributes['id'] = html_writer::random_id('action_link');
4747      }
4748  }
4749  
4750  /**
4751   * An action menu action
4752   *
4753   * @package core
4754   * @category output
4755   * @copyright 2013 Sam Hemelryk
4756   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4757   */
4758  class action_menu_link extends action_link implements renderable {
4759  
4760      /**
4761       * True if this is a primary action. False if not.
4762       * @var bool
4763       */
4764      public $primary = true;
4765  
4766      /**
4767       * The action menu this link has been added to.
4768       * @var action_menu
4769       */
4770      public $actionmenu = null;
4771  
4772      /**
4773       * The number of instances of this action menu link (and its subclasses).
4774       * @var int
4775       */
4776      protected static $instance = 1;
4777  
4778      /**
4779       * Constructs the object.
4780       *
4781       * @param moodle_url $url The URL for the action.
4782       * @param pix_icon|null $icon The icon to represent the action.
4783       * @param string $text The text to represent the action.
4784       * @param bool $primary Whether this is a primary action or not.
4785       * @param array $attributes Any attribtues associated with the action.
4786       */
4787      public function __construct(moodle_url $url, ?pix_icon $icon, $text, $primary = true, array $attributes = array()) {
4788          parent::__construct($url, $text, null, $attributes, $icon);
4789          $this->primary = (bool)$primary;
4790          $this->add_class('menu-action');
4791          $this->attributes['role'] = 'menuitem';
4792      }
4793  
4794      /**
4795       * Export for template.
4796       *
4797       * @param renderer_base $output The renderer.
4798       * @return stdClass
4799       */
4800      public function export_for_template(renderer_base $output) {
4801          $data = parent::export_for_template($output);
4802          $data->instance = self::$instance++;
4803  
4804          // Ignore what the parent did with the attributes, except for ID and class.
4805          $data->attributes = [];
4806          $attributes = $this->attributes;
4807          unset($attributes['id']);
4808          unset($attributes['class']);
4809  
4810          // Handle text being a renderable.
4811          if ($this->text instanceof renderable) {
4812              $data->text = $this->render($this->text);
4813          }
4814  
4815          $data->showtext = (!$this->icon || $this->primary === false);
4816  
4817          $data->icon = null;
4818          if ($this->icon) {
4819              $icon = $this->icon;
4820              if ($this->primary || !$this->actionmenu->will_be_enhanced()) {
4821                  $attributes['title'] = $data->text;
4822              }
4823              $data->icon = $icon ? $icon->export_for_pix() : null;
4824          }
4825  
4826          $data->disabled = !empty($attributes['disabled']);
4827          unset($attributes['disabled']);
4828  
4829          $data->attributes = array_map(function($key, $value) {
4830              return [
4831                  'name' => $key,
4832                  'value' => $value
4833              ];
4834          }, array_keys($attributes), $attributes);
4835  
4836          return $data;
4837      }
4838  }
4839  
4840  /**
4841   * A primary action menu action
4842   *
4843   * @package core
4844   * @category output
4845   * @copyright 2013 Sam Hemelryk
4846   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4847   */
4848  class action_menu_link_primary extends action_menu_link {
4849      /**
4850       * Constructs the object.
4851       *
4852       * @param moodle_url $url
4853       * @param pix_icon|null $icon
4854       * @param string $text
4855       * @param array $attributes
4856       */
4857      public function __construct(moodle_url $url, ?pix_icon $icon, $text, array $attributes = array()) {
4858          parent::__construct($url, $icon, $text, true, $attributes);
4859      }
4860  }
4861  
4862  /**
4863   * A secondary action menu action
4864   *
4865   * @package core
4866   * @category output
4867   * @copyright 2013 Sam Hemelryk
4868   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4869   */
4870  class action_menu_link_secondary extends action_menu_link {
4871      /**
4872       * Constructs the object.
4873       *
4874       * @param moodle_url $url
4875       * @param pix_icon|null $icon
4876       * @param string $text
4877       * @param array $attributes
4878       */
4879      public function __construct(moodle_url $url, ?pix_icon $icon, $text, array $attributes = array()) {
4880          parent::__construct($url, $icon, $text, false, $attributes);
4881      }
4882  }
4883  
4884  /**
4885   * Represents a set of preferences groups.
4886   *
4887   * @package core
4888   * @category output
4889   * @copyright 2015 Frédéric Massart - FMCorz.net
4890   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4891   */
4892  class preferences_groups implements renderable {
4893  
4894      /**
4895       * Array of preferences_group.
4896       * @var array
4897       */
4898      public $groups;
4899  
4900      /**
4901       * Constructor.
4902       * @param array $groups of preferences_group
4903       */
4904      public function __construct($groups) {
4905          $this->groups = $groups;
4906      }
4907  
4908  }
4909  
4910  /**
4911   * Represents a group of preferences page link.
4912   *
4913   * @package core
4914   * @category output
4915   * @copyright 2015 Frédéric Massart - FMCorz.net
4916   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4917   */
4918  class preferences_group implements renderable {
4919  
4920      /**
4921       * Title of the group.
4922       * @var string
4923       */
4924      public $title;
4925  
4926      /**
4927       * Array of navigation_node.
4928       * @var array
4929       */
4930      public $nodes;
4931  
4932      /**
4933       * Constructor.
4934       * @param string $title The title.
4935       * @param array $nodes of navigation_node.
4936       */
4937      public function __construct($title, $nodes) {
4938          $this->title = $title;
4939          $this->nodes = $nodes;
4940      }
4941  }
4942  
4943  /**
4944   * Progress bar class.
4945   *
4946   * Manages the display of a progress bar.
4947   *
4948   * To use this class.
4949   * - construct
4950   * - call create (or use the 3rd param to the constructor)
4951   * - call update or update_full() or update() repeatedly
4952   *
4953   * @copyright 2008 jamiesensei
4954   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4955   * @package core
4956   * @category output
4957   */
4958  class progress_bar implements renderable, templatable {
4959      /** @var string html id */
4960      private $html_id;
4961      /** @var int total width */
4962      private $width;
4963      /** @var int last percentage printed */
4964      private $percent = 0;
4965      /** @var int time when last printed */
4966      private $lastupdate = 0;
4967      /** @var int when did we start printing this */
4968      private $time_start = 0;
4969  
4970      /**
4971       * Constructor
4972       *
4973       * Prints JS code if $autostart true.
4974       *
4975       * @param string $htmlid The container ID.
4976       * @param int $width The suggested width.
4977       * @param bool $autostart Whether to start the progress bar right away.
4978       */
4979      public function __construct($htmlid = '', $width = 500, $autostart = false) {
4980          if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
4981              debugging('progress_bar used in a non-CLI script without setting NO_OUTPUT_BUFFERING.', DEBUG_DEVELOPER);
4982          }
4983  
4984          if (!empty($htmlid)) {
4985              $this->html_id  = $htmlid;
4986          } else {
4987              $this->html_id  = 'pbar_'.uniqid();
4988          }
4989  
4990          $this->width = $width;
4991  
4992          if ($autostart) {
4993              $this->create();
4994          }
4995      }
4996  
4997      /**
4998       * Getter for ID
4999       * @return string id
5000       */
5001      public function get_id() : string {
5002          return $this->html_id;
5003      }
5004  
5005      /**
5006       * Create a new progress bar, this function will output html.
5007       *
5008       * @return void Echo's output
5009       */
5010      public function create() {
5011          global $OUTPUT;
5012  
5013          $this->time_start = microtime(true);
5014  
5015          flush();
5016          echo $OUTPUT->render($this);
5017          flush();
5018      }
5019  
5020      /**
5021       * Update the progress bar.
5022       *
5023       * @param int $percent From 1-100.
5024       * @param string $msg The message.
5025       * @return void Echo's output
5026       * @throws coding_exception
5027       */
5028      private function _update($percent, $msg) {
5029          global $OUTPUT;
5030  
5031          if (empty($this->time_start)) {
5032              throw new coding_exception('You must call create() (or use the $autostart ' .
5033                      'argument to the constructor) before you try updating the progress bar.');
5034          }
5035  
5036          $estimate = $this->estimate($percent);
5037  
5038          if ($estimate === null) {
5039              // Always do the first and last updates.
5040          } else if ($estimate == 0) {
5041              // Always do the last updates.
5042          } else if ($this->lastupdate + 20 < time()) {
5043              // We must update otherwise browser would time out.
5044          } else if (round($this->percent, 2) === round($percent, 2)) {
5045              // No significant change, no need to update anything.
5046              return;
5047          }
5048  
5049          $estimatemsg = '';
5050          if ($estimate != 0 && is_numeric($estimate)) {
5051              $estimatemsg = format_time(round($estimate));
5052          }
5053  
5054          $this->percent = $percent;
5055          $this->lastupdate = microtime(true);
5056  
5057          echo $OUTPUT->render_progress_bar_update($this->html_id, sprintf("%.1f", $this->percent), $msg, $estimatemsg);
5058          flush();
5059      }
5060  
5061      /**
5062       * Estimate how much time it is going to take.
5063       *
5064       * @param int $pt From 1-100.
5065       * @return mixed Null (unknown), or int.
5066       */
5067      private function estimate($pt) {
5068          if ($this->lastupdate == 0) {
5069              return null;
5070          }
5071          if ($pt < 0.00001) {
5072              return null; // We do not know yet how long it will take.
5073          }
5074          if ($pt > 99.99999) {
5075              return 0; // Nearly done, right?
5076          }
5077          $consumed = microtime(true) - $this->time_start;
5078          if ($consumed < 0.001) {
5079              return null;
5080          }
5081  
5082          return (100 - $pt) * ($consumed / $pt);
5083      }
5084  
5085      /**
5086       * Update progress bar according percent.
5087       *
5088       * @param int $percent From 1-100.
5089       * @param string $msg The message needed to be shown.
5090       */
5091      public function update_full($percent, $msg) {
5092          $percent = max(min($percent, 100), 0);
5093          $this->_update($percent, $msg);
5094      }
5095  
5096      /**
5097       * Update progress bar according the number of tasks.
5098       *
5099       * @param int $cur Current task number.
5100       * @param int $total Total task number.
5101       * @param string $msg The message needed to be shown.
5102       */
5103      public function update($cur, $total, $msg) {
5104          $percent = ($cur / $total) * 100;
5105          $this->update_full($percent, $msg);
5106      }
5107  
5108      /**
5109       * Restart the progress bar.
5110       */
5111      public function restart() {
5112          $this->percent    = 0;
5113          $this->lastupdate = 0;
5114          $this->time_start = 0;
5115      }
5116  
5117      /**
5118       * Export for template.
5119       *
5120       * @param  renderer_base $output The renderer.
5121       * @return array
5122       */
5123      public function export_for_template(renderer_base $output) {
5124          return [
5125              'id' => $this->html_id,
5126              'width' => $this->width,
5127          ];
5128      }
5129  }