Skip to main content

TinyMCE-powered StreamField blocks for Wagtail, including a table block with Excel paste support.

Project description

wagtail-tinymce

A set of Wagtail StreamField blocks that embed a TinyMCE editor directly inside the page editor. The headline feature is TinyMCETableBlock: a fully-featured table block that lets editors paste tables straight from Excel or Google Sheets, create header-only or footer-only tables, and merge cells — all without needing Wagtail's built-in table block.


Features

  • Paste from Excel / Google Sheets — TinyMCE's clipboard handling converts spreadsheet data to clean HTML tables automatically.
  • Header rows (<thead>) and footer rows (<tfoot>) — a custom Footer Row toolbar button toggles the selected row between <tbody> and <tfoot>. Header rows use TinyMCE's built-in Row Header button.
  • Header-less tables — nothing forces a header row; plain body tables work fine.
  • Cell merging and splittingcolspan/rowspan via the built-in Merge Cells and Split Cell buttons.
  • wagtail-localize integrationTinyMCETableBlock implements get_translatable_segments and restore_translated_segments so table content is fully translatable, including header cells (<th>), footer cells, empty cells, and merged cells.
  • HTML sanitization — all output is passed through bleach with a strict allowlist before being stored.
  • Customisable — override custom_mce_config, allowed_tags, allowed_attributes, or pass menubar_options / toolbar_options per block instance.

Requirements

Package Minimum version
Python 3.9
Django 4.2
Wagtail 4.0
django-tinymce 5.0
bleach 6.0
beautifulsoup4 4.12
lxml 4.9

wagtail-localize ≥ 1.5 is a required dependency and is installed automatically.


Installation

wagtail-localize is a required dependency — it is always installed automatically. No extra flags are needed.

From PyPI

pip install wagtail-tinymce

From Git (master branch — TinyMCE 7, django-tinymce ≥ 5.0)

pip install git+https://github.com/ogcio/wagtail-tinymce-table.git@master

In a requirements.txt file (PEP 508):

wagtail-tinymce @ git+https://github.com/ogcio/wagtail-tinymce-table.git@master

Legacy version (TinyMCE 6, django-tinymce ≥ 3.5)

If your project requires (TinyMCE 6), install the tag instead:

pip install git+https://github.com/ogcio/wagtail-tinymce-table.git@v0.1.0

In a requirements.txt file (PEP 508):

wagtail-tinymce @ git+https://github.com/ogcio/wagtail-tinymce-table.git@v0.1.0

Add "wagtailtinymce" and "tinymce" to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "tinymce",
    "wagtailtinymce",
]

Add a minimal TinyMCE configuration to your Django settings (required by django-tinymce):

TINYMCE_DEFAULT_CONFIG = {}

Add STATIC_URL if it is not already set:

STATIC_URL = "/static/"

Run collectstatic so the Wagtail telepath adapter script is served:

python manage.py collectstatic

Quick start

Complete working example

The snippets below show every file you need to add or edit in a standard Wagtail project to get a table block working end-to-end.

1. settings.py

INSTALLED_APPS = [
    # --- Wagtail core ---
    "wagtail.contrib.forms",
    "wagtail.contrib.redirects",
    "wagtail.embeds",
    "wagtail.sites",
    "wagtail.users",
    "wagtail.snippets",
    "wagtail.documents",
    "wagtail.images",
    "wagtail.search",
    "wagtail.admin",
    "wagtail",
    # --- Django ---
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # --- Third-party ---
    "tinymce",          # django-tinymce must come before wagtailtinymce
    "wagtailtinymce",   # this package
]

# Required by django-tinymce (may be an empty dict if you rely on block-level config)
TINYMCE_DEFAULT_CONFIG = {}

2. myapp/models.py

from wagtail.models import Page
from wagtail.fields import StreamField
from wagtail.admin.panels import FieldPanel

from wagtailtinymce.core.table_block import TinyMCETableBlock


class TableDemoPage(Page):
    """A page that contains one or more TinyMCE table blocks."""

    body = StreamField(
        [
            ("table", TinyMCETableBlock()),
        ],
        blank=True,
        use_json_field=True,
        verbose_name="Page body",
    )

    content_panels = Page.content_panels + [
        FieldPanel("body"),
    ]

    class Meta:
        verbose_name = "Table demo page"

3. myapp/templates/myapp/table_demo_page.html

{% extends "base.html" %}
{% load wagtailcore_tags %}

{% block content %}
  <article>
    <h1>{{ page.title }}</h1>

    {% for block in page.body %}
      {% if block.block_type == "table" %}
        {# The block value is already sanitised HTML — render it directly #}
        <div class="table-wrapper">
          {{ block.value }}
        </div>
      {% endif %}
    {% endfor %}
  </article>
{% endblock %}

Tip: use {% include_block page.body %} instead of the manual loop if you do not need to wrap individual blocks in extra markup.

4. Optional: add basic table styles

The block stores a plain <table> element. Add CSS so it displays nicely:

/* static/css/content.css  (load this in your base template) */
.table-wrapper {
    overflow-x: auto;          /* horizontal scroll on small screens */
}

.table-wrapper table {
    border-collapse: collapse;
    width: 100%;
}

.table-wrapper th,
.table-wrapper td {
    border: 1px solid #d1d5db;
    padding: 0.5rem 0.75rem;
    text-align: left;
    vertical-align: top;
}

.table-wrapper thead th {
    background-color: #f3f4f6;
    font-weight: 600;
}

.table-wrapper tfoot td {
    background-color: #f9fafb;
    font-style: italic;
}

Table block (minimal)

from wagtail.models import Page
from wagtail.fields import StreamField
from wagtail.admin.panels import FieldPanel
from wagtailtinymce.core.table_block import TinyMCETableBlock

class MyPage(Page):
    body = StreamField(
        [("table", TinyMCETableBlock())],
        blank=True,
        use_json_field=True,
    )

    content_panels = Page.content_panels + [
        FieldPanel("body"),
    ]

Render the block in a template as you would any other StreamField block:

{% load wagtailcore_tags %}
{% include_block page.body %}

Generic TinyMCE block

TinyMCEBlock is the base class. Use it directly when you need a full rich-text editor without the table-specific toolbar:

from wagtailtinymce.blocks import TinyMCEBlock

class MyPage(Page):
    body = StreamField(
        [("rich_text", TinyMCEBlock())],
        blank=True,
        use_json_field=True,
    )

Customisation

Per-instance toolbar / menubar

Pass toolbar_options or menubar_options when declaring the block:

TinyMCETableBlock(
    toolbar_options="bold italic | table tablemergecells",
    menubar_options="",          # hide the menubar
)

Subclassing for a project-wide config

from wagtailtinymce.core.table_block import TinyMCETableBlock

class MyTableBlock(TinyMCETableBlock):
    custom_mce_config = {
        **TinyMCETableBlock.custom_mce_config,
        "language": "ga",        # Irish
        "content_css": "/static/css/editor.css",
    }

    allowed_tags = TinyMCETableBlock.allowed_tags + ["figure", "figcaption"]

Disabling sanitization

Set sanitize_input=False if you need to allow arbitrary HTML (only do this when the editor is trusted):

TinyMCETableBlock(sanitize_input=False)

Toolbar reference

The default TinyMCETableBlock toolbar groups are:

Group Buttons
Formatting bold italic link unlink
Table structure table tablecaption tablecolheader tablerowheader tablefooterrow*
Cell operations tablecellprops tablemergecells tablesplitcells
Row operations tableinsertrowbefore tableinsertrowafter tabledeleterow
Column operations tableinsertcolbefore tableinsertcolafter tabledeletecol

* tablefooterrow is a custom button added by this package. Clicking it moves the selected row into <tfoot> (or back to <tbody> if it is already a footer row).


wagtail-localize integration

When wagtail-localize is installed, TinyMCETableBlock implements the segment protocol so translators see each non-empty cell as an individual string segment.

Behaviour:

  • Empty cells are skipped and do not consume a segment index.
  • Duplicate cell values are extracted once and restored to all matching cells.
  • <th> (header) cells, <tbody> cells, and <tfoot> cells are all included.
  • Cells that contain a nested table are skipped entirely.
  • Merged cells (colspan/rowspan) are treated as a single cell.

No configuration is needed. wagtail-localize is installed automatically with the package, and Wagtail Localize will pick up the segments automatically.


Project structure

wagtailtinymce/
├── __init__.py                        # version
├── apps.py                            # Django AppConfig
├── blocks.py                          # TinyMCEBlock (base class)
├── widgets.py                         # WagtailTinyMCE widget + telepath adapter
├── core/
│   └── table_block.py                 # TinyMCETableBlock
└── static/
    └── wagtailtinymce/js/
        └── tinymce-adapter.js         # Wagtail telepath registration

Running the tests

pip install "wagtail-tinymce[dev]"   # adds pytest + pytest-django
pytest

The test suite has 75 tests covering:

  • _replace_cell_text helper (simple and compound cell paths)
  • TinyMCETableBlock configuration (allowed tags, toolbar, TinyMCE setup callback)
  • get_translatable_segments (empty cells, duplicates, <th>, <tfoot>, merged cells, round-trip)
  • restore_translated_segments (index correctness, compound cells, <tfoot>, duplicates)
  • TinyMCEBlock.sanitize (XSS, allowed tags/attributes, inline formatting)
  • TinyMCEBlock.value_from_form (SafeData, bypass mode)
  • WagtailTinyMCE widget (config merging and overrides)
  • WagtailTinyMCEAdapter (telepath registration)

Changelog

0.2.1

Bug fixes

  • Header rows now produce <th> elements. The default TinyMCE table_header_type value "section" moved rows into <thead> but kept <td> cells. Setting it to "sectionCells" correctly converts cells to <th> as well.
  • Table <caption> text is now included in translatable segments. The caption is extracted before row cells and restored in the same position, so it appears correctly in the Wagtail Localize translation UI.

0.2.0

Breaking changes

  • Requires django-tinymce >= 5.0 (previously 3.5). This bundles TinyMCE 7.8 instead of TinyMCE 6.x. If you cannot upgrade django-tinymce, pin to the v0.1.0 git tag.
  • wagtail-localize >= 1.5 is now a required dependency (previously optional).

Packaging

  • Fixed build backend from setuptools.backends.legacy:build to setuptools.build_meta for compatibility with older setuptools versions.
  • Fixed package-dir mapping so pip install git+… correctly installs the wagtailtinymce module (previously the package was silently installed empty).

0.1.0

  • Initial release.
  • TinyMCEBlock and TinyMCETableBlock StreamField blocks.
  • Excel / Google Sheets paste support via TinyMCE's clipboard plugin.
  • Custom Footer Row toolbar button for <tfoot> support.
  • wagtail-localize segment extraction and restoration, with fixes for empty cells, merged cells with rich content, and <th> cells.

Licence

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wagtail_tinymce_table-0.2.1.tar.gz (21.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wagtail_tinymce_table-0.2.1-py3-none-any.whl (12.4 kB view details)

Uploaded Python 3

File details

Details for the file wagtail_tinymce_table-0.2.1.tar.gz.

File metadata

  • Download URL: wagtail_tinymce_table-0.2.1.tar.gz
  • Upload date:
  • Size: 21.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for wagtail_tinymce_table-0.2.1.tar.gz
Algorithm Hash digest
SHA256 e01252e7b835264cc6ceb67263f84e4395c735253ad7c7288fc3aa445ec2171b
MD5 a1c02c68c5983762ff99774daed2880a
BLAKE2b-256 9d8f35c9a60951e59636eb116d6bf1e6a0a86f3a5a87374f11d1bd84b8860eb7

See more details on using hashes here.

File details

Details for the file wagtail_tinymce_table-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for wagtail_tinymce_table-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7ae10c48fc8759e0d682cc02238478499735a5969ab70fbed80b7052da5c40d6
MD5 f61a625cd3509c6e55bee23d2a4f8cf3
BLAKE2b-256 8ca1a9984e0f539a36ae59a542242856c10b65f0bdae372d4bdfbf55d5dc10be

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page