Rich Text and Email Editors
This page documents which editor implementation is used in each area of eventyay, how they are integrated, and how to reuse the shared Tiptap layer in new forms.
Overview
eventyay uses three editor implementations:
Area |
Editor |
Notes |
|---|---|---|
Talk submission forms (abstract, description, notes) |
Toast UI Editor (Markdown) |
Stays as-is. Fields use |
Video rich text and chat |
Quill 2 (Vue component) |
Stays as-is. Implemented in |
Ticket forms / general rich text |
Tiptap ( |
New. Use for new plain-HTML rich text fields. |
Ticket Message center (compose / outbox / team mail) |
Tiptap ( |
Primary integration. Gmail-like editor with placeholder insertion. |
Order personal email (order detail → Send email) |
Tiptap ( |
Secondary. Same email profile for one-off messages. |
Tiptap Editor Layer
The shared Tiptap implementation lives in two places:
Frontend bundle:
app/eventyay/webapp/editor/Django layer:
app/eventyay/common/
Frontend Bundle (webapp/editor/)
A Vite-built IIFE bundle with no external framework dependency. The built
editor.js and editor.css are committed under eventyay/static/editor/
(same pattern as Toast UI in static/vendored/) so production
collectstatic picks them up without an extra Docker npm build.
Key files:
webapp/editor/ # Source (edit here)
src/
index.js # Entry point; auto-mounts on [data-tiptap-profile]
profiles/
richtext.js # Simple rich text (Bold, Italic, Underline, Lists, Link)
email.js # Email profile (extends richtext + placeholder chips)
extensions/
placeholder-variable.js # Custom non-editable inline node for {var} chips
toolbar.js # Toolbar DOM builder
styles.css # Editor and toolbar styles
eventyay/static/editor/ # Deployed bundle (commit after rebuilding)
editor.js
editor.css
Editor profiles:
richtextFor general-purpose rich text fields. Toolbar: Bold, Italic, Underline, Bullet list, Numbered list, Link, Clear formatting, Undo, Redo.
emailExtends
richtextwith an “Insert placeholder” dropdown (populated fromdata-tiptap-placeholdersJSON on the textarea) and a Preview button (callsdata-tiptap-preview-url).
Progressive enhancement:
The bundle mounts on any <textarea data-tiptap-profile="..."> it finds at
load time. On form submit, it syncs the editor HTML back into the hidden
textarea so the Django form receives the HTML value. When JavaScript is
disabled the original textarea is used directly.
Building:
After changing webapp/editor/ source, rebuild and commit the output under
static/editor/:
npm ci --prefix=eventyay/webapp/editor
OUT_DIR=$(pwd)/eventyay/static/ npm run build --prefix=eventyay/webapp/editor
make npminstall in app/ runs the same editor build step.
Lazy Loader (static/common/js/tiptapLoader.js)
A tiny script included in all base templates. It checks for
[data-tiptap-profile] elements on the current page and, if found,
dynamically injects editor.js and editor.css. Pages without Tiptap
fields incur no bundle download.
The loader is inserted via a <script> tag with data-tiptap-js and
data-tiptap-css data attributes pointing to the compiled bundle URLs. This
mirrors the pattern used by toastuiLoader.js.
Django Integration
Widgets
Both widgets are in eventyay.common.forms.widgets.
RichTextWidgetExtends
Textarea. Setsdata-tiptap-profile="richtext"on the textarea. Uses templatecommon/widgets/richtext.htmlwhich wraps the textarea in<div data-tiptap-wrapper>.EmailEditorWidgetExtends
Textarea. Setsdata-tiptap-profile="email". Acceptsplaceholders(list of variable names) andpreview_url(AJAX endpoint URL). Uses templatecommon/widgets/email_editor.html.
Form Fields
Both fields are in eventyay.common.forms.fields.
RichTextFieldA
CharFieldthat usesRichTextWidgetand callssanitize_rich_text()insideclean().EmailBodyFieldA
CharFieldthat usesEmailEditorWidgetand callssanitize_email_html()insideclean(). Constructor acceptsplaceholdersandpreview_url.I18nEmailEditorWidget/I18nEmailBodyFormFieldFor multi-locale email bodies (Message center). Extends django-i18nfield’s
I18nTextarea/I18nFormField. Each locale tab gets its own Tiptap email editor instance. Sanitizes every locale value inclean().
Message Center (primary wiring)
The Message center plugin (eventyay.plugins.sendmail) is the main
production use case for the Tiptap email editor in Tickets:
Compose — mass mail to orders/attendees (
MailForm)Outbox — edit queued messages before sending (
EmailQueueEditForm)Compose to teams — internal team notifications (
TeamMailForm)
All three forms use I18nEmailBodyFormField for the Message field. The
editor toolbar provides formatting, placeholder insertion, and an AJAX preview
via control:event.editor.email.preview.
Plain-text and Markdown bodies stored before this change continue to work:
compile_email_body() in eventyay.base.templatetags.rich_text detects
whether content is already HTML and only runs markdown_compile_email() for
legacy plain-text bodies.
Example usage (single-locale form):
from eventyay.common.forms.fields import RichTextField, EmailBodyField
class MyForm(forms.Form):
description = RichTextField(label='Description')
message = EmailBodyField(
label='Message',
placeholders=['attendee_name', 'event_name', 'order_code'],
preview_url='/control/.../editor/email-preview',
)
Example usage (Message center, multi-locale):
from eventyay.common.forms.fields import I18nEmailBodyFormField
from eventyay.common.forms.widgets import I18nEmailEditorWidget
self.fields['message'] = I18nEmailBodyFormField(
label='Message',
widget=I18nEmailEditorWidget,
widget_kwargs={'placeholders': placeholder_names, 'preview_url': preview_url},
locales=event.settings.get('locales'),
)
Server-side Sanitization
eventyay.common.sanitizers provides two functions backed by the nh3
library (Rust/Ammonia-based):
sanitize_rich_text(html)Allowed tags:
p,br,strong,b,em,i,u,ul,ol,li,a,blockquote. Allowed link attributes:href,title(http:///https://only). Addsrel="noopener noreferrer"to all anchor elements.sanitize_email_html(html)Same as
sanitize_rich_textplusspan. Does not injectrelattributes because email clients may display or strip them.
All unsafe tags, event attributes (onclick, onerror, etc.), and
non-HTTP/HTTPS link protocols are removed.
Email Preview Endpoint
POST /control/<organizer>/<event>/editor/email-preview
Accepts a JSON body { "html": "<p>...</p>" }, sanitizes it with
sanitize_email_html, and returns { "html": "<p>...</p>" }.
Requires can_change_orders permission on the event. The editor’s Preview
button calls this URL and displays the result in a <dialog> modal.
URL name: control:event.editor.email.preview
Placeholder Variables
The email profile supports {variable_name} placeholder chips. Each
chip is a non-editable inline Tiptap node that serialises as literal
{variable_name} text in the stored HTML.
Placeholders for an email form should be derived from the same source used by
the existing email system (get_available_placeholders from
eventyay.base.email). Pass the plain variable names (without braces) to
EmailBodyField(placeholders=[...]):
from eventyay.base.email import get_available_placeholders
placeholder_names = sorted(
get_available_placeholders(event, ['event', 'order']).keys()
)
field = EmailBodyField(placeholders=placeholder_names, ...)
Toast UI Markdown Editor (existing)
Talk submission fields (abstract, description, notes, biography) continue to
use MarkdownWidget / I18nMarkdownTextarea backed by the vendored Toast
UI Editor bundle in static/vendored/toastui-editor/.
These fields store Markdown, not HTML. Migration to Tiptap (with Markdown serialisation) may be evaluated in a future iteration but is explicitly out of scope for this implementation.
MarkdownWidget is in eventyay.common.forms.widgets.
I18nMarkdownTextarea is in eventyay.base.forms.
Quill (existing, video only)
webapp/video/ uses Quill 2 for exhibitor/poster rich text and the chat
input. Quill is an npm dependency of the video app only and is not shared
with the rest of eventyay. Migration to Tiptap is a future task.
There is also a legacy vendored copy of Quill 1.1.9 in
static/pages/ which appears to be unused by current templates.
Migration Path
The long-term goal is for Tiptap to be the single shared editor framework:
New rich text fields → use
RichTextField/EmailBodyFieldnow.Toast UI (Talk submissions) → evaluate migration to Tiptap with Markdown serialisation in a future iteration.
Quill (video app) → evaluate migration to Tiptap Vue component in a future iteration.
Do not introduce CKEditor, TinyMCE, ProseMirror directly, or any other editor framework into new code.