Skip to content

Commit

Permalink
feat: add themeable table to the TinyMCE Editor (#17)
Browse files Browse the repository at this point in the history
This:
* Replaces static third-party libraries with the ones from the edx-platform.
* Adds the table plugin to the studio TinyMCE editor.
* Adds a way to define custom CSS classes for tables through settings. They 
  can be used to apply styles HTML tables through the comprehensive theme.
* Makes `bleaching.py` compatible with `CSSSanitizer` from `bleach==5.0.0`.
  • Loading branch information
tecoholic authored Jul 8, 2022
1 parent c0e27af commit 8e39d43
Show file tree
Hide file tree
Showing 49 changed files with 102 additions and 38,425 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ Note that as of version 1.0.0, Python 2.7 is no longer supported. The current mi

To enable this block, add `"html5"` and `"excluded_html5"` to the course's advanced module list. The options `Text` and `Exclusion` will appear in the advanced components.

## Configuration

The `table`s added to the content in the WYSIWYG editor can be styled according the theming requirements of the deployment by adding custom CSS classes to them.
Add the following to your XBLOCK_SETTINGS part of the CMS/Studio confguration:

```
XBLOCK_SETTINGS = {
"html5": {
"table_custom_classes": ["your-list", "of-css", "classes"]
}
}
```
These classes will be available in the "General" tab of "Table Properties" dialog, under "Classes".

## Development
If you're willing to develop on this repo, you need to be familiar with different technologies and the repos'
dependencies. However, to make things easier to setup and to manage, there're bunch of make commands that you can use
Expand Down
47 changes: 39 additions & 8 deletions html_xblock/bleaching.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
"""
import bleach

try:
from bleach.css_sanitizer import CSSSanitizer
except (ImportError, ModuleNotFoundError):
# NOTE:
# The bleach library changes the way CSS Styles are cleaned in
# version 5.0.0. Since the edx-platform uses version 4.1.0 in
# Maple and Nutmeg, this import is handled within a try block.
# This try block CAN BE REMOVED after Olive
CSSSanitizer = None


class SanitizedText: # pylint: disable=too-few-public-methods
"""
Expand Down Expand Up @@ -31,11 +41,25 @@ def get_cleaner(self):
It does so by redefining the safe values we're currently using and
considering safe in the platform.
"""
cleaner = bleach.Cleaner(
tags=self._get_allowed_tags(),
attributes=self._get_allowed_attributes(),
styles=self._get_allowed_styles()
)
if CSSSanitizer:
cleaner = bleach.Cleaner(
tags=self._get_allowed_tags(),
attributes=self._get_allowed_attributes(),
css_sanitizer=CSSSanitizer(
allowed_css_properties=self._get_allowed_styles()
)
)
else:
# NOTE: This is maintaining backward compatibility with bleach 4.1.0
# used in Maple and Nutmeg release of edx-platform. This can be removed
# for Olive release which uses bleach 5.0.0

# pylint: disable-next=unexpected-keyword-arg
cleaner = bleach.Cleaner(
tags=self._get_allowed_tags(),
attributes=self._get_allowed_attributes(),
styles=self._get_allowed_styles()
)

return cleaner

Expand Down Expand Up @@ -66,7 +90,12 @@ def _get_allowed_tags(self):
'ol',
'ul',
'p',
'pre'
'pre',
'table',
'tbody',
'th',
'tr',
'td'
]

if not self.strict:
Expand All @@ -89,6 +118,7 @@ def _get_allowed_attributes(self):
'pre': ['class'],
'span': ['style'],
'ul': [],
'table': ['class'],
}

if not self.strict:
Expand Down Expand Up @@ -130,8 +160,9 @@ def _determine_values(self, other):
self_value = self.adulterated_value
other_value = other.adulterated_value
else:
raise TypeError('Unsupported operation between instances of \'{}\' and \'{}\''.format(
type(self).__name__, type(other).__name__))
raise TypeError(
f'Unsupported operation between instances of \'{type(self).__name__}\' and \'{type(other).__name__}\''
)

return self_value, other_value

Expand Down
48 changes: 32 additions & 16 deletions html_xblock/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging

import pkg_resources
from django.conf import settings
from xblock.completable import XBlockCompletionMode
from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String
Expand All @@ -17,6 +18,7 @@
xblock_loader = ResourceLoader(__name__) # pylint: disable=invalid-name


@XBlock.wants('settings')
class HTML5XBlock(StudioEditableXBlockMixin, XBlock):
"""
This XBlock will provide an HTML WYSIWYG interface in Studio to be rendered in LMS.
Expand All @@ -28,7 +30,7 @@ class HTML5XBlock(StudioEditableXBlockMixin, XBlock):
scope=Scope.settings,
default=_('Text')
)
data = String(help=_('Html contents to display for this module'), default=u'', scope=Scope.content)
data = String(help=_('Html contents to display for this module'), default='', scope=Scope.content)
allow_javascript = Boolean(
display_name=_('Allow JavaScript execution'),
help=_('Whether JavaScript should be allowed or not in this module'),
Expand All @@ -49,6 +51,17 @@ class HTML5XBlock(StudioEditableXBlockMixin, XBlock):
scope=Scope.settings
)
editable_fields = ('display_name', 'editor', 'allow_javascript')
block_settings_key = "html5"

def get_settings(self):
"""
Get the XBlock settings bucket via the SettingsService.
"""
settings_service = self.runtime.service(self, 'settings')
if settings_service:
return settings_service.get_settings_bucket(self)

return {}

@staticmethod
def resource_string(path):
Expand Down Expand Up @@ -89,8 +102,11 @@ def studio_view(self, context=None): # pylint: disable=unused-argument

js_data = {
'editor': self.editor,
'skin_url': self.runtime.local_resource_url(self, 'public/skin'),
'external_plugins': self.get_editor_plugins()
'script_url': settings.STATIC_URL + 'js/vendor/tinymce/js/tinymce/tinymce.full.min.js',
'skin_url': settings.STATIC_URL + 'js/vendor/tinymce/js/tinymce/skins/ui/studio-tmce5',
'codemirror_path': settings.STATIC_URL + 'js/vendor/',
'external_plugins': self.get_editor_plugins(),
'table_custom_classes': self.get_settings().get("table_custom_classes", [])
}
frag.initialize_js('HTML5XBlock', js_data)

Expand Down Expand Up @@ -144,38 +160,38 @@ def add_stylesheets(self, frag):
frag.add_css(self.resource_string('static/css/html.css'))

if self.editor == 'raw':
frag.add_css(self.resource_string('public/plugins/codemirror/codemirror-4.8/lib/codemirror.css'))
frag.add_css_url(settings.STATIC_URL + 'js/vendor/CodeMirror/codemirror.css')

def add_scripts(self, frag):
"""
A helper method to add all necessary scripts to the fragment.
:param frag: The fragment that will hold the scripts.
"""
frag.add_javascript(self.resource_string('static/js/tinymce/tinymce.min.js'))
frag.add_javascript(self.resource_string('static/js/tinymce/themes/modern/theme.min.js'))
frag.add_javascript_url(settings.STATIC_URL + 'js/vendor/tinymce/js/tinymce/tinymce.full.min.js')
frag.add_javascript_url(settings.STATIC_URL + 'js/vendor/tinymce/js/tinymce/themes/silver/theme.min.js')
frag.add_javascript(self.resource_string('static/js/html.js'))
frag.add_javascript(loader.load_unicode('public/studio_edit.js'))

if self.editor == 'raw':
code_mirror_dir = 'public/plugins/codemirror/codemirror-4.8/'
frag.add_javascript(self.resource_string(code_mirror_dir + 'lib/codemirror.js'))
frag.add_javascript(self.resource_string(code_mirror_dir + 'mode/xml/xml.js'))
frag.add_javascript_url(settings.STATIC_URL + 'js/vendor/CodeMirror/codemirror.js')
frag.add_javascript_url(settings.STATIC_URL + 'js/vendor/CodeMirror/addons/xml.js')

def get_editor_plugins(self):
@staticmethod
def get_editor_plugins():
"""
This method will generate a list of external plugins urls to be used in TinyMCE editor.
These plugins should live in `public` directory for us to generate URLs for.
const PLUGINS_DIR = "/resource/html5/public/plugins/";
const EXTERNAL_PLUGINS = PLUGINS.map(function(p) { return PLUGINS_DIR + p + "/plugin.min.js" });
:return: A list of URLs
"""
plugin_path = 'public/plugins/{plugin}/plugin.min.js'
plugins = ['codesample', 'image', 'link', 'lists', 'textcolor', 'codemirror']
plugin_path = 'plugins/{plugin}/plugin.min.js'
plugins = ['codesample', 'image', 'link', 'lists', 'codemirror', 'table']

return {
plugin: self.runtime.local_resource_url(self, plugin_path.format(plugin=plugin)) for plugin in plugins
plugin: (
settings.STATIC_URL + "js/vendor/tinymce/js/tinymce/" +
plugin_path.format(plugin=plugin)
) for plugin in plugins
}

def substitute_keywords(self):
Expand Down

This file was deleted.

Loading

0 comments on commit 8e39d43

Please sign in to comment.