Wagtail: Extending the Draftail Editor Part 2 - Block Styles
Introduction
Earlier, I looked at making some basic configuration changes to your Draftail rich text editor.
In the previous article, I looked at creating inline styles and adding these to your editors.
This article will go over customising and creating block styles - those styles that apply to a paragraph or block of text as a whole such as lists or blockquotes.
Note
In Draftail, blocks of text refer to single structural segment of text such as<p>...<p>
,<ul>...</ul>
,<h3>...</h3>
etc.. It does not refer to a WagtailBlock
of text.
I'll go through how to add text alignment buttons which Draftail lacks the ability to do. Because Draftail supports only one concurrent block style (for example, you couldn't mark a block of text H2 and centre aligned in the editor which are both block styles), I'll look at alternatives to using block styles for alignment.
WARNING
Before getting started with block styles you should be aware that the Draftail editor does not support overlapping block styles. You can combine a block style with an entity or inline style, but not two block styles applied to the same block of text.
If you've arrived here looking to add custom list styles, be sure to read Wagtail: Extending the Draftail Editor Part 4 - Custom Lists afterwards.
Customising Existing Block Styles
Your front-end may have some particular styling for block types that you want reflected in the editor interface. The blockquote style is a good example of this.
On this site, the blockquote is styled like this
However, the default editor style looks like this.
It would be preferable to have the front-end style reflected in the editor.
This doesn't require a plug-in, just some custom CSS.
Adding a Custom Admin CSS file
Customising your Wagtail admin site CSS is beyond this article, but, in case you're not already doing so, a quick way to add your own CSS is to create a custom admin base template. The most basic would be the following /wagtailadmin/base.html
created in your site template folder:
{% extends "wagtailadmin/base.html" %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/admin.css' %}" type="text/css">
{% endblock %}
Create an admin.css
in your static CSS folder and add any required fonts and variables needed.
Block Style Editor CSS Classes
Every block style is rendered in the editor with the CSS class class="Draftail-block--
_type
"
. An H3 block uses the feature with _type
header-three
, so the block is rendered in the editor with class="Draftail-block--header-three"
, the blockquote feature is rendered with class="Draftail-block--blockquote"
etc.. This applies to built-in block-styles and also any custom ones you later create (as we'll see later).
On my front-end, the blockquote style is defined:
blockquote, .blockquote {
border-inline-start: 0.5rem solid var(--bs-info);
border-radius: 0.25rem;
background-color: var(--bs-light);
font-style: normal;
font-family: var(--font-family-headings);
font-weight: 500;
padding: 1.2rem 2rem;
margin: 1.6rem 0;
}
All that's required for the Draftail editor to reflect this is to copy the class definition to the admin.css
file and rename it accordingly:
.Draftail-block--blockquote {
border-inline-start: 0.5rem solid var(--bs-info) !important;
border-radius: 0.25rem;
background-color: var(--bs-light);
font-style: normal;
font-family: var(--font-family-headings);
font-weight: 500;
padding: 1.2rem 2rem !important;
margin: 1.6rem 0;
color: var(--bs-dark);
}
Note, I've included an additional definition for text color
. Without this, the text color will inherit the active wagtail theme color. The color I'm using here maintains contrast with the block background-color
.
So long as your CSS variables are also defined (or loaded externally), you will see your blockquote text formatted in the editor as it is on the front-end now.
Note
I needed to add!important
toborder-inline-start
andpadding
. CSS declared in the built-indraftail.css
takes precedence, any conflicting properties in youradmin.css
must include the!important
flag to take effect.
Registering Custom Block Styles with the Draftail Editor
The Wagtail documentation gives a brief introduction to creating new block styles.
It's a very similar process to the one I described in the previous post for creating new inline styles, with a few differences:
- The Draftail control has an optional element property that directs how to style the block in the editor.
- The plugin is registered with the
BlockFeature
method instead ofInlineStyleFeature
- The conversion is set up with
BlockElementHandler
andblock_map
The Block Style Register Function
As with inline style plugins, I created a function to register block plugins and saved this to the same draftail_extensions.py
file:
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
from wagtail.admin.rich_text.converters.html_to_contentstate import (
BlockElementHandler,
InlineStyleElementHandler,
)
from wagtail.admin.rich_text.editors.draftail.features import InlineStyleFeature
def register_block_feature(
features,
feature_name,
description,
type_,
css_class,
element="div",
wrapper=None,
label=None,
icon=None,
editor_style=None,
):
control = {
"type": type_,
"description": description,
"element": element,
}
if label:
control["label"] = label
elif icon:
control["icon"] = icon
else:
control["label"] = description
if editor_style:
control["style"] = editor_style
features.register_editor_plugin(
"draftail",
feature_name,
draftail_features.BlockFeature(control, css={"all": ["css/draftail-editor.css"]}),
)
block_map = {"element": element, "props": {"class": css_class}}
if wrapper:
block_map["wrapper"] = wrapper
features.register_converter_rule(
"contentstate",
feature_name,
{
"from_database_format": {
f"{element}[class={css_class}]": BlockElementHandler(type_)
},
"to_database_format": {
"block_map": {
type_: block_map
}
},
},
)
features
: supplied by theregister_rich_text_features
hook and requires passing throughfeature_name
: the name used internally by Wagtail for this feature - this is the name you use in your feature list when including it in your editor.description
: this is the text displayed when the mouse is hovered over the toolbar button.type_
: For block styles, this will be the suffix for the editor style class as mentioned above. I generally use the feature name in uppercase.css_class
: the front-end CSS class that this element will be rendered with (i.e. the text block will be wrapped with<element class=css_class>...</element>
)element
: optional - this is the HTML element tag to be used (i.e.tag='p'
will render<p>...</p>
),<div>
by default.wrapper
: optional list style for<li>
list elements - see Part 4 for a discussion on custom listseditor_style
: optional, used to alter the way the formatted text is displayed in the editor, defined as a dictionary of css properties (e.g.editor_style={'color': 'red', 'font-size': 'larger'}
). It's preferable to define style using aDraftail-block--
feature_name
CSS class as described in the first part of this article.label
,icon
: choose one of these. label will take unicode glyphs which we'll use for the underline style, while icon can take an SVG path (set for a 1024x1024px viewbox). Ignore the documentation's claim that you can use Font Awesome icons, this is no longer true. If you supply neither, the function will pass the description to the label property.- Finally, after the declarations, are the two methods to register the plug-in and the conversion rule:
features.register_editor_plugin(
"draftail",
feature_name,
draftail_features.BlockFeature(control, css={"all": ["css/draftail-editor.css"]}),
)
I've included a call to css/draftail-editor.css
in the register_editor_plugin
method. This tells the Draftail editor where to find the styling for this block.
It's optional and can be left out, and of course, you can name this CSS as you like. I found it better to have a dedicated CSS for block styles - using the admin CSS causes it to be loaded twice. Leaving the CSS definition off and loading via the admin CSS changes the precedence, any styles that conflict with the built-in Draftail styles will be overwritten.
Understanding the Converter Rule
Note that the register_block_feature
function includes the following code:
block_map = {"element": element, "props": {"class": css_class}}
if wrapper:
block_map["wrapper"] = wrapper
features.register_converter_rule(
"contentstate",
feature_name,
{
"from_database_format": {
f"{element}[class={css_class}]": BlockElementHandler(type_)
},
"to_database_format": {
"block_map": {
type_: block_map
}
},
},
)
This registers the conversion rule that instructs Draftail how to interpret formatted rich text and convert to draft.js raw content and back again:
to_database_format
- This tells Draftail how to take a piece of rendered rich text in the editor and convert it to the raw content stored on saving the page/model.
- It takes a dictionary with single member whose key is
"block_map"
, with a dictionary value which describes properties of the block (usually element and class). - In the
register_block_feature
function, the block map is first set up as a dictionary variable to allow for inclusion of the optional wrapper attribute (more on that later). By default, the element and class are added.
from_database_format
- This tells draftail which registered 'type' to pass into the
BlockElementHandler
for a piece of raw content (found in the stored database value) to render as rich text. It's effectively the reverse ofto_database_format
. - It takes a dictionary with a single key/value pair. Think of this a bit like a css selector string (
f"{element}[class={css_class}]"
) that identifies your particular custom block and style to apply to elements that match that selector (BlockElementHandler(type_)
)
Make sure the 'selector' (key value) is unique and is sufficiently specific to apply only to the relevant custom block type. Overlapping Draftail styles will cause styles to be overwritten unexpectedly when reloading the raw content into the draftail editor. This will happen if your 'selector' could return content with more than one Draftail style.
Creating the Alignment Block Styles
Because my site's body style defaults to justify
, I'll create three styles: left, centre and right aligned. When none of these are applied, the text is justified.
- The alignment property will apply to a paragraph as a whole, so each style will be registered with the
<p>
element. - I will use standard bootstrap classes for the front-end alignment. If you're not using bootstrap, just create appropriate classes in your site's front-end CSS.
- The toolbar buttons will be styled with SVG paths using the control's icon property.
Defining the Icons
I'll start with the svg files that I register with Wagtail for the icons
<svg
id='icon-left-align'
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
d="M 584.19821,168.76226 H 73.024776 C 32.701407,168.76226 0,137.69252 0,99.38113 0, 61.069736 32.701407,30 73.024776,30 H 584.19821 c 40.39183,0 73.02477,31.069736 73.02477, 69.38113 0,38.31139 -32.63294,69.38113 -73.02477,69.38113 z m 0, 555.04902 H 73.024776 C 32.701407,723.81128 0,692.80659 0,654.43015 0,616.05372 32.701407, 585.04902 73.024776,585.04902 H 584.19821 c 40.39183,0 73.02477,31.0047 73.02477,69.38113 0, 38.37644 -32.63294,69.38113 -73.02477,69.38113 z M 0,376.90564 C 0,338.5292 32.701407, 307.52451 73.024776,307.52451 H 949.32209 c 40.39183,0 73.02481,31.00469 73.02481,69.38113 0, 38.37644 -32.63298,69.38113 -73.02481,69.38113 H 73.024776 C 32.701407,446.28677 0,415.28208 0, 376.90564 Z M 949.32209,1001.3358 H 73.024776 C 32.701407,1001.3358 0,970.3311 0,931.95467 0, 893.57823 32.701407,862.57354 73.024776,862.57354 H 949.32209 c 40.39183,0 73.02481, 31.00469 73.02481,69.38113 0,38.37643 -32.63298,69.38113 -73.02481,69.38113 z"
/>
</svg>
<svg
id="icon-centre-align"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
d='M 729.54876,161.46125 H 293.0195 c -40.24254,0 -72.75487,-31.67405 -72.75487,-70.73062 C 220.26463, 51.674059 252.77696,20 293.0195,20 h 436.52926 c 40.24254,0 72.75488,31.674059 72.75488,70.73063 0, 39.05657 -32.51234,70.73062 -72.75488,70.73062 z M 947.81339,444.38376 H 74.754876 C 34.580543, 444.38376 2,412.77601 2,373.65314 2,334.53026 34.580543,302.92251 74.754876, 302.92251 H 947.81339 c 40.24254,0 72.75491,31.60775 72.75491,70.73063 0,39.12287 -32.51237, 70.73062 -72.75491,70.73062 z M 2,939.49815 C 2,900.37528 34.580543,868.76753 74.754876, 868.76753 H 947.81339 c 40.24254,0 72.75491,31.60775 72.75491,70.73062 0,39.12288 -32.51237, 70.73065 -72.75491,70.73065 H 74.754876 C 34.580543,1010.2288 2,978.62103 2,939.49815 Z M 729.54876, 727.30627 H 293.0195 c -40.24254,0 -72.75487,-31.60775 -72.75487,-70.73062 0,-39.12288 32.51233, -70.73063 72.75487,-70.73063 h 436.52926 c 40.24254,0 72.75488,31.60775 72.75488,70.73063 0, 39.12287 -32.51234,70.73062 -72.75488,70.73062 z'
/>
</svg>
<svg
id='icon-right-align'
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
d='M 947.56774,163.47496 H 437.33896 c -40.31719,0 -72.88983,-29.43806 -72.88983, -65.73748 C 364.44913,61.438065 397.02177,32 437.33896,32 h 510.22878 c 40.31718, 0 72.88986,29.438065 72.88986,65.73748 0,36.29942 -32.57268,65.73748 -72.88986, 65.73748 z m 0,525.89984 H 437.33896 c -40.31719,0 -72.88983,-29.37643 -72.88983, -65.73748 0,-36.36104 32.57264,-65.73748 72.88983,-65.73748 h 510.22878 c 40.31718, 0 72.88986,29.37644 72.88986,65.73748 0,36.36105 -32.57268,65.73748 -72.88986,65.73748 z M 0, 360.6874 C 0,324.32636 32.640975,294.94992 72.889826,294.94992 H 947.56774 c 40.31718, 0 72.88986,29.37644 72.88986,65.73748 0,36.36104 -32.57268,65.73748 -72.88986, 65.73748 H 72.889826 C 32.640975,426.42488 0,397.04844 0,360.6874 Z M 947.56774, 952.32472 H 72.889826 C 32.640975,952.32472 0,922.94829 0,886.58724 0,850.2262 32.640975, 820.84976 72.889826,820.84976 H 947.56774 c 40.31718,0 72.88986,29.37644 72.88986,65.73748 0, 36.36105 -32.57268,65.73748 -72.88986,65.73748 z'
/>
</svg>
Note
The use of SVG icons to create button icons was introduced in the previous article. Briefly, you must use an SVG formatted for a 1024x1024px viewbox to render correctly. The icon parameter takes the id property of the SVG file without the 'icon-
' prefix.
Registering the Block Styles
Now, in your wagtail_hooks.py
, we can call the register function for each of the three styles with appropriate parameters:
# wagtail_hooks.py
@hooks.register('register_rich_text_features')
def register_align_left_feature(features):
register_block_feature(
features=features,
feature_name='left-align',
type_='LEFT-ALIGN',
description='Left align text',
css_class='text-start',
element='p',
icon='left-align'
)
@hooks.register('register_rich_text_features')
def register_align_centre_feature(features):
register_block_feature(
features=features,
feature_name='CENTRE-ALIGN',
type_='centre-align',
description='Centre align text',
css_class='text-center',
element='p',
icon='centre-align'
)
@hooks.register('register_rich_text_features')
def register_align_right_feature(features):
register_block_feature(
features=features,
feature_name='RIGHT-ALIGN',
type_='right-align',
description='Right align text',
css_class='text-end',
element='p',
icon='right-align'
)
As with inline styles, you need to add the new block styles to your editor. How to do this was covered in Configuring Rich Text Blocks for Your Wagtail Site. To your features list, add 'left-align', 'centre-align', 'right-align'
and reload the web service on your server, the align buttons will be on your toolbar when you next load a rich text block in your admin.
Adding the Editor CSS
Before the alignment styling is reflected in the editor however, there is one more addition to be made - we need to add the Draftail-block--
classes to the CSS.
When I registered the styles above, I defined the CSS file as draftail-editor.css
. This file should reside in your static/css
directory.
Normally, we would just need to define the Draftail block style here, but Draftail has a default left alignment set in public-DraftStyleDefault-ltr
for some reason which will override any alignment we set in the block class. So, in this case, we define the editor alignment CSS classes as:
.Draftail-block--LEFT-ALIGN .public-DraftStyleDefault-ltr {
text-align: left;
}
.Draftail-block--CENTRE-ALIGN .public-DraftStyleDefault-ltr {
text-align: center;
}
.Draftail-block--RIGHT-ALIGN .public-DraftStyleDefault-ltr {
text-align: right;
}
Now you can style your text with alignment and see it reflected in your editor.
Left
Centre
Right
Pro's & Con's of Block Style Alignment
Pro's
- Familiar interface.
- No extra blocks required, self-contained.
Con's
- Only one block style per block can be applied (a centre-aligned H3 block is not possible like this for example).
- Each new paragraph has default alignment. Alignment must be re-applied every time.
Alternatives to Block Style Alignment
1. Use a Header Block
If it's just headers you want to align on the fly, one option is to use a header block as described in a previous article here.
2. Use a Rich Text StructBlock
The method I use on this site (and elsewhere) is to create a StructBlock
comprised of an alignment choice and the rich text block itself. This StructBlock
then gets incorporated into every other StructBlock
on the site that uses rich text.
This has the advantage of applying alignment to the block as a whole while leaving you free to use other block styles. The lesser disadvantage is that the selected alignment isn't reflected in the editor, but the big change is that this will disable the Wagtail 5+ behaviour of being able to split rich text blocks and insert blocks of other types.
Choices
I use bootstrap classes on this site, so I set the values in my choices to reflect those:
class TextAlignmentChoiceBlock(ChoiceBlock):
choices=[
('justify', 'Justified'),
('start', 'Left'),
('center', 'Centre'),
('end', 'Right')
]
Abstract Base Block
Create a base abstract StructBlock
with the default editor next:
class RichTextStructBlock(StructBlock):
alignment = TextAlignmentChoiceBlock(
default = 'justify',
label="Text Alignment"
)
content = RichTextBlock()
class Meta:
template = 'blocks/richtext_block.html'
label = _("Rich Text Block")
icon = 'pilcrow'
abstract = True
Rich Text Struct Blocks with Configured Editor
Use the base block above to construct a rich text block type for each of the editors defined in your WAGTAILADMIN_RICH_TEXT_EDITORS
setting (more info):
class DefaultRichTextBlock(RichTextStructBlock):
pass
class MinimalRichTextBlock(RichTextStructBlock):
content = RichTextBlock(editor='minimal')
class BasicRichTextBlock(RichTextStructBlock):
content = RichTextBlock(editor='basic')
Template
The alignment values coming from the ChoiceBlock
were justify
, start
, center
, end
. As I'm using bootstrap, text-{{ self.alignment }}
will match bootstrap alignment classes.
{% load wagtailcore_tags %}
<div class="text-{{ self.alignment }}">
{{ self.content }}
</div>
Important
The decision on using the standardRichTextBlock
versus a rich textStructBlock
as above should ideally be made before building out your site content. As the two have different data structure, you would need some careful manual migration to switch on an existing site without losing all your rich text content.
Now the StructBlock
can be called inside other StuctBlock
's rather than calling the RichTextBlock
directly. For example:
class SimpleCard(StructBlock):
background = ColourThemeChoiceBlock(
default='bg-transparent',
label=_("Card Background Colour")
)
text = BasicRichTextBlock(
label=_("Card Body Text"),
help_text=_("Body text for this card."),
)
class Meta:
template = 'blocks/simple_card_block.html'
label = _("Simple Card (Text Only)")
icon = 'form'
Conclusion
In this article, I showed how to customise existing block styles in the Draftail rich text editor, and how to register new block styles in Draftail by creating three alignment options.
We looked briefly at the pro's and con's of using alignment buttons in a rich text block and examined a couple of alternatives using a separate header block and a custom rich text StructBlock
to overcome the Draftail limitation of only one concurrent active block style in any block of text.
In Part 3, I'll cover one method you might use inline styles and JavaScript to achieve dynamic text in your rich text areas.
Part 4 examines creating custom list styles, their limitations and a couple of examples of how you can use these.