diff --git a/admin/roles/assign.php b/admin/roles/assign.php
index c542943da30..82e07a5f72d 100644
--- a/admin/roles/assign.php
+++ b/admin/roles/assign.php
@@ -61,6 +61,7 @@ if ($returnurl) {
}
$PAGE->set_url($pageurl);
$PAGE->set_context($context);
+$PAGE->activityheader->disable();
$contextname = $context->get_context_name();
$courseid = $course->id;
diff --git a/admin/roles/check.php b/admin/roles/check.php
index 631085ab7a7..daa2ccca6d8 100644
--- a/admin/roles/check.php
+++ b/admin/roles/check.php
@@ -87,6 +87,7 @@ if ($context->contextlevel == CONTEXT_BLOCK) {
$PAGE->blocks->show_only_fake_blocks(true);
}
$PAGE->set_title($title);
+$PAGE->activityheader->disable();
switch ($context->contextlevel) {
case CONTEXT_SYSTEM:
diff --git a/admin/roles/permissions.php b/admin/roles/permissions.php
index dfd6547a00b..773c3fc397e 100644
--- a/admin/roles/permissions.php
+++ b/admin/roles/permissions.php
@@ -97,6 +97,7 @@ if ($context->contextlevel == CONTEXT_BLOCK) {
}
$PAGE->set_title($title);
+$PAGE->activityheader->disable();
switch ($context->contextlevel) {
case CONTEXT_SYSTEM:
print_error('cannotoverridebaserole', 'error');
diff --git a/backup/backup.php b/backup/backup.php
index c68bbfa9882..80968967d5c 100644
--- a/backup/backup.php
+++ b/backup/backup.php
@@ -107,6 +107,7 @@ switch ($type) {
$PAGE->set_title($heading);
$PAGE->set_heading($heading);
+$PAGE->activityheader->disable();
if (empty($cancel)) {
// Do not print the header if user cancelled the process, as we are going to redirect the user.
diff --git a/backup/restorefile.php b/backup/restorefile.php
index eef560bf06c..6351918868d 100644
--- a/backup/restorefile.php
+++ b/backup/restorefile.php
@@ -114,6 +114,7 @@ $PAGE->set_context($context);
$PAGE->set_title(get_string('course') . ': ' . $coursefullname);
$PAGE->set_heading($heading);
$PAGE->set_pagelayout('admin');
+$PAGE->activityheader->disable();
$PAGE->requires->js_call_amd('core_backup/async_backup', 'asyncBackupAllStatus', array($context->id));
$form = new course_restore_form(null, array('contextid'=>$contextid));
diff --git a/course/modedit.php b/course/modedit.php
index 71cc3102df1..c4de83c75a9 100644
--- a/course/modedit.php
+++ b/course/modedit.php
@@ -200,6 +200,7 @@ if ($mform->is_cancelled()) {
if (isset($navbaraddition)) {
$PAGE->navbar->add($navbaraddition);
}
+ $PAGE->activityheader->disable();
echo $OUTPUT->header();
diff --git a/filter/manage.php b/filter/manage.php
index bff72f847a0..0b6b6752818 100644
--- a/filter/manage.php
+++ b/filter/manage.php
@@ -123,6 +123,7 @@ $straction = get_string('filters', 'admin'); // Used by tabs.php
$PAGE->set_cacheable(false);
$PAGE->set_title($title);
$PAGE->set_pagelayout('admin');
+$PAGE->activityheader->disable();
echo $OUTPUT->header();
/// Print heading.
diff --git a/grade/grading/manage.php b/grade/grading/manage.php
index 0491d89195b..d38c96721dc 100644
--- a/grade/grading/manage.php
+++ b/grade/grading/manage.php
@@ -82,6 +82,8 @@ $PAGE->set_url($manager->get_management_url($returnurl));
navigation_node::override_active_url($manager->get_management_url());
$PAGE->set_title(get_string('gradingmanagement', 'core_grading'));
$PAGE->set_heading(get_string('gradingmanagement', 'core_grading'));
+// We don't need to show the default header on a management page.
+$PAGE->activityheader->disable();
$output = $PAGE->get_renderer('core_grading');
// process the eventual change of the active grading method
diff --git a/lang/en/moodle.php b/lang/en/moodle.php
index 4ca3c691cc4..10e37273878 100644
--- a/lang/en/moodle.php
+++ b/lang/en/moodle.php
@@ -60,6 +60,7 @@ $string['addedtogroupnot'] = 'Not added to group "{$a}"';
$string['addedtogroupnotenrolled'] = 'Not added to group "{$a}", because not enrolled in course';
$string['addfilehere'] = 'Add file(s) here';
$string['addinganew'] = 'Adding a new {$a}';
+$string['additionalcustomnav'] = 'Additional custom navigation';
$string['addnew'] = 'Add a new {$a}';
$string['addinganewto'] = 'Adding a new {$a->what} to {$a->to}';
$string['addingdatatoexisting'] = 'Adding data to existing';
diff --git a/lib/classes/output/activity_header.php b/lib/classes/output/activity_header.php
new file mode 100644
index 00000000000..af1bd2bf904
--- /dev/null
+++ b/lib/classes/output/activity_header.php
@@ -0,0 +1,184 @@
+.
+
+namespace core\output;
+
+use moodle_page;
+use renderer_base;
+use url_select;
+
+/**
+ * Data structure representing standard components displayed on the activity header.
+ *
+ * Consists of title, header, description. In addition, additional_items can be provided which is a url_select
+ *
+ * @copyright 2021 Peter
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 4.0
+ * @package core
+ * @category output
+ */
+class activity_header implements \renderable, \templatable {
+ /** @var moodle_page $page The current page we are looking at */
+ protected $page;
+ /** @var string $title The title to be displayed in the header. Defaults to activityrecord name. */
+ protected $title;
+ /** @var string $description The description to be displayed. Defaults to activityrecord intro. */
+ protected $description;
+ /** @var \stdClass $user The user we are dealing with */
+ protected $user;
+ /** @var url_select $additionalnavitems Any additional custom navigation elements to be injected into template. */
+ protected $additionalnavitems;
+ /** @var bool $hidecompletion Whether to show completion criteria, if available, or not */
+ protected $hidecompletion;
+ /** @var bool $hideoverflow Whether to show the overflow data or not */
+ protected $hideoverflow;
+ /** @var bool $hideheader Whether or not to show the header */
+ protected $hideheader;
+
+ /**
+ * Constructor for activity_header
+ *
+ * @param moodle_page $page
+ * @param \stdClass $user
+ */
+ public function __construct(moodle_page $page, \stdClass $user) {
+ $this->page = $page;
+ $this->user = $user;
+ $pageoptions = $this->page->theme->activityheaderconfig ?? [];
+ $layoutoptions = $this->page->layout_options['activityheader'] ?? [];
+ // Do a basic setup for the header based on theme/page options.
+ if ($page->activityrecord) {
+ if (empty($pageoptions['notitle']) && empty($layoutoptions['notitle'])) {
+ $this->title = format_string($page->activityrecord->name);
+ }
+
+ if (empty($layoutoptions['nodescription']) && $page->activityrecord->intro && trim($page->activityrecord->intro)) {
+ $this->description = format_module_intro($this->page->activityname, $page->activityrecord, $page->cm->id);
+ }
+ }
+ $this->hidecompletion = !empty($layoutoptions['nocompletion']);
+ $this->hideoverflow = false;
+ $this->hideheader = false;
+ }
+
+ /**
+ * Checks if the theme has specified titles to be displayed.
+ *
+ * @return bool
+ */
+ public function is_title_allowed(): bool {
+ return empty($this->page->theme->activityheaderconfig['notitle']);
+ }
+
+ /**
+ * Bulk set class member variables. Only updates variables which have corresponding setters
+ *
+ * @param array $config
+ */
+ public function set_attrs(array $config): void {
+ foreach ($config as $key => $value) {
+ if (method_exists($this, "set_$key")) {
+ $this->{"set_$key"}($value);
+ }
+ }
+ }
+
+ /**
+ * Sets the hidecompletion class member variable
+ *
+ * @param bool $value
+ */
+ public function set_hidecompletion(bool $value): void {
+ $this->hidecompletion = $value;
+ }
+
+ /**
+ * Sets the additionalnavitems class member variable
+ *
+ * @param url_select $value
+ */
+ public function set_additionalnavitems(url_select $value): void {
+ $this->additionalnavitems = $value;
+ }
+
+ /**
+ * Sets the hideoverflow class member variable
+ *
+ * @param bool $value
+ */
+ public function set_hideoverflow(bool $value): void {
+ $this->hideoverflow = $value;
+ }
+
+ /**
+ * Sets the title class member variable.
+ *
+ * @param string $value
+ */
+ public function set_title(string $value): void {
+ $this->title = preg_replace('/
]*>([.\s\S]*)<\/h2>/', '$1', $value);
+ }
+
+ /**
+ * Sets the description class member variable
+ *
+ * @param string $value
+ */
+ public function set_description(string $value): void {
+ $this->description = $value;
+ }
+
+ /**
+ * Disable the activity header completely. Use this if the page has some custom content, headings to be displayed.
+ */
+ public function disable(): void {
+ $this->hideheader = true;
+ }
+
+ /**
+ * Export items to be rendered with a template.
+ *
+ * @param renderer_base $output
+ * @return array
+ */
+ public function export_for_template(renderer_base $output): array {
+ // Don't need to show anything if not displaying within an activity context.
+ if (!$this->page->activityrecord) {
+ return [];
+ }
+
+ // If within an activity context but requesting to hide the header,
+ // then just trigger the render for maincontent div.
+ if ($this->hideheader) {
+ return ['title' => ''];
+ }
+
+ $completion = null;
+ if (!$this->hidecompletion) {
+ $completiondetails = \core_completion\cm_completion_details::get_instance($this->page->cm, $this->user->id);
+ $activitydates = \core\activity_dates::get_dates_for_module($this->page->cm, $this->user->id);
+ $completion = $output->activity_information($this->page->cm, $completiondetails, $activitydates);
+ }
+
+ return [
+ 'title' => $this->title,
+ 'description' => $this->description,
+ 'completion' => $completion,
+ 'additional_items' => $this->hideoverflow ? '' : $this->additionalnavitems,
+ ];
+ }
+}
diff --git a/lib/outputlib.php b/lib/outputlib.php
index aecd3f47db6..a99c7cdca6a 100644
--- a/lib/outputlib.php
+++ b/lib/outputlib.php
@@ -760,7 +760,7 @@ class theme_config {
'rendererfactory', 'csspostprocess', 'editor_sheets', 'editor_scss', 'rarrow', 'larrow', 'uarrow', 'darrow',
'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations', 'blockrendermethod',
'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition',
- 'iconsystem', 'precompiledcsscallback', 'haseditswitch', 'usescourseindex');
+ 'iconsystem', 'precompiledcsscallback', 'haseditswitch', 'usescourseindex', 'activityheaderconfig');
foreach ($config as $key=>$value) {
if (in_array($key, $configurable)) {
diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php
index 534bbfcf969..b478e9566b3 100644
--- a/lib/outputrenderers.php
+++ b/lib/outputrenderers.php
@@ -1402,7 +1402,11 @@ class core_renderer extends renderer_base {
$this->opencontainers->push('header/footer', $footer);
$this->page->set_state(moodle_page::STATE_IN_BODY);
- return $header . $this->skip_link_target('maincontent');
+ // If an activity record has been set, activity_header will handle this.
+ if (!$this->page->activityrecord || !empty($this->page->layout_options['noactivityheader'])) {
+ $header .= $this->skip_link_target('maincontent');
+ }
+ return $header;
}
/**
@@ -2903,6 +2907,8 @@ EOD;
//$this->page->set_pagelayout('base'); //TODO: MDL-20676 blocks on error pages are weird, unfortunately it somehow detect the pagelayout from URL :-(
$this->page->set_title(get_string('error'));
$this->page->set_heading($this->page->course->fullname);
+ // No need to display the activity header when encountering an error.
+ $this->page->activityheader->disable();
$output .= $this->header();
}
diff --git a/lib/pagelib.php b/lib/pagelib.php
index 189ece5d763..eb7a00117f0 100644
--- a/lib/pagelib.php
+++ b/lib/pagelib.php
@@ -403,6 +403,11 @@ class moodle_page {
*/
protected $_activenodeprimary = null;
+ /**
+ * @var \core\output\activity_header The default activity header for standardised.
+ */
+ protected $_activityheader;
+
/**
* Force the settings menu to be displayed on this page. This will only force the
* settings menu on an activity / resource page that is being displayed on a theme that
@@ -823,6 +828,24 @@ class moodle_page {
return $this->_flatnav;
}
+ /**
+ * Returns the activity header object
+ * @return secondary
+ */
+ protected function magic_get_activityheader() {
+ global $USER;
+ if ($this->_activityheader === null) {
+ $class = 'core\output\activity_header';
+ // Try and load a custom class first.
+ if (class_exists("mod_{$this->activityname}\\output\\activity_header")) {
+ $class = "mod_{$this->activityname}\\output\\activity_header";
+ }
+
+ $this->_activityheader = new $class($this, $USER);
+ }
+ return $this->_activityheader;
+ }
+
/**
* Returns the secondary navigation object
* @return secondary
diff --git a/lib/portfoliolib.php b/lib/portfoliolib.php
index 33cdbe54223..e2201d06e32 100644
--- a/lib/portfoliolib.php
+++ b/lib/portfoliolib.php
@@ -1117,7 +1117,10 @@ function portfolio_export_pagesetup($PAGE, $caller) {
// and now we know the course for sure and maybe the cm, call require_login with it
require_login($PAGE->course, false, $cm);
-
+ $PAGE->activityheader->set_attrs([
+ 'description' => '',
+ 'hidecompletion' => true
+ ]);
foreach ($extranav as $navitem) {
$PAGE->navbar->add($navitem['name']);
}
diff --git a/lib/templates/activity_header.mustache b/lib/templates/activity_header.mustache
new file mode 100644
index 00000000000..f26224b9508
--- /dev/null
+++ b/lib/templates/activity_header.mustache
@@ -0,0 +1,86 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/activity_header
+
+ Activity header template.
+
+ Context variables required for this template:
+ * title - The title of the activity module
+ * description - The intro for the module
+ * completion - The completion info if available for the the module as acquired via the activity_information method
+ * additional_items - Any additional URL select navigation that needs to show up in the header
+
+ Example context (json):
+ {
+ "title": "Assignment 1",
+ "description": "The assignment does something",
+ "completion": "