[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) <dmo@odoo.com>
This commit is contained in:
Nicolas Bayet
2022-11-03 16:43:39 +01:00
parent f4a95639d4
commit e3ed896fbd
3 changed files with 88 additions and 5 deletions
@@ -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);
@@ -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: `<p>a</p>`,
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: `<p>a</p>`,
stepFunction: async editor => {
const p = editor.editable.querySelector('p');
p.className = 'x y';
editor.observerFlush();
editor.historyStep();
editor.historyUndo();
editor.historyRedo();
},
contentAfter: `<p class="y">a</p>`,
}, {
renderingClasses: ['x']
});
});
it('should prevent renderingClasses to be added in historyApply', async () => {
await testEditor(BasicEditor, {
contentBefore: `<p>a</p>`,
stepFunction: async editor => {
const p = editor.editable.querySelector('p');
editor.historyApply([{
attributeName:"class",
id: p.oid,
oldValue: null,
type:"attributes",
value: "x y",
}]);
},
contentAfter: `<p class="y">a</p>`,
}, {
renderingClasses: ['x']
});
});
});
});
describe('columnize', () => {
@@ -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;