26. Edit Richtext¶
A RichTextarea
allows editing or pasting formatted text, similar to traditional “What you see is
what you get” (WYSIWYG) editors. The current implementation offers common text formatting options
such as paragraphs, headings, emphasized and bold text, ordered and bulleted lists, and hyperlinks.
More text formatting options will be implemented in the future.
The django-formset library provides a widget, which can be used as a drop-in replacement for the
HTML element <textarea>
, implemented as a web component. In a Django form’s CharField
, we
just have to replace the built-in widget against formset.widgets.RichTextarea
.
from django.forms import fields, forms
from formset.widgets.richtext import RichTextarea
class BlogForm(forms.Form):
text = fields.CharField(widget=RichTextarea)
This widget can be configured in various ways in order to specifically enable the currently implemented formatting options. With the default settings, this textarea will show up like:
26.1. Configuration¶
When offering a rich textarea, the default formatting options may not be appropriate. Therefore,
the widget class RichTextarea
can be configured using various control elements.
from formset.richtext import controls
from formset.widgets.richtext RichTextarea
richtext_widget = RichTextarea(control_elements=[
controls.Bold(),
controls.Italic(),
])
This configuration would only allow to format text using bold and italic. Currently django-formset implements these formatting options:
26.1.1. Simple Formatting Options¶
Heading
The class formset.richtext.controls.Heading
can itself be configured using a list of levels
from 1 through 6. Heading([1, 2, 3])
allows for instance, to format a heading by using the HTML
tags <h1>
, <h2>
and <h3>
. If provided without parameters, all 6 possible heading
levels are available. If only one level is provided, for instance as Heading(1)
, then the
heading button does not provide a pull down menu, but instead is rendered as a single H1 button.
This allows placing heading buttons for different levels on the toolbar side by side.
Bold
The class formset.richtext.controls.Bold
can be used to format a selected part of the text
in bold variant of the font. It can’t be further configured.
Italic
The class formset.richtext.controls.Italic
can be used to format a selected part of the
text in an emphasized (italic) variant of the font. It can’t be further configured.
Underline
The class formset.richtext.controls.Underline
can be used to format a selected part of the
text as underlined. This option rarely makes sense. It can’t be further configured.
BulletList
The class formset.richtext.controls.BulletList
can be used to format some text as a
bulleted list. It can’t be further configured.
OrderedList
The class formset.richtext.controls.OrderedList
can be used to format some text as ordered
(ie. numbered) list. It can’t be further configured.
HorizontalRule
The class formset.richtext.controls.HorizontalRule
can be used to add a horizontal rule
between paragraphs of text. It can’t be further configured.
Clear Format
The class formset.richtext.controls.ClearFormat
can be used to remove the current format
settings of selected text. It can’t be further configured.
Undo and Redo
The classes formset.richtext.controls.Undo
and formset.richtext.controls.Redo
can
be used to undo and redo changes on the current text. They can’t be further configured.
Subscript
The class formset.richtext.controls.Subscript
can be used to mark text as subscript, which
renders the selected text smaller and below the baseline.
Superscript
The class formset.richtext.controls.Superscript
can be used to mark text as superscript,
which renders the selected text smaller and above the baseline.
Text Align
The class formset.richtext.controls.TextAlign
can be used to align a block of text. It must
be initialized as
TextAlign(['left', 'center', 'right', 'justify])
this will create a drop down menu offering these three options. As an alternative one can for instance use
TextAlign('right')
which creates a single button to align the selectd text box to the right.
Text Color
The class formset.richtext.controls.TextColor
can be used to mark text in different colors.
It offers two different modes: Styles and CSS classes. When used with styles, the control element
must be initialized with colors in rgb format, for instance:
TextColor(['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)'])
This will offer text in three colors, red, green and blue. When used with classes, the control element must be initialized with arbitrary CSS classes, for instance
TextColor(['text-red', 'text-green', 'text-blue'])
The implementor then is responsible for setting the text color in its CSS file for these classes. Style- and class-based initialization can not be interchanged.
Text Indent
The class formset.richtext.controls.TextIndent
can be used to indent and outdent the first
line of a text block. It must be initialized as
TextIndent('indent') # to indent the first line
TextIndent('outdent') # to indent all but the first line
Text Margin
The class formset.richtext.controls.TextMargin
can be used to indent and dedent a text
block. It must be initialized as
TextMargin('increase') # to increase the left margin
TextIndent('decrease') # to decrease the left margin
Blockquote
The class formset.richtext.controls.Blockquote
can be used to mark a text block as quoted
by adding a thick border on its left.
Code Block
The class formset.richtext.controls.CodeBlock
can be used to mark a text block as a code
block. This is useful to show samples of code.
Hard Break
The class formset.richtext.controls.Hardbreak
can be used to add a hard break to a
paragraph, ie. add a <br>
to the rendered HTML.
Separator
The class formset.richtext.controls.Separator
has no functional purpose. It can be used
to separate the other buttons visually using a vertical bar.
Group
The class formset.richtext.controls.Group
is just a wrapper and can be used to group other
control elements. Each group shows a vertical bar for visual separation unless it isn’t the last
entry in the current line. It hence can be used to group buttons which belong together. If the group
does not fit into the menu bar, it wraps altogether to the next line.
FontFamily
The class formset.richtext.controls.FontFamily
can be used to change the font family of
the selected text. It must be initialized with a list of two-tuples, containing the CSS class as its
first element and a label as its second element. The FontFamily
is based on the
ClassBaseControlElement
control element – check below for details.
FontSize
The class formset.richtext.controls.FontSize
can be used to change the font size of the
selected text. It also is based on the ClassBaseControlElement
control element and must be
initialized with a list of two-tuples.
LineHeight
The class formset.richtext.controls.LineHeight
can be used to change the line height of the
current paragraph. It also is based on the ClassBaseControlElement
control element and must be
initialized with a list of two-tuples.
26.1.2. Generic Control Elements¶
TipTap offers a wide range of formatting options, which can be added to the editor. For customized extensions, one however must provide their own extension class written in JavaScript.
When creating a control element for the RichtextArea, we often just need to provide a set of CSS
classes which can be used to style some text. The chosen CSS class out of this set of options then
is applied to the selected text, resulting in a <span class="…">styled text</span>
-element.
For Django developers it would be very tedious to write a JavaScript extension for each of these formatting options. Therefore django-formset offers a generic control element, which can be configured using a list of CSS classes. This control element then adds a drop down menu to the editor’s toolbar and allows the user to select one of the provided classes, which in turn are applied to the selected text.
As an example, assume that we want to be able apply special styling to some text, for instance to mark it. We then can create a control element such as:
from formset.richtext.controls import ClassBaseControlElement
class MarkControl(ClassBaseControlElement):
extension = 'markText'
label = _("Mark Text")
The attribute extension
must be a unique identifier for this control element. The attribute
label
is a human readable string which will be shown as the button’s tooltip. In addition, we
may provide a symbol rendered in the button by specifying the attribute
icon = 'path/to/mark-icon.svg.'
.
When declaring the RichtextArea
widget, we then can add this custom control element to our list
of control elements:
class BlogForm(forms.Form):
text = fields.CharField(widget=RichTextarea(control_elements=[
...
MarkControl({
'text-red': "Red",
'text-green': "Green",
'text-blue': "Blue",
}),
...
])
Now, when the user selects some text and clicks on the button labeled “Mark Text”, a drop down menu will appear, offering the options “Default”, “Red”, “Green” and “Blue”. When selecting one of these options, a span element with the selected CSS class will be wrapped around the selected text. The item labeled “Default” will remove the CSS class from the selected text.
Note
When implementing this, do not forget to declare the named CSS classes in its CSS file.
A variant of the above control element is, when the CSS shall not be applied to the selected text,
but to a whole block. This can be achieved by adding the member extension_type = 'node'
to the
class inheriting from ClassBaseControlElement
. The control element LineHeight
(see above)
is an example for this.
26.1.3. Composed Formatting Options¶
In addition to the simple formatting options, django-formset offer some control elements which
require multiple parameters. They use the class formset.richtext.controls.DialogControl
,
which when clicked opens a Dialog Form. As its only argument, it takes an
instance of a dialog form. Check the possible options below:
Here are the built-in dialog forms:
Link
The class formset.richtext.dialog.SimpleLinkDialogForm
can be used to add a hyperlink to a
selected part of some text. When choosing this option, a dialog pops up and the user can enter a
URL and edit the selected text.
To declare this control write:
from formset.richtext.controls import DialogControl
from formset.richtext.dialogs import SimpleLinkDialogForm
DialogControl(SimpleLinkDialogForm())
The form is named SimpleLinkDialogForm
because it only allows to enter a URL. The users of this
rich text field might however want to edit hyperlinks with the ref
and target
attributes,
and might also want to set links on Django models providing the method get_absolute_url, but
referring to the primary key of the provided object. Since there can’t be any one-size-fits-all
solution, it is the implementor’s responsibility to provide a custom dialog form for this purpose.
Section Richtext Extensions explains in detail how to do this.
Footnote
An instance of the class formset.richtext.dialog.FootnoteDialogForm
can be used to add a
footnote editor to the editable rich text. When choosing this option, a dialog pops up with another
richtext editor inside. This editor can be configured in the same way as the main editor, but
usually one would only allow very few formatting options. The content of this editor will be stored
as a footnote and is not visible in the main text area. Instead, only a [*]
will be rendered.
This control element only works if the editor’s payload is stored as JSON. Reason is that the richtext renderer adds them to the end of the document in a second run. Check for details below.
Image
The class formset.richtext.dialog.SimpleImageDialogForm
can be used to add an image to the
editable rich text. When choosing this option, a dialog pops up and the user can drag an image into
the upload field. It will be uploaded to the server and only a reference to this image will be
stored inside the text. The form is named SimpleImageDialogForm
because it only allows to upload
an image. The users of this rich text field might however want to edit the image size, the alt text,
the caption, the alignment and other custom fields. Since there can’t be any one-size-fits-all
solution, it is the implementor’s responsibility to provide a custom dialog form for this purpose.
Therefore this dialog form can be used as a starting point for a custom image uploading dialog form.
More on this can be found in section Richtext Extensions.
Changed in version 2.0: After submission, the uploaded image is copied from the temporary upload folder into its final
destination, which can be configured using the attribute upload_to
of the form field
formset.formfields.richtext.RichTextField
.
Placeholder
The class formset.richtext.dialog.PlaceholderDialogForm
can be used to add a placeholder to
the selected part of some text. When choosing this option, a dialog pops up and the user can enter a
variable name and edit the selected placeholder text. Such a control element can be used to store
HTML with contextual information. When this HTML content is finally rendered, those placeholder
entries can be replaced against some context using the built-in Django template rendering functions.
Note
Internally the placeholder extension is named “procurator” to avoid a naming conflict, because there is a built-in TipTap extension named “placeholder”.
26.1.4. Additional Attributes¶
Apart from the control elements, the rich text editor widget can be configured using additional attributes:
maxlength
By adding maxlength
to the widget’s attributes, we can limit the number of characters to be
entered into this text field. In the bottom right corner, this will show how many characters can
still be entered.
use_json
If the content of the rich text editor shall be stored as JSON, set use_json=True
. This only is
required when using this widget for a Django form’s CharField
. When using the model field class
formset.modelfields.RichTextField
, this is not necessary.
placeholder
By adding placeholder="Some text"
to the widget’s attributes, we can add a placeholder to the
text field. This will disappear as soon as we start typing.
26.2. Richtext as a Model Field¶
In the example from above, we used a Django form CharField
and replaced the default widget
provided by Django (TextInput
). A more common use case is to store the entered rich text in
a database field. Here django-formset offers two solutions:
26.2.1. Storing rich text as HTML¶
Storing rich text as HTML inside the database using the field django.db.models.fields.TextField
is the simplest solution. It however requires to override the default widget (Textarea
) against
the RichTextarea
provided by django-formset, when instantiating the form associated with
this model.
Always keep in mind that storing HTML provides by arbitrary users is a potential security risk. When rendering this HTML, it is important to sanitize it using a library such as django-nh3. Moreover, do not forget to mark the content as safe text, when rendering it inside a Django template.
If the content of such a field shall be rendered inside a Django template, do not forget to mark it as “safe”, either by using the function django.utils.safestring.mark_safe or by using the template filter {{ …|safe }}.
While this is a quick and fast solution, we shall always keep in mind that storing plain HTML inside a database field, prevents us from transforming the stored information into the final format while rendering. This means that the stored HTML is rendered as-is. A better and more secure alternative is to store that data as JSON.
26.2.2. Storing rich text as JSON¶
Since HTML content has an implicit tree structure, an alternative approach to HTML is to keep this hierarchy unaltered when storing. The best suited format for this is JSON. This approach has the advantage that HTML is rendered during runtime, allowing to adopt the result as needed. It also does not require to sanitize the content, because the JSON structure is only converted to HTML tags allowed by the implementation.
django-formset provides a special model field class
formset.modelfields.RichTextField
. It shall be used as a replacement to Django’s
model field class TextField
. This model field provides the widget RichTextarea
using the
default settings. Often that might not be the desired configuration, and it may be necessary to
re-declare that widget, while creating the form from the model.
In this example we use a model with one field for storing the rich text entered by the user:
from django.db.models import Model
from formset.modelfields import RichTextField
class BlogModel(Model):
body = RichTextField()
We then use that model to create a Django ModelForm. For demonstration purposes we configure all available control elements. Such a configured editor then will look like:
from django.forms.models import ModelForm
from formset.richtext import controls
from formset.richtext import dialogs
from testapp.models import BlogModel
class EditorForm(ModelForm):
class Meta:
model = BlogModel
fields = '__all__'
widgets = {
'body': RichTextarea(
control_elements=[
controls.Heading([1,2,3]),
controls.Bold(),
controls.Blockquote(),
controls.CodeBlock(),
controls.HardBreak(),
controls.Italic(),
controls.Underline(),
controls.TextColor([
'rgb(212, 0, 0)',
'rgb(0, 212, 0)',
'rgb(0, 0, 212)',
]),
controls.TextIndent(),
controls.TextIndent('outdent'),
controls.TextMargin('increase'),
controls.TextMargin('decrease'),
controls.TextAlign(['left', 'center', 'right']),
controls.HorizontalRule(),
controls.Subscript(),
controls.Superscript(),
controls.DialogControl(dialogs.SimpleLinkDialogForm()),
controls.DialogControl(dialogs.PlaceholderDialogForm()),
controls.DialogControl(dialogs.FootnoteDialogForm()),
controls.Separator(),
controls.ClearFormat(),
controls.Redo(),
controls.Undo(),
],
attrs={
'placeholder': "Start typing …",
'maxlength': 2000
},
),
}
Note
After submission, the content of this form is stored in the database. Therefore after reloading this page, the same content will reappear in the form.
26.2.3. Rendering Richtext¶
Since the editor’s content is stored in JSON, it must be converted to HTML before being rendered. For this purpose django-formset offers a templatetag, which can be used such as:
{% load richtext %}
{% render_richtext obj.body %}
Here obj
is a Django model instance with the field body
of type RichTextField
.
26.2.4. Overriding the Renderer¶
By postponing the conversion from JSON to a readable format, we can keep our document structure until it is rendered. django-formset provides default templates for this conversion, but you may want to use your own ones:
{% load richtext %}
{% render_richtext obj.content "path/to/alternative/doc.html" %}
The template doc.html
is the starting point for each document. Looking at the structure of a
rich text document stored in JSON, we see the hierachical structure:
{
"text": {
"type": "doc",
"content": [{
"type": "paragraph",
"content": [{
"type": "text",
"text": "This is "
}, {
"type": "text",
"marks": [{
"type": "bold"
}],
"text": "bold"
}, {
"type": "text",
"text": " "
}, {
"type": "text",
"marks": [{
"type": "italic"
}],
"text": "and italic"
}, {
"type": "text",
"text": " text."
}]
}]
}
}
The type
determines the template to use, whereas content
is a list of nodes, rendered using
their own sub-template determined by their own type
.
When rendered by the default richtext/doc.html
template, its output looks like:
<p>This is <strong>bold</strong> <em>and italic</em> text.</p>
26.3. Implementation Details¶
This Richtext editing widget is based on the headless Tiptap editor. This framework offers many more formatting options than currently implemented by the django-formset library. In the near future I will add them in a similar way to the existing control elements. Please help me to implement them by contributing to this project.
With Tiptap it even is possible to create application specific control elements, which thanks to the internal JSON structure, then can be transformed to any imaginable HTML.