[FIX] web_editor: properly apply color in nested inline contexts
The colorpicker failed in cases like: ```xml <font>a<span>b</span>c</font> ``` where we select the letter "b". This was because `applyColor` only considered direct children of the `font` element. This extends it to deal with further descendants of it. task-2623347 closes odoo/odoo#83254 X-original-commit: 5d8d98812a4f351abd27389c4992bf9e753833b7 Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
import {
|
||||
ancestors,
|
||||
descendants,
|
||||
childNodeIndex,
|
||||
closestBlock,
|
||||
closestElement,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
setSelection,
|
||||
setCursorStart,
|
||||
setTagName,
|
||||
splitAroundUntil,
|
||||
splitElement,
|
||||
splitTextNode,
|
||||
startPos,
|
||||
@@ -505,15 +507,15 @@ export const editorCommands = {
|
||||
const selectedNodes = getSelectedNodes(editor.editable);
|
||||
const fonts = selectedNodes.flatMap(node => {
|
||||
let font = closestElement(node, 'font');
|
||||
const children = font && [...font.childNodes];
|
||||
const children = font && descendants(font);
|
||||
if (font && font.nodeName === 'FONT') {
|
||||
// Partially selected <font>: split it.
|
||||
const selectedChildren = children.filter(child => selectedNodes.includes(child));
|
||||
if (selectedChildren.length) {
|
||||
const after = selectedChildren[selectedChildren.length - 1].nextSibling;
|
||||
font = after ? splitElement(font, childNodeIndex(after))[0] : font;
|
||||
const before = selectedChildren[0].previousSibling;
|
||||
font = before ? splitElement(font, childNodeIndex(before) + 1)[1] : font;
|
||||
const splitResult = splitAroundUntil(selectedChildren, font);
|
||||
font = splitResult[0] || splitResult[1] || font;
|
||||
} else {
|
||||
font = [];
|
||||
}
|
||||
} else if (node.nodeType === Node.TEXT_NODE && isVisibleStr(node)) {
|
||||
// Node is a visible text node: wrap it in a <font>.
|
||||
|
||||
@@ -261,6 +261,20 @@ export function ancestors(node, editable) {
|
||||
return [node.parentElement, ...ancestors(node.parentElement, editable)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a node, return all of its descendants, in depth-first order.
|
||||
*
|
||||
* @param {Node} node
|
||||
* @returns {Node[]}
|
||||
*/
|
||||
export function descendants(node) {
|
||||
const posterity = [];
|
||||
for (const child of (node.childNodes || [])) {
|
||||
posterity.push(child, ...descendants(child));
|
||||
}
|
||||
return posterity;
|
||||
}
|
||||
|
||||
export function closestBlock(node) {
|
||||
return findNode(closestPath(node), node => isBlock(node));
|
||||
}
|
||||
@@ -1320,6 +1334,44 @@ export function splitElement(element, offset) {
|
||||
return [before, after];
|
||||
}
|
||||
|
||||
/**
|
||||
* Split around the given elements, until a given ancestor (included). Elements
|
||||
* will be removed in the process so caution is advised in dealing with their
|
||||
* references. Returns a tuple containing the new elements on both sides of the
|
||||
* split.
|
||||
*
|
||||
* @see splitElement
|
||||
* @param {Node[] | Node} elements
|
||||
* @param {Node} limitAncestor
|
||||
* @returns {[Node, Node]}
|
||||
*/
|
||||
export function splitAroundUntil(elements, limitAncestor) {
|
||||
elements = Array.isArray(elements) ? elements : [elements];
|
||||
let after = elements[elements.length - 1].nextSibling;
|
||||
let newUntil = limitAncestor;
|
||||
let beforeSplit, afterSplit;
|
||||
// Split up ancestors up to font
|
||||
while (after && after.parentElement !== limitAncestor) {
|
||||
afterSplit = splitElement(after.parentElement, childNodeIndex(after))[0];
|
||||
newUntil = afterSplit;
|
||||
after = newUntil.nextSibling;
|
||||
}
|
||||
if (after) {
|
||||
afterSplit = splitElement(limitAncestor, childNodeIndex(after))[0];
|
||||
limitAncestor = afterSplit;
|
||||
}
|
||||
let before = elements[0].previousSibling;
|
||||
while (before && before.parentElement !== limitAncestor) {
|
||||
beforeSplit = splitElement(before.parentElement, childNodeIndex(before) + 1)[1];
|
||||
newUntil = beforeSplit;
|
||||
before = newUntil.previousSibling;
|
||||
}
|
||||
if (before) {
|
||||
beforeSplit = splitElement(limitAncestor, childNodeIndex(before) + 1)[1];
|
||||
}
|
||||
return [beforeSplit, afterSplit];
|
||||
}
|
||||
|
||||
export function insertText(sel, content) {
|
||||
if (sel.anchorNode.nodeType == Node.TEXT_NODE) {
|
||||
const pos = [sel.anchorNode.parentElement, splitTextNode(sel.anchorNode, sel.anchorOffset)];
|
||||
|
||||
@@ -3117,4 +3117,16 @@ X[]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyColor', () => {
|
||||
it('should apply a 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>',
|
||||
stepFunction: editor => editor.execCommand('applyColor', 'rgb(255, 0, 0)', 'color'),
|
||||
contentAfter: '<p>a<font>b<span>c</span></font>' +
|
||||
'<font style="color: rgb(255, 0, 0);"><span>[def]</span></font>' +
|
||||
'<font><span>g</span>h</font>i</p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
childNodeIndex,
|
||||
closestBlock,
|
||||
closestElement,
|
||||
descendants,
|
||||
endPos,
|
||||
firstLeaf,
|
||||
getAdjacentPreviousSiblings,
|
||||
@@ -26,6 +27,8 @@ import {
|
||||
setSelection,
|
||||
setCursorStart,
|
||||
setCursorEnd,
|
||||
splitAroundUntil,
|
||||
splitTextNode,
|
||||
getCursorDirection,
|
||||
DIRECTIONS,
|
||||
isBlock,
|
||||
@@ -351,6 +354,22 @@ describe('Utils', () => {
|
||||
window.chai.expect(result).to.eql([editable]);
|
||||
});
|
||||
});
|
||||
describe('descendants', () => {
|
||||
it('should find all the descendants of a div in depth-first order', () => {
|
||||
const [div] = insertTestHtml(
|
||||
'<div><div><div><p>abc</p><div><p>def</p></div></div></div></div>',
|
||||
);
|
||||
window.chai.expect(descendants(div)).to.eql([
|
||||
div.firstChild, // <div><div>...
|
||||
div.firstChild.firstChild, // <div><div><div>...
|
||||
div.firstChild.firstChild.firstChild, // <p>abc</p>
|
||||
div.firstChild.firstChild.firstChild.firstChild, // "abc"
|
||||
div.firstChild.firstChild.childNodes[1], // <div><p>def</p></div>
|
||||
div.firstChild.firstChild.childNodes[1].firstChild, // <p>def</p>
|
||||
div.firstChild.firstChild.childNodes[1].firstChild.firstChild, // "def"
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('closestBlock', () => {
|
||||
it('should find the closest block of a deeply nested text node', () => {
|
||||
const [div] = insertTestHtml(
|
||||
@@ -1393,4 +1412,56 @@ describe('Utils', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// DOM Modification
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
describe('splitAroundUntil', () => {
|
||||
it('should split a slice of text from its inline ancestry', () => {
|
||||
const [p] = insertTestHtml('<p>a<font>b<span>cde</span>f</font>g</p>');
|
||||
const cde = p.childNodes[1].childNodes[1].firstChild;
|
||||
// We want to test with "cde" being three separate text nodes.
|
||||
splitTextNode(cde, 2);
|
||||
const cd = cde.previousSibling;
|
||||
splitTextNode(cd, 1);
|
||||
const d = cd;
|
||||
const result = splitAroundUntil(d, p.childNodes[1]);
|
||||
window.chai.expect(result.every(node => node.tagName === 'FONT')).to.be.ok;
|
||||
window.chai.expect(p.outerHTML).to.eql(
|
||||
'<p>a<font>b<span>c</span></font><font><span>d</span></font><font><span>e</span>f</font>g</p>'
|
||||
);
|
||||
window.chai.expect(result[0]).to.eql(p.childNodes[2]);
|
||||
// "before" split happened so "after" split is not in the DOM anymore:
|
||||
window.chai.expect(result[1].parentElement).to.eql(null);
|
||||
});
|
||||
it('should split a slice of text from its inline ancestry', () => {
|
||||
const [p] = insertTestHtml('<p>a<font>b<span>cdefg</span>h</font>i</p>');
|
||||
const cdefg = p.childNodes[1].childNodes[1].firstChild;
|
||||
// We want to test with "cdefg" being five separate text nodes.
|
||||
splitTextNode(cdefg, 4);
|
||||
const cdef = cdefg.previousSibling;
|
||||
splitTextNode(cdef, 3);
|
||||
const cde = cdef.previousSibling;
|
||||
splitTextNode(cde, 2);
|
||||
const cd = cde.previousSibling;
|
||||
splitTextNode(cd, 1);
|
||||
const d = cd;
|
||||
const result = splitAroundUntil([d, d.nextSibling.nextSibling], p.childNodes[1]);
|
||||
window.chai.expect(result.every(node => node.tagName === 'FONT')).to.be.ok;
|
||||
window.chai.expect(p.outerHTML).to.eql(
|
||||
'<p>a<font>b<span>c</span></font><font><span>def</span></font><font><span>g</span>h</font>i</p>'
|
||||
);
|
||||
window.chai.expect(result[0]).to.eql(p.childNodes[2]);
|
||||
// "before" split happened so "after" split is not in the DOM anymore:
|
||||
window.chai.expect(result[1].parentElement).to.eql(null);
|
||||
});
|
||||
it('should not do anything (nothing to split)', () => {
|
||||
const [p] = insertTestHtml('<p>a<font>b<span>cde</span>f</font>g</p>');
|
||||
const cde = p.childNodes[1].childNodes[1].firstChild;
|
||||
const result = splitAroundUntil(cde, p.childNodes[1]);
|
||||
window.chai.expect(result.every(node => node === undefined)).to.be.ok;
|
||||
window.chai.expect(p.outerHTML).to.eql('<p>a<font>b<span>cde</span>f</font>g</p>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user