Wagtail: Extending the Draftail Editor Part 3 - Dynamic Text

Introduction

I'd originally intended to do an article on Draftail entities, but after recent experiences, with complete lack of meaningful documentation and hidden "surprises", I would say use this only as an absolute last resort and good luck to you if you have that unlucky task.

One absolute showstopper not mentioned on any documentation at the time of writing is that entities cannot overlap, meaning a content editor couldn't add a hyperlink to an inline entity for example. In fact it will, somewhat ungraciously, crash the Draftail editor.

The requirement I had been working on at the time was to provide a mechanism for inserting dynamic inline text into reports, live results pages, dashboards etc.. A keyword, or set of keywords, could be highlighted with a style from a button which would get used as parameters to a JavaScript function and parsed during document loading. In the real world, this could be status updates, research results, economic/financial data, live resource availability etc..

In the end, I abandoned the entity route and created a Draftail inline plugin with custom element tag that is then processed by the site JavaScript to render the required text.

In the example below, I go through adding a button to insert FontAwesome icons inline in the editor as a proof of concept for more useful applications.

The example below uses the register_inline_styling() function I defined in Part 1 of this series. If you haven't read that article yet, it may be a good place to start.
fa-solid fa-circle-info To use this example on your own site, you will need to load the FontAwesome JavaScript with your own kit ID.

If you don't already have FontAwesome login and kit set up, you can get a free kit from here.

Once you have your kit ID, make sure you have the following script loaded in any page using the plug-in:

<script src="https://kit.fontawesome.com/your-kit-id.js" crossorigin="anonymous"></script>

If you are using a free kit, then only free icons will be rendered with this plug-in.

Feature Requirements

  • We need to create an inline style plugin for Draftail that is easily distinguishable in the editor as special format text.
  • We need to easily identify the rendered feature elements in the DOM and loop through them in the document ready event.
  • From each feature element, take the inner text and use it to render the appropriate dynamic text (in this case, format the element for the FontAwesome JavaScript to render).

This last step is where you would use your real-world code to generate the dynamic text. For example, a dummy stock element that returns ticker prices might be rendered initially by Draftail as <stock>GOOGL open</stock>, the JS would then replace this element with a span tag with inner HTML displaying the last opening price for Google and perhaps an accompanying Sparkline.

Registering the Editor Style

As mentioned previously, in Part 1, I covered inline styles and a custom function to register those easily.

I've added an icon definition for the FontAwesome logo fa-solid fa-font-awesome to the DRAFTAIL_ICONS class I defined in Part 1. The code for the icon is:

class DRAFTAIL_ICONS:
    ....
    font_awesome = [
        "M 1011.0111,38.377438 V 802.30364 c -142.37657,51.24671 -185.81845,72.75487 -269.67821,72.75487 -141.76722,0 -195.43205,-72.75487 -336.92846,-72.75487 -48.94828,0 -86.83863,8.61918 -121.2762,19.93029 -34.66323,10.76317 -66.483,-15.15575 -66.483,-47.89545 v -1.69632 c 0,-21.08755 12.94004,-40.01519 32.54192,-47.42709 42.15555,-16.58334 89.77237,-32.04375 155.21728,-32.04375 141.56412,0 195.22894,72.75488 336.92846,72.75488 57.54639,0 96.7005,-10.46761 161.28786,-33.42177 V 186.84286 c -46.87209,16.5972 -95.3916,33.42177 -161.28786,33.42177 -0.009,0 0.009,0 0,0 -141.74465,0 -195.45461,-72.75488 -336.92846,-72.75488 -128.85878,0 -181.44038,59.3407 -296.08182,68.43506 v 750.0573 c 0,30.23874 -24.259748,54.56619 -54.161303,54.56619 C 24.259752,1020.5683 0,996.24085 0,966.00211 V 56.566157 C 0,26.418355 24.259752,2 54.161307,2 84.062862,2 108.32261,26.418355 108.32261,56.566157 V 108.83599 C 222.96405,97.809077 275.54565,38.377438 404.40443,38.377438 c 141.65438,0 195.09354,72.754872 336.92846,72.754872 84.85272,0 131.56685,-23.076933 269.67821,-72.754872 z"
    ]
The use of SVG paths to create button icons was introduced in Part 1. Briefly, you must use an SVG formatted for a 1024x1024px viewbox to render correctly. The icon takes the d property of the SVG path.

Here, I'll just register the style in wagtail_hooks.py using the previously defined register_inline_styling function with the following notes:

  • feature_name='fontawesome' - this is the value you'll need to add to your editor feature list
  • tag='fa' - this will generate an inline element <fa>...</fa>. Whatever tag you use here should be unique to this style and not used for other purposes.
  • format='style="display:none;"' - we want to hide the element rendered by Draftail as this will contain the raw data that we don't want to display. The JavaScript will render a visible element with the dynamic text.
  • editor_style - this defines how the style will be rendered in the Draftail editor only and has no effect on the front-end styling. Here, I'm using a monospace font wrapped in an orange pill-box to make it easy for the content editor to recognise that this is the dynamic text keyword definition and not something that will appear on the front-end.
  • icon=DRAFTAIL_ICONS.font_awesome - the SVG icon path defined above. If you prefer to just use a simple label, you can change this line to label = '⚐'.
@hooks.register("register_rich_text_features")
def register_fa_styling(features):
    """Add <fa> to the richtext editor and page."""
    register_inline_styling(
        features=features,
        feature_name='fontawesome',
        description="Font Awesome Icon",
        type_="FA",
        tag="fa",
        format='style="display:none;"',
        editor_style={            
            'background-color': 'orange',            
            'color': '#666',
            'font-family': 'monospace',
            'font-size': '0.9rem',
            'font-weight': 'bolder',
            'padding': '0 0.4rem',
            'border-radius': '0.6rem'
        },
        icon=DRAFTAIL_ICONS.font_awesome
    )
fa-solid fa-triangle-exclamation You must add 'fontawesome' to your editor's feature list before it will appear on your toolbar. See here for more details.

You should now see the FontAwesome flag fa-solid fa-font-awesome on your toolbar.

wagtail draftail toolbar with custom plug-ins
Example Draftail 3.x toolbar with the FontAwesome button added

To try it out, enter the classes from a FontAwesome icon as plain text in your rich text editor with some other text, select the text and click the FontAwesome icon.

Draftail Editor

Before styling:

This is the fa-solid fa-house home icon

With the styling applied:

This is the fa-solid fa-house home icon

In the editor, the style is formatted according to the style we specified in the editor_style parameter when registering the feature above.

Now, preview the page, you will see the phrase "This is the home icon". If you inspect the line, you should see that is has been rendered as:

<p>This is the <fa style="display:none";>fa-solid fa-house<fa> home icon<p>

We don't have any defined style for <fa> on the front-end, and since it is a custom tag not associated with any behaviour, it is rendered as an inline element with properties inherited from the encapsulating element.

We added display:none; as a style when we registered the feature so that the initial text would never be displayed on the front-end.

Rendering the Dynamic Text with JavaScript

What's left to do is to loop through the <fa> elements and render the dynamic text.

In this simple example, all we need to do is transform the element into a format that the FontAwesome JavaScript will recognise and render to the associated icon.

In a real world case, this is where you might make a call out to a remote data source and/or do some additional processing and formatting.

 

fa-solid fa-circle-info The Font Awesome Element Format

The FontAwesome documentation states that the icon should be defined in the class properties of an <i> element, for example <i class="fa-solid fa-house"></i>.

Digging a bit further, it states that a <span> element can be used instead. A little experimenting revealed that any element I tried could be used. Whatever element is used is replaced with an <svg> by the FontAwesome code. In this case, we can keep the <fa> element for this purpose.

 

Our steps for this case are:

  1. Identify and loop through all the <fa> elements on the page once the page is ready.
  2. For each one, use the inner text to set the class name attribute.
  3. Replace the displayed text with a placeholder (to display until the dynamic text is rendered). I found the icons to be approximately four spaces wide. Doing this prevents the original text from being displayed and stops "jitter" on the page while the dynamic text loads. Additionally, any invalid icon classes will remain as blank spaces on the page.
  4. Remove the display:none; style applied by the Draftail feature (the FontAwesome script will ignore any hidden or non-displayed elements).

In short, we'll be turning

<fa style="display:none;">fa-solid fa-house</fa>

into

<fa class="fa-solid fa-house">&nbsp;&nbsp;&nbsp;&nbsp;</fa>

The JavaScript for this is:

$(document).ready(() => {
  fa_icons = document.getElementsByTagName('fa');
  for (let i = 0; i < fa_icons.length; i++) {
    const fa_class = fa_icons[i].innerText;
    if (fa_class) { //ignore empty <fa> elements
      fa_icons[i].className = fa_class; // use the inner text to define the class name
      fa_icons[i].innerHTML = "&nbsp;".repeat(4); // replace the inner text/html with placeholder (4 spaces)
      fa_icons[i].removeAttribute('style'); // remove display:none; style
    }
  }
});
As previously mentioned, in a real world case, this is where you would put the meat of your code to pull in external data, perform additional calculations, render graph/image data etc..

With the Font Awesome script commented out in your <head>, you would now see our example above rendered as

This is the      home icon

from html:

This is the <fa class="fa-solid fa-house">&nbsp;&nbsp;&nbsp;&nbsp;</fa> home icon

Enable the script and reload the page, you will see it gets replaced to the following:

This is the fa-solid fa-house home icon

with html

<svg 
  class="svg-inline--fa fa-house"
  data-prefix="fas" data-icon="house"
  role="img"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 576 512"
  data-fa-i2svg="">
  <path 
    fill="currentColor" 
    d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
  >
  </path>
</svg>
<!-- <fa class="fa-solid fa-house">&nbsp;&nbsp;&nbsp;&nbsp;</fa> Font Awesome fontawesome.com -->

Note the final line is our <fa> class that has been commented out by the FontAwesome script.

Using 'Dummy' Elements vs. Class Names

It's just as valid to use a <span> element with a class name (for example <span class="fa-icon">fa-solid fa-house</span> and then get the elements using Document.getElementsByClassName().

What you need to be careful with (and the reason I didn't use it here), is that the returned array from this method is a live HTMLCollection.

From Mozilla's documentation:

fa-solid fa-triangle-exclamation Warning: This is a live HTMLCollection.

Changes in the DOM will reflect in the array as the changes occur. If an element selected by this array no longer qualifies for the selector, it will automatically be removed. Be aware of this for iteration purposes.

So, in the above code, as we would have looped through the elements, changing the class to the FontAwesome value, they would have been popped out of the array, the for loop would then only hit every odd numbered element and finally return an out of bounds error as the number of elements had diminished during looping. A do...while loop would fix this of course, and there are other ways to tackle this.

In this case, I wanted the code as simple as possible for demonstration purposes. Adapt the code as suits your needs.

Inline Styles vs. Draftail Entities

As stated in the introduction, this example is by no means the definitive way to serve dynamic text in your rich text blocks, but it is perhaps the more simpler way.

The big advantage of this method is that you can include the dynamic text in a hyperlink or any other inline or block style as you would a normal piece of text.

A link spanning some dynamic text fa-solid fa-text-width with overlapping highlight inline style and block style.

With an entity, you would need to define a link as a property of the entity, even if the surrounding text was part of that link. The text on either side would have to be defined as separate links etc.. It gets messy and complicated for the content editor who is unlikely to be making design calls on whether something is an entity, block or inline style.

The disadvantage of course is that the keywords being supplied to the JavaScript are entered freehand and not selected from a list of options. Your content editors would need to know the valid keywords to enter. In my case, the content editors were expert enough in their fields to already know what keywords they wanted to use once the format was understood, and because the possible combinations of keywords were both dynamic and vast, pre-made selectors weren't really viable in any case.

To build forms in a Draftail entity requires good React knowledge and also an intimate understanding of how entities work in Draftail and Draft.js. The process to build a simple entity add-in is surprisingly complicated. The documentation is pretty awful in my experience. Key information is missing from the start with no top-level introduction. There are no plain English explanations or methodical case studies to progress through, there is a lot of trial-and-error to see what works and what doesn't.

In the end, which route you choose would depend on the use case, your content editor's preference and capabilities, and whether you have the time/patience to sift through Draftail/Draft.js to work out what is needed to build your component. Sometimes, like Occam's Razor, the simple route is the best.

Conclusion

In this article, I covered one method of rendering inline dynamic text by creating identifiable elements that could be manipulated to produce a result at load time.

  • We created a 'dummy' element tag to identify our dynamic text keywords at load time, and discussed how this also could be achieved using an identifiable class name on span tags.
  • A simple JavaScript example was given to show where you might inject your dynamic text.
  • Finally, we took a quick look at the merits of using this method over creating an entity for this task and when one method or the other might be better suited.

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