[FIX] web_editor: fix paste style

Allow to paste font tag as it is used to style colors.
Allow style attribute on tag B, STRONG, I, S, U, FONT.

task-2980754

closes odoo/odoo#100262

X-original-commit: 0b907fe5cfb9a1f83da4079e30e46e6711ade797
Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
Nicolas Bayet
2022-09-15 19:21:25 +02:00
parent 33048c5dad
commit e851bbdb29
2 changed files with 25 additions and 22 deletions
@@ -119,7 +119,9 @@ export const CLIPBOARD_WHITELISTS = {
'I',
'B',
'U',
'S',
'EM',
'FONT',
'STRONG',
// Table
'TABLE',
@@ -158,7 +160,8 @@ export const CLIPBOARD_WHITELISTS = {
/^fa/,
],
attributes: ['class', 'href', 'src'],
spanStyle: {
styledTags: ['SPAN', 'B', 'STRONG', 'I', 'S', 'U', 'FONT'],
styles: {
'text-decoration': { defaultValues: ['', 'none'] },
'font-weight': { defaultValues: ['', '400'] },
'background-color': { defaultValues: ['', '#fff', '#ffffff', 'rgb(255, 255, 255)', 'rgba(255, 255, 255, 1)'] },
@@ -2795,18 +2798,18 @@ export class OdooEditor extends EventTarget {
// Remove all illegal attributes and classes from the node, then
// clean its children.
for (const attribute of [...node.attributes]) {
// we kee some style on span to be able to paste text styled in the editor
if (node.nodeName === 'SPAN' && attribute.name === 'style') {
const spanInlineStyles = attribute.value.split(';');
// Keep allowed styles on nodes with allowed tags.
if (CLIPBOARD_WHITELISTS.styledTags.includes(node.nodeName) && attribute.name === 'style') {
const spanInlineStyles = attribute.value.split(';').map(x => x.trim());
const allowedSpanInlineStyles = spanInlineStyles.filter(rawStyle => {
const [styleName, styleValue] = rawStyle.split(':');
const style = CLIPBOARD_WHITELISTS.spanStyle[styleName.trim()];
const style = CLIPBOARD_WHITELISTS.styles[styleName.trim()];
return style && !style.defaultValues.includes(styleValue.trim());
});
node.removeAttribute(attribute.name);
if (allowedSpanInlineStyles.length > 0) {
node.setAttribute(attribute.name, allowedSpanInlineStyles.join(';'));
} else {
} else if (['SPAN', 'FONT'].includes(node.tagName)) {
for (const unwrappedNode of unwrapContents(node)) {
this._cleanForPaste(unwrappedNode);
}
@@ -2842,7 +2845,7 @@ export class OdooEditor extends EventTarget {
okClass instanceof RegExp ? okClass.test(item) : okClass === item,
);
} else {
const allowedSpanStyles = Object.keys(CLIPBOARD_WHITELISTS.spanStyle).map(s => `span[style*="${s}"]`);
const allowedSpanStyles = Object.keys(CLIPBOARD_WHITELISTS.styles).map(s => `span[style*="${s}"]`);
return (
item.nodeType === Node.TEXT_NODE ||
(
@@ -28,7 +28,7 @@ describe('Copy and paste', () => {
it('should keep whitelisted Tags tag', async () => {
for (const node of CLIPBOARD_WHITELISTS.nodes) {
if (!['TABLE', 'THEAD', 'TH', 'TBODY', 'TR', 'TD', 'IMG', 'BR', 'LI', '.fa'].includes(node)) {
const isInline = ['I', 'B', 'U', 'EM', 'STRONG', 'IMG', 'BR', 'A'].includes(node)
const isInline = ['I', 'B', 'U', 'S', 'EM', 'STRONG', 'IMG', 'BR', 'A', 'FONT'].includes(node);
const html = isInline ? `a<${node.toLowerCase()}>b</${node.toLowerCase()}>c` : `a</p><${node.toLowerCase()}>b</${node.toLowerCase()}><p>c`;
await testEditor(BasicEditor, {
@@ -532,7 +532,7 @@ describe('Copy and paste', () => {
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>1<b>23</b>&nbsp;4[]abcd</p>',
contentAfter: '<p>1<b style="font-weight: bolder">23</b>&nbsp;4[]abcd</p>',
});
});
it('should paste a text in a p', async () => {
@@ -541,7 +541,7 @@ describe('Copy and paste', () => {
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>ab1<b>23</b>&nbsp;4[]cd</p>',
contentAfter: '<p>ab1<b style="font-weight: bolder">23</b>&nbsp;4[]cd</p>',
});
});
it('should paste a text in a span', async () => {
@@ -550,7 +550,7 @@ describe('Copy and paste', () => {
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>a<span>b1<b>23</b>&nbsp;4[]c</span>d</p>',
contentAfter: '<p>a<span>b1<b style="font-weight: bolder">23</b>&nbsp;4[]c</span>d</p>',
});
});
});
@@ -561,7 +561,7 @@ describe('Copy and paste', () => {
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>a1<b>23</b>&nbsp;4[]d</p>',
contentAfter: '<p>a1<b style="font-weight: bolder">23</b>&nbsp;4[]d</p>',
});
});
it('should paste a text in a span', async () => {
@@ -570,7 +570,7 @@ describe('Copy and paste', () => {
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>a<span>b1<b>23</b>&nbsp;4[]e</span>f</p>',
contentAfter: '<p>a<span>b1<b style="font-weight: bolder">23</b>&nbsp;4[]e</span>f</p>',
});
});
it('should paste a text when selection across two span', async () => {
@@ -579,14 +579,14 @@ describe('Copy and paste', () => {
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>a<span>b1<b>23</b>&nbsp;4[]e</span>f</p>',
contentAfter: '<p>a<span>b1<b style="font-weight: bolder">23</b>&nbsp;4[]e</span>f</p>',
});
await testEditor(BasicEditor, {
contentBefore: '<p>a<span>b[c</span>- -<span>d]e</span>f</p>',
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<p>a<span>b1<b>23</b>&nbsp;4[]e</span>f</p>',
contentAfter: '<p>a<span>b1<b style="font-weight: bolder">23</b>&nbsp;4[]e</span>f</p>',
});
});
it('should paste a text when selection across two p', async () => {
@@ -595,14 +595,14 @@ describe('Copy and paste', () => {
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>a<p>b1<b>23</b>&nbsp;4[]e</p>f</div>',
contentAfter: '<div>a<p>b1<b style="font-weight: bolder">23</b>&nbsp;4[]e</p>f</div>',
});
await testEditor(BasicEditor, {
contentBefore: '<div>a<p>b[c</p>- -<p>d]e</p>f</div>',
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>a<p>b1<b>23</b>&nbsp;4[]e</p>f</div>',
contentAfter: '<div>a<p>b1<b style="font-weight: bolder">23</b>&nbsp;4[]e</p>f</div>',
});
});
it('should paste a text when selection leave a span', async () => {
@@ -611,14 +611,14 @@ describe('Copy and paste', () => {
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>ab<span>c1<b>23</b>&nbsp;4[]</span>f</div>',
contentAfter: '<div>ab<span>c1<b style="font-weight: bolder">23</b>&nbsp;4[]</span>f</div>',
});
await testEditor(BasicEditor, {
contentBefore: '<div>a[b<span>c]d</span>ef</div>',
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>a1<b>23</b>&nbsp;4[]<span>d</span>ef</div>',
contentAfter: '<div>a1<b style="font-weight: bolder">23</b>&nbsp;4[]<span>d</span>ef</div>',
});
});
it('should paste a text when selection across two element', async () => {
@@ -627,21 +627,21 @@ describe('Copy and paste', () => {
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>1a<p>b1<b>23</b>&nbsp;4[]<span>e</span>f</p></div>',
contentAfter: '<div>1a<p>b1<b style="font-weight: bolder">23</b>&nbsp;4[]<span>e</span>f</p></div>',
});
await testEditor(BasicEditor, {
contentBefore: '<div>2a<span>b[c</span><p>d]e</p>f</div>',
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>2a<span>b1<b>23</b>&nbsp;4[]</span>e<br>f</div>',
contentAfter: '<div>2a<span>b1<b style="font-weight: bolder">23</b>&nbsp;4[]</span>e<br>f</div>',
});
await testEditor(BasicEditor, {
contentBefore: '<div>3a<p>b[c</p><p>d]e</p>f</div>',
stepFunction: async editor => {
await pasteHtml(editor, complexHtmlData);
},
contentAfter: '<div>3a<p>b1<b>23</b>&nbsp;4[]e</p>f</div>',
contentAfter: '<div>3a<p>b1<b style="font-weight: bolder">23</b>&nbsp;4[]e</p>f</div>',
});
});
});