Wagtail: Extending the Draftail Editor Part 1 - Inline Styles

Introduction

The previous post looked at basic configuration of the Draftail rich text editor and introduced the concept of adding custom features.

In this article, I'll cover creating custom inline font styles not included by default in the Draftail rich text editor. I'll include:

  • how to add tag-based styling (such as <u>)
  • how to add styled <span> tags
  • demonstrate using both unicode glyphs and svg paths for your toolbar icon

The examples below will create underline, larger, smaller and highlighted styles.

Part 2 covers adding block styles (i.e. styles that are applied to an entire paragraph of text, such as numbered lists) by creating alignment buttons while examining if this is the best route to take for this requirement.

Part 3 looks at one method to add inline dynamic text to your rich text block. In this example, I create a button to add FontAwesome icons to your text.

Registering Custom Inline Styles with the Draftail Editor

As mentioned in the previous article, you can add features to the built-in draftail editor through the use of Wagtail hooks.

The Inline Style Register Function

Taking the example from the documentation page, I created an inline style register function rather than repeating the same block of code with minor differences. To stop the size of the hooks file blowing out, I created a file called draftail_extensions.py for my register functions and SVG icon definitions.

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_inline_styling(
    features,
    feature_name,
    description,
    type_,
    tag='span',
    format=None,
    editor_style=None,
    label=None,
    icon=None
):
    control = {"type": type_, "description": description}
    if label:
        control["label"] = label
    elif icon:
        control["icon"] = icon
    else:
        control["label"] = description
    if editor_style:
        control["style"] = editor_style

    if not format:
        style_map = {"element": tag}
        markup_map = tag
    else:
        style_map = f'{tag} {format}'
        markup_map = f'{tag}[{format}]'

    features.register_editor_plugin(
        "draftail", feature_name, InlineStyleFeature(control)
    )
    db_conversion = {
        "from_database_format": {markup_map: InlineStyleElementHandler(type_)},
        "to_database_format": {"style_map": {type_: style_map}},
    }
    features.register_converter_rule("contentstate", feature_name, db_conversion)
  • features: supplied by the register_rich_text_features hook and requires passing through
  • feature_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_: to be honest, I've never found any documentation for this, but it is required, so following the example on the Wagtail page, I generally just give this a meaningful uppercase word.
  • tag: optional - this is the HTML element tag to be used (i.e. tag='u' will render <u>...</u>), <span> by default.
  • format: optional - valid style and/or class element attribute definition (e.g. format='style="color: red; font-size: larger;"').
  • editor_style: 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'}). Not required for standard tag definitions such as <u>, but needed to instruct the editor how to display your formatted text. It will also be needed for non-standard tags which I'll go through in part 3 when creating inline dynamic text.
  • 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, InlineStyleFeature(control)
)

The function call above registers the plugin as an inline style feature and instructs Draftail how to render the control (i.e. the button on the toolbar).

db_conversion = {
    "from_database_format": {markup_map: InlineStyleElementHandler(type_)},
    "to_database_format": {"style_map": {type_: style_map}}
}
features.register_converter_rule("contentstate", feature_name, db_conversion)

This last chunk creates a nested dictionary with instructions on how that feature should be rendered in the editor (from_database_format) and how it should be rendered on the front end web page (to_database_format) and uses this to register the converter rule.

1. Creating an underline inline style

In this first example, I'll use a unicode glyph as label. To add the underline style, we need a plugin that wraps the highlighted text with <u></u> tags.

In your wagtail_hooks.py, import the above function and add the following:

@hooks.register("register_rich_text_features")
def register_underline_styling(features):
    register_inline_styling(
        features=features,
        feature_name='underline',
        type_='UNDERLINE',
        tag='u',
        description='Underline',
        label='U̲'
    )

Once your web server reloads, the new feature will be added. Remember, to use it in your editor, 'underline' must be included in the feature list in use. Once you've done that, you'll see the button with the U̲ character. Highlight some text and click the button to get the underline style. Use the dev tools to inspect the underlined word to see how draftail renders inline styles.

2. Creating inline styles for larger and smaller font with SVG icons

Next, we can create font styles for larger and smaller text by wrapping highlighted text with a <span> tag styled with font-size:smaller and font-size:larger.

fa-solid fa-circle-info These are simple 'on/off' features that size the font relative to the current font size. They do not incrementally increase and decrease the font size as the same buttons in Word do for instance.

We'll be using the SVG path to define the icons. To keep things a bit tidy and for ease of importing, I put all my Draftail icon definitions into a single class in an external file and import the class into my wagtail_hooks.py.

The paths for these icons are:

class DRAFTAIL_ICONS:
    increase_font = [
        "m 480.2209,84.950689 4.06109,6.54669 3.20118,6.93067 331.19841,843.325311 c 12.1572,30.95579 -3.08196,65.90574 -34.03775,78.06294 -28.74458,11.2889 -60.93322,-1.0447 -75.11868,-27.64918 L 706.58049,985.7786 626.83948,782.77723 H 235.96515 l -79.6808,203.00137 c -11.28884,28.7446 -42.23062,43.9377 -71.378178,36.2277 l -6.684746,-2.19 C 49.476807,1008.5275 34.283682,977.58572 41.993644,948.43815 l 2.19004,-6.68479 L 375.38209,98.428049 C 393.4324,52.466792 453.88195,47.974339 480.2209,84.950689 Z M 431.43242,285.07636 283.23619,662.34145 H 579.56843 Z M 773.67972,4.2455454 c 12.10621,-5.6607226 26.13529,-5.6607226 38.24143,0 l 5.87486,3.3021083 135.55096,90.0738253 5.28773,4.101561 c 16.35361,14.74 19.88226,39.62397 7.33249,58.50999 -12.54977,18.88602 -36.85775,25.2725 -56.78192,15.90587 l -5.82963,-3.28588 -110.55565,-73.445424 -110.55475,73.445424 -5.82958,3.28584 c -19.92417,9.36663 -44.23214,2.98015 -56.78191,-15.90587 -12.54983,-18.88602 -9.02113,-43.76999 7.33243,-58.50999 L 632.25391,97.621437 767.8048,7.5476115 Z"
    ]
    decrease_font = [
        "m 431.77869,370.57963 4.20832,6.44496 3.52572,7.20318 231.02312,554.45552 c 13.0862,31.40688 -1.76563,67.47571 -33.17258,80.56191 -29.16349,12.1514 -62.34679,0.2156 -77.41597,-26.70007 L 556.80126,986.0726 495.56947,839.16561 H 269.65966 L 208.48948,986.0726 c -12.15148,29.1636 -44.11958,44.0526 -73.77058,35.5533 l -6.79133,-2.3807 C 98.764017,1007.0937 83.874965,975.12562 92.374306,945.47457 l 2.380709,-6.79128 231.023125,-554.45552 c 18.95572,-45.49378 78.85589,-50.04316 106.00055,-13.64814 z m -49.13332,197.51885 -61.60616,147.8548 h 123.21233 z m 546.85804,-447.4609 c 12.83909,19.32142 9.2291,44.77907 -7.50154,59.85889 l -5.40964,4.19612 -138.67597,92.1504 c -13.55668,9.00856 -30.68387,10.13463 -45.13335,3.37819 l -6.01048,-3.37821 -138.67592,-92.1504 C 566.84294,170.56954 561.0625,141.89114 575.18553,120.63758 588.02462,101.31616 612.893,94.782442 633.27651,104.36501 l 5.96409,3.3616 113.10337,75.08691 113.10443,-75.08692 c 21.25357,-14.123023 49.93198,-8.342584 64.05501,12.91098 z"
    ]
This will give you the icons and

To create other icons, open your SVG file in a text editor and copy the d attribute of the path element. Paste that string into a single element list (i.e. ['d attribute goes here']) as has been done in the example above.

fa-solid fa-triangle-exclamation IMPORTANT!
To render properly on the toolbar, your SVG icon needs to be scaled for a 1024x1024 pixel viewbox.

Now in our wagtail_hooks.py, we register this as before, but this time by defining icon instead of label:

@hooks.register("register_rich_text_features")
def register_larger_styling(features):
    register_inline_styling(
        features=features,
        feature_name='larger',
        type_='LARGER',
        tag='span',
        format='style="font-size:larger"',
        editor_style={'font-size':'larger'},
        description='Increase Font',
        icon=DRAFTAIL_ICONS.increase_font
    )

@hooks.register("register_rich_text_features")
def register_smaller_styling(features):
    register_inline_styling(
        features=features,
        feature_name='smaller',
        type_='SMALLER',
        tag='span',
        format='style="font-size:smaller"',
        editor_style={'font-size':'smaller'},
        description='Decrease Font',
        icon=DRAFTAIL_ICONS.decrease_font
    )
The format option is telling Draftail how to render the <span> tag on the front end while the editor_style option tells Draftail how to render the style in the editor. In most cases, this is the same style, though it is more common to define a class for the front-end format. In Part 3, you'll see a case for using an editor style that has nothing to do with the rendered front-end format.

Once again, add 'larger' and 'smaller' to the feature list of your editor. After your server has reloaded you will have these styles enabled.

3. Creating a styled span block to highlight text

The first two examples used standard tags to add pre-existing inline styles to the editor. What happens when you want to apply custom formatted text? We can use span with either a class defined in our CSS or by supplying an inline style string as you would add to any span element. It's possible to use both, though I can't think of a reason to do this.

Using a class pushes the definition out of the code block and into your external CSS. Just make sure your class is defined for both your external site and your admin pages.

In this example, I'll create a plugin using an inline style definition to add a yellow background with a little horizontal padding to simulate highlighted text.

I'll start with the icon definition that gives us a marker pen

I append this to the DRAFTAIL_ICONS class defined above:

class DRAFTAIL_ICONS:
    ....
    highlighter = [
        "m 592.83219,630.12307 298.10994,-430.084 -55.14282,-58.81149 -404.8197,316.8619 z m -351.9353,10.00197 v 0 V 496.697 c 0,-30.60597 13.55045,-59.21155 36.69913,-77.21507 L 791.57214,16.803282 C 805.49899,5.801133 822.43706,0 839.75152,0 861.2064,0 881.90848,9.0017582 897.15274,25.204923 L 1000.2867,134.82633 C 1015.531,151.0295 1024,172.83376 1024,195.83825 c 0,18.40359 -5.4578,36.40711 -15.8089,51.21 L 629.34313,793.1549 c -16.93807,24.60482 -44.03897,39.00763 -72.64545,39.00763 H 421.56957 l -47.80299,50.80992 c -23.52508,25.0049 -61.72983,25.0049 -85.25491,0 L 193.09392,781.55266 c -23.52509,-25.0049 -23.52509,-65.61283 0,-90.6177 z M 13.174049,932.7822 131.74049,806.75758 264.61019,947.98516 206.26797,1009.9973 C 197.79893,1018.999 186.31869,1024 174.27385,1024 H 45.168169 C 20.137475,1024 0,1002.5958 0,975.99061 v -9.40181 c 0,-12.8025 4.7050174,-25.00489 13.174049,-34.00665 z"
    ]

Now, in wagtail_hooks.py we can call the register function with the CSS formatting defined instead of the tag (the default <span> tag will be used instead). Note that the editor style needs to be defined in this case so that the text style is reflected in the Draftail editor as well as the front-end website.

@hooks.register('register_rich_text_features')
def register_highlighted_text_feature(features):
    register_inline_styling(
        features=features,
        feature_name='highlight',
        type_='HIGHLIGHT',
        format='style="background-color: yellow;padding-left: 0.15rem;padding-right: 0.15rem;"',
        editor_style={'background-color': 'yellow', 'padding-left': '0.15rem', 'padding-right': '0.15rem'},
        description='Highlighted Text',
        icon=DRAFTAIL_ICONS.highlighter,
    )

Add 'highlight' to your editor definition and reload the editor, you will see the highlighter pen on the toolbar which gives the highlighted text format.

 
wagtail customised draftail rich text editor toolbar
Example Wagtail 3.x draftail toolbar with underline, larger, smaller font and highlighted text added
 

Conclusion

In this article, I showed how to add inline styles to the editor interface using both element tags and custom styles. I also showed how to use a unicode glyph label as a button icon, then defined freeform icons using the path of an svg file.

In Part 2, I'll walk through adding block styles and give two options for approaching the issue of adding text alignment to your rich text areas. Which route you choose has a big impact on your data structure. Ideally, you should choose which before building out your content. Migrating from one to the other would require a fair amount of work.

Finally, in Part 3, I'll cover one method you might use to achieve dynamic text in your rich text areas.


  Please feel free to leave any questions or comments below, or send me a message here