API reference¶
Python¶
TipTapWidget(forms.Textarea)¶
django_tiptap_editor.widgets.tiptap_widget.TipTapWidget
Renders a <textarea> carrying data-tiptap-config. Config resolution (last wins):
get_default_config() → the per-instance config= → subclass overrides
(get_config(self, attrs)). class Media emits the committed bundle. storage is
"html" (default) or "json"; when None it resolves from
settings.TIPTAP_STORAGE_FORMAT. See Storage format.
A features list in the merged config is written out resolved, with the core and
every dependency included and sorted, whichever get_config produced it. See
Restricting features.
AdminTipTapWidget(TipTapWidget)¶
django_tiptap_editor.widgets.admin_tiptap.AdminTipTapWidget — admin-sized variant
(taller default via admin_defaults).
TipTapModelAdminMixin¶
django_tiptap_editor.admin.mixin.TipTapModelAdminMixin — mix in before
admin.ModelAdmin. Swaps AdminTipTapWidget onto TextFields, and makes their form field a
TipTapFormField so the admin cleans them, unless the admin names its own form_class for
TextField in formfield_overrides or the field in Meta.field_classes.
tiptap_fields = "__all__" (default) or an explicit list of field names. An explicit
list is checked at startup: a name that is not a field on the model, or one naming a
field the widget cannot apply to, is a system-check error
(django_tiptap_editor.E001–E003) rather than a silent no-op. The warning
django_tiptap_editor.W001 is described below.
AdminTipTapWidget (in JSON storage mode for a TipTapJSONField) is only the
default. A TipTap widget (a TipTapWidget subclass, or an instance of one) the
project gives the field in formfield_overrides is rendered as given, config and
all, so this is how to size or restrict the admin's editors:
from django_tiptap_editor.constants import STORAGE_FORMAT_JSON
class ArticleAdmin(TipTapModelAdminMixin, admin.ModelAdmin):
formfield_overrides = {
models.TextField: {
"widget": AdminTipTapWidget(config={"height": "200px", "maxHeight": "600px"}),
},
TipTapJSONField: {
"widget": AdminTipTapWidget(storage=STORAGE_FORMAT_JSON, config={"height": "200px"}),
},
}
The entry is looked up the way Django applies it (the first class in the field's
MRO that has one). An entry there is class-wide, so a widget in it that is not a
TipTap widget, such as a smaller Textarea for every TextField, is taken to be for
the fields the mixin leaves alone, and the editor still replaces it on the fields it
applies to; the admin's own AdminTextareaWidget default for TextField is one of
those. A form's Meta.widgets names a single field, so any widget given there is
rendered, TipTap or not, and it wins over a formfield_overrides widget. A
TextField keeps TipTapFormField whichever widget renders it, so what the admin
saves is sanitised either way.
For a TipTapJSONField, give the widget storage=STORAGE_FORMAT_JSON as above: the
mixin uses the widget as given, and one that writes HTML into the field fails
validation on every save. A TipTap widget from formfield_overrides that would do
so is reported at startup, on the admin class, as the system-check warning
django_tiptap_editor.W001. It skips a field the widget cannot reach: one with its
own widget in the form's Meta.widgets, one the form declares itself, and one the
admin leaves off its form through readonly_fields, exclude, fields or
fieldsets. It is a warning rather than an error because get_form, get_fields,
get_exclude, get_readonly_fields and get_fieldsets take a request and can
change the answer, and no startup check can call them; silence it with
SILENCED_SYSTEM_CHECKS where one of those keeps the widget off the field.
TipTapFormField(forms.CharField)¶
django_tiptap_editor.forms.fields.TipTapFormField — a CharField whose default widget
is TipTapWidget. It cleans with sanitize_html against get_html_schema(config), where
config is its widget's merged config, so a field restricted by features stores only what
those features emit. A widget that is not a TipTapWidget gets the unrestricted allowlist.
TipTapJSONFormField does the same for its mirror, and narrows the submitted doc with
narrow_doc.
TipTapJSONField(models.JSONField)¶
django_tiptap_editor.fields.tiptap_json_field.TipTapJSONField — a model field storing a
{doc, html} envelope. The Python value is a TipTapValue; the doc is protocol-allowlisted
on save, and validated (full_clean(), a ModelForm, the admin) against the node and mark
types the HTML mirror can render — see
Custom nodes and JSON storage. Optional link_protocols / image_protocols tuples override the allowlists. Its form
field is TipTapJSONFormField (a forms.Field whose widget is TipTapWidget(storage="json")).
See Storage format.
TipTapValue¶
django_tiptap_editor.types.tiptap_value.TipTapValue — frozen value with .doc (canonical
ProseMirror JSON) and .html (the safe HTML mirror, re-derived from doc on save and sanitised
on construction, so it is safe on every instance however it was built). str(value) /
{{ value }} render the mirror. TipTapValue.from_stored({...}) builds one from a {doc, html}
mapping or a bare doc, and raises ValidationError for anything else — assigning an HTML string
does not convert it.
sanitize_html¶
django_tiptap_editor.utils.sanitize_html.sanitize_html(html, *, schema=None) — reduces HTML to
what the configured editor can emit and marks the result safe. Unknown tags are unwrapped (their
text survives), unknown attributes dropped, script/style bodies discarded, link/image URLs
protocol-allowlisted, inline styles filtered. A tag in the schema's paragraph_blocks is
converted rather than unwrapped: its text becomes a paragraph, and a paragraph the sanitiser
opens is never nested inside another it opened. Raises ValidationError past MAX_DOCUMENT_DEPTH.
TipTapFormField applies it on clean and the tiptap_html filter applies it on display; call it
directly to clean a column in a data migration. See Security.
get_html_schema / HtmlSchema¶
django_tiptap_editor.utils.get_html_schema.get_html_schema(config=None) returns the HtmlSchema
the sanitiser enforces. With no config, or a config without features, it is the union of the
HTML vocabularies of every built-in extension, plus whatever TIPTAP_EXTRA_EXTENSIONS declares.
With features, it is the union of the vocabularies of resolve_features(config), plus only the
extra extensions the config's own extensions names, and an undeclared extension warns only when
the config names it. Link protocols come from the project default either way. Pass one to
sanitize_html(..., schema=...) to sanitise against a different allowlist.
HtmlSchema.paragraph_blocks is the set of built-in block tags (h1-h6, blockquote, pre,
li, td, th) a restricted schema does not admit, which sanitize_html turns into paragraphs.
It is empty by default, so a schema built by hand, and the unrestricted one, unwrap those tags as
before.
narrow_doc¶
django_tiptap_editor.utils.narrow_doc.narrow_doc(doc, *, config) returns a copy of a sanitised
ProseMirror doc holding only what resolve_features(config) and the config's extensions
allow. A heading or code block the config lacks becomes a paragraph; any other node it lacks is
replaced by its children, with loose inline content wrapped in a paragraph; a mark or attribute it
lacks is dropped, and a textStyle mark left with nothing in it goes too. With an unrestricted
config it returns doc itself. Raises ValidationError past MAX_DOCUMENT_DEPTH. The table it
reads, DOCUMENT_FEATURES in constants, maps each node type, mark type and feature-owned
attribute to its feature.
resolve_features¶
django_tiptap_editor.utils.resolve_features.resolve_features(config) returns the
frozenset of built-in extensions a config's features list turns on: the list, plus
FEATURE_CORE, plus each entry's dependencies (FEATURE_DEPENDENCIES). It returns None
when the config has no features key, or it is None, which is the unrestricted editor.
It assumes a config validate_config has accepted. See
Restricting features.
render_doc¶
django_tiptap_editor.utils.render_doc.render_doc(doc, *, link_protocols=…, image_protocols=…)
— renders a ProseMirror doc dict to a safe HTML string in pure Python (no Node), covering the
package's node/mark set with protocol allowlisting + CSS validation. The
{{ value|tiptap_html }} filter wraps it (and uses a TipTapValue's mirror when given one). See
Storage format.
BaseImageUploadView / ImageUploadError¶
django_tiptap_editor.views — subclass BaseImageUploadView and implement
save(self, file) -> str to wire storage; see Contracts.
Template tags¶
{% load tiptap %}
{% tiptap_media %} {# editor assets for the active asset mode #}
{% tiptap_config %} {# the default config as a JSON string #}
{{ value|tiptap_html }} {# render stored HTML / a TipTapValue / a doc, safely #}
All of TipTapWidget, AdminTipTapWidget, TipTapModelAdminMixin, TipTapFormField,
TipTapJSONField, TipTapJSONFormField, TipTapValue, BaseImageUploadView,
ImageUploadError, HtmlSchema, get_default_config, get_html_schema, render_doc,
sanitize_html, and validate_config are re-exported from the package root.
JavaScript — window.DjangoTipTap¶
DjangoTipTap.init(element, config) // explicit mount; returns the editor
DjangoTipTap.get(id) // editor handle | null
DjangoTipTap.destroy(id) // unmount + tear down the shell
DjangoTipTap.autoMount(root?) // idempotent; mounts every config-carrying textarea (incl. manualMount)
DjangoTipTap.registerExtension(name, factory)
DjangoTipTap.registerLocale(code, strings)
DjangoTipTap.renderHTML(doc) // ProseMirror JSON -> HTML string
DjangoTipTap.htmlToJSON(html) // HTML -> ProseMirror JSON
DjangoTipTap.htmlToStored(html) // HTML -> { doc, html } envelope (migration)
DjangoTipTap.ui.registerButton(key, spec)
DjangoTipTap.ui.setTokens(tokens)
DjangoTipTap.ui.setRenderer(region, fn) // "toolbar" | "statusbar" | "bubbleMenu" | "floatingMenu"
DjangoTipTap.ui.setShellRenderer(fn) // replace the whole shell (experimental)
DjangoTipTap.tiptap // re-exported primitives for authors
DjangoTipTap.version // the django-tiptap-editor package version
DjangoTipTap.supportedTipTapVersion // the validated TipTap version
Auto-mount (Path A)¶
Textareas rendered by the widget mount automatically. A MutationObserver watches the
DOM, so editors mount when inserted and tear down when removed — by htmx, Turbo, Unpoly,
Livewire, Alpine, Django admin inlines, or any script — with no framework-specific wiring.
The textarea is hidden and kept in sync; destroy is also exposed for manual teardown.
A destructive swap re-mounts cleanly: when a textarea is replaced by a fresh one with
the same id (an outerHTML swap), the stale editor is destroyed and the new node mounted
— never a duplicate or an orphaned shell. Place {{ form.media }} in the page <head>,
not inside a swapped partial (see
Quickstart).
Idempotency is keyed to the live editor, not the data-tiptap-bound attribute, so two
more swap shapes are handled:
- htmx history (Back/Forward). With
hx-boost/hx-push-url/hx-history, htmx caches a static snapshot of the page — including the rendered shell and the hidden textarea — and restores it on Back (firinghtmx:historyRestore, notafterSwap). The restored field re-mounts a live editor and the dead snapshot shell is removed. For pages where you'd rather skip the snapshot entirely, sethx-history="false"on the editor page. - Morphing swaps (
hx-swap="morph"/ idiomorph). A morph reconciles the live textarea's attributes back to the server markup, strippingdisplay:none/data-tiptap-bound; a per-editor observer re-asserts the hidden state in place (no remount). Mark the editor regionhx-preservefor pure-attribute morphs. Note: once an editor is live it owns the content — a server-side value change delivered via morph is overwritten on the next edit.
Opting a field out of auto-mount — set manualMount: true (per field, or in
TIPTAP_DEFAULT_CONFIG) when renderers/extensions must be registered before the editor
builds. The automatic triggers (initial scan + the MutationObserver) then skip it; mount it
yourself once you're ready with DjangoTipTap.autoMount() (mounts every field, including
manualMount ones) or DjangoTipTap.init(el, config). See Theming → Load order.
Explicit init (Path B)¶
For full control — and for the onChange callback, which can't travel through the JSON
data-tiptap-config attribute:
<textarea id="ed"></textarea>
<script>
const editor = DjangoTipTap.init(document.getElementById("ed"), {
height: "400px",
imageUploadUrl: "/editor/upload/",
mergeTags: [{ label: "First name", value: "{{ first_name }}" }],
onChange(html) { /* live preview, autosave, … */ },
});
</script>
See Extending for registerExtension / registerButton and the load
order rules, and Theming for setTokens, setRenderer, and setShellRenderer.