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 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:

  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[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.2.8.tar.gz (29.0 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.8-py3-none-any.whl (15.0 kB view details)

Uploaded Python 3

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

Hashes for wagtail_tinymce_table-0.2.8.tar.gz
Algorithm Hash digest
SHA256 914824e61ad861a1dc11feff59387eade2377e3089382593e8c4b975e5dd6158
MD5 de410401c33bafdd6100bfb84feb4f57
BLAKE2b-256 07723d01a298209914803d03fb7bd7289203b2c03cf73559d3289c36ab431e09

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wagtail_tinymce_table-0.2.8-py3-none-any.whl
Algorithm Hash digest
SHA256 e38343537c8162f969cdb728240ba31fd82341d16f968ad42a01e0b2ac271bd4
MD5 98a7d538cbc146df6dc0fb0b49bdd08a
BLAKE2b-256 8d1bc1b20faeead7d5d809ab628de239c59309f1f57a27152db3bb88f808b0c4

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