From 119fa9f974a07c5b378448b26d93de3424cea689 Mon Sep 17 00:00:00 2001 From: Nicolas Bayet Date: Thu, 11 Jan 2024 09:48:37 +0100 Subject: [PATCH] [FIX] web_editor: fix isDirty check on html fields The method `getValue` of the wysiwyg could have some cleaning on the value that will always make it different from the original value. The _isDirty method of the html field will now make the original value be processed by the wysiwyg `getValue` to be able to compare it with the current value. The sanizite call in `OdooEditor.cleanForSave` had the wrong root. As we are calling `cleanForSave` for an element that is not inside the editable, the root cannot be `this.editable`. task-3679122 closes odoo/odoo#148996 Signed-off-by: Antoine Guenet (age) --- .../static/src/js/backend/html_field.js | 26 ++++++++++++-- .../js/editor/odoo-editor/src/OdooEditor.js | 2 +- .../static/tests/html_field_tests.js | 35 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/addons/web_editor/static/src/js/backend/html_field.js b/addons/web_editor/static/src/js/backend/html_field.js index 563d7a69b03..10080d5c9ef 100644 --- a/addons/web_editor/static/src/js/backend/html_field.js +++ b/addons/web_editor/static/src/js/backend/html_field.js @@ -420,9 +420,29 @@ export class HtmlField extends Component { const strippedPropValue = stripHistoryIds(String(this.props.record.data[this.props.name])); const strippedEditingValue = stripHistoryIds(this.getEditingValue()); const domParser = new DOMParser(); - const parsedPropValue = domParser.parseFromString(strippedPropValue || '


', 'text/html').body; - const parsedEditingValue = domParser.parseFromString(strippedEditingValue, 'text/html').body; - return !this.props.readonly && parsedPropValue.innerHTML !== parsedEditingValue.innerHTML; + const codeViewEl = this._getCodeViewEl(); + let parsedPreviousValue; + // If the wysiwyg is active, we need to clean the content of the + // initialValue as the editingValue will be cleaned. + if (!codeViewEl && this.wysiwyg) { + const editable = domParser.parseFromString(strippedPropValue || '


', 'text/html').body; + // Temporarily append the editable to the DOM because the + // wysiwyg.getValue can indirectly call methods that needs to have + // access the node.ownerDocument.defaultView.getComputedStyle. + // By appending the editable to the dom, the node.ownerDocument will + // have a `defaultView`. + const div = document.createElement('div'); + div.style.display = 'none'; + div.append(editable); + document.body.append(div); + const editableValue = this.wysiwyg.getValue({ $layout: $(editable) }); + div.remove(); + parsedPreviousValue = domParser.parseFromString(editableValue, 'text/html').body; + } else { + parsedPreviousValue = domParser.parseFromString(strippedPropValue || '


', 'text/html').body; + } + const parsedNewValue = domParser.parseFromString(strippedEditingValue, 'text/html').body; + return !this.props.readonly && parsedPreviousValue.innerHTML !== parsedNewValue.innerHTML; } _getCodeViewEl() { return this.state.showCodeView && this.codeViewRef.el; 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 d96b274477b..f79ec8456fe 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 @@ -4330,7 +4330,7 @@ export class OdooEditor extends EventTarget { node.replaceChildren(); } - sanitize(element, this.editable); + sanitize(element); // Remove contenteditable=false on elements for (const el of element.querySelectorAll('[contenteditable="false"]')) { diff --git a/addons/web_editor/static/tests/html_field_tests.js b/addons/web_editor/static/tests/html_field_tests.js index 3b53e8312c2..941d92d7659 100644 --- a/addons/web_editor/static/tests/html_field_tests.js +++ b/addons/web_editor/static/tests/html_field_tests.js @@ -941,4 +941,39 @@ QUnit.module("WebEditor.HtmlField", ({ beforeEach }) => { assert.strictEqual(p.innerText.replaceAll('\u200B', ''), 'New label', "The link's label should be updated"); }); + + QUnit.module("isDirty"); + + QUnit.test("isDirty should be false when the content is being transformed by the wysiwyg", async (assert) => { + assert.expect(2); + + serverData.models.partner.records.push({ + id: 1, + txt: "

abc

", + }); + let htmlField; + const wysiwygPromise = makeDeferred(); + patchWithCleanup(HtmlField.prototype, { + async startWysiwyg() { + await super.startWysiwyg(...arguments); + htmlField = this; + wysiwygPromise.resolve(); + } + }); + await makeView({ + type: "form", + resId: 1, + resModel: "partner", + serverData, + arch: ` +
+ + `, + }); + await wysiwygPromise; + + assert.strictEqual(htmlField.wysiwyg.getValue(), '

abc

', 'the value should be sanitized by the wysiwyg'); + assert.strictEqual(htmlField._isDirty(), false, 'should not be dirty as the content has not changed'); + + }); });