Skip to content

Commit 1540589

Browse files
committed
Improve translation mechanism
- translate attributes - translate selections - add a message when trying to translate not editable elements
1 parent 97c7471 commit 1540589

14 files changed

+644
-12
lines changed

addons/html_builder/static/src/core/save_plugin.js

+46-11
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,29 @@ export class SavePlugin extends Plugin {
3333
};
3434

3535
async save() {
36+
// TODO: implement the "group by" feature for save
3637
const proms = [];
3738
for (const fn of this.getResource("before_save_handlers")) {
3839
proms.push(fn());
3940
}
4041
await Promise.all(proms);
41-
const saveProms = [...this.editable.querySelectorAll(".o_dirty")].map(async (dirtyEl) => {
42-
dirtyEl.classList.remove("o_dirty");
43-
const cleanedEl = dirtyEl.cloneNode(true);
44-
this.dispatchTo("clean_for_save_handlers", { root: cleanedEl });
45-
46-
if (this.config.isTranslation) {
47-
await this.saveTranslationElement(cleanedEl);
48-
} else {
49-
await this.saveView(cleanedEl);
42+
const dirtyEls = [];
43+
for (const getDirtyElsNotInDom of this.getResource("get_dirty_els_not_in_dom")) {
44+
dirtyEls.push(...getDirtyElsNotInDom());
45+
}
46+
const saveProms = [...dirtyEls, ...this.editable.querySelectorAll(".o_dirty")].map(
47+
async (dirtyEl) => {
48+
dirtyEl.classList.remove("o_dirty");
49+
const cleanedEl = dirtyEl.cloneNode(true);
50+
this.dispatchTo("clean_for_save_handlers", { root: cleanedEl });
51+
52+
if (this.config.isTranslation) {
53+
await this.saveTranslationElement(cleanedEl);
54+
} else {
55+
await this.saveView(cleanedEl);
56+
}
5057
}
51-
});
58+
);
5259
// used to track dirty out of the editable scope, like header, footer or wrapwrap
5360
const willSaves = this.getResource("save_handlers").map((c) => c());
5461
await Promise.all(saveProms.concat(willSaves));
@@ -126,7 +133,7 @@ export class SavePlugin extends Plugin {
126133
if (el.dataset["oeTranslationSourceSha"]) {
127134
const translations = {};
128135
translations[this.services.website.currentWebsite.metadata.lang] = {
129-
[el.dataset["oeTranslationSourceSha"]]: el.innerHTML,
136+
[el.dataset["oeTranslationSourceSha"]]: this.getEscapedElement(el).innerHTML,
130137
};
131138
return rpc("/web_editor/field/translation/update", {
132139
model: el.dataset["oeModel"],
@@ -139,6 +146,34 @@ export class SavePlugin extends Plugin {
139146
return this.saveView(el);
140147
}
141148

149+
getEscapedElement(el) {
150+
const escapedEl = el.cloneNode(true);
151+
const allElements = [escapedEl, ...escapedEl.querySelectorAll("*")];
152+
const exclusion = [];
153+
for (const element of allElements) {
154+
if (
155+
element.matches(
156+
"object,iframe,script,style,[data-oe-model]:not([data-oe-model='ir.ui.view'])"
157+
)
158+
) {
159+
exclusion.push(el);
160+
exclusion.push(...el.querySelectorAll("*"));
161+
}
162+
}
163+
const exclusionSet = new Set(exclusion);
164+
const toEscapeEls = allElements.filter((el) => !exclusionSet.has(el));
165+
for (const toEscapeEl of toEscapeEls) {
166+
for (const child of Array.from(toEscapeEl.childNodes)) {
167+
if (child.nodeType === 3) {
168+
const divEl = document.createElement("div");
169+
divEl.textContent = child.nodeValue;
170+
child.nodeValue = divEl.innerHTML;
171+
}
172+
}
173+
}
174+
return escapedEl;
175+
}
176+
142177
/**
143178
* Handles the flag of the closest savable element to the mutation as dirty
144179
*

addons/html_builder/static/src/core/setup_editor_plugin.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Plugin } from "@html_editor/plugin";
22
import { _t } from "@web/core/l10n/translation";
3+
import { getTranslationEditableEls } from "@html_builder/website_builder/plugins/translation_plugin";
34

45
export class SetupEditorPlugin extends Plugin {
56
static id = "setup_editor_plugin";
@@ -10,9 +11,21 @@ export class SetupEditorPlugin extends Plugin {
1011
};
1112

1213
setup() {
14+
this.websiteService = this.services.website;
1315
this.editable.setAttribute("contenteditable", false);
14-
1516
// Add the `o_editable` class on the editable elements
17+
if (this.config.isTranslation) {
18+
const translationSavableEls = getTranslationEditableEls(
19+
this.websiteService.pageDocument
20+
);
21+
const filteredElements = Array.from(translationSavableEls).filter(
22+
(el) => !el.hasAttribute("data-oe-readonly")
23+
);
24+
for (const filteredElement of filteredElements) {
25+
filteredElement.classList.add("o_editable");
26+
}
27+
return;
28+
}
1629
let editableEls = this.getEditableElements("[data-oe-model]")
1730
.filter((el) => !el.matches("link, script"))
1831
.filter((el) => !el.hasAttribute("data-oe-readonly"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Dialog } from "@web/core/dialog/dialog";
2+
import { _t } from "@web/core/l10n/translation";
3+
import { useState, Component } from "@odoo/owl";
4+
5+
const NO_OP = () => {};
6+
7+
export class WebsiteDialog extends Component {
8+
static template = "website_builder.WebsiteDialog";
9+
static components = { Dialog };
10+
static props = {
11+
...Dialog.props,
12+
primaryTitle: { type: String, optional: true },
13+
primaryClick: { type: Function, optional: true },
14+
secondaryTitle: { type: String, optional: true },
15+
secondaryClick: { type: Function, optional: true },
16+
showSecondaryButton: { type: Boolean, optional: true },
17+
close: { type: Function, optional: true },
18+
closeOnClick: { type: Boolean, optional: true },
19+
body: { type: String, optional: true },
20+
slots: { type: Object, optional: true },
21+
showFooter: { type: Boolean, optional: true },
22+
};
23+
static defaultProps = {
24+
...Dialog.defaultProps,
25+
title: _t("Confirmation"),
26+
showFooter: true,
27+
primaryTitle: _t("Ok"),
28+
secondaryTitle: _t("Cancel"),
29+
showSecondaryButton: true,
30+
size: "md",
31+
closeOnClick: true,
32+
close: NO_OP,
33+
};
34+
35+
setup() {
36+
this.state = useState({
37+
disabled: false,
38+
});
39+
}
40+
/**
41+
* Disables the buttons of the dialog when a click is made.
42+
* If a handler is provided, await for its call.
43+
* If the prop closeOnClick is true, close the dialog.
44+
* Otherwise, restore the button.
45+
*
46+
* @param handler {function|void} The handler to protect.
47+
* @returns {function(): Promise} handler called when a click is made.
48+
*/
49+
protectedClick(handler) {
50+
return async () => {
51+
if (this.state.disabled) {
52+
return;
53+
}
54+
this.state.disabled = true;
55+
if (handler) {
56+
await handler();
57+
}
58+
if (this.props.closeOnClick) {
59+
return this.props.close();
60+
}
61+
this.state.disabled = false;
62+
};
63+
}
64+
65+
get contentClasses() {
66+
const websiteDialogClass = "o_website_dialog";
67+
if (this.props.contentClass) {
68+
return `${websiteDialogClass} ${this.props.contentClass}`;
69+
}
70+
return websiteDialogClass;
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.o_website_dialog {
2+
label {
3+
font-weight: $font-weight-bold;
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<templates xml:space="preserve">
3+
<t t-name="website_builder.WebsiteDialog">
4+
<Dialog contentClass="contentClasses"
5+
footer="props.showFooter ? undefined : false"
6+
size="props.size"
7+
title="props.title">
8+
<t t-set-slot="default">
9+
<t t-if="props.slots and props.slots.default" t-slot="default"/>
10+
<t t-else="" t-esc="props.body"/>
11+
</t>
12+
<t t-if="props.showFooter" t-set-slot="footer">
13+
<t t-if="props.slots and props.slots.footer" t-slot="footer"/>
14+
<t t-else="">
15+
<button class="btn btn-primary" t-on-click="protectedClick(props.primaryClick)" t-att-disabled="state.disabled">
16+
<t t-esc="props.primaryTitle"/>
17+
</button>
18+
<button t-if="props.showSecondaryButton" class="btn btn-secondary" t-on-click="protectedClick(props.secondaryClick)" t-att-disabled="state.disabled">
19+
<t t-esc="props.secondaryTitle"/>
20+
</button>
21+
</t>
22+
</t>
23+
</Dialog>
24+
</t>
25+
26+
</templates>

0 commit comments

Comments
 (0)