[FIX] web_editor: blinky colorpicker when applyColor on removed fonts

Reproduction:

1. in Note, type ` abcd ` in an empty note
2. color the background of all the text, bold `abcd`
3. select from the end till the letter `b`, try to change the color,
traceback pops up

Fix: The main reason is we remove element in `splitElement`/
`splitAroundUntil`. In this special case, there are two selected nodes
which returns the same font element. The first font element will be
removed and replaced first. Because the first font element doesn’t exist
anymore, when recovering selection, the second element considered as
collapsed and the recovered selection is a caret. Thus the color picker
is gone after applying the color (preview)

The fix is then, we always make sure that the font we have is still
connected to the page (exist in the page), otherwise it’s problematic to
apply color to a non-existing element and recover cursor from it. If
there is font element(s) that is not connected, we simply get the fonts
again, because new font element is created for the second node, it won’t
split (remove) the first node’s font element again

Task-3476494

closes odoo/odoo#150500

X-original-commit: 06a8b930176093e2c5a853b78431316af90f3550
Signed-off-by: Nicolas Bayet (nby) <nby@odoo.com>
Signed-off-by: Jinjiu Liu (jili) <jili@odoo.com>
Co-authored-by: Goaman <nby@odoo.com>
This commit is contained in:
Jinjiu Liu
2024-01-23 22:51:57 +00:00
co-authored by Goaman
parent aba7b00cd8
commit bc440ff045
@@ -135,9 +135,9 @@ function hasColor(element, mode) {
}
}
return (
(style[mode] && style[mode] !== 'inherit' && style[mode] !== parent.style[mode]) ||
(style[mode] && style[mode] !== 'inherit' && (!parent || style[mode] !== parent.style[mode])) ||
(classRegex.test(element.className) &&
getComputedStyle(element)[mode] !== getComputedStyle(parent)[mode])
(!parent || getComputedStyle(element)[mode] !== getComputedStyle(parent)[mode]))
);
}
@@ -598,59 +598,69 @@ export const editorCommands = {
.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);
if (font && (font.nodeName === 'FONT' || (font.nodeName === 'SPAN' && font.style[mode]))) {
// Partially selected <font>: split it.
const selectedChildren = children.filter(child => selectedNodes.includes(child));
if (selectedChildren.length) {
font = splitAroundUntil(selectedChildren, font);
function getFonts(selectedNodes) {
return selectedNodes.flatMap(node => {
let font = closestElement(node, 'font') || closestElement(node, 'span');
const children = font && descendants(font);
if (font && (font.nodeName === 'FONT' || (font.nodeName === 'SPAN' && font.style[mode]))) {
// Partially selected <font>: split it.
const selectedChildren = children.filter(child => selectedNodes.includes(child));
if (selectedChildren.length) {
font = splitAroundUntil(selectedChildren, font);
} else {
font = [];
}
} else if ((node.nodeType === Node.TEXT_NODE && !isWhitespace(node))
|| (node.nodeName === 'BR' && isEmptyBlock(node.parentNode))
|| (node.nodeType === Node.ELEMENT_NODE &&
['inline', 'inline-block'].includes(getComputedStyle(node).display) &&
!isWhitespace(node.textContent) &&
!node.classList.contains('btn') &&
!node.querySelector('font'))) {
// Node is a visible text or inline node without font nor a button:
// wrap it in a <font>.
const previous = node.previousSibling;
const classRegex = mode === 'color' ? BG_CLASSES_REGEX : TEXT_CLASSES_REGEX;
if (
previous &&
previous.nodeName === 'FONT' &&
!previous.style[mode === 'color' ? 'backgroundColor' : 'color'] &&
!classRegex.test(previous.className) &&
selectedNodes.includes(previous.firstChild) &&
selectedNodes.includes(previous.lastChild)
) {
// Directly follows a fully selected <font> that isn't
// colored in the other mode: append to that.
font = previous;
} else {
// No <font> found: insert a new one.
font = document.createElement('font');
node.after(font);
}
if (node.textContent) {
font.appendChild(node);
} else {
fillEmpty(font);
}
} else {
font = [];
font = []; // Ignore non-text or invisible text nodes.
}
} else if ((node.nodeType === Node.TEXT_NODE && !isWhitespace(node))
|| (node.nodeName === 'BR' && isEmptyBlock(node.parentNode))
|| (node.nodeType === Node.ELEMENT_NODE &&
['inline', 'inline-block'].includes(getComputedStyle(node).display) &&
!isWhitespace(node.textContent) &&
!node.classList.contains('btn') &&
!node.querySelector('font'))) {
// Node is a visible text or inline node without font nor a button:
// wrap it in a <font>.
const previous = node.previousSibling;
const classRegex = mode === 'color' ? BG_CLASSES_REGEX : TEXT_CLASSES_REGEX;
if (
previous &&
previous.nodeName === 'FONT' &&
!previous.style[mode === 'color' ? 'backgroundColor' : 'color'] &&
!classRegex.test(previous.className) &&
selectedNodes.includes(previous.firstChild) &&
selectedNodes.includes(previous.lastChild)
) {
// Directly follows a fully selected <font> that isn't
// colored in the other mode: append to that.
font = previous;
} else {
// No <font> found: insert a new one.
font = document.createElement('font');
node.after(font);
}
if (node.textContent) {
font.appendChild(node);
} else {
fillEmpty(font);
}
} else {
font = []; // Ignore non-text or invisible text nodes.
}
return font;
});
return font;
});
}
for (const fieldNode of selectedFieldNodes) {
colorElement(fieldNode, color, mode);
}
let fonts = getFonts(selectedNodes);
// Dirty fix as the previous call could have unconnected elements
// because of the `splitAroundUntil`. Another call should provide he
// correct list of fonts.
if (!fonts.every((font) => font.isConnected)) {
fonts = getFonts(selectedNodes);
}
// Color the selected <font>s and remove uncolored fonts.
const fontsSet = new Set(fonts);
for (const font of fontsSet) {