21. 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.richtext.widgets.RichTextarea
.
from django.forms import fields, forms
from formset.richtext.widgets 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:
21.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.richtext.widgets 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:
21.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.
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.
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.
21.1.2. 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 ref:dialog-form`, which has to be specified as argument to this control
element.
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 responsibility to provide a custom dialog form for this purpose.
Section Richtext Extensions explains in detail how to do this.
Footnote
The class formset.richtext.dialog.FootnoteDialogForm
can be used to add a footnote 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 a
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.
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 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.
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”.
21.1.3. 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.
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.
21.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:
21.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.
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 alternative is to store that data as JSON.
21.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.
django-formset provides a special model field class
formset.richtext.fields.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.richtext.fields 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.
21.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
.
21.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>
21.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.