From 5ade4c198751a3b137196bd3ee913df866e67f91 Mon Sep 17 00:00:00 2001 From: Alexander Bias Date: Fri, 27 Dec 2024 19:53:44 +0100 Subject: [PATCH 1/6] Move existing QuickForm colorpicker class to a better place for better re-usability --- classes/form/smartmenu_item_edit_form.php | 6 +++--- .../formelement/colorpicker.php | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) rename form/element-colorpicker.php => classes/formelement/colorpicker.php (93%) diff --git a/classes/form/smartmenu_item_edit_form.php b/classes/form/smartmenu_item_edit_form.php index c7547e890ed..85a2e1b99a5 100644 --- a/classes/form/smartmenu_item_edit_form.php +++ b/classes/form/smartmenu_item_edit_form.php @@ -47,11 +47,11 @@ public function definition() { global $DB, $PAGE, $CFG; // Require and register the QuickForm colorpicker element. - require_once($CFG->dirroot.'/theme/boost_union/form/element-colorpicker.php'); + require_once($CFG->dirroot.'/theme/boost_union/classes/formelement/colorpicker.php'); \MoodleQuickForm::registerElementType( 'theme_boost_union_colorpicker', - $CFG->dirroot.'/theme/boost_union/form/element-colorpicker.php', - 'moodlequickform_themeboostunion_colorpicker' + $CFG->dirroot.'/theme/boost_union/classes/formelement/colorpicker.php', + '\theme_boost_union\formelement\colorpicker' ); // Get an easier handler for the form. diff --git a/form/element-colorpicker.php b/classes/formelement/colorpicker.php similarity index 93% rename from form/element-colorpicker.php rename to classes/formelement/colorpicker.php index 7a17f606572..c45f4f23b64 100644 --- a/form/element-colorpicker.php +++ b/classes/formelement/colorpicker.php @@ -15,13 +15,18 @@ // along with Moodle. If not, see . /** - * Theme Boost Union Login - Form element for color picker + * Theme Boost Union - Form element for color picker * * @package theme_boost_union * @copyright 2023 bdecent GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace theme_boost_union\formelement; + +use MoodleQuickForm_text; +use renderer_base; + defined('MOODLE_INTERNAL') || die(); require_once('HTML/QuickForm/input.php'); @@ -35,9 +40,9 @@ * @copyright 2023 bdecent GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class moodlequickform_themeboostunion_colorpicker extends MoodleQuickForm_text implements \core\output\templatable { +class colorpicker extends MoodleQuickForm_text implements \core\output\templatable { - use templatable_form_element { + use \templatable_form_element { export_for_template as export_for_template_base; } From fc5e2b5bd2f52c3c215821afb65ec248dd19b94a Mon Sep 17 00:00:00 2001 From: Wehr Mario Date: Tue, 18 Jul 2023 17:36:18 +0200 Subject: [PATCH 2/6] Allow overwriting of brand colors in flavours, resolves #155 --- classes/form/colourpicker_form_element.php | 209 +++++++++++++ classes/form/flavour_edit_form.php | 134 +++++++- db/install.xml | 5 + db/upgrade.php | 49 +++ flavours/css.php | 343 +++++++++++++++++++++ flavours/edit.php | 3 + flavours/flavourslib.php | 30 ++ flavours/styles.php | 88 ------ lang/en/theme_boost_union.php | 14 +- lib.php | 74 ++++- locallib.php | 29 -- 11 files changed, 847 insertions(+), 131 deletions(-) create mode 100644 classes/form/colourpicker_form_element.php create mode 100644 flavours/css.php delete mode 100644 flavours/styles.php diff --git a/classes/form/colourpicker_form_element.php b/classes/form/colourpicker_form_element.php new file mode 100644 index 00000000000..292848c6181 --- /dev/null +++ b/classes/form/colourpicker_form_element.php @@ -0,0 +1,209 @@ +. + +namespace theme_boost_union\form; + +use renderer_base; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/lib/form/editor.php'); + +/** + * Form element for handling the colour picker. + * + * @package theme_boost_union + * @copyright 2023 Mario Wehr + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class theme_boost_union_colourpicker_form_element extends \HTML_QuickForm_element implements \templatable { + + // String html for help button, if empty then no help. + public $_helpbutton = ''; + + /** + * Class constructor + * + * @param string Name of the element + * @param mixed Label(s) for the element + * @param mixed Associative array of tag attributes or HTML attributes name="value" pairs + * @since 1.0 + * @access public + * @return void + */ + public function __construct($elementname=null, $elemenlabel=null, $attributes=null) { + parent::__construct($elementname, $elemenlabel, $attributes); + $this->_type = 'static'; + } + + /** + * Sets name of editor + * + * @param string $name name of element + */ + // @codingStandardsIgnoreStart + public function setName($name) { + $this->updateAttributes(array('name' => $name)); + } + // @codingStandardsIgnoreEnd + /** + * Returns name of element + * + * @return string + */ + // @codingStandardsIgnoreStart + function getName() { + return $this->getAttribute('name'); + } + // @codingStandardsIgnoreEnd + /** + * get html for help button + * + * @return string html for help button + */ + // @codingStandardsIgnoreStart + public function getHelpButton() { + return $this->_helpbutton; + } + // @codingStandardsIgnoreEnd + /** + * Sets the value of the form element + * + * @param string $value + */ + // @codingStandardsIgnoreStart + public function setvalue($value) { + $this->updateAttributes(array('value' => $value)); + } + // @codingStandardsIgnoreEnd + /** + * Gets the value of the form element + */ + public function getvalue() { + return $this->getAttribute('value'); + } + + /** + * Returns the html string to display this element. + * + * @return string + */ + public function tohtml() { + global $PAGE, $OUTPUT; + + $icon = new \pix_icon('i/loading', get_string('loading', 'admin'), 'moodle', ['class' => 'loadingicon']); + $context = (object) [ + 'icon' => $icon->export_for_template($OUTPUT), + 'name' => $this->getAttribute('name'), + 'id' => $this->getAttribute('id'), + 'value' => $this->getAttribute('value'), + "readonly" => false, + 'haspreviewconfig' => false, + ]; + $PAGE->requires->js_init_call('M.util.init_colour_picker', array($this->getAttribute('id'), null)); + return $OUTPUT->render_from_template('core_admin/setting_configcolourpicker', $context); + } + + /** + * Function to export the renderer data in a format that is suitable for a mustache template. + * + * @param \renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return \stdClass|array + */ + public function export_for_template(renderer_base $output) { + $context['html'] = $this->toHtml(); + $context['id'] = $this->getAttribute('id'); + return $context; + } +} + +/** + * Colour picker validation rule + * + * @package theme_boost_union + * @copyright 2023 Mario Wehr + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class theme_boost_union_colourpicker_rule extends \HTML_QuickForm_Rule { + + /** + * Validates the colour that was entered by the user + * + * @param string $value Value to check + * @param int|string|array $options Not used yet + * @return bool true if value is not empty + */ + public function validate($value, $options = null) { + + // List of valid HTML colour names. + $colornames = array( + 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', + 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', + 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', + 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', + 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', + 'darkgrey', 'darkgreen', 'darkkhaki', 'darkmagenta', + 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', + 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', + 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', + 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', + 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', + 'ghostwhite', 'gold', 'goldenrod', 'gray', 'grey', 'green', + 'greenyellow', 'honeydew', 'hotpink', 'indianred', 'indigo', + 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', + 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', + 'lightgoldenrodyellow', 'lightgray', 'lightgrey', 'lightgreen', + 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', + 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', + 'lime', 'limegreen', 'linen', 'magenta', 'maroon', + 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', + 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', + 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', + 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', + 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', + 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', + 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'red', + 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', + 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', + 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', + 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', + 'whitesmoke', 'yellow', 'yellowgreen' + ); + + if (preg_match('/^#?([[:xdigit:]]{3}){1,2}$/', $value)) { + if (strpos($value, '#') !== 0) { + $value = '#'.$value; + } + return $value; + } else if (in_array(strtolower($value), $colornames)) { + return $value; + } else if (preg_match('/rgb\(\d{0,3}%?, ?\d{0,3}%?, ?\d{0,3}%?\)/i', $value)) { + return $value; + } else if (preg_match('/rgba\(\d{0,3}%?, ?\d{0,3}%?, ?\d{0,3}%?, ?\d(\.\d)?\)/i', $value)) { + return $value; + } else if (preg_match('/hsl\(\d{0,3}, ?\d{0,3}%, ?\d{0,3}%\)/i', $value)) { + return $value; + } else if (preg_match('/hsla\(\d{0,3}, ?\d{0,3}%,\d{0,3}%, ?\d(\.\d)?\)/i', $value)) { + return $value; + } else if (($value == 'transparent') || ($value == 'currentColor') || ($value == 'inherit')) { + return $value; + } else { + return false; + } + } +} diff --git a/classes/form/flavour_edit_form.php b/classes/form/flavour_edit_form.php index e6d1a115e97..4193cc9e7f6 100644 --- a/classes/form/flavour_edit_form.php +++ b/classes/form/flavour_edit_form.php @@ -26,6 +26,9 @@ namespace theme_boost_union\form; +use ScssPhp\ScssPhp\Exception\CompilerException; +use ScssPhp\ScssPhp\Exception\ParserException; + defined('MOODLE_INTERNAL') || die(); // Require forms library. @@ -51,6 +54,8 @@ class flavour_edit_form extends \moodleform { * @throws \coding_exception */ public function definition() { + global $CFG, $OUTPUT; + // Get an easier handler for the form. $mform = $this->_form; @@ -120,12 +125,98 @@ public function definition() { ]); $mform->addHelpButton('flavours_look_backgroundimage', 'flavoursbackgroundimage', 'theme_boost_union'); + // Create brand colors heading. + $context = new \stdClass(); + $context->title = get_string('brandcolorsheading', 'theme_boost_union', null, true); + $mform->addElement( + 'html', + '
'. $OUTPUT->render_from_template('core_admin/setting_heading', $context), '
' + ); + + // Register custom colourpicker. + \MoodleQuickForm::registerElementType('boost_union_colourpicker', + $CFG->dirroot . '/theme/boost_union/classes/form/colourpicker_form_element.php', + 'theme_boost_union\form\theme_boost_union_colourpicker_form_element'); + // Register validation rule for colourpicker. + \MoodleQuickForm::registerRule('theme_boost_union_colourpicker_rule', + null, + 'theme_boost_union\form\theme_boost_union_colourpicker_rule', + $CFG->dirroot . '/theme/boost_union/classes/form/colourpicker_form_element.php'); + + // Add brandcolour as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'brandcolor', + get_string('brandcolor', 'theme_boost'), + [ + 'id' => 'colourpicker_brandcolour', + ]); + $mform->setDefault('brandcolor', ''); + $mform->addHelpButton('brandcolor', 'flavoursbrandcolour', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('brandcolor', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + + // Create Bootstrap colors heading. + $context = new \stdClass(); + $context->title = get_string('bootstrapcolorsheading', 'theme_boost_union', null, true); + $mform->addElement( + 'html', + '
'. $OUTPUT->render_from_template('core_admin/setting_heading', $context), '
' + ); + + // Add Bootstrap color for 'success' as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'bootstrapcolorsuccess', + get_string('bootstrapcolorsuccesssetting', 'theme_boost_union'), + [ + 'id' => 'colourpicker-bootstrapcolorsuccess', + ]); + $mform->setDefault('bootstrapcolorsuccess', ''); + $mform->addHelpButton('bootstrapcolorsuccess', 'flavoursbootstrapcolorsuccess', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('bootstrapcolorsuccess', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + + // Add Bootstrap color for 'info' as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'bootstrapcolorinfo', + get_string('bootstrapcolorinfosetting', 'theme_boost_union'), + [ + 'id' => 'colourpicker-bootstrapcolorinfo', + ]); + $mform->setDefault('bootstrapcolorinfo', ''); + $mform->addHelpButton('bootstrapcolorinfo', 'flavoursbootstrapcolorinfo', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('bootstrapcolorinfo', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + + // Add Bootstrap color for 'warning' as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'bootstrapcolorwarning', + get_string('bootstrapcolorwarningsetting', 'theme_boost_union'), + [ + 'id' => 'colourpicker-bootstrapcolorwarning', + ]); + $mform->addHelpButton('bootstrapcolorwarning', 'flavoursbootstrapcolorwarning', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('bootstrapcolorwarning', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + + // Add Bootstrap color for 'danger' as colourpicker element. + $mform->addElement( + 'boost_union_colourpicker', + 'bootstrapcolordanger', + get_string('bootstrapcolordangersetting', 'theme_boost_union'), + [ + 'id' => 'colourpicker-bbootstrapcolordanger', + ]); + $mform->setDefault('bootstrapcolordanger', ''); + $mform->addHelpButton('bootstrapcolordanger', 'flavoursbootstrapcolordanger', 'theme_boost_union'); + // Add validation rule. + $mform->addRule('bootstrapcolordanger', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); + // Add custom css as textarea element. - // Note: In the current state of implementation, this setting only allows the usage of custom CSS, not SCSS. - // It will be appended to the stack of CSS code which is shipped to the browser. - // There is a follow-up issue on Github to add SCSS support. - // When this is realized, the widget's title string should be changed to 'theme_boost/rawscss'. - $mform->addElement('textarea', 'look_rawscss', get_string('flavourscustomcss', 'theme_boost_union'), ['rows' => 15]); + $mform->addElement('textarea', 'look_rawscss', get_string('rawscss', 'theme_boost'), array('rows' => 15)); $mform->setType('title', PARAM_TEXT); $mform->addHelpButton('look_rawscss', 'flavourscustomcss', 'theme_boost_union'); @@ -184,4 +275,37 @@ public function definition() { // Add the action buttons. $this->add_action_buttons(); } + + /** + * Theme Boost Union - Flavours edit form validation + * + * @package theme_boost_union + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK (true allowed for backwards compatibility too). + */ + public function validation($data, $files) { + global $PAGE; + + $errors = []; + + if (!empty($data['look_rawscss']) && !empty($data)) { + + $scss = new \core_scss(); + try { + if ($scssproperties = $PAGE->theme->get_scss_property()) { + $scss->setImportPaths($scssproperties[0]); + } + $scss->compile($data['look_rawscss']); + } catch (ParserException $e) { + $errors['look_rawscss'] = get_string('scssinvalid', 'admin', $e->getMessage()); + } catch (CompilerException $e) { + $errors['look_rawscss'] = get_string('scssinvalid', 'admin', $e->getMessage()); + } + $scss = null; + unset($scss); + } + return $errors; + } } diff --git a/db/install.xml b/db/install.xml index a1bb583694c..aa198af0b77 100644 --- a/db/install.xml +++ b/db/install.xml @@ -21,6 +21,11 @@ + + + + + diff --git a/db/upgrade.php b/db/upgrade.php index 8ada2daee60..8682bef5c95 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -348,5 +348,54 @@ function xmldb_theme_boost_union_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2024100702, 'theme', 'boost_union'); } + if ($oldversion < 2024100705) { + + // Define table theme_boost_union_flavours to be altered. + $table = new xmldb_table('theme_boost_union_flavours'); + + // Define field brandcolor to be added to theme_boost_union_flavours. + $field = new xmldb_field('brandcolor', XMLDB_TYPE_CHAR, '32', null, null, null, null); + + // Conditionally launch add field brandcolor. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field bootstrapcolorsuccess to be added to theme_boost_union_flavours. + $field = new xmldb_field('bootstrapcolorsuccess', XMLDB_TYPE_CHAR, '32', null, null, null, null); + + // Conditionally launch add field bootstrapcolorsuccess. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field bootstrapcolorinfo to be added to theme_boost_union_flavours. + $field = new xmldb_field('bootstrapcolorinfo', XMLDB_TYPE_CHAR, '32', null, null, null, null); + + // Conditionally launch add field bootstrapcolorinfo. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field bootstrapcolorwarning to be added to theme_boost_union_flavours. + $field = new xmldb_field('bootstrapcolorwarning', XMLDB_TYPE_CHAR, '32', null, null, null, null); + + // Conditionally launch add field bootstrapcolorwarning. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field bootstrapcolordanger to be added to theme_boost_union_flavours. + $field = new xmldb_field('bootstrapcolordanger', XMLDB_TYPE_CHAR, '32', null, null, null, null); + + // Conditionally launch add field bootstrapcolordanger. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Boost_union savepoint reached. + upgrade_plugin_savepoint(true, 2024100705, 'theme', 'boost_union'); + } + return true; } diff --git a/flavours/css.php b/flavours/css.php new file mode 100644 index 00000000000..a7465090c42 --- /dev/null +++ b/flavours/css.php @@ -0,0 +1,343 @@ +. + +/** + * This file is responsible for serving the one huge CSS of each theme. + * + * @package core + * @copyright 2009 Petr Skoda (skodak) {@link http://skodak.org} modified by 2023 Mario Wehr for bost union theme + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Disable moodle specific debug messages and any errors in output, +// comment out when debugging or better look into error log! +define('NO_DEBUG_DISPLAY', true); + +define('ABORT_AFTER_CONFIG', true); +require('../../../config.php'); +require_once($CFG->dirroot.'/lib/csslib.php'); + +if ($slashargument = min_get_slash_argument()) { + $slashargument = ltrim($slashargument, '/'); + if (substr_count($slashargument, '/') < 2) { + css_send_css_not_found(); + } + + if (strpos($slashargument, '_s/') === 0) { + // Can't use SVG. + $slashargument = substr($slashargument, 3); + $usesvg = false; + } else { + $usesvg = true; + } + + list($themename, $rev, $flavourid, $type) = explode('/', $slashargument, 4); + $themename = min_clean_param($themename, 'SAFEDIR'); + $rev = min_clean_param($rev, 'RAW'); + $flavourid = min_clean_param($flavourid, 'SAFEDIR'); + $typeid = min_clean_param($type, 'SAFEDIR'); + +} else { + $themename = min_optional_param('theme', 'standard', 'SAFEDIR'); + $rev = min_optional_param('rev', 0, 'RAW'); + $type = min_optional_param('type', 'all', 'SAFEDIR'); + $usesvg = (bool)min_optional_param('svg', '1', 'INT'); +} + +// Check if we received a theme sub revision which allows us +// to handle local caching on a per theme basis. +$values = explode('_', $rev); +$rev = min_clean_param(array_shift($values), 'INT'); +$themesubrev = array_shift($values); + +if (!is_null($themesubrev)) { + $themesubrev = min_clean_param($themesubrev, 'INT'); +} + +// Check that type fits into the expected values. +if (!in_array($type, ['all', 'all-rtl', 'editor', 'editor-rtl'])) { + css_send_css_not_found(); +} + +// @codingStandardsIgnoreStart +if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { + // The theme exists in standard location - ok. +} else if (!empty($CFG->themedir) && file_exists("$CFG->themedir/$themename/config.php")) { + // Alternative theme location contains this theme - ok. + // @codingStandardsIgnoreEnd +} else { + header('HTTP/1.0 404 not found'); + die('Theme was not found, sorry.'); +} + +$candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css"; +$candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $flavourid, $usesvg); +$etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg); + +if (file_exists($candidatesheet)) { + if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + // We do not actually need to verify the etag value because our files + // never change in cache because we increment the rev counter. + css_send_unmodified(filemtime($candidatesheet), $etag); + } + css_send_cached_css($candidatesheet, $etag); +} + +// Ok, now we need to start normal moodle script, we need to load all libs and $DB. +define('ABORT_AFTER_CONFIG_CANCEL', true); + +define('NO_MOODLE_COOKIES', true); // Session not used here. +define('NO_UPGRADE_CHECK', true); // Ignore upgrade check. + +require("$CFG->dirroot/lib/setup.php"); + +$theme = theme_config::load($themename); +$theme->force_svg_use($usesvg); +$theme->set_rtl_mode(substr($type, -4) === '-rtl'); + +$themerev = theme_get_revision(); +$currentthemesubrev = theme_get_sub_revision_for_theme($themename); + +$cache = true; +// If the client is requesting a revision that doesn't match both +// the global theme revision and the theme specific revision then +// tell the browser not to cache this style sheet because it's +// likely being regenerated. +if ($themerev <= 0 || $themerev != $rev || $themesubrev != $currentthemesubrev) { + $rev = $themerev; + $themesubrev = $currentthemesubrev; + $cache = false; + + $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css"; + $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $flavourid, $usesvg); + $etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg); +} + +make_localcache_directory('theme', false); + +if ($type === 'editor' || $type === 'editor-rtl') { + $csscontent = $theme->get_css_content_editor(); + + if ($cache) { + css_store_css($theme, $candidatesheet, $csscontent); + css_send_cached_css($candidatesheet, $etag); + } else { + css_send_uncached_css($csscontent); + } + +} + +if (($fallbacksheet = theme_styles_fallback_content($theme)) && !$theme->has_css_cached_content()) { + // The theme is not yet available and a fallback is available. + // Return the fallback immediately, specifying the Content-Length, then generate in the background. + $css = file_get_contents($fallbacksheet); + css_send_temporary_css($css); + + // The fallback content has now been sent. + // There will be an attempt to generate the content, but it should not be served. + // The Content-Length above means that the client will disregard it anyway. + $sendaftergeneration = false; + + // There may be another client currently holding a lock and generating the stylesheet. + // Use a very low lock timeout as the connection will be ended immediately afterwards. + $locktimeout = 1; +} else { + // There is no fallback content to be issued here, therefore the generated content must be output. + $sendaftergeneration = true; + + // Use a realistic lock timeout as the intention is to avoid lock contention. + $locktimeout = rand(90, 120); +} + +// Attempt to fetch the lock. +$lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content'); +$lock = $lockfactory->get_lock($themename, $locktimeout); + +if ($sendaftergeneration || $lock) { + // Either the lock was successful, or the lock was unsuccessful but the content *must* be sent. + + // The content does not exist locally. + // Generate and save it. + $candidatesheet = theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir, $flavourid); + + if ($lock) { + $lock->release(); + } + + if ($sendaftergeneration) { + if (!$cache) { + // Do not pollute browser caches if invalid revision requested, + // let's ignore legacy IE breakage here too. + css_send_uncached_css(file_get_contents($candidatesheet)); + + } else { + // Real browsers - this is the expected result! + css_send_cached_css($candidatesheet, $etag); + } + } +} + +/** + * Generate the theme CSS and store it. + * + * @param theme_config $theme The theme to be generated + * @param int $rev The theme revision + * @param int $themesubrev The theme sub-revision + * @param string $candidatedir The directory that it should be stored in + * @return string The path that the primary CSS was written to + */ +function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir, $flavourid) { + global $CFG; + require_once("{$CFG->libdir}/filelib.php"); + + // Generate the content first. + if (!$csscontent = theme_boost_union_get_css_cached_content($theme, $flavourid)) { + $csscontent = $theme->get_css_content(); + theme_boost_union_set_css_content_cache($theme, $csscontent); + } + + if ($theme->get_rtl_mode()) { + $type = "all-rtl"; + } else { + $type = "all"; + } + + // Determine the candidatesheet path. + $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $flavourid, $themesubrev, $theme->use_svg_icons()); + + // Store the CSS. + css_store_css($theme, $candidatesheet, $csscontent); + + // Store the fallback CSS in the temp directory. + // This file is used as a fallback when waiting for a theme to compile and is not versioned in any way. + $fallbacksheet = make_temp_directory("theme/{$theme->name}") + . "/" + . theme_styles_get_filename($type, $flavourid, $theme->use_svg_icons()); + css_store_css($theme, $fallbacksheet, $csscontent); + + // Delete older theme subrevision CSS from localcache. + $subrevfiles = glob("{$CFG->localcachedir}/theme/{$rev}/{$theme->name}/css/*.css"); + foreach ($subrevfiles as $subrevfile) { + $cachedsubrev = []; + preg_match("/_([0-9]+)_([0-9]+)\.([0-9]+\.)?css$/", $subrevfile, $cachedsubrev); + $cachedsubrev = isset($cachedsubrev[1]) ? intval($cachedsubrev[1]) : 0; + if ($cachedsubrev > 0 && $cachedsubrev < $themesubrev) { + fulldelete($subrevfile); + } + } + + return $candidatesheet; +} + +/** + * Fetch the preferred fallback content location if available. + * + * @param theme_config $theme The theme to be generated + * @return string The path to the fallback sheet on disk + */ +function theme_styles_fallback_content($theme) { + global $CFG; + + if (!$theme->usefallback) { + // This theme does not support fallbacks. + return false; + } + + $type = $theme->get_rtl_mode() ? 'all-rtl' : 'all'; + $filename = theme_styles_get_filename($type); + + $fallbacksheet = "{$CFG->tempdir}/theme/{$theme->name}/{$filename}"; + if (file_exists($fallbacksheet)) { + return $fallbacksheet; + } + + return false; +} + +/** + * Get the filename for the specified configuration. + * + * @param string $type The requested sheet type + * @param int $themesubrev The theme sub-revision + * @param bool $usesvg Whether SVGs are allowed + * @return string The filename for this sheet + */ +function theme_styles_get_filename($type, $themesubrev = 0, $flavourid = 0, $usesvg = true) { + $filename = $type; + $filename .= ($themesubrev > 0) ? "_{$themesubrev}" : ''; + $filename .= ($flavourid > 0) ? "_{$flavourid}" : ''; + $filename .= $usesvg ? '' : '-nosvg'; + + return "{$filename}.css"; +} + +/** + * Determine the correct etag for the specified configuration. + * + * @param string $themename The name of the theme + * @param int $rev The revision number + * @param string $type The requested sheet type + * @param int $themesubrev The theme sub-revision + * @param bool $usesvg Whether SVGs are allowed + * @return string The etag to use for this request + */ +function theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg) { + $etag = [$rev, $themename, $type, $themesubrev]; + + if (!$usesvg) { + $etag[] = 'nosvg'; + } + + return sha1(implode('/', $etag)); +} + +/** + * Return cached post processed CSS content. + * + * @return bool|string The cached css content or false if not found. + */ +function theme_boost_union_get_css_cached_content($theme, $flavourid) { + $key = theme_boost_union_get_css_cache_key($theme, $flavourid); + $cache = cache::make('core', 'postprocessedcss'); + + return $cache->get($key); +} + +/** + * Generate the css content cache key. + * + * @return string The post processed css cache key. + */ +function theme_boost_union_get_css_cache_key($theme, $flavourid) { + $nosvg = (!$theme->use_svg_icons()) ? 'nosvg_' : ''; + $rtlmode = ($theme->get_rtl_mode() == true) ? 'rtl' : 'ltr'; + + return $nosvg . $theme->name . '_' . $flavourid . '_' . $rtlmode; +} + +/** + * Set post processed CSS content cache. + * + * @param string $csscontent The post processed CSS content. + * @return bool True if the content was successfully cached. + */ +function theme_boost_union_set_css_content_cache($theme, $csscontent) { + + $cache = cache::make('core', 'postprocessedcss'); + $key = $theme->get_css_cache_key(); + + return $cache->set($key, $csscontent); +} diff --git a/flavours/edit.php b/flavours/edit.php index d1738ecb34f..ca62f8fd9c9 100644 --- a/flavours/edit.php +++ b/flavours/edit.php @@ -282,6 +282,9 @@ $fs->delete_area_files($context->id, 'theme_boost_union', 'flavours_look_favicon', $data->id); $fs->delete_area_files($context->id, 'theme_boost_union', 'flavours_look_backgroundimage', $data->id); + // Delete fallback sheet. Deleting them all because they get generated on building the all.css. + fulldelete($CFG->tempdir . '/theme/boost_union/'); + // Reset theme cache. // This is necessary as the flavour asset URLs contain the themerev. theme_reset_all_caches(); diff --git a/flavours/flavourslib.php b/flavours/flavourslib.php index 1f3c8f49d91..7757afd64cb 100644 --- a/flavours/flavourslib.php +++ b/flavours/flavourslib.php @@ -313,3 +313,33 @@ function theme_boost_union_flavour_exists_for_cohort($cohortid) { // We didn't find any matching cohort, return false. return false; } + +/** + * Helper function do get a config key from flavour item. + * + * @param string $flavourid + * @param string $configkey + * + * @return string/bool + */ +function theme_boost_union_get_flavour_config_item_for_id(string $flavourid, string $configkey) { + global $DB; + + $cache = cache::make('theme_boost_union', 'flavours'); + + $flavouridkey = 'flavour_' . $flavourid; + // Get the cached flavour config for the current user flavour id. + $flavourconfig = $cache->get($flavouridkey); + + // If we got a cached flavour config. + if ($flavourconfig == false) { + $flavourconfig = $DB->get_record('theme_boost_union_flavours', ['id' => $flavourid]); + $cache->set($flavouridkey, $flavourconfig); + } + + if (isset($flavourconfig->{$configkey})) { // ...isset returns true only if property exits and value != null;. + return $flavourconfig->{$configkey}; + } + + return false; +} diff --git a/flavours/styles.php b/flavours/styles.php deleted file mode 100644 index c5ef1c93ed1..00000000000 --- a/flavours/styles.php +++ /dev/null @@ -1,88 +0,0 @@ -. - -/** - * Theme Boost Union - Flavours styles serving - * - * @package theme_boost_union - * @copyright 2022 Alexander Bias, lern.link GmbH - * @copyright on behalf of Zurich University of Applied Sciences (ZHAW) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -// Do not show any debug messages and any errors which might break the shipped CSS. -define('NO_DEBUG_DISPLAY', true); - -// Do not do any upgrade checks here. -define('NO_UPGRADE_CHECK', true); - -// Require config. -// Let codechecker ignore the next line because otherwise it would complain about a missing login check -// after requiring config.php which is really not needed. -require(__DIR__.'/../../../config.php'); // phpcs:disable moodle.Files.RequireLogin.Missing - -// Require css sending libraries. -require_once($CFG->dirroot.'/lib/csslib.php'); -require_once($CFG->dirroot.'/lib/configonlylib.php'); - -// Get parameters. -$flavourid = required_param('id', PARAM_INT); -$themerev = required_param('rev', PARAM_INT); // We do not really need the theme revision in this script, we just require it - // to support proper cache control in the browser. - -// Initialize SCSS code. -$scss = ''; - -// Get the raw SCSS and the background image from the database, -// throw an exception if it does not exist because then something is really wrong. -try { - // Note: It would be worthwhile to pick this data from a MUC instance instead of fetching it from the DB - // again and again. However, as the result is cached in the browser and the browser should not request - // a flavour's CSS file again and again this should be ok for now. - $flavour = $DB->get_record('theme_boost_union_flavours', - ['id' => $flavourid], 'look_rawscss, look_backgroundimage', MUST_EXIST); - - // Catch the exception. -} catch (\Exception $e) { - // Just die, there is no use to output any error message, it would even be counter-productive if the browser - // tries to interpret it as CSS code. - die; -} - -// If the flavour has raw SCSS code. -// Note: In the current state of implementation, this setting only allows the usage of custom CSS, not SCSS. -// There is a follow-up issue on Github to add SCSS support. -// However, to ease this future improvement, the setting has already been called 'rawscss'. -if (!empty($flavour->look_rawscss)) { - // Add it to the SCSS code. - $scss .= $flavour->look_rawscss; -} - -// If the flavour has a background image. -if ($flavour->look_backgroundimage != null) { - // Compose the URL to the flavour's background image. - $backgroundimageurl = core\url::make_pluginfile_url( - context_system::instance()->id, 'theme_boost_union', 'flavours_look_backgroundimage', $flavourid, - '/'.theme_get_revision(), '/'.$flavour->look_backgroundimage); - - // And add it to the SCSS code, adhering the fact that we must not overwrite the login page background image again. - $scss .= 'body:not(.pagelayout-login) { '; - $scss .= 'background-image: url("'.$backgroundimageurl.'");'; - $scss .= '}'; -} - -// Send out the resulting CSS code. The theme revision will be set as etag to support the browser caching. -css_send_cached_css_content($scss, theme_get_revision()); diff --git a/lang/en/theme_boost_union.php b/lang/en/theme_boost_union.php index 6de97b36214..1bae4e20deb 100644 --- a/lang/en/theme_boost_union.php +++ b/lang/en/theme_boost_union.php @@ -1167,9 +1167,19 @@ $string['flavoursbackgroundimage'] = 'Background image'; $string['flavoursbackgroundimage_help'] = 'With this setting, the flavour will override the background image which is configured in Boost Union\'s look settings.'; $string['flavoursbacktooverview'] = 'Back to flavour overview'; +$string['flavoursbootstrapcolordanger'] = 'Bootstrap color for "Danger"'; +$string['flavoursbootstrapcolordanger_help'] = 'With this setting, the flavour will override the Bootstrap "danger" color which is configured in Boost Union\'s look settings.'; +$string['flavoursbootstrapcolorinfo'] = 'Bootstrap color for "Info"'; +$string['flavoursbootstrapcolorinfo_help'] = 'With this setting, the flavour will override the Bootstrap info "color" which is configured in Boost Union\'s look settings.'; +$string['flavoursbootstrapcolorsuccess'] = 'Bootstrap color for "Success"'; +$string['flavoursbootstrapcolorsuccess_help'] = 'With this setting, the flavour will override the Bootstrap "success" color which is configured in Boost Union\'s look settings.'; +$string['flavoursbootstrapcolorwarning'] = 'Bootstrap color for "Warning"'; +$string['flavoursbootstrapcolorwarning_help'] = 'With this setting, the flavour will override the Bootstrap "warning" color which is configured in Boost Union\'s look settings.'; +$string['flavoursbrandcolour'] = 'Brand colour '; +$string['flavoursbrandcolour_help'] = 'With this setting, the flavour will override the brand colour which is configured in Boost Union\'s look settings.'; $string['flavourscreateflavour'] = 'Create flavour'; -$string['flavourscustomcss'] = 'Custom CSS'; -$string['flavourscustomcss_help'] = 'With this setting, you can write custom CSS for the flavour. It will be appended to the stack of CSS code which is shipped to the browser as soon as the flavour applies. Please note that in the current state of implementation, this setting only allows the usage of custom CSS, not SCSS.'; +$string['flavourscustomcss'] = 'Custom SCSS'; +$string['flavourscustomcss_help'] = 'With this setting, you can write custom SCSS for the flavour. It will be appended to the stack of CSS code which is shipped to the browser as soon as the flavour applies.'; $string['flavoursdelete'] = 'Delete'; $string['flavoursdeleteflavour'] = 'Delete flavour'; $string['flavoursdeleteconfirmation'] = 'Do you really want to delete the flavour {$a}?'; diff --git a/lib.php b/lib.php index 3d6800a45bd..f4f4d5f7b15 100644 --- a/lib.php +++ b/lib.php @@ -173,7 +173,7 @@ function theme_boost_union_get_main_scss_content($theme) { * @return string */ function theme_boost_union_get_pre_scss($theme) { - global $CFG; + global $CFG, $flavourid; // Require local library. require_once($CFG->dirroot . '/theme/boost_union/locallib.php'); @@ -195,7 +195,20 @@ function theme_boost_union_get_pre_scss($theme) { // Prepend variables first. foreach ($configurable as $configkey => $targets) { - $value = get_config('theme_boost_union', $configkey); + $value = isset($theme->settings->{$configkey}) ? $theme->settings->{$configkey} : null; + // Check if flavour is active. + if (isset($flavourid)) { + // Require flavours library. + require_once($CFG->dirroot . '/theme/boost_union/flavours/flavourslib.php'); + // Get flavour config for id. + $flavourvalue = theme_boost_union_get_flavour_config_item_for_id($flavourid, $configkey); + // Check override value. + if (!($flavourvalue)) { + continue; + } + // Override em. + $value = $flavourvalue; + } if (!($value)) { continue; } @@ -277,11 +290,28 @@ function theme_boost_union_get_pre_scss($theme) { // Get and include the external Pre SCSS. $scss .= theme_boost_union_get_external_scss('pre'); - // Prepend pre-scss. - if (get_config('theme_boost_union', 'scsspre')) { - $scss .= get_config('theme_boost_union', 'scsspre'); - } + if (isset($flavourid)) { + $rawscsspre = theme_boost_union_get_flavour_config_item_for_id($flavourid, 'look_rawscss'); + // Append pre-scss. + if ($rawscsspre !== false) { + $scss .= "\n/** RAW-SCSS from theme_boost_union_get_pre_scss **/\n" . $rawscsspre; + } + // If the flavour has a background image. + $backgroundimage = theme_boost_union_get_flavour_config_item_for_id($flavourid, 'look_backgroundimage'); + if ($backgroundimage != false) { + // Compose the URL to the flavour's background image. + $backgroundimageurl = moodle_url::make_pluginfile_url( + context_system::instance()->id, 'theme_boost_union', 'flavours_look_backgroundimage', $flavourid, + '/'.theme_get_revision(), '/'.$backgroundimage); + + // And add it to the SCSS code, adhering the fact that we must not overwrite the login page background image again. + $scss .= 'body:not(.pagelayout-login) { '; + $scss .= 'background-image: url("'.$backgroundimageurl.'");'; + $scss .= '}'; + } + } + // Since setting "precss" is originally from parent boost it is added in theme_boost_get_pre_scss. return $scss; } @@ -360,7 +390,7 @@ function theme_boost_union_get_extra_scss($theme) { // Note: Boost Union is also capable of overriding the background image in its flavours. // In contrast to the other flavour assets like the favicon overriding, this isn't done here in place as this function // is composing Moodle core CSS which has to remain flavour-independent. - // Instead, the flavour is overriding the background image later in flavours/styles.php. + // Instead, the flavour is overriding the background image later in theme_boost_union_get_pre_scss() lib.php. // For the rest of this function, we add SCSS snippets to the SCSS stack based on enabled admin settings. // This is done here as it is quite easy to do. As an alternative, it could also been done in post.scss by using @@ -684,3 +714,33 @@ function theme_boost_union_before_session_start() { // Manipulate Moodle core hooks. theme_boost_union_manipulate_hooks(); } + +function theme_boost_union_alter_css_urls(&$urls) { + global $CFG; + + // Require flavours library. + require_once($CFG->dirroot . '/theme/boost_union/flavours/flavourslib.php'); + + // If any flavour applies to this page. + $flavour = theme_boost_union_get_flavour_which_applies(); + if ($flavour != null) { + if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) { + // No CSS switch during behat runs, or it will take ages to run a scenario. + return; + } + + foreach (array_keys($urls) as $i) { + if ($urls[$i] instanceof moodle_url) { + $pathstyles = preg_quote($CFG->wwwroot . '/theme/styles.php', '|'); + if (preg_match("|^$pathstyles(/_s)?(.*)$|", $urls[$i]->out(false), $matches)) { + if (!empty($CFG->slasharguments)) { + $parts = explode('/', $matches[2]); + $parts[3] = $flavour->id . '/' . $parts[3]; + $urls[$i] = new moodle_url('/theme/boost_union/flavours/css.php'); + $urls[$i]->set_slashargument($matches[1] . join('/', $parts)); + } + } + } + } + } +} diff --git a/locallib.php b/locallib.php index 08b77cbc4be..cfa92499d7c 100644 --- a/locallib.php +++ b/locallib.php @@ -1048,32 +1048,6 @@ function theme_boost_union_get_externaladminpage_heading() { return $SITE->fullname; } -/** - * Helper function which adds the CSS file from the flavour to the Moodle page. - * It's meant to be called by theme_boost_union_before_standard_html_head() only. - * * - * @throws coding_exception - * @throws dml_exception - * @throws moodle_exception - */ -function theme_boost_union_add_flavourcss_to_page() { - global $CFG, $PAGE; - - // Require flavours library. - require_once($CFG->dirroot . '/theme/boost_union/flavours/flavourslib.php'); - - // If any flavour applies to this page. - $flavour = theme_boost_union_get_flavour_which_applies(); - if ($flavour != null) { - // Build the flavour CSS file URL. - $flavourcssurl = new core\url('/theme/boost_union/flavours/styles.php', - ['id' => $flavour->id, 'rev' => theme_get_revision()]); - - // Add the CSS file to the page. - $PAGE->requires->css($flavourcssurl); - } -} - /** * Helper function which returns the course header image url, picking the current course from the course settings * or the fallback image from the theme. @@ -2099,9 +2073,6 @@ function theme_boost_union_callbackimpl_before_standard_html(&$hook = null) { // Require local library. require_once($CFG->dirroot . '/theme/boost_union/locallib.php'); - // Add the flavour CSS to the page. - theme_boost_union_add_flavourcss_to_page(); - // Add the touch icons to the page. $html .= theme_boost_union_get_touchicons_html_for_page(); From 8fc8e6d95480dcfd5228d79a6cee46cb795c7ec1 Mon Sep 17 00:00:00 2001 From: Alexander Bias Date: Fri, 20 Dec 2024 13:52:40 +0100 Subject: [PATCH 3/6] Review changes --- CHANGES.md | 1 + classes/form/flavour_edit_form.php | 250 +++++++++++------- classes/form/smartmenu_item_edit_form.php | 9 + .../colorpicker_rule.php} | 136 ++-------- db/install.xml | 11 +- db/upgrade.php | 42 +-- flavours/edit.php | 2 +- flavours/flavourslib.php | 36 +-- flavours/{css.php => styles.php} | 94 +++++-- lang/en/theme_boost_union.php | 13 +- lib.php | 160 ++++++++--- ...union_flavourssettings_application.feature | 14 +- ...ost_union_flavourssettings_caching.feature | 26 +- ..._boost_union_flavourssettings_look.feature | 2 +- version.php | 2 +- 15 files changed, 458 insertions(+), 340 deletions(-) rename classes/{form/colourpicker_form_element.php => formelement/colorpicker_rule.php} (52%) rename flavours/{css.php => styles.php} (72%) diff --git a/CHANGES.md b/CHANGES.md index 70b9352bf5d..e7cb95494b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Changes ### Unreleased +* 2024-12-22 - Feature: Allow overwriting of brand colors and the usage of SCSS (instead of pure CSS) in flavours, resolves #155. * 2024-12-15 - Feature: Add declaration of accessibility page and accessibility support page, resolves #567. ### v4.5-r4 diff --git a/classes/form/flavour_edit_form.php b/classes/form/flavour_edit_form.php index 4193cc9e7f6..5fdf0308517 100644 --- a/classes/form/flavour_edit_form.php +++ b/classes/form/flavour_edit_form.php @@ -26,9 +26,6 @@ namespace theme_boost_union\form; -use ScssPhp\ScssPhp\Exception\CompilerException; -use ScssPhp\ScssPhp\Exception\ParserException; - defined('MOODLE_INTERNAL') || die(); // Require forms library. @@ -62,6 +59,21 @@ public function definition() { // Prepare yes-no option for multiple usage. $yesnooption = [false => get_string('no'), true => get_string('yes')]; + // Require and register the QuickForm colorpicker element. + require_once($CFG->dirroot.'/theme/boost_union/classes/formelement/colorpicker.php'); + \MoodleQuickForm::registerElementType( + 'theme_boost_union_colorpicker', + $CFG->dirroot.'/theme/boost_union/classes/formelement/colorpicker.php', + '\theme_boost_union\formelement\colorpicker' + ); + // Register validation rule for the QuickForm colorpicker element. + \MoodleQuickForm::registerRule( + 'theme_boost_union_colorpicker_rule', + null, + '\theme_boost_union\formelement\colorpicker_rule', + $CFG->dirroot.'/theme/boost_union/classes/formelement/colorpicker_rule.php' + ); + // Add the flavour ID as hidden element. $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); @@ -85,6 +97,17 @@ public function definition() { $mform->addElement('header', 'looksettingsheader', get_string('configtitlelook', 'theme_boost_union')); $mform->setExpanded('looksettingsheader'); + // Add logos heading. + $context = new \stdClass(); + $context->title = get_string('logosheading', 'theme_boost_union', null, true); + $mform->addElement( + 'html', + // Wrapping the setting headings with a div with the ID "adminsettings" is not really correct as we will have + // duplicate IDs on the page. But it is the only way to re-use the correct styling for the setting heading. + // (And no, applying the ID to the form does not work either as we would trigger other unwanted stylings). + '
'.$OUTPUT->render_from_template('core_admin/setting_heading', $context).'
' + ); + // Add logo as filemanager element. $mform->addElement('filemanager', 'flavours_look_logo', get_string('logo', 'admin'), null, [ @@ -105,6 +128,14 @@ public function definition() { ]); $mform->addHelpButton('flavours_look_logocompact', 'flavourslogocompact', 'theme_boost_union'); + // Add favicon heading. + $context = new \stdClass(); + $context->title = get_string('faviconheading', 'theme_boost_union', null, true); + $mform->addElement( + 'html', + '
'.$OUTPUT->render_from_template('core_admin/setting_heading', $context).'
' + ); + // Add favicon as filemanager element. $mform->addElement('filemanager', 'flavours_look_favicon', get_string('faviconsetting', 'theme_boost_union'), null, [ @@ -115,6 +146,14 @@ public function definition() { ]); $mform->addHelpButton('flavours_look_favicon', 'flavoursfavicon', 'theme_boost_union'); + // Add backgroundimages heading. + $context = new \stdClass(); + $context->title = get_string('backgroundimagesheading', 'theme_boost_union', null, true); + $mform->addElement( + 'html', + '
'.$OUTPUT->render_from_template('core_admin/setting_heading', $context).'
' + ); + // Add backgroundimage as filemanager element. $mform->addElement('filemanager', 'flavours_look_backgroundimage', get_string('backgroundimagesetting', 'theme_boost_union'), null, [ @@ -125,100 +164,94 @@ public function definition() { ]); $mform->addHelpButton('flavours_look_backgroundimage', 'flavoursbackgroundimage', 'theme_boost_union'); - // Create brand colors heading. + // Add brand colors heading. $context = new \stdClass(); $context->title = get_string('brandcolorsheading', 'theme_boost_union', null, true); $mform->addElement( 'html', - '
'. $OUTPUT->render_from_template('core_admin/setting_heading', $context), '
' + '
'.$OUTPUT->render_from_template('core_admin/setting_heading', $context).'
' ); - // Register custom colourpicker. - \MoodleQuickForm::registerElementType('boost_union_colourpicker', - $CFG->dirroot . '/theme/boost_union/classes/form/colourpicker_form_element.php', - 'theme_boost_union\form\theme_boost_union_colourpicker_form_element'); - // Register validation rule for colourpicker. - \MoodleQuickForm::registerRule('theme_boost_union_colourpicker_rule', - null, - 'theme_boost_union\form\theme_boost_union_colourpicker_rule', - $CFG->dirroot . '/theme/boost_union/classes/form/colourpicker_form_element.php'); - - // Add brandcolour as colourpicker element. + // Add brandcolor as colorpicker element. + $this->check_slasharguments_warning($mform); $mform->addElement( - 'boost_union_colourpicker', - 'brandcolor', - get_string('brandcolor', 'theme_boost'), - [ - 'id' => 'colourpicker_brandcolour', - ]); - $mform->setDefault('brandcolor', ''); - $mform->addHelpButton('brandcolor', 'flavoursbrandcolour', 'theme_boost_union'); - // Add validation rule. - $mform->addRule('brandcolor', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); - - // Create Bootstrap colors heading. + 'theme_boost_union_colorpicker', + 'look_brandcolor', + get_string('flavoursbrandcolor', 'theme_boost_union'), + ['id' => 'colourpicker_brandcolour']); + $mform->setType('look_brandcolor', PARAM_TEXT); + $mform->addRule('look_brandcolor', get_string('validateerror', 'admin'), 'theme_boost_union_colorpicker_rule'); + $mform->addHelpButton('look_brandcolor', 'flavoursbrandcolor', 'theme_boost_union'); + + // Add Bootstrap colors heading. $context = new \stdClass(); $context->title = get_string('bootstrapcolorsheading', 'theme_boost_union', null, true); $mform->addElement( - 'html', - '
'. $OUTPUT->render_from_template('core_admin/setting_heading', $context), '
' + 'html', + '
'.$OUTPUT->render_from_template('core_admin/setting_heading', $context).'
' ); - // Add Bootstrap color for 'success' as colourpicker element. + // Add Bootstrap color for 'success' as colorpicker element. + $this->check_slasharguments_warning($mform); $mform->addElement( - 'boost_union_colourpicker', - 'bootstrapcolorsuccess', - get_string('bootstrapcolorsuccesssetting', 'theme_boost_union'), - [ - 'id' => 'colourpicker-bootstrapcolorsuccess', - ]); - $mform->setDefault('bootstrapcolorsuccess', ''); - $mform->addHelpButton('bootstrapcolorsuccess', 'flavoursbootstrapcolorsuccess', 'theme_boost_union'); - // Add validation rule. - $mform->addRule('bootstrapcolorsuccess', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); - - // Add Bootstrap color for 'info' as colourpicker element. + 'theme_boost_union_colorpicker', + 'look_bootstrapcolorsuccess', + get_string('flavoursbootstrapcolorsuccess', 'theme_boost_union'), + ['id' => 'colourpicker_bootstrapcolorsuccess']); + $mform->setType('look_bootstrapcolorsuccess', PARAM_TEXT); + $mform->addRule('look_bootstrapcolorsuccess', get_string('validateerror', 'admin'), 'theme_boost_union_colorpicker_rule'); + $mform->addHelpButton('look_bootstrapcolorsuccess', 'flavoursbootstrapcolorsuccess', 'theme_boost_union'); + + // Add Bootstrap color for 'info' as colorpicker element. + $this->check_slasharguments_warning($mform); $mform->addElement( - 'boost_union_colourpicker', - 'bootstrapcolorinfo', - get_string('bootstrapcolorinfosetting', 'theme_boost_union'), - [ - 'id' => 'colourpicker-bootstrapcolorinfo', - ]); - $mform->setDefault('bootstrapcolorinfo', ''); - $mform->addHelpButton('bootstrapcolorinfo', 'flavoursbootstrapcolorinfo', 'theme_boost_union'); - // Add validation rule. - $mform->addRule('bootstrapcolorinfo', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); - - // Add Bootstrap color for 'warning' as colourpicker element. + 'theme_boost_union_colorpicker', + 'look_bootstrapcolorinfo', + get_string('flavoursbootstrapcolorinfo', 'theme_boost_union'), + ['id' => 'colourpicker_bootstrapcolorinfo']); + $mform->setType('look_bootstrapcolorinfo', PARAM_TEXT); + $mform->addRule('look_bootstrapcolorinfo', get_string('validateerror', 'admin'), 'theme_boost_union_colorpicker_rule'); + $mform->addHelpButton('look_bootstrapcolorinfo', 'flavoursbootstrapcolorinfo', 'theme_boost_union'); + + // Add Bootstrap color for 'warning' as colorpicker element. + $this->check_slasharguments_warning($mform); $mform->addElement( - 'boost_union_colourpicker', - 'bootstrapcolorwarning', - get_string('bootstrapcolorwarningsetting', 'theme_boost_union'), - [ - 'id' => 'colourpicker-bootstrapcolorwarning', - ]); - $mform->addHelpButton('bootstrapcolorwarning', 'flavoursbootstrapcolorwarning', 'theme_boost_union'); - // Add validation rule. - $mform->addRule('bootstrapcolorwarning', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); - - // Add Bootstrap color for 'danger' as colourpicker element. + 'theme_boost_union_colorpicker', + 'look_bootstrapcolorwarning', + get_string('flavoursbootstrapcolorwarning', 'theme_boost_union'), + ['id' => 'colourpicker-bootstrapcolorwarning']); + $mform->setType('look_bootstrapcolorwarning', PARAM_TEXT); + $mform->addRule('look_bootstrapcolorwarning', get_string('validateerror', 'admin'), 'theme_boost_union_colorpicker_rule'); + $mform->addHelpButton('look_bootstrapcolorwarning', 'flavoursbootstrapcolorwarning', 'theme_boost_union'); + + // Add Bootstrap color for 'danger' as colorpicker element. + $this->check_slasharguments_warning($mform); $mform->addElement( - 'boost_union_colourpicker', - 'bootstrapcolordanger', - get_string('bootstrapcolordangersetting', 'theme_boost_union'), - [ - 'id' => 'colourpicker-bbootstrapcolordanger', - ]); - $mform->setDefault('bootstrapcolordanger', ''); - $mform->addHelpButton('bootstrapcolordanger', 'flavoursbootstrapcolordanger', 'theme_boost_union'); - // Add validation rule. - $mform->addRule('bootstrapcolordanger', get_string('validateerror', 'admin'), 'theme_boost_union_colourpicker_rule'); - - // Add custom css as textarea element. - $mform->addElement('textarea', 'look_rawscss', get_string('rawscss', 'theme_boost'), array('rows' => 15)); + 'theme_boost_union_colorpicker', + 'look_bootstrapcolordanger', + get_string('flavoursbootstrapcolordanger', 'theme_boost_union'), + ['id' => 'colourpicker-bbootstrapcolordanger']); + $mform->setType('look_bootstrapcolordanger', PARAM_TEXT); + $mform->addRule('look_bootstrapcolordanger', get_string('validateerror', 'admin'), 'theme_boost_union_colorpicker_rule'); + $mform->addHelpButton('look_bootstrapcolordanger', 'flavoursbootstrapcolordanger', 'theme_boost_union'); + + // Add SCSS heading. + $context = new \stdClass(); + $context->title = get_string('scssheading', 'theme_boost_union', null, true); + $mform->addElement( + 'html', + '
'.$OUTPUT->render_from_template('core_admin/setting_heading', $context).'
' + ); + + // Add custom initial SCSS as textarea element. + $mform->addElement('textarea', 'look_rawscsspre', get_string('flavourscustomscsspre', 'theme_boost_union'), ['rows' => 8]); $mform->setType('title', PARAM_TEXT); - $mform->addHelpButton('look_rawscss', 'flavourscustomcss', 'theme_boost_union'); + $mform->addHelpButton('look_rawscsspre', 'flavourscustomscsspre', 'theme_boost_union'); + + // Add custom SCSS as textarea element. + $mform->addElement('textarea', 'look_rawscss', get_string('flavourscustomscss', 'theme_boost_union'), ['rows' => 8]); + $mform->setType('title', PARAM_TEXT); + $mform->addHelpButton('look_rawscss', 'flavourscustomscss', 'theme_boost_union'); // Add apply-to-cohort as header element. $mform->addElement('header', 'applytocohortheader', get_string('flavoursapplytocohorts', 'theme_boost_union')); @@ -279,7 +312,8 @@ public function definition() { /** * Theme Boost Union - Flavours edit form validation * - * @package theme_boost_union + * The routine to check the SCSS code is copied and modified from admin_setting_scsscode in /lib/adminlib.php. + * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array of "element_name"=>"error_description" if there are errors, @@ -290,22 +324,50 @@ public function validation($data, $files) { $errors = []; - if (!empty($data['look_rawscss']) && !empty($data)) { - - $scss = new \core_scss(); - try { - if ($scssproperties = $PAGE->theme->get_scss_property()) { - $scss->setImportPaths($scssproperties[0]); + // If we have any data. + if (!empty($data)) { + // Iterate over the SCSS fields. + foreach (['look_rawscss', 'look_rawscsspre'] as $field) { + // Check if the SCSS code can be compiled. + if (!empty($data[$field])) { + $compiler = new \core_scss(); + try { + if ($scssproperties = $PAGE->theme->get_scss_property()) { + $compiler->setImportPaths($scssproperties[0]); + } + $compiler->compile($data[$field]); + } catch (\ScssPhp\ScssPhp\Exception\ParserException $e) { + $errors[$field] = get_string('scssinvalid', 'admin', $e->getMessage()); + // phpcs:disable Generic.CodeAnalysis.EmptyStatement.DetectedCatch + } catch (\ScssPhp\ScssPhp\Exception\CompilerException $e) { + // Silently ignore this - it could be a SCSS variable defined from somewhere + // else which we are not examining here. + } } - $scss->compile($data['look_rawscss']); - } catch (ParserException $e) { - $errors['look_rawscss'] = get_string('scssinvalid', 'admin', $e->getMessage()); - } catch (CompilerException $e) { - $errors['look_rawscss'] = get_string('scssinvalid', 'admin', $e->getMessage()); } - $scss = null; - unset($scss); } + return $errors; } + + /** + * Helper function which adds a warning notification to the form if slasharguments is disabled. + * + * @param \MoodleQuickForm $mform The form object. + * @return void + */ + private function check_slasharguments_warning($mform) { + global $CFG, $OUTPUT; + + // If slasharguments is disabled. + if (empty($CFG->slasharguments)) { + // Add a warning notification to the form. + $slashargumentsurl = new \core\url('/admin/search.php', ['query' => 'slasharguments']); + $notification = new \core\output\notification( + get_string('warningslashargumentsdisabled', 'theme_boost_union', ['url' => $slashargumentsurl]), + \core\output\notification::NOTIFY_WARNING); + $notification->set_show_closebutton(false); + $mform->addElement('html', $OUTPUT->render($notification)); + } + } } diff --git a/classes/form/smartmenu_item_edit_form.php b/classes/form/smartmenu_item_edit_form.php index 85a2e1b99a5..145b1582c35 100644 --- a/classes/form/smartmenu_item_edit_form.php +++ b/classes/form/smartmenu_item_edit_form.php @@ -53,6 +53,13 @@ public function definition() { $CFG->dirroot.'/theme/boost_union/classes/formelement/colorpicker.php', '\theme_boost_union\formelement\colorpicker' ); + // Register validation rule for the QuickForm colorpicker element. + \MoodleQuickForm::registerRule( + 'theme_boost_union_colorpicker_rule', + null, + '\theme_boost_union\formelement\colorpicker_rule', + $CFG->dirroot.'/theme/boost_union/classes/formelement/colorpicker_rule.php' + ); // Get an easier handler for the form. $mform = $this->_form; @@ -283,12 +290,14 @@ public function definition() { $mform->addElement('theme_boost_union_colorpicker', 'textcolor', get_string('smartmenusmenuitemcardtextcolor', 'theme_boost_union')); $mform->setType('textcolor', PARAM_TEXT); + $mform->addRule('textcolor', get_string('validateerror', 'admin'), 'theme_boost_union_colorpicker_rule'); $mform->addHelpButton('textcolor', 'smartmenusmenuitemcardtextcolor', 'theme_boost_union'); // Add card background color as color picker element. $mform->addElement('theme_boost_union_colorpicker', 'backgroundcolor', get_string('smartmenusmenuitemcardbackgroundcolor', 'theme_boost_union')); $mform->setType('backgroundcolor', PARAM_TEXT); + $mform->addRule('backgroundcolor', get_string('validateerror', 'admin'), 'theme_boost_union_colorpicker_rule'); $mform->addHelpButton('backgroundcolor', 'smartmenusmenuitemcardbackgroundcolor', 'theme_boost_union'); } diff --git a/classes/form/colourpicker_form_element.php b/classes/formelement/colorpicker_rule.php similarity index 52% rename from classes/form/colourpicker_form_element.php rename to classes/formelement/colorpicker_rule.php index 292848c6181..dc0f4f4e200 100644 --- a/classes/form/colourpicker_form_element.php +++ b/classes/formelement/colorpicker_rule.php @@ -14,135 +14,31 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -namespace theme_boost_union\form; - -use renderer_base; - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; - -require_once($CFG->dirroot . '/lib/form/editor.php'); - /** - * Form element for handling the colour picker. + * Theme Boost Union - Form element for color picker * * @package theme_boost_union - * @copyright 2023 Mario Wehr + * @copyright 2023 bdecent GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class theme_boost_union_colourpicker_form_element extends \HTML_QuickForm_element implements \templatable { - // String html for help button, if empty then no help. - public $_helpbutton = ''; +namespace theme_boost_union\formelement; - /** - * Class constructor - * - * @param string Name of the element - * @param mixed Label(s) for the element - * @param mixed Associative array of tag attributes or HTML attributes name="value" pairs - * @since 1.0 - * @access public - * @return void - */ - public function __construct($elementname=null, $elemenlabel=null, $attributes=null) { - parent::__construct($elementname, $elemenlabel, $attributes); - $this->_type = 'static'; - } - - /** - * Sets name of editor - * - * @param string $name name of element - */ - // @codingStandardsIgnoreStart - public function setName($name) { - $this->updateAttributes(array('name' => $name)); - } - // @codingStandardsIgnoreEnd - /** - * Returns name of element - * - * @return string - */ - // @codingStandardsIgnoreStart - function getName() { - return $this->getAttribute('name'); - } - // @codingStandardsIgnoreEnd - /** - * get html for help button - * - * @return string html for help button - */ - // @codingStandardsIgnoreStart - public function getHelpButton() { - return $this->_helpbutton; - } - // @codingStandardsIgnoreEnd - /** - * Sets the value of the form element - * - * @param string $value - */ - // @codingStandardsIgnoreStart - public function setvalue($value) { - $this->updateAttributes(array('value' => $value)); - } - // @codingStandardsIgnoreEnd - /** - * Gets the value of the form element - */ - public function getvalue() { - return $this->getAttribute('value'); - } - - /** - * Returns the html string to display this element. - * - * @return string - */ - public function tohtml() { - global $PAGE, $OUTPUT; - - $icon = new \pix_icon('i/loading', get_string('loading', 'admin'), 'moodle', ['class' => 'loadingicon']); - $context = (object) [ - 'icon' => $icon->export_for_template($OUTPUT), - 'name' => $this->getAttribute('name'), - 'id' => $this->getAttribute('id'), - 'value' => $this->getAttribute('value'), - "readonly" => false, - 'haspreviewconfig' => false, - ]; - $PAGE->requires->js_init_call('M.util.init_colour_picker', array($this->getAttribute('id'), null)); - return $OUTPUT->render_from_template('core_admin/setting_configcolourpicker', $context); - } - - /** - * Function to export the renderer data in a format that is suitable for a mustache template. - * - * @param \renderer_base $output Used to do a final render of any components that need to be rendered for export. - * @return \stdClass|array - */ - public function export_for_template(renderer_base $output) { - $context['html'] = $this->toHtml(); - $context['id'] = $this->getAttribute('id'); - return $context; - } -} +use HTML_QuickForm_Rule; /** - * Colour picker validation rule + * Validation rule for color picker * - * @package theme_boost_union - * @copyright 2023 Mario Wehr - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * This class is copied and modified from admin_setting_configcolourpicker in /lib/adminlib.php. + * + * @package theme_boost_union + * @copyright 2023 Mario Wehr + * based on code 2010 by Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class theme_boost_union_colourpicker_rule extends \HTML_QuickForm_Rule { - +class colorpicker_rule extends HTML_QuickForm_Rule { /** - * Validates the colour that was entered by the user + * Validates the colour that was entered by the user. * * @param string $value Value to check * @param int|string|array $options Not used yet @@ -151,7 +47,7 @@ class theme_boost_union_colourpicker_rule extends \HTML_QuickForm_Rule { public function validate($value, $options = null) { // List of valid HTML colour names. - $colornames = array( + $colornames = [ 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', @@ -182,8 +78,8 @@ public function validate($value, $options = null) { 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', - 'whitesmoke', 'yellow', 'yellowgreen' - ); + 'whitesmoke', 'yellow', 'yellowgreen', + ]; if (preg_match('/^#?([[:xdigit:]]{3}){1,2}$/', $value)) { if (strpos($value, '#') !== 0) { diff --git a/db/install.xml b/db/install.xml index aa198af0b77..aef4e9a381c 100644 --- a/db/install.xml +++ b/db/install.xml @@ -21,11 +21,12 @@ - - - - - + + + + + + diff --git a/db/upgrade.php b/db/upgrade.php index 8682bef5c95..3024878eb1c 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -348,53 +348,61 @@ function xmldb_theme_boost_union_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2024100702, 'theme', 'boost_union'); } - if ($oldversion < 2024100705) { + if ($oldversion < 2024100706) { // Define table theme_boost_union_flavours to be altered. $table = new xmldb_table('theme_boost_union_flavours'); - // Define field brandcolor to be added to theme_boost_union_flavours. - $field = new xmldb_field('brandcolor', XMLDB_TYPE_CHAR, '32', null, null, null, null); + // Define field look_rawscsspre to be added to theme_boost_union_flavours. + $field = new xmldb_field('look_rawscsspre', XMLDB_TYPE_TEXT, null, null, null, null, null); - // Conditionally launch add field brandcolor. + // Conditionally launch add field look_rawscsspre. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - // Define field bootstrapcolorsuccess to be added to theme_boost_union_flavours. - $field = new xmldb_field('bootstrapcolorsuccess', XMLDB_TYPE_CHAR, '32', null, null, null, null); + // Define field look_brandcolor to be added to theme_boost_union_flavours. + $field = new xmldb_field('look_brandcolor', XMLDB_TYPE_CHAR, '32', null, null, null, null); - // Conditionally launch add field bootstrapcolorsuccess. + // Conditionally launch add field look_brandcolor. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - // Define field bootstrapcolorinfo to be added to theme_boost_union_flavours. - $field = new xmldb_field('bootstrapcolorinfo', XMLDB_TYPE_CHAR, '32', null, null, null, null); + // Define field look_bootstrapcolorsuccess to be added to theme_boost_union_flavours. + $field = new xmldb_field('look_bootstrapcolorsuccess', XMLDB_TYPE_CHAR, '32', null, null, null, null); - // Conditionally launch add field bootstrapcolorinfo. + // Conditionally launch add field look_bootstrapcolorsuccess. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - // Define field bootstrapcolorwarning to be added to theme_boost_union_flavours. - $field = new xmldb_field('bootstrapcolorwarning', XMLDB_TYPE_CHAR, '32', null, null, null, null); + // Define field look_bootstrapcolorinfo to be added to theme_boost_union_flavours. + $field = new xmldb_field('look_bootstrapcolorinfo', XMLDB_TYPE_CHAR, '32', null, null, null, null); - // Conditionally launch add field bootstrapcolorwarning. + // Conditionally launch add field look_bootstrapcolorinfo. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - // Define field bootstrapcolordanger to be added to theme_boost_union_flavours. - $field = new xmldb_field('bootstrapcolordanger', XMLDB_TYPE_CHAR, '32', null, null, null, null); + // Define field look_bootstrapcolorwarning to be added to theme_boost_union_flavours. + $field = new xmldb_field('look_bootstrapcolorwarning', XMLDB_TYPE_CHAR, '32', null, null, null, null); - // Conditionally launch add field bootstrapcolordanger. + // Conditionally launch add field look_bootstrapcolorwarning. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field look_bootstrapcolordanger to be added to theme_boost_union_flavours. + $field = new xmldb_field('look_bootstrapcolordanger', XMLDB_TYPE_CHAR, '32', null, null, null, null); + + // Conditionally launch add field look_bootstrapcolordanger. if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } // Boost_union savepoint reached. - upgrade_plugin_savepoint(true, 2024100705, 'theme', 'boost_union'); + upgrade_plugin_savepoint(true, 2024100706, 'theme', 'boost_union'); } return true; diff --git a/flavours/edit.php b/flavours/edit.php index ca62f8fd9c9..a13bbe0e798 100644 --- a/flavours/edit.php +++ b/flavours/edit.php @@ -282,7 +282,7 @@ $fs->delete_area_files($context->id, 'theme_boost_union', 'flavours_look_favicon', $data->id); $fs->delete_area_files($context->id, 'theme_boost_union', 'flavours_look_backgroundimage', $data->id); - // Delete fallback sheet. Deleting them all because they get generated on building the all.css. + // Delete fallback sheet. And delete them all because they get generated on building the all.css. fulldelete($CFG->tempdir . '/theme/boost_union/'); // Reset theme cache. diff --git a/flavours/flavourslib.php b/flavours/flavourslib.php index 7757afd64cb..80101b20fae 100644 --- a/flavours/flavourslib.php +++ b/flavours/flavourslib.php @@ -315,31 +315,33 @@ function theme_boost_union_flavour_exists_for_cohort($cohortid) { } /** - * Helper function do get a config key from flavour item. + * Helper function to get a config item from the given flavour ID. * - * @param string $flavourid - * @param string $configkey + * This function should only be used during the SCSS generation process (where the generated SCSS will be cached afterwards). + * It should not be used during the page output directly as it will fetch the flavour config item directly from the database. * - * @return string/bool + * @param string $flavourid The flavour id. + * @param string $configkey The config key. + * @return string|null The config item if it exists, otherwise null. */ -function theme_boost_union_get_flavour_config_item_for_id(string $flavourid, string $configkey) { +function theme_boost_union_get_flavour_config_item_for_flavourid(string $flavourid, string $configkey) { global $DB; - $cache = cache::make('theme_boost_union', 'flavours'); + // Initialize static variable for the flavour record as this function might be called multiple times during a page output. + static $flavourrecord; - $flavouridkey = 'flavour_' . $flavourid; - // Get the cached flavour config for the current user flavour id. - $flavourconfig = $cache->get($flavouridkey); - - // If we got a cached flavour config. - if ($flavourconfig == false) { - $flavourconfig = $DB->get_record('theme_boost_union_flavours', ['id' => $flavourid]); - $cache->set($flavouridkey, $flavourconfig); + // If the flavour has not been been fetched yet. + if ($flavourrecord == null) { + // Get the given flavour record with the given flavour ID from the database. + $flavourrecord = $DB->get_record('theme_boost_union_flavours', ['id' => $flavourid]); } - if (isset($flavourconfig->{$configkey})) { // ...isset returns true only if property exits and value != null;. - return $flavourconfig->{$configkey}; + // If the flavour record has a config item with the given key. + if (isset($flavourrecord->{$configkey})) { + // Return it. + return $flavourrecord->{$configkey}; } - return false; + // Fallback: Return null. + return null; } diff --git a/flavours/css.php b/flavours/styles.php similarity index 72% rename from flavours/css.php rename to flavours/styles.php index a7465090c42..494104ab7c5 100644 --- a/flavours/css.php +++ b/flavours/styles.php @@ -17,8 +17,14 @@ /** * This file is responsible for serving the one huge CSS of each theme. * - * @package core - * @copyright 2009 Petr Skoda (skodak) {@link http://skodak.org} modified by 2023 Mario Wehr for bost union theme + * This file is copied and modified from /theme/styles.php. + * It is only called to serve the Boost Union CSS if a flavour is applied to the page. + * If no flavour is applied, the original /theme/styles.php is called. + * This is controlled by theme_boost_union_alter_css_urls(). + * + * @package theme_boost_union + * @copyright 2023 Mario Wehr + * based on code 2009 by Petr Skoda (skodak) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -47,8 +53,8 @@ list($themename, $rev, $flavourid, $type) = explode('/', $slashargument, 4); $themename = min_clean_param($themename, 'SAFEDIR'); $rev = min_clean_param($rev, 'RAW'); - $flavourid = min_clean_param($flavourid, 'SAFEDIR'); - $typeid = min_clean_param($type, 'SAFEDIR'); + $flavourid = min_clean_param($flavourid, 'INT'); + $type = min_clean_param($type, 'SAFEDIR'); } else { $themename = min_optional_param('theme', 'standard', 'SAFEDIR'); @@ -57,6 +63,18 @@ $usesvg = (bool)min_optional_param('svg', '1', 'INT'); } +// If no flavourid is provided, this file must have been called by mistake. +// In this case, we simply die. +if (empty($flavourid)) { + css_send_css_not_found(); +} + +// Store the active flavour in the global scope. +// This global variable is only set here and read in two functions in lib.php. +// This approach feels a bit hacky but it is the most efficient way to get the flavour ID into that function. +global $themeboostunionappliedflavour; +$themeboostunionappliedflavour = $flavourid; + // Check if we received a theme sub revision which allows us // to handle local caching on a per theme basis. $values = explode('_', $rev); @@ -67,24 +85,31 @@ $themesubrev = min_clean_param($themesubrev, 'INT'); } +// Note: We only check validity of the revision number here, we do not check the theme sub-revision because this is +// not solely based on time. +if (!min_is_revision_valid_and_current($rev)) { + // If the rev is invalid, normalise it to -1 to disable all caching. + $rev = -1; +} + // Check that type fits into the expected values. if (!in_array($type, ['all', 'all-rtl', 'editor', 'editor-rtl'])) { css_send_css_not_found(); } -// @codingStandardsIgnoreStart +// phpcs:disable Generic.CodeAnalysis.EmptyStatement.DetectedIf if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { // The theme exists in standard location - ok. +// phpcs:disable Generic.CodeAnalysis.EmptyStatement.DetectedIf } else if (!empty($CFG->themedir) && file_exists("$CFG->themedir/$themename/config.php")) { // Alternative theme location contains this theme - ok. - // @codingStandardsIgnoreEnd } else { header('HTTP/1.0 404 not found'); die('Theme was not found, sorry.'); } $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css"; -$candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $flavourid, $usesvg); +$candidatesheet = "{$candidatedir}/" . theme_boost_union_flavour_styles_get_filename($type, $themesubrev, $flavourid, $usesvg); $etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg); if (file_exists($candidatesheet)) { @@ -122,7 +147,7 @@ $cache = false; $candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css"; - $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $flavourid, $usesvg); + $candidatesheet = "{$candidatedir}/" . theme_boost_union_flavour_styles_get_filename($type, $themesubrev, $flavourid, $usesvg); $etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg); } @@ -171,7 +196,7 @@ // The content does not exist locally. // Generate and save it. - $candidatesheet = theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir, $flavourid); + $candidatesheet = theme_boost_union_flavour_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir, $flavourid); if ($lock) { $lock->release(); @@ -197,16 +222,17 @@ * @param int $rev The theme revision * @param int $themesubrev The theme sub-revision * @param string $candidatedir The directory that it should be stored in + * @param int $flavourid The flavour ID * @return string The path that the primary CSS was written to */ -function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir, $flavourid) { +function theme_boost_union_flavour_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir, $flavourid) { global $CFG; require_once("{$CFG->libdir}/filelib.php"); // Generate the content first. - if (!$csscontent = theme_boost_union_get_css_cached_content($theme, $flavourid)) { + if (!$csscontent = theme_boost_union_flavour_get_css_cached_content($theme, $flavourid)) { $csscontent = $theme->get_css_content(); - theme_boost_union_set_css_content_cache($theme, $csscontent); + theme_boost_union_flavour_set_css_content_cache($theme, $flavourid, $csscontent); } if ($theme->get_rtl_mode()) { @@ -216,7 +242,8 @@ function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidated } // Determine the candidatesheet path. - $candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $flavourid, $themesubrev, $theme->use_svg_icons()); + $candidatesheet = "{$candidatedir}/" . theme_boost_union_flavour_styles_get_filename($type, $themesubrev, $flavourid, + $theme->use_svg_icons()); // Store the CSS. css_store_css($theme, $candidatesheet, $csscontent); @@ -225,9 +252,20 @@ function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidated // This file is used as a fallback when waiting for a theme to compile and is not versioned in any way. $fallbacksheet = make_temp_directory("theme/{$theme->name}") . "/" - . theme_styles_get_filename($type, $flavourid, $theme->use_svg_icons()); + . theme_boost_union_flavour_styles_get_filename($type, $themesubrev, $flavourid, $theme->use_svg_icons()); css_store_css($theme, $fallbacksheet, $csscontent); + // Delete older revisions from localcache. + $themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR); + foreach ($themecachedirs as $localcachedir) { + $cachedrev = []; + preg_match("/\/theme\/([0-9]+)$/", $localcachedir, $cachedrev); + $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0; + if ($cachedrev > 0 && $cachedrev < $rev) { + fulldelete($localcachedir); + } + } + // Delete older theme subrevision CSS from localcache. $subrevfiles = glob("{$CFG->localcachedir}/theme/{$rev}/{$theme->name}/css/*.css"); foreach ($subrevfiles as $subrevfile) { @@ -257,7 +295,7 @@ function theme_styles_fallback_content($theme) { } $type = $theme->get_rtl_mode() ? 'all-rtl' : 'all'; - $filename = theme_styles_get_filename($type); + $filename = theme_boost_union_flavour_styles_get_filename($type); $fallbacksheet = "{$CFG->tempdir}/theme/{$theme->name}/{$filename}"; if (file_exists($fallbacksheet)) { @@ -272,10 +310,11 @@ function theme_styles_fallback_content($theme) { * * @param string $type The requested sheet type * @param int $themesubrev The theme sub-revision + * @param int $flavourid The flavour ID * @param bool $usesvg Whether SVGs are allowed * @return string The filename for this sheet */ -function theme_styles_get_filename($type, $themesubrev = 0, $flavourid = 0, $usesvg = true) { +function theme_boost_union_flavour_styles_get_filename($type, $themesubrev = 0, $flavourid = 0, $usesvg = true) { $filename = $type; $filename .= ($themesubrev > 0) ? "_{$themesubrev}" : ''; $filename .= ($flavourid > 0) ? "_{$flavourid}" : ''; @@ -307,10 +346,14 @@ function theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg) { /** * Return cached post processed CSS content. * + * This function is copied and modified from /lib/classes/output/theme_config.php + * + * @param theme_config $theme The theme to be generated + * @param int $flavourid The flavour ID * @return bool|string The cached css content or false if not found. */ -function theme_boost_union_get_css_cached_content($theme, $flavourid) { - $key = theme_boost_union_get_css_cache_key($theme, $flavourid); +function theme_boost_union_flavour_get_css_cached_content($theme, $flavourid) { + $key = theme_boost_union_flavour_get_css_cache_key($theme, $flavourid); $cache = cache::make('core', 'postprocessedcss'); return $cache->get($key); @@ -319,9 +362,13 @@ function theme_boost_union_get_css_cached_content($theme, $flavourid) { /** * Generate the css content cache key. * + * This function is copied and modified from /lib/classes/output/theme_config.php + * + * @param theme_config $theme The theme to be generated + * @param int $flavourid The flavour ID * @return string The post processed css cache key. */ -function theme_boost_union_get_css_cache_key($theme, $flavourid) { +function theme_boost_union_flavour_get_css_cache_key($theme, $flavourid) { $nosvg = (!$theme->use_svg_icons()) ? 'nosvg_' : ''; $rtlmode = ($theme->get_rtl_mode() == true) ? 'rtl' : 'ltr'; @@ -331,13 +378,16 @@ function theme_boost_union_get_css_cache_key($theme, $flavourid) { /** * Set post processed CSS content cache. * + * This function is copied and modified from /lib/classes/output/theme_config.php + * + * @param theme_config $theme The theme to be generated + * @param int $flavourid The flavour ID * @param string $csscontent The post processed CSS content. * @return bool True if the content was successfully cached. */ -function theme_boost_union_set_css_content_cache($theme, $csscontent) { - +function theme_boost_union_flavour_set_css_content_cache($theme, $flavourid, $csscontent) { $cache = cache::make('core', 'postprocessedcss'); - $key = $theme->get_css_cache_key(); + $key = theme_boost_union_flavour_get_css_cache_key($theme, $flavourid); return $cache->set($key, $csscontent); } diff --git a/lang/en/theme_boost_union.php b/lang/en/theme_boost_union.php index 1bae4e20deb..5b07dbfcb69 100644 --- a/lang/en/theme_boost_union.php +++ b/lang/en/theme_boost_union.php @@ -42,6 +42,9 @@ // Settings: General strings. $string['dontchange'] = 'Do not change anything'; +// Settings: General warnings. +$string['warningslashargumentsdisabled'] = 'Warning: The slasharguments setting is disabled in your Moodle configuration currently. However, this setting is required for the correct operation of the following Boost Union setting. Please enable slasharguments, otherwise the following Boost Union setting will not have any effect.'; + // Settings: Overview page. $string['settingsoverview'] = 'Settings overview'; $string['settingsoverview_title'] = 'Boost Union settings overview'; @@ -1175,11 +1178,13 @@ $string['flavoursbootstrapcolorsuccess_help'] = 'With this setting, the flavour will override the Bootstrap "success" color which is configured in Boost Union\'s look settings.'; $string['flavoursbootstrapcolorwarning'] = 'Bootstrap color for "Warning"'; $string['flavoursbootstrapcolorwarning_help'] = 'With this setting, the flavour will override the Bootstrap "warning" color which is configured in Boost Union\'s look settings.'; -$string['flavoursbrandcolour'] = 'Brand colour '; -$string['flavoursbrandcolour_help'] = 'With this setting, the flavour will override the brand colour which is configured in Boost Union\'s look settings.'; +$string['flavoursbrandcolor'] = 'Brand color'; +$string['flavoursbrandcolor_help'] = 'With this setting, the flavour will override the brand color which is configured in Boost Union\'s look settings.'; $string['flavourscreateflavour'] = 'Create flavour'; -$string['flavourscustomcss'] = 'Custom SCSS'; -$string['flavourscustomcss_help'] = 'With this setting, you can write custom SCSS for the flavour. It will be appended to the stack of CSS code which is shipped to the browser as soon as the flavour applies.'; +$string['flavourscustomscss'] = 'Raw SCSS'; +$string['flavourscustomscss_help'] = 'With this setting, you can write custom SCSS for the flavour. It will be appended to the stack of CSS code which is shipped to the browser as soon as the flavour applies.'; +$string['flavourscustomscsspre'] = 'Raw initial SCSS'; +$string['flavourscustomscsspre_help'] = 'With this setting, you can write custom initial SCSS for the flavour. It will be used when building the CSS code which is shipped to the browser as soon as the flavour applies.'; $string['flavoursdelete'] = 'Delete'; $string['flavoursdeleteflavour'] = 'Delete flavour'; $string['flavoursdeleteconfirmation'] = 'Do you really want to delete the flavour {$a}?'; diff --git a/lib.php b/lib.php index f4f4d5f7b15..94c42a8e380 100644 --- a/lib.php +++ b/lib.php @@ -141,11 +141,12 @@ function theme_boost_union_get_main_scss_content($theme) { global $CFG; - $scss = ''; - // Require Boost Core library. require_once($CFG->dirroot.'/theme/boost/lib.php'); + // Initialize SCSS code. + $scss = ''; + // Include pre.scss from Boost Union. $scss .= file_get_contents($CFG->dirroot . '/theme/boost_union/scss/boost_union/pre.scss'); @@ -173,11 +174,29 @@ function theme_boost_union_get_main_scss_content($theme) { * @return string */ function theme_boost_union_get_pre_scss($theme) { - global $CFG, $flavourid; + global $CFG; // Require local library. require_once($CFG->dirroot . '/theme/boost_union/locallib.php'); + // Pick the active flavour from the global scope. + // This global variable is set in /theme/boost_union/flavour/styles.php. + // It is only set by that file and only needed in this and another single function in this file. + // This approach feels a bit hacky but it is the most efficient way to get the flavour ID into this function. + global $themeboostunionappliedflavour; + if (isset($themeboostunionappliedflavour)) { + $flavourid = $themeboostunionappliedflavour; + } else { + $flavourid = null; + } + + // If any flavour applies to this page. + if ($flavourid != null) { + // Require flavours library. + require_once($CFG->dirroot . '/theme/boost_union/flavours/flavourslib.php'); + } + + // Initialize SCSS code. $scss = ''; // Add SCSS constants for evaluating select setting values in SCSS code. @@ -193,25 +212,44 @@ function theme_boost_union_get_pre_scss($theme) { 'bootstrapcolordanger' => ['danger'], ]; + // Define the configurables which can be overridden by flavours. + // The key is the configurable and the value is the field name in mdl_theme_boost_union_flavours. + $flavourconfigurable = [ + 'brandcolor' => 'look_brandcolor', + 'bootstrapcolorsuccess' => 'look_bootstrapcolorsuccess', + 'bootstrapcolorinfo' => 'look_bootstrapcolorinfo', + 'bootstrapcolorwarning' => 'look_bootstrapcolorwarning', + 'bootstrapcolordanger' => 'look_bootstrapcolordanger', + ]; + // Prepend variables first. foreach ($configurable as $configkey => $targets) { + // Get the global config value for the given config key. $value = isset($theme->settings->{$configkey}) ? $theme->settings->{$configkey} : null; - // Check if flavour is active. - if (isset($flavourid)) { - // Require flavours library. - require_once($CFG->dirroot . '/theme/boost_union/flavours/flavourslib.php'); - // Get flavour config for id. - $flavourvalue = theme_boost_union_get_flavour_config_item_for_id($flavourid, $configkey); - // Check override value. - if (!($flavourvalue)) { + + // If any flavour applies to this page. + if ($flavourid != null) { + // If the configurable can be overridden by flavours. + if (array_key_exists($configkey, $flavourconfigurable)) { + // Pick the flavour config key. + $flavourconfigkey = $flavourconfigurable[$configkey]; + } + // Get the flavour config value for the given flavour id. + $flavourvalue = theme_boost_union_get_flavour_config_item_for_flavourid($flavourid, $flavourconfigkey); + // If the value is not set, continue. + if ($flavourvalue == null || empty($flavourvalue)) { continue; } - // Override em. + // Otherwise, override the global config value with the flavour value. $value = $flavourvalue; } + + // If the value is not set, continue. if (!($value)) { continue; } + + // Otherwise, set the SCSS variable. array_map(function($target) use (&$scss, $value) { $scss .= '$' . $target . ': ' . $value . ";\n"; }, (array) $targets); @@ -290,28 +328,16 @@ function theme_boost_union_get_pre_scss($theme) { // Get and include the external Pre SCSS. $scss .= theme_boost_union_get_external_scss('pre'); - if (isset($flavourid)) { - $rawscsspre = theme_boost_union_get_flavour_config_item_for_id($flavourid, 'look_rawscss'); - // Append pre-scss. - if ($rawscsspre !== false) { - $scss .= "\n/** RAW-SCSS from theme_boost_union_get_pre_scss **/\n" . $rawscsspre; - } - - // If the flavour has a background image. - $backgroundimage = theme_boost_union_get_flavour_config_item_for_id($flavourid, 'look_backgroundimage'); - if ($backgroundimage != false) { - // Compose the URL to the flavour's background image. - $backgroundimageurl = moodle_url::make_pluginfile_url( - context_system::instance()->id, 'theme_boost_union', 'flavours_look_backgroundimage', $flavourid, - '/'.theme_get_revision(), '/'.$backgroundimage); - - // And add it to the SCSS code, adhering the fact that we must not overwrite the login page background image again. - $scss .= 'body:not(.pagelayout-login) { '; - $scss .= 'background-image: url("'.$backgroundimageurl.'");'; - $scss .= '}'; + // If any flavour applies to this page. + if ($flavourid != null) { + // If there is any raw Pre SCSS in the flavour. + $flavourrawscsspre = theme_boost_union_get_flavour_config_item_for_flavourid($flavourid, 'look_rawscsspre'); + // Append it to the SCSS stack. + if ($flavourrawscsspre != null && !empty($flavourrawscsspre)) { + $scss .= $flavourrawscsspre; } } - // Since setting "precss" is originally from parent boost it is added in theme_boost_get_pre_scss. + return $scss; } @@ -327,6 +353,23 @@ function theme_boost_union_get_extra_scss($theme) { // Require the necessary libraries. require_once($CFG->dirroot . '/course/lib.php'); + // Pick the active flavour from the global scope. + // This global variable is set in /theme/boost_union/flavour/styles.php. + // It is only set by that file and only needed in this and another single function in this file. + // This approach feels a bit hacky but it is the most efficient way to get the flavour ID into this function. + global $themeboostunionappliedflavour; + if (isset($themeboostunionappliedflavour)) { + $flavourid = $themeboostunionappliedflavour; + } else { + $flavourid = null; + } + + // If any flavour applies to this page. + if ($flavourid != null) { + // Require flavours library. + require_once($CFG->dirroot . '/theme/boost_union/flavours/flavourslib.php'); + } + // Initialize extra SCSS. $content = ''; @@ -387,10 +430,34 @@ function theme_boost_union_get_extra_scss($theme) { $content .= "background-attachment: fixed;"; $content .= '}'; - // Note: Boost Union is also capable of overriding the background image in its flavours. - // In contrast to the other flavour assets like the favicon overriding, this isn't done here in place as this function - // is composing Moodle core CSS which has to remain flavour-independent. - // Instead, the flavour is overriding the background image later in theme_boost_union_get_pre_scss() lib.php. + // One more thing: Boost Union is also capable of overriding the background image in its flavours. + // So, if any flavour applies to this page. + if ($flavourid != null) { + // And if the flavour has a background image. + $backgroundimage = theme_boost_union_get_flavour_config_item_for_flavourid($flavourid, 'look_backgroundimage'); + if ($backgroundimage != null && !empty($backgroundimage)) { + // Compose the URL to the flavour's background image. + $backgroundimageurl = moodle_url::make_pluginfile_url( + context_system::instance()->id, 'theme_boost_union', 'flavours_look_backgroundimage', $flavourid, + '/'.theme_get_revision(), '/'.$backgroundimage); + + // And add it to the SCSS code, adhering the fact that we must not overwrite the login page background image again. + $content .= 'body:not(.pagelayout-login) { '; + $content .= 'background-image: url("'.$backgroundimageurl.'");'; + $content .= '}'; + } + } + + // Now we want to add the custom SCSS from the flavour. + // If any flavour applies to this page. + if ($flavourid != null) { + // If there is any raw SCSS in the flavour. + $flavourrawscss = theme_boost_union_get_flavour_config_item_for_flavourid($flavourid, 'look_rawscss'); + // Append it to the SCSS stack. + if ($flavourrawscss != null && !empty($flavourrawscss)) { + $content .= $flavourrawscss; + } + } // For the rest of this function, we add SCSS snippets to the SCSS stack based on enabled admin settings. // This is done here as it is quite easy to do. As an alternative, it could also been done in post.scss by using @@ -715,6 +782,15 @@ function theme_boost_union_before_session_start() { theme_boost_union_manipulate_hooks(); } +/** + * Callback function which allows themes to alter the CSS URLs. + * We use this function to change the CSS URL to the flavour CSS URL if a flavour applies to the current page. + * + * @copyright 2023 Mario Wehr + * based on example code by Bas Brands from https://github.com/bmbrands/theme_picture/blob/change_css_urls/lib.php. + * + * @param mixed $urls The CSS URLs (passed as reference). + */ function theme_boost_union_alter_css_urls(&$urls) { global $CFG; @@ -729,14 +805,22 @@ function theme_boost_union_alter_css_urls(&$urls) { return; } + // Iterate over the CSS URLs. foreach (array_keys($urls) as $i) { - if ($urls[$i] instanceof moodle_url) { + // If we have a moodle_url object. + if ($urls[$i] instanceof \core\url) { + // Take the flavour CSS URL and escape it to be used in a regular expression. $pathstyles = preg_quote($CFG->wwwroot . '/theme/styles.php', '|'); + // Replace the CSS URL with the flavour CSS URL. + // As a result, the file /theme/boost_union/flavours/styles.php is called instead of /theme/styles.php and the + // flavour ID is injected into the URL parameters. if (preg_match("|^$pathstyles(/_s)?(.*)$|", $urls[$i]->out(false), $matches)) { + // Do the whole operation only if slasharguments are enabled. + // A warning is shown on the flavour edit page if slasharguments is off. if (!empty($CFG->slasharguments)) { $parts = explode('/', $matches[2]); $parts[3] = $flavour->id . '/' . $parts[3]; - $urls[$i] = new moodle_url('/theme/boost_union/flavours/css.php'); + $urls[$i] = new moodle_url('/theme/boost_union/flavours/styles.php'); $urls[$i]->set_slashargument($matches[1] . join('/', $parts)); } } diff --git a/tests/behat/theme_boost_union_flavourssettings_application.feature b/tests/behat/theme_boost_union_flavourssettings_application.feature index 7189d8dc373..b9743044f2b 100644 --- a/tests/behat/theme_boost_union_flavourssettings_application.feature +++ b/tests/behat/theme_boost_union_flavourssettings_application.feature @@ -55,7 +55,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, applyi And I set the field "Title" to "Cat 1 flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -94,7 +94,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, applyi And I set the field "Title" to "Cat 1 flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -133,7 +133,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, applyi And I set the field "Title" to "Cat 1 flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -169,7 +169,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, applyi And I set the field "Title" to "Cohort 1 flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on the dashboard. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-my #page-header h1 { display: none; } """ @@ -199,7 +199,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, applyi And I set the field "Title" to "Cohort 1 flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on the dashboard. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-my #page-header h1 { display: none; } """ @@ -231,7 +231,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, applyi And I set the field "Title" to "Cat 1a flavour" # We add a small CSS snippet to the flavour which colorizes the heading in the page header on the dashboard. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { color: red; } """ @@ -249,7 +249,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, applyi And I set the field "Title" to "Cat 1b flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on the dashboard. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ diff --git a/tests/behat/theme_boost_union_flavourssettings_caching.feature b/tests/behat/theme_boost_union_flavourssettings_caching.feature index 14c9ba73ef8..0e74174487a 100644 --- a/tests/behat/theme_boost_union_flavourssettings_caching.feature +++ b/tests/behat/theme_boost_union_flavourssettings_caching.feature @@ -33,7 +33,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Effective flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -58,7 +58,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Effective flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -77,7 +77,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Non-effective flavour" # We add a small CSS snippet to the flavour which shows the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: block; } """ @@ -113,7 +113,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Effective flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -132,7 +132,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Non-effective flavour" # We add a small CSS snippet to the flavour which shows the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: block; } """ @@ -162,7 +162,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Effective flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -180,7 +180,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Non-effective flavour" # We add a small CSS snippet to the flavour which shows the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: block; } """ @@ -221,7 +221,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Effective flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -239,7 +239,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Non-effective flavour" # We add a small CSS snippet to the flavour which shows the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: block; } """ @@ -276,7 +276,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Effective flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -294,7 +294,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Non-effective flavour" # We add a small CSS snippet to the flavour which shows the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: block; } """ @@ -334,7 +334,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Effective flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: none; } """ @@ -352,7 +352,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, cachin And I set the field "Title" to "Non-effective flavour" # We add a small CSS snippet to the flavour which shows the heading in the page header on course pages and category overview pages. # This is just to make it easy to detect if this flavour is applied or not. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ .path-course-view #page-header h1, .path-course-index #page-header h1 { display: block; } """ diff --git a/tests/behat/theme_boost_union_flavourssettings_look.feature b/tests/behat/theme_boost_union_flavourssettings_look.feature index cfc91c3c4aa..5522d86ce81 100644 --- a/tests/behat/theme_boost_union_flavourssettings_look.feature +++ b/tests/behat/theme_boost_union_flavourssettings_look.feature @@ -137,7 +137,7 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, applyi And I set the field "Title" to "My shiny new flavour" # We add a small CSS snippet to the flavour which hides the heading in the page header. # This is just to make it easy to detect the effect of this flavour. - And I set the field "Custom CSS" to multiline: + And I set the field "Raw SCSS" to multiline: """ #page-header h1 { display: none; } """ diff --git a/version.php b/version.php index 583b23df43d..d281eb51852 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'theme_boost_union'; -$plugin->version = 2024100705; +$plugin->version = 2024100706; $plugin->release = 'v4.5-r4'; $plugin->requires = 2024100700; $plugin->supported = [405, 405]; From 74d12ec766d0b3b2417602553535298b03e8e8ca Mon Sep 17 00:00:00 2001 From: Alexander Bias Date: Fri, 27 Dec 2024 11:53:04 +0100 Subject: [PATCH 4/6] Bugfix: Fix the order in which all the pre SCSS assets are added to the SCSS stack, resolves #788 + Documentation: Explain the SCSS stack order in the README --- CHANGES.md | 2 ++ README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib.php | 16 ++++++++++++--- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e7cb95494b1..bee16a579db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ Changes ### Unreleased +* 2024-12-25 - Documentation: Explain the SCSS stack order in the README. +* 2024-12-25 - Bugfix: Fix the order in which all the pre SCSS assets are added to the SCSS stack, resolves #788. * 2024-12-22 - Feature: Allow overwriting of brand colors and the usage of SCSS (instead of pure CSS) in flavours, resolves #155. * 2024-12-15 - Feature: Add declaration of accessibility page and accessibility support page, resolves #567. diff --git a/README.md b/README.md index caea584fd49..e2f10927713 100644 --- a/README.md +++ b/README.md @@ -738,6 +738,63 @@ https://github.com/moodle-an-hochschulen/moodle-theme_boost_union_child While Boost Union Child will surely help you to realize all your local Boost Union dreams, please do yourself and the whole community a favour and verify that your planned features are indeed not interesting as a pull request or feature request for the whole Boost Union community and could be contributed to Boost Union directly instead. +SCSS stack order +---------------- + +Within Boost Union, you have multiple possibilities to add your own SCSS code to the Moodle page. And many of the Boost Union settings add SCSS code as well to realize the particular setting's goal. However, as you know, in SCSS the order of the instructions is key. + +The following list should give you an insight in which order all the SCSS code is added to the CSS stack which is shipped to the browser in the end. +To fully understand this list, you have to be aware of two terms in Moodle theming: + +* _Pre SCSS_ or _Raw Initial SCSS_:\ + This SCSS code is used only to initialize SCSS variables and not to write real SCSS code directly. +* _Post SCSS_ or _Raw SCSS_:\ + This SCSS code is the real SCSS code which is compiled to CSS for the browser and which might consume the SCSS variables which have been set in the Pre SCSS. + +Having said that, here's the order how all the SCSS code is added to the SCSS stack: + +1. All plugins' `styles.css` files:\ + Each Moodle plugin can ship with a `styles.css` file which contains CSS code (not SCSS code!) for the plugin. These files are added at the very beginning in the order of the plugin names and types. + +2. `theme_boost` > `get_pre_scss()`: + * Adds the Boost Union Pre SCSS from the theme settings\ + (which is set on `/admin/settings.php?section=theme_boost_union_look#theme_boost_union_look_scss`).\ + Note: In fact, this function adds the _active theme's_ Pre SCSS which becomes important if you use a Boost Union Child theme. + +3. `theme_boost_union` > `get_pre_scss()`: + * Adds the Boost Union Pre SCSS from disk\ + (which is located on `/theme/boost_union/scss/boost_union/pre.scss` and which is empty currently) + * Sets several SCSS variables based on Boost Union or Boost Union flavour settings + * Adds the Boost Union external Pre SCSS\ + (which is set on `/admin/settings.php?section=theme_boost_union_look#theme_boost_union_look_scss`) + * Adds the Boost Union flavour Pre SCSS\ + (which is set within the active flavour on `/theme/boost_union/flavours/overview.php`) + +4. `theme_boost_union` > `get_main_scss()`: + * Calls the `theme_boost` > `get_main_scss()` function + * Adds the Boost Core Preset\ + (which is set on `/admin/settings.php?section=themesettingboost` and defaults to the `/theme/boost/scss/preset/default.scss` file). + With this preset, the FontAwesome library, the Bootstrap library and all the Moodle core stylings are added which means that this preset is the place where all the Moodle core style is added. + * Adds the Boost Union Post SCSS from disk\ + (which is located on `/theme/boost_union/scss/boost_union/post.scss`) + This file holds all the Boost Union specific SCSS code which can be added to the stack without being dependent on specific configurations like configured colors or sizes. + * Add the Boost Union external SCSS\ + (which is set on `/admin/settings.php?section=theme_boost_union_look#theme_boost_union_look_scss`) + +5. `theme_boost` > `get_extra_scss()`: + * Adds the Boost Union Post SCSS from the theme settings\ + (which is set on `/admin/settings.php?section=theme_boost_union_look#theme_boost_union_look_scss`).\ + Note: In fact, this function adds the _active theme's_ Post SCSS which becomes important if you use a Boost Union Child theme. + * Adds the page background image and login page background image + +6. `theme_boost_union` > `get_extra_scss()`: + * Overrides / enhances the background images which have been set before + * Adds the Boost Union flavour Post SCSS\ + (which is set within the active flavour on `/theme/boost_union/flavours/overview.php`) + * Adds the Boost Union features' SCSS. + This is the Boost Union specific SCSS code which has to be added to the stack based on specific configurations, for example for changing the activity icon purposes or for changing the login form order. + + Plugin repositories ------------------- diff --git a/lib.php b/lib.php index 94c42a8e380..2fd3f6a37b7 100644 --- a/lib.php +++ b/lib.php @@ -147,9 +147,6 @@ function theme_boost_union_get_main_scss_content($theme) { // Initialize SCSS code. $scss = ''; - // Include pre.scss from Boost Union. - $scss .= file_get_contents($CFG->dirroot . '/theme/boost_union/scss/boost_union/pre.scss'); - // Get and include the main SCSS from Boost Core. // This particularly covers the theme preset which is set in Boost Core and not Boost Union. $scss .= theme_boost_get_main_scss_content(\core\output\theme_config::load('boost')); @@ -199,6 +196,19 @@ function theme_boost_union_get_pre_scss($theme) { // Initialize SCSS code. $scss = ''; + // You might think that this pre SCSS function is only called for the activated theme. + // However, due to the way how the theme_*_get_pre_scss callback functions are searched and called within Boost child theme + // hierarchy Boost Union not only gets the pre SCSS from this function here but only from theme_boost_get_pre_scss as well. + // + // There, the custom Pre SCSS from $theme->settings->scsspre (which hits the SCSS settings from theme_boost_union even though + // the code is within theme_boost) is already added to the SCSS codebase. + // + // We have to accept this fact here and must not copy the code from theme_boost_get_pre_scss into this function. + // Instead, we must only add additionally CSS code which is based on any Boost Union-only functionality. + + // Include pre.scss from Boost Union. + $scss .= file_get_contents($CFG->dirroot . '/theme/boost_union/scss/boost_union/pre.scss'); + // Add SCSS constants for evaluating select setting values in SCSS code. $scss .= '$boostunionsettingyes: '.THEME_BOOST_UNION_SETTING_SELECT_YES. ";\n"; $scss .= '$boostunionsettingno: '.THEME_BOOST_UNION_SETTING_SELECT_NO. ";\n"; From e40640e864462b83945ef38b00e2e957815d6313 Mon Sep 17 00:00:00 2001 From: Alexander Bias Date: Fri, 27 Dec 2024 11:56:33 +0100 Subject: [PATCH 5/6] Add Lutheran University of Applied Sciences Nuremberg to credits list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e2f10927713..595f8ed1fef 100644 --- a/README.md +++ b/README.md @@ -912,6 +912,7 @@ Moodle an Hochschulen e.V. would like to thank these main contributors (in alpha * lern.link GmbH, Alexander Bias: Code, Peer Review, Ideating, Funding * lern.link GmbH, Lukas MuLu Müller: Code * lern.link GmbH, Beata Waloszczyk: Code +* Lutheran University of Applied Sciences Nuremberg: Funding * Moodle.NRW / Ruhr University Bochum, Annika Lambert: Code * Moodle.NRW / Ruhr University Bochum, Matthias Buttgereit: Code, Ideating * Moodle.NRW / Ruhr University Bochum, Tim Trappen: Code, Ideating From d0515fe81081b046b87375b8a9e570fa26e018a1 Mon Sep 17 00:00:00 2001 From: Alexander Bias Date: Sat, 28 Dec 2024 14:51:43 +0100 Subject: [PATCH 6/6] Review additions: Add Behat tests --- lib.php | 9 +-- ..._boost_union_flavourssettings_look.feature | 76 ++++++++++++++++++- ...heme_boost_union_looksettings_scss.feature | 23 ++---- 3 files changed, 81 insertions(+), 27 deletions(-) diff --git a/lib.php b/lib.php index 2fd3f6a37b7..a228542a189 100644 --- a/lib.php +++ b/lib.php @@ -807,14 +807,13 @@ function theme_boost_union_alter_css_urls(&$urls) { // Require flavours library. require_once($CFG->dirroot . '/theme/boost_union/flavours/flavourslib.php'); + // In the original code, Bas commented: "No CSS switch during behat runs, or it will take ages to run a scenario." + // While there is a reason for this in Bas' context, We do not have to care about this as we do only change the URL + // if a flavour applies and in these cases, the CSS must be switched in any case. + // If any flavour applies to this page. $flavour = theme_boost_union_get_flavour_which_applies(); if ($flavour != null) { - if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) { - // No CSS switch during behat runs, or it will take ages to run a scenario. - return; - } - // Iterate over the CSS URLs. foreach (array_keys($urls) as $i) { // If we have a moodle_url object. diff --git a/tests/behat/theme_boost_union_flavourssettings_look.feature b/tests/behat/theme_boost_union_flavourssettings_look.feature index 5522d86ce81..95d3f7a95fa 100644 --- a/tests/behat/theme_boost_union_flavourssettings_look.feature +++ b/tests/behat/theme_boost_union_flavourssettings_look.feature @@ -129,17 +129,85 @@ Feature: Configuring the theme_boost_union plugin on the "Flavours" page, applyi # Scenario: Flavours: Background image - Do not upload a background image (with a global background image being served properly) @javascript - Scenario: Flavours: Custom SCSS - Add custom SCSS to the page + Scenario: Flavours: Brand color - Set the brand color + Given the following "categories" exist: + | name | category | idnumber | + | Cat 1 | 0 | CAT1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | CAT1 | When I log in as "admin" And I navigate to "Appearance > Boost Union > Flavours" in site administration And I click on "Create flavour" "button" And I should see "Create flavour" in the "#page-header h1" "css_element" + And I expand all fieldsets And I set the field "Title" to "My shiny new flavour" - # We add a small CSS snippet to the flavour which hides the heading in the page header. - # This is just to make it easy to detect the effect of this flavour. + And I set the field "look_brandcolor" to "#FF0000" + And I select "Yes" from the "Apply to course categories" singleselect + And I click on ".form-autocomplete-downarrow" "css_element" in the "#fitem_id_applytocategories_ids" "css_element" + And I click on "Cat 1" item in the autocomplete list + And I press the escape key + And I click on "Save changes" "button" + And the following "activities" exist: + | activity | name | intro | course | + | label | Label one | My test text | C1 | + When I log in as "admin" + And I am on "Course 1" course homepage + And I should see "My test text" + Then DOM element ".mytesttext" should have computed style "color" "rgb(255, 0, 0)" + + @javascript + Scenario Outline: Flavours: Bootstrap colors - Set the Bootstrap colors + Given the following "categories" exist: + | name | category | idnumber | + | Cat 1 | 0 | CAT1 | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | CAT1 | + When I log in as "admin" + And I navigate to "Appearance > Boost Union > Flavours" in site administration + And I click on "Create flavour" "button" + And I should see "Create flavour" in the "#page-header h1" "css_element" + And I expand all fieldsets + And I set the field "Title" to "My shiny new flavour" + And I set the field "look_bootstrapcolor" to "" + And I select "Yes" from the "Apply to course categories" singleselect + And I click on ".form-autocomplete-downarrow" "css_element" in the "#fitem_id_applytocategories_ids" "css_element" + And I click on "Cat 1" item in the autocomplete list + And I press the escape key + And I click on "Save changes" "button" + And the following "activities" exist: + | activity | name | intro | course | + | label | Label one | My test text | C1 | + When I log in as "admin" + And I am on "Course 1" course homepage + And I should see "My test text" + Then DOM element ".mytesttext" should have computed style "color" "" + + Examples: + | type | colorhex | colorrgb | + | success | #FF0000 | rgb(255, 0, 0) | + | info | #00FF00 | rgb(0, 255, 0) | + | warning | #0000FF | rgb(0, 0, 255) | + | danger | #FFFF00 | rgb(255, 255, 0) | + + @javascript + Scenario: Flavours: Raw (initial) SCSS - Add custom SCSS to the page + When I log in as "admin" + And I navigate to "Appearance > Boost Union > Flavours" in site administration + And I click on "Create flavour" "button" + And I should see "Create flavour" in the "#page-header h1" "css_element" + And I expand all fieldsets + And I set the field "Title" to "My shiny new flavour" + # We add a SCSS variable and a small SCSS snippet to the flavour which hides the heading in the page header. + # This is just to make it easy to detect the effect of this flavour and to verify that SCSS is compiled correctly. + And I set the field "Raw initial SCSS" to multiline: + """ + $myvariable: none; + """ And I set the field "Raw SCSS" to multiline: """ - #page-header h1 { display: none; } + #page-header h1 { display: $myvariable; } """ And I click on "Save changes" "button" And I should see "Flavours" in the "#region-main h2" "css_element" diff --git a/tests/behat/theme_boost_union_looksettings_scss.feature b/tests/behat/theme_boost_union_looksettings_scss.feature index 8c6a7dd2ebd..604962442fe 100644 --- a/tests/behat/theme_boost_union_looksettings_scss.feature +++ b/tests/behat/theme_boost_union_looksettings_scss.feature @@ -10,33 +10,20 @@ Feature: Configuring the theme_boost_union plugin for the "SCSS" tab on the "Loo | Course 1 | C1 | @javascript - Scenario: Setting: Raw initial SCSS - Add custom SCSS to the theme + Scenario: Setting: Raw (initial) SCSS - Add custom SCSS to the theme When I log in as "admin" And Behat debugging is disabled And I navigate to "Appearance > Boost Union > Look" in site administration And I click on "SCSS" "link" in the "#adminsettings .nav-tabs" "css_element" - # We add a small CSS snippet to the page which hides the heading in the page header. - # This is just to make it easy to detect the effect of this custom SCSS code. + # We add a SCSS variable and a small SCSS snippet to the page which hides the heading in the page header. + # This is just to make it easy to detect the effect of this custom SCSS code and to verify that SCSS is compiled correctly. And I set the field "Raw initial SCSS" to multiline: """ - #page-header h1 { display: none; } + $myvariable: none; """ - And I press "Save changes" - And Behat debugging is enabled - And I am on "Course 1" course homepage - Then I should not see "Course 1" in the "#page-header .page-header-headings" "css_element" - - @javascript - Scenario: Setting: Raw SCSS - Add custom SCSS to the theme - When I log in as "admin" - And Behat debugging is disabled - And I navigate to "Appearance > Boost Union > Look" in site administration - And I click on "SCSS" "link" in the "#adminsettings .nav-tabs" "css_element" - # We add a small CSS snippet to the page which hides the heading in the page header. - # This is just to make it easy to detect the effect of this custom SCSS code. And I set the field "Raw SCSS" to multiline: """ - #page-header h1 { display: none; } + #page-header h1 { display: $myvariable; } """ And I press "Save changes" And Behat debugging is enabled