. namespace theme_boost\output; use coding_exception; use html_writer; use tabobject; use tabtree; use custom_menu_item; use custom_menu; use block_contents; use navigation_node; use action_link; use stdClass; use moodle_url; use preferences_groups; use action_menu; use help_icon; use single_button; use paging_bar; use context_course; use pix_icon; defined('MOODLE_INTERNAL') || die; /** * Renderers to align Moodle's HTML with that expected by Bootstrap * * @package theme_boost * @copyright 2012 Bas Brands, www.basbrands.nl * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_renderer extends \core_renderer { /** @var custom_menu_item language The language menu if created */ protected $language = null; /** * Outputs the opening section of a box. * * @param string $classes A space-separated list of CSS classes * @param string $id An optional ID * @param array $attributes An array of other attributes to give the box. * @return string the HTML to output. */ public function box_start($classes = 'generalbox', $id = null, $attributes = array()) { if (is_array($classes)) { $classes = implode(' ', $classes); } return parent::box_start($classes . ' p-y-1', $id, $attributes); } /** * Wrapper for header elements. * * @return string HTML to display the main header. */ public function full_header() { global $PAGE; $html = html_writer::start_tag('header', array('id' => 'page-header', 'class' => 'row')); $html .= html_writer::start_div('col-xs-12 p-a-1'); $html .= html_writer::start_div('card'); $html .= html_writer::start_div('card-block'); $html .= html_writer::div($this->context_header_settings_menu(), 'pull-xs-right context-header-settings-menu'); $html .= html_writer::start_div('pull-xs-left'); $html .= $this->context_header(); $html .= html_writer::end_div(); $pageheadingbutton = $this->page_heading_button(); if (empty($PAGE->layout_options['nonavbar'])) { $html .= html_writer::start_div('clearfix w-100 pull-xs-left', array('id' => 'page-navbar')); $html .= html_writer::tag('div', $this->navbar(), array('class' => 'breadcrumb-nav')); $html .= html_writer::div($pageheadingbutton, 'breadcrumb-button pull-xs-right'); $html .= html_writer::end_div(); } else if ($pageheadingbutton) { $html .= html_writer::div($pageheadingbutton, 'breadcrumb-button nonavbar pull-xs-right'); } $html .= html_writer::tag('div', $this->course_header(), array('id' => 'course-header')); $html .= html_writer::end_div(); $html .= html_writer::end_div(); $html .= html_writer::end_div(); $html .= html_writer::end_tag('header'); return $html; } /** * The standard tags that should be included in the tag * including a meta description for the front page * * @return string HTML fragment. */ public function standard_head_html() { global $SITE, $PAGE; $output = parent::standard_head_html(); if ($PAGE->pagelayout == 'frontpage') { $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML))); if (!empty($summary)) { $output .= "\n"; } } return $output; } /* * This renders the navbar. * Uses bootstrap compatible html. */ public function navbar() { return $this->render_from_template('core/navbar', $this->page->navbar); } /** * We don't like these... * */ public function edit_button(moodle_url $url) { return ''; } /** * Override to inject the logo. * * @param array $headerinfo The header info. * @param int $headinglevel What level the 'h' tag will be. * @return string HTML for the header bar. */ public function context_header($headerinfo = null, $headinglevel = 1) { global $SITE; if ($this->should_display_main_logo($headinglevel)) { $sitename = format_string($SITE->fullname, true, array('context' => context_course::instance(SITEID))); return html_writer::div(html_writer::empty_tag('img', [ 'src' => $this->get_logo_url(null, 150), 'alt' => $sitename]), 'logo'); } return parent::context_header($headerinfo, $headinglevel); } /** * Get the compact logo URL. * * @return string */ public function get_compact_logo_url($maxwidth = 100, $maxheight = 100) { return parent::get_compact_logo_url(null, 70); } /** * Whether we should display the main logo. * * @return bool */ public function should_display_main_logo($headinglevel = 1) { global $PAGE; // Only render the logo if we're on the front page or login page and the we have a logo. $logo = $this->get_logo_url(); if ($headinglevel == 1 && !empty($logo)) { if ($PAGE->pagelayout == 'frontpage' || $PAGE->pagelayout == 'login') { return true; } } return false; } /** * Whether we should display the logo in the navbar. * * We will when there are no main logos, and we have compact logo. * * @return bool */ public function should_display_navbar_logo() { $logo = $this->get_compact_logo_url(); return !empty($logo) && !$this->should_display_main_logo(); } /* * Overriding the custom_menu function ensures the custom menu is * always shown, even if no menu items are configured in the global * theme settings page. */ public function custom_menu($custommenuitems = '') { global $CFG; if (empty($custommenuitems) && !empty($CFG->custommenuitems)) { $custommenuitems = $CFG->custommenuitems; } $custommenu = new custom_menu($custommenuitems, current_language()); return $this->render_custom_menu($custommenu); } /** * We want to show the custom menus as a list of links in the footer on small screens. * Just return the menu object exported so we can render it differently. */ public function custom_menu_flat() { global $CFG; $custommenuitems = ''; if (empty($custommenuitems) && !empty($CFG->custommenuitems)) { $custommenuitems = $CFG->custommenuitems; } $custommenu = new custom_menu($custommenuitems, current_language()); $langs = get_string_manager()->get_list_of_translations(); $haslangmenu = $this->lang_menu() != ''; if ($haslangmenu) { $strlang = get_string('language'); $currentlang = current_language(); if (isset($langs[$currentlang])) { $currentlang = $langs[$currentlang]; } else { $currentlang = $strlang; } $this->language = $custommenu->add($currentlang, new moodle_url('#'), $strlang, 10000); foreach ($langs as $langtype => $langname) { $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname); } } return $custommenu->export_for_template($this); } /* * This renders the bootstrap top menu. * * This renderer is needed to enable the Bootstrap style navigation. */ protected function render_custom_menu(custom_menu $menu) { global $CFG; $langs = get_string_manager()->get_list_of_translations(); $haslangmenu = $this->lang_menu() != ''; if (!$menu->has_children() && !$haslangmenu) { return ''; } if ($haslangmenu) { $strlang = get_string('language'); $currentlang = current_language(); if (isset($langs[$currentlang])) { $currentlang = $langs[$currentlang]; } else { $currentlang = $strlang; } $this->language = $menu->add($currentlang, new moodle_url('#'), $strlang, 10000); foreach ($langs as $langtype => $langname) { $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname); } } $content = ''; foreach ($menu->get_children() as $item) { $context = $item->export_for_template($this); $content .= $this->render_from_template('core/custom_menu_item', $context); } return $content; } /** * This code renders the navbar button to control the display of the custom menu * on smaller screens. * * Do not display the button if the menu is empty. * * @return string HTML fragment */ public function navbar_button() { global $CFG; if (empty($CFG->custommenuitems) && $this->lang_menu() == '') { return ''; } $iconbar = html_writer::tag('span', '', array('class' => 'icon-bar')); $button = html_writer::tag('a', $iconbar . "\n" . $iconbar. "\n" . $iconbar, array( 'class' => 'btn btn-navbar', 'data-toggle' => 'collapse', 'data-target' => '.nav-collapse' )); return $button; } /** * Renders tabtree * * @param tabtree $tabtree * @return string */ protected function render_tabtree(tabtree $tabtree) { if (empty($tabtree->subtree)) { return ''; } $data = $tabtree->export_for_template($this); return $this->render_from_template('core/tabtree', $data); } /** * Renders tabobject (part of tabtree) * * This function is called from {@link core_renderer::render_tabtree()} * and also it calls itself when printing the $tabobject subtree recursively. * * @param tabobject $tabobject * @return string HTML fragment */ protected function render_tabobject(tabobject $tab) { throw new coding_exception('Tab objects should not be directly rendered.'); } /** * Prints a nice side block with an optional header. * * @param block_contents $bc HTML for the content * @param string $region the region the block is appearing in. * @return string the HTML to be output. */ public function block(block_contents $bc, $region) { $bc = clone($bc); // Avoid messing up the object passed in. if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) { $bc->collapsible = block_contents::NOT_HIDEABLE; } $id = !empty($bc->attributes['id']) ? $bc->attributes['id'] : uniqid('block-'); $context = new stdClass(); $context->skipid = $bc->skipid; $context->blockinstanceid = $bc->blockinstanceid; $context->dockable = $bc->dockable; $context->id = $id; $context->hidden = $bc->collapsible == block_contents::HIDDEN; $context->skiptitle = strip_tags($bc->title); $context->showskiplink = !empty($context->skiptitle); $context->arialabel = $bc->arialabel; $context->ariarole = !empty($bc->attributes['role']) ? $bc->attributes['role'] : 'complementary'; $context->type = $bc->attributes['data-block']; $context->title = $bc->title; $context->content = $bc->content; $context->annotation = $bc->annotation; $context->footer = $bc->footer; $context->hascontrols = !empty($bc->controls); if ($context->hascontrols) { $context->controls = $this->block_controls($bc->controls, $id); } return $this->render_from_template('core/block', $context); } /** * Returns the CSS classes to apply to the body tag. * * @since Moodle 2.5.1 2.6 * @param array $additionalclasses Any additional classes to apply. * @return string */ public function body_css_classes(array $additionalclasses = array()) { return $this->page->bodyclasses . ' ' . implode(' ', $additionalclasses); } /** * Renders preferences groups. * * @param preferences_groups $renderable The renderable * @return string The output. */ public function render_preferences_groups(preferences_groups $renderable) { return $this->render_from_template('core/preferences_groups', $renderable); } /** * Renders an action menu component. * * @param action_menu $menu * @return string HTML */ public function render_action_menu(action_menu $menu) { // We don't want the class icon there! foreach ($menu->get_secondary_actions() as $action) { if ($action instanceof \action_menu_link && $action->has_class('icon')) { $action->attributes['class'] = preg_replace('/(^|\s+)icon(\s+|$)/i', '', $action->attributes['class']); } } if ($menu->is_empty()) { return ''; } $context = $menu->export_for_template($this); return $this->render_from_template('core/action_menu', $context); } /** * Implementation of user image rendering. * * @param help_icon $helpicon A help icon instance * @return string HTML fragment */ protected function render_help_icon(help_icon $helpicon) { $context = $helpicon->export_for_template($this); return $this->render_from_template('core/help_icon', $context); } /** * Renders a single button widget. * * This will return HTML to display a form containing a single button. * * @param single_button $button * @return string HTML fragment */ protected function render_single_button(single_button $button) { return $this->render_from_template('core/single_button', $button->export_for_template($this)); } /** * Renders a paging bar. * * @param paging_bar $pagingbar The object. * @return string HTML */ protected function render_paging_bar(paging_bar $pagingbar) { // Any more than 10 is not usable and causes wierd wrapping of the pagination in this theme. $pagingbar->maxdisplay = 10; return $this->render_from_template('core/paging_bar', $pagingbar->export_for_template($this)); } /** * Renders the login form. * * @param \core_auth\output\login $form The renderable. * @return string */ public function render_login(\core_auth\output\login $form) { global $SITE; $context = $form->export_for_template($this); // Override because rendering is not supported in template yet. $context->cookieshelpiconformatted = $this->help_icon('cookiesenabled'); $context->errorformatted = $this->error_text($context->error); $url = $this->get_logo_url(); if ($url) { $url = $url->out(false); } $context->logourl = $url; $context->sitename = format_string($SITE->fullname, true, ['context' => context_course::instance(SITEID), "escape" => false]); return $this->render_from_template('core/loginform', $context); } /** * Render the login signup form into a nice template for the theme. * * @param mform $form * @return string */ public function render_login_signup_form($form) { global $SITE; $context = $form->export_for_template($this); $url = $this->get_logo_url(); if ($url) { $url = $url->out(false); } $context['logourl'] = $url; $context['sitename'] = format_string($SITE->fullname, true, ['context' => context_course::instance(SITEID), "escape" => false]); return $this->render_from_template('core/signup_form_layout', $context); } /** * This is an optional menu that can be added to a layout by a theme. It contains the * menu for the course administration, only on the course main page. * * @return string */ public function context_header_settings_menu() { $context = $this->page->context; $menu = new action_menu(); $items = $this->page->navbar->get_items(); $currentnode = end($items); $showcoursemenu = false; $showfrontpagemenu = false; $showusermenu = false; // We are on the course home page. if (($context->contextlevel == CONTEXT_COURSE) && !empty($currentnode) && ($currentnode->type == navigation_node::TYPE_COURSE || $currentnode->type == navigation_node::TYPE_SECTION)) { $showcoursemenu = true; } $courseformat = course_get_format($this->page->course); // This is a single activity course format, always show the course menu on the activity main page. if ($context->contextlevel == CONTEXT_MODULE && !$courseformat->has_view_page()) { $this->page->navigation->initialise(); $activenode = $this->page->navigation->find_active_node(); // If the settings menu has been forced then show the menu. if ($this->page->is_settings_menu_forced()) { $showcoursemenu = true; } else if (!empty($activenode) && ($activenode->type == navigation_node::TYPE_ACTIVITY || $activenode->type == navigation_node::TYPE_RESOURCE)) { // We only want to show the menu on the first page of the activity. This means // the breadcrumb has no additional nodes. if ($currentnode && ($currentnode->key == $activenode->key && $currentnode->type == $activenode->type)) { $showcoursemenu = true; } } } // This is the site front page. if ($context->contextlevel == CONTEXT_COURSE && !empty($currentnode) && $currentnode->key === 'home') { $showfrontpagemenu = true; } // This is the user profile page. if ($context->contextlevel == CONTEXT_USER && !empty($currentnode) && ($currentnode->key === 'myprofile')) { $showusermenu = true; } if ($showfrontpagemenu) { $settingsnode = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING); if ($settingsnode) { // Build an action menu based on the visible nodes from this navigation tree. $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true); // We only add a list to the full settings menu if we didn't include every node in the short menu. if ($skipped) { $text = get_string('morenavigationlinks'); $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id)); $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text)); $menu->add_secondary_action($link); } } } else if ($showcoursemenu) { $settingsnode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE); if ($settingsnode) { // Build an action menu based on the visible nodes from this navigation tree. $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true); // We only add a list to the full settings menu if we didn't include every node in the short menu. if ($skipped) { $text = get_string('morenavigationlinks'); $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id)); $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text)); $menu->add_secondary_action($link); } } } else if ($showusermenu) { // Get the course admin node from the settings navigation. $settingsnode = $this->page->settingsnav->find('useraccount', navigation_node::TYPE_CONTAINER); if ($settingsnode) { // Build an action menu based on the visible nodes from this navigation tree. $this->build_action_menu_from_navigation($menu, $settingsnode); } } return $this->render($menu); } /** * This is an optional menu that can be added to a layout by a theme. It contains the * menu for the most specific thing from the settings block. E.g. Module administration. * * @return string */ public function region_main_settings_menu() { $context = $this->page->context; $menu = new action_menu(); if ($context->contextlevel == CONTEXT_MODULE) { $this->page->navigation->initialise(); $node = $this->page->navigation->find_active_node(); $buildmenu = false; // If the settings menu has been forced then show the menu. if ($this->page->is_settings_menu_forced()) { $buildmenu = true; } else if (!empty($node) && ($node->type == navigation_node::TYPE_ACTIVITY || $node->type == navigation_node::TYPE_RESOURCE)) { $items = $this->page->navbar->get_items(); $navbarnode = end($items); // We only want to show the menu on the first page of the activity. This means // the breadcrumb has no additional nodes. if ($navbarnode && ($navbarnode->key === $node->key && $navbarnode->type == $node->type)) { $buildmenu = true; } } if ($buildmenu) { // Get the course admin node from the settings navigation. $node = $this->page->settingsnav->find('modulesettings', navigation_node::TYPE_SETTING); if ($node) { // Build an action menu based on the visible nodes from this navigation tree. $this->build_action_menu_from_navigation($menu, $node); } } } else if ($context->contextlevel == CONTEXT_COURSECAT) { // For course category context, show category settings menu, if we're on the course category page. if ($this->page->pagetype === 'course-index-category') { $node = $this->page->settingsnav->find('categorysettings', navigation_node::TYPE_CONTAINER); if ($node) { // Build an action menu based on the visible nodes from this navigation tree. $this->build_action_menu_from_navigation($menu, $node); } } } else { $items = $this->page->navbar->get_items(); $navbarnode = end($items); if ($navbarnode && ($navbarnode->key === 'participants')) { $node = $this->page->settingsnav->find('users', navigation_node::TYPE_CONTAINER); if ($node) { // Build an action menu based on the visible nodes from this navigation tree. $this->build_action_menu_from_navigation($menu, $node); } } } return $this->render($menu); } /** * Take a node in the nav tree and make an action menu out of it. * The links are injected in the action menu. * * @param action_menu $menu * @param navigation_node $node * @param boolean $indent * @param boolean $onlytopleafnodes * @return boolean nodesskipped - True if nodes were skipped in building the menu */ protected function build_action_menu_from_navigation(action_menu $menu, navigation_node $node, $indent = false, $onlytopleafnodes = false) { $skipped = false; // Build an action menu based on the visible nodes from this navigation tree. foreach ($node->children as $menuitem) { if ($menuitem->display) { if ($onlytopleafnodes && $menuitem->children->count()) { $skipped = true; continue; } if ($menuitem->action) { if ($menuitem->action instanceof action_link) { $link = $menuitem->action; // Give preference to setting icon over action icon. if (!empty($menuitem->icon)) { $link->icon = $menuitem->icon; } } else { $link = new action_link($menuitem->action, $menuitem->text, null, null, $menuitem->icon); } } else { if ($onlytopleafnodes) { $skipped = true; continue; } $link = new action_link(new moodle_url('#'), $menuitem->text, null, ['disabled' => true], $menuitem->icon); } if ($indent) { $link->add_class('m-l-1'); } if (!empty($menuitem->classes)) { $link->add_class(implode(" ", $menuitem->classes)); } $menu->add_secondary_action($link); $skipped = $skipped || $this->build_action_menu_from_navigation($menu, $menuitem, true); } } return $skipped; } /** * Secure login info. * * @return string */ public function secure_login_info() { return $this->login_info(false); } }