diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dce137db..68bf0e45 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,9 +2,17 @@ Changelog ========= +0.5.3 (18-01-2025) +================== +* fix: Markup error disabled the block toolbar in inline editing +* fix: Handle partial page updates + 0.5.2 (16-01-2025) ================== +* feat: Add text color from list of options by @fsbraun in https://github.com/django-cms/djangocms-text/pull/48 +* feat: allows an ``admin_css: tuple`` in ``RTEConfig`` for a list of CSS files only to be loaded into the admin for the editor by @fsbraun in https://github.com/django-cms/djangocms-text/pull/49 +* feat: Add configurable block and inline styles for Tiptap by @fsbraun in https://github.com/django-cms/djangocms-text/pull/51 * fix: Update CKEditor4 vendor files to work with CMS plugins * fix: Update icon paths for CKEditor4 diff --git a/djangocms_text/__init__.py b/djangocms_text/__init__.py index f3ed4e61..5b7ce6b4 100644 --- a/djangocms_text/__init__.py +++ b/djangocms_text/__init__.py @@ -16,4 +16,4 @@ 10. Github actions will publish the new package to pypi """ -__version__ = "0.5.2" +__version__ = "0.5.3" diff --git a/private/css/cms.toolbar.css b/private/css/cms.toolbar.css index beebaaa0..2396c016 100644 --- a/private/css/cms.toolbar.css +++ b/private/css/cms.toolbar.css @@ -17,6 +17,8 @@ [role="menubar"] { bottom: calc(100% - 1px); + min-width: 375px; + z-index: 1; padding: 2px 0.4rem; /* border-radius: 3px; */ margin: 0 !important; diff --git a/private/js/cms.editor.js b/private/js/cms.editor.js index 3b0ebaf1..2fef2c6a 100644 --- a/private/js/cms.editor.js +++ b/private/js/cms.editor.js @@ -15,10 +15,9 @@ class CMSEditor { // CMS Editor: constructor // Initialize the editor object constructor() { - this._editors = []; - this._generic_editors = []; this._global_settings = {}; this._editor_settings = {}; + this._generic_editors = {}; this._admin_selector = 'textarea.CMS_Editor'; this._admin_add_row_selector = 'body.change-form .add-row a'; this._inline_admin_selector = 'body.change-form .form-row'; @@ -94,83 +93,25 @@ class CMSEditor { // CMS Editor: init // Initialize a single editor init (el) { - let content; - // Get content: json > textarea > innerHTML - if (el.dataset.json) { - content = JSON.parse(el.dataset.json); - } else { - content = el.innerHTML; - } - if (el.tagName === 'TEXTAREA') { - el.visible = false; - content = el.value; - // el = el.insertAdjacentElement('afterend', document.createElement('div')); - } if (!el.id) { el.id = "cms-edit-" + Math.random().toString(36).slice(2, 9); } - const settings = this.getSettings(el); - // Element options overwrite - settings.options = Object.assign({}, - settings.options || {}, - JSON.parse(el.dataset.options || '{}') - ); - - // Add event listener to delete data on modal cancel - if (settings.revert_on_cancel) { - const CMS = this.CMS; - const csrf = CMS.config?.csrf || document.querySelector('input[name="csrfmiddlewaretoken"]').value; - CMS.API.Helpers.addEventListener( - 'modal-close.text-plugin.text-plugin-' + settings.plugin_id, - function(e, opts) { - if (!settings.revert_on_cancel || !settings.cancel_plugin_url) { - return; - } - CMS.$.ajax({ - method: 'POST', - url: settings.cancel_plugin_url, - data: { - token: settings.action_token, - csrfmiddlewaretoken: csrf - }, - }).done(function () { - CMS.API.Helpers.removeEventListener( - 'modal-close.text-plugin.text-plugin-' + settings.plugin_id - ); - opts.instance.close(); - }).fail(function (res) { - CMS.API.Messages.open({ - message: res.responseText + ' | ' + res.status + ' ' + res.statusText, - delay: 0, - error: true - }); - }); - - } - ); + if (el.id in this._editor_settings) { + // Already initialized - happens when an inline editor is scrolled back into the viewport + return; } - const inModal = !!document.querySelector( - '.app-djangocms_text.model-text.change-form #' + el.id - ); - // Create editor - if (!el.dataset.cmsType ||el.dataset.cmsType === 'TextPlugin' || el.dataset.cmsType === 'HTMLField') { - window.cms_editor_plugin.create( - el, - inModal, - content, settings, - el.tagName !== 'TEXTAREA' ? () => this.saveData(el) : () => { - } - ); + if (!el.dataset.cmsType || el.dataset.cmsType === 'TextPlugin' || el.dataset.cmsType === 'HTMLField') { + this._createRTE(el); } else if (el.dataset.cmsType === 'CharField') { - this._generic_editors.push(new CmsTextEditor(el, { + // Creat simple generic text editor + this._generic_editors[el.id] = new CmsTextEditor(el, { spellcheck: el.dataset.spellcheck || 'false', }, (el) => this.saveData(el) - )); + ); } - this._editors.push(el); } // CMS Editor: initInlineEditors @@ -208,8 +149,8 @@ class CMSEditor { if (plugin[1].type === 'plugin' && plugin[1].plugin_type === 'TextPlugin') { // Text plugin - const elements = document.querySelectorAll('.cms-plugin.cms-plugin-' + id); - wrapper = this._initInlineRichText(elements, url, id); + const elements = document.querySelectorAll('.cms-plugin.' + plugin[0]); + wrapper = this._initInlineRichText(elements, url, plugin[0]); if (wrapper) { wrapper.dataset.cmsPluginId = id; wrapper.dataset.cmsType = 'TextPlugin'; @@ -224,7 +165,7 @@ class CMSEditor { const search_key = `${generic_class[2]}-${generic_class[3]}-${edit_fields}`; if (generic_inline_fields[search_key]) { // Inline editable? - wrapper = this._initInlineRichText(document.getElementsByClassName(plugin[0]), url, id); + wrapper = this._initInlineRichText(document.getElementsByClassName(plugin[0]), url, plugin[0]); if (wrapper) { wrapper.dataset.cmsCsrfToken = this.CMS.config.csrf; wrapper.dataset.cmsField = edit_fields; @@ -241,22 +182,25 @@ class CMSEditor { if (wrapper) { // Catch CMS single click event to highlight the plugin // Catch CMS double click event if present, since double click is needed by Editor - this.observer.observe(wrapper); - if (this.CMS) { - // Remove django CMS core's double click event handler which opens an edit dialog - this.CMS.$(wrapper).off('dblclick.cms.plugin') - .on('dblclick.cms-editor', function (event) { - event.stopPropagation(); - }); - wrapper.addEventListener('focusin.cms-editor', () => { - this._highlightTextplugin(id); - }, true); - // Prevent tooltip on hover - this.CMS.$(wrapper).off('pointerover.cms.plugin pointerout.cms.plugin') - .on('pointerover.cms-editor', function (event) { - window.CMS.API.Tooltip.displayToggle(false, event.target, '', id); - event.stopPropagation(); - }); + if (!Array.from(this.observer.root?.children || []).includes(wrapper)) { + // Only add to the observer if not already observed (e.g., if the page only was update partially) + this.observer.observe(wrapper); + if (this.CMS) { + // Remove django CMS core's double click event handler which opens an edit dialog + this.CMS.$(wrapper).off('dblclick.cms.plugin') + .on('dblclick.cms-editor', function (event) { + event.stopPropagation(); + }); + wrapper.addEventListener('focusin', () => { + this._highlightTextplugin(id); + }, true); + // Prevent tooltip on hover + this.CMS.$(wrapper).off('pointerover.cms.plugin pointerout.cms.plugin') + .on('pointerover.cms-editor', function (event) { + window.CMS.API.Tooltip.displayToggle(false, event.target, '', id); + event.stopPropagation(); + }); + } } } } @@ -271,17 +215,22 @@ class CMSEditor { }); } - _initInlineRichText(elements, url, id) { + _initInlineRichText(elements, url, cls) { let wrapper; if (elements.length > 0) { - if (elements.length === 1 && elements[0].tagName === 'DIV' || elements[0].tagName === 'CMS-PLUGIN') { + if (elements.length === 1 && ( + elements[0].tagName === 'DIV' || // Single wrapping div + elements[0].tagName === 'CMS-PLUGIN' || // Single wrapping cms-plugin tag + elements[0].classList.contains('cms-editor-inline-wrapper') // already wrapped + )) { // 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.classList.add('cms-plugin', cls); wrapper = this._wrapAll(elements, wrapper); } wrapper.dataset.cmsEditUrl = url; @@ -291,6 +240,73 @@ class CMSEditor { return undefined; } + _createRTE(el) { + const settings = this.getSettings(el); + // Element options overwrite + settings.options = Object.assign({}, + settings.options || {}, + JSON.parse(el.dataset.options || '{}') + ); + + // Add event listener to delete data on modal cancel + if (settings.revert_on_cancel) { + const CMS = this.CMS; + const csrf = CMS.config?.csrf || document.querySelector('input[name="csrfmiddlewaretoken"]').value; + CMS.API.Helpers.addEventListener( + 'modal-close.text-plugin.text-plugin-' + settings.plugin_id, + function(e, opts) { + if (!settings.revert_on_cancel || !settings.cancel_plugin_url) { + return; + } + CMS.$.ajax({ + method: 'POST', + url: settings.cancel_plugin_url, + data: { + token: settings.action_token, + csrfmiddlewaretoken: csrf + }, + }).done(function () { + CMS.API.Helpers.removeEventListener( + 'modal-close.text-plugin.text-plugin-' + settings.plugin_id + ); + opts.instance.close(); + }).fail(function (res) { + CMS.API.Messages.open({ + message: res.responseText + ' | ' + res.status + ' ' + res.statusText, + delay: 0, + error: true + }); + }); + + } + ); + } + const inModal = !!document.querySelector( + '.app-djangocms_text.model-text.change-form #' + el.id + ); + // Get content: json > textarea > innerHTML + let content; + + if (el.dataset.json) { + content = JSON.parse(el.dataset.json); + } else { + content = el.innerHTML; + } + if (el.tagName === 'TEXTAREA') { + el.visible = false; + content = el.value; + // el = el.insertAdjacentElement('afterend', document.createElement('div')); + } + + window.cms_editor_plugin.create( + el, + inModal, + content, settings, + el.tagName !== 'TEXTAREA' ? () => this.saveData(el) : () => { + } + ); + } + /** * Retrieves the settings for the given editor. * If the element is a string, it will be treated as an element's ID. @@ -315,11 +331,15 @@ class CMSEditor { this._editor_settings[el.id] = Object.assign( {}, this._global_settings, - JSON.parse(settings_el.textContent) || {} + JSON.parse(settings_el.textContent || '{}') + ); + } else { + this._editor_settings[el.id] = Object.assign( + {}, + this._global_settings, ); - return this._editor_settings[el.id]; } - return {}; + return this._editor_settings[el.id]; } /** @@ -336,11 +356,16 @@ class CMSEditor { // CMS Editor: destroy destroyAll() { - while (this._editors.length) { - const el = this._editors.pop(); - this.destroyGenericEditor(el); + this.destroyRTE(); + this.destroyGenericEditor(); + } + + destroyRTE() { + for (const el of Object.keys(this._editor_settings)) { + const element = document.getElementById(el); window.cms_editor_plugin.destroyEditor(el); } + this._editor_settings = {}; } // CMS Editor: destroyGenericEditor @@ -611,7 +636,6 @@ class CMSEditor { // CMS Editor: resetInlineEditors _resetInlineEditors () { - this.destroyAll(); this.initAll(); } @@ -627,7 +651,7 @@ class CMSEditor { } _highlightTextplugin (pluginId) { - const HIGHLIGHT_TIMEOUT = 800; + const HIGHLIGHT_TIMEOUT = 100; if (this.CMS) { const $ = this.CMS.$; @@ -683,5 +707,5 @@ class CMSEditor { // Create global editor object -window.CMS_Editor = new CMSEditor(); +window.CMS_Editor = window.CMS_Editor || new CMSEditor(); diff --git a/private/js/cms.tiptap.js b/private/js/cms.tiptap.js index d62e3387..54d125a5 100644 --- a/private/js/cms.tiptap.js +++ b/private/js/cms.tiptap.js @@ -285,4 +285,4 @@ class CMSTipTapPlugin { } -window.cms_editor_plugin = new CMSTipTapPlugin({}); +window.cms_editor_plugin = window.cms_editor_plugin || new CMSTipTapPlugin({}); diff --git a/private/js/tiptap_plugins/cms.toolbar.js b/private/js/tiptap_plugins/cms.toolbar.js index 79d22ef4..a4cb1f53 100644 --- a/private/js/tiptap_plugins/cms.toolbar.js +++ b/private/js/tiptap_plugins/cms.toolbar.js @@ -413,9 +413,7 @@ function _createToolbarButton(editor, itemName, filter) { `; } const content = repr.icon || `${repr.title}`; - return ``; + return ``; } return ''; } diff --git a/tests/js/cms.editor.test.js b/tests/js/cms.editor.test.js index 370b2d44..5d75e9f4 100644 --- a/tests/js/cms.editor.test.js +++ b/tests/js/cms.editor.test.js @@ -1,3 +1,7 @@ +/* eslint-env es11 */ +/* jshint esversion: 11 */ +/* global window, document, fetch, IntersectionObserver, URLSearchParams, console */ + import CMSEditor from '../../private/js/cms.editor'; import CMSTipTapPlugin from '../../private/js/cms.tiptap'; @@ -18,32 +22,42 @@ describe('CMSEditor', () => { beforeEach(() => { document.body.innerHTML = ` -
{"some": "config"}
+ `; + + window.dispatchEvent(new Event('DOMContentLoaded')); + editor = window.CMS_Editor; + editor._editor_settings = {}; + }); afterEach(() => { editor.destroyAll(); }); + it('reads global settings', () => { + editor.initAll(); + expect(window.CMS_Editor._global_settings).toEqual({some: 'config'}); + }); + it('initializes all editors on the page', () => { editor.initAll(); - expect(editor._editors.length).toBe(2); + expect(Object.keys(editor._editor_settings).length).toBe(2); }); it('initializes a single editor', () => { const el = document.getElementById('editor1'); editor.init(el); - expect(editor._editors.length).toBe(1); + expect(Object.keys(editor._editor_settings).length).toBe(1); }); it('destroys all editors', () => { editor.initAll(); editor.destroyAll(); - expect(editor._editors.length).toBe(0); + expect(Object.keys(editor._editor_settings).length).toBe(0); }); it('handles plugin form loading', (done) => { @@ -68,7 +82,7 @@ describe('CMSEditor', () => { it('resets inline editors', () => { editor.initAll(); editor._resetInlineEditors(); - expect(editor._editors.length).toBe(2); + expect(Object.keys(editor._editor_settings).length).toBe(2); }); it('highlights text plugin', () => { diff --git a/tests/js/cms.tiptap.toolbar.test.js b/tests/js/cms.tiptap.toolbar.test.js index bcb042f9..c1f3e91d 100644 --- a/tests/js/cms.tiptap.toolbar.test.js +++ b/tests/js/cms.tiptap.toolbar.test.js @@ -1,3 +1,7 @@ +/* eslint-env es11 */ +/* jshint esversion: 11 */ +/* global window, document, fetch, IntersectionObserver, URLSearchParams, console */ + import TiptapToolbar from "../../private/js/tiptap_plugins/cms.tiptap.toolbar"; import CMSEditor from '../../private/js/cms.editor'; import CMSTipTapPlugin from '../../private/js/cms.tiptap'; @@ -7,7 +11,7 @@ describe('Tiptap toolbar items', () => { beforeEach(() => { document.body.innerHTML = ` -
{"some": "config"}
+ `; editor = window.CMS_Editor; @@ -20,7 +24,7 @@ describe('Tiptap toolbar items', () => { it('initializes a single editor', () => { const el = document.getElementById('editor1'); editor.init(el); - expect(editor._editors.length).toBe(1); + expect(Object.keys(editor._editor_settings).length).toBe(1); }); it('can execute all commands', () => { diff --git a/tests/js/example.test.js b/tests/js/example.test.js deleted file mode 100644 index 3a541ba6..00000000 --- a/tests/js/example.test.js +++ /dev/null @@ -1,12 +0,0 @@ -describe('Example Test', () => { - test('basic test works', () => { - expect(true).toBe(true); - }); - - test('DOM testing works', () => { - document.body.innerHTML = '
Test Content
'; - const element = document.getElementById('test'); - expect(element).toBeInTheDocument(); - expect(element).toHaveTextContent('Test Content'); - }); -});