[IMP] web_editor: Allow to stylize field on report editor

Before this commit, the tags t-field,t-esc,t-out were difficult to select
and couldn't be stylized.

After this commit, when clicking or selecting a tag t-field,t-esc,t-out
the whole tag is automatically selected and a style can be applied onto it.

task-id-3457404

closes odoo/odoo#135265

Related: odoo/enterprise#47089
Signed-off-by: Lucas Perais (lpe) <lpe@odoo.com>
This commit is contained in:
Bastien PIERRE
2023-10-23 20:07:17 +00:00
committed by Lucas Perais
parent d5b839f713
commit ffef01e327
5 changed files with 95 additions and 0 deletions
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { ancestors } from '@web_editor/js/common/wysiwyg_utils';
import { closestElement } from "@web_editor/js/editor/odoo-editor/src/utils/utils";
export class QWebPlugin {
constructor(options = {}) {
@@ -8,6 +9,7 @@ export class QWebPlugin {
if (this._options.editor) {
this._editable = this._options.editor.editable;
this._document = this._options.editor.document;
this._selectQwebNode(this._options.editor);
} else {
this._editable = this._options.editable;
this._document = this._options.document || window.document;
@@ -131,6 +133,18 @@ export class QWebPlugin {
}
});
}
_selectQwebNode(editor) {
editor.addDomListener(editor.document, 'selectionchange', e => {
const selection = e.target.getSelection();
const qwebNode = selection.anchorNode && closestElement(selection.anchorNode, '[t-field],[t-esc],[t-out]');
if (qwebNode){
const range = new Range();
range.selectNode(qwebNode);
selection.removeAllRanges();
selection.addRange(range);
}
});
}
_makeBranchingSelection() {
const document = this._options.document || window.document;
this._selectElWrapper = document.createElement('div');
@@ -594,6 +594,10 @@ export const editorCommands = {
const selectedNodes = mode === "backgroundColor"
? selectionNodes.filter(node => !closestElement(node, 'table.o_selected_table'))
: selectionNodes;
const selectedFieldNodes = new Set(getSelectedNodes(editor.editable)
.map(n => closestElement(n, "*[t-field],*[t-out],*[t-esc]"))
.filter(Boolean));
const fonts = selectedNodes.flatMap(node => {
let font = closestElement(node, 'font') || closestElement(node, 'span');
const children = font && descendants(font);
@@ -642,6 +646,11 @@ export const editorCommands = {
}
return font;
});
for (const fieldNode of selectedFieldNodes) {
colorElement(fieldNode, color, mode);
}
// Color the selected <font>s and remove uncolored fonts.
const fontsSet = new Set(fonts);
for (const font of fontsSet) {
@@ -1068,6 +1068,10 @@ export const formatSelection = (editor, formatName, {applyStyle, formatProps} =
.filter(n => n.nodeType === Node.TEXT_NODE && closestElement(n).isContentEditable && (isVisibleTextNode(n) || isZWS(n)));
const selectedTextNodes = selectedNodes.length ? selectedNodes : selectedNodesInTds;
const selectedFieldNodes = new Set(getSelectedNodes(editor.editable)
.map(n =>closestElement(n, "*[t-field],*[t-out],*[t-esc]"))
.filter(Boolean));
const formatSpec = formatsSpecs[formatName];
for (const selectedTextNode of selectedTextNodes) {
const inlineAncestors = [];
@@ -1117,6 +1121,14 @@ export const formatSelection = (editor, formatName, {applyStyle, formatProps} =
}
}
for (const selectedFieldNode of selectedFieldNodes) {
if (applyStyle) {
formatSpec.addStyle(selectedFieldNode, formatProps);
} else {
formatSpec.removeStyle(selectedFieldNode);
}
}
if (zws) {
const siblings = [...zws.parentElement.childNodes];
if (
@@ -19,6 +19,19 @@ describe('applyColor', () => {
'<font><span>g</span>h</font>i</p>',
});
});
it('should apply a color to the qweb tag', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><p t-esc="'Test'" contenteditable="false">[Test]</p></div>`,
stepFunction: setColor('rgb(255, 0, 0)', 'color'),
contentAfter: `<div>[<p t-esc="'Test'" contenteditable="false" style="color: rgb(255, 0, 0);">Test</p>]</div>`,
});
await testEditor(BasicEditor, {
contentBefore: `<div><p t-field="record.display_name" contenteditable="false">[Test]</p></div>`,
stepFunction: setColor('rgb(255, 0, 0)', 'color'),
contentAfter: `<div>[<p t-field="record.display_name" contenteditable="false" style="color: rgb(255, 0, 0);">Test</p>]</div>`,
});
});
it('should apply a background color to a slice of text in a span in a font', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>a<font>b<span>c[def]g</span>h</font>i</p>',
@@ -77,6 +77,25 @@ describe('Format', () => {
contentAfter: `<p>[abc</p><p>def]</p>`,
});
});
it('should make qweb tag bold', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><p t-esc="'Test'" contenteditable="false">[Test]</p></div>`,
stepFunction: bold,
contentAfter: `<div><p t-esc="'Test'" contenteditable="false" style="font-weight: bolder;">[Test]</p></div>`,
});
await testEditor(BasicEditor, {
contentBefore: `<div><p t-field="record.name" contenteditable="false">[Test]</p></div>`,
stepFunction: bold,
contentAfter: `<div><p t-field="record.name" contenteditable="false" style="font-weight: bolder;">[Test]</p></div>`,
});
});
it('should make qweb tag bold even with partial selection', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><p t-esc="'Test'" contenteditable="false">T[e]st</p></div>`,
stepFunction: bold,
contentAfter: `<div><p t-esc="'Test'" contenteditable="false" style="font-weight: bolder;">T[e]st</p></div>`,
});
});
it('should make a whole heading bold after a triple click', async () => {
await testEditor(BasicEditor, {
contentBefore: `<h1>${notStrong(`[ab`)}</h1><p>]cd</p>`,
@@ -251,6 +270,13 @@ describe('Format', () => {
contentAfter: `<p>[abc</p><p>def]</p>`,
});
});
it('should make qweb tag italic', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><p t-esc="'Test'" contenteditable="false">[Test]</p></div>`,
stepFunction: italic,
contentAfter: `<div><p t-esc="'Test'" contenteditable="false" style="font-style: italic;">[Test]</p></div>`,
});
});
it('should make a whole heading italic after a triple click', async () => {
await testEditor(BasicEditor, {
contentBefore: `<h1>[ab</h1><p>]cd</p>`,
@@ -339,6 +365,13 @@ describe('Format', () => {
contentAfter: '<p>[abc</p><p>def]</p>',
});
});
it('should make qweb tag underline', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><p t-esc="'Test'" contenteditable="false">[Test]</p></div>`,
stepFunction: underline,
contentAfter: `<div><p t-esc="'Test'" contenteditable="false" style="text-decoration-line: underline;">[Test]</p></div>`,
});
});
it('should make a whole heading underline after a triple click', async () => {
await testEditor(BasicEditor, {
contentBefore: `<h1>[ab</h1><p>]cd</p>`,
@@ -483,6 +516,13 @@ describe('Format', () => {
contentAfter: '<p>[abc</p><p>def]</p>',
});
});
it('should make qweb tag strikeThrough', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><p t-esc="'Test'" contenteditable="false">[Test]</p></div>`,
stepFunction: strikeThrough,
contentAfter: `<div><p t-esc="'Test'" contenteditable="false" style="text-decoration-line: line-through;">[Test]</p></div>`,
});
});
it('should make a whole heading strikeThrough after a triple click', async () => {
await testEditor(BasicEditor, {
contentBefore: `<h1>[ab</h1><p>]cd</p>`,
@@ -728,6 +768,13 @@ describe('Format', () => {
contentAfter: '<p>ab<span style="font-size: 10px;">[cde]</span>fg</p>',
});
});
it('should change the font size the qweb tag', async () => {
await testEditor(BasicEditor, {
contentBefore: `<div><p t-esc="'Test'" contenteditable="false">[Test]</p></div>`,
stepFunction: setFontSize('36px'),
contentAfter: `<div><p t-esc="'Test'" contenteditable="false" style="font-size: 36px;">[Test]</p></div>`,
});
});
it('should change the font size of a whole heading after a triple click', async () => {
await testEditor(BasicEditor, {
contentBefore: '<h1>[ab</h1><p>]cd</p>',