MDL-55074 theme_boost: Navigation and blocks
[moodle.git] / theme / boost / classes / output / core_renderer.php
CommitLineData
536f0460
DW
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
2ede86d7 17namespace theme_boost\output;
536f0460 18
da4d9eb7 19use coding_exception;
536f0460
DW
20use html_writer;
21use tabobject;
22use tabtree;
23use custom_menu_item;
24use custom_menu;
d7fbf722 25use block_contents;
99061152
DW
26use navigation_node;
27use action_link;
d7fbf722 28use stdClass;
f130c411 29use moodle_url;
3f0544b8
DW
30use preferences_groups;
31use action_menu;
b7e95263 32use help_icon;
d7c65752 33use single_button;
bf7f35e9 34use single_select;
b0da86e0 35use paging_bar;
f1b34660 36use url_select;
b71c82ad 37use context_course;
132093f0 38use pix_icon;
536f0460
DW
39
40defined('MOODLE_INTERNAL') || die;
41
42/**
43 * Renderers to align Moodle's HTML with that expected by Bootstrap
44 *
2ede86d7 45 * @package theme_boost
536f0460
DW
46 * @copyright 2012 Bas Brands, www.basbrands.nl
47 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48 */
49
50class core_renderer extends \core_renderer {
51
52 /** @var custom_menu_item language The language menu if created */
53 protected $language = null;
54
7fcfc073
FM
55 /**
56 * Outputs the opening section of a box.
57 *
58 * @param string $classes A space-separated list of CSS classes
59 * @param string $id An optional ID
60 * @param array $attributes An array of other attributes to give the box.
61 * @return string the HTML to output.
62 */
63 public function box_start($classes = 'generalbox', $id = null, $attributes = array()) {
63e4df60
DW
64 if (is_array($classes)) {
65 $classes = implode(' ', $classes);
66 }
7fcfc073
FM
67 return parent::box_start($classes . ' p-a-1', $id, $attributes);
68 }
69
1ecc3daf
FM
70 /**
71 * Wrapper for header elements.
72 *
73 * @return string HTML to display the main header.
74 */
75 public function full_header() {
76 $html = html_writer::start_tag('header', array('id' => 'page-header', 'class' => 'row'));
77 $html .= html_writer::start_div('col-xs-12 p-t-1 p-b-1');
99061152 78 $html .= html_writer::div($this->context_header_settings_menu(), 'pull-xs-right context-header-settings-menu');
1ecc3daf
FM
79 $html .= $this->context_header();
80 $html .= html_writer::start_div('clearfix', array('id' => 'page-navbar'));
81 $html .= html_writer::tag('div', $this->navbar(), array('class' => 'breadcrumb-nav'));
82 $html .= html_writer::div($this->page_heading_button(), 'breadcrumb-button');
83 $html .= html_writer::end_div();
84 $html .= html_writer::tag('div', $this->course_header(), array('id' => 'course-header'));
85 $html .= html_writer::end_div();
86 $html .= html_writer::end_tag('header');
87 return $html;
88 }
89
536f0460
DW
90 /**
91 * The standard tags that should be included in the <head> tag
92 * including a meta description for the front page
93 *
94 * @return string HTML fragment.
95 */
96 public function standard_head_html() {
97 global $SITE, $PAGE;
98
99 $output = parent::standard_head_html();
100 if ($PAGE->pagelayout == 'frontpage') {
101 $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML)));
102 if (!empty($summary)) {
103 $output .= "<meta name=\"description\" content=\"$summary\" />\n";
104 }
105 }
106
107 return $output;
108 }
109
110 /*
111 * This renders the navbar.
112 * Uses bootstrap compatible html.
113 */
114 public function navbar() {
13d07a01 115 return $this->render_from_template('core/navbar', $this->page->navbar);
536f0460
DW
116 }
117
b71c82ad
FM
118 /**
119 * Override to inject the logo.
120 *
121 * @param array $headerinfo The header info.
122 * @param int $headinglevel What level the 'h' tag will be.
123 * @return string HTML for the header bar.
124 */
125 public function context_header($headerinfo = null, $headinglevel = 1) {
126 global $SITE;
127
128 if ($this->should_display_main_logo($headinglevel)) {
129 $sitename = format_string($SITE->fullname, true, array('context' => context_course::instance(SITEID)));
130 return html_writer::div(html_writer::empty_tag('img', [
131 'src' => $this->get_logo_url(null, 75), 'alt' => $sitename]), 'logo');
132 }
133
134 return parent::context_header($headerinfo, $headinglevel);
135 }
136
137 /**
138 * Get the compact logo URL.
139 *
140 * @return string
141 */
142 public function get_compact_logo_url($maxwidth = 100, $maxheight = 100) {
137c289a 143 return parent::get_compact_logo_url(null, 35);
b71c82ad
FM
144 }
145
146 /**
147 * Whether we should display the main logo.
148 *
149 * @return bool
150 */
151 public function should_display_main_logo($headinglevel = 1) {
152 global $PAGE;
153
154 // Only render the logo if we're on the front page or login page and the we have a logo.
155 $logo = $this->get_logo_url();
156 if ($headinglevel == 1 && !empty($logo)) {
157 if ($PAGE->pagelayout == 'frontpage' || $PAGE->pagelayout == 'login') {
158 return true;
159 }
160 }
161
162 return false;
163 }
164 /**
165 * Whether we should display the logo in the navbar.
166 *
167 * We will when there are no main logos, and we have compact logo.
168 *
169 * @return bool
170 */
171 public function should_display_navbar_logo() {
172 $logo = $this->get_compact_logo_url();
173 return !empty($logo) && !$this->should_display_main_logo();
174 }
175
536f0460
DW
176 /*
177 * Overriding the custom_menu function ensures the custom menu is
178 * always shown, even if no menu items are configured in the global
179 * theme settings page.
180 */
181 public function custom_menu($custommenuitems = '') {
182 global $CFG;
183
184 if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
185 $custommenuitems = $CFG->custommenuitems;
186 }
187 $custommenu = new custom_menu($custommenuitems, current_language());
188 return $this->render_custom_menu($custommenu);
189 }
190
191 /*
192 * This renders the bootstrap top menu.
193 *
194 * This renderer is needed to enable the Bootstrap style navigation.
195 */
196 protected function render_custom_menu(custom_menu $menu) {
197 global $CFG;
198
199 $langs = get_string_manager()->get_list_of_translations();
200 $haslangmenu = $this->lang_menu() != '';
201
202 if (!$menu->has_children() && !$haslangmenu) {
203 return '';
204 }
205
206 if ($haslangmenu) {
f130c411 207 $strlang = get_string('language');
536f0460
DW
208 $currentlang = current_language();
209 if (isset($langs[$currentlang])) {
210 $currentlang = $langs[$currentlang];
211 } else {
212 $currentlang = $strlang;
213 }
214 $this->language = $menu->add($currentlang, new moodle_url('#'), $strlang, 10000);
215 foreach ($langs as $langtype => $langname) {
216 $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
217 }
218 }
219
f130c411 220 $content = '';
536f0460 221 foreach ($menu->get_children() as $item) {
f130c411
DW
222 $context = $item->export_for_template($this);
223 $content .= $this->render_from_template('core/custom_menu_item', $context);
536f0460
DW
224 }
225
536f0460
DW
226 return $content;
227 }
228
229 /**
230 * This code renders the navbar button to control the display of the custom menu
231 * on smaller screens.
232 *
233 * Do not display the button if the menu is empty.
234 *
235 * @return string HTML fragment
236 */
237 public function navbar_button() {
238 global $CFG;
239
240 if (empty($CFG->custommenuitems) && $this->lang_menu() == '') {
241 return '';
242 }
243
244 $iconbar = html_writer::tag('span', '', array('class' => 'icon-bar'));
245 $button = html_writer::tag('a', $iconbar . "\n" . $iconbar. "\n" . $iconbar, array(
246 'class' => 'btn btn-navbar',
247 'data-toggle' => 'collapse',
248 'data-target' => '.nav-collapse'
249 ));
250 return $button;
251 }
252
253 /**
254 * Renders tabtree
255 *
256 * @param tabtree $tabtree
257 * @return string
258 */
259 protected function render_tabtree(tabtree $tabtree) {
260 if (empty($tabtree->subtree)) {
261 return '';
262 }
da4d9eb7
FM
263 $data = $tabtree->export_for_template($this);
264 return $this->render_from_template('core/tabtree', $data);
536f0460
DW
265 }
266
267 /**
268 * Renders tabobject (part of tabtree)
269 *
270 * This function is called from {@link core_renderer::render_tabtree()}
271 * and also it calls itself when printing the $tabobject subtree recursively.
272 *
273 * @param tabobject $tabobject
274 * @return string HTML fragment
275 */
276 protected function render_tabobject(tabobject $tab) {
da4d9eb7 277 throw new coding_exception('Tab objects should not be directly rendered.');
536f0460 278 }
d7fbf722
DW
279
280 /**
281 * Prints a nice side block with an optional header.
282 *
283 * @param block_contents $bc HTML for the content
284 * @param string $region the region the block is appearing in.
285 * @return string the HTML to be output.
286 */
287 public function block(block_contents $bc, $region) {
288 $bc = clone($bc); // Avoid messing up the object passed in.
289 if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) {
290 $bc->collapsible = block_contents::NOT_HIDEABLE;
291 }
d7fbf722
DW
292
293 $id = !empty($bc->attributes['id']) ? $bc->attributes['id'] : uniqid('block-');
294 $context = new stdClass();
295 $context->skipid = $bc->skipid;
296 $context->blockinstanceid = $bc->blockinstanceid;
297 $context->dockable = $bc->dockable;
298 $context->id = $id;
299 $context->hidden = $bc->collapsible == block_contents::HIDDEN;
300 $context->skiptitle = strip_tags($bc->title);
301 $context->showskiplink = !empty($context->skiptitle);
302 $context->arialabel = $bc->arialabel;
303 $context->ariarole = !empty($bc->attributes['role']) ? $bc->attributes['role'] : 'complementary';
304 $context->type = $bc->attributes['data-block'];
305 $context->title = $bc->title;
306 $context->content = $bc->content;
307 $context->annotation = $bc->annotation;
308 $context->footer = $bc->footer;
309 $context->hascontrols = !empty($bc->controls);
310 if ($context->hascontrols) {
311 $context->controls = $this->block_controls($bc->controls, $id);
312 }
313
314 return $this->render_from_template('core/block', $context);
315 }
316
317 /**
318 * Returns the CSS classes to apply to the body tag.
319 *
320 * @since Moodle 2.5.1 2.6
321 * @param array $additionalclasses Any additional classes to apply.
322 * @return string
323 */
324 public function body_css_classes(array $additionalclasses = array()) {
99061152 325 return $this->page->bodyclasses . ' ' . implode(' ', $additionalclasses);
d7fbf722 326 }
3f0544b8
DW
327
328 /**
329 * Renders preferences groups.
330 *
331 * @param preferences_groups $renderable The renderable
332 * @return string The output.
333 */
334 public function render_preferences_groups(preferences_groups $renderable) {
335 return $this->render_from_template('core/preferences_groups', $renderable);
336 }
337
3f0544b8
DW
338 /**
339 * Renders an action menu component.
340 *
341 * @param action_menu $menu
342 * @return string HTML
343 */
344 public function render_action_menu(action_menu $menu) {
132093f0
FM
345
346 // We don't want the class icon there!
347 foreach ($menu->get_secondary_actions() as $action) {
348 if ($action instanceof \action_menu_link && $action->has_class('icon')) {
349 $action->attributes['class'] = preg_replace('/(^|\s+)icon(\s+|$)/i', '', $action->attributes['class']);
350 }
351 }
352
99061152
DW
353 if ($menu->is_empty()) {
354 return '';
355 }
132093f0
FM
356 $context = $menu->export_for_template($this);
357
358 // We do not want the icon with the caret, the caret is added by Bootstrap.
359 if (empty($context->primary->menutrigger)) {
360 $newurl = $this->pix_url('t/edit', 'moodle');
361 $context->primary->icon['attributes'] = array_reduce($context->primary->icon['attributes'],
362 function($carry, $item) use ($newurl) {
363 if ($item['name'] === 'src') {
364 $item['value'] = $newurl->out(false);
365 }
366 $carry[] = $item;
367 return $carry;
368 }, []
369 );
370 }
371
372 return $this->render_from_template('core/action_menu', $context);
3f0544b8 373 }
ac4a389e 374
b7e95263
DW
375 /**
376 * Implementation of user image rendering.
377 *
378 * @param help_icon $helpicon A help icon instance
379 * @return string HTML fragment
380 */
381 protected function render_help_icon(help_icon $helpicon) {
382 $context = $helpicon->export_for_template($this);
383 return $this->render_from_template('core/help_icon', $context);
384 }
bf7f35e9 385
d7c65752
FM
386 /**
387 * Renders a single button widget.
388 *
389 * This will return HTML to display a form containing a single button.
390 *
391 * @param single_button $button
392 * @return string HTML fragment
393 */
394 protected function render_single_button(single_button $button) {
395 return $this->render_from_template('core/single_button', $button->export_for_template($this));
396 }
397
bf7f35e9
FM
398 /**
399 * Renders a single select.
400 *
401 * @param single_select $select The object.
402 * @return string HTML
403 */
404 protected function render_single_select(single_select $select) {
405 return $this->render_from_template('core/single_select', $select->export_for_template($this));
406 }
407
b0da86e0
FM
408 /**
409 * Renders a paging bar.
410 *
411 * @param paging_bar $pagingbar The object.
412 * @return string HTML
413 */
414 protected function render_paging_bar(paging_bar $pagingbar) {
8a47abcd
DW
415 // Any more than 10 is not usable and causes wierd wrapping of the pagination in this theme.
416 $pagingbar->maxdisplay = 10;
b0da86e0
FM
417 return $this->render_from_template('core/paging_bar', $pagingbar->export_for_template($this));
418 }
419
f1b34660
FM
420 /**
421 * Renders a url select.
422 *
423 * @param url_select $select The object.
424 * @return string HTML
425 */
426 protected function render_url_select(url_select $select) {
427 return $this->render_from_template('core/url_select', $select->export_for_template($this));
428 }
63e4df60
DW
429
430 /**
431 * Renders a pix_icon widget and returns the HTML to display it.
432 *
433 * @param pix_icon $icon
434 * @return string HTML fragment
435 */
436 protected function render_pix_icon(pix_icon $icon) {
437 $data = $icon->export_for_template($this);
438 foreach ($data['attributes'] as $key => $item) {
439 $name = $item['name'];
440 $value = $item['value'];
441 if ($name == 'class') {
442 $data['extraclasses'] = $value;
443 unset($data['attributes'][$key]);
444 $data['attributes'] = array_values($data['attributes']);
445 break;
446 }
447 }
448 return $this->render_from_template('core/pix_icon', $data);
449 }
99061152
DW
450
451 /**
452 * Renders the login form.
453 *
454 * @param \core_auth\output\login $form The renderable.
455 * @return string
456 */
457 public function render_login(\core_auth\output\login $form) {
458 global $SITE;
459
460 $context = $form->export_for_template($this);
461
462 // Override because rendering is not supported in template yet.
463 $context->cookieshelpiconformatted = $this->help_icon('cookiesenabled');
464 $context->errorformatted = $this->error_text($context->error);
465 $url = $this->get_logo_url();
466 if ($url) {
467 $url = $url->out(false);
468 }
469 $context->logourl = $url;
470 $context->sitename = format_string($SITE->fullname, true, array('context' => context_course::instance(SITEID)));
471
472 return $this->render_from_template('core/login', $context);
473 }
474
475 /**
476 * Render the login signup form into a nice template for the theme.
477 *
478 * @param mform $form
479 * @return string
480 */
481 public function render_login_signup_form($form) {
482 global $SITE;
483
484 $context = $form->export_for_template($this);
485 $url = $this->get_logo_url();
486 if ($url) {
487 $url = $url->out(false);
488 }
489 $context['logourl'] = $url;
490 $context['sitename'] = format_string($SITE->fullname, true, array('context' => context_course::instance(SITEID)));
491
492 return $this->render_from_template('core/signup_form_layout', $context);
493 }
494
495 /**
496 * This is an optional menu that can be added to a layout by a theme. It contains the
497 * menu for the course administration, only on the course main page.
498 *
499 * @return string
500 */
501 public function context_header_settings_menu() {
502 $context = $this->page->context;
503 $menu = new action_menu();
504 if ($context->contextlevel == CONTEXT_COURSE) {
505 // Get the course admin node from the settings navigation.
506 $items = $this->page->navbar->get_items();
507 $node = end($items);
508 if (($node->type == navigation_node::TYPE_COURSE)) {
509 $node = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
510 if ($node) {
511 // Build an action menu based on the visible nodes from this navigation tree.
512 $this->build_action_menu_from_navigation($menu, $node, false, true);
513
514 $text = get_string('courseadministration');
515 $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
516 $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
517 $menu->add_secondary_action($link);
518 }
519 }
520 } else if ($context->contextlevel == CONTEXT_USER) {
521 $items = $this->page->navbar->get_items();
522 $node = end($items);
523 if ($node->key === 'myprofile') {
524 // Get the course admin node from the settings navigation.
525 $node = $this->page->settingsnav->find('useraccount', navigation_node::TYPE_CONTAINER);
526 if ($node) {
527 // Build an action menu based on the visible nodes from this navigation tree.
528 $this->build_action_menu_from_navigation($menu, $node);
529 }
530 }
531 }
532 return $this->render($menu);
533 }
534
535 /**
536 * This is an optional menu that can be added to a layout by a theme. It contains the
537 * menu for the most specific thing from the settings block. E.g. Module administration.
538 *
539 * @return string
540 */
541 public function region_main_settings_menu() {
542 $context = $this->page->context;
543 $menu = new action_menu();
544
545 if ($context->contextlevel == CONTEXT_MODULE) {
546
547 $node = $this->page->navigation->find_active_node();
548 if (($node->type == navigation_node::TYPE_ACTIVITY ||
549 $node->type == navigation_node::TYPE_RESOURCE)) {
550 // Get the course admin node from the settings navigation.
551 $node = $this->page->settingsnav->find('modulesettings', navigation_node::TYPE_SETTING);
552 if ($node) {
553 // Build an action menu based on the visible nodes from this navigation tree.
554 $this->build_action_menu_from_navigation($menu, $node);
555 }
556 }
557 }
558 return $this->render($menu);
559 }
560
561 /**
562 * Take a node in the nav tree and make an action menu out of it.
563 * The links are injected in the action menu.
564 *
565 * @param action_menu $menu
566 * @param navigation_node $node
567 * @param boolean $indent
568 * @param boolean $onlytopleafnodes
569 */
570 private function build_action_menu_from_navigation(action_menu $menu,
571 navigation_node $node,
572 $indent = false,
573 $onlytopleafnodes = false) {
574 // Build an action menu based on the visible nodes from this navigation tree.
575 foreach ($node->children as $menuitem) {
576 if ($menuitem->display) {
577 if ($onlytopleafnodes && $menuitem->children->count()) {
578 continue;
579 }
580 if ($menuitem->action) {
581 $text = $menuitem->text;
582 $link = new action_link($menuitem->action, $menuitem->text, null, null, $menuitem->icon);
583 if ($indent) {
584 $link->add_class('m-l-1');
585 }
586 } else {
587 if ($onlytopleafnodes) {
588 continue;
589 }
590 $link = $menuitem->text;
591 }
592 $menu->add_secondary_action($link);
593 $this->build_action_menu_from_navigation($menu, $menuitem, true);
594 }
595 }
596 }
597
536f0460 598}