diff --git a/CHANGES.md b/CHANGES.md index 70b9352bf5d..bee16a579db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,9 @@ 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. ### v4.5-r4 diff --git a/README.md b/README.md index caea584fd49..595f8ed1fef 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 ------------------- @@ -855,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 diff --git a/classes/form/flavour_edit_form.php b/classes/form/flavour_edit_form.php index e6d1a115e97..5fdf0308517 100644 --- a/classes/form/flavour_edit_form.php +++ b/classes/form/flavour_edit_form.php @@ -51,12 +51,29 @@ 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; // 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); @@ -80,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, [ @@ -100,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, [ @@ -110,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, [ @@ -120,14 +164,94 @@ public function definition() { ]); $mform->addHelpButton('flavours_look_backgroundimage', 'flavoursbackgroundimage', 'theme_boost_union'); - // 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]); + // 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).'
' + ); + + // Add brandcolor as colorpicker element. + $this->check_slasharguments_warning($mform); + $mform->addElement( + '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).'
' + ); + + // Add Bootstrap color for 'success' as colorpicker element. + $this->check_slasharguments_warning($mform); + $mform->addElement( + '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( + '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( + '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( + '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_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', 'flavourscustomcss', 'theme_boost_union'); + $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')); @@ -184,4 +308,66 @@ public function definition() { // Add the action buttons. $this->add_action_buttons(); } + + /** + * Theme Boost Union - Flavours edit form validation + * + * 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, + * or an empty array if everything is OK (true allowed for backwards compatibility too). + */ + public function validation($data, $files) { + global $PAGE; + + $errors = []; + + // 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. + } + } + } + } + + 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 c7547e890ed..145b1582c35 100644 --- a/classes/form/smartmenu_item_edit_form.php +++ b/classes/form/smartmenu_item_edit_form.php @@ -47,11 +47,18 @@ 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' + ); + // 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. @@ -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/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; } diff --git a/classes/formelement/colorpicker_rule.php b/classes/formelement/colorpicker_rule.php new file mode 100644 index 00000000000..dc0f4f4e200 --- /dev/null +++ b/classes/formelement/colorpicker_rule.php @@ -0,0 +1,105 @@ +. + +/** + * 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 HTML_QuickForm_Rule; + +/** + * Validation rule for color picker + * + * 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 colorpicker_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 = [ + '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/db/install.xml b/db/install.xml index a1bb583694c..aef4e9a381c 100644 --- a/db/install.xml +++ b/db/install.xml @@ -21,6 +21,12 @@ + + + + + + diff --git a/db/upgrade.php b/db/upgrade.php index 8ada2daee60..3024878eb1c 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -348,5 +348,62 @@ function xmldb_theme_boost_union_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2024100702, 'theme', 'boost_union'); } + if ($oldversion < 2024100706) { + + // Define table theme_boost_union_flavours to be altered. + $table = new xmldb_table('theme_boost_union_flavours'); + + // 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 look_rawscsspre. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // 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 look_brandcolor. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // 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 look_bootstrapcolorsuccess. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // 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 look_bootstrapcolorinfo. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // 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 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, 2024100706, 'theme', 'boost_union'); + } + return true; } diff --git a/flavours/edit.php b/flavours/edit.php index d1738ecb34f..a13bbe0e798 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. And delete 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..80101b20fae 100644 --- a/flavours/flavourslib.php +++ b/flavours/flavourslib.php @@ -313,3 +313,35 @@ function theme_boost_union_flavour_exists_for_cohort($cohortid) { // We didn't find any matching cohort, return false. return false; } + +/** + * Helper function to get a config item from the given flavour ID. + * + * 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. + * + * @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_flavourid(string $flavourid, string $configkey) { + global $DB; + + // Initialize static variable for the flavour record as this function might be called multiple times during a page output. + static $flavourrecord; + + // 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 the flavour record has a config item with the given key. + if (isset($flavourrecord->{$configkey})) { + // Return it. + return $flavourrecord->{$configkey}; + } + + // Fallback: Return null. + return null; +} diff --git a/flavours/styles.php b/flavours/styles.php index c5ef1c93ed1..494104ab7c5 100644 --- a/flavours/styles.php +++ b/flavours/styles.php @@ -15,74 +15,379 @@ // along with Moodle. If not, see . /** - * Theme Boost Union - Flavours styles serving + * This file is responsible for serving the one huge CSS of each theme. * - * @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 + * 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 */ -// Do not show any debug messages and any errors which might break the shipped CSS. +// 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); -// Do not do any upgrade checks here. -define('NO_UPGRADE_CHECK', 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(); + } -// 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 + if (strpos($slashargument, '_s/') === 0) { + // Can't use SVG. + $slashargument = substr($slashargument, 3); + $usesvg = false; + } else { + $usesvg = true; + } -// 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()); + 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, 'INT'); + $type = 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'); +} + +// 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); +$rev = min_clean_param(array_shift($values), 'INT'); +$themesubrev = array_shift($values); + +if (!is_null($themesubrev)) { + $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(); +} + +// 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. +} else { + header('HTTP/1.0 404 not found'); + die('Theme was not found, sorry.'); +} + +$candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css"; +$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)) { + 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_boost_union_flavour_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_boost_union_flavour_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 + * @param int $flavourid The flavour ID + * @return string The path that the primary CSS was written to + */ +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_flavour_get_css_cached_content($theme, $flavourid)) { + $csscontent = $theme->get_css_content(); + theme_boost_union_flavour_set_css_content_cache($theme, $flavourid, $csscontent); + } + + if ($theme->get_rtl_mode()) { + $type = "all-rtl"; + } else { + $type = "all"; + } + + // Determine the candidatesheet path. + $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); + + // 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_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) { + $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_boost_union_flavour_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 int $flavourid The flavour ID + * @param bool $usesvg Whether SVGs are allowed + * @return string The filename for this sheet + */ +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}" : ''; + $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. + * + * 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_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); +} + +/** + * 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_flavour_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. + * + * 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_flavour_set_css_content_cache($theme, $flavourid, $csscontent) { + $cache = cache::make('core', 'postprocessedcss'); + $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 6de97b36214..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'; @@ -1167,9 +1170,21 @@ $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['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 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['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 3d6800a45bd..a228542a189 100644 --- a/lib.php +++ b/lib.php @@ -141,13 +141,11 @@ function theme_boost_union_get_main_scss_content($theme) { global $CFG; - $scss = ''; - // Require Boost Core library. require_once($CFG->dirroot.'/theme/boost/lib.php'); - // Include pre.scss from Boost Union. - $scss .= file_get_contents($CFG->dirroot . '/theme/boost_union/scss/boost_union/pre.scss'); + // Initialize SCSS code. + $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. @@ -178,8 +176,39 @@ function theme_boost_union_get_pre_scss($theme) { // 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 = ''; + // 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"; @@ -193,12 +222,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) { - $value = get_config('theme_boost_union', $configkey); + // Get the global config value for the given config key. + $value = isset($theme->settings->{$configkey}) ? $theme->settings->{$configkey} : null; + + // 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; + } + // 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); @@ -277,9 +338,14 @@ 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 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; + } } return $scss; @@ -297,6 +363,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 = ''; @@ -357,10 +440,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 flavours/styles.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 @@ -684,3 +791,49 @@ function theme_boost_union_before_session_start() { // Manipulate Moodle core hooks. 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; + + // 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) { + // Iterate over the CSS URLs. + foreach (array_keys($urls) as $i) { + // 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/styles.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(); 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..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 "Custom CSS" to multiline: + 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 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];