[FIX] web_editor: prevent error on font-size change
Before this commit, changing the font size of an element with one ancestor having the same font size could trigger a traceback. For fixing that issue some code has to be refactored to handle the setFontSize case in formatSelection. As a side effect of this refactoring, nested font-size is now properly handled (see added tests). task-3086301 closes odoo/odoo#108633 X-original-commit: d003b82043468bcf609006c5910ac8fc9520e136 Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
committed by
David Monjoie
parent
7bf3502c02
commit
544afd6de9
@@ -914,7 +914,7 @@ const formatsSpecs = {
|
||||
},
|
||||
fontSize: {
|
||||
isFormatted: isFontSize,
|
||||
hasStyle: (node, props) => node.style && node.style['font-size'] === props.size,
|
||||
hasStyle: (node) => node.style && node.style['font-size'],
|
||||
addStyle: (node, props) => node.style['font-size'] = props.size,
|
||||
removeStyle: (node) => removeStyle(node, 'font-size'),
|
||||
},
|
||||
@@ -954,7 +954,7 @@ const removeFormat = (node, formatSpec) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (formatSpec.isTag(node)) {
|
||||
if (formatSpec.isTag && formatSpec.isTag(node)) {
|
||||
const attributesNames = node.getAttributeNames().filter((name)=> {
|
||||
return name !== 'data-oe-zws-empty-inline';
|
||||
});
|
||||
@@ -1008,79 +1008,48 @@ export const formatSelection = (editor, formatName, {applyStyle, formatProps} =
|
||||
|
||||
const formatSpec = formatsSpecs[formatName];
|
||||
for (const selectedTextNode of selectedTextNodes) {
|
||||
// Compute inline ancestors until finding one which is a block or has a class.
|
||||
const inlineAncestors = [];
|
||||
let node = selectedTextNode;
|
||||
while ((node = node.parentElement) && (!isBlock(node) && !(node.classList && node.classList.length))) {
|
||||
inlineAncestors.unshift(node);
|
||||
}
|
||||
const blockOrInlineClass = node;
|
||||
let currentNode = selectedTextNode;
|
||||
let parentNode = selectedTextNode.parentElement;
|
||||
|
||||
const firstBlockOrClassHasFormat = formatSpec.isFormatted(blockOrInlineClass, formatProps);
|
||||
// Remove the format on all inline ancestors until a block or an element
|
||||
// with a class (in case the formating comes from the class).
|
||||
while (parentNode && (!isBlock(parentNode) && !(parentNode.classList && parentNode.classList.length))) {
|
||||
const isUselessZws = parentNode.tagName === 'SPAN' &&
|
||||
parentNode.hasAttribute('data-oe-zws-empty-inline') &&
|
||||
parentNode.getAttributeNames().length === 1;
|
||||
|
||||
let previousAncestorHasFormat = firstBlockOrClassHasFormat;
|
||||
let lastAncestorInlineFormat;
|
||||
|
||||
for (const ancestor of inlineAncestors) {
|
||||
const ancestorIsTag = formatSpec.isTag && formatSpec.isTag(ancestor);
|
||||
const ancestorHasStyle = formatSpec.hasStyle && formatSpec.hasStyle(ancestor, formatProps);
|
||||
const ancestorTagIsFormated = ancestorIsTag || ancestorHasStyle;
|
||||
const isUselessZwsSpan =
|
||||
ancestor.tagName === 'SPAN' &&
|
||||
ancestor.hasAttribute('data-oe-zws-empty-inline') &&
|
||||
ancestor.getAttributeNames().length === 1;
|
||||
const ancestorIsFormatted = formatSpec.isFormatted(ancestor, formatProps);
|
||||
|
||||
if (ancestorTagIsFormated && previousAncestorHasFormat !== ancestorIsFormatted) {
|
||||
if (lastAncestorInlineFormat) {
|
||||
// Remove format on two node that switch the format to flatten the DOM.
|
||||
const newLastAncestorInlineFormat = splitAroundUntil(ancestor, lastAncestorInlineFormat);
|
||||
removeFormat(newLastAncestorInlineFormat, formatSpec);
|
||||
removeFormat(ancestor, formatSpec);
|
||||
lastAncestorInlineFormat = undefined;
|
||||
} else {
|
||||
lastAncestorInlineFormat = ancestor;
|
||||
if (isUselessZws) {
|
||||
unwrapContents(parentNode);
|
||||
} else {
|
||||
const newLastAncestorInlineFormat = splitAroundUntil(currentNode, parentNode);
|
||||
removeFormat(newLastAncestorInlineFormat, formatSpec);
|
||||
if (newLastAncestorInlineFormat.isConnected) {
|
||||
inlineAncestors.push(newLastAncestorInlineFormat);
|
||||
currentNode = newLastAncestorInlineFormat;
|
||||
}
|
||||
// if there is two consecutive format that are the same, remove one.
|
||||
} else if (
|
||||
(ancestorIsTag || ancestorHasStyle) &&
|
||||
(
|
||||
previousAncestorHasFormat ||
|
||||
(!previousAncestorHasFormat && !ancestorIsFormatted)
|
||||
)
|
||||
) {
|
||||
removeFormat(ancestor, formatSpec);
|
||||
} else if (isUselessZwsSpan) {
|
||||
unwrapContents(ancestor);
|
||||
}
|
||||
previousAncestorHasFormat = ancestor.parentElement && formatSpec.isFormatted(ancestor, formatProps);
|
||||
|
||||
parentNode = currentNode.parentElement;
|
||||
}
|
||||
|
||||
if (selectedTextNode.nodeType === Node.TEXT_NODE) {
|
||||
const isLeafNodeFormated = formatSpec.isFormatted(selectedTextNode, formatProps);
|
||||
if (isLeafNodeFormated !== applyStyle) {
|
||||
if (lastAncestorInlineFormat) {
|
||||
const splitNode = splitAroundUntil(selectedTextNode, lastAncestorInlineFormat);
|
||||
removeFormat(splitNode, formatSpec);
|
||||
} else {
|
||||
if (firstBlockOrClassHasFormat && !applyStyle) {
|
||||
formatSpec.addNeutralStyle && formatSpec.addNeutralStyle(getOrCreateSpan(selectedTextNode, inlineAncestors));
|
||||
} else if (!firstBlockOrClassHasFormat && applyStyle) {
|
||||
const tag = formatSpec.tagName && document.createElement(formatSpec.tagName);
|
||||
if (tag) {
|
||||
selectedTextNode.after(tag);
|
||||
tag.append(selectedTextNode);
|
||||
const firstBlockOrClassHasFormat = formatSpec.isFormatted(parentNode, formatProps);
|
||||
|
||||
if (!formatSpec.isFormatted(tag, formatProps)) {
|
||||
tag.after(selectedTextNode);
|
||||
tag.remove();
|
||||
formatSpec.addStyle(getOrCreateSpan(selectedTextNode, inlineAncestors), formatProps);
|
||||
}
|
||||
} else {
|
||||
formatSpec.addStyle(getOrCreateSpan(selectedTextNode, inlineAncestors), formatProps);
|
||||
}
|
||||
}
|
||||
if (firstBlockOrClassHasFormat && !applyStyle) {
|
||||
formatSpec.addNeutralStyle && formatSpec.addNeutralStyle(getOrCreateSpan(selectedTextNode, inlineAncestors));
|
||||
} else if (!firstBlockOrClassHasFormat && applyStyle) {
|
||||
const tag = formatSpec.tagName && document.createElement(formatSpec.tagName);
|
||||
if (tag) {
|
||||
selectedTextNode.after(tag);
|
||||
tag.append(selectedTextNode);
|
||||
|
||||
if (!formatSpec.isFormatted(tag, formatProps)) {
|
||||
tag.after(selectedTextNode);
|
||||
tag.remove();
|
||||
formatSpec.addStyle(getOrCreateSpan(selectedTextNode, inlineAncestors), formatProps);
|
||||
}
|
||||
} else {
|
||||
formatSpec.addStyle(getOrCreateSpan(selectedTextNode, inlineAncestors), formatProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isSelectionFormat } from '../../src/utils/utils.js';
|
||||
import { BasicEditor, testEditor, setTestSelection, Direction } from '../utils.js';
|
||||
import { BasicEditor, testEditor, setTestSelection, Direction, unformat } from '../utils.js';
|
||||
|
||||
const bold = async editor => {
|
||||
await editor.execCommand('bold');
|
||||
@@ -139,9 +139,23 @@ describe('Format', () => {
|
||||
});
|
||||
it('should remove a bold tag that was redondant with different tags while performing the command', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>a${spanBold(`b${strong(`c${b(`[d]`)}e`)}f`)}g</p>`,
|
||||
contentBefore: unformat(`<p>
|
||||
a
|
||||
<span style="font-weight: bolder;">
|
||||
b
|
||||
<strong>c<b>[d]</b>e</strong>
|
||||
f
|
||||
</span>
|
||||
g
|
||||
</p>`),
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>a${spanBold(`bc`)}[d]${spanBold(`ef`)}g</p>`,
|
||||
contentAfter: unformat(`<p>
|
||||
a
|
||||
<span style="font-weight: bolder;">b<strong>c</strong></span>
|
||||
[d]
|
||||
<span style="font-weight: bolder;"><strong>e</strong>f</span>
|
||||
g
|
||||
</p>`),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -535,12 +549,12 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd`))}${s(u(`ghi[]`))}${u(s(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`${u(`ghi`)}${span(`[]\u200b`, 'first')}`)}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}${s(u(`ghi`) + `[]`)}${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`${u(`ghi`)}`)}${s(`[]\u200b`, 'first')}${u(s(`ef`))}</p>`,
|
||||
// The reason the cursor is after the tag <s> is because when the editor get's cleaned, the zws tag gets deleted.
|
||||
contentAfter: `<p>ab${u(s(`cd`))}${s(u(`ghi`))}[]${u(s(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline, write, restore underline, write, remove underline again, write (collapsed, strikeThrough)', async () => {
|
||||
const uselessSpan = content => `<span>${content}</span>`; // TODO: clean
|
||||
const uselessU = u(''); // TODO: clean
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd[]ef`))}</p>`,
|
||||
@@ -552,7 +566,7 @@ describe('Format', () => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('insert', 'C');
|
||||
},
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`A${u(`B`)}${uselessSpan(`C[]`)}${uselessU}`)}${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`A${u(`B`)}C[]${uselessU}`)}${u(s(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove only underline decoration on a span', async () => {
|
||||
@@ -639,12 +653,12 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(em(`cd`))}${em(u(`ghi[]`))}${u(em(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfterEdit: `<p>ab${u(em(`cd`))}${em(u(`ghi`) + `<span data-oe-zws-empty-inline="">[]\u200b</span>`)}${u(em(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(em(`cd`))}${em(u(`ghi`) + `[]`)}${u(em(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(em(`cd`))}${em(u(`ghi`))}<em data-oe-zws-empty-inline="">[]\u200b</em>${u(em(`ef`))}</p>`,
|
||||
// The reason the cursor is after the tag <s> is because when the editor get's cleaned, the zws tag gets deleted.
|
||||
contentAfter: `<p>ab${u(em(`cd`))}${em(u(`ghi`))}[]${u(em(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline, write, restore underline, write, remove underline again, write (collapsed, italic)', async () => {
|
||||
const uselessSpan = content => `<span>${content}</span>`;
|
||||
const uselessU = u(''); // TODO: clean
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(em(`cd[]ef`))}</p>`,
|
||||
@@ -656,7 +670,7 @@ describe('Format', () => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('insert', 'C');
|
||||
},
|
||||
contentAfter: `<p>ab${u(em(`cd`))}${em(`A${u(`B`)}${uselessSpan(`C[]`)}${uselessU}`)}${u(em(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(em(`cd`))}${em(`A${u(`B`)}C[]${uselessU}`)}${u(em(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -684,6 +698,53 @@ describe('Format', () => {
|
||||
contentAfter: '<p>ab[]cd</p>',
|
||||
});
|
||||
});
|
||||
it('should change the font-size for a character in an inline that has a font-size', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>a<span style="font-size: 10px;">b[c]d</span>e</p>`,
|
||||
stepFunction: setFontSize('20px'),
|
||||
contentAfter: unformat(`<p>
|
||||
a
|
||||
<span style="font-size: 10px;">b</span>
|
||||
<span style="font-size: 20px;">[c]</span>
|
||||
<span style="font-size: 10px;">d</span>
|
||||
e
|
||||
</p>`),
|
||||
});
|
||||
});
|
||||
it('should change the font-size of a character with multiples inline ancestors having a font-size', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: unformat(`<p>
|
||||
a
|
||||
<span style="font-size: 10px;">
|
||||
b
|
||||
<span style="font-size: 20px;">c[d]e</span>
|
||||
f
|
||||
</span>
|
||||
g
|
||||
</p>`),
|
||||
stepFunction: setFontSize('30px'),
|
||||
contentAfter: unformat(`<p>
|
||||
a
|
||||
<span style="font-size: 10px;">
|
||||
b
|
||||
<span style="font-size: 20px;">c</span>
|
||||
</span>
|
||||
<span style="font-size: 30px;">[d]</span>
|
||||
<span style="font-size: 10px;">
|
||||
<span style="font-size: 20px;">e</span>
|
||||
f
|
||||
</span>
|
||||
g
|
||||
</p>`),
|
||||
});
|
||||
});
|
||||
it('should remove a redundant font-size', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p style="font-size: 10px">b<span style="font-size: 10px;">[c]</span>d</p>',
|
||||
stepFunction: setFontSize('10px'),
|
||||
contentAfter: '<p style="font-size: 10px">b[c]d</p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should add style to a span parent of an inline', async () => {
|
||||
|
||||
Reference in New Issue
Block a user