diff --git a/adhocracy-plus/assets/extra_css/_ckeditor.scss b/adhocracy-plus/assets/extra_css/_ckeditor.scss new file mode 100644 index 000000000..c67b0bc61 --- /dev/null +++ b/adhocracy-plus/assets/extra_css/_ckeditor.scss @@ -0,0 +1,227 @@ +/* + * CKEditor 5 (v40.2.0) content styles. + * Generated on Wed, 13 Dec 2023 08:57:10 GMT. + * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html + */ + +:root { + --ck-color-image-caption-background: hsl(0deg, 0%, 97%); + --ck-color-image-caption-text: hsl(0deg, 0%, 20%); + --ck-color-mention-background: hsla(341deg, 100%, 30%, 0.1); + --ck-color-mention-text: hsl(341deg, 100%, 30%); + --ck-color-selector-caption-background: hsl(0deg, 0%, 97%); + --ck-color-selector-caption-text: hsl(0deg, 0%, 20%); + --ck-highlight-marker-blue: hsl(201deg, 97%, 72%); + --ck-highlight-marker-green: hsl(120deg, 93%, 68%); + --ck-highlight-marker-pink: hsl(345deg, 96%, 73%); + --ck-highlight-marker-yellow: hsl(60deg, 97%, 73%); + --ck-highlight-pen-green: hsl(112deg, 100%, 27%); + --ck-highlight-pen-red: hsl(0deg, 85%, 49%); + --ck-image-style-spacing: 1.5em; + --ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2); + --ck-todo-list-checkmark-size: 16px; +} + +/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */ +.ck-content .media { + clear: both; + margin: 0.9em 0; + display: block; + min-width: 15em; +} + +/* @ckeditor/ckeditor5-image/theme/image.css */ +.ck-content .image img { + display: block; + margin: 0 auto; + max-width: 100%; + min-width: 100%; + height: auto; +} + +/* @ckeditor/ckeditor5-image/theme/image.css */ +.ck-content .image-inline { + /* + * Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).; + * Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root. + * This strange behavior does not happen with inline-flex. + */ + display: inline-flex; + max-width: 100%; + align-items: flex-start; +} + +/* @ckeditor/ckeditor5-image/theme/image.css */ +.ck-content .image-inline picture, +.ck-content .image-inline img { + flex-grow: 1; + flex-shrink: 1; + max-width: 100%; +} + +/* @ckeditor/ckeditor5-image/theme/imageresize.css */ +.ck-content img.image_resized { + height: auto; +} + +/* @ckeditor/ckeditor5-image/theme/imageresize.css */ +.ck-content .image.image_resized { + max-width: 100%; + display: block; + box-sizing: border-box; +} + +/* @ckeditor/ckeditor5-image/theme/imageresize.css */ +.ck-content .image.image_resized img { + width: 100%; +} + +/* @ckeditor/ckeditor5-image/theme/imagecaption.css */ +.ck-content .image > figcaption { + display: table-caption; + caption-side: bottom; + word-break: break-word; + color: var(--ck-color-image-caption-text); + background-color: var(--ck-color-image-caption-background); + padding: 0.6em; + font-size: 0.75em; + outline-offset: -1px; +} + +/* @ckeditor/ckeditor5-image/theme/imageresize.css */ +.ck-content .image.image_resized > figcaption { + display: block; +} + +/* @ckeditor/ckeditor5-image/theme/image.css */ +.ck-content .image { + display: table; + clear: both; + text-align: center; + margin: 0.9em auto; + min-width: 50px; +} + +/* @ckeditor/ckeditor5-image/theme/image.css */ +.ck-content .image-inline picture { + display: flex; +} + + +/* @ckeditor/ckeditor5-list/theme/list.css */ +.ck-content ol { + list-style-type: decimal; +} + +/* @ckeditor/ckeditor5-list/theme/list.css */ +.ck-content ol ol { + list-style-type: lower-latin; +} + +/* @ckeditor/ckeditor5-list/theme/list.css */ +.ck-content ol ol ol { + list-style-type: lower-roman; +} + +/* @ckeditor/ckeditor5-list/theme/list.css */ +.ck-content ol ol ol ol { + list-style-type: upper-latin; +} + +/* @ckeditor/ckeditor5-list/theme/list.css */ +.ck-content ol ol ol ol ol { + list-style-type: upper-roman; +} + +/* @ckeditor/ckeditor5-list/theme/list.css */ +.ck-content ul { + list-style-type: disc; +} + +/* @ckeditor/ckeditor5-list/theme/list.css */ +.ck-content ul ul { + list-style-type: circle; +} + +/* @ckeditor/ckeditor5-list/theme/list.css */ +.ck-content ul ul ul { + list-style-type: square; +} + +/* @ckeditor/ckeditor5-list/theme/list.css */ +.ck-content ul ul ul ul { + list-style-type: square; +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-style-block-align-left, +.ck-content .image-style-block-align-right { + max-width: calc(100% - var(--ck-image-style-spacing)); +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-style-align-left, +.ck-content .image-style-align-right { + clear: none; +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-style-side { + float: right; + margin-left: var(--ck-image-style-spacing); + max-width: 50%; +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-style-align-left { + float: left; + margin-right: var(--ck-image-style-spacing); +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-style-align-center { + margin-left: auto; + margin-right: auto; +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-style-align-right { + float: right; + margin-left: var(--ck-image-style-spacing); +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-style-block-align-right { + margin-right: 0; + margin-left: auto; +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-style-block-align-left { + margin-left: 0; + margin-right: auto; +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content p + .image-style-align-left, +.ck-content p + .image-style-align-right, +.ck-content p + .image-style-side { + margin-top: 0; +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-inline.image-style-align-left, +.ck-content .image-inline.image-style-align-right { + margin-top: var(--ck-inline-image-style-spacing); + margin-bottom: var(--ck-inline-image-style-spacing); +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-inline.image-style-align-left { + margin-right: var(--ck-inline-image-style-spacing); +} + +/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ +.ck-content .image-inline.image-style-align-right { + margin-left: var(--ck-inline-image-style-spacing); +} diff --git a/adhocracy-plus/assets/js/app.js b/adhocracy-plus/assets/js/app.js index a25cb6b9f..c46e1fc9c 100644 --- a/adhocracy-plus/assets/js/app.js +++ b/adhocracy-plus/assets/js/app.js @@ -67,10 +67,6 @@ function init () { if ($.fn.select2) { $('.js-select2').select2() } - - // This function adds required classes to iframes added by ckeditor - $('.rich-text iframe').addClass('ck_embed_iframe') - $('.ck_embed_iframe').parent('div').addClass('ck_embed_iframe__container') } document.addEventListener('DOMContentLoaded', init, false) diff --git a/adhocracy-plus/assets/scss/_form.scss b/adhocracy-plus/assets/scss/_form.scss index 3f999b897..ab3e131bd 100644 --- a/adhocracy-plus/assets/scss/_form.scss +++ b/adhocracy-plus/assets/scss/_form.scss @@ -12,6 +12,7 @@ label { border-radius: 0; } + textarea, select, input { @@ -86,6 +87,11 @@ div.cke_focus { // adhocracy4 core). margin-bottom: 0; } + + // hide ckeditor input field + .django-ckeditor-widget textarea { + display: none; + } } .form-fieldset { diff --git a/adhocracy-plus/assets/scss/style.scss b/adhocracy-plus/assets/scss/style.scss index deb860544..d069cecd2 100644 --- a/adhocracy-plus/assets/scss/style.scss +++ b/adhocracy-plus/assets/scss/style.scss @@ -135,3 +135,4 @@ @import "utility"; @import "shame"; +@import "../extra_css/ckeditor"; diff --git a/adhocracy-plus/config/settings/base.py b/adhocracy-plus/config/settings/base.py index 046a9ddc3..c473856cb 100644 --- a/adhocracy-plus/config/settings/base.py +++ b/adhocracy-plus/config/settings/base.py @@ -25,6 +25,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.humanize", + "django_ckeditor_5", "widget_tweaks", "rest_framework", "rest_framework.authtoken", @@ -34,8 +35,6 @@ "allauth.socialaccount", "rules.apps.AutodiscoverRulesConfig", "easy_thumbnails", - "ckeditor", - "ckeditor_uploader", "parler", # Wagtail cms components "wagtail.contrib.forms", @@ -305,120 +304,110 @@ ], } -# CKEditor - -CKEDITOR_UPLOAD_PATH = "uploads/" -CKEDITOR_RESTRICT_BY_USER = "username" -CKEDITOR_ALLOW_NONIMAGE_FILES = True - -CKEDITOR_CONFIGS = { +BLEACH_LIST = { "default": { - "width": "100%", - "toolbar": "Custom", - "toolbar_Custom": [ - ["Bold", "Italic", "Underline"], - ["NumberedList", "BulletedList"], - ["Link", "Unlink"], + "tags": [ + "p", + "strong", + "em", + "u", + "ol", + "li", + "ul", + "a", + "img", + "iframe", + "div", ], + "attributes": { + "a": ["href", "rel", "target"], + "img": ["src", "alt", "style"], + "div": ["class"], + "iframe": ["src", "alt", "style"], + }, }, "image-editor": { - "width": "100%", - "toolbar": "Custom", - "toolbar_Custom": [ - ["Bold", "Italic", "Underline"], - ["Image"], - ["NumberedList", "BulletedList"], - ["Link", "Unlink"], - ], - "removeDialogTabs": "image:Link", - }, - "collapsible-image-editor": { - "width": "100%", - "title": _("Rich text editor"), - "toolbar": "Custom", - "toolbar_Custom": [ - ["Bold", "Italic", "Underline"], - ["Image"], - ["NumberedList", "BulletedList"], - ["Link", "Unlink"], - ["CollapsibleItem"], - ["Embed", "EmbedBase"], + "tags": [ + "a", + "em", + "figcaption", + "figure", + "img", + "li", + "ol", + "p", + "span", + "strong", + "u", + "ul", ], - "removePlugins": "stylesheetparser", - "extraAllowedContent": "iframe[*]; div[*]", - "removeDialogTabs": "image:Link", - }, - "video-editor": { - "width": "100%", - "title": _("Rich text editor"), - "toolbar": "Custom", - "toolbar_Custom": [["Embed", "EmbedBase"]], - "removePlugins": "stylesheetparser", - "extraAllowedContent": "iframe[*]; div[*]", - }, -} - -BLEACH_LIST = { - "default": { - "tags": ["p", "strong", "em", "u", "ol", "li", "ul", "a"], "attributes": { "a": ["href", "rel", "target"], + "figure": ["class", "style"], + "figcaption": ["class"], + "img": ["class", "src", "alt", "style", "height", "width"], + "span": ["class", "style"], }, - }, - "image-editor": { - "tags": ["p", "strong", "em", "u", "ol", "li", "ul", "a", "img"], - "attributes": {"a": ["href", "rel", "target"], "img": ["src", "alt", "style"]}, "styles": [ + "aspect-ratio", "float", - "margin", - "padding", - "width", "height", + "margin", "margin-bottom", - "margin-top", "margin-left", "margin-right", + "margin-top", + "padding", + "width", ], }, "collapsible-image-editor": { "tags": [ + "a", + "div", + "em", + "figcaption", + "figure", + "iframe", + "img", + "li", + "ol", "p", + "span", "strong", - "em", "u", - "ol", - "li", "ul", - "a", - "img", - "div", - "iframe", ], "attributes": { "a": ["href", "rel", "target"], - "img": ["src", "alt", "style"], - "div": ["class"], - "iframe": ["src", "alt", "style"], + "div": ["class", "data-oembed-url"], + "figure": ["class", "style"], + "figcaption": ["class"], + "iframe": ["src", "alt"], + "img": ["class", "src", "alt", "style", "height", "width"], + "span": ["class", "style"], }, "styles": [ + "aspect-ratio", "float", - "margin", - "padding", - "width", "height", + "margin", "margin-bottom", - "margin-top", "margin-left", "margin-right", + "margin-top", + "padding", + "width", ], }, "video-editor": { - "tags": ["a", "img", "div", "iframe"], + "tags": ["a", "img", "div", "iframe", "figure"], "attributes": { "a": ["href", "rel", "target"], "img": ["src", "alt", "style"], - "div": ["class"], - "iframe": ["src", "alt", "style"], + "div": ["class", "data-oembed-url"], + "iframe": ["src", "alt"], + "figure": ["class", "div", "iframe"], }, }, } @@ -560,3 +549,131 @@ CELERY_RESULT_BACKEND = "redis://localhost:6379" CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True CELERY_RESULT_EXTENDED = True + +# CKEditor5 config +CKEDITOR_5_FILE_STORAGE = "adhocracy4.ckeditor.storage.CustomStorage" +CKEDITOR_5_PATH_FROM_USERNAME = True +CKEDITOR_5_ALLOW_ALL_FILE_TYPES = True +CKEDITOR_5_UPLOAD_FILE_TYPES = ["jpg", "jpeg", "png", "gif", "pdf", "docx"] +CKEDITOR_5_USER_LANGUAGE = True +CKEDITOR_5_CONFIGS = { + "default": { + "language": ["de", "en", "nl", "ky", "ru"], + "toolbar": [ + "bold", + "italic", + "underline", + "|", + "link", + "bulletedList", + "numberedList", + ], + "list": { + "properties": { + "styles": "true", + "startIndex": "true", + "reversed": "true", + } + }, + "link": {"defaultProtocol": "https://"}, + }, + "image-editor": { + "toolbar": { + "items": [ + "bold", + "italic", + "underline", + "bulletedList", + "numberedList", + "link", + "imageUpload", + "fileUpload", + ], + "shouldNotGroupWhenFull": "true", + }, + "image": { + "toolbar": [ + "imageUpload", + "imageTextAlternative", + "toggleImageCaption", + "imageStyle:inline", + "imageStyle:wrapText", + "imageStyle:breakText", + "imageStyle:alignLeft", + "imageStyle:alignRight", + ], + "insert": {"type": "auto"}, + }, + "list": { + "properties": { + "styles": "true", + "startIndex": "true", + "reversed": "true", + } + }, + "link": {"defaultProtocol": "https://"}, + }, + "collapsible-image-editor": { + "toolbar": [ + "bold", + "italic", + "underline", + "bulletedList", + "numberedList", + "link", + "imageUpload", + "fileUpload", + "mediaEmbed", + "accordionButton", + "fontSize", + ], + "image": { + "toolbar": [ + "imageUpload", + "imageTextAlternative", + "toggleImageCaption", + "imageStyle:inline", + "imageStyle:wrapText", + "imageStyle:breakText", + "imageStyle:alignLeft", + "imageStyle:alignRight", + ], + "insert": {"type": "auto"}, + }, + "list": { + "properties": { + "styles": "true", + "startIndex": "true", + "reversed": "true", + } + }, + "link": {"defaultProtocol": "https://"}, + "mediaEmbed": { + "removeProviders": [ + "dailymotion", + "spotify", + "facebook", + "flickr", + "googleMaps", + "instagram", + "twitter", + ], + "previewsInData": True, + }, + }, + "video-editor": { + "toolbar": ["mediaEmbed"], + "mediaEmbed": { + "removeProviders": [ + "dailymotion", + "spotify", + "facebook", + "flickr", + "googleMaps", + "instagram", + "twitter", + ], + "previewsInData": True, + }, + }, +} diff --git a/adhocracy-plus/config/urls.py b/adhocracy-plus/config/urls.py index 501076c0f..b78877168 100644 --- a/adhocracy-plus/config/urls.py +++ b/adhocracy-plus/config/urls.py @@ -1,16 +1,15 @@ """adhocracy+ URL Configuration.""" -from ckeditor_uploader import views as ck_views from django.conf import settings from django.conf.urls import i18n from django.contrib import admin from django.urls import include from django.urls import path from django.urls import re_path -from django.views.decorators.cache import never_cache from django.views.defaults import server_error from django.views.generic import TemplateView from django.views.i18n import JavaScriptCatalog +from django_ckeditor_5 import views as ckeditor5_views from rest_framework import routers from rest_framework.authtoken.views import obtain_auth_token from wagtail.contrib.sitemaps.views import sitemap as wagtail_sitemap @@ -107,13 +106,10 @@ re_path(r"^api/", include(router.urls)), re_path(r"^api/login", obtain_auth_token, name="api-login"), re_path(r"^api/account/", AccountViewSet.as_view(), name="api-account"), - re_path( - r"^upload/", user_is_project_admin(ck_views.upload), name="ckeditor_upload" - ), - re_path( - r"^browse/", - never_cache(user_is_project_admin(ck_views.browse)), - name="ckeditor_browse", + path( + "ckeditor5/image_upload/", + user_is_project_admin(ckeditor5_views.upload_file), + name="ck_editor_5_upload_file", ), re_path(r"^components/$", contrib_views.ComponentLibraryView.as_view()), re_path(r"^jsi18n/$", JavaScriptCatalog.as_view(), name="javascript-catalog"), diff --git a/apps/activities/admin.py b/apps/activities/admin.py index 783ef6e14..7c31a246c 100644 --- a/apps/activities/admin.py +++ b/apps/activities/admin.py @@ -1,6 +1,6 @@ -from ckeditor_uploader.widgets import CKEditorUploadingWidget from django import forms from django.contrib import admin +from django_ckeditor_5.widgets import CKEditor5Widget from . import models @@ -9,7 +9,7 @@ class ActivityAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["description"].widget = CKEditorUploadingWidget( + self.fields["description"].widget = CKEditor5Widget( config_name="collapsible-image-editor", ) diff --git a/apps/activities/migrations/0002_ckeditor_iframes.py b/apps/activities/migrations/0002_ckeditor_iframes.py new file mode 100644 index 000000000..b27f393ba --- /dev/null +++ b/apps/activities/migrations/0002_ckeditor_iframes.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.20 on 2023-11-16 11:35 + +from bs4 import BeautifulSoup +from django.db import migrations + + +def replace_iframe_with_figur(apps, schema_editor): + template = ( + '
' + ) + Activity = apps.get_model("a4_candy_activities", "Activity") + for activity in Activity.objects.all(): + soup = BeautifulSoup(activity.description, "html.parser") + iframes = soup.findAll("iframe") + for iframe in iframes: + figure = BeautifulSoup( + template.format(url=iframe.attrs["src"]), "html.parser" + ) + iframe.replaceWith(figure) + if iframes: + activity.description = soup.prettify(formatter="html") + activity.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("a4_candy_activities", "0001_initial"), + ] + + operations = [ + migrations.RunPython( + replace_iframe_with_figur, + ), + ] diff --git a/apps/activities/migrations/0003_alter_activity_description.py b/apps/activities/migrations/0003_alter_activity_description.py new file mode 100644 index 000000000..195613599 --- /dev/null +++ b/apps/activities/migrations/0003_alter_activity_description.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.19 on 2024-03-14 12:05 + +from django.db import migrations +import django_ckeditor_5.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("a4_candy_activities", "0002_ckeditor_iframes"), + ] + + operations = [ + migrations.AlterField( + model_name="activity", + name="description", + field=django_ckeditor_5.fields.CKEditor5Field(verbose_name="Description"), + ), + ] diff --git a/apps/activities/models.py b/apps/activities/models.py index bde2cf15b..84c52cdc7 100644 --- a/apps/activities/models.py +++ b/apps/activities/models.py @@ -1,9 +1,9 @@ from autoslug import AutoSlugField from django.db import models from django.utils.translation import gettext_lazy as _ +from django_ckeditor_5.fields import CKEditor5Field from adhocracy4 import transforms as html_transforms -from adhocracy4.ckeditor.fields import RichTextCollapsibleUploadingField from adhocracy4.modules import models as module_models @@ -18,7 +18,7 @@ class Activity(module_models.Item): "or location of your face-to-face event" ), ) - description = RichTextCollapsibleUploadingField( + description = CKEditor5Field( config_name="collapsible-image-editor", verbose_name=_("Description") ) diff --git a/apps/budgeting/migrations/0003_alter_proposal_description.py b/apps/budgeting/migrations/0003_alter_proposal_description.py new file mode 100644 index 000000000..c0ffc0b18 --- /dev/null +++ b/apps/budgeting/migrations/0003_alter_proposal_description.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.19 on 2024-03-26 10:34 + +from django.db import migrations +import django_ckeditor_5.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("a4_candy_budgeting", "0002_rename_moderatorfeedback_fields"), + ] + + operations = [ + migrations.AlterField( + model_name="proposal", + name="description", + field=django_ckeditor_5.fields.CKEditor5Field(verbose_name="Description"), + ), + ] diff --git a/apps/contrib/templates/a4_candy_contrib/item_detail.html b/apps/contrib/templates/a4_candy_contrib/item_detail.html index ec0144b6d..3f2980327 100644 --- a/apps/contrib/templates/a4_candy_contrib/item_detail.html +++ b/apps/contrib/templates/a4_candy_contrib/item_detail.html @@ -57,7 +57,9 @@

{{ object.name }}

- {{ object.description | richtext }} +
+ {{ object.description | richtext }} +
{% block additional_content %}{% endblock %} @@ -144,7 +146,7 @@

{% translate 'Official Feedback' %}

{% html_date object.moderator_feedback_text.created %}
-
+
{{ object.moderator_feedback_text.feedback_text | safe }}
diff --git a/apps/documents/assets/ChapterForm.jsx b/apps/documents/assets/ChapterForm.jsx index caa0bdf2d..6136cd69e 100644 --- a/apps/documents/assets/ChapterForm.jsx +++ b/apps/documents/assets/ChapterForm.jsx @@ -28,20 +28,25 @@ const ChapterForm = (props) => { props.chapter.paragraphs.map(function (paragraph, index, arr) { const key = paragraph.id || paragraph.key return ( - { props.onParagraphDelete(index) }} - onMoveUp={index !== 0 ? () => { props.onParagraphMoveUp(index) } : null} - onMoveDown={index < arr.length - 1 ? () => { props.onParagraphMoveDown(index) } : null} - onParagraphAddBefore={() => { props.onParagraphAddBefore(index) }} - onNameChange={(name) => { props.onParagraphNameChange(index, name) }} - onTextChange={(text) => { props.onParagraphTextChange(index, text) }} - errors={props.errors && props.errors.paragraphs ? props.errors.paragraphs[index] : {}} - /> +
+ { props.onParagraphDelete(index) }} + onMoveUp={index !== 0 ? () => { props.onParagraphMoveUp(index) } : null} + onMoveDown={index < arr.length - 1 ? () => { props.onParagraphMoveDown(index) } : null} + onParagraphAddBefore={() => { props.onParagraphAddBefore(index) }} + onNameChange={(name) => { props.onParagraphNameChange(index, name) }} + onTextChange={(text) => { props.onParagraphTextChange(index, text) }} + errors={props.errors && props.errors.paragraphs ? props.errors.paragraphs[index] : {}} + /> +
) }) } diff --git a/apps/documents/assets/DocumentManagement.jsx b/apps/documents/assets/DocumentManagement.jsx index 61c786eeb..64bceef60 100644 --- a/apps/documents/assets/DocumentManagement.jsx +++ b/apps/documents/assets/DocumentManagement.jsx @@ -314,6 +314,9 @@ class DocumentManagement extends React.Component { onParagraphMoveUp={(paragraphIndex) => { this.handleParagraphMoveUp(chapterIndex, paragraphIndex) }} onParagraphMoveDown={(paragraphIndex) => { this.handleParagraphMoveDown(chapterIndex, paragraphIndex) }} onParagraphDelete={(paragraphIndex) => { this.handleParagraphDelete(chapterIndex, paragraphIndex) }} + csrfCookieName={this.props.csrfCookieName} + uploadUrl={this.props.uploadUrl} + uploadFileTypes={this.props.uploadFileTypes} config={this.props.config} chapter={chapter} errors={chapterErrors} diff --git a/apps/documents/assets/ParagraphForm.jsx b/apps/documents/assets/ParagraphForm.jsx index a4206e310..73c651805 100644 --- a/apps/documents/assets/ParagraphForm.jsx +++ b/apps/documents/assets/ParagraphForm.jsx @@ -1,147 +1,175 @@ -const React = require('react') -const django = require('django') -const FormFieldError = require('adhocracy4/adhocracy4/static/FormFieldError') +import React, { useRef, useEffect } from 'react' +import django from 'django' +import FormFieldError from 'adhocracy4/adhocracy4/static/FormFieldError' -const ckGet = function (id) { - return window.CKEDITOR.instances[id] +// translations +const translations = { + headline: django.gettext('Headline'), + paragraph: django.gettext('Paragraph'), + moveUp: django.gettext('Move up'), + moveDown: django.gettext('Move down'), + delete: django.gettext('Delete'), + helpText: django.gettext( + 'If you add an image, please provide an ' + + 'alternate text. It serves as a textual description of the image ' + + 'content and is read out by screen readers. Describe the image in ' + + 'approx. 80 characters. Example: A busy square with people in summer.' + ) } -const ckReplace = function (id, config) { - return window.CKEDITOR.replace(id, config) -} - -class ParagraphForm extends React.Component { - handleNameChange (e) { - const name = e.target.value - this.props.onNameChange(name) - } +const ParagraphForm = (props) => { + const editor = useRef(null) + const index = useRef(props.index) + const id = 'id_paragraphs-' + props.id + '-text' - ckId () { - return 'id_paragraphs-' + this.props.id + '-text' - } - - ckEditorDestroy () { - const editor = ckGet(this.ckId()) - if (editor) { - editor.destroy() + useEffect(() => { + // destroy if component renders multiple times to prevent + // multiple CKEditors + let destroy = false + const createEditor = async () => { + const config = window.ckeditorPrepareConfig( + JSON.stringify(props.config), + props.csrfCookieName, + props.uploadUrl, + props.uploadFileTypes + ) + const ckeditor = await window.ClassicEditor.create( + document.querySelector('#' + id), + config + ) + if (destroy) { + ckeditor.destroy() + } else { + editor.current = ckeditor + editor.current.model.document.on('change:data', (e) => { + const text = editor.current.getData() + props.onTextChange(text) + }) + editor.current.setData(props.paragraph.text) + } } - } - - ckEditorCreate () { - if (!ckGet(this.ckId())) { - const editor = ckReplace(this.ckId(), this.props.config) - editor.on('change', function (e) { - const text = e.editor.getData() - this.props.onTextChange(text) - }.bind(this)) - editor.setData(this.props.paragraph.text) + if (editor.current) { + if (index.current !== props.index) { + // recreate if index changed + destroyEditor() + } } - } - - UNSAFE_componentWillUpdate (nextProps) { - if (nextProps.index > this.props.index) { - this.ckEditorDestroy() + index.current = props.index + if (!editor.current) { + createEditor() + return () => { + // destroy if still in process of creating + destroy = true + // destroy if already created + destroyEditor() + } } - } + }, [props.index]) - componentDidUpdate (prevProps) { - if (this.props.index > prevProps.index) { - this.ckEditorCreate() + const destroyEditor = () => { + if (editor.current) { + editor.current.destroy() + editor.current = null } } - - componentDidMount () { - this.ckEditorCreate() - } - - componentWillUnmount () { - this.ckEditorDestroy() + const handleNameChange = (e) => { + const name = e.target.value + props.onNameChange(name) } - render () { - const ckEditorToolbarsHeight = 60 // measured on example editor - return ( -
-
+ return ( +
+
+
-
-