MDL-41511 blocks: improved how custom block regions were being rendered.
[moodle.git] / lib / outputrenderers.php
CommitLineData
d9c8f425 1<?php
d9c8f425 2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Classes for rendering HTML output for Moodle.
19 *
7a3c215b 20 * Please see {@link http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML}
d9c8f425 21 * for an overview.
22 *
7a3c215b
SH
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
30 * plugin renderers.
31 *
f8129210 32 * @package core
76be40cc 33 * @category output
78bfb562
PS
34 * @copyright 2009 Tim Hunt
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
d9c8f425 36 */
37
78bfb562
PS
38defined('MOODLE_INTERNAL') || die();
39
d9c8f425 40/**
41 * Simple base class for Moodle renderers.
42 *
43 * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
44 *
45 * Also has methods to facilitate generating HTML output.
46 *
47 * @copyright 2009 Tim Hunt
7a3c215b
SH
48 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49 * @since Moodle 2.0
f8129210 50 * @package core
76be40cc 51 * @category output
d9c8f425 52 */
78946b9b 53class renderer_base {
7a3c215b 54 /**
76be40cc 55 * @var xhtml_container_stack The xhtml_container_stack to use.
7a3c215b 56 */
d9c8f425 57 protected $opencontainers;
7a3c215b
SH
58
59 /**
76be40cc 60 * @var moodle_page The Moodle page the renderer has been created to assist with.
7a3c215b 61 */
d9c8f425 62 protected $page;
7a3c215b
SH
63
64 /**
76be40cc 65 * @var string The requested rendering target.
7a3c215b 66 */
c927e35c 67 protected $target;
d9c8f425 68
69 /**
70 * Constructor
7a3c215b 71 *
3d3fae72 72 * The constructor takes two arguments. The first is the page that the renderer
7a3c215b
SH
73 * has been created to assist with, and the second is the target.
74 * The target is an additional identifier that can be used to load different
75 * renderers for different options.
76 *
d9c8f425 77 * @param moodle_page $page the page we are doing output for.
c927e35c 78 * @param string $target one of rendering target constants
d9c8f425 79 */
c927e35c 80 public function __construct(moodle_page $page, $target) {
d9c8f425 81 $this->opencontainers = $page->opencontainers;
82 $this->page = $page;
c927e35c 83 $this->target = $target;
d9c8f425 84 }
85
86 /**
5d0c95a5 87 * Returns rendered widget.
7a3c215b
SH
88 *
89 * The provided widget needs to be an object that extends the renderable
90 * interface.
3d3fae72 91 * If will then be rendered by a method based upon the classname for the widget.
7a3c215b
SH
92 * For instance a widget of class `crazywidget` will be rendered by a protected
93 * render_crazywidget method of this renderer.
94 *
996b1e0c 95 * @param renderable $widget instance with renderable interface
5d0c95a5 96 * @return string
d9c8f425 97 */
5d0c95a5 98 public function render(renderable $widget) {
2cf81209 99 $rendermethod = 'render_'.get_class($widget);
5d0c95a5
PS
100 if (method_exists($this, $rendermethod)) {
101 return $this->$rendermethod($widget);
102 }
103 throw new coding_exception('Can not render widget, renderer method ('.$rendermethod.') not found.');
d9c8f425 104 }
105
106 /**
7a3c215b
SH
107 * Adds a JS action for the element with the provided id.
108 *
109 * This method adds a JS event for the provided component action to the page
110 * and then returns the id that the event has been attached to.
f8129210 111 * If no id has been provided then a new ID is generated by {@link html_writer::random_id()}
7a3c215b 112 *
f8129210 113 * @param component_action $action
c80877aa
PS
114 * @param string $id
115 * @return string id of element, either original submitted or random new if not supplied
d9c8f425 116 */
7a3c215b 117 public function add_action_handler(component_action $action, $id = null) {
c80877aa
PS
118 if (!$id) {
119 $id = html_writer::random_id($action->event);
120 }
d96d8f03 121 $this->page->requires->event_handler("#$id", $action->event, $action->jsfunction, $action->jsfunctionargs);
c80877aa 122 return $id;
d9c8f425 123 }
124
125 /**
7a3c215b
SH
126 * Returns true is output has already started, and false if not.
127 *
5d0c95a5 128 * @return boolean true if the header has been printed.
d9c8f425 129 */
5d0c95a5
PS
130 public function has_started() {
131 return $this->page->state >= moodle_page::STATE_IN_BODY;
d9c8f425 132 }
133
134 /**
135 * Given an array or space-separated list of classes, prepares and returns the HTML class attribute value
7a3c215b 136 *
d9c8f425 137 * @param mixed $classes Space-separated string or array of classes
138 * @return string HTML class attribute value
139 */
140 public static function prepare_classes($classes) {
141 if (is_array($classes)) {
142 return implode(' ', array_unique($classes));
143 }
144 return $classes;
145 }
146
d9c8f425 147 /**
897e902b 148 * Return the moodle_url for an image.
7a3c215b 149 *
897e902b
PS
150 * The exact image location and extension is determined
151 * automatically by searching for gif|png|jpg|jpeg, please
152 * note there can not be diferent images with the different
153 * extension. The imagename is for historical reasons
154 * a relative path name, it may be changed later for core
155 * images. It is recommended to not use subdirectories
156 * in plugin and theme pix directories.
d9c8f425 157 *
897e902b
PS
158 * There are three types of images:
159 * 1/ theme images - stored in theme/mytheme/pix/,
160 * use component 'theme'
161 * 2/ core images - stored in /pix/,
162 * overridden via theme/mytheme/pix_core/
163 * 3/ plugin images - stored in mod/mymodule/pix,
164 * overridden via theme/mytheme/pix_plugins/mod/mymodule/,
165 * example: pix_url('comment', 'mod_glossary')
166 *
167 * @param string $imagename the pathname of the image
168 * @param string $component full plugin name (aka component) or 'theme'
78946b9b 169 * @return moodle_url
d9c8f425 170 */
c927e35c 171 public function pix_url($imagename, $component = 'moodle') {
c39e5ba2 172 return $this->page->theme->pix_url($imagename, $component);
d9c8f425 173 }
d9c8f425 174}
175
c927e35c 176
75590935
PS
177/**
178 * Basis for all plugin renderers.
179 *
f8129210
SH
180 * @copyright Petr Skoda (skodak)
181 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
182 * @since Moodle 2.0
183 * @package core
76be40cc 184 * @category output
75590935
PS
185 */
186class plugin_renderer_base extends renderer_base {
7a3c215b 187
75590935 188 /**
3d3fae72
SH
189 * @var renderer_base|core_renderer A reference to the current renderer.
190 * The renderer provided here will be determined by the page but will in 90%
f8129210 191 * of cases by the {@link core_renderer}
75590935
PS
192 */
193 protected $output;
194
195 /**
996b1e0c 196 * Constructor method, calls the parent constructor
7a3c215b 197 *
75590935 198 * @param moodle_page $page
c927e35c 199 * @param string $target one of rendering target constants
75590935 200 */
c927e35c 201 public function __construct(moodle_page $page, $target) {
166ac0a3
SH
202 if (empty($target) && $page->pagelayout === 'maintenance') {
203 // If the page is using the maintenance layout then we're going to force the target to maintenance.
204 // This way we'll get a special maintenance renderer that is designed to block access to API's that are likely
205 // unavailable for this page layout.
206 $target = RENDERER_TARGET_MAINTENANCE;
207 }
c927e35c
PS
208 $this->output = $page->get_renderer('core', null, $target);
209 parent::__construct($page, $target);
75590935 210 }
ff5265c6 211
5d0c95a5 212 /**
7a3c215b
SH
213 * Renders the provided widget and returns the HTML to display it.
214 *
71c03ac1 215 * @param renderable $widget instance with renderable interface
5d0c95a5
PS
216 * @return string
217 */
218 public function render(renderable $widget) {
219 $rendermethod = 'render_'.get_class($widget);
220 if (method_exists($this, $rendermethod)) {
221 return $this->$rendermethod($widget);
222 }
223 // pass to core renderer if method not found here
469bf7a4 224 return $this->output->render($widget);
5d0c95a5
PS
225 }
226
ff5265c6
PS
227 /**
228 * Magic method used to pass calls otherwise meant for the standard renderer
996b1e0c 229 * to it to ensure we don't go causing unnecessary grief.
ff5265c6
PS
230 *
231 * @param string $method
232 * @param array $arguments
233 * @return mixed
234 */
235 public function __call($method, $arguments) {
37b5b18e 236 if (method_exists('renderer_base', $method)) {
fede0be5 237 throw new coding_exception('Protected method called against '.get_class($this).' :: '.$method);
37b5b18e 238 }
ff5265c6
PS
239 if (method_exists($this->output, $method)) {
240 return call_user_func_array(array($this->output, $method), $arguments);
241 } else {
fede0be5 242 throw new coding_exception('Unknown method called against '.get_class($this).' :: '.$method);
ff5265c6
PS
243 }
244 }
75590935 245}
d9c8f425 246
c927e35c 247
d9c8f425 248/**
78946b9b 249 * The standard implementation of the core_renderer interface.
d9c8f425 250 *
251 * @copyright 2009 Tim Hunt
7a3c215b
SH
252 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
253 * @since Moodle 2.0
f8129210 254 * @package core
76be40cc 255 * @category output
d9c8f425 256 */
78946b9b 257class core_renderer extends renderer_base {
72009b87
PS
258 /**
259 * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
260 * in layout files instead.
72009b87 261 * @deprecated
f8129210 262 * @var string used in {@link core_renderer::header()}.
72009b87 263 */
d9c8f425 264 const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
7a3c215b
SH
265
266 /**
f8129210
SH
267 * @var string Used to pass information from {@link core_renderer::doctype()} to
268 * {@link core_renderer::standard_head_html()}.
7a3c215b 269 */
d9c8f425 270 protected $contenttype;
7a3c215b
SH
271
272 /**
f8129210
SH
273 * @var string Used by {@link core_renderer::redirect_message()} method to communicate
274 * with {@link core_renderer::header()}.
7a3c215b 275 */
d9c8f425 276 protected $metarefreshtag = '';
7a3c215b
SH
277
278 /**
76be40cc 279 * @var string Unique token for the closing HTML
7a3c215b 280 */
72009b87 281 protected $unique_end_html_token;
7a3c215b
SH
282
283 /**
76be40cc 284 * @var string Unique token for performance information
7a3c215b 285 */
72009b87 286 protected $unique_performance_info_token;
7a3c215b
SH
287
288 /**
76be40cc 289 * @var string Unique token for the main content.
7a3c215b 290 */
72009b87
PS
291 protected $unique_main_content_token;
292
293 /**
294 * Constructor
7a3c215b 295 *
72009b87
PS
296 * @param moodle_page $page the page we are doing output for.
297 * @param string $target one of rendering target constants
298 */
299 public function __construct(moodle_page $page, $target) {
300 $this->opencontainers = $page->opencontainers;
301 $this->page = $page;
302 $this->target = $target;
303
304 $this->unique_end_html_token = '%%ENDHTML-'.sesskey().'%%';
305 $this->unique_performance_info_token = '%%PERFORMANCEINFO-'.sesskey().'%%';
306 $this->unique_main_content_token = '[MAIN CONTENT GOES HERE - '.sesskey().']';
307 }
d9c8f425 308
309 /**
310 * Get the DOCTYPE declaration that should be used with this page. Designed to
311 * be called in theme layout.php files.
7a3c215b 312 *
13725b37 313 * @return string the DOCTYPE declaration that should be used.
d9c8f425 314 */
315 public function doctype() {
13725b37
PS
316 if ($this->page->theme->doctype === 'html5') {
317 $this->contenttype = 'text/html; charset=utf-8';
318 return "<!DOCTYPE html>\n";
d9c8f425 319
13725b37 320 } else if ($this->page->theme->doctype === 'xhtml5') {
d9c8f425 321 $this->contenttype = 'application/xhtml+xml; charset=utf-8';
13725b37 322 return "<!DOCTYPE html>\n";
d9c8f425 323
324 } else {
13725b37
PS
325 // legacy xhtml 1.0
326 $this->contenttype = 'text/html; charset=utf-8';
327 return ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n");
d9c8f425 328 }
d9c8f425 329 }
330
331 /**
332 * The attributes that should be added to the <html> tag. Designed to
333 * be called in theme layout.php files.
7a3c215b 334 *
d9c8f425 335 * @return string HTML fragment.
336 */
337 public function htmlattributes() {
13725b37
PS
338 $return = get_html_lang(true);
339 if ($this->page->theme->doctype !== 'html5') {
340 $return .= ' xmlns="http://www.w3.org/1999/xhtml"';
341 }
342 return $return;
d9c8f425 343 }
344
345 /**
346 * The standard tags (meta tags, links to stylesheets and JavaScript, etc.)
347 * that should be included in the <head> tag. Designed to be called in theme
348 * layout.php files.
7a3c215b 349 *
d9c8f425 350 * @return string HTML fragment.
351 */
352 public function standard_head_html() {
b5bbeaf0 353 global $CFG, $SESSION;
59849f79
AN
354
355 // Before we output any content, we need to ensure that certain
356 // page components are set up.
357
358 // Blocks must be set up early as they may require javascript which
359 // has to be included in the page header before output is created.
360 foreach ($this->page->blocks->get_regions() as $region) {
361 $this->page->blocks->ensure_content_created($region, $this);
362 }
363
d9c8f425 364 $output = '';
365 $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
366 $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
f8129210 367 // This is only set by the {@link redirect()} method
d9c8f425 368 $output .= $this->metarefreshtag;
369
370 // Check if a periodic refresh delay has been set and make sure we arn't
371 // already meta refreshing
372 if ($this->metarefreshtag=='' && $this->page->periodicrefreshdelay!==null) {
373 $output .= '<meta http-equiv="refresh" content="'.$this->page->periodicrefreshdelay.';url='.$this->page->url->out().'" />';
374 }
375
fcd2cbaf
PS
376 // flow player embedding support
377 $this->page->requires->js_function_call('M.util.load_flowplayer');
378
238b8bc9 379 // Set up help link popups for all links with the helptooltip class
afe3566c
ARN
380 $this->page->requires->js_init_call('M.util.help_popups.setup');
381
238b8bc9
ARN
382 // Setup help icon overlays.
383 $this->page->requires->yui_module('moodle-core-popuphelp', 'M.core.init_popuphelp');
384 $this->page->requires->strings_for_js(array(
385 'morehelp',
386 'loadinghelp',
387 ), 'moodle');
388
7d2a0492 389 $this->page->requires->js_function_call('setTimeout', array('fix_column_widths()', 20));
d9c8f425 390
391 $focus = $this->page->focuscontrol;
392 if (!empty($focus)) {
393 if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
394 // This is a horrifically bad way to handle focus but it is passed in
395 // through messy formslib::moodleform
7d2a0492 396 $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2]));
d9c8f425 397 } else if (strpos($focus, '.')!==false) {
398 // Old style of focus, bad way to do it
399 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);
400 $this->page->requires->js_function_call('old_onload_focus', explode('.', $focus, 2));
401 } else {
402 // Focus element with given id
7d2a0492 403 $this->page->requires->js_function_call('focuscontrol', array($focus));
d9c8f425 404 }
405 }
406
78946b9b
PS
407 // Get the theme stylesheet - this has to be always first CSS, this loads also styles.css from all plugins;
408 // any other custom CSS can not be overridden via themes and is highly discouraged
efaa4c08 409 $urls = $this->page->theme->css_urls($this->page);
78946b9b 410 foreach ($urls as $url) {
c0467479 411 $this->page->requires->css_theme($url);
78946b9b
PS
412 }
413
04c01408 414 // Get the theme javascript head and footer
d7656956
ARN
415 if ($jsurl = $this->page->theme->javascript_url(true)) {
416 $this->page->requires->js($jsurl, true);
417 }
418 if ($jsurl = $this->page->theme->javascript_url(false)) {
419 $this->page->requires->js($jsurl);
420 }
5d0c95a5 421
d9c8f425 422 // Get any HTML from the page_requirements_manager.
945f19f7 423 $output .= $this->page->requires->get_head_code($this->page, $this);
d9c8f425 424
425 // List alternate versions.
426 foreach ($this->page->alternateversions as $type => $alt) {
5d0c95a5 427 $output .= html_writer::empty_tag('link', array('rel' => 'alternate',
d9c8f425 428 'type' => $type, 'title' => $alt->title, 'href' => $alt->url));
429 }
8a7703ce 430
90e920c7
SH
431 if (!empty($CFG->additionalhtmlhead)) {
432 $output .= "\n".$CFG->additionalhtmlhead;
433 }
d9c8f425 434
435 return $output;
436 }
437
438 /**
439 * The standard tags (typically skip links) that should be output just inside
440 * the start of the <body> tag. Designed to be called in theme layout.php files.
7a3c215b 441 *
d9c8f425 442 * @return string HTML fragment.
443 */
444 public function standard_top_of_body_html() {
90e920c7
SH
445 global $CFG;
446 $output = $this->page->requires->get_top_of_body_code();
447 if (!empty($CFG->additionalhtmltopofbody)) {
448 $output .= "\n".$CFG->additionalhtmltopofbody;
449 }
48e114a5
PS
450 $output .= $this->maintenance_warning();
451 return $output;
452 }
453
454 /**
455 * Scheduled maintenance warning message.
456 *
457 * Note: This is a nasty hack to display maintenance notice, this should be moved
458 * to some general notification area once we have it.
459 *
460 * @return string
461 */
462 public function maintenance_warning() {
463 global $CFG;
464
465 $output = '';
466 if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) {
f487a8f8
RT
467 $timeleft = $CFG->maintenance_later - time();
468 // If timeleft less than 30 sec, set the class on block to error to highlight.
469 $errorclass = ($timeleft < 30) ? 'error' : 'warning';
470 $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning');
471 $a = new stdClass();
472 $a->min = (int)($timeleft/60);
473 $a->sec = (int)($timeleft % 60);
474 $output .= get_string('maintenancemodeisscheduled', 'admin', $a) ;
48e114a5 475 $output .= $this->box_end();
f487a8f8
RT
476 $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer',
477 array(array('timeleftinsec' => $timeleft)));
478 $this->page->requires->strings_for_js(
479 array('maintenancemodeisscheduled', 'sitemaintenance'),
480 'admin');
48e114a5 481 }
90e920c7 482 return $output;
d9c8f425 483 }
484
485 /**
486 * The standard tags (typically performance information and validation links,
487 * if we are in developer debug mode) that should be output in the footer area
488 * of the page. Designed to be called in theme layout.php files.
7a3c215b 489 *
d9c8f425 490 * @return string HTML fragment.
491 */
492 public function standard_footer_html() {
6af80cae 493 global $CFG, $SCRIPT;
d9c8f425 494
ec3ce3a9
PS
495 if (during_initial_install()) {
496 // Debugging info can not work before install is finished,
497 // in any case we do not want any links during installation!
498 return '';
499 }
500
f8129210 501 // This function is normally called from a layout.php file in {@link core_renderer::header()}
d9c8f425 502 // but some of the content won't be known until later, so we return a placeholder
f8129210 503 // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
72009b87 504 $output = $this->unique_performance_info_token;
e5824bb9 505 if ($this->page->devicetypeinuse == 'legacy') {
ee8df661
SH
506 // The legacy theme is in use print the notification
507 $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
508 }
37959dd4 509
e5824bb9 510 // Get links to switch device types (only shown for users not on a default device)
37959dd4
AF
511 $output .= $this->theme_switch_links();
512
d9c8f425 513 if (!empty($CFG->debugpageinfo)) {
d4c3f025 514 $output .= '<div class="performanceinfo pageinfo">This page is: ' . $this->page->debug_summary() . '</div>';
d9c8f425 515 }
b0c6dc1c 516 if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) { // Only in developer mode
6af80cae
EL
517 // Add link to profiling report if necessary
518 if (function_exists('profiling_is_running') && profiling_is_running()) {
519 $txt = get_string('profiledscript', 'admin');
520 $title = get_string('profiledscriptview', 'admin');
4ac92d2a 521 $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
6af80cae
EL
522 $link= '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
523 $output .= '<div class="profilingfooter">' . $link . '</div>';
524 }
2d22f3d9
TH
525 $purgeurl = new moodle_url('/admin/purgecaches.php', array('confirm' => 1,
526 'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false)));
527 $output .= '<div class="purgecaches">' .
528 html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
ba6c97ee 529 }
d9c8f425 530 if (!empty($CFG->debugvalidators)) {
f0202ae9 531 // 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
d9c8f425 532 $output .= '<div class="validators"><ul>
533 <li><a href="http://validator.w3.org/check?verbose=1&amp;ss=1&amp;uri=' . urlencode(qualified_me()) . '">Validate HTML</a></li>
534 <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=-1&amp;url1=' . urlencode(qualified_me()) . '">Section 508 Check</a></li>
535 <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=0&amp;warnp2n3e=1&amp;url1=' . urlencode(qualified_me()) . '">WCAG 1 (2,3) Check</a></li>
536 </ul></div>';
537 }
538 return $output;
539 }
540
72009b87
PS
541 /**
542 * Returns standard main content placeholder.
543 * Designed to be called in theme layout.php files.
7a3c215b 544 *
72009b87
PS
545 * @return string HTML fragment.
546 */
547 public function main_content() {
537ba512
SH
548 // This is here because it is the only place we can inject the "main" role over the entire main content area
549 // without requiring all theme's to manually do it, and without creating yet another thing people need to
550 // remember in the theme.
551 // This is an unfortunate hack. DO NO EVER add anything more here.
552 // DO NOT add classes.
553 // DO NOT add an id.
554 return '<div role="main">'.$this->unique_main_content_token.'</div>';
72009b87
PS
555 }
556
d9c8f425 557 /**
558 * The standard tags (typically script tags that are not needed earlier) that
391edc51 559 * should be output after everything else. Designed to be called in theme layout.php files.
7a3c215b 560 *
d9c8f425 561 * @return string HTML fragment.
562 */
563 public function standard_end_of_body_html() {
391edc51
TH
564 global $CFG;
565
f8129210 566 // This function is normally called from a layout.php file in {@link core_renderer::header()}
d9c8f425 567 // but some of the content won't be known until later, so we return a placeholder
f8129210 568 // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
391edc51
TH
569 $output = '';
570 if (!empty($CFG->additionalhtmlfooter)) {
571 $output .= "\n".$CFG->additionalhtmlfooter;
572 }
573 $output .= $this->unique_end_html_token;
574 return $output;
d9c8f425 575 }
576
577 /**
578 * Return the standard string that says whether you are logged in (and switched
579 * roles/logged in as another user).
2d0e682d
MV
580 * @param bool $withlinks if false, then don't include any links in the HTML produced.
581 * If not set, the default is the nologinlinks option from the theme config.php file,
582 * and if that is not set, then links are included.
d9c8f425 583 * @return string HTML fragment.
584 */
2d0e682d 585 public function login_info($withlinks = null) {
8f0fe0b8 586 global $USER, $CFG, $DB, $SESSION;
4bcc5118 587
244a32c6
PS
588 if (during_initial_install()) {
589 return '';
590 }
4bcc5118 591
2d0e682d
MV
592 if (is_null($withlinks)) {
593 $withlinks = empty($this->page->layout_options['nologinlinks']);
594 }
595
1dedecf2 596 $loginpage = ((string)$this->page->url === get_login_url());
244a32c6 597 $course = $this->page->course;
d79d5ac2
PS
598 if (\core\session\manager::is_loggedinas()) {
599 $realuser = \core\session\manager::get_realuser();
244a32c6 600 $fullname = fullname($realuser, true);
2d0e682d 601 if ($withlinks) {
fa84b901
RT
602 $loginastitle = get_string('loginas');
603 $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&amp;sesskey=".sesskey()."\"";
604 $realuserinfo .= "title =\"".$loginastitle."\">$fullname</a>] ";
2d0e682d
MV
605 } else {
606 $realuserinfo = " [$fullname] ";
607 }
244a32c6
PS
608 } else {
609 $realuserinfo = '';
610 }
4bcc5118 611
244a32c6 612 $loginurl = get_login_url();
4bcc5118 613
244a32c6
PS
614 if (empty($course->id)) {
615 // $course->id is not defined during installation
616 return '';
4f0c2d00 617 } else if (isloggedin()) {
b0c6dc1c 618 $context = context_course::instance($course->id);
4bcc5118 619
244a32c6 620 $fullname = fullname($USER, true);
03d9401e 621 // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
2d0e682d 622 if ($withlinks) {
c7fe9f81
RT
623 $linktitle = get_string('viewprofile');
624 $username = "<a href=\"$CFG->wwwroot/user/profile.php?id=$USER->id\" title=\"$linktitle\">$fullname</a>";
2d0e682d
MV
625 } else {
626 $username = $fullname;
627 }
244a32c6 628 if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid))) {
2d0e682d
MV
629 if ($withlinks) {
630 $username .= " from <a href=\"{$idprovider->wwwroot}\">{$idprovider->name}</a>";
631 } else {
632 $username .= " from {$idprovider->name}";
633 }
244a32c6 634 }
b3df1764 635 if (isguestuser()) {
2778744b 636 $loggedinas = $realuserinfo.get_string('loggedinasguest');
2d0e682d 637 if (!$loginpage && $withlinks) {
2778744b
PS
638 $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
639 }
f5c1e621 640 } else if (is_role_switched($course->id)) { // Has switched roles
244a32c6
PS
641 $rolename = '';
642 if ($role = $DB->get_record('role', array('id'=>$USER->access['rsw'][$context->path]))) {
f2cf0f84 643 $rolename = ': '.role_get_name($role, $context);
244a32c6 644 }
2d0e682d
MV
645 $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename;
646 if ($withlinks) {
aae028d9
PS
647 $url = new moodle_url('/course/switchrole.php', array('id'=>$course->id,'sesskey'=>sesskey(), 'switchrole'=>0, 'returnurl'=>$this->page->url->out_as_local_url(false)));
648 $loggedinas .= '('.html_writer::tag('a', get_string('switchrolereturn'), array('href'=>$url)).')';
2d0e682d 649 }
244a32c6 650 } else {
2d0e682d
MV
651 $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username);
652 if ($withlinks) {
653 $loggedinas .= " (<a href=\"$CFG->wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').'</a>)';
654 }
244a32c6
PS
655 }
656 } else {
2778744b 657 $loggedinas = get_string('loggedinnot', 'moodle');
2d0e682d 658 if (!$loginpage && $withlinks) {
2778744b
PS
659 $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
660 }
244a32c6 661 }
4bcc5118 662
244a32c6 663 $loggedinas = '<div class="logininfo">'.$loggedinas.'</div>';
4bcc5118 664
244a32c6
PS
665 if (isset($SESSION->justloggedin)) {
666 unset($SESSION->justloggedin);
667 if (!empty($CFG->displayloginfailures)) {
b3df1764 668 if (!isguestuser()) {
52dc1de7
AA
669 // Include this file only when required.
670 require_once($CFG->dirroot . '/user/lib.php');
671 if ($count = user_count_login_failures($USER)) {
2b0c88e2 672 $loggedinas .= '<div class="loginfailures">';
52dc1de7
AA
673 $a = new stdClass();
674 $a->attempts = $count;
675 $loggedinas .= get_string('failedloginattempts', '', $a);
b0c6dc1c 676 if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
52dc1de7
AA
677 $loggedinas .= html_writer::link(new moodle_url('/report/log/index.php', array('chooselog' => 1,
678 'id' => 0 , 'modid' => 'site_errors')), '(' . get_string('logs') . ')');
244a32c6
PS
679 }
680 $loggedinas .= '</div>';
681 }
682 }
683 }
684 }
4bcc5118 685
244a32c6 686 return $loggedinas;
d9c8f425 687 }
688
689 /**
690 * Return the 'back' link that normally appears in the footer.
7a3c215b 691 *
d9c8f425 692 * @return string HTML fragment.
693 */
694 public function home_link() {
695 global $CFG, $SITE;
696
697 if ($this->page->pagetype == 'site-index') {
698 // Special case for site home page - please do not remove
699 return '<div class="sitelink">' .
34dff6aa 700 '<a title="Moodle" href="http://moodle.org/">' .
53228896 701 '<img style="width:100px;height:30px" src="' . $this->pix_url('moodlelogo') . '" alt="moodlelogo" /></a></div>';
d9c8f425 702
703 } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) {
704 // Special case for during install/upgrade.
705 return '<div class="sitelink">'.
34dff6aa 706 '<a title="Moodle" href="http://docs.moodle.org/en/Administrator_documentation" onclick="this.target=\'_blank\'">' .
53228896 707 '<img style="width:100px;height:30px" src="' . $this->pix_url('moodlelogo') . '" alt="moodlelogo" /></a></div>';
d9c8f425 708
709 } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) {
710 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/">' .
711 get_string('home') . '</a></div>';
712
713 } else {
714 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/course/view.php?id=' . $this->page->course->id . '">' .
8ebbb06a 715 format_string($this->page->course->shortname, true, array('context' => $this->page->context)) . '</a></div>';
d9c8f425 716 }
717 }
718
719 /**
720 * Redirects the user by any means possible given the current state
721 *
722 * This function should not be called directly, it should always be called using
723 * the redirect function in lib/weblib.php
724 *
725 * The redirect function should really only be called before page output has started
726 * however it will allow itself to be called during the state STATE_IN_BODY
727 *
728 * @param string $encodedurl The URL to send to encoded if required
729 * @param string $message The message to display to the user if any
730 * @param int $delay The delay before redirecting a user, if $message has been
731 * set this is a requirement and defaults to 3, set to 0 no delay
732 * @param boolean $debugdisableredirect this redirect has been disabled for
733 * debugging purposes. Display a message that explains, and don't
734 * trigger the redirect.
735 * @return string The HTML to display to the user before dying, may contain
736 * meta refresh, javascript refresh, and may have set header redirects
737 */
738 public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect) {
739 global $CFG;
740 $url = str_replace('&amp;', '&', $encodedurl);
741
742 switch ($this->page->state) {
743 case moodle_page::STATE_BEFORE_HEADER :
744 // No output yet it is safe to delivery the full arsenal of redirect methods
745 if (!$debugdisableredirect) {
746 // Don't use exactly the same time here, it can cause problems when both redirects fire at the same time.
747 $this->metarefreshtag = '<meta http-equiv="refresh" content="'. $delay .'; url='. $encodedurl .'" />'."\n";
593f9b87 748 $this->page->requires->js_function_call('document.location.replace', array($url), false, ($delay + 3));
d9c8f425 749 }
750 $output = $this->header();
751 break;
752 case moodle_page::STATE_PRINTING_HEADER :
753 // We should hopefully never get here
754 throw new coding_exception('You cannot redirect while printing the page header');
755 break;
756 case moodle_page::STATE_IN_BODY :
757 // We really shouldn't be here but we can deal with this
758 debugging("You should really redirect before you start page output");
759 if (!$debugdisableredirect) {
593f9b87 760 $this->page->requires->js_function_call('document.location.replace', array($url), false, $delay);
d9c8f425 761 }
762 $output = $this->opencontainers->pop_all_but_last();
763 break;
764 case moodle_page::STATE_DONE :
765 // Too late to be calling redirect now
766 throw new coding_exception('You cannot redirect after the entire page has been generated');
767 break;
768 }
769 $output .= $this->notification($message, 'redirectmessage');
3ab2e357 770 $output .= '<div class="continuebutton">(<a href="'. $encodedurl .'">'. get_string('continue') .'</a>)</div>';
d9c8f425 771 if ($debugdisableredirect) {
772 $output .= '<p><strong>Error output, so disabling automatic redirect.</strong></p>';
773 }
774 $output .= $this->footer();
775 return $output;
776 }
777
778 /**
779 * Start output by sending the HTTP headers, and printing the HTML <head>
780 * and the start of the <body>.
781 *
782 * To control what is printed, you should set properties on $PAGE. If you
f8129210 783 * are familiar with the old {@link print_header()} function from Moodle 1.9
d9c8f425 784 * you will find that there are properties on $PAGE that correspond to most
785 * of the old parameters to could be passed to print_header.
786 *
787 * Not that, in due course, the remaining $navigation, $menu parameters here
788 * will be replaced by more properties of $PAGE, but that is still to do.
789 *
d9c8f425 790 * @return string HTML that you must output this, preferably immediately.
791 */
e120c61d 792 public function header() {
d9c8f425 793 global $USER, $CFG;
794
d79d5ac2 795 if (\core\session\manager::is_loggedinas()) {
e884f63a
PS
796 $this->page->add_body_class('userloggedinas');
797 }
798
63c88397
PS
799 // Give themes a chance to init/alter the page object.
800 $this->page->theme->init_page($this->page);
801
d9c8f425 802 $this->page->set_state(moodle_page::STATE_PRINTING_HEADER);
803
78946b9b
PS
804 // Find the appropriate page layout file, based on $this->page->pagelayout.
805 $layoutfile = $this->page->theme->layout_file($this->page->pagelayout);
806 // Render the layout using the layout file.
807 $rendered = $this->render_page_layout($layoutfile);
67e84a7f 808
78946b9b 809 // Slice the rendered output into header and footer.
72009b87
PS
810 $cutpos = strpos($rendered, $this->unique_main_content_token);
811 if ($cutpos === false) {
812 $cutpos = strpos($rendered, self::MAIN_CONTENT_TOKEN);
813 $token = self::MAIN_CONTENT_TOKEN;
814 } else {
815 $token = $this->unique_main_content_token;
816 }
817
d9c8f425 818 if ($cutpos === false) {
72009b87 819 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.');
d9c8f425 820 }
78946b9b 821 $header = substr($rendered, 0, $cutpos);
72009b87 822 $footer = substr($rendered, $cutpos + strlen($token));
d9c8f425 823
824 if (empty($this->contenttype)) {
78946b9b 825 debugging('The page layout file did not call $OUTPUT->doctype()');
67e84a7f 826 $header = $this->doctype() . $header;
d9c8f425 827 }
828
fdd4b9a5
MG
829 // If this theme version is below 2.4 release and this is a course view page
830 if ((!isset($this->page->theme->settings->version) || $this->page->theme->settings->version < 2012101500) &&
831 $this->page->pagelayout === 'course' && $this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
832 // check if course content header/footer have not been output during render of theme layout
833 $coursecontentheader = $this->course_content_header(true);
834 $coursecontentfooter = $this->course_content_footer(true);
835 if (!empty($coursecontentheader)) {
836 // display debug message and add header and footer right above and below main content
837 // Please note that course header and footer (to be displayed above and below the whole page)
838 // are not displayed in this case at all.
839 // Besides the content header and footer are not displayed on any other course page
840 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);
841 $header .= $coursecontentheader;
842 $footer = $coursecontentfooter. $footer;
843 }
844 }
845
d9c8f425 846 send_headers($this->contenttype, $this->page->cacheable);
67e84a7f 847
d9c8f425 848 $this->opencontainers->push('header/footer', $footer);
849 $this->page->set_state(moodle_page::STATE_IN_BODY);
67e84a7f 850
29ba64e5 851 return $header . $this->skip_link_target('maincontent');
d9c8f425 852 }
853
854 /**
78946b9b 855 * Renders and outputs the page layout file.
7a3c215b
SH
856 *
857 * This is done by preparing the normal globals available to a script, and
858 * then including the layout file provided by the current theme for the
859 * requested layout.
860 *
78946b9b 861 * @param string $layoutfile The name of the layout file
d9c8f425 862 * @return string HTML code
863 */
78946b9b 864 protected function render_page_layout($layoutfile) {
92e01ab7 865 global $CFG, $SITE, $USER;
d9c8f425 866 // The next lines are a bit tricky. The point is, here we are in a method
867 // of a renderer class, and this object may, or may not, be the same as
78946b9b 868 // the global $OUTPUT object. When rendering the page layout file, we want to use
d9c8f425 869 // this object. However, people writing Moodle code expect the current
870 // renderer to be called $OUTPUT, not $this, so define a variable called
871 // $OUTPUT pointing at $this. The same comment applies to $PAGE and $COURSE.
872 $OUTPUT = $this;
873 $PAGE = $this->page;
874 $COURSE = $this->page->course;
875
d9c8f425 876 ob_start();
78946b9b
PS
877 include($layoutfile);
878 $rendered = ob_get_contents();
d9c8f425 879 ob_end_clean();
78946b9b 880 return $rendered;
d9c8f425 881 }
882
883 /**
884 * Outputs the page's footer
7a3c215b 885 *
d9c8f425 886 * @return string HTML fragment
887 */
888 public function footer() {
d5a8d9aa 889 global $CFG, $DB;
0f0801f4 890
f6794ace 891 $output = $this->container_end_all(true);
4d2ee4c2 892
d9c8f425 893 $footer = $this->opencontainers->pop('header/footer');
894
d5a8d9aa 895 if (debugging() and $DB and $DB->is_transaction_started()) {
03221650 896 // TODO: MDL-20625 print warning - transaction will be rolled back
d5a8d9aa
PS
897 }
898
d9c8f425 899 // Provide some performance info if required
900 $performanceinfo = '';
901 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
902 $perf = get_performance_info();
d9c8f425 903 if (defined('MDL_PERFTOFOOT') || debugging() || $CFG->perfdebug > 7) {
904 $performanceinfo = $perf['html'];
905 }
906 }
58a3a34e
DM
907
908 // We always want performance data when running a performance test, even if the user is redirected to another page.
909 if (MDL_PERF_TEST && strpos($footer, $this->unique_performance_info_token) === false) {
910 $footer = $this->unique_performance_info_token . $footer;
911 }
72009b87 912 $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
d9c8f425 913
72009b87 914 $footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
d9c8f425 915
916 $this->page->set_state(moodle_page::STATE_DONE);
d9c8f425 917
918 return $output . $footer;
919 }
920
f6794ace
PS
921 /**
922 * Close all but the last open container. This is useful in places like error
923 * handling, where you want to close all the open containers (apart from <body>)
924 * before outputting the error message.
7a3c215b 925 *
f6794ace
PS
926 * @param bool $shouldbenone assert that the stack should be empty now - causes a
927 * developer debug warning if it isn't.
928 * @return string the HTML required to close any open containers inside <body>.
929 */
930 public function container_end_all($shouldbenone = false) {
931 return $this->opencontainers->pop_all_but_last($shouldbenone);
932 }
933
fdd4b9a5
MG
934 /**
935 * Returns course-specific information to be output immediately above content on any course page
936 * (for the current course)
937 *
938 * @param bool $onlyifnotcalledbefore output content only if it has not been output before
939 * @return string
940 */
941 public function course_content_header($onlyifnotcalledbefore = false) {
942 global $CFG;
943 if ($this->page->course->id == SITEID) {
944 // return immediately and do not include /course/lib.php if not necessary
945 return '';
946 }
947 static $functioncalled = false;
948 if ($functioncalled && $onlyifnotcalledbefore) {
949 // we have already output the content header
950 return '';
951 }
952 require_once($CFG->dirroot.'/course/lib.php');
953 $functioncalled = true;
954 $courseformat = course_get_format($this->page->course);
955 if (($obj = $courseformat->course_content_header()) !== null) {
06a72e01 956 return html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-header');
fdd4b9a5
MG
957 }
958 return '';
959 }
960
961 /**
962 * Returns course-specific information to be output immediately below content on any course page
963 * (for the current course)
964 *
965 * @param bool $onlyifnotcalledbefore output content only if it has not been output before
966 * @return string
967 */
968 public function course_content_footer($onlyifnotcalledbefore = false) {
969 global $CFG;
970 if ($this->page->course->id == SITEID) {
971 // return immediately and do not include /course/lib.php if not necessary
972 return '';
973 }
974 static $functioncalled = false;
975 if ($functioncalled && $onlyifnotcalledbefore) {
976 // we have already output the content footer
977 return '';
978 }
979 $functioncalled = true;
980 require_once($CFG->dirroot.'/course/lib.php');
981 $courseformat = course_get_format($this->page->course);
982 if (($obj = $courseformat->course_content_footer()) !== null) {
06a72e01 983 return html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-footer');
fdd4b9a5
MG
984 }
985 return '';
986 }
987
988 /**
989 * Returns course-specific information to be output on any course page in the header area
990 * (for the current course)
991 *
992 * @return string
993 */
994 public function course_header() {
995 global $CFG;
996 if ($this->page->course->id == SITEID) {
997 // return immediately and do not include /course/lib.php if not necessary
998 return '';
999 }
1000 require_once($CFG->dirroot.'/course/lib.php');
1001 $courseformat = course_get_format($this->page->course);
1002 if (($obj = $courseformat->course_header()) !== null) {
1003 return $courseformat->get_renderer($this->page)->render($obj);
1004 }
1005 return '';
1006 }
1007
1008 /**
1009 * Returns course-specific information to be output on any course page in the footer area
1010 * (for the current course)
1011 *
1012 * @return string
1013 */
1014 public function course_footer() {
1015 global $CFG;
1016 if ($this->page->course->id == SITEID) {
1017 // return immediately and do not include /course/lib.php if not necessary
1018 return '';
1019 }
1020 require_once($CFG->dirroot.'/course/lib.php');
1021 $courseformat = course_get_format($this->page->course);
1022 if (($obj = $courseformat->course_footer()) !== null) {
1023 return $courseformat->get_renderer($this->page)->render($obj);
1024 }
1025 return '';
1026 }
1027
244a32c6
PS
1028 /**
1029 * Returns lang menu or '', this method also checks forcing of languages in courses.
7a3c215b 1030 *
2fada290
MG
1031 * This function calls {@link core_renderer::render_single_select()} to actually display the language menu.
1032 *
7a3c215b 1033 * @return string The lang menu HTML or empty string
244a32c6
PS
1034 */
1035 public function lang_menu() {
1036 global $CFG;
1037
1038 if (empty($CFG->langmenu)) {
1039 return '';
1040 }
1041
1042 if ($this->page->course != SITEID and !empty($this->page->course->lang)) {
1043 // do not show lang menu if language forced
1044 return '';
1045 }
1046
1047 $currlang = current_language();
1f96e907 1048 $langs = get_string_manager()->get_list_of_translations();
4bcc5118 1049
244a32c6
PS
1050 if (count($langs) < 2) {
1051 return '';
1052 }
1053
a9967cf5
PS
1054 $s = new single_select($this->page->url, 'lang', $langs, $currlang, null);
1055 $s->label = get_accesshide(get_string('language'));
1056 $s->class = 'langmenu';
1057 return $this->render($s);
244a32c6
PS
1058 }
1059
d9c8f425 1060 /**
1061 * Output the row of editing icons for a block, as defined by the controls array.
7a3c215b 1062 *
f8129210 1063 * @param array $controls an array like {@link block_contents::$controls}.
b59f2e3b 1064 * @param string $blockid The ID given to the block.
7a3c215b 1065 * @return string HTML fragment.
d9c8f425 1066 */
b59f2e3b 1067 public function block_controls($actions, $blockid = null) {
10fc1569 1068 global $CFG;
b59f2e3b
SH
1069 if (empty($actions)) {
1070 return '';
1071 }
cf69a00a 1072 $menu = new action_menu($actions);
b59f2e3b
SH
1073 if ($blockid !== null) {
1074 $menu->set_owner_selector('#'.$blockid);
1075 }
ae3fd8eb 1076 $menu->set_constraint('.block-region');
b59f2e3b 1077 $menu->attributes['class'] .= ' block-control-actions commands';
10fc1569
SH
1078 if (isset($CFG->blockeditingmenu) && !$CFG->blockeditingmenu) {
1079 $menu->do_not_enhance();
1080 }
b59f2e3b
SH
1081 return $this->render($menu);
1082 }
1083
1084 /**
1085 * Renders an action menu component.
1086 *
3665af78
SH
1087 * ARIA references:
1088 * - http://www.w3.org/WAI/GL/wiki/Using_ARIA_menus
1089 * - http://stackoverflow.com/questions/12279113/recommended-wai-aria-implementation-for-navigation-bar-menu
1090 *
b59f2e3b
SH
1091 * @param action_menu $menu
1092 * @return string HTML
1093 */
1094 public function render_action_menu(action_menu $menu) {
1095 $menu->initialise_js($this->page);
1096
1097 $output = html_writer::start_tag('div', $menu->attributes);
3665af78 1098 $output .= html_writer::start_tag('ul', $menu->attributesprimary);
b59f2e3b
SH
1099 foreach ($menu->get_primary_actions($this) as $action) {
1100 if ($action instanceof renderable) {
3665af78 1101 $content = $this->render($action);
b59f2e3b 1102 } else {
3665af78 1103 $content = $action;
e282c679 1104 }
11dd4dad 1105 $output .= html_writer::tag('li', $content, array('role' => 'presentation'));
d9c8f425 1106 }
3665af78
SH
1107 $output .= html_writer::end_tag('ul');
1108 $output .= html_writer::start_tag('ul', $menu->attributessecondary);
b59f2e3b 1109 foreach ($menu->get_secondary_actions() as $action) {
3665af78
SH
1110 if ($action instanceof renderable) {
1111 $content = $this->render($action);
3665af78
SH
1112 } else {
1113 $content = $action;
3665af78 1114 }
11dd4dad 1115 $output .= html_writer::tag('li', $content, array('role' => 'presentation'));
b59f2e3b 1116 }
3665af78 1117 $output .= html_writer::end_tag('ul');
b59f2e3b 1118 $output .= html_writer::end_tag('div');
e282c679 1119 return $output;
d9c8f425 1120 }
1121
cf69a00a 1122 /**
3665af78 1123 * Renders an action_menu_link item.
cf69a00a 1124 *
3665af78 1125 * @param action_menu_link $action
cf69a00a
SH
1126 * @return string HTML fragment
1127 */
3665af78 1128 protected function render_action_menu_link(action_menu_link $action) {
d0647301
SH
1129 static $actioncount = 0;
1130 $actioncount++;
cf69a00a 1131
ea5a01fb 1132 $comparetoalt = '';
cf69a00a 1133 $text = '';
ea5a01fb 1134 if (!$action->icon || $action->primary === false) {
d0647301 1135 $text .= html_writer::start_tag('span', array('class'=>'menu-action-text', 'id' => 'actionmenuaction-'.$actioncount));
cf69a00a
SH
1136 if ($action->text instanceof renderable) {
1137 $text .= $this->render($action->text);
1138 } else {
1139 $text .= $action->text;
cfd1e08a 1140 $comparetoalt = (string)$action->text;
cf69a00a
SH
1141 }
1142 $text .= html_writer::end_tag('span');
1143 }
1144
ea5a01fb
SH
1145 $icon = '';
1146 if ($action->icon) {
1147 $icon = $action->icon;
f803ce26 1148 if ($action->primary || !$action->actionmenu->will_be_enhanced()) {
ea5a01fb
SH
1149 $action->attributes['title'] = $action->text;
1150 }
cfd1e08a
SH
1151 if (!$action->primary && $action->actionmenu->will_be_enhanced()) {
1152 if ((string)$icon->attributes['alt'] === $comparetoalt) {
d0647301 1153 $icon->attributes['alt'] = '';
cfd1e08a 1154 }
c7a2291f 1155 if (isset($icon->attributes['title']) && (string)$icon->attributes['title'] === $comparetoalt) {
cfd1e08a
SH
1156 unset($icon->attributes['title']);
1157 }
1158 }
ea5a01fb
SH
1159 $icon = $this->render($icon);
1160 }
1161
3665af78 1162 // A disabled link is rendered as formatted text.
cf69a00a 1163 if (!empty($action->attributes['disabled'])) {
3665af78 1164 // Do not use div here due to nesting restriction in xhtml strict.
ea5a01fb 1165 return html_writer::tag('span', $icon.$text, array('class'=>'currentlink', 'role' => 'menuitem'));
cf69a00a
SH
1166 }
1167
1168 $attributes = $action->attributes;
1169 unset($action->attributes['disabled']);
1170 $attributes['href'] = $action->url;
d0647301
SH
1171 if ($text !== '') {
1172 $attributes['aria-labelledby'] = 'actionmenuaction-'.$actioncount;
1173 }
cf69a00a 1174
ea5a01fb 1175 return html_writer::tag('a', $icon.$text, $attributes);
cf69a00a
SH
1176 }
1177
9ac099a1
AN
1178 /**
1179 * Renders a primary action_menu_filler item.
1180 *
1181 * @param action_menu_link_filler $action
1182 * @return string HTML fragment
1183 */
1184 protected function render_action_menu_filler(action_menu_filler $action) {
1185 return html_writer::span('&nbsp;', 'filler');
1186 }
1187
cf69a00a 1188 /**
3665af78 1189 * Renders a primary action_menu_link item.
cf69a00a 1190 *
3665af78 1191 * @param action_menu_link_primary $action
cf69a00a
SH
1192 * @return string HTML fragment
1193 */
3665af78
SH
1194 protected function render_action_menu_link_primary(action_menu_link_primary $action) {
1195 return $this->render_action_menu_link($action);
cf69a00a
SH
1196 }
1197
1198 /**
3665af78 1199 * Renders a secondary action_menu_link item.
cf69a00a 1200 *
3665af78 1201 * @param action_menu_link_secondary $action
cf69a00a
SH
1202 * @return string HTML fragment
1203 */
3665af78
SH
1204 protected function render_action_menu_link_secondary(action_menu_link_secondary $action) {
1205 return $this->render_action_menu_link($action);
cf69a00a
SH
1206 }
1207
d9c8f425 1208 /**
1209 * Prints a nice side block with an optional header.
1210 *
1211 * The content is described
f8129210 1212 * by a {@link core_renderer::block_contents} object.
d9c8f425 1213 *
cbb54cce
SH
1214 * <div id="inst{$instanceid}" class="block_{$blockname} block">
1215 * <div class="header"></div>
1216 * <div class="content">
1217 * ...CONTENT...
1218 * <div class="footer">
1219 * </div>
1220 * </div>
1221 * <div class="annotation">
1222 * </div>
1223 * </div>
1224 *
d9c8f425 1225 * @param block_contents $bc HTML for the content
1226 * @param string $region the region the block is appearing in.
1227 * @return string the HTML to be output.
1228 */
7a3c215b 1229 public function block(block_contents $bc, $region) {
d9c8f425 1230 $bc = clone($bc); // Avoid messing up the object passed in.
dd72b308
PS
1231 if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) {
1232 $bc->collapsible = block_contents::NOT_HIDEABLE;
1233 }
84192d78
SH
1234 if (!empty($bc->blockinstanceid)) {
1235 $bc->attributes['data-instanceid'] = $bc->blockinstanceid;
1236 }
91d941c3
SH
1237 $skiptitle = strip_tags($bc->title);
1238 if ($bc->blockinstanceid && !empty($skiptitle)) {
1239 $bc->attributes['aria-labelledby'] = 'instance-'.$bc->blockinstanceid.'-header';
1240 } else if (!empty($bc->arialabel)) {
1241 $bc->attributes['aria-label'] = $bc->arialabel;
1242 }
84192d78
SH
1243 if ($bc->dockable) {
1244 $bc->attributes['data-dockable'] = 1;
1245 }
dd72b308
PS
1246 if ($bc->collapsible == block_contents::HIDDEN) {
1247 $bc->add_class('hidden');
1248 }
1249 if (!empty($bc->controls)) {
1250 $bc->add_class('block_with_controls');
1251 }
d9c8f425 1252
91d941c3 1253
d9c8f425 1254 if (empty($skiptitle)) {
1255 $output = '';
1256 $skipdest = '';
1257 } else {
26acc814
PS
1258 $output = html_writer::tag('a', get_string('skipa', 'access', $skiptitle), array('href' => '#sb-' . $bc->skipid, 'class' => 'skip-block'));
1259 $skipdest = html_writer::tag('span', '', array('id' => 'sb-' . $bc->skipid, 'class' => 'skip-block-to'));
d9c8f425 1260 }
4d2ee4c2 1261
5d0c95a5 1262 $output .= html_writer::start_tag('div', $bc->attributes);
d9c8f425 1263
9f5c39b5
SH
1264 $output .= $this->block_header($bc);
1265 $output .= $this->block_content($bc);
1266
1267 $output .= html_writer::end_tag('div');
1268
1269 $output .= $this->block_annotation($bc);
1270
1271 $output .= $skipdest;
1272
1273 $this->init_block_hider_js($bc);
1274 return $output;
1275 }
1276
1277 /**
1278 * Produces a header for a block
fa7f2a45 1279 *
9f5c39b5
SH
1280 * @param block_contents $bc
1281 * @return string
1282 */
1283 protected function block_header(block_contents $bc) {
d9c8f425 1284
1285 $title = '';
1286 if ($bc->title) {
91d941c3
SH
1287 $attributes = array();
1288 if ($bc->blockinstanceid) {
1289 $attributes['id'] = 'instance-'.$bc->blockinstanceid.'-header';
1290 }
1291 $title = html_writer::tag('h2', $bc->title, $attributes);
d9c8f425 1292 }
1293
b59f2e3b
SH
1294 $blockid = null;
1295 if (isset($bc->attributes['id'])) {
1296 $blockid = $bc->attributes['id'];
1297 }
1298 $controlshtml = $this->block_controls($bc->controls, $blockid);
9f5c39b5
SH
1299
1300 $output = '';
d9c8f425 1301 if ($title || $controlshtml) {
46de77b6 1302 $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'));
d9c8f425 1303 }
9f5c39b5
SH
1304 return $output;
1305 }
d9c8f425 1306
9f5c39b5
SH
1307 /**
1308 * Produces the content area for a block
1309 *
1310 * @param block_contents $bc
1311 * @return string
1312 */
1313 protected function block_content(block_contents $bc) {
1314 $output = html_writer::start_tag('div', array('class' => 'content'));
1315 if (!$bc->title && !$this->block_controls($bc->controls)) {
46de77b6
SH
1316 $output .= html_writer::tag('div', '', array('class'=>'block_action notitle'));
1317 }
d9c8f425 1318 $output .= $bc->content;
9f5c39b5
SH
1319 $output .= $this->block_footer($bc);
1320 $output .= html_writer::end_tag('div');
fa7f2a45 1321
9f5c39b5
SH
1322 return $output;
1323 }
d9c8f425 1324
9f5c39b5
SH
1325 /**
1326 * Produces the footer for a block
1327 *
1328 * @param block_contents $bc
1329 * @return string
1330 */
1331 protected function block_footer(block_contents $bc) {
1332 $output = '';
d9c8f425 1333 if ($bc->footer) {
26acc814 1334 $output .= html_writer::tag('div', $bc->footer, array('class' => 'footer'));
d9c8f425 1335 }
9f5c39b5
SH
1336 return $output;
1337 }
d9c8f425 1338
9f5c39b5
SH
1339 /**
1340 * Produces the annotation for a block
1341 *
1342 * @param block_contents $bc
1343 * @return string
1344 */
1345 protected function block_annotation(block_contents $bc) {
1346 $output = '';
d9c8f425 1347 if ($bc->annotation) {
26acc814 1348 $output .= html_writer::tag('div', $bc->annotation, array('class' => 'blockannotation'));
d9c8f425 1349 }
d9c8f425 1350 return $output;
1351 }
1352
1353 /**
1354 * Calls the JS require function to hide a block.
7a3c215b 1355 *
d9c8f425 1356 * @param block_contents $bc A block_contents object
d9c8f425 1357 */
dd72b308
PS
1358 protected function init_block_hider_js(block_contents $bc) {
1359 if (!empty($bc->attributes['id']) and $bc->collapsible != block_contents::NOT_HIDEABLE) {
cbb54cce
SH
1360 $config = new stdClass;
1361 $config->id = $bc->attributes['id'];
1362 $config->title = strip_tags($bc->title);
1363 $config->preference = 'block' . $bc->blockinstanceid . 'hidden';
1364 $config->tooltipVisible = get_string('hideblocka', 'access', $config->title);
1365 $config->tooltipHidden = get_string('showblocka', 'access', $config->title);
1366
1367 $this->page->requires->js_init_call('M.util.init_block_hider', array($config));
1368 user_preference_allow_ajax_update($config->preference, PARAM_BOOL);
d9c8f425 1369 }
1370 }
1371
1372 /**
1373 * Render the contents of a block_list.
7a3c215b 1374 *
d9c8f425 1375 * @param array $icons the icon for each item.
1376 * @param array $items the content of each item.
1377 * @return string HTML
1378 */
1379 public function list_block_contents($icons, $items) {
1380 $row = 0;
1381 $lis = array();
1382 foreach ($items as $key => $string) {
5d0c95a5 1383 $item = html_writer::start_tag('li', array('class' => 'r' . $row));
2c5ec833 1384 if (!empty($icons[$key])) { //test if the content has an assigned icon
26acc814 1385 $item .= html_writer::tag('div', $icons[$key], array('class' => 'icon column c0'));
d9c8f425 1386 }
26acc814 1387 $item .= html_writer::tag('div', $string, array('class' => 'column c1'));
5d0c95a5 1388 $item .= html_writer::end_tag('li');
d9c8f425 1389 $lis[] = $item;
1390 $row = 1 - $row; // Flip even/odd.
1391 }
f2a04fc1 1392 return html_writer::tag('ul', implode("\n", $lis), array('class' => 'unlist'));
d9c8f425 1393 }
1394
1395 /**
1396 * Output all the blocks in a particular region.
7a3c215b 1397 *
d9c8f425 1398 * @param string $region the name of a region on this page.
1399 * @return string the HTML to be output.
1400 */
1401 public function blocks_for_region($region) {
1402 $blockcontents = $this->page->blocks->get_content_for_region($region, $this);
6671fa73
JF
1403 $blocks = $this->page->blocks->get_blocks_for_region($region);
1404 $lastblock = null;
1405 $zones = array();
1406 foreach ($blocks as $block) {
1407 $zones[] = $block->title;
1408 }
d9c8f425 1409 $output = '';
6671fa73 1410
d9c8f425 1411 foreach ($blockcontents as $bc) {
1412 if ($bc instanceof block_contents) {
1413 $output .= $this->block($bc, $region);
6671fa73 1414 $lastblock = $bc->title;
d9c8f425 1415 } else if ($bc instanceof block_move_target) {
686e3b3a 1416 $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
d9c8f425 1417 } else {
1418 throw new coding_exception('Unexpected type of thing (' . get_class($bc) . ') found in list of block contents.');
1419 }
1420 }
1421 return $output;
1422 }
1423
1424 /**
1425 * Output a place where the block that is currently being moved can be dropped.
7a3c215b 1426 *
d9c8f425 1427 * @param block_move_target $target with the necessary details.
6671fa73
JF
1428 * @param array $zones array of areas where the block can be moved to
1429 * @param string $previous the block located before the area currently being rendered.
686e3b3a 1430 * @param string $region the name of the region
d9c8f425 1431 * @return string the HTML to be output.
1432 */
686e3b3a 1433 public function block_move_target($target, $zones, $previous, $region) {
0e2ca62e 1434 if ($previous == null) {
686e3b3a
FM
1435 if (empty($zones)) {
1436 // There are no zones, probably because there are no blocks.
1437 $regions = $this->page->theme->get_all_block_regions();
1438 $position = get_string('moveblockinregion', 'block', $regions[$region]);
1439 } else {
1440 $position = get_string('moveblockbefore', 'block', $zones[0]);
1441 }
6671fa73
JF
1442 } else {
1443 $position = get_string('moveblockafter', 'block', $previous);
1444 }
1445 return html_writer::tag('a', html_writer::tag('span', $position, array('class' => 'accesshide')), array('href' => $target->url, 'class' => 'blockmovetarget'));
d9c8f425 1446 }
1447
574fbea4 1448 /**
996b1e0c 1449 * Renders a special html link with attached action
574fbea4 1450 *
2fada290
MG
1451 * Theme developers: DO NOT OVERRIDE! Please override function
1452 * {@link core_renderer::render_action_link()} instead.
1453 *
574fbea4
PS
1454 * @param string|moodle_url $url
1455 * @param string $text HTML fragment
1456 * @param component_action $action
11820bac 1457 * @param array $attributes associative array of html link attributes + disabled
e282c679 1458 * @param pix_icon optional pix icon to render with the link
7a3c215b 1459 * @return string HTML fragment
574fbea4 1460 */
e282c679 1461 public function action_link($url, $text, component_action $action = null, array $attributes = null, $icon = null) {
574fbea4
PS
1462 if (!($url instanceof moodle_url)) {
1463 $url = new moodle_url($url);
1464 }
e282c679 1465 $link = new action_link($url, $text, $action, $attributes, $icon);
574fbea4 1466
f14b641b 1467 return $this->render($link);
574fbea4
PS
1468 }
1469
1470 /**
7a3c215b
SH
1471 * Renders an action_link object.
1472 *
1473 * The provided link is renderer and the HTML returned. At the same time the
f8129210 1474 * associated actions are setup in JS by {@link core_renderer::add_action_handler()}
7a3c215b 1475 *
574fbea4
PS
1476 * @param action_link $link
1477 * @return string HTML fragment
1478 */
1479 protected function render_action_link(action_link $link) {
1480 global $CFG;
1481
e282c679
SH
1482 $text = '';
1483 if ($link->icon) {
1484 $text .= $this->render($link->icon);
1485 }
1486
7749e187 1487 if ($link->text instanceof renderable) {
e282c679 1488 $text .= $this->render($link->text);
7749e187 1489 } else {
e282c679 1490 $text .= $link->text;
7749e187
SH
1491 }
1492
574fbea4
PS
1493 // A disabled link is rendered as formatted text
1494 if (!empty($link->attributes['disabled'])) {
1495 // do not use div here due to nesting restriction in xhtml strict
7749e187 1496 return html_writer::tag('span', $text, array('class'=>'currentlink'));
574fbea4 1497 }
11820bac 1498
574fbea4
PS
1499 $attributes = $link->attributes;
1500 unset($link->attributes['disabled']);
1501 $attributes['href'] = $link->url;
1502
1503 if ($link->actions) {
f14b641b 1504 if (empty($attributes['id'])) {
574fbea4
PS
1505 $id = html_writer::random_id('action_link');
1506 $attributes['id'] = $id;
1507 } else {
1508 $id = $attributes['id'];
1509 }
1510 foreach ($link->actions as $action) {
c80877aa 1511 $this->add_action_handler($action, $id);
574fbea4
PS
1512 }
1513 }
1514
7749e187 1515 return html_writer::tag('a', $text, $attributes);
574fbea4
PS
1516 }
1517
c63923bd
PS
1518
1519 /**
7a3c215b
SH
1520 * Renders an action_icon.
1521 *
f8129210 1522 * This function uses the {@link core_renderer::action_link()} method for the
7a3c215b
SH
1523 * most part. What it does different is prepare the icon as HTML and use it
1524 * as the link text.
c63923bd 1525 *
2fada290
MG
1526 * Theme developers: If you want to change how action links and/or icons are rendered,
1527 * consider overriding function {@link core_renderer::render_action_link()} and
1528 * {@link core_renderer::render_pix_icon()}.
1529 *
c63923bd
PS
1530 * @param string|moodle_url $url A string URL or moodel_url
1531 * @param pix_icon $pixicon
1532 * @param component_action $action
1533 * @param array $attributes associative array of html link attributes + disabled
1534 * @param bool $linktext show title next to image in link
1535 * @return string HTML fragment
1536 */
1537 public function action_icon($url, pix_icon $pixicon, component_action $action = null, array $attributes = null, $linktext=false) {
1538 if (!($url instanceof moodle_url)) {
1539 $url = new moodle_url($url);
1540 }
1541 $attributes = (array)$attributes;
1542
524645e7 1543 if (empty($attributes['class'])) {
c63923bd
PS
1544 // let ppl override the class via $options
1545 $attributes['class'] = 'action-icon';
1546 }
1547
1548 $icon = $this->render($pixicon);
1549
1550 if ($linktext) {
1551 $text = $pixicon->attributes['alt'];
1552 } else {
1553 $text = '';
1554 }
1555
1556 return $this->action_link($url, $text.$icon, $action, $attributes);
1557 }
1558
d9c8f425 1559 /**
0b634d75 1560 * Print a message along with button choices for Continue/Cancel
1561 *
4ed85790 1562 * If a string or moodle_url is given instead of a single_button, method defaults to post.
0b634d75 1563 *
d9c8f425 1564 * @param string $message The question to ask the user
3ba60ee1
PS
1565 * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer. Can also be a moodle_url or string URL
1566 * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer. Can also be a moodle_url or string URL
d9c8f425 1567 * @return string HTML fragment
1568 */
1569 public function confirm($message, $continue, $cancel) {
4871a238 1570 if ($continue instanceof single_button) {
11820bac 1571 // ok
4871a238
PS
1572 } else if (is_string($continue)) {
1573 $continue = new single_button(new moodle_url($continue), get_string('continue'), 'post');
1574 } else if ($continue instanceof moodle_url) {
26eab8d4 1575 $continue = new single_button($continue, get_string('continue'), 'post');
d9c8f425 1576 } else {
4ed85790 1577 throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
d9c8f425 1578 }
1579
4871a238 1580 if ($cancel instanceof single_button) {
11820bac 1581 // ok
4871a238
PS
1582 } else if (is_string($cancel)) {
1583 $cancel = new single_button(new moodle_url($cancel), get_string('cancel'), 'get');
1584 } else if ($cancel instanceof moodle_url) {
26eab8d4 1585 $cancel = new single_button($cancel, get_string('cancel'), 'get');
d9c8f425 1586 } else {
4ed85790 1587 throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
d9c8f425 1588 }
1589
d9c8f425 1590 $output = $this->box_start('generalbox', 'notice');
26acc814
PS
1591 $output .= html_writer::tag('p', $message);
1592 $output .= html_writer::tag('div', $this->render($continue) . $this->render($cancel), array('class' => 'buttons'));
d9c8f425 1593 $output .= $this->box_end();
1594 return $output;
1595 }
1596
3cd5305f 1597 /**
3ba60ee1 1598 * Returns a form with a single button.
3cd5305f 1599 *
2fada290
MG
1600 * Theme developers: DO NOT OVERRIDE! Please override function
1601 * {@link core_renderer::render_single_button()} instead.
1602 *
3ba60ee1 1603 * @param string|moodle_url $url
3cd5305f
PS
1604 * @param string $label button text
1605 * @param string $method get or post submit method
3ba60ee1 1606 * @param array $options associative array {disabled, title, etc.}
3cd5305f
PS
1607 * @return string HTML fragment
1608 */
3ba60ee1 1609 public function single_button($url, $label, $method='post', array $options=null) {
574fbea4
PS
1610 if (!($url instanceof moodle_url)) {
1611 $url = new moodle_url($url);
3ba60ee1 1612 }
574fbea4
PS
1613 $button = new single_button($url, $label, $method);
1614
3ba60ee1
PS
1615 foreach ((array)$options as $key=>$value) {
1616 if (array_key_exists($key, $button)) {
1617 $button->$key = $value;
1618 }
3cd5305f
PS
1619 }
1620
3ba60ee1 1621 return $this->render($button);
3cd5305f
PS
1622 }
1623
d9c8f425 1624 /**
7a3c215b
SH
1625 * Renders a single button widget.
1626 *
1627 * This will return HTML to display a form containing a single button.
1628 *
3ba60ee1 1629 * @param single_button $button
d9c8f425 1630 * @return string HTML fragment
1631 */
3ba60ee1
PS
1632 protected function render_single_button(single_button $button) {
1633 $attributes = array('type' => 'submit',
1634 'value' => $button->label,
db09524d 1635 'disabled' => $button->disabled ? 'disabled' : null,
3ba60ee1
PS
1636 'title' => $button->tooltip);
1637
1638 if ($button->actions) {
1639 $id = html_writer::random_id('single_button');
1640 $attributes['id'] = $id;
1641 foreach ($button->actions as $action) {
c80877aa 1642 $this->add_action_handler($action, $id);
3ba60ee1 1643 }
d9c8f425 1644 }
d9c8f425 1645
3ba60ee1
PS
1646 // first the input element
1647 $output = html_writer::empty_tag('input', $attributes);
d9c8f425 1648
3ba60ee1
PS
1649 // then hidden fields
1650 $params = $button->url->params();
1651 if ($button->method === 'post') {
1652 $params['sesskey'] = sesskey();
1653 }
1654 foreach ($params as $var => $val) {
1655 $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $var, 'value' => $val));
1656 }
d9c8f425 1657
3ba60ee1 1658 // then div wrapper for xhtml strictness
26acc814 1659 $output = html_writer::tag('div', $output);
d9c8f425 1660
3ba60ee1 1661 // now the form itself around it
a12cd69c
DM
1662 if ($button->method === 'get') {
1663 $url = $button->url->out_omit_querystring(true); // url without params, the anchor part allowed
1664 } else {
1665 $url = $button->url->out_omit_querystring(); // url without params, the anchor part not allowed
1666 }
a6855934
PS
1667 if ($url === '') {
1668 $url = '#'; // there has to be always some action
1669 }
3ba60ee1 1670 $attributes = array('method' => $button->method,
a6855934 1671 'action' => $url,
3ba60ee1 1672 'id' => $button->formid);
26acc814 1673 $output = html_writer::tag('form', $output, $attributes);
d9c8f425 1674
3ba60ee1 1675 // and finally one more wrapper with class
26acc814 1676 return html_writer::tag('div', $output, array('class' => $button->class));
d9c8f425 1677 }
1678
a9967cf5 1679 /**
ab08be98 1680 * Returns a form with a single select widget.
7a3c215b 1681 *
2fada290
MG
1682 * Theme developers: DO NOT OVERRIDE! Please override function
1683 * {@link core_renderer::render_single_select()} instead.
1684 *
a9967cf5
PS
1685 * @param moodle_url $url form action target, includes hidden fields
1686 * @param string $name name of selection field - the changing parameter in url
1687 * @param array $options list of options
1688 * @param string $selected selected element
1689 * @param array $nothing
f8dab966 1690 * @param string $formid
a9967cf5
PS
1691 * @return string HTML fragment
1692 */
7a3c215b 1693 public function single_select($url, $name, array $options, $selected = '', $nothing = array('' => 'choosedots'), $formid = null) {
a9967cf5
PS
1694 if (!($url instanceof moodle_url)) {
1695 $url = new moodle_url($url);
1696 }
f8dab966 1697 $select = new single_select($url, $name, $options, $selected, $nothing, $formid);
a9967cf5
PS
1698
1699 return $this->render($select);
1700 }
1701
1702 /**
1703 * Internal implementation of single_select rendering
7a3c215b 1704 *
a9967cf5
PS
1705 * @param single_select $select
1706 * @return string HTML fragment
1707 */
1708 protected function render_single_select(single_select $select) {
1709 $select = clone($select);
1710 if (empty($select->formid)) {
1711 $select->formid = html_writer::random_id('single_select_f');
1712 }
1713
1714 $output = '';
1715 $params = $select->url->params();
1716 if ($select->method === 'post') {
1717 $params['sesskey'] = sesskey();
1718 }
1719 foreach ($params as $name=>$value) {
1720 $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>$name, 'value'=>$value));
1721 }
1722
1723 if (empty($select->attributes['id'])) {
1724 $select->attributes['id'] = html_writer::random_id('single_select');
1725 }
1726
0b2cb132
PS
1727 if ($select->disabled) {
1728 $select->attributes['disabled'] = 'disabled';
1729 }
4d10e579 1730
a9967cf5
PS
1731 if ($select->tooltip) {
1732 $select->attributes['title'] = $select->tooltip;
1733 }
1734
7266bd3e
ARN
1735 $select->attributes['class'] = 'autosubmit';
1736 if ($select->class) {
1737 $select->attributes['class'] .= ' ' . $select->class;
1738 }
1739
a9967cf5 1740 if ($select->label) {
ecc5cc31 1741 $output .= html_writer::label($select->label, $select->attributes['id'], false, $select->labelattributes);
a9967cf5
PS
1742 }
1743
1744 if ($select->helpicon instanceof help_icon) {
1745 $output .= $this->render($select->helpicon);
1746 }
a9967cf5
PS
1747 $output .= html_writer::select($select->options, $select->name, $select->selected, $select->nothing, $select->attributes);
1748
1749 $go = html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('go')));
ebc583e4 1750 $output .= html_writer::tag('noscript', html_writer::tag('div', $go), array('class' => 'inline'));
a9967cf5
PS
1751
1752 $nothing = empty($select->nothing) ? false : key($select->nothing);
7266bd3e
ARN
1753 $this->page->requires->yui_module('moodle-core-formautosubmit',
1754 'M.core.init_formautosubmit',
1755 array(array('selectid' => $select->attributes['id'], 'nothing' => $nothing))
1756 );
a9967cf5
PS
1757
1758 // then div wrapper for xhtml strictness
26acc814 1759 $output = html_writer::tag('div', $output);
a9967cf5
PS
1760
1761 // now the form itself around it
a12cd69c
DM
1762 if ($select->method === 'get') {
1763 $url = $select->url->out_omit_querystring(true); // url without params, the anchor part allowed
1764 } else {
1765 $url = $select->url->out_omit_querystring(); // url without params, the anchor part not allowed
1766 }
a9967cf5 1767 $formattributes = array('method' => $select->method,
a12cd69c 1768 'action' => $url,
a9967cf5 1769 'id' => $select->formid);
26acc814 1770 $output = html_writer::tag('form', $output, $formattributes);
4d10e579
PS
1771
1772 // and finally one more wrapper with class
26acc814 1773 return html_writer::tag('div', $output, array('class' => $select->class));
4d10e579
PS
1774 }
1775
1776 /**
ab08be98 1777 * Returns a form with a url select widget.
7a3c215b 1778 *
2fada290
MG
1779 * Theme developers: DO NOT OVERRIDE! Please override function
1780 * {@link core_renderer::render_url_select()} instead.
1781 *
4d10e579
PS
1782 * @param array $urls list of urls - array('/course/view.php?id=1'=>'Frontpage', ....)
1783 * @param string $selected selected element
1784 * @param array $nothing
1785 * @param string $formid
1786 * @return string HTML fragment
1787 */
7a3c215b 1788 public function url_select(array $urls, $selected, $nothing = array('' => 'choosedots'), $formid = null) {
4d10e579
PS
1789 $select = new url_select($urls, $selected, $nothing, $formid);
1790 return $this->render($select);
1791 }
1792
1793 /**
ab08be98 1794 * Internal implementation of url_select rendering
7a3c215b
SH
1795 *
1796 * @param url_select $select
4d10e579
PS
1797 * @return string HTML fragment
1798 */
1799 protected function render_url_select(url_select $select) {
c422efcf
PS
1800 global $CFG;
1801
4d10e579
PS
1802 $select = clone($select);
1803 if (empty($select->formid)) {
1804 $select->formid = html_writer::random_id('url_select_f');
1805 }
1806
1807 if (empty($select->attributes['id'])) {
1808 $select->attributes['id'] = html_writer::random_id('url_select');
1809 }
1810
1811 if ($select->disabled) {
1812 $select->attributes['disabled'] = 'disabled';
1813 }
1814
1815 if ($select->tooltip) {
1816 $select->attributes['title'] = $select->tooltip;
1817 }
1818
1819 $output = '';
1820
1821 if ($select->label) {
ecc5cc31 1822 $output .= html_writer::label($select->label, $select->attributes['id'], false, $select->labelattributes);
4d10e579
PS
1823 }
1824
50d6ad84
ARN
1825 $classes = array();
1826 if (!$select->showbutton) {
1827 $classes[] = 'autosubmit';
1828 }
7266bd3e 1829 if ($select->class) {
50d6ad84
ARN
1830 $classes[] = $select->class;
1831 }
1832 if (count($classes)) {
1833 $select->attributes['class'] = implode(' ', $classes);
7266bd3e
ARN
1834 }
1835
4d10e579
PS
1836 if ($select->helpicon instanceof help_icon) {
1837 $output .= $this->render($select->helpicon);
1838 }
1839
d4dcfc6b
DM
1840 // For security reasons, the script course/jumpto.php requires URL starting with '/'. To keep
1841 // backward compatibility, we are removing heading $CFG->wwwroot from URLs here.
c422efcf
PS
1842 $urls = array();
1843 foreach ($select->urls as $k=>$v) {
d4dcfc6b
DM
1844 if (is_array($v)) {
1845 // optgroup structure
1846 foreach ($v as $optgrouptitle => $optgroupoptions) {
1847 foreach ($optgroupoptions as $optionurl => $optiontitle) {
1848 if (empty($optionurl)) {
1849 $safeoptionurl = '';
1850 } else if (strpos($optionurl, $CFG->wwwroot.'/') === 0) {
1851 // debugging('URLs passed to url_select should be in local relative form - please fix the code.', DEBUG_DEVELOPER);
1852 $safeoptionurl = str_replace($CFG->wwwroot, '', $optionurl);
1853 } else if (strpos($optionurl, '/') !== 0) {
1854 debugging("Invalid url_select urls parameter inside optgroup: url '$optionurl' is not local relative url!");
1855 continue;
1856 } else {
1857 $safeoptionurl = $optionurl;
1858 }
1859 $urls[$k][$optgrouptitle][$safeoptionurl] = $optiontitle;
1860 }
1861 }
1862 } else {
1863 // plain list structure
1864 if (empty($k)) {
1865 // nothing selected option
1866 } else if (strpos($k, $CFG->wwwroot.'/') === 0) {
1867 $k = str_replace($CFG->wwwroot, '', $k);
1868 } else if (strpos($k, '/') !== 0) {
1869 debugging("Invalid url_select urls parameter: url '$k' is not local relative url!");
1870 continue;
1871 }
1872 $urls[$k] = $v;
1873 }
1874 }
1875 $selected = $select->selected;
1876 if (!empty($selected)) {
1877 if (strpos($select->selected, $CFG->wwwroot.'/') === 0) {
1878 $selected = str_replace($CFG->wwwroot, '', $selected);
1879 } else if (strpos($selected, '/') !== 0) {
1880 debugging("Invalid value of parameter 'selected': url '$selected' is not local relative url!");
c422efcf 1881 }
c422efcf
PS
1882 }
1883
4d10e579 1884 $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
d4dcfc6b 1885 $output .= html_writer::select($urls, 'jump', $selected, $select->nothing, $select->attributes);
4d10e579 1886
15e48a1a
SM
1887 if (!$select->showbutton) {
1888 $go = html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('go')));
ebc583e4 1889 $output .= html_writer::tag('noscript', html_writer::tag('div', $go), array('class' => 'inline'));
15e48a1a 1890 $nothing = empty($select->nothing) ? false : key($select->nothing);
7266bd3e
ARN
1891 $this->page->requires->yui_module('moodle-core-formautosubmit',
1892 'M.core.init_formautosubmit',
1893 array(array('selectid' => $select->attributes['id'], 'nothing' => $nothing))
1894 );
15e48a1a
SM
1895 } else {
1896 $output .= html_writer::empty_tag('input', array('type'=>'submit', 'value'=>$select->showbutton));
1897 }
4d10e579
PS
1898
1899 // then div wrapper for xhtml strictness
26acc814 1900 $output = html_writer::tag('div', $output);
4d10e579
PS
1901
1902 // now the form itself around it
1903 $formattributes = array('method' => 'post',
1904 'action' => new moodle_url('/course/jumpto.php'),
1905 'id' => $select->formid);
26acc814 1906 $output = html_writer::tag('form', $output, $formattributes);
a9967cf5
PS
1907
1908 // and finally one more wrapper with class
26acc814 1909 return html_writer::tag('div', $output, array('class' => $select->class));
a9967cf5
PS
1910 }
1911
d9c8f425 1912 /**
1913 * Returns a string containing a link to the user documentation.
1914 * Also contains an icon by default. Shown to teachers and admin only.
7a3c215b 1915 *
d9c8f425 1916 * @param string $path The page link after doc root and language, no leading slash.
1917 * @param string $text The text to be displayed for the link
afe3566c 1918 * @param boolean $forcepopup Whether to force a popup regardless of the value of $CFG->doctonewwindow
996b1e0c 1919 * @return string
d9c8f425 1920 */
afe3566c 1921 public function doc_link($path, $text = '', $forcepopup = false) {
8ae8bf8a
PS
1922 global $CFG;
1923
c22fbd38 1924 $icon = $this->pix_icon('docs', $text, 'moodle', array('class'=>'iconhelp icon-pre'));
8ae8bf8a 1925
000c278c 1926 $url = new moodle_url(get_docs_url($path));
8ae8bf8a 1927
c80877aa 1928 $attributes = array('href'=>$url);
afe3566c
ARN
1929 if (!empty($CFG->doctonewwindow) || $forcepopup) {
1930 $attributes['class'] = 'helplinkpopup';
d9c8f425 1931 }
1adaa404 1932
26acc814 1933 return html_writer::tag('a', $icon.$text, $attributes);
d9c8f425 1934 }
1935
000c278c 1936 /**
7a3c215b
SH
1937 * Return HTML for a pix_icon.
1938 *
2fada290
MG
1939 * Theme developers: DO NOT OVERRIDE! Please override function
1940 * {@link core_renderer::render_pix_icon()} instead.
1941 *
000c278c
PS
1942 * @param string $pix short pix name
1943 * @param string $alt mandatory alt attribute
eb557002 1944 * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
000c278c
PS
1945 * @param array $attributes htm lattributes
1946 * @return string HTML fragment
1947 */
1948 public function pix_icon($pix, $alt, $component='moodle', array $attributes = null) {
1949 $icon = new pix_icon($pix, $alt, $component, $attributes);
1950 return $this->render($icon);
1951 }
1952
1953 /**
7a3c215b
SH
1954 * Renders a pix_icon widget and returns the HTML to display it.
1955 *
000c278c
PS
1956 * @param pix_icon $icon
1957 * @return string HTML fragment
1958 */
ce0110bf 1959 protected function render_pix_icon(pix_icon $icon) {
000c278c
PS
1960 $attributes = $icon->attributes;
1961 $attributes['src'] = $this->pix_url($icon->pix, $icon->component);
c80877aa 1962 return html_writer::empty_tag('img', $attributes);
000c278c
PS
1963 }
1964
d63c5073 1965 /**
7a3c215b
SH
1966 * Return HTML to display an emoticon icon.
1967 *
d63c5073
DM
1968 * @param pix_emoticon $emoticon
1969 * @return string HTML fragment
1970 */
1971 protected function render_pix_emoticon(pix_emoticon $emoticon) {
1972 $attributes = $emoticon->attributes;
1973 $attributes['src'] = $this->pix_url($emoticon->pix, $emoticon->component);
1974 return html_writer::empty_tag('img', $attributes);
1975 }
1976
a09aeee4 1977 /**
7a3c215b
SH
1978 * Produces the html that represents this rating in the UI
1979 *
1980 * @param rating $rating the page object on which this rating will appear
1981 * @return string
1982 */
a09aeee4 1983 function render_rating(rating $rating) {
7ac928a7 1984 global $CFG, $USER;
a09aeee4 1985
2b04c41c 1986 if ($rating->settings->aggregationmethod == RATING_AGGREGATE_NONE) {
63e87951
AD
1987 return null;//ratings are turned off
1988 }
1989
2b04c41c
SH
1990 $ratingmanager = new rating_manager();
1991 // Initialise the JavaScript so ratings can be done by AJAX.
1992 $ratingmanager->initialise_rating_javascript($this->page);
a09aeee4 1993
63e87951
AD
1994 $strrate = get_string("rate", "rating");
1995 $ratinghtml = ''; //the string we'll return
1996
2b04c41c
SH
1997 // permissions check - can they view the aggregate?
1998 if ($rating->user_can_view_aggregate()) {
a09aeee4 1999
2b04c41c
SH
2000 $aggregatelabel = $ratingmanager->get_aggregate_label($rating->settings->aggregationmethod);
2001 $aggregatestr = $rating->get_aggregate_string();
a09aeee4 2002
6278ce45 2003 $aggregatehtml = html_writer::tag('span', $aggregatestr, array('id' => 'ratingaggregate'.$rating->itemid, 'class' => 'ratingaggregate')).' ';
2b04c41c 2004 if ($rating->count > 0) {
6278ce45 2005 $countstr = "({$rating->count})";
d251b259 2006 } else {
6278ce45 2007 $countstr = '-';
d251b259 2008 }
6278ce45 2009 $aggregatehtml .= html_writer::tag('span', $countstr, array('id'=>"ratingcount{$rating->itemid}", 'class' => 'ratingcount')).' ';
63e87951 2010
c6de9cef 2011 $ratinghtml .= html_writer::tag('span', $aggregatelabel, array('class'=>'rating-aggregate-label'));
d251b259 2012 if ($rating->settings->permissions->viewall && $rating->settings->pluginpermissions->viewall) {
2b04c41c
SH
2013
2014 $nonpopuplink = $rating->get_view_ratings_url();
2015 $popuplink = $rating->get_view_ratings_url(true);
a09aeee4 2016
d251b259 2017 $action = new popup_action('click', $popuplink, 'ratings', array('height' => 400, 'width' => 600));
c6de9cef 2018 $ratinghtml .= $this->action_link($nonpopuplink, $aggregatehtml, $action);
d251b259 2019 } else {
c6de9cef 2020 $ratinghtml .= $aggregatehtml;
a09aeee4 2021 }
d251b259 2022 }
a09aeee4 2023
d251b259 2024 $formstart = null;
2b04c41c
SH
2025 // if the item doesn't belong to the current user, the user has permission to rate
2026 // and we're within the assessable period
2027 if ($rating->user_can_rate()) {
771b3fbe 2028
2b04c41c
SH
2029 $rateurl = $rating->get_rate_url();
2030 $inputs = $rateurl->params();
771b3fbe 2031
2b04c41c
SH
2032 //start the rating form
2033 $formattrs = array(
2034 'id' => "postrating{$rating->itemid}",
2035 'class' => 'postratingform',
2036 'method' => 'post',
2037 'action' => $rateurl->out_omit_querystring()
2038 );
2039 $formstart = html_writer::start_tag('form', $formattrs);
2040 $formstart .= html_writer::start_tag('div', array('class' => 'ratingform'));
2041
2042 // add the hidden inputs
2043 foreach ($inputs as $name => $value) {
2044 $attributes = array('type' => 'hidden', 'class' => 'ratinginput', 'name' => $name, 'value' => $value);
2045 $formstart .= html_writer::empty_tag('input', $attributes);
2046 }
3180bc2c 2047
d251b259
AD
2048 if (empty($ratinghtml)) {
2049 $ratinghtml .= $strrate.': ';
2050 }
d251b259 2051 $ratinghtml = $formstart.$ratinghtml;
63e87951 2052
2b04c41c
SH
2053 $scalearray = array(RATING_UNSET_RATING => $strrate.'...') + $rating->settings->scale->scaleitems;
2054 $scaleattrs = array('class'=>'postratingmenu ratinginput','id'=>'menurating'.$rating->itemid);
ecc5cc31 2055 $ratinghtml .= html_writer::label($rating->rating, 'menurating'.$rating->itemid, false, array('class' => 'accesshide'));
2b04c41c 2056 $ratinghtml .= html_writer::select($scalearray, 'rating', $rating->rating, false, $scaleattrs);
a09aeee4 2057
d251b259 2058 //output submit button
771b3fbe
AD
2059 $ratinghtml .= html_writer::start_tag('span', array('class'=>"ratingsubmit"));
2060
2b04c41c 2061 $attributes = array('type' => 'submit', 'class' => 'postratingmenusubmit', 'id' => 'postratingsubmit'.$rating->itemid, 'value' => s(get_string('rate', 'rating')));
771b3fbe 2062 $ratinghtml .= html_writer::empty_tag('input', $attributes);
a09aeee4 2063
2b04c41c 2064 if (!$rating->settings->scale->isnumeric) {
eaf52ff0
MN
2065 // If a global scale, try to find current course ID from the context
2066 if (empty($rating->settings->scale->courseid) and $coursecontext = $rating->context->get_course_context(false)) {
2067 $courseid = $coursecontext->instanceid;
2068 } else {
2069 $courseid = $rating->settings->scale->courseid;
2070 }
2071 $ratinghtml .= $this->help_icon_scale($courseid, $rating->settings->scale);
a09aeee4 2072 }
771b3fbe
AD
2073 $ratinghtml .= html_writer::end_tag('span');
2074 $ratinghtml .= html_writer::end_tag('div');
2075 $ratinghtml .= html_writer::end_tag('form');
a09aeee4
AD
2076 }
2077
63e87951 2078 return $ratinghtml;
a09aeee4
AD
2079 }
2080
7a3c215b 2081 /**
d9c8f425 2082 * Centered heading with attached help button (same title text)
7a3c215b
SH
2083 * and optional icon attached.
2084 *
4bcc5118 2085 * @param string $text A heading text
53a78cef 2086 * @param string $helpidentifier The keyword that defines a help page
4bcc5118
PS
2087 * @param string $component component name
2088 * @param string|moodle_url $icon
2089 * @param string $iconalt icon alt text
699e2fd0
RW
2090 * @param int $level The level of importance of the heading. Defaulting to 2
2091 * @param string $classnames A space-separated list of CSS classes. Defaulting to null
d9c8f425 2092 * @return string HTML fragment
2093 */
699e2fd0 2094 public function heading_with_help($text, $helpidentifier, $component = 'moodle', $icon = '', $iconalt = '', $level = 2, $classnames = null) {
4bcc5118
PS
2095 $image = '';
2096 if ($icon) {
8ef1aa40 2097 $image = $this->pix_icon($icon, $iconalt, $component, array('class'=>'icon iconlarge'));
d9c8f425 2098 }
4bcc5118 2099
259c165d
PS
2100 $help = '';
2101 if ($helpidentifier) {
2102 $help = $this->help_icon($helpidentifier, $component);
2103 }
4bcc5118 2104
699e2fd0 2105 return $this->heading($image.$text.$help, $level, $classnames);
d9c8f425 2106 }
2107
2108 /**
7a3c215b 2109 * Returns HTML to display a help icon.
d9c8f425 2110 *
cb616be8 2111 * @deprecated since Moodle 2.0
bf11293a 2112 */
596509e4 2113 public function old_help_icon($helpidentifier, $title, $component = 'moodle', $linktext = '') {
a6d81a73 2114 throw new coding_exception('old_help_icon() can not be used any more, please see help_icon().');
d9c8f425 2115 }
2116
259c165d 2117 /**
7a3c215b 2118 * Returns HTML to display a help icon.
259c165d 2119 *
2fada290
MG
2120 * Theme developers: DO NOT OVERRIDE! Please override function
2121 * {@link core_renderer::render_help_icon()} instead.
2122 *
259c165d
PS
2123 * @param string $identifier The keyword that defines a help page
2124 * @param string $component component name
2125 * @param string|bool $linktext true means use $title as link text, string means link text value
2126 * @return string HTML fragment
2127 */
2128 public function help_icon($identifier, $component = 'moodle', $linktext = '') {
2cf81209 2129 $icon = new help_icon($identifier, $component);
259c165d
PS
2130 $icon->diag_strings();
2131 if ($linktext === true) {
2132 $icon->linktext = get_string($icon->identifier, $icon->component);
2133 } else if (!empty($linktext)) {
2134 $icon->linktext = $linktext;
2135 }
2136 return $this->render($icon);
2137 }
2138
2139 /**
2140 * Implementation of user image rendering.
7a3c215b 2141 *
3d3fae72 2142 * @param help_icon $helpicon A help icon instance
259c165d
PS
2143 * @return string HTML fragment
2144 */
2145 protected function render_help_icon(help_icon $helpicon) {
2146 global $CFG;
2147
2148 // first get the help image icon
2149 $src = $this->pix_url('help');
2150
2151 $title = get_string($helpicon->identifier, $helpicon->component);
2152
2153 if (empty($helpicon->linktext)) {
cab2c7ea 2154 $alt = get_string('helpprefix2', '', trim($title, ". \t"));
259c165d
PS
2155 } else {
2156 $alt = get_string('helpwiththis');
2157 }
2158
2159 $attributes = array('src'=>$src, 'alt'=>$alt, 'class'=>'iconhelp');
2160 $output = html_writer::empty_tag('img', $attributes);
2161
2162 // add the link text if given
2163 if (!empty($helpicon->linktext)) {
2164 // the spacing has to be done through CSS
2165 $output .= $helpicon->linktext;
2166 }
2167
69542fb3
PS
2168 // now create the link around it - we need https on loginhttps pages
2169 $url = new moodle_url($CFG->httpswwwroot.'/help.php', array('component' => $helpicon->component, 'identifier' => $helpicon->identifier, 'lang'=>current_language()));
259c165d
PS
2170
2171 // note: this title is displayed only if JS is disabled, otherwise the link will have the new ajax tooltip
2172 $title = get_string('helpprefix2', '', trim($title, ". \t"));
2173
e88419a2 2174 $attributes = array('href' => $url, 'title' => $title, 'aria-haspopup' => 'true', 'target'=>'_blank');
259c165d
PS
2175 $output = html_writer::tag('a', $output, $attributes);
2176
2177 // and finally span
238b8bc9 2178 return html_writer::tag('span', $output, array('class' => 'helptooltip'));
259c165d
PS
2179 }
2180
d9c8f425 2181 /**
7a3c215b 2182 * Returns HTML to display a scale help icon.
d9c8f425 2183 *
4bcc5118 2184 * @param int $courseid
7a3c215b
SH
2185 * @param stdClass $scale instance
2186 * @return string HTML fragment
d9c8f425 2187 */
4bcc5118
PS
2188 public function help_icon_scale($courseid, stdClass $scale) {
2189 global $CFG;
02f64f97 2190
4bcc5118 2191 $title = get_string('helpprefix2', '', $scale->name) .' ('.get_string('newwindow').')';
02f64f97 2192
0029a917 2193 $icon = $this->pix_icon('help', get_string('scales'), 'moodle', array('class'=>'iconhelp'));
02f64f97 2194
68bf577b
AD
2195 $scaleid = abs($scale->id);
2196
2197 $link = new moodle_url('/course/scales.php', array('id' => $courseid, 'list' => true, 'scaleid' => $scaleid));
230ec401 2198 $action = new popup_action('click', $link, 'ratingscale');
02f64f97 2199
26acc814 2200 return html_writer::tag('span', $this->action_link($link, $icon, $action), array('class' => 'helplink'));
d9c8f425 2201 }
2202
2203 /**
2204 * Creates and returns a spacer image with optional line break.
2205 *
3d3fae72
SH
2206 * @param array $attributes Any HTML attributes to add to the spaced.
2207 * @param bool $br Include a BR after the spacer.... DON'T USE THIS. Don't be
2208 * laxy do it with CSS which is a much better solution.
d9c8f425 2209 * @return string HTML fragment
2210 */
0029a917
PS
2211 public function spacer(array $attributes = null, $br = false) {
2212 $attributes = (array)$attributes;
2213 if (empty($attributes['width'])) {
2214 $attributes['width'] = 1;
1ba862ec 2215 }
e1a5a9cc 2216 if (empty($attributes['height'])) {
0029a917 2217 $attributes['height'] = 1;
d9c8f425 2218 }
0029a917 2219 $attributes['class'] = 'spacer';
d9c8f425 2220
0029a917 2221 $output = $this->pix_icon('spacer', '', 'moodle', $attributes);
b65bfc3e 2222
0029a917 2223 if (!empty($br)) {
1ba862ec
PS
2224 $output .= '<br />';
2225 }
d9c8f425 2226
2227 return $output;
2228 }
2229
d9c8f425 2230 /**
7a3c215b 2231 * Returns HTML to display the specified user's avatar.
d9c8f425 2232 *
5d0c95a5 2233 * User avatar may be obtained in two ways:
d9c8f425 2234 * <pre>
812dbaf7
PS
2235 * // Option 1: (shortcut for simple cases, preferred way)
2236 * // $user has come from the DB and has fields id, picture, imagealt, firstname and lastname
2237 * $OUTPUT->user_picture($user, array('popup'=>true));
2238 *
5d0c95a5
PS
2239 * // Option 2:
2240 * $userpic = new user_picture($user);
d9c8f425 2241 * // Set properties of $userpic
812dbaf7 2242 * $userpic->popup = true;
5d0c95a5 2243 * $OUTPUT->render($userpic);
d9c8f425 2244 * </pre>
2245 *
2fada290
MG
2246 * Theme developers: DO NOT OVERRIDE! Please override function
2247 * {@link core_renderer::render_user_picture()} instead.
2248 *
7a3c215b 2249 * @param stdClass $user Object with at least fields id, picture, imagealt, firstname, lastname
812dbaf7 2250 * If any of these are missing, the database is queried. Avoid this
d9c8f425 2251 * if at all possible, particularly for reports. It is very bad for performance.
812dbaf7
PS
2252 * @param array $options associative array with user picture options, used only if not a user_picture object,
2253 * options are:
2254 * - courseid=$this->page->course->id (course id of user profile in link)
2255 * - size=35 (size of image)
2256 * - link=true (make image clickable - the link leads to user profile)
2257 * - popup=false (open in popup)
2258 * - alttext=true (add image alt attribute)
5d0c95a5 2259 * - class = image class attribute (default 'userpicture')
d9c8f425 2260 * @return string HTML fragment
2261 */
5d0c95a5
PS
2262 public function user_picture(stdClass $user, array $options = null) {
2263 $userpicture = new user_picture($user);
2264 foreach ((array)$options as $key=>$value) {
2265 if (array_key_exists($key, $userpicture)) {
2266 $userpicture->$key = $value;
2267 }
2268 }
2269 return $this->render($userpicture);
2270 }
2271
2272 /**
2273 * Internal implementation of user image rendering.
7a3c215b 2274 *
5d0c95a5
PS
2275 * @param user_picture $userpicture
2276 * @return string
2277 */
2278 protected function render_user_picture(user_picture $userpicture) {
2279 global $CFG, $DB;
812dbaf7 2280
5d0c95a5
PS
2281 $user = $userpicture->user;
2282
2283 if ($userpicture->alttext) {
2284 if (!empty($user->imagealt)) {
2285 $alt = $user->imagealt;
2286 } else {
2287 $alt = get_string('pictureof', '', fullname($user));
2288 }
d9c8f425 2289 } else {
97c10099 2290 $alt = '';
5d0c95a5
PS
2291 }
2292
2293 if (empty($userpicture->size)) {
5d0c95a5
PS
2294 $size = 35;
2295 } else if ($userpicture->size === true or $userpicture->size == 1) {
5d0c95a5 2296 $size = 100;
5d0c95a5 2297 } else {
5d0c95a5 2298 $size = $userpicture->size;
d9c8f425 2299 }
2300
5d0c95a5 2301 $class = $userpicture->class;
d9c8f425 2302
4d254790 2303 if ($user->picture == 0) {
5d0c95a5 2304 $class .= ' defaultuserpic';
5d0c95a5 2305 }
d9c8f425 2306
871a3ec5
SH
2307 $src = $userpicture->get_url($this->page, $this);
2308
29cf6631 2309 $attributes = array('src'=>$src, 'alt'=>$alt, 'title'=>$alt, 'class'=>$class, 'width'=>$size, 'height'=>$size);
5d0c95a5
PS
2310
2311 // get the image html output fisrt
0e35ba6f 2312 $output = html_writer::empty_tag('img', $attributes);
5d0c95a5
PS
2313
2314 // then wrap it in link if needed
2315 if (!$userpicture->link) {
2316 return $output;
d9c8f425 2317 }
2318
5d0c95a5
PS
2319 if (empty($userpicture->courseid)) {
2320 $courseid = $this->page->course->id;
2321 } else {
2322 $courseid = $userpicture->courseid;
2323 }
2324
03d9401e
MD
2325 if ($courseid == SITEID) {
2326 $url = new moodle_url('/user/profile.php', array('id' => $user->id));
2327 } else {
2328 $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $courseid));
2329 }
5d0c95a5
PS
2330
2331 $attributes = array('href'=>$url);
2332
2333 if ($userpicture->popup) {
2334 $id = html_writer::random_id('userpicture');
2335 $attributes['id'] = $id;
c80877aa 2336 $this->add_action_handler(new popup_action('click', $url), $id);
5d0c95a5
PS
2337 }
2338
26acc814 2339 return html_writer::tag('a', $output, $attributes);
d9c8f425 2340 }
b80ef420 2341
b80ef420
DC
2342 /**
2343 * Internal implementation of file tree viewer items rendering.
7a3c215b 2344 *
b80ef420
DC
2345 * @param array $dir
2346 * @return string
2347 */
2348 public function htmllize_file_tree($dir) {
2349 if (empty($dir['subdirs']) and empty($dir['files'])) {
2350 return '';
2351 }
2352 $result = '<ul>';
2353 foreach ($dir['subdirs'] as $subdir) {
2354 $result .= '<li>'.s($subdir['dirname']).' '.$this->htmllize_file_tree($subdir).'</li>';
2355 }
2356 foreach ($dir['files'] as $file) {
2357 $filename = $file->get_filename();
2358 $result .= '<li><span>'.html_writer::link($file->fileurl, $filename).'</span></li>';
2359 }
2360 $result .= '</ul>';
2361
2362 return $result;
2363 }
7a3c215b 2364
bb496de7 2365 /**
7a3c215b 2366 * Returns HTML to display the file picker
bb496de7
DC
2367 *
2368 * <pre>
2369 * $OUTPUT->file_picker($options);
2370 * </pre>
2371 *
2fada290
MG
2372 * Theme developers: DO NOT OVERRIDE! Please override function
2373 * {@link core_renderer::render_file_picker()} instead.
2374 *
bb496de7
DC
2375 * @param array $options associative array with file manager options
2376 * options are:
2377 * maxbytes=>-1,
2378 * itemid=>0,
2379 * client_id=>uniqid(),
2380 * acepted_types=>'*',
2381 * return_types=>FILE_INTERNAL,
2382 * context=>$PAGE->context
2383 * @return string HTML fragment
2384 */
2385 public function file_picker($options) {
2386 $fp = new file_picker($options);
2387 return $this->render($fp);
2388 }
7a3c215b 2389
b80ef420
DC
2390 /**
2391 * Internal implementation of file picker rendering.
7a3c215b 2392 *
b80ef420
DC
2393 * @param file_picker $fp
2394 * @return string
2395 */
bb496de7
DC
2396 public function render_file_picker(file_picker $fp) {
2397 global $CFG, $OUTPUT, $USER;
2398 $options = $fp->options;
2399 $client_id = $options->client_id;
2400 $strsaved = get_string('filesaved', 'repository');
2401 $straddfile = get_string('openpicker', 'repository');
2402 $strloading = get_string('loading', 'repository');
adce0230 2403 $strdndenabled = get_string('dndenabled_inbox', 'moodle');
906e7d89 2404 $strdroptoupload = get_string('droptoupload', 'moodle');
bb496de7
DC
2405 $icon_progress = $OUTPUT->pix_icon('i/loading_small', $strloading).'';
2406
2407 $currentfile = $options->currentfile;
2408 if (empty($currentfile)) {
322945e9
FM
2409 $currentfile = '';
2410 } else {
2411 $currentfile .= ' - ';
bb496de7 2412 }
b817205b
DC
2413 if ($options->maxbytes) {
2414 $size = $options->maxbytes;
2415 } else {
2416 $size = get_max_upload_file_size();
2417 }
513aed3c 2418 if ($size == -1) {
831399c4 2419 $maxsize = '';
513aed3c
DC
2420 } else {
2421 $maxsize = get_string('maxfilesize', 'moodle', display_size($size));
2422 }
f50a61fb 2423 if ($options->buttonname) {
4b72f9eb
AW
2424 $buttonname = ' name="' . $options->buttonname . '"';
2425 } else {
2426 $buttonname = '';
2427 }
bb496de7
DC
2428 $html = <<<EOD
2429<div class="filemanager-loading mdl-align" id='filepicker-loading-{$client_id}'>
2430$icon_progress
2431</div>
2432<div id="filepicker-wrapper-{$client_id}" class="mdl-left" style="display:none">
2433 <div>
c81f3328 2434 <input type="button" class="fp-btn-choose" id="filepicker-button-{$client_id}" value="{$straddfile}"{$buttonname}/>
fa7f2a45 2435 <span> $maxsize </span>
bb496de7
DC
2436 </div>
2437EOD;
2438 if ($options->env != 'url') {
2439 $html .= <<<EOD
50597880 2440 <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist" style="position: relative">
a9352f1f 2441 <div class="filepicker-filename">
08a6a19d 2442 <div class="filepicker-container">$currentfile<div class="dndupload-message">$strdndenabled <br/><div class="dndupload-arrow"></div></div></div>
0f94289c 2443 <div class="dndupload-progressbars"></div>
a9352f1f 2444 </div>
08a6a19d 2445 <div><div class="dndupload-target">{$strdroptoupload}<br/><div class="dndupload-arrow"></div></div></div>
f08fac7c 2446 </div>
bb496de7
DC
2447EOD;
2448 }
2449 $html .= '</div>';
2450 return $html;
2451 }
d9c8f425 2452
2453 /**
7a3c215b 2454 * Returns HTML to display the 'Update this Modulename' button that appears on module pages.
d9c8f425 2455 *
2456 * @param string $cmid the course_module id.
2457 * @param string $modulename the module name, eg. "forum", "quiz" or "workshop"
2458 * @return string the HTML for the button, if this user has permission to edit it, else an empty string.
2459 */
2460 public function update_module_button($cmid, $modulename) {
2461 global $CFG;
b0c6dc1c 2462 if (has_capability('moodle/course:manageactivities', context_module::instance($cmid))) {
d9c8f425 2463 $modulename = get_string('modulename', $modulename);
2464 $string = get_string('updatethis', '', $modulename);
3ba60ee1
PS
2465 $url = new moodle_url("$CFG->wwwroot/course/mod.php", array('update' => $cmid, 'return' => true, 'sesskey' => sesskey()));
2466 return $this->single_button($url, $string);
d9c8f425 2467 } else {
2468 return '';
2469 }
2470 }
2471
2472 /**
7a3c215b
SH
2473 * Returns HTML to display a "Turn editing on/off" button in a form.
2474 *
d9c8f425 2475 * @param moodle_url $url The URL + params to send through when clicking the button
2476 * @return string HTML the button
2477 */
2478 public function edit_button(moodle_url $url) {
3362dfdc
EL
2479
2480 $url->param('sesskey', sesskey());
2481 if ($this->page->user_is_editing()) {
2482 $url->param('edit', 'off');
2483 $editstring = get_string('turneditingoff');
d9c8f425 2484 } else {
3362dfdc
EL
2485 $url->param('edit', 'on');
2486 $editstring = get_string('turneditingon');
d9c8f425 2487 }
2488
3362dfdc 2489 return $this->single_button($url, $editstring);
d9c8f425 2490 }
2491
d9c8f425 2492 /**
7a3c215b 2493 * Returns HTML to display a simple button to close a window
d9c8f425 2494 *
d9c8f425 2495 * @param string $text The lang string for the button's label (already output from get_string())
3ba60ee1 2496 * @return string html fragment
d9c8f425 2497 */
7a5c78e0 2498 public function close_window_button($text='') {
d9c8f425 2499 if (empty($text)) {
2500 $text = get_string('closewindow');
2501 }
a6855934
PS
2502 $button = new single_button(new moodle_url('#'), $text, 'get');
2503 $button->add_action(new component_action('click', 'close_window'));
3ba60ee1
PS
2504
2505 return $this->container($this->render($button), 'closewindow');
d9c8f425 2506 }
2507
d9c8f425 2508 /**
2509 * Output an error message. By default wraps the error message in <span class="error">.
2510 * If the error message is blank, nothing is output.
7a3c215b 2511 *
d9c8f425 2512 * @param string $message the error message.
2513 * @return string the HTML to output.
2514 */
2515 public function error_text($message) {
2516 if (empty($message)) {
2517 return '';
2518 }
3246648b 2519 $message = $this->pix_icon('i/warning', get_string('error'), '', array('class' => 'icon icon-pre', 'title'=>'')) . $message;
26acc814 2520 return html_writer::tag('span', $message, array('class' => 'error'));
d9c8f425 2521 }
2522
2523 /**
2524 * Do not call this function directly.
2525 *
f8129210 2526 * To terminate the current script with a fatal error, call the {@link print_error}
d9c8f425 2527 * function, or throw an exception. Doing either of those things will then call this
2528 * function to display the error, before terminating the execution.
2529 *
2530 * @param string $message The message to output
2531 * @param string $moreinfourl URL where more info can be found about the error
2532 * @param string $link Link for the Continue button
2533 * @param array $backtrace The execution backtrace
2534 * @param string $debuginfo Debugging information
d9c8f425 2535 * @return string the HTML to output.
2536 */
83267ec0 2537 public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null) {
6bd8d7e7 2538 global $CFG;
d9c8f425 2539
2540 $output = '';
6f8f4d83 2541 $obbuffer = '';
e57c283d 2542
d9c8f425 2543 if ($this->has_started()) {
50764d37
PS
2544 // we can not always recover properly here, we have problems with output buffering,
2545 // html tables, etc.
d9c8f425 2546 $output .= $this->opencontainers->pop_all_but_last();
50764d37 2547
d9c8f425 2548 } else {
50764d37
PS
2549 // It is really bad if library code throws exception when output buffering is on,
2550 // because the buffered text would be printed before our start of page.
2551 // NOTE: this hack might be behave unexpectedly in case output buffering is enabled in PHP.ini
6bd8d7e7 2552 error_reporting(0); // disable notices from gzip compression, etc.
50764d37 2553 while (ob_get_level() > 0) {
2cadd443
PS
2554 $buff = ob_get_clean();
2555 if ($buff === false) {
2556 break;
2557 }
2558 $obbuffer .= $buff;
50764d37 2559 }
6bd8d7e7 2560 error_reporting($CFG->debug);
6f8f4d83 2561
f22f1caf
PS
2562 // Output not yet started.
2563 $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
2564 if (empty($_SERVER['HTTP_RANGE'])) {
2565 @header($protocol . ' 404 Not Found');
2566 } else {
2567 // Must stop byteserving attempts somehow,
2568 // this is weird but Chrome PDF viewer can be stopped only with 407!
2569 @header($protocol . ' 407 Proxy Authentication Required');
85309744 2570 }
f22f1caf 2571
eb5bdb35 2572 $this->page->set_context(null); // ugly hack - make sure page context is set to something, we do not want bogus warnings here
7fde1e4b 2573 $this->page->set_url('/'); // no url
191b267b 2574 //$this->page->set_pagelayout('base'); //TODO: MDL-20676 blocks on error pages are weird, unfortunately it somehow detect the pagelayout from URL :-(
dcfb9b78 2575 $this->page->set_title(get_string('error'));
8093188f 2576 $this->page->set_heading($this->page->course->fullname);
d9c8f425 2577 $output .= $this->header();
2578 }
2579
2580 $message = '<p class="errormessage">' . $message . '</p>'.
2581 '<p class="errorcode"><a href="' . $moreinfourl . '">' .
2582 get_string('moreinformation') . '</a></p>';
1ad8143a
PS
2583 if (empty($CFG->rolesactive)) {
2584 $message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
2585 //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.
2586 }
4c2892c6 2587 $output .= $this->box($message, 'errorbox', null, array('data-rel' => 'fatalerror'));
d9c8f425 2588
96f81ea3 2589 if ($CFG->debugdeveloper) {
6f8f4d83 2590 if (!empty($debuginfo)) {
c5d18164
PS
2591 $debuginfo = s($debuginfo); // removes all nasty JS
2592 $debuginfo = str_replace("\n", '<br />', $debuginfo); // keep newlines
2593 $output .= $this->notification('<strong>Debug info:</strong> '.$debuginfo, 'notifytiny');
6f8f4d83
PS
2594 }
2595 if (!empty($backtrace)) {
2596 $output .= $this->notification('<strong>Stack trace:</strong> '.format_backtrace($backtrace), 'notifytiny');
2597 }
2598 if ($obbuffer !== '' ) {
2599 $output .= $this->notification('<strong>Output buffer:</strong> '.s($obbuffer), 'notifytiny');
2600 }
d9c8f425 2601 }
2602
3efe6bbb
PS
2603 if (empty($CFG->rolesactive)) {
2604 // continue does not make much sense if moodle is not installed yet because error is most probably not recoverable
2605 } else if (!empty($link)) {
d9c8f425 2606 $output .= $this->continue_button($link);
2607 }
2608
2609 $output .= $this->footer();
2610
2611 // Padding to encourage IE to display our error page, rather than its own.
2612 $output .= str_repeat(' ', 512);
2613
2614 return $output;
2615 }
2616
2617 /**
2618 * Output a notification (that is, a status message about something that has
2619 * just happened).
2620 *
2621 * @param string $message the message to print out
2622 * @param string $classes normally 'notifyproblem' or 'notifysuccess'.
2623 * @return string the HTML to output.
2624 */
2625 public function notification($message, $classes = 'notifyproblem') {
26acc814 2626 return html_writer::tag('div', clean_text($message), array('class' => renderer_base::prepare_classes($classes)));
d9c8f425 2627 }
2628
2629 /**
7a3c215b 2630 * Returns HTML to display a continue button that goes to a particular URL.
d9c8f425 2631 *
3ba60ee1 2632 * @param string|moodle_url $url The url the button goes to.
d9c8f425 2633 * @return string the HTML to output.
2634 */
3ba60ee1
PS
2635 public function continue_button($url) {
2636 if (!($url instanceof moodle_url)) {
2637 $url = new moodle_url($url);
d9c8f425 2638 }
3ba60ee1
PS
2639 $button = new single_button($url, get_string('continue'), 'get');
2640 $button->class = 'continuebutton';
d9c8f425 2641
3ba60ee1 2642 return $this->render($button);
d9c8f425 2643 }
2644
2645 /**
7a3c215b 2646 * Returns HTML to display a single paging bar to provide access to other pages (usually in a search)
d9c8f425 2647 *
2fada290
MG
2648 * Theme developers: DO NOT OVERRIDE! Please override function
2649 * {@link core_renderer::render_paging_bar()} instead.
2650 *
71c03ac1 2651 * @param int $totalcount The total number of entries available to be paged through
929d7a83
PS
2652 * @param int $page The page you are currently viewing
2653 * @param int $perpage The number of entries that should be shown per page
2654 * @param string|moodle_url $baseurl url of the current page, the $pagevar parameter is added
2655 * @param string $pagevar name of page parameter that holds the page number
d9c8f425 2656 * @return string the HTML to output.
2657 */
929d7a83
PS
2658 public function paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar = 'page') {
2659 $pb = new paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar);
2660 return $this->render($pb);
2661 }
2662
2663 /**
2664 * Internal implementation of paging bar rendering.
7a3c215b 2665 *
929d7a83
PS
2666 * @param paging_bar $pagingbar
2667 * @return string
2668 */
2669 protected function render_paging_bar(paging_bar $pagingbar) {
d9c8f425 2670 $output = '';
2671 $pagingbar = clone($pagingbar);
34059565 2672 $pagingbar->prepare($this, $this->page, $this->target);
d9c8f425 2673
2674 if ($pagingbar->totalcount > $pagingbar->perpage) {
2675 $output .= get_string('page') . ':';
2676
2677 if (!empty($pagingbar->previouslink)) {
56ddb719 2678 $output .= '&#160;(' . $pagingbar->previouslink . ')&#160;';
d9c8f425 2679 }
2680
2681 if (!empty($pagingbar->firstlink)) {
56ddb719 2682 $output .= '&#160;' . $pagingbar->firstlink . '&#160;...';
d9c8f425 2683 }
2684
2685 foreach ($pagingbar->pagelinks as $link) {
56ddb719 2686 $output .= "&#160;&#160;$link";
d9c8f425 2687 }
2688
2689 if (!empty($pagingbar->lastlink)) {
56ddb719 2690 $output .= '&#160;...' . $pagingbar->lastlink . '&#160;';
d9c8f425 2691 }
2692
2693 if (!empty($pagingbar->nextlink)) {
56ddb719 2694 $output .= '&#160;&#160;(' . $pagingbar->nextlink . ')';
d9c8f425 2695 }
2696 }
2697
26acc814 2698 return html_writer::tag('div', $output, array('class' => 'paging'));
d9c8f425 2699 }
2700
d9c8f425 2701 /**
2702 * Output the place a skip link goes to.
7a3c215b 2703 *
d9c8f425 2704 * @param string $id The target name from the corresponding $PAGE->requires->skip_link_to($target) call.
2705 * @return string the HTML to output.
2706 */
fe213365 2707 public function skip_link_target($id = null) {
26acc814 2708 return html_writer::tag('span', '', array('id' => $id));
d9c8f425 2709 }
2710
2711 /**
2712 * Outputs a heading
7a3c215b 2713 *
d9c8f425 2714 * @param string $text The text of the heading
2715 * @param int $level The level of importance of the heading. Defaulting to 2
699e2fd0 2716 * @param string $classes A space-separated list of CSS classes. Defaulting to null
d9c8f425 2717 * @param string $id An optional ID
2718 * @return string the HTML to output.
2719 */
699e2fd0 2720 public function heading($text, $level = 2, $classes = null, $id = null) {
d9c8f425 2721 $level = (integer) $level;
2722 if ($level < 1 or $level > 6) {
2723 throw new coding_exception('Heading level must be an integer between 1 and 6.');
2724 }
26acc814 2725 return html_writer::tag('h' . $level, $text, array('id' => $id, 'class' => renderer_base::prepare_classes($classes)));
d9c8f425 2726 }
2727
2728 /**
2729 * Outputs a box.
7a3c215b 2730 *
d9c8f425 2731 * @param string $contents The contents of the box
2732 * @param string $classes A space-separated list of CSS classes
2733 * @param string $id An optional ID
3e76c7fa 2734 * @param array $attributes An array of other attributes to give the box.
d9c8f425 2735 * @return string the HTML to output.
2736 */
3e76c7fa
SH
2737 public function box($contents, $classes = 'generalbox', $id = null, $attributes = array()) {
2738 return $this->box_start($classes, $id, $attributes) . $contents . $this->box_end();
d9c8f425 2739 }
2740
2741 /**
3d3fae72 2742 * Outputs the opening section of a box.
7a3c215b 2743 *
d9c8f425 2744 * @param string $classes A space-separated list of CSS classes
2745 * @param string $id An optional ID
3e76c7fa 2746 * @param array $attributes An array of other attributes to give the box.
d9c8f425 2747 * @return string the HTML to output.
2748 */
3e76c7fa 2749 public function box_start($classes = 'generalbox', $id = null, $attributes = array()) {
5d0c95a5 2750 $this->opencontainers->push('box', html_writer::end_tag('div'));
3e76c7fa
SH
2751 $attributes['id'] = $id;
2752 $attributes['class'] = 'box ' . renderer_base::prepare_classes($classes);
2753 return html_writer::start_tag('div', $attributes);
d9c8f425 2754 }
2755
2756 /**
2757 * Outputs the closing section of a box.
7a3c215b 2758 *
d9c8f425 2759 * @return string the HTML to output.
2760 */
2761 public function box_end() {
2762 return $this->opencontainers->pop('box');
2763 }
2764
2765 /**
2766 * Outputs a container.
7a3c215b 2767 *
d9c8f425 2768 * @param string $contents The contents of the box
2769 * @param string $classes A space-separated list of CSS classes
2770 * @param string $id An optional ID
2771 * @return string the HTML to output.
2772 */
fe213365 2773 public function container($contents, $classes = null, $id = null) {
d9c8f425 2774 return $this->container_start($classes, $id) . $contents . $this->container_end();
2775 }
2776
2777 /**
2778 * Outputs the opening section of a container.
7a3c215b 2779 *
d9c8f425 2780 * @param string $classes A space-separated list of CSS classes
2781 * @param string $id An optional ID
2782 * @return string the HTML to output.
2783 */
fe213365 2784 public function container_start($classes = null, $id = null) {
5d0c95a5
PS
2785 $this->opencontainers->push('container', html_writer::end_tag('div'));
2786 return html_writer::start_tag('div', array('id' => $id,
78946b9b 2787 'class' => renderer_base::prepare_classes($classes)));
d9c8f425 2788 }
2789
2790 /**
2791 * Outputs the closing section of a container.
7a3c215b 2792 *
d9c8f425 2793 * @return string the HTML to output.
2794 */
2795 public function container_end() {
2796 return $this->opencontainers->pop('container');
2797 }
7d2a0492 2798
3406acde 2799 /**
7d2a0492 2800 * Make nested HTML lists out of the items
2801 *
2802 * The resulting list will look something like this:
2803 *
2804 * <pre>
2805 * <<ul>>
2806 * <<li>><div class='tree_item parent'>(item contents)</div>
2807 * <<ul>
2808 * <<li>><div class='tree_item'>(item contents)</div><</li>>
2809 * <</ul>>
2810 * <</li>>
2811 * <</ul>>
2812 * </pre>
2813 *
7a3c215b
SH
2814 * @param array $items
2815 * @param array $attrs html attributes passed to the top ofs the list
7d2a0492 2816 * @return string HTML
2817 */
7a3c215b 2818 public function tree_block_contents($items, $attrs = array()) {
7d2a0492 2819 // exit if empty, we don't want an empty ul element
2820 if (empty($items)) {
2821 return '';
2822 }
2823 // array of nested li elements
2824 $lis = array();
2825 foreach ($items as $item) {
2826 // this applies to the li item which contains all child lists too
2827 $content = $item->content($this);
2828 $liclasses = array($item->get_css_type());
3406acde 2829 if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count()==0 && $item->nodetype==navigation_node::NODETYPE_BRANCH)) {
7d2a0492 2830 $liclasses[] = 'collapsed';
2831 }
2832 if ($item->isactive === true) {
2833 $liclasses[] = 'current_branch';
2834 }
2835 $liattr = array('class'=>join(' ',$liclasses));
2836 // class attribute on the div item which only contains the item content
2837 $divclasses = array('tree_item');
3406acde 2838 if ($item->children->count()>0 || $item->nodetype==navigation_node::NODETYPE_BRANCH) {
7d2a0492 2839 $divclasses[] = 'branch';
2840 } else {
2841 $divclasses[] = 'leaf';
2842 }
2843 if (!empty($item->classes) && count($item->classes)>0) {
2844 $divclasses[] = join(' ', $item->classes);
2845 }
2846 $divattr = array('class'=>join(' ', $divclasses));
2847 if (!empty($item->id)) {
2848 $divattr['id'] = $item->id;
2849 }
26acc814 2850 $content = html_writer::tag('p', $content, $divattr) . $this->tree_block_contents($item->children);
7d2a0492 2851 if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
26acc814 2852 $content = html_writer::empty_tag('hr') . $content;
7d2a0492 2853 }
26acc814 2854 $content = html_writer::tag('li', $content, $liattr);
7d2a0492 2855 $lis[] = $content;
2856 }
26acc814 2857 return html_writer::tag('ul', implode("\n", $lis), $attrs);
7d2a0492 2858 }
2859
2860 /**
2861 * Return the navbar content so that it can be echoed out by the layout
7a3c215b 2862 *
7d2a0492 2863 * @return string XHTML navbar
2864 */
2865 public function navbar() {
3406acde 2866 $items = $this->page->navbar->get_items();
06a72e01
SH
2867 $itemcount = count($items);
2868 if ($itemcount === 0) {
2869 return '';
2870 }
3406acde 2871
3406acde
SH
2872 $htmlblocks = array();
2873 // Iterate the navarray and display each node
ffca6f4b
SH
2874 $separator = get_separator();
2875 for ($i=0;$i < $itemcount;$i++) {
2876 $item = $items[$i];
493a48f3 2877 $item->hideicon = true;
ffca6f4b
SH
2878 if ($i===0) {
2879 $content = html_writer::tag('li', $this->render($item));
2880 } else {
2881 $content = html_writer::tag('li', $separator.$this->render($item));
2882 }
2883 $htmlblocks[] = $content;
3406acde
SH
2884 }
2885
dcfb9b78
RW
2886 //accessibility: heading for navbar list (MDL-20446)
2887 $navbarcontent = html_writer::tag('span', get_string('pagepath'), array('class'=>'accesshide'));
27e30578 2888 $navbarcontent .= html_writer::tag('ul', join('', $htmlblocks), array('role'=>'navigation'));
3406acde 2889 // XHTML
dcfb9b78 2890 return $navbarcontent;
3406acde
SH
2891 }
2892
7a3c215b
SH
2893 /**
2894 * Renders a navigation node object.
2895 *
2896 * @param navigation_node $item The navigation node to render.
2897 * @return string HTML fragment
2898 */
3406acde
SH
2899 protected function render_navigation_node(navigation_node $item) {
2900 $content = $item->get_content();
2901 $title = $item->get_title();
493a48f3 2902 if ($item->icon instanceof renderable && !$item->hideicon) {
3406acde 2903 $icon = $this->render($item->icon);
48fa9484 2904 $content = $icon.$content; // use CSS for spacing of icons
3406acde
SH
2905 }
2906 if ($item->helpbutton !== null) {
32561caf 2907 $content = trim($item->helpbutton).html_writer::tag('span', $content, array('class'=>'clearhelpbutton', 'tabindex'=>'0'));
3406acde
SH
2908 }
2909 if ($content === '') {
b4c458a3 2910 return '';
3406acde
SH
2911 }
2912 if ($item->action instanceof action_link) {
3406acde
SH
2913 $link = $item->action;
2914 if ($item->hidden) {
2915 $link->add_class('dimmed');
2916 }
0064f3cf
SH
2917 if (!empty($content)) {
2918 // Providing there is content we will use that for the link content.
2919 $link->text = $content;
2920 }
62594358 2921 $content = $this->render($link);
3406acde
SH
2922 } else if ($item->action instanceof moodle_url) {
2923 $attributes = array();
2924 if ($title !== '') {
2925 $attributes['title'] = $title;
2926 }
2927 if ($item->hidden) {
2928 $attributes['class'] = 'dimmed_text';
2929 }
2930 $content = html_writer::link($item->action, $content, $attributes);
2931
2932 } else if (is_string($item->action) || empty($item->action)) {
32561caf 2933 $attributes = array('tabindex'=>'0'); //add tab support to span but still maintain character stream sequence.
3406acde
SH
2934 if ($title !== '') {
2935 $attributes['title'] = $title;
2936 }
2937 if ($item->hidden) {
2938 $attributes['class'] = 'dimmed_text';
2939 }
2940 $content = html_writer::tag('span', $content, $attributes);
2941 }
2942 return $content;
7d2a0492 2943 }
92e01ab7
PS
2944
2945 /**
2946 * Accessibility: Right arrow-like character is
2947 * used in the breadcrumb trail, course navigation menu
2948 * (previous/next activity), calendar, and search forum block.
2949 * If the theme does not set characters, appropriate defaults
2950 * are set automatically. Please DO NOT
2951 * use &lt; &gt; &raquo; - these are confusing for blind users.
7a3c215b 2952 *
92e01ab7
PS
2953 * @return string
2954 */
2955 public function rarrow() {
2956 return $this->page->theme->rarrow;
2957 }
2958
2959 /**
2960 * Accessibility: Right arrow-like character is
2961 * used in the breadcrumb trail, course navigation menu
2962 * (previous/next activity), calendar, and search forum block.
2963 * If the theme does not set characters, appropriate defaults
2964 * are set automatically. Please DO NOT
2965 * use &lt; &gt; &raquo; - these are confusing for blind users.
7a3c215b 2966 *
92e01ab7
PS
2967 * @return string
2968 */
2969 public function larrow() {
2970 return $this->page->theme->larrow;
2971 }
088ccb43 2972
d2dbd0c0
SH
2973 /**
2974 * Returns the custom menu if one has been set
2975 *
71c03ac1 2976 * A custom menu can be configured by browsing to
d2dbd0c0
SH
2977 * Settings: Administration > Appearance > Themes > Theme settings
2978 * and then configuring the custommenu config setting as described.
4d2ee4c2 2979 *
2fada290
MG
2980 * Theme developers: DO NOT OVERRIDE! Please override function
2981 * {@link core_renderer::render_custom_menu()} instead.
2982 *
0f6d9349 2983 * @param string $custommenuitems - custom menuitems set by theme instead of global theme settings
d2dbd0c0
SH
2984 * @return string
2985 */
0f6d9349 2986 public function custom_menu($custommenuitems = '') {
12cc75ae 2987 global $CFG;
0f6d9349
DM
2988 if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
2989 $custommenuitems = $CFG->custommenuitems;
2990 }
2991 if (empty($custommenuitems)) {
12cc75ae
SH
2992 return '';
2993 }
0f6d9349 2994 $custommenu = new custom_menu($custommenuitems, current_language());
2fada290 2995 return $this->render($custommenu);
d2dbd0c0
SH
2996 }
2997
2998 /**
2999 * Renders a custom menu object (located in outputcomponents.php)
3000 *
3001 * The custom menu this method produces makes use of the YUI3 menunav widget
3002 * and requires very specific html elements and classes.
3003 *
3004 * @staticvar int $menucount
3005 * @param custom_menu $menu
3006 * @return string
3007 */
3008 protected function render_custom_menu(custom_menu $menu) {
3009 static $menucount = 0;
3010 // If the menu has no children return an empty string
3011 if (!$menu->has_children()) {
3012 return '';
3013 }
3014 // Increment the menu count. This is used for ID's that get worked with
3015 // in JavaScript as is essential
3016 $menucount++;
6c95e46a
SH
3017 // Initialise this custom menu (the custom menu object is contained in javascript-static
3018 $jscode = js_writer::function_call_with_Y('M.core_custom_menu.init', array('custom_menu_'.$menucount));
3019 $jscode = "(function(){{$jscode}})";
3020 $this->page->requires->yui_module('node-menunav', $jscode);
d2dbd0c0 3021 // Build the root nodes as required by YUI
06a72e01 3022 $content = html_writer::start_tag('div', array('id'=>'custom_menu_'.$menucount, 'class'=>'yui3-menu yui3-menu-horizontal javascript-disabled custom-menu'));
d2dbd0c0
SH
3023 $content .= html_writer::start_tag('div', array('class'=>'yui3-menu-content'));
3024 $content .= html_writer::start_tag('ul');
3025 // Render each child
3026 foreach ($menu->get_children() as $item) {
3027 $content .= $this->render_custom_menu_item($item);
3028 }
3029 // Close the open tags
3030 $content .= html_writer::end_tag('ul');
3031 $content .= html_writer::end_tag('div');
3032 $content .= html_writer::end_tag('div');
3033 // Return the custom menu
3034 return $content;
3035 }
3036
3037 /**
3038 * Renders a custom menu node as part of a submenu
3039 *
3040 * The custom menu this method produces makes use of the YUI3 menunav widget
3041 * and requires very specific html elements and classes.
3042 *
7a3c215b 3043 * @see core:renderer::render_custom_menu()
d2dbd0c0
SH
3044 *
3045 * @staticvar int $submenucount
3046 * @param custom_menu_item $menunode
3047 * @return string
3048 */
3049 protected function render_custom_menu_item(custom_menu_item $menunode) {
3050 // Required to ensure we get unique trackable id's
3051 static $submenucount = 0;
3052 if ($menunode->has_children()) {
3053 // If the child has menus render it as a sub menu
3054 $submenucount++;
3055 $content = html_writer::start_tag('li');
3056 if ($menunode->get_url() !== null) {
3057 $url = $menunode->get_url();
3058 } else {
3059 $url = '#cm_submenu_'.$submenucount;
3060 }
3061 $content .= html_writer::link($url, $menunode->get_text(), array('class'=>'yui3-menu-label', 'title'=>$menunode->get_title()));