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 splitting —
colspan/rowspanvia the built-in Merge Cells and Split Cell buttons. wagtail-localizeintegration —TinyMCETableBlockimplementsget_translatable_segmentsandrestore_translated_segmentsso 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 passmenubar_options/toolbar_optionsper block instance.
Compatibility
Version matrix
| wagtail-tinymce-table | Wagtail | Django | Python | wagtail-localize |
|---|---|---|---|---|
| 0.4.x (current) | 4.0 – 8.x | 4.2 LTS, 5.2 LTS, 6.0 | 3.9 – 3.14 ¹ | ≥ 1.5 ² |
| 0.3.x | 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.4.x, Wagtail 4 – 8, 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:
- Frontend rendering — making the published page apply your site's CSS to new tables.
- 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 generictable tdrule and a.table tdrule, 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_cssandcontent_stylecan 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_texthelper (simple and compound cell paths)TinyMCETableBlockconfiguration (allowed tags, toolbar, TinyMCEsetupcallback)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)WagtailTinyMCEwidget (config merging and overrides)WagtailTinyMCEAdapter(telepath registration)
Changelog
See CHANGELOG.md for the full release history.
Licence
MIT
Metadata
Release files for wagtail-tinymce-table 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| wagtail_tinymce_table-0.4.0.tar.gz | 30.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wagtail_tinymce_table-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.3 kB
Release files / wagtail_tinymce_table-0.4.0.tar.gz
| Download URL | wagtail_tinymce_table-0.4.0.tar.gz |
|---|---|
| Size | 30.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5d7dbc83bc2590193b0176f19c3ba83168d7e2723601398ffc55d4a8e0db4e24
|
|
BLAKE2b-256 checksum How to use checksums |
9f1966c415a277367b8f32987d177bcd1c2084202c1cd50bebcb0a759d2a9bba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / wagtail_tinymce_table-0.4.0-py3-none-any.whl
| Download URL | wagtail_tinymce_table-0.4.0-py3-none-any.whl |
|---|---|
| Size | 15.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
aedba02acab657ef74f6621ca25df811561f0f718d1ce636d52f6ab8a67e14f4
|
|
BLAKE2b-256 checksum How to use checksums |
005207a3431abf8af45a54b7b5c9edcfdf708099a7b0072359bec05eafa4c8ce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|