Add more built-in rich text formats (#5141)

pull/5151/head
Md. Arifin Ibne Matin 2019-03-15 01:39:18 +01:00 zatwierdzone przez Thibaud Colas
rodzic a700e1352c
commit edfd9afc1d
15 zmienionych plików z 1377 dodań i 1856 usunięć

Wyświetl plik

@ -19,6 +19,7 @@ Changelog
* Add a Django setting `TAG_LIMIT` to limit number of tags that can be added to any taggit model (Mani)
* Added instructions on how to generate urls for `ModelAdmin` to documentation (LB (Ben Johnston), Andy Babic)
* Added option to specify a fallback URL on `{% pageurl %}` (Arthur Holzner)
* Add support for more rich text formats, disabled by default: `blockquote`, `superscript`, `subscript`, `strikethrough`, `code` (Md Arifin Ibne Matin)
* Fix: Set `SERVER_PORT` to 443 in `Page.dummy_request()` for HTTPS sites (Sergey Fedoseev)
* Fix: Include port number in `Host` header of `Page.dummy_request()` (Sergey Fedoseev)
* Fix: Validation error messages in `InlinePanel` no longer count towards `max_num` when disabling the 'add' button (Todd Dembrey, Thibaud Colas)

Wyświetl plik

@ -356,6 +356,7 @@ Contributors
* Esper Kuijs
* Damian Grinwis
* Wesley van Lee
* Md Arifin Ibne Matin
Translators
===========

Wyświetl plik

@ -103,3 +103,10 @@ $draftail-editor-font-family: $font-serif;
.title .Draftail-Editor .public-DraftEditorPlaceholder-root {
font-size: 2em;
}
.Draftail-block--blockquote {
border-left: 0.25em solid $color-grey-3;
color: $color-grey-2;
margin: 1em 0;
padding: 1em 2em;
}

Wyświetl plik

@ -9,7 +9,7 @@ Plugins come in three types:
* Blocks – To indicate the structure of the content, eg. ``blockquote``, ``ol``.
* Entities – To enter additional data/metadata, eg. ``link`` (with a URL), ``image`` (with a file).
All of these plugins are created with a similar baseline, which we can demonstrate with one of the simplest examples – a custom feature for an inline style of ``strikethrough``. Place the following in a ``wagtail_hooks.py`` file in any installed app:
All of these plugins are created with a similar baseline, which we can demonstrate with one of the simplest examples – a custom feature for an inline style of ``mark``. Place the following in a ``wagtail_hooks.py`` file in any installed app:
.. code-block:: python
@ -19,21 +19,21 @@ All of these plugins are created with a similar baseline, which we can demonstra
# 1. Use the register_rich_text_features hook.
@hooks.register('register_rich_text_features')
def register_strikethrough_feature(features):
def register_mark_feature(features):
"""
Registering the `strikethrough` feature, which uses the `STRIKETHROUGH` Draft.js inline style type,
and is stored as HTML with an `<s>` tag.
Registering the `mark` feature, which uses the `MARK` Draft.js inline style type,
and is stored as HTML with a `<mark>` tag.
"""
feature_name = 'strikethrough'
type_ = 'STRIKETHROUGH'
tag = 's'
feature_name = 'mark'
type_ = 'MARK'
tag = 'mark'
# 2. Configure how Draftail handles the feature in its toolbar.
control = {
'type': type_,
'label': 'S',
'description': 'Strikethrough',
# This isnt even required – Draftail has predefined styles for STRIKETHROUGH.
'label': '',
'description': 'Mark',
# This isnt even required – Draftail has predefined styles for MARK.
# 'style': {'textDecoration': 'line-through'},
}
@ -53,7 +53,7 @@ All of these plugins are created with a similar baseline, which we can demonstra
# 6. (optional) Add the feature to the default features list to make it available
# on rich text fields that do not specify an explicit 'features' list
features.default_features.append('strikethrough')
features.default_features.append('mark')
These steps will always be the same for all Draftail plugins. The important parts are to:
@ -72,7 +72,7 @@ Creating new inline styles
In addition to the initial example, inline styles take a ``style`` property to define what CSS rules will be applied to text in the editor. Be sure to read the `Draftail documentation <https://www.draftail.org/docs/formatting-options>`_ on inline styles.
Finally, the DB to/from conversion uses an ``InlineStyleElementHandler`` to map from a given tag (``<s>`` in the example above) to a Draftail type, and the inverse mapping is done with `Draft.js exporter configuration <https://github.com/springload/draftjs_exporter>`_ of the ``style_map``.
Finally, the DB to/from conversion uses an ``InlineStyleElementHandler`` to map from a given tag (``<mark>`` in the example above) to a Draftail type, and the inverse mapping is done with `Draft.js exporter configuration <https://github.com/springload/draftjs_exporter>`_ of the ``style_map``.
Creating new blocks
~~~~~~~~~~~~~~~~~~~
@ -84,30 +84,29 @@ Blocks are nearly as simple as inline styles:
from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler
@hooks.register('register_rich_text_features')
def register_blockquote_feature(features):
def register_help_text_feature(features):
"""
Registering the `blockquote` feature, which uses the `blockquote` Draft.js block type,
and is stored as HTML with a `<blockquote>` tag.
Registering the `help-text` feature, which uses the `help-text` Draft.js block type,
and is stored as HTML with a `<div class="help-text">` tag.
"""
feature_name = 'blockquote'
type_ = 'blockquote'
tag = 'blockquote'
feature_name = 'help-text'
type_ = 'help-text'
control = {
'type': type_,
'label': '',
'description': 'Blockquote',
'label': '?',
'description': 'Help text',
# Optionally, we can tell Draftail what element to use when displaying those blocks in the editor.
'element': 'blockquote',
'element': 'div',
}
features.register_editor_plugin(
'draftail', feature_name, draftail_features.BlockFeature(control)
'draftail', feature_name, draftail_features.BlockFeature(control, css={'all': ['help-text.css']})
)
features.register_converter_rule('contentstate', feature_name, {
'from_database_format': {tag: BlockElementHandler(type_)},
'to_database_format': {'block_map': {type_: tag}},
'from_database_format': {'div.help-text': BlockElementHandler(type_)},
'to_database_format': {'block_map': {type_: {'element': 'div', 'props': {'class': 'help-text'}}}},
})
Here are the main differences:
@ -116,7 +115,7 @@ Here are the main differences:
* We register the plugin with ``BlockFeature``.
* We set up the conversion with ``BlockElementHandler`` and ``block_map``.
Optionally, we can also define styles for the blocks with the ``Draftail-block--blockquote`` (``Draftail-block--<block type>``) CSS class.
Optionally, we can also define styles for the blocks with the ``Draftail-block--help-text`` (``Draftail-block--<block type>``) CSS class.
Thats it! The extra complexity is that you may need to write CSS to style the blocks in the editor.

Wyświetl plik

@ -86,6 +86,12 @@ The feature identifiers provided on a default Wagtail installation are as follow
* ``embed`` - embedded media (see :ref:`embedded_content`)
We have few additional feature identifiers as well. They are not enabled by default, but you can use them in your list of identifers. These are as follows:
* ``code`` - inline code
* ``superscript``, ``subscript``, ``strikethrough`` - text formatting
* ``blockquote`` - blockquote
The process for creating new features is described in the following pages:
* :doc:`./rich_text_internals`

Wyświetl plik

@ -29,6 +29,7 @@ Other features
* Add a Django setting ``TAG_LIMIT`` to limit number of tags that can be added to any taggit model (Mani)
* Added instructions on how to generate urls for ``ModelAdmin`` to documentation (LB (Ben Johnston), Andy Babic)
* Added option to specify a fallback URL on ``{% pageurl %}`` (Arthur Holzner)
* Add support for more rich text formats, disabled by default: ``blockquote``, ``superscript``, ``subscript``, ``strikethrough``, ``code`` (Md Arifin Ibne Matin)
Bug fixes
@ -95,3 +96,17 @@ should become:
.. code-block:: html+django
{% include "wagtailadmin/shared/ajax_pagination_nav.html" with items=page_obj %}
New rich text formats
~~~~~~~~~~~~~~~~~~~~~
Wagtail now has built-in support for new rich text formats, disabled by default:
* ``blockquote``, using the ``blockquote`` Draft.js block type, saved as a ``<blockquote>`` tag.
* ``superscript``, using the ``SUPERSCRIPT`` Draft.js inline style, saved as a ``<sup>`` tag.
* ``subscript``, using the ``SUBSCRIPT`` Draft.js inline style, saved as a ``<sub>`` tag.
* ``strikethrough``, using the ``STRIKETHROUGH`` Draft.js inline style, saved as a ``<s>`` tag.
* ``code``, using the ``CODE`` Draft.js inline style, saved as a ``<code>`` tag.
Projects already using those exact Draft.js type and HTML tag combinations can safely replace their feature definitions with the new built-ins. Projects that use the same feature identifier can keep their existing feature definitions as overrides. Finally, if the Draft.js types / HTML tags are used but with a different combination, do not enable the new feature definitions to avoid conflicts in storage or editor behavior.

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -75,7 +75,10 @@ $icons: (
'horizontalrule': '\2014',
'chain-broken': '\e900',
'table': '\f0ce',
'logout': '\e901'
'logout': '\e901',
'superscript': '\f12b',
'subscript': '\f12c',
'strikethrough': '\f0cc'
);
$icons-after: (

Wyświetl plik

@ -184,7 +184,7 @@ class TestHalloRichText(BaseRichTextEditHandlerTestCase, WagtailTestUtils):
@override_settings(WAGTAILADMIN_RICH_TEXT_EDITORS={
'default': {
'WIDGET': 'wagtail.admin.rich_text.DraftailRichTextArea',
'OPTIONS': {'features': ['h2', 'blockquote']}
'OPTIONS': {'features': ['h2', 'quotation']}
},
})
class TestDraftailFeatureMedia(BaseRichTextEditHandlerTestCase, WagtailTestUtils):
@ -205,8 +205,8 @@ class TestDraftailFeatureMedia(BaseRichTextEditHandlerTestCase, WagtailTestUtils
))
self.assertContains(response, 'wagtailadmin/js/draftail.js')
self.assertContains(response, 'testapp/js/draftail-blockquote.js')
self.assertContains(response, 'testapp/css/draftail-blockquote.css')
self.assertContains(response, 'testapp/js/draftail-quotation.js')
self.assertContains(response, 'testapp/css/draftail-quotation.css')
def test_feature_media_on_rich_text_block(self):
response = self.client.get(reverse(
@ -214,8 +214,8 @@ class TestDraftailFeatureMedia(BaseRichTextEditHandlerTestCase, WagtailTestUtils
))
self.assertContains(response, 'wagtailadmin/js/draftail.js')
self.assertContains(response, 'testapp/js/draftail-blockquote.js')
self.assertContains(response, 'testapp/css/draftail-blockquote.css')
self.assertContains(response, 'testapp/js/draftail-quotation.js')
self.assertContains(response, 'testapp/css/draftail-quotation.css')
@override_settings(WAGTAILADMIN_RICH_TEXT_EDITORS={
@ -392,33 +392,33 @@ class TestHalloJsWithFeaturesKwarg(BaseRichTextEditHandlerTestCase, WagtailTestU
self.assertEqual(response.status_code, 200)
# Check that the custom plugin options are being passed in the hallo initialiser
self.assertContains(response, '"halloblockquote":')
self.assertContains(response, '"halloquotation":')
self.assertContains(response, '"hallowagtailembeds":')
self.assertNotContains(response, '"hallolists":')
self.assertNotContains(response, '"hallowagtailimage":')
# check that media (js/css) from the features is being imported
self.assertContains(response, 'testapp/js/hallo-blockquote.js')
self.assertContains(response, 'testapp/css/hallo-blockquote.css')
self.assertContains(response, 'testapp/js/hallo-quotation.js')
self.assertContains(response, 'testapp/css/hallo-quotation.css')
# check that we're NOT importing media for the default features we're not using
self.assertNotContains(response, 'wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js')
def test_features_list_on_rich_text_block(self):
block = RichTextBlock(features=['blockquote', 'embed', 'made-up-feature'])
block = RichTextBlock(features=['quotation', 'embed', 'made-up-feature'])
form_html = block.render_form(block.to_python("<p>hello</p>"), 'body')
# Check that the custom plugin options are being passed in the hallo initialiser
self.assertIn('"halloblockquote":', form_html)
self.assertIn('"halloquotation":', form_html)
self.assertIn('"hallowagtailembeds":', form_html)
self.assertNotIn('"hallolists":', form_html)
self.assertNotIn('"hallowagtailimage":', form_html)
# check that media (js/css) from the features is being imported
media_html = str(block.media)
self.assertIn('testapp/js/hallo-blockquote.js', media_html)
self.assertIn('testapp/css/hallo-blockquote.css', media_html)
self.assertIn('testapp/js/hallo-quotation.js', media_html)
self.assertIn('testapp/css/hallo-quotation.css', media_html)
# check that we're NOT importing media for the default features we're not using
self.assertNotIn('wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js', media_html)
@ -464,17 +464,81 @@ class TestDraftailWithFeatureOptions(BaseRichTextEditHandlerTestCase, WagtailTes
self.assertNotIn('"type": "ordered-list-item""', form_html)
class TestDraftailWithAdditionalFeatures(BaseRichTextEditHandlerTestCase, WagtailTestUtils):
def setUp(self):
super().setUp()
# Find root page
self.root_page = Page.objects.get(id=2)
self.login()
@override_settings(WAGTAILADMIN_RICH_TEXT_EDITORS={
'default': {
'WIDGET': 'wagtail.admin.rich_text.DraftailRichTextArea',
},
})
def test_additional_features_should_not_be_included_by_default(self):
response = self.client.get(reverse(
'wagtailadmin_pages:add', args=('tests', 'defaultrichtextfieldpage', self.root_page.id)
))
self.assertEqual(response.status_code, 200)
# default ones are there
self.assertContains(response, '"type": "header-two"')
self.assertContains(response, '"type": "LINK"')
self.assertContains(response, '"type": "ITALIC"')
# not the additional ones.
self.assertNotContains(response, '"type": "CODE"')
self.assertNotContains(response, '"type": "blockquote"')
self.assertNotContains(response, '"type": "SUPERSCRIPT"')
self.assertNotContains(response, '"type": "SUBSCRIPT"')
self.assertNotContains(response, '"type": "STRIKETHROUGH"')
@override_settings(WAGTAILADMIN_RICH_TEXT_EDITORS={
'default': {
'WIDGET': 'wagtail.admin.rich_text.DraftailRichTextArea',
'OPTIONS': {
'features': [
'h2', 'code', 'blockquote',
'strikethrough', 'subscript', 'superscript'
]
}
},
})
def test_additional_features_included(self):
response = self.client.get(reverse(
'wagtailadmin_pages:add', args=('tests', 'defaultrichtextfieldpage', self.root_page.id)
))
self.assertEqual(response.status_code, 200)
# Added features are there
self.assertContains(response, '"type": "header-two"')
self.assertContains(response, '"type": "CODE"')
self.assertContains(response, '"type": "blockquote"')
self.assertContains(response, '"type": "SUPERSCRIPT"')
self.assertContains(response, '"type": "SUBSCRIPT"')
self.assertContains(response, '"type": "STRIKETHROUGH"')
# But not the unprovided default ones.
self.assertNotContains(response, '"type": "LINK"')
self.assertNotContains(response, '"type": "ITALIC"')
@override_settings(WAGTAILADMIN_RICH_TEXT_EDITORS={
'default': {
'WIDGET': 'wagtail.admin.rich_text.HalloRichTextArea',
'OPTIONS': {
'features': ['blockquote', 'image']
'features': ['quotation', 'image']
}
},
'custom': {
'WIDGET': 'wagtail.admin.rich_text.HalloRichTextArea',
'OPTIONS': {
'features': ['blockquote', 'image']
'features': ['quotation', 'image']
}
},
})
@ -497,7 +561,7 @@ class TestHalloJsWithCustomFeatureOptions(BaseRichTextEditHandlerTestCase, Wagta
self.assertEqual(response.status_code, 200)
# Check that the custom plugin options are being passed in the hallo initialiser
self.assertContains(response, '"halloblockquote":')
self.assertContains(response, '"halloquotation":')
self.assertContains(response, '"hallowagtailimage":')
self.assertNotContains(response, '"hallolists":')
self.assertNotContains(response, '"hallowagtailembeds":')
@ -508,14 +572,14 @@ class TestHalloJsWithCustomFeatureOptions(BaseRichTextEditHandlerTestCase, Wagta
'wagtailadmin_pages:add', args=('tests', 'richtextfieldwithfeaturespage', self.root_page.id)
))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '"halloblockquote":')
self.assertContains(response, '"halloquotation":')
self.assertContains(response, '"hallowagtailembeds":')
self.assertNotContains(response, '"hallolists":')
self.assertNotContains(response, '"hallowagtailimage":')
# check that media (js/css) from the features is being imported
self.assertContains(response, 'testapp/js/hallo-blockquote.js')
self.assertContains(response, 'testapp/css/hallo-blockquote.css')
self.assertContains(response, 'testapp/js/hallo-quotation.js')
self.assertContains(response, 'testapp/css/hallo-quotation.css')
# check that we're NOT importing media for the default features we're not using
self.assertNotContains(response, 'wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js')
@ -526,26 +590,26 @@ class TestHalloJsWithCustomFeatureOptions(BaseRichTextEditHandlerTestCase, Wagta
form_html = block.render_form(block.to_python("<p>hello</p>"), 'body')
# Check that the custom plugin options are being passed in the hallo initialiser
self.assertIn('"halloblockquote":', form_html)
self.assertIn('"halloquotation":', form_html)
self.assertIn('"hallowagtailimage":', form_html)
self.assertNotIn('"hallowagtailembeds":', form_html)
self.assertNotIn('"hallolists":', form_html)
# a 'features' list passed on the RichTextBlock
# should override the list in OPTIONS
block = RichTextBlock(editor='custom', features=['blockquote', 'embed'])
block = RichTextBlock(editor='custom', features=['quotation', 'embed'])
form_html = block.render_form(block.to_python("<p>hello</p>"), 'body')
self.assertIn('"halloblockquote":', form_html)
self.assertIn('"halloquotation":', form_html)
self.assertIn('"hallowagtailembeds":', form_html)
self.assertNotIn('"hallowagtailimage":', form_html)
self.assertNotIn('"hallolists":', form_html)
# check that media (js/css) from the features is being imported
media_html = str(block.media)
self.assertIn('testapp/js/hallo-blockquote.js', media_html)
self.assertIn('testapp/css/hallo-blockquote.css', media_html)
self.assertIn('testapp/js/hallo-quotation.js', media_html)
self.assertIn('testapp/css/hallo-quotation.css', media_html)
# check that we're NOT importing media for the default features we're not using
self.assertNotIn('wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js', media_html)

Wyświetl plik

@ -459,6 +459,21 @@ def register_core_features(features):
'block_map': {'ordered-list-item': {'element': 'li', 'wrapper': 'ol'}}
}
})
features.register_editor_plugin(
'draftail', 'blockquote', draftail_features.BlockFeature({
'type': 'blockquote',
'icon': 'openquote',
'description': ugettext('Blockquote'),
})
)
features.register_converter_rule('contentstate', 'blockquote', {
'from_database_format': {
'blockquote': BlockElementHandler('blockquote'),
},
'to_database_format': {
'block_map': {'blockquote': 'blockquote'}
}
})
features.register_editor_plugin(
'draftail', 'bold', draftail_features.InlineStyleFeature({
@ -518,3 +533,63 @@ def register_core_features(features):
'entity_decorators': {'LINK': link_entity}
}
})
features.register_editor_plugin(
'draftail', 'superscript', draftail_features.InlineStyleFeature({
'type': 'SUPERSCRIPT',
'icon': 'superscript',
'description': ugettext('Superscript'),
})
)
features.register_converter_rule('contentstate', 'superscript', {
'from_database_format': {
'sup': InlineStyleElementHandler('SUPERSCRIPT'),
},
'to_database_format': {
'style_map': {'SUPERSCRIPT': 'sup'}
}
})
features.register_editor_plugin(
'draftail', 'subscript', draftail_features.InlineStyleFeature({
'type': 'SUBSCRIPT',
'icon': 'subscript',
'description': ugettext('Subscript'),
})
)
features.register_converter_rule('contentstate', 'subscript', {
'from_database_format': {
'sub': InlineStyleElementHandler('SUBSCRIPT'),
},
'to_database_format': {
'style_map': {'SUBSCRIPT': 'sub'}
}
})
features.register_editor_plugin(
'draftail', 'strikethrough', draftail_features.InlineStyleFeature({
'type': 'STRIKETHROUGH',
'icon': 'strikethrough',
'description': ugettext('Strikethrough'),
})
)
features.register_converter_rule('contentstate', 'strikethrough', {
'from_database_format': {
's': InlineStyleElementHandler('STRIKETHROUGH'),
},
'to_database_format': {
'style_map': {'STRIKETHROUGH': 's'}
}
})
features.register_editor_plugin(
'draftail', 'code', draftail_features.InlineStyleFeature({
'type': 'CODE',
'icon': 'code',
'description': ugettext('Code'),
})
)
features.register_converter_rule('contentstate', 'code', {
'from_database_format': {
'code': InlineStyleElementHandler('CODE'),
},
'to_database_format': {
'style_map': {'CODE': 'code'}
}
})

Wyświetl plik

@ -880,6 +880,9 @@
<li class="icon icon-chain-broken">chain-broken</li>
<li class="icon icon-table">table</li>
<li class="icon icon-logout">logout</li>
<li class="icon icon-superscript">superscript</li>
<li class="icon icon-subscript">subscript</li>
<li class="icon icon-strikethrough">strikethrough</li>
</ul>
</section>

Wyświetl plik

@ -106,8 +106,8 @@ class TestFeatureRegistry(TestCase):
# testapp/wagtail_hooks.py defines a 'blockquote' rich text feature with a hallo.js
# plugin, via the register_rich_text_features hook; test that we can retrieve it here
features = FeatureRegistry()
blockquote = features.get_editor_plugin('hallo', 'blockquote')
self.assertEqual(blockquote.name, 'halloblockquote')
quotation = features.get_editor_plugin('hallo', 'quotation')
self.assertEqual(quotation.name, 'halloquotation')
def test_missing_editor_plugin_returns_none(self):
features = FeatureRegistry()

Wyświetl plik

@ -1170,7 +1170,7 @@ class CustomRichBlockFieldPage(Page):
class RichTextFieldWithFeaturesPage(Page):
body = RichTextField(features=['blockquote', 'embed', 'made-up-feature'])
body = RichTextField(features=['quotation', 'embed', 'made-up-feature'])
content_panels = [
FieldPanel('title', classname="full title"),

Wyświetl plik

@ -88,22 +88,22 @@ def hide_hidden_pages(parent_page, pages, request):
return pages.exclude(title__icontains='hidden')
# register 'blockquote' as a rich text feature supported by a hallo.js plugin
# register 'quotation' as a rich text feature supported by a hallo.js plugin
# and a Draftail feature
@hooks.register('register_rich_text_features')
def register_blockquote_feature(features):
def register_quotation_feature(features):
features.register_editor_plugin(
'hallo', 'blockquote', HalloPlugin(
name='halloblockquote',
js=['testapp/js/hallo-blockquote.js'],
css={'all': ['testapp/css/hallo-blockquote.css']},
'hallo', 'quotation', HalloPlugin(
name='halloquotation',
js=['testapp/js/hallo-quotation.js'],
css={'all': ['testapp/css/hallo-quotation.css']},
)
)
features.register_editor_plugin(
'draftail', 'blockquote', draftail_features.EntityFeature(
'draftail', 'quotation', draftail_features.EntityFeature(
{},
js=['testapp/js/draftail-blockquote.js'],
css={'all': ['testapp/css/draftail-blockquote.css']},
js=['testapp/js/draftail-quotation.js'],
css={'all': ['testapp/css/draftail-quotation.css']},
)
)