Skip to content

Instantly share code, notes, and snippets.

@urlsangel
Last active July 14, 2022 08:31
Show Gist options
  • Save urlsangel/d2b627ce6c237d8ee90d528ad7d83451 to your computer and use it in GitHub Desktop.
Save urlsangel/d2b627ce6c237d8ee90d528ad7d83451 to your computer and use it in GitHub Desktop.
Adding an advanced table block to Wagtail
// These should match and implement CSS values as per the colour_choices in helpers.py
@import '../abstract/colors';
.advanced-table {
&__wrapper {
table {
background-color: $white;
}
}
&__cell {
color: $white;
&--red {
background-color: $red !important;
}
&--orange {
background-color: $orange !important;
color: $black;
}
&--yellow {
background-color: $yellow !important;
color: $black;
}
&--green {
background-color: $green !important;
color: $black;
}
&--aubergine {
background-color: $aubergine !important;
}
&--violet {
background-color: $violet !important;
}
&--purple {
background-color: $purple !important;
}
&--blue {
background-color: $blue !important;
}
&--turquoise {
background-color: $turquoise !important;
}
}
}
.mce-item-table {
border-collapse: collapse;
width: 100%;
border: 1px solid !important;
th, td {
border: 1px solid !important;
}
}
// ...
@import 'advanced-table';
@import 'advanced-table';
{% for item in nodes %}
<div class="advanced-table__wrapper">
{{ item }}
</div>
{% endfor %}
# Add to installed apps
# Note: "richtext" is the name of this app,
# so you'll need to create that folder with the files referenced in this gist
# such as `helpers.py`, `constants.py`, `wagtail_hooks.py`, etc.
INSTALLED_APPS = [
# ...
"wagtailtinymce",
"richtext",
]
# Add extra rich text editor
WAGTAILADMIN_RICH_TEXT_EDITORS = {
# ...
"secondary": {
"WIDGET": "richtext.tinymce.CustomTinyMCERichTextArea",
},
}
from django.utils.translation import gettext_lazy as _
from wagtail.core.blocks import StructBlock, RichTextBlock
from richtext.helpers import get_nodes_for_block
class AdvancedTableBlock(StructBlock):
"""A rich text field that implements TinyMCE for advanced table editing"""
class Meta:
template = "advanced_table.html"
label = _("Advanced table")
icon = "table"
content = RichTextBlock(
required=True,
editor="secondary",
)
# modify the context to clean the db data and return a list of table nodes
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context=parent_context)
content = value.get("content")
nodes = get_nodes_for_block(content)
context.update(
nodes=nodes,
)
return context
TABLE_ATTRS = {
"style": True,
}
TR_ATTRS = {
"class": True,
"style": True,
}
TH_TD_ATTRS = {
"class": True,
"colspan": True,
"rowspan": True,
"style": True,
"scope": True,
}
ALLOWED_TAGS = [
"table",
"thead",
"tbody",
"tfoot",
"tr",
"th",
"td",
"b",
"strong",
"br",
]
ALLOWED_ROOT_NODES = [
"table",
]
ALLOWED_STYLE_PROPERTIES_BLOCK = [
"text-align",
"vertical-align",
]
ALLOWED_STYLE_PROPERTIES_DB = ALLOWED_STYLE_PROPERTIES_BLOCK + [
"height",
"width",
]
ALLOWED_CLASSNAMES = [
"advanced-table__cell",
]
from wagtail.core.blocks import StreamBlock
from wagtail.core.fields import StreamField
from .blocks import AdvancedTableBlock
def AdvancedTableStreamField(blank=False, default="", help_text=""):
required = not blank
return StreamField(
StreamBlock(
[
("advanced_table", AdvancedTableBlock()),
],
required=required,
),
blank=blank,
default=default,
help_text=help_text,
)
import bleach
from bleach.css_sanitizer import CSSSanitizer
from bs4 import BeautifulSoup
from django.utils.safestring import mark_safe
from richtext.constants import (
TABLE_ATTRS,
TR_ATTRS,
TH_TD_ATTRS,
ALLOWED_TAGS,
ALLOWED_ROOT_NODES,
ALLOWED_STYLE_PROPERTIES_BLOCK,
ALLOWED_STYLE_PROPERTIES_DB,
ALLOWED_CLASSNAMES,
)
block_css_sanitizer = CSSSanitizer(
allowed_css_properties=ALLOWED_STYLE_PROPERTIES_BLOCK,
)
def colour_choices():
return (
("red", _("Red")),
("orange", _("Orange")),
("yellow", _("Yellow")),
("green", _("Green")),
("aubergine", _("Aubergine")),
("violet", _("Violet")),
("purple", _("Purple")),
("blue", _("Blue")),
("turquoise", _("Turquoise")),
)
db_css_sanitizer = CSSSanitizer(
allowed_css_properties=ALLOWED_STYLE_PROPERTIES_DB,
)
def get_style_formats():
colours = colour_choices()
formats = []
for item in colours:
element_classname = "advanced-table__cell"
modifier_classname = f"advanced-table__cell--{item[0]}"
title = str(item[1])
style = {
"title": title,
"selector": "tr,th,td",
"classes": [element_classname, modifier_classname],
}
formats.append(style)
return formats
def get_allowed_attrs():
attrs = set(
list(TABLE_ATTRS.keys()) + list(TR_ATTRS.keys()) + list(TH_TD_ATTRS.keys())
)
return list(attrs)
def clean_data_for_db(data):
# convert data to bs object
data = BeautifulSoup(data, "html.parser")
# remove any disallowed root nodes
for child in data.contents:
if child.name and child.name.lower() not in ALLOWED_ROOT_NODES:
child.decompose()
# remove any disallowed classes
for child in data.find_all():
try:
classes = child["class"]
child["class"] = [
x for x in classes if x.startswith(tuple(ALLOWED_CLASSNAMES))
]
except KeyError:
pass
# remove any disallowed style properties
data = mark_safe(
bleach.clean(
str(data),
tags=ALLOWED_TAGS,
attributes=get_allowed_attrs(),
css_sanitizer=db_css_sanitizer,
strip_comments=True,
strip=True,
)
)
return data
def get_nodes_for_block(data):
# leave only classes on elements
data = mark_safe(
bleach.clean(
str(data),
tags=ALLOWED_TAGS,
attributes=get_allowed_attrs(),
css_sanitizer=block_css_sanitizer,
strip_comments=True,
strip=True,
)
)
# convert data to bs object
data = BeautifulSoup(data, "html.parser")
# split nodes into list
nodes = [
mark_safe(x.prettify())
for x in data.contents
if x.name and x.name.lower() in ALLOWED_ROOT_NODES
]
return nodes
...
wagtailtinymce = {git = "https://github.com/Junatum/wagtailtinymce.git"}
bleach = {extras = ["css"], version = "*"}
from wagtail.core.rich_text import features
from wagtailtinymce.rich_text import TinyMCERichTextArea
from richtext.helpers import get_style_formats, clean_data_for_db
class CustomTinyMCERichTextArea(TinyMCERichTextArea):
@classmethod
def getDefaultArgs(cls):
args = super().getDefaultArgs()
# config for tinymce button bar
args["buttons"] = [
[
["undo", "redo"],
["styleselect"],
["bold"],
["table"],
["alignleft", "aligncenter", "alignright"],
["pastetext"],
]
]
# config for tinymce init items
args["passthru_init_keys"] = {
"selector": "textarea",
"style_formats": get_style_formats(),
"style_formats_merge": False,
"style_formats_autohide": True,
"content_css": "/static/css/admin.css",
"table_toolbar": """
tabledelete |
tableinsertrowbefore tableinsertrowafter tabledeleterow |
tableinsertcolbefore tableinsertcolafter tabledeletecol
""",
"table_appearance_options": False,
"table_advtab": False,
"table_cell_advtab": False,
}
return args
def __init__(self, attrs=None, **kwargs):
super().__init__(attrs, **kwargs)
# add rules added by wagtail hooks, as they aren't picked up otherwise
rules = []
for rule in features.converter_rules_by_converter["editorhtml"].values():
for item in rule:
rules.append(item)
self.converter.converter_rules = rules
def value_from_datadict(self, data, files, name):
original_value = super(TinyMCERichTextArea, self).value_from_datadict(
data, files, name
)
if original_value is None:
return None
# convert to database format
db_format_data = self.converter.to_database_format(original_value)
# clean html to remove unwanted attrs and styles
cleaned_data = clean_data_for_db(db_format_data)
return cleaned_data
from wagtail.admin.rich_text import HalloPlugin
from wagtail.admin.rich_text.converters.editor_html import WhitelistRule
from wagtail.core import hooks
from wagtail.core.whitelist import allow_without_attributes, attribute_rule
from richtext.constants import TABLE_ATTRS, TR_ATTRS, TH_TD_ATTRS
@hooks.register("register_rich_text_features")
def register_embed_feature(features):
features.register_editor_plugin(
"editorhtml",
"table",
HalloPlugin(
name="tinymcetable",
),
)
features.register_editor_plugin(
"editorhtml",
"thead",
HalloPlugin(
name="tinymcethead",
),
)
features.register_editor_plugin(
"editorhtml",
"tbody",
HalloPlugin(
name="tinymcetbody",
),
)
features.register_editor_plugin(
"editorhtml",
"tfoot",
HalloPlugin(
name="tinymcetfoot",
),
)
features.register_editor_plugin(
"editorhtml",
"tr",
HalloPlugin(
name="tinymcetr",
),
)
features.register_editor_plugin(
"editorhtml",
"th",
HalloPlugin(
name="tinymceth",
),
)
features.register_editor_plugin(
"editorhtml",
"td",
HalloPlugin(
name="tinymcetd",
),
)
features.register_converter_rule(
"editorhtml",
"table",
[
WhitelistRule("table", attribute_rule(TABLE_ATTRS)),
],
)
features.register_converter_rule(
"editorhtml",
"thead",
[
WhitelistRule("thead", allow_without_attributes),
],
)
features.register_converter_rule(
"editorhtml",
"tbody",
[
WhitelistRule("tbody", allow_without_attributes),
],
)
features.register_converter_rule(
"editorhtml",
"tfoot",
[
WhitelistRule("tfoot", allow_without_attributes),
],
)
features.register_converter_rule(
"editorhtml",
"tr",
[
WhitelistRule("tr", attribute_rule(TR_ATTRS)),
],
)
features.register_converter_rule(
"editorhtml",
"th",
[
WhitelistRule("th", attribute_rule(TH_TD_ATTRS)),
],
)
features.register_converter_rule(
"editorhtml",
"td",
[
WhitelistRule("td", attribute_rule(TH_TD_ATTRS)),
],
)
@coredumperror
Copy link

A couple notes for future viewers of this code:

richtext is the name of this app. So you'll want to update all such references to the name of the app that you actually put this code into.

If you're using Wagtail 3, you'll need to use the wagtail-hallo app, since Wagtail has removed Hallo.js from its main codebase, and this code relies on that functionality.

If you're using Django 4.x (and possibly also 3.x, though I'm not sure), you'll need to use a newer fork of the wagtailtinymce package, as it is not compatible with Django 4. I'm personally using this line in my requirements.txt to bring in a working fork:

git+https://github.com/Junatum/wagtailtinymce.git@custom-1.2#egg=wagtailtinymce

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment