From e3ed896fbd3bacae8b22403c0a53bd0cdd5d7ae4 Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Mon, 24 Oct 2022 15:39:11 +0000 Subject: [PATCH] [FIX] web_editor: prevent some class to be used in history When a change was added within a field like adding a class (eg. o_dirty, o_transform_removal, or oe_edited_link), it triggered the `_observeOdooFieldChanges` mechanism that replace all identical fields from the first mutated field in the set of identical fields. The bug appeared when there were two identical in the pages - field A: first to appear in the document - field B: second to appear in the document When clicking on a link inside field B, multiples class could be added (o_dirty, o_transform_removal, or oe_edited_link) within the fields. Field A and field B were mutated. This activated the mechanism `_observeOdooFieldChanges` and replaced the innerHTML of field B with innerHTML of field A. That replacement of innerHTML made the reference of the anchor node in the link tool being outdated and changes made inside that link tool ineffective. Step to reproduce: - Install website - Go to any page and enter edit mode - Click on the navbar and in the right panel select Vertical header - Activate the CTA option in that navbar right panel (should already be) - Click on the CTA button - Try to edit the URL in the right panel or any link option (style etc) The link will not update properly and the history undo create a new link. This commit add the ability in the editor to prevent classes from being taken into consideration in the filterMutationRecords and in undo/redo and when receiving external steps. closes odoo/odoo#104872 X-original-commit: e07e149e01646cc2e59c8ac205a5047fd74ae0ba Signed-off-by: David Monjoie (dmo) --- .../js/editor/odoo-editor/src/OdooEditor.js | 35 +++++++++++-- .../odoo-editor/test/spec/editor.test.js | 50 +++++++++++++++++++ .../static/src/js/wysiwyg/wysiwyg.js | 8 ++- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js index 5a5150d3808..82d3f3e7ec2 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js @@ -240,6 +240,7 @@ export class OdooEditor extends EventTarget { direction: 'ltr', _t: string => string, allowCommandVideo: true, + renderingClasses: [], }, options, ); @@ -902,6 +903,24 @@ export class OdooEditor extends EventTarget { } attributeCache.set(record.target, attributeCache.get(record.target) || {}); + if (record.attributeName === 'class') { + const classBefore = (record.oldValue && record.oldValue.split(' ')) || []; + const classAfter = (record.target.className && record.target.className.split && record.target.className.split(' ')) || []; + const excludedClasses = []; + for (const klass of classBefore) { + if (!classAfter.includes(klass)) { + excludedClasses.push(klass); + } + } + for (const klass of classAfter) { + if (!classBefore.includes(klass)) { + excludedClasses.push(klass); + } + } + if (excludedClasses.every(c => this.options.renderingClasses.includes(c))) { + continue; + } + } if ( typeof attributeCache.get(record.target)[record.attributeName] === 'undefined' ) { @@ -1026,10 +1045,14 @@ export class OdooEditor extends EventTarget { } else if (record.type === 'attributes') { const node = this.idFind(record.id); if (node) { + let value = record.value; + if (typeof value === 'string' && record.attributeName === 'class') { + value = value.split(' ').filter(c => !this.options.renderingClasses.includes(c)).join(' '); + } if (this._collabClientId) { - this._safeSetAttribute(node, record.attributeName, record.value); + this._safeSetAttribute(node, record.attributeName, value); } else { - node.setAttribute(record.attributeName, record.value); + node.setAttribute(record.attributeName, value); } } } else if (record.type === 'remove') { @@ -1166,10 +1189,14 @@ export class OdooEditor extends EventTarget { const node = this.idFind(mutation.id); if (node) { if (mutation.oldValue) { + let value = mutation.oldValue; + if (typeof value === 'string' && mutation.attributeName === 'class') { + value = value.split(' ').filter(c => !this.options.renderingClasses.includes(c)).join(' '); + } if (this._collabClientId) { - this._safeSetAttribute(node, mutation.attributeName, mutation.oldValue); + this._safeSetAttribute(node, mutation.attributeName, value); } else { - node.setAttribute(mutation.attributeName, mutation.oldValue); + node.setAttribute(mutation.attributeName, value); } } else { node.removeAttribute(mutation.attributeName); diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js index d133fe3eda7..fbb51015c46 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/editor.test.js @@ -3671,6 +3671,56 @@ X[] }); }); }); + describe('prevent renderingClasses to be set from history', () => { + it('should prevent renderingClasses to be added', async () => { + await testEditor(BasicEditor, { + contentBefore: `

a

`, + stepFunction: async editor => { + const p = editor.editable.querySelector('p'); + p.className = 'x'; + editor.observerFlush(); + editor.historyStep(); + window.chai.expect(editor._historySteps.length).to.eql(1); + }, + }, { + renderingClasses: ['x'] + }); + }); + it('should prevent renderingClasses to be added when adding 2 classes', async () => { + await testEditor(BasicEditor, { + contentBefore: `

a

`, + stepFunction: async editor => { + const p = editor.editable.querySelector('p'); + p.className = 'x y'; + editor.observerFlush(); + editor.historyStep(); + editor.historyUndo(); + editor.historyRedo(); + }, + contentAfter: `

a

`, + }, { + renderingClasses: ['x'] + }); + }); + it('should prevent renderingClasses to be added in historyApply', async () => { + await testEditor(BasicEditor, { + contentBefore: `

a

`, + stepFunction: async editor => { + const p = editor.editable.querySelector('p'); + editor.historyApply([{ + attributeName:"class", + id: p.oid, + oldValue: null, + type:"attributes", + value: "x y", + }]); + }, + contentAfter: `

a

`, + }, { + renderingClasses: ['x'] + }); + }); + }); }); describe('columnize', () => { diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index d1df8a32a94..bfce8c259bc 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -204,6 +204,7 @@ const Wysiwyg = Widget.extend({ plugins: options.editorPlugins, direction: options.direction || localization.direction || 'ltr', collaborationClientAvatarUrl: `${browser.location.origin}/web/image?model=res.users&field=avatar_128&id=${this.getSession().uid}`, + renderingClasses: ['o_dirty', 'o_transform_removal', 'oe_edited_link'], }, editorCollaborationOptions)); this.odooEditor.addEventListener('contentChanged', function () { @@ -1021,6 +1022,7 @@ const Wysiwyg = Widget.extend({ subtree: true, attributes: true, characterData: true, + attributeOldValue: true, }; if (this.odooFieldObservers) { for (let observerData of this.odooFieldObservers) { @@ -1032,7 +1034,11 @@ const Wysiwyg = Widget.extend({ this.odooFieldObservers = []; $odooFields.each((i, field) => { - const observer = new MutationObserver(() => { + const observer = new MutationObserver((mutations) => { + mutations = this.odooEditor.filterMutationRecords(mutations); + if (!mutations.length) { + return; + } let $node = $(field); let $nodes = $odooFields.filter(function () { return this !== field;