Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Creating a theme based on boost

From MoodleDocs
Revision as of 09:58, 16 August 2023 by koen roggemans (talk | contribs) (→‎Theme specific files: added missing setting in config.php)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Moodle 3.2


This is a tutorial for how to create a new theme in any version of Moodle from 3.2 and later.

Moodle 3.2 introduced a new core theme named "Boost" which is a great starting point for themers wanting to build a modern Moodle theme utilising all the latest features available to themes in Moodle. If you aren't sure where to start, then START HERE!  :-)

Getting started

What is a theme? A theme in Moodle is just another type of plugin that can be developed. Themes are responsible for setting up the structure of each page and have the ability to customise the output of any page in Moodle.

This tutorial is based on a working theme named "theme_photo" you can download it or view the source code for it here: https://github.com/damyon/moodle-theme_photo

Choosing a name

Your new theme will need a name. Try and think of something short and memorable - and make sure it is not a name that has already been used by someone else. A quick search on the moodle.org/plugins can save you a lot of work renaming things later.

Lets call our new example theme "photo" as we will add some settings to allow "photos" in various places in Moodle.

Starting files

As a plugin, themes must start with the basic structure of a plugin in Moodle. See https://docs.moodle.org/dev/Tutorial#The_skeleton_of_your_plugin for an overview of the files common to all plugins in Moodle.

Following this guide we can start creating our theme. First we create the folder for the new theme under "/theme/" folder in the Moodle root directory.

/theme/photo/

Now we need to add some standard plugin files to our theme. First is version.php

/theme/photo/version.php

<?php
// Every file should have GPL and copyright in the header - we skip it in tutorials but you should not skip it for real.

// This line protects the file from being accessed by a URL directly.                                                               
defined('MOODLE_INTERNAL') || die();                                                                                                
                                                                                                                                    
// This is the version of the plugin.                                                                                               
$plugin->version = '2016102100';                                                                                                    
                                                                                                                                    
// This is the version of Moodle this plugin requires.                                                                              
$plugin->requires = '2016070700';                                                                                                   
                                                                                                                                    
// This is the component name of the plugin - it always starts with 'theme_'                                                        
// for themes and should be the same as the name of the folder.                                                                     
$plugin->component = 'theme_photo';                                                                                                 
                                                                                                                                    
// This is a list of plugins, this plugin depends on (and their versions).                                                          
$plugin->dependencies = [                                                                                                           
    'theme_boost' => '2016102100'                                                                                                   
];

We also need a language file so that all our strings can be translated into different languages. The name of this file is the component name of our plugin and it sits in the lang/en/ folder for our plugin. We can include translations of our plugin, but we can also provide translations via the https://lang.moodle.org/ website once our plugin has been published to the plugins database at http://www.moodle.org/plugins/.

/theme/photo/lang/en/theme_photo.php

<?php
// Every file should have GPL and copyright in the header - we skip it in tutorials but you should not skip it for real.

// This line protects the file from being accessed by a URL directly.                                                               
defined('MOODLE_INTERNAL') || die();                                                                                                
                                                                                                                                    
// A description shown in the admin theme selector.                                                                                 
$string['choosereadme'] = 'Theme photo is a child theme of Boost. It adds the ability to upload background photos.';                
// The name of our plugin.                                                                                                          
$string['pluginname'] = 'Photo';                                                                                                    
// We need to include a lang string for each block region.                                                                          
$string['region-side-pre'] = 'Right';

Theme specific files

Theme plugins have a few more standard files they need to define.

Themes require a favicon file to show in the address bar. See [Favicon].

pix/favicon.ico

(Image file not shown).

Themes also require an example screenshot to be displayed in the theme selector.

pix/screenshot.jpg

(Image file not shown).

Themes require a lib.php file. This file contains callbacks used by various API's in Moodle. Initially this file can be empty, but as we add features to our theme we will need to add some functions here.

lib.php

<?php

// Every file should have GPL and copyright in the header - we skip it in tutorials but you should not skip it for real.

// This line protects the file from being accessed by a URL directly.                                                               
defined('MOODLE_INTERNAL') || die();

// We will add callbacks here as we add features to our theme.

Theme config goes in a config.php file. This is one of the most important files in our theme. Once we add this file we will be ready to test our theme for the first time.

config.php

<?php

// Every file should have GPL and copyright in the header - we skip it in tutorials but you should not skip it for real.

// This line protects the file from being accessed by a URL directly.                                                               
defined('MOODLE_INTERNAL') || die();

// $THEME is defined before this page is included and we can define settings by adding properties to this global object.            
                                                                                                                                    
// The first setting we need is the name of the theme. This should be the last part of the component name, and the same             
// as the directory name for our theme.                                                                                             
$THEME->name = 'photo';                                                                                                             
                                                                                                                                    
// This setting list the style sheets we want to include in our theme. Because we want to use SCSS instead of CSS - we won't        
// list any style sheets. If we did we would list the name of a file in the /style/ folder for our theme without any css file      
// extensions.                                                                                                                      
$THEME->sheets = [];                                                                                                                
                                                                                                                                    
// This is a setting that can be used to provide some styling to the content in the TinyMCE text editor. This is no longer the      
// default text editor and "Atto" does not need this setting so we won't provide anything. If we did it would work the same         
// as the previous setting - listing a file in the /styles/ folder.                                                                 
$THEME->editor_sheets = [];                                                                                                         
                                                                                                                                    
// This is a critical setting. We want to inherit from theme_boost because it provides a great starting point for SCSS bootstrap4   
// themes. We could add more than one parent here to inherit from multiple parents, and if we did they would be processed in        
// order of importance (later themes overriding earlier ones). Things we will inherit from the parent theme include                 
// styles and mustache templates and some (not all) settings.                                                                       
$THEME->parents = ['boost'];                                                                                                        
                                                                                                                                    
// A dock is a way to take blocks out of the page and put them in a persistent floating area on the side of the page. Boost         
// does not support a dock so we won't either - but look at bootstrapbase for an example of a theme with a dock.                    
$THEME->enable_dock = false;                                                                                                        
                                                                                                                                    
// This is an old setting used to load specific CSS for some YUI JS. We don't need it in Boost based themes because Boost           
// provides default styling for the YUI modules that we use. It is not recommended to use this setting anymore.                     
$THEME->yuicssmodules = array();                                                                                                    
                                                                                                                                    
// Most themes will use this rendererfactory as this is the one that allows the theme to override any other renderer.               
$THEME->rendererfactory = 'theme_overridden_renderer_factory';                                                                      
                                                                                                                                    
// This is a list of blocks that are required to exist on all pages for this theme to function correctly. For example               
// bootstrap base requires the settings and navigation blocks because otherwise there would be no way to navigate to all the        
// pages in Moodle. Boost does not require these blocks because it provides other ways to navigate built into the theme.            
$THEME->requiredblocks = '';   

// This is a feature that tells the blocks library not to use the "Add a block" block. We don't want this in boost based themes because
// it forces a block region into the page when editing is enabled and it takes up too much room.
$THEME->addblockposition = BLOCK_ADDBLOCK_POSITION_FLATNAV;

$THEME->haseditswitch = true;

Ready set go!

If you have been following along - now we are at the point where we can actually install and test our new theme. Try it now by visiting the admin notifications page to install the new plugin, and then choosing the new theme from the theme selector.

[Theme selector]

When you choose the new theme - you will find that it looks exactly the same as Boost. At this point with our minimal configuration - we are inheriting almost everything from our parent theme including styles and templates. You will notice though that we don't inherit the settings from our parent theme. If you choose a different preset in Boost - it is not applied in this child theme.

Note: make sure your config.php file contains $THEME->hidefromselector = false; (or at least set to false) or else, your theme wont show up in theme selector.

What if I want the settings too?

This section is included for completeness - but it is not recommended for themes to utilise settings from their parents. To find out how to duplicate the settings from the parent theme so they operate independently skip to #Duplicate the settings from Boost.

If I want the settings from Boost to apply to my child theme as well I need to know a bit about how each of the settings in Boost is applied to make them work in my child theme.

Setting 1 - preset.

The "preset" file is the file that is used as the main file for scss compilation. In Boost this is controlled by the theme config value:

theme_boost/config.php

$THEME->scss = function($theme) {
    return theme_boost_get_main_scss_content($theme);
};

theme_boost/lib.php

function theme_boost_get_main_scss_content($theme) {                                                                                
    global $CFG;                                                                                                                    
                                                                                                                                    
    $scss = '';                                                                                                                     
    $filename = !empty($theme->settings->preset) ? $theme->settings->preset : null;                                                 
    $fs = get_file_storage();                                                                                                       
                                                                                                                                    
    $context = context_system::instance();                                                                                          
    if ($filename == 'default.scss') {                                                                                              
        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');                                        
    } else if ($filename == 'plain.scss') {                                                                                         
        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/plain.scss');                                          
    } else if ($filename && ($presetfile = $fs->get_file($context->id, 'theme_boost', 'preset', 0, '/', $filename))) {              
        $scss .= $presetfile->get_content();                                                                                        
    } else {                                                                                                                        
        // Safety fallback - maybe new installs etc.                                                                                
        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');                                        
    }                                                                                                                               
                                                                                                                                    
    return $scss;                                                                                                                   
}

What this function is doing is checking for a theme setting "preset" and either fetching the file from Moodles internal file storage, or from the /preset/ folder. The function then returns the contents of this file.

It does not automatically work for our child theme because we have no setting named "preset" in our child theme and this code is not searching the theme parents for the setting. If we wanted it to apply we could do this in our child theme:

config.php

// This setting defines the main scss file for our theme to be compiled. We could set it to a static file in the scss folder or to a function which returns the SCSS based on theme settings.
$THEME->scss = function($theme) {

    // We need to load the config for our parent theme because that is where the preset setting is defined.
    $parentconfig = theme_config::load('boost');
    // Call a function from our parent themes lib.php file to fetch the content of the themes main SCSS file based on it's own config, not ours.
    return theme_boost_get_main_scss_content($parentconfig);
};

Settings 2+3 brandcolor + scsspre

The way these settings work is they generate a chunk of SCSS to be prepended to the main scss file. In boost they work like this:

theme_boost/config.php

$THEME->prescsscallback = 'theme_boost_get_pre_scss';

theme_boost/lib.php

function theme_boost_get_pre_scss($theme) {                                                                                         
    global $CFG;                                                                                                                    
                                                                                                                                    
    $scss = '';                                                                                                                     
    $configurable = [                                                                                                               
        // Config key => [variableName, ...].                                                                                       
        'brandcolor' => ['brand-primary'],                                                                                          
    ];                                                                                                                              
                                                                                                                                    
    // Prepend variables first.                                                                                                     
    foreach ($configurable as $configkey => $targets) {                                                                             
        $value = isset($theme->settings->{$configkey}) ? $theme->settings->{$configkey} : null;                                     
        if (empty($value)) {                                                                                                        
            continue;                                                                                                               
        }                                                                                                                           
        array_map(function($target) use (&$scss, $value) {                                                                          
            $scss .= '$' . $target . ': ' . $value . ";\n";                                                                         
        }, (array) $targets);                                                                                                       
    }                                                                                                                               
                                                                                                                                    
    // Prepend pre-scss.                                                                                                            
    if (!empty($theme->settings->scsspre)) {                                                                                        
        $scss .= $theme->settings->scsspre;                                                                                         
    }                                                                                                                               
                                                                                                                                    
    return $scss;                                                                                                                   
}

What this code is doing is:

  • looping over a list of theme settings that map to a SCSS variable and building some SCSS to initialise that variable from the setting. In Boost there is only one * brandprimary - but if we wanted to expose more bootstrap variables as theme settings we could use this function as a template in our child theme and add more settings to the $configurable array.
  • Adding all of the raw SCSS from the scsspre theme setting
  • Returning the whole thing as a string to be added before the main scss file.

If we want this code to work in our child theme, using the settings from Boost we could do this:

config.php

// This is a function that returns some SCSS as a string to prepend to the main SCSS file.                                          
$THEME->prescsscallback = 'theme_photo_get_pre_scss';

lib.php

// Function to return the SCSS to prepend to our main SCSS for this theme.
// Note the function name starts with the component name because this is a global function and we don't want namespace clashes.
function theme_photo_get_pre_scss($theme) {
    // Load the settings from the parent.                                                                                           
    $theme = theme_config::load('boost');                                                                                           
    // Call the parent themes get_pre_scss function.                                                                                
    return theme_boost_get_pre_scss($theme);                         
}

Setting 4 scss (post)

The final setting from Boost is a raw text field which adds SCSS to the end of the main SCSS file. This is a useful place to add style rules as they will override previously defined styles with the same specificity. It is applied in Boost by the following lines:

theme_boost/config.php

$THEME->extrascsscallback = 'theme_boost_get_extra_scss';

theme_boost/lib.php

function theme_boost_get_extra_scss($theme) {                                                                                       
    return !empty($theme->settings->scss) ? $theme->settings->scss : '';                                                            
}

This is just returning the value of the setting as a string.

To make this setting apply in our child theme too - we use similar code to the previous sections.

config.php

// This is a function that returns some SCSS as a string to append to the main SCSS file.                                          
$THEME->extrascsscallback = 'theme_photo_get_extra_scss';

lib.php

// Function to return the SCSS to append to our main SCSS for this theme.
// Note the function name starts with the component name because this is a global function and we don't want namespace clashes.
function theme_photo_get_extra_scss($theme) {
    // Load the settings from the parent.                                                                                           
    $theme = theme_config::load('boost');                                                                                           
    // Call the parent themes get_extra_scss function.                                                                                
    return theme_boost_get_extra_scss($theme);                         
}

Duplicate the settings from Boost

This is the recommended way to extend boost as your child theme will not be affected by changes to the Boost settings, and both themes can be in use with different settings applied.

All that needs to happen is to create theme settings in the child theme that match each of the settings in the parent theme. You will need to add matching language strings for each of these settings in the language file.

This example shows how to do it - We are re-using the nice looking theme_boost_admin_settingspage_tabs class from boost and creating duplicate versions of each of the settings.

settings.php

<?php

// Every file should have GPL and copyright in the header - we skip it in tutorials but you should not skip it for real.

// This line protects the file from being accessed by a URL directly.                                                               
defined('MOODLE_INTERNAL') || die();                                                                                                
                                                                                                                                    
// This is used for performance, we don't need to know about these settings on every page in Moodle, only when                      
// we are looking at the admin settings pages.                                                                                      
if ($ADMIN->fulltree) {                                                                                                             
                                                                                                                                    
    // Boost provides a nice setting page which splits settings onto separate tabs. We want to use it here.                         
    $settings = new theme_boost_admin_settingspage_tabs('themesettingphoto', get_string('configtitle', 'theme_photo'));             
                                                                                                                                    
    // Each page is a tab - the first is the "General" tab.                                                                         
    $page = new admin_settingpage('theme_photo_general', get_string('generalsettings', 'theme_photo'));                             
                                                                                                                                    
    // Replicate the preset setting from boost.                                                                                     
    $name = 'theme_photo/preset';                                                                                                   
    $title = get_string('preset', 'theme_photo');                                                                                   
    $description = get_string('preset_desc', 'theme_photo');                                                                        
    $default = 'default.scss';                                                                                                      
                                                                                                                                    
    // We list files in our own file area to add to the drop down. We will provide our own function to                              
    // load all the presets from the correct paths.                                                                                 
    $context = context_system::instance();                                                                                          
    $fs = get_file_storage();                                                                                                       
    $files = $fs->get_area_files($context->id, 'theme_photo', 'preset', 0, 'itemid, filepath, filename', false);                    
                                                                                                                                    
    $choices = [];                                                                                                                  
    foreach ($files as $file) {                                                                                                     
        $choices[$file->get_filename()] = $file->get_filename();                                                                    
    }                                                                                                                               
    // These are the built in presets from Boost.                                                                                   
    $choices['default.scss'] = 'default.scss';                                                                                      
    $choices['plain.scss'] = 'plain.scss';                                                                                          
                                                                                                                                    
    $setting = new admin_setting_configselect($name, $title, $description, $default, $choices);                                     
    $setting->set_updatedcallback('theme_reset_all_caches');                                                                        
    $page->add($setting);                                                                                                           
                                                                                                                                    
    // Preset files setting.                                                                                                        
    $name = 'theme_photo/presetfiles';                                                                                              
    $title = get_string('presetfiles','theme_photo');                                                                               
    $description = get_string('presetfiles_desc', 'theme_photo');                                                                   
                                                                                                                                    
    $setting = new admin_setting_configstoredfile($name, $title, $description, 'preset', 0,                                         
        array('maxfiles' => 20, 'accepted_types' => array('.scss')));                                                               
    $page->add($setting);     

    // Variable $brand-color.                                                                                                       
    // We use an empty default value because the default colour should come from the preset.                                        
    $name = 'theme_photo/brandcolor';                                                                                               
    $title = get_string('brandcolor', 'theme_photo');                                                                               
    $description = get_string('brandcolor_desc', 'theme_photo');                                                                    
    $setting = new admin_setting_configcolourpicker($name, $title, $description, '');                                               
    $setting->set_updatedcallback('theme_reset_all_caches');                                                                        
    $page->add($setting);                                                                                                           
                                                                                                                                    
    // Must add the page after definiting all the settings!                                                                         
    $settings->add($page);                                                                                                          
                                                                                                                                    
    // Advanced settings.                                                                                                           
    $page = new admin_settingpage('theme_photo_advanced', get_string('advancedsettings', 'theme_photo'));                           
                                                                                                                                    
    // Raw SCSS to include before the content.                                                                                      
    $setting = new admin_setting_configtextarea('theme_photo/scsspre',                                                              
        get_string('rawscsspre', 'theme_photo'), get_string('rawscsspre_desc', 'theme_photo'), '', PARAM_RAW);                      
    $setting->set_updatedcallback('theme_reset_all_caches');                                                                        
    $page->add($setting);                                                                                                           
                                                                                                                                    
    // Raw SCSS to include after the content.                                                                                       
    $setting = new admin_setting_configtextarea('theme_photo/scss', get_string('rawscss', 'theme_photo'),                           
        get_string('rawscss_desc', 'theme_photo'), '', PARAM_RAW);                                                                  
    $setting->set_updatedcallback('theme_reset_all_caches');                                                                        
    $page->add($setting);                                                                                                           
                                                                                                                                    
    $settings->add($page);                                                                                                          
}

lang/en/theme_photo.php

<?php

// Every file should have GPL and copyright in the header - we skip it in tutorials but you should not skip it for real.

// This line protects the file from being accessed by a URL directly.                                                               
defined('MOODLE_INTERNAL') || die();
// The name of the second tab in the theme settings.                                                                                
$string['advancedsettings'] = 'Advanced settings';                                                                                  
// The brand colour setting.                                                                                                        
$string['brandcolor'] = 'Brand colour';                                                                                             
// The brand colour setting description.                                                                                            
$string['brandcolor_desc'] = 'The accent colour.';     
// A description shown in the admin theme selector.                                                                                 
$string['choosereadme'] = 'Theme photo is a child theme of Boost. It adds the ability to upload background photos.';                
// Name of the settings pages.                                                                                                      
$string['configtitle'] = 'Photo settings';                                                                                          
// Name of the first settings tab.                                                                                                  
$string['generalsettings'] = 'General settings';                                                                                    
// The name of our plugin.                                                                                                          
$string['pluginname'] = 'Photo';                                                                                                    
// Preset files setting.                                                                                                            
$string['presetfiles'] = 'Additional theme preset files';                                                                           
// Preset files help text.                                                                                                          
$string['presetfiles_desc'] = 'Preset files can be used to dramatically alter the appearance of the theme. See <a href=https://docs.moodle.org/dev/Boost_Presets>Boost presets</a> for information on creating and sharing your own preset files, and see the <a href=http://moodle.net/boost>Presets repository</a> for presets that others have shared.';
// Preset setting.                                                                                                                  
$string['preset'] = 'Theme preset';                                                                                                 
// Preset help text.                                                                                                                
$string['preset_desc'] = 'Pick a preset to broadly change the look of the theme.';                                                  
// Raw SCSS setting.                                                                                                                
$string['rawscss'] = 'Raw SCSS';                                                                                                    
// Raw SCSS setting help text.                                                                                                      
$string['rawscss_desc'] = 'Use this field to provide SCSS or CSS code which will be injected at the end of the style sheet.';       
// Raw initial SCSS setting.                                                                                                        
$string['rawscsspre'] = 'Raw initial SCSS';                                                                                         
// Raw initial SCSS setting help text.                                                                                              
$string['rawscsspre_desc'] = 'In this field you can provide initialising SCSS code, it will be injected before everything else. Most of the time you will use this setting to define variables.';
// We need to include a lang string for each block region.                                                                          
$string['region-side-pre'] = 'Right';

We aren't quite there yet as you will notice if you try and upload a preset file and then choose it. The "theme_boost_get_main_scss_content" function from Boost is expecting the preset files to be stored in a file area for theme_boost only. We need to add this function to our own theme and tweak it.

config.php

// This is the function that returns the SCSS source for the main file in our theme. We override the boost version because          
// we want to allow presets uploaded to our own theme file area to be selected in the preset list.                                  
$THEME->scss = function($theme) {                                                                                                   
    return theme_photo_get_main_scss_content($theme);                                                                               
};

lib.php

function theme_photo_get_main_scss_content($theme) {                                                                                
    global $CFG;                                                                                                                    
                                                                                                                                    
    $scss = '';                                                                                                                     
    $filename = !empty($theme->settings->preset) ? $theme->settings->preset : null;                                                 
    $fs = get_file_storage();                                                                                                       
                                                                                                                                    
    $context = context_system::instance();                                                                                          
    if ($filename == 'default.scss') {                                                                                              
        // We still load the default preset files directly from the boost theme. No sense in duplicating them.                      
        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');                                        
    } else if ($filename == 'plain.scss') {                                                                                         
        // We still load the default preset files directly from the boost theme. No sense in duplicating them.                      
        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/plain.scss');                                          
                                                                                                                                    
    } else if ($filename && ($presetfile = $fs->get_file($context->id, 'theme_photo', 'preset', 0, '/', $filename))) {              
        // This preset file was fetched from the file area for theme_photo and not theme_boost (see the line above).                
        $scss .= $presetfile->get_content();                                                                                        
    } else {                                                                                                                        
        // Safety fallback - maybe new installs etc.                                                                                
        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');                                        
    }                                                                                                                                       
                                                                                                                                    
    return $scss;                                                                                                                   
}

Stop and try it out

So now what is working and what isn't. Well - everything should be working and you should have a nice new theme extending boost with it's own settings pages. When Boost gets updates for bug fixes your new theme will inherit those fixes as it inherits all the SCSS and templates from theme boost. In addition your new theme will accept preset files and supports all the same features and settings as boost, ready to add more.

Take it further

Now we have our very nice starting point we can start changing or adding features, customising templates and output or tweaking the SCSS.

Add some new settings

From this point in the tutorial we will start adding features to our theme to extend it and make it AWESOME!

Lets start with a new setting to set a background image on the login page. As you saw earlier, our theme defines it's list of settings in a file called settings.php. See Admin settings for more information about adding admin settings to any plugin. All of the different kinds of admin settings can be found in lib/adminlib.php. In our case we will want to add a setting which allows an admin to upload an image file. This is an "admin_setting_configstoredfile" and we add it to our theme by adding this code to the settings.php file (inside the "if ($ADMIN->fulltree) {" condition.

settings.php

    // Login page background setting.                                                                                               
    // We use variables for readability.                                                                                            
    $name = 'theme_photo/loginbackgroundimage';                                                                                     
    $title = get_string('loginbackgroundimage', 'theme_photo');                                                                     
    $description = get_string('loginbackgroundimage_desc', 'theme_photo');                                                          
    // This creates the new setting.                                                                                                
    $setting = new admin_setting_configstoredfile($name, $title, $description, 'loginbackgroundimage');                             
    // This means that theme caches will automatically be cleared when this setting is changed.                                     
    $setting->set_updatedcallback('theme_reset_all_caches');                                                                        
    // We always have to add the setting to a page for it to have any effect.                                                       
    $page->add($setting);

We also need to add new lang strings to our language file.

"lang/en/theme_photo.php"

// Background image for login page.                                                                                                 
$string['loginbackgroundimage'] = 'Login page background image';                                                                    
// Background image for login page.                                                                                                 
$string['loginbackgroundimage_desc'] = 'An image that will be stretched to fill the background of the login page.';

Now we have a new setting that lets us upload a file - but it doesn't actually do anything yet. We need to update the theme to set this image background on the login page.

In order to change the SCSS for our theme we will add 2 additional SCSS files and include them before and after our main scss.

We already have a function in our lib.php file which fetches the main SCSS for our theme. This is a good point to include additional SCSS files. We can add 2 lines to the end of this function to achieve this.

lib.php

function theme_photo_get_main_scss_content($theme) {                                                                                
    global $CFG;                                                                                                                    
                                                                                                                                    
    $scss = '';                                                                                                                     
    $filename = !empty($theme->settings->preset) ? $theme->settings->preset : null;                                                 
    $fs = get_file_storage();                                                                                                       
                                                                                                                                    
    $context = context_system::instance();                                                                                          
    if ($filename == 'default.scss') {                                                                                              
        // We still load the default preset files directly from the boost theme. No sense in duplicating them.                      
        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');                                        
    } else if ($filename == 'plain.scss') {                                                                                         
        // We still load the default preset files directly from the boost theme. No sense in duplicating them.                      
        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/plain.scss');                                          
                                                                                                                                    
    } else if ($filename && ($presetfile = $fs->get_file($context->id, 'theme_photo', 'preset', 0, '/', $filename))) {              
        // This preset file was fetched from the file area for theme_photo and not theme_boost (see the line above).                
        $scss .= $presetfile->get_content();                                                                                        
    } else {                                                                                                                        
        // Safety fallback - maybe new installs etc.                                                                                
        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');                                        
    }                                                                                                                               
                                                                                                                                    
    // Pre CSS - this is loaded AFTER any prescss from the setting but before the main scss.                                        
    $pre = file_get_contents($CFG->dirroot . '/theme/photo/scss/pre.scss');                                                         
    // Post CSS - this is loaded AFTER the main scss but before the extra scss from the setting.                                    
    $post = file_get_contents($CFG->dirroot . '/theme/photo/scss/post.scss');                                                       
                                                                                                                                    
    // Combine them together.                                                                                                       
    return $pre . "\n" . $scss . "\n" . $post;                                                                                      
}

scss/pre.scss

// Pre SCSS for the theme.

scss/post.scss

// Post SCSS for the theme.

Why do we use pre and post SCSS?

SCSS/SASS is a powerful language for creating CSS. It supports variables, functions, loops just like php. For lots of information and an introduction to SCSS read about Sass on wikipedia.

Pre SCSS

In Boost we use many variables when creating style rules.

When declaring a variable in SCSS - it is best practice to define it like this:

$mycoolcolour: #FF0 !default;

What this means is - if this variable is not defined already - set it to the hex value '#FF0';

So by setting this variable in some PRE scss we can override the default value of this variable everywhere it is used. Boost is built with the Bootstrap 4 (Alpha 4) CSS Framework. This is a framework which uses SCSS to provide many extremely useful layouts and components which can be reused without adding specific CSS rules to style every page. Because bootstrap consistently uses variables we can customise many of these components easily by setting the value of the variables before we include the Bootstrap SCSS files.

Many variables are available in /theme/boost/scss/bootstrap/_variables.scss - as an example we can change the look of all buttons and input fields with this one variable:

$border-radius: 8px;

Post SCSS

Post SCSS is useful for defining full CSS rules. Because they are included last, they override any previous matching selector with the same Specificity.

Tips for customising Moodle with SCSS (or CSS)

Some handy things to know when you are new to theming is that Moodle adds some classes to every page that helps you target a specific page or set of pages with some style rules.

If you inspect a moodle page and look at the body tag you will see a long list of classes e.g.

<body id="page-course-view-weeks" class="format-weeks  path-course path-course-view safari dir-ltr lang-en yui-skin-sam yui3-skin-sam damyon-per-in-moodle-com--stable_master pagelayout-course course-11 context-497 category-1 drawer-open-left jsenabled">

You can see from this example that each page gets an id. This is a simplified representation of the current page in the navigation. In this example we are looking at the course page and this course is using the weeks format.

The classes on the body tag can also be used to target a page or set of pages.

  • format-weeks
    
    The course format
  • path-course path-course-view
    
    The parts of the bread-crumb leading up to the current page (The current page is what goes in the id)
  • safari
    
    Some server side guessing of the browser type - it's not accurate so I would not rely on it
  • dir-ltr
    
    The direction of the current language for the page ( dir-rtl for RTL languages )
  • lang-en
    
    The current language of the page
  • yui*
    
    Legacy yui classes - ignore these
  • damyon-per-in-moodle-com--stable_master
    
    The current hostname for the site + the site name
  • pagelayout-course
    
    The layout type for the current page
  • course-11
    
    The id of the current course
  • context-497
    
    The current context id
  • category-1
    
    The category for the current course
  • drawer-open-left
    
    Added / removed when the navigation drawer is opened / closed in Boost
  • jsenabled
    
    True if the browser supports javascript

Read about Body id and classes.

In our example the page layout is the most useful here as we have a specific page layout for login pages. We can now craft a rule that applies a background image only to login pages using "body.pagelayout-login".

How do we refer to an image in SCSS ?

So now we can add a rule to the post.scss which will set the background image for the login page. First we need to know how to construct a url to the background images from our stylesheet.

In stylesheets in Moodle we can use a special pix tag to refer to images from our theme. The rule to attach the background image looks like this: scss/post.scss

body.pagelayout-login {                                                                                                             
    background-image: url([[pix:theme_photo|loginbackgroundimage]]);                                                                
    background-size: cover;                                                                                                         
}

There are 2 important parts to this tag (the bit in square brackets). The first is "theme_photo". In this case we are passing a component name, the image serving code in theme_config::resolve_image_location() will then look for an image file in several locations. One of these is in "$CFG->dataroot/pix_plugins/$type/$plugin/$image". We can use this to make sure the image gets served correctly by copying the file saved in the setting to this location every time it is updated. (There is an alterative way we could do this by implementing pluginfile.php in our theme - which you can read about in the File API ).

If we had just passed the value "theme" the image serving code would have looked for the image in the "pix" folder of our theme.

So - we need this final change to copy the image file into our dataroot each time the setting is changed.

In the settings file we will update the callback to a new function which we will define in our lib.php. settings.php

    // Login page background setting.                                                                                               
    // We use variables for readability.                                                                                            
    $name = 'theme_photo/loginbackgroundimage';                                                                                     
    $title = get_string('loginbackgroundimage', 'theme_photo');                                                                     
    $description = get_string('loginbackgroundimage_desc', 'theme_photo');                                                          
    // This creates the new setting.                                                                                                
    $setting = new admin_setting_configstoredfile($name, $title, $description, 'loginbackgroundimage');                             
    // This function will copy the image into the data_root location it can be served from.                                         
    $setting->set_updatedcallback('theme_photo_update_settings_images');                                                            
    // We always have to add the setting to a page for it to have any effect.                                                       
    $page->add($setting);

lib.php

function theme_photo_update_settings_images($settingname) {                                                                         
    global $CFG;                                                                                                                    
                                                                                                                                    
    // The setting name that was updated comes as a string like 's_theme_photo_loginbackgroundimage'.                               
    // We split it on '_' characters.                                                                                               
    $parts = explode('_', $settingname);                                                                                            
    // And get the last one to get the setting name..                                                                               
    $settingname = end($parts);                                                                                                     
                                                                                                                                    
    // Admin settings are stored in system context.                                                                                 
    $syscontext = context_system::instance();                                                                                       
    // This is the component name the setting is stored in.                                                                         
    $component = 'theme_photo';                                                                                                     
                                                                                                                                    
    // This is the value of the admin setting which is the filename of the uploaded file.                                           
    $filename = get_config($component, $settingname);                                                                               
    // We extract the file extension because we want to preserve it.                                                                
    $extension = substr($filename, strrpos($filename, '.') + 1);                                                                    
                                                                                                                                    
    // This is the path in the moodle internal file system.                                                                         
    $fullpath = "/{$syscontext->id}/{$component}/{$settingname}/0{$filename}";                                                      
    // Get an instance of the moodle file storage.                                                                                  
    $fs = get_file_storage();                                                                                                       
    // This is an efficient way to get a file if we know the exact path.                                                            
    if ($file = $fs->get_file_by_hash(sha1($fullpath))) {                                                                           
        // We got the stored file - copy it to dataroot.                                                                            
        // This location matches the searched for location in theme_config::resolve_image_location.                                 
        $pathname = $CFG->dataroot . '/pix_plugins/theme/photo/' . $settingname . '.' . $extension;                                 
                                                                                                                                    
        // This pattern matches any previous files with maybe different file extensions.                                            
        $pathpattern = $CFG->dataroot . '/pix_plugins/theme/photo/' . $settingname . '.*';                                          
                                                                                                                                    
        // Make sure this dir exists.                                                                                               
        @mkdir($CFG->dataroot . '/pix_plugins/theme/photo/', $CFG->directorypermissions, true);                                      
                                                                                                                                    
        // Delete any existing files for this setting.                                                                              
        foreach (glob($pathpattern) as $filename) {                                                                                 
            @unlink($filename);                                                                                                     
        }                                                                                                                           
                                                                                                                                    
        // Copy the current file to this location.                                                                                  
        $file->copy_content_to($pathname);                                                                                          
    }                                                                                                                               
                                                                                                                                    
    // Reset theme caches.                                                                                                          
    theme_reset_all_caches();                                                                                                       
}

I won't show it here - but it is now easy to add a new background image setting for every layout type. I can re-use this theme_photo_update_settings_images callback for each one - so all I have to do is add the setting to the settings.php file, add the SCSS rule to the scss/post.scss file and add the lang strings to the language file.

Thats it for this tutorial, but there are more Themes docs to browse.