Last active
July 14, 2022 08:31
-
-
Save urlsangel/d2b627ce6c237d8ee90d528ad7d83451 to your computer and use it in GitHub Desktop.
Adding an advanced table block to Wagtail
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ... | |
@import 'advanced-table'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import 'advanced-table'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% for item in nodes %} | |
<div class="advanced-table__wrapper"> | |
{{ item }} | |
</div> | |
{% endfor %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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", | |
}, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... | |
wagtailtinymce = {git = "https://github.com/Junatum/wagtailtinymce.git"} | |
bleach = {extras = ["css"], version = "*"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)), | |
], | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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: