MDL-55074 theme_boost: Navigation and blocks
[moodle.git] / theme / boost / classes / output / core_renderer.php
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/>.
17 namespace theme_boost\output;
19 use coding_exception;
20 use html_writer;
21 use tabobject;
22 use tabtree;
23 use custom_menu_item;
24 use custom_menu;
25 use block_contents;
26 use navigation_node;
27 use action_link;
28 use stdClass;
29 use moodle_url;
30 use preferences_groups;
31 use action_menu;
32 use help_icon;
33 use single_button;
34 use single_select;
35 use paging_bar;
36 use url_select;
37 use context_course;
38 use pix_icon;
40 defined('MOODLE_INTERNAL') || die;
42 /**
43  * Renderers to align Moodle's HTML with that expected by Bootstrap
44  *
45  * @package    theme_boost
46  * @copyright  2012 Bas Brands, www.basbrands.nl
47  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48  */
50 class core_renderer extends \core_renderer {
52     /** @var custom_menu_item language The language menu if created */
53     protected $language = null;
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()) {
64         if (is_array($classes)) {
65             $classes = implode(' ', $classes);
66         }
67         return parent::box_start($classes . ' p-a-1', $id, $attributes);
68     }
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');
78         $html .= html_writer::div($this->context_header_settings_menu(), 'pull-xs-right context-header-settings-menu');
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     }
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;
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         }
107         return $output;
108     }
110     /*
111      * This renders the navbar.
112      * Uses bootstrap compatible html.
113      */
114     public function navbar() {
115         return $this->render_from_template('core/navbar', $this->page->navbar);
116     }
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;
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         }
134         return parent::context_header($headerinfo, $headinglevel);
135     }
137     /**
138      * Get the compact logo URL.
139      *
140      * @return string
141      */
142     public function get_compact_logo_url($maxwidth = 100, $maxheight = 100) {
143         return parent::get_compact_logo_url(null, 35);
144     }
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;
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         }
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     }
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;
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     }
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;
199         $langs = get_string_manager()->get_list_of_translations();
200         $haslangmenu = $this->lang_menu() != '';
202         if (!$menu->has_children() && !$haslangmenu) {
203             return '';
204         }
206         if ($haslangmenu) {
207             $strlang = get_string('language');
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         }
220         $content = '';
221         foreach ($menu->get_children() as $item) {
222             $context = $item->export_for_template($this);
223             $content .= $this->render_from_template('core/custom_menu_item', $context);
224         }
226         return $content;
227     }
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;
240         if (empty($CFG->custommenuitems) && $this->lang_menu() == '') {
241             return '';
242         }
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     }
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         }
263         $data = $tabtree->export_for_template($this);
264         return $this->render_from_template('core/tabtree', $data);
265     }
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) {
277         throw new coding_exception('Tab objects should not be directly rendered.');
278     }
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         }
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         }
314         return $this->render_from_template('core/block', $context);
315     }
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()) {
325         return $this->page->bodyclasses . ' ' . implode(' ', $additionalclasses);
326     }
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     }
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) {
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         }
353         if ($menu->is_empty()) {
354             return '';
355         }
356         $context = $menu->export_for_template($this);
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         }
372         return $this->render_from_template('core/action_menu', $context);
373     }
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     }
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     }
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     }
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) {
415         // Any more than 10 is not usable and causes wierd wrapping of the pagination in this theme.
416         $pagingbar->maxdisplay = 10;
417         return $this->render_from_template('core/paging_bar', $pagingbar->export_for_template($this));
418     }
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     }
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     }
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;
460         $context = $form->export_for_template($this);
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)));
472         return $this->render_from_template('core/login', $context);
473     }
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;
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)));
492         return $this->render_from_template('core/signup_form_layout', $context);
493     }
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);
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     }
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();
545         if ($context->contextlevel == CONTEXT_MODULE) {
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     }
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     }