diff --git a/djangocms_text/apps.py b/djangocms_text/apps.py
index e84229a5..686edae0 100644
--- a/djangocms_text/apps.py
+++ b/djangocms_text/apps.py
@@ -8,7 +8,40 @@ class TextConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
def ready(self):
- register(check_ckeditor_settings)
+ register(check_ckeditor_settings)
+
+ from django.contrib.admin import site
+
+ registered_inline_fields = ["HTMLField", "CharField"]
+ inline_models = {}
+ blacklist_apps = ["auth", "admin", "sessions", "contenttypes", "sites", "cms", "djangocms_text", "djangocms_alias"]
+ for model, modeladmin in site._registry.items():
+ if model._meta.app_label in blacklist_apps:
+ continue
+ try:
+ form = modeladmin.get_form(request=None) # Worth a try
+ except Exception:
+ form = getattr(modeladmin, "form", None)
+ if form:
+ for field_name, field_instance in form.base_fields.items():
+ if field_instance.__class__.__name__ in registered_inline_fields:
+ inline_models[
+ f"{model._meta.app_label}-{model._meta.model_name}-{field_name}"
+ ] = field_instance.__class__.__name__
+
+ from cms.plugin_pool import plugin_pool
+ for plugin in plugin_pool.plugins.values():
+ model = plugin.model
+ if model._meta.app_label in blacklist_apps:
+ continue
+ form = plugin.form
+ for field_name, field_instance in form.base_fields.items():
+ if field_instance.__class__.__name__ in registered_inline_fields:
+ inline_models[
+ f"{model._meta.app_label}-{model._meta.model_name}-{field_name}"
+ ] = field_instance.__class__.__name__
+
+ self.inline_models = inline_models
def check_ckeditor_settings(app_configs, **kwargs):
diff --git a/djangocms_text/cms_toolbars.py b/djangocms_text/cms_toolbars.py
index 1a2c927c..e2e0cce3 100644
--- a/djangocms_text/cms_toolbars.py
+++ b/djangocms_text/cms_toolbars.py
@@ -1,5 +1,6 @@
from urllib.parse import urlparse, urlunparse
+from django.apps import apps
from django.forms import forms
from django.http import QueryDict
from django.templatetags.static import static
@@ -7,7 +8,7 @@
from django.utils.translation import gettext_lazy as _
from cms.cms_toolbars import CMSToolbar
-from cms.toolbar.items import Button, ButtonList
+from cms.toolbar.items import Button, ButtonList, TemplateItem
from cms.toolbar_pool import toolbar_pool
from . import settings
@@ -51,20 +52,33 @@ def inline_editing(self):
return inline_editing
def populate(self):
- if self.toolbar.edit_mode_active:
+ if self.toolbar.edit_mode_active or self.toolbar.structure_mode_active:
item = ButtonList(side=self.toolbar.RIGHT)
item.add_item(
IconButton(
name=_("Toggle inline editing mode for text plugins"),
url=self.get_full_path_with_param(
"inline_editing", int(not self.inline_editing)
- ),
+ ).replace("/structure/","/edit/"),
active=self.inline_editing,
extra_classes=["cms-icon cms-icon-pencil"],
),
)
self.toolbar.add_item(item)
+ config = settings.CKEDITOR_SETTINGS # TODO: Change to TEXT_EDITOR_SETTINGS
+ if "toolbar_HTMLField" in config:
+ config["toolbar"] = config["toolbar_HTMLField"]
+ item = TemplateItem(
+ "cms/toolbar/config.html",
+ extra_context={
+ "html_field_config": config,
+ "allowed_inlines": apps.get_app_config("djangocms_text").inline_models,
+ },
+ side=self.toolbar.RIGHT,
+ )
+ self.toolbar.add_item(item)
+
def get_full_path_with_param(self, key, value):
"""
Adds key=value to the query parameters, replacing an existing key if necessary
diff --git a/djangocms_text/contrib/text_tinymce/__init__.py b/djangocms_text/contrib/text_tinymce/__init__.py
index e69de29b..7588f4c4 100644
--- a/djangocms_text/contrib/text_tinymce/__init__.py
+++ b/djangocms_text/contrib/text_tinymce/__init__.py
@@ -0,0 +1,12 @@
+from djangocms_text.editors import RTEConfig
+
+
+tinymce = RTEConfig(
+ name="tinymce",
+ config="TINYMCE",
+ js=(
+ "https://cdn.tiny.cloud/1/no-api-key/tinymce/7/tinymce.min.js",
+ "djangocms_text/bundles/bundle.tinymce.min.js",
+ ),
+ css={"all": ("djangocms_text/css/cms.tinymce.css",)},
+)
diff --git a/djangocms_text/templates/cms/toolbar/config.html b/djangocms_text/templates/cms/toolbar/config.html
new file mode 100644
index 00000000..ec6d92be
--- /dev/null
+++ b/djangocms_text/templates/cms/toolbar/config.html
@@ -0,0 +1,2 @@
+{% if html_field_config %}{{ html_field_config|json_script:"cms-cfg-htmlfield-inline-config" }}{% endif %}
+{% if allowed_inlines %}{{ allowed_inlines|json_script:"cms-generic-inline-fields" }}{% endif %}
\ No newline at end of file
diff --git a/private/js/cms.ckeditor5.js b/private/js/cms.ckeditor5.js
index c21fd302..93e424dd 100644
--- a/private/js/cms.ckeditor5.js
+++ b/private/js/cms.ckeditor5.js
@@ -220,7 +220,10 @@ class CMSCKEditor5Plugin {
// returns the edited html code
getHTML (el) {
- return this._editors[el.id].getData();
+ if (el.id in this._editors) {
+ return this._editors[el.id].getData();
+ }
+ return undefined;
}
// returns the edited content as json
@@ -231,8 +234,10 @@ class CMSCKEditor5Plugin {
// destroy the editor
destroyEditor (el) {
- this._editors[el.id].destroy();
- delete this._editors[el.id];
+ if (el.id in this._editors) {
+ this._editors[el.id].destroy();
+ delete this._editors[el.id];
+ }
}
_init() {
diff --git a/private/js/cms.dialog.js b/private/js/cms.dialog.js
index bea2521c..fe297649 100644
--- a/private/js/cms.dialog.js
+++ b/private/js/cms.dialog.js
@@ -4,7 +4,7 @@
class CmsDialog {
/**
- * Constructor for creating an instance of the class whowing a django CMS modal in a
+ * Constructor for creating an instance of the class showing a django CMS modal in a
* modal HTML dialog element to show a plugin admin form in an iframe.
*
* The django CMS modal is resizable (thanks to CSS) and movable. It cannot be
diff --git a/private/js/cms.editor.js b/private/js/cms.editor.js
index 28c27664..0269b317 100644
--- a/private/js/cms.editor.js
+++ b/private/js/cms.editor.js
@@ -2,6 +2,8 @@
/* jshint esversion: 6 */
/* global window, document, fetch, IntersectionObserver, URLSearchParams, console */
+import CmsTextEditor from './cms.texteditor.js';
+
// #############################################################################
// CMS Editor
// #############################################################################
@@ -12,6 +14,7 @@ class CMSEditor {
// Initialize the editor object
constructor() {
this._editors = [];
+ this._generic_editors = [];
this._options = {};
this._editor_settings = {};
@@ -97,12 +100,21 @@ class CMSEditor {
);
// Create editor
- window.cms_editor_plugin.create(
- el,
- inModal,
- content, settings,
- el.tagName !== 'TEXTAREA' ? () => this.saveData(el) : () => {}
- );
+ if (el.dataset.cmsType === 'TextPlugin' || el.dataset.cmsType === 'HTMLField') {
+ window.cms_editor_plugin.create(
+ el,
+ inModal,
+ content, settings,
+ el.tagName !== 'TEXTAREA' ? () => this.saveData(el) : () => {
+ }
+ );
+ } else if (el.dataset.cmsType === 'CharField') {
+ this._generic_editors.push(new CmsTextEditor(el, {
+ spellcheck: el.dataset.spellcheck || 'false',
+ },
+ (el) => this.saveData(el)
+ ));
+ }
this._editors.push(el);
}
@@ -128,38 +140,49 @@ class CMSEditor {
threshold: 0.05
});
+ let generic_inline_fields = document.getElementById('cms-generic-inline-fields') || {};
+ if (generic_inline_fields) {
+ generic_inline_fields = JSON.parse(generic_inline_fields.textContent || '{}');
+ }
plugins.forEach(function (plugin) {
- if (plugin[1].plugin_type === 'TextPlugin') {
+ if (plugin[1].type === 'plugin' || plugin[1].type === 'generic') {
const url = plugin[1].urls.edit_plugin;
const id = plugin[1].plugin_id;
- const elements = document.querySelectorAll('.cms-plugin.cms-plugin-' + id);
- let wrapper;
-
- if (elements.length > 0) {
- if (elements.length === 1 && elements[0].tagName === 'DIV') { // already wrapped?
- wrapper = elements[0];
- wrapper.classList.add('cms-editor-inline-wrapper');
- } else { // no, wrap now!
- wrapper = document.createElement('div');
- wrapper.classList.add('cms-editor-inline-wrapper', 'wrapped');
- wrapper = this._wrapAll(elements, wrapper);
- wrapper.classList.add('cms-plugin', 'cms-plugin-' + id);
- for (let child of wrapper.children) {
- child.classList.remove('cms-plugin', 'cms-plugin-' + id);
+ let editorElements = [];
+
+ if (plugin[1].plugin_type === 'TextPlugin') {
+ const elements = document.querySelectorAll('.cms-plugin.cms-plugin-' + id);
+ editorElements = this._initInlineRichText(elements, url, id);
+ } else if (plugin[1].type === 'generic') {
+ editorElements = document.getElementsByClassName(plugin[0]);
+ const edit_fields = new URL(url.replace('&', '&'), 'https://random-base.org')
+ .searchParams.get('edit_fields');
+ if (edit_fields && edit_fields.indexOf(',') === -1 && edit_fields !== 'changelist') {
+ const generic_class = plugin[0].split('-');
+ const search_key = `${generic_class[2]}-${generic_class[3]}-${edit_fields}`;
+ console.log(search_key);
+ console.log(generic_inline_fields) ;
+ if (generic_inline_fields[search_key]) {
+ this._initInlineGeneric(
+ editorElements, url, id, edit_fields, generic_inline_fields[search_key], plugin[1].onClose
+ );
}
}
- wrapper.dataset.cmsEditUrl = url;
- wrapper.dataset.cmsPluginId = id;
+ }
+ if (editorElements) {
// Catch CMS single click event to highlight the plugin
// Catch CMS double click event if present, since double click is needed by Editor
- if (this.CMS) {
- this.CMS.$(wrapper).on('dblclick.cms-editor', function (event) {
- event.stopPropagation();
- });
- wrapper.addEventListener('focusin.cms-editor', () => {
- this._highlightTextplugin(id);
- }, true);
+ for (let wrapper of editorElements) {
+ this.observer.observe(wrapper);
+ if (this.CMS) {
+ this.CMS.$(wrapper).on('dblclick.cms-editor', function (event) {
+ event.stopPropagation();
+ });
+ wrapper.addEventListener('focusin.cms-editor', () => {
+ this._highlightTextplugin(id);
+ }, true);
+ }
}
// Prevent tooltip on hover
@@ -170,8 +193,6 @@ class CMSEditor {
this.CMS.API.Tooltip.displayToggle(false, event.target, '', id);
}, 0);
});
-
- this.observer.observe(wrapper);
}
}
}, this);
@@ -185,6 +206,44 @@ class CMSEditor {
});
}
+ _initInlineRichText(elements, url, id) {
+ let wrapper;
+
+ if (elements.length > 0) {
+ if (elements.length === 1 && elements[0].tagName === 'DIV' || elements[0].tagName === 'CMS-PLUGIN') {
+ // already wrapped?
+ wrapper = elements[0];
+ wrapper.classList.add('cms-editor-inline-wrapper');
+ } else { // no, wrap now!
+ wrapper = document.createElement('div');
+ wrapper.classList.add('cms-editor-inline-wrapper', 'wrapped');
+ wrapper = this._wrapAll(elements, wrapper);
+ wrapper.classList.add('cms-plugin', 'cms-plugin-' + id);
+ for (let child of wrapper.children) {
+ child.classList.remove('cms-plugin', 'cms-plugin-' + id);
+ }
+ }
+ wrapper.dataset.cmsEditUrl = url;
+ wrapper.dataset.cmsPluginId = id;
+ wrapper.dataset.cmsType = 'TextPlugin';
+
+ return [wrapper];
+ }
+ return undefined;
+ }
+
+ _initInlineGeneric(elements, url, id, edit_field, field_type, onClose) {
+ for (let el of elements) {
+ el.dataset.cmsEditUrl = url;
+ el.dataset.cmsCsrfToken = this.CMS.config.csrf;
+ el.dataset.onClose = onClose;
+ el.dataset.cmsField = edit_field;
+ el.dataset.cmsType = field_type;
+ el.dataset.settings = 'htmlfield-inline-config';
+ }
+ return elements;
+ }
+
/**
* Retrieves the settings for the given editor.
* If the element is a string, it will be treated as an element's ID.
@@ -249,10 +308,20 @@ class CMSEditor {
// CMS Editor: destroy
destroyAll() {
while (this._editors.length) {
- window.cms_editor_plugin.destroyEditor(this._editors.pop());
+ const el = this._editors.pop();
+ this.destroyGenericEditor(el);
+ window.cms_editor_plugin.destroyEditor(el);
}
}
+ // CMS Editor: destroyGenericEditor
+ destroyGenericEditor (el) {
+ if (el in this._generic_editors) {
+ this._generic_editors[el].destroy();
+ delete this._generic_editors[el];
+ this._generic_editors.pop(el);
+ }
+ }
saveData(el, action) {
if (el && el.dataset.changed === "true") {
@@ -261,20 +330,27 @@ class CMSEditor {
let url = el.dataset.cmsEditUrl;
let csrf = el.dataset.cmsCsrfToken;
+ let field = el.dataset.cmsField;
if (this.CMS) {
this.CMS.API.Toolbar.showLoader();
url = this.CMS.API.Helpers.updateUrlWithPath(url);
csrf = this.CMS.config.csrf;
}
+ let data = {
+ csrfmiddlewaretoken: csrf,
+ _save: 'Save'
+ };
+ if (field) {
+ data[field] = el.textContent;
+ } else {
+ data.body = html;
+ data.json = JSON.stringify(json) || '';
+ }
+
fetch(url, {
method: 'POST',
- body: new URLSearchParams({
- csrfmiddlewaretoken: csrf,
- body: html,
- json: JSON.stringify(json) || '',
- _save: 'Save'
- }),
+ body: new URLSearchParams(data),
})
.then(response => {
el.dataset.changed = 'false';
@@ -286,11 +362,14 @@ class CMSEditor {
}
return response.text();
}).then(body => {
- // Read the CMS databridge values from the response, either directly or from a script tag or
- // from the response using regex.
+ // If the edited field does not force a reload, read the CMS databridge values from the response,
+ // either directly or from a script tag or from the response using regex.
// This depends on the exact format django CMS core returns it. This will need to be adjusted
// if the format changes.
// Fallback solution is to reload the page as djagocms-text-ckeditor used to do.
+ if (el.dataset.onClose) {
+ this.CMS.API.Helpers.reloadBrowser(el.dataset.onClose);
+ }
const dom = document.createElement('div');
dom.innerHTML = body;
const script = dom.querySelector('script#data-bridge');
@@ -303,8 +382,7 @@ class CMSEditor {
this.CMS.API.Helpers.dataBridge = JSON.parse(regex1[1]);
this.CMS.API.Helpers.dataBridge.structure = JSON.parse(regex2[1]);
} else {
- // No databridge found
- // Reload
+ // No databridge found: reload
this.CMS.API.Helpers.reloadBrowser('REFRESH_PAGE');
return;
}
diff --git a/private/js/cms.texteditor.js b/private/js/cms.texteditor.js
new file mode 100644
index 00000000..595f83c0
--- /dev/null
+++ b/private/js/cms.texteditor.js
@@ -0,0 +1,95 @@
+/* eslint-env es6 */
+/* jshint esversion: 6 */
+/* global document, window, console */
+
+class CmsTextEditor {
+ constructor (el, options, save_callback) {
+ this.el = el;
+ this.plugin_identifier = this.find_plugin_identifier();
+ const id_split = this.plugin_identifier.split('-');
+ this.plugin_id = parseInt(id_split[id_split.length-1]);
+ this.options = options;
+ this.events = {};
+ this.save = (el) => {
+ save_callback(el);
+ };
+ this.init();
+ }
+
+ destroy () {
+ this.el.removeEventListener('focus', this._focus.bind(this));
+ this.el.removeEventListener('blur', this._blur.bind(this));
+ this.el.removeEventListener('input', this._change);
+ this.el.removeEventListener('keydown', this._key_down);
+ this.el.removeEventListener('paste', this._paste);
+ this.el.setAttribute('contenteditable', 'false');
+ }
+
+ init () {
+ this.el.setAttribute('contenteditable', 'plaintext-only');
+ if (!this.el.isContentEditable) {
+ this.el.setAttribute('contenteditable', 'true');
+ this.options.enforcePlaintext = true;
+
+ }
+ this.el.setAttribute('spellcheck', this.options.spellcheck || 'false');
+ this.el.addEventListener('input', this._change);
+ this.el.addEventListener('focus', this._focus.bind(this));
+ this.el.addEventListener('blur', this._blur.bind(this));
+ this.el.addEventListener('keydown', this._key_down);
+ if (this.options.enforcePlaintext) {
+ this.el.addEventListener('paste', this._paste);
+ }
+ }
+
+ _key_down (e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ e.target.blur();
+ }
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ e.target.innerText = e.target.dataset.undo;
+ e.target.blur();
+ }
+ }
+
+ _focus (e) {
+ this.options.undo = this.el.innerText;
+ }
+
+ _blur (e) {
+ if (this.el.innerText !== this.options.undo) {
+ this.save(e.target);
+ }
+ }
+
+ _paste (e) {
+ // Upon past only take the plain text
+ e.preventDefault();
+ let text = e.clipboardData.getData('text/plain');
+ if (text) {
+ const [start, end] = [e.target.selectionStart, this.el.selectionEnd];
+ e.target.setRangeText(text, start, end, 'select');
+ }
+ }
+
+ _change (e) {
+ e.target.dataset.changed = 'true';
+ }
+ find_plugin_identifier () {
+ const header = 'cms-plugin-';
+
+ for (let cls of this.el.classList) {
+ if (cls.startsWith(header)) {
+ let items = cls.substring(header.length).split('-');
+ if (items.length === 4 && items[items.length-1] == parseInt(items[items.length-1])) {
+ return items.join('-');
+ }
+ }
+ }
+ return null;
+ }
+}
+
+export { CmsTextEditor as default };
diff --git a/private/js/cms.tiptap.js b/private/js/cms.tiptap.js
index 048f8a71..d133e626 100644
--- a/private/js/cms.tiptap.js
+++ b/private/js/cms.tiptap.js
@@ -157,7 +157,10 @@ class CMSTipTapPlugin {
* @return {string} - The HTML content of the specified editor element.
*/
getHTML(el) {
- return this._editors[el.id].getHTML();
+ if (el.id in this._editors) {
+ return this._editors[el.id].getHTML();
+ }
+ return undefined;
}
/**
@@ -169,7 +172,10 @@ class CMSTipTapPlugin {
* @return {Object} - The JSON representation of the element.
*/
getJSON(el) {
- return this._editors[el.id].getJSON();
+ if (el.id in this._editors) {
+ return this._editors[el.id].getJSON();
+ }
+ return undefined;
}
/**
@@ -183,8 +189,10 @@ class CMSTipTapPlugin {
if (document.getElementById(el.id + '_editor')) {
document.getElementById(el.id + '_editor').remove();
}
- this._editors[el.id].destroy();
- delete this._editors[el.id];
+ if (el.id in this._editors) {
+ this._editors[el.id].destroy();
+ delete this._editors[el.id];
+ }
}
// transforms the textarea into a div, and returns the div
diff --git a/private/js/tiptap_plugins/cms.plugin.js b/private/js/tiptap_plugins/cms.plugin.js
index 932cf284..8c927308 100644
--- a/private/js/tiptap_plugins/cms.plugin.js
+++ b/private/js/tiptap_plugins/cms.plugin.js
@@ -99,7 +99,6 @@ function renderCmsPluginMenu(editor, item, filter) {
dropdown += ``;
}
dropdown += ``;
- console.log(plugin);
}
return `${icon}${dropdown}
`;