summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorabd-msyukyu-odoo <abd@odoo.com>2025-01-10 18:20:25 +0100
committerabd-msyukyu-odoo <abd@odoo.com>2025-02-04 08:33:05 +0000
commitbb140ee6a741625224596deaaa37648732ab4019 (patch)
tree37cd1170d94cbda37ec9bda3bd9f58ccde8d3da0
parentb93409ac067f8e2c3cc9e02afe638eea568ff565 (diff)
[IMP] html_editor: protect non-editable qweb elements
Qweb nodes using `t-out`, `t-esc`, `t-raw`, `t-field` were set as `contenteditable='false'`, but this didn't prevent the editor to make considerations such as handling such nodes as empty (since they have no children). But even if they are technically empty, such nodes are visible for the user (i.e. a `t-field` will always display the name of the field). Which resulted in weird cases where the command hint would be displayed with an overlap with the field name in case of an empty `t-field`. This commit sets such nodes as `data-oe-protected="true"`, so that the nodes are considered as "black boxes" for the editor, which will not be able to make such considerations anymore. A protecting node is set to `contenteditable='false'` like before. A side-effect of this is that mutations inside these nodes are not registered as history steps anymore, but their content was not supposed to be changed so this is currently not an issue. In cases of Qweb elements, the user should be able to click on them to open the special toolbar. This commit overrides the filtering for event handlers on protected nodes in that case to allow opening the custom toolbar for Qweb elements such as `t-field`. task-4294440 Part-of: odoo/odoo#185340 Related: odoo/enterprise#75187 Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
-rw-r--r--addons/html_editor/static/src/others/qweb_plugin.js23
-rw-r--r--addons/html_editor/static/src/plugin.js11
-rw-r--r--addons/html_editor/static/tests/qweb.test.js20
3 files changed, 35 insertions, 19 deletions
diff --git a/addons/html_editor/static/src/others/qweb_plugin.js b/addons/html_editor/static/src/others/qweb_plugin.js
index 0894caccf29..ebcb7886939 100644
--- a/addons/html_editor/static/src/others/qweb_plugin.js
+++ b/addons/html_editor/static/src/others/qweb_plugin.js
@@ -19,17 +19,20 @@ const isUnsplittableQWebElement = (node) =>
"t-raw",
].some((attr) => node.getAttribute(attr)));
+const PROTECTED_QWEB_SELECTOR = "[t-esc], [t-raw], [t-out], [t-field]";
+
export class QWebPlugin extends Plugin {
static id = "qweb";
- static dependencies = ["overlay", "selection"];
+ static dependencies = ["overlay", "protectedNode", "selection"];
resources = {
/** Handlers */
selectionchange_handlers: this.onSelectionChange.bind(this),
clean_handlers: this.clearDataAttributes.bind(this),
clean_for_save_handlers: ({ root }) => {
this.clearDataAttributes(root);
- for (const element of root.querySelectorAll("[t-esc], [t-raw], [t-out], [t-field]")) {
+ for (const element of root.querySelectorAll(PROTECTED_QWEB_SELECTOR)) {
element.removeAttribute("contenteditable");
+ delete element.dataset.oeProtected;
}
},
normalize_handlers: this.normalize.bind(this),
@@ -64,6 +67,18 @@ export class QWebPlugin extends Plugin {
return true;
}
+ isValidTargetForDomListener(ev) {
+ if (
+ ev.type === "click" &&
+ ev.target &&
+ closestElement(ev.target, PROTECTED_QWEB_SELECTOR)
+ ) {
+ // Allow clicking on a protected QWEB node to open the custom toolbar.
+ return true;
+ }
+ return super.isValidTargetForDomListener(ev);
+ }
+
/**
* @param { SelectionData } selectionData
*/
@@ -89,8 +104,8 @@ export class QWebPlugin extends Plugin {
normalize(root) {
this.normalizeInline(root);
- for (const element of selectElements(root, "[t-esc], [t-raw], [t-out], [t-field]")) {
- element.setAttribute("contenteditable", "false");
+ for (const element of selectElements(root, PROTECTED_QWEB_SELECTOR)) {
+ this.dependencies.protectedNode.setProtectingNode(element, true);
}
this.applyGroupQwebBranching(root);
}
diff --git a/addons/html_editor/static/src/plugin.js b/addons/html_editor/static/src/plugin.js
index 2aa7f670844..8b73490d1e8 100644
--- a/addons/html_editor/static/src/plugin.js
+++ b/addons/html_editor/static/src/plugin.js
@@ -37,12 +37,13 @@ export class Plugin {
setup() {}
- addDomListener(target, eventName, fn, capture) {
+ isValidTargetForDomListener(ev) {
+ return !isProtecting(ev.target) && (!isProtected(ev.target) || isUnprotecting(ev.target));
+ }
+
+ addDomListener(target, eventName, fn, capture = false) {
const handler = (ev) => {
- if (
- !isProtecting(ev.target) &&
- (!isProtected(ev.target) || isUnprotecting(ev.target))
- ) {
+ if (this.isValidTargetForDomListener(ev)) {
fn?.call(this, ev);
}
};
diff --git a/addons/html_editor/static/tests/qweb.test.js b/addons/html_editor/static/tests/qweb.test.js
index 09239084030..828baeb6b1e 100644
--- a/addons/html_editor/static/tests/qweb.test.js
+++ b/addons/html_editor/static/tests/qweb.test.js
@@ -223,14 +223,14 @@ test("select text inside t-out", async () => {
config,
});
expect(getContent(el)).toBe(
- `<div><t t-out="test" data-oe-t-inline="true" contenteditable="false">Hello</t></div>`
+ `<div><t t-out="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t></div>`
);
setSelection({ anchorNode: el.querySelector("t[t-out]").childNodes[0], anchorOffset: 1 });
await tick();
expect(getContent(el)).toBe(
- `<div>[<t t-out="test" data-oe-t-inline="true" contenteditable="false">Hello</t>]</div>`
+ `<div>[<t t-out="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t>]</div>`
);
});
@@ -239,14 +239,14 @@ test("select text inside t-esc", async () => {
config,
});
expect(getContent(el)).toBe(
- `<div><t t-esc="test" data-oe-t-inline="true" contenteditable="false">Hello</t></div>`
+ `<div><t t-esc="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t></div>`
);
setSelection({ anchorNode: el.querySelector("t[t-esc]").childNodes[0], anchorOffset: 1 });
await tick();
expect(getContent(el)).toBe(
- `<div>[<t t-esc="test" data-oe-t-inline="true" contenteditable="false">Hello</t>]</div>`
+ `<div>[<t t-esc="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t>]</div>`
);
});
@@ -255,14 +255,14 @@ test("select text inside t-field", async () => {
config,
});
expect(getContent(el)).toBe(
- `<div><t t-field="test" data-oe-t-inline="true" contenteditable="false">Hello</t></div>`
+ `<div><t t-field="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t></div>`
);
setSelection({ anchorNode: el.querySelector("t[t-field]").childNodes[0], anchorOffset: 1 });
await tick();
expect(getContent(el)).toBe(
- `<div>[<t t-field="test" data-oe-t-inline="true" contenteditable="false">Hello</t>]</div>`
+ `<div>[<t t-field="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t>]</div>`
);
});
@@ -281,10 +281,10 @@ test("cleaning removes content editable", async () => {
);
expect(getContent(el)).toBe(`
<div>
- <t t-field="test" data-oe-t-inline="true" contenteditable="false">Hello</t>
- <t t-out="test" data-oe-t-inline="true" contenteditable="false">Hello</t>
- <t t-esc="test" data-oe-t-inline="true" contenteditable="false">Hello</t>
- <t t-raw="test" data-oe-t-inline="true" contenteditable="false">Hello</t>
+ <t t-field="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t>
+ <t t-out="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t>
+ <t t-esc="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t>
+ <t t-raw="test" data-oe-t-inline="true" data-oe-protected="true" contenteditable="false">Hello</t>
</div>`);
expect(editor.getContent()).toBe(`