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 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.
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 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 @ 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[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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wagtail_tinymce_table-0.2.8.tar.gz.
File metadata
- Download URL: wagtail_tinymce_table-0.2.8.tar.gz
- Upload date:
- Size: 29.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
914824e61ad861a1dc11feff59387eade2377e3089382593e8c4b975e5dd6158
|
|
| MD5 |
de410401c33bafdd6100bfb84feb4f57
|
|
| BLAKE2b-256 |
07723d01a298209914803d03fb7bd7289203b2c03cf73559d3289c36ab431e09
|
File details
Details for the file wagtail_tinymce_table-0.2.8-py3-none-any.whl.
File metadata
- Download URL: wagtail_tinymce_table-0.2.8-py3-none-any.whl
- Upload date:
- Size: 15.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e38343537c8162f969cdb728240ba31fd82341d16f968ad42a01e0b2ac271bd4
|
|
| MD5 |
98a7d538cbc146df6dc0fb0b49bdd08a
|
|
| BLAKE2b-256 |
8d1bc1b20faeead7d5d809ab628de239c59309f1f57a27152db3bb88f808b0c4
|