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 bed37dc3565..676fc314321 100644
--- a/addons/web_editor/static/src/js/backend/html_field.js
+++ b/addons/web_editor/static/src/js/backend/html_field.js
@@ -74,6 +74,12 @@ export class HtmlFieldWysiwygAdapterComponent extends ComponentAdapter {
export class HtmlField extends Component {
setup() {
+ // readonly if nodes in
as the head will be removed on insertion in the DOM
+ // this prevents the field from overwriting the value with invalid HTML when relevant
+ const domParser = new DOMParser();
+ const parsedOriginal = domParser.parseFromString(this.props.record.data[this.props.name] || '', 'text/html');
+ this.containsComplexHTML = !!parsedOriginal.head.innerHTML.trim();
+
this.readonlyElementRef = useRef("readonlyElement");
this.codeViewRef = useRef("codeView");
this.iframeRef = useRef("iframe");
@@ -326,19 +332,21 @@ export class HtmlField extends Component {
}
/**
* Toggle the code view and update the UI.
- *
- * @param {JQuery} $codeview
*/
toggleCodeView() {
this.state.showCodeView = !this.state.showCodeView;
- this.wysiwyg.odooEditor.observerUnactive('toggleCodeView');
- if (this.state.showCodeView) {
- this.wysiwyg.odooEditor.toolbarHide();
- const value = this.wysiwyg.getValue();
- this.props.record.update({ [this.props.name]: value });
- } else {
- this.wysiwyg.odooEditor.observerActive('toggleCodeView');
+ if (this.wysiwyg) {
+ this.wysiwyg.odooEditor.observerUnactive('toggleCodeView');
+ if (this.state.showCodeView) {
+ this.wysiwyg.odooEditor.toolbarHide();
+ const value = this.wysiwyg.getValue();
+ this.props.record.update({ [this.props.name]: value });
+ } else {
+ this.wysiwyg.odooEditor.observerActive('toggleCodeView');
+ }
+ }
+ if (!this.state.showCodeView) {
const $codeview = $(this.codeViewRef.el);
const value = $codeview.val();
this.props.record.update({ [this.props.name]: value });
diff --git a/addons/web_editor/static/src/js/backend/html_field.xml b/addons/web_editor/static/src/js/backend/html_field.xml
index 59e79040520..1c287cd22d6 100644
--- a/addons/web_editor/static/src/js/backend/html_field.xml
+++ b/addons/web_editor/static/src/js/backend/html_field.xml
@@ -2,7 +2,7 @@
-
+
@@ -13,11 +13,6 @@
+
+
+
diff --git a/addons/web_editor/static/tests/html_field_tests.js b/addons/web_editor/static/tests/html_field_tests.js
new file mode 100644
index 00000000000..876312599f1
--- /dev/null
+++ b/addons/web_editor/static/tests/html_field_tests.js
@@ -0,0 +1,126 @@
+/** @odoo-module **/
+
+import { click, editInput, getFixture, makeDeferred, patchWithCleanup } from "@web/../tests/helpers/utils";
+import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+import { HtmlField } from "@web_editor/js/backend/html_field";
+import { onRendered } from "@odoo/owl";
+
+QUnit.module("WebEditor.HtmlField", ({ beforeEach }) => {
+ let serverData;
+ let target;
+
+ beforeEach(() => {
+ serverData = {
+ models: {
+ partner: {
+ fields: {
+ txt: { string: "txt", type: "html", trim: true },
+ },
+ records: [],
+ },
+ },
+ };
+ target = getFixture();
+
+ setupViewRegistries();
+ });
+
+ /**
+ * Check that documents with data in a node are set to readonly
+ * with a codeview option.
+ */
+ QUnit.test("html fields with complete HTML document", async (assert) => {
+ assert.timeout(2000);
+ assert.expect(12);
+ let codeViewState = false;
+ let togglePromiseId = 0;
+ const togglePromises = [makeDeferred(), makeDeferred()];
+ patchWithCleanup(HtmlField.prototype, {
+ setup: function () {
+ this._super(...arguments);
+ onRendered(() => {
+ if (codeViewState !== this.state.showCodeView) {
+ togglePromises[togglePromiseId].resolve();
+ }
+ codeViewState = this.state.showCodeView;
+ });
+ },
+ });
+ const htmlDocumentTextTemplate = (text, color) => `
+
+
+
+
+
+
+
+
+
+ ${text}
+
+
+ `;
+ serverData.models.partner.records = [{
+ id: 1,
+ txt: htmlDocumentTextTemplate('Hello', 'red'),
+ }];
+ const writePromise = makeDeferred();
+ await makeView({
+ type: "form",
+ resId: 1,
+ resModel: "partner",
+ serverData,
+ arch: `
+ `,
+ mockRPC(route, args) {
+ if (args.method === "write" && args.model === 'partner') {
+ assert.equal(args.args[1].txt, htmlDocumentTextTemplate('Hi', 'black'));
+ writePromise.resolve();
+ }
+ }
+ });
+
+ const fieldHtml = target.querySelector('.o_field_html');
+ let readonlyNode = fieldHtml.querySelector('.o_readonly');
+ assert.ok(readonlyNode);
+ assert.equal(readonlyNode.innerText, 'Hello');
+ assert.equal(window.getComputedStyle(readonlyNode).color, 'rgb(255, 0, 0)');
+
+ const codeViewButton = fieldHtml.querySelector('.o_codeview_btn');
+ assert.ok(codeViewButton);
+
+ await click(codeViewButton);
+ await togglePromises[togglePromiseId];
+ const codeView = fieldHtml.querySelector('textarea.o_codeview');
+ assert.ok(codeView);
+ assert.equal(codeView.value, htmlDocumentTextTemplate('Hello', 'red'));
+
+ await editInput(codeView, null, htmlDocumentTextTemplate('Hi', 'black'));
+
+ assert.ok(codeViewButton);
+ togglePromiseId++;
+ await click(codeViewButton);
+ await togglePromises[togglePromiseId];
+ readonlyNode = fieldHtml.querySelector('.o_readonly');
+ assert.ok(readonlyNode);
+ assert.equal(readonlyNode.innerText, 'Hi');
+ assert.equal(window.getComputedStyle(readonlyNode).color, 'rgb(0, 0, 0)');
+
+ const saveButton = target.querySelector('.o_form_button_save');
+ assert.ok(saveButton);
+ await click(saveButton);
+ await writePromise;
+ });
+});