Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Handle partial page updates #54

Merged
merged 6 commits into from
Jan 18, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion djangocms_text/__init__.py
Original file line number Diff line number Diff line change
@@ -16,4 +16,4 @@
10. Github actions will publish the new package to pypi
"""

__version__ = "0.5.2"
__version__ = "0.5.3"
2 changes: 2 additions & 0 deletions private/css/cms.toolbar.css
Original file line number Diff line number Diff line change
@@ -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;
220 changes: 122 additions & 98 deletions private/js/cms.editor.js
Original file line number Diff line number Diff line change
@@ -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();
});
}
}
}
Comment on lines 182 to 205
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
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();
});
}
}
}
if (wrapper && !Array.from(this.observer.root?.children || []).includes(wrapper)) {
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();
});
}
}


ExplanationReading deeply nested conditional code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

}
@@ -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;
fsbraun marked this conversation as resolved.
Show resolved Hide resolved
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();

2 changes: 1 addition & 1 deletion private/js/cms.tiptap.js
Original file line number Diff line number Diff line change
@@ -285,4 +285,4 @@ class CMSTipTapPlugin {
}


window.cms_editor_plugin = new CMSTipTapPlugin({});
window.cms_editor_plugin = window.cms_editor_plugin || new CMSTipTapPlugin({});
4 changes: 1 addition & 3 deletions private/js/tiptap_plugins/cms.toolbar.js
Original file line number Diff line number Diff line change
@@ -413,9 +413,7 @@ function _createToolbarButton(editor, itemName, filter) {
</form>`;
}
const content = repr.icon || `<span>${repr.title}</span>`;
return `<button tabindex="-1" data-action="${repr.dataaction}" ${cmsplugin}${title}${position}class="${classes}">
${content}${form}
</button>`;
return `<button data-action="${repr.dataaction}" ${cmsplugin}${title}${position}class="${classes}">${content}${form}</button>`;
}
return '';
}
24 changes: 19 additions & 5 deletions tests/js/cms.editor.test.js
Original file line number Diff line number Diff line change
@@ -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 = `
<div id="cms-editor-cfg">{"some": "config"}</div>
<script id="cms-editor-cfg" type="application/json">{"some": "config"}</script>
<textarea class="CMS_Editor" id="editor1"></textarea>
<textarea class="CMS_Editor" id="editor2"></textarea>
`;

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', () => {
8 changes: 6 additions & 2 deletions tests/js/cms.tiptap.toolbar.test.js
Original file line number Diff line number Diff line change
@@ -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 = `
<div id="cms-editor-cfg">{"some": "config"}</div>
<script id="cms-editor-cfg" type="application/json">{"some": "config"}</script>
<textarea class="CMS_Editor" id="editor1"></textarea>
`;
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', () => {
12 changes: 0 additions & 12 deletions tests/js/example.test.js

This file was deleted.