Skip to main content

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

Project description

wagtail-tinymce-table

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.

Compatibility

Version matrix

wagtail-tinymce-table Wagtail Django Python wagtail-localize
0.3.x (current) 4.0 – 7.x 4.2 LTS, 5.2 LTS, 6.0 3.9 – 3.14 ¹ ≥ 1.5 ²
0.2.x 4.0 – 6.x 4.2, 5.0, 5.1 3.9 – 3.13 ≥ 1.5
0.1.x 4.0 – 5.x 4.2, 5.0 3.9 – 3.12 ≥ 1.5

¹ Wagtail 7 itself requires Python ≥ 3.10.
² When installed alongside Wagtail 7, pip automatically resolves wagtail-localize ≥ 1.12 (the first release that supports Wagtail 7).

Package requirements

Package Minimum version
Python 3.9
Django 4.2
Wagtail 4.0
django-tinymce 5.0 (bundles TinyMCE 7)
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-table

From Git — latest (0.3.x, Wagtail 4 – 7, TinyMCE 7)

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

Pin to a specific release tag for reproducible installs:

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

In a requirements.txt file (PEP 508):

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

Legacy version (TinyMCE 6, Wagtail 4 – 5, django-tinymce ≥ 3.5)

If your project requires TinyMCE 6, install the v0.1.0 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-table @ 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"]

Applying your site's table styles to new tables

There are two independent concerns here:

  1. Frontend rendering — making the published page apply your site's CSS to new tables.
  2. Editor preview — making the TinyMCE iframe look the same as the frontend.

1. Frontend rendering

TinyMCETableBlock stores a plain <table> element. All tables rendered inside <section class="block-table"> can be targeted in your site's stylesheet without needing to add any class to the table itself:

/* targets every TinyMCE table block on the published page */
section.block-table table {
    border-collapse: collapse;
    width: 100%;
}

section.block-table table td,
section.block-table table th {
    border: 1px solid #dbdbdb;
    padding: 0.5em 0.75em;
    vertical-align: top;
}

This approach is CSS-framework-agnostic and works regardless of what class (if any) the table carries.

If your framework uses a CSS class hook (e.g. Bulma's .table.is-bordered), stamp every new table with that class via table_default_attributes:

class MyTableBlock(TinyMCETableBlock):
    custom_mce_config = {
        **TinyMCETableBlock.custom_mce_config,
        # Bulma example — adjust to your framework's class names
        "table_default_attributes": {"class": "table is-bordered"},
    }

Note on CSS specificity: a class selector (.table td, specificity 0,1,1) overrides an element selector (table td, specificity 0,0,2). If your site has a generic table td rule and a .table td rule, the class rule wins for tables that carry the class. Make sure the class-scoped rules include everything you need.

2. Editor preview

TinyMCE renders inside an iframe isolated from your host stylesheet. Use content_css to load your compiled stylesheet into the iframe so the editing experience matches the frontend:

class MyTableBlock(TinyMCETableBlock):
    custom_mce_config = {
        **TinyMCETableBlock.custom_mce_config,
        # Path served by Django's staticfiles — adjust to match your project.
        "content_css": "/static/css/your-app.css",
    }

If you only need a small number of rules you can inline them with content_style instead — no extra HTTP request:

class MyTableBlock(TinyMCETableBlock):
    custom_mce_config = {
        **TinyMCETableBlock.custom_mce_config,
        "content_style": (
            "table { border-collapse: collapse; width: 100%; }"
            "td, th { border: 1px solid #dbdbdb; padding: 0.5em 0.75em; }"
        ),
    }

Tip: content_css and content_style can be combined — TinyMCE applies both.

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-table[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

See CHANGELOG.md for the full release history.


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.3.0.tar.gz (30.4 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.3.0-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: wagtail_tinymce_table-0.3.0.tar.gz
  • Upload date:
  • Size: 30.4 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.3.0.tar.gz
Algorithm Hash digest
SHA256 63eb03efeb437ec7af01bf25b3593690658f2af1c745dad96188f4b68ade7dd4
MD5 fa37315c212f2e418e7d31839b590e40
BLAKE2b-256 562b7806232fa18c15f3d55edbda88e7b88b3e2cf90aaf50703c1fa155a2b30f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wagtail_tinymce_table-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dde8b58094ad77d43065cdc833f906c6bebd9e95bfe7d2d6c85ddc81b0e933dc
MD5 52d62601821c4cf7857d9be1e822036a
BLAKE2b-256 4f969add283c9fb323c7a8b38a041c47a76d98f1950ae3928e3d07fc51421156

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