From 585b0604622fa3be71a0a106c59fca2176273bad Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 13 Jun 2024 00:33:03 +0800 Subject: [PATCH 1/7] MDL-82183 core: Do not load subsystem db/legacyclasses.php Subsystems do not support the notion of a `db` folder. Instead we must conflate the lib/db/legacyclasses.php syntax to allow it to support paths as either a string, or an array with [subsystem, path]. --- lib/classes/component.php | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/classes/component.php b/lib/classes/component.php index e7f3eb4b324..26b83933d71 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -727,14 +727,13 @@ $cache = ' . var_export($cache, true) . '; self::$classmap = []; self::load_classes('core', "$CFG->dirroot/lib/classes"); - self::load_legacy_classes($CFG->libdir); + self::load_legacy_classes($CFG->libdir, true); foreach (self::$subsystems as $subsystem => $fulldir) { if (!$fulldir) { continue; } self::load_classes('core_' . $subsystem, "$fulldir/classes"); - self::load_legacy_classes($fulldir); } foreach (self::$plugins as $plugintype => $plugins) { @@ -1448,8 +1447,12 @@ $cache = ' . var_export($cache, true) . '; * and the value is the path to the class file within the relative ../classes/ directory. * * @param string|null $fulldir The directory to the legacy classes. + * @param bool $allowsubsystems Whether to allow the specification of alternative subsystems for this path. */ - protected static function load_legacy_classes(?string $fulldir): void { + protected static function load_legacy_classes( + ?string $fulldir, + bool $allowsubsystems = false, + ): void { if (is_null($fulldir)) { return; } @@ -1460,7 +1463,32 @@ $cache = ' . var_export($cache, true) . '; require($file); if (is_array($legacyclasses)) { foreach ($legacyclasses as $classname => $path) { - self::$classmap[$classname] = "{$fulldir}/classes/{$path}"; + if (is_array($path)) { + if (!$allowsubsystems) { + throw new \Exception( + "Invalid legacy classes path entry for {$classname}. " . + "Only files within the component can be specified.", + ); + } + if (count($path) !== 2) { + throw new \Exception( + "Invalid legacy classes path entry for {$classname}. " . + "Entries must be in the format [subsystem, path].", + ); + } + [$subsystem, $path] = $path; + $subsystem = substr($subsystem, 5); + if (!array_key_exists($subsystem, self::$subsystems)) { + throw new \Exception( + "Unknown subsystem '{$subsystem}' for legacy classes entry of '{$classname}'", + ); + } + + $subsystemfulldir = self::$subsystems[$subsystem]; + self::$classmap[$classname] = "{$subsystemfulldir}/classes/{$path}"; + } else { + self::$classmap[$classname] = "{$fulldir}/classes/{$path}"; + } } } } From 2fff48b07c3a6ced72b67dcabd3f20427f691b71 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 12 Jun 2024 23:45:33 +0800 Subject: [PATCH 2/7] MDL-82183 core: Migrate all output classes to autoloaded --- blocks/classes/output/block_contents.php | 161 + blocks/classes/output/block_move_target.php | 44 + lib/classes/component.php | 1 + lib/classes/output/action_link.php | 156 + lib/classes/output/action_menu.php | 666 ++ lib/classes/output/action_menu/filler.php | 39 + lib/classes/output/action_menu/link.php | 106 + .../output/action_menu/link_primary.php | 37 + .../output/action_menu/link_secondary.php | 37 + .../output/actions/component_action.php | 79 + lib/classes/output/actions/confirm_action.php | 47 + lib/classes/output/actions/popup_action.php | 106 + lib/classes/output/context_header.php | 147 + lib/classes/output/core_renderer.php | 4765 ++++++++++++++ lib/classes/output/core_renderer_ajax.php | 134 + lib/classes/output/core_renderer_cli.php | 250 + .../output/core_renderer_maintenance.php | 229 + lib/classes/output/custom_menu.php | 179 + lib/classes/output/custom_menu_item.php | 261 + lib/classes/output/file_picker.php | 93 + lib/classes/output/help_icon.php | 118 + lib/classes/output/html_writer.php | 870 +++ lib/classes/output/image_icon.php | 27 + lib/classes/output/initials_bar.php | 148 + lib/classes/output/js_writer.php | 147 + lib/classes/output/paging_bar.php | 255 + lib/classes/output/pix_emoticon.php | 40 + lib/classes/output/pix_icon.php | 143 + lib/classes/output/pix_icon_font.php | 93 + lib/classes/output/pix_icon_fontawesome.php | 26 + lib/classes/output/plugin_renderer_base.php | 131 + lib/classes/output/preferences_group.php | 47 + lib/classes/output/preferences_groups.php | 39 + lib/classes/output/progress_bar.php | 204 + lib/classes/output/renderable.php | 26 + lib/classes/output/renderer_base.php | 406 ++ .../renderer_factory_base.php | 197 + .../renderer_factory_interface.php | 61 + .../standard_renderer_factory.php | 80 + .../theme_overridden_renderer_factory.php | 118 + .../fragment_requirements_manager.php | 118 + .../page_requirements_manager.php | 1814 +++++ lib/classes/output/requirements/yui.php | 351 + lib/classes/output/single_button.php | 266 + lib/classes/output/single_select.php | 289 + lib/classes/output/tabobject.php | 140 + lib/classes/output/tabtree.php | 91 + lib/classes/output/templatable.php | 37 + lib/classes/output/theme_config.php | 2312 +++++++ lib/classes/output/url_select.php | 282 + lib/classes/output/user_picture.php | 302 + lib/classes/output/xhtml_container_stack.php | 153 + lib/db/legacyclasses.php | 83 + lib/form/button.php | 1 - lib/form/templatable_form_element.php | 12 - lib/outputactions.php | 209 +- lib/outputcomponents.php | 5355 +-------------- lib/outputfactories.php | 438 +- lib/outputfragmentrequirementslib.php | 118 +- lib/outputlib.php | 2465 +------ lib/outputrenderers.php | 5857 +---------------- lib/outputrequirementslib.php | 2186 +----- lib/pagelib.php | 3 - lib/table/classes/output/html_table.php | 207 + lib/table/classes/output/html_table_cell.php | 82 + lib/table/classes/output/html_table_row.php | 63 + lib/tests/coverage.php | 6 - lib/tests/fixtures/test_renderer_factory.php | 14 - lib/tests/html_writer_test.php | 17 +- lib/tests/outputcomponents_test.php | 8 +- lib/tests/outputfactories_test.php | 16 +- lib/tests/outputrequirementslib_test.php | 8 +- mod/wiki/parser/utils.php | 3 - 73 files changed, 17381 insertions(+), 16638 deletions(-) create mode 100644 blocks/classes/output/block_contents.php create mode 100644 blocks/classes/output/block_move_target.php create mode 100644 lib/classes/output/action_link.php create mode 100644 lib/classes/output/action_menu.php create mode 100644 lib/classes/output/action_menu/filler.php create mode 100644 lib/classes/output/action_menu/link.php create mode 100644 lib/classes/output/action_menu/link_primary.php create mode 100644 lib/classes/output/action_menu/link_secondary.php create mode 100644 lib/classes/output/actions/component_action.php create mode 100644 lib/classes/output/actions/confirm_action.php create mode 100644 lib/classes/output/actions/popup_action.php create mode 100644 lib/classes/output/context_header.php create mode 100644 lib/classes/output/core_renderer.php create mode 100644 lib/classes/output/core_renderer_ajax.php create mode 100644 lib/classes/output/core_renderer_cli.php create mode 100644 lib/classes/output/core_renderer_maintenance.php create mode 100644 lib/classes/output/custom_menu.php create mode 100644 lib/classes/output/custom_menu_item.php create mode 100644 lib/classes/output/file_picker.php create mode 100644 lib/classes/output/help_icon.php create mode 100644 lib/classes/output/html_writer.php create mode 100644 lib/classes/output/image_icon.php create mode 100644 lib/classes/output/initials_bar.php create mode 100644 lib/classes/output/js_writer.php create mode 100644 lib/classes/output/paging_bar.php create mode 100644 lib/classes/output/pix_emoticon.php create mode 100644 lib/classes/output/pix_icon.php create mode 100644 lib/classes/output/pix_icon_font.php create mode 100644 lib/classes/output/pix_icon_fontawesome.php create mode 100644 lib/classes/output/plugin_renderer_base.php create mode 100644 lib/classes/output/preferences_group.php create mode 100644 lib/classes/output/preferences_groups.php create mode 100644 lib/classes/output/progress_bar.php create mode 100644 lib/classes/output/renderable.php create mode 100644 lib/classes/output/renderer_base.php create mode 100644 lib/classes/output/renderer_factory/renderer_factory_base.php create mode 100644 lib/classes/output/renderer_factory/renderer_factory_interface.php create mode 100644 lib/classes/output/renderer_factory/standard_renderer_factory.php create mode 100644 lib/classes/output/renderer_factory/theme_overridden_renderer_factory.php create mode 100644 lib/classes/output/requirements/fragment_requirements_manager.php create mode 100644 lib/classes/output/requirements/page_requirements_manager.php create mode 100644 lib/classes/output/requirements/yui.php create mode 100644 lib/classes/output/single_button.php create mode 100644 lib/classes/output/single_select.php create mode 100644 lib/classes/output/tabobject.php create mode 100644 lib/classes/output/tabtree.php create mode 100644 lib/classes/output/templatable.php create mode 100644 lib/classes/output/theme_config.php create mode 100644 lib/classes/output/url_select.php create mode 100644 lib/classes/output/user_picture.php create mode 100644 lib/classes/output/xhtml_container_stack.php create mode 100644 lib/table/classes/output/html_table.php create mode 100644 lib/table/classes/output/html_table_cell.php create mode 100644 lib/table/classes/output/html_table_row.php diff --git a/blocks/classes/output/block_contents.php b/blocks/classes/output/block_contents.php new file mode 100644 index 00000000000..a18800bbe1b --- /dev/null +++ b/blocks/classes/output/block_contents.php @@ -0,0 +1,161 @@ +. + +/** + * This class represents how a block appears on a page. + * + * During output, each block instance is asked to return a block_contents object, + * those are then passed to the $OUTPUT->block function for display. + * + * contents should probably be generated using a moodle_block_..._renderer. + * + * Other block-like things that need to appear on the page, for example the + * add new block UI, are also represented as block_contents objects. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * @package core + * @category output + */ +class block_contents { + + /** Used when the block cannot be collapsed **/ + const NOT_HIDEABLE = 0; + + /** Used when the block can be collapsed but currently is not **/ + const VISIBLE = 1; + + /** Used when the block has been collapsed **/ + const HIDDEN = 2; + + /** + * @var int Used to set $skipid. + */ + protected static $idcounter = 1; + + /** + * @var int All the blocks (or things that look like blocks) printed on + * a page are given a unique number that can be used to construct id="" attributes. + * This is set automatically be the {@link prepare()} method. + * Do not try to set it manually. + */ + public $skipid; + + /** + * @var int If this is the contents of a real block, this should be set + * to the block_instance.id. Otherwise this should be set to 0. + */ + public $blockinstanceid = 0; + + /** + * @var int If this is a real block instance, and there is a corresponding + * block_position.id for the block on this page, this should be set to that id. + * Otherwise it should be 0. + */ + public $blockpositionid = 0; + + /** + * @var array An array of attribute => value pairs that are put on the outer div of this + * block. {@link $id} and {@link $classes} attributes should be set separately. + */ + public $attributes; + + /** + * @var string The title of this block. If this came from user input, it should already + * have had format_string() processing done on it. This will be output inside + *

tags. Please do not cause invalid XHTML. + */ + public $title = ''; + + /** + * @var string The label to use when the block does not, or will not have a visible title. + * You should never set this as well as title... it will just be ignored. + */ + public $arialabel = ''; + + /** + * @var string HTML for the content + */ + public $content = ''; + + /** + * @var array An alternative to $content, it you want a list of things with optional icons. + */ + public $footer = ''; + + /** + * @var string Any small print that should appear under the block to explain + * to the teacher about the block, for example 'This is a sticky block that was + * added in the system context.' + */ + public $annotation = ''; + + /** + * @var int One of the constants NOT_HIDEABLE, VISIBLE, HIDDEN. Whether + * the user can toggle whether this block is visible. + */ + public $collapsible = self::NOT_HIDEABLE; + + /** + * Set this to true if the block is dockable. + * @var bool + */ + public $dockable = false; + + /** + * @var array A (possibly empty) array of editing controls. Each element of + * this array should be an array('url' => $url, 'icon' => $icon, 'caption' => $caption). + * $icon is the icon name. Fed to $OUTPUT->image_url. + */ + public $controls = array(); + + + /** + * Create new instance of block content + * @param array $attributes + */ + public function __construct(array $attributes = null) { + $this->skipid = self::$idcounter; + self::$idcounter += 1; + + if ($attributes) { + // standard block + $this->attributes = $attributes; + } else { + // simple "fake" blocks used in some modules and "Add new block" block + $this->attributes = array('class'=>'block'); + } + } + + /** + * Add html class to block + * + * @param string $class + */ + public function add_class($class) { + $this->attributes['class'] .= ' '.$class; + } + + /** + * Check if the block is a fake block. + * + * @return boolean + */ + public function is_fake() { + return isset($this->attributes['data-block']) && $this->attributes['data-block'] == '_fake'; + } +} diff --git a/blocks/classes/output/block_move_target.php b/blocks/classes/output/block_move_target.php new file mode 100644 index 00000000000..c0312747b8b --- /dev/null +++ b/blocks/classes/output/block_move_target.php @@ -0,0 +1,44 @@ +. + +/** + * This class represents a target for where a block can go when it is being moved. + * + * This needs to be rendered as a form with the given hidden from fields, and + * clicking anywhere in the form should submit it. The form action should be + * $PAGE->url. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * @package core + * @category output + */ +class block_move_target { + + /** + * @var moodle_url Move url + */ + public $url; + + /** + * Constructor + * @param moodle_url $url + */ + public function __construct(moodle_url $url) { + $this->url = $url; + } +} diff --git a/lib/classes/component.php b/lib/classes/component.php index 26b83933d71..50a5be5f4c3 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -390,6 +390,7 @@ class core_component { $keyclasses = [ \core\exception\moodle_exception::class, \core\output\bootstrap_renderer::class, + \renderable::class, ]; foreach ($keyclasses as $classname) { if (!array_key_exists($classname, $cache['classmap'])) { diff --git a/lib/classes/output/action_link.php b/lib/classes/output/action_link.php new file mode 100644 index 00000000000..ae006adfe27 --- /dev/null +++ b/lib/classes/output/action_link.php @@ -0,0 +1,156 @@ +. + +/** + * Data structure describing html link with special action attached. + * + * @copyright 2010 Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * @package core + * @category output + */ +class action_link implements renderable { + + /** + * @var moodle_url Href url + */ + public $url; + + /** + * @var string Link text HTML fragment + */ + public $text; + + /** + * @var array HTML attributes + */ + public $attributes; + + /** + * @var array List of actions attached to link + */ + public $actions; + + /** + * @var pix_icon Optional pix icon to render with the link + */ + public $icon; + + /** + * Constructor + * @param moodle_url $url + * @param string $text HTML fragment + * @param component_action $action + * @param array $attributes associative array of html link attributes + disabled + * @param pix_icon $icon optional pix_icon to render with the link text + */ + public function __construct(moodle_url $url, + $text, + component_action $action=null, + array $attributes=null, + pix_icon $icon=null) { + $this->url = clone($url); + $this->text = $text; + if (empty($attributes['id'])) { + $attributes['id'] = html_writer::random_id('action_link'); + } + $this->attributes = (array)$attributes; + if ($action) { + $this->add_action($action); + } + $this->icon = $icon; + } + + /** + * Add action to the link. + * + * @param component_action $action + */ + public function add_action(component_action $action) { + $this->actions[] = $action; + } + + /** + * Adds a CSS class to this action link object + * @param string $class + */ + public function add_class($class) { + if (empty($this->attributes['class'])) { + $this->attributes['class'] = $class; + } else { + $this->attributes['class'] .= ' ' . $class; + } + } + + /** + * Returns true if the specified class has been added to this link. + * @param string $class + * @return bool + */ + public function has_class($class) { + return strpos(' ' . $this->attributes['class'] . ' ', ' ' . $class . ' ') !== false; + } + + /** + * Return the rendered HTML for the icon. Useful for rendering action links in a template. + * @return string + */ + public function get_icon_html() { + global $OUTPUT; + if (!$this->icon) { + return ''; + } + return $OUTPUT->render($this->icon); + } + + /** + * Export for template. + * + * @param renderer_base $output The renderer. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $attributes = $this->attributes; + + $data->id = $attributes['id']; + unset($attributes['id']); + + $data->disabled = !empty($attributes['disabled']); + unset($attributes['disabled']); + + $data->text = $this->text instanceof renderable ? $output->render($this->text) : (string) $this->text; + $data->url = $this->url ? $this->url->out(false) : ''; + $data->icon = $this->icon ? $this->icon->export_for_pix() : null; + $data->classes = isset($attributes['class']) ? $attributes['class'] : ''; + unset($attributes['class']); + + $data->attributes = array_map(function($key, $value) { + return [ + 'name' => $key, + 'value' => $value + ]; + }, array_keys($attributes), $attributes); + + $data->actions = array_map(function($action) use ($output) { + return $action->export_for_template($output); + }, !empty($this->actions) ? $this->actions : []); + $data->hasactions = !empty($this->actions); + + return $data; + } +} diff --git a/lib/classes/output/action_menu.php b/lib/classes/output/action_menu.php new file mode 100644 index 00000000000..2906f1c86fa --- /dev/null +++ b/lib/classes/output/action_menu.php @@ -0,0 +1,666 @@ +. + +use core\output\local\action_menu\subpanel; + +/** + * An action menu. + * + * This action menu component takes a series of primary and secondary actions. + * The primary actions are displayed permanently and the secondary attributes are displayed within a drop + * down menu. + * + * @package core + * @category output + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class action_menu implements renderable, templatable { + + /** + * Top right alignment. + */ + const TL = 1; + + /** + * Top right alignment. + */ + const TR = 2; + + /** + * Top right alignment. + */ + const BL = 3; + + /** + * Top right alignment. + */ + const BR = 4; + + /** + * The instance number. This is unique to this instance of the action menu. + * @var int + */ + protected $instance = 0; + + /** + * An array of primary actions. Please use {@link action_menu::add_primary_action()} to add actions. + * @var array + */ + protected $primaryactions = array(); + + /** + * An array of secondary actions. Please use {@link action_menu::add_secondary_action()} to add actions. + * @var array + */ + protected $secondaryactions = array(); + + /** + * An array of attributes added to the container of the action menu. + * Initialised with defaults during construction. + * @var array + */ + public $attributes = array(); + /** + * An array of attributes added to the container of the primary actions. + * Initialised with defaults during construction. + * @var array + */ + public $attributesprimary = array(); + /** + * An array of attributes added to the container of the secondary actions. + * Initialised with defaults during construction. + * @var array + */ + public $attributessecondary = array(); + + /** + * The string to use next to the icon for the action icon relating to the secondary (dropdown) menu. + * @var array + */ + public $actiontext = null; + + /** + * The string to use for the accessible label for the menu. + * @var array + */ + public $actionlabel = null; + + /** + * An icon to use for the toggling the secondary menu (dropdown). + * @var pix_icon + */ + public $actionicon; + + /** + * Any text to use for the toggling the secondary menu (dropdown). + * @var string + */ + public $menutrigger = ''; + + /** + * An array of attributes added to the trigger element of the secondary menu. + * @var array + */ + public $triggerattributes = []; + + /** + * Any extra classes for toggling to the secondary menu. + * @var string + */ + public $triggerextraclasses = ''; + + /** + * Place the action menu before all other actions. + * @var bool + */ + public $prioritise = false; + + /** + * Dropdown menu alignment class. + * @var string + */ + public $dropdownalignment = ''; + + /** + * Constructs the action menu with the given items. + * + * @param array $actions An array of actions (action_menu_link|pix_icon|string). + */ + public function __construct(array $actions = array()) { + static $initialised = 0; + $this->instance = $initialised; + $initialised++; + + $this->attributes = array( + 'id' => 'action-menu-'.$this->instance, + 'class' => 'moodle-actionmenu', + 'data-enhance' => 'moodle-core-actionmenu' + ); + $this->attributesprimary = array( + 'id' => 'action-menu-'.$this->instance.'-menubar', + 'class' => 'menubar', + ); + $this->attributessecondary = array( + 'id' => 'action-menu-'.$this->instance.'-menu', + 'class' => 'menu', + 'data-rel' => 'menu-content', + 'aria-labelledby' => 'action-menu-toggle-'.$this->instance, + 'role' => 'menu' + ); + $this->dropdownalignment = 'dropdown-menu-right'; + foreach ($actions as $action) { + $this->add($action); + } + } + + /** + * Sets the label for the menu trigger. + * + * @param string $label The text + */ + public function set_action_label($label) { + $this->actionlabel = $label; + } + + /** + * Sets the menu trigger text. + * + * @param string $trigger The text + * @param string $extraclasses Extra classes to style the secondary menu toggle. + */ + public function set_menu_trigger($trigger, $extraclasses = '') { + $this->menutrigger = $trigger; + $this->triggerextraclasses = $extraclasses; + } + + /** + * Classes for the trigger menu + */ + const DEFAULT_KEBAB_TRIGGER_CLASSES = 'btn btn-icon d-flex align-items-center justify-content-center no-caret'; + + /** + * Setup trigger as in the kebab menu. + * + * @param string|null $triggername + * @param core_renderer|null $output + * @param string|null $extraclasses extra classes for the trigger {@see self::set_menu_trigger()} + * @throws coding_exception + */ + public function set_kebab_trigger(?string $triggername = null, ?core_renderer $output = null, + ?string $extraclasses = '') { + global $OUTPUT; + if (empty($output)) { + $output = $OUTPUT; + } + $label = $triggername ?? get_string('actions'); + $triggerclasses = self::DEFAULT_KEBAB_TRIGGER_CLASSES . ' ' . $extraclasses; + $icon = $output->pix_icon('i/menu', $label); + $this->set_menu_trigger($icon, $triggerclasses); + } + + /** + * Return true if there is at least one visible link in the menu. + * + * @return bool + */ + public function is_empty() { + return !count($this->primaryactions) && !count($this->secondaryactions); + } + + /** + * Initialises JS required fore the action menu. + * The JS is only required once as it manages all action menu's on the page. + * + * @param moodle_page $page + */ + public function initialise_js(moodle_page $page) { + static $initialised = false; + if (!$initialised) { + $page->requires->yui_module('moodle-core-actionmenu', 'M.core.actionmenu.init'); + $initialised = true; + } + } + + /** + * Adds an action to this action menu. + * + * @param action_link|pix_icon|subpanel|string $action + */ + public function add($action) { + + if ($action instanceof subpanel) { + $this->add_secondary_subpanel($action); + } else if ($action instanceof action_link) { + if ($action->primary) { + $this->add_primary_action($action); + } else { + $this->add_secondary_action($action); + } + } else if ($action instanceof pix_icon) { + $this->add_primary_action($action); + } else { + $this->add_secondary_action($action); + } + } + + /** + * Adds a secondary subpanel. + * @param subpanel $subpanel + */ + public function add_secondary_subpanel(subpanel $subpanel) { + $this->secondaryactions[] = $subpanel; + } + + /** + * Adds a primary action to the action menu. + * + * @param action_menu_link|action_link|pix_icon|string $action + */ + public function add_primary_action($action) { + if ($action instanceof action_link || $action instanceof pix_icon) { + $action->attributes['role'] = 'menuitem'; + $action->attributes['tabindex'] = '-1'; + if ($action instanceof action_menu_link) { + $action->actionmenu = $this; + } + } + $this->primaryactions[] = $action; + } + + /** + * Adds a secondary action to the action menu. + * + * @param action_link|pix_icon|string $action + */ + public function add_secondary_action($action) { + if ($action instanceof action_link || $action instanceof pix_icon) { + $action->attributes['role'] = 'menuitem'; + $action->attributes['tabindex'] = '-1'; + if ($action instanceof action_menu_link) { + $action->actionmenu = $this; + } + } + $this->secondaryactions[] = $action; + } + + /** + * Returns the primary actions ready to be rendered. + * + * @param core_renderer $output The renderer to use for getting icons. + * @return array + */ + public function get_primary_actions(core_renderer $output = null) { + global $OUTPUT; + if ($output === null) { + $output = $OUTPUT; + } + $pixicon = $this->actionicon; + $linkclasses = array('toggle-display'); + + $title = ''; + if (!empty($this->menutrigger)) { + $pixicon = ''; + $linkclasses[] = 'textmenu'; + } else { + $title = new lang_string('actionsmenu', 'moodle'); + $this->actionicon = new pix_icon( + 't/edit_menu', + '', + 'moodle', + array('class' => 'iconsmall actionmenu', 'title' => '') + ); + $pixicon = $this->actionicon; + } + if ($pixicon instanceof renderable) { + $pixicon = $output->render($pixicon); + if ($pixicon instanceof pix_icon && isset($pixicon->attributes['alt'])) { + $title = $pixicon->attributes['alt']; + } + } + $string = ''; + if ($this->actiontext) { + $string = $this->actiontext; + } + $label = ''; + if ($this->actionlabel) { + $label = $this->actionlabel; + } else { + $label = $title; + } + $actions = $this->primaryactions; + $attributes = array( + 'class' => implode(' ', $linkclasses), + 'title' => $title, + 'aria-label' => $label, + 'id' => 'action-menu-toggle-'.$this->instance, + 'role' => 'menuitem', + 'tabindex' => '-1', + ); + $link = html_writer::link('#', $string . $this->menutrigger . $pixicon, $attributes); + if ($this->prioritise) { + array_unshift($actions, $link); + } else { + $actions[] = $link; + } + return $actions; + } + + /** + * Returns the secondary actions ready to be rendered. + * @return array + */ + public function get_secondary_actions() { + return $this->secondaryactions; + } + + /** + * Sets the selector that should be used to find the owning node of this menu. + * @param string $selector A CSS/YUI selector to identify the owner of the menu. + */ + public function set_owner_selector($selector) { + $this->attributes['data-owner'] = $selector; + } + + /** + * Sets the alignment of the dialogue in relation to button used to toggle it. + * + * @deprecated since Moodle 4.0 + * + * @param int $dialogue One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR. + * @param int $button One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR. + */ + public function set_alignment($dialogue, $button) { + debugging('The method action_menu::set_alignment() is deprecated, use action_menu::set_menu_left()', DEBUG_DEVELOPER); + if (isset($this->attributessecondary['data-align'])) { + // We've already got one set, lets remove the old class so as to avoid troubles. + $class = $this->attributessecondary['class']; + $search = 'align-'.$this->attributessecondary['data-align']; + $this->attributessecondary['class'] = str_replace($search, '', $class); + } + $align = $this->get_align_string($dialogue) . '-' . $this->get_align_string($button); + $this->attributessecondary['data-align'] = $align; + $this->attributessecondary['class'] .= ' align-'.$align; + } + + /** + * Returns a string to describe the alignment. + * + * @param int $align One of action_menu::TL, action_menu::TR, action_menu::BL, action_menu::BR. + * @return string + */ + protected function get_align_string($align) { + switch ($align) { + case self::TL : + return 'tl'; + case self::TR : + return 'tr'; + case self::BL : + return 'bl'; + case self::BR : + return 'br'; + default : + return 'tl'; + } + } + + /** + * Aligns the left corner of the dropdown. + * + */ + public function set_menu_left() { + $this->dropdownalignment = 'dropdown-menu-left'; + } + + /** + * Sets a constraint for the dialogue. + * + * The constraint is applied when the dialogue is shown and limits the display of the dialogue to within the + * element the constraint identifies. + * + * This is required whenever the action menu is displayed inside any CSS element with the .no-overflow class + * (flexible_table and any of it's child classes are a likely candidate). + * + * @deprecated since Moodle 4.3 + * @param string $ancestorselector A snippet of CSS used to identify the ancestor to contrain the dialogue to. + */ + public function set_constraint($ancestorselector) { + debugging('The method set_constraint() is deprecated. Please use the set_boundary() method instead.', DEBUG_DEVELOPER); + $this->set_boundary('window'); + } + + /** + * Set the overflow constraint boundary of the dropdown menu. + * @see https://getbootstrap.com/docs/4.6/components/dropdowns/#options The 'boundary' option in the Bootstrap documentation + * + * @param string $boundary Accepts the values of 'viewport', 'window', or 'scrollParent'. + * @throws coding_exception + */ + public function set_boundary(string $boundary) { + if (!in_array($boundary, ['viewport', 'window', 'scrollParent'])) { + throw new coding_exception("HTMLElement reference boundaries are not supported." . + "Accepted boundaries are 'viewport', 'window', or 'scrollParent'.", DEBUG_DEVELOPER); + } + + $this->triggerattributes['data-boundary'] = $boundary; + } + + /** + * If you call this method the action menu will be displayed but will not be enhanced. + * + * By not displaying the menu enhanced all items will be displayed in a single row. + * + * @deprecated since Moodle 3.2 + */ + public function do_not_enhance() { + debugging('The method action_menu::do_not_enhance() is deprecated, use a list of action_icon instead.', DEBUG_DEVELOPER); + } + + /** + * Returns true if this action menu will be enhanced. + * + * @return bool + */ + public function will_be_enhanced() { + return isset($this->attributes['data-enhance']); + } + + /** + * Sets nowrap on items. If true menu items should not wrap lines if they are longer than the available space. + * + * This property can be useful when the action menu is displayed within a parent element that is either floated + * or relatively positioned. + * In that situation the width of the menu is determined by the width of the parent element which may not be large + * enough for the menu items without them wrapping. + * This disables the wrapping so that the menu takes on the width of the longest item. + * + * @param bool $value If true nowrap gets set, if false it gets removed. Defaults to true. + */ + public function set_nowrap_on_items($value = true) { + $class = 'nowrap-items'; + if (!empty($this->attributes['class'])) { + $pos = strpos($this->attributes['class'], $class); + if ($value === true && $pos === false) { + // The value is true and the class has not been set yet. Add it. + $this->attributes['class'] .= ' '.$class; + } else if ($value === false && $pos !== false) { + // The value is false and the class has been set. Remove it. + $this->attributes['class'] = substr($this->attributes['class'], $pos, strlen($class)); + } + } else if ($value) { + // The value is true and the class has not been set yet. Add it. + $this->attributes['class'] = $class; + } + } + + /** + * Add classes to the action menu for an easier styling. + * + * @param string $class The class to add to attributes. + */ + public function set_additional_classes(string $class = '') { + if (!empty($this->attributes['class'])) { + $this->attributes['class'] .= " ".$class; + } else { + $this->attributes['class'] = $class; + } + } + + /** + * Export for template. + * + * @param renderer_base $output The renderer. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + // Assign a role of menubar to this action menu when: + // - it contains 2 or more primary actions; or + // - if it contains a primary action and secondary actions. + if (count($this->primaryactions) > 1 || (!empty($this->primaryactions) && !empty($this->secondaryactions))) { + $this->attributes['role'] = 'menubar'; + } + $attributes = $this->attributes; + + $data->instance = $this->instance; + + $data->classes = isset($attributes['class']) ? $attributes['class'] : ''; + unset($attributes['class']); + + $data->attributes = array_map(function($key, $value) { + return [ 'name' => $key, 'value' => $value ]; + }, array_keys($attributes), $attributes); + + $data->primary = $this->export_primary_actions_for_template($output); + $data->secondary = $this->export_secondary_actions_for_template($output); + $data->dropdownalignment = $this->dropdownalignment; + + return $data; + } + + /** + * Export the primary actions for the template. + * @param renderer_base $output + * @return stdClass + */ + protected function export_primary_actions_for_template(renderer_base $output): stdClass { + $attributes = $this->attributes; + $attributesprimary = $this->attributesprimary; + + $primary = new stdClass(); + $primary->title = ''; + $primary->prioritise = $this->prioritise; + + $primary->classes = isset($attributesprimary['class']) ? $attributesprimary['class'] : ''; + unset($attributesprimary['class']); + + $primary->attributes = array_map(function ($key, $value) { + return ['name' => $key, 'value' => $value]; + }, array_keys($attributesprimary), $attributesprimary); + $primary->triggerattributes = array_map(function ($key, $value) { + return ['name' => $key, 'value' => $value]; + }, array_keys($this->triggerattributes), $this->triggerattributes); + + $actionicon = $this->actionicon; + if (!empty($this->menutrigger)) { + $primary->menutrigger = $this->menutrigger; + $primary->triggerextraclasses = $this->triggerextraclasses; + if ($this->actionlabel) { + $primary->title = $this->actionlabel; + } else if ($this->actiontext) { + $primary->title = $this->actiontext; + } else { + $primary->title = strip_tags($this->menutrigger); + } + } else { + $primary->title = get_string('actionsmenu'); + $iconattributes = ['class' => 'iconsmall actionmenu', 'title' => $primary->title]; + $actionicon = new pix_icon('t/edit_menu', '', 'moodle', $iconattributes); + } + + // If the menu trigger is within the menubar, assign a role of menuitem. Otherwise, assign as a button. + $primary->triggerrole = 'button'; + if (isset($attributes['role']) && $attributes['role'] === 'menubar') { + $primary->triggerrole = 'menuitem'; + } + + if ($actionicon instanceof pix_icon) { + $primary->icon = $actionicon->export_for_pix(); + if (!empty($actionicon->attributes['alt'])) { + $primary->title = $actionicon->attributes['alt']; + } + } else { + $primary->iconraw = $actionicon ? $output->render($actionicon) : ''; + } + + $primary->actiontext = $this->actiontext ? (string) $this->actiontext : ''; + $primary->items = array_map(function ($item) use ($output) { + $data = (object) []; + if ($item instanceof action_menu_link) { + $data->actionmenulink = $item->export_for_template($output); + } else if ($item instanceof action_menu_filler) { + $data->actionmenufiller = $item->export_for_template($output); + } else if ($item instanceof action_link) { + $data->actionlink = $item->export_for_template($output); + } else if ($item instanceof pix_icon) { + $data->pixicon = $item->export_for_template($output); + } else { + $data->rawhtml = ($item instanceof renderable) ? $output->render($item) : $item; + } + return $data; + }, $this->primaryactions); + return $primary; + } + + /** + * Export the secondary actions for the template. + * @param renderer_base $output + * @return stdClass + */ + protected function export_secondary_actions_for_template(renderer_base $output): stdClass { + $attributessecondary = $this->attributessecondary; + $secondary = new stdClass(); + $secondary->classes = isset($attributessecondary['class']) ? $attributessecondary['class'] : ''; + unset($attributessecondary['class']); + + $secondary->attributes = array_map(function ($key, $value) { + return ['name' => $key, 'value' => $value]; + }, array_keys($attributessecondary), $attributessecondary); + $secondary->items = array_map(function ($item) use ($output) { + $data = (object) [ + 'simpleitem' => true, + ]; + if ($item instanceof action_menu_link) { + $data->actionmenulink = $item->export_for_template($output); + $data->simpleitem = false; + } else if ($item instanceof action_menu_filler) { + $data->actionmenufiller = $item->export_for_template($output); + $data->simpleitem = false; + } else if ($item instanceof subpanel) { + $data->subpanel = $item->export_for_template($output); + $data->simpleitem = false; + } else if ($item instanceof action_link) { + $data->actionlink = $item->export_for_template($output); + } else if ($item instanceof pix_icon) { + $data->pixicon = $item->export_for_template($output); + } else { + $data->rawhtml = ($item instanceof renderable) ? $output->render($item) : $item; + } + return $data; + }, $this->secondaryactions); + return $secondary; + } +} diff --git a/lib/classes/output/action_menu/filler.php b/lib/classes/output/action_menu/filler.php new file mode 100644 index 00000000000..2a2fe9a0d00 --- /dev/null +++ b/lib/classes/output/action_menu/filler.php @@ -0,0 +1,39 @@ +. + +/** + * An action menu filler + * + * @package core + * @category output + * @copyright 2013 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class action_menu_filler extends action_link implements renderable { + + /** + * True if this is a primary action. False if not. + * @var bool + */ + public $primary = true; + + /** + * Constructs the object. + */ + public function __construct() { + $this->attributes['id'] = html_writer::random_id('action_link'); + } +} diff --git a/lib/classes/output/action_menu/link.php b/lib/classes/output/action_menu/link.php new file mode 100644 index 00000000000..d91abd646bc --- /dev/null +++ b/lib/classes/output/action_menu/link.php @@ -0,0 +1,106 @@ +. + +/** + * An action menu action + * + * @package core + * @category output + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class action_menu_link extends action_link implements renderable { + + /** + * True if this is a primary action. False if not. + * @var bool + */ + public $primary = true; + + /** + * The action menu this link has been added to. + * @var action_menu + */ + public $actionmenu = null; + + /** + * The number of instances of this action menu link (and its subclasses). + * + * @deprecated since Moodle 4.4. + * @var int + */ + protected static $instance = 1; + + /** + * Constructs the object. + * + * @param moodle_url $url The URL for the action. + * @param pix_icon|null $icon The icon to represent the action. + * @param string $text The text to represent the action. + * @param bool $primary Whether this is a primary action or not. + * @param array $attributes Any attribtues associated with the action. + */ + public function __construct(moodle_url $url, ?pix_icon $icon, $text, $primary = true, array $attributes = array()) { + parent::__construct($url, $text, null, $attributes, $icon); + $this->primary = (bool)$primary; + $this->add_class('menu-action'); + $this->attributes['role'] = 'menuitem'; + } + + /** + * Export for template. + * + * @param renderer_base $output The renderer. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = parent::export_for_template($output); + + // Ignore what the parent did with the attributes, except for ID and class. + $data->attributes = []; + $attributes = $this->attributes; + unset($attributes['id']); + unset($attributes['class']); + + // Handle text being a renderable. + if ($this->text instanceof renderable) { + $data->text = $this->render($this->text); + } + + $data->showtext = (!$this->icon || $this->primary === false); + + $data->icon = null; + if ($this->icon) { + $icon = $this->icon; + if ($this->primary || !$this->actionmenu->will_be_enhanced()) { + $attributes['title'] = $data->text; + } + $data->icon = $icon ? $icon->export_for_pix() : null; + } + + $data->disabled = !empty($attributes['disabled']); + unset($attributes['disabled']); + + $data->attributes = array_map(function($key, $value) { + return [ + 'name' => $key, + 'value' => $value + ]; + }, array_keys($attributes), $attributes); + + return $data; + } +} diff --git a/lib/classes/output/action_menu/link_primary.php b/lib/classes/output/action_menu/link_primary.php new file mode 100644 index 00000000000..7359fbe4d61 --- /dev/null +++ b/lib/classes/output/action_menu/link_primary.php @@ -0,0 +1,37 @@ +. + +/** + * A primary action menu action + * + * @package core + * @category output + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class action_menu_link_primary extends action_menu_link { + /** + * Constructs the object. + * + * @param moodle_url $url + * @param pix_icon|null $icon + * @param string $text + * @param array $attributes + */ + public function __construct(moodle_url $url, ?pix_icon $icon, $text, array $attributes = array()) { + parent::__construct($url, $icon, $text, true, $attributes); + } +} diff --git a/lib/classes/output/action_menu/link_secondary.php b/lib/classes/output/action_menu/link_secondary.php new file mode 100644 index 00000000000..45780a1f9ea --- /dev/null +++ b/lib/classes/output/action_menu/link_secondary.php @@ -0,0 +1,37 @@ +. + +/** + * A secondary action menu action + * + * @package core + * @category output + * @copyright 2013 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class action_menu_link_secondary extends action_menu_link { + /** + * Constructs the object. + * + * @param moodle_url $url + * @param pix_icon|null $icon + * @param string $text + * @param array $attributes + */ + public function __construct(moodle_url $url, ?pix_icon $icon, $text, array $attributes = array()) { + parent::__construct($url, $icon, $text, false, $attributes); + } +} diff --git a/lib/classes/output/actions/component_action.php b/lib/classes/output/actions/component_action.php new file mode 100644 index 00000000000..72bb07c0480 --- /dev/null +++ b/lib/classes/output/actions/component_action.php @@ -0,0 +1,79 @@ +. + +/** + * Helper class used by other components that involve an action on the page (URL or JS). + * + * @copyright 2009 Nicolas Connault + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * @package core + * @category output + */ +class component_action implements templatable { + + /** + * @var string $event The DOM event that will trigger this action when caught + */ + public $event; + + /** + * @var string A function name to call when the button is clicked + * The JS function you create must have two arguments: + * 1. The event object + * 2. An object/array of arguments ($jsfunctionargs) + */ + public $jsfunction = false; + + /** + * @var array An array of arguments to pass to the JS function + */ + public $jsfunctionargs = array(); + + /** + * Constructor + * @param string $event DOM event + * @param string $jsfunction An optional JS function. Required if jsfunctionargs is given + * @param array $jsfunctionargs An array of arguments to pass to the jsfunction + */ + public function __construct($event, $jsfunction, $jsfunctionargs=array()) { + $this->event = $event; + + $this->jsfunction = $jsfunction; + $this->jsfunctionargs = $jsfunctionargs; + + if (!empty($this->jsfunctionargs)) { + if (empty($this->jsfunction)) { + throw new coding_exception('The component_action object needs a jsfunction value to pass the jsfunctionargs to.'); + } + } + } + + /** + * Export for template. + * + * @param renderer_base $output The renderer. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $args = !empty($this->jsfunctionargs) ? json_encode($this->jsfunctionargs) : false; + return (object) [ + 'event' => $this->event, + 'jsfunction' => $this->jsfunction, + 'jsfunctionargs' => $args, + ]; + } +} diff --git a/lib/classes/output/actions/confirm_action.php b/lib/classes/output/actions/confirm_action.php new file mode 100644 index 00000000000..5ce5b5bfed7 --- /dev/null +++ b/lib/classes/output/actions/confirm_action.php @@ -0,0 +1,47 @@ +. + +/** + * Confirm action + * + * @copyright 2009 Nicolas Connault + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * @package core + * @category output + */ +class confirm_action extends component_action { + /** + * Constructs the confirm action object + * + * @param string $message The message to display to the user when they are shown + * the confirm dialogue. + * @param string $callback Deprecated since 2.7 + * @param string $continuelabel The string to use for he continue button + * @param string $cancellabel The string to use for the cancel button + */ + public function __construct($message, $callback = null, $continuelabel = null, $cancellabel = null) { + if ($callback !== null) { + debugging('The callback argument to new confirm_action() has been deprecated.' . + ' If you need to use a callback, please write Javascript to use moodle-core-notification-confirmation ' . + 'and attach to the provided events.', + DEBUG_DEVELOPER); + } + parent::__construct('click', 'M.util.show_confirm_dialog', array( + 'message' => $message, + 'continuelabel' => $continuelabel, 'cancellabel' => $cancellabel)); + } +} diff --git a/lib/classes/output/actions/popup_action.php b/lib/classes/output/actions/popup_action.php new file mode 100644 index 00000000000..4c0bdc49c4e --- /dev/null +++ b/lib/classes/output/actions/popup_action.php @@ -0,0 +1,106 @@ +. +/** + * Component action for a popup window. + * + * @copyright 2009 Nicolas Connault + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * @package core + * @category output + */ +class popup_action extends component_action { + /** + * @var string The JS function to call for the popup + */ + public $jsfunction = 'openpopup'; + + /** + * @var array An array of parameters that will be passed to the openpopup JS function + */ + public $params = array( + 'height' => 400, + 'width' => 500, + 'top' => 0, + 'left' => 0, + 'menubar' => false, + 'location' => false, + 'scrollbars' => true, + 'resizable' => true, + 'toolbar' => true, + 'status' => true, + 'directories' => false, + 'fullscreen' => false, + 'dependent' => true); + + /** + * Constructor + * + * @param string $event DOM event + * @param moodle_url|string $url A moodle_url object, required if no jsfunction is given + * @param string $name The JS function to call for the popup (default 'popup') + * @param array $params An array of popup parameters + */ + public function __construct($event, $url, $name='popup', $params=array()) { + global $CFG; + + $url = new moodle_url($url); + + if ($name) { + $_name = $name; + if (($_name = preg_replace("/\s/", '_', $_name)) != $name) { + throw new coding_exception('The $name of a popup window shouldn\'t contain spaces - string modified. '. $name .' changed to '. $_name); + $name = $_name; + } + } else { + $name = 'popup'; + } + + foreach ($this->params as $var => $val) { + if (array_key_exists($var, $params)) { + $this->params[$var] = $params[$var]; + } + } + + $attributes = array('url' => $url->out(false), 'name' => $name, 'options' => $this->get_js_options($params)); + if (!empty($params['fullscreen'])) { + $attributes['fullscreen'] = 1; + } + parent::__construct($event, $this->jsfunction, $attributes); + } + + /** + * Returns a string of concatenated option->value pairs used by JS to call the popup window, + * based on this object's variables + * + * @return string String of option->value pairs for JS popup function. + */ + public function get_js_options() { + $jsoptions = ''; + + foreach ($this->params as $var => $val) { + if (is_string($val) || is_int($val)) { + $jsoptions .= "$var=$val,"; + } elseif (is_bool($val)) { + $jsoptions .= ($val) ? "$var," : "$var=0,"; + } + } + + $jsoptions = substr($jsoptions, 0, strlen($jsoptions) - 1); + + return $jsoptions; + } +} diff --git a/lib/classes/output/context_header.php b/lib/classes/output/context_header.php new file mode 100644 index 00000000000..e1c02d8614c --- /dev/null +++ b/lib/classes/output/context_header.php @@ -0,0 +1,147 @@ +. + +/** + * Renderable for the main page header. + * + * @package core + * @category output + * @since 2.9 + * @copyright 2015 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class context_header implements renderable, templatable { + /** + * @var string $heading Main heading. + */ + public $heading; + /** + * @var int $headinglevel Main heading 'h' tag level. + */ + public $headinglevel; + /** + * @var string|null $imagedata HTML code for the picture in the page header. + */ + public $imagedata; + /** + * @var array $additionalbuttons Additional buttons for the header e.g. Messaging button for the user header. + * array elements - title => alternate text for the image, or if no image is available the button text. + * url => Link for the button to head to. Should be a moodle_url. + * image => location to the image, or name of the image in /pix/t/{image name}. + * linkattributes => additional attributes for the element. + * page => page object. Don't include if the image is an external image. + */ + public $additionalbuttons; + /** + * @var string $prefix A string that is before the title. + */ + public $prefix; + + /** + * Constructor. + * + * @param string $heading Main heading data. + * @param int $headinglevel Main heading 'h' tag level. + * @param string|null $imagedata HTML code for the picture in the page header. + * @param string $additionalbuttons Buttons for the header e.g. Messaging button for the user header. + * @param string $prefix Text that precedes the heading. + */ + public function __construct($heading = null, $headinglevel = 1, $imagedata = null, $additionalbuttons = null, $prefix = null) { + + $this->heading = $heading; + $this->headinglevel = $headinglevel; + $this->imagedata = $imagedata; + $this->additionalbuttons = $additionalbuttons; + // If we have buttons then format them. + if (isset($this->additionalbuttons)) { + $this->format_button_images(); + } + $this->prefix = $prefix; + } + + /** + * Adds an array element for a formatted image. + */ + protected function format_button_images() { + + foreach ($this->additionalbuttons as $buttontype => $button) { + $page = $button['page']; + // If no image is provided then just use the title. + if (!isset($button['image'])) { + $this->additionalbuttons[$buttontype]['formattedimage'] = $button['title']; + } else { + // Check to see if this is an internal Moodle icon. + $internalimage = $page->theme->resolve_image_location('t/' . $button['image'], 'moodle'); + if ($internalimage) { + $this->additionalbuttons[$buttontype]['formattedimage'] = 't/' . $button['image']; + } else { + // Treat as an external image. + $this->additionalbuttons[$buttontype]['formattedimage'] = $button['image']; + } + } + + if (isset($button['linkattributes']['class'])) { + $class = $button['linkattributes']['class'] . ' btn'; + } else { + $class = 'btn'; + } + // Add the bootstrap 'btn' class for formatting. + $this->additionalbuttons[$buttontype]['linkattributes'] = array_merge($button['linkattributes'], + array('class' => $class)); + } + } + + /** + * Export for template. + * + * @param renderer_base $output Renderer. + * @return array + */ + public function export_for_template(renderer_base $output): array { + // Heading. + $headingtext = isset($this->heading) ? $this->heading : $output->get_page()->heading; + $heading = $output->heading($headingtext, $this->headinglevel, "h2 mb-0"); + + // Buttons. + if (isset($this->additionalbuttons)) { + $additionalbuttons = []; + foreach ($this->additionalbuttons as $button) { + if (!isset($button->page)) { + // Include js for messaging. + if ($button['buttontype'] === 'togglecontact') { + \core_message\helper::togglecontact_requirejs(); + } + if ($button['buttontype'] === 'message') { + \core_message\helper::messageuser_requirejs(); + } + } + foreach ($button['linkattributes'] as $key => $value) { + $button['attributes'][] = ['name' => $key, 'value' => $value]; + } + $additionalbuttons[] = $button; + } + } + + return [ + 'heading' => $heading, + 'headinglevel' => $this->headinglevel, + 'imagedata' => $this->imagedata, + 'prefix' => $this->prefix, + 'hasadditionalbuttons' => !empty($additionalbuttons), + 'additionalbuttons' => $additionalbuttons ?? [], + ]; + } +} diff --git a/lib/classes/output/core_renderer.php b/lib/classes/output/core_renderer.php new file mode 100644 index 00000000000..df1de09d584 --- /dev/null +++ b/lib/classes/output/core_renderer.php @@ -0,0 +1,4765 @@ +. + +use core\di; +use core\hook\manager as hook_manager; +use core\hook\output\after_standard_main_region_html_generation; +use core\hook\output\before_footer_html_generation; +use core\hook\output\before_html_attributes; +use core\hook\output\before_http_headers; +use core\hook\output\before_standard_footer_html_generation; +use core\hook\output\before_standard_top_of_body_html_generation; +use core_completion\cm_completion_details; +use core_course\output\activity_information; + +/** + * The standard implementation of the core_renderer interface. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * @package core + * @category output + */ +class core_renderer extends renderer_base { + /** + * Do NOT use, please use main_content() ?> + * in layout files instead. + * @deprecated + * @var string used in {@link core_renderer::header()}. + */ + const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]'; + + /** + * @var string Used to pass information from {@link core_renderer::doctype()} to + * {@link core_renderer::standard_head_html()}. + */ + protected $contenttype; + + /** + * @var string Used by {@link core_renderer::redirect_message()} method to communicate + * with {@link core_renderer::header()}. + */ + protected $metarefreshtag = ''; + + /** + * @var string Unique token for the closing HTML + */ + protected $unique_end_html_token; + + /** + * @var string Unique token for performance information + */ + protected $unique_performance_info_token; + + /** + * @var string Unique token for the main content. + */ + protected $unique_main_content_token; + + /** @var custom_menu_item language The language menu if created */ + protected $language = null; + + /** @var string The current selector for an element being streamed into */ + protected $currentselector = ''; + + /** @var string The current element tag which is being streamed into */ + protected $currentelement = ''; + + /** + * Constructor + * + * @param moodle_page $page the page we are doing output for. + * @param string $target one of rendering target constants + */ + public function __construct(moodle_page $page, $target) { + $this->opencontainers = $page->opencontainers; + $this->page = $page; + $this->target = $target; + + $this->unique_end_html_token = '%%ENDHTML-'.sesskey().'%%'; + $this->unique_performance_info_token = '%%PERFORMANCEINFO-'.sesskey().'%%'; + $this->unique_main_content_token = '[MAIN CONTENT GOES HERE - '.sesskey().']'; + } + + /** + * Get the DOCTYPE declaration that should be used with this page. Designed to + * be called in theme layout.php files. + * + * @return string the DOCTYPE declaration that should be used. + */ + public function doctype() { + if ($this->page->theme->doctype === 'html5') { + $this->contenttype = 'text/html; charset=utf-8'; + return "\n"; + + } else if ($this->page->theme->doctype === 'xhtml5') { + $this->contenttype = 'application/xhtml+xml; charset=utf-8'; + return "\n"; + + } else { + // legacy xhtml 1.0 + $this->contenttype = 'text/html; charset=utf-8'; + return ('' . "\n"); + } + } + + /** + * The attributes that should be added to the tag. Designed to + * be called in theme layout.php files. + * + * @return string HTML fragment. + */ + public function htmlattributes() { + $return = get_html_lang(true); + + $hook = new before_html_attributes($this); + + if ($this->page->theme->doctype !== 'html5') { + $hook->add_attribute('xmlns', 'http://www.w3.org/1999/xhtml'); + } + + $hook->process_legacy_callbacks(); + di::get(hook_manager::class)->dispatch($hook); + + foreach ($hook->get_attributes() as $key => $val) { + $val = s($val); + $return .= " $key=\"$val\""; + } + + return $return; + } + + /** + * The standard tags (meta tags, links to stylesheets and JavaScript, etc.) + * that should be included in the tag. Designed to be called in theme + * layout.php files. + * + * @return string HTML fragment. + */ + public function standard_head_html() { + global $CFG, $SESSION, $SITE; + + // Before we output any content, we need to ensure that certain + // page components are set up. + + // Blocks must be set up early as they may require javascript which + // has to be included in the page header before output is created. + foreach ($this->page->blocks->get_regions() as $region) { + $this->page->blocks->ensure_content_created($region, $this); + } + + // Give plugins an opportunity to add any head elements. The callback + // must always return a string containing valid html head content. + + $hook = new \core\hook\output\before_standard_head_html_generation($this); + $hook->process_legacy_callbacks(); + di::get(hook_manager::class)->dispatch($hook); + + // Allow a url_rewrite plugin to setup any dynamic head content. + if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) { + $class = $CFG->urlrewriteclass; + $hook->add_html($class::html_head_setup()); + } + + $hook->add_html('' . "\n"); + $hook->add_html('' . "\n"); + // This is only set by the {@link redirect()} method + $hook->add_html($this->metarefreshtag); + + // Check if a periodic refresh delay has been set and make sure we arn't + // already meta refreshing + if ($this->metarefreshtag=='' && $this->page->periodicrefreshdelay!==null) { + $hook->add_html( + html_writer::empty_tag('meta', [ + 'http-equiv' => 'refresh', + 'content' => $this->page->periodicrefreshdelay . ';url='.$this->page->url->out(), + ]), + ); + } + + $output = $hook->get_output(); + + // Set up help link popups for all links with the helptooltip class + $this->page->requires->js_init_call('M.util.help_popups.setup'); + + $focus = $this->page->focuscontrol; + if (!empty($focus)) { + if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) { + // This is a horrifically bad way to handle focus but it is passed in + // through messy formslib::moodleform + $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2])); + } else if (strpos($focus, '.')!==false) { + // Old style of focus, bad way to do it + 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); + $this->page->requires->js_function_call('old_onload_focus', explode('.', $focus, 2)); + } else { + // Focus element with given id + $this->page->requires->js_function_call('focuscontrol', array($focus)); + } + } + + // Get the theme stylesheet - this has to be always first CSS, this loads also styles.css from all plugins; + // any other custom CSS can not be overridden via themes and is highly discouraged + $urls = $this->page->theme->css_urls($this->page); + foreach ($urls as $url) { + $this->page->requires->css_theme($url); + } + + // Get the theme javascript head and footer + if ($jsurl = $this->page->theme->javascript_url(true)) { + $this->page->requires->js($jsurl, true); + } + if ($jsurl = $this->page->theme->javascript_url(false)) { + $this->page->requires->js($jsurl); + } + + // Get any HTML from the page_requirements_manager. + $output .= $this->page->requires->get_head_code($this->page, $this); + + // List alternate versions. + foreach ($this->page->alternateversions as $type => $alt) { + $output .= html_writer::empty_tag('link', array('rel' => 'alternate', + 'type' => $type, 'title' => $alt->title, 'href' => $alt->url)); + } + + // Add noindex tag if relevant page and setting applied. + $allowindexing = isset($CFG->allowindexing) ? $CFG->allowindexing : 0; + $loginpages = array('login-index', 'login-signup'); + if ($allowindexing == 2 || ($allowindexing == 0 && in_array($this->page->pagetype, $loginpages))) { + if (!isset($CFG->additionalhtmlhead)) { + $CFG->additionalhtmlhead = ''; + } + $CFG->additionalhtmlhead .= ''; + } + + if (!empty($CFG->additionalhtmlhead)) { + $output .= "\n".$CFG->additionalhtmlhead; + } + + if ($this->page->pagelayout == 'frontpage') { + $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML))); + if (!empty($summary)) { + $output .= "\n"; + } + } + + return $output; + } + + /** + * The standard tags (typically skip links) that should be output just inside + * the start of the tag. Designed to be called in theme layout.php files. + * + * @return string HTML fragment. + */ + public function standard_top_of_body_html() { + global $CFG; + $output = $this->page->requires->get_top_of_body_code($this); + if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) { + $output .= "\n".$CFG->additionalhtmltopofbody; + } + + // Allow components to add content to the top of the body. + $hook = new before_standard_top_of_body_html_generation($this, $output); + $hook->process_legacy_callbacks(); + di::get(hook_manager::class)->dispatch($hook); + $output = $hook->get_output(); + + $output .= $this->maintenance_warning(); + + return $output; + } + + /** + * Scheduled maintenance warning message. + * + * Note: This is a nasty hack to display maintenance notice, this should be moved + * to some general notification area once we have it. + * + * @return string + */ + public function maintenance_warning() { + global $CFG; + + $output = ''; + if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) { + $timeleft = $CFG->maintenance_later - time(); + // If timeleft less than 30 sec, set the class on block to error to highlight. + $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning'; + $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-3 alert'); + $a = new stdClass(); + $a->hour = (int)($timeleft / 3600); + $a->min = (int)(floor($timeleft / 60) % 60); + $a->sec = (int)($timeleft % 60); + if ($a->hour > 0) { + $output .= get_string('maintenancemodeisscheduledlong', 'admin', $a); + } else { + $output .= get_string('maintenancemodeisscheduled', 'admin', $a); + } + + $output .= $this->box_end(); + $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer', + array(array('timeleftinsec' => $timeleft))); + $this->page->requires->strings_for_js( + array('maintenancemodeisscheduled', 'maintenancemodeisscheduledlong', 'sitemaintenance'), + 'admin'); + } + return $output; + } + + /** + * content that should be output in the footer area + * of the page. Designed to be called in theme layout.php files. + * + * @return string HTML fragment. + */ + public function standard_footer_html() { + if (during_initial_install()) { + // Debugging info can not work before install is finished, + // in any case we do not want any links during installation! + return ''; + } + + $hook = new before_standard_footer_html_generation($this); + $hook->process_legacy_callbacks(); + di::get(hook_manager::class)->dispatch($hook); + $output = $hook->get_output(); + + if ($this->page->devicetypeinuse == 'legacy') { + // The legacy theme is in use print the notification + $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse')); + } + + // Get links to switch device types (only shown for users not on a default device) + $output .= $this->theme_switch_links(); + + return $output; + } + + /** + * Performance information and validation links for debugging. + * + * @return string HTML fragment. + */ + public function debug_footer_html() { + global $CFG, $SCRIPT; + $output = ''; + + if (during_initial_install()) { + // Debugging info can not work before install is finished. + return $output; + } + + // This function is normally called from a layout.php file + // but some of the content won't be known until later, so we return a placeholder + // for now. This will be replaced with the real content in the footer. + $output .= $this->unique_performance_info_token; + + if (!empty($CFG->debugpageinfo)) { + $output .= '
' . get_string('pageinfodebugsummary', 'core_admin', + $this->page->debug_summary()) . '
'; + } + if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) { // Only in developer mode + + // Add link to profiling report if necessary + if (function_exists('profiling_is_running') && profiling_is_running()) { + $txt = get_string('profiledscript', 'admin'); + $title = get_string('profiledscriptview', 'admin'); + $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT); + $link= '
' . $txt . ''; + $output .= '
' . $link . '
'; + } + $purgeurl = new moodle_url('/admin/purgecaches.php', array('confirm' => 1, + 'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false))); + $output .= '
' . + html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '
'; + + // Reactive module debug panel. + $output .= $this->render_from_template('core/local/reactive/debugpanel', []); + } + if (!empty($CFG->debugvalidators)) { + $siteurl = qualified_me(); + $nuurl = new moodle_url('https://validator.w3.org/nu/', ['doc' => $siteurl, 'showsource' => 'yes']); + $waveurl = new moodle_url('https://wave.webaim.org/report#/' . urlencode($siteurl)); + $validatorlinks = [ + html_writer::link($nuurl, get_string('validatehtml')), + html_writer::link($waveurl, get_string('wcagcheck')) + ]; + $validatorlinkslist = html_writer::alist($validatorlinks, ['class' => 'list-unstyled ml-1']); + $output .= html_writer::div($validatorlinkslist, 'validators'); + } + return $output; + } + + /** + * Returns standard main content placeholder. + * Designed to be called in theme layout.php files. + * + * @return string HTML fragment. + */ + public function main_content() { + // This is here because it is the only place we can inject the "main" role over the entire main content area + // without requiring all theme's to manually do it, and without creating yet another thing people need to + // remember in the theme. + // This is an unfortunate hack. DO NO EVER add anything more here. + // DO NOT add classes. + // DO NOT add an id. + return '
'.$this->unique_main_content_token.'
'; + } + + /** + * Returns information about an activity. + * + * @deprecated since Moodle 4.3 MDL-78744 + * @todo MDL-78926 This method will be deleted in Moodle 4.7 + * @param cm_info $cminfo The course module information. + * @param cm_completion_details $completiondetails The completion details for this activity module. + * @param array $activitydates The dates for this activity module. + * @return string the activity information HTML. + * @throws coding_exception + */ + public function activity_information(cm_info $cminfo, cm_completion_details $completiondetails, array $activitydates): string { + debugging('activity_information method is deprecated.', DEBUG_DEVELOPER); + if (!$completiondetails->has_completion() && empty($activitydates)) { + // No need to render the activity information when there's no completion info and activity dates to show. + return ''; + } + $activityinfo = new activity_information($cminfo, $completiondetails, $activitydates); + $renderer = $this->page->get_renderer('core', 'course'); + return $renderer->render($activityinfo); + } + + /** + * Returns standard navigation between activities in a course. + * + * @return string the navigation HTML. + */ + public function activity_navigation() { + // First we should check if we want to add navigation. + $context = $this->page->context; + if (($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop') + || $context->contextlevel != CONTEXT_MODULE) { + return ''; + } + + // If the activity is in stealth mode, show no links. + if ($this->page->cm->is_stealth()) { + return ''; + } + + $course = $this->page->cm->get_course(); + $courseformat = course_get_format($course); + + // If the theme implements course index and the current course format uses course index and the current + // page layout is not 'frametop' (this layout does not support course index), show no links. + if ($this->page->theme->usescourseindex && $courseformat->uses_course_index() && + $this->page->pagelayout !== 'frametop') { + return ''; + } + + // Get a list of all the activities in the course. + $modules = get_fast_modinfo($course->id)->get_cms(); + + // Put the modules into an array in order by the position they are shown in the course. + $mods = []; + $activitylist = []; + foreach ($modules as $module) { + // Only add activities the user can access, aren't in stealth mode and have a url (eg. mod_label does not). + if (!$module->uservisible || $module->is_stealth() || empty($module->url)) { + continue; + } + $mods[$module->id] = $module; + + // No need to add the current module to the list for the activity dropdown menu. + if ($module->id == $this->page->cm->id) { + continue; + } + // Module name. + $modname = $module->get_formatted_name(); + // Display the hidden text if necessary. + if (!$module->visible) { + $modname .= ' ' . get_string('hiddenwithbrackets'); + } + // Module URL. + $linkurl = new moodle_url($module->url, array('forceview' => 1)); + // Add module URL (as key) and name (as value) to the activity list array. + $activitylist[$linkurl->out(false)] = $modname; + } + + $nummods = count($mods); + + // If there is only one mod then do nothing. + if ($nummods == 1) { + return ''; + } + + // Get an array of just the course module ids used to get the cmid value based on their position in the course. + $modids = array_keys($mods); + + // Get the position in the array of the course module we are viewing. + $position = array_search($this->page->cm->id, $modids); + + $prevmod = null; + $nextmod = null; + + // Check if we have a previous mod to show. + if ($position > 0) { + $prevmod = $mods[$modids[$position - 1]]; + } + + // Check if we have a next mod to show. + if ($position < ($nummods - 1)) { + $nextmod = $mods[$modids[$position + 1]]; + } + + $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod, $activitylist); + $renderer = $this->page->get_renderer('core', 'course'); + return $renderer->render($activitynav); + } + + /** + * The standard tags (typically script tags that are not needed earlier) that + * should be output after everything else. Designed to be called in theme layout.php files. + * + * @return string HTML fragment. + */ + public function standard_end_of_body_html() { + global $CFG; + + // This function is normally called from a layout.php file in {@link core_renderer::header()} + // but some of the content won't be known until later, so we return a placeholder + // for now. This will be replaced with the real content in {@link core_renderer::footer()}. + $output = ''; + if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) { + $output .= "\n".$CFG->additionalhtmlfooter; + } + $output .= $this->unique_end_html_token; + return $output; + } + + /** + * The standard HTML that should be output just before the