Skip to content

matthiask/django-prose-editor

Repository files navigation

django-prose-editor

Prose editor for the Django admin based on ProseMirror and Tiptap. Announcement blog post.

About rich text editors

Copied from the django-content-editor documentation.

We have been struggling with rich text editors for a long time. To be honest, I do not think it was a good idea to add that many features to the rich text editor. Resizing images uploaded into a rich text editor is a real pain, and what if you’d like to reuse these images or display them using a lightbox script or something similar? You have to resort to writing loads of JavaScript code which will only work on one browser. You cannot really filter the HTML code generated by the user to kick out ugly HTML code generated by copy-pasting from word. The user will upload 10mb JPEGs and resize them to 50x50 pixels in the rich text editor.

All of this convinced me that offering the user a rich text editor with too much capabilities is a really bad idea. The rich text editor in FeinCMS only has bold, italic, bullets, link and headlines activated (and the HTML code button, because that’s sort of inevitable – sometimes the rich text editor messes up and you cannot fix it other than going directly into the HTML code. Plus, if someone really knows what they are doing, I’d still like to give them the power to shot their own foot).

If this does not seem convincing you can always add your own rich text plugin with a different configuration (or just override the rich text editor initialization template in your own project). We do not want to force our world view on you, it’s just that we think that in this case, more choice has the bigger potential to hurt than to help.

Installation

The first step is to ensure that you have an activated virtualenv for your current project, using something like . .venv/bin/activate.

Install the package into your environment:

pip install django-prose-editor[sanitize]

The sanitize extra automatically installs nh3 for the recommended HTML sanitization. You can omit this if you want to use a different HTML sanitizer.

Add django_prose_editor to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "django_prose_editor",
]

Add the importmap by adding the js_asset.context_processors.importmap context processor and inserting {{ importmap }} somewhere in your base template, above all other scripts.

Replace models.TextField with ProseEditorField where appropriate:

from django_prose_editor.fields import ProseEditorField

class Project(models.Model):
    description = ProseEditorField(extensions={"Bold": True, "Italic": True})

Note! No migrations will be generated when switching from and to models.TextField. That's by design. Those migrations are mostly annoying.

Configuration

ProseMirror does a really good job of only allowing content which conforms to a particular scheme. Of course users can submit what they want, they are not constrainted by the HTML widgets you're using. You should always sanitize the HTML submitted on the server side.

The recommended approach is to use the extensions mechanism for configuring the prose editor field which automatically synchronizes editor extensions with sanitization rules:

from django_prose_editor.fields import ProseEditorField

content = ProseEditorField(
    extensions={
        "Bold": True,
        "Italic": True,
        "BulletList": True,
        "Link": True,
    },
    # sanitize=True is the default when using extensions
)

This ensures that the HTML sanitization rules exactly match what the editor allows, preventing inconsistencies between editing capabilities and allowed output. Note that you need the nh3 library for this which is automatically installed when you specify the requirement as django-prose-editor[sanitize].

Old Approach

For backward compatibility, you can still use the legacy SanitizedProseEditorField, although this approach is now discouraged since it uses the default configuration of the nh3 sanitizer which is safe but allows many many HTML tags and attributes:

from django_prose_editor.sanitized import SanitizedProseEditorField

description = SanitizedProseEditorField()

Alternatively, you can pass your own callable receiving and returning HTML using the sanitize keyword argument.

Convenience

Sometimes it may be useful to show an excerpt of the HTML field; the ProseEditorField automatically adds a get_*_excerpt method to models which returns the truncated and stripped beginning of your HTML field's content. The name would be Project.get_description_excerpt in the example above.

Customization

The editor can be customized in several ways:

  1. Using the new extensions mechanism with ProseEditorField (recommended).
  2. Using the config parameter to include/exclude specific extensions (legacy approach)
  3. Creating custom presets for more advanced customization

Note that the ProseEditorField automatically uses the extension mechanism except if you initialize it with the legacy config dictionary.

Simple customization with extensions

from django_prose_editor.fields import ProseEditorField

class Article(models.Model):
    content = ProseEditorField(
        extensions={
            "HardBreak": True,
            "Bold": True,
            "Italic": True,
            "BulletList": True,
            "OrderedList": True,
            "HorizontalRule": True,
            "Link": True,
            "Table": True,
            "History": True,
            "HTML": True,  # Enable HTML editing
            "Typographic": True,  # Highlight typographic characters
        },
        # sanitize=True,  # It's the default.
    )

Some extensions also support additional configuration, for example:

extensions={
    # ...
    "Link": {"enableTarget": False},  # Disable the 'open in new window' checkbox
    "Heading": {"levels": [1, 2, 3]},  # Offer a subset of H1-H6
    "OrderedList": {"enableListAttributes": False},  # Disable the start/type dialog
    # ...
}

Available extensions include:

  • Text formatting: Bold, Italic, Strike, Subscript, Superscript, Underline
  • Lists: BulletList, OrderedList, ListItem
  • Structure: Blockquote, Heading, HorizontalRule
  • Links: Link
  • Tables: Table, TableRow, TableHeader, TableCell

Check the source code for more!

The extensions which are enabled by default are Document, Paragraph and Text for the document, Menu, History, Dropcursor and Gapcursor for the editor functionality and NoSpellCheck to avoid ugly spell checker interference. You may disable some of these core extensions e.g. by adding "History": False to the extensions dict.

For additional details, check the docs/configuration_language.rst document.

Simple Customization with Config (Deprecated)

For basic customization, you can use the config parameter to specify which extensions should be enabled. This was the only available way to configure the prose editor up to version 0.9. It's now deprecated because using the extensions mechanism documented above is much more powerful, integrated and secure.

from django_prose_editor.fields import ProseEditorField

class Article(models.Model):
    content = ProseEditorField(
        config={
            "types": [
                "Bold", "Italic", "Strike", "BulletList", "OrderedList",
                "HorizontalRule", "Link",
            ],
            "history": True,
            "html": True,
            "typographic": True,
        }
    )

All extension names now use the Tiptap names (e.g., Bold, Italic, BulletList, HorizontalRule). For backward compatibility, the following legacy ProseMirror-style names are still supported:

  • Legacy node names: bullet_listBulletList, ordered_listOrderedList, horizontal_ruleHorizontalRule
  • Legacy mark names: strongBold, emItalic, strikethroughStrike, subSubscript, supSuperscript, linkLink

Usage with JavaScript bundlers

If you're using a bundler such as esbuild, rspack or webpack you have to ensure that the django-prose-editor JavaScript library is treated as an external and not bundled into a centeral JavaScript file. In the case of rspack this means adding the following lines to your rspack configuration:

module.exports = {
    // ...
    experiments: { outputModule: true },
    externals: {
        "django-prose-editor/editor": "module django-prose-editor/editor",
        "django-prose-editor/configurable": "module django-prose-editor/configurable",
    },
}

This makes rspack emit ES modules and preserves imports of django-prose-editor/editor and similar in the output instead of trying to bundle the library.

Usage outside the Django admin

The prose editor can easily be used outside the Django admin. The form field respectively the widget includes the necessary CSS and JavaScript:

from django_prose_editor.fields import ProseEditorFormField

class Form(forms.Form):
    text = ProseEditorFormField()

Or maybe you want to use django_prose_editor.widgets.ProseEditorWidget, but why make it more complicated than necessary.

If you're rendering the form in a template you have to include the form media:

<form method="post">
  {% csrf_token %}
  {{ form.media }}  {# This is the important line! #}

  {{ form.errors }} {# Always makes sense #}
  {{ form.as_div }}
  <button type="submit">send</button>
</form>

Note that the form media isn't django-prose-editor specific, that's a Django feature.

The django-prose-editor CSS uses the following CSS custom properties.

  • --prose-editor-background
  • --prose-editor-foreground
  • --prose-editor-border-color
  • --prose-editor-active-color
  • --prose-editor-disabled-color

If you do not set them, they get their value from the following properties that are defined in the Django admin's CSS:

  • --border-color
  • --body-fg
  • --body-bg
  • --primary

You should set these properties with appropriate values to use django-prose-editor outside the admin in your site.

In addition, you may optionally set a --prose-editor-typographic property to control the color of typographic characters when shown.

Development

For the best development experience:

  1. Install django-prose-editor in editable mode in your project:

    pip install -e /path/to/django-prose-editor
  2. Run yarn && yarn dev in the django-prose-editor directory to watch for asset changes.

When using yarn dev:

  • The generated CSS and JavaScript is not minified, making it easier to debug.
  • Source maps are generated to help identify exactly where in the source code an error occurs.
  • The watcher will rebuild files automatically when you make changes.

Source maps are generated in development mode (yarn dev) for easier debugging, but not included in production builds to keep the package size manageable. The JavaScript in this project is quite extensive, so source maps would significantly increase the distribution size.

The pre-commit configuration includes a hook that prevents committing files with source map references, ensuring that development artifacts don't make it into the repository.

Browser Testing with Playwright

This project uses tox to describe environments and Playwright for browser-based testing of the prose editor. Browser tests are run as a part of the normal tests so just use tox as you normally would.

Code Style and Linting

This project uses pre-commit hooks to enforce coding style guidelines. We use Ruff for Python linting and formatting, Biome for JavaScript/TypeScript linting and formatting and a few other hooks.

To set up pre-commit using uv:

uv tool install pre-commit
pre-commit install

Pre-commit will automatically check your code for style issues when you commit changes.