django-prose-editor

repository·main·Indexed 41 days ago

https://github.com/feincms/django-prose-editor

A rich-text editor for the Django admin interface based on ProseMirror and Tiptap. It provides ProseEditorField, SanitizedProseEditorField, and specialized widgets like AdminProseEditorWidget, featuring integrated HTML sanitization via nh3 and a configurable extension system for managing HTML tags, attributes, and JavaScript modules.

Tokens
28.2K
Snippets
82
Records
107
Agent score
52%

What's inside django-prose-editor

  1. What is the NodeClass extension?

    main

    The NodeClass extension allows you to apply arbitrary CSS classes to both block-level nodes (like paragraphs, tables, and headings) and marks (like bold, italic, and links).

    Unlike the TextClass extension which uses <span> tags for inline text, NodeClass adds classes directly to the HTML tags themselves (e.g., <p class="highlight"> or <strong class="emphasis">). This results in cleaner, more semantic HTML output.

  2. Use classloom for advanced class management

    main

    If you require more flexibility than NodeClass provides, consider using classloom.

    classloom supports multiple independent class groups. Each group can target a different node or mark type and appear as its own separate menu dropdown. This is ideal when you need more than one group of classes or want finer control over where the UI buttons appear.

    Note: Using classloom requires explicit Menu configuration to surface the dropdowns in the editor UI.

  3. How sanitization works with extensions

    main

    When sanitize=True is used, the system maps your extension settings to HTML restrictions. For example, if you configure a Link extension with specific protocols, the sanitizer will automatically strip any links using protocols not included in that list (e.g., removing javascript: or data: links if only http and https are allowed).

    content = ProseEditorField(
        extensions={
            "Link": {
                "protocols": ["http", "https", "mailto"],  # Only allow these protocols
            }
        },
        sanitize=True
    )
  4. How StyleLoom contexts resolve

    main

    StyleLoom uses contexts to determine which styling dialogs to show based on the user's position in the document.

    Context Resolution Logic

    When a user clicks a StyleLoom button, the extension walks up the ancestor tree from the cursor to find the deepest node whose type matches the context's types list.

    • Catch-all Contexts: If a context has no types defined, it acts as a catch-all for any types not claimed by other specific contexts.
    • Text Styles: Properties with the type "textStyle" are only included if the current selection is non-empty and is not a NodeSelection (i.e., it is a text selection).
    • Node Selection: If a NodeSelection is active (e.g., selecting a whole table via its handle), the selected node itself is checked before walking up the ancestors.

    Why use multiple contexts?

    Because StyleLoom walks up the tree and stops at the first match, a single context targeting multiple types (e.g., ["table", "tableCell"]) would stop at the deepest match (the cell), making the parent properties (the table) unreachable. By defining separate contexts for table and tableCell, both buttons can be visible simultaneously when the cursor is inside a cell, allowing the user to style both the cell and the table independently.

  5. Integrate extensions with the Menu system

    main

    The Menu extension allows you to organize UI elements into groups. You can integrate your extension by implementing the addMenuItems method, which is called during editor initialization. This method provides access to the editor instance, a buttons helper, and the menu object.

    When calling menu.defineItem({...}), you can use the following properties:

    • name: Unique identifier for the menu item.
    • groups: A space-separated list of groups this item belongs to.
    • priority: (Optional) Higher priorities are sorted first.
    • command: A function (editor) => void that performs an action.
    • button: The DOM element representing the menu button.
    • option: (Optional) Element when shown in a dropdown.
    • enabled: (Optional) Function returning a boolean indicating if the item is clickable.
    • active: (Optional) Function returning a boolean indicating if the item should appear active.
    • hidden: (Optional) Function returning a boolean indicating if the item should be hidden.
    • update: (Optional) Function to update dynamic content in the menu item.
    export const MyExtension = SomeBaseExtension.extend({
      addMenuItems({ editor, buttons, menu }) {
        // Register menu items for this extension using the new menu API
        menu.defineItem({
          name: "myExtension",
          groups: "myExtension",
          command: (editor) => editor.chain().myCommand().focus().run(),
          button: buttons.material("icon_name", "tooltip text"),
          enabled: (editor) => true,
          active: (editor) => editor.isActive("myExtension")
        })
      },
    })
  6. Compare NodeClass and TextClass

    main

    When deciding how to apply styles to your editor content, choose between NodeClass and TextClass based on the HTML structure you require:

    FeatureTextClassNodeClass
    TargetInline text spansBlock-level elements AND inline marks
    HTML OutputUses <span> tags (<span class="...">)Uses native semantic tags (<p class="...">, <strong class="...">, etc.)
    Best Use CaseStyling arbitrary text segments without changing the tag.Styling specific semantic elements (paragraphs, tables, links) with their native tags.

    They can be used together for comprehensive styling control.

  7. TextClass vs TextStyle: When to use TextClass

    main

    While Tiptap's TextStyle extension uses inline style attributes, TextClass uses CSS classes.

    Use TextClass when you want:

    • Maintainability: Easier to update styles globally via CSS.
    • Consistency: Ensures uniform styling across different parts of the application.
    • Theming Support: Allows styles to change based on context (e.g., light/dark mode or responsive design).
    • Security: Validates class names to prevent arbitrary style injection.
    • Performance: CSS classes are more efficient for the browser than inline styles.
  8. Release a new version to PyPI

    main

    Releases are automated via GitHub Actions using PyPI trusted publishing (OIDC). To trigger a release, create a new git tag and push it to the origin. The release process requires manual approval in the GitHub Actions UI via the release environment.

    Steps to release:

    1. Create a tag: git tag 1.2.3
    2. Push the tag: git push origin 1.2.3
    3. Approve the pending deployment in the GitHub Actions UI.
    git tag 1.2.3
    git push origin 1.2.3
  9. Troubleshoot django-prose-editor system checks

    main

    Django Prose Editor includes system checks that run automatically during Django startup or when running python manage.py check. These checks identify configuration errors and security warnings. You can run checks specifically for this package using the following command:

    python manage.py check django_prose_editor
    python manage.py check django_prose_editor
  10. Configure code style and linting with pre-commit

    main

    The project uses pre-commit hooks to enforce coding standards:

    • Ruff for Python linting and formatting.
    • Biome for JavaScript/TypeScript linting and formatting.

    You can set up prek (a Rust-based implementation of pre-commit) using uv to manage these hooks.

    uv tool install prek
    prek install
  11. Configure ClassLoom in Python

    main

    To use ClassLoom in a Django model field, include it in the extensions dictionary of a ProseEditorField. You must define a groups object where each key represents a menu group.

    To make the ClassLoom buttons visible in the editor UI, you must also integrate them into the Menu extension configuration by referencing them with the prefix classLoom:<key>.

    Note on Menu Integration: Each group produces a dropdown named classLoom:<key>. Add these to your Menu configuration to display the buttons. Each dropdown includes a default entry to clear all classes in that group, plus an entry for each defined class.

    content = ProseEditorField(
        extensions={
            "Bold": True,
            "Italic": True,
            "Table": True,
            "ClassLoom": {
                "groups": {
                    "tableStyles": {
                        "title": "Table style",
                        "type": "table",
                        "classes": [
                            {"className": "table--bordered", "title": "Bordered"},
                            {"className": "table--striped",  "title": "Striped"},
                        ],
                    },
                    "emphasisStyles": {
                        "title": "Emphasis style",
                        "type": "italic",
                        "classes": ["theme-color", "grey-color"],
                    },
                    "textDecoration": {
                        "title": "Text decoration",
                        "type": "text",
                        "combinable": True,
                        "classes": [
                            {"className": "color--primary",      "title": "Primary colour"},
                            {"className": "underline--dashed",   "title": "Dashed underline"},
                        ],
                    },
                },
            },
            "Menu": {
                "groups": [
                    {"group": "blockType -lists", "type": "dropdown", "minItems": 2},
                    {"group": "lists"},
                    {"group": "nodes -blockType -lists"},
                    {"group": "marks"},
                    {"group": "classLoom:emphasisStyles", "type": "dropdown"},
                    {"group": "classLoom:textDecoration", "type": "dropdown"},
                    {"group": "link"},
                    {"group": "table"},
                    {"group": "classLoom:tableStyles", "type": "dropdown"},
                    {"group": "history"},
                    {"group": "utility"},
                ],
            },
        },
    )
  12. Configure esbuild or webpack to treat django-prose-editor as an external

    main
    If you are using esbuild or webpack, you must ensure that the django-prose-editor JavaScript library is treated as an external dependency. This prevents the bundler from attempting to include the library's source code inside your main bundled JavaScript file, allowing the browser to load it as a separate module.