[FIX] web_editor: properly clean zero-width spaces inserted for formats
When inserting a format with a collapsed selection, we need to insert a zero-width space. It should not be saved so we need to isolate it and put the "oe-zws-empty-inline" attribute on its parent so it gets cleaned up by the sanitizer. X-original-commit: 4ce274ba39cd8046b9ef4e0b99df66913375f993 Part-of: odoo/odoo#86930
This commit is contained in:
@@ -354,6 +354,19 @@ export function toggleFormat(editor, format) {
|
||||
}
|
||||
}
|
||||
if (wasCollapsed) {
|
||||
const siblings = [...zws.parentElement.childNodes];
|
||||
if (
|
||||
selectedTextNodes.includes(siblings[0]) &&
|
||||
selectedTextNodes.includes(siblings[siblings.length - 1])
|
||||
) {
|
||||
zws.parentElement.setAttribute('oe-zws-empty-inline', '');
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.setAttribute('oe-zws-empty-inline', '');
|
||||
zws.before(span);
|
||||
span.append(zws);
|
||||
|
||||
}
|
||||
setSelection(zws, 1);
|
||||
} else {
|
||||
setSelection(anchorNode, anchorOffset, focusNode, focusOffset);
|
||||
@@ -376,6 +389,21 @@ export function toggleFormat(editor, format) {
|
||||
} else {
|
||||
el.style[style.name] = style.value;
|
||||
}
|
||||
if (zws) {
|
||||
const siblings = [...zws.parentElement.childNodes];
|
||||
if (
|
||||
selectedTextNodes.includes(siblings[0]) &&
|
||||
selectedTextNodes.includes(siblings[siblings.length - 1])
|
||||
) {
|
||||
zws.parentElement.setAttribute('oe-zws-empty-inline', '');
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.setAttribute('oe-zws-empty-inline', '');
|
||||
zws.before(span);
|
||||
span.append(zws);
|
||||
|
||||
}
|
||||
}
|
||||
}, format, !isAlreadyFormatted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ const strikeThrough = async editor => {
|
||||
};
|
||||
|
||||
describe('Format', () => {
|
||||
const b = content => `<span style="font-weight: bolder;">${content}</span>`;
|
||||
const notB = (content, weight) => `<span style="font-weight: ${weight || 'normal'};">${content}</span>`;
|
||||
const b = (content, zws) => `<span${zws === 'first' ? ' oe-zws-empty-inline=""' : ''} style="font-weight: bolder;"${zws === 'last' ? ' oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
const notB = (content, weight, zws) => `<span${zws === 'first' ? ' oe-zws-empty-inline=""' : ''} style="font-weight: ${weight || 'normal'};"${zws === 'last' ? ' oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
describe('bold', () => {
|
||||
it('should make a few characters bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
@@ -86,19 +86,21 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: '<p>ab[]cd</p>',
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>ab${b(`[]\u200B`)}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${b(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to type in not bold', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${b(`ab[]cd`)}</p>`,
|
||||
stepFunction: bold,
|
||||
contentAfter: `<p>${b(`ab${notB(`[]\u200B`)}cd`)}</p>`,
|
||||
contentAfterEdit: `<p>${b(`ab${notB(`[]\u200B`, undefined, 'first')}cd`)}</p>`,
|
||||
contentAfter: `<p>${b(`ab[]cd`)}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
const i = content => `<span style="font-style: italic;">${content}</span>`;
|
||||
const notI = content => `<span style="font-style: normal;">${content}</span>`;
|
||||
const i = (content, zws) => `<span${zws === 'first' ? ' oe-zws-empty-inline=""' : ''} style="font-style: italic;"${zws === 'last' ? ' oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
const notI = (content, zws) => `<span${zws === 'first' ? ' oe-zws-empty-inline=""' : ''} style="font-style: normal;"${zws === 'last' ? ' oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
describe('italic', () => {
|
||||
it('should make a few characters italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
@@ -168,18 +170,20 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfter: `<p>ab${i(`[]\u200B`)}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${i(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to type in not italic', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${i(`ab[]cd`)}</p>`,
|
||||
stepFunction: italic,
|
||||
contentAfter: `<p>${i(`ab${notI(`[]\u200B`)}cd`)}</p>`,
|
||||
contentAfterEdit: `<p>${i(`ab${notI(`[]\u200B`, 'first')}cd`)}</p>`,
|
||||
contentAfter: `<p>${i(`ab[]cd`)}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
const u = content => `<span style="text-decoration-line: underline;">${content}</span>`;
|
||||
const u = (content, zws) => `<span${zws === 'first' ? ' oe-zws-empty-inline=""' : ''} style="text-decoration-line: underline;"${zws === 'last' ? ' oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
describe('underline', () => {
|
||||
it('should make a few characters underline', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
@@ -248,18 +252,20 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>ab${u(`[]\u200B`)}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${u(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to type in not underline', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${u(`ab[]cd`)}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>${u(`ab`)}\u200B[]${u(`cd`)}</p>`,
|
||||
contentAfterEdit: `<p>${u(`ab`)}<span oe-zws-empty-inline="">\u200B[]</span>${u(`cd`)}</p>`,
|
||||
contentAfter: `<p>${u(`ab`)}[]${u(`cd`)}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
const s = content => `<span style="text-decoration-line: line-through;">${content}</span>`;
|
||||
const s = (content, zws) => `<span${zws === 'first' ? ' oe-zws-empty-inline=""' : ''} style="text-decoration-line: line-through;"${zws === 'last' ? ' oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
describe('strikeThrough', () => {
|
||||
it('should make a few characters strikeThrough', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
@@ -328,14 +334,16 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
stepFunction: strikeThrough,
|
||||
contentAfter: `<p>ab${s(`[]\u200B`)}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${s(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to type in not strikeThrough', async () => {
|
||||
it('should get ready to type in not underline', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>${s(`ab[]cd`)}</p>`,
|
||||
stepFunction: strikeThrough,
|
||||
contentAfter: `<p>${s(`ab`)}\u200B[]${s(`cd`)}</p>`,
|
||||
contentAfterEdit: `<p>${s(`ab`)}<span oe-zws-empty-inline="">\u200B[]</span>${s(`cd`)}</p>`,
|
||||
contentAfter: `<p>${s(`ab`)}[]${s(`cd`)}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -344,32 +352,37 @@ describe('Format', () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd[]ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}${s(`\u200b[]`)}${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`\u200b[]`, 'last')}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}[]${u(s(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should restore underline after removing it (collapsed, strikeThrough)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd`))}${s(`\u200b[]`)}${u(s(`ef`))}</p>`,
|
||||
contentBefore: `<p>ab${u(s(`cd`))}${s(`\u200b[]`, 'first')}${u(s(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}${s(u(`[]\u200b`))}${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(u(`[]\u200b`, 'first'), 'first')}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}[]${u(s(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline after restoring it after removing it (collapsed, strikeThrough)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd`))}${s(u(`[]\u200b`))}${u(s(`ef`))}</p>`,
|
||||
contentBefore: `<p>ab${u(s(`cd`))}${s(u(`[]\u200b`, 'first'))}${u(s(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}${s(`\u200b[]`)}${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`\u200b[]`, 'last')}${u(s(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}[]${u(s(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline after restoring it and writing after removing it (collapsed, strikeThrough)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd`))}${s(u(`ghi[]`))}${u(s(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>ab${u(s(`cd`))}${s(u(`ghi`) + `\u200b[]`)}${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(u(`ghi`) + `<span oe-zws-empty-inline="">\u200b[]</span>`)}${u(s(`ef`))}</p>`,
|
||||
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 = u(''); // TODO: clean
|
||||
const uselessSpan = content => `<span>${content}</span>`; // TODO: clean
|
||||
const uselessU = u(''); // TODO: clean
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(s(`cd[]ef`))}</p>`,
|
||||
stepFunction: async editor => {
|
||||
@@ -380,12 +393,12 @@ describe('Format', () => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('insertText', 'C');
|
||||
},
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`A${u(`B`)}C[]${uselessSpan}`)}${u(s(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(s(`cd`))}${s(`A${u(`B`)}${uselessSpan(`C[]`)}${uselessU}`)}${u(s(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('underline + italic', () => {
|
||||
const iAndU = content => `<span style="font-style: italic; text-decoration-line: underline;">${content}</span>`;
|
||||
const iAndU = (content, zws) => `<span${zws === 'first' ? ' oe-zws-empty-inline=""' : ''} style="font-style: italic; text-decoration-line: underline;"${zws === 'last' ? ' oe-zws-empty-inline=""' : ''}>${content}</span>`;
|
||||
it('should get ready to write in italic and underline', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
@@ -393,7 +406,8 @@ describe('Format', () => {
|
||||
await editor.execCommand('italic');
|
||||
await editor.execCommand('underline');
|
||||
},
|
||||
contentAfter: `<p>ab${iAndU(`[]\u200B`)}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${iAndU(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to write in italic, after changing one\'s mind about underline', async () => {
|
||||
@@ -404,7 +418,8 @@ describe('Format', () => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('underline');
|
||||
},
|
||||
contentAfter: `<p>ab${i(`\u200B[]`)}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${i(`\u200B[]`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
@@ -413,7 +428,8 @@ describe('Format', () => {
|
||||
await editor.execCommand('italic');
|
||||
await editor.execCommand('underline');
|
||||
},
|
||||
contentAfter: `<p>ab${i(`\u200B[]`)}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${i(`\u200B[]`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab[]cd</p>`,
|
||||
@@ -422,39 +438,45 @@ describe('Format', () => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('italic');
|
||||
},
|
||||
contentAfter: `<p>ab${i(`[]\u200B`)}cd</p>`,
|
||||
contentAfterEdit: `<p>ab${i(`[]\u200B`, 'first')}cd</p>`,
|
||||
contentAfter: `<p>ab[]cd</p>`,
|
||||
});
|
||||
});
|
||||
it('should get ready to write in italic without underline (underline was first)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd[]ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}${i(`\u200b[]`)}${u(i(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(i(`cd`))}${i(`\u200b[]`, 'last')}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}[]${u(i(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should restore underline after removing it (collapsed, italic)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd`))}${i(`\u200b[]`)}${u(i(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}${iAndU(`[]\u200b`)}${u(i(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(i(`cd`))}${iAndU(`[]\u200b`, 'last')}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}[]${u(i(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline after restoring it after removing it (collapsed, italic)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd`))}${i(u(`[]\u200b`))}${u(i(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}${i(`\u200b[]`)}${u(i(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(i(`cd`))}${i(`\u200b[]`, 'last')}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}[]${u(i(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline after restoring it and writing after removing it (collapsed, italic)', async () => {
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd`))}${i(u(`ghi[]`))}${u(i(`ef`))}</p>`,
|
||||
stepFunction: underline,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}${i(u(`ghi`) + `\u200b[]`)}${u(i(`ef`))}</p>`,
|
||||
contentAfterEdit: `<p>ab${u(i(`cd`))}${i(u(`ghi`) + `<span oe-zws-empty-inline="">\u200b[]</span>`)}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}${i(u(`ghi`) + `[]`)}${u(i(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
it('should remove underline, write, restore underline, write, remove underline again, write (collapsed, italic)', async () => {
|
||||
const uselessSpan = u(''); // TODO: clean
|
||||
const uselessSpan = content => `<span>${content}</span>`;
|
||||
const uselessU = u(''); // TODO: clean
|
||||
await testEditor(BasicEditor, {
|
||||
contentBefore: `<p>ab${u(i(`cd[]ef`))}</p>`,
|
||||
stepFunction: async editor => {
|
||||
@@ -465,7 +487,7 @@ describe('Format', () => {
|
||||
await editor.execCommand('underline');
|
||||
await editor.execCommand('insertText', 'C');
|
||||
},
|
||||
contentAfterEdit: `<p>ab${u(i(`cd`))}${i(`A${u(`B`)}C[]${uselessSpan}`)}${u(i(`ef`))}</p>`,
|
||||
contentAfter: `<p>ab${u(i(`cd`))}${i(`A${u(`B`)}${uselessSpan(`C[]`)}${uselessU}`)}${u(i(`ef`))}</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user