2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Classes for rendering HTML output for Moodle.
20 * Please see {@link http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML}
23 * Included in this file are the primary renderer classes:
24 * - renderer_base: The renderer outline class that all renderers
25 * should inherit from.
26 * - core_renderer: The standard HTML renderer.
27 * - core_renderer_cli: An adaption of the standard renderer for CLI scripts.
28 * - core_renderer_ajax: An adaption of the standard renderer for AJAX scripts.
29 * - plugin_renderer_base: A renderer class that should be extended by all
34 * @copyright 2009 Tim Hunt
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38 defined('MOODLE_INTERNAL') || die();
41 * Simple base class for Moodle renderers.
43 * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
45 * Also has methods to facilitate generating HTML output.
47 * @copyright 2009 Tim Hunt
48 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
55 * @var xhtml_container_stack The xhtml_container_stack to use.
57 protected $opencontainers;
60 * @var moodle_page The Moodle page the renderer has been created to assist with.
65 * @var string The requested rendering target.
70 * @var Mustache_Engine $mustache The mustache template compiler
75 * Return an instance of the mustache class.
78 * @return Mustache_Engine
80 protected function get_mustache() {
83 if ($this->mustache === null) {
84 $themename = $this->page->theme->name;
85 $themerev = theme_get_revision();
87 $cachedir = make_localcache_directory("mustache/$themerev/$themename");
89 $loader = new \core\output\mustache_filesystem_loader();
90 $stringhelper = new \core\output\mustache_string_helper();
91 $quotehelper = new \core\output\mustache_quote_helper();
92 $jshelper = new \core\output\mustache_javascript_helper($this->page->requires);
93 $pixhelper = new \core\output\mustache_pix_helper($this);
94 $shortentexthelper = new \core\output\mustache_shorten_text_helper();
95 $userdatehelper = new \core\output\mustache_user_date_helper();
97 // We only expose the variables that are exposed to JS templates.
98 $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);
100 $helpers = array('config' => $safeconfig,
101 'str' => array($stringhelper, 'str'),
102 'quote' => array($quotehelper, 'quote'),
103 'js' => array($jshelper, 'help'),
104 'pix' => array($pixhelper, 'pix'),
105 'shortentext' => array($shortentexthelper, 'shorten'),
106 'userdate' => array($userdatehelper, 'transform'),
109 $this->mustache = new Mustache_Engine(array(
110 'cache' => $cachedir,
113 'helpers' => $helpers,
114 'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS]));
118 return $this->mustache;
125 * The constructor takes two arguments. The first is the page that the renderer
126 * has been created to assist with, and the second is the target.
127 * The target is an additional identifier that can be used to load different
128 * renderers for different options.
130 * @param moodle_page $page the page we are doing output for.
131 * @param string $target one of rendering target constants
133 public function __construct(moodle_page $page, $target) {
134 $this->opencontainers = $page->opencontainers;
136 $this->target = $target;
140 * Renders a template by name with the given context.
142 * The provided data needs to be array/stdClass made up of only simple types.
143 * Simple types are array,stdClass,bool,int,float,string
146 * @param array|stdClass $context Context containing data for the template.
147 * @return string|boolean
149 public function render_from_template($templatename, $context) {
150 static $templatecache = array();
151 $mustache = $this->get_mustache();
154 // Grab a copy of the existing helper to be restored later.
155 $uniqidhelper = $mustache->getHelper('uniqid');
156 } catch (Mustache_Exception_UnknownHelperException $e) {
157 // Helper doesn't exist.
158 $uniqidhelper = null;
161 // Provide 1 random value that will not change within a template
162 // but will be different from template to template. This is useful for
163 // e.g. aria attributes that only work with id attributes and must be
165 $mustache->addHelper('uniqid', new \core\output\mustache_uniqid_helper());
166 if (isset($templatecache[$templatename])) {
167 $template = $templatecache[$templatename];
170 $template = $mustache->loadTemplate($templatename);
171 $templatecache[$templatename] = $template;
172 } catch (Mustache_Exception_UnknownTemplateException $e) {
173 throw new moodle_exception('Unknown template: ' . $templatename);
177 $renderedtemplate = trim($template->render($context));
179 // If we had an existing uniqid helper then we need to restore it to allow
180 // handle nested calls of render_from_template.
182 $mustache->addHelper('uniqid', $uniqidhelper);
185 return $renderedtemplate;
190 * Returns rendered widget.
192 * The provided widget needs to be an object that extends the renderable
194 * If will then be rendered by a method based upon the classname for the widget.
195 * For instance a widget of class `crazywidget` will be rendered by a protected
196 * render_crazywidget method of this renderer.
198 * @param renderable $widget instance with renderable interface
201 public function render(renderable $widget) {
202 $classname = get_class($widget);
204 $classname = preg_replace('/^.*\\\/', '', $classname);
205 // Remove _renderable suffixes
206 $classname = preg_replace('/_renderable$/', '', $classname);
208 $rendermethod = 'render_'.$classname;
209 if (method_exists($this, $rendermethod)) {
210 return $this->$rendermethod($widget);
212 throw new coding_exception('Can not render widget, renderer method ('.$rendermethod.') not found.');
216 * Adds a JS action for the element with the provided id.
218 * This method adds a JS event for the provided component action to the page
219 * and then returns the id that the event has been attached to.
220 * If no id has been provided then a new ID is generated by {@link html_writer::random_id()}
222 * @param component_action $action
224 * @return string id of element, either original submitted or random new if not supplied
226 public function add_action_handler(component_action $action, $id = null) {
228 $id = html_writer::random_id($action->event);
230 $this->page->requires->event_handler("#$id", $action->event, $action->jsfunction, $action->jsfunctionargs);
235 * Returns true is output has already started, and false if not.
237 * @return boolean true if the header has been printed.
239 public function has_started() {
240 return $this->page->state >= moodle_page::STATE_IN_BODY;
244 * Given an array or space-separated list of classes, prepares and returns the HTML class attribute value
246 * @param mixed $classes Space-separated string or array of classes
247 * @return string HTML class attribute value
249 public static function prepare_classes($classes) {
250 if (is_array($classes)) {
251 return implode(' ', array_unique($classes));
256 public function pix_url($imagename, $component = 'moodle') {
257 debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.');
258 return $this->page->theme->image_url($imagename, $component);
262 * Return the moodle_url for an image.
264 * The exact image location and extension is determined
265 * automatically by searching for gif|png|jpg|jpeg, please
266 * note there can not be diferent images with the different
267 * extension. The imagename is for historical reasons
268 * a relative path name, it may be changed later for core
269 * images. It is recommended to not use subdirectories
270 * in plugin and theme pix directories.
272 * There are three types of images:
273 * 1/ theme images - stored in theme/mytheme/pix/,
274 * use component 'theme'
275 * 2/ core images - stored in /pix/,
276 * overridden via theme/mytheme/pix_core/
277 * 3/ plugin images - stored in mod/mymodule/pix,
278 * overridden via theme/mytheme/pix_plugins/mod/mymodule/,
279 * example: image_url('comment', 'mod_glossary')
281 * @param string $imagename the pathname of the image
282 * @param string $component full plugin name (aka component) or 'theme'
285 public function image_url($imagename, $component = 'moodle') {
286 return $this->page->theme->image_url($imagename, $component);
290 * Return the site's logo URL, if any.
292 * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
293 * @param int $maxheight The maximum height, or null when the maximum height does not matter.
294 * @return moodle_url|false
296 public function get_logo_url($maxwidth = null, $maxheight = 200) {
298 $logo = get_config('core_admin', 'logo');
303 // 200px high is the default image size which should be displayed at 100px in the page to account for retina displays.
304 // It's not worth the overhead of detecting and serving 2 different images based on the device.
306 // Hide the requested size in the file path.
307 $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
309 // Use $CFG->themerev to prevent browser caching when the file changes.
310 return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logo', $filepath,
311 theme_get_revision(), $logo);
315 * Return the site's compact logo URL, if any.
317 * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
318 * @param int $maxheight The maximum height, or null when the maximum height does not matter.
319 * @return moodle_url|false
321 public function get_compact_logo_url($maxwidth = 100, $maxheight = 100) {
323 $logo = get_config('core_admin', 'logocompact');
328 // Hide the requested size in the file path.
329 $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
331 // Use $CFG->themerev to prevent browser caching when the file changes.
332 return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logocompact', $filepath,
333 theme_get_revision(), $logo);
340 * Basis for all plugin renderers.
342 * @copyright Petr Skoda (skodak)
343 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
348 class plugin_renderer_base extends renderer_base {
351 * @var renderer_base|core_renderer A reference to the current renderer.
352 * The renderer provided here will be determined by the page but will in 90%
353 * of cases by the {@link core_renderer}
358 * Constructor method, calls the parent constructor
360 * @param moodle_page $page
361 * @param string $target one of rendering target constants
363 public function __construct(moodle_page $page, $target) {
364 if (empty($target) && $page->pagelayout === 'maintenance') {
365 // If the page is using the maintenance layout then we're going to force the target to maintenance.
366 // This way we'll get a special maintenance renderer that is designed to block access to API's that are likely
367 // unavailable for this page layout.
368 $target = RENDERER_TARGET_MAINTENANCE;
370 $this->output = $page->get_renderer('core', null, $target);
371 parent::__construct($page, $target);
375 * Renders the provided widget and returns the HTML to display it.
377 * @param renderable $widget instance with renderable interface
380 public function render(renderable $widget) {
381 $classname = get_class($widget);
383 $classname = preg_replace('/^.*\\\/', '', $classname);
384 // Keep a copy at this point, we may need to look for a deprecated method.
385 $deprecatedmethod = 'render_'.$classname;
386 // Remove _renderable suffixes
387 $classname = preg_replace('/_renderable$/', '', $classname);
389 $rendermethod = 'render_'.$classname;
390 if (method_exists($this, $rendermethod)) {
391 return $this->$rendermethod($widget);
393 if ($rendermethod !== $deprecatedmethod && method_exists($this, $deprecatedmethod)) {
394 // This is exactly where we don't want to be.
395 // If you have arrived here you have a renderable component within your plugin that has the name
396 // blah_renderable, and you have a render method render_blah_renderable on your plugin.
397 // In 2.8 we revamped output, as part of this change we changed slightly how renderables got rendered
398 // and the _renderable suffix now gets removed when looking for a render method.
399 // You need to change your renderers render_blah_renderable to render_blah.
400 // Until you do this it will not be possible for a theme to override the renderer to override your method.
401 // Please do it ASAP.
402 static $debugged = array();
403 if (!isset($debugged[$deprecatedmethod])) {
404 debugging(sprintf('Deprecated call. Please rename your renderables render method from %s to %s.',
405 $deprecatedmethod, $rendermethod), DEBUG_DEVELOPER);
406 $debugged[$deprecatedmethod] = true;
408 return $this->$deprecatedmethod($widget);
410 // pass to core renderer if method not found here
411 return $this->output->render($widget);
415 * Magic method used to pass calls otherwise meant for the standard renderer
416 * to it to ensure we don't go causing unnecessary grief.
418 * @param string $method
419 * @param array $arguments
422 public function __call($method, $arguments) {
423 if (method_exists('renderer_base', $method)) {
424 throw new coding_exception('Protected method called against '.get_class($this).' :: '.$method);
426 if (method_exists($this->output, $method)) {
427 return call_user_func_array(array($this->output, $method), $arguments);
429 throw new coding_exception('Unknown method called against '.get_class($this).' :: '.$method);
436 * The standard implementation of the core_renderer interface.
438 * @copyright 2009 Tim Hunt
439 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
444 class core_renderer extends renderer_base {
446 * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
447 * in layout files instead.
449 * @var string used in {@link core_renderer::header()}.
451 const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
454 * @var string Used to pass information from {@link core_renderer::doctype()} to
455 * {@link core_renderer::standard_head_html()}.
457 protected $contenttype;
460 * @var string Used by {@link core_renderer::redirect_message()} method to communicate
461 * with {@link core_renderer::header()}.
463 protected $metarefreshtag = '';
466 * @var string Unique token for the closing HTML
468 protected $unique_end_html_token;
471 * @var string Unique token for performance information
473 protected $unique_performance_info_token;
476 * @var string Unique token for the main content.
478 protected $unique_main_content_token;
483 * @param moodle_page $page the page we are doing output for.
484 * @param string $target one of rendering target constants
486 public function __construct(moodle_page $page, $target) {
487 $this->opencontainers = $page->opencontainers;
489 $this->target = $target;
491 $this->unique_end_html_token = '%%ENDHTML-'.sesskey().'%%';
492 $this->unique_performance_info_token = '%%PERFORMANCEINFO-'.sesskey().'%%';
493 $this->unique_main_content_token = '[MAIN CONTENT GOES HERE - '.sesskey().']';
497 * Get the DOCTYPE declaration that should be used with this page. Designed to
498 * be called in theme layout.php files.
500 * @return string the DOCTYPE declaration that should be used.
502 public function doctype() {
503 if ($this->page->theme->doctype === 'html5') {
504 $this->contenttype = 'text/html; charset=utf-8';
505 return "<!DOCTYPE html>\n";
507 } else if ($this->page->theme->doctype === 'xhtml5') {
508 $this->contenttype = 'application/xhtml+xml; charset=utf-8';
509 return "<!DOCTYPE html>\n";
513 $this->contenttype = 'text/html; charset=utf-8';
514 return ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n");
519 * The attributes that should be added to the <html> tag. Designed to
520 * be called in theme layout.php files.
522 * @return string HTML fragment.
524 public function htmlattributes() {
525 $return = get_html_lang(true);
526 $attributes = array();
527 if ($this->page->theme->doctype !== 'html5') {
528 $attributes['xmlns'] = 'http://www.w3.org/1999/xhtml';
531 // Give plugins an opportunity to add things like xml namespaces to the html element.
532 // This function should return an array of html attribute names => values.
533 $pluginswithfunction = get_plugins_with_function('add_htmlattributes', 'lib.php');
534 foreach ($pluginswithfunction as $plugins) {
535 foreach ($plugins as $function) {
536 $newattrs = $function();
537 unset($newattrs['dir']);
538 unset($newattrs['lang']);
539 unset($newattrs['xmlns']);
540 unset($newattrs['xml:lang']);
541 $attributes += $newattrs;
545 foreach ($attributes as $key => $val) {
547 $return .= " $key=\"$val\"";
554 * The standard tags (meta tags, links to stylesheets and JavaScript, etc.)
555 * that should be included in the <head> tag. Designed to be called in theme
558 * @return string HTML fragment.
560 public function standard_head_html() {
561 global $CFG, $SESSION;
563 // Before we output any content, we need to ensure that certain
564 // page components are set up.
566 // Blocks must be set up early as they may require javascript which
567 // has to be included in the page header before output is created.
568 foreach ($this->page->blocks->get_regions() as $region) {
569 $this->page->blocks->ensure_content_created($region, $this);
574 // Give plugins an opportunity to add any head elements. The callback
575 // must always return a string containing valid html head content.
576 $pluginswithfunction = get_plugins_with_function('before_standard_html_head', 'lib.php');
577 foreach ($pluginswithfunction as $plugins) {
578 foreach ($plugins as $function) {
579 $output .= $function();
583 // Allow a url_rewrite plugin to setup any dynamic head content.
584 if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) {
585 $class = $CFG->urlrewriteclass;
586 $output .= $class::html_head_setup();
589 $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
590 $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
591 // This is only set by the {@link redirect()} method
592 $output .= $this->metarefreshtag;
594 // Check if a periodic refresh delay has been set and make sure we arn't
595 // already meta refreshing
596 if ($this->metarefreshtag=='' && $this->page->periodicrefreshdelay!==null) {
597 $output .= '<meta http-equiv="refresh" content="'.$this->page->periodicrefreshdelay.';url='.$this->page->url->out().'" />';
600 // Smart App Banners meta tag is only displayed if mobile services are enabled and configured.
601 if (!empty($CFG->enablemobilewebservice)) {
602 $mobilesettings = get_config('tool_mobile');
603 if (!empty($mobilesettings->enablesmartappbanners) and !empty($mobilesettings->iosappid)) {
604 $output .= '<meta name="apple-itunes-app" content="app-id=' . s($mobilesettings->iosappid) . ', ';
605 $output .= 'app-argument=' . $this->page->url->out() . '"/>';
609 // Set up help link popups for all links with the helptooltip class
610 $this->page->requires->js_init_call('M.util.help_popups.setup');
612 $focus = $this->page->focuscontrol;
613 if (!empty($focus)) {
614 if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
615 // This is a horrifically bad way to handle focus but it is passed in
616 // through messy formslib::moodleform
617 $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2]));
618 } else if (strpos($focus, '.')!==false) {
619 // Old style of focus, bad way to do it
620 debugging('This code is using the old style focus event, Please update this code to focus on an element id or the moodleform focus method.', DEBUG_DEVELOPER);
621 $this->page->requires->js_function_call('old_onload_focus', explode('.', $focus, 2));
623 // Focus element with given id
624 $this->page->requires->js_function_call('focuscontrol', array($focus));
628 // Get the theme stylesheet - this has to be always first CSS, this loads also styles.css from all plugins;
629 // any other custom CSS can not be overridden via themes and is highly discouraged
630 $urls = $this->page->theme->css_urls($this->page);
631 foreach ($urls as $url) {
632 $this->page->requires->css_theme($url);
635 // Get the theme javascript head and footer
636 if ($jsurl = $this->page->theme->javascript_url(true)) {
637 $this->page->requires->js($jsurl, true);
639 if ($jsurl = $this->page->theme->javascript_url(false)) {
640 $this->page->requires->js($jsurl);
643 // Get any HTML from the page_requirements_manager.
644 $output .= $this->page->requires->get_head_code($this->page, $this);
646 // List alternate versions.
647 foreach ($this->page->alternateversions as $type => $alt) {
648 $output .= html_writer::empty_tag('link', array('rel' => 'alternate',
649 'type' => $type, 'title' => $alt->title, 'href' => $alt->url));
652 if (!empty($CFG->additionalhtmlhead)) {
653 $output .= "\n".$CFG->additionalhtmlhead;
660 * The standard tags (typically skip links) that should be output just inside
661 * the start of the <body> tag. Designed to be called in theme layout.php files.
663 * @return string HTML fragment.
665 public function standard_top_of_body_html() {
667 $output = $this->page->requires->get_top_of_body_code($this);
668 if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
669 $output .= "\n".$CFG->additionalhtmltopofbody;
672 // Give plugins an opportunity to inject extra html content. The callback
673 // must always return a string containing valid html.
674 $pluginswithfunction = get_plugins_with_function('before_standard_top_of_body_html', 'lib.php');
675 foreach ($pluginswithfunction as $plugins) {
676 foreach ($plugins as $function) {
677 $output .= $function();
681 $output .= $this->maintenance_warning();
687 * Scheduled maintenance warning message.
689 * Note: This is a nasty hack to display maintenance notice, this should be moved
690 * to some general notification area once we have it.
694 public function maintenance_warning() {
698 if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) {
699 $timeleft = $CFG->maintenance_later - time();
700 // If timeleft less than 30 sec, set the class on block to error to highlight.
701 $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
702 $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-a-1 alert');
704 $a->hour = (int)($timeleft / 3600);
705 $a->min = (int)(($timeleft / 60) % 60);
706 $a->sec = (int)($timeleft % 60);
708 $output .= get_string('maintenancemodeisscheduledlong', 'admin', $a);
710 $output .= get_string('maintenancemodeisscheduled', 'admin', $a);
713 $output .= $this->box_end();
714 $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer',
715 array(array('timeleftinsec' => $timeleft)));
716 $this->page->requires->strings_for_js(
717 array('maintenancemodeisscheduled', 'maintenancemodeisscheduledlong', 'sitemaintenance'),
724 * The standard tags (typically performance information and validation links,
725 * if we are in developer debug mode) that should be output in the footer area
726 * of the page. Designed to be called in theme layout.php files.
728 * @return string HTML fragment.
730 public function standard_footer_html() {
731 global $CFG, $SCRIPT;
733 if (during_initial_install()) {
734 // Debugging info can not work before install is finished,
735 // in any case we do not want any links during installation!
739 // This function is normally called from a layout.php file in {@link core_renderer::header()}
740 // but some of the content won't be known until later, so we return a placeholder
741 // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
742 $output = $this->unique_performance_info_token;
743 if ($this->page->devicetypeinuse == 'legacy') {
744 // The legacy theme is in use print the notification
745 $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
748 // Get links to switch device types (only shown for users not on a default device)
749 $output .= $this->theme_switch_links();
751 if (!empty($CFG->debugpageinfo)) {
752 $output .= '<div class="performanceinfo pageinfo">This page is: ' . $this->page->debug_summary() . '</div>';
754 if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) { // Only in developer mode
755 // Add link to profiling report if necessary
756 if (function_exists('profiling_is_running') && profiling_is_running()) {
757 $txt = get_string('profiledscript', 'admin');
758 $title = get_string('profiledscriptview', 'admin');
759 $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
760 $link= '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
761 $output .= '<div class="profilingfooter">' . $link . '</div>';
763 $purgeurl = new moodle_url('/admin/purgecaches.php', array('confirm' => 1,
764 'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false)));
765 $output .= '<div class="purgecaches">' .
766 html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
768 if (!empty($CFG->debugvalidators)) {
769 // NOTE: this is not a nice hack, $PAGE->url is not always accurate and $FULLME neither, it is not a bug if it fails. --skodak
770 $output .= '<div class="validators"><ul class="list-unstyled m-l-1">
771 <li><a href="http://validator.w3.org/check?verbose=1&ss=1&uri=' . urlencode(qualified_me()) . '">Validate HTML</a></li>
772 <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=-1&url1=' . urlencode(qualified_me()) . '">Section 508 Check</a></li>
773 <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=0&warnp2n3e=1&url1=' . urlencode(qualified_me()) . '">WCAG 1 (2,3) Check</a></li>
780 * Returns standard main content placeholder.
781 * Designed to be called in theme layout.php files.
783 * @return string HTML fragment.
785 public function main_content() {
786 // This is here because it is the only place we can inject the "main" role over the entire main content area
787 // without requiring all theme's to manually do it, and without creating yet another thing people need to
788 // remember in the theme.
789 // This is an unfortunate hack. DO NO EVER add anything more here.
790 // DO NOT add classes.
792 return '<div role="main">'.$this->unique_main_content_token.'</div>';
796 * The standard tags (typically script tags that are not needed earlier) that
797 * should be output after everything else. Designed to be called in theme layout.php files.
799 * @return string HTML fragment.
801 public function standard_end_of_body_html() {
804 // This function is normally called from a layout.php file in {@link core_renderer::header()}
805 // but some of the content won't be known until later, so we return a placeholder
806 // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
808 if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
809 $output .= "\n".$CFG->additionalhtmlfooter;
811 $output .= $this->unique_end_html_token;
816 * Return the standard string that says whether you are logged in (and switched
817 * roles/logged in as another user).
818 * @param bool $withlinks if false, then don't include any links in the HTML produced.
819 * If not set, the default is the nologinlinks option from the theme config.php file,
820 * and if that is not set, then links are included.
821 * @return string HTML fragment.
823 public function login_info($withlinks = null) {
824 global $USER, $CFG, $DB, $SESSION;
826 if (during_initial_install()) {
830 if (is_null($withlinks)) {
831 $withlinks = empty($this->page->layout_options['nologinlinks']);
834 $course = $this->page->course;
835 if (\core\session\manager::is_loggedinas()) {
836 $realuser = \core\session\manager::get_realuser();
837 $fullname = fullname($realuser, true);
839 $loginastitle = get_string('loginas');
840 $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&sesskey=".sesskey()."\"";
841 $realuserinfo .= "title =\"".$loginastitle."\">$fullname</a>] ";
843 $realuserinfo = " [$fullname] ";
849 $loginpage = $this->is_login_page();
850 $loginurl = get_login_url();
852 if (empty($course->id)) {
853 // $course->id is not defined during installation
855 } else if (isloggedin()) {
856 $context = context_course::instance($course->id);
858 $fullname = fullname($USER, true);
859 // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
861 $linktitle = get_string('viewprofile');
862 $username = "<a href=\"$CFG->wwwroot/user/profile.php?id=$USER->id\" title=\"$linktitle\">$fullname</a>";
864 $username = $fullname;
866 if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid))) {
868 $username .= " from <a href=\"{$idprovider->wwwroot}\">{$idprovider->name}</a>";
870 $username .= " from {$idprovider->name}";
874 $loggedinas = $realuserinfo.get_string('loggedinasguest');
875 if (!$loginpage && $withlinks) {
876 $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
878 } else if (is_role_switched($course->id)) { // Has switched roles
880 if ($role = $DB->get_record('role', array('id'=>$USER->access['rsw'][$context->path]))) {
881 $rolename = ': '.role_get_name($role, $context);
883 $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename;
885 $url = new moodle_url('/course/switchrole.php', array('id'=>$course->id,'sesskey'=>sesskey(), 'switchrole'=>0, 'returnurl'=>$this->page->url->out_as_local_url(false)));
886 $loggedinas .= ' ('.html_writer::tag('a', get_string('switchrolereturn'), array('href' => $url)).')';
889 $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username);
891 $loggedinas .= " (<a href=\"$CFG->wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').'</a>)';
895 $loggedinas = get_string('loggedinnot', 'moodle');
896 if (!$loginpage && $withlinks) {
897 $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
901 $loggedinas = '<div class="logininfo">'.$loggedinas.'</div>';
903 if (isset($SESSION->justloggedin)) {
904 unset($SESSION->justloggedin);
905 if (!empty($CFG->displayloginfailures)) {
906 if (!isguestuser()) {
907 // Include this file only when required.
908 require_once($CFG->dirroot . '/user/lib.php');
909 if ($count = user_count_login_failures($USER)) {
910 $loggedinas .= '<div class="loginfailures">';
912 $a->attempts = $count;
913 $loggedinas .= get_string('failedloginattempts', '', $a);
914 if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
915 $loggedinas .= ' ('.html_writer::link(new moodle_url('/report/log/index.php', array('chooselog' => 1,
916 'id' => 0 , 'modid' => 'site_errors')), get_string('logs')).')';
918 $loggedinas .= '</div>';
928 * Check whether the current page is a login page.
933 protected function is_login_page() {
934 // This is a real bit of a hack, but its a rarety that we need to do something like this.
935 // In fact the login pages should be only these two pages and as exposing this as an option for all pages
936 // could lead to abuse (or at least unneedingly complex code) the hack is the way to go.
938 $this->page->url->out_as_local_url(false, array()),
941 '/login/forgot_password.php',
947 * Return the 'back' link that normally appears in the footer.
949 * @return string HTML fragment.
951 public function home_link() {
954 if ($this->page->pagetype == 'site-index') {
955 // Special case for site home page - please do not remove
956 return '<div class="sitelink">' .
957 '<a title="Moodle" href="http://moodle.org/">' .
958 '<img src="' . $this->image_url('moodlelogo') . '" alt="'.get_string('moodlelogo').'" /></a></div>';
960 } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) {
961 // Special case for during install/upgrade.
962 return '<div class="sitelink">'.
963 '<a title="Moodle" href="http://docs.moodle.org/en/Administrator_documentation" onclick="this.target=\'_blank\'">' .
964 '<img src="' . $this->image_url('moodlelogo') . '" alt="'.get_string('moodlelogo').'" /></a></div>';
966 } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) {
967 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/">' .
968 get_string('home') . '</a></div>';
971 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/course/view.php?id=' . $this->page->course->id . '">' .
972 format_string($this->page->course->shortname, true, array('context' => $this->page->context)) . '</a></div>';
977 * Redirects the user by any means possible given the current state
979 * This function should not be called directly, it should always be called using
980 * the redirect function in lib/weblib.php
982 * The redirect function should really only be called before page output has started
983 * however it will allow itself to be called during the state STATE_IN_BODY
985 * @param string $encodedurl The URL to send to encoded if required
986 * @param string $message The message to display to the user if any
987 * @param int $delay The delay before redirecting a user, if $message has been
988 * set this is a requirement and defaults to 3, set to 0 no delay
989 * @param boolean $debugdisableredirect this redirect has been disabled for
990 * debugging purposes. Display a message that explains, and don't
991 * trigger the redirect.
992 * @param string $messagetype The type of notification to show the message in.
993 * See constants on \core\output\notification.
994 * @return string The HTML to display to the user before dying, may contain
995 * meta refresh, javascript refresh, and may have set header redirects
997 public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect,
998 $messagetype = \core\output\notification::NOTIFY_INFO) {
1000 $url = str_replace('&', '&', $encodedurl);
1002 switch ($this->page->state) {
1003 case moodle_page::STATE_BEFORE_HEADER :
1004 // No output yet it is safe to delivery the full arsenal of redirect methods
1005 if (!$debugdisableredirect) {
1006 // Don't use exactly the same time here, it can cause problems when both redirects fire at the same time.
1007 $this->metarefreshtag = '<meta http-equiv="refresh" content="'. $delay .'; url='. $encodedurl .'" />'."\n";
1008 $this->page->requires->js_function_call('document.location.replace', array($url), false, ($delay + 3));
1010 $output = $this->header();
1012 case moodle_page::STATE_PRINTING_HEADER :
1013 // We should hopefully never get here
1014 throw new coding_exception('You cannot redirect while printing the page header');
1016 case moodle_page::STATE_IN_BODY :
1017 // We really shouldn't be here but we can deal with this
1018 debugging("You should really redirect before you start page output");
1019 if (!$debugdisableredirect) {
1020 $this->page->requires->js_function_call('document.location.replace', array($url), false, $delay);
1022 $output = $this->opencontainers->pop_all_but_last();
1024 case moodle_page::STATE_DONE :
1025 // Too late to be calling redirect now
1026 throw new coding_exception('You cannot redirect after the entire page has been generated');
1029 $output .= $this->notification($message, $messagetype);
1030 $output .= '<div class="continuebutton">(<a href="'. $encodedurl .'">'. get_string('continue') .'</a>)</div>';
1031 if ($debugdisableredirect) {
1032 $output .= '<p><strong>'.get_string('erroroutput', 'error').'</strong></p>';
1034 $output .= $this->footer();
1039 * Start output by sending the HTTP headers, and printing the HTML <head>
1040 * and the start of the <body>.
1042 * To control what is printed, you should set properties on $PAGE. If you
1043 * are familiar with the old {@link print_header()} function from Moodle 1.9
1044 * you will find that there are properties on $PAGE that correspond to most
1045 * of the old parameters to could be passed to print_header.
1047 * Not that, in due course, the remaining $navigation, $menu parameters here
1048 * will be replaced by more properties of $PAGE, but that is still to do.
1050 * @return string HTML that you must output this, preferably immediately.
1052 public function header() {
1053 global $USER, $CFG, $SESSION;
1055 // Give plugins an opportunity touch things before the http headers are sent
1056 // such as adding additional headers. The return value is ignored.
1057 $pluginswithfunction = get_plugins_with_function('before_http_headers', 'lib.php');
1058 foreach ($pluginswithfunction as $plugins) {
1059 foreach ($plugins as $function) {
1064 if (\core\session\manager::is_loggedinas()) {
1065 $this->page->add_body_class('userloggedinas');
1068 if (isset($SESSION->justloggedin) && !empty($CFG->displayloginfailures)) {
1069 require_once($CFG->dirroot . '/user/lib.php');
1070 // Set second parameter to false as we do not want reset the counter, the same message appears on footer.
1071 if ($count = user_count_login_failures($USER, false)) {
1072 $this->page->add_body_class('loginfailures');
1076 // If the user is logged in, and we're not in initial install,
1077 // check to see if the user is role-switched and add the appropriate
1078 // CSS class to the body element.
1079 if (!during_initial_install() && isloggedin() && is_role_switched($this->page->course->id)) {
1080 $this->page->add_body_class('userswitchedrole');
1083 // Give themes a chance to init/alter the page object.
1084 $this->page->theme->init_page($this->page);
1086 $this->page->set_state(moodle_page::STATE_PRINTING_HEADER);
1088 // Find the appropriate page layout file, based on $this->page->pagelayout.
1089 $layoutfile = $this->page->theme->layout_file($this->page->pagelayout);
1090 // Render the layout using the layout file.
1091 $rendered = $this->render_page_layout($layoutfile);
1093 // Slice the rendered output into header and footer.
1094 $cutpos = strpos($rendered, $this->unique_main_content_token);
1095 if ($cutpos === false) {
1096 $cutpos = strpos($rendered, self::MAIN_CONTENT_TOKEN);
1097 $token = self::MAIN_CONTENT_TOKEN;
1099 $token = $this->unique_main_content_token;
1102 if ($cutpos === false) {
1103 throw new coding_exception('page layout file ' . $layoutfile . ' does not contain the main content placeholder, please include "<?php echo $OUTPUT->main_content() ?>" in theme layout file.');
1105 $header = substr($rendered, 0, $cutpos);
1106 $footer = substr($rendered, $cutpos + strlen($token));
1108 if (empty($this->contenttype)) {
1109 debugging('The page layout file did not call $OUTPUT->doctype()');
1110 $header = $this->doctype() . $header;
1113 // If this theme version is below 2.4 release and this is a course view page
1114 if ((!isset($this->page->theme->settings->version) || $this->page->theme->settings->version < 2012101500) &&
1115 $this->page->pagelayout === 'course' && $this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
1116 // check if course content header/footer have not been output during render of theme layout
1117 $coursecontentheader = $this->course_content_header(true);
1118 $coursecontentfooter = $this->course_content_footer(true);
1119 if (!empty($coursecontentheader)) {
1120 // display debug message and add header and footer right above and below main content
1121 // Please note that course header and footer (to be displayed above and below the whole page)
1122 // are not displayed in this case at all.
1123 // Besides the content header and footer are not displayed on any other course page
1124 debugging('The current theme is not optimised for 2.4, the course-specific header and footer defined in course format will not be output', DEBUG_DEVELOPER);
1125 $header .= $coursecontentheader;
1126 $footer = $coursecontentfooter. $footer;
1130 send_headers($this->contenttype, $this->page->cacheable);
1132 $this->opencontainers->push('header/footer', $footer);
1133 $this->page->set_state(moodle_page::STATE_IN_BODY);
1135 return $header . $this->skip_link_target('maincontent');
1139 * Renders and outputs the page layout file.
1141 * This is done by preparing the normal globals available to a script, and
1142 * then including the layout file provided by the current theme for the
1145 * @param string $layoutfile The name of the layout file
1146 * @return string HTML code
1148 protected function render_page_layout($layoutfile) {
1149 global $CFG, $SITE, $USER;
1150 // The next lines are a bit tricky. The point is, here we are in a method
1151 // of a renderer class, and this object may, or may not, be the same as
1152 // the global $OUTPUT object. When rendering the page layout file, we want to use
1153 // this object. However, people writing Moodle code expect the current
1154 // renderer to be called $OUTPUT, not $this, so define a variable called
1155 // $OUTPUT pointing at $this. The same comment applies to $PAGE and $COURSE.
1157 $PAGE = $this->page;
1158 $COURSE = $this->page->course;
1161 include($layoutfile);
1162 $rendered = ob_get_contents();
1168 * Outputs the page's footer
1170 * @return string HTML fragment
1172 public function footer() {
1173 global $CFG, $DB, $PAGE;
1175 // Give plugins an opportunity to touch the page before JS is finalized.
1176 $pluginswithfunction = get_plugins_with_function('before_footer', 'lib.php');
1177 foreach ($pluginswithfunction as $plugins) {
1178 foreach ($plugins as $function) {
1183 $output = $this->container_end_all(true);
1185 $footer = $this->opencontainers->pop('header/footer');
1187 if (debugging() and $DB and $DB->is_transaction_started()) {
1188 // TODO: MDL-20625 print warning - transaction will be rolled back
1191 // Provide some performance info if required
1192 $performanceinfo = '';
1193 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
1194 $perf = get_performance_info();
1195 if (defined('MDL_PERFTOFOOT') || debugging() || $CFG->perfdebug > 7) {
1196 $performanceinfo = $perf['html'];
1200 // We always want performance data when running a performance test, even if the user is redirected to another page.
1201 if (MDL_PERF_TEST && strpos($footer, $this->unique_performance_info_token) === false) {
1202 $footer = $this->unique_performance_info_token . $footer;
1204 $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
1206 // Only show notifications when we have a $PAGE context id.
1207 if (!empty($PAGE->context->id)) {
1208 $this->page->requires->js_call_amd('core/notification', 'init', array(
1210 \core\notification::fetch_as_array($this)
1213 $footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
1215 $this->page->set_state(moodle_page::STATE_DONE);
1217 return $output . $footer;
1221 * Close all but the last open container. This is useful in places like error
1222 * handling, where you want to close all the open containers (apart from <body>)
1223 * before outputting the error message.
1225 * @param bool $shouldbenone assert that the stack should be empty now - causes a
1226 * developer debug warning if it isn't.
1227 * @return string the HTML required to close any open containers inside <body>.
1229 public function container_end_all($shouldbenone = false) {
1230 return $this->opencontainers->pop_all_but_last($shouldbenone);
1234 * Returns course-specific information to be output immediately above content on any course page
1235 * (for the current course)
1237 * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1240 public function course_content_header($onlyifnotcalledbefore = false) {
1242 static $functioncalled = false;
1243 if ($functioncalled && $onlyifnotcalledbefore) {
1244 // we have already output the content header
1248 // Output any session notification.
1249 $notifications = \core\notification::fetch();
1251 $bodynotifications = '';
1252 foreach ($notifications as $notification) {
1253 $bodynotifications .= $this->render_from_template(
1254 $notification->get_template_name(),
1255 $notification->export_for_template($this)
1259 $output = html_writer::span($bodynotifications, 'notifications', array('id' => 'user-notifications'));
1261 if ($this->page->course->id == SITEID) {
1262 // return immediately and do not include /course/lib.php if not necessary
1266 require_once($CFG->dirroot.'/course/lib.php');
1267 $functioncalled = true;
1268 $courseformat = course_get_format($this->page->course);
1269 if (($obj = $courseformat->course_content_header()) !== null) {
1270 $output .= html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-header');
1276 * Returns course-specific information to be output immediately below content on any course page
1277 * (for the current course)
1279 * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1282 public function course_content_footer($onlyifnotcalledbefore = false) {
1284 if ($this->page->course->id == SITEID) {
1285 // return immediately and do not include /course/lib.php if not necessary
1288 static $functioncalled = false;
1289 if ($functioncalled && $onlyifnotcalledbefore) {
1290 // we have already output the content footer
1293 $functioncalled = true;
1294 require_once($CFG->dirroot.'/course/lib.php');
1295 $courseformat = course_get_format($this->page->course);
1296 if (($obj = $courseformat->course_content_footer()) !== null) {
1297 return html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-footer');
1303 * Returns course-specific information to be output on any course page in the header area
1304 * (for the current course)
1308 public function course_header() {
1310 if ($this->page->course->id == SITEID) {
1311 // return immediately and do not include /course/lib.php if not necessary
1314 require_once($CFG->dirroot.'/course/lib.php');
1315 $courseformat = course_get_format($this->page->course);
1316 if (($obj = $courseformat->course_header()) !== null) {
1317 return $courseformat->get_renderer($this->page)->render($obj);
1323 * Returns course-specific information to be output on any course page in the footer area
1324 * (for the current course)
1328 public function course_footer() {
1330 if ($this->page->course->id == SITEID) {
1331 // return immediately and do not include /course/lib.php if not necessary
1334 require_once($CFG->dirroot.'/course/lib.php');
1335 $courseformat = course_get_format($this->page->course);
1336 if (($obj = $courseformat->course_footer()) !== null) {
1337 return $courseformat->get_renderer($this->page)->render($obj);
1343 * Returns lang menu or '', this method also checks forcing of languages in courses.
1345 * This function calls {@link core_renderer::render_single_select()} to actually display the language menu.
1347 * @return string The lang menu HTML or empty string
1349 public function lang_menu() {
1352 if (empty($CFG->langmenu)) {
1356 if ($this->page->course != SITEID and !empty($this->page->course->lang)) {
1357 // do not show lang menu if language forced
1361 $currlang = current_language();
1362 $langs = get_string_manager()->get_list_of_translations();
1364 if (count($langs) < 2) {
1368 $s = new single_select($this->page->url, 'lang', $langs, $currlang, null);
1369 $s->label = get_accesshide(get_string('language'));
1370 $s->class = 'langmenu';
1371 return $this->render($s);
1375 * Output the row of editing icons for a block, as defined by the controls array.
1377 * @param array $controls an array like {@link block_contents::$controls}.
1378 * @param string $blockid The ID given to the block.
1379 * @return string HTML fragment.
1381 public function block_controls($actions, $blockid = null) {
1383 if (empty($actions)) {
1386 $menu = new action_menu($actions);
1387 if ($blockid !== null) {
1388 $menu->set_owner_selector('#'.$blockid);
1390 $menu->set_constraint('.block-region');
1391 $menu->attributes['class'] .= ' block-control-actions commands';
1392 return $this->render($menu);
1396 * Renders an action menu component.
1399 * - http://www.w3.org/WAI/GL/wiki/Using_ARIA_menus
1400 * - http://stackoverflow.com/questions/12279113/recommended-wai-aria-implementation-for-navigation-bar-menu
1402 * @param action_menu $menu
1403 * @return string HTML
1405 public function render_action_menu(action_menu $menu) {
1406 $context = $menu->export_for_template($this);
1407 return $this->render_from_template('core/action_menu', $context);
1411 * Renders an action_menu_link item.
1413 * @param action_menu_link $action
1414 * @return string HTML fragment
1416 protected function render_action_menu_link(action_menu_link $action) {
1417 return $this->render_from_template('core/action_menu_link', $action->export_for_template($this));
1421 * Renders a primary action_menu_filler item.
1423 * @param action_menu_link_filler $action
1424 * @return string HTML fragment
1426 protected function render_action_menu_filler(action_menu_filler $action) {
1427 return html_writer::span(' ', 'filler');
1431 * Renders a primary action_menu_link item.
1433 * @param action_menu_link_primary $action
1434 * @return string HTML fragment
1436 protected function render_action_menu_link_primary(action_menu_link_primary $action) {
1437 return $this->render_action_menu_link($action);
1441 * Renders a secondary action_menu_link item.
1443 * @param action_menu_link_secondary $action
1444 * @return string HTML fragment
1446 protected function render_action_menu_link_secondary(action_menu_link_secondary $action) {
1447 return $this->render_action_menu_link($action);
1451 * Prints a nice side block with an optional header.
1453 * The content is described
1454 * by a {@link core_renderer::block_contents} object.
1456 * <div id="inst{$instanceid}" class="block_{$blockname} block">
1457 * <div class="header"></div>
1458 * <div class="content">
1460 * <div class="footer">
1463 * <div class="annotation">
1467 * @param block_contents $bc HTML for the content
1468 * @param string $region the region the block is appearing in.
1469 * @return string the HTML to be output.
1471 public function block(block_contents $bc, $region) {
1472 $bc = clone($bc); // Avoid messing up the object passed in.
1473 if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) {
1474 $bc->collapsible = block_contents::NOT_HIDEABLE;
1476 if (!empty($bc->blockinstanceid)) {
1477 $bc->attributes['data-instanceid'] = $bc->blockinstanceid;
1479 $skiptitle = strip_tags($bc->title);
1480 if ($bc->blockinstanceid && !empty($skiptitle)) {
1481 $bc->attributes['aria-labelledby'] = 'instance-'.$bc->blockinstanceid.'-header';
1482 } else if (!empty($bc->arialabel)) {
1483 $bc->attributes['aria-label'] = $bc->arialabel;
1485 if ($bc->dockable) {
1486 $bc->attributes['data-dockable'] = 1;
1488 if ($bc->collapsible == block_contents::HIDDEN) {
1489 $bc->add_class('hidden');
1491 if (!empty($bc->controls)) {
1492 $bc->add_class('block_with_controls');
1496 if (empty($skiptitle)) {
1500 $output = html_writer::link('#sb-'.$bc->skipid, get_string('skipa', 'access', $skiptitle),
1501 array('class' => 'skip skip-block', 'id' => 'fsb-' . $bc->skipid));
1502 $skipdest = html_writer::span('', 'skip-block-to',
1503 array('id' => 'sb-' . $bc->skipid));
1506 $output .= html_writer::start_tag('div', $bc->attributes);
1508 $output .= $this->block_header($bc);
1509 $output .= $this->block_content($bc);
1511 $output .= html_writer::end_tag('div');
1513 $output .= $this->block_annotation($bc);
1515 $output .= $skipdest;
1517 $this->init_block_hider_js($bc);
1522 * Produces a header for a block
1524 * @param block_contents $bc
1527 protected function block_header(block_contents $bc) {
1531 $attributes = array();
1532 if ($bc->blockinstanceid) {
1533 $attributes['id'] = 'instance-'.$bc->blockinstanceid.'-header';
1535 $title = html_writer::tag('h2', $bc->title, $attributes);
1539 if (isset($bc->attributes['id'])) {
1540 $blockid = $bc->attributes['id'];
1542 $controlshtml = $this->block_controls($bc->controls, $blockid);
1545 if ($title || $controlshtml) {
1546 $output .= html_writer::tag('div', html_writer::tag('div', html_writer::tag('div', '', array('class'=>'block_action')). $title . $controlshtml, array('class' => 'title')), array('class' => 'header'));
1552 * Produces the content area for a block
1554 * @param block_contents $bc
1557 protected function block_content(block_contents $bc) {
1558 $output = html_writer::start_tag('div', array('class' => 'content'));
1559 if (!$bc->title && !$this->block_controls($bc->controls)) {
1560 $output .= html_writer::tag('div', '', array('class'=>'block_action notitle'));
1562 $output .= $bc->content;
1563 $output .= $this->block_footer($bc);
1564 $output .= html_writer::end_tag('div');
1570 * Produces the footer for a block
1572 * @param block_contents $bc
1575 protected function block_footer(block_contents $bc) {
1578 $output .= html_writer::tag('div', $bc->footer, array('class' => 'footer'));
1584 * Produces the annotation for a block
1586 * @param block_contents $bc
1589 protected function block_annotation(block_contents $bc) {
1591 if ($bc->annotation) {
1592 $output .= html_writer::tag('div', $bc->annotation, array('class' => 'blockannotation'));
1598 * Calls the JS require function to hide a block.
1600 * @param block_contents $bc A block_contents object
1602 protected function init_block_hider_js(block_contents $bc) {
1603 if (!empty($bc->attributes['id']) and $bc->collapsible != block_contents::NOT_HIDEABLE) {
1604 $config = new stdClass;
1605 $config->id = $bc->attributes['id'];
1606 $config->title = strip_tags($bc->title);
1607 $config->preference = 'block' . $bc->blockinstanceid . 'hidden';
1608 $config->tooltipVisible = get_string('hideblocka', 'access', $config->title);
1609 $config->tooltipHidden = get_string('showblocka', 'access', $config->title);
1611 $this->page->requires->js_init_call('M.util.init_block_hider', array($config));
1612 user_preference_allow_ajax_update($config->preference, PARAM_BOOL);
1617 * Render the contents of a block_list.
1619 * @param array $icons the icon for each item.
1620 * @param array $items the content of each item.
1621 * @return string HTML
1623 public function list_block_contents($icons, $items) {
1626 foreach ($items as $key => $string) {
1627 $item = html_writer::start_tag('li', array('class' => 'r' . $row));
1628 if (!empty($icons[$key])) { //test if the content has an assigned icon
1629 $item .= html_writer::tag('div', $icons[$key], array('class' => 'icon column c0'));
1631 $item .= html_writer::tag('div', $string, array('class' => 'column c1'));
1632 $item .= html_writer::end_tag('li');
1634 $row = 1 - $row; // Flip even/odd.
1636 return html_writer::tag('ul', implode("\n", $lis), array('class' => 'unlist'));
1640 * Output all the blocks in a particular region.
1642 * @param string $region the name of a region on this page.
1643 * @return string the HTML to be output.
1645 public function blocks_for_region($region) {
1646 $blockcontents = $this->page->blocks->get_content_for_region($region, $this);
1647 $blocks = $this->page->blocks->get_blocks_for_region($region);
1650 foreach ($blocks as $block) {
1651 $zones[] = $block->title;
1655 foreach ($blockcontents as $bc) {
1656 if ($bc instanceof block_contents) {
1657 $output .= $this->block($bc, $region);
1658 $lastblock = $bc->title;
1659 } else if ($bc instanceof block_move_target) {
1660 $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
1662 throw new coding_exception('Unexpected type of thing (' . get_class($bc) . ') found in list of block contents.');
1669 * Output a place where the block that is currently being moved can be dropped.
1671 * @param block_move_target $target with the necessary details.
1672 * @param array $zones array of areas where the block can be moved to
1673 * @param string $previous the block located before the area currently being rendered.
1674 * @param string $region the name of the region
1675 * @return string the HTML to be output.
1677 public function block_move_target($target, $zones, $previous, $region) {
1678 if ($previous == null) {
1679 if (empty($zones)) {
1680 // There are no zones, probably because there are no blocks.
1681 $regions = $this->page->theme->get_all_block_regions();
1682 $position = get_string('moveblockinregion', 'block', $regions[$region]);
1684 $position = get_string('moveblockbefore', 'block', $zones[0]);
1687 $position = get_string('moveblockafter', 'block', $previous);
1689 return html_writer::tag('a', html_writer::tag('span', $position, array('class' => 'accesshide')), array('href' => $target->url, 'class' => 'blockmovetarget'));
1693 * Renders a special html link with attached action
1695 * Theme developers: DO NOT OVERRIDE! Please override function
1696 * {@link core_renderer::render_action_link()} instead.
1698 * @param string|moodle_url $url
1699 * @param string $text HTML fragment
1700 * @param component_action $action
1701 * @param array $attributes associative array of html link attributes + disabled
1702 * @param pix_icon optional pix icon to render with the link
1703 * @return string HTML fragment
1705 public function action_link($url, $text, component_action $action = null, array $attributes = null, $icon = null) {
1706 if (!($url instanceof moodle_url)) {
1707 $url = new moodle_url($url);
1709 $link = new action_link($url, $text, $action, $attributes, $icon);
1711 return $this->render($link);
1715 * Renders an action_link object.
1717 * The provided link is renderer and the HTML returned. At the same time the
1718 * associated actions are setup in JS by {@link core_renderer::add_action_handler()}
1720 * @param action_link $link
1721 * @return string HTML fragment
1723 protected function render_action_link(action_link $link) {
1724 return $this->render_from_template('core/action_link', $link->export_for_template($this));
1728 * Renders an action_icon.
1730 * This function uses the {@link core_renderer::action_link()} method for the
1731 * most part. What it does different is prepare the icon as HTML and use it
1734 * Theme developers: If you want to change how action links and/or icons are rendered,
1735 * consider overriding function {@link core_renderer::render_action_link()} and
1736 * {@link core_renderer::render_pix_icon()}.
1738 * @param string|moodle_url $url A string URL or moodel_url
1739 * @param pix_icon $pixicon
1740 * @param component_action $action
1741 * @param array $attributes associative array of html link attributes + disabled
1742 * @param bool $linktext show title next to image in link
1743 * @return string HTML fragment
1745 public function action_icon($url, pix_icon $pixicon, component_action $action = null, array $attributes = null, $linktext=false) {
1746 if (!($url instanceof moodle_url)) {
1747 $url = new moodle_url($url);
1749 $attributes = (array)$attributes;
1751 if (empty($attributes['class'])) {
1752 // let ppl override the class via $options
1753 $attributes['class'] = 'action-icon';
1756 $icon = $this->render($pixicon);
1759 $text = $pixicon->attributes['alt'];
1764 return $this->action_link($url, $text.$icon, $action, $attributes);
1768 * Print a message along with button choices for Continue/Cancel
1770 * If a string or moodle_url is given instead of a single_button, method defaults to post.
1772 * @param string $message The question to ask the user
1773 * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer. Can also be a moodle_url or string URL
1774 * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer. Can also be a moodle_url or string URL
1775 * @return string HTML fragment
1777 public function confirm($message, $continue, $cancel) {
1778 if ($continue instanceof single_button) {
1780 $continue->primary = true;
1781 } else if (is_string($continue)) {
1782 $continue = new single_button(new moodle_url($continue), get_string('continue'), 'post', true);
1783 } else if ($continue instanceof moodle_url) {
1784 $continue = new single_button($continue, get_string('continue'), 'post', true);
1786 throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
1789 if ($cancel instanceof single_button) {
1791 } else if (is_string($cancel)) {
1792 $cancel = new single_button(new moodle_url($cancel), get_string('cancel'), 'get');
1793 } else if ($cancel instanceof moodle_url) {
1794 $cancel = new single_button($cancel, get_string('cancel'), 'get');
1796 throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
1799 $output = $this->box_start('generalbox modal modal-dialog modal-in-page show', 'notice');
1800 $output .= $this->box_start('modal-content', 'modal-content');
1801 $output .= $this->box_start('modal-header', 'modal-header');
1802 $output .= html_writer::tag('h4', get_string('confirm'));
1803 $output .= $this->box_end();
1804 $output .= $this->box_start('modal-body', 'modal-body');
1805 $output .= html_writer::tag('p', $message);
1806 $output .= $this->box_end();
1807 $output .= $this->box_start('modal-footer', 'modal-footer');
1808 $output .= html_writer::tag('div', $this->render($continue) . $this->render($cancel), array('class' => 'buttons'));
1809 $output .= $this->box_end();
1810 $output .= $this->box_end();
1811 $output .= $this->box_end();
1816 * Returns a form with a single button.
1818 * Theme developers: DO NOT OVERRIDE! Please override function
1819 * {@link core_renderer::render_single_button()} instead.
1821 * @param string|moodle_url $url
1822 * @param string $label button text
1823 * @param string $method get or post submit method
1824 * @param array $options associative array {disabled, title, etc.}
1825 * @return string HTML fragment
1827 public function single_button($url, $label, $method='post', array $options=null) {
1828 if (!($url instanceof moodle_url)) {
1829 $url = new moodle_url($url);
1831 $button = new single_button($url, $label, $method);
1833 foreach ((array)$options as $key=>$value) {
1834 if (array_key_exists($key, $button)) {
1835 $button->$key = $value;
1839 return $this->render($button);
1843 * Renders a single button widget.
1845 * This will return HTML to display a form containing a single button.
1847 * @param single_button $button
1848 * @return string HTML fragment
1850 protected function render_single_button(single_button $button) {
1851 $attributes = array('type' => 'submit',
1852 'value' => $button->label,
1853 'disabled' => $button->disabled ? 'disabled' : null,
1854 'title' => $button->tooltip);
1856 if ($button->actions) {
1857 $id = html_writer::random_id('single_button');
1858 $attributes['id'] = $id;
1859 foreach ($button->actions as $action) {
1860 $this->add_action_handler($action, $id);
1864 // first the input element
1865 $output = html_writer::empty_tag('input', $attributes);
1867 // then hidden fields
1868 $params = $button->url->params();
1869 if ($button->method === 'post') {
1870 $params['sesskey'] = sesskey();
1872 foreach ($params as $var => $val) {
1873 $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $var, 'value' => $val));
1876 // then div wrapper for xhtml strictness
1877 $output = html_writer::tag('div', $output);
1879 // now the form itself around it
1880 if ($button->method === 'get') {
1881 $url = $button->url->out_omit_querystring(true); // url without params, the anchor part allowed
1883 $url = $button->url->out_omit_querystring(); // url without params, the anchor part not allowed
1886 $url = '#'; // there has to be always some action
1888 $attributes = array('method' => $button->method,
1890 'id' => $button->formid);
1891 $output = html_writer::tag('form', $output, $attributes);
1893 // and finally one more wrapper with class
1894 return html_writer::tag('div', $output, array('class' => $button->class));
1898 * Returns a form with a single select widget.
1900 * Theme developers: DO NOT OVERRIDE! Please override function
1901 * {@link core_renderer::render_single_select()} instead.
1903 * @param moodle_url $url form action target, includes hidden fields
1904 * @param string $name name of selection field - the changing parameter in url
1905 * @param array $options list of options
1906 * @param string $selected selected element
1907 * @param array $nothing
1908 * @param string $formid
1909 * @param array $attributes other attributes for the single select
1910 * @return string HTML fragment
1912 public function single_select($url, $name, array $options, $selected = '',
1913 $nothing = array('' => 'choosedots'), $formid = null, $attributes = array()) {
1914 if (!($url instanceof moodle_url)) {
1915 $url = new moodle_url($url);
1917 $select = new single_select($url, $name, $options, $selected, $nothing, $formid);
1919 if (array_key_exists('label', $attributes)) {
1920 $select->set_label($attributes['label']);
1921 unset($attributes['label']);
1923 $select->attributes = $attributes;
1925 return $this->render($select);
1929 * Returns a dataformat selection and download form
1931 * @param string $label A text label
1932 * @param moodle_url|string $base The download page url
1933 * @param string $name The query param which will hold the type of the download
1934 * @param array $params Extra params sent to the download page
1935 * @return string HTML fragment
1937 public function download_dataformat_selector($label, $base, $name = 'dataformat', $params = array()) {
1939 $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat');
1941 foreach ($formats as $format) {
1942 if ($format->is_enabled()) {
1944 'value' => $format->name,
1945 'label' => get_string('dataformat', $format->component),
1949 $hiddenparams = array();
1950 foreach ($params as $key => $value) {
1951 $hiddenparams[] = array(
1960 'params' => $hiddenparams,
1961 'options' => $options,
1962 'sesskey' => sesskey(),
1963 'submit' => get_string('download'),
1966 return $this->render_from_template('core/dataformat_selector', $data);
1971 * Internal implementation of single_select rendering
1973 * @param single_select $select
1974 * @return string HTML fragment
1976 protected function render_single_select(single_select $select) {
1977 return $this->render_from_template('core/single_select', $select->export_for_template($this));
1981 * Returns a form with a url select widget.
1983 * Theme developers: DO NOT OVERRIDE! Please override function
1984 * {@link core_renderer::render_url_select()} instead.
1986 * @param array $urls list of urls - array('/course/view.php?id=1'=>'Frontpage', ....)
1987 * @param string $selected selected element
1988 * @param array $nothing
1989 * @param string $formid
1990 * @return string HTML fragment
1992 public function url_select(array $urls, $selected, $nothing = array('' => 'choosedots'), $formid = null) {
1993 $select = new url_select($urls, $selected, $nothing, $formid);
1994 return $this->render($select);
1998 * Internal implementation of url_select rendering
2000 * @param url_select $select
2001 * @return string HTML fragment
2003 protected function render_url_select(url_select $select) {
2004 return $this->render_from_template('core/url_select', $select->export_for_template($this));
2008 * Returns a string containing a link to the user documentation.
2009 * Also contains an icon by default. Shown to teachers and admin only.
2011 * @param string $path The page link after doc root and language, no leading slash.
2012 * @param string $text The text to be displayed for the link
2013 * @param boolean $forcepopup Whether to force a popup regardless of the value of $CFG->doctonewwindow
2016 public function doc_link($path, $text = '', $forcepopup = false) {
2019 $icon = $this->pix_icon('docs', '', 'moodle', array('class'=>'iconhelp icon-pre', 'role'=>'presentation'));
2021 $url = new moodle_url(get_docs_url($path));
2023 $attributes = array('href'=>$url);
2024 if (!empty($CFG->doctonewwindow) || $forcepopup) {
2025 $attributes['class'] = 'helplinkpopup';
2028 return html_writer::tag('a', $icon.$text, $attributes);
2032 * Return HTML for an activity_icon.
2034 * Theme developers: DO NOT OVERRIDE! Please override function
2035 * {@link core_renderer::render_activity_icon()} instead.
2037 * @param string $pix short pix name
2038 * @param string $alt mandatory alt attribute
2039 * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
2040 * @param array $attributes htm lattributes
2041 * @return string HTML fragment
2043 public function activity_icon($pix, $alt, $component='moodle', array $attributes = null) {
2044 $icon = new activity_icon($pix, $alt, $component, $attributes);
2045 return $this->render($icon);
2049 * Renders a pix_icon widget and returns the HTML to display it.
2051 * @param pix_icon $icon
2052 * @return string HTML fragment
2054 protected function render_activity_icon(activity_icon $icon) {
2057 $system = \core\output\icon_system::instance(\core\output\icon_system::STANDARD);
2058 return $system->render_pix_icon($this, $icon);
2062 * Return HTML for a pix_icon.
2064 * Theme developers: DO NOT OVERRIDE! Please override function
2065 * {@link core_renderer::render_pix_icon()} instead.
2067 * @param string $pix short pix name
2068 * @param string $alt mandatory alt attribute
2069 * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
2070 * @param array $attributes htm lattributes
2071 * @return string HTML fragment
2073 public function pix_icon($pix, $alt, $component='moodle', array $attributes = null) {
2074 $icon = new pix_icon($pix, $alt, $component, $attributes);
2075 return $this->render($icon);
2079 * Renders a pix_icon widget and returns the HTML to display it.
2081 * @param pix_icon $icon
2082 * @return string HTML fragment
2084 protected function render_pix_icon(pix_icon $icon) {
2087 $system = \core\output\icon_system::instance();
2088 return $system->render_pix_icon($this, $icon);
2092 * Return HTML to display an emoticon icon.
2094 * @param pix_emoticon $emoticon
2095 * @return string HTML fragment
2097 protected function render_pix_emoticon(pix_emoticon $emoticon) {
2098 return $this->render_pix_icon($emoticon);
2102 * Produces the html that represents this rating in the UI
2104 * @param rating $rating the page object on which this rating will appear
2107 function render_rating(rating $rating) {
2110 if ($rating->settings->aggregationmethod == RATING_AGGREGATE_NONE) {
2111 return null;//ratings are turned off
2114 $ratingmanager = new rating_manager();
2115 // Initialise the JavaScript so ratings can be done by AJAX.
2116 $ratingmanager->initialise_rating_javascript($this->page);
2118 $strrate = get_string("rate", "rating");
2119 $ratinghtml = ''; //the string we'll return
2121 // permissions check - can they view the aggregate?
2122 if ($rating->user_can_view_aggregate()) {
2124 $aggregatelabel = $ratingmanager->get_aggregate_label($rating->settings->aggregationmethod);
2125 $aggregatestr = $rating->get_aggregate_string();
2127 $aggregatehtml = html_writer::tag('span', $aggregatestr, array('id' => 'ratingaggregate'.$rating->itemid, 'class' => 'ratingaggregate')).' ';
2128 if ($rating->count > 0) {
2129 $countstr = "({$rating->count})";
2133 $aggregatehtml .= html_writer::tag('span', $countstr, array('id'=>"ratingcount{$rating->itemid}", 'class' => 'ratingcount')).' ';
2135 $ratinghtml .= html_writer::tag('span', $aggregatelabel, array('class'=>'rating-aggregate-label'));
2136 if ($rating->settings->permissions->viewall && $rating->settings->pluginpermissions->viewall) {
2138 $nonpopuplink = $rating->get_view_ratings_url();
2139 $popuplink = $rating->get_view_ratings_url(true);
2141 $action = new popup_action('click', $popuplink, 'ratings', array('height' => 400, 'width' => 600));
2142 $ratinghtml .= $this->action_link($nonpopuplink, $aggregatehtml, $action);
2144 $ratinghtml .= $aggregatehtml;
2149 // if the item doesn't belong to the current user, the user has permission to rate
2150 // and we're within the assessable period
2151 if ($rating->user_can_rate()) {
2153 $rateurl = $rating->get_rate_url();
2154 $inputs = $rateurl->params();
2156 //start the rating form
2158 'id' => "postrating{$rating->itemid}",
2159 'class' => 'postratingform',
2161 'action' => $rateurl->out_omit_querystring()
2163 $formstart = html_writer::start_tag('form', $formattrs);
2164 $formstart .= html_writer::start_tag('div', array('class' => 'ratingform'));
2166 // add the hidden inputs
2167 foreach ($inputs as $name => $value) {
2168 $attributes = array('type' => 'hidden', 'class' => 'ratinginput', 'name' => $name, 'value' => $value);
2169 $formstart .= html_writer::empty_tag('input', $attributes);
2172 if (empty($ratinghtml)) {
2173 $ratinghtml .= $strrate.': ';
2175 $ratinghtml = $formstart.$ratinghtml;
2177 $scalearray = array(RATING_UNSET_RATING => $strrate.'...') + $rating->settings->scale->scaleitems;
2178 $scaleattrs = array('class'=>'postratingmenu ratinginput','id'=>'menurating'.$rating->itemid);
2179 $ratinghtml .= html_writer::label($rating->rating, 'menurating'.$rating->itemid, false, array('class' => 'accesshide'));
2180 $ratinghtml .= html_writer::select($scalearray, 'rating', $rating->rating, false, $scaleattrs);
2182 //output submit button
2183 $ratinghtml .= html_writer::start_tag('span', array('class'=>"ratingsubmit"));
2185 $attributes = array('type' => 'submit', 'class' => 'postratingmenusubmit', 'id' => 'postratingsubmit'.$rating->itemid, 'value' => s(get_string('rate', 'rating')));
2186 $ratinghtml .= html_writer::empty_tag('input', $attributes);
2188 if (!$rating->settings->scale->isnumeric) {
2189 // If a global scale, try to find current course ID from the context
2190 if (empty($rating->settings->scale->courseid) and $coursecontext = $rating->context->get_course_context(false)) {
2191 $courseid = $coursecontext->instanceid;
2193 $courseid = $rating->settings->scale->courseid;
2195 $ratinghtml .= $this->help_icon_scale($courseid, $rating->settings->scale);
2197 $ratinghtml .= html_writer::end_tag('span');
2198 $ratinghtml .= html_writer::end_tag('div');
2199 $ratinghtml .= html_writer::end_tag('form');
2206 * Centered heading with attached help button (same title text)
2207 * and optional icon attached.
2209 * @param string $text A heading text
2210 * @param string $helpidentifier The keyword that defines a help page
2211 * @param string $component component name
2212 * @param string|moodle_url $icon
2213 * @param string $iconalt icon alt text
2214 * @param int $level The level of importance of the heading. Defaulting to 2
2215 * @param string $classnames A space-separated list of CSS classes. Defaulting to null
2216 * @return string HTML fragment
2218 public function heading_with_help($text, $helpidentifier, $component = 'moodle', $icon = '', $iconalt = '', $level = 2, $classnames = null) {
2221 $image = $this->pix_icon($icon, $iconalt, $component, array('class'=>'icon iconlarge'));
2225 if ($helpidentifier) {
2226 $help = $this->help_icon($helpidentifier, $component);
2229 return $this->heading($image.$text.$help, $level, $classnames);
2233 * Returns HTML to display a help icon.
2235 * @deprecated since Moodle 2.0
2237 public function old_help_icon($helpidentifier, $title, $component = 'moodle', $linktext = '') {
2238 throw new coding_exception('old_help_icon() can not be used any more, please see help_icon().');
2242 * Returns HTML to display a help icon.
2244 * Theme developers: DO NOT OVERRIDE! Please override function
2245 * {@link core_renderer::render_help_icon()} instead.
2247 * @param string $identifier The keyword that defines a help page
2248 * @param string $component component name
2249 * @param string|bool $linktext true means use $title as link text, string means link text value
2250 * @return string HTML fragment
2252 public function help_icon($identifier, $component = 'moodle', $linktext = '') {
2253 $icon = new help_icon($identifier, $component);
2254 $icon->diag_strings();
2255 if ($linktext === true) {
2256 $icon->linktext = get_string($icon->identifier, $icon->component);
2257 } else if (!empty($linktext)) {
2258 $icon->linktext = $linktext;
2260 return $this->render($icon);
2264 * Implementation of user image rendering.
2266 * @param help_icon $helpicon A help icon instance
2267 * @return string HTML fragment
2269 protected function render_help_icon(help_icon $helpicon) {
2270 return $this->render_from_template('core/help_icon', $helpicon->export_for_template($this));
2274 * Returns HTML to display a scale help icon.
2276 * @param int $courseid
2277 * @param stdClass $scale instance
2278 * @return string HTML fragment
2280 public function help_icon_scale($courseid, stdClass $scale) {
2283 $title = get_string('helpprefix2', '', $scale->name) .' ('.get_string('newwindow').')';
2285 $icon = $this->pix_icon('help', get_string('scales'), 'moodle', array('class'=>'iconhelp'));
2287 $scaleid = abs($scale->id);
2289 $link = new moodle_url('/course/scales.php', array('id' => $courseid, 'list' => true, 'scaleid' => $scaleid));
2290 $action = new popup_action('click', $link, 'ratingscale');
2292 return html_writer::tag('span', $this->action_link($link, $icon, $action), array('class' => 'helplink'));
2296 * Creates and returns a spacer image with optional line break.
2298 * @param array $attributes Any HTML attributes to add to the spaced.
2299 * @param bool $br Include a BR after the spacer.... DON'T USE THIS. Don't be
2300 * laxy do it with CSS which is a much better solution.
2301 * @return string HTML fragment
2303 public function spacer(array $attributes = null, $br = false) {
2304 $attributes = (array)$attributes;
2305 if (empty($attributes['width'])) {
2306 $attributes['width'] = 1;
2308 if (empty($attributes['height'])) {
2309 $attributes['height'] = 1;
2311 $attributes['class'] = 'spacer';
2313 $output = $this->pix_icon('spacer', '', 'moodle', $attributes);
2316 $output .= '<br />';
2323 * Returns HTML to display the specified user's avatar.
2325 * User avatar may be obtained in two ways:
2327 * // Option 1: (shortcut for simple cases, preferred way)
2328 * // $user has come from the DB and has fields id, picture, imagealt, firstname and lastname
2329 * $OUTPUT->user_picture($user, array('popup'=>true));
2332 * $userpic = new user_picture($user);
2333 * // Set properties of $userpic
2334 * $userpic->popup = true;
2335 * $OUTPUT->render($userpic);
2338 * Theme developers: DO NOT OVERRIDE! Please override function
2339 * {@link core_renderer::render_user_picture()} instead.
2341 * @param stdClass $user Object with at least fields id, picture, imagealt, firstname, lastname
2342 * If any of these are missing, the database is queried. Avoid this
2343 * if at all possible, particularly for reports. It is very bad for performance.
2344 * @param array $options associative array with user picture options, used only if not a user_picture object,
2346 * - courseid=$this->page->course->id (course id of user profile in link)
2347 * - size=35 (size of image)
2348 * - link=true (make image clickable - the link leads to user profile)
2349 * - popup=false (open in popup)
2350 * - alttext=true (add image alt attribute)
2351 * - class = image class attribute (default 'userpicture')
2352 * - visibletoscreenreaders=true (whether to be visible to screen readers)
2353 * @return string HTML fragment
2355 public function user_picture(stdClass $user, array $options = null) {
2356 $userpicture = new user_picture($user);
2357 foreach ((array)$options as $key=>$value) {
2358 if (array_key_exists($key, $userpicture)) {
2359 $userpicture->$key = $value;
2362 return $this->render($userpicture);
2366 * Internal implementation of user image rendering.
2368 * @param user_picture $userpicture
2371 protected function render_user_picture(user_picture $userpicture) {
2374 $user = $userpicture->user;
2376 if ($userpicture->alttext) {
2377 if (!empty($user->imagealt)) {
2378 $alt = $user->imagealt;
2380 $alt = get_string('pictureof', '', fullname($user));
2386 if (empty($userpicture->size)) {
2388 } else if ($userpicture->size === true or $userpicture->size == 1) {
2391 $size = $userpicture->size;
2394 $class = $userpicture->class;
2396 if ($user->picture == 0) {
2397 $class .= ' defaultuserpic';
2400 $src = $userpicture->get_url($this->page, $this);
2402 $attributes = array('src'=>$src, 'alt'=>$alt, 'title'=>$alt, 'class'=>$class, 'width'=>$size, 'height'=>$size);
2403 if (!$userpicture->visibletoscreenreaders) {
2404 $attributes['role'] = 'presentation';
2407 // get the image html output fisrt
2408 $output = html_writer::empty_tag('img', $attributes);
2410 // then wrap it in link if needed
2411 if (!$userpicture->link) {
2415 if (empty($userpicture->courseid)) {
2416 $courseid = $this->page->course->id;
2418 $courseid = $userpicture->courseid;
2421 if ($courseid == SITEID) {
2422 $url = new moodle_url('/user/profile.php', array('id' => $user->id));
2424 $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $courseid));
2427 $attributes = array('href'=>$url);
2428 if (!$userpicture->visibletoscreenreaders) {
2429 $attributes['tabindex'] = '-1';
2430 $attributes['aria-hidden'] = 'true';
2433 if ($userpicture->popup) {
2434 $id = html_writer::random_id('userpicture');
2435 $attributes['id'] = $id;
2436 $this->add_action_handler(new popup_action('click', $url), $id);
2439 return html_writer::tag('a', $output, $attributes);
2443 * Internal implementation of file tree viewer items rendering.
2448 public function htmllize_file_tree($dir) {
2449 if (empty($dir['subdirs']) and empty($dir['files'])) {
2453 foreach ($dir['subdirs'] as $subdir) {
2454 $result .= '<li>'.s($subdir['dirname']).' '.$this->htmllize_file_tree($subdir).'</li>';
2456 foreach ($dir['files'] as $file) {
2457 $filename = $file->get_filename();
2458 $result .= '<li><span>'.html_writer::link($file->fileurl, $filename).'</span></li>';
2466 * Returns HTML to display the file picker
2469 * $OUTPUT->file_picker($options);
2472 * Theme developers: DO NOT OVERRIDE! Please override function
2473 * {@link core_renderer::render_file_picker()} instead.
2475 * @param array $options associative array with file manager options
2479 * client_id=>uniqid(),
2480 * acepted_types=>'*',
2481 * return_types=>FILE_INTERNAL,
2482 * context=>$PAGE->context
2483 * @return string HTML fragment
2485 public function file_picker($options) {
2486 $fp = new file_picker($options);
2487 return $this->render($fp);
2491 * Internal implementation of file picker rendering.
2493 * @param file_picker $fp
2496 public function render_file_picker(file_picker $fp) {
2497 global $CFG, $OUTPUT, $USER;
2498 $options = $fp->options;
2499 $client_id = $options->client_id;
2500 $strsaved = get_string('filesaved', 'repository');
2501 $straddfile = get_string('openpicker', 'repository');
2502 $strloading = get_string('loading', 'repository');
2503 $strdndenabled = get_string('dndenabled_inbox', 'moodle');
2504 $strdroptoupload = get_string('droptoupload', 'moodle');
2505 $icon_progress = $OUTPUT->pix_icon('i/loading_small', $strloading).'';
2507 $currentfile = $options->currentfile;
2508 if (empty($currentfile)) {
2511 $currentfile .= ' - ';
2513 if ($options->maxbytes) {
2514 $size = $options->maxbytes;
2516 $size = get_max_upload_file_size();
2521 $maxsize = get_string('maxfilesize', 'moodle', display_size($size));
2523 if ($options->buttonname) {
2524 $buttonname = ' name="' . $options->buttonname . '"';
2529 <div class="filemanager-loading mdl-align" id='filepicker-loading-{$client_id}'>
2532 <div id="filepicker-wrapper-{$client_id}" class="mdl-left" style="display:none">
2534 <input type="button" class="btn btn-secondary fp-btn-choose" id="filepicker-button-{$client_id}" value="{$straddfile}"{$buttonname}/>
2535 <span> $maxsize </span>
2538 if ($options->env != 'url') {
2540 <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist" style="position: relative">
2541 <div class="filepicker-filename">
2542 <div class="filepicker-container">$currentfile<div class="dndupload-message">$strdndenabled <br/><div class="dndupload-arrow"></div></div></div>
2543 <div class="dndupload-progressbars"></div>
2545 <div><div class="dndupload-target">{$strdroptoupload}<br/><div class="dndupload-arrow"></div></div></div>
2554 * Returns HTML to display the 'Update this Modulename' button that appears on module pages.
2556 * @deprecated since Moodle 3.2
2558 * @param string $cmid the course_module id.
2559 * @param string $modulename the module name, eg. "forum", "quiz" or "workshop"
2560 * @return string the HTML for the button, if this user has permission to edit it, else an empty string.
2562 public function update_module_button($cmid, $modulename) {
2565 debugging('core_renderer::update_module_button() has been deprecated and should not be used anymore. Activity modules ' .
2566 'should not add the edit module button, the link is already available in the Administration block. Themes can choose ' .
2567 'to display the link in the buttons row consistently for all module types.', DEBUG_DEVELOPER);
2569 if (has_capability('moodle/course:manageactivities', context_module::instance($cmid))) {
2570 $modulename = get_string('modulename', $modulename);
2571 $string = get_string('updatethis', '', $modulename);
2572 $url = new moodle_url("$CFG->wwwroot/course/mod.php", array('update' => $cmid, 'return' => true, 'sesskey' => sesskey()));
2573 return $this->single_button($url, $string);
2580 * Returns HTML to display a "Turn editing on/off" button in a form.
2582 * @param moodle_url $url The URL + params to send through when clicking the button
2583 * @return string HTML the button
2585 public function edit_button(moodle_url $url) {
2587 $url->param('sesskey', sesskey());
2588 if ($this->page->user_is_editing()) {
2589 $url->param('edit', 'off');
2590 $editstring = get_string('turneditingoff');
2592 $url->param('edit', 'on');
2593 $editstring = get_string('turneditingon');
2596 return $this->single_button($url, $editstring);
2600 * Returns HTML to display a simple button to close a window
2602 * @param string $text The lang string for the button's label (already output from get_string())
2603 * @return string html fragment
2605 public function close_window_button($text='') {
2607 $text = get_string('closewindow');
2609 $button = new single_button(new moodle_url('#'), $text, 'get');
2610 $button->add_action(new component_action('click', 'close_window'));
2612 return $this->container($this->render($button), 'closewindow');
2616 * Output an error message. By default wraps the error message in <span class="error">.
2617 * If the error message is blank, nothing is output.
2619 * @param string $message the error message.
2620 * @return string the HTML to output.
2622 public function error_text($message) {
2623 if (empty($message)) {
2626 $message = $this->pix_icon('i/warning', get_string('error'), '', array('class' => 'icon icon-pre', 'title'=>'')) . $message;
2627 return html_writer::tag('span', $message, array('class' => 'error'));
2631 * Do not call this function directly.
2633 * To terminate the current script with a fatal error, call the {@link print_error}
2634 * function, or throw an exception. Doing either of those things will then call this
2635 * function to display the error, before terminating the execution.
2637 * @param string $message The message to output
2638 * @param string $moreinfourl URL where more info can be found about the error
2639 * @param string $link Link for the Continue button
2640 * @param array $backtrace The execution backtrace
2641 * @param string $debuginfo Debugging information
2642 * @return string the HTML to output.
2644 public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
2650 if ($this->has_started()) {
2651 // we can not always recover properly here, we have problems with output buffering,
2652 // html tables, etc.
2653 $output .= $this->opencontainers->pop_all_but_last();
2656 // It is really bad if library code throws exception when output buffering is on,
2657 // because the buffered text would be printed before our start of page.
2658 // NOTE: this hack might be behave unexpectedly in case output buffering is enabled in PHP.ini
2659 error_reporting(0); // disable notices from gzip compression, etc.
2660 while (ob_get_level() > 0) {
2661 $buff = ob_get_clean();
2662 if ($buff === false) {
2667 error_reporting($CFG->debug);
2669 // Output not yet started.
2670 $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
2671 if (empty($_SERVER['HTTP_RANGE'])) {
2672 @header($protocol . ' 404 Not Found');
2673 } else if (core_useragent::check_safari_ios_version(602) && !empty($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) {
2674 // Coax iOS 10 into sending the session cookie.
2675 @header($protocol . ' 403 Forbidden');
2677 // Must stop byteserving attempts somehow,
2678 // this is weird but Chrome PDF viewer can be stopped only with 407!
2679 @header($protocol . ' 407 Proxy Authentication Required');
2682 $this->page->set_context(null); // ugly hack - make sure page context is set to something, we do not want bogus warnings here
2683 $this->page->set_url('/'); // no url
2684 //$this->page->set_pagelayout('base'); //TODO: MDL-20676 blocks on error pages are weird, unfortunately it somehow detect the pagelayout from URL :-(
2685 $this->page->set_title(get_string('error'));
2686 $this->page->set_heading($this->page->course->fullname);
2687 $output .= $this->header();
2690 $message = '<p class="errormessage">' . $message . '</p>'.
2691 '<p class="errorcode"><a href="' . $moreinfourl . '">' .
2692 get_string('moreinformation') . '</a></p>';
2693 if (empty($CFG->rolesactive)) {
2694 $message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
2695 //It is usually not possible to recover from errors triggered during installation, you may need to create a new database or use a different database prefix for new installation.
2697 $output .= $this->box($message, 'errorbox', null, array('data-rel' => 'fatalerror'));
2699 if ($CFG->debugdeveloper) {
2700 if (!empty($debuginfo)) {
2701 $debuginfo = s($debuginfo); // removes all nasty JS
2702 $debuginfo = str_replace("\n", '<br />', $debuginfo); // keep newlines
2703 $output .= $this->notification('<strong>Debug info:</strong> '.$debuginfo, 'notifytiny');
2705 if (!empty($backtrace)) {
2706 $output .= $this->notification('<strong>Stack trace:</strong> '.format_backtrace($backtrace), 'notifytiny');
2708 if ($obbuffer !== '' ) {
2709 $output .= $this->notification('<strong>Output buffer:</strong> '.s($obbuffer), 'notifytiny');
2713 if (empty($CFG->rolesactive)) {
2714 // continue does not make much sense if moodle is not installed yet because error is most probably not recoverable
2715 } else if (!empty($link)) {
2716 $output .= $this->continue_button($link);
2719 $output .= $this->footer();
2721 // Padding to encourage IE to display our error page, rather than its own.
2722 $output .= str_repeat(' ', 512);
2728 * Output a notification (that is, a status message about something that has just happened).
2730 * Note: \core\notification::add() may be more suitable for your usage.
2732 * @param string $message The message to print out.
2733 * @param string $type The type of notification. See constants on \core\output\notification.
2734 * @return string the HTML to output.
2736 public function notification($message, $type = null) {
2739 'success' => \core\output\notification::NOTIFY_SUCCESS,
2740 'info' => \core\output\notification::NOTIFY_INFO,
2741 'warning' => \core\output\notification::NOTIFY_WARNING,
2742 'error' => \core\output\notification::NOTIFY_ERROR,
2744 // Legacy types mapped to current types.
2745 'notifyproblem' => \core\output\notification::NOTIFY_ERROR,
2746 'notifytiny' => \core\output\notification::NOTIFY_ERROR,
2747 'notifyerror' => \core\output\notification::NOTIFY_ERROR,
2748 'notifysuccess' => \core\output\notification::NOTIFY_SUCCESS,
2749 'notifymessage' => \core\output\notification::NOTIFY_INFO,
2750 'notifyredirect' => \core\output\notification::NOTIFY_INFO,
2751 'redirectmessage' => \core\output\notification::NOTIFY_INFO,
2757 if (strpos($type, ' ') === false) {
2758 // No spaces in the list of classes, therefore no need to loop over and determine the class.
2759 if (isset($typemappings[$type])) {
2760 $type = $typemappings[$type];
2762 // The value provided did not match a known type. It must be an extra class.
2763 $extraclasses = [$type];
2766 // Identify what type of notification this is.
2767 $classarray = explode(' ', self::prepare_classes($type));
2769 // Separate out the type of notification from the extra classes.
2770 foreach ($classarray as $class) {
2771 if (isset($typemappings[$class])) {
2772 $type = $typemappings[$class];
2774 $extraclasses[] = $class;
2780 $notification = new \core\output\notification($message, $type);
2781 if (count($extraclasses)) {
2782 $notification->set_extra_classes($extraclasses);
2785 // Return the rendered template.
2786 return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
2790 * Output a notification at a particular level - in this case, NOTIFY_PROBLEM.
2792 * @param string $message the message to print out
2793 * @return string HTML fragment.
2794 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2795 * @todo MDL-53113 This will be removed in Moodle 3.5.
2796 * @see \core\output\notification
2798 public function notify_problem($message) {
2799 debugging(__FUNCTION__ . ' is deprecated.' .
2800 'Please use \core\notification::add, or \core\output\notification as required',
2802 $n = new \core\output\notification($message, \core\output\notification::NOTIFY_ERROR);
2803 return $this->render($n);
2807 * Output a notification at a particular level - in this case, NOTIFY_SUCCESS.
2809 * @param string $message the message to print out
2810 * @return string HTML fragment.
2811 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2812 * @todo MDL-53113 This will be removed in Moodle 3.5.
2813 * @see \core\output\notification
2815 public function notify_success($message) {
2816 debugging(__FUNCTION__ . ' is deprecated.' .
2817 'Please use \core\notification::add, or \core\output\notification as required',
2819 $n = new \core\output\notification($message, \core\output\notification::NOTIFY_SUCCESS);
2820 return $this->render($n);
2824 * Output a notification at a particular level - in this case, NOTIFY_MESSAGE.
2826 * @param string $message the message to print out
2827 * @return string HTML fragment.
2828 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2829 * @todo MDL-53113 This will be removed in Moodle 3.5.
2830 * @see \core\output\notification
2832 public function notify_message($message) {
2833 debugging(__FUNCTION__ . ' is deprecated.' .
2834 'Please use \core\notification::add, or \core\output\notification as required',
2836 $n = new \core\output\notification($message, \core\output\notification::NOTIFY_INFO);
2837 return $this->render($n);
2841 * Output a notification at a particular level - in this case, NOTIFY_REDIRECT.
2843 * @param string $message the message to print out
2844 * @return string HTML fragment.
2845 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2846 * @todo MDL-53113 This will be removed in Moodle 3.5.
2847 * @see \core\output\notification
2849 public function notify_redirect($message) {
2850 debugging(__FUNCTION__ . ' is deprecated.' .
2851 'Please use \core\notification::add, or \core\output\notification as required',
2853 $n = new \core\output\notification($message, \core\output\notification::NOTIFY_INFO);
2854 return $this->render($n);
2858 * Render a notification (that is, a status message about something that has
2861 * @param \core\output\notification $notification the notification to print out
2862 * @return string the HTML to output.
2864 protected function render_notification(\core\output\notification $notification) {
2865 return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
2869 * Returns HTML to display a continue button that goes to a particular URL.
2871 * @param string|moodle_url $url The url the button goes to.
2872 * @return string the HTML to output.
2874 public function continue_button($url) {
2875 if (!($url instanceof moodle_url)) {
2876 $url = new moodle_url($url);
2878 $button = new single_button($url, get_string('continue'), 'get', true);
2879 $button->class = 'continuebutton';
2881 return $this->render($button);
2885 * Returns HTML to display a single paging bar to provide access to other pages (usually in a search)
2887 * Theme developers: DO NOT OVERRIDE! Please override function
2888 * {@link core_renderer::render_paging_bar()} instead.
2890 * @param int $totalcount The total number of entries available to be paged through
2891 * @param int $page The page you are currently viewing
2892 * @param int $perpage The number of entries that should be shown per page
2893 * @param string|moodle_url $baseurl url of the current page, the $pagevar parameter is added
2894 * @param string $pagevar name of page parameter that holds the page number
2895 * @return string the HTML to output.
2897 public function paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar = 'page') {
2898 $pb = new paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar);
2899 return $this->render($pb);
2903 * Internal implementation of paging bar rendering.
2905 * @param paging_bar $pagingbar
2908 protected function render_paging_bar(paging_bar $pagingbar) {
2910 $pagingbar = clone($pagingbar);
2911 $pagingbar->prepare($this, $this->page, $this->target);
2913 if ($pagingbar->totalcount > $pagingbar->perpage) {
2914 $output .= get_string('page') . ':';
2916 if (!empty($pagingbar->previouslink)) {
2917 $output .= ' (' . $pagingbar->previouslink . ') ';
2920 if (!empty($pagingbar->firstlink)) {
2921 $output .= ' ' . $pagingbar->firstlink . ' ...';
2924 foreach ($pagingbar->pagelinks as $link) {
2925 $output .= " $link";
2928 if (!empty($pagingbar->lastlink)) {
2929 $output .= ' ... ' . $pagingbar->lastlink . ' ';
2932 if (!empty($pagingbar->nextlink)) {
2933 $output .= ' (' . $pagingbar->nextlink . ')';
2937 return html_writer::tag('div', $output, array('class' => 'paging'));
2941 * Output the place a skip link goes to.
2943 * @param string $id The target name from the corresponding $PAGE->requires->skip_link_to($target) call.
2944 * @return string the HTML to output.
2946 public function skip_link_target($id = null) {
2947 return html_writer::span('', '', array('id' => $id));
2953 * @param string $text The text of the heading
2954 * @param int $level The level of importance of the heading. Defaulting to 2
2955 * @param string $classes A space-separated list of CSS classes. Defaulting to null
2956 * @param string $id An optional ID
2957 * @return string the HTML to output.
2959 public function heading($text, $level = 2, $classes = null, $id = null) {
2960 $level = (integer) $level;
2961 if ($level < 1 or $level > 6) {
2962 throw new coding_exception('Heading level must be an integer between 1 and 6.');
2964 return html_writer::tag('h' . $level, $text, array('id' => $id, 'class' => renderer_base::prepare_classes($classes)));
2970 * @param string $contents The contents of the box
2971 * @param string $classes A space-separated list of CSS classes
2972 * @param string $id An optional ID
2973 * @param array $attributes An array of other attributes to give the box.
2974 * @return string the HTML to output.
2976 public function box($contents, $classes = 'generalbox', $id = null, $attributes = array()) {
2977 return $this->box_start($classes, $id, $attributes) . $contents . $this->box_end();
2981 * Outputs the opening section of a box.
2983 * @param string $classes A space-separated list of CSS classes
2984 * @param string $id An optional ID
2985 * @param array $attributes An array of other attributes to give the box.
2986 * @return string the HTML to output.
2988 public function box_start($classes = 'generalbox', $id = null, $attributes = array()) {
2989 $this->opencontainers->push('box', html_writer::end_tag('div'));
2990 $attributes['id'] = $id;
2991 $attributes['class'] = 'box ' . renderer_base::prepare_classes($classes);
2992 return html_writer::start_tag('div', $attributes);
2996 * Outputs the closing section of a box.
2998 * @return string the HTML to output.
3000 public function box_end() {
3001 return $this->opencontainers->pop('box');
3005 * Outputs a container.
3007 * @param string $contents The contents of the box
3008 * @param string $classes A space-separated list of CSS classes
3009 * @param string $id An optional ID
3010 * @return string the HTML to output.
3012 public function container($contents, $classes = null, $id = null) {
3013 return $this->container_start($classes, $id) . $contents . $this->container_end();
3017 * Outputs the opening section of a container.
3019 * @param string $classes A space-separated list of CSS classes
3020 * @param string $id An optional ID
3021 * @return string the HTML to output.
3023 public function container_start($classes = null, $id = null) {
3024 $this->opencontainers->push('container', html_writer::end_tag('div'));
3025 return html_writer::start_tag('div', array('id' => $id,
3026 'class' => renderer_base::prepare_classes($classes)));
3030 * Outputs the closing section of a container.
3032 * @return string the HTML to output.
3034 public function container_end() {
3035 return $this->opencontainers->pop('container');
3039 * Make nested HTML lists out of the items
3041 * The resulting list will look something like this:
3045 * <<li>><div class='tree_item parent'>(item contents)</div>
3047 * <<li>><div class='tree_item'>(item contents)</div><</li>>
3053 * @param array $items
3054 * @param array $attrs html attributes passed to the top ofs the list
3055 * @return string HTML
3057 public function tree_block_contents($items, $attrs = array()) {
3058 // exit if empty, we don't want an empty ul element
3059 if (empty($items)) {
3062 // array of nested li elements
3064 foreach ($items as $item) {
3065 // this applies to the li item which contains all child lists too
3066 $content = $item->content($this);
3067 $liclasses = array($item->get_css_type());
3068 if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count()==0 && $item->nodetype==navigation_node::NODETYPE_BRANCH)) {
3069 $liclasses[] = 'collapsed';
3071 if ($item->isactive === true) {
3072 $liclasses[] = 'current_branch';
3074 $liattr = array('class'=>join(' ',$liclasses));
3075 // class attribute on the div item which only contains the item content
3076 $divclasses = array('tree_item');
3077 if ($item->children->count()>0 || $item->nodetype==navigation_node::NODETYPE_BRANCH) {
3078 $divclasses[] = 'branch';
3080 $divclasses[] = 'leaf';
3082 if (!empty($item->classes) && count($item->classes)>0) {
3083 $divclasses[] = join(' ', $item->classes);
3085 $divattr = array('class'=>join(' ', $divclasses));
3086 if (!empty($item->id)) {
3087 $divattr['id'] = $item->id;
3089 $content = html_writer::tag('p', $content, $divattr) . $this->tree_block_contents($item->children);
3090 if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
3091 $content = html_writer::empty_tag('hr') . $content;
3093 $content = html_writer::tag('li', $content, $liattr);
3096 return html_writer::tag('ul', implode("\n", $lis), $attrs);
3100 * Returns a search box.
3102 * @param string $id The search box wrapper div id, defaults to an autogenerated one.
3103 * @return string HTML with the search form hidden by default.
3105 public function search_box($id = false) {
3108 // Accessing $CFG directly as using \core_search::is_global_search_enabled would
3109 // result in an extra included file for each site, even the ones where global search
3111 if (empty($CFG->enableglobalsearch) || !has_capability('moodle/search:query', context_system::instance())) {
3118 // Needs to be cleaned, we use it for the input id.
3119 $id = clean_param($id, PARAM_ALPHANUMEXT);
3122 // JS to animate the form.
3123 $this->page->requires->js_call_amd('core/search-input', 'init', array($id));
3125 $searchicon = html_writer::tag('div', $this->pix_icon('a/search', get_string('search', 'search'), 'moodle'),
3126 array('role' => 'button', 'tabindex' => 0));
3127 $formattrs = array('class' => 'search-input-form', 'action' => $CFG->wwwroot . '/search/index.php');
3128 $inputattrs = array('type' => 'text', 'name' => 'q', 'placeholder' => get_string('search', 'search'),
3129 'size' => 13, 'tabindex' => -1, 'id' => 'id_q_' . $id);
3131 $contents = html_writer::tag('label', get_string('enteryoursearchquery', 'search'),
3132 array('for' => 'id_q_' . $id, 'class' => 'accesshide')) . html_writer::tag('input', '', $inputattrs);
3133 $searchinput = html_writer::tag('form', $contents, $formattrs);
3135 return html_writer::tag('div', $searchicon . $searchinput, array('class' => 'search-input-wrapper nav-link', 'id' => $id));
3139 * Allow plugins to provide some content to be rendered in the navbar.
3140 * The plugin must define a PLUGIN_render_navbar_output function that returns
3141 * the HTML they wish to add to the navbar.
3143 * @return string HTML for the navbar
3145 public function navbar_plugin_output() {
3148 if ($pluginsfunction = get_plugins_with_function('render_navbar_output')) {
3149 foreach ($pluginsfunction as $plugintype => $plugins) {
3150 foreach ($plugins as $pluginfunction) {
3151 $output .= $pluginfunction($this);
3160 * Construct a user menu, returning HTML that can be echoed out by a
3163 * @param stdClass $user A user object, usually $USER.
3164 * @param bool $withlinks true if a dropdown should be built.
3165 * @return string HTML fragment.
3167 public function user_menu($user = null, $withlinks = null) {
3169 require_once($CFG->dirroot . '/user/lib.php');
3171 if (is_null($user)) {
3175 // Note: this behaviour is intended to match that of core_renderer::login_info,
3176 // but should not be considered to be good practice; layout options are
3177 // intended to be theme-specific. Please don't copy this snippet anywhere else.
3178 if (is_null($withlinks)) {
3179 $withlinks = empty($this->page->layout_options['nologinlinks']);
3182 // Add a class for when $withlinks is false.
3183 $usermenuclasses = 'usermenu';
3185 $usermenuclasses .= ' withoutlinks';
3190 // If during initial install, return the empty return string.
3191 if (during_initial_install()) {
3195 $loginpage = $this->is_login_page();
3196 $loginurl = get_login_url();
3197 // If not logged in, show the typical not-logged-in string.
3198 if (!isloggedin()) {
3199 $returnstr = get_string('loggedinnot', 'moodle');
3201 $returnstr .= " (<a href=\"$loginurl\">" . get_string('login') . '</a>)';
3203 return html_writer::div(
3213 // If logged in as a guest user, show a string to that effect.
3214 if (isguestuser()) {
3215 $returnstr = get_string('loggedinasguest');
3216 if (!$loginpage && $withlinks) {
3217 $returnstr .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
3220 return html_writer::div(
3229 // Get some navigation opts.
3230 $opts = user_get_user_navigation_info($user, $this->page);
3232 $avatarclasses = "avatars";
3233 $avatarcontents = html_writer::span($opts->metadata['useravatar'], 'avatar current');
3234 $usertextcontents = $opts->metadata['userfullname'];
3237 if (!empty($opts->metadata['asotheruser'])) {
3238 $avatarcontents .= html_writer::span(
3239 $opts->metadata['realuseravatar'],
3242 $usertextcontents = $opts->metadata['realuserfullname'];
3243 $usertextcontents .= html_writer::tag(
3249 $opts->metadata['userfullname'],
3253 array('class' => 'meta viewingas')
3258 if (!empty($opts->metadata['asotherrole'])) {
3259 $role = core_text::strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['rolename'])));
3260 $usertextcontents .= html_writer::span(
3261 $opts->metadata['rolename'],
3262 'meta role role-' . $role
3266 // User login failures.
3267 if (!empty($opts->metadata['userloginfail'])) {
3268 $usertextcontents .= html_writer::span(
3269 $opts->metadata['userloginfail'],
3270 'meta loginfailures'
3275 if (!empty($opts->metadata['asmnetuser'])) {
3276 $mnet = strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['mnetidprovidername'])));
3277 $usertextcontents .= html_writer::span(
3278 $opts->metadata['mnetidprovidername'],
3279 'meta mnet mnet-' . $mnet
3283 $returnstr .= html_writer::span(
3284 html_writer::span($usertextcontents, 'usertext') .
3285 html_writer::span($avatarcontents, $avatarclasses),
3289 // Create a divider (well, a filler).
3290 $divider = new action_menu_filler();
3291 $divider->primary = false;
3293 $am = new action_menu();
3294 $am->set_menu_trigger(
3297 $am->set_alignment(action_menu::TR, action_menu::BR);
3298 $am->set_nowrap_on_items();
3300 $navitemcount = count($opts->navitems);
3302 foreach ($opts->navitems as $key => $value) {
3304 switch ($value->itemtype) {
3306 // If the nav item is a divider, add one and skip link processing.
3311 // Silently skip invalid entries (should we post a notification?).
3315 // Process this as a link item.
3317 if (isset($value->pix) && !empty($value->pix)) {
3318 $pix = new pix_icon($value->pix, $value->title, null, array('class' => 'iconsmall'));
3319 } else if (isset($value->imgsrc) && !empty($value->imgsrc)) {
3320 $value->title = html_writer::img(
3323 array('class' => 'iconsmall')
3327 $al = new action_menu_link_secondary(
3331 array('class' => 'icon')
3333 if (!empty($value->titleidentifier)) {
3334 $al->attributes['data-title'] = $value->titleidentifier;
3342 // Add dividers after the first item and before the last item.
3343 if ($idx == 1 || $idx == $navitemcount - 1) {
3349 return html_writer::div(
3356 * Return the navbar content so that it can be echoed out by the layout
3358 * @return string XHTML navbar
3360 public function navbar() {
3361 $items = $this->page->navbar->get_items();
3362 $itemcount = count($items);
3363 if ($itemcount === 0) {
3367 $htmlblocks = array();
3368 // Iterate the navarray and display each node
3369 $separator = get_separator();
3370 for ($i=0;$i < $itemcount;$i++) {
3372 $item->hideicon = true;
3374 $content = html_writer::tag('li', $this->render($item));
3376 $content = html_writer::tag('li', $separator.$this->render($item));
3378 $htmlblocks[] = $content;