Skip to content

Commit 37f8957

Browse files
authored
fix: Handle partial page updates (#54)
1 parent e90fb53 commit 37f8957

File tree

9 files changed

+160
-122
lines changed

9 files changed

+160
-122
lines changed

CHANGELOG.rst

+8
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22
Changelog
33
=========
44

5+
0.5.3 (18-01-2025)
6+
==================
7+
* fix: Markup error disabled the block toolbar in inline editing
8+
* fix: Handle partial page updates
9+
510
0.5.2 (16-01-2025)
611
==================
712

13+
* feat: Add text color from list of options by @fsbraun in https://github.com/django-cms/djangocms-text/pull/48
14+
* 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
15+
* feat: Add configurable block and inline styles for Tiptap by @fsbraun in https://github.com/django-cms/djangocms-text/pull/51
816
* fix: Update CKEditor4 vendor files to work with CMS plugins
917
* fix: Update icon paths for CKEditor4
1018

djangocms_text/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
10. Github actions will publish the new package to pypi
1717
"""
1818

19-
__version__ = "0.5.2"
19+
__version__ = "0.5.3"

private/css/cms.toolbar.css

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
[role="menubar"] {
1919
bottom: calc(100% - 1px);
20+
min-width: 375px;
21+
z-index: 1;
2022
padding: 2px 0.4rem;
2123
/* border-radius: 3px; */
2224
margin: 0 !important;

private/js/cms.editor.js

+122-98
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ class CMSEditor {
1515
// CMS Editor: constructor
1616
// Initialize the editor object
1717
constructor() {
18-
this._editors = [];
19-
this._generic_editors = [];
2018
this._global_settings = {};
2119
this._editor_settings = {};
20+
this._generic_editors = {};
2221
this._admin_selector = 'textarea.CMS_Editor';
2322
this._admin_add_row_selector = 'body.change-form .add-row a';
2423
this._inline_admin_selector = 'body.change-form .form-row';
@@ -94,83 +93,25 @@ class CMSEditor {
9493
// CMS Editor: init
9594
// Initialize a single editor
9695
init (el) {
97-
let content;
9896

99-
// Get content: json > textarea > innerHTML
100-
if (el.dataset.json) {
101-
content = JSON.parse(el.dataset.json);
102-
} else {
103-
content = el.innerHTML;
104-
}
105-
if (el.tagName === 'TEXTAREA') {
106-
el.visible = false;
107-
content = el.value;
108-
// el = el.insertAdjacentElement('afterend', document.createElement('div'));
109-
}
11097
if (!el.id) {
11198
el.id = "cms-edit-" + Math.random().toString(36).slice(2, 9);
11299
}
113-
const settings = this.getSettings(el);
114-
// Element options overwrite
115-
settings.options = Object.assign({},
116-
settings.options || {},
117-
JSON.parse(el.dataset.options || '{}')
118-
);
119-
120-
// Add event listener to delete data on modal cancel
121-
if (settings.revert_on_cancel) {
122-
const CMS = this.CMS;
123-
const csrf = CMS.config?.csrf || document.querySelector('input[name="csrfmiddlewaretoken"]').value;
124-
CMS.API.Helpers.addEventListener(
125-
'modal-close.text-plugin.text-plugin-' + settings.plugin_id,
126-
function(e, opts) {
127-
if (!settings.revert_on_cancel || !settings.cancel_plugin_url) {
128-
return;
129-
}
130-
CMS.$.ajax({
131-
method: 'POST',
132-
url: settings.cancel_plugin_url,
133-
data: {
134-
token: settings.action_token,
135-
csrfmiddlewaretoken: csrf
136-
},
137-
}).done(function () {
138-
CMS.API.Helpers.removeEventListener(
139-
'modal-close.text-plugin.text-plugin-' + settings.plugin_id
140-
);
141-
opts.instance.close();
142-
}).fail(function (res) {
143-
CMS.API.Messages.open({
144-
message: res.responseText + ' | ' + res.status + ' ' + res.statusText,
145-
delay: 0,
146-
error: true
147-
});
148-
});
149-
150-
}
151-
);
100+
if (el.id in this._editor_settings) {
101+
// Already initialized - happens when an inline editor is scrolled back into the viewport
102+
return;
152103
}
153-
const inModal = !!document.querySelector(
154-
'.app-djangocms_text.model-text.change-form #' + el.id
155-
);
156-
157104
// Create editor
158-
if (!el.dataset.cmsType ||el.dataset.cmsType === 'TextPlugin' || el.dataset.cmsType === 'HTMLField') {
159-
window.cms_editor_plugin.create(
160-
el,
161-
inModal,
162-
content, settings,
163-
el.tagName !== 'TEXTAREA' ? () => this.saveData(el) : () => {
164-
}
165-
);
105+
if (!el.dataset.cmsType || el.dataset.cmsType === 'TextPlugin' || el.dataset.cmsType === 'HTMLField') {
106+
this._createRTE(el);
166107
} else if (el.dataset.cmsType === 'CharField') {
167-
this._generic_editors.push(new CmsTextEditor(el, {
108+
// Creat simple generic text editor
109+
this._generic_editors[el.id] = new CmsTextEditor(el, {
168110
spellcheck: el.dataset.spellcheck || 'false',
169111
},
170112
(el) => this.saveData(el)
171-
));
113+
);
172114
}
173-
this._editors.push(el);
174115
}
175116

176117
// CMS Editor: initInlineEditors
@@ -208,8 +149,8 @@ class CMSEditor {
208149

209150
if (plugin[1].type === 'plugin' && plugin[1].plugin_type === 'TextPlugin') {
210151
// Text plugin
211-
const elements = document.querySelectorAll('.cms-plugin.cms-plugin-' + id);
212-
wrapper = this._initInlineRichText(elements, url, id);
152+
const elements = document.querySelectorAll('.cms-plugin.' + plugin[0]);
153+
wrapper = this._initInlineRichText(elements, url, plugin[0]);
213154
if (wrapper) {
214155
wrapper.dataset.cmsPluginId = id;
215156
wrapper.dataset.cmsType = 'TextPlugin';
@@ -224,7 +165,7 @@ class CMSEditor {
224165
const search_key = `${generic_class[2]}-${generic_class[3]}-${edit_fields}`;
225166
if (generic_inline_fields[search_key]) {
226167
// Inline editable?
227-
wrapper = this._initInlineRichText(document.getElementsByClassName(plugin[0]), url, id);
168+
wrapper = this._initInlineRichText(document.getElementsByClassName(plugin[0]), url, plugin[0]);
228169
if (wrapper) {
229170
wrapper.dataset.cmsCsrfToken = this.CMS.config.csrf;
230171
wrapper.dataset.cmsField = edit_fields;
@@ -241,22 +182,25 @@ class CMSEditor {
241182
if (wrapper) {
242183
// Catch CMS single click event to highlight the plugin
243184
// Catch CMS double click event if present, since double click is needed by Editor
244-
this.observer.observe(wrapper);
245-
if (this.CMS) {
246-
// Remove django CMS core's double click event handler which opens an edit dialog
247-
this.CMS.$(wrapper).off('dblclick.cms.plugin')
248-
.on('dblclick.cms-editor', function (event) {
249-
event.stopPropagation();
250-
});
251-
wrapper.addEventListener('focusin.cms-editor', () => {
252-
this._highlightTextplugin(id);
253-
}, true);
254-
// Prevent tooltip on hover
255-
this.CMS.$(wrapper).off('pointerover.cms.plugin pointerout.cms.plugin')
256-
.on('pointerover.cms-editor', function (event) {
257-
window.CMS.API.Tooltip.displayToggle(false, event.target, '', id);
258-
event.stopPropagation();
259-
});
185+
if (!Array.from(this.observer.root?.children || []).includes(wrapper)) {
186+
// Only add to the observer if not already observed (e.g., if the page only was update partially)
187+
this.observer.observe(wrapper);
188+
if (this.CMS) {
189+
// Remove django CMS core's double click event handler which opens an edit dialog
190+
this.CMS.$(wrapper).off('dblclick.cms.plugin')
191+
.on('dblclick.cms-editor', function (event) {
192+
event.stopPropagation();
193+
});
194+
wrapper.addEventListener('focusin', () => {
195+
this._highlightTextplugin(id);
196+
}, true);
197+
// Prevent tooltip on hover
198+
this.CMS.$(wrapper).off('pointerover.cms.plugin pointerout.cms.plugin')
199+
.on('pointerover.cms-editor', function (event) {
200+
window.CMS.API.Tooltip.displayToggle(false, event.target, '', id);
201+
event.stopPropagation();
202+
});
203+
}
260204
}
261205
}
262206
}
@@ -271,17 +215,22 @@ class CMSEditor {
271215
});
272216
}
273217

274-
_initInlineRichText(elements, url, id) {
218+
_initInlineRichText(elements, url, cls) {
275219
let wrapper;
276220

277221
if (elements.length > 0) {
278-
if (elements.length === 1 && elements[0].tagName === 'DIV' || elements[0].tagName === 'CMS-PLUGIN') {
222+
if (elements.length === 1 && (
223+
elements[0].tagName === 'DIV' || // Single wrapping div
224+
elements[0].tagName === 'CMS-PLUGIN' || // Single wrapping cms-plugin tag
225+
elements[0].classList.contains('cms-editor-inline-wrapper') // already wrapped
226+
)) {
279227
// already wrapped?
280228
wrapper = elements[0];
281229
wrapper.classList.add('cms-editor-inline-wrapper');
282230
} else { // no, wrap now!
283231
wrapper = document.createElement('div');
284232
wrapper.classList.add('cms-editor-inline-wrapper', 'wrapped');
233+
wrapper.classList.add('cms-plugin', cls);
285234
wrapper = this._wrapAll(elements, wrapper);
286235
}
287236
wrapper.dataset.cmsEditUrl = url;
@@ -291,6 +240,73 @@ class CMSEditor {
291240
return undefined;
292241
}
293242

243+
_createRTE(el) {
244+
const settings = this.getSettings(el);
245+
// Element options overwrite
246+
settings.options = Object.assign({},
247+
settings.options || {},
248+
JSON.parse(el.dataset.options || '{}')
249+
);
250+
251+
// Add event listener to delete data on modal cancel
252+
if (settings.revert_on_cancel) {
253+
const CMS = this.CMS;
254+
const csrf = CMS.config?.csrf || document.querySelector('input[name="csrfmiddlewaretoken"]').value;
255+
CMS.API.Helpers.addEventListener(
256+
'modal-close.text-plugin.text-plugin-' + settings.plugin_id,
257+
function(e, opts) {
258+
if (!settings.revert_on_cancel || !settings.cancel_plugin_url) {
259+
return;
260+
}
261+
CMS.$.ajax({
262+
method: 'POST',
263+
url: settings.cancel_plugin_url,
264+
data: {
265+
token: settings.action_token,
266+
csrfmiddlewaretoken: csrf
267+
},
268+
}).done(function () {
269+
CMS.API.Helpers.removeEventListener(
270+
'modal-close.text-plugin.text-plugin-' + settings.plugin_id
271+
);
272+
opts.instance.close();
273+
}).fail(function (res) {
274+
CMS.API.Messages.open({
275+
message: res.responseText + ' | ' + res.status + ' ' + res.statusText,
276+
delay: 0,
277+
error: true
278+
});
279+
});
280+
281+
}
282+
);
283+
}
284+
const inModal = !!document.querySelector(
285+
'.app-djangocms_text.model-text.change-form #' + el.id
286+
);
287+
// Get content: json > textarea > innerHTML
288+
let content;
289+
290+
if (el.dataset.json) {
291+
content = JSON.parse(el.dataset.json);
292+
} else {
293+
content = el.innerHTML;
294+
}
295+
if (el.tagName === 'TEXTAREA') {
296+
el.visible = false;
297+
content = el.value;
298+
// el = el.insertAdjacentElement('afterend', document.createElement('div'));
299+
}
300+
301+
window.cms_editor_plugin.create(
302+
el,
303+
inModal,
304+
content, settings,
305+
el.tagName !== 'TEXTAREA' ? () => this.saveData(el) : () => {
306+
}
307+
);
308+
}
309+
294310
/**
295311
* Retrieves the settings for the given editor.
296312
* If the element is a string, it will be treated as an element's ID.
@@ -315,11 +331,15 @@ class CMSEditor {
315331
this._editor_settings[el.id] = Object.assign(
316332
{},
317333
this._global_settings,
318-
JSON.parse(settings_el.textContent) || {}
334+
JSON.parse(settings_el.textContent || '{}')
335+
);
336+
} else {
337+
this._editor_settings[el.id] = Object.assign(
338+
{},
339+
this._global_settings,
319340
);
320-
return this._editor_settings[el.id];
321341
}
322-
return {};
342+
return this._editor_settings[el.id];
323343
}
324344

325345
/**
@@ -336,11 +356,16 @@ class CMSEditor {
336356

337357
// CMS Editor: destroy
338358
destroyAll() {
339-
while (this._editors.length) {
340-
const el = this._editors.pop();
341-
this.destroyGenericEditor(el);
359+
this.destroyRTE();
360+
this.destroyGenericEditor();
361+
}
362+
363+
destroyRTE() {
364+
for (const el of Object.keys(this._editor_settings)) {
365+
const element = document.getElementById(el);
342366
window.cms_editor_plugin.destroyEditor(el);
343367
}
368+
this._editor_settings = {};
344369
}
345370

346371
// CMS Editor: destroyGenericEditor
@@ -611,7 +636,6 @@ class CMSEditor {
611636

612637
// CMS Editor: resetInlineEditors
613638
_resetInlineEditors () {
614-
this.destroyAll();
615639
this.initAll();
616640
}
617641

@@ -627,7 +651,7 @@ class CMSEditor {
627651
}
628652

629653
_highlightTextplugin (pluginId) {
630-
const HIGHLIGHT_TIMEOUT = 800;
654+
const HIGHLIGHT_TIMEOUT = 100;
631655

632656
if (this.CMS) {
633657
const $ = this.CMS.$;
@@ -683,5 +707,5 @@ class CMSEditor {
683707

684708

685709
// Create global editor object
686-
window.CMS_Editor = new CMSEditor();
710+
window.CMS_Editor = window.CMS_Editor || new CMSEditor();
687711

private/js/cms.tiptap.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,4 @@ class CMSTipTapPlugin {
285285
}
286286

287287

288-
window.cms_editor_plugin = new CMSTipTapPlugin({});
288+
window.cms_editor_plugin = window.cms_editor_plugin || new CMSTipTapPlugin({});

private/js/tiptap_plugins/cms.toolbar.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -413,9 +413,7 @@ function _createToolbarButton(editor, itemName, filter) {
413413
</form>`;
414414
}
415415
const content = repr.icon || `<span>${repr.title}</span>`;
416-
return `<button tabindex="-1" data-action="${repr.dataaction}" ${cmsplugin}${title}${position}class="${classes}">
417-
${content}${form}
418-
</button>`;
416+
return `<button data-action="${repr.dataaction}" ${cmsplugin}${title}${position}class="${classes}">${content}${form}</button>`;
419417
}
420418
return '';
421419
}

0 commit comments

Comments
 (0)